akemon 0.3.4 → 0.3.6
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/DATA_POLICY.md +120 -0
- package/README.md +43 -0
- package/TRADEMARK.md +74 -0
- package/dist/cli.js +311 -71
- 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 +181 -103
- package/dist/software-agent-memory.js +9 -11
- package/dist/software-agent-peripheral.js +453 -22
- package/dist/software-agent-result-cli.js +69 -0
- package/dist/software-agent-stream-cli.js +124 -0
- package/dist/work-memory.js +295 -0
- package/package.json +5 -3
|
@@ -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,13 @@ 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
|
+
workMemoryDir: this.config.workMemoryDir,
|
|
137
|
+
environment: buildSoftwareAgentChildEnvironment({
|
|
138
|
+
policy: this.config.envPolicy,
|
|
139
|
+
allowlist: this.config.envAllowlist,
|
|
140
|
+
sourceEnv: this.config.sourceEnv,
|
|
141
|
+
}).audit,
|
|
97
142
|
};
|
|
98
143
|
}
|
|
99
144
|
async send(signal) {
|
|
@@ -115,10 +160,25 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
115
160
|
}
|
|
116
161
|
const { signal, observer } = normalizeSoftwareAgentTaskOptions(taskOptions);
|
|
117
162
|
const taskId = envelope.taskId || `sw_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
163
|
+
const contextSessionId = normalizeContextSessionId(envelope.contextSessionId) || taskId;
|
|
118
164
|
const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
|
|
119
165
|
const workdir = workdirSafety.effectiveWorkdir;
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
let effectiveEnvelope = {
|
|
167
|
+
...envelope,
|
|
168
|
+
taskId,
|
|
169
|
+
contextSessionId,
|
|
170
|
+
workdir,
|
|
171
|
+
workdirSafety,
|
|
172
|
+
workMemoryDir: envelope.workMemoryDir || this.config.workMemoryDir,
|
|
173
|
+
};
|
|
174
|
+
const contextSession = this.config.contextSessionDir
|
|
175
|
+
? writeSoftwareAgentContextPacket(this.config.contextSessionDir, effectiveEnvelope)
|
|
176
|
+
: undefined;
|
|
177
|
+
if (contextSession)
|
|
178
|
+
effectiveEnvelope = contextSession.envelope;
|
|
179
|
+
const prompt = contextSession
|
|
180
|
+
? buildContextPacketLaunchPrompt(effectiveEnvelope, contextSession.audit.packetPath)
|
|
181
|
+
: buildTaskEnvelopePrompt(effectiveEnvelope);
|
|
122
182
|
const { cmd, args } = buildCodexExecCommand({
|
|
123
183
|
command: this.config.command || "codex",
|
|
124
184
|
workdir,
|
|
@@ -132,6 +192,16 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
132
192
|
const spawnImpl = this.config.spawnImpl || spawn;
|
|
133
193
|
const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
|
|
134
194
|
const workdirStatus = this.collectWorkdirStatus(workdir);
|
|
195
|
+
const taskMetadata = {
|
|
196
|
+
contextSessionId: effectiveEnvelope.contextSessionId,
|
|
197
|
+
contextPacketPath: effectiveEnvelope.contextPacketPath,
|
|
198
|
+
workMemoryDir: effectiveEnvelope.workMemoryDir,
|
|
199
|
+
};
|
|
200
|
+
const childEnvironment = buildSoftwareAgentChildEnvironment({
|
|
201
|
+
policy: this.config.envPolicy,
|
|
202
|
+
allowlist: this.config.envAllowlist,
|
|
203
|
+
sourceEnv: this.config.sourceEnv,
|
|
204
|
+
});
|
|
135
205
|
const baseTaskRecord = () => ({
|
|
136
206
|
schemaVersion: 1,
|
|
137
207
|
taskId,
|
|
@@ -141,6 +211,8 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
141
211
|
commandLine,
|
|
142
212
|
envelope: effectiveEnvelope,
|
|
143
213
|
startedAt: startedAtIso,
|
|
214
|
+
environment: childEnvironment.audit,
|
|
215
|
+
contextSession: contextSession?.audit,
|
|
144
216
|
workdirStatus,
|
|
145
217
|
});
|
|
146
218
|
return new Promise((resolve) => {
|
|
@@ -153,7 +225,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
153
225
|
});
|
|
154
226
|
const origin = "software_agent";
|
|
155
227
|
relay.sendTaskStart(taskId, origin, commandLine);
|
|
156
|
-
observer?.onStart?.({ taskId, origin, commandLine });
|
|
228
|
+
observer?.onStart?.({ taskId, origin, commandLine, ...taskMetadata });
|
|
157
229
|
this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
|
|
158
230
|
taskId,
|
|
159
231
|
taskType: "software_agent",
|
|
@@ -165,7 +237,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
165
237
|
try {
|
|
166
238
|
child = spawnImpl(cmd, args, {
|
|
167
239
|
cwd: workdir,
|
|
168
|
-
env:
|
|
240
|
+
env: childEnvironment.env,
|
|
169
241
|
stdio: ["pipe", "pipe", "pipe"],
|
|
170
242
|
detached: true,
|
|
171
243
|
});
|
|
@@ -182,9 +254,16 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
182
254
|
error: err.message || String(err),
|
|
183
255
|
exitCode: null,
|
|
184
256
|
durationMs,
|
|
257
|
+
...taskMetadata,
|
|
185
258
|
};
|
|
186
259
|
relay.sendTaskEnd(taskId, null, durationMs);
|
|
187
|
-
observer?.onEnd?.({
|
|
260
|
+
observer?.onEnd?.({
|
|
261
|
+
taskId,
|
|
262
|
+
exitCode: null,
|
|
263
|
+
durationMs,
|
|
264
|
+
result: redactSecrets(result),
|
|
265
|
+
...taskMetadata,
|
|
266
|
+
});
|
|
188
267
|
const completedAt = new Date().toISOString();
|
|
189
268
|
this.writeTaskRecord({
|
|
190
269
|
...baseTaskRecord(),
|
|
@@ -196,6 +275,9 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
196
275
|
stdoutSummary: summarizeText(""),
|
|
197
276
|
stderrSummary: summarizeText(result.error || ""),
|
|
198
277
|
});
|
|
278
|
+
if (contextSession) {
|
|
279
|
+
writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
|
|
280
|
+
}
|
|
199
281
|
this.bus?.emit(SIG.TASK_FAILED, sig(SIG.TASK_FAILED, result, this.id));
|
|
200
282
|
resolve(result);
|
|
201
283
|
return;
|
|
@@ -207,6 +289,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
207
289
|
let aborted = false;
|
|
208
290
|
const outDecoder = new StringDecoder("utf8");
|
|
209
291
|
const errDecoder = new StringDecoder("utf8");
|
|
292
|
+
const outRedactor = new StreamingRedactor();
|
|
293
|
+
const errRedactor = new StreamingRedactor();
|
|
294
|
+
const emitSafeStream = (stream, text) => {
|
|
295
|
+
if (!text)
|
|
296
|
+
return;
|
|
297
|
+
relay.sendTaskStream(taskId, stream, text);
|
|
298
|
+
observer?.onStream?.({ taskId, stream, chunk: text });
|
|
299
|
+
};
|
|
210
300
|
const finish = (exitCode, error) => {
|
|
211
301
|
if (finished)
|
|
212
302
|
return;
|
|
@@ -217,14 +307,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
217
307
|
const tailErr = errDecoder.end();
|
|
218
308
|
if (tailOut) {
|
|
219
309
|
stdout += tailOut;
|
|
220
|
-
|
|
221
|
-
observer?.onStream?.({ taskId, stream: "stdout", chunk: tailOut });
|
|
310
|
+
emitSafeStream("stdout", outRedactor.push(tailOut));
|
|
222
311
|
}
|
|
223
312
|
if (tailErr) {
|
|
224
313
|
stderr += tailErr;
|
|
225
|
-
|
|
226
|
-
observer?.onStream?.({ taskId, stream: "stderr", chunk: tailErr });
|
|
314
|
+
emitSafeStream("stderr", errRedactor.push(tailErr));
|
|
227
315
|
}
|
|
316
|
+
emitSafeStream("stdout", outRedactor.flush());
|
|
317
|
+
emitSafeStream("stderr", errRedactor.flush());
|
|
228
318
|
const durationMs = Date.now() - startedAt;
|
|
229
319
|
this.activeChild = null;
|
|
230
320
|
this.activeTaskId = null;
|
|
@@ -238,9 +328,16 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
238
328
|
error: success ? undefined : error || stderr.trim() || `codex exited with code ${exitCode}`,
|
|
239
329
|
exitCode,
|
|
240
330
|
durationMs,
|
|
331
|
+
...taskMetadata,
|
|
241
332
|
};
|
|
242
333
|
relay.sendTaskEnd(taskId, exitCode, durationMs);
|
|
243
|
-
observer?.onEnd?.({
|
|
334
|
+
observer?.onEnd?.({
|
|
335
|
+
taskId,
|
|
336
|
+
exitCode,
|
|
337
|
+
durationMs,
|
|
338
|
+
result: redactSecrets(result),
|
|
339
|
+
...taskMetadata,
|
|
340
|
+
});
|
|
244
341
|
const completedAt = new Date().toISOString();
|
|
245
342
|
this.writeTaskRecord({
|
|
246
343
|
...baseTaskRecord(),
|
|
@@ -252,6 +349,9 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
252
349
|
stdoutSummary: summarizeText(stdout),
|
|
253
350
|
stderrSummary: summarizeText(stderr),
|
|
254
351
|
});
|
|
352
|
+
if (contextSession) {
|
|
353
|
+
writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
|
|
354
|
+
}
|
|
255
355
|
this.bus?.emit(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
|
|
256
356
|
...result,
|
|
257
357
|
taskLabel: `software_agent:${this.id}`,
|
|
@@ -302,16 +402,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
302
402
|
if (!text)
|
|
303
403
|
return;
|
|
304
404
|
stdout += text;
|
|
305
|
-
|
|
306
|
-
observer?.onStream?.({ taskId, stream: "stdout", chunk: text });
|
|
405
|
+
emitSafeStream("stdout", outRedactor.push(text));
|
|
307
406
|
});
|
|
308
407
|
child.stderr?.on("data", (chunk) => {
|
|
309
408
|
const text = errDecoder.write(chunk);
|
|
310
409
|
if (!text)
|
|
311
410
|
return;
|
|
312
411
|
stderr += text;
|
|
313
|
-
|
|
314
|
-
observer?.onStream?.({ taskId, stream: "stderr", chunk: text });
|
|
412
|
+
emitSafeStream("stderr", errRedactor.push(text));
|
|
315
413
|
});
|
|
316
414
|
child.on("close", (code) => {
|
|
317
415
|
child.unref();
|
|
@@ -330,7 +428,8 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
330
428
|
try {
|
|
331
429
|
mkdirSync(dir, { recursive: true });
|
|
332
430
|
const safeTaskId = safeTaskFilename(record.taskId);
|
|
333
|
-
writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(record, null, 2)}\n`);
|
|
431
|
+
writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(redactSecrets(record), null, 2)}\n`);
|
|
432
|
+
pruneSoftwareAgentTaskRecords(dir, this.config.taskLedgerMaxRecords, record.taskId);
|
|
334
433
|
}
|
|
335
434
|
catch (err) {
|
|
336
435
|
console.error(`[software-agent] Failed to write task ledger: ${err.message || String(err)}`);
|
|
@@ -371,6 +470,7 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
371
470
|
"[Akemon Software Peripheral Task Envelope]",
|
|
372
471
|
"",
|
|
373
472
|
`Task ID: ${envelope.taskId || "(unspecified)"}`,
|
|
473
|
+
`Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
|
|
374
474
|
`Source module: ${envelope.sourceModule}`,
|
|
375
475
|
`Purpose: ${envelope.purpose}`,
|
|
376
476
|
`Role scope: ${envelope.roleScope}`,
|
|
@@ -378,6 +478,12 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
378
478
|
`Risk level: ${envelope.riskLevel}`,
|
|
379
479
|
`Workdir: ${envelope.workdir}`,
|
|
380
480
|
];
|
|
481
|
+
if (envelope.workMemoryDir) {
|
|
482
|
+
lines.push(`Work memory directory: ${envelope.workMemoryDir}`);
|
|
483
|
+
}
|
|
484
|
+
if (envelope.contextPacketPath) {
|
|
485
|
+
lines.push(`Context packet path: ${envelope.contextPacketPath}`);
|
|
486
|
+
}
|
|
381
487
|
if (envelope.workdirSafety) {
|
|
382
488
|
lines.push(`Base workdir: ${envelope.workdirSafety.baseWorkdir}`);
|
|
383
489
|
lines.push(`Requested workdir: ${envelope.workdirSafety.requestedWorkdir}`);
|
|
@@ -386,11 +492,30 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
386
492
|
lines.push(`Outside workdir explicitly allowed: ${envelope.workdirSafety.allowOutsideWorkdir ? "yes" : "no"}`);
|
|
387
493
|
}
|
|
388
494
|
lines.push("", "Goal:", envelope.goal, "");
|
|
495
|
+
if (envelope.previousTaskSummary?.trim()) {
|
|
496
|
+
lines.push("Previous task summary for this Akemon context session:");
|
|
497
|
+
lines.push(envelope.previousTaskSummary.trim());
|
|
498
|
+
lines.push("");
|
|
499
|
+
}
|
|
389
500
|
if (envelope.memorySummary?.trim()) {
|
|
390
501
|
lines.push("Visible Akemon memory/context:");
|
|
391
502
|
lines.push(envelope.memorySummary.trim());
|
|
392
503
|
lines.push("");
|
|
393
504
|
}
|
|
505
|
+
if (envelope.workMemoryDir) {
|
|
506
|
+
lines.push("Work memory:");
|
|
507
|
+
lines.push("- This is user-owned working context for engineering/task continuity.");
|
|
508
|
+
lines.push("- You may read it with grep, direct file browsing, or semantic review as appropriate.");
|
|
509
|
+
lines.push("- You may update files under this directory when the task or user asks you to maintain work memory.");
|
|
510
|
+
lines.push("- Do not read or edit Akemon self memory as part of this software-agent task.");
|
|
511
|
+
lines.push("- For a quick append, use `akemon work-note \"<durable work memory>\" --source codex --kind note`.");
|
|
512
|
+
lines.push("");
|
|
513
|
+
}
|
|
514
|
+
if (envelope.workMemoryContext?.trim()) {
|
|
515
|
+
lines.push("Included work-memory context:");
|
|
516
|
+
lines.push(envelope.workMemoryContext.trim());
|
|
517
|
+
lines.push("");
|
|
518
|
+
}
|
|
394
519
|
if (envelope.allowedActions?.length) {
|
|
395
520
|
lines.push("Allowed actions:");
|
|
396
521
|
for (const item of envelope.allowedActions)
|
|
@@ -411,10 +536,35 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
411
536
|
lines.push("Instructions:");
|
|
412
537
|
lines.push("- Treat this envelope as the complete Akemon-provided context for this task.");
|
|
413
538
|
lines.push("- Do not attempt to read Akemon private memory outside the visible context above.");
|
|
539
|
+
lines.push("- Do not read or edit Akemon self memory unless the user explicitly names a normal file to inspect.");
|
|
414
540
|
lines.push("- Work only in the stated workdir unless the envelope explicitly allows otherwise.");
|
|
541
|
+
lines.push("- If you learn durable work memory, update the work memory directory or use `akemon work-note`.");
|
|
415
542
|
lines.push("- Report what changed, what you verified, and any remaining risk.");
|
|
416
543
|
return lines.join("\n");
|
|
417
544
|
}
|
|
545
|
+
export function buildContextPacketLaunchPrompt(envelope, contextPacketPath) {
|
|
546
|
+
return [
|
|
547
|
+
"[Akemon Software Peripheral Task]",
|
|
548
|
+
"",
|
|
549
|
+
`Task ID: ${envelope.taskId || "(unspecified)"}`,
|
|
550
|
+
`Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
|
|
551
|
+
`Context packet: ${contextPacketPath}`,
|
|
552
|
+
`Workdir: ${envelope.workdir}`,
|
|
553
|
+
`Work memory directory: ${envelope.workMemoryDir || "(not configured)"}`,
|
|
554
|
+
"",
|
|
555
|
+
"Goal:",
|
|
556
|
+
envelope.goal,
|
|
557
|
+
"",
|
|
558
|
+
"Instructions:",
|
|
559
|
+
"- Read the context packet first before doing repository work.",
|
|
560
|
+
"- Treat that file as the complete Akemon-provided context for this task.",
|
|
561
|
+
"- Do not read Akemon private memory outside the context packet.",
|
|
562
|
+
"- Do not read or edit Akemon self memory unless the user explicitly names a normal file to inspect.",
|
|
563
|
+
"- Work only in the stated workdir unless the packet explicitly allows otherwise.",
|
|
564
|
+
"- If you learn durable work memory, update the work memory directory or use `akemon work-note`.",
|
|
565
|
+
"- Report what changed, what you verified, and any remaining risk.",
|
|
566
|
+
].join("\n");
|
|
567
|
+
}
|
|
418
568
|
export function createOwnerTaskEnvelope(body, defaultWorkdir) {
|
|
419
569
|
const goal = typeof body?.goal === "string" ? body.goal.trim() : "";
|
|
420
570
|
if (!goal)
|
|
@@ -437,6 +587,7 @@ export function createOwnerTaskEnvelope(body, defaultWorkdir) {
|
|
|
437
587
|
: [...DEFAULT_OWNER_ALLOWED_ACTIONS],
|
|
438
588
|
forbiddenActions: [...new Set([...DEFAULT_OWNER_FORBIDDEN_ACTIONS, ...callerForbiddenActions])],
|
|
439
589
|
memorySummary: typeof body?.memorySummary === "string" ? body.memorySummary : "",
|
|
590
|
+
contextSessionId: readOptionalContextSessionId(body?.contextSessionId ?? body?.sessionId, "contextSessionId"),
|
|
440
591
|
deliverable: typeof body?.deliverable === "string"
|
|
441
592
|
? body.deliverable
|
|
442
593
|
: "Return a concise engineering summary with changes, verification, and remaining risks.",
|
|
@@ -516,8 +667,9 @@ export function readGitWorktreeStatus(workdir) {
|
|
|
516
667
|
};
|
|
517
668
|
}
|
|
518
669
|
}
|
|
519
|
-
export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
|
|
670
|
+
export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20, opts = {}) {
|
|
520
671
|
const safeLimit = normalizeTaskRecordLimit(limit);
|
|
672
|
+
const contextSessionId = opts.contextSessionId?.trim();
|
|
521
673
|
try {
|
|
522
674
|
if (!existsSync(taskLedgerDir))
|
|
523
675
|
return [];
|
|
@@ -525,6 +677,7 @@ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
|
|
|
525
677
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
526
678
|
.map((entry) => readSoftwareAgentTaskRecordFile(join(taskLedgerDir, entry.name)))
|
|
527
679
|
.filter((record) => !!record)
|
|
680
|
+
.filter((record) => !contextSessionId || record.contextSession?.sessionId === contextSessionId || record.envelope.contextSessionId === contextSessionId)
|
|
528
681
|
.sort(compareSoftwareAgentTaskRecords)
|
|
529
682
|
.slice(0, safeLimit);
|
|
530
683
|
}
|
|
@@ -536,6 +689,219 @@ export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
|
|
|
536
689
|
const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
|
|
537
690
|
return readSoftwareAgentTaskRecordFile(file);
|
|
538
691
|
}
|
|
692
|
+
export function listSoftwareAgentContextSessions(contextSessionDir, limit = 20) {
|
|
693
|
+
const safeLimit = normalizeTaskRecordLimit(limit);
|
|
694
|
+
try {
|
|
695
|
+
if (!existsSync(contextSessionDir))
|
|
696
|
+
return [];
|
|
697
|
+
return readdirSync(contextSessionDir, { withFileTypes: true })
|
|
698
|
+
.filter((entry) => entry.isDirectory())
|
|
699
|
+
.map((entry) => {
|
|
700
|
+
try {
|
|
701
|
+
return readSoftwareAgentContextSession(contextSessionDir, entry.name);
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
})
|
|
707
|
+
.filter((record) => !!record)
|
|
708
|
+
.sort(compareSoftwareAgentContextSessions)
|
|
709
|
+
.slice(0, safeLimit);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
return [];
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
export function readSoftwareAgentContextSession(contextSessionDir, sessionId, opts = {}) {
|
|
716
|
+
const safeSessionId = normalizeContextSessionId(sessionId, "sessionId");
|
|
717
|
+
if (!safeSessionId)
|
|
718
|
+
return null;
|
|
719
|
+
const sessionDir = join(contextSessionDir, safeSessionId);
|
|
720
|
+
if (!existsSync(sessionDir))
|
|
721
|
+
return null;
|
|
722
|
+
const packetPath = join(sessionDir, CONTEXT_PACKET_FILENAME);
|
|
723
|
+
const statePath = join(sessionDir, CONTEXT_SESSION_STATE_FILENAME);
|
|
724
|
+
const state = readSoftwareAgentContextSessionState(statePath);
|
|
725
|
+
const record = {
|
|
726
|
+
sessionId: safeSessionId,
|
|
727
|
+
packetPath,
|
|
728
|
+
statePath,
|
|
729
|
+
hasContextPacket: existsSync(packetPath),
|
|
730
|
+
};
|
|
731
|
+
if (state) {
|
|
732
|
+
record.updatedAt = state.updatedAt;
|
|
733
|
+
record.workMemoryDir = state.workMemoryDir;
|
|
734
|
+
record.lastTaskId = state.lastTaskId;
|
|
735
|
+
record.lastGoal = state.lastGoal;
|
|
736
|
+
record.lastResult = state.lastResult;
|
|
737
|
+
}
|
|
738
|
+
if (opts.includeContextPacket && record.hasContextPacket) {
|
|
739
|
+
try {
|
|
740
|
+
record.contextPacket = readFileSync(packetPath, "utf8");
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
record.contextPacket = "";
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return record;
|
|
747
|
+
}
|
|
748
|
+
export function pruneSoftwareAgentTaskRecords(taskLedgerDir, maxRecords = DEFAULT_TASK_LEDGER_MAX_RECORDS, preserveTaskId) {
|
|
749
|
+
const safeMaxRecords = normalizeTaskLedgerMaxRecords(maxRecords);
|
|
750
|
+
try {
|
|
751
|
+
if (!existsSync(taskLedgerDir))
|
|
752
|
+
return 0;
|
|
753
|
+
const records = readdirSync(taskLedgerDir, { withFileTypes: true })
|
|
754
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
755
|
+
.map((entry) => {
|
|
756
|
+
const file = join(taskLedgerDir, entry.name);
|
|
757
|
+
const record = readSoftwareAgentTaskRecordFile(file);
|
|
758
|
+
return record ? { file, record } : null;
|
|
759
|
+
})
|
|
760
|
+
.filter((entry) => !!entry)
|
|
761
|
+
.sort((a, b) => compareSoftwareAgentTaskRecords(a.record, b.record));
|
|
762
|
+
const keepTaskIds = new Set(records.slice(0, safeMaxRecords).map((entry) => entry.record.taskId));
|
|
763
|
+
if (preserveTaskId)
|
|
764
|
+
keepTaskIds.add(preserveTaskId);
|
|
765
|
+
let deleted = 0;
|
|
766
|
+
for (const entry of records) {
|
|
767
|
+
if (keepTaskIds.has(entry.record.taskId))
|
|
768
|
+
continue;
|
|
769
|
+
try {
|
|
770
|
+
unlinkSync(entry.file);
|
|
771
|
+
deleted++;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
// Best effort: retention should not break task completion.
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return deleted;
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return 0;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
export function buildSoftwareAgentChildEnvironment(opts = {}) {
|
|
784
|
+
const policy = normalizeSoftwareAgentEnvPolicy(opts.policy);
|
|
785
|
+
const sourceEnv = opts.sourceEnv || process.env;
|
|
786
|
+
if (policy === "inherit") {
|
|
787
|
+
return {
|
|
788
|
+
env: sourceEnv,
|
|
789
|
+
audit: { policy },
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
const allowlist = normalizeSoftwareAgentEnvAllowlist([
|
|
793
|
+
...DEFAULT_SOFTWARE_AGENT_ENV_ALLOWLIST,
|
|
794
|
+
...(opts.allowlist || []),
|
|
795
|
+
]);
|
|
796
|
+
const env = {};
|
|
797
|
+
for (const key of allowlist) {
|
|
798
|
+
if (isForbiddenSoftwareAgentEnvKey(key))
|
|
799
|
+
continue;
|
|
800
|
+
const value = sourceEnv[key];
|
|
801
|
+
if (value !== undefined)
|
|
802
|
+
env[key] = value;
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
env,
|
|
806
|
+
audit: {
|
|
807
|
+
policy,
|
|
808
|
+
allowedKeys: Object.keys(env).sort(),
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function writeSoftwareAgentContextPacket(contextSessionDir, envelope) {
|
|
813
|
+
const sessionId = normalizeContextSessionId(envelope.contextSessionId) || envelope.taskId || randomUUID();
|
|
814
|
+
const sessionDir = join(contextSessionDir, sessionId);
|
|
815
|
+
const packetPath = join(sessionDir, CONTEXT_PACKET_FILENAME);
|
|
816
|
+
const statePath = join(sessionDir, CONTEXT_SESSION_STATE_FILENAME);
|
|
817
|
+
const previousTaskSummary = readSoftwareAgentContextSessionSummary(statePath);
|
|
818
|
+
const packetEnvelope = {
|
|
819
|
+
...envelope,
|
|
820
|
+
contextSessionId: sessionId,
|
|
821
|
+
contextPacketPath: packetPath,
|
|
822
|
+
previousTaskSummary,
|
|
823
|
+
};
|
|
824
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
825
|
+
const content = buildTaskEnvelopePrompt(packetEnvelope);
|
|
826
|
+
writeFileSync(packetPath, `${redactSecrets(content)}\n`);
|
|
827
|
+
return {
|
|
828
|
+
envelope: packetEnvelope,
|
|
829
|
+
audit: { sessionId, packetPath, statePath },
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function writeSoftwareAgentContextSessionState(statePath, envelope, result, updatedAt) {
|
|
833
|
+
try {
|
|
834
|
+
const state = {
|
|
835
|
+
schemaVersion: 1,
|
|
836
|
+
sessionId: envelope.contextSessionId,
|
|
837
|
+
updatedAt,
|
|
838
|
+
workMemoryDir: envelope.workMemoryDir,
|
|
839
|
+
lastTaskId: result.taskId,
|
|
840
|
+
lastGoal: envelope.goal,
|
|
841
|
+
lastResult: {
|
|
842
|
+
success: result.success,
|
|
843
|
+
exitCode: result.exitCode,
|
|
844
|
+
durationMs: result.durationMs,
|
|
845
|
+
outputSummary: summarizeText(result.output || "", MAX_CONTEXT_SESSION_SUMMARY_CHARS),
|
|
846
|
+
errorSummary: result.error ? summarizeText(result.error, MAX_CONTEXT_SESSION_SUMMARY_CHARS) : undefined,
|
|
847
|
+
},
|
|
848
|
+
};
|
|
849
|
+
writeFileSync(statePath, `${JSON.stringify(redactSecrets(state), null, 2)}\n`);
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
console.error(`[software-agent] Failed to write context session state: ${err.message || String(err)}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function readSoftwareAgentContextSessionSummary(statePath) {
|
|
856
|
+
try {
|
|
857
|
+
const parsed = readSoftwareAgentContextSessionState(statePath);
|
|
858
|
+
if (!parsed?.lastTaskId || !parsed.lastResult)
|
|
859
|
+
return undefined;
|
|
860
|
+
const result = parsed.lastResult;
|
|
861
|
+
const status = result.success === true ? "completed" : "failed";
|
|
862
|
+
const lines = [
|
|
863
|
+
`Previous task: ${parsed.lastTaskId}`,
|
|
864
|
+
parsed.lastGoal ? `Previous goal: ${parsed.lastGoal}` : "",
|
|
865
|
+
`Status: ${status}`,
|
|
866
|
+
Number.isInteger(result.exitCode) ? `Exit code: ${result.exitCode}` : "Exit code: null",
|
|
867
|
+
Number.isInteger(result.durationMs) ? `Duration: ${result.durationMs}ms` : "",
|
|
868
|
+
result.outputSummary?.text ? "Previous output summary:" : "",
|
|
869
|
+
result.outputSummary?.text || "",
|
|
870
|
+
result.errorSummary?.text ? "Previous error summary:" : "",
|
|
871
|
+
result.errorSummary?.text || "",
|
|
872
|
+
].filter(Boolean);
|
|
873
|
+
const summary = lines.join("\n").trim();
|
|
874
|
+
return summary ? summarizeText(summary, MAX_CONTEXT_SESSION_SUMMARY_CHARS).text : undefined;
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function readSoftwareAgentContextSessionState(statePath) {
|
|
881
|
+
try {
|
|
882
|
+
if (!existsSync(statePath))
|
|
883
|
+
return null;
|
|
884
|
+
const parsed = JSON.parse(readFileSync(statePath, "utf8"));
|
|
885
|
+
if (!isSoftwareAgentContextSessionState(parsed))
|
|
886
|
+
return null;
|
|
887
|
+
return parsed;
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function isSoftwareAgentContextSessionState(value) {
|
|
894
|
+
return value
|
|
895
|
+
&& value.schemaVersion === 1
|
|
896
|
+
&& typeof value.sessionId === "string"
|
|
897
|
+
&& typeof value.updatedAt === "string"
|
|
898
|
+
&& (value.workMemoryDir === undefined || typeof value.workMemoryDir === "string")
|
|
899
|
+
&& typeof value.lastTaskId === "string"
|
|
900
|
+
&& value.lastResult
|
|
901
|
+
&& typeof value.lastResult.success === "boolean"
|
|
902
|
+
&& (typeof value.lastResult.exitCode === "number" || value.lastResult.exitCode === null)
|
|
903
|
+
&& typeof value.lastResult.durationMs === "number";
|
|
904
|
+
}
|
|
539
905
|
function readOptionalString(value, field) {
|
|
540
906
|
if (value === undefined || value === null)
|
|
541
907
|
return undefined;
|
|
@@ -544,6 +910,27 @@ function readOptionalString(value, field) {
|
|
|
544
910
|
const trimmed = value.trim();
|
|
545
911
|
return trimmed || undefined;
|
|
546
912
|
}
|
|
913
|
+
function readOptionalContextSessionId(value, field) {
|
|
914
|
+
if (value === undefined || value === null || value === "")
|
|
915
|
+
return undefined;
|
|
916
|
+
if (typeof value !== "string")
|
|
917
|
+
throw new Error(`Invalid ${field}: expected string`);
|
|
918
|
+
return normalizeContextSessionId(value, field);
|
|
919
|
+
}
|
|
920
|
+
function normalizeContextSessionId(value, field = "contextSessionId") {
|
|
921
|
+
if (!value)
|
|
922
|
+
return undefined;
|
|
923
|
+
const trimmed = value.trim();
|
|
924
|
+
if (!trimmed)
|
|
925
|
+
return undefined;
|
|
926
|
+
if (trimmed.length > MAX_CONTEXT_SESSION_ID_LENGTH) {
|
|
927
|
+
throw new Error(`Invalid ${field}: expected at most ${MAX_CONTEXT_SESSION_ID_LENGTH} characters`);
|
|
928
|
+
}
|
|
929
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(trimmed)) {
|
|
930
|
+
throw new Error(`Invalid ${field}: expected letters, numbers, dot, underscore, or hyphen`);
|
|
931
|
+
}
|
|
932
|
+
return trimmed;
|
|
933
|
+
}
|
|
547
934
|
function normalizeSoftwareAgentTaskOptions(options) {
|
|
548
935
|
if (!options)
|
|
549
936
|
return {};
|
|
@@ -552,6 +939,38 @@ function normalizeSoftwareAgentTaskOptions(options) {
|
|
|
552
939
|
}
|
|
553
940
|
return options;
|
|
554
941
|
}
|
|
942
|
+
function normalizeSoftwareAgentEnvPolicy(value) {
|
|
943
|
+
if (value === undefined || value === null || value === "")
|
|
944
|
+
return DEFAULT_SOFTWARE_AGENT_ENV_POLICY;
|
|
945
|
+
if (value === "inherit" || value === "allowlist")
|
|
946
|
+
return value;
|
|
947
|
+
throw new Error("Invalid software-agent env policy: expected inherit or allowlist");
|
|
948
|
+
}
|
|
949
|
+
function normalizeSoftwareAgentEnvAllowlist(values) {
|
|
950
|
+
if (!values)
|
|
951
|
+
return [];
|
|
952
|
+
const seen = new Set();
|
|
953
|
+
for (const value of values) {
|
|
954
|
+
if (typeof value !== "string") {
|
|
955
|
+
throw new Error("Invalid software-agent env allowlist entry: expected string");
|
|
956
|
+
}
|
|
957
|
+
const key = value.trim();
|
|
958
|
+
if (!key)
|
|
959
|
+
continue;
|
|
960
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
961
|
+
throw new Error(`Invalid software-agent env allowlist entry: ${key}`);
|
|
962
|
+
}
|
|
963
|
+
seen.add(key);
|
|
964
|
+
}
|
|
965
|
+
return [...seen];
|
|
966
|
+
}
|
|
967
|
+
function isForbiddenSoftwareAgentEnvKey(key) {
|
|
968
|
+
const upper = key.toUpperCase();
|
|
969
|
+
if (upper.startsWith("AKEMON_"))
|
|
970
|
+
return true;
|
|
971
|
+
const looksLikeCredential = /(?:SECRET|TOKEN|ACCESS|KEY|CREDENTIAL)/.test(upper);
|
|
972
|
+
return looksLikeCredential && (upper.includes("RELAY") || upper.includes("OWNER"));
|
|
973
|
+
}
|
|
555
974
|
function isAbortSignal(value) {
|
|
556
975
|
return !!value
|
|
557
976
|
&& typeof value.aborted === "boolean"
|
|
@@ -610,6 +1029,11 @@ function normalizeTaskRecordLimit(limit) {
|
|
|
610
1029
|
return 20;
|
|
611
1030
|
return Math.min(limit, 100);
|
|
612
1031
|
}
|
|
1032
|
+
function normalizeTaskLedgerMaxRecords(limit) {
|
|
1033
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
1034
|
+
return DEFAULT_TASK_LEDGER_MAX_RECORDS;
|
|
1035
|
+
return Math.min(limit, 10_000);
|
|
1036
|
+
}
|
|
613
1037
|
function readSoftwareAgentTaskRecordFile(file) {
|
|
614
1038
|
try {
|
|
615
1039
|
if (!existsSync(file))
|
|
@@ -640,6 +1064,13 @@ function compareSoftwareAgentTaskRecords(a, b) {
|
|
|
640
1064
|
return bTime - aTime;
|
|
641
1065
|
return b.taskId.localeCompare(a.taskId);
|
|
642
1066
|
}
|
|
1067
|
+
function compareSoftwareAgentContextSessions(a, b) {
|
|
1068
|
+
const bTime = Date.parse(b.updatedAt || "") || 0;
|
|
1069
|
+
const aTime = Date.parse(a.updatedAt || "") || 0;
|
|
1070
|
+
if (bTime !== aTime)
|
|
1071
|
+
return bTime - aTime;
|
|
1072
|
+
return b.sessionId.localeCompare(a.sessionId);
|
|
1073
|
+
}
|
|
643
1074
|
function buildCodexExecCommand(opts) {
|
|
644
1075
|
const args = [
|
|
645
1076
|
"exec",
|