@darkhunt-security/endpoint-codex 0.9.16 → 0.9.18
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/.codex-plugin/plugin.json +1 -1
- package/commands/backfill.md +55 -1
- package/commands/status.md +5 -0
- package/dist/bin/backfill.mjs +269 -28
- package/dist/bin/enroll.mjs +12 -1
- package/dist/bin/forwarder.mjs +227 -22
- package/dist/bin/guard.mjs +11 -1
- package/dist/bin/init.mjs +12 -1
- package/dist/bin/spool.mjs +11 -1
- package/dist/bin/status.mjs +79 -26
- package/package.json +2 -2
package/commands/backfill.md
CHANGED
|
@@ -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
|
package/commands/status.md
CHANGED
|
@@ -34,5 +34,10 @@ What the fields mean, and what to say:
|
|
|
34
34
|
export is being rejected — check `last pass` to tell those apart.
|
|
35
35
|
- **last pass FAILED** — read out the cause. Checkpoints are held on failure, so nothing
|
|
36
36
|
has been lost; it will re-ship once the cause is fixed.
|
|
37
|
+
- **held N transcript(s) waiting on the platform** — a pass could not reach Darkhunt, so
|
|
38
|
+
those sessions are queued on this machine with their checkpoints held. The forwarder
|
|
39
|
+
retries on its own for about an hour after a failure, and every later hook retries
|
|
40
|
+
them too, so this clears itself once the platform answers. Say since when they have
|
|
41
|
+
been held; nothing is lost while it is non-zero.
|
|
37
42
|
|
|
38
43
|
Do not run anything else, and do not offer to change the configuration unless asked.
|
package/dist/bin/backfill.mjs
CHANGED
|
@@ -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:
|
|
18232
|
+
capture: resolveCapture(vendorFile.capture),
|
|
18216
18233
|
enforce: {
|
|
18217
18234
|
mode: vendorFile.enforce?.mode ?? "off",
|
|
18218
18235
|
failClosed: vendorFile.enforce?.failClosed ?? true
|
|
@@ -18257,6 +18274,9 @@ function liveTranscripts(beat, now = Date.now()) {
|
|
|
18257
18274
|
return [...paths];
|
|
18258
18275
|
}
|
|
18259
18276
|
|
|
18277
|
+
// packages/core/dist/emit/emitter.js
|
|
18278
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
18279
|
+
|
|
18260
18280
|
// packages/core/dist/emit/ids.js
|
|
18261
18281
|
import { createHash, randomBytes } from "node:crypto";
|
|
18262
18282
|
var SPAN_ID_HEX = 16;
|
|
@@ -18296,6 +18316,20 @@ var SeededIdGenerator = class {
|
|
|
18296
18316
|
};
|
|
18297
18317
|
|
|
18298
18318
|
// packages/core/dist/emit/emitter.js
|
|
18319
|
+
var PAYLOAD_LIMIT = 128 * 1024;
|
|
18320
|
+
function capPayload(value, limit = PAYLOAD_LIMIT) {
|
|
18321
|
+
if (value === void 0 || value === null)
|
|
18322
|
+
return value;
|
|
18323
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
18324
|
+
if (text === void 0 || text.length <= limit)
|
|
18325
|
+
return value;
|
|
18326
|
+
const digest = createHash2("sha256").update(text).digest("hex").slice(0, 16);
|
|
18327
|
+
const marker = `\u2026 [truncated by darkhunt: ${text.length} chars total, sha256:${digest}]`;
|
|
18328
|
+
return `${text.slice(0, Math.max(0, limit - marker.length))}${marker}`;
|
|
18329
|
+
}
|
|
18330
|
+
function capText(text, limit = PAYLOAD_LIMIT) {
|
|
18331
|
+
return capPayload(text, limit);
|
|
18332
|
+
}
|
|
18299
18333
|
var TOOL_SUMMARY_LIMIT = 2e3;
|
|
18300
18334
|
function summarize(output) {
|
|
18301
18335
|
const text = typeof output === "string" ? output : JSON.stringify(output ?? null);
|
|
@@ -18422,7 +18456,7 @@ var SessionEmitter = class {
|
|
|
18422
18456
|
// A subagent's first record is the task it was given, and it has already been
|
|
18423
18457
|
// absorbed by the time this fires — so the child trace opens with its brief
|
|
18424
18458
|
// rather than empty.
|
|
18425
|
-
...this.lastUserText ? { input: this.lastUserText } : {},
|
|
18459
|
+
...this.lastUserText ? { input: capText(this.lastUserText) } : {},
|
|
18426
18460
|
...this.options.userId !== void 0 ? { userId: this.options.userId } : {}
|
|
18427
18461
|
}));
|
|
18428
18462
|
this.sidechains.push(owner);
|
|
@@ -18463,7 +18497,7 @@ var SessionEmitter = class {
|
|
|
18463
18497
|
case "thinking": {
|
|
18464
18498
|
this.openGeneration(record, owner);
|
|
18465
18499
|
const host = this.generation ?? owner;
|
|
18466
|
-
this.seeded(["thinking", record.uuid], () => host.span("thinking", { startTime: record.ts, output: record.text })).end({ endTime: record.ts });
|
|
18500
|
+
this.seeded(["thinking", record.uuid], () => host.span("thinking", { startTime: record.ts, output: capPayload(record.text) })).end({ endTime: record.ts });
|
|
18467
18501
|
break;
|
|
18468
18502
|
}
|
|
18469
18503
|
case "tool_call": {
|
|
@@ -18474,8 +18508,8 @@ var SessionEmitter = class {
|
|
|
18474
18508
|
startTime: record.ts,
|
|
18475
18509
|
...record.toolName !== void 0 ? { toolName: record.toolName } : {},
|
|
18476
18510
|
...record.toolCallId !== void 0 ? { toolCallId: record.toolCallId } : {},
|
|
18477
|
-
toolArguments: record.input,
|
|
18478
|
-
input: record.input
|
|
18511
|
+
toolArguments: capPayload(record.input),
|
|
18512
|
+
input: capPayload(record.input)
|
|
18479
18513
|
}));
|
|
18480
18514
|
if (record.toolCallId)
|
|
18481
18515
|
this.toolSpans.set(record.toolCallId, span);
|
|
@@ -18487,7 +18521,7 @@ var SessionEmitter = class {
|
|
|
18487
18521
|
const span = record.toolCallId ? this.toolSpans.get(record.toolCallId) : void 0;
|
|
18488
18522
|
if (span) {
|
|
18489
18523
|
span.end({
|
|
18490
|
-
output: record.output,
|
|
18524
|
+
output: capPayload(record.output),
|
|
18491
18525
|
endTime: record.ts,
|
|
18492
18526
|
...record.isError ? { level: "ERROR" } : {},
|
|
18493
18527
|
...record.errorKind !== void 0 ? { statusMessage: record.errorKind } : {}
|
|
@@ -18501,7 +18535,7 @@ var SessionEmitter = class {
|
|
|
18501
18535
|
this.endGeneration();
|
|
18502
18536
|
this.seeded(["compaction", record.uuid], () => owner.span("compaction", {
|
|
18503
18537
|
startTime: record.ts,
|
|
18504
|
-
...record.text ? { output: record.text } : {}
|
|
18538
|
+
...record.text ? { output: capPayload(record.text) } : {}
|
|
18505
18539
|
})).end({ endTime: record.ts });
|
|
18506
18540
|
break;
|
|
18507
18541
|
case "permission_grant": {
|
|
@@ -18563,7 +18597,7 @@ var SessionEmitter = class {
|
|
|
18563
18597
|
...this.title !== void 0 ? { name: this.title } : {},
|
|
18564
18598
|
// The pass's result. Paired with the `input` set when the trace opened, this is
|
|
18565
18599
|
// what stops the consumer inferring the trace's I/O from a descendant.
|
|
18566
|
-
...this.lastAssistantText !== void 0 ? { output: this.lastAssistantText } : {}
|
|
18600
|
+
...this.lastAssistantText !== void 0 ? { output: capText(this.lastAssistantText) } : {}
|
|
18567
18601
|
});
|
|
18568
18602
|
this.trace.end();
|
|
18569
18603
|
this.trace = void 0;
|
|
@@ -18646,7 +18680,7 @@ var SessionEmitter = class {
|
|
|
18646
18680
|
// What the agent was asked. `TraceArgs.input` is construction-only — the SDK's
|
|
18647
18681
|
// `update()` carries output alone — which is why `ingest` absorbs the user turn
|
|
18648
18682
|
// before it calls this.
|
|
18649
|
-
...this.lastUserText ? { input: this.lastUserText } : {},
|
|
18683
|
+
...this.lastUserText ? { input: capText(this.lastUserText) } : {},
|
|
18650
18684
|
tags: [this.options.vendor, ...this.options.agentId ? ["subagent"] : []],
|
|
18651
18685
|
metadata: {
|
|
18652
18686
|
...this.meta,
|
|
@@ -18715,7 +18749,7 @@ var SessionEmitter = class {
|
|
|
18715
18749
|
buildInputMessages() {
|
|
18716
18750
|
const messages = [];
|
|
18717
18751
|
if (this.lastUserText !== void 0) {
|
|
18718
|
-
messages.push({ role: "user", content: this.lastUserText });
|
|
18752
|
+
messages.push({ role: "user", content: capText(this.lastUserText) });
|
|
18719
18753
|
}
|
|
18720
18754
|
for (const result2 of this.pendingToolResults) {
|
|
18721
18755
|
messages.push({ role: "tool", content: result2 });
|
|
@@ -18725,7 +18759,7 @@ var SessionEmitter = class {
|
|
|
18725
18759
|
endGeneration() {
|
|
18726
18760
|
if (!this.generation)
|
|
18727
18761
|
return;
|
|
18728
|
-
const output = this.outputText.join("\n");
|
|
18762
|
+
const output = capText(this.outputText.join("\n"));
|
|
18729
18763
|
if (output)
|
|
18730
18764
|
this.lastAssistantText = output;
|
|
18731
18765
|
this.generation.end({
|
|
@@ -18754,8 +18788,9 @@ function installSeededIdGenerator(client) {
|
|
|
18754
18788
|
}
|
|
18755
18789
|
|
|
18756
18790
|
// 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";
|
|
18791
|
+
import { copyFileSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
18758
18792
|
import { join as join6 } from "node:path";
|
|
18793
|
+
var SPOOL_KEY = "::spool::";
|
|
18759
18794
|
function checkpointPath(scope) {
|
|
18760
18795
|
return join6(CONFIG_DIR, `${scope}.checkpoints.json`);
|
|
18761
18796
|
}
|
|
@@ -18766,6 +18801,25 @@ function loadCheckpoints(scope) {
|
|
|
18766
18801
|
return {};
|
|
18767
18802
|
}
|
|
18768
18803
|
}
|
|
18804
|
+
function laggingTranscripts(checkpoints, now = statSize) {
|
|
18805
|
+
const paths = [];
|
|
18806
|
+
for (const [path, checkpoint] of Object.entries(checkpoints)) {
|
|
18807
|
+
if (path === SPOOL_KEY)
|
|
18808
|
+
continue;
|
|
18809
|
+
const size = now(path);
|
|
18810
|
+
if (size === void 0 || size === checkpoint.offset)
|
|
18811
|
+
continue;
|
|
18812
|
+
paths.push(path);
|
|
18813
|
+
}
|
|
18814
|
+
return paths;
|
|
18815
|
+
}
|
|
18816
|
+
function statSize(path) {
|
|
18817
|
+
try {
|
|
18818
|
+
return statSync2(path).size;
|
|
18819
|
+
} catch {
|
|
18820
|
+
return void 0;
|
|
18821
|
+
}
|
|
18822
|
+
}
|
|
18769
18823
|
function orphanedScopes(vendor, scope) {
|
|
18770
18824
|
const suffix = ".checkpoints.json";
|
|
18771
18825
|
try {
|
|
@@ -18800,6 +18854,26 @@ function resumeTurn(checkpoints, path, from) {
|
|
|
18800
18854
|
return void 0;
|
|
18801
18855
|
return checkpoints[path]?.turn;
|
|
18802
18856
|
}
|
|
18857
|
+
function clearCheckpoints(scope, paths) {
|
|
18858
|
+
const checkpoints = loadCheckpoints(scope);
|
|
18859
|
+
const wanted = paths === void 0 ? void 0 : new Set(paths);
|
|
18860
|
+
const transcripts = Object.keys(checkpoints).filter((key) => key !== SPOOL_KEY);
|
|
18861
|
+
const cleared = transcripts.filter((key) => wanted === void 0 || wanted.has(key));
|
|
18862
|
+
const total = transcripts.length;
|
|
18863
|
+
if (cleared.length === 0)
|
|
18864
|
+
return { cleared, total };
|
|
18865
|
+
const backup = `${checkpointPath(scope)}.bak-${stamp(/* @__PURE__ */ new Date())}-prereset`;
|
|
18866
|
+
copyFileSync(checkpointPath(scope), backup);
|
|
18867
|
+
const kept = { ...checkpoints };
|
|
18868
|
+
for (const key of cleared)
|
|
18869
|
+
delete kept[key];
|
|
18870
|
+
saveCheckpoints(scope, kept);
|
|
18871
|
+
return { cleared, total, backup };
|
|
18872
|
+
}
|
|
18873
|
+
function stamp(at) {
|
|
18874
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
18875
|
+
return `${at.getFullYear()}${pad(at.getMonth() + 1)}${pad(at.getDate())}-${pad(at.getHours())}${pad(at.getMinutes())}${pad(at.getSeconds())}`;
|
|
18876
|
+
}
|
|
18803
18877
|
|
|
18804
18878
|
// packages/core/dist/forwarder/tail.js
|
|
18805
18879
|
import { closeSync, openSync, readSync, statSync as statSync3 } from "node:fs";
|
|
@@ -18849,6 +18923,7 @@ function readNewLines(path, offset, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
18849
18923
|
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
18850
18924
|
import { join as join7 } from "node:path";
|
|
18851
18925
|
var STALE_MS = 5 * 60 * 1e3;
|
|
18926
|
+
var HEARTBEAT_MS = 60 * 1e3;
|
|
18852
18927
|
function acquireLock(scope) {
|
|
18853
18928
|
mkdirSync3(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
18854
18929
|
const path = join7(CONFIG_DIR, `${scope}.forwarder.lock`);
|
|
@@ -18870,7 +18945,17 @@ function acquireLock(scope) {
|
|
|
18870
18945
|
} catch {
|
|
18871
18946
|
return null;
|
|
18872
18947
|
}
|
|
18873
|
-
|
|
18948
|
+
const beat = setInterval(() => {
|
|
18949
|
+
try {
|
|
18950
|
+
writeFileSync3(path, JSON.stringify({ pid: process.pid, ts: Date.now() }), { mode: 384 });
|
|
18951
|
+
} catch {
|
|
18952
|
+
}
|
|
18953
|
+
}, HEARTBEAT_MS);
|
|
18954
|
+
beat.unref();
|
|
18955
|
+
return () => {
|
|
18956
|
+
clearInterval(beat);
|
|
18957
|
+
rmSync(path, { force: true });
|
|
18958
|
+
};
|
|
18874
18959
|
}
|
|
18875
18960
|
function isAlive(pid, kill = (p, s) => {
|
|
18876
18961
|
process.kill(p, s);
|
|
@@ -18971,6 +19056,61 @@ function saveHealth(scope, health) {
|
|
|
18971
19056
|
}
|
|
18972
19057
|
}
|
|
18973
19058
|
|
|
19059
|
+
// packages/core/dist/forwarder/pending.js
|
|
19060
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
19061
|
+
import { join as join9 } from "node:path";
|
|
19062
|
+
var MAX_PENDING_PATHS = 256;
|
|
19063
|
+
function pendingPath(scope) {
|
|
19064
|
+
return join9(CONFIG_DIR, `${scope}.pending.json`);
|
|
19065
|
+
}
|
|
19066
|
+
function loadPending(scope) {
|
|
19067
|
+
try {
|
|
19068
|
+
const backlog = JSON.parse(readFileSync6(pendingPath(scope), "utf8"));
|
|
19069
|
+
if (!Array.isArray(backlog.paths))
|
|
19070
|
+
return void 0;
|
|
19071
|
+
return backlog;
|
|
19072
|
+
} catch {
|
|
19073
|
+
return void 0;
|
|
19074
|
+
}
|
|
19075
|
+
}
|
|
19076
|
+
function pendingTranscripts(scope) {
|
|
19077
|
+
const backlog = loadPending(scope);
|
|
19078
|
+
if (!backlog)
|
|
19079
|
+
return [];
|
|
19080
|
+
return backlog.paths.filter((path) => {
|
|
19081
|
+
try {
|
|
19082
|
+
return existsSync3(path);
|
|
19083
|
+
} catch {
|
|
19084
|
+
return false;
|
|
19085
|
+
}
|
|
19086
|
+
});
|
|
19087
|
+
}
|
|
19088
|
+
function savePending(scope, paths, now = /* @__PURE__ */ new Date()) {
|
|
19089
|
+
try {
|
|
19090
|
+
const at = now.toISOString();
|
|
19091
|
+
const previous = loadPending(scope);
|
|
19092
|
+
const merged = [.../* @__PURE__ */ new Set([...previous?.paths ?? [], ...paths])];
|
|
19093
|
+
const kept = merged.slice(Math.max(0, merged.length - MAX_PENDING_PATHS));
|
|
19094
|
+
if (kept.length === 0)
|
|
19095
|
+
return;
|
|
19096
|
+
write(scope, { since: previous?.since ?? at, at, paths: kept });
|
|
19097
|
+
} catch {
|
|
19098
|
+
}
|
|
19099
|
+
}
|
|
19100
|
+
function clearPending(scope) {
|
|
19101
|
+
try {
|
|
19102
|
+
rmSync2(pendingPath(scope), { force: true });
|
|
19103
|
+
} catch {
|
|
19104
|
+
}
|
|
19105
|
+
}
|
|
19106
|
+
function write(scope, backlog) {
|
|
19107
|
+
mkdirSync5(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
19108
|
+
const target = pendingPath(scope);
|
|
19109
|
+
const tmp = `${target}.tmp`;
|
|
19110
|
+
writeFileSync5(tmp, JSON.stringify(backlog), { mode: 384 });
|
|
19111
|
+
renameSync4(tmp, target);
|
|
19112
|
+
}
|
|
19113
|
+
|
|
18974
19114
|
// packages/core/dist/forwarder/run.js
|
|
18975
19115
|
import { readdirSync as readdirSync2, statSync as statSync4 } from "node:fs";
|
|
18976
19116
|
|
|
@@ -21694,7 +21834,6 @@ function toFloat(value, fallback) {
|
|
|
21694
21834
|
}
|
|
21695
21835
|
|
|
21696
21836
|
// packages/core/dist/forwarder/run.js
|
|
21697
|
-
var SPOOL_KEY = "::spool::";
|
|
21698
21837
|
var DARKHUNT_SOURCE_HINT = "darkhunt-telemetry";
|
|
21699
21838
|
function drainSpool(mapper, checkpoints) {
|
|
21700
21839
|
const path = spoolPath(mapper.vendor);
|
|
@@ -21807,7 +21946,14 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21807
21946
|
}
|
|
21808
21947
|
}
|
|
21809
21948
|
const drain = options.paths ? { paths: options.paths } : drainSpool(mapper, checkpoints);
|
|
21810
|
-
const named = options.paths ? drain.paths : [
|
|
21949
|
+
const named = options.paths ? drain.paths : [
|
|
21950
|
+
.../* @__PURE__ */ new Set([
|
|
21951
|
+
...drain.paths,
|
|
21952
|
+
...liveTranscripts(loadBeat(config.scope)),
|
|
21953
|
+
...pendingTranscripts(config.scope),
|
|
21954
|
+
...laggingTranscripts(checkpoints)
|
|
21955
|
+
])
|
|
21956
|
+
];
|
|
21811
21957
|
const paths = [...new Set(named.flatMap((path) => [path, ...related(mapper, path)]))];
|
|
21812
21958
|
if (paths.length === 0) {
|
|
21813
21959
|
if (drain.checkpoint)
|
|
@@ -21904,13 +22050,16 @@ async function runForwarder(mapper, options = {}) {
|
|
|
21904
22050
|
result2.emitted = { transcripts: result2.transcripts, records: result2.records };
|
|
21905
22051
|
if (watch.failures() > 0) {
|
|
21906
22052
|
result2.ok = false;
|
|
22053
|
+
result2.retryable = true;
|
|
21907
22054
|
result2.error = watch.reason() ?? "span export failed";
|
|
21908
22055
|
result2.transcripts = 0;
|
|
21909
22056
|
result2.records = 0;
|
|
22057
|
+
savePending(config.scope, paths);
|
|
21910
22058
|
} else {
|
|
21911
22059
|
if (drain.checkpoint)
|
|
21912
22060
|
staged[SPOOL_KEY] = drain.checkpoint;
|
|
21913
22061
|
saveCheckpoints(config.scope, staged);
|
|
22062
|
+
clearPending(config.scope);
|
|
21914
22063
|
}
|
|
21915
22064
|
const previous = loadHealth(config.scope);
|
|
21916
22065
|
saveHealth(config.scope, {
|
|
@@ -21975,16 +22124,22 @@ function discoverTranscripts(mapper) {
|
|
|
21975
22124
|
return found;
|
|
21976
22125
|
}
|
|
21977
22126
|
|
|
22127
|
+
// packages/core/dist/forwarder/retry.js
|
|
22128
|
+
var CLAIM_STALE_MS = 45 * 60 * 1e3;
|
|
22129
|
+
|
|
21978
22130
|
// packages/core/dist/cli/status.js
|
|
21979
22131
|
var SESSION_HOOK_STALE_MS = 30 * 60 * 1e3;
|
|
21980
22132
|
|
|
21981
22133
|
// packages/core/dist/cli/enroll.js
|
|
21982
22134
|
import { homedir as homedir3 } from "node:os";
|
|
21983
|
-
import { join as
|
|
21984
|
-
var CLI_CREDENTIALS_PATH =
|
|
22135
|
+
import { join as join10 } from "node:path";
|
|
22136
|
+
var CLI_CREDENTIALS_PATH = join10(homedir3(), ".darkhunt", "credentials.json");
|
|
21985
22137
|
|
|
21986
22138
|
// packages/core/dist/cli/backfill.js
|
|
21987
|
-
var BOOLEAN_FLAGS = /* @__PURE__ */ new
|
|
22139
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Map([
|
|
22140
|
+
["dry-run", "dryRun"],
|
|
22141
|
+
["reset", "reset"]
|
|
22142
|
+
]);
|
|
21988
22143
|
var VALUE_FLAGS = /* @__PURE__ */ new Set(["profile", "credentials", "days", "batch"]);
|
|
21989
22144
|
var DEFAULT_BATCH = 25;
|
|
21990
22145
|
function usage(command) {
|
|
@@ -21992,6 +22147,8 @@ function usage(command) {
|
|
|
21992
22147
|
`usage: ${command} [options] [paths...]`,
|
|
21993
22148
|
"",
|
|
21994
22149
|
" --dry-run map only: no network, no checkpoints",
|
|
22150
|
+
" --reset re-send: forget how far the selected transcripts have",
|
|
22151
|
+
" been shipped, then ship them from the start again",
|
|
21995
22152
|
" --days=N only transcripts written to in the last N days",
|
|
21996
22153
|
" --batch=N transcripts per pass (default " + String(DEFAULT_BATCH) + ")",
|
|
21997
22154
|
" --profile=NAME endpoint profile to ship to",
|
|
@@ -21999,13 +22156,22 @@ function usage(command) {
|
|
|
21999
22156
|
" -h, --help this message",
|
|
22000
22157
|
"",
|
|
22001
22158
|
" 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."
|
|
22159
|
+
" or by naming transcripts, and confirm the destination with --dry-run first.",
|
|
22160
|
+
"",
|
|
22161
|
+
" A checkpoint records how far a transcript has been shipped, never where it was",
|
|
22162
|
+
" shipped to. After re-enrolling against a different application, a plain backfill",
|
|
22163
|
+
" finds nothing to send and exits 0; --reset is what re-sends it.",
|
|
22164
|
+
"",
|
|
22165
|
+
" This command is refused, exit 4, when the config sets",
|
|
22166
|
+
' "capture": { "backfill": false }',
|
|
22167
|
+
" which stops retroactive imports on this machine and leaves live capture running.",
|
|
22168
|
+
" --dry-run still works while it is off."
|
|
22003
22169
|
].join("\n");
|
|
22004
22170
|
}
|
|
22005
22171
|
function parseBackfillArgs(argv) {
|
|
22006
22172
|
const paths = [];
|
|
22007
22173
|
const values = /* @__PURE__ */ new Map();
|
|
22008
|
-
|
|
22174
|
+
const booleans = { dryRun: false, reset: false };
|
|
22009
22175
|
for (const arg of argv) {
|
|
22010
22176
|
if (arg === "-h" || arg === "--help")
|
|
22011
22177
|
return { kind: "help" };
|
|
@@ -22016,10 +22182,11 @@ function parseBackfillArgs(argv) {
|
|
|
22016
22182
|
const body = arg.slice(2);
|
|
22017
22183
|
const eq = body.indexOf("=");
|
|
22018
22184
|
const name = eq === -1 ? body : body.slice(0, eq);
|
|
22019
|
-
|
|
22185
|
+
const boolean = BOOLEAN_FLAGS.get(name);
|
|
22186
|
+
if (boolean !== void 0) {
|
|
22020
22187
|
if (eq !== -1)
|
|
22021
22188
|
return { kind: "error", message: `--${name} takes no value` };
|
|
22022
|
-
|
|
22189
|
+
booleans[boolean] = true;
|
|
22023
22190
|
continue;
|
|
22024
22191
|
}
|
|
22025
22192
|
if (VALUE_FLAGS.has(name)) {
|
|
@@ -22030,7 +22197,7 @@ function parseBackfillArgs(argv) {
|
|
|
22030
22197
|
}
|
|
22031
22198
|
return {
|
|
22032
22199
|
kind: "error",
|
|
22033
|
-
message: `unknown option '${arg}'. Known: ${[...BOOLEAN_FLAGS, ...VALUE_FLAGS].map((f) => `--${f}`).sort().join(", ")}, --help`
|
|
22200
|
+
message: `unknown option '${arg}'. Known: ${[...BOOLEAN_FLAGS.keys(), ...VALUE_FLAGS].map((f) => `--${f}`).sort().join(", ")}, --help`
|
|
22034
22201
|
};
|
|
22035
22202
|
}
|
|
22036
22203
|
const positive = (name) => {
|
|
@@ -22043,6 +22210,12 @@ function parseBackfillArgs(argv) {
|
|
|
22043
22210
|
}
|
|
22044
22211
|
return n;
|
|
22045
22212
|
};
|
|
22213
|
+
if (booleans.dryRun && booleans.reset) {
|
|
22214
|
+
return {
|
|
22215
|
+
kind: "error",
|
|
22216
|
+
message: "--reset cannot be combined with --dry-run: a dry run maps every transcript from the start already, and never writes checkpoints"
|
|
22217
|
+
};
|
|
22218
|
+
}
|
|
22046
22219
|
const sinceDays = positive("days");
|
|
22047
22220
|
if (typeof sinceDays === "object")
|
|
22048
22221
|
return sinceDays;
|
|
@@ -22052,7 +22225,8 @@ function parseBackfillArgs(argv) {
|
|
|
22052
22225
|
return {
|
|
22053
22226
|
kind: "run",
|
|
22054
22227
|
options: {
|
|
22055
|
-
dryRun,
|
|
22228
|
+
dryRun: booleans.dryRun,
|
|
22229
|
+
reset: booleans.reset,
|
|
22056
22230
|
paths,
|
|
22057
22231
|
...values.get("profile") !== void 0 ? { profile: values.get("profile") } : {},
|
|
22058
22232
|
...values.get("credentials") !== void 0 ? { credentialsPath: values.get("credentials") } : {},
|
|
@@ -22068,10 +22242,21 @@ function batches(items, size) {
|
|
|
22068
22242
|
return out;
|
|
22069
22243
|
}
|
|
22070
22244
|
async function runBackfill(mapper, options, say2) {
|
|
22245
|
+
if (!options.dryRun && backfillIsOff(mapper, options)) {
|
|
22246
|
+
say2("BACKFILL DISABLED \u2014 the retroactive import is opt-in, and this machine has not opted in");
|
|
22247
|
+
say2(`set "capture": { "backfill": true } in ${configPath()} to allow it`);
|
|
22248
|
+
say2("nothing was sent and no checkpoint was cleared; live capture is unaffected");
|
|
22249
|
+
return disabledResult();
|
|
22250
|
+
}
|
|
22071
22251
|
const discovered = options.paths.length > 0 ? options.paths : discoverTranscripts(mapper);
|
|
22072
22252
|
const paths = options.sinceDays === void 0 ? discovered : withinDays(discovered, options.sinceDays);
|
|
22073
22253
|
const window2 = options.sinceDays === void 0 ? "" : ` written to in the last ${options.sinceDays} day(s)`;
|
|
22074
22254
|
say2(`${options.dryRun ? "DRY RUN \u2014 nothing will be sent. " : ""}${paths.length} transcript(s)${window2}`);
|
|
22255
|
+
if (options.reset) {
|
|
22256
|
+
const reset = await resetCheckpoints(mapper, options, paths, say2);
|
|
22257
|
+
if (reset === void 0)
|
|
22258
|
+
return lockBusyResult();
|
|
22259
|
+
}
|
|
22075
22260
|
const pass = (subset) => runForwarder(mapper, {
|
|
22076
22261
|
paths: subset,
|
|
22077
22262
|
dryRun: options.dryRun,
|
|
@@ -22088,6 +22273,61 @@ async function runBackfill(mapper, options, say2) {
|
|
|
22088
22273
|
return pass([]);
|
|
22089
22274
|
return runPasses(batches(paths, options.batch ?? DEFAULT_BATCH), pass, say2);
|
|
22090
22275
|
}
|
|
22276
|
+
async function resetCheckpoints(mapper, options, paths, say2) {
|
|
22277
|
+
const { scope } = loadRuntimeSettings(mapper.vendor, options.profile !== void 0 ? { profile: options.profile } : {});
|
|
22278
|
+
const release = await acquireLockWithin(scope, options.lockWaitMs ?? DEFAULT_LOCK_WAIT_MS);
|
|
22279
|
+
if (!release)
|
|
22280
|
+
return void 0;
|
|
22281
|
+
let reset;
|
|
22282
|
+
try {
|
|
22283
|
+
reset = clearCheckpoints(scope, paths);
|
|
22284
|
+
} finally {
|
|
22285
|
+
release();
|
|
22286
|
+
}
|
|
22287
|
+
if (reset.cleared.length === 0) {
|
|
22288
|
+
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`);
|
|
22289
|
+
return reset;
|
|
22290
|
+
}
|
|
22291
|
+
say2(`RESET \u2014 forgot the offset for ${reset.cleared.length} transcript(s); they re-ship in full`);
|
|
22292
|
+
say2(`previous checkpoints saved as ${reset.backup}`);
|
|
22293
|
+
return reset;
|
|
22294
|
+
}
|
|
22295
|
+
function backfillIsOff(mapper, options) {
|
|
22296
|
+
try {
|
|
22297
|
+
const { capture } = loadRuntimeSettings(mapper.vendor, options.profile !== void 0 ? { profile: options.profile } : {});
|
|
22298
|
+
return !capture.backfill;
|
|
22299
|
+
} catch {
|
|
22300
|
+
return false;
|
|
22301
|
+
}
|
|
22302
|
+
}
|
|
22303
|
+
function disabledResult() {
|
|
22304
|
+
return {
|
|
22305
|
+
transcripts: 0,
|
|
22306
|
+
records: 0,
|
|
22307
|
+
skipped: ["backfill is disabled"],
|
|
22308
|
+
kinds: {},
|
|
22309
|
+
warnings: [],
|
|
22310
|
+
emitted: { transcripts: 0, records: 0 },
|
|
22311
|
+
backfillDisabled: true,
|
|
22312
|
+
// Non-zero exit, for the reason `shipped 0` cannot carry: an up-to-date machine
|
|
22313
|
+
// prints that too, and a script chaining backfills must not read a refusal as done.
|
|
22314
|
+
ok: false,
|
|
22315
|
+
error: "backfill is disabled by capture.backfill"
|
|
22316
|
+
};
|
|
22317
|
+
}
|
|
22318
|
+
function lockBusyResult() {
|
|
22319
|
+
return {
|
|
22320
|
+
transcripts: 0,
|
|
22321
|
+
records: 0,
|
|
22322
|
+
skipped: ["another forwarder holds the lock"],
|
|
22323
|
+
kinds: {},
|
|
22324
|
+
warnings: [],
|
|
22325
|
+
emitted: { transcripts: 0, records: 0 },
|
|
22326
|
+
lockBusy: true,
|
|
22327
|
+
ok: false,
|
|
22328
|
+
error: "another forwarder holds the lock"
|
|
22329
|
+
};
|
|
22330
|
+
}
|
|
22091
22331
|
async function runPasses(groups, pass, say2) {
|
|
22092
22332
|
const total = {
|
|
22093
22333
|
transcripts: 0,
|
|
@@ -22126,8 +22366,8 @@ async function runPasses(groups, pass, say2) {
|
|
|
22126
22366
|
}
|
|
22127
22367
|
|
|
22128
22368
|
// adapters/codex/dist/transcript.js
|
|
22129
|
-
import { readFileSync as
|
|
22130
|
-
import { basename, join as
|
|
22369
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
22370
|
+
import { basename, join as join11 } from "node:path";
|
|
22131
22371
|
import { homedir as homedir4 } from "node:os";
|
|
22132
22372
|
function str(value) {
|
|
22133
22373
|
return typeof value === "string" ? value : void 0;
|
|
@@ -22271,7 +22511,7 @@ function stripSuffix(path) {
|
|
|
22271
22511
|
var codexTranscript = {
|
|
22272
22512
|
vendor: "codex",
|
|
22273
22513
|
sessionRoots() {
|
|
22274
|
-
return [
|
|
22514
|
+
return [join11(homedir4(), ".codex", "sessions")];
|
|
22275
22515
|
},
|
|
22276
22516
|
/**
|
|
22277
22517
|
* `~/.codex/auth.json` -> the `email` claim of `tokens.id_token`.
|
|
@@ -22293,7 +22533,7 @@ var codexTranscript = {
|
|
|
22293
22533
|
*/
|
|
22294
22534
|
resolveUserId() {
|
|
22295
22535
|
try {
|
|
22296
|
-
return emailFromAuth(
|
|
22536
|
+
return emailFromAuth(readFileSync7(join11(homedir4(), ".codex", "auth.json"), "utf8"));
|
|
22297
22537
|
} catch {
|
|
22298
22538
|
return void 0;
|
|
22299
22539
|
}
|
|
@@ -22517,6 +22757,7 @@ if (parsed.kind === "error") {
|
|
|
22517
22757
|
process.exit(2);
|
|
22518
22758
|
}
|
|
22519
22759
|
var result = await runBackfill(codexTranscript, parsed.options, say);
|
|
22760
|
+
if (result.backfillDisabled === true) process.exit(4);
|
|
22520
22761
|
say(
|
|
22521
22762
|
`${parsed.options.dryRun ? "mapped" : "shipped"} ${result.records} record(s) from ${result.transcripts} transcript(s)`
|
|
22522
22763
|
);
|