Skip to content

Doc nit: skill.zod.ts's docblock still says an authored permissions key is "silently stripped" — SkillSchema now rejects it outright #7567

Closed

Description

@huangyiirene

Symptom

The prose and the behaviour disagree, and the prose is the wrong one.

packages/spec/src/ai/skill.zod.ts carries a NOTE paragraph in its docblock:

NOTE — there is deliberately NO per-skill permissions field. Access to AI capability is gated at the AGENT level (agent.access / agent.permissions, both enforced at the chat route), and each tool enforces its own authz when invoked. A permissions key authored on a skill is unknown to this schema and silently stripped at parse time — it grants and restricts nothing (ADR-0049: no unenforced security-shaped fields). Do not author one.

But SkillSchema no longer strips it — it refuses it, with a located message keyed on permissions in the same file:

permissions is not a skill key — skill invocation was never permission-gated, so this was stripped in silence and the author believed they had a gate. Gate at the AGENT instead (access / permissions on the agent, enforced since #1884), or on the underlying tools' actions.

The behaviour is strictly safer than the docblock claims (an author who writes permissions is now told, instead of quietly believing they have a gate — the exact hazard the refusal message describes). Only the docblock needs to change: it should say the key is rejected, and point at the refusal message, so nobody reads "silently stripped" and concludes the authoring surface is still lax.

Note the refusal message's own "this was stripped in silence" is fine as written — it narrates the historical reason for the refusal, not current behaviour. The NOTE paragraph is the one that states the stale present tense.

Still present on origin/main as of 2026-08-11.

Root cause

Documentation drift: the permissions key was moved onto the strict unknown-key refusal list without the surrounding docblock being updated in the same pass. Not a behaviour defect — no code change is required, only the prose.

Reproduction

  1. Read the docblock paragraph beginning "NOTE — there is deliberately NO per-skill permissions field" in packages/spec/src/ai/skill.zod.ts.
  2. Read the permissions: entry in the unknown-key refusal messages further down the same file.
  3. Parse a skill authored with a permissions key: it is rejected with that message, not accepted-and-stripped as the docblock states.

Source

Extracted from the QA run #7463 (framework a86db17).

Activity

  1. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings triage: resolved the co-applied pm:queue + finding dual state → pm:queue, and re-routed domain:spec → domain:spec-surface (acceptance-surface criterion, standing instruction ③ / #6298 split): the fix is one docblock paragraph — every input that parses today parses byte-identically after it, so this is the contract's text face, not its semantic face. The refusal behaviour itself is correct and stays untouched.

    • Anchors verified on origin/main @ 34c01a5: stale present-tense "silently stripped at parse time" at packages/spec/src/ai/skill.zod.ts:243; the actual refusal message keyed on permissions at :284. Both as the card describes.
    • Grade: queue-class — concrete, scoped, verified-current; natural member for the spec-surface seat's next sweep card (one line in a truth-sweep clean-up, changeset patch).
    • Dup check: no open card owns this NOTE paragraph; the refusal-list work that introduced the drift is closed history.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 11, 2026
  3. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Claim — spec-surface seat (session session_016R9de1FqP7NvwKvqXi92Gh, seat card #6298). pm:queue → pm:dispatched. Round 4.

    • Branch: claude/issue-7567-skill-permissions-docblock
    • Worktree: dev-owned per-task worktree ../objectstack-issue-7567
    • Domain: domain:spec-surface (triage's routing: one docblock paragraph, every input parses byte-identically after it)
    • File surface: packages/spec/src/ai/skill.zod.ts (the NOTE paragraph, ~:243 at triage's 34c01a5) only — the refusal message at ~:284 is correct and stays; its historical "stripped in silence" narration is fine per the card
    • Container & model: os-dev subagent (in-process), model claude-sonnet-5 (S-mechanical: the card states the exact fix)
    • Serial constraints: nothing in flight touches skill.zod.ts; rounds 1–3 all closed.

    Fix as the card specifies: rewrite the NOTE paragraph's stale present tense — the key is rejected outright with a located message, not silently stripped — and point at the refusal message so a reader doesn't conclude the authoring surface is lax. Report channel: issue comment here with the os-dev-report HTML-comment marker at draft-PR time.


    Generated by Claude Code

  4. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    <!-- os-dev-report -->

    {
      "issue": 7567,
      "status": "done",
      "branch": "claude/issue-7567-skill-permissions-docblock",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7610",
      "premise_still_valid": true,
      "summary": "Rewrote the stale NOTE paragraph in SkillSchema's docblock (packages/spec/src/ai/skill.zod.ts): it claimed an authored `permissions` key is 'silently stripped at parse time', but SkillSchema now REJECTS it outright via guidance.permissions with a located message. The paragraph now states the current truth and points at that refusal message; the refusal message itself (its 'stripped in silence' history narration) was left untouched per the card. Verified byte-identical parse behaviour (git diff touches only comment lines) and zero generated-docs drift.",
      "tests": "pnpm --filter @objectstack/spec typecheck -> green (tsc + check:scripts-typecheck + check:test-typecheck all pass). pnpm --filter @objectstack/spec test -> 375 test files / 9840 tests passed, 0 failed (395s). node scripts/check-nul-bytes.mjs -> 'OK (scanned 7038 text file(s) ... no raw ASCII control bytes)'. pnpm --filter @objectstack/spec gen:schema then check:docs -> '231 generated files in sync with packages/spec' (zero diff; git status confirms no content/docs/** changes). Grepped for tests pinning the stale wording -- none found; skill.test.ts's existing test already asserts the rejection behaviour.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Details

    Before -> after (packages/spec/src/ai/skill.zod.ts, ~line 239-244):

    Before:
    > NOTE — there is deliberately NO per-skill permissions field. Access to AI capability is gated at the AGENT level (agent.access / agent.permissions, both enforced at the chat route), and each tool enforces its own authz when invoked. A permissions key authored on a skill is unknown to this schema and silently stripped at parse time — it grants and restricts nothing (ADR-0049: no unenforced security-shaped fields). Do not author one.

    After:
    > NOTE — there is deliberately NO per-skill permissions field. Access to AI capability is gated at the AGENT level (agent.access / agent.permissions, both enforced at the chat route), and each tool enforces its own authz when invoked. A permissions key authored on a skill is unknown to this schema and is REJECTED outright at parse time, with a located message keyed on permissions (see guidance.permissions below) pointing the author at the agent-level gate instead (ADR-0049: no unenforced security-shaped fields). Do not author one.

    The refusal message at ~line 284 (guidance.permissions) was not touched — its "this was stripped in silence" phrasing narrates history and is correct as written, per the card.

    git diff confirms only comment lines changed (2 removed / 4 added, both inside the block comment) — no schema, key, or behaviour change.

    Docs-render measurement: The NOTE paragraph lives in the JSDoc block directly above export const SkillSchema, which is not the first docblock in the file (that's SkillTriggerConditionSchema's, per the #7113 positioning comment at the top of the file) — and build-docs.ts only renders the first docblock as the page blurb, plus each property's .describe() string into the properties table. Confirmed empirically: content/docs/references/ai/skill.mdx contains no occurrence of "silently stripped" or the NOTE text either before or after the change, and pnpm --filter @objectstack/spec check:docs (after a fresh gen:schema, since json-schema/ is gitignored) reports "231 generated files in sync with packages/spec" with zero diff. No content/docs/** files were touched or need to be.

    Changeset: Added .changeset/skill-permissions-docblock-truth.md (patch, @objectstack/spec), following the #7444 / #7473 / #7565 precedent for describe/TSDoc-only spec docs fixes.

    Labels: No skip-changeset label applied — this PR ships a changeset, so that label does not apply. Read PR #7610 back after opening; Auto Label bot had not yet applied any labels at read time.

    Out-of-scope findings: none.

    Open questions: none — the card's fix was fully mechanical and unambiguous.

    PR left in draft; not flipped ready, auto-merge not armed, not waiting on CI (PM owns convergence per the current reporting contract).


    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions