Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions BUGS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# BOTCHA — Active Issues Tracker

*Last updated: 2026-04-13 by Choco*
*Last updated: 2026-04-27 by Choco*

---

Expand All @@ -26,23 +26,35 @@ Full OIDC-A attestation endpoint — EAT tokens, agent grants, OAuth AS metadata
### PR #26 — A2A Agent Card (MERGED)
A2A trust oracle with agent cards. Merged.

### PR #41 — TAP UX Improvements (MERGED v0.24.0)
**Bugs fixed:** agents/me 404 fix, INVALID_TTL validation, time_remaining_seconds, ACTION_CATEGORY_MISMATCH hint, reputation alias route.

### Issue #37 / PR #40 — CJS Support (MERGED v0.24.0)
Dual ESM/CJS build via tsconfig.cjs.json + verify-cjs.cjs CI check.

---

## 🔄 IN PROGRESS

### PR #41 — TAP UX Improvements (open, needs BOTCHA verify + CI)
**URL:** https://github.com/dupe-com/botcha/pull/41
**Bugs fixed (all confirmed on live API 2026-04-13):**
1. `GET /v1/agents/me` → 404 (now resolves from Bearer token)
2. `ttl_seconds: -100` on `POST /v1/sessions/tap` → silently accepted (now 400 INVALID_TTL)
3. `GET /v1/sessions/:id/tap` returns `time_remaining` in ms (renamed to `time_remaining_seconds`, now integer seconds)
4. `ACTION_CATEGORY_MISMATCH` error gives no hint about valid actions (now includes `valid_actions` array)
5. `GET /v1/agents/:id/reputation` → 404 (alias route added, must come before generic `:id`)
### PR — Four UX/correctness bugs (2026-04-27 sprint, Choco)
**Branch:** `fix/token-validate-all-types`
**Bugs confirmed on live API:**

**Bug 1 (2026-04-20): `GET /v1/agents/me` rejects agent-identity tokens**
`verifyToken` called with `undefined` options (defaults to `botcha-verified` only). OAuth-refresh tokens are blocked on the one route designed specifically to help agents identify themselves.
- **Fix:** Pass `{ allowedTypes: ['botcha-verified', 'botcha-agent-identity'] }`

### Issue #37 — CJS Support
**URL:** https://github.com/dupe-com/botcha/issues/37
**PRs:** #39 (Copilot, uses tsup), #40 (chocothebot, uses tsc + tsconfig.cjs.json)
**Recommendation:** Merge PR #39 — more comprehensive, covers langchain + verify packages, uses tsup for better bundler compatibility. Supersedes #40.
**Bug 2 (2026-04-20): `GET /v1/agents/:id/reputation` → 400 MISSING_AGENT_ID**
Alias route registered with `:id` param but `getReputationRoute` reads `c.req.param('agent_id')` — always undefined via this alias.
- **Fix:** Try `c.req.param('agent_id') || c.req.param('id')` in handler

**Bug 3 (2026-04-20): `POST /v1/delegations` — string capabilities give misleading error**
Passing `["browse", "search"]` returns "Invalid capability action. Valid: browse…" — implying the value is wrong when the actual issue is the format (`{action: "browse"}` required).
- **Fix:** Normalize strings to `{action: string}` objects before validation; clearer error message

**Bug 4 (2026-04-27): `POST /v1/token/validate` rejects attestation and agent-identity tokens**
The public validation endpoint is documented as "verify any BOTCHA token" but calls `verifyToken` with `undefined` options — defaulting to `allowedTypes: ['botcha-verified']`. Any non-challenge token (agent-identity, attestation, ANS badge, VC) gets `{"valid": false, "error": "Invalid token type"}`.
- **Fix:** Export `ALL_BOTCHA_ACCESS_TOKEN_TYPES` constant from `auth.ts`; pass it as `allowedTypes` to the validate endpoint. Refresh tokens intentionally excluded.

---

Expand Down Expand Up @@ -73,6 +85,10 @@ A2A trust oracle with agent cards. Merged.

### 5. Delegation field naming inconsistency (docs vs API)
**Location:** `POST /v1/delegations`
**Issue:** Natural field names are `delegator_agent_id`/`delegate_agent_id` but API uses `grantor_id`/`grantee_id`. AI agents consistently use the wrong names (tested 2026-04-13).
**Issue:** Natural field names are `delegator_agent_id`/`delegate_agent_id` but API uses `grantor_id`/`grantee_id`. AI agents consistently use the wrong names (tested 2026-04-13 and 2026-04-27).
**Fix:** Accept both field names (alias) or update docs/OpenAPI to be clearer
**Priority:** 🟡 MINOR — docs confusion

### 6. No session listing endpoint
**Issue:** Agents cannot list their own active sessions (e.g., `GET /v1/sessions?agent_id=...`). Must manually track session IDs.
**Priority:** 🟡 MINOR — convenience feature, not a correctness bug
14 changes: 14 additions & 0 deletions packages/cloudflare-workers/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,20 @@ export async function verifyToken(
}
}

