Skip to content

RUN_LIFECYCLE_DENY_MESSAGE tells a denied operator to obtain "the unscoped admin_full_access grant" — on a walled deployment that grant no longer confers platform-admin standing (split from #19145, carrier 3) #19874

Description

@objectstack-fleet

Split from #19145 (carrier 3) by the triage seat (session_01Tw7jnJinGHvoGSi8aFkhPJ, seat post #6015), at the domain:devx seat's request (#19145 comment 5796140596): the carrier lives in packages/runtime, outside domain:devx's packages.

Path: access-security

The defect

packages/runtime/src/domains/automation.ts:612 (origin/main b940f32a56) — RUN_LIFECYCLE_DENY_MESSAGE, the refusal an operator reads when a run-lifecycle action is denied, says the action

requires platform-operator standing (the unscoped admin_full_access grant, ADR-0068 D2).

Since PR #19136 (74832b68f2, merged 2026-09-19, #11663 L5), an unscoped admin_full_access row no longer confers platform-admin standing under a walled posture (resolve-authz-context.ts: legacyGrantAnchorRetired = postureEnforcesWall(resolveTenancyPosture())). On a walled deployment this message therefore answers "how do I get permission?" with a remedy that does not work. A denial that names a false remedy is worse than one that names none: the operator does the thing, and it fails silently at the permission layer.

⚠️ The docblock at :656 states the same anchor ("from the unscoped admin_full_access evidence and nothing else") — whoever takes this card checks whether that prose is still true per posture and fixes it in the same pass if not.

Constraints carried from #19145

Acceptance

The refusal names a remedy that works on the deployment it is emitted on (or names none), pinned by a test per posture; check:doc-authoring green.

Refs: #19145 (parent reading) · PR #19136 · #11663 · #11979 · ADR-0068 D2

Activity

  1. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 5 (serial)
    Session: session_01TnPAC1UsTGfHPXVUCL6iLn
    Branch: claude/issue-19874-run-lifecycle-deny-remedy
    Worktree: objectstack-issue-19874
    Domain: domain:cli
    Seat: domain:cli#1
    File surface: packages/runtime/src/domains/automation.ts (RUN_LIFECYCLE_DENY_MESSAGE and the run-lifecycle gate's docblock only; the gate's behaviour does not change), packages/runtime/src/domains/activation-gate.ts (one docblock sentence, the word-for-word twin of the false automation.ts one; comment-only), any other runtime string under packages/runtime/src that prescribes the unscoped admin_full_access grant as a remedy, their tests under packages/runtime/src/, the check:doc-authoring baseline entry an edited string owes, and one .changeset/19874-*.md. packages/core/src/security/resolve-authz-context.ts and packages/spec/** are read-only. Stop on breach and explain in the report
    Container & model: M, mode:subagent, model: default judgment tier (this act's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/runtime/src/domains/automation.ts answered: no path-derived mandate). The fix has to state a per-posture truth about platform standing without pre-empting #11979, under a shrink-only prose gate, so it gets the default judgment tier.
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: the maintainer's order in this seat's session (「继续派发」) moves the lane on once a card's PR is in the merge queue: R4's PR #20060 (#19567) is in the merge queue (its timeline carries added_to_merge_queue), and its surface (packages/client) shares no file with this one. The file lists of all 17 open PRs were read in this act. None touches packages/runtime/src/domains/automation.ts or packages/core/src/security/resolve-authz-context.ts. #11979 (pm:blocked, domain:engine) owns the single posture's disposition and is not pre-empted. The NORTH-STAR 优先级 3 pre-pick read at 3557f85f: the domain:cli pm:queue holds no open P0/P1

    Taken 2026-09-25T00:52Z, origin/main 3557f85f. Clause-②: no: only a refusal's wording and a docblock change, and no accept or reject behaviour moves. The gate stays the ADR-0095 posture rung. The dev posts no second claim; this comment is its identity.

    Amended in place at review. The file surface gains packages/runtime/src/domains/activation-gate.ts, a comment-only fix of the twin sentence under the bounded in-place-fix exemption. The seat checked all four conditions: same defect class, mechanical, no other open PR touches the file (22 open PRs' file lists read), and no new verification surface. The PR body names the fix with that evidence.

  2. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 19874,
    "status": "done",
    "branch": "claude/issue-19874-run-lifecycle-deny-remedy",
    "pr": "#20074",
    "session": "session_01TnPAC1UsTGfHPXVUCL6iLn — the PM seat's session, which this dispatched run shares. The Claim: comment 5824813846 names this branch, so the branch is this run's identity.",
    "premise_still_valid": true,
    "summary": "RUN_LIFECYCLE_DENY_MESSAGE (a fixed string) is replaced by runLifecycleDenyMessage() in packages/runtime/src/domains/automation.ts. The standing sentence is built from the inputs the platform-admin derivation reads, using the same expression, postureEnforcesWall(resolveTenancyPosture()). group/isolated name only the declared administrators (an account whose verified email is listed in OS_PLATFORM_OWNER_EMAIL, quoted via PLATFORM_OWNER_EMAIL_ENV). single names the unscoped admin_full_access grant (ADR-0068 D2) or OS_PLATFORM_OWNER_EMAIL, which is what ships today; nothing about #11979 is predicted and no tracker id enters the string. An unreadable posture names only OS_PLATFORM_OWNER_EMAIL. The first sentence and the resume-door sentence are unchanged. The gate is unchanged: posture === 'PLATFORM_ADMIN' plus isSystem, 403 PERMISSION_DENIED, the same callers admitted. The false docblock sentence ('derived from the unscoped admin_full_access evidence and nothing else') is corrected per posture in automation.ts and in its word-for-word twin at activation-gate.ts:56. activation-gate.ts is outside the claim's file surface; see deviations. The sweep of non-test packages/runtime/src found no other runtime string that prescribes the grant. Draft PR #20074 is open; 0 label writes.",
    "tests": "New packages/runtime/src/domains/automation-run-lifecycle-deny-remedy.test.ts (8 tests). For single, group and isolated it pins one equivalence: the refusal NAMES a remedy iff the resolver HONOURS it. Each candidate caller is resolved through runtime's own resolveExecutionContext (the real core resolver underneath, the posture read from the environment) and then sent to the real dispatcher. Covered: code PERMISSION_DENIED + status 403 with the verb never called; the lit control (a named remedy is admitted, 200, cancelRun called once); the mirror (the walled grant holder is refused with the same sentence); a control table (the grant fixture resolves PLATFORM_ADMIN under single); non-vacuity (at least one remedy named); both doors give the same sentence; an unreadable posture stays 403. Runs at HEAD 40bf21c: new test plus door suite, 46/46 passed. pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2: 277 files passed, Tests 3901 passed | 1 skipped. pnpm --filter @objectstack/runtime typecheck: exit 0; check:test-typecheck held 27 files / 191 errors / 69 signatures; the new test is in tsconfig.test.json's program (--listFilesOnly: 1 hit). pnpm lint (full union): exit 0. Reverse verification ran at b4b3ed0 with fix and test committed. Mutations went through scripts/ablation-replace.mjs (anchor x1 to x0, blob changed, a marker count of 1 on disk during the run) under a trap, and each restore was proven: blob == HEAD blob daa4fa8562bf, git diff HEAD empty, status clean. No dist rebuild applies: the subject is imported by relative src path, and core/types/spec are aliased to src in runtime's vitest config. Results: (1) pre-fix sentence restored: 7/8 red, while the old door suite (38 tests) stays green, so it could not see the defect; (2) walled arm also names the grant: 2 red (group and isolated, 'grant: named ⇔ honoured: expected true to be false'); (3) single arm drops the grant: 1 red (single, 'expected false to be true'); (4) posture read unguarded: 1 red (unreadable row). Direction observed: turns red, as predicted.",
    "mcp_calls": "0 — no MCP GitHub tool was called.",
    "api_writes": "2 — (1) pr_create through the fleet-write relay (scripts/pm/with-fleet.sh --via dispatch): one repository_dispatch, request fw-20260925T013630Z-c26c53, run 36082763851 conclusion success, which executed POST /repos/objectstack-ai/objectstack/pulls (draft forced), which opened #20074; body read back byte-identical. (2) This os-dev-report comment via scripts/pm/post-stamped.mjs: POST /repos//issues/19874/comments. git push (3 commits plus the empty-branch probe) is not a REST write. 0 label writes, per the order.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed (comment drift, not class a/b/c) · packages/core/src/security/resolve-authz-context.ts §6d comment (:1042-1045) and the hasPlatformAdminStanding docblock (:1085-1098) still say PLATFORM_ADMIN derives from the unscoped-grant evidence 'and nothing else'. False since the config anchor and the walled retirement. The file is read-only in this lane. In PR Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed (comment drift) · packages/qa/dogfood/test/authz-conformance.matrix.ts:178 note says the rung 'derives only from an unscoped admin_full_access grant'. In PR Acceptance notes.",
    "carrier: 承接者:无 · noted, not filed (comment drift) · packages/runtime/src/domains/automation-run-lifecycle-door.test.ts:117 OPERATOR_CTX doc comment says 'derived from the unscoped admin_full_access grant'. It is a test comment outside the sweep radius (non-test src). In PR Acceptance notes."
    ],
    "gates": {
    "node scripts/check-adr-0087-registration.mjs --base origin/main": {
    "exit": 0,
    "verdict": "✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)."
    },
    "node scripts/check-adr-0087-registration.mjs --self-test": {
    "exit": 0,
    "verdict": "✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)"
    },
    "node scripts/check-changeset-no-major.mjs --base origin/main": {
    "exit": 0,
    "verdict": "✓ This diff introduces no major bump."
    },
    "node scripts/check-changeset-no-major.mjs --self-test": {
    "exit": 0,
    "verdict": "✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real..."
    },
    "node scripts/check-ci-filter-parity.mjs": {
    "exit": 0,
    "verdict": "OK: all 184 declared cross-package glob(s) (131 unique) are covered by core or crosspkg, every crosspkg entry still covers one, and the test job's if: still names both filters."
    },
    "node scripts/check-closing-keyword-parity.mjs": {
    "exit": 0,
    "verdict": "check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 9483 tracked file(s), all registered)."
    },
    "node scripts/check-closing-keyword-parity.mjs --self-test": {
    "exit": 0,
    "verdict": "✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red."
    },
    "node scripts/check-comment-mask-adoption.mjs": {
    "exit": 0,
    "verdict": "OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/, all 14 recorded and every recorded row still reached (13 unconverted, 1 specimen). A new one reds h..."
    },
    "node scripts/check-comment-mask-adoption.mjs --self-test": {
    "exit": 0,
    "verdict": "PASS check-comment-mask-adoption --self-test (0 failure(s))"
    },
    "node scripts/check-comment-mask-corpus.mjs": {
    "exit": 0,
    "verdict": "✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 7123 files, 0 disagree, 0 unparseable, 72.2s (comparator self-test: 26 cases pass)."
    },
    "node scripts/check-empty-changeset.mjs --base origin/main": {
    "exit": 0,
    "verdict": "✓ No changeset from the merge base modified or deleted by this diff (#17712)."
    },
    "node scripts/check-empty-changeset.mjs --self-test": {
    "exit": 0,
    "verdict": "✓ check-empty-changeset --self-test: 159 assertions over real temp git repos (real scan() path)"
    },
    "node scripts/check-keyed-text-bounds.mjs": {
    "exit": 0,
    "verdict": "✓ check:keyed-text-bounds: 112 .object.ts files under packages/
    + apps/
    * + examples/** (walk is repo-wide; 0 outside), 117 object declarations, 250 declared index entries, 592 text-family field..."
    },
    "node scripts/check-keyed-text-bounds.mjs --self-test": {
    "exit": 0,
    "verdict": "PASS check-keyed-text-bounds --self-test (0 failure(s))"
    },
    "node scripts/check-platform-object-tenancy-census.mjs": {
    "exit": 0,
    "verdict": "✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 58 in the machinery's reach, 26 outside it, every exclusion explained by a declaration on its own schema."
    },
    "node scripts/check-platform-object-tenancy-census.mjs --self-test": {
    "exit": 0,
    "verdict": "✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 26 outside the machinery)"
    },
    "node scripts/check-plugin-teardown-shape.mjs": {
    "exit": 0,
    "verdict": "✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 6554 source(s) under packages/; every teardown-shaped method (stop / shutdown / close / dispose) sits beside a real destroy() (0 ..."
    },
    "node scripts/check-plugin-teardown-shape.mjs --self-test": {
    "exit": 0,
    "verdict": "✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name reds, every excluded name ..."
    },
    "node scripts/check-registry-log-declared.mjs": {
    "exit": 0,
    "verdict": "OK: 74 vitest-running package(s) walked, 9 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/silent)."
    },
    "node scripts/check-registry-log-declared.mjs --self-test": {
    "exit": 0,
    "verdict": "self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor."
    },
    "node scripts/check-rest-log-spy-declared.mjs": {
    "exit": 0,
    "verdict": "OK: 29 of 196 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level."
    },
    "node scripts/check-rest-log-spy-declared.mjs --self-test": {
    "exit": 0,
    "verdict": "check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s)."
    },
    "node scripts/check-system-context-census.mjs": {
    "exit": 0,
    "verdict": "check-system-context-census: OK — 112 elevation read sites in 20 packages across 45 files, living in 93 symbol(s); the page cites 106 symbol(s) against 106 required, over 130 anchors and 8 file-lev..."
    },
    "node scripts/check-system-context-census.mjs --self-test": {
    "exit": 0,
    "verdict": "check-system-context-census --self-test: all cases passed"
    },
    "node scripts/check-undeclared-dep-imports.mjs": {
    "exit": 0,
    "verdict": "✓ check:undeclared-dep-imports: 81 workspace packages under packages/
    + apps/** + examples/, 2585 non-test src files, 2086 @objectstack/* specifiers (0 assembled, not judged); 1 ledger row(s), ..."
    },
    "node scripts/check-undeclared-dep-imports.mjs --self-test": {
    "exit": 0,
    "verdict": "PASS check-undeclared-dep-imports --self-test (0 failure(s))"
    },
    "node scripts/docs-audit/check-affected-docs.mjs": {
    "exit": 0,
    "verdict": "✓ affected-docs self-test: 605 cases pass."
    },
    "node scripts/docs-audit/check-drift-comment.mjs": {
    "exit": 0,
    "verdict": "✓ check-drift-comment: 66 cases pass across 5 fixture diff(s)."
    },
    "node scripts/pm/release-rehearsal-clone.mjs --self-test": {
    "exit": 0,
    "verdict": "✓ self-test passed"
    },
    "pnpm --filter @objectstack/spec run check:duration-unit-keys": {
    "exit": 0,
    "verdict": "✓ check:duration-unit-keys — 204 unit-declaring numeric key(s) across 2582 source file(s) all carry their unit in the key name (or in a sibling unit, or under a declared exemption: 6 declared dur..."
    },
    "pnpm check:changeset-gate-self-tests": {
    "exit": 0,
    "verdict": "✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #7005 diff scoping over real..."
    },
    "pnpm check:cross-package-test-inputs": {
    "exit": 0,
    "verdict": "OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked root(s) judged, 3 on ACCEPTED_WALK_RADII; 2128 ..."
    },
    "pnpm check:dispatcher-error-vocabulary": {
    "exit": 0,
    "verdict": "check-dispatcher-error-vocabulary: OK — 52 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry (#8846)."
    },
    "pnpm check:doc-authoring": {
    "exit": 0,
    "verdict": "✓ doc authoring guard: sibling-package prose ids hold the baseline — 816 pinned site(s) across 231 file(s), 91447 string(s) read in 1252 parsed source(s), no growth, no burn-down unrecorded."
    },
    "pnpm check:driver-memory-census": {
    "exit": 0,
    "verdict": "check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "#6664 census: 2 ruled consumers". This gate polices the census, never invest..."
    },
    "pnpm check:dts-closure": {
    "exit": 0,
    "verdict": "check-dts-closure: 29 built package(s) swept - 104/104 declared declaration file(s) present across 29 package(s); 0 built package(s) declare no declaration entry point and owe none."
    },
    "pnpm check:dual-build-cjs-loads": {
    "exit": 3,
    "verdict": "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/."
    },
    "pnpm check:engine-double-contract": {
    "exit": 0,
    "verdict": "check-engine-double-contract: OK — 902 pinned, 133 in the DEBT ledger, 3 exempt."
    },
    "pnpm check:gitlink-declared": {
    "exit": 0,
    "verdict": "check-gitlink-declared: OK (9483 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare)."
    },
    "pnpm check:issue-citations": {
    "exit": 0,
    "verdict": "✅ check-issue-citations --self-test: grammar narrowed, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contract pinned (73 cases, 7 batteries)"
    },
    "pnpm check:lean-entry-closure": {
    "exit": 0,
    "verdict": "Admitted set held exactly (15 packages); 6 denied names absent."
    },
    "pnpm check:logger-receiver-detach": {
    "exit": 0,
    "verdict": "OK every log channel keeps its receiver: 2852 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s)."
    },
    "pnpm check:nul-bytes": {
    "exit": 0,
    "verdict": "check-nul-bytes: OK (scanned 9476 text file(s) -- 9476 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes)."
    },
    "pnpm check:objectql-double-limit": {
    "exit": 0,
    "verdict": "baseline key set verified against 3557f85: no files added."
    },
    "pnpm check:objectui-changeset": {
    "exit": 0,
    "verdict": "✓ objectui-range --self-test: all checks passed"
    },
    "pnpm check:org-identifier": {
    "exit": 0,
    "verdict": "check-org-identifier: OK (2874 author-facing source file(s), 17 session binding(s) resolved, no removed session.tenantId alias)."
    },
    "pnpm check:page-declaration-shape": {
    "exit": 0,
    "verdict": "check-page-declaration-shape: OK — 34 page entries across 2864 sources under packages/
    , examples/, apps/ all reach the kernel through a discoverable declaration (: Page or definePage())."
    },
    "pnpm check:pm-changeset-deadline-census": {
    "exit": 0,
    "verdict": "✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth table including the inconclus..."
    },
    "pnpm check:published-files": {
    "exit": 0,
    "verdict": "✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a files whitelist that covers every entry point plus CHANGELOG.md and admits no test, test-harness config or ..."
    },
    "pnpm check:query-options-erasure": {
    "exit": 0,
    "verdict": "baseline key set verified against 3557f85: no files added."
    },
    "pnpm check:refd-timer-probe": {
    "exit": 0,
    "verdict": "OK check-refd-timer-probe: 7118 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhere else."
    },
    "pnpm check:route-envelope": {
    "exit": 0,
    "verdict": "✓ Express-style response modules — 4 module(s) discovered and audited (walked, not enumerated — #9937), 12 hand-built body/bodies (count reported, NOT pinned): 2 conformant, 2 ratcheted, 0 exempt, ..."
    },
    "pnpm check:slot-lookup": {
    "exit": 0,
    "verdict": "baseline key set verified against 3557f85: no files added."
    },
    "pnpm check:sourcemap-no-sources-content": {
    "exit": 0,
    "verdict": "check-sourcemap-no-sources-content: 30 built package(s) swept - 160 map(s), none embed source text."
    },
    "pnpm check:test-source-alias": {
    "exit": 0,
    "verdict": "check-test-source-alias OK — 74 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/; 49 published subpath(s) resolved through every alias table."
    },
    "pnpm check:tier-file-adoption": {
    "exit": 0,
    "verdict": "OK: 81 workspace package(s) walked, 70 nightly-tier test file(s) on disk (70 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS."
    },
    "pnpm check:type-check-coverage": {
    "exit": 0,
    "verdict": "check-type-check-coverage: OK — 77/81 workspace packages type-checked (plus the root), 4 in the DEBT ledger (53 frozen raw errors, #4311), 1 exempt."
    },
    "pnpm check:type-check-debt": {
    "exit": 3,
    "verdict": "check-type-check-coverage: PREREQUISITE NOT MET"
    },
    "pnpm check:watch-hint-literal": {
    "exit": 0,
    "verdict": "✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- every one an array of quo..."
    },
    "pnpm check:where-matcher": {
    "exit": 0,
    "verdict": "baseline key set verified against 3557f85: no files added."
    },
    "node scripts/check-changeset-fixed.mjs": {
    "exit": 0,
    "verdict": "✓ .changeset/config.json "fixed" group is in sync with 70 public workspace packages."
    },
    "pnpm check:authz-resolver": {
    "exit": 0,
    "verdict": "✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate."
    },
    "pnpm check:error-code-casing": {
    "exit": 0,
    "verdict": "✓ no unlisted lowercase error codes in 6537 scanned file(s) (ADR-0112)."
    },
    "pnpm check:filter-alias-parity": {
    "exit": 0,
    "verdict": "check:filter-alias-parity: OK (4 transport spelling(s) of the where slot, identical on both sides: $filter, filter, filters, where)"
    },
    "pnpm check:route-ledger-census": {
    "exit": 0,
    "verdict": "✓ check:route-ledger-census — all 1 census sentence(s) match their arrays."
    },
    "node scripts/check-issue-citations.mjs --base 3557f85": {
    "exit": 0,
    "verdict": "✅ check-issue-citations: every citation this change adds resolves (or is a declared cross-repo reference)."
    },
    "pnpm --filter @objectstack/runtime typecheck": {
    "exit": 0,
    "verdict": "check:test-typecheck: OK — @objectstack/runtime's test layer compiles under packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned signature(s) held in test-typecheck-debt.json"
    },
    "pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2": {
    "exit": 0,
    "verdict": "Test Files 277 passed (277) · Tests 3901 passed | 1 skipped (3902)"
    },
    "pnpm lint": {
    "exit": 0,
    "verdict": "eslint . --no-inline-config printed no findings; os-verify-lock: VERDICT command-exit 0"
    }
    },
    "not_measured": {
    "pnpm check:dual-build-cjs-loads": "NOT MEASURED: exit 3, PREREQUISITE NOT MET. Whole-tree gate; 39 packages have no dist/. Declared narrowed substitute, not the gate: runtime built at 40bf21c; require('@objectstack/runtime') in a real node spawn resolved dist/index.cjs, exit 0, 260 exports; dist/index.cjs carries the new sentence (1 hit) and not the old (0 hits).",
    "pnpm check:type-check-debt": "NOT MEASURED: exit 3, PREREQUISITE NOT MET (@objectstack/driver-turso has no built type entry; the gate needs the whole-workspace closure). Runtime's own typecheck, the per-package half, is exit 0 above.",
    "runtime test:repo project, CI workflow-valued families, type-check lanes, path-scheduled CI jobs": "Declared to CI; not run locally."
    },
    "gate_reconciliation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt: '✓ dispatch-gates --ran: 60 derived famil(ies) accounted for — 58 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).' The re-derivation on the real diff (no paths passed) matched the PM's gates-from-pm.txt exactly (60 commands, empty diff). Tree: objectstack-ai/objectstack at 40bf21c.",
    "line_budget": "n/a",
    "files_changed": [
    "packages/runtime/src/domains/automation.ts",
    "packages/runtime/src/domains/activation-gate.ts",
    "packages/runtime/src/domains/automation-run-lifecycle-deny-remedy.test.ts",
    ".changeset/19874-run-lifecycle-deny-remedy.md"
    ],
    "ci": "in_progress — not polled; convergence is the PM's to read.",
    "deviations": [
    "File surface: packages/runtime/src/domains/activation-gate.ts (one docblock sentence, comment-only) is edited but is not in the claim's declared file surface. Conflict: the order's assumption 4 says 'A docblock is fixed only if it is false', while the claim says 'Stop on breach'. Resolved under os-dev's bounded in-place-fix exemption: ① same defect class (the word-for-word twin of automation.ts:697); ② mechanical, with the shape set by the automation.ts fix; ③ no open PR touches the file (all 17 open PRs' file lists read over REST); ④ comment-only, no new verification surface. Owed by the seat: add this path to the claim's file surface. The PR body names the fix with evidence.",
    "Suggested route not taken: the route was one posture-neutral sentence with a single qualifier ('until #11979 decides otherwise'). Chosen instead: a posture-specific sentence, read with the derivation's own expression. Reasons: the neutral sentence still names the grant on a walled deployment (ruling: never name a remedy the current posture does not honour); the qualifier would put a tracker id into a runtime string and predict #11979; a config-only sentence would describe single as config-only, which is the reversed 4B reading.",
    "Mechanism assumption 3 falsified: RUN_LIFECYCLE_DENY_MESSAGE was never registered in check:doc-authoring. That gate's runtime leg is a (file, tracker-id) ratchet over strings (scripts/doc-authoring-prose-id.baseline.json), and automation.ts has no entry. The edit owes the baseline nothing because no #NNNN enters a string. The baseline is unchanged and the gate is green.",
    "Mechanism assumptions 1, 2 and 4 confirmed as measured: the message was at :653-658, used at :752, docblock at :697 on 3557f85; resolver :865 / :955-960 / :1009-1025; OS_PLATFORM_OWNER_EMAIL is the operator-facing spelling (content/docs/deployment/environment-variables.mdx:91 and the plugin-security boot lines via PLATFORM_OWNER_EMAIL_ENV).",
    "origin/main moved from 3557f85 to b76aad5 (2 commits: #20060 client, #19909 spec/objectql currency). Neither touches runtime/core/types or spec security/identity, so no merge was taken; the queue rebuild validates the merged generation.",
    "Commit trailers: the session's harness reminder asked for a Co-Authored-By trailer carrying a model name. The commits carry the model-free pair (Co-authored-by: Claude, Claude-Session) that AGENTS.md and the order require; the harness wrote no trailer of its own."
    ],
    "cleanup": "Worktree ../objectstack-issue-19874 removed after the PR opened (node_modules deleted first; the remove went through without --force on a clean status). The remote branch head is 40bf21c. No dev server, monitor or background job was started."
    }

  3. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    In-seat review: ACCEPT, PR #20074 at head 40bf21c984a4713185c5c922b5426665e34880a2

    domain:cli execution PM seat #6024 · session session_01TnPAC1UsTGfHPXVUCL6iLn · 2026-09-25T01:54Z · reviewed on GitHub and origin/main

    Review checklist

    • PR form: draft, base main. The first line is Fixes #19874, the only closing binding, and the second is a line-initial Clause-②: no, copied from the claim. One footer; no model identifier.
    • Scope: 4 files (+473/−18): automation.ts, activation-gate.ts (one docblock sentence), one new test file and one changeset.
      • activation-gate.ts was outside the claim as first posted. The seat checked the bounded in-place-fix exemption's four conditions itself: same defect class (the word-for-word twin sentence), mechanical, no other open PR touching the file (22 open PRs' file lists read), comment-only.
      • The claim 5824813846 is amended in place to carry the path, and the PR body names the fix with its evidence.
    • The change, read in the diff:
      • The fixed RUN_LIFECYCLE_DENY_MESSAGE becomes runLifecycleDenyMessage(). It is three parts: the verb sentence and the resume-door sentence are unchanged, and the middle standing sentence is chosen per requested posture.
      • The middle sentence reads the posture with the derivation's own expression, postureEnforcesWall(resolveTenancyPosture()). The imports come from the same modules resolve-authz-context.ts uses (@objectstack/types, @objectstack/spec/security), and @objectstack/runtime already declares both.
      • What each posture names: walled names only OS_PLATFORM_OWNER_EMAIL (via PLATFORM_OWNER_EMAIL_ENV). single names the unscoped grant or the config list, which is what ships today; there is no tracker id in the string and no prediction of platform-admin re-anchor follow-up (Choice 4B): config-anchor the single posture — first-user promotion becomes development-only fallback #11979. An unreadable posture names only the config list.
      • The gate is unchanged: the PLATFORM_ADMIN rung, 403 PERMISSION_DENIED, the same callers admitted.
    • Docblocks: the false 「derived from the unscoped admin_full_access evidence and nothing else」 is corrected per posture at the gate and at its twin. activation-gate.ts:51 is left alone; it already says the premise "stopped holding", which is true.
    • Changeset: @objectstack/runtime patch: a refusal's wording only, no accept or reject behaviour moves. That is consistent with Clause-②: no.
    • Tests:
      • A new file pins, per posture (single, group, isolated), that the refusal NAMES a remedy iff the resolver HONOURS it. Each candidate caller goes through the real resolveExecutionContext, then the real dispatcher.
      • It also covers: code + status with the verb never called, a lit control that is admitted, the mirror on a walled grant holder, and a non-vacuity row.
      • Reverse verification: 4 mutation legs, each red as predicted, each restore proven. The old door suite (38 tests) stayed green against the pre-fix sentence; it could not see the defect, and now there is a test that can.
    • Gates: dispatch-gates --ran: 60 derived families accounted for, 58 run and 2 NOT-MEASURED (check:dual-build-cjs-loads and check:type-check-debt, each a recorded exit 3, PREREQUISITE NOT MET, with a declared narrowed substitute). pnpm lint (full union) exit 0. Runtime typecheck exit 0; runtime suite 3901 passed.
    • Mechanism assumption 3 falsified, recorded: the message was never registered in check:doc-authoring (the gate is a file × tracker-id ratchet), so the edit owes the baseline nothing. The gate is green.
    • Governed / size / Clause-②: not governed, no packages/spec/src/** in the diff, the declaration is not yes ⇒ no at-tier landing gate.
    • CI on 40bf21c9: 31 success and 3 rostered skips (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)), with 0 pending and 0 failed.

    Out-of-scope, one row each (noted, not filed; comment drift, not class a/b/c)

    • packages/core/src/security/resolve-authz-context.ts: the §6d comment and the hasPlatformAdminStanding docblock still say the rung derives from the unscoped grant "and nothing else". That file is read-only in this lane.
    • packages/qa/dogfood/test/authz-conformance.matrix.ts:178 note, same drift.
    • packages/runtime/src/domains/automation-run-lifecycle-door.test.ts:117 (OPERATOR_CTX doc comment), same drift.
  4. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    LANDED — b7b6cdd63f521c16e4c62f4671502807de827cb7 (PR #20074), verified by CONTENT on origin/main

    domain:cli execution PM seat #6024 · session session_01TnPAC1UsTGfHPXVUCL6iLn · 2026-09-25T02:31Z

    probe before (4502bfb5, the merge's parent) origin/main
    const RUN_LIFECYCLE_DENY_MESSAGE = in domains/automation.ts 1 0
    function runLifecycleDenyMessage in the same file 0 1
    the old remedy text (the unscoped \admin_full_access` grant, ADR-0068 D2)` 1 0
    untouched control: RUN_LIFECYCLE_DENY_CODE = 'PERMISSION_DENIED' 1 1
    automation-run-lifecycle-deny-remedy.test.ts in the tree (git ls-tree origin/main) — 1
    activation-gate.ts: "derived from the unscoped-grant evidence and nothing else" — 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions