@librechat/agents 3.1.77 → 3.1.78-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/common/enum.cjs +54 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +148 -4
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +291 -0
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs.map +1 -0
- package/dist/cjs/main.cjs +90 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/anthropicToolCache.cjs +102 -0
- package/dist/cjs/messages/anthropicToolCache.cjs.map +1 -0
- package/dist/cjs/messages/prune.cjs +27 -0
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/messages/recency.cjs +99 -0
- package/dist/cjs/messages/recency.cjs.map +1 -0
- package/dist/cjs/run.cjs +30 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +100 -6
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +635 -23
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/local/CompileCheckTool.cjs +227 -0
- package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -0
- package/dist/cjs/tools/local/FileCheckpointer.cjs +90 -0
- package/dist/cjs/tools/local/FileCheckpointer.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalCodingTools.cjs +1098 -0
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1042 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionTools.cjs +122 -0
- package/dist/cjs/tools/local/LocalExecutionTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +453 -0
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/local/attachments.cjs +183 -0
- package/dist/cjs/tools/local/attachments.cjs.map +1 -0
- package/dist/cjs/tools/local/bashAst.cjs +129 -0
- package/dist/cjs/tools/local/bashAst.cjs.map +1 -0
- package/dist/cjs/tools/local/editStrategies.cjs +188 -0
- package/dist/cjs/tools/local/editStrategies.cjs.map +1 -0
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +141 -0
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -0
- package/dist/cjs/tools/local/syntaxCheck.cjs +182 -0
- package/dist/cjs/tools/local/syntaxCheck.cjs.map +1 -0
- package/dist/cjs/tools/local/textEncoding.cjs +30 -0
- package/dist/cjs/tools/local/textEncoding.cjs.map +1 -0
- package/dist/cjs/tools/local/workspaceFS.cjs +51 -0
- package/dist/cjs/tools/local/workspaceFS.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +53 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +149 -5
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +289 -0
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs.map +1 -0
- package/dist/esm/main.mjs +17 -2
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/anthropicToolCache.mjs +99 -0
- package/dist/esm/messages/anthropicToolCache.mjs.map +1 -0
- package/dist/esm/messages/prune.mjs +26 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/messages/recency.mjs +97 -0
- package/dist/esm/messages/recency.mjs.map +1 -0
- package/dist/esm/run.mjs +30 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +100 -6
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +635 -23
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/local/CompileCheckTool.mjs +223 -0
- package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -0
- package/dist/esm/tools/local/FileCheckpointer.mjs +87 -0
- package/dist/esm/tools/local/FileCheckpointer.mjs.map +1 -0
- package/dist/esm/tools/local/LocalCodingTools.mjs +1075 -0
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +1022 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionTools.mjs +117 -0
- package/dist/esm/tools/local/LocalExecutionTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +448 -0
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/local/attachments.mjs +180 -0
- package/dist/esm/tools/local/attachments.mjs.map +1 -0
- package/dist/esm/tools/local/bashAst.mjs +126 -0
- package/dist/esm/tools/local/bashAst.mjs.map +1 -0
- package/dist/esm/tools/local/editStrategies.mjs +185 -0
- package/dist/esm/tools/local/editStrategies.mjs.map +1 -0
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +137 -0
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -0
- package/dist/esm/tools/local/syntaxCheck.mjs +179 -0
- package/dist/esm/tools/local/syntaxCheck.mjs.map +1 -0
- package/dist/esm/tools/local/textEncoding.mjs +27 -0
- package/dist/esm/tools/local/textEncoding.mjs.map +1 -0
- package/dist/esm/tools/local/workspaceFS.mjs +49 -0
- package/dist/esm/tools/local/workspaceFS.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +39 -1
- package/dist/types/graphs/Graph.d.ts +34 -0
- package/dist/types/hooks/createWorkspacePolicyHook.d.ts +95 -0
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messages/anthropicToolCache.d.ts +51 -0
- package/dist/types/messages/index.d.ts +2 -0
- package/dist/types/messages/prune.d.ts +11 -0
- package/dist/types/messages/recency.d.ts +64 -0
- package/dist/types/run.d.ts +21 -0
- package/dist/types/tools/ToolNode.d.ts +145 -2
- package/dist/types/tools/local/CompileCheckTool.d.ts +31 -0
- package/dist/types/tools/local/FileCheckpointer.d.ts +39 -0
- package/dist/types/tools/local/LocalCodingTools.d.ts +57 -0
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +149 -0
- package/dist/types/tools/local/LocalExecutionTools.d.ts +9 -0
- package/dist/types/tools/local/LocalProgrammaticToolCalling.d.ts +21 -0
- package/dist/types/tools/local/attachments.d.ts +84 -0
- package/dist/types/tools/local/bashAst.d.ts +11 -0
- package/dist/types/tools/local/editStrategies.d.ts +28 -0
- package/dist/types/tools/local/index.d.ts +12 -0
- package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +38 -0
- package/dist/types/tools/local/syntaxCheck.d.ts +42 -0
- package/dist/types/tools/local/textEncoding.d.ts +21 -0
- package/dist/types/tools/local/workspaceFS.d.ts +49 -0
- package/dist/types/types/hitl.d.ts +56 -27
- package/dist/types/types/run.d.ts +8 -1
- package/dist/types/types/summarize.d.ts +30 -0
- package/dist/types/types/tools.d.ts +341 -6
- package/package.json +21 -2
- package/src/common/enum.ts +54 -0
- package/src/graphs/Graph.ts +164 -6
- package/src/hooks/__tests__/compactHooks.test.ts +38 -2
- package/src/hooks/__tests__/createWorkspacePolicyHook.test.ts +393 -0
- package/src/hooks/createWorkspacePolicyHook.ts +355 -0
- package/src/hooks/index.ts +6 -0
- package/src/index.ts +1 -0
- package/src/messages/__tests__/anthropicToolCache.test.ts +125 -0
- package/src/messages/__tests__/recency.test.ts +267 -0
- package/src/messages/anthropicToolCache.ts +116 -0
- package/src/messages/index.ts +2 -0
- package/src/messages/prune.ts +27 -1
- package/src/messages/recency.ts +155 -0
- package/src/run.ts +31 -0
- package/src/scripts/compare_pi_vs_ours.ts +840 -0
- package/src/scripts/local_engine.ts +166 -0
- package/src/scripts/local_engine_checkpointer.ts +205 -0
- package/src/scripts/local_engine_compile.ts +263 -0
- package/src/scripts/local_engine_hooks.ts +226 -0
- package/src/scripts/local_engine_image.ts +201 -0
- package/src/scripts/local_engine_ptc.ts +151 -0
- package/src/scripts/local_engine_workspace.ts +258 -0
- package/src/scripts/summarization-recency.ts +462 -0
- package/src/specs/prune.test.ts +39 -0
- package/src/summarization/__tests__/node.test.ts +499 -3
- package/src/summarization/node.ts +124 -7
- package/src/tools/ToolNode.ts +769 -20
- package/src/tools/__tests__/LocalExecutionTools.test.ts +2647 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +175 -0
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +114 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +84 -0
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +467 -0
- package/src/tools/__tests__/directToolHooks.test.ts +411 -0
- package/src/tools/__tests__/localToolNames.test.ts +73 -0
- package/src/tools/__tests__/workspaceSeam.test.ts +134 -0
- package/src/tools/local/CompileCheckTool.ts +278 -0
- package/src/tools/local/FileCheckpointer.ts +93 -0
- package/src/tools/local/LocalCodingTools.ts +1342 -0
- package/src/tools/local/LocalExecutionEngine.ts +1329 -0
- package/src/tools/local/LocalExecutionTools.ts +167 -0
- package/src/tools/local/LocalProgrammaticToolCalling.ts +594 -0
- package/src/tools/local/__tests__/FileCheckpointer.test.ts +120 -0
- package/src/tools/local/__tests__/editStrategies.test.ts +134 -0
- package/src/tools/local/attachments.ts +251 -0
- package/src/tools/local/bashAst.ts +151 -0
- package/src/tools/local/editStrategies.ts +188 -0
- package/src/tools/local/index.ts +12 -0
- package/src/tools/local/resolveLocalExecutionTools.ts +208 -0
- package/src/tools/local/syntaxCheck.ts +243 -0
- package/src/tools/local/textEncoding.ts +37 -0
- package/src/tools/local/workspaceFS.ts +89 -0
- package/src/types/hitl.ts +56 -27
- package/src/types/run.ts +12 -1
- package/src/types/summarize.ts +31 -0
- package/src/types/tools.ts +359 -7
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { tmpdir } from 'os';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
|
+
import { mkdtemp, rm } from 'fs/promises';
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
5
|
+
import { createWorkspacePolicyHook } from '../createWorkspacePolicyHook';
|
|
6
|
+
import type {
|
|
7
|
+
HookCallback,
|
|
8
|
+
PreToolUseHookInput,
|
|
9
|
+
PreToolUseHookOutput,
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
function call(
|
|
13
|
+
hook: HookCallback<'PreToolUse'>,
|
|
14
|
+
input: PreToolUseHookInput
|
|
15
|
+
): Promise<PreToolUseHookOutput> {
|
|
16
|
+
return Promise.resolve(hook(input, new AbortController().signal));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeInput(
|
|
20
|
+
toolName: string,
|
|
21
|
+
toolInput: Record<string, unknown>
|
|
22
|
+
): PreToolUseHookInput {
|
|
23
|
+
return {
|
|
24
|
+
hook_event_name: 'PreToolUse',
|
|
25
|
+
runId: 'r',
|
|
26
|
+
threadId: 't',
|
|
27
|
+
agentId: 'a',
|
|
28
|
+
toolName,
|
|
29
|
+
toolInput,
|
|
30
|
+
toolUseId: 'tu',
|
|
31
|
+
stepId: 's',
|
|
32
|
+
turn: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('createWorkspacePolicyHook', () => {
|
|
37
|
+
let workspace: string;
|
|
38
|
+
let extra: string;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
workspace = await mkdtemp(join(tmpdir(), 'lc-wp-'));
|
|
42
|
+
extra = await mkdtemp(join(tmpdir(), 'lc-wp-extra-'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await rm(workspace, { recursive: true, force: true });
|
|
47
|
+
await rm(extra, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('allows in-workspace relative paths', async () => {
|
|
51
|
+
const hook = createWorkspacePolicyHook({ root: workspace });
|
|
52
|
+
const out = await call(hook,makeInput('read_file', { file_path: 'src/x.ts' }));
|
|
53
|
+
expect(out.decision).toBe('allow');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('allows in-workspace absolute paths', async () => {
|
|
57
|
+
const hook = createWorkspacePolicyHook({ root: workspace });
|
|
58
|
+
const out = await call(hook,
|
|
59
|
+
makeInput('read_file', { file_path: join(workspace, 'src/x.ts') })
|
|
60
|
+
);
|
|
61
|
+
expect(out.decision).toBe('allow');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('allows additionalRoots paths', async () => {
|
|
65
|
+
const hook = createWorkspacePolicyHook({
|
|
66
|
+
root: workspace,
|
|
67
|
+
additionalRoots: [extra],
|
|
68
|
+
});
|
|
69
|
+
const out = await call(hook,
|
|
70
|
+
makeInput('read_file', { file_path: join(extra, 'lib/y.ts') })
|
|
71
|
+
);
|
|
72
|
+
expect(out.decision).toBe('allow');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('asks by default when path is outside the workspace (read tool)', async () => {
|
|
76
|
+
const hook = createWorkspacePolicyHook({ root: workspace });
|
|
77
|
+
const out = await call(hook,
|
|
78
|
+
makeInput('read_file', { file_path: '/etc/passwd' })
|
|
79
|
+
);
|
|
80
|
+
expect(out.decision).toBe('ask');
|
|
81
|
+
expect(out.reason).toContain('/etc/passwd');
|
|
82
|
+
expect(out.allowedDecisions).toEqual(['approve', 'reject']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('asks by default when path is outside the workspace (write tool)', async () => {
|
|
86
|
+
const hook = createWorkspacePolicyHook({ root: workspace });
|
|
87
|
+
const out = await call(hook,
|
|
88
|
+
makeInput('write_file', {
|
|
89
|
+
file_path: '/etc/foo',
|
|
90
|
+
content: 'malicious',
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
expect(out.decision).toBe('ask');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('honours `outsideRead: deny` for read tools', async () => {
|
|
97
|
+
const hook = createWorkspacePolicyHook({
|
|
98
|
+
root: workspace,
|
|
99
|
+
outsideRead: 'deny',
|
|
100
|
+
});
|
|
101
|
+
const out = await call(hook,
|
|
102
|
+
makeInput('read_file', { file_path: '/etc/passwd' })
|
|
103
|
+
);
|
|
104
|
+
expect(out.decision).toBe('deny');
|
|
105
|
+
expect(out.reason).toContain('/etc/passwd');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('honours `outsideWrite: deny` for write tools', async () => {
|
|
109
|
+
const hook = createWorkspacePolicyHook({
|
|
110
|
+
root: workspace,
|
|
111
|
+
outsideWrite: 'deny',
|
|
112
|
+
});
|
|
113
|
+
const out = await call(hook,
|
|
114
|
+
makeInput('edit_file', {
|
|
115
|
+
file_path: '/etc/foo',
|
|
116
|
+
old_text: 'a',
|
|
117
|
+
new_text: 'b',
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
expect(out.decision).toBe('deny');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('honours `outsideRead: allow`', async () => {
|
|
124
|
+
const hook = createWorkspacePolicyHook({
|
|
125
|
+
root: workspace,
|
|
126
|
+
outsideRead: 'allow',
|
|
127
|
+
});
|
|
128
|
+
const out = await call(hook,
|
|
129
|
+
makeInput('read_file', { file_path: '/etc/passwd' })
|
|
130
|
+
);
|
|
131
|
+
expect(out.decision).toBe('allow');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('passes through when the tool has no extractor (e.g. bash)', async () => {
|
|
135
|
+
const hook = createWorkspacePolicyHook({ root: workspace });
|
|
136
|
+
const out = await call(hook,
|
|
137
|
+
makeInput('bash', { command: 'cat /etc/passwd' })
|
|
138
|
+
);
|
|
139
|
+
expect(out.decision).toBe('allow');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('passes through when the path arg is missing/empty', async () => {
|
|
143
|
+
const hook = createWorkspacePolicyHook({ root: workspace });
|
|
144
|
+
const out = await call(hook,makeInput('grep_search', { pattern: 'x' }));
|
|
145
|
+
expect(out.decision).toBe('allow');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('allows custom extractors to opt new tools in', async () => {
|
|
149
|
+
const hook = createWorkspacePolicyHook({
|
|
150
|
+
root: workspace,
|
|
151
|
+
pathExtractors: {
|
|
152
|
+
my_custom_tool: (i) =>
|
|
153
|
+
typeof i.target === 'string' ? [i.target] : [],
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const inside = await call(hook,
|
|
157
|
+
makeInput('my_custom_tool', { target: 'in/workspace.ts' })
|
|
158
|
+
);
|
|
159
|
+
expect(inside.decision).toBe('allow');
|
|
160
|
+
|
|
161
|
+
const outside = await call(hook,
|
|
162
|
+
makeInput('my_custom_tool', { target: '/elsewhere/x.ts' })
|
|
163
|
+
);
|
|
164
|
+
// Unknown tools default to write-policy (stricter): 'ask'.
|
|
165
|
+
expect(outside.decision).toBe('ask');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('formats the reason via {tool} and {paths}', async () => {
|
|
169
|
+
const hook = createWorkspacePolicyHook({
|
|
170
|
+
root: workspace,
|
|
171
|
+
reason: '{tool} blocked from {paths}',
|
|
172
|
+
});
|
|
173
|
+
const out = await call(hook,
|
|
174
|
+
makeInput('read_file', { file_path: '/somewhere/else.ts' })
|
|
175
|
+
);
|
|
176
|
+
expect(out.reason).toBe('read_file blocked from /somewhere/else.ts');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('handles a tool input with an array of paths', async () => {
|
|
180
|
+
const hook = createWorkspacePolicyHook({
|
|
181
|
+
root: workspace,
|
|
182
|
+
pathExtractors: {
|
|
183
|
+
multi_file_tool: (i) =>
|
|
184
|
+
Array.isArray(i.paths) ? (i.paths as string[]) : [],
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const out = await call(hook,
|
|
188
|
+
makeInput('multi_file_tool', {
|
|
189
|
+
paths: [join(workspace, 'a.ts'), '/etc/passwd'],
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
expect(out.decision).toBe('ask');
|
|
193
|
+
expect(out.reason).toContain('/etc/passwd');
|
|
194
|
+
expect(out.reason).not.toContain(join(workspace, 'a.ts'));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('respects resolved root paths (relative root config)', async () => {
|
|
198
|
+
const hook = createWorkspacePolicyHook({ root: '.' });
|
|
199
|
+
const out = await call(hook,
|
|
200
|
+
makeInput('read_file', { file_path: resolve('.', 'src/x.ts') })
|
|
201
|
+
);
|
|
202
|
+
expect(out.decision).toBe('allow');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('symlink-escape (Codex P1 #10)', () => {
|
|
206
|
+
it('rejects a symlink inside the workspace that points outside', async () => {
|
|
207
|
+
const fs = await import('fs/promises');
|
|
208
|
+
await fs.writeFile(join(extra, 'secret.txt'), 'top-secret\n');
|
|
209
|
+
await fs.symlink(
|
|
210
|
+
join(extra, 'secret.txt'),
|
|
211
|
+
join(workspace, 'escape')
|
|
212
|
+
);
|
|
213
|
+
const hook = createWorkspacePolicyHook({
|
|
214
|
+
root: workspace,
|
|
215
|
+
outsideRead: 'deny',
|
|
216
|
+
});
|
|
217
|
+
const out = await call(
|
|
218
|
+
hook,
|
|
219
|
+
makeInput('read_file', { file_path: 'escape' })
|
|
220
|
+
);
|
|
221
|
+
expect(out.decision).toBe('deny');
|
|
222
|
+
expect(out.reason).toContain('escape');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('still allows a lexically-outside path that realpaths back inside (alternate mount via symlink)', async () => {
|
|
226
|
+
const fs = await import('fs/promises');
|
|
227
|
+
await fs.writeFile(join(workspace, 'file.ts'), 'export {};\n');
|
|
228
|
+
const altMount = join(extra, 'alt-mount');
|
|
229
|
+
await fs.symlink(workspace, altMount);
|
|
230
|
+
const hook = createWorkspacePolicyHook({
|
|
231
|
+
root: workspace,
|
|
232
|
+
outsideRead: 'deny',
|
|
233
|
+
});
|
|
234
|
+
const out = await call(
|
|
235
|
+
hook,
|
|
236
|
+
makeInput('read_file', { file_path: join(altMount, 'file.ts') })
|
|
237
|
+
);
|
|
238
|
+
expect(out.decision).toBe('allow');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('compile_check command path extraction (Codex P1 #26)', () => {
|
|
243
|
+
it('extracts absolute paths from compile_check commands so the policy fires', async () => {
|
|
244
|
+
const hook = createWorkspacePolicyHook({
|
|
245
|
+
root: workspace,
|
|
246
|
+
outsideRead: 'deny',
|
|
247
|
+
});
|
|
248
|
+
const out = await call(
|
|
249
|
+
hook,
|
|
250
|
+
makeInput('compile_check', { command: 'cat /etc/passwd' })
|
|
251
|
+
);
|
|
252
|
+
expect(out.decision).toBe('deny');
|
|
253
|
+
expect(out.reason).toContain('/etc/passwd');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('extracts paths from --flag=/path-style tokens', async () => {
|
|
257
|
+
// compile_check is in READ_TOOLS, so the policy uses outsideRead.
|
|
258
|
+
const hook = createWorkspacePolicyHook({
|
|
259
|
+
root: workspace,
|
|
260
|
+
outsideRead: 'deny',
|
|
261
|
+
});
|
|
262
|
+
const out = await call(
|
|
263
|
+
hook,
|
|
264
|
+
makeInput('compile_check', { command: 'tsc --out=/etc/foo' })
|
|
265
|
+
);
|
|
266
|
+
expect(out.decision).toBe('deny');
|
|
267
|
+
expect(out.reason).toContain('/etc/foo');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('extracts ~/ and $HOME/ tokens', async () => {
|
|
271
|
+
const hook = createWorkspacePolicyHook({
|
|
272
|
+
root: workspace,
|
|
273
|
+
outsideRead: 'deny',
|
|
274
|
+
});
|
|
275
|
+
const tilde = await call(
|
|
276
|
+
hook,
|
|
277
|
+
makeInput('compile_check', { command: 'cat ~/secret' })
|
|
278
|
+
);
|
|
279
|
+
expect(tilde.decision).toBe('deny');
|
|
280
|
+
const homeVar = await call(
|
|
281
|
+
hook,
|
|
282
|
+
makeInput('compile_check', { command: 'cat $HOME/secret' })
|
|
283
|
+
);
|
|
284
|
+
expect(homeVar.decision).toBe('deny');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('allows in-workspace absolute paths in the command', async () => {
|
|
288
|
+
const hook = createWorkspacePolicyHook({
|
|
289
|
+
root: workspace,
|
|
290
|
+
outsideRead: 'deny',
|
|
291
|
+
});
|
|
292
|
+
const out = await call(
|
|
293
|
+
hook,
|
|
294
|
+
makeInput('compile_check', {
|
|
295
|
+
command: `tsc --noEmit ${join(workspace, 'src/x.ts')}`,
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
expect(out.decision).toBe('allow');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('allows commands with no absolute paths (relative-only auto-detect form)', async () => {
|
|
302
|
+
const hook = createWorkspacePolicyHook({
|
|
303
|
+
root: workspace,
|
|
304
|
+
outsideRead: 'deny',
|
|
305
|
+
});
|
|
306
|
+
const out = await call(
|
|
307
|
+
hook,
|
|
308
|
+
makeInput('compile_check', { command: 'npx tsc --noEmit' })
|
|
309
|
+
);
|
|
310
|
+
expect(out.decision).toBe('allow');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('extracts paths wrapped in double quotes (Codex P1 #31)', async () => {
|
|
314
|
+
const hook = createWorkspacePolicyHook({
|
|
315
|
+
root: workspace,
|
|
316
|
+
outsideRead: 'deny',
|
|
317
|
+
});
|
|
318
|
+
const out = await call(
|
|
319
|
+
hook,
|
|
320
|
+
makeInput('compile_check', { command: 'cat "/etc/passwd"' })
|
|
321
|
+
);
|
|
322
|
+
expect(out.decision).toBe('deny');
|
|
323
|
+
expect(out.reason).toContain('/etc/passwd');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('extracts paths wrapped in single quotes (Codex P1 #31)', async () => {
|
|
327
|
+
const hook = createWorkspacePolicyHook({
|
|
328
|
+
root: workspace,
|
|
329
|
+
outsideRead: 'deny',
|
|
330
|
+
});
|
|
331
|
+
const out = await call(
|
|
332
|
+
hook,
|
|
333
|
+
makeInput('compile_check', { command: 'cat \'/etc/passwd\'' })
|
|
334
|
+
);
|
|
335
|
+
expect(out.decision).toBe('deny');
|
|
336
|
+
expect(out.reason).toContain('/etc/passwd');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('extracts quoted --flag="/path" tokens (Codex P1 #31)', async () => {
|
|
340
|
+
const hook = createWorkspacePolicyHook({
|
|
341
|
+
root: workspace,
|
|
342
|
+
outsideRead: 'deny',
|
|
343
|
+
});
|
|
344
|
+
const out = await call(
|
|
345
|
+
hook,
|
|
346
|
+
makeInput('compile_check', { command: 'tsc --out="/tmp/x"' })
|
|
347
|
+
);
|
|
348
|
+
expect(out.decision).toBe('deny');
|
|
349
|
+
expect(out.reason).toContain('/tmp/x');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('extracts ../ parent-traversal tokens (Codex P2 #35)', async () => {
|
|
353
|
+
// `../secrets.txt` resolves to `<parent-of-workspace>/secrets.txt`
|
|
354
|
+
// which is outside the workspace boundary. Pre-fix the regex
|
|
355
|
+
// ignored relative tokens entirely so this looked like an
|
|
356
|
+
// empty-extract → allow.
|
|
357
|
+
const hook = createWorkspacePolicyHook({
|
|
358
|
+
root: workspace,
|
|
359
|
+
outsideRead: 'deny',
|
|
360
|
+
});
|
|
361
|
+
const out = await call(
|
|
362
|
+
hook,
|
|
363
|
+
makeInput('compile_check', { command: 'cat ../secrets.txt' })
|
|
364
|
+
);
|
|
365
|
+
expect(out.decision).toBe('deny');
|
|
366
|
+
expect(out.reason).toContain('../secrets.txt');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('extracts deeper traversal like ../../etc/foo (Codex P2 #35)', async () => {
|
|
370
|
+
const hook = createWorkspacePolicyHook({
|
|
371
|
+
root: workspace,
|
|
372
|
+
outsideRead: 'deny',
|
|
373
|
+
});
|
|
374
|
+
const out = await call(
|
|
375
|
+
hook,
|
|
376
|
+
makeInput('compile_check', { command: 'cat ../../etc/foo' })
|
|
377
|
+
);
|
|
378
|
+
expect(out.decision).toBe('deny');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('does NOT trip on `./` or single-dot tokens (no false positives)', async () => {
|
|
382
|
+
const hook = createWorkspacePolicyHook({
|
|
383
|
+
root: workspace,
|
|
384
|
+
outsideRead: 'deny',
|
|
385
|
+
});
|
|
386
|
+
const out = await call(
|
|
387
|
+
hook,
|
|
388
|
+
makeInput('compile_check', { command: 'cd ./src && npx tsc' })
|
|
389
|
+
);
|
|
390
|
+
expect(out.decision).toBe('allow');
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|