@indigoai-us/hq-cli 5.108.25 → 5.109.0
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 +88 -0
- package/dist/commands/__fixtures__/access-vault.d.ts +93 -0
- package/dist/commands/__fixtures__/access-vault.js +166 -0
- package/dist/commands/access.d.ts +158 -0
- package/dist/commands/access.js +783 -0
- package/dist/commands/cloud.js +11 -1
- package/dist/commands/files-browse.d.ts +25 -1
- package/dist/commands/files-browse.js +81 -17
- package/dist/commands/files.js +15 -5
- package/dist/commands/integrations-api.d.ts +15 -0
- package/dist/commands/integrations-connect.js +84 -3
- package/dist/commands/integrations-oauth.js +62 -3
- package/dist/commands/mcp-registration.d.ts +17 -7
- package/dist/commands/mcp-registration.js +16 -27
- package/dist/commands/mesh.js +174 -50
- package/dist/commands/pack-install.js +5 -5
- package/dist/commands/secrets.d.ts +7 -0
- package/dist/commands/secrets.js +26 -2
- package/dist/commands/sync-mode.js +12 -1
- package/dist/commands/sync-narrow.js +12 -1
- package/dist/lib/mesh/live/backfill-held.d.ts +42 -1
- package/dist/lib/mesh/live/backfill-held.js +95 -13
- package/dist/lib/mesh/live/daemon/doctor.d.ts +15 -0
- package/dist/lib/mesh/live/daemon/doctor.js +41 -10
- package/dist/lib/mesh/live/daemon/mode.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/mode.js +88 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +8 -0
- package/dist/lib/mesh/live/daemon/run.js +39 -28
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/emit-client.d.ts +99 -0
- package/dist/lib/mesh/live/emit-client.js +193 -0
- package/dist/lib/mesh/live/emit-evidence.d.ts +49 -0
- package/dist/lib/mesh/live/emit-evidence.js +77 -0
- package/dist/lib/mesh/live/emit-replay.d.ts +26 -0
- package/dist/lib/mesh/live/emit-replay.js +157 -0
- package/dist/lib/mesh/live/emit-retry.d.ts +25 -0
- package/dist/lib/mesh/live/emit-retry.js +79 -0
- package/dist/lib/mesh/live/emit.d.ts +54 -0
- package/dist/lib/mesh/live/emit.js +153 -0
- package/dist/lib/narrow-hint-banner.d.ts +3 -7
- package/dist/lib/narrow-hint-banner.js +13 -34
- package/dist/lib/plan-limit-nag.d.ts +0 -3
- package/dist/lib/plan-limit-nag.js +10 -20
- package/dist/register-all.js +3 -0
- package/dist/utils/access-denied-hint.d.ts +32 -0
- package/dist/utils/access-denied-hint.js +139 -0
- package/dist/utils/access-requests.d.ts +28 -0
- package/dist/utils/access-requests.js +98 -0
- package/package.json +1 -1
|
@@ -57,7 +57,6 @@ import * as fs from 'fs';
|
|
|
57
57
|
import * as os from 'os';
|
|
58
58
|
import * as path from 'path';
|
|
59
59
|
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
60
|
-
import { resolveFlagGate, resolveProcessFlagGate, } from '../lib/flag-registry.js';
|
|
61
60
|
// ---------------------------------------------------------------------------
|
|
62
61
|
// Named error classes (no bare catch-all anywhere in this module).
|
|
63
62
|
//
|
|
@@ -1226,28 +1225,18 @@ export function manifestTarget(manifest) {
|
|
|
1226
1225
|
}
|
|
1227
1226
|
return manifest.url ?? '';
|
|
1228
1227
|
}
|
|
1229
|
-
/**
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
/** Resolve the gate once at the start of an MCP registration operation. */
|
|
1242
|
-
export function isMcpRegistrationEnabled(flagReader) {
|
|
1243
|
-
return flagReader
|
|
1244
|
-
? resolveFlagGate(flagReader, 'cli.mcp-registration', MCP_REGISTRATION_LOOKUP, mcpRegistrationLegacyFallback)
|
|
1245
|
-
: resolveProcessFlagGate('cli.mcp-registration', MCP_REGISTRATION_LOOKUP, mcpRegistrationLegacyFallback);
|
|
1246
|
-
}
|
|
1247
|
-
/** Freeze a registration decision so multi-server work cannot split on refresh. */
|
|
1248
|
-
export function captureMcpRegistrationDecision(flagReader) {
|
|
1249
|
-
const enabled = isMcpRegistrationEnabled(flagReader);
|
|
1250
|
-
return { isEnabled: () => enabled };
|
|
1228
|
+
/**
|
|
1229
|
+
* Resolve the MCP registration kill switch from its env var alone.
|
|
1230
|
+
*
|
|
1231
|
+
* `HQ_DISABLE_MCP_REGISTRATION` is an operator's own machine-local opt-out, not
|
|
1232
|
+
* a rollout flag, so it is read straight from the environment — no registry
|
|
1233
|
+
* lookup. The polarity is inverted and the value match is asymmetric on
|
|
1234
|
+
* purpose: ONLY the exact value "1" disables registration; unset and every
|
|
1235
|
+
* other value (including "0" and "false") leave it enabled. Do not "tidy" this
|
|
1236
|
+
* into a boolean parse — that would silently disable anyone who wrote "false".
|
|
1237
|
+
*/
|
|
1238
|
+
export function isMcpRegistrationEnabled(env = process.env) {
|
|
1239
|
+
return env.HQ_DISABLE_MCP_REGISTRATION !== '1';
|
|
1251
1240
|
}
|
|
1252
1241
|
/**
|
|
1253
1242
|
* Register one pack's MCP servers into the shared agent configs (the public seam
|
|
@@ -1275,14 +1264,14 @@ export function registerMcpServers(pkg, names, options) {
|
|
|
1275
1264
|
// ORDERING: this is checked FIRST, BEFORE the loadManifest programmer-error guard
|
|
1276
1265
|
// below. The kill-switch is a USER/OPERATOR condition; the missing-loadManifest
|
|
1277
1266
|
// throw is a PROGRAMMER error. An operator who set the kill-switch must never hit a
|
|
1278
|
-
// spurious McpManifestError, so the operator path wins.
|
|
1279
|
-
//
|
|
1280
|
-
// home-path injector, not process env.
|
|
1267
|
+
// spurious McpManifestError, so the operator path wins. The decision is read
|
|
1268
|
+
// straight from `HQ_DISABLE_MCP_REGISTRATION`; `options.env` is the
|
|
1269
|
+
// SafeWriteEnv home-path injector, not the process env this switch reads.
|
|
1281
1270
|
//
|
|
1282
1271
|
// Returns an empty RegisterServerResult[] (`[]`) — the correct "no servers
|
|
1283
1272
|
// registered" semantic — so callers (pack-install) consume it gracefully. The skip
|
|
1284
1273
|
// NOTICE is emitted exactly once per registerMcpServers call.
|
|
1285
|
-
if (!
|
|
1274
|
+
if (!(options?.registrationEnabled ?? isMcpRegistrationEnabled())) {
|
|
1286
1275
|
process.stderr.write('MCP registration skipped (HQ_DISABLE_MCP_REGISTRATION=1)\n');
|
|
1287
1276
|
return [];
|
|
1288
1277
|
}
|
package/dist/commands/mesh.js
CHANGED
|
@@ -14,10 +14,17 @@ import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, cre
|
|
|
14
14
|
import { clearDefaultCompany, getDefaultCompany, readDeviceConfig, recordMigrationCapabilitySnapshot, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
15
15
|
import { DefaultCompanyLockedError, DefaultCompanyUnavailableError, } from "../lib/work-context/errors.js";
|
|
16
16
|
import { isValidSessionId } from "../lib/mesh/live/session-identity.js";
|
|
17
|
-
import { CLI_KIND_TO_SCHEMA,
|
|
17
|
+
import { CLI_KIND_TO_SCHEMA, resolveEnqueueSessionId, } from "../lib/mesh/live/index.js";
|
|
18
18
|
import { flushSessionEvents } from "../lib/mesh/live/flush.js";
|
|
19
19
|
import { backfillHeldSessions } from "../lib/mesh/live/backfill-held.js";
|
|
20
20
|
import { createSessionEventsPoster, resolveVaultApiBase, } from "../lib/mesh/live/session-events-client.js";
|
|
21
|
+
import { buildEmitEvidence } from "../lib/mesh/live/emit-evidence.js";
|
|
22
|
+
import { createEmitPoster, buildMeshEmitEvent } from "../lib/mesh/live/emit-client.js";
|
|
23
|
+
import { emitEvents } from "../lib/mesh/live/emit.js";
|
|
24
|
+
import { readEmitRetry, writeEmitRetry } from "../lib/mesh/live/emit-retry.js";
|
|
25
|
+
import { generateUlid } from "../lib/mesh/live/ulid.js";
|
|
26
|
+
import { readLegacyBacklog } from "../lib/mesh/live/emit-replay.js";
|
|
27
|
+
import { resolveMeshEmitMode, writeMeshConfigMode, } from "../lib/mesh/live/daemon/mode.js";
|
|
21
28
|
import { workMeshRoot } from "../lib/mesh/live/paths.js";
|
|
22
29
|
import { buildInstallPaths, collectDaemonDoctor, daemonServiceStatus, detectPlatform, formatDaemonDoctor, installDaemonService, readDaemonState, runMeshDaemon, uninstallDaemonService, daemonDir, } from "../lib/mesh/live/daemon/index.js";
|
|
23
30
|
import { workContextRoot } from "../lib/work-context/paths.js";
|
|
@@ -319,6 +326,9 @@ async function runContextBackfillHeld(opts) {
|
|
|
319
326
|
workContextRoot: root,
|
|
320
327
|
dryRun,
|
|
321
328
|
limit,
|
|
329
|
+
// Mirror the reconcile closure's non-forcing hint in the dry-run predictor
|
|
330
|
+
// so `--dry-run` and the real run report the same counts.
|
|
331
|
+
remoteOwnerHint: companyHint || undefined,
|
|
322
332
|
reconcile: (obs) => reconcileObservation(
|
|
323
333
|
// Pass --company as a non-forcing hint (remoteOwnerSlug feeds
|
|
324
334
|
// deterministic resolution BELOW the identity-file / device default,
|
|
@@ -725,9 +735,9 @@ function workMeshHomeRoot() {
|
|
|
725
735
|
return workMeshRoot(os.homedir(), process.env);
|
|
726
736
|
}
|
|
727
737
|
async function runSessionEnqueue(cliKind, opts) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
738
|
+
// `--enqueue` is retained for hook back-compat but now means "emit directly"
|
|
739
|
+
// (owner decision 2026-09-08: sessions POST events to the server over HTTPS;
|
|
740
|
+
// no local spool/daemon on the emit path).
|
|
731
741
|
const kind = CLI_KIND_TO_SCHEMA[cliKind];
|
|
732
742
|
if (!kind)
|
|
733
743
|
fail(`Unknown session verb: ${cliKind}`);
|
|
@@ -741,57 +751,155 @@ async function runSessionEnqueue(cliKind, opts) {
|
|
|
741
751
|
const seq = Number(opts.seq);
|
|
742
752
|
if (!Number.isInteger(seq) || seq < 1)
|
|
743
753
|
fail("--seq must be an integer >= 1");
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
754
|
+
const sessionId = resolveEnqueueSessionId(opts.sessionId, process.env);
|
|
755
|
+
if (!sessionId)
|
|
756
|
+
fail("sessionId required (--session-id or HQ_SESSION_ID)");
|
|
757
|
+
const evidence = buildEmitEvidence({
|
|
758
|
+
sessionId: sessionId,
|
|
759
|
+
hqRoot: opts.hqRoot,
|
|
760
|
+
cwd: opts.cwd,
|
|
761
|
+
touchedPaths: opts.touchedPath,
|
|
762
|
+
repoPath: opts.repoPath,
|
|
763
|
+
companySlug: opts.companySlug,
|
|
764
|
+
project: opts.project,
|
|
765
|
+
task: opts.task,
|
|
766
|
+
});
|
|
767
|
+
const event = buildMeshEmitEvent({
|
|
768
|
+
eventId: opts.eventId?.trim() || generateUlid(Date.now()),
|
|
769
|
+
kind: kind,
|
|
770
|
+
sessionId: sessionId,
|
|
771
|
+
harness,
|
|
772
|
+
adapterVersion,
|
|
773
|
+
at: opts.at?.trim() || new Date().toISOString(),
|
|
774
|
+
seq,
|
|
775
|
+
runtimeVersion: opts.runtimeVersion,
|
|
776
|
+
source: "hooks",
|
|
777
|
+
taskId: opts.taskId,
|
|
778
|
+
status: opts.status,
|
|
779
|
+
reason: opts.reason,
|
|
780
|
+
summary: opts.summary,
|
|
781
|
+
evidence,
|
|
782
|
+
});
|
|
783
|
+
const meshRoot = workMeshHomeRoot();
|
|
784
|
+
// Resolve a token non-interactively (same path as every CLI call). If the box
|
|
785
|
+
// is not logged in, retain the event locally for the next invocation to drain.
|
|
786
|
+
let token;
|
|
787
|
+
try {
|
|
788
|
+
token = await requireToken();
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
token = null;
|
|
792
|
+
}
|
|
793
|
+
if (!token) {
|
|
794
|
+
const pending = readEmitRetry(meshRoot);
|
|
795
|
+
const { written } = writeEmitRetry(meshRoot, [...pending, event]);
|
|
796
|
+
if (opts.json) {
|
|
797
|
+
console.log(JSON.stringify({ ok: true, action: "emit", kind, deferred: true, retryDepth: written }, null, 2));
|
|
798
|
+
return;
|
|
749
799
|
}
|
|
800
|
+
console.error(`work-mesh: no token; deferred ${kind} (retryDepth=${written})`);
|
|
801
|
+
return;
|
|
750
802
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
});
|
|
803
|
+
const poster = createEmitPoster({
|
|
804
|
+
token,
|
|
805
|
+
baseUrl: resolveVaultApiBase(process.env),
|
|
806
|
+
});
|
|
807
|
+
const summary = await emitEvents({
|
|
808
|
+
workMeshRoot: meshRoot,
|
|
809
|
+
poster,
|
|
810
|
+
newEvents: [event],
|
|
811
|
+
});
|
|
812
|
+
if (opts.json) {
|
|
813
|
+
console.log(JSON.stringify({ ok: true, action: "emit", kind, ...summary }, null, 2));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
console.error(`work-mesh: emit ${kind} accepted=${summary.accepted}` +
|
|
817
|
+
` unassigned=${summary.unassigned} rejected=${summary.rejected}` +
|
|
818
|
+
` retryDepth=${summary.retryDepth}`);
|
|
819
|
+
}
|
|
820
|
+
async function runMeshMode(action, opts) {
|
|
821
|
+
const meshRoot = workMeshHomeRoot();
|
|
822
|
+
const act = (action ?? "get").toLowerCase();
|
|
823
|
+
if (act === "get") {
|
|
824
|
+
const mode = resolveMeshEmitMode({ env: process.env, meshRoot });
|
|
774
825
|
if (opts.json) {
|
|
775
|
-
console.log(JSON.stringify({
|
|
776
|
-
ok: true,
|
|
777
|
-
action: "enqueue",
|
|
778
|
-
kind,
|
|
779
|
-
spoolPath: result.spoolPath,
|
|
780
|
-
eventId: result.event.eventId,
|
|
781
|
-
}, null, 2));
|
|
826
|
+
console.log(JSON.stringify({ ok: true, action: "mode", mode }, null, 2));
|
|
782
827
|
return;
|
|
783
828
|
}
|
|
784
|
-
|
|
785
|
-
|
|
829
|
+
console.log(`mesh emit mode: ${mode}`);
|
|
830
|
+
return;
|
|
786
831
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
832
|
+
if (act === "legacy" || act === "direct") {
|
|
833
|
+
writeMeshConfigMode(meshRoot, act);
|
|
834
|
+
const mode = resolveMeshEmitMode({ env: process.env, meshRoot });
|
|
835
|
+
if (opts.json) {
|
|
836
|
+
console.log(JSON.stringify({ ok: true, action: "mode", set: act, effective: mode }, null, 2));
|
|
791
837
|
return;
|
|
792
838
|
}
|
|
793
|
-
|
|
839
|
+
console.log(`mesh emit mode set to ${act}` +
|
|
840
|
+
(mode !== act
|
|
841
|
+
? ` (note: HQ_MESH_MODE=${process.env.HQ_MESH_MODE} overrides → ${mode})`
|
|
842
|
+
: "") +
|
|
843
|
+
`. Restart the daemon (hq mesh daemon run) to apply.`);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (act === "check") {
|
|
847
|
+
// Probe the server route with an empty batch: 2xx/400 => route live,
|
|
848
|
+
// 404 => not deployed yet (safe to stay legacy).
|
|
849
|
+
let token;
|
|
850
|
+
try {
|
|
851
|
+
token = await requireToken();
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const poster = createEmitPoster({
|
|
858
|
+
token,
|
|
859
|
+
baseUrl: resolveVaultApiBase(process.env),
|
|
860
|
+
});
|
|
861
|
+
const res = await poster([]);
|
|
862
|
+
const ready = res.status === 200 || res.status === 400;
|
|
863
|
+
if (opts.json) {
|
|
864
|
+
console.log(JSON.stringify({ ok: true, action: "mode", check: true, status: res.status, ready }, null, 2));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
console.log(`mesh route /v1/mesh/events: status=${res.status} ${ready ? "READY (safe to `hq mesh mode direct`)" : "not ready (stay legacy)"}`);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
fail(`Unknown mode action: ${act} (use get|legacy|direct|check)`);
|
|
871
|
+
}
|
|
872
|
+
async function runMeshEmit(opts) {
|
|
873
|
+
const token = await requireToken();
|
|
874
|
+
const meshRoot = workMeshHomeRoot();
|
|
875
|
+
const poster = createEmitPoster({
|
|
876
|
+
token,
|
|
877
|
+
baseUrl: resolveVaultApiBase(process.env),
|
|
878
|
+
});
|
|
879
|
+
let scanned = 0;
|
|
880
|
+
let skipped = 0;
|
|
881
|
+
let newEvents = [];
|
|
882
|
+
if (opts.replayLegacy) {
|
|
883
|
+
const backlog = readLegacyBacklog(meshRoot);
|
|
884
|
+
scanned = backlog.scanned;
|
|
885
|
+
skipped = backlog.skipped;
|
|
886
|
+
newEvents = backlog.events;
|
|
887
|
+
}
|
|
888
|
+
const summary = await emitEvents({ workMeshRoot: meshRoot, poster, newEvents });
|
|
889
|
+
const out = {
|
|
890
|
+
ok: true,
|
|
891
|
+
action: "emit",
|
|
892
|
+
...(opts.replayLegacy ? { replayLegacy: true, legacyScanned: scanned, legacySkipped: skipped } : {}),
|
|
893
|
+
...summary,
|
|
894
|
+
};
|
|
895
|
+
if (opts.json) {
|
|
896
|
+
console.log(JSON.stringify(out, null, 2));
|
|
897
|
+
return;
|
|
794
898
|
}
|
|
899
|
+
console.log(`work-mesh emit: attempted=${summary.attempted} accepted=${summary.accepted}` +
|
|
900
|
+
` unassigned=${summary.unassigned} rejected=${summary.rejected}` +
|
|
901
|
+
` retryDepth=${summary.retryDepth}` +
|
|
902
|
+
(opts.replayLegacy ? ` (legacy scanned=${scanned} skipped=${skipped})` : ""));
|
|
795
903
|
}
|
|
796
904
|
async function runSessionFlush(opts) {
|
|
797
905
|
const token = await requireToken();
|
|
@@ -829,10 +937,12 @@ function addSessionEnqueueFlags(cmd) {
|
|
|
829
937
|
.option("--summary <text>", "Short summary (max 280)")
|
|
830
938
|
.option("--cwd <path>", "Local-only working directory")
|
|
831
939
|
.option("--hq-root <path>", "Local-only HQ tree root")
|
|
832
|
-
.option("--company-slug <slug>", "
|
|
833
|
-
.option("--project <name>", "
|
|
834
|
-
.option("--task <label>", "
|
|
835
|
-
.option("--
|
|
940
|
+
.option("--company-slug <slug>", "Attribution evidence: bound company slug")
|
|
941
|
+
.option("--project <name>", "Attribution evidence: project id/slug")
|
|
942
|
+
.option("--task <label>", "Attribution evidence: task id/label")
|
|
943
|
+
.option("--touched-path <path>", "Attribution evidence: a file path this tool call touched (repeatable)", collectRepeatable)
|
|
944
|
+
.option("--repo-path <path>", "Attribution evidence: enclosing repo path")
|
|
945
|
+
.option("--tool-writes <n>", "Local-only tool write count (unused on emit)")
|
|
836
946
|
.option("--json", "Print machine-readable JSON");
|
|
837
947
|
}
|
|
838
948
|
function wrap(action) {
|
|
@@ -1012,6 +1122,20 @@ export function registerMeshCommand(program) {
|
|
|
1012
1122
|
.description("Claim spool/held by rename, hold or drop by context state, POST batches of ≤100")
|
|
1013
1123
|
.option("--json", "Print machine-readable JSON")
|
|
1014
1124
|
.action((opts) => wrap(() => runSessionFlush(opts))());
|
|
1125
|
+
mesh
|
|
1126
|
+
.command("mode")
|
|
1127
|
+
.description("Get/set the daemon emit mode (legacy = spool/flush; direct = receive-only). " +
|
|
1128
|
+
"`check` probes whether the server route is live.")
|
|
1129
|
+
.argument("[action]", "get | legacy | direct | check (default get)")
|
|
1130
|
+
.option("--json", "Print machine-readable JSON")
|
|
1131
|
+
.action((action, opts) => wrap(() => runMeshMode(action, opts))());
|
|
1132
|
+
mesh
|
|
1133
|
+
.command("emit")
|
|
1134
|
+
.description("Direct-emit drain: POST any deferred events (retry file) to /v1/mesh/events; " +
|
|
1135
|
+
"--replay-legacy also replays the legacy spool/held backlog through the new endpoint")
|
|
1136
|
+
.option("--replay-legacy", "Also replay spool.jsonl + held.jsonl with their evidence")
|
|
1137
|
+
.option("--json", "Print machine-readable JSON")
|
|
1138
|
+
.action((opts) => wrap(() => runMeshEmit(opts))());
|
|
1015
1139
|
session
|
|
1016
1140
|
.command("status")
|
|
1017
1141
|
.description("Print the company-wide live read (US-004) as a table or --json")
|
|
@@ -52,7 +52,7 @@ import { safeExtractTarball } from './safe-extract.js';
|
|
|
52
52
|
import { getCompanyUid, vaultApiFetch, vaultApiFetchPublic, } from '../utils/vault-api.js';
|
|
53
53
|
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
54
54
|
import { formatApiKeyCapabilityDenial, peekHqApiKey, } from '../utils/resolve-vault-credential.js';
|
|
55
|
-
import { redactSecrets, SECRET_REDACTION,
|
|
55
|
+
import { redactSecrets, SECRET_REDACTION, isMcpRegistrationEnabled, registerMcpServers, McpManifestError, } from './mcp-registration.js';
|
|
56
56
|
import { listSecretCacheScopes } from '../utils/secrets-cache.js';
|
|
57
57
|
import { loadRevealedSecrets } from './secrets.js';
|
|
58
58
|
const PACK_UPDATE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -1492,9 +1492,9 @@ async function wireMcpServers(pkg, destDir, company) {
|
|
|
1492
1492
|
const loadManifest = (name) => loadMcpManifestFrom(destDir, name);
|
|
1493
1493
|
const registered = [];
|
|
1494
1494
|
const skipped = [];
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
const
|
|
1495
|
+
// Resolve the kill-switch once and freeze it before the loop so an install
|
|
1496
|
+
// never writes only an arbitrary prefix of a pack's servers.
|
|
1497
|
+
const registrationEnabled = isMcpRegistrationEnabled();
|
|
1498
1498
|
for (const name of pkg.contributes.mcp ?? []) {
|
|
1499
1499
|
try {
|
|
1500
1500
|
const resolveSecret = await makeInstallSecretResolver(collectManifestSecretNames(loadManifest(name)), company);
|
|
@@ -1503,7 +1503,7 @@ async function wireMcpServers(pkg, destDir, company) {
|
|
|
1503
1503
|
const results = registerMcpServers(pkg.name, [name], {
|
|
1504
1504
|
loadManifest,
|
|
1505
1505
|
resolveSecret,
|
|
1506
|
-
|
|
1506
|
+
registrationEnabled,
|
|
1507
1507
|
});
|
|
1508
1508
|
if (results.length > 0)
|
|
1509
1509
|
registered.push(name);
|
|
@@ -3,6 +3,13 @@ import { writeSync } from "node:fs";
|
|
|
3
3
|
import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
|
|
4
4
|
export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
5
5
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
6
|
+
/**
|
|
7
|
+
* Mirrors `RESOURCE_SURFACES` in hq-pro
|
|
8
|
+
* `src/journey/limited-resource-telemetry.ts`. Keep this list in sync with
|
|
9
|
+
* the server so CLI telemetry cannot silently fall back to `api`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const HQ_PRO_RESOURCE_SURFACES: readonly ["api", "bot_invite", "bot_join_now", "calendar_auto_schedule", "calendar_preferences", "cli_secrets_set", "console_secrets_form", "deploy_precheck", "external_connection", "factory_install", "integrations_connect", "oauth_callback", "plan_limit_check", "secrets_input_link", "web"];
|
|
12
|
+
export declare const CLI_SECRETS_SET_SURFACE = "cli_secrets_set";
|
|
6
13
|
export type SecretTier = "standard" | "sensitive" | "nuclear";
|
|
7
14
|
export type SecretScriptLockMode = "off" | "enforced";
|
|
8
15
|
export type SecretUsageChannel = "run" | "exec" | "env" | "sandbox" | "reveal" | "submit-link";
|
package/dist/commands/secrets.js
CHANGED
|
@@ -12,6 +12,29 @@ import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from
|
|
|
12
12
|
import { HQ_API_KEY_PREFIX, formatApiKeyCapabilityDenial, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
|
|
13
13
|
import { SandboxRunnerClient, } from "../utils/sandbox-runner-client.js";
|
|
14
14
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
15
|
+
/**
|
|
16
|
+
* Mirrors `RESOURCE_SURFACES` in hq-pro
|
|
17
|
+
* `src/journey/limited-resource-telemetry.ts`. Keep this list in sync with
|
|
18
|
+
* the server so CLI telemetry cannot silently fall back to `api`.
|
|
19
|
+
*/
|
|
20
|
+
export const HQ_PRO_RESOURCE_SURFACES = [
|
|
21
|
+
"api",
|
|
22
|
+
"bot_invite",
|
|
23
|
+
"bot_join_now",
|
|
24
|
+
"calendar_auto_schedule",
|
|
25
|
+
"calendar_preferences",
|
|
26
|
+
"cli_secrets_set",
|
|
27
|
+
"console_secrets_form",
|
|
28
|
+
"deploy_precheck",
|
|
29
|
+
"external_connection",
|
|
30
|
+
"factory_install",
|
|
31
|
+
"integrations_connect",
|
|
32
|
+
"oauth_callback",
|
|
33
|
+
"plan_limit_check",
|
|
34
|
+
"secrets_input_link",
|
|
35
|
+
"web",
|
|
36
|
+
];
|
|
37
|
+
export const CLI_SECRETS_SET_SURFACE = "cli_secrets_set";
|
|
15
38
|
/**
|
|
16
39
|
* Cognito session for secrets surfaces hq-pro serves on the JWT routes only —
|
|
17
40
|
* the capability would apply, but there is no Bearer `hqk_` route (no HEAD
|
|
@@ -829,9 +852,10 @@ export function registerSecretsCommand(program) {
|
|
|
829
852
|
body: {
|
|
830
853
|
name,
|
|
831
854
|
value,
|
|
855
|
+
surface: CLI_SECRETS_SET_SURFACE,
|
|
832
856
|
// Only present when --high-security was passed — an ordinary
|
|
833
|
-
// `set` with no flags sends
|
|
834
|
-
//
|
|
857
|
+
// `set` with no flags sends only its name/value plus this additive
|
|
858
|
+
// telemetry attribution field.
|
|
835
859
|
...(opts.highSecurity ? { highSecurity: true } : {}),
|
|
836
860
|
...(destinations ? { destinations } : {}),
|
|
837
861
|
...(injection ? { injection } : {}),
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* in package.json — revert that line to `^5.20.0` (or whatever the
|
|
31
31
|
* published cut is) once US-004 ships to npm.
|
|
32
32
|
*/
|
|
33
|
+
import { isAccessDeniedError, accessDeniedKeyOf, reportAccessDenied, accessDeniedCompanyOf, } from "../utils/access-denied-hint.js";
|
|
33
34
|
import chalk from "chalk";
|
|
34
35
|
import * as fs from "node:fs";
|
|
35
36
|
import * as path from "node:path";
|
|
@@ -242,7 +243,17 @@ export function registerSyncModeCommand(syncCmd) {
|
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
catch (err) {
|
|
245
|
-
|
|
246
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
247
|
+
if (isAccessDeniedError(err)) {
|
|
248
|
+
await reportAccessDenied(chalk.red("✗ sync mode failed:") + " " + message, accessDeniedKeyOf(err) ??
|
|
249
|
+
(options.company ? `companies/${options.company}` : undefined), {
|
|
250
|
+
company: accessDeniedCompanyOf(err) ?? options.company,
|
|
251
|
+
hqRoot: options.hqRoot,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
console.error(chalk.red("✗ sync mode failed:"), message);
|
|
256
|
+
}
|
|
246
257
|
process.exit(1);
|
|
247
258
|
}
|
|
248
259
|
});
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* release is unpublished, hq-cli pins `@indigoai-us/hq-cloud` to
|
|
37
37
|
* `file:../hq-cloud` via `pnpm.overrides`.
|
|
38
38
|
*/
|
|
39
|
+
import { isAccessDeniedError, accessDeniedKeyOf, reportAccessDenied, accessDeniedCompanyOf, } from "../utils/access-denied-hint.js";
|
|
39
40
|
import chalk from "chalk";
|
|
40
41
|
import * as readline from "node:readline";
|
|
41
42
|
import * as fs from "node:fs";
|
|
@@ -327,7 +328,17 @@ export function registerSyncNarrowCommand(syncCmd) {
|
|
|
327
328
|
console.log(chalk.dim(` audit: server-side MEMBERSHIP_SYNC_CONFIG_CHANGED row written by PUT /v1/memberships/${target.membership.membershipKey}/sync-config`));
|
|
328
329
|
}
|
|
329
330
|
catch (err) {
|
|
330
|
-
|
|
331
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
332
|
+
if (isAccessDeniedError(err)) {
|
|
333
|
+
await reportAccessDenied(chalk.red("✗ sync narrow failed:") + " " + message, accessDeniedKeyOf(err) ??
|
|
334
|
+
(options.company ? `companies/${options.company}` : undefined), {
|
|
335
|
+
company: accessDeniedCompanyOf(err) ?? options.company,
|
|
336
|
+
hqRoot: options.hqRoot,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
console.error(chalk.red("✗ sync narrow failed:"), message);
|
|
341
|
+
}
|
|
331
342
|
process.exit(1);
|
|
332
343
|
}
|
|
333
344
|
});
|
|
@@ -21,10 +21,28 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { ReconcileObservation, ReconcileOutcome } from "../../work-context/reconcile.js";
|
|
23
23
|
import { type SessionStateFile } from "../../work-context/state.js";
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* One distinct session discovered in held.jsonl, carrying the company evidence
|
|
26
|
+
* its held events recorded at enqueue time (cwd / hqRoot / companySlug /
|
|
27
|
+
* project / task). ENDED sessions never reconcile again, so this per-event
|
|
28
|
+
* evidence is the only company signal a backfill can use — the synthesized
|
|
29
|
+
* observation forwards it so the shared resolver (cwd → companies/{slug}/,
|
|
30
|
+
* meta.yaml company_slug via hqRoot, or the event's own bound companySlug)
|
|
31
|
+
* can attribute the backlog instead of holding it forever.
|
|
32
|
+
*/
|
|
25
33
|
export interface HeldSessionRef {
|
|
26
34
|
sessionId: string;
|
|
27
35
|
harness?: string;
|
|
36
|
+
/** Working directory the events were emitted from (deterministic company/project evidence). */
|
|
37
|
+
cwd?: string;
|
|
38
|
+
/** HQ root, so the resolver can read workspace/sessions/<sid>/meta.yaml. */
|
|
39
|
+
hqRoot?: string;
|
|
40
|
+
/** Company the session was bound to at emit time (enqueue --company-slug). */
|
|
41
|
+
companySlug?: string;
|
|
42
|
+
/** Project id carried on the event, if any. */
|
|
43
|
+
project?: string;
|
|
44
|
+
/** Task id carried on the event, if any. */
|
|
45
|
+
task?: string;
|
|
28
46
|
}
|
|
29
47
|
export interface BackfillHeldResult {
|
|
30
48
|
/** Distinct sessions found in held.jsonl (before --limit). */
|
|
@@ -63,9 +81,32 @@ export interface BackfillHeldDeps {
|
|
|
63
81
|
* `hq mesh context reconcile` handler.
|
|
64
82
|
*/
|
|
65
83
|
reconcile: (obs: ReconcileObservation) => Promise<ReconcileOutcome>;
|
|
84
|
+
/**
|
|
85
|
+
* Non-forcing company hint (the `--company` flag). Fed to the resolver as
|
|
86
|
+
* deterministic remote-owner evidence (below identity / explicit / session
|
|
87
|
+
* meta) in BOTH the dry-run predictor and the write path, so the two agree.
|
|
88
|
+
*/
|
|
89
|
+
remoteOwnerHint?: string;
|
|
90
|
+
/** Env passed to the dry-run resolver (tests). Default process.env. */
|
|
91
|
+
env?: NodeJS.ProcessEnv;
|
|
92
|
+
/**
|
|
93
|
+
* Read-only company predictor for --dry-run. MUST mirror what the write
|
|
94
|
+
* path's reconcile would resolve, so dry-run and write report the same
|
|
95
|
+
* counts (the earlier bug: dry-run counted every session as would-reconcile
|
|
96
|
+
* while the write path, given no evidence, resolved none). Default runs the
|
|
97
|
+
* shared `resolveCompany` over the same reconstructed evidence.
|
|
98
|
+
*/
|
|
99
|
+
predictCompany?: (ref: HeldSessionRef) => boolean;
|
|
66
100
|
/** Optional progress logger. */
|
|
67
101
|
log?: (message: string) => void;
|
|
68
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Build the reconcile observation for one held session, forwarding the company
|
|
105
|
+
* evidence its events recorded (cwd / hqRoot / bound companySlug / project /
|
|
106
|
+
* task). The event's own companySlug is trusted explicit — it is the company
|
|
107
|
+
* the session was bound to when the events were emitted on this box.
|
|
108
|
+
*/
|
|
109
|
+
export declare function observationFromHeldRef(ref: HeldSessionRef, contractVersion: number, clientOperationId: string): ReconcileObservation;
|
|
69
110
|
/**
|
|
70
111
|
* Read held.jsonl and return distinct sessions (first occurrence wins),
|
|
71
112
|
* carrying the harness from whichever held event we saw first for that session.
|