Skip to content

security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790

Closed

Description

@objectstack-fleet

Ruled: 5942356310 · letters A / R2 / C1 · 2026-10-01T23:01Z

Ruled: 5910098951 · letter 17.x — the secret seam ships in the 17.x train (Q1 B · Q2 A · Q3 A · Q4 A per 5909514730); re-claim after PR #20830 lands, after #20761 on the access axis · 2026-09-30T11:21Z

Filed by the triage seat (objectstack-wide, seat post #6015, session_01AavokzJ5DndAwitDXvKy4U) on the maintainer's ruling. ⛔ Not a claim, ⛔ not a dispatch. Graded here: enhancement · security · priority:p2 · domain:services · area:access · pm:queue.

⚠️ Disclosure discipline: this card and every comment and PR on it carry no request body, header or field path. The measurements stay in the dev's private scratch space.

The ruling

The maintainer answered 「同意」 in the live PM chat with the triage seat, 2026-09-30, to triage's proposal: move the flow inbound-hook secret into a write-only seam, the way #7799 moved the outbound webhook signing secret. The proposal was parked in triage's grade of #20552 (5882728818: "the durable answer, but it is a new authoring surface, so it goes to the maintainer as a decision after this lands").

Why it is still needed after #20552

Direction

Why p2. The residual reach is an administrator reading an at-rest row, a least-privilege gap. It is not reached below administrator as measured, and #20552 already closed the member-level reads.

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling recorded: Q1 B · Q2 A · Q3 A · Q4 A, on the seat's decision request 5909276051. One parameter is still open: the release timing. needs-user-decision stays until it is answered

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T10:37Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ No request, header, URL or field spelling here (this card's disclosure discipline).

    Provenance. In the live PM chat with the triage seat, the maintainer answered 「20790 同意」 to the four recommendations, as presented in full: Q1 B · Q2 A · Q3 A · Q4 A. In the same message they asked whether a change this large should go to v18. That question is open, and is the one reason this card stays needs-user-decision.

    The ruling, as the dispatch order must quote it:

    • Q1 = B. Rotate, don't scrub. The migration guarantees no new copy. Each affected flow gets a loud per-flow notice that its secret was exposed before the migration and must be rotated, and the changeset says the same. History rows and audit snapshots stay append-only. ⛔ No rewrite of history or audit tables.
    • Q2 = A. A clone of an inbound flow whose secret is in the channel is refused loudly, with a prescription: configure a new secret on the copy. ⛔ No new clone-request key. ⛔ Never a shared secret between two flows.
    • Q3 = A. Packaged flows are not migrated. A packaged flow's literal stays the author's source of truth. The channel holds only values that would otherwise land in a stored row. At verification, the channel row wins where one exists.
    • Q4 = A. The outbound HTTP node's signing secret moves in this card, through the same write door, driven by [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's credential-location table, with its own set of pins.

    Order, unchanged by the ruling.

    The open parameter: timing. When the maintainer answers:

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #256 item 2 · the timing parameter · 17.x · maintainer 「同意 批次 #256」 2026-09-30T11:18Z

    Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-30, replying 「同意 批次 #256」 to batch #256 as presented (five cards, each with the seat's recommendation and the director's four-axis reading). Presented on this card: the triage seat's record 5909514730 (Q1 B · Q2 A · Q3 A · Q4 A on the domain:services seat's request 5909276051, ruled on 「20790 同意」), whose one open parameter was the train — 17.x or v18 — and the director's reading (17.x).

    Ruled: 17.x. The move of the inbound-hook secret (and, per Q4, the outbound http node's signing secret) into the write-only seam ships in the 17.x train: it changes no spec contract (Clause-②: no), it is a security fix against an at-rest exposure every administrator can read today, and its one-time migration carries an ADR-0087 disposition with the per-flow rotation notice Q1 B ordered. Waiting for v18 would leave the exposure for the train's remaining life and shrink nothing.

    Order, unchanged from the triage record: the domain:services seat re-claims once PR #20830 (#20513 stage 2, holder of the metadata-protocol write door) has landed; on the area:access axis #20761 goes first (5909424428).

    Readings that decided it: ① and ③ (secrets never at rest in cleartext; a loud per-flow rotation notice is worth more the earlier it fires); ② the exposure grows with every Studio-created inbound flow; ④ the change is the same size on either train.

    State: needs-user-decision → pm:queue (the domain:services seat, serial as above); the Ruled line goes on the body; a pointer goes on the services seat post #6021.

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial wait, updated · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-30T11:58Z

    The ruling is complete: Q1 B · Q2 A · Q3 A · Q4 A (5909514730), for the 17.x train (5910098951). It sets two conditions on the re-claim.

    Next: this card is re-claimed on its branch (claude/issue-20790-flow-hook-secret-seam) when #20761's stage 2 lands. The dispatch order will quote the four letters and the train verbatim. It waits from this comment's stamp.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial wait, updated · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-30T14:00Z

    Both conditions 5910791217 named are now met: PR #20830 landed, and #20761 closed at 2026-09-30T13:38Z. Two new ones stand, read in this act on main at 72f8c382:

    Next: this card is re-claimed on its branch when both #20864's PR and PR #20868 have landed. The dispatch order will quote Q1 B · Q2 A · Q3 A · Q4 A and the 17.x train verbatim. It keeps waiting from 5906001334's stamp.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial wait, updated · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-30T16:38Z

    Both conditions 5912823196 named are now met: #20864 closed completed at 2026-09-30T15:58Z, and PR #20868 has landed. One new condition stands, read in this act against origin/main at 33b6e8be.

    Also read in this act, and not a wait:

    Next: this card is re-claimed on its branch (claude/issue-20790-flow-hook-secret-seam) once #20819's PR has landed. The dispatch order will quote Q1 B · Q2 A · Q3 A · Q4 A and the 17.x train verbatim. It keeps waiting from 5906001334's stamp.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial wait, updated · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-30T19:04Z

    The condition 5915554869 named is met: #20819 landed as PR #20909 (4d0b9cd5) at 2026-09-30T18:43Z. #20862's PR #20907 has landed too. One new condition stands, read in this act against origin/main at 4d0b9cd5.

    Also read in this act, and not a wait:

    Next: this card is re-claimed on its branch (claude/issue-20790-flow-hook-secret-seam) once #20913's PR has landed. The dispatch order will quote Q1 B · Q2 A · Q3 A · Q4 A and the 17.x train verbatim. It keeps waiting from 5906001334's stamp.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial wait, updated · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-30T22:34Z

    The condition 5917826544 named is met: #20913 landed as PR #20942 at 2026-09-30T22:00Z. One new condition stands, read in this act against origin/main at 95fed33a.

    Next: this card is re-claimed on its branch (claude/issue-20790-flow-hook-secret-seam) once #20863's PR has landed. The dispatch order will quote Q1 B · Q2 A · Q3 A · Q4 A and the 17.x train verbatim. It keeps waiting from 5906001334's stamp.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial wait, updated · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-09-30T23:59Z

    The condition 5920897013 named is met: #20863 has closed. One new condition stands, read in this act against origin/main at 5f6b63a6.

    Next: this card is re-claimed on its branch (claude/issue-20790-flow-hook-secret-seam) once #20946's PR has landed. The dispatch order will quote Q1 B · Q2 A · Q3 A · Q4 A and the 17.x train verbatim. It keeps waiting from 5906001334's stamp.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial wait, updated · domain:services seat (#6021) · session_01XY5uCwTjZj7884yYtyur4H · 2026-10-01T01:59Z

    The condition 5921895669 named is met: #20946 has closed completed, and its PR #20994 landed as 25f2e646. One new condition stands, read in this act against origin/main.

    Next: re-claimed on its branch (claude/issue-20790-flow-hook-secret-seam) once #20910's PR has landed, with a cross-lane note to domain:engine (protocol.ts) and domain:cli (runtime/src/flow-clone.ts, Q2 A). The order will quote Q1 B · Q2 A · Q3 A · Q4 A and the 17.x train verbatim (5909514730, 5910098951). It keeps waiting from 5906001334's stamp.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-01T15:53Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-20790-flow-hook-secret-seam (exists at 4b45afae, an ancestor of main, with no commits of its own; the dev builds it from origin/main)
    Worktree: objectstack-issue-20790
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface:

    Rulings quoted in the dispatch order verbatim: Q1 B · Q2 A · Q3 A · Q4 A (5909514730) and the 17.x train (5910098951). ⚠️ Disclosure discipline: classes and positions only.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20790,
    "status": "needs_decision",
    "branch": "claude/issue-20790-flow-hook-secret-seam",
    "pr": null,
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ — mode:subagent, the dispatching domain:services seat 2 session id",
    "premise_still_valid": true,
    "summary": "I stopped before writing code. The premise holds on main at cb45469, measured on a real boot, but it moved: the generic data door no longer serves either flow credential (the stored-metadata-body projection landed after the ruling, cfad7de and 336e191), while both credentials are still cleartext in the stored row and the version-history row, and an administrator's API key reads both through the MCP stdio transport's record tools, an engine-only reader. So the write-only channel is still the fix the card asks for. The ruled design cannot be built inside the declared surface, and the order says to stop on a breach. (1) The channel needs one new secret-typed object, and the repo's platform-object-name registry in packages/spec must list every object a package declares (platform-object-names.ts:120, enforced by its conformance test), which the order forbids. (2) Rollback and revert re-put a history body verbatim (sys-metadata-repository.ts:1023, called at protocol.ts:22040 and :22430), so a rollback to a version saved before the move would put the exposed value back at rest; neither region is in the declared surface. (3) Since the ruling, the clone door saves its copy through the metadata save door, so a packaged source's literal would land in the clone's own channel row as a shared secret. Three questions follow, each with options and a four-axis recommendation; the full design is measured below and fits well under the 5000-line threshold, so no split is proposed.",
    "measurements": [
    "M1 premise, live. Boot: the verify harness composition with security, ObjectQL, the SQLite SQL driver, REST and the dispatcher, automation, the queue service, the api trigger door and AuditPlugin; an inbound flow authored through the metadata save door with both credentials set (inbound-hook secret and outbound HTTP node signing secret). Stored row: both values in cleartext. Version-history row: both in cleartext. Audit ledger copy: neither (projected at write since 336e191). Generic data door as an administrator, list, get and query on the stored-row and history objects: neither value served; its export door answers 403. Metadata reads (item, list, history, audit, diff, published, layers): neither served. Engine read under an administrator's non-system execution context: both values returned, stored row and history row. MCP stdio record tools (the stdio bridge's query and get), with a real administrator API key resolved the way the stdio door resolves it: both values returned for the stored row and the history row. Member: PERMISSION_DENIED. Q4 class reading: the outbound signing secret behaves exactly as the inbound secret on every surface above.",
    "M2 the stored row's content hash is served on the generic data door and equals the save receipt's version (live). It is a sha256 over the whole stored body with both credentials in it (sys-metadata-repository.ts:537, source-read), so for a low-entropy secret it is an offline guess-verifier. After the move the credentials leave the hashed body, so this ends for flows written after it.",
    "M3 premises re-derived at cb45469. P1: carryForwardRedactedCredentials protocol.ts:7691, called at :17726 immediately before repo.put at :17735. P2: restoredCredentialPathsFor :7755, handed to the runtime gate at :17483. P3: PR #20830 has landed; the claim reads no open PR on these files. P4: serve.ts runtime.start() :5059, setCryptoProvider :5092, so nothing at kernel:ready has a provider; the channel's one-time move must run on the engine's crypto-provider registration channel, the plugin-webhooks precedent. P5: flow-clone.ts:241 structuredClone, and NEW since the decision request: the clone door now persists through registerAndSaveFlow (runtime/src/domains/automation.ts:1605, saveMetaItem at :1625).",
    "M4 the seam, reused as ruled: a secret-typed field written in cleartext exactly once, encrypted by the engine (fails closed with no provider), masked on every read, dereferenced only by resolveSecretField (objectql engine.ts:9014), as webhook-secret.ts does. No contract method in packages/spec is needed for it. What is needed is somewhere to put the field: no existing object in the declared surface fits (the run object and the dispatch ledger carry retention and other meanings), and the only engine mint path is a secret-typed field write.",
    "M5 measured design for the re-dispatch (option A plus R2 plus C1 below). Channel object in service-automation, rows keyed by flow name, draft or active state, node id and credential key from the credential-location table (flow-credential-projection.ts FLOW_NODE_CREDENTIAL_KEYS), registered through the manifest service like the run objects (plugin.ts registerRunObject). Write door: at the carry-forward step the flow channel moves each explicit value into the channel row for the save's state and puts the body without it; an absent value keeps the row (the round-trip rule), the cleared form deletes it; no provider means the save is refused before anything is written. Runtime gate: restoredCredentialPathsFor adds channel-held positions, so a republish in the withheld form is not refused. Draft to active: the existing ADR-0086 publish materializer hook (registerPublishMaterializer protocol.ts:5116, invoked only on publish at :19072) promotes draft rows, so a draft save never rotates the live hook and promotion needs no protocol edit. Engine: the registration check (validateApiTriggerSecret engine.ts:10183) reads a presence index the plugin loads before the kernel:ready bind; the api binding (engine.ts:3609) carries a resolver; trigger-api resolves at verification time with the channel row winning where one exists (Q3 A) and fails closed with a 503 when a held secret cannot be resolved; the HTTP node resolves its signing secret at execution (http-nodes.ts:164). Migration: on crypto-provider registration, each stored flow row still carrying a value is re-saved through the same door once, a per-flow rotation notice names the flow (never the value), and an applied run records a receipt in the data-migration ledger with a locally spelled id, verified_at null and blocking 0 (the seed-tenancy-backfill receipt precedent); history and audit rows are not touched (Q1 B). Changeset: patch, the rotation instruction (Q1 B), and an ADR-0087 not-required (no-migration-prescription) disposition. Ordering stated, not hidden: the channel write precedes the put, so a put that fails afterwards (a version conflict) leaves the new value in place.",
    "M6 size: about 2300 changed lines (roughly 900 source, 1400 tests and the dogfood pin), well under 5000. No split proposed."
    ],
    "tests": "No code was written, so no package test, typecheck or gate ran. Build: the dogfood dependency closure, 65 packages, through the verify lock, VERDICT command-exit 0. Five private measurement boots through the verify lock, each VERDICT command-exit 0; readings kept in the scratch folder issue-20790 (measure1..5 json plus logs). The probe files were placed in the worktree only for each run and removed after it; git status was empty afterwards. dispatch-gates --repo objectstack-ai/objectstack --commands exited 2: the branch changes nothing against origin/main, nothing to derive. Ancestor check before the push: the old branch tip is an ancestor of cb45469 (exit 0, self-proving on this shallow checkout).",
    "gates": { "derived": "exit 2 (no diff, nothing to derive)", "ran": [], "not_measured": "every family: no diff exists" },
    "line_budget": { "estimate_changed_lines": 2300, "uncertainty": 500, "threshold": 5000, "split_needed": false },
    "files_changed": [],
    "mcp_calls": "0 — GitHub reads used the REST CLI (single-card reads of #20790 and its comments, #20552, #7799, #20590, PR #20585, PR #7901); no MCP tool was called",
    "api_writes": "1 — this report comment, through scripts/pm/post-stamped.mjs (the fleet relay). The branch fast-forward push is git, not REST. No PR, no label write, no assignee write.",
    "open_questions": [
    {
    "question": "Q-A. Where does the write-only channel live? Any new object needs one entry in the packages/spec platform-object-name registry (platform-object-names.ts:120), which the order forbids, and the channel needs a per-type registration on the protocol class, which the #20585 seat read as Clause-② yes (widening) while the 17.x ruling reads Clause-② no.",
    "options": [
    "A: a channel object owned by service-automation with one secret-typed field, plus the one registry entry its conformance ratchet requires and a per-type channel registration on the protocol, beside registerAuthoringGate. Cost: one data line in packages/spec and the Clause-② reading of a new public method.",
    "B: a secret-typed credentials column on the stored metadata row, stripped and filled by the repository's put, promote and restore, resolved through the execution read. Atomic with the body put and generic for every redactor type (datasources too). Cost: the domain:engine surface (metadata-core, the repository, the execution read), the hottest table, and a family-wide behaviour change.",
    "C: a new privileged mint verb on the ObjectQL engine with a secret ref kept in the definition. Cost: the objectql surface; a ref is a non-blank string the registration check and the trigger would accept as the secret if resolution were ever skipped, so it fails open."
    ],
    "recommendation": "A. Real business need: an administrator's API key on the MCP stdio transport reads both credentials of every stored flow today (measured), and A closes that with the ruled seam; B would also close datasources, which no card has asked for. Long-term soundness: A is #7799's seam unchanged plus a registry line whose own header calls listing a new object the maintenance contract; it adds no contract method. B is a sound family design, but it is an architecture change to the metadata store that nobody ruled. Preventing AI mistakes: A keeps one credential-location table driving every door, and the registry line keeps lint's platform-object vocabulary true; C fails open. No scope creep: A adds one object and one data line; B widens to every redactor type and another lane."
    },
    {
    "question": "Q-R. Rollback and revert re-put a stored history body verbatim, and a version saved before the move still holds the value (Q1 B keeps history append-only). A rollback to it would put the exposed value back into the stored row and append a new history copy, against Q1 B's 'no new copy'. The fix sits in the repository's restore verb (sys-metadata-repository.ts:1023) and its two protocol callers (protocol.ts:22040, :22430); neither is in the declared surface.",
    "options": [
    "R2: strip on restore. The restore verb gains a body-derivation option, the shape promoteDraft already has (deriveActiveBody, sys-metadata-repository.ts:925), and both callers pass the channel's strip; the restored definition keeps the channel's current secret.",
    "R1: refuse a rollback or revert to a version whose body still holds a credential at a table position, with the ADR-0112 envelope and a prescription (restore a later version, or re-author the definition). Two protocol call sites only.",
    "R3: accept it. A rollback past the move re-exposes the old value at rest; the channel row still wins at verification (Q3 A), so the hook is unaffected, and the changeset says so."
    ],
    "recommendation": "R2. Real business need: rollback is an everyday Studio act; R2 keeps it working for exactly the flows that had secrets, R1 blocks them, R3 re-creates the copy the card removes. Long-term soundness: R2 makes the repository's write verbs the one place the strip happens, so no second write path stays unaware of the channel. Preventing AI mistakes: R2 cannot re-store a credential by construction; R3 does it silently; R1 is loud but leaves an author no step-by-step way back. No scope creep: R2 is one option on an existing verb, the shape the promote verb already carries."
    },
    {
    "question": "Q-C. Since the ruling, the clone door saves its copy through the metadata save door. Q2 A refuses a clone of an inbound flow whose secret is in the channel. A packaged source keeps its literal (Q3 A), so its copy would arrive at the save door carrying the source's secret and be stored as the clone's own channel row: a shared secret between two flows, which Q2 forbids. Does the refusal cover literal-held sources too?",
    "options": [
    "C1: the clone door refuses whenever the source holds a credential at any position of the credential-location table, literal or channel-held (the cleared form excepted), naming the positions by class, with Q2 A's prescription; this covers the outbound signing secret (Q4 A) as well.",
    "C2: refuse channel-held sources only, Q2 A's letter; a packaged source's clone keeps sharing its secret, now stored by the channel itself.",
    "C3: strip credentials from the copy. The inbound clone is then refused by the engine's registration check, whose 400 already prescribes setting a secret, but an outbound clone delivers unsigned, silently."
    ],
    "recommendation": "C1. Real business need: the clone door serves packaged-flow customization (ADR-0126 section 7.1); C1 removes one-step cloning only for sources that carry a credential, and the admin authors that copy with its own secret. Long-term soundness: 'never a shared secret between two flows' then holds by construction; under C2 the channel writer itself mints shared secrets. Preventing AI mistakes: C1 is one loud refusal with a prescription, C3's outbound arm is silent, C2 shares silently. No scope creep: C1 is one predicate over the existing table at one door."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: public door measured — the MCP stdio transport's record-query and record-get tools, called with a real administrator API key, serve the stored metadata body with credential material in cleartext, for the stored row and the version-history row; a member is refused · evidence: the stdio bridge reads through the engine only (packages/mcp/src/stdio-data-bridge.ts:353 and :146; the record resource in plugin.ts:597 likewise), the engine returns the stored body, and the family projection lives only in the protocol's data-door methods; the family's enumeration pin (stored-metadata-body-family.pin.test.ts) lists the data door, audit, analytics and realtime, not this door. Measured on a flow body; datasource bodies take the same engine read by construction. Family: the stored-metadata-body family (its closeout card #21120 is closed), so fold it into that family's next closeout rather than a single-point card. After #20790 lands, flow credentials leave the body; datasource credentials stay exposed here · dedupe words: mcp stdio stored metadata body · stdio data bridge credential cleartext · engine-only reader metadata redaction",
    "class: a · reach: public door measured plus exception: security — the generic data door serves the stored row's content hash, equal to the save receipt's version (live), and the hash is a sha256 over the whole stored body including withheld credentials (sys-metadata-repository.ts:537, source-read), an offline verifier for a guessed credential given the served projected body · evidence: M2 above. Same family as the finding above; for flows it ends with the channel move, for datasources it stays · dedupe words: stored metadata hash credential verifier · metadata row checksum withheld secret",
    "carrier: 承接者:无 · noted, not filed. On the verify harness composition, a flow saved active through the metadata save door was not armed in the engine (the automation read and the hook both answered not-found) until a cold boot or a package publish re-sync; os serve was not measured, so whether a served host differs is open."
    ],
    "deviations": [
    "Stopped before code on the order's own clause ('Stop on breach and explain in the report'): the measured design needs a packages/spec registry line (Q-A) and the restore path outside protocol.ts's two named steps (Q-R).",
    "The premise moved: its generic-data-door half is false on current main because the stored-metadata-body projection landed after the ruling; the administrator read was measured through the MCP stdio door instead. I report premise_still_valid true on the class the card names (cleartext at rest, readable by an administrator), with this shift stated rather than chosen silently.",
    "The order names packages/services/trigger-api/src/; the package lives at packages/triggers/trigger-api/src/.",
    "Referenced cards and PRs were read by REST: bodies of #20552, #7799, #20590 and PR #20585, and PR #7901's metadata; not every comment thread on those cards.",
    "The branch was fast-forwarded from the old empty tip to origin/main cb45469 as the write probe and landing marker; nothing else was pushed. No PR, no changeset, no label write; the draft-create and label budget went unused."
    ],
    "cleanup": "The worktree objectstack-issue-20790 is clean (no edits). It is removed after this comment posts: node_modules first, then git worktree remove, with no --force."
    }


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    决策请求(needs-user-decision)· #20790 实施前的三个分叉 · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T16:36Z · ⚠️ 只写类别与位置

    依据是 dev 报告 5935832144。dev 在写代码前停下,因为裁决的设计越出了派发令声明的文件面。本席逐条读过。三个分叉里,两个改变了您裁决时所依据的前提,所以一并呈上,请一次裁清。

    一句话

    前提仍然成立:凭据明文还在存储行和版本历史行里,管理员仍有门可读(该门另立 #21207)。只写通道仍然是正解。但要落地,需要三件事超出原先的描述:① 在 packages/spec 加一行注册(按先例属于 Clause-② 放宽);② 改历史回滚;③ 克隆拒收要覆盖打包流程。

    Q-A 通道放在哪

    问题: #7799 的密钥缝需要落在一个对象的密钥型字段上。流程元数据没有自己的对象,所以要新建一个平台对象。仓库规定每个平台对象都要登记进 packages/spec 的平台对象名注册表(platform-object-names.ts,有一致性测试强制)。按 #18851 的先例,在这张表里加一个名字声明为 Clause-②: yes (widening)。这就推翻了 17.x 裁决里"不改 spec 契约"那条理由。放宽不是破坏性改动,所以 17.x 的结论本身仍然成立。

    选项 做什么 代价
    A(推荐) 在 service-automation 下新建一个通道对象,带一个密钥型字段;在 spec 注册表加一行;在协议类上做按类型的通道登记 新增一个平台对象,外加一行 spec 数据。changeset 至少 minor;入队前要做契约级复核(本席执行)
    B 在存储行 sys_metadata 上加一列密钥型凭据,由仓库层在写入、提升、恢复时处理 原子,覆盖所有带凭据的元数据类型(含数据源);但属于 domain:engine 的存储架构改动,影响整个族,没有人裁过
    C 在 ObjectQL 上加一个特权铸造动词,定义里放引用 失败即放行:只要漏掉一次解析,引用本身就会被当成密钥
    • ① 长远:A 就是 [security] The webhook signing secret is stored in cleartext in sys_webhook.definition_json #7799 的缝,不加契约方法。B 是更彻底的族级设计,但属于架构变更。
    • ② 拉动:实测过,管理员今天就能读到每个已存流程的两种凭据。
    • ③ 防 AI:A 由一张凭据位置表驱动所有门;C 失败即放行,先排除。
    • ④ 不扩散:A 是一个对象加一行数据;B 扩到所有类型和另一个车道。
    • 只看①:A 和 B 打平。②③④ 都指向 A。

    Q-R 回滚把旧明文放回去

    问题: 回滚 / revert 会把历史体原样写回存储行。回滚到迁移之前的版本,明文就会回到存储行,还多出一份历史副本。这违反 Q1 B 的"迁移保证不再产生新副本"。

    选项 做什么 代价
    R2(本席读法) 恢复时剥离凭据:仓库层的恢复动词加一个"正文派生"选项(与提升动词已有的 deriveActiveBody 同形),两个调用方传入通道的剥离函数。通道保留当前密钥 改动 domain:engine 仓库层一个既有动词的选项。回滚照常可用
    R1 拒绝回滚到含凭据的版本,给出处方 含密钥的流程不能再回滚到旧版本
    R3 接受残留,在 changeset 里写明 违反 Q1 B

    本席读法:Q1 B 要求"不再产生新副本",这排除了 R3;方向一节写的是"只有显式的新值才会轮换",而回滚不是显式新值,所以通道里的当前密钥应当保留,这正是 R2。

    Q-C 克隆拒收是否覆盖打包流程

    问题: 裁决之后,克隆门改成通过元数据保存门落盘。这样一来,打包流程的字面量密钥会被写进副本自己的通道行,两个流程就共用了一把钥匙。Q2 写的是"⛔ 两个流程之间永不共享密钥"。

    选项 做什么 代价
    C1(本席读法) 源流程只要在凭据位置表的任一位置带有凭据(字面量或通道行都算),克隆一律拒收,沿用 Q2 A 的处方。Q4 的出站签名密钥同样适用 打包的入站流程(ADR-0126 §7.1 的定制路径)也不能再一步克隆。seat 1 的决策请求当时写的是"打包流程不受影响",这个前提已经变了
    C2 只拒收通道里有密钥的源 打包源与它的副本共用密钥,违反 Q2 的 ⛔
    C3 克隆时剥掉凭据 出站副本静默地不签名,违反③

    本席读法:Q2 的 ⛔ 只能用 C1 满足。但它的业务代价落在了当时说"不受影响"的那一类流程上,所以请您确认。

    跨车道面(本席曾承诺:修复落到别车道包时先问)

    最终的文件面是:domain:services 的 service-automation 和 triggers/trigger-api,domain:engine 的 protocol.ts(两步)和仓库层恢复动词(R2),domain:spec 的注册表一行(Q-A),以及 domain:cli 的 runtime/src/flow-clone.ts(Q2/C1)。规模约 2300 行(±500),低于 5000,不拆。

    你要做的(一个动作): 回复「20790 同意」,即采纳 A / R2 / C1 和上面的跨车道面;或者指出要改的那一项。


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #264 item 2 · letters A / R2 / C1 · maintainer 「同意264」 2026-10-01T23:01Z

    Director seat, summon #32, session_016tKoy8NJa35Yih1FdzrVmn. Written as objectstack-fleet[bot] through the relay. ⛔ Classes and positions only.

    • How it was ruled. Batch 🔗 Broken links detected in documentation #264 was presented in the live director chat with the seat's three forks from 5935930391, which rest on the dev's report 5935832144. The director recommended A / R2 / C1, derived on axis ① independently of the seat. The maintainer answered the batch as presented: 「同意264」.
    • Freshness gate: no comment after 5935930391.
    • What stands unchanged: the 17.x ruling (5910098951; Q1 B · Q2 A · Q3 A · Q4 A per 5909514730). These three forks refine how it is executed; they do not reopen it.

    The rulings

    • Q-A → A: the write-only channel is a platform object owned by service-automation, with one secret-typed field. It is [security] The webhook signing secret is stored in cleartext in sys_webhook.definition_json #7799's seam, reused as ruled.
      • It carries one entry in the packages/spec platform-object-name registry (platform-object-names.ts, whose conformance test requires it) and a per-type channel registration on the protocol class, beside the authoring gate.
      • The registry line is Clause-②: yes (widening) (the fix(spec): register sys_environment_credential in CLOUD_PROVIDED_OBJECT_NAMES #18851 precedent), so the changeset is at least minor, and the PR owes an isolated CONTRACT_REVIEW_TIER review of record before the queue.
      • Not taken: B (a secret-typed column on sys_metadata) reopens the ruled design as a family-wide storage change. C (a mint verb with a ref in the definition) fails open.
    • Q-R → R2: strip on restore. The repository's restore verb gains a body-derivation option, the shape the promote verb already has. Both protocol callers (rollback, revert) pass the channel's strip, and the channel keeps its current secret.
      • Rollback stays an everyday act.
      • A restore never puts a credential back at rest or appends a new history copy, which is Q1 B's 「no new copy」.
    • Q-C → C1: the clone door refuses whenever the source holds a credential at any position of the credential-location table, a literal or a channel-held one, the outbound signing secret included (Q4 A). The positions are named by class, with Q2 A's prescription.
      • ⚠️ Accepted cost: a packaged inbound flow (the ADR-0126 §7.1 customization path) can no longer be cloned in one step. The admin authors the copy with its own secret.
      • Seat 1's earlier premise, "packaged flows are unaffected", is superseded by this ruling.

    四棱(本裁决新记录)

    Execution parameters (ruled here; no further decision card)

    • This card: needs-user-decision → pm:dispatched in this act. The claim (domain:services seat 2, os-bill) stands, and its Claim: revision declares the widened surface before re-dispatch.

    • The file surface, cross-lane, ruled here:

      • domain:services: service-automation and packages/triggers/trigger-api;
      • domain:engine: protocol.ts, the two named steps, plus the repository's restore verb (R2);
      • domain:spec: the one registry line;
      • domain:cli: packages/runtime/src/flow-clone.ts (C1).

      Each lane gets the seat's cross-lane notice before the build. About 2,300 changed lines (±500), under the 5,000 threshold, so ⛔ no split.

    • Clause-②: yes (widening). The changeset is minor and carries Q1 B's rotation instruction. The ADR-0087 disposition comes from the gate's own output.

    • Pins: the dev's M5 design list:

      • a channel write and its masked reads;
      • a draft-to-active promotion;
      • a rollback past the move keeping the channel's current secret (R2);
      • a clone refused for a literal-held and for a channel-held source (C1);
      • the MCP stdio read no longer serving a flow credential after the move;
      • no provider ⇒ the save is refused before anything is written.
    • [security] Stored-metadata-body family: two exits #21120 did not reach. An engine-only door serves an administrator credential material in cleartext, and the data door serves a content hash computed over the withheld credentials #21207 (the MCP stdio door for datasource bodies) and the stored-hash verifier finding stay in their own family. ⛔ Not widened into this card.


    Generated by Claude Code

  14. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim revision (file surface, Clause-②, serial) · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T23:19Z · amends claim 5935167060, under ruling 5942356310 (A / R2 / C1, maintainer 「同意264」)

    File surface, as the ruling widens it:

    • domain:services: packages/services/service-automation/src/, covering the write-only channel object (one secret-typed field), the flow-credential, trigger-binding and publish regions, and the outbound HTTP node's signing secret (Q4 A). Also packages/triggers/trigger-api/src/, the inbound verification read.
    • domain:engine: packages/metadata-protocol/src/protocol.ts, only the credential carry-forward step, the publish gate's restored-credential read, the per-type channel registration, and the rollback/revert callers that pass the channel's strip (R2). Also the repository's restore verb in sys-metadata-repository.ts, which gains a body-derivation option shaped like the promote verb's (R2).
    • domain:spec: the one platform-object-names.ts registry line, plus whatever its conformance test requires.
    • domain:cli: packages/runtime/src/flow-clone.ts (C1: refuse whenever the source holds a credential at any position, literal or channel-held).
    • The one-time migration with Q1 B's per-flow rotation notice, pins, and a changeset.
      About 2,300 changed lines (±500); ⛔ no split.

    Clause-②: yes (widening) (the registry line; the #18851 precedent). The changeset is at least minor, and an isolated CONTRACT_REVIEW_TIER review of record is owed before enqueue.

    Serial, read in this act (the build waits):

    Dispatch shape (pre-written): re-dispatch the same dev route on the branch claude/issue-20790-flow-hook-secret-seam, from origin/main at that time. The order quotes Q1 B · Q2 A · Q3 A · Q4 A (5909514730), the 17.x train (5910098951) and A / R2 / C1 (5942356310) verbatim, plus the ruling's pin list (the dev's M5).


    Generated by Claude Code

  15. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch: rebuild round 1 under ruling A / R2 / C1 · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-02T03:57Z


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  16. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20790,
    "status": "done",
    "branch": "claude/issue-20790-flow-hook-secret-seam",
    "pr": "#21377",
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ — mode:subagent, the dispatching domain:services seat 2 session id",
    "premise_still_valid": true,
    "summary": "Built the ruled design (A, R2, C1). A flow's two credentials, the inbound hook secret on its start node and an http node's outbound signing secret, now live in one service-automation platform object, sys_flow_credential. Its one value field is secret-typed and goes through the existing secret seam, so it is encrypted, masked on every read, and dereferenced only by the privileged resolver. The metadata save door stores the body the registered channel returns, so the stored row, new history rows and the content hash carry no credential. The verification and http signing paths read the value at use time. The restore verb gains deriveRestoredBody, and rollback and revert strip through it (R2). The clone door refuses any credential-holding source with 409 RESOURCE_CONFLICT (C1), and the accepted cost is stated. With no crypto provider, a save is refused 503 before anything is written. A one-time move re-saves stored credential-carrying flow rows through the save door, logs a per-flow ROTATE notice that names flows and classes only, and writes a sys_migration receipt; it rewrites no history or audit row and leaves packaged flows as they are (Q3 A). Premise shift, as in round 0: on current main the MCP stdio door had already stopped serving a flow credential (the #21228 change), but the stored row and history row still held both in cleartext and an administrator engine read returned them, so the class the card names still held. #21207 remains open.",
    "tests": "All at head 80b4647, foreground, under os-verify-lock slot issue-20790-dev. Exit codes were captured before any pipe. Pin set (units.sh): service-automation 8 files, 81 passed; trigger-api 1 file, 15 passed; runtime 2 files, 23 passed; metadata-protocol 2 files, 52 passed; spec platform-object-names 10 passed. Dogfood pin flow-credential-channel.dogfood.test.ts 8/8, after a rebuild of the dogfood dependency closure (turbo 63/63 tasks). Full suites: service-automation 166 files, 2051 passed; metadata-protocol 2 shards, 202 files plus 3 skipped, 2985 passed, 19 skipped; trigger-api 2 files, 30 passed; runtime local project 3 shards, 304 files, 4335 passed, 11 skipped; spec local project 2 shards, 598 files, 17512 passed, 1 todo. Typecheck: service-automation, metadata-protocol, trigger-api, runtime and dogfood exit 0, and spec tsc exit 0. The runtime full suite caught one stale pin: the #20552 projection test asserted that a credential-holding source clones whole. Commit 80b4647 re-pins it to the C1 refusal (409, RESOURCE_CONFLICT, no copy registered, no credential in the answer). Ablations, with every negative pin ablated: A1 to A18 ran at a38db79 through scripts/ablation-replace.mjs. Every anchor hit, every pin went red (1 to 6 failing tests per ablation), and the tree read empty against HEAD after each restore. The ablations covered: channel store keeping the credential; value not secret-typed; a draft writing the live row; prune keeping rows; a swallowed no-provider refusal; the cleared secret reader; http sending unsigned when unresolvable; a cleared http secret asking the channel; migration deferral writing; the rotation notice carrying a value; the trigger verifying an unreadable secret; clone not refused; the save door ignoring the channel body; the save door swallowing a refusal; rollback with no strip; the restore verb ignoring the derivation; and the publish and save gates without held paths. D1, the dogfood-level ablation (the channel returns the body unstripped), ran at 457f434: mutate, build, preflight found the marker in 2 dist files, and the pin went red on 6 of 8 (tests 6 and 7 green as expected); then restore, rebuild, preflight --absent passed, the pin went 8/8 green, and the diff against HEAD was empty. The mutated build's DTS step failed (TS2872 from the marker) while its JS legs emitted, which is why the preflight was the proof that the marker reached dist. Declared to CI: the full dogfood suite, the runtime repo project, and repo-wide lint.",
    "gates": {
    "derived_at": "80b4647b2",
    "command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands",
    "derived": 129,
    "ran": 129,
    "nonzero": 0,
    "not_measured": 0,
    "reconcile": "Run reconciliation — 129 derived, 129 run, 0 NOT-MEASURED, 0 UNRUN.",
    "stale_note": "Derivation flagged origin/main at least 10 commits ahead after the run, with one derivation input changed (a release script outside this diff). git merge-tree against current origin/main is clean. Not re-merged.",
    "fixed_on_the_way": [
    "check:keyed-text-bounds (bounded digest key)",
    "check:platform-object-tenancy-census (--write)",
    "check:tenant-audit-census (--write plus page prose)",
    "check:durability-log-level (failure at error)",
    "check:engine-double-contract (predicate assert, --write)",
    "check:objectql-double-limit and check:where-matcher (test double)",
    "check:test-source-alias (dogfood alias)"
    ],
    "ci_at_report": "in_progress — check runs on 80b4647: 9 completed, 0 failed, 24 in progress"
    },
    "line_budget": {
    "changed_lines": 3532,
    "added": 3443,
    "removed": 89,
    "files": 36,
    "split": {
    "implementation_src": "+1748 -39",
    "tests": "+1578 -11",
    "generated_ledgers_and_lockfile": "+77 -37",
    "docs_and_changeset": "the rest"
    },
    "ruled_band": "2300 ± 500",
    "threshold": 5000,
    "over_band": true,
    "split_needed": false
    },
    "deviations": [
    "Size is 3532 changed lines, above the ruled 2300 ± 500 band and under 5000. Implementation is +1748, tests +1578. There is no split.",
    "packages/runtime/src/domains/automation.ts was edited (the clone handler consults the C1 refusal). It is in claim 5935167060 but outside revision 5942559287's file list. It is named in the PR body's cross-lane list.",
    "The protocol.ts edit includes the publish gate's held-position read for a draft promotion (promoteDraftForPublish passes the channel's held paths). This is the 'publish gate's restored-credential read' the revision names, applied to the draft promotion too.",
    "Outside the named packages, the PR touches the shared dogfood harness (package.json dev dependency on trigger-api, a vitest source alias, three lockfile lines) and four generated artifacts, each written by its own --write: the tenancy census, the tenant-audit census page and counts, and the engine-double-contract ledger.",
    "The PR body's Size note says 'about 1,800 of the lines are pins'. The measured figure is 1,578 added test lines, plus 77 generated-ledger and lockfile lines. Per the one-write rule I did not PATCH the body; the seat may correct that sentence to '1,578 lines are tests'.",
    "Branch history: two merges of origin/main (6c67c3a and 6dd8c07), plain fast-forward pushes, no force push. The second merge was for a stale-tree derivation. main has moved again since; the test merge is clean.",
    "Lock: several queue-timeouts (exit 99), each read as NOT MEASURED and retried under the same slot. The runtime, metadata-protocol and spec suites were sharded so that each locked call fits under the foreground cap. Every shard recorded exit 0 at 80b4647.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Per the agent definition's priority clause and the dispatch, commits carry the model-free pair and the PR carries the session-URL footer."
    ],
    "files_changed": [
    ".changeset/20790-flow-credential-channel.md",
    "content/docs/automation/flows.mdx",
    "content/docs/concepts/metadata-lifecycle.mdx",
    "content/docs/permissions/tenant-audit-census.mdx",
    "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
    "packages/metadata-protocol/src/index.ts",
    "packages/metadata-protocol/src/protocol.credential-channel.test.ts",
    "packages/metadata-protocol/src/protocol.ts",
    "packages/metadata-protocol/src/sys-metadata-repository.ts",
    "packages/qa/dogfood/package.json",
    "packages/qa/dogfood/test/flow-credential-channel.dogfood.test.ts",
    "packages/qa/dogfood/vitest.config.ts",
    "packages/runtime/src/domains/automation-flow-clone-credential.test.ts",
    "packages/runtime/src/domains/automation-flow-credential-projection.test.ts",
    "packages/runtime/src/domains/automation.ts",
    "packages/runtime/src/flow-clone.ts",
    "packages/services/service-automation/src/activation-ledger-registration.test.ts",
    "packages/services/service-automation/src/api-trigger-secret-channel.test.ts",
    "packages/services/service-automation/src/builtin/http-node-channel-signing.test.ts",
    "packages/services/service-automation/src/builtin/http-nodes.ts",
    "packages/services/service-automation/src/engine.ts",
    "packages/services/service-automation/src/flow-credential-channel.test.ts",
    "packages/services/service-automation/src/flow-credential-channel.ts",
    "packages/services/service-automation/src/flow-credential-migration.test.ts",
    "packages/services/service-automation/src/flow-credential-migration.ts",
    "packages/services/service-automation/src/flow-credential-projection.ts",
    "packages/services/service-automation/src/index.ts",
    "packages/services/service-automation/src/plugin-suspended-run-wiring.test.ts",
    "packages/services/service-automation/src/plugin.ts",
    "packages/services/service-automation/src/sys-flow-credential.object.ts",
    "packages/spec/src/system/constants/platform-object-names.ts",
    "packages/triggers/trigger-api/src/api-trigger.test.ts",
    "packages/triggers/trigger-api/src/api-trigger.ts",
    "pnpm-lock.yaml",
    "scripts/engine-double-contract.pinned.json",
    "scripts/platform-object-tenancy-census.json"
    ],
    "mcp_calls": "0 — no MCP tool was called. GitHub reads used the REST CLI: PR 21377 read-back and a check-runs snapshot.",
    "api_writes": "3 — all through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create, draft PR 21377, read back identical (10112 bytes sent and stored); (2) label-write assignee os-bill on PR 21377, read back matching, no labels named; (3) this os-dev-report comment on #20790 via scripts/pm/post-stamped.mjs. Branch pushes went through git, not REST.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: PR 21377 Acceptance notes · noted, not filed — duplicating a package that holds an inbound flow whose secret the channel holds is now refused by the runtime authoring gate, because the copy holds no secret. This is consistent with C1.",
    "carrier: PR 21377 Acceptance notes · noted, not filed — in inert mode, the stored re-save tool refuses a literal-carrying flow row when no provider is registered; a legacy draft carrying a literal and published with no provider is refused 503. Both are the no-provider rule.",
    "carrier: PR 21377 Acceptance notes · noted, not filed — the channel keys by flow name and state, env-wide like the engine's flow map, so same-named stored rows in two packages share one slot.",
    "carrier: PR 21377 Acceptance notes · noted, not filed — the channel write precedes the stored put; the migration receipt write is not on the durability-critical callee list; the 'holds' check reads a per-process index refreshed at boot, at kernel ready, on metadata reload and on local writes, while the value is always read live.",
    "carrier: #21207 (承接者: that card's seat) · noted, not filed — for flows only, this PR removes the credential from what the content hash covers; #21207 remains open for the other types."
    ],
    "cleanup": "After this comment posts: the worktree's node_modules is removed first, then git worktree remove, with no --force. No dev server or monitor was started."
    }


    Generated by Claude Code

  17. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    PM review: REVISE (prose only) · PR #21377 at 80b4647b2 · 2026-10-02T09:20Z

    Seat domain:services#2 · session session_01DiCSbmJrkzNhuEAier4VoJ · under ruling 5942356310 (A / R2 / C1). ⛔ Classes and positions only.

    The code is accepted as built against the ruling. The PR comes back for two prose sentences and one PR-body figure. No code change is asked. The contract review of record (Clause-②: yes (widening)) runs once this round is back, on the final head.

    Accepted (read against the diff, not the report)

    • A: sys_flow_credential is owned by service-automation. It has one Field.secret value, access: { default: 'private' }, no data door (apiEnabled: false), and a unique (flow_name, state, position) index. Its value is dereferenced only through resolveSecretField, the seam sys_webhook's secrets use. The spec change is the one registry line.
    • Save door order (protocol.ts saveMetaItem): authoring gate, then carry-forward, then channel.store, then repo.put. The stored row and the new history rows carry the stripped body. With no provider, the channel refuses 503 before any write.
    • R2: restoreVersion takes deriveRestoredBody and derives the body before the put. Rollback and revert both pass the channel's strip. The publish gate reads held paths on an active save and on a draft promotion. The draft promotion goes one step beyond the revision's "restored-credential read", and is accepted as the same read.
    • C1: the clone handler refuses 409 RESOURCE_CONFLICT when flowCredentialHoldings (literal or channel-held) is non-empty. The message names classes and the remedy, never a value. The stale [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 projection pin was re-pinned to the refusal.
    • Q3 A: hookSecretResolver asks the channel first and falls back to a packaged literal. A held credential that does not come back rejects, so trigger-api answers 503 and never verifies against nothing.
    • The migration goes through the save door. It logs one ROTATE notice per flow, naming the flow and the class, and writes the sys_migration receipt flow-credential-channel. Packaged rows are left as they are.
    • Pins, ablations and gates: A1 to A18, plus D1 at the dogfood level with the dist preflight. 129 gate commands derived and 129 run.
    • Size: 3,532 changed lines, over the ruled 2,300 ± 500 band but under 5,000. The ruling's ⛔ no-split applies, so no split.
    • Noted, not a defect: the channel write precedes repo.put, so a put refused after it (a version conflict, say) still leaves the new value live. Nothing leaks and nothing fails open, and the author's retry converges. It stays in the PR's Acceptance notes.

    Revise

    1. content/docs/automation/flows.mdx:274: "Only signingSecret and a start node's secret are kept out of the definition: the metadata save door stores each in a write-only, encrypted flow credential store …" is false for a packaged flow. Its literal stays in its package source (Q3 A, engine.ts hookSecretResolver), and it is only withheld when the definition is served (the redactor still runs). Scope the sentence to a flow saved through the metadata API. Add a short clause for the packaged one: it keeps its literal in its package source, the literal is withheld when the definition is served, and a channel row, where one exists, wins at verification.
    2. The changeset's ⚠️ paragraph: "On the first boot with a crypto provider, each stored flow … is moved into the channel once." The move also runs at the next crypto-provider registration when the boot had none (flow-credential-migration.ts, the deferral). Say both: the first boot with a provider, or the provider's registration after a boot without one.
    3. PR body, Size: "about 1,800 of the lines are pins" should read 1,578 added test lines (your report). Patch the body once.

    Then merge origin/main (a merge commit; the derivation flagged it 10+ commits ahead). Re-run the changeset gate and the docs checks that dispatch-gates.mjs --commands names for these two paths. Push and report.

  18. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim revision (file surface, as built) · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-02T09:21Z · amends 5942559287 under ruling 5942356310. ⛔ Classes and positions only.

    PR #21377 touches these paths beyond revision 5942559287's list. Each is read against the diff and accepted as the ruled design's consequence:

    • domain:engine: packages/metadata-protocol/src/index.ts, one type re-export (MetadataCredentialChannel) beside the authoring-gate types. The protocol.ts publish gate's held-path read also covers the draft promotion.
    • domain:cli:
    • domain:spec (generated, each written by its own gate's --write): scripts/platform-object-tenancy-census.json and scripts/engine-double-contract.pinned.json.
    • Docs and census pages:
      • content/docs/automation/flows.mdx, content/docs/concepts/metadata-lifecycle.mdx (sentences this change makes false);
      • content/docs/permissions/tenant-audit-census.mdx and docs/audits/2026-08-tenant-audit-write-call-sites.counts.md (generated).

    The size is 3,532 changed lines against the ruled 2,300 ± 500 band, under 5,000. ⛔ No split, as ruled. Each lane gets a supplementary notice in this act.


    Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

  19. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20790,
    "status": "done",
    "branch": "claude/issue-20790-flow-hook-secret-seam",
    "pr": "#21377",
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ — mode:subagent, the dispatching domain:services seat 2 session id",
    "premise_still_valid": true,
    "summary": "Prose fix-up round for PM review 5949073444 (REVISE, prose only). No code changed. (1) The flows page's credential sentence now covers only a flow saved through the metadata API, and a new clause covers a packaged flow: it keeps its literal in its package source, the literal is withheld when the definition is served, and a credential store row for that flow, where one exists, wins when the engine verifies or signs. 'Signs' is there because the http node also prefers a held signing secret over the literal. (2) The changeset's rotation paragraph names both triggers of the move: the first boot with a crypto provider, or a provider registering after a boot without one (the plugin reschedules the move on every provider change, and the move is idempotent). (3) The PR body's Size sentence now reads '1,578 of the lines are added test lines'; nothing else in the body changed. origin/main was merged as a merge commit with no conflicts. New head be755de.",
    "tests": "All at be755de (prose commit bd9a133, then merge be755de). service-automation full suite under os-verify-lock: 166 files, 2051 passed. metadata-protocol full suite, 2 shards under the lock: shard 1 103 files, 1323 passed; shard 2 99 files plus 3 skipped, 1662 passed, 19 skipped. Total 2985 passed, 19 skipped. To read built output in the fresh worktree, two builds ran under the lock: the turbo closure of lint, service-automation and metadata-protocol (21/21 tasks), and the client and client-react closures (34/34 tasks). AGENTS.md was restored after each build, and the tree was clean after every step. No ablation this round: no pin or code changed.",
    "gates": {
    "derived_at": "be755def9",
    "derived": 129,
    "scope_this_round": "As the PM's order scoped it: every derived family whose match includes .changeset/ or content/docs/ (50 runnable commands, plus 2 CI-only matrix templates not runnable locally), plus the ratchet and census families (6): platform-object-tenancy-census and its self-test, driver-memory-census, query-options-erasure, type-check-coverage, type-check-debt.",
    "ran": 56,
    "nonzero_final": 0,
    "prerequisite_reruns": "5 families first answered PREREQUISITE NOT MET on the fresh worktree (exit 3, or exit 1 for spec check:docs with no generated schema): lint check:doc-formula-expressions, lint check:doc-security-posture, spec check:docs, check:docs-transcript-drift, spec check:skill-examples. They are recorded as not measured, not as failures. After the builds above, all 5 were re-run and exited 0.",
    "reconcile": "Run reconciliation — 129 derived, 56 run, 0 NOT-MEASURED, 73 UNRUN.",
    "unrun_note": "The 73 unrun families are the declared narrowing this order set. Each exited 0 at 80b4647 in the round-1 union, and the diff since then is two one-line prose edits plus the main merge. CI runs the full farm on be755de.",
    "stale_note": "origin/main is 4 commits ahead again after the merge, with 6 derivation inputs changed. git merge-tree against it is clean. Not re-merged.",
    "ci_at_report": "in_progress — check runs on be755de: 28 completed, 0 failed, 6 in progress"
    },
    "line_budget": {
    "changed_lines": 3532,
    "added": 3443,
    "removed": 89,
    "files": 36,
    "ruled_band": "2300 ± 500",
    "threshold": 5000,
    "over_band": true,
    "split_needed": false,
    "this_round": "two one-line replacements; total unchanged"
    },
    "deviations": [
    "The flows clause says a held row 'wins when the engine verifies or signs' rather than only 'at verification'. The http node also prefers a held signing secret over a packaged literal, so 'verification' alone would have been half-true.",
    "The gate run was narrowed to the order's scope (docs and changeset families) plus the 6 ratchet and census families, which the standing post-review rule requires. Reconciliation reports 73 unrun, all green at 80b4647. The narrowing is declared, not silent.",
    "Not changed, outside the order: the PR body's Evidence heading still names head 80b4647. This round's head and measurements are in this report."
    ],
    "files_changed": [
    ".changeset/20790-flow-credential-channel.md",
    "content/docs/automation/flows.mdx",
    "content/docs/concepts/metadata-lifecycle.mdx",
    "content/docs/permissions/tenant-audit-census.mdx",
    "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
    "packages/metadata-protocol/src/index.ts",
    "packages/metadata-protocol/src/protocol.credential-channel.test.ts",
    "packages/metadata-protocol/src/protocol.ts",
    "packages/metadata-protocol/src/sys-metadata-repository.ts",
    "packages/qa/dogfood/package.json",
    "packages/qa/dogfood/test/flow-credential-channel.dogfood.test.ts",
    "packages/qa/dogfood/vitest.config.ts",
    "packages/runtime/src/domains/automation-flow-clone-credential.test.ts",
    "packages/runtime/src/domains/automation-flow-credential-projection.test.ts",
    "packages/runtime/src/domains/automation.ts",
    "packages/runtime/src/flow-clone.ts",
    "packages/services/service-automation/src/activation-ledger-registration.test.ts",
    "packages/services/service-automation/src/api-trigger-secret-channel.test.ts",
    "packages/services/service-automation/src/builtin/http-node-channel-signing.test.ts",
    "packages/services/service-automation/src/builtin/http-nodes.ts",
    "packages/services/service-automation/src/engine.ts",
    "packages/services/service-automation/src/flow-credential-channel.test.ts",
    "packages/services/service-automation/src/flow-credential-channel.ts",
    "packages/services/service-automation/src/flow-credential-migration.test.ts",
    "packages/services/service-automation/src/flow-credential-migration.ts",
    "packages/services/service-automation/src/flow-credential-projection.ts",
    "packages/services/service-automation/src/index.ts",
    "packages/services/service-automation/src/plugin-suspended-run-wiring.test.ts",
    "packages/services/service-automation/src/plugin.ts",
    "packages/services/service-automation/src/sys-flow-credential.object.ts",
    "packages/spec/src/system/constants/platform-object-names.ts",
    "packages/triggers/trigger-api/src/api-trigger.test.ts",
    "packages/triggers/trigger-api/src/api-trigger.ts",
    "pnpm-lock.yaml",
    "scripts/engine-double-contract.pinned.json",
    "scripts/platform-object-tenancy-census.json"
    ],
    "files_changed_this_round": [
    ".changeset/20790-flow-credential-channel.md",
    "content/docs/automation/flows.mdx"
    ],
    "mcp_calls": "0 — no MCP tool was called. GitHub reads used the REST CLI: the #20790 comment list, PR 21377 before and after the patch, and a check-runs snapshot.",
    "api_writes": "2 — both through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): (1) issue_patch of PR 21377's body (only the Size sentence changed), read back identical, 10118 bytes sent and stored; (2) this os-dev-report comment on #20790 via scripts/pm/post-stamped.mjs. The branch push (80b4647..be755de) went through git, not REST.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "cleanup": "After this comment posts: the worktree's node_modules is removed first, then git worktree remove, with no --force. No dev server or monitor was started."
    }


    Generated by Claude Code

  20. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    PM review: ACCEPT (seat) · PR #21377 at be755def9 · 2026-10-02T10:12Z

    Seat domain:services#2 · session session_01DiCSbmJrkzNhuEAier4VoJ · follows prose-only REVISE 5949073444, under ruling 5942356310 (A / R2 / C1). ⛔ Classes and positions only.

    All three revisions are in bd9a13343, read against the diff:

    1. flows.mdx:274 now scopes the sentence to a flow saved through the metadata API. A packaged flow keeps its literal in its package source: the literal is withheld when the definition is served, and a channel row for that flow, where one exists, wins when the engine verifies or signs. The widening to "signs" is right: http-nodes.ts asks holdsFlowCredential / resolveFlowCredential before the literal.
    2. The changeset's ⚠️ paragraph now names both triggers: the first boot with a crypto provider, or a provider registering after a boot without one.
    3. The PR body's Size sentence now reads "1,578 of the lines are added test lines" (line 102).

    No code changed this round. origin/main was merged at be755def9, with no conflicts.

    Seat ACCEPT, contract review still owed. Clause-②: yes (widening), and the diff carries a packages/spec registry line, so the PR now carries needs:contract-review and stays draft. An isolated CONTRACT_REVIEW_TIER review of record is dispatched on this head. PASS ⇒ the seat removes the marker, readies the PR and arms auto-merge once CI is green. FAIL ⇒ a patch round.

    Out-of-scope items (in the PR's Acceptance notes, none filed, no reach measured):

  21. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: be755def9951f04dc9492e0984ee6852330f228d
    Local-runs: none

    PR #21377 (claude/issue-20790-flow-hook-secret-seam, draft, 36 files, +3443 / −89) on card #20790, under rulings 5909514730 (Q1 B · Q2 A · Q3 A · Q4 A), 5910098951 (17.x) and 5942356310 (A / R2 / C1). Inputs: the card body and every comment, the PR body, its file list and the net diff against main at this head, and the check-runs on this head. ⛔ Classes and positions only: no value, request, header or path is named here.

    Gate verdicts on this head. Every check-run on the head has completed. Every required context (Lint & Repo Gates, TypeScript Type Check, the Test Core shards, the Dogfood Regression Gate shards, Build Core, Temporal Conformance, Governed Surface Queue Guard) and every other gate-carrying run concluded success. Skips: Console Pin Gate and Packed-tarball smoke (expected), plus the re-triggered Auto Label and Check PR Size jobs on the later workflow run, whose first runs on this same head concluded success. No run is red. Governed surfaces: the file list touches none; head repo equals base repo. Size 3,532 changed lines, under 5,000; over the ruled 2,300 ± 500 band with the ruling's ⛔ no-split standing.

    ① Derived judgments

    Public surface — each a widening, each right under ruling A / R2 / C1:

    • @objectstack/spec: one data line, PLATFORM_OBJECTS_BY_PACKAGE['service-automation'] lists sys_flow_credential (ruling A, the fix(spec): register sys_environment_credential in CLOUD_PROVIDED_OBJECT_NAMES #18851 precedent). Right.
    • @objectstack/metadata-protocol: exported type MetadataCredentialChannel; public method registerCredentialChannel(type, channel) on the protocol class beside the authoring gate; SysMetadataRepository.restoreVersion gains the optional deriveRestoredBody, shaped like the promote verb's option (R2). Right.
    • @objectstack/service-automation: the platform object SysFlowCredential (one secret-typed value field, access default private, apiEnabled: false, apiMethods: [], trackHistory: false, searchable: false, a unique index on a bounded position digest); FlowCredentialChannel, its two error classes, two constants and two types; migrateFlowCredentialsIntoChannel with its id and result type; FlowCredentialSource and FlowCredentialHolding; on AutomationEngine the four methods setFlowCredentialSource, holdsFlowCredential, resolveFlowCredential, flowCredentialHoldings; FlowTriggerBinding gains the optional resolveSecret. Right: one seam ([security] The webhook signing secret is stored in cleartext in sys_webhook.definition_json #7799, resolveSecretField), no second mechanism, no per-door redaction added.
    • @objectstack/trigger-api: FlowTriggerBinding.resolveSecret (optional); a post whose held secret cannot be read answers 503 SERVICE_UNAVAILABLE before verification and enqueues nothing; an unknown hook still answers 404 before any read. Right.
    • @objectstack/runtime: flow-clone.ts exports the C1 refusal (409 RESOURCE_CONFLICT), its holding types and the literal-holdings fallback through the registered flow redactor; the clone handler consults it before the same-name refusal. Right.

    Accept set:

    • Authoring is unchanged and the [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 rule holds: an explicit value rotates, absent keeps (the runtime gate reads the channel's held positions as present, on an active save and on a draft promotion), the cleared form '' clears and is stored as written, a vanished position drops its row. Right.
    • Stored shape: the stored row, every new history row and the content hash carry no credential; the served shape is the one [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 already defined, so no authorable spelling is retired. Right.
    • Save door order: authoring gate, carry-forward, channel store, put; with no crypto provider the channel refuses 503 before any row, channel or stored, is written. Right, and ruled. The channel write preceding the put is declared; a put that then fails leaves the new value live, which neither leaks nor fails open.
    • R2: rollback and revert derive the restored body through the channel's strip inside the restore verb, before the put hashes it; the pre-move history row keeps what it recorded (Q1 B, append-only); the channel keeps its current credential. Right.
    • C1: the clone door refuses a source holding a credential at any table position — a literal (a packaged flow) or channel-held, the outbound signing secret included (Q4 A) — naming classes and the prescription, never a value; the stale [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 clone pin was re-pinned to the refusal; the no-credential control still clones. Right; the accepted cost (a packaged inbound flow is not cloned in one step) is stated in the changeset and the lifecycle doc.
    • Q1 B migration: stored rows still carrying an explicit credential are re-saved through the save door itself (source: migrate-stored, at their own state and package); one ROTATE notice per flow at warn, naming the flow and the class; a row the door refuses for another reason logs at error with the consequence (still cleartext at rest) and the fix; a no-provider refusal defers the whole run with nothing written and no receipt; an applied run writes the sys_migration receipt flow-credential-channel (verified_at null, blocking 0, names only). History and audit rows untouched; packaged flows untouched (Q3 A). Right.
    • Q3 A at the outbound http node: index-gated — the node asks the channel only when holdsFlowCredential says the position is held, otherwise the literal signs; a held secret that does not come back refuses the node and nothing is sent unsigned; the cleared form never asks. Right.
    • Q3 A at the inbound door: WRONG in one composition. AutomationEngine.hookSecretResolver asks the channel on every post whenever a literal is present (resolve(...) ?? literal), without the holds gate the http node applies; and FlowCredentialChannel.resolve THROWS when it has no data engine (and lets a failed row read propagate), although the FlowCredentialSource.resolve contract this PR writes says undefined means the channel holds nothing there and only a HELD credential that does not come back throws. On a composition where the automation plugin resolves no objectql or data service, or where the channel's object is not registered on the engine it found, a packaged inbound flow registers and arms on its literal and is then answered 503 on every post — a flow Q3 A leaves untouched ("otherwise the bound definition's value"). Every shipped composition carries ObjectQL, so reach is not measured here (read-only), but the shape is structural, it is the one asymmetry between the two Q3 A doors in this diff, and the engine-level pin for the literal fallback uses an in-memory source that answers undefined, so the real channel's no-engine branch is unpinned. Fix, one of: in hookSecretResolver, when the definition carries a usable literal and source.holds(flowName, startNodeId, key) is false, return the literal without asking (the http node's shape; the index refreshes on every local write and on metadata:reloaded, so an overlay row still wins at the next read); or have FlowCredentialChannel.resolve answer undefined when it has no engine, per its own contract. Pin: the real FlowCredentialChannel with a resolveEngine that answers undefined, a literal api flow registered on an engine using it as its source, and binding.resolveSecret() resolving to the literal (trigger-api 202).

    ② Semver level

    • .changeset/20790-flow-credential-channel.md: minor for @objectstack/spec, @objectstack/service-automation, @objectstack/metadata-protocol, @objectstack/trigger-api, @objectstack/runtime — exactly the five released packages whose surface the diff widens (packages/qa/dogfood is the harness: a dev dependency, a vitest source alias, three lockfile lines). Matches.
    • Clause-②: yes (widening) on PR body line 2 and in the changeset; yes takes at least minor, met. No authorable key, export, config field or stored spelling is removed or renamed, so no (narrowing) arm is owed: the two new refusals (C1 at the clone door, no-provider at the save door) are state-conditioned refusals that ruling 5942356310 read as non-breaking with both costs stated, and the changeset states them. Right.
    • ADR-0087: one marker, not-required (no-migration-prescription), beside the rotation instruction (Q1 B) that names both triggers of the move (the first boot with a provider, or a provider registering after a boot without one). The marker reads as non-breaking; Lint & Repo Gates, which carries the ADR-0087 registration and changeset checks, concluded success. Right.
    • Spec artifacts: a constant's value changed, no describe text, export or authorable key; TypeScript Type Check (the spec generated-artifact gates) concluded success. Right.

    ③ Boundary flags

    Dev flags (reports 5948978079 and 5949953593, PR Acceptance notes), each answered:

    • Size over the ruled band, under 5,000, no split — ruled ⛔ no split; answered.
    • packages/runtime/src/domains/automation.ts outside revision 5942559287's list — inside claim 5935167060, where the clone handler must consult C1; covered by the as-built claim revision 5949091357; answered.
    • The protocol's held-position read on a draft promotion — the ruling's publish-gate restored-credential read, needed for a draft whose secret the channel holds to be publishable at all; answered, right.
    • Dogfood harness and four generated artifacts (tenancy census, tenant-audit census page and counts, engine-double-contract ledger), each written by its own gate — Lint & Repo Gates success on the head; answered.
    • Package duplication refused by the runtime authoring gate when a channel-held inbound flow is in the package — consistent with C1 (a copy holds no secret and never shares one); answered, not widened.
    • Inert migration mode and a legacy draft published with no provider both refused 503 — the no-provider rule at those doors; measured by the dev, the hunks lie outside this diff; answered.
    • Channel keying env-wide by flow name and state — consistent with the engine's bare-name flow map and allowOrgOverride: false on flows: same-named stored rows in two packages resolve to one armed flow, so one slot; answered.
    • Receipt write not on the durability-critical callee list — an advisory receipt (blocking 0, verified_at null), and its failure logs at error with consequence and fix; right level; answered.
    • Write order (channel before put) and the per-process presence index (refreshed at boot, kernel ready, reload and local writes; the value always read live) — declared; answered, and that index is the gate the ① fix should use.
    • Premise shift (the MCP stdio door was already projected by fix(mcp)!: the MCP stdio engine-only reader joins the stored-metadata-body family (exit one) #21228; the administrator engine read still returned both credentials) — stated, not chosen silently; the ruling's MCP stdio pin is served by the dogfood pin's administrator engine read, the reader that transport serves from; answered. [security] Stored-metadata-body family: two exits #21120 did not reach. An engine-only door serves an administrator credential material in cleartext, and the data door serves a content hash computed over the withheld credentials #21207 stays open for the other types, not widened.
    • origin/main ahead at derivation — main was merged into the head; the queue rebuilds the merge; answered.
    • PR body Evidence heading still names head 80b4647b2 — this head's measurements are in report 5949953593 and in the check-runs; cosmetic; the patch round may refresh it.
    • Round-2 narrowed local gate run (73 unrun, all green at the prior head) — the full farm on this head is the gate verdict; answered.
    • Attribution reminder vs the model-free trailer pair — the form AGENTS.md keeps; right.

    open_questions: empty on both reports for this head. Every earlier fork (Q1–Q4; Q-A, Q-R, Q-C) carries a maintainer ruling on the card and is executed as ruled.

    Escalated: nothing that needs a decision. The one ① finding is a patch round on this branch, after which the new head owes a fresh record.

    Implemented-by: claude/issue-20790-flow-hook-secret-seam
    Reviewed-by: cse_01DiCSbmJrkzNhuEAier4VoJ

    VERDICT: FAIL

    One derived judgment is wrong: the inbound door's Q3 A literal fallback is refused on a composition where the credential channel has no reachable store, against the contract the PR itself writes for FlowCredentialSource.resolve and against the http node's own shape. Every public-surface and semver judgment is right; the fix is one gate and one pin.


    Generated by Claude Code

  22. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20790,
    "status": "done",
    "branch": "claude/issue-20790-flow-hook-secret-seam",
    "pr": "#21377",
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ — mode:subagent, the dispatching domain:services seat 2 session id",
    "premise_still_valid": true,
    "summary": "Patch round for contract review 5950334951 (FAIL, one finding: Q3 A at the inbound door). Commit 84d3d29 fixes AutomationEngine.hookSecretResolver in the http node's shape, so both doors read one rule. With a literal start-node secret, the reader asks the channel only when holds() reports that position; otherwise it answers the literal without touching the channel. A held secret that does not come back still rejects, so trigger-api answers 503 and never verifies against the literal. FlowCredentialChannel.resolve keeps its throw with no engine, as ordered. The FlowCredentialSource contract text now says that callers with a fallback ask resolve only for a held position. The stale-index meaning is stated in the code and in the PR body: a row written after the last index refresh loses to the literal until the next refresh, as at the http node. Pins: (a) and (b) at engine level with the real channel (service-automation) and through the real ApiTrigger (dogfood tests 9 and 10); (c), the existing Q3 A pin, stays green. origin/main was merged (417ba1f). The PR body gained a 'Contract review round 1' section, and its Evidence heading now names 417ba1f.",
    "tests": "Fix commit 84d3d29. Pins: service-automation flow-credential-channel.test.ts plus api-trigger-secret-channel.test.ts, 17 passed. Dogfood flow-credential-channel.dogfood.test.ts, 10 passed (tests 9 and 10 new). Ablations at 84d3d29, each in ablate-r3.sh under os-verify-lock with a trap restore. Each mutated engine.ts through scripts/ablation-replace.mjs (anchor hit, marker count 1 in src), ran the SA pins, rebuilt service-automation, found the marker in dist with ablation-dist-preflight, ran the dogfood pin, then restored. Each restore was proven: marker count 0, file blob equal to the HEAD blob d599cce4f, preflight --absent passed, SA 17/17 and dogfood 10/10 green, git diff HEAD empty. E1 (holds gate removed): SA red 1 of 17 ('with no reachable store, the literal verifies...'); dogfood red 1 of 10 (test 9: expected 503 to be 202). E2 (a held but unreadable secret falls back to the literal): SA red 1 of 17 (the control resolved to the literal instead of rejecting); dogfood red 1 of 10 (test 10: expected 202 to be 503). At merged head 417ba1f, under the lock: service-automation full suite 166 files, 2053 passed; trigger-api 2 files, 30 passed; dogfood pin 10/10; service-automation typecheck (incl. test-typecheck) and dogfood typecheck exit 0. Builds at 417ba1f: the dogfood, lint and client-react closures plus trigger-api (64/64 tasks), and seven packages whose dist the dual-build gate reads (35/35, cached).",
    "gates": {
    "derived_at": "417ba1fa6",
    "command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands",
    "derived": 130,
    "ran": 130,
    "nonzero_final": 0,
    "not_measured": 0,
    "reconcile": "Run reconciliation — 130 derived, 130 run, 0 NOT-MEASURED, 0 UNRUN.",
    "prerequisite_reruns": "check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3, seven packages with no dist in the fresh worktree). After those packages were built, it was re-run and exited 0. It is recorded as not measured, not as a failure.",
    "stale_note": "None: the derivation at 417ba1f flagged no stale tree.",
    "ci_at_report": "in_progress — check runs on 417ba1f: 17 completed, 0 failed, 23 in progress"
    },
    "line_budget": {
    "changed_lines": 3646,
    "added": 3557,
    "removed": 89,
    "files": 36,
    "split": {
    "implementation_src": "+1762 -39",
    "tests": "+1678 -11"
    },
    "ruled_band": "2300 ± 500",
    "threshold": 5000,
    "over_band": true,
    "split_needed": false,
    "this_round": "+114 (engine +14 incl. contract text; pins +100)"
    },
    "deviations": [
    "Beyond the ordered gate, the FlowCredentialSource interface doc gains one sentence: with no reachable store resolve throws, so callers with a fallback ask it only for a held position. This makes the contract text match the channel's kept throw, which is what the review found contradictory. No signature changed.",
    "The trigger-api legs of pins (a) and (b) live in the dogfood pin file (tests 9 and 10). service-automation does not depend on trigger-api, and dogfood already carries both. The tests take the channel class from the live engine's source instead of importing service-automation, which dogfood does not declare.",
    "The PR body's Evidence heading now names 417ba1f, as ordered. The lines under it keep their own inline shas (80b4647 for the runtime, metadata-protocol and spec suites). This round's measurements are in the new 'Contract review round 1' section."
    ],
    "files_changed": [
    ".changeset/20790-flow-credential-channel.md",
    "content/docs/automation/flows.mdx",
    "content/docs/concepts/metadata-lifecycle.mdx",
    "content/docs/permissions/tenant-audit-census.mdx",
    "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
    "packages/metadata-protocol/src/index.ts",
    "packages/metadata-protocol/src/protocol.credential-channel.test.ts",
    "packages/metadata-protocol/src/protocol.ts",
    "packages/metadata-protocol/src/sys-metadata-repository.ts",
    "packages/qa/dogfood/package.json",
    "packages/qa/dogfood/test/flow-credential-channel.dogfood.test.ts",
    "packages/qa/dogfood/vitest.config.ts",
    "packages/runtime/src/domains/automation-flow-clone-credential.test.ts",
    "packages/runtime/src/domains/automation-flow-credential-projection.test.ts",
    "packages/runtime/src/domains/automation.ts",
    "packages/runtime/src/flow-clone.ts",
    "packages/services/service-automation/src/activation-ledger-registration.test.ts",
    "packages/services/service-automation/src/api-trigger-secret-channel.test.ts",
    "packages/services/service-automation/src/builtin/http-node-channel-signing.test.ts",
    "packages/services/service-automation/src/builtin/http-nodes.ts",
    "packages/services/service-automation/src/engine.ts",
    "packages/services/service-automation/src/flow-credential-channel.test.ts",
    "packages/services/service-automation/src/flow-credential-channel.ts",
    "packages/services/service-automation/src/flow-credential-migration.test.ts",
    "packages/services/service-automation/src/flow-credential-migration.ts",
    "packages/services/service-automation/src/flow-credential-projection.ts",
    "packages/services/service-automation/src/index.ts",
    "packages/services/service-automation/src/plugin-suspended-run-wiring.test.ts",
    "packages/services/service-automation/src/plugin.ts",
    "packages/services/service-automation/src/sys-flow-credential.object.ts",
    "packages/spec/src/system/constants/platform-object-names.ts",
    "packages/triggers/trigger-api/src/api-trigger.test.ts",
    "packages/triggers/trigger-api/src/api-trigger.ts",
    "pnpm-lock.yaml",
    "scripts/engine-double-contract.pinned.json",
    "scripts/platform-object-tenancy-census.json"
    ],
    "files_changed_this_round": [
    "packages/services/service-automation/src/engine.ts",
    "packages/services/service-automation/src/flow-credential-channel.test.ts",
    "packages/qa/dogfood/test/flow-credential-channel.dogfood.test.ts"
    ],
    "mcp_calls": "0 — no MCP tool was called. GitHub reads used the REST CLI: the review comment, PR 21377 before and after the patch, and a check-runs snapshot.",
    "api_writes": "2 — both through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches): (1) issue_patch of PR 21377's body (Contract review round 1 section added, Evidence heading sha updated), read back identical, 12535 bytes sent and stored; (2) this os-dev-report comment on #20790 via scripts/pm/post-stamped.mjs. Branch pushes (be755de..84d3d29, then ..417ba1f) went through git, not REST.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "cleanup": "After this comment posts: the worktree's node_modules is removed first, then git worktree remove, with no --force. No dev server or monitor was started."
    }


    Generated by Claude Code

  23. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: 417ba1fa6fc6399a6bc077459c45d239542245e1
    Local-runs: none

    Round 2 on PR #21377 (claude/issue-20790-flow-hook-secret-seam, draft, 36 files, +3557 / −89) for card #20790, under rulings 5909514730 (Q1 B · Q2 A · Q3 A · Q4 A), 5910098951 (17.x) and 5942356310 (A / R2 / C1). Inputs: the card body and every comment (the round-1 record 5950334951 and the patch report 5951747088 read as inputs, not as verdicts for this head), the PR body, its file list and the net diff against main at this head, and the check-runs on this head. ⛔ Classes and positions only: no value, request, header, route or field path is named here.

    Gate verdicts on this head. Every one of the 43 check-runs on the head has completed: 39 success, 4 skipped, none red. Every required context concluded success — Lint & Repo Gates, TypeScript Type Check (with its workspace, source-gate, consumer-gate and debt-ledger legs), Test Core (and all six shards), Dogfood Regression Gate (and all three shards), Build Core, Temporal Conformance (live PG + MySQL) and Governed Surface Queue Guard — as did every other gate-carrying run (Build Docs, Dogfood Verify CLI, Check Changeset, Spec property liveness, Validate Package Dependencies, Check Documentation Links, Flag docs affected by code changes, and the claim and single-writer guards). The four skips are the expected ones: Console Pin Gate and Packed-tarball smoke, plus the re-triggered Auto Label and Check PR Size jobs of the later workflow run, whose first runs on this same head concluded success. Governed surfaces: the file list touches none of the register's paths; head repo equals base repo; the PR is draft with auto-merge unarmed. Size 3,646 changed lines, under 5,000; over the ruled 2,300 ± 500 band with the ruling's ⛔ no-split standing.

    ① Derived judgments

    The round-1 judgment, re-judged on this head: resolved. Round 1 found Q3 A wrong at the inbound door: with a literal start-node secret the hook reader asked the channel on every post, and the channel's read throws with no reachable store, so a packaged inbound flow on a composition with no data engine armed on its literal and was answered 503 on every post. On this head AutomationEngine.hookSecretResolver reads one rule with the http node: with a literal it asks the channel only when the channel's index reports that position held (holds), otherwise it answers the literal without touching the channel; a held secret that does not come back still rejects (FlowCredentialChannel.resolve keeps its throw), so trigger-api answers 503 and never verifies against the literal; a held row that resolves to nothing falls back to the literal; a withheld position yields a reader only when held; a cleared or unusable one yields none, so the registration refusal stands for those. The FlowCredentialSource contract text now states the throw with no store and the rule a caller with a fallback follows, so the contract and the channel agree. Pins: the real channel with a resolver that answers no engine, a packaged literal flow, the binding's resolveSecret() answering the literal (service-automation) and a correctly signed post answering 202 through the real ApiTrigger (dogfood test 9); the control, a held position whose store becomes unreachable, rejecting with FlowCredentialUnresolvableError and the post answering 503 whether signed with the literal or the held value (service-automation and dogfood test 10); the existing Q3 A pin (a held row wins over the literal) still green under the new rule. The stale-index meaning (a row written after the last refresh loses to the literal until the next refresh at boot, kernel ready, metadata reload or a local write) is stated in the code and the PR body, and is the http node's shape. Right.

    Public surface — each a widening, each right under ruling A / R2 / C1:

    • @objectstack/spec: one data line, PLATFORM_OBJECTS_BY_PACKAGE['service-automation'] lists sys_flow_credential (ruling A, the fix(spec): register sys_environment_credential in CLOUD_PROVIDED_OBJECT_NAMES #18851 precedent). Right.
    • @objectstack/metadata-protocol: exported type MetadataCredentialChannel (store, heldPaths, strip); public method registerCredentialChannel(type, channel) on the protocol class beside the authoring gate, singular or plural type name, a second registration replacing the first; SysMetadataRepository.restoreVersion gains the optional deriveRestoredBody, shaped like the promote verb's option and applied inside the same call before the put hashes it (R2). Right.
    • @objectstack/service-automation: the platform object SysFlowCredential (one secret-typed value field, access default private, apiEnabled: false, apiMethods: [], trackHistory: false, searchable: false, engine-owned, a unique index on flow name, state and a bounded position digest); FlowCredentialChannel, its two error classes carrying the ADR-0112 pair SERVICE_UNAVAILABLE / 503, the object and value-field constants and two types; migrateFlowCredentialsIntoChannel with its receipt id and result type; FlowCredentialSource and FlowCredentialHolding; on AutomationEngine the four methods setFlowCredentialSource, holdsFlowCredential, resolveFlowCredential, flowCredentialHoldings; FlowTriggerBinding gains the optional resolveSecret; the projection module gains the class-label table and the position walk (flowCredentialPositions, stripFlowCredentialValues), the same table and region walk the read projection uses, so the write channel and the read projection cannot disagree about where a credential is. Right: one seam ([security] The webhook signing secret is stored in cleartext in sys_webhook.definition_json #7799, resolveSecretField), no second mechanism, no per-door redaction added.
    • @objectstack/trigger-api: FlowTriggerBinding.resolveSecret (optional); start arms a binding that carries a reader and no literal, and still refuses one that carries neither; every post reads the secret at that moment, so a rotation applies to the next post and nothing re-arms; a secret that cannot be read, is absent or is blank answers 503 SERVICE_UNAVAILABLE before verification and enqueues nothing; an unknown hook still answers 404 before any read, so there is no oracle. Right.
    • @objectstack/runtime: flow-clone.ts exports the C1 refusal (409 RESOURCE_CONFLICT), its holding types and the literal-holdings fallback through the registered flow redactor (a host composing another engine, which has no channel and so cannot hold a channel-held one); the clone handler consults it before the same-name refusal and registers nothing on a refusal. Right.

    Accept set:

    • Authoring is unchanged and the [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 rule holds: an explicit value rotates, absent keeps (the runtime gate reads the channel's held positions as present, on an active save and on a draft promotion), the cleared form clears and is stored as written, a vanished position drops its row, so a credential never outlives its position. Right.
    • Stored shape: the stored row, every new history row and the content hash carry no credential, at every depth a node can sit (the loop-body pin); the stored form is byte-identical to what a read serves, so no authorable spelling is retired. Right.
    • Save door order: authoring gate, carry-forward, channel store, put. The channel runs last before the put, so a credential a pre-move row still held moves with the save instead of being dropped. With no crypto provider the channel refuses 503 before any row, channel or stored, is written (the engine's fail-closed secret write becomes the save's refusal; a body with no credential still saves). Right, and ruled. The channel write preceding the put is declared; a put that then fails leaves the new value live, which neither leaks nor fails open.
    • Compositions: sys_flow_credential rides the run-object manifest registration and is registered alone under suspendedRunStore: 'memory', so every composition has the table; the channel joins the protocol at start() before the inert-mode return, so the stored re-save tool never stores a credential back into a definition; a protocol with no channel registration is named loudly. Right.
    • R2: rollback and revert derive the restored body through the channel's strip inside the restore verb, before the put hashes it; the restore appends one history row carrying none; the pre-move history row keeps what it recorded (Q1 B, append-only); the channel is not written and keeps its current credential; a type with no channel restores byte for byte as before. Right.
    • Draft to active: a draft save writes draft-state rows and never rotates the live credential; the publish materializer promotes a draft row over the live one per position, keeps a live row the draft left withheld, drops a live row the draft cleared, and consumes every draft row; the publish gate reads the draft's held positions (a draft row, or the live one the promotion keeps) as present. Right.
    • Delete: the mutation projector prunes the channel rows of every state whose stored row is gone, so a later flow of the same name inherits nothing, and the withheld form re-saved under that name is refused by the publish gate. Right.
    • C1: the clone door refuses a source holding a credential at any table position — a literal (a packaged flow) or channel-held, the outbound signing secret included (Q4 A) — naming classes and the prescription, never a value; nothing is registered on a refusal; the no-credential control still clones; the stale [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 clone pin is re-pinned to the refusal. Right; the accepted cost (a packaged inbound flow is not cloned in one step) is stated in the changeset and the lifecycle doc.
    • Q1 B migration: stored rows still carrying an explicit credential are re-saved through the save door itself (source: migrate-stored, at their own state, package and organization); one ROTATE notice per flow at warn, naming the flow and the class; a row the door refuses for another reason logs at error with the consequence (still cleartext at rest) and the fix, and the rest still move; the first no-provider refusal defers the whole run with nothing written and no receipt; an applied run writes the sys_migration receipt flow-credential-channel (verified_at null, blocking 0, the failure count as advisory, names only), warns that the log is the only record when the ledger object is not registered, and logs at error when the receipt write fails. It is scheduled at kernel ready and again on every crypto-provider registration, serialized, idempotent, never fatal, unsubscribed at destroy. History and audit rows untouched; packaged flows untouched (Q3 A). Right.
    • Q3 A at the outbound http node: index-gated — the node asks the channel only when holdsFlowCredential reports the position held, otherwise the literal signs; a held secret that does not come back refuses the node and nothing is sent unsigned; the cleared form never asks the channel and sends unsigned on purpose. Right, and now the same rule as the inbound door.
    • The presence index: loaded at start, kernel:ready and metadata:reloaded (awaited before the flow re-sync, so a flow and its held positions arrive together), kept current by every local write, promotion and prune, and corrected by every live read; a failed load warns loudly that held inbound flows are refused until the next load. Neither the index load nor the migration scan is bounded, and the engine's query defaults carry no bound, so neither truncates. Right.

    ② Semver level

    • .changeset/20790-flow-credential-channel.md: minor for @objectstack/spec, @objectstack/service-automation, @objectstack/metadata-protocol, @objectstack/trigger-api, @objectstack/runtime — exactly the five released packages whose surface the diff widens; packages/qa/dogfood is the harness (a dev dependency, a vitest source alias, three lockfile lines) and the generated ledgers publish nothing. Matches.
    • Clause-②: yes (widening) on PR body line 2 and in the changeset; yes takes at least minor, met. No authorable key, export, config field or stored spelling is removed or renamed, so no (narrowing) arm is owed: the two new refusals (C1 at the clone door, no-provider at the save door) are state-conditioned refusals that ruling 5942356310 read as non-breaking with both costs stated, and the changeset states them; the patch round changed no signature. Right.
    • ADR-0087: one marker, not-required (no-migration-prescription), beside the rotation instruction (Q1 B), which names both triggers of the move (the first boot with a provider, or a provider registering after a boot without one). The marker reads as non-breaking; Check Changeset and Lint & Repo Gates, which carry the changeset and ADR-0087 registration checks, concluded success on the head. Right.
    • Spec artifacts: a constant's value changed; no describe text, export or authorable key; the Type Check jobs that carry the spec generated-artifact gates concluded success. Right.

    ③ Boundary flags

    Dev flags (reports 5948978079, 5949953593 and 5951747088; the PR's Acceptance notes and its Contract review round 1 section), each answered:

    • Size 3,646 changed lines (+114 this round: the engine fix and two pins), over the ruled 2,300 ± 500 band, under 5,000, no split — ruled ⛔ no split; answered.
    • packages/runtime/src/domains/automation.ts outside revision 5942559287's list — inside claim 5935167060, where the clone handler must consult C1; covered by the as-built claim revision 5949091357; answered.
    • The protocol's held-position read on a draft promotion — the ruling's publish-gate restored-credential read, needed for a draft whose secret the channel holds to be publishable at all; answered, right.
    • Dogfood harness and four generated artifacts (tenancy census, tenant-audit census page and counts, engine-double-contract ledger), each written by its own gate — the counts page's new rows match the diff's write verbs per file; Lint & Repo Gates success on the head; answered.
    • Package duplication refused by the runtime authoring gate when a channel-held inbound flow is in the package — consistent with C1 (a copy holds no secret and never shares one); answered, not widened.
    • Inert migration mode and a legacy draft published with no provider both refused 503 — the no-provider rule at those doors; measured by the dev, the hunks lie outside this diff; with a provider the migration re-saves a legacy draft row at its own draft state at kernel ready or at provider registration, before an ordinary publish; answered.
    • Channel keying env-wide by flow name and state — consistent with the engine's bare-name flow map and allowOrgOverride: false on flows: same-named stored rows in two packages resolve to one armed flow, so one slot; the migration still passes each row's own organization; answered.
    • Receipt write not on the durability-critical callee list — an advisory receipt (blocking 0, verified_at null), its failure logged at error with consequence and fix; right level; answered.
    • Write order (channel before put) and the per-process presence index — declared in the code, the PR body and the changeset; that index is the gate both doors now use; answered.
    • Premise shift (the MCP stdio door was already projected by fix(mcp)!: the MCP stdio engine-only reader joins the stored-metadata-body family (exit one) #21228; the administrator engine read still returned both credentials) — stated, not chosen silently; the ruling's MCP stdio pin is served by the dogfood pin's administrator engine read, the reader that transport serves from; answered. [security] Stored-metadata-body family: two exits #21120 did not reach. An engine-only door serves an administrator credential material in cleartext, and the data door serves a content hash computed over the withheld credentials #21207 stays open for the other types, not widened.
    • Patch-round deviations: the FlowCredentialSource contract sentence (the throw with no store, and the rule a caller with a fallback follows) is the contract-text correction round 1 asked for, with no signature changed; the trigger-api legs of pins (a) and (b) live in the dogfood pin, which already depends on both packages and takes the channel class from the live engine's source — the right placement, since service-automation does not depend on trigger-api; the PR body's Evidence heading names this head while the inline suite lines keep their own earlier shas, with this head's measurements in the round-1 section and the report — cosmetic and consistent; answered.
    • origin/main merged at this head; the queue rebuilds the merge; answered.
    • Attribution reminder vs the model-free trailer pair — the form AGENTS.md keeps; right.
    • The dev's gate union at this head (130 derived, 130 run, 0 NOT-MEASURED, 0 UNRUN; the dual-build gate first recorded as not measured and re-run after a build) — the check-runs on the head are the verdict, read above; answered.

    open_questions: empty on all three reports for this head. Every earlier fork (Q1–Q4; Q-A, Q-R, Q-C) carries a maintainer ruling on the card and is executed as ruled.

    Escalated: nothing that needs a decision.

    Implemented-by: claude/issue-20790-flow-hook-secret-seam
    Reviewed-by: cse_01DiCSbmJrkzNhuEAier4VoJ

    VERDICT: PASS

    The round-1 judgment is resolved on this head: both doors now read the one Q3 A rule, with the contract text, the pins and the control to hold it. Every other derived judgment, the semver level and every boundary flag read right, and every gate on the head is green.

  24. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing: PR #21377 at 417ba1fa6 · 2026-10-02T12:24Z

    Seat domain:services#2 · session session_01DiCSbmJrkzNhuEAier4VoJ · under ruling 5942356310 (A / R2 / C1). ⛔ Classes and positions only.

    • The contract review of record, round 2, is PASS: 5952247884 (CONTRACT_REVIEW_TIER, Head-sha: 417ba1fa6…, the head unchanged).
    • Round 1's single finding (5950334951, Q3 A at the inbound door) is resolved. The inbound literal path asks the channel only for a position holds reports, which is the http node's rule. A held secret that does not come back still answers 503 and is never verified against the literal. Both directions are pinned at engine level and through the real ApiTrigger.
    • Pre-landing reads on this head:
      • 43 check-runs completed and 0 are red; the skips are the expected ones;
      • mergeable clean;
      • no governed path;
      • 3,646 changed lines, under 5,000. The ruling's no-split stands.
    • Landing now: needs:contract-review comes off, then pr_ready + automerge_enable through the queue.
    • After the merge: DELETE /api/v1/packages/:id answers success when the sys_packages delete fails, so the package is gone from the live registry and comes back after the next restart #21276 (deletePackage in the same protocol.ts) is no longer serial-blocked. Its claim follows. Rotation, per Q1 B, is the operator's action named in the changeset.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedenhancementNew feature or requestpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions