@indigoai-us/hq-cli 5.108.25 → 5.108.26
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 +68 -0
- 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/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/package.json +1 -1
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 } : {}),
|
|
@@ -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.
|
|
@@ -22,7 +22,65 @@
|
|
|
22
22
|
import * as fs from "node:fs";
|
|
23
23
|
import { readSessionState } from "../../work-context/state.js";
|
|
24
24
|
import { WORK_CONTEXT_CONTRACT_VERSION } from "../../work-context/contract.js";
|
|
25
|
+
import { resolveCompany } from "../../work-context/company.js";
|
|
26
|
+
import { deriveRemoteOwnerSlug, deriveRepoIdentityKey, } from "../../work-context/repo-remote.js";
|
|
25
27
|
import { workMeshHeldPath } from "./paths.js";
|
|
28
|
+
/**
|
|
29
|
+
* Build the reconcile observation for one held session, forwarding the company
|
|
30
|
+
* evidence its events recorded (cwd / hqRoot / bound companySlug / project /
|
|
31
|
+
* task). The event's own companySlug is trusted explicit — it is the company
|
|
32
|
+
* the session was bound to when the events were emitted on this box.
|
|
33
|
+
*/
|
|
34
|
+
export function observationFromHeldRef(ref, contractVersion, clientOperationId) {
|
|
35
|
+
const obs = {
|
|
36
|
+
contractVersion,
|
|
37
|
+
identity: ref.harness
|
|
38
|
+
? { sessionId: ref.sessionId, harness: ref.harness }
|
|
39
|
+
: { sessionId: ref.sessionId },
|
|
40
|
+
clientOperationId,
|
|
41
|
+
};
|
|
42
|
+
if (ref.cwd)
|
|
43
|
+
obs.cwd = ref.cwd;
|
|
44
|
+
if (ref.hqRoot)
|
|
45
|
+
obs.hqRoot = ref.hqRoot;
|
|
46
|
+
if (ref.companySlug || ref.project || ref.task) {
|
|
47
|
+
obs.explicit = {
|
|
48
|
+
...(ref.companySlug ? { companySlug: ref.companySlug } : {}),
|
|
49
|
+
...(ref.project ? { projectId: ref.project } : {}),
|
|
50
|
+
...(ref.task ? { taskId: ref.task } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return obs;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Default dry-run predictor: run the shared `resolveCompany` over the same
|
|
57
|
+
* evidence the write path forwards. Returns true when a company WOULD resolve
|
|
58
|
+
* (so dry-run's `reconciled` count matches the write run's). Purely local:
|
|
59
|
+
* reads config / meta / git-remote, never the network. A slug-only resolution
|
|
60
|
+
* that later fails online membership validation is the one narrow case where
|
|
61
|
+
* the write run can still under-count; the gross all-or-nothing mismatch is
|
|
62
|
+
* gone because both paths key off this same resolver + evidence.
|
|
63
|
+
*/
|
|
64
|
+
function defaultPredictCompany(ref, workContextRoot, remoteOwnerHint, env) {
|
|
65
|
+
const remoteOwnerSlug = remoteOwnerHint?.trim() ||
|
|
66
|
+
(ref.cwd
|
|
67
|
+
? deriveRemoteOwnerSlug({ cwd: ref.cwd, hqRoot: ref.hqRoot }) ?? undefined
|
|
68
|
+
: undefined);
|
|
69
|
+
const repoIdentityKey = ref.cwd
|
|
70
|
+
? deriveRepoIdentityKey({ cwd: ref.cwd })
|
|
71
|
+
: null;
|
|
72
|
+
const resolution = resolveCompany({
|
|
73
|
+
root: workContextRoot,
|
|
74
|
+
sessionId: ref.sessionId,
|
|
75
|
+
env,
|
|
76
|
+
trusted: ref.companySlug ? { companySlug: ref.companySlug } : undefined,
|
|
77
|
+
cwd: ref.cwd,
|
|
78
|
+
hqRoot: ref.hqRoot,
|
|
79
|
+
remoteOwnerSlug,
|
|
80
|
+
repoIdentityKey,
|
|
81
|
+
});
|
|
82
|
+
return resolution.status === "resolved";
|
|
83
|
+
}
|
|
26
84
|
/**
|
|
27
85
|
* Read held.jsonl and return distinct sessions (first occurrence wins),
|
|
28
86
|
* carrying the harness from whichever held event we saw first for that session.
|
|
@@ -65,10 +123,27 @@ export function readHeldSessions(workMeshRoot) {
|
|
|
65
123
|
: null;
|
|
66
124
|
if (!sessionId || seen.has(sessionId))
|
|
67
125
|
continue;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
126
|
+
const str = (v) => typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
127
|
+
const ref = { sessionId };
|
|
128
|
+
const harness = str(event.harness);
|
|
129
|
+
if (harness)
|
|
130
|
+
ref.harness = harness;
|
|
131
|
+
const cwd = str(event.cwd);
|
|
132
|
+
if (cwd)
|
|
133
|
+
ref.cwd = cwd;
|
|
134
|
+
const hqRoot = str(event.hqRoot);
|
|
135
|
+
if (hqRoot)
|
|
136
|
+
ref.hqRoot = hqRoot;
|
|
137
|
+
const companySlug = str(event.companySlug);
|
|
138
|
+
if (companySlug)
|
|
139
|
+
ref.companySlug = companySlug;
|
|
140
|
+
const project = str(event.project);
|
|
141
|
+
if (project)
|
|
142
|
+
ref.project = project;
|
|
143
|
+
const task = str(event.task);
|
|
144
|
+
if (task)
|
|
145
|
+
ref.task = task;
|
|
146
|
+
seen.set(sessionId, ref);
|
|
72
147
|
}
|
|
73
148
|
return [...seen.values()];
|
|
74
149
|
}
|
|
@@ -81,7 +156,10 @@ export async function backfillHeldSessions(deps) {
|
|
|
81
156
|
const limit = deps.limit && deps.limit > 0 ? deps.limit : 0;
|
|
82
157
|
const contractVersion = deps.contractVersion ?? WORK_CONTEXT_CONTRACT_VERSION;
|
|
83
158
|
const readState = deps.readState ?? readSessionState;
|
|
159
|
+
const env = deps.env ?? process.env;
|
|
84
160
|
const newOperationId = deps.newOperationId ?? (() => globalThis.crypto.randomUUID());
|
|
161
|
+
const predictCompany = deps.predictCompany ??
|
|
162
|
+
((ref) => defaultPredictCompany(ref, deps.workContextRoot, deps.remoteOwnerHint, env));
|
|
85
163
|
const sessions = readHeldSessions(deps.workMeshRoot);
|
|
86
164
|
const considered = limit > 0 ? sessions.slice(0, limit) : sessions;
|
|
87
165
|
const result = {
|
|
@@ -93,23 +171,27 @@ export async function backfillHeldSessions(deps) {
|
|
|
93
171
|
errors: 0,
|
|
94
172
|
dryRun,
|
|
95
173
|
};
|
|
96
|
-
for (const
|
|
174
|
+
for (const ref of considered) {
|
|
175
|
+
const { sessionId, harness } = ref;
|
|
97
176
|
const state = readState(sessionId, deps.workContextRoot);
|
|
98
177
|
if (state?.companyUid) {
|
|
99
178
|
result.alreadyAttributed += 1;
|
|
100
179
|
continue;
|
|
101
180
|
}
|
|
102
181
|
if (dryRun) {
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
182
|
+
// Predict with the SAME evidence + resolver the write path uses, so the
|
|
183
|
+
// dry-run counts match a real run (no blanket would-reconcile).
|
|
184
|
+
if (predictCompany(ref)) {
|
|
185
|
+
result.reconciled += 1;
|
|
186
|
+
deps.log?.(`would reconcile ${sessionId}${harness ? ` (${harness})` : ""}`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
result.unresolved += 1;
|
|
190
|
+
deps.log?.(`would stay unresolved ${sessionId} (no company evidence)`);
|
|
191
|
+
}
|
|
106
192
|
continue;
|
|
107
193
|
}
|
|
108
|
-
const obs =
|
|
109
|
-
contractVersion,
|
|
110
|
-
identity: harness ? { sessionId, harness } : { sessionId },
|
|
111
|
-
clientOperationId: newOperationId(),
|
|
112
|
-
};
|
|
194
|
+
const obs = observationFromHeldRef(ref, contractVersion, newOperationId());
|
|
113
195
|
try {
|
|
114
196
|
const outcome = await deps.reconcile(obs);
|
|
115
197
|
if (outcome.result.companyUid) {
|
|
@@ -10,6 +10,10 @@ export interface DaemonDoctorReport {
|
|
|
10
10
|
pid?: number;
|
|
11
11
|
mqttState: string;
|
|
12
12
|
companiesOnline: string[];
|
|
13
|
+
/** Daemon emit mode: "legacy" (spool/flush) or "direct" (receive-only). */
|
|
14
|
+
emitMode: "legacy" | "direct";
|
|
15
|
+
/** True when the receive subscriber is connected (mqtt connected). */
|
|
16
|
+
subscribed: boolean;
|
|
13
17
|
spoolDepth: number;
|
|
14
18
|
heldCount: number;
|
|
15
19
|
deadLetterCount: number;
|
|
@@ -25,6 +29,17 @@ export interface DaemonDoctorReport {
|
|
|
25
29
|
actorKind: CognitoActorKind;
|
|
26
30
|
/** Credential vend refused (disabled / unsupported / 403). */
|
|
27
31
|
presenceRefusal?: DaemonStateFile["presenceRefusal"];
|
|
32
|
+
/** Pending events awaiting a retry drain (POST /v1/mesh/events). */
|
|
33
|
+
emitRetryDepth: number;
|
|
34
|
+
/** Last successful direct-emit POST time. */
|
|
35
|
+
lastPostAt?: string;
|
|
36
|
+
/** Per-status counts from the last emit attempt. */
|
|
37
|
+
lastEmit?: {
|
|
38
|
+
accepted?: number;
|
|
39
|
+
unassigned?: number;
|
|
40
|
+
rejected?: number;
|
|
41
|
+
error?: string;
|
|
42
|
+
};
|
|
28
43
|
}
|
|
29
44
|
export interface DaemonDoctorDeps {
|
|
30
45
|
home?: string;
|