@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.
- package/agent-runtime/.github/workflows/ci.yml +1 -2
- package/agent-runtime/.github/workflows/release.yml +6 -29
- package/apps/imessagebot/src/app/config.ts +76 -0
- package/apps/imessagebot/src/app/context.ts +39 -0
- package/apps/itermbot/config/tool.yaml +19 -0
- package/apps/itermbot/src/app/config.ts +117 -0
- package/apps/itermbot/src/app/context.ts +39 -0
- package/apps/itermbot/src/iterm/target-panel-policy.ts +220 -0
- package/apps/itermbot/test/target-panel-policy.test.mjs +60 -0
- package/package.json +1 -1
|
@@ -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:
|
|
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:
|
|
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
|
+
});
|