Skip to content

Weekly tech debt audit: dispatch - 2026-06-17 #408

@itsmiso-ai

Description

@itsmiso-ai

Weekly tech debt audit: dispatch - 2026-06-17

Summary / Overall Risk Level

Overall risk: P1 / medium. The 2026-06-03 audit's 10 children are all closed, dependency state is clean (npm audit --omit=dev reports 0 vulnerabilities), CI is green (1308/1308 tests pass, lint + typecheck clean), and the new harness-agnostic agent task contract is well-tested.

However, since the last audit Dispatch absorbed a large new surface area (5 new endpoints, 4 new lib modules) and that surface has accumulated debt around authentication, persistence, and documentation drift. The most concerning issues are an unauthenticated mutating endpoint (/api/agents/[agentName]/tasks/report) and an unauthenticated queue-leaking endpoint (/api/agents/[agentName]/next-task). The .npmrc fix from issue #315 is also still emitting a deprecation warning.

No code changes or PRs were made during this audit. All 10 child issues from the 2026-06-03 umbrella (#308#311-#320) are closed. The 11 open umbrella children (#381#388, #392, #399#401) for the "configurable lanes" and "harness-agnostic agent control plane" epics are not in this audit's scope.

Top Findings

P1 — Unauthenticated mutating endpoint: POST /api/agents/[agentName]/tasks/report

A new mutating route was added since the last audit that accepts any anonymous POST, validates the body, and returns 200 { ok: true, report } — but does not persist anything to the database. The endpoint is named "report" but is a no-op echo. It has no authorizeRequest() call, no getAuthMode() awareness, and no test asserting that bad auth is rejected (because it cannot be — it accepts everything). A malicious actor can spam it for log poisoning, body-shape probing, or to confuse downstream consumers who think a reported task is real.

Evidence:

  • src/app/api/agents/[agentName]/tasks/report/route.ts defines POST without authorizeRequest (only Request parameter).
  • Route summary: auth=no verbs=POST(.
  • The test file src/app/api/agents/[agentName]/tasks/report/route.test.ts asserts only validation, not auth: 12 test cases, zero 401 paths.
  • Body shape: { taskType, outcome, repoFullName?, issueNumber?, pullRequestNumber?, pullRequestUrl?, summary?, error? } — echoes back via TaskReportBody.
  • Comment in code says "validated" and "report" but no prisma.auditLog.create / prisma.agentRun.create call exists.

P1 — Unauthenticated queue/data leak: GET /api/agents/[agentName]/next-task

The new next-task endpoint is the most powerful read endpoint in the system: given an agentName, it returns the next implement/followup-pr/groom task, the lane, and the full task contract. It is unauthenticated, like the older read endpoints (queue, active-work, work-summary), but it consolidates more decision-critical data into a single response than the older endpoints do. The agent queue and lease identifiers are exposed, which is useful reconnaissance for any actor probing the system.

Evidence:

  • src/app/api/agents/[agentName]/next-task/route.ts:1-30: no authorizeRequest import; route summary auth=no verbs=GET(.
  • The route file is 229 lines; reads from prisma.issue.findMany (open issues from enabled repos), listQueuedPrFixItems (PR fix queue), and findLeasedIssueIds (other agents' leases).
  • The test file next-task/route.test.ts has 0 cases for 401/unauthorized — confirms the design intent (intentionally public) but does not document that intent anywhere.

P1 — .npmrc "fix" from #315 emits a deprecation warning and is not the canonical solution

The previous audit (#315) added .npmrc with omit= to force-include devDependencies. The current npm CLI rejects this value as invalid: "npm warn invalid config omit="" set in .npmrc" on every install/typecheck/test/lint. The intended behaviour (force-install devDependencies) is actually achieved (because npm's default is to include dev unless omit=dev is set), so this is currently working by accident — but the warning is noisy in CI logs and the value is on the npm deprecation path. The canonical fix is include=dev (added in npm v7) or simply deleting the .npmrc.

Evidence:

  • cat .npmrc shows omit= on a single line.
  • npm install, npm run typecheck, npm run test, npm run lint, and npm audit --omit=dev all print:
    npm warn invalid config omit="" set in /data/git/mispospace/dispatch/.npmrc
    npm warn invalid config Must be one or more of: dev, optional, peer
    
  • Confirmed locally: replacing omit= with include=dev produces no warning and gives the same install behavior.
  • The fix was originally omit= → omit=dev per Restore reproducible local validation #315's resolution text in the previous audit, but the actual commit d002ee1 shipped omit=.

P1 — AGENTS.md does not document the four new agent-facing endpoints

The "OpenClaw Agent Workflow Contract" section of AGENTS.md documents POST /api/sync, POST /api/agent-runs, GET /api/issues, and GET /api/agents/[agentName]/queue, but does not mention:

  • GET /api/agents/[agentName]/next-task (the canonical entry point for new harnesses)
  • POST /api/agents/[agentName]/tasks/report (where harnesses report task outcomes)
  • GET /api/agents/[agentName]/work-summary (lane-aware work counts)
  • GET /api/agents/[agentName]/active-work (resume context)
  • GET /api/agents/[agentName]/queue is documented but the AgentTask contract from src/lib/agent-task.ts is not.

This drift is exactly what the previous audit flagged (#317 "Reconcile maintainer docs with implemented lane routes"), but the next wave of endpoint additions was not picked up by the same fix.

Evidence:

  • grep -n "/api/agents" AGENTS.md returns only queue/heartbeat/lane references.
  • src/lib/agent-task.ts (200+ lines) is not referenced anywhere in AGENTS.md or README.md.
  • docs/worker-execution-contract.md references getQueuedPrFixItems indirectly but does not show the next-task contract shape.

P2 — tasks/report is a stub: no persistence, no AgentRun row, no AuditLog row

Beyond the auth concern above, the endpoint is functionally incomplete. Other "report" surfaces in Dispatch (POST /api/agent-runs, POST /api/audit) persist data. This one returns 200 { ok: true, report } and discards the body. If a worker calls it, the operator gets no signal that work happened. The test file confirms persistence is not part of the contract.

Evidence:

  • src/app/api/agents/[agentName]/tasks/report/route.ts:80-100 returns NextResponse.json({ ok: true, agentName, report }) with no await prisma.* call.
  • The test file mocks prisma.issue.update and prisma.prFixQueueItem.update and prisma.lease.delete but never invokes any of them.
  • Compare with src/app/api/agents/[agentName]/heartbeat/route.ts:101-110 which calls prisma.agentRun.create.

P2 — next-task and queue duplicate 80%+ of their query logic and could be unified

The two routes share issue-fetching, lease-filtering, PR-fix-queue-fetching, and queue-building logic. next-task then takes the first item and wraps it in an AgentTask. As the lane configuration work lands (#381-#388), the two will need to be updated in lockstep. A shared helper that returns { rankedQueue, prFixItems } would let next-task and queue evolve together.

Evidence:

  • src/app/api/agents/[agentName]/queue/route.ts:14-87 and src/app/api/agents/[agentName]/next-task/route.ts:90-150 are structurally similar.
  • Both call prisma.issue.findMany({ where: { state: "open", repository: { enabled: true } }, select: { ... linkedPrHealth ... } }) with the same select shape.
  • Both call findLeasedIssueIds(agentName) and listQueuedPrFixItems(asPrFixQueueClient(prisma), { lane }).

P2 — lane-config.ts is added but not yet consumed by the rest of the system

src/lib/lane-config.ts was added in PR #391 as the foundation for configurable lanes, but a grep -RIn "lane-config" src/ shows that only its own test file imports from it. The hardcoded "normal" | "escalated" | "backlog" literals in src/lib/issue-reconciliation.ts:14, src/lib/issue-lane.ts:8, and src/types/index.ts:VALID_LANES are unchanged. This means the new module is dead code until #383 ("refactor: replace hardcoded lane constants with lane helpers") lands.

Evidence:

P2 — Route coverage gap: 17 of 48 route.ts files have no co-located test

Roughly a third of API routes (mostly older ones) have no co-located route.test.ts. Several of the unauthenticated list/read routes (/api/audit, /api/repos, /api/automation/events, /api/automation/workflows, /api/issues/untriaged) leak data without auth and would benefit from regression coverage. The PR-fix-queue and PR-followup ingestion routes have route-level coverage gaps too.

Evidence:

  • find src/app/api -name 'route.test.ts' | wc -l → 31
  • find src/app/api -name 'route.ts' | wc -l → 48
  • Routes without co-located tests (selected):
    • /api/audit/route.ts (unauthenticated, returns all AuditLog rows)
    • /api/repos/route.ts (unauthenticated GET)
    • /api/automation/events/route.ts (unauthenticated GET)
    • /api/automation/workflows/route.ts (unauthenticated GET)
    • /api/automation/workflows/[id]/route.ts (unauthenticated GET)
    • /api/pr-fix-queue/mark/route.ts (mutating, has auth)
    • /api/pr-fix-queue/queued/route.ts (auth)
    • /api/pr-fix-queue/enqueue/route.ts (auth)
    • /api/pr-followup/sync/route.ts (auth)
    • /api/pr-followup/webhook/route.ts (auth, has signature verification)
    • /api/issues/reconcile/route.ts (auth)
    • /api/issues/untriaged/route.ts (unauthenticated GET)
    • /api/issues/[issueId]/pr-health/refresh/route.ts (auth)
    • /api/agent-runs/route.ts (auth)
    • /api/health/route.ts (unauthenticated)
    • /api/auth/logout/route.ts (intentionally no auth)
    • /api/auth/[...nextauth]/route.ts (NextAuth-managed)

P2 — Repo label drift: typos and deprecated labels still on GitHub

labels.yaml is now the source of truth (per PR #373), but several legacy/dead labels still exist on the repo and aren't tracked anywhere: priotity/p1 (typo), needs-gpt (deprecated per AGENTS.md lane routing rules), type/bug (alongside the canonical bug), several ad-hoc agent/* labels (agent/Dispatch MCP, agent/OpenCode-Mac, agent/saffron, agent/dispatch-agent).

Evidence:

  • gh label list --repo misospace/dispatch --limit 100 --json name minus .github/labels.yaml labels: 22 unmanaged labels.
  • AGENTS.md:71 says "Do NOT route to ESCALATED only because labels include… legacy needs-gpt" — so the label is documented as legacy but not removed.

P2 — npm audit is clean, but SECURITY-ACCEPTED-RISKS.md still lists advisories that may be obsolete

The accepted-risks file documents two moderate CVEs (postcss XSS, hono/node-server path bypass) with the rationale "no viable upgrade path." Both packages now have patched versions; the rationale may no longer be accurate. CI scan (aquasecurity/trivy-action@ed142fd) shows the current state but does not include npm audit output.

Evidence:

  • cat SECURITY-ACCEPTED-RISKS.md lists next@16.2.7 bundles postcss@8.4.31 and prisma@7.8.0 / @hono/node-server < 1.19.13.
  • npm audit --omit=dev --json reports 0 vulnerabilities — confirming the underlying issues are no longer present at the installed version, but the accepted-risks doc has not been updated.

P3 — next-task test file is missing auth-coverage and a contract test for idle mode

The new next-task test file has 9 cases focused on happy-path task shaping, but no contract test for:

  • the idle task shape returned when neither queue nor PR-fix has work
  • the mode=groom groom-task-shape
  • the ?includeClaimed=true / ?includeRenovate=true query parameter handling

The tasks/report test file is similarly happy-path only (12 cases, no auth, no persistence).

Evidence:

  • wc -l src/app/api/agents/[agentName]/next-task/route.test.ts → not super high; count of it( cases: 9.
  • wc -l src/app/api/agents/[agentName]/tasks/report/route.test.ts → 12 cases, all validation only.

P3 — Heartbeat test file has only 2 shared-helper cases for a 184-line module

src/lib/heartbeat.test.ts is 82 lines and only covers the runSyncBestEffort / runReconcileBestEffort happy/empty paths. The 184-line heartbeat.ts includes aggregation, touchedIssueUrls collection, and warning/error bucket logic that is not covered. The integration is covered by src/app/api/agents/[agentName]/heartbeat/route.test.ts (412 lines) but the unit-level coverage is thin.

Evidence:

  • wc -l src/lib/heartbeat.ts src/lib/heartbeat.test.ts → 184 vs 82.
  • grep -c "describe\|it(" src/lib/heartbeat.test.ts → 2.

P3 — mc-client.ts resolveAgentName requires explicit agentName but the MCP server test environment may drift

The src/mcp/server.ts claimIssueHandler and claimWorkHandler use resolveAgentName which reads DISPATCH_AGENT_NAME from env, falling back to requiring an explicit agentName argument. The src/mcp/server.test.ts (747 lines) sets DISPATCH_AGENT_NAME via process.env in the test file, but the production MCP server entrypoint (src/mcp/server.ts) does not check whether DISPATCH_AGENT_NAME is unset and log a startup warning. If a model invokes the MCP without an explicit agentName and the env is unset, the response is "agentName is required" — not a startup error.

Evidence:

  • src/mcp/server.ts:40-50 shows the agentName requirement handling.
  • src/mcp/server.ts has no startup-time validation that DISPATCH_AGENT_NAME is set.
  • The src/mcp/index.ts entrypoint is not present in the source tree (only src/mcp/server.ts), so the startup path needs to be re-verified.

Recommended Issue Breakdown

  1. P1 — Add authentication to POST /api/agents/[agentName]/tasks/report or remove the endpoint until it actually persists reports.
  2. P1 — Add authorizeRequest (Bearer or basic) to GET /api/agents/[agentName]/next-task and document the auth model in the OpenClaw workflow contract.
  3. P1 — Replace the .npmrc omit= line with include=dev (or delete the file) and verify CI is still clean.
  4. P1 — Reconcile AGENTS.md and docs/worker-execution-contract.md with the new next-task, tasks/report, work-summary, active-work, and AgentTask contract.
  5. P2 — Decide the persistence model for tasks/report (audit log row, agent run row, both) and add tests.
  6. P2 — Extract shared queue-fetching logic between agents/[agentName]/queue/route.ts and agents/[agentName]/next-task/route.ts so lane-config and lease-filter changes apply to both.
  7. P2 — Migrate the hardcoded Lane = "normal" | "escalated" | "backlog" literals in issue-reconciliation.ts, issue-lane.ts, and types/index.ts:VALID_LANES to use the new lane-config.ts helpers.
  8. P2 — Add co-located route.test.ts for the 17 routes currently uncovered, prioritized by data-leak risk (/api/audit, /api/automation/*, /api/issues/untriaged).
  9. P2 — Prune legacy/dead labels: priotity/p1 (typo), needs-gpt, type/bug (alongside bug), ad-hoc agent/* labels.
  10. P2 — Update SECURITY-ACCEPTED-RISKS.md to reflect the current clean npm audit state, or document why the advisories remain on the accepted list.
  11. P3 — Add idle and mode=groom contract tests for /api/agents/[agentName]/next-task; add auth-coverage tests once fix(deps): update nextjs monorepo (14.2.18 → 14.2.35) #2 lands.
  12. P3 — Expand src/lib/heartbeat.test.ts to cover aggregation, touchedIssueUrls, and warning/error bucketing.
  13. P3 — Add a startup-time warning in the MCP server entrypoint if DISPATCH_AGENT_NAME is unset (mirror the DISPATCH_AUTH_MODE=disabled warning in docker-entrypoint.sh).
  14. P3 — Add a smoke check to docs/smoke-checklist.md that exercises the next-task end-to-end happy path and an idle path (mirroring the existing health/automation/sync checks).

Not Worth Doing Yet

  • Do not introduce an RBAC system yet — the Bearer/Basic/OIDC contract is still being normalized. First, add auth to the new endpoints.
  • Do not rewrite the Prisma client or split into microservices — the current module boundaries are workable.
  • Do not chase a generalized plugin system for next-task task types — the three shapes (implement, followup-pr, groom) are well-defined and easy to test.
  • Do not remove the /api/agents/[agentName]/tasks/report endpoint until Configure Renovate #1 is triaged — it may be a placeholder for an in-flight integration.
  • Do not chase cosmetic refactors of the tasks/report validation block — wait for the persistence decision.
  • Do not create child issues manually from this umbrella; the audit decomposer cron should do that deterministically.
  • Do not migrate lane-config.ts to the Prisma DB (epic: make Dispatch lanes configurable #381 epic) until the in-memory helper has at least one consumer outside of its own test — that would be premature.
  • Do not push back on the configurability work the in-flight epics (epic: make Dispatch lanes configurable #381chore: remove external runtime repo dependency from supported Dispatch workflows #401) are doing — those are well-scoped.

Audit Notes

Safe read-only checks performed:

  • Refreshed local main (already at 4c47335).
  • Inspected src/app/api/, src/lib/, src/components/, src/mcp/, prisma/schema.prisma, prisma/migrations/, .github/workflows/, AGENTS.md, docs/, SECURITY-ACCEPTED-RISKS.md, docker-entrypoint.sh, Dockerfile, package.json, tsconfig.json, eslint.config.mjs, vitest.config.ts, prisma.config.ts, .env.example, .npmrc, next.config.js.
  • npm run test → 1308/1308 pass.
  • npm run typecheck → clean.
  • npm run lint → clean.
  • npm audit --omit=dev --json → 0 vulnerabilities.
  • Cross-checked gh issue list (open and closed with audit label) and the previous umbrella (Weekly tech debt audit: dispatch - 2026-06-03 #308).
  • Cross-checked gh pr list for the merged-in-this-window surface.
  • gh label list vs .github/labels.yaml to find unmanaged label drift.
  • Verified the .npmrc deprecation warning by reproducing it in a throwaway project at /tmp/test-npmrc-*.

Not done (would require write access or live state):

  • Did not exercise the live Dispatch instance.
  • Did not time the new next-task and queue routes against realistic data volumes.
  • Did not attempt to re-run the OpenClaw harness against the new next-task contract.

Local repo state after audit: Clean. No files modified.

Decomposed into

Metadata

Metadata

Assignees

No one assigned

    Labels

    auditAudit, review, or investigation work.enhancementNew feature or improvement.priority/p1High priority.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions