Skip to content

Commit 5c9cfc8

Browse files
os-zhuangclaude
andcommitted
feat(spec)!: remove SkillSchema.permissions — it never gated anything (#3686)
Owner decision on the enforce-or-prune call: prune. skill.permissions was declared, surfaced in the Studio form under an "Access — Required permissions to use this skill" section, and echoed by the objectui preview — but no runtime read it. The cloud SkillRegistry selects skills by active/triggerConditions/tools only. A security-shaped field that enforces nothing invites an author (or an AI) to believe a skill is gated when it is not — same disposition as agent `visibility` (#1901) and PolicySchema (#2387). Removed: the schema property; the form's whole Access section (it existed only for this field); its generated i18n keys (surgically, not via a full extract rerun, which would drag in unrelated drift); the liveness-ledger entry + count table row; and the `permissions` line from the objectstack-ai skill doc — both its prop table (replaced by a migration note) and its os:check TS block, which compiles against the spec and would otherwise fail check:skill-examples. Added a regression test pinning that an authored `permissions` key is stripped (SkillSchema is non-strict, so it degrades silently rather than breaking boot). Verified: 6688 spec tests; check:liveness / check:docs / check:api-surface / check:skill-docs / check:skill-examples all green. Refs #3686, #1878. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 735f850 commit 5c9cfc8

9 files changed

Lines changed: 54 additions & 33 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: remove `SkillSchema.permissions` — it never gated anything (#3686)
6+
7+
Owner decision on the enforce-or-prune call filed in #3686: **prune**.
8+
9+
`skill.permissions` was declared, surfaced in the Studio authoring form under a
10+
section labelled *"Access — Required permissions to use this skill"*, and echoed
11+
by the objectui preview — but **no runtime ever read it**. The cloud
12+
`SkillRegistry` selects skills by `active` / `triggerConditions` / `tools` only.
13+
A security-shaped field that enforces nothing is worse than no field: it invites
14+
an author (or an AI) to believe a skill is gated when it is not. Same disposition
15+
as agent `visibility` (#1901) and the `PolicySchema` tree (#2387).
16+
17+
Removed: the schema property, the form's whole `Access` section (it existed only
18+
for this field), its generated i18n keys, the liveness-ledger entry, and the
19+
`permissions` line from the objectstack-ai skill doc's `os:check` example. The
20+
objectui preview's "Required Permissions" panel is removed in the companion
21+
objectui change.
22+
23+
**Migration** — gate access where it is actually enforced:
24+
- **Agent level**`access` / `permissions` on `defineAgent` ARE enforced at the
25+
chat route (403 for a caller missing any of them, #1884). Bind the restricted
26+
skill only to a restricted agent.
27+
- **Action level** — gate the underlying actions the skill's tools invoke via
28+
permission sets (ADR-0066).
29+
30+
`SkillSchema` is non-strict, so an existing `permissions:` key is silently
31+
stripped on parse rather than rejected — no boot break, but it stops appearing
32+
anywhere.

content/docs/references/ai/skill.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ const result = Skill.parse(data);
4141
| **tools** | `string[]` || Tool names belonging to this skill (supports trailing wildcard, e.g. `action_*`) |
4242
| **triggerPhrases** | `string[]` | optional | Phrases that activate this skill |
4343
| **triggerConditions** | `{ field: string; operator: Enum<'eq' \| 'neq' \| 'in' \| 'not_in' \| 'contains'>; value: string \| string[] }[]` | optional | Programmatic activation conditions |
44-
| **permissions** | `string[]` | optional | Required permissions or roles |
4544
| **active** | `boolean` || Whether the skill is enabled |
4645
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this skill. |
4746
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |

packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,10 +1717,6 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
17171717
triggers: {
17181718
label: "Triggers",
17191719
description: "When this skill should activate."
1720-
},
1721-
access: {
1722-
label: "Access",
1723-
description: "Required permissions to use this skill."
17241720
}
17251721
},
17261722
fields: {
@@ -1755,10 +1751,6 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
17551751
triggerConditions: {
17561752
label: "Trigger Conditions",
17571753
helpText: "Programmatic conditions (e.g., objectName == \"case\")"
1758-
},
1759-
permissions: {
1760-
label: "Permissions",
1761-
helpText: "Required permissions to use this skill"
17621754
}
17631755
}
17641756
}

packages/spec/liveness/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ EOF
232232
| position | 4 |||| (role's ADR-0090 successor) fully live |
233233
| agent | 13 | 5 | 1 || dead `tenantId` + `planning.strategy`/`allowReplan` REMOVED (#2377) — only `planning.maxIterations` live; autonomy tier experimental; `knowledge` CORRECTED to dead 2026-07 — `search_knowledge` takes `sourceIds` from the LLM's tool-call args, never from the agent record (#1878 §3 recheck) |
234234
| tool | 9 | 1 | 1 || `permissions` dead — tool invocation not permission-gated by it |
235-
| skill | 9 || 1 || `permissions` CORRECTED to dead 2026-07 — the cloud SkillRegistry reads only `active`/`triggerConditions`/`tools`; mirrors the identical `tool.permissions` entry (#1878 §3 recheck) |
235+
| skill | 9 || || `permissions` REMOVED 2026-07 — never permission-gated anything (the cloud SkillRegistry reads only `active`/`triggerConditions`/`tools`); owner call was prune, not enforce. Gate at the agent (`access`/`permissions`, #1884) or on the tools' underlying actions |
236236
| dataset | 19 || 0 || `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) |
237237
| page | 16 ||| 1 | fully live + one planned |
238238
| view | 70 | 0 | 5 || list/form drilled via `children` (#2998 Track B); dead = list.{responsive,performance} + form.{data,defaultSort,aria}, all but aria authorWarn'd; form.{buttons,defaults} now live — objectui ObjectForm folds them onto its flat props (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification (submitBehavior, sharing.lockedBy, list ViewData providers, and the ADR-0021 chart shape — all live now); level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only |

packages/spec/liveness/skill.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,6 @@
4545
"status": "live",
4646
"evidence": "objectui: packages/app-shell/src/views/metadata-admin/previews/SkillPreview.tsx:44",
4747
"note": "LIVE via objectui metadata-admin authoring UI — the 2026-06 audit missed objectui; verified by reading the file."
48-
},
49-
"permissions": {
50-
"status": "dead",
51-
"evidence": "skill.form.ts + objectui SkillPreview.tsx:48 (both authoring-side echo only); no gate in the cloud SkillRegistry",
52-
"authorWarn": true,
53-
"authorHint": "Skill invocation is NOT permission-gated by this list — the registry reads only `active`/`triggerConditions`/`tools`. Agent-level `access`/`permissions` ARE enforced (#1884), so gate at the agent, or gate the underlying tools' actions via permission sets (ADR-0066).",
54-
"note": "CORRECTED 2026-07 (#1878 §3 recheck): was marked live on the evidence of the metadata-admin PREVIEW renderer — an echo of what the author typed, not runtime consumption. Mirrors the identical `tool.permissions` entry. Cloud documents the intent ('Layer permissions via Skill.permissions independent of the agent's permissions', service-ai-studio/src/skills/metadata-authoring-skill.ts:15) without implementing it — enforce-or-prune tracked separately."
5548
}
5649
}
5750
}

packages/spec/src/ai/skill.form.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,5 @@ export const skillForm = defineForm({
4040
{ field: 'triggerConditions', type: 'repeater', helpText: 'Programmatic conditions (e.g., objectName == "case")' },
4141
],
4242
},
43-
{
44-
label: 'Access',
45-
description: 'Required permissions to use this skill.',
46-
collapsible: true,
47-
collapsed: true,
48-
fields: [
49-
{ field: 'permissions', widget: 'string-tags', helpText: 'Required permissions to use this skill' },
50-
],
51-
},
5243
],
5344
});

packages/spec/src/ai/skill.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ describe('SkillSchema', () => {
6464
{ field: 'objectName', operator: 'eq' as const, value: 'order' },
6565
{ field: 'userRole', operator: 'in' as const, value: ['sales', 'support'] },
6666
],
67-
permissions: ['order.manage', 'order.view'],
6867
active: true,
6968
};
7069

@@ -73,7 +72,20 @@ describe('SkillSchema', () => {
7372
expect(result.tools).toHaveLength(4);
7473
expect(result.triggerPhrases).toHaveLength(3);
7574
expect(result.triggerConditions).toHaveLength(2);
76-
expect(result.permissions).toEqual(['order.manage', 'order.view']);
75+
});
76+
77+
it('drops a `permissions` key — skill invocation was never permission-gated (pruned)', () => {
78+
const parsed = SkillSchema.parse({
79+
name: 'order_management',
80+
label: 'Order Management',
81+
instructions: 'x',
82+
tools: ['create_order'],
83+
// Authored against the retired key: the schema is non-strict, so it is
84+
// stripped rather than rejected. Gate at the AGENT (`access`/`permissions`,
85+
// enforced #1884) or on the underlying tools' actions instead.
86+
permissions: ['order.manage'],
87+
} as Record<string, unknown>);
88+
expect('permissions' in parsed).toBe(false);
7789
});
7890

7991
it('should enforce snake_case for skill name', () => {

packages/spec/src/ai/skill.zod.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,6 @@ export const SkillSchema = lazySchema(() => z.object({
117117
*/
118118
triggerConditions: z.array(SkillTriggerConditionSchema).optional().describe('Programmatic activation conditions'),
119119

120-
/** Permission profiles/roles required to use this skill */
121-
permissions: z.array(z.string()).optional().describe('Required permissions or roles'),
122-
123120
/** Whether the skill is enabled */
124121
active: z.boolean().default(true).describe('Whether the skill is enabled'),
125122
/**

skills/objectstack-ai/SKILL.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,15 @@ trigger conditions.
238238
| `instructions` | LLM prompt guidance specific to this skill's context |
239239
| `triggerPhrases` | Natural language phrases that activate the skill |
240240
| `triggerConditions` | Programmatic activation rules |
241-
| `permissions` | Required permission profiles/roles |
242241
| `active` | Is the skill enabled (default: `true`) |
243242

243+
> A skill has **no `permissions` key** — it was removed in 16.x. Skill invocation
244+
> was never gated by it (the registry reads only `active` / `triggerConditions` /
245+
> `tools`), and a security-shaped field that enforces nothing is worse than no
246+
> field at all. Gate access at the **agent** instead — `access` / `permissions` on
247+
> `defineAgent` are enforced at the chat route — or on the underlying actions the
248+
> skill's tools call (permission sets, ADR-0066).
249+
244250
### Skill Example
245251

246252
<!-- os:check -->
@@ -272,7 +278,6 @@ export default defineSkill({
272278
triggerConditions: [
273279
{ field: 'objectName', operator: 'eq', value: 'support_case' },
274280
],
275-
permissions: ['support_agent', 'support_admin'],
276281
active: true,
277282
});
278283
```

0 commit comments

Comments
 (0)