Skip to content

fix(console): the injection assertion reads the plain runs inside long or escaped describe literals, so a .objectstack-sha pin gets a real witness - #24672

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24663-injection-witness
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24663-injection-witness

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24663
Clause-②: no

The in-build console spec-injection assertion was vacuous at .objectui-sha 12dc7b47 and passed the control bundle (exit 0). The cause is in describeCandidates (scripts/console-spec-probes.mjs): it took only .describe() literals of 32 to 160 characters with no escape. Between objectui's .objectstack-sha f4755a68a797 and this tree, the one describe that changed (the flow runAs text) is over 160 characters, and its em dash is a backslash-u escape (a backslash, then u2014) in the built dist. So both candidate pools came out empty and the stamp recorded freshWitness: null.

The change

  • A second candidate source. A describe literal the old rule skips is cut at every escape sequence and every quote character. Each run of 32 characters or more is now a candidate.

    • Why cut at escapes. An escape can be spelled one way in a spec's dist and another way in a bundle, but the text between escapes is plain characters in both.
    • Why cut at quotes. A bundler may re-quote a string and add a backslash before a quote.
    • Why runs are not cut to 160. That bound was about a whole literal surviving the bundler. Windows of a changed run would also still be contained in the other spec, and pickProbe's substring rule would drop them.
  • Old candidates are unchanged. A literal the old rule accepted is still taken whole, so every candidate it produced is still produced. chooseProbes, the stamp shape and the cache-hit replay (check-console-injection.mjs) are unchanged.

  • The fixture. Self-test battery 16 in scripts/check-console-injection.mjs (16 cases) is shaped like this pin:

    • a .objectstack-sha pin, with the reference as objectui's own build of it;
    • each distinguishing literal over 160 characters and carrying a backslash-u escape (a backslash, then u2014);
    • a shared literal over 160 characters, and a shared run before the escape;
    • an injected bundle where the dash is emitted raw, and a control bundle where the escape is kept.

    It asserts that the stamp carries the changed run as freshWitness, that the assertion exits 1 on the control bundle, and that the good stamp replayed over the control exits 1. The roster floor is 13 → 14.

  • No release workflow and nothing under .github/workflows/** changed. The diff is 2 files under scripts/.

Reverse verification

The fix was committed first. Then the literalSegments call was replaced by a comment, and node scripts/check-console-injection.mjs --self-test exited 1 with 8 failures. Among them: the control bundle "expected 1, got 0", the witness "expected … got null", and the replay "expected 1, got 0". Restored with git restore --source=HEAD, the porcelain was clean and the self-test exited 0 with 100 assertions.

Proof on a real build (pin 12dc7b47c340)

PR #24660 has not landed, so .objectui-sha on main is still 4016370af3c9. The build ran in a throwaway worktree: this branch at 50f12801 merged locally with #24660's head 0b45222fef. Nothing from that merge is pushed. I will merge main into this branch once #24660 lands.

The build ran turbo run build --filter=@objectstack/client... --concurrency=2 && bash scripts/build-console.sh, which is what pnpm objectui:build runs, and it exited 0. It took the .objectstack-sha branch, built objectui's own @objectstack/* at f4755a68a797 as the reference (.stamp.json: source: pin, sha f4755a68a797…), and packed this tree in through OBJECTSTACK_ROOT. Its log:

✓ Console bundle carries THIS tree's @objectstack/spec, and only it (vendored: @objectstack/spec of objectui's own build of objectstack f4755a68a797).
    present (injected only): " system flows keep system privileges; after an approval decision their attribution user is the decider; user = the human whose act is running this segment: the trigger user until a human decision, the decider after it (RLS-respecting; the nodes after an approval decision run with the decider"
      — 3 of 3 injected-only descriptions are in the bundle
    absent  (vendored only): " not yet rendered] The PARENT record this related list hangs off, bound by a query of its own: an object binding "
      — and all 2 pin-only descriptions are absent

The stamp records skew: true, vendoredPin f4755a68a797…, freshWitness the run quoted above (was null), and staleDetector the pin-only run.

reading before (main's probes) this branch
candidate pools at this pin (injected-only / pin-only) 0 / 0 3 / 2
pnpm check:console-injection --require-stamp on the injected dist 0 ("nothing assertable") 0, the witness present and the detector absent
assert-console-spec-injection.mjs on the control bundle 0, "no observable skew" 1, "still carries the @objectstack/spec of objectui's own build of objectstack f4755a6"
the injected stamp replayed over the control dist (check-console-injection.mjs --dist … --require-stamp) n/a (null witness) 1
  • The control bundle is objectui's own build of the pin, with nothing from this tree, built in the same build tree with OBJECTSTACK_ROOT unset (objectstack-pin.mjs status: "built at the pin f4755a6"). The bundle has no stamp.
  • Grep counts over each dist's assets/*.js (grep -lF): the full witness is in 1 injected asset and 0 control assets; the full detector is in 0 injected assets and 1 control asset.

Gates

  • node scripts/pm/check-governed-merges.mjs --branch claude/issue-24663-injection-witness: NOT governed; 134 changed lines.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 35 commands, and all 35 exited 0. The --ran reconciliation reports "35 run, 0 NOT-MEASURED".
  • pnpm exec eslint on both files exited 0.
  • No changeset: the diff publishes nothing from any released package (repo tooling under scripts/), as with PR fix(release): judge a .objectstack-sha console bundle against objectui's own build of that commit #24532. Label skip-changeset.
  • Not measured:
    • The Console Pin Gate does not run on a PR or in the merge queue. Its first CI run on this change is main's battery after the merge.
    • The 15 pending-changeset families do not apply without a changeset.

Acceptance notes

  • The cache-hit replay labels the stale line "absent (published only)" even when the stamp's reference is a pin build. That label is unchanged here.
  • The issue's other suggested source, the metadata-type registry description: text (for example the sharing_rule entry), is not added. The describe runs alone give a non-null witness at this pin.

🤖 Generated with Claude Code


Generated by Claude Code

… escaped describe literals

describeCandidates skipped every describe literal over 160 characters or
carrying an escape. At a pin whose reference differs from this tree only in
such literals (objectui 12dc7b47 against objectstack f4755a6: the flow
runAs text) both pools were empty, the stamp recorded freshWitness null, and
the assertion passed the control bundle. A skipped literal is now cut at each
escape and quote, and each run of 32 characters or more is a candidate.
Self-test battery 16 pins the pin's shape.

Claude-Session: https://claude.ai/code/session_019oG4xEYrGxr5n2cdSNbWMK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling labels Oct 19, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat review · TIER_DEFAULT (in-seat) · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-19T22:35Z

Head 50f12801. The change is in 2 files under scripts/ (+130 / -4). It touches no workflow, no release act, and no governed path.

  • describeCandidates only gains candidates (scripts/console-spec-probes.mjs). A literal the old rule accepted (32–160 characters, no escape) is still taken whole. A literal it skipped is now cut at escape sequences and quote characters, and each plain run of 32 characters or more becomes a candidate. Every probe that existed before is still produced. Two cuts are explained in the docblock, and both are right:
    • quotes are cut because a bundler re-quotes strings;
    • runs are not windowed, because a window would break pickProbe's substring rule.
  • Self-test. Battery 16 (16 cases) is shaped like the 12dc7b47 pin. Reverse-verified: with the new source disabled, 8 cases fail, including the control's "expected 1, got 0".
  • Real build at pin 12dc7b47, merged locally with chore(console): move the objectui pin 4016370af3c9 → 12dc7b47c340, regime C for sharing rules (objectui#13050) #24660:
    • the stamp's freshWitness is non-null (the flow runAs run), with 3/3 injected-only probes present and 2/2 pin-only probes absent;
    • main's probes reproduce the defect, exiting 0 on the control bundle, and this branch's probes exit 1;
    • check:console-injection --require-stamp on the injected dist exits 0 (os-dev-report 6278854813).
  • Gates: 35/35 derived gates exit 0, ESLint 0, not governed.

VERDICT: ACCEPT. The PR shares no file with #24660, so it lands on its own. The seat sets it ready with auto-merge on.

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

2 participants