@agentvault/grok-bridge 0.3.2 → 0.4.0
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/bridge.d.ts +9 -0
- package/dist/config.d.ts +28 -14
- package/dist/index.js +517 -159
- package/dist/service/backend.d.ts +15 -0
- package/dist/service/backend.d.ts.map +1 -1
- package/dist/service/doctor-cli.d.ts +36 -0
- package/dist/service/doctor-cli.d.ts.map +1 -0
- package/dist/service/doctor.d.ts +93 -0
- package/dist/service/doctor.d.ts.map +1 -0
- package/dist/service/launchd.d.ts +27 -1
- package/dist/service/launchd.d.ts.map +1 -1
- package/dist/service/self-uninstall.d.ts +19 -0
- package/dist/service/self-uninstall.d.ts.map +1 -0
- package/dist/service/subcommand.d.ts +1 -0
- package/dist/service/subcommand.d.ts.map +1 -1
- package/dist/service/systemd.d.ts +19 -1
- package/dist/service/systemd.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bridge.d.ts
CHANGED
|
@@ -136,6 +136,15 @@ export declare function attachLifecycle(channel: LifecycleChannel, opts?: {
|
|
|
136
136
|
onTerminal?: (reason: string, o: {
|
|
137
137
|
restart: boolean;
|
|
138
138
|
}) => void;
|
|
139
|
+
/** #913 item 2 — remove this agent's supervised service when its device is
|
|
140
|
+
* gone for good. Injected for tests; defaults to the real remover, because
|
|
141
|
+
* an opt-in default is a feature nobody gets.
|
|
142
|
+
*
|
|
143
|
+
* Every terminal reason is offered to it and IT decides. Filtering here as
|
|
144
|
+
* well would put the same rule in two files, and the two would eventually
|
|
145
|
+
* disagree — which is how `session_replaced` (device still alive) would end
|
|
146
|
+
* up removing a healthy install. */
|
|
147
|
+
selfUninstall?: (reason: string, env: NodeJS.ProcessEnv) => boolean;
|
|
139
148
|
}): void;
|
|
140
149
|
/**
|
|
141
150
|
* How a ROOM turn is announced to the model (#366 / #499 Facet A).
|
package/dist/config.d.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The one-paste install flag: enrol, install the supervised service, then EXIT so
|
|
3
|
-
* the customer's terminal comes back.
|
|
4
|
-
*
|
|
5
|
-
* WIRE LITERAL. `packages/web/lib/framework-install.ts` emits this exact string in
|
|
6
|
-
* the connect command. The two packages cannot import each other, so the only
|
|
7
|
-
* tripwire is a test on each side that names the other — see config.test.ts
|
|
8
|
-
* "freezes the wire literal".
|
|
9
|
-
*
|
|
10
|
-
* Deliberately "-" prefixed: loadConfig's token slot rejects "-" args, so adding a
|
|
11
|
-
* flag can never be mistaken for an invite token.
|
|
12
|
-
*/
|
|
13
|
-
export declare const INSTALL_FLAG = "--install";
|
|
14
1
|
/** Informational flags. Handled BEFORE loadConfig in index.ts, because asking
|
|
15
2
|
* "what version is this?" must not require an invite token. Listed here too so
|
|
16
3
|
* the strict unknown-flag check does not reject them. */
|
|
@@ -35,12 +22,38 @@ export declare function parseInfoFlags(argv: string[]): {
|
|
|
35
22
|
export declare function isEphemeralEntrypoint(entrypoint: string): boolean;
|
|
36
23
|
/** Throw unless `entrypoint` is durable enough to bake into a supervised service. */
|
|
37
24
|
export declare function assertInstallableEntrypoint(entrypoint: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* #913 item 3 — the device id inside this data dir's credentials, or null.
|
|
27
|
+
*
|
|
28
|
+
* Lives HERE, beside CRED_FILES and BACKUP_FILE, because this module already
|
|
29
|
+
* owns the knowledge of which files hold credentials. `doctor` reading those
|
|
30
|
+
* names itself would make a second copy of the list, and the copy that is not
|
|
31
|
+
* next to the constant is the one that goes stale.
|
|
32
|
+
*
|
|
33
|
+
* Checks the same files in the same order as hasPersistedCreds, INCLUDING the
|
|
34
|
+
* .bak: a device_revoked wipe deletes agentvault.json and leaves the backup, so
|
|
35
|
+
* the .bak is exactly where the id lives for the machine most likely to be
|
|
36
|
+
* running doctor.
|
|
37
|
+
*
|
|
38
|
+
* Returns null rather than throwing on missing/unreadable/malformed input — a
|
|
39
|
+
* doctor that dies on one bad file reports nothing about the others.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readPersistedDeviceId(dataDir: string): string | null;
|
|
38
42
|
export declare function hasPersistedCreds(dataDir: string): boolean;
|
|
39
43
|
export declare function slugify(name: string): string;
|
|
40
44
|
export type DataDirSource = "explicit" | "per-agent";
|
|
45
|
+
/** Where the agent name came from.
|
|
46
|
+
*
|
|
47
|
+
* #927's test for a safe default is: can a reader of the output tell the input
|
|
48
|
+
* was MISSING? A bare `agentName` cannot answer that — "Grok" reads the same
|
|
49
|
+
* whether the customer typed it or the bridge chose it. This field is what
|
|
50
|
+
* makes the two distinguishable, so callers can SAY which one happened. */
|
|
51
|
+
export type AgentNameSource = "supplied" | "defaulted";
|
|
41
52
|
export declare function resolveDataDir(env: NodeJS.ProcessEnv): {
|
|
42
53
|
dataDir: string;
|
|
43
54
|
source: DataDirSource;
|
|
55
|
+
agentName: string;
|
|
56
|
+
agentNameSource: AgentNameSource;
|
|
44
57
|
};
|
|
45
58
|
export interface BridgeConfig {
|
|
46
59
|
inviteToken: string;
|
|
@@ -48,6 +61,8 @@ export interface BridgeConfig {
|
|
|
48
61
|
dataDirSource: DataDirSource;
|
|
49
62
|
apiUrl: string;
|
|
50
63
|
agentName: string;
|
|
64
|
+
/** Whether AV_AGENT_NAME was given, or the package default was used (#927). */
|
|
65
|
+
agentNameSource: AgentNameSource;
|
|
51
66
|
roomFilter?: string;
|
|
52
67
|
/** Per-agent filesystem sandbox. Always set: auto-defaults to a sibling of
|
|
53
68
|
* the key/data dir unless AV_WORKSPACE_DIR overrides it. */
|
|
@@ -78,7 +93,6 @@ export interface BridgeConfig {
|
|
|
78
93
|
/** Set by --install: enrol in the foreground, install the supervised service,
|
|
79
94
|
* then EXIT so the customer's terminal returns. False leaves a bare run
|
|
80
95
|
* byte-for-byte as it behaves today. */
|
|
81
|
-
installAfterEnroll: boolean;
|
|
82
96
|
/** --version / -v. index.ts answers these before loadConfig runs. */
|
|
83
97
|
showVersion: boolean;
|
|
84
98
|
/** --help / -h. Same. */
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,24 @@ var init_pending_invite = __esm({
|
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
// src/log.ts
|
|
58
|
+
import { format } from "node:util";
|
|
59
|
+
function stamp(message) {
|
|
60
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
61
|
+
return message.split("\n").map((line) => `${ts} ${line}`).join("\n");
|
|
62
|
+
}
|
|
63
|
+
function logLine(...args) {
|
|
64
|
+
console.error(stamp(format(...args)));
|
|
65
|
+
}
|
|
66
|
+
function printLine(text) {
|
|
67
|
+
console.log(text);
|
|
68
|
+
}
|
|
69
|
+
var init_log = __esm({
|
|
70
|
+
"src/log.ts"() {
|
|
71
|
+
"use strict";
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
57
75
|
// src/config.ts
|
|
58
76
|
import { existsSync as existsSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "node:fs";
|
|
59
77
|
import { join as join6, resolve as resolve2, sep, dirname } from "node:path";
|
|
@@ -71,7 +89,7 @@ function assertInstallableEntrypoint(entrypoint) {
|
|
|
71
89
|
if (isEphemeralEntrypoint(entrypoint)) {
|
|
72
90
|
throw new Error(
|
|
73
91
|
`cannot install a background service from an ephemeral package cache (${entrypoint}) \u2014 it is deleted on cleanup and the service would silently die later. Install globally first:
|
|
74
|
-
npm i -g @agentvault/grok-bridge && agentvault-grok-bridge `
|
|
92
|
+
npm i -g @agentvault/grok-bridge && agentvault-grok-bridge install`
|
|
75
93
|
);
|
|
76
94
|
}
|
|
77
95
|
}
|
|
@@ -83,6 +101,17 @@ function hasRecoverableBackup(dataDir) {
|
|
|
83
101
|
return false;
|
|
84
102
|
}
|
|
85
103
|
}
|
|
104
|
+
function readPersistedDeviceId(dataDir) {
|
|
105
|
+
for (const f2 of [...CRED_FILES, BACKUP_FILE]) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(readFileSync3(join6(dataDir, f2), "utf-8"));
|
|
108
|
+
const id = parsed?.deviceId;
|
|
109
|
+
if (typeof id === "string" && id) return id;
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
86
115
|
function hasPersistedCreds(dataDir) {
|
|
87
116
|
if (CRED_FILES.some((f2) => existsSync2(join6(dataDir, f2)))) return true;
|
|
88
117
|
return hasRecoverableBackup(dataDir);
|
|
@@ -91,20 +120,41 @@ function slugify2(name) {
|
|
|
91
120
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
92
121
|
}
|
|
93
122
|
function resolveDataDir(env) {
|
|
94
|
-
|
|
123
|
+
const supplied = env.AV_AGENT_NAME?.trim();
|
|
124
|
+
const agentName = supplied || LEGACY_DEFAULT_AGENT_NAME;
|
|
125
|
+
const agentNameSource = supplied ? "supplied" : "defaulted";
|
|
126
|
+
if (env.AV_DATA_DIR)
|
|
127
|
+
return { dataDir: env.AV_DATA_DIR, source: "explicit", agentName, agentNameSource };
|
|
95
128
|
const base = join6(env.HOME ?? "", ".agentvault", "grok-room-bridge");
|
|
96
|
-
|
|
129
|
+
const perAgent = join6(base, slugify2(agentName));
|
|
130
|
+
if (!supplied && !hasPersistedCreds(perAgent)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`AV_AGENT_NAME is required \u2014 it names WHICH agent this bridge runs as, and therefore where its credentials live. No credentials were found at ${perAgent}, so there is no existing install to infer a name from. Re-run with AV_AGENT_NAME=<agent-name> set.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return { dataDir: perAgent, source: "per-agent", agentName, agentNameSource };
|
|
97
136
|
}
|
|
98
137
|
function loadConfig(env, argv = []) {
|
|
99
|
-
const { dataDir, source: dataDirSource } = resolveDataDir(env);
|
|
138
|
+
const { dataDir, source: dataDirSource, agentName, agentNameSource } = resolveDataDir(env);
|
|
139
|
+
if (agentNameSource === "defaulted") {
|
|
140
|
+
logLine(
|
|
141
|
+
`[config] AV_AGENT_NAME was not set \u2014 running as "${agentName}" from the existing credentials at ${dataDir}. Set AV_AGENT_NAME to name this agent explicitly; a future install will require it.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
100
144
|
const inviteToken = (argv[0] && !argv[0].startsWith("-") ? argv[0] : "") || env.AV_INVITE_TOKEN || readPendingInvite(dataDir) || "";
|
|
145
|
+
if (argv.includes(REMOVED_INSTALL_FLAG)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`${REMOVED_INSTALL_FLAG} was removed \u2014 it handed the agent over from a foreground process that could be reaped before the encrypted group finished forming, leaving an agent that was connected and mute (#909). Use the subcommand instead, which lets the service enrol itself:
|
|
148
|
+
npm i -g @agentvault/grok-bridge && AV_AGENT_NAME=<agent-name> AV_INVITE_TOKEN=<token> agentvault-grok-bridge install`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
101
151
|
const unknownFlags = argv.filter((a2) => a2.startsWith("-") && !KNOWN_FLAGS.has(a2));
|
|
102
152
|
if (unknownFlags.length) {
|
|
103
153
|
throw new Error(
|
|
104
154
|
`unknown option(s): ${unknownFlags.join(", ")} \u2014 supported flags are ${[...KNOWN_FLAGS].join(", ")}`
|
|
105
155
|
);
|
|
106
156
|
}
|
|
107
|
-
const workspaceDir = env.AV_WORKSPACE_DIR || join6(dirname(dataDir), "workspaces", slugify2(
|
|
157
|
+
const workspaceDir = env.AV_WORKSPACE_DIR || join6(dirname(dataDir), "workspaces", slugify2(agentName));
|
|
108
158
|
const wsReal = resolve2(workspaceDir);
|
|
109
159
|
const ddReal = resolve2(dataDir);
|
|
110
160
|
if (wsReal === ddReal || wsReal.startsWith(ddReal + sep) || ddReal.startsWith(wsReal + sep)) {
|
|
@@ -129,17 +179,14 @@ function loadConfig(env, argv = []) {
|
|
|
129
179
|
inviteToken,
|
|
130
180
|
dataDir,
|
|
131
181
|
dataDirSource,
|
|
132
|
-
// One-paste install (--install): enrol, hand off to the supervisor, exit.
|
|
133
|
-
// Default false keeps a bare `npx @agentvault/grok-bridge` run identical.
|
|
134
|
-
installAfterEnroll: argv.includes(INSTALL_FLAG),
|
|
135
182
|
...parseInfoFlags(argv),
|
|
136
183
|
apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
|
|
137
184
|
// Identity shown to the agent's own session. Set this to the agent's
|
|
138
185
|
// AgentVault name via AV_AGENT_NAME (the native connect command passes it).
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
186
|
+
// Resolved in resolveDataDir so the name, the data dir and the workspace dir
|
|
187
|
+
// can never disagree about who this agent is (#921/#927).
|
|
188
|
+
agentName,
|
|
189
|
+
agentNameSource,
|
|
143
190
|
roomFilter: env.AV_ROOM_ID || void 0,
|
|
144
191
|
workspaceDir,
|
|
145
192
|
grokPath: env.AV_GROK_PATH || void 0,
|
|
@@ -149,35 +196,19 @@ function loadConfig(env, argv = []) {
|
|
|
149
196
|
systemPrompt: env.AV_SYSTEM_PROMPT || void 0
|
|
150
197
|
};
|
|
151
198
|
}
|
|
152
|
-
var
|
|
199
|
+
var REMOVED_INSTALL_FLAG, VERSION_FLAGS, HELP_FLAGS, KNOWN_FLAGS, CRED_FILES, BACKUP_FILE, LEGACY_DEFAULT_AGENT_NAME;
|
|
153
200
|
var init_config = __esm({
|
|
154
201
|
"src/config.ts"() {
|
|
155
202
|
"use strict";
|
|
156
203
|
init_pending_invite();
|
|
157
|
-
|
|
204
|
+
init_log();
|
|
205
|
+
REMOVED_INSTALL_FLAG = "--install";
|
|
158
206
|
VERSION_FLAGS = ["--version", "-v"];
|
|
159
207
|
HELP_FLAGS = ["--help", "-h"];
|
|
160
|
-
KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
208
|
+
KNOWN_FLAGS = /* @__PURE__ */ new Set([...VERSION_FLAGS, ...HELP_FLAGS]);
|
|
161
209
|
CRED_FILES = ["agentvault.json", "secure-channel.json"];
|
|
162
210
|
BACKUP_FILE = "agentvault.json.bak";
|
|
163
|
-
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// src/log.ts
|
|
167
|
-
import { format } from "node:util";
|
|
168
|
-
function stamp(message) {
|
|
169
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
170
|
-
return message.split("\n").map((line) => `${ts} ${line}`).join("\n");
|
|
171
|
-
}
|
|
172
|
-
function logLine(...args) {
|
|
173
|
-
console.error(stamp(format(...args)));
|
|
174
|
-
}
|
|
175
|
-
function printLine(text) {
|
|
176
|
-
console.log(text);
|
|
177
|
-
}
|
|
178
|
-
var init_log = __esm({
|
|
179
|
-
"src/log.ts"() {
|
|
180
|
-
"use strict";
|
|
211
|
+
LEGACY_DEFAULT_AGENT_NAME = "Grok";
|
|
181
212
|
}
|
|
182
213
|
});
|
|
183
214
|
|
|
@@ -211,16 +242,6 @@ function writeLauncher(deps, spec) {
|
|
|
211
242
|
deps.writeFile(spec.launcherPath, spec.launcherScript);
|
|
212
243
|
deps.chmod(spec.launcherPath, 493);
|
|
213
244
|
}
|
|
214
|
-
function parseLauncherEntrypoint(script) {
|
|
215
|
-
if (!script) return void 0;
|
|
216
|
-
const m2 = /^if n=\$\(command -v .+\); then exec "\$n" (.+) "\$@"; fi$/m.exec(script);
|
|
217
|
-
if (!m2) return void 0;
|
|
218
|
-
return unSq(m2[1]);
|
|
219
|
-
}
|
|
220
|
-
function unSq(quoted) {
|
|
221
|
-
if (quoted.length < 2 || !quoted.startsWith("'") || !quoted.endsWith("'")) return void 0;
|
|
222
|
-
return quoted.slice(1, -1).replace(/'\\''/g, "'");
|
|
223
|
-
}
|
|
224
245
|
var FALLBACK_NODES;
|
|
225
246
|
var init_launcher = __esm({
|
|
226
247
|
"src/service/launcher.ts"() {
|
|
@@ -314,6 +335,37 @@ ${envEntries}
|
|
|
314
335
|
</plist>
|
|
315
336
|
`;
|
|
316
337
|
}
|
|
338
|
+
function parsePlistEnv(xml) {
|
|
339
|
+
const out = {};
|
|
340
|
+
const keyIdx = xml.indexOf("<key>EnvironmentVariables</key>");
|
|
341
|
+
if (keyIdx < 0) return out;
|
|
342
|
+
const open = xml.indexOf("<dict>", keyIdx);
|
|
343
|
+
if (open < 0) return out;
|
|
344
|
+
let depth = 0;
|
|
345
|
+
let i2 = open;
|
|
346
|
+
let close = -1;
|
|
347
|
+
const tag = /<(\/?)dict>/g;
|
|
348
|
+
tag.lastIndex = open;
|
|
349
|
+
let m2;
|
|
350
|
+
while (m2 = tag.exec(xml)) {
|
|
351
|
+
depth += m2[1] ? -1 : 1;
|
|
352
|
+
if (depth === 0) {
|
|
353
|
+
close = m2.index;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
i2 = m2.index;
|
|
357
|
+
}
|
|
358
|
+
void i2;
|
|
359
|
+
if (close < 0) return out;
|
|
360
|
+
const body = xml.slice(open, close);
|
|
361
|
+
const pair = /<key>([\s\S]*?)<\/key>\s*<string>([\s\S]*?)<\/string>/g;
|
|
362
|
+
let p2;
|
|
363
|
+
while (p2 = pair.exec(body)) out[unesc(p2[1])] = unesc(p2[2]);
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
function unesc(s2) {
|
|
367
|
+
return s2.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
368
|
+
}
|
|
317
369
|
var LaunchdBackend;
|
|
318
370
|
var init_launchd = __esm({
|
|
319
371
|
"src/service/launchd.ts"() {
|
|
@@ -368,6 +420,26 @@ var init_launchd = __esm({
|
|
|
368
420
|
const r2 = this.deps.exec("launchctl", ["print", `${this.domain()}/${label}`]);
|
|
369
421
|
return { loaded: r2.code === 0, detail: r2.stdout };
|
|
370
422
|
}
|
|
423
|
+
/** #913 item 3 — every installed service whose label starts with `prefix`.
|
|
424
|
+
*
|
|
425
|
+
* Reads the plist directory rather than `launchctl list`, because an ORPHAN
|
|
426
|
+
* is typically installed-but-not-loaded: the revoked device exits 0 and
|
|
427
|
+
* KeepAlive leaves it down. `launchctl list` would not show the very thing
|
|
428
|
+
* doctor exists to find. */
|
|
429
|
+
list(prefix) {
|
|
430
|
+
const dir = join11(this.deps.home, "Library", "LaunchAgents");
|
|
431
|
+
const out = [];
|
|
432
|
+
for (const name of this.deps.readdir(dir)) {
|
|
433
|
+
if (!name.startsWith(prefix) || !name.endsWith(".plist")) continue;
|
|
434
|
+
const path2 = join11(dir, name);
|
|
435
|
+
out.push({
|
|
436
|
+
label: name.slice(0, -".plist".length),
|
|
437
|
+
path: path2,
|
|
438
|
+
env: parsePlistEnv(this.deps.readFile(path2))
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return out.sort((a2, b2) => a2.label.localeCompare(b2.label));
|
|
442
|
+
}
|
|
371
443
|
};
|
|
372
444
|
}
|
|
373
445
|
});
|
|
@@ -399,6 +471,24 @@ StandardError=append:${spec.logErr}
|
|
|
399
471
|
WantedBy=default.target
|
|
400
472
|
`;
|
|
401
473
|
}
|
|
474
|
+
function parseUnitEnv(text) {
|
|
475
|
+
const out = {};
|
|
476
|
+
for (const line of text.split("\n")) {
|
|
477
|
+
const m2 = /^Environment="([\s\S]*)"\s*$/.exec(line.trim());
|
|
478
|
+
if (!m2) continue;
|
|
479
|
+
const eq = unq(m2[1]);
|
|
480
|
+
const at = eq.indexOf("=");
|
|
481
|
+
if (at <= 0) continue;
|
|
482
|
+
out[eq.slice(0, at)] = eq.slice(at + 1);
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
function unq(s2) {
|
|
487
|
+
return s2.replace(/%%/g, "%").replace(
|
|
488
|
+
/\\([\s\S])/g,
|
|
489
|
+
(_2, c2) => c2 === "n" ? "\n" : c2 === "r" ? "\r" : c2 === "t" ? " " : c2
|
|
490
|
+
);
|
|
491
|
+
}
|
|
402
492
|
var SystemdBackend;
|
|
403
493
|
var init_systemd = __esm({
|
|
404
494
|
"src/service/systemd.ts"() {
|
|
@@ -439,18 +529,33 @@ var init_systemd = __esm({
|
|
|
439
529
|
const active = this.deps.exec("systemctl", ["--user", "is-active", `${label}.service`]);
|
|
440
530
|
return { loaded: enabled.code === 0, detail: `${enabled.stdout.trim() || "?"} / ${active.stdout.trim() || "?"}` };
|
|
441
531
|
}
|
|
532
|
+
/** #913 item 3 — every installed unit whose label starts with `prefix`.
|
|
533
|
+
*
|
|
534
|
+
* Reads the unit directory rather than `systemctl list-units`, because an
|
|
535
|
+
* ORPHAN is installed-but-inactive: the revoked device exits 0, which
|
|
536
|
+
* SuccessExitStatus=0 treats as success, so it is never restarted. A
|
|
537
|
+
* running-units listing would not show the thing doctor exists to find. */
|
|
538
|
+
list(prefix) {
|
|
539
|
+
const dir = join12(this.deps.home, ".config", "systemd", "user");
|
|
540
|
+
const out = [];
|
|
541
|
+
for (const name of this.deps.readdir(dir)) {
|
|
542
|
+
if (!name.startsWith(prefix) || !name.endsWith(".service")) continue;
|
|
543
|
+
const path2 = join12(dir, name);
|
|
544
|
+
out.push({
|
|
545
|
+
label: name.slice(0, -".service".length),
|
|
546
|
+
path: path2,
|
|
547
|
+
env: parseUnitEnv(this.deps.readFile(path2))
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return out.sort((a2, b2) => a2.label.localeCompare(b2.label));
|
|
551
|
+
}
|
|
442
552
|
};
|
|
443
553
|
}
|
|
444
554
|
});
|
|
445
555
|
|
|
446
556
|
// src/service/backend.ts
|
|
447
|
-
var backend_exports = {};
|
|
448
|
-
__export(backend_exports, {
|
|
449
|
-
defaultDeps: () => defaultDeps,
|
|
450
|
-
selectBackend: () => selectBackend
|
|
451
|
-
});
|
|
452
557
|
import { spawnSync } from "node:child_process";
|
|
453
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync2, existsSync as existsSync4, chmodSync as chmodSync5 } from "node:fs";
|
|
558
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, rmSync as rmSync2, existsSync as existsSync4, chmodSync as chmodSync5, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
454
559
|
import { userInfo } from "node:os";
|
|
455
560
|
function defaultDeps() {
|
|
456
561
|
return {
|
|
@@ -465,7 +570,21 @@ function defaultDeps() {
|
|
|
465
570
|
mkdir: (p2) => mkdirSync5(p2, { recursive: true }),
|
|
466
571
|
chmod: (p2, m2) => chmodSync5(p2, m2),
|
|
467
572
|
rm: (p2) => rmSync2(p2, { force: true }),
|
|
468
|
-
exists: (p2) => existsSync4(p2)
|
|
573
|
+
exists: (p2) => existsSync4(p2),
|
|
574
|
+
readdir: (p2) => {
|
|
575
|
+
try {
|
|
576
|
+
return readdirSync(p2);
|
|
577
|
+
} catch {
|
|
578
|
+
return [];
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
readFile: (p2) => {
|
|
582
|
+
try {
|
|
583
|
+
return readFileSync4(p2, "utf8");
|
|
584
|
+
} catch {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
}
|
|
469
588
|
};
|
|
470
589
|
}
|
|
471
590
|
function selectBackend(platform, deps) {
|
|
@@ -488,6 +607,7 @@ var init_backend = __esm({
|
|
|
488
607
|
// src/service/subcommand.ts
|
|
489
608
|
var subcommand_exports = {};
|
|
490
609
|
__export(subcommand_exports, {
|
|
610
|
+
labelFor: () => labelFor,
|
|
491
611
|
maybeRunServiceSubcommand: () => maybeRunServiceSubcommand
|
|
492
612
|
});
|
|
493
613
|
function requireAgentName(env) {
|
|
@@ -510,7 +630,9 @@ function maybeRunServiceSubcommand(argv, env, deps) {
|
|
|
510
630
|
const looksLikeFlag = cmd.startsWith("-");
|
|
511
631
|
if (!looksLikeToken && !looksLikeFlag) {
|
|
512
632
|
throw new Error(
|
|
513
|
-
|
|
633
|
+
// "doctor" is dispatched earlier, in index.ts, so it never reaches here.
|
|
634
|
+
// Named anyway: a customer who typed `doctr` should see it offered.
|
|
635
|
+
`unknown subcommand: ${cmd} \u2014 expected one of ${[...SERVICE_CMDS, "doctor"].join(", ")}, an invite token, or no argument at all`
|
|
514
636
|
);
|
|
515
637
|
}
|
|
516
638
|
return false;
|
|
@@ -521,6 +643,7 @@ function maybeRunServiceSubcommand(argv, env, deps) {
|
|
|
521
643
|
try {
|
|
522
644
|
if (cmd === "install") {
|
|
523
645
|
requireAgentName(env);
|
|
646
|
+
assertInstallableEntrypoint(entrypoint);
|
|
524
647
|
const cfg = loadConfig(env, []);
|
|
525
648
|
const firstBoot = !hasPersistedCreds(cfg.dataDir) && !!cfg.inviteToken;
|
|
526
649
|
if (firstBoot) {
|
|
@@ -578,96 +701,313 @@ var init_subcommand = __esm({
|
|
|
578
701
|
}
|
|
579
702
|
});
|
|
580
703
|
|
|
581
|
-
// src/service/
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
704
|
+
// src/service/doctor.ts
|
|
705
|
+
import { basename } from "node:path";
|
|
706
|
+
async function buildDoctorReport(d2) {
|
|
707
|
+
const installed = d2.labelPrefixes.flatMap((p2) => d2.backend.list(p2));
|
|
708
|
+
const services = [];
|
|
709
|
+
for (const s2 of installed) services.push(await inspect(s2, d2));
|
|
710
|
+
const claimed = new Set(installed.map((s2) => s2.env.AV_DATA_DIR).filter(Boolean));
|
|
711
|
+
const unsupervised = [];
|
|
712
|
+
for (const dataDir of d2.credDirs()) {
|
|
713
|
+
if (claimed.has(dataDir)) continue;
|
|
714
|
+
unsupervised.push(await inspectUnsupervised(dataDir, d2));
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
labelPrefixes: d2.labelPrefixes,
|
|
718
|
+
services,
|
|
719
|
+
unsupervised,
|
|
720
|
+
notChecked: d2.otherLabels()
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function inspect(s2, d2) {
|
|
724
|
+
const agentName = s2.env.AV_AGENT_NAME ?? "";
|
|
725
|
+
const dataDir = s2.env.AV_DATA_DIR ?? "";
|
|
726
|
+
let loaded = false;
|
|
727
|
+
try {
|
|
728
|
+
loaded = d2.backend.status(s2.label).loaded;
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
const base = { label: s2.label, agentName, dataDir, loaded, deviceId: "" };
|
|
732
|
+
if (!dataDir) {
|
|
733
|
+
return {
|
|
734
|
+
...base,
|
|
735
|
+
verdict: "WARN",
|
|
736
|
+
detail: "the service definition bakes no AV_DATA_DIR, so doctor cannot find its credentials and cannot check its device"
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
const creds = d2.readCreds(dataDir);
|
|
740
|
+
if (!creds?.deviceId) {
|
|
741
|
+
return {
|
|
742
|
+
...base,
|
|
743
|
+
verdict: "WARN",
|
|
744
|
+
detail: `no credentials at ${dataDir} \u2014 installed, but this agent never enrolled`
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
const deviceId = creds.deviceId;
|
|
748
|
+
let probe;
|
|
749
|
+
try {
|
|
750
|
+
probe = await d2.fetchStatus(d2.apiUrl, deviceId);
|
|
751
|
+
} catch (e) {
|
|
752
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e.message}` };
|
|
753
|
+
}
|
|
754
|
+
if (probe.inconclusive) {
|
|
755
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
|
|
756
|
+
}
|
|
757
|
+
if (probe.gone) {
|
|
758
|
+
return {
|
|
759
|
+
...base,
|
|
760
|
+
deviceId,
|
|
761
|
+
verdict: "FAIL",
|
|
762
|
+
detail: "the device no longer exists \u2014 this service is an orphan",
|
|
763
|
+
fix: d2.uninstallCommand(agentName)
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
switch (probe.status) {
|
|
767
|
+
case "ACTIVE":
|
|
768
|
+
return { ...base, deviceId, verdict: "PASS", detail: "device ACTIVE" };
|
|
769
|
+
case "PENDING":
|
|
770
|
+
return {
|
|
771
|
+
...base,
|
|
772
|
+
deviceId,
|
|
773
|
+
verdict: "WARN",
|
|
774
|
+
detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
|
|
775
|
+
};
|
|
776
|
+
case "REVOKED":
|
|
777
|
+
return {
|
|
778
|
+
...base,
|
|
779
|
+
deviceId,
|
|
780
|
+
verdict: "FAIL",
|
|
781
|
+
detail: "the device was REVOKED \u2014 this service is an orphan",
|
|
782
|
+
fix: d2.uninstallCommand(agentName)
|
|
783
|
+
};
|
|
784
|
+
default:
|
|
785
|
+
return {
|
|
786
|
+
...base,
|
|
787
|
+
deviceId,
|
|
788
|
+
verdict: "SKIP",
|
|
789
|
+
detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
async function inspectUnsupervised(dataDir, d2) {
|
|
794
|
+
const agentName = basename(dataDir);
|
|
795
|
+
const base = { dataDir, agentName, deviceId: "" };
|
|
796
|
+
const deviceId = d2.readCreds(dataDir)?.deviceId;
|
|
797
|
+
if (!deviceId) {
|
|
798
|
+
return { ...base, verdict: "WARN", detail: "credentials present but they hold no device id" };
|
|
799
|
+
}
|
|
800
|
+
let probe;
|
|
801
|
+
try {
|
|
802
|
+
probe = await d2.fetchStatus(d2.apiUrl, deviceId);
|
|
803
|
+
} catch (e) {
|
|
804
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${e.message}` };
|
|
805
|
+
}
|
|
806
|
+
if (probe.inconclusive) {
|
|
807
|
+
return { ...base, deviceId, verdict: "SKIP", detail: `could not check the device: ${probe.inconclusive}` };
|
|
808
|
+
}
|
|
809
|
+
if (probe.gone) {
|
|
810
|
+
return {
|
|
811
|
+
...base,
|
|
812
|
+
deviceId,
|
|
813
|
+
verdict: "WARN",
|
|
814
|
+
detail: "the device no longer exists \u2014 these are leftover credentials for a deleted agent"
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (probe.status === "ACTIVE") {
|
|
818
|
+
return {
|
|
819
|
+
...base,
|
|
820
|
+
deviceId,
|
|
821
|
+
verdict: "WARN",
|
|
822
|
+
detail: "the device is ACTIVE but nothing supervises it \u2014 this agent is not running",
|
|
823
|
+
fix: d2.installCommand(agentName)
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
if (probe.status === "PENDING") {
|
|
827
|
+
return {
|
|
828
|
+
...base,
|
|
829
|
+
deviceId,
|
|
830
|
+
verdict: "WARN",
|
|
831
|
+
detail: "device PENDING \u2014 enrolled, but never approved in AgentVault"
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
if (probe.status === "REVOKED") {
|
|
835
|
+
return {
|
|
836
|
+
...base,
|
|
837
|
+
deviceId,
|
|
838
|
+
verdict: "WARN",
|
|
839
|
+
detail: "the device was REVOKED \u2014 these are leftover credentials"
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
...base,
|
|
844
|
+
deviceId,
|
|
845
|
+
verdict: "SKIP",
|
|
846
|
+
detail: `the server reported a status this build does not recognise (${probe.status || "none"})`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function formatDoctorReport(r2) {
|
|
850
|
+
const out = [];
|
|
851
|
+
out.push(`Services installed for this bridge (${r2.labelPrefixes.map((p2) => p2 + "*").join(", ")})`);
|
|
852
|
+
if (r2.services.length === 0) {
|
|
853
|
+
out.push(` No services installed for this bridge on this machine.`);
|
|
854
|
+
}
|
|
855
|
+
for (const s2 of r2.services) {
|
|
856
|
+
out.push(
|
|
857
|
+
` ${s2.verdict.padEnd(PAD)}${s2.label} [${s2.agentName || "unnamed"}] ${s2.loaded ? "loaded" : "not loaded"} \u2014 ${s2.detail}`
|
|
858
|
+
);
|
|
859
|
+
if (s2.fix) out.push(` ${"".padEnd(PAD)}fix: ${s2.fix}`);
|
|
860
|
+
}
|
|
861
|
+
if (r2.unsupervised.length) {
|
|
862
|
+
out.push("");
|
|
863
|
+
out.push("Credentials on disk that no installed service points at");
|
|
864
|
+
for (const u2 of r2.unsupervised) {
|
|
865
|
+
out.push(` ${u2.verdict.padEnd(PAD)}${u2.agentName} \u2014 ${u2.detail}`);
|
|
866
|
+
out.push(` ${"".padEnd(PAD)}${u2.dataDir}`);
|
|
867
|
+
if (u2.fix) out.push(` ${"".padEnd(PAD)}fix: ${u2.fix}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (r2.notChecked.length) {
|
|
871
|
+
out.push("");
|
|
872
|
+
out.push("Found on this machine, NOT checked by this command");
|
|
873
|
+
for (const l2 of r2.notChecked) out.push(` ${l2}`);
|
|
874
|
+
out.push(
|
|
875
|
+
" These belong to other AgentVault agent families. This command can only read the credentials of its own, so it says nothing about their health."
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const counts = { PASS: 0, WARN: 0, FAIL: 0, SKIP: 0 };
|
|
879
|
+
for (const s2 of r2.services) counts[s2.verdict]++;
|
|
880
|
+
for (const u2 of r2.unsupervised) counts[u2.verdict]++;
|
|
881
|
+
out.push("");
|
|
882
|
+
out.push(
|
|
883
|
+
`Summary: ${counts.PASS} PASS, ${counts.WARN} WARN, ${counts.FAIL} FAIL, ${counts.SKIP} SKIP` + (counts.SKIP ? " (SKIP = could not check, NOT a problem found)" : "")
|
|
884
|
+
);
|
|
885
|
+
if (counts.FAIL) {
|
|
886
|
+
out.push(
|
|
887
|
+
"`uninstall` removes the SERVICE only \u2014 your credentials stay on disk, so a mistake here costs a re-install, not an identity."
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
return out;
|
|
891
|
+
}
|
|
892
|
+
var PAD;
|
|
893
|
+
var init_doctor = __esm({
|
|
894
|
+
"src/service/doctor.ts"() {
|
|
895
|
+
"use strict";
|
|
896
|
+
PAD = 6;
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// src/service/doctor-cli.ts
|
|
901
|
+
var doctor_cli_exports = {};
|
|
902
|
+
__export(doctor_cli_exports, {
|
|
903
|
+
fetchDeviceStatus: () => fetchDeviceStatus,
|
|
904
|
+
maybeRunDoctorSubcommand: () => maybeRunDoctorSubcommand,
|
|
905
|
+
realDoctorDeps: () => realDoctorDeps
|
|
585
906
|
});
|
|
586
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
587
907
|
import { join as join13 } from "node:path";
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
let fired = false;
|
|
593
|
-
channel.on("ready", () => {
|
|
594
|
-
if (fired) return;
|
|
595
|
-
fired = true;
|
|
596
|
-
void (async () => {
|
|
597
|
-
try {
|
|
598
|
-
if (!hasCreds(cfg.dataDir)) {
|
|
599
|
-
throw new Error(
|
|
600
|
-
`enrolment did not persist credentials to ${cfg.dataDir} \u2014 refusing to install a service that would crash-loop`
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
if (!fenceAuthExists(cfg.fenceHome)) {
|
|
604
|
-
throw new Error(
|
|
605
|
-
"no grok credential in the fence \u2014 run `grok login`, then paste the same command again (your enrolment is already saved, so the token is not needed). Note that XAI_API_KEY from your shell is NOT inherited by the background service."
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
if (deps.waitForIdle) {
|
|
609
|
-
const timeoutMs = deps.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
610
|
-
let timer;
|
|
611
|
-
const timedOut = await Promise.race([
|
|
612
|
-
deps.waitForIdle().then(() => false),
|
|
613
|
-
new Promise((r2) => {
|
|
614
|
-
timer = setTimeout(() => r2(true), timeoutMs);
|
|
615
|
-
})
|
|
616
|
-
]);
|
|
617
|
-
if (timer) clearTimeout(timer);
|
|
618
|
-
if (timedOut) {
|
|
619
|
-
deps.log(
|
|
620
|
-
`agent still busy after ${timeoutMs}ms \u2014 handing off anyway; that reply may be lost`
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
const reaped = await deps.reap();
|
|
625
|
-
if (!reaped.ok) {
|
|
626
|
-
throw new Error(
|
|
627
|
-
`teardown did not complete (${reaped.failures.join("; ")}) \u2014 refusing to install over a session that may still be open`
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
const backend = deps.backend ?? selectBackend(process.platform);
|
|
631
|
-
const spec = buildServiceSpec(cfg, {
|
|
632
|
-
entrypoint: deps.entrypoint,
|
|
633
|
-
home: deps.env.HOME ?? "",
|
|
634
|
-
path: deps.env.PATH ?? ""
|
|
635
|
-
});
|
|
636
|
-
const installedEntrypoint = parseLauncherEntrypoint(readLauncher(spec.launcherPath));
|
|
637
|
-
const entrypointIsCurrent = installedEntrypoint !== void 0 && !isEphemeralEntrypoint(installedEntrypoint) && installedEntrypoint === deps.entrypoint;
|
|
638
|
-
if (backend.status(spec.label).loaded && entrypointIsCurrent) {
|
|
639
|
-
backend.restart(spec.label);
|
|
640
|
-
deps.log(`service ${spec.label} already installed and current \u2014 restarted`);
|
|
641
|
-
} else {
|
|
642
|
-
backend.install(spec);
|
|
643
|
-
deps.log(
|
|
644
|
-
installedEntrypoint && !entrypointIsCurrent ? `service ${spec.label} pointed at a stale entrypoint (${installedEntrypoint}) \u2014 re-installed` : `installed service ${spec.label}`
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
const after = backend.status(spec.label);
|
|
648
|
-
if (!after.loaded) {
|
|
649
|
-
throw new Error(`service did not load: ${after.detail}`);
|
|
650
|
-
}
|
|
651
|
-
deps.log(`it is running now. logs: ${spec.logErr}`);
|
|
652
|
-
deps.log(`check it: agentvault-grok-bridge status`);
|
|
653
|
-
deps.log(`restart it: agentvault-grok-bridge restart (never 'install')`);
|
|
654
|
-
deps.exit(0);
|
|
655
|
-
} catch (e) {
|
|
656
|
-
deps.log(`--install failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
657
|
-
deps.exit(1);
|
|
658
|
-
}
|
|
659
|
-
})();
|
|
908
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
909
|
+
async function fetchDeviceStatus(apiUrl, deviceId, doFetch = fetch) {
|
|
910
|
+
const res = await doFetch(`${apiUrl}/api/v1/devices/${deviceId}/status`, {
|
|
911
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
660
912
|
});
|
|
913
|
+
if (res.status === 404) {
|
|
914
|
+
try {
|
|
915
|
+
await res.json();
|
|
916
|
+
} catch {
|
|
917
|
+
return {
|
|
918
|
+
inconclusive: `${apiUrl} answered 404 with a non-JSON body \u2014 check AV_API_URL points at the API`
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return { gone: true };
|
|
922
|
+
}
|
|
923
|
+
if (res.status === 429) return { inconclusive: "rate limited by the API (429)" };
|
|
924
|
+
if (!res.ok) return { inconclusive: `the API answered ${res.status}` };
|
|
925
|
+
let body;
|
|
926
|
+
try {
|
|
927
|
+
body = await res.json();
|
|
928
|
+
} catch {
|
|
929
|
+
return { inconclusive: `${apiUrl} answered 200 with a non-JSON body \u2014 check AV_API_URL` };
|
|
930
|
+
}
|
|
931
|
+
return { status: body?.status };
|
|
932
|
+
}
|
|
933
|
+
function listCredDirs(base) {
|
|
934
|
+
const out = [];
|
|
935
|
+
if (hasPersistedCreds(base)) out.push(base);
|
|
936
|
+
let names;
|
|
937
|
+
try {
|
|
938
|
+
names = readdirSync2(base);
|
|
939
|
+
} catch {
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
942
|
+
for (const name of names) {
|
|
943
|
+
const dir = join13(base, name);
|
|
944
|
+
if (hasPersistedCreds(dir)) out.push(dir);
|
|
945
|
+
}
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
function otherFamilyLabels(backend) {
|
|
949
|
+
const out = [];
|
|
950
|
+
for (const p2 of OTHER_FAMILY_PREFIXES) {
|
|
951
|
+
try {
|
|
952
|
+
for (const s2 of backend.list(p2)) out.push(s2.label);
|
|
953
|
+
} catch {
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return out.sort();
|
|
957
|
+
}
|
|
958
|
+
function realDoctorDeps(env) {
|
|
959
|
+
const backend = selectBackend(process.platform);
|
|
960
|
+
const base = join13(env.HOME ?? "", ".agentvault", "grok-room-bridge");
|
|
961
|
+
return {
|
|
962
|
+
backend,
|
|
963
|
+
labelPrefixes: [LABEL_PREFIX],
|
|
964
|
+
base,
|
|
965
|
+
// Same default as config.ts. Read from the env directly rather than through
|
|
966
|
+
// loadConfig, which now REFUSES without AV_AGENT_NAME (#921) — and doctor's
|
|
967
|
+
// whole job is to run when you do not know which agents exist.
|
|
968
|
+
apiUrl: env.AV_API_URL ?? "https://api.agentvault.chat",
|
|
969
|
+
uninstallCommand: (n2) => `AV_AGENT_NAME=${n2 || "<agent-name>"} agentvault-grok-bridge uninstall`,
|
|
970
|
+
installCommand: (n2) => `AV_AGENT_NAME=${n2 || "<agent-name>"} agentvault-grok-bridge install`,
|
|
971
|
+
readCreds: (dir) => {
|
|
972
|
+
const deviceId = readPersistedDeviceId(dir);
|
|
973
|
+
return deviceId ? { deviceId } : null;
|
|
974
|
+
},
|
|
975
|
+
credDirs: () => listCredDirs(base),
|
|
976
|
+
fetchStatus: (apiUrl, deviceId) => fetchDeviceStatus(apiUrl, deviceId),
|
|
977
|
+
otherLabels: () => otherFamilyLabels(backend)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
async function maybeRunDoctorSubcommand(argv, opts = {}) {
|
|
981
|
+
if (argv[2] !== "doctor") return false;
|
|
982
|
+
const log = opts.log ?? printLine;
|
|
983
|
+
try {
|
|
984
|
+
const report = await buildDoctorReport(opts.deps ?? realDoctorDeps(opts.env ?? process.env));
|
|
985
|
+
for (const line of formatDoctorReport(report)) log(line);
|
|
986
|
+
if (report.services.some((s2) => s2.verdict === "FAIL")) process.exitCode = 1;
|
|
987
|
+
} catch (e) {
|
|
988
|
+
log(`doctor could not run: ${e.message}`);
|
|
989
|
+
process.exitCode = 1;
|
|
990
|
+
}
|
|
991
|
+
return true;
|
|
661
992
|
}
|
|
662
|
-
var
|
|
663
|
-
var
|
|
664
|
-
"src/service/
|
|
993
|
+
var LABEL_PREFIX, OTHER_FAMILY_PREFIXES, PROBE_TIMEOUT_MS;
|
|
994
|
+
var init_doctor_cli = __esm({
|
|
995
|
+
"src/service/doctor-cli.ts"() {
|
|
665
996
|
"use strict";
|
|
666
|
-
init_config();
|
|
667
|
-
init_spec();
|
|
668
|
-
init_launcher();
|
|
669
997
|
init_backend();
|
|
670
|
-
|
|
998
|
+
init_config();
|
|
999
|
+
init_log();
|
|
1000
|
+
init_doctor();
|
|
1001
|
+
LABEL_PREFIX = "dev.agentvault.grok-";
|
|
1002
|
+
OTHER_FAMILY_PREFIXES = [
|
|
1003
|
+
"dev.agentvault.bridge-",
|
|
1004
|
+
// claude bridge, current label
|
|
1005
|
+
"com.agentvault.claude-bridge.",
|
|
1006
|
+
// claude bridge, pre-rename label (#837)
|
|
1007
|
+
"ai.hermes.gateway"
|
|
1008
|
+
// hermes, one service per profile
|
|
1009
|
+
];
|
|
1010
|
+
PROBE_TIMEOUT_MS = 8e3;
|
|
671
1011
|
}
|
|
672
1012
|
});
|
|
673
1013
|
|
|
@@ -98965,6 +99305,40 @@ async function startRoomMcpServer(opts) {
|
|
|
98965
99305
|
};
|
|
98966
99306
|
}
|
|
98967
99307
|
|
|
99308
|
+
// src/service/self-uninstall.ts
|
|
99309
|
+
init_subcommand();
|
|
99310
|
+
init_backend();
|
|
99311
|
+
init_log();
|
|
99312
|
+
var DEVICE_IS_GONE = "device_revoked";
|
|
99313
|
+
function selfUninstallOnTerminal(reason, env, deps = {}) {
|
|
99314
|
+
if (reason !== DEVICE_IS_GONE) return false;
|
|
99315
|
+
const log = deps.log ?? ((m2) => logLine("[grok-bridge] " + m2));
|
|
99316
|
+
let label;
|
|
99317
|
+
try {
|
|
99318
|
+
label = labelFor(env);
|
|
99319
|
+
} catch (e) {
|
|
99320
|
+
log(
|
|
99321
|
+
`device was revoked, but ${e.message} \u2014 cannot tell which service to remove. Remove it by hand with: AV_AGENT_NAME=<agent-name> agentvault-grok-bridge uninstall`
|
|
99322
|
+
);
|
|
99323
|
+
return false;
|
|
99324
|
+
}
|
|
99325
|
+
try {
|
|
99326
|
+
if (!deps.backend && process.platform !== "darwin" && process.platform !== "linux") return false;
|
|
99327
|
+
const backend = deps.backend ?? selectBackend(process.platform);
|
|
99328
|
+
if (!backend.status(label).loaded) return false;
|
|
99329
|
+
backend.uninstall(label);
|
|
99330
|
+
log(
|
|
99331
|
+
`device was revoked \u2014 removed the supervised service ${label} so it does not sit installed on this machine pointing at an agent that no longer exists. Your credentials are still on disk and were NOT touched; re-install with a fresh invite token from AgentVault to reconnect.`
|
|
99332
|
+
);
|
|
99333
|
+
return true;
|
|
99334
|
+
} catch (e) {
|
|
99335
|
+
log(
|
|
99336
|
+
`device was revoked, but removing the service ${label} failed: ${e.message}. Remove it by hand with: AV_AGENT_NAME=<agent-name> agentvault-grok-bridge uninstall`
|
|
99337
|
+
);
|
|
99338
|
+
return false;
|
|
99339
|
+
}
|
|
99340
|
+
}
|
|
99341
|
+
|
|
98968
99342
|
// src/bridge.ts
|
|
98969
99343
|
var RoomHushState = class {
|
|
98970
99344
|
until = /* @__PURE__ */ new Map();
|
|
@@ -99020,11 +99394,13 @@ var ActiveTarget = class {
|
|
|
99020
99394
|
function attachLifecycle2(channel, opts = {}) {
|
|
99021
99395
|
const log = opts.log ?? (() => {
|
|
99022
99396
|
});
|
|
99397
|
+
const selfUninstall = opts.selfUninstall ?? selfUninstallOnTerminal;
|
|
99023
99398
|
const onTerminal = opts.onTerminal ?? ((_reason, o2) => process.exit(o2.restart ? 1 : 0));
|
|
99024
99399
|
const terminal = (reason, restart) => {
|
|
99025
99400
|
log(
|
|
99026
99401
|
`terminal condition (${reason}) \u2014 ${restart ? "gave up reconnecting; supervisor may respawn" : "this device must re-enroll or another session has taken over"}. Re-run/re-install with a fresh invite token from AgentVault if this persists.`
|
|
99027
99402
|
);
|
|
99403
|
+
selfUninstall(reason, process.env);
|
|
99028
99404
|
onTerminal(reason, { restart });
|
|
99029
99405
|
};
|
|
99030
99406
|
channel.on("auth_failed", (e) => terminal(e.reason, false));
|
|
@@ -99178,6 +99554,10 @@ var shutdown = async (code = 0) => {
|
|
|
99178
99554
|
}
|
|
99179
99555
|
};
|
|
99180
99556
|
async function main() {
|
|
99557
|
+
const { maybeRunDoctorSubcommand: maybeRunDoctorSubcommand2 } = await Promise.resolve().then(() => (init_doctor_cli(), doctor_cli_exports));
|
|
99558
|
+
if (await maybeRunDoctorSubcommand2(process.argv)) {
|
|
99559
|
+
process.exit(process.exitCode ?? 0);
|
|
99560
|
+
}
|
|
99181
99561
|
const { maybeRunServiceSubcommand: maybeRunServiceSubcommand2 } = await Promise.resolve().then(() => (init_subcommand(), subcommand_exports));
|
|
99182
99562
|
if (maybeRunServiceSubcommand2(process.argv, process.env, {
|
|
99183
99563
|
log: (m2) => logLine("[grok-bridge] " + m2)
|
|
@@ -99187,7 +99567,7 @@ async function main() {
|
|
|
99187
99567
|
const info = parseInfoFlags(process.argv.slice(2));
|
|
99188
99568
|
if (info.showVersion || info.showHelp) {
|
|
99189
99569
|
const version2 = buildClientVersion2(
|
|
99190
|
-
true ? "0.
|
|
99570
|
+
true ? "0.4.0" : void 0
|
|
99191
99571
|
);
|
|
99192
99572
|
if (info.showVersion) printLine(version2);
|
|
99193
99573
|
if (info.showHelp) {
|
|
@@ -99226,11 +99606,6 @@ async function main() {
|
|
|
99226
99606
|
process.exit(0);
|
|
99227
99607
|
}
|
|
99228
99608
|
const cfg = loadConfig(process.env, process.argv.slice(2));
|
|
99229
|
-
if (cfg.installAfterEnroll) {
|
|
99230
|
-
assertInstallableEntrypoint(process.argv[1]);
|
|
99231
|
-
const { selectBackend: selectBackend2 } = await Promise.resolve().then(() => (init_backend(), backend_exports));
|
|
99232
|
-
selectBackend2(process.platform);
|
|
99233
|
-
}
|
|
99234
99609
|
mkdirSync6(cfg.workspaceDir, { recursive: true });
|
|
99235
99610
|
mkdirSync6(cfg.fenceHome, { recursive: true });
|
|
99236
99611
|
seedFenceCredential({
|
|
@@ -99245,7 +99620,7 @@ async function main() {
|
|
|
99245
99620
|
agentName: cfg.agentName,
|
|
99246
99621
|
platform: "node",
|
|
99247
99622
|
clientVersion: buildClientVersion2(
|
|
99248
|
-
true ? "0.
|
|
99623
|
+
true ? "0.4.0" : void 0
|
|
99249
99624
|
)
|
|
99250
99625
|
});
|
|
99251
99626
|
channelRef = channel;
|
|
@@ -99336,23 +99711,6 @@ async function main() {
|
|
|
99336
99711
|
logLine("[grok-bridge] enrolled \u2014 pending invite consumed and removed");
|
|
99337
99712
|
}
|
|
99338
99713
|
});
|
|
99339
|
-
if (cfg.installAfterEnroll) {
|
|
99340
|
-
const { installOnReady: installOnReady2 } = await Promise.resolve().then(() => (init_install_on_ready(), install_on_ready_exports));
|
|
99341
|
-
installOnReady2(channel, cfg, {
|
|
99342
|
-
entrypoint: process.argv[1],
|
|
99343
|
-
env: process.env,
|
|
99344
|
-
// FOUND LIVE 2026-08-16: the owner's activation greeting is usually already
|
|
99345
|
-
// being answered when `ready` fires, and reaping mid-turn killed the reply
|
|
99346
|
-
// for good — the message was acked, so the service never saw it again.
|
|
99347
|
-
// GrokSession.idle() resolves when the queue drains.
|
|
99348
|
-
waitForIdle: () => session.idle(),
|
|
99349
|
-
// The one shared teardown. `exiting` is deliberately NOT set here: Ctrl-C
|
|
99350
|
-
// must keep working for the whole multi-second launchctl window.
|
|
99351
|
-
reap: reapAll,
|
|
99352
|
-
exit: (code) => process.exit(code),
|
|
99353
|
-
log: (m2) => logLine("[grok-bridge] " + m2)
|
|
99354
|
-
});
|
|
99355
|
-
}
|
|
99356
99714
|
await channel.start();
|
|
99357
99715
|
}
|
|
99358
99716
|
main().catch(async (err) => {
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { ServiceSpec } from "./spec.js";
|
|
2
|
+
/** One installed service found on disk, with the env its definition bakes in. */
|
|
3
|
+
export interface InstalledService {
|
|
4
|
+
label: string;
|
|
5
|
+
/** Absolute path of the plist / unit file. */
|
|
6
|
+
path: string;
|
|
7
|
+
env: Record<string, string>;
|
|
8
|
+
}
|
|
2
9
|
export interface ServiceBackend {
|
|
3
10
|
install(spec: ServiceSpec): void;
|
|
4
11
|
uninstall(label: string): void;
|
|
12
|
+
/** #913 item 3 — every installed service whose label starts with `prefix`,
|
|
13
|
+
* read from the definition DIRECTORY, not from the running-service list. An
|
|
14
|
+
* orphan is installed-but-not-running by construction. */
|
|
15
|
+
list(prefix: string): InstalledService[];
|
|
5
16
|
/** Restart an ALREADY-INSTALLED service without touching its spec (#837).
|
|
6
17
|
* Distinct from install, which re-bakes the plist from the current shell. */
|
|
7
18
|
restart(label: string): void;
|
|
@@ -23,6 +34,10 @@ export interface BackendDeps {
|
|
|
23
34
|
chmod: (path: string, mode: number) => void;
|
|
24
35
|
rm: (path: string) => void;
|
|
25
36
|
exists: (path: string) => boolean;
|
|
37
|
+
/** Names in a directory. Returns [] when the directory does not exist — a
|
|
38
|
+
* machine with no services installed is not an error. */
|
|
39
|
+
readdir: (path: string) => string[];
|
|
40
|
+
readFile: (path: string) => string;
|
|
26
41
|
}
|
|
27
42
|
export declare function defaultDeps(): BackendDeps;
|
|
28
43
|
export declare function selectBackend(platform: NodeJS.Platform, deps?: BackendDeps): ServiceBackend;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/service/backend.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;kFAC8E;IAC9E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/service/backend.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;+DAE2D;IAC3D,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAAC;IACzC;kFAC8E;IAC9E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAClC;8DAC0D;IAC1D,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACpC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACpC;AAED,wBAAgB,WAAW,IAAI,WAAW,CAoCzC;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,cAAc,CAU3F"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type DeviceProbe, type DoctorDeps } from "./doctor.js";
|
|
2
|
+
/**
|
|
3
|
+
* Ask the public status endpoint about one device.
|
|
4
|
+
*
|
|
5
|
+
* `GET /api/v1/devices/{id}/status` needs no auth and is rate limited per
|
|
6
|
+
* device_id, so checking several agents in a row is fine.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ A 404 is the strongest claim this command makes — it is what turns into
|
|
9
|
+
* "orphan, run uninstall". So it is only believed when the body is JSON, i.e.
|
|
10
|
+
* an actual API error shape. `agentvault.dev` serves a GoDaddy parked page that
|
|
11
|
+
* answers with confident HTTP codes and no API behind it, and on 2026-07-25
|
|
12
|
+
* that fooled a smoke check into reporting a healthy backend. A wrong
|
|
13
|
+
* AV_API_URL must read as "I could not check", never as "all your agents are
|
|
14
|
+
* dead".
|
|
15
|
+
*/
|
|
16
|
+
export declare function fetchDeviceStatus(apiUrl: string, deviceId: string, doFetch?: typeof fetch): Promise<DeviceProbe>;
|
|
17
|
+
export declare function realDoctorDeps(env: NodeJS.ProcessEnv): DoctorDeps;
|
|
18
|
+
/**
|
|
19
|
+
* Handle `agentvault-grok-bridge doctor`. Returns true when handled.
|
|
20
|
+
*
|
|
21
|
+
* Async, which is why it does not live in maybeRunServiceSubcommand: that
|
|
22
|
+
* dispatcher is synchronous and doctor has to await the network. index.ts runs
|
|
23
|
+
* this FIRST. If it ever did not, `doctor` would fall through to the
|
|
24
|
+
* unknown-subcommand guard and throw — loud, and safe. A typo must never boot a
|
|
25
|
+
* daemon (#921), and neither must a command we forgot to wire.
|
|
26
|
+
*
|
|
27
|
+
* READ-ONLY by construction: it never calls install, uninstall or restart. It
|
|
28
|
+
* prints the command instead, because deciding to remove a customer's service
|
|
29
|
+
* is theirs to make with the evidence in front of them.
|
|
30
|
+
*/
|
|
31
|
+
export declare function maybeRunDoctorSubcommand(argv: string[], opts?: {
|
|
32
|
+
log?: (msg: string) => void;
|
|
33
|
+
deps?: DoctorDeps;
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
}): Promise<boolean>;
|
|
36
|
+
//# sourceMappingURL=doctor-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-cli.d.ts","sourceRoot":"","sources":["../../src/service/doctor-cli.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAoBrB;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,WAAW,CAAC,CAuBtB;AA+BD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,UAAU,CAsBjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACrF,OAAO,CAAC,OAAO,CAAC,CAelB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ServiceBackend } from "./backend.js";
|
|
2
|
+
/**
|
|
3
|
+
* #913 item 3 — `doctor`: list what is installed, check each against the API,
|
|
4
|
+
* and say which ones are orphaned.
|
|
5
|
+
*
|
|
6
|
+
* Deleting an agent in the dashboard removes the DEVICE ROW and nothing else.
|
|
7
|
+
* The service stays installed, pointing at a device that no longer exists, and
|
|
8
|
+
* the customer has no signal at all. Item 1 names the uninstall command AT
|
|
9
|
+
* DELETE TIME; item 2 removes the service when the running bridge is told the
|
|
10
|
+
* device is gone. Neither helps the machine that already accumulated orphans —
|
|
11
|
+
* from a delete done in another browser, before those shipped, or while the
|
|
12
|
+
* bridge was not running. That is what this reads.
|
|
13
|
+
*
|
|
14
|
+
* TWO RULES, both earned the hard way:
|
|
15
|
+
*
|
|
16
|
+
* 1. AN UNREACHABLE API IS NOT AN ORPHAN. #919: a default for a missing input is
|
|
17
|
+
* a guess, and reporting on it is a lie. Every inconclusive probe is SKIP,
|
|
18
|
+
* never FAIL — including a status string this build does not recognise.
|
|
19
|
+
* Printing FAIL on a network blip sends a customer to uninstall a healthy
|
|
20
|
+
* agent.
|
|
21
|
+
*
|
|
22
|
+
* 2. SILENCE ABOUT WHAT IT CANNOT SEE IS A FALSE ALL-CLEAR. #941: the delete
|
|
23
|
+
* modal covered 3 of 15 devices and said nothing for the other 12, which
|
|
24
|
+
* reads as "nothing to clean up". This command can only check its own family
|
|
25
|
+
* — the agents whose credentials it can read — so it LISTS the other
|
|
26
|
+
* AgentVault services it found and says it did not check them.
|
|
27
|
+
*/
|
|
28
|
+
export type Verdict = "PASS" | "WARN" | "FAIL" | "SKIP";
|
|
29
|
+
/** What the server said about one device. Exactly one field is meaningful. */
|
|
30
|
+
export interface DeviceProbe {
|
|
31
|
+
/** The device's status string, verbatim from the API. */
|
|
32
|
+
status?: string;
|
|
33
|
+
/** 404 — the device row does not exist. */
|
|
34
|
+
gone?: boolean;
|
|
35
|
+
/** Why the check could not conclude. Present => the answer is UNKNOWN. */
|
|
36
|
+
inconclusive?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ServiceFinding {
|
|
39
|
+
label: string;
|
|
40
|
+
agentName: string;
|
|
41
|
+
dataDir: string;
|
|
42
|
+
loaded: boolean;
|
|
43
|
+
deviceId: string;
|
|
44
|
+
verdict: Verdict;
|
|
45
|
+
detail: string;
|
|
46
|
+
/** The exact command that fixes it. Set ONLY for a confirmed orphan. */
|
|
47
|
+
fix?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface UnsupervisedFinding {
|
|
50
|
+
dataDir: string;
|
|
51
|
+
agentName: string;
|
|
52
|
+
deviceId: string;
|
|
53
|
+
verdict: Verdict;
|
|
54
|
+
detail: string;
|
|
55
|
+
/** How to bring a live agent back. Never a suggestion to delete key material:
|
|
56
|
+
* `uninstall` leaves credentials ON PURPOSE, and a doctor that says "remove
|
|
57
|
+
* this directory" turns a recoverable mistake into a lost identity. */
|
|
58
|
+
fix?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface DoctorReport {
|
|
61
|
+
labelPrefixes: string[];
|
|
62
|
+
services: ServiceFinding[];
|
|
63
|
+
/** Credentials on disk that no installed service points at — CHECKED against
|
|
64
|
+
* the API, not merely listed. "Leftover from a deleted agent" and "a live
|
|
65
|
+
* agent whose service was removed" look identical on disk and need opposite
|
|
66
|
+
* responses, so the verdict is the whole point. */
|
|
67
|
+
unsupervised: UnsupervisedFinding[];
|
|
68
|
+
/** AgentVault services from other families. Found, listed, NOT checked. */
|
|
69
|
+
notChecked: string[];
|
|
70
|
+
}
|
|
71
|
+
export interface DoctorDeps {
|
|
72
|
+
backend: ServiceBackend;
|
|
73
|
+
/** Every label prefix this bridge OWNS — plural because #837 renamed the
|
|
74
|
+
* claude label and pre-rename installs are still registered under the old
|
|
75
|
+
* one. Those are the longest-running services on customer machines, i.e. the
|
|
76
|
+
* ones most likely to have accumulated an orphan. */
|
|
77
|
+
labelPrefixes: string[];
|
|
78
|
+
base: string;
|
|
79
|
+
apiUrl: string;
|
|
80
|
+
uninstallCommand: (agentName: string) => string;
|
|
81
|
+
installCommand: (agentName: string) => string;
|
|
82
|
+
readCreds: (dataDir: string) => {
|
|
83
|
+
deviceId?: string;
|
|
84
|
+
} | null;
|
|
85
|
+
/** Absolute data dirs under `base` that hold credentials. */
|
|
86
|
+
credDirs: () => string[];
|
|
87
|
+
fetchStatus: (apiUrl: string, deviceId: string) => Promise<DeviceProbe>;
|
|
88
|
+
/** Labels belonging to other AgentVault families installed on this machine. */
|
|
89
|
+
otherLabels: () => string[];
|
|
90
|
+
}
|
|
91
|
+
export declare function buildDoctorReport(d: DoctorDeps): Promise<DoctorReport>;
|
|
92
|
+
export declare function formatDoctorReport(r: DoctorReport): string[];
|
|
93
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/service/doctor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAExD,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;4EAEwE;IACxE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B;;;wDAGoD;IACpD,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,2EAA2E;IAC3E,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,cAAc,CAAC;IACxB;;;0DAGsD;IACtD,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9C,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7D,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,MAAM,EAAE,CAAC;IACzB,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACxE,+EAA+E;IAC/E,WAAW,EAAE,MAAM,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAqB5E;AAiJD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,EAAE,CAqD5D"}
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import type { ServiceSpec } from "./spec.js";
|
|
2
|
-
import type { BackendDeps, ServiceBackend } from "./backend.js";
|
|
2
|
+
import type { BackendDeps, ServiceBackend, InstalledService } from "./backend.js";
|
|
3
3
|
/** Render a launchd LaunchAgent plist for a bridge ServiceSpec. Pure string. */
|
|
4
4
|
export declare function renderLaunchdPlist(spec: ServiceSpec): string;
|
|
5
|
+
/**
|
|
6
|
+
* Read the EnvironmentVariables dict back out of an installed plist.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately tolerant of formatting, and deliberately NOT tolerant of scope.
|
|
9
|
+
*
|
|
10
|
+
* TOLERANT: `loopita`'s plist is hand-edited, so it keeps none of the
|
|
11
|
+
* whitespace `renderLaunchdPlist` writes. A parser that only understood its own
|
|
12
|
+
* output would report "no data dir" for precisely the long-lived install a
|
|
13
|
+
* customer is most likely to be asking `doctor` about.
|
|
14
|
+
*
|
|
15
|
+
* NOT TOLERANT: it reads ONLY the EnvironmentVariables dict. `Label`,
|
|
16
|
+
* `WorkingDirectory` and `StandardOutPath` are <key>/<string> pairs too, so a
|
|
17
|
+
* whole-file scan would hand back a log path as if it were AV_DATA_DIR and then
|
|
18
|
+
* go looking for credentials in it.
|
|
19
|
+
*
|
|
20
|
+
* Returns {} for junk. A doctor that throws on one malformed plist reports
|
|
21
|
+
* nothing about the other five.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parsePlistEnv(xml: string): Record<string, string>;
|
|
5
24
|
export declare class LaunchdBackend implements ServiceBackend {
|
|
6
25
|
private deps;
|
|
7
26
|
constructor(deps: BackendDeps);
|
|
@@ -17,5 +36,12 @@ export declare class LaunchdBackend implements ServiceBackend {
|
|
|
17
36
|
loaded: boolean;
|
|
18
37
|
detail: string;
|
|
19
38
|
};
|
|
39
|
+
/** #913 item 3 — every installed service whose label starts with `prefix`.
|
|
40
|
+
*
|
|
41
|
+
* Reads the plist directory rather than `launchctl list`, because an ORPHAN
|
|
42
|
+
* is typically installed-but-not-loaded: the revoked device exits 0 and
|
|
43
|
+
* KeepAlive leaves it down. `launchctl list` would not show the very thing
|
|
44
|
+
* doctor exists to find. */
|
|
45
|
+
list(prefix: string): InstalledService[];
|
|
20
46
|
}
|
|
21
47
|
//# sourceMappingURL=launchd.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launchd.d.ts","sourceRoot":"","sources":["../../src/service/launchd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"launchd.d.ts","sourceRoot":"","sources":["../../src/service/launchd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAUlF,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAkC5D;AAGD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA6BjE;AAOD,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,WAAW;IAErC,OAAO,CAAC,SAAS;IAGjB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IA2BhC,OAAO,CAAC,aAAa;IAOrB;+EAC2E;IAC3E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM9B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAK1D;;;;;iCAK6B;IAC7B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAezC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ServiceBackend } from "./backend.js";
|
|
2
|
+
export interface SelfUninstallDeps {
|
|
3
|
+
backend?: ServiceBackend;
|
|
4
|
+
log?: (msg: string) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Remove this agent's supervised service after its device was revoked.
|
|
8
|
+
*
|
|
9
|
+
* Returns whether a service was actually removed. NEVER throws: this runs on
|
|
10
|
+
* the shutdown path, and a cleanup that cannot happen must not stop the bridge
|
|
11
|
+
* exiting cleanly — `KeepAlive {SuccessfulExit: false}` depends on that exit(0)
|
|
12
|
+
* to leave the service down rather than crash-looping it.
|
|
13
|
+
*
|
|
14
|
+
* The stakes are asymmetric and worth stating: `uninstall` removes the SERVICE
|
|
15
|
+
* and LEAVES the credentials on disk. A false positive therefore costs the
|
|
16
|
+
* customer a re-install, not their identity.
|
|
17
|
+
*/
|
|
18
|
+
export declare function selfUninstallOnTerminal(reason: string, env: NodeJS.ProcessEnv, deps?: SelfUninstallDeps): boolean;
|
|
19
|
+
//# sourceMappingURL=self-uninstall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-uninstall.d.ts","sourceRoot":"","sources":["../../src/service/self-uninstall.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAuBlE,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAyCT"}
|
|
@@ -4,6 +4,7 @@ export interface ServiceCmdDeps {
|
|
|
4
4
|
entrypoint?: string;
|
|
5
5
|
log?: (msg: string) => void;
|
|
6
6
|
}
|
|
7
|
+
export declare function labelFor(env: NodeJS.ProcessEnv): string;
|
|
7
8
|
/** Handle `agentvault-grok-bridge <install|uninstall|status|logs>` before boot.
|
|
8
9
|
* Returns true when handled (caller should exit). Secrets are never placed on
|
|
9
10
|
* the service command line — config flows via the plist env; the token is omitted. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../src/service/subcommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAElE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;
|
|
1
|
+
{"version":3,"file":"subcommand.d.ts","sourceRoot":"","sources":["../../src/service/subcommand.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAElE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AA6CD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,CAEvD;AAED;;uFAEuF;AACvF,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,IAAI,CAAC,EAAE,cAAc,GACpB,OAAO,CAgIT"}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { ServiceSpec } from "./spec.js";
|
|
2
|
-
import type { BackendDeps, ServiceBackend } from "./backend.js";
|
|
2
|
+
import type { BackendDeps, ServiceBackend, InstalledService } from "./backend.js";
|
|
3
3
|
/** Render a systemd --user service unit for a bridge ServiceSpec. Pure string.
|
|
4
4
|
* Restart=on-failure + SuccessExitStatus=0 is the KeepAlive={SuccessfulExit:false}
|
|
5
5
|
* equivalent: a clean exit(0) (revoked device) is treated as success => NOT
|
|
6
6
|
* restarted; a non-zero crash is restarted. WantedBy=default.target is RunAtLoad. */
|
|
7
7
|
export declare function renderSystemdUnit(spec: ServiceSpec): string;
|
|
8
|
+
/**
|
|
9
|
+
* Read `Environment=` back out of an installed systemd unit.
|
|
10
|
+
*
|
|
11
|
+
* Reverses `q` exactly: un-double the '%' first, then walk the backslash
|
|
12
|
+
* escapes left to right in ONE pass. Two passes would turn a literal
|
|
13
|
+
* backslash-n (written as \\n) into a newline.
|
|
14
|
+
*
|
|
15
|
+
* Returns {} for junk — a doctor that throws on one malformed unit reports
|
|
16
|
+
* nothing about the others.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseUnitEnv(text: string): Record<string, string>;
|
|
8
19
|
export declare class SystemdBackend implements ServiceBackend {
|
|
9
20
|
private deps;
|
|
10
21
|
constructor(deps: BackendDeps);
|
|
@@ -18,5 +29,12 @@ export declare class SystemdBackend implements ServiceBackend {
|
|
|
18
29
|
loaded: boolean;
|
|
19
30
|
detail: string;
|
|
20
31
|
};
|
|
32
|
+
/** #913 item 3 — every installed unit whose label starts with `prefix`.
|
|
33
|
+
*
|
|
34
|
+
* Reads the unit directory rather than `systemctl list-units`, because an
|
|
35
|
+
* ORPHAN is installed-but-inactive: the revoked device exits 0, which
|
|
36
|
+
* SuccessExitStatus=0 treats as success, so it is never restarted. A
|
|
37
|
+
* running-units listing would not show the thing doctor exists to find. */
|
|
38
|
+
list(prefix: string): InstalledService[];
|
|
21
39
|
}
|
|
22
40
|
//# sourceMappingURL=systemd.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"systemd.d.ts","sourceRoot":"","sources":["../../src/service/systemd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"systemd.d.ts","sourceRoot":"","sources":["../../src/service/systemd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAmBlF;;;sFAGsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAsB3D;AAGD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWjE;AAQD,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,IAAI;gBAAJ,IAAI,EAAE,WAAW;IAErC,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI;IAoBhC;2DACuD;IACvD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAU9B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAS1D;;;;;gFAK4E;IAC5E,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAczC"}
|
package/package.json
CHANGED