@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/command-catalog.generated.d.ts +126 -0
  3. package/dist/command-catalog.generated.js +155 -0
  4. package/dist/commands/bot.d.ts +29 -1
  5. package/dist/commands/bot.js +282 -22
  6. package/dist/commands/channels.d.ts +18 -1
  7. package/dist/commands/channels.js +185 -1
  8. package/dist/commands/lanes.d.ts +1 -1
  9. package/dist/commands/lanes.js +6 -2
  10. package/dist/commands/mesh.js +19 -0
  11. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
  12. package/dist/hq-cloud-sync-runner/package.json +1 -1
  13. package/dist/lib/bot/config.d.ts +8 -0
  14. package/dist/lib/bot/fleet-local.d.ts +7 -3
  15. package/dist/lib/bot/fleet-local.js +234 -64
  16. package/dist/lib/bot/run.js +39 -2
  17. package/dist/lib/bot/runtime-auth.d.ts +119 -0
  18. package/dist/lib/bot/runtime-auth.js +251 -0
  19. package/dist/lib/daemon/catalog.d.ts +1 -0
  20. package/dist/lib/daemon/catalog.js +1 -1
  21. package/dist/lib/daemon/run.d.ts +2 -0
  22. package/dist/lib/daemon/run.js +64 -16
  23. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  24. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  25. package/dist/lib/hot-channels/adapters/meetings.js +3 -1
  26. package/dist/lib/hot-channels/contract.d.ts +6 -0
  27. package/dist/lib/hot-channels/ledger.js +17 -0
  28. package/dist/lib/hot-channels/loop.js +2 -0
  29. package/dist/lib/lanes/accounts.d.ts +1 -17
  30. package/dist/lib/lanes/accounts.js +5 -45
  31. package/dist/lib/lanes/provider-home.d.ts +24 -0
  32. package/dist/lib/lanes/provider-home.js +52 -0
  33. package/dist/lib/lanes/spawn.d.ts +12 -0
  34. package/dist/lib/lanes/spawn.js +27 -0
  35. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  36. package/dist/lib/mesh/live/daemon/doctor.js +13 -0
  37. package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
  38. package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
  39. package/dist/lib/mesh/live/daemon/run.js +31 -0
  40. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
  41. package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
  42. package/dist/lib/mesh/live/paths.d.ts +2 -0
  43. package/dist/lib/mesh/live/paths.js +2 -0
  44. package/dist/lib/sync-core/flag.d.ts +1 -1
  45. package/dist/lib/sync-core/flag.js +1 -1
  46. package/dist/lib/sync-core/reconcile.d.ts +1 -1
  47. package/dist/lib/sync-core/reconcile.js +9 -7
  48. package/dist/lib/sync-core/release.d.ts +3 -3
  49. package/dist/lib/sync-core/release.js +4 -4
  50. package/dist/lib/sync-core/rollout-state.d.ts +1 -0
  51. package/dist/lib/sync-core/rollout-state.js +1 -0
  52. 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) {
@@ -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(): {
@@ -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) => inspectEngineHome(home, hqRoot, {
73
- uid: vault.uid,
74
- kind: vault.kind,
75
- ...(vault.slug ? { slug: vault.slug } : {}),
76
- phase: vault.phase,
77
- engineHome: vault.engineHome,
78
- binaryPath: state.binaryPath,
79
- release: state.release,
80
- ...(vault.legacyOffSince ? { legacyOffSince: vault.legacyOffSince } : {}),
81
- }, platform));
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 vault = rememberedSyncCoreVaults.get(unit.id.slice("sync-core:".length));
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
- const transitionContext = {
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
- const waiting = planUnits(services, transitionContext);
107
- const legacy = waiting.find((unit) => unit.id === "sync");
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 });