@darkhunt-security/endpoint-codex 0.9.15 → 0.9.17

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.15",
3
+ "version": "0.9.17",
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": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Import Codex CLI sessions that happened before this machine was connected
3
- argument-hint: '[--days=N, transcript paths, or nothing for everything on disk]'
3
+ argument-hint: '[--days=N, --reset, transcript paths, or nothing for everything on disk]'
4
4
  allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/backfill.mjs:*), Bash(${CLAUDE_PLUGIN_ROOT}/dist/bin/status.mjs:*)
5
5
  ---
6
6
 
@@ -64,6 +64,60 @@ cannot be taken back and looks exactly like a correct one afterwards.
64
64
  a failed pass holds every checkpoint, so the identical command re-ships the same
65
65
  byte range once the cause is fixed.
66
66
 
67
+ ## When it ships nothing
68
+
69
+ `0 record(s) from 0 transcript(s)` with exit 0 has two very different causes, and they
70
+ read identically:
71
+
72
+ - **Already imported.** The normal case. Re-running a backfill is a no-op by design.
73
+ - **Re-enrolled, and the history went to the old application.** A checkpoint records how
74
+ far a transcript has been shipped — never _where_ it was shipped to. After pointing
75
+ this machine at a different tenant, workspace or application, every transcript is
76
+ still marked done, so a backfill correctly finds nothing to send and the new
77
+ application stays empty.
78
+
79
+ The status block above tells you which one you are in: if it names an application the
80
+ sessions were never shipped to, it is the second.
81
+
82
+ `--reset` is the lever for that case. It forgets the offsets for the transcripts you
83
+ selected — the same selection `--days` and paths make — and ships them again from the
84
+ start:
85
+
86
+ ```
87
+ ${CLAUDE_PLUGIN_ROOT}/dist/bin/backfill.mjs --reset --days=7
88
+ ```
89
+
90
+ Before running it:
91
+
92
+ - **Confirm the destination again.** This is the full send that the first backfill was
93
+ not, and it is one-way.
94
+ - **Say how big it is.** `--dry-run` (without `--reset`; the two are refused together)
95
+ maps the same selection and prints the record count. That count is what `--reset` will
96
+ actually ship — without `--reset` it is only the size of the window, not the size of
97
+ the shipment.
98
+ - **Do not offer it as a fix for a failed run.** A failed pass already holds its
99
+ checkpoints; re-running the plain command re-ships that byte range. `--reset` is for a
100
+ run that succeeded and sent nothing.
101
+
102
+ It writes the old offsets to a `.bak-<timestamp>-prereset` file beside the checkpoints
103
+ and prints the path. Records already in the old application stay there — this adds a
104
+ copy in the new one, it does not move anything.
105
+
106
+ ## When the machine has not opted in
107
+
108
+ **The retroactive import is off by default.** Most machines are in this state, so expect
109
+ it rather than treating it as a fault.
110
+
111
+ Exit **4**, with `BACKFILL DISABLED` on stderr, means the config has no
112
+ `"capture": { "backfill": true }`. Nothing was sent and no checkpoint was cleared — a
113
+ refused `--reset` has not moved the machine either, so there is nothing to undo and
114
+ nothing to retry.
115
+
116
+ Tell them plainly what it is: the import is opt-in, and this machine has not opted in.
117
+ Do not edit the config yourself to get around it — send them to `/darkhunt-guard:setup`.
118
+ `--dry-run` still works while it is off, so you can still show them exactly what enabling
119
+ it would send, which is the more useful thing to offer anyway.
120
+
67
121
  ## Also
68
122
 
69
123
  - Narrow it by passing paths — one session, or a date range — rather than adding flags
@@ -18160,6 +18160,23 @@ function settingsFor(file, vendor) {
18160
18160
  ...file.enforce ?? override.enforce ? { enforce: { ...file.enforce, ...override.enforce } } : {}
18161
18161
  };
18162
18162
  }
18163
+ function loadRuntimeSettings(vendor, options = {}) {
18164
+ const vendorFile = readConfigFile(vendor);
18165
+ const profile = resolveProfile(options.profile, vendorFile.profile);
18166
+ return {
18167
+ profile,
18168
+ scope: scopeKey(vendor, profile),
18169
+ enabled: vendorFile.enabled ?? true,
18170
+ capture: resolveCapture(vendorFile.capture),
18171
+ enforce: {
18172
+ mode: vendorFile.enforce?.mode ?? "off",
18173
+ failClosed: vendorFile.enforce?.failClosed ?? true
18174
+ }
18175
+ };
18176
+ }
18177
+ function resolveCapture(capture) {
18178
+ return { enabled: capture?.enabled ?? true, backfill: capture?.backfill ?? false };
18179
+ }
18163
18180
  function readConfigFile(vendor) {
18164
18181
  if (!existsSync(configPath())) {
18165
18182
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
@@ -18212,7 +18229,7 @@ function loadLocalConfig(vendor, options = {}) {
18212
18229
  applicationId,
18213
18230
  ...(creds.userId ?? vendorFile.userId) !== void 0 ? { userId: creds.userId ?? vendorFile.userId } : {},
18214
18231
  enabled: vendorFile.enabled ?? true,
18215
- capture: { enabled: vendorFile.capture?.enabled ?? true },
18232
+ capture: resolveCapture(vendorFile.capture),
18216
18233
  enforce: {
18217
18234
  mode: vendorFile.enforce?.mode ?? "off",
18218
18235
  failClosed: vendorFile.enforce?.failClosed ?? true
@@ -18754,8 +18771,9 @@ function installSeededIdGenerator(client) {
18754
18771
  }
18755
18772
 
18756
18773
  // packages/core/dist/forwarder/checkpoint.js
18757
- import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
18774
+ import { copyFileSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
18758
18775
  import { join as join6 } from "node:path";
18776
+ var SPOOL_KEY = "::spool::";
18759
18777
  function checkpointPath(scope) {
18760
18778
  return join6(CONFIG_DIR, `${scope}.checkpoints.json`);
18761
18779
  }
@@ -18800,6 +18818,26 @@ function resumeTurn(checkpoints, path, from) {
18800
18818
  return void 0;
18801
18819
  return checkpoints[path]?.turn;
18802
18820
  }
18821
+ function clearCheckpoints(scope, paths) {
18822
+ const checkpoints = loadCheckpoints(scope);
18823
+ const wanted = paths === void 0 ? void 0 : new Set(paths);
18824
+ const transcripts = Object.keys(checkpoints).filter((key) => key !== SPOOL_KEY);
18825
+ const cleared = transcripts.filter((key) => wanted === void 0 || wanted.has(key));
18826
+ const total = transcripts.length;
18827
+ if (cleared.length === 0)
18828
+ return { cleared, total };
18829
+ const backup = `${checkpointPath(scope)}.bak-${stamp(/* @__PURE__ */ new Date())}-prereset`;
18830
+ copyFileSync(checkpointPath(scope), backup);
18831
+ const kept = { ...checkpoints };
18832
+ for (const key of cleared)
18833
+ delete kept[key];
18834
+ saveCheckpoints(scope, kept);
18835
+ return { cleared, total, backup };
18836
+ }
18837
+ function stamp(at) {
18838
+ const pad = (n) => String(n).padStart(2, "0");
18839
+ return `${at.getFullYear()}${pad(at.getMonth() + 1)}${pad(at.getDate())}-${pad(at.getHours())}${pad(at.getMinutes())}${pad(at.getSeconds())}`;
18840
+ }
18803
18841
 
18804
18842
  // packages/core/dist/forwarder/tail.js
18805
18843
  import { closeSync, openSync, readSync, statSync as statSync3 } from "node:fs";
@@ -18872,12 +18910,14 @@ function acquireLock(scope) {
18872
18910
  }
18873
18911
  return () => rmSync(path, { force: true });
18874
18912
  }
18875
- function isAlive(pid) {
18913
+ function isAlive(pid, kill = (p, s) => {
18914
+ process.kill(p, s);
18915
+ }) {
18876
18916
  try {
18877
- process.kill(pid, 0);
18917
+ kill(pid, 0);
18878
18918
  return true;
18879
- } catch {
18880
- return false;
18919
+ } catch (err) {
18920
+ return err.code !== "ESRCH";
18881
18921
  }
18882
18922
  }
18883
18923
  var DEFAULT_LOCK_WAIT_MS = 60 * 1e3;
@@ -21692,7 +21732,6 @@ function toFloat(value, fallback) {
21692
21732
  }
21693
21733
 
21694
21734
  // packages/core/dist/forwarder/run.js
21695
- var SPOOL_KEY = "::spool::";
21696
21735
  var DARKHUNT_SOURCE_HINT = "darkhunt-telemetry";
21697
21736
  function drainSpool(mapper, checkpoints) {
21698
21737
  const path = spoolPath(mapper.vendor);
@@ -21982,7 +22021,10 @@ import { join as join9 } from "node:path";
21982
22021
  var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
21983
22022
 
21984
22023
  // packages/core/dist/cli/backfill.js
21985
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["dry-run"]);
22024
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Map([
22025
+ ["dry-run", "dryRun"],
22026
+ ["reset", "reset"]
22027
+ ]);
21986
22028
  var VALUE_FLAGS = /* @__PURE__ */ new Set(["profile", "credentials", "days", "batch"]);
21987
22029
  var DEFAULT_BATCH = 25;
21988
22030
  function usage(command) {
@@ -21990,6 +22032,8 @@ function usage(command) {
21990
22032
  `usage: ${command} [options] [paths...]`,
21991
22033
  "",
21992
22034
  " --dry-run map only: no network, no checkpoints",
22035
+ " --reset re-send: forget how far the selected transcripts have",
22036
+ " been shipped, then ship them from the start again",
21993
22037
  " --days=N only transcripts written to in the last N days",
21994
22038
  " --batch=N transcripts per pass (default " + String(DEFAULT_BATCH) + ")",
21995
22039
  " --profile=NAME endpoint profile to ship to",
@@ -21997,13 +22041,22 @@ function usage(command) {
21997
22041
  " -h, --help this message",
21998
22042
  "",
21999
22043
  " With no paths this ships EVERY transcript on disk. Narrow it with --days",
22000
- " or by naming transcripts, and confirm the destination with --dry-run first."
22044
+ " or by naming transcripts, and confirm the destination with --dry-run first.",
22045
+ "",
22046
+ " A checkpoint records how far a transcript has been shipped, never where it was",
22047
+ " shipped to. After re-enrolling against a different application, a plain backfill",
22048
+ " finds nothing to send and exits 0; --reset is what re-sends it.",
22049
+ "",
22050
+ " This command is refused, exit 4, when the config sets",
22051
+ ' "capture": { "backfill": false }',
22052
+ " which stops retroactive imports on this machine and leaves live capture running.",
22053
+ " --dry-run still works while it is off."
22001
22054
  ].join("\n");
22002
22055
  }
22003
22056
  function parseBackfillArgs(argv) {
22004
22057
  const paths = [];
22005
22058
  const values = /* @__PURE__ */ new Map();
22006
- let dryRun = false;
22059
+ const booleans = { dryRun: false, reset: false };
22007
22060
  for (const arg of argv) {
22008
22061
  if (arg === "-h" || arg === "--help")
22009
22062
  return { kind: "help" };
@@ -22014,10 +22067,11 @@ function parseBackfillArgs(argv) {
22014
22067
  const body = arg.slice(2);
22015
22068
  const eq = body.indexOf("=");
22016
22069
  const name = eq === -1 ? body : body.slice(0, eq);
22017
- if (BOOLEAN_FLAGS.has(name)) {
22070
+ const boolean = BOOLEAN_FLAGS.get(name);
22071
+ if (boolean !== void 0) {
22018
22072
  if (eq !== -1)
22019
22073
  return { kind: "error", message: `--${name} takes no value` };
22020
- dryRun = true;
22074
+ booleans[boolean] = true;
22021
22075
  continue;
22022
22076
  }
22023
22077
  if (VALUE_FLAGS.has(name)) {
@@ -22028,7 +22082,7 @@ function parseBackfillArgs(argv) {
22028
22082
  }
22029
22083
  return {
22030
22084
  kind: "error",
22031
- message: `unknown option '${arg}'. Known: ${[...BOOLEAN_FLAGS, ...VALUE_FLAGS].map((f) => `--${f}`).sort().join(", ")}, --help`
22085
+ message: `unknown option '${arg}'. Known: ${[...BOOLEAN_FLAGS.keys(), ...VALUE_FLAGS].map((f) => `--${f}`).sort().join(", ")}, --help`
22032
22086
  };
22033
22087
  }
22034
22088
  const positive = (name) => {
@@ -22041,6 +22095,12 @@ function parseBackfillArgs(argv) {
22041
22095
  }
22042
22096
  return n;
22043
22097
  };
22098
+ if (booleans.dryRun && booleans.reset) {
22099
+ return {
22100
+ kind: "error",
22101
+ message: "--reset cannot be combined with --dry-run: a dry run maps every transcript from the start already, and never writes checkpoints"
22102
+ };
22103
+ }
22044
22104
  const sinceDays = positive("days");
22045
22105
  if (typeof sinceDays === "object")
22046
22106
  return sinceDays;
@@ -22050,7 +22110,8 @@ function parseBackfillArgs(argv) {
22050
22110
  return {
22051
22111
  kind: "run",
22052
22112
  options: {
22053
- dryRun,
22113
+ dryRun: booleans.dryRun,
22114
+ reset: booleans.reset,
22054
22115
  paths,
22055
22116
  ...values.get("profile") !== void 0 ? { profile: values.get("profile") } : {},
22056
22117
  ...values.get("credentials") !== void 0 ? { credentialsPath: values.get("credentials") } : {},
@@ -22066,10 +22127,21 @@ function batches(items, size) {
22066
22127
  return out;
22067
22128
  }
22068
22129
  async function runBackfill(mapper, options, say2) {
22130
+ if (!options.dryRun && backfillIsOff(mapper, options)) {
22131
+ say2("BACKFILL DISABLED \u2014 the retroactive import is opt-in, and this machine has not opted in");
22132
+ say2(`set "capture": { "backfill": true } in ${configPath()} to allow it`);
22133
+ say2("nothing was sent and no checkpoint was cleared; live capture is unaffected");
22134
+ return disabledResult();
22135
+ }
22069
22136
  const discovered = options.paths.length > 0 ? options.paths : discoverTranscripts(mapper);
22070
22137
  const paths = options.sinceDays === void 0 ? discovered : withinDays(discovered, options.sinceDays);
22071
22138
  const window2 = options.sinceDays === void 0 ? "" : ` written to in the last ${options.sinceDays} day(s)`;
22072
22139
  say2(`${options.dryRun ? "DRY RUN \u2014 nothing will be sent. " : ""}${paths.length} transcript(s)${window2}`);
22140
+ if (options.reset) {
22141
+ const reset = await resetCheckpoints(mapper, options, paths, say2);
22142
+ if (reset === void 0)
22143
+ return lockBusyResult();
22144
+ }
22073
22145
  const pass = (subset) => runForwarder(mapper, {
22074
22146
  paths: subset,
22075
22147
  dryRun: options.dryRun,
@@ -22086,6 +22158,61 @@ async function runBackfill(mapper, options, say2) {
22086
22158
  return pass([]);
22087
22159
  return runPasses(batches(paths, options.batch ?? DEFAULT_BATCH), pass, say2);
22088
22160
  }
22161
+ async function resetCheckpoints(mapper, options, paths, say2) {
22162
+ const { scope } = loadRuntimeSettings(mapper.vendor, options.profile !== void 0 ? { profile: options.profile } : {});
22163
+ const release = await acquireLockWithin(scope, options.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS);
22164
+ if (!release)
22165
+ return void 0;
22166
+ let reset;
22167
+ try {
22168
+ reset = clearCheckpoints(scope, paths);
22169
+ } finally {
22170
+ release();
22171
+ }
22172
+ if (reset.cleared.length === 0) {
22173
+ say2(reset.total === 0 ? "RESET \u2014 nothing had been shipped yet; these transcripts ship from the start anyway" : `RESET \u2014 NOTHING CLEARED: none of the selected paths match the ${reset.total} checkpoint(s) on file, so they will ship from wherever they left off`);
22174
+ return reset;
22175
+ }
22176
+ say2(`RESET \u2014 forgot the offset for ${reset.cleared.length} transcript(s); they re-ship in full`);
22177
+ say2(`previous checkpoints saved as ${reset.backup}`);
22178
+ return reset;
22179
+ }
22180
+ function backfillIsOff(mapper, options) {
22181
+ try {
22182
+ const { capture } = loadRuntimeSettings(mapper.vendor, options.profile !== void 0 ? { profile: options.profile } : {});
22183
+ return !capture.backfill;
22184
+ } catch {
22185
+ return false;
22186
+ }
22187
+ }
22188
+ function disabledResult() {
22189
+ return {
22190
+ transcripts: 0,
22191
+ records: 0,
22192
+ skipped: ["backfill is disabled"],
22193
+ kinds: {},
22194
+ warnings: [],
22195
+ emitted: { transcripts: 0, records: 0 },
22196
+ backfillDisabled: true,
22197
+ // Non-zero exit, for the reason `shipped 0` cannot carry: an up-to-date machine
22198
+ // prints that too, and a script chaining backfills must not read a refusal as done.
22199
+ ok: false,
22200
+ error: "backfill is disabled by capture.backfill"
22201
+ };
22202
+ }
22203
+ function lockBusyResult() {
22204
+ return {
22205
+ transcripts: 0,
22206
+ records: 0,
22207
+ skipped: ["another forwarder holds the lock"],
22208
+ kinds: {},
22209
+ warnings: [],
22210
+ emitted: { transcripts: 0, records: 0 },
22211
+ lockBusy: true,
22212
+ ok: false,
22213
+ error: "another forwarder holds the lock"
22214
+ };
22215
+ }
22089
22216
  async function runPasses(groups, pass, say2) {
22090
22217
  const total = {
22091
22218
  transcripts: 0,
@@ -22515,6 +22642,7 @@ if (parsed.kind === "error") {
22515
22642
  process.exit(2);
22516
22643
  }
22517
22644
  var result = await runBackfill(codexTranscript, parsed.options, say);
22645
+ if (result.backfillDisabled === true) process.exit(4);
22518
22646
  say(
22519
22647
  `${parsed.options.dryRun ? "mapped" : "shipped"} ${result.records} record(s) from ${result.transcripts} transcript(s)`
22520
22648
  );
@@ -1033,7 +1033,11 @@ function init(argv) {
1033
1033
  // a named profile resolve without the flag on every later run.
1034
1034
  ...separateFile ? { profile, credentials: credsPath, ...stripCredentials(existingConfig) } : credentials,
1035
1035
  enabled: existingConfig["enabled"] ?? true,
1036
- capture: existingConfig["capture"] ?? { enabled: true },
1036
+ // Written explicitly rather than left to the default: `backfill: false` is the one
1037
+ // setting someone is likely to go looking for, and a key that is not in the file is
1038
+ // not findable. An existing block is preserved untouched, so this never flips a
1039
+ // machine that had already opted in.
1040
+ capture: existingConfig["capture"] ?? { enabled: true, backfill: false },
1037
1041
  // Enforcement stays off until the mapper is validated — capture only for now.
1038
1042
  enforce: existingConfig["enforce"] ?? { mode: "off", failClosed: true },
1039
1043
  // Whatever another endpoint wrote is preserved: one file, several endpoints, and
@@ -18160,6 +18160,9 @@ function settingsFor(file, vendor) {
18160
18160
  ...file.enforce ?? override.enforce ? { enforce: { ...file.enforce, ...override.enforce } } : {}
18161
18161
  };
18162
18162
  }
18163
+ function resolveCapture(capture) {
18164
+ return { enabled: capture?.enabled ?? true, backfill: capture?.backfill ?? false };
18165
+ }
18163
18166
  function readConfigFile(vendor) {
18164
18167
  if (!existsSync(configPath())) {
18165
18168
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
@@ -18212,7 +18215,7 @@ function loadLocalConfig(vendor, options = {}) {
18212
18215
  applicationId,
18213
18216
  ...(creds.userId ?? vendorFile.userId) !== void 0 ? { userId: creds.userId ?? vendorFile.userId } : {},
18214
18217
  enabled: vendorFile.enabled ?? true,
18215
- capture: { enabled: vendorFile.capture?.enabled ?? true },
18218
+ capture: resolveCapture(vendorFile.capture),
18216
18219
  enforce: {
18217
18220
  mode: vendorFile.enforce?.mode ?? "off",
18218
18221
  failClosed: vendorFile.enforce?.failClosed ?? true
@@ -18754,8 +18757,9 @@ function installSeededIdGenerator(client) {
18754
18757
  }
18755
18758
 
18756
18759
  // packages/core/dist/forwarder/checkpoint.js
18757
- import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
18760
+ import { copyFileSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
18758
18761
  import { join as join6 } from "node:path";
18762
+ var SPOOL_KEY = "::spool::";
18759
18763
  function checkpointPath(scope) {
18760
18764
  return join6(CONFIG_DIR, `${scope}.checkpoints.json`);
18761
18765
  }
@@ -18872,12 +18876,14 @@ function acquireLock(scope) {
18872
18876
  }
18873
18877
  return () => rmSync(path, { force: true });
18874
18878
  }
18875
- function isAlive(pid) {
18879
+ function isAlive(pid, kill = (p, s) => {
18880
+ process.kill(p, s);
18881
+ }) {
18876
18882
  try {
18877
- process.kill(pid, 0);
18883
+ kill(pid, 0);
18878
18884
  return true;
18879
- } catch {
18880
- return false;
18885
+ } catch (err) {
18886
+ return err.code !== "ESRCH";
18881
18887
  }
18882
18888
  }
18883
18889
  var DEFAULT_LOCK_WAIT_MS = 60 * 1e3;
@@ -21692,7 +21698,6 @@ function toFloat(value, fallback) {
21692
21698
  }
21693
21699
 
21694
21700
  // packages/core/dist/forwarder/run.js
21695
- var SPOOL_KEY = "::spool::";
21696
21701
  var DARKHUNT_SOURCE_HINT = "darkhunt-telemetry";
21697
21702
  function drainSpool(mapper, checkpoints) {
21698
21703
  const path = spoolPath(mapper.vendor);
@@ -228,13 +228,16 @@ function loadRuntimeSettings(vendor, options = {}) {
228
228
  profile,
229
229
  scope: scopeKey(vendor, profile),
230
230
  enabled: vendorFile.enabled ?? true,
231
- capture: { enabled: vendorFile.capture?.enabled ?? true },
231
+ capture: resolveCapture(vendorFile.capture),
232
232
  enforce: {
233
233
  mode: vendorFile.enforce?.mode ?? "off",
234
234
  failClosed: vendorFile.enforce?.failClosed ?? true
235
235
  }
236
236
  };
237
237
  }
238
+ function resolveCapture(capture) {
239
+ return { enabled: capture?.enabled ?? true, backfill: capture?.backfill ?? false };
240
+ }
238
241
  function readConfigFile(vendor) {
239
242
  if (!existsSync(configPath())) {
240
243
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
package/dist/bin/init.mjs CHANGED
@@ -1025,7 +1025,11 @@ function init(argv) {
1025
1025
  // a named profile resolve without the flag on every later run.
1026
1026
  ...separateFile ? { profile, credentials: credsPath, ...stripCredentials(existingConfig) } : credentials,
1027
1027
  enabled: existingConfig["enabled"] ?? true,
1028
- capture: existingConfig["capture"] ?? { enabled: true },
1028
+ // Written explicitly rather than left to the default: `backfill: false` is the one
1029
+ // setting someone is likely to go looking for, and a key that is not in the file is
1030
+ // not findable. An existing block is preserved untouched, so this never flips a
1031
+ // machine that had already opted in.
1032
+ capture: existingConfig["capture"] ?? { enabled: true, backfill: false },
1029
1033
  // Enforcement stays off until the mapper is validated — capture only for now.
1030
1034
  enforce: existingConfig["enforce"] ?? { mode: "off", failClosed: true },
1031
1035
  // Whatever another endpoint wrote is preserved: one file, several endpoints, and
@@ -74,13 +74,16 @@ function loadRuntimeSettings(vendor, options = {}) {
74
74
  profile,
75
75
  scope: scopeKey(vendor, profile),
76
76
  enabled: vendorFile.enabled ?? true,
77
- capture: { enabled: vendorFile.capture?.enabled ?? true },
77
+ capture: resolveCapture(vendorFile.capture),
78
78
  enforce: {
79
79
  mode: vendorFile.enforce?.mode ?? "off",
80
80
  failClosed: vendorFile.enforce?.failClosed ?? true
81
81
  }
82
82
  };
83
83
  }
84
+ function resolveCapture(capture) {
85
+ return { enabled: capture?.enabled ?? true, backfill: capture?.backfill ?? false };
86
+ }
84
87
  function readConfigFile(vendor) {
85
88
  if (!existsSync(configPath())) {
86
89
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
@@ -87,6 +87,9 @@ function settingsFor(file, vendor) {
87
87
  ...file.enforce ?? override.enforce ? { enforce: { ...file.enforce, ...override.enforce } } : {}
88
88
  };
89
89
  }
90
+ function resolveCapture(capture) {
91
+ return { enabled: capture?.enabled ?? true, backfill: capture?.backfill ?? false };
92
+ }
90
93
  function readConfigFile(vendor) {
91
94
  if (!existsSync(configPath())) {
92
95
  throw new ConfigMissingError(`${configPath()} does not exist \u2014 run init.mjs to create it`);
@@ -139,7 +142,7 @@ function loadLocalConfig(vendor, options = {}) {
139
142
  applicationId,
140
143
  ...(creds.userId ?? vendorFile.userId) !== void 0 ? { userId: creds.userId ?? vendorFile.userId } : {},
141
144
  enabled: vendorFile.enabled ?? true,
142
- capture: { enabled: vendorFile.capture?.enabled ?? true },
145
+ capture: resolveCapture(vendorFile.capture),
143
146
  enforce: {
144
147
  mode: vendorFile.enforce?.mode ?? "off",
145
148
  failClosed: vendorFile.enforce?.failClosed ?? true
@@ -171,7 +174,7 @@ function loadBeat(scope) {
171
174
  }
172
175
 
173
176
  // packages/core/dist/forwarder/checkpoint.js
174
- import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
177
+ import { copyFileSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
175
178
  import { join as join6 } from "node:path";
176
179
  function checkpointPath(scope) {
177
180
  return join6(CONFIG_DIR, `${scope}.checkpoints.json`);
@@ -1208,6 +1211,9 @@ function status(vendor, mapper) {
1208
1211
  workspaceId: config.workspaceId,
1209
1212
  scope: config.scope,
1210
1213
  capture: config.capture.enabled && config.enabled,
1214
+ // Each switch above it can also stop an import, so all three have to agree before
1215
+ // this may claim a backfill would run.
1216
+ backfill: config.capture.backfill && config.capture.enabled && config.enabled,
1211
1217
  enforce: config.enabled ? config.enforce.mode : "off (kill switch)",
1212
1218
  spoolEvents,
1213
1219
  ...spoolLastAt !== void 0 ? { spoolLastAt } : {},
@@ -1680,7 +1686,9 @@ try {
1680
1686
  console.log(
1681
1687
  ` user ${s.userId ?? "(none \u2014 traces are unattributed)"}` + (s.userIdSource === "none" ? "" : ` [from ${s.userIdSource}]`)
1682
1688
  );
1683
- console.log(` capture ${s.capture ? "ON" : "OFF"} enforce ${s.enforce}`);
1689
+ console.log(
1690
+ ` capture ${s.capture ? "ON" : "OFF"} enforce ${s.enforce} backfill ${s.backfill ? "ON" : "OFF"}`
1691
+ );
1684
1692
  if (s.subagents && s.subagents.found > 0) {
1685
1693
  const { found, shipped } = s.subagents;
1686
1694
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkhunt-security/endpoint-codex",
3
- "version": "0.9.15",
3
+ "version": "0.9.17",
4
4
  "type": "module",
5
5
  "description": "Darkhunt endpoint adapter for Codex CLI: hook codec, rollout transcript mapper, plugin manifest.",
6
6
  "bin": {