@indigoai-us/hq-cli 5.345.202 → 5.345.204
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 +18 -0
- package/dist/command-catalog.generated.d.ts +24 -0
- package/dist/command-catalog.generated.js +26 -0
- package/dist/commands/bot.js +7 -2
- package/dist/commands/capacity.js +10 -2
- package/dist/commands/cloud.d.ts +14 -2
- package/dist/commands/cloud.js +25 -16
- package/dist/commands/daemon.d.ts +2 -1
- package/dist/commands/daemon.js +225 -4
- package/dist/commands/integrations-core.d.ts +3 -0
- package/dist/commands/integrations-core.js +11 -0
- package/dist/commands/integrations-manage.js +2 -2
- package/dist/commands/integrations.js +5 -5
- package/dist/commands/sync-core.js +4 -1
- package/dist/lib/bot/claude-login.d.ts +7 -1
- package/dist/lib/bot/claude-login.js +56 -16
- package/dist/lib/bot/executive-settings.d.ts +1 -1
- package/dist/lib/bot/executive-settings.js +8 -6
- package/dist/lib/bot/jobs/meeting-detect.js +2 -2
- package/dist/lib/bot/jobs/meeting-rider.js +2 -2
- package/dist/lib/bot/run.js +3 -3
- package/dist/lib/capacity/codex-availability.d.ts +6 -0
- package/dist/lib/capacity/codex-availability.js +33 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +53 -5
- package/dist/lib/capacity/collectors/claude-oauth.js +251 -71
- package/dist/lib/capacity/ledger.d.ts +0 -11
- package/dist/lib/capacity/ledger.js +30 -3
- package/dist/lib/capacity/route.js +11 -5
- package/dist/lib/daemon/catalog.d.ts +3 -0
- package/dist/lib/daemon/catalog.js +17 -4
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +9 -2
- package/dist/lib/daemon/install.d.ts +99 -0
- package/dist/lib/daemon/install.js +316 -7
- package/dist/lib/daemon/migrate.d.ts +3 -1
- package/dist/lib/daemon/migrate.js +14 -2
- package/dist/lib/daemon/plist-refresh.d.ts +34 -0
- package/dist/lib/daemon/plist-refresh.js +62 -0
- package/dist/lib/daemon/run.js +27 -2
- package/dist/lib/daemon/selfcheck.d.ts +71 -0
- package/dist/lib/daemon/selfcheck.js +150 -0
- package/dist/lib/daemon/supervisor.d.ts +31 -0
- package/dist/lib/daemon/supervisor.js +130 -6
- package/dist/lib/daemon/unit-holds.js +2 -1
- package/dist/lib/lanes/config.js +13 -0
- package/dist/lib/lanes/throttle.d.ts +12 -1
- package/dist/lib/lanes/throttle.js +27 -8
- package/dist/lib/lanes/types.d.ts +5 -0
- package/dist/lib/service-manager/launchd.d.ts +25 -0
- package/dist/lib/service-manager/launchd.js +51 -3
- package/dist/lib/service-manager/migration.d.ts +14 -0
- package/dist/lib/service-manager/migration.js +34 -0
- package/dist/lib/service-manager/systemd.d.ts +13 -0
- package/dist/lib/service-manager/systemd.js +19 -0
- package/dist/lib/service-manager/types.d.ts +8 -0
- package/dist/lib/signals/steps/extract.d.ts +110 -4
- package/dist/lib/signals/steps/extract.js +520 -39
- package/dist/lib/sync-core/engine-home.d.ts +2 -1
- package/dist/lib/sync-core/engine-home.js +25 -8
- package/dist/lib/sync-core/reconcile.d.ts +7 -2
- package/dist/lib/sync-core/reconcile.js +32 -9
- package/dist/lib/sync-core/release.d.ts +35 -3
- package/dist/lib/sync-core/release.js +95 -12
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.204] — 2026-11-05
|
|
6
|
+
- `hq bot create`, `hq doctor` and the Claude bot sign-in check now read the macOS Keychain item that Claude Code keeps under your login account name first. A Mac can also hold an older item for the same service under another account (often "unknown", with no Claude login in it). The check used to read that one and said Claude was not signed in right after you signed in from HQ Desktop, so Create bot failed.
|
|
7
|
+
- `hq bot create --json` now includes `brainSignIn` (the same shape `hq bot list`, `status` and `start` report) when it refuses a Claude bot because Claude Code is not signed in, so HQ Desktop can offer its own sign-in instead of "Could not create".
|
|
8
|
+
- `hq capacity` renews expired Claude tokens on a busy Mac again. The refresh check reads the lane list once per collect instead of once per account, runs its two reads together without blocking the mesh daemon, skips the Outpost mirror refresh it does not need, and allows each read 30 seconds instead of 15. On macOS it now reads each running `claude` process's environment, so an open session on the default home no longer blocks the refresh of every other account home. A process whose environment cannot be read still blocks every home, and if the lane read fails no token is refreshed. A session started with `HOME` set to another directory and no `CLAUDE_CONFIG_DIR` now blocks the refresh of that directory's `.claude` home.
|
|
9
|
+
- `hq signals run` now runs the extract step on the Grok CLI when the main local bot runs on Grok, instead of failing every source item with "grok has no restricted tool mode". Each turn runs grok 1.0.70 or newer in `dontAsk` mode with only the file reader allowed, grok's own read-only shell list denied, the MCP bridge, web tools and subagents off, a turn cap, and a per-turn `strict` sandbox profile that can read only the item's channel folder (plus the scripts the candidate command needs) and grants no extra writes. Reads of the Grok login are denied for the literal `~/.grok` spelling, the isolated home and the real home, because Grok matches a `~` path literally before expanding it. An older grok, or a main bot on grok with no grok installed, is never run unrestricted: the step uses Claude Code, then Codex, when that tool is installed and signed in, records the fallback in the run's step detail, and otherwise fails the run naming the grok version it needs. On macOS and Windows the step does not select Grok, because Grok only blocks child-process network on Linux, and falls through the same way. Grok has no terminal tool: HQ validates its returned candidate commands, checks the pinned writer still matches the script it generated, and then invokes that writer with fixed arguments, rejecting shell chaining, substitutions and redirections. The host writes candidates after the turn. The resolved Grok executable is pinned for the probe and turn, and probe caching distinguishes changes to PATH. Its isolated temporary home carries only a private login-file copy, excludes user configuration and hooks, and is removed when the turn ends, and its environment is an allowlist.
|
|
10
|
+
- `hq lanes` admission on macOS no longer holds every new lane while `kern.memorystatus_vm_pressure_level` is warn. A warn level now holds only when the free percentage (`kern.memorystatus_level`) is at or below `throttle.memory_warn_min_free_percent` (default 20, a number from 0 to 100); critical still always holds, and `throttle.min_available_mem_mb` still applies. The hold message states this rule.
|
|
11
|
+
- The daemon's new-sync-engine units now plan on Windows. A Windows machine downloads the `windows-x64` or `windows-arm64` zip, installs `hq-syncd.exe` (moving a running copy aside to `hq-syncd.exe.old`), and writes an engine home with no symlinks: the config names the token file and the engine reads `menubar.json` and `work-mesh/` from the daemon's state folder. Windows vaults stay on the current sync, and no engine is downloaded, until `SYNC_CORE_WINDOWS_FULL_RELEASE` names an engine release whose Windows build runs full sync; it is unset in this release. One machine can test a build first by setting `HQ_SYNC_CORE_WINDOWS_FULL_RELEASE` to that release.
|
|
12
|
+
- Show the upload-hold explanation and recovery command in sync status.
|
|
13
|
+
|
|
14
|
+
## [5.345.203] — 2026-11-05
|
|
15
|
+
- Fix Codex capacity routing after plan exhaustion: available credits keep accounts usable at lowest priority, and superseded snapshots or passed resets no longer retain exhausted usage.
|
|
16
|
+
- Keep company Outpost integration commands on their delegated machine session and allow sharing connections with an `out_*` principal.
|
|
17
|
+
- On macOS the `hq daemon` LaunchAgent now runs node directly and is no longer attributed to the HQ app, so launchd loads it at login whether or not the app is installed or allowed. `hq daemon status` now asks launchd or systemd and says whether the daemon runs under the service manager, runs outside it, or is not loaded. New `hq daemon start` loads the unit when it is missing and starts it. A Mac that still has the older plist, which runs through the app, gets it rewritten and reloaded when the daemon next starts or when `hq daemon start` runs, with no `hq daemon install` needed; `hq daemon status` warns while the plist, or the job launchd has loaded, still runs through the app, and `hq daemon start` reloads a job left on the old definition. `hq daemon start` leaves a daemon the desktop app hosts alone, and no longer mistakes state left by an earlier app-hosted daemon for that. `hq daemon install` treats a failed bootstrap as an error and retries it, and `hq daemon migrate --apply` loads the daemon's agent first and loads an older agent again if taking it over fails.
|
|
18
|
+
- The daemon no longer holds a sync engine for 30 minutes when its heartbeat timeouts come from a slow transfer the engine reports progress on (up to six such restarts per episode; after that they count again), and a sync engine held because another process had its lock (exit 17) starts again as soon as that process exits.
|
|
19
|
+
- The sync engine release can now be pinned on the server (`pin.json` on the engine download base); the CLI's built-in release is the fallback, and an installed engine is never downgraded.
|
|
20
|
+
- A self-check in the daemon sends the person signed in on the machine a DM when a sync engine unit has been held or failing for over 30 minutes, at most once a day per machine, naming every affected vault. It reports only units the daemon runs, and sends nothing when the machine is signed in as an agent.
|
|
21
|
+
- The Executive opt-in key is now `executive.enabled` in the HQ menubar settings file. The old `executiveMeetingRider.enabled` key is ignored, so values saved under it earlier now read as off.
|
|
22
|
+
|
|
5
23
|
## [5.345.202] — 2026-11-04
|
|
6
24
|
- The long-running `hq daemon run` and `hq mesh daemon run` processes no longer start Sentry. On some Macs, Sentry in these processes kept opening connections after network changes until the machine ran out of ports, so Claude and HQ lost internet access while other apps kept working. Short-lived CLI commands still report errors to Sentry, and the daemons keep their local logs.
|
|
7
25
|
- The Executive (meeting rider, action-item lanes and source report DMs) is now off unless `executiveMeetingRider.enabled` is set to `true` in the HQ menubar settings file. In 5.345.200 it was on by default for every setup bot.
|
|
@@ -18236,6 +18236,18 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
18236
18236
|
readonly hidden: false;
|
|
18237
18237
|
}];
|
|
18238
18238
|
readonly subcommands: readonly [];
|
|
18239
|
+
}, {
|
|
18240
|
+
readonly name: "start";
|
|
18241
|
+
readonly description: "Load the daemon's OS unit if the service manager does not have it, and start it";
|
|
18242
|
+
readonly aliases: readonly [];
|
|
18243
|
+
readonly hidden: false;
|
|
18244
|
+
readonly helpOption: true;
|
|
18245
|
+
readonly helpCommand: false;
|
|
18246
|
+
readonly usage: "[options]";
|
|
18247
|
+
readonly helpText: readonly [];
|
|
18248
|
+
readonly arguments: readonly [];
|
|
18249
|
+
readonly options: readonly [];
|
|
18250
|
+
readonly subcommands: readonly [];
|
|
18239
18251
|
}, {
|
|
18240
18252
|
readonly name: "uninstall";
|
|
18241
18253
|
readonly description: "Stop the daemon and remove its OS unit";
|
|
@@ -18816,6 +18828,18 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
18816
18828
|
readonly hidden: false;
|
|
18817
18829
|
}];
|
|
18818
18830
|
readonly subcommands: readonly [];
|
|
18831
|
+
}, {
|
|
18832
|
+
readonly name: "sync-core-selfcheck";
|
|
18833
|
+
readonly description: "DM the person signed in here when a sync engine unit has been held or failing for over 30 minutes (at most daily per unit set)";
|
|
18834
|
+
readonly aliases: readonly [];
|
|
18835
|
+
readonly hidden: false;
|
|
18836
|
+
readonly helpOption: true;
|
|
18837
|
+
readonly helpCommand: false;
|
|
18838
|
+
readonly usage: "[options]";
|
|
18839
|
+
readonly helpText: readonly [];
|
|
18840
|
+
readonly arguments: readonly [];
|
|
18841
|
+
readonly options: readonly [];
|
|
18842
|
+
readonly subcommands: readonly [];
|
|
18819
18843
|
}, {
|
|
18820
18844
|
readonly name: "sync-core-manifest";
|
|
18821
18845
|
readonly description: "Upload the sync-audit manifest for every vault the new sync engine owns on this machine";
|
|
@@ -22248,6 +22248,19 @@ export const COMMAND_CATALOG = [
|
|
|
22248
22248
|
],
|
|
22249
22249
|
"subcommands": []
|
|
22250
22250
|
},
|
|
22251
|
+
{
|
|
22252
|
+
"name": "start",
|
|
22253
|
+
"description": "Load the daemon's OS unit if the service manager does not have it, and start it",
|
|
22254
|
+
"aliases": [],
|
|
22255
|
+
"hidden": false,
|
|
22256
|
+
"helpOption": true,
|
|
22257
|
+
"helpCommand": false,
|
|
22258
|
+
"usage": "[options]",
|
|
22259
|
+
"helpText": [],
|
|
22260
|
+
"arguments": [],
|
|
22261
|
+
"options": [],
|
|
22262
|
+
"subcommands": []
|
|
22263
|
+
},
|
|
22251
22264
|
{
|
|
22252
22265
|
"name": "uninstall",
|
|
22253
22266
|
"description": "Stop the daemon and remove its OS unit",
|
|
@@ -22941,6 +22954,19 @@ export const COMMAND_CATALOG = [
|
|
|
22941
22954
|
],
|
|
22942
22955
|
"subcommands": []
|
|
22943
22956
|
},
|
|
22957
|
+
{
|
|
22958
|
+
"name": "sync-core-selfcheck",
|
|
22959
|
+
"description": "DM the person signed in here when a sync engine unit has been held or failing for over 30 minutes (at most daily per unit set)",
|
|
22960
|
+
"aliases": [],
|
|
22961
|
+
"hidden": false,
|
|
22962
|
+
"helpOption": true,
|
|
22963
|
+
"helpCommand": false,
|
|
22964
|
+
"usage": "[options]",
|
|
22965
|
+
"helpText": [],
|
|
22966
|
+
"arguments": [],
|
|
22967
|
+
"options": [],
|
|
22968
|
+
"subcommands": []
|
|
22969
|
+
},
|
|
22944
22970
|
{
|
|
22945
22971
|
"name": "sync-core-manifest",
|
|
22946
22972
|
"description": "Upload the sync-audit manifest for every vault the new sync engine owns on this machine",
|
package/dist/commands/bot.js
CHANGED
|
@@ -935,7 +935,11 @@ function requireClaudeCodeLoginForCreate(name) {
|
|
|
935
935
|
const check = botCreateSeams.claudeLogin();
|
|
936
936
|
if (check.signedIn)
|
|
937
937
|
return;
|
|
938
|
-
|
|
938
|
+
const reason = `Claude Code is not signed in on this computer, so ${name} could not answer.`;
|
|
939
|
+
// The same `brainSignIn` shape list, status and start report, so the
|
|
940
|
+
// desktop app offers its own sign-in instead of showing a terminal command.
|
|
941
|
+
const brainSignIn = { provider: "claude", needed: true, reason, command: "claude /login" };
|
|
942
|
+
throw Object.assign(new Error(`${reason} ${describeClaudeLoginSources(check)} ${claudeLoginFix()}`.replace(/\s+/g, " ").trim()), { expected: true, brainSignIn });
|
|
939
943
|
}
|
|
940
944
|
export const botStopSeams = {
|
|
941
945
|
/** True when this process runs inside the bot process `botPid` (one of its turns). */
|
|
@@ -1099,7 +1103,8 @@ export async function runBotCreate(nameArg, opts) {
|
|
|
1099
1103
|
// there too, not only a line on stderr.
|
|
1100
1104
|
if (opts.json && !err?.jsonReported) {
|
|
1101
1105
|
const message = err instanceof Error ? err.message : String(err);
|
|
1102
|
-
|
|
1106
|
+
const brainSignIn = err?.brainSignIn ?? null;
|
|
1107
|
+
console.log(JSON.stringify({ ok: false, name: nameArg, reason: "create-failed", message, ...(brainSignIn ? { brainSignIn } : {}) }, null, 2));
|
|
1103
1108
|
}
|
|
1104
1109
|
throw err;
|
|
1105
1110
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasCodexCreditRoom } from "../lib/capacity/codex-availability.js";
|
|
1
2
|
import * as crypto from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import * as os from "node:os";
|
|
@@ -98,7 +99,10 @@ export function formatCapacityTable(view, nowMs, accounts = []) {
|
|
|
98
99
|
return view.providers.map((provider) => {
|
|
99
100
|
const head = `${provider.provider} ${provider.state}`;
|
|
100
101
|
if (!provider.accounts || provider.accounts.length === 0) {
|
|
101
|
-
|
|
102
|
+
const creditNote = provider.provider === "codex" && provider.state === "ok"
|
|
103
|
+
&& provider.limits.some((limit) => limit.state === "exhausted")
|
|
104
|
+
? " — usable on Codex credits" : "";
|
|
105
|
+
return `${head}${creditNote}\n${formatLimitTable(provider.limits, nowMs)}`;
|
|
102
106
|
}
|
|
103
107
|
const blocks = provider.accounts.map((account) => {
|
|
104
108
|
const labels = account.labels.map((label) => {
|
|
@@ -112,7 +116,11 @@ export function formatCapacityTable(view, nowMs, accounts = []) {
|
|
|
112
116
|
const fix = presented.command ? formatRefreshLine(provider.provider, presented.command) : "";
|
|
113
117
|
return fix ? `${title}\n${reason}\n${fix}` : `${title}\n${reason}`;
|
|
114
118
|
}
|
|
115
|
-
|
|
119
|
+
const creditNote = provider.provider === "codex"
|
|
120
|
+
&& account.limits.some((limit) => limit.state === "exhausted")
|
|
121
|
+
&& hasCodexCreditRoom(account.limits, nowMs)
|
|
122
|
+
? " — usable on Codex credits (ranked last)" : "";
|
|
123
|
+
return `${title}${creditNote}\n${formatLimitTable(account.limits, nowMs)}`;
|
|
116
124
|
});
|
|
117
125
|
return [head, ...blocks].join("\n");
|
|
118
126
|
}).join("\n\n");
|
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -210,15 +210,27 @@ export declare function scopeExcludedWarning(count: number): string | null;
|
|
|
210
210
|
* largest count it sees instead of a sum: a repeated emit for the same hold
|
|
211
211
|
* cannot inflate the total. A missing, non-numeric or negative count reads as 0.
|
|
212
212
|
*/
|
|
213
|
+
export interface IdentityHeldNotice {
|
|
214
|
+
count: number;
|
|
215
|
+
/** Plain-language hold from hq-cloud, when that build sends one. */
|
|
216
|
+
message?: string;
|
|
217
|
+
}
|
|
213
218
|
export declare function createIdentityHeldTracker(): {
|
|
214
219
|
observe: (event: {
|
|
215
220
|
type: string;
|
|
216
221
|
count?: unknown;
|
|
222
|
+
message?: unknown;
|
|
223
|
+
reason?: unknown;
|
|
217
224
|
}) => void;
|
|
218
225
|
count: () => number;
|
|
226
|
+
notice: () => IdentityHeldNotice;
|
|
219
227
|
};
|
|
220
|
-
/**
|
|
221
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Recovery lines for files held by the account-switch push fence.
|
|
230
|
+
* A newer hq-cloud sends `message` (file names and `hq sync release-held`).
|
|
231
|
+
* Older builds send a count only, and this keeps the previous sentence.
|
|
232
|
+
*/
|
|
233
|
+
export declare function identityHeldLines(count: number, notice?: Pick<IdentityHeldNotice, "message">): string[];
|
|
222
234
|
/** Actionable recovery for files refused because the vault baseline is stale. */
|
|
223
235
|
export declare function staleUploadRefusalLines(count: number, paths?: readonly string[]): string[];
|
|
224
236
|
/**
|
package/dist/commands/cloud.js
CHANGED
|
@@ -500,19 +500,9 @@ export function scopeExcludedWarning(count) {
|
|
|
500
500
|
`They were skipped, not synced. Re-run with --json to list them, or ` +
|
|
501
501
|
`ask an admin to grant you write on those paths.`);
|
|
502
502
|
}
|
|
503
|
-
/**
|
|
504
|
-
* Counts the files share() held back because this sync scope is fenced after an account
|
|
505
|
-
* switch on this computer (feedback_a69e12ba). share() reports the hold only as
|
|
506
|
-
* an `identity-switch-push-held` event and leaves the files out of every
|
|
507
|
-
* result counter, so without this the push printed "Pushed 0 file(s) (0 B,
|
|
508
|
-
* 0 skipped)" as a success while the vault kept the old copy.
|
|
509
|
-
*
|
|
510
|
-
* Each event carries the size of one fenced hold, so the tracker keeps the
|
|
511
|
-
* largest count it sees instead of a sum: a repeated emit for the same hold
|
|
512
|
-
* cannot inflate the total. A missing, non-numeric or negative count reads as 0.
|
|
513
|
-
*/
|
|
514
503
|
export function createIdentityHeldTracker() {
|
|
515
504
|
let held = 0;
|
|
505
|
+
let message;
|
|
516
506
|
return {
|
|
517
507
|
observe(event) {
|
|
518
508
|
if (event.type !== "identity-switch-push-held")
|
|
@@ -521,12 +511,24 @@ export function createIdentityHeldTracker() {
|
|
|
521
511
|
? Math.max(0, Math.trunc(event.count))
|
|
522
512
|
: 0;
|
|
523
513
|
held = Math.max(held, n);
|
|
514
|
+
// A company rebind uses the same event type and keeps its own warning.
|
|
515
|
+
if (event.reason === "company-rebind")
|
|
516
|
+
return;
|
|
517
|
+
if (typeof event.message === "string" && event.message.trim())
|
|
518
|
+
message = event.message.trim();
|
|
524
519
|
},
|
|
525
520
|
count: () => held,
|
|
521
|
+
notice: () => ({ count: held, ...(message ? { message } : {}) }),
|
|
526
522
|
};
|
|
527
523
|
}
|
|
528
|
-
/**
|
|
529
|
-
|
|
524
|
+
/**
|
|
525
|
+
* Recovery lines for files held by the account-switch push fence.
|
|
526
|
+
* A newer hq-cloud sends `message` (file names and `hq sync release-held`).
|
|
527
|
+
* Older builds send a count only, and this keeps the previous sentence.
|
|
528
|
+
*/
|
|
529
|
+
export function identityHeldLines(count, notice) {
|
|
530
|
+
if (notice?.message)
|
|
531
|
+
return [`⚠ ${notice.message}`];
|
|
530
532
|
if (count <= 0)
|
|
531
533
|
return [];
|
|
532
534
|
return [
|
|
@@ -1566,7 +1568,7 @@ export function registerCloudCommands(program) {
|
|
|
1566
1568
|
for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
|
|
1567
1569
|
log(chalk.yellow(line));
|
|
1568
1570
|
}
|
|
1569
|
-
for (const line of identityHeldLines(identityHeldCount)) {
|
|
1571
|
+
for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
|
|
1570
1572
|
log(chalk.yellow(line));
|
|
1571
1573
|
}
|
|
1572
1574
|
await syncHealth.succeeded();
|
|
@@ -1599,7 +1601,12 @@ export function registerCloudCommands(program) {
|
|
|
1599
1601
|
for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
|
|
1600
1602
|
log(chalk.yellow(line));
|
|
1601
1603
|
}
|
|
1602
|
-
for (const line of identityHeldLines(identityHeldCount)) {
|
|
1604
|
+
for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
|
|
1605
|
+
log(chalk.yellow(line));
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
else if (identityHeld.notice().message) {
|
|
1609
|
+
for (const line of identityHeldLines(identityHeldCount, identityHeld.notice())) {
|
|
1603
1610
|
log(chalk.yellow(line));
|
|
1604
1611
|
}
|
|
1605
1612
|
}
|
|
@@ -2469,8 +2476,10 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2469
2476
|
const pushRefused = planLimitTracker.summary();
|
|
2470
2477
|
const pushUploadFailed = readUploadFailures(pushResult);
|
|
2471
2478
|
const pushHeldCount = pushIdentityHeld.count();
|
|
2479
|
+
const pushHeldNotice = pushIdentityHeld.notice();
|
|
2472
2480
|
const pushStatus = pushResult.aborted ||
|
|
2473
2481
|
pushHeldCount > 0 ||
|
|
2482
|
+
Boolean(pushHeldNotice.message) ||
|
|
2474
2483
|
pushResult.filesExcludedByScope > 0 ||
|
|
2475
2484
|
(pushResult.filesRefusedStale ?? 0) > 0 ||
|
|
2476
2485
|
pushUploadFailed.count > 0 ||
|
|
@@ -2498,7 +2507,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
2498
2507
|
for (const line of uploadFailedLines(pushUploadFailed.count, pushUploadFailed.paths)) {
|
|
2499
2508
|
console.log(chalk.yellow(line));
|
|
2500
2509
|
}
|
|
2501
|
-
for (const line of identityHeldLines(pushHeldCount)) {
|
|
2510
|
+
for (const line of identityHeldLines(pushHeldCount, pushHeldNotice)) {
|
|
2502
2511
|
console.log(chalk.yellow(line));
|
|
2503
2512
|
}
|
|
2504
2513
|
if (pushResult.aborted) {
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* hq daemon status (same as `hq daemon status`)
|
|
5
5
|
* hq daemon install detect the machine type, install the one OS unit
|
|
6
6
|
* hq daemon run what the OS unit runs; starts the default services
|
|
7
|
-
* hq daemon
|
|
7
|
+
* hq daemon start load the OS unit if needed and start it
|
|
8
|
+
* hq daemon status [--json] includes whether the service manager runs it
|
|
8
9
|
* hq daemon logs [unit] [--follow] [--lines N]
|
|
9
10
|
* hq daemon restart [unit] | trigger <unit>
|
|
10
11
|
* hq daemon enable|disable <service...> per-machine overrides
|
package/dist/commands/daemon.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* hq daemon status (same as `hq daemon status`)
|
|
5
5
|
* hq daemon install detect the machine type, install the one OS unit
|
|
6
6
|
* hq daemon run what the OS unit runs; starts the default services
|
|
7
|
-
* hq daemon
|
|
7
|
+
* hq daemon start load the OS unit if needed and start it
|
|
8
|
+
* hq daemon status [--json] includes whether the service manager runs it
|
|
8
9
|
* hq daemon logs [unit] [--follow] [--lines N]
|
|
9
10
|
* hq daemon restart [unit] | trigger <unit>
|
|
10
11
|
* hq daemon enable|disable <service...> per-machine overrides
|
|
@@ -24,7 +25,8 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits }
|
|
|
24
25
|
import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
25
26
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
26
27
|
import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
|
|
27
|
-
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, classifyWindowsTask, legacyWindowsTask, readWindowsSid, uninstallDaemonUnit, windowsInteractiveSession, windowsTaskName, } from "../lib/daemon/install.js";
|
|
28
|
+
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, classifyWindowsTask, daemonServiceStatus, describeSupervision, legacyWindowsTask, startDaemonUnit, readWindowsSid, uninstallDaemonUnit, windowsInteractiveSession, windowsTaskName, } from "../lib/daemon/install.js";
|
|
29
|
+
import { PLIST_REFRESH_MARKER } from "../lib/daemon/plist-refresh.js";
|
|
28
30
|
import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
|
|
29
31
|
import { isDaemonProcess, liveDaemonLock, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
30
32
|
import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
|
|
@@ -438,28 +440,126 @@ function formatUnit(u) {
|
|
|
438
440
|
const restarts = u.restarts > 0 ? ` (restarted ${u.restarts}x)` : "";
|
|
439
441
|
return ` ${u.id.padEnd(40)} ${u.status.padEnd(12)} ${detail}${restarts}`;
|
|
440
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* True when the daemon holding the lock is the one the desktop app hosts.
|
|
445
|
+
* state.json outlives the daemon that wrote it, and a new daemon writes its own
|
|
446
|
+
* only after starting up, so a `host: "desktop"` left by an earlier app-hosted
|
|
447
|
+
* daemon counts only when its pid is the lock holder's.
|
|
448
|
+
*/
|
|
449
|
+
function hostedByDesktop(state, lockPid) {
|
|
450
|
+
return lockPid !== null && state?.host === "desktop" && state.pid === lockPid;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The service manager's warnings, plus where the automatic plist rewrite
|
|
454
|
+
* logged when it was tried and the plist still needs it.
|
|
455
|
+
*/
|
|
456
|
+
function statusWarnings(service, paths) {
|
|
457
|
+
const warnings = [...(service.warnings ?? [])];
|
|
458
|
+
if (warnings.length > 0 && fs.existsSync(path.join(paths.root, PLIST_REFRESH_MARKER))) {
|
|
459
|
+
warnings.push(`The daemon tried to rewrite it automatically and did not finish. See: ${path.join(paths.root, "logs", "plist-refresh.log")}`);
|
|
460
|
+
}
|
|
461
|
+
return warnings;
|
|
462
|
+
}
|
|
441
463
|
async function runStatus(d, opts) {
|
|
442
464
|
const host = currentHost(d);
|
|
443
465
|
const { paths } = host;
|
|
444
466
|
const pid = runningDaemonPid(paths.lockPath, d.daemonAlive);
|
|
445
467
|
const state = pid ? readState(paths) : null;
|
|
446
468
|
const config = readDaemonConfig(paths.configPath);
|
|
469
|
+
// The pid lock alone says nothing about whether the daemon comes back after
|
|
470
|
+
// a crash or a login: ask the service manager too.
|
|
471
|
+
const service = daemonServiceStatus(hostFor(d, paths, false), d.run, pid);
|
|
472
|
+
// A daemon hosted by the desktop app is supervised by the app, not the OS.
|
|
473
|
+
const hosted = hostedByDesktop(state, pid);
|
|
447
474
|
if (opts.json) {
|
|
448
|
-
d.out(JSON.stringify({
|
|
475
|
+
d.out(JSON.stringify({
|
|
476
|
+
running: pid !== null,
|
|
477
|
+
pid,
|
|
478
|
+
supervision: hosted && service.supervision === "unmanaged" ? "desktop-app" : service.supervision,
|
|
479
|
+
serviceManager: {
|
|
480
|
+
manager: service.manager,
|
|
481
|
+
unitInstalled: service.unitInstalled,
|
|
482
|
+
loaded: service.loaded,
|
|
483
|
+
active: service.active,
|
|
484
|
+
pid: service.managerPid ?? null,
|
|
485
|
+
detail: service.detail,
|
|
486
|
+
},
|
|
487
|
+
warnings: statusWarnings(service, paths),
|
|
488
|
+
machineType: state?.machineType ?? config.machineType ?? null,
|
|
489
|
+
units: state?.units ?? [],
|
|
490
|
+
}, null, 2));
|
|
449
491
|
return;
|
|
450
492
|
}
|
|
493
|
+
const managerLine = hosted && service.supervision === "unmanaged"
|
|
494
|
+
? `Service manager: running as the HQ desktop app's child (${service.detail}).`
|
|
495
|
+
: `Service manager: ${describeSupervision(service)}.`;
|
|
496
|
+
const warnings = statusWarnings(service, paths).map((w) => `Warning: ${w}`);
|
|
451
497
|
if (!pid || !state) {
|
|
452
498
|
const { detected } = host;
|
|
453
499
|
d.out(`hq daemon is not running. Machine type: ${detected.type} (${detected.detail}).`);
|
|
500
|
+
d.out(managerLine);
|
|
501
|
+
for (const line of warnings)
|
|
502
|
+
d.out(line);
|
|
454
503
|
d.out("Default services for this machine:");
|
|
455
504
|
for (const line of describeServices(detected.type, config))
|
|
456
505
|
d.out(line);
|
|
457
506
|
return;
|
|
458
507
|
}
|
|
459
508
|
d.out(`hq daemon is running (pid ${pid}) on a ${state.machineType} machine.`);
|
|
509
|
+
d.out(managerLine);
|
|
510
|
+
for (const line of warnings)
|
|
511
|
+
d.out(line);
|
|
460
512
|
for (const u of state.units)
|
|
461
513
|
d.out(formatUnit(u));
|
|
462
514
|
}
|
|
515
|
+
async function runStart(d) {
|
|
516
|
+
const host = currentHost(d);
|
|
517
|
+
const { paths } = host;
|
|
518
|
+
const pid = runningDaemonPid(paths.lockPath, d.daemonAlive);
|
|
519
|
+
// The desktop app hosts this daemon and restarts it itself. Loading the OS
|
|
520
|
+
// unit now would start a second daemon that exits on the lock and is
|
|
521
|
+
// respawned until the app quits.
|
|
522
|
+
if (hostedByDesktop(readState(paths), pid)) {
|
|
523
|
+
d.out(`hq daemon is running (pid ${pid}) inside the desktop app, which restarts it. ` +
|
|
524
|
+
"Nothing to start. To run it under the service manager instead, quit the app, then run: hq daemon start");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const before = daemonServiceStatus(hostFor(d, paths, false), d.run, pid);
|
|
528
|
+
if (!before.unitInstalled && before.manager !== "none") {
|
|
529
|
+
// Nothing to load: install writes the unit, loads it and starts it.
|
|
530
|
+
d.out("hq daemon is not installed here yet; installing it.");
|
|
531
|
+
await runInstall(d, {});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const result = startDaemonUnit(hostFor(d, paths, false), {
|
|
535
|
+
run: d.run,
|
|
536
|
+
sleep: d.sleep,
|
|
537
|
+
daemonPid: () => runningDaemonPid(paths.lockPath, d.daemonAlive),
|
|
538
|
+
});
|
|
539
|
+
handOver(d, host);
|
|
540
|
+
const refresh = result.refresh;
|
|
541
|
+
if (refresh?.action === "rewritten" || refresh?.action === "reloaded") {
|
|
542
|
+
d.out(`Rewrote ${refresh.plistPath} so launchd loads the daemon at login without HQ.app.`);
|
|
543
|
+
}
|
|
544
|
+
if (refresh?.error)
|
|
545
|
+
d.err(`warning: ${refresh.error}`);
|
|
546
|
+
if (result.action === "already-running") {
|
|
547
|
+
d.out(`hq daemon is already running (pid ${result.status.daemonPid}) under the service manager.`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (result.status.supervision !== "managed" || result.status.daemonPid === null) {
|
|
551
|
+
const what = result.action === "loaded"
|
|
552
|
+
? "Loaded the daemon's unit"
|
|
553
|
+
: result.action === "reloaded"
|
|
554
|
+
? "Reloaded the daemon's unit"
|
|
555
|
+
: "Asked the service manager to start the daemon";
|
|
556
|
+
const why = result.status.supervision === "managed"
|
|
557
|
+
? `no daemon took the lock within 15 seconds (${result.status.detail})`
|
|
558
|
+
: `it is ${describeSupervision(result.status)}`;
|
|
559
|
+
throw new DaemonCommandError(`${what}, but ${why}. See: hq daemon logs`);
|
|
560
|
+
}
|
|
561
|
+
d.out(`${result.action === "loaded" ? "Loaded and started" : result.action === "reloaded" ? "Reloaded and started" : "Started"} hq daemon (pid ${result.status.daemonPid}) under the service manager.`);
|
|
562
|
+
}
|
|
463
563
|
function tailLines(file, lines) {
|
|
464
564
|
const text = fs.readFileSync(file, "utf8");
|
|
465
565
|
return text.split("\n").filter((l, i, all) => i < all.length - 1 || l !== "").slice(-lines);
|
|
@@ -886,7 +986,26 @@ async function runMigrate(d, opts) {
|
|
|
886
986
|
if (!installed) {
|
|
887
987
|
throw new DaemonCommandError("hq daemon is not installed here yet. Run `hq daemon install` first, then `hq daemon migrate --apply`.");
|
|
888
988
|
}
|
|
889
|
-
|
|
989
|
+
// The daemon starts the adopted services, so its own unit must be loaded
|
|
990
|
+
// before the older units are booted out (an installed but unloaded plist
|
|
991
|
+
// would leave every adopted service off until the next manual start).
|
|
992
|
+
const ensureDaemonLoaded = (when) => {
|
|
993
|
+
const daemonPid = () => runningDaemonPid(host.paths.lockPath, d.daemonAlive);
|
|
994
|
+
const status = daemonServiceStatus(hostFor(d, host.paths, false), d.run, daemonPid());
|
|
995
|
+
if (status.loaded || status.manager === "none")
|
|
996
|
+
return;
|
|
997
|
+
try {
|
|
998
|
+
startDaemonUnit(hostFor(d, host.paths, false), { run: d.run, sleep: d.sleep, daemonPid, waitMs: 0 });
|
|
999
|
+
d.out(`Loaded the daemon's unit ${when}.`);
|
|
1000
|
+
}
|
|
1001
|
+
catch (err) {
|
|
1002
|
+
throw new DaemonCommandError(`The daemon's unit is not loaded and could not be loaded ${when}: ${err.message}`);
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
ensureDaemonLoaded("before taking over the older units");
|
|
1006
|
+
const result = adoptLegacyUnits(units, { ...migrateDeps(d, host.paths, host.runAsUser), sleep: d.sleep });
|
|
1007
|
+
if (result.adopted.length > 0)
|
|
1008
|
+
ensureDaemonLoaded("after taking over the older units");
|
|
890
1009
|
handOver(d, host);
|
|
891
1010
|
for (const a of result.adopted)
|
|
892
1011
|
d.out(`Took over ${a.unitId} from ${describeLegacyUnit(a)}`);
|
|
@@ -1306,6 +1425,68 @@ async function runServiceCodexRemoteControl(d, opts) {
|
|
|
1306
1425
|
watch: codex,
|
|
1307
1426
|
});
|
|
1308
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* DM the person signed in on this machine (the machine owner) through the
|
|
1430
|
+
* same route as `hq dm`. A machine identity (agent box) has no person to tell.
|
|
1431
|
+
*/
|
|
1432
|
+
/**
|
|
1433
|
+
* Which engine vaults the self-check reports, and their names: a vault skipped
|
|
1434
|
+
* in ~/.hq/sync-core/skip.json or not on the rollout recorded in rollout.json
|
|
1435
|
+
* is kept on the old sync on purpose, so a hold left over for it is not an outage.
|
|
1436
|
+
*/
|
|
1437
|
+
async function selfCheckVaults(home) {
|
|
1438
|
+
const [{ readSyncCoreSkips }, { readSyncCoreRolloutState }] = await Promise.all([
|
|
1439
|
+
import("../lib/sync-core/skip.js"),
|
|
1440
|
+
import("../lib/sync-core/rollout-state.js"),
|
|
1441
|
+
]);
|
|
1442
|
+
let skipped = new Set();
|
|
1443
|
+
try {
|
|
1444
|
+
skipped = new Set(readSyncCoreSkips(home).map((s) => s.uid));
|
|
1445
|
+
}
|
|
1446
|
+
catch {
|
|
1447
|
+
// An unreadable skip list skips nothing here; the reconcile reports it.
|
|
1448
|
+
}
|
|
1449
|
+
let rollout = null;
|
|
1450
|
+
try {
|
|
1451
|
+
rollout = readSyncCoreRolloutState(home);
|
|
1452
|
+
}
|
|
1453
|
+
catch {
|
|
1454
|
+
// No rollout file: every vault counts.
|
|
1455
|
+
}
|
|
1456
|
+
const onRollout = rollout ? new Map(rollout.vaults.map((v) => [v.uid, v])) : null;
|
|
1457
|
+
return {
|
|
1458
|
+
wanted: (uid) => !skipped.has(uid) && (onRollout === null || onRollout.has(uid)),
|
|
1459
|
+
vaultName: (uid) => {
|
|
1460
|
+
const v = onRollout?.get(uid);
|
|
1461
|
+
if (!v)
|
|
1462
|
+
return undefined;
|
|
1463
|
+
return v.kind === "personal" ? "your personal vault" : v.slug;
|
|
1464
|
+
},
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
async function sendDmToMachineOwner(message) {
|
|
1468
|
+
const [{ ensureCognitoToken }, { agentUidFromToken, resolveCallerPersonUid, vaultApiFetch }] = await Promise.all([
|
|
1469
|
+
import("../utils/cognito-session.js"),
|
|
1470
|
+
import("../utils/vault-api.js"),
|
|
1471
|
+
]);
|
|
1472
|
+
const token = await ensureCognitoToken();
|
|
1473
|
+
// An agent box has no person signed in, and the server does not deliver a
|
|
1474
|
+
// DM an agent sends itself: there is no owner to reach from here.
|
|
1475
|
+
if (agentUidFromToken(token))
|
|
1476
|
+
return null;
|
|
1477
|
+
const personUid = await resolveCallerPersonUid(token, undefined, { command: "hq daemon service sync-core-selfcheck" });
|
|
1478
|
+
const res = await vaultApiFetch({
|
|
1479
|
+
token,
|
|
1480
|
+
path: "/v1/notify/dm",
|
|
1481
|
+
method: "POST",
|
|
1482
|
+
body: { toPersonUid: personUid, body: message.body, details: message.details },
|
|
1483
|
+
});
|
|
1484
|
+
if (!res.ok) {
|
|
1485
|
+
const err = (await res.json().catch(() => ({})));
|
|
1486
|
+
throw new DaemonCommandError(`could not send the self-check DM: HTTP ${res.status} ${err.code ?? ""} ${err.error ?? err.message ?? ""}`.trim());
|
|
1487
|
+
}
|
|
1488
|
+
return personUid;
|
|
1489
|
+
}
|
|
1309
1490
|
export function registerDaemonCommand(program, over = {}) {
|
|
1310
1491
|
let resolved = null;
|
|
1311
1492
|
const deps = () => (resolved ??= loadDefaults(over));
|
|
@@ -1330,6 +1511,10 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
1330
1511
|
.option("--hq-root <path>", "HQ folder the services work against")
|
|
1331
1512
|
.option("--user <login>", "on a box: the login user the daemon runs as (never root)")
|
|
1332
1513
|
.action(wrap((d, opts) => runInstall(d, opts)));
|
|
1514
|
+
daemon
|
|
1515
|
+
.command("start")
|
|
1516
|
+
.description("Load the daemon's OS unit if the service manager does not have it, and start it")
|
|
1517
|
+
.action(wrap((d) => runStart(d)));
|
|
1333
1518
|
daemon
|
|
1334
1519
|
.command("uninstall")
|
|
1335
1520
|
.description("Stop the daemon and remove its OS unit")
|
|
@@ -1601,6 +1786,42 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
1601
1786
|
err: d.err,
|
|
1602
1787
|
});
|
|
1603
1788
|
}));
|
|
1789
|
+
service
|
|
1790
|
+
.command("sync-core-selfcheck")
|
|
1791
|
+
.description("DM the person signed in here when a sync engine unit has been held or failing for over 30 minutes (at most daily per unit set)")
|
|
1792
|
+
.action(wrap(async (d) => {
|
|
1793
|
+
const [{ runSelfCheck }, { readUnitHolds }] = await Promise.all([
|
|
1794
|
+
import("../lib/daemon/selfcheck.js"),
|
|
1795
|
+
import("../lib/daemon/unit-holds.js"),
|
|
1796
|
+
]);
|
|
1797
|
+
const { paths } = currentHost(d);
|
|
1798
|
+
await runSelfCheck({
|
|
1799
|
+
statePath: paths.statePath,
|
|
1800
|
+
holdsPath: paths.holdsPath,
|
|
1801
|
+
recordPath: path.join(paths.root, "selfcheck.json"),
|
|
1802
|
+
now: Date.now,
|
|
1803
|
+
machine: os.hostname(),
|
|
1804
|
+
readState: () => {
|
|
1805
|
+
try {
|
|
1806
|
+
return readState(paths);
|
|
1807
|
+
}
|
|
1808
|
+
catch {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
},
|
|
1812
|
+
readHolds: () => {
|
|
1813
|
+
try {
|
|
1814
|
+
return readUnitHolds(paths.holdsPath);
|
|
1815
|
+
}
|
|
1816
|
+
catch {
|
|
1817
|
+
return {};
|
|
1818
|
+
}
|
|
1819
|
+
},
|
|
1820
|
+
sendToOwner: (message) => sendDmToMachineOwner(message),
|
|
1821
|
+
...(await selfCheckVaults(d.home)),
|
|
1822
|
+
log: (line) => d.out(line),
|
|
1823
|
+
});
|
|
1824
|
+
}));
|
|
1604
1825
|
service
|
|
1605
1826
|
.command("sync-core-manifest")
|
|
1606
1827
|
.description("Upload the sync-audit manifest for every vault the new sync engine owns on this machine")
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Sentry live in exactly one place. `integrations.ts` re-exports this module's
|
|
9
9
|
* public surface, so existing importers keep working unchanged.
|
|
10
10
|
*/
|
|
11
|
+
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
11
12
|
export { bareProvider } from "../lib/integrations/provider-slug.js";
|
|
12
13
|
/** Write policy on a connection — mirrors hq-pro's `WritePolicy`. */
|
|
13
14
|
export type WritePolicy = "auto-allow" | "confirm" | "deny";
|
|
@@ -415,4 +416,6 @@ export declare function selectAccountConnections(connections: readonly AdminConn
|
|
|
415
416
|
account: string;
|
|
416
417
|
provider?: string;
|
|
417
418
|
}): AdminConnection[];
|
|
419
|
+
/** A runner-selected session must never fall back to a cached human identity. */
|
|
420
|
+
export declare function ensureIntegrationIdToken(options?: Parameters<typeof ensureCognitoIdToken>[0]): Promise<string>;
|
|
418
421
|
//# sourceMappingURL=integrations-core.d.ts.map
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* public surface, so existing importers keep working unchanged.
|
|
10
10
|
*/
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { delegatedMachineSessionFilePath, loadDelegatedMachineSession, getValidMachineTokens } from "@indigoai-us/hq-cloud";
|
|
13
|
+
import { DEFAULT_COGNITO, ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
12
14
|
import chalk from "chalk";
|
|
13
15
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
14
16
|
import { AuthError } from "../utils/auth-error.js";
|
|
@@ -991,4 +993,13 @@ export function selectAccountConnections(connections, opts) {
|
|
|
991
993
|
}
|
|
992
994
|
return matches;
|
|
993
995
|
}
|
|
996
|
+
/** A runner-selected session must never fall back to a cached human identity. */
|
|
997
|
+
export async function ensureIntegrationIdToken(options) {
|
|
998
|
+
if (delegatedMachineSessionFilePath() !== null) {
|
|
999
|
+
if (!loadDelegatedMachineSession())
|
|
1000
|
+
throw new Error("HQ_MACHINE_SESSION_FILE is not a usable delegated session; the job runner must rewrite it.");
|
|
1001
|
+
return (await getValidMachineTokens(DEFAULT_COGNITO)).idToken;
|
|
1002
|
+
}
|
|
1003
|
+
return options === undefined ? ensureCognitoIdToken() : ensureCognitoIdToken(options);
|
|
1004
|
+
}
|
|
994
1005
|
//# sourceMappingURL=integrations-core.js.map
|
|
@@ -71,10 +71,10 @@ export function parsePrincipal(raw) {
|
|
|
71
71
|
return { granteeType: "group", granteeId: value };
|
|
72
72
|
// Agents are people to the ACL: they hold an identity and a membership, so a
|
|
73
73
|
// grant to `agt_…` is a person grant, not a separate grantee kind.
|
|
74
|
-
if (/^(prs|psn|agt)_[A-Za-z0-9_-]+$/i.test(value)) {
|
|
74
|
+
if (/^(prs|psn|agt|out)_[A-Za-z0-9_-]+$/i.test(value)) {
|
|
75
75
|
return { granteeType: "person", granteeId: value };
|
|
76
76
|
}
|
|
77
|
-
throw new IntegrationsCliError(`Could not tell what '${raw}' refers to. Use an email address, a uid (prs_… / agt_… / grp_…), \`everyone\`, or an explicit \`person:<id>\` / \`group:<id>\` form.`, { expected: true });
|
|
77
|
+
throw new IntegrationsCliError(`Could not tell what '${raw}' refers to. Use an email address, a uid (prs_… / agt_… / out_… / grp_…), \`everyone\`, or an explicit \`person:<id>\` / \`group:<id>\` form.`, { expected: true });
|
|
78
78
|
}
|
|
79
79
|
/** `--with` / `--to` / `--from` help. Groups resolve membership at call time. */
|
|
80
80
|
export const PRINCIPAL_HELP = "Email, uid, vault group (grp_…), or `everyone`";
|