@indigoai-us/hq-cli 5.345.169 → 5.345.171
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 +69 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +181 -9
- package/dist/command-catalog.generated.js +224 -10
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +119 -5
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +141 -14
- package/dist/commands/cloud.d.ts +78 -1
- package/dist/commands/cloud.js +274 -9
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/feedback-triage.d.ts +43 -1
- package/dist/commands/feedback-triage.js +123 -23
- package/dist/commands/feedback.d.ts +40 -1
- package/dist/commands/feedback.js +184 -4
- package/dist/commands/files.d.ts +42 -0
- package/dist/commands/files.js +223 -30
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/mesh.js +6 -0
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-manifest.d.ts +16 -0
- package/dist/commands/sync-manifest.js +17 -1
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/decisions.d.ts +62 -0
- package/dist/lib/bot/decisions.js +228 -0
- package/dist/lib/bot/fleet-local.d.ts +127 -0
- package/dist/lib/bot/fleet-local.js +638 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/run.d.ts +7 -0
- package/dist/lib/bot/run.js +104 -1
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +63 -29
- package/dist/lib/daemon/config.d.ts +7 -0
- package/dist/lib/daemon/config.js +32 -0
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +23 -3
- package/dist/lib/daemon/supervisor.d.ts +1 -0
- package/dist/lib/daemon/supervisor.js +4 -0
- package/dist/lib/daemon/sync-core-canary.d.ts +12 -0
- package/dist/lib/daemon/sync-core-canary.js +61 -0
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/checks/sync-health.d.ts +32 -0
- package/dist/lib/doctor/checks/sync-health.js +54 -1
- package/dist/lib/doctor/fix/apply.d.ts +26 -4
- package/dist/lib/doctor/fix/apply.js +149 -11
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- 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/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/mesh/live/daemon/work-subscribe.js +31 -0
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/work-context/organize.d.ts +7 -0
- package/dist/lib/work-context/organize.js +21 -1
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* checks the result before it leaves this machine: a bot's machine credentials
|
|
10
10
|
* live only in ~/.hq/bots/<name>/machine-creds.json and never travel here.
|
|
11
11
|
*/
|
|
12
|
-
import { botMemoryMode, effectiveBotCompanies, effectiveBotKind, isBotKind, isBotMemoryMode, isBotRuntimeId, } from "./config.js";
|
|
12
|
+
import { botMemoryMode, effectiveBotCompanies, effectiveBotKind, isBotKind, isBotEngineMetadata, isBotMemoryMode, isBotRuntimeId, } from "./config.js";
|
|
13
13
|
/** Same limit the server enforces on the stored value. */
|
|
14
14
|
const BOT_LOCAL_CONFIG_MAX_BYTES = 8 * 1024;
|
|
15
15
|
/** Key names the server rejects outright (case-insensitive, substring). */
|
|
@@ -41,7 +41,8 @@ export function buildBotLocalConfig(config) {
|
|
|
41
41
|
const companies = effectiveBotCompanies(config);
|
|
42
42
|
const local = {
|
|
43
43
|
v: 1,
|
|
44
|
-
runtime: config.runtime,
|
|
44
|
+
...(config.engine ? {} : { runtime: config.runtime }),
|
|
45
|
+
...(config.engine ? { engine: config.engine } : {}),
|
|
45
46
|
workerSource: config.workerSource ?? "scaffold",
|
|
46
47
|
...(config.workerId ? { workerId: config.workerId } : {}),
|
|
47
48
|
...(config.companySlug ? { companySlug: config.companySlug } : {}),
|
|
@@ -80,6 +81,8 @@ export function parseBotLocalConfig(value) {
|
|
|
80
81
|
const runtime = str("runtime");
|
|
81
82
|
if (isBotRuntimeId(runtime))
|
|
82
83
|
out.runtime = runtime;
|
|
84
|
+
if (isBotEngineMetadata(raw.engine))
|
|
85
|
+
out.engine = raw.engine;
|
|
83
86
|
const workerSource = str("workerSource");
|
|
84
87
|
if (workerSource === "worker" || workerSource === "scaffold")
|
|
85
88
|
out.workerSource = workerSource;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { BotApi, BotPromotionState } from "./api.js";
|
|
2
2
|
import type { BotConfig } from "./config.js";
|
|
3
|
+
import type { BotMachineCreds } from "./creds.js";
|
|
4
|
+
/** A bot whose process and HQ machine credential live outside its state folder,
|
|
5
|
+
* such as an HQ Fleet Runtime bot: promotion stops its service through the
|
|
6
|
+
* runtime and reads the credential from the runtime's secure store.
|
|
7
|
+
*/
|
|
8
|
+
export interface LocalBotPromotionRuntime {
|
|
9
|
+
stop(): void;
|
|
10
|
+
machineCredential(): BotMachineCreds;
|
|
11
|
+
}
|
|
3
12
|
/** One recoverable local-to-cloud step, used by CLI and desktop. Never kills the
|
|
4
13
|
* local runtime, recopies provider login files, or changes another bot.
|
|
5
14
|
*/
|
|
@@ -9,6 +18,7 @@ export declare function promoteLocalBot(input: {
|
|
|
9
18
|
companyUid: string;
|
|
10
19
|
api: BotApi;
|
|
11
20
|
drainTimeoutMs?: number;
|
|
21
|
+
runtime?: LocalBotPromotionRuntime;
|
|
12
22
|
}): Promise<{
|
|
13
23
|
promotion: BotPromotionState;
|
|
14
24
|
pending: boolean;
|
package/dist/lib/bot/promote.js
CHANGED
|
@@ -41,7 +41,7 @@ function readPrivateJson(file, limit) {
|
|
|
41
41
|
* local runtime, recopies provider login files, or changes another bot.
|
|
42
42
|
*/
|
|
43
43
|
export async function promoteLocalBot(input) {
|
|
44
|
-
const { dir, config, companyUid, api } = input;
|
|
44
|
+
const { dir, config, companyUid, api, runtime } = input;
|
|
45
45
|
const existing = await api.promotionStatus(config.agentUid, companyUid);
|
|
46
46
|
const holdFile = path.join(dir, "promotion-hold.json");
|
|
47
47
|
const localHold = fs.existsSync(holdFile) ? readPrivateJson(holdFile, 2048) : null;
|
|
@@ -58,6 +58,7 @@ export async function promoteLocalBot(input) {
|
|
|
58
58
|
recordActivePromotion(dir, hold, existing);
|
|
59
59
|
return { promotion: existing, pending: false };
|
|
60
60
|
}
|
|
61
|
+
runtime?.stop();
|
|
61
62
|
await waitForPromotionDrain(dir, input.drainTimeoutMs ?? 60_000);
|
|
62
63
|
const file = path.join(dir, `promotion-snapshot-${operationId}.json`);
|
|
63
64
|
if (!fs.existsSync(file)) {
|
|
@@ -83,7 +84,7 @@ export async function promoteLocalBot(input) {
|
|
|
83
84
|
throw new Error("Frozen snapshot does not match the cloud operation");
|
|
84
85
|
let state = existing;
|
|
85
86
|
if (!state || state.phase === "preparing") {
|
|
86
|
-
const machine = readPrivateJson(botCredsPath(dir), 8192);
|
|
87
|
+
const machine = runtime ? runtime.machineCredential() : readPrivateJson(botCredsPath(dir), 8192);
|
|
87
88
|
if (!machine || typeof machine !== "object" || typeof machine.secret !== "string")
|
|
88
89
|
throw new Error("Invalid bot machine credential");
|
|
89
90
|
try {
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ import { type BotLogger } from "./log.js";
|
|
|
54
54
|
import { type CodingToolActivity, type DetectCodingToolActivityInput } from "./coding-tool-activity.js";
|
|
55
55
|
import { type BotRuntime, type RuntimeTurnInput, type RuntimeTurnResult } from "./runtime/index.js";
|
|
56
56
|
import { type BotSessionScope } from "./paths.js";
|
|
57
|
+
import { type DecisionDmClient } from "./decisions.js";
|
|
57
58
|
import type { VaultObjectDownloader } from "../agent-kit/attachment-download.js";
|
|
58
59
|
import { type HeartbeatAgentUidSource, type HeartbeatCredsSource, type HeartbeatIdentityMismatchReporter } from "../heartbeat-identity.js";
|
|
59
60
|
import { type BotState, type BotStatusFile } from "./status.js";
|
|
@@ -188,6 +189,12 @@ export interface BotRunDeps {
|
|
|
188
189
|
* never read the real home folder.
|
|
189
190
|
*/
|
|
190
191
|
detectCodingToolActivity?: (input: DetectCodingToolActivityInput) => CodingToolActivity;
|
|
192
|
+
/**
|
|
193
|
+
* US-005: the decisions client (`hq decide`), called with the bot's own
|
|
194
|
+
* machine token. Owner DMs that answer a decision talking in this owner-bot
|
|
195
|
+
* thread are recorded through it. Unset leaves every owner DM on the model path.
|
|
196
|
+
*/
|
|
197
|
+
decisions?: DecisionDmClient;
|
|
191
198
|
}
|
|
192
199
|
export declare const KICKOFF_MESSAGE_ID = "kickoff";
|
|
193
200
|
/**
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -74,6 +74,7 @@ import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
|
|
|
74
74
|
import { botAttachmentsDir, dmSessionScope, isDmSessionScope } from "./paths.js";
|
|
75
75
|
import { briefJobRunId, briefJobSessionScope, isBriefJobSessionScope } from "./brief-job.js";
|
|
76
76
|
import { appendDmHistory, dmHistoryHasModelThread, markDmHistoryModelTurn, readDmHistory, withRecentDmHistory } from "./dm-history.js";
|
|
77
|
+
import { handleDecisionDm, looksLikePick, DECISIONS_NOT_LIVE_REPLY, readDecisionTalkState, writeDecisionTalkState } from "./decisions.js";
|
|
77
78
|
import { attachmentTurnBlock, downloadTurnAttachments } from "./attachments.js";
|
|
78
79
|
import { inboxEntryAttachments } from "../agent-kit/inbox-state.js";
|
|
79
80
|
import { isAgentSender, isDmItem, isMetaNonReply, resolveReplyTarget } from "./room-policy.js";
|
|
@@ -1942,6 +1943,104 @@ export async function runBot(deps) {
|
|
|
1942
1943
|
log("info", `answered ${id} with the setup explanation (no model turn)`);
|
|
1943
1944
|
return true;
|
|
1944
1945
|
};
|
|
1946
|
+
/**
|
|
1947
|
+
* US-005: an owner DM about a decision talking in this owner-bot thread.
|
|
1948
|
+
* Returns `done` when the message was answered here with no model turn
|
|
1949
|
+
* (a recorded pick, an ambiguity question, or a failure line), or a note for
|
|
1950
|
+
* the model turn. A send failure leaves the message unacked for the next poll.
|
|
1951
|
+
*/
|
|
1952
|
+
let decisionsNotLiveUntil = 0;
|
|
1953
|
+
const DECISIONS_NOT_LIVE_RECHECK_MS = 10 * 60 * 1000;
|
|
1954
|
+
const runDecisionDm = async (item, text) => {
|
|
1955
|
+
const decisionClient = deps.decisions;
|
|
1956
|
+
if (!decisionClient || item.fromPersonUid !== config.ownerUid)
|
|
1957
|
+
return { done: false };
|
|
1958
|
+
let result;
|
|
1959
|
+
const pendingReply = readDecisionTalkState(dir).pending?.[item.messageId];
|
|
1960
|
+
if (pendingReply !== undefined) {
|
|
1961
|
+
// Decided on an earlier poll (the pick may already be saved): only resend.
|
|
1962
|
+
result = { kind: "reply", text: pendingReply, recorded: false };
|
|
1963
|
+
}
|
|
1964
|
+
else if (now().getTime() < decisionsNotLiveUntil) {
|
|
1965
|
+
result = looksLikePick(text) ? { kind: "reply", text: DECISIONS_NOT_LIVE_REPLY, recorded: false, notLive: true } : { kind: "none" };
|
|
1966
|
+
}
|
|
1967
|
+
else {
|
|
1968
|
+
let companyUids = null;
|
|
1969
|
+
if (deps.ownerContext) {
|
|
1970
|
+
try {
|
|
1971
|
+
const ctx = await deps.ownerContext();
|
|
1972
|
+
if (ctx.status === "ok")
|
|
1973
|
+
companyUids = ctx.companies.map((company) => company.companyUid);
|
|
1974
|
+
}
|
|
1975
|
+
catch (err) {
|
|
1976
|
+
log("warn", `decision company check failed (${err instanceof Error ? err.message : String(err)}); no pick is recorded`);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
const talk = readDecisionTalkState(dir);
|
|
1980
|
+
result = await handleDecisionDm({
|
|
1981
|
+
text,
|
|
1982
|
+
speakerUid: item.fromPersonUid,
|
|
1983
|
+
...(item.rootEventId ? { rootEventId: item.rootEventId } : {}),
|
|
1984
|
+
ownerUid: config.ownerUid,
|
|
1985
|
+
agentUid: config.agentUid,
|
|
1986
|
+
companyUids,
|
|
1987
|
+
nowMs: now().getTime(),
|
|
1988
|
+
restated: new Set(Object.keys(talk.restated)),
|
|
1989
|
+
}, decisionClient);
|
|
1990
|
+
if (result.kind !== "model" && result.notLive) {
|
|
1991
|
+
decisionsNotLiveUntil = now().getTime() + DECISIONS_NOT_LIVE_RECHECK_MS;
|
|
1992
|
+
log("warn", "decisions API returned 404 (not deployed here); decision picks are off for 10 minutes");
|
|
1993
|
+
}
|
|
1994
|
+
if (result.kind === "model" && result.restate) {
|
|
1995
|
+
try {
|
|
1996
|
+
await sendToTarget({ kind: "dm", peerUid: config.ownerUid }, result.restate, item.rootEventId, "human");
|
|
1997
|
+
const latest = readDecisionTalkState(dir);
|
|
1998
|
+
writeDecisionTalkState(dir, { ...latest, restated: { ...latest.restated, [result.decisionId]: now().toISOString() } });
|
|
1999
|
+
}
|
|
2000
|
+
catch (err) {
|
|
2001
|
+
log("warn", `decision restate for ${result.decisionId} not sent (${err instanceof Error ? err.message : String(err)})`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (result.kind === "none")
|
|
2006
|
+
return { done: false };
|
|
2007
|
+
if (result.kind === "model")
|
|
2008
|
+
return { done: false, note: result.note };
|
|
2009
|
+
const id = item.messageId;
|
|
2010
|
+
const setPending = (text) => {
|
|
2011
|
+
const state = readDecisionTalkState(dir);
|
|
2012
|
+
const pending = { ...(state.pending ?? {}) };
|
|
2013
|
+
if (text === null)
|
|
2014
|
+
delete pending[id];
|
|
2015
|
+
else
|
|
2016
|
+
pending[id] = text;
|
|
2017
|
+
writeDecisionTalkState(dir, { v: 1, restated: state.restated, ...(Object.keys(pending).length ? { pending } : {}) });
|
|
2018
|
+
};
|
|
2019
|
+
try {
|
|
2020
|
+
setPending(result.text);
|
|
2021
|
+
}
|
|
2022
|
+
catch (err) {
|
|
2023
|
+
log("warn", `decision reply for ${id} not saved locally (${err instanceof Error ? err.message : String(err)})`);
|
|
2024
|
+
}
|
|
2025
|
+
try {
|
|
2026
|
+
await sendToTarget({ kind: "dm", peerUid: config.ownerUid }, result.text, item.rootEventId, "human");
|
|
2027
|
+
}
|
|
2028
|
+
catch (err) {
|
|
2029
|
+
log("warn", `decision reply for ${id} could not be sent (${err instanceof Error ? err.message : String(err)}); it is retried on the next poll`);
|
|
2030
|
+
return { done: true };
|
|
2031
|
+
}
|
|
2032
|
+
markProcessed(dir, id, now().toISOString());
|
|
2033
|
+
try {
|
|
2034
|
+
setPending(null);
|
|
2035
|
+
}
|
|
2036
|
+
catch {
|
|
2037
|
+
// A stale entry is harmless: the message is processed and never re-read.
|
|
2038
|
+
}
|
|
2039
|
+
await api.ackInbox(config.agentUid, id);
|
|
2040
|
+
setStatus({ repliesSent: (readStatus()?.repliesSent ?? 0) + 1, lastActivityAt: now().toISOString() });
|
|
2041
|
+
log("info", `answered ${id} for decision ${result.decisionId ?? "(none)"}${result.recorded ? " (pick recorded)" : ""}, no model turn`);
|
|
2042
|
+
return { done: true };
|
|
2043
|
+
};
|
|
1945
2044
|
const handleItem = async (item) => {
|
|
1946
2045
|
const id = item.messageId;
|
|
1947
2046
|
const inboxState = readInboxState(dir);
|
|
@@ -2005,6 +2104,9 @@ export async function runBot(deps) {
|
|
|
2005
2104
|
const ownerDm = target.kind === "dm" && target.peerUid === config.ownerUid;
|
|
2006
2105
|
if (isSetupBot && ownerDm && (await sendSetupExplanation(item, messageText, attachmentRefs.length)))
|
|
2007
2106
|
return;
|
|
2107
|
+
const decisionTurn = ownerDm && messageText ? await runDecisionDm(item, messageText) : { done: false };
|
|
2108
|
+
if (decisionTurn.done)
|
|
2109
|
+
return;
|
|
2008
2110
|
const attempt = (attempts.get(id) ?? 0) + 1;
|
|
2009
2111
|
attempts.set(id, attempt);
|
|
2010
2112
|
const reactionId = item.eventId ?? id;
|
|
@@ -2100,7 +2202,8 @@ export async function runBot(deps) {
|
|
|
2100
2202
|
// answering every note re-posted that question (three times in a row on
|
|
2101
2203
|
// 2026-10-12). Such a turn may stay quiet.
|
|
2102
2204
|
const appSetupNote = isSetupBot && ownerDm && isAppSetupNote(messageText);
|
|
2103
|
-
const
|
|
2205
|
+
const ownerTurnText = decisionTurn.note ? `${decisionTurn.note}\n\n${ownerText}` : ownerText;
|
|
2206
|
+
const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? (appSetupNote ? `${APP_SETUP_NOTE_TURN_NOTE}\n\n${ownerTurnText}` : ownerTurnText) :
|
|
2104
2207
|
`[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
|
|
2105
2208
|
const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
|
|
2106
2209
|
const { sendProgress, sendToPerson, saved } = trackingSend(markerTarget, item.rootEventId);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
-
import * as os from "node:os";
|
|
5
4
|
import * as path from "node:path";
|
|
6
5
|
import { planBotRuntimeSpawn } from "./windows.js";
|
|
7
6
|
import { GrokAcpClient } from "./grok-acp.js";
|
|
@@ -9,6 +8,7 @@ import { createClaudeRuntime } from "./claude.js";
|
|
|
9
8
|
import { createCodexRuntime } from "./codex.js";
|
|
10
9
|
import { createGrokRuntime, grokRetryArgsAfterInvalidOutputFormat, probeGrokOutputFormat } from "./grok.js";
|
|
11
10
|
import { stripControlTokens } from "./sanitize.js";
|
|
11
|
+
import { createTurnScratchDir } from "./scratch-dir.js";
|
|
12
12
|
import { isRuntimeVersionTooOld, versionTooOldModel } from "../runtime-failure.js";
|
|
13
13
|
import { parseJsonLine, RuntimeError, turnTimeoutMs, stderrExcerpt, MAX_MODEL_FALLBACK_RETRIES, } from "./types.js";
|
|
14
14
|
export * from "./types.js";
|
|
@@ -345,10 +345,13 @@ export function forgetModelFallbacks() {
|
|
|
345
345
|
*/
|
|
346
346
|
export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
347
347
|
const spawnImpl = deps.spawnImpl ?? defaultSpawn;
|
|
348
|
-
const
|
|
348
|
+
const turnEnv = input.env ?? deps.env ?? process.env;
|
|
349
|
+
const turnPlatform = deps.platform ?? input.platform ?? process.platform;
|
|
350
|
+
const scratch = deps.scratchDir ? { dir: deps.scratchDir } : createTurnScratchDir("hq-bot-turn-", { platform: turnPlatform });
|
|
351
|
+
const scratchDir = scratch.dir;
|
|
349
352
|
const outputFile = path.join(scratchDir, "last-message.txt");
|
|
350
353
|
const newSessionId = input.newSessionId ?? (deps.newSessionId ?? randomUUID)();
|
|
351
|
-
const turnInput = { ...input, platform:
|
|
354
|
+
const turnInput = { ...input, platform: turnPlatform };
|
|
352
355
|
const nowMs = deps.now ?? Date.now;
|
|
353
356
|
const memoKey = fallbackKey(runtime, input.model);
|
|
354
357
|
const memo = rememberedModelFallbacks.get(memoKey);
|
|
@@ -358,7 +361,8 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
|
358
361
|
const args = runtime.buildArgs(remembered ? { ...turnInput, model: remembered.to } : turnInput, { outputFile, newSessionId });
|
|
359
362
|
// Enforce at the model boundary too: old plists and direct `bot run` callers
|
|
360
363
|
// may lack the guard. A bot prompt must never quit the user's desktop app.
|
|
361
|
-
|
|
364
|
+
// When TMPDIR was unwritable, hand the runtime CLI the working temp root too.
|
|
365
|
+
const baseEnv = { ...turnEnv, ...(scratch.fallbackRoot ? { TMPDIR: scratch.fallbackRoot } : {}), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
362
366
|
const env = { ...(runtime.runtimeEnv?.(baseEnv) ?? baseEnv), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
363
367
|
const binary = runtime.resolveBinary?.(env, input.cwd, turnInput.platform) ?? runtime.binary;
|
|
364
368
|
const stdin = runtime.stdinInput?.(turnInput);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn scratch folder for the local bot runtime (feedback_f0131909).
|
|
3
|
+
*
|
|
4
|
+
* A bot started by launchd/systemd at login can inherit a TMPDIR it cannot
|
|
5
|
+
* write yet (an external volume that is not mounted or not yet readable by the
|
|
6
|
+
* agent), so `mkdtemp` failed with EPERM on every turn until `hq bot restart`
|
|
7
|
+
* re-launched it with a working TMPDIR. Try the configured folder first, then
|
|
8
|
+
* /tmp, then ~/.hq/tmp. When TMPDIR works nothing changes.
|
|
9
|
+
*/
|
|
10
|
+
export interface TurnScratchDir {
|
|
11
|
+
/** The created scratch folder. */
|
|
12
|
+
dir: string;
|
|
13
|
+
/** The temp root used, when it is not the configured one. */
|
|
14
|
+
fallbackRoot?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TurnScratchDeps {
|
|
17
|
+
/** The configured temp root. Defaults to os.tmpdir(), exactly as before. */
|
|
18
|
+
tmpdir?: string;
|
|
19
|
+
platform?: NodeJS.Platform;
|
|
20
|
+
home?: string;
|
|
21
|
+
warn?: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createTurnScratchDir(prefix: string, deps?: TurnScratchDeps): TurnScratchDir;
|
|
24
|
+
//# sourceMappingURL=scratch-dir.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
/** Warn once per unwritable folder per bot process, not on every turn. */
|
|
5
|
+
const warned = new Set();
|
|
6
|
+
/** Errors that mean "this folder cannot be used"; anything else is rethrown. */
|
|
7
|
+
const UNUSABLE = new Set(["EPERM", "EACCES", "ENOENT", "ENOTDIR", "EROFS"]);
|
|
8
|
+
export function createTurnScratchDir(prefix, deps = {}) {
|
|
9
|
+
const platform = deps.platform ?? process.platform;
|
|
10
|
+
const home = deps.home ?? os.homedir();
|
|
11
|
+
const warn = deps.warn ?? ((line) => process.stderr.write(`${line}\n`));
|
|
12
|
+
const configured = deps.tmpdir ?? os.tmpdir();
|
|
13
|
+
const fallbacks = [];
|
|
14
|
+
if (platform !== "win32")
|
|
15
|
+
fallbacks.push("/tmp");
|
|
16
|
+
fallbacks.push(path.join(home, ".hq", "tmp"));
|
|
17
|
+
let firstError;
|
|
18
|
+
for (const root of [configured, ...fallbacks.filter((f) => f !== configured)]) {
|
|
19
|
+
try {
|
|
20
|
+
if (root !== configured)
|
|
21
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
22
|
+
const dir = fs.mkdtempSync(path.join(root, prefix));
|
|
23
|
+
if (root === configured) {
|
|
24
|
+
warned.delete(configured);
|
|
25
|
+
return { dir };
|
|
26
|
+
}
|
|
27
|
+
if (!warned.has(configured))
|
|
28
|
+
warn(`hq bot: temp folder ${configured} is not writable (${firstError?.code ?? "error"}); using ${root} instead. Fix TMPDIR or run hq bot restart.`);
|
|
29
|
+
warned.add(configured);
|
|
30
|
+
return { dir, fallbackRoot: root };
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const e = err;
|
|
34
|
+
if (!e.code || !UNUSABLE.has(e.code))
|
|
35
|
+
throw err;
|
|
36
|
+
firstError ??= e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`hq bot cannot create a temporary folder: ${configured} is not writable (${firstError?.code ?? "error"}), and ${fallbacks.join(" and ")} did not work either. Fix TMPDIR or run hq bot restart.`);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=scratch-dir.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function callerIsMachinePrincipal(env?: NodeJS.ProcessEnv): boolean;
|
|
2
|
+
/** True when the token names an agent (agt_) or outpost (out_) identity. */
|
|
3
|
+
export declare function tokenIsMachinePrincipal(token: string): boolean;
|
|
4
|
+
//# sourceMappingURL=caller-principal.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the current caller is a machine principal (a local bot, an
|
|
3
|
+
* agent-kit agent, a hosted agent or an outpost) or a person login.
|
|
4
|
+
*
|
|
5
|
+
* The answer comes from the credentials the request will actually use, never
|
|
6
|
+
* from a user flag:
|
|
7
|
+
* - CLI sends go through `ensureCognitoToken()`, which picks machine tokens
|
|
8
|
+
* exactly when `wantsMachineCognitoTokens()` is true (HQ_MACHINE_CREDS_FILE
|
|
9
|
+
* set and valid, or the default machine creds file on a box with no person
|
|
10
|
+
* session). This check reads local state only and makes no network call.
|
|
11
|
+
* - Callers that already hold a token (the agent-kit MCP tools) use
|
|
12
|
+
* `tokenIsMachinePrincipal`, which reads the token's identity claims.
|
|
13
|
+
*/
|
|
14
|
+
import { wantsMachineCognitoTokens } from "../utils/machine-principal-state.js";
|
|
15
|
+
import { peekIdToken } from "../utils/id-token.js";
|
|
16
|
+
export function callerIsMachinePrincipal(env = process.env) {
|
|
17
|
+
try {
|
|
18
|
+
return wantsMachineCognitoTokens({}, env);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const MACHINE_ENTITY_TYPES = new Set(["agent", "outpost", "company-outpost"]);
|
|
25
|
+
/** True when the token names an agent (agt_) or outpost (out_) identity. */
|
|
26
|
+
export function tokenIsMachinePrincipal(token) {
|
|
27
|
+
let claims;
|
|
28
|
+
try {
|
|
29
|
+
claims = peekIdToken(token);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const type = claims["custom:entityType"];
|
|
35
|
+
if (typeof type === "string" && MACHINE_ENTITY_TYPES.has(type))
|
|
36
|
+
return true;
|
|
37
|
+
const uid = claims["custom:entityUid"];
|
|
38
|
+
return typeof uid === "string" && /^(agt|out)_/.test(uid);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=caller-principal.js.map
|
|
@@ -192,7 +192,10 @@ export function mapGrokBillingBody(body, options) {
|
|
|
192
192
|
export async function collectGrok(options = {}) {
|
|
193
193
|
const ledger = ledgerOptions(options);
|
|
194
194
|
const root = ledger.root ?? capacityRoot(ledger.env, ledger.home);
|
|
195
|
-
|
|
195
|
+
// Forward the injected clock so ledger retention and now.json use the same
|
|
196
|
+
// instant as observedAt; otherwise retention runs on the wall clock and can
|
|
197
|
+
// delete readings this call just wrote.
|
|
198
|
+
const scoped = { ...ledger, root, now: options.now ?? ledger.now };
|
|
196
199
|
if (readCollectorMark(scoped)?.enabled === false && !options.allowDisabled) {
|
|
197
200
|
return { readings: [], failures: [], view: null, skipped: "disabled", accountId: null };
|
|
198
201
|
}
|
|
@@ -474,7 +477,10 @@ function stampAccount(row, options, planType) {
|
|
|
474
477
|
async function collectEnumeratedGrok(options) {
|
|
475
478
|
const ledger = ledgerOptions(options);
|
|
476
479
|
const root = ledger.root ?? capacityRoot(ledger.env, ledger.home);
|
|
477
|
-
|
|
480
|
+
// Forward the injected clock so ledger retention and now.json use the same
|
|
481
|
+
// instant as observedAt; otherwise retention runs on the wall clock and can
|
|
482
|
+
// delete readings this call just wrote.
|
|
483
|
+
const scoped = { ...ledger, root, now: options.now ?? ledger.now };
|
|
478
484
|
const clock = options.now ?? ledger.now ?? (() => new Date());
|
|
479
485
|
const cliVersion = options.cliVersion ?? readInstalledGrokVersion(options.readVersion);
|
|
480
486
|
const groups = new Map();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** hq-flags keys used by CLI-wide routes. */
|
|
2
2
|
export declare const CLI_COMMAND_HELP_CATALOG_FLAG = "cli.command-help-catalog";
|
|
3
3
|
export declare const MULTI_COMPANY_SESSION_LOCK_FLAG = "hooks.multi-company-session-lock";
|
|
4
|
+
/** Kill switch (default on) for company-scoped `hq core rebuild-index` root indexes. */
|
|
5
|
+
export declare const CORE_COMPANY_INDEX_SCOPE_FLAG = "cli.rebuild-index-company-scope";
|
|
4
6
|
/**
|
|
5
7
|
* hq-flags key for HQ Storage (`hq storage` and Settings > Storage in the
|
|
6
8
|
* desktop app). Shared with hq-desktop-app, so it carries no `cli.` prefix,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** hq-flags keys used by CLI-wide routes. */
|
|
2
2
|
export const CLI_COMMAND_HELP_CATALOG_FLAG = "cli.command-help-catalog";
|
|
3
3
|
export const MULTI_COMPANY_SESSION_LOCK_FLAG = "hooks.multi-company-session-lock";
|
|
4
|
+
/** Kill switch (default on) for company-scoped `hq core rebuild-index` root indexes. */
|
|
5
|
+
export const CORE_COMPANY_INDEX_SCOPE_FLAG = "cli.rebuild-index-company-scope";
|
|
4
6
|
/**
|
|
5
7
|
* hq-flags key for HQ Storage (`hq storage` and Settings > Storage in the
|
|
6
8
|
* desktop app). Shared with hq-desktop-app, so it carries no `cli.` prefix,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { MachineType } from "./machine-type.js";
|
|
13
13
|
import type { SyncOwner } from "./config.js";
|
|
14
|
+
import type { SyncCoreCanaryPlan } from "./sync-core-canary.js";
|
|
14
15
|
import type { RemoteControlPoolsConfig } from "./remote-control-pools.js";
|
|
15
16
|
import { type SyncRunnerProcess } from "./sync-runner.js";
|
|
16
17
|
import { type VaultSyncSwitches } from "../vault-sync-switches.js";
|
|
@@ -112,6 +113,9 @@ export interface PlanContext {
|
|
|
112
113
|
vaultSync?: VaultSyncSwitches;
|
|
113
114
|
/** Persisted/effective owner selected for this machine. */
|
|
114
115
|
syncOwner: SyncOwner;
|
|
116
|
+
syncCoreCanary?: SyncCoreCanaryPlan;
|
|
117
|
+
legacySyncReadyForCore?: boolean;
|
|
118
|
+
syncCoreCanaryRunning?: boolean;
|
|
115
119
|
/** A legacy desktop app or sync unit is still managing sync. */
|
|
116
120
|
desktopAppManagesSync: boolean;
|
|
117
121
|
/** Full process list, or null when the daemon could not read it. */
|
|
@@ -123,33 +123,13 @@ function planService(service, ctx) {
|
|
|
123
123
|
const agentMachine = ctx.machineType === "agent-kit" || ctx.machineType === "agent-box";
|
|
124
124
|
switch (service) {
|
|
125
125
|
case "sync": {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"--companies",
|
|
134
|
-
"--direction",
|
|
135
|
-
"both",
|
|
136
|
-
"--on-conflict",
|
|
137
|
-
"keep",
|
|
138
|
-
"--hq-root",
|
|
139
|
-
ctx.hqRoot ?? "",
|
|
140
|
-
"--watch",
|
|
141
|
-
...(ctx.instantSyncEnabled === false ? [] : ["--event-push"]),
|
|
142
|
-
"--owner",
|
|
143
|
-
SYNC_RUNNER_OWNER,
|
|
144
|
-
],
|
|
145
|
-
},
|
|
146
|
-
env: {
|
|
147
|
-
...ctx.legacySyncEnv,
|
|
148
|
-
...(ctx.vaultSync ? vaultSkipEnv(ctx.vaultSync) : {}),
|
|
149
|
-
HQ_SYNC_PASS_REQUEST_DIR: ctx.syncPassRequestsDir,
|
|
150
|
-
},
|
|
151
|
-
heartbeatTimeoutMs: ctx.syncHeartbeatTimeoutMs,
|
|
152
|
-
});
|
|
126
|
+
const vaultSync = ctx.syncCoreCanaryRunning
|
|
127
|
+
? {
|
|
128
|
+
allOff: ctx.vaultSync?.allOff === true,
|
|
129
|
+
personalOff: true,
|
|
130
|
+
offCompanies: ctx.vaultSync?.offCompanies ?? new Set(),
|
|
131
|
+
}
|
|
132
|
+
: ctx.vaultSync;
|
|
153
133
|
let ownerBlock;
|
|
154
134
|
if (ctx.machineType === "desktop" && ctx.syncOwner === "desktop-app" && ctx.desktopAppManagesSync) {
|
|
155
135
|
ownerBlock = "the desktop app is still managing sync (run `hq daemon sync take-ownership` after it stops its runner)";
|
|
@@ -163,7 +143,7 @@ function planService(service, ctx) {
|
|
|
163
143
|
? "could not check for an existing sync runner; waiting to avoid starting a duplicate"
|
|
164
144
|
: syncRunnerUnleasedBlockReason(ctx.hqRoot, ctx.syncRunnerProcesses, ctx.syncRunnerOwnerPid, ctx.platform, ctx.daemonSyncRunnerPids);
|
|
165
145
|
}
|
|
166
|
-
|
|
146
|
+
const commonBlock = firstBlock([
|
|
167
147
|
[ctx.syncPaused, "sync is paused (run `hq daemon sync resume`)"],
|
|
168
148
|
[
|
|
169
149
|
ctx.vaultSync?.allOff === true,
|
|
@@ -173,7 +153,61 @@ function planService(service, ctx) {
|
|
|
173
153
|
[!ctx.signedIn, NOT_SIGNED_IN],
|
|
174
154
|
[!!ownerBlock, ownerBlock ?? ""],
|
|
175
155
|
]);
|
|
176
|
-
|
|
156
|
+
const unit = ctx.syncRunnerPath
|
|
157
|
+
? base(service, {
|
|
158
|
+
command: {
|
|
159
|
+
type: "node",
|
|
160
|
+
script: ctx.syncRunnerPath,
|
|
161
|
+
args: [
|
|
162
|
+
"--companies",
|
|
163
|
+
"--direction",
|
|
164
|
+
"both",
|
|
165
|
+
"--on-conflict",
|
|
166
|
+
"keep",
|
|
167
|
+
"--hq-root",
|
|
168
|
+
ctx.hqRoot ?? "",
|
|
169
|
+
"--watch",
|
|
170
|
+
...(ctx.instantSyncEnabled === false ? [] : ["--event-push"]),
|
|
171
|
+
"--owner",
|
|
172
|
+
SYNC_RUNNER_OWNER,
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
env: {
|
|
176
|
+
...ctx.legacySyncEnv,
|
|
177
|
+
...(vaultSync ? vaultSkipEnv(vaultSync) : {}),
|
|
178
|
+
HQ_SYNC_PASS_REQUEST_DIR: ctx.syncPassRequestsDir,
|
|
179
|
+
},
|
|
180
|
+
heartbeatTimeoutMs: ctx.syncHeartbeatTimeoutMs,
|
|
181
|
+
blocked: commonBlock,
|
|
182
|
+
})
|
|
183
|
+
: unavailable(service, "the hq-cloud sync runner is missing from this CLI install");
|
|
184
|
+
if (!ctx.syncCoreCanary)
|
|
185
|
+
return [unit];
|
|
186
|
+
const core = base(service, {
|
|
187
|
+
id: "sync-core-canary",
|
|
188
|
+
description: ctx.syncCoreCanary.fullVault
|
|
189
|
+
? "keeps the personal vault in sync with sync-core"
|
|
190
|
+
: "keeps one personal-vault canary in sync with sync-core",
|
|
191
|
+
command: ctx.syncCoreCanary.binaryPath
|
|
192
|
+
? { type: "exec", file: ctx.syncCoreCanary.binaryPath, args: ["start", "--loop"] }
|
|
193
|
+
: null,
|
|
194
|
+
env: {
|
|
195
|
+
HQ_SYNC_ENGINE: "rust-test",
|
|
196
|
+
HQ_SYNC_MODE: "full",
|
|
197
|
+
HQ_SYNC_PROTOCOL: "1",
|
|
198
|
+
...(ctx.syncCoreCanary.vaultUid ? { HQ_SYNC_CANARY_VAULT: ctx.syncCoreCanary.vaultUid } : {}),
|
|
199
|
+
...(ctx.syncCoreCanary.canaryPath ? { HQ_SYNC_CANARY_PATH: ctx.syncCoreCanary.canaryPath } : {}),
|
|
200
|
+
...(ctx.syncCoreCanary.fullVault ? { HQ_SYNC_FULL_VAULT: "1" } : {}),
|
|
201
|
+
},
|
|
202
|
+
heartbeatTimeoutMs: 180_000,
|
|
203
|
+
blocked: firstBlock([
|
|
204
|
+
[!!commonBlock, commonBlock?.reason ?? ""],
|
|
205
|
+
[!!ctx.syncCoreCanary.blockedReason, ctx.syncCoreCanary.blockedReason ?? ""],
|
|
206
|
+
[ctx.vaultSync?.personalOff !== true, "turn the personal vault off in the current sync before starting the sync-core canary"],
|
|
207
|
+
[ctx.legacySyncReadyForCore !== true, "waiting for legacy sync to run its company-only plan"],
|
|
208
|
+
]),
|
|
209
|
+
});
|
|
210
|
+
return [unit, core];
|
|
177
211
|
}
|
|
178
212
|
case "mesh": {
|
|
179
213
|
const args = ["mesh", "daemon", "run", ...(ctx.hqRoot ? ["--hq-root", ctx.hqRoot] : [])];
|
|
@@ -18,12 +18,19 @@ export interface DaemonConfig {
|
|
|
18
18
|
instantSyncEnabled?: boolean;
|
|
19
19
|
/** Explicit desktop handoff; absent values preserve the legacy per-machine owner. */
|
|
20
20
|
syncOwner?: SyncOwner;
|
|
21
|
+
syncCoreCanary?: SyncCoreCanaryConfig;
|
|
22
|
+
syncCoreCanaryError?: string;
|
|
23
|
+
syncCoreCanaryInvalid?: unknown;
|
|
21
24
|
/** Services turned on here beyond the machine type's defaults. */
|
|
22
25
|
enable: string[];
|
|
23
26
|
/** Default services turned off here. */
|
|
24
27
|
disable: string[];
|
|
25
28
|
}
|
|
26
29
|
export type SyncOwner = "daemon" | "desktop-app";
|
|
30
|
+
export interface SyncCoreCanaryConfig {
|
|
31
|
+
binaryPath: string;
|
|
32
|
+
vaultUid: string;
|
|
33
|
+
}
|
|
27
34
|
export declare function effectiveSyncOwner(config: Pick<DaemonConfig, "syncOwner">, machineType: MachineType): SyncOwner;
|
|
28
35
|
export declare function readDaemonConfig(file: string): DaemonConfig;
|
|
29
36
|
export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
|
|
@@ -19,6 +19,27 @@ function emptyDaemonConfig() {
|
|
|
19
19
|
function stringList(value) {
|
|
20
20
|
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
21
21
|
}
|
|
22
|
+
function syncCoreCanary(value, file) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return {};
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
26
|
+
return { error: `hq daemon config at ${file} has invalid syncCoreCanary`, invalid: value };
|
|
27
|
+
}
|
|
28
|
+
const candidate = value;
|
|
29
|
+
if (typeof candidate.binaryPath !== "string" || !path.isAbsolute(candidate.binaryPath)) {
|
|
30
|
+
return {
|
|
31
|
+
error: `hq daemon config at ${file} has invalid syncCoreCanary: binaryPath must be absolute`,
|
|
32
|
+
invalid: value,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (typeof candidate.vaultUid !== "string" || !/^prs_[A-Za-z0-9][A-Za-z0-9_-]*$/.test(candidate.vaultUid)) {
|
|
36
|
+
return {
|
|
37
|
+
error: `hq daemon config at ${file} has invalid syncCoreCanary: vaultUid must name one personal vault`,
|
|
38
|
+
invalid: value,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { config: { binaryPath: candidate.binaryPath, vaultUid: candidate.vaultUid } };
|
|
42
|
+
}
|
|
22
43
|
export function readDaemonConfig(file) {
|
|
23
44
|
let text;
|
|
24
45
|
try {
|
|
@@ -51,6 +72,13 @@ export function readDaemonConfig(file) {
|
|
|
51
72
|
config.machineType = obj.machineType;
|
|
52
73
|
if (obj.syncOwner === "daemon" || obj.syncOwner === "desktop-app")
|
|
53
74
|
config.syncOwner = obj.syncOwner;
|
|
75
|
+
const parsedCanary = syncCoreCanary(obj.syncCoreCanary, file);
|
|
76
|
+
if (parsedCanary.config)
|
|
77
|
+
config.syncCoreCanary = parsedCanary.config;
|
|
78
|
+
if (parsedCanary.error)
|
|
79
|
+
config.syncCoreCanaryError = parsedCanary.error;
|
|
80
|
+
if (parsedCanary.error)
|
|
81
|
+
config.syncCoreCanaryInvalid = parsedCanary.invalid;
|
|
54
82
|
if (typeof obj.hqRoot === "string" && obj.hqRoot.trim())
|
|
55
83
|
config.hqRoot = obj.hqRoot;
|
|
56
84
|
if (typeof obj.hqCloudVersion === "string" && validSemver(obj.hqCloudVersion) === obj.hqCloudVersion) {
|
|
@@ -73,6 +101,10 @@ export function writeDaemonConfig(file, config) {
|
|
|
73
101
|
ordered.instantSyncEnabled = config.instantSyncEnabled;
|
|
74
102
|
if (config.syncOwner)
|
|
75
103
|
ordered.syncOwner = config.syncOwner;
|
|
104
|
+
if (config.syncCoreCanary)
|
|
105
|
+
ordered.syncCoreCanary = config.syncCoreCanary;
|
|
106
|
+
else if (config.syncCoreCanaryError)
|
|
107
|
+
ordered.syncCoreCanary = config.syncCoreCanaryInvalid;
|
|
76
108
|
ordered.enable = config.enable;
|
|
77
109
|
ordered.disable = config.disable;
|
|
78
110
|
fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}\n`, { mode: 0o600 });
|