@indigoai-us/hq-cli 5.121.1 → 5.122.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/CHANGELOG.md +34 -0
- package/dist/command-catalog.generated.d.ts +38 -4
- package/dist/command-catalog.generated.js +48 -4
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +26 -0
- package/dist/commands/agent-enroll.js +63 -6
- package/dist/commands/agent.js +15 -1
- package/dist/commands/cloud-demote.js +3 -2
- package/dist/commands/cloud-provision.d.ts +11 -0
- package/dist/commands/cloud-provision.js +25 -0
- package/dist/commands/cloud-retire.d.ts +51 -0
- package/dist/commands/cloud-retire.js +154 -0
- package/dist/commands/mesh.js +26 -26
- package/dist/lib/agent-kit/adopt-identity.d.ts +27 -0
- package/dist/lib/agent-kit/adopt-identity.js +51 -0
- package/dist/lib/agent-kit/paths.d.ts +20 -0
- package/dist/lib/agent-kit/paths.js +61 -1
- package/dist/lib/doctor/checks/work-context.js +1 -1
- package/dist/lib/mesh/client.d.ts +3 -2
- package/dist/lib/mesh/client.js +3 -2
- package/dist/lib/mesh/live/backfill-held.d.ts +3 -2
- package/dist/lib/mesh/live/backfill-held.js +3 -2
- package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +12 -2
- package/dist/lib/mesh/live/daemon/run.d.ts +7 -0
- package/dist/lib/mesh/live/daemon/run.js +41 -0
- package/dist/lib/mesh/live/flush.js +44 -2
- package/dist/lib/mesh/live/spool.d.ts +13 -0
- package/dist/lib/mesh/live/spool.js +67 -0
- package/dist/lib/work-context/config.d.ts +1 -4
- package/dist/lib/work-context/config.js +1 -5
- package/dist/lib/work-context/outbox.d.ts +8 -0
- package/dist/lib/work-context/outbox.js +47 -0
- package/dist/lib/work-context/reconcile.js +12 -2
- package/dist/lib/work-context/state.d.ts +23 -0
- package/dist/lib/work-context/state.js +67 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Work Mesh now keeps the company you selected when its session context is
|
|
8
|
+
acknowledged or recovered after a restart. Mesh activity stays attached to
|
|
9
|
+
the right company instead of being held for missing attribution.
|
|
10
|
+
|
|
11
|
+
## [5.122.0] — 2026-09-18
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A bot can now join HQ from the computer its owner already uses. Before,
|
|
16
|
+
`hq agent enroll` refused on such a machine — the owner is signed in there,
|
|
17
|
+
and an agent identity must not share a host account with a person's login —
|
|
18
|
+
which left two bad options: a second user account, or `--replace`.
|
|
19
|
+
|
|
20
|
+
Enrollment now moves the bot into its own directory instead of refusing:
|
|
21
|
+
`~/.hq-agent/<name>/`, with its own credentials, key, state and logs. The
|
|
22
|
+
owner's session file is never read, moved, or changed. `hq agent probe`,
|
|
23
|
+
`hq agent kit`, `hq agent inbox` and `hq agent mcp` all find that directory
|
|
24
|
+
on their own, so the bot is itself without anyone exporting environment
|
|
25
|
+
variables. Use `--name <bot>` to choose the directory, or to run a second
|
|
26
|
+
bot on the same computer.
|
|
27
|
+
|
|
28
|
+
Two or more local bots are deliberately ambiguous: `hq agent` commands then
|
|
29
|
+
ask for `HQ_AGENT_DIR` rather than guessing which identity to act as.
|
|
30
|
+
|
|
31
|
+
## [5.121.2] — 2026-09-18
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `hq cloud retire company <slug>` soft-tombstones a cloud company (owner
|
|
36
|
+
only, confirm unless `--yes`) so `hq cloud demote company` can run without
|
|
37
|
+
`--propagate-deletes`. Vault files stay; local folders are not touched.
|
|
38
|
+
|
|
5
39
|
## [5.121.1] — 2026-09-18
|
|
6
40
|
|
|
7
41
|
### Fixed
|
|
@@ -722,6 +722,37 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
722
722
|
}];
|
|
723
723
|
readonly subcommands: readonly [];
|
|
724
724
|
}];
|
|
725
|
+
}, {
|
|
726
|
+
readonly name: "retire";
|
|
727
|
+
readonly description: "Soft-tombstone a cloud-backed company (owner only)";
|
|
728
|
+
readonly aliases: readonly [];
|
|
729
|
+
readonly hidden: false;
|
|
730
|
+
readonly usage: "[options] [command]";
|
|
731
|
+
readonly arguments: readonly [];
|
|
732
|
+
readonly options: readonly [];
|
|
733
|
+
readonly subcommands: readonly [{
|
|
734
|
+
readonly name: "company";
|
|
735
|
+
readonly description: "Soft-tombstone a cloud company so `hq cloud demote company` can run. Does not delete vault files or local folders. Prompts unless --yes.";
|
|
736
|
+
readonly aliases: readonly [];
|
|
737
|
+
readonly hidden: false;
|
|
738
|
+
readonly usage: "[options] <slug>";
|
|
739
|
+
readonly arguments: readonly [{
|
|
740
|
+
readonly name: "slug";
|
|
741
|
+
readonly required: true;
|
|
742
|
+
readonly variadic: false;
|
|
743
|
+
}];
|
|
744
|
+
readonly options: readonly [{
|
|
745
|
+
readonly flags: "--hq-root <path>";
|
|
746
|
+
readonly description: "Local HQ tree root (default: <runtime-path>)";
|
|
747
|
+
}, {
|
|
748
|
+
readonly flags: "--vault-api-url <url>";
|
|
749
|
+
readonly description: "Vault API URL (default: https://hqapi.hq.computer)";
|
|
750
|
+
}, {
|
|
751
|
+
readonly flags: "-y, --yes";
|
|
752
|
+
readonly description: "Skip the confirmation prompt (for scripts)";
|
|
753
|
+
}];
|
|
754
|
+
readonly subcommands: readonly [];
|
|
755
|
+
}];
|
|
725
756
|
}, {
|
|
726
757
|
readonly name: "demote";
|
|
727
758
|
readonly description: "Demote a cloud-backed entity back to local-only";
|
|
@@ -732,7 +763,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
732
763
|
readonly options: readonly [];
|
|
733
764
|
readonly subcommands: readonly [{
|
|
734
765
|
readonly name: "company";
|
|
735
|
-
readonly description: "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned
|
|
766
|
+
readonly description: "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned (`hq cloud retire company` or hq-console). Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.";
|
|
736
767
|
readonly aliases: readonly [];
|
|
737
768
|
readonly hidden: false;
|
|
738
769
|
readonly usage: "[options] <slug>";
|
|
@@ -4240,6 +4271,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4240
4271
|
}, {
|
|
4241
4272
|
readonly flags: "--replace";
|
|
4242
4273
|
readonly description: "Overwrite an existing machine identity on this host";
|
|
4274
|
+
}, {
|
|
4275
|
+
readonly flags: "--name <bot>";
|
|
4276
|
+
readonly description: "Enroll into ~/.hq-agent/<bot>/ — for a bot sharing a computer with its owner";
|
|
4243
4277
|
}, {
|
|
4244
4278
|
readonly flags: "--api-base-url <url>";
|
|
4245
4279
|
readonly description: "hq-pro control plane (default: HQ_VAULT_API_URL or production)";
|
|
@@ -5716,7 +5750,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5716
5750
|
readonly subcommands: readonly [];
|
|
5717
5751
|
}, {
|
|
5718
5752
|
readonly name: "set";
|
|
5719
|
-
readonly description: "Set the device default company slug
|
|
5753
|
+
readonly description: "Set the device default company slug after verifying membership";
|
|
5720
5754
|
readonly aliases: readonly [];
|
|
5721
5755
|
readonly hidden: false;
|
|
5722
5756
|
readonly usage: "[options] <slug>";
|
|
@@ -5727,10 +5761,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5727
5761
|
}];
|
|
5728
5762
|
readonly options: readonly [{
|
|
5729
5763
|
readonly flags: "--allow-without-migration";
|
|
5730
|
-
readonly description: "
|
|
5764
|
+
readonly description: "Deprecated compatibility no-op; selected-company membership is always verified";
|
|
5731
5765
|
}, {
|
|
5732
5766
|
readonly flags: "--company <slug|uid>";
|
|
5733
|
-
readonly description: "Deprecated no-op;
|
|
5767
|
+
readonly description: "Deprecated no-op; the selected company is verified directly";
|
|
5734
5768
|
}, {
|
|
5735
5769
|
readonly flags: "--json";
|
|
5736
5770
|
readonly description: "Print machine-readable JSON";
|
|
@@ -924,6 +924,46 @@ export const COMMAND_CATALOG = [
|
|
|
924
924
|
}
|
|
925
925
|
]
|
|
926
926
|
},
|
|
927
|
+
{
|
|
928
|
+
"name": "retire",
|
|
929
|
+
"description": "Soft-tombstone a cloud-backed company (owner only)",
|
|
930
|
+
"aliases": [],
|
|
931
|
+
"hidden": false,
|
|
932
|
+
"usage": "[options] [command]",
|
|
933
|
+
"arguments": [],
|
|
934
|
+
"options": [],
|
|
935
|
+
"subcommands": [
|
|
936
|
+
{
|
|
937
|
+
"name": "company",
|
|
938
|
+
"description": "Soft-tombstone a cloud company so `hq cloud demote company` can run. Does not delete vault files or local folders. Prompts unless --yes.",
|
|
939
|
+
"aliases": [],
|
|
940
|
+
"hidden": false,
|
|
941
|
+
"usage": "[options] <slug>",
|
|
942
|
+
"arguments": [
|
|
943
|
+
{
|
|
944
|
+
"name": "slug",
|
|
945
|
+
"required": true,
|
|
946
|
+
"variadic": false
|
|
947
|
+
}
|
|
948
|
+
],
|
|
949
|
+
"options": [
|
|
950
|
+
{
|
|
951
|
+
"flags": "--hq-root <path>",
|
|
952
|
+
"description": "Local HQ tree root (default: <runtime-path>)"
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
"flags": "--vault-api-url <url>",
|
|
956
|
+
"description": "Vault API URL (default: https://hqapi.hq.computer)"
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
"flags": "-y, --yes",
|
|
960
|
+
"description": "Skip the confirmation prompt (for scripts)"
|
|
961
|
+
}
|
|
962
|
+
],
|
|
963
|
+
"subcommands": []
|
|
964
|
+
}
|
|
965
|
+
]
|
|
966
|
+
},
|
|
927
967
|
{
|
|
928
968
|
"name": "demote",
|
|
929
969
|
"description": "Demote a cloud-backed entity back to local-only",
|
|
@@ -935,7 +975,7 @@ export const COMMAND_CATALOG = [
|
|
|
935
975
|
"subcommands": [
|
|
936
976
|
{
|
|
937
977
|
"name": "company",
|
|
938
|
-
"description": "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned
|
|
978
|
+
"description": "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned (`hq cloud retire company` or hq-console). Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.",
|
|
939
979
|
"aliases": [],
|
|
940
980
|
"hidden": false,
|
|
941
981
|
"usage": "[options] <slug>",
|
|
@@ -5489,6 +5529,10 @@ export const COMMAND_CATALOG = [
|
|
|
5489
5529
|
"flags": "--replace",
|
|
5490
5530
|
"description": "Overwrite an existing machine identity on this host"
|
|
5491
5531
|
},
|
|
5532
|
+
{
|
|
5533
|
+
"flags": "--name <bot>",
|
|
5534
|
+
"description": "Enroll into ~/.hq-agent/<bot>/ — for a bot sharing a computer with its owner"
|
|
5535
|
+
},
|
|
5492
5536
|
{
|
|
5493
5537
|
"flags": "--api-base-url <url>",
|
|
5494
5538
|
"description": "hq-pro control plane (default: HQ_VAULT_API_URL or production)"
|
|
@@ -7374,7 +7418,7 @@ export const COMMAND_CATALOG = [
|
|
|
7374
7418
|
},
|
|
7375
7419
|
{
|
|
7376
7420
|
"name": "set",
|
|
7377
|
-
"description": "Set the device default company slug
|
|
7421
|
+
"description": "Set the device default company slug after verifying membership",
|
|
7378
7422
|
"aliases": [],
|
|
7379
7423
|
"hidden": false,
|
|
7380
7424
|
"usage": "[options] <slug>",
|
|
@@ -7388,11 +7432,11 @@ export const COMMAND_CATALOG = [
|
|
|
7388
7432
|
"options": [
|
|
7389
7433
|
{
|
|
7390
7434
|
"flags": "--allow-without-migration",
|
|
7391
|
-
"description": "
|
|
7435
|
+
"description": "Deprecated compatibility no-op; selected-company membership is always verified"
|
|
7392
7436
|
},
|
|
7393
7437
|
{
|
|
7394
7438
|
"flags": "--company <slug|uid>",
|
|
7395
|
-
"description": "Deprecated no-op;
|
|
7439
|
+
"description": "Deprecated no-op; the selected company is verified directly"
|
|
7396
7440
|
},
|
|
7397
7441
|
{
|
|
7398
7442
|
"flags": "--json",
|
|
@@ -141,6 +141,12 @@ export declare const REGISTRATION_PLAN: readonly [{
|
|
|
141
141
|
readonly parent: "cloud";
|
|
142
142
|
readonly module: "./commands/cloud-provision.js";
|
|
143
143
|
readonly exportName: "registerCloudProvisionCommands";
|
|
144
|
+
}, {
|
|
145
|
+
readonly type: "registrar";
|
|
146
|
+
readonly root: "cloud";
|
|
147
|
+
readonly parent: "cloud";
|
|
148
|
+
readonly module: "./commands/cloud-retire.js";
|
|
149
|
+
readonly exportName: "registerCloudRetireCommands";
|
|
144
150
|
}, {
|
|
145
151
|
readonly type: "registrar";
|
|
146
152
|
readonly root: "cloud";
|
|
@@ -26,6 +26,7 @@ export const REGISTRATION_PLAN = [
|
|
|
26
26
|
{ type: "registrar", root: "sync", parent: "sync", module: "./commands/sync-manifest.js", exportName: "registerSyncManifestCommand" },
|
|
27
27
|
{ type: "group", root: "cloud", id: "cloud", name: "cloud", description: "Cloud commands — provision entities and manage cloud-backed companies" },
|
|
28
28
|
{ type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-provision.js", exportName: "registerCloudProvisionCommands" },
|
|
29
|
+
{ type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-retire.js", exportName: "registerCloudRetireCommands" },
|
|
29
30
|
{ type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-demote.js", exportName: "registerCloudDemoteCommands" },
|
|
30
31
|
{ type: "registrar", root: "team-sync", parent: "program", module: "./commands/team-sync.js", exportName: "registerTeamSyncCommand" },
|
|
31
32
|
{ type: "registrar", root: "login", parent: "program", module: "./commands/login.js", exportName: "registerLoginCommand" },
|
|
@@ -88,7 +88,29 @@ export interface EnrollOptions {
|
|
|
88
88
|
company?: string;
|
|
89
89
|
replace?: boolean;
|
|
90
90
|
apiBaseUrl?: string;
|
|
91
|
+
/** Enroll into `~/.hq-agent/<name>/` — a bot sharing a host with a person. */
|
|
92
|
+
name?: string;
|
|
91
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Where this enrollment should write, given what already lives on the host.
|
|
96
|
+
*
|
|
97
|
+
* A bot that runs on its owner's own computer is the common case, not an
|
|
98
|
+
* error: the machine has the owner's HQ session, and the two identities have
|
|
99
|
+
* to coexist. So an unpinned enrollment onto such a host does NOT refuse and
|
|
100
|
+
* does NOT overwrite anything — it moves into `~/.hq-agent/<name>/`, its own
|
|
101
|
+
* tree with its own credentials, key, state and logs. The person's session
|
|
102
|
+
* file is neither read nor touched, and `hq agent …` finds the bot's tree on
|
|
103
|
+
* its own afterwards (see agentDir).
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveEnrollTarget(opts: {
|
|
106
|
+
name?: string;
|
|
107
|
+
replace?: boolean;
|
|
108
|
+
}, env?: NodeJS.ProcessEnv, home?: string): {
|
|
109
|
+
paths: AgentKitPaths;
|
|
110
|
+
local: boolean;
|
|
111
|
+
};
|
|
112
|
+
/** Lowercase slug, so the directory name can never escape `~/.hq-agent`. */
|
|
113
|
+
export declare function validateLocalAgentName(name: string): string;
|
|
92
114
|
export interface EnrollResult {
|
|
93
115
|
agentUid: string;
|
|
94
116
|
companySlug: string;
|
|
@@ -98,6 +120,10 @@ export interface EnrollResult {
|
|
|
98
120
|
hostKeyPath: string;
|
|
99
121
|
/** Set when --company disagreed with the server's company. */
|
|
100
122
|
companyMismatch?: string;
|
|
123
|
+
/** True when this enrolled beside a person's session in its own directory. */
|
|
124
|
+
local: boolean;
|
|
125
|
+
/** The tree this identity lives in. */
|
|
126
|
+
agentDir: string;
|
|
101
127
|
}
|
|
102
128
|
/** Pure-ish orchestration so tests can drive it without a TTY or network. */
|
|
103
129
|
export declare function enrollHost(opts: EnrollOptions, deps?: EnrollDeps): Promise<EnrollResult>;
|
|
@@ -22,7 +22,7 @@ import { DEFAULT_VAULT_API_URL, personTokenCacheFile, } from "../utils/cognito-s
|
|
|
22
22
|
import { HQ_CLIENT_NAME } from "../utils/vault-api.js";
|
|
23
23
|
import { networkTransportErrorCode } from "../utils/network-transport-error.js";
|
|
24
24
|
import { generateHostKeyPair, machineCredsFileExists, readExternalMachineCreds, writeHostKeyPair, writeMachineCreds, } from "../lib/agent-kit/creds.js";
|
|
25
|
-
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
25
|
+
import { DEFAULT_LOCAL_AGENT_NAME, agentKitPaths, localAgentDir, } from "../lib/agent-kit/paths.js";
|
|
26
26
|
export const ENROLL_PATH = "/v1/agents/enroll";
|
|
27
27
|
/** Canonical code: un-grouped, uppercase (server hashes exactly this). */
|
|
28
28
|
export function normalizeEnrollmentCode(raw) {
|
|
@@ -63,9 +63,14 @@ export function refusalMessage(existing) {
|
|
|
63
63
|
case "none":
|
|
64
64
|
return null;
|
|
65
65
|
case "human":
|
|
66
|
+
// Reached only when a caller pins the tree (HQ_AGENT_DIR / --dir) at a
|
|
67
|
+
// home that would sit on top of a person's login. The unpinned path
|
|
68
|
+
// enrolls into a sibling directory instead of refusing — see
|
|
69
|
+
// resolveEnrollTarget.
|
|
66
70
|
return (`A human HQ session exists at ${existing.file}. An agent identity must not ` +
|
|
67
|
-
`share a host account with a person's login.
|
|
68
|
-
`
|
|
71
|
+
`share a host account with a person's login. Enroll into a directory of its ` +
|
|
72
|
+
`own (drop HQ_AGENT_DIR and this happens automatically, or pass ` +
|
|
73
|
+
`--name <bot>), or pass --replace to enroll here anyway — the human session ` +
|
|
69
74
|
`file is left untouched and never read.`);
|
|
70
75
|
case "machine":
|
|
71
76
|
return (`This host already has a machine identity at ${existing.file}` +
|
|
@@ -169,10 +174,49 @@ function parseEnrollResponse(raw) {
|
|
|
169
174
|
},
|
|
170
175
|
};
|
|
171
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Where this enrollment should write, given what already lives on the host.
|
|
179
|
+
*
|
|
180
|
+
* A bot that runs on its owner's own computer is the common case, not an
|
|
181
|
+
* error: the machine has the owner's HQ session, and the two identities have
|
|
182
|
+
* to coexist. So an unpinned enrollment onto such a host does NOT refuse and
|
|
183
|
+
* does NOT overwrite anything — it moves into `~/.hq-agent/<name>/`, its own
|
|
184
|
+
* tree with its own credentials, key, state and logs. The person's session
|
|
185
|
+
* file is neither read nor touched, and `hq agent …` finds the bot's tree on
|
|
186
|
+
* its own afterwards (see agentDir).
|
|
187
|
+
*/
|
|
188
|
+
export function resolveEnrollTarget(opts, env = process.env, home = os.homedir()) {
|
|
189
|
+
const pinned = Boolean(env.HQ_AGENT_DIR?.trim() || env.HQ_MACHINE_CREDS_FILE?.trim());
|
|
190
|
+
if (opts.name) {
|
|
191
|
+
const dir = localAgentDir(validateLocalAgentName(opts.name), home);
|
|
192
|
+
return { paths: agentKitPaths(home, { ...env, HQ_AGENT_DIR: dir }), local: true };
|
|
193
|
+
}
|
|
194
|
+
const base = agentKitPaths(home, env);
|
|
195
|
+
// A pinned tree is the caller's explicit choice; --replace is an explicit
|
|
196
|
+
// choice too. Neither gets silently redirected.
|
|
197
|
+
if (pinned || opts.replace)
|
|
198
|
+
return { paths: base, local: false };
|
|
199
|
+
if (detectExistingIdentity(base, env).kind !== "human") {
|
|
200
|
+
return { paths: base, local: false };
|
|
201
|
+
}
|
|
202
|
+
const dir = localAgentDir(DEFAULT_LOCAL_AGENT_NAME, home);
|
|
203
|
+
return { paths: agentKitPaths(home, { ...env, HQ_AGENT_DIR: dir }), local: true };
|
|
204
|
+
}
|
|
205
|
+
/** Lowercase slug, so the directory name can never escape `~/.hq-agent`. */
|
|
206
|
+
export function validateLocalAgentName(name) {
|
|
207
|
+
const trimmed = (name ?? "").trim();
|
|
208
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(trimmed) || trimmed.includes("--")) {
|
|
209
|
+
throw new EnrollError(`Invalid --name "${name}": use 1–40 lowercase letters, digits and single hyphens (e.g. "grokbot").`);
|
|
210
|
+
}
|
|
211
|
+
return trimmed;
|
|
212
|
+
}
|
|
172
213
|
/** Pure-ish orchestration so tests can drive it without a TTY or network. */
|
|
173
214
|
export async function enrollHost(opts, deps = {}) {
|
|
174
215
|
const env = deps.env ?? process.env;
|
|
175
|
-
const
|
|
216
|
+
const target = deps.paths
|
|
217
|
+
? { paths: deps.paths, local: false }
|
|
218
|
+
: resolveEnrollTarget({ name: opts.name, replace: opts.replace }, env);
|
|
219
|
+
const paths = target.paths;
|
|
176
220
|
const doFetch = deps.fetch ?? fetch;
|
|
177
221
|
const apiBaseUrl = (opts.apiBaseUrl ?? env.HQ_VAULT_API_URL ?? DEFAULT_VAULT_API_URL).replace(/\/+$/, "");
|
|
178
222
|
const code = normalizeEnrollmentCode(opts.code);
|
|
@@ -224,6 +268,8 @@ export async function enrollHost(opts, deps = {}) {
|
|
|
224
268
|
};
|
|
225
269
|
writeMachineCreds(paths, creds);
|
|
226
270
|
return {
|
|
271
|
+
local: target.local,
|
|
272
|
+
agentDir: paths.agentDir,
|
|
227
273
|
agentUid: parsed.agentUid,
|
|
228
274
|
companySlug: parsed.companySlug,
|
|
229
275
|
companyUid: parsed.companyUid,
|
|
@@ -241,6 +287,7 @@ export function registerAgentEnrollCommand(agent) {
|
|
|
241
287
|
.description("Enroll this host as an external HQ agent using a one-time code")
|
|
242
288
|
.option("--company <slug>", "Expected company slug (warns when the code belongs elsewhere)")
|
|
243
289
|
.option("--replace", "Overwrite an existing machine identity on this host")
|
|
290
|
+
.option("--name <bot>", "Enroll into ~/.hq-agent/<bot>/ — for a bot sharing a computer with its owner")
|
|
244
291
|
.option("--api-base-url <url>", "hq-pro control plane (default: HQ_VAULT_API_URL or production)")
|
|
245
292
|
.action(async (code, opts) => {
|
|
246
293
|
try {
|
|
@@ -249,8 +296,16 @@ export function registerAgentEnrollCommand(agent) {
|
|
|
249
296
|
company: opts.company,
|
|
250
297
|
replace: opts.replace,
|
|
251
298
|
apiBaseUrl: opts.apiBaseUrl,
|
|
299
|
+
name: opts.name,
|
|
252
300
|
});
|
|
253
301
|
console.log(chalk.green(`Enrolled as ${result.agentUid} in ${result.companySlug}.`));
|
|
302
|
+
if (result.local) {
|
|
303
|
+
// Say it plainly: this host belongs to a person, and the bot is a
|
|
304
|
+
// guest on it. Nothing of theirs was read, moved or overwritten.
|
|
305
|
+
console.log(chalk.dim(` This computer already has a person signed in, so this agent lives in its own\n` +
|
|
306
|
+
` directory beside them: ${result.agentDir}. Their session was not read or changed.\n` +
|
|
307
|
+
` Every hq agent command finds this tree on its own — no environment to set.`));
|
|
308
|
+
}
|
|
254
309
|
console.log(` host fingerprint: ${result.hostFingerprint}`);
|
|
255
310
|
console.log(` credentials: ${result.credsPath} (0600)`);
|
|
256
311
|
console.log(` host key: ${result.hostKeyPath} (0600)`);
|
|
@@ -260,8 +315,10 @@ export function registerAgentEnrollCommand(agent) {
|
|
|
260
315
|
}
|
|
261
316
|
console.log("");
|
|
262
317
|
console.log("Next:");
|
|
263
|
-
console.log(
|
|
264
|
-
|
|
318
|
+
console.log(result.local
|
|
319
|
+
? " hq agent probe # its first line reports this agent, not you"
|
|
320
|
+
: " hq whoami # should report the agent identity");
|
|
321
|
+
console.log(" hq agent kit install # inbox poller and heartbeat services");
|
|
265
322
|
console.log(" hq agent probe # end-to-end check, reported to the console");
|
|
266
323
|
}
|
|
267
324
|
catch (err) {
|
package/dist/commands/agent.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* hq agent mcp stdio MCP server for the bot framework
|
|
9
9
|
* hq agent inbox [done <id…>] pending HQ messages / mark handled
|
|
10
10
|
*/
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import { adoptAgentIdentityEnv } from "../lib/agent-kit/adopt-identity.js";
|
|
13
|
+
import { agentKitPaths } from "../lib/agent-kit/paths.js";
|
|
11
14
|
import { registerAgentEnrollCommand } from "./agent-enroll.js";
|
|
12
15
|
import { registerAgentInboxCommand } from "./agent-inbox.js";
|
|
13
16
|
import { registerAgentKitCommand } from "./agent-kit.js";
|
|
@@ -16,7 +19,18 @@ import { registerAgentProbeCommand } from "./agent-probe.js";
|
|
|
16
19
|
export function registerAgentCommand(program) {
|
|
17
20
|
const agent = program
|
|
18
21
|
.command("agent")
|
|
19
|
-
.description("Enroll and run this host as an external HQ agent")
|
|
22
|
+
.description("Enroll and run this host as an external HQ agent")
|
|
23
|
+
// A bot that shares a computer with its owner keeps its identity in its
|
|
24
|
+
// own directory. Publish that tree to the environment before any
|
|
25
|
+
// subcommand runs, so the CLI and hq-cloud both mint as the AGENT rather
|
|
26
|
+
// than falling back to the one fixed path — and, on a person's computer,
|
|
27
|
+
// to the person. Enroll is exempt: it is the command that decides where
|
|
28
|
+
// the tree goes.
|
|
29
|
+
.hook("preAction", (_thisCommand, actionCommand) => {
|
|
30
|
+
if (actionCommand.name() === "enroll")
|
|
31
|
+
return;
|
|
32
|
+
adoptAgentIdentityEnv(agentKitPaths(os.homedir(), process.env), process.env);
|
|
33
|
+
});
|
|
20
34
|
registerAgentEnrollCommand(agent);
|
|
21
35
|
registerAgentKitCommand(agent);
|
|
22
36
|
registerAgentProbeCommand(agent);
|
|
@@ -160,8 +160,9 @@ export function registerCloudDemoteCommands(program) {
|
|
|
160
160
|
demoteCmd
|
|
161
161
|
.command("company")
|
|
162
162
|
.description("Demote a cloud-backed company to local-only after the cloud entity " +
|
|
163
|
-
"has been soft-tombstoned
|
|
164
|
-
"flips company.yaml `cloud: false`, and
|
|
163
|
+
"has been soft-tombstoned (`hq cloud retire company` or hq-console). " +
|
|
164
|
+
"Removes .hq/config.json, flips company.yaml `cloud: false`, and " +
|
|
165
|
+
"strips the manifest cloud refs.")
|
|
165
166
|
.argument("<slug>", "Company slug")
|
|
166
167
|
.option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
|
|
167
168
|
.option("--vault-api-url <url>", `Vault API URL (default: ${DEFAULT_VAULT_API_URL})`, DEFAULT_VAULT_API_URL)
|
|
@@ -38,6 +38,8 @@ export interface VaultEntity {
|
|
|
38
38
|
kmsKeyId?: string | null;
|
|
39
39
|
status?: string;
|
|
40
40
|
ownerUid?: string;
|
|
41
|
+
/** Set by hq-pro after a soft tombstone. Absent on live companies. */
|
|
42
|
+
deleted?: boolean;
|
|
41
43
|
}
|
|
42
44
|
/** Per-company `.hq/config.json` schema (matches AppBar `provision.rs::CompanyConfig`). */
|
|
43
45
|
export interface CompanyConfig {
|
|
@@ -142,6 +144,15 @@ export interface VaultClient {
|
|
|
142
144
|
name: string;
|
|
143
145
|
ownerUid?: string;
|
|
144
146
|
}): Promise<VaultEntity>;
|
|
147
|
+
/**
|
|
148
|
+
* Soft-tombstone a company (`DELETE /entity/{uid}`). Owner-only. Leaves the
|
|
149
|
+
* row, S3 bucket, KMS key, and memberships intact. Idempotent on an already
|
|
150
|
+
* deleted company. Used by `hq cloud retire company`.
|
|
151
|
+
*/
|
|
152
|
+
softDeleteCompany(uid: string): Promise<{
|
|
153
|
+
deleted: true;
|
|
154
|
+
uid: string;
|
|
155
|
+
}>;
|
|
145
156
|
}
|
|
146
157
|
/** Custom error class so the CLI runner can map to exit codes. */
|
|
147
158
|
export declare class ProvisionError extends Error {
|
|
@@ -364,6 +364,31 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
|
|
|
364
364
|
}
|
|
365
365
|
return data.entity;
|
|
366
366
|
},
|
|
367
|
+
async softDeleteCompany(uid) {
|
|
368
|
+
const url = `${apiUrl.replace(/\/$/, "")}/entity/${encodeURIComponent(uid)}`;
|
|
369
|
+
const res = await fetch(url, { method: "DELETE", headers });
|
|
370
|
+
if (res.status === 403) {
|
|
371
|
+
const body = await safeBody(res);
|
|
372
|
+
throw new ProvisionError(2, `Refusing to retire '${uid}': only the company owner can soft-tombstone it. ${body}`);
|
|
373
|
+
}
|
|
374
|
+
if (res.status === 404) {
|
|
375
|
+
const body = await safeBody(res);
|
|
376
|
+
throw new ProvisionError(2, `Vault DELETE /entity/${uid} failed: not found — ${body}`);
|
|
377
|
+
}
|
|
378
|
+
if (res.status === 400) {
|
|
379
|
+
const body = await safeBody(res);
|
|
380
|
+
throw new ProvisionError(2, `Vault DELETE /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
|
|
381
|
+
}
|
|
382
|
+
if (!res.ok) {
|
|
383
|
+
const body = await safeBody(res);
|
|
384
|
+
throw new ProvisionError(1, `Vault DELETE /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
|
|
385
|
+
}
|
|
386
|
+
const data = (await res.json());
|
|
387
|
+
if (data.deleted !== true || typeof data.uid !== "string") {
|
|
388
|
+
throw new ProvisionError(1, `Vault DELETE /entity/${uid} returned ${res.status} with no deleted confirmation`);
|
|
389
|
+
}
|
|
390
|
+
return { deleted: true, uid: data.uid };
|
|
391
|
+
},
|
|
367
392
|
};
|
|
368
393
|
}
|
|
369
394
|
async function safeBody(res) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq cloud retire company <slug>` — soft-tombstone a cloud company so
|
|
3
|
+
* `hq cloud demote company` can run without `--propagate-deletes`.
|
|
4
|
+
*
|
|
5
|
+
* Console equivalent: Settings → Delete company (`DELETE /entity/{uid}`).
|
|
6
|
+
* Owner-only. The entity row, S3 bucket, KMS key, and memberships stay;
|
|
7
|
+
* `/membership/me` stops listing the company. Local files are not touched —
|
|
8
|
+
* demote is the follow-up that flips the local tree back to local-only.
|
|
9
|
+
*
|
|
10
|
+
* Confirm step is on by default (`[y/N]`). `--yes` skips it for scripts.
|
|
11
|
+
* A non-TTY stdin without `--yes` refuses rather than hanging.
|
|
12
|
+
*
|
|
13
|
+
* Exit codes (mirrors cloud-provision / cloud-demote):
|
|
14
|
+
* 0 — success, already tombstoned, or the operator cancelled.
|
|
15
|
+
* 1 — vault HTTP failure (auth/network/5xx).
|
|
16
|
+
* 2 — validation (bad slug, no entity, not owner, missing confirm).
|
|
17
|
+
*/
|
|
18
|
+
import { Command } from "commander";
|
|
19
|
+
import { type VaultClient } from "./cloud-provision.js";
|
|
20
|
+
/** Final stdout JSON shape. */
|
|
21
|
+
export interface RetireResult {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
company_slug: string;
|
|
24
|
+
cloud_uid: string | null;
|
|
25
|
+
/** True when hq-pro already had deleted=true (no DELETE sent). */
|
|
26
|
+
already_deleted: boolean;
|
|
27
|
+
cancelled?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface RetireCompanyOptions {
|
|
30
|
+
slug: string;
|
|
31
|
+
hqRoot: string;
|
|
32
|
+
vaultApiUrl: string;
|
|
33
|
+
/** Skip the interactive confirmation prompt. */
|
|
34
|
+
yes?: boolean;
|
|
35
|
+
vaultClient?: VaultClient;
|
|
36
|
+
resolveAccessToken?: () => Promise<string>;
|
|
37
|
+
confirm?: (question: string) => Promise<boolean>;
|
|
38
|
+
stdinIsTTY?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function defaultConfirm(question: string): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Read `companyUid` from `companies/<slug>/.hq/config.json` when the by-slug
|
|
43
|
+
* lookup misses (a tombstoned company is filtered from that route).
|
|
44
|
+
*/
|
|
45
|
+
export declare function readLocalCompanyUid(hqRoot: string, slug: string): string | null;
|
|
46
|
+
export declare function retireCompany(options: RetireCompanyOptions): Promise<RetireResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Register `retire company <slug>` under the `cloud` command group.
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerCloudRetireCommands(program: Command): void;
|
|
51
|
+
//# sourceMappingURL=cloud-retire.d.ts.map
|