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
54 changes: 28 additions & 26 deletions BUGS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# BOTCHA — Active Issues Tracker

*Last updated: 2026-02-23 by Choco*
*Last updated: 2026-04-13 by Choco*

---

Expand All @@ -20,42 +20,38 @@ Closed/merged work is tracked in `CHANGELOG.md`. This file tracks only open issu
**Commit:** b6f0c98
**Fix:** Removed `requireDashboardAuth` from `GET /device`; device code is now the sole trust anchor (RFC 8628 §6.1)

---
### PR #28 — OIDC-A Attestation (MERGED)
Full OIDC-A attestation endpoint — EAT tokens, agent grants, OAuth AS metadata. Merged.

## 🔄 IN PROGRESS (PR #28 — open)
### PR #26 — A2A Agent Card (MERGED)
A2A trust oracle with agent cards. Merged.

### PR #28 — OIDC-A Attestation
**Landed on this branch:**
- ✅ `GET /v1/auth/agent-grant/:id/status` now requires bearer auth and enforces same-app ownership
- ✅ `POST /v1/auth/agent-grant/:id/resolve` now requires bearer auth and enforces same-app ownership
- ✅ `POST /v1/attestation/eat` now validates `ttl_seconds` as a positive finite number
- ✅ OIDC docs/metadata now use `/.well-known/jwks` (not `/v1/jwks`)
- ✅ Added focused OIDC-A tests (`tests/unit/agents/tap-oidca.test.ts`)
- ✅ Rebased with main and resolved route conflicts (`index.tsx`)
- ✅ OIDC-A routes documented in OpenAPI/static docs (`packages/cloudflare-workers/src/static.ts`)
---

**Open issue:**
- 🟡 Grant resolve policy is app-owner scoped; stricter enterprise admin model may still be needed
## 🔄 IN PROGRESS

**Remaining before merge:**
1. Decide on stricter admin policy for grant resolve (currently app-owner scoped)
2. Final PR review + merge
### 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`)

### TAP Route Test Stability (cross-branch)
- ✅ `tests/unit/agents/tap-routes.test.ts` now passes on current branch (`41/41`)
- ✅ Replaced `vi.mocked(...)` usage with Bun-compatible explicit mocks
- ✅ Added missing auth stubs in rotate-key tests
### 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.

---

## 🔮 TECHNICAL DEBT (post-merge, existing in main)

These were identified during TAP feature testing but deprioritized in favor of the 5 epics:
## 🔮 TECHNICAL DEBT (existing in main, deprioritized)

### 1. KV Read-Modify-Write Race Condition
**Location:** `last_verified_at` updates on TAP session creation
**Risk:** Two simultaneous requests updating agent metadata can silently lose one update
**Fix:** Implement compare-and-swap or use `put` with `putOptions.ifMatch` (Cloudflare KV doesn't support CAS natively — workaround: use Durable Objects or pessimistic locking)
**Fix:** Implement compare-and-swap or use Durable Objects for pessimistic locking
**Priority:** 🟠 MAJOR — affects correctness of reputation/trust tracking under load

### 2. RFC 9421 HTTP Message Signatures (Dead Code)
Expand All @@ -66,11 +62,17 @@ These were identified during TAP feature testing but deprioritized in favor of t

### 3. Payment/Invoice Flow Untested
**Endpoints:** `/v1/invoices/*`, Consumer/Payment Container verification
**Issue:** Requires `card_acceptor_id` to test — not available in our test environment
**Issue:** Requires `card_acceptor_id` to test — not available in test environment
**Priority:** 🟡 MINOR — feature exists, just untested

### 4. x402 X-Payment Header Path Hangs
**Location:** `GET /v1/x402/challenge` with `X-Payment` header
**Issue:** Well-formed fake payments pass structural validation and reach the nonce KV step — if KV is slow/unavailable this can hang
**Fix:** Add explicit timeout around `noncesKV.get()` calls; return 504 on timeout
**Priority:** 🟡 MINOR — degrades gracefully in practice

### 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).
**Fix:** Accept both field names (alias) or update docs/OpenAPI to be clearer
**Priority:** 🟡 MINOR — docs confusion
11 changes: 1 addition & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 33 additions & 6 deletions packages/cloudflare-workers/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2489,6 +2489,10 @@ app.post('/v1/agents/register/tap', registerTAPAgentRoute);
app.get('/v1/agents/tap', listTAPAgentsRoute);
app.get('/v1/agents/:id/tap', getTAPAgentRoute);

// Convenience alias — GET /v1/agents/:id/reputation → GET /v1/reputation/:agent_id
// Must be registered before the generic /v1/agents/:id handler (Hono first-match routing).
app.get('/v1/agents/:id/reputation', getReputationRoute);

// Agent identity auth — prove you are a specific registered agent
app.post('/v1/agents/auth', handleAgentAuthChallenge);
app.post('/v1/agents/auth/verify', handleAgentAuthVerify);
Expand Down Expand Up @@ -2720,29 +2724,52 @@ app.post('/v1/agents/register', async (c) => {
}
});

// Get agent by ID
// Get agent by ID — supports "me" as a shorthand for the authenticated agent
app.get('/v1/agents/:id', async (c) => {
try {
const agent_id = c.req.param('id');
let agent_id = c.req.param('id');

if (!agent_id) {
return c.json({
success: false,
error: 'MISSING_AGENT_ID',
message: 'Agent ID is required',
}, 400);
}


// Support "me" as a shorthand for the currently authenticated agent
if (agent_id === 'me') {
const authHeader = c.req.header('authorization');
const bearerToken = extractBearerToken(authHeader);
if (!bearerToken) {
return c.json({
success: false,
error: 'UNAUTHORIZED',
message: 'Authorization: Bearer <token> is required to use /v1/agents/me',
}, 401);
}
const publicKey = getPublicKey(c.env);
const verification = await verifyToken(bearerToken, c.env.JWT_SECRET, c.env, undefined, 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',
}, 401);
}
agent_id = verification.payload.agent_id;
}

const agent = await getAgent(c.env.AGENTS, agent_id);

if (!agent) {
return c.json({
success: false,
error: 'AGENT_NOT_FOUND',
message: `No agent found with ID: ${agent_id}`,
}, 404);
}

return c.json({
success: true,
agent_id: agent.agent_id,
Expand Down
31 changes: 18 additions & 13 deletions packages/cloudflare-workers/src/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ curl https://botcha.ai/agent-only -H "Authorization: Bearer <token>"
| Method | Path | Description |
|--------|------|-------------|
| \`POST\` | \`/v1/agents/register\` | Register agent identity (name, operator, version) |
| \`GET\` | \`/v1/agents/:id\` | Get agent by ID (public, no auth) |
| \`GET\` | \`/v1/agents/:id\` | Get agent by ID (public, no auth); use \`me\` with Bearer token to resolve current agent |
| \`GET\` | \`/v1/agents\` | List all agents for your app (auth required) |

### Webhooks (v0.22.0)
Expand Down Expand Up @@ -244,7 +244,7 @@ curl https://botcha.ai/agent-only/x402 \

| Method | Path | Description |
|--------|------|-------------|
| \`GET\` | \`/v1/reputation/:agent_id\` | Get agent reputation score |
| \`GET\` | \`/v1/reputation/:agent_id\` | Get agent reputation score (alias: \`GET /v1/agents/:id/reputation\`) |
| \`POST\` | \`/v1/reputation/events\` | Record a reputation event |
| \`GET\` | \`/v1/reputation/:agent_id/events\` | List reputation events |
| \`POST\` | \`/v1/reputation/:agent_id/reset\` | Reset reputation (admin) |
Expand Down Expand Up @@ -532,7 +532,7 @@ Endpoint: POST https://botcha.ai/gate - Submit code form, redirects to /go/:code

# Agent Registry Endpoints (app_id required)
Endpoint: POST https://botcha.ai/v1/agents/register - Register agent identity — requires app_id
Endpoint: GET https://botcha.ai/v1/agents/:id - Get agent by ID (public, no auth) — requires app_id
Endpoint: GET https://botcha.ai/v1/agents/:id - Get agent by ID (public, no auth) — requires app_id; use "me" as the ID with a Bearer token to fetch the currently authenticated agent
Endpoint: GET https://botcha.ai/v1/agents - List all agents for authenticated app — requires app_id
Endpoint: DELETE https://botcha.ai/v1/agents/:id - Delete agent — requires dashboard session

Expand Down Expand Up @@ -599,6 +599,8 @@ Endpoint: POST https://botcha.ai/v1/verify/attestation - Verify attestation toke
# Agent Reputation Scoring (v0.18.0) (app_id required)
Endpoint: GET https://botcha.ai/v1/reputation/:agent_id - Get agent reputation score (0-1000, 5 tiers) — requires app_id
Endpoint: POST https://botcha.ai/v1/reputation/events - Record a reputation event (18 action types, 6 categories) — requires app_id
Reputation-Event-Categories: verification: challenge_solved|challenge_failed|auth_success|auth_failure; attestation: attestation_issued|attestation_verified|attestation_revoked; delegation: delegation_granted|delegation_received|delegation_revoked; session: session_created|session_expired|session_terminated; violation: rate_limit_exceeded|invalid_token|abuse_detected; endorsement: endorsement_received|endorsement_given
Reputation-Alias: GET /v1/agents/:id/reputation is an alias for GET /v1/reputation/:agent_id
Endpoint: GET https://botcha.ai/v1/reputation/:agent_id/events - List reputation events (?category=&limit=) — requires app_id
Endpoint: POST https://botcha.ai/v1/reputation/:agent_id/reset - Reset reputation to default (admin action) — requires app_id

Expand Down Expand Up @@ -750,8 +752,8 @@ TAP-Register: POST /v1/agents/register/tap with {name, public_key, signature_alg
TAP-Algorithms: ed25519 (Visa recommended), ecdsa-p256-sha256, rsa-pss-sha256
TAP-Trust-Levels: basic, verified, enterprise
TAP-Capabilities: Array of {action, resource, constraints} — scoped access control
TAP-Session-Create: POST /v1/sessions/tap with {agent_id, user_context, intent}
TAP-Session-Get: GET /v1/sessions/:id/tap — includes time_remaining
TAP-Session-Create: POST /v1/sessions/tap with {agent_id, user_context, intent, ttl_seconds?} — ttl_seconds must be a positive integer (max 86400); defaults to 3600 if omitted
TAP-Session-Get: GET /v1/sessions/:id/tap — includes time_remaining_seconds (integer, seconds until expiry)
TAP-Get-Agent: GET /v1/agents/:id/tap — includes public_key for verification
TAP-List-Agents: GET /v1/agents/tap?app_id=...&tap_only=true
TAP-Middleware-Modes: tap, signature-only, challenge-only, flexible
Expand Down Expand Up @@ -1656,15 +1658,15 @@ export function getOpenApiSpec(version: string) {
"/v1/agents/{id}": {
get: {
summary: "Get agent by ID",
description: "Retrieve agent information by agent ID. Public endpoint, no authentication required.",
description: "Retrieve agent information by agent ID. Public endpoint, no authentication required. Use \"me\" as the id with a Bearer token (Authorization: Bearer <access_token>) to retrieve the currently authenticated agent without knowing your own agent_id.",
operationId: "getAgent",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
description: "The agent_id to retrieve (e.g., 'agent_abc123')"
description: "The agent_id to retrieve (e.g., 'agent_abc123'), or the special value 'me' to resolve from the Bearer token"
}
],
responses: {
Expand All @@ -1676,6 +1678,7 @@ export function getOpenApiSpec(version: string) {
}
}
},
"401": { description: "Unauthorized — only returned when using 'me' without a valid Bearer token" },
"404": { description: "Agent not found" }
}
}
Expand Down Expand Up @@ -2265,22 +2268,24 @@ export function getOpenApiSpec(version: string) {
"user_context": { type: "string", description: "User context identifier" },
"intent": {
type: "object",
required: ["action"],
properties: {
"action": { type: "string", description: "Intended action (e.g., read, write)" },
"action": { type: "string", enum: ["browse", "compare", "purchase", "audit", "search"], description: "Intended action" },
"resource": { type: "string", description: "Target resource path" },
"purpose": { type: "string", description: "Human-readable purpose" }
"scope": { type: "array", items: { type: "string" }, description: "Optional scope constraints" }
},
description: "Declared intent for the session"
}
},
"ttl_seconds": { type: "integer", minimum: 1, maximum: 86400, description: "Session TTL in seconds (default: 3600, max: 86400). Must be a positive integer." }
}
}
}
}
},
responses: {
"201": { description: "TAP session created with capabilities and expiry" },
"400": { description: "Missing required fields or invalid intent" },
"403": { description: "Agent lacks required capability for declared intent" },
"400": { description: "Missing required fields, invalid intent, or invalid ttl_seconds" },
"403": { description: "Agent lacks required capability for declared intent or TAP not enabled" },
"404": { description: "Agent not found" }
}
}
Expand Down Expand Up @@ -2810,7 +2815,7 @@ export function getOpenApiSpec(version: string) {
properties: {
"agent_id": { type: "string", description: "Agent to record event for" },
"category": { type: "string", enum: ["verification", "attestation", "delegation", "session", "violation", "endorsement"], description: "Event category" },
"action": { type: "string", description: "Event action (e.g. challenge_solved, abuse_detected)" },
"action": { type: "string", enum: ["challenge_solved", "challenge_failed", "auth_success", "auth_failure", "attestation_issued", "attestation_verified", "attestation_revoked", "delegation_granted", "delegation_received", "delegation_revoked", "session_created", "session_expired", "session_terminated", "rate_limit_exceeded", "invalid_token", "abuse_detected", "endorsement_received", "endorsement_given"], description: "Event action — must match the selected category. verification: challenge_solved|challenge_failed|auth_success|auth_failure; attestation: attestation_issued|attestation_verified|attestation_revoked; delegation: delegation_granted|delegation_received|delegation_revoked; session: session_created|session_expired|session_terminated; violation: rate_limit_exceeded|invalid_token|abuse_detected; endorsement: endorsement_received|endorsement_given" },
"source_agent_id": { type: "string", description: "Source agent for endorsements" },
"metadata": { type: "object", additionalProperties: { type: "string" }, description: "Optional key/value metadata" }
}
Expand Down
5 changes: 4 additions & 1 deletion packages/cloudflare-workers/src/tap-reputation-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isValidCategory,
isValidAction,
isValidCategoryAction,
CATEGORY_ACTIONS,
type RecordEventOptions,
type ReputationEventCategory,
type ReputationEventAction,
Expand Down Expand Up @@ -161,10 +162,12 @@ export async function recordReputationEventRoute(c: Context) {
}

if (!isValidCategoryAction(body.category, body.action)) {
const validActionsForCategory = CATEGORY_ACTIONS[body.category as ReputationEventCategory] ?? [];
return c.json({
success: false,
error: 'ACTION_CATEGORY_MISMATCH',
message: `Action "${body.action}" does not belong to category "${body.category}"`
message: `Action "${body.action}" does not belong to category "${body.category}". Valid actions for "${body.category}": ${validActionsForCategory.join(', ')}`,
valid_actions: validActionsForCategory,
}, 400);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cloudflare-workers/src/tap-reputation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ export async function resetReputation(

// ============ VALIDATION ============

const CATEGORY_ACTIONS: Record<ReputationEventCategory, ReputationEventAction[]> = {
export const CATEGORY_ACTIONS: Record<ReputationEventCategory, ReputationEventAction[]> = {
verification: ['challenge_solved', 'challenge_failed', 'auth_success', 'auth_failure'],
attestation: ['attestation_issued', 'attestation_verified', 'attestation_revoked'],
delegation: ['delegation_granted', 'delegation_received', 'delegation_revoked'],
Expand Down
Loading
Loading