Skip to content

docs(skills, spec): every published package declares one anchor-safe isDefault permission set - #25930

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-2drwet
Oct 26, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-2drwet

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread
Fixes #25773

Clause-②: no
Metadata-protocol: none

Summary

Consequence (ii) of maintainer ruling record 6341690661: the published skill and the marketplace reference now state that every published package declares exactly one anchor-safe isDefault: true permission set. Text only. No lint rule, schema key or runtime path changes, and no new gate (the ruling orders the text; a gate needs the maintainer's own word).

What changed

  1. skills/objectstack-data/rules/security.md, the isDefault: true bullet under the RBAC section. It keeps the semantics (the everyone baseline, ADR-0090 D5) and adds the requirement as a check an author can run: exactly one such set, anchor-safe, meaning describeAnchorForbiddenBits(set, 'everyone') from @objectstack/spec/security returns null. It names the two refusals that already exist (lint rule security-anchor-high-privilege, and the binding), says what happens with none (invited members get nothing until an admin assigns a set to each), and says that no gate counts the sets. The old "lint and boot refuse a platform capability or undeclared name" clause is folded in: it is exactly what the anchor-safe predicate refuses in systemPermissions, so the bullet now says the set's systemPermissions may name only app capabilities declared under capabilities:.
  2. Marketplace docs. The source is the Marketplace Protocol module doc block at the top of packages/spec/src/marketplace/marketplace.zod.ts, which build-docs.ts renders as content/docs/references/marketplace/marketplace.mdx (AUTO-GEN, not hand-edited). That block already carries the "Developer Publishing Flow" a publisher reads, so the new section sits right after it. The exact source sentence added: "Exactly one anchor-safe default permission set (ADR-0090 D5): isDefault: true, and describeAnchorForbiddenBits(set, 'everyone') from @objectstack/spec/security returns null." The section then gives the reason and the enforcement facts. Regenerated with pnpm --filter @objectstack/spec gen:docs; the only regenerated diff is the new 9-line "What a published package declares" section in marketplace.mdx (commit f1340f3).
  3. .changeset/25773-published-package-default-permission-set.md: patch for @objectstack/skills (ships skills/**) and @objectstack/spec (ships src/**/*.zod.ts, so the edited doc block is published text).

Where the predicate and the refusals live (read at cb0a2fcf)

  • Predicate: describeAnchorForbiddenBits in packages/spec/src/security/high-privilege.ts, exported from @objectstack/spec/security. For everyone it is describeHighPrivilegeBits with the stack's declared capabilities as context: no platform or undeclared systemPermissions, no viewAllRecords / modifyAllRecords, no allowDelete / allowTransfer / allowExport on any object.
  • Author-time refusal: packages/lint/src/validate-security-posture.ts, rule SECURITY_ANCHOR_HIGH_PRIVILEGE = security-anchor-high-privilege (error), run on every isDefault: true set.
  • Bind-time refusal: confirmAudienceBindingSuggestion in packages/plugins/plugin-security/src/suggested-audience-bindings.ts runs the same predicate; at boot everyoneBaseline in security-plugin.ts withholds a high-privilege set from everyone.
  • Presence and count: nothing enforces them. appDefaultPermissionSetName (plugin-security/src/app-default-permission-set.ts) takes the first isDefault set and returns undefined when there is none, with no diagnostic. The text says "no gate counts them" for that reason.

Token ratchet (skills/**)

Reading Before (cb0a2fcf) After (f1340f32)
rules/security.md tokens (ceiling 2563, not raised) 2563 2561
rules/security.md lines 220 220
skills/objectstack-data/** lines 3806 3806
all skills/*/SKILL.md lines (untouched) 4383 4383
bundle total tokens (check-skills-token-ratchet; before = after + 2, only this file moved) 155469 155467

The new text (+66 tokens on the bullet as first drafted) is paid by deleting content in the same file, with no re-wrap counted as savings: the stack.zod.ts path aside on the permissionSets: bullet; the parenthetical after the rls.zod.ts source line; the parenthetical after the field.zod.ts source line; ADR-0105 §"Today's two postures" from the multi-tenancy reference (ADR-0093 and the tenancy-modes URL stay); "plugin-security's FieldMasker enforces it. The real channels are:" shortened to "(FieldMasker). Two more channels:"; and the capability clause folded into the anchor-safe sentence as described above.

Gates (head f1340f32)

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 113 commands. All 113 run with exit codes captured before any pipe: 113 exit 0. Six first answered exit 3 (PREREQUISITE NOT MET: check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure) and answered exit 0 after a full pnpm turbo run build under the verify lock. dispatch-gates --ran: "113 derived famil(ies) accounted for — 113 run, 0 NOT-MEASURED". Also pnpm --filter @objectstack/spec build exit 0, pnpm --filter @objectstack/spec typecheck exit 0, check:generated exit 0 after gen:docs. No tests changed and no tested behaviour changed, so there is no ablation. CI families (Test Core, Build Core, Build Docs, type-check lanes) are left to this PR's check-runs.

维护者速读(草稿)

  • 改了什么:对外发布的技能文档 security.md 和 marketplace 参考文档里,写明"每个发布包必须声明且只声明一个锚安全的默认权限集(isDefault: true)",并给出作者可自查的判据(describeAnchorForbiddenBits(set, 'everyone') 返回 null)。只改文字,不加门禁、不改代码行为。
  • 为什么改:落实裁决 6341690661 的第 (ii) 项:默认访问只通过一个机制到达组织邀请的成员;包里没有这样的权限集,新成员就什么都看不到,直到管理员逐人分配。原文只讲了含义,没讲要求。
  • 风险与代价(含回滚):技能文件 token 不增(2563 → 2561),用删掉同文件三处源码路径旁注、一处 ADR 章节引用和一句话来抵账。回滚即还原这两个文件并重生成文档页。
  • 席位意见:
  • 你要做的:这是 Tier H(skills/**),需要你批准后由席位落地。

Acceptance notes

  • No gate refuses a package that declares no isDefault set or more than one; the app-level reader appDefaultPermissionSetName silently takes the first. Whether a gate should enforce this at publish time is left for the maintainer's word, per the card's boundary.

🤖 Generated with Claude Code

https://claude.ai/code/session_014LdiGPrydHj5PHTzYQUvoW


Generated by Claude Code

…isDefault permission set

The objectstack-data security rule and the Marketplace Protocol header now state
the requirement: exactly one `isDefault: true` set for which
`describeAnchorForbiddenBits(set, 'everyone')` returns null. The new text is paid
for by trimming the same skill file, which stays under its token ceiling.

Claude-Session: https://claude.ai/code/session_014LdiGPrydHj5PHTzYQUvoW
Co-Authored-By: Claude <noreply@anthropic.com>
@os-zhuang
os-zhuang marked this pull request as ready for review October 26, 2026 04:36
@os-zhuang
os-zhuang enabled auto-merge October 26, 2026 04:36
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/marketplace/marketplace.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/marketplace/marketplace.zod.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2a728ba2acf52c13d1918c2afca7b79bf391e1f5 → packageMentionDocs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling labels Oct 26, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f1340f32fa7594793d379cf024f5b0dd39f7b013
Local-runs: none
Scope: packages/spec/src/marketplace/marketplace.zod.ts, skills/objectstack-data/rules/security.md + packages/spec/src/security/high-privilege.ts, packages/spec/src/security/index.ts, packages/spec/package.json, packages/lint/src/validate-security-posture.ts, packages/plugins/plugin-security/src/suggested-audience-bindings.ts, packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/plugin-security/src/app-default-permission-set.ts, scripts/check-skills-token-ratchet.mjs, content/docs/references/marketplace/marketplace.mdx, .changeset/25773-published-package-default-permission-set.md, packages/skills/package.json

Face: governed rule text (skills/**) + published spec file (packages/spec/src/**/*.zod.ts ships in files[]). Inputs: card #25773 body and comments 6397310741 / 6407727166 / 6408339288, ruling record 6341690661 on #25083, PR #25930 body, file list, net diff cb0a2fcf..f1340f32 (merge-base cb0a2fcf), check-runs on f1340f32. Author objectstack-fleet[bot] (fleet account; no external-author note owed).

① Derived judgments

  1. Accept set: unchanged — right. The only packages/spec/src/** hunk is inside the /** # Marketplace Protocol … */ module doc block of marketplace.zod.ts (L31–L40: one new ## What a published package declares paragraph, one trailing-space fix). No schema, key, enum value, export or .describe() moved; CONSUMER_INSTALLABLE_TYPES import and every *Schema below are byte-identical. No new spec key, so the land-with-first-reader rule is not engaged. Clause-②: no and Metadata-protocol: none are correct for this diff.
  2. Fidelity to consequence (ii) — right, no scope creep. The ruling orders text only: 「every published package declares one anchor-safe isDefault: true set」 in security.md (semantics → requirement) and the marketplace docs. The diff is four files, all prose/doc-block/changeset; no lint rule, runtime branch, schema or gate was added. The security.md bullet states the rule as a predicate an author can run, naming the symbol rather than paraphrasing it, as the card asked.
  3. The predicate exists and means what the text says — right, with one precision note. describeAnchorForbiddenBits(def, anchor: 'everyone' | 'guest', context?) is defined in packages/spec/src/security/high-privilege.ts L192–L215, re-exported by export * from './high-privilege' (security/index.ts L21), and @objectstack/spec/security is a real subpath in packages/spec/package.json#exports. For 'everyone' it returns describeHighPrivilegeBits(def, context) and otherwise null (L203–L205), i.e. null ⇔ no platform/undeclared systemPermissions, no VAMA, no delete/transfer/export bit — exactly the anchor-safe reading the text gives. Precision note for the approver: the text shows the two-argument call; the gates pass the stack's declared capabilities as the third argument (anchorContext in lint L966, capabilities in the derivation L660), and without it an app capability in systemPermissions reads as forbidden (high-privilege.test.ts L162 vs L163). The call as written is therefore stricter than the gate, never looser — a set it passes is anchor-safe — and the bullet's next sentence (systemPermissions may name only app capabilities declared under capabilities:) carries the context semantics. Acceptable at this token budget; noted, not a defect.
  4. The named refusals exist — right. Lint: SECURITY_ANCHOR_HIGH_PRIVILEGE = 'security-anchor-high-privilege' (validate-security-posture.ts L129), applied with severity: 'error' to every ps.isDefault === true set via describeAnchorForbiddenBits(ps, 'everyone', anchorContext) (L966–L983). Binding: suggested-audience-bindings.ts imports the predicate (L81) and the everyone derivation withholds an offending set with reason (L660–L661, EVERYONE_POSITION = 'everyone' from spec/identity/position.zod.ts); confirmAudienceBindingSuggestion (L995) runs the same gate. Boot: everyoneBaseline in security-plugin.ts L2237–L2243 still refuses via describeHighPrivilegeBits. The old wording 「lint and boot refuse …」 became 「lint … and the binding refuse it」; the boot-time withhold is the binding derivation re-run, so the meaning survives.
  5. The honesty clauses are true — right. 「With none, invited members get nothing until an admin assigns each; no gate counts them」 / mdx 「no gate refuses a package that declares none, or more than one」: appDefaultPermissionSetName (app-default-permission-set.ts) returns the first isDefault set's name or undefined, with no diagnostic; the only isDefault counting in packages/lint is flow out-edges (lint-flow-patterns.ts L1187), not permission sets. Nothing in the cited code refuses zero or many. The published text states the gap instead of implying enforcement.
  6. Token ratchet paid in-file — right. Recomputed with the gate's own convention (ceil(utf8 bytes / 4), check-skills-token-ratchet.mjs header) from the blobs, no local run: cb0a2fcf 10250 B → 2563; f1340f32 10244 B → 2561; pinned ceiling ['skills/objectstack-data/rules/security.md', 2563] (L555) untouched (script not in the diff). Lines 220 → 220, so no re-wrap is counted as savings. Deletions and where their meaning lives: (a) stack.zod.ts path aside — the schema name ObjectStackDefinitionSchema stays; (b) rls.zod.ts parenthetical — the source line stays, the table above it already lists the grammar; (c) field.zod.ts parenthetical — the section heading already names secret + requiredPermissions; (d) ADR-0105 §「Today's two postures」 pointer — the paragraph itself still states both postures and keeps ADR-0093, ADR-0132 and the tenancy-modes URL (the §-level pointer to docs/adr/0105 L68 is the one real loss, minor); (e) 「plugin-security's FieldMasker enforces it. The real channels are:」 → 「(FieldMasker). Two more channels:」 — exactly two channels follow (secret, requiredPermissions); (f) the capability clause folded into the anchor-safe sentence, which is what the predicate refuses in systemPermissions. No meaning the file carried is gone.
  7. Marketplace page regenerated, not hand-edited — right. marketplace.mdx keeps its AUTO-GENERATED — DO NOT EDIT banner; the generator demotes module-doc headings one level (# → ##, ## → ###, matching the neighbouring ### Platform Management Flow), and the nine added mdx lines are the nine source lines verbatim. The CI sync step (Check generated reference docs are in sync with the spec, lint.yml L5469, job Type Check · source gates) is continue-on-error: true, so that job's colour is not the evidence here; the textual comparison above is.
  8. Card closure — right. Fixes #25773 is correct: all three 「What to change」 items (skill text, marketplace source + regenerated page, ratchet paid in-file) land in this PR; the card's two boundary items (gate enforcement, hotcrm#2231) are explicitly not this card.

② Semver level

  • @objectstack/skills (private unset, files: [dist, …], dist is skills/** copied by scripts/sync-catalog.mjs) ships the edited rules/security.md → published text, patch.
  • @objectstack/spec (files[] includes src/**/*.zod.ts) ships the edited doc block → published text, patch.
  • Changeset .changeset/25773-published-package-default-permission-set.md names exactly these two packages at patch, carries line-initial Clause-②: no and Metadata-protocol: none matching the PR body, and its prose matches the diff (requirement, predicate, refusals, honesty clause, in-file trims). Per AGENTS.md Post-Task Checklist step 3, none takes patch and a diff that publishes from a released package is never skip-changeset. Correct.

③ Boundary flags

  • open_questions: [] — nothing to answer.
  • out_of_scope_findings 「carrier: maintainer — no gate refuses a package that declares none or more than one」: ESCALATE (maintainer-only, non-blocking). The ruling orders the text; the card's boundary says a publish-time gate needs the maintainer's own word; the published text already states the absence honestly (①5). Nothing for this PR to do; the question rides to the maintainer with the Tier H approval reading of this PR body (「Acceptance notes」).
  • deviations: attribution link points at the card thread (harness block) and the worktree was kept — neither touches the diff; no action.
  • Dev-reported local gate runs (113 families, check:generated, spec build/typecheck) are the dev's evidence; this record's gate reading is the head's check-runs below.
  • Observation for the lane seat, not a diff fact: the PR is draft: false while the claim (6407727166) said Tier H 「stays draft after an at-tier contract review PASS」. Readiness changes nothing here — the governed queue guard still requires a GOVERNED_APPROVERS approval — but the seat may want to reconcile the claim's wording with the relay's pr_create shape.

Check-runs on f1340f32 (final state):
Read via REST after every run completed (0 in progress, 46 polls). 41 success — among them the two required floors Lint & Repo Gates and TypeScript Type Check, Type Check · source gates (the job carrying the published-skills token ratchet step), Build Docs, Check Changeset, Spec property liveness, Governed Surface Queue Guard (run 40269931909), Test Core 1–8 and aggregate, Build Core, Type Check UI, Type Check · consumer gates, Type Check · workspace, and the card/branch/single-writer guards. 7 skipped (UI shards, Temporal Conformance, dogfood matrix, two opt-in harnesses — path-filtered, not failures). 1 cancelled: Governed Surface Queue Guard run 40269862184 (pull_request, created 2026-10-26T04:35:31Z), superseded 49 s later by the success run 40269931909 of the same workflow on the same head (created 2026-10-26T04:36:20Z) — a concurrency cancellation, not a red. No failure on the head; the second landing condition (all checks green) holds at this reading. Tier H: this PASS is the first landing condition; the PR still waits for a GOVERNED_APPROVERS approval.

Implemented-by: claude/project-thread-2drwet
Reviewed-by: session_014FZ3TJucS1L7BdMe5DAzXC

VERDICT: PASS

Isolated contract reviewer (pm-dispatch CONTRACT_REVIEW_TIER), read-only over the diff, the card and the head's check-runs; record written 2026-10-26T05:05Z.

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/s tooling

Projects

None yet

3 participants