@sema-agent/core 2.7.0 → 2.9.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/dist/agents/send-message-tool.d.ts +2 -0
- package/dist/agents/send-message-tool.js +38 -30
- package/dist/agents/subagent.js +15 -3
- package/dist/brain/circuit-breaker.js +18 -8
- package/dist/brain/retry.d.ts +1 -0
- package/dist/brain/retry.js +29 -7
- package/dist/brain/stream-engine.d.ts +1 -0
- package/dist/brain/stream-engine.js +74 -12
- package/dist/core/auto-compaction.js +9 -1
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/background-agent-store.js +20 -0
- package/dist/core/mcp.js +8 -5
- package/dist/core/runner/prepare-task.js +25 -8
- package/dist/core/runner/runtask.js +20 -7
- package/dist/core/runner/tool-disclosure.d.ts +8 -3
- package/dist/core/runner/tool-disclosure.js +22 -8
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +257 -28
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -1
- package/dist/core/store-contracts/file-snapshot-store-contract.js +11 -3
- package/dist/core/task-registry-agent.d.ts +2 -1
- package/dist/core/task-registry-agent.js +47 -54
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +1 -1
- package/dist/core/tools.d.ts +6 -2
- package/dist/core/tools.js +3 -2
- package/dist/core/types.d.ts +5 -1
- package/dist/engine/compaction/compaction.js +71 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/harness.d.ts +1 -1
- package/dist/internal/harness.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +1 -0
- package/dist/orchestration/run-workflow-tool.js +4 -1
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/orchestration/workflow.js +2 -2
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
- package/dist/tools/fs/bash-readonly-classifier.js +113 -33
- package/dist/tools/fs/fs-bash.d.ts +2 -1
- package/dist/tools/fs/fs-bash.js +26 -6
- package/dist/tools/fs/fs-shared.d.ts +1 -0
- package/dist/tools/fs/fs-shared.js +44 -2
- package/dist/tools/fs/index.js +1 -0
- package/package.json +1 -1
|
@@ -758,6 +758,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
758
758
|
clientContext: spec.clientContext,
|
|
759
759
|
excludeTools: toolFaceSnapshot.exclude,
|
|
760
760
|
deferTools: toolFaceSnapshot.defer,
|
|
761
|
+
alwaysLoadTools: toolFaceSnapshot.alwaysLoad,
|
|
761
762
|
promptProfile,
|
|
762
763
|
...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
|
|
763
764
|
...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
|
|
@@ -885,6 +886,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
885
886
|
governanceBaseline: deps.workflowGovernanceBaseline,
|
|
886
887
|
parentExcludeTools: toolFaceSnapshot.exclude,
|
|
887
888
|
parentDeferTools: toolFaceSnapshot.defer,
|
|
889
|
+
parentAlwaysLoadTools: toolFaceSnapshot.alwaysLoad,
|
|
888
890
|
parentPromptProfile: promptProfile,
|
|
889
891
|
models: deps.models,
|
|
890
892
|
agents: deps.agents,
|
|
@@ -1278,6 +1280,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1278
1280
|
...(internals?.parentRetainLedger !== undefined ? { siblingRetain: internals.parentRetainLedger } : {}),
|
|
1279
1281
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
1280
1282
|
...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
|
|
1283
|
+
enrichCtx: enrichSpecToolCtx,
|
|
1281
1284
|
...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}),
|
|
1282
1285
|
...(deps.onBackgroundChildEvent ? { onBackgroundChildEvent: deps.onBackgroundChildEvent } : {}),
|
|
1283
1286
|
...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
|
|
@@ -1795,7 +1798,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1795
1798
|
deferNames: (toolFaceSnapshot.defer ?? []).filter((n) => tools.some((t) => t.name === n)),
|
|
1796
1799
|
alwaysLoadNames: [
|
|
1797
1800
|
...(toolFaceSnapshot.alwaysLoad ?? []),
|
|
1798
|
-
...mcp.tools
|
|
1801
|
+
...mcp.tools
|
|
1802
|
+
.filter((t) => t.mcpAlwaysLoad === true && !(toolFaceSnapshot.defer ?? []).includes(t.name))
|
|
1803
|
+
.map((t) => t.name),
|
|
1799
1804
|
],
|
|
1800
1805
|
});
|
|
1801
1806
|
for (const n of [...deferred]) {
|
|
@@ -1830,21 +1835,32 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1830
1835
|
throw e;
|
|
1831
1836
|
}
|
|
1832
1837
|
const registry = buildDeferredRegistry(deferred, tools);
|
|
1833
|
-
const realByName = new Map(tools.map((t) => [t.name, t]));
|
|
1834
1838
|
const directCallFor = (name) => {
|
|
1835
1839
|
if (spec.deferSelfResolve === false)
|
|
1836
1840
|
return undefined;
|
|
1837
|
-
const
|
|
1838
|
-
if (real === undefined)
|
|
1839
|
-
return undefined;
|
|
1841
|
+
const executionMode = tools.find((t) => t.name === name)?.executionMode;
|
|
1840
1842
|
return {
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
+
resolveReal: () => {
|
|
1844
|
+
const real = tools.find((t) => t.name === name);
|
|
1845
|
+
if (real === undefined)
|
|
1846
|
+
return undefined;
|
|
1847
|
+
return {
|
|
1848
|
+
parameters: real.parameters,
|
|
1849
|
+
invoke: (toolCallId, params, signal, onUpdate) => real.execute(toolCallId, params, signal, onUpdate),
|
|
1850
|
+
};
|
|
1851
|
+
},
|
|
1852
|
+
...(executionMode !== undefined ? { executionMode } : {}),
|
|
1843
1853
|
activate: async () => {
|
|
1844
1854
|
if (activeTools.has(name))
|
|
1845
1855
|
return;
|
|
1846
1856
|
activeTools.add(name);
|
|
1847
|
-
|
|
1857
|
+
try {
|
|
1858
|
+
await rematerialize(activeTools);
|
|
1859
|
+
}
|
|
1860
|
+
catch (e) {
|
|
1861
|
+
activeTools.delete(name);
|
|
1862
|
+
throw e;
|
|
1863
|
+
}
|
|
1848
1864
|
},
|
|
1849
1865
|
};
|
|
1850
1866
|
};
|
|
@@ -1917,6 +1933,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1917
1933
|
rematerialize,
|
|
1918
1934
|
listingRide: (newly) => listingRideRef.current?.(newly),
|
|
1919
1935
|
mountedNames: () => new Set(buildToolList(activeTools).map((t) => t.name).filter((n) => !registry.has(n) || activeTools.has(n))),
|
|
1936
|
+
directCallEnabled: spec.deferSelfResolve !== false,
|
|
1920
1937
|
});
|
|
1921
1938
|
harnessTools = buildToolList(activeTools);
|
|
1922
1939
|
}
|
|
@@ -688,13 +688,6 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
688
688
|
queue.push({ type: "compaction_outcome", outcome, trigger: passTrigger, ...ident() });
|
|
689
689
|
}
|
|
690
690
|
}
|
|
691
|
-
if (comp.compacted && !forceManual) {
|
|
692
|
-
if (recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
|
|
693
|
-
compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
|
|
694
|
-
runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
|
|
695
|
-
queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
691
|
runnerHooks.recordCompactionReuse(prepared, comp);
|
|
699
692
|
if (comp.compacted) {
|
|
700
693
|
queue.push({
|
|
@@ -713,6 +706,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
713
706
|
...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
|
|
714
707
|
...ident(),
|
|
715
708
|
});
|
|
709
|
+
if (!forceManual && recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
|
|
710
|
+
compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
|
|
711
|
+
queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
|
|
712
|
+
runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
|
|
713
|
+
}
|
|
716
714
|
if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
|
|
717
715
|
const pd = comp.phaseDurations;
|
|
718
716
|
const pdDur = comp.durationMs;
|
|
@@ -2074,6 +2072,9 @@ export class Runner {
|
|
|
2074
2072
|
phase: s.phase,
|
|
2075
2073
|
...(s.detail !== undefined ? { detail: s.detail } : {}),
|
|
2076
2074
|
...(s.retryInSec !== undefined ? { retryInSec: s.retryInSec } : {}),
|
|
2075
|
+
...(s.retryInMs !== undefined ? { retryInMs: s.retryInMs } : {}),
|
|
2076
|
+
...(s.attempt !== undefined ? { attempt: s.attempt } : {}),
|
|
2077
|
+
...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
|
|
2077
2078
|
...ident(),
|
|
2078
2079
|
});
|
|
2079
2080
|
};
|
|
@@ -2675,6 +2676,7 @@ export class Runner {
|
|
|
2675
2676
|
minTokens: rs.counters.compactionFloor,
|
|
2676
2677
|
brain: compactionBrain,
|
|
2677
2678
|
windowSafety: windowSafetyOptions(prepared.model),
|
|
2679
|
+
onCompactionFailed: (reason) => queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "auto", reason, ...ident() }),
|
|
2678
2680
|
});
|
|
2679
2681
|
try {
|
|
2680
2682
|
if (comp?.compacted) {
|
|
@@ -3516,6 +3518,17 @@ export class Runner {
|
|
|
3516
3518
|
comp = finishComp;
|
|
3517
3519
|
}
|
|
3518
3520
|
catch (err) {
|
|
3521
|
+
const failMsg = String(err instanceof Error ? err.message : err);
|
|
3522
|
+
const failReason = failMsg.length > 512 ? `${failMsg.slice(0, 512)}…` : failMsg;
|
|
3523
|
+
emitTrace(spec.tracer ?? this.deps.tracer, () => ({
|
|
3524
|
+
kind: "compaction.failed",
|
|
3525
|
+
version: 1,
|
|
3526
|
+
taskId: spec.taskId ?? prepared.sessionId,
|
|
3527
|
+
trigger: "auto",
|
|
3528
|
+
reason: failReason,
|
|
3529
|
+
ts: Date.now(),
|
|
3530
|
+
}));
|
|
3531
|
+
opts?.onCompactionFailed?.(failReason);
|
|
3519
3532
|
this.deps.onError?.(err, { phase: "compaction", sessionId: prepared.sessionId });
|
|
3520
3533
|
}
|
|
3521
3534
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TSchema } from "typebox";
|
|
2
|
-
import type { AgentMessage, AgentTool, AgentToolResult } from "../../internal/harness-types.js";
|
|
2
|
+
import type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode } from "../../internal/harness-types.js";
|
|
3
3
|
import type { Model } from "../../internal/llm.js";
|
|
4
4
|
import type { ToolSpec } from "../types.js";
|
|
5
5
|
export declare const TOOL_SEARCH_NAME = "ToolSearch";
|
|
@@ -29,10 +29,14 @@ export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, too
|
|
|
29
29
|
description: string;
|
|
30
30
|
}>): Map<string, DeferredToolInfo>;
|
|
31
31
|
export interface PlaceholderDirectCall {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
resolveReal: () => PlaceholderDirectTarget | undefined;
|
|
33
|
+
executionMode?: ToolExecutionMode;
|
|
34
34
|
activate: () => Promise<void>;
|
|
35
35
|
}
|
|
36
|
+
export interface PlaceholderDirectTarget {
|
|
37
|
+
parameters: TSchema;
|
|
38
|
+
invoke: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<unknown>) => Promise<AgentToolResult<unknown>>;
|
|
39
|
+
}
|
|
36
40
|
export declare function createPlaceholderTool(info: DeferredToolInfo, direct?: PlaceholderDirectCall): AgentTool;
|
|
37
41
|
export declare function scoreToolMatch(query: string, info: DeferredToolInfo): number;
|
|
38
42
|
export interface ToolSearchArgs {
|
|
@@ -52,4 +56,5 @@ export declare function createToolSearchTool(opts: {
|
|
|
52
56
|
rematerialize: (active: ReadonlySet<string>) => Promise<void>;
|
|
53
57
|
listingRide?: (newlyActivated: readonly string[]) => string | undefined;
|
|
54
58
|
mountedNames?: () => ReadonlySet<string>;
|
|
59
|
+
directCallEnabled?: boolean;
|
|
55
60
|
}): AgentTool;
|
|
@@ -43,7 +43,8 @@ export function classifyDeferred(opts) {
|
|
|
43
43
|
deferred.add(name);
|
|
44
44
|
if (opts.deferMode === "auto") {
|
|
45
45
|
const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name));
|
|
46
|
-
const
|
|
46
|
+
const inlineFace = opts.fullTools.filter((t) => !deferred.has(t.name));
|
|
47
|
+
const total = inlineFace.reduce((n, t) => n + inlinedChars(t), 0);
|
|
47
48
|
const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
|
|
48
49
|
if (window > 0 && total > DEFER_AUTO_FRACTION * window) {
|
|
49
50
|
for (const t of candidates)
|
|
@@ -70,12 +71,16 @@ export function createPlaceholderTool(info, direct) {
|
|
|
70
71
|
return {
|
|
71
72
|
name: info.name,
|
|
72
73
|
label: info.name,
|
|
73
|
-
description: `${info.hint} — deferred:
|
|
74
|
+
description: `${info.hint} — deferred: its parameters are not listed here. Call ` +
|
|
75
|
+
`${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load them; a call that already matches this ` +
|
|
76
|
+
`tool's real parameters runs directly and activates it.`,
|
|
74
77
|
parameters: EMPTY_PARAMS,
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
...(direct.executionMode !== undefined ? { executionMode: direct.executionMode } : {}),
|
|
79
|
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
80
|
+
const real = direct.resolveReal();
|
|
81
|
+
if (real !== undefined && Value.Check(real.parameters, params)) {
|
|
77
82
|
await direct.activate();
|
|
78
|
-
return
|
|
83
|
+
return real.invoke(toolCallId, params, signal, onUpdate);
|
|
79
84
|
}
|
|
80
85
|
return teachingRejection();
|
|
81
86
|
},
|
|
@@ -178,13 +183,22 @@ export function extractDiscoveredToolNames(messages, registry) {
|
|
|
178
183
|
}
|
|
179
184
|
export function createToolSearchTool(opts) {
|
|
180
185
|
const { registry, active, rematerialize, listingRide, mountedNames } = opts;
|
|
186
|
+
const directCallEnabled = opts.directCallEnabled !== false;
|
|
187
|
+
const activationPosture = directCallEnabled
|
|
188
|
+
? "Most tools start as name-only placeholders to keep requests small; activating one here loads its full " +
|
|
189
|
+
"parameter schema. Until you have that schema you cannot reliably form a call, so activate a tool rather " +
|
|
190
|
+
"than guessing its arguments — a call that does match the real schema executes and activates the tool. " +
|
|
191
|
+
"When any instruction, reminder, or another tool's description names a deferred tool, activate it here " +
|
|
192
|
+
'with query "select:<name>". '
|
|
193
|
+
: "Most tools start as name-only placeholders to keep requests small; to USE one you must activate it here " +
|
|
194
|
+
"first. When any instruction, reminder, or another tool's description names a deferred tool, activate it " +
|
|
195
|
+
'with query "select:<name>" before calling it. ';
|
|
181
196
|
let activationChain = Promise.resolve();
|
|
182
197
|
return defineTool({
|
|
183
198
|
name: TOOL_SEARCH_NAME,
|
|
184
199
|
contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
|
|
185
|
-
description: "Discover and activate deferred tools.
|
|
186
|
-
|
|
187
|
-
'tool\'s description names a deferred tool, activate it with query "select:<name>" before calling it. ' +
|
|
200
|
+
description: "Discover and activate deferred tools. " +
|
|
201
|
+
activationPosture +
|
|
188
202
|
"Query forms: " +
|
|
189
203
|
'"select:ToolA,ToolB" — activate these exact tools by name; ' +
|
|
190
204
|
'"notebook jupyter" — keyword search, up to max_results best matches; ' +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SkillSpec } from "./types.js";
|
|
2
|
-
export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "attachment_skipped" | "read_failed";
|
|
2
|
+
export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
|
|
3
3
|
export interface SkillsDirectoryWarning {
|
|
4
4
|
code: SkillsDirectoryWarningCode;
|
|
5
5
|
skill: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
3
|
+
import { canonicalToolName } from "./tool-name-aliases.js";
|
|
3
4
|
const SKILL_FILE = "SKILL.md";
|
|
4
5
|
const RESOURCE_DIRS = ["assets", "references", "scripts"];
|
|
5
6
|
const NAME_MAX_CHARS = 64;
|
|
@@ -22,26 +23,139 @@ function parseSkillFile(text) {
|
|
|
22
23
|
}
|
|
23
24
|
if (end === -1)
|
|
24
25
|
return { fields, body: normalized, hadFrontmatter: false };
|
|
25
|
-
|
|
26
|
+
let i = 1;
|
|
27
|
+
while (i < end) {
|
|
26
28
|
const line = lines[i];
|
|
27
29
|
const trimmed = line.trim();
|
|
28
|
-
if (trimmed === "" || trimmed.startsWith("#"))
|
|
30
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
31
|
+
i++;
|
|
29
32
|
continue;
|
|
30
|
-
|
|
33
|
+
}
|
|
34
|
+
if (/^\s/.test(line)) {
|
|
35
|
+
i++;
|
|
31
36
|
continue;
|
|
37
|
+
}
|
|
32
38
|
const kv = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(trimmed);
|
|
33
|
-
if (!kv)
|
|
39
|
+
if (!kv) {
|
|
40
|
+
i++;
|
|
34
41
|
continue;
|
|
42
|
+
}
|
|
35
43
|
const [, key, raw] = kv;
|
|
44
|
+
const rawValue = raw.trim();
|
|
45
|
+
const block = parseBlockScalarHeader(rawValue);
|
|
46
|
+
if (block !== undefined) {
|
|
47
|
+
const read = readBlockScalar(lines, i + 1, end, block);
|
|
48
|
+
i = read.next;
|
|
49
|
+
if (!fields.has(key))
|
|
50
|
+
fields.set(key, read.value);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (rawValue.startsWith("[") && !isFlowSequenceClosed(rawValue)) {
|
|
54
|
+
const joined = readFlowSequenceContinuation(lines, i + 1, end, rawValue);
|
|
55
|
+
i = joined.next;
|
|
56
|
+
if (!fields.has(key))
|
|
57
|
+
fields.set(key, joined.value);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
i++;
|
|
36
61
|
if (fields.has(key))
|
|
37
62
|
continue;
|
|
38
|
-
fields.set(key, unquote(
|
|
63
|
+
fields.set(key, unquote(rawValue));
|
|
39
64
|
}
|
|
40
65
|
let body = lines.slice(end + 1).join("\n");
|
|
41
66
|
if (body.startsWith("\n"))
|
|
42
67
|
body = body.slice(1);
|
|
43
68
|
return { fields, body, hadFrontmatter: true };
|
|
44
69
|
}
|
|
70
|
+
function parseBlockScalarHeader(value) {
|
|
71
|
+
const m = /^([|>])([0-9+-]*)$/.exec(value);
|
|
72
|
+
if (!m)
|
|
73
|
+
return undefined;
|
|
74
|
+
const rest = m[2];
|
|
75
|
+
if (!/^(?:[1-9]?[+-]?|[+-][1-9]?)$/.test(rest))
|
|
76
|
+
return undefined;
|
|
77
|
+
const digit = /[1-9]/.exec(rest)?.[0];
|
|
78
|
+
return { style: m[1] === "|" ? "literal" : "folded", ...(digit !== undefined ? { indent: Number(digit) } : {}) };
|
|
79
|
+
}
|
|
80
|
+
function readBlockScalar(lines, start, end, header) {
|
|
81
|
+
const indentOf = (s) => /^[ \t]*/.exec(s)[0].length;
|
|
82
|
+
let indent = header.indent;
|
|
83
|
+
if (indent === undefined) {
|
|
84
|
+
for (let j = start; j < end; j++) {
|
|
85
|
+
if (lines[j].trim() === "")
|
|
86
|
+
continue;
|
|
87
|
+
indent = indentOf(lines[j]);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (indent === undefined || indent === 0)
|
|
92
|
+
return { value: "", next: start };
|
|
93
|
+
const taken = [];
|
|
94
|
+
let j = start;
|
|
95
|
+
for (; j < end; j++) {
|
|
96
|
+
const line = lines[j];
|
|
97
|
+
if (line.trim() === "") {
|
|
98
|
+
taken.push("");
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (indentOf(line) < indent)
|
|
102
|
+
break;
|
|
103
|
+
taken.push(line.slice(indent));
|
|
104
|
+
}
|
|
105
|
+
while (taken.length > 0 && taken[taken.length - 1] === "")
|
|
106
|
+
taken.pop();
|
|
107
|
+
if (header.style === "literal")
|
|
108
|
+
return { value: taken.join("\n"), next: j };
|
|
109
|
+
let folded = "";
|
|
110
|
+
let blanks = 0;
|
|
111
|
+
let started = false;
|
|
112
|
+
let prevMoreIndented = false;
|
|
113
|
+
for (const cur of taken) {
|
|
114
|
+
if (cur === "") {
|
|
115
|
+
blanks++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const moreIndented = indentOf(cur) > 0;
|
|
119
|
+
if (!started) {
|
|
120
|
+
folded = cur;
|
|
121
|
+
started = true;
|
|
122
|
+
}
|
|
123
|
+
else if (blanks > 0) {
|
|
124
|
+
folded += "\n".repeat(blanks) + cur;
|
|
125
|
+
}
|
|
126
|
+
else if (moreIndented || prevMoreIndented) {
|
|
127
|
+
folded += `\n${cur}`;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
folded += ` ${cur}`;
|
|
131
|
+
}
|
|
132
|
+
blanks = 0;
|
|
133
|
+
prevMoreIndented = moreIndented;
|
|
134
|
+
}
|
|
135
|
+
return { value: folded, next: j };
|
|
136
|
+
}
|
|
137
|
+
function isFlowSequenceClosed(text) {
|
|
138
|
+
let depth = 0;
|
|
139
|
+
for (const ch of text) {
|
|
140
|
+
if (ch === "[")
|
|
141
|
+
depth++;
|
|
142
|
+
else if (ch === "]")
|
|
143
|
+
depth--;
|
|
144
|
+
}
|
|
145
|
+
return depth <= 0;
|
|
146
|
+
}
|
|
147
|
+
function readFlowSequenceContinuation(lines, start, end, head) {
|
|
148
|
+
let acc = head;
|
|
149
|
+
let j = start;
|
|
150
|
+
for (; j < end; j++) {
|
|
151
|
+
acc += ` ${lines[j].trim()}`;
|
|
152
|
+
if (isFlowSequenceClosed(acc)) {
|
|
153
|
+
j++;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { value: acc, next: j };
|
|
158
|
+
}
|
|
45
159
|
function unquote(value) {
|
|
46
160
|
if (value.length < 2)
|
|
47
161
|
return value;
|
|
@@ -51,7 +165,13 @@ function unquote(value) {
|
|
|
51
165
|
const inner = value.slice(1, -1);
|
|
52
166
|
return q === '"' ? inner.replace(/\\t/g, "\t").replace(/\\n/g, "\n") : inner;
|
|
53
167
|
}
|
|
54
|
-
function
|
|
168
|
+
function isInsideRoot(root, p) {
|
|
169
|
+
if (p === root)
|
|
170
|
+
return true;
|
|
171
|
+
const rel = relative(root, p);
|
|
172
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
173
|
+
}
|
|
174
|
+
function listRelativeFiles(base, dir, prefix, out, walk) {
|
|
55
175
|
let entries;
|
|
56
176
|
try {
|
|
57
177
|
entries = readdirSync(join(base, dir), { withFileTypes: true });
|
|
@@ -61,24 +181,68 @@ function listRelativeFiles(base, dir, prefix, out) {
|
|
|
61
181
|
}
|
|
62
182
|
for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
63
183
|
const rel = `${prefix}${e.name}`;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
184
|
+
const abs = join(base, dir, e.name);
|
|
185
|
+
const resolved = resolveInsideSkill(abs, rel, walk);
|
|
186
|
+
if (resolved === undefined)
|
|
187
|
+
continue;
|
|
188
|
+
if (resolved.stats.isDirectory()) {
|
|
189
|
+
if (walk.visited.has(resolved.real))
|
|
190
|
+
continue;
|
|
191
|
+
walk.visited.add(resolved.real);
|
|
192
|
+
listRelativeFiles(base, join(dir, e.name), `${rel}/`, out, walk);
|
|
193
|
+
}
|
|
194
|
+
else if (resolved.stats.isFile()) {
|
|
67
195
|
out.push(rel);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function resolveInsideSkill(abs, rel, walk) {
|
|
200
|
+
let real;
|
|
201
|
+
let stats;
|
|
202
|
+
try {
|
|
203
|
+
real = realpathSync(abs);
|
|
204
|
+
stats = statSync(abs);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
walk.warn({ code: "attachment_skipped", skill: walk.skill, detail: `${rel}: could not be resolved (${errText(err)})` });
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
if (!isInsideRoot(walk.realRoot, real)) {
|
|
211
|
+
walk.warn({
|
|
212
|
+
code: "attachment_skipped",
|
|
213
|
+
skill: walk.skill,
|
|
214
|
+
detail: `${rel}: resolves outside the skill directory — skipped (attachments are collected from the declared skill directory only)`,
|
|
215
|
+
});
|
|
216
|
+
return undefined;
|
|
68
217
|
}
|
|
218
|
+
return { real, stats };
|
|
69
219
|
}
|
|
70
220
|
function readAttachments(skillDir, skillName, budgetBytes, warn) {
|
|
221
|
+
let realRoot;
|
|
222
|
+
try {
|
|
223
|
+
realRoot = realpathSync(skillDir);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
warn({ code: "read_failed", skill: skillName, detail: `the skill directory could not be resolved (${errText(err)}) — attachments skipped` });
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
const walk = { realRoot, skill: skillName, warn, visited: new Set([realRoot]) };
|
|
71
230
|
const relPaths = [];
|
|
72
231
|
for (const d of RESOURCE_DIRS) {
|
|
73
|
-
let
|
|
232
|
+
let exists = true;
|
|
74
233
|
try {
|
|
75
|
-
|
|
234
|
+
statSync(join(skillDir, d));
|
|
76
235
|
}
|
|
77
236
|
catch {
|
|
78
|
-
|
|
237
|
+
exists = false;
|
|
79
238
|
}
|
|
80
|
-
if (
|
|
81
|
-
|
|
239
|
+
if (!exists)
|
|
240
|
+
continue;
|
|
241
|
+
const resolved = resolveInsideSkill(join(skillDir, d), d, walk);
|
|
242
|
+
if (resolved === undefined || !resolved.stats.isDirectory())
|
|
243
|
+
continue;
|
|
244
|
+
walk.visited.add(resolved.real);
|
|
245
|
+
listRelativeFiles(skillDir, d, `${d}/`, relPaths, walk);
|
|
82
246
|
}
|
|
83
247
|
relPaths.sort();
|
|
84
248
|
const files = [];
|
|
@@ -114,20 +278,60 @@ function isDecodableText(bytes) {
|
|
|
114
278
|
function errText(err) {
|
|
115
279
|
return err instanceof Error ? err.message : String(err);
|
|
116
280
|
}
|
|
117
|
-
|
|
281
|
+
const ALL_TOOLS_WILDCARD = "*";
|
|
282
|
+
function splitToolNames(declared) {
|
|
283
|
+
let text = declared.trim();
|
|
284
|
+
if (text.startsWith("[") && text.endsWith("]"))
|
|
285
|
+
text = text.slice(1, -1);
|
|
118
286
|
const names = [];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
287
|
+
let current = "";
|
|
288
|
+
let depth = 0;
|
|
289
|
+
const flush = () => {
|
|
290
|
+
const name = unquote(current.trim());
|
|
291
|
+
if (name !== "")
|
|
292
|
+
names.push(name);
|
|
293
|
+
current = "";
|
|
294
|
+
};
|
|
295
|
+
for (const ch of text) {
|
|
296
|
+
if (ch === "(") {
|
|
297
|
+
depth++;
|
|
298
|
+
current += ch;
|
|
299
|
+
}
|
|
300
|
+
else if (ch === ")") {
|
|
301
|
+
if (depth > 0)
|
|
302
|
+
depth--;
|
|
303
|
+
current += ch;
|
|
304
|
+
}
|
|
305
|
+
else if (depth === 0 && (ch === "," || /\s/.test(ch))) {
|
|
306
|
+
flush();
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
current += ch;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
flush();
|
|
313
|
+
return names;
|
|
314
|
+
}
|
|
315
|
+
function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools, warn) {
|
|
316
|
+
const names = [];
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
for (const n of splitToolNames(declared)) {
|
|
319
|
+
const key = canonicalToolName(n);
|
|
320
|
+
if (seen.has(key))
|
|
321
|
+
continue;
|
|
322
|
+
seen.add(key);
|
|
323
|
+
names.push(n);
|
|
122
324
|
}
|
|
123
325
|
if (names.length === 0)
|
|
124
326
|
return undefined;
|
|
327
|
+
if (names.some((n) => n === ALL_TOOLS_WILDCARD))
|
|
328
|
+
return undefined;
|
|
125
329
|
let allowTools = names;
|
|
126
330
|
if (deployedTools !== undefined) {
|
|
127
|
-
const mounted = new Set(deployedTools);
|
|
128
|
-
allowTools = names.filter((n) => mounted.has(n));
|
|
331
|
+
const mounted = new Set(deployedTools.map(canonicalToolName));
|
|
332
|
+
allowTools = names.filter((n) => mounted.has(canonicalToolName(n)));
|
|
129
333
|
for (const n of names) {
|
|
130
|
-
if (!mounted.has(n)) {
|
|
334
|
+
if (!mounted.has(canonicalToolName(n))) {
|
|
131
335
|
warn({
|
|
132
336
|
code: "allowed_tool_not_mounted",
|
|
133
337
|
skill: skillName,
|
|
@@ -136,6 +340,10 @@ function manifestFromAllowedTools(declared, skillName, deployedTools, warn) {
|
|
|
136
340
|
}
|
|
137
341
|
}
|
|
138
342
|
}
|
|
343
|
+
if (disallowed.length > 0) {
|
|
344
|
+
const denied = new Set(disallowed.map(canonicalToolName));
|
|
345
|
+
allowTools = allowTools.filter((n) => !denied.has(canonicalToolName(n)));
|
|
346
|
+
}
|
|
139
347
|
return { allowTools, lineageId: `skill:${skillName}` };
|
|
140
348
|
}
|
|
141
349
|
export function createSkillsFromDirectory(dir, options = {}) {
|
|
@@ -153,10 +361,23 @@ export function createSkillsFromDirectory(dir, options = {}) {
|
|
|
153
361
|
catch (err) {
|
|
154
362
|
throw new Error(`skills directory could not be read: ${dir} (${errText(err)})`);
|
|
155
363
|
}
|
|
156
|
-
const dirNames =
|
|
157
|
-
|
|
158
|
-
.
|
|
159
|
-
|
|
364
|
+
const dirNames = [];
|
|
365
|
+
for (const e of entries) {
|
|
366
|
+
if (e.isDirectory()) {
|
|
367
|
+
dirNames.push(e.name);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!e.isSymbolicLink())
|
|
371
|
+
continue;
|
|
372
|
+
try {
|
|
373
|
+
if (statSync(join(dir, e.name)).isDirectory())
|
|
374
|
+
dirNames.push(e.name);
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
warn({ code: "read_failed", skill: e.name, detail: `entry is a symbolic link that could not be resolved (${errText(err)}) — skipped` });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
dirNames.sort();
|
|
160
381
|
const skills = [];
|
|
161
382
|
for (const name of dirNames) {
|
|
162
383
|
const skillDir = join(dir, name);
|
|
@@ -200,7 +421,15 @@ export function createSkillsFromDirectory(dir, options = {}) {
|
|
|
200
421
|
continue;
|
|
201
422
|
}
|
|
202
423
|
const allowed = parsed.fields.get("allowed-tools");
|
|
203
|
-
const
|
|
424
|
+
const disallowed = splitToolNames(parsed.fields.get("disallowed-tools") ?? "");
|
|
425
|
+
const manifest = allowed === undefined ? undefined : manifestFromAllowedTools(allowed, disallowed, name, options.deployedTools, warn);
|
|
426
|
+
if (disallowed.length > 0 && manifest === undefined) {
|
|
427
|
+
warn({
|
|
428
|
+
code: "disallowed_tools_unenforced",
|
|
429
|
+
skill: name,
|
|
430
|
+
detail: `disallowed-tools names ${disallowed.map((n) => `"${n}"`).join(", ")}, but this skill produced no allowlist to subtract from (allowed-tools is absent or claims every tool) — the deny declaration is NOT enforced (express it in the task's own tool policy)`,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
204
433
|
const files = readAttachments(skillDir, name, budget, warn);
|
|
205
434
|
skills.push({
|
|
206
435
|
name: declaredName,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { FileSnapshotStore } from "../file-snapshot-store.js";
|
|
2
2
|
import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
3
|
-
export declare function fileSnapshotStoreContract(make: () => FileSnapshotStore, runAssertion?: ContractAssertionRunner
|
|
3
|
+
export declare function fileSnapshotStoreContract(make: () => FileSnapshotStore, runAssertion?: ContractAssertionRunner, options?: {
|
|
4
|
+
blobGc?: "immediate" | "eventual";
|
|
5
|
+
}): Promise<void>;
|
|
@@ -4,8 +4,9 @@ import { beginContract } from "./contract-harness.js";
|
|
|
4
4
|
const bytes = (s) => new TextEncoder().encode(s);
|
|
5
5
|
const sha256 = (s) => createHash("sha256").update(bytes(s)).digest("hex");
|
|
6
6
|
const srcBlob = async (hash) => hash === sha256("alpha") ? bytes("alpha") : hash === sha256("beta") ? bytes("beta") : undefined;
|
|
7
|
-
export async function fileSnapshotStoreContract(make, runAssertion) {
|
|
7
|
+
export async function fileSnapshotStoreContract(make, runAssertion, options) {
|
|
8
8
|
const { run, settle } = beginContract(runAssertion);
|
|
9
|
+
const blobGc = options?.blobGc ?? "immediate";
|
|
9
10
|
run("kit prerequisites: exportManifest/getBlob/putBlob/importManifest are implemented (REQUIRED by this kit)", async () => {
|
|
10
11
|
const probe = make();
|
|
11
12
|
const missing = ["exportManifest", "getBlob", "putBlob", "importManifest"].filter((m) => typeof probe[m] !== "function");
|
|
@@ -115,12 +116,19 @@ export async function fileSnapshotStoreContract(make, runAssertion) {
|
|
|
115
116
|
assert.deepEqual(await store.listKeys("sc"), ["k1"]);
|
|
116
117
|
assert.deepEqual([...(await store.getBlob(sha256("alpha")))], [...bytes("alpha")]);
|
|
117
118
|
});
|
|
118
|
-
run("reap keep-nothing drops the key
|
|
119
|
+
run("reap keep-nothing drops the key IMMEDIATELY (every backend)", async () => {
|
|
119
120
|
const store = make();
|
|
120
121
|
await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
|
|
121
122
|
assert.equal(await store.reap("sc", []), 1);
|
|
122
123
|
assert.equal(await store.has("sc", "k1"), false);
|
|
123
|
-
assert.equal(await store.getBlob(sha256("alpha")), undefined);
|
|
124
124
|
});
|
|
125
|
+
if (blobGc === "immediate") {
|
|
126
|
+
run("reap keep-nothing GCs the now-unreferenced blob bytes immediately (blobGc: immediate)", async () => {
|
|
127
|
+
const store = make();
|
|
128
|
+
await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
|
|
129
|
+
assert.equal(await store.reap("sc", []), 1);
|
|
130
|
+
assert.equal(await store.getBlob(sha256("alpha")), undefined);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
125
133
|
await settle();
|
|
126
134
|
}
|