@foldrun/core 0.4.0 → 0.6.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/README.md +7 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/src/account.d.ts +45 -0
- package/dist/src/account.js +130 -0
- package/dist/src/api-tools.d.ts +8 -0
- package/dist/src/api-tools.js +22 -4
- package/dist/src/browser-pod.d.ts +148 -0
- package/dist/src/browser-pod.js +333 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/clock.d.ts +49 -0
- package/dist/src/clock.js +121 -0
- package/dist/src/completions.js +17 -8
- package/dist/src/confine.d.ts +22 -0
- package/dist/src/confine.js +218 -3
- package/dist/src/container.d.ts +9 -0
- package/dist/src/container.js +115 -5
- package/dist/src/context-tools.d.ts +1 -1
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +7 -0
- package/dist/src/egress.js +6 -0
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +5 -0
- package/dist/src/evals.js +9 -2
- package/dist/src/flow-lint.d.ts +8 -0
- package/dist/src/flow-lint.js +223 -1
- package/dist/src/flow-patterns.d.ts +141 -0
- package/dist/src/flow-patterns.js +568 -0
- package/dist/src/gitrepo.d.ts +18 -0
- package/dist/src/gitrepo.js +49 -0
- package/dist/src/host-env.js +5 -0
- package/dist/src/kinds.js +1 -1
- package/dist/src/language.d.ts +17 -0
- package/dist/src/language.js +55 -0
- package/dist/src/layout.d.ts +69 -0
- package/dist/src/layout.js +200 -0
- package/dist/src/library.d.ts +9 -0
- package/dist/src/library.js +30 -3
- package/dist/src/limits.d.ts +84 -0
- package/dist/src/limits.js +287 -0
- package/dist/src/locale.d.ts +47 -0
- package/dist/src/locale.js +177 -0
- package/dist/src/mail.d.ts +88 -0
- package/dist/src/mail.js +247 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +58 -0
- package/dist/src/model-probe.d.ts +3 -0
- package/dist/src/model-probe.js +1 -0
- package/dist/src/notify.d.ts +53 -7
- package/dist/src/notify.js +194 -126
- package/dist/src/openapi.js +5 -3
- package/dist/src/operator.d.ts +201 -0
- package/dist/src/operator.js +251 -0
- package/dist/src/paths.d.ts +28 -0
- package/dist/src/paths.js +55 -0
- package/dist/src/platform.d.ts +43 -3
- package/dist/src/platform.js +9 -1
- package/dist/src/provider-health.js +3 -2
- package/dist/src/providers.d.ts +481 -0
- package/dist/src/providers.js +1105 -6
- package/dist/src/refusal.d.ts +83 -0
- package/dist/src/refusal.js +281 -0
- package/dist/src/run-container.d.ts +149 -13
- package/dist/src/run-container.js +424 -43
- package/dist/src/runner.d.ts +177 -9
- package/dist/src/runner.js +1146 -154
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +17 -2
- package/dist/src/script-tools.js +82 -10
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +30 -6
- package/dist/src/secret-health.js +42 -20
- package/dist/src/secrets.d.ts +19 -0
- package/dist/src/secrets.js +155 -86
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +92 -11
- package/dist/src/step-exec.d.ts +72 -2
- package/dist/src/step-exec.js +431 -37
- package/dist/src/step-removal.d.ts +15 -0
- package/dist/src/step-removal.js +49 -0
- package/dist/src/storage.d.ts +23 -4
- package/dist/src/storage.js +125 -23
- package/dist/src/store.d.ts +222 -37
- package/dist/src/store.js +349 -62
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/test-mode.d.ts +17 -2
- package/dist/src/test-mode.js +53 -13
- package/dist/src/tool-names.d.ts +13 -1
- package/dist/src/tool-names.js +38 -11
- package/dist/src/tool-programs.d.ts +26 -0
- package/dist/src/tool-programs.js +172 -0
- package/dist/src/tool-test.js +104 -12
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +69 -23
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/triggers.d.ts +3 -0
- package/dist/src/triggers.js +10 -2
- package/dist/src/untrusted-fetch.d.ts +11 -0
- package/dist/src/untrusted-fetch.js +77 -0
- package/dist/src/web-actions.d.ts +91 -0
- package/dist/src/web-actions.js +199 -0
- package/dist/src/webhook.d.ts +4 -0
- package/dist/src/webhook.js +39 -0
- package/index.ts +4 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +350 -0
- package/src/catalog.ts +2 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +218 -3
- package/src/container.ts +112 -5
- package/src/context-tools.ts +1 -1
- package/src/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +14 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/host-env.ts +5 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +37 -3
- package/src/limits.ts +290 -0
- package/src/locale.ts +217 -0
- package/src/mail.ts +318 -0
- package/src/model-credential.ts +71 -0
- package/src/model-probe.ts +4 -0
- package/src/notify.ts +189 -103
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +54 -0
- package/src/platform.ts +52 -3
- package/src/provider-health.ts +3 -2
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +460 -44
- package/src/runner.ts +1168 -155
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +81 -10
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +64 -19
- package/src/secrets.ts +149 -87
- package/src/starter.ts +93 -11
- package/src/step-exec.ts +447 -38
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +506 -70
- package/src/subagents.ts +189 -0
- package/src/test-mode.ts +51 -11
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +110 -12
- package/src/translator.ts +72 -22
- package/src/trigger-log.ts +1 -0
- package/src/triggers.ts +10 -2
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +41 -0
- package/templates/blog-desk/agents/writer/agent.md +1 -1
- package/templates/hello/agents/curator/agent.md +4 -4
- package/templates/hello/agents/notetaker/agent.md +3 -3
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Plain JSON: this crosses into the run container. */
|
|
2
|
+
export interface SubagentSpec {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
prompt: string;
|
|
6
|
+
/** Always explicit — never absent, never inherited. */
|
|
7
|
+
tools: string[];
|
|
8
|
+
disallowedTools: string[];
|
|
9
|
+
/** A model alias (haiku/sonnet/opus, remapped by the step's provider like
|
|
10
|
+
* the step's own) or "inherit". */
|
|
11
|
+
model: string;
|
|
12
|
+
}
|
|
13
|
+
/** The SDK's delegation tool, under both names it has had. Never granted to a
|
|
14
|
+
* sub-agent: that is what makes the depth one. */
|
|
15
|
+
export declare const DELEGATE_TOOLS: readonly ["Agent", "Task"];
|
|
16
|
+
export declare const DELEGATE_TOOL = "Agent";
|
|
17
|
+
/** The named sub-agents' folders, host-side. Unknown names are reported. */
|
|
18
|
+
export declare function gatherSubagents(workspaceRoot: string, names: unknown): {
|
|
19
|
+
found: {
|
|
20
|
+
name: string;
|
|
21
|
+
dir: string;
|
|
22
|
+
}[];
|
|
23
|
+
missing: string[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A sub-agent's tools: its own, only where the parent holds them too. An MCP
|
|
27
|
+
* tool is held when the parent holds that exact name or its whole server
|
|
28
|
+
* (`mcp__<server>` / `mcp__<server>__*`). The delegate tool never passes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function subagentTools(own: string[], parent: string[]): string[];
|
|
31
|
+
/** The sub-agent's system prompt: its own prose, told where it stands. It
|
|
32
|
+
* works in the PARENT's directory — the SDK shares the session's cwd. */
|
|
33
|
+
export declare function subagentPrompt(o: {
|
|
34
|
+
body: string;
|
|
35
|
+
name: string;
|
|
36
|
+
parent: string;
|
|
37
|
+
workspace: string;
|
|
38
|
+
shared?: string | null;
|
|
39
|
+
}): string;
|
|
40
|
+
/** SDK definitions. `tools` is always set — an absent list inherits every
|
|
41
|
+
* tool the parent has, which is the one thing this module exists to stop. */
|
|
42
|
+
export declare function toAgentDefinitions(specs: SubagentSpec[]): Record<string, {
|
|
43
|
+
description: string;
|
|
44
|
+
prompt: string;
|
|
45
|
+
tools: string[];
|
|
46
|
+
disallowedTools: string[];
|
|
47
|
+
model: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** One tool call as the parent's PreToolUse hook sees it. */
|
|
50
|
+
export interface GuardedCall {
|
|
51
|
+
/** The SDK's agent_id: set when the call comes from inside a sub-agent. */
|
|
52
|
+
agentId?: string;
|
|
53
|
+
/** The SDK's agent_type: which sub-agent. */
|
|
54
|
+
agentType?: string;
|
|
55
|
+
tool: string;
|
|
56
|
+
input: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* For the parent's PreToolUse hook, on a step that has sub-agents: a reason
|
|
60
|
+
* to refuse the call — or null.
|
|
61
|
+
*
|
|
62
|
+
* Only DECLARED sub-agents may run. The SDK also offers its built-ins
|
|
63
|
+
* (general-purpose and the rest, with every tool the session has); they are
|
|
64
|
+
* switched off in executeStep, and refused here too. A call from inside a
|
|
65
|
+
* sub-agent whose type is missing or not declared used to pass unjudged —
|
|
66
|
+
* `general-purpose` was a way round every sub-agent's own list.
|
|
67
|
+
*/
|
|
68
|
+
export declare function subagentGuard(specs: SubagentSpec[]): (call: GuardedCall) => string | null;
|
|
69
|
+
/**
|
|
70
|
+
* What `foldrun check` can say without running anything: the SDK and built-in
|
|
71
|
+
* tool names two `tools:` lists grant, and their overlap. Own tools compare by
|
|
72
|
+
* name. Used to warn when a sub-agent would be left with nothing.
|
|
73
|
+
*/
|
|
74
|
+
export declare function grantedNames(tools: string[]): Set<string>;
|
|
75
|
+
export declare function checkOverlap(parentTools: string[], subTools: string[]): string[];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Sub-agents: delegate a job to a colleague with its OWN context and its own
|
|
2
|
+
// tools, inside the step that asked.
|
|
3
|
+
//
|
|
4
|
+
// agents: [editor] # consult: a quick opinion, no tools (agent-tools.ts)
|
|
5
|
+
// subagents: [researcher] # delegate: own context, own tools
|
|
6
|
+
//
|
|
7
|
+
// Built on the Agent SDK's native sub-agents (query()'s `agents` option and
|
|
8
|
+
// its Agent tool), so a delegation behaves as it does in Claude Code: a fresh
|
|
9
|
+
// context does the work — reads forty pages, runs the checks — and hands back
|
|
10
|
+
// a summary, keeping the parent's context on its own job.
|
|
11
|
+
//
|
|
12
|
+
// What keeps it the platform's and not a second, wider agent:
|
|
13
|
+
// - never wider than the parent: a sub-agent's tools are its own `tools:`
|
|
14
|
+
// resolved exactly as a step's, INTERSECTED with what the parent step
|
|
15
|
+
// holds — and always an explicit list: the SDK gives a definition with
|
|
16
|
+
// no `tools` every tool the parent has
|
|
17
|
+
// - depth one: the Agent/Task tool is never in a sub-agent's list, so it
|
|
18
|
+
// cannot delegate again
|
|
19
|
+
// - the same step: same sandbox, files, secrets, egress lease, and the
|
|
20
|
+
// parent's hooks — the SDK runs a sub-agent's tool calls through the
|
|
21
|
+
// session's PreToolUse and canUseTool (tagged agent_id/agent_type), so
|
|
22
|
+
// confinement applies unchanged; subagentGuard() below refuses anything
|
|
23
|
+
// outside the sub-agent's own list there too, belt and braces
|
|
24
|
+
// - cost and trace land on the parent step; flows still decide what runs
|
|
25
|
+
// next — a sub-agent is a helper inside one step, never a step.
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { refNames } from "./refs.js";
|
|
29
|
+
import { TOOL_MAP } from "./tool-names.js";
|
|
30
|
+
/** The SDK's delegation tool, under both names it has had. Never granted to a
|
|
31
|
+
* sub-agent: that is what makes the depth one. */
|
|
32
|
+
export const DELEGATE_TOOLS = ["Agent", "Task"];
|
|
33
|
+
export const DELEGATE_TOOL = "Agent";
|
|
34
|
+
const NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
35
|
+
/** The named sub-agents' folders, host-side. Unknown names are reported. */
|
|
36
|
+
export function gatherSubagents(workspaceRoot, names) {
|
|
37
|
+
const found = [];
|
|
38
|
+
const missing = [];
|
|
39
|
+
for (const name of refNames(names)) {
|
|
40
|
+
const dir = path.join(workspaceRoot, "agents", name);
|
|
41
|
+
if (!NAME.test(name) || !fs.existsSync(path.join(dir, "agent.md")))
|
|
42
|
+
missing.push(name);
|
|
43
|
+
else if (!found.some((f) => f.name === name))
|
|
44
|
+
found.push({ name, dir });
|
|
45
|
+
}
|
|
46
|
+
return { found, missing };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A sub-agent's tools: its own, only where the parent holds them too. An MCP
|
|
50
|
+
* tool is held when the parent holds that exact name or its whole server
|
|
51
|
+
* (`mcp__<server>` / `mcp__<server>__*`). The delegate tool never passes.
|
|
52
|
+
*/
|
|
53
|
+
export function subagentTools(own, parent) {
|
|
54
|
+
const held = new Set(parent);
|
|
55
|
+
const servers = new Set(parent.filter((t) => /^mcp__[^_]/.test(t)).map((t) => t.replace(/__\*$/, "")).filter((t) => !t.slice(5).includes("__")));
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const t of own) {
|
|
58
|
+
if (DELEGATE_TOOLS.includes(t) || out.includes(t))
|
|
59
|
+
continue;
|
|
60
|
+
const server = /^mcp__([^_].*?)__/.exec(t)?.[1];
|
|
61
|
+
if (held.has(t) || (server && servers.has(`mcp__${server}`)))
|
|
62
|
+
out.push(t);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/** The sub-agent's system prompt: its own prose, told where it stands. It
|
|
67
|
+
* works in the PARENT's directory — the SDK shares the session's cwd. */
|
|
68
|
+
export function subagentPrompt(o) {
|
|
69
|
+
return [
|
|
70
|
+
o.body.trim(),
|
|
71
|
+
`# Where you are\n\n` +
|
|
72
|
+
`You are ${o.name}, a sub-agent started by ${o.parent} to do one job and report back. ` +
|
|
73
|
+
`You work in ${o.parent}'s directory, \`agents/${o.parent}/\` in the \`${o.workspace}\` workspace: ` +
|
|
74
|
+
`\`workspace/\` is the workspace root, its knowledge is at \`workspace/knowledge/\`, and absolute paths are refused. ` +
|
|
75
|
+
`You have only the tools listed for you. When the job is done, reply with what you found or made — ` +
|
|
76
|
+
`your reply is all ${o.parent} sees of your work, so put the facts and file paths in it.`,
|
|
77
|
+
...(o.shared?.trim() ? [o.shared.trim()] : []),
|
|
78
|
+
].join("\n\n");
|
|
79
|
+
}
|
|
80
|
+
/** SDK definitions. `tools` is always set — an absent list inherits every
|
|
81
|
+
* tool the parent has, which is the one thing this module exists to stop. */
|
|
82
|
+
export function toAgentDefinitions(specs) {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const s of specs) {
|
|
85
|
+
out[s.name] = {
|
|
86
|
+
description: s.description,
|
|
87
|
+
prompt: s.prompt,
|
|
88
|
+
tools: [...s.tools],
|
|
89
|
+
disallowedTools: [...new Set([...s.disallowedTools, ...DELEGATE_TOOLS])],
|
|
90
|
+
model: s.model || "inherit",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* For the parent's PreToolUse hook, on a step that has sub-agents: a reason
|
|
97
|
+
* to refuse the call — or null.
|
|
98
|
+
*
|
|
99
|
+
* Only DECLARED sub-agents may run. The SDK also offers its built-ins
|
|
100
|
+
* (general-purpose and the rest, with every tool the session has); they are
|
|
101
|
+
* switched off in executeStep, and refused here too. A call from inside a
|
|
102
|
+
* sub-agent whose type is missing or not declared used to pass unjudged —
|
|
103
|
+
* `general-purpose` was a way round every sub-agent's own list.
|
|
104
|
+
*/
|
|
105
|
+
export function subagentGuard(specs) {
|
|
106
|
+
const by = new Map(specs.map((s) => [s.name, s]));
|
|
107
|
+
const declared = specs.map((s) => s.name).join(", ");
|
|
108
|
+
const isDelegate = (tool) => DELEGATE_TOOLS.includes(tool);
|
|
109
|
+
return ({ agentId, agentType, tool, input }) => {
|
|
110
|
+
if (!agentId) {
|
|
111
|
+
// The main thread: its tools are canUseTool's business — except the
|
|
112
|
+
// delegation itself, which may name only a declared sub-agent.
|
|
113
|
+
if (!isDelegate(tool))
|
|
114
|
+
return null;
|
|
115
|
+
const to = typeof input.subagent_type === "string" ? input.subagent_type : "";
|
|
116
|
+
if (by.has(to))
|
|
117
|
+
return null;
|
|
118
|
+
return `${to ? `"${to}" is not a sub-agent this step declared` : "name the sub-agent to delegate to"} — subagent_type must be one of: ${declared}`;
|
|
119
|
+
}
|
|
120
|
+
const s = agentType ? by.get(agentType) : undefined;
|
|
121
|
+
if (!s)
|
|
122
|
+
return `${agentType ? `"${agentType}"` : "an unnamed sub-agent"} is not declared in subagents: for this step (only ${declared}) — its calls are refused`;
|
|
123
|
+
if (isDelegate(tool)) {
|
|
124
|
+
return `${s.name} is a sub-agent and cannot start another — do the work with your own tools`;
|
|
125
|
+
}
|
|
126
|
+
if (s.disallowedTools.includes(tool))
|
|
127
|
+
return `${tool} is disallowed for ${s.name}`;
|
|
128
|
+
if (subagentTools([tool], s.tools).length === 0)
|
|
129
|
+
return `${tool} is not one of ${s.name}'s tools`;
|
|
130
|
+
return null;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* What `foldrun check` can say without running anything: the SDK and built-in
|
|
135
|
+
* tool names two `tools:` lists grant, and their overlap. Own tools compare by
|
|
136
|
+
* name. Used to warn when a sub-agent would be left with nothing.
|
|
137
|
+
*/
|
|
138
|
+
export function grantedNames(tools) {
|
|
139
|
+
const out = new Set();
|
|
140
|
+
for (const t of tools) {
|
|
141
|
+
for (const n of TOOL_MAP[t] ?? [t])
|
|
142
|
+
out.add(n);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
export function checkOverlap(parentTools, subTools) {
|
|
147
|
+
const parent = grantedNames(parentTools);
|
|
148
|
+
return [...grantedNames(subTools)].filter((t) => parent.has(t) && !DELEGATE_TOOLS.includes(t));
|
|
149
|
+
}
|
package/dist/src/test-mode.d.ts
CHANGED
|
@@ -103,8 +103,23 @@ export declare function divertedSummary(rel: string, was: Buffer | null, next: B
|
|
|
103
103
|
export declare function snapshotDivertedDirs(wsRoot: string): Map<string, Buffer>;
|
|
104
104
|
/** Undo a step's writes to the diverted directories, keeping each changed
|
|
105
105
|
* or new file under `to` by its workspace-relative path. Deletions are
|
|
106
|
-
* undone too — the original is put back.
|
|
107
|
-
|
|
106
|
+
* undone too — the original is put back. `handed` is what the step was
|
|
107
|
+
* given when that was more than the workspace (the run's earlier test
|
|
108
|
+
* writes, laid over it): only what the step changed from THAT is its
|
|
109
|
+
* write. Returns what was set aside. */
|
|
110
|
+
export declare function restoreDivertedDirs(wsRoot: string, before: Map<string, Buffer>, to: string, note: (rel: string, summary: string) => void, handed?: Map<string, Buffer>): string[];
|
|
111
|
+
/**
|
|
112
|
+
* A test run's later steps see what its earlier steps wrote. Those writes
|
|
113
|
+
* are kept under runs/<id>/test-writes/, never on the workspace, so each
|
|
114
|
+
* step's copy of the workspace gets them laid over it — otherwise a step
|
|
115
|
+
* reads the real files, and a flow that hands work on through storage/
|
|
116
|
+
* tests something no live run does: on 2026-10-03 a strata-desk test run's
|
|
117
|
+
* compiler wrote today's PASS report, and the mailer was handed the
|
|
118
|
+
* morning's FAIL one and refused. Times are kept, so a verify that asks
|
|
119
|
+
* whether a file is fresh answers for the step that wrote it. Returns the
|
|
120
|
+
* workspace-relative paths laid over.
|
|
121
|
+
*/
|
|
122
|
+
export declare function overlayTestWrites(wsCopy: string, from: string): string[];
|
|
108
123
|
/** `[test] ` in front of a headline, once. */
|
|
109
124
|
export declare function testHeadline(summary: string): string;
|
|
110
125
|
/** The env every test-run sandbox gets, beside its secrets. */
|
package/dist/src/test-mode.js
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
// a test run; the env var says TEST_MODE_WITHHELD instead, so a script
|
|
19
19
|
// that forgot to check fails at the provider with an auth error
|
|
20
20
|
// - the write-back: state/ and storage/ go INTO the sandbox as usual and
|
|
21
|
-
// come out into runs/<id>/test-writes/ instead of the real directories
|
|
21
|
+
// come out into runs/<id>/test-writes/ instead of the real directories;
|
|
22
|
+
// the next step's copy has those writes laid over it (overlayTestWrites),
|
|
23
|
+
// so the run reads its own work and the workspace never sees it
|
|
22
24
|
//
|
|
23
25
|
// Nothing here weakens a live run: every function is a no-op unless the run
|
|
24
26
|
// says `test: true`.
|
|
@@ -247,21 +249,27 @@ export function snapshotDivertedDirs(wsRoot) {
|
|
|
247
249
|
}
|
|
248
250
|
/** Undo a step's writes to the diverted directories, keeping each changed
|
|
249
251
|
* or new file under `to` by its workspace-relative path. Deletions are
|
|
250
|
-
* undone too — the original is put back.
|
|
251
|
-
|
|
252
|
+
* undone too — the original is put back. `handed` is what the step was
|
|
253
|
+
* given when that was more than the workspace (the run's earlier test
|
|
254
|
+
* writes, laid over it): only what the step changed from THAT is its
|
|
255
|
+
* write. Returns what was set aside. */
|
|
256
|
+
export function restoreDivertedDirs(wsRoot, before, to, note, handed = before) {
|
|
252
257
|
const now = snapshotDivertedDirs(wsRoot);
|
|
253
258
|
const aside = [];
|
|
254
259
|
for (const [rel, next] of now) {
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
const given = handed.get(rel) ?? null;
|
|
261
|
+
if (!(given && given.equals(next))) {
|
|
262
|
+
const dest = path.join(to, rel);
|
|
263
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
264
|
+
fs.writeFileSync(dest, next);
|
|
265
|
+
aside.push(rel);
|
|
266
|
+
note(rel, divertedSummary(rel, before.get(rel) ?? null, next));
|
|
267
|
+
}
|
|
268
|
+
const orig = before.get(rel);
|
|
269
|
+
if (orig) {
|
|
270
|
+
if (!orig.equals(next))
|
|
271
|
+
fs.writeFileSync(path.join(wsRoot, rel), orig);
|
|
272
|
+
}
|
|
265
273
|
else
|
|
266
274
|
fs.rmSync(path.join(wsRoot, rel), { force: true });
|
|
267
275
|
}
|
|
@@ -275,6 +283,38 @@ export function restoreDivertedDirs(wsRoot, before, to, note) {
|
|
|
275
283
|
}
|
|
276
284
|
return aside;
|
|
277
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* A test run's later steps see what its earlier steps wrote. Those writes
|
|
288
|
+
* are kept under runs/<id>/test-writes/, never on the workspace, so each
|
|
289
|
+
* step's copy of the workspace gets them laid over it — otherwise a step
|
|
290
|
+
* reads the real files, and a flow that hands work on through storage/
|
|
291
|
+
* tests something no live run does: on 2026-10-03 a strata-desk test run's
|
|
292
|
+
* compiler wrote today's PASS report, and the mailer was handed the
|
|
293
|
+
* morning's FAIL one and refused. Times are kept, so a verify that asks
|
|
294
|
+
* whether a file is fresh answers for the step that wrote it. Returns the
|
|
295
|
+
* workspace-relative paths laid over.
|
|
296
|
+
*/
|
|
297
|
+
export function overlayTestWrites(wsCopy, from) {
|
|
298
|
+
const laid = [];
|
|
299
|
+
for (const dir of TEST_DIVERTED_DIRS) {
|
|
300
|
+
const root = path.join(from, dir);
|
|
301
|
+
if (!fs.existsSync(root))
|
|
302
|
+
continue;
|
|
303
|
+
for (const entry of fs.readdirSync(root, { recursive: true, withFileTypes: true })) {
|
|
304
|
+
if (!entry.isFile())
|
|
305
|
+
continue;
|
|
306
|
+
const abs = path.join(entry.parentPath, entry.name);
|
|
307
|
+
const rel = path.relative(from, abs).replaceAll("\\", "/");
|
|
308
|
+
const dest = path.join(wsCopy, rel);
|
|
309
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
310
|
+
fs.copyFileSync(abs, dest);
|
|
311
|
+
const st = fs.statSync(abs);
|
|
312
|
+
fs.utimesSync(dest, st.atime, st.mtime);
|
|
313
|
+
laid.push(rel);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return laid;
|
|
317
|
+
}
|
|
278
318
|
/** `[test] ` in front of a headline, once. */
|
|
279
319
|
export function testHeadline(summary) {
|
|
280
320
|
return summary.startsWith("[test]") ? summary : `[test] ${summary}`;
|
package/dist/src/tool-names.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { type Ref } from "./refs.ts";
|
|
2
2
|
/** Groups: one word an author can hold in their head, expanded to SDK names. */
|
|
3
3
|
export declare const TOOL_MAP: Record<string, string[]>;
|
|
4
|
+
/** Names an author may no longer write, and what to write instead. They are
|
|
5
|
+
* still granted, so nothing deployed breaks; `check` and the run log say the
|
|
6
|
+
* rewrite. */
|
|
7
|
+
export declare const RETIRED_TOOLS: Record<string, string>;
|
|
4
8
|
/** Exact SDK tool names, accepted alongside the group aliases so a Claude Code
|
|
5
9
|
* subagent's `tools: Read, Grep` works unchanged. The aliases exist because
|
|
6
|
-
* vendors rename tools; `
|
|
10
|
+
* vendors rename tools; `write` survives a rename that `Write` would not.
|
|
11
|
+
* The SDK's WebSearch and WebFetch are not here: the web is our `web` tool,
|
|
12
|
+
* and a model provider's own search (`web: {search: zai}`) is the runtime's
|
|
13
|
+
* to swap in, not the author's to name. */
|
|
7
14
|
export declare const BUILTIN_TOOLS: Set<string>;
|
|
8
15
|
/** The platform's own groups, served in-process rather than by the SDK. */
|
|
9
16
|
export declare const PLATFORM_GROUPS: Set<string>;
|
|
@@ -34,6 +41,11 @@ export declare function ownToolNames(front: ToolFrontmatter): string[];
|
|
|
34
41
|
/** What a file still says under the removed `use:` key — nothing is granted
|
|
35
42
|
* for these; they exist so the error can quote the exact `tools:` line. */
|
|
36
43
|
export declare function legacyUseNames(front: ToolFrontmatter): string[];
|
|
44
|
+
/** Retired names written bare in `tools:`, deduped. A `[[link]]` is the
|
|
45
|
+
* author's own file, so it is never one of these. */
|
|
46
|
+
export declare function retiredToolNames(front: ToolFrontmatter): string[];
|
|
47
|
+
/** The one sentence every reader of a retired name says. */
|
|
48
|
+
export declare function retiredToolError(name: string): string;
|
|
37
49
|
/** The one sentence every reader of a `use:` key says. */
|
|
38
50
|
export declare function legacyUseError(names: string[]): string;
|
|
39
51
|
export {};
|
package/dist/src/tool-names.js
CHANGED
|
@@ -12,27 +12,40 @@
|
|
|
12
12
|
import { refList, refNames } from "./refs.js";
|
|
13
13
|
/** Groups: one word an author can hold in their head, expanded to SDK names. */
|
|
14
14
|
export const TOOL_MAP = {
|
|
15
|
-
// `web`
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
15
|
+
// The web is one tool of ours, `web` — search, fetch, browse, crawl, map,
|
|
16
|
+
// extract, answer, monitor, served from the gallery and on the run record
|
|
17
|
+
// whichever model is driving. So `web` is deliberately NOT here: a name in
|
|
18
|
+
// TOOL_MAP shadows a real tool of the same name (see runner.ts).
|
|
19
|
+
// `read` is deliberately separate from `write`: an agent that may inspect a
|
|
20
|
+
// repository but must never modify it is a real and common design. The pair
|
|
21
|
+
// says what it grants. `files` and `bash` were the old names for `write`
|
|
22
|
+
// and `code` (`code`, because it is not only shell — anything in the
|
|
23
|
+
// sandbox runs through it); both are retired, below.
|
|
23
24
|
read: ["Read", "Glob", "Grep"],
|
|
25
|
+
write: ["Read", "Write", "Edit", "Glob", "Grep"],
|
|
24
26
|
files: ["Read", "Write", "Edit", "Glob", "Grep"],
|
|
27
|
+
code: ["Bash"],
|
|
25
28
|
bash: ["Bash"],
|
|
26
29
|
};
|
|
30
|
+
/** Names an author may no longer write, and what to write instead. They are
|
|
31
|
+
* still granted, so nothing deployed breaks; `check` and the run log say the
|
|
32
|
+
* rewrite. */
|
|
33
|
+
export const RETIRED_TOOLS = {
|
|
34
|
+
files: "write",
|
|
35
|
+
bash: "code",
|
|
36
|
+
};
|
|
27
37
|
/** Exact SDK tool names, accepted alongside the group aliases so a Claude Code
|
|
28
38
|
* subagent's `tools: Read, Grep` works unchanged. The aliases exist because
|
|
29
|
-
* vendors rename tools; `
|
|
39
|
+
* vendors rename tools; `write` survives a rename that `Write` would not.
|
|
40
|
+
* The SDK's WebSearch and WebFetch are not here: the web is our `web` tool,
|
|
41
|
+
* and a model provider's own search (`web: {search: zai}`) is the runtime's
|
|
42
|
+
* to swap in, not the author's to name. */
|
|
30
43
|
export const BUILTIN_TOOLS = new Set([
|
|
31
44
|
"Read", "Write", "Edit", "MultiEdit", "Glob", "Grep", "Bash",
|
|
32
|
-
"
|
|
45
|
+
"NotebookEdit", "TodoWrite",
|
|
33
46
|
]);
|
|
34
47
|
/** The platform's own groups, served in-process rather than by the SDK. */
|
|
35
|
-
export const PLATFORM_GROUPS = new Set(["search", "history", "desks"]);
|
|
48
|
+
export const PLATFORM_GROUPS = new Set(["search", "history", "desks", "ask"]);
|
|
36
49
|
/** Would this name resolve to something the runtime provides? Built-ins win a
|
|
37
50
|
* clash, so a tool file of the same name is shadowed rather than granted. */
|
|
38
51
|
export function isRuntimeTool(name) {
|
|
@@ -73,6 +86,20 @@ export function ownToolNames(front) {
|
|
|
73
86
|
export function legacyUseNames(front) {
|
|
74
87
|
return refNames(front.use);
|
|
75
88
|
}
|
|
89
|
+
/** Retired names written bare in `tools:`, deduped. A `[[link]]` is the
|
|
90
|
+
* author's own file, so it is never one of these. */
|
|
91
|
+
export function retiredToolNames(front) {
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const ref of toolRefs(front)) {
|
|
94
|
+
if (!ref.linked && RETIRED_TOOLS[ref.name] && !out.includes(ref.name))
|
|
95
|
+
out.push(ref.name);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/** The one sentence every reader of a retired name says. */
|
|
100
|
+
export function retiredToolError(name) {
|
|
101
|
+
return `tools: "${name}" is retired — use ${RETIRED_TOOLS[name]} instead: the same grant, under the name that says what it does. It is still granted for now.`;
|
|
102
|
+
}
|
|
76
103
|
/** The one sentence every reader of a `use:` key says. */
|
|
77
104
|
export function legacyUseError(names) {
|
|
78
105
|
return `\`use:\` is no longer read — write \`tools: [${names.join(", ")}]\` instead (scripts/migrate-use-to-tools.mjs rewrites every agent). Nothing was granted for it.`;
|
|
@@ -16,3 +16,29 @@ export interface MissingProgram {
|
|
|
16
16
|
* call a working account tool broken.
|
|
17
17
|
*/
|
|
18
18
|
export declare function missingToolPrograms(tenant: string, workspace: string): MissingProgram[];
|
|
19
|
+
/** The names `import` would use for a declared distribution. */
|
|
20
|
+
export declare function importNamesOf(requirement: string): string[];
|
|
21
|
+
/** Top-level modules a Python file imports; relative imports excluded. */
|
|
22
|
+
export declare function pythonImports(source: string): string[];
|
|
23
|
+
/** Packages a JavaScript file imports or requires; relative and node: excluded. */
|
|
24
|
+
export declare function nodeImports(source: string): string[];
|
|
25
|
+
export interface UndeclaredImport {
|
|
26
|
+
name: string;
|
|
27
|
+
scope: "workspace" | "account";
|
|
28
|
+
file: string;
|
|
29
|
+
modules: string[];
|
|
30
|
+
language: "python" | "node";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Script tools whose program imports a package nothing declares. Advisory: a
|
|
34
|
+
* warning, because the import may be satisfied some way this cannot see (a
|
|
35
|
+
* vendored module in a subfolder, a package the agent's own `runtime:`
|
|
36
|
+
* brings). What it catches is the common case — a tool that `import openpyxl`
|
|
37
|
+
* with no `runtime:` anywhere, which runs fine on the laptop that has it and
|
|
38
|
+
* fails on the platform on its first call.
|
|
39
|
+
*
|
|
40
|
+
* Declared means: the tool's own runtime (tool.md, requirements.txt,
|
|
41
|
+
* package.json), the workspace's AGENTS.md runtime, or any agent's runtime
|
|
42
|
+
* in the workspace — the union, so an agent-level declaration is trusted.
|
|
43
|
+
*/
|
|
44
|
+
export declare function undeclaredImports(tenant: string, workspace: string): UndeclaredImport[];
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// runner — passing a tool that cannot run, or failing one that can.
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
|
+
import { builtinModules } from "node:module";
|
|
18
|
+
import matter from "gray-matter";
|
|
19
|
+
import { parseRuntime } from "./runtime.js";
|
|
17
20
|
import { workspaceDir, workspaceTools } from "./store.js";
|
|
18
21
|
import { libraryDir, libraryTools } from "./library.js";
|
|
19
22
|
import { resolveRunPath } from "./script-tools.js";
|
|
@@ -50,3 +53,172 @@ export function missingToolPrograms(tenant, workspace) {
|
|
|
50
53
|
scan(workspaceTools(tenant, workspace), "workspace");
|
|
51
54
|
return out;
|
|
52
55
|
}
|
|
56
|
+
// ---------------------------------------------------------------- imports
|
|
57
|
+
/**
|
|
58
|
+
* The Python standard library, top-level names: CPython's
|
|
59
|
+
* sys.stdlib_module_names, plus what 3.13 removed (the image's python is
|
|
60
|
+
* older than the list's). Never a declaration's business.
|
|
61
|
+
*/
|
|
62
|
+
const PY_STDLIB = new Set(`
|
|
63
|
+
abc aifc annotationlib antigravity argparse array ast asynchat asyncio
|
|
64
|
+
asyncore atexit audioop base64 bdb binascii bisect builtins bz2 cProfile
|
|
65
|
+
calendar cgi cgitb chunk cmath cmd code codecs codeop collections colorsys
|
|
66
|
+
compileall compression concurrent configparser contextlib contextvars copy
|
|
67
|
+
copyreg crypt csv ctypes curses dataclasses datetime dbm decimal difflib
|
|
68
|
+
dis distutils doctest email encodings ensurepip enum errno faulthandler
|
|
69
|
+
fcntl filecmp fileinput fnmatch fractions ftplib functools gc genericpath
|
|
70
|
+
getopt getpass gettext glob graphlib grp gzip hashlib heapq hmac html http
|
|
71
|
+
idlelib imaplib imghdr imp importlib inspect io ipaddress itertools json
|
|
72
|
+
keyword lib2to3 linecache locale logging lzma mailbox mailcap marshal math
|
|
73
|
+
mimetypes mmap modulefinder msilib msvcrt multiprocessing netrc nis nntplib
|
|
74
|
+
nt ntpath nturl2path numbers opcode operator optparse os ossaudiodev
|
|
75
|
+
pathlib pdb pickle pickletools pipes pkgutil platform plistlib poplib posix
|
|
76
|
+
posixpath pprint profile pstats pty pwd py_compile pyclbr pydoc pydoc_data
|
|
77
|
+
pyexpat queue quopri random re readline reprlib resource rlcompleter runpy
|
|
78
|
+
sched secrets select selectors shelve shlex shutil signal site smtpd
|
|
79
|
+
smtplib sndhdr socket socketserver spwd sqlite3 sre_compile sre_constants
|
|
80
|
+
sre_parse ssl stat statistics string stringprep struct subprocess sunau
|
|
81
|
+
symtable sys sysconfig syslog tabnanny tarfile telnetlib tempfile termios
|
|
82
|
+
textwrap this threading time timeit tkinter token tokenize tomllib trace
|
|
83
|
+
traceback tracemalloc tty turtle turtledemo types typing unicodedata
|
|
84
|
+
unittest urllib uu uuid venv warnings wave weakref webbrowser winreg
|
|
85
|
+
winsound wsgiref xdrlib xml xmlrpc zipapp zipfile zipimport zlib zoneinfo
|
|
86
|
+
`
|
|
87
|
+
.trim()
|
|
88
|
+
.split(/\s+/));
|
|
89
|
+
/** Distributions whose import name is not their own name, lower-cased. */
|
|
90
|
+
const PY_IMPORT_ALIASES = {
|
|
91
|
+
beautifulsoup4: ["bs4"],
|
|
92
|
+
pillow: ["PIL"],
|
|
93
|
+
pyyaml: ["yaml"],
|
|
94
|
+
"python-dateutil": ["dateutil"],
|
|
95
|
+
"scikit-learn": ["sklearn"],
|
|
96
|
+
"opencv-python": ["cv2"],
|
|
97
|
+
"opencv-python-headless": ["cv2"],
|
|
98
|
+
"python-docx": ["docx"],
|
|
99
|
+
"python-pptx": ["pptx"],
|
|
100
|
+
pymupdf: ["fitz", "pymupdf"],
|
|
101
|
+
"google-api-python-client": ["googleapiclient"],
|
|
102
|
+
"google-auth": ["google"],
|
|
103
|
+
protobuf: ["google"],
|
|
104
|
+
"psycopg2-binary": ["psycopg2"],
|
|
105
|
+
"psycopg-binary": ["psycopg"],
|
|
106
|
+
attrs: ["attr", "attrs"],
|
|
107
|
+
"python-dotenv": ["dotenv"],
|
|
108
|
+
pyjwt: ["jwt"],
|
|
109
|
+
"typing-extensions": ["typing_extensions"],
|
|
110
|
+
};
|
|
111
|
+
/** The names `import` would use for a declared distribution. */
|
|
112
|
+
export function importNamesOf(requirement) {
|
|
113
|
+
const dist = requirement.split(/[[<>=!~@ ;]/)[0].trim().toLowerCase();
|
|
114
|
+
return PY_IMPORT_ALIASES[dist] ?? [dist.replace(/[-.]/g, "_")];
|
|
115
|
+
}
|
|
116
|
+
/** Top-level modules a Python file imports; relative imports excluded. */
|
|
117
|
+
export function pythonImports(source) {
|
|
118
|
+
const out = new Set();
|
|
119
|
+
for (const line of source.split("\n")) {
|
|
120
|
+
const from = /^\s*from\s+([A-Za-z_][\w.]*)\s+import\b/.exec(line);
|
|
121
|
+
if (from)
|
|
122
|
+
out.add(from[1].split(".")[0]);
|
|
123
|
+
const imp = /^\s*import\s+([A-Za-z_][\w.]*(?:\s+as\s+\w+)?(?:\s*,\s*[A-Za-z_][\w.]*(?:\s+as\s+\w+)?)*)/.exec(line);
|
|
124
|
+
if (imp)
|
|
125
|
+
for (const part of imp[1].split(","))
|
|
126
|
+
out.add(part.trim().split(/\s+/)[0].split(".")[0]);
|
|
127
|
+
}
|
|
128
|
+
return [...out];
|
|
129
|
+
}
|
|
130
|
+
/** Packages a JavaScript file imports or requires; relative and node: excluded. */
|
|
131
|
+
export function nodeImports(source) {
|
|
132
|
+
const out = new Set();
|
|
133
|
+
const re = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)["']([^"'./][^"']*)["']/gm;
|
|
134
|
+
for (const m of source.matchAll(re)) {
|
|
135
|
+
const spec = m[1];
|
|
136
|
+
if (spec.startsWith("node:"))
|
|
137
|
+
continue;
|
|
138
|
+
out.add(spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0]);
|
|
139
|
+
}
|
|
140
|
+
return [...out];
|
|
141
|
+
}
|
|
142
|
+
/** What the runner image provides without a declaration (run-container.ts). */
|
|
143
|
+
const IMAGE_NODE_MODULES = new Set(["playwright", "playwright-core", "axe-core", "@foldrun/core"]);
|
|
144
|
+
/**
|
|
145
|
+
* Script tools whose program imports a package nothing declares. Advisory: a
|
|
146
|
+
* warning, because the import may be satisfied some way this cannot see (a
|
|
147
|
+
* vendored module in a subfolder, a package the agent's own `runtime:`
|
|
148
|
+
* brings). What it catches is the common case — a tool that `import openpyxl`
|
|
149
|
+
* with no `runtime:` anywhere, which runs fine on the laptop that has it and
|
|
150
|
+
* fails on the platform on its first call.
|
|
151
|
+
*
|
|
152
|
+
* Declared means: the tool's own runtime (tool.md, requirements.txt,
|
|
153
|
+
* package.json), the workspace's AGENTS.md runtime, or any agent's runtime
|
|
154
|
+
* in the workspace — the union, so an agent-level declaration is trusted.
|
|
155
|
+
*/
|
|
156
|
+
export function undeclaredImports(tenant, workspace) {
|
|
157
|
+
const dir = workspaceDir(tenant, workspace);
|
|
158
|
+
const from = path.join(dir, "agents", "_probe");
|
|
159
|
+
const libScripts = libraryDir(tenant, "scripts");
|
|
160
|
+
const sharedPy = new Set();
|
|
161
|
+
const sharedNpm = new Set();
|
|
162
|
+
const addRuntime = (raw) => {
|
|
163
|
+
const rt = parseRuntime(raw);
|
|
164
|
+
if (!rt)
|
|
165
|
+
return;
|
|
166
|
+
for (const p of rt.packages)
|
|
167
|
+
for (const n of importNamesOf(p))
|
|
168
|
+
sharedPy.add(n.toLowerCase());
|
|
169
|
+
for (const n of rt.npm)
|
|
170
|
+
sharedNpm.add(n.replace(/^(@[^/]+\/[^@]+|[^@]+).*$/, "$1"));
|
|
171
|
+
};
|
|
172
|
+
const readFront = (file) => {
|
|
173
|
+
try {
|
|
174
|
+
return matter(fs.readFileSync(file, "utf8")).data;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
addRuntime(readFront(path.join(dir, "AGENTS.md")).runtime);
|
|
181
|
+
addRuntime(readFront(path.join(dir, "project.md")).runtime);
|
|
182
|
+
const agentsRoot = path.join(dir, "agents");
|
|
183
|
+
if (fs.existsSync(agentsRoot)) {
|
|
184
|
+
for (const a of fs.readdirSync(agentsRoot))
|
|
185
|
+
addRuntime(readFront(path.join(agentsRoot, a, "agent.md")).runtime);
|
|
186
|
+
}
|
|
187
|
+
const out = [];
|
|
188
|
+
const scan = (defs, scope) => {
|
|
189
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
190
|
+
if (def.kind !== "script")
|
|
191
|
+
continue;
|
|
192
|
+
const run = String(def.spec.run ?? "");
|
|
193
|
+
if (!run)
|
|
194
|
+
continue;
|
|
195
|
+
const file = resolveRunPath(from, run, libScripts);
|
|
196
|
+
if (!fs.existsSync(file))
|
|
197
|
+
continue;
|
|
198
|
+
const own = parseRuntime(def.spec.runtime);
|
|
199
|
+
const ext = path.extname(file).toLowerCase();
|
|
200
|
+
const source = fs.readFileSync(file, "utf8");
|
|
201
|
+
const siblings = new Set(fs.readdirSync(path.dirname(file)).map((f) => f.replace(/\.(py|mjs|cjs|js|ts)$/, "").toLowerCase()));
|
|
202
|
+
if (ext === ".py" || (!ext && /^#!.*python/.test(source))) {
|
|
203
|
+
const declared = new Set(sharedPy);
|
|
204
|
+
for (const p of own?.packages ?? [])
|
|
205
|
+
for (const n of importNamesOf(p))
|
|
206
|
+
declared.add(n.toLowerCase());
|
|
207
|
+
const missing = pythonImports(source).filter((m) => !PY_STDLIB.has(m) && !declared.has(m.toLowerCase()) && !siblings.has(m.toLowerCase()));
|
|
208
|
+
if (missing.length)
|
|
209
|
+
out.push({ name, scope, file: run, modules: missing, language: "python" });
|
|
210
|
+
}
|
|
211
|
+
else if ([".js", ".mjs", ".cjs", ".ts"].includes(ext)) {
|
|
212
|
+
const declared = new Set(sharedNpm);
|
|
213
|
+
for (const n of own?.npm ?? [])
|
|
214
|
+
declared.add(n.replace(/^(@[^/]+\/[^@]+|[^@]+).*$/, "$1"));
|
|
215
|
+
const missing = nodeImports(source).filter((m) => !builtinModules.includes(m) && !declared.has(m) && !IMAGE_NODE_MODULES.has(m));
|
|
216
|
+
if (missing.length)
|
|
217
|
+
out.push({ name, scope, file: run, modules: missing, language: "node" });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
scan(libraryTools(tenant), "account");
|
|
222
|
+
scan(workspaceTools(tenant, workspace), "workspace");
|
|
223
|
+
return out;
|
|
224
|
+
}
|