@indigoai-us/hq-cli 5.75.0 → 5.76.0
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/dist/commands/mcp-registration.d.ts +4 -5
- package/dist/commands/mcp-registration.js +5 -4
- package/dist/commands/outposts.d.ts +20 -4
- package/dist/commands/outposts.js +77 -8
- package/dist/commands/pack-install.d.ts +14 -17
- package/dist/commands/pack-install.js +53 -29
- package/dist/commands/pkg-install.js +3 -1
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +9 -3
- package/dist/commands/secrets.js +189 -87
- package/dist/run/hq-plugin.js +94 -31
- package/dist/utils/sandbox-runner-client.d.ts +1 -0
- package/dist/utils/sandbox-runner-client.js +1 -0
- package/dist/utils/secrets-cache.d.ts +4 -5
- package/dist/utils/secrets-cache.js +5 -8
- package/package.json +3 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/commands/mcp-registration.ts +9 -9
- package/src/commands/outposts.test.ts +118 -24
- package/src/commands/outposts.ts +197 -43
- package/src/commands/pack-install-secret-authorization.test.ts +115 -0
- package/src/commands/pack-install.test.ts +5 -1
- package/src/commands/pack-install.ts +67 -29
- package/src/commands/pkg-install.ts +3 -1
- package/src/commands/run.test.ts +45 -0
- package/src/commands/run.ts +20 -4
- package/src/commands/secrets.test.ts +366 -25
- package/src/commands/secrets.ts +222 -96
- package/src/run/hq-plugin.test.ts +186 -10
- package/src/run/hq-plugin.ts +102 -32
- package/src/utils/__fixtures__/scan-packages.generated-block.sh +23 -0
- package/src/utils/pack-contributions.test.ts +90 -31
- package/src/utils/sandbox-runner-client.test.ts +28 -0
- package/src/utils/sandbox-runner-client.ts +2 -0
- package/src/utils/secrets-cache.ts +5 -8
- package/test/commands/signals.test.ts +2 -2
- package/test/commands/sources.test.ts +2 -2
- package/test/helpers/vault-service-mock.ts +76 -17
- package/test/sources-signals/smoke.test.ts +2 -2
|
@@ -429,11 +429,10 @@ export interface McpManifest {
|
|
|
429
429
|
/**
|
|
430
430
|
* Resolve a `${secret:NAME}` reference to its plaintext value, or return `null`
|
|
431
431
|
* when the secret is unavailable (un-minted / TTL-expired / no company context).
|
|
432
|
-
* Production binds this to
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* reference at all.
|
|
432
|
+
* Production binds this to an in-memory map populated by a fresh vault `/load`
|
|
433
|
+
* authorization response; tests inject a pure map. Returning `null` for a
|
|
434
|
+
* referenced secret is a hard error at emit (we refuse to write a broken
|
|
435
|
+
* header), distinct from a name with no reference at all.
|
|
437
436
|
*/
|
|
438
437
|
export type SecretResolver = (name: string) => string | null;
|
|
439
438
|
/** True iff `value` contains at least one `${secret:NAME}` reference. */
|
|
@@ -848,7 +848,8 @@ function restoreSafely(realTarget, backupDir) {
|
|
|
848
848
|
//
|
|
849
849
|
// SECRET SAFETY (the hard rule from the PRD authModel / US-002 AC):
|
|
850
850
|
// - `${secret:NAME}` resolves ONLY here, at emit, via the injected
|
|
851
|
-
// {@link SecretResolver}
|
|
851
|
+
// {@link SecretResolver}. Production injects values returned by a fresh
|
|
852
|
+
// server authorization; the encrypted disk cache is never a value fallback.
|
|
852
853
|
// - The RESOLVED value lands in `~/.claude.json` — a 0600 user-global file that
|
|
853
854
|
// is NOT synced/committed — because the runtime needs the real header to work.
|
|
854
855
|
// - The resolved value is REDACTED from every return value and never echoed; we
|
|
@@ -885,9 +886,9 @@ export function resolveSecretRefs(value, resolve, secretSink) {
|
|
|
885
886
|
return value.replace(SECRET_REF_RE, (_match, name) => {
|
|
886
887
|
const resolved = resolve(name);
|
|
887
888
|
if (resolved === null || resolved === undefined) {
|
|
888
|
-
throw new McpManifestError(`cannot resolve \${secret:${name}} at emit — the secret
|
|
889
|
-
'
|
|
890
|
-
'write a half-resolved header.');
|
|
889
|
+
throw new McpManifestError(`cannot resolve \${secret:${name}} at emit — the secret was not available ` +
|
|
890
|
+
'after online vault authorization (check login, company scope, and access, ' +
|
|
891
|
+
'then re-install). HQ refuses to write a half-resolved header.');
|
|
891
892
|
}
|
|
892
893
|
if (resolved.length > 0)
|
|
893
894
|
secretSink.add(resolved);
|
|
@@ -23,13 +23,27 @@
|
|
|
23
23
|
import { Command } from "commander";
|
|
24
24
|
import { spawnSync } from "node:child_process";
|
|
25
25
|
import { type BillingErrorPayload } from "../utils/billing-gate.js";
|
|
26
|
+
/**
|
|
27
|
+
* hq-pro's per-person cap envelope on a `409` provision block. Unlike every
|
|
28
|
+
* other `/outpost/*` failure this body carries NO `message`/`error` field —
|
|
29
|
+
* only the cap facts — so it has to be decoded structurally or the reason
|
|
30
|
+
* degrades to a bare `res.statusText` ("Conflict").
|
|
31
|
+
*/
|
|
32
|
+
export interface OutpostCappedPayload {
|
|
33
|
+
limit: number;
|
|
34
|
+
outposts: OutpostSummary[];
|
|
35
|
+
}
|
|
36
|
+
/** Decode hq-pro's `{ capped, limit, outposts }` cap envelope, if present. */
|
|
37
|
+
export declare function parseCappedPayload(body: unknown): OutpostCappedPayload | undefined;
|
|
26
38
|
/** A non-2xx from the `/outpost/*` control plane. Carries status + `step`. */
|
|
27
39
|
export declare class OutpostHttpError extends Error {
|
|
28
40
|
status: number;
|
|
29
41
|
step?: string;
|
|
30
42
|
/** hq-pro's billing envelope on a `402 billing_required` provision block. */
|
|
31
43
|
billing?: BillingErrorPayload;
|
|
32
|
-
|
|
44
|
+
/** hq-pro's cap envelope on a `409` provision block. */
|
|
45
|
+
capped?: OutpostCappedPayload;
|
|
46
|
+
constructor(status: number, message: string, step?: string, billing?: BillingErrorPayload, capped?: OutpostCappedPayload);
|
|
33
47
|
}
|
|
34
48
|
/** Row summary from `GET /outpost/list`. */
|
|
35
49
|
export interface OutpostSummary {
|
|
@@ -59,9 +73,11 @@ export declare function outpostRequest<T>(opts: {
|
|
|
59
73
|
/**
|
|
60
74
|
* Provision the caller's Outpost. Sends the cached Cognito refresh token so the
|
|
61
75
|
* box can authenticate AS the caller (the same body the console's
|
|
62
|
-
* `provisionMyOutpost` sends).
|
|
63
|
-
* per-person cap gets
|
|
64
|
-
*
|
|
76
|
+
* `provisionMyOutpost` sends). No duplicate is ever created: a caller already at
|
|
77
|
+
* their per-person cap gets a `409` whose body lists their existing boxes —
|
|
78
|
+
* thrown here as an `OutpostHttpError` carrying `capped` (hq-pro checks the cap
|
|
79
|
+
* BEFORE activation billing, so a capped call is never charged). The refresh
|
|
80
|
+
* token is sent over HTTPS and NEVER printed.
|
|
65
81
|
*/
|
|
66
82
|
export declare function provisionOutpost(token: string, input: {
|
|
67
83
|
refreshToken: string;
|
|
@@ -32,18 +32,33 @@ import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
|
32
32
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
33
33
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
34
34
|
import { OUTPOST_PRICE_CENTS, confirmChargeOrExit, parseBillingPayload, surfaceBillingRequired, } from "../utils/billing-gate.js";
|
|
35
|
+
/** Decode hq-pro's `{ capped, limit, outposts }` cap envelope, if present. */
|
|
36
|
+
export function parseCappedPayload(body) {
|
|
37
|
+
if (!body || typeof body !== "object")
|
|
38
|
+
return undefined;
|
|
39
|
+
const b = body;
|
|
40
|
+
if (b.capped !== true)
|
|
41
|
+
return undefined;
|
|
42
|
+
return {
|
|
43
|
+
limit: typeof b.limit === "number" ? b.limit : 0,
|
|
44
|
+
outposts: Array.isArray(b.outposts) ? b.outposts : [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
35
47
|
/** A non-2xx from the `/outpost/*` control plane. Carries status + `step`. */
|
|
36
48
|
export class OutpostHttpError extends Error {
|
|
37
49
|
status;
|
|
38
50
|
step;
|
|
39
51
|
/** hq-pro's billing envelope on a `402 billing_required` provision block. */
|
|
40
52
|
billing;
|
|
41
|
-
|
|
53
|
+
/** hq-pro's cap envelope on a `409` provision block. */
|
|
54
|
+
capped;
|
|
55
|
+
constructor(status, message, step, billing, capped) {
|
|
42
56
|
super(message);
|
|
43
57
|
this.name = "OutpostHttpError";
|
|
44
58
|
this.status = status;
|
|
45
59
|
this.step = step;
|
|
46
60
|
this.billing = billing;
|
|
61
|
+
this.capped = capped;
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
64
|
/**
|
|
@@ -62,16 +77,18 @@ export async function outpostRequest(opts) {
|
|
|
62
77
|
: typeof body.error === "string"
|
|
63
78
|
? body.error
|
|
64
79
|
: res.statusText;
|
|
65
|
-
throw new OutpostHttpError(res.status, message, body.step, parseBillingPayload(body));
|
|
80
|
+
throw new OutpostHttpError(res.status, message, body.step, parseBillingPayload(body), parseCappedPayload(body));
|
|
66
81
|
}
|
|
67
82
|
return (await res.json());
|
|
68
83
|
}
|
|
69
84
|
/**
|
|
70
85
|
* Provision the caller's Outpost. Sends the cached Cognito refresh token so the
|
|
71
86
|
* box can authenticate AS the caller (the same body the console's
|
|
72
|
-
* `provisionMyOutpost` sends).
|
|
73
|
-
* per-person cap gets
|
|
74
|
-
*
|
|
87
|
+
* `provisionMyOutpost` sends). No duplicate is ever created: a caller already at
|
|
88
|
+
* their per-person cap gets a `409` whose body lists their existing boxes —
|
|
89
|
+
* thrown here as an `OutpostHttpError` carrying `capped` (hq-pro checks the cap
|
|
90
|
+
* BEFORE activation billing, so a capped call is never charged). The refresh
|
|
91
|
+
* token is sent over HTTPS and NEVER printed.
|
|
75
92
|
*/
|
|
76
93
|
export async function provisionOutpost(token, input) {
|
|
77
94
|
return outpostRequest({
|
|
@@ -300,6 +317,24 @@ function ensureTrailingNewline(s) {
|
|
|
300
317
|
// ---------------------------------------------------------------------------
|
|
301
318
|
// Command registration
|
|
302
319
|
// ---------------------------------------------------------------------------
|
|
320
|
+
/**
|
|
321
|
+
* Explain a `409` per-person cap. hq-pro checks the cap BEFORE activation
|
|
322
|
+
* billing, so nothing was charged — worth saying, since the caller just
|
|
323
|
+
* confirmed a recurring charge to get here.
|
|
324
|
+
*/
|
|
325
|
+
function surfaceOutpostCapped(capped) {
|
|
326
|
+
const owned = capped.outposts.length;
|
|
327
|
+
console.error(chalk.yellow(`You're already at your Outpost limit (${owned} of ${capped.limit}). ` +
|
|
328
|
+
`No new box was provisioned and you have not been charged.`));
|
|
329
|
+
for (const o of capped.outposts) {
|
|
330
|
+
const detail = [o.state, o.instanceName, o.region]
|
|
331
|
+
.filter(Boolean)
|
|
332
|
+
.join(" ");
|
|
333
|
+
console.error(` ${o.outpostId} ${detail}`);
|
|
334
|
+
}
|
|
335
|
+
console.error(chalk.dim("Inspect it: hq outposts status"));
|
|
336
|
+
console.error(chalk.dim("Or tear it down first: hq outposts destroy --id <id> --yes"));
|
|
337
|
+
}
|
|
303
338
|
function fail(err) {
|
|
304
339
|
if (err instanceof OutpostHttpError) {
|
|
305
340
|
console.error(chalk.red(err.message));
|
|
@@ -615,7 +650,17 @@ function authGitHubViaVault(deps) {
|
|
|
615
650
|
"unset GITHUB_TOKEN",
|
|
616
651
|
'printf "%s" "$TOKEN" | gh auth login --with-token',
|
|
617
652
|
].join("\n");
|
|
618
|
-
const authed = runBestEffort(deps, "hq", [
|
|
653
|
+
const authed = runBestEffort(deps, "hq", [
|
|
654
|
+
"secrets",
|
|
655
|
+
"--personal",
|
|
656
|
+
"exec",
|
|
657
|
+
"--only",
|
|
658
|
+
"GITHUB_TOKEN",
|
|
659
|
+
"--",
|
|
660
|
+
"bash",
|
|
661
|
+
"-c",
|
|
662
|
+
ghLogin,
|
|
663
|
+
], "GitHub auth via vault");
|
|
619
664
|
if (authed) {
|
|
620
665
|
// Let plain `git clone https://github.com/...` reuse gh's token so private
|
|
621
666
|
// repos don't prompt for a username/password.
|
|
@@ -670,7 +715,15 @@ async function replicaSyncOutpost(opts, deps) {
|
|
|
670
715
|
console.warn("replica-sync: auth refresh failed — skipping this cycle (token likely expired).");
|
|
671
716
|
return;
|
|
672
717
|
}
|
|
673
|
-
runBestEffort(deps, "hq", [
|
|
718
|
+
runBestEffort(deps, "hq", [
|
|
719
|
+
"sync",
|
|
720
|
+
"pull",
|
|
721
|
+
"--personal",
|
|
722
|
+
"--hq-root",
|
|
723
|
+
hqRoot,
|
|
724
|
+
"--on-conflict",
|
|
725
|
+
"keep",
|
|
726
|
+
], "personal vault pull");
|
|
674
727
|
// rescue lays down the core kernel and expects companies/ to exist.
|
|
675
728
|
deps.mkdirp(path.join(hqRoot, "companies"));
|
|
676
729
|
runBestEffort(deps, "hq", ["rescue", "--hq-root", hqRoot, "--yes"], "HQ kernel rescue");
|
|
@@ -722,9 +775,17 @@ export function registerOutpostsCommand(program, selfDeployOverrides = {}) {
|
|
|
722
775
|
.option("--yes", "Confirm the $80/month charge (required to provision)")
|
|
723
776
|
.action(async function (opts) {
|
|
724
777
|
try {
|
|
778
|
+
// Reject an unrecognized runtime rather than resolving it to claude:
|
|
779
|
+
// `--runtime codx` would otherwise hand back a silently Claude box.
|
|
780
|
+
if (opts.runtime !== undefined &&
|
|
781
|
+
opts.runtime !== "claude" &&
|
|
782
|
+
opts.runtime !== "codex") {
|
|
783
|
+
console.error(chalk.red(`Invalid --runtime '${opts.runtime}': must be 'claude' or 'codex'.`));
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
725
786
|
const agentRuntime = opts.runtime === "codex"
|
|
726
787
|
? "codex"
|
|
727
|
-
: opts.runtime
|
|
788
|
+
: opts.runtime === "claude"
|
|
728
789
|
? "claude"
|
|
729
790
|
: undefined;
|
|
730
791
|
let diskSizeGb;
|
|
@@ -767,6 +828,14 @@ export function registerOutpostsCommand(program, selfDeployOverrides = {}) {
|
|
|
767
828
|
await surfaceBillingRequired(token, err.billing);
|
|
768
829
|
process.exit(1);
|
|
769
830
|
}
|
|
831
|
+
// Already at the per-person cap → say so and name the box they own,
|
|
832
|
+
// not a bare "Conflict".
|
|
833
|
+
if (err instanceof OutpostHttpError &&
|
|
834
|
+
err.status === 409 &&
|
|
835
|
+
err.capped) {
|
|
836
|
+
surfaceOutpostCapped(err.capped);
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
770
839
|
throw err;
|
|
771
840
|
}
|
|
772
841
|
}
|
|
@@ -305,26 +305,21 @@ export declare function renderMcpServerLine(payloadDir: string, item: string): s
|
|
|
305
305
|
*/
|
|
306
306
|
export declare function confirmMcp(pkg: PackManifest, payloadDir: string, allowMcp: boolean): Promise<boolean>;
|
|
307
307
|
/**
|
|
308
|
-
* Build
|
|
309
|
-
*
|
|
308
|
+
* Build an install-time {@link SecretResolver} from a fresh server authorization
|
|
309
|
+
* decision. The encrypted cache may identify one unambiguous scope, but its
|
|
310
|
+
* plaintext is never trusted or read to register an MCP server.
|
|
310
311
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
* context just provisioned the secret (e.g. the one `/connect-shopify` minted
|
|
318
|
-
* `VYG_API_KEY` under), without guessing, and works whether the vault scoped the
|
|
319
|
-
* key under a company or person entity.
|
|
312
|
+
* With `--company`, the caller-selected company is resolved normally. Without
|
|
313
|
+
* it, exactly one cache scope may identify the intended company/person context;
|
|
314
|
+
* multiple scopes are ambiguous and fail closed. The requested names are then
|
|
315
|
+
* loaded through the same server-authorized endpoint as exec/env/run. Offline,
|
|
316
|
+
* expired-session, forbidden, not-found, script-lock, and malformed-response
|
|
317
|
+
* outcomes all produce an empty resolver, so MCP registration is deferred.
|
|
320
318
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* unresolvable-secret path keys off to defer that server gracefully. With no
|
|
324
|
-
* cached scopes at all (`listSecretCacheScopes()` → `[]`) it simply returns
|
|
325
|
-
* `null` for every name, the desired graceful-deferral behavior.
|
|
319
|
+
* The returned synchronous resolver contains only values from that fresh
|
|
320
|
+
* authorization response. No cached value is a fallback.
|
|
326
321
|
*/
|
|
327
|
-
export declare function makeInstallSecretResolver(): SecretResolver
|
|
322
|
+
export declare function makeInstallSecretResolver(secretNames: string[], company?: string): Promise<SecretResolver>;
|
|
328
323
|
/**
|
|
329
324
|
* Install the fetched payload to `<hqRoot>/core/packages/<pkg.name>/` (HQ
|
|
330
325
|
* v12+ layout). The HQ template (`hq-core` / `hq-core-staging`) ships
|
|
@@ -381,6 +376,8 @@ export declare function runScanPackages(hqRoot: string, opts?: {
|
|
|
381
376
|
}): void;
|
|
382
377
|
export interface InstallPackOptions {
|
|
383
378
|
allowHooks?: boolean;
|
|
379
|
+
/** Company context for online authorization of install-time MCP secret refs. */
|
|
380
|
+
company?: string;
|
|
384
381
|
/**
|
|
385
382
|
* US-010 install-time MCP trust prompt bypass (CI/ambient-trust). Mirrors
|
|
386
383
|
* `allowHooks`: when set, `confirmMcp` skips the prompt and prints a yellow
|
|
@@ -49,9 +49,11 @@ import { findHqRoot } from '../utils/manifest.js';
|
|
|
49
49
|
import { readHqVersion, routeContribution, listInstalledPacks } from '../utils/pack-contributions.js';
|
|
50
50
|
import { CONTRIBUTION_TABLE, payloadFor } from '../utils/contribution-table.js';
|
|
51
51
|
import { safeExtractTarball } from './safe-extract.js';
|
|
52
|
-
import { vaultApiFetchPublic } from '../utils/vault-api.js';
|
|
52
|
+
import { getCompanyUid, vaultApiFetchPublic } from '../utils/vault-api.js';
|
|
53
|
+
import { ensureCognitoToken } from '../utils/cognito-session.js';
|
|
53
54
|
import { redactSecrets, SECRET_REDACTION, registerMcpServers, McpManifestError, } from './mcp-registration.js';
|
|
54
|
-
import {
|
|
55
|
+
import { listSecretCacheScopes } from '../utils/secrets-cache.js';
|
|
56
|
+
import { loadRevealedSecrets } from './secrets.js';
|
|
55
57
|
const PACK_UPDATE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
56
58
|
const PACK_UPDATE_FETCH_TIMEOUT_MS = 3_000;
|
|
57
59
|
const gitLsRemoteMemo = new Map();
|
|
@@ -1349,34 +1351,56 @@ function evalConditional(expr) {
|
|
|
1349
1351
|
// key) still SUCCEEDS, skipping only the server whose secret is not yet present.
|
|
1350
1352
|
// ---------------------------------------------------------------------------
|
|
1351
1353
|
/**
|
|
1352
|
-
* Build
|
|
1353
|
-
*
|
|
1354
|
+
* Build an install-time {@link SecretResolver} from a fresh server authorization
|
|
1355
|
+
* decision. The encrypted cache may identify one unambiguous scope, but its
|
|
1356
|
+
* plaintext is never trusted or read to register an MCP server.
|
|
1354
1357
|
*
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
* context just provisioned the secret (e.g. the one `/connect-shopify` minted
|
|
1362
|
-
* `VYG_API_KEY` under), without guessing, and works whether the vault scoped the
|
|
1363
|
-
* key under a company or person entity.
|
|
1358
|
+
* With `--company`, the caller-selected company is resolved normally. Without
|
|
1359
|
+
* it, exactly one cache scope may identify the intended company/person context;
|
|
1360
|
+
* multiple scopes are ambiguous and fail closed. The requested names are then
|
|
1361
|
+
* loaded through the same server-authorized endpoint as exec/env/run. Offline,
|
|
1362
|
+
* expired-session, forbidden, not-found, script-lock, and malformed-response
|
|
1363
|
+
* outcomes all produce an empty resolver, so MCP registration is deferred.
|
|
1364
1364
|
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
* unresolvable-secret path keys off to defer that server gracefully. With no
|
|
1368
|
-
* cached scopes at all (`listSecretCacheScopes()` → `[]`) it simply returns
|
|
1369
|
-
* `null` for every name, the desired graceful-deferral behavior.
|
|
1365
|
+
* The returned synchronous resolver contains only values from that fresh
|
|
1366
|
+
* authorization response. No cached value is a fallback.
|
|
1370
1367
|
*/
|
|
1371
|
-
export function makeInstallSecretResolver() {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1368
|
+
export async function makeInstallSecretResolver(secretNames, company) {
|
|
1369
|
+
const names = [...new Set(secretNames)];
|
|
1370
|
+
if (names.length === 0)
|
|
1371
|
+
return () => null;
|
|
1372
|
+
try {
|
|
1373
|
+
const token = await ensureCognitoToken({ interactive: false });
|
|
1374
|
+
let scopeUid;
|
|
1375
|
+
if (company) {
|
|
1376
|
+
scopeUid = await getCompanyUid(token, company);
|
|
1377
1377
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1378
|
+
else {
|
|
1379
|
+
const scopes = listSecretCacheScopes();
|
|
1380
|
+
if (scopes.length !== 1)
|
|
1381
|
+
return () => null;
|
|
1382
|
+
[scopeUid] = scopes;
|
|
1383
|
+
}
|
|
1384
|
+
const authorized = await loadRevealedSecrets(token, scopeUid, names);
|
|
1385
|
+
return (name) => authorized.get(name) ?? null;
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
return () => null;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const INSTALL_SECRET_REF_RE = /\$\{secret:([A-Z][A-Z0-9_]*(?:\/[A-Z][A-Z0-9_]+)*)\}/g;
|
|
1392
|
+
function collectManifestSecretNames(manifest) {
|
|
1393
|
+
const names = new Set();
|
|
1394
|
+
for (const value of [
|
|
1395
|
+
...Object.values(manifest.headers ?? {}),
|
|
1396
|
+
...Object.values(manifest.env ?? {}),
|
|
1397
|
+
]) {
|
|
1398
|
+
INSTALL_SECRET_REF_RE.lastIndex = 0;
|
|
1399
|
+
for (let match = INSTALL_SECRET_REF_RE.exec(value); match; match = INSTALL_SECRET_REF_RE.exec(value)) {
|
|
1400
|
+
names.add(match[1]);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return [...names];
|
|
1380
1404
|
}
|
|
1381
1405
|
/**
|
|
1382
1406
|
* Load + shape-validate + parse a pack's per-server MCP manifest from disk.
|
|
@@ -1424,13 +1448,13 @@ function extractDeferredSecretName(message) {
|
|
|
1424
1448
|
* - ANY OTHER error → RE-THROW (abort install). Only the
|
|
1425
1449
|
* unresolvable-secret case is swallowed.
|
|
1426
1450
|
*/
|
|
1427
|
-
function wireMcpServers(pkg, destDir) {
|
|
1428
|
-
const resolveSecret = makeInstallSecretResolver();
|
|
1451
|
+
async function wireMcpServers(pkg, destDir, company) {
|
|
1429
1452
|
const loadManifest = (name) => loadMcpManifestFrom(destDir, name);
|
|
1430
1453
|
const registered = [];
|
|
1431
1454
|
const skipped = [];
|
|
1432
1455
|
for (const name of pkg.contributes.mcp ?? []) {
|
|
1433
1456
|
try {
|
|
1457
|
+
const resolveSecret = await makeInstallSecretResolver(collectManifestSecretNames(loadManifest(name)), company);
|
|
1434
1458
|
// Per-server call: registerMcpServers throws on the FIRST unresolvable
|
|
1435
1459
|
// secret, so calling it one name at a time lets us catch + continue.
|
|
1436
1460
|
registerMcpServers(pkg.name, [name], { loadManifest, resolveSecret });
|
|
@@ -1676,7 +1700,7 @@ export async function installPack(source, opts = {}) {
|
|
|
1676
1700
|
// Per-server secret-deferral keeps a fresh install (key not yet minted) at
|
|
1677
1701
|
// exit 0, deferring only the unresolvable server (see wireMcpServers).
|
|
1678
1702
|
if (Array.isArray(pkg.contributes.mcp) && pkg.contributes.mcp.length > 0) {
|
|
1679
|
-
const { registered, skipped } = wireMcpServers(pkg, destDir);
|
|
1703
|
+
const { registered, skipped } = await wireMcpServers(pkg, destDir, opts.company);
|
|
1680
1704
|
// One-line summary (server NAMES only — never resolved secret VALUES).
|
|
1681
1705
|
say(chalk.dim(` MCP servers: registered [${registered.join(', ')}]; ` +
|
|
1682
1706
|
`skipped [${skipped.join(', ')}].`));
|
|
@@ -29,7 +29,7 @@ export function registerPackageInstallCommand(parent) {
|
|
|
29
29
|
.command('install <source>')
|
|
30
30
|
.description('Install a package. Sources: bare slug (registry, Cognito-gated), ' +
|
|
31
31
|
'@scope/name[@ver] (npm pack), git URL[#ref], or local path.')
|
|
32
|
-
.option('--company <co>', 'Scope
|
|
32
|
+
.option('--company <co>', 'Scope package secret authorization to a specific company')
|
|
33
33
|
.option('--allow-hooks', 'Skip the hooks confirmation prompt (content-pack flow)')
|
|
34
34
|
.option('--allow-mcp', 'Skip the MCP server confirmation prompt (content-pack flow)')
|
|
35
35
|
.option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
|
|
@@ -37,6 +37,7 @@ export function registerPackageInstallCommand(parent) {
|
|
|
37
37
|
try {
|
|
38
38
|
if (sourceMatchesPackPattern(source)) {
|
|
39
39
|
await installPack(source, {
|
|
40
|
+
company: opts.company,
|
|
40
41
|
allowHooks: opts.allowHooks,
|
|
41
42
|
allowMcp: opts.allowMcp,
|
|
42
43
|
followBranch: opts.branch,
|
|
@@ -48,6 +49,7 @@ export function registerPackageInstallCommand(parent) {
|
|
|
48
49
|
// would fail at registry setup. Resolve it through the marketplace
|
|
49
50
|
// listings transport — the live install path — instead.
|
|
50
51
|
await installPack(`${MARKETPLACE_PREFIX}${source}`, {
|
|
52
|
+
company: opts.company,
|
|
51
53
|
allowHooks: opts.allowHooks,
|
|
52
54
|
allowMcp: opts.allowMcp,
|
|
53
55
|
followBranch: opts.branch,
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import type { SecretUsage } from './secrets.js';
|
|
3
|
+
export declare function buildRunUsage(scriptPath?: string, scriptId?: string): Promise<SecretUsage | undefined>;
|
|
2
4
|
export declare function registerRunCommand(program: Command): void;
|
|
3
5
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/commands/run.js
CHANGED
|
@@ -7,7 +7,11 @@ import { computeSha256 } from '../utils/integrity.js';
|
|
|
7
7
|
import { vaultApiFetch, getCompanyUid } from '../utils/vault-api.js';
|
|
8
8
|
import { discoverSchemas } from '../run/discover-schemas.js';
|
|
9
9
|
import { installHqPlugin, prewarmHqSecrets } from '../run/hq-plugin.js';
|
|
10
|
-
|
|
10
|
+
const SECRET_LOAD_TIMEOUT_MS = 30_000;
|
|
11
|
+
export async function buildRunUsage(scriptPath, scriptId) {
|
|
12
|
+
if (scriptId && !scriptPath) {
|
|
13
|
+
throw new Error('--script-id requires --script');
|
|
14
|
+
}
|
|
11
15
|
if (!scriptPath) {
|
|
12
16
|
return undefined;
|
|
13
17
|
}
|
|
@@ -15,7 +19,7 @@ async function buildRunUsage(scriptPath) {
|
|
|
15
19
|
return {
|
|
16
20
|
channel: 'run',
|
|
17
21
|
script: {
|
|
18
|
-
scriptId: resolvedPath,
|
|
22
|
+
scriptId: scriptId ?? resolvedPath,
|
|
19
23
|
path: resolvedPath,
|
|
20
24
|
sha256: await computeSha256(resolvedPath),
|
|
21
25
|
attestationLevel: 'self-asserted-hash',
|
|
@@ -29,6 +33,7 @@ export function registerRunCommand(program) {
|
|
|
29
33
|
.option('--company <slug>', 'Company slug (overrides @hqCompany in schema)')
|
|
30
34
|
.option('--schema <path>', 'Explicit schema path (skips walk-up discovery)')
|
|
31
35
|
.option('--script <path>', 'Attach local script identity for script-locked secrets')
|
|
36
|
+
.option('--script-id <id>', 'Stable script identifier approved by policy')
|
|
32
37
|
.option('--check', 'Resolve schema and validate vars without executing the command')
|
|
33
38
|
.allowUnknownOption(true)
|
|
34
39
|
.action(async (opts) => {
|
|
@@ -68,13 +73,14 @@ export function registerRunCommand(program) {
|
|
|
68
73
|
}
|
|
69
74
|
const token = await ensureCognitoToken();
|
|
70
75
|
const uid = await getCompanyUid(token, slug);
|
|
71
|
-
const usage = await buildRunUsage(opts.script);
|
|
76
|
+
const usage = await buildRunUsage(opts.script, opts.scriptId);
|
|
72
77
|
const fetchBatch = async (companyUid, names, requestUsage) => {
|
|
73
78
|
const res = await vaultApiFetch({
|
|
74
79
|
token,
|
|
75
80
|
path: `/secrets/${encodeURIComponent(companyUid)}/load`,
|
|
76
81
|
method: 'POST',
|
|
77
82
|
body: requestUsage ? { names, usage: requestUsage } : { names },
|
|
83
|
+
signal: AbortSignal.timeout(SECRET_LOAD_TIMEOUT_MS),
|
|
78
84
|
});
|
|
79
85
|
if (!res.ok) {
|
|
80
86
|
const body = await res.json().catch(() => ({}));
|