Skip to content

Latest commit

 

History

History
1106 lines (950 loc) · 54.1 KB

File metadata and controls

1106 lines (950 loc) · 54.1 KB

Known Issues

Last updated: 2026-05-28 — Added two payment-related medium entries under Bindu Core after #563/#565 landed settle-first ordering: x402-settle-false-negative-silent-orphans (facilitator timeout vs Base confirmation race creates undetected orphan payments) and x402-no-auto-refund-for-orphan-payments (Bindu has no outbound-wallet path; refunding is manual ops work). Both share the EIP-3009 recovery metadata persisted by _settle_payment in bindu/server/workers/manifest_worker.py; the detection (reconciliation worker) and remediation (auto-refund) pieces are scoped out as separate follow-ups. Earlier: 2026-05-12 — Added skale-facilitator-cert-expired (low/ops) under Bindu Core after adding operator-extensible network support to X402Settings.extra_networks and the register_money_parser plumbing in applications.py. SKALE-aware facilitator facilitator.x402.fi has an expired TLS cert and is the only public endpoint that advertises SKALE chains in its /supported response; the shipped default settings include skale-europa so the structure is visible, but operators need to bring their own working facilitator URL to actually settle. See tests/integration/x402/test_skale_facilitator_supported.py for the opt-in smoke check. Earlier: 2026-05-12 — Four x402 payment-bypass entries removed (x402-middleware-fails-open-on-body-parse, x402-no-replay-prevention, x402-no-signature-verification, x402-balance-check-skipped-on-missing-contract-code). The middleware was rewritten against x402 SDK v2 (x402ResourceServer.verify_payment replaces the hand-rolled _validate_payment_manually) and a new Redis-backed nonce store (bindu/server/middleware/x402/nonce_store.py) enforces replay prevention before the facilitator round-trip. See core/2026-05-12-x402-v2-migration-hardening.md. Previously: 2026-04-26 — hydra-token-cache-revocation-lag removed. The Hydra middleware now skips its introspection cache for sensitive scopes (admin, agent:execute, payment:capture, key:rotate) and exposes invalidate_token_cache / revoke_token for in-process invalidation. See core/2026-04-26-hydra-token-cache-revocation-lag.md. 2026-04-20 — recipes/SSE/DID work on branch feat/gateway-recipes added 15 gateway entries (5 medium, 6 low, 5 nit) and narrowed two existing ones (tool-name-collisions-silentparse-agent-from-tool-greedy-mismatch, signature-verification-ok-when-unsignedsignature-verification-non-text-parts-unverified) to the residual sub-bugs after partial resolutions landed. did-document-endpoint-returns-raw-dict removed — see core/2026-04-19-did-document-endpoint-raw-dict.md.

This file is user-facing. It's the first thing a new contributor or operator reads to calibrate expectations: what Bindu doesn't do today, and what behaves in surprising ways. Every entry has a workaround where one exists.

How to read this file

  1. Scan the tables first. Each subsystem (Gateway, Bindu Core, SDKs, Frontend) starts with a one-line-per-issue table — slug, severity, short description. That's enough to decide whether an issue matters to your deployment.
  2. Read the story when it bites you. High-severity entries have a Scenario block telling you what the bug looks like from the outside. Start there when you suspect you're hitting one of these.
  3. Medium / low / nit entries stay terse — Summary / Workaround / Tracking — because they're either easy to understand or easy to work around.

Entries are REMOVED from this file as issues are fixed. If the fix teaches a generalizable lesson, a dated postmortem lands in the matching bugs/<subsystem>/ folder:

See bugs/README.md for the postmortem template.

Reporting: if you hit one of these and want to help, open a GitHub Issue referencing the slug (e.g. "Fixes context-window-hardcoded").


Table of contents

Subsystem High Medium Low Nit
Gateway 2 14 19 9
Bindu Core (Python) 0 8 3 0
SDKs (TypeScript)

Gateway

Quick index

