@aragon-agent/core 0.1.3
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 +126 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/engine/agent-loop.d.ts +52 -0
- package/dist/engine/agent-loop.js +260 -0
- package/dist/engine/agent.d.ts +129 -0
- package/dist/engine/agent.js +283 -0
- package/dist/engine/message-manager.d.ts +26 -0
- package/dist/engine/message-manager.js +39 -0
- package/dist/engine/steering.d.ts +27 -0
- package/dist/engine/steering.js +51 -0
- package/dist/engine/watchdog.d.ts +24 -0
- package/dist/engine/watchdog.js +50 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +63 -0
- package/dist/llm/model-registry.d.ts +51 -0
- package/dist/llm/model-registry.js +277 -0
- package/dist/llm/provider.d.ts +72 -0
- package/dist/llm/provider.js +77 -0
- package/dist/llm/providers/anthropic.d.ts +23 -0
- package/dist/llm/providers/anthropic.js +420 -0
- package/dist/llm/providers/google.d.ts +22 -0
- package/dist/llm/providers/google.js +336 -0
- package/dist/llm/providers/index.d.ts +67 -0
- package/dist/llm/providers/index.js +127 -0
- package/dist/llm/providers/openai.d.ts +22 -0
- package/dist/llm/providers/openai.js +353 -0
- package/dist/llm/stream-utils.d.ts +33 -0
- package/dist/llm/stream-utils.js +132 -0
- package/dist/llm/types.d.ts +129 -0
- package/dist/llm/types.js +17 -0
- package/dist/sandbox/console-capture.d.ts +29 -0
- package/dist/sandbox/console-capture.js +56 -0
- package/dist/sandbox/http-bridge.d.ts +47 -0
- package/dist/sandbox/http-bridge.js +143 -0
- package/dist/sandbox/isolated-vm-sandbox.d.ts +50 -0
- package/dist/sandbox/isolated-vm-sandbox.js +226 -0
- package/dist/sandbox/tool-bridge.d.ts +42 -0
- package/dist/sandbox/tool-bridge.js +98 -0
- package/dist/sandbox/types.d.ts +84 -0
- package/dist/sandbox/types.js +8 -0
- package/dist/skills/constants.d.ts +122 -0
- package/dist/skills/constants.js +179 -0
- package/dist/skills/disclosure.d.ts +146 -0
- package/dist/skills/disclosure.js +788 -0
- package/dist/skills/frontmatter.d.ts +27 -0
- package/dist/skills/frontmatter.js +177 -0
- package/dist/skills/index.d.ts +21 -0
- package/dist/skills/index.js +18 -0
- package/dist/skills/skill-find-tool.d.ts +41 -0
- package/dist/skills/skill-find-tool.js +117 -0
- package/dist/skills/skill-registry.d.ts +96 -0
- package/dist/skills/skill-registry.js +208 -0
- package/dist/skills/skill-tool.d.ts +44 -0
- package/dist/skills/skill-tool.js +96 -0
- package/dist/skills/tool-policy.d.ts +91 -0
- package/dist/skills/tool-policy.js +272 -0
- package/dist/skills/types.d.ts +271 -0
- package/dist/skills/types.js +8 -0
- package/dist/skills/validate.d.ts +37 -0
- package/dist/skills/validate.js +264 -0
- package/dist/tools/executor.d.ts +54 -0
- package/dist/tools/executor.js +206 -0
- package/dist/tools/helpers.d.ts +69 -0
- package/dist/tools/helpers.js +87 -0
- package/dist/tools/registry.d.ts +58 -0
- package/dist/tools/registry.js +119 -0
- package/dist/tools/types.d.ts +140 -0
- package/dist/tools/types.js +7 -0
- package/dist/tools/validator.d.ts +46 -0
- package/dist/tools/validator.js +210 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +7 -0
- package/package.json +115 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn-scoped tool ceiling (spec §5) — what turns `allowed-tools` from a
|
|
3
|
+
* decorative frontmatter field into an executable declaration.
|
|
4
|
+
*
|
|
5
|
+
* PURE FUNCTIONS ONLY. This module does not know which tools the host has; the
|
|
6
|
+
* caller passes `registered` and `floor` in (D-G7). Core owns the SKILL.md
|
|
7
|
+
* FORMAT — which includes knowing that `Read` is what Claude Code calls its
|
|
8
|
+
* read-a-file tool — while the CLI owns the inventory of what actually exists.
|
|
9
|
+
*
|
|
10
|
+
* THE DIRECTION OF FAILURE IS THE WHOLE DESIGN (D-G4). A declaration this host
|
|
11
|
+
* cannot fully resolve waives that skill's contribution ENTIRELY rather than
|
|
12
|
+
* narrowing it: honouring the half we understood would silently constrain an
|
|
13
|
+
* author to something they never wrote, and the reason (one mistyped tool name)
|
|
14
|
+
* would be invisible. Fail-open plus a loud notice is correct here; the
|
|
15
|
+
* fail-closed rule lives on the install gate, where the adversary is a person
|
|
16
|
+
* rather than a typo.
|
|
17
|
+
*/
|
|
18
|
+
import { TOOL_POLICY_DENY_ESCALATE_AT } from './constants.js';
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Name resolution (§5.3)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** Lower-case and strip `_`, `-` and whitespace: `Read`→`read`, `read_file`→`readfile`. */
|
|
23
|
+
export function normalizeToolAlias(name) {
|
|
24
|
+
return name.toLowerCase().replace(/[_\-\s]/g, '');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Claude Code's vocabulary → this host's snake_case names.
|
|
28
|
+
*
|
|
29
|
+
* Keys are already normalized. This table is why `.claude/skills` interop — the
|
|
30
|
+
* README's headline feature — survives enforcement at all: without it every
|
|
31
|
+
* community declaration (`allowed-tools: [Read, Bash]`) would resolve to nothing
|
|
32
|
+
* and every such skill would take the fail-open path, making the whole ceiling
|
|
33
|
+
* ornamental (FG2 / RG2).
|
|
34
|
+
*/
|
|
35
|
+
export const SKILL_TOOL_ALIASES = Object.freeze({
|
|
36
|
+
read: 'read_file',
|
|
37
|
+
readfile: 'read_file',
|
|
38
|
+
view: 'read_file',
|
|
39
|
+
cat: 'read_file',
|
|
40
|
+
write: 'write_file',
|
|
41
|
+
writefile: 'write_file',
|
|
42
|
+
create: 'write_file',
|
|
43
|
+
edit: 'edit_file',
|
|
44
|
+
editfile: 'edit_file',
|
|
45
|
+
multiedit: 'edit_file',
|
|
46
|
+
strreplaceeditor: 'edit_file',
|
|
47
|
+
strreplacebasededitor: 'edit_file',
|
|
48
|
+
bash: 'bash',
|
|
49
|
+
shell: 'bash',
|
|
50
|
+
sh: 'bash',
|
|
51
|
+
runcommand: 'bash',
|
|
52
|
+
terminal: 'bash',
|
|
53
|
+
ls: 'list_dir',
|
|
54
|
+
listdir: 'list_dir',
|
|
55
|
+
listdirectory: 'list_dir',
|
|
56
|
+
glob: 'glob',
|
|
57
|
+
findfiles: 'glob',
|
|
58
|
+
grep: 'grep',
|
|
59
|
+
search: 'grep',
|
|
60
|
+
ripgrep: 'grep',
|
|
61
|
+
skill: 'skill',
|
|
62
|
+
skillfind: 'skill_find',
|
|
63
|
+
skillinstall: 'skill_install',
|
|
64
|
+
skillcreate: 'skill_create',
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Capabilities this host simply does not have. A declaration naming one is
|
|
68
|
+
* DROPPED, not treated as unresolvable (D-G5).
|
|
69
|
+
*
|
|
70
|
+
* If these counted as "cannot resolve", nearly every community skill would take
|
|
71
|
+
* the fail-open path and enforcement would be theatre. Dropping them changes no
|
|
72
|
+
* executable set — the model could never have called them — while leaving the
|
|
73
|
+
* rest of the declaration in force. That is precision, not a concession.
|
|
74
|
+
*
|
|
75
|
+
* THIS TABLE ONLY GROWS. Removing an entry pushes already-working skills into
|
|
76
|
+
* fail-open with no symptom whatsoever.
|
|
77
|
+
*/
|
|
78
|
+
export const SKILL_TOOL_KNOWN_ABSENT = new Set([
|
|
79
|
+
'webfetch',
|
|
80
|
+
'websearch',
|
|
81
|
+
'task',
|
|
82
|
+
'agent',
|
|
83
|
+
'todowrite',
|
|
84
|
+
'todoread',
|
|
85
|
+
'notebookedit',
|
|
86
|
+
'notebookread',
|
|
87
|
+
'bashoutput',
|
|
88
|
+
'killshell',
|
|
89
|
+
'slashcommand',
|
|
90
|
+
'exitplanmode',
|
|
91
|
+
'askuserquestion',
|
|
92
|
+
'computer',
|
|
93
|
+
]);
|
|
94
|
+
/**
|
|
95
|
+
* Resolve one declared name against the live tool list.
|
|
96
|
+
*
|
|
97
|
+
* `string` — a tool on this host.
|
|
98
|
+
* `'absent'` — a real Claude Code tool this host lacks; drop it silently.
|
|
99
|
+
* `null` — unrecognisable; the whole skill then waives its contribution.
|
|
100
|
+
*
|
|
101
|
+
* THE ORDER OF THE FOUR STEPS IS THE SEMANTICS — do not reshuffle it. Exact
|
|
102
|
+
* match first means a host that adds a new tool needs no table maintenance. The
|
|
103
|
+
* alias table is consulted BEFORE the absent table so that mistakenly listing a
|
|
104
|
+
* tool this host actually has could never silently downgrade a skill.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveDeclaredTool(name, registered) {
|
|
107
|
+
const trimmed = name.trim();
|
|
108
|
+
if (trimmed.length === 0)
|
|
109
|
+
return null;
|
|
110
|
+
// 1. Exact match against what is registered right now.
|
|
111
|
+
if (registered.includes(trimmed))
|
|
112
|
+
return trimmed;
|
|
113
|
+
const key = normalizeToolAlias(trimmed);
|
|
114
|
+
// 2. Known alias. Resolving to a tool this host does NOT have is a miss, not a
|
|
115
|
+
// drop: the author asked for a capability we cannot grant, and pretending
|
|
116
|
+
// otherwise would narrow their declaration behind their back.
|
|
117
|
+
const candidate = SKILL_TOOL_ALIASES[key];
|
|
118
|
+
if (candidate !== undefined)
|
|
119
|
+
return registered.includes(candidate) ? candidate : null;
|
|
120
|
+
// 3. A real tool elsewhere that does not exist here.
|
|
121
|
+
if (SKILL_TOOL_KNOWN_ABSENT.has(key))
|
|
122
|
+
return 'absent';
|
|
123
|
+
// 4. Anything else — a typo, or a tool nobody here has heard of.
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Decision (§5.2)
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
function noCeiling(mode, ignored = []) {
|
|
130
|
+
return { mode, allowed: null, sources: [], sourceNames: [], ignored };
|
|
131
|
+
}
|
|
132
|
+
function contributes(record) {
|
|
133
|
+
if (record.disabled || record.invalid)
|
|
134
|
+
return false;
|
|
135
|
+
// Redundant with the frame's own gate, and deliberately so (D-G9): an
|
|
136
|
+
// `activation: always` skill is ambient reference material, not a procedure
|
|
137
|
+
// the model chose to run, so it must never constrain a turn.
|
|
138
|
+
if (record.frontmatter.activation === 'always')
|
|
139
|
+
return false;
|
|
140
|
+
return record.frontmatter.allowedTools.length > 0;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Compute the ceiling for a turn.
|
|
144
|
+
*
|
|
145
|
+
* TWO INVARIANTS THIS FUNCTION OWNS (I-G1 / I-G2, pinned by AC-G23):
|
|
146
|
+
*
|
|
147
|
+
* I-G1 `registered.length === 0` ⇒ `allowed === null`.
|
|
148
|
+
* I-G2 `allowed !== null` ⇒ the set is NON-EMPTY.
|
|
149
|
+
*
|
|
150
|
+
* Both matter because the failure they exclude is catastrophic and silent: an
|
|
151
|
+
* empty permitted set is not "a tight ceiling", it is an agent that can no
|
|
152
|
+
* longer do anything, for a reason no user could ever guess. The construction
|
|
153
|
+
* makes them hold — every unresolved name waives its whole skill, and the final
|
|
154
|
+
* guard below refuses to hand back an empty set — rather than leaving them to be
|
|
155
|
+
* re-derived by whoever refactors this next.
|
|
156
|
+
*/
|
|
157
|
+
export function computeToolPolicy(input) {
|
|
158
|
+
const { mode, registered, floor } = input;
|
|
159
|
+
if (mode === 'off')
|
|
160
|
+
return noCeiling(mode);
|
|
161
|
+
const sources = [];
|
|
162
|
+
const ignored = [];
|
|
163
|
+
const granted = new Set();
|
|
164
|
+
for (const record of input.frame) {
|
|
165
|
+
if (!contributes(record))
|
|
166
|
+
continue;
|
|
167
|
+
const declared = record.frontmatter.allowedTools;
|
|
168
|
+
const resolved = [];
|
|
169
|
+
const unresolved = [];
|
|
170
|
+
for (const declaredName of declared) {
|
|
171
|
+
const outcome = resolveDeclaredTool(declaredName, registered);
|
|
172
|
+
if (outcome === null)
|
|
173
|
+
unresolved.push(declaredName);
|
|
174
|
+
else if (outcome !== 'absent')
|
|
175
|
+
resolved.push(outcome);
|
|
176
|
+
}
|
|
177
|
+
if (unresolved.length > 0) {
|
|
178
|
+
ignored.push({ name: record.name, unresolved });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
for (const toolName of resolved)
|
|
182
|
+
granted.add(toolName);
|
|
183
|
+
sources.push({
|
|
184
|
+
name: record.name,
|
|
185
|
+
declared: [...declared],
|
|
186
|
+
granted: [...new Set(resolved)].sort((a, b) => a.localeCompare(b, 'en')),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (sources.length === 0)
|
|
190
|
+
return noCeiling(mode, ignored);
|
|
191
|
+
sources.sort((a, b) => a.name.localeCompare(b.name, 'en'));
|
|
192
|
+
const allowed = new Set([...floor, ...granted]);
|
|
193
|
+
// I-G2's structural guard. Reachable only when the floor is empty AND every
|
|
194
|
+
// declaration resolved to `absent` — pathological, but the consequence would
|
|
195
|
+
// be a fully locked agent, so it is checked rather than argued about.
|
|
196
|
+
if (allowed.size === 0)
|
|
197
|
+
return noCeiling(mode, ignored);
|
|
198
|
+
return { mode, allowed, sources, sourceNames: sources.map((s) => s.name), ignored };
|
|
199
|
+
}
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Verdict (§5.2)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
function sorted(values) {
|
|
204
|
+
return [...values].sort((a, b) => a.localeCompare(b, 'en'));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* How to name the skills responsible, for both the model and the user.
|
|
208
|
+
*
|
|
209
|
+
* Multi-source turns list EVERY name. Picking one would be wrong in some
|
|
210
|
+
* arrangement of skills every time, and a confidently wrong attribution is worse
|
|
211
|
+
* than a slightly longer sentence.
|
|
212
|
+
*/
|
|
213
|
+
function describeSources(names) {
|
|
214
|
+
if (names.length === 1)
|
|
215
|
+
return `skill "${names[0]}"`;
|
|
216
|
+
return `skills ${names.map((n) => `"${n}"`).join(', ')}`;
|
|
217
|
+
}
|
|
218
|
+
/** The union of what the frame's skills actually asked for, floor excluded. */
|
|
219
|
+
function grantedUnion(decision) {
|
|
220
|
+
return sorted(new Set(decision.sources.flatMap((s) => s.granted)));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Decide a single call. Deterministic, allocation-light, and safe to run on
|
|
224
|
+
* every tool invocation — `allowed === null` short-circuits before any string
|
|
225
|
+
* work, which is the common case.
|
|
226
|
+
*/
|
|
227
|
+
export function evaluateToolCall(tool, d) {
|
|
228
|
+
if (d.allowed === null || d.allowed.has(tool))
|
|
229
|
+
return { allow: true };
|
|
230
|
+
const subject = describeSources(d.sourceNames);
|
|
231
|
+
const declaredList = grantedUnion(d).join(', ');
|
|
232
|
+
if (d.mode === 'warn') {
|
|
233
|
+
return {
|
|
234
|
+
allow: true,
|
|
235
|
+
notice: `${tool} is outside the tool ceiling of ${subject} (declared: ${declaredList}). ` +
|
|
236
|
+
'Running it anyway - skills.toolPolicy is "warn".',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const message = [
|
|
240
|
+
`Tool "${tool}" is not permitted while ${subject} is in effect.`,
|
|
241
|
+
// The RAW declaration, not the mapped names: the author has to be able to
|
|
242
|
+
// find this word in the frontmatter line they wrote.
|
|
243
|
+
...d.sources.map((s) => `- "${s.name}" declares allowed-tools: ${s.declared.join(', ')}`),
|
|
244
|
+
`Permitted right now: ${sorted(d.allowed).join(', ')}.`,
|
|
245
|
+
`Use one of those, or tell the user "${tool}" must be added to allowed-tools.`,
|
|
246
|
+
'Do not retry this tool for the rest of this turn.',
|
|
247
|
+
].join('\n');
|
|
248
|
+
return {
|
|
249
|
+
allow: false,
|
|
250
|
+
message,
|
|
251
|
+
notice: `Blocked ${tool}: ${subject} limits this turn to ${declaredList}. ` +
|
|
252
|
+
'Run /skills policy off to disable.',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The line appended to a refusal once the same tool has been refused
|
|
257
|
+
* `TOOL_POLICY_DENY_ESCALATE_AT` times in one turn (§5.5).
|
|
258
|
+
*
|
|
259
|
+
* Appended to the SAME message rather than emitted as a new one: a second
|
|
260
|
+
* message would be one more thing for the model to skim past, whereas a longer
|
|
261
|
+
* version of the text it is already reading is unavoidable.
|
|
262
|
+
*/
|
|
263
|
+
export function renderDenyEscalation(tool, count) {
|
|
264
|
+
return (`You have now been refused "${tool}" ${count} times this turn. Stop calling it; ` +
|
|
265
|
+
"tell the user which tool the skill's allowed-tools is missing.");
|
|
266
|
+
}
|
|
267
|
+
/** The matching one-liner for the user, emitted once per (turn, tool). */
|
|
268
|
+
export function renderDenyEscalationNotice(skill, tool, count) {
|
|
269
|
+
return (`Skill "${skill}" blocked ${tool} ${count}x this turn - the skill's allowed-tools ` +
|
|
270
|
+
'may be wrong (/skills policy off to bypass).');
|
|
271
|
+
}
|
|
272
|
+
export { TOOL_POLICY_DENY_ESCALATE_AT };
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill data model (spec §10.1).
|
|
3
|
+
*
|
|
4
|
+
* Pure types + one injected port. This module must stay free of `node:*` — the
|
|
5
|
+
* host filesystem is reached only through `SkillHost`, which the CLI implements
|
|
6
|
+
* (D1). `no-host-coupling.test.ts` enforces that red line mechanically.
|
|
7
|
+
*/
|
|
8
|
+
export type SkillScope = 'bundled' | 'user' | 'project' | 'env';
|
|
9
|
+
export type SkillActivation = 'auto' | 'always' | 'manual';
|
|
10
|
+
export interface SkillFrontmatter {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
version: string;
|
|
14
|
+
license?: string;
|
|
15
|
+
author?: string;
|
|
16
|
+
homepage?: string;
|
|
17
|
+
keywords: string[];
|
|
18
|
+
allowedTools: string[];
|
|
19
|
+
activation: SkillActivation;
|
|
20
|
+
/** Every parsed key, including unknown ones (forward compat). */
|
|
21
|
+
raw: Record<string, string | string[]>;
|
|
22
|
+
}
|
|
23
|
+
export interface SkillFileRef {
|
|
24
|
+
/** Path relative to the skill directory, always with `/` separators. */
|
|
25
|
+
path: string;
|
|
26
|
+
bytes: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SkillValidationIssue {
|
|
29
|
+
level: 'error' | 'warn';
|
|
30
|
+
code: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SkillManifestFile {
|
|
34
|
+
path: string;
|
|
35
|
+
bytes: number;
|
|
36
|
+
sha256: string;
|
|
37
|
+
}
|
|
38
|
+
export interface SkillManifestSource {
|
|
39
|
+
kind: 'local-dir' | 'local-file' | 'git' | 'https-md' | 'https-zip' | 'inline';
|
|
40
|
+
url: string;
|
|
41
|
+
ref: string | null;
|
|
42
|
+
subdir: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* The commit sha a git source resolved to at install time; `null` for every
|
|
45
|
+
* other kind and whenever `git rev-parse` did not answer.
|
|
46
|
+
*
|
|
47
|
+
* OPTIONAL ON PURPOSE (F10): `readManifest()` returns `null` for any
|
|
48
|
+
* `schema !== 1`, so bumping the schema would demote every already-installed
|
|
49
|
+
* skill to "no provenance" the moment an older CLI read it. New fields are
|
|
50
|
+
* therefore always additive and always optional.
|
|
51
|
+
*/
|
|
52
|
+
resolvedRef?: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface SkillManifest {
|
|
55
|
+
schema: number;
|
|
56
|
+
name: string;
|
|
57
|
+
version: string;
|
|
58
|
+
installedAt: number;
|
|
59
|
+
installer: string;
|
|
60
|
+
source: SkillManifestSource;
|
|
61
|
+
files: SkillManifestFile[];
|
|
62
|
+
totalBytes: number;
|
|
63
|
+
/** Set by `skills update`; absent for a skill that was never updated. */
|
|
64
|
+
updatedAt?: number;
|
|
65
|
+
/** The version this skill carried before the last update. */
|
|
66
|
+
previousVersion?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* How a skill's `SKILL.md` compares to what the install manifest recorded (§8).
|
|
70
|
+
*
|
|
71
|
+
* `unverified` — no manifest, or the check is switched off. Hand-authored,
|
|
72
|
+
* bundled and `.claude/skills` skills live here permanently.
|
|
73
|
+
* `ok` — byte-identical to the approved copy.
|
|
74
|
+
* `modified` — the file on disk is not the one that was approved.
|
|
75
|
+
*/
|
|
76
|
+
export type SkillIntegrity = 'unverified' | 'ok' | 'modified';
|
|
77
|
+
export interface SkillUsageStat {
|
|
78
|
+
useCount: number;
|
|
79
|
+
/** epoch ms; `0` means never used. */
|
|
80
|
+
lastUsedAt: number;
|
|
81
|
+
}
|
|
82
|
+
/** Skill name → usage counters. Local-only; see `usage.ts`. */
|
|
83
|
+
export type SkillUsageMap = Record<string, SkillUsageStat>;
|
|
84
|
+
export interface SkillRecord {
|
|
85
|
+
name: string;
|
|
86
|
+
description: string;
|
|
87
|
+
scope: SkillScope;
|
|
88
|
+
/** Absolute skill directory. */
|
|
89
|
+
dir: string;
|
|
90
|
+
/** Absolute path to SKILL.md. */
|
|
91
|
+
entryPath: string;
|
|
92
|
+
frontmatter: SkillFrontmatter;
|
|
93
|
+
/** Lazily filled at Level 2. */
|
|
94
|
+
body: string | null;
|
|
95
|
+
/** Lazily filled at Level 2. */
|
|
96
|
+
files: SkillFileRef[] | null;
|
|
97
|
+
/** Size of SKILL.md in bytes. */
|
|
98
|
+
bytes: number;
|
|
99
|
+
disabled: boolean;
|
|
100
|
+
issues: SkillValidationIssue[];
|
|
101
|
+
invalid: boolean;
|
|
102
|
+
/** Same-name records from lower-precedence roots. */
|
|
103
|
+
shadowed: Array<{
|
|
104
|
+
scope: SkillScope;
|
|
105
|
+
dir: string;
|
|
106
|
+
}>;
|
|
107
|
+
/** Parsed .aragon-skill.json when present. */
|
|
108
|
+
manifest: SkillManifest | null;
|
|
109
|
+
/** False for `bundled`, `env`, and `.claude/skills` roots. */
|
|
110
|
+
writable: boolean;
|
|
111
|
+
/** SKILL.md vs. the manifest, computed during discovery at no extra I/O (§8.1). */
|
|
112
|
+
integrity: SkillIntegrity;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The injected filesystem port — keeps core free of `node:*`.
|
|
116
|
+
*
|
|
117
|
+
* ERROR CONTRACT (P2-8): every method here MAY THROW (the Node implementation
|
|
118
|
+
* wraps sync fs calls: ENOENT / EACCES / EISDIR / ELOOP all surface as
|
|
119
|
+
* exceptions). Core callers are responsible for try/catch; "never throws" is an
|
|
120
|
+
* obligation of a tool's `execute()` (§5.2), never of this port.
|
|
121
|
+
*/
|
|
122
|
+
export interface SkillHost {
|
|
123
|
+
readTextFile(path: string): string;
|
|
124
|
+
listDir(path: string): Array<{
|
|
125
|
+
name: string;
|
|
126
|
+
isDirectory: boolean;
|
|
127
|
+
bytes: number;
|
|
128
|
+
}>;
|
|
129
|
+
exists(path: string): boolean;
|
|
130
|
+
join(...parts: string[]): string;
|
|
131
|
+
relative(from: string, to: string): string;
|
|
132
|
+
isAbsolute(p: string): boolean;
|
|
133
|
+
}
|
|
134
|
+
export interface SkillCatalogOptions {
|
|
135
|
+
/** UTF-8 byte ceiling for the whole block. */
|
|
136
|
+
maxBytes?: number;
|
|
137
|
+
/** Character cap for a single entry's description. */
|
|
138
|
+
descLineMax?: number;
|
|
139
|
+
/** Append the `Skill tools: …` footer line. Default true. */
|
|
140
|
+
includeToolLine?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Usage counters used to rank entries within a scope (§5.2). Omitting this
|
|
143
|
+
* reproduces the pre-ranking order byte for byte (invariant I-A1).
|
|
144
|
+
*/
|
|
145
|
+
usage?: SkillUsageMap;
|
|
146
|
+
/** Injected clock for the recency buckets; defaults to `Date.now()`. */
|
|
147
|
+
now?: number;
|
|
148
|
+
}
|
|
149
|
+
export interface SkillFindOptions {
|
|
150
|
+
/** Total number of installed skills searched, for the `of="N"` attribute. */
|
|
151
|
+
total?: number;
|
|
152
|
+
/**
|
|
153
|
+
* How many skills matched BEFORE the caller applied its own `limit`. Defaults
|
|
154
|
+
* to `matched.length`. Pass it whenever the array handed in has already been
|
|
155
|
+
* sliced, so the `matched="N"` attribute stays truthful and the overflow line
|
|
156
|
+
* still appears — a silently shortened result list is the same failure this
|
|
157
|
+
* whole tool exists to undo.
|
|
158
|
+
*/
|
|
159
|
+
matchedTotal?: number;
|
|
160
|
+
/** UTF-8 byte ceiling for the whole result block. */
|
|
161
|
+
maxBytes?: number;
|
|
162
|
+
/** Character cap for a single entry's description. */
|
|
163
|
+
descLineMax?: number;
|
|
164
|
+
/** Near-miss names offered when nothing matched. */
|
|
165
|
+
suggestions?: string[];
|
|
166
|
+
}
|
|
167
|
+
export type SkillToolPolicyMode = 'off' | 'warn' | 'enforce';
|
|
168
|
+
/**
|
|
169
|
+
* The host's shell family. Injected by the CLI (D-G14) — core never reads
|
|
170
|
+
* `process`, because that would make the renderers untestable across platforms:
|
|
171
|
+
* a snapshot taken on Windows could never cover the POSIX branch.
|
|
172
|
+
*/
|
|
173
|
+
export type SkillPlatform = 'win32' | 'posix';
|
|
174
|
+
/**
|
|
175
|
+
* One skill's COMPLETE contribution to the ceiling.
|
|
176
|
+
*
|
|
177
|
+
* Not a bare name (evaluated P0-1): the refusal text has to say what THAT skill
|
|
178
|
+
* declared, and a union `allowed` set — which also carries the floor — cannot be
|
|
179
|
+
* reverse-engineered back into per-skill declarations.
|
|
180
|
+
*/
|
|
181
|
+
export interface ToolPolicySource {
|
|
182
|
+
name: string;
|
|
183
|
+
/** The raw declaration, verbatim and unmapped, so an author can grep for it. */
|
|
184
|
+
declared: string[];
|
|
185
|
+
/** Declared names that resolved to a tool on this host (ascending, deduped). */
|
|
186
|
+
granted: string[];
|
|
187
|
+
}
|
|
188
|
+
export interface ToolPolicyInput {
|
|
189
|
+
/** Skill records in the current turn's frame (order irrelevant). */
|
|
190
|
+
frame: SkillRecord[];
|
|
191
|
+
/** Tool names actually registered in this session (host-supplied). */
|
|
192
|
+
registered: readonly string[];
|
|
193
|
+
/** Always-permitted tools (host-supplied; D-G6 / D-G7). */
|
|
194
|
+
floor: readonly string[];
|
|
195
|
+
mode: SkillToolPolicyMode;
|
|
196
|
+
}
|
|
197
|
+
export interface ToolPolicyDecision {
|
|
198
|
+
mode: SkillToolPolicyMode;
|
|
199
|
+
/** `null` = no ceiling at all. Non-null is the permitted set. NEVER empty (I-G2). */
|
|
200
|
+
allowed: ReadonlySet<string> | null;
|
|
201
|
+
/** Skills that contributed, ascending by `name`. */
|
|
202
|
+
sources: ToolPolicySource[];
|
|
203
|
+
/** Convenience mirror of `sources.map(s => s.name)` for display / events. */
|
|
204
|
+
sourceNames: string[];
|
|
205
|
+
/** Skills waived wholesale by D-G4, with the names that could not be resolved. */
|
|
206
|
+
ignored: Array<{
|
|
207
|
+
name: string;
|
|
208
|
+
unresolved: string[];
|
|
209
|
+
}>;
|
|
210
|
+
}
|
|
211
|
+
export interface ToolPolicyVerdict {
|
|
212
|
+
allow: boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Present only when `allow === false`: the actionable explanation the MODEL
|
|
215
|
+
* reads. Mutable on purpose — `SkillService` appends the escalation line to it
|
|
216
|
+
* on the Nth refusal rather than emitting a second message (§5.5).
|
|
217
|
+
*/
|
|
218
|
+
message?: string;
|
|
219
|
+
/** Present whenever the user should hear about it: one line for the transcript. */
|
|
220
|
+
notice?: string;
|
|
221
|
+
}
|
|
222
|
+
/** The slice of a decision the renderers need. Kept flat so it can be snapshotted. */
|
|
223
|
+
export interface SkillPolicyView {
|
|
224
|
+
mode: SkillToolPolicyMode;
|
|
225
|
+
/** The full permitted set, ascending. */
|
|
226
|
+
allowed: string[];
|
|
227
|
+
}
|
|
228
|
+
export interface SkillBodyOptions {
|
|
229
|
+
bodyMaxBytes?: number;
|
|
230
|
+
/** Hard ceiling on the whole rendered string. */
|
|
231
|
+
resultMaxBytes?: number;
|
|
232
|
+
filesMax?: number;
|
|
233
|
+
alreadyLoaded?: boolean;
|
|
234
|
+
arguments?: string;
|
|
235
|
+
/** `'digest'` omits the body and points at the earlier copy (§7.2). */
|
|
236
|
+
mode?: 'full' | 'digest';
|
|
237
|
+
/**
|
|
238
|
+
* Drives the one-line tool-ceiling statement.
|
|
239
|
+
*
|
|
240
|
+
* OMITTING IT MEANS "SAY NOTHING" — and that is the default for a reason
|
|
241
|
+
* (P0-2 / D-G18). `activation: always` and `--skill` skills are exempt from the
|
|
242
|
+
* ceiling, so telling the model every single turn that "only these tools are
|
|
243
|
+
* permitted" would impose a restriction that does not exist, invisibly.
|
|
244
|
+
*/
|
|
245
|
+
policy?: SkillPolicyView;
|
|
246
|
+
platform?: SkillPlatform;
|
|
247
|
+
}
|
|
248
|
+
/** Options for the `/<skill-name>` invocation message (§6). */
|
|
249
|
+
export interface SkillInvocationOptions {
|
|
250
|
+
bodyMaxBytes?: number;
|
|
251
|
+
/** Defaults to `SKILL_INVOCATION_MAX_BYTES`. */
|
|
252
|
+
resultMaxBytes?: number;
|
|
253
|
+
filesMax?: number;
|
|
254
|
+
platform?: SkillPlatform;
|
|
255
|
+
policy?: SkillPolicyView;
|
|
256
|
+
}
|
|
257
|
+
/** A staged (not yet installed) file, as seen by `validateStagedSkill`. */
|
|
258
|
+
export interface StagedFile {
|
|
259
|
+
/** Path relative to the staging root, `/`-separated. */
|
|
260
|
+
path: string;
|
|
261
|
+
bytes: number;
|
|
262
|
+
/** True for symlinks and any non-regular entry. */
|
|
263
|
+
isSymlink?: boolean;
|
|
264
|
+
}
|
|
265
|
+
export interface StagedValidationResult {
|
|
266
|
+
ok: boolean;
|
|
267
|
+
issues: SkillValidationIssue[];
|
|
268
|
+
frontmatter: SkillFrontmatter | null;
|
|
269
|
+
fileCount: number;
|
|
270
|
+
totalBytes: number;
|
|
271
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill data model (spec §10.1).
|
|
3
|
+
*
|
|
4
|
+
* Pure types + one injected port. This module must stay free of `node:*` — the
|
|
5
|
+
* host filesystem is reached only through `SkillHost`, which the CLI implements
|
|
6
|
+
* (D1). `no-host-coupling.test.ts` enforces that red line mechanically.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill validation — frontmatter contract (§4.4) and staged-install safety (§8.3).
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only: no filesystem, no process. The staged validator takes a
|
|
5
|
+
* pre-listed file table so the same rules run identically over a real staging
|
|
6
|
+
* directory (CLI) and over a fixture (tests).
|
|
7
|
+
*/
|
|
8
|
+
import type { SkillFrontmatter, SkillValidationIssue, StagedFile, StagedValidationResult } from './types.js';
|
|
9
|
+
/** True when `s` holds a C0/DEL control character. */
|
|
10
|
+
export declare function hasControlChar(s: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Validate a parsed frontmatter map against the §4.2 contract.
|
|
13
|
+
*
|
|
14
|
+
* Error codes are a stable public surface — the UI and the tests both key off
|
|
15
|
+
* them, so renaming one is a breaking change.
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateSkillFrontmatter(data: Record<string, string | string[]>, dirName: string): SkillValidationIssue[];
|
|
18
|
+
/** Build the normalized frontmatter view, applying every documented default. */
|
|
19
|
+
export declare function normalizeFrontmatter(data: Record<string, string | string[]>, fallbackName: string): SkillFrontmatter;
|
|
20
|
+
/**
|
|
21
|
+
* Reject a relative path that could escape the staging root or is unusable on
|
|
22
|
+
* Windows. This is the SECOND line of defence — the archive extractor applies
|
|
23
|
+
* the same rules while writing (§9.2.1). Both are required: the extractor
|
|
24
|
+
* guards what lands on disk, this guards what we then promote into the skills
|
|
25
|
+
* root (a plain local-directory source never goes through the extractor).
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkStagedPath(path: string): SkillValidationIssue | null;
|
|
28
|
+
/**
|
|
29
|
+
* Validate a staged skill before it is promoted into a skills root.
|
|
30
|
+
*
|
|
31
|
+
* `entryText` is the raw SKILL.md; `files` is the full staged file table
|
|
32
|
+
* (SKILL.md included). Returns `ok: false` if ANY error-level issue is found —
|
|
33
|
+
* the installer must then abort and leave the target untouched.
|
|
34
|
+
*/
|
|
35
|
+
export declare function validateStagedSkill(entryText: string | null, files: StagedFile[], dirName: string, parse: (source: string) => {
|
|
36
|
+
data: Record<string, string | string[]>;
|
|
37
|
+
} | null): StagedValidationResult;
|