Repository navigation
Commit 081e6a0
docs(spec): ISecurityService and the data-engine contract page state the ADR-0096 D5 refusal of a principal-less context (#22327)
Fixes #22302
Clause-②: no
## Summary
ADR-0096 D5 strict mode (merged as a3bcbcf) made `plugin-security`
refuse a data-engine context that carries no principal and is not a
system context. Two published texts still taught the old hand-off, and
this PR corrects them. Text only: no shape, runtime or export moves.
- `packages/spec/src/contracts/security-service.ts`: the four sentences
the card names, at three methods. Each now states what that context
gets, in the words of the method's return, and cites ADR-0096 D5.
- `content/docs/kernel/contracts/data-engine.mdx`: the
`ExecutionContext` passage no longer teaches `{ flowRunId }` alone as an
admitted context. It names the two admitted contexts and the refusal.
- `.changeset/22302-security-service-principal-less-text.md`:
`@objectstack/spec` patch (the TSDoc ships in the published `.d.ts`).
## What each sentence says now
A "principal" is what the existing text already defines: a position, a
named permission set or a user id. The context in question has none of
the three and is not `isSystem`.
| Where | Before | After |
|:---|:---|:---|
| `getReadFilter`, deny baseline paragraph | only a context with no
principal at all keeps the scope the other layers compose, "ADR-0096
stages that context separately" | so does a non-system context with no
principal at all (ADR-0096 D5 strict mode): the DENY filter, before any
layer composes a scope, as the engine middleware's principal-less
refusal (`403 PERMISSION_DENIED`) answers the same context |
| `canExport`, fails-closed paragraph | only a context with no principal
at all is admitted, as the middleware hands it through | so does a
non-system context with no principal at all (ADR-0096 D5 strict mode):
`false` before anything resolves, the answer the middleware's
principal-less refusal (`403 PERMISSION_DENIED`) gives it |
| `canReadObject`, arm list | the deny baseline arm, with "only a
context that carries no principal at all is handed through" in
parentheses | a new arm after `isSystem`: the ADR-0096 D5 refusal of a
context with no principal at all (`false`, before anything resolves);
the ADR-0056 D2 arm keeps its wording |
| `canReadObject`, fails-closed paragraph | only a context with no
principal at all is admitted, as the middleware hands it through | same
sentence as `canExport` |
| `data-engine.mdx`, `ExecutionContext` passage | a run with no
resolvable identity passes only `{ flowRunId }`, a context that
deliberately carries no principal | what the engine needs is a principal
or an explicit elevation: the caller's own context, or `{ isSystem: true
}`. A context with neither is refused by the security plugin with `403
PERMISSION_DENIED` before the operation runs, for every verb. `{
flowRunId }` alone is such a context: a run id is provenance, not
identity |
Unchanged on purpose: the system bypass sentences, the ADR-0056 D2 deny
baseline for a caller that carries a principal and resolves no set, and
the `getReadableFields` paragraph (it already says "a non-system caller
who resolves no permission set", which a principal-less context now is).
No PR number or tracker id is cited in the shipped text.
## Hypotheses measured before writing
Measured against origin/main at fe98cc6 (the tree moved after the base
59d993c; the moved files do not touch principal-less handling, checked
by diff of `security-plugin.ts`, which changed only in the
organization-wall region).
- **H1 holds. No mode switch.** `isPrincipalLessContext(context)` is
read at five call sites in `security-plugin.ts`: the engine middleware
(throws `principalLessDenial`, a `PermissionDeniedError`, `403
PERMISSION_DENIED`, for every verb, after the `isSystem` bypass, the
public-form-grant branch and the pre-resolution gates, and before any
permission-set resolution), `getReadFilter` (returns the deny filter),
`canReadObject`, `canWriteObject` and `canExport` (each returns
`false`). The sixth site, `resolveProjectionFieldMask`, no longer
settles a principal-less context early: it falls through to the zero-set
mask like any caller that resolves no set. `SecurityPluginOptions` has
two keys (`defaultPermissionSets`, `fallbackPermissionSet`) and the file
reads no environment variable on this path.
`principal-less-strict-mode.test.ts`,
`can-read-object-admission.test.ts` and
`can-write-object-admission.test.ts` pass (3 files, 103 tests).
- **H2: the file holds exactly the four sentences, and no
`canWriteObject` passage.** `canWriteObject` is not a member of
`ISecurityService` (it is a method of the plugin only), so the contract
file never describes it. The other `principal` hits in the file were
each read: `hasWriteBypass` (:698), `resolveWriteScope` (:718),
`describeDelegationNarrowing` (:754) and `checkAuthoredRowWrite` (:798)
say a principal-less context fails closed, answers `narrowed: false` or
abstains, all still true. `getMetadataReadableFields` (:389) says
`canReadObject` is "the deny baseline for a caller that carries a
principal", which is true and incomplete, not a hand-off statement, and
is left alone.
- **H3 holds for `content/docs`.** `data-engine.mdx` is the only page
that teaches `{ flowRunId }` as an admitted context. The other
`flowRunId` and "no principal" hits are auto-generated reference tables
or are about other subjects (record-view auditing, the time-triggered
flow acting organization, realtime subscriptions, rate-limit keys). One
neighbouring sentence is named under Acceptance notes.
## Verification
All on commit 4363c58 (the head of this branch).
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 101 commands (95 at dispatch, plus 6
that apply once the changeset exists). 100 ran with exit 0. 1 is NOT
MEASURED: `pnpm check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT
MET (it reads `dist/` of 32 packages and needs a repo-wide `pnpm build`;
nothing was measured). Basis for leaving it to CI (`Build Core`): this
diff emits no JavaScript. The new sentences appear only in
`packages/spec/dist/contracts/index.d.ts` and `index.d.mts`; a recursive
grep of `dist/` finds them in no `.js` or `.cjs`, and finds none of the
old sentences anywhere. Three gates first exited 3 for unbuilt
prerequisites (`@objectstack/formula`, `@objectstack/lint`,
`@objectstack/client-react`); after building those they ran and exited
0.
- `--ran` verdict line: `dispatch-gates --ran: 101 derived famil(ies)
accounted for — 100 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit
3).` Exit 0.
- `pnpm --filter @objectstack/spec build` exit 0. `pnpm --filter
@objectstack/spec typecheck` exit 0.
- `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2
src/contracts/security-service.test.ts
src/contracts/core-service-contracts.test.ts`: 2 files, 33 tests passed.
- Reverse verification and ablation do not apply: no behaviour moves, so
there is no assertion to turn red.
- Not run here, left to CI: the repo-wide lint run, `Test Core`,
`Dogfood Regression Gate`, `Build Docs` and the type-check lanes outside
`@objectstack/spec`.
## Line budget
59 changed lines against the merge base (42 added, 17 removed) in 3
files. No governed path is touched (`docs/adr/**`, `skills/**`,
`.claude/**`, `AGENTS.md`, `CLAUDE.md`: 0).
## Acceptance notes
Noted, not edited, because they sit outside the two files the claim
names. They state the same hand-off in published or shipped text and are
worth one follow-up edit together:
- `packages/spec/src/kernel/execution-context.zod.ts`, the `flowRunId`
field TSDoc (about :336-341): "A run that resolves no principal ...
passes exactly `{ flowRunId }`: ... such a context authorizes
identically to no context at all, and the run keeps the unscoped posture
it already had". Stale: that context is refused now.
- `packages/spec/src/kernel/execution-context.zod.ts` about :499-501,
the `ExecutionContext` type TSDoc: "a flow run with no resolvable
identity passes provenance alone (`{ flowRunId }`)". Teaches the refused
context as a valid one.
- `packages/spec/src/data/data-engine.zod.ts` about :66-71, the
`BaseEngineOptionsSchema.context` TSDoc: "a flow run that resolves no
identity passes provenance alone (`{ flowRunId }`), a context
deliberately carrying no principal at all". Same.
- `packages/spec/src/contracts/analytics-service.ts` about :441-443: the
rationale for forwarding the request context says a context-less bridge
ran "completely unscoped" because "the security middleware's fall-open
skipped its own RLS injection". Historical, but with `plugin-security`
composed the context-less call is refused rather than unscoped.
- `content/docs/permissions/access-recipes.mdx` :52: "A
schedule-triggered run has no user, so under `user` it runs unscoped".
The `runAs` text in `content/docs/automation/approvals.mdx`, `hooks.mdx`
and the generated `automation-api.mdx` says such a run's data operations
are refused. I did not run a schedule-triggered flow, so this is a
read-only inference.
- `hook.zod.ts` about :1126 ("a schedule-triggered flow run resolves no
principal") reads true as written and is left alone.
One behaviour edge, measured by reading and by
`principal-less-strict-mode.test.ts`, not a text defect: the
middleware's public-form-grant branch (ADR-0056) sits ahead of the
principal-less refusal, so a hand-built `{ publicFormGrant: { object }
}` with no named set would be admitted for its create and read-back on
that object, while `canReadObject`, `canExport` and `getReadFilter`
answer deny for it. The only producer in the tree, the REST form-submit
route, builds its context with `permissions: ['guest_portal']`, so it
carries a principal and is not in the class this text describes. The new
text says "the middleware's principal-less refusal" rather than claiming
the middleware refuses every such context for that reason.
## Patch round 1 (the seat amended the claim surface)
Adds the TSDoc of two more published sources and one docs line. Comment
and docs text only: 0 of 71 changed lines in the three .ts files is
outside a comment block.
- packages/spec/src/kernel/execution-context.zod.ts: the flowRunId field
TSDoc and the ExecutionContext input type TSDoc.
packages/spec/src/data/data-engine.zod.ts: the
BaseEngineOptionsSchema.context TSDoc. They no longer say a run with no
principal passes exactly the flowRunId-only context, which authorizes
like no context and keeps the unscoped posture, or that such a context
is deliberately principal-less. They now say a non-system context with
no principal is refused with 403 PERMISSION_DENIED (ADR-0096 D5), that a
run id is provenance and not a principal, and, for the flowRunId field,
what the automation engine really hands the engine.
- content/docs/permissions/access-recipes.mdx line 52: a
schedule-triggered run under runAs user is refused, not run unscoped.
- The changeset gains one sentence naming the three passages (still
@objectstack/spec patch, Clause-② no).
Measured before written, on origin/main at 28bff18 (the merge base of
this round):
packages/services/service-automation/src/runtime-identity.ts,
resolveRunDataContext (line 178). With runAs system it returns isSystem
true plus actor svc:flow:NAME, empty positions and permissions, the run
id, and the trigger userId and tenantId only when the trigger resolved
them (lines 180-218). With runAs user and no trigger user, which is what
a schedule run has, it throws UnscopedRunDataAccessError (code
AUTOMATION_UNSCOPED_RUN_DATA_ACCESS) at line 231, before any call
reaches the data engine. With a trigger user it returns that user
identity (line 233 on). engine.ts resolveRunContext (lines 6057 and
6062) stamps runAs (default user) and flowRunId on the run context.
Nothing in service-automation builds a flowRunId-only context: the
RunProvenanceContext type is exported but produced nowhere. Pinned by
src/builtin/crud-runas.test.ts (24 tests pass; lines 221, 247, 288 and
316: a user-mode run with no user throws, a schedule run reaches the
data layer with zero operations and fails, a record-change run fired by
a user-less write is refused the same way).
Verification on head b7cc3e2: spec build exit 0; the old sentences are
in no file of packages/spec/dist (.d.ts, .d.mts, .js, .mjs: 0 files);
the type TSDoc lands in manifest.zod-*.d.ts and .d.mts, the field TSDoc
in the kernel, data, api and browser JS bundles only (a field comment is
not emitted into a declaration); spec typecheck exit 0; check:docs exit
0; dispatch-gates --ran: 102 derived, 101 run, 1 NOT-MEASURED
(check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET, needs a
repo-wide build, left to Build Core). Line budget against the merge
base: 101 changed lines (68 added, 33 removed), 6 files, 0 governed
paths. This round the comments do ship in JS bundles, so the round-0
statement that the diff emits no JavaScript no longer holds; the gate
stays NOT MEASURED on the basis that every changed line in the three .ts
files is a comment line.
Acceptance notes added: (1) skills/objectstack-query/SKILL.md line 66
still reads that flowRunId is passed for provenance alone; it is a
governed Tier H file, not edited, needs its own carrier. (2)
packages/services/service-automation/src/runtime-identity.ts lines 56-81
and engine.ts lines 6042-6046 still describe the pre-refusal middleware
skip, and the RunProvenanceContext export has no producer; noted, not
edited, carrier none. (3) contracts/analytics-service.ts about line 441
stays, as the seat ruled: past-tense history.
CI note, not caused by a test: Test Core 6 of 6 fails at the step Check
this shard timing drift (exit 1) on this PR at both 4363c58 and
b7cc3e2, with the shard test step before it passing; the same shard is
green on the current origin/main push run. The job log is not reachable
from this container (the log blob host is refused), so the drift numbers
are NOT MEASURED here.
## Merge-main round (head `7c79aade4`)
#22014's shard-timings refresh (PR #22368) unblocked `Test Core (6/6)`'s
drift step, so `main` (`27a8b33de`, 53 commits past the earlier base
`28bff18d0`) was merged with `scripts/pm/os-regen-merge.sh`. Zero files
conflicted.
- **The PR's delta is unchanged:** the same six files, +68 / −33, 101
changed lines. The three `.ts` files change comments only
(`stripComments` comparison, `27a8b33de` vs head).
- **No sentence went false.** `main` touched none of the six paths, and
#22357 / #22382 restate the same principal-less rule in agreement.
- **Measured:** `@objectstack/spec` unit, `local` project 627 files /
18757 passed and `repo` project 54 / 915. 102 derived gates ran: 101
exited 0, and `check:dual-build-cjs-loads` was left to CI's Build Core
(comment-only diff).
- **CI on this head:** 33 success, 2 skipped.
- **Review:** the fresh contract review **PASS** `6076493127`. The
Verification and Line budget sections above keep the round-0 numbers for
the record.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 05c7c3f commit 081e6a0
6 files changed
Lines changed: 68 additions & 33 deletions
File tree
- .changeset
- content/docs
- kernel/contracts
- permissions
- packages/spec/src
- contracts
- data
- kernel
| 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 | |
|---|---|---|---|
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
126 | | - | |
127 | | - | |
128 | | - | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
129 | 136 | | |
130 | 137 | | |
131 | 138 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
| 52 | + | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
313 | 313 | | |
314 | 314 | | |
315 | 315 | | |
316 | | - | |
317 | | - | |
318 | | - | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
319 | 320 | | |
320 | 321 | | |
321 | 322 | | |
| |||
627 | 628 | | |
628 | 629 | | |
629 | 630 | | |
630 | | - | |
631 | | - | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
632 | 635 | | |
633 | 636 | | |
634 | 637 | | |
| |||
647 | 650 | | |
648 | 651 | | |
649 | 652 | | |
650 | | - | |
651 | | - | |
652 | | - | |
653 | | - | |
654 | | - | |
655 | | - | |
656 | | - | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
657 | 660 | | |
658 | 661 | | |
659 | 662 | | |
| |||
667 | 670 | | |
668 | 671 | | |
669 | 672 | | |
670 | | - | |
671 | | - | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
672 | 677 | | |
673 | 678 | | |
674 | 679 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
72 | 74 | | |
73 | 75 | | |
74 | 76 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
331 | 331 | | |
332 | 332 | | |
333 | 333 | | |
334 | | - | |
335 | | - | |
336 | | - | |
337 | | - | |
338 | | - | |
339 | | - | |
340 | | - | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
341 | 344 | | |
342 | 345 | | |
343 | 346 | | |
| |||
496 | 499 | | |
497 | 500 | | |
498 | 501 | | |
499 | | - | |
500 | | - | |
501 | | - | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
502 | 508 | | |
503 | 509 | | |
504 | 510 | | |
| |||
0 commit comments