forkit-connect 0.1.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/QUICKSTART.md +55 -0
- package/README.md +96 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +4724 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +21 -0
- package/dist/launcher.d.ts +33 -0
- package/dist/launcher.js +9344 -0
- package/dist/ps-list-loader.d.ts +5 -0
- package/dist/ps-list-loader.js +20 -0
- package/dist/v1/agent-observation.d.ts +42 -0
- package/dist/v1/agent-observation.js +499 -0
- package/dist/v1/api.d.ts +276 -0
- package/dist/v1/api.js +390 -0
- package/dist/v1/credential-store.d.ts +92 -0
- package/dist/v1/credential-store.js +797 -0
- package/dist/v1/currency.d.ts +41 -0
- package/dist/v1/currency.js +127 -0
- package/dist/v1/daemon.d.ts +50 -0
- package/dist/v1/daemon.js +265 -0
- package/dist/v1/discovery.d.ts +61 -0
- package/dist/v1/discovery.js +168 -0
- package/dist/v1/filesystem-models.d.ts +11 -0
- package/dist/v1/filesystem-models.js +261 -0
- package/dist/v1/heartbeat.d.ts +45 -0
- package/dist/v1/heartbeat.js +463 -0
- package/dist/v1/lifecycle-monitor.d.ts +78 -0
- package/dist/v1/lifecycle-monitor.js +512 -0
- package/dist/v1/lmstudio.d.ts +11 -0
- package/dist/v1/lmstudio.js +148 -0
- package/dist/v1/ollama.d.ts +19 -0
- package/dist/v1/ollama.js +164 -0
- package/dist/v1/openai-compatible.d.ts +12 -0
- package/dist/v1/openai-compatible.js +124 -0
- package/dist/v1/process-scout.d.ts +50 -0
- package/dist/v1/process-scout.js +715 -0
- package/dist/v1/providers.d.ts +50 -0
- package/dist/v1/providers.js +106 -0
- package/dist/v1/service.d.ts +680 -0
- package/dist/v1/service.js +8286 -0
- package/dist/v1/state.d.ts +87 -0
- package/dist/v1/state.js +1318 -0
- package/dist/v1/test-credential-backend.d.ts +19 -0
- package/dist/v1/test-credential-backend.js +49 -0
- package/dist/v1/types.d.ts +873 -0
- package/dist/v1/types.js +3 -0
- package/dist/v1/update.d.ts +38 -0
- package/dist/v1/update.js +184 -0
- package/dist/v1/vitality-pulse.d.ts +36 -0
- package/dist/v1/vitality-pulse.js +512 -0
- package/package.json +53 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadPsList = loadPsList;
|
|
4
|
+
exports.listProcesses = listProcesses;
|
|
5
|
+
// `ps-list` is ESM-only; load it lazily so the CommonJS Connect build can still run.
|
|
6
|
+
const importPsList = new Function('specifier', 'return import(specifier)');
|
|
7
|
+
let cachedPsList = null;
|
|
8
|
+
async function loadPsList() {
|
|
9
|
+
if (cachedPsList) {
|
|
10
|
+
return cachedPsList;
|
|
11
|
+
}
|
|
12
|
+
const module = await importPsList('ps-list');
|
|
13
|
+
cachedPsList = module.default;
|
|
14
|
+
return cachedPsList;
|
|
15
|
+
}
|
|
16
|
+
async function listProcesses() {
|
|
17
|
+
const psList = await loadPsList();
|
|
18
|
+
return await psList();
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=ps-list-loader.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type ObservedAgentToolName = 'list_workspace_files' | 'read_workspace_file' | 'search_workspace_text' | 'run_workspace_command';
|
|
2
|
+
export type ObservedAgentToolDecision = 'allowed' | 'blocked' | 'timed_out' | 'error';
|
|
3
|
+
export interface ObservedAgentToolCallInput {
|
|
4
|
+
workspaceRoot: string;
|
|
5
|
+
toolName: ObservedAgentToolName;
|
|
6
|
+
path?: string | null;
|
|
7
|
+
query?: string | null;
|
|
8
|
+
command?: string | null;
|
|
9
|
+
args?: string[];
|
|
10
|
+
limit?: number | null;
|
|
11
|
+
timeoutMs?: number | null;
|
|
12
|
+
maxBytes?: number | null;
|
|
13
|
+
maxOutputBytes?: number | null;
|
|
14
|
+
}
|
|
15
|
+
export interface ObservedAgentToolMatch {
|
|
16
|
+
path: string;
|
|
17
|
+
line: number;
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ObservedAgentToolCallOutput {
|
|
21
|
+
files?: string[];
|
|
22
|
+
content?: string;
|
|
23
|
+
matches?: ObservedAgentToolMatch[];
|
|
24
|
+
stdout?: string;
|
|
25
|
+
stderr?: string;
|
|
26
|
+
exitCode?: number | null;
|
|
27
|
+
truncated?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ObservedAgentToolCallResult {
|
|
30
|
+
decision: ObservedAgentToolDecision;
|
|
31
|
+
toolName: ObservedAgentToolName;
|
|
32
|
+
startedAt: string;
|
|
33
|
+
finishedAt: string;
|
|
34
|
+
durationMs: number;
|
|
35
|
+
blockedReason: string | null;
|
|
36
|
+
metadata: Record<string, unknown>;
|
|
37
|
+
output: ObservedAgentToolCallOutput;
|
|
38
|
+
}
|
|
39
|
+
export declare function normalizeObservationWorkspaceRoot(workspaceRoot: string): string;
|
|
40
|
+
export declare function hashObservationWorkspaceRoot(workspaceRoot: string): string;
|
|
41
|
+
export declare function runObservedAgentToolCall(input: ObservedAgentToolCallInput): Promise<ObservedAgentToolCallResult>;
|
|
42
|
+
//# sourceMappingURL=agent-observation.d.ts.map
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.normalizeObservationWorkspaceRoot = normalizeObservationWorkspaceRoot;
|
|
7
|
+
exports.hashObservationWorkspaceRoot = hashObservationWorkspaceRoot;
|
|
8
|
+
exports.runObservedAgentToolCall = runObservedAgentToolCall;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const DEFAULT_LIST_LIMIT = 200;
|
|
14
|
+
const DEFAULT_SEARCH_LIMIT = 50;
|
|
15
|
+
const DEFAULT_READ_BYTES = 32 * 1024;
|
|
16
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 8_000;
|
|
17
|
+
const DEFAULT_COMMAND_OUTPUT_BYTES = 16 * 1024;
|
|
18
|
+
const MAX_DEPTH = 6;
|
|
19
|
+
const SEARCH_FILE_BYTES_LIMIT = 512 * 1024;
|
|
20
|
+
const SKIPPED_DIRECTORY_NAMES = new Set([
|
|
21
|
+
'.git',
|
|
22
|
+
'node_modules',
|
|
23
|
+
'dist',
|
|
24
|
+
'build',
|
|
25
|
+
'coverage',
|
|
26
|
+
'.next',
|
|
27
|
+
'.turbo',
|
|
28
|
+
]);
|
|
29
|
+
const READ_ONLY_COMMAND_ALLOWLIST = new Set(['git', 'rg', 'cat', 'head', 'tail', 'wc']);
|
|
30
|
+
const GIT_ALLOWED_SUBCOMMANDS = new Set(['status', 'diff', 'log', 'show', 'branch', 'rev-parse']);
|
|
31
|
+
const GIT_BLOCKED_FLAGS = new Set(['-C', '--git-dir', '--work-tree']);
|
|
32
|
+
const RIPGREP_BLOCKED_FLAGS = new Set(['--pre', '--pre-glob']);
|
|
33
|
+
const TAIL_ALLOWED_FLAGS = new Set(['-f', '-n']);
|
|
34
|
+
const HEAD_ALLOWED_FLAGS = new Set(['-n']);
|
|
35
|
+
const WC_ALLOWED_FLAGS = new Set(['-l', '-c', '-m', '-w']);
|
|
36
|
+
function nowIso() {
|
|
37
|
+
return new Date().toISOString();
|
|
38
|
+
}
|
|
39
|
+
function hashText(value) {
|
|
40
|
+
return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
|
|
41
|
+
}
|
|
42
|
+
function sanitizeText(value, maxLength) {
|
|
43
|
+
return value.trim().slice(0, maxLength);
|
|
44
|
+
}
|
|
45
|
+
function boundedInt(value, fallback, min, max) {
|
|
46
|
+
const numeric = Number(value);
|
|
47
|
+
if (!Number.isFinite(numeric))
|
|
48
|
+
return fallback;
|
|
49
|
+
return Math.max(min, Math.min(max, Math.floor(numeric)));
|
|
50
|
+
}
|
|
51
|
+
function pathToPosix(value) {
|
|
52
|
+
return value.split(node_path_1.default.sep).join('/');
|
|
53
|
+
}
|
|
54
|
+
function normalizeObservationWorkspaceRoot(workspaceRoot) {
|
|
55
|
+
const candidate = String(workspaceRoot || '').trim();
|
|
56
|
+
if (!candidate) {
|
|
57
|
+
throw new Error('WORKSPACE_ROOT_REQUIRED');
|
|
58
|
+
}
|
|
59
|
+
const resolved = node_fs_1.default.realpathSync.native(node_path_1.default.resolve(candidate));
|
|
60
|
+
const stats = node_fs_1.default.statSync(resolved);
|
|
61
|
+
if (!stats.isDirectory()) {
|
|
62
|
+
throw new Error('WORKSPACE_ROOT_NOT_DIRECTORY');
|
|
63
|
+
}
|
|
64
|
+
return resolved;
|
|
65
|
+
}
|
|
66
|
+
function hashObservationWorkspaceRoot(workspaceRoot) {
|
|
67
|
+
return hashText(normalizeObservationWorkspaceRoot(workspaceRoot));
|
|
68
|
+
}
|
|
69
|
+
function isInsideWorkspace(workspaceRoot, candidatePath) {
|
|
70
|
+
const relative = node_path_1.default.relative(workspaceRoot, candidatePath);
|
|
71
|
+
return relative === ''
|
|
72
|
+
|| (!relative.startsWith('..') && !node_path_1.default.isAbsolute(relative));
|
|
73
|
+
}
|
|
74
|
+
function resolveScopedPath(workspaceRoot, candidate, allowWorkspaceRoot = true) {
|
|
75
|
+
const raw = String(candidate || '.').trim() || '.';
|
|
76
|
+
const absoluteCandidate = node_path_1.default.resolve(workspaceRoot, raw);
|
|
77
|
+
const absolutePath = node_fs_1.default.realpathSync.native(absoluteCandidate);
|
|
78
|
+
if (!isInsideWorkspace(workspaceRoot, absolutePath)) {
|
|
79
|
+
throw new Error('PATH_OUTSIDE_WORKSPACE');
|
|
80
|
+
}
|
|
81
|
+
const stats = node_fs_1.default.statSync(absolutePath);
|
|
82
|
+
if (!allowWorkspaceRoot && absolutePath === workspaceRoot) {
|
|
83
|
+
throw new Error('FILE_PATH_REQUIRED');
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
absolutePath,
|
|
87
|
+
relativePath: absolutePath === workspaceRoot ? '.' : pathToPosix(node_path_1.default.relative(workspaceRoot, absolutePath)),
|
|
88
|
+
stats,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function collectWorkspaceFiles(workspaceRoot, directoryPath, limit) {
|
|
92
|
+
const results = [];
|
|
93
|
+
let truncated = false;
|
|
94
|
+
function walk(currentPath, depth) {
|
|
95
|
+
if (results.length >= limit || depth > MAX_DEPTH) {
|
|
96
|
+
truncated = truncated || results.length >= limit;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const entries = node_fs_1.default.readdirSync(currentPath, { withFileTypes: true })
|
|
100
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (results.length >= limit) {
|
|
103
|
+
truncated = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (entry.name.startsWith('.DS_Store')) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (entry.isDirectory() && SKIPPED_DIRECTORY_NAMES.has(entry.name)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const absoluteEntryPath = node_path_1.default.join(currentPath, entry.name);
|
|
113
|
+
const relativeEntryPath = absoluteEntryPath === workspaceRoot
|
|
114
|
+
? '.'
|
|
115
|
+
: pathToPosix(node_path_1.default.relative(workspaceRoot, absoluteEntryPath));
|
|
116
|
+
if (entry.isDirectory()) {
|
|
117
|
+
walk(absoluteEntryPath, depth + 1);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (entry.isFile()) {
|
|
121
|
+
results.push(relativeEntryPath);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
walk(directoryPath, 0);
|
|
126
|
+
return {
|
|
127
|
+
files: results,
|
|
128
|
+
truncated,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function readWorkspaceFile(workspaceRoot, candidatePath, maxBytes) {
|
|
132
|
+
const startedAt = nowIso();
|
|
133
|
+
const startedMs = Date.now();
|
|
134
|
+
const { absolutePath, relativePath, stats } = resolveScopedPath(workspaceRoot, candidatePath, false);
|
|
135
|
+
if (!stats.isFile()) {
|
|
136
|
+
throw new Error('FILE_PATH_REQUIRED');
|
|
137
|
+
}
|
|
138
|
+
const raw = node_fs_1.default.readFileSync(absolutePath);
|
|
139
|
+
const truncated = raw.byteLength > maxBytes;
|
|
140
|
+
const content = raw.subarray(0, maxBytes).toString('utf8');
|
|
141
|
+
return {
|
|
142
|
+
decision: 'allowed',
|
|
143
|
+
toolName: 'read_workspace_file',
|
|
144
|
+
startedAt,
|
|
145
|
+
finishedAt: nowIso(),
|
|
146
|
+
durationMs: Date.now() - startedMs,
|
|
147
|
+
blockedReason: null,
|
|
148
|
+
metadata: {
|
|
149
|
+
relative_path_hash: hashText(relativePath),
|
|
150
|
+
relative_path_depth: relativePath === '.' ? 0 : relativePath.split('/').length,
|
|
151
|
+
file_extension: node_path_1.default.extname(relativePath) || null,
|
|
152
|
+
bytes_requested: maxBytes,
|
|
153
|
+
bytes_available: raw.byteLength,
|
|
154
|
+
bytes_returned: Buffer.byteLength(content, 'utf8'),
|
|
155
|
+
},
|
|
156
|
+
output: {
|
|
157
|
+
content,
|
|
158
|
+
truncated,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function searchWorkspaceText(workspaceRoot, candidatePath, query, limit) {
|
|
163
|
+
const startedAt = nowIso();
|
|
164
|
+
const startedMs = Date.now();
|
|
165
|
+
const scopedPath = resolveScopedPath(workspaceRoot, candidatePath, true);
|
|
166
|
+
const matches = [];
|
|
167
|
+
let scannedFiles = 0;
|
|
168
|
+
let truncated = false;
|
|
169
|
+
const searchTargets = scopedPath.stats.isDirectory()
|
|
170
|
+
? collectWorkspaceFiles(workspaceRoot, scopedPath.absolutePath, Math.max(limit * 4, limit))
|
|
171
|
+
.files
|
|
172
|
+
.map((relativePath) => node_path_1.default.join(workspaceRoot, relativePath))
|
|
173
|
+
: [scopedPath.absolutePath];
|
|
174
|
+
for (const absolutePath of searchTargets) {
|
|
175
|
+
if (matches.length >= limit) {
|
|
176
|
+
truncated = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
const stats = node_fs_1.default.statSync(absolutePath);
|
|
180
|
+
if (!stats.isFile() || stats.size > SEARCH_FILE_BYTES_LIMIT) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
scannedFiles += 1;
|
|
184
|
+
const relativePath = absolutePath === workspaceRoot ? '.' : pathToPosix(node_path_1.default.relative(workspaceRoot, absolutePath));
|
|
185
|
+
const content = node_fs_1.default.readFileSync(absolutePath, 'utf8');
|
|
186
|
+
const lines = content.split(/\r?\n/);
|
|
187
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
188
|
+
if (!lines[index]?.includes(query)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
matches.push({
|
|
192
|
+
path: relativePath,
|
|
193
|
+
line: index + 1,
|
|
194
|
+
text: sanitizeText(String(lines[index]), 240),
|
|
195
|
+
});
|
|
196
|
+
if (matches.length >= limit) {
|
|
197
|
+
truncated = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
decision: 'allowed',
|
|
204
|
+
toolName: 'search_workspace_text',
|
|
205
|
+
startedAt,
|
|
206
|
+
finishedAt: nowIso(),
|
|
207
|
+
durationMs: Date.now() - startedMs,
|
|
208
|
+
blockedReason: null,
|
|
209
|
+
metadata: {
|
|
210
|
+
query_hash: hashText(query),
|
|
211
|
+
query_length: query.length,
|
|
212
|
+
relative_path_hash: hashText(scopedPath.relativePath),
|
|
213
|
+
scanned_files: scannedFiles,
|
|
214
|
+
match_count: matches.length,
|
|
215
|
+
},
|
|
216
|
+
output: {
|
|
217
|
+
matches,
|
|
218
|
+
truncated,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function listWorkspaceFiles(workspaceRoot, candidatePath, limit) {
|
|
223
|
+
const startedAt = nowIso();
|
|
224
|
+
const startedMs = Date.now();
|
|
225
|
+
const scopedPath = resolveScopedPath(workspaceRoot, candidatePath, true);
|
|
226
|
+
if (!scopedPath.stats.isDirectory()) {
|
|
227
|
+
throw new Error('DIRECTORY_PATH_REQUIRED');
|
|
228
|
+
}
|
|
229
|
+
const { files, truncated } = collectWorkspaceFiles(workspaceRoot, scopedPath.absolutePath, limit);
|
|
230
|
+
return {
|
|
231
|
+
decision: 'allowed',
|
|
232
|
+
toolName: 'list_workspace_files',
|
|
233
|
+
startedAt,
|
|
234
|
+
finishedAt: nowIso(),
|
|
235
|
+
durationMs: Date.now() - startedMs,
|
|
236
|
+
blockedReason: null,
|
|
237
|
+
metadata: {
|
|
238
|
+
relative_path_hash: hashText(scopedPath.relativePath),
|
|
239
|
+
result_count: files.length,
|
|
240
|
+
},
|
|
241
|
+
output: {
|
|
242
|
+
files,
|
|
243
|
+
truncated,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function validateFlagSet(args, allowedFlags) {
|
|
248
|
+
for (const arg of args) {
|
|
249
|
+
if (!arg.startsWith('-')) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (!allowedFlags.has(arg) && ![...allowedFlags].some((candidate) => arg.startsWith(`${candidate}=`))) {
|
|
253
|
+
throw new Error('COMMAND_FLAG_NOT_ALLOWED');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function maybeValidatePathArgument(workspaceRoot, arg) {
|
|
258
|
+
const trimmed = arg.trim();
|
|
259
|
+
if (!trimmed)
|
|
260
|
+
return;
|
|
261
|
+
if (trimmed.includes('\u0000') || trimmed.includes('\n') || trimmed.includes('\r')) {
|
|
262
|
+
throw new Error('COMMAND_ARGUMENT_INVALID');
|
|
263
|
+
}
|
|
264
|
+
const looksAbsolute = node_path_1.default.isAbsolute(trimmed);
|
|
265
|
+
const looksRelative = trimmed === '.'
|
|
266
|
+
|| trimmed === '..'
|
|
267
|
+
|| trimmed.startsWith(`.${node_path_1.default.sep}`)
|
|
268
|
+
|| trimmed.startsWith(`..${node_path_1.default.sep}`)
|
|
269
|
+
|| trimmed.startsWith('./')
|
|
270
|
+
|| trimmed.startsWith('../');
|
|
271
|
+
const containsPathSeparator = trimmed.includes(node_path_1.default.sep) || trimmed.includes('/');
|
|
272
|
+
const candidateAbsolutePath = node_path_1.default.resolve(workspaceRoot, trimmed);
|
|
273
|
+
const existsInWorkspace = node_fs_1.default.existsSync(candidateAbsolutePath);
|
|
274
|
+
if (!looksAbsolute && !looksRelative && !containsPathSeparator && !existsInWorkspace) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const pathToCheck = node_fs_1.default.existsSync(candidateAbsolutePath)
|
|
278
|
+
? node_fs_1.default.realpathSync.native(candidateAbsolutePath)
|
|
279
|
+
: candidateAbsolutePath;
|
|
280
|
+
if (!isInsideWorkspace(workspaceRoot, pathToCheck)) {
|
|
281
|
+
throw new Error('COMMAND_PATH_OUTSIDE_WORKSPACE');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function validateWorkspaceCommand(workspaceRoot, command, args) {
|
|
285
|
+
if (!READ_ONLY_COMMAND_ALLOWLIST.has(command)) {
|
|
286
|
+
throw new Error('COMMAND_NOT_ALLOWED');
|
|
287
|
+
}
|
|
288
|
+
if (command === 'git') {
|
|
289
|
+
const subcommand = args.find((arg) => !arg.startsWith('-'));
|
|
290
|
+
if (!subcommand || !GIT_ALLOWED_SUBCOMMANDS.has(subcommand)) {
|
|
291
|
+
throw new Error('COMMAND_NOT_ALLOWED');
|
|
292
|
+
}
|
|
293
|
+
for (const arg of args) {
|
|
294
|
+
if (GIT_BLOCKED_FLAGS.has(arg) || arg.startsWith('--git-dir=') || arg.startsWith('--work-tree=')) {
|
|
295
|
+
throw new Error('COMMAND_FLAG_NOT_ALLOWED');
|
|
296
|
+
}
|
|
297
|
+
maybeValidatePathArgument(workspaceRoot, arg);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (command === 'rg') {
|
|
302
|
+
for (const arg of args) {
|
|
303
|
+
if (RIPGREP_BLOCKED_FLAGS.has(arg) || arg.startsWith('--pre=')) {
|
|
304
|
+
throw new Error('COMMAND_FLAG_NOT_ALLOWED');
|
|
305
|
+
}
|
|
306
|
+
maybeValidatePathArgument(workspaceRoot, arg);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (command === 'tail') {
|
|
311
|
+
validateFlagSet(args.filter((arg) => arg.startsWith('-')), TAIL_ALLOWED_FLAGS);
|
|
312
|
+
}
|
|
313
|
+
else if (command === 'head') {
|
|
314
|
+
validateFlagSet(args.filter((arg) => arg.startsWith('-')), HEAD_ALLOWED_FLAGS);
|
|
315
|
+
}
|
|
316
|
+
else if (command === 'wc') {
|
|
317
|
+
validateFlagSet(args.filter((arg) => arg.startsWith('-')), WC_ALLOWED_FLAGS);
|
|
318
|
+
}
|
|
319
|
+
else if (command === 'cat' && args.some((arg) => arg.startsWith('-'))) {
|
|
320
|
+
throw new Error('COMMAND_FLAG_NOT_ALLOWED');
|
|
321
|
+
}
|
|
322
|
+
for (const arg of args) {
|
|
323
|
+
if (arg.startsWith('-')) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
maybeValidatePathArgument(workspaceRoot, arg);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function runWorkspaceCommand(workspaceRoot, command, args, timeoutMs, maxOutputBytes) {
|
|
330
|
+
const startedAt = nowIso();
|
|
331
|
+
const startedMs = Date.now();
|
|
332
|
+
validateWorkspaceCommand(workspaceRoot, command, args);
|
|
333
|
+
return new Promise((resolve) => {
|
|
334
|
+
const child = (0, node_child_process_1.spawn)(command, args, {
|
|
335
|
+
cwd: workspaceRoot,
|
|
336
|
+
env: {
|
|
337
|
+
...process.env,
|
|
338
|
+
CI: process.env.CI || '1',
|
|
339
|
+
},
|
|
340
|
+
shell: false,
|
|
341
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
342
|
+
});
|
|
343
|
+
let stdout = '';
|
|
344
|
+
let stderr = '';
|
|
345
|
+
let stdoutBytes = 0;
|
|
346
|
+
let stderrBytes = 0;
|
|
347
|
+
let truncated = false;
|
|
348
|
+
let finished = false;
|
|
349
|
+
let timedOut = false;
|
|
350
|
+
const capture = (buffer, current, currentBytes) => {
|
|
351
|
+
if (currentBytes >= maxOutputBytes) {
|
|
352
|
+
truncated = true;
|
|
353
|
+
return { next: current, nextBytes: currentBytes };
|
|
354
|
+
}
|
|
355
|
+
const remainingBytes = maxOutputBytes - currentBytes;
|
|
356
|
+
const slice = buffer.subarray(0, remainingBytes);
|
|
357
|
+
if (slice.byteLength < buffer.byteLength) {
|
|
358
|
+
truncated = true;
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
next: current + slice.toString('utf8'),
|
|
362
|
+
nextBytes: currentBytes + slice.byteLength,
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
const timer = setTimeout(() => {
|
|
366
|
+
timedOut = true;
|
|
367
|
+
child.kill('SIGTERM');
|
|
368
|
+
setTimeout(() => child.kill('SIGKILL'), 250).unref();
|
|
369
|
+
}, timeoutMs);
|
|
370
|
+
child.stdout.on('data', (chunk) => {
|
|
371
|
+
const captured = capture(chunk, stdout, stdoutBytes);
|
|
372
|
+
stdout = captured.next;
|
|
373
|
+
stdoutBytes = captured.nextBytes;
|
|
374
|
+
});
|
|
375
|
+
child.stderr.on('data', (chunk) => {
|
|
376
|
+
const captured = capture(chunk, stderr, stderrBytes);
|
|
377
|
+
stderr = captured.next;
|
|
378
|
+
stderrBytes = captured.nextBytes;
|
|
379
|
+
});
|
|
380
|
+
child.on('error', (error) => {
|
|
381
|
+
if (finished)
|
|
382
|
+
return;
|
|
383
|
+
finished = true;
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
resolve({
|
|
386
|
+
decision: 'error',
|
|
387
|
+
toolName: 'run_workspace_command',
|
|
388
|
+
startedAt,
|
|
389
|
+
finishedAt: nowIso(),
|
|
390
|
+
durationMs: Date.now() - startedMs,
|
|
391
|
+
blockedReason: sanitizeText(error.message || 'Command execution failed.', 240),
|
|
392
|
+
metadata: {
|
|
393
|
+
command,
|
|
394
|
+
args_count: args.length,
|
|
395
|
+
command_hash: hashText([command, ...args].join('\u001f')),
|
|
396
|
+
},
|
|
397
|
+
output: {
|
|
398
|
+
stdout,
|
|
399
|
+
stderr,
|
|
400
|
+
exitCode: null,
|
|
401
|
+
truncated,
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
child.on('close', (code) => {
|
|
406
|
+
if (finished)
|
|
407
|
+
return;
|
|
408
|
+
finished = true;
|
|
409
|
+
clearTimeout(timer);
|
|
410
|
+
resolve({
|
|
411
|
+
decision: timedOut ? 'timed_out' : 'allowed',
|
|
412
|
+
toolName: 'run_workspace_command',
|
|
413
|
+
startedAt,
|
|
414
|
+
finishedAt: nowIso(),
|
|
415
|
+
durationMs: Date.now() - startedMs,
|
|
416
|
+
blockedReason: timedOut ? 'COMMAND_TIMED_OUT' : null,
|
|
417
|
+
metadata: {
|
|
418
|
+
command,
|
|
419
|
+
args_count: args.length,
|
|
420
|
+
command_hash: hashText([command, ...args].join('\u001f')),
|
|
421
|
+
timeout_ms: timeoutMs,
|
|
422
|
+
stdout_bytes: stdoutBytes,
|
|
423
|
+
stderr_bytes: stderrBytes,
|
|
424
|
+
},
|
|
425
|
+
output: {
|
|
426
|
+
stdout,
|
|
427
|
+
stderr,
|
|
428
|
+
exitCode: code,
|
|
429
|
+
truncated,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function blockedResult(toolName, blockedReason, metadata = {}) {
|
|
436
|
+
const startedAt = nowIso();
|
|
437
|
+
return {
|
|
438
|
+
decision: 'blocked',
|
|
439
|
+
toolName,
|
|
440
|
+
startedAt,
|
|
441
|
+
finishedAt: startedAt,
|
|
442
|
+
durationMs: 0,
|
|
443
|
+
blockedReason,
|
|
444
|
+
metadata,
|
|
445
|
+
output: {},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function runObservedAgentToolCall(input) {
|
|
449
|
+
const workspaceRoot = normalizeObservationWorkspaceRoot(input.workspaceRoot);
|
|
450
|
+
try {
|
|
451
|
+
switch (input.toolName) {
|
|
452
|
+
case 'list_workspace_files':
|
|
453
|
+
return listWorkspaceFiles(workspaceRoot, input.path ?? '.', boundedInt(input.limit, DEFAULT_LIST_LIMIT, 1, 500));
|
|
454
|
+
case 'read_workspace_file':
|
|
455
|
+
return readWorkspaceFile(workspaceRoot, input.path, boundedInt(input.maxBytes, DEFAULT_READ_BYTES, 256, 256 * 1024));
|
|
456
|
+
case 'search_workspace_text': {
|
|
457
|
+
const query = String(input.query || '').trim();
|
|
458
|
+
if (!query) {
|
|
459
|
+
return blockedResult('search_workspace_text', 'SEARCH_QUERY_REQUIRED');
|
|
460
|
+
}
|
|
461
|
+
return searchWorkspaceText(workspaceRoot, input.path ?? '.', query, boundedInt(input.limit, DEFAULT_SEARCH_LIMIT, 1, 200));
|
|
462
|
+
}
|
|
463
|
+
case 'run_workspace_command': {
|
|
464
|
+
const command = String(input.command || '').trim();
|
|
465
|
+
if (!command) {
|
|
466
|
+
return blockedResult('run_workspace_command', 'COMMAND_REQUIRED');
|
|
467
|
+
}
|
|
468
|
+
return runWorkspaceCommand(workspaceRoot, command, Array.isArray(input.args) ? input.args.map((arg) => String(arg)) : [], boundedInt(input.timeoutMs, DEFAULT_COMMAND_TIMEOUT_MS, 250, 30_000), boundedInt(input.maxOutputBytes, DEFAULT_COMMAND_OUTPUT_BYTES, 512, 128 * 1024));
|
|
469
|
+
}
|
|
470
|
+
default:
|
|
471
|
+
return blockedResult(input.toolName, 'TOOL_NOT_SUPPORTED');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
476
|
+
const isBlocked = [
|
|
477
|
+
'WORKSPACE_ROOT_REQUIRED',
|
|
478
|
+
'WORKSPACE_ROOT_NOT_DIRECTORY',
|
|
479
|
+
'PATH_OUTSIDE_WORKSPACE',
|
|
480
|
+
'FILE_PATH_REQUIRED',
|
|
481
|
+
'DIRECTORY_PATH_REQUIRED',
|
|
482
|
+
'COMMAND_NOT_ALLOWED',
|
|
483
|
+
'COMMAND_FLAG_NOT_ALLOWED',
|
|
484
|
+
'COMMAND_ARGUMENT_INVALID',
|
|
485
|
+
'COMMAND_PATH_OUTSIDE_WORKSPACE',
|
|
486
|
+
].includes(message);
|
|
487
|
+
return {
|
|
488
|
+
decision: isBlocked ? 'blocked' : 'error',
|
|
489
|
+
toolName: input.toolName,
|
|
490
|
+
startedAt: nowIso(),
|
|
491
|
+
finishedAt: nowIso(),
|
|
492
|
+
durationMs: 0,
|
|
493
|
+
blockedReason: isBlocked ? message : sanitizeText(message, 240),
|
|
494
|
+
metadata: {},
|
|
495
|
+
output: {},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=agent-observation.js.map
|