@indigoai-us/hq-cli 5.345.170 → 5.345.172
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 +84 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +206 -7
- package/dist/command-catalog.generated.js +252 -8
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +112 -3
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +179 -18
- package/dist/commands/cloud.d.ts +58 -1
- package/dist/commands/cloud.js +228 -10
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/daemon.js +16 -1
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +22 -4
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/commands/sync-core.d.ts +22 -0
- package/dist/commands/sync-core.js +124 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/fleet-local.d.ts +142 -0
- package/dist/lib/bot/fleet-local.js +689 -0
- package/dist/lib/bot/fleet-python.d.ts +47 -0
- package/dist/lib/bot/fleet-python.js +218 -0
- package/dist/lib/bot/index.d.ts +2 -0
- package/dist/lib/bot/index.js +2 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/prompt.d.ts +6 -1
- package/dist/lib/bot/prompt.js +8 -3
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.d.ts +12 -4
- package/dist/lib/daemon/catalog.js +70 -24
- package/dist/lib/daemon/config.d.ts +0 -7
- package/dist/lib/daemon/config.js +0 -32
- package/dist/lib/daemon/run.d.ts +5 -1
- package/dist/lib/daemon/run.js +48 -11
- package/dist/lib/daemon/supervisor.js +3 -1
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/fix/apply.js +54 -0
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -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/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/lib/sync-core/engine-home.d.ts +44 -0
- package/dist/lib/sync-core/engine-home.js +193 -0
- package/dist/lib/sync-core/flag.d.ts +17 -0
- package/dist/lib/sync-core/flag.js +47 -0
- package/dist/lib/sync-core/reconcile.d.ts +31 -0
- package/dist/lib/sync-core/reconcile.js +196 -0
- package/dist/lib/sync-core/release.d.ts +32 -0
- package/dist/lib/sync-core/release.js +140 -0
- package/dist/lib/sync-core/rollout-state.d.ts +24 -0
- package/dist/lib/sync-core/rollout-state.js +94 -0
- package/dist/lib/vault-sync-switches.d.ts +5 -0
- package/dist/lib/vault-sync-switches.js +43 -0
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/remote-list-not-converging.d.ts +21 -0
- package/dist/utils/remote-list-not-converging.js +40 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
- package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
- package/dist/lib/daemon/sync-core-canary.js +0 -56
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-state checks that decide machine vs person Cognito tokens. Reads env
|
|
3
|
+
* and files only; no network code, so fast native entries can import it
|
|
4
|
+
* (e.g. via src/lib/caller-principal.ts) without pulling cognito-session.ts.
|
|
5
|
+
*/
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import { accessTokenFingerprint, isMachineIdentity } from "@indigoai-us/hq-cloud";
|
|
9
|
+
import { personTokenCacheFile, readPersonTokenFile } from "./person-token-cache.js";
|
|
10
|
+
/**
|
|
11
|
+
* True when `HQ_MACHINE_CREDS_FILE` is explicitly set (non-empty) in `env`.
|
|
12
|
+
* Explicit env remains the primary opt-in for daemon units. Fresh agent boxes
|
|
13
|
+
* without the env still mint via {@link wantsMachineCognitoTokens}'s
|
|
14
|
+
* default-file fallback when no usable person session exists.
|
|
15
|
+
*/
|
|
16
|
+
export function isMachineCredsFileEnvSet(env = process.env) {
|
|
17
|
+
const raw = env.HQ_MACHINE_CREDS_FILE;
|
|
18
|
+
return typeof raw === "string" && raw.trim().length > 0;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Align `process.env.HQ_MACHINE_CREDS_FILE` with a (possibly synthetic) `env`
|
|
22
|
+
* for the duration of `fn`, so hq-cloud's `isMachineIdentity()` sees the same
|
|
23
|
+
* value doctor / tests intended.
|
|
24
|
+
*/
|
|
25
|
+
function withAlignedMachineCredsEnv(env, fn) {
|
|
26
|
+
const prevCreds = process.env.HQ_MACHINE_CREDS_FILE;
|
|
27
|
+
const nextCreds = env.HQ_MACHINE_CREDS_FILE;
|
|
28
|
+
if (nextCreds !== prevCreds) {
|
|
29
|
+
if (nextCreds === undefined || String(nextCreds).trim() === "") {
|
|
30
|
+
delete process.env.HQ_MACHINE_CREDS_FILE;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
process.env.HQ_MACHINE_CREDS_FILE = nextCreds;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return fn();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (nextCreds !== prevCreds) {
|
|
41
|
+
if (prevCreds === undefined)
|
|
42
|
+
delete process.env.HQ_MACHINE_CREDS_FILE;
|
|
43
|
+
else
|
|
44
|
+
process.env.HQ_MACHINE_CREDS_FILE = prevCreds;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* True when a usable person Cognito cache file exists under the effective
|
|
50
|
+
* `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
|
|
51
|
+
* person path even when the default machine-creds file is also on disk
|
|
52
|
+
* (no-flip guarantee).
|
|
53
|
+
*/
|
|
54
|
+
export function hasPersonCachedSession(env = process.env) {
|
|
55
|
+
try {
|
|
56
|
+
const file = personTokenCacheFile(os.homedir(), env);
|
|
57
|
+
const raw = readPersonTokenFile(os.homedir(), env);
|
|
58
|
+
if (!raw ||
|
|
59
|
+
typeof raw.accessToken !== "string" ||
|
|
60
|
+
raw.accessToken.length === 0 ||
|
|
61
|
+
typeof raw.idToken !== "string" ||
|
|
62
|
+
raw.idToken.length === 0) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// Definitive refresh rejection writes `${file}.invalid.<fingerprint>`;
|
|
66
|
+
// treat that as no usable person session so status/whoami report machine.
|
|
67
|
+
const marker = `${file}.invalid.${accessTokenFingerprint(raw.accessToken)}`;
|
|
68
|
+
if (fs.existsSync(marker))
|
|
69
|
+
return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether hq-cloud currently sees readable machine creds (env override or
|
|
78
|
+
* default `~/.hq-agent/machine-creds.json`), honoring a synthetic `env`.
|
|
79
|
+
*/
|
|
80
|
+
export function probeMachineIdentity(env) {
|
|
81
|
+
return withAlignedMachineCredsEnv(env, () => isMachineIdentity());
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether this call should mint/cache via machine creds (USER_PASSWORD_AUTH).
|
|
85
|
+
*
|
|
86
|
+
* Machine path when:
|
|
87
|
+
* - explicit `{ tokenSource: "machine" }` and creds are readable, or
|
|
88
|
+
* - `HQ_MACHINE_CREDS_FILE` is set and readable, or
|
|
89
|
+
* - fresh-box fallback: env unset, no person cache, default
|
|
90
|
+
* `~/.hq-agent/machine-creds.json` readable.
|
|
91
|
+
*
|
|
92
|
+
* Explicit `{ tokenSource: "person" }` never uses machine creds. A present
|
|
93
|
+
* person cache keeps the person path even when the default creds file exists
|
|
94
|
+
* (do not flip a human's commands onto machine tokens).
|
|
95
|
+
*/
|
|
96
|
+
export function wantsMachineCognitoTokens(options = {}, env = process.env) {
|
|
97
|
+
if (options.tokenSource === "person")
|
|
98
|
+
return false;
|
|
99
|
+
if (options.tokenSource === "machine") {
|
|
100
|
+
return probeMachineIdentity(env);
|
|
101
|
+
}
|
|
102
|
+
if (isMachineCredsFileEnvSet(env)) {
|
|
103
|
+
return probeMachineIdentity(env);
|
|
104
|
+
}
|
|
105
|
+
// Fresh-box fallback — default creds file only when no person session.
|
|
106
|
+
if (hasPersonCachedSession(env))
|
|
107
|
+
return false;
|
|
108
|
+
return probeMachineIdentity(env);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=machine-principal-state.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-language rendering for hq-cloud's `RemoteListNotConvergingError`
|
|
3
|
+
* (code `REMOTE_LIST_NOT_CONVERGING`, hq-cloud >= 6.18.102). hq-cloud throws
|
|
4
|
+
* it when a company's vault file listing keeps stopping early (a repeated
|
|
5
|
+
* cursor, or a truncated page with no cursor) after its retries run out.
|
|
6
|
+
*
|
|
7
|
+
* Duck-typed on `code` so hq-cli stays compatible while hq-cloud and the CLI
|
|
8
|
+
* are released independently.
|
|
9
|
+
*/
|
|
10
|
+
export declare const REMOTE_LIST_NOT_CONVERGING_CODE = "REMOTE_LIST_NOT_CONVERGING";
|
|
11
|
+
export declare const REMOTE_LIST_NOT_CONVERGING_HINT: string;
|
|
12
|
+
export declare function isRemoteListNotConvergingError(err: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the user-facing message for a non-converging remote listing, with
|
|
15
|
+
* the original engine detail on a second line for bug reports, or null when
|
|
16
|
+
* `err` is some other failure.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatRemoteListNotConverging(err: unknown): string | null;
|
|
19
|
+
/** Remote-list hint when it applies, else the raw error text. */
|
|
20
|
+
export declare function describeSyncError(err: unknown): string;
|
|
21
|
+
//# sourceMappingURL=remote-list-not-converging.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-language rendering for hq-cloud's `RemoteListNotConvergingError`
|
|
3
|
+
* (code `REMOTE_LIST_NOT_CONVERGING`, hq-cloud >= 6.18.102). hq-cloud throws
|
|
4
|
+
* it when a company's vault file listing keeps stopping early (a repeated
|
|
5
|
+
* cursor, or a truncated page with no cursor) after its retries run out.
|
|
6
|
+
*
|
|
7
|
+
* Duck-typed on `code` so hq-cli stays compatible while hq-cloud and the CLI
|
|
8
|
+
* are released independently.
|
|
9
|
+
*/
|
|
10
|
+
export const REMOTE_LIST_NOT_CONVERGING_CODE = "REMOTE_LIST_NOT_CONVERGING";
|
|
11
|
+
export const REMOTE_LIST_NOT_CONVERGING_HINT = "HQ could not finish listing this company's files. Try hq sync now again in a minute; " +
|
|
12
|
+
"if it keeps happening, run hq feedback bug.";
|
|
13
|
+
export function isRemoteListNotConvergingError(err) {
|
|
14
|
+
return (typeof err === "object" &&
|
|
15
|
+
err !== null &&
|
|
16
|
+
err.code === REMOTE_LIST_NOT_CONVERGING_CODE);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns the user-facing message for a non-converging remote listing, with
|
|
20
|
+
* the original engine detail on a second line for bug reports, or null when
|
|
21
|
+
* `err` is some other failure.
|
|
22
|
+
*/
|
|
23
|
+
export function formatRemoteListNotConverging(err) {
|
|
24
|
+
if (!isRemoteListNotConvergingError(err))
|
|
25
|
+
return null;
|
|
26
|
+
const detail = err instanceof Error
|
|
27
|
+
? err.message
|
|
28
|
+
: typeof err.message === "string"
|
|
29
|
+
? err.message
|
|
30
|
+
: "";
|
|
31
|
+
return detail.length > 0
|
|
32
|
+
? `${REMOTE_LIST_NOT_CONVERGING_HINT}\nDetail: ${detail}`
|
|
33
|
+
: REMOTE_LIST_NOT_CONVERGING_HINT;
|
|
34
|
+
}
|
|
35
|
+
/** Remote-list hint when it applies, else the raw error text. */
|
|
36
|
+
export function describeSyncError(err) {
|
|
37
|
+
return (formatRemoteListNotConverging(err) ??
|
|
38
|
+
(err instanceof Error ? err.message : String(err)));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=remote-list-not-converging.js.map
|
package/dist/utils/safe-link.js
CHANGED
|
@@ -284,6 +284,9 @@ export function removeLinkOnly(linkPath) {
|
|
|
284
284
|
if (!st.isSymbolicLink()) {
|
|
285
285
|
throw new Error(`Refusing to remove ${linkPath}: it is not a link.`);
|
|
286
286
|
}
|
|
287
|
+
// Re-check immediately before each removal call, so an entry swapped for a
|
|
288
|
+
// real file or folder after the first check is refused, not removed.
|
|
289
|
+
assertStillLink(linkPath);
|
|
287
290
|
try {
|
|
288
291
|
fs.unlinkSync(linkPath);
|
|
289
292
|
}
|
|
@@ -293,8 +296,14 @@ export function removeLinkOnly(linkPath) {
|
|
|
293
296
|
// unlink(). A non-recursive rmdir removes only the junction entry.
|
|
294
297
|
if (code !== 'EPERM' && code !== 'EISDIR')
|
|
295
298
|
throw err;
|
|
299
|
+
assertStillLink(linkPath);
|
|
296
300
|
fs.rmdirSync(linkPath);
|
|
297
301
|
}
|
|
298
302
|
return true;
|
|
299
303
|
}
|
|
304
|
+
function assertStillLink(linkPath) {
|
|
305
|
+
if (!fs.lstatSync(linkPath).isSymbolicLink()) {
|
|
306
|
+
throw new Error(`Refusing to remove ${linkPath}: it is no longer a link.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
300
309
|
//# sourceMappingURL=safe-link.js.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind a script-locked secret release to the process that is actually run
|
|
3
|
+
* (feedback_0e6abcb8).
|
|
4
|
+
*
|
|
5
|
+
* `--script <path>` sends the script's path and hash to the vault, which
|
|
6
|
+
* releases a script-locked secret when that pair is approved. Before this
|
|
7
|
+
* module the command after `--` was never compared with that script, so any
|
|
8
|
+
* command that named an approved script received the secret.
|
|
9
|
+
*
|
|
10
|
+
* When any requested secret is script-locked (or its lock state is unknown),
|
|
11
|
+
* the command after `--` must be one of:
|
|
12
|
+
* 1. the approved script itself: `-- ./approved.sh [args...]` (realpath
|
|
13
|
+
* match). Its `#!` line is parsed and the interpreter it names goes
|
|
14
|
+
* through the same allowlist; `#!/usr/bin/env X` resolves X against a
|
|
15
|
+
* fixed system PATH, never the caller's PATH;
|
|
16
|
+
* 2. an allowlisted interpreter followed directly by the script:
|
|
17
|
+
* `-- bash approved.sh [args...]`, also resolved against the fixed PATH.
|
|
18
|
+
* Anything else (wrappers such as env, xargs, sudo, a shell `-c` string, an
|
|
19
|
+
* interpreter option before the script) is refused and nothing is injected.
|
|
20
|
+
*
|
|
21
|
+
* Allowlisted interpreters: sh, dash, ksh, bash (run with --noprofile
|
|
22
|
+
* --norc), zsh (run with -f), python3/python (run with -I -S), node. Ruby and
|
|
23
|
+
* perl are not allowlisted. The interpreter's realpath and every parent
|
|
24
|
+
* directory must be owned by root and not group- or world-writable, so
|
|
25
|
+
* Homebrew or other user-writable installs are refused.
|
|
26
|
+
*
|
|
27
|
+
* At spawn time the script bytes are read once, hashed, compared with the hash
|
|
28
|
+
* sent to the vault, written to a fresh 0700 private directory, re-hashed, and
|
|
29
|
+
* that copy is passed to the interpreter. The original path is exposed to the
|
|
30
|
+
* script as HQ_APPROVED_SCRIPT and its directory as HQ_APPROVED_SCRIPT_DIR.
|
|
31
|
+
* `$0` / `__file__` / `__dirname` name the copy, so a script that loads
|
|
32
|
+
* neighbour files relative to itself must use HQ_APPROVED_SCRIPT_DIR. The
|
|
33
|
+
* working directory is left as the caller's.
|
|
34
|
+
*
|
|
35
|
+
* The child environment gets a fixed system PATH and loses the variables that
|
|
36
|
+
* make an interpreter, the loader or the macOS xcrun shims run other code.
|
|
37
|
+
* The same names are refused when they arrive in the injected secrets or in
|
|
38
|
+
* an env-schema / env-local value, because those are merged last.
|
|
39
|
+
*
|
|
40
|
+
* The child environment is built from an allowlist (see scriptBoundChildEnv),
|
|
41
|
+
* not inherited from the caller.
|
|
42
|
+
*
|
|
43
|
+
* What the binding does and does not protect: it controls which entrypoint
|
|
44
|
+
* receives the secret. It does not isolate what that entrypoint runs. A
|
|
45
|
+
* same-user caller can still influence tools the script calls through files
|
|
46
|
+
* in HOME (for example ~/.gitconfig and fsmonitor hooks, ~/.curlrc,
|
|
47
|
+
* ~/.ssh/config, ~/.netrc) or through anything else the user can write. Real
|
|
48
|
+
* isolation needs the vault-side sandbox release (`hq secrets sandbox`).
|
|
49
|
+
*
|
|
50
|
+
* Remaining limits, documented on purpose:
|
|
51
|
+
* - Only the script file is hashed. Files it sources, requires or imports
|
|
52
|
+
* (neighbour files, modules) are not covered by the approval.
|
|
53
|
+
* - A process running as the same user can still race the private copy
|
|
54
|
+
* between its re-hash and the interpreter opening it. The window is short
|
|
55
|
+
* and the path is random, but same-user code is not a boundary this
|
|
56
|
+
* client-side check can enforce.
|
|
57
|
+
* - The check runs in the CLI. A modified CLI, or a direct call to the vault
|
|
58
|
+
* API with a self-asserted hash, is outside what it can stop.
|
|
59
|
+
*/
|
|
60
|
+
export declare class ScriptBindingError extends Error {
|
|
61
|
+
constructor(message: string);
|
|
62
|
+
}
|
|
63
|
+
/** Root-owned search path used for interpreters and for the child. */
|
|
64
|
+
export declare const FIXED_SYSTEM_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
|
|
65
|
+
export interface ScriptBoundCommand {
|
|
66
|
+
scriptRealPath: string;
|
|
67
|
+
interpreterPath: string;
|
|
68
|
+
interpreterArgs: string[];
|
|
69
|
+
scriptArgs: string[];
|
|
70
|
+
/** sha256 of the script when the command was resolved. */
|
|
71
|
+
sha256: string;
|
|
72
|
+
}
|
|
73
|
+
export interface PreparedBoundSpawn {
|
|
74
|
+
file: string;
|
|
75
|
+
args: string[];
|
|
76
|
+
copyPath: string;
|
|
77
|
+
cleanup: () => void;
|
|
78
|
+
}
|
|
79
|
+
/** True for a variable a bound script's environment must not take from the caller. */
|
|
80
|
+
export declare function isDeniedEnvName(name: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Schema / local env literal names a bound `hq run --script` launch accepts
|
|
83
|
+
* (fifth review). Literals are caller-written, so instead of denying
|
|
84
|
+
* tool-config names one by one, only these app-level switches are allowed.
|
|
85
|
+
* Everything else must come from the vault through hq().
|
|
86
|
+
*/
|
|
87
|
+
export declare const SAFE_LITERAL_NAMES: Set<string>;
|
|
88
|
+
/** Refuse schema literals that are not on SAFE_LITERAL_NAMES (or are denied). */
|
|
89
|
+
export declare function assertLiteralNamesAllowed(names: Iterable<string>): void;
|
|
90
|
+
/**
|
|
91
|
+
* Design (feedback_0e6abcb8, fourth review): an injected name is accepted in a
|
|
92
|
+
* bound launch only when it is not on the loader/interpreter denylist, not
|
|
93
|
+
* one of the inherited allowlist names (HOME, USER, LANG, LC_*, TZ, TMPDIR,
|
|
94
|
+
* ...), and not on the explicit dangerous list above. Ordinary application
|
|
95
|
+
* names (FAKE_API_KEY, DATABASE_URL, NODE_ENV) pass.
|
|
96
|
+
*/
|
|
97
|
+
export declare function isDeniedInjectedName(name: string): boolean;
|
|
98
|
+
/** Refuse a bound launch whose injected values use a denied name. */
|
|
99
|
+
export declare function assertInjectedNamesAllowed(names: Iterable<string>): void;
|
|
100
|
+
export declare function privateCopyRoot(): string;
|
|
101
|
+
/**
|
|
102
|
+
* True when realPath and every parent directory are owned by root and not
|
|
103
|
+
* group- or world-writable.
|
|
104
|
+
*/
|
|
105
|
+
export declare function isRootOwnedChain(realPath: string): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the command after `--` against the approved script. Throws
|
|
108
|
+
* ScriptBindingError when the command is not exactly the script invocation.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveScriptBoundCommand(scriptPath: string, childArgs: string[]): Promise<ScriptBoundCommand>;
|
|
111
|
+
/** Compare the script on disk with the hash sent to the vault. */
|
|
112
|
+
export declare function assertScriptUnchanged(bound: ScriptBoundCommand, attestedSha256: string | undefined): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Read the script once, check its hash, copy those bytes into a fresh 0700
|
|
115
|
+
* directory, re-hash the copy, and return the spawn target that runs the copy.
|
|
116
|
+
*/
|
|
117
|
+
export declare function prepareBoundSpawn(bound: ScriptBoundCommand, attestedSha256: string | undefined): PreparedBoundSpawn;
|
|
118
|
+
/**
|
|
119
|
+
* Remove the private copy when the parent gets SIGINT, SIGTERM or SIGHUP, or
|
|
120
|
+
* exits, then re-raise the signal so the default termination still happens.
|
|
121
|
+
* SIGKILL cannot be handled; a copy left by it stays in the 0700 temp
|
|
122
|
+
* directory until the OS cleans the temp dir.
|
|
123
|
+
*/
|
|
124
|
+
export declare function cleanupOnParentSignals(cleanup: () => void, proc?: Pick<NodeJS.Process, "once" | "removeListener" | "kill" | "pid">): () => void;
|
|
125
|
+
/**
|
|
126
|
+
* Child environment for a bound script, built from an allowlist: HOME, USER,
|
|
127
|
+
* LOGNAME, LANG, LC_*, TERM, TZ and TMPDIR from the caller, a fixed PATH, and
|
|
128
|
+
* the HQ_APPROVED_* variables. Injected secrets and schema values are added by
|
|
129
|
+
* the caller after a denylist check.
|
|
130
|
+
*/
|
|
131
|
+
export declare function scriptBoundChildEnv(env: NodeJS.ProcessEnv, scriptRealPath?: string): NodeJS.ProcessEnv;
|
|
132
|
+
/**
|
|
133
|
+
* Whether the release must be bound to the script. Only rows the vault marked
|
|
134
|
+
* `scriptLock.mode: "off"` are exempt; enforced or unknown fails closed.
|
|
135
|
+
*/
|
|
136
|
+
export declare function requiresScriptBinding(keys: string[], lockModes: Map<string, string | undefined>): boolean;
|
|
137
|
+
export interface ScriptBindingPlan {
|
|
138
|
+
bound?: ScriptBoundCommand;
|
|
139
|
+
error?: ScriptBindingError;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the binding before any fetch. A refusal is held, not thrown, so a
|
|
143
|
+
* caller whose secrets turn out not to be script-locked keeps working.
|
|
144
|
+
*/
|
|
145
|
+
export declare function planScriptBinding(scriptPath: string | undefined, childArgs: string[]): Promise<ScriptBindingPlan | undefined>;
|
|
146
|
+
export interface ChildLaunch {
|
|
147
|
+
file: string;
|
|
148
|
+
args: string[];
|
|
149
|
+
env: NodeJS.ProcessEnv;
|
|
150
|
+
cleanup: () => void;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Pick what to spawn once the vault has answered. Unlocked secrets keep the
|
|
154
|
+
* caller's command unchanged; enforced or unknown lock state requires the
|
|
155
|
+
* bound script and throws ScriptBindingError otherwise.
|
|
156
|
+
*/
|
|
157
|
+
export declare function selectChildLaunch(plan: ScriptBindingPlan | undefined, keys: string[], lockModes: Map<string, string | undefined>, attestedSha256: string | undefined, childArgs: string[], env: NodeJS.ProcessEnv, injected: Record<string, string>): ChildLaunch;
|
|
158
|
+
//# sourceMappingURL=script-bound-exec.d.ts.map
|