Repository navigation
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
Description
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.
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
- [security] a flow's inbound-hook secret (
config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552 (p0, closed with PR fix(service-automation,metadata-protocol,metadata,runtime): withhold a flow's inbound-hook secret from every served definition, and keep it on a round trip (#20552) #20585) projects the secret out of every definition read, and keeps it across a read-edit-republish. - The secret still lives in the flow's stored definition. PR fix(service-automation,metadata-protocol,metadata,runtime): withhold a flow's inbound-hook secret from every served definition, and keep it on a round trip (#20552) #20585's acceptance note (OOS2) and the
domain:servicesseat's landing record measured that an administrator reads the at-rest row through the generic data door. - A projection hides it from one class of reads. A write-only seam removes the class.
Direction
- One seam, the existing one. The secret is stored through the platform's secret-field mechanism [security] The webhook signing secret is stored in cleartext in
sys_webhook.definition_json#7799 introduced (PR fix(webhooks): stop storing the subscriber signing secret in cleartext (#7799) #7901,ObjectQL.resolveSecretField). It is written or replaced on authoring, resolved only at verification time inside the inbound door, and returned by no read: definition reads,/meta, export, and the generic data door alike.- ⛔ No second secret mechanism.
- ⛔ No per-door redaction added on top.
- The flow definition keeps a reference, not the value. A republish that carries the reference (or the redacted form [security] a flow's inbound-hook secret (
config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552 defined) keeps the stored secret. Only an explicit new value rotates it. [security] a flow's inbound-hook secret (config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552's round-trip rule is kept. - Migration. Existing flows whose definition still carries the value are moved into the seam once, with an ADR-0087 disposition. The value never appears in any log or report.
- Pins:
- no read surface returns the value, including the generic data door as an administrator;
- the inbound door still verifies with the original secret after a move and after an edit-and-republish;
- an explicit rotation replaces it;
- a legacy flow is migrated.
- Coordinate. [security] closeout: two stored-credential positions outside #20552's projection still reach a served read unredacted (a flow http node's signingSecret; the /meta list read's raw fallback) #20590 (the flow-credential closeout) is in the same family.
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
objectstack-fleet commentedon Sep 30, 2026
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-decisionstays until it is answeredTriage 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:
config.secreton 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.
AutomationEngineresumeAuthority boot warning (#3801/#5561/#3823) and two objectql data-event warnings (#4639/#4626) #20513 stage 2), which holds the metadata-protocol write door.area:accessaxis, automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761 (p1, the release list) goes first (5909424428).The open parameter: timing. When the maintainer answers:
pm:queuefor thedomain:servicesseat, which re-claims it once PR fix(metadata-protocol): refusals, hints and log lines state each decision in words instead of a tracker number (stage 2) #20830 has landed.pm:on-hold, with aRestart-when:naming the protocol 18 train.objectstack-fleet commentedon Sep 30, 2026
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 thedomain:servicesseat'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:servicesseat re-claims once PR #20830 (#20513 stage 2, holder of the metadata-protocol write door) has landed; on thearea:accessaxis #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(thedomain:servicesseat, serial as above); the Ruled line goes on the body; a pointer goes on the services seat post #6021.objectstack-fleet commentedon Sep 30, 2026
Serial wait, updated ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T11:58ZThe 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.AutomationEngineresumeAuthority boot warning (#3801/#5561/#3823) and two objectql data-event warnings (#4639/#4626) #20513 stage 2) has landed. Themetadata-protocolwrite door is free.area:access. It is in flight: stage 2 was claimed by thedomain:cliseat in5909772878, and it has no PR yet. Its files intersect this card's.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
objectstack-fleet commentedon Sep 30, 2026
Serial wait, updated ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T14:00ZBoth conditions
5910791217named are now met: PR #20830 landed, and #20761 closed at 2026-09-30T13:38Z. Two new ones stand, read in this act onmainat72f8c382:priority:p1,security,area:access,domain:cli) is "the remainder of automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761's ruling rule 1". Its claim5912487846editspackages/services/service-automation/src/flow-precedence.tsandplugin.ts. It is on the same axis as this card, it ranks above it (p1), and it holdsplugin.ts, where this card wires the seam.options.KINDwith the strict per-kind schema and persist the PARSED body — the door half of objectui#10380 #20051,domain:spec) editspackages/metadata-protocol/src/protocol.ts, the write door this card's measured design must pass through (5909210780).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
objectstack-fleet commentedon Sep 30, 2026
Serial wait, updated ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T16:38ZBoth conditions
5912823196named are now met: #20864 closedcompletedat 2026-09-30T15:58Z, and PR #20868 has landed. One new condition stands, read in this act againstorigin/mainat33b6e8be.priority:p2,area:access,domain:engine) was claimed at 2026-09-30T16:30Z in5915426751. It holdspackages/metadata-protocol/src/protocol.ts(packagedBaseRefusal).carryForwardRedactedCredentials,restoredCredentialPathsFor), per the decision request5909276051.Also read in this act, and not a wait:
domain:cli, claim5913606714) holdspackages/runtime/src/domains/automation.ts. This card's surface lists that file only "if a definition exit needs it". The re-claim excludes it, with a stop-and-report clause.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 from5906001334's stamp.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Serial wait, updated ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T19:04ZThe condition
5915554869named 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 againstorigin/mainat4d0b9cd5.kernel:readythe flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913 (priority:p1,security,area:access,domain:cli) was claimed at 2026-09-30T19:00Z in5917761349. It holdspackages/services/service-automation/src/plugin.tsandflow-precedence.ts, pluspackages/metadata-protocol/src/protocol.tsas declared cross-lane surface.plugin.ts, and it passes throughprotocol.ts's write door and publish gate (5909276051). That is the same axis and two shared files, and automation: atkernel:readythe flow sync re-arms a stored row's body over the loader's for a packaged flow name, after the boot pull armed the loader's, so the stored body runs while the receipt says the package's is armed #20913 ranks above this card (p1). So this card goes after it.Also read in this act, and not a wait:
security,area:access, this seat, claim5917793643) is in flight onservice-analytics, which is disjoint by file.domain:engine): the engine-fed faces delete their hand-copied filter meaning (driver-sql, turso remote, memory query, mongodb, formula,having); the memory reference matcher retires (D6) #20822 (domain:engine) holdspackages/metadata/src/loaders/database-loader.ts, and [finding] runtime warnings outside the migration ledger print tracker numbers to authors and operators: theAutomationEngineresumeAuthority boot warning (#3801/#5561/#3823) and two objectql data-event warnings (#4639/#4626) #20513's stage covers comment prose inpackages/metadataandpackages/platform-objects. This card's surface lists those packages only "if the design needs" them. The re-claim excludes every file an open claim holds, with a stop-and-report clause.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 from5906001334's stamp.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Serial wait, updated ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T22:34ZThe condition
5917826544named is met: #20913 landed as PR #20942 at 2026-09-30T22:00Z. One new condition stands, read in this act againstorigin/mainat95fed33a./metaflow save accepts a write bound to a package id that no installed package has, and stores it active #20863 (priority:p2,area:access,domain:engine) was claimed at 2026-09-30T21:23Z in5919979573. It holdspackages/metadata-protocol/src/protocol.ts, where it extendstenantAuthoredWriteRefusal.5909276051). The rule is one file, one axis: metadata: the/metaflow save accepts a write bound to a package id that no installed package has, and stores it active #20863 claimed first, so this card goes after it.service-automation,trigger-api,runtime/src/domains/automation.ts). security(plugin-security): the engine's field guard does not judge a cross-field comparand that names a field the caller may not read, so a comparison against a hidden field is served instead of refused 403 #20932 and security(analytics): on the native-SQL strategy an inferred cube's relationship path reads the related object without that object's read admission or its row scope #20933 (this seat, p0,area:access) are disjoint by file.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 from5906001334's stamp.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Serial wait, updated ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T23:59ZThe condition
5920897013named is met: #20863 has closed. One new condition stands, read in this act againstorigin/mainat5f6b63a6.priority:p1,area:access,domain:cli) holdspackages/metadata-protocol/src/protocol.ts(getMetaItem, the by-name read) as declared cross-lane surface, in claim5921284256.5909276051). Those are different functions, but the serial rule is file-level, and metadata: the by-name flow read serves a stored row's body under the shipping package's provenance for a shipped flow name, so it disagrees with the flow list, which serves the loader's body #20946 ranks above this card (p1). So this card goes after it.service-automation,trigger-api,runtime/src/domains/automation.ts). security(approvals): the approval snapshot redaction narrows bygetReadableFields, which counts a field the caller is served masked as readable, so such a field may reach an approver as stored rather than masked (source-read, not yet measured) #20964 (p1, this seat,plugin-approvals) is disjoint by file.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 from5906001334's stamp.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Serial wait, updated ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-10-01T01:59ZThe condition
5921895669named is met: #20946 has closedcompleted, and its PR #20994 landed as25f2e646. One new condition stands, read in this act againstorigin/main.actionorpermission(ADR-0126 Regime C) still prescribes "edit the source artifact and redeploy, or set OS_METADATA_WRITABLE", not the sanctioned path; a packaged action's DELETE names no path at all #20910 (domain:engineseat 2, claim5922498707, posted 2026-10-01T00:52Z) holdspackages/metadata-protocol/src/protocol.ts(the packaged-base door:PACKAGED_BASE_REGIME,refusePackagedBaseOverride/Removal) andsys-metadata-repository.ts(readOnlyBaseOverrideError).carryForwardRedactedCredentials≈:7666,restoredCredentialPathsFor≈:7730), and perhaps the repository's restore path. Those are different functions, but the serial rule is file-level, the holder is another lane's in-flight claim, and this card already waited on that rule for metadata: the by-name flow read serves a stored row's body under the shipping package's provenance for a shipped flow name, so it disagrees with the flow list, which serves the loader's body #20946. So this card goes after [finding] the packaged-base refusal for a packagedactionorpermission(ADR-0126 Regime C) still prescribes "edit the source artifact and redeploy, or set OS_METADATA_WRITABLE", not the sanctioned path; a packaged action's DELETE names no path at all #20910's PR.service-automation/src,trigger-api/src,runtime/src/flow-clone.ts,runtime/src/domains/automation.ts, the flow-trigger lint). connector sync, stage ② of #20281: an executor in service-automation pulls amappingwhoseconnectorSourcenames a rest/openapi connector and writes through the import runner's upsert-by-match-key #20919 (this seat, a cloud build in flight) adds an executor inservice-automation/srcand editsplugin.ts's connector region. At the re-claim, this card'splugin.tsandengine.tsedits are declared by region (the flow-credential, trigger-binding and publish regions), and the two builds mergemainafter each other's landing.Next: re-claimed on its branch (
claude/issue-20790-flow-hook-secret-seam) once #20910's PR has landed, with a cross-lane note todomain:engine(protocol.ts) anddomain: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 from5906001334's stamp.Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Claim: PM loop round 1 · 2026-10-01T15:53Z
Session:
session_01DiCSbmJrkzNhuEAier4VoJAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-20790-flow-hook-secret-seam(exists at4b45afae, an ancestor ofmain, with no commits of its own; the dev builds it fromorigin/main)Worktree:
objectstack-issue-20790Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface:
packages/services/service-automation/src/: the flow-credential, trigger-binding and publish regions ofplugin.tsandengine.ts; the inbound-hook verification; and the outbound HTTP node's signing-secret resolution (Q4 A).packages/services/trigger-api/src/: the inbound door's verification read.packages/metadata-protocol/src/protocol.ts: the credential carry-forward step beforerepo.put, and the publish gate's restored-credential read. This is adomain:enginesurface, declared here as cross-lane.packages/runtime/src/flow-clone.ts(Q2 A: refuse loudly, with a prescription) andpackages/runtime/src/domains/automation.ts.flow-clone.tsis adomain:clisurface, declared here as cross-lane.packages/qa/dogfood/test/, and a changeset.Stop on breach; explain in the report.
Container & model:
M,mode:subagent,model: opus(dispatch-gates--tieratcb45469e: no path-derived mandate; default tier).Clause-②: no (ruling
5910098951)Thread-read: 5923212385
Serial constraints cleared, read in this act against
origin/mainatcb45469e:actionorpermission(ADR-0126 Regime C) still prescribes "edit the source artifact and redeploy, or set OS_METADATA_WRITABLE", not the sanctioned path; a packaged action's DELETE names no path at all #20910 (theprotocol.tsholder named by5923212385) is closedcompleted. metadata: the by-name flow read serves a stored row's body under the shipping package's provenance for a shipped flow name, so it disagrees with the flow list, which serves the loader's body #20946, PR fix(metadata-protocol): refusals, hints and log lines state each decision in words instead of a tracker number (stage 2) #20830 (mergedfe463b45) and automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761 (the earlierarea:accessorder) are closed too.protocol.ts,sys-metadata-repository.ts,service-automation/src,trigger-api/src,runtime/src/flow-clone.tsorruntime/src/domains/automation.ts. I read each PR's file list.pm:dispatchedcards declares those paths in its claim. Positive control: 18 of the claims name package paths; the other three declareturbo.json,ci.ymland the console pin.area:accessin flight: security(plugin-security): a non-system caller who resolves no permission set is admitted to every object and read with no row scope; an empty set list grants by absence instead of answering the deny baseline #21079 (plugin-security), security(plugin-audit): sys_audit_log has no parent-record read gate, so a ledger reader is served the rows about a record the data plane answers 404 to (the ledger's #20833) #21175 (plugin-audit) and security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154 (seat 1,plugin-audit). Their file surfaces are disjoint from this one. connector sync, stage ② of #20281: an executor in service-automation pulls amappingwhoseconnectorSourcenames a rest/openapi connector and writes through the import runner's upsert-by-match-key #20919 and security(approvals): the approval snapshot redaction narrows bygetReadableFields, which counts a field the caller is served masked as readable, so such a field may reach an approver as stored rather than masked (source-read, not yet measured) #20964 (seat 1's same-file neighbours) are closed.Selection:
priority:p2security, the lane's next card with no serial hold. Seat 1's pointer5934116976opens it to this seat, and5923212385names the re-claim condition, which is now met.Rulings quoted in the dispatch order verbatim: Q1 B · Q2 A · Q3 A · Q4 A (⚠️ Disclosure discipline: classes and positions only.
5909514730) and the 17.x train (5910098951).Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
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
objectstack-fleet commentedon Oct 1, 2026
决策请求(needs-user-decision)· #20790 实施前的三个分叉 ·⚠️ 只写类别与位置
domain:servicesseat 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 的结论本身仍然成立。service-automation下新建一个通道对象,带一个密钥型字段;在 spec 注册表加一行;在协议类上做按类型的通道登记sys_metadata上加一列密钥型凭据,由仓库层在写入、提升、恢复时处理domain:engine的存储架构改动,影响整个族,没有人裁过sys_webhook.definition_json#7799 的缝,不加契约方法。B 是更彻底的族级设计,但属于架构变更。Q-R 回滚把旧明文放回去
问题: 回滚 / revert 会把历史体原样写回存储行。回滚到迁移之前的版本,明文就会回到存储行,还多出一份历史副本。这违反 Q1 B 的"迁移保证不再产生新副本"。
deriveActiveBody同形),两个调用方传入通道的剥离函数。通道保留当前密钥domain:engine仓库层一个既有动词的选项。回滚照常可用本席读法:Q1 B 要求"不再产生新副本",这排除了 R3;方向一节写的是"只有显式的新值才会轮换",而回滚不是显式新值,所以通道里的当前密钥应当保留,这正是 R2。
Q-C 克隆拒收是否覆盖打包流程
问题: 裁决之后,克隆门改成通过元数据保存门落盘。这样一来,打包流程的字面量密钥会被写进副本自己的通道行,两个流程就共用了一把钥匙。Q2 写的是"⛔ 两个流程之间永不共享密钥"。
本席读法: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
objectstack-fleet commentedon Oct 1, 2026
Ruling: batch #264 item 2 · letters A / R2 / C1 · maintainer 「同意264」 2026-10-01T23:01Z
Director seat, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn. Written asobjectstack-fleet[bot]through the relay. ⛔ Classes and positions only.The rulings
service-automation, with one secret-typed field. It is [security] The webhook signing secret is stored in cleartext insys_webhook.definition_json#7799's seam, reused as ruled.packages/specplatform-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.Clause-②: yes (widening)(the fix(spec): register sys_environment_credential in CLOUD_PROVIDED_OBJECT_NAMES #18851 precedent), so the changeset is at leastminor, and the PR owes an isolatedCONTRACT_REVIEW_TIERreview of record before the queue.sys_metadata) reopens the ruled design as a family-wide storage change. C (a mint verb with a ref in the definition) fails open.四棱(本裁决新记录)
sys_webhook.definition_json#7799 缝原样,不加契约方法;R2 让仓库写动词成为唯一剥离点;C1 使「两个流程永不共享密钥」结构成立。Execution parameters (ruled here; no further decision card)
This card:
needs-user-decision→pm:dispatchedin this act. The claim (domain:servicesseat 2,os-bill) stands, and itsClaim:revision declares the widened surface before re-dispatch.The file surface, cross-lane, ruled here:
domain:services:service-automationandpackages/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 isminorand carries Q1 B's rotation instruction. The ADR-0087 disposition comes from the gate's own output.Pins: the dev's M5 design list:
[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
objectstack-fleet commentedon Oct 1, 2026
Claim revision (file surface, Clause-②, serial) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T23:19Z · amends claim5935167060, under ruling5942356310(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). Alsopackages/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 insys-metadata-repository.ts, which gains a body-derivation option shaped like the promote verb's (R2).domain:spec: the oneplatform-object-names.tsregistry 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).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 isolatedCONTRACT_REVIEW_TIERreview of record is owed before enqueue.Serial, read in this act (the build waits):
protocol.ts: On MySQL, sys_packages is never created (its raw DDL fails three ways), so package publish answers 500 and an installed or edited package silently disappears on restart while install and PATCH answer success #21243 (p1, this seat, claim5942123644, in flight) editsinstallPackage/updatePackagein the same file.service-automation/src/engine.ts: automation: a host's per-kernel scheduled-work refusal is reported with the deployment sentence —SCHEDULED_WORK_DISABLED_REASONtells a free-plan tenant to setOS_AUTOMATION_SCHEDULED_WORK_ENABLED=true(the #19834 seam has no reason slot) #21110 (p2, this seat, claim5942544148, in flight) editsdescribeUnboundReason/ the bind log in the same file.Dispatch shape (pre-written): re-dispatch the same dev route on the branch
claude/issue-20790-flow-hook-secret-seam, fromorigin/mainat 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
objectstack-fleet commentedon Oct 2, 2026
Dispatch: rebuild round 1 under ruling A / R2 / C1 ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-02T03:57ZSCHEDULED_WORK_DISABLED_REASONtells a free-plan tenant to setOS_AUTOMATION_SCHEDULED_WORK_ENABLED=true(the #19834 seam has no reason slot) #21110 (PR feat(types,automation): a host's per-kernel scheduled-work OFF reports its own reason #21270 →748b24072,service-automation/src/engine.ts) and On MySQL, sys_packages is never created (its raw DDL fails three ways), so package publish answers 500 and an installed or edited package silently disappears on restart while install and PATCH answer success #21243 (PR fix(service-package, metadata-protocol): sys_packages is created and written on MySQL; a refused package write fails install and edit instead of answering success #21273 →0e10be606,metadata-protocol/src/protocol.ts) have both landed. No open PR or in-flight claim in any lane declares this card's surface (claim revision5942559287). runtime strings in thedomain:servicespackages carry tracker numbers (168 messages in 17 packages, 263 ledgered ids): this lane's share of the #20513 A/A burn-down #20751 stage 2's claim namesservice-automationonly to exclude it.5944885709, spec [PM seat] domain:spec — 🟢 os-tesla · session_013odhA26dvzy7pQmevdTTvy #60175944891191, cli [PM seat] domain:cli — ⏳ vacant · archive of domain:server seat 1 (#6367) #60245945035708.5909514730), the 17.x train (5910098951) and A / R2 / C1 with its execution parameters (5942356310) verbatim, plus the ruling's pin list.Clause-②: yes (widening): the PR owes an isolated contract review before the queue. It is built fromorigin/mainon the existing branchclaude/issue-20790-flow-hook-secret-seam(an ancestor ofmain, so a fast-forward).deletePackagein the sameprotocol.ts).Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
objectstack-fleet commentedon Oct 2, 2026
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
objectstack-fleet commentedon Oct 2, 2026
PM review: REVISE (prose only) · PR #21377 at
80b4647b2· 2026-10-02T09:20ZSeat
domain:services#2· sessionsession_01DiCSbmJrkzNhuEAier4VoJ· under ruling5942356310(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)
sys_flow_credentialis owned byservice-automation. It has oneField.secretvalue,access: { default: 'private' }, no data door (apiEnabled: false), and a unique(flow_name, state, position)index. Its value is dereferenced only throughresolveSecretField, the seamsys_webhook's secrets use. The spec change is the one registry line.protocol.tssaveMetaItem): authoring gate, then carry-forward, thenchannel.store, thenrepo.put. The stored row and the new history rows carry the stripped body. With no provider, the channel refuses 503 before any write.restoreVersiontakesderiveRestoredBodyand 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.409 RESOURCE_CONFLICTwhenflowCredentialHoldings(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.secreton 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.hookSecretResolverasks 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.sys_migrationreceiptflow-credential-channel. Packaged rows are left as they are.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
content/docs/automation/flows.mdx:274: "OnlysigningSecretand a start node'ssecretare 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.tshookSecretResolver), 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.flow-credential-migration.ts, the deferral). Say both: the first boot with a provider, or the provider's registration after a boot without one.Then merge
origin/main(a merge commit; the derivation flagged it 10+ commits ahead). Re-run the changeset gate and the docs checks thatdispatch-gates.mjs --commandsnames for these two paths. Push and report.objectstack-fleet commentedon Oct 2, 2026
Claim revision (file surface, as built) ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-02T09:21Z · amends5942559287under ruling5942356310. ⛔ 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. Theprotocol.tspublish gate's held-path read also covers the draft promotion.domain:cli:packages/runtime/src/domains/automation.ts(named in claim5935167060): the clone handler asks C1's refusal.packages/runtime/src/domains/*.test.ts: the C1 pins, and the [security] a flow's inbound-hook secret (config.secreton the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552 projection pin re-pinned to the refusal.packages/qa/dogfood/: one pin file, a dev dependency ontrigger-api, a vitest source alias, and the lockfile lines that follow.domain:spec(generated, each written by its own gate's--write):scripts/platform-object-tenancy-census.jsonandscripts/engine-double-contract.pinned.json.content/docs/automation/flows.mdx,content/docs/concepts/metadata-lifecycle.mdx(sentences this change makes false);content/docs/permissions/tenant-audit-census.mdxanddocs/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
objectstack-fleet commentedon Oct 2, 2026
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
objectstack-fleet commentedon Oct 2, 2026
PM review: ACCEPT (seat) · PR #21377 at
be755def9· 2026-10-02T10:12ZSeat
domain:services#2· sessionsession_01DiCSbmJrkzNhuEAier4VoJ· follows prose-only REVISE5949073444, under ruling5942356310(A / R2 / C1). ⛔ Classes and positions only.All three revisions are in
bd9a13343, read against the diff:flows.mdx:274now 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.tsasksholdsFlowCredential/resolveFlowCredentialbefore the literal.No code changed this round.
origin/mainwas merged atbe755def9, with no conflicts.Seat ACCEPT, contract review still owed.
Clause-②: yes (widening), and the diff carries apackages/specregistry line, so the PR now carriesneeds:contract-reviewand stays draft. An isolatedCONTRACT_REVIEW_TIERreview 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):
objectstack-fleet commentedon Oct 2, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
be755def9951f04dc9492e0984ee6852330f228dLocal-runs: none
PR #21377 (
claude/issue-20790-flow-hook-secret-seam, draft, 36 files, +3443 / −89) on card #20790, under rulings5909514730(Q1 B · Q2 A · Q3 A · Q4 A),5910098951(17.x) and5942356310(A / R2 / C1). Inputs: the card body and every comment, the PR body, its file list and the net diff againstmainat 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 concludedsuccess. 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']listssys_flow_credential(ruling A, the fix(spec): register sys_environment_credential in CLOUD_PROVIDED_OBJECT_NAMES #18851 precedent). Right.@objectstack/metadata-protocol: exported typeMetadataCredentialChannel; public methodregisterCredentialChannel(type, channel)on the protocol class beside the authoring gate;SysMetadataRepository.restoreVersiongains the optionalderiveRestoredBody, shaped like the promote verb's option (R2). Right.@objectstack/service-automation: the platform objectSysFlowCredential(onesecret-typed value field,accessdefaultprivate,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;migrateFlowCredentialsIntoChannelwith its id and result type;FlowCredentialSourceandFlowCredentialHolding; onAutomationEnginethe four methodssetFlowCredentialSource,holdsFlowCredential,resolveFlowCredential,flowCredentialHoldings;FlowTriggerBindinggains the optionalresolveSecret. Right: one seam ([security] The webhook signing secret is stored in cleartext insys_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 answers503 SERVICE_UNAVAILABLEbefore verification and enqueues nothing; an unknown hook still answers404before any read. Right.@objectstack/runtime:flow-clone.tsexports the C1 refusal (409 RESOURCE_CONFLICT), its holding types and the literal-holdings fallback through the registeredflowredactor; the clone handler consults it before the same-name refusal. Right.Accept set:
config.secreton 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.config.secreton 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.503before 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.config.secreton 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.source: migrate-stored, at their own state and package); one ROTATE notice per flow atwarn, naming the flow and the class; a row the door refuses for another reason logs aterrorwith 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 thesys_migrationreceiptflow-credential-channel(verified_atnull,blocking0, names only). History and audit rows untouched; packaged flows untouched (Q3 A). Right.holdsFlowCredentialsays 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.AutomationEngine.hookSecretResolverasks the channel on every post whenever a literal is present (resolve(...) ?? literal), without theholdsgate the http node applies; andFlowCredentialChannel.resolveTHROWS when it has no data engine (and lets a failed row read propagate), although theFlowCredentialSource.resolvecontract this PR writes saysundefinedmeans the channel holds nothing there and only a HELD credential that does not come back throws. On a composition where the automation plugin resolves noobjectqlordataservice, 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 answered503on 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 answersundefined, so the real channel's no-engine branch is unpinned. Fix, one of: inhookSecretResolver, when the definition carries a usable literal andsource.holds(flowName, startNodeId, key)is false, return the literal without asking (the http node's shape; the index refreshes on every local write and onmetadata:reloaded, so an overlay row still wins at the next read); or haveFlowCredentialChannel.resolveanswerundefinedwhen it has no engine, per its own contract. Pin: the realFlowCredentialChannelwith aresolveEnginethat answersundefined, a literalapiflow registered on an engine using it as its source, andbinding.resolveSecret()resolving to the literal (trigger-api202).② Semver level
.changeset/20790-flow-credential-channel.md:minorfor@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/dogfoodis 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;yestakes at leastminor, 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 ruling5942356310read as non-breaking with both costs stated, and the changeset states them. Right.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, concludedsuccess. Right.success. Right.③ Boundary flags
Dev flags (reports
5948978079and5949953593, PR Acceptance notes), each answered:packages/runtime/src/domains/automation.tsoutside revision5942559287's list — inside claim5935167060, where the clone handler must consult C1; covered by the as-built claim revision5949091357; answered.successon the head; answered.503— the no-provider rule at those doors; measured by the dev, the hunks lie outside this diff; answered.allowOrgOverride: falseon flows: same-named stored rows in two packages resolve to one armed flow, so one slot; answered.blocking0,verified_atnull), and its failure logs aterrorwith consequence and fix; right level; answered.origin/mainahead at derivation —mainwas merged into the head; the queue rebuilds the merge; answered.80b4647b2— this head's measurements are in report5949953593and in the check-runs; cosmetic; the patch round may refresh it.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-seamReviewed-by:
cse_01DiCSbmJrkzNhuEAier4VoJVERDICT: 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.resolveand 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
objectstack-fleet commentedon Oct 2, 2026
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
objectstack-fleet commentedon Oct 2, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
417ba1fa6fc6399a6bc077459c45d239542245e1Local-runs: none
Round 2 on PR #21377 (
claude/issue-20790-flow-hook-secret-seam, draft, 36 files, +3557 / −89) for card #20790, under rulings5909514730(Q1 B · Q2 A · Q3 A · Q4 A),5910098951(17.x) and5942356310(A / R2 / C1). Inputs: the card body and every comment (the round-1 record5950334951and the patch report5951747088read as inputs, not as verdicts for this head), the PR body, its file list and the net diff againstmainat 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, 4skipped, none red. Every required context concludedsuccess— 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 concludedsuccess. 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.hookSecretResolverreads 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.resolvekeeps 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. TheFlowCredentialSourcecontract 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'sresolveSecret()answering the literal (service-automation) and a correctly signed post answering 202 through the realApiTrigger(dogfood test 9); the control, a held position whose store becomes unreachable, rejecting withFlowCredentialUnresolvableErrorand 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']listssys_flow_credential(ruling A, the fix(spec): register sys_environment_credential in CLOUD_PROVIDED_OBJECT_NAMES #18851 precedent). Right.@objectstack/metadata-protocol: exported typeMetadataCredentialChannel(store,heldPaths,strip); public methodregisterCredentialChannel(type, channel)on the protocol class beside the authoring gate, singular or plural type name, a second registration replacing the first;SysMetadataRepository.restoreVersiongains the optionalderiveRestoredBody, 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 objectSysFlowCredential(one secret-typed value field,accessdefault 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 pairSERVICE_UNAVAILABLE/ 503, the object and value-field constants and two types;migrateFlowCredentialsIntoChannelwith its receipt id and result type;FlowCredentialSourceandFlowCredentialHolding; onAutomationEnginethe four methodssetFlowCredentialSource,holdsFlowCredential,resolveFlowCredential,flowCredentialHoldings;FlowTriggerBindinggains the optionalresolveSecret; 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 insys_webhook.definition_json#7799,resolveSecretField), no second mechanism, no per-door redaction added.@objectstack/trigger-api:FlowTriggerBinding.resolveSecret(optional);startarms 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 answers503 SERVICE_UNAVAILABLEbefore verification and enqueues nothing; an unknown hook still answers 404 before any read, so there is no oracle. Right.@objectstack/runtime:flow-clone.tsexports the C1 refusal (409 RESOURCE_CONFLICT), its holding types and the literal-holdings fallback through the registeredflowredactor (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:
config.secreton 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.sys_flow_credentialrides the run-object manifest registration and is registered alone undersuspendedRunStore: 'memory', so every composition has the table; the channel joins the protocol atstart()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.config.secreton 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.source: migrate-stored, at their own state, package and organization); one ROTATE notice per flow atwarn, naming the flow and the class; a row the door refuses for another reason logs aterrorwith 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 thesys_migrationreceiptflow-credential-channel(verified_atnull,blocking0, the failure count as advisory, names only), warns that the log is the only record when the ledger object is not registered, and logs aterrorwhen 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.holdsFlowCredentialreports 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.start,kernel:readyandmetadata: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:minorfor@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/dogfoodis 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;yestakes at leastminor, 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 ruling5942356310read as non-breaking with both costs stated, and the changeset states them; the patch round changed no signature. Right.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, concludedsuccesson the head. Right.success. Right.③ Boundary flags
Dev flags (reports
5948978079,5949953593and5951747088; the PR's Acceptance notes and its Contract review round 1 section), each answered:packages/runtime/src/domains/automation.tsoutside revision5942559287's list — inside claim5935167060, where the clone handler must consult C1; covered by the as-built claim revision5949091357; answered.successon the head; answered.allowOrgOverride: falseon 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.blocking0,verified_atnull), its failure logged aterrorwith consequence and fix; right level; answered.FlowCredentialSourcecontract 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/mainmerged at this head; the queue rebuilds the merge; 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-seamReviewed-by:
cse_01DiCSbmJrkzNhuEAier4VoJVERDICT: 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.
objectstack-fleet commentedon Oct 2, 2026
Landing: PR #21377 at
417ba1fa6· 2026-10-02T12:24ZSeat
domain:services#2· sessionsession_01DiCSbmJrkzNhuEAier4VoJ· under ruling5942356310(A / R2 / C1). ⛔ Classes and positions only.5952247884(CONTRACT_REVIEW_TIER,Head-sha: 417ba1fa6…, the head unchanged).5950334951, Q3 A at the inbound door) is resolved. The inbound literal path asks the channel only for a positionholdsreports, 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 realApiTrigger.clean;needs:contract-reviewcomes off, thenpr_ready+automerge_enablethrough the queue.deletePackagein the sameprotocol.ts) is no longer serial-blocked. Its claim follows. Rotation, per Q1 B, is the operator's action named in the changeset.ci: the core filter starts Build Core for a build-input-only diff, he…
feat(automation): a flow's credentials live in a write-only channel o…
fix(metadata-protocol,metadata): every runtime write door refuses a b…