Skip to content

Commit 7d846b5

Browse files
fix(runtime): a flow started through the REST or action door carries its caller's posture rung beside their positions and permission sets (#28167)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DSjjU2t6tHxnYfSeVjAtdGr)_ domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Fixes #28099 Clause-②: yes (widening) — `AutomationContext` (`@objectstack/spec/contracts`) gains one optional member, `posture`, typed `ExecutionContext['posture']`; the REST trigger door and the action door fill it from the request's resolved execution context, and a run that carries it is served what that rung admits. Metadata-protocol: none ## 维护者速读 - 改了什么:经 REST 触发门(`buildAutomationContext`)和动作门(`dispatchFlowAction`)启动的流程,现在把调用者请求上已解析的姿态等级(`ec.posture`)带进运行,和 positions、permissions 一起带。`AutomationContext` 加一个可选成员 `posture`。 - 为什么改:两个门以前只带 positions/permissions/租户/语言。运行带着自己的 grants,运行准备不再重新解析,所以运行没有 rung。平台管理员手动跑的流程只读到组织行,读不到部署级账本行(收窄,不是泄露)。#28070 修了引擎解析的那一半,这是门这一半。 - 风险与回滚:只转发调用者自己的 rung,原样转发,不从流程作者推导,不抬高,没有就不补。请求体里的 `posture` 键仍是流程参数,不能伪造。门只带了 rung、没带 grants 时,运行准备自己解析 grants,并改用那次解析的 rung(没有就删掉)。回滚即 revert,无数据迁移。 - 先看:`packages/runtime/src/domains/automation.ts` 与 `action-execution.ts` 各一行;`run-resume.ts` 的 `seedRunGrants` 一处;`automation-service.ts` 新成员的文档。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/flow-run-posture.test.ts`(预期 7 passed),以及 `pnpm --filter @objectstack/service-automation exec vitest run src/runas-user-posture-rung.integration.test.ts`(预期 20 passed)。 - 需要你做的:无,席位落地(欠一次契约复审,席位安排)。 ## What changes - **`packages/spec/src/contracts/automation-service.ts`.** `AutomationContext.posture?: ExecutionContext['posture']`. It is type-only and needs no new import (`ExecutionContext` is already imported as a type). It is documented beside `positions` / `permissions`: who sets it (the two doors), that it is the caller's own rung as-is, how run setup treats it, and that a missing rung narrows. - **`packages/runtime/src/domains/automation.ts`** (`buildAutomationContext`, which serves `POST /api/v1/automation/:name/trigger`, the legacy trigger route and a declared `type: 'flow'` endpoint). One line copies `ec.posture` when it is a non-empty string, the way `ec.locale` is copied. It is read off the execution context only: a body key named `posture` stays a flow param. - **`packages/runtime/src/action-execution.ts`** (`dispatchFlowAction`, REST `/actions` and MCP `run_action`). The same copy, in the context handed to `automation.execute`. - **`packages/services/service-automation/src/run-resume.ts`** (`seedRunGrants`, one hunk). When run setup resolves the grants itself (the context carries no `permissions`), the rung of that resolution replaces a forwarded one, or the forwarded one is deleted when the resolution has none. A door forwards positions and permission sets only when they are non-empty, so a rung can arrive alone. Without this hunk it would stand beside grants it was not derived from. A context that carries its grants keeps its rung as forwarded: setup neither re-derives nor raises it. The engine already carried a context-supplied rung to the data context (`resolveRunDataContext` reads it through `runPosture`, #28070), so nothing else in the package changes. - **`content/docs/permissions/record-view-auditing.mdx`.** The sentence from #28098 that said a door-started run "is not, yet" served the deployment-level rows now says it carries its caller's posture and is served them the same way. - **Tests.** `packages/runtime/src/flow-run-posture.test.ts` (new, the producer half). `runas-user-posture-rung.integration.test.ts` gains five envelope cells and three real-stack cells (the consumer half). - **`.changeset/28099-door-runs-carry-posture-rung.md`.** Patch for `@objectstack/spec`, `@objectstack/runtime` and `@objectstack/service-automation`. ## Why Triage `6523377748`: 「The REST and action doors forward the caller's own posture rung beside their positions and permission sets. It is never derived from the flow's author, and never raised.」 The claim declares one optional `AutomationContext` member and owes `Clause-②: yes (widening)` and a contract review. ### The measurement (before the fix, at `origin/main` `47a8b588`) The new pins were run against the unchanged source. - `flow-run-posture.test.ts`: **5 red, 2 green**. `buildAutomationContext` given an execution context with `posture: 'PLATFORM_ADMIN'` returned a context with no `posture` key. Both trigger routes handed the engine `posture: undefined`, and so did `dispatchFlowAction`. The flat-body cell was red because the door forwarded no rung at all. The two absence cells were green, as expected. - `runas-user-posture-rung.integration.test.ts`, real stack: the door-shaped envelope for the platform administrator (their own resolved positions and permission sets, no rung, which is what both doors built) ran `qa_ledger_read` and was **served the organization row only**. The same administrator's request is served every deployment-level row. That cell is kept as a CONTROL. - The engine side was already able to carry a forwarded rung on `main`: the envelope cells for a carried rung were green before the fix. Only the cell "a forwarded rung without forwarded grants is dropped when the resolution answers none" was red, which is this PR's `run-resume.ts` hunk. ### Clause-② One optional member is added to an exported contract interface. No existing member changes, no producer is required to set it, and a consumer that ignores it behaves as before. So this is `yes (widening)`. Its only readers are the run's data context (`runPosture` → `resolveRunDataContext`), which `#28070` already wired. ## Verification Head `0c325aab` (one commit `bd3ac11c` plus a plain merge of `origin/main` after PR #28110 landed). - **Pins.** `flow-run-posture.test.ts` covers both doors: the trigger door carries the rung beside the grants, both trigger routes hand it to the engine, the action door carries it, a request with no rung (or an anonymous one) adds no key, and a body cannot name a rung. That is 7 passed. `runas-user-posture-rung.integration.test.ts` has 20 passed. The envelope cells: a door-started run carries the forwarded `PLATFORM_ADMIN` beside the forwarded grants; a forwarded `MEMBER` is not raised to the resolver's `PLATFORM_ADMIN`; no forwarded rung gives no rung; a rung forwarded without grants is replaced by the resolved one, or dropped when the resolution has none. The real-stack cells: the administrator's door-started run is served every deployment-level row, as their request is; a member's door-started run is served the organization row only, as their request is; plus the CONTROL above. The #28070 cells (an engine-resolved run) are unchanged and green. - **Ablations**, each through `scripts/ablation-replace.mjs`, each restored to HEAD: 1. Trigger door copy removed: 4 red. 2. Trigger door defaults a missing rung to `MEMBER`: 2 red (the absence cell and the body cell). 3. Trigger door reads `ctxBody.posture` first: 2 red. 4. Action door copy removed: 1 red. 5. Action door defaults a missing rung: 1 red. 6. `seedRunGrants`'s `else delete` removed: 1 red. 7. `seedRunGrants` seeds the resolver's rung even over carried grants (re-derivation): 5 red. 8. `seedRunGrants` seeds no resolved rung (#28070's case): 5 red. This shows the engine-resolved pins still bind. - **Typecheck.** `pnpm --filter PKG typecheck` exits 0 for `@objectstack/spec`, `@objectstack/runtime` and `@objectstack/service-automation`. The runtime one needs `@objectstack/driver-turso`'s dist in a fresh worktree. - **Suites.** Before the merge: runtime 488 files / 8832 tests, service-automation 229 / 2899, spec `src/contracts` 50 / 469, all exit 0. After the merge: runtime 488 / 8832, service-automation 230 / 2912, and the three typechecks, all exit 0. - **Gates.** `dispatch-gates --commands` derived 108 families, and the list is the same after the merge. All 108 exit 0. `check:skill-examples` and `check:dual-build-cjs-loads` first exited 3 (prerequisite) and passed after a whole-tree build. `dispatch-gates --ran`: 108 run, 0 NOT-MEASURED. `pnpm lint` exit 0. `pnpm check:route-envelope` exit 0. `check-commit-card-trailers --range origin/main..HEAD` exit 0. ## Acceptance notes - `SeededRunContext.posture` in `service-automation/src/runtime-identity.ts` now restates the member it inherits from `AutomationContext` (same type, `AuthzPosture`). It is left as is because the file is outside the claimed surface. A later touch of that file can drop it. - `content/docs/automation/flows.mdx` (the trigger route's **Identity** paragraph) lists the forwarded identity as "user, positions, permissions, tenant". The list is incomplete but not false. It is outside the claimed surface, so I did not edit it. - Skills-docs: `grep -rn -i "posture" skills/objectstack-automation` finds no text that says which facts a door forwards, so nothing in skills changes. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (One type-only interface member.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (None move for an interface member: the spec gates and the spec build leave the tree clean.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (The `维护者速读` block is the maintainer's summary, as on the lane's other PRs.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4czNtyUvgvgGC1mdxrU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 35f2055 commit 7d846b5

9 files changed

Lines changed: 318 additions & 4 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/runtime': patch
4+
'@objectstack/service-automation': patch
5+
---
6+
7+
fix(runtime): a flow started through the REST or action door carries its caller's posture rung beside their positions and permission sets, so a platform administrator's manual run is served the ledger's deployment-level rows their request is served (#28099)
8+
9+
Clause-②: yes (widening) — `AutomationContext` (`@objectstack/spec/contracts`) gains one optional member, `posture` (typed `ExecutionContext['posture']`); the REST trigger door and the action door fill it from the request's resolved execution context, and a run carrying it is served what that rung admits.
10+
Metadata-protocol: none
11+
12+
A request carries the posture rung its user resolves to (`ExecutionContext.posture`, ADR-0095 D2/D3). Since #28070 a `runAs: 'user'` run whose grants the engine resolves (a record-change run, the segment after an approval decision) carries that rung too. A run started through a door did not: the trigger door (`buildAutomationContext`, `POST /api/v1/automation/:name/trigger`, the legacy trigger route and a declared `type: 'flow'` endpoint) and the action door (`dispatchFlowAction`, REST `/actions` and MCP `run_action`) forward the caller's positions and permission sets, so run setup does not re-resolve them, and they dropped the rung. A platform administrator's manually-run flow was then served the organization's ledger rows only. That failed closed (fewer rows, never more).
13+
14+
- **Now.** Both doors copy the request's own `ec.posture` onto the run context, as they copy its positions, permission sets, organization and locale. Run setup keeps it beside the forwarded grants and does not re-derive it, and every data node carries it to the data engine. The platform administrator's door-started run is served the deployment-level rows, as their request is.
15+
- **The caller's own rung, as-is.** It is read off the execution context only (a request body key named `posture` stays a flow param), never derived from the flow's author, never raised, and never filled in when the request carries none: such a run carries no rung, as before.
16+
- **Only beside the grants it came from.** A door forwards positions and permission sets only when they are non-empty, so a rung can arrive without them. Run setup then drops the forwarded rung and resolves the grants itself, taking the rung off that same resolution; a run whose grants stay unresolved (the resolver has no rung, rejects, answers nothing, or is not wired) carries none.
17+
- **Docs.** The trigger route's *Identity* paragraph in `flows.mdx` now names every fact the doors forward (user, positions, permissions, tenant, posture rung, language), and `record-view-auditing.mdx` no longer says a door-started run is "not, yet" served the deployment-level rows.
18+
- **Unchanged.** A run whose grants the engine resolves (#28070), the approval decision switch, and a `runAs: 'system'` run.

‎content/docs/automation/flows.mdx‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,9 +2515,13 @@ unbound unless its declaration carries a
25152515
[`defaultValue`](#defaultvalue--declaring-a-variable-is-not-the-same-as-binding-it);
25162516
a param you send explicitly always wins, `false` and `null` included.
25172517

2518-
**Identity.** The caller's identity (user, positions, permissions, tenant) is
2519-
forwarded into the run, so a `runAs: 'user'` flow executes under that caller's
2520-
row-level security. See [API Authentication](/docs/api#authentication) for
2518+
**Identity.** The caller's identity (user, positions, permissions, tenant,
2519+
posture rung) and language are forwarded into the run, as the caller's request
2520+
resolved them, so a `runAs: 'user'` flow executes under that caller's
2521+
row-level security and reads what the caller's own request reads. The rung is
2522+
the caller's own, never the flow author's and never raised; a request without
2523+
one starts a run without one. The run's setup adds the user's email and
2524+
organization peers. See [API Authentication](/docs/api#authentication) for
25212525
credentials.
25222526

25232527
<Callout type="warn">

‎content/docs/permissions/record-view-auditing.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ reader is served one, this capability's holders included. A `runAs: 'user'`
228228
flow whose grants the engine resolves for its user (a record-change run, or the
229229
segment after an approval decision) carries that user's posture, so one run as
230230
the platform administrator is served these rows as their request is; a run
231-
started through the REST or action door is not, yet.
231+
started through the REST or action door carries the posture its caller's
232+
request carries, and is served them the same way.
232233
Rows carry the
233234
ADR-0057 `audit` lifecycle class: retained hot for
234235
90 days, then archived for seven years where an `archive` datasource is

‎packages/runtime/src/action-execution.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,10 @@ export async function dispatchFlowAction(deps: ActionExecutionDeps,
20772077
...(Array.isArray(ec?.positions) && ec.positions.length ? { positions: ec.positions } : {}),
20782078
...(Array.isArray(ec?.permissions) && ec.permissions.length ? { permissions: ec.permissions } : {}),
20792079
...(ec?.tenantId ? { tenantId: ec.tenantId } : {}),
2080+
// [#28099] The caller's already-resolved posture rung, the trigger
2081+
// door's twin (`buildAutomationContext`): never re-derived or raised,
2082+
// absent when the request carries none. See `AutomationContext.posture`.
2083+
...(typeof ec?.posture === 'string' && ec.posture.length > 0 ? { posture: ec.posture } : {}),
20802084
// [#22450] The caller's already-resolved language, the trigger door's
20812085
// twin (`buildAutomationContext`): a refused run's message renders in
20822086
// it. See `AutomationContext.locale`.

‎packages/runtime/src/domains/automation.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ export function buildAutomationContext(body: any, context: HttpProtocolContext):
204204
if (Array.isArray(ec?.positions) && ec.positions.length) automationContext.positions = ec.positions;
205205
if (Array.isArray(ec?.permissions) && ec.permissions.length) automationContext.permissions = ec.permissions;
206206
if (ec?.tenantId) automationContext.tenantId = ec.tenantId;
207+
// [#28099] The caller's posture rung, already resolved beside the grants
208+
// above — forwarded, never re-derived or raised, absent when the request
209+
// carries none. The run carries those grants, so its setup does not
210+
// re-resolve them and this is the only way it gets one.
211+
// `AutomationContext.posture` names its readers.
212+
if (typeof ec?.posture === 'string' && ec.posture.length > 0) automationContext.posture = ec.posture;
207213
// [#22450] The caller's language, already resolved (the request's
208214
// `Accept-Language` first, then the `localization` settings) — forwarded,
209215
// never re-derived, so a refused run's message renders in the language of
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The two flow doors forward the caller's posture rung —
5+
* `AutomationContext.posture` (#28099, the door half of #28070).
6+
*
7+
* A request carries the rung its user resolves to (`ExecutionContext.posture`,
8+
* ADR-0095 D2/D3), derived once by `resolveAuthzContext`. The doors already
9+
* hold it beside the caller's positions and permission sets, which they
10+
* forward; a run that carries those grants is not re-resolved, so a rung the
11+
* door drops is a rung the run never has. The door forwards the caller's OWN
12+
* rung as-is — ⛔ never derived from the flow's author, never raised, never
13+
* filled in when the request carries none.
14+
*
15+
* What this file pins — the PRODUCER half. The consumer half, on the real
16+
* automation engine, is `runas-user-posture-rung.integration.test.ts` in
17+
* `@objectstack/service-automation`, which runs a door-shaped context (carried
18+
* grants plus a carried rung) to a data node.
19+
*
20+
* 1. The trigger door (`buildAutomationContext`) forwards `ec.posture`, and
21+
* both of its routes hand it to the engine.
22+
* 2. The action door (`dispatchFlowAction`) forwards it.
23+
* 3. A request with no rung (anonymous, or a principal the resolver gave
24+
* none) adds no key — absent, never a default rung.
25+
* 4. The request body cannot name one: `buildAutomationContext` reads the
26+
* rung off the execution context only, and a body key named `posture`
27+
* stays a flow param.
28+
*/
29+
30+
import { describe, it, expect, vi } from 'vitest';
31+
32+
import { HttpDispatcher } from './http-dispatcher.js';
33+
import { dispatchFlowAction, loadActionSubjectRecord } from './action-execution.js';
34+
import { buildAutomationContext } from './domains/automation.js';
35+
36+
const FLOW_NAME = 'quote_generation';
37+
38+
const QUOTE = {
39+
name: 'generate_quote',
40+
label: 'Generate quote',
41+
objectName: 'crm_opportunity',
42+
type: 'flow',
43+
target: FLOW_NAME,
44+
};
45+
46+
function ec(posture?: string) {
47+
return {
48+
userId: 'usr_1', tenantId: 'org_1', positions: ['platform_admin'], permissions: ['admin_full_access'], systemPermissions: [],
49+
...(posture !== undefined ? { posture } : {}),
50+
};
51+
}
52+
53+
/** An automation double that records the context it is handed. */
54+
function makeAutomation() {
55+
const execute = vi.fn(async (_flow: string, _context?: any) => ({ success: true, output: {} }));
56+
const getFlow = vi.fn(async (name: string) => (name === FLOW_NAME ? { name } : null));
57+
return { execute, getFlow };
58+
}
59+
const contextOf = (automation: { execute: any }) => automation.execute.mock.calls[0]?.[1];
60+
const flowDeps = (automation: any): any => ({
61+
resolveService: async (_ctx: any, name: string) => (name === 'automation' ? automation : undefined),
62+
});
63+
const REQUEST: any = { request: {}, environmentId: 'platform' };
64+
65+
describe('[#28099] the trigger door forwards the caller\'s posture rung', () => {
66+
it('`buildAutomationContext` carries `executionContext.posture` as `posture`, beside the grants it carries', () => {
67+
const ctx: any = buildAutomationContext(
68+
{ recordId: 'opp_1', objectName: 'crm_opportunity', params: {} },
69+
{ request: {}, executionContext: ec('PLATFORM_ADMIN') } as any,
70+
);
71+
expect(ctx).toMatchObject({ permissions: ['admin_full_access'], posture: 'PLATFORM_ADMIN' });
72+
});
73+
74+
it('a request with no rung adds no key: absent, never a default rung', () => {
75+
const ctx: any = buildAutomationContext(
76+
{ recordId: 'opp_1', objectName: 'crm_opportunity', params: {} },
77+
{ request: {}, executionContext: ec() } as any,
78+
);
79+
expect('posture' in ctx).toBe(false);
80+
const anonymous: any = buildAutomationContext({ params: {} }, { request: {} } as any);
81+
expect('posture' in anonymous).toBe(false);
82+
});
83+
84+
it('the body cannot name a rung: a flat `posture` key stays a flow param, a wrapped one never leaves `params`', () => {
85+
const flat: any = buildAutomationContext(
86+
{ posture: 'PLATFORM_ADMIN' },
87+
{ request: {}, executionContext: ec('MEMBER') } as any,
88+
);
89+
expect(flat.posture).toBe('MEMBER');
90+
expect(flat.params.posture).toBe('PLATFORM_ADMIN');
91+
const noRung: any = buildAutomationContext(
92+
{ posture: 'PLATFORM_ADMIN', params: { posture: 'PLATFORM_ADMIN' } },
93+
{ request: {}, executionContext: ec() } as any,
94+
);
95+
expect('posture' in noRung).toBe(false);
96+
});
97+
98+
for (const route of [
99+
{ label: 'POST /:name/trigger', path: `/${FLOW_NAME}/trigger` },
100+
{ label: 'legacy POST /trigger/:name', path: `/trigger/${FLOW_NAME}` },
101+
]) {
102+
it(`${route.label} hands the engine the rung`, async () => {
103+
const automation = makeAutomation();
104+
const resolve = (n: string) => (n === 'automation' ? automation : undefined);
105+
const kernel: any = { getService: resolve, getServiceAsync: async (n: string) => resolve(n), context: { getService: resolve } };
106+
await new HttpDispatcher(kernel).handleAutomation(
107+
route.path, 'POST',
108+
{ recordId: 'opp_1', objectName: 'crm_opportunity', params: {} },
109+
{ request: {}, executionContext: ec('PLATFORM_ADMIN') } as any,
110+
);
111+
expect(automation.execute).toHaveBeenCalledTimes(1);
112+
expect(contextOf(automation).posture).toBe('PLATFORM_ADMIN');
113+
});
114+
}
115+
});
116+
117+
describe('[#28099] the action door forwards the caller\'s posture rung', () => {
118+
it('`dispatchFlowAction` carries `ec.posture` as `posture`, beside the grants it carries', async () => {
119+
const automation = makeAutomation();
120+
const subject = await loadActionSubjectRecord('crm_opportunity', 'opp_1', async () => ({ record: { id: 'opp_1' } }));
121+
await dispatchFlowAction(flowDeps(automation), REQUEST, QUOTE, {
122+
objectName: 'crm_opportunity', subject, params: {}, recordId: 'opp_1', ec: ec('PLATFORM_ADMIN'), envId: 'platform',
123+
});
124+
expect(contextOf(automation)).toMatchObject({ permissions: ['admin_full_access'], posture: 'PLATFORM_ADMIN' });
125+
});
126+
127+
it('a request with no rung adds no key: absent, never a default rung', async () => {
128+
const automation = makeAutomation();
129+
const subject = await loadActionSubjectRecord('crm_opportunity', 'opp_1', async () => ({ record: { id: 'opp_1' } }));
130+
await dispatchFlowAction(flowDeps(automation), REQUEST, QUOTE, {
131+
objectName: 'crm_opportunity', subject, params: {}, recordId: 'opp_1', ec: ec(), envId: 'platform',
132+
});
133+
expect('posture' in contextOf(automation)).toBe(false);
134+
});
135+
});

‎packages/services/service-automation/src/run-resume.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ export async function seedRunGrants(
205205
const needsGrants = !Array.isArray(runContext.permissions);
206206
const needsPeers = runOrgUserIds(runContext) === undefined;
207207
const needsEmail = !runContext.email;
208+
// A rung a door forwarded without grants (#28099) is never kept beside
209+
// grants it was not derived from: the resolution below re-seeds the rung
210+
// with the grants, and a run whose grants stay unresolved (the resolver
211+
// throws, answers nothing, or is not wired) carries none.
212+
if (needsGrants) delete (runContext as SeededRunContext).posture;
208213
if (
209214
runContext.runAs !== 'system' &&
210215
runContext.userId &&

0 commit comments

Comments
 (0)