@indigoai-us/hq-cli 5.345.146 → 5.345.148
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 +45 -0
- package/README.md +14 -0
- package/assets/bot-workers/setup/worker.yaml +11 -0
- package/dist/command-catalog.generated.d.ts +326 -4
- package/dist/command-catalog.generated.js +403 -4
- package/dist/commands/agent-inbox.js +2 -1
- package/dist/commands/auth.js +4 -2
- package/dist/commands/bot.js +1 -1
- package/dist/commands/cloud.js +31 -1
- package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
- package/dist/commands/core-refresh-vault-access-native.js +214 -19
- package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
- package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
- package/dist/commands/core.js +6 -0
- package/dist/commands/daemon.d.ts +7 -0
- package/dist/commands/daemon.js +37 -3
- package/dist/commands/dm.js +9 -2
- package/dist/commands/doctor.d.ts +11 -1
- package/dist/commands/doctor.js +56 -1
- package/dist/commands/files.js +10 -6
- package/dist/commands/install-global.d.ts +7 -0
- package/dist/commands/install-global.js +129 -35
- package/dist/commands/lanes-shared.d.ts +3 -1
- package/dist/commands/lanes.d.ts +4 -4
- package/dist/commands/lanes.js +380 -25
- package/dist/commands/login.js +4 -3
- package/dist/commands/pack-install.d.ts +2 -2
- package/dist/commands/pack-install.js +133 -35
- package/dist/commands/sync-manifest.d.ts +2 -9
- package/dist/commands/sync-manifest.js +4 -15
- package/dist/hook-engine-path.d.ts +4 -0
- package/dist/hook-engine-path.js +36 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2784 -2360
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +6 -0
- package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
- package/dist/lib/agent-kit/inbox-redact.js +361 -0
- package/dist/lib/agent-kit/inbox-state.js +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +36 -7
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
- package/dist/lib/anywhere-runtime-flag-key.js +9 -0
- package/dist/lib/bot/daemon.js +3 -0
- package/dist/lib/bot/runtime/index.js +3 -1
- package/dist/lib/company-skill-wrapper.js +6 -4
- package/dist/lib/daemon/install.d.ts +47 -1
- package/dist/lib/daemon/install.js +181 -15
- package/dist/lib/daemon/lock.d.ts +32 -0
- package/dist/lib/daemon/lock.js +55 -2
- package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
- package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
- package/dist/lib/doctor/checks/anywhere.js +23 -8
- package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
- package/dist/lib/doctor/checks/cli-path.js +45 -0
- package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
- package/dist/lib/doctor/checks/hook-engine.js +16 -0
- package/dist/lib/doctor/checks/release-links.d.ts +23 -0
- package/dist/lib/doctor/checks/release-links.js +152 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
- package/dist/lib/doctor/checks/sync-health.js +38 -2
- package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
- package/dist/lib/doctor/checks/temp-homes.js +54 -0
- package/dist/lib/doctor/fix/apply.d.ts +90 -1
- package/dist/lib/doctor/fix/apply.js +176 -10
- package/dist/lib/doctor/fix/json-output.d.ts +4 -1
- package/dist/lib/doctor/fix/json-output.js +6 -0
- package/dist/lib/doctor/fix/remediation.d.ts +7 -1
- package/dist/lib/doctor/fix/remediation.js +23 -1
- package/dist/lib/doctor/registry.js +13 -0
- package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
- package/dist/lib/doctor/temp-home-sweeper.js +418 -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/flag-registry.js +9 -1
- package/dist/lib/flag-snapshot-file.js +8 -3
- package/dist/lib/hook-engine/resolve.d.ts +31 -0
- package/dist/lib/hook-engine/resolve.js +134 -0
- package/dist/lib/lanes/admission-host.d.ts +1 -0
- package/dist/lib/lanes/admission-host.js +8 -1
- package/dist/lib/lanes/admission.d.ts +4 -0
- package/dist/lib/lanes/admission.js +6 -2
- package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.js +4 -1
- package/dist/lib/lanes/local-host-identity.d.ts +40 -0
- package/dist/lib/lanes/local-host-identity.js +314 -0
- package/dist/lib/lanes/reaper.d.ts +1 -1
- package/dist/lib/lanes/reaper.js +7 -0
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +16 -8
- package/dist/lib/lanes/spawn.d.ts +20 -0
- package/dist/lib/lanes/spawn.js +48 -2
- package/dist/lib/lanes/status-fields.js +6 -1
- package/dist/lib/lanes/store.js +8 -1
- package/dist/lib/lanes/throttle.d.ts +49 -6
- package/dist/lib/lanes/throttle.js +154 -17
- package/dist/lib/lanes/types.d.ts +22 -1
- package/dist/lib/lanes/types.js +27 -3
- package/dist/lib/lanes/watcher.js +27 -2
- package/dist/lib/lanes/worker-continuity.d.ts +2 -0
- package/dist/lib/lanes/worker-continuity.js +18 -11
- package/dist/lib/manifest-disabled.d.ts +13 -0
- package/dist/lib/manifest-disabled.js +19 -0
- package/dist/lib/service-manager/launchd.d.ts +1 -0
- package/dist/lib/service-manager/launchd.js +1 -1
- package/dist/lib/windows/user-path.d.ts +48 -0
- package/dist/lib/windows/user-path.js +114 -0
- package/dist/lib/work-context/auto-bind.d.ts +1 -1
- package/dist/lib/work-context/auto-bind.js +22 -3
- package/dist/lib/work-context/lane-bind.d.ts +31 -0
- package/dist/lib/work-context/lane-bind.js +101 -0
- package/dist/lib/work-context/organize.js +1 -0
- package/dist/lib/work-context/prd-sync.js +7 -0
- package/dist/lib/work-context/project.d.ts +36 -10
- package/dist/lib/work-context/project.js +130 -41
- package/dist/lib/work-context/reconcile.d.ts +6 -0
- package/dist/lib/work-context/reconcile.js +25 -5
- package/dist/lib/work-context/state.d.ts +6 -0
- package/dist/lib/work-context/state.js +29 -1
- package/dist/lib/workers-registry/index.d.ts +6 -0
- package/dist/lib/workers-registry/index.js +8 -0
- package/dist/utils/copy-tree.d.ts +61 -0
- package/dist/utils/copy-tree.js +249 -0
- package/dist/utils/environmental-error.js +7 -3
- package/dist/utils/headless.d.ts +2 -0
- package/dist/utils/headless.js +8 -0
- package/dist/utils/login-browser.d.ts +4 -0
- package/dist/utils/login-browser.js +122 -0
- package/dist/utils/npm-invocation.d.ts +27 -0
- package/dist/utils/npm-invocation.js +54 -0
- package/dist/utils/safe-link.d.ts +50 -0
- package/dist/utils/safe-link.js +300 -0
- package/dist/utils/team-upgrade.js +1 -7
- package/package.json +17 -5
- package/scripts/hook-engine-manifest.mjs +22 -0
- package/scripts/install-hook-engine.mjs +10 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
1
|
+
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.88","bundleSha256":"7e9ee380eae55e059a474fd5f415e8aeb16088805b761d791318d5299645becd","formatVersion":1}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { installWindowsHideDefault } from "./utils/windows-hide-default.js";
|
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { parseMonitorCheckFastRequest } from "./lanes-monitor-check-argv.js";
|
|
18
18
|
import { createRequire } from "node:module";
|
|
19
|
+
import { hookEnginePathArgs, runHookEnginePath } from "./hook-engine-path.js";
|
|
19
20
|
function cygpathMixedPath(value) {
|
|
20
21
|
// Require only a Node builtin: the package supports Node releases before
|
|
21
22
|
// require(ESM) became available, and this runs in the synchronous dispatcher.
|
|
@@ -159,6 +160,11 @@ function dispatchCliEntry() {
|
|
|
159
160
|
// Before anything spawns: a background hq on Windows (no terminal) hides the
|
|
160
161
|
// console window of every child it starts (feedback_351a40d5).
|
|
161
162
|
installWindowsHideDefault();
|
|
163
|
+
const engineArgs = hookEnginePathArgs(process.argv);
|
|
164
|
+
if (engineArgs !== null) {
|
|
165
|
+
void runHookEnginePath(engineArgs);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
162
168
|
if (process.platform === "win32" && coreCommandHasPathOperands(process.argv)) {
|
|
163
169
|
// Keep cygpath off non-Windows and non-path command routes.
|
|
164
170
|
process.argv = normalizeCorePathOperands(process.argv, "win32", cygpathMixedPath);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner STS credentials ride on an agent inbox item as
|
|
3
|
+
* `actingContext.credentials`. The mirror files are readable by whatever
|
|
4
|
+
* model has file tools, so those credentials are removed before a write and
|
|
5
|
+
* again on read. Only `mode`, `ownerPersonUid`, `ownerRole`, and `expiresAt`
|
|
6
|
+
* stay on `actingContext`. Any other key under that object is dropped.
|
|
7
|
+
*
|
|
8
|
+
* A scrub that cannot write (EPERM, ENOSPC, and the same class) is counted
|
|
9
|
+
* and skipped. It does not throw into the poller or a reader. The next poll
|
|
10
|
+
* or read tries again. Failure logs are a count and errno codes, never a
|
|
11
|
+
* path or a file byte.
|
|
12
|
+
*
|
|
13
|
+
* `hq dm` output drops the rest of `actingContext` as well. That stricter
|
|
14
|
+
* stdout rule lives with the command; credential removal has this one
|
|
15
|
+
* implementation.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentKitPaths } from "./paths.js";
|
|
18
|
+
export interface InboxScrubReport {
|
|
19
|
+
scrubbed: number;
|
|
20
|
+
failures: number;
|
|
21
|
+
/** Errno names only. Never paths, file names, or file contents. */
|
|
22
|
+
codes: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One writer at a time for the mirror directory. The journal is rewritten
|
|
26
|
+
* in place when credentials are scrubbed and appended when a message arrives;
|
|
27
|
+
* those two must not overlap or the rewrite can drop the append.
|
|
28
|
+
* Returns undefined when the directory is missing or the lock is not acquired.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withInboxMirrorLock<T>(inboxDir: string, waitMs: number, fn: () => T): T | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Remove credential and unknown `actingContext` keys. Allowed delegation
|
|
33
|
+
* fields are copied through. Does not mutate `item`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function redactAgentInboxItem<T>(item: T): T;
|
|
36
|
+
/**
|
|
37
|
+
* Rewrite mirror files that still contain delegated credentials, drop torn
|
|
38
|
+
* journal lines that contain them, and delete stale `*.json.tmp.*` files.
|
|
39
|
+
* A clean directory is left untouched, so repeating the call is a no-op.
|
|
40
|
+
* Returns how many records were rewritten or removed. Write and rename
|
|
41
|
+
* errors are counted on the report and are not thrown.
|
|
42
|
+
*/
|
|
43
|
+
export declare function scrubInboxMirror(paths: Pick<AgentKitPaths, "inboxDir">, log?: (report: InboxScrubReport) => void): number;
|
|
44
|
+
//# sourceMappingURL=inbox-redact.d.ts.map
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner STS credentials ride on an agent inbox item as
|
|
3
|
+
* `actingContext.credentials`. The mirror files are readable by whatever
|
|
4
|
+
* model has file tools, so those credentials are removed before a write and
|
|
5
|
+
* again on read. Only `mode`, `ownerPersonUid`, `ownerRole`, and `expiresAt`
|
|
6
|
+
* stay on `actingContext`. Any other key under that object is dropped.
|
|
7
|
+
*
|
|
8
|
+
* A scrub that cannot write (EPERM, ENOSPC, and the same class) is counted
|
|
9
|
+
* and skipped. It does not throw into the poller or a reader. The next poll
|
|
10
|
+
* or read tries again. Failure logs are a count and errno codes, never a
|
|
11
|
+
* path or a file byte.
|
|
12
|
+
*
|
|
13
|
+
* `hq dm` output drops the rest of `actingContext` as well. That stricter
|
|
14
|
+
* stdout rule lives with the command; credential removal has this one
|
|
15
|
+
* implementation.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { writePrivateFile } from "./private-file.js";
|
|
20
|
+
const INBOX_JSONL_NAME = "inbox.jsonl";
|
|
21
|
+
const STATE_FILES = new Set(["seen-ids.json", "done-ids.json"]);
|
|
22
|
+
const LOCK_DIR_NAME = ".inbox-write.lock";
|
|
23
|
+
const LOCK_STALE_MS = 30_000;
|
|
24
|
+
const ACTING_CONTEXT_KEYS = ["mode", "ownerPersonUid", "ownerRole", "expiresAt"];
|
|
25
|
+
const ALLOWED_ACTING_KEYS = new Set(ACTING_CONTEXT_KEYS);
|
|
26
|
+
/** Substrings that mark a torn line or temp. The values themselves are not logged. */
|
|
27
|
+
const CREDENTIAL_MARKERS = ["\"credentials\"", "secretAccessKey", "sessionToken", "accessKeyId"];
|
|
28
|
+
function emptyTally() {
|
|
29
|
+
return { scrubbed: 0, failures: 0, codes: [] };
|
|
30
|
+
}
|
|
31
|
+
function errnoCode(err) {
|
|
32
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : undefined;
|
|
33
|
+
return typeof code === "string" && /^[A-Z][A-Z0-9]{1,15}$/.test(code) ? code : "ERR";
|
|
34
|
+
}
|
|
35
|
+
function noteFailure(tally, err) {
|
|
36
|
+
tally.failures += 1;
|
|
37
|
+
const code = errnoCode(err);
|
|
38
|
+
if (!tally.codes.includes(code))
|
|
39
|
+
tally.codes.push(code);
|
|
40
|
+
}
|
|
41
|
+
function sleepMs(ms) {
|
|
42
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One writer at a time for the mirror directory. The journal is rewritten
|
|
46
|
+
* in place when credentials are scrubbed and appended when a message arrives;
|
|
47
|
+
* those two must not overlap or the rewrite can drop the append.
|
|
48
|
+
* Returns undefined when the directory is missing or the lock is not acquired.
|
|
49
|
+
*/
|
|
50
|
+
export function withInboxMirrorLock(inboxDir, waitMs, fn) {
|
|
51
|
+
let dirStat;
|
|
52
|
+
try {
|
|
53
|
+
dirStat = fs.lstatSync(inboxDir);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (!dirStat.isDirectory())
|
|
59
|
+
return undefined;
|
|
60
|
+
const lockDir = path.join(inboxDir, LOCK_DIR_NAME);
|
|
61
|
+
const deadline = Date.now() + Math.max(0, waitMs);
|
|
62
|
+
for (;;) {
|
|
63
|
+
try {
|
|
64
|
+
fs.mkdirSync(lockDir);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err.code !== "EEXIST")
|
|
68
|
+
return undefined;
|
|
69
|
+
try {
|
|
70
|
+
if (Date.now() - fs.statSync(lockDir).mtimeMs > LOCK_STALE_MS)
|
|
71
|
+
fs.rmdirSync(lockDir);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* the holder released it, or it is not a directory */
|
|
75
|
+
}
|
|
76
|
+
if (Date.now() >= deadline)
|
|
77
|
+
return undefined;
|
|
78
|
+
sleepMs(20);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return fn();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
try {
|
|
86
|
+
fs.rmdirSync(lockDir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* already released */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function actingContextRecord(value) {
|
|
95
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
96
|
+
return null;
|
|
97
|
+
const acting = value.actingContext;
|
|
98
|
+
if (!acting || typeof acting !== "object" || Array.isArray(acting))
|
|
99
|
+
return null;
|
|
100
|
+
return acting;
|
|
101
|
+
}
|
|
102
|
+
function actingNeedsRewrite(acting) {
|
|
103
|
+
return Object.keys(acting).some((key) => !ALLOWED_ACTING_KEYS.has(key));
|
|
104
|
+
}
|
|
105
|
+
function allowlistedActing(acting) {
|
|
106
|
+
const kept = {};
|
|
107
|
+
for (const key of ACTING_CONTEXT_KEYS) {
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(acting, key))
|
|
109
|
+
kept[key] = acting[key];
|
|
110
|
+
}
|
|
111
|
+
return kept;
|
|
112
|
+
}
|
|
113
|
+
function textHasCredentialMaterial(text) {
|
|
114
|
+
return CREDENTIAL_MARKERS.some((marker) => text.includes(marker));
|
|
115
|
+
}
|
|
116
|
+
function isStaleJsonTemp(name) {
|
|
117
|
+
return (name.includes(".json.tmp.") ||
|
|
118
|
+
name.includes(".jsonl.tmp.") ||
|
|
119
|
+
name.endsWith(".json.tmp") ||
|
|
120
|
+
name.endsWith(".jsonl.tmp"));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Remove credential and unknown `actingContext` keys. Allowed delegation
|
|
124
|
+
* fields are copied through. Does not mutate `item`.
|
|
125
|
+
*/
|
|
126
|
+
export function redactAgentInboxItem(item) {
|
|
127
|
+
const acting = actingContextRecord(item);
|
|
128
|
+
if (!acting || !actingNeedsRewrite(acting))
|
|
129
|
+
return item;
|
|
130
|
+
const record = item;
|
|
131
|
+
const kept = allowlistedActing(acting);
|
|
132
|
+
if (Object.keys(kept).length === 0) {
|
|
133
|
+
const { actingContext: _actingContext, ...without } = record;
|
|
134
|
+
return without;
|
|
135
|
+
}
|
|
136
|
+
return { ...record, actingContext: kept };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* One journal record per line. A torn line cannot be parsed, and an
|
|
140
|
+
* interrupted append can still hold a credential on that line. Drop those
|
|
141
|
+
* lines. A torn line with no credential marker stays, so a truncated
|
|
142
|
+
* message that is not a secret is not thrown away.
|
|
143
|
+
*/
|
|
144
|
+
function redactJsonl(text) {
|
|
145
|
+
let changed = 0;
|
|
146
|
+
const kept = [];
|
|
147
|
+
for (const line of text.split("\n")) {
|
|
148
|
+
if (!line.trim()) {
|
|
149
|
+
kept.push(line);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
let parsed;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(line);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
if (!textHasCredentialMaterial(line)) {
|
|
158
|
+
kept.push(line);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
changed += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const acting = actingContextRecord(parsed);
|
|
165
|
+
if (!acting || !actingNeedsRewrite(acting)) {
|
|
166
|
+
kept.push(line);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
changed += 1;
|
|
170
|
+
kept.push(JSON.stringify(redactAgentInboxItem(parsed)));
|
|
171
|
+
}
|
|
172
|
+
return { text: kept.join("\n"), changed };
|
|
173
|
+
}
|
|
174
|
+
function scrubJsonFile(file, tally) {
|
|
175
|
+
let text;
|
|
176
|
+
try {
|
|
177
|
+
const st = fs.lstatSync(file);
|
|
178
|
+
if (!st.isFile())
|
|
179
|
+
return;
|
|
180
|
+
text = fs.readFileSync(file, "utf8");
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (err.code === "ENOENT")
|
|
184
|
+
return;
|
|
185
|
+
noteFailure(tally, err);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let parsed;
|
|
189
|
+
try {
|
|
190
|
+
parsed = JSON.parse(text);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
if (!textHasCredentialMaterial(text))
|
|
194
|
+
return;
|
|
195
|
+
try {
|
|
196
|
+
fs.unlinkSync(file);
|
|
197
|
+
tally.scrubbed += 1;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
noteFailure(tally, err);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const acting = actingContextRecord(parsed);
|
|
205
|
+
if (!acting || !actingNeedsRewrite(acting))
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
// The temp writePrivateFile creates is the redacted body, not the original.
|
|
209
|
+
writePrivateFile(file, `${JSON.stringify(redactAgentInboxItem(parsed), null, 2)}\n`, 0o600);
|
|
210
|
+
tally.scrubbed += 1;
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
noteFailure(tally, err);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function redactedTempBody(text) {
|
|
217
|
+
try {
|
|
218
|
+
return `${JSON.stringify(redactAgentInboxItem(JSON.parse(text)), null, 2)}\n`;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return "\n";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** Delete a leftover `*.json.tmp.*` from a crashed atomic write. Rewrite it if it cannot be removed. */
|
|
225
|
+
function discardStaleTemp(file, tally) {
|
|
226
|
+
let text;
|
|
227
|
+
try {
|
|
228
|
+
const st = fs.lstatSync(file);
|
|
229
|
+
if (!st.isFile())
|
|
230
|
+
return;
|
|
231
|
+
text = fs.readFileSync(file, "utf8");
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err.code === "ENOENT")
|
|
235
|
+
return;
|
|
236
|
+
text = undefined;
|
|
237
|
+
}
|
|
238
|
+
const poisoned = text !== undefined && textHasCredentialMaterial(text);
|
|
239
|
+
try {
|
|
240
|
+
fs.unlinkSync(file);
|
|
241
|
+
if (poisoned)
|
|
242
|
+
tally.scrubbed += 1;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
if (err.code === "ENOENT")
|
|
247
|
+
return;
|
|
248
|
+
if (text === undefined) {
|
|
249
|
+
noteFailure(tally, err);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (text === undefined)
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
writePrivateFile(file, redactedTempBody(text), 0o600);
|
|
257
|
+
if (poisoned)
|
|
258
|
+
tally.scrubbed += 1;
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
noteFailure(tally, err);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function scrubJournal(journal, tally) {
|
|
265
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
266
|
+
let before;
|
|
267
|
+
try {
|
|
268
|
+
before = fs.readFileSync(journal, "utf8");
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
if (err.code === "ENOENT")
|
|
272
|
+
return;
|
|
273
|
+
noteFailure(tally, err);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const redacted = redactJsonl(before);
|
|
277
|
+
if (redacted.changed === 0)
|
|
278
|
+
return;
|
|
279
|
+
let latest;
|
|
280
|
+
try {
|
|
281
|
+
latest = fs.readFileSync(journal, "utf8");
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
noteFailure(tally, err);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (latest !== before)
|
|
288
|
+
continue;
|
|
289
|
+
try {
|
|
290
|
+
writePrivateFile(journal, redacted.text, 0o600);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
noteFailure(tally, err);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
if (fs.readFileSync(journal, "utf8") === redacted.text) {
|
|
298
|
+
tally.scrubbed += redacted.changed;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
/* the next pass, or the next read, rewrites whatever landed */
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
noteFailure(tally, Object.assign(new Error("inbox scrub did not land"), { code: "EAGAIN" }));
|
|
307
|
+
}
|
|
308
|
+
function scanInboxDir(inboxDir) {
|
|
309
|
+
const tally = emptyTally();
|
|
310
|
+
let names;
|
|
311
|
+
try {
|
|
312
|
+
names = fs.readdirSync(inboxDir);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
noteFailure(tally, err);
|
|
316
|
+
return tally;
|
|
317
|
+
}
|
|
318
|
+
for (const name of names) {
|
|
319
|
+
if (isStaleJsonTemp(name)) {
|
|
320
|
+
discardStaleTemp(path.join(inboxDir, name), tally);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (!name.endsWith(".json") || STATE_FILES.has(name))
|
|
324
|
+
continue;
|
|
325
|
+
scrubJsonFile(path.join(inboxDir, name), tally);
|
|
326
|
+
}
|
|
327
|
+
scrubJournal(path.join(inboxDir, INBOX_JSONL_NAME), tally);
|
|
328
|
+
return tally;
|
|
329
|
+
}
|
|
330
|
+
function defaultScrubLog(report) {
|
|
331
|
+
if (report.scrubbed > 0) {
|
|
332
|
+
console.error(`inbox: scrubbed ${report.scrubbed} mirrored record(s) containing delegated credentials`);
|
|
333
|
+
}
|
|
334
|
+
if (report.failures > 0) {
|
|
335
|
+
const codes = report.codes.length > 0 ? report.codes.join(",") : "ERR";
|
|
336
|
+
console.error(`inbox: scrub failed for ${report.failures} file(s) (${codes})`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Rewrite mirror files that still contain delegated credentials, drop torn
|
|
341
|
+
* journal lines that contain them, and delete stale `*.json.tmp.*` files.
|
|
342
|
+
* A clean directory is left untouched, so repeating the call is a no-op.
|
|
343
|
+
* Returns how many records were rewritten or removed. Write and rename
|
|
344
|
+
* errors are counted on the report and are not thrown.
|
|
345
|
+
*/
|
|
346
|
+
export function scrubInboxMirror(paths, log = defaultScrubLog) {
|
|
347
|
+
let tally = emptyTally();
|
|
348
|
+
try {
|
|
349
|
+
// Missing directory: do not create it. Lock not acquired: leave the files
|
|
350
|
+
// for the next read rather than rewriting the journal without the lock.
|
|
351
|
+
tally = withInboxMirrorLock(paths.inboxDir, 2_000, () => scanInboxDir(paths.inboxDir)) ?? tally;
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
noteFailure(tally, err);
|
|
355
|
+
}
|
|
356
|
+
if (tally.scrubbed > 0 || tally.failures > 0) {
|
|
357
|
+
log({ scrubbed: tally.scrubbed, failures: tally.failures, codes: tally.codes });
|
|
358
|
+
}
|
|
359
|
+
return tally.scrubbed;
|
|
360
|
+
}
|
|
361
|
+
//# sourceMappingURL=inbox-redact.js.map
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { redactAgentInboxItem, scrubInboxMirror } from "./inbox-redact.js";
|
|
13
14
|
import { writePrivateFile } from "./private-file.js";
|
|
14
15
|
const DONE_IDS_NAME = "done-ids.json";
|
|
15
16
|
const MAX_DONE_IDS = 5000;
|
|
@@ -37,6 +38,14 @@ export function markInboxDone(paths, ids) {
|
|
|
37
38
|
}
|
|
38
39
|
/** Every mirrored entry, oldest first, de-duplicated by id (last write wins). */
|
|
39
40
|
export function readMirroredInbox(paths) {
|
|
41
|
+
// Files written before credential redaction are rewritten here. A failed
|
|
42
|
+
// rewrite is retried on the next read. It must not hide mail already on disk.
|
|
43
|
+
try {
|
|
44
|
+
scrubInboxMirror(paths);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* scrubInboxMirror records write failures and does not throw them */
|
|
48
|
+
}
|
|
40
49
|
let text;
|
|
41
50
|
try {
|
|
42
51
|
text = fs.readFileSync(path.join(paths.inboxDir, INBOX_JSONL), "utf8");
|
|
@@ -49,7 +58,7 @@ export function readMirroredInbox(paths) {
|
|
|
49
58
|
if (!line.trim())
|
|
50
59
|
continue;
|
|
51
60
|
try {
|
|
52
|
-
const entry = JSON.parse(line);
|
|
61
|
+
const entry = redactAgentInboxItem(JSON.parse(line));
|
|
53
62
|
if (typeof entry.id === "string" && entry.id) {
|
|
54
63
|
byId.delete(entry.id);
|
|
55
64
|
byId.set(entry.id, entry);
|
|
@@ -144,7 +153,7 @@ export async function mergeAgentInbox(paths, fetchServerInbox) {
|
|
|
144
153
|
const raw = m;
|
|
145
154
|
const id = typeof raw.id === "string" ? raw.id : typeof raw.messageId === "string" ? raw.messageId : null;
|
|
146
155
|
if (id && !byId.has(id))
|
|
147
|
-
byId.set(id, { ...raw, id });
|
|
156
|
+
byId.set(id, redactAgentInboxItem({ ...raw, id }));
|
|
148
157
|
}
|
|
149
158
|
}
|
|
150
159
|
else {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { CLI_VERSION } from "../../../cli-version.js";
|
|
19
19
|
import { peekIdToken } from "../../../utils/id-token.js";
|
|
20
20
|
import { isRecipientUid } from "../../../lib/notify-recipient.js";
|
|
21
|
+
import { redactAgentInboxItem } from "../inbox-redact.js";
|
|
21
22
|
import { markInboxDone, mergeAgentInbox, readDoneIds, summarizeInboxEntry } from "../inbox-state.js";
|
|
22
23
|
import { AttachmentCompanyUnresolvedError, fetchChatAttachment } from "./attachments.js";
|
|
23
24
|
import { errorResult, McpToolInputError, textResult } from "./jsonrpc.js";
|
|
@@ -287,7 +288,7 @@ export function buildAgentMcpTools(clients) {
|
|
|
287
288
|
const rows = [...byId.values()]
|
|
288
289
|
.filter((e) => includeDone || !done.has(e.id))
|
|
289
290
|
.slice(0, limit)
|
|
290
|
-
.map((e) => summarizeInboxEntry(e, done.has(e.id)));
|
|
291
|
+
.map((e) => summarizeInboxEntry(redactAgentInboxItem(e), done.has(e.id)));
|
|
291
292
|
const hasFiles = rows.some((r) => r.attachments?.length);
|
|
292
293
|
return textResult(clampOutput(JSON.stringify({
|
|
293
294
|
count: rows.length,
|
|
@@ -20,7 +20,8 @@ import * as path from "node:path";
|
|
|
20
20
|
import { vaultApiFetch } from "../../../utils/vault-api.js";
|
|
21
21
|
import { writeComponentStatus } from "../creds.js";
|
|
22
22
|
import { pendingInbox } from "../inbox-state.js";
|
|
23
|
-
import {
|
|
23
|
+
import { redactAgentInboxItem, scrubInboxMirror, withInboxMirrorLock } from "../inbox-redact.js";
|
|
24
|
+
import { mkdirPrivate, writePrivateFile } from "./../private-file.js";
|
|
24
25
|
import { createAgentKitInboxBackoffFlagReader } from "../inbox-poll-backoff-flag.js";
|
|
25
26
|
import { buildWakeNotice, currentWakeState, fireWake, readWakeConfig, shouldWake, } from "../wake.js";
|
|
26
27
|
export const SEEN_IDS_NAME = "seen-ids.json";
|
|
@@ -79,12 +80,22 @@ function inboxJournalContains(paths, id) {
|
|
|
79
80
|
}
|
|
80
81
|
export function mirrorInboxItem(paths, item) {
|
|
81
82
|
const dest = path.join(paths.inboxDir, `${item.id}.json`);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
// One redaction covers the per-id file and the journal line. The journal
|
|
84
|
+
// spreads this object, so a credential left on `raw` would be written twice.
|
|
85
|
+
const safeRaw = redactAgentInboxItem(item.raw);
|
|
86
|
+
const write = () => {
|
|
87
|
+
const recoveringInterruptedMirror = fs.existsSync(dest);
|
|
88
|
+
writePrivateFile(dest, `${JSON.stringify(safeRaw, null, 2)}\n`, 0o600);
|
|
89
|
+
if (!recoveringInterruptedMirror || !inboxJournalContains(paths, item.id)) {
|
|
90
|
+
fs.appendFileSync(path.join(paths.inboxDir, INBOX_JSONL_NAME), `${JSON.stringify({ id: item.id, mirroredAt: new Date().toISOString(), ...safeRaw })}\n`, { mode: 0o600 });
|
|
91
|
+
}
|
|
92
|
+
return dest;
|
|
93
|
+
};
|
|
94
|
+
// Create the directory first so the scrub lock has somewhere to live.
|
|
95
|
+
// The write still runs without the lock if the directory cannot be locked;
|
|
96
|
+
// dropping the message is worse than a rare overlapping rewrite.
|
|
97
|
+
mkdirPrivate(paths.inboxDir);
|
|
98
|
+
return withInboxMirrorLock(paths.inboxDir, 10_000, write) ?? write();
|
|
88
99
|
}
|
|
89
100
|
export const INBOX_ERROR_BODY_MAX = 300;
|
|
90
101
|
/**
|
|
@@ -142,6 +153,24 @@ export function defaultAckItem(agentUid, apiBaseUrl) {
|
|
|
142
153
|
export async function pollInboxOnce(deps) {
|
|
143
154
|
if (deps.signal?.aborted)
|
|
144
155
|
return { ok: false, mirrored: 0 };
|
|
156
|
+
// Mirrors written before credential redaction are scrubbed on the next poll,
|
|
157
|
+
// before a wake can send the bot at the files. A write or rename error is
|
|
158
|
+
// counted and retried on the next poll. The log is a count and errno codes,
|
|
159
|
+
// never a path or a file byte.
|
|
160
|
+
try {
|
|
161
|
+
scrubInboxMirror(deps.paths, (report) => {
|
|
162
|
+
if (report.scrubbed > 0) {
|
|
163
|
+
deps.log("info", `inbox scrubbed ${report.scrubbed} mirrored record(s) containing delegated credentials`);
|
|
164
|
+
}
|
|
165
|
+
if (report.failures > 0) {
|
|
166
|
+
const codes = report.codes.length > 0 ? report.codes.join(",") : "ERR";
|
|
167
|
+
deps.log("warn", `inbox scrub failed for ${report.failures} file(s) (${codes})`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
deps.log("warn", "inbox scrub failed for 1 file(s) (ERR)");
|
|
173
|
+
}
|
|
145
174
|
const fetchInbox = deps.fetchInbox ?? defaultFetchInbox(deps.agentUid, deps.apiBaseUrl);
|
|
146
175
|
const ackItem = deps.ackItem ?? defaultAckItem(deps.agentUid, deps.apiBaseUrl);
|
|
147
176
|
// No default cache warm: the human conversation routes 403 for agent callers.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
|
|
2
2
|
export declare const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
|
|
3
3
|
export declare const HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER = "hq-anywhere-runtime.revoked";
|
|
4
|
+
export declare const HQ_ANYWHERE_RUNTIME_FALSE_MAX_AGE_MS: number;
|
|
5
|
+
/** An old disable decision must not survive an unavailable registry indefinitely. */
|
|
6
|
+
export declare function freshAnywhereRuntimeFlags(flags: Record<string, boolean>, fetchedAt: number, now?: number): Record<string, boolean>;
|
|
4
7
|
/** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
|
|
5
8
|
export declare function hqAnywhereRuntimeLocallyDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
6
9
|
//# sourceMappingURL=anywhere-runtime-flag-key.d.ts.map
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
|
|
2
2
|
export const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
|
|
3
3
|
export const HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER = "hq-anywhere-runtime.revoked";
|
|
4
|
+
export const HQ_ANYWHERE_RUNTIME_FALSE_MAX_AGE_MS = 60 * 60 * 1000;
|
|
5
|
+
/** An old disable decision must not survive an unavailable registry indefinitely. */
|
|
6
|
+
export function freshAnywhereRuntimeFlags(flags, fetchedAt, now = Date.now()) {
|
|
7
|
+
if (flags[HQ_ANYWHERE_RUNTIME_FLAG] !== false || now - fetchedAt < HQ_ANYWHERE_RUNTIME_FALSE_MAX_AGE_MS)
|
|
8
|
+
return flags;
|
|
9
|
+
const fresh = { ...flags };
|
|
10
|
+
delete fresh[HQ_ANYWHERE_RUNTIME_FLAG];
|
|
11
|
+
return fresh;
|
|
12
|
+
}
|
|
4
13
|
/** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
|
|
5
14
|
export function hqAnywhereRuntimeLocallyDisabled(env = process.env) {
|
|
6
15
|
const value = env.HQ_FLAG_HQ_ANYWHERE_RUNTIME;
|
package/dist/lib/bot/daemon.js
CHANGED
|
@@ -77,6 +77,8 @@ export function renderBotLaunchdPlist(p) {
|
|
|
77
77
|
["HOME", p.home],
|
|
78
78
|
["PATH", p.pathEntries.join(":")],
|
|
79
79
|
["HQ_NO_UPDATE_CHECK", "1"],
|
|
80
|
+
// Bots must never replace/relaunch the interactive desktop, including during setup.
|
|
81
|
+
["HQ_NO_ENSURE_HQ_DESKTOP", "1"],
|
|
80
82
|
...(p.hqRoot ? [["HQ_ROOT", p.hqRoot]] : []),
|
|
81
83
|
]
|
|
82
84
|
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
@@ -135,6 +137,7 @@ RestartSec=5
|
|
|
135
137
|
Environment=HOME=${p.home}
|
|
136
138
|
Environment=PATH=${p.pathEntries.join(":")}
|
|
137
139
|
Environment=HQ_NO_UPDATE_CHECK=1
|
|
140
|
+
Environment=HQ_NO_ENSURE_HQ_DESKTOP=1
|
|
138
141
|
${p.hqRoot ? `Environment=HQ_ROOT=${p.hqRoot}\n` : ""}StandardOutput=append:${p.logPath}
|
|
139
142
|
StandardError=append:${p.logPath}
|
|
140
143
|
|
|
@@ -345,7 +345,9 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
|
345
345
|
const recheck = memo !== undefined && (nowMs() - memo.at >= MODEL_FALLBACK_RECHECK_MS || memo.turns >= MODEL_FALLBACK_RECHECK_TURNS);
|
|
346
346
|
const remembered = memo && !recheck ? memo : undefined;
|
|
347
347
|
const args = runtime.buildArgs(remembered ? { ...turnInput, model: remembered.to } : turnInput, { outputFile, newSessionId });
|
|
348
|
-
|
|
348
|
+
// Enforce at the model boundary too: old plists and direct `bot run` callers
|
|
349
|
+
// may lack the guard. A bot prompt must never quit the user's desktop app.
|
|
350
|
+
const env = { ...(input.env ?? deps.env ?? process.env), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
349
351
|
const binary = runtime.resolveBinary?.(env, input.cwd, turnInput.platform) ?? runtime.binary;
|
|
350
352
|
const stdin = runtime.stdinInput?.(turnInput);
|
|
351
353
|
const protocol = runtime.spawnProtocol?.(turnInput);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { removeEntryNoFollow } from "../utils/safe-link.js";
|
|
3
4
|
const WIN32_RESERVED_CHARS = /[<>:"/\\|?*]/;
|
|
4
5
|
const WIN32_DEVICE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
5
6
|
const LOCAL_SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -227,7 +228,8 @@ function pruneStaleNamespacedWrappers(runtimeRoot, currentWrapperName, hqRoot, c
|
|
|
227
228
|
if (LOCAL_SLUG.test(skillSlug) &&
|
|
228
229
|
!fs.existsSync(canonicalSkill) &&
|
|
229
230
|
isGeneratedNamespacedWrapper(candidate, hqRoot, companySlug)) {
|
|
230
|
-
|
|
231
|
+
// A Windows wrapper is a junction; never recurse into its target.
|
|
232
|
+
removeEntryNoFollow(candidate);
|
|
231
233
|
removeGeneratedMarker(candidate);
|
|
232
234
|
}
|
|
233
235
|
}
|
|
@@ -340,7 +342,7 @@ export function surfaceCompanySkill(input) {
|
|
|
340
342
|
}
|
|
341
343
|
if (previousMoved) {
|
|
342
344
|
try {
|
|
343
|
-
|
|
345
|
+
removeEntryNoFollow(backupPath, { force: true });
|
|
344
346
|
}
|
|
345
347
|
catch {
|
|
346
348
|
// The new wrapper is already committed. A hidden generated backup is
|
|
@@ -354,13 +356,13 @@ export function surfaceCompanySkill(input) {
|
|
|
354
356
|
for (const legacyPath of [legacyBase, `${legacyBase}.md`]) {
|
|
355
357
|
if (legacyPath !== wrapperPath &&
|
|
356
358
|
isGeneratedLegacyEntry(legacyPath, sourceDir, skillFile)) {
|
|
357
|
-
|
|
359
|
+
removeEntryNoFollow(legacyPath);
|
|
358
360
|
}
|
|
359
361
|
}
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
364
|
catch (error) {
|
|
363
|
-
|
|
365
|
+
removeEntryNoFollow(stagingPath, { force: true });
|
|
364
366
|
throw error;
|
|
365
367
|
}
|
|
366
368
|
return { status: 0, wrapperPath };
|