@hilbras/omninode 2.0.0-alpha.3 → 2.0.0-alpha.4

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/dist/index.js CHANGED
@@ -140,6 +140,11 @@ var agentConfigSchema = z.object({
140
140
  input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
141
141
  timeout_ms: z.number().int().positive().optional(),
142
142
  inherit_env: z.boolean().optional(),
143
+ env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
144
+ env_allowlist: z.array(z.string()).optional(),
145
+ env_denylist: z.array(z.string()).optional(),
146
+ max_output_bytes: z.number().int().positive().optional(),
147
+ allow_external_cwd: z.boolean().optional(),
143
148
  metadata: z.record(z.unknown()).optional()
144
149
  }).strict();
145
150
  var roleConfigSchema = z.object({
@@ -314,6 +319,11 @@ function toAgentConfig(raw) {
314
319
  inputMode: raw.input_mode,
315
320
  timeoutMs: raw.timeout_ms,
316
321
  inheritEnv: raw.inherit_env,
322
+ envPolicy: raw.env_policy,
323
+ envAllowlist: raw.env_allowlist,
324
+ envDenylist: raw.env_denylist,
325
+ maxOutputBytes: raw.max_output_bytes,
326
+ allowExternalCwd: raw.allow_external_cwd,
317
327
  metadata: raw.metadata
318
328
  };
319
329
  }
@@ -1010,20 +1020,94 @@ function requireCompletion(session) {
1010
1020
  }
1011
1021
  }
1012
1022
 
