@indigoai-us/hq-cli 5.345.151 → 5.345.153
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 +129 -0
- package/dist/bin/hq-auth-refresh.d.ts +4 -0
- package/dist/bin/hq-auth-refresh.js +9 -1
- package/dist/command-catalog.generated.d.ts +188 -3
- package/dist/command-catalog.generated.js +230 -3
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +12 -0
- package/dist/commands/agent-enroll.js +37 -4
- package/dist/commands/core-policy.js +4 -4
- package/dist/commands/core-session-project-native.js +19 -4
- package/dist/commands/decide.d.ts +13 -0
- package/dist/commands/decide.js +74 -0
- package/dist/commands/files.js +23 -7
- package/dist/commands/integrations-connect.js +24 -1
- package/dist/commands/integrations-core.d.ts +14 -0
- package/dist/commands/integrations-core.js +19 -1
- package/dist/commands/integrations-import.js +22 -1
- package/dist/commands/integrations.js +43 -2
- package/dist/commands/lanes-message.js +4 -11
- package/dist/commands/lanes-questions.js +3 -0
- package/dist/commands/lanes-resume.js +4 -11
- package/dist/commands/lanes-stories.js +3 -0
- package/dist/commands/rescue.d.ts +7 -0
- package/dist/commands/rescue.js +22 -13
- package/dist/commands/run.js +14 -5
- package/dist/commands/secrets.d.ts +13 -0
- package/dist/commands/secrets.js +51 -8
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +186 -186
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/adopt-identity.d.ts +7 -1
- package/dist/lib/agent-kit/adopt-identity.js +18 -3
- package/dist/lib/agent-kit/services.d.ts +3 -1
- package/dist/lib/agent-kit/services.js +5 -1
- package/dist/lib/daemon/ops/policy.d.ts +9 -1
- package/dist/lib/daemon/ops/policy.js +37 -2
- package/dist/lib/decisions/client.d.ts +67 -0
- package/dist/lib/decisions/client.js +120 -0
- package/dist/lib/decisions/expiry.d.ts +2 -0
- package/dist/lib/decisions/expiry.js +12 -0
- package/dist/lib/decisions/fixtures/us001.d.ts +137 -0
- package/dist/lib/decisions/fixtures/us001.js +21 -0
- package/dist/lib/decisions/import-lanes.d.ts +16 -0
- package/dist/lib/decisions/import-lanes.js +152 -0
- package/dist/lib/decisions/principal.d.ts +11 -0
- package/dist/lib/decisions/principal.js +52 -0
- package/dist/lib/doctor/checks/claude-wiring.d.ts +2 -2
- package/dist/lib/doctor/checks/claude-wiring.js +21 -10
- package/dist/lib/doctor/checks/work-context.js +7 -2
- package/dist/lib/doctor/fix/apply.d.ts +3 -2
- package/dist/lib/doctor/fix/apply.js +19 -15
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +14 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.js +30 -2
- package/dist/lib/doctor/fix/remediation.js +4 -2
- package/dist/lib/doctor/hook-gate-profiles.d.ts +19 -0
- package/dist/lib/doctor/hook-gate-profiles.js +34 -0
- package/dist/lib/doctor/types.d.ts +5 -0
- package/dist/lib/install/provider-wrappers.js +5 -1
- package/dist/lib/lanes/autopilot.js +4 -1
- package/dist/lib/lanes/imported-question.d.ts +5 -0
- package/dist/lib/lanes/imported-question.js +17 -0
- package/dist/lib/lanes/questions.d.ts +0 -5
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +21 -10
- package/dist/lib/lanes/store.d.ts +0 -8
- package/dist/lib/lanes/store.js +2 -1
- package/dist/lib/lanes/stories.d.ts +15 -0
- package/dist/lib/lanes/stories.js +25 -0
- package/dist/lib/mesh/client.d.ts +8 -0
- package/dist/lib/mesh/client.js +44 -3
- package/dist/lib/mesh/denial-codes.js +1 -1
- package/dist/lib/mesh/live/daemon/run.d.ts +9 -1
- package/dist/lib/mesh/live/daemon/run.js +30 -1
- package/dist/lib/mesh/live/flush.d.ts +7 -0
- package/dist/lib/mesh/live/flush.js +53 -10
- package/dist/lib/registry/registry.js +6 -0
- package/dist/lib/scan-packages/index.js +7 -3
- package/dist/lib/work-context/organize.d.ts +2 -0
- package/dist/lib/work-context/organize.js +22 -4
- package/dist/lib/workers-registry/index.d.ts +12 -1
- package/dist/lib/workers-registry/index.js +39 -1
- package/dist/utils/cognito-session.d.ts +4 -1
- package/dist/utils/cognito-session.js +11 -1
- package/dist/utils/hook-trust.d.ts +27 -0
- package/dist/utils/hook-trust.js +150 -8
- package/dist/utils/shipped-codex-hooks.d.ts +17 -0
- package/dist/utils/shipped-codex-hooks.js +86 -0
- package/package.json +8 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { isQuestionImported } from "../lib/lanes/imported-question.js";
|
|
2
3
|
import { buildOutpostQuestionAnswerCommand, buildOutpostQuestionCancelCommand, buildOutpostQuestionShowCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
|
|
3
4
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
4
5
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
@@ -639,6 +640,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
639
640
|
const visibleQuestion = deliveredLocalQuestions.get(q.question_id) ?? q;
|
|
640
641
|
const row = {
|
|
641
642
|
...visibleQuestion,
|
|
643
|
+
...(isQuestionImported(hqRoot, q.question_id) ? { status_display: "moved to hq decide", moved_to: "hq decide" } : {}),
|
|
642
644
|
priority: visibleQuestion.priority ?? null,
|
|
643
645
|
priority_display: visibleQuestion.priority ?? "-",
|
|
644
646
|
audience: resolveAudience(hqRoot, visibleQuestion, ctx),
|
|
@@ -741,6 +743,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
741
743
|
ok: true,
|
|
742
744
|
action: "questions.show",
|
|
743
745
|
question,
|
|
746
|
+
...(isQuestionImported(hqRoot, question.question_id) ? { status_display: "moved to hq decide", moved_to: "hq decide" } : {}),
|
|
744
747
|
audience: resolveAudience(hqRoot, question, ctx),
|
|
745
748
|
body,
|
|
746
749
|
});
|
|
@@ -4,7 +4,7 @@ import { appendCappedHistory, isFailedQuotaKeepAliveLane, providerSessionPortabl
|
|
|
4
4
|
import { parseCliEffort } from "../lib/lanes/effort.js";
|
|
5
5
|
import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
|
|
6
6
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
7
|
-
import { laneStories,
|
|
7
|
+
import { attributedStoryRow, laneStories, storiesAfterAttribution, storyWarning } from "../lib/lanes/stories.js";
|
|
8
8
|
import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
|
|
9
9
|
import { markSpawnFailure, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
10
10
|
import { appendProjectFocusOverride, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
@@ -359,15 +359,13 @@ export function createLaneResumeHandlers(deps) {
|
|
|
359
359
|
const already = current.attribution_history.some((entry) => entry.message_id === messageId);
|
|
360
360
|
if (already)
|
|
361
361
|
return current;
|
|
362
|
-
const
|
|
362
|
+
const addedStory = attributedStoryRow(current, projectId, storyId, at);
|
|
363
363
|
storySwitchRollback = {
|
|
364
364
|
message_id: messageId,
|
|
365
365
|
project_id: current.project_id,
|
|
366
366
|
story_id: current.story_id,
|
|
367
367
|
no_story_reason: current.no_story_reason,
|
|
368
|
-
...(
|
|
369
|
-
added_story: { project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
370
|
-
} : {}),
|
|
368
|
+
...(addedStory ? { added_story: addedStory } : {}),
|
|
371
369
|
story_switch_unacknowledged: current.story_switch_unacknowledged,
|
|
372
370
|
};
|
|
373
371
|
return {
|
|
@@ -375,12 +373,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
375
373
|
project_id: projectId,
|
|
376
374
|
story_id: storyId,
|
|
377
375
|
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
378
|
-
stories:
|
|
379
|
-
? laneStories(current)
|
|
380
|
-
: [
|
|
381
|
-
...laneStories(current),
|
|
382
|
-
{ project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
383
|
-
],
|
|
376
|
+
stories: storiesAfterAttribution(current, projectId, storyId, at),
|
|
384
377
|
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
385
378
|
project_id: projectId,
|
|
386
379
|
story_id: storyId,
|
|
@@ -44,6 +44,9 @@ export function createLaneStoryHandlers(deps) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
function addCarriedStory(lane, projectId, storyId, at) {
|
|
47
|
+
if (!storyId.trim()) {
|
|
48
|
+
throw new LanesError("missing_story", "A story id is required to add a story to a lane", { lane_id: lane.lane_id });
|
|
49
|
+
}
|
|
47
50
|
if (laneStory(lane, projectId, storyId)) {
|
|
48
51
|
throw new LanesError("story_already_on_lane", `Story ${projectId}/${storyId} is already carried by lane ${lane.lane_id}`, {
|
|
49
52
|
lane_id: lane.lane_id,
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* `HQ_NO_UPDATE_CHECK=1`; always best-effort.
|
|
21
21
|
*/
|
|
22
22
|
import { Command } from 'commander';
|
|
23
|
+
import { retrustCodexHooksAfterRescue } from '../utils/hook-trust.js';
|
|
23
24
|
export interface RescueTarget {
|
|
24
25
|
source: string;
|
|
25
26
|
/** undefined → let the script apply its own default ref (`main`). */
|
|
@@ -45,6 +46,12 @@ export declare function shouldAttemptSelfUpdate(opts: {
|
|
|
45
46
|
check?: boolean;
|
|
46
47
|
selfUpdate?: boolean;
|
|
47
48
|
}): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Re-trust HQ's Codex hooks after a rescue that changed files on disk.
|
|
51
|
+
* Exported for tests. Best-effort: a trust failure is reported but never
|
|
52
|
+
* changes the rescue's exit status, because the core update itself succeeded.
|
|
53
|
+
*/
|
|
54
|
+
export declare function retrustHooksAfterRescue(hqRoot: string, retrust?: typeof retrustCodexHooksAfterRescue): Promise<void>;
|
|
48
55
|
/** Latest release tag for a repo, e.g. `v12.3.0`. Throws on failure. */
|
|
49
56
|
export declare function getLatestReleaseTag(repo: string, token?: string): Promise<string>;
|
|
50
57
|
/** Resolve a tag to its commit SHA, dereferencing annotated tags. */
|
package/dist/commands/rescue.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from 'child_process';
|
|
2
1
|
import * as fs from 'fs';
|
|
3
2
|
import * as path from 'path';
|
|
4
3
|
import * as yaml from 'js-yaml';
|
|
@@ -6,8 +5,8 @@ import chalk from 'chalk';
|
|
|
6
5
|
import { rescue } from '@indigoai-us/hq-cloud';
|
|
7
6
|
import { findHqRoot } from '../utils/manifest.js';
|
|
8
7
|
import { selfUpdateAndReexec } from '../utils/self-update.js';
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { retrustCodexHooksAfterRescue } from '../utils/hook-trust.js';
|
|
9
|
+
import { PROD_SOURCE, STAGING_SOURCE, resolveGhToken } from '../utils/shipped-codex-hooks.js';
|
|
11
10
|
/**
|
|
12
11
|
* Resolve the source repo + ref from the user's flags. Pure + exported for
|
|
13
12
|
* tests. `latestTag` is the resolved latest release tag (prod only); ignored
|
|
@@ -28,20 +27,27 @@ export function resolveRescueTarget(opts, latestTag) {
|
|
|
28
27
|
export function shouldAttemptSelfUpdate(opts) {
|
|
29
28
|
return opts.selfUpdate !== false && !opts.check;
|
|
30
29
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Re-trust HQ's Codex hooks after a rescue that changed files on disk.
|
|
32
|
+
* Exported for tests. Best-effort: a trust failure is reported but never
|
|
33
|
+
* changes the rescue's exit status, because the core update itself succeeded.
|
|
34
|
+
*/
|
|
35
|
+
export async function retrustHooksAfterRescue(hqRoot, retrust = retrustCodexHooksAfterRescue) {
|
|
36
|
+
let root = hqRoot;
|
|
33
37
|
try {
|
|
34
|
-
|
|
35
|
-
if (res.status === 0) {
|
|
36
|
-
const tok = res.stdout.trim();
|
|
37
|
-
if (tok)
|
|
38
|
-
return tok;
|
|
39
|
-
}
|
|
38
|
+
root = fs.realpathSync(hqRoot);
|
|
40
39
|
}
|
|
41
40
|
catch {
|
|
42
|
-
//
|
|
41
|
+
// Keep the path as given; trust reports its own failure if it is unusable.
|
|
42
|
+
}
|
|
43
|
+
const result = await retrust(root);
|
|
44
|
+
if (result.status === 'trusted') {
|
|
45
|
+
console.log(`rescue: re-trusted ${result.trusted} Codex HQ hook${result.trusted === 1 ? '' : 's'}`);
|
|
46
|
+
}
|
|
47
|
+
else if (result.status === 'failed') {
|
|
48
|
+
console.warn(chalk.yellow(`rescue: could not re-trust Codex HQ hooks: ${result.reason ?? 'unknown error'}. ` +
|
|
49
|
+
'Codex will skip changed HQ hooks until you run `hq reindex`.'));
|
|
43
50
|
}
|
|
44
|
-
return process.env.GH_TOKEN || process.env.GITHUB_TOKEN || undefined;
|
|
45
51
|
}
|
|
46
52
|
function ghHeaders(token) {
|
|
47
53
|
const headers = {
|
|
@@ -178,6 +184,9 @@ export function registerRescueCommand(program) {
|
|
|
178
184
|
cloudUpdate: opts.cloudUpdate,
|
|
179
185
|
ghToken: token,
|
|
180
186
|
});
|
|
187
|
+
if (status === 0 && !opts.check) {
|
|
188
|
+
await retrustHooksAfterRescue(hqRoot);
|
|
189
|
+
}
|
|
181
190
|
process.exit(status);
|
|
182
191
|
}
|
|
183
192
|
catch (error) {
|
package/dist/commands/run.js
CHANGED
|
@@ -149,11 +149,20 @@ export function registerRunCommand(program) {
|
|
|
149
149
|
// error instead of hanging on varlock's never-settling promise, in case
|
|
150
150
|
// the shape-dependent guard above ever silently degrades.
|
|
151
151
|
await resolveEnvValuesOrThrow(graph);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
// varlock's item.errors includes warning-severity entries (isWarning),
|
|
153
|
+
// e.g. "Value is very short! Is it actually sensitive?" for a short
|
|
154
|
+
// sensitive value. Print those and carry on; only real errors are fatal.
|
|
155
|
+
const schemaItems = Object.entries(graph.configSchema);
|
|
156
|
+
const itemMessages = (wantWarnings) => schemaItems.flatMap(([k, item]) => (item.errors ?? [])
|
|
157
|
+
.filter((e) => Boolean(e?.isWarning) === wantWarnings)
|
|
158
|
+
.map((e) => ` ${k}: ${e?.message ?? String(e)}`));
|
|
159
|
+
const warningMsgs = itemMessages(true);
|
|
160
|
+
if (warningMsgs.length > 0) {
|
|
161
|
+
process.stderr.write(`Warning: env var checks reported warnings:\n${warningMsgs.join('\n')}\n`);
|
|
162
|
+
}
|
|
163
|
+
const errorMsgs = itemMessages(false);
|
|
164
|
+
if (errorMsgs.length > 0) {
|
|
165
|
+
process.stderr.write(`Error: failed to resolve env vars:\n${errorMsgs.join('\n')}\n`);
|
|
157
166
|
process.exit(1);
|
|
158
167
|
}
|
|
159
168
|
const resolvedEnv = graph.getResolvedEnvObject();
|
|
@@ -3,6 +3,13 @@ export { readSessionCompanyLock } from "../lib/session-company-lock.js";
|
|
|
3
3
|
export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
|
|
4
4
|
export type { SecretLoadResponse, SecretScriptLockMode, SecretTier, SecretUsage, SecretUsageChannel } from "../secrets-env-action.js";
|
|
5
5
|
export type { SecretScriptUsage, SecretMetadata, SecretLoadSuccessRow } from "../secrets-env-action.js";
|
|
6
|
+
/**
|
|
7
|
+
* Exit code for `hq secrets exec` run with no session id and no explicit
|
|
8
|
+
* company. 78 is EX_CONFIG from sysexits.h: the caller's invocation is missing
|
|
9
|
+
* required configuration, so retrying the same command will not help.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SECRETS_EXEC_UNATTENDED_NO_COMPANY_EXIT = 78;
|
|
12
|
+
export declare const SECRETS_EXEC_UNATTENDED_NO_COMPANY_MESSAGE: string;
|
|
6
13
|
import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
|
|
7
14
|
export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
8
15
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
@@ -17,6 +24,12 @@ interface SecretAclPrincipal {
|
|
|
17
24
|
granteeType: "email" | "group" | "company-wide" | "person";
|
|
18
25
|
granteeId: string;
|
|
19
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* hq-pro refuses any Outpost grant above read with 400
|
|
29
|
+
* OUTPOST_GRANT_READ_ONLY. The CLI refuses it first with the same message.
|
|
30
|
+
*/
|
|
31
|
+
export declare const OUTPOST_GRANT_READ_ONLY_CODE = "OUTPOST_GRANT_READ_ONLY";
|
|
32
|
+
export declare const OUTPOST_GRANT_READ_ONLY_MESSAGE = "Company Outposts can only be given read access to a secret.";
|
|
20
33
|
export declare function parseSecretAclPrincipal(principal: string): SecretAclPrincipal | null;
|
|
21
34
|
export interface SecretInjectionRecipe {
|
|
22
35
|
header: string;
|
package/dist/commands/secrets.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import * as yaml from "js-yaml";
|
|
4
4
|
import { assertApiKeyCompany, buildSecretUsage, extractApiMessage, highSecuritySandboxOnlyMessage, loadRevealedSecrets, normalizeScriptLockMode, normalizeSecretTier, parseSecretNameList, requestedApiKeyCompany, runSecretsEnvCommand, scopeOpts, secretsCredentialOptions, } from "../secrets-env-action.js";
|
|
5
5
|
import { readSessionCompanyLock } from "../lib/session-company-lock.js";
|
|
6
|
+
import { sessionIdFromEnv } from "../lib/core-utils/session-id.js";
|
|
6
7
|
export { readSessionCompanyLock } from "../lib/session-company-lock.js";
|
|
7
8
|
export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
|
|
8
9
|
function companyUidForSlug(root, slug) {
|
|
@@ -53,6 +54,16 @@ async function resolveApiKeyCompanyWithinLock(input) {
|
|
|
53
54
|
}
|
|
54
55
|
import chalk from "chalk";
|
|
55
56
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
57
|
+
/**
|
|
58
|
+
* Exit code for `hq secrets exec` run with no session id and no explicit
|
|
59
|
+
* company. 78 is EX_CONFIG from sysexits.h: the caller's invocation is missing
|
|
60
|
+
* required configuration, so retrying the same command will not help.
|
|
61
|
+
*/
|
|
62
|
+
export const SECRETS_EXEC_UNATTENDED_NO_COMPANY_EXIT = 78;
|
|
63
|
+
export const SECRETS_EXEC_UNATTENDED_NO_COMPANY_MESSAGE = "hq secrets exec has no session id (HQ_SESSION_ID, CLAUDE_CODE_SESSION_ID, CLAUDE_SESSION_ID, " +
|
|
64
|
+
"CODEX_SESSION_ID, CODEX_THREAD_ID and GROK_SESSION_ID are all unset), so it will not borrow a company " +
|
|
65
|
+
"from whichever interactive session last wrote workspace/sessions/.current. " +
|
|
66
|
+
"Pass --company <slug> (or --personal), or set HQ_SESSION_ID to the session this job runs under.";
|
|
56
67
|
import { removeCacheEntry, clearAllCache } from "../utils/secrets-cache.js";
|
|
57
68
|
import { isHqCliChild, releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
|
|
58
69
|
import { resolveDefaultHqRoot } from "../utils/hq-root-resolver.js";
|
|
@@ -63,7 +74,7 @@ import { isPlanGateError } from "../utils/plan-gate-error.js";
|
|
|
63
74
|
import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN, OUTPOST_UID_PATTERN, } from "./_patterns.js";
|
|
64
75
|
import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, formatSecretsListHeader, } from "./secrets-scope.js";
|
|
65
76
|
import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
|
|
66
|
-
import { formatApiKeyCapabilityDenial, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
|
|
77
|
+
import { formatApiKeyCapabilityDenial, peekHqApiKey, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
|
|
67
78
|
import { runSandboxJobAfterSubmission } from "../utils/secrets-sandbox-resident.js";
|
|
68
79
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
69
80
|
/**
|
|
@@ -277,6 +288,12 @@ function promptSecretInteractively() {
|
|
|
277
288
|
process.stdin.resume();
|
|
278
289
|
});
|
|
279
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* hq-pro refuses any Outpost grant above read with 400
|
|
293
|
+
* OUTPOST_GRANT_READ_ONLY. The CLI refuses it first with the same message.
|
|
294
|
+
*/
|
|
295
|
+
export const OUTPOST_GRANT_READ_ONLY_CODE = "OUTPOST_GRANT_READ_ONLY";
|
|
296
|
+
export const OUTPOST_GRANT_READ_ONLY_MESSAGE = "Company Outposts can only be given read access to a secret.";
|
|
280
297
|
export function parseSecretAclPrincipal(principal) {
|
|
281
298
|
const p = principal.trim();
|
|
282
299
|
if (p === "@all") {
|
|
@@ -1164,11 +1181,23 @@ export function registerSecretsCommand(program) {
|
|
|
1164
1181
|
process.exit(1);
|
|
1165
1182
|
}
|
|
1166
1183
|
const keys = parseSecretNameList(_opts.only);
|
|
1167
|
-
const multiCompanyLockEnabled = await fastFlagEnabledDefaultOn(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
1168
|
-
const sessionLock = multiCompanyLockEnabled ? readSessionCompanyLock(process.env) : undefined;
|
|
1169
1184
|
const parentScope = secrets.opts();
|
|
1170
1185
|
const inheritedCompany = parentScope.company;
|
|
1171
1186
|
const requestedCompany = _opts.company ?? inheritedCompany;
|
|
1187
|
+
// A launchd or cron job has no session id. Session resolution would then
|
|
1188
|
+
// fall back to workspace/sessions/.current, which names whichever
|
|
1189
|
+
// interactive session last ran a hook, and the job would read that
|
|
1190
|
+
// session's company's same-named secret. Without a session id the caller
|
|
1191
|
+
// must name its scope. HQ_API_KEY is exempt: the key is bound to one
|
|
1192
|
+
// company and never reads the session lock below.
|
|
1193
|
+
const hasCallerSession = Boolean(sessionIdFromEnv(process.env));
|
|
1194
|
+
if (!hasCallerSession && !requestedCompany && !parentScope.personal && peekHqApiKey() === undefined) {
|
|
1195
|
+
console.error(chalk.red("Error:"), SECRETS_EXEC_UNATTENDED_NO_COMPANY_MESSAGE);
|
|
1196
|
+
process.exit(SECRETS_EXEC_UNATTENDED_NO_COMPANY_EXIT);
|
|
1197
|
+
}
|
|
1198
|
+
const multiCompanyLockEnabled = await fastFlagEnabledDefaultOn(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
1199
|
+
// Only the caller's own session id may supply a lock; never .current.
|
|
1200
|
+
const sessionLock = multiCompanyLockEnabled && hasCallerSession ? readSessionCompanyLock(process.env) : undefined;
|
|
1172
1201
|
const hasLockedCompanies = (sessionLock?.companies.length ?? 0) > 0;
|
|
1173
1202
|
const lockRoot = hasLockedCompanies ? resolveDefaultHqRoot({ env: process.env }) : undefined;
|
|
1174
1203
|
const selectedCompany = (hasLockedCompanies && requestedCompany && looksLikeCompanyUid(requestedCompany)
|
|
@@ -1319,7 +1348,7 @@ export function registerSecretsCommand(program) {
|
|
|
1319
1348
|
.command("share <path>")
|
|
1320
1349
|
.description("Share a secret with an email address, group, company Outpost (out_…), or the entire company (@all)")
|
|
1321
1350
|
.requiredOption("--with <principal>", "Email address, group id, out_ Outpost id, or @all (entire company) to share with")
|
|
1322
|
-
.
|
|
1351
|
+
.option("--permission <level>", "Permission level: read | write | admin (required, except for a company Outpost, which can only be given read and defaults to it)")
|
|
1323
1352
|
.action(async (path, opts) => {
|
|
1324
1353
|
try {
|
|
1325
1354
|
rejectIfPersonal(secrets.opts(), "share");
|
|
@@ -1327,7 +1356,8 @@ export function registerSecretsCommand(program) {
|
|
|
1327
1356
|
console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
|
|
1328
1357
|
process.exit(1);
|
|
1329
1358
|
}
|
|
1330
|
-
if (
|
|
1359
|
+
if (opts.permission !== undefined &&
|
|
1360
|
+
!["read", "write", "admin"].includes(opts.permission)) {
|
|
1331
1361
|
console.error(chalk.red(`Invalid permission '${opts.permission}': must be one of read, write, admin`));
|
|
1332
1362
|
process.exit(1);
|
|
1333
1363
|
}
|
|
@@ -1335,6 +1365,16 @@ export function registerSecretsCommand(program) {
|
|
|
1335
1365
|
if (!principal) {
|
|
1336
1366
|
process.exit(1);
|
|
1337
1367
|
}
|
|
1368
|
+
const isOutpost = OUTPOST_UID_PATTERN.test(principal.granteeId);
|
|
1369
|
+
if (opts.permission === undefined && !isOutpost) {
|
|
1370
|
+
console.error(chalk.red("Missing --permission: must be one of read, write, admin"));
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
const permission = opts.permission ?? "read";
|
|
1374
|
+
if (isOutpost && permission !== "read") {
|
|
1375
|
+
console.error(chalk.red(OUTPOST_GRANT_READ_ONLY_MESSAGE));
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1338
1378
|
const target = await resolveSecretsTarget("secrets:share", "secrets share", secrets.opts());
|
|
1339
1379
|
const res = await vaultApiFetch({
|
|
1340
1380
|
token: target.token,
|
|
@@ -1344,12 +1384,15 @@ export function registerSecretsCommand(program) {
|
|
|
1344
1384
|
path,
|
|
1345
1385
|
granteeType: principal.granteeType,
|
|
1346
1386
|
granteeId: principal.granteeId,
|
|
1347
|
-
permission
|
|
1387
|
+
permission,
|
|
1348
1388
|
},
|
|
1349
1389
|
});
|
|
1350
1390
|
if (!res.ok) {
|
|
1351
1391
|
const body = await res.json().catch(() => ({}));
|
|
1352
|
-
if (res.status ===
|
|
1392
|
+
if (res.status === 400 && body.code === OUTPOST_GRANT_READ_ONLY_CODE) {
|
|
1393
|
+
console.error(chalk.red(OUTPOST_GRANT_READ_ONLY_MESSAGE));
|
|
1394
|
+
}
|
|
1395
|
+
else if (res.status === 401) {
|
|
1353
1396
|
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
1354
1397
|
}
|
|
1355
1398
|
else if (res.status === 403) {
|
|
@@ -1371,7 +1414,7 @@ export function registerSecretsCommand(program) {
|
|
|
1371
1414
|
}
|
|
1372
1415
|
process.exit(1);
|
|
1373
1416
|
}
|
|
1374
|
-
console.log(chalk.green(`Shared '${path}' with ${describeSecretAclPrincipal(principal)} (${
|
|
1417
|
+
console.log(chalk.green(`Shared '${path}' with ${describeSecretAclPrincipal(principal)} (${permission})`));
|
|
1375
1418
|
}
|
|
1376
1419
|
catch (err) {
|
|
1377
1420
|
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|