@indigoai-us/hq-cli 5.345.179 → 5.345.181
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/CHANGELOG.md +23 -0
- package/dist/command-catalog.generated.d.ts +126 -0
- package/dist/command-catalog.generated.js +155 -0
- package/dist/commands/bot.d.ts +29 -1
- package/dist/commands/bot.js +282 -22
- package/dist/commands/channels.d.ts +18 -1
- package/dist/commands/channels.js +185 -1
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +6 -2
- package/dist/commands/mesh.js +19 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/config.d.ts +8 -0
- package/dist/lib/bot/fleet-local.d.ts +7 -3
- package/dist/lib/bot/fleet-local.js +234 -64
- package/dist/lib/bot/run.js +39 -2
- package/dist/lib/bot/runtime-auth.d.ts +119 -0
- package/dist/lib/bot/runtime-auth.js +251 -0
- package/dist/lib/daemon/catalog.d.ts +1 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +64 -16
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hot-channels/adapters/meetings.js +3 -1
- package/dist/lib/hot-channels/contract.d.ts +6 -0
- package/dist/lib/hot-channels/ledger.js +17 -0
- package/dist/lib/hot-channels/loop.js +2 -0
- package/dist/lib/lanes/accounts.d.ts +1 -17
- package/dist/lib/lanes/accounts.js +5 -45
- package/dist/lib/lanes/provider-home.d.ts +24 -0
- package/dist/lib/lanes/provider-home.js +52 -0
- package/dist/lib/lanes/spawn.d.ts +12 -0
- package/dist/lib/lanes/spawn.js +27 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +13 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
- package/dist/lib/mesh/live/daemon/run.js +31 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +2 -0
- package/dist/lib/sync-core/flag.d.ts +1 -1
- package/dist/lib/sync-core/flag.js +1 -1
- package/dist/lib/sync-core/reconcile.d.ts +1 -1
- package/dist/lib/sync-core/reconcile.js +9 -7
- package/dist/lib/sync-core/release.d.ts +3 -3
- package/dist/lib/sync-core/release.js +4 -4
- package/dist/lib/sync-core/rollout-state.d.ts +1 -0
- package/dist/lib/sync-core/rollout-state.js +1 -0
- package/package.json +8 -8
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which model login a local bot uses, per runtime.
|
|
3
|
+
*
|
|
4
|
+
* - default: the owner's own sign-in for that runtime (no config dir set).
|
|
5
|
+
* - own: a sign-in that belongs to the bot alone, kept in
|
|
6
|
+
* ~/.hq/bots/<name>/auth/<runtime>/ (CLAUDE_CONFIG_DIR / CODEX_HOME).
|
|
7
|
+
* - account: an existing `hq capacity` / lanes account home, by id.
|
|
8
|
+
*
|
|
9
|
+
* Also the session set-aside a runtime switch needs: session files belong to
|
|
10
|
+
* one runtime, so they move to a backup folder and the new runtime starts
|
|
11
|
+
* fresh. DM history, memory and the cloud identity are never touched.
|
|
12
|
+
*
|
|
13
|
+
* This module never reads, prints, copies or hashes a credential. It only
|
|
14
|
+
* names directories and checks whether they exist.
|
|
15
|
+
*/
|
|
16
|
+
import type { BotConfig, BotRuntimeId } from "./config.js";
|
|
17
|
+
import type { LanesConfig } from "../lanes/types.js";
|
|
18
|
+
/** Runtimes `hq bot login` can sign in for (each has a config-dir variable and a login command). */
|
|
19
|
+
export declare const BOT_LOGIN_RUNTIMES: readonly ["claude", "codex"];
|
|
20
|
+
export type BotLoginRuntime = (typeof BOT_LOGIN_RUNTIMES)[number];
|
|
21
|
+
export declare function isBotLoginRuntime(value: unknown): value is BotLoginRuntime;
|
|
22
|
+
export type BotRuntimeAuth = {
|
|
23
|
+
mode: "own";
|
|
24
|
+
} | {
|
|
25
|
+
mode: "account";
|
|
26
|
+
account: string;
|
|
27
|
+
};
|
|
28
|
+
export type BotRuntimeAuthMap = Partial<Record<BotRuntimeId, BotRuntimeAuth>>;
|
|
29
|
+
export declare const BOT_AUTH_DIRNAME = "auth";
|
|
30
|
+
export declare const BOT_SESSION_ARCHIVE_DIRNAME = "session-archive";
|
|
31
|
+
export declare const VENDOR_LOGIN: Record<BotLoginRuntime, {
|
|
32
|
+
command: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
}>;
|
|
35
|
+
export declare const VENDOR_LOGOUT: Record<BotLoginRuntime, {
|
|
36
|
+
command: string;
|
|
37
|
+
args: string[];
|
|
38
|
+
}>;
|
|
39
|
+
/** The bot's own config home for a runtime: <botDir>/auth/<runtime>. */
|
|
40
|
+
export declare function botOwnAuthHome(dir: string, runtime: BotRuntimeId): string;
|
|
41
|
+
/** The saved login choice for a runtime, or null for the owner's default. Tolerant of junk on read. */
|
|
42
|
+
export declare function botRuntimeAuth(config: Pick<BotConfig, "runtime" | "runtimeAuth">, runtime?: BotRuntimeId): BotRuntimeAuth | null;
|
|
43
|
+
/** A copy of the map with `runtime` set to `auth` (null = back to the owner's default). */
|
|
44
|
+
export declare function withRuntimeAuth(map: BotRuntimeAuthMap | undefined, runtime: BotRuntimeId, auth: BotRuntimeAuth | null): BotRuntimeAuthMap | undefined;
|
|
45
|
+
export interface AccountLookupDeps {
|
|
46
|
+
loadConfig?: (hqRoot: string) => LanesConfig;
|
|
47
|
+
existsSync?: (p: string) => boolean;
|
|
48
|
+
}
|
|
49
|
+
/** The home directory of a capacity account for a runtime. Throws an expected error when it is not usable. */
|
|
50
|
+
export declare function capacityAccountHome(hqRoot: string, runtime: BotRuntimeId, accountId: string, deps?: AccountLookupDeps): string;
|
|
51
|
+
/**
|
|
52
|
+
* The environment a bot's model turn runs with: `base` with the runtime's
|
|
53
|
+
* config-dir variable pointed at the bot's chosen login. A missing own login
|
|
54
|
+
* or a gone account is an error: falling back to the owner's login is the
|
|
55
|
+
* thing this setting exists to prevent.
|
|
56
|
+
*/
|
|
57
|
+
export declare function botAuthEnv(dir: string, config: Pick<BotConfig, "name" | "runtime" | "runtimeAuth" | "hqRoot">, base: NodeJS.ProcessEnv, deps?: AccountLookupDeps): NodeJS.ProcessEnv;
|
|
58
|
+
export interface BotAuthDescription {
|
|
59
|
+
runtime: BotRuntimeId;
|
|
60
|
+
login: "default" | "own" | "account" | "invalid";
|
|
61
|
+
account?: string;
|
|
62
|
+
/** The config-dir variable the bot's turns set (never its value's contents). */
|
|
63
|
+
envVar?: string;
|
|
64
|
+
/** Own login: whether its folder exists. Never a token check. */
|
|
65
|
+
present?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export declare function describeBotAuth(dir: string, config: Pick<BotConfig, "runtime" | "runtimeAuth">, existsSync?: (p: string) => boolean): BotAuthDescription;
|
|
68
|
+
export declare function formatBotAuth(d: BotAuthDescription, name: string): string;
|
|
69
|
+
export type SpawnInteractive = (command: string, args: string[], env: NodeJS.ProcessEnv) => {
|
|
70
|
+
status: number | null;
|
|
71
|
+
error?: Error;
|
|
72
|
+
};
|
|
73
|
+
/** Runs the vendor's own login/logout with the terminal attached. Its output goes to the terminal only, never to HQ's log. */
|
|
74
|
+
export declare const defaultSpawnInteractive: SpawnInteractive;
|
|
75
|
+
/** Logout runs quietly: nothing it prints is shown or logged. */
|
|
76
|
+
export declare const defaultSpawnQuiet: SpawnInteractive;
|
|
77
|
+
/**
|
|
78
|
+
* Sign a bot in with its own login for `runtime`. The vendor CLI writes the
|
|
79
|
+
* credential into the bot's home (or, for Claude on macOS, a keychain item
|
|
80
|
+
* keyed by that home). HQ never sees it.
|
|
81
|
+
*/
|
|
82
|
+
export declare function signInBotRuntime(dir: string, runtime: BotLoginRuntime, deps?: {
|
|
83
|
+
spawn?: SpawnInteractive;
|
|
84
|
+
quietSpawn?: SpawnInteractive;
|
|
85
|
+
env?: NodeJS.ProcessEnv;
|
|
86
|
+
}): {
|
|
87
|
+
home: string;
|
|
88
|
+
created: boolean;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Sign the bot's own login out and delete its home. The vendor logout runs
|
|
92
|
+
* first so a Claude keychain item keyed by the home is removed too.
|
|
93
|
+
*/
|
|
94
|
+
export declare function signOutBotRuntime(dir: string, runtime: BotLoginRuntime, deps?: {
|
|
95
|
+
spawn?: SpawnInteractive;
|
|
96
|
+
env?: NodeJS.ProcessEnv;
|
|
97
|
+
}): {
|
|
98
|
+
removed: boolean;
|
|
99
|
+
vendorLogout: "ok" | "failed" | "skipped";
|
|
100
|
+
};
|
|
101
|
+
/** True for the model-session files of a bot dir (owner DM, other DMs, rooms, jobs). */
|
|
102
|
+
export declare function isBotSessionFile(name: string): boolean;
|
|
103
|
+
export interface SessionSetAside {
|
|
104
|
+
archiveDir: string;
|
|
105
|
+
moved: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Before a runtime switch: back up bot.json and move every session file into
|
|
109
|
+
* <dir>/session-archive/<stamp>-<oldRuntime>/, so the new runtime starts
|
|
110
|
+
* fresh and the old sessions can be put back.
|
|
111
|
+
*/
|
|
112
|
+
export declare function setAsideBotSessions(dir: string, oldRuntime: BotRuntimeId, now?: Date): SessionSetAside;
|
|
113
|
+
/**
|
|
114
|
+
* Undo setAsideBotSessions: the moved session files go back. bot.json is not
|
|
115
|
+
* overwritten (the bot may have written fields since); the caller patches the
|
|
116
|
+
* settings it changed back. The backup folder stays for manual recovery.
|
|
117
|
+
*/
|
|
118
|
+
export declare function restoreBotSessions(dir: string, aside: SessionSetAside): void;
|
|
119
|
+
//# sourceMappingURL=runtime-auth.d.ts.map
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which model login a local bot uses, per runtime.
|
|
3
|
+
*
|
|
4
|
+
* - default: the owner's own sign-in for that runtime (no config dir set).
|
|
5
|
+
* - own: a sign-in that belongs to the bot alone, kept in
|
|
6
|
+
* ~/.hq/bots/<name>/auth/<runtime>/ (CLAUDE_CONFIG_DIR / CODEX_HOME).
|
|
7
|
+
* - account: an existing `hq capacity` / lanes account home, by id.
|
|
8
|
+
*
|
|
9
|
+
* Also the session set-aside a runtime switch needs: session files belong to
|
|
10
|
+
* one runtime, so they move to a backup folder and the new runtime starts
|
|
11
|
+
* fresh. DM history, memory and the cloud identity are never touched.
|
|
12
|
+
*
|
|
13
|
+
* This module never reads, prints, copies or hashes a credential. It only
|
|
14
|
+
* names directories and checks whether they exist.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { botConfigPath } from "./paths.js";
|
|
20
|
+
import { PROVIDER_HOME_ENV, isDefaultProviderHome, withProviderHomeEnv } from "../lanes/provider-home.js";
|
|
21
|
+
import { effectiveAccountHome } from "../lanes/account-home.js";
|
|
22
|
+
import { loadLanesConfig } from "../lanes/config.js";
|
|
23
|
+
/** Runtimes `hq bot login` can sign in for (each has a config-dir variable and a login command). */
|
|
24
|
+
export const BOT_LOGIN_RUNTIMES = ["claude", "codex"];
|
|
25
|
+
export function isBotLoginRuntime(value) {
|
|
26
|
+
return typeof value === "string" && BOT_LOGIN_RUNTIMES.includes(value);
|
|
27
|
+
}
|
|
28
|
+
export const BOT_AUTH_DIRNAME = "auth";
|
|
29
|
+
export const BOT_SESSION_ARCHIVE_DIRNAME = "session-archive";
|
|
30
|
+
const ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
31
|
+
export const VENDOR_LOGIN = {
|
|
32
|
+
claude: { command: "claude", args: ["auth", "login"] },
|
|
33
|
+
codex: { command: "codex", args: ["login"] },
|
|
34
|
+
};
|
|
35
|
+
export const VENDOR_LOGOUT = {
|
|
36
|
+
claude: { command: "claude", args: ["auth", "logout"] },
|
|
37
|
+
codex: { command: "codex", args: ["logout"] },
|
|
38
|
+
};
|
|
39
|
+
function expected(message) {
|
|
40
|
+
return Object.assign(new Error(message), { expected: true });
|
|
41
|
+
}
|
|
42
|
+
/** The bot's own config home for a runtime: <botDir>/auth/<runtime>. */
|
|
43
|
+
export function botOwnAuthHome(dir, runtime) {
|
|
44
|
+
return path.join(dir, BOT_AUTH_DIRNAME, runtime);
|
|
45
|
+
}
|
|
46
|
+
/** The saved login choice for a runtime, or null for the owner's default. Tolerant of junk on read. */
|
|
47
|
+
export function botRuntimeAuth(config, runtime = config.runtime) {
|
|
48
|
+
const raw = config.runtimeAuth?.[runtime];
|
|
49
|
+
if (!raw || typeof raw !== "object")
|
|
50
|
+
return null;
|
|
51
|
+
if (raw.mode === "own")
|
|
52
|
+
return { mode: "own" };
|
|
53
|
+
if (raw.mode === "account" && typeof raw.account === "string" && ACCOUNT_ID_RE.test(raw.account)) {
|
|
54
|
+
return { mode: "account", account: raw.account };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/** A copy of the map with `runtime` set to `auth` (null = back to the owner's default). */
|
|
59
|
+
export function withRuntimeAuth(map, runtime, auth) {
|
|
60
|
+
const next = { ...(map ?? {}) };
|
|
61
|
+
if (auth)
|
|
62
|
+
next[runtime] = auth;
|
|
63
|
+
else
|
|
64
|
+
delete next[runtime];
|
|
65
|
+
return Object.keys(next).length ? next : undefined;
|
|
66
|
+
}
|
|
67
|
+
/** The home directory of a capacity account for a runtime. Throws an expected error when it is not usable. */
|
|
68
|
+
export function capacityAccountHome(hqRoot, runtime, accountId, deps = {}) {
|
|
69
|
+
if (!ACCOUNT_ID_RE.test(accountId))
|
|
70
|
+
throw expected(`"${accountId.slice(0, 40)}" is not an account id.`);
|
|
71
|
+
const cfg = (deps.loadConfig ?? loadLanesConfig)(hqRoot);
|
|
72
|
+
const provider = cfg.providers[runtime];
|
|
73
|
+
const account = provider?.accounts[accountId];
|
|
74
|
+
if (!account) {
|
|
75
|
+
const known = Object.keys(provider?.accounts ?? {});
|
|
76
|
+
throw expected(`No ${runtime} account named "${accountId}"${known.length ? ` (known: ${known.join(", ")})` : ""}. Add one with: hq capacity login ${runtime}`);
|
|
77
|
+
}
|
|
78
|
+
if (!account.enabled)
|
|
79
|
+
throw expected(`The ${runtime} account "${accountId}" is disabled.`);
|
|
80
|
+
const home = effectiveAccountHome(account).home;
|
|
81
|
+
if (isDefaultProviderHome(runtime, home)) {
|
|
82
|
+
throw expected(`The ${runtime} account "${accountId}" is your default ${runtime} login, so it would not give the bot a login of its own. Pick another account, or use: hq bot login`);
|
|
83
|
+
}
|
|
84
|
+
if (!(deps.existsSync ?? fs.existsSync)(home))
|
|
85
|
+
throw expected(`The ${runtime} account "${accountId}" has no home folder on this computer. Sign it in with: hq capacity login ${runtime} --account ${accountId}`);
|
|
86
|
+
return home;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The environment a bot's model turn runs with: `base` with the runtime's
|
|
90
|
+
* config-dir variable pointed at the bot's chosen login. A missing own login
|
|
91
|
+
* or a gone account is an error: falling back to the owner's login is the
|
|
92
|
+
* thing this setting exists to prevent.
|
|
93
|
+
*/
|
|
94
|
+
export function botAuthEnv(dir, config, base, deps = {}) {
|
|
95
|
+
const auth = botRuntimeAuth(config);
|
|
96
|
+
const runtime = config.runtime;
|
|
97
|
+
if (!auth) {
|
|
98
|
+
// A saved choice HQ cannot read is not the owner's default: refuse it.
|
|
99
|
+
if (config.runtimeAuth?.[runtime] !== undefined) {
|
|
100
|
+
throw expected(`${config.name}'s saved ${runtime} login setting is not readable. Set it again with: hq bot login ${config.name} --runtime ${runtime}, or hq bot set ${config.name} --account <id|default>`);
|
|
101
|
+
}
|
|
102
|
+
return { ...base };
|
|
103
|
+
}
|
|
104
|
+
if (auth.mode === "own") {
|
|
105
|
+
const home = botOwnAuthHome(dir, runtime);
|
|
106
|
+
if (!(deps.existsSync ?? fs.existsSync)(home)) {
|
|
107
|
+
throw expected(`${config.name} is set to its own ${runtime} login but has none on this computer. Sign it in with: hq bot login ${config.name} --runtime ${runtime}`);
|
|
108
|
+
}
|
|
109
|
+
return withProviderHomeEnv(base, runtime, home);
|
|
110
|
+
}
|
|
111
|
+
return withProviderHomeEnv(base, runtime, capacityAccountHome(config.hqRoot, runtime, auth.account, deps));
|
|
112
|
+
}
|
|
113
|
+
export function describeBotAuth(dir, config, existsSync = fs.existsSync) {
|
|
114
|
+
const auth = botRuntimeAuth(config);
|
|
115
|
+
const runtime = config.runtime;
|
|
116
|
+
if (!auth)
|
|
117
|
+
return config.runtimeAuth?.[runtime] !== undefined ? { runtime, login: "invalid" } : { runtime, login: "default" };
|
|
118
|
+
const envVar = PROVIDER_HOME_ENV[runtime];
|
|
119
|
+
if (auth.mode === "own")
|
|
120
|
+
return { runtime, login: "own", ...(envVar ? { envVar } : {}), present: existsSync(botOwnAuthHome(dir, runtime)) };
|
|
121
|
+
return { runtime, login: "account", account: auth.account, ...(envVar ? { envVar } : {}) };
|
|
122
|
+
}
|
|
123
|
+
export function formatBotAuth(d, name) {
|
|
124
|
+
if (d.login === "default")
|
|
125
|
+
return `default (your own ${d.runtime} sign-in)`;
|
|
126
|
+
if (d.login === "invalid")
|
|
127
|
+
return `unreadable setting; turns are refused until it is set again (hq bot login ${name} --runtime ${d.runtime})`;
|
|
128
|
+
if (d.login === "account")
|
|
129
|
+
return `account ${d.account}`;
|
|
130
|
+
return d.present ? `own (signed in for ${name} only)` : `own, but not signed in (run: hq bot login ${name} --runtime ${d.runtime})`;
|
|
131
|
+
}
|
|
132
|
+
/** Runs the vendor's own login/logout with the terminal attached. Its output goes to the terminal only, never to HQ's log. */
|
|
133
|
+
export const defaultSpawnInteractive = (command, args, env) => {
|
|
134
|
+
const r = spawnSync(command, args, { stdio: "inherit", env });
|
|
135
|
+
return { status: r.status, ...(r.error ? { error: r.error } : {}) };
|
|
136
|
+
};
|
|
137
|
+
/** Logout runs quietly: nothing it prints is shown or logged. */
|
|
138
|
+
export const defaultSpawnQuiet = (command, args, env) => {
|
|
139
|
+
const r = spawnSync(command, args, { stdio: ["ignore", "ignore", "ignore"], env, timeout: 30_000 });
|
|
140
|
+
return { status: r.status, ...(r.error ? { error: r.error } : {}) };
|
|
141
|
+
};
|
|
142
|
+
/** Refuse to touch anything that is not exactly <dir>/auth/<runtime>. */
|
|
143
|
+
function assertOwnAuthHome(dir, home) {
|
|
144
|
+
const root = path.resolve(dir, BOT_AUTH_DIRNAME);
|
|
145
|
+
const rel = path.relative(root, path.resolve(home));
|
|
146
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel) || rel.includes(path.sep)) {
|
|
147
|
+
throw new Error(`refusing to touch ${home}: not a bot login folder`);
|
|
148
|
+
}
|
|
149
|
+
for (const p of [root, home]) {
|
|
150
|
+
let link = false;
|
|
151
|
+
try {
|
|
152
|
+
link = fs.lstatSync(p).isSymbolicLink();
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
if (err.code !== "ENOENT")
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
if (link)
|
|
159
|
+
throw new Error(`refusing to touch ${p}: it is a symbolic link`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Sign a bot in with its own login for `runtime`. The vendor CLI writes the
|
|
164
|
+
* credential into the bot's home (or, for Claude on macOS, a keychain item
|
|
165
|
+
* keyed by that home). HQ never sees it.
|
|
166
|
+
*/
|
|
167
|
+
export function signInBotRuntime(dir, runtime, deps = {}) {
|
|
168
|
+
const home = botOwnAuthHome(dir, runtime);
|
|
169
|
+
assertOwnAuthHome(dir, home);
|
|
170
|
+
const created = !fs.existsSync(home);
|
|
171
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
172
|
+
fs.chmodSync(path.join(dir, BOT_AUTH_DIRNAME), 0o700);
|
|
173
|
+
fs.chmodSync(home, 0o700);
|
|
174
|
+
const { command, args } = VENDOR_LOGIN[runtime];
|
|
175
|
+
const r = (deps.spawn ?? defaultSpawnInteractive)(command, args, withProviderHomeEnv(deps.env ?? process.env, runtime, home));
|
|
176
|
+
if (r.error || r.status !== 0) {
|
|
177
|
+
// A first sign-in that failed leaves nothing behind.
|
|
178
|
+
if (created) {
|
|
179
|
+
// A keychain item keyed by this folder may already exist: sign it out first.
|
|
180
|
+
const { command: out, args: outArgs } = VENDOR_LOGOUT[runtime];
|
|
181
|
+
(deps.quietSpawn ?? defaultSpawnQuiet)(out, outArgs, withProviderHomeEnv(deps.env ?? process.env, runtime, home));
|
|
182
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
const why = r.error?.code === "ENOENT" ? `${command} is not installed` : `${command} ${args.join(" ")} did not finish (exit ${r.status ?? "signal"})`;
|
|
185
|
+
throw expected(`Sign-in failed: ${why}. Nothing was changed.`);
|
|
186
|
+
}
|
|
187
|
+
return { home, created };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Sign the bot's own login out and delete its home. The vendor logout runs
|
|
191
|
+
* first so a Claude keychain item keyed by the home is removed too.
|
|
192
|
+
*/
|
|
193
|
+
export function signOutBotRuntime(dir, runtime, deps = {}) {
|
|
194
|
+
const home = botOwnAuthHome(dir, runtime);
|
|
195
|
+
assertOwnAuthHome(dir, home);
|
|
196
|
+
if (!fs.existsSync(home))
|
|
197
|
+
return { removed: false, vendorLogout: "skipped" };
|
|
198
|
+
const { command, args } = VENDOR_LOGOUT[runtime];
|
|
199
|
+
const r = (deps.spawn ?? defaultSpawnQuiet)(command, args, withProviderHomeEnv(deps.env ?? process.env, runtime, home));
|
|
200
|
+
if (r.error || r.status !== 0) {
|
|
201
|
+
// Keep the folder: it is the only handle on a keychain item keyed by it.
|
|
202
|
+
return { removed: false, vendorLogout: "failed" };
|
|
203
|
+
}
|
|
204
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
205
|
+
return { removed: true, vendorLogout: "ok" };
|
|
206
|
+
}
|
|
207
|
+
/** True for the model-session files of a bot dir (owner DM, other DMs, rooms, jobs). */
|
|
208
|
+
export function isBotSessionFile(name) {
|
|
209
|
+
return name === "session.json" || /^session-(dm|room|job)-[A-Za-z0-9._-]+\.json$/.test(name);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Before a runtime switch: back up bot.json and move every session file into
|
|
213
|
+
* <dir>/session-archive/<stamp>-<oldRuntime>/, so the new runtime starts
|
|
214
|
+
* fresh and the old sessions can be put back.
|
|
215
|
+
*/
|
|
216
|
+
export function setAsideBotSessions(dir, oldRuntime, now = new Date()) {
|
|
217
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
218
|
+
const archiveDir = path.join(dir, BOT_SESSION_ARCHIVE_DIRNAME, `${stamp}-${oldRuntime}`);
|
|
219
|
+
fs.mkdirSync(archiveDir, { recursive: true, mode: 0o700 });
|
|
220
|
+
fs.copyFileSync(botConfigPath(dir), path.join(archiveDir, "bot.json"));
|
|
221
|
+
const moved = [];
|
|
222
|
+
try {
|
|
223
|
+
for (const name of fs.readdirSync(dir)) {
|
|
224
|
+
if (!isBotSessionFile(name))
|
|
225
|
+
continue;
|
|
226
|
+
fs.renameSync(path.join(dir, name), path.join(archiveDir, name));
|
|
227
|
+
moved.push(name);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
restoreBotSessions(dir, { archiveDir, moved });
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
return { archiveDir, moved };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Undo setAsideBotSessions: the moved session files go back. bot.json is not
|
|
238
|
+
* overwritten (the bot may have written fields since); the caller patches the
|
|
239
|
+
* settings it changed back. The backup folder stays for manual recovery.
|
|
240
|
+
*/
|
|
241
|
+
export function restoreBotSessions(dir, aside) {
|
|
242
|
+
for (const name of fs.readdirSync(dir)) {
|
|
243
|
+
// Files the new runtime wrote in the meantime are not the old runtime's.
|
|
244
|
+
if (isBotSessionFile(name) && !aside.moved.includes(name))
|
|
245
|
+
fs.rmSync(path.join(dir, name), { force: true });
|
|
246
|
+
}
|
|
247
|
+
for (const name of aside.moved) {
|
|
248
|
+
fs.copyFileSync(path.join(aside.archiveDir, name), path.join(dir, name));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=runtime-auth.js.map
|
|
@@ -189,6 +189,7 @@ export declare function hqCloudUnits(machineType: MachineType, enabled: readonly
|
|
|
189
189
|
export declare const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
|
|
190
190
|
/** The outpost's Claude runner script, read for the session name it uses. */
|
|
191
191
|
export declare const CLAUDE_RUNNER_SCRIPT = "/usr/local/bin/outpost-runner.sh";
|
|
192
|
+
export declare function daemonAloneRunsLegacySync(ctx: PlanContext): boolean;
|
|
192
193
|
/** Turn enabled services into concrete units for this host. */
|
|
193
194
|
export declare function planUnits(services: readonly ServiceName[], ctx: PlanContext): UnitPlan[];
|
|
194
195
|
export {};
|
|
@@ -148,7 +148,7 @@ function syncOwnerBlock(ctx) {
|
|
|
148
148
|
}
|
|
149
149
|
return undefined;
|
|
150
150
|
}
|
|
151
|
-
function daemonAloneRunsLegacySync(ctx) {
|
|
151
|
+
export function daemonAloneRunsLegacySync(ctx) {
|
|
152
152
|
return ctx.syncOwner === "daemon" && ctx.legacySyncUnitManagesSync !== true && syncOwnerBlock(ctx) === undefined;
|
|
153
153
|
}
|
|
154
154
|
function planService(service, ctx) {
|
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* signing in takes effect without restarting the daemon.
|
|
7
7
|
*/
|
|
8
8
|
import { type PlanContext, type ServiceName, type UnitPlan } from "./catalog.js";
|
|
9
|
+
import { type SyncCoreVaultPlan } from "../sync-core/engine-home.js";
|
|
9
10
|
import { type LegacyUnit, type WindowsTaskOwner } from "./legacy.js";
|
|
10
11
|
import { type DaemonLockResult } from "./lock.js";
|
|
11
12
|
import { type MachineType } from "./machine-type.js";
|
|
@@ -14,6 +15,7 @@ import { type HostApp, type Supervisor } from "./supervisor.js";
|
|
|
14
15
|
export declare const LOG_ROTATE_BYTES: number;
|
|
15
16
|
/** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
|
|
16
17
|
export declare const RESTART_EXIT_CODE = 75;
|
|
18
|
+
export declare function syncCoreVaultPlans(home: string, hqRoot: string | undefined, platform: NodeJS.Platform): SyncCoreVaultPlan[];
|
|
17
19
|
export declare function planUnitsWithSyncCoreInterlock(services: readonly ServiceName[], context: PlanContext, supervisor: Pick<Supervisor, "runningPlanKey" | "state"> | null | undefined): UnitPlan[];
|
|
18
20
|
/** A bounded shutdown deadline that can account for sync push persistence. */
|
|
19
21
|
export declare function createShutdownCap(): {
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -23,6 +23,7 @@ import { botsRoot } from "../bot/paths.js";
|
|
|
23
23
|
import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
|
|
24
24
|
import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
|
|
25
25
|
import { inspectEngineHome } from "../sync-core/engine-home.js";
|
|
26
|
+
import { fullModeHold } from "../sync-core/reconcile.js";
|
|
26
27
|
import { readSyncCoreRolloutState } from "../sync-core/rollout-state.js";
|
|
27
28
|
import { assertPersonalServiceSessionCapability, readRemoteControlPoolsConfig, remoteControlPoolSecretsEnv, } from "./remote-control-pools.js";
|
|
28
29
|
import { loadLanesConfig } from "../lanes/config.js";
|
|
@@ -34,7 +35,7 @@ import { DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG, DAEMON_LANES_RECONCILE_FILE_
|
|
|
34
35
|
import { createLanesReconcileWatcher } from "./lanes-reconcile-watcher.js";
|
|
35
36
|
import { findLegacyUnits, legacyMeshUnitsToRetire, legacyOwnerLookup, } from "./legacy.js";
|
|
36
37
|
import { readLegacySyncEnv } from "./legacy-sync-env.js";
|
|
37
|
-
import { readVaultSyncSwitches, unreadableSwitchesWarning } from "../vault-sync-switches.js";
|
|
38
|
+
import { readVaultSyncSwitches, unreadableSwitchesWarning, writeVaultSyncSwitches } from "../vault-sync-switches.js";
|
|
38
39
|
import { acquireDaemonLock, isProcessAlive, processCommandLine, releaseDaemonLock } from "./lock.js";
|
|
39
40
|
import { retireLegacyMeshUnits } from "./migrate.js";
|
|
40
41
|
import { daemonDir as meshDaemonDir, daemonPidPath as meshDaemonPidPath } from "../mesh/live/daemon/paths.js";
|
|
@@ -65,24 +66,29 @@ export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
|
65
66
|
export const RESTART_EXIT_CODE = 75;
|
|
66
67
|
const NON_SYNC_SHUTDOWN_CAP_MS = STOP_ESCALATION_MS + 5_000;
|
|
67
68
|
const SYNC_RUNNER_SHUTDOWN_CAP_MS = STOP_ESCALATION_MS + SYNC_RUNNER_PERSIST_GRACE_MS + 5_000;
|
|
68
|
-
function syncCoreVaultPlans(home, hqRoot, platform) {
|
|
69
|
+
export function syncCoreVaultPlans(home, hqRoot, platform) {
|
|
69
70
|
const state = readSyncCoreRolloutState(home);
|
|
70
71
|
if (!state)
|
|
71
72
|
return [];
|
|
72
|
-
return state.vaults.map((vault) =>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
return state.vaults.map((vault) => {
|
|
74
|
+
const plan = inspectEngineHome(home, hqRoot, {
|
|
75
|
+
uid: vault.uid,
|
|
76
|
+
kind: vault.kind,
|
|
77
|
+
...(vault.slug ? { slug: vault.slug } : {}),
|
|
78
|
+
phase: vault.phase,
|
|
79
|
+
engineHome: vault.engineHome,
|
|
80
|
+
binaryPath: state.binaryPath,
|
|
81
|
+
release: state.release,
|
|
82
|
+
...(vault.legacyOffSince ? { legacyOffSince: vault.legacyOffSince } : {}),
|
|
83
|
+
}, platform);
|
|
84
|
+
const releaseHold = vault.phase === "full" ? fullModeHold(vault, state.release) : undefined;
|
|
85
|
+
return releaseHold ? { ...plan, blockedReason: releaseHold } : plan;
|
|
86
|
+
});
|
|
82
87
|
}
|
|
83
88
|
const rememberedSyncCoreVaults = new Map();
|
|
84
89
|
export function planUnitsWithSyncCoreInterlock(services, context, supervisor) {
|
|
85
90
|
const companies = new Set();
|
|
91
|
+
const runningVaults = new Set();
|
|
86
92
|
let personal = false;
|
|
87
93
|
for (const vault of context.syncCoreVaults ?? []) {
|
|
88
94
|
rememberedSyncCoreVaults.set(vault.uid, { kind: vault.kind, slug: vault.slug, full: vault.phase === "full" });
|
|
@@ -90,7 +96,9 @@ export function planUnitsWithSyncCoreInterlock(services, context, supervisor) {
|
|
|
90
96
|
for (const unit of supervisor?.state().units ?? []) {
|
|
91
97
|
if (!unit.id.startsWith("sync-core:") || supervisor?.runningPlanKey(unit.id) === undefined)
|
|
92
98
|
continue;
|
|
93
|
-
const
|
|
99
|
+
const uid = unit.id.slice("sync-core:".length);
|
|
100
|
+
runningVaults.add(uid);
|
|
101
|
+
const vault = rememberedSyncCoreVaults.get(uid);
|
|
94
102
|
if (!vault?.full)
|
|
95
103
|
continue;
|
|
96
104
|
if (vault.kind === "personal")
|
|
@@ -98,13 +106,53 @@ export function planUnitsWithSyncCoreInterlock(services, context, supervisor) {
|
|
|
98
106
|
else if (vault.slug)
|
|
99
107
|
companies.add(vault.slug);
|
|
100
108
|
}
|
|
101
|
-
|
|
109
|
+
let vaultSync = context.vaultSync;
|
|
110
|
+
let fallbackPersistenceFailed = false;
|
|
111
|
+
if (context.vaultSync && !context.vaultSync.allOff) {
|
|
112
|
+
let personalOff = context.vaultSync.personalOff;
|
|
113
|
+
const offCompanies = new Set(context.vaultSync.offCompanies);
|
|
114
|
+
let restorePersonal = false;
|
|
115
|
+
const restoreCompanies = {};
|
|
116
|
+
for (const vault of context.syncCoreVaults ?? []) {
|
|
117
|
+
if (vault.phase !== "full" || runningVaults.has(vault.uid) || !fullModeHold(vault, vault.release))
|
|
118
|
+
continue;
|
|
119
|
+
if (vault.kind === "personal") {
|
|
120
|
+
restorePersonal = personalOff;
|
|
121
|
+
personalOff = false;
|
|
122
|
+
}
|
|
123
|
+
else if (vault.slug) {
|
|
124
|
+
if (offCompanies.has(vault.slug))
|
|
125
|
+
restoreCompanies[vault.slug] = true;
|
|
126
|
+
offCompanies.delete(vault.slug);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (restorePersonal || Object.keys(restoreCompanies).length > 0) {
|
|
130
|
+
try {
|
|
131
|
+
writeVaultSyncSwitches(context.home, {
|
|
132
|
+
...(restorePersonal ? { personal: true } : {}),
|
|
133
|
+
companies: restoreCompanies,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
fallbackPersistenceFailed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
vaultSync = { ...context.vaultSync, personalOff, offCompanies };
|
|
141
|
+
}
|
|
142
|
+
let transitionContext = {
|
|
102
143
|
...context,
|
|
144
|
+
vaultSync,
|
|
103
145
|
syncCoreRunning: { personal, companies },
|
|
104
146
|
legacySyncReadyForCore: false,
|
|
105
147
|
};
|
|
106
|
-
|
|
107
|
-
|
|
148
|
+
let waiting = planUnits(services, transitionContext);
|
|
149
|
+
let legacy = waiting.find((unit) => unit.id === "sync");
|
|
150
|
+
if (fallbackPersistenceFailed && (legacy === undefined || legacy.blocked !== undefined)) {
|
|
151
|
+
vaultSync = context.vaultSync;
|
|
152
|
+
transitionContext = { ...transitionContext, vaultSync };
|
|
153
|
+
waiting = planUnits(services, transitionContext);
|
|
154
|
+
legacy = waiting.find((unit) => unit.id === "sync");
|
|
155
|
+
}
|
|
108
156
|
const legacySyncReadyForCore = legacy !== undefined
|
|
109
157
|
&& (legacy.blocked !== undefined || supervisor?.runningPlanKey("sync") === planKey(legacy, CLI_VERSION));
|
|
110
158
|
return planUnits(services, { ...transitionContext, legacySyncReadyForCore });
|