@hilbras/omninode 2.0.0-alpha.2 → 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/cli/index.js CHANGED
@@ -6,7 +6,7 @@ import { realpathSync } from "fs";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/version.ts
9
- var OMNINODE_VERSION = "2.0.0-alpha.2";
9
+ var OMNINODE_VERSION = "2.0.0-alpha.4";
10
10
 
11
11
  // src/errors/index.ts
12
12
  var OmniNodeError = class extends Error {
@@ -44,11 +44,6 @@ var PipelineError = class extends OmniNodeError {
44
44
  super(code, message, options);
45
45
  }
46
46
  };
47
- var ProtocolError = class extends OmniNodeError {
48
- constructor(message, options = {}) {
49
- super("PROTOCOL_VIOLATION", message, options);
50
- }
51
- };
52
47
  var PlannerError = class extends OmniNodeError {
53
48
  constructor(code, message, options = {}) {
54
49
  super(code, message, options);
@@ -88,6 +83,11 @@ var agentConfigSchema = z.object({
88
83
  input_mode: z.enum(["stdin", "arg", "protocol"]).default("stdin"),
89
84
  timeout_ms: z.number().int().positive().optional(),
90
85
  inherit_env: z.boolean().optional(),
86
+ env_policy: z.enum(["inherit", "allowlist", "denylist", "explicit"]).optional(),
87
+ env_allowlist: z.array(z.string()).optional(),
88
+ env_denylist: z.array(z.string()).optional(),
89
+ max_output_bytes: z.number().int().positive().optional(),
90
+ allow_external_cwd: z.boolean().optional(),
91
91
  metadata: z.record(z.unknown()).optional()
92
92
  }).strict();
93
93
  var roleConfigSchema = z.object({
@@ -262,6 +262,11 @@ function toAgentConfig(raw) {
262
262
  inputMode: raw.input_mode,
263
263
  timeoutMs: raw.timeout_ms,
264
264
  inheritEnv: raw.inherit_env,
265
+ envPolicy: raw.env_policy,
266
+ envAllowlist: raw.env_allowlist,
267
+ envDenylist: raw.env_denylist,
268
+ maxOutputBytes: raw.max_output_bytes,
269
+ allowExternalCwd: raw.allow_external_cwd,
265
270
  metadata: raw.metadata
266
271
  };
267
272
  }
@@ -897,19 +902,416 @@ function registerModelsCommand(program) {
897
902
 
898
903
  // src/agents/process/index.ts
899
904
  import { spawn } from "child_process";
900
- var DEFAULT_TIMEOUT_MS2 = 6e5;
901
- var KILL_GRACE_MS = 5e3;
905
+
906
+ // src/agent-protocol/version.ts
907
+ var PROTOCOL_V2 = "omninode-agent-protocol/2";
908
+ var SUPPORTED_PROTOCOL_VERSIONS = [
909
+ "omninode-agent-protocol/1",
910
+ PROTOCOL_V2
911
+ ];
912
+ var MAX_MESSAGE_BYTES = 1048576;
913
+ function isSupportedProtocol(protocol) {
914
+ return SUPPORTED_PROTOCOL_VERSIONS.includes(protocol);
915
+ }
916
+
917
+ // src/agent-protocol/types.ts
918
+ function newMessageId(prefix = "msg") {
919
+ const random = Math.random().toString(36).slice(2, 10);
920
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
921
+ }
922
+ function envelope(type, payload, ids = {}) {
923
+ return {
924
+ protocol: PROTOCOL_V2,
925
+ messageId: newMessageId(),
926
+ ...ids.requestId !== void 0 ? { requestId: ids.requestId } : {},
927
+ ...ids.taskId !== void 0 ? { taskId: ids.taskId } : {},
928
+ ...ids.agentId !== void 0 ? { agentId: ids.agentId } : {},
929
+ ...ids.pipelineId !== void 0 ? { pipelineId: ids.pipelineId } : {},
930
+ ...ids.executionId !== void 0 ? { executionId: ids.executionId } : {},
931
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
932
+ type,
933
+ payload,
934
+ ...ids.metadata !== void 0 ? { metadata: ids.metadata } : {}
935
+ };
936
+ }
937
+
938
+ // src/agent-protocol/codec.ts
939
+ var KNOWN_TYPES = /* @__PURE__ */ new Set([
940
+ // node → agent
941
+ "TASK",
942
+ "HELLO_ACK",
943
+ "CONTEXT",
944
+ "INSTRUCTION",
945
+ "RESPONSE",
946
+ "CANCEL",
947
+ // agent → node
948
+ "HELLO",
949
+ "TASK_REQUEST",
950
+ "TASK_ACCEPTED",
951
+ "TASK_STARTED",
952
+ "STATUS",
953
+ "QUESTION",
954
+ "REPORT",
955
+ "ARTIFACT",
956
+ "COMPLETION",
957
+ "ERROR",
958
+ "CANCELLED"
959
+ ]);
960
+ var PAYLOAD_REQUIRED_TYPES = /* @__PURE__ */ new Set(["TASK", "REPORT", "COMPLETION", "ERROR", "QUESTION", "ARTIFACT", "HELLO"]);
961
+ function validateEnvelope(value, line) {
962
+ const sample = line?.slice(0, 200);
963
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
964
+ return { ok: false, error: { code: "NOT_AN_OBJECT", message: "message must be a JSON object", ...sample ? { sample } : {} } };
965
+ }
966
+ const raw = value;
967
+ const protocol = raw.protocol;
968
+ if (protocol === void 0) {
969
+ const type = raw.type;
970
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
971
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(type)}`, ...sample ? { sample } : {} } };
972
+ }
973
+ return {
974
+ ok: true,
975
+ legacy: true,
976
+ envelope: {
977
+ protocol: "omninode-agent-protocol/1",
978
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "legacy",
979
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
980
+ type,
981
+ payload: raw.payload
982
+ }
983
+ };
984
+ }
985
+ if (typeof protocol !== "string" || !isSupportedProtocol(protocol)) {
986
+ return {
987
+ ok: false,
988
+ error: { code: "UNSUPPORTED_VERSION", message: `unsupported protocol "${String(protocol)}"`, ...sample ? { sample } : {} }
989
+ };
990
+ }
991
+ if (raw.type === void 0) {
992
+ return { ok: false, error: { code: "MISSING_FIELD", message: "message is missing 'type'", ...sample ? { sample } : {} } };
993
+ }
994
+ if (typeof raw.type !== "string" || !KNOWN_TYPES.has(raw.type)) {
995
+ return { ok: false, error: { code: "UNKNOWN_TYPE", message: `unknown message type ${String(raw.type)}`, ...sample ? { sample } : {} } };
996
+ }
997
+ if (raw.timestamp !== void 0 && typeof raw.timestamp !== "string") {
998
+ return { ok: false, error: { code: "MISSING_FIELD", message: "'timestamp' must be a string", ...sample ? { sample } : {} } };
999
+ }
1000
+ if (PAYLOAD_REQUIRED_TYPES.has(raw.type) && (raw.payload === void 0 || raw.payload === null)) {
1001
+ return { ok: false, error: { code: "INVALID_PAYLOAD", message: `${raw.type} requires a payload`, ...sample ? { sample } : {} } };
1002
+ }
1003
+ return {
1004
+ ok: true,
1005
+ legacy: protocol !== PROTOCOL_V2,
1006
+ envelope: {
1007
+ protocol,
1008
+ messageId: typeof raw.messageId === "string" ? raw.messageId : "unknown",
1009
+ ...typeof raw.requestId === "string" ? { requestId: raw.requestId } : {},
1010
+ ...typeof raw.taskId === "string" ? { taskId: raw.taskId } : {},
1011
+ ...typeof raw.agentId === "string" ? { agentId: raw.agentId } : {},
1012
+ ...typeof raw.pipelineId === "string" ? { pipelineId: raw.pipelineId } : {},
1013
+ ...typeof raw.executionId === "string" ? { executionId: raw.executionId } : {},
1014
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
1015
+ type: raw.type,
1016
+ payload: raw.payload,
1017
+ ...raw.metadata !== null && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}
1018
+ }
1019
+ };
1020
+ }
1021
+ function decodeMessage(line, maxBytes = MAX_MESSAGE_BYTES) {
1022
+ const trimmed = line.trim();
1023
+ if (trimmed.length === 0) {
1024
+ return { ok: false, error: { code: "INVALID_JSON", message: "empty line" } };
1025
+ }
1026
+ if (trimmed.length > maxBytes) {
1027
+ return {
1028
+ ok: false,
1029
+ error: { code: "OVERSIZED", message: `message of ${trimmed.length} bytes exceeds the ${maxBytes} byte limit`, sample: trimmed.slice(0, 200) }
1030
+ };
1031
+ }
1032
+ let value;
1033
+ try {
1034
+ value = JSON.parse(trimmed);
1035
+ } catch (error) {
1036
+ return {
1037
+ ok: false,
1038
+ error: { code: "INVALID_JSON", message: error instanceof Error ? error.message : "invalid JSON", sample: trimmed.slice(0, 200) }
1039
+ };
1040
+ }
1041
+ return validateEnvelope(value, trimmed);
1042
+ }
1043
+ function encodeMessage(envelope2, maxBytes = MAX_MESSAGE_BYTES) {
1044
+ const line = JSON.stringify(envelope2);
1045
+ if (line.length > maxBytes) {
1046
+ throw new Error(`outbound protocol message of ${line.length} bytes exceeds the ${maxBytes} byte limit`);
1047
+ }
1048
+ return line;
1049
+ }
1050
+ var ProtocolDecoder = class {
1051
+ buffer = "";
1052
+ maxBytes;
1053
+ constructor(maxBytes = MAX_MESSAGE_BYTES) {
1054
+ this.maxBytes = maxBytes;
1055
+ }
1056
+ push(chunk) {
1057
+ this.buffer += chunk;
1058
+ const results = [];
1059
+ let index = this.buffer.indexOf("\n");
1060
+ while (index >= 0) {
1061
+ const line = this.buffer.slice(0, index);
1062
+ this.buffer = this.buffer.slice(index + 1);
1063
+ if (line.trim().length > 0) results.push(decodeMessage(line, this.maxBytes));
1064
+ index = this.buffer.indexOf("\n");
1065
+ }
1066
+ if (this.buffer.length > this.maxBytes) {
1067
+ results.push({
1068
+ ok: false,
1069
+ error: { code: "OVERSIZED", message: `line exceeded ${this.maxBytes} bytes without a newline`, sample: this.buffer.slice(0, 200) }
1070
+ });
1071
+ this.buffer = "";
1072
+ }
1073
+ return results;
1074
+ }
1075
+ /** Flush any trailing line when the process exits. */
1076
+ flush() {
1077
+ if (this.buffer.trim().length === 0) {
1078
+ this.buffer = "";
1079
+ return [];
1080
+ }
1081
+ const result = decodeMessage(this.buffer, this.maxBytes);
1082
+ this.buffer = "";
1083
+ return [result];
1084
+ }
1085
+ };
1086
+
1087
+ // src/agent-protocol/handshake.ts
1088
+ function parseHello(envelopeValue) {
1089
+ if (envelopeValue.type !== "HELLO") {
1090
+ return { ok: false, reason: `expected HELLO, received ${envelopeValue.type}` };
1091
+ }
1092
+ const payload = envelopeValue.payload;
1093
+ if (payload === null || typeof payload !== "object") {
1094
+ return { ok: false, reason: "HELLO payload must be an object" };
1095
+ }
1096
+ const p = payload;
1097
+ if (typeof p.name !== "string" || p.name.length === 0) {
1098
+ return { ok: false, reason: "HELLO payload is missing 'name'" };
1099
+ }
1100
+ return {
1101
+ ok: true,
1102
+ descriptor: {
1103
+ protocol: envelopeValue.protocol,
1104
+ name: p.name,
1105
+ ...typeof p.version === "string" ? { version: p.version } : {},
1106
+ capabilities: Array.isArray(p.capabilities) ? p.capabilities.filter((c) => typeof c === "string") : [],
1107
+ supports: Array.isArray(p.supports) ? p.supports.filter((s) => typeof s === "string") : []
1108
+ }
1109
+ };
1110
+ }
1111
+ function buildHelloAck(descriptor) {
1112
+ return envelope("HELLO_ACK", {
1113
+ ok: true,
1114
+ protocol: PROTOCOL_V2,
1115
+ agent: descriptor.name,
1116
+ ...descriptor.version !== void 0 ? { agentVersion: descriptor.version } : {},
1117
+ supportedProtocols: [PROTOCOL_V2]
1118
+ }, { agentId: descriptor.name });
1119
+ }
1120
+
1121
+ // src/agent-protocol/session.ts
1122
+ var ProtocolSession = class {
1123
+ decoder;
1124
+ ids;
1125
+ responder;
1126
+ violations = [];
1127
+ questions = [];
1128
+ artifacts = [];
1129
+ reports = [];
1130
+ descriptor;
1131
+ completionSummary;
1132
+ errorMessage;
1133
+ constructor(ids, options = {}) {
1134
+ this.ids = ids;
1135
+ this.responder = options.responder;
1136
+ this.decoder = new ProtocolDecoder(options.maxBytes);
1137
+ }
1138
+ /** The v2 messages OmniNode sends to open a task (written to the agent's stdin). */
1139
+ taskMessages(objective, role, context, instruction) {
1140
+ const ids = this.ids;
1141
+ const messages = [envelope("TASK", { taskId: ids.taskId, objective }, ids)];
1142
+ if (role !== void 0) messages.push(envelope("CONTEXT", { role }, ids));
1143
+ if (context !== void 0 && context.length > 0) messages.push(envelope("CONTEXT", { body: context }, ids));
1144
+ if (instruction !== void 0 && instruction.length > 0) messages.push(envelope("INSTRUCTION", { text: instruction }, ids));
1145
+ return messages.map((m) => encodeMessage(m));
1146
+ }
1147
+ helloAck(descriptor) {
1148
+ return encodeMessage(buildHelloAck(descriptor));
1149
+ }
1150
+ /** Consumes decoded stdout, answering questions as they arrive. */
1151
+ async handle(decoded) {
1152
+ if (!decoded.ok) {
1153
+ this.violations.push(decoded.error);
1154
+ return;
1155
+ }
1156
+ const message = decoded.envelope;
1157
+ if (message.protocol === PROTOCOL_V2) this.sawV2 = true;
1158
+ if (message.type === "HELLO") {
1159
+ const hello = parseHello(message);
1160
+ if (hello.ok) this.descriptor = hello.descriptor;
1161
+ else this.violations.push({ code: "INVALID_PAYLOAD", message: `bad HELLO: ${hello.reason}` });
1162
+ return;
1163
+ }
1164
+ if (message.type === "QUESTION") {
1165
+ await this.answerQuestion(message);
1166
+ return;
1167
+ }
1168
+ if (message.type === "REPORT") {
1169
+ this.reports.push(message.payload);
1170
+ return;
1171
+ }
1172
+ if (message.type === "ARTIFACT") {
1173
+ this.artifacts.push(message.payload);
1174
+ return;
1175
+ }
1176
+ if (message.type === "COMPLETION") {
1177
+ const payload = message.payload;
1178
+ this.completionSummary = typeof payload?.summary === "string" ? payload.summary : "";
1179
+ return;
1180
+ }
1181
+ if (message.type === "ERROR") {
1182
+ const payload = message.payload;
1183
+ const detail = typeof payload?.message === "string" ? payload.message : "agent reported an error";
1184
+ const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1185
+ this.errorMessage = `Agent error${code}: ${detail}`;
1186
+ return;
1187
+ }
1188
+ if (message.type === "TASK_ACCEPTED" || message.type === "TASK_STARTED" || message.type === "STATUS") {
1189
+ return;
1190
+ }
1191
+ this.violations.push({
1192
+ code: "UNKNOWN_TYPE",
1193
+ message: `unexpected message type ${message.type} from agent`
1194
+ });
1195
+ }
1196
+ async answerQuestion(message) {
1197
+ const payload = message.payload ?? {};
1198
+ const questionId = typeof payload.questionId === "string" ? payload.questionId : message.messageId;
1199
+ const question = typeof payload.question === "string" ? payload.question : "";
1200
+ const expected = typeof payload.expected === "string" ? payload.expected : "text";
1201
+ let answer;
1202
+ if (this.responder) {
1203
+ try {
1204
+ answer = await this.responder({ questionId, question, expected });
1205
+ } catch (error) {
1206
+ this.violations.push({
1207
+ code: "INVALID_PAYLOAD",
1208
+ message: `question responder failed: ${error instanceof Error ? error.message : String(error)}`
1209
+ });
1210
+ }
1211
+ }
1212
+ this.questions.push({ questionId, question, ...answer !== void 0 ? { answer } : {} });
1213
+ this.pendingAnswers.push(
1214
+ encodeMessage(
1215
+ envelope("RESPONSE", { questionId, ...answer !== void 0 ? { body: answer } : {} }, {
1216
+ ...this.ids,
1217
+ requestId: message.requestId ?? this.ids.requestId
1218
+ })
1219
+ )
1220
+ );
1221
+ }
1222
+ pendingAnswers = [];
1223
+ /** True once any inbound message carried the v2 protocol field. */
1224
+ sawV2 = false;
1225
+ };
1226
+
1227
+ // src/agents/cwd.ts
1228
+ import { statSync } from "fs";
1229
+ import { isAbsolute, relative, resolve } from "path";
1230
+ function validateWorkingDirectory(cwd, options = {}) {
1231
+ if (!cwd) return void 0;
1232
+ const normalized = isAbsolute(cwd) ? resolve(cwd) : resolve(process.cwd(), cwd);
1233
+ let stats;
1234
+ try {
1235
+ stats = statSync(normalized);
1236
+ } catch (error) {
1237
+ throw new AgentError(
1238
+ "AGENT_NOT_FOUND",
1239
+ `Working directory "${normalized}" does not exist or is not accessible.`,
1240
+ { cause: error, details: { cwd: normalized } }
1241
+ );
1242
+ }
1243
+ if (!stats.isDirectory()) {
1244
+ throw new AgentError("AGENT_NOT_FOUND", `Working directory "${normalized}" is not a directory.`, {
1245
+ details: { cwd: normalized }
1246
+ });
1247
+ }
1248
+ try {
1249
+ statSync(resolve(normalized, "."));
1250
+ } catch (error) {
1251
+ throw new AgentError(
1252
+ "AGENT_NOT_FOUND",
1253
+ `Working directory "${normalized}" is not accessible.`,
1254
+ { cause: error }
1255
+ );
1256
+ }
1257
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
1258
+ const rel = relative(projectRoot, normalized);
1259
+ const outsideProject = rel.startsWith("..") || isAbsolute(rel);
1260
+ if (outsideProject && options.allowExternal !== true) {
1261
+ throw new AgentError(
1262
+ "AGENT_NOT_FOUND",
1263
+ `Working directory "${normalized}" is outside the project root "${projectRoot}". Set allow_external_cwd: true on the agent to permit this.`,
1264
+ { details: { cwd: normalized, projectRoot } }
1265
+ );
1266
+ }
1267
+ return { cwd: normalized, outsideProject };
1268
+ }
1269
+
1270
+ // src/agents/env.ts
1271
+ var BASELINE_KEYS = ["PATH", "HOME"];
1272
+ function resolveEnvPolicy(config) {
1273
+ if (config.envPolicy !== void 0) return config.envPolicy;
1274
+ if (config.inheritEnv === false) return "explicit";
1275
+ return "inherit";
1276
+ }
902
1277
  function buildChildEnv(config) {
903
1278
  const configured = config.env ?? {};
904
- if (config.inheritEnv !== false) {
1279
+ const policy = resolveEnvPolicy(config);
1280
+ if (policy === "inherit") {
905
1281
  return { ...process.env, ...configured };
906
1282
  }
907
- return {
908
- PATH: process.env.PATH ?? "",
909
- HOME: process.env.HOME ?? "",
910
- ...configured
911
- };
1283
+ const base = {};
1284
+ for (const key of BASELINE_KEYS) {
1285
+ const value = process.env[key];
1286
+ if (value !== void 0) base[key] = value;
1287
+ }
1288
+ switch (policy) {
1289
+ case "explicit":
1290
+ return { ...base, ...configured };
1291
+ case "allowlist": {
1292
+ const allowed = { ...base };
1293
+ for (const key of config.envAllowlist ?? []) {
1294
+ const value = process.env[key];
1295
+ if (value !== void 0) allowed[key] = value;
1296
+ }
1297
+ return { ...allowed, ...configured };
1298
+ }
1299
+ case "denylist": {
1300
+ const denied = new Set((config.envDenylist ?? []).map((k) => k.toUpperCase()));
1301
+ const filtered = {};
1302
+ for (const [key, value] of Object.entries(process.env)) {
1303
+ if (denied.has(key.toUpperCase())) continue;
1304
+ filtered[key] = value;
1305
+ }
1306
+ return { ...filtered, ...configured };
1307
+ }
1308
+ }
912
1309
  }
1310
+
1311
+ // src/agents/process/index.ts
1312
+ var DEFAULT_TIMEOUT_MS2 = 6e5;
1313
+ var KILL_GRACE_MS = 5e3;
1314
+ var DEFAULT_MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
913
1315
  function composeTaskText(input) {
914
1316
  const sections = [`# Objective
915
1317
  ${input.objective}`];
@@ -930,18 +1332,19 @@ ${input.instruction}`);
930
1332
  return sections.join("\n\n");
931
1333
  }
932
1334
  function buildProtocolMessages(input, agentName) {
933
- const meta = {
1335
+ const session = new ProtocolSession({
1336
+ requestId: `req-${input.taskId}`,
934
1337
  taskId: input.taskId,
935
- agent: agentName,
936
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
937
- };
938
- const messages = [
939
- { type: "TASK", payload: { taskId: input.taskId, objective: input.objective } }
940
- ];
941
- if (input.role) messages.push({ type: "ROLE", payload: input.role });
942
- if (input.context) messages.push({ type: "CONTEXT", payload: { body: input.context } });
943
- if (input.instruction) messages.push({ type: "INSTRUCTION", payload: { text: input.instruction } });
944
- return messages.map((message) => JSON.stringify({ meta, message }));
1338
+ agentId: agentName,
1339
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1340
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1341
+ });
1342
+ return session.taskMessages(
1343
+ input.objective,
1344
+ input.role,
1345
+ input.context,
1346
+ input.instruction
1347
+ );
945
1348
  }
946
1349
  var ProcessAgent = class {
947
1350
  config;
@@ -970,40 +1373,73 @@ var ProcessAgent = class {
970
1373
  }
971
1374
  const mode = this.config.inputMode ?? "stdin";
972
1375
  const timeoutMs = this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
973
- return new Promise((resolve, reject) => {
1376
+ return new Promise((resolve2, reject) => {
1377
+ let cwd;
1378
+ try {
1379
+ cwd = validateWorkingDirectory(this.config.cwd, {
1380
+ allowExternal: this.config.allowExternalCwd
1381
+ })?.cwd;
1382
+ } catch (error) {
1383
+ reject(error);
1384
+ return;
1385
+ }
974
1386
  const child = spawn(
975
1387
  command,
976
1388
  [...this.config.args ?? [], ...mode === "arg" ? [composeTaskText(input)] : []],
977
1389
  {
978
- cwd: this.config.cwd,
1390
+ ...cwd !== void 0 ? { cwd } : {},
979
1391
  env: buildChildEnv(this.config),
980
- stdio: ["pipe", "pipe", "pipe"]
1392
+ stdio: ["pipe", "pipe", "pipe"],
1393
+ // Own process group so the whole tree can be terminated together
1394
+ // (roadmap §8: no orphaned children).
1395
+ ...process.platform === "win32" ? {} : { detached: true }
981
1396
  }
982
1397
  );
983
1398
  this.running.set(input.taskId, child);
1399
+ GLOBAL_RUNNING.add(child);
1400
+ installSignalForwarding();
984
1401
  let stdout = "";
985
1402
  let stderr = "";
986
1403
  let timedOut = false;
1404
+ let outputLimitExceeded = false;
987
1405
  let settled = false;
1406
+ const maxOutputBytes = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
988
1407
  const timer = setTimeout(() => {
989
1408
  timedOut = true;
990
- this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process.`);
991
- child.kill("SIGTERM");
992
- setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
1409
+ this.log.warn(`Task ${input.taskId} exceeded ${timeoutMs}ms \u2014 killing process tree.`);
1410
+ killTree(child, "SIGTERM");
1411
+ setTimeout(() => killTree(child, "SIGKILL"), KILL_GRACE_MS).unref();
993
1412
  }, timeoutMs);
994
1413
  const settle = (finish) => {
995
1414
  if (settled) return;
996
1415
  settled = true;
997
1416
  clearTimeout(timer);
998
1417
  this.running.delete(input.taskId);
1418
+ GLOBAL_RUNNING.delete(child);
999
1419
  finish();
1000
1420
  };
1001
- child.stdout?.on("data", (chunk) => {
1002
- stdout += chunk.toString();
1003
- });
1004
- child.stderr?.on("data", (chunk) => {
1005
- stderr += chunk.toString();
1006
- });
1421
+ const collect = (into, chunk) => {
1422
+ if (outputLimitExceeded) return;
1423
+ const text = sanitizeChunk(chunk);
1424
+ if (into === "stdout") {
1425
+ if (stdout.length + text.length > maxOutputBytes) {
1426
+ stdout = (stdout + text).slice(0, maxOutputBytes);
1427
+ outputLimitExceeded = true;
1428
+ this.log.warn(`Agent "${this.config.name}" exceeded the ${maxOutputBytes} byte output limit \u2014 killing it.`);
1429
+ killTree(child, "SIGKILL");
1430
+ return;
1431
+ }
1432
+ stdout += text;
1433
+ } else {
1434
+ if (stderr.length + text.length > maxOutputBytes) {
1435
+ stderr = (stderr + text).slice(0, maxOutputBytes);
1436
+ return;
1437
+ }
1438
+ stderr += text;
1439
+ }
1440
+ };
1441
+ child.stdout?.on("data", (chunk) => collect("stdout", chunk));
1442
+ child.stderr?.on("data", (chunk) => collect("stderr", chunk));
1007
1443
  child.stdin?.on("error", () => {
1008
1444
  });
1009
1445
  if (mode !== "arg") {
@@ -1057,7 +1493,7 @@ var ProcessAgent = class {
1057
1493
  const logs = stderr.trim().length > 0 ? stderr.split("\n") : void 0;
1058
1494
  if (mode === "protocol") {
1059
1495
  try {
1060
- resolve(
1496
+ resolve2(
1061
1497
  interpretProtocolRun({
1062
1498
  input,
1063
1499
  agentName: this.config.name,
@@ -1072,14 +1508,14 @@ var ProcessAgent = class {
1072
1508
  return;
1073
1509
  }
1074
1510
  const trimmed = stdout.trim();
1075
- resolve({
1511
+ resolve2({
1076
1512
  taskId: input.taskId,
1077
- status: code === 0 ? "completed" : "failed",
1513
+ status: code === 0 && !outputLimitExceeded ? "completed" : "failed",
1078
1514
  summary: trimmed.length > 0 ? firstLine(trimmed) : void 0,
1079
1515
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
1080
1516
  exitCode: code ?? void 0,
1081
1517
  logs,
1082
- error: code === 0 ? void 0 : `Process exited with code ${code ?? "unknown"}${signal ? ` (signal: ${signal})` : ""}`
1518
+ ...outputLimitExceeded ? { error: `Agent exceeded the ${maxOutputBytes} byte output limit and was terminated.` } : code === 0 ? {} : { error: describeExit(code, signal) }
1083
1519
  });
1084
1520
  });
1085
1521
  });
@@ -1088,67 +1524,117 @@ var ProcessAgent = class {
1088
1524
  /** Ask the agent to stop current work (SIGTERM). */
1089
1525
  async cancel(taskId) {
1090
1526
  const child = this.running.get(taskId);
1091
- if (child) child.kill("SIGTERM");
1527
+ if (child) killTree(child, "SIGTERM");
1092
1528
  }
1093
1529
  runningTaskIds() {
1094
1530
  return [...this.running.keys()];
1095
1531
  }
1096
1532
  };
1097
- function interpretProtocolRun({ input, agentName, stdout, exitCode, logs }) {
1098
- const reports = [];
1099
- let summary;
1100
- let error;
1101
- let sawProtocolMessage = false;
1102
- for (const line of stdout.split("\n")) {
1103
- const trimmed2 = line.trim();
1104
- if (trimmed2.length === 0) continue;
1105
- const message = parseAgentMessage(trimmed2);
1106
- if (!message) continue;
1107
- sawProtocolMessage = true;
1108
- if (message.type === "REPORT") {
1109
- const report = normalizeReport(message.payload, input.taskId, agentName, reports.length);
1110
- if (report) reports.push(report);
1111
- } else if (message.type === "COMPLETION") {
1112
- const payload = message.payload;
1113
- if (typeof payload?.summary === "string") summary = payload.summary;
1114
- } else if (message.type === "ERROR") {
1115
- const payload = message.payload;
1116
- const detail = typeof payload?.message === "string" ? payload.message : "unknown error";
1117
- const code = typeof payload?.code === "string" ? ` [${payload.code}]` : "";
1118
- error = `Agent error${code}: ${detail}`;
1119
- }
1533
+ function interpretProtocolRun({
1534
+ input,
1535
+ agentName,
1536
+ stdout,
1537
+ exitCode,
1538
+ logs,
1539
+ responder
1540
+ }) {
1541
+ const session = new ProtocolSession(
1542
+ {
1543
+ requestId: `req-${input.taskId}`,
1544
+ taskId: input.taskId,
1545
+ agentId: agentName,
1546
+ ...input.pipelineId !== void 0 ? { pipelineId: input.pipelineId } : {},
1547
+ ...input.executionId !== void 0 ? { executionId: input.executionId } : {}
1548
+ },
1549
+ { ...responder !== void 0 ? { responder } : {} }
1550
+ );
1551
+ for (const result of session.decoder.push(stdout)) {
1552
+ session.handle(result);
1120
1553
  }
1121
- if (!sawProtocolMessage && exitCode === 0) {
1122
- throw new ProtocolError(
1123
- `Agent "${agentName}" produced no protocol messages on stdout (input mode "protocol").`,
1124
- { details: { taskId: input.taskId, stdout: stdout.slice(0, 2e3) } }
1125
- );
1554
+ for (const result of session.decoder.flush()) {
1555
+ session.handle(result);
1126
1556
  }
1557
+ const reports = session.reports.flatMap((payload) => Array.isArray(payload) ? payload : [payload]).map((payload, index) => normalizeReport(payload, input.taskId, agentName, index)).filter((report) => report !== void 0);
1127
1558
  const trimmed = stdout.trim();
1559
+ const protocolBlock = {
1560
+ protocol: session.descriptor?.protocol ?? PROTOCOL_V2,
1561
+ legacy: !session.sawV2 && session.descriptor === void 0,
1562
+ ...session.descriptor !== void 0 ? { descriptor: session.descriptor } : {},
1563
+ questions: session.questions,
1564
+ violations: session.violations.map((v) => `${v.code}: ${v.message}`)
1565
+ };
1566
+ if (session.errorMessage !== void 0) {
1567
+ return {
1568
+ taskId: input.taskId,
1569
+ status: "failed",
1570
+ error: session.errorMessage,
1571
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1572
+ exitCode: exitCode ?? void 0,
1573
+ logs,
1574
+ protocol: protocolBlock,
1575
+ ...reports.length > 0 ? { reports } : {}
1576
+ };
1577
+ }
1578
+ if (session.descriptor === void 0 && session.violations.length > 0 && session.reports.length === 0 && session.completionSummary === void 0) {
1579
+ return {
1580
+ taskId: input.taskId,
1581
+ status: "failed",
1582
+ error: `agent produced no valid protocol messages: ${protocolBlock.violations.join("; ")}`,
1583
+ rawOutput: trimmed.length > 0 ? trimmed : void 0,
1584
+ exitCode: exitCode ?? void 0,
1585
+ logs,
1586
+ protocol: protocolBlock
1587
+ };
1588
+ }
1589
+ const completed = exitCode === 0 && session.completionSummary !== void 0;
1128
1590
  return {
1129
1591
  taskId: input.taskId,
1130
- status: error !== void 0 || exitCode !== 0 ? "failed" : "completed",
1131
- summary: summary ?? reports[0]?.summary,
1592
+ status: completed || exitCode === 0 ? completed ? "completed" : "completed" : "failed",
1593
+ summary: session.completionSummary ?? reports[0]?.summary,
1132
1594
  rawOutput: trimmed.length > 0 ? trimmed : void 0,
1133
1595
  exitCode: exitCode ?? void 0,
1134
1596
  logs,
1135
- reports: reports.length > 0 ? reports : void 0,
1136
- error: error ?? (exitCode !== 0 ? `Process exited with code ${exitCode ?? "unknown"}` : void 0)
1597
+ protocol: protocolBlock,
1598
+ ...reports.length > 0 ? { reports } : {},
1599
+ ...exitCode !== 0 ? { error: `Process exited with code ${exitCode ?? "unknown"}` } : {}
1137
1600
  };
1138
1601
  }
1139
- function parseAgentMessage(line) {
1140
- try {
1141
- const parsed = JSON.parse(line);
1142
- if (typeof parsed.type === "string") {
1143
- return { type: parsed.type, payload: parsed.payload };
1144
- }
1145
- const inner = parsed.message;
1146
- if (inner && typeof inner.type === "string") {
1147
- return { type: inner.type, payload: inner.payload };
1602
+ var GLOBAL_RUNNING = /* @__PURE__ */ new Set();
1603
+ var signalForwardingInstalled = false;
1604
+ function installSignalForwarding() {
1605
+ if (signalForwardingInstalled) return;
1606
+ signalForwardingInstalled = true;
1607
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1608
+ process.on(signal, () => {
1609
+ for (const child of GLOBAL_RUNNING) {
1610
+ killTree(child, "SIGTERM");
1611
+ }
1612
+ });
1613
+ }
1614
+ }
1615
+ function killTree(child, signal) {
1616
+ if (child.pid === void 0) return;
1617
+ if (process.platform !== "win32") {
1618
+ try {
1619
+ process.kill(-child.pid, signal);
1620
+ return;
1621
+ } catch {
1148
1622
  }
1623
+ }
1624
+ try {
1625
+ child.kill(signal);
1149
1626
  } catch {
1150
1627
  }
1151
- return void 0;
1628
+ }
1629
+ function describeExit(code, signal) {
1630
+ if (signal !== null) {
1631
+ return signal === "SIGKILL" ? "Process was killed (SIGKILL) \u2014 usually an external kill or OOM." : `Process terminated by signal ${signal}.`;
1632
+ }
1633
+ if (code === null) return "Process terminated without an exit code.";
1634
+ return `Process exited with code ${code}.`;
1635
+ }
1636
+ function sanitizeChunk(chunk) {
1637
+ return chunk.toString("utf8").replace(/\u0000/g, "");
1152
1638
  }
1153
1639
  function normalizeReport(payload, taskId, agentName, index) {
1154
1640
  if (payload === null || typeof payload !== "object") return void 0;
@@ -1169,6 +1655,40 @@ function firstLine(text, maxLength = 200) {
1169
1655
  return line.length > maxLength ? `${line.slice(0, maxLength)}\u2026` : line;
1170
1656
  }
1171
1657
 
1658
+ // src/agents/adapters.ts
1659
+ var AgentAdapterRegistry = class {
1660
+ adapters = /* @__PURE__ */ new Map();
1661
+ register(adapter) {
1662
+ this.adapters.set(adapter.integration, adapter);
1663
+ }
1664
+ get(integration) {
1665
+ return this.adapters.get(integration);
1666
+ }
1667
+ list() {
1668
+ return [...this.adapters.values()];
1669
+ }
1670
+ create(config) {
1671
+ const adapter = this.get(config.integration);
1672
+ if (!adapter) {
1673
+ throw new AgentError(
1674
+ "AGENT_NOT_FOUND",
1675
+ `No agent adapter registered for integration "${config.integration}". Registered: ${[...this.adapters.keys()].join(", ") || "(none)"}.`
1676
+ );
1677
+ }
1678
+ return adapter.create(config);
1679
+ }
1680
+ };
1681
+ var ProcessAgentAdapter = class {
1682
+ constructor(factory) {
1683
+ this.factory = factory;
1684
+ }
1685
+ factory;
1686
+ integration = "process";
1687
+ create(config) {
1688
+ return this.factory(config);
1689
+ }
1690
+ };
1691
+
1172
1692
  // src/agents/registry.ts
1173
1693
  var AgentRegistry = class {
1174
1694
  agents = /* @__PURE__ */ new Map();
@@ -1184,14 +1704,15 @@ var AgentRegistry = class {
1184
1704
  };
1185
1705
 
1186
1706
  // src/agents/factory.ts
1187
- function createAgent(config) {
1188
- if (config.integration === "process") {
1189
- return new ProcessAgent(config);
1190
- }
1707
+ var defaultAgentAdapters = new AgentAdapterRegistry();
1708
+ defaultAgentAdapters.register(new ProcessAgentAdapter((config) => new ProcessAgent(config)));
1709
+ function createAgent(config, adapters = defaultAgentAdapters) {
1710
+ const registered = adapters.get(config.integration);
1711
+ if (registered) return registered.create(config);
1191
1712
  if (config.integration === "native") {
1192
1713
  throw new OmniNodeError(
1193
1714
  "NOT_IMPLEMENTED",
1194
- `Native agent adapters are planned for a later phase. Use input over stdin/stdout (type "cli") for "${config.name}" for now.`
1715
+ `No native adapter is registered for "${config.name}". Register an IAgentAdapter (createAgent(config, adapters)) or use input over stdin/stdout (type "cli").`
1195
1716
  );
1196
1717
  }
1197
1718
  throw new OmniNodeError("NOT_IMPLEMENTED", `Agent integration "${config.integration}" has no adapter yet.`);
@@ -1234,7 +1755,7 @@ function registerAgentCommands(program) {
1234
1755
  "--input-mode <mode>",
1235
1756
  "How the task is delivered: stdin (prompt on stdin), arg (prompt as last argument) or protocol (\xA721 JSON-lines task protocol).",
1236
1757
  "stdin"
1237
- ).option(
1758
+ ).option("--env-policy <policy>", "Environment policy: inherit, allowlist, denylist or explicit.").option("--allow-env <name>", "Variable to forward under the allowlist policy (repeatable).", collectArgs).option("--deny-env <name>", "Variable to remove under the denylist policy (repeatable).", collectArgs).option("--max-output-bytes <n>", "Per-stream output budget in bytes (default 5 MiB).", (value) => Number(value)).option("--allow-external-cwd", "Allow a working directory outside the project root.").option(
1238
1759
  "--timeout-ms <ms>",
1239
1760
  "Per-task timeout in milliseconds.",
1240
1761
  (value) => Number(value)
@@ -1249,7 +1770,12 @@ async function addAgent(options) {
1249
1770
  ...options.arg !== void 0 && options.arg.length > 0 ? { args: options.arg } : {},
1250
1771
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1251
1772
  input_mode: options.inputMode,
1252
- ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {}
1773
+ ...options.timeoutMs !== void 0 ? { timeout_ms: options.timeoutMs } : {},
1774
+ ...options.envPolicy !== void 0 ? { env_policy: options.envPolicy } : {},
1775
+ ...options.allowEnv !== void 0 && options.allowEnv.length > 0 ? { env_allowlist: options.allowEnv } : {},
1776
+ ...options.denyEnv !== void 0 && options.denyEnv.length > 0 ? { env_denylist: options.denyEnv } : {},
1777
+ ...options.maxOutputBytes !== void 0 ? { max_output_bytes: options.maxOutputBytes } : {},
1778
+ ...options.allowExternalCwd !== void 0 ? { allow_external_cwd: options.allowExternalCwd } : {}
1253
1779
  });
1254
1780
  if (!candidate.success) {
1255
1781
  const issues = candidate.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
@@ -1653,7 +2179,7 @@ function classifyThrown(error) {
1653
2179
  return "failed";
1654
2180
  }
1655
2181
  function sleep(ms) {
1656
- return new Promise((resolve) => setTimeout(resolve, ms));
2182
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1657
2183
  }
1658
2184
 
1659
2185
  // src/audit/index.ts