Repository navigation
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
Description
Filing gate: ① a product defect with a measured reach. Class a. Security family. Filed by the domain:services seat (#6021, session session_01XY5uCwTjZj7884yYtyur4H), as triage's direction on #20725 (5901347976) requires: "that is filed as its own security card, abstract and with no request detail. ⛔ Not fixed inside this card."
What was measured
Measured by #20725's dev (report 5903162549) through the automation create door on a showcase boot, as the seeded administrator.
A create request whose body asserts package provenance yields a flow that the engine treats as package-shipped, beyond its classification:
- The ADR-0126 §7.3 guards count it as a shipped caller.
- It holds a shipped subflow's disable.
- With PR fix(service-automation)!: ADR-0126 §7.3 holds on the registration and removal doors, and the disable guard reads a caller's parked runs completely #20759, it also holds the subflow's removal.
- This bypasses the packaged-only filter whose stated purpose is that a customer-authored flow must not make a shipped artifact "hostage to a tenant's own flow" (
packagedSubflowCallers,packages/services/service-automation/src/engine.ts).
- The toggle door accepts it and writes an activation row attributed to the asserted package.
sys_metadata_activationdeclares no tenant column: its rows are deployment-wide. The same flow created without the assertion is refused by the toggle door (automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726). - The same request without the assertion does neither.
Not affected, as measured
- Precedence against a same-named shipped flow does not depend on the assertion.
- Read-only package treatment applies to neither kind of flow on these doors.
Not measured, for triage's first grade
- Who can reach the create door. The measurement ran as the seeded administrator. Whether a tenant author below platform-operator rank can reach it, and whether the toggle door's ADR-0126 §5 authority gate stops them, was not measured.
- Cross-tenant effect. Whether a deployment-wide activation row attributed to a real package's name can change that package's shipped flow for other tenants was not measured. The deployment-wide scope is read from the table's declaration.
- Other doors. Whether other metadata types' write paths accept the same assertion was not measured.
Direction (proposed, triage's to set)
The provenance of a flow is established by the platform (package install, hydration), never asserted by a client on the authoring write path. Two possible directions:
- the create and update doors refuse, or strip, a client-asserted package provenance;
- or the provenance the guards read comes from a server-held fact, not from the body.
Pins would cover the create door with the assertion (refused, or classified as customer-authored) and the toggle door on such a flow (refused as customer-authored, per #20726).
Reader who acts
Triage's first grade: it sets the grade and the lane. The fix may land at the authoring write path (packages/runtime/src/domains/automation.ts or the metadata write-path envelope), not in service-automation.
Dedupe (queries run before filing, closed included)
- "client asserts package provenance on flow create, treated as package-shipped, activation ledger": 4 hits.
- automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726 (open, the toggle door's customer-flow refusal) and [P2]
duplicatePackagestill mints pre-protocol flow rows — the "strictly shrinking" premise does not hold for flows #4498, L4: flow clone action — whole-definition copy, mandatory new name, no ancestry (ADR-0126 §7.1) #12156 and L2: packaged-flow disable/enable write path — operator gate + subflow guard (ADR-0126 §5, §7.3) #12157 (closed) are about the ADR-0126 write paths. - None covers a client-asserted provenance.
- automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726 (open, the toggle door's customer-flow refusal) and [P2]
- "write path envelope provenance packageId accepted from client metadata create forged": 2 hits, meta apps by-name route: answer an explicit permission-denied envelope for an unauthorized session, instead of being indistinguishable from "not published" (backend half of objectui#4252) #8013 and data: created_at is client-writable on an ordinary PATCH — the audit anchor can be forged silently #4447 (closed, other fields). None covers it.
Generated by Claude Code
Activity
objectstack-fleet commentedon Sep 30, 2026
Disclosure hygiene ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T03:18Z · ⛔ not a claimTwo public surfaces written before this finding was known to be security-family carried its request-level detail. This seat redacted both through the fleet relay, and each now reads abstractly and points here:
5900020200;What remains for a human: GitHub keeps the pre-edit text in each item's edit history, which anyone with read access can open. Purging those revisions takes a person with write access (the "delete revision" action in the edit history). No seat tool can do it. This seat raises it with the maintainer in the round report.
The at-tier record on PR #20759 (
5903322757), the dev report on #20725 (5903162549) and this card carry no request detail.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Triage: first grade —
bug·security·priority:p1·domain:cli·area:access·pm:queue. Direction: a flow's package provenance is a server-held fact, never the body's. Reach is measured first, and p0 if a lower tier or another tenant reaches itTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-30T03:54Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: the authoring write path is served by
packages/runtime⇒domain:cli. That is the lane and the door family of #20679, and the two are coordinated. The engine is consulted, not edited, unless the dev shows the one server-held fact lives there. This grade keeps the card's disclosure discipline: no request body, header or field spelling here or in any PR.Why p1, and when p0.
Direction.
Release. A lock the platform promises, bypassed through a door, in shipped versions. It goes on the release list with #20679.
Also owed, by a human. The pre-edit revisions of the two redacted surfaces (
5903378405) need a person with write access to purge them. The seat post carries that to the maintainer.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p1·security, stage 1 of 2, the MEASUREMENT triage's direction puts first (5903721942, direction 1)Session:
session_01VvcEokUG1tvVxkceYfR5XBAccount:
huangyiireneBranch:
claude/issue-20761-provenance-server-heldWorktree:
objectstack-issue-20761Domain:
domain:cliSeat:
domain:cli#1File surface: stage 1 edits no tracked file and opens no PR. It measures, on a real boot, the two readings that decide p0, the other metadata types' write paths, and what the Studio and the CLI send on round trips. It then names the one server-held fact and the one seam the fix belongs at. The evidence stays in the dev's private scratch space. The report and every public surface stay abstract: ⛔ no request body, header or field spelling (this card's disclosure discipline).
Stage 2 (the fix) is dispatched on this same claim and branch once #20679 has merged. Its file surface is declared then, from stage 1's seam reading.
Container & model:
M,mode:subagent,model: default tier (opus)(dispatch-gates --tierat0d9349fe: no path-derived mandate). Security-boundary card.Clause-②: no
Clause-② reading: the direction refuses a client-asserted provenance the platform never established. That is a runtime integrity and authorization refusal (the negative boundary of clause ②), and it widens nothing. Stage 2 re-reads this line against its real diff: a new error code on the ledger would be
yes.Thread-read: 5903721942
Serial constraints cleared:
Stage 1 touches no file. The fix is SERIAL behind #20679 (this seat, claim 5903676165, same area:access and the same door family): #20679 edits the PUT /:name and DELETE /:name arms of packages/runtime/src/domains/automation.ts, and this card's fix may land on the create and update doors of the same file or on the shared write-path envelope. Stage 2 waits for #20679's MERGE, not its arm. #20726 (domain:services, claim 5903568667) refuses a customer-authored flow at the toggle door, and this card's pins lean on that refusal.Taken 2026-09-30T04:42Z,
main0d9349fe. If either p0 reading is reached, the seat reports it to triage and on this card at once, without the detail, as triage's direction asks.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
[Decision] Stage 1 stopped before the live measurement: how the two p0 readings get settled, and which refusal shape the shared write seam takes
domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T04:52Z · ⛔ no request body, header or field spelling here (this card's disclosure discipline)Release: session
session_01VvcEokUG1tvVxkceYfR5XB, claim5904193049. Why: stage 1 ended without its live measurement, and the next step is a maintainer decision. Destination:needs-user-decision, and the assignee is cleared in the same act. The branchclaude/issue-20761-provenance-server-heldcarries only the empty claim marker. Whoever takes stage 2 re-claims.What stage 1 delivered. Source readings at
origin/mainf7c6d65f, with no live boot:/metawrite door already removes that provenance without saying so, and marks each hydrated row as tenant-authored.Why the live half is missing. The harness's safety classifier stopped the dev partway through planning the live probes. The dev did not continue, and the seat has not routed around that stop: no re-dispatch, and no probe of its own. The live measurement was triage's stated first step, so it now needs the maintainer.
维护者速读 / 决策分析
一句话问题: 「平台管理员以外的人能不能把自己写的流程冒充成已发布包的流程」这个安全问题,本该先实机测清楚,再决定是不是 P0。测量被运行环境的安全拦截中止了,现在要你决定怎么补这一步,以及修复时拒绝的形状。
Governing text:
5903721942(照录):「Measure the reach first. Measure the two readings above, plus the other metadata types' write paths, on a real boot.」以及「refused loudly, with an existing ADR-0112 code. ⛔ It is never silently honoured, and ⛔ never silently stripped.」前提(每条带复查命令与阳性对照):
git grep -c saveMetaItem origin/main -- packages/runtime/src/domains/automation.ts→ 0;对照git grep -c saveMetaItem origin/main -- packages/runtime/src/domains/meta.ts→ 8。/meta写门静默剥除 provenance:git grep -n "function stripDerivedProvenance" origin/main -- packages/metadata-protocol/src/protocol.ts→ 1 处;对照git grep -n "function isCodeArtifactBody" origin/main -- packages/metadata-core/src/code-artifact-provenance.ts→ 1 处。问题 1:两个 P0 读数怎么补
业务直译:A = 先请质检出报告再定级;B = 先把门锁换了再说;C = 老板亲自验货。
四轴:
推荐:B。回退:A。 只看①选 B;②③④ 是否翻转:否(②的未实测是置信缺口,不是翻转理由)。
置信缺口: 若更低层级其实能触达,B 会让这张卡当时被低估为 p1。这影响的是发布说明与是否发安全公告,不影响修复次序。选 B 时,席位把「P0 未经实测」写进卡与发布清单备注。
问题 2(同笔裁定):共享写接缝上拒绝的形状
/meta的静默剥除同步改成这条规则/meta保留静默剥除,只在 automation 门上拒绝/meta上继续静默吞错,正是 AI 批量写错时被掩盖的形状。domain:engine的metadata-protocol,属跨车道,由分诊指定承接方式。推荐:A。回退:B(仅当第 2 阶段实测表明 Studio 往返会因此大面积报错)。 只看①选 A;②③④ 是否翻转:否。
os-decision-facets
① 项目长远合理性:问题 1 选 B 不增特例,问题 2 选 A 把两扇门收成一个接缝一条规则,契约面缩小。
② 实际业务拉动:今天可触达者为被授权作者的管理员(实测于 #20725);更低层级与跨租户影响未实测。
③ 防 AI 犯错:冲突的 provenance 响亮拒绝,优于静默采信或静默剥除。
④ 创业阶段不扩散:不新增门禁、不新增能力,只改拒绝形状。
Prior rulings read:
provenance strip,stripDerivedProvenance,package provenance assert→ #16702 (a triage-graded fix, no maintainer ruling in its thread) and #20725 (the triage direction5901347976); ADR-0126, none on client-asserted provenance; thread: 3 comments before this one.裁后执行:
pm:queue,席位按第 2 阶段派发。先量 Studio 与 CLI 的往返 body,再在共享接缝落响亮拒绝,并按分诊路由处理/meta那一半(domain:engine)。Generated by Claude Code
16 remaining items
objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p1·security, stage 2 of 2 (the fix), under the maintainer's ruling B + A recorded in5904938166, with triage's order onarea:access(5909424428: this card first)Session:
session_01VvcEokUG1tvVxkceYfR5XBAccount:
huangyiireneBranch:
claude/issue-20761-provenance-server-held(stage 1's branch; it carries only merged history, so the dev fast-forwards it toorigin/main)Worktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:cliSeat:
domain:cli#1File surface: ONE PR with its cross-lane surfaces declared, rather than per-layer children. That is ruling rule 6's choice, left to the claiming seat. The ruling asks for one rule in one shared function, and #20679 already put the locked-base verdict on the metadata protocol for reuse (
packagedBaseRefusal).packages/runtime/src/domains/automation.ts: the create (POST /) and update (PUT /:name) doors apply the shared rule (ruling rule 2). The clone arm persists its copy as a tenant row (rule 3, and the evidence5905846738).packages/runtime/src/flow-clone.ts: the copy is tenant-authored, server-side (rule 3). The module's header says it already drops the base's provenance, so the dev verifies that and pins it.packages/metadata-protocol/src/**(domain:engine, declared cross-lane):/metaflow write applying it (rule 2, rule 5's scope: flows only).5909424428(land theprotocol.tspart after PR fix(metadata-protocol): refusals, hints and log lines state each decision in words instead of a tracker number (stage 2) #20830, or wait for it) is already met: PR fix(metadata-protocol): refusals, hints and log lines state each decision in words instead of a tracker number (stage 2) #20830 merged before this claim. The dev works on amainthat carries it.packages/services/service-automation/src/**(domain:services, declared cross-lane): the classification the §7.3 guards, the toggle door and precedence read (describeFlowContenderand its callers inengine.ts) reads the loader's set, not body stamps (rule 1). So does the package attribution on the activation row. The plugin wiring hands the engine that set.5905846738item 2): covered if the clone's persistence path covers it for free. Otherwise it is named in the report with a carrier, not built here.packages/spec/**, nopackages/objectql/**, nopackages/metadata-core/**. If one is needed, stop and report.(stop on a breach outside these; explain in the report)
Container & model:
L,mode:cloud,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). Security-boundary card, L across three packages; a cloud session outlives this PM session, so a p1 fix does not stall on it. The contract review runs atCONTRACT_REVIEW_TIER.Dev session:
session_016SuKjJJSq2iLTm4Z2PbAmY(the cloud session created for this claim at 10:56Z; the seat archives it at MERGED or close)Clause-②: yes (widening)
Clause-② reading: the shared rule is a new public member of
@objectstack/metadata-protocol, which is a widening. The loud refusal of a client-asserted provenance the platform never established restores ADR-0126 §2 and ADR-0131 D6. That is the negative boundary of clause ②, as the record on PR fix(runtime,metadata-protocol): the /automation write doors keep the packaged-base lock the /meta door keeps (#20679) #20817 judged the same door family, so it is not a narrowing. The dev re-reads this line against the real diff, and the PR's line 2 carries the measured grammar.Thread-read: 5909424428
Serial constraints cleared:
4b45afaed5); it was the serial predecessor.domain:services, the same axis) is out of flight (needs-user-decision), and triage orders it after this card.domain:engine,protocol.tstext edits) has merged, soprotocol.tshas no other open holder.mainb28054654d): only feat(spec): thepicklistmetadata kind — a shared option list select fields reference by name (#19518) #20823 (domain:spec) touchesmetadata-protocol/src, and only two test files (protocol.code-only-types.test.ts,protocol.meta-types-degenerate-derivation.test.ts), which this surface does not edit. PR fix(objectql)!: a caller-supplied formula value is stripped on every write path and reported as computed, and engine.validate runs the write doors (#20805) #20834 (crud-nodes.ts) has merged.requiresnames a plugin that is not loaded (1 key) #20312 (this seat) holdsprotocol.ts's gate call andruntime-authoring-gate.ts, region-disjoint from the/metaflow write. dead tracker citations in thedomain:servicespackages (629 sites, 112 numbers, 152 files): the ruling C+D stage for this lane (from #20556) #20596 (domain:services) holdsservice-settingsonly.objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{ "issue": 20761, "status": "done", "branch": "claude/issue-20761-provenance-server-held", "pr": "https://github.com/objectstack-ai/objectstack/pull/20853", "session": "session_016SuKjJJSq2iLTm4Z2PbAmY", "premise_still_valid": true, "summary": "Ruling B + A (5904938166), stage 2, implemented as one PR with the declared cross-lane surfaces. (1) The automation engine classifies a flow by the loader's set only: setPackagedFlowSource / packagedFlowOwner replace every body-stamp reading (the §7.3 guards in both directions, the arming gate, the toggle door, and the activation row's package). The plugin hands it packagedFlowReader, which asks the metadata protocol's new packagedArtifactOwner at question time (the same registry lookup packagedBaseRefusal reads). (2) One shared rule, tenantAuthoredWriteRefusal in metadata-protocol, is asked by the automation create, update and clone doors and by saveMetaItem for flows. A name in the set gets packagedBaseRefusal's own answer (403 NOT_OVERRIDABLE), so a shipped flow's round trip is refused as a locked base. A name not in the set, with stamps that classify the body as code-shipped, is refused 422 INVALID_METADATA, an existing metadata-protocol code, and nothing is written. Stamps that agree with the server's fact are a no-op: a stored row of that name bound to the stamped package, or the base the write names. That edge was MEASURED: a package-bound tenant flow is served with its binding as a package stamp and no tenant marker. Other /meta types and the two server-stated row rewrites are untouched. (3) The clone copy is judged by the same rule and saved as a tenant row through protocol.saveMetaItem, env-wide, with rollback if the save fails. It reads back on /meta and survives a cold boot. The update door's own persistence was NOT built (claim allowance); it is measured and named below.", "tests": "All at HEAD f0de8fe69 (origin/main 30839063b merged). metadata-protocol vitest: 192 files passed | 3 skipped, 2813 passed | 19 skipped, exit 0. service-automation vitest: 158/158 files, 1984/1984, exit 0. runtime vitest: 298/298 files, 4961 passed | 1 skipped, exit 0. dogfood (flow-provenance-server-held, packaged-flow-write-door-parity, automation-flow-clone-door, automation-toggle-tenant-scope, packaged-activation-ledger-reach): 5/5 files, 42/42, exit 0. typecheck of metadata-protocol, service-automation (with check:test-typecheck), runtime and dogfood: exit 0. Ablation, one-shot and not kept: the five fix sources (protocol.ts, automation.ts, engine.ts, plugin.ts, service-automation index.ts) were restored to the merge base on the working tree, with a trap restore. Build exit 0. ablation-dist-preflight --absent passed for metadata-protocol (tenantAuthoredWriteRefusal), service-automation (setPackagedFlowSource) and runtime (tenantAuthoredWriteRefusal). The dogfood pin file then read 8 failed / 5 passed of 13. The 5 that pass both ways are preservation pins: the two locked-base refusals, the two /meta round trips, and the other-type control. Restore: git status --porcelain empty, preflight showed the markers present in dist again, and the file read 13/13. The first ablation attempt was VOID: service-automation's build went red on the unreverted barrel export, so runtime dist kept the fix. It was redone with the barrel included, and that is stated in the PR.", "mcp_calls": "0 — no GitHub MCP tool was called (card and comment reads were unauthenticated REST GETs)", "api_writes": "3 relay strokes, each ONE repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (#20853, draft, body read back byte-identical, 11082 bytes); (2) label-write assign → POST /repos/objectstack-ai/objectstack/issues/20853/assignees (huangyiirene, read back MATCHES); (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/20761/comments. Plus git push (not REST) to the claim branch.", "open_questions": [ { "question": "Which existing ADR-0112 code refuses a body claiming a package's provenance for a name no package ships?", "options": [ "A INVALID_METADATA / 422 (metadata-protocol ledger row; the precedent is saveMetaItem's refusal of a layered read envelope, a body carrying server-owned keys)", "B VALIDATION_ERROR / 400 (standard catalog)", "C RESOURCE_CONFLICT / 409" ], "recommendation": "A, chosen. Axis 3: a loud 4xx naming the keys to remove, with no new code minted. Axis 2: it is the code this package already emits for bodies a client may not persist. B would put a second dialect on the same door. C is wrong because the name is free and no current state is contested." }, { "question": "How does the rule tell a legitimate round trip from a claim, given the measured echo of a package-bound tenant row (package stamp, no tenant marker)?", "options": [ "A Agreement with the server's fact: a stored row of that name bound to the stamped package, or the base the write names", "B Literal reading: any code-shipped classification is refused", "C Refuse only stamps naming a read-only (managed) package" ], "recommendation": "A, chosen. It is what the ruling means by 'agree with the server's fact'. B was measured to break a legitimate round trip (422 on the showcase). C would still let a body claim any non-managed package that the server never bound. It costs one store read, and only on the rare stamped path." }, { "question": "What does an engine with no loader's-set reader attached treat as packaged?", "options": [ "A Nothing (fail closed: guards protect nothing, the toggle door refuses every flow)", "B Fall back to body stamps" ], "recommendation": "A, chosen. B reintroduces exactly the trust this card removes. Every real composition with objectql has the protocol service. Suites that pin guard logic attach an explicit stand-in (loader-set.test-support.ts), and packaged-flow-source.test.ts proves the engine reads the attached set, not stamps." }, { "question": "Do the stored-metadata migration and package duplication (server-stated rewrites of rows already stored) go through the rule?", "options": [ "A No, exempt by their server-stated source / writeFace", "B Yes" ], "recommendation": "A, chosen. They are not authoring doors, and a pre-#16702 stored row may carry stale stamps. B would turn those rewrites into failures, where today they strip silently." }, { "question": "Grade of @objectstack/runtime in the changeset", "options": [ "A patch (no public surface change; behaviour fix)", "B minor" ], "recommendation": "A, chosen. metadata-protocol and service-automation carry the new public members at minor under Clause-②: yes (widening). check-changeset-no-major exit 0 locally; the PR-scoped level axis is CI's." } ], "out_of_scope_findings": [ "class: a · reach: POST /api/v1/automation answers 200 for a new flow, and after a cold boot on the same database file GET /automation/NAME answers 404; PUT /api/v1/automation/NAME on a flow stored through /meta answers 200, and after a cold boot the stored definition wins (both measured on the showcase via bootStack with a database file at f0de8fe69) · evidence: the create and update doors register in the engine only and write no metadata row (seat reading 5905846738 item 2, now measured) · dedupe words: automation create update door not durable, flow lost after restart, registerFlow no sys_metadata row, update door persistence", "carrier: none · the claim allowed naming it rather than building it; the clone's save does not cover these two doors for free, because making them durable changes both doors' contract. Also in the PR's Acceptance notes.", "class: a · reach: PUT /api/v1/meta/flow/NAME with a package query naming an id no installed package has answers 200 state=active (measured on the showcase host-config boot at f0de8fe69) · evidence: the row is stored bound to a package that does not exist, and its served body then carries that binding · dedupe words: meta save package id not installed accepted, orphan package binding sys_metadata, WRITABLE_PACKAGE_REQUIRED nonexistent package", "carrier: none · noted in the PR's Acceptance notes; not filed by the dev (seat files).", "carrier: none · observation, not filed: the loader's set is the SchemaRegistry view for both the #20679 lock and now the engine (one source, so they cannot disagree). A dev-time artifact reload that adds a code flow is not re-registered into that view until restart (source reading of the objectql and metadata reload hooks, not measured)." ], "gates": "dispatch-gates --commands at f0de8fe69: 69 derived; all 69 run with exit codes recorded; --ran reconciliation: 69 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero). Two first readings were not measurements and were rerun: check-plugin-teardown-shape --self-test exited 3 on the shallow clone and 0 after git fetch --unshallow; check:dual-build-cjs-loads answered PREREQUISITE NOT MET (eight unrelated packages had no dist) and exited 0 after they were built. pnpm lint: a proven narrowing, not a full run. (1) Population: every changed .ts is under packages/**, which eslint.config.mjs's packages/** and **/*.ts blocks lint. (2) eslint --no-inline-config --format json: 23 files, 0 errors, 0 warnings. (3) Invariance: the config never enables type-aware linting (eslint.config.mjs:327-328), so untouched files' verdicts cannot move. CI: in_progress at report time (not awaited, per os-dev).", "deviations": [ "The claim names the cloud session's own checkout as the worktree; per os-dev rule 1 and the worktree guard hook, the work ran in a dedicated worktree ../objectstack-issue-20761 on the claim branch (the primary checkout was detached to free the branch). The branch was fast-forwarded f7c6d65f5 → bbe03f406 (ancestor check exit 0) before any edit; no force-push.", "#20679's own pin (runtime automation-packaged-base-lock.test.ts) asserted that a body claiming a package on the customer's flow is accepted with 200. Ruling point 2 reverses that, so the assertion now reads 422 INVALID_METADATA with the engine never entered. Its clone case stands in for the new save with a spy (the save is pinned in the new runtime file).", "Ten existing service-automation suites that pin §7.3 and toggle LOGIC with stamped bodies now attach a test-only loader's-set stand-in (loader-set.test-support.ts) on the specific engines that needed it; the set-vs-stamps contract itself is pinned separately.", "The metadata-protocol fold-population pin (protocol.read-verb-canonical-fold.test.ts) gains the two new folding verbs; its title count went from fourteen to sixteen. PR #20823's two files are untouched.", "The first ablation run was void (partial build); it was redone and reported as such." ], "files_changed": ".changeset/20761-flow-provenance-server-held.md; packages/metadata-protocol/src/{protocol.ts, protocol.tenant-authored-write.test.ts (new), protocol.read-verb-canonical-fold.test.ts}; packages/runtime/src/{domains/automation.ts, flow-clone.ts (header comment), domains/automation-tenant-authored-write.test.ts (new), domains/automation-packaged-base-lock.test.ts}; packages/services/service-automation/src/{engine.ts, plugin.ts, index.ts, packaged-flow-source.test.ts (new), loader-set.test-support.ts (new), and 10 suites attaching the stand-in: engine, engine-residual-log-cause, flow-activation-late-trigger, flow-activation-ledger, flow-label-on-result, flow-terminal-messages, node-type-vocabulary-seal-warning, subflow-guard-every-door, suspended-run-store, toggle-door-packaged-only}; packages/qa/dogfood/test/flow-provenance-server-held.dogfood.test.ts (new). 24 files, +1399 / -70. All within claim 5909772878's surface; no spec, objectql or metadata-core file.", "round_trips": "Studio: NOT MEASURED, reason: the Studio source lives in objectui, which is not in this container. The server-side echo Studio receives WAS measured on a showcase boot. A customer flow created through the automation door is served with no provenance stamps, on both read doors. A customer flow stored through /meta is served with none either. A shipped flow is served with its package's stamps. A package-bound tenant flow is served with its binding as a package stamp and no tenant marker; its round trip is accepted on both doors (pinned, including after a cold boot), and it read 422 under the literal rule before the agreement edge was added. CLI: source reading of packages/cli/src/commands/meta/register.ts:59-76 — os meta register sends the file verbatim to PUT /meta/:type/:name, and os meta get prints the served document. So the CLI round trip is the /meta round trip, measured above: a customer flow is accepted, a package-bound customer flow is accepted, and a shipped flow is refused as a locked base.", "seam": "loader's-set read: packages/metadata-protocol/src/protocol.ts ObjectStackProtocolImplementation.packagedArtifactOwner. The one shared rule: ObjectStackProtocolImplementation.tenantAuthoredWriteRefusal in the same file; its lock branch reuses packagedBaseRefusal, which gains an optional packageId for the named-base save. /meta applies it in saveMetaItem before the silent strip (authoring writes only). Automation doors: packages/runtime/src/domains/automation.ts refuseUnauthoredFlowWrite (POST /, PUT /:name, and clone), plus the clone arm's protocol.saveMetaItem. Engine: packages/services/service-automation/src/engine.ts AutomationEngine.setPackagedFlowSource / packagedFlowOwner / isPackagedFlow, wired in plugin.ts by packagedFlowReader (the kernel 'protocol' service, read lazily)." }Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
ACCEPT: PR #20853 at
f0de8fe6(stage 2: a flow's package provenance is the loader's fact, and every authoring door refuses a claim the platform never made)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T13:05Z · ⛔ no request body, header or field spelling (this card's disclosure discipline)5911889128on the PR,CONTRACT_REVIEW_TIER, headf0de8fe6, PASS./metafor flows. A shipped name is refused as a locked base (403 NOT_OVERRIDABLE); a body claiming a package the loader never loaded is refused422 INVALID_METADATA, an existing ledgered code.main, first lineFixes #20761(stage 2 is the card's last stage), andClause-②: yes (widening), judged right: the new refusal is the negative boundary of clause ②, as the fix(runtime,metadata-protocol): the /automation write doors keep the packaged-base lock the /meta door keeps (#20679) #20817 record judged for the same door family.5909772878's declared cross-lane surface. Nospec,objectqlormetadata-corefile.@objectstack/metadata-protocoland@objectstack/service-automationminor;@objectstack/runtimepatch./metaflow save accepts a write bound to a package id that no installed package has, and stores it active #20863 (class a): the/metaflow save accepts a binding to a package id no installed package has. After this PR it is display-only, as the card states.domain:cliand p1.Fixesdoes not, removespm:dispatched, and archives the cloud dev session.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Landed: PR #20853 →
76bd58fac4(stage 2: which flows are packaged is the loader's fact; every flow written through an authoring door is tenant-authored)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T13:36Z · ⛔ no request body, header or field spelling76bd58fac472c8ff20c3adfb173f09c8ac9a00adis onorigin/mainas a single-parent squash overb531c7bf0(PR feat(cli,metadata-protocol): the save door compiles an html page's source against the deployment's SDUI manifest #20852, which merged just ahead of it in the same queue batch).git diff --statof the PR headf0de8fe6against76bd58faover the PR's 24 files differs in one file only,packages/metadata-protocol/src/protocol.ts, by 54 inserted lines. Those lines equal PR feat(cli,metadata-protocol): the save door compiles an html page's source against the deployment's SDUI manifest #20852's ownprotocol.tshunks exactly (50 content lines on each side, identical), so the squash is this PR on top of feat(cli,metadata-protocol): the save door compiles an html page's source against the deployment's SDUI manifest #20852.tenantAuthoredWriteRefusalis onorigin/main(6 lines inprotocol.ts).completedand removespm:dispatchedin the same act. The cloud dev sessionsession_016SuKjJJSq2iLTm4Z2PbAmYis archived.pm:queue), dispatched next by this seat;/metaflow save accepts a write bound to a package id that no installed package has, and stores it active #20863 (/metaaccepts a binding to an uninstalled package id), both waiting for triage.Generated by Claude Code
fix(runtime): mount POST /automation/:name/clone on the dispatcher br…
fix(automation): which flows are packaged is the loader's fact, and e…
fix(service-automation): boot-time flow precedence classifies contend…
fix(runtime)!: the /automation create and update doors save the flow …
fix(metadata-protocol)!: refuse a flow saved into a package this depl…
fix(metadata-protocol): the by-name read of a shipped flow name serve…
fix(metadata-protocol): the layered read of a shipped flow name repor…
fix(rest,runtime): the published-snapshot doors answer the package's …