1013
- // src/agents/process/index.ts
1014
- var DEFAULT_TIMEOUT_MS2 = 6e5;
1015
- var KILL_GRACE_MS = 5e3;
1023
+ // src/agents/cwd.ts
1024
+ import { statSync } from "fs";
1025
+ import { isAbsolute, relative, resolve } from "path";
1026
+ function validateWorkingDirectory(cwd, options = {}) {
1027
+ if (!cwd) return void 0;
1028
+ const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
1029
+ let stats;
1030
+ try {
1031
+ stats = statSync(normalized);
1032
+ } catch (error) {
1033
+ throw new AgentError(
1034
+ "AGENT_NOT_FOUND",
1035
+ `Working directory "${normalized}" does not exist or is not accessible.`,
1036
+ { cause: error, details: { cwd: normalized } }
1037
+ );
1038
+ }
1039
+ if (!stats.isDirectory()) {
1040
+ throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
1041
+ details: { cwd: normalized }
1042
+ });
1043
+ }
1044
+ try {
1045
+ statSync(resolve(normalized, "."));
1046
+ } catch (error) {
1047
+ throw new AgentError(
1048
+ "AGENT_NOT_FOUND",
1049
+ `Working directory "${normalized}" is not accessible.`,
1050
+ { cause: error }
1051
+ );
1052
+ }
1053
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
1054
+ const rel = relative(projectRoot, normalized);
1055
+ const outsideProject = rel.startsWith("..") || isAbsolute(rel);
1056
+ if (outsideProject && options.allowExternal !== true) {
1057
+ throw new AgentError(
1058
+ "AGENT_NOT_FOUND",
1059
+ `Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
1060
+ { details: { cwd: normalized, projectRoot } }
1061
+ );
1062
+ }
1063
+ return { cwd: normalized, outsideProject };
1064
+ }
1065
+
1066
+ // src/agents/env.ts
1067
+ var BASELINE_KEYS = ["PATH", "HOME"];
1068
+ function resolveEnvPolicy(config) {
1069
+ if (config.envPolicy !== void 0) return config.envPolicy;
1070
+ if (config.inheritEnv === false) return "explicit";
1071
+ return "inherit";
1072
+ }
1016
1073
  function buildChildEnv(config) {
1017
1074
  const configured = config.env ?? {};
1018
- if (config.inheritEnv !== false) {
1075
+ const policy = resolveEnvPolicy(config);
1076
+ if (policy === "inherit") {
1019
1077
  return { ...process.env, ...configured };
1020
1078
  }
1021
- return {
1022
- PATH: process.env.PATH ?? "",
1023
- HOME: process.env.HOME ?? "",
1024
- ...configured
1025
- };
1079
+ const base = {};
1080
+ for (const key of BASELINE_KEYS) {
1081
+ const value = process.env[key];
1082
+ if (value !== void 0) base[key] = value;
1083
+ }
1084
+ switch (policy) {
1085
+ case "explicit":
1086
+ return { ...base, ...configured };
1087
+ case "allowlist": {
1088
+ const allowed = { ...base };
1089
+ for (const key of config.envAllowlist ?? []) {
1090
+ const value = process.env[key];
1091
+ if (value !== void 0) allowed[key] = value;
1092
+ }
1093
+ return { ...allowed, ...configured };
1094
+ }
1095
+ case "denylist": {
1096
+ const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
1097
+ const filtered = {};
1098
+ for (const [key, value] of Object.entries(process.env)) {
1099
+ if (denied.has(key.toUpperCase())) continue;
1100
+ filtered[key] = value;
1101
+ }
1102
+ return { ...filtered, ...configured };
1103
+ }
1104
+ }
1026
1105
  }
1106
+
1107
+ // src/agents/process/index.ts
1108
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
1109
+ var KILL_GRACE_MS = 5e3;
1110
+ var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
1027
1111
  function composeTaskText(input) {
1028
1112
  const sections = [`# Objective
1029
1113
  ${input.objective}`];
@@ -1085,40 +1169,73 @@ var ProcessAgent = class {
1085
1169
  }
1086
1170
  const mode = this.config.inputMode ?? "stdin";
1087
1171
  const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
1088
- return new Promise((resolve, reject) => {
1172
+ return new Promise((resolve2, reject) => {
1173
+ let cwd;
1174
+ try {
1175
+ cwd = validateWorkingDirectory(this.config.cwd, {
1176
+ allowExternal: this.config.allowExternalCwd
1177
+ })?.cwd;
1178
+ } catch (error) {
1179
+ reject(error);
1180
+ return;
1181
+ }
1089
1182
  const child = spawn(
1090
1183
  command,
1091
1184
  [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
1092
1185
  {
1093
- cwd: this.config.cwd,
1186
+ ...cwd !== void 0 ? { cwd } : {},
1094
1187
  env: buildChildEnv(this.config),
1095
- stdio: ["pipe", "pipe", "pipe"]
1188
+ stdio: ["pipe", "pipe", "pipe"],
1189
+ // Own process group so the whole tree can be terminated together
1190
+ // (roadmap §8: no orphaned children).
1191
+ ...process.platform === "win32" ? {} : { detached: true }
1096
1192
  }
1097
1193
  );
1098
1194
  this.running.set(input.taskId, child);
1195
+ GLOBAL_RUNNING.add(child);
1196
+ installSignalForwarding();
1099
1197
  let stdout = "";
1100
1198
  let stderr = "";
1101
1199
  let timedOut = false;
1200
+ let outputLimitExceeded = false;
1102
1201
  let settled = false;
1202
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
1103
1203
  const timer = setTimeout(() => {
1104
1204
  timedOut = true;
1105
- this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
1106
- child.kill("SIGTERM");
1107
- setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
1205
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
1206
+ killTree(child, "SIGTERM");
1207
+ setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
1108
1208
  }, timeoutMs);
1109
1209
  const settle = (finish) => {
1110
1210
  if (settled) return;
1111
1211
  settled = true;
1112
1212
  clearTimeout(timer);
1113
1213
  this.running.delete(input.taskId);
1214
+ GLOBAL_RUNNING.delete(child);
1114
1215
  finish();
1115
1216
  };
1116
- child.stdout?.on("data", (chunk) => {
1117
- stdout += chunk.toString();
1118
- });
1119
- child.stderr?.on("data", (chunk) => {
1120
- stderr += chunk.toString();
1121
- });
1217
+ const collect = (into, chunk) => {
1218
+ if (outputLimitExceeded) return;
1219
+ const text = sanitizeChunk(chunk);
1220
+ if (into === "stdout") {
1221
+ if (stdout.length + text.length > maxOutputBytes) {
1222
+ stdout = (stdout + text).slice(0, maxOutputBytes);
1223
+ outputLimitExceeded = true;
1224
+ this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
1225
+ killTree(child, "SIGKILL");
1226
+ return;
1227
+ }
1228
+ stdout += text;
1229
+ } else {
1230
+ if (stderr.length + text.length > maxOutputBytes) {
1231
+ stderr = (stderr + text).slice(0, maxOutputBytes);
1232
+ return;
1233
+ }
1234
+ stderr += text;
1235
+ }
1236
+ };
1237
+ child.stdout?.on("data", (chunk) => collect("stdout", chunk));
1238
+ child.stderr?.on("data", (chunk) => collect("stderr", chunk));
1122
1239
  child.stdin?.on("error", () => {
1123
1240
  });
1124
1241
  if (mode !== "arg") {
@@ -1172,7 +1289,7 @@ var ProcessAgent = class {
1172
1289
  const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
1173
1290
  if (mode === "protocol") {
1174
1291
  try {
1175
- resolve(
1292
+ resolve2(
1176
1293
  interpretProtocolRun({
1177
1294
  input,
1178
1295
  agentName: this.config.name,
@@ -1187,14 +1304,14 @@ var ProcessAgent = class {
1187
1304
  return;
1188
1305
  }
1189
1306
  const trimmed = stdout.trim();
1190
- resolve({
1307
+ resolve2({
1191
1308
  taskId: input.taskId,
1192
- status: code === 0 ? "completed" : "failed",
1309
+ status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
1193
1310
  summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
1194
1311
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
1195
1312
  exitCode: code ?? void 0,
1196
1313
  logs,
1197
- error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
1314
+ ...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
1198
1315
  });
1199
1316
  });
1200
1317
  });
@@ -1203,7 +1320,7 @@ var ProcessAgent = class {
1203
1320
  /** Ask the agent to stop current work (SIGTERM). */
1204
1321
  async cancel(taskId) {
1205
1322
  const child = this.running.get(taskId);
1206
- if (child) child.kill("SIGTERM");
1323
+ if (child) killTree(child, "SIGTERM");
1207
1324
  }
1208
1325
  runningTaskIds() {
1209
1326
  return [...this.running.keys()];
@@ -1278,6 +1395,43 @@ function interpretProtocolRun({
1278
1395
  ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
1279
1396
  };
1280
1397
  }
1398
+ var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
1399
+ var signalForwardingInstalled = false;
1400
+ function installSignalForwarding() {
1401
+ if (signalForwardingInstalled) return;
1402
+ signalForwardingInstalled = true;
1403
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1404
+ process.on(signal, () => {
1405
+ for (const child of GLOBAL_RUNNING) {
1406
+ killTree(child, "SIGTERM");
1407
+ }
1408
+ });
1409
+ }
1410
+ }
1411
+ function killTree(child, signal) {
1412
+ if (child.pid === void 0) return;
1413
+ if (process.platform !== "win32") {
1414
+ try {
1415
+ process.kill(-child.pid, signal);
1416
+ return;
1417
+ } catch {
1418
+ }
1419
+ }
1420
+ try {
1421
+ child.kill(signal);
1422
+ } catch {
1423
+ }
1424
+ }
1425
+ function describeExit(code, signal) {
1426
+ if (signal !== null) {
1427
+ return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
1428
+ }
1429
+ if (code === null) return "Process terminated without an exit code.";
1430
+ return `Process exited with code ${code}.`;
1431
+ }
1432
+ function sanitizeChunk(chunk) {
1433
+ return chunk.toString("utf8").replace(/\u0000/g, "");
1434
+ }
1281
1435
  function normalizeReport(payload, taskId, agentName, index) {
1282
1436
  if (payload === null || typeof payload !== "object") return void 0;
1283
1437
  const raw = payload;
@@ -1297,6 +1451,40 @@ function firstLine(text, maxLength = 200) {
1297
1451
  return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
1298
1452
  }
1299
1453
 
1454
+ // src/agents/adapters.ts
1455
+ var AgentAdapterRegistry = class {
1456
+ adapters = /* @__PURE__ */ new Map();
1457
+ register(adapter) {
1458
+ this.adapters.set(adapter.integration, adapter);
1459
+ }
1460
+ get(integration) {
1461
+ return this.adapters.get(integration);
1462
+ }
1463
+ list() {
1464
+ return [...this.adapters.values()];
1465
+ }
1466
+ create(config) {
1467
+ const adapter = this.get(config.integration);
1468
+ if (!adapter) {
1469
+ throw new AgentError(
1470
+ "AGENT_NOT_FOUND",
1471
+ `No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
1472
+ );
1473
+ }
1474
+ return adapter.create(config);
1475
+ }
1476
+ };
1477
+ var ProcessAgentAdapter = class {
1478
+ constructor(factory) {
1479
+ this.factory = factory;
1480
+ }
1481
+ factory;
1482
+ integration = "process";
1483
+ create(config) {
1484
+ return this.factory(config);
1485
+ }
1486
+ };
1487
+
1300
1488
  // src/agents/registry.ts
