@indigoai-us/hq-cli 5.345.46 → 5.345.48
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 +12 -0
- package/assets/scaffold/core/scripts/hq-session.sh +113 -7
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +101 -0
- package/dist/command-catalog.generated.d.ts +4 -0
- package/dist/command-catalog.generated.js +5 -0
- package/dist/commands/core-hq-session-native.d.ts +2 -0
- package/dist/commands/core-hq-session-native.js +242 -52
- package/dist/commands/core-resolve-company-native.d.ts +1 -0
- package/dist/commands/core-resolve-company-native.js +41 -11
- package/dist/commands/files.js +4 -1
- package/dist/commands/install-global.d.ts +2 -0
- package/dist/commands/install-global.js +38 -4
- package/dist/commands/secrets.d.ts +5 -0
- package/dist/commands/secrets.js +181 -2
- package/dist/commands/workers.d.ts +4 -1
- package/dist/commands/workers.js +57 -15
- package/dist/lib/cli-flag-keys.d.ts +1 -0
- package/dist/lib/cli-flag-keys.js +1 -0
- package/dist/lib/core-utils/session-id.d.ts +6 -0
- package/dist/lib/core-utils/session-id.js +35 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/lanes-reconcile-guard.d.ts +6 -5
- package/dist/lib/daemon/lanes-reconcile-guard.js +26 -7
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +36 -9
- package/dist/lib/daemon/supervisor.d.ts +7 -1
- package/dist/lib/daemon/supervisor.js +50 -4
- package/dist/lib/install/provider-wrappers.d.ts +9 -0
- package/dist/lib/install/provider-wrappers.js +190 -0
- package/dist/lib/lanes/provider-rejection.d.ts +5 -5
- package/dist/lib/lanes/provider-rejection.js +15 -10
- package/dist/lib/lanes/spawn.js +9 -4
- package/dist/lib/lanes/watcher.d.ts +6 -2
- package/dist/lib/lanes/watcher.js +37 -17
- package/dist/utils/resident-package-preload.js +4 -0
- package/dist/utils/self-update.js +8 -1
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +37 -0
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ import { trustCodexUserHooks } from '../utils/hook-trust.js';
|
|
|
27
27
|
import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
|
|
28
28
|
import { readDaemonConfig } from '../lib/daemon/config.js';
|
|
29
29
|
import { daemonPaths } from '../lib/daemon/paths.js';
|
|
30
|
+
import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
|
|
30
31
|
import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
|
|
31
32
|
import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
|
|
32
33
|
/** True when an hq daemon unit (launchd plist or systemd unit) exists. */
|
|
@@ -348,7 +349,14 @@ export function installGlobalClaude(opts) {
|
|
|
348
349
|
const env = opts.env ?? process.env;
|
|
349
350
|
const log = opts.log ?? ((l) => console.log(l));
|
|
350
351
|
const hqRoot = opts.hqRoot;
|
|
352
|
+
const providerBin = path.join(home, '.hq', 'bin');
|
|
353
|
+
// The wrappers are /bin/sh scripts reached through POSIX startup files, so a
|
|
354
|
+
// Windows shell would never pick them up. Skip them until a native shim exists.
|
|
355
|
+
const providerWrappersEnabled = process.platform !== 'win32'
|
|
356
|
+
&& (opts.installProviderWrappers ?? isAnywhereEnabled(home, env));
|
|
351
357
|
assertHqRoot(hqRoot);
|
|
358
|
+
if (providerWrappersEnabled)
|
|
359
|
+
assertProviderWrappersInstallable(providerBin);
|
|
352
360
|
const t = claudeTargets(home);
|
|
353
361
|
const prior = readState(t);
|
|
354
362
|
const pluginSource = opts.via === 'plugin' ? resolveClaudePluginSource(opts.marketplaceDir, env, opts.marketplaceRepository) : undefined;
|
|
@@ -429,6 +437,8 @@ export function installGlobalClaude(opts) {
|
|
|
429
437
|
if (pluginSource)
|
|
430
438
|
touched.push(t.settingsJson);
|
|
431
439
|
touched.push(regFile, t.stateFile);
|
|
440
|
+
if (providerWrappersEnabled)
|
|
441
|
+
touched.push(...providerWrapperInstallPaths(home));
|
|
432
442
|
if (opts.dryRun) {
|
|
433
443
|
log(`hq install --global --runtime claude --dry-run (HQ root ${hqRoot})`);
|
|
434
444
|
log('Would write:');
|
|
@@ -560,6 +570,8 @@ export function installGlobalClaude(opts) {
|
|
|
560
570
|
seeded: [...new Set([...(prior?.seeded ?? []), ...ownedSeeded])].sort(),
|
|
561
571
|
};
|
|
562
572
|
writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
573
|
+
if (providerWrappersEnabled)
|
|
574
|
+
installProviderWrappers(providerBin, home);
|
|
563
575
|
log(`HQ installed for Claude Code from ${hqRoot}.`);
|
|
564
576
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
565
577
|
reportSkippedSkills(skills, log, false);
|
|
@@ -634,6 +646,9 @@ export function uninstallGlobalClaude(opts = {}) {
|
|
|
634
646
|
}
|
|
635
647
|
const pointer = readOrNull(t.hqPointer);
|
|
636
648
|
const codexStillInstalled = fs.existsSync(codexTargets(home).stateFile);
|
|
649
|
+
const providerBin = path.join(home, '.hq', 'bin');
|
|
650
|
+
if (!codexStillInstalled)
|
|
651
|
+
touched.push(path.join(providerBin, 'claude'), path.join(providerBin, 'codex'));
|
|
637
652
|
const pointerOwned = !codexStillInstalled && pointer !== null && pointer.trim() === hqRoot;
|
|
638
653
|
if (pointerOwned)
|
|
639
654
|
touched.push(t.hqPointer);
|
|
@@ -656,6 +671,8 @@ export function uninstallGlobalClaude(opts = {}) {
|
|
|
656
671
|
if (pluginSource && state?.plugin?.marketplaceAdded) {
|
|
657
672
|
runClaude(['marketplace', 'remove', '--scope', 'user', pluginSource.marketplaceName], home, env);
|
|
658
673
|
}
|
|
674
|
+
if (!codexStillInstalled)
|
|
675
|
+
removeProviderWrappers(providerBin);
|
|
659
676
|
unregisterClaudeServer({ name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK, env: { home } });
|
|
660
677
|
if (fs.existsSync(t.settingsJson)) {
|
|
661
678
|
writeConfigAtomic({
|
|
@@ -739,7 +756,14 @@ export function installGlobalCodex(opts) {
|
|
|
739
756
|
const env = opts.env ?? process.env;
|
|
740
757
|
const log = opts.log ?? ((l) => console.log(l));
|
|
741
758
|
const hqRoot = opts.hqRoot;
|
|
759
|
+
const providerBin = path.join(home, '.hq', 'bin');
|
|
760
|
+
// The wrappers are /bin/sh scripts reached through POSIX startup files, so a
|
|
761
|
+
// Windows shell would never pick them up. Skip them until a native shim exists.
|
|
762
|
+
const providerWrappersEnabled = process.platform !== 'win32'
|
|
763
|
+
&& (opts.installProviderWrappers ?? isAnywhereEnabled(home, env));
|
|
742
764
|
assertCodexHqRoot(hqRoot);
|
|
765
|
+
if (providerWrappersEnabled)
|
|
766
|
+
assertProviderWrappersInstallable(providerBin);
|
|
743
767
|
if (!isCodexInstalled({ home })) {
|
|
744
768
|
throw Object.assign(new Error(`Codex is not installed (${path.join(home, '.codex')} is missing).`), {
|
|
745
769
|
expected: true,
|
|
@@ -793,6 +817,8 @@ export function installGlobalCodex(opts) {
|
|
|
793
817
|
if (tomlNext !== tomlDoc || legacyPackServerPresent)
|
|
794
818
|
touched.push(t.configToml);
|
|
795
819
|
touched.push(regFile, t.stateFile);
|
|
820
|
+
if (providerWrappersEnabled)
|
|
821
|
+
touched.push(...providerWrapperInstallPaths(home));
|
|
796
822
|
if (opts.dryRun) {
|
|
797
823
|
log(`hq install --global --runtime codex --dry-run (HQ root ${hqRoot})`);
|
|
798
824
|
log('Would write:');
|
|
@@ -875,6 +901,8 @@ export function installGlobalCodex(opts) {
|
|
|
875
901
|
seeded: [...new Set([...(prior?.seeded ?? []), ...ownedSeeded])].sort(),
|
|
876
902
|
};
|
|
877
903
|
writeFileAtomic(t.stateFile, `${JSON.stringify(state, null, 2)}\n`);
|
|
904
|
+
if (providerWrappersEnabled)
|
|
905
|
+
installProviderWrappers(providerBin, home);
|
|
878
906
|
log(`HQ installed for Codex from ${hqRoot}.`);
|
|
879
907
|
log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
|
|
880
908
|
reportSkippedSkills(skills, log, false);
|
|
@@ -895,8 +923,11 @@ export function uninstallGlobalCodex(opts = {}) {
|
|
|
895
923
|
throw new Error('No HQ global install found (no install state and no ~/.hq/root).');
|
|
896
924
|
const regFile = opts.registryFile ?? registryFile(registryDir(home, env));
|
|
897
925
|
const claudeStillInstalled = fs.existsSync(claudeTargets(home).stateFile);
|
|
926
|
+
const providerBin = path.join(home, '.hq', 'bin');
|
|
898
927
|
const packStillInstalled = fs.existsSync(path.join(hqRoot, 'core', 'packages', 'hq-anywhere', 'package.yaml'));
|
|
899
928
|
const touched = [];
|
|
929
|
+
if (!claudeStillInstalled)
|
|
930
|
+
touched.push(path.join(providerBin, 'claude'), path.join(providerBin, 'codex'));
|
|
900
931
|
const tomlDoc = readConfigDoc(t.configToml, codexConfigFormat).doc;
|
|
901
932
|
const servers = tomlDoc.value.mcp_servers;
|
|
902
933
|
if (!packStillInstalled && servers?.[HQ_MCP_SERVER_NAME]?._hqPack === HQ_ANYWHERE_PACK)
|
|
@@ -954,6 +985,8 @@ export function uninstallGlobalCodex(opts = {}) {
|
|
|
954
985
|
}
|
|
955
986
|
if (!packStillInstalled)
|
|
956
987
|
unregisterCodexServer({ name: HQ_MCP_SERVER_NAME, pack: HQ_ANYWHERE_PACK, env: { home } });
|
|
988
|
+
if (!claudeStillInstalled)
|
|
989
|
+
removeProviderWrappers(providerBin);
|
|
957
990
|
// Removal re-serializes the TOML (dropping comments). When what is left equals
|
|
958
991
|
// the pre-install document, put the original bytes back.
|
|
959
992
|
const tomlOriginal = state?.configToml.original ?? null;
|
|
@@ -1059,11 +1092,11 @@ export function resolveInstallRoot(explicit, options = {}) {
|
|
|
1059
1092
|
export async function runInstallGlobal(opts) {
|
|
1060
1093
|
const runtime = assertRuntime(opts.runtime);
|
|
1061
1094
|
const env = opts.env ?? process.env;
|
|
1095
|
+
const enabled = await anywhereRuntimeAllowed(env, {
|
|
1096
|
+
...(opts.personSettingReader ? { readSetting: opts.personSettingReader } : {}),
|
|
1097
|
+
onError: (name) => (opts.log ?? console.log)(`HQ Anywhere person setting unavailable (${name}); access remains off.`),
|
|
1098
|
+
});
|
|
1062
1099
|
if (runtime === 'chatgpt' || runtime === 'grok' || opts.dryRun) {
|
|
1063
|
-
const enabled = await anywhereRuntimeAllowed(env, {
|
|
1064
|
-
...(opts.personSettingReader ? { readSetting: opts.personSettingReader } : {}),
|
|
1065
|
-
onError: (name) => (opts.log ?? console.log)(`HQ Anywhere person setting unavailable (${name}); access remains off.`),
|
|
1066
|
-
});
|
|
1067
1100
|
if (!enabled)
|
|
1068
1101
|
throw new AnywhereRuntimeDisabledError();
|
|
1069
1102
|
if (runtime === 'chatgpt' || runtime === 'grok') {
|
|
@@ -1083,6 +1116,7 @@ export async function runInstallGlobal(opts) {
|
|
|
1083
1116
|
via: opts.via,
|
|
1084
1117
|
marketplaceDir: opts.marketplaceDir,
|
|
1085
1118
|
marketplaceRepository: opts.marketplaceRepository,
|
|
1119
|
+
installProviderWrappers: enabled,
|
|
1086
1120
|
env,
|
|
1087
1121
|
log: opts.log,
|
|
1088
1122
|
});
|
|
@@ -2,6 +2,11 @@ import type { Command } from "commander";
|
|
|
2
2
|
export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
|
|
3
3
|
export type { SecretLoadResponse, SecretScriptLockMode, SecretTier, SecretUsage, SecretUsageChannel } from "../secrets-env-action.js";
|
|
4
4
|
export type { SecretScriptUsage, SecretMetadata, SecretLoadSuccessRow } from "../secrets-env-action.js";
|
|
5
|
+
type SessionCompanyLock = {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
companies: string[];
|
|
8
|
+
} | undefined;
|
|
9
|
+
export declare function readSessionCompanyLock(env?: NodeJS.ProcessEnv): SessionCompanyLock;
|
|
5
10
|
import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
|
|
6
11
|
export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
7
12
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
package/dist/commands/secrets.js
CHANGED
|
@@ -1,9 +1,153 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
1
4
|
import { buildSecretUsage, extractApiMessage, highSecuritySandboxOnlyMessage, loadRevealedSecrets, normalizeScriptLockMode, normalizeSecretTier, parseSecretNameList, runSecretsEnvCommand, scopeOpts, secretsCredentialOptions, } from "../secrets-env-action.js";
|
|
2
5
|
export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
|
|
6
|
+
export function readSessionCompanyLock(env = process.env) {
|
|
7
|
+
const root = resolveDefaultHqRoot({ env });
|
|
8
|
+
const sessionId = resolveSessionId(root, env);
|
|
9
|
+
if (!sessionId)
|
|
10
|
+
return undefined;
|
|
11
|
+
const sessionDir = path.join(root, "workspace/sessions", sessionId);
|
|
12
|
+
const capabilityPath = path.join(sessionDir, "scope-capability.json");
|
|
13
|
+
const metaPath = path.join(sessionDir, "meta.yaml");
|
|
14
|
+
let primary;
|
|
15
|
+
let companies;
|
|
16
|
+
let hasCapability = false;
|
|
17
|
+
try {
|
|
18
|
+
fs.lstatSync(capabilityPath);
|
|
19
|
+
hasCapability = true;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const code = error.code;
|
|
23
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
24
|
+
throw new Error("Cannot verify this session's company lock because its scope capability is unreadable.");
|
|
25
|
+
}
|
|
26
|
+
if (hasCapability) {
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(fs.readFileSync(capabilityPath, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new Error("Cannot verify this session's company lock because its scope capability is unreadable.");
|
|
33
|
+
}
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
35
|
+
throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
|
|
36
|
+
const cap = value;
|
|
37
|
+
if (cap.session_id !== sessionId)
|
|
38
|
+
throw new Error("Cannot verify this session's company lock because its scope capability does not match the active session.");
|
|
39
|
+
if (typeof cap.company_slug !== "string" || !/^[A-Za-z0-9_-]+$/.test(cap.company_slug))
|
|
40
|
+
throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
|
|
41
|
+
primary = cap.company_slug;
|
|
42
|
+
if (cap.company_slugs === undefined)
|
|
43
|
+
companies = [primary];
|
|
44
|
+
else if (Array.isArray(cap.company_slugs)
|
|
45
|
+
&& cap.company_slugs.length > 0
|
|
46
|
+
&& cap.company_slugs.every((slug) => typeof slug === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(slug))
|
|
47
|
+
&& cap.company_slugs[0] === primary
|
|
48
|
+
&& new Set(cap.company_slugs).size === cap.company_slugs.length
|
|
49
|
+
&& ((primary === "personal" && cap.company_slugs.length === 1)
|
|
50
|
+
|| (primary !== "personal" && !cap.company_slugs.includes("personal"))))
|
|
51
|
+
companies = [...cap.company_slugs];
|
|
52
|
+
else
|
|
53
|
+
throw new Error("Cannot verify this session's company lock because its scope capability is invalid.");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
try {
|
|
57
|
+
const meta = fs.readFileSync(metaPath, "utf8");
|
|
58
|
+
const read = (key) => meta.split(/\r?\n/).find((line) => new RegExp(`^\\s*${key}:`).test(line))?.replace(/^\s*[^:]+:\s*/, "").replace(/^"|"$/g, "") ?? "";
|
|
59
|
+
primary = read("company_slug");
|
|
60
|
+
const hasList = meta.split(/\r?\n/).some((line) => /^\s*company_slugs:/.test(line));
|
|
61
|
+
const listed = hasList ? read("company_slugs") : "";
|
|
62
|
+
if (hasList && (!listed || !primary))
|
|
63
|
+
throw new Error("invalid company lock");
|
|
64
|
+
companies = hasList ? listed.split(",") : primary ? [primary] : [];
|
|
65
|
+
if (companies.some((slug) => !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(slug)))
|
|
66
|
+
throw new Error("invalid company lock");
|
|
67
|
+
if (new Set(companies).size !== companies.length)
|
|
68
|
+
throw new Error("invalid company lock");
|
|
69
|
+
if (primary && companies[0] !== primary)
|
|
70
|
+
throw new Error("invalid company lock");
|
|
71
|
+
if (companies.length > 1 && companies.includes("personal"))
|
|
72
|
+
throw new Error("invalid company lock");
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const code = error.code;
|
|
76
|
+
if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES")
|
|
77
|
+
return { sessionId, companies: [] };
|
|
78
|
+
throw new Error("Cannot verify this session's company lock because its metadata is invalid.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const real = companies.filter((slug) => {
|
|
82
|
+
if (slug === "personal")
|
|
83
|
+
return true;
|
|
84
|
+
try {
|
|
85
|
+
const companyPath = path.join(root, "companies", slug);
|
|
86
|
+
const stat = fs.lstatSync(companyPath);
|
|
87
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
if (primary && real[0] !== primary)
|
|
94
|
+
return { sessionId, companies: [] };
|
|
95
|
+
return { sessionId, companies: real };
|
|
96
|
+
}
|
|
97
|
+
function companyUidForSlug(root, slug) {
|
|
98
|
+
try {
|
|
99
|
+
const doc = yaml.load(fs.readFileSync(path.join(root, "companies", "manifest.yaml"), "utf8"));
|
|
100
|
+
const uid = doc.companies?.[slug]?.cloud_uid;
|
|
101
|
+
return typeof uid === "string" && looksLikeCompanyUid(uid) ? uid : undefined;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function companySlugForUid(root, companies, uid) {
|
|
108
|
+
if (!looksLikeCompanyUid(uid))
|
|
109
|
+
return undefined;
|
|
110
|
+
try {
|
|
111
|
+
const doc = yaml.load(fs.readFileSync(path.join(root, "companies", "manifest.yaml"), "utf8"));
|
|
112
|
+
return companies.find((slug) => doc.companies?.[slug]?.cloud_uid === uid);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function resolveApiKeyCompanyWithinLock(input) {
|
|
119
|
+
let candidates = input.companies;
|
|
120
|
+
if (input.requestedCompany) {
|
|
121
|
+
const companySlug = input.companies.includes(input.requestedCompany)
|
|
122
|
+
? input.requestedCompany
|
|
123
|
+
: companySlugForUid(input.root, input.companies, input.requestedCompany);
|
|
124
|
+
if (!companySlug) {
|
|
125
|
+
throw new Error("HQ_API_KEY is not bound to a company in this session's locked company set.");
|
|
126
|
+
}
|
|
127
|
+
candidates = [companySlug];
|
|
128
|
+
}
|
|
129
|
+
for (const companySlug of candidates) {
|
|
130
|
+
const companyUid = companyUidForSlug(input.root, companySlug);
|
|
131
|
+
if (!companyUid)
|
|
132
|
+
continue;
|
|
133
|
+
const response = await vaultApiFetch({ token: input.token, path: `/v1/keys/secrets/${encodeURIComponent(companyUid)}` });
|
|
134
|
+
if (response.ok)
|
|
135
|
+
return { companyUid, companySlug };
|
|
136
|
+
if (response.status === 401)
|
|
137
|
+
throw new Error("HQ_API_KEY is invalid or no longer active.");
|
|
138
|
+
if (response.status !== 403)
|
|
139
|
+
throw new Error(`Could not verify HQ_API_KEY company scope (HTTP ${response.status}).`);
|
|
140
|
+
}
|
|
141
|
+
throw new Error("HQ_API_KEY is not bound to a company in this session's locked company set.");
|
|
142
|
+
}
|
|
3
143
|
import chalk from "chalk";
|
|
4
144
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
145
|
import { removeCacheEntry, clearAllCache } from "../utils/secrets-cache.js";
|
|
6
146
|
import { isHqCliChild, releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
|
|
147
|
+
import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
|
|
148
|
+
import { fastFlagEnabled } from "../native-fast-route.js";
|
|
149
|
+
import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
|
|
150
|
+
import { resolveSessionId } from "../lib/core-utils/session-id.js";
|
|
7
151
|
import { secretsExecChildExitCode } from "../utils/child-wrapper-exit.js";
|
|
8
152
|
import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
9
153
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
|
|
@@ -1085,6 +1229,7 @@ export function registerSecretsCommand(program) {
|
|
|
1085
1229
|
.command("exec")
|
|
1086
1230
|
.description("Run a command with secrets injected as env vars")
|
|
1087
1231
|
.requiredOption("--only <keys>", "Secret names to inject (comma-separated; may be repeated) (required)", collectSecretNames)
|
|
1232
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)")
|
|
1088
1233
|
.option("--script <path>", "Attach local script identity for script-locked secrets")
|
|
1089
1234
|
.option("--script-id <id>", "Stable script identifier approved by policy")
|
|
1090
1235
|
.allowUnknownOption(true)
|
|
@@ -1104,10 +1249,44 @@ export function registerSecretsCommand(program) {
|
|
|
1104
1249
|
process.exit(1);
|
|
1105
1250
|
}
|
|
1106
1251
|
const keys = parseSecretNameList(_opts.only);
|
|
1252
|
+
const multiCompanyLockEnabled = await fastFlagEnabled(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
1253
|
+
const sessionLock = multiCompanyLockEnabled ? readSessionCompanyLock(process.env) : undefined;
|
|
1254
|
+
const parentScope = secrets.opts();
|
|
1255
|
+
const inheritedCompany = parentScope.company;
|
|
1256
|
+
const requestedCompany = _opts.company ?? inheritedCompany;
|
|
1257
|
+
const hasLockedCompanies = (sessionLock?.companies.length ?? 0) > 0;
|
|
1258
|
+
const lockRoot = hasLockedCompanies ? resolveDefaultHqRoot({ env: process.env }) : undefined;
|
|
1259
|
+
const selectedCompany = (hasLockedCompanies && requestedCompany && looksLikeCompanyUid(requestedCompany)
|
|
1260
|
+
? companySlugForUid(lockRoot, sessionLock.companies, requestedCompany) ?? requestedCompany
|
|
1261
|
+
: requestedCompany)
|
|
1262
|
+
?? (multiCompanyLockEnabled && !parentScope.personal ? sessionLock?.companies[0] : undefined);
|
|
1263
|
+
if (hasLockedCompanies && selectedCompany && !sessionLock.companies.includes(selectedCompany)) {
|
|
1264
|
+
throw new Error(`Company '${selectedCompany}' is outside this session's locked company set.`);
|
|
1265
|
+
}
|
|
1266
|
+
if (hasLockedCompanies && parentScope.personal && !sessionLock.companies.includes("personal")) {
|
|
1267
|
+
throw new Error("Personal scope is outside this session's locked company set.");
|
|
1268
|
+
}
|
|
1269
|
+
if (hasLockedCompanies && !selectedCompany && !parentScope.personal) {
|
|
1270
|
+
throw new Error("This session has no locked company scope for secrets exec.");
|
|
1271
|
+
}
|
|
1272
|
+
if (hasLockedCompanies && !requestedCompany && sessionLock.companies.length > 1) {
|
|
1273
|
+
console.error(`Using primary company ${sessionLock.companies[0]} from this session's lock; pass --company <slug> to choose another locked company.`);
|
|
1274
|
+
}
|
|
1275
|
+
const secretScope = selectedCompany === "personal"
|
|
1276
|
+
? scopeOpts({ personal: true })
|
|
1277
|
+
: scopeOpts({ ...parentScope, ...(selectedCompany ? { company: selectedCompany } : {}) });
|
|
1107
1278
|
const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets exec", secretsCredentialOptions());
|
|
1279
|
+
const apiKeyCompany = cred.kind === "api-key" && hasLockedCompanies
|
|
1280
|
+
? await resolveApiKeyCompanyWithinLock({
|
|
1281
|
+
token: cred.token,
|
|
1282
|
+
root: lockRoot,
|
|
1283
|
+
companies: sessionLock.companies,
|
|
1284
|
+
...(requestedCompany ? { requestedCompany } : {}),
|
|
1285
|
+
})
|
|
1286
|
+
: undefined;
|
|
1108
1287
|
const companyUid = cred.kind === "api-key"
|
|
1109
|
-
? "__api_key__"
|
|
1110
|
-
: await getEntityUid(cred.token,
|
|
1288
|
+
? (apiKeyCompany?.companyUid ?? "__api_key__")
|
|
1289
|
+
: await getEntityUid(cred.token, secretScope, { allowOfflineScopeFallback: true });
|
|
1111
1290
|
const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
|
|
1112
1291
|
? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
|
|
1113
1292
|
: undefined, { allowOfflineFallback: true });
|
|
@@ -6,12 +6,15 @@ import { type RegistryWorker } from "../lib/workers-registry/read.js";
|
|
|
6
6
|
* company_slug. Undefined when no session context is set. Pure/read-only.
|
|
7
7
|
*/
|
|
8
8
|
export declare function resolveActiveCompany(hqRoot: string): string | undefined;
|
|
9
|
+
/** Resolve the active ordered lock set; default-off preserves primary-only access. */
|
|
10
|
+
export declare function resolveActiveCompanies(hqRoot: string, multiCompanyLockEnabled: boolean): string[] | undefined;
|
|
11
|
+
export declare function denyWorkerCompanyOutsideLock(company: string, lockedCompanies: string[] | undefined): boolean;
|
|
9
12
|
/**
|
|
10
13
|
* Membership-aware access filter for worker discovery: public workers always;
|
|
11
14
|
* company workers only for the active company. Mirrors the /run skill and the
|
|
12
15
|
* inject-worker-suggestion hook so all three surfaces agree.
|
|
13
16
|
*/
|
|
14
|
-
export declare function filterAccessibleWorkers(workers: RegistryWorker[], activeCompany: string | undefined): RegistryWorker[];
|
|
17
|
+
export declare function filterAccessibleWorkers(workers: RegistryWorker[], activeCompany: string | string[] | undefined): RegistryWorker[];
|
|
15
18
|
/**
|
|
16
19
|
* The company-relative vault prefix for a worker, e.g.
|
|
17
20
|
* companies/indigo/workers/deal-brain/ -> workers/deal-brain/. Returns null for
|
package/dist/commands/workers.js
CHANGED
|
@@ -7,6 +7,9 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
7
7
|
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
8
8
|
import { GROUP_ID_PATTERN, EMAIL_PATTERN, toSharedFolderGlob } from "./_patterns.js";
|
|
9
9
|
import { findHqRoot } from "../utils/manifest.js";
|
|
10
|
+
import { fastFlagEnabled } from "../native-fast-route.js";
|
|
11
|
+
import { MULTI_COMPANY_SESSION_LOCK_FLAG } from "../lib/cli-flag-keys.js";
|
|
12
|
+
import { resolveSessionId } from "../lib/core-utils/session-id.js";
|
|
10
13
|
export { readWorkerRegistry } from "../lib/workers-registry/read.js";
|
|
11
14
|
import { readWorkerRegistry } from "../lib/workers-registry/read.js";
|
|
12
15
|
/**
|
|
@@ -14,34 +17,68 @@ import { readWorkerRegistry } from "../lib/workers-registry/read.js";
|
|
|
14
17
|
* company_slug. Undefined when no session context is set. Pure/read-only.
|
|
15
18
|
*/
|
|
16
19
|
export function resolveActiveCompany(hqRoot) {
|
|
20
|
+
return resolveActiveCompanies(hqRoot, false)?.[0];
|
|
21
|
+
}
|
|
22
|
+
/** Resolve the active ordered lock set; default-off preserves primary-only access. */
|
|
23
|
+
export function resolveActiveCompanies(hqRoot, multiCompanyLockEnabled) {
|
|
24
|
+
const current = resolveSessionId(hqRoot);
|
|
25
|
+
if (!current)
|
|
26
|
+
return undefined;
|
|
17
27
|
try {
|
|
18
|
-
const currentFile = path.join(hqRoot, "workspace/sessions/.current");
|
|
19
|
-
const current = fs.readFileSync(currentFile, "utf8").trim();
|
|
20
|
-
if (!current)
|
|
21
|
-
return undefined;
|
|
22
28
|
const metaPath = path.join(hqRoot, "workspace/sessions", current, "meta.yaml");
|
|
23
29
|
if (!fs.existsSync(metaPath))
|
|
24
|
-
return undefined;
|
|
30
|
+
return multiCompanyLockEnabled ? [] : undefined;
|
|
25
31
|
const meta = yaml.load(fs.readFileSync(metaPath, "utf8"));
|
|
26
|
-
const
|
|
27
|
-
|
|
32
|
+
const primary = meta?.company_slug;
|
|
33
|
+
if (typeof primary !== "string" || !/^[A-Za-z0-9_-]+$/.test(primary))
|
|
34
|
+
return multiCompanyLockEnabled ? [] : undefined;
|
|
35
|
+
const rawListed = meta?.company_slugs;
|
|
36
|
+
const listed = multiCompanyLockEnabled && typeof rawListed === "string"
|
|
37
|
+
? rawListed.split(",")
|
|
38
|
+
: [primary];
|
|
39
|
+
if (!multiCompanyLockEnabled)
|
|
40
|
+
return [primary];
|
|
41
|
+
const companies = [...new Set(listed.filter((slug) => {
|
|
42
|
+
if (typeof slug !== "string" || !/^[A-Za-z0-9_-]+$/.test(slug))
|
|
43
|
+
return false;
|
|
44
|
+
if (slug === "personal")
|
|
45
|
+
return true;
|
|
46
|
+
try {
|
|
47
|
+
const stat = fs.lstatSync(path.join(hqRoot, "companies", slug));
|
|
48
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}))];
|
|
54
|
+
if (companies.length > 1 && companies.includes("personal"))
|
|
55
|
+
return [primary];
|
|
56
|
+
return companies[0] === primary ? companies : undefined;
|
|
28
57
|
}
|
|
29
58
|
catch {
|
|
30
|
-
return undefined;
|
|
59
|
+
return multiCompanyLockEnabled ? [] : undefined;
|
|
31
60
|
}
|
|
32
61
|
}
|
|
62
|
+
export function denyWorkerCompanyOutsideLock(company, lockedCompanies) {
|
|
63
|
+
if (lockedCompanies === undefined || lockedCompanies.includes(company))
|
|
64
|
+
return false;
|
|
65
|
+
console.error(`Company '${company}' is outside this session's locked company set.`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
33
69
|
/**
|
|
34
70
|
* Membership-aware access filter for worker discovery: public workers always;
|
|
35
71
|
* company workers only for the active company. Mirrors the /run skill and the
|
|
36
72
|
* inject-worker-suggestion hook so all three surfaces agree.
|
|
37
73
|
*/
|
|
38
74
|
export function filterAccessibleWorkers(workers, activeCompany) {
|
|
75
|
+
const allowed = new Set(Array.isArray(activeCompany) ? activeCompany : activeCompany ? [activeCompany] : []);
|
|
39
76
|
return workers.filter((w) => {
|
|
40
77
|
if (w.visibility === "public")
|
|
41
78
|
return true;
|
|
42
79
|
if (!w.company)
|
|
43
80
|
return false;
|
|
44
|
-
return
|
|
81
|
+
return allowed.has(w.company);
|
|
45
82
|
});
|
|
46
83
|
}
|
|
47
84
|
/**
|
|
@@ -159,14 +196,18 @@ async function runWorkersShare(workerId, opts) {
|
|
|
159
196
|
console.log(chalk.green("✓"), `Shared worker '${workerId}' with ${classified.label} (${permission}).`);
|
|
160
197
|
console.log(chalk.dim(` Vault prefix ${prefix} granted in company '${companySlug}'. It will sync to granted members and appear in their /run list.`));
|
|
161
198
|
}
|
|
162
|
-
function runWorkersList(opts) {
|
|
199
|
+
async function runWorkersList(opts) {
|
|
163
200
|
const hqRoot = findHqRoot();
|
|
164
|
-
const
|
|
165
|
-
|
|
201
|
+
const enabled = await fastFlagEnabled(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
202
|
+
const lockedCompanies = resolveActiveCompanies(hqRoot, enabled);
|
|
203
|
+
if (enabled && opts.company && denyWorkerCompanyOutsideLock(opts.company, lockedCompanies))
|
|
204
|
+
return;
|
|
205
|
+
const activeCompanies = opts.company ? [opts.company] : lockedCompanies;
|
|
206
|
+
let workers = filterAccessibleWorkers(readWorkerRegistry(hqRoot), activeCompanies);
|
|
166
207
|
if (opts.shared)
|
|
167
208
|
workers = workers.filter((w) => Boolean(w.grants && w.grants.trim()));
|
|
168
209
|
if (opts.mine)
|
|
169
|
-
workers = workers.filter((w) => Boolean(w.company) && w.company ===
|
|
210
|
+
workers = workers.filter((w) => Boolean(w.company) && activeCompanies?.includes(w.company) === true);
|
|
170
211
|
if (workers.length === 0) {
|
|
171
212
|
console.log("No accessible workers.");
|
|
172
213
|
return;
|
|
@@ -185,8 +226,9 @@ function runWorkersList(opts) {
|
|
|
185
226
|
}
|
|
186
227
|
};
|
|
187
228
|
printGroup("Public", publicWorkers);
|
|
188
|
-
|
|
189
|
-
printGroup(
|
|
229
|
+
for (const company of activeCompanies ?? []) {
|
|
230
|
+
printGroup(company, companyWorkers.filter((worker) => worker.company === company));
|
|
231
|
+
}
|
|
190
232
|
console.log(chalk.dim("\nUsage: hq run {worker-id} [skill] [args]"));
|
|
191
233
|
console.log(chalk.dim("Share a company worker: hq workers share {worker-id} --with {grp_<name>|@all} --permission read"));
|
|
192
234
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const SESSION_ID_ENV_KEYS: readonly ["HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID"];
|
|
2
|
+
export declare function validSessionId(value: string): boolean;
|
|
3
|
+
export declare function sessionIdFromEnv(env: NodeJS.ProcessEnv): string;
|
|
4
|
+
/** Resolve the caller's session before using the global, last-writer-wins .current fallback. */
|
|
5
|
+
export declare function resolveSessionId(root: string, env?: NodeJS.ProcessEnv, readFile?: (filePath: string, encoding: BufferEncoding) => string): string;
|
|
6
|
+
//# sourceMappingURL=session-id.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export const SESSION_ID_ENV_KEYS = [
|
|
4
|
+
"HQ_SESSION_ID",
|
|
5
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
6
|
+
"CLAUDE_SESSION_ID",
|
|
7
|
+
"CODEX_SESSION_ID",
|
|
8
|
+
"CODEX_THREAD_ID",
|
|
9
|
+
"GROK_SESSION_ID",
|
|
10
|
+
];
|
|
11
|
+
export function validSessionId(value) {
|
|
12
|
+
return Boolean(value) && value !== "." && value !== ".." && /^[A-Za-z0-9._-]+$/.test(value);
|
|
13
|
+
}
|
|
14
|
+
export function sessionIdFromEnv(env) {
|
|
15
|
+
for (const key of SESSION_ID_ENV_KEYS) {
|
|
16
|
+
const value = (env[key] ?? "").replace(/[\t\n\v\f\r ]/g, "");
|
|
17
|
+
if (validSessionId(value))
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
/** Resolve the caller's session before using the global, last-writer-wins .current fallback. */
|
|
23
|
+
export function resolveSessionId(root, env = process.env, readFile = (filePath, encoding) => fs.readFileSync(filePath, encoding)) {
|
|
24
|
+
const fromEnv = sessionIdFromEnv(env);
|
|
25
|
+
if (fromEnv)
|
|
26
|
+
return fromEnv;
|
|
27
|
+
try {
|
|
28
|
+
const current = readFile(path.join(root, "workspace", "sessions", ".current"), "utf8").replace(/[\t\n\v\f\r ]/g, "");
|
|
29
|
+
return validSessionId(current) ? current : "";
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=session-id.js.map
|
|
@@ -230,7 +230,7 @@ function planService(service, ctx) {
|
|
|
230
230
|
return [
|
|
231
231
|
base(service, {
|
|
232
232
|
kind: "scheduled",
|
|
233
|
-
// Polls every 15 s so keep-alive resumes are admitted on time (#1396).
|
|
233
|
+
// Polls every 15 s so local keep-alive resumes are admitted on time (#1396).
|
|
234
234
|
// Each pass is a fresh `hq` node process (~1.8 s CPU, ~200 MB RSS), so
|
|
235
235
|
// desktops first check the lane store in-process
|
|
236
236
|
// (lanes-reconcile-guard.ts) and fall back to the pre-#1396 60 s
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* JSON reads, no child process) and tells the supervisor how urgent the next
|
|
8
8
|
* pass is:
|
|
9
9
|
*
|
|
10
|
-
* - `spawn`: something the 15-second cadence exists for may be due —
|
|
11
|
-
*
|
|
10
|
+
* - `spawn`: something the desktop's 15-second cadence exists for may be due —
|
|
11
|
+
* local lane admission/heartbeat, a local keep-alive resume (#1396), or a
|
|
12
|
+
* pending question.
|
|
12
13
|
* - `idle`: only work that ran on the pre-#1396 60-second cadence remains
|
|
13
|
-
* (terminal lanes, worker-profile resumes, question aging,
|
|
14
|
-
* retention, lead-autopilot registry). The supervisor runs it
|
|
15
|
-
* per `idleIntervalSec`.
|
|
14
|
+
* (terminal lanes, worker-profile resumes, question aging, outpost mirror
|
|
15
|
+
* refresh, Codex retention, lead-autopilot registry). The supervisor runs it
|
|
16
|
+
* at most once per `idleIntervalSec`.
|
|
16
17
|
* - `skip`: the store holds no lane or question records, no lead registry and
|
|
17
18
|
* no lanes config, so a reconcile pass has no input to act on.
|
|
18
19
|
*
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* JSON reads, no child process) and tells the supervisor how urgent the next
|
|
8
8
|
* pass is:
|
|
9
9
|
*
|
|
10
|
-
* - `spawn`: something the 15-second cadence exists for may be due —
|
|
11
|
-
*
|
|
10
|
+
* - `spawn`: something the desktop's 15-second cadence exists for may be due —
|
|
11
|
+
* local lane admission/heartbeat, a local keep-alive resume (#1396), or a
|
|
12
|
+
* pending question.
|
|
12
13
|
* - `idle`: only work that ran on the pre-#1396 60-second cadence remains
|
|
13
|
-
* (terminal lanes, worker-profile resumes, question aging,
|
|
14
|
-
* retention, lead-autopilot registry). The supervisor runs it
|
|
15
|
-
* per `idleIntervalSec`.
|
|
14
|
+
* (terminal lanes, worker-profile resumes, question aging, outpost mirror
|
|
15
|
+
* refresh, Codex retention, lead-autopilot registry). The supervisor runs it
|
|
16
|
+
* at most once per `idleIntervalSec`.
|
|
16
17
|
* - `skip`: the store holds no lane or question records, no lead registry and
|
|
17
18
|
* no lanes config, so a reconcile pass has no input to act on.
|
|
18
19
|
*
|
|
@@ -38,6 +39,9 @@ export const LEAD_REGISTRY_FILE = path.join("workspace", "lane-briefs", "juniors
|
|
|
38
39
|
* keep-alive, which is checked separately.
|
|
39
40
|
*/
|
|
40
41
|
const QUIET_LANE_STATES = new Set(["done", "failed", "interrupted", "cancelled", "killed", "blocked"]);
|
|
42
|
+
const KNOWN_LANE_STATES = new Set([
|
|
43
|
+
"queued", "running", "awaiting_input", "blocked", "done", "failed", "interrupted", "cancelled", "killed",
|
|
44
|
+
]);
|
|
41
45
|
const realFs = {
|
|
42
46
|
readdirSync: (dir) => fs.readdirSync(dir),
|
|
43
47
|
readFileSync: (file, encoding) => fs.readFileSync(file, encoding),
|
|
@@ -63,9 +67,24 @@ function readRecord(fsDeps, file) {
|
|
|
63
67
|
}
|
|
64
68
|
return parsed;
|
|
65
69
|
}
|
|
66
|
-
/** A lane that may need the 15-second cadence. Unknown shapes count as urgent. */
|
|
70
|
+
/** A lane that may need the desktop's 15-second cadence. Unknown shapes count as urgent. */
|
|
67
71
|
function laneIsUrgent(lane) {
|
|
68
|
-
if (typeof lane.state !== "string" || !
|
|
72
|
+
if (typeof lane.state !== "string" || !KNOWN_LANE_STATES.has(lane.state))
|
|
73
|
+
return true;
|
|
74
|
+
const host = lane.host;
|
|
75
|
+
if (typeof host !== "object" || host === null || Array.isArray(host))
|
|
76
|
+
return true;
|
|
77
|
+
const hostRecord = host;
|
|
78
|
+
if (typeof hostRecord.id !== "string" || hostRecord.id.length === 0)
|
|
79
|
+
return true;
|
|
80
|
+
// This guard is installed only on desktops. The outpost's own daemon keeps
|
|
81
|
+
// its 15-second cadence; the desktop's outpost mirror/registry refresh can
|
|
82
|
+
// use the 60-second idle cadence. Pending questions remain urgent below.
|
|
83
|
+
if (hostRecord.kind === "outpost")
|
|
84
|
+
return false;
|
|
85
|
+
if (hostRecord.kind !== "local")
|
|
86
|
+
return true;
|
|
87
|
+
if (!QUIET_LANE_STATES.has(lane.state))
|
|
69
88
|
return true;
|
|
70
89
|
if (lane.keep_alive === true)
|
|
71
90
|
return true;
|