@indigoai-us/hq-cli 5.345.155 → 5.345.159
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 +289 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +536 -7
- package/dist/command-catalog.generated.js +676 -24
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-inbox.js +2 -2
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +13 -1
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files-browse.d.ts +16 -0
- package/dist/commands/files-browse.js +52 -1
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/integrations-core.d.ts +13 -1
- package/dist/commands/integrations-core.js +49 -4
- package/dist/commands/integrations.js +4 -2
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +185 -16
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
- package/dist/lib/agent-kit/inbox-state.js +5 -1
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
- package/dist/lib/doctor/checks/sync-health.js +66 -1
- package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
- package/dist/lib/doctor/company-resolution-state.js +89 -0
- package/dist/lib/doctor/fix/apply.d.ts +36 -6
- package/dist/lib/doctor/fix/apply.js +128 -12
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.js +6 -2
- package/dist/lib/index-render/company-knowledge.d.ts +1 -1
- package/dist/lib/index-render/company-knowledge.js +4 -5
- package/dist/lib/index-render/index.js +44 -3
- package/dist/lib/index-render/projects.d.ts +1 -1
- package/dist/lib/index-render/projects.js +5 -5
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/access-denied-hint.js +6 -0
- package/dist/utils/client-health.d.ts +34 -0
- package/dist/utils/client-health.js +96 -3
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level allowlists: `[[allowlists]]` (and the legacy `[allowlist]`) from
|
|
3
|
+
* the scanned repo's own `.gitleaks.toml`, with gitleaks semantics for
|
|
4
|
+
* `paths`, `regexes`, `regexTarget`, `stopwords`, `commits`, `targetRules` and
|
|
5
|
+
* `condition`. They apply on top of the built-in rules and allowlists, to that
|
|
6
|
+
* repo only. Repo `[[rules]]`, `[extend]` and the rest of the file are ignored:
|
|
7
|
+
* a repo can allow known false positives, not change what is detected.
|
|
8
|
+
*
|
|
9
|
+
* The file is read from git, never the working tree:
|
|
10
|
+
* - The staged (index) copy applies only when it is identical to the copy
|
|
11
|
+
* already committed at HEAD (or at MERGE_HEAD during a merge). A commit
|
|
12
|
+
* that changes the allowlist is scanned with the HEAD allowlist, so
|
|
13
|
+
* widening the allowlist and adding the secret cannot happen in one commit.
|
|
14
|
+
* - A changed staged copy is still validated, so a broken file fails now.
|
|
15
|
+
* - A symlinked .gitleaks.toml is rejected.
|
|
16
|
+
*
|
|
17
|
+
* Narrow by construction: every allowlist needs `paths`, each anchored with a
|
|
18
|
+
* literal prefix (`^golden/...`); `regexes` and `stopwords` need
|
|
19
|
+
* `condition = "AND"`, so they never reach files outside those paths.
|
|
20
|
+
*
|
|
21
|
+
* Fails closed: anything not acceptable is a ScanConfigError, and the scan
|
|
22
|
+
* exits 2 rather than running with a guess.
|
|
23
|
+
*/
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { parse } from "smol-toml";
|
|
26
|
+
import { compileGoRegex } from "./go-regex.js";
|
|
27
|
+
import { compileAllowlist } from "./rules.js";
|
|
28
|
+
export class ScanConfigError extends Error {
|
|
29
|
+
}
|
|
30
|
+
export const REPO_CONFIG_FILE = ".gitleaks.toml";
|
|
31
|
+
const KEYS = new Set(["description", "condition", "regexTarget", "paths", "regexes", "stopwords", "commits", "targetRules"]);
|
|
32
|
+
const MIN_PATH_PREFIX = 3;
|
|
33
|
+
const MIN_STOPWORD = 3;
|
|
34
|
+
/** User regexes run on JavaScript's backtracking engine, not RE2. */
|
|
35
|
+
const MAX_REGEX_LENGTH = 256;
|
|
36
|
+
/** A quantified group that itself contains a quantifier, e.g. (a+)+ or (?:x*y)*. */
|
|
37
|
+
const NESTED_QUANTIFIER = /\((?:[^()\\]|\\.)*[*+}](?:[^()\\]|\\.)*\)[*+{]/;
|
|
38
|
+
/** Paths no real allowlist path should match all of. */
|
|
39
|
+
const PROBE_PATHS = ["a", ".env", "src/index.ts", "config/credentials.yml", "deep/nested/dir/file.json"];
|
|
40
|
+
function fail(file, where, msg) {
|
|
41
|
+
throw new ScanConfigError(`${file}: ${where}: ${msg}`);
|
|
42
|
+
}
|
|
43
|
+
function stringList(file, where, key, v) {
|
|
44
|
+
if (v === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string"))
|
|
47
|
+
fail(file, where, `${key} must be a list of strings`);
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Length of the literal text right after a leading `^`: letters, digits,
|
|
52
|
+
* `/ _ - @ ~` and backslash-escaped punctuation. A quantifier on the last
|
|
53
|
+
* literal makes it optional, so it does not count.
|
|
54
|
+
*/
|
|
55
|
+
export function literalPathPrefix(src) {
|
|
56
|
+
if (!src.startsWith("^"))
|
|
57
|
+
return 0;
|
|
58
|
+
let n = 0;
|
|
59
|
+
let i = 1;
|
|
60
|
+
while (i < src.length) {
|
|
61
|
+
const ch = src[i];
|
|
62
|
+
if (/[A-Za-z0-9/_\-@~]/.test(ch)) {
|
|
63
|
+
n++;
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
else if (ch === "\\" && i + 1 < src.length && /[^A-Za-z0-9]/.test(src[i + 1])) {
|
|
67
|
+
n++;
|
|
68
|
+
i += 2;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (n > 0 && /^[*?{]/.test(src.slice(i)))
|
|
75
|
+
n--;
|
|
76
|
+
return n;
|
|
77
|
+
}
|
|
78
|
+
/** True when `src` has a `|` outside any group, which would bypass the `^` prefix. */
|
|
79
|
+
function topLevelAlternation(src) {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let inClass = false;
|
|
82
|
+
for (let i = 0; i < src.length; i++) {
|
|
83
|
+
const ch = src[i];
|
|
84
|
+
if (ch === "\\")
|
|
85
|
+
i++;
|
|
86
|
+
else if (inClass)
|
|
87
|
+
inClass = ch !== "]";
|
|
88
|
+
else if (ch === "[")
|
|
89
|
+
inClass = true;
|
|
90
|
+
else if (ch === "(")
|
|
91
|
+
depth++;
|
|
92
|
+
else if (ch === ")")
|
|
93
|
+
depth--;
|
|
94
|
+
else if (ch === "|" && depth === 0)
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
function compileChecked(file, where, key, src) {
|
|
100
|
+
if (src.length > MAX_REGEX_LENGTH)
|
|
101
|
+
fail(file, where, `${key} entry is longer than ${MAX_REGEX_LENGTH} characters`);
|
|
102
|
+
if (NESTED_QUANTIFIER.test(src))
|
|
103
|
+
fail(file, where, `${key} entry ${JSON.stringify(src)} has a nested quantifier; simplify it`);
|
|
104
|
+
try {
|
|
105
|
+
return compileGoRegex(src);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
fail(file, where, `${key} entry ${JSON.stringify(src)} is not a supported regex (${err instanceof Error ? err.message : String(err)})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function validate(file, where, raw) {
|
|
112
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
113
|
+
fail(file, where, "must be a table");
|
|
114
|
+
const t = raw;
|
|
115
|
+
for (const k of Object.keys(t))
|
|
116
|
+
if (!KEYS.has(k))
|
|
117
|
+
fail(file, where, `unknown key "${k}"`);
|
|
118
|
+
if (t.description !== undefined && typeof t.description !== "string")
|
|
119
|
+
fail(file, where, "description must be a string");
|
|
120
|
+
let condition;
|
|
121
|
+
if (t.condition !== undefined) {
|
|
122
|
+
const c = typeof t.condition === "string" ? t.condition.trim().toUpperCase() : "";
|
|
123
|
+
if (c !== "OR" && c !== "AND")
|
|
124
|
+
fail(file, where, 'condition must be "AND" or "OR"');
|
|
125
|
+
condition = c;
|
|
126
|
+
}
|
|
127
|
+
if (t.regexTarget !== undefined && t.regexTarget !== "secret" && t.regexTarget !== "match" && t.regexTarget !== "line") {
|
|
128
|
+
fail(file, where, 'regexTarget must be "secret", "match" or "line"');
|
|
129
|
+
}
|
|
130
|
+
const al = {
|
|
131
|
+
condition,
|
|
132
|
+
regexTarget: t.regexTarget,
|
|
133
|
+
paths: stringList(file, where, "paths", t.paths),
|
|
134
|
+
regexes: stringList(file, where, "regexes", t.regexes),
|
|
135
|
+
stopwords: stringList(file, where, "stopwords", t.stopwords),
|
|
136
|
+
commits: stringList(file, where, "commits", t.commits),
|
|
137
|
+
targetRules: stringList(file, where, "targetRules", t.targetRules),
|
|
138
|
+
};
|
|
139
|
+
// A repo may narrow what is reported, never switch the scanner off: every
|
|
140
|
+
// allowlist is scoped to files named by an anchored literal prefix.
|
|
141
|
+
if (!al.paths?.length) {
|
|
142
|
+
fail(file, where, "needs paths, each starting with ^ and a literal directory or file name (e.g. '^fixtures/')");
|
|
143
|
+
}
|
|
144
|
+
for (const src of al.paths) {
|
|
145
|
+
const re = compileChecked(file, where, "paths", src);
|
|
146
|
+
if (literalPathPrefix(src) < MIN_PATH_PREFIX || topLevelAlternation(src)) {
|
|
147
|
+
fail(file, where, `paths entry ${JSON.stringify(src)} must start with ^ and at least ${MIN_PATH_PREFIX} literal characters (e.g. '^fixtures/')`);
|
|
148
|
+
}
|
|
149
|
+
if (re.test("") || PROBE_PATHS.every((p) => re.test(p))) {
|
|
150
|
+
fail(file, where, `paths entry ${JSON.stringify(src)} matches everything; allowlist specific files`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if ((al.regexes?.length || al.stopwords?.length) && al.condition !== "AND") {
|
|
154
|
+
fail(file, where, 'regexes and stopwords need condition = "AND", so they only apply inside paths');
|
|
155
|
+
}
|
|
156
|
+
for (const src of al.regexes ?? []) {
|
|
157
|
+
if (compileChecked(file, where, "regexes", src).test(""))
|
|
158
|
+
fail(file, where, `regexes entry ${JSON.stringify(src)} matches everything`);
|
|
159
|
+
}
|
|
160
|
+
for (const s of al.stopwords ?? []) {
|
|
161
|
+
if (s.trim().length < MIN_STOPWORD)
|
|
162
|
+
fail(file, where, `stopword ${JSON.stringify(s)} is shorter than ${MIN_STOPWORD} characters`);
|
|
163
|
+
}
|
|
164
|
+
return al;
|
|
165
|
+
}
|
|
166
|
+
/** Validated allowlists from the text of a .gitleaks.toml. */
|
|
167
|
+
export function parseRepoAllowlists(text, file = REPO_CONFIG_FILE) {
|
|
168
|
+
let doc;
|
|
169
|
+
try {
|
|
170
|
+
doc = parse(text);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
throw new ScanConfigError(`${file}: could not be parsed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
174
|
+
}
|
|
175
|
+
const out = [];
|
|
176
|
+
if (doc.allowlist !== undefined)
|
|
177
|
+
out.push(validate(file, "[allowlist]", doc.allowlist));
|
|
178
|
+
if (doc.allowlists !== undefined) {
|
|
179
|
+
if (!Array.isArray(doc.allowlists))
|
|
180
|
+
fail(file, "allowlists", "must be an array of tables ([[allowlists]])");
|
|
181
|
+
doc.allowlists.forEach((a, i) => out.push(validate(file, `[[allowlists]] #${i + 1}`, a)));
|
|
182
|
+
}
|
|
183
|
+
return out.map(compileAllowlist);
|
|
184
|
+
}
|
|
185
|
+
function git(repoRoot, args) {
|
|
186
|
+
try {
|
|
187
|
+
return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** `<mode> <type> <blob>\t<path>` (ls-tree, blobField 2) or `<mode> <blob> <stage>\t<path>` (ls-files -s, blobField 1). */
|
|
194
|
+
function entry(out, blobField) {
|
|
195
|
+
const line = out?.split("\n").find(Boolean);
|
|
196
|
+
if (!line)
|
|
197
|
+
return null;
|
|
198
|
+
const f = line.split("\t")[0].split(" ");
|
|
199
|
+
return { mode: f[0], blob: f[blobField] };
|
|
200
|
+
}
|
|
201
|
+
function readEntry(repoRoot, e, where) {
|
|
202
|
+
if (e.mode === "120000")
|
|
203
|
+
throw new ScanConfigError(`${REPO_CONFIG_FILE} (${where}) is a symlink; commit a regular file`);
|
|
204
|
+
const text = git(repoRoot, ["cat-file", "blob", e.blob]);
|
|
205
|
+
if (text === null)
|
|
206
|
+
throw new ScanConfigError(`${REPO_CONFIG_FILE} (${where}) could not be read from git`);
|
|
207
|
+
return text;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Compiled allowlists for a staged scan of the repo at `repoRoot`, read from
|
|
211
|
+
* git as described at the top of this file. [] when the repo has none.
|
|
212
|
+
*/
|
|
213
|
+
export function loadRepoAllowlists(repoRoot) {
|
|
214
|
+
const lsFiles = git(repoRoot, ["ls-files", "-s", "--", REPO_CONFIG_FILE]);
|
|
215
|
+
// Only a resolved (stage 0) index entry counts. A conflicted .gitleaks.toml
|
|
216
|
+
// has stages 1-3 and no stage 0; it is not trusted and the HEAD copy is used.
|
|
217
|
+
const stage0 = lsFiles?.split("\n").find((l) => l.split("\t")[0].split(" ")[2] === "0") ?? null;
|
|
218
|
+
const staged = entry(stage0, 1);
|
|
219
|
+
if (!staged)
|
|
220
|
+
return lsFiles?.trim() ? headAllowlists(repoRoot) : [];
|
|
221
|
+
const stagedLists = parseRepoAllowlists(readEntry(repoRoot, staged, "staged"));
|
|
222
|
+
const committed = ["HEAD", "MERGE_HEAD"]
|
|
223
|
+
.filter((rev) => git(repoRoot, ["rev-parse", "--verify", "-q", `${rev}^{commit}`]) !== null)
|
|
224
|
+
.map((rev) => ({ rev, e: entry(git(repoRoot, ["ls-tree", rev, "--", REPO_CONFIG_FILE]), 2) }))
|
|
225
|
+
.filter((c) => c.e !== null);
|
|
226
|
+
if (committed.some((c) => c.e.blob === staged.blob && c.e.mode === staged.mode))
|
|
227
|
+
return stagedLists;
|
|
228
|
+
// Changed in this commit: scan with the version already committed at HEAD.
|
|
229
|
+
return headAllowlists(repoRoot);
|
|
230
|
+
}
|
|
231
|
+
function headAllowlists(repoRoot) {
|
|
232
|
+
if (git(repoRoot, ["rev-parse", "--verify", "-q", "HEAD^{commit}"]) === null)
|
|
233
|
+
return [];
|
|
234
|
+
const head = entry(git(repoRoot, ["ls-tree", "HEAD", "--", REPO_CONFIG_FILE]), 2);
|
|
235
|
+
return head ? parseRepoAllowlists(readEntry(repoRoot, head, "HEAD"), `${REPO_CONFIG_FILE} (HEAD)`) : [];
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=repo-config.js.map
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* Compiled rule set: the vendored gitleaks default rules plus HQ's own token
|
|
3
3
|
* patterns (src/lib/core-utils/secret-patterns.ts). Compiled once per process.
|
|
4
4
|
*/
|
|
5
|
-
import type { GitleaksRule } from "./types.js";
|
|
5
|
+
import type { GitleaksAllowlist, GitleaksRule } from "./types.js";
|
|
6
6
|
export interface CompiledAllowlist {
|
|
7
7
|
condition: "OR" | "AND";
|
|
8
8
|
target: "secret" | "match" | "line";
|
|
9
9
|
paths: RegExp[];
|
|
10
10
|
regexes: RegExp[];
|
|
11
11
|
stopwords: string[];
|
|
12
|
+
commits: string[];
|
|
13
|
+
/** Empty: applies to every rule. */
|
|
14
|
+
targetRules: string[];
|
|
12
15
|
}
|
|
13
16
|
export interface CompiledRule {
|
|
14
17
|
id: string;
|
|
@@ -24,7 +27,10 @@ export interface CompiledRule {
|
|
|
24
27
|
export interface CompiledRuleSet {
|
|
25
28
|
global: CompiledAllowlist;
|
|
26
29
|
rules: CompiledRule[];
|
|
30
|
+
/** Allowlists from the scanned repo's own .gitleaks.toml (see repo-config.ts). */
|
|
31
|
+
repo?: CompiledAllowlist[];
|
|
27
32
|
}
|
|
33
|
+
export declare function compileAllowlist(a: GitleaksAllowlist): CompiledAllowlist;
|
|
28
34
|
/** HQ's own patterns, expressed as gitleaks-shaped rules. */
|
|
29
35
|
export declare function hqRules(): GitleaksRule[];
|
|
30
36
|
export declare function compiledRules(): CompiledRuleSet;
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
import { SECRET_PATTERNS } from "../core-utils/secret-patterns.js";
|
|
6
6
|
import { GITLEAKS_CONFIG } from "./gitleaks-rules.generated.js";
|
|
7
7
|
import { compileGoRegex } from "./go-regex.js";
|
|
8
|
-
function compileAllowlist(a) {
|
|
8
|
+
export function compileAllowlist(a) {
|
|
9
9
|
return {
|
|
10
10
|
condition: a.condition === "AND" ? "AND" : "OR",
|
|
11
11
|
target: a.regexTarget ?? "secret",
|
|
12
12
|
paths: (a.paths ?? []).map((p) => compileGoRegex(p)),
|
|
13
13
|
regexes: (a.regexes ?? []).map((r) => compileGoRegex(r)),
|
|
14
14
|
stopwords: (a.stopwords ?? []).map((s) => s.toLowerCase()),
|
|
15
|
+
commits: (a.commits ?? []).map((c) => c.toLowerCase()),
|
|
16
|
+
targetRules: a.targetRules ?? [],
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
function compileRule(r) {
|
|
@@ -12,6 +12,11 @@ export interface ParsedDiff {
|
|
|
12
12
|
/** Parse `git diff --unified=0` output into one fragment of added lines per hunk. */
|
|
13
13
|
export declare function parseUnifiedDiff(diff: string): ParsedDiff;
|
|
14
14
|
export declare function stagedDiff(cwd: string): string;
|
|
15
|
+
export declare function repoRoot(cwd: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Built-in rules plus the repo's own .gitleaks.toml allowlists. Diff paths are
|
|
18
|
+
* relative to the repo root, as gitleaks' allowlist paths are.
|
|
19
|
+
*/
|
|
15
20
|
export declare function scanStaged(cwd: string): SecretFinding[];
|
|
16
21
|
export declare function scanFiles(paths: string[]): SecretFinding[];
|
|
17
22
|
//# sourceMappingURL=staged.d.ts.map
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { readFileSync, statSync } from "node:fs";
|
|
6
6
|
import { scanFragment, scanPath } from "./detect.js";
|
|
7
|
+
import { loadRepoAllowlists } from "./repo-config.js";
|
|
8
|
+
import { compiledRules } from "./rules.js";
|
|
7
9
|
export class ScanInputError extends Error {
|
|
8
10
|
}
|
|
9
11
|
const DIFF_ARGS = [
|
|
@@ -89,9 +91,24 @@ export function stagedDiff(cwd) {
|
|
|
89
91
|
throw new ScanInputError(`could not read the staged changes: ${detail || "git diff failed"}`);
|
|
90
92
|
}
|
|
91
93
|
}
|
|
94
|
+
export function repoRoot(cwd) {
|
|
95
|
+
try {
|
|
96
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const detail = err instanceof Error && "stderr" in err ? String(err.stderr).trim() : String(err);
|
|
100
|
+
throw new ScanInputError(`could not find the repo root: ${detail || "git rev-parse failed"}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Built-in rules plus the repo's own .gitleaks.toml allowlists. Diff paths are
|
|
105
|
+
* relative to the repo root, as gitleaks' allowlist paths are.
|
|
106
|
+
*/
|
|
92
107
|
export function scanStaged(cwd) {
|
|
93
|
-
const
|
|
94
|
-
|
|
108
|
+
const diff = stagedDiff(cwd);
|
|
109
|
+
const rules = { ...compiledRules(), repo: loadRepoAllowlists(repoRoot(cwd)) };
|
|
110
|
+
const { fragments, binaryFiles } = parseUnifiedDiff(diff);
|
|
111
|
+
return [...fragments.flatMap((f) => scanFragment(f, rules)), ...binaryFiles.flatMap((f) => scanPath(f, rules))];
|
|
95
112
|
}
|
|
96
113
|
export function scanFiles(paths) {
|
|
97
114
|
const out = [];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Company-relative folders the server owns and a client push never uploads. */
|
|
2
|
+
export declare const SERVER_OWNED_FOLDERS: readonly string[];
|
|
3
|
+
/**
|
|
4
|
+
* True for a company-relative path a client push refuses as server-owned.
|
|
5
|
+
* Mirrors hq-cloud's `isLocallyIneligibleForPush` with the entity exception:
|
|
6
|
+
* `ontology/entities/` can be client-owned, so it is not reported here.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isServerOwnedNeverPushed(rel: string): boolean;
|
|
9
|
+
export type ServerOwnedLocalOnlyResult = {
|
|
10
|
+
files: string[];
|
|
11
|
+
total: number;
|
|
12
|
+
};
|
|
13
|
+
export type FindServerOwnedLocalOnlyOptions = {
|
|
14
|
+
hqRoot: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
/** Company-relative keys already in the sync journal (live or tombstoned). */
|
|
17
|
+
journalKeys: ReadonlySet<string>;
|
|
18
|
+
/** Engine ignore filter over absolute paths; true = would sync. */
|
|
19
|
+
shouldSync?: (absPath: string, isDir?: boolean) => boolean;
|
|
20
|
+
/** Max paths returned; `total` still counts every match. */
|
|
21
|
+
limit?: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Walk the server-owned folders under `companies/<slug>/` and return files
|
|
25
|
+
* that are not in the journal. A journaled file arrived by pull and is in
|
|
26
|
+
* sync; anything else was written locally and will never upload.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findServerOwnedLocalOnly(opts: FindServerOwnedLocalOnlyOptions): ServerOwnedLocalOnlyResult;
|
|
29
|
+
//# sourceMappingURL=server-owned-local-only.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-only files under server-owned vault folders (feedback_0414d71c).
|
|
3
|
+
*
|
|
4
|
+
* The sync engine treats `sources/`, `signals/` and the non-entity part of
|
|
5
|
+
* `ontology/` as cloud-authoritative: the server pipeline writes them and a
|
|
6
|
+
* client push refuses them (`refuse-server-owned` in hq-cloud's share plan,
|
|
7
|
+
* see hq-cloud `src/lib/cloud-authoritative.ts`). The recurring runner skips
|
|
8
|
+
* them quietly, so a file written locally into one of these folders never
|
|
9
|
+
* reaches the vault and nothing reports it as pending. `hq sync status` lists
|
|
10
|
+
* them so the gap is visible.
|
|
11
|
+
*
|
|
12
|
+
* Read-only and network-free: the walk covers only the server-owned folders
|
|
13
|
+
* and the "already synced" set is the company's sync journal the caller
|
|
14
|
+
* already has.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
/** Company-relative folders the server owns and a client push never uploads. */
|
|
19
|
+
export const SERVER_OWNED_FOLDERS = ["sources", "signals", "ontology"];
|
|
20
|
+
/**
|
|
21
|
+
* True for a company-relative path a client push refuses as server-owned.
|
|
22
|
+
* Mirrors hq-cloud's `isLocallyIneligibleForPush` with the entity exception:
|
|
23
|
+
* `ontology/entities/` can be client-owned, so it is not reported here.
|
|
24
|
+
*/
|
|
25
|
+
export function isServerOwnedNeverPushed(rel) {
|
|
26
|
+
const vaultRel = rel.split("\\").join("/").replace(/^\.?\//, "");
|
|
27
|
+
if (vaultRel.startsWith("ontology/entities/"))
|
|
28
|
+
return false;
|
|
29
|
+
return SERVER_OWNED_FOLDERS.some((folder) => vaultRel.startsWith(`${folder}/`));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Walk the server-owned folders under `companies/<slug>/` and return files
|
|
33
|
+
* that are not in the journal. A journaled file arrived by pull and is in
|
|
34
|
+
* sync; anything else was written locally and will never upload.
|
|
35
|
+
*/
|
|
36
|
+
export function findServerOwnedLocalOnly(opts) {
|
|
37
|
+
const root = path.join(opts.hqRoot, "companies", opts.slug);
|
|
38
|
+
const limit = opts.limit ?? 20;
|
|
39
|
+
const found = [];
|
|
40
|
+
const stack = SERVER_OWNED_FOLDERS.map((folder) => path.join(root, folder));
|
|
41
|
+
while (stack.length > 0) {
|
|
42
|
+
const dir = stack.pop();
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const abs = path.join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
if (entry.name === ".git" || entry.name === "node_modules")
|
|
54
|
+
continue;
|
|
55
|
+
if (opts.shouldSync && !opts.shouldSync(abs, true))
|
|
56
|
+
continue;
|
|
57
|
+
stack.push(abs);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!entry.isFile())
|
|
61
|
+
continue;
|
|
62
|
+
if (opts.shouldSync && !opts.shouldSync(abs, false))
|
|
63
|
+
continue;
|
|
64
|
+
const rel = path.relative(root, abs).split(path.sep).join("/");
|
|
65
|
+
if (!isServerOwnedNeverPushed(rel))
|
|
66
|
+
continue;
|
|
67
|
+
if (opts.journalKeys.has(rel))
|
|
68
|
+
continue;
|
|
69
|
+
found.push(rel);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
found.sort((a, b) => a.localeCompare(b));
|
|
73
|
+
return { files: found.slice(0, limit), total: found.length };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=server-owned-local-only.js.map
|
|
@@ -31,6 +31,12 @@ function errMessage(err) {
|
|
|
31
31
|
export function isAccessDeniedError(err) {
|
|
32
32
|
const rec = errRecord(err);
|
|
33
33
|
if (rec) {
|
|
34
|
+
// A presigned storage download that failed AFTER the API authorized the
|
|
35
|
+
// key (createCompanyPresignClient). The ACL already passed, so pointing
|
|
36
|
+
// the caller at `hq access` would loop. Checked before the message regex
|
|
37
|
+
// because the message names the HTTP status.
|
|
38
|
+
if (rec.code === "FILES_STORAGE_DOWNLOAD_FAILED")
|
|
39
|
+
return false;
|
|
34
40
|
if (rec.status === 403)
|
|
35
41
|
return true;
|
|
36
42
|
const meta = rec.$metadata;
|
|
@@ -64,6 +64,17 @@ export declare const CLIENT_HEALTH_OBSERVATION_FILE = "cli-client-health.observa
|
|
|
64
64
|
* always re-observe — they have opened the store anyway, so it costs nothing.
|
|
65
65
|
*/
|
|
66
66
|
export declare const JOURNAL_OBSERVATION_TTL_MS: number;
|
|
67
|
+
/**
|
|
68
|
+
* V8 old-space cap, in MB, for the child process that enumerates journals on
|
|
69
|
+
* behalf of a plain invocation. `listJournals()` materializes every area of
|
|
70
|
+
* the local sync state store; on a large store that took a short-lived
|
|
71
|
+
* command such as `hq secrets exec` to 2.5-4 GB RSS (feedback_d70e9d63). The
|
|
72
|
+
* child either finishes inside this cap or dies, and the command never holds
|
|
73
|
+
* that memory itself.
|
|
74
|
+
*/
|
|
75
|
+
export declare const JOURNAL_OBSERVER_HEAP_MB = 512;
|
|
76
|
+
/** Wall-clock bound for the isolated journal observer. */
|
|
77
|
+
export declare const JOURNAL_OBSERVER_TIMEOUT_MS = 20000;
|
|
67
78
|
export interface CliClientHealthState {
|
|
68
79
|
/** Stable random installation identity — regenerated only if invalid. */
|
|
69
80
|
installationId: string;
|
|
@@ -120,6 +131,24 @@ interface JournalLike {
|
|
|
120
131
|
* contract ISO-UTC. Null when nothing has ever synced.
|
|
121
132
|
*/
|
|
122
133
|
export declare function latestJournalSyncTime(journals: readonly JournalLike[]): string | null;
|
|
134
|
+
/**
|
|
135
|
+
* The isolated observer could not produce a value (heap cap, timeout, crash or
|
|
136
|
+
* unreadable output). The caller stamps the observation so the next attempt
|
|
137
|
+
* waits a full TTL instead of re-running a read already known to fail here.
|
|
138
|
+
*/
|
|
139
|
+
export declare class JournalObservationUnavailableError extends Error {
|
|
140
|
+
constructor(message: string);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Run `listJournals()` in a heap-capped child process and return only the
|
|
144
|
+
* `lastSync` values. Used for plain invocations so the full store
|
|
145
|
+
* materialization never lands in the invoking command's own memory.
|
|
146
|
+
*/
|
|
147
|
+
export declare function observeJournalsIsolated(opts?: {
|
|
148
|
+
engineUrl?: string;
|
|
149
|
+
heapMb?: number;
|
|
150
|
+
timeoutMs?: number;
|
|
151
|
+
}): JournalLike[];
|
|
123
152
|
/**
|
|
124
153
|
* True when this event kind must pay for a fresh `listJournals()` read.
|
|
125
154
|
*
|
|
@@ -167,6 +196,11 @@ export interface ClientHealthDeps {
|
|
|
167
196
|
expiring?: typeof isExpiring;
|
|
168
197
|
versions?: () => VersionInfo;
|
|
169
198
|
journals?: () => readonly JournalLike[];
|
|
199
|
+
/**
|
|
200
|
+
* Journal source for plain invocations when `journals` is not set. Defaults
|
|
201
|
+
* to {@link observeJournalsIsolated}; sync events read in-process.
|
|
202
|
+
*/
|
|
203
|
+
isolatedJournals?: () => readonly JournalLike[];
|
|
170
204
|
post?: HeartbeatPoster;
|
|
171
205
|
timeoutMs?: number;
|
|
172
206
|
/** US-016: overridable for tests; defaults to {@link collectLocalFilesOverview}. */
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
import * as fs from "node:fs";
|
|
34
34
|
import * as path from "node:path";
|
|
35
35
|
import { randomBytes } from "node:crypto";
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { createRequire } from "node:module";
|
|
38
|
+
import { pathToFileURL } from "node:url";
|
|
36
39
|
import { getStateDir, listJournals } from "@indigoai-us/hq-cloud";
|
|
37
40
|
import { isExpiring, isMachineIdentity, loadCachedTokens, } from "./cognito-session.js";
|
|
38
41
|
import { vaultApiFetch } from "./vault-api.js";
|
|
@@ -73,6 +76,17 @@ export const CLIENT_HEALTH_OBSERVATION_FILE = "cli-client-health.observation.jso
|
|
|
73
76
|
* always re-observe — they have opened the store anyway, so it costs nothing.
|
|
74
77
|
*/
|
|
75
78
|
export const JOURNAL_OBSERVATION_TTL_MS = 15 * 60_000;
|
|
79
|
+
/**
|
|
80
|
+
* V8 old-space cap, in MB, for the child process that enumerates journals on
|
|
81
|
+
* behalf of a plain invocation. `listJournals()` materializes every area of
|
|
82
|
+
* the local sync state store; on a large store that took a short-lived
|
|
83
|
+
* command such as `hq secrets exec` to 2.5-4 GB RSS (feedback_d70e9d63). The
|
|
84
|
+
* child either finishes inside this cap or dies, and the command never holds
|
|
85
|
+
* that memory itself.
|
|
86
|
+
*/
|
|
87
|
+
export const JOURNAL_OBSERVER_HEAP_MB = 512;
|
|
88
|
+
/** Wall-clock bound for the isolated journal observer. */
|
|
89
|
+
export const JOURNAL_OBSERVER_TIMEOUT_MS = 20_000;
|
|
76
90
|
/** An observation lock older than this is treated as abandoned. */
|
|
77
91
|
const JOURNAL_OBSERVATION_LOCK_STALE_MS = 5 * 60_000;
|
|
78
92
|
/**
|
|
@@ -234,6 +248,66 @@ export function latestJournalSyncTime(journals) {
|
|
|
234
248
|
}
|
|
235
249
|
return latest === null ? null : new Date(latest).toISOString();
|
|
236
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The isolated observer could not produce a value (heap cap, timeout, crash or
|
|
253
|
+
* unreadable output). The caller stamps the observation so the next attempt
|
|
254
|
+
* waits a full TTL instead of re-running a read already known to fail here.
|
|
255
|
+
*/
|
|
256
|
+
export class JournalObservationUnavailableError extends Error {
|
|
257
|
+
constructor(message) {
|
|
258
|
+
super(message);
|
|
259
|
+
this.name = "JournalObservationUnavailableError";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const JOURNAL_OBSERVER_SOURCE = [
|
|
263
|
+
"const engine = await import(process.argv[1]);",
|
|
264
|
+
"const out = [];",
|
|
265
|
+
"for (const entry of engine.listJournals()) {",
|
|
266
|
+
" const value = entry && entry.journal ? entry.journal.lastSync : undefined;",
|
|
267
|
+
" if (typeof value === 'string') out.push(value);",
|
|
268
|
+
"}",
|
|
269
|
+
"process.stdout.write(JSON.stringify(out));",
|
|
270
|
+
].join("\n");
|
|
271
|
+
function defaultJournalEngineUrl() {
|
|
272
|
+
return pathToFileURL(createRequire(import.meta.url).resolve("@indigoai-us/hq-cloud")).href;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Run `listJournals()` in a heap-capped child process and return only the
|
|
276
|
+
* `lastSync` values. Used for plain invocations so the full store
|
|
277
|
+
* materialization never lands in the invoking command's own memory.
|
|
278
|
+
*/
|
|
279
|
+
export function observeJournalsIsolated(opts = {}) {
|
|
280
|
+
const engineUrl = opts.engineUrl ?? defaultJournalEngineUrl();
|
|
281
|
+
const result = spawnSync(process.execPath, [
|
|
282
|
+
`--max-old-space-size=${opts.heapMb ?? JOURNAL_OBSERVER_HEAP_MB}`,
|
|
283
|
+
"--input-type=module",
|
|
284
|
+
"-e",
|
|
285
|
+
JOURNAL_OBSERVER_SOURCE,
|
|
286
|
+
engineUrl,
|
|
287
|
+
], {
|
|
288
|
+
encoding: "utf8",
|
|
289
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
290
|
+
timeout: opts.timeoutMs ?? JOURNAL_OBSERVER_TIMEOUT_MS,
|
|
291
|
+
maxBuffer: 1024 * 1024,
|
|
292
|
+
windowsHide: true,
|
|
293
|
+
});
|
|
294
|
+
if (result.error || result.status !== 0 || result.signal) {
|
|
295
|
+
throw new JournalObservationUnavailableError(`journal observer failed (status ${String(result.status)}, signal ${String(result.signal)})`);
|
|
296
|
+
}
|
|
297
|
+
let values;
|
|
298
|
+
try {
|
|
299
|
+
values = JSON.parse(result.stdout);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
throw new JournalObservationUnavailableError("journal observer output was not JSON");
|
|
303
|
+
}
|
|
304
|
+
if (!Array.isArray(values)) {
|
|
305
|
+
throw new JournalObservationUnavailableError("journal observer output was not a list");
|
|
306
|
+
}
|
|
307
|
+
return values
|
|
308
|
+
.filter((value) => typeof value === "string")
|
|
309
|
+
.map((lastSync) => ({ journal: { lastSync } }));
|
|
310
|
+
}
|
|
237
311
|
/**
|
|
238
312
|
* True when this event kind must pay for a fresh `listJournals()` read.
|
|
239
313
|
*
|
|
@@ -527,7 +601,14 @@ async function contributeClientHealthUnbounded(kind, deps, failureReason) {
|
|
|
527
601
|
}
|
|
528
602
|
else {
|
|
529
603
|
try {
|
|
530
|
-
|
|
604
|
+
// A plain invocation (any command, `hq secrets exec` included) reads
|
|
605
|
+
// the store in a heap-capped child. Sync commands have the store open
|
|
606
|
+
// already and read in-process.
|
|
607
|
+
const source = deps.journals ??
|
|
608
|
+
(kind === "invocation"
|
|
609
|
+
? (deps.isolatedJournals ?? observeJournalsIsolated)
|
|
610
|
+
: listJournals);
|
|
611
|
+
const observed = latestJournalSyncTime(source());
|
|
531
612
|
if (observed !== null) {
|
|
532
613
|
journalLastSyncAt = observed;
|
|
533
614
|
observation.lastSyncAt = observed;
|
|
@@ -543,8 +624,20 @@ async function contributeClientHealthUnbounded(kind, deps, failureReason) {
|
|
|
543
624
|
observation.observedAt = now.toISOString();
|
|
544
625
|
persistJournalObservation(stateDir, observation);
|
|
545
626
|
}
|
|
546
|
-
catch {
|
|
547
|
-
// Journal enumeration is best effort.
|
|
627
|
+
catch (error) {
|
|
628
|
+
// Journal enumeration is best effort. When the isolated observer hit
|
|
629
|
+
// its heap or time cap, keep the previous value but stamp the attempt
|
|
630
|
+
// so the next invocation waits a full TTL instead of retrying a read
|
|
631
|
+
// already known to fail on this machine.
|
|
632
|
+
if (error instanceof JournalObservationUnavailableError) {
|
|
633
|
+
observation.observedAt = now.toISOString();
|
|
634
|
+
try {
|
|
635
|
+
persistJournalObservation(stateDir, observation);
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
// Best effort.
|
|
639
|
+
}
|
|
640
|
+
}
|
|
548
641
|
}
|
|
549
642
|
finally {
|
|
550
643
|
release();
|