@kiturone/kapibala 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/index.cjs +1859 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +354 -22
- package/dist/index.d.ts +354 -22
- package/dist/index.js +1841 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -35,32 +35,50 @@ __export(index_exports, {
|
|
|
35
35
|
AgentSession: () => AgentSession,
|
|
36
36
|
DEFAULT_CONTEXT_WINDOW_TOKENS: () => DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
37
37
|
FatalError: () => FatalError,
|
|
38
|
+
FileLogSink: () => FileLogSink,
|
|
38
39
|
HookRegistry: () => HookRegistry,
|
|
39
40
|
JSONLMessageStore: () => JSONLMessageStore,
|
|
40
41
|
KapibalaError: () => KapibalaError,
|
|
41
42
|
ModelError: () => ModelError,
|
|
42
43
|
OpenAICompatibleProvider: () => OpenAICompatibleProvider,
|
|
43
44
|
PathSandbox: () => PathSandbox,
|
|
45
|
+
PermissionPolicy: () => PermissionPolicy,
|
|
44
46
|
PromptAssembler: () => PromptAssembler,
|
|
47
|
+
SessionApprovalCache: () => SessionApprovalCache,
|
|
45
48
|
SessionBusyError: () => SessionBusyError,
|
|
46
49
|
SimpleModelRouter: () => SimpleModelRouter,
|
|
50
|
+
StructuredLogger: () => StructuredLogger,
|
|
47
51
|
ToolError: () => ToolError,
|
|
48
52
|
ToolExecutor: () => ToolExecutor,
|
|
49
53
|
ToolNotFound: () => ToolNotFound,
|
|
50
54
|
ToolRegistry: () => ToolRegistry,
|
|
51
55
|
TransportError: () => TransportError,
|
|
52
56
|
builtinTools: () => builtinTools,
|
|
57
|
+
classifyBashOutput: () => classifyBashOutput,
|
|
58
|
+
cleanupCommandResults: () => cleanupCommandResults,
|
|
59
|
+
commandAuditSummary: () => commandAuditSummary,
|
|
60
|
+
createDefaultLogSinks: () => createDefaultLogSinks,
|
|
61
|
+
createLogId: () => createLogId,
|
|
62
|
+
createRunCommandTool: () => createRunCommandTool,
|
|
53
63
|
defineTool: () => defineTool,
|
|
64
|
+
detectShell: () => detectShell,
|
|
54
65
|
editFileTool: () => editFileTool,
|
|
66
|
+
executionFingerprint: () => executionFingerprint,
|
|
55
67
|
globTool: () => globTool,
|
|
56
68
|
grepTool: () => grepTool,
|
|
57
69
|
isTextBlock: () => isTextBlock,
|
|
58
70
|
isThinkingBlock: () => isThinkingBlock,
|
|
59
71
|
isToolResultBlock: () => isToolResultBlock,
|
|
60
72
|
isToolUseBlock: () => isToolUseBlock,
|
|
73
|
+
loadInstructions: () => loadInstructions,
|
|
74
|
+
projectSafeFields: () => projectSafeFields,
|
|
61
75
|
readFileTool: () => readFileTool,
|
|
76
|
+
recoverIncompleteAudit: () => recoverIncompleteAudit,
|
|
62
77
|
resolveContextWindow: () => resolveContextWindow,
|
|
78
|
+
stableJsonStringify: () => stableJsonStringify,
|
|
63
79
|
toToolDefinition: () => toToolDefinition,
|
|
80
|
+
validatePermissionRules: () => validatePermissionRules,
|
|
81
|
+
visibleTools: () => visibleTools,
|
|
64
82
|
writeFileTool: () => writeFileTool
|
|
65
83
|
});
|
|
66
84
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -81,9 +99,15 @@ function isThinkingBlock(block) {
|
|
|
81
99
|
|
|
82
100
|
// src/errors/index.ts
|
|
83
101
|
var KapibalaError = class extends Error {
|
|
84
|
-
|
|
102
|
+
code;
|
|
103
|
+
retryPolicy;
|
|
104
|
+
safeMessage;
|
|
105
|
+
constructor(message, options) {
|
|
85
106
|
super(message);
|
|
86
107
|
this.name = "KapibalaError";
|
|
108
|
+
this.code = options?.code ?? "INTERNAL_ERROR";
|
|
109
|
+
this.retryPolicy = options?.retryPolicy ?? "never";
|
|
110
|
+
this.safeMessage = options?.safeMessage ?? "Operation failed";
|
|
87
111
|
}
|
|
88
112
|
};
|
|
89
113
|
var ModelError = class extends KapibalaError {
|
|
@@ -106,7 +130,11 @@ var TransportError = class extends KapibalaError {
|
|
|
106
130
|
};
|
|
107
131
|
var ToolError = class extends KapibalaError {
|
|
108
132
|
constructor(message) {
|
|
109
|
-
super(message
|
|
133
|
+
super(message, {
|
|
134
|
+
code: "TOOL_ERROR",
|
|
135
|
+
retryPolicy: "never",
|
|
136
|
+
safeMessage: "Tool execution failed"
|
|
137
|
+
});
|
|
110
138
|
this.name = "ToolError";
|
|
111
139
|
}
|
|
112
140
|
};
|
|
@@ -114,7 +142,11 @@ var ToolNotFound = class extends KapibalaError {
|
|
|
114
142
|
toolName;
|
|
115
143
|
availableTools;
|
|
116
144
|
constructor(toolName, availableTools = []) {
|
|
117
|
-
super(`Tool '${toolName}' not found. Available tools: ${availableTools.join(", ") || "none"}
|
|
145
|
+
super(`Tool '${toolName}' not found. Available tools: ${availableTools.join(", ") || "none"}`, {
|
|
146
|
+
code: "TOOL_NOT_FOUND",
|
|
147
|
+
retryPolicy: "never",
|
|
148
|
+
safeMessage: "Requested tool is unavailable"
|
|
149
|
+
});
|
|
118
150
|
this.name = "ToolNotFound";
|
|
119
151
|
this.toolName = toolName;
|
|
120
152
|
this.availableTools = availableTools;
|
|
@@ -122,7 +154,11 @@ var ToolNotFound = class extends KapibalaError {
|
|
|
122
154
|
};
|
|
123
155
|
var AbortError = class extends KapibalaError {
|
|
124
156
|
constructor(message = "Execution aborted by user") {
|
|
125
|
-
super(message
|
|
157
|
+
super(message, {
|
|
158
|
+
code: "ABORTED",
|
|
159
|
+
retryPolicy: "after_user_action",
|
|
160
|
+
safeMessage: "Operation cancelled"
|
|
161
|
+
});
|
|
126
162
|
this.name = "AbortError";
|
|
127
163
|
}
|
|
128
164
|
};
|
|
@@ -139,12 +175,1028 @@ var SessionBusyError = class extends KapibalaError {
|
|
|
139
175
|
}
|
|
140
176
|
};
|
|
141
177
|
|
|
142
|
-
// src/
|
|
178
|
+
// src/logging/index.ts
|
|
179
|
+
var import_node_crypto = require("crypto");
|
|
180
|
+
var SAFE_FIELDS = /* @__PURE__ */ new Set([
|
|
181
|
+
"mode",
|
|
182
|
+
"toolName",
|
|
183
|
+
"durationMs",
|
|
184
|
+
"decision",
|
|
185
|
+
"decisionSource",
|
|
186
|
+
"ruleSource",
|
|
187
|
+
"approvalChoice",
|
|
188
|
+
"status",
|
|
189
|
+
"phase",
|
|
190
|
+
"errorCode",
|
|
191
|
+
"retryPolicy",
|
|
192
|
+
"reason",
|
|
193
|
+
"shell",
|
|
194
|
+
"executableDigest",
|
|
195
|
+
"cwdDigest",
|
|
196
|
+
"commandDigest",
|
|
197
|
+
"commandPreview",
|
|
198
|
+
"commandLength",
|
|
199
|
+
"argumentCount",
|
|
200
|
+
"executionFingerprint",
|
|
201
|
+
"exitCode",
|
|
202
|
+
"outputBytes",
|
|
203
|
+
"outputTruncated",
|
|
204
|
+
"outputFileTruncated",
|
|
205
|
+
"reclaimed",
|
|
206
|
+
"modelId",
|
|
207
|
+
"provider",
|
|
208
|
+
"promptTokens",
|
|
209
|
+
"completionTokens",
|
|
210
|
+
"totalTokens",
|
|
211
|
+
"attempt",
|
|
212
|
+
"count",
|
|
213
|
+
"pathDigest",
|
|
214
|
+
"source",
|
|
215
|
+
"sessionMode",
|
|
216
|
+
"processId"
|
|
217
|
+
]);
|
|
218
|
+
var SENSITIVE_VALUE = /(?:\bBearer\s+\S+|\b(?:sk|api|key)-[A-Za-z0-9_-]{5,}|(?:api[_-]?key|token|password|secret)\s*[:=]\s*\S+)/i;
|
|
219
|
+
function projectSafeFields(fields) {
|
|
220
|
+
const safe = {};
|
|
221
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
222
|
+
if (!SAFE_FIELDS.has(name)) continue;
|
|
223
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
224
|
+
safe[name] = value;
|
|
225
|
+
} else if (typeof value === "string") {
|
|
226
|
+
const validPreview = name !== "commandPreview" || /^(?:[a-z][a-z0-9-]* \(\d+ args\)|\[redacted\])$/.test(value);
|
|
227
|
+
const validReason = name !== "reason" || /^[a-z][a-z0-9_.-]{0,80}$/i.test(value);
|
|
228
|
+
safe[name] = SENSITIVE_VALUE.test(value) || !validPreview || !validReason || value.includes("\n") || value.includes("\r") ? "[redacted]" : value.slice(0, 512);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return safe;
|
|
232
|
+
}
|
|
233
|
+
function createLogId() {
|
|
234
|
+
return (0, import_node_crypto.randomUUID)();
|
|
235
|
+
}
|
|
236
|
+
function commandAuditSummary(command) {
|
|
237
|
+
const parts = command.trim().split(/\s+/).filter(Boolean);
|
|
238
|
+
const first = parts[0]?.toLowerCase();
|
|
239
|
+
const category = first && (/* @__PURE__ */ new Set([
|
|
240
|
+
"git",
|
|
241
|
+
"pnpm",
|
|
242
|
+
"npm",
|
|
243
|
+
"node",
|
|
244
|
+
"rg",
|
|
245
|
+
"ls",
|
|
246
|
+
"pwd",
|
|
247
|
+
"cat",
|
|
248
|
+
"get-childitem",
|
|
249
|
+
"get-content"
|
|
250
|
+
])).has(first) ? first : void 0;
|
|
251
|
+
return {
|
|
252
|
+
commandDigest: (0, import_node_crypto.createHash)("sha256").update(command, "utf8").digest("hex"),
|
|
253
|
+
commandPreview: category ? `${category} (${Math.max(0, parts.length - 1)} args)` : "[redacted]",
|
|
254
|
+
commandLength: Buffer.byteLength(command, "utf8"),
|
|
255
|
+
argumentCount: Math.max(0, parts.length - 1)
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function executionFingerprint(input) {
|
|
259
|
+
return (0, import_node_crypto.createHash)("sha256").update(
|
|
260
|
+
JSON.stringify([
|
|
261
|
+
input.command,
|
|
262
|
+
input.shell,
|
|
263
|
+
input.executable,
|
|
264
|
+
input.cwd,
|
|
265
|
+
input.environmentDigest,
|
|
266
|
+
input.timeoutMs,
|
|
267
|
+
input.outputLimitBytes,
|
|
268
|
+
input.targetDigest ?? null
|
|
269
|
+
])
|
|
270
|
+
).digest("hex");
|
|
271
|
+
}
|
|
272
|
+
var StructuredLogger = class {
|
|
273
|
+
operationSink;
|
|
274
|
+
auditSink;
|
|
275
|
+
debug;
|
|
276
|
+
onDiagnostic;
|
|
277
|
+
constructor(options) {
|
|
278
|
+
this.operationSink = options.operationSink;
|
|
279
|
+
this.auditSink = options.auditSink;
|
|
280
|
+
this.debug = options.debug ?? false;
|
|
281
|
+
this.onDiagnostic = options.onDiagnostic;
|
|
282
|
+
}
|
|
283
|
+
/** 普通日志写入失败不阻断业务,也不声称持久化成功;诊断不携带原始异常内容。 */
|
|
284
|
+
async record(event) {
|
|
285
|
+
if (!this.debug && (event.level === "debug" || event.level === "trace")) return;
|
|
286
|
+
try {
|
|
287
|
+
await this.operationSink.write(this.format(event, "operation"));
|
|
288
|
+
} catch {
|
|
289
|
+
try {
|
|
290
|
+
this.onDiagnostic?.("Operation log write failed; the event was not persisted");
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** 审计必须成功持久化;异常交由执行器拒绝启动工具或报告已执行操作的结果未知。 */
|
|
296
|
+
async recordAudit(event) {
|
|
297
|
+
await this.auditSink.write(this.format(event, "audit"));
|
|
298
|
+
}
|
|
299
|
+
format(input, channel) {
|
|
300
|
+
return {
|
|
301
|
+
schemaVersion: 1,
|
|
302
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
303
|
+
level: input.level,
|
|
304
|
+
channel,
|
|
305
|
+
event: input.event,
|
|
306
|
+
sessionId: input.sessionId,
|
|
307
|
+
...input.runId === void 0 ? {} : { runId: input.runId },
|
|
308
|
+
...input.operationId === void 0 ? {} : { operationId: input.operationId },
|
|
309
|
+
...input.toolUseId === void 0 ? {} : { toolUseId: input.toolUseId },
|
|
310
|
+
fields: projectSafeFields(input.fields ?? {})
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/logging/file-sink.ts
|
|
143
316
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
317
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
144
318
|
var import_node_path = __toESM(require("path"), 1);
|
|
319
|
+
var import_node_readline = __toESM(require("readline"), 1);
|
|
320
|
+
var FileLogSink = class {
|
|
321
|
+
directory;
|
|
322
|
+
prefix;
|
|
323
|
+
maxBytes;
|
|
324
|
+
retentionDays;
|
|
325
|
+
sync;
|
|
326
|
+
constructor(options) {
|
|
327
|
+
this.directory = import_node_path.default.resolve(options.directory);
|
|
328
|
+
this.prefix = options.prefix;
|
|
329
|
+
this.maxBytes = options.maxBytes ?? 10 * 1024 * 1024;
|
|
330
|
+
this.retentionDays = options.prefix === "audit" ? void 0 : options.retentionDays;
|
|
331
|
+
this.sync = options.prefix === "audit" || (options.sync ?? false);
|
|
332
|
+
}
|
|
333
|
+
async write(event) {
|
|
334
|
+
const line = `${JSON.stringify(event)}
|
|
335
|
+
`;
|
|
336
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
337
|
+
const date = event.timestamp.slice(0, 10);
|
|
338
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new Error("Invalid log timestamp");
|
|
339
|
+
import_node_fs.default.mkdirSync(this.directory, { recursive: true, mode: 448 });
|
|
340
|
+
const filePath = this.chooseFile(date, bytes);
|
|
341
|
+
const descriptor = import_node_fs.default.openSync(filePath, "a", 384);
|
|
342
|
+
try {
|
|
343
|
+
import_node_fs.default.writeSync(descriptor, line, void 0, "utf8");
|
|
344
|
+
if (this.sync) import_node_fs.default.fsyncSync(descriptor);
|
|
345
|
+
} finally {
|
|
346
|
+
import_node_fs.default.closeSync(descriptor);
|
|
347
|
+
}
|
|
348
|
+
if (this.retentionDays !== void 0) this.cleanupExpired(date);
|
|
349
|
+
}
|
|
350
|
+
/** 返回该通道已占用的字节数,供宿主提示长期保留的审计用量。 */
|
|
351
|
+
getUsageBytes() {
|
|
352
|
+
if (!import_node_fs.default.existsSync(this.directory)) return 0;
|
|
353
|
+
return import_node_fs.default.readdirSync(this.directory).filter((name) => this.isOwnedFile(name)).reduce((total, name) => total + import_node_fs.default.statSync(import_node_path.default.join(this.directory, name)).size, 0);
|
|
354
|
+
}
|
|
355
|
+
chooseFile(date, bytes) {
|
|
356
|
+
const names = import_node_fs.default.readdirSync(this.directory).filter((name) => name.startsWith(`${this.prefix}-${date}-`) && this.isOwnedFile(name)).sort();
|
|
357
|
+
const lastName = names.at(-1);
|
|
358
|
+
if (lastName) {
|
|
359
|
+
const lastPath = import_node_path.default.join(this.directory, lastName);
|
|
360
|
+
if (import_node_fs.default.statSync(lastPath).size + bytes <= this.maxBytes) return lastPath;
|
|
361
|
+
}
|
|
362
|
+
const next = lastName ? Number(lastName.slice(-9, -6)) + 1 : 1;
|
|
363
|
+
return import_node_path.default.join(
|
|
364
|
+
this.directory,
|
|
365
|
+
`${this.prefix}-${date}-${String(next).padStart(3, "0")}.jsonl`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
cleanupExpired(currentDate) {
|
|
369
|
+
const current = Date.parse(`${currentDate}T00:00:00.000Z`);
|
|
370
|
+
const cutoff = current - (this.retentionDays ?? 0) * 864e5;
|
|
371
|
+
for (const name of import_node_fs.default.readdirSync(this.directory)) {
|
|
372
|
+
if (!this.isOwnedFile(name)) continue;
|
|
373
|
+
const fileDate = name.slice(this.prefix.length + 1, this.prefix.length + 11);
|
|
374
|
+
if (Date.parse(`${fileDate}T00:00:00.000Z`) < cutoff) {
|
|
375
|
+
import_node_fs.default.unlinkSync(import_node_path.default.join(this.directory, name));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
isOwnedFile(name) {
|
|
380
|
+
return new RegExp(`^${this.prefix}-\\d{4}-\\d{2}-\\d{2}-\\d{3}\\.jsonl$`).test(name);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function createDefaultLogSinks(baseDirectory = import_node_path.default.join(import_node_os.default.homedir(), ".kapibala")) {
|
|
384
|
+
return {
|
|
385
|
+
operationSink: new FileLogSink({
|
|
386
|
+
directory: import_node_path.default.join(baseDirectory, "logs"),
|
|
387
|
+
prefix: "operation",
|
|
388
|
+
retentionDays: 30
|
|
389
|
+
}),
|
|
390
|
+
auditSink: new FileLogSink({
|
|
391
|
+
directory: import_node_path.default.join(baseDirectory, "audit"),
|
|
392
|
+
prefix: "audit",
|
|
393
|
+
sync: true
|
|
394
|
+
})
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async function recoverIncompleteAudit(directory) {
|
|
398
|
+
if (!import_node_fs.default.existsSync(directory)) return 0;
|
|
399
|
+
const files = import_node_fs.default.readdirSync(directory).filter((name) => /^audit-\d{4}-\d{2}-\d{2}-\d{3}\.jsonl$/.test(name)).sort();
|
|
400
|
+
const open = /* @__PURE__ */ new Map();
|
|
401
|
+
for (const name of files) {
|
|
402
|
+
const lines = import_node_readline.default.createInterface({
|
|
403
|
+
input: import_node_fs.default.createReadStream(import_node_path.default.join(directory, name), { encoding: "utf8" }),
|
|
404
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
405
|
+
});
|
|
406
|
+
for await (const line of lines) {
|
|
407
|
+
if (!line) continue;
|
|
408
|
+
let event;
|
|
409
|
+
try {
|
|
410
|
+
event = JSON.parse(line);
|
|
411
|
+
} catch {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (!event.operationId || typeof event.operationId !== "string") continue;
|
|
415
|
+
if (event.event === "tool.requested") open.set(event.operationId, event);
|
|
416
|
+
if (event.event === "tool.finished" || event.event === "audit.outcome_unknown") {
|
|
417
|
+
open.delete(event.operationId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const sink = new FileLogSink({ directory, prefix: "audit" });
|
|
422
|
+
let recovered = 0;
|
|
423
|
+
for (const event of open.values()) {
|
|
424
|
+
const processId = event.fields?.processId;
|
|
425
|
+
if (typeof processId === "number" && Number.isSafeInteger(processId) && processId > 0) {
|
|
426
|
+
try {
|
|
427
|
+
process.kill(processId, 0);
|
|
428
|
+
continue;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (error.code === "EPERM") continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
await sink.write({
|
|
434
|
+
schemaVersion: 1,
|
|
435
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
436
|
+
channel: "audit",
|
|
437
|
+
level: "warn",
|
|
438
|
+
event: "audit.outcome_unknown",
|
|
439
|
+
sessionId: event.sessionId,
|
|
440
|
+
runId: event.runId,
|
|
441
|
+
operationId: event.operationId,
|
|
442
|
+
toolUseId: event.toolUseId,
|
|
443
|
+
fields: { status: "unknown_after_restart" }
|
|
444
|
+
});
|
|
445
|
+
recovered++;
|
|
446
|
+
}
|
|
447
|
+
return recovered;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/instructions/index.ts
|
|
451
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
452
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
453
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
454
|
+
var MAX_FILE_BYTES = 32 * 1024;
|
|
455
|
+
var MAX_TOTAL_BYTES = 128 * 1024;
|
|
456
|
+
var MAX_LEVELS = 16;
|
|
457
|
+
function loadInstructions(options) {
|
|
458
|
+
const root = import_node_fs2.default.realpathSync(options.projectRoot);
|
|
459
|
+
const cwd = import_node_fs2.default.realpathSync(options.cwd);
|
|
460
|
+
const relative = import_node_path2.default.relative(root, cwd);
|
|
461
|
+
if (relative === ".." || relative.startsWith(`..${import_node_path2.default.sep}`) || import_node_path2.default.isAbsolute(relative)) {
|
|
462
|
+
throw new Error("Instruction cwd is outside project root");
|
|
463
|
+
}
|
|
464
|
+
const segments = relative ? relative.split(import_node_path2.default.sep) : [];
|
|
465
|
+
if (segments.length + 1 > MAX_LEVELS)
|
|
466
|
+
throw new Error(`Instruction directory chain exceeds ${MAX_LEVELS} levels`);
|
|
467
|
+
const userFile = options.userFile ?? import_node_path2.default.join(import_node_os2.default.homedir(), ".kapibala", "AGENTS.md");
|
|
468
|
+
const candidates = [userFile];
|
|
469
|
+
let directory = root;
|
|
470
|
+
candidates.push(import_node_path2.default.join(directory, "AGENTS.md"));
|
|
471
|
+
for (const segment of segments) {
|
|
472
|
+
directory = import_node_path2.default.join(directory, segment);
|
|
473
|
+
candidates.push(import_node_path2.default.join(directory, "AGENTS.md"));
|
|
474
|
+
}
|
|
475
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
476
|
+
const uniqueCandidates = [];
|
|
477
|
+
for (const file of candidates) {
|
|
478
|
+
const resolved = import_node_path2.default.resolve(file);
|
|
479
|
+
const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
480
|
+
if (!seenPaths.has(key)) {
|
|
481
|
+
seenPaths.add(key);
|
|
482
|
+
uniqueCandidates.push(file);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const sources = [];
|
|
486
|
+
let totalBytes = 0;
|
|
487
|
+
for (const file of uniqueCandidates) {
|
|
488
|
+
let stat;
|
|
489
|
+
try {
|
|
490
|
+
stat = import_node_fs2.default.lstatSync(file);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error.code === "ENOENT") continue;
|
|
493
|
+
throw new Error(`Cannot inspect instructions at ${file}`, { cause: error });
|
|
494
|
+
}
|
|
495
|
+
if (stat.isSymbolicLink())
|
|
496
|
+
throw new Error(`Instruction symbolic link is not accepted: ${file}`);
|
|
497
|
+
if (!stat.isFile()) throw new Error(`Instruction source is not a regular file: ${file}`);
|
|
498
|
+
if (stat.size > MAX_FILE_BYTES) throw new Error(`Instruction ${file} exceeds 32 KiB`);
|
|
499
|
+
totalBytes += stat.size;
|
|
500
|
+
if (totalBytes > MAX_TOTAL_BYTES) throw new Error("Instructions exceed 128 KiB total");
|
|
501
|
+
let bytes;
|
|
502
|
+
try {
|
|
503
|
+
bytes = import_node_fs2.default.readFileSync(file);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
throw new Error(`Cannot read instructions at ${file}`, { cause: error });
|
|
506
|
+
}
|
|
507
|
+
if (bytes.byteLength > MAX_FILE_BYTES) throw new Error(`Instruction ${file} exceeds 32 KiB`);
|
|
508
|
+
totalBytes += bytes.byteLength - stat.size;
|
|
509
|
+
if (totalBytes > MAX_TOTAL_BYTES) throw new Error("Instructions exceed 128 KiB total");
|
|
510
|
+
let content;
|
|
511
|
+
try {
|
|
512
|
+
content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
throw new Error(`Instruction ${file} is not valid UTF-8`, { cause: error });
|
|
515
|
+
}
|
|
516
|
+
sources.push({ path: file, content });
|
|
517
|
+
}
|
|
518
|
+
return { sources, totalBytes };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/shell/detect.ts
|
|
522
|
+
var import_node_child_process = require("child_process");
|
|
523
|
+
var import_node_crypto2 = require("crypto");
|
|
524
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
525
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
526
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
527
|
+
function classifyBashOutput(unameOutput, platform) {
|
|
528
|
+
const uname = unameOutput.trim().split(/\r?\n/)[0]?.toLowerCase() ?? "";
|
|
529
|
+
if (platform === "win32") {
|
|
530
|
+
if (uname.startsWith("mingw") || uname.startsWith("msys") || uname.startsWith("cygwin")) {
|
|
531
|
+
return "bash";
|
|
532
|
+
}
|
|
533
|
+
return uname === "linux" ? "wsl" : void 0;
|
|
534
|
+
}
|
|
535
|
+
return uname ? "bash" : void 0;
|
|
536
|
+
}
|
|
537
|
+
function findExecutables(name, pathEnv, platform) {
|
|
538
|
+
const extension = platform === "win32" && !name.toLowerCase().endsWith(".exe") ? ".exe" : "";
|
|
539
|
+
const found = [];
|
|
540
|
+
const seen = /* @__PURE__ */ new Set();
|
|
541
|
+
for (const directory of pathEnv.split(import_node_path3.default.delimiter)) {
|
|
542
|
+
if (!directory) continue;
|
|
543
|
+
const candidate = import_node_path3.default.join(directory, name + extension);
|
|
544
|
+
try {
|
|
545
|
+
if (!import_node_fs3.default.statSync(candidate).isFile()) continue;
|
|
546
|
+
const real = import_node_fs3.default.realpathSync(candidate);
|
|
547
|
+
const key = platform === "win32" ? real.toLowerCase() : real;
|
|
548
|
+
if (seen.has(key)) continue;
|
|
549
|
+
seen.add(key);
|
|
550
|
+
found.push(real);
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return found;
|
|
555
|
+
}
|
|
556
|
+
function probeBash(executable, cwd, platform) {
|
|
557
|
+
const result = (0, import_node_child_process.spawnSync)(executable, ["--noprofile", "--norc", "-c", "uname -s; pwd"], {
|
|
558
|
+
cwd,
|
|
559
|
+
encoding: "utf8",
|
|
560
|
+
timeout: 15e3,
|
|
561
|
+
windowsHide: true
|
|
562
|
+
});
|
|
563
|
+
if (result.status !== 0 || !result.stdout?.trim()) return { ok: false };
|
|
564
|
+
return { ok: true, kind: classifyBashOutput(result.stdout, platform) };
|
|
565
|
+
}
|
|
566
|
+
function probePowerShell(executable, cwd) {
|
|
567
|
+
const result = (0, import_node_child_process.spawnSync)(executable, ["-NoProfile", "-NonInteractive", "-Command", "$PWD.Path"], {
|
|
568
|
+
cwd,
|
|
569
|
+
encoding: "utf8",
|
|
570
|
+
timeout: 15e3,
|
|
571
|
+
windowsHide: true
|
|
572
|
+
});
|
|
573
|
+
return result.status === 0 && Boolean(result.stdout?.trim());
|
|
574
|
+
}
|
|
575
|
+
function buildEnvironment(executable, kind, cwd) {
|
|
576
|
+
const stat = import_node_fs3.default.statSync(executable);
|
|
577
|
+
return {
|
|
578
|
+
kind,
|
|
579
|
+
executable,
|
|
580
|
+
cwd,
|
|
581
|
+
executableDigest: (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([executable, stat.size, stat.mtimeMs])).digest("hex")
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function candidatesFor(kind, platform, pathEnv) {
|
|
585
|
+
if (kind === "bash" || kind === "wsl") {
|
|
586
|
+
return findExecutables("bash", pathEnv, platform);
|
|
587
|
+
}
|
|
588
|
+
if (platform !== "win32" && kind === "powershell") return [];
|
|
589
|
+
return findExecutables(kind, pathEnv, platform);
|
|
590
|
+
}
|
|
591
|
+
function tryKind(kind, platform, pathEnv, cwd) {
|
|
592
|
+
for (const executable of candidatesFor(kind, platform, pathEnv)) {
|
|
593
|
+
try {
|
|
594
|
+
if (kind === "bash" || kind === "wsl") {
|
|
595
|
+
const probe = probeBash(executable, cwd, platform);
|
|
596
|
+
if (probe.ok && probe.kind === kind) return buildEnvironment(executable, kind, cwd);
|
|
597
|
+
} else if (probePowerShell(executable, cwd)) {
|
|
598
|
+
return buildEnvironment(executable, kind, cwd);
|
|
599
|
+
}
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return void 0;
|
|
604
|
+
}
|
|
605
|
+
function detectExplicitPath(preference, cwd, platform) {
|
|
606
|
+
const executable = import_node_fs3.default.realpathSync(preference);
|
|
607
|
+
if (!import_node_fs3.default.statSync(executable).isFile()) {
|
|
608
|
+
throw new Error(`Requested interpreter path is not a file: ${preference}`);
|
|
609
|
+
}
|
|
610
|
+
const base = import_node_path3.default.basename(executable).toLowerCase();
|
|
611
|
+
if (/^pwsh(\.exe)?$/.test(base) || /^powershell(_ise)?(\.exe)?$/.test(base)) {
|
|
612
|
+
const kind = base.startsWith("pwsh") ? "pwsh" : "powershell";
|
|
613
|
+
if (platform !== "win32" && kind === "powershell") {
|
|
614
|
+
throw new Error(`Requested ${kind} interpreter is unavailable on this platform`);
|
|
615
|
+
}
|
|
616
|
+
if (!probePowerShell(executable, cwd)) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
`Requested ${kind} interpreter is unavailable or incompatible with this working directory`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return buildEnvironment(executable, kind, cwd);
|
|
622
|
+
}
|
|
623
|
+
const probe = probeBash(executable, cwd, platform);
|
|
624
|
+
if (!probe.ok || !probe.kind) {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`Requested interpreter is unavailable or incompatible with this working directory: ${preference}`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return buildEnvironment(executable, probe.kind, cwd);
|
|
630
|
+
}
|
|
631
|
+
function detectShell(options) {
|
|
632
|
+
const platform = options.platform ?? import_node_os3.default.platform();
|
|
633
|
+
const cwd = import_node_fs3.default.realpathSync(options.cwd);
|
|
634
|
+
const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
|
|
635
|
+
const preference = options.preference ?? "auto";
|
|
636
|
+
if (/[\\/]/.test(preference) || preference.toLowerCase().endsWith(".exe")) {
|
|
637
|
+
return detectExplicitPath(preference, cwd, platform);
|
|
638
|
+
}
|
|
639
|
+
const kinds = preference === "auto" ? platform === "win32" ? ["bash", "wsl", "pwsh", "powershell"] : ["bash"] : [preference];
|
|
640
|
+
for (const kind of kinds) {
|
|
641
|
+
const environment = tryKind(kind, platform, pathEnv, cwd);
|
|
642
|
+
if (environment) return environment;
|
|
643
|
+
}
|
|
644
|
+
throw new Error(
|
|
645
|
+
preference === "auto" ? "No usable Bash or PowerShell interpreter found" : `Requested ${preference} interpreter is unavailable or incompatible with this working directory`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/shell/tool.ts
|
|
650
|
+
var import_node_child_process2 = require("child_process");
|
|
651
|
+
var import_node_crypto3 = require("crypto");
|
|
652
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
653
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
654
|
+
var import_node_string_decoder = require("string_decoder");
|
|
655
|
+
|
|
656
|
+
// src/tools/index.ts
|
|
657
|
+
function defineTool(tool) {
|
|
658
|
+
return tool;
|
|
659
|
+
}
|
|
660
|
+
function toToolDefinition(tool) {
|
|
661
|
+
return {
|
|
662
|
+
name: tool.name,
|
|
663
|
+
description: tool.description,
|
|
664
|
+
parameters: tool.parameters
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/shell/tool.ts
|
|
669
|
+
var INLINE_LIMIT = 64 * 1024;
|
|
670
|
+
var FILE_LIMIT = 16 * 1024 * 1024;
|
|
671
|
+
var DEFAULT_TIMEOUT = 12e4;
|
|
672
|
+
var MAX_TIMEOUT = 6e5;
|
|
673
|
+
var MAX_COMMAND_BYTES = 8 * 1024;
|
|
674
|
+
var OWNED_RESULT = /^run-[a-f0-9-]{36}\.txt$/;
|
|
675
|
+
var RESULT_MANIFEST = "managed-results.jsonl";
|
|
676
|
+
function limitedEnvironment(kind) {
|
|
677
|
+
const allowed = [
|
|
678
|
+
"PATH",
|
|
679
|
+
"Path",
|
|
680
|
+
"SystemRoot",
|
|
681
|
+
"ComSpec",
|
|
682
|
+
"TEMP",
|
|
683
|
+
"TMP",
|
|
684
|
+
"HOME",
|
|
685
|
+
"USERPROFILE",
|
|
686
|
+
"APPDATA",
|
|
687
|
+
"LOCALAPPDATA",
|
|
688
|
+
"LANG",
|
|
689
|
+
"LC_ALL",
|
|
690
|
+
"TERM"
|
|
691
|
+
];
|
|
692
|
+
const environment = Object.fromEntries(
|
|
693
|
+
allowed.filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]])
|
|
694
|
+
);
|
|
695
|
+
if (kind === "wsl") environment.WSL_UTF8 = "1";
|
|
696
|
+
return environment;
|
|
697
|
+
}
|
|
698
|
+
function utf8Prefix(bytes, limit) {
|
|
699
|
+
let end = Math.min(bytes.length, limit);
|
|
700
|
+
let start = end - 1;
|
|
701
|
+
while (start >= 0 && (bytes[start] & 192) === 128) start--;
|
|
702
|
+
if (start >= 0) {
|
|
703
|
+
const lead = bytes[start];
|
|
704
|
+
const length = lead >= 240 ? 4 : lead >= 224 ? 3 : lead >= 192 ? 2 : 1;
|
|
705
|
+
if (start + length > end) end = start;
|
|
706
|
+
}
|
|
707
|
+
return bytes.subarray(0, end);
|
|
708
|
+
}
|
|
709
|
+
function effectiveCwd(input, rootDir) {
|
|
710
|
+
const requested = input.cwd ? import_node_path4.default.resolve(rootDir, input.cwd) : rootDir;
|
|
711
|
+
const real = import_node_fs4.default.realpathSync(requested);
|
|
712
|
+
if (!import_node_fs4.default.statSync(real).isDirectory())
|
|
713
|
+
throw new Error("Command working directory is not a directory");
|
|
714
|
+
return real;
|
|
715
|
+
}
|
|
716
|
+
function effectiveTimeout(input) {
|
|
717
|
+
const timeout = input.timeout_ms ?? DEFAULT_TIMEOUT;
|
|
718
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > MAX_TIMEOUT) {
|
|
719
|
+
throw new Error(`Command timeout must be 1-${MAX_TIMEOUT} ms`);
|
|
720
|
+
}
|
|
721
|
+
return timeout;
|
|
722
|
+
}
|
|
723
|
+
function validateCommand(input) {
|
|
724
|
+
if (typeof input.command !== "string" || input.command.trim().length === 0 || Buffer.byteLength(input.command, "utf8") > MAX_COMMAND_BYTES) {
|
|
725
|
+
throw new Error(`Command must be nonempty and at most ${MAX_COMMAND_BYTES} bytes`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function directScriptDigest(command, kind, cwd) {
|
|
729
|
+
const bashLike = kind === "bash" || kind === "wsl";
|
|
730
|
+
if (bashLike && command.includes("\\")) return void 0;
|
|
731
|
+
const normalized = command.trim().replaceAll("\\", "/");
|
|
732
|
+
const match = bashLike ? /^(?:bash )?\.\/([A-Za-z0-9_./-]+\.sh)$/.exec(normalized) : /^\.\/([A-Za-z0-9_./-]+\.ps1)$/i.exec(normalized);
|
|
733
|
+
if (!match) return void 0;
|
|
734
|
+
try {
|
|
735
|
+
const script = import_node_fs4.default.realpathSync(import_node_path4.default.resolve(cwd, match[1]));
|
|
736
|
+
const relative = import_node_path4.default.relative(cwd, script);
|
|
737
|
+
if (relative.startsWith("..") || import_node_path4.default.isAbsolute(relative)) return void 0;
|
|
738
|
+
const stat = import_node_fs4.default.statSync(script);
|
|
739
|
+
if (!stat.isFile() || stat.size > 1024 * 1024) return void 0;
|
|
740
|
+
return (0, import_node_crypto3.createHash)("sha256").update(script).update("\0").update(import_node_fs4.default.readFileSync(script)).digest("hex");
|
|
741
|
+
} catch {
|
|
742
|
+
return void 0;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
function interpreterArgs(environment, command) {
|
|
746
|
+
if (environment.kind === "bash" || environment.kind === "wsl") {
|
|
747
|
+
return ["--noprofile", "--norc", "-c", command];
|
|
748
|
+
}
|
|
749
|
+
const utf8 = "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $OutputEncoding = [Console]::OutputEncoding; ";
|
|
750
|
+
return ["-NoProfile", "-NonInteractive", "-Command", utf8 + command];
|
|
751
|
+
}
|
|
752
|
+
function getTaskkillPath() {
|
|
753
|
+
if (process.platform !== "win32") return "taskkill";
|
|
754
|
+
const systemRoot = process.env.SystemRoot ?? process.env.windir ?? "C:\\Windows";
|
|
755
|
+
const candidate = import_node_path4.default.join(systemRoot, "System32", "taskkill.exe");
|
|
756
|
+
try {
|
|
757
|
+
if (import_node_fs4.default.existsSync(candidate)) return candidate;
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
760
|
+
return "taskkill.exe";
|
|
761
|
+
}
|
|
762
|
+
function createNativeBashGroup(environment) {
|
|
763
|
+
if (process.platform !== "win32" || environment.kind !== "bash") return void 0;
|
|
764
|
+
let resolveReady = () => {
|
|
765
|
+
};
|
|
766
|
+
let pending = "";
|
|
767
|
+
let accepted = false;
|
|
768
|
+
const group = {
|
|
769
|
+
environment,
|
|
770
|
+
token: (0, import_node_crypto3.randomUUID)(),
|
|
771
|
+
ready: new Promise((resolve) => {
|
|
772
|
+
resolveReady = resolve;
|
|
773
|
+
}),
|
|
774
|
+
accept(text) {
|
|
775
|
+
if (accepted) return text;
|
|
776
|
+
pending += text;
|
|
777
|
+
const newline = pending.indexOf("\n");
|
|
778
|
+
if (newline < 0) return "";
|
|
779
|
+
accepted = true;
|
|
780
|
+
const prefix = `${group.token}:`;
|
|
781
|
+
const line = pending.slice(0, newline).trimEnd();
|
|
782
|
+
const pid = line.startsWith(prefix) ? Number(line.slice(prefix.length)) : Number.NaN;
|
|
783
|
+
if (Number.isSafeInteger(pid) && pid > 0) group.pid = pid;
|
|
784
|
+
resolveReady();
|
|
785
|
+
return group.pid ? pending.slice(newline + 1) : pending;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
return group;
|
|
789
|
+
}
|
|
790
|
+
function signalNativeBashGroup(group, signal) {
|
|
791
|
+
if (!group.pid) return false;
|
|
792
|
+
const result = (0, import_node_child_process2.spawnSync)(
|
|
793
|
+
group.environment.executable,
|
|
794
|
+
["--noprofile", "--norc", "-c", `kill -${signal} -- -${group.pid}`],
|
|
795
|
+
{ windowsHide: true, timeout: 3e3, env: limitedEnvironment("bash") }
|
|
796
|
+
);
|
|
797
|
+
return result.status === 0;
|
|
798
|
+
}
|
|
799
|
+
function nativeBashGroupStopped(group) {
|
|
800
|
+
if (!group.pid) return false;
|
|
801
|
+
const result = (0, import_node_child_process2.spawnSync)(group.environment.executable, ["--noprofile", "--norc", "-c", "ps -l"], {
|
|
802
|
+
windowsHide: true,
|
|
803
|
+
timeout: 3e3,
|
|
804
|
+
encoding: "utf8",
|
|
805
|
+
env: limitedEnvironment("bash")
|
|
806
|
+
});
|
|
807
|
+
if (result.status !== 0 || !result.stdout.includes("PGID")) return false;
|
|
808
|
+
return !result.stdout.split("\n").some((line) => {
|
|
809
|
+
const columns = line.trim().split(/\s+/);
|
|
810
|
+
return /^\d+$/.test(columns[0] ?? "") && Number(columns[2]) === group.pid;
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
function stopProcessTree(child, nativeGroup) {
|
|
814
|
+
if (!child.pid) return false;
|
|
815
|
+
if (process.platform === "win32") {
|
|
816
|
+
const groupStopped = !nativeGroup || signalNativeBashGroup(nativeGroup, "TERM");
|
|
817
|
+
const result = (0, import_node_child_process2.spawnSync)(getTaskkillPath(), ["/PID", String(child.pid), "/T", "/F"], {
|
|
818
|
+
windowsHide: true,
|
|
819
|
+
timeout: 3e3
|
|
820
|
+
});
|
|
821
|
+
return groupStopped && (result.status === 0 || Boolean(nativeGroup));
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
process.kill(-child.pid, "SIGTERM");
|
|
825
|
+
return true;
|
|
826
|
+
} catch {
|
|
827
|
+
try {
|
|
828
|
+
child.kill("SIGTERM");
|
|
829
|
+
} catch {
|
|
830
|
+
}
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function forceProcessTree(child, nativeGroup) {
|
|
835
|
+
if (!child.pid) return false;
|
|
836
|
+
if (process.platform === "win32") {
|
|
837
|
+
const groupStopped = !nativeGroup || signalNativeBashGroup(nativeGroup, "KILL");
|
|
838
|
+
const result = (0, import_node_child_process2.spawnSync)(getTaskkillPath(), ["/PID", String(child.pid), "/T", "/F"], {
|
|
839
|
+
windowsHide: true,
|
|
840
|
+
timeout: 3e3
|
|
841
|
+
});
|
|
842
|
+
return groupStopped && (result.status === 0 || Boolean(nativeGroup));
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
process.kill(-child.pid, "SIGKILL");
|
|
846
|
+
return true;
|
|
847
|
+
} catch {
|
|
848
|
+
try {
|
|
849
|
+
child.kill("SIGKILL");
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async function confirmProcessTreeStopped(child, terminationSucceeded, nativeGroup) {
|
|
856
|
+
if (!terminationSucceeded || !child.pid) return false;
|
|
857
|
+
if (nativeGroup) {
|
|
858
|
+
const deadline2 = Date.now() + 1e3;
|
|
859
|
+
while (Date.now() < deadline2) {
|
|
860
|
+
if (nativeBashGroupStopped(nativeGroup)) return true;
|
|
861
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
862
|
+
}
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
if (process.platform === "win32") return true;
|
|
866
|
+
const deadline = Date.now() + 1e3;
|
|
867
|
+
while (Date.now() < deadline) {
|
|
868
|
+
try {
|
|
869
|
+
process.kill(-child.pid, 0);
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (error.code === "ESRCH") return true;
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
875
|
+
}
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
function waitForClose(promise, milliseconds) {
|
|
879
|
+
return new Promise((resolve) => {
|
|
880
|
+
const timer = setTimeout(() => resolve(false), milliseconds);
|
|
881
|
+
void promise.then(
|
|
882
|
+
() => {
|
|
883
|
+
clearTimeout(timer);
|
|
884
|
+
resolve(true);
|
|
885
|
+
},
|
|
886
|
+
() => {
|
|
887
|
+
clearTimeout(timer);
|
|
888
|
+
resolve(true);
|
|
889
|
+
}
|
|
890
|
+
);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function resultDirectory(rootDir) {
|
|
894
|
+
return import_node_path4.default.join(rootDir, ".kapibala", "tool-results");
|
|
895
|
+
}
|
|
896
|
+
function cleanupCommandResults(rootDir, now = Date.now()) {
|
|
897
|
+
const directory = resultDirectory(rootDir);
|
|
898
|
+
if (!import_node_fs4.default.existsSync(directory)) return;
|
|
899
|
+
const manifest = import_node_path4.default.join(directory, RESULT_MANIFEST);
|
|
900
|
+
if (!import_node_fs4.default.existsSync(manifest) || !import_node_fs4.default.lstatSync(manifest).isFile()) return;
|
|
901
|
+
const cutoff = now - 7 * 864e5;
|
|
902
|
+
const retained = [];
|
|
903
|
+
for (const line of import_node_fs4.default.readFileSync(manifest, "utf8").split("\n")) {
|
|
904
|
+
if (!line) continue;
|
|
905
|
+
let record;
|
|
906
|
+
try {
|
|
907
|
+
record = JSON.parse(line);
|
|
908
|
+
} catch {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (!OWNED_RESULT.test(record.name)) continue;
|
|
912
|
+
const file = import_node_path4.default.join(directory, record.name);
|
|
913
|
+
if (!import_node_fs4.default.existsSync(file)) continue;
|
|
914
|
+
const stat = import_node_fs4.default.lstatSync(file);
|
|
915
|
+
if (!stat.isFile() || stat.dev !== record.dev || stat.ino !== record.ino) continue;
|
|
916
|
+
if (stat.mtimeMs < cutoff) import_node_fs4.default.unlinkSync(file);
|
|
917
|
+
else retained.push(line);
|
|
918
|
+
}
|
|
919
|
+
const replacement = import_node_path4.default.join(directory, `.managed-results-${(0, import_node_crypto3.randomUUID)()}.tmp`);
|
|
920
|
+
import_node_fs4.default.writeFileSync(replacement, retained.length ? `${retained.join("\n")}
|
|
921
|
+
` : "", { mode: 384 });
|
|
922
|
+
import_node_fs4.default.renameSync(replacement, manifest);
|
|
923
|
+
}
|
|
924
|
+
function recordManagedResult(directory, file, descriptor) {
|
|
925
|
+
const stat = import_node_fs4.default.fstatSync(descriptor);
|
|
926
|
+
import_node_fs4.default.appendFileSync(
|
|
927
|
+
import_node_path4.default.join(directory, RESULT_MANIFEST),
|
|
928
|
+
`${JSON.stringify({ name: import_node_path4.default.basename(file), dev: stat.dev, ino: stat.ino })}
|
|
929
|
+
`,
|
|
930
|
+
{ mode: 384 }
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
function createRunCommandTool(environment) {
|
|
934
|
+
return defineTool({
|
|
935
|
+
name: "run_command",
|
|
936
|
+
description: `Run a ${environment.kind === "wsl" ? "bash (WSL)" : environment.kind} command as the current OS user. The file tool PathSandbox does not isolate this process or its network access.`,
|
|
937
|
+
metadata: {
|
|
938
|
+
source: "builtin",
|
|
939
|
+
permissions: ["fs:read", "fs:write", "exec", "net:outbound", "env:read"],
|
|
940
|
+
dangerous: true,
|
|
941
|
+
managesTimeout: true
|
|
942
|
+
},
|
|
943
|
+
parameters: {
|
|
944
|
+
type: "object",
|
|
945
|
+
properties: {
|
|
946
|
+
command: {
|
|
947
|
+
type: "string",
|
|
948
|
+
description: `Command using ${environment.kind === "wsl" ? "bash" : environment.kind} syntax.`
|
|
949
|
+
},
|
|
950
|
+
cwd: { type: "string", description: "Working directory; defaults to workspace root." },
|
|
951
|
+
timeout_ms: { type: "integer", description: "Timeout in milliseconds, maximum 600000." }
|
|
952
|
+
},
|
|
953
|
+
required: ["command"]
|
|
954
|
+
},
|
|
955
|
+
approvalScope(input, rootDir) {
|
|
956
|
+
validateCommand(input);
|
|
957
|
+
const cwd = effectiveCwd(input, rootDir);
|
|
958
|
+
const timeoutMs = effectiveTimeout(input);
|
|
959
|
+
const scriptDigest = directScriptDigest(input.command, environment.kind, cwd);
|
|
960
|
+
const stat = import_node_fs4.default.statSync(environment.executable);
|
|
961
|
+
const executableDigest = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify([environment.executable, stat.size, stat.mtimeMs])).digest("hex");
|
|
962
|
+
const environmentDigest = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(limitedEnvironment(environment.kind))).digest("hex");
|
|
963
|
+
return {
|
|
964
|
+
command: input.command,
|
|
965
|
+
interpreter: environment.kind,
|
|
966
|
+
executablePath: environment.executable,
|
|
967
|
+
cwd,
|
|
968
|
+
executableDigest,
|
|
969
|
+
...scriptDigest ? { scriptDigest } : {},
|
|
970
|
+
executionFingerprint: executionFingerprint({
|
|
971
|
+
command: input.command,
|
|
972
|
+
shell: environment.kind,
|
|
973
|
+
executable: executableDigest,
|
|
974
|
+
cwd,
|
|
975
|
+
environmentDigest,
|
|
976
|
+
timeoutMs,
|
|
977
|
+
outputLimitBytes: FILE_LIMIT,
|
|
978
|
+
targetDigest: scriptDigest
|
|
979
|
+
})
|
|
980
|
+
};
|
|
981
|
+
},
|
|
982
|
+
async execute(input, ctx) {
|
|
983
|
+
const startedAt = Date.now();
|
|
984
|
+
validateCommand(input);
|
|
985
|
+
if (ctx.signal?.aborted) {
|
|
986
|
+
throw new KapibalaError("Command aborted before start", {
|
|
987
|
+
code: "ABORTED",
|
|
988
|
+
retryPolicy: "after_user_action",
|
|
989
|
+
safeMessage: "Command was not started because the run was aborted"
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
const cwd = effectiveCwd(input, ctx.rootDir);
|
|
993
|
+
const timeoutMs = effectiveTimeout(input);
|
|
994
|
+
cleanupCommandResults(ctx.rootDir);
|
|
995
|
+
const nativeGroup = createNativeBashGroup(environment);
|
|
996
|
+
const command = nativeGroup ? `printf '${nativeGroup.token}:%s\\n' "$$" >&2
|
|
997
|
+
${input.command}` : input.command;
|
|
998
|
+
const child = (0, import_node_child_process2.spawn)(environment.executable, interpreterArgs(environment, command), {
|
|
999
|
+
cwd,
|
|
1000
|
+
env: limitedEnvironment(environment.kind),
|
|
1001
|
+
windowsHide: true,
|
|
1002
|
+
detached: process.platform !== "win32",
|
|
1003
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1004
|
+
});
|
|
1005
|
+
const inline = [];
|
|
1006
|
+
let inlineBytes = 0;
|
|
1007
|
+
let outputBytes = 0;
|
|
1008
|
+
let fileBytes = 0;
|
|
1009
|
+
let outputFile;
|
|
1010
|
+
let outputFileTruncated = false;
|
|
1011
|
+
let outputFailure = false;
|
|
1012
|
+
let lastProgressAt = startedAt;
|
|
1013
|
+
let descriptor;
|
|
1014
|
+
const collect = (chunk) => {
|
|
1015
|
+
outputBytes += chunk.length;
|
|
1016
|
+
if (outputFailure) return;
|
|
1017
|
+
if (inlineBytes < INLINE_LIMIT) {
|
|
1018
|
+
const part = chunk.subarray(0, INLINE_LIMIT - inlineBytes);
|
|
1019
|
+
inline.push(part);
|
|
1020
|
+
inlineBytes += part.length;
|
|
1021
|
+
}
|
|
1022
|
+
if (outputBytes > INLINE_LIMIT && descriptor === void 0) {
|
|
1023
|
+
const directory = resultDirectory(ctx.rootDir);
|
|
1024
|
+
import_node_fs4.default.mkdirSync(directory, { recursive: true, mode: 448 });
|
|
1025
|
+
outputFile = import_node_path4.default.join(directory, `run-${(0, import_node_crypto3.randomUUID)()}.txt`);
|
|
1026
|
+
descriptor = import_node_fs4.default.openSync(outputFile, "wx", 384);
|
|
1027
|
+
recordManagedResult(directory, outputFile, descriptor);
|
|
1028
|
+
const prefix = Buffer.concat(inline);
|
|
1029
|
+
import_node_fs4.default.writeSync(descriptor, prefix);
|
|
1030
|
+
fileBytes = prefix.length;
|
|
1031
|
+
const remaining = chunk.subarray(
|
|
1032
|
+
Math.max(0, prefix.length - (outputBytes - chunk.length))
|
|
1033
|
+
);
|
|
1034
|
+
const writable = utf8Prefix(remaining, FILE_LIMIT - fileBytes);
|
|
1035
|
+
if (writable.length > 0) import_node_fs4.default.writeSync(descriptor, writable);
|
|
1036
|
+
fileBytes += writable.length;
|
|
1037
|
+
if (writable.length < remaining.length) outputFileTruncated = true;
|
|
1038
|
+
} else if (descriptor !== void 0 && !outputFileTruncated) {
|
|
1039
|
+
const writable = utf8Prefix(chunk, FILE_LIMIT - fileBytes);
|
|
1040
|
+
if (writable.length > 0) import_node_fs4.default.writeSync(descriptor, writable);
|
|
1041
|
+
fileBytes += writable.length;
|
|
1042
|
+
if (writable.length < chunk.length) outputFileTruncated = true;
|
|
1043
|
+
} else if (descriptor !== void 0) {
|
|
1044
|
+
outputFileTruncated = true;
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
const onData = (chunk) => {
|
|
1048
|
+
try {
|
|
1049
|
+
collect(chunk);
|
|
1050
|
+
} catch {
|
|
1051
|
+
outputFailure = true;
|
|
1052
|
+
if (descriptor !== void 0) {
|
|
1053
|
+
try {
|
|
1054
|
+
import_node_fs4.default.closeSync(descriptor);
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
descriptor = void 0;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const now = Date.now();
|
|
1061
|
+
if (now - lastProgressAt >= 250) {
|
|
1062
|
+
lastProgressAt = now;
|
|
1063
|
+
ctx.onProgress?.({ elapsedMs: now - startedAt, outputBytes });
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
1067
|
+
const decoder = new import_node_string_decoder.StringDecoder("utf8");
|
|
1068
|
+
stream?.on("data", (chunk) => {
|
|
1069
|
+
let text = decoder.write(chunk);
|
|
1070
|
+
if (stream === child.stderr && nativeGroup) text = nativeGroup.accept(text);
|
|
1071
|
+
if (text) onData(Buffer.from(text, "utf8"));
|
|
1072
|
+
});
|
|
1073
|
+
stream?.once("end", () => {
|
|
1074
|
+
const text = decoder.end();
|
|
1075
|
+
if (text) onData(Buffer.from(text, "utf8"));
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
const close = new Promise((resolve, reject) => {
|
|
1079
|
+
child.once("error", reject);
|
|
1080
|
+
child.once("close", (code) => resolve({ code }));
|
|
1081
|
+
});
|
|
1082
|
+
let closed = false;
|
|
1083
|
+
void close.finally(() => {
|
|
1084
|
+
closed = true;
|
|
1085
|
+
}).catch(() => {
|
|
1086
|
+
});
|
|
1087
|
+
try {
|
|
1088
|
+
await ctx.onProcessSpawned?.();
|
|
1089
|
+
} catch {
|
|
1090
|
+
if (nativeGroup) await waitForClose(nativeGroup.ready, 1e3);
|
|
1091
|
+
stopProcessTree(child, nativeGroup);
|
|
1092
|
+
if (!await waitForClose(close, 2e3)) {
|
|
1093
|
+
forceProcessTree(child, nativeGroup);
|
|
1094
|
+
await waitForClose(close, 3e3);
|
|
1095
|
+
}
|
|
1096
|
+
if (descriptor !== void 0) import_node_fs4.default.closeSync(descriptor);
|
|
1097
|
+
throw new KapibalaError("Process started but spawn audit failed", {
|
|
1098
|
+
code: "OUTCOME_UNKNOWN",
|
|
1099
|
+
retryPolicy: "after_user_action",
|
|
1100
|
+
safeMessage: "Command started but audit failed; outcome is unknown"
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
let stopReason;
|
|
1104
|
+
let stopPromise;
|
|
1105
|
+
const stop = (reason) => {
|
|
1106
|
+
if (stopPromise) return;
|
|
1107
|
+
stopReason = reason;
|
|
1108
|
+
stopPromise = (async () => {
|
|
1109
|
+
if (nativeGroup) await waitForClose(nativeGroup.ready, 1e3);
|
|
1110
|
+
const stopped = stopProcessTree(child, nativeGroup);
|
|
1111
|
+
if (await waitForClose(close, 2e3)) {
|
|
1112
|
+
if (await confirmProcessTreeStopped(child, stopped, nativeGroup)) return true;
|
|
1113
|
+
}
|
|
1114
|
+
const forced = forceProcessTree(child, nativeGroup);
|
|
1115
|
+
if (!await waitForClose(close, 3e3)) return false;
|
|
1116
|
+
return confirmProcessTreeStopped(child, forced, nativeGroup);
|
|
1117
|
+
})();
|
|
1118
|
+
};
|
|
1119
|
+
const timer = setTimeout(() => stop("timeout"), timeoutMs);
|
|
1120
|
+
const progressTimer = setInterval(() => {
|
|
1121
|
+
const now = Date.now();
|
|
1122
|
+
if (now - lastProgressAt >= 500) {
|
|
1123
|
+
lastProgressAt = now;
|
|
1124
|
+
ctx.onProgress?.({ elapsedMs: now - startedAt, outputBytes });
|
|
1125
|
+
}
|
|
1126
|
+
}, 500);
|
|
1127
|
+
const onAbort = () => stop("aborted");
|
|
1128
|
+
if (ctx.signal?.aborted) onAbort();
|
|
1129
|
+
else ctx.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1130
|
+
try {
|
|
1131
|
+
const outcome = await Promise.race([
|
|
1132
|
+
close.then((value) => ({ kind: "closed", value })),
|
|
1133
|
+
new Promise((resolve) => {
|
|
1134
|
+
const check = () => {
|
|
1135
|
+
if (closed) return;
|
|
1136
|
+
if (stopPromise)
|
|
1137
|
+
void stopPromise.then((confirmed) => {
|
|
1138
|
+
if (!confirmed) resolve({ kind: "unknown" });
|
|
1139
|
+
});
|
|
1140
|
+
else setTimeout(check, 10);
|
|
1141
|
+
};
|
|
1142
|
+
check();
|
|
1143
|
+
})
|
|
1144
|
+
]);
|
|
1145
|
+
if (outcome.kind === "unknown") {
|
|
1146
|
+
throw new KapibalaError("Command process tree could not be confirmed stopped", {
|
|
1147
|
+
code: "OUTCOME_UNKNOWN",
|
|
1148
|
+
retryPolicy: "after_user_action",
|
|
1149
|
+
safeMessage: "Command outcome is unknown; inspect the system before retrying"
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
if (stopReason) {
|
|
1153
|
+
if (!await stopPromise) {
|
|
1154
|
+
throw new KapibalaError("Command process tree could not be confirmed stopped", {
|
|
1155
|
+
code: "OUTCOME_UNKNOWN",
|
|
1156
|
+
retryPolicy: "after_user_action",
|
|
1157
|
+
safeMessage: "Command outcome is unknown; inspect the system before retrying"
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
throw new KapibalaError(`Command ${stopReason}`, {
|
|
1161
|
+
code: stopReason === "timeout" ? "COMMAND_TIMEOUT" : "ABORTED",
|
|
1162
|
+
retryPolicy: "after_user_action",
|
|
1163
|
+
safeMessage: `Command ${stopReason}; process exit was observed`
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
if (outputFailure) {
|
|
1167
|
+
throw new KapibalaError("Command output storage failed", {
|
|
1168
|
+
code: "OUTPUT_STORAGE_FAILED",
|
|
1169
|
+
retryPolicy: "after_user_action",
|
|
1170
|
+
safeMessage: "Command finished but its output could not be stored"
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
const inlineBuffer = Buffer.concat(inline);
|
|
1174
|
+
const output = utf8Prefix(inlineBuffer, INLINE_LIMIT).toString("utf8");
|
|
1175
|
+
return {
|
|
1176
|
+
exitCode: outcome.value.code,
|
|
1177
|
+
output,
|
|
1178
|
+
outputTruncated: outputBytes > INLINE_LIMIT,
|
|
1179
|
+
...outputFile ? { outputFile } : {},
|
|
1180
|
+
outputFileTruncated,
|
|
1181
|
+
outputBytes,
|
|
1182
|
+
shell: environment.kind
|
|
1183
|
+
};
|
|
1184
|
+
} finally {
|
|
1185
|
+
clearTimeout(timer);
|
|
1186
|
+
clearInterval(progressTimer);
|
|
1187
|
+
ctx.signal?.removeEventListener("abort", onAbort);
|
|
1188
|
+
if (descriptor !== void 0) import_node_fs4.default.closeSync(descriptor);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/security/sandbox.ts
|
|
1195
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1196
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
145
1197
|
function tryRealpath(target) {
|
|
146
1198
|
try {
|
|
147
|
-
return
|
|
1199
|
+
return import_node_fs5.default.realpathSync(target);
|
|
148
1200
|
} catch {
|
|
149
1201
|
return void 0;
|
|
150
1202
|
}
|
|
@@ -154,7 +1206,7 @@ var PathSandbox = class {
|
|
|
154
1206
|
allowSymlinks;
|
|
155
1207
|
cachedRealRootDir;
|
|
156
1208
|
constructor(config) {
|
|
157
|
-
this.rootDir =
|
|
1209
|
+
this.rootDir = import_node_path5.default.resolve(config.rootDir);
|
|
158
1210
|
this.allowSymlinks = config.allowSymlinks ?? false;
|
|
159
1211
|
}
|
|
160
1212
|
/**
|
|
@@ -177,16 +1229,16 @@ var PathSandbox = class {
|
|
|
177
1229
|
if (!targetPath || typeof targetPath !== "string") {
|
|
178
1230
|
throw new ToolError("Invalid path: path must be a non-empty string");
|
|
179
1231
|
}
|
|
180
|
-
const resolvedPath =
|
|
181
|
-
const relative =
|
|
182
|
-
if (relative.startsWith("..") ||
|
|
1232
|
+
const resolvedPath = import_node_path5.default.isAbsolute(targetPath) ? import_node_path5.default.resolve(targetPath) : import_node_path5.default.resolve(this.rootDir, targetPath);
|
|
1233
|
+
const relative = import_node_path5.default.relative(this.rootDir, resolvedPath);
|
|
1234
|
+
if (relative.startsWith("..") || import_node_path5.default.isAbsolute(relative)) {
|
|
183
1235
|
throw new ToolError(
|
|
184
1236
|
`Access denied: path '${targetPath}' escapes root directory '${this.rootDir}'`
|
|
185
1237
|
);
|
|
186
1238
|
}
|
|
187
1239
|
const physicalPath = this.toPhysicalPath(resolvedPath);
|
|
188
|
-
const physicalRelative =
|
|
189
|
-
if (physicalRelative.startsWith("..") ||
|
|
1240
|
+
const physicalRelative = import_node_path5.default.relative(this.realRootDir, physicalPath);
|
|
1241
|
+
if (physicalRelative.startsWith("..") || import_node_path5.default.isAbsolute(physicalRelative)) {
|
|
190
1242
|
throw new ToolError(
|
|
191
1243
|
`Access denied: path '${targetPath}' resolves outside root directory '${this.rootDir}' (via symlink)`
|
|
192
1244
|
);
|
|
@@ -208,7 +1260,7 @@ var PathSandbox = class {
|
|
|
208
1260
|
while (true) {
|
|
209
1261
|
let stat;
|
|
210
1262
|
try {
|
|
211
|
-
stat =
|
|
1263
|
+
stat = import_node_fs5.default.lstatSync(probe);
|
|
212
1264
|
} catch {
|
|
213
1265
|
stat = void 0;
|
|
214
1266
|
}
|
|
@@ -219,30 +1271,146 @@ var PathSandbox = class {
|
|
|
219
1271
|
`Access denied: cannot resolve real path for '${resolvedPath}' (broken or unresolvable symlink)`
|
|
220
1272
|
);
|
|
221
1273
|
}
|
|
222
|
-
return pendingSegments.length > 0 ?
|
|
1274
|
+
return pendingSegments.length > 0 ? import_node_path5.default.join(real, ...pendingSegments) : real;
|
|
223
1275
|
}
|
|
224
|
-
const parent =
|
|
1276
|
+
const parent = import_node_path5.default.dirname(probe);
|
|
225
1277
|
if (parent === probe) {
|
|
226
1278
|
return resolvedPath;
|
|
227
1279
|
}
|
|
228
|
-
pendingSegments.unshift(
|
|
1280
|
+
pendingSegments.unshift(import_node_path5.default.basename(probe));
|
|
229
1281
|
probe = parent;
|
|
230
1282
|
}
|
|
231
1283
|
}
|
|
232
1284
|
};
|
|
233
1285
|
|
|
234
|
-
// src/
|
|
235
|
-
|
|
236
|
-
|
|
1286
|
+
// src/security/permissions.ts
|
|
1287
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1288
|
+
var CAPABILITIES = /* @__PURE__ */ new Set([
|
|
1289
|
+
"fs:read",
|
|
1290
|
+
"fs:write",
|
|
1291
|
+
"exec",
|
|
1292
|
+
"net:outbound",
|
|
1293
|
+
"env:read",
|
|
1294
|
+
"agent:spawn"
|
|
1295
|
+
]);
|
|
1296
|
+
function validatePermissionRules(rules) {
|
|
1297
|
+
for (const [index, rule] of rules.entries()) {
|
|
1298
|
+
const ruleNumber = index + 1;
|
|
1299
|
+
if (!["allow", "ask", "deny"].includes(rule.action)) {
|
|
1300
|
+
throw new Error(`Permission rule ${ruleNumber}: invalid action`);
|
|
1301
|
+
}
|
|
1302
|
+
const scopes = Number(rule.tool !== void 0) + Number(rule.capability !== void 0) + Number(rule.shell !== void 0);
|
|
1303
|
+
if (scopes !== 1)
|
|
1304
|
+
throw new Error(`Permission rule ${ruleNumber}: exactly one scope is required`);
|
|
1305
|
+
if (rule.capability && !CAPABILITIES.has(rule.capability)) {
|
|
1306
|
+
throw new Error(`Permission rule ${ruleNumber}: unknown capability`);
|
|
1307
|
+
}
|
|
1308
|
+
if (rule.tool !== void 0 && (!rule.tool.trim() || /[*?]/.test(rule.tool))) {
|
|
1309
|
+
throw new Error(`Permission rule ${ruleNumber}: tool scope must be an exact name`);
|
|
1310
|
+
}
|
|
1311
|
+
if (rule.shell) {
|
|
1312
|
+
const { command, interpreter, cwd } = rule.shell;
|
|
1313
|
+
if (!command?.trim() || !interpreter?.trim() || !cwd?.trim() || /[*?]/.test(command) || /[*?]/.test(interpreter) || /[*?]/.test(cwd)) {
|
|
1314
|
+
throw new Error(
|
|
1315
|
+
`Permission rule ${ruleNumber}: shell rules require an exact command, interpreter and cwd`
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
237
1320
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
1321
|
+
function samePath(a, b) {
|
|
1322
|
+
const na = import_node_path6.default.resolve(a);
|
|
1323
|
+
const nb = import_node_path6.default.resolve(b);
|
|
1324
|
+
return process.platform === "win32" ? na.toLowerCase() === nb.toLowerCase() : na === nb;
|
|
1325
|
+
}
|
|
1326
|
+
var PermissionPolicy = class {
|
|
1327
|
+
decide(request) {
|
|
1328
|
+
if (request.hardDenied) return { decision: "deny", source: "hard_limit" };
|
|
1329
|
+
if (request.mode === "Plan" && (request.dangerous || request.capabilities.length === 0 || request.capabilities.some((capability) => capability !== "fs:read"))) {
|
|
1330
|
+
return { decision: "deny", source: "plan" };
|
|
1331
|
+
}
|
|
1332
|
+
const matches = (rule) => {
|
|
1333
|
+
if (rule.tool !== void 0) return rule.tool === request.toolName;
|
|
1334
|
+
if (rule.capability !== void 0) return request.capabilities.includes(rule.capability);
|
|
1335
|
+
if (!rule.shell || !request.shell) return false;
|
|
1336
|
+
return rule.shell.command === request.shell.command && rule.shell.interpreter === request.shell.interpreter && samePath(rule.shell.cwd, request.shell.cwd);
|
|
1337
|
+
};
|
|
1338
|
+
const rules = request.rules ?? [];
|
|
1339
|
+
for (const action of ["deny", "ask"]) {
|
|
1340
|
+
const ruleIndex = rules.findIndex((rule) => rule.action === action && matches(rule));
|
|
1341
|
+
if (ruleIndex >= 0) return { decision: action, source: "explicit_rule", ruleIndex };
|
|
1342
|
+
if (action === "deny" && request.cachedDenial)
|
|
1343
|
+
return { decision: "deny", source: "session_cache" };
|
|
1344
|
+
}
|
|
1345
|
+
if (request.cachedApproval) return { decision: "allow", source: "session_cache" };
|
|
1346
|
+
if (request.capabilities.length === 0) return { decision: "ask", source: "mode_default" };
|
|
1347
|
+
const allowIndex = rules.findIndex(
|
|
1348
|
+
(rule) => rule.action === "allow" && matches(rule) && (request.toolName !== "run_command" || rule.shell !== void 0)
|
|
1349
|
+
);
|
|
1350
|
+
if (allowIndex >= 0)
|
|
1351
|
+
return { decision: "allow", source: "explicit_rule", ruleIndex: allowIndex };
|
|
1352
|
+
if (request.capabilities.length === 0 || request.dangerous && request.mode !== "FullAccess") {
|
|
1353
|
+
return { decision: "ask", source: "mode_default" };
|
|
1354
|
+
}
|
|
1355
|
+
if (request.mode === "FullAccess") return { decision: "allow", source: "mode_default" };
|
|
1356
|
+
if (request.capabilities.every((capability) => capability === "fs:read")) {
|
|
1357
|
+
return { decision: "allow", source: "mode_default" };
|
|
1358
|
+
}
|
|
1359
|
+
if (request.mode === "Auto" && request.capabilities.every(
|
|
1360
|
+
(capability) => capability === "fs:read" || capability === "fs:write"
|
|
1361
|
+
)) {
|
|
1362
|
+
return { decision: "allow", source: "mode_default" };
|
|
1363
|
+
}
|
|
1364
|
+
return { decision: "ask", source: "mode_default" };
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
function visibleTools(tools, mode, rules = []) {
|
|
1368
|
+
const policy = new PermissionPolicy();
|
|
1369
|
+
return tools.filter(
|
|
1370
|
+
(tool) => policy.decide({
|
|
1371
|
+
mode,
|
|
1372
|
+
toolName: tool.name,
|
|
1373
|
+
capabilities: tool.metadata?.permissions ?? [],
|
|
1374
|
+
dangerous: tool.metadata?.dangerous,
|
|
1375
|
+
rules
|
|
1376
|
+
}).decision !== "deny"
|
|
1377
|
+
);
|
|
244
1378
|
}
|
|
245
1379
|
|
|
1380
|
+
// src/security/approval.ts
|
|
1381
|
+
var import_node_crypto4 = require("crypto");
|
|
1382
|
+
function stableJsonStringify(value) {
|
|
1383
|
+
if (value === null || typeof value !== "object") {
|
|
1384
|
+
return JSON.stringify(value);
|
|
1385
|
+
}
|
|
1386
|
+
if (Array.isArray(value)) {
|
|
1387
|
+
return `[${value.map(stableJsonStringify).join(",")}]`;
|
|
1388
|
+
}
|
|
1389
|
+
const obj = value;
|
|
1390
|
+
const keys = Object.keys(obj).sort();
|
|
1391
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableJsonStringify(obj[k])}`).join(",")}}`;
|
|
1392
|
+
}
|
|
1393
|
+
var SessionApprovalCache = class {
|
|
1394
|
+
choices = /* @__PURE__ */ new Map();
|
|
1395
|
+
key(request) {
|
|
1396
|
+
return (0, import_node_crypto4.createHash)("sha256").update(
|
|
1397
|
+
stableJsonStringify([
|
|
1398
|
+
request.toolName,
|
|
1399
|
+
[...request.capabilities].sort(),
|
|
1400
|
+
request.input,
|
|
1401
|
+
request.rootDir,
|
|
1402
|
+
request.shell ?? null
|
|
1403
|
+
])
|
|
1404
|
+
).digest("hex");
|
|
1405
|
+
}
|
|
1406
|
+
get(request) {
|
|
1407
|
+
return this.choices.get(this.key(request));
|
|
1408
|
+
}
|
|
1409
|
+
set(request, choice) {
|
|
1410
|
+
this.choices.set(this.key(request), choice);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
|
|
246
1414
|
// src/tools/registry.ts
|
|
247
1415
|
var ToolRegistry = class {
|
|
248
1416
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -300,8 +1468,8 @@ var ToolRegistry = class {
|
|
|
300
1468
|
};
|
|
301
1469
|
|
|
302
1470
|
// src/tools/builtin/fs.ts
|
|
303
|
-
var
|
|
304
|
-
var
|
|
1471
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
1472
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
305
1473
|
var sandboxCache = /* @__PURE__ */ new Map();
|
|
306
1474
|
function getSandbox(rootDir) {
|
|
307
1475
|
let sandbox = sandboxCache.get(rootDir);
|
|
@@ -339,10 +1507,10 @@ var readFileTool = defineTool({
|
|
|
339
1507
|
async execute(input, ctx) {
|
|
340
1508
|
const sandbox = getSandbox(ctx.rootDir);
|
|
341
1509
|
const safePath = sandbox.resolveSafePath(input.path);
|
|
342
|
-
if (!
|
|
1510
|
+
if (!import_node_fs6.default.existsSync(safePath)) {
|
|
343
1511
|
throw new ToolError(`File not found: ${input.path}`);
|
|
344
1512
|
}
|
|
345
|
-
const stat =
|
|
1513
|
+
const stat = import_node_fs6.default.statSync(safePath);
|
|
346
1514
|
if (stat.isDirectory()) {
|
|
347
1515
|
throw new ToolError(`Target path is a directory, not a file: ${input.path}`);
|
|
348
1516
|
}
|
|
@@ -355,11 +1523,11 @@ var readFileTool = defineTool({
|
|
|
355
1523
|
}
|
|
356
1524
|
const prefixSize = Math.min(stat.size, 8192);
|
|
357
1525
|
const prefix = Buffer.alloc(prefixSize);
|
|
358
|
-
const fileDescriptor =
|
|
1526
|
+
const fileDescriptor = import_node_fs6.default.openSync(safePath, "r");
|
|
359
1527
|
try {
|
|
360
|
-
|
|
1528
|
+
import_node_fs6.default.readSync(fileDescriptor, prefix, 0, prefixSize, 0);
|
|
361
1529
|
} finally {
|
|
362
|
-
|
|
1530
|
+
import_node_fs6.default.closeSync(fileDescriptor);
|
|
363
1531
|
}
|
|
364
1532
|
if (prefix.includes(0)) {
|
|
365
1533
|
throw new ToolError(`Cannot read binary file: ${input.path}`);
|
|
@@ -372,7 +1540,7 @@ var readFileTool = defineTool({
|
|
|
372
1540
|
maxReadBytes
|
|
373
1541
|
);
|
|
374
1542
|
}
|
|
375
|
-
return
|
|
1543
|
+
return import_node_fs6.default.readFileSync(safePath, "utf-8");
|
|
376
1544
|
}
|
|
377
1545
|
});
|
|
378
1546
|
async function readFileLineRange(filePath, startLine, endLine, maxReadBytes) {
|
|
@@ -382,7 +1550,7 @@ async function readFileLineRange(filePath, startLine, endLine, maxReadBytes) {
|
|
|
382
1550
|
let currentLine = 1;
|
|
383
1551
|
let pending = "";
|
|
384
1552
|
let reachedEnd = false;
|
|
385
|
-
const stream =
|
|
1553
|
+
const stream = import_node_fs6.default.createReadStream(filePath, { encoding: "utf-8" });
|
|
386
1554
|
const selectLine = (line) => {
|
|
387
1555
|
if (currentLine >= startLine && (endLine === void 0 || currentLine <= endLine)) {
|
|
388
1556
|
const addedBytes = Buffer.byteLength(line, "utf-8") + (selectedLines.length > 0 ? 1 : 0);
|
|
@@ -445,11 +1613,11 @@ var writeFileTool = defineTool({
|
|
|
445
1613
|
async execute(input, ctx) {
|
|
446
1614
|
const sandbox = getSandbox(ctx.rootDir);
|
|
447
1615
|
const safePath = sandbox.resolveSafePath(input.path);
|
|
448
|
-
const dir =
|
|
449
|
-
if (!
|
|
450
|
-
|
|
1616
|
+
const dir = import_node_path7.default.dirname(safePath);
|
|
1617
|
+
if (!import_node_fs6.default.existsSync(dir)) {
|
|
1618
|
+
import_node_fs6.default.mkdirSync(dir, { recursive: true });
|
|
451
1619
|
}
|
|
452
|
-
|
|
1620
|
+
import_node_fs6.default.writeFileSync(safePath, input.content, "utf-8");
|
|
453
1621
|
return `Successfully wrote ${Buffer.byteLength(input.content, "utf-8")} bytes to ${input.path}`;
|
|
454
1622
|
}
|
|
455
1623
|
});
|
|
@@ -481,13 +1649,13 @@ var editFileTool = defineTool({
|
|
|
481
1649
|
async execute(input, ctx) {
|
|
482
1650
|
const sandbox = getSandbox(ctx.rootDir);
|
|
483
1651
|
const safePath = sandbox.resolveSafePath(input.path);
|
|
484
|
-
if (!
|
|
1652
|
+
if (!import_node_fs6.default.existsSync(safePath)) {
|
|
485
1653
|
throw new ToolError(`File not found: ${input.path}`);
|
|
486
1654
|
}
|
|
487
1655
|
if (!input.targetContent) {
|
|
488
1656
|
throw new ToolError("Target content must not be empty");
|
|
489
1657
|
}
|
|
490
|
-
const content =
|
|
1658
|
+
const content = import_node_fs6.default.readFileSync(safePath, "utf-8");
|
|
491
1659
|
if (!content.includes(input.targetContent)) {
|
|
492
1660
|
throw new ToolError(
|
|
493
1661
|
`Target content not found in ${input.path}. Ensure exact indentation and characters.`
|
|
@@ -500,7 +1668,7 @@ var editFileTool = defineTool({
|
|
|
500
1668
|
);
|
|
501
1669
|
}
|
|
502
1670
|
const newContent = content.replace(input.targetContent, () => input.replacementContent);
|
|
503
|
-
|
|
1671
|
+
import_node_fs6.default.writeFileSync(safePath, newContent, "utf-8");
|
|
504
1672
|
return `Successfully replaced target content in ${input.path}`;
|
|
505
1673
|
}
|
|
506
1674
|
});
|
|
@@ -528,20 +1696,20 @@ var globTool = defineTool({
|
|
|
528
1696
|
async execute(input, ctx) {
|
|
529
1697
|
const sandbox = getSandbox(ctx.rootDir);
|
|
530
1698
|
const searchRoot = input.subDirectory ? sandbox.resolveSafePath(input.subDirectory) : ctx.rootDir;
|
|
531
|
-
if (!
|
|
1699
|
+
if (!import_node_fs6.default.existsSync(searchRoot)) {
|
|
532
1700
|
throw new ToolError(`Directory not found: ${input.subDirectory ?? "."}`);
|
|
533
1701
|
}
|
|
534
1702
|
const results = [];
|
|
535
1703
|
const maxResults = 100;
|
|
536
1704
|
function walk(currentDir) {
|
|
537
1705
|
if (results.length >= maxResults) return;
|
|
538
|
-
const entries =
|
|
1706
|
+
const entries = import_node_fs6.default.readdirSync(currentDir, { withFileTypes: true });
|
|
539
1707
|
for (const entry of entries) {
|
|
540
1708
|
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
|
|
541
1709
|
continue;
|
|
542
1710
|
}
|
|
543
|
-
const fullPath =
|
|
544
|
-
const relPath =
|
|
1711
|
+
const fullPath = import_node_path7.default.join(currentDir, entry.name);
|
|
1712
|
+
const relPath = import_node_path7.default.relative(ctx.rootDir, fullPath).replace(/\\/g, "/");
|
|
545
1713
|
if (entry.isDirectory()) {
|
|
546
1714
|
walk(fullPath);
|
|
547
1715
|
} else {
|
|
@@ -581,7 +1749,7 @@ var grepTool = defineTool({
|
|
|
581
1749
|
if (ctx.signal?.aborted) throw new AbortError();
|
|
582
1750
|
const sandbox = getSandbox(ctx.rootDir);
|
|
583
1751
|
const target = input.path ? sandbox.resolveSafePath(input.path) : ctx.rootDir;
|
|
584
|
-
if (!
|
|
1752
|
+
if (!import_node_fs6.default.existsSync(target)) {
|
|
585
1753
|
throw new ToolError(`Path not found: ${input.path ?? "."}`);
|
|
586
1754
|
}
|
|
587
1755
|
if (!input.pattern.trim()) throw new ToolError("Search pattern must not be empty");
|
|
@@ -593,8 +1761,8 @@ var grepTool = defineTool({
|
|
|
593
1761
|
if (matches.length >= maxMatches) return;
|
|
594
1762
|
const maxLineBytes = 512 * 1024;
|
|
595
1763
|
const maxOutputBytes = 512 * 1024;
|
|
596
|
-
const stream =
|
|
597
|
-
const relPath =
|
|
1764
|
+
const stream = import_node_fs6.default.createReadStream(filePath, { encoding: "utf-8" });
|
|
1765
|
+
const relPath = import_node_path7.default.relative(ctx.rootDir, filePath).replace(/\\/g, "/");
|
|
598
1766
|
let lineNumber = 0;
|
|
599
1767
|
let pending = "";
|
|
600
1768
|
let skippingLongLine = false;
|
|
@@ -644,14 +1812,14 @@ var grepTool = defineTool({
|
|
|
644
1812
|
}
|
|
645
1813
|
async function walk(dirPath) {
|
|
646
1814
|
if (matches.length >= maxMatches) return;
|
|
647
|
-
const entries =
|
|
1815
|
+
const entries = import_node_fs6.default.readdirSync(dirPath, { withFileTypes: true });
|
|
648
1816
|
for (const entry of entries) {
|
|
649
1817
|
if (ctx.signal?.aborted) throw new AbortError();
|
|
650
1818
|
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
|
|
651
1819
|
continue;
|
|
652
1820
|
}
|
|
653
1821
|
if (entry.isSymbolicLink()) continue;
|
|
654
|
-
const fullPath =
|
|
1822
|
+
const fullPath = import_node_path7.default.join(dirPath, entry.name);
|
|
655
1823
|
if (entry.isDirectory()) {
|
|
656
1824
|
await walk(fullPath);
|
|
657
1825
|
} else {
|
|
@@ -660,7 +1828,7 @@ var grepTool = defineTool({
|
|
|
660
1828
|
if (matches.length >= maxMatches) return;
|
|
661
1829
|
}
|
|
662
1830
|
}
|
|
663
|
-
const stat =
|
|
1831
|
+
const stat = import_node_fs6.default.statSync(target);
|
|
664
1832
|
if (stat.isFile()) {
|
|
665
1833
|
await searchFile(target);
|
|
666
1834
|
} else {
|
|
@@ -700,6 +1868,7 @@ var HookRegistry = class {
|
|
|
700
1868
|
};
|
|
701
1869
|
|
|
702
1870
|
// src/executor/index.ts
|
|
1871
|
+
var import_node_crypto5 = require("crypto");
|
|
703
1872
|
var DEFAULT_TOOL_TIMEOUT_MS = 12e4;
|
|
704
1873
|
var ToolExecutor = class {
|
|
705
1874
|
tools;
|
|
@@ -708,54 +1877,310 @@ var ToolExecutor = class {
|
|
|
708
1877
|
signal;
|
|
709
1878
|
logger;
|
|
710
1879
|
toolTimeoutMs;
|
|
1880
|
+
eventLogger;
|
|
1881
|
+
sessionId;
|
|
1882
|
+
runId;
|
|
1883
|
+
mode;
|
|
1884
|
+
permissionRules;
|
|
1885
|
+
approvalChannel;
|
|
1886
|
+
approvalCache;
|
|
1887
|
+
policy = new PermissionPolicy();
|
|
1888
|
+
neverFailures = /* @__PURE__ */ new Set();
|
|
1889
|
+
cancellation = new AbortController();
|
|
711
1890
|
constructor(options) {
|
|
712
1891
|
this.tools = options.tools;
|
|
713
1892
|
this.hooks = options.hooks;
|
|
714
1893
|
this.rootDir = options.rootDir;
|
|
715
|
-
this.signal = options.signal;
|
|
1894
|
+
this.signal = options.signal ? AbortSignal.any([options.signal, this.cancellation.signal]) : this.cancellation.signal;
|
|
716
1895
|
this.logger = options.logger;
|
|
717
1896
|
this.toolTimeoutMs = options.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
718
|
-
|
|
719
|
-
|
|
1897
|
+
this.eventLogger = options.eventLogger;
|
|
1898
|
+
this.sessionId = options.sessionId;
|
|
1899
|
+
this.runId = options.runId;
|
|
1900
|
+
this.mode = options.mode ?? "Approval";
|
|
1901
|
+
this.permissionRules = options.permissionRules ?? [];
|
|
1902
|
+
this.approvalChannel = options.approvalChannel;
|
|
1903
|
+
this.approvalCache = options.approvalCache ?? new SessionApprovalCache();
|
|
1904
|
+
}
|
|
1905
|
+
/** 中止本轮正在等待或执行的工具;调用方仍须等待 runAll 完成资源回收及结果审计。 */
|
|
1906
|
+
cancel() {
|
|
1907
|
+
this.cancellation.abort();
|
|
1908
|
+
}
|
|
1909
|
+
async runAll(calls, onProgress) {
|
|
720
1910
|
const results = [];
|
|
1911
|
+
let requiresUserAction = false;
|
|
721
1912
|
for (const call of calls) {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
toolUseId: call.id,
|
|
726
|
-
content: "Tool execution aborted",
|
|
727
|
-
isError: true
|
|
728
|
-
});
|
|
729
|
-
continue;
|
|
730
|
-
}
|
|
731
|
-
results.push(await this.executeOne(call));
|
|
1913
|
+
const result = await this.executeOne(call, onProgress, requiresUserAction);
|
|
1914
|
+
results.push(result);
|
|
1915
|
+
if (result.retryPolicy === "after_user_action") requiresUserAction = true;
|
|
732
1916
|
}
|
|
733
1917
|
return results;
|
|
734
1918
|
}
|
|
735
|
-
|
|
1919
|
+
/** 在最终参数上裁决并审计一次调用,所有拒绝和异常均闭合为工具结果。 */
|
|
1920
|
+
async executeOne(call, onProgress, forcedDeny = false) {
|
|
736
1921
|
const startedAt = Date.now();
|
|
1922
|
+
const operationId = createLogId();
|
|
1923
|
+
const failure = (message, errorCode, retryPolicy = "never") => ({
|
|
1924
|
+
type: "tool_result",
|
|
1925
|
+
toolUseId: call.id,
|
|
1926
|
+
content: JSON.stringify({ code: errorCode, retryPolicy, message }),
|
|
1927
|
+
isError: true,
|
|
1928
|
+
errorCode,
|
|
1929
|
+
retryPolicy,
|
|
1930
|
+
durationMs: Math.max(0, Date.now() - startedAt)
|
|
1931
|
+
});
|
|
1932
|
+
const audit = async (event, fields = {}) => {
|
|
1933
|
+
if (!this.eventLogger || !this.sessionId) return;
|
|
1934
|
+
await this.eventLogger.recordAudit({
|
|
1935
|
+
level: "info",
|
|
1936
|
+
event,
|
|
1937
|
+
sessionId: this.sessionId,
|
|
1938
|
+
runId: this.runId,
|
|
1939
|
+
operationId,
|
|
1940
|
+
toolUseId: call.id,
|
|
1941
|
+
fields: { toolName: call.name, ...fields }
|
|
1942
|
+
});
|
|
1943
|
+
};
|
|
1944
|
+
try {
|
|
1945
|
+
await audit("tool.requested", { status: "requested", processId: process.pid });
|
|
1946
|
+
} catch {
|
|
1947
|
+
return failure(
|
|
1948
|
+
"Audit unavailable; tool was not started",
|
|
1949
|
+
"AUDIT_UNAVAILABLE",
|
|
1950
|
+
"after_user_action"
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
if (forcedDeny) {
|
|
1954
|
+
try {
|
|
1955
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "prior_user_action" });
|
|
1956
|
+
await audit("tool.finished", { status: "skipped", errorCode: "SKIPPED_AFTER_USER_ACTION" });
|
|
1957
|
+
} catch {
|
|
1958
|
+
return failure(
|
|
1959
|
+
"Audit unavailable; tool was not started",
|
|
1960
|
+
"AUDIT_UNAVAILABLE",
|
|
1961
|
+
"after_user_action"
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
return failure(
|
|
1965
|
+
"Earlier tool call requires user action",
|
|
1966
|
+
"SKIPPED_AFTER_USER_ACTION",
|
|
1967
|
+
"after_user_action"
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
737
1970
|
const hookCtx = { signal: this.signal, logger: this.logger };
|
|
738
1971
|
let currentInput = call.input;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1972
|
+
try {
|
|
1973
|
+
for (const hook of this.hooks.get("tool:before")) {
|
|
1974
|
+
const decision = await hook(hookCtx, { id: call.id, name: call.name, input: currentInput });
|
|
1975
|
+
if (decision.action === "modify") currentInput = decision.input;
|
|
1976
|
+
if (decision.action === "skip") {
|
|
1977
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "hook" });
|
|
1978
|
+
await audit("tool.finished", { status: "skipped" });
|
|
1979
|
+
return {
|
|
1980
|
+
type: "tool_result",
|
|
1981
|
+
toolUseId: call.id,
|
|
1982
|
+
content: decision.result,
|
|
1983
|
+
isError: decision.isError ?? false,
|
|
1984
|
+
durationMs: Math.max(0, Date.now() - startedAt)
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
} catch {
|
|
1989
|
+
try {
|
|
1990
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "hook" });
|
|
1991
|
+
await audit("tool.finished", { status: "failed" });
|
|
1992
|
+
} catch {
|
|
1993
|
+
}
|
|
1994
|
+
return failure("Tool preparation failed", "HOOK_ERROR");
|
|
1995
|
+
}
|
|
1996
|
+
let tool;
|
|
1997
|
+
try {
|
|
1998
|
+
tool = this.tools.resolve(call.name);
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
const known = error instanceof KapibalaError ? error : void 0;
|
|
2001
|
+
try {
|
|
2002
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "unavailable" });
|
|
2003
|
+
await audit("tool.finished", { status: "failed" });
|
|
2004
|
+
} catch {
|
|
2005
|
+
}
|
|
2006
|
+
return failure(
|
|
2007
|
+
known?.safeMessage ?? "Requested tool is unavailable",
|
|
2008
|
+
known?.code ?? "TOOL_NOT_FOUND"
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
try {
|
|
2012
|
+
return await this.executeResolved(
|
|
2013
|
+
call,
|
|
2014
|
+
currentInput,
|
|
2015
|
+
tool,
|
|
2016
|
+
startedAt,
|
|
2017
|
+
audit,
|
|
2018
|
+
failure,
|
|
2019
|
+
hookCtx,
|
|
2020
|
+
onProgress
|
|
2021
|
+
);
|
|
2022
|
+
} catch {
|
|
2023
|
+
try {
|
|
2024
|
+
await audit("tool.finished", { status: "unknown", errorCode: "OUTCOME_UNKNOWN" });
|
|
2025
|
+
} catch {
|
|
750
2026
|
}
|
|
751
|
-
|
|
752
|
-
|
|
2027
|
+
return failure(
|
|
2028
|
+
"Tool outcome is unknown after an executor failure",
|
|
2029
|
+
"OUTCOME_UNKNOWN",
|
|
2030
|
+
"after_user_action"
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
async executeResolved(call, currentInput, tool, startedAt, audit, failure, hookCtx, onProgress) {
|
|
2035
|
+
let shell;
|
|
2036
|
+
try {
|
|
2037
|
+
shell = tool.approvalScope?.(currentInput, this.rootDir);
|
|
2038
|
+
} catch {
|
|
2039
|
+
try {
|
|
2040
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "invalid_target" });
|
|
2041
|
+
await audit("tool.finished", { status: "failed", errorCode: "INVALID_TARGET" });
|
|
2042
|
+
} catch {
|
|
753
2043
|
}
|
|
2044
|
+
return failure("Invalid tool execution target", "INVALID_TARGET");
|
|
2045
|
+
}
|
|
2046
|
+
let auditFields = {};
|
|
2047
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2048
|
+
const request = {
|
|
2049
|
+
toolName: call.name,
|
|
2050
|
+
capabilities: tool.metadata?.permissions ?? [],
|
|
2051
|
+
input: currentInput,
|
|
2052
|
+
rootDir: this.rootDir,
|
|
2053
|
+
...shell ? { shell } : {},
|
|
2054
|
+
// 只有无参数的解释器内建查询能确认不依赖外部脚本;其余命令仅允许单次批准。
|
|
2055
|
+
sessionAllowed: !shell || /^(?:pwd|get-location)$/i.test(shell.command.trim()) || shell.scriptDigest !== void 0
|
|
2056
|
+
};
|
|
2057
|
+
const repeatKey = (0, import_node_crypto5.createHash)("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex");
|
|
2058
|
+
if (this.neverFailures.has(repeatKey)) {
|
|
2059
|
+
try {
|
|
2060
|
+
await audit("tool.decided", { decision: "deny", decisionSource: "repeat_block" });
|
|
2061
|
+
await audit("tool.finished", { status: "blocked", errorCode: "REPEAT_BLOCKED" });
|
|
2062
|
+
} catch {
|
|
2063
|
+
return failure(
|
|
2064
|
+
"Audit unavailable; tool was not started",
|
|
2065
|
+
"AUDIT_UNAVAILABLE",
|
|
2066
|
+
"after_user_action"
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
return failure(
|
|
2070
|
+
"Identical failed tool call cannot be repeated in this run",
|
|
2071
|
+
"REPEAT_BLOCKED"
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
const cached = this.approvalCache.get(request);
|
|
2075
|
+
const decision = this.policy.decide({
|
|
2076
|
+
mode: typeof this.mode === "function" ? this.mode() : this.mode,
|
|
2077
|
+
toolName: call.name,
|
|
2078
|
+
capabilities: request.capabilities,
|
|
2079
|
+
dangerous: tool.metadata?.dangerous,
|
|
2080
|
+
cachedApproval: cached === "allow",
|
|
2081
|
+
cachedDenial: cached === "deny",
|
|
2082
|
+
rules: this.permissionRules,
|
|
2083
|
+
...shell ? { shell } : {}
|
|
2084
|
+
});
|
|
2085
|
+
let permitted = decision.decision === "allow";
|
|
2086
|
+
let source = decision.source;
|
|
2087
|
+
let approvalChoice;
|
|
2088
|
+
if (decision.decision === "ask") {
|
|
2089
|
+
let choice = "deny_once";
|
|
2090
|
+
try {
|
|
2091
|
+
choice = await this.approvalChannel?.requestApproval(request, this.signal) ?? "deny_once";
|
|
2092
|
+
} catch {
|
|
2093
|
+
choice = "cancel";
|
|
2094
|
+
}
|
|
2095
|
+
approvalChoice = choice;
|
|
2096
|
+
if (choice === "allow_session" && request.sessionAllowed)
|
|
2097
|
+
this.approvalCache.set(request, "allow");
|
|
2098
|
+
if (choice === "deny_session") this.approvalCache.set(request, "deny");
|
|
2099
|
+
permitted = choice === "allow_once" || choice === "allow_session" && request.sessionAllowed;
|
|
2100
|
+
source = this.approvalChannel ? "human" : "non_interactive";
|
|
2101
|
+
}
|
|
2102
|
+
auditFields = shell ? {
|
|
2103
|
+
shell: shell.interpreter,
|
|
2104
|
+
cwdDigest: (0, import_node_crypto5.createHash)("sha256").update(shell.cwd).digest("hex"),
|
|
2105
|
+
executableDigest: shell.executableDigest,
|
|
2106
|
+
executionFingerprint: shell.executionFingerprint,
|
|
2107
|
+
...commandAuditSummary(shell.command)
|
|
2108
|
+
} : {};
|
|
2109
|
+
try {
|
|
2110
|
+
await audit("tool.decided", {
|
|
2111
|
+
decision: permitted ? "allow" : "deny",
|
|
2112
|
+
decisionSource: source,
|
|
2113
|
+
...approvalChoice ? { approvalChoice } : {},
|
|
2114
|
+
...auditFields
|
|
2115
|
+
});
|
|
2116
|
+
} catch {
|
|
2117
|
+
return failure(
|
|
2118
|
+
"Audit unavailable; tool was not started",
|
|
2119
|
+
"AUDIT_UNAVAILABLE",
|
|
2120
|
+
"after_user_action"
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
if (!permitted || this.signal?.aborted) {
|
|
2124
|
+
const code = this.signal?.aborted ? "ABORTED" : "PERMISSION_DENIED";
|
|
2125
|
+
try {
|
|
2126
|
+
await audit("tool.finished", { status: "denied", errorCode: code });
|
|
2127
|
+
} catch {
|
|
2128
|
+
return failure("Audit result unavailable", "AUDIT_UNAVAILABLE", "after_user_action");
|
|
2129
|
+
}
|
|
2130
|
+
return failure(
|
|
2131
|
+
this.signal?.aborted ? "Operation cancelled" : "Tool permission denied",
|
|
2132
|
+
code,
|
|
2133
|
+
"after_user_action"
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
if (shell) {
|
|
2137
|
+
try {
|
|
2138
|
+
const next = tool.approvalScope?.(currentInput, this.rootDir);
|
|
2139
|
+
if (next?.executionFingerprint !== shell.executionFingerprint) {
|
|
2140
|
+
if (attempt === 0 && next) {
|
|
2141
|
+
try {
|
|
2142
|
+
await audit("tool.target_changed", { status: "redeciding" });
|
|
2143
|
+
} catch {
|
|
2144
|
+
return failure(
|
|
2145
|
+
"Audit unavailable; tool was not started",
|
|
2146
|
+
"AUDIT_UNAVAILABLE",
|
|
2147
|
+
"after_user_action"
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
shell = next;
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
await audit("tool.finished", { status: "target_changed", errorCode: "TARGET_CHANGED" });
|
|
2154
|
+
return failure(
|
|
2155
|
+
"Execution target changed after approval",
|
|
2156
|
+
"TARGET_CHANGED",
|
|
2157
|
+
"after_user_action"
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
} catch {
|
|
2161
|
+
return failure(
|
|
2162
|
+
"Execution target changed after approval",
|
|
2163
|
+
"TARGET_CHANGED",
|
|
2164
|
+
"after_user_action"
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
break;
|
|
2169
|
+
}
|
|
2170
|
+
try {
|
|
2171
|
+
await audit("tool.started", { status: "started", ...auditFields });
|
|
2172
|
+
} catch {
|
|
2173
|
+
return failure(
|
|
2174
|
+
"Audit unavailable; tool was not started",
|
|
2175
|
+
"AUDIT_UNAVAILABLE",
|
|
2176
|
+
"after_user_action"
|
|
2177
|
+
);
|
|
754
2178
|
}
|
|
755
2179
|
let outputStr;
|
|
756
2180
|
let isError = false;
|
|
2181
|
+
let errorCode;
|
|
2182
|
+
let retryPolicy;
|
|
757
2183
|
try {
|
|
758
|
-
const tool = this.tools.resolve(call.name);
|
|
759
2184
|
const executionController = new AbortController();
|
|
760
2185
|
const abortFromSession = () => executionController.abort();
|
|
761
2186
|
if (this.signal?.aborted) {
|
|
@@ -766,37 +2191,85 @@ var ToolExecutor = class {
|
|
|
766
2191
|
const toolCtx = {
|
|
767
2192
|
rootDir: this.rootDir,
|
|
768
2193
|
signal: executionController.signal,
|
|
769
|
-
logger: this.logger
|
|
2194
|
+
logger: this.logger,
|
|
2195
|
+
onProgress: (progress) => onProgress?.({ id: call.id, name: call.name, ...progress }),
|
|
2196
|
+
...shell ? {
|
|
2197
|
+
onProcessSpawned: () => audit("process.spawned", { status: "spawned", ...auditFields })
|
|
2198
|
+
} : {}
|
|
770
2199
|
};
|
|
771
2200
|
try {
|
|
772
|
-
const rawResult = await this.withTimeout(
|
|
2201
|
+
const rawResult = tool.metadata?.managesTimeout ? await tool.execute(currentInput, toolCtx) : await this.withTimeout(
|
|
773
2202
|
tool.execute(currentInput, toolCtx),
|
|
774
2203
|
call.name,
|
|
775
2204
|
executionController
|
|
776
2205
|
);
|
|
777
2206
|
outputStr = typeof rawResult === "string" ? rawResult : JSON.stringify(rawResult, null, 2);
|
|
2207
|
+
if (call.name === "run_command" && typeof rawResult === "object" && rawResult !== null && "exitCode" in rawResult && rawResult.exitCode !== 0) {
|
|
2208
|
+
isError = true;
|
|
2209
|
+
errorCode = "COMMAND_EXIT_NONZERO";
|
|
2210
|
+
retryPolicy = "never";
|
|
2211
|
+
}
|
|
778
2212
|
} finally {
|
|
779
2213
|
this.signal?.removeEventListener("abort", abortFromSession);
|
|
780
2214
|
}
|
|
781
2215
|
} catch (err) {
|
|
782
2216
|
isError = true;
|
|
783
|
-
|
|
2217
|
+
const known = err instanceof KapibalaError ? err : void 0;
|
|
2218
|
+
outputStr = known?.safeMessage ?? "Tool execution failed";
|
|
2219
|
+
errorCode = known?.code ?? "TOOL_FAILED";
|
|
2220
|
+
retryPolicy = known?.retryPolicy ?? "never";
|
|
784
2221
|
}
|
|
785
2222
|
const resultBlock = {
|
|
786
2223
|
type: "tool_result",
|
|
787
2224
|
toolUseId: call.id,
|
|
788
2225
|
content: outputStr,
|
|
789
|
-
isError
|
|
2226
|
+
isError,
|
|
2227
|
+
...errorCode ? { errorCode, retryPolicy } : {}
|
|
790
2228
|
};
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
2229
|
+
if (resultBlock.isError && resultBlock.errorCode) {
|
|
2230
|
+
resultBlock.content = JSON.stringify({
|
|
2231
|
+
code: resultBlock.errorCode,
|
|
2232
|
+
retryPolicy: resultBlock.retryPolicy,
|
|
2233
|
+
message: outputStr
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
try {
|
|
2237
|
+
for (const hook of this.hooks.get("tool:after")) {
|
|
2238
|
+
await hook(
|
|
2239
|
+
hookCtx,
|
|
2240
|
+
{ id: call.id, name: call.name, input: currentInput },
|
|
2241
|
+
{ output: outputStr, isError }
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
} catch {
|
|
2245
|
+
resultBlock.content = JSON.stringify({
|
|
2246
|
+
code: "HOOK_ERROR",
|
|
2247
|
+
retryPolicy: "after_user_action",
|
|
2248
|
+
message: "Tool finished but result processing failed"
|
|
2249
|
+
});
|
|
2250
|
+
resultBlock.isError = true;
|
|
2251
|
+
resultBlock.errorCode = "HOOK_ERROR";
|
|
2252
|
+
resultBlock.retryPolicy = "after_user_action";
|
|
798
2253
|
}
|
|
799
2254
|
resultBlock.durationMs = Math.max(0, Date.now() - startedAt);
|
|
2255
|
+
if (resultBlock.isError && resultBlock.retryPolicy === "never") {
|
|
2256
|
+
this.neverFailures.add(
|
|
2257
|
+
(0, import_node_crypto5.createHash)("sha256").update(stableJsonStringify([call.name, currentInput])).digest("hex")
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
try {
|
|
2261
|
+
await audit("tool.finished", {
|
|
2262
|
+
status: resultBlock.isError ? "failed" : "completed",
|
|
2263
|
+
...resultBlock.errorCode ? { errorCode: resultBlock.errorCode } : {},
|
|
2264
|
+
durationMs: resultBlock.durationMs
|
|
2265
|
+
});
|
|
2266
|
+
} catch {
|
|
2267
|
+
return failure(
|
|
2268
|
+
"Tool outcome is unknown because the audit result could not be written",
|
|
2269
|
+
"OUTCOME_UNKNOWN",
|
|
2270
|
+
"after_user_action"
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
800
2273
|
return resultBlock;
|
|
801
2274
|
}
|
|
802
2275
|
/**
|
|
@@ -810,7 +2283,14 @@ var ToolExecutor = class {
|
|
|
810
2283
|
let timer;
|
|
811
2284
|
const timeout = new Promise((_, reject) => {
|
|
812
2285
|
timer = setTimeout(() => {
|
|
813
|
-
const error = new
|
|
2286
|
+
const error = new KapibalaError(
|
|
2287
|
+
`Tool '${toolName}' timed out after ${this.toolTimeoutMs}ms`,
|
|
2288
|
+
{
|
|
2289
|
+
code: "OUTCOME_UNKNOWN",
|
|
2290
|
+
retryPolicy: "after_user_action",
|
|
2291
|
+
safeMessage: "Tool timed out; completion of side effects is unknown"
|
|
2292
|
+
}
|
|
2293
|
+
);
|
|
814
2294
|
reject(error);
|
|
815
2295
|
executionController.abort(error);
|
|
816
2296
|
}, this.toolTimeoutMs);
|
|
@@ -1145,15 +2625,15 @@ var OpenAICompatibleProvider = class {
|
|
|
1145
2625
|
};
|
|
1146
2626
|
|
|
1147
2627
|
// src/store/jsonl.ts
|
|
1148
|
-
var
|
|
1149
|
-
var
|
|
2628
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
2629
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1150
2630
|
var JSONLMessageStore = class {
|
|
1151
2631
|
filePath;
|
|
1152
2632
|
constructor(filePath) {
|
|
1153
|
-
this.filePath =
|
|
1154
|
-
const dir =
|
|
1155
|
-
if (!
|
|
1156
|
-
|
|
2633
|
+
this.filePath = import_node_path8.default.resolve(filePath);
|
|
2634
|
+
const dir = import_node_path8.default.dirname(this.filePath);
|
|
2635
|
+
if (!import_node_fs7.default.existsSync(dir)) {
|
|
2636
|
+
import_node_fs7.default.mkdirSync(dir, { recursive: true });
|
|
1157
2637
|
}
|
|
1158
2638
|
}
|
|
1159
2639
|
async append(message) {
|
|
@@ -1162,14 +2642,14 @@ var JSONLMessageStore = class {
|
|
|
1162
2642
|
role: message.role,
|
|
1163
2643
|
content: message.content
|
|
1164
2644
|
};
|
|
1165
|
-
|
|
2645
|
+
import_node_fs7.default.appendFileSync(this.filePath, `${JSON.stringify(record)}
|
|
1166
2646
|
`, "utf-8");
|
|
1167
2647
|
}
|
|
1168
2648
|
async load() {
|
|
1169
|
-
if (!
|
|
2649
|
+
if (!import_node_fs7.default.existsSync(this.filePath)) {
|
|
1170
2650
|
return [];
|
|
1171
2651
|
}
|
|
1172
|
-
const content =
|
|
2652
|
+
const content = import_node_fs7.default.readFileSync(this.filePath, "utf-8");
|
|
1173
2653
|
const lines = content.split("\n");
|
|
1174
2654
|
const messages = [];
|
|
1175
2655
|
for (const line of lines) {
|
|
@@ -1191,8 +2671,8 @@ var JSONLMessageStore = class {
|
|
|
1191
2671
|
return messages;
|
|
1192
2672
|
}
|
|
1193
2673
|
async clear() {
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
2674
|
+
if (import_node_fs7.default.existsSync(this.filePath)) {
|
|
2675
|
+
import_node_fs7.default.unlinkSync(this.filePath);
|
|
1196
2676
|
}
|
|
1197
2677
|
}
|
|
1198
2678
|
/**
|
|
@@ -1215,7 +2695,9 @@ var JSONLMessageStore = class {
|
|
|
1215
2695
|
type: "tool_result",
|
|
1216
2696
|
toolUseId: toolUse.id,
|
|
1217
2697
|
content: "Tool execution was interrupted or crashed in previous session",
|
|
1218
|
-
isError: true
|
|
2698
|
+
isError: true,
|
|
2699
|
+
errorCode: "OUTCOME_UNKNOWN",
|
|
2700
|
+
retryPolicy: "after_user_action"
|
|
1219
2701
|
})
|
|
1220
2702
|
),
|
|
1221
2703
|
timestamp: Date.now()
|
|
@@ -1247,17 +2729,21 @@ var JSONLMessageStore = class {
|
|
|
1247
2729
|
};
|
|
1248
2730
|
|
|
1249
2731
|
// src/prompt/index.ts
|
|
1250
|
-
var
|
|
2732
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
1251
2733
|
var PromptAssembler = class {
|
|
1252
2734
|
tools;
|
|
1253
2735
|
rootDir;
|
|
1254
2736
|
agentName;
|
|
1255
2737
|
customInstructions;
|
|
2738
|
+
instructionSources;
|
|
2739
|
+
visibleTools;
|
|
1256
2740
|
constructor(options) {
|
|
1257
2741
|
this.tools = options.tools;
|
|
1258
2742
|
this.rootDir = options.rootDir;
|
|
1259
2743
|
this.agentName = options.agentName ?? "Kapibala";
|
|
1260
2744
|
this.customInstructions = options.customInstructions;
|
|
2745
|
+
this.instructionSources = options.instructionSources ?? [];
|
|
2746
|
+
this.visibleTools = options.visibleTools;
|
|
1261
2747
|
}
|
|
1262
2748
|
assemble() {
|
|
1263
2749
|
const sections = [];
|
|
@@ -1269,6 +2755,13 @@ var PromptAssembler = class {
|
|
|
1269
2755
|
if (this.customInstructions?.trim()) {
|
|
1270
2756
|
sections.push(this.customInstructions.trim());
|
|
1271
2757
|
}
|
|
2758
|
+
if (this.instructionSources.length > 0) {
|
|
2759
|
+
sections.push(
|
|
2760
|
+
`# L4 Project Instructions
|
|
2761
|
+
${this.instructionSources.map((source, index) => `## ${index + 1}. ${source.path}
|
|
2762
|
+
${source.content}`).join("\n\n")}`
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
1272
2765
|
return sections.filter(Boolean).join("\n\n---\n\n");
|
|
1273
2766
|
}
|
|
1274
2767
|
getL1Behavior() {
|
|
@@ -1285,7 +2778,7 @@ Follow these key behavioral rules:
|
|
|
1285
2778
|
}
|
|
1286
2779
|
getL2Tools() {
|
|
1287
2780
|
if (!this.tools) return "";
|
|
1288
|
-
const tools = this.tools.list();
|
|
2781
|
+
const tools = this.visibleTools ?? this.tools.list();
|
|
1289
2782
|
if (tools.length === 0) return "";
|
|
1290
2783
|
const lines = ["# Available Tools:"];
|
|
1291
2784
|
for (const tool of tools) {
|
|
@@ -1295,7 +2788,7 @@ Follow these key behavioral rules:
|
|
|
1295
2788
|
}
|
|
1296
2789
|
getL3Environment() {
|
|
1297
2790
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1298
|
-
const platform =
|
|
2791
|
+
const platform = import_node_os4.default.platform();
|
|
1299
2792
|
return `# Environment Context:
|
|
1300
2793
|
- Current Working Directory (Workspace Root): ${this.rootDir}
|
|
1301
2794
|
- Operating System: ${platform}
|
|
@@ -1313,6 +2806,11 @@ var AgentLoop = class {
|
|
|
1313
2806
|
maxSteps;
|
|
1314
2807
|
maxConsecutiveErrors;
|
|
1315
2808
|
signal;
|
|
2809
|
+
eventLogger;
|
|
2810
|
+
sessionId;
|
|
2811
|
+
runId;
|
|
2812
|
+
getMode;
|
|
2813
|
+
permissionRules;
|
|
1316
2814
|
constructor(options) {
|
|
1317
2815
|
this.provider = options.provider;
|
|
1318
2816
|
this.executor = options.executor;
|
|
@@ -1322,6 +2820,11 @@ var AgentLoop = class {
|
|
|
1322
2820
|
this.maxSteps = options.maxSteps ?? 20;
|
|
1323
2821
|
this.maxConsecutiveErrors = options.maxConsecutiveErrors ?? 3;
|
|
1324
2822
|
this.signal = options.signal;
|
|
2823
|
+
this.eventLogger = options.eventLogger;
|
|
2824
|
+
this.sessionId = options.sessionId;
|
|
2825
|
+
this.runId = options.runId;
|
|
2826
|
+
this.getMode = options.getMode ?? (() => "Approval");
|
|
2827
|
+
this.permissionRules = options.permissionRules ?? [];
|
|
1325
2828
|
}
|
|
1326
2829
|
async *run(history) {
|
|
1327
2830
|
let step = 0;
|
|
@@ -1337,7 +2840,9 @@ var AgentLoop = class {
|
|
|
1337
2840
|
let request = {
|
|
1338
2841
|
systemPrompt: this.systemPrompt,
|
|
1339
2842
|
messages: history,
|
|
1340
|
-
tools: this.tools.
|
|
2843
|
+
tools: visibleTools(this.tools.list(), this.getMode(), this.permissionRules).map(
|
|
2844
|
+
toToolDefinition
|
|
2845
|
+
),
|
|
1341
2846
|
signal: this.signal
|
|
1342
2847
|
};
|
|
1343
2848
|
const beforeModelHooks = this.hooks.get("model:before");
|
|
@@ -1360,6 +2865,15 @@ var AgentLoop = class {
|
|
|
1360
2865
|
let turnUsage;
|
|
1361
2866
|
let turnTtftMs;
|
|
1362
2867
|
const modelStartTime = Date.now();
|
|
2868
|
+
if (this.eventLogger && this.sessionId) {
|
|
2869
|
+
await this.eventLogger.record({
|
|
2870
|
+
level: "info",
|
|
2871
|
+
event: "model.requested",
|
|
2872
|
+
sessionId: this.sessionId,
|
|
2873
|
+
runId: this.runId,
|
|
2874
|
+
fields: { attempt: step, count: request.messages.length }
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
1363
2877
|
let firstTokenReceived = false;
|
|
1364
2878
|
const buildMetrics = (modelDurationMs2, toolDurationMs2, toolCallsCount) => {
|
|
1365
2879
|
const endTime = Date.now();
|
|
@@ -1420,6 +2934,15 @@ var AgentLoop = class {
|
|
|
1420
2934
|
throw new AbortError();
|
|
1421
2935
|
}
|
|
1422
2936
|
const modelError = err instanceof Error ? err : new Error(String(err));
|
|
2937
|
+
if (this.eventLogger && this.sessionId) {
|
|
2938
|
+
await this.eventLogger.record({
|
|
2939
|
+
level: "error",
|
|
2940
|
+
event: "model.failed",
|
|
2941
|
+
sessionId: this.sessionId,
|
|
2942
|
+
runId: this.runId,
|
|
2943
|
+
fields: { attempt: step, durationMs: Date.now() - modelStartTime }
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
1423
2946
|
await this.hooks.emit("error", hookCtx, modelError);
|
|
1424
2947
|
yield { type: "error", error: modelError };
|
|
1425
2948
|
yield {
|
|
@@ -1431,6 +2954,20 @@ var AgentLoop = class {
|
|
|
1431
2954
|
break;
|
|
1432
2955
|
}
|
|
1433
2956
|
const modelDurationMs = Date.now() - modelStartTime;
|
|
2957
|
+
if (this.eventLogger && this.sessionId) {
|
|
2958
|
+
await this.eventLogger.record({
|
|
2959
|
+
level: "info",
|
|
2960
|
+
event: "model.finished",
|
|
2961
|
+
sessionId: this.sessionId,
|
|
2962
|
+
runId: this.runId,
|
|
2963
|
+
fields: {
|
|
2964
|
+
attempt: step,
|
|
2965
|
+
durationMs: modelDurationMs,
|
|
2966
|
+
promptTokens: turnUsage?.promptTokens ?? 0,
|
|
2967
|
+
completionTokens: turnUsage?.completionTokens ?? 0
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
1434
2971
|
yield {
|
|
1435
2972
|
type: "step_log",
|
|
1436
2973
|
log: {
|
|
@@ -1496,12 +3033,44 @@ var AgentLoop = class {
|
|
|
1496
3033
|
yield { type: "tool_start", id: call.id, name: call.name, input: call.input };
|
|
1497
3034
|
}
|
|
1498
3035
|
const toolStartTime = Date.now();
|
|
1499
|
-
const
|
|
3036
|
+
const progressQueue = [];
|
|
3037
|
+
let progressWake;
|
|
3038
|
+
let toolsFinished = false;
|
|
3039
|
+
const toolRun = this.executor.runAll(toolCalls, (progress) => {
|
|
3040
|
+
progressQueue.push({ type: "tool_progress", ...progress });
|
|
3041
|
+
progressWake?.();
|
|
3042
|
+
});
|
|
3043
|
+
void toolRun.then(
|
|
3044
|
+
() => {
|
|
3045
|
+
toolsFinished = true;
|
|
3046
|
+
progressWake?.();
|
|
3047
|
+
},
|
|
3048
|
+
() => {
|
|
3049
|
+
toolsFinished = true;
|
|
3050
|
+
progressWake?.();
|
|
3051
|
+
}
|
|
3052
|
+
);
|
|
3053
|
+
let toolResults;
|
|
3054
|
+
let toolMessages;
|
|
3055
|
+
try {
|
|
3056
|
+
while (!toolsFinished || progressQueue.length > 0) {
|
|
3057
|
+
const next = progressQueue.shift();
|
|
3058
|
+
if (next) yield next;
|
|
3059
|
+
else
|
|
3060
|
+
await new Promise((resolve) => {
|
|
3061
|
+
progressWake = resolve;
|
|
3062
|
+
});
|
|
3063
|
+
progressWake = void 0;
|
|
3064
|
+
}
|
|
3065
|
+
} finally {
|
|
3066
|
+
if (!toolsFinished) this.executor.cancel();
|
|
3067
|
+
toolResults = await toolRun;
|
|
3068
|
+
toolMessages = this.provider.assembleToolResults(toolResults);
|
|
3069
|
+
history.push(assistantMessage, ...toolMessages);
|
|
3070
|
+
}
|
|
1500
3071
|
const toolDurationMs = Date.now() - toolStartTime;
|
|
1501
3072
|
const hasError = toolResults.some((result) => result.isError);
|
|
1502
3073
|
const metrics = buildMetrics(modelDurationMs, toolDurationMs, toolCalls.length);
|
|
1503
|
-
const toolMessages = this.provider.assembleToolResults(toolResults);
|
|
1504
|
-
history.push(assistantMessage, ...toolMessages);
|
|
1505
3074
|
yield {
|
|
1506
3075
|
type: "message_stop",
|
|
1507
3076
|
message: assistantMessage,
|
|
@@ -1531,6 +3100,19 @@ var AgentLoop = class {
|
|
|
1531
3100
|
}
|
|
1532
3101
|
};
|
|
1533
3102
|
yield { type: "tool_messages", messages: toolMessages };
|
|
3103
|
+
if (toolResults.some((result) => result.retryPolicy === "after_user_action")) {
|
|
3104
|
+
if (this.signal?.aborted) {
|
|
3105
|
+
yield { type: "turn_finish", turn: step, usage: turnUsage, metrics };
|
|
3106
|
+
throw new AbortError();
|
|
3107
|
+
}
|
|
3108
|
+
yield {
|
|
3109
|
+
type: "error",
|
|
3110
|
+
error: new Error("Tool execution requires user action before continuing")
|
|
3111
|
+
};
|
|
3112
|
+
yield { type: "turn_finish", turn: step, usage: turnUsage, metrics };
|
|
3113
|
+
completed = true;
|
|
3114
|
+
break;
|
|
3115
|
+
}
|
|
1534
3116
|
if (hasError) {
|
|
1535
3117
|
consecutiveErrors++;
|
|
1536
3118
|
if (consecutiveErrors >= this.maxConsecutiveErrors) {
|
|
@@ -1580,6 +3162,17 @@ var AgentSession = class {
|
|
|
1580
3162
|
customSystemPrompt;
|
|
1581
3163
|
maxSteps;
|
|
1582
3164
|
logger;
|
|
3165
|
+
eventLogger;
|
|
3166
|
+
sessionId = createLogId();
|
|
3167
|
+
mode;
|
|
3168
|
+
permissionRules;
|
|
3169
|
+
approvalChannel;
|
|
3170
|
+
approvalCache = new SessionApprovalCache();
|
|
3171
|
+
projectRoot;
|
|
3172
|
+
cwd;
|
|
3173
|
+
userInstructionsPath;
|
|
3174
|
+
instructionSnapshot;
|
|
3175
|
+
auditRecoveryDirectory;
|
|
1583
3176
|
history = [];
|
|
1584
3177
|
totalTurns = 0;
|
|
1585
3178
|
usageStats = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
@@ -1599,13 +3192,49 @@ var AgentSession = class {
|
|
|
1599
3192
|
this.customSystemPrompt = config.systemPrompt;
|
|
1600
3193
|
this.maxSteps = config.maxSteps ?? 20;
|
|
1601
3194
|
this.logger = config.logger;
|
|
3195
|
+
validatePermissionRules(config.permissionRules ?? []);
|
|
3196
|
+
this.mode = config.mode ?? "Approval";
|
|
3197
|
+
this.permissionRules = config.permissionRules ?? [];
|
|
3198
|
+
this.approvalChannel = config.approvalChannel;
|
|
3199
|
+
this.projectRoot = config.projectRoot ?? this.rootDir;
|
|
3200
|
+
this.cwd = config.cwd ?? this.rootDir;
|
|
3201
|
+
this.userInstructionsPath = config.userInstructionsPath;
|
|
3202
|
+
const sinks = config.eventLogger ? void 0 : createDefaultLogSinks(config.loggingDirectory);
|
|
3203
|
+
this.eventLogger = config.eventLogger ?? new StructuredLogger({
|
|
3204
|
+
operationSink: sinks.operationSink,
|
|
3205
|
+
auditSink: sinks.auditSink,
|
|
3206
|
+
debug: config.logger !== void 0,
|
|
3207
|
+
onDiagnostic: config.onDiagnostic ?? config.logger
|
|
3208
|
+
});
|
|
3209
|
+
this.auditRecoveryDirectory = config.auditRecoveryDirectory ?? sinks?.auditSink.directory;
|
|
1602
3210
|
}
|
|
1603
3211
|
async init() {
|
|
1604
3212
|
if (this.initialized) return;
|
|
3213
|
+
if (this.auditRecoveryDirectory) {
|
|
3214
|
+
const count = await recoverIncompleteAudit(this.auditRecoveryDirectory);
|
|
3215
|
+
if (count > 0)
|
|
3216
|
+
await this.eventLogger.record({
|
|
3217
|
+
level: "warn",
|
|
3218
|
+
event: "audit.recovered_unknown",
|
|
3219
|
+
sessionId: this.sessionId,
|
|
3220
|
+
fields: { count }
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
this.instructionSnapshot = loadInstructions({
|
|
3224
|
+
projectRoot: this.projectRoot,
|
|
3225
|
+
cwd: this.cwd,
|
|
3226
|
+
userFile: this.userInstructionsPath
|
|
3227
|
+
});
|
|
1605
3228
|
if (this.store) {
|
|
1606
3229
|
this.history = await this.store.load();
|
|
1607
3230
|
}
|
|
1608
3231
|
await this.hooks.emit("session:start", { logger: this.logger });
|
|
3232
|
+
await this.eventLogger.record({
|
|
3233
|
+
level: "info",
|
|
3234
|
+
event: "session.started",
|
|
3235
|
+
sessionId: this.sessionId,
|
|
3236
|
+
fields: { modelId: this.router.getProfile("default").id }
|
|
3237
|
+
});
|
|
1609
3238
|
this.initialized = true;
|
|
1610
3239
|
}
|
|
1611
3240
|
async use(plugin) {
|
|
@@ -1633,6 +3262,18 @@ var AgentSession = class {
|
|
|
1633
3262
|
getActiveProfile(role = "default") {
|
|
1634
3263
|
return this.router.getProfile(role);
|
|
1635
3264
|
}
|
|
3265
|
+
/** 只在空闲时切换本次会话模式;宿主负责 FullAccess 的显式选择交互。 */
|
|
3266
|
+
switchMode(mode) {
|
|
3267
|
+
this.assertIdle("switch permission mode");
|
|
3268
|
+
this.mode = mode;
|
|
3269
|
+
}
|
|
3270
|
+
getMode() {
|
|
3271
|
+
return this.mode;
|
|
3272
|
+
}
|
|
3273
|
+
/** 返回最近一次成功装载的来源路径,不暴露指令正文。 */
|
|
3274
|
+
getInstructionSources() {
|
|
3275
|
+
return this.instructionSnapshot?.sources.map((source) => source.path) ?? [];
|
|
3276
|
+
}
|
|
1636
3277
|
getHistory() {
|
|
1637
3278
|
return [...this.history];
|
|
1638
3279
|
}
|
|
@@ -1680,6 +3321,7 @@ var AgentSession = class {
|
|
|
1680
3321
|
this.assertIdle("start another run");
|
|
1681
3322
|
resolveContextWindow(this.router.getProfile("default").contextWindow);
|
|
1682
3323
|
this.activeRun = true;
|
|
3324
|
+
const runId = createLogId();
|
|
1683
3325
|
const runStartTime = Date.now();
|
|
1684
3326
|
const aggregate = {
|
|
1685
3327
|
modelDurationMs: 0,
|
|
@@ -1709,6 +3351,28 @@ var AgentSession = class {
|
|
|
1709
3351
|
if (!this.initialized) {
|
|
1710
3352
|
await this.init();
|
|
1711
3353
|
}
|
|
3354
|
+
await this.eventLogger.record({
|
|
3355
|
+
level: "info",
|
|
3356
|
+
event: "run.started",
|
|
3357
|
+
sessionId: this.sessionId,
|
|
3358
|
+
runId
|
|
3359
|
+
});
|
|
3360
|
+
try {
|
|
3361
|
+
this.instructionSnapshot = loadInstructions({
|
|
3362
|
+
projectRoot: this.projectRoot,
|
|
3363
|
+
cwd: this.cwd,
|
|
3364
|
+
userFile: this.userInstructionsPath
|
|
3365
|
+
});
|
|
3366
|
+
} catch (error) {
|
|
3367
|
+
await this.eventLogger.record({
|
|
3368
|
+
level: "warn",
|
|
3369
|
+
event: "instructions.refresh_failed",
|
|
3370
|
+
sessionId: this.sessionId,
|
|
3371
|
+
runId,
|
|
3372
|
+
fields: { status: "retained_previous_snapshot" }
|
|
3373
|
+
});
|
|
3374
|
+
if (!this.instructionSnapshot) throw error;
|
|
3375
|
+
}
|
|
1712
3376
|
this.totalTurns++;
|
|
1713
3377
|
const userMessage = {
|
|
1714
3378
|
role: "user",
|
|
@@ -1722,7 +3386,9 @@ var AgentSession = class {
|
|
|
1722
3386
|
const promptAssembler = new PromptAssembler({
|
|
1723
3387
|
tools: this.tools,
|
|
1724
3388
|
rootDir: this.rootDir,
|
|
1725
|
-
customInstructions: this.customSystemPrompt
|
|
3389
|
+
customInstructions: this.customSystemPrompt,
|
|
3390
|
+
instructionSources: this.instructionSnapshot.sources,
|
|
3391
|
+
visibleTools: visibleTools(this.tools.list(), this.mode, this.permissionRules)
|
|
1726
3392
|
});
|
|
1727
3393
|
const assembledSystemPrompt = promptAssembler.assemble();
|
|
1728
3394
|
const executor = new ToolExecutor({
|
|
@@ -1730,7 +3396,14 @@ var AgentSession = class {
|
|
|
1730
3396
|
hooks: this.hooks,
|
|
1731
3397
|
rootDir: this.rootDir,
|
|
1732
3398
|
signal: options?.signal,
|
|
1733
|
-
logger: this.logger
|
|
3399
|
+
logger: this.logger,
|
|
3400
|
+
eventLogger: this.eventLogger,
|
|
3401
|
+
sessionId: this.sessionId,
|
|
3402
|
+
runId,
|
|
3403
|
+
mode: () => this.mode,
|
|
3404
|
+
permissionRules: this.permissionRules,
|
|
3405
|
+
approvalChannel: this.approvalChannel,
|
|
3406
|
+
approvalCache: this.approvalCache
|
|
1734
3407
|
});
|
|
1735
3408
|
const loop = new AgentLoop({
|
|
1736
3409
|
provider: this.router.resolve("default"),
|
|
@@ -1739,7 +3412,12 @@ var AgentSession = class {
|
|
|
1739
3412
|
hooks: this.hooks,
|
|
1740
3413
|
systemPrompt: assembledSystemPrompt,
|
|
1741
3414
|
maxSteps: this.maxSteps,
|
|
1742
|
-
signal: options?.signal
|
|
3415
|
+
signal: options?.signal,
|
|
3416
|
+
eventLogger: this.eventLogger,
|
|
3417
|
+
sessionId: this.sessionId,
|
|
3418
|
+
runId,
|
|
3419
|
+
getMode: () => this.mode,
|
|
3420
|
+
permissionRules: this.permissionRules
|
|
1743
3421
|
});
|
|
1744
3422
|
let pendingAssistant;
|
|
1745
3423
|
let pendingEvents = [];
|
|
@@ -1747,10 +3425,24 @@ var AgentSession = class {
|
|
|
1747
3425
|
let pendingAssistantPersisted = false;
|
|
1748
3426
|
let pendingPersistenceStarted = false;
|
|
1749
3427
|
let pendingToolMessages = [];
|
|
3428
|
+
let persistedHistoryLength = this.history.length;
|
|
3429
|
+
let loopCompleted = false;
|
|
3430
|
+
let loopFailed = false;
|
|
1750
3431
|
try {
|
|
1751
3432
|
for await (const event of loop.run(this.history)) {
|
|
1752
3433
|
if (event.type === "step_log") {
|
|
1753
3434
|
this.logger?.(`[Turn ${event.log.turn} | ${event.log.stage}] ${event.log.message}`);
|
|
3435
|
+
await this.eventLogger.record({
|
|
3436
|
+
level: "debug",
|
|
3437
|
+
event: "diagnostic.step",
|
|
3438
|
+
sessionId: this.sessionId,
|
|
3439
|
+
runId,
|
|
3440
|
+
fields: {
|
|
3441
|
+
phase: event.log.stage,
|
|
3442
|
+
durationMs: event.log.durationMs ?? 0,
|
|
3443
|
+
attempt: event.log.turn
|
|
3444
|
+
}
|
|
3445
|
+
});
|
|
1754
3446
|
}
|
|
1755
3447
|
if (event.type === "turn_finish") {
|
|
1756
3448
|
this.lastMetrics = event.metrics;
|
|
@@ -1785,6 +3477,7 @@ var AgentSession = class {
|
|
|
1785
3477
|
await this.persistAssistant(pendingAssistant);
|
|
1786
3478
|
pendingAssistantPersisted = true;
|
|
1787
3479
|
await this.persistToolMessages(event.messages);
|
|
3480
|
+
persistedHistoryLength = this.history.length;
|
|
1788
3481
|
const releasable = pendingEvents;
|
|
1789
3482
|
pendingAssistant = void 0;
|
|
1790
3483
|
pendingEvents = [];
|
|
@@ -1796,9 +3489,14 @@ var AgentSession = class {
|
|
|
1796
3489
|
} else if (event.type === "tool_messages") {
|
|
1797
3490
|
await this.persistToolMessages(event.messages);
|
|
1798
3491
|
}
|
|
3492
|
+
if (event.type === "message_stop" || event.type === "tool_messages") {
|
|
3493
|
+
persistedHistoryLength = this.history.length;
|
|
3494
|
+
}
|
|
1799
3495
|
yield event;
|
|
1800
3496
|
}
|
|
3497
|
+
loopCompleted = true;
|
|
1801
3498
|
} catch (error) {
|
|
3499
|
+
loopFailed = true;
|
|
1802
3500
|
if (pendingAssistant && !pendingPersistenceStarted) {
|
|
1803
3501
|
const interruptedMessages = pendingClosedInMemory ? pendingToolMessages : this.closeInterruptedToolCalls(pendingAssistant, error);
|
|
1804
3502
|
if (!pendingAssistantPersisted) {
|
|
@@ -1809,16 +3507,36 @@ var AgentSession = class {
|
|
|
1809
3507
|
yield { type: "tool_messages", messages: interruptedMessages };
|
|
1810
3508
|
}
|
|
1811
3509
|
throw error;
|
|
3510
|
+
} finally {
|
|
3511
|
+
if (!loopCompleted && !loopFailed && this.store) {
|
|
3512
|
+
for (const message of this.history.slice(persistedHistoryLength)) {
|
|
3513
|
+
await this.store.append(message);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
1812
3516
|
}
|
|
1813
3517
|
const metrics = finalizeRun(runStatus);
|
|
1814
3518
|
this.lastRunMetrics = metrics;
|
|
1815
3519
|
runFinalized = true;
|
|
3520
|
+
await this.eventLogger.record({
|
|
3521
|
+
level: "info",
|
|
3522
|
+
event: "run.finished",
|
|
3523
|
+
sessionId: this.sessionId,
|
|
3524
|
+
runId,
|
|
3525
|
+
fields: { status: metrics.status, durationMs: metrics.totalDurationMs }
|
|
3526
|
+
});
|
|
1816
3527
|
yield { type: "run_finish", metrics };
|
|
1817
3528
|
} catch (error) {
|
|
1818
3529
|
const status = error instanceof AbortError || options?.signal?.aborted ? "aborted" : "failed";
|
|
1819
3530
|
const metrics = finalizeRun(status);
|
|
1820
3531
|
this.lastRunMetrics = metrics;
|
|
1821
3532
|
runFinalized = true;
|
|
3533
|
+
await this.eventLogger.record({
|
|
3534
|
+
level: "error",
|
|
3535
|
+
event: "run.finished",
|
|
3536
|
+
sessionId: this.sessionId,
|
|
3537
|
+
runId,
|
|
3538
|
+
fields: { status: metrics.status, durationMs: metrics.totalDurationMs }
|
|
3539
|
+
});
|
|
1822
3540
|
yield { type: "run_finish", metrics };
|
|
1823
3541
|
throw error;
|
|
1824
3542
|
} finally {
|
|
@@ -1835,8 +3553,8 @@ var AgentSession = class {
|
|
|
1835
3553
|
for (const plugin of this.plugins) {
|
|
1836
3554
|
try {
|
|
1837
3555
|
await plugin.teardown?.();
|
|
1838
|
-
} catch
|
|
1839
|
-
this.logger?.(
|
|
3556
|
+
} catch {
|
|
3557
|
+
this.logger?.("Plugin teardown failed");
|
|
1840
3558
|
}
|
|
1841
3559
|
}
|
|
1842
3560
|
this.plugins.length = 0;
|
|
@@ -1876,8 +3594,14 @@ var AgentSession = class {
|
|
|
1876
3594
|
content: assistantEvent.message.content.filter((block) => block.type === "tool_use").map((toolUse) => ({
|
|
1877
3595
|
type: "tool_result",
|
|
1878
3596
|
toolUseId: toolUse.id,
|
|
1879
|
-
content:
|
|
1880
|
-
|
|
3597
|
+
content: JSON.stringify({
|
|
3598
|
+
code: "OUTCOME_UNKNOWN",
|
|
3599
|
+
retryPolicy: "after_user_action",
|
|
3600
|
+
message: "Tool execution was interrupted; outcome is unknown"
|
|
3601
|
+
}),
|
|
3602
|
+
isError: true,
|
|
3603
|
+
errorCode: "OUTCOME_UNKNOWN",
|
|
3604
|
+
retryPolicy: "after_user_action"
|
|
1881
3605
|
})),
|
|
1882
3606
|
timestamp: Date.now()
|
|
1883
3607
|
};
|
|
@@ -1909,32 +3633,50 @@ var AgentSession = class {
|
|
|
1909
3633
|
AgentSession,
|
|
1910
3634
|
DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
1911
3635
|
FatalError,
|
|
3636
|
+
FileLogSink,
|
|
1912
3637
|
HookRegistry,
|
|
1913
3638
|
JSONLMessageStore,
|
|
1914
3639
|
KapibalaError,
|
|
1915
3640
|
ModelError,
|
|
1916
3641
|
OpenAICompatibleProvider,
|
|
1917
3642
|
PathSandbox,
|
|
3643
|
+
PermissionPolicy,
|
|
1918
3644
|
PromptAssembler,
|
|
3645
|
+
SessionApprovalCache,
|
|
1919
3646
|
SessionBusyError,
|
|
1920
3647
|
SimpleModelRouter,
|
|
3648
|
+
StructuredLogger,
|
|
1921
3649
|
ToolError,
|
|
1922
3650
|
ToolExecutor,
|
|
1923
3651
|
ToolNotFound,
|
|
1924
3652
|
ToolRegistry,
|
|
1925
3653
|
TransportError,
|
|
1926
3654
|
builtinTools,
|
|
3655
|
+
classifyBashOutput,
|
|
3656
|
+
cleanupCommandResults,
|
|
3657
|
+
commandAuditSummary,
|
|
3658
|
+
createDefaultLogSinks,
|
|
3659
|
+
createLogId,
|
|
3660
|
+
createRunCommandTool,
|
|
1927
3661
|
defineTool,
|
|
3662
|
+
detectShell,
|
|
1928
3663
|
editFileTool,
|
|
3664
|
+
executionFingerprint,
|
|
1929
3665
|
globTool,
|
|
1930
3666
|
grepTool,
|
|
1931
3667
|
isTextBlock,
|
|
1932
3668
|
isThinkingBlock,
|
|
1933
3669
|
isToolResultBlock,
|
|
1934
3670
|
isToolUseBlock,
|
|
3671
|
+
loadInstructions,
|
|
3672
|
+
projectSafeFields,
|
|
1935
3673
|
readFileTool,
|
|
3674
|
+
recoverIncompleteAudit,
|
|
1936
3675
|
resolveContextWindow,
|
|
3676
|
+
stableJsonStringify,
|
|
1937
3677
|
toToolDefinition,
|
|
3678
|
+
validatePermissionRules,
|
|
3679
|
+
visibleTools,
|
|
1938
3680
|
writeFileTool
|
|
1939
3681
|
});
|
|
1940
3682
|
//# sourceMappingURL=index.cjs.map
|