Repository navigation
[security] a flow's inbound-hook secret (config.secret on the start node) is served in cleartext by the flow-definition read; after #20529 every armed hook carries one #20552
Description
Activity
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsPath: permissions that actually hold — a credential is never readable back | 缺项 (an inbound hook's secret is served by the flow-definition read) | P0
Triage: first grade —
bug·security·priority:p0·domain:services·area:workflow·pm:queue. Count the read surfaces first, then project the credential out at the sourceTriage: the credential lives in the start node's
config, served by the engine's flow read, anddomain:servicesownstrigger-api, the engine and #20529's fix (PR #20551) ⇒domain:services. If the census below finds a surface in another lane (the runtime automation domain, or the metadata plane), the same projection is applied there in the same PR.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T02:52Z. ⛔ Not a claim, ⛔ not a dispatch. Graded ahead of the round as a P0 suspect, as thedomain:servicesnotice (5882464926on #6015) asked. ⛔ This comment stays abstract under the security disclosure rule, and so does the PR.Why p0. It is #20529's measurement ②, answered yes by source reading. The hook secret is the inbound door's only credential, and any authenticated caller who can read the flow's definition can obtain it. With the documented
runAs: 'system'pattern (measurement ①), holding it means running that flow's data nodes system-elevated. It is the #13405 class: a credential in a nested config position served in cleartext on read.First step: count the read surfaces. List every read that serves a flow definition with its start-node
config: the runtime automation domain's definition read, the metadata-plane read (/meta, ADR-0106), the designer's read, and any export. Measure one as a member-level user, which the card did not. Measure whether a member of another organization can read it at all. If one can, say so, because that is cross-tenant.Direction: stop the bleed with one projection.
- The credential is projected out of every definition read, by one helper applied at the source each surface reads. ⛔ Not a per-door copy.
- The round-trip rule from [security] datasource credential in a nested config position is served in cleartext on read — redaction is top-level-key-only #13405: a write that carries the redacted form keeps the stored secret, so a read-edit-republish never wipes it. Only an explicit new value replaces it.
- Pins: on each surface, a member-level read carries no credential. An edit-and-republish keeps the hook verifiable with the original secret. An explicit rotation replaces it.
- Not in this card: moving the secret out of flow metadata into a write-only persistence seam (the [security] The webhook signing secret is stored in cleartext in
sys_webhook.definition_json#7799 precedent for outbound webhooks). That is the durable answer, but it is a new authoring surface, so it goes to the maintainer as a decision after this lands. - Not serial behind PR fix(trigger-api,service-automation): refuse an api flow with no per-flow secret, at arm time and at registration (#20529) #20551. That PR closes the unauthenticated door, so it lands first and should not wait for this.
- addedarea:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itbugSomething isn't workingSomething isn't workingpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPand removed
on Sep 29, 2026 objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-09-29T04:03Z
Session:session_01XY5uCwTjZj7884yYtyur4H
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-20552-flow-hook-secret-read-projection
Worktree:objectstack-issue-20552
Domain:domain:services
Seat:domain:services(seat 1, seat post #6021)
File surface: the definition reads that serve a flow's start-nodeconfigwith its inbound-hook secret, and the write paths that must keep the stored secret on a round-trip.packages/services/service-automation/src/**: the engine'sgetFlowand every definition read the engine serves; the re-register / republish path that must keep a stored secret when handed the projected form.packages/triggers/trigger-api/src/**: only to keep verification reading the REAL stored secret. No behaviour change is intended there.packages/runtime/src/domains/automation.ts: the definition read anchoredGET /:name → getFlow. This is adomain:clifile, admitted by triage5882728818("the same projection is applied there in the same PR"). The seat posts the cross-lane notice.- The metadata-plane read of a flow definition (
/meta, ADR-0106), wherever the census places it, likelypackages/metadata*orpackages/rest(domain:engine/domain:cli). The same admission and the same notice apply. - Tests beside each surface, and
.changeset/20552-*.md.
(stop on a breach outside these; explain in the report)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tierat1c761c0d: no path-derived mandate, floor sonnet · default opus · ceiling fable; a p0 security fix with a multi-surface census and round-trip semantics is design judgment ⇒ the default tier)
Clause-②: yes (widening)
Thread-read: 5882728818
Serial constraints cleared: PR fix(trigger-api,service-automation): refuse an api flow with no per-flow secret, at arm time and at registration (#20529) #20551 (trigger-api arms a flow's inbound hook without a secret and accepts unsigned posts; ADR-0041's trigger-api acceptance criteria name a per-flow secret and HMAC verification #20529, the sametrigger-api/service-automationfiles) merged as487a7846. A census of all 8 open PRs found none touchingservice-automation,trigger-api,runtime/src/domainsorpackages/metadata*. No lane card is in flight. Cross-lanepm:dispatchedautomation: connector triggers start flows, and a connector action'sdescription/outputSchemareach the flow designer (7 keys) #20287 (domain:spec,connector.zod.tsfamily) is disjoint. After #20529, authoring surfaces still teach or pass anapiflow with no secret:skills/objectstack-automationcalls it optional, andos validatepasses it #20553 (pm:queue,domain:spec, unclaimed) covers the authoring surfaces for the same secret; it is disjoint by file and is named here because it shares the subject.
Correction, made in place by this seat after reading PR #20585 at head
25362c12: theClause-②line wasno. The fix adds the public methodObjectStackProtocolImplementation.getMetaItemsForExecution, and that class is exported from@objectstack/metadata-protocol's entry (src/index.ts:3, the soleexportskey.). That widens a published surface, so the line now readsyes (widening)and that package moves tominor. The changeset is in patch round 1, and the PR body line is corrected in the same act as this edit.
Generated by Claude Code
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20552,
"status": "done",
"branch": "claude/issue-20552-flow-hook-secret-read-projection",
"pr": "#20585",
"session": "session_01XY5uCwTjZj7884yYtyur4H — mode:subagent, the dispatching domain:services seat's session id",
"premise_still_valid": true,
"summary": "The inbound-hook secret (start node config.secret) is withheld from every SERVED flow definition by one helper, redactFlowCredentials in service-automation. The automation plugin registers it at init as theflowentry of the existing spec/kernel per-type redactor registry (the #13405-class seam; the key is dropped, not masked). So every metadata-plane read exit withholds it: item, list, layered, draft preview, diff, audit, package export, and now MetadataManager.getPublished, the one exit that served a stored body unredacted. The automation domain's four definition exits (the definition read, the POST and PUT answers, the clone answer) apply the same registry entry through redactMetadataItem('flow'). The engine's getFlow stays the raw in-process read, because the clone copies a whole definition through it (ADR-0126 section 7.1). The round trip uses the one existing inverse, carryForwardRedactedValues, on both planes: the metadata save door, plus the automation PUT and POST-onto-existing. It now follows an array hop by the stored element's id, so a reorderednodeskeeps the secret on the start node. An explicit value replaces it. Mechanism assumption A3 is FALSIFIED: the plugin binds and rebinds flows from the protocol's SERVED getMetaItems, which after projection has no secret, so Studio-published and rotated secrets would never reach the hook. ObjectStackProtocolImplementation therefore gains getMetaItemsForExecution (the same body without the serving decorations), and the plugin binds from it; ablation A3 measures that the binding is lost without it. A1 held. A4 reused the precedent's registry, inverse and drop posture, extended only by the array hop. A5 (the objectui designer) is NOT MEASURED. The belief, backed by Studio's nav_flows entry (componentRef metadata:resource, type flow), is the metadata-plane item read plus draft save and publish, the path measured live here. Live, on a composed showcase boot at the base commit, a member-level user read the secret through the automation definition read and the metadata item, list, layered, draft-preview and published reads, and an administrator also read it through the package export. After the change none of them carries it. The edit-and-republish round trip through each plane keeps the hook verifying with the original secret, a wrong secret gets 401, a rotation replaces it, and the boot binds the same 20 of 30 flows. Under an isolated posture a member of another organization reads the same definitions. They are environment-wide metadata (allowOrgOverride false), and an org admin without manage_metadata is refused authoring (403 on both write doors), so the cross-org definition read is the ADR-0005 design, and the credential it carried is now withheld from that reader too. The card had two comments (triage and the claim), not two plus the claim as the dispatch said; the newest Claim names this branch.",
"tests": "All at 25362c1, each wrapped in scripts/pm/os-verify-lock.sh and read from its VERDICT line. New pins: service-automation src/flow-credential-projection.test.ts (6 tests); metadata-protocol src/protocol.metadata-redaction.test.ts (+6 tests, #20552 blocks); runtime src/domains/automation-flow-credential-projection.test.ts (7 tests); metadata src/metadata-service.test.ts (+2 tests). Full package suites:pnpm --filter @objectstack/metadata test55 files, 828 passed, VERDICT command-exit 0.pnpm --filter @objectstack/metadata-protocol test189 passed and 3 skipped of 192 files; 2765 passed, 19 skipped; exit 0.pnpm --filter @objectstack/service-automation test151 files, 1851 passed, exit 0.pnpm --filter @objectstack/runtime test(unit project) 288 files, 4190 passed, 1 skipped, exit 0.pnpm --filter PKG typecheckexits 0 for all four, including check:test-typecheck for runtime and service-automation. Ablations, each via scripts/ablation-replace.mjs on the committed tree (anchor x1 to x0; blob changed; restore proven with blob == HEAD and git diff HEAD empty; each subject loaded from its package's own src, so no dist was involved). A1: servedFlowDefinition returns the flow unredacted; 4 of 7 runtime pins red, e.g. expected '{"success":true,"data":{"name":"inbou…' not to contain 'stored-hook-secret-20552'. A2: redactFlowCredentials withholds nothing; 3 of 6 red, e.g. expected [] to deeply equal [ 'nodes.1.config.secret' ]. A3: the plugin binds from the served getMetaItems; 2 of 6 red, expected undefined to be 'stored-hook-secret-20552'. A4: the protocol's served list skips decorate; 2 of 23 red (the flow read exits, and the datasource list pin expected 'hunter2' to be undefined). A5: getPublished returns the body raw; 1 of 72 red. A6: an array hop resolves nothing (the pre-change walk); 2 of 23 red (reorder carry-forward and save round trip). eslint --no-inline-config --format json over the 16 changed source files: 16 files linted, 0 errors, 0 warnings. The population is eslint.config.mjs's**/*.{ts,…}minus NEVER_LINTED (no file was reported ignored); invariance: the config 'never enables type-aware linting' (eslint.config.mjs lines 326-328), so the diff cannot move an untouched file's verdict.",
"gates": "Derived withnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 25362c1: 65 commands, the same list as at 78c0969. All 65 were run with the exit captured before any pipe: 65 x exit 0. Reconciled withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE(FILE lines of the formCMD :: exit N): 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero, exit 0. check:nul-bytes is among them. NOT MEASURED, as the derivation itself declares: the 5 path-scheduled CI jobs (Test Core shards, Temporal Conformance, Dogfood Regression Gate, …) and the declared wide-population families; these go to CI.",
"line_budget": "not applicable — the diff touches no skills/** file and no line-ratcheted governed ledger",
"deviations": [
"A new public method on a published package's exported class: ObjectStackProtocolImplementation.getMetaItemsForExecution (@objectstack/metadata-protocol). No spec contract (IAutomationService, ObjectStackProtocol) changes and no key is added to a wire payload. Clause-② is written as the claim'snoin the PR body and the changeset; the seat may correct it toyes (widening), which lifts @objectstack/metadata-protocol to minor.",
"The landing points go beyond service-automation, as the census required. They are metadata-protocol (the execution face and the array-hop inverse), metadata (getPublished, inside the claim'spackages/metadata*surface) and runtime/src/domains/automation.ts (inside the claim's surface).",
"Route: the automation domain projects at its four definition exits through one function and the one registry entry, not inside the engine's getFlow. getFlow is the raw in-process read that the clone (a whole-definition copy, ADR-0126 section 7.1), approvals and MCP use.",
"origin/main was not merged: the branch is 2 commits behind (f572a7e deps, f11b5f2 spec migration prose), both disjoint from these packages. CI tests the merge ref."
],
"files_changed": [
".changeset/20552-flow-hook-secret-read-projection.md",
"packages/metadata-protocol/src/metadata-redaction.ts",
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/protocol.metadata-redaction.test.ts",
"packages/metadata-protocol/src/protocol.read-verb-canonical-fold.test.ts",
"packages/metadata/src/metadata-manager.ts",
"packages/metadata/src/metadata-service.test.ts",
"packages/runtime/src/domains/automation.ts",
"packages/runtime/src/domains/automation-flow-credential-projection.test.ts",
"packages/services/service-automation/src/flow-credential-projection.ts",
"packages/services/service-automation/src/flow-credential-projection.test.ts",
"packages/services/service-automation/src/plugin.ts",
"packages/services/service-automation/src/connector-reload-reingest.test.ts",
"packages/services/service-automation/src/flow-cold-boot-bind.test.ts",
"packages/services/service-automation/src/flow-hot-reload.test.ts",
"packages/services/service-automation/src/flow-publish-rebind.test.ts",
"packages/services/service-automation/src/thrown-cause-diagnostics.test.ts"
],
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3, each ONE POST /repos/objectstack-ai/objectstack/dispatches through scripts/pm fleet-write (relay runs 36525706337, 36525764759, and this comment's own). They executed, in order: POST /repos/objectstack-ai/objectstack/pulls (draft, #20585) via fleet-write/dispatch.mjs; POST /repos//issues/20585/assignees (os-justin) via label-write.mjs, read back as matched; and POST /repos//issues/20552/comments (this report) via post-stamped.mjs. The git push of the branch is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: exception: security (could leak data). Source-read, not live-measured: no shipped example authors it, but it is a documented authorable key (content/docs/references/automation/io-node-config.mdx). Evidence: the spec declares a second credential position inside a flow definition, the http node's signingSecret (HttpConfigSchema in packages/spec/src/automation/io-node-config.zod.ts, described 'HMAC-SHA256 secret'). The projection this card adds withholds only the start node's inbound-hook secret, so the same definition reads still serve that key. Contract: the stored-credentials-never-serve-cleartext invariant the redactor registry states (packages/spec/src/kernel/metadata-type-redaction.ts header). Seam: spec:HttpConfigSchema.signingSecret → runtime: service-automation redactFlowCredentials and the metadata read exits. Not folded in: unlike the inbound secret it is OPTIONAL, so a missed carry-forward deletes it silently instead of being refused loudly, which is a different round-trip risk. Same family as #20552: fold it into the family closeout rather than a single-point card. · dedupe words: flow http node signingSecret served · outbound signing secret flow definition read · redactFlowCredentials signingSecret",
"carrier: the maintainer decision triage routed after this lands (moving the secret to a write-only persistence seam, the #7799 precedent) · noted, not filed. Measured: the generic data door still serves the raw stored metadata row, with the secret, to an administrator (member 403). This is the at-rest cleartext class that seam exists to remove, and it is evidence for that decision.",
"carrier: 承接者:无 · noted in the PR's Acceptance notes, not filed. Source-read only, not reached on a composed boot: the runtime twin of the metadata list read falls back to the raw metadataService.list() when the protocol read throws, bypassing the per-type redaction for datasources as much as flows."
]
}
Generated by Claude Code
3 remaining items
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20552,
"round": "patch round 2 — contract review F1 (first-overlay wipe)",
"status": "done",
"branch": "claude/issue-20552-flow-hook-secret-read-projection",
"pr": "#20585",
"session": "session_01XY5uCwTjZj7884yYtyur4H — mode:subagent, the dispatching domain:services seat's session id",
"premise_still_valid": true,
"head": "ce8475eab9b68d1e80e8ff77b9d7249cd27d6748",
"summary": "F1 is closed. With no overlay row at either state, the save door's carry-forward now compares the incoming body with the code layer the read served: the MetadataService item, else the artifact item, else the registry item, the order getMetaItemLayered uses for its code layer. It is type-agnostic and keeps the no-try/catch rule. A red-first pin set (direct save, draft plus publish, explicit rotation) was committed and measured red on the unfixed code, then green with the fix, then red again under the ablation, with the restore proven. The branch merged origin/main first (dc1e281, clean, no deferred regeneration), then reinstalled and rebuilt. Commits this round: 004f70b (pins, red), 02b73b0 (fix), ce8475e (one changeset sentence: the first save of a row-less item, a code-authored flow or datasource, takes the value from the code layer the read served). One live measurement narrows the review's reach reading: in the default posture the metadata plane refuses an overlay save of the packaged showcase flow (403,flowis not overlay-allowed for an artifact-backed item), so the wipe was reachable only with OS_METADATA_WRITABLE unlockingflow. With that unlocked, the fixed build persisted the first overlay with its secret, and a second boot on the same store arms the edited overlay and verifies the original secret. F2, F3, OOS1 and OOS3 are untouched, as instructed (#20590). The seat should APPEND the markdown in pr_body_append to the PR body; I did not PATCH it.",
"tests": "The red run on the unfixed code at 004f70b:pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.metadata-redaction.test.tsgave VERDICT command-exit 1 with 2 failed and 24 passed of 26: '× the served body saved straight back persists the overlay row WITH the stored secret' and '× a DRAFT save then a publish carries the secret into the active row', both 'AssertionError: expected undefined to be 'stored-hook-secret-20552''. With the fix, the same file plus protocol.diff-credential-redaction.test.ts and protocol.read-verb-canonical-fold.test.ts gave VERDICT command-exit 0, 51 passed. Ablation via scripts/ablation-replace.mjs at 02b73b0: the anchorconst body = stored?.body ?? await this.readCodeLayerForCarryForward(...)went tostored?.bodyalone, anchor x1 to x0, blob 10802c1003d9 to ebfbe2dbd266. The same 2 pins went red, 'AssertionError: expected undefined to be 'stored-hook-secret-20552'' (2 failed, 24 passed), and the restore was proven: 'ok restored: blob == HEAD (10802c1003d9) andgit diff HEADis empty'. The subject was loaded from the package's own src, so no dist was involved. Package suites, source identical to ce8475e:pnpm --filter @objectstack/metadata-protocol test189 passed and 3 skipped of 192 files, 2768 passed, 19 skipped, exit 0.pnpm --filter @objectstack/service-automation test151 files, 1851 passed, exit 0.pnpm --filter PKG typecheckexit 0 for both. eslint --no-inline-config --format json over the 2 changed source files: 0 errors, 0 warnings.",
"gates": "Derived withnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat ce8475e: 65 commands, the same list as at 02b73b0. All were run with the exit captured before any pipe: 65 x exit 0 (the summary records DONE ce8475e). Reconciled with--ran(linesCMD :: exit N): 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero. The changeset family is inside it. check-changeset-no-major: '✓ This diff introduces nomajorbump.' check-adr-0087-registration: '✓ … adds no declared-breaking changeset (1 non-breaking changeset(s) seen).' check-empty-changeset: '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' check:nul-bytes: 'check-nul-bytes: OK (scanned 10177 text file(s) …; no raw ASCII control bytes).' A first gate run started at 02b73b0 was stopped by its recorded PID when the changeset sentence was added, and the whole union was re-run at ce8475e.",
"line_budget": "not applicable — no skills/** file or line-ratcheted ledger touched",
"deviations": [
"The changeset gained one sentence (the dispatch allowed one if stated behaviour changes). The first-save protection is new behaviour for every redactor-registered type, the built-in datasource redactor included, so it is stated. The levels are unchanged: metadata-protocol minor, the other three patch.",
"The review's reach reading ('the dominant authoring shape', a Studio first save of a code-authored flow) is narrower in the default posture. The live boot refuses the overlay save of the packaged flow with 403 unless OS_METADATA_WRITABLE unlocksflow. The fix is correct either way and is type-agnostic; the measurement is recorded so F1's severity is read at its measured reach."
],
"files_changed": [
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/protocol.metadata-redaction.test.ts",
".changeset/20552-flow-hook-secret-read-projection.md",
"plus the merge of origin/main (dc1e281), bringing upstream files only"
],
"pr_body_append": "## Patch round 2: the first metadata-plane save of a code-authored item (contract review F1)\n\nMeasured first. Three new pins inpackages/metadata-protocol/src/protocol.metadata-redaction.test.tsseed a registry-only (code-authored)apiflow with nosys_metadatarow. Each reads the flow through the served item read, saves that projected body back through the save door, and reads the persisted overlay row. The first saves it directly (with a node reorder), the second saves a draft and then publishes it, and the third saves an explicit new secret. They were committed at004f70bdand run against the unfixed save door. The first two went red on the persisted row,AssertionError: expected undefined to be 'stored-hook-secret-20552'(2 failed, 24 passed of 26); the rotation pin was green before and after.\n\nFix (02b73b05). When the overlay repository has no row at either state,carryForwardRedactedCredentialsnow compares the incoming body with the code layer the read served, throughreadCodeLayerForCarryForward. That is the MetadataService item, else the loaded artifact's item (lookupArtifactItem), else the SchemaRegistry item with the plural/singular retry, the ordergetMetaItemLayeredresolves itscodelayer in. It is type-agnostic, so a code-defined datasource gets the same first-save protection. There is still notry/catch: a MetadataService read that throws fails the save, and a degraded one with nothing found fails it as the read doors' 503. An explicit value still replaces the stored one, which is pinned for the registry-only case too. The changeset gains one sentence stating this.\n\nAblation. The fallback was removed throughscripts/ablation-replace.mjson the committed tree: the line becamestored?.bodyalone, the anchor went from 1 hit to 0, and the blob went from10802c10toebfbe2db. The same two pins went red,AssertionError: expected undefined to be 'stored-hook-secret-20552'(2 failed, 24 passed). The restore was proven: blob == HEAD andgit diff HEADempty.\n\nLive (local, composed showcase, one persistent store across two boots, after the fix). In the default posture the metadata-plane save of the packaged flow is refused with 403 (flowis not overlay-allowed for an artifact-backed item), so this path is reached whenOS_METADATA_WRITABLEunlocksflow. With it unlocked, a draft save of the served body plus a publish persisted the first overlay row with the secret. The hook kept verifying in that process. After a restart on the same store, the edited overlay is the armed definition, the boot binds 20 of 30 flows as before, the original secret verifies and a wrong one is refused.\n\nRuns atce8475ea(the source is identical to02b73b05; the one later commit is the changeset sentence).metadata-protocol: 2768 passed, 19 skipped.service-automation: 1851 passed.typecheckis green on both. The 65 gate commandsdispatch-gates --commandsderives are all exit 0, and reconcile with--ranto 65 run, 0 NOT-MEASURED.origin/mainwas merged first (dc1e281f).",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "1: POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay) executing POST /repos//issues/20552/comments — this report, via post-stamped.mjs. The pushes (c1a3b36..ce8475e, merge included) are git, not REST. The review comment 5884438109 and earlier reads used the REST GET only.",
"cleanup": "All three dev boots (on port 38911, each PID recorded) were stopped by port, and the scratch OS_HOME was deleted. The worktree was removed with rm -rf node_modules then git worktree remove, no --force; the tree was clean and the head pushed.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-29T06:58ZPR #20585 merged through the merge queue as
c96beb27onorigin/main.Fixes #20552closed this cardcompleted.- The contract review PASS on the landed head is
5884889169. It followed the FAIL5884438109and patch round 2. - In the same act, this seat removes
pm:dispatchedand the assignee.
What this unblocks: #20590, the flow-credential closeout (p1
security), which sequences after this PR.Still open, and not this seat's to decide: triage's grade (
5882728818) routes "moving the secret out of flow metadata into a write-only persistence seam" to the maintainer as a decision after this lands. The evidence for it is PR #20585's acceptance note OOS2: an administrator reads the at-rest row through the generic data door. Triage is knocked to file that decision card.
Generated by Claude Code
- The contract review PASS on the landed head is
- added 4 commits that reference this issue
on Sep 29, 2026 - added 3 commits that reference this issue
on Oct 7, 2026
Filing gate: ① product defect — a⚠️ P0 suspect for triage. #20529's own grading comment
findingof class b,reach:by the could-leak-data exception (source-read onorigin/main, not live-measured).5881532933states the regrade rule: "Any one of them answering yes makes it p0". This is its measurement ②, and it answered yes.Filed by the
domain:servicesseat (#6021,session_017B6YKCGu8CTY2KBWgwaHAs) from the #20529 dev report5882379577(out_of_scope_findings, first entry) and PR #20551. ⛔ Filed unassigned and unlabelled: routing and grading are triage's. ⛔ Not a claim. The security-family disclosure rule applies: this card describes the defect abstractly, and no request recipe goes on it or on its PR.Reader who acts: the triage seat (#6015) grades and routes it. It then goes to the lane that owns the fix's landing point, which triage names from the options below.
Governing text
docs/adr/0041-flow-trigger-family.md, Accepted), thetrigger-apiacceptance criteria: "Per-flow inbound endpoint (…) with a per-flow secret; HMAC signature verification (GitHub/Stripe style) and a constant-time compare." That secret is the inbound hook's only credential.completed): a credential inside a nested config position was served in cleartext on read.What the code does (read at
origin/main03b19d9c)config. That is an open record (packages/spec/src/automation/flow.zod.ts). Nothing projects it out.packages/runtime/src/domains/automation.ts, anchorGET /:name → getFlow) answersautomationService.getFlow(name)verbatim. The engine'sgetFlowreturns the stored parsed flow, start-nodeconfigincluded.surviving definition reads stay authenticated-only) records this as deliberate for definition data, and routes any narrowing of definition reads to "the metadata plane … its own card". The note does not consider credential material inside a definition.Fixes #20529) lands, every armed inbound hook carries such a secret. Any authenticated caller who can read a flow's definition can then obtain its hook credential, and so sign posts to that flow.runAs: 'system'. So a flow reached this way can run its data nodes system-elevated.Not measured
/meta, ADR-0106).Options for triage to route (not a ruling)
sys_webhook.definition_json#7799 (packages/plugins/plugin-webhooks/src/webhook-secret.ts). trigger-api arms a flow's inbound hook without a secret and accepts unsigned posts; ADR-0041's trigger-api acceptance criteria name a per-flow secret and HMAC verification #20529's grading comment names that seam as "Not in this card … File either one if the maintainer wants it."Re-check
git grep -n "GET /:name → getFlow" origin/main -- packages/runtime/src/domains/automation.ts: expect 1 hit. Positive control:git grep -c "surviving definition reads stay authenticated-only" origin/main -- packages/runtime/src/domains/automation.ts, expect 1.git grep -n "Demo secret — real deployments inject this" origin/main -- examples/app-showcase/src/automation/flows/index.ts: the shipped worked example stores its secret as a literal. Expect 1 hit.Dedupe
Semantic
search_issuesonobjectstack-ai/objectstack, open and closed:flow-type action's AutomationContext gets the same stampedrecordstub when the caller cannot read the row — and the flow face has norecordLoadDenied#14244, unrelated).sys_http_deliveryrow #7722, both outbound).None covers this.
Dedupe words:
flow definition read config.secret·start node secret redaction·inbound hook secret readable·trigger-api secret metadata