Skip to content

fix(lint,metadata-protocol)!: /meta refuses a secretless api flow, and the runtime authoring gate reads a withheld-and-stored secret as present (#20611) - #20692

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20611-gate-reads-redaction-context
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20611-gate-reads-redaction-context

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20611
Clause-②: yes (narrowing)

The declaration above is copied verbatim from the dispatch claim. H2 measured a published-surface widening in @objectstack/lint (below), and the claim says its value is then amended to yes. The changeset already declares yes with the narrowing arm. Whether this line's value becomes yes is the seat's call.

What changes

flow-api-trigger-secret-missing now runs on the runtime metadata publish gate as well as on os validate / os build / os lint. The gate is handed the redaction context, so a signed flow's ordinary round trip still passes.

  • Why the gate needed context. The flow read path withholds the start node's config.secret from every served definition ([security] a flow's inbound-hook secret (config.secret on the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552). So a body saved back after a read arrives without it. saveMetaItem restores the stored secret in carryForwardRedactedCredentials, deliberately AFTER every gate, so that no gate handles a restored credential. At the gate, a withheld secret and a missing one looked the same.
  • The fix, per triage 5886067949. The carry-forward is not moved. saveMetaItem computes the POSITIONS the carry-forward will fill from the stored row. It uses the same stored body and the same plan, and it grafts nothing. Those positions go to the gate. The rule reads a withheld-and-stored secret as present, and one that is absent and not stored as missing.
  • The refusal. A secretless api flow saved active at /meta (or a draft of one, at publish) now gets 422 INVALID_METADATA, with the issue flow-api-trigger-secret-missing at the start node's config.secret. Nothing is stored. Before this change it was stored, and the engine refused it at registration.

Where it lands

  • packages/metadata-protocol/src/metadata-redaction.ts: carryForwardRedactedValues is refactored onto one internal plan (planCarryForward), unchanged in behaviour. The new internal redactedPathsCarriedForward(type, incoming, stored) answers where that plan lands a value: the positions only, relative to the incoming body. It is not on the package entry.
  • packages/metadata-protocol/src/protocol.ts:
    • saveMetaItem passes the gate a lazily resolved restoredCredentialPaths. It is resolved only after the gate's early returns, so a draft save, the package-author channel and migrate-stored pay no read.
    • restoredCredentialPathsFor reads the stored body through the carry-forward's own reader. That reader is factored out as storedBodyForCarryForward: the active row, else the code layer.
    • The read is not wrapped in the collections' best-effort guard. A failed stored-row read fails the save, as the carry-forward's own read already does.
  • packages/metadata-protocol/src/runtime-authoring-gate.ts: evaluateRuntimeAuthoringGate (internal) forwards the positions.
  • packages/lint/src/runtime-gate.ts: runRuntimeAuthoringRules accepts the item-relative dotted positions (nodes.1.config.secret). It re-spells them against the written item's place in the candidate snapshot (flows[0].nodes[1].config.secret), reading [n] versus .key off the item itself. It hands them to the rules as AuthoringRuleContext.restoredCredentialPaths.
  • packages/lint/src/authoring-rules.ts: the validateFlowApiTriggerSecret entry becomes CLI_AND_RUNTIME with runtimeTypes: ['flow'], and the surfaceReason naming this card is gone. AuthoringRuleContext gains the optional restoredCredentialPaths. runAuthoringRules (the CLI) never forwards it.
  • packages/lint/src/validate-flow-trigger-readiness.ts: validateFlowApiTriggerSecret(stack, options?) treats a start-node secret position listed in options.restoredCredentialPaths as present.

Dispatch hypotheses, measured

  • H1 held, with the second site measured. The second assertRuntimeAuthoringRules call site is promoteDraftForPublish, the publish and publish-package-drafts door. It judges the STORED draft row, which the draft's own save already carried forward (the draft carry-forward falls back to the active row, then the code layer). So it needs no context.
    • Pinned both ways: "a DRAFT save then a publish carries the secret into the active row" stays green with the rule live. A secretless api draft saves, and its publish is refused with no active row.
  • H2 held: a published surface widens. AuthoringRuleContext and runRuntimeAuthoringRules are on @objectstack/lint's root entry (runRuntimeAuthoringRules also on ./runtime). Both gain an optional field. The exported validateFlowApiTriggerSecret gains an optional second parameter. @objectstack/metadata-protocol's entry is unchanged: redactedPathsCarriedForward and evaluateRuntimeAuthoringGate are not exported from it.
  • H3 held. The positions are computed from the redactor's paths and the stored body, by the carry-forward's own plan. The graft is not re-run early.
  • H4 held. The gate answers 422 / INVALID_METADATA (ADR-0112), the same envelope as the other gating runtime rules (protocol.runtime-authoring-gate.test.ts). The new pins assert code + status + the rule id and path.

Pin sweep

Tests

All figures are at 27173c26cc unless noted.

  • @objectstack/metadata-protocol, full suite: 189 files passed, 3 skipped; 2789 tests passed, 19 skipped.
  • @objectstack/lint, full suite: 115 files, 5382 tests passed. validate-flow-trigger-readiness.test.ts: 90/90.
  • Typecheck: @objectstack/lint (tsc --noEmit plus check:test-typecheck, test layer OK) and @objectstack/metadata-protocol (tsc --noEmit, 192 test files in its program) are both green.
  • Downstream: @objectstack/objectql's 39 test files that call saveMetaItem / publishMetaItem: 506/506, against the rebuilt metadata-protocol dist.
  • ESLint (--no-inline-config --format json) on the 8 touched .ts files: 8 files, 0 errors, 0 warnings.
    • The config's files glob **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} covers all of them.
    • eslint.config.mjs enables no type-aware linting (no parserOptions.project, no typed rules). So this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint is CI's.

Ablations (committed first; scripts/ablation-replace.mjs, restored with blob equal to HEAD and git diff HEAD empty)

  1. protocol.ts, the gate no longer receives the context (restoredCredentialPaths: renamed to restoredCredentialPathsAblated: at the saveMetaItem call). The anchor went 1 to 0 and the blob e3825069c5df became 57762308e583.
  2. validate-flow-trigger-readiness.ts, the rule ignores the set (&& false &&). The anchor went 1 to 0 and the blob fd5bd29b1e00 became 40aa85516a7b.
    • Red, 3 of 90: the three new lint pins that read restored positions.
    • Source-resolved: lint's tests import ./validate-flow-trigger-readiness.js directly, so no dist/ was involved.
  3. Reverse type check. runtime-authoring-gate.ts passed restoredCredentialPaths: 42 into runRuntimeAuthoringRules, and tsc --noEmit in metadata-protocol failed with TS2345 against the REBUILT @objectstack/lint .d.ts. Restored.

Gates

  • node scripts/pm/dispatch-gates.mjs --commands at 27173c26cc derived 63 families. --ran reconciled them: 61 run with exit 0, 2 NOT MEASURED, 0 unrun.
  • NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (no dist/ for 59+ packages; the gate needs a whole-repo pnpm build).
  • NOT MEASURED: check:type-check-debt, reason: PREREQUISITE NOT MET (18 ledgered packages' dependency closures are not built).
  • check:lean-entry-closure first exited 3 (no objectql dist/). After pnpm --filter @objectstack/objectql build it passed.
  • check-adr-0087-registration judged the changeset: [BREAKING+clause-②-narrowing] not-required (no-migration-prescription).

Acceptance notes

  • Boundary, per the ruling. "Withheld and stored" reads as present whatever the stored value is. The real flow projection withholds any start-node secret except the empty string. So a legacy row whose stored secret is whitespace-only or a non-string passes the gate on a round trip, and the engine still refuses it at registration. Judging the stored VALUE would hand the gate a restored credential, which triage ruled out.
  • Cost. One more indexed sys_metadata read on an active save of a type with a registered redactor (datasource built in; flow once the automation plugin registers its projection). Nothing for other types or for drafts.
    • The gate's read and the carry-forward's read are separate on purpose: the carry-forward's stays immediately before the put. A write racing between them could leave the gate's positions one version behind the carry-forward's.
  • Finding for the seat (class c, not filed here). skills/objectstack-automation/SKILL.md gives two stale instructions:

Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/metadata-protocol, touching 19 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object), runtimeTypes (symbol, a field of const object AUTHORING_RULES), saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/cluster.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/authorization.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

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.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 14f80e23957165f6fb23c2b3d59bdc7668652dfb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 34fbb9c3a1b913062063c28bb4013d21e2bd0ac8 — the merge of head 27173c26cc49c590d8c88ecfb5cc438201577a78 into base 14f80e23957165f6fb23c2b3d59bdc7668652dfb, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 34fbb9c3a1b913062063c28bb4013d21e2bd0ac8 && git checkout 34fbb9c3a1b913062063c28bb4013d21e2bd0ac8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 14f80e23957165f6fb23c2b3d59bdc7668652dfb 27173c26cc49c590d8c88ecfb5cc438201577a78 && git checkout -B drift-repro 14f80e23957165f6fb23c2b3d59bdc7668652dfb && git merge --no-ff 27173c26cc49c590d8c88ecfb5cc438201577a78

node scripts/docs-audit/affected-docs.mjs --json 14f80e23957165f6fb23c2b3d59bdc7668652dfb

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 14f80e23957165f6fb23c2b3d59bdc7668652dfb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 27173c26cc49c590d8c88ecfb5cc438201577a78
Local-runs: none

Inputs: card #20611 (body; comments 5886067949 triage, 5894817672 claim, 5895749311 os-dev-report, 5895775380 amended claim), PR #20692 (body, 9-file list, net diff against main, merge base 6981abfd26, base 14f80e2395), and the 38 check-runs on the head, read at 2026-09-29T18:12Z. Nothing built, run or re-run; every derived gate family is answered by a check-run below.

① Derived judgments

Accept set, runtime /meta write door (@objectstack/metadata-protocol).

  • A1 NARROWING, right. An active save of an api-bound flow whose start node has no usable config.secret (absent, blank, non-string) is refused 422 / INVALID_METADATA, issue flow-api-trigger-secret-missing at flows[N].nodes[i].config.secret, nothing stored. Carried by the AUTHORING_RULES entry validateFlowApiTriggerSecret moving to CLI_AND_RUNTIME with runtimeTypes: ['flow']; pinned in protocol.metadata-redaction.test.ts (fresh api flow, and a stored row that never had a secret). Triage direction met: the engine already refused this shape at registration (service-automation validateApiTriggerSecret, in its ## 17.5.0 changelog), so the door now refuses what was dead on arrival.
  • A2 NARROWING, right. The publish of a secretless api draft (promoteDraftForPublish, reached by publishMetaItem and publishPackageDrafts) is refused the same way; the draft row stays. Pinned ("the draft door is not a bypass"). D1 posture kept: the draft save itself is still admitted.
  • A3 NARROWING, right. A round trip that sends an explicit secret: '' is refused: the projection serves '' as written (FLOW_CREDENTIAL_CLEARED = ''), so the plan does not carry the stored value over it and the rule reads a blank. Pinned; the stored secret stays at rest.
  • A4 NARROWING, right, and the one pin flip. #20590's relocation pin saved a relocated api flow whose NEW start node typed no secret, which is shape A1 (the carry-forward drops the old secret at the changed-kind node, position 3 of [security] closeout: two stored-credential positions outside #20552's projection still reach a served read unredacted (a flow http node's signingSecret; the /meta list read's raw fallback) #20590). The subject is kept with a typed secret on the new start node plus at-rest assertions, and a twin pin asserts the refusal at flows[0].nodes[2].config.secret with the row equal to the seed. Right flip, and the only one the repo-wide grep the PR describes could have found.
  • A5 UNCHANGED, right. A signed flow's GET → edit → PUT is admitted and keeps its secret: restoredCredentialPathsFor reads the same overlay repo (getOverlayRepo(request.organizationId ?? null)), the same ref shape (type singular, name, org: orgId ?? 'env'), the same packageId and state: 'active' as the carry-forward at the put; request.item is not reassigned between the gate call and the carry-forward, so positions are computed against the very body the graft lands in. The two [security] a flow's inbound-hook secret (config.secret on the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552 round-trip pins run in the same describe as the refusal pins, so their green is a verdict. The code-layer fallback is shared through storedBodyForCarryForward, so the first save of a code-authored flow keeps its secret too.
  • A6 UNCHANGED, right. Draft saves, the package-author channel and migrate-stored pay no stored-row read and see no rule: evt.restoredCredentialPaths?.() is invoked inside the Promise.all that follows the three early returns.
  • A7 UNCHANGED, right. os validate / os build / os lint: runAuthoringRules builds its context from sduiManifest and judgeFilter only, so a caller-stated set never reaches a rule; pinned per command.
  • A8 UNCHANGED, right. A legacy row whose stored secret is whitespace-only or a non-string is withheld by the projection (only '' is excepted), so its round trip reads as present at the gate and the engine keeps refusing it at registration. This is what the ruling buys, see ③.
  • A9 UNCHANGED, right. The schema check still judges the author's body first; feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) #8078's inline-credential refusal is untouched (no schema, no packages/spec file in the diff).

Public surface.

  • P1 @objectstack/lint root: AuthoringRuleContext.restoredCredentialPaths?: ReadonlySet of string (optional field). Widening, additive, declared.
  • P2 @objectstack/lint root and ./runtime: runRuntimeAuthoringRules gains optional restoredCredentialPaths?: readonly string[]. Widening, additive, declared.
  • P3 @objectstack/lint root: validateFlowApiTriggerSecret(stack, options?) gains an optional second parameter. Widening, additive, declared.
  • P4 @objectstack/lint root: the exported AUTHORING_RULES entry for validateFlowApiTriggerSecret changes value (surfaces, runtimeTypes, surfaceReason removed). A published-table value change that carries A1; named in the changeset. Right.
  • P5 @objectstack/metadata-protocol entry unchanged: index.ts re-exports carryForwardRedactedValues, hasMetadataRedactor, redactMetadataItem, redactMetadataItems only; redactedPathsCarriedForward and evaluateRuntimeAuthoringGate are not on it (the test imports the former from the module path and says so). saveMetaItem's signature is unchanged; its behaviour is A1, declared BREAKING.

Triage ruling kept: the gate sees positions, never values. redactedPathsCarriedForward returns planCarryForward(...).landed, a string[]; withValueAt copies (spread and slice), so the gated body is never mutated and the grafted out is discarded inside metadata-redaction.ts; evaluateRuntimeAuthoringGate forwards readonly string[]; runRuntimeAuthoringRules re-spells them into a ReadonlySet of string against the candidate (the written item is appended LAST by buildRuntimeWriteSnapshots, same-name baseline entry filtered, [n] spelled only where the item holds an array); the rule does has(secretPath). No credential value crosses into any gate. The carry-forward is not moved (still immediately before repo.put). One wording nit: the PR body's "it grafts nothing" is loose; the plan grafts into a discarded copy to run the redactor's position check. Nothing reaches the gate or the request body, so the ruling holds.

Shipped prose, each factual sentence judged against origin/main and the diff.

  • Refusal message (rule message and hint, unchanged text now shown at a second surface; the diff changes only the path expression): "the automation engine refuses to register this flow, whatever its status, and it never receives a post" TRUE (validateApiTriggerSecret refuses whatever status); "armed only with a per-flow secret that every post is HMAC-verified against (ADR-0041)" TRUE (ADR-0041 flow-trigger family; the 17.5.0 entry says every armed hook verifies); the autolaunched prescription TRUE (mirrors the engine's own message). No tracker number in the runtime string. Gate envelope TYPE/NAME failed author-time validation: ... with code INVALID_METADATA, status 422, issues, rulesRun unchanged, ADR-0112 vocabulary.
  • Changeset .changeset/20611-gate-reads-redaction-context.md: headline TRUE; "packages/spec is untouched, start node config stays the open record" TRUE (file list); "objectstack migrate meta could not rewrite that shape even in principle" TRUE; "the automation engine has refused to register such a flow since 17.5.0, and that load path's disposition is recorded in its own published changelog entry" TRUE (service-automation CHANGELOG.md ## 17.5.0, entry 487a784 for trigger-api arms a flow's inbound hook without a secret and accepts unsigned posts; ADR-0041's trigger-api acceptance criteria name a per-flow secret and HMAC verification #20529 carries its own adr-0087 marker; package.json 17.5.0); "used to be stored; the automation engine then refused to register it (400 on the /automation doors, a skip with a warning at boot)" TRUE (engine docblock and that entry); PUT /api/v1/meta/flow/:name TRUE (client entry documents PUT /api/v1/meta/:type/:name); "now refused ... 422 INVALID_METADATA ... nothing is stored. A draft save is still accepted; its publish is refused the same way" TRUE (pins); the one-line fix TRUE; "Every served flow definition withholds the start node's config.secret" TRUE for the shipped composition (the automation service's registerFlowCredentialRedactor), and the same file later qualifies "flow where the automation plugin registers one"; "the save restores the stored secret only after every gate has run" TRUE; "positions only, never the values" TRUE; "a GET → edit → PUT of a signed flow, and the first save of a code-authored flow whose secret is in the app's source, keep passing and keep their secret" TRUE; "An explicit empty config.secret is the author's own value and is refused as blank" TRUE; the four @objectstack/lint bullets TRUE (P1 to P4, A7); the @objectstack/metadata-protocol paragraph TRUE (one indexed read on an active save of a redactor type, datasource built in and flow when registered, nothing for other types or drafts; carry-forward unchanged; the promotion judges the stored draft row).
  • Docblocks on exported symbols (ship in .d.ts): AuthoringRuleContext.restoredCredentialPaths "Set only by the runtime publish gate ... ABSENT on the three CLI commands" TRUE, "Read by validateFlowApiTriggerSecret only" TRUE (the only readers are the registry entry forwarding it and the rule), "Positions only, never values" TRUE; runRuntimeAuthoringRules.restoredCredentialPaths "the @objectstack/spec/kernel redactor registry's redactedKeys spelling (nodes.1.config.secret)" TRUE (MetadataRedactionResult.redactedKeys is dotted and item-relative; the projection emits nodes.INDEX.config.secret), "Omitted, every position is judged on the body as sent" TRUE; validateFlowApiTriggerSecret "The CLI never sets the option" TRUE, @param spelling TRUE; the stale "Measured on 825c33f ... stays CLI-only" paragraph is removed, so no shipped docblock still calls the rule CLI-only. No hand-written page under content/docs on main names the rule id, so the docs-drift list carries nothing this diff falsifies.

Check-runs on the head at the reading above (38). Success: Build Core (hosts check:dual-build-cjs-loads), Type Check · source gates, Type Check · debt ledger (check:type-check-debt), Dogfood Regression Gate (1/3), Dogfood Verify CLI, Check Documentation Links, Check Changeset (first batch), Governed Surface Queue Guard, Flag docs affected by code changes, filter, Check PR Size, Auto Label, The card this PR closes must claim this branch (both), No other open PR may claim the same issue (both), No other open PR may claim the same single-writer path (both), Part-of PR must not also close its card (both). Skipped: Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in), second-batch Auto Label and Check PR Size. In progress: Test Core 1 to 6 of 6, Dogfood Regression Gate 2/3 and 3/3, Temporal Conformance (live PG + MySQL), Type Check · consumer gates, Type Check · workspace, Lint & Repo Gates, and the second-batch Check Changeset (re-triggered by the PR-body edit that amended line 2). No failure at the reading; the in-progress ones are the dev's measured suites (metadata-protocol 189 files / 2789 tests, lint 115 files / 5382 tests, objectql 506/506) and the changeset and ADR-0087 gates the dev ran locally with exit 0.

② Semver level

  • Changeset present (.changeset/20611-gate-reads-redaction-context.md), no skip-changeset label on the PR (labels: documentation, size/l, tests, tooling). Right: both packages publish.
  • '@objectstack/lint': minor, '@objectstack/metadata-protocol': minor. Right: P1 to P3 are additive widenings on a published surface (at least minor); A1 to A4 are an accept-set narrowing, BREAKING, shipped as minor under the launch-window convention that check-changeset-no-major enforces; the **BREAKING** banner and the one-line fix are in the body.
  • Clause-②: yes (narrowing) in the changeset, on PR body line 2, and on the amended claim 5895775380. The three carriers agree. Legal spelling (yes plus the (narrowing) arm; yes needs at least minor, held). The value is right: the diff both enlarges @objectstack/lint's public surface and narrows /meta's accept set. The original claim's no (narrowing) was conditional on H2 and H2 measured the widening; the seat's amendment answers the dev's open question with option A, and this record agrees.
  • ADR-0087 disposition: one marker, not-required (no-migration-prescription). Right category: nothing authorable is removed, renamed or retyped, so there is no FROM → TO mapping and nothing objectstack migrate meta could rewrite; the "one-line fix" is an authoring instruction (type a secret, or declare autolaunched), not a consumer code migration, the same shape the 17.5.0 entry 487a784 landed under. The dev's local check-adr-0087-registration read it [BREAKING+clause-②-narrowing] not-required (no-migration-prescription); CI's Lint & Repo Gates is the verdict of record and is in progress at the reading.

③ Boundary flags

  • Clause-② open question (dev, options A/B). Answered A by the seat's amended claim 5895775380; PR body line 2 now reads yes (narrowing); changeset agrees. Closed. Cosmetic: the blockquote under PR line 2 ("Whether this line's value becomes yes is the seat's call") is spent prose now that the line was edited; PR-body text, not a shipped surface.
  • Second gate call site promoteDraftForPublish left without the context. Right. It judges draftForGate.body, the stored draft row (read with state: 'draft'), and that row already holds what the draft's own save carried forward: storedBodyForCarryForward for a draft save compares against the draft row, else the active row, else the code layer. So a listed position would name nothing the stored draft lacks. Pinned in the diff (secretless draft saves, its publish is refused, draft row stays). Accepted.
  • Pin flipped for the relocation save door. Right, see A4: the flipped pin's subject (a stored secret never carried into a served position) is preserved with a typed secret and strengthened at rest; the refused shape gets its own twin. Accepted.
  • Class (c) finding, skills/objectstack-automation/SKILL.md. TRUE on origin/main: line 356 says secret is "Strongly recommended, without it unsigned posts are accepted and a warning is logged", but since 17.5.0 the engine refuses such a flow at registration and trigger-api at arm time, and the "armed WITHOUT a secret" warning is gone (that package's 17.5.0 changelog entry); line 52 says a type: 'api' flow is "invoked explicitly via the API / engine.execute(), or bound as an inbound webhook", but deriveTriggerBinding resolves type: 'api' to the inbound trigger and validateApiTriggerSecret refuses it without a secret whatever its status; an explicit-only flow is autolaunched. An AI author following the skill writes exactly the flow this PR refuses at /meta. skills/** is Tier H and is correctly untouched here; escalated to the seat's filing as the dev asked, with the dedupe words in the os-dev-report.
  • Acceptance note: any stored value reads as present. Accepted as the ruling's own consequence (A8): judging the stored value would hand a gate a restored credential, which triage ruled out; the engine remains the value judge at registration; the one exception ('') is served as written and refused (A3). Disclosed in the PR body and the report.
  • Acceptance note: one extra indexed read per active save of a redactor type, race window. Accepted. The two reads are deliberately separate so the carry-forward stays immediately before the put; a write racing between them leaves the gate's positions one version behind, which fails safe (a refusal or an admit that the carry-forward then grafts from the latest row), never a value leak. Observation, non-blocking: on a datasource active save the read's result is consumed by no runtime rule (validateFlowApiTriggerSecret is the only reader, runtimeTypes: ['flow']), so it is pure cost there; a follow-up could skip the read when no runtime rule for the type reads the context.
  • check:dual-build-cjs-loads and check:type-check-debt NOT MEASURED locally. Answered by the head's check-runs: Build Core (which hosts the dual-build check) and Type Check · debt ledger are both success.
  • Attribution: the four commits end with the repo's model-free trailer pair; the PR body ends with the session-URL footer. Per AGENTS.md precedence, right. Line budget 9 files, +629 / -104, under the threshold; no governed surface in the file list.

Implemented-by: claude/issue-20611-gate-reads-redaction-context
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

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/l tests tooling

Projects

None yet

2 participants