Slug Severity One-line
context-window-hardcoded high Compaction threshold assumes a 200k-token context window
no-session-concurrency-guard high Two /plan calls on the same session tangle their histories
permission-rules-not-enforced-for-tool-calls medium Permission service exists but is never called for tool calls
parse-agent-from-tool-greedy-mismatch medium SSE agent field wrong when agent names contain underscores
agent-catalog-overwrite medium Each /plan wholesale-overwrites the session's agent catalog
signature-verification-non-text-parts-unverified medium (sec) DataPart / FilePart bypass signature verification
pinned-did-no-format-validation medium Any string is accepted as pinnedDID; no DID shape check
recipe-permission-ask-treated-as-allow medium permission: recipe: { "x": "ask" } silently allows
ctx-ask-not-wired-for-recipes medium Recipe permission hook is a no-op at runtime
supabase-error-surfaces-as-generic-sse-error medium Mid-stream DB failures emit untyped event: error
did-resolver-no-key-id-selection medium (sec) Resolver picks the first public key; breaks during rotation
list-messages-pagination-silent medium Sessions >1000 messages silently truncate oldest
tool-input-sent-as-textpart medium Skills expecting DataPart receive TextPart with stringified JSON
no-rate-limit-cors-body-size-limit medium No rate limit / CORS / body-size limit on the Hono app
prompt-injection-scrubbing-theater medium (sec) The regex scrubber offers false confidence
no-graceful-shutdown medium In-flight /plan streams drop mid-frame on close
assistant-message-lost-on-stream-error medium Mid-stream errors lose completed tool calls from history
json-schema-to-zod-incomplete medium Converter ignores enum / oneOf / pattern / etc.
compaction-dedupe-single-process-only low Dedupe is in-process; horizontal scaling would race
no-ttl-cleanup low session.ttlDays configured but no cleanup job runs
token-estimation-chars-div-4 low chars/4 wrong for code and CJK
did-resolver-no-stampede-protection low Simultaneous cache-miss fetches for the same DID
bearer-env-error-collapses-to-transport low Missing env var surfaces as a generic transport error
resume-race-duplicate-session low Concurrent first-time /plan for a session throws 500 on second
cancel-casing-not-retried low tasks/cancel sends only camelCase; leaks tasks on snake-case peers
health-endpoint-no-dependency-probe low GET /health doesn't probe dependencies
no-request-id-in-logs low No correlation ID for /plan requests
no-config-hot-reload low Config changes need a full restart
resolve-env-limited-to-simple-var low Env interpolation only matches bare $VAR
compaction-summary-injected-as-user-role low Summary injected as role: "user" — could confuse the LLM
revert-millisecond-ties-nondeterministic low Millisecond-tied created_at ordering is non-deterministic
revert-doesnt-cancel-remote-tasks low revertTo doesn't cancel still-running peer tasks
empty-agents-catalog-no-400 low /plan with empty agents[] runs and returns an LLM error
no-migration-rollback low Migrations forward-only — no paired down.sql
recipe-no-hot-reload low Recipe changes require a gateway restart
load-recipe-sse-frames-ambiguous low load_recipe and peer calls use the same SSE agent field
recipe-file-enumeration-no-truncation-signal low 10+ files per recipe silently truncated in tool output
faq-agent-did-name-mismatch low Fleet's faq_agent.py registers as bindu_docs_agent
fleet-env-can-desync-after-seed-rotation low Stale $*_DID env vars survive agent seed rotation
provider-openrouter-hardcoded low model field assumes OpenRouter prefix
tasks-recorded-is-dead-state nit Unused tasksRecorded field in the planner
map-finish-reason-pointless-ternary nit Conditional type that evaluates to any either way
db-effect-promise-swallows-errors nit Effect.promise silently swallows rejected promises
test-coverage-gaps nit Backlog list of missing test scenarios
accept-header-not-enforced-on-plan nit Accept: text/event-stream not required
openapi-sse-schemas-unused nit 13 redocly no-unused-components warnings on SSE schemas
no-planner-integration-test nit Request → SSE end-to-end test missing
no-health-handler-integration-test nit /health only has pure-helper unit tests
story-chapter5-missing-oauth-scope-explanation nit DID chapter assumes reader knows OAuth scopes

High

context-window-hardcoded

Severity: high

Scenario. You switch the gateway to GPT-4o-mini (128k context). After a few long turns, the next planner request sends >128k tokens to the provider and gets back a context_length_exceeded 400. You check the logs expecting to see compaction kick in. It didn't. The threshold is hardwired to 200k.

What's wrong. The compaction threshold in gateway/src/session/overflow.ts assumes a 200,000-token context window (Claude Opus 4.x). Smaller models (GPT-4o-mini 128k, Haiku 200k) or much larger ones (Gemini Flash 1M) make compaction fire at the wrong time — either too late (provider rejects for overflow before compaction runs) or too early (unnecessary summarization cost).

Workaround: Pass an explicit threshold.contextWindow matching your actual model when calling compactIfNeeded. For multi-model deployments this needs per-model configuration the gateway doesn't currently expose.

Status:Fixed. thresholdForModel() in gateway/src/session/overflow.ts now resolves the context window from a lookup table keyed on provider/modelId (Anthropic 4.x = 200k, GPT-4o/4o-mini = 128k, GPT-4.1 = ~1M, o3 = 200k). Unknown models fall back to 128k (conservative — triggers compaction earlier rather than letting the caller hit context_length_exceeded). Operators can still pass an explicit override via compactIfNeeded({threshold}) for exotic models. Threshold wiring goes through compaction.ts:compactIfNeeded which now takes the planner's actual model into account. Guarded by gateway/tests/session/overflow-threshold.test.ts (14 tests).

Tracking: (no issue yet)

no-session-concurrency-guard

Severity: high

Scenario. Alice has two browser tabs on the same session. Tab 1 sends /plan. Before it finishes, tab 2 sends another /plan for the same session. Both planners append to gateway_messages concurrently. The second LLM call sees the first call's half-written tool_use without its paired tool_result — the assistant message sequence is now broken. LLM hallucinates or errors. Worse: the session's history is now permanently in an inconsistent state on disk.

What's wrong. Two /plan requests sharing the same session_id both append to gateway_messages with no serialization. Their histories interleave and the LLM's tool_use / tool_result pairing silently tangles across sessions. The compaction dedupe fix (commit 0655ac1) prevents compaction races but does not serialize plan turns themselves.

Workaround: Clients should not run concurrent /plan requests against the same session. For genuine concurrent use (multiple tabs, etc.), use distinct session IDs and reconcile externally.

Status:Fixed. withSessionLock in gateway/src/planner/index.ts serializes runPlan per sessionID via a per-key promise chain (Map<SessionID, Promise<unknown>> scoped to the planner layer). The second /plan call awaits the first's tail before its body starts, so message-history writes can no longer interleave. Effect.ensuring releases the lock on success, failure, OR interruption — an aborted or crashed plan can't deadlock the next caller. Prior errors are swallowed by the chain so a single failure doesn't poison the session forever. Different session_ids are unaffected (separate map keys). Limitation mirrors the compaction dedupe: per-process state only — multi-process gateway deployments would still need a Postgres advisory lock or a gateway_sessions version column with optimistic-concurrency on the message insert (Phase 2). Guarded by gateway/tests/planner/session-lock.test.ts (4 tests: serialization, cross-session independence, failure recovery, tail cleanup).

