@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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory skill registry (spec §10.1). Pure state: no filesystem, no network.
|
|
3
|
+
*
|
|
4
|
+
* Precedence (D7) is decided by the ORDER records are added — the CLI scanner
|
|
5
|
+
* walks roots low-to-high (bundled → user → project → env), so a later `add()`
|
|
6
|
+
* of the same name wins and the loser is recorded in `shadowed[]` rather than
|
|
7
|
+
* silently dropped. `/skills` renders that as `~shadowed by project`, which is
|
|
8
|
+
* the difference between "my edit did nothing" being a mystery and being a
|
|
9
|
+
* one-line answer.
|
|
10
|
+
*/
|
|
11
|
+
import { catalogRecords } from './disclosure.js';
|
|
12
|
+
export class SkillRegistry {
|
|
13
|
+
byName = new Map();
|
|
14
|
+
/** Skills loaded via the `skill` tool during THIS session. */
|
|
15
|
+
active = new Set();
|
|
16
|
+
// -------------------------------------------------------------------------
|
|
17
|
+
// Frames (§5.1) — the turn-scoped source set behind the tool ceiling.
|
|
18
|
+
//
|
|
19
|
+
// NOT the same thing as `active`, and conflating them is the mistake this
|
|
20
|
+
// split exists to prevent: `active` answers "has this body already been sent
|
|
21
|
+
// in this conversation" and only ever grows, whereas a ceiling has to expire
|
|
22
|
+
// the moment the user's intent changes. Two segments, because a slash command
|
|
23
|
+
// submits the very message that opens the turn it must constrain (D-G2): it
|
|
24
|
+
// QUEUES, and `beginUserTurn()` promotes.
|
|
25
|
+
//
|
|
26
|
+
// NAMES, NOT RECORDS (D-G20). `discover()` swaps the whole table via
|
|
27
|
+
// `replaceAll()`, and it runs mid-turn — `/skills reload`, `/skills trust`,
|
|
28
|
+
// any install. A held record reference would quietly become an orphan whose
|
|
29
|
+
// `disabled` flag and integrity verdict are frozen in the past.
|
|
30
|
+
// -------------------------------------------------------------------------
|
|
31
|
+
frameActive = new Set();
|
|
32
|
+
framePending = new Set();
|
|
33
|
+
/**
|
|
34
|
+
* Announce-once ledgers (§5.5 / P1-2). They live here rather than in the tool
|
|
35
|
+
* wrapper because this class is already the sole authority on when a turn
|
|
36
|
+
* begins — a `Set` closed over in `withToolPolicy` would be session-scoped and
|
|
37
|
+
* would silence `warn` mode after its very first message.
|
|
38
|
+
*/
|
|
39
|
+
warnedThisTurn = new Set();
|
|
40
|
+
failOpenNoticed = new Set();
|
|
41
|
+
deniedThisTurn = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* Insert a record, resolving a same-name collision in favour of the newcomer
|
|
44
|
+
* (higher-precedence root) while preserving the loser as a shadow entry.
|
|
45
|
+
*/
|
|
46
|
+
add(record) {
|
|
47
|
+
const existing = this.byName.get(record.name);
|
|
48
|
+
if (!existing) {
|
|
49
|
+
this.byName.set(record.name, record);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Highest-precedence loser first, so `/skills` can render "~shadowed by
|
|
53
|
+
// <shadowed[0].scope>" without re-deriving the ordering.
|
|
54
|
+
record.shadowed = [
|
|
55
|
+
{ scope: existing.scope, dir: existing.dir },
|
|
56
|
+
...existing.shadowed,
|
|
57
|
+
...record.shadowed,
|
|
58
|
+
];
|
|
59
|
+
this.byName.set(record.name, record);
|
|
60
|
+
}
|
|
61
|
+
get(name) {
|
|
62
|
+
return this.byName.get(name);
|
|
63
|
+
}
|
|
64
|
+
/** Every record, including disabled and invalid ones, sorted by name. */
|
|
65
|
+
list() {
|
|
66
|
+
return Array.from(this.byName.values()).sort((a, b) => a.name.localeCompare(b.name, 'en'));
|
|
67
|
+
}
|
|
68
|
+
names() {
|
|
69
|
+
return this.list().map((r) => r.name);
|
|
70
|
+
}
|
|
71
|
+
/** Level 1-eligible records in canonical order (scope desc, then name asc). */
|
|
72
|
+
catalog() {
|
|
73
|
+
return catalogRecords(Array.from(this.byName.values()));
|
|
74
|
+
}
|
|
75
|
+
/** Records that should be injected as Level 2 at session start (§5.4). */
|
|
76
|
+
alwaysOn(extraNames = []) {
|
|
77
|
+
const forced = new Set(extraNames);
|
|
78
|
+
return Array.from(this.byName.values()).filter((r) => !r.disabled && !r.invalid && (r.frontmatter.activation === 'always' || forced.has(r.name)));
|
|
79
|
+
}
|
|
80
|
+
setDisabled(name, disabled) {
|
|
81
|
+
const record = this.byName.get(name);
|
|
82
|
+
if (record)
|
|
83
|
+
record.disabled = disabled;
|
|
84
|
+
}
|
|
85
|
+
/** Idempotent: marks a skill as loaded and reports whether it already was. */
|
|
86
|
+
activate(name) {
|
|
87
|
+
const wasActive = this.active.has(name);
|
|
88
|
+
this.active.add(name);
|
|
89
|
+
return wasActive;
|
|
90
|
+
}
|
|
91
|
+
isActive(name) {
|
|
92
|
+
return this.active.has(name);
|
|
93
|
+
}
|
|
94
|
+
get activeNames() {
|
|
95
|
+
return Array.from(this.active).sort();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Drop the session's loaded-skill set. Called by `/reset` (P2-9) — without it
|
|
99
|
+
* the "already loaded earlier in this conversation" hint would keep claiming
|
|
100
|
+
* a skill is in a context that no longer exists.
|
|
101
|
+
*
|
|
102
|
+
* Clearing the frames here rather than at the call site is deliberate (§7.3):
|
|
103
|
+
* "remember to call the other method too" is not a design, and the symptom of
|
|
104
|
+
* forgetting — a ceiling surviving a `/reset` — would be attributed to
|
|
105
|
+
* anything but this.
|
|
106
|
+
*/
|
|
107
|
+
clearActive() {
|
|
108
|
+
this.active.clear();
|
|
109
|
+
this.clearFrames();
|
|
110
|
+
this.failOpenNoticed.clear();
|
|
111
|
+
}
|
|
112
|
+
// -------------------------------------------------------------------------
|
|
113
|
+
// Frame lifecycle (§5.1). Exactly four events move a name; there is no fifth.
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
/** The `skill` tool succeeded: this skill constrains the turn from now on. */
|
|
116
|
+
enterFrame(name) {
|
|
117
|
+
this.frameActive.add(name);
|
|
118
|
+
}
|
|
119
|
+
/** A `/<skill-name>` command ran: constrain the turn its message is about to open. */
|
|
120
|
+
queueFrame(name) {
|
|
121
|
+
this.framePending.add(name);
|
|
122
|
+
}
|
|
123
|
+
/** A new user message arrived: the previous turn's ceiling expires, queued ones promote. */
|
|
124
|
+
beginUserTurn() {
|
|
125
|
+
this.frameActive.clear();
|
|
126
|
+
for (const name of this.framePending)
|
|
127
|
+
this.frameActive.add(name);
|
|
128
|
+
this.framePending.clear();
|
|
129
|
+
this.resetTurnLedgers();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A user message arrived MID-RUN (`steer`). Promotes the queue without
|
|
133
|
+
* clearing what is already in force (D-G3): the turn has not ended, so neither
|
|
134
|
+
* has the skill procedure — and letting an interjection lift the ceiling would
|
|
135
|
+
* make "ask a follow-up question" the documented way around it.
|
|
136
|
+
*
|
|
137
|
+
* The turn ledgers are deliberately NOT reset: this is the same turn.
|
|
138
|
+
*/
|
|
139
|
+
absorbPendingFrames() {
|
|
140
|
+
for (const name of this.framePending)
|
|
141
|
+
this.frameActive.add(name);
|
|
142
|
+
this.framePending.clear();
|
|
143
|
+
}
|
|
144
|
+
/** `/skills unload` — drops the ceiling. Does NOT reclaim context; nothing can. */
|
|
145
|
+
clearFrames() {
|
|
146
|
+
this.frameActive.clear();
|
|
147
|
+
this.framePending.clear();
|
|
148
|
+
this.resetTurnLedgers();
|
|
149
|
+
}
|
|
150
|
+
resetTurnLedgers() {
|
|
151
|
+
this.warnedThisTurn.clear();
|
|
152
|
+
this.deniedThisTurn.clear();
|
|
153
|
+
}
|
|
154
|
+
get frameNames() {
|
|
155
|
+
return Array.from(this.frameActive).sort();
|
|
156
|
+
}
|
|
157
|
+
/** Names queued by a slash command, awaiting the user message they will constrain. */
|
|
158
|
+
get pendingFrameNames() {
|
|
159
|
+
return Array.from(this.framePending).sort();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The frame's records that STILL EXIST and are still usable.
|
|
163
|
+
*
|
|
164
|
+
* Unresolvable / disabled / invalid names are dropped from the result but KEPT
|
|
165
|
+
* in `frameActive`, so `frameNames` can still say which skill was supposed to
|
|
166
|
+
* be constraining this turn. The caller owes the user one notice per lifted
|
|
167
|
+
* skill (§5.6f): uninstall, disable and a flip to `integrity: strict` all end
|
|
168
|
+
* with the ceiling gone, and a ceiling that disappears without a word is the
|
|
169
|
+
* exact failure class this whole iteration exists to remove.
|
|
170
|
+
*/
|
|
171
|
+
frameRecords() {
|
|
172
|
+
const out = [];
|
|
173
|
+
for (const name of this.frameNames) {
|
|
174
|
+
const record = this.byName.get(name);
|
|
175
|
+
if (!record || record.disabled || record.invalid)
|
|
176
|
+
continue;
|
|
177
|
+
out.push(record);
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** True on the first `(skill, tool)` pair this TURN; false afterwards. */
|
|
182
|
+
noteWarnOnce(skill, tool) {
|
|
183
|
+
const key = `${skill}::${tool}`;
|
|
184
|
+
if (this.warnedThisTurn.has(key))
|
|
185
|
+
return false;
|
|
186
|
+
this.warnedThisTurn.add(key);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
/** True on the first mention of `key` this SESSION; false afterwards. */
|
|
190
|
+
noteFailOpenOnce(key) {
|
|
191
|
+
if (this.failOpenNoticed.has(key))
|
|
192
|
+
return false;
|
|
193
|
+
this.failOpenNoticed.add(key);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
/** Count this refusal and return the running total for the tool, this turn. */
|
|
197
|
+
countDeny(tool) {
|
|
198
|
+
const next = (this.deniedThisTurn.get(tool) ?? 0) + 1;
|
|
199
|
+
this.deniedThisTurn.set(tool, next);
|
|
200
|
+
return next;
|
|
201
|
+
}
|
|
202
|
+
/** Swap the whole table after a rescan, preserving the active set. */
|
|
203
|
+
replaceAll(records) {
|
|
204
|
+
this.byName.clear();
|
|
205
|
+
for (const record of records)
|
|
206
|
+
this.add(record);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `skill` tool — Level 2 of progressive disclosure (spec §5.2).
|
|
3
|
+
*
|
|
4
|
+
* WHY A TOOL RESULT AND NOT A SYSTEM-PROMPT REWRITE (D2): `Agent.prompt()`
|
|
5
|
+
* snapshots `this.systemPrompt` when the loop starts, so mutating it mid-run is
|
|
6
|
+
* a no-op for the current turn. A tool result, by contrast, enters the message
|
|
7
|
+
* history immediately, survives `/save` + `/resume` for free, and costs nothing
|
|
8
|
+
* on turns where no skill is used.
|
|
9
|
+
*
|
|
10
|
+
* NAMING (D4): lowercase `skill`, matching the seven existing snake_case tools,
|
|
11
|
+
* rather than Claude Code's `Skill`. The catalog block spells out the mapping so
|
|
12
|
+
* community skills that say "call the Skill tool" still resolve.
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentTool } from '../tools/types.js';
|
|
15
|
+
import type { SkillRegistry } from './skill-registry.js';
|
|
16
|
+
import type { SkillFileRef, SkillPlatform, SkillPolicyView } from './types.js';
|
|
17
|
+
export interface SkillToolDeps {
|
|
18
|
+
registry: SkillRegistry;
|
|
19
|
+
/**
|
|
20
|
+
* Read SKILL.md + list bundled files for a skill.
|
|
21
|
+
* MAY THROW — the Node host wraps sync fs calls (§10.1 error contract), so
|
|
22
|
+
* `execute()` below owns the try/catch. "Never throws" is the TOOL's promise.
|
|
23
|
+
*/
|
|
24
|
+
loadBody: (name: string) => {
|
|
25
|
+
body: string;
|
|
26
|
+
files: SkillFileRef[];
|
|
27
|
+
};
|
|
28
|
+
bodyMaxBytes?: number;
|
|
29
|
+
resultMaxBytes?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Take a policy snapshot for the ceiling line in the rendered body.
|
|
32
|
+
* `undefined` (the port, or its return value) means "render no policy line".
|
|
33
|
+
*
|
|
34
|
+
* MUST BE CALLED AFTER `enterFrame(name)` — see the numbered sequence in
|
|
35
|
+
* `execute()`. The skill being loaded is itself one of the ceiling's sources,
|
|
36
|
+
* so a snapshot taken first would print a permitted set missing the very
|
|
37
|
+
* tools this skill just granted: the one wrong answer a model would act on
|
|
38
|
+
* without hesitating.
|
|
39
|
+
*/
|
|
40
|
+
policySnapshot?: () => SkillPolicyView | undefined;
|
|
41
|
+
/** Host shell family for the Level 3 guidance (D-G14; core never reads `process`). */
|
|
42
|
+
platform?: SkillPlatform;
|
|
43
|
+
}
|
|
44
|
+
export declare function createSkillTool(deps: SkillToolDeps): AgentTool;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `skill` tool — Level 2 of progressive disclosure (spec §5.2).
|
|
3
|
+
*
|
|
4
|
+
* WHY A TOOL RESULT AND NOT A SYSTEM-PROMPT REWRITE (D2): `Agent.prompt()`
|
|
5
|
+
* snapshots `this.systemPrompt` when the loop starts, so mutating it mid-run is
|
|
6
|
+
* a no-op for the current turn. A tool result, by contrast, enters the message
|
|
7
|
+
* history immediately, survives `/save` + `/resume` for free, and costs nothing
|
|
8
|
+
* on turns where no skill is used.
|
|
9
|
+
*
|
|
10
|
+
* NAMING (D4): lowercase `skill`, matching the seven existing snake_case tools,
|
|
11
|
+
* rather than Claude Code's `Skill`. The catalog block spells out the mapping so
|
|
12
|
+
* community skills that say "call the Skill tool" still resolve.
|
|
13
|
+
*/
|
|
14
|
+
import { errorResult, textResult } from '../tools/helpers.js';
|
|
15
|
+
import { renderSkillBody, suggestSkillNames } from './disclosure.js';
|
|
16
|
+
const DESCRIPTION = 'Load the full instructions of an installed skill before doing work it covers. ' +
|
|
17
|
+
'Pick the exact name from <available_skills>. Returns the skill body plus a list of ' +
|
|
18
|
+
'bundled files you can then read with read_file or run with bash. ' +
|
|
19
|
+
'Loading a skill is cheap - prefer it over re-deriving a procedure yourself.';
|
|
20
|
+
export function createSkillTool(deps) {
|
|
21
|
+
return {
|
|
22
|
+
name: 'skill',
|
|
23
|
+
label: 'Skill',
|
|
24
|
+
description: DESCRIPTION,
|
|
25
|
+
parameters: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
name: { type: 'string', description: 'Exact skill name from <available_skills>.' },
|
|
29
|
+
arguments: { type: 'string', description: 'Optional free-form arguments for the skill.' },
|
|
30
|
+
force: {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
description: 'Re-send the full body even if this skill was already loaded in this conversation. ' +
|
|
33
|
+
'Only needed if you cannot find the earlier copy.',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: ['name'],
|
|
37
|
+
},
|
|
38
|
+
async execute(_id, rawParams) {
|
|
39
|
+
const params = rawParams;
|
|
40
|
+
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
41
|
+
if (name.length === 0)
|
|
42
|
+
return errorResult('skill requires a "name" parameter.');
|
|
43
|
+
const record = deps.registry.get(name);
|
|
44
|
+
if (!record) {
|
|
45
|
+
const suggestions = suggestSkillNames(name, deps.registry.names());
|
|
46
|
+
const hint = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(', ')}?` : '';
|
|
47
|
+
return errorResult(`Unknown skill "${name}".${hint} Run /skills to list all.`);
|
|
48
|
+
}
|
|
49
|
+
if (record.disabled) {
|
|
50
|
+
return errorResult(`Skill "${name}" is disabled. Enable it with /skills enable ${name}.`);
|
|
51
|
+
}
|
|
52
|
+
if (record.invalid) {
|
|
53
|
+
const first = record.issues.find((i) => i.level === 'error');
|
|
54
|
+
return errorResult(`Skill "${name}" failed validation: ${first ? `${first.code} - ${first.message}` : 'unknown error'}.`);
|
|
55
|
+
}
|
|
56
|
+
// The single place where a host-side throw is converted into a diagnosable
|
|
57
|
+
// tool error. The engine would otherwise turn it into a generic stack trace.
|
|
58
|
+
let loaded;
|
|
59
|
+
try {
|
|
60
|
+
loaded = deps.loadBody(name);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
return errorResult(`Could not read SKILL.md for "${name}": ${message}`);
|
|
65
|
+
}
|
|
66
|
+
// ── The order below is load-bearing; do not reorder (§7.2). ──────────
|
|
67
|
+
// 1. Hydrate.
|
|
68
|
+
record.body = loaded.body;
|
|
69
|
+
record.files = loaded.files;
|
|
70
|
+
// 2. Record the session-level load and learn whether it is a repeat.
|
|
71
|
+
const alreadyLoaded = deps.registry.activate(name);
|
|
72
|
+
// 3. Enter the frame BEFORE step 4 — this skill is one of the ceiling's
|
|
73
|
+
// own sources, and a digest counts as a "I am using this" statement
|
|
74
|
+
// just as much as a full load does, so the ceiling applies either way.
|
|
75
|
+
deps.registry.enterFrame(name);
|
|
76
|
+
// 4. Only now is a policy snapshot truthful.
|
|
77
|
+
const policy = deps.policySnapshot?.();
|
|
78
|
+
// 5. Repeat loads get the digest unless the model explicitly asks otherwise.
|
|
79
|
+
const force = params.force === true;
|
|
80
|
+
const mode = alreadyLoaded && !force ? 'digest' : 'full';
|
|
81
|
+
const args = typeof params.arguments === 'string' ? params.arguments : '';
|
|
82
|
+
// 6. Render.
|
|
83
|
+
return textResult(renderSkillBody(record, {
|
|
84
|
+
...(deps.bodyMaxBytes !== undefined ? { bodyMaxBytes: deps.bodyMaxBytes } : {}),
|
|
85
|
+
...(deps.resultMaxBytes !== undefined ? { resultMaxBytes: deps.resultMaxBytes } : {}),
|
|
86
|
+
// The legacy one-line hint belongs to the full re-send only; the digest
|
|
87
|
+
// states the same fact in its own opening line.
|
|
88
|
+
...(mode === 'full' && alreadyLoaded ? { alreadyLoaded: true } : {}),
|
|
89
|
+
mode,
|
|
90
|
+
...(policy ? { policy } : {}),
|
|
91
|
+
...(deps.platform ? { platform: deps.platform } : {}),
|
|
92
|
+
...(args.length > 0 ? { arguments: args } : {}),
|
|
93
|
+
}));
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
import type { ToolPolicyDecision, ToolPolicyInput, ToolPolicyVerdict } from './types.js';
|
|
20
|
+
/** Lower-case and strip `_`, `-` and whitespace: `Read`→`read`, `read_file`→`readfile`. */
|
|
21
|
+
export declare function normalizeToolAlias(name: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Claude Code's vocabulary → this host's snake_case names.
|
|
24
|
+
*
|
|
25
|
+
* Keys are already normalized. This table is why `.claude/skills` interop — the
|
|
26
|
+
* README's headline feature — survives enforcement at all: without it every
|
|
27
|
+
* community declaration (`allowed-tools: [Read, Bash]`) would resolve to nothing
|
|
28
|
+
* and every such skill would take the fail-open path, making the whole ceiling
|
|
29
|
+
* ornamental (FG2 / RG2).
|
|
30
|
+
*/
|
|
31
|
+
export declare const SKILL_TOOL_ALIASES: Readonly<Record<string, string>>;
|
|
32
|
+
/**
|
|
33
|
+
* Capabilities this host simply does not have. A declaration naming one is
|
|
34
|
+
* DROPPED, not treated as unresolvable (D-G5).
|
|
35
|
+
*
|
|
36
|
+
* If these counted as "cannot resolve", nearly every community skill would take
|
|
37
|
+
* the fail-open path and enforcement would be theatre. Dropping them changes no
|
|
38
|
+
* executable set — the model could never have called them — while leaving the
|
|
39
|
+
* rest of the declaration in force. That is precision, not a concession.
|
|
40
|
+
*
|
|
41
|
+
* THIS TABLE ONLY GROWS. Removing an entry pushes already-working skills into
|
|
42
|
+
* fail-open with no symptom whatsoever.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SKILL_TOOL_KNOWN_ABSENT: ReadonlySet<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve one declared name against the live tool list.
|
|
47
|
+
*
|
|
48
|
+
* `string` — a tool on this host.
|
|
49
|
+
* `'absent'` — a real Claude Code tool this host lacks; drop it silently.
|
|
50
|
+
* `null` — unrecognisable; the whole skill then waives its contribution.
|
|
51
|
+
*
|
|
52
|
+
* THE ORDER OF THE FOUR STEPS IS THE SEMANTICS — do not reshuffle it. Exact
|
|
53
|
+
* match first means a host that adds a new tool needs no table maintenance. The
|
|
54
|
+
* alias table is consulted BEFORE the absent table so that mistakenly listing a
|
|
55
|
+
* tool this host actually has could never silently downgrade a skill.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveDeclaredTool(name: string, registered: readonly string[]): string | 'absent' | null;
|
|
58
|
+
/**
|
|
59
|
+
* Compute the ceiling for a turn.
|
|
60
|
+
*
|
|
61
|
+
* TWO INVARIANTS THIS FUNCTION OWNS (I-G1 / I-G2, pinned by AC-G23):
|
|
62
|
+
*
|
|
63
|
+
* I-G1 `registered.length === 0` ⇒ `allowed === null`.
|
|
64
|
+
* I-G2 `allowed !== null` ⇒ the set is NON-EMPTY.
|
|
65
|
+
*
|
|
66
|
+
* Both matter because the failure they exclude is catastrophic and silent: an
|
|
67
|
+
* empty permitted set is not "a tight ceiling", it is an agent that can no
|
|
68
|
+
* longer do anything, for a reason no user could ever guess. The construction
|
|
69
|
+
* makes them hold — every unresolved name waives its whole skill, and the final
|
|
70
|
+
* guard below refuses to hand back an empty set — rather than leaving them to be
|
|
71
|
+
* re-derived by whoever refactors this next.
|
|
72
|
+
*/
|
|
73
|
+
export declare function computeToolPolicy(input: ToolPolicyInput): ToolPolicyDecision;
|
|
74
|
+
/**
|
|
75
|
+
* Decide a single call. Deterministic, allocation-light, and safe to run on
|
|
76
|
+
* every tool invocation — `allowed === null` short-circuits before any string
|
|
77
|
+
* work, which is the common case.
|
|
78
|
+
*/
|
|
79
|
+
export declare function evaluateToolCall(tool: string, d: ToolPolicyDecision): ToolPolicyVerdict;
|
|
80
|
+
/**
|
|
81
|
+
* The line appended to a refusal once the same tool has been refused
|
|
82
|
+
* `TOOL_POLICY_DENY_ESCALATE_AT` times in one turn (§5.5).
|
|
83
|
+
*
|
|
84
|
+
* Appended to the SAME message rather than emitted as a new one: a second
|
|
85
|
+
* message would be one more thing for the model to skim past, whereas a longer
|
|
86
|
+
* version of the text it is already reading is unavoidable.
|
|
87
|
+
*/
|
|
88
|
+
export declare function renderDenyEscalation(tool: string, count: number): string;
|
|
89
|
+
/** The matching one-liner for the user, emitted once per (turn, tool). */
|
|
90
|
+
export declare function renderDenyEscalationNotice(skill: string, tool: string, count: number): string;
|
|
91
|
+
export { TOOL_POLICY_DENY_ESCALATE_AT };
|