@darkhunt-security/endpoint-codex 0.9.7 → 0.9.9

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "darkhunt-guard",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "hooks": "./hooks/hooks.json",
5
5
  "description": "Darkhunt endpoint plugin for Codex CLI \u2014 full session trace capture and PreToolUse guardrails.",
6
6
  "author": {
@@ -18135,6 +18135,11 @@ function readSecure(path) {
18135
18135
  throw new ConfigError(`${path} is mode ${mode.toString(8)}; must be 0600`);
18136
18136
  return readFileSync(path, "utf8");
18137
18137
  }
18138
+ function isBlank(path) {
18139
+ if (statSync(path).size === 0)
18140
+ return true;
18141
+ return readFileSync(path, "utf8").trim() === "";
18142
+ }
18138
18143
  function readJson(path) {
18139
18144
  try {
18140
18145
  return JSON.parse(readSecure(path));
@@ -18159,6 +18164,9 @@ function readConfigFile(vendor) {
18159
18164
  if (!existsSync(configPath())) {
18160
18165
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
18161
18166
  }
18167
+ if (isBlank(configPath())) {
18168
+ throw new ConfigMissingError(`${configPath()} is empty \u2014 run init.mjs to create it`);
18169
+ }
18162
18170
  const file = readJson(configPath());
18163
18171
  if (!file) {
18164
18172
  throw new ConfigError(`cannot read ${configPath()} \u2014 it exists but did not parse`);
@@ -18295,6 +18303,10 @@ function summarize(output) {
18295
18303
  return text;
18296
18304
  return `${text.slice(0, TOOL_SUMMARY_LIMIT)}\u2026 [truncated, ${text.length} chars]`;
18297
18305
  }
18306
+ function attachmentLabel(record) {
18307
+ const size = record.bytes ? `, ${Math.round(record.bytes / 1024)} KB` : "";
18308
+ return `[${record.mediaType ?? "attachment"}${size}]`;
18309
+ }
18298
18310
  var RESUMED_USER_TEXT_LIMIT = 4e3;
18299
18311
  var SessionEmitter = class {
18300
18312
  client;
@@ -18316,8 +18328,12 @@ var SessionEmitter = class {
18316
18328
  /** True while consecutive user records still belong to the same turn. */
18317
18329
  userTurnOpen = false;
18318
18330
  pendingAttachments = [];
18331
+ /** Attachments seen before the trace could be opened knowing the prompt. */
18332
+ deferred = [];
18319
18333
  pendingUsage;
18320
18334
  outputText = [];
18335
+ /** The last thing the assistant said in this pass — the trace's result. */
18336
+ lastAssistantText;
18321
18337
  count = 0;
18322
18338
  constructor(client, options) {
18323
18339
  this.client = client;
@@ -18368,6 +18384,28 @@ var SessionEmitter = class {
18368
18384
  return;
18369
18385
  }
18370
18386
  this.count++;
18387
+ if (record.kind === "attachment")
18388
+ this.pendingAttachments.push(attachmentLabel(record));
18389
+ if (record.kind === "user_message")
18390
+ this.absorbUserTurn(record);
18391
+ if (!this.trace && record.kind === "attachment") {
18392
+ this.deferred.push(record);
18393
+ return;
18394
+ }
18395
+ this.flushDeferred();
18396
+ this.place(record);
18397
+ }
18398
+ /** Open the trace on the turn held back, then replay it. No-op once the trace exists. */
18399
+ flushDeferred() {
18400
+ if (this.deferred.length === 0)
18401
+ return;
18402
+ const held = this.deferred.splice(0);
18403
+ this.ensureTrace(held[0].ts);
18404
+ for (const record of held)
18405
+ this.place(record);
18406
+ }
18407
+ /** Route one record onto its owning trace and emit whatever it is worth. */
18408
+ place(record) {
18371
18409
  const root = this.ensureTrace(record.ts);
18372
18410
  const parentTrace = record.parentUuid ? this.owners.get(record.parentUuid) : void 0;
18373
18411
  let owner = parentTrace ?? root;
@@ -18377,28 +18415,21 @@ var SessionEmitter = class {
18377
18415
  sessionId: this.options.sessionId,
18378
18416
  handoffFrom: [root.handoffToken()],
18379
18417
  startTime: record.ts,
18418
+ // A subagent's first record is the task it was given, and it has already been
18419
+ // absorbed by the time this fires — so the child trace opens with its brief
18420
+ // rather than empty.
18421
+ ...this.lastUserText ? { input: this.lastUserText } : {},
18380
18422
  ...this.options.userId !== void 0 ? { userId: this.options.userId } : {}
18381
18423
  }));
18382
18424
  this.sidechains.push(owner);
18383
18425
  }
18384
18426
  this.owners.set(record.uuid, owner);
18385
18427
  switch (record.kind) {
18386
- case "user_message": {
18428
+ case "user_message":
18387
18429
  this.endGeneration();
18388
- const part = [this.pendingAttachments.join(" "), record.text].filter(Boolean).join(" ");
18389
- this.pendingAttachments = [];
18390
- if (this.userTurnOpen) {
18391
- this.lastUserText = [this.lastUserText, part].filter(Boolean).join(" ");
18392
- } else {
18393
- this.lastUserText = part;
18394
- this.userTurnOpen = true;
18395
- this.pendingToolResults = [];
18396
- }
18397
18430
  break;
18398
- }
18399
18431
  case "attachment": {
18400
- const label = `[${record.mediaType ?? "attachment"}${record.bytes ? `, ${Math.round(record.bytes / 1024)} KB` : ""}]`;
18401
- this.pendingAttachments.push(label);
18432
+ const label = attachmentLabel(record);
18402
18433
  this.seeded(["attachment", record.uuid], () => owner.span("attachment", {
18403
18434
  startTime: record.ts,
18404
18435
  metadata: {
@@ -18455,8 +18486,26 @@ var SessionEmitter = class {
18455
18486
  break;
18456
18487
  }
18457
18488
  }
18489
+ /**
18490
+ * Fold a user record into the turn in flight.
18491
+ *
18492
+ * Split out of the `user_message` case so it can run before the trace is opened —
18493
+ * see the call site in {@link ingest}.
18494
+ */
18495
+ absorbUserTurn(record) {
18496
+ const part = [this.pendingAttachments.join(" "), record.text].filter(Boolean).join(" ");
18497
+ this.pendingAttachments = [];
18498
+ if (this.userTurnOpen) {
18499
+ this.lastUserText = [this.lastUserText, part].filter(Boolean).join(" ");
18500
+ } else {
18501
+ this.lastUserText = part;
18502
+ this.userTurnOpen = true;
18503
+ this.pendingToolResults = [];
18504
+ }
18505
+ }
18458
18506
  /** Ends everything still open and flushes. Safe to call twice. */
18459
18507
  async finish() {
18508
+ this.flushDeferred();
18460
18509
  this.endGeneration();
18461
18510
  for (const span of this.toolSpans.values())
18462
18511
  span.end();
@@ -18467,7 +18516,10 @@ var SessionEmitter = class {
18467
18516
  if (this.trace) {
18468
18517
  this.trace.update({
18469
18518
  metadata: this.meta,
18470
- ...this.title !== void 0 ? { name: this.title } : {}
18519
+ ...this.title !== void 0 ? { name: this.title } : {},
18520
+ // The pass's result. Paired with the `input` set when the trace opened, this is
18521
+ // what stops the consumer inferring the trace's I/O from a descendant.
18522
+ ...this.lastAssistantText !== void 0 ? { output: this.lastAssistantText } : {}
18471
18523
  });
18472
18524
  this.trace.end();
18473
18525
  this.trace = void 0;
@@ -18523,6 +18575,10 @@ var SessionEmitter = class {
18523
18575
  name: this.title ?? this.defaultTraceName(),
18524
18576
  sessionId: this.grouping ?? this.options.sessionId,
18525
18577
  startTime: ts,
18578
+ // What the agent was asked. `TraceArgs.input` is construction-only — the SDK's
18579
+ // `update()` carries output alone — which is why `ingest` absorbs the user turn
18580
+ // before it calls this.
18581
+ ...this.lastUserText ? { input: this.lastUserText } : {},
18526
18582
  tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
18527
18583
  metadata: {
18528
18584
  ...this.meta,
@@ -18588,6 +18644,8 @@ var SessionEmitter = class {
18588
18644
  if (!this.generation)
18589
18645
  return;
18590
18646
  const output = this.outputText.join("\n");
18647
+ if (output)
18648
+ this.lastAssistantText = output;
18591
18649
  this.generation.end({
18592
18650
  ...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
18593
18651
  ...this.model !== void 0 ? { model: this.model } : {},
@@ -18135,6 +18135,11 @@ function readSecure(path) {
18135
18135
  throw new ConfigError(`${path} is mode ${mode.toString(8)}; must be 0600`);
18136
18136
  return readFileSync(path, "utf8");
18137
18137
  }
18138
+ function isBlank(path) {
18139
+ if (statSync(path).size === 0)
18140
+ return true;
18141
+ return readFileSync(path, "utf8").trim() === "";
18142
+ }
18138
18143
  function readJson(path) {
18139
18144
  try {
18140
18145
  return JSON.parse(readSecure(path));
@@ -18159,6 +18164,9 @@ function readConfigFile(vendor) {
18159
18164
  if (!existsSync(configPath())) {
18160
18165
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
18161
18166
  }
18167
+ if (isBlank(configPath())) {
18168
+ throw new ConfigMissingError(`${configPath()} is empty \u2014 run init.mjs to create it`);
18169
+ }
18162
18170
  const file = readJson(configPath());
18163
18171
  if (!file) {
18164
18172
  throw new ConfigError(`cannot read ${configPath()} \u2014 it exists but did not parse`);
@@ -18295,6 +18303,10 @@ function summarize(output) {
18295
18303
  return text;
18296
18304
  return `${text.slice(0, TOOL_SUMMARY_LIMIT)}\u2026 [truncated, ${text.length} chars]`;
18297
18305
  }
18306
+ function attachmentLabel(record) {
18307
+ const size = record.bytes ? `, ${Math.round(record.bytes / 1024)} KB` : "";
18308
+ return `[${record.mediaType ?? "attachment"}${size}]`;
18309
+ }
18298
18310
  var RESUMED_USER_TEXT_LIMIT = 4e3;
18299
18311
  var SessionEmitter = class {
18300
18312
  client;
@@ -18316,8 +18328,12 @@ var SessionEmitter = class {
18316
18328
  /** True while consecutive user records still belong to the same turn. */
18317
18329
  userTurnOpen = false;
18318
18330
  pendingAttachments = [];
18331
+ /** Attachments seen before the trace could be opened knowing the prompt. */
18332
+ deferred = [];
18319
18333
  pendingUsage;
18320
18334
  outputText = [];
18335
+ /** The last thing the assistant said in this pass — the trace's result. */
18336
+ lastAssistantText;
18321
18337
  count = 0;
18322
18338
  constructor(client, options) {
18323
18339
  this.client = client;
@@ -18368,6 +18384,28 @@ var SessionEmitter = class {
18368
18384
  return;
18369
18385
  }
18370
18386
  this.count++;
18387
+ if (record.kind === "attachment")
18388
+ this.pendingAttachments.push(attachmentLabel(record));
18389
+ if (record.kind === "user_message")
18390
+ this.absorbUserTurn(record);
18391
+ if (!this.trace && record.kind === "attachment") {
18392
+ this.deferred.push(record);
18393
+ return;
18394
+ }
18395
+ this.flushDeferred();
18396
+ this.place(record);
18397
+ }
18398
+ /** Open the trace on the turn held back, then replay it. No-op once the trace exists. */
18399
+ flushDeferred() {
18400
+ if (this.deferred.length === 0)
18401
+ return;
18402
+ const held = this.deferred.splice(0);
18403
+ this.ensureTrace(held[0].ts);
18404
+ for (const record of held)
18405
+ this.place(record);
18406
+ }
18407
+ /** Route one record onto its owning trace and emit whatever it is worth. */
18408
+ place(record) {
18371
18409
  const root = this.ensureTrace(record.ts);
18372
18410
  const parentTrace = record.parentUuid ? this.owners.get(record.parentUuid) : void 0;
18373
18411
  let owner = parentTrace ?? root;
@@ -18377,28 +18415,21 @@ var SessionEmitter = class {
18377
18415
  sessionId: this.options.sessionId,
18378
18416
  handoffFrom: [root.handoffToken()],
18379
18417
  startTime: record.ts,
18418
+ // A subagent's first record is the task it was given, and it has already been
18419
+ // absorbed by the time this fires — so the child trace opens with its brief
18420
+ // rather than empty.
18421
+ ...this.lastUserText ? { input: this.lastUserText } : {},
18380
18422
  ...this.options.userId !== void 0 ? { userId: this.options.userId } : {}
18381
18423
  }));
18382
18424
  this.sidechains.push(owner);
18383
18425
  }
18384
18426
  this.owners.set(record.uuid, owner);
18385
18427
  switch (record.kind) {
18386
- case "user_message": {
18428
+ case "user_message":
18387
18429
  this.endGeneration();
18388
- const part = [this.pendingAttachments.join(" "), record.text].filter(Boolean).join(" ");
18389
- this.pendingAttachments = [];
18390
- if (this.userTurnOpen) {
18391
- this.lastUserText = [this.lastUserText, part].filter(Boolean).join(" ");
18392
- } else {
18393
- this.lastUserText = part;
18394
- this.userTurnOpen = true;
18395
- this.pendingToolResults = [];
18396
- }
18397
18430
  break;
18398
- }
18399
18431
  case "attachment": {
18400
- const label = `[${record.mediaType ?? "attachment"}${record.bytes ? `, ${Math.round(record.bytes / 1024)} KB` : ""}]`;
18401
- this.pendingAttachments.push(label);
18432
+ const label = attachmentLabel(record);
18402
18433
  this.seeded(["attachment", record.uuid], () => owner.span("attachment", {
18403
18434
  startTime: record.ts,
18404
18435
  metadata: {
@@ -18455,8 +18486,26 @@ var SessionEmitter = class {
18455
18486
  break;
18456
18487
  }
18457
18488
  }
18489
+ /**
18490
+ * Fold a user record into the turn in flight.
18491
+ *
18492
+ * Split out of the `user_message` case so it can run before the trace is opened —
18493
+ * see the call site in {@link ingest}.
18494
+ */
18495
+ absorbUserTurn(record) {
18496
+ const part = [this.pendingAttachments.join(" "), record.text].filter(Boolean).join(" ");
18497
+ this.pendingAttachments = [];
18498
+ if (this.userTurnOpen) {
18499
+ this.lastUserText = [this.lastUserText, part].filter(Boolean).join(" ");
18500
+ } else {
18501
+ this.lastUserText = part;
18502
+ this.userTurnOpen = true;
18503
+ this.pendingToolResults = [];
18504
+ }
18505
+ }
18458
18506
  /** Ends everything still open and flushes. Safe to call twice. */
18459
18507
  async finish() {
18508
+ this.flushDeferred();
18460
18509
  this.endGeneration();
18461
18510
  for (const span of this.toolSpans.values())
18462
18511
  span.end();
@@ -18467,7 +18516,10 @@ var SessionEmitter = class {
18467
18516
  if (this.trace) {
18468
18517
  this.trace.update({
18469
18518
  metadata: this.meta,
18470
- ...this.title !== void 0 ? { name: this.title } : {}
18519
+ ...this.title !== void 0 ? { name: this.title } : {},
18520
+ // The pass's result. Paired with the `input` set when the trace opened, this is
18521
+ // what stops the consumer inferring the trace's I/O from a descendant.
18522
+ ...this.lastAssistantText !== void 0 ? { output: this.lastAssistantText } : {}
18471
18523
  });
18472
18524
  this.trace.end();
18473
18525
  this.trace = void 0;
@@ -18523,6 +18575,10 @@ var SessionEmitter = class {
18523
18575
  name: this.title ?? this.defaultTraceName(),
18524
18576
  sessionId: this.grouping ?? this.options.sessionId,
18525
18577
  startTime: ts,
18578
+ // What the agent was asked. `TraceArgs.input` is construction-only — the SDK's
18579
+ // `update()` carries output alone — which is why `ingest` absorbs the user turn
18580
+ // before it calls this.
18581
+ ...this.lastUserText ? { input: this.lastUserText } : {},
18526
18582
  tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
18527
18583
  metadata: {
18528
18584
  ...this.meta,
@@ -18588,6 +18644,8 @@ var SessionEmitter = class {
18588
18644
  if (!this.generation)
18589
18645
  return;
18590
18646
  const output = this.outputText.join("\n");
18647
+ if (output)
18648
+ this.lastAssistantText = output;
18591
18649
  this.generation.end({
18592
18650
  ...output ? { outputMessages: [{ role: "assistant", content: output }] } : {},
18593
18651
  ...this.model !== void 0 ? { model: this.model } : {},
@@ -196,6 +196,11 @@ function readSecure(path) {
196
196
  throw new ConfigError(`${path} is mode ${mode.toString(8)}; must be 0600`);
197
197
  return readFileSync(path, "utf8");
198
198
  }
199
+ function isBlank(path) {
200
+ if (statSync(path).size === 0)
201
+ return true;
202
+ return readFileSync(path, "utf8").trim() === "";
203
+ }
199
204
  function readJson(path) {
200
205
  try {
201
206
  return JSON.parse(readSecure(path));
@@ -234,6 +239,9 @@ function readConfigFile(vendor) {
234
239
  if (!existsSync(configPath())) {
235
240
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
236
241
  }
242
+ if (isBlank(configPath())) {
243
+ throw new ConfigMissingError(`${configPath()} is empty \u2014 run init.mjs to create it`);
244
+ }
237
245
  const file = readJson(configPath());
238
246
  if (!file) {
239
247
  throw new ConfigError(`cannot read ${configPath()} \u2014 it exists but did not parse`);
@@ -42,6 +42,11 @@ function readSecure(path) {
42
42
  throw new ConfigError(`${path} is mode ${mode.toString(8)}; must be 0600`);
43
43
  return readFileSync(path, "utf8");
44
44
  }
45
+ function isBlank(path) {
46
+ if (statSync(path).size === 0)
47
+ return true;
48
+ return readFileSync(path, "utf8").trim() === "";
49
+ }
45
50
  function readJson(path) {
46
51
  try {
47
52
  return JSON.parse(readSecure(path));
@@ -80,6 +85,9 @@ function readConfigFile(vendor) {
80
85
  if (!existsSync(configPath())) {
81
86
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
82
87
  }
88
+ if (isBlank(configPath())) {
89
+ throw new ConfigMissingError(`${configPath()} is empty \u2014 run init.mjs to create it`);
90
+ }
83
91
  const file = readJson(configPath());
84
92
  if (!file) {
85
93
  throw new ConfigError(`cannot read ${configPath()} \u2014 it exists but did not parse`);
@@ -62,6 +62,11 @@ function readSecure(path) {
62
62
  throw new ConfigError(`${path} is mode ${mode.toString(8)}; must be 0600`);
63
63
  return readFileSync(path, "utf8");
64
64
  }
65
+ function isBlank(path) {
66
+ if (statSync(path).size === 0)
67
+ return true;
68
+ return readFileSync(path, "utf8").trim() === "";
69
+ }
65
70
  function readJson(path) {
66
71
  try {
67
72
  return JSON.parse(readSecure(path));
@@ -86,6 +91,9 @@ function readConfigFile(vendor) {
86
91
  if (!existsSync(configPath())) {
87
92
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
88
93
  }
94
+ if (isBlank(configPath())) {
95
+ throw new ConfigMissingError(`${configPath()} is empty \u2014 run init.mjs to create it`);
96
+ }
89
97
  const file = readJson(configPath());
90
98
  if (!file) {
91
99
  throw new ConfigError(`cannot read ${configPath()} \u2014 it exists but did not parse`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkhunt-security/endpoint-codex",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "type": "module",
5
5
  "description": "Darkhunt endpoint adapter for Codex CLI: hook codec, rollout transcript mapper, plugin manifest.",
6
6
  "bin": {