Skip to content

docs(skills): objectstack-query names the context the engine admits since ADR-0096 D5 - #22382

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-22372-query-skill-principal-context
Oct 9, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-22372-query-skill-principal-context

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

维护者速读(草稿)

  • 改了什么: 已发布的 objectstack-query 技能里,"Execution Context" 一节的一段话。原来教"只传 { flowRunId } 做溯源"也是合法的执行上下文;现在改为:系统读传 { isSystem: true },否则传调用者自己的上下文(带用户、岗位或权限集),两者都没有则引擎拒绝(403 PERMISSION_DENIED,ADR-0096 D5)。只改这一段,净增 1 行(预算 0、上限 +1)。
  • 为什么改: ADR-0096 D5 严格模式(PR feat(plugin-security)!: refuse a principal-less, non-system data-engine context (ADR-0096 D5 strict mode) #22297)落地后,plugin-security 在全部站点拒收"无 principal 且非 system"的上下文。按旧句写出来的上下文会被引擎直接拒绝,而这份技能是通过 npx skills add 装进客户项目的,AI 作者先读到它、再撞上 403。同时按卡片要求对全部 skills/** 做了枚举:只有这一行教旧读法,其余命中都是 isSystem/runAs:'system' 这类显式提权或别的主题;评测文件没有断言旧句。
  • 风险与代价(含回滚): 纯文本改动,不碰代码、不发包、无 changeset(skills/** 不在任何已发布包的 files[] 内)。措辞按 main 上的 D5 契约原文写;PR docs(spec): ISecurityService and the data-engine contract page state the ADR-0096 D5 refusal of a principal-less context #22327 仍是 draft,它落地后两边说法一致。回滚即还原这一个文件的一次提交。
  • 席位意见:
  • 你要做的: 看一眼第 65-70 行这一段表述是否认可,认可就给一个批准。

Generated by Claude Code

…ince ADR-0096 D5

`skills/objectstack-query/SKILL.md` taught `{ flowRunId }` "for provenance
alone" as a valid execution context. Since ADR-0096 D5 strict mode the
security plugin refuses a non-system context that carries no principal
(no user, no position, no permission set) with `403 PERMISSION_DENIED`,
so the line prescribed a context the engine refuses. The paragraph now
names the two shapes the engine admits: `{ isSystem: true }` for a system
read, otherwise the caller's own context, and the refusal otherwise.

Claude-Session: https://claude.ai/code/session_01JmWtcHfGbC4ncw4GFKWuRA
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xs documentation Improvements or additions to documentation labels Oct 9, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 9, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f4e5170b1115ec85b880d74f053876764b6ea961
Local-runs: none

① Derived judgments

  • Accept set: unchanged. The diff edits one paragraph of published skill prose (skills/objectstack-query/SKILL.md:65-70); no schema, no runtime, no error code, no export. Right.
  • Public surface: the published catalog (skills/**, shipped by npx skills add) changes by one sentence — the context it prescribes moves from a shape the engine refuses since ADR-0096 D5 ({ flowRunId } alone) to the two shapes the engine admits ({ isSystem: true }, or a context carrying a user, position or permission set) and the refusal otherwise (403 PERMISSION_DENIED). Checked against main at 16096e8d7: packages/plugins/plugin-security/src/security-plugin.ts:383-387 (isPrincipalLessContext — no userId, no positions, no explicit permission sets) and :398-404 (principalLessDenial, PermissionDeniedError). The sentence names exactly that predicate's three fields and the refusal code — right; it narrows the prose to the contract and widens nothing.
  • Enumeration pin over skills/** (PR body): class 1 one hit (this line), class 2 zero hits across the listed query words, every non-zero word read and dispositioned; the seat's own control (isSystem → 3 files; flowRunId → 1 line) agrees. The two files held by open PRs (feat(formula): isoDate(t) / isoDatetime(t), the string form of a CEL timestamp #22347, fix(spec): the submitBehavior property help carries no ruling date #22322) are untouched. Right.
  • Evals: evals/filters-pagination-search.json case 5 prescribes isSystem: true; nothing asserts the old clause (flowRunId / provenance → 0 hits on the head). Unchanged and still true. Right.
  • Lines 68–70 are the original 67–69 re-wrapped with the text unchanged; net +1 line against a budget of 0 with a cap of +1 — within the cap; the stated reason (a 112-character contract sentence replacing a 37-character clause, re-wrap not a currency) holds. Right.

② Semver level

Clause-②: no on the claim and in the PR body — correct: nothing an npm package publishes changes. skip-changeset is the right declaration: skills/** is in no released package's files[] (the PR body measures every packages/**/package.json, with the positive control that the same old sentence inside packages/spec/src/kernel/execution-context.zod.ts:501 IS published, which is why PR #22327 carries a changeset). No .changeset/*.md touched.

③ Boundary flags

Implemented-by: claude/issue-22372-query-skill-principal-context
Reviewed-by: session_01JmWtcHfGbC4ncw4GFKWuRA

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 — PR #22382(#22372)— skills 席 1,2026-10-09T00:59Z

  • 改了什么: 已发布技能 objectstack-query 的 "Execution Context" 一段(SKILL.md 第 65–70 行)。原来教"只传 { flowRunId } 做溯源"也算合法执行上下文;现在改为:系统读传 { isSystem: true },否则传调用者自己的上下文(带用户、岗位或权限集),两者都没有则引擎拒绝(403 PERMISSION_DENIED,ADR-0096 D5)。只改这一段,净增 1 行。
  • 为什么改: ADR-0096 D5(PR feat(plugin-security)!: refuse a principal-less, non-system data-engine context (ADR-0096 D5 strict mode) #22297)落地后,plugin-security 在全部站点拒收"无 principal 且非 system"的上下文;旧句教出来的上下文会被引擎直接 403,而这份技能经 npx skills add 装进客户项目,AI 作者先读它、再撞 403。全 skills/** 枚举只有这一行教旧读法,评测文件未断言旧句。
  • 风险与代价(含回滚): 纯文本,不碰代码、不发包、无 changeset;措辞按 main 上的 D5 判定点原文写(security-plugin.ts:383–404),PR docs(spec): ISecurityService and the data-engine contract page state the ADR-0096 D5 refusal of a principal-less context #22327(spec/文档侧的同一句)仍是 draft,它落地后两边一致。回滚 = 还原这一个文件的一次提交。
  • 席位意见: 建议批准。改后句与引擎判定字段逐一对应(user / position / permission set + isSystem),是把发布面收窄到契约,不新增任何声明;+1 行在派发上限内,理由成立。席内契约复核 PASS 在案(6072073121)。
  • 你要做的: 看一眼第 65–70 行这段表述,认可就给一个 APPROVED;批准后席位落地(你顺手 ready + auto-merge 也可)。

@os-zhuang
os-zhuang marked this pull request as ready for review October 9, 2026 01:02
@os-zhuang
os-zhuang enabled auto-merge October 9, 2026 01:02
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 1915434 Oct 9, 2026
41 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-22372-query-skill-principal-context branch October 9, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills(objectstack-query): SKILL.md:66 teaches { flowRunId } as a context "for provenance alone", which the engine refuses since ADR-0096 D5

3 participants