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.
@@ -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 STREAM_SUMMARY_TAIL_CHARS = 8_000;
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 = 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
- if (this.activeChild?.pid) {
104
+ const activePid = this.activeChild?.pid;
105
+ if (activePid) {
106
+ const processGroupId = -activePid;
69
107
  try {
70
- process.kill(-this.activeChild.pid, "SIGTERM");
108
+ process.kill(processGroupId, "SIGTERM");
71
109
  }
72
110
  catch { }
73
111
  setTimeout(() => {
74
112
  try {
75
- process.kill(-this.activeChild.pid, "SIGKILL");
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
- const effectiveEnvelope = { ...envelope, taskId, workdir, workdirSafety };
121
- const prompt = buildTaskEnvelopePrompt(effectiveEnvelope);
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: process.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
- relay.sendTaskStream(taskId, "stdout", tailOut);
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
- relay.sendTaskStream(taskId, "stderr", tailErr);
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
- relay.sendTaskStream(taskId, "stdout", text);
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
- relay.sendTaskStream(taskId, "stderr", text);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",