Tracking: (no issue yet)

Medium

permission-rules-not-enforced-for-tool-calls

Severity: medium Summary: agents/planner.md declares permission: agent_call: ask, and the Permission service exists and evaluates wildcards correctly. But the planner's tool-execution path (gateway/src/session/prompt.ts) never calls Permission.Service.evaluate() before running a Bindu tool. The permission system is effectively dead code for tool calls today. Workaround: Control tool access via the agents[] catalog the caller sends in the /plan request — only include agents the caller is allowed to invoke. Tracking: (no issue yet)

parse-agent-from-tool-greedy-mismatch

Severity: medium Summary: The collision half of this bug is now rejected at plan-open time — findDuplicateToolIds in gateway/src/planner/index.ts returns a 400 when two (agent, skill) pairs normalize to the same tool id. What remains: parseAgentFromTool in gateway/src/api/plan-route.ts uses a non-greedy regex ^call_(.+?)_(.+)$, so an agent whose name contains an underscore (e.g. research_v2) emits SSE task.started events with agent=research, skill=v2_x instead of the intended agent=research_v2, skill=x. Workaround: Avoid underscores in agent names in the catalog you send on /plan. If you must, rely on agent_did / task_id for correlation, not the SSE agent field. Tracking: (no issue yet)

agent-catalog-overwrite

Severity: medium Summary: db.updateSessionCatalog wholesale-overwrites gateway_sessions.agent_catalog on every /plan. If the caller omits an agent from a subsequent turn's catalog (e.g. temporary unreachability, inventory churn), the gateway drops it from the session's recorded catalog even though the session's history references its prior tool calls. Also exposed to a concurrency race (see 2026-04-18-compaction-concurrent-races.md — same shape, different column). Workaround: Always send the full agent catalog on every turn, even if individual agents are temporarily unavailable. Tracking: (no issue yet)

signature-verification-non-text-parts-unverified

Severity: medium (security-adjacent) Summary: The envelope-ambiguity half of this bug is fixed<remote_content verified="..."> is now four-valued (yes | no | unsigned | unknown), so the planner LLM can tell a real cryptographic pass from "nothing was signed". What remains: verifyArtifact in gateway/src/bindu/identity/verify.ts only inspects text parts. file and data parts are never verified, so a peer moving payload into a DataPart bypasses signature checks entirely even when verifyDID: true is set. Workaround: Refuse peers that return data or file parts for responses that must be verified. The gateway does not enforce this constraint today. Tracking: (no issue yet)

did-resolver-no-key-id-selection

Severity: medium (security-adjacent) Summary: primaryPublicKeyBase58 in gateway/src/bindu/identity/resolve.ts picks the FIRST public key found in a peer's DID Document. If the peer publishes multiple keys — e.g. during a rotation window listing both old and new — the wrong key may be selected for verification, causing valid signatures to fail or the wrong key to be used altogether. The A2A DID spec allows explicit keyId selection in signature metadata; the gateway does not consult it. Workaround: For peers using DID verification, pin them to a specific DID via trust.pinnedDID and coordinate rotation windows out-of-band. Tracking: (no issue yet)

list-messages-pagination-silent

Severity: medium Summary: db.listMessages has a default limit of 1000 rows (gateway/src/db/index.ts). Very long sessions silently truncate to the most recent 1000 messages, skipping the oldest. The planner loads this into its history; the LLM sees a partial session that starts mid-stream. No error, no warning. Compaction can fire and accurately summarize what it sees, but the oldest messages were never in scope. Workaround: Trigger compaction early on sessions expected to grow large. Long-term, paginate via created_at cursor. Tracking: (no issue yet)

tool-input-sent-as-textpart

Severity: medium Summary: The planner's tool-execution path (gateway/src/planner/index.ts) serializes arguments with JSON.stringify(args) and sends them as a Bindu TextPart. Many deployed skills expect a structured DataPart ({kind:"data", data:{…}}) — especially skills whose server-side validator parses a typed input schema. Skills that only accept DataPart reject the TextPart outright; skills that try to parse the JSON string out of a text field behave unpredictably. Workaround: None client-side — the gateway always sends TextPart. Affected skills must accept either form on their server side until this is fixed. Tracking: (no issue yet)

no-rate-limit-cors-body-size-limit

Severity: medium Summary: The Hono app (gateway/src/server/index.ts) has no rate limit, no CORS policy, and no body-size limit. A single client can submit large /plan requests or fire many concurrent requests without any throttling; browser clients from different origins have no explicit policy. The absence of a body-size limit is a trivial DoS vector — a 500 MB JSON payload is accepted, parsed, and held in memory. Workaround: Deploy behind a reverse proxy (nginx, Cloudflare, API Gateway) that provides these controls. The gateway assumes it runs behind such a proxy today. Tracking: (no issue yet)

prompt-injection-scrubbing-theater

Severity: medium (security) Summary: wrapRemoteContent in gateway/src/planner/index.ts strips literal strings like "ignore previous" and "disregard earlier" from peer responses before handing them to the planner LLM. This is trivially bypassable with capitalization, Unicode homoglyphs, paraphrasing, JSON-encoding the instruction, or placing it in a non-text part (file, data — which aren't scrubbed at all). It offers a false sense of safety without actually blocking prompt injection; the current defense is worse than no defense because downstream code may assume it's doing something. Workaround: Do not rely on the scrubber for prompt-injection resistance. For untrusted peers, apply one or more of: (a) isolate peer output into an LLM sub-call with its own restricted system prompt that produces structured data, not free-form instructions; (b) use provider-side structured-output / tool-choice constraints to prevent the planner from obeying arbitrary peer instructions; (c) cap peer responses to a strict JSON schema server-side. Tracking: (no issue yet)

no-graceful-shutdown

Severity: medium Summary: The gateway's close() function (gateway/src/index.ts) calls httpServer.close() and runtime.dispose() back-to-back. In-flight /plan streams are dropped mid-frame — clients see a truncated SSE, and the assistant message may be partially written but not committed. No draining, no deadline, no 5xx return for requests in flight during a rolling restart. Workaround: Rely on the reverse proxy to drain connections before sending SIGTERM to the gateway, and run at least two gateway replicas so dropped connections can be retried against the other. Tracking: (no issue yet)

assistant-message-lost-on-stream-error

Severity: medium (data loss, billing) Summary: If the LLM stream errors mid-turn (gateway/src/session/prompt.ts), the Effect generator fails immediately. Tool calls that already completed (and were already billed via the Bindu agent) are lost from the assistant message — they never get persisted to gateway_messages. The audit row in gateway_tasks still exists (the tool call completed from the gateway's perspective), but the session-level history shows no record of the tool_use, so replay is inconsistent with the audit log. Workaround: None at the application level. Operators should correlate gateway_tasks with gateway_messages when investigating session gaps — don't trust the assistant-message view alone. Tracking: (no issue yet)

json-schema-to-zod-incomplete

Severity: medium Summary: jsonSchemaToZod in gateway/src/planner/index.ts converts an inbound skill's inputSchema to a Zod validator for the planner LLM. It handles type: string|number|integer|boolean|array| object but ignores every other keyword: enum, oneOf, anyOf, pattern, minLength/maxLength, minimum/maximum, additionalProperties, format, and more. The planner LLM therefore receives no signal about valid values; invalid input passes local validation and reaches the peer, failing late with a JSON-RPC error. Workaround: Skills should document their full input constraints in their human-readable description so the planner LLM picks them up from the prompt rather than the schema. Tracking: (no issue yet)

Low

compaction-dedupe-single-process-only

Severity: low (correct today, architectural ceiling) Summary: The fix for concurrent compaction races (commit 0655ac1 / 2026-04-18-compaction-concurrent-races.md) uses an in-process Map<SessionID, Promise>. A horizontally-scaled deployment of the gateway (multiple Node processes fronting one Supabase) could still race across processes. Single-process Phase 1 is correct. Workaround: Run a single gateway process. Horizontal scaling is a Phase 2 concern — when it lands, add a Postgres version column on gateway_sessions with optimistic-concurrency semantics in the compaction UPDATE, or wrap the whole compaction in a stored procedure. Tracking: (no issue yet)

no-ttl-cleanup

Severity: low Summary: Config declares gateway.session.ttlDays (default 30) but nothing actually cleans up old sessions. gateway_sessions, gateway_messages, and gateway_tasks grow unbounded. Workaround: Run a scheduled Supabase SQL job to delete rows older than your desired TTL. The gateway does not do this automatically. Tracking: (no issue yet)

token-estimation-chars-div-4

Severity: low Summary: approxTokens in gateway/src/session/overflow.ts uses chars / 4 as its token count heuristic. Accurate for English prose; wrong for code (more tokens per char due to punctuation), wildly wrong for CJK (closer to chars / 1.5). Combined with context-window-hardcoded, compaction timing for non-English sessions is unreliable. Workaround: Set a more conservative triggerFraction (e.g. 0.6 instead of 0.8) if your sessions are primarily code or CJK-language content. Tracking: (no issue yet)

did-resolver-no-stampede-protection

Severity: low Summary: The DID resolver in gateway/src/bindu/identity/resolve.ts caches DID Documents with a 5-minute TTL. When the cache expires (or is cold on first call), concurrent resolve() calls for the same DID all miss and issue simultaneous HTTP fetches to the peer's /did/resolve. Functionally harmless; wasteful. Workaround: None needed in practice — 5 minutes is long enough that stampedes are rare. If they become a problem, add a second-level in-flight dedupe (same pattern as compaction-dedupe). Tracking: (no issue yet)

bearer-env-error-collapses-to-transport

Severity: low Summary: When auth: { type: "bearer_env", envVar: "FOO" } is configured for a peer but $FOO is unset, authHeaders() throws from inside the async runCall in the Bindu client. The error is caught by the Effect machinery and wrapped as a generic transport error (BinduError.transport(...)), losing the "configuration missing" context. The operator can't tell "missing env" from "peer down." Workaround: Validate required env vars at gateway boot. Or tail the logs for "auth: env var … is not set" — the message is still present in the error string, just buried under "transport:". Tracking: (no issue yet)

resume-race-duplicate-session

Severity: low Summary: Two concurrent /plan requests with the same session_id where neither yet exists in gateway_sessions will both miss the getSession lookup and both call sessions.create(), producing two rows with the same external_session_id. The UNIQUE constraint on external_session_id will cause the second insert to fail — the second request errors back to the caller with a 500. Workaround: Retry the failing request. The first insert succeeded, so a retry resolves to the existing row. Tracking: (no issue yet)

cancel-casing-not-retried

Severity: low Summary: When sendAndPoll exhausts its poll budget (gateway/src/bindu/client/poll.ts), it issues a best-effort tasks/cancel to the peer. That cancel sends taskId in camelCase only — it never does the snake_case retry flip that the poll loop itself performs. For peers that require task_id in snake_case (the very case the flip exists for), the cancel silently fails and the remote task leaks. Workaround: None. Peers that require snake-case params for tasks/cancel need to support camelCase too, or the task will orphan on poll timeout. Tracking: (no issue yet)

health-endpoint-no-dependency-probe

Severity: low Summary: GET /health (gateway/src/server/index.ts) returns 200 {ok: true} regardless of whether Supabase is reachable or the configured LLM provider is accepting requests. Load balancers see "healthy" while the gateway is unable to serve /plan traffic. Good for liveness, useless for readiness. Workaround: Add a separate readiness check in your deployment that hits /plan with a no-op payload and a short deadline. Tracking: (no issue yet)

no-request-id-in-logs

Severity: low Summary: /plan handler produces no request ID, correlation ID, or tracing context. When an SSE stream errors and the client reports a problem, there's no way to correlate the client's observation with a server-side log line. Server-side logs don't include session ID either for most events. Workaround: Set X-Request-Id on the reverse proxy and include it in proxy access logs; correlate by timestamp and peer URL. Tracking: (no issue yet)

no-config-hot-reload

Severity: low Summary: Changes to agents/planner.md, gateway.config.json, or any permission rules require a full gateway restart. The config loader reads files once at boot via gateway/src/config/loader.ts. Workaround: Restart the gateway. For live-tuning the planner prompt, consider reading it dynamically from DB or a separate mutable source. Tracking: (no issue yet)

resolve-env-limited-to-simple-var

Severity: low Summary: resolveEnv in gateway/src/config/loader.ts only matches bare "$VAR" strings. It does not handle "${VAR}/suffix", default values ("${VAR:-default}"), nested interpolation, or shell-style expansion. A config value like "https://${HOST}/api" passes through as a literal string. Workaround: Precompute interpolated values in your env or config file; don't rely on shell-style expansion inside config strings. Tracking: (no issue yet)

compaction-summary-injected-as-user-role

Severity: low Summary: The compaction summary is injected back into history as a synthetic message with role: "user" (gateway/src/session/index.ts). Works, but the LLM may mistake the summary for the current user turn's message — especially when the summary starts with a phrase the model could interpret as a directive. A system-role injection (or explicit [SYSTEM: prior context] tagging) would be safer. Workaround: The prefix [Prior session context, compacted] already signals the nature of the content; in practice the planner handles it correctly. Watch for cases where the model echoes the summary verbatim as if it were a user question. Tracking: (no issue yet)

revert-millisecond-ties-nondeterministic

Severity: low Summary: revertTo and revertLastTurn in gateway/src/session/revert.ts use created_at as the boundary for "everything after this message." If multiple messages were inserted within the same millisecond (rare but possible under contention), their relative order is non-deterministic — revert may include or exclude some of them based on DB-internal ordering. Workaround: Inspect the reverted row set after a revert; manually unmark any mis-reverted rows. A Phase 2 fix would switch to a monotonically-increasing seq column. Tracking: (no issue yet)

revert-doesnt-cancel-remote-tasks

Severity: low Summary: revertTo marks local audit rows as reverted=true but does NOT send tasks/cancel to the peers for any still-running Bindu tasks in the reverted window. The remote tasks continue, consuming peer resources and (for paid skills) accruing cost until they finish on their own. This is documented as intentional in the code — peers have already done the work, and cancel semantics are complex — but may surprise operators. Workaround: Accept that revert only clears local state. Stragglers complete on the peer side and their audit rows stay marked reverted so they're hidden from resume. Tracking: (no issue yet)

empty-agents-catalog-no-400

Severity: low Summary: PlanRequest.agents has a default of [], so a /plan request with no agents is accepted. The planner runs with zero tools, the LLM attempts to call a tool it has no access to, and the response is an error message from the LLM — not a clear "you forgot to send agents" 400. Workaround: Always include at least one agent in the request, or pre-validate the agents.length client-side. Tracking: (no issue yet)

no-migration-rollback

Severity: low Summary: Migrations under gateway/migrations/ are forward-only — no paired down.sql for each change. Reverting a migration requires manual SQL work. Workaround: For a production deployment that may need rollback, maintain rollback scripts outside the migrations/ folder. This is a common choice for small projects; fix only if the team actively needs reversible migrations. Tracking: (no issue yet)

Nits

tasks-recorded-is-dead-state

Severity: nit Summary: tasksRecorded: string[] accumulated in the planner (gateway/src/planner/index.ts) is populated inside buildSkillTool but never returned via SSE, never persisted, and never read. Dead code. Workaround: None needed. Remove in a cleanup pass. Tracking: (no issue yet)

map-finish-reason-pointless-ternary

Severity: nit Summary: mapFinishReason in gateway/src/session/prompt.ts has a parameter type StreamEvent["type"] extends "finish" ? any : any which is always any. The conditional type adds no information. Workaround: None needed. Simplify in a cleanup pass. Tracking: (no issue yet)

db-effect-promise-swallows-errors

Severity: nit (correctness-adjacent) Summary: Two paths in gateway/src/db/index.ts use Effect.promise(...) which resolves even when the underlying promise rejects (it treats rejection as a defect, not an Effect error). Transient Supabase failures at those call sites may silently resolve without being surfaced to the caller. Workaround: None client-side. Audit Effect.promise call sites; prefer Effect.tryPromise with an explicit catch for any non-trivial operation. Tracking: (no issue yet)

test-coverage-gaps

Severity: nit Summary: The test suite does not cover: concurrent /plan requests end-to-end (only the pubsub filter is tested); compaction correctness on long multi-pass sessions (only the wrapper dedupe and summarizer prompt are tested); revert; SSE frame ordering under load; non-English payloads; sessions larger than the 1000-row pagination limit; missing bearer_env env vars; aborted requests propagating to the Bindu client; the snake_case flip on tasks/cancel. Workaround: None — this is an internal backlog item. Contributors tackling any of the fixable items above should add a test for it in the same PR. Tracking: (no issue yet)

pinned-did-no-format-validation

Severity: medium Summary: PeerAuthRequest.trust.pinnedDID in gateway/src/planner/index.ts is z.string().optional() with no shape check. A caller can send pinnedDID: "hello" or accidentally an un-interpolated template literal like "${RESEARCH_DID}", and the gateway will echo that string in every SSE agent_did frame and try (vainly) to resolve it for signature verification. Observed in practice when a Postman user copy-pasted a bash-style variable reference into the request body. Workaround: Validate DID shape client-side before sending. At minimum ensure the string starts with did:bindu: or did:key:. Tracking: (no issue yet)

recipe-permission-ask-treated-as-allow

Severity: medium Summary: Recipe.available filters the recipe list shown to an agent by excluding anything whose permission.recipe resolves to deny. The evaluator is three-valued (allow | deny | ask) — ask falls through and the recipe is shown AND loadable. With the ctx.ask hook unwired (see next entry), "ask" is silently identical to "allow". Workaround: Treat "ask" as if it were "allow" for recipes today. If you need to restrict, use "deny". Tracking: (no issue yet)

ctx-ask-not-wired-for-recipes

Severity: medium Summary: tool/recipe.ts calls ctx.ask({permission: "recipe", target: name}) before loading a recipe body, guarded by if (ctx.ask). The ctx.ask field is marked optional in ToolContext and wrapTool never sets it. So the gate is a permanent no-op — recipes load unconditionally regardless of agent permission config. This is a separate concern from permission-rules-not-enforced-for-tool-calls which targets the broader tool-call gate. Workaround: Don't rely on permission.recipe in agent configs for production access control. Wait for Phase-2 permission UI. Tracking: (no issue yet)

supabase-error-surfaces-as-generic-sse-error

Severity: medium Summary: plan-route.ts wraps runPlan in a catch that emits event: error with only {message: string}. Callers can't programmatically distinguish a Supabase outage from a peer crash from an LLM failure — all three look identical on the wire. Operators debugging prod incidents have to grep logs. Workaround: Tail gateway logs out of band. Client-side, treat any event: error as "retry with exponential backoff" regardless of cause. Tracking: (no issue yet)

recipe-no-hot-reload

Severity: low Summary: Recipe.layer reads gateway/recipes/ once at boot. Adding, editing, or deleting a recipe markdown file has no effect until the gateway process restarts. Authoring loop is Ctrl-C + npm run dev per change. Workaround: Script npm run dev to auto-restart on recipes/**/*.md changes using nodemon or tsx watch with a wider include. Or edit + restart; the recipe layer init is cheap. Tracking: (no issue yet)

load-recipe-sse-frames-ambiguous

Severity: low Summary: When the planner calls the internal load_recipe tool, the SSE stream emits task.started / task.artifact / task.finished frames with agent: "load_recipe" and agent_did: null. Consumers parsing task.* frames by peer correlation have no crisp way to tell this apart from a peer call that happens to have a null DID. The agent_did_source: null field helps (peer calls from unpinned observed-failed peers also have it null) but doesn't cleanly partition. Fix: add an explicit tool_kind: "peer" | "local" field on the task.* SSE frames. Workaround: Filter on agent === "load_recipe" client-side. Tracking: (no issue yet)

recipe-file-enumeration-no-truncation-signal

Severity: low Summary: The load_recipe tool returns a <recipe_files> block listing sibling files inside a bundled recipe's directory, capped at 10 entries. If a recipe directory has more than 10 files, the excess silently disappears from the list — the planner has no way to know it's seeing a sample, not the full set. Fix: include an explicit truncated: true marker in the tool result's metadata, and emit a console.warn at boot when a recipe directory exceeds the cap. Workaround: Keep bundled recipe directories under 10 files. Tracking: (no issue yet)

faq-agent-did-name-mismatch

Severity: low Summary: The fleet demo's examples/gateway_test_fleet/faq_agent.py registers its DID as bindu_docs_agent but the filename and the operator-facing port labels (3778) say faq_agent. First-time readers following docs/GATEWAY.md Chapter 3 see the mismatch in the SSE agent_did strings vs the catalog agent field and wonder if something is wrong. Python-side concern, no gateway code involved. Workaround: The mismatch is cosmetic — signature verification and routing both work. Ignore or pin faq_agent DID explicitly in the catalog. Tracking: (no issue yet)

fleet-env-can-desync-after-seed-rotation

Severity: low Summary: examples/gateway_test_fleet/start_fleet.sh writes fresh DIDs to .fleet.env on every run. But if an agent's seed rotates (rm -rf ~/.bindu, restart) and a user has an old shell with $RESEARCH_DID still sourced from a prior .fleet.env, their next /plan pins a stale DID. Signature verification fails with a cryptic mismatch error. Fix: have start_fleet.sh print a "re-source .fleet.env if you had one loaded previously" hint whenever any agent's DID differs from the previous run's cache. Workaround: Always source .fleet.env fresh after any fleet restart. Tracking: (no issue yet)

provider-openrouter-hardcoded

Severity: low Summary: gateway/src/provider/index.ts and downstream code assume the openrouter/ model prefix. Adding direct Anthropic or direct OpenAI support (without going through OpenRouter's proxy) is a code change, not config. Workaround: Use OpenRouter as the universal proxy; it supports every major provider. Only reach for this when you need direct provider features (e.g. Anthropic prompt caching, OpenAI fine-tuned model access) that OpenRouter doesn't proxy. Tracking: (no issue yet)

accept-header-not-enforced-on-plan

Severity: nit Summary: POST /plan returns text/event-stream regardless of whether the client sent Accept: text/event-stream. Clients that forget the header still get SSE, which is convenient but breaks strict content-negotiation semantics. Not currently documented as required in openapi.yaml either. Fix: either document the header as required and return 406 when absent, or keep the permissive behavior and document it. Currently we're in the worst middle ground. Workaround: None needed; current behavior is operationally fine. Consumers relying on strict 406 on wrong Accept won't get it. Tracking: (no issue yet)

openapi-sse-schemas-unused

Severity: nit Summary: redocly lint gateway/openapi.yaml reports 13 no-unused-components warnings on the SSEEvent_* schemas. OpenAPI 3.1 has no native SSE modeling, so those schemas sit as reference material rather than being $ref'd from a response body. The warnings are expected given the format's limitations, not indicative of drift. Fix options: (a) accept — pragmatic, 0 errors, just warnings; (b) use oneOf inside the text/event-stream response schema to enumerate each event shape (stretches OpenAPI); (c) publish a separate AsyncAPI 2.x/3.x spec for the SSE surface. Workaround: None needed; warnings don't break consumers. Tracking: (no issue yet)

no-planner-integration-test

Severity: nit Summary: The unit tests cover every Gateway module in isolation, but no single test walks a /plan request end-to-end with a mocked LLM provider. The recipes feature, signatures surfacing, and observed-DID resolution were all verified manually against the real stack and via targeted unit tests for their pure helpers. A regression in the cross-cutting glue (tool → Bus → SSE JSON) would be caught only at integration time. Fix: mock Provider.Service with a fake emitting a canned StreamEvent sequence; assert SSE output matches expectations. Workaround: None — treat this as internal backlog. Tracking: (no issue yet)

no-health-handler-integration-test

Severity: nit Summary: tests/api/health-route.test.ts covers only the pure helpers (splitModelId, deriveGatewayId, deriveAuthor). The handler's full response shape — version, planner-model nesting, runtime flags, uptime math — is verified by manual curl, not by a test that builds the layer graph and asserts the JSON. Drift between openapi.yaml's HealthResponse schema and the actual response would go unnoticed until someone hand-checks. Fix: build a minimal layer graph (mock Supabase) in a test, invoke the handler against a stub Hono context, assert the body matches the openapi schema. Workaround: Manual curl against a running gateway. Tracking: (no issue yet)

story-chapter5-missing-oauth-scope-explanation

Severity: nit Summary: docs/GATEWAY.md Chapter 5 sets BINDU_GATEWAY_HYDRA_SCOPE via env vars but never explains what OAuth scopes are or why agent:read + agent:write are the defaults. A reader walking the story linearly hits the config step without context. Fix: one-paragraph sidebar in Chapter 5 explaining "scopes are labels we ask Hydra to stamp on tokens; peers check them before accepting a message/send". Workaround: Cross-ref to gateway/README.md §DID signing which explains it. Tracking: (no issue yet)


Bindu Core (Python)

Quick index

Slug Severity One-line
authz-scope-check-behind-optional-flag medium (sec) Scope check is optional; flipping the flag removes all authz
cors-allow-credentials-with-user-origins medium (sec) Credentials + loose origins risk credentialed CORS
no-rate-limit-or-quota-per-caller medium No per-caller quota; single caller can exhaust resources
x402-settle-false-negative-silent-orphans medium Facilitator /settle times out, chain confirms anyway, payer debited but task failed
x402-no-auto-refund-for-orphan-payments medium Orphan payments need a manual USDC transfer; Bindu has no outbound-wallet path

Medium

authz-scope-check-behind-optional-flag

Severity: medium (security, authorization) Summary: The scope check in bindu/server/endpoints/a2a_protocol.py line 153 is wrapped in if app_settings.auth.require_permissions:. When the flag is falsy (common during bringup, demos, or debugging), the A2A endpoint accepts any authenticated token for any method — there is no authorization layer at all, only authentication. Authorization being a feature flag is a deployment landmine: an operator who turns it off to "unblock" something forgets to turn it back on and ships a scopeless service. Workaround: Always deploy with require_permissions: true and define per-method scopes in auth.permissions. Treat the flag as deprecated in your configuration and add a startup assertion that refuses to boot when the flag is false and auth is enabled. Tracking: (no issue yet)

cors-allow-credentials-with-user-origins

Severity: medium (security, CORS misconfig) Summary: bindu/server/applications.py lines 563–571 instantiate CORSMiddleware with allow_credentials=True, allow_methods=["*"], allow_headers=["*"], and allow_origins=cors_origins where cors_origins is an operator-supplied list. Starlette does reject the literal wildcard ["*"] with credentials, but an operator passing ["https://example.com", "null"], a reflected-origin scheme, or simply an over-broad list (every internal tool) still gets a credentialed cross-origin surface. There is no startup assertion that the supplied origins are compatible with allow_credentials=True. Workaround: Set cors_origins to an exhaustive, minimal list of known origins. Never include "null", "*", or a reflected-origin scheme. If possible, terminate CORS at a reverse proxy and leave cors_origins=None on the Bindu app. Tracking: (no issue yet)

no-rate-limit-or-quota-per-caller

Severity: medium (DoS) Summary: The A2A endpoint, the scheduler, and ManifestWorker all run without per-caller quotas or global concurrency caps. A single authenticated DID can fire message/send in a loop and exhaust the scheduler queue, storage writes, and memory (tasks are kept hot for fast lookup). Request-body size is also uncapped on the Bindu app (Starlette default, no explicit limit). Nothing in bindu/server/applications.py or bindu/server/endpoints/a2a_protocol.py imposes rate limits, per-caller task caps, or a worker-pool semaphore. Workaround: Deploy behind a reverse proxy (nginx, Cloudflare, API Gateway) that enforces request-rate and body-size limits per client IP or DID. Operators running Bindu directly on the public internet are currently exposed. The fix is per-DID quotas enforced at the TaskManager.send_message level plus an explicit body-size limit on the Starlette app. Tracking: (no issue yet) (shape-equivalent to the gateway's no-rate-limit-cors-body-size-limit entry)

x402-settle-false-negative-silent-orphans

Severity: medium (revenue / fairness)

Scenario. Carol pays your agent 1 USDC on Base mainnet. The network is congested — block confirmation runs ~25s. Your facilitator's /settle endpoint waits 10s and returns a timeout failure. Bindu (settle-first) sees the failure, marks the task failed, and refuses to run the LLM. ~15 seconds later, the chain confirms the transfer anyway. Carol's wallet shows the debit. Your agent's pay_to wallet shows the credit. Your task storage says Carol's request failed. Nobody told you.

What's wrong. Settle-first closes the LLM-cost half of #562 but introduces a quieter failure mode at the boundary between the facilitator's timeout (typically 5-10s) and Base's confirmation latency (2-28s under congestion). The facilitator can answer "no" while the chain ultimately answers "yes" — the result is an orphan payment with no matching payment-orphaned tag, because the worker's only signal was success=False from settle. Magnitude is operator-dependent; under quiet network conditions it's near-zero, but during congestion spikes (e.g. NFT mints, mainnet flash events) it's the dominant orphan source.

The fields needed to reconcile are persisted on every failed-settle task (_settle_payment in bindu/server/workers/manifest_worker.py extracts x402_nonce, x402_authorization, x402_network before attempting settle, so they land in metadata even on exception paths). What's missing is the periodic worker that uses them — queries the chain for AuthorizationUsed(from, nonce) on the USDC contract, and flips matching payment-failed tasks to payment-orphaned-reconciled once confirmation lands.

Workaround: Periodically scan task.metadata["x402.payment.status"] == "payment-failed" tasks older than ~5 minutes. For each, re-call the facilitator's /settle with the same payload (idempotent — already- settled nonces return success with the existing tx hash) OR query eth_getLogs on the USDC contract for AuthorizationUsed filtered to (from=x402_authorization.from, nonce=x402_nonce). If found, the on-chain transfer happened; refund or re-execute as the operator's policy dictates.

Tracking: (no issue yet) — pairs with x402-no-auto-refund-for-orphan-payments; reconciliation is the detection half, auto-refund is the remediation half.

x402-no-auto-refund-for-orphan-payments

Severity: medium (operator friction)

Summary: Bindu correctly identifies orphan payments today — _handle_task_failure in bindu/server/workers/manifest_worker.py tags task.metadata["x402.payment.status"] = "payment-orphaned" when manifest.run raises after a successful settle, and persists the full EIP-3009 fields. But it can't do anything about them. x402 has no native refund primitive (the protocol is one-shot and one-directional — payer signs, server settles, transfer is final), and Bindu's architecture has never managed an outbound wallet: pay_to_address has only ever been a config string, no private key, no Base RPC connection, no gas balance. Refunding an orphan today is an entirely out-of-band ops process — the operator opens their own wallet, reads x402_authorization.from / .value / x402_network from the task metadata, and sends a regular USDC transfer back.

Workaround: Manual USDC transfer(to, amount) from the agent's wallet. Fields are all in task.metadata:

Field Use
x402_authorization.from Recipient (the payer)
x402_authorization.value Amount in atomic units (1 USDC = 1_000_000)
x402_network Which chain (eip155:8453 = Base mainnet, eip155:84532 = Base Sepolia)
x402.payment.receipts[0].transaction Original payment tx hash, for audit linkage

To avoid double-refunding, record the refund tx hash back onto the task somehow (e.g. set task.metadata["x402.refund.tx_hash"] after sending) so a future scan can skip already-refunded orphans.

Tracking: (no issue yet) — scoped out in the #562/#565 work as "build when there's real volume to justify the custody surface." Pairs with x402-settle-false-negative-silent-orphans (detection without remediation is half-useful).


SDKs (TypeScript)

No entries yet. Add them when the TS SDK's review pass lands. New postmortems for fixed SDK bugs go in bugs/sdk/.