@easynet/agent-runtime 1.0.4 → 1.0.5

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.
@@ -26,8 +26,6 @@ jobs:
26
26
  uses: actions/setup-node@v4
27
27
  with:
28
28
  node-version: '20'
29
- cache: 'npm'
30
- registry-url: 'https://registry.npmjs.org'
31
29
 
32
30
  - name: Force npm registry to npmjs.org
33
31
  run: |
@@ -47,6 +45,7 @@ jobs:
47
45
  NPM_CONFIG_REGISTRY: "https://registry.npmjs.org/"
48
46
  run: |
49
47
  rm -f package-lock.json
48
+ unset NPM_CONFIG_USERCONFIG NODE_AUTH_TOKEN
50
49
  npm install --legacy-peer-deps --ignore-scripts
51
50
 
52
51
  - name: Build
@@ -32,8 +32,6 @@ jobs:
32
32
  uses: actions/setup-node@v4
33
33
  with:
34
34
  node-version: '20'
35
- cache: 'npm'
36
- registry-url: 'https://registry.npmjs.org'
37
35
 
38
36
  - name: Force npm registry to npmjs.org
39
37
  run: |
@@ -42,40 +40,19 @@ jobs:
42
40
  grep -q '@easynet:registry=' .npmrc || echo "@easynet:registry=https://registry.npmjs.org/" >> .npmrc
43
41
  grep -q '@wallee:registry=' .npmrc || echo "@wallee:registry=https://registry.npmjs.org/" >> .npmrc
44
42
 
45
- - name: Use @easynet deps from npm (CI has no file:../)
43
+ - name: Rewrite file deps to npm/git refs for CI
46
44
  env:
47
45
  AGENT_SKILL_READ_TOKEN: ${{ secrets.AGENT_SKILL_READ_TOKEN }}
48
46
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
- run: |
50
- node -e "
51
- const fs = require('fs');
52
- const pkgPath = 'package.json';
53
- const p = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
54
- const deps = { ...p.dependencies, ...p.devDependencies };
55
- const token = process.env.AGENT_SKILL_READ_TOKEN || process.env.GITHUB_TOKEN || '';
56
- if (!token) {
57
- console.error('Missing AGENT_SKILL_READ_TOKEN (or GITHUB_TOKEN) for @easynet/agent-skill private dependency.');
58
- process.exit(1);
59
- }
60
- let changed = false;
61
- for (const [name, v] of Object.entries(deps)) {
62
- if (name.startsWith('@easynet/') && typeof v === 'string' && v.startsWith('file:')) {
63
- const resolved = name === '@easynet/agent-skill'
64
- ? ('git+https://x-access-token:' + token + '@github.com/easynet-world/agent-skill.git#master')
65
- : 'latest';
66
- if (p.dependencies[name]) { p.dependencies[name] = resolved; changed = true; }
67
- if (p.devDependencies[name]) { p.devDependencies[name] = resolved; changed = true; }
68
- }
69
- }
70
- if (changed) fs.writeFileSync(pkgPath, JSON.stringify(p, null, 2) + '\n');
71
- "
72
- - name: Remove lockfile to avoid stale ssh git refs
73
- run: rm -f package-lock.json
47
+ run: node scripts/resolve-deps.js
74
48
 
75
49
  - name: Install dependencies
76
50
  env:
77
51
  NPM_CONFIG_REGISTRY: "https://registry.npmjs.org/"
78
- run: npm install --legacy-peer-deps
52
+ run: |
53
+ rm -f package-lock.json
54
+ unset NPM_CONFIG_USERCONFIG NODE_AUTH_TOKEN
55
+ npm install --legacy-peer-deps --ignore-scripts
79
56
 
80
57
  - name: Build
81
58
  run: npm run build --if-present
