akemon 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/cli.js +89 -40
- package/dist/engine-peripheral.js +5 -4
- package/dist/engine-routing.js +99 -0
- package/dist/event-bus.js +63 -17
- package/dist/privacy-filter.js +269 -0
- package/dist/redaction.js +159 -0
- package/dist/relay-client.js +39 -2
- package/dist/server.js +86 -101
- package/dist/software-agent-memory.js +9 -11
- package/dist/software-agent-peripheral.js +313 -20
- package/dist/software-agent-stream-cli.js +101 -0
- package/package.json +1 -1
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { randomUUID } from "crypto";
|
|
14
14
|
import { spawn, spawnSync } from "child_process";
|
|
15
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
16
16
|
import { isAbsolute, join, relative, resolve as resolvePath } from "path";
|
|
17
17
|
import { StringDecoder } from "string_decoder";
|
|
18
18
|
import { SIG, sig } from "./types.js";
|
|
19
19
|
import { sendTaskEnd, sendTaskStart, sendTaskStream } from "./relay-client.js";
|
|
20
|
+
import { redactSecrets, StreamingRedactor } from "./redaction.js";
|
|
20
21
|
const defaultTaskRelay = {
|
|
21
22
|
sendTaskStart,
|
|
22
23
|
sendTaskStream,
|
|
@@ -34,7 +35,38 @@ const MEMORY_SCOPES = ["none", "public", "task", "owner"];
|
|
|
34
35
|
const RISK_LEVELS = ["low", "medium", "high"];
|
|
35
36
|
const MAX_STREAM_SUMMARY_CHARS = 12_000;
|
|
36
37
|
const STREAM_SUMMARY_HEAD_CHARS = 4_000;
|
|
37
|
-
const
|
|
38
|
+
const DEFAULT_TASK_LEDGER_MAX_RECORDS = 200;
|
|
39
|
+
const CONTEXT_PACKET_FILENAME = "TASK_CONTEXT.md";
|
|
40
|
+
const CONTEXT_SESSION_STATE_FILENAME = "SESSION.json";
|
|
41
|
+
const MAX_CONTEXT_SESSION_ID_LENGTH = 120;
|
|
42
|
+
const MAX_CONTEXT_SESSION_SUMMARY_CHARS = 4_000;
|
|
43
|
+
const DEFAULT_SOFTWARE_AGENT_ENV_POLICY = "inherit";
|
|
44
|
+
const DEFAULT_SOFTWARE_AGENT_ENV_ALLOWLIST = [
|
|
45
|
+
"PATH",
|
|
46
|
+
"HOME",
|
|
47
|
+
"SHELL",
|
|
48
|
+
"USER",
|
|
49
|
+
"LOGNAME",
|
|
50
|
+
"TMPDIR",
|
|
51
|
+
"TEMP",
|
|
52
|
+
"TMP",
|
|
53
|
+
"LANG",
|
|
54
|
+
"LC_ALL",
|
|
55
|
+
"LC_CTYPE",
|
|
56
|
+
"TERM",
|
|
57
|
+
"COLORTERM",
|
|
58
|
+
"NO_COLOR",
|
|
59
|
+
"FORCE_COLOR",
|
|
60
|
+
"OPENAI_API_KEY",
|
|
61
|
+
"OPENAI_BASE_URL",
|
|
62
|
+
"OPENAI_ORG_ID",
|
|
63
|
+
"OPENAI_ORGANIZATION",
|
|
64
|
+
"OPENAI_PROJECT",
|
|
65
|
+
"OPENAI_MODEL",
|
|
66
|
+
"CODEX_HOME",
|
|
67
|
+
"CODEX_MODEL",
|
|
68
|
+
"CODEX_PROFILE",
|
|
69
|
+
];
|
|
38
70
|
export class CodexSoftwareAgentPeripheral {
|
|
39
71
|
id;
|
|
40
72
|
name;
|
|
@@ -47,7 +79,11 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
47
79
|
activeWorkdir = null;
|
|
48
80
|
sessionId = randomUUID();
|
|
49
81
|
constructor(config) {
|
|
50
|
-
this.config =
|
|
82
|
+
this.config = {
|
|
83
|
+
...config,
|
|
84
|
+
envPolicy: normalizeSoftwareAgentEnvPolicy(config.envPolicy),
|
|
85
|
+
envAllowlist: normalizeSoftwareAgentEnvAllowlist(config.envAllowlist),
|
|
86
|
+
};
|
|
51
87
|
this.id = config.id || "software-agent:codex";
|
|
52
88
|
this.name = config.name || "Codex CLI Software Agent";
|
|
53
89
|
}
|
|
@@ -65,14 +101,16 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
65
101
|
this.sessionId = randomUUID();
|
|
66
102
|
}
|
|
67
103
|
async resetSession() {
|
|
68
|
-
|
|
104
|
+
const activePid = this.activeChild?.pid;
|
|
105
|
+
if (activePid) {
|
|
106
|
+
const processGroupId = -activePid;
|
|
69
107
|
try {
|
|
70
|
-
process.kill(
|
|
108
|
+
process.kill(processGroupId, "SIGTERM");
|
|
71
109
|
}
|
|
72
110
|
catch { }
|
|
73
111
|
setTimeout(() => {
|
|
74
112
|
try {
|
|
75
|
-
process.kill(
|
|
113
|
+
process.kill(processGroupId, "SIGKILL");
|
|
76
114
|
}
|
|
77
115
|
catch { }
|
|
78
116
|
}, 3000).unref();
|
|
@@ -94,6 +132,12 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
94
132
|
baseWorkdir: resolvePath(this.config.workdir),
|
|
95
133
|
workdirStatus: this.collectWorkdirStatus(currentWorkdir),
|
|
96
134
|
taskLedgerDir: this.config.taskLedgerDir,
|
|
135
|
+
contextSessionDir: this.config.contextSessionDir,
|
|
136
|
+
environment: buildSoftwareAgentChildEnvironment({
|
|
137
|
+
policy: this.config.envPolicy,
|
|
138
|
+
allowlist: this.config.envAllowlist,
|
|
139
|
+
sourceEnv: this.config.sourceEnv,
|
|
140
|
+
}).audit,
|
|
97
141
|
};
|
|
98
142
|
}
|
|
99
143
|
async send(signal) {
|
|
@@ -115,10 +159,18 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
115
159
|
}
|
|
116
160
|
const { signal, observer } = normalizeSoftwareAgentTaskOptions(taskOptions);
|
|
117
161
|
const taskId = envelope.taskId || `sw_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
162
|
+
const contextSessionId = normalizeContextSessionId(envelope.contextSessionId) || taskId;
|
|
118
163
|
const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
|
|
119
164
|
const workdir = workdirSafety.effectiveWorkdir;
|
|
120
|
-
|
|
121
|
-
const
|
|
165
|
+
let effectiveEnvelope = { ...envelope, taskId, contextSessionId, workdir, workdirSafety };
|
|
166
|
+
const contextSession = this.config.contextSessionDir
|
|
167
|
+
? writeSoftwareAgentContextPacket(this.config.contextSessionDir, effectiveEnvelope)
|
|
168
|
+
: undefined;
|
|
169
|
+
if (contextSession)
|
|
170
|
+
effectiveEnvelope = contextSession.envelope;
|
|
171
|
+
const prompt = contextSession
|
|
172
|
+
? buildContextPacketLaunchPrompt(effectiveEnvelope, contextSession.audit.packetPath)
|
|
173
|
+
: buildTaskEnvelopePrompt(effectiveEnvelope);
|
|
122
174
|
const { cmd, args } = buildCodexExecCommand({
|
|
123
175
|
command: this.config.command || "codex",
|
|
124
176
|
workdir,
|
|
@@ -132,6 +184,11 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
132
184
|
const spawnImpl = this.config.spawnImpl || spawn;
|
|
133
185
|
const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
|
|
134
186
|
const workdirStatus = this.collectWorkdirStatus(workdir);
|
|
187
|
+
const childEnvironment = buildSoftwareAgentChildEnvironment({
|
|
188
|
+
policy: this.config.envPolicy,
|
|
189
|
+
allowlist: this.config.envAllowlist,
|
|
190
|
+
sourceEnv: this.config.sourceEnv,
|
|
191
|
+
});
|
|
135
192
|
const baseTaskRecord = () => ({
|
|
136
193
|
schemaVersion: 1,
|
|
137
194
|
taskId,
|
|
@@ -141,6 +198,8 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
141
198
|
commandLine,
|
|
142
199
|
envelope: effectiveEnvelope,
|
|
143
200
|
startedAt: startedAtIso,
|
|
201
|
+
environment: childEnvironment.audit,
|
|
202
|
+
contextSession: contextSession?.audit,
|
|
144
203
|
workdirStatus,
|
|
145
204
|
});
|
|
146
205
|
return new Promise((resolve) => {
|
|
@@ -165,7 +224,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
165
224
|
try {
|
|
166
225
|
child = spawnImpl(cmd, args, {
|
|
167
226
|
cwd: workdir,
|
|
168
|
-
env:
|
|
227
|
+
env: childEnvironment.env,
|
|
169
228
|
stdio: ["pipe", "pipe", "pipe"],
|
|
170
229
|
detached: true,
|
|
171
230
|
});
|
|
@@ -184,7 +243,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
184
243
|
durationMs,
|
|
185
244
|
};
|
|
186
245
|
relay.sendTaskEnd(taskId, null, durationMs);
|
|
187
|
-
observer?.onEnd?.({ taskId, exitCode: null, durationMs, result });
|
|
246
|
+
observer?.onEnd?.({ taskId, exitCode: null, durationMs, result: redactSecrets(result) });
|
|
188
247
|
const completedAt = new Date().toISOString();
|
|
189
248
|
this.writeTaskRecord({
|
|
190
249
|
...baseTaskRecord(),
|
|
@@ -196,6 +255,9 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
196
255
|
stdoutSummary: summarizeText(""),
|
|
197
256
|
stderrSummary: summarizeText(result.error || ""),
|
|
198
257
|
});
|
|
258
|
+
if (contextSession) {
|
|
259
|
+
writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
|
|
260
|
+
}
|
|
199
261
|
this.bus?.emit(SIG.TASK_FAILED, sig(SIG.TASK_FAILED, result, this.id));
|
|
200
262
|
resolve(result);
|
|
201
263
|
return;
|
|
@@ -207,6 +269,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
207
269
|
let aborted = false;
|
|
208
270
|
const outDecoder = new StringDecoder("utf8");
|
|
209
271
|
const errDecoder = new StringDecoder("utf8");
|
|
272
|
+
const outRedactor = new StreamingRedactor();
|
|
273
|
+
const errRedactor = new StreamingRedactor();
|
|
274
|
+
const emitSafeStream = (stream, text) => {
|
|
275
|
+
if (!text)
|
|
276
|
+
return;
|
|
277
|
+
relay.sendTaskStream(taskId, stream, text);
|
|
278
|
+
observer?.onStream?.({ taskId, stream, chunk: text });
|
|
279
|
+
};
|
|
210
280
|
const finish = (exitCode, error) => {
|
|
211
281
|
if (finished)
|
|
212
282
|
return;
|
|
@@ -217,14 +287,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
217
287
|
const tailErr = errDecoder.end();
|
|
218
288
|
if (tailOut) {
|
|
219
289
|
stdout += tailOut;
|
|
220
|
-
|
|
221
|
-
observer?.onStream?.({ taskId, stream: "stdout", chunk: tailOut });
|
|
290
|
+
emitSafeStream("stdout", outRedactor.push(tailOut));
|
|
222
291
|
}
|
|
223
292
|
if (tailErr) {
|
|
224
293
|
stderr += tailErr;
|
|
225
|
-
|
|
226
|
-
observer?.onStream?.({ taskId, stream: "stderr", chunk: tailErr });
|
|
294
|
+
emitSafeStream("stderr", errRedactor.push(tailErr));
|
|
227
295
|
}
|
|
296
|
+
emitSafeStream("stdout", outRedactor.flush());
|
|
297
|
+
emitSafeStream("stderr", errRedactor.flush());
|
|
228
298
|
const durationMs = Date.now() - startedAt;
|
|
229
299
|
this.activeChild = null;
|
|
230
300
|
this.activeTaskId = null;
|
|
@@ -240,7 +310,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
240
310
|
durationMs,
|
|
241
311
|
};
|
|
242
312
|
relay.sendTaskEnd(taskId, exitCode, durationMs);
|
|
243
|
-
observer?.onEnd?.({ taskId, exitCode, durationMs, result });
|
|
313
|
+
observer?.onEnd?.({ taskId, exitCode, durationMs, result: redactSecrets(result) });
|
|
244
314
|
const completedAt = new Date().toISOString();
|
|
245
315
|
this.writeTaskRecord({
|
|
246
316
|
...baseTaskRecord(),
|
|
@@ -252,6 +322,9 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
252
322
|
stdoutSummary: summarizeText(stdout),
|
|
253
323
|
stderrSummary: summarizeText(stderr),
|
|
254
324
|
});
|
|
325
|
+
if (contextSession) {
|
|
326
|
+
writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
|
|
327
|
+
}
|
|
255
328
|
this.bus?.emit(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
|
|
256
329
|
...result,
|
|
257
330
|
taskLabel: `software_agent:${this.id}`,
|
|
@@ -302,16 +375,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
302
375
|
if (!text)
|
|
303
376
|
return;
|
|
304
377
|
stdout += text;
|
|
305
|
-
|
|
306
|
-
observer?.onStream?.({ taskId, stream: "stdout", chunk: text });
|
|
378
|
+
emitSafeStream("stdout", outRedactor.push(text));
|
|
307
379
|
});
|
|
308
380
|
child.stderr?.on("data", (chunk) => {
|
|
309
381
|
const text = errDecoder.write(chunk);
|
|
310
382
|
if (!text)
|
|
311
383
|
return;
|
|
312
384
|
stderr += text;
|
|
313
|
-
|
|
314
|
-
observer?.onStream?.({ taskId, stream: "stderr", chunk: text });
|
|
385
|
+
emitSafeStream("stderr", errRedactor.push(text));
|
|
315
386
|
});
|
|
316
387
|
child.on("close", (code) => {
|
|
317
388
|
child.unref();
|
|
@@ -330,7 +401,8 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
330
401
|
try {
|
|
331
402
|
mkdirSync(dir, { recursive: true });
|
|
332
403
|
const safeTaskId = safeTaskFilename(record.taskId);
|
|
333
|
-
writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(record, null, 2)}\n`);
|
|
404
|
+
writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(redactSecrets(record), null, 2)}\n`);
|
|
405
|
+
pruneSoftwareAgentTaskRecords(dir, this.config.taskLedgerMaxRecords, record.taskId);
|
|
334
406
|
}
|
|
335
407
|
catch (err) {
|
|
336
408
|
console.error(`[software-agent] Failed to write task ledger: ${err.message || String(err)}`);
|
|
@@ -371,6 +443,7 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
371
443
|
"[Akemon Software Peripheral Task Envelope]",
|
|
372
444
|
"",
|
|
373
445
|
`Task ID: ${envelope.taskId || "(unspecified)"}`,
|
|
446
|
+
`Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
|
|
374
447
|
`Source module: ${envelope.sourceModule}`,
|
|
375
448
|
`Purpose: ${envelope.purpose}`,
|
|
376
449
|
`Role scope: ${envelope.roleScope}`,
|
|
@@ -378,6 +451,9 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
378
451
|
`Risk level: ${envelope.riskLevel}`,
|
|
379
452
|
`Workdir: ${envelope.workdir}`,
|
|
380
453
|
];
|
|
454
|
+
if (envelope.contextPacketPath) {
|
|
455
|
+
lines.push(`Context packet path: ${envelope.contextPacketPath}`);
|
|
456
|
+
}
|
|
381
457
|
if (envelope.workdirSafety) {
|
|
382
458
|
lines.push(`Base workdir: ${envelope.workdirSafety.baseWorkdir}`);
|
|
383
459
|
lines.push(`Requested workdir: ${envelope.workdirSafety.requestedWorkdir}`);
|
|
@@ -386,6 +462,11 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
386
462
|
lines.push(`Outside workdir explicitly allowed: ${envelope.workdirSafety.allowOutsideWorkdir ? "yes" : "no"}`);
|
|
387
463
|
}
|
|
388
464
|
lines.push("", "Goal:", envelope.goal, "");
|
|
465
|
+
if (envelope.previousTaskSummary?.trim()) {
|
|
466
|
+
lines.push("Previous task summary for this Akemon context session:");
|
|
467
|
+
lines.push(envelope.previousTaskSummary.trim());
|
|
468
|
+
lines.push("");
|
|
469
|
+
}
|
|
389
470
|
if (envelope.memorySummary?.trim()) {
|
|
390
471
|
lines.push("Visible Akemon memory/context:");
|
|
391
472
|
lines.push(envelope.memorySummary.trim());
|
|
@@ -415,6 +496,26 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
415
496
|
lines.push("- Report what changed, what you verified, and any remaining risk.");
|
|
416
497
|
return lines.join("\n");
|
|
417
498
|
}
|
|
499
|
+
export function buildContextPacketLaunchPrompt(envelope, contextPacketPath) {
|
|
500
|
+
return [
|
|
501
|
+
"[Akemon Software Peripheral Task]",
|
|
502
|
+
"",
|
|
503
|
+
`Task ID: ${envelope.taskId || "(unspecified)"}`,
|
|
504
|
+
`Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
|
|
505
|
+
`Context packet: ${contextPacketPath}`,
|
|
506
|
+
`Workdir: ${envelope.workdir}`,
|
|
507
|
+
"",
|
|
508
|
+
"Goal:",
|
|
509
|
+
envelope.goal,
|
|
510
|
+
"",
|
|
511
|
+
"Instructions:",
|
|
512
|
+
"- Read the context packet first before doing repository work.",
|
|
513
|
+
"- Treat that file as the complete Akemon-provided context for this task.",
|
|
514
|
+
"- Do not read Akemon private memory outside the context packet.",
|
|
515
|
+
"- Work only in the stated workdir unless the packet explicitly allows otherwise.",
|
|
516
|
+
"- Report what changed, what you verified, and any remaining risk.",
|
|
517
|
+
].join("\n");
|
|
518
|
+
}
|
|
418
519
|
export function createOwnerTaskEnvelope(body, defaultWorkdir) {
|
|
419
520
|
const goal = typeof body?.goal === "string" ? body.goal.trim() : "";
|
|
420
521
|
if (!goal)
|
|
@@ -437,6 +538,7 @@ export function createOwnerTaskEnvelope(body, defaultWorkdir) {
|
|
|
437
538
|
: [...DEFAULT_OWNER_ALLOWED_ACTIONS],
|
|
438
539
|
forbiddenActions: [...new Set([...DEFAULT_OWNER_FORBIDDEN_ACTIONS, ...callerForbiddenActions])],
|
|
439
540
|
memorySummary: typeof body?.memorySummary === "string" ? body.memorySummary : "",
|
|
541
|
+
contextSessionId: readOptionalContextSessionId(body?.contextSessionId ?? body?.sessionId, "contextSessionId"),
|
|
440
542
|
deliverable: typeof body?.deliverable === "string"
|
|
441
543
|
? body.deliverable
|
|
442
544
|
: "Return a concise engineering summary with changes, verification, and remaining risks.",
|
|
@@ -536,6 +638,139 @@ export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
|
|
|
536
638
|
const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
|
|
537
639
|
return readSoftwareAgentTaskRecordFile(file);
|
|
538
640
|
}
|
|
641
|
+
export function pruneSoftwareAgentTaskRecords(taskLedgerDir, maxRecords = DEFAULT_TASK_LEDGER_MAX_RECORDS, preserveTaskId) {
|
|
642
|
+
const safeMaxRecords = normalizeTaskLedgerMaxRecords(maxRecords);
|
|
643
|
+
try {
|
|
644
|
+
if (!existsSync(taskLedgerDir))
|
|
645
|
+
return 0;
|
|
646
|
+
const records = readdirSync(taskLedgerDir, { withFileTypes: true })
|
|
647
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
648
|
+
.map((entry) => {
|
|
649
|
+
const file = join(taskLedgerDir, entry.name);
|
|
650
|
+
const record = readSoftwareAgentTaskRecordFile(file);
|
|
651
|
+
return record ? { file, record } : null;
|
|
652
|
+
})
|
|
653
|
+
.filter((entry) => !!entry)
|
|
654
|
+
.sort((a, b) => compareSoftwareAgentTaskRecords(a.record, b.record));
|
|
655
|
+
const keepTaskIds = new Set(records.slice(0, safeMaxRecords).map((entry) => entry.record.taskId));
|
|
656
|
+
if (preserveTaskId)
|
|
657
|
+
keepTaskIds.add(preserveTaskId);
|
|
658
|
+
let deleted = 0;
|
|
659
|
+
for (const entry of records) {
|
|
660
|
+
if (keepTaskIds.has(entry.record.taskId))
|
|
661
|
+
continue;
|
|
662
|
+
try {
|
|
663
|
+
unlinkSync(entry.file);
|
|
664
|
+
deleted++;
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
// Best effort: retention should not break task completion.
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return deleted;
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
export function buildSoftwareAgentChildEnvironment(opts = {}) {
|
|
677
|
+
const policy = normalizeSoftwareAgentEnvPolicy(opts.policy);
|
|
678
|
+
const sourceEnv = opts.sourceEnv || process.env;
|
|
679
|
+
if (policy === "inherit") {
|
|
680
|
+
return {
|
|
681
|
+
env: sourceEnv,
|
|
682
|
+
audit: { policy },
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const allowlist = normalizeSoftwareAgentEnvAllowlist([
|
|
686
|
+
...DEFAULT_SOFTWARE_AGENT_ENV_ALLOWLIST,
|
|
687
|
+
...(opts.allowlist || []),
|
|
688
|
+
]);
|
|
689
|
+
const env = {};
|
|
690
|
+
for (const key of allowlist) {
|
|
691
|
+
if (isForbiddenSoftwareAgentEnvKey(key))
|
|
692
|
+
continue;
|
|
693
|
+
const value = sourceEnv[key];
|
|
694
|
+
if (value !== undefined)
|
|
695
|
+
env[key] = value;
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
env,
|
|
699
|
+
audit: {
|
|
700
|
+
policy,
|
|
701
|
+
allowedKeys: Object.keys(env).sort(),
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function writeSoftwareAgentContextPacket(contextSessionDir, envelope) {
|
|
706
|
+
const sessionId = normalizeContextSessionId(envelope.contextSessionId) || envelope.taskId || randomUUID();
|
|
707
|
+
const sessionDir = join(contextSessionDir, sessionId);
|
|
708
|
+
const packetPath = join(sessionDir, CONTEXT_PACKET_FILENAME);
|
|
709
|
+
const statePath = join(sessionDir, CONTEXT_SESSION_STATE_FILENAME);
|
|
710
|
+
const previousTaskSummary = readSoftwareAgentContextSessionSummary(statePath);
|
|
711
|
+
const packetEnvelope = {
|
|
712
|
+
...envelope,
|
|
713
|
+
contextSessionId: sessionId,
|
|
714
|
+
contextPacketPath: packetPath,
|
|
715
|
+
previousTaskSummary,
|
|
716
|
+
};
|
|
717
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
718
|
+
const content = buildTaskEnvelopePrompt(packetEnvelope);
|
|
719
|
+
writeFileSync(packetPath, `${redactSecrets(content)}\n`);
|
|
720
|
+
return {
|
|
721
|
+
envelope: packetEnvelope,
|
|
722
|
+
audit: { sessionId, packetPath, statePath },
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function writeSoftwareAgentContextSessionState(statePath, envelope, result, updatedAt) {
|
|
726
|
+
try {
|
|
727
|
+
const state = {
|
|
728
|
+
schemaVersion: 1,
|
|
729
|
+
sessionId: envelope.contextSessionId,
|
|
730
|
+
updatedAt,
|
|
731
|
+
lastTaskId: result.taskId,
|
|
732
|
+
lastGoal: envelope.goal,
|
|
733
|
+
lastResult: {
|
|
734
|
+
success: result.success,
|
|
735
|
+
exitCode: result.exitCode,
|
|
736
|
+
durationMs: result.durationMs,
|
|
737
|
+
outputSummary: summarizeText(result.output || "", MAX_CONTEXT_SESSION_SUMMARY_CHARS),
|
|
738
|
+
errorSummary: result.error ? summarizeText(result.error, MAX_CONTEXT_SESSION_SUMMARY_CHARS) : undefined,
|
|
739
|
+
},
|
|
740
|
+
};
|
|
741
|
+
writeFileSync(statePath, `${JSON.stringify(redactSecrets(state), null, 2)}\n`);
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
console.error(`[software-agent] Failed to write context session state: ${err.message || String(err)}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function readSoftwareAgentContextSessionSummary(statePath) {
|
|
748
|
+
try {
|
|
749
|
+
if (!existsSync(statePath))
|
|
750
|
+
return undefined;
|
|
751
|
+
const parsed = JSON.parse(readFileSync(statePath, "utf8"));
|
|
752
|
+
if (!parsed || parsed.schemaVersion !== 1 || !parsed.lastTaskId || !parsed.lastResult)
|
|
753
|
+
return undefined;
|
|
754
|
+
const result = parsed.lastResult;
|
|
755
|
+
const status = result.success === true ? "completed" : "failed";
|
|
756
|
+
const lines = [
|
|
757
|
+
`Previous task: ${parsed.lastTaskId}`,
|
|
758
|
+
parsed.lastGoal ? `Previous goal: ${parsed.lastGoal}` : "",
|
|
759
|
+
`Status: ${status}`,
|
|
760
|
+
Number.isInteger(result.exitCode) ? `Exit code: ${result.exitCode}` : "Exit code: null",
|
|
761
|
+
Number.isInteger(result.durationMs) ? `Duration: ${result.durationMs}ms` : "",
|
|
762
|
+
result.outputSummary?.text ? "Previous output summary:" : "",
|
|
763
|
+
result.outputSummary?.text || "",
|
|
764
|
+
result.errorSummary?.text ? "Previous error summary:" : "",
|
|
765
|
+
result.errorSummary?.text || "",
|
|
766
|
+
].filter(Boolean);
|
|
767
|
+
const summary = lines.join("\n").trim();
|
|
768
|
+
return summary ? summarizeText(summary, MAX_CONTEXT_SESSION_SUMMARY_CHARS).text : undefined;
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
539
774
|
function readOptionalString(value, field) {
|
|
540
775
|
if (value === undefined || value === null)
|
|
541
776
|
return undefined;
|
|
@@ -544,6 +779,27 @@ function readOptionalString(value, field) {
|
|
|
544
779
|
const trimmed = value.trim();
|
|
545
780
|
return trimmed || undefined;
|
|
546
781
|
}
|
|
782
|
+
function readOptionalContextSessionId(value, field) {
|
|
783
|
+
if (value === undefined || value === null || value === "")
|
|
784
|
+
return undefined;
|
|
785
|
+
if (typeof value !== "string")
|
|
786
|
+
throw new Error(`Invalid ${field}: expected string`);
|
|
787
|
+
return normalizeContextSessionId(value, field);
|
|
788
|
+
}
|
|
789
|
+
function normalizeContextSessionId(value, field = "contextSessionId") {
|
|
790
|
+
if (!value)
|
|
791
|
+
return undefined;
|
|
792
|
+
const trimmed = value.trim();
|
|
793
|
+
if (!trimmed)
|
|
794
|
+
return undefined;
|
|
795
|
+
if (trimmed.length > MAX_CONTEXT_SESSION_ID_LENGTH) {
|
|
796
|
+
throw new Error(`Invalid ${field}: expected at most ${MAX_CONTEXT_SESSION_ID_LENGTH} characters`);
|
|
797
|
+
}
|
|
798
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(trimmed)) {
|
|
799
|
+
throw new Error(`Invalid ${field}: expected letters, numbers, dot, underscore, or hyphen`);
|
|
800
|
+
}
|
|
801
|
+
return trimmed;
|
|
802
|
+
}
|
|
547
803
|
function normalizeSoftwareAgentTaskOptions(options) {
|
|
548
804
|
if (!options)
|
|
549
805
|
return {};
|
|
@@ -552,6 +808,38 @@ function normalizeSoftwareAgentTaskOptions(options) {
|
|
|
552
808
|
}
|
|
553
809
|
return options;
|
|
554
810
|
}
|
|
811
|
+
function normalizeSoftwareAgentEnvPolicy(value) {
|
|
812
|
+
if (value === undefined || value === null || value === "")
|
|
813
|
+
return DEFAULT_SOFTWARE_AGENT_ENV_POLICY;
|
|
814
|
+
if (value === "inherit" || value === "allowlist")
|
|
815
|
+
return value;
|
|
816
|
+
throw new Error("Invalid software-agent env policy: expected inherit or allowlist");
|
|
817
|
+
}
|
|
818
|
+
function normalizeSoftwareAgentEnvAllowlist(values) {
|
|
819
|
+
if (!values)
|
|
820
|
+
return [];
|
|
821
|
+
const seen = new Set();
|
|
822
|
+
for (const value of values) {
|
|
823
|
+
if (typeof value !== "string") {
|
|
824
|
+
throw new Error("Invalid software-agent env allowlist entry: expected string");
|
|
825
|
+
}
|
|
826
|
+
const key = value.trim();
|
|
827
|
+
if (!key)
|
|
828
|
+
continue;
|
|
829
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
830
|
+
throw new Error(`Invalid software-agent env allowlist entry: ${key}`);
|
|
831
|
+
}
|
|
832
|
+
seen.add(key);
|
|
833
|
+
}
|
|
834
|
+
return [...seen];
|
|
835
|
+
}
|
|
836
|
+
function isForbiddenSoftwareAgentEnvKey(key) {
|
|
837
|
+
const upper = key.toUpperCase();
|
|
838
|
+
if (upper.startsWith("AKEMON_"))
|
|
839
|
+
return true;
|
|
840
|
+
const looksLikeCredential = /(?:SECRET|TOKEN|ACCESS|KEY|CREDENTIAL)/.test(upper);
|
|
841
|
+
return looksLikeCredential && (upper.includes("RELAY") || upper.includes("OWNER"));
|
|
842
|
+
}
|
|
555
843
|
function isAbortSignal(value) {
|
|
556
844
|
return !!value
|
|
557
845
|
&& typeof value.aborted === "boolean"
|
|
@@ -610,6 +898,11 @@ function normalizeTaskRecordLimit(limit) {
|
|
|
610
898
|
return 20;
|
|
611
899
|
return Math.min(limit, 100);
|
|
612
900
|
}
|
|
901
|
+
function normalizeTaskLedgerMaxRecords(limit) {
|
|
902
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
903
|
+
return DEFAULT_TASK_LEDGER_MAX_RECORDS;
|
|
904
|
+
return Math.min(limit, 10_000);
|
|
905
|
+
}
|
|
613
906
|
function readSoftwareAgentTaskRecordFile(file) {
|
|
614
907
|
try {
|
|
615
908
|
if (!existsSync(file))
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export class SoftwareAgentStreamCliRenderer {
|
|
2
|
+
writers;
|
|
3
|
+
taskId;
|
|
4
|
+
stderrEndsWithNewline = true;
|
|
5
|
+
constructor(writers = {
|
|
6
|
+
stdout: (chunk) => process.stdout.write(chunk),
|
|
7
|
+
stderr: (chunk) => process.stderr.write(chunk),
|
|
8
|
+
}) {
|
|
9
|
+
this.writers = writers;
|
|
10
|
+
}
|
|
11
|
+
handleLine(line) {
|
|
12
|
+
const trimmed = line.trim();
|
|
13
|
+
if (!trimmed)
|
|
14
|
+
return false;
|
|
15
|
+
let event;
|
|
16
|
+
try {
|
|
17
|
+
event = JSON.parse(trimmed);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
this.stderrLine(`[software-agent] non-json: ${trimmed}`);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return this.handleEvent(event);
|
|
24
|
+
}
|
|
25
|
+
handleEvent(event) {
|
|
26
|
+
const type = typeof event?.type === "string" ? event.type : "";
|
|
27
|
+
if (type === "start") {
|
|
28
|
+
const taskId = readString(event.taskId) || "unknown";
|
|
29
|
+
this.taskId = taskId;
|
|
30
|
+
this.stderrLine(`[software-agent] task ${taskId} started`);
|
|
31
|
+
const commandLine = readString(event.commandLine);
|
|
32
|
+
if (commandLine)
|
|
33
|
+
this.stderrLine(`[software-agent] command: ${truncateOneLine(commandLine, 160)}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (type === "stdout" && typeof event.chunk === "string") {
|
|
37
|
+
this.writers.stdout(event.chunk);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (type === "stderr" && typeof event.chunk === "string") {
|
|
41
|
+
this.writers.stderr(event.chunk);
|
|
42
|
+
this.stderrEndsWithNewline = event.chunk.endsWith("\n") || event.chunk.endsWith("\r");
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (type === "end") {
|
|
46
|
+
return this.handleEnd(event);
|
|
47
|
+
}
|
|
48
|
+
if (type === "error") {
|
|
49
|
+
this.stderrLine(`[software-agent] stream error: ${readString(event.error) || "unknown error"}`);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
this.stderrLine(`[software-agent] ignored stream event: ${type || "unknown"}`);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
handleEnd(event) {
|
|
56
|
+
const result = isObject(event.result) ? event.result : {};
|
|
57
|
+
const taskId = readString(event.taskId) || readString(result.taskId) || this.taskId || "unknown";
|
|
58
|
+
const success = result.success === false ? false : true;
|
|
59
|
+
const exitCode = readExitCode(event.exitCode) ?? readExitCode(result.exitCode);
|
|
60
|
+
const durationMs = readDurationMs(event.durationMs) ?? readDurationMs(result.durationMs);
|
|
61
|
+
const parts = [`[software-agent] task ${taskId} ${success ? "finished" : "failed"}`];
|
|
62
|
+
if (exitCode !== undefined)
|
|
63
|
+
parts.push(`exit=${exitCode}`);
|
|
64
|
+
if (durationMs !== undefined)
|
|
65
|
+
parts.push(`duration=${durationMs}ms`);
|
|
66
|
+
this.stderrLine(parts.join(" "));
|
|
67
|
+
const error = readString(result.error);
|
|
68
|
+
if (!success && error) {
|
|
69
|
+
this.stderrLine(`[software-agent] error: ${truncateOneLine(error, 240)}`);
|
|
70
|
+
}
|
|
71
|
+
const output = readString(result.output);
|
|
72
|
+
if (output) {
|
|
73
|
+
this.stderrLine(`[software-agent] summary: ${truncateOneLine(output, 240)}`);
|
|
74
|
+
}
|
|
75
|
+
return !success;
|
|
76
|
+
}
|
|
77
|
+
stderrLine(line) {
|
|
78
|
+
if (!this.stderrEndsWithNewline)
|
|
79
|
+
this.writers.stderr("\n");
|
|
80
|
+
this.writers.stderr(`${line}\n`);
|
|
81
|
+
this.stderrEndsWithNewline = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isObject(value) {
|
|
85
|
+
return typeof value === "object" && value !== null;
|
|
86
|
+
}
|
|
87
|
+
function readString(value) {
|
|
88
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
function readExitCode(value) {
|
|
91
|
+
return typeof value === "number" && Number.isInteger(value) ? value : undefined;
|
|
92
|
+
}
|
|
93
|
+
function readDurationMs(value) {
|
|
94
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
95
|
+
}
|
|
96
|
+
function truncateOneLine(value, max) {
|
|
97
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
98
|
+
if (oneLine.length <= max)
|
|
99
|
+
return oneLine;
|
|
100
|
+
return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
|
|
101
|
+
}
|