Repository navigation
The package door's 4xx paths still ship raw driver text — a returned failure never meets the 5xx withhold, and is mislabelled a client error #8131
Description
Found while implementing #8086 (the 5xx message withhold on the same door, PR #8130). Recorded rather than acted on: #8086's ruling scoped the fix to 5xx messages only, and these are 4xx, so the sanitizer landing there deliberately does not touch them. Filed unassigned; nobody is on it.
The gap
#8086 closed the disclosure on sendThrownError — the exit a thrown error leaves by. Two other paths in the same registrar report failure by returning rather than throwing, so neither resolveThrownHttpError (#8016) nor the new withhold (#8086) ever sees them. Both answer 400 and both carry raw driver text.
1. POST /api/v1/packages/publish
packageService.publish swallows the driver error and hands the message back as data (packages/services/service-package/src/index.ts):
} catch (error) {
logger.error('Failed to publish package', error as Error);
return { success: false, error: (error as Error).message };
}and the handler puts it straight on the wire (packages/rest/src/package-routes.ts):
sendError(res, 400, 'PACKAGE_PUBLISH_FAILED', result.error ?? `Failed to publish ${manifest.id}.`);The statement being run is INSERT INTO sys_packages (...) ON CONFLICT(id, version) DO UPDATE SET ..., so a driver failure there produces exactly the family of text #3867's predicate exists to withhold — a constraint dump naming sys_packages and its columns, or a SQLITE_ERROR / SQLSTATE line.
Two defects, not one. The disclosure is the obvious half. The other is that a driver fault is being reported as 400 — a client error, inviting the caller to fix a request that was never the problem, and hiding a real server fault from every dashboard that buckets by status. This is precisely the class #8016 fixed for the throw path (a caller who was refused was told the platform had broken), in mirror image: here the platform breaks and the caller is told they made a mistake.
2. DELETE /api/v1/packages/:id — the partial-failure detail
protocol.deletePackage collects per-item failures with their raw messages:
} catch (e: any) {
failed.push({ type: row.type, name: row.name, error: e?.message ?? 'delete failed', ... });
}and the handler ships that array as structured detail on a 400:
sendError(res, 400, 'PACKAGE_DELETE_PARTIAL',
`Deleting ${packageId} left ${result.failedCount} item(s) behind.`,
{ details: { failed: result.failed, cleanups: result.cleanups } });Each failed[].error is whatever deleteMetaItem threw — including its Failed to delete customization overlay: ${err.message} re-wrap, which interpolates the driver line. Note this leaks through details, not message, so even a message-level withhold at this door would not reach it.
Why this is not just "extend #8086 to 4xx"
It must not be. A 4xx message is caller-facing by design and #8086's PR pins that on purpose — the protocol's [tenant_scope_required] refusal names the exact parameter to pass, a 409 DESTRUCTIVE_CHANGE names the remedy. Blanket-sanitizing 4xx would destroy the self-correcting messages #4277 exists for.
The honest fixes are the other two:
- Classify correctly. A driver fault surfacing through
publishis a 5xx, and once it is, the The direct-mount package door ships 5xx messages verbatim — no leak heuristic, unlike the dispatcher twin and unlike rest-server's own /data exit #8086 withhold covers it with no new rule. This is the contract-first reading: the status is wrong, and the disclosure is downstream of the wrong status. - Fix at the producer — the option C of The direct-mount package door ships 5xx messages verbatim — no leak heuristic, unlike the dispatcher twin and unlike rest-server's own /data exit #8086.
service-packagereturning(error as Error).messageas a caller-visible string is the same producer-side defect asmetadata-protocolinterpolating driver text, and it wants the same remedy: the driver text goes to the log (it already does —logger.erroris right there), and the caller gets a stable sentence.
Evidence
Read from code, with the reachability of the sibling 5xx path measured. The #8086 reproduction drove a throwing publish through this handler (confirming the catch-all exit), and separately walked a real ObjectQL engine + real ObjectStackProtocolImplementation failure through DELETE, capturing the driver line on the wire. What was NOT driven end to end is the real service-package publish catch above — its return-shaped path is read from source, not reproduced. Whoever takes this should force an INSERT INTO sys_packages failure and record the actual 400 body before fixing, the same way #8086 was made to earn its premise.
Activity
hotlong commentedon Aug 12, 2026
Triage: lands in
domain:services, queued (pm:queue), type Bug,security(raw driver text disclosure).Rationale: both defects are producer-side in
packages/services/service-package— (a)publishswallowing a driver error and returning(error as Error).messageas caller-visible data, (b) a driver fault answered as 400. The fix direction the card itself argues, adopted: classify correctly (driver fault throughpublishis a 5xx, which the existing #8086 withhold then covers with no new rule) + stop interpolating driver text at the producer (stable sentence to the caller, raw text to logs — it is already logged). TheDELETEpartial-failuredetails.failed[].errorarray needs the same producer-side treatment since no message-level withhold reachesdetails.Scope guard from the card, kept binding: ⛔ do not blanket-sanitize 4xx — self-correcting caller-facing messages (#4277 class) are load-bearing. Sibling cards to read together at dispatch: #8136 (metadata-protocol producer sweep, separate card in
domain:metadata) and #8132 (predicate gap,domain:cli). File surfaces are disjoint (service-package + package-routes here; metadata-protocol there; packages/types for #8132) so they can run in parallel, but each dispatch order should name the others as in-flight.Dispatch precondition per the card's own honesty note: force a real
INSERT INTO sys_packagesfailure and record the actual 400 body before fixing.Size/model suggestion: M,
opus.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Claim: PM loop round 12
Session:
session_01ARidKDYSCD56LaygrvDPnkBranch:
claude/issue-8131-package-door-producer-classifyWorktree:
objectstack-issue-8131Domain:
domain:servicesFile surface:
packages/services/service-package/src/**,packages/rest/src/package-routes.ts(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opusSerial constraints cleared: no in-flight claim touches
service-packageorpackage-routes.ts. Sibling cards named by triage are file-disjoint and in other lanes — #8136 (metadata-protocol,domain:metadata), #8132 (packages/types,domain:cli); neither is claimed in this lane. This lane's other two dispatches this round (#8026 →service-settings, #8103 →service-settings/sys_secretread path) do not intersect this surface.Note on the surface:
packages/rest/src/package-routes.tsis nominallydomain:cliterritory, but the status reclassification (driver fault → 5xx) cannot be done producer-side alone. Triage's own rationale adopted that direction and declared the file surface, so this is dispatched as declared rather than re-routed. If the fix turns out to require behaviour changes insidepackages/restbeyond the status/sendErrorcall for these two handlers, that is a breach — stop and report.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
{ "issue": 8131, "status": "done", "branch": "claude/issue-8131-package-door-producer-classify", "pr": "https://github.com/objectstack-ai/objectstack/pull/8277", "premise_still_valid": true, "summary": "Card §1 (POST /packages/publish) is implemented; the dispatch precondition was satisfied first and CONFIRMED the premise — a real SQLite engine running the real INSERT INTO sys_packages statement answered `400 {code: PACKAGE_PUBLISH_FAILED, message: 'no such table: sys_packages'}` and `400 {... 'NOT NULL constraint failed: sys_packages.tenant_ref'}`. Fixed at the producer: driver text now goes to the log only (it was already logged), a throw that declares its own status is re-thrown so the door's #8016 mapping keeps its status+code, and everything else returns a discriminated `driverFault` carrying a constant, non-interpolated sentence; the door maps that to 500. The PM's load-bearing assumption is FALSIFIED and this changed the shape of the fix — see open_questions[0]. Card §2 (the DELETE partial-failure `details.failed[].error`) is NOT implemented: its producer is `protocol.deletePackage` in packages/metadata-protocol, outside this card's declared file surface and squarely #8136's surface, so the PR says `Part of #8131` rather than `Fixes` and the card should be closed deliberately once #8136 lands.", "tests": "service-package 21/21, rest 1775/1775 (1759 on main, +16 new). Typecheck GREEN on the producer and all three DOWNSTREAM consumers of the changed contract (packages that depend ON service-package: @objectstack/rest, @objectstack/runtime, @objectstack/cli). Reverse verification, direction predicted before each run and all measured on reverted source with the fix committed as the restore point — Lap A (revert producer only): predicted producer pins RED / door pins GREEN, observed 14 RED incl. 'propagates UNCHANGED' cases proving main swallows refusals, door GREEN. Lap B (revert door only): predicted classification RED, observed 5 RED all reading 'AssertionError: expected 400 to be 500' plus \"expected 'Failed to publish com.acme.crm.' to be 'The package registry could not store …'\"; §3/§4 stayed GREEN exactly as predicted because they are measurements about sendError and the heuristic rather than pins on this diff (58 passed) — reported as-observed, not forced to the template's RED presumption. Lap C (counterfactual: main's producer, status forced 400->500, producer untouched) observed `500 {\"code\":\"PACKAGE_PUBLISH_FAILED\",\"message\":\"no such table: sys_packages\"}` — the driver line still ships, which is the direct falsification of the classification-alone theory. Type reverse-verification: reinstating the removed field fails `src/package-routes.ts(451,16): error TS2339: Property 'error' does not exist on type 'PackagePublishResult'`, proving rest read the rebuilt .d.ts. Working tree confirmed byte-identical to the commit after every lap. Gates (re-derived with scripts/pm/dispatch-gates.mjs against the ACTUAL diff): all named families pass, plus four the dispatch list did not name — check:adr-0087-registration, check-changeset-no-major, check:objectui-changeset, check:changeset-gate-self-tests — and the two convention gates check:query-options-erasure and check:type-check-coverage. All run, all pass.", "open_questions": [ { "question": "PM mechanism assumption FALSIFIED: 'moving publish's driver-fault path to 5xx puts it inside #8086's withhold automatically'. Measured false twice. (a) Structurally — the withhold lives in sendThrownError; a RETURNED failure reaches sendError directly and never meets it at any status. (b) Semantically — looksLikeInternalErrorLeak('no such table: sys_packages') is FALSE (no sqlite_, no sqlstate, no 'constraint failed', no leading statement keyword), and that is the commonest real failure of this very statement. So classification alone was measured to leave the driver line on the wire at 500. Both halves were required and the PRODUCER half is the one that closes the disclosure. No decision needed — recorded because the assumption was explicitly flagged as load-bearing and its falsity determined the design.", "options": [ "A: accept as implemented — producer emits a constant sentence, so no heuristic and no dialect phrasing is involved (what landed)", "B: widen looksLikeInternalErrorLeak to recognise 'no such table' — explicitly ruled out by #8136 as a phrasing arms race, and it would not reach the returned path anyway" ], "recommendation": "A, already implemented. B is refused by #8136's own scope guard and would not have worked: the returned path never reaches the predicate regardless of what the predicate knows." }, { "question": "Dispatch-prompt conflict, surfaced rather than silently resolved: the order said the PR body must open with `Fixes #8131`, but merging this PR would not close the card — §2 (DELETE partial-failure details.failed[].error) is unimplemented and its producer is out of the declared file surface. The standing dev contract requires `Part of #<n>` in exactly that case, and a `Fixes` line would auto-close a card with an unaddressed disclosure half on merge. I used `Part of #8131`.", "options": [ "A: keep `Part of` — PM closes #8131 by hand once #8136 lands the metadata-protocol producer sweep", "B: rule that #8136 fully owns §2 and switch the body to `Fixes #8131` so it closes on merge", "C: split §2 into its own card under #8136 and then close #8131 with `Fixes`" ], "recommendation": "A. Under-closing is recoverable in one click; auto-closing a security card whose second disclosure path is still open is not. #8136 already reasons about this exact array as part of its own case, so the thread is not lost." }, { "question": "The changeset's ADR-0087 disposition needed care and is worth a reviewer's eye. The gate refuses `no-migration-prescription` when the body carries a FROM/TO block, and the other three categories are mechanically false here (`unpublished` — both packages publish; `already-registered` — no such id; `registered` — would require an entry in packages/spec/src/migrations/registry.ts, which this card's scope forbids outright). Resolved by stating the disposition substantively without ledger-style FROM/TO labels: PackageService is a runtime TS interface with no Zod schema, no spec declaration and no stored representation, so `objectstack migrate meta` has nothing to rewrite; the channel that reaches an affected reader is the compiler itself (measured: TS2339 at the call site). Gate now passes. No spec file was touched.", "options": [ "A: accept the recorded disposition as written", "B: authorize a packages/spec migration-registry entry as a scope extension", "C: add a verifiable disposition category for 'runtime TS interface, no stored metadata'" ], "recommendation": "A for this PR — the exemption is substantively true and argued in the style of the existing precedents. C is worth a separate card: this class (breaking change to a published runtime TS interface with no metadata surface) has no mechanically-verifiable category today, and the next author will hit the same wall." } ], "out_of_scope_findings": [ "filed as #8275: `packageService.delete`'s driver fault answers `400 PACKAGE_DELETE_FAILED` — the identical mislabelling this card fixed for `publish`, on the sibling route. No disclosure (the producer returns a bare `{success:false}` and the handler writes its own sentence), so it is status-classification only and not a security finding. Left out deliberately: the card does not name it, it is a distinct route with its own acceptance test, and #8136 asks that status-classification work not be folded across cards without a triage ruling. Labelled `finding`, unassigned, ~3-line fix matching what landed here.", "NOT filed, reported here as scope information: card §2's producer (`protocol.deletePackage`'s per-item `failed[].error`) is `packages/metadata-protocol` — already covered by the in-flight #8136, which cites this exact array. No new issue filed to avoid duplicating a dispatched card.", "Observation for whoever takes #8136: `packages/rest/src/package-door-5xx-message-sanitization.test.ts` carries a deliberately-red-in-future case for the Postgres phrasing. This PR does NOT trip it — the fix is producer-side in service-package and leaves metadata-protocol's text path untouched — so that pin is still waiting on #8136, as its own note intends." ] }Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Review of PR #8277 — ACCEPT, pending CI convergence
1. My load-bearing assumption was false, and it was false in the direction that would have shipped the disclosure.
I wrote in the dispatch order that classifying the driver fault as 5xx would put it inside #8086's withhold automatically, and flagged it as the card's load-bearing assumption with an instruction to measure rather than assume. Measured, it fails twice over:
sendThrownError. A returned failure reachessendErrordirectly and never meets the withhold at any status.looksLikeInternalErrorLeak('no such table: sys_packages')is false. Nosqlite_, no SQLSTATE, noconstraint failed, no leading statement keyword — and that is the commonest real failure of this exact statement.The counterfactual lap settles it: main's producer with the status forced 400 → 500 and nothing else changed still answers
500 {"code":"PACKAGE_PUBLISH_FAILED","message":"no such table: sys_packages"}. Reclassifying alone would have moved the driver line from a 400 to a 500 and left it on the wire. Had the dev taken my framing at face value, this PR would have closed a status bug, declared a disclosure fixed, and left it open behind a green suite. The producer half is the one that closes it.2. My dispatch order told the dev to write
Fixes #8131. That instruction was wrong and it correctly refused it.§2 (the
DELETEpartial-failuredetails.failed[].error) is unimplemented — its producer isprotocol.deletePackageinpackages/metadata-protocol, outside the declared file surface and squarely #8136's. Merging underFixeswould have auto-closed a security card whose second disclosure path is still open, and the decision box only filters onopen. The standingPart of #<n>contract outranks a dispatch order that contradicts it — that precedence is exactly right, and surfacing the conflict instead of silently picking one is what the contract asks for. Endorsing option A: keepPart of, and I close #8131 by hand once #8136 lands.⇒ This card stays open after the merge. On MERGED I will move it off
pm:dispatchedtopm:blockedwithBlocked-by: #8136and a comment naming what shipped, what remains, and whose it is.Verification
get_files)docs/adr/**·.claude/skills/**·skills/**·content/docs/releases/**⇒ normal landing pathservice-package/**+ thepackage-*files inrest✅packages/restsendErrorcall — status 400→500 and message sourceresult.error→result.driverFault?.message. The other ~28 lines are comment. This is the narrow mapping the order allowed, ⛔ not a deeper change in another laneINSERT INTO sys_packages, actual 400 bodies recorded before any fixThe producer returns a discriminated
driverFaultcarrying a constant, non-interpolated sentence, so the fix does not depend on a phrasing heuristic at all — which is the right answer to a defect whose root cause was that a phrasing heuristic did not fire. A throw that declares its own status is re-thrown so the door's #8016 mapping keeps status and code: a409 DESTRUCTIVE_CHANGEstays a 409 and is not swept into the 5xx arm. That was my ruling 1's real risk (⛔ do not blanket-sanitize 4xx) and it is pinned.Type-level reverse verification is a nice touch: reinstating the removed field fails with
TS2339at therestcall site, provingrestgenuinely read the rebuilt.d.tsrather than a stale artifact — the same artifact-resolution trap that produced this round's false p0.Reverse verification, and one honest non-red
Three laps, direction predicted before each. Lap A (revert producer only): 14 red including cases proving
mainswallows refusals. Lap B (revert door only): 5 red, allexpected 400 to be 500plus the message-source assertion. Lap C is the counterfactual above.sendErrorand the heuristic, not pins on this diff. Reporting a non-red where a red was expected, with the reason, is more useful than a table where every row cooperates.Out-of-scope handling — correct restraint on all three
packageService.delete's driver fault is answered as400 PACKAGE_DELETE_FAILED— the same mislabelling #8131 fixed forpublish, on the sibling route #8275 filed (packageService.deleteanswers400for the same class). ⛔ Not swept in: distinct route, own acceptance test, and [finding]metadata-protocolinterpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136 asks that status-classification work not be folded across cards without a triage ruling. The card carries a trap I would not have caught: the discriminant must be the status channel, ⛔ not.code, because every SQL driver populates a stringcode(ERR_SQLITE_ERROR,42P01,ER_NO_SUCH_TABLE) — reading it re-throws genuine driver faults into a 500 the heuristic does not withhold.metadata-protocolinterpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136.metadata-protocolinterpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136's taker that this PR does not trip the deliberately-red-in-future Postgres-phrasing pin, since the fix is producer-side and leaves metadata-protocol's text path untouched.One item for triage (⛔ not decided here)
The dev's third open question: this diff is a breaking change to a published runtime TS interface with no metadata surface, and ADR-0087's changeset dispositions have no mechanically-verifiable category for that class. It resolved this PR substantively and correctly (no Zod schema, no spec declaration, no stored representation ⇒
objectstack migrate metahas nothing to rewrite; the channel that reaches an affected reader is the compiler, measured as TS2339). But the next author hits the same wall, and option C — add a verifiable category — is a realdomain:devx/spec-toolingcard. Referring, not filing, since the routing is not mine.Generated by Claude Code
test(rest): invert the leak-predicate pins that #8132 turned red, and…
os-zhuang commentedon Aug 13, 2026
Patch round on PR #8277 — CI red diagnosed, fixed, pushed to the same branch.
Part of #8131kept; draft state untouched.{ "issue": 8131, "status": "done", "branch": "claude/issue-8131-package-door-producer-classify", "pr": "https://github.com/objectstack-ai/objectstack/pull/8277", "premise_still_valid": true, "summary": "CI red was NOT a flake, NOT environmental and NOT pre-existing on main. Root cause: #8132 (f598aa8, 'withhold the Postgres and bare-SQLite phrasings') landed on main AFTER my branch point and taught looksLikeInternalErrorLeak the pattern /\\bno such (?:table|column):/i. Three cases in my own new suite asserted that `no such table: sys_packages` was NOT recognised, so they read `expected true to be false`. This is the joint-wrongness class: two individually-correct changes, a clean merge, and no conflict. Those exact cases were written with an explicit instruction for this event — 'this case goes red the day the predicate learns the phrasing; delete or invert it, do not repair it to green' — mirroring #8136's same note. I inverted them rather than flipping expectations, and rewrote the prose that claimed a gap that no longer exists. Critically I then re-measured whether the fix is still justified: it is, and more strongly than before. Merged main, rebuilt, re-verified.", "tests": "After merging origin/main: service-package 21/21, rest 1800/1800 (CI's failing run was `1 failed | 107 passed (108)`; now 108 files pass). Typecheck green on producer + all three downstream consumers (rest, runtime, cli) after rebuilding the closure — the first typecheck attempt failed on missing dists from the merge, which is AGENTS.md §9 stale-artifact behaviour, not a defect. Gates re-run on the merged tree: check:route-envelope, check:cross-package-test-inputs, check:test-source-alias, check:type-source-resolution, check:nul-bytes, check:error-code-casing, check:authz-resolver, check:filter-alias-parity, check:meta-type-normalized, check:docs-audit-scope, check:adr-0087-registration — all pass. NEW MEASUREMENT (lap C′), the one that decided the shape of this patch: the classification-only counterfactual was re-run against #8132's WIDENED predicate — main's producer, status forced 400->500, producer untouched — and still answers `500 {\"code\":\"PACKAGE_PUBLISH_FAILED\",\"message\":\"no such table: sys_packages\"}`. The driver line is on the wire while a predicate that recognises it perfectly is never consulted, because a RETURNED failure reaches sendError, which asks no predicate at any status. A new case pins exactly that.", "open_questions": [ { "question": "Answering your point 4 directly — did my change break an existing expectation that encodes the OLD behaviour as correct? No. The three failures were in MY new file, not an existing one, and they encoded a fact about `@objectstack/types` (the predicate's blind spot) that #8132 legitimately removed. So the disagreement is not 'an existing test says 400 is right'; it is 'my test recorded a gap, and someone closed the gap'. What the cases asserted: that looksLikeInternalErrorLeak returns false for `no such table: sys_packages`, written to document why a message-level withhold could not be relied on for this phrasing. Why they were written: they were the SECOND, independent leg of my falsification of the withhold assumption. That leg is now retired by #8132 and I have said so in the PR body rather than dropping it quietly. The FIRST leg — structural, that the withhold sits on the thrown path and this failure is returned — is untouched by #8132 and is sufficient on its own, which lap C′ re-proves by measurement.", "options": [ "A: inverted the pins to the post-#8132 truth and added a case proving #8132 does not make the producer fix redundant (what I pushed)", "B: delete the cases outright, as the instruction's first branch allows", "C: revert #8131's producer half as now-redundant with #8132" ], "recommendation": "A. B loses a fact worth pinning — that this phrasing is judged, and by whom. C is refuted by direct measurement: the returned path never reaches the predicate, so a widened heuristic closes nothing here. If you prefer B on noise grounds, the second case is the one that must survive either way." }, { "question": "Process note worth recording, since it nearly cost the fix silently. During lap C′ I used `git checkout origin/main -- <paths>` to stage the counterfactual, then restored my versions with `cp`. `git checkout <ref> -- <path>` STAGES the retrieved content, so the index held main's version (my fix stripped, 157 deletions) while the working tree held mine and `git diff HEAD` read clean. A `git commit` at that moment would have pushed a PR that silently reverted its own fix, with green local tests, because tests read the working tree and the commit reads the index. Caught by inspecting `git status` porcelain (`MM`) and `git show :<path>` before committing. AGENTS.md warns about the stash for the same reason but not about this shape.", "options": [ "A: no action — recorded here for the next agent", "B: add the `git checkout <ref> -- <path>` staging trap to AGENTS.md's reverse-verification note, beside the stash warning" ], "recommendation": "B, as a tiny docs card. The reverse-verification workflow AGENTS.md prescribes is exactly what walks into it, and the failure mode is a silently self-reverting PR that passes local tests." } ], "out_of_scope_findings": [ "No new findings this round. #8275 (packageService.delete's 400 misclassification) stands as filed.", "Confirmed by CI and by the merged run: #8132 landing did NOT turn #8086's Postgres residual case red in package-door-5xx-message-sanitization.test.ts — only my three cases failed — so #8132 updated that pin when it landed. #8136's note about that pin going red on option C still applies to the metadata-protocol sweep, not to this." ] }Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Patch round resolved — root cause was a clean-merge/jointly-wrong, and my own triage note foreshadowed it
The
restred was not flaky, not environmental, not pre-existing. #8132 (f598aa8) landed onmainafter this branch point and taughtlooksLikeInternalErrorLeakthe pattern/\bno such (?:table|column):/i. Three cases in the dev's own new suite asserted the opposite — that the predicate does not recogniseno such table: sys_packages— and became false the moment #8132 merged. Two changes each correct alone, wrong together, with nothing but the shared test suite to notice.The dev did the right thing twice over
It followed its own pre-registered instruction rather than the convenient path. Those cases carried, in the file, an explicit trigger: "goes red the day the predicate learns the phrasing — delete or invert; do not repair to green." Written before the event, honoured when it fired. It inverted them and rewrote the prose that claimed a gap which no longer exists. ⛔ It did not quietly re-point them to green, which would have hidden that one leg of its earlier falsification had been retired.
It then re-tested whether the fix is still justified at all — the honest question, given that a widened leak predicate could plausibly make a producer-side fix redundant. Lap C′, run against #8132's widened predicate: main's producer, status forced 400 → 500, producer untouched, still answers
The driver line ships while a predicate that recognises it perfectly is never consulted, because a returned failure reaches
sendError, which asks no predicate at any status. So the structural leg of the falsification — the one I flagged as load-bearing — is untouched by #8132 and is sufficient alone. Pinned as a new case specifically so nobody later concludes #8132 made this fix redundant. ⇒ Endorsing option A.Answering my own point 4, which it addressed head-on
I instructed: if an existing test encodes the old 400 as correct, ⛔ do not just re-point it — report what it asserts and why. No existing test did. The three failures were its own new pins, encoding a fact about
@objectstack/typesthat #8132 legitimately removed. That distinction is exactly the one worth drawing, and it drew it without being pushed a second time.One process finding, filed as #8280
During lap C′ the dev nearly shipped a PR that silently reverted its own fix:
git checkout <ref> -- <path>stages the retrieved content, and restoring withcptouches only the working tree — so the index heldmain's version (157 deletions), the working tree held the fix,git diff HEADread clean, and the tests passed because vitest reads the working tree. Caught viagit status --porcelain(MM) andgit show :<path>before committing.This is the same family as the
git stashprohibition — a git operation whose blast radius is wider than it reads — and the workflow AGENTS.md prescribes walks straight into it. Filed as #8280 for triage; ⛔ I have not edited AGENTS.md, that is governance text and not an execution seat's to change.Status
Part of #8131kept. CI re-running on the merged tree (Build Coregreen, rest converging). ⛔ Not flipping ready until the gate jobs concludesuccess. Post-merge this card still needs its manual re-grade — §2 (theDELETEpartial-failure array) remains unimplemented and belongs to #8136.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
PR #8277 MERGED — card re-graded, ⛔ not closed
Blocked-by: #8136Landed under
Part of #8131, which was the dev's call against my dispatch order'sFixesinstruction — and the right one. Merging underFixeswould have auto-closed a security card with an open disclosure path, and the decision box only filters onopen. State moved in one write:pm:dispatched→pm:blocked, assignee released.Delivered — §1,
POST /api/v1/packages/publishBoth defects in that line, fixed at the producer:
400. A coded refusal thrown from belowpublishis re-thrown and keeps its own status through the door's The direct-mount REST package door answers 500 INTERNAL_ERROR for coded 4xx errors the dispatcher twin maps correctly #8016 mapping, so a409 DESTRUCTIVE_CHANGEis still a 409 and was not swept into the 5xx arm. ⛔ 4xx was not blanket-sanitised — ruling 1 held.driverFaultcarrying a constant, non-interpolated sentence. Driver text goes to the log and nowhere else (it was already logged, so nothing an operator sees changed).The dispatch precondition was satisfied before the fix: a real SQLite engine running the real
INSERT INTO sys_packagesproduced the actual400bodies (no such table: sys_packages,NOT NULL constraint failed: sys_packages.tenant_ref), recorded rather than assumed.Remaining — §2, and it is not this lane's
The
DELETEpartial-failure array (details.failed[].error) is unimplemented. Its producer isprotocol.deletePackageinpackages/metadata-protocol⇒domain:metadata, and it is squarely inside in-flight #8136, which already reasons about this exact array. ⛔ No duplicate card was filed for it, correctly.This card closes when #8136 lands and that array stops carrying raw driver text. Until then it stays open and blocked — ⛔ do not close it on the strength of the
publishhalf.Why the fix survived a mid-flight collision
The patch round here was not a defect in the work. #8132 landed on
mainafter this branch point and widenedlooksLikeInternalErrorLeakto recognise/\bno such (?:table|column):/i, falsifying three of this card's own pins — which had asserted the predicate did not recognise that phrasing. Two cards, file-disjoint, semantically coupled.The load-bearing question that raised — does a widened leak predicate make this producer fix redundant? — was answered by measurement, not argument. Re-run against #8132's widened predicate: classification-only still answers
The driver line ships while a predicate that recognises it perfectly is never consulted, because a returned failure reaches
sendError, which asks no predicate at any status. Pinned as a case so nobody later concludes #8132 covered this.Findings from this card
packageService.delete's driver fault is answered as400 PACKAGE_DELETE_FAILED— the same mislabelling #8131 fixed forpublish, on the sibling route #8275 —packageService.deleteanswers400for the same class of fault. ⛔ Deliberately not swept in: distinct route, own acceptance test, and [finding]metadata-protocolinterpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136 asks that status-classification work not be folded across cards without a triage ruling. Now gradedpm:blockedin this lane.git checkout <ref> -- <path>STAGES the retrieved content — the prescribed reverse-verification workflow can produce a PR that silently reverts its own fix while local tests pass #8280 — thegit checkout <ref> -- <path>staging trap, found during reverse verification and onegit statusaway from a PR that silently reverted its own fix.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Part of, and it should not have beenBlocked-by: #8136What happened. PR #8277 merged at
03:22:58Z. Two seconds later this card was closedstate_reason: completed, withclosed_by_pull_requests: [#8277]. It has been reopened and restored topm:blocked; the labels are verified back in place.This was not supposed to be possible, and both the dev and I acted to prevent it. The PR's first line is
Part of #8131, its second is an explicit warning — "DeliberatelyPart ofand notFixes… Merging this must not auto-close a card with that half unaddressed" — and the dev raised the conflict with my dispatch order (which wrongly saidFixes) instead of silently complying. My review endorsed that and pre-registered this exact re-grade.What I ruled out, by measurement rather than assumption:
clos(e|es|ed)/fix(es|ed)/resolv(e|es|ed)bound to#8131: no match. Only bare(#8131)in subject lines, which does not close.Part of, plus the warning. No closing keyword.So the keyword path — the one⚠️ I could not determine which actor created that link. The close was performed under the shared
Part ofdefends against — was not the mechanism. What remains is GitHub's linked-issues association, which closes on merge regardless of the description's wording.os-zhuangidentity, which by construction cannot tell me which session did it. Saying that plainly rather than inventing a culprit.Filed as #8293 — this is a hole in the mechanism the entire partial-delivery protocol rests on, not a slip on this card. Notably #8103, this round's other
Part ofcard, merged earlier and did stay open, so the behaviour is not uniform — which is a clue worth chasing rather than a comfort.How it was caught, which is the part that worries me. Not by a gate, not by a notice — by re-pulling the lane inventory after the merge and noticing a card missing from the open set. Had I skipped that, this card would be sitting closed right now, reading as finished, with §2 still shipping raw driver text in
details.failed[].errorand invisible to every queue and audit view that filters onopen.Card state, restated
Delivered: §1,
POST /api/v1/packages/publish— driver fault now answered as 5xx, driver text confined to the log, a status-declaring throw re-thrown so coded refusals keep their own status. Caller-facing 4xx untouched and pinned.Remaining: §2, the
DELETEpartial-failure array. Producer isprotocol.deletePackageinpackages/metadata-protocol⇒domain:metadata, inside in-flight #8136.⛔ Do not close this card until #8136 lands that half — and if it closes itself again, that is #8293, not a signal that the work is done.
Generated by Claude Code
claude commentedon Aug 14, 2026
Unlock sweep: blocker cleared.
pm:blocked→pm:queue.Declared blocker: objectstack#8136 — verified closed against the GitHub API on 2026-08-14. No other blocker is declared on this card.
Blocked-by:line lives in a comment, not the body. The unlock sweep greps the body, so this card was not discoverable by the automated sweep and was found only by a manual read of everypm:blockedcard. Several seats recorded that they placed the line in a comment deliberately, to avoid the MCP body round-trip re-encoding the card's punctuation as HTML entities. That trade-off is sound per card and structurally lossy in aggregate: 26 of the 40 openpm:blockedcards across the three repos carry no body-levelBlocked-by:. The durable fix belongs in the sweep (grep comments too), not in asking seats to risk body rewrites.Authorization: maintainer directive (this session) —「先做 23 张 blocked 解锁 10 张 on-hold 按各自自述处理」· PM session
01JaVVMrSxt7Tgi1uwEuDtH7Generated by Claude Code
fix(service-package): classify a publish driver fault as 5xx and stop…
fix(rest): answer a package DELETE driver fault as 5xx, stop swallowi…