@bli-cockpit/cli 0.2.286 → 0.2.287
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capture-opt-outs.js +129 -0
- package/dist/collection-scope.js +201 -0
- package/dist/commands/agent-session-report.js +3 -0
- package/dist/commands/analyze.js +2 -0
- package/dist/commands/backfill-scan.js +16 -11
- package/dist/commands/backfill-scope.js +16 -4
- package/dist/commands/backfill-session-report.js +2 -0
- package/dist/commands/cli-io.js +1 -3
- package/dist/commands/doctor-access.js +7 -0
- package/dist/commands/doctor-registration.js +39 -0
- package/dist/commands/doctor-report.js +1 -0
- package/dist/commands/doctor-shelf.js +62 -0
- package/dist/commands/doctor-update.js +2 -0
- package/dist/commands/doctor-usage-telemetry.js +18 -2
- package/dist/commands/doctor.js +9 -0
- package/dist/commands/heartbeat-post.js +19 -0
- package/dist/commands/heartbeat.js +13 -2
- package/dist/commands/install-receipts.js +4 -0
- package/dist/commands/local-args-collector-setup.js +2 -0
- package/dist/commands/local-args-collector-status.js +7 -21
- package/dist/commands/local-args.js +2 -0
- package/dist/commands/local-discovery.js +2 -2
- package/dist/commands/local-help-commands-tower.js +6 -1
- package/dist/commands/local-help-commands.js +15 -2
- package/dist/commands/local-help.js +4 -2
- package/dist/commands/local.js +7 -0
- package/dist/commands/memory-capture-prompt.js +130 -0
- package/dist/commands/memory-install-claude.js +51 -4
- package/dist/commands/memory-install-report.js +19 -3
- package/dist/commands/memory-install-toml.js +22 -0
- package/dist/commands/memory-install.js +110 -11
- package/dist/commands/memory-store.js +5 -0
- package/dist/commands/memory-switch.js +165 -0
- package/dist/commands/memory-uninstall.js +187 -0
- package/dist/commands/memory-verb-list.js +6 -2
- package/dist/commands/onboard.js +5 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/roots-label.js +85 -0
- package/dist/commands/roots.js +28 -10
- package/dist/commands/session-sync-plan.js +8 -4
- package/dist/commands/session-sync-scan.js +25 -3
- package/dist/commands/session-sync-shelf.js +143 -0
- package/dist/commands/session-sync-upload.js +6 -2
- package/dist/commands/session-sync.js +68 -8
- package/dist/commands/shelf.js +165 -0
- package/dist/commands/sources-check.js +2 -2
- package/dist/commands/sources-reconciliation.js +2 -1
- package/dist/commands/sync-doctor.js +2 -1
- package/dist/commands/sync-receipt.js +1 -0
- package/dist/commands/sync-report.js +1 -0
- package/dist/commands/sync-stub-heal.js +2 -2
- package/dist/commands/sync.js +12 -0
- package/dist/error-message.js +11 -0
- package/dist/evidence-reconcile-client.js +2 -0
- package/dist/evidence-redelivery-plan.js +6 -0
- package/dist/evidence-redelivery.js +6 -1
- package/dist/evidence-stubs-client.js +2 -0
- package/dist/evidence-transcripts-client.js +2 -0
- package/dist/evidence-upload-transport.js +2 -0
- package/dist/founder-standing-file.js +52 -0
- package/dist/founder-standing.js +117 -0
- package/dist/image-describe-client.js +2 -1
- package/dist/live-founder-memo.js +57 -0
- package/dist/local-state-config.js +13 -7
- package/dist/memory-capture-choice.js +73 -0
- package/dist/root-work.js +55 -7
- package/dist/shelf-decision.js +52 -0
- package/dist/shelf-headers.js +15 -0
- package/dist/shelf-reports.js +167 -0
- package/dist/shelf-state.js +112 -0
- package/dist/source-opt-out.js +83 -6
- package/dist/state-dir-audit.js +2 -0
- package/dist/tower-client.js +4 -1
- package/dist/upload-agent-artifacts.js +2 -0
- package/dist/upload-envelope-build.js +2 -2
- package/dist/upload-ingest-receipt.js +2 -0
- package/dist/upload-session-reports-wire.js +2 -0
- package/package.json +4 -4
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit shelf …` (BLI-23478, three-shelves design 9.1 item 7): a founder's shelf settings on THIS laptop.
|
|
3
|
+
*
|
|
4
|
+
* cockpit shelf status the lock, the default, and what is in the drawer here
|
|
5
|
+
* cockpit shelf default team|drawer where work outside every labelled folder goes
|
|
6
|
+
* cockpit shelf metrics --allow day,tool,model,spawn|none
|
|
7
|
+
* the opt-in categories on a drawer session's bare record
|
|
8
|
+
* cockpit shelf drawer purge --older-than <days>
|
|
9
|
+
* the ONLY way drawer items are removed
|
|
10
|
+
*
|
|
11
|
+
* A machine command: it acts on this laptop and calls no Tower door. `safe` is refused everywhere until the safe is
|
|
12
|
+
* live (`safe_not_live`); `default drawer` is refused on a laptop Tower has not named an owner's.
|
|
13
|
+
*
|
|
14
|
+
* Logs carry labels and counts only.
|
|
15
|
+
*/
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import { safeLog, SHELF_METRIC_CATEGORIES } from "@bli-cockpit/telemetry-core";
|
|
18
|
+
import { listDrawerItems, purgeDrawerItems } from "@bli-cockpit/memory-mcp/dist/drawer-spool.js";
|
|
19
|
+
import { ensureLocalCollectorConfig, getCollectorRuntimePaths, readLocalCollectorConfig } from "../local-state.js";
|
|
20
|
+
import { writeJsonFile } from "../local-state-files.js";
|
|
21
|
+
import { readRootLabels } from "../root-work.js";
|
|
22
|
+
import { countShelvedSessions } from "../shelf-reports.js";
|
|
23
|
+
import { readShelfState } from "../shelf-state.js";
|
|
24
|
+
import { askFounderLive, OWNER_CHECK_UNAVAILABLE } from "../shelf-decision.js";
|
|
25
|
+
import { configForDisk } from "../source-opt-out.js";
|
|
26
|
+
import { writeLine } from "./cli-io.js";
|
|
27
|
+
const TAG = "shelf";
|
|
28
|
+
export const SHELF_USAGE = "Usage: cockpit shelf status | default team|drawer | metrics --allow <day,tool,model,spawn|none> | drawer purge --older-than <days> [--json]";
|
|
29
|
+
export function parseShelfArgs(args) {
|
|
30
|
+
const positional = [];
|
|
31
|
+
let homeDir;
|
|
32
|
+
let allow;
|
|
33
|
+
let olderThan;
|
|
34
|
+
let json = false;
|
|
35
|
+
for (let index = 0; index < args.length; index++) {
|
|
36
|
+
const token = args[index];
|
|
37
|
+
if (token === "--json") {
|
|
38
|
+
json = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (token === "--home-dir" && args[index + 1]) {
|
|
42
|
+
homeDir = args[++index];
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (token === "--allow" && args[index + 1]) {
|
|
46
|
+
allow = args[++index];
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (token === "--older-than" && args[index + 1]) {
|
|
50
|
+
olderThan = args[++index];
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (token.startsWith("--"))
|
|
54
|
+
throw new Error(`Unknown shelf flag: ${token}`);
|
|
55
|
+
positional.push(token);
|
|
56
|
+
}
|
|
57
|
+
const [action, value, extra] = positional;
|
|
58
|
+
if (action === "status" && !value)
|
|
59
|
+
return { kind: "shelf", action, homeDir, json };
|
|
60
|
+
if (action === "default" && value && !extra)
|
|
61
|
+
return { kind: "shelf", action, shelf: value, homeDir, json };
|
|
62
|
+
if (action === "metrics" && !value && allow !== undefined) {
|
|
63
|
+
const words = allow.split(",").map((word) => word.trim()).filter(Boolean);
|
|
64
|
+
const chosen = words.length === 1 && words[0] === "none" ? [] : words;
|
|
65
|
+
const unknown = chosen.filter((word) => !SHELF_METRIC_CATEGORIES.includes(word));
|
|
66
|
+
if (unknown.length > 0 || chosen.length === 0 && words[0] !== "none")
|
|
67
|
+
throw new Error(SHELF_USAGE);
|
|
68
|
+
return { kind: "shelf", action, allow: [...new Set(chosen)], homeDir, json };
|
|
69
|
+
}
|
|
70
|
+
if (action === "drawer" && value === "purge" && !extra && olderThan !== undefined) {
|
|
71
|
+
const days = Number(olderThan.replace(/d$/u, ""));
|
|
72
|
+
if (!Number.isInteger(days) || days < 0)
|
|
73
|
+
throw new Error("--older-than takes a whole number of days, e.g. 30 or 30d.");
|
|
74
|
+
return { kind: "shelf", action: "purge", olderThanDays: days, homeDir, json };
|
|
75
|
+
}
|
|
76
|
+
throw new Error(SHELF_USAGE);
|
|
77
|
+
}
|
|
78
|
+
export async function runShelf(command, io) {
|
|
79
|
+
const homeDir = command.homeDir ?? os.homedir();
|
|
80
|
+
if (command.action === "status")
|
|
81
|
+
return shelfStatus(homeDir, command.json, io);
|
|
82
|
+
if (command.action === "purge") {
|
|
83
|
+
const result = purgeDrawerItems(command.olderThanDays, { homeDir });
|
|
84
|
+
writeLine(io.stdout, command.json
|
|
85
|
+
? JSON.stringify({ ok: result.failed === 0, ...result })
|
|
86
|
+
: `Removed ${result.removed} drawer item(s) older than ${command.olderThanDays} days; ${result.kept} kept${result.failed ? `; ${result.failed} could not be removed` : ""}.`);
|
|
87
|
+
return result.failed === 0 ? 0 : 1;
|
|
88
|
+
}
|
|
89
|
+
const { config, paths } = await ensureLocalCollectorConfig({ homeDir });
|
|
90
|
+
if (command.action === "metrics") {
|
|
91
|
+
await writeJsonFile(paths.config_file, configForDisk({ ...config, shelf_metrics_allow: command.allow }));
|
|
92
|
+
safeLog(TAG, "shelf_metrics_allow_set", { categories: command.allow.join(",") || "none" });
|
|
93
|
+
writeLine(io.stdout, command.json
|
|
94
|
+
? JSON.stringify({ ok: true, shelf_metrics_allow: command.allow })
|
|
95
|
+
: `Bare records from this laptop now carry: the always-sent numbers${command.allow.length ? ` plus ${command.allow.join(", ")}` : " only"}.`);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const refuse = (reason, message) => {
|
|
99
|
+
safeLog(TAG, "shelf_default_refused", { refused: reason });
|
|
100
|
+
writeLine(command.json ? io.stdout : io.stderr, command.json ? JSON.stringify({ ok: false, reason, message }) : message);
|
|
101
|
+
return 1;
|
|
102
|
+
};
|
|
103
|
+
if (command.shelf === "safe")
|
|
104
|
+
return refuse("safe_not_live", "The safe is not live on this laptop yet. Use drawer: it stays on this laptop.");
|
|
105
|
+
if (command.shelf !== "team" && command.shelf !== "drawer")
|
|
106
|
+
return refuse("unknown_shelf", "The default is team or drawer.");
|
|
107
|
+
// Tower is asked live (never the saved heartbeat copy): only a founder may make the drawer the default.
|
|
108
|
+
const founder = command.shelf === "drawer" ? await askFounderLive({ homeDir, fetchImpl: io.fetch }) : null;
|
|
109
|
+
if (founder?.standing === "unknown")
|
|
110
|
+
return refuse(OWNER_CHECK_UNAVAILABLE, "Tower could not be asked whether this is a founder's laptop, so nothing changed. Try again shortly.");
|
|
111
|
+
if (founder && founder.standing !== "founder")
|
|
112
|
+
return refuse("shelf_owner_only", "The drawer is for founders' laptops, and Tower says this is not one.");
|
|
113
|
+
await writeJsonFile(paths.config_file, configForDisk({ ...config, shelf_default: command.shelf }));
|
|
114
|
+
safeLog(TAG, "shelf_default_set", { shelf: command.shelf });
|
|
115
|
+
writeLine(io.stdout, command.json
|
|
116
|
+
? JSON.stringify({ ok: true, shelf_default: command.shelf })
|
|
117
|
+
: `Work outside every labelled folder now goes to: ${command.shelf}.`);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
async function shelfStatus(homeDir, json, io) {
|
|
121
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
122
|
+
const state = readShelfState(homeDir);
|
|
123
|
+
const [labels, sessions, config] = await Promise.all([
|
|
124
|
+
readRootLabels(paths).catch(() => ({})),
|
|
125
|
+
countShelvedSessions(paths),
|
|
126
|
+
// Read only: status never creates a config on a laptop that has none.
|
|
127
|
+
readLocalCollectorConfig(paths).catch(() => null),
|
|
128
|
+
]);
|
|
129
|
+
const drawer = listDrawerItems({ homeDir });
|
|
130
|
+
const oldest = [sessions.oldest, drawer.oldest].filter((value) => Boolean(value)).sort()[0] ?? null;
|
|
131
|
+
const status = {
|
|
132
|
+
is_owner: state?.is_owner ?? null,
|
|
133
|
+
founder_routing_enabled: state?.founder_routing_enabled ?? null,
|
|
134
|
+
shelf_enforced: state?.shelf_enforced ?? null,
|
|
135
|
+
cli_too_old: state?.cli_too_old ?? null,
|
|
136
|
+
lock_checked_at: state?.checked_at ?? null,
|
|
137
|
+
shelf_default: config?.shelf_default ?? "team",
|
|
138
|
+
shelf_metrics_allow: config?.shelf_metrics_allow ?? [],
|
|
139
|
+
labelled_folders: Object.keys(labels).length,
|
|
140
|
+
drawer_folders: Object.values(labels).filter((label) => label !== "team").length,
|
|
141
|
+
drawer_sessions: sessions.sessions,
|
|
142
|
+
drawer_memory_items: drawer.items.length,
|
|
143
|
+
drawer_corrupt_items: drawer.corrupt,
|
|
144
|
+
drawer_oldest: oldest,
|
|
145
|
+
drawer_backed_up: false,
|
|
146
|
+
};
|
|
147
|
+
safeLog(TAG, "shelf_status_read", {
|
|
148
|
+
drawer_sessions: status.drawer_sessions,
|
|
149
|
+
drawer_memory_items: status.drawer_memory_items,
|
|
150
|
+
drawer_corrupt_items: status.drawer_corrupt_items,
|
|
151
|
+
labelled_folders: status.labelled_folders,
|
|
152
|
+
});
|
|
153
|
+
if (json) {
|
|
154
|
+
writeLine(io.stdout, JSON.stringify({ ok: true, ...status }));
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
const owner = status.is_owner === true ? "a founder's laptop" : status.is_owner === false ? "not a founder's laptop" : "not yet checked in with Tower";
|
|
158
|
+
writeLine(io.stdout, `Shelves on this laptop (${owner}).`);
|
|
159
|
+
writeLine(io.stdout, ` Tower's founder lock: ${status.founder_routing_enabled === true ? "on" : status.founder_routing_enabled === false ? "off" : "unknown"}${status.cli_too_old ? " (this CLI is too old; run cockpit update)" : ""}.`);
|
|
160
|
+
writeLine(io.stdout, ` Outside every labelled folder: ${status.shelf_default}. Labelled folders: ${status.labelled_folders} (${status.drawer_folders} drawer).`);
|
|
161
|
+
writeLine(io.stdout, ` ${status.drawer_sessions} sessions and ${status.drawer_memory_items} memory items in the drawer on this laptop; not backed up; oldest ${oldest ? oldest.slice(0, 10) : "none"}.`);
|
|
162
|
+
if (status.drawer_corrupt_items > 0)
|
|
163
|
+
writeLine(io.stdout, ` ${status.drawer_corrupt_items} drawer file(s) could not be read; they are kept and never sent.`);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* "not stored". A list cut short (`capped`, `failed_partial`) still answers,
|
|
20
20
|
* marked incomplete: a missing page can only understate what Tower holds.
|
|
21
21
|
*/
|
|
22
|
-
import {
|
|
22
|
+
import { readSourceSwitches } from "../source-opt-out.js";
|
|
23
23
|
import { fetchSourceInventory, reconcileSource } from "./sources-reconciliation.js";
|
|
24
24
|
import { writeLine } from "./cli-io.js";
|
|
25
25
|
import { readDescribedImageKeys } from "../image-description-state.js";
|
|
@@ -52,7 +52,7 @@ export async function runSourcesCheck(command, io, now = new Date()) {
|
|
|
52
52
|
}
|
|
53
53
|
const homeDir = command.homeDir ?? paths.home_dir;
|
|
54
54
|
const olderThanMs = command.olderThanDays === null ? null : now.getTime() - command.olderThanDays * DAY_MS;
|
|
55
|
-
const switches =
|
|
55
|
+
const switches = await readSourceSwitches(config, paths, "sources_check", { fetch: io.fetch });
|
|
56
56
|
const optOutClaude = command.reconcile === true && !switches.claude;
|
|
57
57
|
const optOutCodex = command.reconcile === true && !switches.codex;
|
|
58
58
|
const optOutAll = optOutClaude && optOutCodex;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Metadata reconciliation only. Never writes cursors, starts collection or fetches bodies. */
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { collectorShelfHeaders } from "../shelf-headers.js";
|
|
3
4
|
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
4
5
|
export async function fetchSourceInventory(options) {
|
|
5
6
|
const rows = new Map();
|
|
@@ -9,7 +10,7 @@ export async function fetchSourceInventory(options) {
|
|
|
9
10
|
for (let page = 0; page < (options.maxPages ?? 400); page++) {
|
|
10
11
|
result.requested += 500;
|
|
11
12
|
const response = await options.fetchImpl(`${options.dashboardUrl}/api/ambient/evidence/transcripts?inventory=1`, {
|
|
12
|
-
method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.deviceToken}
|
|
13
|
+
method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.deviceToken}`, ...collectorShelfHeaders() },
|
|
13
14
|
body: JSON.stringify({ schema_version: "evidence-transcripts.v1", after, limit: 500 }),
|
|
14
15
|
signal: AbortSignal.timeout(30_000),
|
|
15
16
|
});
|
|
@@ -15,6 +15,7 @@ import { runSelfUpdate } from "./install-update.js";
|
|
|
15
15
|
import { reportInstallEventsBestEffort } from "./install-receipts.js";
|
|
16
16
|
import { clearDoctorRequest, requestedDoctorId } from "./sync-doctor-request.js";
|
|
17
17
|
import { envWithNodeRuntimeOnPath } from "../scheduled-self-update.js";
|
|
18
|
+
import { collectorShelfHeaders } from "../shelf-headers.js";
|
|
18
19
|
export async function runDoctorAfterSync(command, io, dashboardUrl, overrides = {}, facts) {
|
|
19
20
|
if (shouldSuppressFleetReceipts({ dashboardUrl }).suppressed) {
|
|
20
21
|
console.error("[sync-doctor] checkout does not repair the fleet machine", JSON.stringify({ reason: "receipt_suppressed:dev_build" }));
|
|
@@ -149,7 +150,7 @@ async function postReceipt(receipt, command, io, dashboardUrl) {
|
|
|
149
150
|
}
|
|
150
151
|
try {
|
|
151
152
|
const response = await io.fetch(`${dashboardUrl}/api/ambient/devices/${encodeURIComponent(session.device_id)}/doctor`, {
|
|
152
|
-
method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${session.device_token}
|
|
153
|
+
method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${session.device_token}`, ...collectorShelfHeaders() },
|
|
153
154
|
body: JSON.stringify(receipt), signal: AbortSignal.timeout(5_000),
|
|
154
155
|
});
|
|
155
156
|
console.error("[sync-doctor] receipt delivery", JSON.stringify({ reason: response.ok ? "receipt_recorded" : "receipt_rejected", http_status: response.status, request_id: receipt.request_id }));
|
|
@@ -150,5 +150,6 @@ function heartbeatCounts(run) {
|
|
|
150
150
|
outsideRootDirectories: run.outsideRootDirectories,
|
|
151
151
|
sessionsNewThisTick: run.sessionsNewThisTick,
|
|
152
152
|
sessionsPendingUpload: run.sessionsPendingUpload,
|
|
153
|
+
...(run.collectionScope ? { collectionScope: run.collectionScope } : {}),
|
|
153
154
|
};
|
|
154
155
|
}
|
|
@@ -158,6 +158,7 @@ function syncResult(run) {
|
|
|
158
158
|
outsideRootDirectories: run.outside_root_directories,
|
|
159
159
|
sessionsNewThisTick: run.sessions_new_this_tick,
|
|
160
160
|
sessionsPendingUpload: run.sessions_pending_upload,
|
|
161
|
+
...(run.collection_scope ? { collectionScope: run.collection_scope } : {}),
|
|
161
162
|
};
|
|
162
163
|
}
|
|
163
164
|
/** The local raw-evidence sweep a successful run earns before it reports. */
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* of the operator's machines is not this machine's to heal; it is counted as
|
|
15
15
|
* remaining and skipped without a scan. Never throws and never fails the tick.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { readSourceSwitches } from "../source-opt-out.js";
|
|
18
18
|
import { runBackfill } from "./backfill.js";
|
|
19
19
|
import { loadStubReofferPlan } from "./backfill-stub-reoffer.js";
|
|
20
20
|
import { backfillWaitingCheck } from "./sync-yield.js";
|
|
@@ -62,7 +62,7 @@ async function runStubPass(command, io, deps, now, summary) {
|
|
|
62
62
|
const sessionFile = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
63
63
|
if (!sessionFile)
|
|
64
64
|
return "no_device_session";
|
|
65
|
-
const switches =
|
|
65
|
+
const switches = await readSourceSwitches(config, paths, "stub_heal");
|
|
66
66
|
const sources = [
|
|
67
67
|
...(switches.codex ? ["codex"] : []),
|
|
68
68
|
...(switches.claude ? ["claude_code"] : []),
|
package/dist/commands/sync.js
CHANGED
|
@@ -47,11 +47,13 @@ import { runSyncWithHealthReceipt } from "./sync-receipt.js";
|
|
|
47
47
|
import { runAutostartSelfHealAfterSync, runMemoryInstallAfterSync, runScheduledSelfUpdateAfterSync, runStagingPruneAfterSync, } from "./sync-followups.js";
|
|
48
48
|
import { rotateCollectorLogsBestEffort } from "../log-rotation.js";
|
|
49
49
|
import { settleTickImageModeBestEffort } from "../image-mode-state.js";
|
|
50
|
+
import { settleTickCollectionScopeBestEffort } from "../collection-scope.js";
|
|
50
51
|
import { stampSyncTick } from "../sync-tick-stamp.js";
|
|
51
52
|
import { withCommandGuard } from "../command-guard.js";
|
|
52
53
|
import { installHttp1GlobalFetch } from "../http1-fetch.js";
|
|
53
54
|
import { startTickTimer } from "../sync-tick-timing.js";
|
|
54
55
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
|
|
56
|
+
import { refreshFounderStandingForOptOuts } from "../capture-opt-outs.js";
|
|
55
57
|
/**
|
|
56
58
|
* The tick, timed (BLI-12872): ends with one `[sync] tick finished` line that
|
|
57
59
|
* carries its duration and the time spent in each step, win or throw. The
|
|
@@ -113,6 +115,14 @@ async function reexecuteSync(command, io) {
|
|
|
113
115
|
return result.code;
|
|
114
116
|
}
|
|
115
117
|
async function collectAndReportTick(command, io, tick, dashboardUrl) {
|
|
118
|
+
// BLI-22640: a machine that holds an opt-out asks Tower whether its person is
|
|
119
|
+
// a founder BEFORE anything is collected, so this tick obeys or ignores the
|
|
120
|
+
// opt-out on Tower's current answer. Never throws; asks nothing otherwise.
|
|
121
|
+
await refreshFounderStandingForOptOuts({
|
|
122
|
+
...(command.homeDir ? { homeDir: command.homeDir } : {}),
|
|
123
|
+
dashboardUrl,
|
|
124
|
+
fetch: io.fetch,
|
|
125
|
+
});
|
|
116
126
|
try {
|
|
117
127
|
const result = await tick.collect(() => runSyncWithHealthReceipt(command, io));
|
|
118
128
|
// BLI-3551: every tick checks in, including one that collected nothing.
|
|
@@ -167,6 +177,8 @@ async function reportTickStarted(command, io, dashboardUrl) {
|
|
|
167
177
|
// BLI-17273: the reply also names this machine's image-handling mode. Put it on file BEFORE
|
|
168
178
|
// collection starts (the uploader reads the file); an offline tick keeps the last value on file.
|
|
169
179
|
await settleTickImageModeBestEffort(getCollectorRuntimePaths(command.homeDir).state_dir, observed.imageHandlingMode);
|
|
180
|
+
// BLI-23590: and this person's collection scope. On file BEFORE collection starts, because the scan reads it.
|
|
181
|
+
await settleTickCollectionScopeBestEffort(getCollectorRuntimePaths(command.homeDir).state_dir, observed.collectionScope);
|
|
170
182
|
return minCliVersion;
|
|
171
183
|
}
|
|
172
184
|
/** Post the receipt the run earned, whatever it says. */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-line text of a thrown value. A leaf with no imports on purpose:
|
|
3
|
+
* `tower-client.ts` needs it, and pulling it from `commands/cli-io.ts` dragged
|
|
4
|
+
* the whole CLI import graph (autostart, admin, the argument parsers) into every
|
|
5
|
+
* reader that only wanted Tower's answer, including the dashboard's typecheck.
|
|
6
|
+
*
|
|
7
|
+
* A new sibling must also join `scripts/build-public-cli.mjs` `runtimeFiles`.
|
|
8
|
+
*/
|
|
9
|
+
export function errorMessage(error) {
|
|
10
|
+
return error instanceof Error ? error.message : String(error);
|
|
11
|
+
}
|
|
@@ -23,6 +23,7 @@ import { describeError } from "./health-detail.js";
|
|
|
23
23
|
import { getCollectorRuntimePaths, readLocalCollectorSessionFile } from "./local-state.js";
|
|
24
24
|
import { readReconcileCursor, recordReconcileResult, writeReconcileCursor, } from "./cursors/raw-evidence-reconcile-cursor.js";
|
|
25
25
|
import { readStagingInventory } from "./disk-usage.js";
|
|
26
|
+
import { collectorShelfHeaders } from "./shelf-headers.js";
|
|
26
27
|
const RECONCILE_BATCH_TIMEOUT_MS = 15_000;
|
|
27
28
|
/** The prune the sync tick and `cockpit clean --reconcile` both call. Never throws. */
|
|
28
29
|
export async function runEvidenceReconcile(options) {
|
|
@@ -178,6 +179,7 @@ async function sendReconcileBatch(options) {
|
|
|
178
179
|
headers: {
|
|
179
180
|
"Content-Type": "application/json",
|
|
180
181
|
Authorization: `Bearer ${options.deviceToken}`,
|
|
182
|
+
...collectorShelfHeaders(),
|
|
181
183
|
},
|
|
182
184
|
body: JSON.stringify({
|
|
183
185
|
schema_version: EVIDENCE_RECONCILE_SCHEMA_VERSION,
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import fs from "node:fs/promises";
|
|
35
35
|
import path from "node:path";
|
|
36
|
+
import { COLLECTION_SCOPE_ROOTS_ONLY_REASON } from "@bli-cockpit/telemetry-core";
|
|
36
37
|
import { RAW_EVIDENCE_BUCKET, RAW_EVIDENCE_RETENTION_MODE, } from "./adapters/raw-evidence-manifest.js";
|
|
37
38
|
import { RAW_EVIDENCE_DIR } from "./disk-usage.js";
|
|
38
39
|
import { describeError } from "./health-detail.js";
|
|
@@ -107,6 +108,11 @@ export async function planEvidenceRedelivery(options) {
|
|
|
107
108
|
countUnplannable(plan, "manifest_unreadable");
|
|
108
109
|
continue;
|
|
109
110
|
}
|
|
111
|
+
if (options.withholdRepoLabels &&
|
|
112
|
+
[manifest.provenance.repo, manifest.provenance.repo_label].some((label) => label && options.withholdRepoLabels.has(label))) {
|
|
113
|
+
countUnplannable(plan, COLLECTION_SCOPE_ROOTS_ONLY_REASON);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
110
116
|
const row = manifest.files.get(object.relative_path);
|
|
111
117
|
if (!row) {
|
|
112
118
|
countUnplannable(plan, "no_manifest_row");
|
|
@@ -27,7 +27,9 @@ import { planEvidenceRedelivery, } from "./evidence-redelivery-plan.js";
|
|
|
27
27
|
import { markObjectCommitted, readRawEvidenceCursor, writeRawEvidenceCursor, } from "./cursors/raw-evidence-cursor.js";
|
|
28
28
|
import { readStagingInventory } from "./disk-usage.js";
|
|
29
29
|
import { describeError } from "./health-detail.js";
|
|
30
|
-
import { getCollectorRuntimePaths, readLocalCollectorSessionFile, } from "./local-state.js";
|
|
30
|
+
import { getCollectorRuntimePaths, readLocalCollectorConfig, readLocalCollectorSessionFile, } from "./local-state.js";
|
|
31
|
+
import { settleCollectionScope } from "./collection-scope.js";
|
|
32
|
+
import { OUTSIDE_ROOTS_WORKSPACE_LABEL } from "./commands/backfill-outside-roots.js";
|
|
31
33
|
import { clearDeliveryAttempt, readRawEvidenceStagingState, recordDeliveryFailure, terminalDeliveryReason, writeRawEvidenceStagingState, } from "./raw-evidence-staging.js";
|
|
32
34
|
/**
|
|
33
35
|
* The drain the sync tick runs and `cockpit doctor`'s disk fix reuses.
|
|
@@ -49,11 +51,14 @@ export async function runEvidenceRedelivery(options) {
|
|
|
49
51
|
return { ...emptyResult("nothing_uncommitted", "ok"), terminal: inventory.terminal_count ?? 0, terminal_bytes: inventory.terminal_bytes ?? 0 };
|
|
50
52
|
}
|
|
51
53
|
const staging = await readRawEvidenceStagingState(paths.state_dir);
|
|
54
|
+
// BLI-23590: a person on `roots_only` never re-offers what was staged for an outside-root session.
|
|
55
|
+
const scope = await settleCollectionScope(paths.state_dir, (await readLocalCollectorConfig(paths).catch(() => null))?.collection_scope);
|
|
52
56
|
const plan = await planEvidenceRedelivery({
|
|
53
57
|
stateDir: paths.state_dir,
|
|
54
58
|
inventory,
|
|
55
59
|
staging,
|
|
56
60
|
now,
|
|
61
|
+
...(scope.scope === "roots_only" ? { withholdRepoLabels: new Set([OUTSIDE_ROOTS_WORKSPACE_LABEL]) } : {}),
|
|
57
62
|
...(options.maxBytes != null ? { maxBytes: options.maxBytes } : {}),
|
|
58
63
|
...(options.maxObjects != null ? { maxObjects: options.maxObjects } : {}),
|
|
59
64
|
});
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { EVIDENCE_STUBS_MAX_PAGE, EVIDENCE_STUBS_SCHEMA_VERSION, EvidenceStubsResponseSchema, transcriptSessionFromObjectKey, } from "@bli-cockpit/telemetry-core";
|
|
22
22
|
import { describeError } from "./health-detail.js";
|
|
23
|
+
import { collectorShelfHeaders } from "./shelf-headers.js";
|
|
23
24
|
const STUBS_PAGE_TIMEOUT_MS = 15_000;
|
|
24
25
|
export const STUBS_DEFAULT_MAX_PAGES = 20;
|
|
25
26
|
export function stubSessionKey(source, sessionId) {
|
|
@@ -91,6 +92,7 @@ async function requestPage(options) {
|
|
|
91
92
|
headers: {
|
|
92
93
|
"Content-Type": "application/json",
|
|
93
94
|
Authorization: `Bearer ${options.deviceToken}`,
|
|
95
|
+
...collectorShelfHeaders(),
|
|
94
96
|
},
|
|
95
97
|
body: JSON.stringify({
|
|
96
98
|
schema_version: EVIDENCE_STUBS_SCHEMA_VERSION,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { EVIDENCE_TRANSCRIPTS_MAX_PAGE, EVIDENCE_TRANSCRIPTS_SCHEMA_VERSION, EvidenceTranscriptsResponseSchema, transcriptFileFromObjectKey, transcriptFileKey, } from "@bli-cockpit/telemetry-core";
|
|
16
16
|
import { describeError } from "./health-detail.js";
|
|
17
|
+
import { collectorShelfHeaders } from "./shelf-headers.js";
|
|
17
18
|
const TRANSCRIPTS_PAGE_TIMEOUT_MS = 30_000;
|
|
18
19
|
/** 400 pages of 500 is 200,000 current transcript objects: far past any operator today. */
|
|
19
20
|
export const TRANSCRIPTS_DEFAULT_MAX_PAGES = 400;
|
|
@@ -95,6 +96,7 @@ async function requestPage(options) {
|
|
|
95
96
|
headers: {
|
|
96
97
|
"Content-Type": "application/json",
|
|
97
98
|
Authorization: `Bearer ${options.deviceToken}`,
|
|
99
|
+
...collectorShelfHeaders(),
|
|
98
100
|
},
|
|
99
101
|
body: JSON.stringify({
|
|
100
102
|
schema_version: EVIDENCE_TRANSCRIPTS_SCHEMA_VERSION,
|
|
@@ -4,6 +4,7 @@ import { localEncodings } from "./evidence-upload-encoding.js";
|
|
|
4
4
|
import { describeError } from "./health-detail.js";
|
|
5
5
|
import { fetchWithDeadline, isUploadDeadlineError, uploadDeadlineMs } from "./evidence-upload-deadline.js";
|
|
6
6
|
import { noteSyncProgress } from "./sync-lock.js";
|
|
7
|
+
import { collectorShelfHeaders } from "./shelf-headers.js";
|
|
7
8
|
/**
|
|
8
9
|
* The `send` stage: one retrying POST to an evidence-upload route, plus the
|
|
9
10
|
* response-body plumbing every later stage reads a reason off of.
|
|
@@ -27,6 +28,7 @@ export async function requestJson(options, routePath, body) {
|
|
|
27
28
|
headers: {
|
|
28
29
|
"Authorization": `Bearer ${options.deviceToken}`,
|
|
29
30
|
"Content-Type": "application/json",
|
|
31
|
+
...collectorShelfHeaders(),
|
|
30
32
|
...(routePath.endsWith("/evidence/upload/begin")
|
|
31
33
|
? { "x-tower-evidence-direct": "staged-gzip-v1", [EVIDENCE_CODECS_HEADER]: localEncodings().join(",") } : {}),
|
|
32
34
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FOUNDER_STANDING_FILE_NAME, parseFounderStanding, renderFounderStanding, } from "@bli-cockpit/telemetry-core";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function standingPath(paths) {
|
|
5
|
+
return path.join(paths.state_dir, FOUNDER_STANDING_FILE_NAME);
|
|
6
|
+
}
|
|
7
|
+
/** The last answer Tower gave this machine. Never throws: no file, or an unreadable one, is `unknown`. */
|
|
8
|
+
export async function readFounderStanding(paths) {
|
|
9
|
+
let raw;
|
|
10
|
+
try {
|
|
11
|
+
raw = await fs.readFile(standingPath(paths), "utf8");
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const code = error?.code;
|
|
15
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
16
|
+
console.error("[founder-standing] standing file unreadable; treating it as unknown", JSON.stringify({ reason: "standing_unreadable", error_code: code ?? "unknown" }));
|
|
17
|
+
return { standing: "unknown", reason: "standing_unreadable", record: null };
|
|
18
|
+
}
|
|
19
|
+
raw = null;
|
|
20
|
+
}
|
|
21
|
+
return parseFounderStanding(raw);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Records one Tower answer. Never throws: a reader then sees the previous
|
|
25
|
+
* answer, and this line says why the new one did not land. Logs both branches,
|
|
26
|
+
* and says when the answer changed.
|
|
27
|
+
*/
|
|
28
|
+
export async function recordFounderStanding(paths, founder, source, now = new Date()) {
|
|
29
|
+
const before = await readFounderStanding(paths);
|
|
30
|
+
try {
|
|
31
|
+
await fs.mkdir(paths.state_dir, { recursive: true, mode: 0o700 });
|
|
32
|
+
// A file a person made read-only would keep a stale answer; take the mode
|
|
33
|
+
// back first (no-op when it is not there). Both macOS and Windows honour it.
|
|
34
|
+
await fs.chmod(standingPath(paths), 0o600).catch(() => undefined);
|
|
35
|
+
await fs.writeFile(standingPath(paths), renderFounderStanding({ founder, checked_at: now.toISOString(), source }), { encoding: "utf8", mode: 0o600 });
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error("[founder-standing] Tower's answer could not be recorded; the previous one stands", JSON.stringify({
|
|
39
|
+
reason: "standing_write_failed",
|
|
40
|
+
source,
|
|
41
|
+
error_code: error?.code ?? "unknown",
|
|
42
|
+
}));
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const standing = founder ? "founder" : "not_founder";
|
|
46
|
+
const changed = before.standing !== standing;
|
|
47
|
+
// Every tick writes this; only a change is worth a line on an ordinary tick.
|
|
48
|
+
if (changed || source === "standing_door") {
|
|
49
|
+
console.error("[founder-standing] recorded Tower's answer", JSON.stringify({ reason: founder ? "tower_says_founder" : "tower_says_not_founder", source, changed, previous: before.standing }));
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { FOUNDER_CHECK_UNAVAILABLE, FOUNDERS_ONLY_REFUSAL, FOUNDER_STANDING_PATH, founderAnswerFromReply, founderAnswerNotPaired, founderAnswerUnreachable, } from "@bli-cockpit/telemetry-core";
|
|
2
|
+
import { recordFounderStanding } from "./founder-standing-file.js";
|
|
3
|
+
import { getCollectorRuntimePaths } from "./local-state.js";
|
|
4
|
+
import { recallLiveFounderAnswer, rememberLiveFounderAnswer } from "./live-founder-memo.js";
|
|
5
|
+
import { loadPairedSession, towerJsonRequest } from "./tower-client.js";
|
|
6
|
+
export { readFounderStanding, recordFounderStanding } from "./founder-standing-file.js";
|
|
7
|
+
/**
|
|
8
|
+
* BLI-22640 (Jeetesh's review of 2026-10-25, tightened 2026-10-26): is the
|
|
9
|
+
* person this machine is paired to a founder? The collector's half of
|
|
10
|
+
* `founder-standing-contract.ts` (telemetry-core), which says why only
|
|
11
|
+
* founders may keep work out of Tower and why the answer is Tower's.
|
|
12
|
+
*
|
|
13
|
+
* Every decision about what leaves the machine asks Tower LIVE. The answer is
|
|
14
|
+
* `LiveFounderAnswer`: founder, not a founder, or `unknown` with the reason
|
|
15
|
+
* Tower could not say. Nothing here falls back to `<state_dir>/founder-standing.json`;
|
|
16
|
+
* that file is written from each reply for display (`cockpit doctor`, support)
|
|
17
|
+
* and no decision reads it. A caller handed `unknown` HOLDS
|
|
18
|
+
* (`owner_check_unavailable`, `optOutVerdict`).
|
|
19
|
+
*
|
|
20
|
+
* - `askTowerFounderStanding` always asks `GET /api/ambient/founder-standing`.
|
|
21
|
+
* The verbs that RECORD an off need this and refuse without a live answer.
|
|
22
|
+
* - `liveFounderStanding` is the decider's door: it reuses what THIS process
|
|
23
|
+
* heard from Tower in the last minute (`live-founder-memo.ts`, memory only)
|
|
24
|
+
* so a tick asks once, and asks otherwise.
|
|
25
|
+
* - the heartbeat reply's `is_owner` is recorded every tick for display
|
|
26
|
+
* (`heartbeat-post.ts`); it is not a source for a decision either.
|
|
27
|
+
*
|
|
28
|
+
* A new sibling must also join `scripts/build-public-cli.mjs` `runtimeFiles`.
|
|
29
|
+
*/
|
|
30
|
+
const STANDING_DEADLINE_MS = 10_000;
|
|
31
|
+
/**
|
|
32
|
+
* Asks Tower now, remembers the answer for this process, and (on a real
|
|
33
|
+
* answer) records it for display. Never throws. Logs every branch.
|
|
34
|
+
*/
|
|
35
|
+
export async function askTowerFounderStanding(options = {}) {
|
|
36
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
37
|
+
const answer = await askDoor(options);
|
|
38
|
+
rememberLiveFounderAnswer(paths.state_dir, answer, options.now?.getTime());
|
|
39
|
+
if (answer.standing === "unknown") {
|
|
40
|
+
console.error("[founder-standing] Tower could not say whether this person is a founder; readers hold", JSON.stringify({ reason: answer.reason, ...(answer.httpStatus === undefined ? {} : { http_status: answer.httpStatus }) }));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await recordFounderStanding(paths, answer.standing === "founder", "standing_door", options.now);
|
|
44
|
+
}
|
|
45
|
+
return answer;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* What a decider asks. Tower's answer from the last minute in THIS process, or
|
|
49
|
+
* a fresh ask. Never the saved file.
|
|
50
|
+
*/
|
|
51
|
+
export async function liveFounderStanding(options = {}) {
|
|
52
|
+
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
53
|
+
const known = recallLiveFounderAnswer(paths.state_dir, options.now?.getTime());
|
|
54
|
+
if (known)
|
|
55
|
+
return known;
|
|
56
|
+
return askTowerFounderStanding(options);
|
|
57
|
+
}
|
|
58
|
+
async function askDoor(options) {
|
|
59
|
+
let session;
|
|
60
|
+
try {
|
|
61
|
+
session = await loadPairedSession("memory off", options.homeDir);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return founderAnswerNotPaired();
|
|
65
|
+
}
|
|
66
|
+
const result = await towerJsonRequest({
|
|
67
|
+
dashboardUrl: options.dashboardUrl ?? session.dashboard_url,
|
|
68
|
+
path: FOUNDER_STANDING_PATH,
|
|
69
|
+
deviceToken: session.device_token,
|
|
70
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
71
|
+
method: "GET",
|
|
72
|
+
timeoutMs: STANDING_DEADLINE_MS,
|
|
73
|
+
label: "founder-standing",
|
|
74
|
+
...(options.homeDir === undefined ? {} : { homeDir: options.homeDir }),
|
|
75
|
+
log: (line) => console.error(line),
|
|
76
|
+
});
|
|
77
|
+
if (!result.ok) {
|
|
78
|
+
return result.reason === "http_error"
|
|
79
|
+
? { standing: "unknown", reason: "founder_check_refused", ...(result.httpStatus === undefined ? {} : { httpStatus: result.httpStatus }) }
|
|
80
|
+
: founderAnswerUnreachable();
|
|
81
|
+
}
|
|
82
|
+
return founderAnswerFromReply({ status: result.httpStatus ?? 200, body: result.body });
|
|
83
|
+
}
|
|
84
|
+
/** The standing a decider acts on: Tower's live word, or `unknown` (hold). */
|
|
85
|
+
export function standingOf(answer) {
|
|
86
|
+
return answer.standing;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* May this machine record an "off"? Only for a founder, and only on Tower's
|
|
90
|
+
* LIVE answer, asked now and never from this process's memory: a file or a
|
|
91
|
+
* remembered answer is never enough to record one, so an edit cannot open
|
|
92
|
+
* this door. Every door that records an off asks this (`memory off|uninstall`,
|
|
93
|
+
* `doctor --no-memory`, the one-time question).
|
|
94
|
+
*/
|
|
95
|
+
export async function mayRecordOff(options) {
|
|
96
|
+
const answer = await askTowerFounderStanding(options);
|
|
97
|
+
if (answer.standing === "founder") {
|
|
98
|
+
console.error("[founder-standing] off allowed", JSON.stringify({ reason: "tower_says_founder", door: options.door }));
|
|
99
|
+
return { allowed: true };
|
|
100
|
+
}
|
|
101
|
+
if (answer.standing === "not_founder") {
|
|
102
|
+
console.error("[founder-standing] off refused", JSON.stringify({ reason: FOUNDERS_ONLY_REFUSAL, door: options.door }));
|
|
103
|
+
return {
|
|
104
|
+
allowed: false,
|
|
105
|
+
reason: FOUNDERS_ONLY_REFUSAL,
|
|
106
|
+
detail: null,
|
|
107
|
+
line: `BLI Memory stays on: only Tower's founders can turn it off on a machine, because work on BLI machines goes to Tower (${FOUNDERS_ONLY_REFUSAL}). Nothing was changed.`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
console.error("[founder-standing] off refused", JSON.stringify({ reason: FOUNDER_CHECK_UNAVAILABLE, detail: answer.reason, door: options.door }));
|
|
111
|
+
return {
|
|
112
|
+
allowed: false,
|
|
113
|
+
reason: FOUNDER_CHECK_UNAVAILABLE,
|
|
114
|
+
detail: answer.reason,
|
|
115
|
+
line: `BLI Memory was not turned off: Tower could not confirm that you are a founder (${answer.reason}), and only founders can turn it off (${FOUNDER_CHECK_UNAVAILABLE}). Nothing was changed.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -14,6 +14,7 @@ import { describeError } from "./health-detail.js";
|
|
|
14
14
|
import { fetchWithDeadline, isUploadDeadlineError, uploadDeadlineMs } from "./evidence-upload-deadline.js";
|
|
15
15
|
import { safeFailureDetail } from "./evidence-upload-transport.js";
|
|
16
16
|
import { noteSyncProgress } from "./sync-lock.js";
|
|
17
|
+
import { collectorShelfHeaders } from "./shelf-headers.js";
|
|
17
18
|
const MAX_ATTEMPTS = 2;
|
|
18
19
|
const RETRY_DELAY_MS = 500;
|
|
19
20
|
export async function postImageToDescribeDoor(options, request) {
|
|
@@ -25,7 +26,7 @@ export async function postImageToDescribeDoor(options, request) {
|
|
|
25
26
|
try {
|
|
26
27
|
const response = await fetchWithDeadline(options.fetchImpl, `${options.dashboardUrl}/api/ambient/images`, {
|
|
27
28
|
method: "POST",
|
|
28
|
-
headers: { Authorization: `Bearer ${options.deviceToken}`, "Content-Type": "application/json" },
|
|
29
|
+
headers: { Authorization: `Bearer ${options.deviceToken}`, "Content-Type": "application/json", ...collectorShelfHeaders() },
|
|
29
30
|
body: payload,
|
|
30
31
|
}, deadlineMs);
|
|
31
32
|
const body = await readJson(response);
|