Skip to content

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

Closed

Description

@hotlong

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:

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

  1. added theissue type on Aug 12, 2026
  2. hotlong commented on Aug 12, 2026

    @hotlong
    ContributorAuthor

    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) publish swallowing a driver error and returning (error as Error).message as caller-visible data, (b) a driver fault answered as 400. The fix direction the card itself argues, adopted: classify correctly (driver fault through publish is 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). The DELETE partial-failure details.failed[].error array needs the same producer-side treatment since no message-level withhold reaches details.

    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_packages failure and record the actual 400 body before fixing.

    Size/model suggestion: M, opus.


    Generated by Claude Code

  3. self-assigned this
    on Aug 13, 2026
  4. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 12
    Session: session_01ARidKDYSCD56LaygrvDPnk
    Branch: claude/issue-8131-package-door-producer-classify
    Worktree: objectstack-issue-8131
    Domain: domain:services
    File 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: opus
    Serial constraints cleared: no in-flight claim touches service-package or package-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_secret read path) do not intersect this surface.

    Note on the surface: packages/rest/src/package-routes.ts is nominally domain:cli territory, 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 inside packages/rest beyond the status/sendError call for these two handlers, that is a breach — stop and report.


    Generated by Claude Code

  5. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor
    {
      "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

  6. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Review of PR #8277 — ACCEPT, pending CI convergence

    ⚠️ First, two corrections against myself. Both are mine, and the dev was right on both.

    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:

    • Structurally — the withhold lives in sendThrownError. A returned failure reaches sendError directly and never meets the withhold at any status.
    • 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 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 DELETE partial-failure details.failed[].error) is unimplemented — its producer is protocol.deletePackage in packages/metadata-protocol, outside the declared file surface and squarely #8136's. Merging under Fixes would have auto-closed a security card whose second disclosure path is still open, and the decision box only filters on open. The standing Part 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: keep Part 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:dispatched to pm:blocked with Blocked-by: #8136 and a comment naming what shipped, what remains, and whose it is.

    Verification

    Check Reading
    Path face (get_files) 6 files, no docs/adr/** · .claude/skills/** · skills/** · content/docs/releases/** ⇒ normal landing path
    Declared surface nothing outside service-package/** + the package-* files in rest ✅
    The cross-lane reach into packages/rest one sendError call — status 400→500 and message source result.error → result.driverFault?.message. The other ~28 lines are comment. This is the narrow mapping the order allowed, ⛔ not a deeper change in another lane
    Dispatch precondition satisfied first, as required — real SQLite engine, real INSERT INTO sys_packages, actual 400 bodies recorded before any fix

    The producer returns a discriminated driverFault carrying 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: a 409 DESTRUCTIVE_CHANGE stays 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 TS2339 at the rest call site, proving rest genuinely read the rebuilt .d.ts rather 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 main swallows refusals. Lap B (revert door only): 5 red, all expected 400 to be 500 plus the message-source assertion. Lap C is the counterfactual above.

    ⚠️ Worth noting because it is the kind of thing usually massaged: in Lap B, §3/§4 stayed green, and the dev reported that as-observed rather than forcing it to the template's red presumption — those cases are measurements about sendError and 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

    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 meta has 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 real domain:devx / spec-tooling card. Referring, not filing, since the routing is not mine.


    Generated by Claude Code

  7. added a commit that references this issue on Aug 13, 2026
  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Patch round on PR #8277 — CI red diagnosed, fixed, pushed to the same branch. Part of #8131 kept; 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

  9. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Patch round resolved — root cause was a clean-merge/jointly-wrong, and my own triage note foreshadowed it

    The rest red was not flaky, not environmental, not pre-existing. #8132 (f598aa8) landed on main after this branch point and taught looksLikeInternalErrorLeak the pattern /\bno such (?:table|column):/i. Three cases in the dev's own new suite asserted the opposite — that the predicate does not recognise no 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.

    ⚠️ This was foreseeable from the dispatch order and I did not price it in. Triage's own note on this card named #8132 as a sibling in flight, and my order repeated it as "file-disjoint, do not edit their surfaces". File-disjoint they were — but they were not semantically disjoint: one card's pins encoded a fact about the other card's predicate. File-surface disjointness is the wrong test for that, and I applied it as if it were sufficient. The lane's serial rules already say the thing relay hand-offs pass along is semantics, not text; I did not extend it to parallel cards in other lanes.

    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

    500 {"code":"PACKAGE_PUBLISH_FAILED","message":"no such table: sys_packages"}
    

    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/types that #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 with cp touches only the working tree — so the index held main's version (157 deletions), the working tree held the fix, git diff HEAD read clean, and the tests passed because vitest reads the working tree. Caught via git status --porcelain (MM) and git show :<path> before committing.

    This is the same family as the git stash prohibition — 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 #8131 kept. CI re-running on the merged tree (Build Core green, rest converging). ⛔ Not flipping ready until the gate jobs conclude success. Post-merge this card still needs its manual re-grade — §2 (the DELETE partial-failure array) remains unimplemented and belongs to #8136.


    Generated by Claude Code

  10. removed their assignment
    on Aug 13, 2026
  11. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    PR #8277 MERGED — card re-graded, ⛔ not closed

    Blocked-by: #8136

    Landed under Part of #8131, which was the dev's call against my dispatch order's Fixes instruction — and the right one. Merging under Fixes would have auto-closed a security card with an open disclosure path, and the decision box only filters on open. State moved in one write: pm:dispatched → pm:blocked, assignee released.

    Delivered — §1, POST /api/v1/packages/publish

    Both defects in that line, fixed at the producer:

    • Status — a driver fault is now a 5xx, not a 400. A coded refusal thrown from below publish is 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 a 409 DESTRUCTIVE_CHANGE is still a 409 and was not swept into the 5xx arm. ⛔ 4xx was not blanket-sanitised — ruling 1 held.
    • Disclosure — the producer returns a discriminated driverFault carrying 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_packages produced the actual 400 bodies (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 DELETE partial-failure array (details.failed[].error) is unimplemented. Its producer is protocol.deletePackage in packages/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 publish half.

    Why the fix survived a mid-flight collision

    The patch round here was not a defect in the work. #8132 landed on main after this branch point and widened looksLikeInternalErrorLeak to 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

    500 {"code":"PACKAGE_PUBLISH_FAILED","message":"no such table: sys_packages"}
    

    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.

    ⚠️ That collision was foreseeable from triage's own sibling note and I priced it as file-disjointness, which is the wrong test for it. Filed as #8281 against the lane protocol, not against this work.

    Findings from this card


    Generated by Claude Code

  12. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    ⚠️ REOPENED — this card was auto-closed on merge despite Part of, and it should not have been

    Blocked-by: #8136

    What happened. PR #8277 merged at 03:22:58Z. Two seconds later this card was closed state_reason: completed, with closed_by_pull_requests: [#8277]. It has been reopened and restored to pm: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 — "Deliberately Part of and not Fixes… Merging this must not auto-close a card with that half unaddressed" — and the dev raised the conflict with my dispatch order (which wrongly said Fixes) instead of silently complying. My review endorsed that and pre-registered this exact re-grade.

    What I ruled out, by measurement rather than assumption:

    • Merge commit message — scanned in full for 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.
    • PR description — read in full. Part of, plus the warning. No closing keyword.

    So the keyword path — the one Part of defends against — was not the mechanism. What remains is GitHub's linked-issues association, which closes on merge regardless of the description's wording. ⚠️ I could not determine which actor created that link. The close was performed under the shared os-zhuang identity, 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 of card, 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[].error and invisible to every queue and audit view that filters on open.

    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 DELETE partial-failure array. Producer is protocol.deletePackage in packages/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

  13. claude commented on Aug 14, 2026

    @claude
    Contributor

    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.

    ⚠️ This card's 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 every pm:blocked card. 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 open pm:blocked cards across the three repos carry no body-level Blocked-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 01JaVVMrSxt7Tgi1uwEuDtH7


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions