Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/prune-skill-permissions.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 0 additions & 1 deletion content/docs/references/ai/skill.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1717,10 +1717,6 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
triggers: {
label: "Triggers",
description: "When this skill should activate."
},
access: {
label: "Access",
description: "Required permissions to use this skill."
}
},
fields: {
Expand Down Expand Up @@ -1755,10 +1751,6 @@ export const enMetadataForms: NonNullable<TranslationData['metadataForms']> = {
triggerConditions: {
label: "Trigger Conditions",
helpText: "Programmatic conditions (e.g., objectName == \"case\")"
},
permissions: {
label: "Permissions",
helpText: "Required permissions to use this skill"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1717,10 +1717,6 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
triggers: {
label: "Disparadores",
description: "Cuándo debe activarse esta skill."
},
access: {
label: "Acceso",
description: "Permisos necesarios para usar esta skill."
}
},
fields: {
Expand Down Expand Up @@ -1755,10 +1751,6 @@ export const esESMetadataForms: NonNullable<TranslationData['metadataForms']> =
triggerConditions: {
label: "Condiciones disparadoras",
helpText: "Condiciones programáticas (p. ej., objectName == \"case\")"
},
permissions: {
label: "Permisos",
helpText: "Permisos necesarios para usar esta skill"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1717,10 +1717,6 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
triggers: {
label: "トリガー",
description: "このスキルの起動タイミング。"
},
access: {
label: "アクセス",
description: "このスキルの使用に必要な権限。"
}
},
fields: {
Expand Down Expand Up @@ -1755,10 +1751,6 @@ export const jaJPMetadataForms: NonNullable<TranslationData['metadataForms']> =
triggerConditions: {
label: "トリガー条件",
helpText: "プログラム条件(例: objectName == \"case\")"
},
permissions: {
label: "権限",
helpText: "このスキルの使用に必要な権限"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1717,10 +1717,6 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
triggers: {
label: "触发条件",
description: "触发关键词与上下文"
},
access: {
label: "访问",
description: "哪些智能体可使用"
}
},
fields: {
Expand Down Expand Up @@ -1755,10 +1751,6 @@ export const zhCNMetadataForms: NonNullable<TranslationData['metadataForms']> =
triggerConditions: {
label: "触发条件",
helpText: "程序化条件(如 objectName == \"case\")"
},
permissions: {
label: "权限",
helpText: "使用此技能所需的权限"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/liveness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
7 changes: 0 additions & 7 deletions packages/spec/liveness/skill.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
9 changes: 0 additions & 9 deletions packages/spec/src/ai/skill.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
],
});
16 changes: 14 additions & 2 deletions packages/spec/src/ai/skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -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<string, unknown>);
expect('permissions' in parsed).toBe(false);
});

it('should enforce snake_case for skill name', () => {
Expand Down
3 changes: 0 additions & 3 deletions packages/spec/src/ai/skill.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
/**
Expand Down
2 changes: 1 addition & 1 deletion scripts/role-word-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions skills/objectstack-ai/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- os:check -->
Expand Down Expand Up @@ -272,7 +278,6 @@ export default defineSkill({
triggerConditions: [
{ field: 'objectName', operator: 'eq', value: 'support_case' },
],
permissions: ['support_agent', 'support_admin'],
active: true,
});
```
Expand Down
Loading