@@ -0,0 +1,76 @@
1
+ import { asObject, resolveKindResourceFile } from "@easynet/agent-common";
2
+ import {
3
+ createRuntimeConfig,
4
+ getModelsConfigPath as getRuntimeModelsConfigPath,
5
+ getMemoryConfigPath as getRuntimeMemoryConfigPath,
6
+ getToolConfigPath as getRuntimeToolConfigPath,
7
+ resolveDefaultAgentName as resolveRuntimeDefaultAgentName,
8
+ type AgentProfileConfig,
9
+ } from "@easynet/agent-runtime";
10
+
11
+ type IMessageAppSpec = {
12
+ agent?: string;
13
+ };
14
+
15
+ export interface AppConfig {
16
+ app?: {
17
+ agent?: Record<string, AgentProfileConfig>;
18
+ defaultAgent?: string;
19
+ };
20
+ }
21
+
22
+ export interface AppConfigDefaults {
23
+ modelsPath?: string;
24
+ memoryPath?: string;
25
+ toolPath?: string;
26
+ toolDevPath?: string;
27
+ toolProdPath?: string;
28
+ }
29
+
30
+ export async function loadAppConfig(configPath?: string): Promise<AppConfig> {
31
+ const appResource = await resolveKindResourceFile<IMessageAppSpec>(configPath ?? "config/app.yaml", {
32
+ baseDir: process.cwd(),
33
+ expectedApiVersion: "easynet.world/v1",
34
+ expectedKind: "AppConfig",
35
+ });
36
+ const spec = asObject(appResource.spec) as IMessageAppSpec | undefined;
37
+ const runtimeConfig = await createRuntimeConfig({
38
+ configPath,
39
+ overrides: {
40
+ app: {
41
+ defaultAgent: spec?.agent,
42
+ },
43
+ },
44
+ });
45
+ const defaultAgent = resolveRuntimeDefaultAgentName(runtimeConfig, spec?.agent);
46
+ return {
47
+ app: {
48
+ agent: runtimeConfig.app?.agent,
49
+ defaultAgent,
50
+ },
51
+ };
52
+ }
53
+
54
+ export function getModelsConfigPath(
55
+ config: AppConfig,
56
+ agentName?: string,
57
+ defaults?: AppConfigDefaults,
58
+ ): string {
59
+ return getRuntimeModelsConfigPath(config, agentName, defaults);
60
+ }
61
+
62
+ export function getMemoryConfigPath(
63
+ config: AppConfig,
64
+ agentName?: string,
65
+ defaults?: AppConfigDefaults,
66
+ ): string {
67
+ return getRuntimeMemoryConfigPath(config, agentName, defaults);
68
+ }
69
+
70
+ export function getToolConfigPath(
71
+ config: AppConfig,
72
+ agentName?: string,
73
+ defaults?: AppConfigDefaults,
74
+ ): string {
75
+ return getRuntimeToolConfigPath(config, agentName, defaults);
76
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared runtime context: LLM, memory, tools.
3
+ * Built once and passed to both ReAct and Deep agents.
4
+ */
5
+ import {
6
+ createContextBuilders,
7
+ type AppContextBuilders,
8
+ type BotContext,
9
+ type BotTool,
10
+ type CreateContextOptions,
11
+ } from "@easynet/agent-runtime";
12
+ import {
13
+ loadAppConfig,
14
+ getModelsConfigPath,
15
+ getMemoryConfigPath,
16
+ getToolConfigPath,
17
+ type AppConfig,
18
+ } from "./config.js";
19
+
20
+ export type { BotContext, BotTool, CreateContextOptions };
21
+
22
+ const builders: AppContextBuilders = createContextBuilders<AppConfig>({
23
+ configApi: {
24
+ loadAgentConfig: loadAppConfig,
25
+ getModelsConfigPath: (config: AppConfig, agentName?: string) => getModelsConfigPath(config, agentName),
26
+ getMemoryConfigPath: (config: AppConfig, agentName?: string) => getMemoryConfigPath(config, agentName),
27
+ getToolConfigPath: (config: AppConfig, agentName?: string) => getToolConfigPath(config, agentName),
28
+ },
29
+ });
30
+
31
+ export const getAgentLlm: AppContextBuilders["createAgentLlm"] = builders.createAgentLlm;
32
+ export const getAgentMemory: AppContextBuilders["createAgentMemory"] = builders.createAgentMemory;
33
+ export const getAgentTools: AppContextBuilders["createAgentTools"] = builders.createAgentTools;
34
+
35
+ export const createAgentLlm: AppContextBuilders["createAgentLlm"] = builders.createAgentLlm;
36
+ export const createAgentMemory: AppContextBuilders["createAgentMemory"] = builders.createAgentMemory;
37
+ export const createAgentTools: AppContextBuilders["createAgentTools"] = builders.createAgentTools;
38
+ export const createAgent: AppContextBuilders["createAgent"] = builders.createAgent;
39
+ export const createBotContext: AppContextBuilders["createBotContext"] = builders.createBotContext;
@@ -0,0 +1,19 @@
1
+ apiVersion: easynet.world/v1
2
+ kind: ToolConfig
3
+ metadata:
4
+ name: itermbot-local-tool-config
5
+ spec:
6
+ tools:
7
+ list:
8
+ - file:../../../agent-tool-buildin#itermCreateWindow
9
+ - file:../../../agent-tool-buildin#itermCreateTab
10
+ - file:../../../agent-tool-buildin#itermSplitPane
11
+ - file:../../../agent-tool-buildin#itermResizeWindow
12
+ - file:../../../agent-tool-buildin#itermRename
13
+ - file:../../../agent-tool-buildin#itermSetBackgroundColor
14
+ - file:../../../agent-tool-buildin#itermSetSessionColors
15
+ - file:../../../agent-tool-buildin#itermSendText
16
+ - file:../../../agent-tool-buildin#itermRunCommandInSession
17
+ - file:../../../agent-tool-buildin#itermListCurrentWindowSessions
18
+ - file:../../../agent-tool-buildin#itermGetSessionInfo
19
+ - file:../../../agent-tool-buildin#itermListWindows
@@ -0,0 +1,117 @@
1
+ import { asObject, resolveKindResourceFile } from "@easynet/agent-common";
2
+ import {
3
+ createRuntimeConfig,
4
+ getModelsConfigPath as getRuntimeModelsConfigPath,
5
+ getMemoryConfigPath as getRuntimeMemoryConfigPath,
6
+ getToolConfigPath as getRuntimeToolConfigPath,
7
+ resolveDefaultAgentName as resolveRuntimeDefaultAgentName,
8
+ type AgentRuntimeConfig,
9
+ } from "@easynet/agent-runtime";
10
+
11
+ type PromptTemplates = {
12
+ itermPolicy?: string;
13
+ targetSession?: string;
14
+ };
15
+
16
+ type ItermAppSpec = {
17
+ agent?: string;
18
+ printSteps?: boolean;
19
+ fallbackText?: string;
20
+ commandWindowLabel?: string;
21
+ toolConfigPath?: string;
22
+ promptTemplates?: PromptTemplates;
23
+ };
24
+
25
+ export type AppConfig = AgentRuntimeConfig & {
26
+ app?: (AgentRuntimeConfig["app"] & {
27
+ defaultAgent?: string;
28
+ printSteps?: boolean;
29
+ fallbackText?: string;
30
+ commandWindowLabel?: string;
31
+ promptTemplates?: PromptTemplates;
32
+ });
33
+ };
34
+
35
+ export interface AppConfigDefaults {
36
+ modelsPath?: string;
37
+ memoryPath?: string;
38
+ toolPath?: string;
39
+ toolDevPath?: string;
40
+ toolProdPath?: string;
41
+ }
42
+
43
+ export async function loadAppConfig(configPath?: string): Promise<AppConfig> {
44
+ const appResource = await resolveKindResourceFile<ItermAppSpec>(configPath ?? "config/app.yaml", {
45
+ baseDir: process.cwd(),
46
+ expectedApiVersion: "easynet.world/v1",
47
+ expectedKind: "AppConfig",
48
+ });
49
+ const spec = asObject(appResource.spec) as ItermAppSpec | undefined;
50
+ const runtimeConfig = await createRuntimeConfig({
51
+ configPath,
52
+ overrides: {
53
+ app: {
54
+ defaultAgent: spec?.agent,
55
+ printSteps: spec?.printSteps,
56
+ fallbackText: spec?.fallbackText,
57
+ commandWindowLabel: spec?.commandWindowLabel,
58
+ promptTemplates: spec?.promptTemplates,
59
+ },
60
+ },
61
+ });
62
+ const defaultAgent = resolveRuntimeDefaultAgentName(
63
+ runtimeConfig,
64
+ (runtimeConfig.app?.defaultAgent as string | undefined) ?? spec?.agent,
65
+ );
66
+ const selectedAgent = defaultAgent || spec?.agent || "";
67
+ const selectedAgentConfig =
68
+ selectedAgent && runtimeConfig.app?.agent && runtimeConfig.app.agent[selectedAgent]
69
+ ? runtimeConfig.app.agent[selectedAgent]
70
+ : {};
71
+ const resolvedToolConfigPath = spec?.toolConfigPath?.trim() || "config/tool.yaml";
72
+
73
+ return {
74
+ app: {
75
+ ...runtimeConfig.app,
76
+ defaultAgent,
77
+ agent: {
78
+ ...(runtimeConfig.app?.agent ?? {}),
79
+ ...(selectedAgent
80
+ ? {
81
+ [selectedAgent]: {
82
+ ...selectedAgentConfig,
83
+ tools: {
84
+ ...(selectedAgentConfig?.tools ?? {}),
85
+ ref: resolvedToolConfigPath,
86
+ },
87
+ },
88
+ }
89
+ : {}),
90
+ },
91
+ },
92
+ };
93
+ }
94
+
95
+ export function getModelsConfigPath(
96
+ config: AppConfig,
97
+ agentName?: string,
98
+ defaults?: AppConfigDefaults,
99
+ ): string {
100
+ return getRuntimeModelsConfigPath(config, agentName, defaults);
101
+ }
102
+
103
+ export function getMemoryConfigPath(
104
+ config: AppConfig,
105
+ agentName?: string,
106
+ defaults?: AppConfigDefaults,
107
+ ): string {
108
+ return getRuntimeMemoryConfigPath(config, agentName, defaults);
109
+ }
110
+
111
+ export function getToolConfigPath(
112
+ config: AppConfig,
113
+ agentName?: string,
114
+ defaults?: AppConfigDefaults,
115
+ ): string {
116
+ return getRuntimeToolConfigPath(config, agentName, defaults);
117
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared runtime context: LLM, memory, tools.
3
+ * Built once and passed to both ReAct and Deep agents.
4
+ */
5
+ import {
6
+ createContextBuilders,
7
+ type AppContextBuilders,
8
+ type BotContext,
9
+ type BotTool,
10
+ type CreateContextOptions,
11
+ } from "@easynet/agent-runtime";
12
+ import {
13
+ loadAppConfig,
14
+ getModelsConfigPath,
15
+ getMemoryConfigPath,
16
+ getToolConfigPath,
17
+ type AppConfig,
18
+ } from "./config.js";
19
+
20
+ export type { BotContext, BotTool, CreateContextOptions };
21
+
22
+ const builders: AppContextBuilders = createContextBuilders<AppConfig>({
23
+ configApi: {
24
+ loadAgentConfig: loadAppConfig,
25
+ getModelsConfigPath: (config: AppConfig, agentName?: string) => getModelsConfigPath(config, agentName),
26
+ getMemoryConfigPath: (config: AppConfig, agentName?: string) => getMemoryConfigPath(config, agentName),
27
+ getToolConfigPath: (config: AppConfig, agentName?: string) => getToolConfigPath(config, agentName),
28
+ },
29
+ });
30
+
31
+ export const getAgentLlm: AppContextBuilders["createAgentLlm"] = builders.createAgentLlm;
32
+ export const getAgentMemory: AppContextBuilders["createAgentMemory"] = builders.createAgentMemory;
33
+ export const getAgentTools: AppContextBuilders["createAgentTools"] = builders.createAgentTools;
34
+
35
+ export const createAgentLlm: AppContextBuilders["createAgentLlm"] = builders.createAgentLlm;
36
+ export const createAgentMemory: AppContextBuilders["createAgentMemory"] = builders.createAgentMemory;
37
+ export const createAgentTools: AppContextBuilders["createAgentTools"] = builders.createAgentTools;
38
+ export const createAgent: AppContextBuilders["createAgent"] = builders.createAgent;
39
+ export const createBotContext: AppContextBuilders["createBotContext"] = builders.createBotContext;
@@ -0,0 +1,220 @@
1
+ import type { BotContext } from "@easynet/agent-runtime";
2
+
3
+ type ToolLike = {
4
+ name?: unknown;
5
+ invoke?: (args: unknown) => Promise<unknown>;
6
+ };
7
+
8
+ const BLOCKED_SHORT_NAMES = new Set([
9
+ "listDir",
10
+ "readText",
11
+ "writeText",
12
+ "runCommand",
13
+ "gitRead",
14
+ "gitAdd",
15
+ "gitCommit",
16
+ "gitDiff",
17
+ "gitPull",
18
+ "gitPush",
19
+ "gitSwitchBranch",
20
+ "gitLogHistory",
21
+ ]);
22
+
23
+ type RedirectableShortName =
24
+ | "listDir"
25
+ | "readText"
26
+ | "runCommand"
27
+ | "itermListWindows"
28
+ | "itermListCurrentWindowSessions"
29
+ | "itermGetSessionInfo";
30
+
31
+ type ItermCommandArgs = {
32
+ command: string;
33
+ waitMs?: number;
34
+ maxOutputLines?: number;
35
+ outputOffsetLines?: number;
36
+ };
37
+
38
+ function shortName(name: string): string {
39
+ const parts = name.split(".");
40
+ return parts[parts.length - 1] ?? name;
41
+ }
42
+
43
+ function shouldBlockTool(name: string): boolean {
44
+ if (name.includes("itermRunCommandInSession")) return false;
45
+ if (name.includes("iterm")) return true;
46
+ return BLOCKED_SHORT_NAMES.has(shortName(name));
47
+ }
48
+
49
+ function policyError(toolName: string): Error {
50
+ return new Error(
51
+ `Tool "${toolName}" is blocked in iTermBot policy. ` +
52
+ `Use itermRunCommandInSession on target panel, then analyze returned output.`,
53
+ );
54
+ }
55
+
56
+ function asRecord(input: unknown): Record<string, unknown> {
57
+ if (!input || typeof input !== "object" || Array.isArray(input)) return {};
58
+ return input as Record<string, unknown>;
59
+ }
60
+
61
+ function quoteForBash(value: string): string {
62
+ return `'${value.replaceAll("'", `'\\''`)}'`;
63
+ }
64
+
65
+ function asBoolean(value: unknown, fallback: boolean): boolean {
66
+ return typeof value === "boolean" ? value : fallback;
67
+ }
68
+
69
+ function asBoundedInt(value: unknown, fallback: number, min: number, max: number): number {
70
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
71
+ const normalized = Math.floor(value);
72
+ return Math.max(min, Math.min(max, normalized));
73
+ }
74
+
75
+ function buildListDirCommand(args: Record<string, unknown>): ItermCommandArgs {
76
+ const path = typeof args.path === "string" && args.path.trim() ? args.path.trim() : ".";
77
+ const recursive = asBoolean(args.recursive, false);
78
+ const includeHidden = asBoolean(args.includeHidden, false);
79
+ const maxDepth = asBoundedInt(args.maxDepth, 3, 1, 20);
80
+ const maxEntries = asBoundedInt(args.maxEntries, 200, 1, 2000);
81
+ const quotedPath = quoteForBash(path);
82
+ if (!recursive) {
83
+ const lsFlags = includeHidden ? "-la" : "-l";
84
+ return { command: `ls ${lsFlags} ${quotedPath} | head -n ${maxEntries}` };
85
+ }
86
+ const hiddenFilter = includeHidden ? "" : ` | grep -Ev '/\\.[^/]+($|/)'`;
87
+ return {
88
+ command: `find ${quotedPath} -maxdepth ${maxDepth} -print${hiddenFilter} | head -n ${maxEntries}`,
89
+ maxOutputLines: Math.min(maxEntries + 50, 3000),
90
+ };
91
+ }
92
+
93
+ function buildReadTextCommand(args: Record<string, unknown>): ItermCommandArgs | null {
94
+ const path = typeof args.path === "string" ? args.path.trim() : "";
95
+ if (!path) return null;
96
+ const maxBytes = asBoundedInt(args.maxBytes, 24_000, 256, 200_000);
97
+ const maxLines = asBoundedInt(Math.floor(maxBytes / 120), 200, 20, 2000);
98
+ return {
99
+ command: `sed -n '1,${maxLines}p' ${quoteForBash(path)}`,
100
+ maxOutputLines: Math.min(maxLines + 40, 2500),
101
+ };
102
+ }
103
+
104
+ function buildRunCommandCommand(args: Record<string, unknown>): ItermCommandArgs | null {
105
+ if (typeof args.command === "string" && args.command.trim()) {
106
+ return { command: args.command.trim() };
107
+ }
108
+ if (typeof args.cmd === "string" && args.cmd.trim()) {
109
+ return { command: args.cmd.trim() };
110
+ }
111
+ if (Array.isArray(args.cmdArray) && args.cmdArray.length > 0) {
112
+ const joined = args.cmdArray.map((part) => String(part)).join(" ").trim();
113
+ if (joined) return { command: joined };
114
+ }
115
+ return null;
116
+ }
117
+
118
+ function toItermCommandArgs(toolName: string, args: unknown): ItermCommandArgs | null {
119
+ const tool = shortName(toolName);
120
+ const record = asRecord(args);
121
+ if (tool === "listDir") return buildListDirCommand(record);
122
+ if (tool === "readText") return buildReadTextCommand(record);
123
+ if (tool === "runCommand") return buildRunCommandCommand(record);
124
+ if (tool === "itermListWindows") return { command: "pwd && ls -la" };
125
+ if (tool === "itermListCurrentWindowSessions") return { command: "pwd && ls -la" };
126
+ if (tool === "itermGetSessionInfo") return { command: "pwd && ls -la" };
127
+ return null;
128
+ }
129
+
130
+ function isRedirectableTool(toolName: string): toolName is RedirectableShortName {
131
+ return (
132
+ toolName === "listDir" ||
133
+ toolName === "readText" ||
134
+ toolName === "runCommand" ||
135
+ toolName === "itermListWindows" ||
136
+ toolName === "itermListCurrentWindowSessions" ||
137
+ toolName === "itermGetSessionInfo"
138
+ );
139
+ }
140
+
141
+ function findItermCommandTool(tools: ToolLike[]): ToolLike | null {
142
+ return tools.find((tool) => typeof tool.name === "string" && typeof tool.invoke === "function" && tool.name.includes("itermRunCommandInSession")) ?? null;
143
+ }
144
+
145
+ async function invokeRedirect(
146
+ blockedName: string,
147
+ blockedArgs: unknown,
148
+ commandTool: ToolLike | null,
149
+ ): Promise<unknown> {
150
+ const mapped = toItermCommandArgs(blockedName, blockedArgs);
151
+ if (!mapped || !commandTool || typeof commandTool.invoke !== "function") {
152
+ throw policyError(blockedName);
153
+ }
154
+ const output = await commandTool.invoke(mapped);
155
+ return {
156
+ result: {
157
+ blockedTool: blockedName,
158
+ redirectedTool: commandTool.name,
159
+ redirected: true,
160
+ originalArgs: asRecord(blockedArgs),
161
+ mappedCommand: mapped.command,
162
+ output,
163
+ },
164
+ evidence: [
165
+ {
166
+ type: "policy",
167
+ ref: "target-panel-policy",
168
+ summary: `Redirected ${blockedName} to ${String(commandTool.name)} with target-panel command execution`,
169
+ createdAt: new Date().toISOString(),
170
+ },
171
+ ],
172
+ };
173
+ }
174
+
175
+ function blockedResult(toolName: string): unknown {
176
+ return {
177
+ result: {
178
+ blocked: true,
179
+ blockedTool: toolName,
180
+ requiredTool: "itermRunCommandInSession",
181
+ message:
182
+ `Tool "${toolName}" is blocked in iTermBot policy. ` +
183
+ `Use itermRunCommandInSession on target panel, then analyze returned output.`,
184
+ suggestedCommand: "pwd && ls -la",
185
+ },
186
+ evidence: [
187
+ {
188
+ type: "policy",
189
+ ref: "target-panel-policy",
190
+ summary: `Blocked ${toolName}; returned policy guidance for target-panel command execution`,
191
+ createdAt: new Date().toISOString(),
192
+ },
193
+ ],
194
+ };
195
+ }
196
+
197
+ export function enforceTargetPanelExecutionPolicy(ctx: BotContext): () => void {
198
+ const unpatchFns: Array<() => void> = [];
199
+ const allTools = ctx.tools as unknown as ToolLike[];
200
+ const itermCommandTool = findItermCommandTool(allTools);
201
+
202
+ for (const tool of allTools) {
203
+ if (!tool || typeof tool.name !== "string" || typeof tool.invoke !== "function") continue;
204
+ if (!shouldBlockTool(tool.name)) continue;
205
+ const originalInvoke = tool.invoke.bind(tool);
206
+ tool.invoke = async (args: unknown): Promise<unknown> => {
207
+ const toolShortName = shortName(tool.name as string);
208
+ if (isRedirectableTool(toolShortName)) {
209
+ return invokeRedirect(tool.name as string, args, itermCommandTool);
210
+ }
211
+ return blockedResult(tool.name as string);
212
+ };
213
+ unpatchFns.push(() => {
214
+ tool.invoke = originalInvoke;
215
+ });
216
+ }
217
+ return () => {
218
+ for (const unpatch of unpatchFns) unpatch();
219
+ };
220
+ }
@@ -0,0 +1,60 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { enforceTargetPanelExecutionPolicy } from "../dist/iterm/target-panel-policy.js";
4
+
5
+ function createTool(name, invoke) {
6
+ return { name, invoke };
7
+ }
8
+
9
+ test("redirects listDir to itermRunCommandInSession", async () => {
10
+ const calls = [];
11
+ const itermTool = createTool(
12
+ "npm.easynet.agent.tool.buildin.0.0.70.itermRunCommandInSession",
13
+ async (args) => {
14
+ calls.push(args);
15
+ return { result: { ok: true, args } };
16
+ },
17
+ );
18
+ const blockedTool = createTool(
19
+ "npm.easynet.agent.tool.buildin.0.0.70.listDir",
20
+ async () => ({ result: { shouldNotReach: true } }),
21
+ );
22
+ const ctx = { tools: [blockedTool, itermTool] };
23
+
24
+ const unpatch = enforceTargetPanelExecutionPolicy(ctx);
25
+ const out = await blockedTool.invoke({
26
+ path: ".",
27
+ recursive: true,
28
+ includeHidden: false,
29
+ maxDepth: 2,
30
+ maxEntries: 50,
31
+ });
32
+ unpatch();
33
+
34
+ assert.equal(calls.length, 1);
35
+ assert.equal(typeof calls[0].command, "string");
36
+ assert.equal(calls[0].command.includes("find"), true);
37
+ assert.equal(out?.result?.redirected, true);
38
+ assert.equal(
39
+ out?.result?.redirectedTool,
40
+ "npm.easynet.agent.tool.buildin.0.0.70.itermRunCommandInSession",
41
+ );
42
+ });
43
+
44
+ test("keeps non-redirectable blocked tools rejected", async () => {
45
+ const itermTool = createTool(
46
+ "npm.easynet.agent.tool.buildin.0.0.70.itermRunCommandInSession",
47
+ async () => ({ result: { ok: true } }),
48
+ );
49
+ const gitReadTool = createTool(
50
+ "npm.easynet.agent.tool.buildin.0.0.70.gitRead",
51
+ async () => ({ result: { shouldNotReach: true } }),
52
+ );
53
+ const ctx = { tools: [gitReadTool, itermTool] };
54
+
55
+ const unpatch = enforceTargetPanelExecutionPolicy(ctx);
56
+ const out = await gitReadTool.invoke({});
57
+ unpatch();
58
+ assert.equal(out?.result?.blocked, true);
59
+ assert.equal(out?.result?.requiredTool, "itermRunCommandInSession");
60
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easynet/agent-runtime",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Agent runtime factories: ReAct (LangChain), Deep (DeepAgents), sub-agent, and CLI runner",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",