@sema-agent/core 7.40.1 → 7.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -0
- package/dist/agents/agent-transcript-tool.d.ts +10 -2
- package/dist/agents/agent-transcript-tool.js +52 -26
- package/dist/agents/list-agents-tool.d.ts +17 -2
- package/dist/agents/list-agents-tool.js +28 -16
- package/dist/agents/peer-session-drain.d.ts +3 -1
- package/dist/agents/peer-session-drain.js +1 -0
- package/dist/agents/subagent-steps.d.ts +12 -2
- package/dist/agents/subagent-steps.js +8 -4
- package/dist/agents/subagent.d.ts +1 -17
- package/dist/agents/subagent.js +12 -24
- package/dist/core/agent-definition.d.ts +19 -40
- package/dist/core/agent-state.d.ts +91 -0
- package/dist/core/agent-state.js +79 -0
- package/dist/core/ask-origin.d.ts +28 -6
- package/dist/core/ask-origin.js +9 -5
- package/dist/core/ask-unresolvable-notice.d.ts +27 -4
- package/dist/core/ask-unresolvable-notice.js +8 -0
- package/dist/core/auto-promote.d.ts +14 -27
- package/dist/core/auto-promote.js +5 -5
- package/dist/core/checkpoint-store.d.ts +11 -12
- package/dist/core/decision-receipt.d.ts +5 -2
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/engine-notice.d.ts +7 -1
- package/dist/core/gate-crash-trace.d.ts +1 -1
- package/dist/core/gate-fold.js +7 -3
- package/dist/core/gate-lanes.js +22 -3
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +20 -6
- package/dist/core/hooks.js +28 -28
- package/dist/core/mcp-tool-meta.d.ts +24 -0
- package/dist/core/mcp-tool-meta.js +17 -0
- package/dist/core/mcp.d.ts +1 -6
- package/dist/core/mcp.js +6 -7
- package/dist/core/mode-policy.d.ts +25 -0
- package/dist/core/mode-policy.js +8 -0
- package/dist/core/protocol-table.d.ts +3 -0
- package/dist/core/protocol-table.js +3 -0
- package/dist/core/reminder-mint.d.ts +21 -1
- package/dist/core/reminder-mint.js +20 -0
- package/dist/core/retired-keys.d.ts +2 -1
- package/dist/core/retired-keys.js +13 -2
- package/dist/core/rule-denial-sentence.d.ts +2 -2
- package/dist/core/runner/call-hook-stations.d.ts +106 -0
- package/dist/core/runner/call-hook-stations.js +118 -0
- package/dist/core/runner/contracts.d.ts +35 -25
- package/dist/core/runner/gate-exit.d.ts +5 -1
- package/dist/core/runner/gate-exit.js +10 -6
- package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
- package/dist/core/runner/prepare-delegation-surface.js +4 -4
- package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
- package/dist/core/runner/prepare-gate-stations.js +13 -70
- package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
- package/dist/core/runner/prepare-inherited-gate.js +5 -3
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
- package/dist/core/runner/prepare-policy-chain.js +14 -25
- package/dist/core/runner/prepare-project-context.d.ts +15 -16
- package/dist/core/runner/prepare-project-context.js +15 -11
- package/dist/core/runner/prepare-run-refs.d.ts +8 -8
- package/dist/core/runner/prepare-run-refs.js +3 -3
- package/dist/core/runner/prepare-safety-scan.js +3 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
- package/dist/core/runner/prepare-suspend-saga.js +2 -1
- package/dist/core/runner/prepare-task.js +14 -12
- package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
- package/dist/core/runner/prepare-turn-wiring.js +16 -13
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-apply.js +12 -23
- package/dist/core/runner/resume-checkpoint-screen.js +3 -0
- package/dist/core/runner/run-leg.js +61 -47
- package/dist/core/runner/runtask.d.ts +1 -1
- package/dist/core/runner/session-rule-policy.d.ts +7 -19
- package/dist/core/runner/session-rule-policy.js +1 -2
- package/dist/core/runner/skill-base-dir.d.ts +10 -0
- package/dist/core/runner/skill-base-dir.js +4 -0
- package/dist/core/runner/synthetic-tools.d.ts +11 -17
- package/dist/core/runner/synthetic-tools.js +7 -8
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +2 -0
- package/dist/core/session-policy-store.d.ts +63 -11
- package/dist/core/session-policy-store.js +52 -6
- package/dist/core/skill-grant.d.ts +239 -0
- package/dist/core/skill-grant.js +313 -0
- package/dist/core/skills-directory.d.ts +5 -34
- package/dist/core/skills-directory.js +9 -58
- package/dist/core/task-registry-agent.d.ts +14 -2
- package/dist/core/task-registry-agent.js +23 -8
- package/dist/core/task-registry-shared.d.ts +6 -0
- package/dist/core/task-registry.d.ts +16 -0
- package/dist/core/task-registry.js +19 -6
- package/dist/core/task-spec.d.ts +10 -0
- package/dist/core/tool-face.d.ts +3 -2
- package/dist/core/tool-name-aliases.d.ts +38 -12
- package/dist/core/tool-name-aliases.js +16 -0
- package/dist/core/tool-name-readings.d.ts +6 -2
- package/dist/core/tool-name-readings.js +6 -4
- package/dist/core/tool-policy.d.ts +2 -2
- package/dist/core/tool-registry.d.ts +1 -4
- package/dist/core/tool-registry.js +0 -3
- package/dist/core/tool-spec.d.ts +11 -13
- package/dist/core/tools.d.ts +1 -1
- package/dist/core/trace.d.ts +15 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/harness/agent-harness.d.ts +12 -1
- package/dist/engine/harness/agent-harness.js +17 -13
- package/dist/engine/loop/agent-loop.d.ts +16 -1
- package/dist/engine/loop/agent-loop.js +9 -15
- package/dist/engine/loop/types.d.ts +2 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/stores/file/adoption/adopt.js +23 -16
- package/dist/stores/file/index.d.ts +8 -2
- package/dist/stores/file/index.js +2 -2
- package/dist/stores/file/mailbox-store.js +2 -2
- package/dist/stores/file/permission-rule-adopt.js +10 -1
- package/dist/stores/file/permission-rule-store.d.ts +6 -4
- package/dist/stores/file/permission-rule-store.js +127 -59
- package/dist/stores/file/session-policy-store.d.ts +86 -14
- package/dist/stores/file/session-policy-store.js +135 -118
- package/dist/stores/file/task-list-store.d.ts +5 -0
- package/dist/stores/file/task-list-store.js +18 -4
- package/dist/stores/file/usage-window-store.d.ts +9 -1
- package/dist/stores/file/usage-window-store.js +47 -33
- package/dist/tools/fs/safety.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +45 -5
- package/wire-changes/core@7.41.0.json +1865 -0
- package/dist/core/runner/active-skill-scope.d.ts +0 -58
- package/dist/core/runner/active-skill-scope.js +0 -133
- package/dist/core/skill-tool-specifier.d.ts +0 -80
- package/dist/core/skill-tool-specifier.js +0 -57
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { PermissionResult, ToolCallRequest } from "./tool-policy.js";
|
|
2
|
+
import { type PersistedRule } from "./permission-rule-model.js";
|
|
3
|
+
import type { PermissionMode } from "./ask-origin.js";
|
|
4
|
+
import { type PermissionSeatInputs, type PermissionSeatReading } from "./mode-policy.js";
|
|
5
|
+
import type { AgentTool } from "../internal/harness-types.js";
|
|
6
|
+
import type { EngineNotice } from "./engine-notice.js";
|
|
7
|
+
/** Who wrote a row: the `Skill` tool on a successful activation, prepare for a skill the person invoked by name, or a
|
|
8
|
+
* delegated child's seed from its parent. */
|
|
9
|
+
export declare const SKILL_GRANT_ORIGINS: readonly ["model", "user", "inherited"];
|
|
10
|
+
export type SkillGrantOrigin = (typeof SKILL_GRANT_ORIGINS)[number];
|
|
11
|
+
/** The PORTABLE form of one row — what a delegated child captures and what a park checkpoint section freezes: the
|
|
12
|
+
* skill, the list it came from, and the entry as the skill spells it. The compiled reading is re-derived wherever a
|
|
13
|
+
* record is seeded (never trusted off the record). */
|
|
14
|
+
export interface SkillGrantRecord {
|
|
15
|
+
readonly skill: string;
|
|
16
|
+
readonly behavior: "allow" | "deny";
|
|
17
|
+
readonly entry: string;
|
|
18
|
+
readonly origin?: SkillGrantOrigin;
|
|
19
|
+
}
|
|
20
|
+
/** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names. */
|
|
21
|
+
export declare const SKILL_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"];
|
|
22
|
+
export type SkillEntryInertReason = (typeof SKILL_ENTRY_INERT_REASONS)[number];
|
|
23
|
+
/** One entry the grammar reads (kind closed): a whole non-shell tool; a protocol namespace; the whole shell tool; a shell
|
|
24
|
+
* command rule (`interpreter` = an interpreter-headed open prefix); the Read directory form; a deny-side path pattern. */
|
|
25
|
+
type CompiledEntry = {
|
|
26
|
+
readonly kind: "tool";
|
|
27
|
+
readonly name: string;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: "namespace";
|
|
30
|
+
readonly pattern: string;
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: "shell";
|
|
33
|
+
readonly name: string;
|
|
34
|
+
} | {
|
|
35
|
+
readonly kind: "command";
|
|
36
|
+
readonly rule: PersistedRule;
|
|
37
|
+
readonly interpreter: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: "read_dir";
|
|
40
|
+
readonly rule: PersistedRule;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: "path";
|
|
43
|
+
readonly rule: PersistedRule;
|
|
44
|
+
};
|
|
45
|
+
/** One row of the table. */
|
|
46
|
+
export interface SkillGrantRow {
|
|
47
|
+
readonly skill: string;
|
|
48
|
+
readonly origin: SkillGrantOrigin;
|
|
49
|
+
readonly behavior: "allow" | "deny";
|
|
50
|
+
readonly entry: string;
|
|
51
|
+
readonly compiled: CompiledEntry;
|
|
52
|
+
}
|
|
53
|
+
/** An entry the grammar reads as granting or refusing nothing. */
|
|
54
|
+
export interface SkillInertEntry {
|
|
55
|
+
readonly skill: string;
|
|
56
|
+
readonly list: "allowed-tools" | "disallowed-tools";
|
|
57
|
+
readonly entry: string;
|
|
58
|
+
readonly reason: SkillEntryInertReason;
|
|
59
|
+
}
|
|
60
|
+
/** The two lists a skill declares (the shape of a `SkillSpec`, read by name). */
|
|
61
|
+
export interface SkillGrantSource {
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly allowedTools?: readonly string[];
|
|
64
|
+
readonly disallowedTools?: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The skill a `Skill` call's arguments name among `skills`: the definition whose `name` equals `args.skill` exactly —
|
|
68
|
+
* case-sensitive, no alias, no trimming, first of a name (a prepared table is already deduplicated) — or undefined (no such
|
|
69
|
+
* name, or a `skill` member that is not a string). The ONE resolution: the `Skill` tool activates through it, the card is
|
|
70
|
+
* asked through it ({@link skillSeatReader}), a parked approval is bound and judged through it ({@link skillGrantDigest}) and
|
|
71
|
+
* the person's invoked names are read through it ({@link seedSkillGrants}).
|
|
72
|
+
*/
|
|
73
|
+
export declare function skillNamedBy<T extends Pick<SkillGrantSource, "name">>(args: unknown, skills: readonly T[]): T | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases: the skill's name
|
|
76
|
+
* as the table spells it ({@link skillNamedBy} resolved it, so it equals the call's `skill` exactly) and its two lists in the
|
|
77
|
+
* rule grammar's canonical spelling (`Tool()` / `Tool(*)` read as the bare name), de-duplicated and sorted, hashed as the
|
|
78
|
+
* approval binding is hashed (`boundInputHashOf`). `undefined` — no skill named — digests as a nameless skill declaring
|
|
79
|
+
* nothing (activating it grants nothing). Any change to the name or to either list changes it; the body does not. The read
|
|
80
|
+
* point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation).
|
|
81
|
+
*/
|
|
82
|
+
export declare function skillGrantDigest(skill: Pick<SkillGrantSource, "name" | "allowedTools" | "disallowedTools"> | undefined): string;
|
|
83
|
+
/**
|
|
84
|
+
* The digest a parked call to the engine's `Skill` tool binds — written by the park beside the row's input binding — or
|
|
85
|
+
* undefined for any other call, and when no skill is loaded (no engine `Skill` tool is mounted, so a call by that name is
|
|
86
|
+
* not an activation).
|
|
87
|
+
*/
|
|
88
|
+
export declare function skillCallGrantDigest(call: {
|
|
89
|
+
readonly toolName: string;
|
|
90
|
+
}, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string): string | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* The approving resume of a parked call, judged against the card it was asked under. A row is a `Skill` approval when it
|
|
93
|
+
* carries a digest (the park said so), or when the resume would run the engine's `Skill` tool on it (skills loaded, the
|
|
94
|
+
* row's wire name). For such a row: the refusal sentence when the skill the arguments name — as the resume's skills define
|
|
95
|
+
* it, none loaded included — does not digest as the row's `boundGrantDigest`, or the row carries none (parked before the
|
|
96
|
+
* binding); undefined otherwise. ONE judgement (the digest), judged at two sites: the claim (`site: "claim"`, the decided
|
|
97
|
+
* bytes — the row stays pending: deny it, then activate again) and the resumed execution boundary (`site: "execution"`, the
|
|
98
|
+
* bytes that will run, after every station that can rewrite them — the approval is spent: the call does not run). The
|
|
99
|
+
* sentence says which side of the digest differs, read off the row's own arguments: the arguments name another skill than
|
|
100
|
+
* the card's (`row.args`), or the card's skill no longer declares the lists the card showed (or is not served). Names the
|
|
101
|
+
* skills, never a path; says the way out.
|
|
102
|
+
*/
|
|
103
|
+
export declare function parkedSkillApprovalRefusal(row: {
|
|
104
|
+
readonly toolName: string;
|
|
105
|
+
readonly args: unknown;
|
|
106
|
+
readonly boundGrantDigest?: string;
|
|
107
|
+
}, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string, site?: "claim" | "execution"): string | undefined;
|
|
108
|
+
/** A resume's skills read ONCE: each definition copied with its two lists, so the parked Skill approval's binding and the
|
|
109
|
+
* resumed leg's prepare read one value — a later edit to the caller's own objects or arrays reaches neither. */
|
|
110
|
+
export declare function detachSkillLists<T extends SkillGrantSource>(skills: readonly T[]): T[];
|
|
111
|
+
/** Does this skill declare a list (either, non-empty)? The one test of "the Skill card is asked for this skill". */
|
|
112
|
+
export declare function declaresSkillLists(skill: Pick<SkillGrantSource, "allowedTools" | "disallowedTools">): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Compile one skill's two lists into rows (`origin` stamped on each) and the entries that read as nothing. An allow list
|
|
115
|
+
* holding `*` voids that list whole (every other entry on it is reported too: the list grants nothing — the safe direction).
|
|
116
|
+
* A `*` on the deny list reads as nothing by itself and the rest of that list still refuses: a deny list never loses an
|
|
117
|
+
* entry to a wildcard beside it.
|
|
118
|
+
*/
|
|
119
|
+
export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin): {
|
|
120
|
+
rows: SkillGrantRow[];
|
|
121
|
+
inert: SkillInertEntry[];
|
|
122
|
+
};
|
|
123
|
+
/** The call facts a content row is judged against — the persisted lane's own bases. */
|
|
124
|
+
export interface SkillGrantJudgeContext {
|
|
125
|
+
readonly root: string | undefined;
|
|
126
|
+
readonly sessionId: string | undefined;
|
|
127
|
+
readonly liveCwd: () => string | undefined;
|
|
128
|
+
readonly home: string | undefined;
|
|
129
|
+
}
|
|
130
|
+
/** What spoke for a call: the skill and the entries it stands on. */
|
|
131
|
+
export interface SkillGrantHit {
|
|
132
|
+
readonly skill: string;
|
|
133
|
+
readonly entries: readonly string[];
|
|
134
|
+
}
|
|
135
|
+
/** The refusal a deny row writes. */
|
|
136
|
+
export declare function skillDenyDecision(hit: SkillGrantHit): PermissionResult;
|
|
137
|
+
/** The run's deny rows over `req` through `lane` — the refusal they write, or undefined (no lane, no row). The one
|
|
138
|
+
* reading the gate's regular decision, its approved-edit round and the resumed execution boundary share. */
|
|
139
|
+
export declare function skillRowRefusal(lane: Pick<SkillGrantLane, "denying"> | undefined, req: ToolCallRequest): PermissionResult | undefined;
|
|
140
|
+
/** The allow a grant writes (the allow layer's skill member). */
|
|
141
|
+
export declare function skillAllowDecision(hit: SkillGrantHit, updatedInput: unknown): PermissionResult;
|
|
142
|
+
/**
|
|
143
|
+
* The Skill card's question: the reference implementation's sentence (`Execute skill: <name>`), and — when approving
|
|
144
|
+
* releases anything — the entries it releases for the rest of the run in this mode (an entry that grants nothing, or
|
|
145
|
+
* stands down in this mode, is not named). Deployment text, sanitized and capped.
|
|
146
|
+
*/
|
|
147
|
+
export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined): string;
|
|
148
|
+
/** The face the gate and the resumed execution boundary read the table through, bound to the leg's call facts. */
|
|
149
|
+
export interface SkillGrantLane {
|
|
150
|
+
/** The allow layer's skill member: what grants this call in this mode, or undefined. */
|
|
151
|
+
allowing(req: ToolCallRequest, mode: PermissionMode | undefined): SkillGrantHit | undefined;
|
|
152
|
+
/** The run's deny rows: what refuses this call, or undefined. */
|
|
153
|
+
denying(req: ToolCallRequest): SkillGrantHit | undefined;
|
|
154
|
+
}
|
|
155
|
+
/** The gate inputs the run's skill rows ride (a member group of `ToolGateInput`). */
|
|
156
|
+
export interface SkillGateSeats {
|
|
157
|
+
/**
|
|
158
|
+
* The active skills' rows for this run. DENY rows (a skill's `disallowed-tools`) are read where a person's deny rows are
|
|
159
|
+
* read — on every decision right after them, and on an approved edit's bytes — and refuse with `decisionReason: "skill"`
|
|
160
|
+
* (`deniedBy: "policy"`). ALLOW rows are the allow layer's member beside a person's stored allow rows, under the same
|
|
161
|
+
* consumption predicate: they clear only an ask an allow rule may clear (never a mandated one, a hook's, an ask rule's,
|
|
162
|
+
* governance's or a marked call's) and are not re-read on an approved edit. `onAllowed` observes a call they cleared (the
|
|
163
|
+
* allow-side attribution; never affects the outcome). Absent ⇒ no skill rows.
|
|
164
|
+
*/
|
|
165
|
+
skillGrants?: SkillGrantLane & {
|
|
166
|
+
onAllowed?: (info: {
|
|
167
|
+
toolName: string;
|
|
168
|
+
toolCallId: string;
|
|
169
|
+
skill: string;
|
|
170
|
+
entries: readonly string[];
|
|
171
|
+
}) => void;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** The `Skill` tool's per-call seat over the loaded skills: a skill that declares a list states no permission of its own. */
|
|
175
|
+
export type SkillSeatReader = (args: unknown, mode: PermissionMode | undefined) => PermissionSeatReading | undefined;
|
|
176
|
+
/** The reader over these skills (`undefined` when none is loaded): `Execute skill: <name>` for a skill declaring a list. */
|
|
177
|
+
export declare function skillSeatReader(skills: readonly SkillGrantSource[]): SkillSeatReader | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* The gate's seat inputs for one call, composed in ONE place: the mounted instance's static seat (`calledName`), the `Skill`
|
|
180
|
+
* tool's per-call reader in the leg's live mode (only when the call's wire name is the engine's `Skill` — `skillToolName`),
|
|
181
|
+
* and the run's skill rows with their attribution sink.
|
|
182
|
+
*/
|
|
183
|
+
export declare function skillGateWiring(opts: {
|
|
184
|
+
tools: readonly AgentTool[];
|
|
185
|
+
skillToolName: string;
|
|
186
|
+
seatOf: SkillSeatReader | undefined;
|
|
187
|
+
mode: () => PermissionMode | undefined;
|
|
188
|
+
lane: SkillGrantLane;
|
|
189
|
+
onAllowed: NonNullable<SkillGateSeats["skillGrants"]>["onAllowed"];
|
|
190
|
+
}): (calledName: string, wireName: string) => PermissionSeatInputs & SkillGateSeats;
|
|
191
|
+
/**
|
|
192
|
+
* Seed a task's table at prepare, before any call is gated: the rows from outside this leg's own writers (`inherited` — a
|
|
193
|
+
* parent's capture, held until this child ends, and a resumed leg's frozen rows, which keep the word `model` where the
|
|
194
|
+
* model wrote them) and the skills the person invoked by name (`user` — the invocation is the statement, so no card). Every invoked
|
|
195
|
+
* name must be one of the loaded skills; anything else (or a value that is not a list of names) refuses the run loudly
|
|
196
|
+
* (`config.user_invoked_skill_unknown`), never dropped. Returns the entries the grammar reads as nothing, across every
|
|
197
|
+
* loaded skill — the start-of-run notice's content.
|
|
198
|
+
*/
|
|
199
|
+
export declare function seedSkillGrants(table: SkillGrantTable, opts: {
|
|
200
|
+
skills: readonly SkillGrantSource[];
|
|
201
|
+
userInvokedSkills: unknown;
|
|
202
|
+
inherited: readonly SkillGrantRecord[] | undefined;
|
|
203
|
+
}): readonly SkillInertEntry[];
|
|
204
|
+
/** The per-task table. Identity is the contract: the `Skill` tool, prepare's seeds, the tool ctx's snapshot and the
|
|
205
|
+
* lane all hold this one object. */
|
|
206
|
+
export declare class SkillGrantTable {
|
|
207
|
+
private rowsHeld;
|
|
208
|
+
/** The table never holds the same row twice (skill, origin, list, entry) — one rule for both writers: a re-activation, a
|
|
209
|
+
* capture re-supplied beside the frozen rows that already carry it. Keyed on the ROW, not the skill: a definition that
|
|
210
|
+
* changed across a park writes its new rows when it is activated again. */
|
|
211
|
+
private hold;
|
|
212
|
+
/** Write a skill's rows (rows already held are not held twice). Returns the entries that read as nothing. */
|
|
213
|
+
activate(skill: SkillGrantSource, origin: SkillGrantOrigin): readonly SkillInertEntry[];
|
|
214
|
+
/** Seed portable records (a delegated child's capture, a resumed leg's frozen rows): a record the model wrote keeps that
|
|
215
|
+
* word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded. */
|
|
216
|
+
seed(records: readonly SkillGrantRecord[], origin: SkillGrantOrigin): void;
|
|
217
|
+
/** The rows as portable records (a copy). */
|
|
218
|
+
records(): readonly SkillGrantRecord[];
|
|
219
|
+
/** The lane over this table, bound to the leg's call facts. */
|
|
220
|
+
lane(ctx: SkillGrantJudgeContext): SkillGrantLane;
|
|
221
|
+
}
|
|
222
|
+
/** Read a delegated child's capture ({@link readCarriedRecords}): `undefined` refuses the delegation. */
|
|
223
|
+
export declare function readSkillGrantCapture(snapshot: readonly unknown[] | undefined): readonly SkillGrantRecord[] | undefined;
|
|
224
|
+
/** The section a park freezes (`CheckpointState.skillGrants`): the table's rows under the frozen origins, or undefined
|
|
225
|
+
* when there are none (an absent section IS zero rows on every reader). */
|
|
226
|
+
export declare function frozenSkillGrants(table: Pick<SkillGrantTable, "records">): SkillGrantRecord[] | undefined;
|
|
227
|
+
/** Read a park's frozen section ({@link readCarriedRecords}): absent ⇒ none; `undefined` ⇒ damaged — the resume refuses
|
|
228
|
+
* it before the claim and prepare never seeds from it. */
|
|
229
|
+
export declare function readFrozenSkillGrants(section: unknown): readonly SkillGrantRecord[] | undefined;
|
|
230
|
+
/** The start-of-run notice for entries that grant (or refuse) nothing. */
|
|
231
|
+
export declare const SKILL_ENTRY_NOT_GRANTED_CODE = "skills.allowed_tool_not_granted";
|
|
232
|
+
/** The refusal for a `TaskSpec.userInvokedSkills` name the run does not carry (or a value that is not a list of names). */
|
|
233
|
+
export declare const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
|
|
234
|
+
/**
|
|
235
|
+
* The notice for entries this grammar reads as nothing — one per prepared task, delivered only when one exists. The
|
|
236
|
+
* deployment's operator owns the skill files; the run goes on (an entry that grants nothing makes it ask more).
|
|
237
|
+
*/
|
|
238
|
+
export declare function skillEntriesNotGrantedNotice(inert: readonly SkillInertEntry[], sessionId: string): EngineNotice;
|
|
239
|
+
export {};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { heldBy, wireOf } from "./tool-name-readings.js";
|
|
2
|
+
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
3
|
+
import { protocolOf } from "./protocol-table.js";
|
|
4
|
+
import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
|
|
5
|
+
import { COMMAND_RULE_TOOL, READ_RULE_TOOL, findAdmittingRule, laneToolNamesOf, parseRuleText, programRunReachOf, ruleToolGrammarOf } from "./permission-rule-model.js";
|
|
6
|
+
import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
|
|
7
|
+
import { mountedToolPermissionSeat } from "./mode-policy.js";
|
|
8
|
+
import { inlineUntrusted } from "./untrusted-text.js";
|
|
9
|
+
import { boundInputHashOf } from "./canonical-json.js";
|
|
10
|
+
export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
|
|
11
|
+
const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
|
|
12
|
+
export const SKILL_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar"]);
|
|
13
|
+
export function skillNamedBy(args, skills) {
|
|
14
|
+
const named = args?.skill;
|
|
15
|
+
return typeof named === "string" ? skills.find((s) => s.name === named) : undefined;
|
|
16
|
+
}
|
|
17
|
+
export function skillGrantDigest(skill) {
|
|
18
|
+
const canonical = (entries) => [...new Set((entries ?? []).map((e) => {
|
|
19
|
+
const p = parsePermissionRule(e);
|
|
20
|
+
return p.ruleContent === undefined ? p.toolName : `${p.toolName}(${p.ruleContent})`;
|
|
21
|
+
}))].sort();
|
|
22
|
+
return boundInputHashOf({ skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) });
|
|
23
|
+
}
|
|
24
|
+
export function skillCallGrantDigest(call, args, skills, skillToolName) {
|
|
25
|
+
return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName ? skillGrantDigest(skillNamedBy(args, skills)) : undefined;
|
|
26
|
+
}
|
|
27
|
+
export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, site = "claim") {
|
|
28
|
+
if (row.boundGrantDigest === undefined && skillCallGrantDigest(row, args, skills, skillToolName) === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (skillGrantDigest(skillNamedBy(args, skills)) === row.boundGrantDigest)
|
|
31
|
+
return undefined;
|
|
32
|
+
const nameIn = (a) => a?.skill;
|
|
33
|
+
const shown = (n) => inlineUntrusted(typeof n === "string" ? n : String(n), 120);
|
|
34
|
+
const chosen = shown(nameIn(args));
|
|
35
|
+
const why = row.boundGrantDigest === undefined
|
|
36
|
+
? `this parked Skill activation of "${chosen}" carries no digest of the allowed-tools / disallowed-tools it was asked under (it was parked before Skill approvals bound them), so it cannot cover what the activation grants or refuses`
|
|
37
|
+
: nameIn(args) !== nameIn(row.args)
|
|
38
|
+
? `this Skill approval was given on the card for "${shown(nameIn(row.args))}", and ${site === "claim" ? "this decision selects" : "the call would now activate"} "${chosen}" — not the Skill that card asked about, so the approval does not cover activating it`
|
|
39
|
+
: `"${chosen}" no longer declares the allowed-tools / disallowed-tools this Skill approval was asked under — as this resume serves it (one it does not load counts as absent), it changed after the approval was given, so the approval does not cover what activating it would now grant or refuse`;
|
|
40
|
+
return site === "claim"
|
|
41
|
+
? `${why} — refusing pre-CAS (nothing ran; the checkpoint stays pending). Decide this checkpoint as deny and re-activate it: the new card shows the current lists.`
|
|
42
|
+
: `The approved Skill call was not executed: ${why}. This approval is spent — to use "${chosen}", have the model activate it again: the new card shows its lists.`;
|
|
43
|
+
}
|
|
44
|
+
export function detachSkillLists(skills) {
|
|
45
|
+
return skills.map((s) => (typeof s === "object" && s !== null ? { ...s, ...(Array.isArray(s.allowedTools) ? { allowedTools: [...s.allowedTools] } : {}), ...(Array.isArray(s.disallowedTools) ? { disallowedTools: [...s.disallowedTools] } : {}) } : s));
|
|
46
|
+
}
|
|
47
|
+
export function declaresSkillLists(skill) {
|
|
48
|
+
return (skill.allowedTools?.length ?? 0) > 0 || (skill.disallowedTools?.length ?? 0) > 0;
|
|
49
|
+
}
|
|
50
|
+
function ruleRowOf(parsed) {
|
|
51
|
+
return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "skill", counter: 1 }, origin: "user", createdAt: "" }] };
|
|
52
|
+
}
|
|
53
|
+
function compileEntry(raw, behavior) {
|
|
54
|
+
const parsed = parsePermissionRule(raw);
|
|
55
|
+
const name = parsed.toolName;
|
|
56
|
+
if (name === "*")
|
|
57
|
+
return "wildcard_all";
|
|
58
|
+
if (parsed.ruleContent === undefined) {
|
|
59
|
+
const ns = protocolOf(name);
|
|
60
|
+
if (ns !== undefined) {
|
|
61
|
+
if (name.slice(ns.prefix.length).split("__")[0]?.includes("*") === true)
|
|
62
|
+
return "namespace_wildcard";
|
|
63
|
+
if (isNamespacedCoveringRuleName(name))
|
|
64
|
+
return { kind: "namespace", pattern: name };
|
|
65
|
+
return { kind: "tool", name };
|
|
66
|
+
}
|
|
67
|
+
return COARSE_SHELL_TOOLS.includes(name) ? { kind: "shell", name } : { kind: "tool", name };
|
|
68
|
+
}
|
|
69
|
+
if (name === COMMAND_RULE_TOOL) {
|
|
70
|
+
const allowRead = parseRuleText(raw, behavior);
|
|
71
|
+
if ("rule" in allowRead)
|
|
72
|
+
return { kind: "command", rule: ruleRowOf(allowRead.rule), interpreter: false };
|
|
73
|
+
if (behavior === "allow" && allowRead.reject.code === "invalid.bare_interpreter_prefix") {
|
|
74
|
+
const wide = parseRuleText(raw, "deny");
|
|
75
|
+
if ("rule" in wide)
|
|
76
|
+
return { kind: "command", rule: ruleRowOf({ ...wide.rule, behavior: "allow" }), interpreter: true };
|
|
77
|
+
}
|
|
78
|
+
return "grammar";
|
|
79
|
+
}
|
|
80
|
+
if (ruleToolGrammarOf(name) === "path") {
|
|
81
|
+
const read = parseRuleText(raw, behavior);
|
|
82
|
+
if ("rule" in read) {
|
|
83
|
+
if (read.rule.match === "subpath" && read.rule.tool === READ_RULE_TOOL && behavior === "allow")
|
|
84
|
+
return { kind: "read_dir", rule: ruleRowOf(read.rule) };
|
|
85
|
+
if (behavior === "deny")
|
|
86
|
+
return { kind: "path", rule: ruleRowOf(read.rule) };
|
|
87
|
+
}
|
|
88
|
+
return "path_pattern";
|
|
89
|
+
}
|
|
90
|
+
if (/^domain:/.test(parsed.ruleContent))
|
|
91
|
+
return "domain";
|
|
92
|
+
return "content_unsupported";
|
|
93
|
+
}
|
|
94
|
+
export function compileSkillGrant(skill, origin) {
|
|
95
|
+
const rows = [];
|
|
96
|
+
const inert = [];
|
|
97
|
+
for (const [list, behavior, entries] of [
|
|
98
|
+
["allowed-tools", "allow", skill.allowedTools ?? []],
|
|
99
|
+
["disallowed-tools", "deny", skill.disallowedTools ?? []],
|
|
100
|
+
]) {
|
|
101
|
+
const voided = behavior === "allow" && entries.some((e) => parsePermissionRule(e).toolName === "*");
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const compiled = voided ? "wildcard_all" : compileEntry(entry, behavior);
|
|
104
|
+
if (typeof compiled === "string")
|
|
105
|
+
inert.push({ skill: skill.name, list, entry, reason: compiled });
|
|
106
|
+
else
|
|
107
|
+
rows.push({ skill: skill.name, origin, behavior, entry, compiled });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { rows, inert };
|
|
111
|
+
}
|
|
112
|
+
function standsDownIn(row, mode, req) {
|
|
113
|
+
if (mode !== "auto")
|
|
114
|
+
return false;
|
|
115
|
+
if (row.compiled.kind === "shell" || (row.compiled.kind === "command" && row.compiled.interpreter))
|
|
116
|
+
return true;
|
|
117
|
+
return row.compiled.kind === "tool" && req !== undefined && answersTo(req, (n) => COARSE_SHELL_TOOLS.includes(n));
|
|
118
|
+
}
|
|
119
|
+
function answersTo(req, has) {
|
|
120
|
+
return heldBy("skillGrant.names", { has }, req);
|
|
121
|
+
}
|
|
122
|
+
function nameRowReaches(compiled, req) {
|
|
123
|
+
const covers = compiled.kind === "namespace" ? (n) => namespacedRuleNameCovers(compiled.pattern, n) : compiled.kind === "tool" || compiled.kind === "shell" ? (n) => n === compiled.name : undefined;
|
|
124
|
+
return covers !== undefined && answersTo(req, covers);
|
|
125
|
+
}
|
|
126
|
+
function commandOf(req) {
|
|
127
|
+
const tool = laneToolNamesOf(req).find((n) => ruleToolGrammarOf(n) === "command");
|
|
128
|
+
const command = req.args?.command;
|
|
129
|
+
return tool !== undefined && typeof command === "string" ? { tool, command } : undefined;
|
|
130
|
+
}
|
|
131
|
+
function judge(rows, req, behavior, ctx) {
|
|
132
|
+
const live = rows.filter((r) => r.behavior === behavior);
|
|
133
|
+
if (live.length === 0)
|
|
134
|
+
return undefined;
|
|
135
|
+
for (const row of live)
|
|
136
|
+
if (nameRowReaches(row.compiled, req))
|
|
137
|
+
return { skill: row.skill, entries: [row.entry] };
|
|
138
|
+
const call = commandOf(req);
|
|
139
|
+
const liveCwd = ctx.liveCwd();
|
|
140
|
+
if (call !== undefined) {
|
|
141
|
+
const byRule = new Map();
|
|
142
|
+
for (const r of live)
|
|
143
|
+
if (r.compiled.kind === "command")
|
|
144
|
+
byRule.set(r.compiled.rule, r);
|
|
145
|
+
if (behavior === "allow") {
|
|
146
|
+
const covering = findAdmittingRule([...byRule.keys()], { tool: call.tool, command: call.command, cwd: ctx.root, ...(ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : {}), ...(liveCwd !== undefined ? { execCwd: liveCwd } : {}) });
|
|
147
|
+
const hits = (covering ?? []).flatMap((rule) => byRule.get(rule) ?? []);
|
|
148
|
+
const first = hits[0];
|
|
149
|
+
if (first !== undefined)
|
|
150
|
+
return { skill: first.skill, entries: hits.map((r) => r.entry) };
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
for (const [rule, row] of byRule) {
|
|
154
|
+
if (programRunReachOf(rule, call.command).reach !== "not_reached")
|
|
155
|
+
return { skill: row.skill, entries: [row.entry] };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const pathKind = behavior === "allow" ? "read_dir" : "path";
|
|
160
|
+
const pathRows = live.filter((r) => r.compiled.kind === pathKind);
|
|
161
|
+
if (pathRows.length > 0) {
|
|
162
|
+
const table = pathRows.map((r) => r.compiled.rule);
|
|
163
|
+
for (const name of laneToolNamesOf(req).filter((n) => ruleToolGrammarOf(n) === "path")) {
|
|
164
|
+
const answer = pathRuleLaneAnswer(table, { ...req, toolName: name }, { root: ctx.root, sessionId: ctx.sessionId, liveCwd, home: ctx.home });
|
|
165
|
+
const spoke = answer !== undefined && ("unreadable" in answer ? behavior === "deny" : "behavior" in answer && answer.behavior === behavior);
|
|
166
|
+
if (spoke) {
|
|
167
|
+
const rule = answer !== undefined && "rules" in answer ? answer.rules[0]?.rule : undefined;
|
|
168
|
+
const row = pathRows.find((r) => r.compiled.rule.rule === rule) ?? pathRows[0];
|
|
169
|
+
return { skill: row.skill, entries: [row.entry] };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
export function skillDenyDecision(hit) {
|
|
176
|
+
return {
|
|
177
|
+
action: "deny",
|
|
178
|
+
message: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
|
|
179
|
+
decisionReason: "skill",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function skillRowRefusal(lane, req) {
|
|
183
|
+
const hit = lane?.denying(req);
|
|
184
|
+
return hit === undefined ? undefined : skillDenyDecision(hit);
|
|
185
|
+
}
|
|
186
|
+
export function skillAllowDecision(hit, updatedInput) {
|
|
187
|
+
return {
|
|
188
|
+
action: "allow",
|
|
189
|
+
message: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in allowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")})`,
|
|
190
|
+
decisionReason: "skill",
|
|
191
|
+
...(updatedInput !== undefined ? { updatedInput } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const CARD_ENTRIES_MAX = 10;
|
|
195
|
+
const CARD_ENTRY_MAX_CHARS = 120;
|
|
196
|
+
export function skillActivationQuestion(skill, mode) {
|
|
197
|
+
const head = `Execute skill: ${inlineUntrusted(skill.name, 200)}`;
|
|
198
|
+
const granting = compileSkillGrant(skill, "model").rows.filter((r) => r.behavior === "allow" && !standsDownIn(r, mode));
|
|
199
|
+
if (granting.length === 0)
|
|
200
|
+
return head;
|
|
201
|
+
const shown = granting.slice(0, CARD_ENTRIES_MAX).map((r) => inlineUntrusted(r.entry, CARD_ENTRY_MAX_CHARS));
|
|
202
|
+
const rest = granting.length - shown.length;
|
|
203
|
+
return `${head}\nApproving also lets these tools be used without asking for the rest of this run: ${shown.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
|
|
204
|
+
}
|
|
205
|
+
export function skillSeatReader(skills) {
|
|
206
|
+
if (skills.length === 0)
|
|
207
|
+
return undefined;
|
|
208
|
+
return (args, mode) => {
|
|
209
|
+
const skill = skillNamedBy(args, skills);
|
|
210
|
+
return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode) } : undefined;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export function skillGateWiring(opts) {
|
|
214
|
+
const skillGrants = { ...opts.lane, ...(opts.onAllowed !== undefined ? { onAllowed: opts.onAllowed } : {}) };
|
|
215
|
+
return (calledName, wireName) => ({
|
|
216
|
+
...(mountedToolPermissionSeat(opts.tools, calledName) === "passthrough" ? { permissionSeat: "passthrough" } : {}),
|
|
217
|
+
...(opts.seatOf !== undefined && wireName === opts.skillToolName ? { permissionSeatOf: (args) => opts.seatOf?.(args, opts.mode()) } : {}),
|
|
218
|
+
skillGrants,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function seedSkillGrants(table, opts) {
|
|
222
|
+
table.seed(opts.inherited ?? [], "inherited");
|
|
223
|
+
const named = (n) => skillNamedBy({ skill: n }, opts.skills);
|
|
224
|
+
const invoked = opts.userInvokedSkills;
|
|
225
|
+
if (invoked !== undefined) {
|
|
226
|
+
const unknown = Array.isArray(invoked) ? invoked.filter((n) => named(n) === undefined) : [invoked];
|
|
227
|
+
if (unknown.length > 0) {
|
|
228
|
+
const e = new Error(`TaskSpec.userInvokedSkills names ${unknown.map((n) => JSON.stringify(n)).join(", ")}, which ${unknown.length === 1 ? "is" : "are"} not a skill this run carries (the loaded skills: ${opts.skills.map((s) => s.name).join(", ") || "none"}) — refusing rather than running without the invocation`);
|
|
229
|
+
e.code = USER_INVOKED_SKILL_UNKNOWN_CODE;
|
|
230
|
+
throw e;
|
|
231
|
+
}
|
|
232
|
+
for (const name of invoked)
|
|
233
|
+
table.activate(named(name), "user");
|
|
234
|
+
}
|
|
235
|
+
return opts.skills.flatMap((s) => compileSkillGrant(s, "model").inert);
|
|
236
|
+
}
|
|
237
|
+
export class SkillGrantTable {
|
|
238
|
+
rowsHeld = [];
|
|
239
|
+
hold(row) {
|
|
240
|
+
if (!this.rowsHeld.some((r) => r.skill === row.skill && r.origin === row.origin && r.behavior === row.behavior && r.entry === row.entry))
|
|
241
|
+
this.rowsHeld.push(row);
|
|
242
|
+
}
|
|
243
|
+
activate(skill, origin) {
|
|
244
|
+
const { rows, inert } = compileSkillGrant(skill, origin);
|
|
245
|
+
for (const row of rows)
|
|
246
|
+
this.hold(row);
|
|
247
|
+
return inert;
|
|
248
|
+
}
|
|
249
|
+
seed(records, origin) {
|
|
250
|
+
for (const r of records) {
|
|
251
|
+
const compiled = compileEntry(r.entry, r.behavior);
|
|
252
|
+
if (typeof compiled === "string")
|
|
253
|
+
continue;
|
|
254
|
+
this.hold({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
records() {
|
|
258
|
+
return this.rowsHeld.map((r) => ({ skill: r.skill, behavior: r.behavior, entry: r.entry, origin: r.origin }));
|
|
259
|
+
}
|
|
260
|
+
lane(ctx) {
|
|
261
|
+
return {
|
|
262
|
+
allowing: (req, mode) => judge(this.rowsHeld.filter((r) => !standsDownIn(r, mode, req)), req, "allow", ctx),
|
|
263
|
+
denying: (req) => judge(this.rowsHeld, req, "deny", ctx),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function readCarriedRecords(raw, carrier) {
|
|
268
|
+
if (raw === undefined)
|
|
269
|
+
return [];
|
|
270
|
+
if (!Array.isArray(raw))
|
|
271
|
+
return undefined;
|
|
272
|
+
const out = [];
|
|
273
|
+
for (const r of raw) {
|
|
274
|
+
if (typeof r !== "object" || r === null)
|
|
275
|
+
return undefined;
|
|
276
|
+
const { skill, behavior, entry, origin } = r;
|
|
277
|
+
if (typeof skill !== "string" || typeof entry !== "string" || (behavior !== "allow" && behavior !== "deny"))
|
|
278
|
+
return undefined;
|
|
279
|
+
if (typeof compileEntry(entry, behavior) === "string")
|
|
280
|
+
return undefined;
|
|
281
|
+
if (carrier === "capture") {
|
|
282
|
+
out.push({ skill, behavior, entry });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!FROZEN_SKILL_GRANT_ORIGINS.has(origin))
|
|
286
|
+
return undefined;
|
|
287
|
+
out.push({ skill, behavior, entry, origin: origin });
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
export function readSkillGrantCapture(snapshot) {
|
|
292
|
+
return readCarriedRecords(snapshot, "capture");
|
|
293
|
+
}
|
|
294
|
+
export function frozenSkillGrants(table) {
|
|
295
|
+
const rows = table.records().filter((r) => FROZEN_SKILL_GRANT_ORIGINS.has(r.origin));
|
|
296
|
+
return rows.length > 0 ? rows : undefined;
|
|
297
|
+
}
|
|
298
|
+
export function readFrozenSkillGrants(section) {
|
|
299
|
+
return readCarriedRecords(section, "frozen");
|
|
300
|
+
}
|
|
301
|
+
export const SKILL_ENTRY_NOT_GRANTED_CODE = "skills.allowed_tool_not_granted";
|
|
302
|
+
export const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
|
|
303
|
+
const NOTICE_ENTRIES_SHOWN = 5;
|
|
304
|
+
export function skillEntriesNotGrantedNotice(inert, sessionId) {
|
|
305
|
+
const shown = inert.slice(0, NOTICE_ENTRIES_SHOWN).map((e) => `${inlineUntrusted(e.skill, 80)}: ${e.list} "${inlineUntrusted(e.entry, 120)}" (${e.reason})`);
|
|
306
|
+
const rest = inert.length - shown.length;
|
|
307
|
+
return {
|
|
308
|
+
code: SKILL_ENTRY_NOT_GRANTED_CODE,
|
|
309
|
+
message: `${inert.length} skill list entr${inert.length === 1 ? "y grants" : "ies grant"} or refuse${inert.length === 1 ? "s" : ""} nothing — ${shown.join("; ")}${rest > 0 ? `; and ${rest} more` : ""}. ` +
|
|
310
|
+
"An allowed-tools list holding `*` voids the list, `mcp__*` names no server, and a write-path or domain entry has no allow reading; name the tools (or `mcp__<server>`) instead.",
|
|
311
|
+
detail: { sessionId, entries: inert.map((e) => ({ skill: e.skill, list: e.list, entry: e.entry, reason: e.reason })) },
|
|
312
|
+
};
|
|
313
|
+
}
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* directory third parties can write to is the same as pasting their text into the system prompt. The
|
|
19
19
|
* loader validates SHAPE (spec conformance), never intent.
|
|
20
20
|
*
|
|
21
|
-
* **`allowed-tools`
|
|
21
|
+
* **`allowed-tools` / `disallowed-tools` are COPIED, never judged.** Each frontmatter list reaches
|
|
22
|
+
* {@link SkillSpec.allowedTools} / {@link SkillSpec.disallowedTools} entry by entry as the author wrote it — no
|
|
23
|
+
* intersection with a roster, no projection onto a tool name, no refusal, no de-duplication. What an entry means
|
|
24
|
+
* (a tool, a namespace, a command rule, a path, a wildcard that grants nothing) is read in ONE place, the run's
|
|
25
|
+
* grant table, which announces what it cannot read; a loader that also judged would be a second reader of the grammar.
|
|
22
26
|
*
|
|
23
27
|
* Frontmatter is parsed by a deliberately closed subset reader (no YAML dependency — the runtime
|
|
24
28
|
* dependency budget is a standing constraint): top-level `key: value` scalars, optional surrounding
|
|
@@ -42,17 +46,8 @@ export type SkillsDirectoryWarningCode =
|
|
|
42
46
|
| "name_mismatch"
|
|
43
47
|
/** Required `description` absent. */
|
|
44
48
|
| "missing_description"
|
|
45
|
-
/** An `allowed-tools` entry carries a pattern specifier (`Bash(git:*)`) — the scope policy
|
|
46
|
-
* matches names, not patterns, so the entry cannot match and the tool is DISABLED in this skill
|
|
47
|
-
* frame. Disclosed loudly until pattern enforcement lands (the entry is deliberately NOT widened to
|
|
48
|
-
* its base name: that would turn "only git" into "all of Bash"). */
|
|
49
|
-
| "allowed_tool_pattern_unsupported"
|
|
50
49
|
/** `description` exceeds the spec cap. */
|
|
51
50
|
| "description_too_long"
|
|
52
|
-
/** An `allowed-tools` entry the deployment does not mount — dropped (tighten-only). */
|
|
53
|
-
| "allowed_tool_not_mounted"
|
|
54
|
-
/** A `disallowed-tools` declaration this mapping cannot carry — see {@link SkillsDirectoryOptions.deployedTools}. */
|
|
55
|
-
| "disallowed_tools_unenforced"
|
|
56
51
|
/** An attachment was left out (non-text bytes, over budget, unreadable, or resolving outside the skill). */
|
|
57
52
|
| "attachment_skipped"
|
|
58
53
|
/** A file or directory entry existed but could not be read/resolved. */
|
|
@@ -66,30 +61,6 @@ export interface SkillsDirectoryWarning {
|
|
|
66
61
|
detail: string;
|
|
67
62
|
}
|
|
68
63
|
export interface SkillsDirectoryOptions {
|
|
69
|
-
/**
|
|
70
|
-
* The tool names this deployment actually mounts. When given, a skill's `allowed-tools` declaration is
|
|
71
|
-
* INTERSECTED with it and every name outside is dropped with an `allowed_tool_not_mounted` warning.
|
|
72
|
-
*
|
|
73
|
-
* 🔴 **Tighten-only, by ruling.** `allowed-tools` is a skill-authored capability claim and the format
|
|
74
|
-
* marks it experimental; sema's permission discipline is tighten-only, so it is mapped exclusively onto
|
|
75
|
-
* {@link SkillManifest} — a structure that can only SUBTRACT capability (the scope policy returns
|
|
76
|
-
* `allow`/`deny` and composes ahead of the task policy under a deny-short-circuit fold, so it can never
|
|
77
|
-
* re-grant what the task withheld). Consequences worth stating plainly:
|
|
78
|
-
* - a name outside `deployedTools` is dropped, NOT honored — a skill cannot pull in a tool the
|
|
79
|
-
* deployment never mounted;
|
|
80
|
-
* - when the intersection comes out EMPTY the manifest is still emitted with an empty `allowTools`
|
|
81
|
-
* (narrow to nothing while the skill's frame is active). Dropping the manifest instead would widen
|
|
82
|
-
* the run relative to what the skill asked for, which is the one direction this mapping may not take;
|
|
83
|
-
* - omitting `deployedTools` skips the filter. Still safe — an unmounted name simply never matches a
|
|
84
|
-
* call — but the warning is then unavailable, so a deployment that wants the audit trail should pass it.
|
|
85
|
-
*
|
|
86
|
-
* Names are compared in CANONICAL form on both sides (the same normalization the enforcing policy
|
|
87
|
-
* applies), so passing this option can never produce a stricter result than omitting it: a declared
|
|
88
|
-
* legacy alias and the mounted current name are one tool here exactly as they are at call time.
|
|
89
|
-
* A wildcard declaration (`allowed-tools: "*"`) claims the whole roster and therefore narrows by
|
|
90
|
-
* nothing — no manifest is emitted, leaving the task policy untouched.
|
|
91
|
-
*/
|
|
92
|
-
deployedTools?: readonly string[];
|
|
93
64
|
/** Sink for non-fatal diagnostics. Absent ⇒ warnings are dropped and the load still proceeds. */
|
|
94
65
|
onWarning?: (warning: SkillsDirectoryWarning) => void;
|
|
95
66
|
/**
|