@theokit/sdk 2.3.0 → 2.5.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 +113 -0
- package/dist/a2a/index.cjs +103 -48
- package/dist/a2a/index.cjs.map +1 -1
- package/dist/a2a/index.js +104 -49
- package/dist/a2a/index.js.map +1 -1
- package/dist/compaction.cjs +78 -0
- package/dist/compaction.cjs.map +1 -0
- package/dist/compaction.d.cts +76 -0
- package/dist/compaction.d.ts +76 -0
- package/dist/compaction.js +70 -0
- package/dist/compaction.js.map +1 -0
- package/dist/{cron-B_H8rn-j.d.cts → cron-B656C3iq.d.cts} +8 -0
- package/dist/{cron-DX6HbHxd.d.ts → cron-CM2M9mhB.d.ts} +8 -0
- package/dist/cron.cjs +104 -57
- package/dist/cron.cjs.map +1 -1
- package/dist/cron.d.cts +1 -1
- package/dist/cron.d.ts +1 -1
- package/dist/cron.js +104 -57
- package/dist/cron.js.map +1 -1
- package/dist/eval.cjs +296 -73
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.d.cts +2 -0
- package/dist/eval.d.ts +2 -0
- package/dist/eval.js +295 -75
- package/dist/eval.js.map +1 -1
- package/dist/index.cjs +135 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -7
- package/dist/index.d.ts +42 -7
- package/dist/index.js +135 -66
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-loop/loop.d.ts +5 -0
- package/dist/internal/eval/code-runner.d.ts +28 -0
- package/dist/internal/llm/model-capabilities.d.ts +40 -0
- package/dist/internal/llm/model-identifier.d.ts +9 -1
- package/dist/internal/llm/model-option.d.ts +38 -0
- package/dist/internal/persistence/index.cjs +68 -0
- package/dist/internal/persistence/index.cjs.map +1 -1
- package/dist/internal/persistence/index.d.cts +1 -0
- package/dist/internal/persistence/index.d.ts +1 -0
- package/dist/internal/persistence/index.js +65 -1
- package/dist/internal/persistence/index.js.map +1 -1
- package/dist/internal/persistence/jsonl.d.cts +34 -0
- package/dist/internal/persistence/jsonl.d.ts +34 -0
- package/dist/internal/runtime/compression/compression-attempt.d.ts +24 -0
- package/dist/internal/runtime/compression/compression-config.d.ts +33 -0
- package/dist/internal/runtime/compression/compression-decision.d.ts +10 -0
- package/dist/internal/runtime/compression/compression-helpers.d.ts +18 -0
- package/dist/internal/runtime/compression/compression-model-registry.d.ts +41 -0
- package/dist/internal/runtime/compression/compression-summarizer.d.ts +29 -0
- package/dist/internal/runtime/context/project-instructions.d.ts +66 -0
- package/dist/internal/runtime/context/replay-history.d.ts +43 -0
- package/dist/internal/runtime/hooks/hooks-frontmatter.d.ts +1 -1
- package/dist/internal/runtime/skills/discover-skills.d.ts +68 -0
- package/dist/internal/runtime/skills/skills-block.d.ts +18 -0
- package/dist/internal/runtime/skills/subagent-tool-scope.d.ts +25 -0
- package/dist/messages.cjs +24 -0
- package/dist/messages.cjs.map +1 -0
- package/dist/messages.d.cts +33 -0
- package/dist/messages.d.ts +33 -0
- package/dist/messages.js +20 -0
- package/dist/messages.js.map +1 -0
- package/dist/models.cjs +233 -0
- package/dist/models.cjs.map +1 -0
- package/dist/models.d.cts +16 -0
- package/dist/models.d.ts +16 -0
- package/dist/models.js +228 -0
- package/dist/models.js.map +1 -0
- package/dist/permission-engine.d.ts +12 -4
- package/dist/project.cjs +149 -0
- package/dist/project.cjs.map +1 -0
- package/dist/project.d.cts +14 -0
- package/dist/project.d.ts +14 -0
- package/dist/project.js +146 -0
- package/dist/project.js.map +1 -0
- package/dist/sandbox/index.cjs +71 -1
- package/dist/sandbox/index.cjs.map +1 -1
- package/dist/sandbox/index.d.cts +1 -0
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/index.js +70 -2
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/provision.d.cts +53 -0
- package/dist/sandbox/provision.d.ts +53 -0
- package/dist/sandbox/shell-escape.d.cts +8 -0
- package/dist/sandbox/shell-escape.d.ts +8 -0
- package/dist/scorers.d.ts +19 -1
- package/dist/skills.cjs +282 -0
- package/dist/skills.cjs.map +1 -0
- package/dist/skills.d.cts +19 -0
- package/dist/skills.d.ts +19 -0
- package/dist/skills.js +279 -0
- package/dist/skills.js.map +1 -0
- package/dist/subagents.cjs +24 -0
- package/dist/subagents.cjs.map +1 -0
- package/dist/subagents.d.cts +14 -0
- package/dist/subagents.d.ts +14 -0
- package/dist/subagents.js +21 -0
- package/dist/subagents.js.map +1 -0
- package/dist/types/agent.d.ts +8 -0
- package/dist/types/eval.d.ts +71 -0
- package/package.json +74 -14
package/dist/skills.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { readFile, readdir } from 'fs/promises';
|
|
2
|
+
import { join, resolve, sep, dirname } from 'path';
|
|
3
|
+
import 'crypto';
|
|
4
|
+
import { realpathSync, lstatSync, readlinkSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
// src/internal/runtime/skills/discover-skills.ts
|
|
7
|
+
|
|
8
|
+
// src/errors.ts
|
|
9
|
+
var TheokitAgentError = class extends Error {
|
|
10
|
+
name = "TheokitAgentError";
|
|
11
|
+
isRetryable;
|
|
12
|
+
code;
|
|
13
|
+
protoErrorCode;
|
|
14
|
+
metadata;
|
|
15
|
+
constructor(message, options = {}) {
|
|
16
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
17
|
+
this.isRetryable = options.isRetryable ?? false;
|
|
18
|
+
if (options.code !== void 0) this.code = options.code;
|
|
19
|
+
if (options.protoErrorCode !== void 0) this.protoErrorCode = options.protoErrorCode;
|
|
20
|
+
if (options.metadata !== void 0) this.metadata = options.metadata;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var ConfigurationError = class extends TheokitAgentError {
|
|
24
|
+
name = "ConfigurationError";
|
|
25
|
+
constructor(message, options = {}) {
|
|
26
|
+
super(message, { ...options, isRetryable: false });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var PathTraversalError = class extends ConfigurationError {
|
|
30
|
+
name = "PathTraversalError";
|
|
31
|
+
constructor(input, resolvedPath) {
|
|
32
|
+
super(`Path traversal attempt: ${input} \u2192 ${resolvedPath}`, {
|
|
33
|
+
code: "path_traversal"
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function safePathJoin(base, ...parts) {
|
|
38
|
+
if (base === "") {
|
|
39
|
+
throw new Error("safePathJoin: base must be non-empty");
|
|
40
|
+
}
|
|
41
|
+
rejectNulAndControlChars(base, "base");
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
rejectNulAndControlChars(part, "path segment");
|
|
44
|
+
}
|
|
45
|
+
const baseResolved = resolve(base);
|
|
46
|
+
const target = resolve(base, ...parts);
|
|
47
|
+
if (target !== baseResolved && !target.startsWith(baseResolved + sep)) {
|
|
48
|
+
throw new PathTraversalError(parts.join("/"), target);
|
|
49
|
+
}
|
|
50
|
+
return target;
|
|
51
|
+
}
|
|
52
|
+
function rejectNulAndControlChars(input, role) {
|
|
53
|
+
for (let i = 0; i < input.length; i++) {
|
|
54
|
+
const code = input.charCodeAt(i);
|
|
55
|
+
if (code === 0 || code >= 1 && code <= 31 || code === 127) {
|
|
56
|
+
const label = code === 0 ? "<nul-byte>" : `<control-char-0x${code.toString(16)}>`;
|
|
57
|
+
throw new PathTraversalError(`${role}: ${input}`, label);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function assertNoSymlinkEscape(path, base) {
|
|
62
|
+
rejectNulAndControlChars(path, "path");
|
|
63
|
+
rejectNulAndControlChars(base, "base");
|
|
64
|
+
let baseResolved;
|
|
65
|
+
try {
|
|
66
|
+
baseResolved = realpathSync(base);
|
|
67
|
+
} catch {
|
|
68
|
+
baseResolved = resolve(base);
|
|
69
|
+
}
|
|
70
|
+
const resolved = realpathOfDeepestExisting(path);
|
|
71
|
+
if (resolved === void 0) return;
|
|
72
|
+
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + sep)) {
|
|
73
|
+
throw new PathTraversalError(`symlink ${path}`, resolved);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function realpathOfDeepestExisting(path) {
|
|
77
|
+
try {
|
|
78
|
+
return realpathSync(path);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const stat = lstatSync(path);
|
|
83
|
+
if (stat.isSymbolicLink()) {
|
|
84
|
+
const target = readlinkSync(path);
|
|
85
|
+
const parentReal = realpathOfDeepestExisting(dirname(path));
|
|
86
|
+
const parentBase = parentReal ?? dirname(path);
|
|
87
|
+
return resolve(parentBase, target);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
let cursor = dirname(path);
|
|
92
|
+
let suffix = path.slice(cursor.length);
|
|
93
|
+
while (cursor !== dirname(cursor)) {
|
|
94
|
+
try {
|
|
95
|
+
const real = realpathSync(cursor);
|
|
96
|
+
return resolve(real, `.${suffix}`);
|
|
97
|
+
} catch {
|
|
98
|
+
suffix = path.slice(dirname(cursor).length);
|
|
99
|
+
cursor = dirname(cursor);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
async function readWorkspaceDir(root, errorCode, describe) {
|
|
105
|
+
try {
|
|
106
|
+
return await readdir(root, { withFileTypes: true });
|
|
107
|
+
} catch (cause) {
|
|
108
|
+
const err = cause;
|
|
109
|
+
if (err.code === "ENOENT") return [];
|
|
110
|
+
throw new ConfigurationError(`Failed to read ${describe}: ${root}`, {
|
|
111
|
+
code: errorCode,
|
|
112
|
+
cause
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/internal/runtime/context/yaml-frontmatter.ts
|
|
118
|
+
function parseSimpleYaml(text) {
|
|
119
|
+
const fields = {};
|
|
120
|
+
for (const line of text.split(/\r?\n/)) {
|
|
121
|
+
const colonIndex = line.indexOf(":");
|
|
122
|
+
if (colonIndex === -1) continue;
|
|
123
|
+
const key = line.slice(0, colonIndex).trim();
|
|
124
|
+
if (key.length === 0) continue;
|
|
125
|
+
const raw = line.slice(colonIndex + 1).trim();
|
|
126
|
+
fields[key] = coerce(raw);
|
|
127
|
+
}
|
|
128
|
+
return fields;
|
|
129
|
+
}
|
|
130
|
+
function coerce(raw) {
|
|
131
|
+
if (raw.length === 0) return void 0;
|
|
132
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
133
|
+
return raw.slice(1, -1).split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
134
|
+
}
|
|
135
|
+
if (raw === "true" || raw === "false") return raw === "true";
|
|
136
|
+
const n = Number(raw);
|
|
137
|
+
if (Number.isFinite(n) && raw === String(n)) return n;
|
|
138
|
+
return raw;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/internal/runtime/skills/skill-frontmatter.ts
|
|
142
|
+
function asString(v) {
|
|
143
|
+
return typeof v === "string" ? v : void 0;
|
|
144
|
+
}
|
|
145
|
+
function toStringFields(raw) {
|
|
146
|
+
const out = {};
|
|
147
|
+
for (const [k, v] of Object.entries(raw)) out[k] = asString(v);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function parseSkillFrontmatter(raw, fallbackName) {
|
|
151
|
+
const fields = extractAndParseFrontmatter(raw, fallbackName);
|
|
152
|
+
const name = resolveName(fields, fallbackName);
|
|
153
|
+
ensureRequiredFields(fields, name);
|
|
154
|
+
return buildFrontmatter(fields, name);
|
|
155
|
+
}
|
|
156
|
+
function extractAndParseFrontmatter(raw, fallbackName) {
|
|
157
|
+
const match = /^---\s*\n([\s\S]*?)\n---\s*\n/.exec(raw);
|
|
158
|
+
if (match === null) {
|
|
159
|
+
throw new ConfigurationError(`Skill ${fallbackName} is missing frontmatter`, {
|
|
160
|
+
code: "missing_frontmatter"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const frontmatter = match[1] ?? "";
|
|
164
|
+
try {
|
|
165
|
+
return toStringFields(parseSimpleYaml(frontmatter));
|
|
166
|
+
} catch (cause) {
|
|
167
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
168
|
+
throw new ConfigurationError(
|
|
169
|
+
`Skill ${fallbackName} has malformed YAML frontmatter: ${detail}`,
|
|
170
|
+
{ code: "schema_invalid", cause }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function resolveName(fields, fallbackName) {
|
|
175
|
+
if (hasContent(fields.name)) return fields.name;
|
|
176
|
+
if (hasContent(fallbackName)) return fallbackName;
|
|
177
|
+
throw new ConfigurationError("Skill at unknown path is missing required field: name", {
|
|
178
|
+
code: "schema_invalid"
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function ensureRequiredFields(fields, name) {
|
|
182
|
+
if (!hasContent(fields.description)) {
|
|
183
|
+
throw new ConfigurationError(`Skill ${name} is missing required field: description`, {
|
|
184
|
+
code: "schema_invalid"
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function buildFrontmatter(fields, name) {
|
|
189
|
+
const description = fields.description;
|
|
190
|
+
if (description === void 0) {
|
|
191
|
+
throw new ConfigurationError(`Skill ${name} missing description`, { code: "schema_invalid" });
|
|
192
|
+
}
|
|
193
|
+
const result = { name, description };
|
|
194
|
+
if (hasContent(fields.category)) result.category = fields.category;
|
|
195
|
+
const deps = parseDependencies(fields.dependencies);
|
|
196
|
+
if (deps !== void 0) result.dependencies = deps;
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
function parseDependencies(raw) {
|
|
200
|
+
if (!hasContent(raw)) return void 0;
|
|
201
|
+
const deps = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
202
|
+
return deps.length > 0 ? deps : void 0;
|
|
203
|
+
}
|
|
204
|
+
function hasContent(value) {
|
|
205
|
+
return value !== void 0 && value.trim().length > 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/internal/runtime/skills/discover-skills.ts
|
|
209
|
+
async function discoverSkills(dir, options) {
|
|
210
|
+
let entries;
|
|
211
|
+
try {
|
|
212
|
+
entries = await readWorkspaceDir(dir, "skills_read_error", "skills directory");
|
|
213
|
+
} catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
const skills = [];
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
if (!entry.isDirectory()) continue;
|
|
219
|
+
let skillDir;
|
|
220
|
+
try {
|
|
221
|
+
skillDir = safePathJoin(dir, entry.name);
|
|
222
|
+
assertNoSymlinkEscape(skillDir, dir);
|
|
223
|
+
} catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
227
|
+
let raw;
|
|
228
|
+
try {
|
|
229
|
+
raw = await readFile(skillPath, "utf8");
|
|
230
|
+
} catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const skill = tryParseSkill(raw, entry.name, skillPath, options);
|
|
234
|
+
if (skill !== void 0) skills.push(skill);
|
|
235
|
+
}
|
|
236
|
+
return skills;
|
|
237
|
+
}
|
|
238
|
+
function tryParseSkill(raw, fallbackName, source, options) {
|
|
239
|
+
try {
|
|
240
|
+
const frontmatter = parseSkillFrontmatter(raw, fallbackName);
|
|
241
|
+
const skill = {
|
|
242
|
+
name: frontmatter.name,
|
|
243
|
+
description: frontmatter.description,
|
|
244
|
+
source
|
|
245
|
+
};
|
|
246
|
+
if (frontmatter.category !== void 0) skill.category = frontmatter.category;
|
|
247
|
+
if (frontmatter.dependencies !== void 0) skill.dependencies = frontmatter.dependencies;
|
|
248
|
+
return skill;
|
|
249
|
+
} catch (cause) {
|
|
250
|
+
if (cause instanceof ConfigurationError) {
|
|
251
|
+
options?.onInvalidSkill?.({
|
|
252
|
+
name: fallbackName,
|
|
253
|
+
source,
|
|
254
|
+
code: cause.code ?? "unknown",
|
|
255
|
+
message: cause.message
|
|
256
|
+
});
|
|
257
|
+
return void 0;
|
|
258
|
+
}
|
|
259
|
+
throw cause;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/internal/runtime/system-prompt/escape.ts
|
|
264
|
+
var escapeBlockBody = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
265
|
+
|
|
266
|
+
// src/internal/runtime/skills/skills-block.ts
|
|
267
|
+
function buildSkillsBlock(skills) {
|
|
268
|
+
if (skills.length === 0) return void 0;
|
|
269
|
+
const lines = skills.map(
|
|
270
|
+
(skill) => ` - ${escapeBlockBody(skill.name)}: ${escapeBlockBody(skill.description)}`
|
|
271
|
+
);
|
|
272
|
+
return `<skills>
|
|
273
|
+
${lines.join("\n")}
|
|
274
|
+
</skills>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { buildSkillsBlock, discoverSkills };
|
|
278
|
+
//# sourceMappingURL=skills.js.map
|
|
279
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/internal/security/path-guard.ts","../src/internal/runtime/config/workspace-dir.ts","../src/internal/runtime/context/yaml-frontmatter.ts","../src/internal/runtime/skills/skill-frontmatter.ts","../src/internal/runtime/skills/discover-skills.ts","../src/internal/runtime/system-prompt/escape.ts","../src/internal/runtime/skills/skills-block.ts"],"names":[],"mappings":"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","file":"skills.js","sourcesContent":["import { defaultRetriableForCode } from \"./internal/default-retriable.js\";\nimport { redactSecrets } from \"./internal/security/redact.js\";\nimport type { RunOperation } from \"./types/run.js\";\n\n/**\n * Finite, machine-readable error codes for provider-originated errors\n * (ADR D66). Consumers can `switch (err.metadata?.code)` exhaustively\n * — adding a new variant is an explicit decision + test coverage.\n *\n * @public\n */\nexport type ErrorCode =\n | \"rate_limit\"\n | \"auth_failed\"\n | \"invalid_request\"\n | \"timeout\"\n | \"server_error\"\n | \"context_too_long\"\n | \"content_filtered\"\n | \"model_unavailable\"\n | \"network\"\n | \"quota_exceeded\"\n | \"unknown\";\n\n/**\n * Codes used by {@link AgentRunError} (Production-Readiness #3, ADR D311).\n *\n * Superset of {@link ErrorCode} extended with codes that do NOT originate\n * from a provider HTTP response:\n *\n * - `quota_exceeded` — billing limit hit (provider 402 or signalled error)\n * - `tool_runtime_error` — custom tool handler threw inside dispatch\n * - `aborted` — caller's `AbortSignal` fired (Phase 4)\n * - `invalid_model` — model id rejected by provider (400 \"model not found\")\n * - `safety_blocked` — provider safety filter blocked req or resp\n * - `provider_unreachable` — DNS/TCP/timeout/5xx at transport boundary\n *\n * The `& {}` tail keeps the literal-union ergonomics (autocomplete) while\n * accepting any string for forward compatibility with constructor calls\n * that pass arbitrary code values (legacy callers).\n *\n * @public\n */\n/**\n * T1.1 — closed literal union for `AgentRunError.code`. The previous\n * `(string & {})` escape hatch let arbitrary strings slip into the type\n * surface and defeated exhaustive `switch (code)` discrimination. This is\n * the canonical closed form. `AgentRunErrorCode` is re-aliased below for\n * source-level back-compat.\n *\n * Adding a new code: append the literal here AND audit every `switch (err.code)`\n * in callers. Type-checker enforces the audit via the `default: assertNever(code)`\n * convention.\n *\n * @public\n */\nexport type KnownAgentRunErrorCode =\n | ErrorCode\n | \"quota_exceeded\"\n | \"tool_runtime_error\"\n | \"aborted\"\n | \"invalid_model\"\n | \"safety_blocked\"\n | \"provider_unreachable\";\n\n/**\n * Back-compat alias of {@link KnownAgentRunErrorCode}. Pre-T1.1 callers that\n * imported `AgentRunErrorCode` keep working; new code SHOULD prefer\n * `KnownAgentRunErrorCode` to make the closed-union intent explicit.\n *\n * @public\n */\nexport type AgentRunErrorCode = KnownAgentRunErrorCode;\n\n/** Snapshot of every known code at runtime — used by the boundary coercer. */\nconst KNOWN_AGENT_RUN_ERROR_CODES = new Set<string>([\n \"rate_limit\",\n \"auth_failed\",\n \"invalid_request\",\n \"timeout\",\n \"server_error\",\n \"context_too_long\",\n \"content_filtered\",\n \"model_unavailable\",\n \"network\",\n \"unknown\",\n \"quota_exceeded\",\n \"tool_runtime_error\",\n \"aborted\",\n \"invalid_model\",\n \"safety_blocked\",\n \"provider_unreachable\",\n]);\n\n/**\n * T1.1 boundary helper — coerce an arbitrary string (typically arriving from\n * a downstream `RunErrorDetail.code` or a deserialized cloud response) into a\n * `KnownAgentRunErrorCode`. Unknown strings collapse to `\"unknown\"` so the\n * closed type contract holds without forcing every caller to switch.\n *\n * @internal\n */\nexport function coerceToKnownAgentRunErrorCode(code: string | undefined): KnownAgentRunErrorCode {\n if (code !== undefined && KNOWN_AGENT_RUN_ERROR_CODES.has(code)) {\n return code as KnownAgentRunErrorCode;\n }\n return \"unknown\";\n}\n\n/**\n * Structured context for errors that originated from a provider HTTP\n * call (ADR D65). Lets callers retry with the right backoff (`retryAfter`),\n * surface actionable diagnostics (`provider`, `endpoint`), and inspect the\n * raw response body when needed (`raw`, capped at ~2KB by the mapper).\n *\n * @public\n */\nexport interface ErrorMetadata {\n /** Provider canonical name (e.g., `\"anthropic\"`, `\"openai\"`, `\"openrouter\"`, `\"gemini\"`). */\n provider: string;\n /** HTTP endpoint that failed (e.g., `\"/v1/messages\"`, `\"/v1/chat/completions\"`). */\n endpoint: string;\n /** Machine-readable error code (finite enum). */\n code: ErrorCode;\n /** HTTP status code if applicable. */\n statusCode?: number;\n /** Seconds to wait before retry, per provider's `retry-after` header (numeric form only). */\n retryAfter?: number;\n /** Raw response body for debugging (truncated to ~2KB by the mapper). */\n raw?: unknown;\n}\n\n/**\n * Base class for all errors thrown by `@theokit/sdk`.\n *\n * Use `isRetryable` to drive retry/backoff logic. `code` and `protoErrorCode`\n * are populated for server-originated errors when available. `metadata`\n * (ADR D65) carries structured `{ provider, endpoint, code, ... }` when\n * the error originated from a provider HTTP call.\n *\n * @public\n */\nexport class TheokitAgentError extends Error {\n override readonly name: string = \"TheokitAgentError\";\n readonly isRetryable: boolean;\n readonly code?: string;\n readonly protoErrorCode?: string;\n readonly metadata?: ErrorMetadata;\n\n constructor(\n message: string,\n options: {\n isRetryable?: boolean;\n code?: string;\n protoErrorCode?: string;\n cause?: unknown;\n metadata?: ErrorMetadata;\n } = {},\n ) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined);\n this.isRetryable = options.isRetryable ?? false;\n if (options.code !== undefined) this.code = options.code;\n if (options.protoErrorCode !== undefined) this.protoErrorCode = options.protoErrorCode;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n}\n\n/**\n * Invalid API key, not logged in, insufficient permissions.\n *\n * @public\n */\nexport class AuthenticationError extends TheokitAgentError {\n override readonly name: string = \"AuthenticationError\";\n\n constructor(\n message: string,\n options: { code?: string; cause?: unknown; metadata?: ErrorMetadata } = {},\n ) {\n super(message, { ...options, isRetryable: false });\n }\n}\n\n/**\n * Too many requests or usage limits exceeded.\n *\n * @public\n */\nexport class RateLimitError extends TheokitAgentError {\n override readonly name: string = \"RateLimitError\";\n\n constructor(\n message: string,\n options: { code?: string; cause?: unknown; metadata?: ErrorMetadata } = {},\n ) {\n super(message, { ...options, isRetryable: true });\n }\n}\n\n/**\n * Invalid model, bad request parameters, malformed options.\n *\n * @public\n */\nexport class ConfigurationError extends TheokitAgentError {\n override readonly name: string = \"ConfigurationError\";\n\n constructor(\n message: string,\n options: { code?: string; cause?: unknown; metadata?: ErrorMetadata } = {},\n ) {\n super(message, { ...options, isRetryable: false });\n }\n}\n\n/**\n * Thrown when creating a cloud agent for a repo whose SCM provider is not\n * connected. Use `helpUrl` to point the user at the right reconnect flow.\n *\n * @public\n */\nexport class IntegrationNotConnectedError extends ConfigurationError {\n override readonly name: string = \"IntegrationNotConnectedError\";\n readonly provider: string;\n readonly helpUrl: string;\n\n constructor(\n message: string,\n options: {\n provider: string;\n helpUrl: string;\n code?: string;\n cause?: unknown;\n metadata?: ErrorMetadata;\n },\n ) {\n super(message, options);\n this.provider = options.provider;\n this.helpUrl = options.helpUrl;\n }\n}\n\n/**\n * Service unavailable, timeout, transport-level failure.\n *\n * @public\n */\nexport class NetworkError extends TheokitAgentError {\n override readonly name: string = \"NetworkError\";\n\n constructor(\n message: string,\n options: { code?: string; cause?: unknown; metadata?: ErrorMetadata } = {},\n ) {\n super(message, { ...options, isRetryable: true });\n }\n}\n\n/**\n * Catch-all for unclassified server or runtime errors.\n *\n * @public\n */\nexport class UnknownAgentError extends TheokitAgentError {\n override readonly name: string = \"UnknownAgentError\";\n\n constructor(\n message: string,\n options: { code?: string; cause?: unknown; metadata?: ErrorMetadata } = {},\n ) {\n super(message, { ...options, isRetryable: false });\n }\n}\n\n/**\n * Thrown by `Agent.prompt` (and helpers that go through `run.wait()`) when\n * the option `{ throwOnError: true }` is set and the run terminates with\n * `status: 'error'`. Carries the structured `RunResult.error` fields so\n * callers can `catch` once and branch on `code` / `provider` instead of\n * unwrapping the run.\n *\n * Extends {@link TheokitAgentError} per ADR D65 — no new hierarchy.\n *\n * @example\n * try {\n * await Agent.prompt(msg, { apiKey, model, throwOnError: true });\n * } catch (err) {\n * if (err instanceof AgentRunError && err.code === 'auth_failed') {\n * // bad key\n * }\n * }\n *\n * @public\n */\nexport class AgentRunError extends TheokitAgentError {\n override readonly name: string = \"AgentRunError\";\n readonly provider?: string;\n readonly raw?: string;\n /** Provider's request id (`x-request-id` / `request-id` header). Useful for support tickets. */\n readonly requestId?: string;\n /** SDK conversation id this error was raised inside. */\n readonly conversationId?: string;\n\n constructor(\n message: string,\n options: {\n code: AgentRunErrorCode;\n provider?: string;\n raw?: string;\n requestId?: string;\n conversationId?: string;\n retriable?: boolean;\n cause?: unknown;\n metadata?: ErrorMetadata;\n },\n ) {\n super(message, {\n code: options.code,\n cause: options.cause,\n metadata: options.metadata,\n // D311: most AgentRunErrors are not retriable (auth, validation, abort).\n // Provider mappers (D314) override per-status — explicit `retriable` wins\n // over the implicit default when supplied.\n isRetryable: options.retriable ?? defaultRetriableForCode(options.code),\n });\n if (options.provider !== undefined) this.provider = options.provider;\n if (options.raw !== undefined) this.raw = options.raw;\n if (options.requestId !== undefined) this.requestId = options.requestId;\n if (options.conversationId !== undefined) this.conversationId = options.conversationId;\n }\n\n /**\n * Production-Readiness #3 (ADR D311): alias for `isRetryable` exposed as\n * `retriable` to match the handoff contract. Future v2 will deprecate\n * `isRetryable` in favor of this.\n */\n get retriable(): boolean {\n return this.isRetryable;\n }\n\n /**\n * D312: provider's `Retry-After` header in **milliseconds**. Mappers store\n * the header value (seconds) in `metadata.retryAfter`; this getter\n * multiplies by 1000 so the result composes with `Date.now()`/`setTimeout`.\n *\n * Returns `undefined` when no hint was provided. `0` is a legitimate value\n * — use `=== undefined` check rather than truthy check.\n */\n get retryAfterMs(): number | undefined {\n if (this.metadata?.retryAfter === undefined) return undefined;\n return this.metadata.retryAfter * 1000;\n }\n\n /**\n * D313 + T1.5: alias for `metadata.raw`. Provider response body for\n * debugging. T1.5 wraps the value in `redactSecrets` at the getter\n * boundary so secret-shaped substrings (`sk-...`, Bearer JWTs, etc.) are\n * stripped before reaching the caller. Available but NEVER serialized\n * into `.message` (anti-leak invariant).\n */\n get providerError(): unknown {\n const raw = this.metadata?.raw;\n if (raw === undefined) return undefined;\n if (typeof raw === \"string\") return redactSecrets(raw);\n // Non-string raw (object/buffer) — stringify then redact.\n try {\n return redactSecrets(JSON.stringify(raw));\n } catch {\n return redactSecrets(String(raw));\n }\n }\n\n /**\n * T1.5 — sanitized JSON form. `metadata.raw` is OMITTED by default; opt\n * in via `THEOKIT_DEBUG_RAW_ERRORS=1` to surface the (redacted) raw\n * payload for diagnostics. Every other field stays accessible.\n *\n * The single env-var gate is read each call so operators can toggle at\n * runtime without restarting the process.\n */\n toJSON(): Record<string, unknown> {\n const json: Record<string, unknown> = {\n name: this.name,\n message: this.message,\n isRetryable: this.isRetryable,\n };\n addOptionalFields(json, this);\n const safeMeta = sanitizeMetadata(this.metadata);\n if (safeMeta !== undefined) json.metadata = safeMeta;\n return json;\n }\n}\n\nfunction addOptionalFields(json: Record<string, unknown>, err: AgentRunError): void {\n if (err.code !== undefined) json.code = err.code;\n if (err.provider !== undefined) json.provider = err.provider;\n if (err.requestId !== undefined) json.requestId = err.requestId;\n if (err.conversationId !== undefined) json.conversationId = err.conversationId;\n if (err.raw !== undefined) json.raw = redactSecrets(err.raw);\n}\n\nfunction sanitizeMetadata(meta: ErrorMetadata | undefined): ErrorMetadata | undefined {\n if (meta === undefined) return undefined;\n const { raw, ...rest } = meta;\n const debugRaw = process.env.THEOKIT_DEBUG_RAW_ERRORS === \"1\";\n if (debugRaw && raw !== undefined) {\n const redactedRaw =\n typeof raw === \"string\" ? redactSecrets(raw) : redactSecrets(safeStringify(raw));\n return { ...rest, raw: redactedRaw } as ErrorMetadata;\n }\n return rest as ErrorMetadata;\n}\n\nfunction safeStringify(value: unknown): string {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Is this error transient (worth retrying)?\n *\n * Returns the SDK's own retryability verdict: every {@link TheokitAgentError}\n * subclass computes `isRetryable` at construction (rate-limit / network /\n * credential-pool-exhausted are retryable; auth / configuration / unsupported\n * are not), so this predicate is a single source of truth rather than a\n * re-derivation. Non-SDK errors return `false` conservatively — wrap a foreign\n * error in the appropriate SDK error first if you want it considered transient.\n * It never inspects `err.message`.\n *\n * @example\n * try {\n * await agent.send(message, { throwOnError: true });\n * } catch (err) {\n * if (isTransientError(err)) return retryWithBackoff();\n * throw err;\n * }\n *\n * @public\n */\nexport function isTransientError(err: unknown): boolean {\n return err instanceof TheokitAgentError && err.isRetryable === true;\n}\n\n/**\n * Thrown when a {@link Run} or agent operation is not available on the current\n * runtime. Check first with `run.supports(operation)`.\n *\n * Extends {@link TheokitAgentError} (so error-catching code that branches on\n * `instanceof TheokitAgentError` continues to work) but is never retryable —\n * an unsupported operation will not become supported on retry.\n *\n * @public\n */\nexport class UnsupportedRunOperationError extends TheokitAgentError {\n override readonly name: string = \"UnsupportedRunOperationError\";\n readonly operation: RunOperation;\n\n constructor(\n message: string,\n operation: RunOperation,\n options: { code?: string; cause?: unknown } = {},\n ) {\n super(message, {\n ...options,\n isRetryable: false,\n code: options.code ?? \"unsupported_run_operation\",\n });\n this.operation = operation;\n }\n}\n\n/**\n * Thrown when every credential in a per-provider pool is in cooldown\n * and no healthy key is available (ADR D133). The caller's\n * {@link import(\"./internal/llm/fallback-client.js\").FallbackLlmClient}\n * catches this and tries the next provider in the fallback chain.\n *\n * `metadata.nextRetryAt` (epoch ms) tells callers when the soonest\n * pool entry resumes — useful for manual retry scheduling.\n *\n * @public\n */\nexport class CredentialPoolExhaustedError extends TheokitAgentError {\n override readonly name: string = \"CredentialPoolExhaustedError\";\n readonly provider: string;\n readonly nextRetryAt: number | undefined;\n\n constructor(\n message: string,\n options: {\n provider: string;\n nextRetryAt?: number;\n code?: string;\n cause?: unknown;\n metadata?: ErrorMetadata;\n },\n ) {\n super(message, {\n ...options,\n isRetryable: true,\n code: options.code ?? \"credential_pool_exhausted\",\n });\n this.provider = options.provider;\n this.nextRetryAt = options.nextRetryAt;\n }\n}\n\n/**\n * Finite error codes specific to memory adapter operations (ADR D141).\n *\n * @public\n */\nexport type MemoryAdapterErrorCode =\n | \"auth_failed\"\n | \"rate_limited\"\n | \"not_found\"\n | \"network\"\n | \"invalid_input\"\n | \"unknown\";\n\n/**\n * Error raised by `@theokit-memory-*` adapters. Carries `adapterId`\n * so callers can branch on which provider failed (ADR D141).\n *\n * @public\n */\nexport class MemoryAdapterError extends TheokitAgentError {\n override readonly name: string = \"MemoryAdapterError\";\n readonly adapterId: string;\n\n constructor(\n message: string,\n options: {\n adapterId: string;\n code: MemoryAdapterErrorCode;\n cause?: unknown;\n metadata?: ErrorMetadata;\n },\n ) {\n super(message, {\n isRetryable: options.code === \"rate_limited\" || options.code === \"network\",\n code: options.code,\n ...(options.cause !== undefined ? { cause: options.cause } : {}),\n ...(options.metadata !== undefined ? { metadata: options.metadata } : {}),\n });\n this.adapterId = options.adapterId;\n }\n}\n\n/**\n * Thrown when a user-supplied task ID violates the grammar\n * `^[a-z0-9][a-z0-9_-]*$` (D368) OR starts with a reserved adapter\n * prefix (`wf-` / `b-` / `cron-`, EC-5).\n *\n * @public\n */\nexport class InvalidTaskIdError extends TheokitAgentError {\n override readonly name: string = \"InvalidTaskIdError\";\n readonly taskId: string;\n\n constructor(message: string, taskId: string, options: { cause?: unknown } = {}) {\n super(message, {\n ...options,\n isRetryable: false,\n code: \"invalid_task_id\",\n });\n this.taskId = taskId;\n }\n}\n\n/**\n * Thrown when `Task.subscribe(id)` is called for a task that has been\n * evicted, never submitted, or evicted after retention (D373).\n *\n * @public\n */\nexport class TaskNotFoundError extends TheokitAgentError {\n override readonly name: string = \"TaskNotFoundError\";\n readonly taskId: string;\n\n constructor(taskId: string, options: { cause?: unknown } = {}) {\n super(`Task not found: ${taskId}`, {\n ...options,\n isRetryable: false,\n code: \"task_not_found\",\n });\n this.taskId = taskId;\n }\n}\n\n/**\n * Thrown when `CloudAgent` is asked to wrap a task (D370). Cloud\n * task observability is deferred until Theo PaaS GA.\n *\n * @public\n */\nexport class UnsupportedTaskOperationError extends TheokitAgentError {\n override readonly name: string = \"UnsupportedTaskOperationError\";\n readonly operation: string;\n\n constructor(operation: string, options: { cause?: unknown } = {}) {\n super(\n `Task operation \"${operation}\" is not supported on CloudAgent (pre-release; see ADR D370)`,\n {\n ...options,\n isRetryable: false,\n code: \"task_op_unsupported\",\n },\n );\n this.operation = operation;\n }\n}\n\n/**\n * Thrown by `Budget` enforcement (ADR D386) when a `mode: \"block\"`\n * budget would be exceeded by the upcoming LLM call. Caller pega\n * tipado para retry-after-window-reset or surface to the user.\n *\n * @public\n */\nexport class BudgetExceededError extends TheokitAgentError {\n override readonly name: string = \"BudgetExceededError\";\n readonly budgetName: string;\n readonly window: import(\"./types/budget.js\").BudgetWindow;\n readonly spentUsd: number;\n readonly limitUsd: number;\n readonly mode: import(\"./types/budget.js\").BudgetMode;\n\n constructor(args: {\n budgetName: string;\n window: import(\"./types/budget.js\").BudgetWindow;\n spentUsd: number;\n limitUsd: number;\n mode: import(\"./types/budget.js\").BudgetMode;\n cause?: unknown;\n }) {\n super(\n `Budget \"${args.budgetName}\" exceeded for window ${args.window}: spent $${args.spentUsd.toFixed(4)} > limit $${args.limitUsd.toFixed(4)}`,\n {\n ...(args.cause !== undefined ? { cause: args.cause } : {}),\n isRetryable: false,\n code: \"budget_exceeded\",\n },\n );\n this.budgetName = args.budgetName;\n this.window = args.window;\n this.spentUsd = args.spentUsd;\n this.limitUsd = args.limitUsd;\n this.mode = args.mode;\n }\n}\n\n/**\n * Thrown when `CloudAgent.send({ budget })` is invoked (D388). Cloud\n * budget surface waits for Theo PaaS GA.\n *\n * @public\n */\n/**\n * T1.6 — Thrown when a consumer calls `agent.send()` or any method\n * on an agent that has already been `dispose()`d. Pre-T1.6 this was\n * a generic `new Error(\"Agent has been disposed\")` — consumers\n * couldn't catch it without string-matching the message.\n *\n * @public\n */\nexport class AgentDisposedError extends TheokitAgentError {\n override readonly name: string = \"AgentDisposedError\";\n readonly agentId: string;\n\n constructor(agentId: string) {\n super(`Agent \"${agentId}\" has been disposed. Create a new agent or use Agent.resume().`, {\n isRetryable: false,\n code: \"agent_disposed\",\n });\n this.agentId = agentId;\n }\n}\n\nexport class UnsupportedBudgetOperationError extends TheokitAgentError {\n override readonly name: string = \"UnsupportedBudgetOperationError\";\n readonly operation: string;\n\n constructor(operation: string, options: { cause?: unknown } = {}) {\n super(\n `Budget operation \"${operation}\" is not supported on CloudAgent (pre-release; see ADR D388)`,\n {\n ...options,\n isRetryable: false,\n code: \"budget_op_unsupported\",\n },\n );\n this.operation = operation;\n }\n}\n","/**\n * Canonical path-guard module (ADRs D79-D81).\n *\n * Three primitives + one typed error:\n * - `safePathJoin(base, ...parts)` — resolve THEN prefix-check (ADR D80).\n * - `assertNoSymlinkEscape(path, base)` — `realpathSync` resolves entire\n * symlink chain (EC-1 fix; Hermes v0.2 #386, #61).\n * - `sanitizeIdentifier(input, { maxLen })` — strict grammar\n * `^[a-z0-9][a-z0-9-_]*$` (ADR D81; case-insensitive on input,\n * lowercase on output).\n * - `PathTraversalError` — extends ConfigurationError with code\n * `path_traversal` (ADR D65: no new hierarchy).\n *\n * Wire at all sites where user input becomes a path. CI lint gate\n * `tests/lint/no-unguarded-path-input.test.ts` prevents regression\n * (ADR D85).\n *\n * @internal\n */\n\nimport { createHash } from \"node:crypto\";\nimport { lstatSync, readlinkSync, realpathSync, type Stats } from \"node:fs\";\nimport { dirname, resolve, sep } from \"node:path\";\n\nimport { ConfigurationError } from \"../../errors.js\";\n\n/**\n * Thrown when a path operation would escape its allowed base directory.\n * Extends `ConfigurationError` (no new error hierarchy per ADR D65).\n *\n * @internal\n */\nexport class PathTraversalError extends ConfigurationError {\n override readonly name: string = \"PathTraversalError\";\n\n constructor(input: string, resolvedPath: string) {\n super(`Path traversal attempt: ${input} → ${resolvedPath}`, {\n code: \"path_traversal\",\n });\n }\n}\n\n/**\n * Thrown when an agent tool is asked to read or write a sensitive path\n * that the blocklist forbids (`.env`, `.git/`, `node_modules/`, `.theo/`,\n * lock files). Distinct from `PathTraversalError` because the path is\n * lexically inside the project — it is just sensitive.\n *\n * Extends `ConfigurationError` (no new error hierarchy per ADR D65).\n *\n * @public\n */\nexport class ForbiddenPathError extends ConfigurationError {\n override readonly name: string = \"ForbiddenPathError\";\n\n constructor(path: string) {\n super(\n `Path '${path}' is in the sensitive-file blocklist (.env, .git/, node_modules/, .theo/, lock files)`,\n {\n code: \"forbidden_path\",\n },\n );\n }\n}\n\n/**\n * Join `base` with `...parts` and ensure the resolved absolute path stays\n * under `base`. Resolves FIRST, then prefix-checks (ADR D80) — prevents\n * normalized-escape bypasses like `subdir/.\\\\./bar`.\n *\n * Returns the safe absolute path. Throws `PathTraversalError` if escape.\n *\n * @internal\n */\nexport function safePathJoin(base: string, ...parts: string[]): string {\n if (base === \"\") {\n throw new Error(\"safePathJoin: base must be non-empty\");\n }\n // T5.5 — NUL byte + C0/DEL control char rejection at the boundary.\n // Apply before path resolution so a malicious input never reaches\n // `resolve` (which on some platforms behaved unexpectedly with NUL\n // and in N-API callers historically silently truncated).\n rejectNulAndControlChars(base, \"base\");\n for (const part of parts) {\n rejectNulAndControlChars(part, \"path segment\");\n }\n const baseResolved = resolve(base);\n const target = resolve(base, ...parts);\n if (target !== baseResolved && !target.startsWith(baseResolved + sep)) {\n throw new PathTraversalError(parts.join(\"/\"), target);\n }\n return target;\n}\n\n/**\n * T5.5 — Reject NUL (`\\x00`) and C0/DEL control characters\n * (`\\x01-\\x1F`, `\\x7F`) in any path-shaped or identifier-shaped input.\n * Centralizes the check so every public path-guard / sanitize entrypoint\n * shares the same defense.\n *\n * Throws `PathTraversalError` (the same shape as other path-shape\n * rejections) so callers don't need to learn a new error class.\n *\n * @internal\n */\nfunction rejectNulAndControlChars(input: string, role: string): void {\n for (let i = 0; i < input.length; i++) {\n const code = input.charCodeAt(i);\n if (code === 0x00 || (code >= 0x01 && code <= 0x1f) || code === 0x7f) {\n const label = code === 0x00 ? \"<nul-byte>\" : `<control-char-0x${code.toString(16)}>`;\n throw new PathTraversalError(`${role}: ${input}`, label);\n }\n }\n}\n\n/**\n * Assert that `path` — including every directory component in the chain —\n * stays under `base` after symlink resolution. No-op when nothing on the\n * path exists yet.\n *\n * Two-bug history:\n * 1. **EC-1** (original fix, kept): a multi-level symlink chain A → B → C\n * must be resolved end-to-end. `realpathSync` does this in 1 syscall.\n * 2. **Defence-in-depth** (added v1.x): the previous implementation only\n * called `lstatSync(path)` on the terminal component. If an INTERMEDIATE\n * directory was a symlink (`base/inner-symlink → /outside`), `lstat` on\n * `base/inner-symlink/file.txt` followed the symlink and reported the\n * regular file — escape went undetected. Fix: walk up to the nearest\n * existing ancestor and `realpath` THAT, then re-attach the suffix and\n * check the result against the canonical base.\n *\n * @internal\n */\nexport function assertNoSymlinkEscape(path: string, base: string): void {\n // T5.5 — reject NUL / control chars before any FS call (a NUL byte\n // in the path used to silently truncate at the C boundary on legacy\n // libc — defense in depth even on modern Node).\n rejectNulAndControlChars(path, \"path\");\n rejectNulAndControlChars(base, \"base\");\n // Canonical base — symlinks in the base path itself are absorbed once here.\n let baseResolved: string;\n try {\n baseResolved = realpathSync(base);\n } catch {\n // base doesn't exist as a real directory yet — fall back to lexical resolve.\n baseResolved = resolve(base);\n }\n\n // Find the deepest ancestor of `path` that exists, then realpath it.\n // Anything from there onward is \"not yet on disk\" and contributes only\n // its lexical suffix. This covers three cases:\n // - path exists (regular file or symlink at any depth) → realpath the full path\n // - path doesn't exist but intermediate dir is a symlink → realpath the ancestor\n // - nothing on the path exists → no escape risk (return)\n const resolved = realpathOfDeepestExisting(path);\n if (resolved === undefined) return; // path has no existing prefix — nothing to attack\n\n if (resolved !== baseResolved && !resolved.startsWith(baseResolved + sep)) {\n throw new PathTraversalError(`symlink ${path}`, resolved);\n }\n}\n\n/**\n * Find the deepest ancestor of `path` that exists on disk, resolve all\n * symlinks in that ancestor via `realpathSync`, and re-attach the\n * lexical suffix. Returns `undefined` when no ancestor exists.\n *\n * Handles dangling symlinks: if the terminal IS a symlink but its target\n * is missing, we still detect escape via `readlinkSync` + parent resolve.\n */\nfunction realpathOfDeepestExisting(path: string): string | undefined {\n // First try the full path — the common case.\n try {\n return realpathSync(path);\n } catch {\n // Not resolvable. Two sub-cases.\n }\n\n // Sub-case A: terminal is a dangling symlink.\n try {\n const stat: Stats = lstatSync(path);\n if (stat.isSymbolicLink()) {\n const target = readlinkSync(path);\n // Resolve target relative to the REAL parent dir, so intermediate\n // symlinks in the parent chain are absorbed.\n const parentReal = realpathOfDeepestExisting(dirname(path));\n const parentBase = parentReal ?? dirname(path);\n return resolve(parentBase, target);\n }\n } catch {\n // lstat failed too — terminal doesn't exist at all.\n }\n\n // Sub-case B: walk up to the nearest existing ancestor, then re-attach\n // the suffix lexically.\n let cursor = dirname(path);\n let suffix = path.slice(cursor.length);\n while (cursor !== dirname(cursor)) {\n try {\n const real = realpathSync(cursor);\n // Reconstruct: ancestor's realpath + remaining (still-lexical) suffix\n return resolve(real, `.${suffix}`);\n } catch {\n suffix = path.slice(dirname(cursor).length);\n cursor = dirname(cursor);\n }\n }\n // Reached filesystem root without finding any existing ancestor.\n return undefined;\n}\n\nconst LOCK_FILES = new Set([\"pnpm-lock.yaml\", \"package-lock.json\", \"yarn.lock\", \"bun.lockb\"]);\n\n// T5.6 — top-level credential dot-dirs / dot-files. Matching against\n// the FIRST path segment (lowercase). Adding any entry here costs a\n// CHANGELOG note + an explicit case-fold test (entries are lowercased\n// at module load).\nconst SENSITIVE_FIRST_SEGMENTS = new Set([\n \".ssh\",\n \".aws\",\n \".docker\",\n \".kube\",\n \".npmrc\",\n \".netrc\",\n \".pgpass\",\n]);\n\n// T5.6 — credential basenames blocked at ANY depth (lowercase). Catches\n// the developer-laptop case where an agent recurses into a subdir.\nconst SENSITIVE_BASENAMES = new Set([\n \"id_rsa\",\n \"id_ed25519\",\n \"id_ecdsa\",\n \"id_dsa\",\n \"authorized_keys\",\n \"known_hosts\",\n \".npmrc\",\n \".netrc\",\n \".pgpass\",\n]);\n\n// T5.6 — extension suffixes blocked at ANY depth (lowercase). Covers\n// the entire `*.pem` / `*.key` private-material family.\nconst SENSITIVE_SUFFIXES = [\".pem\", \".key\", \".p12\", \".pfx\"];\n\n/**\n * Decide whether a project-relative path points to a known-sensitive file\n * that a coding agent must not read or write.\n *\n * Universal blocklist (works for any agent operating on a project tree):\n *\n * - `.env`, `.env.<anything>` — except `.env.example` (template safe to read)\n * - `.git/` — version control internals\n * - `node_modules/` — dependency cache (changes don't belong to the user)\n * - `.theo/` — TheoKit build artefacts / state\n * - Lock files at any depth: `pnpm-lock.yaml`, `package-lock.json`,\n * `yarn.lock`, `bun.lockb`\n *\n * Operates on path segments (forward-slash normalized). Cross-platform safe.\n *\n * Use together with `safePathJoin` + `assertNoSymlinkEscape`: the former two\n * defeat traversal, this one defeats reading a file that is lexically inside\n * the project but should not be agent-visible.\n *\n * @public\n */\nexport function isForbiddenPath(input: string): boolean {\n // T5.6 — lowercase normalization defeats case-only bypass on\n // case-insensitive filesystems (Windows/macOS-default) where `.ENV`\n // and `.env` map to the same inode but a case-sensitive string\n // check passes the former.\n const normalized = input.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\").toLowerCase();\n if (normalized.length === 0) return false;\n\n const segments = normalized.split(\"/\").filter((s) => s.length > 0);\n if (segments.length === 0) return false;\n\n if (isForbiddenFirstSegment(segments[0]!)) return true;\n if (isForbiddenBasename(segments[segments.length - 1]!)) return true;\n return false;\n}\n\nfunction isForbiddenFirstSegment(first: string): boolean {\n // .env.example is explicitly allowlisted (template safe to read)\n if (first === \".env.example\") return false;\n if (first === \".env\") return true;\n if (/^\\.env\\./.test(first)) return true;\n if (first === \".git\" || first === \"node_modules\" || first === \".theo\") return true;\n return SENSITIVE_FIRST_SEGMENTS.has(first);\n}\n\nfunction isForbiddenBasename(basename: string): boolean {\n if (LOCK_FILES.has(basename)) return true;\n if (SENSITIVE_BASENAMES.has(basename)) return true;\n for (const suffix of SENSITIVE_SUFFIXES) {\n if (basename.endsWith(suffix)) return true;\n }\n return false;\n}\n\nconst IDENTIFIER_PATTERN = /^[a-z0-9][a-z0-9\\-_]*$/i;\n\n/**\n * Validate that `input` is a safe path component (skill name, agent ID,\n * namespace, etc.) and return its lowercase form. Strict grammar\n * `^[a-z0-9][a-z0-9-_]*$` rejects path separators, dots, null bytes,\n * whitespace, unicode invisible chars, and any leading `-`/`_`.\n *\n * @param input - User-supplied identifier candidate.\n * @param options.maxLen - Maximum allowed length (default 64).\n * @returns Lowercase form of `input`.\n * @throws `ConfigurationError` with code `invalid_identifier` on rejection.\n *\n * @internal\n */\n/**\n * T1.4 — validate a relative artifact path string BEFORE it is used to look\n * up a fixture or to fetch from PaaS. Rejects every well-known traversal\n * vector at the boundary, throwing `PathTraversalError`.\n *\n * Vectors rejected:\n * - classic `..` parent-directory traversal (any segment).\n * - backslash separators (Windows-style `..\\\\windows`).\n * - URL-encoded `%2e%2e` / `%2E%2E` (double-decoded traversal).\n * - NUL byte injection (`\\x00`).\n * - Windows drive letter prefix (`C:`, `D:\\\\...`).\n * - Home-tilde expansion (`~/`, `~root/...`).\n * - Absolute paths starting with `/`.\n *\n * Does NOT touch the filesystem — the call is shape-only. Live symlink\n * traversal protection happens via `assertNoSymlinkEscape` at the FS-resolve\n * boundary.\n *\n * @param input - Caller-supplied artifact path.\n * @throws `PathTraversalError` on any rejection.\n *\n * @internal\n */\nexport function validateArtifactPath(input: string): void {\n rejectKnownPrefixVectors(input);\n const normalized = decodeAndNormalize(input);\n rejectParentTraversal(input, normalized);\n}\n\nfunction rejectKnownPrefixVectors(input: string): void {\n if (input.includes(\"\\x00\")) {\n throw new PathTraversalError(input, \"<nul-byte>\");\n }\n if (input.startsWith(\"/\") || input.startsWith(\"~\")) {\n throw new PathTraversalError(input, input);\n }\n if (/^[A-Za-z]:[\\\\/]?/.test(input)) {\n throw new PathTraversalError(input, input);\n }\n}\n\nfunction decodeAndNormalize(input: string): string {\n // URL-encoded traversal — 2 passes catches `%252e%252e`.\n // Malformed sequences (decodeURIComponent throws) are themselves a rejection.\n let decoded = input;\n for (let i = 0; i < 2; i += 1) {\n try {\n const next = decodeURIComponent(decoded);\n if (next === decoded) break;\n decoded = next;\n } catch {\n throw new PathTraversalError(input, \"<malformed-url-encoding>\");\n }\n }\n // Normalize backslash to forward slash before segment-walking.\n return decoded.replace(/\\\\/g, \"/\");\n}\n\nfunction rejectParentTraversal(input: string, normalized: string): void {\n for (const segment of normalized.split(\"/\")) {\n if (segment === \"..\" || segment === \"..%00\") {\n throw new PathTraversalError(input, normalized);\n }\n }\n // Defense in depth: literal `..` anywhere in the normalized string.\n if (normalized.includes(\"..\")) {\n throw new PathTraversalError(input, normalized);\n }\n}\n\nexport function sanitizeIdentifier(input: string, options?: { maxLen?: number }): string {\n const maxLen = options?.maxLen ?? 64;\n if (input.length === 0 || input.length > maxLen) {\n throw new ConfigurationError(`Identifier length out of range (1-${maxLen}): \"${input}\"`, {\n code: \"invalid_identifier\",\n });\n }\n // T5.5 — explicit NUL / control char rejection ahead of the generic\n // pattern check. The IDENTIFIER_PATTERN regex already excludes these\n // (they are not in `[a-z0-9\\-_]`), but routing them through the same\n // helper used by safePathJoin gives operators a precise diagnostic\n // (\"nul-byte\" / \"control-char-0x..\") instead of the generic\n // \"invalid characters\" message — making prompt-injection traces\n // legible per Inquebrável Rule 3.\n rejectNulAndControlChars(input, \"identifier\");\n if (!IDENTIFIER_PATTERN.test(input)) {\n throw new ConfigurationError(`Identifier contains invalid characters: \"${input}\"`, {\n code: \"invalid_identifier\",\n });\n }\n return input.toLowerCase();\n}\n\n/**\n * Convert ANY opaque id (agent id, run id, conversation id, namespace, email,\n * arbitrary string) into a deterministic, filesystem-safe filename component.\n *\n * Unlike {@link sanitizeIdentifier} (which THROWS on non-conforming input),\n * this is a total function: it NEVER throws on a non-empty string. It returns\n * the lowercased id verbatim when it already matches the safe grammar\n * `^[a-z0-9][a-z0-9-_]*$` and fits `maxLen` (so UUIDs, hashes, and slugs stay\n * human-readable), otherwise a deterministic `h-<16 hex>` sha256 token\n * (collision-resistant and always a valid filename). The output charset is\n * always `[a-z0-9_-]`, safe as a literal path segment on every filesystem.\n *\n * @param id - any opaque identifier (must be a non-empty string)\n * @param options.maxLen - max length for the passthrough branch (default 128).\n * Ids longer than this are hashed; the hash token itself is always short.\n * @throws ConfigurationError (code `invalid_filename_id`) only on empty input.\n *\n * @example\n * safeFilenameForId(\"550e8400-e29b-41d4-a716-446655440000\") // passthrough\n * safeFilenameForId(\"user@example.com\") // \"h-<16hex>\"\n *\n * @internal — public via `@theokit/sdk/path-safety`\n */\nexport function safeFilenameForId(id: string, options?: { maxLen?: number }): string {\n if (id.length === 0) {\n throw new ConfigurationError(\"Filename id must be a non-empty string\", {\n code: \"invalid_filename_id\",\n });\n }\n const maxLen = options?.maxLen ?? 128;\n const lower = id.toLowerCase();\n if (lower.length <= maxLen && IDENTIFIER_PATTERN.test(lower)) {\n return lower;\n }\n return `h-${createHash(\"sha256\").update(id).digest(\"hex\").slice(0, 16)}`;\n}\n","import { readdir } from \"node:fs/promises\";\n\nimport { ConfigurationError } from \"../../../errors.js\";\n\n/**\n * Entry returned by `readWorkspaceDir`. Mirrors the subset of\n * `fs.Dirent` the file-based loaders use.\n */\nexport interface WorkspaceDirEntry {\n name: string;\n isDirectory(): boolean;\n isFile(): boolean;\n}\n\n/**\n * Read a workspace subdirectory and return its entries. When the directory\n * does not exist (`ENOENT`), returns an empty array — the file-based loaders\n * (skills, plugins, agents) treat a missing directory as \"no entries\"\n * rather than an error.\n *\n * Any other I/O failure is wrapped as `ConfigurationError` so callers can\n * surface a stable error code.\n *\n * @internal\n */\nexport async function readWorkspaceDir(\n root: string,\n errorCode: string,\n describe: string,\n): Promise<WorkspaceDirEntry[]> {\n try {\n return (await readdir(root, { withFileTypes: true })) as WorkspaceDirEntry[];\n } catch (cause) {\n const err = cause as NodeJS.ErrnoException;\n if (err.code === \"ENOENT\") return [];\n throw new ConfigurationError(`Failed to read ${describe}: ${root}`, {\n code: errorCode,\n cause,\n });\n }\n}\n","/**\n * Tiny YAML-frontmatter parser shared by the file-based loaders (skills,\n * subagents, hooks, context, plugins). Supports four scalar shapes:\n *\n * key: bar → \"bar\" (string)\n * key: 42 → 42 (number)\n * key: true → true (boolean)\n * key: [a, b, c] → [\"a\",\"b\",\"c\"](string[])\n * key: → undefined (caller's Zod default kicks in)\n *\n * Limitations (intentional — keep parser tiny, no dep):\n * - No nested objects (use flat keys like `providerId` not `provider.id`).\n * - No quoted strings — `match: \"1\"` becomes the literal 3-char string `\"1\"`.\n * - List values cannot contain a literal comma inside an element; the\n * `tags: [a,b, c]` splitter is greedy on `,`. Use multi-line lists or\n * reword if you need this.\n *\n * @internal\n */\n\nexport type FrontmatterValue = string | number | boolean | string[];\n\nexport function parseSimpleYaml(text: string): Record<string, FrontmatterValue | undefined> {\n const fields: Record<string, FrontmatterValue | undefined> = {};\n for (const line of text.split(/\\r?\\n/)) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex === -1) continue;\n const key = line.slice(0, colonIndex).trim();\n if (key.length === 0) continue;\n const raw = line.slice(colonIndex + 1).trim();\n fields[key] = coerce(raw);\n }\n return fields;\n}\n\nfunction coerce(raw: string): FrontmatterValue | undefined {\n // EC-3: empty value → undefined so Zod `.optional().default(...)` applies.\n if (raw.length === 0) return undefined;\n if (raw.startsWith(\"[\") && raw.endsWith(\"]\")) {\n return raw\n .slice(1, -1)\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n if (raw === \"true\" || raw === \"false\") return raw === \"true\";\n const n = Number(raw);\n if (Number.isFinite(n) && raw === String(n)) return n;\n return raw;\n}\n","import { ConfigurationError } from \"../../../errors.js\";\nimport { type FrontmatterValue, parseSimpleYaml } from \"../context/yaml-frontmatter.js\";\n\ntype StringFields = Record<string, string | undefined>;\n\n/** Narrow a FrontmatterValue to string; non-strings + undefined → undefined. */\nfunction asString(v: FrontmatterValue | undefined): string | undefined {\n return typeof v === \"string\" ? v : undefined;\n}\n\n/** Coerce parser output to legacy string-only shape (skill schema is all-string). */\nfunction toStringFields(raw: Record<string, FrontmatterValue | undefined>): StringFields {\n const out: StringFields = {};\n for (const [k, v] of Object.entries(raw)) out[k] = asString(v);\n return out;\n}\n\n/**\n * Strict skill frontmatter schema (ADR D10).\n *\n * Required: `name`, `description`.\n * Optional: `category`, `dependencies` (comma-separated string in the\n * simple-YAML dialect — parsed to `string[]`).\n *\n * Unknown fields are ignored (forward-compat). Malformed YAML or missing\n * required fields surface as `ConfigurationError` with one of the typed\n * codes below.\n *\n * @internal\n */\nexport interface SkillFrontmatter {\n name: string;\n description: string;\n category?: string;\n dependencies?: string[];\n}\n\nexport type SkillFrontmatterErrorCode = \"missing_frontmatter\" | \"schema_invalid\";\n\n/**\n * Parse a SKILL.md file body into validated frontmatter.\n *\n * @throws ConfigurationError(code: \"missing_frontmatter\") — no `---` block at file head.\n * @throws ConfigurationError(code: \"schema_invalid\") — YAML malformed OR required field missing.\n *\n * @internal\n */\nexport function parseSkillFrontmatter(raw: string, fallbackName: string): SkillFrontmatter {\n const fields = extractAndParseFrontmatter(raw, fallbackName);\n const name = resolveName(fields, fallbackName);\n ensureRequiredFields(fields, name);\n return buildFrontmatter(fields, name);\n}\n\nfunction extractAndParseFrontmatter(raw: string, fallbackName: string): StringFields {\n const match = /^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/.exec(raw);\n if (match === null) {\n throw new ConfigurationError(`Skill ${fallbackName} is missing frontmatter`, {\n code: \"missing_frontmatter\",\n });\n }\n const frontmatter = match[1] ?? \"\";\n // EC-5: guard against syntactically invalid frontmatter so the loader\n // surfaces schema_invalid rather than crashing.\n try {\n return toStringFields(parseSimpleYaml(frontmatter));\n } catch (cause) {\n const detail = cause instanceof Error ? cause.message : String(cause);\n throw new ConfigurationError(\n `Skill ${fallbackName} has malformed YAML frontmatter: ${detail}`,\n { code: \"schema_invalid\", cause },\n );\n }\n}\n\nfunction resolveName(fields: StringFields, fallbackName: string): string {\n if (hasContent(fields.name)) return fields.name;\n if (hasContent(fallbackName)) return fallbackName;\n throw new ConfigurationError(\"Skill at unknown path is missing required field: name\", {\n code: \"schema_invalid\",\n });\n}\n\nfunction ensureRequiredFields(fields: StringFields, name: string): void {\n if (!hasContent(fields.description)) {\n throw new ConfigurationError(`Skill ${name} is missing required field: description`, {\n code: \"schema_invalid\",\n });\n }\n}\n\nfunction buildFrontmatter(fields: StringFields, name: string): SkillFrontmatter {\n const description = fields.description;\n if (description === undefined) {\n // ensureRequiredFields already threw; this is unreachable but satisfies TS\n throw new ConfigurationError(`Skill ${name} missing description`, { code: \"schema_invalid\" });\n }\n const result: SkillFrontmatter = { name, description };\n if (hasContent(fields.category)) result.category = fields.category;\n const deps = parseDependencies(fields.dependencies);\n if (deps !== undefined) result.dependencies = deps;\n return result;\n}\n\nfunction parseDependencies(raw: string | undefined): string[] | undefined {\n if (!hasContent(raw)) return undefined;\n const deps = (raw as string)\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n return deps.length > 0 ? deps : undefined;\n}\n\nfunction hasContent(value: string | undefined): value is string {\n return value !== undefined && value.trim().length > 0;\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { ConfigurationError } from \"../../../errors.js\";\nimport { assertNoSymlinkEscape, safePathJoin } from \"../../security/path-guard.js\";\nimport { readWorkspaceDir } from \"../config/workspace-dir.js\";\nimport { parseSkillFrontmatter } from \"./skill-frontmatter.js\";\n\n/**\n * A discovered skill's metadata. The skill BODY is never included — only the\n * strict frontmatter fields plus the resolved `source` path.\n *\n * Public via `@theokit/sdk/skills`.\n *\n * @public\n */\nexport interface Skill {\n name: string;\n description: string;\n /** Absolute path to the discovered `SKILL.md`. */\n source: string;\n category?: string;\n dependencies?: string[];\n}\n\n/**\n * Information passed to `onInvalidSkill` when a `SKILL.md` is present but its\n * frontmatter is malformed (missing required field or invalid YAML).\n *\n * @public\n */\nexport interface InvalidSkillInfo {\n /** The skill directory name (used as the fallback skill name). */\n name: string;\n /** Absolute path to the offending `SKILL.md`. */\n source: string;\n /** Typed reason: `missing_frontmatter` or `schema_invalid`. */\n code: string;\n message: string;\n}\n\n/**\n * Options for {@link discoverSkills}.\n *\n * @public\n */\nexport interface DiscoverSkillsOptions {\n /**\n * Called once per directory that contains a `SKILL.md` with malformed\n * frontmatter. The skill is excluded from the result; discovery continues\n * (strict-frontmatter ADR / EC-5). A directory WITHOUT a `SKILL.md` is NOT a\n * malformed skill and does not trigger this callback.\n *\n * Default: no-op (a library primitive must not write to the consumer's\n * stderr by default).\n */\n onInvalidSkill?: (info: InvalidSkillInfo) => void;\n}\n\n/**\n * Discover `SKILL.md` skills under an arbitrary directory.\n *\n * For each immediate subdirectory `<dir>/<name>/` containing a `SKILL.md`, the\n * file's strict YAML frontmatter is parsed (`name`/`description` required;\n * `category`/`dependencies` optional). Malformed skills are skipped (optionally\n * reported via {@link DiscoverSkillsOptions.onInvalidSkill}); a subdirectory\n * whose realpath escapes `dir` (via symlink) is skipped (symlink-escape guard,\n * reusing `@theokit/sdk/path-safety`).\n *\n * NEVER throws: a missing, unreadable, or non-directory `dir` yields `[]`.\n *\n * Discovery order follows the filesystem `readdir` order (OS-dependent). Sort\n * the result before {@link buildSkillsBlock} if a stable block order matters.\n *\n * Public via `@theokit/sdk/skills`.\n *\n * @public\n */\nexport async function discoverSkills(\n dir: string,\n options?: DiscoverSkillsOptions,\n): Promise<Skill[]> {\n let entries: Awaited<ReturnType<typeof readWorkspaceDir>>;\n try {\n entries = await readWorkspaceDir(dir, \"skills_read_error\", \"skills directory\");\n } catch {\n // never-throw contract: unreadable / not-a-directory → no skills (EC-1)\n return [];\n }\n\n const skills: Skill[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n let skillDir: string;\n try {\n skillDir = safePathJoin(dir, entry.name);\n assertNoSymlinkEscape(skillDir, dir);\n } catch {\n continue;\n }\n const skillPath = join(skillDir, \"SKILL.md\");\n let raw: string;\n try {\n raw = await readFile(skillPath, \"utf8\");\n } catch {\n // no SKILL.md in this subdir → not a skill, not an error (EC-2)\n continue;\n }\n const skill = tryParseSkill(raw, entry.name, skillPath, options);\n if (skill !== undefined) skills.push(skill);\n }\n return skills;\n}\n\nfunction tryParseSkill(\n raw: string,\n fallbackName: string,\n source: string,\n options: DiscoverSkillsOptions | undefined,\n): Skill | undefined {\n try {\n const frontmatter = parseSkillFrontmatter(raw, fallbackName);\n const skill: Skill = {\n name: frontmatter.name,\n description: frontmatter.description,\n source,\n };\n if (frontmatter.category !== undefined) skill.category = frontmatter.category;\n if (frontmatter.dependencies !== undefined) skill.dependencies = frontmatter.dependencies;\n return skill;\n } catch (cause) {\n if (cause instanceof ConfigurationError) {\n options?.onInvalidSkill?.({\n name: fallbackName,\n source,\n code: cause.code ?? \"unknown\",\n message: cause.message,\n });\n return undefined;\n }\n throw cause;\n }\n}\n","/**\n * Block-body XML escape (ADR D9 — prompt-injection defence).\n *\n * Order matters: `&` MUST be escaped first so subsequent `<`/`>` replacements\n * do not double-encode the `&` characters they introduce.\n *\n * @internal\n */\nexport const escapeBlockBody = (s: string): string =>\n s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\n","import { escapeBlockBody } from \"../system-prompt/escape.js\";\n\n/**\n * Render the `<skills>` system-prompt block from a skill list.\n *\n * Input is the structural subset `{ name, description }` — the skill BODY is\n * NOT in the type, so it cannot leak into the prompt. Both fields are passed\n * through `escapeBlockBody` to neutralise prompt-injection vectors hidden in\n * user-controlled SKILL.md frontmatter (injection-escape ADR).\n *\n * Returns `undefined` for an empty list so the caller can omit the block.\n *\n * Public via `@theokit/sdk/skills`.\n *\n * @public\n */\nexport function buildSkillsBlock(\n skills: ReadonlyArray<{ name: string; description: string }>,\n): string | undefined {\n if (skills.length === 0) return undefined;\n const lines = skills.map(\n (skill) => ` - ${escapeBlockBody(skill.name)}: ${escapeBlockBody(skill.description)}`,\n );\n return `<skills>\\n${lines.join(\"\\n\")}\\n</skills>`;\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var async_hooks = require('async_hooks');
|
|
4
|
+
|
|
5
|
+
// src/internal/runtime/concurrency/async-local-storage.ts
|
|
6
|
+
var toolWhitelistStore = new async_hooks.AsyncLocalStorage();
|
|
7
|
+
async function withToolWhitelist(whitelist, fn) {
|
|
8
|
+
return toolWhitelistStore.run(whitelist, fn);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/internal/runtime/skills/subagent-tool-scope.ts
|
|
12
|
+
function subagentToolWhitelist(definition) {
|
|
13
|
+
const { tools } = definition;
|
|
14
|
+
return Array.isArray(tools) && tools.length > 0 ? new Set(tools) : void 0;
|
|
15
|
+
}
|
|
16
|
+
function withSubagentToolScope(definition, fn) {
|
|
17
|
+
const whitelist = subagentToolWhitelist(definition);
|
|
18
|
+
return whitelist ? withToolWhitelist(whitelist, fn) : fn();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exports.subagentToolWhitelist = subagentToolWhitelist;
|
|
22
|
+
exports.withSubagentToolScope = withSubagentToolScope;
|
|
23
|
+
//# sourceMappingURL=subagents.cjs.map
|
|
24
|
+
//# sourceMappingURL=subagents.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/runtime/concurrency/async-local-storage.ts","../src/internal/runtime/skills/subagent-tool-scope.ts"],"names":["AsyncLocalStorage"],"mappings":";;;;;AAsBA,IAAM,kBAAA,GAAqB,IAAIA,6BAAA,EAA+B;AAQ9D,eAAsB,iBAAA,CACpB,WACA,EAAA,EACY;AACZ,EAAA,OAAO,kBAAA,CAAmB,GAAA,CAAI,SAAA,EAAW,EAAE,CAAA;AAC7C;;;ACvBO,SAAS,sBAAsB,UAAA,EAAsD;AAC1F,EAAA,MAAM,EAAE,OAAM,GAAI,UAAA;AAClB,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,CAAM,SAAS,CAAA,GAAI,IAAI,GAAA,CAAI,KAAK,CAAA,GAAI,MAAA;AACrE;AAeO,SAAS,qBAAA,CACd,YACA,EAAA,EACY;AACZ,EAAA,MAAM,SAAA,GAAY,sBAAsB,UAAU,CAAA;AAClD,EAAA,OAAO,SAAA,GAAY,iBAAA,CAAkB,SAAA,EAAW,EAAE,IAAI,EAAA,EAAG;AAC3D","file":"subagents.cjs","sourcesContent":["/**\n * Per-fork tool whitelist via `AsyncLocalStorage` (ADR D111).\n *\n * Forked agents (background review, curator, judge) need a tool subset\n * distinct from the parent's. A global mutable `let _whitelist` would\n * corrupt state when two forks run in parallel. `AsyncLocalStorage`\n * propagates the whitelist through the async chain so each fork sees its\n * own — no cross-fork bleed.\n *\n * Outside a `withToolWhitelist(...)` scope, `currentToolWhitelist()`\n * returns `undefined` and `checkToolWhitelist` allows every tool — the\n * parent agent is unaffected.\n *\n * Wire site: `internal/agent-loop/tool-dispatch.ts:dispatchSingleCall`\n * calls `checkToolWhitelist` after the repair middleware and before\n * `tools.find`.\n *\n * @internal\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nconst toolWhitelistStore = new AsyncLocalStorage<Set<string>>();\n\n/**\n * Run `fn` with `whitelist` as the active tool filter. Nested calls\n * shadow the outer set; the outer is restored on return (EC-F).\n *\n * @internal\n */\nexport async function withToolWhitelist<T>(\n whitelist: Set<string>,\n fn: () => Promise<T>,\n): Promise<T> {\n return toolWhitelistStore.run(whitelist, fn);\n}\n\n/**\n * Active tool whitelist for the current async context, or `undefined`\n * when not inside a `withToolWhitelist(...)` scope.\n *\n * @internal\n */\nexport function currentToolWhitelist(): Set<string> | undefined {\n return toolWhitelistStore.getStore();\n}\n\n/**\n * Decision returned by {@link checkToolWhitelist}.\n *\n * @internal\n */\nexport interface ToolWhitelistDecision {\n allowed: boolean;\n /** Populated only when `allowed === false`. */\n reason?: string;\n}\n\n/**\n * Check whether `toolName` is allowed in the current fork context.\n * Returns `{ allowed: true }` when no fork is active — preserves the\n * parent agent's full tool surface.\n *\n * @internal\n */\nexport function checkToolWhitelist(toolName: string): ToolWhitelistDecision {\n const whitelist = currentToolWhitelist();\n if (whitelist === undefined) return { allowed: true };\n if (!whitelist.has(toolName)) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" not available in this fork context`,\n };\n }\n return { allowed: true };\n}\n","import type { AgentDefinition } from \"../../../types/agent.js\";\nimport { withToolWhitelist } from \"../concurrency/async-local-storage.js\";\n\n/**\n * Resolve a sub-agent's tool whitelist from its {@link AgentDefinition.tools}\n * (M4-6). Returns a `Set` of allowed tool names when `tools` is a non-empty\n * array, else `undefined` (unscoped — the sub-agent inherits the parent's full\n * toolset). The `Set` is the exact shape `withToolWhitelist` /\n * `ForkOptions.allowedTools` consume.\n *\n * @public\n */\nexport function subagentToolWhitelist(definition: AgentDefinition): Set<string> | undefined {\n const { tools } = definition;\n return Array.isArray(tools) && tools.length > 0 ? new Set(tools) : undefined;\n}\n\n/**\n * Run `fn` under the sub-agent's tool whitelist (M4-6). When the definition\n * declares `tools`, the run executes inside `withToolWhitelist(set, fn)` — so\n * every tool call the sub-agent makes is vetoed at dispatch (`checkToolWhitelist`,\n * the same enforcement forks use) unless its canonical name is whitelisted: a\n * `tools: [\"read_file\"]` sub-agent provably cannot call `write_file`/`shell_exec`.\n * Enforcement is `withToolWhitelist`, NOT `PermissionEngine`.\n *\n * An unscoped definition (no/empty `tools`) runs `fn` directly — the parent's\n * full toolset is preserved.\n *\n * @public\n */\nexport function withSubagentToolScope<T>(\n definition: AgentDefinition,\n fn: () => Promise<T>,\n): Promise<T> {\n const whitelist = subagentToolWhitelist(definition);\n return whitelist ? withToolWhitelist(whitelist, fn) : fn();\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@theokit/sdk/subagents` — sub-agent tool scoping (M4-6).
|
|
3
|
+
*
|
|
4
|
+
* `subagentToolWhitelist(definition)` derives a `Set` of allowed tool names
|
|
5
|
+
* from an `AgentDefinition.tools` whitelist (or `undefined` when unscoped).
|
|
6
|
+
* `withSubagentToolScope(definition, fn)` runs `fn` under that whitelist via
|
|
7
|
+
* the SDK's existing `withToolWhitelist` enforcement (the same dispatch veto
|
|
8
|
+
* forks use) — so a `tools: ["read_file"]` sub-agent provably cannot call
|
|
9
|
+
* `write_file`/`shell_exec`. NOT `PermissionEngine`.
|
|
10
|
+
*
|
|
11
|
+
* Lives on a dedicated sub-export (not the main barrel) because it reaches into
|
|
12
|
+
* `internal/runtime` — same isolation pattern as `@theokit/sdk/path-safety`.
|
|
13
|
+
*/
|
|
14
|
+
export { subagentToolWhitelist, withSubagentToolScope, } from "./internal/runtime/skills/subagent-tool-scope.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@theokit/sdk/subagents` — sub-agent tool scoping (M4-6).
|
|
3
|
+
*
|
|
4
|
+
* `subagentToolWhitelist(definition)` derives a `Set` of allowed tool names
|
|
5
|
+
* from an `AgentDefinition.tools` whitelist (or `undefined` when unscoped).
|
|
6
|
+
* `withSubagentToolScope(definition, fn)` runs `fn` under that whitelist via
|
|
7
|
+
* the SDK's existing `withToolWhitelist` enforcement (the same dispatch veto
|
|
8
|
+
* forks use) — so a `tools: ["read_file"]` sub-agent provably cannot call
|
|
9
|
+
* `write_file`/`shell_exec`. NOT `PermissionEngine`.
|
|
10
|
+
*
|
|
11
|
+
* Lives on a dedicated sub-export (not the main barrel) because it reaches into
|
|
12
|
+
* `internal/runtime` — same isolation pattern as `@theokit/sdk/path-safety`.
|
|
13
|
+
*/
|
|
14
|
+
export { subagentToolWhitelist, withSubagentToolScope, } from "./internal/runtime/skills/subagent-tool-scope.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
// src/internal/runtime/concurrency/async-local-storage.ts
|
|
4
|
+
var toolWhitelistStore = new AsyncLocalStorage();
|
|
5
|
+
async function withToolWhitelist(whitelist, fn) {
|
|
6
|
+
return toolWhitelistStore.run(whitelist, fn);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/internal/runtime/skills/subagent-tool-scope.ts
|
|
10
|
+
function subagentToolWhitelist(definition) {
|
|
11
|
+
const { tools } = definition;
|
|
12
|
+
return Array.isArray(tools) && tools.length > 0 ? new Set(tools) : void 0;
|
|
13
|
+
}
|
|
14
|
+
function withSubagentToolScope(definition, fn) {
|
|
15
|
+
const whitelist = subagentToolWhitelist(definition);
|
|
16
|
+
return whitelist ? withToolWhitelist(whitelist, fn) : fn();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { subagentToolWhitelist, withSubagentToolScope };
|
|
20
|
+
//# sourceMappingURL=subagents.js.map
|
|
21
|
+
//# sourceMappingURL=subagents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/runtime/concurrency/async-local-storage.ts","../src/internal/runtime/skills/subagent-tool-scope.ts"],"names":[],"mappings":";;;AAsBA,IAAM,kBAAA,GAAqB,IAAI,iBAAA,EAA+B;AAQ9D,eAAsB,iBAAA,CACpB,WACA,EAAA,EACY;AACZ,EAAA,OAAO,kBAAA,CAAmB,GAAA,CAAI,SAAA,EAAW,EAAE,CAAA;AAC7C;;;ACvBO,SAAS,sBAAsB,UAAA,EAAsD;AAC1F,EAAA,MAAM,EAAE,OAAM,GAAI,UAAA;AAClB,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,CAAM,SAAS,CAAA,GAAI,IAAI,GAAA,CAAI,KAAK,CAAA,GAAI,MAAA;AACrE;AAeO,SAAS,qBAAA,CACd,YACA,EAAA,EACY;AACZ,EAAA,MAAM,SAAA,GAAY,sBAAsB,UAAU,CAAA;AAClD,EAAA,OAAO,SAAA,GAAY,iBAAA,CAAkB,SAAA,EAAW,EAAE,IAAI,EAAA,EAAG;AAC3D","file":"subagents.js","sourcesContent":["/**\n * Per-fork tool whitelist via `AsyncLocalStorage` (ADR D111).\n *\n * Forked agents (background review, curator, judge) need a tool subset\n * distinct from the parent's. A global mutable `let _whitelist` would\n * corrupt state when two forks run in parallel. `AsyncLocalStorage`\n * propagates the whitelist through the async chain so each fork sees its\n * own — no cross-fork bleed.\n *\n * Outside a `withToolWhitelist(...)` scope, `currentToolWhitelist()`\n * returns `undefined` and `checkToolWhitelist` allows every tool — the\n * parent agent is unaffected.\n *\n * Wire site: `internal/agent-loop/tool-dispatch.ts:dispatchSingleCall`\n * calls `checkToolWhitelist` after the repair middleware and before\n * `tools.find`.\n *\n * @internal\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nconst toolWhitelistStore = new AsyncLocalStorage<Set<string>>();\n\n/**\n * Run `fn` with `whitelist` as the active tool filter. Nested calls\n * shadow the outer set; the outer is restored on return (EC-F).\n *\n * @internal\n */\nexport async function withToolWhitelist<T>(\n whitelist: Set<string>,\n fn: () => Promise<T>,\n): Promise<T> {\n return toolWhitelistStore.run(whitelist, fn);\n}\n\n/**\n * Active tool whitelist for the current async context, or `undefined`\n * when not inside a `withToolWhitelist(...)` scope.\n *\n * @internal\n */\nexport function currentToolWhitelist(): Set<string> | undefined {\n return toolWhitelistStore.getStore();\n}\n\n/**\n * Decision returned by {@link checkToolWhitelist}.\n *\n * @internal\n */\nexport interface ToolWhitelistDecision {\n allowed: boolean;\n /** Populated only when `allowed === false`. */\n reason?: string;\n}\n\n/**\n * Check whether `toolName` is allowed in the current fork context.\n * Returns `{ allowed: true }` when no fork is active — preserves the\n * parent agent's full tool surface.\n *\n * @internal\n */\nexport function checkToolWhitelist(toolName: string): ToolWhitelistDecision {\n const whitelist = currentToolWhitelist();\n if (whitelist === undefined) return { allowed: true };\n if (!whitelist.has(toolName)) {\n return {\n allowed: false,\n reason: `Tool \"${toolName}\" not available in this fork context`,\n };\n }\n return { allowed: true };\n}\n","import type { AgentDefinition } from \"../../../types/agent.js\";\nimport { withToolWhitelist } from \"../concurrency/async-local-storage.js\";\n\n/**\n * Resolve a sub-agent's tool whitelist from its {@link AgentDefinition.tools}\n * (M4-6). Returns a `Set` of allowed tool names when `tools` is a non-empty\n * array, else `undefined` (unscoped — the sub-agent inherits the parent's full\n * toolset). The `Set` is the exact shape `withToolWhitelist` /\n * `ForkOptions.allowedTools` consume.\n *\n * @public\n */\nexport function subagentToolWhitelist(definition: AgentDefinition): Set<string> | undefined {\n const { tools } = definition;\n return Array.isArray(tools) && tools.length > 0 ? new Set(tools) : undefined;\n}\n\n/**\n * Run `fn` under the sub-agent's tool whitelist (M4-6). When the definition\n * declares `tools`, the run executes inside `withToolWhitelist(set, fn)` — so\n * every tool call the sub-agent makes is vetoed at dispatch (`checkToolWhitelist`,\n * the same enforcement forks use) unless its canonical name is whitelisted: a\n * `tools: [\"read_file\"]` sub-agent provably cannot call `write_file`/`shell_exec`.\n * Enforcement is `withToolWhitelist`, NOT `PermissionEngine`.\n *\n * An unscoped definition (no/empty `tools`) runs `fn` directly — the parent's\n * full toolset is preserved.\n *\n * @public\n */\nexport function withSubagentToolScope<T>(\n definition: AgentDefinition,\n fn: () => Promise<T>,\n): Promise<T> {\n const whitelist = subagentToolWhitelist(definition);\n return whitelist ? withToolWhitelist(whitelist, fn) : fn();\n}\n"]}
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -68,6 +68,14 @@ export interface AgentDefinition {
|
|
|
68
68
|
prompt: string;
|
|
69
69
|
model?: ModelSelection | "inherit";
|
|
70
70
|
mcpServers?: Array<string | Record<string, McpServerConfig>>;
|
|
71
|
+
/**
|
|
72
|
+
* Tool whitelist (M4-6). When set, the sub-agent may ONLY call tools whose
|
|
73
|
+
* canonical (post-repair, lowercase) name is in this list — any other tool
|
|
74
|
+
* call is vetoed at dispatch via the same `withToolWhitelist` enforcement
|
|
75
|
+
* forks use (NOT `PermissionEngine`). Absent/empty → unscoped (inherits the
|
|
76
|
+
* parent's full toolset). Apply with `withSubagentToolScope`.
|
|
77
|
+
*/
|
|
78
|
+
tools?: string[];
|
|
71
79
|
}
|
|
72
80
|
/**
|
|
73
81
|
* Public skill metadata exposed to the system-prompt resolver. Mirrors the
|
package/dist/types/eval.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @public
|
|
10
10
|
*/
|
|
11
|
+
import type { SandboxBackend } from "../sandbox/types.js";
|
|
11
12
|
import type { SDKAgent } from "./agent.js";
|
|
12
13
|
/** Inferred `Agent.create` options shape — avoid cycling through `AgentOptions` directly. */
|
|
13
14
|
export type EvalAgentOptions = Parameters<typeof import("../agent.js").Agent.create>[0];
|
|
@@ -92,6 +93,43 @@ export interface EvalRowResult {
|
|
|
92
93
|
readonly tokensOut?: number;
|
|
93
94
|
readonly error?: string;
|
|
94
95
|
readonly metadata?: Record<string, unknown>;
|
|
96
|
+
/**
|
|
97
|
+
* Free-form taxonomy label assigned by `EvalRunOptions.classify` (M6-1).
|
|
98
|
+
* Persisted alongside the row when `persist` is set.
|
|
99
|
+
*/
|
|
100
|
+
readonly outcome?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Captured code change (M6-4): the working-tree `git diff` an agent produced
|
|
103
|
+
* and whether it reverse-applies cleanly. Produced by `captureArtifact`; the
|
|
104
|
+
* caller attaches it to the row (the runner does not set it automatically).
|
|
105
|
+
*/
|
|
106
|
+
readonly artifact?: {
|
|
107
|
+
readonly diff: string;
|
|
108
|
+
readonly applies: boolean;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Options for `Scorers.verifyGate` (M6-2) — grade a patch by running the
|
|
113
|
+
* project's tests in a provisioned repo and reading the exit code.
|
|
114
|
+
*/
|
|
115
|
+
export interface VerifyGateOptions {
|
|
116
|
+
/** Sandbox the test command runs in (D2 — Local/Docker/E2B). */
|
|
117
|
+
readonly sandbox: SandboxBackend;
|
|
118
|
+
/** Repo dir to run the tests from (typically `provisionRepo`'s `repoDir`). */
|
|
119
|
+
readonly repoDir: string;
|
|
120
|
+
/** SWE-bench tests that must flip to passing after the patch. */
|
|
121
|
+
readonly failToPass: readonly string[];
|
|
122
|
+
/** SWE-bench tests that must stay passing after the patch. */
|
|
123
|
+
readonly passToPass: readonly string[];
|
|
124
|
+
/**
|
|
125
|
+
* REQUIRED — builds the shell command from the combined test list, e.g.
|
|
126
|
+
* `(t) => "pytest " + t.join(" ")`. The builder OWNS shell-safety of the test
|
|
127
|
+
* identifiers: the SDK runs the returned string in a shell, so a builder that
|
|
128
|
+
* concatenates untrusted dataset test names without quoting is an injection
|
|
129
|
+
* vector (the SDK deliberately ships NO unsafe default that would run bare
|
|
130
|
+
* identifiers — see SECURITY note on `verifyGate`).
|
|
131
|
+
*/
|
|
132
|
+
readonly command: (tests: readonly string[]) => string;
|
|
95
133
|
}
|
|
96
134
|
/** Per-scorer breakdown computed across all rows. */
|
|
97
135
|
export interface PerScorerStats {
|
|
@@ -125,8 +163,41 @@ export interface EvalRun {
|
|
|
125
163
|
readonly rows: ReadonlyArray<EvalRowResult>;
|
|
126
164
|
readonly metadata?: Record<string, unknown>;
|
|
127
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Crash-durable persistence for `eval.run(...)` (M6-1). Each completed row is
|
|
168
|
+
* appended to `path` the instant it finishes (one `\n`-terminated JSON line),
|
|
169
|
+
* so a crashed multi-hour run resumes without re-paying completed work.
|
|
170
|
+
*
|
|
171
|
+
* Single-process contract: `appendFileSync` serializes writes within one Node
|
|
172
|
+
* process; do NOT point two concurrent processes at the same `path`.
|
|
173
|
+
*/
|
|
174
|
+
export interface EvalPersistOptions {
|
|
175
|
+
/** JSONL output path. Parent directories are created on first append. */
|
|
176
|
+
readonly path: string;
|
|
177
|
+
/**
|
|
178
|
+
* Stable identity of a row, used for resume. MUST be computed only from the
|
|
179
|
+
* fields available at resume-probe time (`index` / `input` / `expected` /
|
|
180
|
+
* `metadata`) — the type enforces this: at resume the key is computed from a
|
|
181
|
+
* probe row BEFORE the agent runs, so `output` / `scores` / `meanScore` are
|
|
182
|
+
* not yet known. Reading any non-durable field would silently break resume.
|
|
183
|
+
*/
|
|
184
|
+
readonly key: (row: Pick<EvalRowResult, "index" | "input" | "expected" | "metadata">) => string;
|
|
185
|
+
/**
|
|
186
|
+
* When `true`, rows whose `key` already appears in `path` with a SUCCESSFUL
|
|
187
|
+
* (no `error`) result are skipped — not re-executed and not re-emitted in
|
|
188
|
+
* `EvalRun.rows`. Failed rows are always retried.
|
|
189
|
+
*/
|
|
190
|
+
readonly resume?: boolean;
|
|
191
|
+
}
|
|
128
192
|
/** Per-call options for `eval.run(...)`. */
|
|
129
193
|
export interface EvalRunOptions {
|
|
130
194
|
/** Cancels pending rows; in-flight rows complete (D140 pattern). */
|
|
131
195
|
readonly signal?: AbortSignal;
|
|
196
|
+
/** Durable, resumable per-row persistence (M6-1). Absent → no file is written. */
|
|
197
|
+
readonly persist?: EvalPersistOptions;
|
|
198
|
+
/**
|
|
199
|
+
* Optional taxonomy classifier applied to each completed row; the result is
|
|
200
|
+
* stored on `EvalRowResult.outcome` and persisted (M6-1).
|
|
201
|
+
*/
|
|
202
|
+
readonly classify?: (row: EvalRowResult) => string;
|
|
132
203
|
}
|