@indigoai-us/hq-cli 5.345.155 → 5.345.156
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 +58 -0
- package/dist/command-catalog.generated.d.ts +5 -1
- package/dist/command-catalog.generated.js +6 -1
- package/dist/commands/agent-inbox.js +2 -2
- package/dist/commands/core.js +1 -1
- package/dist/commands/files-browse.d.ts +16 -0
- package/dist/commands/files-browse.js +52 -1
- 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/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +72 -3
- 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/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/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 +3 -2
- 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 +4 -2
- 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/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.156] — 2026-10-21
|
|
6
|
+
- `hq doctor --fix` stops pulling a company the sync service could not find
|
|
7
|
+
for your HQ account. When a pull fails because the company does not exist
|
|
8
|
+
for you or access is refused, the next check shows that company's stale
|
|
9
|
+
sync journal as "not on this machine" with one next step: check you are
|
|
10
|
+
signed in to the right account and ask to be re-invited, or delete the old
|
|
11
|
+
journal. It is no longer auto-fixable, so the client-health hook stops
|
|
12
|
+
reporting it. This classification requires another company pull to succeed
|
|
13
|
+
in the same run; an outage affecting every company remains visible. Any
|
|
14
|
+
later successful sync invalidates the cached failure, and `--fix` retries
|
|
15
|
+
unresolved companies after 14 days.
|
|
16
|
+
- The same company's stale sync manifest now follows its journal: it shows as
|
|
17
|
+
"not on this machine" and `--fix` stops re-uploading it. A manifest upload
|
|
18
|
+
that fails with "No membership found" counts as the same evidence, under the
|
|
19
|
+
same rule that another company must succeed in that run. A later successful
|
|
20
|
+
sync or manifest upload clears it.
|
|
21
|
+
- One `hq doctor --fix` run now refreshes up to 10 stale sync journals
|
|
22
|
+
instead of 2. Unattended runs (`--yes`) stop starting new pulls after 120
|
|
23
|
+
seconds so they still finish inside the health hook's time limit. When a
|
|
24
|
+
run stops early it says how many are left and asks you to run it again.
|
|
25
|
+
- `hq files cat` / `get` and the agent file tools now say why a signed vault download failed after access was granted: a response that did not come from the storage host is reported as a likely proxy or egress block naming the host to allow, and a storage refusal shows S3's error code and request id. These failures no longer send you to `hq access`, which only re-checked the grant that had already passed.
|
|
26
|
+
- `hq agent inbox --json` and the agent `hq_inbox_read` tool now include `source`: `server` when the server inbox was read, `local-mirror` when the server could not be reached and the list may be stale, so automation can tell a stale fallback from a real empty inbox.
|
|
27
|
+
- Fix company knowledge and project index rebuilds to honor help and explicit
|
|
28
|
+
company arguments. Invalid arguments fail before writing, and a
|
|
29
|
+
company-scoped project rebuild leaves the personal index untouched.
|
|
30
|
+
- `hq integrations call` has a new `--timeout <seconds>` option (default 270,
|
|
31
|
+
max 1800) that sets how long it waits for a call the gateway runs as a
|
|
32
|
+
background job. When a call that asked for background execution instead
|
|
33
|
+
comes back as an inline HTTP 500, 502 or 504, the error now says the call
|
|
34
|
+
likely hit the gateway's 30-second inline limit and that async integration
|
|
35
|
+
jobs must be enabled for the company, instead of only suggesting a retry.
|
|
36
|
+
Feedback 4b2573d6-fab7-4b63-8fa6-f50b472a887f.
|
|
37
|
+
- Every `hq` command (including `hq secrets exec`) sends a client-health
|
|
38
|
+
heartbeat, and about once every 15 minutes that heartbeat re-reads the local
|
|
39
|
+
sync journals. That read loads the whole local sync state store into memory,
|
|
40
|
+
and on a large store it took the `hq` process itself to several GB. Commands
|
|
41
|
+
that are not sync commands now do this read in a separate child process
|
|
42
|
+
capped at a 512 MB heap and 20 seconds. If the child hits a cap, the command
|
|
43
|
+
keeps the last known sync time and does not retry the read for 15 minutes.
|
|
44
|
+
Sync commands still read the journals in-process.
|
|
45
|
+
Feedback d70e9d63-7b46-42bc-8c28-a545235cd7c6.
|
|
46
|
+
- `hq secrets script approve --remote-path` now accepts a path relative to the
|
|
47
|
+
HQ root (for example `companies/<co>/skills/<skill>/<file>.mjs`). Once
|
|
48
|
+
hq-pro has the matching change deployed, that one approval matches the
|
|
49
|
+
script on every HQ root, including a hosted agent's. For a relative path a
|
|
50
|
+
leading `./` is dropped and `\` becomes `/`. Approve refuses, with a plain
|
|
51
|
+
error and no API call, a relative path that could never match: a bare file
|
|
52
|
+
name with no folder, a `.`, `..` or empty segment, a trailing `/`, a leading
|
|
53
|
+
`~`, a drive letter without a separator, or surrounding whitespace. An
|
|
54
|
+
absolute path is sent exactly as typed and must match exactly. If the
|
|
55
|
+
server answers without an active approval for the id (for example the id was
|
|
56
|
+
revoked earlier), approve now fails with a hint to use a new `--id` instead
|
|
57
|
+
of printing success. The success line names the stored path.
|
|
58
|
+
Feedback 6e0a1040-4d8e-4743-863e-386eb98aa0a4.
|
|
59
|
+
- Installing the native hook engine no longer fails with `ERR_FS_CP_EEXIST` on
|
|
60
|
+
Node 24.22 and later. The engine is now copied into a new directory inside the
|
|
61
|
+
staging folder instead of onto the staging folder itself.
|
|
62
|
+
|
|
5
63
|
## [5.345.155] — 2026-10-21
|
|
6
64
|
- `hq secrets exec --personal` now works in a session locked to one company.
|
|
7
65
|
It injects your own personal-vault secrets into the command, read-only, and
|
|
@@ -2066,7 +2066,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
2066
2066
|
readonly hidden: false;
|
|
2067
2067
|
}, {
|
|
2068
2068
|
readonly flags: "--remote-path <path>";
|
|
2069
|
-
readonly description: "Script path
|
|
2069
|
+
readonly description: "Script path to approve instead of the local path. A path relative to the HQ root (for example companies/<co>/skills/<skill>/<file>) matches on every HQ root; an absolute path must match the runtime's path exactly";
|
|
2070
2070
|
readonly hidden: false;
|
|
2071
2071
|
}, {
|
|
2072
2072
|
readonly flags: "--attestation <level>";
|
|
@@ -5075,6 +5075,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5075
5075
|
readonly flags: "--idempotency-key <key>";
|
|
5076
5076
|
readonly description: "Stable key so retries execute at most once (default: generated)";
|
|
5077
5077
|
readonly hidden: false;
|
|
5078
|
+
}, {
|
|
5079
|
+
readonly flags: "--timeout <seconds>";
|
|
5080
|
+
readonly description: "How long to wait for a long call that runs as a background job (default 270, max 1800)";
|
|
5081
|
+
readonly hidden: false;
|
|
5078
5082
|
}, {
|
|
5079
5083
|
readonly flags: "--json";
|
|
5080
5084
|
readonly description: "Machine-readable output";
|
|
@@ -2481,7 +2481,7 @@ export const COMMAND_CATALOG = [
|
|
|
2481
2481
|
},
|
|
2482
2482
|
{
|
|
2483
2483
|
"flags": "--remote-path <path>",
|
|
2484
|
-
"description": "Script path
|
|
2484
|
+
"description": "Script path to approve instead of the local path. A path relative to the HQ root (for example companies/<co>/skills/<skill>/<file>) matches on every HQ root; an absolute path must match the runtime's path exactly",
|
|
2485
2485
|
"hidden": false
|
|
2486
2486
|
},
|
|
2487
2487
|
{
|
|
@@ -6145,6 +6145,11 @@ export const COMMAND_CATALOG = [
|
|
|
6145
6145
|
"description": "Stable key so retries execute at most once (default: generated)",
|
|
6146
6146
|
"hidden": false
|
|
6147
6147
|
},
|
|
6148
|
+
{
|
|
6149
|
+
"flags": "--timeout <seconds>",
|
|
6150
|
+
"description": "How long to wait for a long call that runs as a background job (default 270, max 1800)",
|
|
6151
|
+
"hidden": false
|
|
6152
|
+
},
|
|
6148
6153
|
{
|
|
6149
6154
|
"flags": "--json",
|
|
6150
6155
|
"description": "Machine-readable output",
|
|
@@ -26,7 +26,7 @@ export function registerAgentInboxCommand(agent) {
|
|
|
26
26
|
// Merged with the server, never the local mirror alone: a dead kit
|
|
27
27
|
// poller would otherwise make this command answer confidently with
|
|
28
28
|
// yesterday's mail and no sign anything was wrong.
|
|
29
|
-
const { entries, note } = await mergeAgentInbox(paths, async () => {
|
|
29
|
+
const { entries, note, source } = await mergeAgentInbox(paths, async () => {
|
|
30
30
|
const token = await ensureCognitoToken({ tokenSource: "machine", interactive: false });
|
|
31
31
|
const res = await vaultApiFetch({
|
|
32
32
|
token,
|
|
@@ -39,7 +39,7 @@ export function registerAgentInboxCommand(agent) {
|
|
|
39
39
|
.filter((e) => opts.all || !done.has(e.id))
|
|
40
40
|
.map((e) => summarizeInboxEntry(redactAgentInboxItem(e), done.has(e.id)));
|
|
41
41
|
if (opts.json) {
|
|
42
|
-
console.log(JSON.stringify({ count: rows.length, messages: rows, ...(note ? { note } : {}) }, null, 2));
|
|
42
|
+
console.log(JSON.stringify({ count: rows.length, source, messages: rows, ...(note ? { note } : {}) }, null, 2));
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
if (note)
|
package/dist/commands/core.js
CHANGED
|
@@ -663,7 +663,7 @@ export function registerCoreCommands(program) {
|
|
|
663
663
|
const rendererArgs = passthrough.filter((arg) => arg !== "--accept");
|
|
664
664
|
const isCompaniesTarget = entry.renderer === "companies";
|
|
665
665
|
const wantsHelp = rendererArgs.some((arg) => arg === "--help" || arg === "-h");
|
|
666
|
-
if (
|
|
666
|
+
if (wantsHelp && ["companies", "company-knowledge", "projects"].includes(entry.renderer)) {
|
|
667
667
|
renderIndexTarget(entry.renderer, { root: "", output: (message) => process.stdout.write(message) }, rendererArgs);
|
|
668
668
|
return;
|
|
669
669
|
}
|
|
@@ -443,6 +443,22 @@ export declare const PRESIGN_BUFFER_MAX_BYTES: number;
|
|
|
443
443
|
* access token + companyUid are captured here; the orchestrator just calls
|
|
444
444
|
* `send(...)` as if it held an S3 client.
|
|
445
445
|
*/
|
|
446
|
+
/** Error code for a presigned storage download that failed after the API authorized it. */
|
|
447
|
+
export declare const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAILED";
|
|
448
|
+
/**
|
|
449
|
+
* Build the error for a failed presigned download. The API already checked the
|
|
450
|
+
* caller's ACL before it signed the URL, so a failure here is a storage or
|
|
451
|
+
* network outcome, not a missing grant. The error carries no `status: 403` and
|
|
452
|
+
* a code that `isAccessDeniedError` treats as not an access denial, so the CLI
|
|
453
|
+
* does not send the caller to `hq access` (which only re-checks the ACL that
|
|
454
|
+
* already passed).
|
|
455
|
+
*
|
|
456
|
+
* A response that really came from S3 carries `x-amz-request-id`; we report
|
|
457
|
+
* S3's error code from the XML body. A response without it came from something
|
|
458
|
+
* between this machine and the storage host, usually a sandbox egress proxy or
|
|
459
|
+
* firewall that does not allow the bucket host, and we say so.
|
|
460
|
+
*/
|
|
461
|
+
export declare function presignedDownloadError(key: string, url: string, dl: Response): Promise<Error>;
|
|
446
462
|
export declare function createCompanyPresignClient(input: {
|
|
447
463
|
token: string;
|
|
448
464
|
companyUid: string;
|
|
@@ -709,6 +709,57 @@ export const PRESIGN_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
|
|
|
709
709
|
* access token + companyUid are captured here; the orchestrator just calls
|
|
710
710
|
* `send(...)` as if it held an S3 client.
|
|
711
711
|
*/
|
|
712
|
+
/** Error code for a presigned storage download that failed after the API authorized it. */
|
|
713
|
+
export const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAILED";
|
|
714
|
+
/**
|
|
715
|
+
* Build the error for a failed presigned download. The API already checked the
|
|
716
|
+
* caller's ACL before it signed the URL, so a failure here is a storage or
|
|
717
|
+
* network outcome, not a missing grant. The error carries no `status: 403` and
|
|
718
|
+
* a code that `isAccessDeniedError` treats as not an access denial, so the CLI
|
|
719
|
+
* does not send the caller to `hq access` (which only re-checks the ACL that
|
|
720
|
+
* already passed).
|
|
721
|
+
*
|
|
722
|
+
* A response that really came from S3 carries `x-amz-request-id`; we report
|
|
723
|
+
* S3's error code from the XML body. A response without it came from something
|
|
724
|
+
* between this machine and the storage host, usually a sandbox egress proxy or
|
|
725
|
+
* firewall that does not allow the bucket host, and we say so.
|
|
726
|
+
*/
|
|
727
|
+
export async function presignedDownloadError(key, url, dl) {
|
|
728
|
+
let host = "the storage host";
|
|
729
|
+
try {
|
|
730
|
+
host = new URL(url).host;
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
// keep the generic label
|
|
734
|
+
}
|
|
735
|
+
const requestId = dl.headers.get("x-amz-request-id");
|
|
736
|
+
const bodyText = (await dl.text().catch(() => "")).slice(0, 4096);
|
|
737
|
+
const fromStorage = requestId !== null && requestId.length > 0;
|
|
738
|
+
let message;
|
|
739
|
+
let storageCode;
|
|
740
|
+
if (fromStorage) {
|
|
741
|
+
storageCode = /<Code>([^<]{1,100})<\/Code>/.exec(bodyText)?.[1];
|
|
742
|
+
message =
|
|
743
|
+
`Failed to download '${key}' (HTTP ${dl.status}` +
|
|
744
|
+
(storageCode ? `, storage error ${storageCode}` : "") +
|
|
745
|
+
`, request id ${requestId}). Your file access was granted; the storage ` +
|
|
746
|
+
`service refused the signed download. Report this with: hq feedback bug`;
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
message =
|
|
750
|
+
`Failed to download '${key}' (HTTP ${dl.status}). Your file access was ` +
|
|
751
|
+
`granted, but the response did not come from ${host}. A proxy or network ` +
|
|
752
|
+
`egress rule on this machine likely blocked it. Allow outbound HTTPS to ` +
|
|
753
|
+
`${host} (HQ vault files are served from *.s3.us-east-1.amazonaws.com).`;
|
|
754
|
+
}
|
|
755
|
+
return Object.assign(new Error(message), {
|
|
756
|
+
code: STORAGE_DOWNLOAD_FAILED_CODE,
|
|
757
|
+
key,
|
|
758
|
+
httpStatus: dl.status,
|
|
759
|
+
fromStorage,
|
|
760
|
+
...(storageCode ? { storageCode } : {}),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
712
763
|
export function createCompanyPresignClient(input) {
|
|
713
764
|
const { token, companyUid, companySlug, baseUrl } = input;
|
|
714
765
|
const denied = (message, key, extra) => Object.assign(new Error(message), {
|
|
@@ -780,7 +831,7 @@ export function createCompanyPresignClient(input) {
|
|
|
780
831
|
}
|
|
781
832
|
const dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
|
|
782
833
|
if (!dl.ok) {
|
|
783
|
-
throw
|
|
834
|
+
throw await presignedDownloadError(key, first.url, dl);
|
|
784
835
|
}
|
|
785
836
|
// HQ-CLI-5: when the response declares a length at or below the ceiling,
|
|
786
837
|
// DRAIN it here — read the whole body before send() resolves and replay it
|
|
@@ -120,6 +120,16 @@ interface GatewayMessage {
|
|
|
120
120
|
}
|
|
121
121
|
export type GatewayErrorShape = "string_error" | "object_no_code" | "object_code" | "string_code" | "nested_code" | "empty" | "redacted_empty";
|
|
122
122
|
export type GatewayFailureCode = "invalid_envelope";
|
|
123
|
+
/** Default client wait for a background (202) integration job. */
|
|
124
|
+
export declare const ASYNC_GATEWAY_DEADLINE_MS: number;
|
|
125
|
+
/** Upper bound for `hq integrations call --timeout`, in seconds. */
|
|
126
|
+
export declare const MAX_CALL_TIMEOUT_SECONDS: number;
|
|
127
|
+
/**
|
|
128
|
+
* Parse `--timeout <seconds>` for `hq integrations call` into milliseconds.
|
|
129
|
+
* Bounds how long the CLI waits for a background job; it never shortens the
|
|
130
|
+
* gateway's own limits.
|
|
131
|
+
*/
|
|
132
|
+
export declare function parseCallTimeoutMs(raw: string | undefined): number;
|
|
123
133
|
export declare class IntegrationsCliError extends Error {
|
|
124
134
|
/**
|
|
125
135
|
* True when the error is the caller's request/state/permission (a client 4xx
|
|
@@ -338,7 +348,9 @@ export declare function resolveConnection(token: string, companyUid: string, app
|
|
|
338
348
|
}, recoveryOpts?: {
|
|
339
349
|
allowSingleRevoked?: boolean;
|
|
340
350
|
}): Promise<AdminConnection>;
|
|
341
|
-
export declare function callGateway(token: string, params: Record<string, unknown>, idempotencyKey?: string, method?: "tools/call" | "tools/list"
|
|
351
|
+
export declare function callGateway(token: string, params: Record<string, unknown>, idempotencyKey?: string, method?: "tools/call" | "tools/list", opts?: {
|
|
352
|
+
waitMs?: number;
|
|
353
|
+
}): Promise<GatewayMessage>;
|
|
342
354
|
/**
|
|
343
355
|
* Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
|
|
344
356
|
* where `text` is the provider's JSON. Unwrap to the inner payload; fall back
|
|
@@ -19,7 +19,43 @@ import { connectionHealthNotice } from "../lib/integrations/health.js";
|
|
|
19
19
|
import { bareProvider } from "../lib/integrations/provider-slug.js";
|
|
20
20
|
export { bareProvider } from "../lib/integrations/provider-slug.js";
|
|
21
21
|
const ASYNC_GATEWAY_POLL_INTERVAL_MS = 1_000;
|
|
22
|
-
|
|
22
|
+
/** Default client wait for a background (202) integration job. */
|
|
23
|
+
export const ASYNC_GATEWAY_DEADLINE_MS = 4 * 60_000 + 30_000;
|
|
24
|
+
/** Upper bound for `hq integrations call --timeout`, in seconds. */
|
|
25
|
+
export const MAX_CALL_TIMEOUT_SECONDS = 30 * 60;
|
|
26
|
+
/**
|
|
27
|
+
* Parse `--timeout <seconds>` for `hq integrations call` into milliseconds.
|
|
28
|
+
* Bounds how long the CLI waits for a background job; it never shortens the
|
|
29
|
+
* gateway's own limits.
|
|
30
|
+
*/
|
|
31
|
+
export function parseCallTimeoutMs(raw) {
|
|
32
|
+
if (raw === undefined)
|
|
33
|
+
return ASYNC_GATEWAY_DEADLINE_MS;
|
|
34
|
+
const trimmed = raw.trim();
|
|
35
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
36
|
+
throw new IntegrationsCliError(`--timeout must be a whole number of seconds (got "${raw}").`, { expected: true });
|
|
37
|
+
}
|
|
38
|
+
const seconds = Number(trimmed);
|
|
39
|
+
if (seconds < 1 || seconds > MAX_CALL_TIMEOUT_SECONDS) {
|
|
40
|
+
throw new IntegrationsCliError(`--timeout must be between 1 and ${MAX_CALL_TIMEOUT_SECONDS} seconds (got ${seconds}).`, { expected: true });
|
|
41
|
+
}
|
|
42
|
+
return seconds * 1_000;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Statuses the gateway returns when an inline (non-background) tools/call ran
|
|
46
|
+
* into the IntegrationMcpFunction 30s wall or its provider budget.
|
|
47
|
+
*/
|
|
48
|
+
function isInlineTimeoutStatus(status) {
|
|
49
|
+
return status === 500 || status === 502 || status === 504;
|
|
50
|
+
}
|
|
51
|
+
function inlineTimeoutHint(status) {
|
|
52
|
+
return (`${upstreamUnavailableMessage(status)} ` +
|
|
53
|
+
"If this was a long-running call (for example a large analytical read), it may have hit the " +
|
|
54
|
+
"gateway's 30-second limit: the gateway ran it inline, not as a background job. Long calls " +
|
|
55
|
+
"run as background jobs only when async integration jobs are enabled for your company; ask " +
|
|
56
|
+
"HQ support to enable them. Once enabled, hq integrations call waits for the result " +
|
|
57
|
+
"automatically (see --timeout).");
|
|
58
|
+
}
|
|
23
59
|
export class IntegrationsCliError extends Error {
|
|
24
60
|
/**
|
|
25
61
|
* True when the error is the caller's request/state/permission (a client 4xx
|
|
@@ -627,9 +663,9 @@ function gatewaySubcommandFor(params, method) {
|
|
|
627
663
|
return "tools_call";
|
|
628
664
|
return "other";
|
|
629
665
|
}
|
|
630
|
-
export async function callGateway(token, params, idempotencyKey, method = "tools/call") {
|
|
666
|
+
export async function callGateway(token, params, idempotencyKey, method = "tools/call", opts = {}) {
|
|
631
667
|
const gatewaySubcommand = gatewaySubcommandFor(params, method);
|
|
632
|
-
const deadline = Date.now() + ASYNC_GATEWAY_DEADLINE_MS;
|
|
668
|
+
const deadline = Date.now() + (opts.waitMs ?? ASYNC_GATEWAY_DEADLINE_MS);
|
|
633
669
|
const res = await vaultApiFetch({
|
|
634
670
|
token,
|
|
635
671
|
path: "/v1/integrations/mcp",
|
|
@@ -663,6 +699,15 @@ export async function callGateway(token, params, idempotencyKey, method = "tools
|
|
|
663
699
|
}
|
|
664
700
|
if (res.status !== 202 && (!res.ok || !terminalMessage)) {
|
|
665
701
|
raiseIfUnauthorized(res);
|
|
702
|
+
if (method === "tools/call" &&
|
|
703
|
+
params.acceptAsync === true &&
|
|
704
|
+
isInlineTimeoutStatus(res.status)) {
|
|
705
|
+
throw new IntegrationsCliError(inlineTimeoutHint(res.status), {
|
|
706
|
+
expected: true,
|
|
707
|
+
status: res.status,
|
|
708
|
+
gatewaySubcommand,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
666
711
|
raiseIfUpstreamUnavailable(res);
|
|
667
712
|
// Carry the HTTP status so a reportable transport failure (e.g. a 501 that
|
|
668
713
|
// is neither 401 nor an upstream-availability status, so it is captured)
|
|
@@ -773,7 +818,7 @@ async function pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, dea
|
|
|
773
818
|
break;
|
|
774
819
|
await new Promise((resolve) => setTimeout(resolve, Math.min(ASYNC_GATEWAY_POLL_INTERVAL_MS, remainingMs)));
|
|
775
820
|
}
|
|
776
|
-
throw new IntegrationsCliError(`Timed out waiting for integration job ${jobId}; the server job may still finish. To safely retry, rerun with --idempotency-key ${idempotencyKey}.`, { expected: true });
|
|
821
|
+
throw new IntegrationsCliError(`Timed out waiting for integration job ${jobId}; the server job may still finish. To safely retry, rerun with --idempotency-key ${idempotencyKey} (add --timeout <seconds> to wait longer).`, { expected: true });
|
|
777
822
|
}
|
|
778
823
|
/**
|
|
779
824
|
* Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
|
|
@@ -56,7 +56,7 @@ import chalk from "chalk";
|
|
|
56
56
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
57
57
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
58
58
|
import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
|
|
59
|
-
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, fetchUsableApps, isBuiltInGatewayProvider, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
59
|
+
import { IntegrationsCliError, bareProvider, callGateway, parseCallTimeoutMs, fetchConnections, fetchUsableApps, isBuiltInGatewayProvider, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
60
60
|
import { registerConnectCommands } from "./integrations-connect.js";
|
|
61
61
|
import { registerImportCommands } from "./integrations-import.js";
|
|
62
62
|
import { registerManageCommands } from "./integrations-manage.js";
|
|
@@ -483,8 +483,10 @@ export function registerIntegrationsCommand(program) {
|
|
|
483
483
|
.option("--args <json>", "Tool arguments as a JSON object (default {}); use - to read the JSON from stdin")
|
|
484
484
|
.option("--args-file <path>", "Read tool arguments from a JSON file (use - for stdin); avoids shell quoting of apostrophes and parentheses")
|
|
485
485
|
.option("--idempotency-key <key>", "Stable key so retries execute at most once (default: generated)")
|
|
486
|
+
.option("--timeout <seconds>", "How long to wait for a long call that runs as a background job (default 270, max 1800)")
|
|
486
487
|
.option("--json", "Machine-readable output")
|
|
487
488
|
.action(async (tool, opts) => {
|
|
489
|
+
const waitMs = parseCallTimeoutMs(opts.timeout);
|
|
488
490
|
const parsedArgs = await resolveCallArgs(opts);
|
|
489
491
|
const token = await ensureCognitoIdToken();
|
|
490
492
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
@@ -522,7 +524,7 @@ export function registerIntegrationsCommand(program) {
|
|
|
522
524
|
},
|
|
523
525
|
};
|
|
524
526
|
}
|
|
525
|
-
const message = await callGateway(token, gatewayParams, idempotencyKey);
|
|
527
|
+
const message = await callGateway(token, gatewayParams, idempotencyKey, "tools/call", { waitMs });
|
|
526
528
|
const payload = unwrapGatewayResult(message.result);
|
|
527
529
|
const queued = queuedOutcome(payload);
|
|
528
530
|
if (queued) {
|
|
@@ -45,5 +45,21 @@ export declare function parseDestinationUrl(raw: string): {
|
|
|
45
45
|
};
|
|
46
46
|
export declare function collectSecretNames(value: string, previous?: string[]): string[];
|
|
47
47
|
export declare function scrubSandboxOutput(text: string, secretNames?: string[]): string;
|
|
48
|
+
/**
|
|
49
|
+
* Check a `--remote-path` value and return the path to store, or the reason it
|
|
50
|
+
* can never match.
|
|
51
|
+
*
|
|
52
|
+
* An absolute path is sent exactly as typed: the server compares absolute
|
|
53
|
+
* approvals byte for byte with the path `hq secrets exec` reports. A relative
|
|
54
|
+
* path names the script from the HQ root. `\` becomes `/` and a leading `./`
|
|
55
|
+
* is dropped, then the same rules the server applies are checked: at least one
|
|
56
|
+
* folder before the file name, no `.`, `..` or empty segments, no `~`, no
|
|
57
|
+
* drive letter, and no surrounding whitespace.
|
|
58
|
+
*/
|
|
59
|
+
export declare function normalizeApprovedScriptPath(input: string): {
|
|
60
|
+
path: string;
|
|
61
|
+
} | {
|
|
62
|
+
error: string;
|
|
63
|
+
};
|
|
48
64
|
export declare function registerSecretsCommand(program: Command): void;
|
|
49
65
|
//# sourceMappingURL=secrets.d.ts.map
|
package/dist/commands/secrets.js
CHANGED
|
@@ -462,6 +462,61 @@ function normalizePolicyRecord(secretPath, data) {
|
|
|
462
462
|
scripts,
|
|
463
463
|
};
|
|
464
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Check a `--remote-path` value and return the path to store, or the reason it
|
|
467
|
+
* can never match.
|
|
468
|
+
*
|
|
469
|
+
* An absolute path is sent exactly as typed: the server compares absolute
|
|
470
|
+
* approvals byte for byte with the path `hq secrets exec` reports. A relative
|
|
471
|
+
* path names the script from the HQ root. `\` becomes `/` and a leading `./`
|
|
472
|
+
* is dropped, then the same rules the server applies are checked: at least one
|
|
473
|
+
* folder before the file name, no `.`, `..` or empty segments, no `~`, no
|
|
474
|
+
* drive letter, and no surrounding whitespace.
|
|
475
|
+
*/
|
|
476
|
+
export function normalizeApprovedScriptPath(input) {
|
|
477
|
+
if (input.length === 0 || input.trim().length === 0) {
|
|
478
|
+
return { error: "the path is empty" };
|
|
479
|
+
}
|
|
480
|
+
if (input !== input.trim()) {
|
|
481
|
+
return { error: "the path starts or ends with whitespace" };
|
|
482
|
+
}
|
|
483
|
+
if (input.startsWith("/") || input.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(input)) {
|
|
484
|
+
return { path: input };
|
|
485
|
+
}
|
|
486
|
+
if (input.startsWith("~")) {
|
|
487
|
+
return {
|
|
488
|
+
error: "the path starts with ~, which is never expanded; give the absolute path or a path relative to the HQ root",
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (/^[A-Za-z]:/.test(input)) {
|
|
492
|
+
return { error: "the path starts with a drive letter but is not absolute" };
|
|
493
|
+
}
|
|
494
|
+
const relative = input.replace(/\\/g, "/").replace(/^(\.\/)+/, "");
|
|
495
|
+
const segments = relative.split("/");
|
|
496
|
+
if (segments.some((segment) => segment === "")) {
|
|
497
|
+
return {
|
|
498
|
+
error: "the path has an empty segment (a trailing or doubled /)",
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
502
|
+
return { error: "the path has a . or .. segment" };
|
|
503
|
+
}
|
|
504
|
+
if (segments.length < 2) {
|
|
505
|
+
return {
|
|
506
|
+
error: "a bare file name would match that file in any folder; include at least one folder, for example skills/<skill>/<file>",
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return { path: relative };
|
|
510
|
+
}
|
|
511
|
+
function approvalIsActive(policy, scriptId) {
|
|
512
|
+
const approved = policy.scriptLock?.approvedScripts;
|
|
513
|
+
if (!Array.isArray(approved))
|
|
514
|
+
return true;
|
|
515
|
+
return approved.some((script) => script !== null &&
|
|
516
|
+
typeof script === "object" &&
|
|
517
|
+
script.scriptId === scriptId &&
|
|
518
|
+
!script.revokedAt);
|
|
519
|
+
}
|
|
465
520
|
function isSecretPolicyScript(value) {
|
|
466
521
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
467
522
|
return false;
|
|
@@ -988,7 +1043,7 @@ export function registerSecretsCommand(program) {
|
|
|
988
1043
|
.description("Approve a script for a secret path")
|
|
989
1044
|
.requiredOption("--id <scriptId>", "Stable script identifier")
|
|
990
1045
|
.requiredOption("--script <path>", "Path to the local script file")
|
|
991
|
-
.option("--remote-path <path>", "Script path
|
|
1046
|
+
.option("--remote-path <path>", "Script path to approve instead of the local path. A path relative to the HQ root (for example companies/<co>/skills/<skill>/<file>) matches on every HQ root; an absolute path must match the runtime's path exactly")
|
|
992
1047
|
.option("--attestation <level>", "Attestation level", "self-asserted-hash")
|
|
993
1048
|
.action(async (secretPath, opts) => {
|
|
994
1049
|
try {
|
|
@@ -997,7 +1052,16 @@ export function registerSecretsCommand(program) {
|
|
|
997
1052
|
console.error(chalk.red(`Invalid secret path '${secretPath}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
998
1053
|
process.exit(1);
|
|
999
1054
|
}
|
|
1055
|
+
const checkedRemotePath = opts.remotePath === undefined
|
|
1056
|
+
? undefined
|
|
1057
|
+
: normalizeApprovedScriptPath(opts.remotePath);
|
|
1058
|
+
if (checkedRemotePath && "error" in checkedRemotePath) {
|
|
1059
|
+
console.error(chalk.red(`Invalid --remote-path '${opts.remotePath}': ${checkedRemotePath.error}.`));
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
const remotePath = checkedRemotePath?.path;
|
|
1000
1063
|
const usage = await buildSecretUsage("exec", opts.script, opts.id, opts.attestation);
|
|
1064
|
+
const approvedPath = remotePath ?? usage.script?.path;
|
|
1001
1065
|
const token = await requireCognitoTokenForSecrets("secrets script approve", "secrets:write");
|
|
1002
1066
|
const companyUid = await getEntityUid(token, scopeOpts(secrets.opts()));
|
|
1003
1067
|
const res = await vaultApiFetch({
|
|
@@ -1007,7 +1071,7 @@ export function registerSecretsCommand(program) {
|
|
|
1007
1071
|
body: {
|
|
1008
1072
|
path: secretPath,
|
|
1009
1073
|
scriptId: usage.script?.scriptId,
|
|
1010
|
-
scriptPath:
|
|
1074
|
+
scriptPath: approvedPath,
|
|
1011
1075
|
sha256: usage.script?.sha256,
|
|
1012
1076
|
attestationLevel: usage.script?.attestationLevel,
|
|
1013
1077
|
},
|
|
@@ -1018,7 +1082,12 @@ export function registerSecretsCommand(program) {
|
|
|
1018
1082
|
process.exit(1);
|
|
1019
1083
|
}
|
|
1020
1084
|
removeCacheEntry(companyUid, secretPath);
|
|
1021
|
-
|
|
1085
|
+
const data = (await res.json().catch(() => ({})));
|
|
1086
|
+
if (data.policy && !approvalIsActive(data.policy, opts.id)) {
|
|
1087
|
+
console.error(chalk.red(`Failed to approve script: the server did not store an active approval for '${opts.id}' on '${secretPath}'. If this id was revoked before, approve the script under a new --id and pass that id to --script-id.`));
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
console.log(chalk.green(`Approved script '${opts.id}' for '${secretPath}' (path: ${approvedPath}).`));
|
|
1022
1091
|
}
|
|
1023
1092
|
catch (err) {
|
|
1024
1093
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
@@ -75,6 +75,13 @@ export interface MergedInbox {
|
|
|
75
75
|
entries: MirroredInboxEntry[];
|
|
76
76
|
/** Set when the server could not be consulted, so the caller can say so. */
|
|
77
77
|
note?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Machine-readable origin of the answer. `server` means the server inbox was
|
|
80
|
+
* read (merged with the mirror); `local-mirror` means the server could not be
|
|
81
|
+
* consulted and the entries may be stale. Lets an automated consumer tell a
|
|
82
|
+
* stale fallback from a verified empty inbox without parsing `note`.
|
|
83
|
+
*/
|
|
84
|
+
source: "server" | "local-mirror";
|
|
78
85
|
}
|
|
79
86
|
export declare function mergeAgentInbox(paths: Pick<AgentKitPaths, "inboxDir">, fetchServerInbox: () => Promise<{
|
|
80
87
|
status: number;
|
|
@@ -163,6 +163,10 @@ export async function mergeAgentInbox(paths, fetchServerInbox) {
|
|
|
163
163
|
catch (err) {
|
|
164
164
|
note = `server inbox unavailable (${err instanceof Error ? err.message : String(err)}); showing the local mirror only, which may be stale`;
|
|
165
165
|
}
|
|
166
|
-
return {
|
|
166
|
+
return {
|
|
167
|
+
entries: [...byId.values()],
|
|
168
|
+
source: note ? "local-mirror" : "server",
|
|
169
|
+
...(note ? { note } : {}),
|
|
170
|
+
};
|
|
167
171
|
}
|
|
168
172
|
//# sourceMappingURL=inbox-state.js.map
|
|
@@ -280,7 +280,7 @@ export function buildAgentMcpTools(clients) {
|
|
|
280
280
|
const limit = int(args, "limit", { min: 1, max: 100, fallback: 20 });
|
|
281
281
|
const includeDone = args.include_done === true;
|
|
282
282
|
const done = readDoneIds(clients.paths);
|
|
283
|
-
const { entries, note: serverNote } = await mergeAgentInbox(clients.paths, async () => {
|
|
283
|
+
const { entries, note: serverNote, source } = await mergeAgentInbox(clients.paths, async () => {
|
|
284
284
|
const token = await clients.getToken();
|
|
285
285
|
return clients.apiJson(token, `/v1/agents/${encodeURIComponent(creds.entityUid)}/inbox`);
|
|
286
286
|
});
|
|
@@ -292,6 +292,7 @@ export function buildAgentMcpTools(clients) {
|
|
|
292
292
|
const hasFiles = rows.some((r) => r.attachments?.length);
|
|
293
293
|
return textResult(clampOutput(JSON.stringify({
|
|
294
294
|
count: rows.length,
|
|
295
|
+
source,
|
|
295
296
|
messages: rows,
|
|
296
297
|
...(serverNote ? { note: serverNote } : {}),
|
|
297
298
|
...(hasFiles ? { attachmentsHint: ATTACHMENTS_HINT } : {}),
|
|
@@ -100,6 +100,15 @@ export interface SyncHealthDeps {
|
|
|
100
100
|
* unhealthy-sync signal, and `hq doctor --fix` has nothing local to refresh.
|
|
101
101
|
*/
|
|
102
102
|
localCompanyExists?: (slug: string) => boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Whether the sync service can resolve the company for this machine's
|
|
105
|
+
* identity. Defaults to true unless a recent `hq doctor --fix` pull of it
|
|
106
|
+
* failed because the service has no such company for the caller or refused
|
|
107
|
+
* access (see company-resolution-state.ts). A stale journal for a company
|
|
108
|
+
* that fails this has nothing `--fix` can pull, so it is NA ("not on this
|
|
109
|
+
* machine") rather than an auto-fixable WARN.
|
|
110
|
+
*/
|
|
111
|
+
companySyncResolvable?: (slug: string, lastSync?: string | null) => boolean;
|
|
103
112
|
/**
|
|
104
113
|
* Whether HQ_SYNC_MANIFEST_DISABLED turns manifest uploads off on this
|
|
105
114
|
* machine. Defaults to reading the environment the way `hq sync manifest`
|