@indigoai-us/hq-cli 5.345.178 → 5.345.180
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 +39 -0
- package/dist/command-catalog.generated.d.ts +197 -1
- package/dist/command-catalog.generated.js +241 -1
- package/dist/commands/bot-jobs.d.ts +11 -9
- package/dist/commands/bot-jobs.js +56 -21
- package/dist/commands/bot.d.ts +37 -1
- package/dist/commands/bot.js +289 -13
- package/dist/commands/channels.d.ts +18 -1
- package/dist/commands/channels.js +185 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +15 -4
- package/dist/commands/integrations-core.d.ts +2 -0
- package/dist/commands/integrations-core.js +1 -0
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +6 -2
- package/dist/commands/mesh.js +19 -0
- package/dist/commands/sources.d.ts +89 -0
- package/dist/commands/sources.js +363 -1
- 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 +1 -1
- package/dist/lib/bot/fleet-local.js +82 -30
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +109 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +177 -0
- package/dist/lib/bot/local-jobs.d.ts +40 -4
- package/dist/lib/bot/local-jobs.js +72 -5
- package/dist/lib/bot/run.d.ts +15 -0
- package/dist/lib/bot/run.js +99 -1
- 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/handoff.d.ts +4 -0
- package/dist/lib/daemon/handoff.js +6 -2
- package/dist/lib/daemon/install.d.ts +70 -2
- package/dist/lib/daemon/install.js +185 -27
- package/dist/lib/daemon/legacy.d.ts +9 -3
- package/dist/lib/daemon/legacy.js +11 -4
- package/dist/lib/daemon/migrate.js +1 -1
- package/dist/lib/daemon/run.d.ts +5 -2
- package/dist/lib/daemon/run.js +74 -22
- package/dist/lib/doctor/checks/anywhere.d.ts +6 -0
- package/dist/lib/doctor/checks/anywhere.js +35 -6
- 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/__fixtures__/dummy.d.ts +33 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/dummy.js +53 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.d.ts +38 -0
- package/dist/lib/hot-channels/adapters/__fixtures__/meeting-providers.js +64 -0
- package/dist/lib/hot-channels/adapters/attio.d.ts +67 -0
- package/dist/lib/hot-channels/adapters/attio.js +374 -0
- package/dist/lib/hot-channels/adapters/gmail.d.ts +111 -0
- package/dist/lib/hot-channels/adapters/gmail.js +413 -0
- package/dist/lib/hot-channels/adapters/index.d.ts +23 -0
- package/dist/lib/hot-channels/adapters/index.js +39 -0
- package/dist/lib/hot-channels/adapters/linear.d.ts +79 -0
- package/dist/lib/hot-channels/adapters/linear.js +321 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.d.ts +86 -0
- package/dist/lib/hot-channels/adapters/meeting-providers.js +253 -0
- package/dist/lib/hot-channels/adapters/meetings.d.ts +81 -0
- package/dist/lib/hot-channels/adapters/meetings.js +525 -0
- package/dist/lib/hot-channels/adapters/slack.d.ts +70 -0
- package/dist/lib/hot-channels/adapters/slack.js +277 -0
- package/dist/lib/hot-channels/alert.d.ts +78 -0
- package/dist/lib/hot-channels/alert.js +179 -0
- package/dist/lib/hot-channels/audience.d.ts +15 -0
- package/dist/lib/hot-channels/audience.js +25 -0
- package/dist/lib/hot-channels/contract.d.ts +146 -0
- package/dist/lib/hot-channels/contract.js +20 -0
- package/dist/lib/hot-channels/cursors.d.ts +41 -0
- package/dist/lib/hot-channels/cursors.js +84 -0
- package/dist/lib/hot-channels/index.d.ts +10 -0
- package/dist/lib/hot-channels/index.js +10 -0
- package/dist/lib/hot-channels/landing.d.ts +83 -0
- package/dist/lib/hot-channels/landing.js +175 -0
- package/dist/lib/hot-channels/ledger.d.ts +101 -0
- package/dist/lib/hot-channels/ledger.js +203 -0
- package/dist/lib/hot-channels/loop.d.ts +74 -0
- package/dist/lib/hot-channels/loop.js +221 -0
- package/dist/lib/hot-channels/prompts/signals-pass.md +22 -0
- package/dist/lib/hot-channels/registry.d.ts +28 -0
- package/dist/lib/hot-channels/registry.js +121 -0
- package/dist/lib/hot-channels/run.d.ts +137 -0
- package/dist/lib/hot-channels/run.js +324 -0
- package/dist/lib/hot-channels/signals-pass.d.ts +150 -0
- package/dist/lib/hot-channels/signals-pass.js +581 -0
- package/dist/lib/hot-channels/sync-lock.d.ts +51 -0
- package/dist/lib/hot-channels/sync-lock.js +224 -0
- package/dist/lib/install/hook-target.d.ts +6 -2
- package/dist/lib/install/hook-target.js +9 -4
- package/dist/lib/lanes/accounts.d.ts +1 -17
- package/dist/lib/lanes/accounts.js +5 -45
- package/dist/lib/lanes/loop.d.ts +10 -0
- package/dist/lib/lanes/loop.js +18 -1
- 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
|
@@ -237,10 +237,14 @@ function rateIntervalMs(expr) {
|
|
|
237
237
|
function isRunnableSchedule(expr) {
|
|
238
238
|
return rateIntervalMs(expr) !== null || parseCron(expr) !== null;
|
|
239
239
|
}
|
|
240
|
+
/** Where the current schedule starts counting: its last change, else creation. */
|
|
241
|
+
function scheduleOrigin(job) {
|
|
242
|
+
return Date.parse(job.scheduledFrom ?? job.createdAt);
|
|
243
|
+
}
|
|
240
244
|
export function latestDueFire(job, now) {
|
|
241
245
|
const interval = rateIntervalMs(job.cron);
|
|
242
246
|
if (interval) {
|
|
243
|
-
const origin =
|
|
247
|
+
const origin = scheduleOrigin(job);
|
|
244
248
|
const steps = Math.floor((now.getTime() - origin) / interval);
|
|
245
249
|
if (steps < 1)
|
|
246
250
|
return null;
|
|
@@ -252,7 +256,7 @@ export function latestDueFire(job, now) {
|
|
|
252
256
|
return null;
|
|
253
257
|
const start = new Date(now);
|
|
254
258
|
start.setUTCSeconds(0, 0);
|
|
255
|
-
const floor = Math.max(job.lastFiredAt ? Date.parse(job.lastFiredAt) : Date.parse(job.createdAt), start.getTime() - LOOKBACK_MINUTES * 60_000);
|
|
259
|
+
const floor = Math.max(job.lastFiredAt ? Date.parse(job.lastFiredAt) : Date.parse(job.createdAt), job.scheduledFrom ? Date.parse(job.scheduledFrom) : 0, start.getTime() - LOOKBACK_MINUTES * 60_000);
|
|
256
260
|
for (let t = start.getTime(); t > floor; t -= 60_000) {
|
|
257
261
|
const at = new Date(t);
|
|
258
262
|
if (cronMatches(cron, at, job.timezone))
|
|
@@ -264,7 +268,7 @@ export function latestDueFire(job, now) {
|
|
|
264
268
|
export function nextDueFire(job, now) {
|
|
265
269
|
const interval = rateIntervalMs(job.cron);
|
|
266
270
|
if (interval) {
|
|
267
|
-
const origin =
|
|
271
|
+
const origin = scheduleOrigin(job);
|
|
268
272
|
const steps = Math.max(0, Math.floor((now.getTime() - origin) / interval)) + 1;
|
|
269
273
|
return new Date(origin + steps * interval);
|
|
270
274
|
}
|
|
@@ -310,6 +314,53 @@ export function addLocalJob(dir, input, now = new Date()) {
|
|
|
310
314
|
withJobsLock(dir, () => writeLocalJobs(dir, [...readLocalJobs(dir), job]));
|
|
311
315
|
return job;
|
|
312
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Add a job unless one already matches, as one locked read-and-write, so two
|
|
319
|
+
* bot starts racing each other still leave a single job.
|
|
320
|
+
*/
|
|
321
|
+
export function ensureLocalJob(dir, matches, input, now = new Date()) {
|
|
322
|
+
if (!isRunnableSchedule(input.cron))
|
|
323
|
+
throw new Error(`"${input.cron}" is not a schedule this bot can run.`);
|
|
324
|
+
return withJobsLock(dir, () => {
|
|
325
|
+
const jobs = readLocalJobs(dir);
|
|
326
|
+
const existing = jobs.find(matches);
|
|
327
|
+
if (existing)
|
|
328
|
+
return { job: existing, created: false };
|
|
329
|
+
const job = {
|
|
330
|
+
jobId: `job_${randomUUID().replace(/-/g, "").slice(0, 20)}`,
|
|
331
|
+
prompt: input.prompt,
|
|
332
|
+
cron: input.cron,
|
|
333
|
+
timezone: input.timezone,
|
|
334
|
+
...(input.context ? { context: input.context } : {}),
|
|
335
|
+
createdAt: now.toISOString(),
|
|
336
|
+
};
|
|
337
|
+
writeLocalJobs(dir, [...jobs, job]);
|
|
338
|
+
return { job, created: true };
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Change a job's schedule in place, keeping its id, instruction, context and
|
|
343
|
+
* run history. Returns null when the bot has no job with that id.
|
|
344
|
+
*/
|
|
345
|
+
export function rescheduleLocalJob(dir, jobId, schedule, now = new Date()) {
|
|
346
|
+
if (!isRunnableSchedule(schedule.cron))
|
|
347
|
+
throw new Error(`"${schedule.cron}" is not a schedule this bot can run.`);
|
|
348
|
+
return withJobsLock(dir, () => {
|
|
349
|
+
const jobs = readLocalJobs(dir);
|
|
350
|
+
const index = jobs.findIndex((job) => job.jobId === jobId);
|
|
351
|
+
if (index < 0)
|
|
352
|
+
return null;
|
|
353
|
+
const updated = {
|
|
354
|
+
...jobs[index],
|
|
355
|
+
cron: schedule.cron,
|
|
356
|
+
timezone: schedule.timezone,
|
|
357
|
+
scheduledFrom: now.toISOString(),
|
|
358
|
+
};
|
|
359
|
+
jobs[index] = updated;
|
|
360
|
+
writeLocalJobs(dir, jobs);
|
|
361
|
+
return updated;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
313
364
|
/** Remove a job. Returns false when the bot has no job with that id. */
|
|
314
365
|
function removeLocalJob(dir, jobId) {
|
|
315
366
|
return withJobsLock(dir, () => {
|
|
@@ -321,6 +372,16 @@ function removeLocalJob(dir, jobId) {
|
|
|
321
372
|
return true;
|
|
322
373
|
});
|
|
323
374
|
}
|
|
375
|
+
/** Remove every job `matches` picks, as one locked read-and-write. Returns the removed jobs. */
|
|
376
|
+
export function removeLocalJobsWhere(dir, matches) {
|
|
377
|
+
return withJobsLock(dir, () => {
|
|
378
|
+
const jobs = readLocalJobs(dir);
|
|
379
|
+
const removed = jobs.filter(matches);
|
|
380
|
+
if (removed.length > 0)
|
|
381
|
+
writeLocalJobs(dir, jobs.filter((job) => !matches(job)));
|
|
382
|
+
return removed;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
324
385
|
// ─── Firing ──────────────────────────────────────────────────────────────────
|
|
325
386
|
/**
|
|
326
387
|
* Hand over every job that is due now. Each fire is recorded (lastFiredAt and
|
|
@@ -430,8 +491,8 @@ export function withLocalJobs(api, dir, clock = () => new Date()) {
|
|
|
430
491
|
/** The most jobs one bot may hold (the same cap the cloud uses). */
|
|
431
492
|
const MAX_LOCAL_JOBS = 25;
|
|
432
493
|
/**
|
|
433
|
-
* `hq bot jobs` against the bot's own folder: the same
|
|
434
|
-
*
|
|
494
|
+
* `hq bot jobs` against the bot's own folder: the same calls the cloud client
|
|
495
|
+
* offers, plus reschedule, answered from `jobs.json`.
|
|
435
496
|
*/
|
|
436
497
|
export function localJobsClient(dir, clock = () => new Date()) {
|
|
437
498
|
return {
|
|
@@ -454,6 +515,12 @@ export function localJobsClient(dir, clock = () => new Date()) {
|
|
|
454
515
|
throw new BotApiError(404, "local jobs", "JOB_NOT_FOUND", `Job '${jobId}' not found`);
|
|
455
516
|
}
|
|
456
517
|
},
|
|
518
|
+
async rescheduleJob(_agentUid, jobId, schedule) {
|
|
519
|
+
const job = rescheduleLocalJob(dir, jobId, schedule, clock());
|
|
520
|
+
if (!job)
|
|
521
|
+
throw new BotApiError(404, "local jobs", "JOB_NOT_FOUND", `Job '${jobId}' not found`);
|
|
522
|
+
return toJobRecord(job, clock());
|
|
523
|
+
},
|
|
457
524
|
};
|
|
458
525
|
}
|
|
459
526
|
//# sourceMappingURL=local-jobs.js.map
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -134,6 +134,21 @@ export interface BotRunDeps {
|
|
|
134
134
|
inboxPollBackoffEnabled?: () => Promise<boolean>;
|
|
135
135
|
/** Default-off gate for the per-job quiet-success option. */
|
|
136
136
|
jobQuietOptionEnabled?: () => boolean | Promise<boolean>;
|
|
137
|
+
/**
|
|
138
|
+
* The built-in hot-signal-sync job (jobs/hot-signal-sync.ts), run in-process
|
|
139
|
+
* with no model turn. Unset = a fired run of that job fails with a plain
|
|
140
|
+
* report instead of handing the instruction to the model.
|
|
141
|
+
*/
|
|
142
|
+
hotSignalSync?: (input: {
|
|
143
|
+
company: string;
|
|
144
|
+
catchUp: boolean;
|
|
145
|
+
now: () => Date;
|
|
146
|
+
/** The signals-failure alert to the owner, sent even when the job is quiet (US-012). */
|
|
147
|
+
sendAlert: (text: string) => Promise<void>;
|
|
148
|
+
}) => Promise<{
|
|
149
|
+
ok: boolean;
|
|
150
|
+
text: string;
|
|
151
|
+
}>;
|
|
137
152
|
heartbeatIntervalMs?: number;
|
|
138
153
|
/** Override the rendered system prompt (tests). */
|
|
139
154
|
systemPromptOverride?: string;
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
*
|
|
47
47
|
* Everything with a side effect is injectable so the loop is unit-testable.
|
|
48
48
|
*/
|
|
49
|
+
import { botAuthEnv, describeBotAuth, formatBotAuth } from "./runtime-auth.js";
|
|
49
50
|
import { ownerContextBlock } from "./owner-context.js";
|
|
50
51
|
import { randomUUID } from "node:crypto";
|
|
51
52
|
import { hasPromotionHold } from "./promotion-hold.js";
|
|
@@ -82,6 +83,7 @@ import { isAgentSender, isDmItem, isMetaNonReply, resolveReplyTarget } from "./r
|
|
|
82
83
|
import { resolveRoomMentionsForBot, roomPostHasMentionTokens, } from "./room-mentions.js";
|
|
83
84
|
import { buildJobTurnPrompt, isCatchUpRun, isJobItem, jobReportHeader, jobRunOutput, machineTimeZone, planJobRuns, stripJobHeader, SUPERSEDED_DISPOSITION } from "./job-run.js";
|
|
84
85
|
import { botJobQuietOptionEnabled } from "./job-quiet-flag.js";
|
|
86
|
+
import { currentHotSignalSyncCompany, hotSignalSyncJobCompany, removeStaleHotSignalSyncJobs } from "./jobs/hot-signal-sync.js";
|
|
85
87
|
import { clearPendingJobCompletion, isCompletionDue, isCompletionExpired, isCompletionForOtherAgent, readPendingJobCompletions, recordPendingJobCompletion } from "./job-completions.js";
|
|
86
88
|
import { SIGN_IN_CHECK_FRESH_MS, SIGN_IN_CHECK_MS, SIGN_IN_RETRY_MS, signInNeededText, signedOutExcerpt, } from "./runtime-sign-in.js";
|
|
87
89
|
import { checkRuntimeSignIn } from "./runtime-sign-in-check.js";
|
|
@@ -557,6 +559,12 @@ export async function runBot(deps) {
|
|
|
557
559
|
setStatus({ ...fallback(), pid, state: "starting", canDrainForPromotion: false, runtimeSignIn: null, cliVersion });
|
|
558
560
|
log("info", `starting ${config.name} on hq-cli ${cliVersion} (${config.runtime}, ${kind} bot${companies.length ? ` of ${companies.join(", ")}` : ""}) as ${config.agentUid} for ${config.ownerUid}`);
|
|
559
561
|
log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid, companySlug), turnCwd)}`);
|
|
562
|
+
{
|
|
563
|
+
const login = describeBotAuth(dir, config);
|
|
564
|
+
log("info", `${runtime.id} login: ${formatBotAuth(login, config.name)}`);
|
|
565
|
+
if (login.login === "own" && !login.present)
|
|
566
|
+
setStatus({ lastError: `own ${runtime.id} login missing: run hq bot login ${config.name} --runtime ${runtime.id}` });
|
|
567
|
+
}
|
|
560
568
|
deps.cliUpdate?.startup(log);
|
|
561
569
|
try {
|
|
562
570
|
const record = await api.getAgent(config.agentUid);
|
|
@@ -1047,7 +1055,15 @@ export async function runBot(deps) {
|
|
|
1047
1055
|
// resets). Claude gets the full hook set via its payload session id;
|
|
1048
1056
|
// Codex/Grok only see HQ_SESSION_ID.
|
|
1049
1057
|
const sid = session?.sessionId ?? randomUUID();
|
|
1050
|
-
|
|
1058
|
+
let env;
|
|
1059
|
+
try {
|
|
1060
|
+
// The bot's own login or account, when set. A missing one fails the
|
|
1061
|
+
// turn loudly (status + reply); it never falls back to the owner's login.
|
|
1062
|
+
env = withOsUser(botAuthEnv(dir, { ...live, runtime: config.runtime }, turnEnv(process.env, kind, config.agentUid, companySlug)));
|
|
1063
|
+
}
|
|
1064
|
+
catch (err) {
|
|
1065
|
+
throw new CompanyBindingError(`I can't answer yet: ${err instanceof Error ? err.message : String(err)} I did not use anyone else's login; this work is still pending.`);
|
|
1066
|
+
}
|
|
1051
1067
|
let bind = { env };
|
|
1052
1068
|
if (companySlug) {
|
|
1053
1069
|
try {
|
|
@@ -1932,6 +1948,88 @@ export async function runBot(deps) {
|
|
|
1932
1948
|
const markReported = (status) => {
|
|
1933
1949
|
patchInflight(dir, id, { jobCompleted: status });
|
|
1934
1950
|
};
|
|
1951
|
+
// The built-in hot-signal-sync job runs here, in the bot, with no model
|
|
1952
|
+
// turn. It is marked processed as soon as the sync finishes, so a report
|
|
1953
|
+
// that fails to post (or a redelivered item) never syncs a second time.
|
|
1954
|
+
const syncCompany = hotSignalSyncJobCompany(job.context);
|
|
1955
|
+
if (syncCompany) {
|
|
1956
|
+
// The job's stored company is checked against the bot's current config
|
|
1957
|
+
// (read fresh from disk) every time it fires. A company the bot no longer
|
|
1958
|
+
// serves is never synced: the run is skipped and the job removed.
|
|
1959
|
+
const freshConfig = readBotConfig(dir);
|
|
1960
|
+
if (!freshConfig) {
|
|
1961
|
+
// bot.json is missing or unreadable, so which company the bot serves is
|
|
1962
|
+
// unknown. Skip this run without writing anything; the job stays for the
|
|
1963
|
+
// next fire.
|
|
1964
|
+
const text = `Skipped the hot-channel sync for ${syncCompany}: the bot's config (bot.json) could not be read, so nothing was written. The job will try again at its next run.`;
|
|
1965
|
+
markProcessed(dir, id, now().toISOString());
|
|
1966
|
+
try {
|
|
1967
|
+
await sendToPerson(text);
|
|
1968
|
+
}
|
|
1969
|
+
catch (err) {
|
|
1970
|
+
log("warn", `report for run ${id} of job ${jobId} was not delivered: ${err instanceof Error ? err.message : String(err)}`);
|
|
1971
|
+
}
|
|
1972
|
+
await completeJobRun(jobId, id, { status: "skipped-precondition", output: jobRunOutput(text) });
|
|
1973
|
+
markReported("skipped-precondition");
|
|
1974
|
+
await api.ackInbox(config.agentUid, id);
|
|
1975
|
+
clearInflight(dir, id);
|
|
1976
|
+
attempts.delete(id);
|
|
1977
|
+
setStatus({ lastActivityAt: now().toISOString() });
|
|
1978
|
+
log("warn", `hot-signal-sync job ${jobId} run ${id} skipped: bot.json missing or unreadable`);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const currentCompany = currentHotSignalSyncCompany(freshConfig);
|
|
1982
|
+
if (currentCompany !== syncCompany) {
|
|
1983
|
+
let removedNote = "";
|
|
1984
|
+
try {
|
|
1985
|
+
removeStaleHotSignalSyncJobs(dir, currentCompany);
|
|
1986
|
+
removedNote = ` Removed job ${jobId}.`;
|
|
1987
|
+
}
|
|
1988
|
+
catch (err) {
|
|
1989
|
+
removedNote = ` Could not remove job ${jobId}: ${err instanceof Error ? err.message : String(err)}`;
|
|
1990
|
+
}
|
|
1991
|
+
const text = `Skipped the hot-channel sync for ${syncCompany}: this bot no longer syncs that company (current: ${currentCompany ?? "none"}). Nothing was written.${removedNote}`;
|
|
1992
|
+
markProcessed(dir, id, now().toISOString());
|
|
1993
|
+
try {
|
|
1994
|
+
await sendToPerson(text);
|
|
1995
|
+
}
|
|
1996
|
+
catch (err) {
|
|
1997
|
+
log("warn", `report for run ${id} of job ${jobId} was not delivered: ${err instanceof Error ? err.message : String(err)}`);
|
|
1998
|
+
}
|
|
1999
|
+
await completeJobRun(jobId, id, { status: "skipped-precondition", output: jobRunOutput(text) });
|
|
2000
|
+
markReported("skipped-precondition");
|
|
2001
|
+
await api.ackInbox(config.agentUid, id);
|
|
2002
|
+
clearInflight(dir, id);
|
|
2003
|
+
attempts.delete(id);
|
|
2004
|
+
setStatus({ lastActivityAt: now().toISOString() });
|
|
2005
|
+
log("info", `hot-signal-sync job ${jobId} run ${id} skipped: company ${syncCompany} no longer served (current: ${currentCompany ?? "none"})`);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
const result = deps.hotSignalSync
|
|
2009
|
+
? await deps.hotSignalSync({ company: syncCompany, catchUp, now, sendAlert: (text) => sendToPerson(text) })
|
|
2010
|
+
: { ok: false, text: `This bot cannot run the hot-channel sync for ${syncCompany}.` };
|
|
2011
|
+
markProcessed(dir, id, now().toISOString());
|
|
2012
|
+
const status = result.ok ? "succeeded" : "failed";
|
|
2013
|
+
if (!(quietJob && result.ok)) {
|
|
2014
|
+
try {
|
|
2015
|
+
await sendToPerson(result.text);
|
|
2016
|
+
}
|
|
2017
|
+
catch (err) {
|
|
2018
|
+
log("warn", `report for run ${id} of job ${jobId} was not delivered: ${err instanceof Error ? err.message : String(err)}`);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
await completeJobRun(jobId, id, { status, output: jobRunOutput(result.text) });
|
|
2022
|
+
markReported(status);
|
|
2023
|
+
await api.ackInbox(config.agentUid, id);
|
|
2024
|
+
clearInflight(dir, id);
|
|
2025
|
+
attempts.delete(id);
|
|
2026
|
+
setStatus({
|
|
2027
|
+
...(!(quietJob && result.ok) ? { repliesSent: (readStatus()?.repliesSent ?? 0) + 1 } : {}),
|
|
2028
|
+
lastActivityAt: now().toISOString(),
|
|
2029
|
+
});
|
|
2030
|
+
log("info", `hot-signal-sync job ${jobId} run ${id} ${status} for ${syncCompany}${catchUp ? " (catch-up)" : ""}`);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
1935
2033
|
const { outcome, poster } = await runTurnWithProgress({
|
|
1936
2034
|
marker: { messageId: id, target: markerTarget, jobId, jobHeader: header, agentUid: config.agentUid },
|
|
1937
2035
|
prompt: buildJobTurnPrompt(job, { now: now(), timeZone }),
|
|
@@ -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 {};
|