Repository navigation
Commit 1915434
docs(skills): objectstack-query names the context the engine admits since ADR-0096 D5 (#22382)
Fixes #22372
Clause-②: no
The published `objectstack-query` skill taught `{ flowRunId }` "for
provenance alone" as a valid execution context. Since ADR-0096 D5 strict
mode (PR #22297) the security plugin refuses a non-system context that
carries no principal with `403 PERMISSION_DENIED`, so an AI author
following that line wrote a context the engine refuses. This PR rewrites
that one paragraph in place so it names the two shapes the engine admits
and the refusal otherwise, and records the `skills/**` enumeration the
card asked for.
## The paragraph
Before (`skills/objectstack-query/SKILL.md:65-69` on `main` at
`16096e8d7`):
```
Pass any SUBSET of the execution envelope (identity, tenant, transaction):
`{ isSystem: true }` for a system read, `{ flowRunId }` for provenance alone. On
the READ methods it may sit in the query bag (above) OR in the trailing options
argument, `engine.find(obj, query, { context })`; the trailing one wins when
both are given. Writes take only the trailing argument.
```
After (`:65-70`):
```
Pass any SUBSET of the execution envelope (identity, tenant, transaction):
`{ isSystem: true }` for a system read, otherwise the caller's own context
(user, position or permission set), or `403 PERMISSION_DENIED` (ADR-0096 D5).
On the READ methods it may sit in the query bag (above) OR in the trailing
options argument, `engine.find(obj, query, { context })`; the trailing one wins
when both are given. Writes take only the trailing argument.
```
Lines 68-70 are the original 67-69 re-wrapped, text unchanged.
## The contract the sentence follows — `main` at `16096e8d7`, verbatim
at each site
- `packages/plugins/plugin-security/src/security-plugin.ts:383-387`,
`isPrincipalLessContext`: `positions.length === 0 &&
explicitPermissionSets.length === 0 && !context?.userId`. That is the
"(user, position or permission set)" gloss.
- `:398-404`, `principalLessDenial`, the refusal text: "was called with
a context that carries no principal (no user, no position, no permission
set) and is not a system context. Pass the caller's execution context,
or, for platform plumbing whose own door already authorized the caller,
the explicit system opt-in (isSystem: true)." — `PermissionDeniedError`,
`403 PERMISSION_DENIED`.
- `:349-367`, the predicate's docblock: "A non-system context of this
class is REFUSED, at every layer that used to hand it through: the
engine middleware throws principalLessDenial before it resolves
anything, the object-admission probes (`canReadObject` /
`canWriteObject` / `canExport`) answer `false`, and its row scope is the
deny sentinel (`getReadFilter`)." and "The two ways to reach the engine
are explicit, never a missing field: carry the caller's principal, or …
the explicit system opt-in (`isSystem: true`)."
- `docs/adr/0096-execution-surface-identity-admission.md`, the D5 note
dated 2026-10-08: "An engine context that carries no principal (no user,
no position, no permission set) and is not a system context is refused
with `PermissionDeniedError` (`403 PERMISSION_DENIED`) wherever the
security plugin used to hand it through".
**Wording and PR #22327.** PR #22327 (card #22302) is still an open
draft as of this PR (read via REST: `state: open`, `draft: true`; it
edits `packages/spec/src/contracts/security-service.ts`,
`packages/spec/src/kernel/execution-context.zod.ts`,
`packages/spec/src/data/data-engine.zod.ts`,
`content/docs/kernel/contracts/data-engine.mdx` and
`content/docs/permissions/access-recipes.mdx`). So the sentence here
follows the D5 contract as it stands on `main` — the plugin's refusal
text and predicate above — not that PR's draft text; the two agree on
substance (principal or `isSystem: true`, else `403 PERMISSION_DENIED`,
ADR-0096 D5). On `main` the old sentence still stands at
`execution-context.zod.ts:335-336` and `:501`,
`data-engine.zod.ts:67-70` and `data-engine.mdx:127-128`; those are
#22327's files and are not touched here.
## Enumeration pin — `git grep -n -i` over `skills/**` on `main` at
`16096e8d7`
**Class 1, a `{ flowRunId }`-only context.** `flowRunId`: 1 hit,
`objectstack-query/SKILL.md:66`, fixed here. `provenance`: 1 hit, the
same line. `runId` / `run id`: 2 hits, the same line plus
`objectstack-automation/references/state-machines-and-approvals.md:208`,
a `:runId` URL path parameter, not a context.
**Class 2, a principal-less context that is admitted, keeps its scope or
falls open.** Zero hits for each of: `no principal`, `without a
principal`, `principal-less`, `principalless`, `anonymous context`,
`context: {}` (fixed-string, and the regex `context:\s*\{\s*\}`), `empty
context`, `fall open`, `falls open`, `fall-open`, `fail open`,
`fail-open`, `hand(ed|s)? (it )?through`, `keeps its scope`, `skips?
(the )?(permission |security )?checks`, `no identity`, `without
identity`, `resolves no identity`, `without (a )?context`,
`contextless`, `no context`, `RLS-on`, `sees-nothing`, `SYSTEM_CTX`,
`passes only`. Non-zero query words, each hit read and dispositioned:
- `unauthenticated` (4): `objectstack-api/SKILL.md:162` — `authRequired:
false` opens an anonymous HTTP entry point (ADR-0121 D6 pairing); that
is the door's authentication, not an engine context. The public-form
endpoints at `:87-88` run under a synthetic `{ permissions:
['guest_portal'], anonymous: true }` context, which carries a named
permission set and so is not principal-less under the predicate.
`objectstack-data/references/data-hooks.md:361`, `:602`, `:629` —
`ctx.user` is `undefined` for system / unauthenticated writes: the ctx
shape, no admission claim.
- `no user` (2): `objectstack-automation/SKILL.md:192-194` — a `'user'`
hook whose trigger resolved no user has its `ctx.api` refused
(`HOOK_UNSCOPED_DATA_ACCESS`, 403) "rather than run unscoped":
fail-closed, consistent with D5. `data-hooks.md:930` — "system
operations carry no user", a system context.
- `context-less` (1): `objectstack-ui/rules/actions.md:127` — `ctx.user`
is `undefined` for a context-less / self-invoked call (the
`ScopedRepo.execute()` path,
`packages/runtime/src/sandbox/body-runner.ts:1188-1198` says that path
carries no caller identity). It describes `ctx.user`; it does not say
the engine admits such a context. Left alone.
- `carries no` (5), `sees nothing` (1), `anonymous` (11), `bypass` (7),
`elevat` (12), `runAs` (20), `system context` (3), `resolve[sd] no` (3),
`no resolvable` (1), `unscoped` (4): every hit is either an explicit
elevation the engine admits (`isSystem`, `runAs: 'system'`:
`objectstack-automation/SKILL.md:183`,
`references/examples-flows.md:23`, `:88` teach `runAs: 'system'` for a
run with no trigger user, which is the D5-correct prescription) or a
different subject (anonymous records, sharing `bypass`, public forms, a
search axis, a time dimension).
Control: `isSystem` hits 3 files (`data-hooks.md`,
`objectstack-query/SKILL.md`,
`objectstack-query/evals/filters-pagination-search.json`), matching the
seat's reading. No hit lands in `skills/objectstack-formula/SKILL.md`
(PR #22347) or `skills/objectstack-ui/references/react-blocks.md` (PR
#22322); neither file is touched.
## Evals
`skills/objectstack-query/evals/filters-pagination-search.json`: the 2
`isSystem` hits are both in case `id: 5`, whose `expected_output`
prescribes `context: { isSystem: true }` and whose `must_contain` is
`["context", "isSystem: true", "limit: 1"]`. `flowRunId` / `provenance`
/ `envelope`: 0 hits across `evals/`. The old line is not asserted; the
eval is unchanged and stays true.
## Budget — net-line budget 0, cap +1: spent +1
| reading | before (`16096e8d7`) | after (`f4e5170b`) |
|---|---|---|
| `skills/objectstack-query/SKILL.md`, lines | 400 | 401 (+1) |
| whole package, all `skills/*/SKILL.md`, lines | 4409 | 4410 (+1) |
| `SKILL.md` tokens, `ceil(bytes/4)` (ceiling 5552) | 4109 | 4128
(headroom 1424) |
Why not 0: the replaced clause (`{ flowRunId }` for provenance alone)
was 37 characters; the replacement that states the admitted shape, the
refusal code and the ADR is 112. A 0-net fit required deleting content —
the envelope's "(identity, tenant, transaction)" or the principal gloss
— and re-wrap is not a currency, so the one line the cap allows was
spent instead. `scripts/pm/check-skill-line-ratchet.mjs` does not cover
the published root (its header says so); the token ratchet is the
binding one and it is green with headroom.
## Changeset
`skills/**` is in no released package's `files[]`: no `package.json`
under `packages/` names a `skills` path and none carries an entry that
escapes its own directory (both measured over every
`packages/**/package.json`); the catalog reaches customers through `npx
skills add objectstack-ai/objectstack/skills` from this repository,
which `packages/create-objectstack` invokes at scaffold time rather than
bundling (`src/created-summary.ts:31`). Positive control: the same old
sentence in `packages/spec/src/kernel/execution-context.zod.ts:501` IS
inside spec's `files[]` (`src/**/*.zod.ts`), which is why #22327 carries
a changeset and this PR does not. No `.changeset/*.md`; `skip-changeset`
is the repo's skip form.
## Gates — merge base `16096e8d7`, final commit `f4e5170b`
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 24 families from the worktree change
set (1 path); each ran with its exit code captured before any pipe;
`--ran` reconciliation: "24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN",
with every line carrying its exit code ("a DERIVED zero — all 24
recorded an exit code and none of them is 3"). All 24 exit 0:
- `node scripts/check-skills-token-ratchet.mjs` (+ `--self-test`):
"skills/objectstack-query/SKILL.md is 4128 tokens (ceiling 5552;
headroom 1424)".
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions`,
after building its prerequisite under the verify lock (`pnpm exec turbo
run build --filter=@objectstack/formula --filter=@objectstack/lint`,
`VERDICT command-exit 0`, held 105s, waited 0s).
- `pnpm --filter @objectstack/spec run check:skill-docs` (reads
frontmatter only; unchanged), `pnpm check:doc-authoring`, `pnpm
check:skill-identifier-liveness`, `pnpm check:skill-frame-sync`, `pnpm
check:skill-compatibility`, `pnpm check:corpus-claim-drift`, `pnpm
check:cross-package-test-inputs`, `pnpm check:agent-test-spelling`,
`pnpm check:role-word`, `pnpm check:gitlink-declared`, `pnpm
check:driver-memory-census`, `pnpm check:refd-timer-probe`, `pnpm
check:watch-hint-literal`, `pnpm check:pm-governed-merges`, `pnpm
check:nul-bytes`, `node scripts/check-ci-filter-parity.mjs`, `node
scripts/check-closing-keyword-parity.mjs` (+ `--self-test`), `node
scripts/check-comment-mask-corpus.mjs`, `node
scripts/check-doc-route-spelling.mjs --advisory` (+ `--self-test`).
Beyond the derivation: `pnpm check:pm-skill-ratchet` exit 0 (the
published root is outside its map, as its header states); `pnpm --filter
@objectstack/spec run check:skill-refs` exit 0 ("9 generated files in
sync", nothing to regenerate); a control-byte scan over the edited file
finds none. The 52 artifact-roster families, the 11 declared
wide-population families and the type-check lanes the derivation lists
outside the derived total are CI's runs on this PR; `pnpm lint`
(repo-level eslint) was not run locally — the diff is one Markdown file.
## Acceptance notes
- Governed surface, Tier H (`skills/**`): this PR stays draft; landing
waits for an authorized approval, and the dispatch names the
contract-review tier as mandatory on this path.
- Commit identity: this cloud container cannot mint the fleet identity
(`OS_FLEET_APP_ID` / `OS_FLEET_PRIVATE_KEY` are unset and the relay
hands out no token for `git`), so the one commit carries the worktree's
harness identity; every later commit on this branch keeps that same
identity.
- Observed, not filed (code comments are #22345's lane, PR #22357):
`packages/runtime/src/sandbox/body-runner.ts:979-984` still says "A
caller that has no context to give gets the same identity-less behavior
as before", a pre-D5 reading in a code comment.
- Historical `CHANGELOG.md` entries ("A run with no principal now passes
provenance alone.") are release-owned records of what shipped and are
not edited.
## 维护者速读(草稿)
- **改了什么:** 已发布的 `objectstack-query` 技能里,"Execution Context"
一节的一段话。原来教"只传 `{ flowRunId }` 做溯源"也是合法的执行上下文;现在改为:系统读传 `{ isSystem: true
}`,否则传调用者自己的上下文(带用户、岗位或权限集),两者都没有则引擎拒绝(`403 PERMISSION_DENIED`,ADR-0096
D5)。只改这一段,净增 1 行(预算 0、上限 +1)。
- **为什么改:** ADR-0096 D5 严格模式(PR #22297)落地后,`plugin-security` 在全部站点拒收"无
principal 且非 system"的上下文。按旧句写出来的上下文会被引擎直接拒绝,而这份技能是通过 `npx skills add`
装进客户项目的,AI 作者先读到它、再撞上 403。同时按卡片要求对全部 `skills/**` 做了枚举:只有这一行教旧读法,其余命中都是
`isSystem`/`runAs:'system'` 这类显式提权或别的主题;评测文件没有断言旧句。
- **风险与代价(含回滚):** 纯文本改动,不碰代码、不发包、无 changeset(`skills/**` 不在任何已发布包的
`files[]` 内)。措辞按 `main` 上的 D5 契约原文写;PR #22327 仍是
draft,它落地后两边说法一致。回滚即还原这一个文件的一次提交。
- **席位意见:**
- **你要做的:** 看一眼第 65-70 行这一段表述是否认可,认可就给一个批准。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JmWtcHfGbC4ncw4GFKWuRA)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent f6e1d49 commit 1915434
1 file changed
Lines changed: 5 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
70 | 71 | | |
71 | 72 | | |
72 | 73 | | |
| |||
0 commit comments