Repository navigation
Commit fdfdd7e
fix(service-automation, objectql): runAs refusal and run-setup warning texts hold on both kernels (#22390)
Fixes #22362
Clause-②: no
`domain:services` seat 1, branch
`claude/issue-22362-unscoped-run-strings`, dispatched under the claim
`6071780429`, executing triage `6070703882` (unlocked at `6071449995`).
This is the runtime-strings pass of the pre-D5 family. The other members
are not touched here: `packages/spec` is #22302, comments and docstrings
were #22345 (PR #22357), and the skills surface is #22372.
## What this does
Three author-facing runtime strings told a flow or hook author that a
`runAs: 'user'` data operation with no trigger user "would execute
UNSCOPED (elevated, RLS-bypassing)". That counterfactual holds only on a
kernel with no security plugin. Since ADR-0096 D5, a kernel with
`plugin-security` refuses an operation that carries no principal:
`security-plugin.ts:2655` throws `403 PERMISSION_DENIED` for every verb
when `isPrincipalLessContext` (`:383`) holds. D5 landed in `a3bcbcf3ca`;
`git merge-base --is-ancestor a3bcbcf 16096e8` exits 0.
Each string now states what such an operation meets on both kernels, in
the triage's words: refused by the security plugin where one is
composed, unscoped where none is. Each remedy is kept word for word.
| # | Site | Lane |
|:-|:-|:-|
| A | `service-automation/src/runtime-identity.ts:87-92`, the
`UnscopedRunDataAccessError` message | `domain:services` |
| B | `service-automation/src/engine.ts:6133-6140`, the `[runAs]`
warning `resolveRunContext` logs at run setup | `domain:services` |
| C | `objectql/src/hook-run-as.ts:118-125`, the
`HookUnscopedDataAccessError` message | `domain:engine` (strings only,
declared below) |
The triage named A and B. The enumeration found C: the hook-side twin of
A, whose docblock says its "wording mirrors" A's. It states the same
counterfactual. `withRunAs('user')` hands a hook with no `userId` an
`UnscopedHookApi` instead of `{ ...triggering context, isSystem: false
}`, and that context would carry no principal.
### Before and after (as the runtime prints them; `WHERE` and `FLOW` are
placeholders)
**A, before:** `[runAs] refusing a data operation (WHERE): this run's
effective runAs is 'user' but no trigger user could be resolved, so the
operation would execute UNSCOPED (elevated, RLS-bypassing) rather than
restricted to a user. Declare `runAs: 'system'` on the flow to make the
elevation explicit and intended, or arrange for the trigger to supply a
user (a write made with a system context carries none). (ADR-0049)`
**A, after:** `[runAs] refusing a data operation (WHERE): this run's
effective runAs is 'user' but no trigger user could be resolved, so the
operation cannot be restricted to a user. Without one it would carry no
principal: refused by the security plugin where one is composed,
unscoped where none is. Declare `runAs: 'system'` on the flow to make
the elevation explicit and intended, or arrange for the trigger to
supply a user (a write made with a system context carries none).
(ADR-0049)`
**B, before:** `[runAs] flow 'FLOW' executes with runAs:'user' but its
trigger resolved no user — its data operations will be REFUSED. Running
them would execute UNSCOPED (elevated, RLS-bypassing) rather than
restricted, which is the fail-open ADR-0049 forbids. Declare
runAs:'system' to make the elevation explicit and intended, or arrange
for the trigger to supply a user. Note a user-less trigger is NOT only a
schedule: a record-change flow fired by a system write carries no user
either (ADR-0049).`
**B, after:** `[runAs] flow 'FLOW' executes with runAs:'user' but its
trigger resolved no user — its data operations will be REFUSED. Without
a user they would carry no principal: refused by the security plugin
where one is composed, unscoped where none is (the fail-open ADR-0049
forbids). Declare runAs:'system' to make the elevation explicit and
intended, or arrange for the trigger to supply a user. Note a user-less
trigger is NOT only a schedule: a record-change flow fired by a system
write carries no user either (ADR-0049).`
**C, before:** `[runAs] refusing a data operation (WHERE): this hook's
runAs is 'user' but no trigger user could be resolved, so the operation
would execute UNSCOPED (elevated, RLS-bypassing) rather than restricted
to a user. Declare `runAs: 'system'` on the hook to make the elevation
explicit and intended, or arrange for the trigger to supply a user (a
write made with a system context carries none). Branch on `code ===
'HOOK_UNSCOPED_DATA_ACCESS'` (ADR-0112) to detect this. (ADR-0049)`
**C, after:** `[runAs] refusing a data operation (WHERE): this hook's
runAs is 'user' but no trigger user could be resolved, so the operation
cannot be restricted to a user. Without one it would carry no principal:
refused by the security plugin where one is composed, unscoped where
none is. Declare `runAs: 'system'` on the hook to make the elevation
explicit and intended, or arrange for the trigger to supply a user (a
write made with a system context carries none). Branch on `code ===
'HOOK_UNSCOPED_DATA_ACCESS'` (ADR-0112) to detect this. (ADR-0049)`
A and C after are read back from the rebuilt `dist` (`new
UnscopedRunDataAccessError(...)` and `new
HookUnscopedDataAccessError(...)` through each package's `exports`). B
is the source template. Codes, class names, the `403` status on C and
the order of every refusal are unchanged.
### No non-string token moved
Each changed `.ts` file was projected through
`scripts/js-comment-mask.mjs` `scanSource`: comment bytes and literal
CONTENT bytes blanked, literal delimiters and `${...}` interpolation
code kept, whitespace runs collapsed. All 6 projections are
byte-identical to base `16096e8d7b`, and so are the line counts:
| File | Lines (base → head) | Projection sha256 (first 16), base = head
|
|:-|:-|:-|
| `objectql/src/hook-run-as.ts` | 208 → 208 | `5df1af062d3f07f7` |
| `objectql/src/hook-run-as.test.ts` | 426 → 426 | `f42dd65d4b606a02` |
| `service-automation/src/runtime-identity.ts` | 335 → 335 |
`7b34e026b883b71e` |
| `service-automation/src/engine.ts` | 12888 → 12888 |
`dd43f6d3d3fd367c` |
| `service-automation/src/builtin/crud-runas.test.ts` | 502 → 502 |
`22f4d9aa2de782a3` |
| `trigger-schedule/src/schedule-runas-e2e.test.ts` | 142 → 142 |
`446edc7895f0eb06` |
To keep that true, each message keeps the number of concatenated
template pieces it had. Control leg (in memory, no disk write): the same
projection DIFFERS when the warning's `this.logger.warn(` becomes
`this.logger.error(` (anchor hit 1), and when one extra empty piece is
concatenated. The only other file is the changeset.
## The enumeration (the pin)
**Method.** A comment-level `git grep` cannot tell a string from a
comment, and #22345 already closed the comments. So the sweep reads only
string, template and regex literal content: each tracked
`.ts/.tsx/.mts/.cts/.js/.jsx/.mjs/.cjs` file outside `packages/spec`
(6,420 files) is projected through `scanSource` with everything that is
not literal content blanked. It reports every literal line naming a
user-less or principal-less run or context (SUBJECT) with a claim term
(CLAIM) within 3 lines. Run it from the repository root:
```js
// node --input-type=module, with this file on stdin, from the repository root
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { scanSource } from './scripts/js-comment-mask.mjs';
const SUBJECT = /principal-?less|no principal|without (a |any )?principal|user-?less|no (trigger )?user\b|without (a |any )?user\b|no identity|identity-?less|no (execution ?)?context|context-?less|no session|anonymous|provenance[- ]only/i;
const CLAIM = /unscoped|fall(s|ing)?[- ]open|fell[- ]open|fail(s|ed|ing)?[- ]open|\badmit(s|ted|ting)?\b|\bskip(s|ped|ping)?\b|bypass|straight (through|to)|\belevat|unrestricted|full access|unfiltered|wave[sd]? through|hand(ed|s)?[- ](off|through)|pass(es|ed)? through|not scoped/i;
let n = 0;
for (const f of execFileSync('git', ['ls-files'], { encoding: 'utf8', maxBuffer: 2 ** 28 }).split('\n')) {
if (!/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(f) || f.startsWith('packages/spec/')) continue;
const text = readFileSync(f, 'utf8');
const { literal, interpolation } = scanSource(text);
const lines = text.split('').map((c, k) => (c === '\n' || (literal[k] && !interpolation[k]) ? c : ' ')).join('').split('\n');
lines.forEach((l, i) => {
if (SUBJECT.test(l) && CLAIM.test(lines.slice(Math.max(0, i - 3), i + 4).join(' '))) {
n++;
console.log(`${f}:${i + 1}: ${l.replace(/\s+/g, ' ').trim().slice(0, 160)}`);
}
});
}
console.error(`${n} line(s)`);
```
It gives **92 lines at base `16096e8d7b`** and 94 at head `7b2cdf7255`.
The two extra lines are the new "would carry no principal" sentences of
B and C, which name a principal-less operation and are in the reworded
class.
### The 92 base lines, each with its class
**Reworded here: false on a kernel with `plugin-security` (5 lines, 3
strings)**
- `objectql/src/hook-run-as.ts:119` (C)
- `service-automation/src/engine.ts:6134, :6138, :6139` (B; `:6138-6139`
are its "Note a user-less trigger is NOT only a schedule" tail, which is
true and kept)
- `service-automation/src/runtime-identity.ts:89` (A)
**Production, true on both kernels, left (7)**
- `lint/src/lint-flow-patterns.ts:1612`: the `flow-runas-unscoped`
finding says the data node "will be REFUSED at run time", and its hint
says "the runtime refuses the operation rather than run it unscoped".
Both state the refusal, which holds on both kernels; neither says what
the middleware would do.
- `runtime/src/action-execution.ts:2369`: an explicit system elevation
(`isSystem`) of an action body.
- `runtime/src/route-ledger.ts:435, :441, :445, :459`: fail-closed on an
absent `executionContext`; the anonymous floor answers 401 first.
- `service-knowledge/src/knowledge-service.ts:340`: the knowledge
service's own corpus filter fails closed ("rather than searching the
whole corpus unscoped"). That counterfactual is the knowledge service's
own, not the data middleware's.
**Production, another subject, left (2)**:
`cloud-connection/src/cloud-connection-route-ledger.ts:262` (an
anonymous browser surface of the catalog proxy) and
`metadata-core/src/contract-suite.ts:189` (an "anonymous exception" in a
contract suite).
**Repository tooling, another subject, left (2)**:
`scripts/pm/check-half-states.mjs:34597` and
`scripts/tenant-audit-census.mjs:2813`.
**Test files: test titles, assertion messages and fixture strings, left
(76).** They are not error messages, warnings or logs a runtime prints,
and none ships (`files` is `dist`, `README.md`, `CHANGELOG.md` in every
package touched). By what they say:
- *State the refusal or name the D5 denial (true):*
`objectql/src/hook-run-as.test.ts:190`;
`plugin-auth/src/auth-manager.org-slug-guard-system-context.test.ts:156,
:160`; `plugin-security/src/authored-row-write-verdict.test.ts:525,
:541` (explicit elevation);
`plugin-security/src/controlled-by-parent-master-widener.test.ts:583`;
`plugin-security/src/public-form-grant-masking.test.ts:233, :234`
(negation); `plugin-sharing/src/share-link-service.test.ts:580`;
`qa/dogfood/test/declarative-endpoint-anonymous-guest.dogfood.test.ts:283`;
`runtime/src/sandbox/hook-run-as.integration.test.ts:198`;
`service-automation/src/builtin/crud-runas.test.ts:289, :290, :337`;
`trigger-record-change/src/record-change-integration.test.ts:406`.
- *Assertion messages that name the failure shape the test exists to
catch:* `qa/dogfood/test/flow-runas-schedule.dogfood.test.ts:128, :133,
:140, :143`;
`trigger-record-change/src/record-change-integration.test.ts:448`;
`service-automation/src/runas-grant-resolution.integration.test.ts:102`;
`runtime/src/dispatcher-plugin.endpoint-fallback.integration.test.ts:571`;
`service-automation/src/builtin/crud-runas.test.ts:91, :118`.
- *Test titles that use the pre-D5 word for the mechanism (see
Acceptance notes):* `plugin-security/src/security-plugin.test.ts:2317,
:2571, :2674` and `can-write-object-admission.test.ts:640` ("gate is
before the fall-open");
`trigger-schedule/src/schedule-runas-e2e.test.ts:95, :96` ("user-less
runAs fail-open", "runs the flow UNSCOPED").
- *A guard's or hook's own carve-out for a context-less call (another
subject: the guard skips itself, not the data middleware):*
`plugin-audit/src/comment-access-hooks.test.ts:178, :187`;
`plugin-audit/src/comment-read-visibility.test.ts:201`;
`plugin-auth/src/identity-write-guard.test.ts:84, :172`;
`plugin-security/src/system-write-guard.test.ts:82`;
`service-storage/src/attachment-access-hooks.test.ts:139, :152, :678,
:818`; `service-storage/src/attachment-read-visibility.test.ts:253`.
- *Another subject:* `qa/dogfood/test/authz-conformance.matrix.ts` (17
lines: `:202, :203, :243-:247, :285, :286, :302, :309, :320, :324, :361,
:460-:462`, the anonymous HTTP posture rows);
`driver-sql/src/sql-driver-tenant-scope.test.ts:351` and
`driver-sqlite-wasm/src/sqlite-wasm-driver-tenant-scope.test.ts:250`
(driver tenant scope); `lint/src/lint-flow-patterns.test.ts:363` (the
rule's name); `objectql/src/engine-repo-execute-elevation.test.ts:155`;
`plugin-auth/src/audience-posture.test.ts:843`,
`send-verification-email.test.ts:63`, `set-initial-password.test.ts:65`;
`qa/dogfood/test/flow-runas-schedule.dogfood.test.ts:157` (explicit
`runAs:'system'`); `qa/dogfood/test/form-self-auth.dogfood.test.ts:35`;
`rest/src/ui-view-route-identity.measurement.test.ts:343`,
`ui-view-route-tenancy.measurement.test.ts:508`;
`runtime/src/action-body-identity.test.ts:114, :235`;
`runtime/src/dispatcher-plugin.endpoint-fallback.integration.test.ts:577`;
`runtime/src/endpoint-policy.test.ts:272`;
`runtime/src/http-dispatcher.mcp-oauth.test.ts:207`;
`service-automation/src/builtin/crud-runas.test.ts:376` (the predicate's
name); `service-automation/src/runas-attribution-contract.test.ts:244`.
### Claim-word scans the window cannot pair, production only
Each claim term was also scanned alone over the same literal projection
(`unscoped` 330 lines, `bypass` 408, fall/fail-open 236, `admit` 1,069,
subject terms 315). The production lines on this subject that the window
above does not pair:
- `lint/src/lint-flow-patterns.ts:728`: "an unscoped run" names the
refusal class among guard refusals. True.
- `plugin-security/src/security-plugin.ts:401`: the D5 refusal itself.
True.
- `plugin-dev/src/dev-plugin.ts:887, :889`: "plugin-security not
installed — skipping security". True: no security plugin is composed.
- `service-analytics/src/plugin.ts:806-812`: no `admitObjectRead` and no
security service, so analytics queries are "admitted the same way".
True: it fires only where no security service is composed. `:819-821` is
the fail-closed branch.
- `runtime/src/domains/actions.ts:805` and
`metadata-core/src/object-schema-fls-contract.ts:348`: an explicit
`isSystem` elevation. True.
- `service-storage/src/storage-routes.ts:465`: upload routes "accept
anonymous requests" when no session resolver is wired (bare kernel).
Another subject: an HTTP route's session gate, not a data context.
None of these is false on a kernel with `plugin-security`, so there is
no fourth string.
### A re-runnable pin over the result
```
git grep -n -i -E "would execute unscoped|RLS-bypassing\) rather" -- ':!packages/spec/**' ':!**/CHANGELOG.md'
git grep -n -F "refused by the security plugin where one is composed, unscoped where none is" -- ':!packages/spec/**'
```
At base, the first gives 9 lines: A (`runtime-identity.ts:89, :90`), B
(`engine.ts:6135, :6136`) and C (`hook-run-as.ts:120`), plus 4 that are
not runtime strings (see Acceptance notes). At head it gives the same 4
and the changeset's quotation of the old text. The second gives nothing
at base. At head it gives the three strings (`hook-run-as.ts:121`,
`runtime-identity.ts:90`, `engine.ts:6136`), the three moved pins, and
the changeset.
## Pins moved with the wording
| Pin | Before | After |
|:-|:-|:-|
| `service-automation/src/builtin/crud-runas.test.ts:238` |
`toMatch(/UNSCOPED/)` | `toMatch(/refused by the security plugin where
one is composed, unscoped where none is/)` |
| `trigger-schedule/src/schedule-runas-e2e.test.ts:122` |
`toMatch(/UNSCOPED/)` | the same regex |
| `objectql/src/hook-run-as.test.ts:210` | `toContain('UNSCOPED')` |
`toContain('refused by the security plugin where one is composed,
unscoped where none is')` |
The triage named the first two. The third pins C and matched the old
word, so it moves with it. No pin was added; each pin is still one
matcher, now on the clause the triage dictated. The `REFUSED` and
`runAs:'system'` pins beside them are unchanged and still pass.
### Reverse verification: each source-resolved moved pin rejects the old
counterfactual
The fix was committed first (HEAD `7b2cdf7255`). Then `node
scripts/ablation-replace.mjs` ran in WRAP mode: it puts the old text
back on disk, runs the test, and restores from `HEAD` with proof. Both
legs ran under `scripts/pm/os-verify-lock.sh`. The expected direction
was red, and red is what was observed.
- **`objectql/src/hook-run-as.ts`.** The anchor `refused by the security
plugin where one is composed, unscoped where none is.` (1 hit, 1 → 0)
was replaced by `would execute UNSCOPED (elevated, RLS-bypassing) rather
than restricted to a user.` (0 → 1). Blob `26d796cde2d7` →
`e524cf30db66`.
- `vitest run src/hook-run-as.test.ts`: `Tests 1 failed | 13 passed
(14)`. The one failure is the moved pin: `expected '[runAs] refusing a
data operation (ho…' to contain 'refused by the security plugin where
…'`.
- Restored: blob == HEAD (`26d796cde2d7`), and `git diff HEAD` is empty.
- **`service-automation/src/runtime-identity.ts`.** The same anchor and
replacement (1 → 0, 0 → 1). Blob `613932cfc20f` → `8a5b98066be8`.
- `vitest run src/builtin/crud-runas.test.ts`: `Tests 1 failed | 23
passed (24)`. The one failure is the moved pin, in "the refusal names
the fix": `expected '[runAs] refusing a data operation (ob…' to match
/refused by the security plugin where …/`.
- Restored: blob == HEAD (`613932cfc20f`), and `git diff HEAD` is empty.
- After both legs, `git status --porcelain` printed 0 lines and `git
diff HEAD` 0 bytes.
Both test files import their subject from source (`./hook-run-as.js`,
`../runtime-identity.js`), so no build was involved and none is owed.
The third pin (`trigger-schedule`) is different: it reads
`@objectstack/service-automation` through `dist`, and
`KNOWN_UNALIASED_TEST_IMPORTS` lists that pair. It was not ablated. Its
green run reads the rebuilt `dist`, which carries the new clause 2 times
and the old phrase 0 times.
## Changeset
`.changeset/22362-unscoped-run-strings.md`:
`@objectstack/service-automation` `patch` and `@objectstack/objectql`
`patch`, `Clause-②: no`. Both packages ship the moved text: after `turbo
run build`, `service-automation/dist/index.js` and `dist/index.cjs` each
hold the new clause 2 times and `RLS-bypassing) rather` 0 times.
`objectql/dist/index.js` and `dist/index.mjs` hold it 1 time and the old
phrase 0 times. As a control, the unchanged `a write made with a system`
is present 1 time in each. `@objectstack/trigger-schedule` changes a
test only, and its `files` is `dist`, `README.md` and `CHANGELOG.md`, so
it gets no line.
## Cross-lane paths (strings and one test pin; declared for the owning
seat)
- `domain:engine`: `packages/objectql/src/hook-run-as.ts` (string C) and
`packages/objectql/src/hook-run-as.test.ts:210` (its pin).
- `packages/triggers/trigger-schedule` is this lane's.
## Verification (head `7b2cdf7255`)
All builds and tests ran under `scripts/pm/os-verify-lock.sh`, and each
printed `VERDICT command-exit 0`.
**Builds**
- Closure build: `turbo run build
--filter=@objectstack/trigger-schedule...
--filter=@objectstack/service-automation...
--filter=@objectstack/objectql... --concurrency=1` gave `Tasks: 31
successful, 31 total` (19 cached).
- Full build: `turbo run build --filter=!@objectstack/docs
--concurrency=1` gave `Tasks: 72 successful, 72 total` (71 cached).
**Tests**
- `@objectstack/service-automation`, full suite (`vitest run
--maxWorkers=2`): `Test Files 178 passed (178)`, `Tests 2177 passed
(2177)`.
- `@objectstack/trigger-schedule`, full suite: `Test Files 8 passed
(8)`, `Tests 174 passed (174)`.
- `@objectstack/objectql`, the `test` script's project (`vitest run
--project local --maxWorkers=2`): `Test Files 388 passed (388)`, `Tests
7633 passed (7633)`.
- `@objectstack/runtime` `src/sandbox/hook-run-as.integration.test.ts`,
the one other test that reads C (by its code, which C still names):
`Tests 4 passed (4)`.
**Typecheck**
- `service-automation`: `check:test-typecheck: OK … 0 file(s) / 0
error(s)`.
- `objectql`: `check:test-typecheck: OK … 40 file(s) / 234 error(s) / 65
pinned signature(s) held in test-typecheck-debt.json`. The ledger is
unchanged.
- `trigger-schedule`: `tsc --noEmit` exits 0, and its program includes
`schedule-runas-e2e.test.ts` (`--listFilesOnly` count 1).
**ESLint, narrowed to the 6 changed `.ts` files** (`--no-inline-config
--format json`): 6 files, errors=0, warnings=0. The narrowing is a
measurement, on three pieces of evidence:
- The population is the config's `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`
block (`eslint.config.mjs:971`).
- The file count, 6, is read from the JSON output.
- `eslint.config.mjs:327-328` states the config "never enables
type-aware linting (no `parserOptions.project`, no typed
`@typescript-eslint` rules) for ANY file". So this diff cannot move any
untouched file's verdict.
The full `pnpm lint` is CI's.
**Gates**
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 68 commands from the 7 changed paths
at `7b2cdf7255`. All 68 exit 0.
- `--ran` with recorded exit codes: "✓ dispatch-gates --ran: 68 derived
famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero — all
68 recorded an exit code and none of them is 3)".
- Seven were run again after the full build and a history deepening, and
the recorded codes are from those runs. In the first pass,
`check:dual-build-cjs-loads` exited 3 (`PREREQUISITE NOT MET`, 36
packages unbuilt) and `check-engine-split-ratio --days 90` refused on
the shallow clone (exit 2). `check:dts-closure` and
`check:sourcemap-no-sources-content` had swept only the 31 built
packages.
Verdict lines from the gates:
- `check:nul-bytes`: "OK (scanned 10368 text file(s) … no raw ASCII
control bytes)".
- `check:doc-authoring`: "doc authoring guard: … 89492 string(s) read in
1285 parsed source(s) … no growth".
- `check-adr-0087-registration`: "this PR adds no declared-breaking
changeset (1 non-breaking changeset(s) seen)".
- `check-changeset-no-major`: "This diff introduces no `major` bump."
- `check:dual-build-cjs-loads`: "107 published require entry point(s)
across 66 package(s) load; 717 emitted CommonJS file(s) parse".
- `check:dts-closure`: "72 built package(s) swept - 172/172 declared
declaration file(s) present".
- `check-system-context-census`: "OK — 118 elevation read sites in 20
packages".
- `check:test-source-alias`, `check:cross-package-test-inputs`,
`check:published-files` and `check-issue-citations` are green.
**The derivation's stale-tree note.** While the gates ran, `origin/main`
moved 4 commits, to `117d34de3f`. Two gate-input files changed upstream:
`scripts/platform-object-tenancy-census.json` and
`scripts/migrate/overlay-views-to-sys-view-definition.md`. None of the 4
commits touches a file this PR changes, so the merge ref is CI's to
judge.
**Declared narrowings**
- objectql's `repo` vitest project (`test:repo`) and the Dogfood
Regression Gate are left to CI. The diff moves no export, type or code
token (see the projection above), only message text.
## Acceptance notes
- **Test titles that use the pre-D5 word, left.** The card's ruling
covers error, warning and log strings; these are titles.
`plugin-security/src/security-plugin.test.ts:2317, :2571, :2674` and
`can-write-object-admission.test.ts:640` say "(gate is before the
fall-open)". The gates still run before the D5 refusal, so the DENIES
assertions hold. `trigger-schedule/src/schedule-runas-e2e.test.ts:95-96`
say "user-less runAs fail-open" and "runs the flow UNSCOPED
(user-less)". That test runs a stub data executor on a bare
`AutomationEngine`, and what it asserts is the warning. #22345 left the
same set.
- **Comments with the same phrase as a class name, left (comment-only
edits are outside this card).** `service-automation/src/engine.ts:357`
and `guard-refusal.ts:17` list "a run would execute unscoped" among
guard refusals. `qa/dogfood/test/flow-runas-schedule.dogfood.test.ts:12`
already carries #22345's D5 correction.
- **Hand-written docs, not runtime strings.**
`content/docs/automation/flows.mdx:1593` lists "a run that would execute
unscoped" among guard refusals. That names the refusal class the same
way `lint-flow-patterns.ts:728` does.
`content/docs/automation/hooks.mdx:155` says such operations are
"refused (`HOOK_UNSCOPED_DATA_ACCESS`) rather than run unscoped", the
same reading as the lint hint.
`skills/objectstack-automation/SKILL.md:194` is #22372's surface. Taker:
none.
- **Possible overlap.** #22343 (`domain:spec` seat 2) edits
`engine.ts`'s `validateNodeConfigKeys`, away from the run-setup warning.
It had not landed as of the last fetch of `origin/main` (`191543456f`,
none of whose commits since base touches a file this PR changes), so no
merge of `main` was owed.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3089848 commit fdfdd7e
7 files changed
Lines changed: 25 additions & 10 deletions
File tree
- .changeset
- packages
- objectql/src
- services/service-automation/src
- builtin
- triggers/trigger-schedule/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
210 | | - | |
| 210 | + | |
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
120 | | - | |
121 | | - | |
| 120 | + | |
| 121 | + | |
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
235 | 235 | | |
236 | 236 | | |
237 | 237 | | |
238 | | - | |
| 238 | + | |
239 | 239 | | |
240 | 240 | | |
241 | 241 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6132 | 6132 | | |
6133 | 6133 | | |
6134 | 6134 | | |
6135 | | - | |
6136 | | - | |
6137 | | - | |
| 6135 | + | |
| 6136 | + | |
| 6137 | + | |
6138 | 6138 | | |
6139 | 6139 | | |
6140 | 6140 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
90 | | - | |
| 89 | + | |
| 90 | + | |
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
119 | 119 | | |
120 | 120 | | |
121 | 121 | | |
122 | | - | |
| 122 | + | |
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
| |||
0 commit comments