@indigoai-us/hq-cli 5.345.30 → 5.345.32
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 +6 -0
- package/dist/command-catalog.generated.d.ts +8 -0
- package/dist/command-catalog.generated.js +10 -0
- package/dist/commands/anywhere-runtime-flag.js +1 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +6 -3
- package/dist/commands/install-global.d.ts +3 -1
- package/dist/commands/install-global.js +17 -7
- package/dist/commands/integrations-core.d.ts +40 -0
- package/dist/commands/integrations-core.js +90 -0
- package/dist/commands/integrations.d.ts +2 -2
- package/dist/commands/integrations.js +27 -3
- package/dist/commands/link.d.ts +7 -3
- package/dist/commands/link.js +14 -9
- package/dist/commands/mcp-serve.d.ts +2 -0
- package/dist/commands/mcp-serve.js +5 -3
- package/dist/commands/packs.d.ts +26 -0
- package/dist/commands/packs.js +12 -1
- package/dist/commands/pkg-install.js +10 -9
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +2 -2
- package/dist/lib/anywhere-person-setting.d.ts +17 -0
- package/dist/lib/anywhere-person-setting.js +41 -0
- package/dist/lib/daemon/mcp-oauth.d.ts +1 -1
- package/dist/lib/daemon/mcp-oauth.js +3 -3
- package/dist/lib/daemon/run.js +4 -1
- package/dist/lib/daemon/server.d.ts +2 -0
- package/dist/lib/daemon/server.js +11 -3
- package/dist/lib/doctor/__testing__/run-doctor.js +10 -1
- package/dist/lib/doctor/checks/anywhere.d.ts +2 -0
- package/dist/lib/doctor/checks/anywhere.js +12 -3
- package/dist/utils/resident-package-preload.js +7 -1
- package/docs/anywhere-runtime.md +13 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.32] — 2026-10-06
|
|
6
|
+
- `hq integrations list --usable` lists only the apps you can use right now: connected, shared with you under the same access rule the HQ gateway applies, and backed by a provider the gateway has registered. Each row shows the app name, its website domain, and the flag that selects exactly that connection in `hq integrations tools` and `call` (`--provider <slug>`, or `--connection <id>` when two connections share a provider). `--json` returns `{companyUid, viewerAccessKnown, apps}`. `--no-login` makes `list` fail instead of opening a browser sign-in, for hooks and scripts.
|
|
7
|
+
|
|
8
|
+
## [5.345.31] — 2026-10-06
|
|
9
|
+
- HQ Anywhere commands now require both the operator runtime switch and the signed-in person's `hq-anywhere` setting. `hq doctor` reports the setting state and access stays off when it cannot be read.
|
|
10
|
+
|
|
5
11
|
## [5.345.30] — 2026-10-06
|
|
6
12
|
- Sanitize malformed journal scope identifiers before composing per-scope check IDs.
|
|
7
13
|
|
|
@@ -4054,6 +4054,14 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4054
4054
|
readonly flags: "--json";
|
|
4055
4055
|
readonly description: "Machine-readable output";
|
|
4056
4056
|
readonly hidden: false;
|
|
4057
|
+
}, {
|
|
4058
|
+
readonly flags: "--usable";
|
|
4059
|
+
readonly description: "Only apps you can use right now (connected and shared with you), with the --provider value to call them";
|
|
4060
|
+
readonly hidden: false;
|
|
4061
|
+
}, {
|
|
4062
|
+
readonly flags: "--no-login";
|
|
4063
|
+
readonly description: "Fail instead of opening a browser sign-in when there is no HQ session";
|
|
4064
|
+
readonly hidden: false;
|
|
4057
4065
|
}];
|
|
4058
4066
|
readonly subcommands: readonly [];
|
|
4059
4067
|
}, {
|
|
@@ -4897,6 +4897,16 @@ export const COMMAND_CATALOG = [
|
|
|
4897
4897
|
"flags": "--json",
|
|
4898
4898
|
"description": "Machine-readable output",
|
|
4899
4899
|
"hidden": false
|
|
4900
|
+
},
|
|
4901
|
+
{
|
|
4902
|
+
"flags": "--usable",
|
|
4903
|
+
"description": "Only apps you can use right now (connected and shared with you), with the --provider value to call them",
|
|
4904
|
+
"hidden": false
|
|
4905
|
+
},
|
|
4906
|
+
{
|
|
4907
|
+
"flags": "--no-login",
|
|
4908
|
+
"description": "Fail instead of opening a browser sign-in when there is no HQ session",
|
|
4909
|
+
"hidden": false
|
|
4900
4910
|
}
|
|
4901
4911
|
],
|
|
4902
4912
|
"subcommands": []
|
|
@@ -4,7 +4,7 @@ export { HQ_ANYWHERE_RUNTIME_FLAG };
|
|
|
4
4
|
export class AnywhereRuntimeDisabledError extends Error {
|
|
5
5
|
expected = true;
|
|
6
6
|
constructor() {
|
|
7
|
-
super("HQ Anywhere is not enabled.
|
|
7
|
+
super("HQ Anywhere is not enabled. The hq-anywhere-runtime flag and your HQ Anywhere person setting must both be on.");
|
|
8
8
|
this.name = "AnywhereRuntimeDisabledError";
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -43,6 +43,8 @@ export interface DaemonCommandDeps {
|
|
|
43
43
|
userHome: (user: string) => string;
|
|
44
44
|
/** The pid is a live `hq daemon run`. */
|
|
45
45
|
daemonAlive: (pid: number) => boolean;
|
|
46
|
+
/** Test seam for the combined hq-flags and person-setting gate. */
|
|
47
|
+
runtimeAllowed?: () => boolean | Promise<boolean>;
|
|
46
48
|
/** Test seam for the company sync-mode commands. */
|
|
47
49
|
syncModeClient?: () => Promise<SyncModeVaultClient>;
|
|
48
50
|
/** Reads stdin for the one-shot daemon machine login; never reads env or argv credentials. */
|
package/dist/commands/daemon.js
CHANGED
|
@@ -33,7 +33,7 @@ import { runDaemonCall } from "../lib/daemon/command-call.js";
|
|
|
33
33
|
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, syncRunnerPathFromCommandLine, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
34
34
|
import { stagedSyncRunnerVersion, syncRunnerBundleStageRoot } from "../lib/daemon/sync-runner-bundle.js";
|
|
35
35
|
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
36
|
-
import {
|
|
36
|
+
import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
|
|
37
37
|
import { hqdRequest, hqdSocketPath, HqdClientError } from "../lib/daemon/protocol.js";
|
|
38
38
|
import { parseStdinMachineCredentials } from "../lib/daemon/machine-login.js";
|
|
39
39
|
import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
@@ -130,6 +130,7 @@ async function loadDefaults(over) {
|
|
|
130
130
|
windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
|
|
131
131
|
systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
132
132
|
syncModeClient: over.syncModeClient,
|
|
133
|
+
runtimeAllowed: over.runtimeAllowed,
|
|
133
134
|
systemRecordPath: over.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
|
|
134
135
|
};
|
|
135
136
|
}
|
|
@@ -892,7 +893,8 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
892
893
|
.option("--status", "Show whether hqd has an in-memory machine sign-in")
|
|
893
894
|
.allowUnknownOption(true)
|
|
894
895
|
.action(wrap(async (d, extra, opts) => {
|
|
895
|
-
|
|
896
|
+
if (!(await (d.runtimeAllowed ?? (() => anywhereRuntimeAllowed(d.env)))()))
|
|
897
|
+
throw new DaemonCommandError("HQ Anywhere is not enabled for this person or the hq-anywhere-runtime switch is off.");
|
|
896
898
|
if (extra?.length)
|
|
897
899
|
throw new DaemonCommandError("Machine credentials are accepted from stdin only; do not pass credentials as arguments.");
|
|
898
900
|
const socketPath = hqdSocketPath(d.home, d.env, d.platform);
|
|
@@ -930,7 +932,8 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
930
932
|
.option("--json <args>", "Request args as a JSON object", "{}")
|
|
931
933
|
.option("--timeout <ms>", "Give up after this many milliseconds")
|
|
932
934
|
.action(wrap(async (d, op, opts) => {
|
|
933
|
-
|
|
935
|
+
if (!(await (d.runtimeAllowed ?? (() => anywhereRuntimeAllowed(d.env)))()))
|
|
936
|
+
throw new DaemonCommandError("HQ Anywhere is not enabled for this person or the hq-anywhere-runtime switch is off.");
|
|
934
937
|
if (op === "auth.login") {
|
|
935
938
|
throw new Error("Use `hq daemon login --stdin`; credentials are accepted from stdin only.");
|
|
936
939
|
}
|
|
@@ -73,9 +73,11 @@ export interface GlobalCommandOpts {
|
|
|
73
73
|
/** Dependency injection for tests. */
|
|
74
74
|
env?: NodeJS.ProcessEnv;
|
|
75
75
|
log?: (line: string) => void;
|
|
76
|
+
/** Test seam for the hq-pro person preference read. */
|
|
77
|
+
personSettingReader?: () => Promise<boolean | null>;
|
|
76
78
|
}
|
|
77
79
|
export declare function resolveInstallRoot(explicit: string | undefined): string;
|
|
78
80
|
/** Entry used by `hq install --global` (wired from pkg-install.ts). */
|
|
79
|
-
export declare function runInstallGlobal(opts: GlobalCommandOpts): void
|
|
81
|
+
export declare function runInstallGlobal(opts: GlobalCommandOpts): Promise<void>;
|
|
80
82
|
export declare function registerUninstallCommand(program: Command): void;
|
|
81
83
|
//# sourceMappingURL=install-global.d.ts.map
|
|
@@ -17,6 +17,7 @@ import { resolveHqRoot } from './doctor.js';
|
|
|
17
17
|
import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
|
|
18
18
|
import { resolveHqMcpEndpoint } from '../lib/daemon/mcp-endpoint.js';
|
|
19
19
|
import { chatgptConnectorInstructions } from '../lib/install/chatgpt-targets.js';
|
|
20
|
+
import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
|
|
20
21
|
import { grokConnectorInstructions } from '../lib/install/grok-targets.js';
|
|
21
22
|
import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/registry/registry.js';
|
|
22
23
|
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
|
|
@@ -294,8 +295,6 @@ export function installGlobalClaude(opts) {
|
|
|
294
295
|
const log = opts.log ?? ((l) => console.log(l));
|
|
295
296
|
const hqRoot = opts.hqRoot;
|
|
296
297
|
assertHqRoot(hqRoot);
|
|
297
|
-
if (!isAnywhereEnabled(home, env))
|
|
298
|
-
throw new AnywhereRuntimeDisabledError();
|
|
299
298
|
const t = claudeTargets(home);
|
|
300
299
|
const prior = readState(t);
|
|
301
300
|
const pluginSource = opts.via === 'plugin' ? resolveClaudePluginSource(opts.marketplaceDir, env, opts.marketplaceRepository) : undefined;
|
|
@@ -680,8 +679,6 @@ export function installGlobalCodex(opts) {
|
|
|
680
679
|
expected: true,
|
|
681
680
|
});
|
|
682
681
|
}
|
|
683
|
-
if (!isAnywhereEnabled(home, env))
|
|
684
|
-
throw new AnywhereRuntimeDisabledError();
|
|
685
682
|
const t = codexTargets(home);
|
|
686
683
|
const hqPointer = claudeTargets(home).hqPointer;
|
|
687
684
|
const prior = readCodexState(t);
|
|
@@ -957,10 +954,15 @@ export function resolveInstallRoot(explicit) {
|
|
|
957
954
|
return root;
|
|
958
955
|
}
|
|
959
956
|
/** Entry used by `hq install --global` (wired from pkg-install.ts). */
|
|
960
|
-
export function runInstallGlobal(opts) {
|
|
957
|
+
export async function runInstallGlobal(opts) {
|
|
961
958
|
const runtime = assertRuntime(opts.runtime);
|
|
959
|
+
const env = opts.env ?? process.env;
|
|
962
960
|
if (runtime === 'chatgpt' || runtime === 'grok') {
|
|
963
|
-
|
|
961
|
+
const enabled = await anywhereRuntimeAllowed(env, {
|
|
962
|
+
...(opts.personSettingReader ? { readSetting: opts.personSettingReader } : {}),
|
|
963
|
+
onError: (name) => (opts.log ?? console.log)(`HQ Anywhere person setting unavailable (${name}); access remains off.`),
|
|
964
|
+
});
|
|
965
|
+
if (!enabled)
|
|
964
966
|
throw new AnywhereRuntimeDisabledError();
|
|
965
967
|
const endpoint = resolveHqMcpEndpoint(opts.env ?? process.env).toString();
|
|
966
968
|
const lines = runtime === 'chatgpt' ? chatgptConnectorInstructions(endpoint) : grokConnectorInstructions(endpoint);
|
|
@@ -969,7 +971,15 @@ export function runInstallGlobal(opts) {
|
|
|
969
971
|
return;
|
|
970
972
|
}
|
|
971
973
|
const install = runtime === 'codex' ? installGlobalCodex : installGlobalClaude;
|
|
972
|
-
install({
|
|
974
|
+
install({
|
|
975
|
+
hqRoot: resolveInstallRoot(opts.hqRoot),
|
|
976
|
+
dryRun: opts.dryRun,
|
|
977
|
+
via: opts.via,
|
|
978
|
+
marketplaceDir: opts.marketplaceDir,
|
|
979
|
+
marketplaceRepository: opts.marketplaceRepository,
|
|
980
|
+
env,
|
|
981
|
+
log: opts.log,
|
|
982
|
+
});
|
|
973
983
|
}
|
|
974
984
|
export function registerUninstallCommand(program) {
|
|
975
985
|
program
|
|
@@ -225,6 +225,46 @@ export declare function toolPrefixForProvider(provider: string): string;
|
|
|
225
225
|
*/
|
|
226
226
|
export declare function fetchAdminSurface(token: string, companyUid: string): Promise<AdminSurface>;
|
|
227
227
|
export declare function fetchConnections(token: string, companyUid: string): Promise<AdminConnection[]>;
|
|
228
|
+
/**
|
|
229
|
+
* One app the caller can use right now through the HQ gateway. Field names
|
|
230
|
+
* follow the bot `company_apps` projection (name, provider, domain, status),
|
|
231
|
+
* so session context and bot context describe an app the same way. No
|
|
232
|
+
* endpoint URLs, credentials, scopes, or creator identities.
|
|
233
|
+
*/
|
|
234
|
+
export interface UsableApp {
|
|
235
|
+
connectionId: string;
|
|
236
|
+
/** Display name, stripped of control characters and capped in length. */
|
|
237
|
+
name: string;
|
|
238
|
+
provider: string;
|
|
239
|
+
domain: string | null;
|
|
240
|
+
status: string;
|
|
241
|
+
/** Bare provider slug, e.g. `notion`. */
|
|
242
|
+
providerArg: string;
|
|
243
|
+
/**
|
|
244
|
+
* The flag that resolves to exactly this connection in `hq integrations
|
|
245
|
+
* tools|call`: `--provider <slug>` when no other active row shares the
|
|
246
|
+
* provider, else `--connection <id>`.
|
|
247
|
+
*/
|
|
248
|
+
selector: string;
|
|
249
|
+
}
|
|
250
|
+
export interface UsableAppsResult {
|
|
251
|
+
companyUid: string;
|
|
252
|
+
/**
|
|
253
|
+
* False when the server ignored `view=summary` (an older deploy). Its rows
|
|
254
|
+
* carry no `viewerAccess`, so nothing can be confirmed usable and `apps` is
|
|
255
|
+
* empty rather than guessed.
|
|
256
|
+
*/
|
|
257
|
+
viewerAccessKnown: boolean;
|
|
258
|
+
apps: UsableApp[];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
|
|
262
|
+
* A row counts only when all of these hold, mirroring the gateway's checks:
|
|
263
|
+
* the server's per-caller ACL decision (`viewerAccess.usable`) is true, the
|
|
264
|
+
* status is one the gateway serves, and the gateway has a provider registered
|
|
265
|
+
* for it. Fails closed: a row without `viewerAccess` is not listed.
|
|
266
|
+
*/
|
|
267
|
+
export declare function fetchUsableApps(token: string, companyUid: string): Promise<UsableAppsResult>;
|
|
228
268
|
/**
|
|
229
269
|
* Resolve one connection by `--connection acct_…` or `--provider linear`
|
|
230
270
|
* (matches `factory:<slug>`, bare provider ids, and an installation's human
|
|
@@ -355,6 +355,96 @@ export async function fetchAdminSurface(token, companyUid) {
|
|
|
355
355
|
export async function fetchConnections(token, companyUid) {
|
|
356
356
|
return (await fetchAdminSurface(token, companyUid)).connections;
|
|
357
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Statuses the gateway serves. `needs-attention` is derived by the summary
|
|
360
|
+
* view from a stored `connected` row whose token renewal is failing, so the
|
|
361
|
+
* gateway still dispatches to it.
|
|
362
|
+
*/
|
|
363
|
+
const GATEWAY_SERVED_STATUSES = new Set(["connected", "needs-attention"]);
|
|
364
|
+
/**
|
|
365
|
+
* Providers hq-pro registers statically (`createIntegrationProviderRegistry`).
|
|
366
|
+
* Every other provider is registered per company from a factory installation
|
|
367
|
+
* keyed by its `runtimeProviderId`; a connection with neither is refused by the
|
|
368
|
+
* gateway as `ProviderUnsupported`.
|
|
369
|
+
*/
|
|
370
|
+
const STATIC_GATEWAY_PROVIDERS = new Set(["google-workspace", "linear", "slack"]);
|
|
371
|
+
const MAX_APP_NAME_LENGTH = 60;
|
|
372
|
+
/**
|
|
373
|
+
* Display names are set by whoever installed the app and end up in agent
|
|
374
|
+
* context, so keep them to one short line of printable text.
|
|
375
|
+
*/
|
|
376
|
+
function safeAppName(raw, fallback) {
|
|
377
|
+
const cleaned = (raw ?? "")
|
|
378
|
+
.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, " ")
|
|
379
|
+
.replace(/\s+/g, " ")
|
|
380
|
+
.trim();
|
|
381
|
+
if (!cleaned)
|
|
382
|
+
return fallback;
|
|
383
|
+
const chars = Array.from(cleaned);
|
|
384
|
+
return chars.length > MAX_APP_NAME_LENGTH
|
|
385
|
+
? `${chars.slice(0, MAX_APP_NAME_LENGTH - 1).join("").trimEnd()}…`
|
|
386
|
+
: cleaned;
|
|
387
|
+
}
|
|
388
|
+
/** Case-insensitive code-point order, so the list reads the same in every locale. */
|
|
389
|
+
function compareNames(a, b) {
|
|
390
|
+
const x = a.toLowerCase();
|
|
391
|
+
const y = b.toLowerCase();
|
|
392
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
393
|
+
}
|
|
394
|
+
function gatewayHasProvider(row) {
|
|
395
|
+
if (STATIC_GATEWAY_PROVIDERS.has(row.provider))
|
|
396
|
+
return true;
|
|
397
|
+
return row.installation?.runtimeProviderId === row.provider;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
|
|
401
|
+
* A row counts only when all of these hold, mirroring the gateway's checks:
|
|
402
|
+
* the server's per-caller ACL decision (`viewerAccess.usable`) is true, the
|
|
403
|
+
* status is one the gateway serves, and the gateway has a provider registered
|
|
404
|
+
* for it. Fails closed: a row without `viewerAccess` is not listed.
|
|
405
|
+
*/
|
|
406
|
+
export async function fetchUsableApps(token, companyUid) {
|
|
407
|
+
const res = await vaultApiFetch({
|
|
408
|
+
token,
|
|
409
|
+
path: "/v1/integrations/admin",
|
|
410
|
+
query: { companyUid, view: "summary" },
|
|
411
|
+
});
|
|
412
|
+
if (!res.ok)
|
|
413
|
+
await raiseForResponse(res, "Failed to list integrations");
|
|
414
|
+
const data = (await res.json());
|
|
415
|
+
const viewerAccessKnown = data.view === "summary";
|
|
416
|
+
const rows = Array.isArray(data.connections) ? data.connections : [];
|
|
417
|
+
// `--provider` picks the first non-revoked row with that provider, whoever
|
|
418
|
+
// may use it, so a shared provider needs `--connection` to be exact.
|
|
419
|
+
const activeByProvider = new Map();
|
|
420
|
+
for (const row of rows) {
|
|
421
|
+
if (row.status === "revoked")
|
|
422
|
+
continue;
|
|
423
|
+
const key = bareProvider(row.provider).toLowerCase();
|
|
424
|
+
activeByProvider.set(key, (activeByProvider.get(key) ?? 0) + 1);
|
|
425
|
+
}
|
|
426
|
+
const apps = viewerAccessKnown
|
|
427
|
+
? rows
|
|
428
|
+
.filter((row) => row.viewerAccess?.usable === true &&
|
|
429
|
+
GATEWAY_SERVED_STATUSES.has(row.status) &&
|
|
430
|
+
gatewayHasProvider(row))
|
|
431
|
+
.map((row) => {
|
|
432
|
+
const providerArg = bareProvider(row.provider);
|
|
433
|
+
const shared = (activeByProvider.get(providerArg.toLowerCase()) ?? 0) > 1;
|
|
434
|
+
return {
|
|
435
|
+
connectionId: row.id,
|
|
436
|
+
name: safeAppName(row.installation?.displayName, providerArg),
|
|
437
|
+
provider: row.provider,
|
|
438
|
+
domain: typeof row.domain === "string" && row.domain ? row.domain : null,
|
|
439
|
+
status: row.status,
|
|
440
|
+
providerArg,
|
|
441
|
+
selector: shared ? `--connection ${row.id}` : `--provider ${providerArg}`,
|
|
442
|
+
};
|
|
443
|
+
})
|
|
444
|
+
.sort((a, b) => compareNames(a.name, b.name))
|
|
445
|
+
: [];
|
|
446
|
+
return { companyUid: data.companyUid ?? companyUid, viewerAccessKnown, apps };
|
|
447
|
+
}
|
|
358
448
|
/**
|
|
359
449
|
* Resolve one connection by `--connection acct_…` or `--provider linear`
|
|
360
450
|
* (matches `factory:<slug>`, bare provider ids, and an installation's human
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
*/
|
|
52
52
|
import { Command } from "commander";
|
|
53
53
|
import { type PendingApproval } from "./integrations-api.js";
|
|
54
|
-
export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
55
|
-
export type { AdminConnection } from "./integrations-core.js";
|
|
54
|
+
export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
55
|
+
export type { AdminConnection, UsableApp, UsableAppsResult } from "./integrations-core.js";
|
|
56
56
|
/** Wait for a decided queue item without resending the approve request. */
|
|
57
57
|
export declare function waitForApprovalTerminal(token: string, companyUid: string, queueId: string, options?: {
|
|
58
58
|
timeoutMs?: number;
|
|
@@ -54,12 +54,12 @@ import chalk from "chalk";
|
|
|
54
54
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
55
55
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
56
56
|
import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
|
|
57
|
-
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
57
|
+
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, fetchUsableApps, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
58
58
|
import { registerConnectCommands } from "./integrations-connect.js";
|
|
59
59
|
import { registerImportCommands } from "./integrations-import.js";
|
|
60
60
|
import { registerManageCommands } from "./integrations-manage.js";
|
|
61
61
|
import { fetchPendingApprovals } from "./integrations-api.js";
|
|
62
|
-
export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
62
|
+
export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
63
63
|
/**
|
|
64
64
|
* Gateway metadata is additive and may be absent when talking to an older
|
|
65
65
|
* gateway. Parse it defensively so an unknown or malformed value never breaks
|
|
@@ -220,9 +220,33 @@ export function registerIntegrationsCommand(program) {
|
|
|
220
220
|
.description("List the company's connected apps")
|
|
221
221
|
.option("--company <slug>", "Company slug, e.g. indigo (defaults to your single active company)")
|
|
222
222
|
.option("--json", "Machine-readable output")
|
|
223
|
+
.option("--usable", "Only apps you can use right now (connected and shared with you), with the --provider value to call them")
|
|
224
|
+
.option("--no-login", "Fail instead of opening a browser sign-in when there is no HQ session")
|
|
223
225
|
.action(async (opts) => {
|
|
224
|
-
const token = await ensureCognitoIdToken();
|
|
226
|
+
const token = await ensureCognitoIdToken(opts.login === false ? { interactive: false } : {});
|
|
225
227
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
228
|
+
if (opts.usable) {
|
|
229
|
+
const result = await fetchUsableApps(token, companyUid);
|
|
230
|
+
if (opts.json) {
|
|
231
|
+
printJson(result);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!result.viewerAccessKnown) {
|
|
235
|
+
console.log("This HQ server cannot report which apps are shared with you yet. Run `hq integrations list` to see every connected app.");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (result.apps.length === 0) {
|
|
239
|
+
console.log("No connected apps are available to you in this company. Run `hq integrations list` to see what is connected, or ask an admin to share one.");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
for (const app of result.apps) {
|
|
243
|
+
const where = app.domain ? ` (${app.domain})` : "";
|
|
244
|
+
const flag = app.status === "needs-attention" ? chalk.yellow(" needs attention") : "";
|
|
245
|
+
console.log(`${chalk.bold(app.name)}${chalk.dim(where)}${flag}`);
|
|
246
|
+
console.log(chalk.dim(` hq integrations tools ${app.selector}`));
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
226
250
|
const connections = await fetchConnections(token, companyUid);
|
|
227
251
|
if (opts.json) {
|
|
228
252
|
printJson(connections);
|
package/dist/commands/link.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface LinkDeps {
|
|
|
15
15
|
listMemberships?: () => Promise<MembershipCompany[]>;
|
|
16
16
|
now?: () => Date;
|
|
17
17
|
}
|
|
18
|
+
export interface LinkCommandDeps {
|
|
19
|
+
/** Test seam for the combined hq-flags and person-setting gate. */
|
|
20
|
+
runtimeAllowed?: () => boolean | Promise<boolean>;
|
|
21
|
+
}
|
|
18
22
|
/** Link `dir` to `company` after confirming the caller is an active member. */
|
|
19
23
|
export declare function linkFolder(company: string, dir: string | undefined, deps?: LinkDeps): Promise<{
|
|
20
24
|
key: string;
|
|
@@ -24,7 +28,7 @@ export declare function unlinkFolder(dir: string | undefined, deps?: LinkDeps):
|
|
|
24
28
|
export declare function resolveCompany(dir: string | undefined, deps?: LinkDeps): import("../lib/registry/registry.js").ResolveResult;
|
|
25
29
|
/** Seed manifest-sourced entries; called by `hq install --global` (US-005). */
|
|
26
30
|
export declare function seedRegistryFromHqRoot(hqRoot: string, deps?: LinkDeps): import("../lib/registry/registry.js").SeedResult;
|
|
27
|
-
export declare function registerLinkCommand(program: Command): void;
|
|
28
|
-
export declare function registerUnlinkCommand(program: Command): void;
|
|
29
|
-
export declare function registerResolveCompanyCommand(program: Command): void;
|
|
31
|
+
export declare function registerLinkCommand(program: Command, deps?: LinkCommandDeps): void;
|
|
32
|
+
export declare function registerUnlinkCommand(program: Command, deps?: LinkCommandDeps): void;
|
|
33
|
+
export declare function registerResolveCompanyCommand(program: Command, deps?: LinkCommandDeps): void;
|
|
30
34
|
//# sourceMappingURL=link.d.ts.map
|
package/dist/commands/link.js
CHANGED
|
@@ -9,9 +9,14 @@ import { Option } from "commander";
|
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
11
11
|
import { listActiveMembershipCompanies } from "../lib/mesh/api.js";
|
|
12
|
-
import {
|
|
12
|
+
import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
|
|
13
13
|
import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
|
|
14
14
|
import { canonicalDir, keyForDir, readRegistry, registryDir, registryFile, removeEntriesForDir, resolveCompanyForDir, seedFromManifest, setEntry, writeRegistry, } from "../lib/registry/registry.js";
|
|
15
|
+
async function requireAnywhereRuntime(deps) {
|
|
16
|
+
if (!(await (deps.runtimeAllowed ?? (() => anywhereRuntimeAllowed()))())) {
|
|
17
|
+
throw new Error("HQ Anywhere is disabled until both the runtime flag and your person setting are on.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
15
20
|
async function defaultListMemberships() {
|
|
16
21
|
const token = await ensureCognitoToken({ interactive: false });
|
|
17
22
|
return listActiveMembershipCompanies(token);
|
|
@@ -90,7 +95,7 @@ function fail(err) {
|
|
|
90
95
|
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
91
96
|
process.exitCode = 1;
|
|
92
97
|
}
|
|
93
|
-
export function registerLinkCommand(program) {
|
|
98
|
+
export function registerLinkCommand(program, deps = {}) {
|
|
94
99
|
program
|
|
95
100
|
.command("link [company]")
|
|
96
101
|
.description("Link this folder (or its git remote) to an HQ company")
|
|
@@ -100,7 +105,7 @@ export function registerLinkCommand(program) {
|
|
|
100
105
|
.addOption(new Option("--seed-manifest <hqRoot>", "Seed entries from an HQ companies/manifest.yaml").hideHelp())
|
|
101
106
|
.action(async (company, opts) => {
|
|
102
107
|
try {
|
|
103
|
-
requireAnywhereRuntime();
|
|
108
|
+
await requireAnywhereRuntime(deps);
|
|
104
109
|
if (opts.list) {
|
|
105
110
|
const registry = readRegistry();
|
|
106
111
|
if (opts.json)
|
|
@@ -132,14 +137,14 @@ export function registerLinkCommand(program) {
|
|
|
132
137
|
}
|
|
133
138
|
});
|
|
134
139
|
}
|
|
135
|
-
export function registerUnlinkCommand(program) {
|
|
140
|
+
export function registerUnlinkCommand(program, deps = {}) {
|
|
136
141
|
program
|
|
137
142
|
.command("unlink")
|
|
138
143
|
.description("Remove the HQ company link for this folder")
|
|
139
144
|
.option("--path <dir>", "Folder to unlink (default: current directory)")
|
|
140
|
-
.action((opts) => {
|
|
145
|
+
.action(async (opts) => {
|
|
141
146
|
try {
|
|
142
|
-
requireAnywhereRuntime();
|
|
147
|
+
await requireAnywhereRuntime(deps);
|
|
143
148
|
const removed = unlinkFolder(opts.path);
|
|
144
149
|
if (removed.length === 0)
|
|
145
150
|
console.log(chalk.dim("Nothing linked here."));
|
|
@@ -152,15 +157,15 @@ export function registerUnlinkCommand(program) {
|
|
|
152
157
|
}
|
|
153
158
|
});
|
|
154
159
|
}
|
|
155
|
-
export function registerResolveCompanyCommand(program) {
|
|
160
|
+
export function registerResolveCompanyCommand(program, deps = {}) {
|
|
156
161
|
program
|
|
157
162
|
.command("resolve-company")
|
|
158
163
|
.description("Print the HQ company linked to this folder (never guesses)")
|
|
159
164
|
.option("--path <dir>", "Folder to resolve (default: current directory)")
|
|
160
165
|
.option("--json", "Print JSON output")
|
|
161
|
-
.action((opts) => {
|
|
166
|
+
.action(async (opts) => {
|
|
162
167
|
try {
|
|
163
|
-
requireAnywhereRuntime();
|
|
168
|
+
await requireAnywhereRuntime(deps);
|
|
164
169
|
}
|
|
165
170
|
catch (err) {
|
|
166
171
|
fail(err);
|
|
@@ -24,6 +24,8 @@ export interface McpServeOptions {
|
|
|
24
24
|
preloadResidentModules?: (residentPath: "mcp", modules: readonly string[]) => Promise<void>;
|
|
25
25
|
/** Test seam for releasing the process package lease after resident preload. */
|
|
26
26
|
releasePackageUseLease?: () => void;
|
|
27
|
+
/** Test seam for combined hq-flags and person-setting authorization. */
|
|
28
|
+
runtimeAllowed?: () => boolean | Promise<boolean>;
|
|
27
29
|
}
|
|
28
30
|
/** HQ root for prompts: HQ_ROOT, else the ~/.hq/root pointer written by `hq install --global`. */
|
|
29
31
|
export declare function readHqRoot(env: NodeJS.ProcessEnv, home: string): string | null;
|
|
@@ -16,7 +16,7 @@ import * as os from "node:os";
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { CLI_VERSION } from "../cli-version.js";
|
|
18
18
|
import { HQ_ANYWHERE_RUNTIME_FLAG } from "../lib/anywhere-runtime-flag-key.js";
|
|
19
|
-
import {
|
|
19
|
+
import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
|
|
20
20
|
import { createLineSplitter, HqdClientError, hqdRequest, hqdSocketPath } from "../lib/daemon/protocol.js";
|
|
21
21
|
import { releaseCurrentPackageUseLease, releasePackageUseLeaseAfterResidentPreload } from "../utils/package-use-lease.js";
|
|
22
22
|
import { preloadResidentPackageModules, reportResidentPackagePreloadFailure, RESIDENT_PACKAGE_MODULES } from "../utils/resident-package-preload.js";
|
|
@@ -54,8 +54,10 @@ export async function runMcpServe(opts = {}) {
|
|
|
54
54
|
const cwd = opts.cwd ?? process.cwd();
|
|
55
55
|
const socketPath = hqdSocketPath(home, env);
|
|
56
56
|
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
57
|
-
if (!
|
|
58
|
-
process.stderr.write(`HQ Anywhere
|
|
57
|
+
if (!(await (opts.runtimeAllowed ?? (() => anywhereRuntimeAllowed(env, {
|
|
58
|
+
onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
|
|
59
|
+
})))())) {
|
|
60
|
+
process.stderr.write(`HQ Anywhere is not enabled. The ${HQ_ANYWHERE_RUNTIME_FLAG} flag and your HQ Anywhere person setting must both be on.\n`);
|
|
59
61
|
process.exitCode = 1;
|
|
60
62
|
return;
|
|
61
63
|
}
|
package/dist/commands/packs.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ import { Command } from 'commander';
|
|
|
20
20
|
import { type LatestResult } from './pack-install.js';
|
|
21
21
|
import { type InstalledPack, type LinkStatus } from '../utils/pack-contributions.js';
|
|
22
22
|
import type { PackContributeKey } from '../types.js';
|
|
23
|
+
interface CommonOpts {
|
|
24
|
+
json?: boolean;
|
|
25
|
+
hqRoot?: string;
|
|
26
|
+
}
|
|
23
27
|
interface InstalledPackView {
|
|
24
28
|
name: string;
|
|
25
29
|
version?: string;
|
|
@@ -49,6 +53,28 @@ interface InstalledPackView {
|
|
|
49
53
|
error?: string;
|
|
50
54
|
}
|
|
51
55
|
export declare function buildInstalledView(hqRoot: string, hqVersion: string | null, pack: InstalledPack, installedSources: Set<string>, checkUpdates: boolean, latestProbe?: LatestResult): InstalledPackView;
|
|
56
|
+
interface UpdateCheck {
|
|
57
|
+
name: string;
|
|
58
|
+
transport: string;
|
|
59
|
+
current?: string;
|
|
60
|
+
latest?: string;
|
|
61
|
+
updateAvailable: boolean | null;
|
|
62
|
+
applied: boolean;
|
|
63
|
+
reason?: string;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
66
|
+
interface UpdateOpts extends CommonOpts {
|
|
67
|
+
checkOnly?: boolean;
|
|
68
|
+
refresh?: boolean;
|
|
69
|
+
yes?: boolean;
|
|
70
|
+
allowHooks?: boolean;
|
|
71
|
+
allowMcp?: boolean;
|
|
72
|
+
branch?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface UpdateDependencies {
|
|
75
|
+
runtimeAllowed?: () => Promise<boolean>;
|
|
76
|
+
}
|
|
77
|
+
export declare function runUpdate(name: string | undefined, opts: UpdateOpts, deps?: UpdateDependencies): Promise<UpdateCheck[]>;
|
|
52
78
|
export declare function registerPacksListCommand(parent: Command): void;
|
|
53
79
|
export declare function registerPacksCommand(parent: Command): void;
|
|
54
80
|
export {};
|
package/dist/commands/packs.js
CHANGED
|
@@ -26,6 +26,7 @@ import chalk from 'chalk';
|
|
|
26
26
|
import semverSatisfies from 'semver/functions/satisfies.js';
|
|
27
27
|
import { findHqRoot } from '../utils/manifest.js';
|
|
28
28
|
import { resolveHqRoot as resolveDetectedHqRoot } from './doctor.js';
|
|
29
|
+
import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
|
|
29
30
|
import { classify, PINNED_HQ_ANYWHERE_SOURCE, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
|
|
30
31
|
import { contributionLinks, linkStatus, listInstalledPacks, findDependentPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
|
|
31
32
|
import { syncCodexPackHooks, syncCodexPackSkills } from '../lib/install/codex-pack-contributions.js';
|
|
@@ -207,7 +208,7 @@ function printListHuman(view) {
|
|
|
207
208
|
console.log(chalk.dim('\n Install with: hq install <source>\n'));
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
|
-
async function runUpdate(name, opts) {
|
|
211
|
+
export async function runUpdate(name, opts, deps = {}) {
|
|
211
212
|
const hqRoot = resolveRoot(opts);
|
|
212
213
|
let packs = listInstalledPacks(hqRoot).filter((p) => p.manifest);
|
|
213
214
|
if (name) {
|
|
@@ -225,6 +226,16 @@ async function runUpdate(name, opts) {
|
|
|
225
226
|
results.push({ name: pname, transport: 'unknown', updateAvailable: null, applied: false, error: 'no stamped source -- cannot update' });
|
|
226
227
|
continue;
|
|
227
228
|
}
|
|
229
|
+
if (pname === 'hq-anywhere' && !(await (deps.runtimeAllowed ?? (() => anywhereRuntimeAllowed(process.env)))())) {
|
|
230
|
+
results.push({
|
|
231
|
+
name: pname,
|
|
232
|
+
transport: safeClassify(source) ?? 'unknown',
|
|
233
|
+
updateAvailable: null,
|
|
234
|
+
applied: false,
|
|
235
|
+
error: 'Updating hq-anywhere requires both the hq-anywhere-runtime flag and your person setting.',
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
228
239
|
// US-006: marketplace sources need the async listings probe (network) —
|
|
229
240
|
// the sync `resolveLatest` cannot make that call and returns null. Every
|
|
230
241
|
// other transport keeps the existing synchronous probe unchanged.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { peekHqApiKey, requireApiKeyCapability, } from '../utils/resolve-vault-credential.js';
|
|
3
3
|
import { MARKETPLACE_PREFIX, PINNED_HQ_ANYWHERE_SOURCE, installPack, sourceMatchesPackPattern, } from './pack-install.js';
|
|
4
|
-
import {
|
|
4
|
+
import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
|
|
5
5
|
/**
|
|
6
6
|
* True when `source` installs through the marketplace — an explicit
|
|
7
7
|
* `marketplace:` source, or a bare slug (which the dispatch below rewrites into
|
|
@@ -68,15 +68,16 @@ export function registerPackageInstallCommand(parent) {
|
|
|
68
68
|
process.exitCode = 1;
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
const marketplaceDir = opts.marketplaceDir;
|
|
72
72
|
if (opts.via === 'plugin' &&
|
|
73
73
|
!marketplaceDir?.trim() &&
|
|
74
|
-
!process.env.HQ_CLAUDE_PLUGIN_MARKETPLACE?.trim()
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
!process.env.HQ_CLAUDE_PLUGIN_MARKETPLACE?.trim()) {
|
|
75
|
+
if (await anywhereRuntimeAllowed(process.env)) {
|
|
76
|
+
await runInstallGlobal({ ...opts, marketplaceRepository: 'indigoai-us/hq-packages' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
78
79
|
}
|
|
79
|
-
runInstallGlobal({ ...opts, marketplaceDir });
|
|
80
|
+
await runInstallGlobal({ ...opts, marketplaceDir });
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
83
|
if (!source) {
|
|
@@ -95,8 +96,8 @@ export function registerPackageInstallCommand(parent) {
|
|
|
95
96
|
requireApiKeyCapability('packs:install', 'hq install');
|
|
96
97
|
assertApiKeyCanAuthorizeInstall(source);
|
|
97
98
|
if (source === '@indigoai-us/hq-anywhere') {
|
|
98
|
-
if (!
|
|
99
|
-
console.error(chalk.red('Install failed:'), 'HQ Anywhere installation
|
|
99
|
+
if (!(await anywhereRuntimeAllowed(process.env))) {
|
|
100
|
+
console.error(chalk.red('Install failed:'), 'HQ Anywhere installation requires both the hq-anywhere-runtime flag and your person setting.');
|
|
100
101
|
process.exit(1);
|
|
101
102
|
}
|
|
102
103
|
await installPack(PINNED_HQ_ANYWHERE_SOURCE, {
|
|
@@ -15,6 +15,7 @@ export interface RestartAndVerifyDeps extends ConvergenceDeps {
|
|
|
15
15
|
env?: NodeJS.ProcessEnv;
|
|
16
16
|
installedVersion?: () => string | null;
|
|
17
17
|
log?: (line: string) => void;
|
|
18
|
+
runtimeAllowed?: () => boolean | Promise<boolean>;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Restart the daemon after an update and verify it converged on the installed
|
package/dist/commands/update.js
CHANGED
|
@@ -10,14 +10,14 @@ import { daemonPaths } from "../lib/daemon/paths.js";
|
|
|
10
10
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
11
11
|
import { hqdSocketPath } from "../lib/daemon/protocol.js";
|
|
12
12
|
import { describeNonConvergence, installedCliVersion, pingHqdVersion, verifyDaemonConvergence, } from "../lib/daemon/convergence.js";
|
|
13
|
-
import {
|
|
13
|
+
import { anywhereRuntimeAllowed } from "../lib/anywhere-person-setting.js";
|
|
14
14
|
/**
|
|
15
15
|
* Restart the daemon after an update and verify it converged on the installed
|
|
16
16
|
* hq-cli (policy hq-self-updater-must-verify-convergence-after-install).
|
|
17
17
|
* Returns false when it did not; the caller must report that as a failure.
|
|
18
18
|
*/
|
|
19
19
|
export async function restartAndVerifyDaemon(hqRoot, deps = {}) {
|
|
20
|
-
if (!
|
|
20
|
+
if (!(await (deps.runtimeAllowed ?? (() => anywhereRuntimeAllowed(deps.env ?? process.env)))()))
|
|
21
21
|
return true;
|
|
22
22
|
const log = deps.log ?? ((line) => console.log(line));
|
|
23
23
|
const env = deps.env ?? process.env;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ReadHqAnywherePersonSettingOptions {
|
|
2
|
+
readSetting?: () => Promise<boolean | null>;
|
|
3
|
+
getPersonToken?: () => Promise<string>;
|
|
4
|
+
fetchImpl?: typeof fetch;
|
|
5
|
+
apiBaseUrl?: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
onError?: (errorName: string) => void;
|
|
8
|
+
}
|
|
9
|
+
/** Returns null when the person preference cannot be read; callers must fail closed. */
|
|
10
|
+
export declare function readHqAnywherePersonSetting(options?: ReadHqAnywherePersonSettingOptions): Promise<boolean | null>;
|
|
11
|
+
export declare function evaluateAnywhereRuntime(flagEnabled: boolean, setting: boolean | null): {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
setting: boolean | null;
|
|
14
|
+
};
|
|
15
|
+
/** The operator flag always wins; the person preference is fetched only while it is on. */
|
|
16
|
+
export declare function anywhereRuntimeAllowed(env?: NodeJS.ProcessEnv, options?: ReadHqAnywherePersonSettingOptions): Promise<boolean>;
|
|
17
|
+
//# sourceMappingURL=anywhere-person-setting.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { anywhereRuntimeEnabledFromSnapshot } from "./anywhere-runtime-fast-flag.js";
|
|
2
|
+
/** Returns null when the person preference cannot be read; callers must fail closed. */
|
|
3
|
+
export async function readHqAnywherePersonSetting(options = {}) {
|
|
4
|
+
const env = options.env ?? process.env;
|
|
5
|
+
try {
|
|
6
|
+
const getPersonToken = options.getPersonToken ?? (async () => {
|
|
7
|
+
const { ensureCognitoToken } = await import("../utils/cognito-session.js");
|
|
8
|
+
return ensureCognitoToken({ interactive: false, tokenSource: "person" });
|
|
9
|
+
});
|
|
10
|
+
const token = await getPersonToken();
|
|
11
|
+
const apiBaseUrl = options.apiBaseUrl ?? env.HQ_PRO_API_URL ?? env.HQ_API_URL ?? env.HQ_VAULT_API_URL ?? "https://hqapi.hq.computer";
|
|
12
|
+
const response = await (options.fetchImpl ?? fetch)(`${apiBaseUrl.replace(/\/$/, "")}/v1/bot/auto-schedule?setting=hq-anywhere`, { method: "GET", headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5_000) });
|
|
13
|
+
if (!response.ok)
|
|
14
|
+
throw new Error(`Http${response.status}`);
|
|
15
|
+
const body = await response.json();
|
|
16
|
+
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
17
|
+
throw new Error("InvalidEnvelope");
|
|
18
|
+
const envelope = body;
|
|
19
|
+
if (envelope.key !== "hq-anywhere" || typeof envelope.value !== "boolean")
|
|
20
|
+
throw new Error("InvalidEnvelope");
|
|
21
|
+
return envelope.value;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const errorName = error instanceof Error && /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(error.name)
|
|
25
|
+
? error.name
|
|
26
|
+
: "UnknownError";
|
|
27
|
+
(options.onError ?? ((name) => console.error(`HQ Anywhere person setting unavailable (${name}); access remains off.`)))(errorName);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function evaluateAnywhereRuntime(flagEnabled, setting) {
|
|
32
|
+
return { enabled: flagEnabled && setting === true, setting };
|
|
33
|
+
}
|
|
34
|
+
/** The operator flag always wins; the person preference is fetched only while it is on. */
|
|
35
|
+
export async function anywhereRuntimeAllowed(env = process.env, options = {}) {
|
|
36
|
+
if (!anywhereRuntimeEnabledFromSnapshot(env))
|
|
37
|
+
return false;
|
|
38
|
+
const setting = options.readSetting ? await options.readSetting() : await readHqAnywherePersonSetting({ ...options, env });
|
|
39
|
+
return evaluateAnywhereRuntime(true, setting).enabled;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=anywhere-person-setting.js.map
|
|
@@ -9,7 +9,7 @@ export interface McpOAuthOptions {
|
|
|
9
9
|
fetchImpl?: typeof fetch;
|
|
10
10
|
openBrowser?: (url: string) => Promise<unknown>;
|
|
11
11
|
writeOutput?: (message: string) => void;
|
|
12
|
-
isEnabled?: () => boolean
|
|
12
|
+
isEnabled?: () => boolean | Promise<boolean>;
|
|
13
13
|
isMachine?: () => boolean;
|
|
14
14
|
now?: () => number;
|
|
15
15
|
callbackTimeoutMs?: number;
|
|
@@ -6,7 +6,7 @@ import * as path from "node:path";
|
|
|
6
6
|
import open from "open";
|
|
7
7
|
import { resolveCognitoTokenSource } from "../../utils/cognito-session.js";
|
|
8
8
|
import { markExpectedUserError } from "../../utils/expected-cli-error.js";
|
|
9
|
-
import {
|
|
9
|
+
import { anywhereRuntimeAllowed } from "../anywhere-person-setting.js";
|
|
10
10
|
import { resolveHqMcpOAuthOrigin } from "./mcp-endpoint.js";
|
|
11
11
|
const MCP_SCOPE = "openid email profile hq-mcp/use";
|
|
12
12
|
const EXPIRY_SKEW_MS = 60_000;
|
|
@@ -128,8 +128,8 @@ function newPkce() {
|
|
|
128
128
|
}
|
|
129
129
|
async function runLogin(options) {
|
|
130
130
|
const env = options.env ?? process.env;
|
|
131
|
-
if (!(options.isEnabled ?? (() =>
|
|
132
|
-
throw markExpectedUserError(new McpOAuthError("HQ Anywhere is not enabled.
|
|
131
|
+
if (!(await (options.isEnabled ?? (() => anywhereRuntimeAllowed(env)))())) {
|
|
132
|
+
throw markExpectedUserError(new McpOAuthError("HQ Anywhere is not enabled. The hq-anywhere-runtime flag and your HQ Anywhere person setting must both be on to use `hq mcp login`."));
|
|
133
133
|
}
|
|
134
134
|
if ((options.isMachine ?? (() => resolveCognitoTokenSource(env) === "machine"))()) {
|
|
135
135
|
throw new McpOAuthError("`hq mcp login` is for person accounts; this box uses machine credentials.");
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
15
15
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
16
16
|
import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
|
|
17
|
+
import { anywhereRuntimeAllowed } from "../anywhere-person-setting.js";
|
|
17
18
|
import { currentPackageUseLeaseInstall, currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
|
|
18
19
|
import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
|
|
19
20
|
import { agentKitPaths } from "../agent-kit/paths.js";
|
|
@@ -836,7 +837,9 @@ export async function runDaemon(opts) {
|
|
|
836
837
|
// hqd: the session/company socket every runtime talks to (hq-anywhere US-008).
|
|
837
838
|
let hqd = null;
|
|
838
839
|
let hqdShutdown = null;
|
|
839
|
-
if (shouldStartHqd(
|
|
840
|
+
if (shouldStartHqd(await anywhereRuntimeAllowed(env, {
|
|
841
|
+
onError: (name) => log("warn", `hqd person setting read failed (${name}); runtime remains off`),
|
|
842
|
+
}))) {
|
|
840
843
|
try {
|
|
841
844
|
const startConfig = readDaemonConfig(paths.configPath);
|
|
842
845
|
hqd = await startHqdServer({
|
|
@@ -43,6 +43,8 @@ export interface HqdServerOptions {
|
|
|
43
43
|
mcpEnabled?: () => boolean | Promise<boolean>;
|
|
44
44
|
/** Test seam for the live hq-flags read used by the daemon's bounded runtime monitor. */
|
|
45
45
|
runtimeFlagReader?: (options: ReadDaemonFlagOptions) => Promise<boolean>;
|
|
46
|
+
/** Test seam for the human opt-in setting; production reads hq-pro. */
|
|
47
|
+
personSettingReader?: () => Promise<boolean | null>;
|
|
46
48
|
/** Test seam for the hosted Streamable HTTP MCP transport. */
|
|
47
49
|
mcpBridge?: HqMcpBridge;
|
|
48
50
|
/** Test seam for machine login while keeping production credentials off disk. */
|
|
@@ -23,6 +23,7 @@ import { defaultMcpToken, HqMcpBridge, jsonResult, McpOpError, runWorker } from
|
|
|
23
23
|
import { authenticateMachineCredentials, machineTokenExpiresAt, MachineLoginError, parseStdinMachineCredentials } from "./machine-login.js";
|
|
24
24
|
import { HQ_MCP_TOOL_NAMES, HQD_NATIVE_TOOL_NAMES } from "../mcp/tools.js";
|
|
25
25
|
import { HQ_ANYWHERE_RUNTIME_FLAG } from "../../commands/anywhere-runtime-flag.js";
|
|
26
|
+
import { readHqAnywherePersonSetting } from "../anywhere-person-setting.js";
|
|
26
27
|
import { createBoundedDaemonFlagMonitor } from "./flag-monitor.js";
|
|
27
28
|
import { readDaemonFlag } from "./flag.js";
|
|
28
29
|
import { startWindowsPipeHost, validateWindowsPipeAcl, WINDOWS_PIPE_STARTUP_REFUSAL } from "./windows-pipe-host.js";
|
|
@@ -396,16 +397,23 @@ export async function startHqdServer(opts) {
|
|
|
396
397
|
}
|
|
397
398
|
}
|
|
398
399
|
const runtimeFlagMonitor = opts.mcpEnabled ? undefined : createBoundedDaemonFlagMonitor({
|
|
399
|
-
read: () => {
|
|
400
|
+
read: async () => {
|
|
400
401
|
const options = {
|
|
401
402
|
env,
|
|
402
403
|
throwOnError: true,
|
|
403
404
|
// A local opt-out is honored, while a local opt-in cannot override the operator kill switch.
|
|
404
405
|
localOverrideMode: "off-only",
|
|
405
406
|
};
|
|
406
|
-
|
|
407
|
+
const flagEnabled = await (opts.runtimeFlagReader
|
|
407
408
|
? opts.runtimeFlagReader(options)
|
|
408
|
-
: readDaemonFlag(HQ_ANYWHERE_RUNTIME_FLAG, options);
|
|
409
|
+
: readDaemonFlag(HQ_ANYWHERE_RUNTIME_FLAG, options));
|
|
410
|
+
if (!flagEnabled)
|
|
411
|
+
return false;
|
|
412
|
+
const personSetting = await (opts.personSettingReader ?? (() => readHqAnywherePersonSetting({
|
|
413
|
+
env,
|
|
414
|
+
onError: (name) => log("warn", `hqd person setting refresh failed (${name}); keeping HQ Anywhere off`),
|
|
415
|
+
})))();
|
|
416
|
+
return personSetting === true;
|
|
409
417
|
},
|
|
410
418
|
onChange: (enabled) => {
|
|
411
419
|
if (!enabled) {
|
|
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { runDoctor as runDoctorCommand, } from "../../../commands/doctor.js";
|
|
5
|
+
import { createAnywhereFamily } from "../checks/anywhere.js";
|
|
6
|
+
import { createDefaultRegistry } from "../registry.js";
|
|
5
7
|
/**
|
|
6
8
|
* Run doctor tests against synthetic state. The production registry normally
|
|
7
9
|
* reads local HQ work-context, sync, daemon, and session state; tests must not
|
|
@@ -37,7 +39,14 @@ export async function runDoctorForTest(options = {}) {
|
|
|
37
39
|
else
|
|
38
40
|
process.env[key] = value;
|
|
39
41
|
}
|
|
40
|
-
|
|
42
|
+
let registry = options.registry;
|
|
43
|
+
if (!registry) {
|
|
44
|
+
registry = createDefaultRegistry();
|
|
45
|
+
// Command tests exercise doctor rendering and exit behavior offline. The
|
|
46
|
+
// real person preference is covered in the anywhere-family tests.
|
|
47
|
+
registry.register(createAnywhereFamily({ personSettingReader: async () => false }));
|
|
48
|
+
}
|
|
49
|
+
return await runDoctorCommand({ ...options, registry, integrations: false });
|
|
41
50
|
}
|
|
42
51
|
finally {
|
|
43
52
|
for (const [key, value] of originalEnv) {
|
|
@@ -31,6 +31,8 @@ export interface AnywhereDeps {
|
|
|
31
31
|
health?: (cwd: string) => Promise<HqdHealthResult>;
|
|
32
32
|
/** Test seam for the local runtime gate used to keep other checks out of a disabled runtime. */
|
|
33
33
|
runtimeEnabled?: () => boolean;
|
|
34
|
+
/** Test seam for the per-person HQ Anywhere setting; production reads hq-pro. */
|
|
35
|
+
personSettingReader?: () => Promise<boolean | null>;
|
|
34
36
|
/** The hq-cli version installed on this machine. Default: this CLI's version. */
|
|
35
37
|
cliVersion?: string;
|
|
36
38
|
serviceState?: () => DaemonServiceState;
|
|
@@ -24,6 +24,7 @@ import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK } from "
|
|
|
24
24
|
import { hqdSocketPath } from "../../daemon/protocol.js";
|
|
25
25
|
import { readRegistry, registryDir, registryFile } from "../../registry/registry.js";
|
|
26
26
|
import { anywhereRuntimeEnabled } from "../../../commands/anywhere-runtime-flag.js";
|
|
27
|
+
import { readHqAnywherePersonSetting } from "../../anywhere-person-setting.js";
|
|
27
28
|
const ANYWHERE_FAMILY_ID = "anywhere";
|
|
28
29
|
export const REMEDIATION = {
|
|
29
30
|
installClaude: "hq install --global --runtime claude",
|
|
@@ -44,7 +45,15 @@ async function runAnywhereChecks(context, deps = {}) {
|
|
|
44
45
|
const env = deps.env ?? process.env;
|
|
45
46
|
const socketPath = hqdSocketPath(home, env);
|
|
46
47
|
const cwd = context.cwd ?? process.cwd();
|
|
47
|
-
const
|
|
48
|
+
const flagEnabled = (deps.runtimeEnabled ?? anywhereRuntimeEnabled)();
|
|
49
|
+
const personSetting = await (deps.personSettingReader ?? (deps.runtimeEnabled ? async () => true : () => readHqAnywherePersonSetting({
|
|
50
|
+
env,
|
|
51
|
+
onError: (name) => process.stderr.write(`HQ Anywhere person setting unavailable (${name}); access remains off.\n`),
|
|
52
|
+
})))();
|
|
53
|
+
const runtimeEnabled = flagEnabled && personSetting === true;
|
|
54
|
+
const settingCheck = personSetting === null
|
|
55
|
+
? result("UNKNOWN", "person-setting", "HQ Anywhere person setting could not be read; runtime access is off")
|
|
56
|
+
: result(personSetting ? "PASS" : "WARN", "person-setting", `HQ Anywhere person setting is ${personSetting ? "on" : "off"}`);
|
|
48
57
|
let health;
|
|
49
58
|
let healthUnsupported = false;
|
|
50
59
|
try {
|
|
@@ -107,12 +116,12 @@ async function runAnywhereChecks(context, deps = {}) {
|
|
|
107
116
|
: undefined;
|
|
108
117
|
const healthCheck = runtimeHealthCheck(health, healthUnsupported, serviceState);
|
|
109
118
|
if (!runtimeEnabled)
|
|
110
|
-
return [healthCheck];
|
|
119
|
+
return [healthCheck, settingCheck];
|
|
111
120
|
const remoteModes = [
|
|
112
121
|
result("WARN", "chatgpt-mode", `ChatGPT integration mode: ${CHATGPT_MODE}; host connector setup is managed in ChatGPT`, "hq install --global --runtime chatgpt"),
|
|
113
122
|
result("WARN", "grok-mode", `Grok integration mode: ${GROK_MODE}; host connector setup is managed in Grok`, "hq install --global --runtime grok"),
|
|
114
123
|
];
|
|
115
|
-
const out = [healthCheck, ...remoteModes];
|
|
124
|
+
const out = [healthCheck, settingCheck, ...remoteModes];
|
|
116
125
|
if (!installed) {
|
|
117
126
|
return [...out, result("NA", "installed", "HQ local anywhere runtime is not installed on this machine", REMEDIATION.installClaude)];
|
|
118
127
|
}
|
|
@@ -40,7 +40,7 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
40
40
|
"@indigoai-us/hq-cloud",
|
|
41
41
|
],
|
|
42
42
|
"bot-logs-follow": [],
|
|
43
|
-
mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js"],
|
|
43
|
+
mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud", "../sentry.js"],
|
|
44
44
|
childWrapper: ["../main.js"],
|
|
45
45
|
coreJobRun: ["../lib/flag-registry.js"],
|
|
46
46
|
"daemon-call": ["../lib/daemon/protocol.js"],
|
|
@@ -170,6 +170,12 @@ const LOADERS = {
|
|
|
170
170
|
mcp: [
|
|
171
171
|
() => import("../lib/mcp/tools.js"),
|
|
172
172
|
() => import("../lib/mcp/prompts.js"),
|
|
173
|
+
() => import("../utils/cognito-session.js"),
|
|
174
|
+
() => import("@indigoai-us/hq-cloud"),
|
|
175
|
+
async () => {
|
|
176
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
177
|
+
preloadSentrySdk();
|
|
178
|
+
},
|
|
173
179
|
],
|
|
174
180
|
childWrapper: [
|
|
175
181
|
() => import("../main.js"),
|
package/docs/anywhere-runtime.md
CHANGED
|
@@ -5,7 +5,9 @@ local hqd daemon on macOS, Linux, and Windows. On Windows, hqd uses a bundled
|
|
|
5
5
|
PowerShell pipe host that applies and verifies a user-only named-pipe DACL
|
|
6
6
|
before becoming ready. A per-user
|
|
7
7
|
registry links repositories to HQ companies. The `hq-anywhere-runtime` flag
|
|
8
|
-
|
|
8
|
+
is the operator kill switch and is off by default. Runtime access also
|
|
9
|
+
requires the current person to enable the `hq-anywhere` setting in HQ. If that
|
|
10
|
+
setting cannot be read, runtime access stays off.
|
|
9
11
|
|
|
10
12
|
## Install
|
|
11
13
|
|
|
@@ -18,10 +20,12 @@ hq daemon status
|
|
|
18
20
|
hq install --global --runtime claude
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
`hq install --global` defaults to Claude
|
|
22
|
-
|
|
23
|
+
`hq install --global` defaults to Claude and requires both the runtime flag
|
|
24
|
+
and the current person's `hq-anywhere` setting. Select other supported
|
|
25
|
+
runtimes explicitly with `--runtime codex`, `--runtime chatgpt`, or
|
|
26
|
+
`--runtime grok`.
|
|
23
27
|
Claude and Codex receive local hook configuration. ChatGPT and Grok print
|
|
24
|
-
connector setup instructions and require
|
|
28
|
+
connector setup instructions and require both gates to be enabled.
|
|
25
29
|
|
|
26
30
|
The CLI stores the HQ root at `~/.hq/root` and keeps the folder registry in
|
|
27
31
|
`~/.hq/registry.json`. These files belong to the current host. They are not
|
|
@@ -66,7 +70,9 @@ request error. The daemon process itself does not terminate.
|
|
|
66
70
|
|
|
67
71
|
`hq doctor` reports hqd reachability, version and uptime, the last successful
|
|
68
72
|
runtime-flag read and its age, and whether hqd is holding that decision after a
|
|
69
|
-
read error. It
|
|
73
|
+
read error. It reports the current person's `hq-anywhere` setting as on, off,
|
|
74
|
+
or unknown when the setting endpoint cannot be read; unknown keeps runtime
|
|
75
|
+
access off. It also reports the company selected for the current directory or
|
|
70
76
|
the reason it fell back to personal context. On Unix, the socket line includes
|
|
71
77
|
its permission mode, owner check and whether it is user-only. Windows reports
|
|
72
78
|
the named-pipe transport without Unix mode or owner fields. Use `hq doctor
|
|
@@ -80,8 +86,8 @@ resident daemon does not support the health operation, doctor falls back to
|
|
|
80
86
|
`ping` for reachability, version and uptime, then warns that a restart is needed
|
|
81
87
|
for full diagnostics.
|
|
82
88
|
|
|
83
|
-
The
|
|
84
|
-
and Codex pack source. When
|
|
89
|
+
The flag and person setting also gate the published Claude plugin marketplace
|
|
90
|
+
and Codex pack source. When both are on, hq-cli adds the `indigoai-us/hq-packages`
|
|
85
91
|
Claude marketplace and installs the `hq` plugin, or resolves the exact
|
|
86
92
|
`@indigoai-us/hq-anywhere` alias to
|
|
87
93
|
`github:indigoai-us/hq-packages#packages/hq-pack-hq-anywhere`. The flag-off
|