1301
1489
  var AgentRegistry = class {
1302
1490
  agents = /* @__PURE__ */ new Map();
@@ -1312,14 +1500,15 @@ var AgentRegistry = class {
1312
1500
  };
1313
1501
 
1314
1502
  // src/agents/factory.ts
1315
- function createAgent(config) {
1316
- if (config.integration === "process") {
1317
- return new ProcessAgent(config);
1318
- }
1503
+ var defaultAgentAdapters = new AgentAdapterRegistry();
1504
+ defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
1505
+ function createAgent(config, adapters = defaultAgentAdapters) {
1506
+ const registered = adapters.get(config.integration);
1507
+ if (registered) return registered.create(config);
1319
1508
  if (config.integration === "native") {
1320
1509
  throw new OmniNodeError(
1321
1510
  "NOT_IMPLEMENTED",
1322
- `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1511
+ `No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
1323
1512
  );
1324
1513
  }
1325
1514
  throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
@@ -1699,7 +1888,7 @@ function classifyThrown(error) {
1699
1888
  return "failed";
1700
1889
  }
1701
1890
  function sleep(ms) {
1702
- return new Promise((resolve) => setTimeout(resolve, ms));
1891
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1703
1892
  }
1704
1893
 
1705
1894
  // src/audit/index.ts
@@ -3037,12 +3226,14 @@ var ProjectStores = class {
3037
3226
  };
3038
3227
 
3039
3228
  // src/version.ts
3040
- var OMNINODE_VERSION = "2.0.0-alpha.3";
3229
+ var OMNINODE_VERSION = "2.0.0-alpha.4";
3041
3230
  export {
3231
+ AgentAdapterRegistry,
3042
3232
  AgentError,
3043
3233
  AgentRegistry,
3044
3234
  ConfigError,
3045
3235
  ConsoleLogSink,
3236
+ DEFAULT_MAX_OUTPUT_BYTES,
3046
3237
  FileAuditLog,
3047
3238
  FilePipelineRunStore,
3048
3239
  FilePlanStore,
@@ -3067,6 +3258,7 @@ export {
3067
3258
  PipelineError,
3068
3259
  PlannerError,
3069
3260
  ProcessAgent,
3261
+ ProcessAgentAdapter,
3070
3262
  ProjectStores,
3071
3263
  ProtocolDecoder,
3072
3264
  ProtocolError,
@@ -3100,6 +3292,7 @@ export {
3100
3292
  decodeMessage,
3101
3293
  decodeStream,
3102
3294
  defaultProjectConfigYaml,
3295
+ describeExit,
3103
3296
  encodeMessage,
3104
3297
  envelope,
3105
3298
  expandEnvRefs,
@@ -3108,6 +3301,7 @@ export {
3108
3301
  isOmniNodeError,
3109
3302
  isSupportedProtocol,
3110
3303
  keywords,
3304
+ killTree,
3111
3305
  loadConfig,
3112
3306
  logger,
3113
3307
  newMessageId,
@@ -3122,8 +3316,11 @@ export {
3122
3316
  requestJson,
3123
3317
  requireCompletion,
3124
3318
  resolveApiKey,
3319
+ resolveEnvPolicy,
3125
3320
  roleConfigSchema,
3321
+ sanitizeChunk,
3126
3322
  similar,
3127
- validateEnvelope
3323
+ validateEnvelope,
3324
+ validateWorkingDirectory
3128
3325
  };
3129
3326
  //# sourceMappingURL=index.js.map