/**
* All known BOTCHA token types.
* Used by the public /v1/token/validate endpoint to accept any BOTCHA-issued token.
* Note: refresh tokens (botcha-refresh) are intentionally excluded — they are
* bearer credentials and should not be validated by third parties.
*/
export const ALL_BOTCHA_ACCESS_TOKEN_TYPES = [
'botcha-verified', // Standard challenge-pass token
'botcha-agent-identity', // Agent OAuth / refresh-flow identity token
'botcha-attestation', // TAP capability attestation token
'botcha-ans-badge', // ANS badge token
'botcha-vc', // W3C Verifiable Credential token
] as const;

/**
* Extract Bearer token from Authorization header
*/
Expand Down
14 changes: 10 additions & 4 deletions packages/cloudflare-workers/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
type KVNamespace,
} from './challenges';
import { SignJWT, jwtVerify } from 'jose';
import { generateToken, verifyToken, extractBearerToken, revokeToken, refreshAccessToken, getSigningPublicKeyJWK, type ES256SigningKeyJWK, type BotchaTokenPayload } from './auth';
import { generateToken, verifyToken, extractBearerToken, revokeToken, refreshAccessToken, getSigningPublicKeyJWK, ALL_BOTCHA_ACCESS_TOKEN_TYPES, type ES256SigningKeyJWK, type BotchaTokenPayload } from './auth';
import { checkRateLimit, getClientIP } from './rate-limit';
import { verifyBadge, generateBadgeSvg, generateBadgeHtml, createBadgeResponse } from './badge';
import streamRoutes from './routes/stream';
Expand Down Expand Up @@ -1631,7 +1631,12 @@ app.post('/v1/token/validate', async (c) => {
}

const validatePublicKey = getPublicKey(c.env);
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, validatePublicKey);
// Accept all BOTCHA access token types — this is a public validation endpoint.
// Refresh tokens (botcha-refresh) are excluded: they are bearer credentials
// and must never be exposed to third-party validators.
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, {
allowedTypes: [...ALL_BOTCHA_ACCESS_TOKEN_TYPES],
}, validatePublicKey);

if (!result.valid) {
return c.json({
Expand Down Expand Up @@ -2749,12 +2754,13 @@ app.get('/v1/agents/:id', async (c) => {
}, 401);
}
const publicKey = getPublicKey(c.env);
const verification = await verifyToken(bearerToken, c.env.JWT_SECRET, c.env, undefined, publicKey);
// Accept both challenge-verified tokens and agent-identity tokens (from OAuth refresh flow)
const verification = await verifyToken(bearerToken, c.env.JWT_SECRET, c.env, { allowedTypes: ['botcha-verified', 'botcha-agent-identity'] }, publicKey);
if (!verification.valid || !verification.payload?.agent_id) {
return c.json({
success: false,
error: 'UNAUTHORIZED',
message: 'Invalid or expired token — must be an agent-identity token to use /v1/agents/me',
message: 'Invalid or expired token — must be a botcha-verified or agent-identity token to use /v1/agents/me',
}, 401);
}
agent_id = verification.payload.agent_id;
Expand Down
12 changes: 9 additions & 3 deletions packages/cloudflare-workers/src/tap-delegation-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,27 @@ export async function createDelegationRoute(c: Context) {
}, 400);
}

// Normalize: accept plain strings like ["browse", "search"] as well as objects [{action:"browse"}]
const normalizedCapabilities = body.capabilities.map((cap: any) =>
typeof cap === 'string' ? { action: cap } : cap
);

// Validate capability actions
for (const cap of body.capabilities) {
for (const cap of normalizedCapabilities) {
if (!cap.action || !(TAP_VALID_ACTIONS as readonly string[]).includes(cap.action)) {
return c.json({
success: false,
error: 'INVALID_CAPABILITY',
message: `Invalid capability action. Valid: ${TAP_VALID_ACTIONS.join(', ')}`
message: `Invalid capability action "${cap.action}". Valid actions: ${TAP_VALID_ACTIONS.join(', ')}. ` +
`Capabilities must be objects like {"action":"browse"} or plain strings like "browse".`
}, 400);
}
}

const options: CreateDelegationOptions = {
grantor_id: body.grantor_id,
grantee_id: body.grantee_id,
capabilities: body.capabilities,
capabilities: normalizedCapabilities,
duration_seconds: body.duration_seconds,
max_depth: body.max_depth,
parent_delegation_id: body.parent_delegation_id,
Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare-workers/src/tap-reputation-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import {
*/
export async function getReputationRoute(c: Context) {
try {
const agentId = c.req.param('agent_id');
// Support both :agent_id (primary route) and :id (alias route /v1/agents/:id/reputation)
const agentId = c.req.param('agent_id') || c.req.param('id');
if (!agentId) {
return c.json({
success: false,
Expand Down
Loading
Loading