diff --git a/.changeset/prune-skill-permissions.md b/.changeset/prune-skill-permissions.md new file mode 100644 index 0000000000..dd3ce85820 --- /dev/null +++ b/.changeset/prune-skill-permissions.md @@ -0,0 +1,32 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec)!: remove `SkillSchema.permissions` — it never gated anything (#3686) + +Owner decision on the enforce-or-prune call filed in #3686: **prune**. + +`skill.permissions` was declared, surfaced in the Studio authoring form under a +section labelled *"Access — Required permissions to use this skill"*, and echoed +by the objectui preview — but **no runtime ever read it**. The cloud +`SkillRegistry` selects skills by `active` / `triggerConditions` / `tools` only. +A security-shaped field that enforces nothing is worse than no field: it invites +an author (or an AI) to believe a skill is gated when it is not. Same disposition +as agent `visibility` (#1901) and the `PolicySchema` tree (#2387). + +Removed: the schema property, the form's whole `Access` section (it existed only +for this field), its generated i18n keys, the liveness-ledger entry, and the +`permissions` line from the objectstack-ai skill doc's `os:check` example. The +objectui preview's "Required Permissions" panel is removed in the companion +objectui change. + +**Migration** — gate access where it is actually enforced: +- **Agent level** — `access` / `permissions` on `defineAgent` ARE enforced at the + chat route (403 for a caller missing any of them, #1884). Bind the restricted + skill only to a restricted agent. +- **Action level** — gate the underlying actions the skill's tools invoke via + permission sets (ADR-0066). + +`SkillSchema` is non-strict, so an existing `permissions:` key is silently +stripped on parse rather than rejected — no boot break, but it stops appearing +anywhere. diff --git a/content/docs/references/ai/skill.mdx b/content/docs/references/ai/skill.mdx index 63021f602b..a887aae0c1 100644 --- a/content/docs/references/ai/skill.mdx +++ b/content/docs/references/ai/skill.mdx @@ -41,7 +41,6 @@ const result = Skill.parse(data); | **tools** | `string[]` | ✅ | Tool names belonging to this skill (supports trailing wildcard, e.g. `action_*`) | | **triggerPhrases** | `string[]` | optional | Phrases that activate this skill | | **triggerConditions** | `{ field: string; operator: Enum<'eq' \| 'neq' \| 'in' \| 'not_in' \| 'contains'>; value: string \| string[] }[]` | optional | Programmatic activation conditions | -| **permissions** | `string[]` | optional | Required permissions or roles | | **active** | `boolean` | ✅ | Whether the skill is enabled | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this skill. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index e68b84143c..dac4f8ffb8 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -1717,10 +1717,6 @@ export const enMetadataForms: NonNullable = { triggers: { label: "Triggers", description: "When this skill should activate." - }, - access: { - label: "Access", - description: "Required permissions to use this skill." } }, fields: { @@ -1755,10 +1751,6 @@ export const enMetadataForms: NonNullable = { triggerConditions: { label: "Trigger Conditions", helpText: "Programmatic conditions (e.g., objectName == \"case\")" - }, - permissions: { - label: "Permissions", - helpText: "Required permissions to use this skill" } } } diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index 5404014358..f30b10f859 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -1717,10 +1717,6 @@ export const esESMetadataForms: NonNullable = triggers: { label: "Disparadores", description: "Cuándo debe activarse esta skill." - }, - access: { - label: "Acceso", - description: "Permisos necesarios para usar esta skill." } }, fields: { @@ -1755,10 +1751,6 @@ export const esESMetadataForms: NonNullable = triggerConditions: { label: "Condiciones disparadoras", helpText: "Condiciones programáticas (p. ej., objectName == \"case\")" - }, - permissions: { - label: "Permisos", - helpText: "Permisos necesarios para usar esta skill" } } } diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index e8cc63576d..5f94301d19 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -1717,10 +1717,6 @@ export const jaJPMetadataForms: NonNullable = triggers: { label: "トリガー", description: "このスキルの起動タイミング。" - }, - access: { - label: "アクセス", - description: "このスキルの使用に必要な権限。" } }, fields: { @@ -1755,10 +1751,6 @@ export const jaJPMetadataForms: NonNullable = triggerConditions: { label: "トリガー条件", helpText: "プログラム条件(例: objectName == \"case\")" - }, - permissions: { - label: "権限", - helpText: "このスキルの使用に必要な権限" } } } diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index b6ec4c4fe7..a521220e5a 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -1717,10 +1717,6 @@ export const zhCNMetadataForms: NonNullable = triggers: { label: "触发条件", description: "触发关键词与上下文" - }, - access: { - label: "访问", - description: "哪些智能体可使用" } }, fields: { @@ -1755,10 +1751,6 @@ export const zhCNMetadataForms: NonNullable = triggerConditions: { label: "触发条件", helpText: "程序化条件(如 objectName == \"case\")" - }, - permissions: { - label: "权限", - helpText: "使用此技能所需的权限" } } } diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 4c12057b77..d2fc48e1f7 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -232,7 +232,7 @@ EOF | position | 4 | – | – | – | (role's ADR-0090 successor) fully live | | 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) | | tool | 9 | 1 | 1 | – | `permissions` dead — tool invocation not permission-gated by it | -| 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) | +| 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 | | dataset | 19 | – | 0 | – | `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) | | page | 16 | – | – | 1 | fully live + one planned | | 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 | diff --git a/packages/spec/liveness/skill.json b/packages/spec/liveness/skill.json index a57aad0c45..20e36176f3 100644 --- a/packages/spec/liveness/skill.json +++ b/packages/spec/liveness/skill.json @@ -45,13 +45,6 @@ "status": "live", "evidence": "objectui: packages/app-shell/src/views/metadata-admin/previews/SkillPreview.tsx:44", "note": "LIVE via objectui metadata-admin authoring UI — the 2026-06 audit missed objectui; verified by reading the file." - }, - "permissions": { - "status": "dead", - "evidence": "skill.form.ts + objectui SkillPreview.tsx:48 (both authoring-side echo only); no gate in the cloud SkillRegistry", - "authorWarn": true, - "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).", - "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." } } } diff --git a/packages/spec/src/ai/skill.form.ts b/packages/spec/src/ai/skill.form.ts index fbceed050d..d91a6e311f 100644 --- a/packages/spec/src/ai/skill.form.ts +++ b/packages/spec/src/ai/skill.form.ts @@ -40,14 +40,5 @@ export const skillForm = defineForm({ { field: 'triggerConditions', type: 'repeater', helpText: 'Programmatic conditions (e.g., objectName == "case")' }, ], }, - { - label: 'Access', - description: 'Required permissions to use this skill.', - collapsible: true, - collapsed: true, - fields: [ - { field: 'permissions', widget: 'string-tags', helpText: 'Required permissions to use this skill' }, - ], - }, ], }); diff --git a/packages/spec/src/ai/skill.test.ts b/packages/spec/src/ai/skill.test.ts index 392fb4816f..778ae25c35 100644 --- a/packages/spec/src/ai/skill.test.ts +++ b/packages/spec/src/ai/skill.test.ts @@ -64,7 +64,6 @@ describe('SkillSchema', () => { { field: 'objectName', operator: 'eq' as const, value: 'order' }, { field: 'userRole', operator: 'in' as const, value: ['sales', 'support'] }, ], - permissions: ['order.manage', 'order.view'], active: true, }; @@ -73,7 +72,20 @@ describe('SkillSchema', () => { expect(result.tools).toHaveLength(4); expect(result.triggerPhrases).toHaveLength(3); expect(result.triggerConditions).toHaveLength(2); - expect(result.permissions).toEqual(['order.manage', 'order.view']); + }); + + it('drops a `permissions` key — skill invocation was never permission-gated (pruned)', () => { + const parsed = SkillSchema.parse({ + name: 'order_management', + label: 'Order Management', + instructions: 'x', + tools: ['create_order'], + // Authored against the retired key: the schema is non-strict, so it is + // stripped rather than rejected. Gate at the AGENT (`access`/`permissions`, + // enforced #1884) or on the underlying tools' actions instead. + permissions: ['order.manage'], + } as Record); + expect('permissions' in parsed).toBe(false); }); it('should enforce snake_case for skill name', () => { diff --git a/packages/spec/src/ai/skill.zod.ts b/packages/spec/src/ai/skill.zod.ts index 0f899a9b76..00d434914f 100644 --- a/packages/spec/src/ai/skill.zod.ts +++ b/packages/spec/src/ai/skill.zod.ts @@ -117,9 +117,6 @@ export const SkillSchema = lazySchema(() => z.object({ */ triggerConditions: z.array(SkillTriggerConditionSchema).optional().describe('Programmatic activation conditions'), - /** Permission profiles/roles required to use this skill */ - permissions: z.array(z.string()).optional().describe('Required permissions or roles'), - /** Whether the skill is enabled */ active: z.boolean().default(true).describe('Whether the skill is enabled'), /** diff --git a/scripts/role-word-baseline.json b/scripts/role-word-baseline.json index 6b38802901..77fe59585c 100644 --- a/scripts/role-word-baseline.json +++ b/scripts/role-word-baseline.json @@ -34,7 +34,7 @@ "content/docs/releases/v14.mdx": 5, "content/docs/releases/v16.mdx": 5, "content/docs/ui/forms.mdx": 3, - "skills/objectstack-ai/SKILL.md": 6, + "skills/objectstack-ai/SKILL.md": 5, "skills/objectstack-api/SKILL.md": 1, "skills/objectstack-automation/SKILL.md": 1, "skills/objectstack-data/SKILL.md": 4, diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index 8519991b7e..fc61fbdbfe 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -238,9 +238,15 @@ trigger conditions. | `instructions` | LLM prompt guidance specific to this skill's context | | `triggerPhrases` | Natural language phrases that activate the skill | | `triggerConditions` | Programmatic activation rules | -| `permissions` | Required permission profiles/roles | | `active` | Is the skill enabled (default: `true`) | +> A skill has **no `permissions` key** — it was removed in 16.x. Skill invocation +> was never gated by it (the registry reads only `active` / `triggerConditions` / +> `tools`), and a security-shaped field that enforces nothing is worse than no +> field at all. Gate access at the **agent** instead — `access` / `permissions` on +> `defineAgent` are enforced at the chat route — or on the underlying actions the +> skill's tools call (permission sets, ADR-0066). + ### Skill Example @@ -272,7 +278,6 @@ export default defineSkill({ triggerConditions: [ { field: 'objectName', operator: 'eq', value: 'support_case' }, ], - permissions: ['support_agent', 'support_admin'], active: true, }); ```