Skip to content

fix(runtime): stop the package-publish door disclosing driver text on seedApplied - #8518

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-8443-runtime-seed-apply-disclosure
Aug 13, 2026
Merged

fix(runtime): stop the package-publish door disclosing driver text on seedApplied#8518
os-zhuang merged 3 commits into
mainfrom
claude/issue-8443-runtime-seed-apply-disclosure

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #8443

The runtime package-publish door keeps a route-level seed apply for protocols that do not apply seeds inside publishPackageDrafts themselves. That fallback is a second copy of metadata-protocol's applySeedBodies, and it kept the ADR-0112 defect PR #8436 fixed there (#8333's P9): a caught error's sentence interpolated onto a client-facing payload.

Reproduced first — the card was read-from-source, and reproducing changed the answer

Driven through HttpDispatcher.handlePackages with a real SeedLoaderService (only the engine, the metadata service and the protocol doubled), before any implementation change. Recorded pre-change response — HTTP 200:

{ "success": true, "data": {
  "success": true, "publishedCount": 1, "failedCount": 0,
  "published": [{ "type": "seed", "name": "project_seed", "version": "h" }],
  "failed": [],
  "seedApplied": { "success": false,
                   "error": "SQLITE_ERROR: no such table: sys_metadata" } } }

Positive control, same harness, healthy engine: seedApplied: { success: true, inserted: 2 } with one real bulk insert('project', [Apollo, Gemini]) at the engine — so the injection perturbs a path that genuinely runs, and "no driver text" can never be confused with "nothing happened".

The premise holds, and the reproduction added something the card did not have. Four populations reach that field; measured, pre-change:

injection pre-change seedApplied
B metadata.getObject throws — the loader's dependency-graph read, unguarded in resolveObjectDefinition error: "SQLITE_ERROR: no such table: sys_metadata"
C protocol.getMetaItem throws — the seed body read-back errors: ["read project_seed: SQLITE_ERROR: no such table: sys_metadata", …]
D a malformed seed body error: a multi-line JSON dump of raw zod internals
E a declared 4xx refusal on the read-back the authored sentence, verbatim — correct, and must stay

C is a second carrier on the same field, and the card named only B. It is also the one a sys_metadata outage reaches first: the read-back happens before the loader is ever constructed. A fix confined to the door's catch would have left the commonest outage shape disclosing exactly as before, while the PR claimed the door was fixed. Both are converted here.

The helpers question, settled producer-side

metadata-protocol now exports clientFacingFailureText and seedRequestValidationError; runtime imports them. Enabling export only — no behaviour in metadata-protocol changes, and its suite is untouched and green.

Why exporting beats restating, on the merits rather than by preference:

  • Real business need. Both are consumed by this PR — this is not a speculative surface. The rule is one decision ("when may a caught sentence be quoted") that two producers must answer identically, and the same file already imports isWritablePackage from this package with the identical argument written above it: "this defect existed precisely because the lifecycle routes had no copy of it, and a second copy would be the next place it drifts." recordNotFoundError is the standing precedent for exporting an envelope so a runtime fallback builds the same one.
  • Long-term soundness. A second private restatement is a second de-facto contract for a security-relevant rule, and the two copies diverge silently — the failure mode being that one of them starts quoting again and no gate can see it.
  • Hard to get wrong. The anti-drift pin is executable: section 2 asserts the sentence a caller receives equals what seedRequestValidationError mints for the same rejection, so a future local restatement in runtime goes red on the first edit to either side.

declaresClientRefusal is deliberately not exported: nothing outside its file needs the raw predicate, and an export with no consumer is declared-but-unpulled surface.

Authoring feedback is preserved, not blanked — the same cure #8333 used

Population D is why the rule could not simply be applied. A raw ZodError declares nothing, so the withhold would have replaced a real authoring error with seed apply failed. The cure is the producer-side one, not a loosened collector: the seed request is parsed with safeParse and its rejection minted as a declared INVALID_METADATA / 422, so it satisfies the positive list on its own merits. The author is strictly better off — a curated summary naming the seed and the key, instead of a dump of zod internals.

Reverse verification — direction predicted BEFORE the run

Predicted, with domains/packages.ts reverted to origin/main and the export kept: RED for the two withholds (B, C) and the two authoring-envelope cases (D) = 4 failed | 2 passed; green-in-both-directions for the positive control and the declared-refusal [GUARD] (E).

Measured: 4 failed | 2 passed — as predicted, and each red failed on the text, not on a vague change:

AssertionError: expected 'SQLITE_ERROR: no such table: sys_meta…' to be 'seed apply failed'
AssertionError: expected 'read project_seed: SQLITE_ERROR: no s…' to be 'read project_seed: the reason is in t…'
AssertionError: expected '[\n  {\n    "code": "invalid_value",\…' to contain '[invalid_metadata]'

Second variant, which is what makes the guard load-bearing: with clientFacingFailureText forced to withhold unconditionally (a blanket blank), predicted 3 failed | 3 passed — section 2's two quotes and section 3's guard. Measured 3 failed | 3 passed, those exact three. Without them the file would be satisfied by "withhold everything", which deletes the self-correcting refusals #4277 exists for.

No missed prediction on this card.

File surface

  • packages/runtime/src/domains/packages.ts — the two conversions plus parse to safeParse, and the withheld originals now go to the log.
  • packages/runtime/src/domains/packages-seed-apply-disclosure.test.ts — new; 6 cases across positive control / withhold / authoring quote / over-block guard. Every case asserts the contents of the field inside the 200 body; none asserts a status code alone and none uses a bare toThrow() (this door does not throw, it reports).
  • packages/metadata-protocol/src/protocol.ts, src/index.tsenabling export only, no behaviour change (flagged for the metadata lane).
  • .changeset/runtime-seed-apply-driver-text.md.

Nothing outside those two packages.

Verification

Numbers below are the post-merge run: main moved 14 commits under this branch, including two that touch packages/metadata-protocol, so everything was re-run after merging and rebuilding rather than reported from the pre-merge lap. The one conflict was in metadata-protocol/src/index.ts — two export blocks added at the same line, both kept.

  • pnpm --filter @objectstack/runtime test152 files / 2325 tests passed.
  • pnpm --filter @objectstack/metadata-protocol test82 files / 1223 tests passed.
  • pnpm --filter @objectstack/runtime typecheck — clean.
  • pnpm --filter @objectstack/spec check:generated"All 13 generated artifacts are up to date" (main moved packages/spec; nothing here regenerates).
  • Gates re-derived from the actual changed paths (scripts/pm/dispatch-gates.mjs) and run green: check:nul-bytes, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:engine-double-contract, check:filter-alias-parity, check:objectui-changeset, check:route-envelope, check:query-options-erasure, check:error-code-casing, check:test-source-alias, check:type-check-coverage.
  • check:type-check-debt on a full built closure (70/70 turbo build tasks): green — "33 ledger entries re-measured, 1969 raw errors, none above its recorded number; surplus: none". The ledger was not raised.

Out of scope, filed not fixed

#8516 — the same handler's unhideError and rebindError fields put caught text on that same 200 body from two different producers (the ADR-0045 visibility flip and the metadata:reloaded announce). Read from source, not reproduced, so it gets its own card with the same reproduce-first condition rather than riding along unmeasured here.


Generated by Claude Code

claude added 2 commits August 13, 2026 16:28
… seedApplied

The route-level seed apply that runs for protocols which do not self-apply
seeds is a second copy of metadata-protocol's applySeedBodies, and carried the
same ADR-0112 defect #8333's P9 fixed there: caught driver text interpolated
onto a client-facing payload. seedApplied rides on a 200 publish response as
data, so no HTTP boundary's 5xx message withhold can reach it.

Reproduced first, through HttpDispatcher.handlePackages with a real
SeedLoaderService. The reproduction found two carriers, not one: the door's
catch (a driver failure under the loader's dependency-graph read) and the
per-read errors[] entries (a driver failure on the seed body read-back, which
is the carrier a sys_metadata outage reaches first).

The rule is imported from the producer rather than restated: metadata-protocol
now exports clientFacingFailureText and seedRequestValidationError (enabling
export only, no behaviour change there). The seed request parse becomes a
safeParse whose rejection is minted as a real 422, so a malformed seed body is
still quoted back to its author while undeclared driver text is withheld.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7vaLs7bhBPi9m3JyzkhDj
…the changeset

Both predictions held: 4 failed | 2 passed with the door reverted, and
3 failed | 3 passed under the over-broad variant (nothing ever quoted), the
three being the two authoring quotes and the declared-refusal guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7vaLs7bhBPi9m3JyzkhDj
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 13, 2026 5:17pm

Request Review

…time-seed-apply-disclosure

# Conflicts:
#	packages/metadata-protocol/src/index.ts
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/runtime.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/permissions/system-context.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata-protocol, @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)

3 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)
  • content/docs/releases/v17.mdx (via @objectstack/runtime)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 13, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 13, 2026 19:28
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit b30963d Aug 13, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8443-runtime-seed-apply-disclosure branch August 13, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

packages/runtime's package-publish door has its own copy of the P9 seed-apply disclosure, outside metadata-protocol

2 participants