Skip to content

fix(routes): reject same-server MCP App resourceUri collisions with AB4829 and sweep orphaned Playground staging files - #430

Merged
ScriptedAlchemy merged 2 commits into
mainfrom
fix/sweep-resource-uri-collision-stage-sweep
Sep 3, 2026
Merged

fix(routes): reject same-server MCP App resourceUri collisions with AB4829 and sweep orphaned Playground staging files#430
ScriptedAlchemy merged 2 commits into
mainfrom
fix/sweep-resource-uri-collision-stage-sweep

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

Final-sweep lane B2, items 7 and 8.

7. Route graph — AB4829 for same-server MCP App resourceUri collisions. Two App routes of one generated server declaring the same static config.resourceUri resolved first-wins with no diagnostic (only the AB4812 presence check existed). compileRouteGraph now tracks App routes per generated server by resourceUri and reports AB4829 (error) for every additional claimant, naming both route files and the server; the route stays visible in the IR and validate/inspect fail the way AB4812 does. The same URI on App routes of different servers is not a collision (each generated server registers only its own Apps, per #42 / #418), and servers pinned custom/command/remote are not checked (they ship no Apps).

  • docs/diagnostics.md: new AB4829 row (next free code in the AB48xx block; the website diagnostics reference is generated from it).
  • docs/entry-conventions.md, website/docs/{en,zh}/guide/authoring/mcp.mdx: the App-route paragraph now names AB4812/AB4829.

8. Native Playground publisher — bounded orphan staging sweep. A publisher that dies before its staging cleanup leaves .<epoch>.stage-<pid>-<nonce> files behind. Entries still hard-linked to their epoch's sidecar are already recovered by the reader (#377/#408); entries that alias nothing (died before link(), lost the EEXIST race, sidecar withdrawn later) were never swept. Each successful #persistSnapshot now runs #sweepAbandonedStaging once over its own catalog directory:

  • no recursion; at most 64 pattern-matching entries examined and 16 removed per publish (nativePlaygroundStagingSweepLimits, @internal);
  • removes an entry only when it matches this publisher's own naming pattern, belongs to an epoch other than the one being published, its embedded publisher pid has exited (stagingPublisherExited), and lstat reports a regular file with nlink === 1 that does not alias <epoch>.json (#sidecarStillLinked inode check);
  • a live winner's linked staging entry, a running publisher's file, guards, directories, symlinks, doubly linked aliases, and foreign look-alike names are kept;
  • the sweep is hygiene: any filesystem failure ends it quietly and never changes the publication outcome.

Changeset: .changeset/sweep-resource-uri-collision-stage-sweep.md (agent-bundle patch, mentions AB4829).

Evidence

  • packages/agent-bundle/tests/route-graph.test.ts — new test rejects two App routes of one server that declare the same resourceUri with AB4829, but not the same URI across servers: same server → ['AB4829'] with both files and "curator" in the message, sourcePath = second route; three claimants → two AB4829; two servers sharing one URI → no diagnostics; custom server → no diagnostics; validate reports AB4829 and inspect is invalid.
  • packages/agent-bundle/tests/native-playground-service.test.ts — three new tests: (a) older-epoch orphan + orphaned guard removed on the next publish, same-epoch orphan left until a later epoch publishes; (b) live winner's hard-linked entry (inode equal to the sidecar, nlink 2), live-pid file, five foreign look-alikes, a directory, a symlink to a file outside the directory, and a link aliasing an outside file are all kept, and the kept older epoch still reads back through the recovery path; (c) 20 orphans → 16 removed on the first publish, the rest on the second; 64 live-pid candidates ahead of an orphan exhaust the examination cap until they are gone.
  • Gates in the worktree after pnpm build: pnpm typecheck ✅, pnpm lint ✅ (0/0), pnpm test:projection ✅, pnpm docs:site:build ✅ (language parity ok). pnpm test:unit and pnpm test:route-unit: all tests pass except two 5 s timeouts unrelated to this change under load average ≈ 25 on the shared box (native-claude-contract.test.ts "fails closed when the candidate plugin…" — snapshots the real ~/.claude; route-unit/lifecycle-replay.test.ts — passed on isolated re-run). Both touch neither routes/graph.ts nor the Playground service.

Test plan

  • pnpm exec rstest --config rstest.unit.config.ts packages/agent-bundle/tests/route-graph.test.ts (50/50)
  • pnpm exec rstest --config rstest.unit.config.ts packages/agent-bundle/tests/native-playground-service.test.ts (42/42)
  • pnpm typecheck, pnpm lint, pnpm test:unit, pnpm test:route-unit, pnpm test:projection, pnpm docs:site:build
  • CI green

Review status

  • chatgpt-codex-connector answered "You have reached your Codex usage limits for code reviews" to both the automatic review and the explicit @codex review request (2026-09-03 16:50Z / 16:51Z). No review threads were opened.
  • Last reviewed SHA: none. Unreviewed SHAs: ad29670196e9f154c799eff39406ad87aba1f436, 4891697b41d9b097a7eb0c4c32ca0964b3c2a876 (head).
  • Merged on CI green under the sweep-lane quota rule; a follow-up PR will answer any review threads left on this merged PR.

…B4829 and sweep orphaned Playground staging files

Two App routes of one generated server declaring the same static config.resourceUri resolved first-wins with no diagnostic; the route-graph compiler now reports AB4829 naming both route files and the server, while the same URI across different servers still passes. The native Playground catalog publisher sweeps singly linked .<epoch>.stage-<pid>-<nonce> files that an exited publisher of another epoch left behind, bounded per publish and confined to its own directory, keeping a live winner's hard-linked staging entry, live publishers' files, and foreign entries.
@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4891697

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@430
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@430
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@430

commit: 4891697

@ScriptedAlchemy
ScriptedAlchemy merged commit bf04ae1 into main Sep 3, 2026
13 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the fix/sweep-resource-uri-collision-stage-sweep branch September 3, 2026 17:12
ScriptedAlchemy added a commit that referenced this pull request Sep 3, 2026
…g; renumber the retention diagnostic to AB4833

- a replayed admission (attempt receipt already present) resolves the next-event
  ceiling from the ledger's current advertisement instead of the notice's class,
  so a store reopened under a lowered ceiling never replays the authored secret
- AB4829 was taken by #430 on main; the notices.retention diagnostic is AB4833
- inspect-state: the retention CLI assertions get their own bounded test
ScriptedAlchemy added a commit that referenced this pull request Sep 3, 2026
…g; renumber the retention diagnostic to AB4833

- a replayed admission (attempt receipt already present) resolves the next-event
  ceiling from the ledger's current advertisement instead of the notice's class,
  so a store reopened under a lowered ceiling never replays the authored secret
- AB4829 was taken by #430 on main; the notices.retention diagnostic is AB4833
- inspect-state: the retention CLI assertions get their own bounded test
ScriptedAlchemy added a commit that referenced this pull request Sep 3, 2026
…g; renumber the retention diagnostic to AB4833

- a replayed admission (attempt receipt already present) resolves the next-event
  ceiling from the ledger's current advertisement instead of the notice's class,
  so a store reopened under a lowered ceiling never replays the authored secret
- AB4829 was taken by #430 on main; the notices.retention diagnostic is AB4833
- inspect-state: the retention CLI assertions get their own bounded test
ScriptedAlchemy added a commit that referenced this pull request Sep 3, 2026
…g; renumber the retention diagnostic to AB4833

- a replayed admission (attempt receipt already present) resolves the next-event
  ceiling from the ledger's current advertisement instead of the notice's class,
  so a store reopened under a lowered ceiling never replays the authored secret
- AB4829 was taken by #430 on main; the notices.retention diagnostic is AB4833
- inspect-state: the retention CLI assertions get their own bounded test
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
…acceptance item 7 (#437)

* feat(notices): redaction contract and retention policy close out #99 acceptance item 7

- sensitivity: 'public' | 'internal' | 'secret' on publish; per-route host ceilings
  (noticeDelivery.*.sensitivity with dated evidence) honoured by the ledger, inbox
  resource, event admission, and resources/updated signaller; withholdings recorded
  on the notice; internal content secret-pattern redacted on egress
- notices.retention config (AB4829) -> ledger retain()/inspect(); terminal notices
  pruned on admitted events; journal compaction via AgentStateStore.compact()/inspect()
  (compact baseline record, kernel format 2, pruned-key bookkeeping) on both drivers
- adapter capability rows + adapterRevision bumps; inspect --state and Workbench
  State panel show the resolved retention policy
- mcp-server-runtime.d.ts spells GeneratedNoticeDeliveryBinding locally; packed
  assertion that no aliased/public declaration resolves through the notices subpath

* test(workbench): bound the State-panel retention assertions by the browser timeout

* chore(changeset): reference #437

* fix(workbench): admit noticeRetention in the strict route-manifest wire schema

The dev server's manifest now carries the resolved notice retention policy; the
browser client's strictObject rejected the unknown key and rendered the whole
Routes catalog as unavailable (caught by examples-real.e2e).

* fix(notices): disclose acknowledged content per route and record signaller withholdings

- acknowledge() returns the notice as the acknowledging route may disclose it
  (next-event for events, the inbox ceiling otherwise); a withheld class comes
  back as a [REDACTED] placeholder, so an id from a redacted inbox unlocks nothing
- the resources/updated signaller records its refusal durably through the new
  AgentNoticeLedger.recordWithholding() / 'withheld' event, once per subscription

* fix(notices): disclose replayed deliveries against the current ceiling; renumber the retention diagnostic to AB4833

- a replayed admission (attempt receipt already present) resolves the next-event
  ceiling from the ledger's current advertisement instead of the notice's class,
  so a store reopened under a lowered ceiling never replays the authored secret
- AB4829 was taken by #430 on main; the notices.retention diagnostic is AB4833
- inspect-state: the retention CLI assertions get their own bounded test

* fix(notices): mask credential-keyed JSON whole; replay only the live baseline; match d.ts specifiers not prose

- redactJson masks every string under a credential-shaped key (isSecretKey, pinned
  equal to the compiler's isCredentialKey) so { password: 'hunter2' } never leaks
- replayJournal parses only the latest baseline at or below the target, so a
  compact/reset baseline written before a later migration is never fed to the
  current schema (conformance case on both drivers)
- the packed d.ts assertion matches import specifiers, not doc comments

* fix(notices): fail closed on unknown ceilings at construction; replay a retried admission instead of conflicting

- routeSensitivityCeiling admits nothing for a ceiling outside the vocabulary and the
  ledger and signaller validate the advertisement when constructed (invalid-input)
- a retry of the same event invocation whose recomputed admission differs replays the
  committed admission on idempotency-conflict instead of failing the request

* fix(notices): mask JSON keys and keep deduped publish from leaking content

A guessed dedupe key must not return another author's notice text, and
credential-shaped JSON member names are redacted like any other prose.

* feat(notices): redact notice content with flare-redact instead of hand-rolled patterns

Per maintainer direction the secret pass is an npm library, not custom code:
flare-redact@1.6.1 (MIT, zero deps, browser-safe root entry) becomes an
exact-pinned runtime dependency; the ledger runs its default detectors and
credential-shaped member names with every finding replaced whole by
[REDACTED]. The hand-rolled pattern/key sources, isSecretKey, and the
cross-package parity test are removed; credentials.ts and redactProbeText
return to main. README records the evaluated libraries.

* fix(notices): keep the byte bound on egress; fail closed on library limits; bounded-history wording

Self-review findings: a redacted document that grew past the Agent Document
byte bound is handed out as the placeholder; RedactionLimitError falls back
to the mark instead of failing the inbox; the pinned detectors' assignment
and OpenAI length limits are documented as contract; the generated notice
page no longer calls the ledger append-only.

* docs(notices): name noticeRedactionPlaceholder in the changeset and README

Self-review second pass: the helper is exported from
@agent-bundle/runtime/notices on purpose (embedders recognise the
placeholder a withholding route hands out), so the release text and the
README name it alongside the other new exports.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant