@darkhunt-security/endpoint-codex 0.9.16 → 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.16",
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";
@@ -21694,7 +21732,6 @@ function toFloat(value, fallback) {
21694
21732
  }
21695
21733
 
21696
21734
  // packages/core/dist/forwarder/run.js
21697
- var SPOOL_KEY = "::spool::";
21698
21735
  var DARKHUNT_SOURCE_HINT = "darkhunt-telemetry";
21699
21736
  function drainSpool(mapper, checkpoints) {
21700
21737
  const path = spoolPath(mapper.vendor);
@@ -21984,7 +22021,10 @@ import { join as join9 } from "node:path";
21984
22021
  var CLI_CREDENTIALS_PATH = join9(homedir3(), ".darkhunt", "credentials.json");
21985
22022
 
21986
22023
  // packages/core/dist/cli/backfill.js
21987
- var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["dry-run"]);
22024
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Map([
22025
+ ["dry-run", "dryRun"],
22026
+ ["reset", "reset"]
22027
+ ]);
21988
22028
  var VALUE_FLAGS = /* @__PURE__ */ new Set(["profile", "credentials", "days", "batch"]);
21989
22029
  var DEFAULT_BATCH = 25;
21990
22030
  function usage(command) {
@@ -21992,6 +22032,8 @@ function usage(command) {
21992
22032
  `usage: ${command} [options] [paths...]`,
21993
22033
  "",
21994
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",
21995
22037
  " --days=N only transcripts written to in the last N days",
21996
22038
  " --batch=N transcripts per pass (default " + String(DEFAULT_BATCH) + ")",
21997
22039
  " --profile=NAME endpoint profile to ship to",
@@ -21999,13 +22041,22 @@ function usage(command) {
21999
22041
  " -h, --help this message",
22000
22042
  "",
22001
22043
  " With no paths this ships EVERY transcript on disk. Narrow it with --days",
22002
- " 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."
22003
22054
  ].join("\n");
22004
22055
  }
22005
22056
  function parseBackfillArgs(argv) {
22006
22057
  const paths = [];
22007
22058
  const values = /* @__PURE__ */ new Map();
22008
- let dryRun = false;
22059
+ const booleans = { dryRun: false, reset: false };
22009
22060
  for (const arg of argv) {
22010
22061
  if (arg === "-h" || arg === "--help")
22011
22062
  return { kind: "help" };
@@ -22016,10 +22067,11 @@ function parseBackfillArgs(argv) {
22016
22067
  const body = arg.slice(2);
22017
22068
  const eq = body.indexOf("=");
22018
22069
  const name = eq === -1 ? body : body.slice(0, eq);
22019
- if (BOOLEAN_FLAGS.has(name)) {
22070
+ const boolean = BOOLEAN_FLAGS.get(name);
22071
+ if (boolean !== void 0) {
22020
22072
  if (eq !== -1)
22021
22073
  return { kind: "error", message: `--${name} takes no value` };
22022
- dryRun = true;
22074
+ booleans[boolean] = true;
22023
22075
  continue;
22024
22076
  }
22025
22077
  if (VALUE_FLAGS.has(name)) {
@@ -22030,7 +22082,7 @@ function parseBackfillArgs(argv) {
22030
22082
  }
22031
22083
  return {
22032
22084
  kind: "error",
22033
- 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`
22034
22086
  };
22035
22087
  }
22036
22088
  const positive = (name) => {
@@ -22043,6 +22095,12 @@ function parseBackfillArgs(argv) {
22043
22095
  }
22044
22096
  return n;
22045
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
+ }
22046
22104
  const sinceDays = positive("days");
22047
22105
  if (typeof sinceDays === "object")
22048
22106
  return sinceDays;
@@ -22052,7 +22110,8 @@ function parseBackfillArgs(argv) {
22052
22110
  return {
22053
22111
  kind: "run",
22054
22112
  options: {
22055
- dryRun,
22113
+ dryRun: booleans.dryRun,
22114
+ reset: booleans.reset,
22056
22115
  paths,
22057
22116
  ...values.get("profile") !== void 0 ? { profile: values.get("profile") } : {},
22058
22117
  ...values.get("credentials") !== void 0 ? { credentialsPath: values.get("credentials") } : {},
@@ -22068,10 +22127,21 @@ function batches(items, size) {
22068
22127
  return out;
22069
22128
  }
22070
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
+ }
22071
22136
  const discovered = options.paths.length > 0 ? options.paths : discoverTranscripts(mapper);
22072
22137
  const paths = options.sinceDays === void 0 ? discovered : withinDays(discovered, options.sinceDays);
22073
22138
  const window2 = options.sinceDays === void 0 ? "" : ` written to in the last ${options.sinceDays} day(s)`;
22074
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
+ }
22075
22145
  const pass = (subset) => runForwarder(mapper, {
22076
22146
  paths: subset,
22077
22147
  dryRun: options.dryRun,
@@ -22088,6 +22158,61 @@ async function runBackfill(mapper, options, say2) {
22088
22158
  return pass([]);
22089
22159
  return runPasses(batches(paths, options.batch ?? DEFAULT_BATCH), pass, say2);
22090
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
+ }
22091
22216
  async function runPasses(groups, pass, say2) {
22092
22217
  const total = {
22093
22218
  transcripts: 0,
@@ -22517,6 +22642,7 @@ if (parsed.kind === "error") {
22517
22642
  process.exit(2);
22518
22643
  }
22519
22644
  var result = await runBackfill(codexTranscript, parsed.options, say);
22645
+ if (result.backfillDisabled === true) process.exit(4);
22520
22646
  say(
22521
22647
  `${parsed.options.dryRun ? "mapped" : "shipped"} ${result.records} record(s) from ${result.transcripts} transcript(s)`
22522
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
  }
@@ -21694,7 +21698,6 @@ function toFloat(value, fallback) {
21694
21698
  }
21695
21699
 
21696
21700
  // packages/core/dist/forwarder/run.js
21697
- var SPOOL_KEY = "::spool::";
21698
21701
  var DARKHUNT_SOURCE_HINT = "darkhunt-telemetry";
21699
21702
  function drainSpool(mapper, checkpoints) {
21700
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.16",
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": {