@indigoai-us/hq-cli 5.121.2 → 5.122.1
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 +35 -0
- package/dist/command-catalog.generated.d.ts +6 -3
- package/dist/command-catalog.generated.js +7 -3
- 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-provision.js +46 -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,41 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.122.1] — 2026-09-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq cloud provision company` no longer silently accepts a server-suffixed
|
|
10
|
+
slug. When another owner already holds the requested name, the vault assigns
|
|
11
|
+
`<slug>-<tag>` and used to write local config as if the original name had
|
|
12
|
+
been granted — sync then cannot push, because it looks for a folder matching
|
|
13
|
+
the cloud slug. Provision now fails before writing, and retires the
|
|
14
|
+
just-created entity so it does not leave a suffixed orphan.
|
|
15
|
+
|
|
16
|
+
- Work Mesh now keeps the company you selected when its session context is
|
|
17
|
+
acknowledged or recovered after a restart. Mesh activity stays attached to
|
|
18
|
+
the right company instead of being held for missing attribution.
|
|
19
|
+
|
|
20
|
+
## [5.122.0] — 2026-09-18
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- A bot can now join HQ from the computer its owner already uses. Before,
|
|
25
|
+
`hq agent enroll` refused on such a machine — the owner is signed in there,
|
|
26
|
+
and an agent identity must not share a host account with a person's login —
|
|
27
|
+
which left two bad options: a second user account, or `--replace`.
|
|
28
|
+
|
|
29
|
+
Enrollment now moves the bot into its own directory instead of refusing:
|
|
30
|
+
`~/.hq-agent/<name>/`, with its own credentials, key, state and logs. The
|
|
31
|
+
owner's session file is never read, moved, or changed. `hq agent probe`,
|
|
32
|
+
`hq agent kit`, `hq agent inbox` and `hq agent mcp` all find that directory
|
|
33
|
+
on their own, so the bot is itself without anyone exporting environment
|
|
34
|
+
variables. Use `--name <bot>` to choose the directory, or to run a second
|
|
35
|
+
bot on the same computer.
|
|
36
|
+
|
|
37
|
+
Two or more local bots are deliberately ambiguous: `hq agent` commands then
|
|
38
|
+
ask for `HQ_AGENT_DIR` rather than guessing which identity to act as.
|
|
39
|
+
|
|
5
40
|
## [5.121.2] — 2026-09-18
|
|
6
41
|
|
|
7
42
|
### Added
|
|
@@ -4271,6 +4271,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
4271
4271
|
}, {
|
|
4272
4272
|
readonly flags: "--replace";
|
|
4273
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";
|
|
4274
4277
|
}, {
|
|
4275
4278
|
readonly flags: "--api-base-url <url>";
|
|
4276
4279
|
readonly description: "hq-pro control plane (default: HQ_VAULT_API_URL or production)";
|
|
@@ -5747,7 +5750,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5747
5750
|
readonly subcommands: readonly [];
|
|
5748
5751
|
}, {
|
|
5749
5752
|
readonly name: "set";
|
|
5750
|
-
readonly description: "Set the device default company slug
|
|
5753
|
+
readonly description: "Set the device default company slug after verifying membership";
|
|
5751
5754
|
readonly aliases: readonly [];
|
|
5752
5755
|
readonly hidden: false;
|
|
5753
5756
|
readonly usage: "[options] <slug>";
|
|
@@ -5758,10 +5761,10 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5758
5761
|
}];
|
|
5759
5762
|
readonly options: readonly [{
|
|
5760
5763
|
readonly flags: "--allow-without-migration";
|
|
5761
|
-
readonly description: "
|
|
5764
|
+
readonly description: "Deprecated compatibility no-op; selected-company membership is always verified";
|
|
5762
5765
|
}, {
|
|
5763
5766
|
readonly flags: "--company <slug|uid>";
|
|
5764
|
-
readonly description: "Deprecated no-op;
|
|
5767
|
+
readonly description: "Deprecated no-op; the selected company is verified directly";
|
|
5765
5768
|
}, {
|
|
5766
5769
|
readonly flags: "--json";
|
|
5767
5770
|
readonly description: "Print machine-readable JSON";
|
|
@@ -5529,6 +5529,10 @@ export const COMMAND_CATALOG = [
|
|
|
5529
5529
|
"flags": "--replace",
|
|
5530
5530
|
"description": "Overwrite an existing machine identity on this host"
|
|
5531
5531
|
},
|
|
5532
|
+
{
|
|
5533
|
+
"flags": "--name <bot>",
|
|
5534
|
+
"description": "Enroll into ~/.hq-agent/<bot>/ — for a bot sharing a computer with its owner"
|
|
5535
|
+
},
|
|
5532
5536
|
{
|
|
5533
5537
|
"flags": "--api-base-url <url>",
|
|
5534
5538
|
"description": "hq-pro control plane (default: HQ_VAULT_API_URL or production)"
|
|
@@ -7414,7 +7418,7 @@ export const COMMAND_CATALOG = [
|
|
|
7414
7418
|
},
|
|
7415
7419
|
{
|
|
7416
7420
|
"name": "set",
|
|
7417
|
-
"description": "Set the device default company slug
|
|
7421
|
+
"description": "Set the device default company slug after verifying membership",
|
|
7418
7422
|
"aliases": [],
|
|
7419
7423
|
"hidden": false,
|
|
7420
7424
|
"usage": "[options] <slug>",
|
|
@@ -7428,11 +7432,11 @@ export const COMMAND_CATALOG = [
|
|
|
7428
7432
|
"options": [
|
|
7429
7433
|
{
|
|
7430
7434
|
"flags": "--allow-without-migration",
|
|
7431
|
-
"description": "
|
|
7435
|
+
"description": "Deprecated compatibility no-op; selected-company membership is always verified"
|
|
7432
7436
|
},
|
|
7433
7437
|
{
|
|
7434
7438
|
"flags": "--company <slug|uid>",
|
|
7435
|
-
"description": "Deprecated no-op;
|
|
7439
|
+
"description": "Deprecated no-op; the selected company is verified directly"
|
|
7436
7440
|
},
|
|
7437
7441
|
{
|
|
7438
7442
|
"flags": "--json",
|
|
@@ -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);
|
|
@@ -513,6 +513,52 @@ export async function provisionCompany(options) {
|
|
|
513
513
|
createdEntity = true;
|
|
514
514
|
log(`created vault entity uid=${entity.uid}`);
|
|
515
515
|
}
|
|
516
|
+
// hq-pro auto-suffixes a company slug when another owner already holds the
|
|
517
|
+
// bare name (`<slug>-<ownerTag>`). `/entity/check-slug/me` only looks at the
|
|
518
|
+
// caller's namespace, so it reports "available" and we POST; the server then
|
|
519
|
+
// returns a different slug. Writing companies/<requested>/.hq/config.json
|
|
520
|
+
// with the requested slug (or syncing with --company <assigned>) leaves the
|
|
521
|
+
// local folder and the cloud slug pointing at two different paths — share()
|
|
522
|
+
// derives syncRoot from ctx.slug, so the company can never be pushed.
|
|
523
|
+
// Fail before any local write. Retire a just-created entity so we do not
|
|
524
|
+
// leave a suffixed orphan.
|
|
525
|
+
if (entity.slug !== options.slug) {
|
|
526
|
+
const assigned = entity.slug || "(missing)";
|
|
527
|
+
let retired = false;
|
|
528
|
+
if (createdEntity) {
|
|
529
|
+
try {
|
|
530
|
+
await vaultClient.softDeleteCompany(entity.uid);
|
|
531
|
+
retired = true;
|
|
532
|
+
log(`retired vault entity uid=${entity.uid} — assigned slug ${assigned} differs from requested ${options.slug}`);
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
536
|
+
log(`failed to retire mismatched vault entity uid=${entity.uid}: ${msg}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const retireNote = retired
|
|
540
|
+
? `The just-created entity ${entity.uid} was retired.`
|
|
541
|
+
: createdEntity
|
|
542
|
+
? `Retire entity ${entity.uid} with \`hq cloud retire company\`, then retry after the original slug is free.`
|
|
543
|
+
: `Existing entity ${entity.uid} was not modified.`;
|
|
544
|
+
throw new ProvisionError(1, `Cloud assigned slug "${assigned}" instead of requested "${options.slug}". ` +
|
|
545
|
+
`Local folder is companies/${options.slug}; sync derives the company folder from the cloud slug, so this company cannot be pushed. ` +
|
|
546
|
+
`${retireNote} Ask an admin to release the original slug, or rename the local folder to "${assigned}" and re-run.`, {
|
|
547
|
+
ok: false,
|
|
548
|
+
company_slug: options.slug,
|
|
549
|
+
cloud_uid: entity.uid,
|
|
550
|
+
bucket_name: entity.bucketName ?? "",
|
|
551
|
+
vault_api_url: options.vaultApiUrl,
|
|
552
|
+
kms_key_id: entity.kmsKeyId ?? null,
|
|
553
|
+
created_entity: createdEntity,
|
|
554
|
+
manifest_patched: false,
|
|
555
|
+
config_written: false,
|
|
556
|
+
initial_sync: {
|
|
557
|
+
ok: false,
|
|
558
|
+
error: `assigned slug "${assigned}" differs from requested "${options.slug}"`,
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
}
|
|
516
562
|
if (!entity.bucketName) {
|
|
517
563
|
// Vault returned an entity without a bucket — this would happen if the
|
|
518
564
|
// provisioning Lambda asynchronously failed. We have a `cloud_uid` but
|
package/dist/commands/mesh.js
CHANGED
|
@@ -12,7 +12,7 @@ import * as readline from "node:readline/promises";
|
|
|
12
12
|
import { STORY_STATUSES, appendThreadEvent, callerLabelFromToken, ensureProjectThread, eventPayload, listActiveMembershipCompanies, listActiveThreads, patchStoryStatus, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
13
13
|
import { createCandidatesFetcher, createMigratePoster, createOrganizePoster, createWorkSessionDeliverer, fetchCompanyLive, formatCompanyLiveTable, openMeshTransport, probeMigrationCapabilityForMemberships, requireToken, } from "../lib/mesh/client.js";
|
|
14
14
|
import { clearDefaultCompany, getDefaultCompany, readDeviceConfig, recordMigrationCapabilitySnapshot, setDefaultCompany, } from "../lib/work-context/config.js";
|
|
15
|
-
import {
|
|
15
|
+
import { DefaultCompanyUnavailableError, } from "../lib/work-context/errors.js";
|
|
16
16
|
import { isValidSessionId } from "../lib/mesh/live/session-identity.js";
|
|
17
17
|
import { CLI_KIND_TO_SCHEMA, resolveEnqueueSessionId, } from "../lib/mesh/live/index.js";
|
|
18
18
|
import { flushSessionEvents } from "../lib/mesh/live/flush.js";
|
|
@@ -375,7 +375,7 @@ async function runContextDefaultGet(opts) {
|
|
|
375
375
|
}
|
|
376
376
|
async function runContextDefaultSet(slug, opts) {
|
|
377
377
|
const root = workContextHomeRoot();
|
|
378
|
-
void opts.company; // retained for CLI compatibility
|
|
378
|
+
void opts.company; // retained for CLI compatibility
|
|
379
379
|
let token;
|
|
380
380
|
try {
|
|
381
381
|
token = await requireToken();
|
|
@@ -383,42 +383,38 @@ async function runContextDefaultSet(slug, opts) {
|
|
|
383
383
|
catch {
|
|
384
384
|
token = undefined;
|
|
385
385
|
}
|
|
386
|
-
//
|
|
386
|
+
// Migration remains useful diagnostic context, but it must never prevent a
|
|
387
|
+
// verified member from choosing their own device-local preference.
|
|
387
388
|
let migrationCapability = false;
|
|
389
|
+
let migrationWarning;
|
|
388
390
|
if (token) {
|
|
389
391
|
const probe = await probeMigrationCapabilityForMemberships(token);
|
|
390
392
|
recordMigrationCapabilitySnapshot(probe, { root });
|
|
391
393
|
migrationCapability = probe.unlocked;
|
|
392
|
-
if (probe.offline
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return;
|
|
394
|
+
if (probe.offline) {
|
|
395
|
+
migrationWarning =
|
|
396
|
+
"Warning: migration capability could not be checked; membership for the selected company will still be verified.";
|
|
396
397
|
}
|
|
397
|
-
if (!probe.unlocked
|
|
398
|
+
else if (!probe.unlocked) {
|
|
398
399
|
const detail = probe.companies.length === 0
|
|
399
400
|
? "no active memberships"
|
|
400
401
|
: probe.companies
|
|
401
402
|
.filter((c) => !c.migration)
|
|
402
403
|
.map((c) => c.companySlug || c.companyUid)
|
|
403
404
|
.join(", ");
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
405
|
+
migrationWarning =
|
|
406
|
+
`Warning: migration capability is mixed or unavailable for ${detail || "some memberships"}; ` +
|
|
407
|
+
"the selected company can still be set because membership is verified directly.";
|
|
407
408
|
}
|
|
408
409
|
}
|
|
409
|
-
else
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
if (!migrationCapability && opts.allowWithoutMigration) {
|
|
415
|
-
console.error(chalk.yellow("Warning: --allow-without-migration bypasses DEFAULT_COMPANY_LOCKED; a wrong default is hard to correct until migration is available for every company you belong to."));
|
|
410
|
+
else {
|
|
411
|
+
migrationWarning =
|
|
412
|
+
"Warning: migration capability could not be checked without a Cognito session; membership for the selected company must still be verified.";
|
|
416
413
|
}
|
|
417
414
|
try {
|
|
418
415
|
const cfg = await setDefaultCompany(slug, {
|
|
419
416
|
root,
|
|
420
417
|
migrationCapability,
|
|
421
|
-
allowWithoutMigration: opts.allowWithoutMigration,
|
|
422
418
|
validateMembership: async (candidate) => {
|
|
423
419
|
if (!token) {
|
|
424
420
|
throw new DefaultCompanyUnavailableError(`Cannot verify membership for company "${candidate}" (no Cognito session; offline or not logged in)`);
|
|
@@ -437,15 +433,16 @@ async function runContextDefaultSet(slug, opts) {
|
|
|
437
433
|
},
|
|
438
434
|
});
|
|
439
435
|
if (opts.json) {
|
|
440
|
-
console.log(JSON.stringify({ ok: true, config: cfg }, null, 2));
|
|
436
|
+
console.log(JSON.stringify({ ok: true, config: cfg, migrationWarning }, null, 2));
|
|
441
437
|
return;
|
|
442
438
|
}
|
|
443
439
|
const stored = cfg.defaultCompany;
|
|
444
440
|
console.log(`Default company set to ${stored?.slug ?? slug}${stored?.uid ? ` (${stored.uid})` : ""}`);
|
|
441
|
+
if (migrationWarning)
|
|
442
|
+
console.error(chalk.yellow(migrationWarning));
|
|
445
443
|
}
|
|
446
444
|
catch (err) {
|
|
447
|
-
if (err instanceof
|
|
448
|
-
err instanceof DefaultCompanyUnavailableError) {
|
|
445
|
+
if (err instanceof DefaultCompanyUnavailableError) {
|
|
449
446
|
console.error(chalk.red(`${err.code}: ${err.message}`));
|
|
450
447
|
process.exitCode = 1;
|
|
451
448
|
return;
|
|
@@ -715,13 +712,16 @@ async function runSessionStatus(opts) {
|
|
|
715
712
|
}
|
|
716
713
|
const { token, company } = await withCompany({ company: opts.company });
|
|
717
714
|
const live = await fetchCompanyLive(token, company.companyUid);
|
|
715
|
+
const unattributedEvents = collectDaemonDoctor().unattributedEvents;
|
|
718
716
|
if (opts.json) {
|
|
719
|
-
console.log(JSON.stringify({ ok: true, action: "session-status", company, live }, null, 2));
|
|
717
|
+
console.log(JSON.stringify({ ok: true, action: "session-status", company, live, unattributedEvents }, null, 2));
|
|
720
718
|
return;
|
|
721
719
|
}
|
|
722
720
|
for (const line of formatCompanyLiveTable(live, company.companySlug || company.companyUid)) {
|
|
723
721
|
console.log(line);
|
|
724
722
|
}
|
|
723
|
+
console.log(`unattributed events: ${unattributedEvents.total} ` +
|
|
724
|
+
`(spool=${unattributedEvents.spool} held=${unattributedEvents.held} dead-letter=${unattributedEvents.deadLetter})`);
|
|
725
725
|
}
|
|
726
726
|
const HARNESSES = new Set([
|
|
727
727
|
"claude-code",
|
|
@@ -1085,10 +1085,10 @@ export function registerMeshCommand(program) {
|
|
|
1085
1085
|
.action((opts) => wrap(() => runContextDefaultGet(opts))());
|
|
1086
1086
|
def
|
|
1087
1087
|
.command("set")
|
|
1088
|
-
.description("Set the device default company slug
|
|
1088
|
+
.description("Set the device default company slug after verifying membership")
|
|
1089
1089
|
.argument("<slug>", "Company slug")
|
|
1090
|
-
.option("--allow-without-migration", "
|
|
1091
|
-
.option("--company <slug|uid>", "Deprecated no-op;
|
|
1090
|
+
.option("--allow-without-migration", "Deprecated compatibility no-op; selected-company membership is always verified")
|
|
1091
|
+
.option("--company <slug|uid>", "Deprecated no-op; the selected company is verified directly")
|
|
1092
1092
|
.option("--json", "Print machine-readable JSON")
|
|
1093
1093
|
.action((slug, opts) => wrap(() => runContextDefaultSet(slug, opts))());
|
|
1094
1094
|
def
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make `hq agent …` act as THIS host's agent, even on a computer whose owner
|
|
3
|
+
* is signed in to HQ.
|
|
4
|
+
*
|
|
5
|
+
* The token layer resolves machine credentials from `HQ_MACHINE_CREDS_FILE`,
|
|
6
|
+
* falling back to the one fixed path `~/.hq-agent/machine-creds.json` — and
|
|
7
|
+
* hq-cloud's own `loadMachineCreds` reads the same variable. A bot that shares
|
|
8
|
+
* a computer with its owner lives in a sibling directory instead, so without
|
|
9
|
+
* this the CLI would find the layout of an agent and the credentials of a
|
|
10
|
+
* person: `hq agent probe` would report its owner's identity and the bot would
|
|
11
|
+
* act as them.
|
|
12
|
+
*
|
|
13
|
+
* So before any `hq agent` subcommand runs, the resolved tree is published to
|
|
14
|
+
* the environment. An explicit `HQ_MACHINE_CREDS_FILE` is never overwritten —
|
|
15
|
+
* a daemon unit that pins one stays pinned.
|
|
16
|
+
*
|
|
17
|
+
* The person's own session file is not read, moved, or changed by any of this.
|
|
18
|
+
*/
|
|
19
|
+
import type { AgentKitPaths } from "./paths.js";
|
|
20
|
+
export declare const MACHINE_CREDS_FILE_ENV = "HQ_MACHINE_CREDS_FILE";
|
|
21
|
+
export declare const MACHINE_TOKEN_STATE_DIR_ENV = "HQ_MACHINE_TOKEN_STATE_DIR";
|
|
22
|
+
/**
|
|
23
|
+
* Point the token layer at `paths` unless the caller already pinned it.
|
|
24
|
+
* Returns the variables it set, for tests and for `--verbose` output.
|
|
25
|
+
*/
|
|
26
|
+
export declare function adoptAgentIdentityEnv(paths: Pick<AgentKitPaths, "agentDir" | "machineCredsPath">, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
27
|
+
//# sourceMappingURL=adopt-identity.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make `hq agent …` act as THIS host's agent, even on a computer whose owner
|
|
3
|
+
* is signed in to HQ.
|
|
4
|
+
*
|
|
5
|
+
* The token layer resolves machine credentials from `HQ_MACHINE_CREDS_FILE`,
|
|
6
|
+
* falling back to the one fixed path `~/.hq-agent/machine-creds.json` — and
|
|
7
|
+
* hq-cloud's own `loadMachineCreds` reads the same variable. A bot that shares
|
|
8
|
+
* a computer with its owner lives in a sibling directory instead, so without
|
|
9
|
+
* this the CLI would find the layout of an agent and the credentials of a
|
|
10
|
+
* person: `hq agent probe` would report its owner's identity and the bot would
|
|
11
|
+
* act as them.
|
|
12
|
+
*
|
|
13
|
+
* So before any `hq agent` subcommand runs, the resolved tree is published to
|
|
14
|
+
* the environment. An explicit `HQ_MACHINE_CREDS_FILE` is never overwritten —
|
|
15
|
+
* a daemon unit that pins one stays pinned.
|
|
16
|
+
*
|
|
17
|
+
* The person's own session file is not read, moved, or changed by any of this.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
export const MACHINE_CREDS_FILE_ENV = "HQ_MACHINE_CREDS_FILE";
|
|
22
|
+
export const MACHINE_TOKEN_STATE_DIR_ENV = "HQ_MACHINE_TOKEN_STATE_DIR";
|
|
23
|
+
/**
|
|
24
|
+
* Point the token layer at `paths` unless the caller already pinned it.
|
|
25
|
+
* Returns the variables it set, for tests and for `--verbose` output.
|
|
26
|
+
*/
|
|
27
|
+
export function adoptAgentIdentityEnv(paths, env = process.env) {
|
|
28
|
+
const applied = {};
|
|
29
|
+
if (env[MACHINE_CREDS_FILE_ENV]?.trim())
|
|
30
|
+
return applied;
|
|
31
|
+
// Only adopt a tree that actually holds an identity: on a host that has not
|
|
32
|
+
// enrolled yet, leaving the environment alone keeps today's error messages.
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.statSync(paths.machineCredsPath).isFile())
|
|
35
|
+
return applied;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return applied;
|
|
39
|
+
}
|
|
40
|
+
applied[MACHINE_CREDS_FILE_ENV] = paths.machineCredsPath;
|
|
41
|
+
env[MACHINE_CREDS_FILE_ENV] = paths.machineCredsPath;
|
|
42
|
+
// Per-agent token cache. Two identities on one computer must never share a
|
|
43
|
+
// minted-token file; the agent's belongs inside the agent's own tree.
|
|
44
|
+
if (!env[MACHINE_TOKEN_STATE_DIR_ENV]?.trim()) {
|
|
45
|
+
const dir = path.join(paths.agentDir, "token-state");
|
|
46
|
+
applied[MACHINE_TOKEN_STATE_DIR_ENV] = dir;
|
|
47
|
+
env[MACHINE_TOKEN_STATE_DIR_ENV] = dir;
|
|
48
|
+
}
|
|
49
|
+
return applied;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=adopt-identity.js.map
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
* `HQ_AGENT_DIR` relocates the whole tree (tests, containers). The creds file
|
|
14
14
|
* additionally honours `HQ_MACHINE_CREDS_FILE`, the override hq-cloud reads,
|
|
15
15
|
* so a kit pointed at a custom creds path and hq-cloud's mint agree.
|
|
16
|
+
*
|
|
17
|
+
* LOCAL BOTS. A bot that runs on its owner's own computer cannot use the
|
|
18
|
+
* default tree: that machine already has the owner's HQ session, and an agent
|
|
19
|
+
* identity must not share a host account with a person's login. Such a bot
|
|
20
|
+
* enrolls into a SIBLING directory instead — `~/.hq-agent/<name>/` with the
|
|
21
|
+
* same layout — and every `hq agent …` command finds it here, so the bot does
|
|
22
|
+
* not have to carry environment variables around to be itself. Two or more
|
|
23
|
+
* local agents are ambiguous on purpose: pick one with `HQ_AGENT_DIR`.
|
|
16
24
|
*/
|
|
17
25
|
export declare const AGENT_DIR_ENV = "HQ_AGENT_DIR";
|
|
18
26
|
export declare const HOST_KEY_NAME = "host-key";
|
|
@@ -35,6 +43,18 @@ export interface AgentKitPaths {
|
|
|
35
43
|
skillsDir: string;
|
|
36
44
|
lastHeartbeatPath: string;
|
|
37
45
|
}
|
|
46
|
+
/** Default name for a bot enrolled alongside its owner's own session. */
|
|
47
|
+
export declare const DEFAULT_LOCAL_AGENT_NAME = "local";
|
|
48
|
+
/** `~/.hq-agent` — the root, whether or not it holds an identity itself. */
|
|
49
|
+
export declare function agentRootDir(home?: string): string;
|
|
50
|
+
/** `~/.hq-agent/<name>` — an isolated home for one local bot. */
|
|
51
|
+
export declare function localAgentDir(name: string, home?: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Local agent homes under `~/.hq-agent`, by name, oldest name order. A
|
|
54
|
+
* directory counts only once it holds a creds file, so a half-written tree is
|
|
55
|
+
* never mistaken for an identity.
|
|
56
|
+
*/
|
|
57
|
+
export declare function listLocalAgentDirs(home?: string): string[];
|
|
38
58
|
export declare function agentDir(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
39
59
|
export declare function agentKitPaths(home?: string, env?: NodeJS.ProcessEnv): AgentKitPaths;
|
|
40
60
|
export declare function componentStatePath(paths: Pick<AgentKitPaths, "stateDir">, component: KitComponent): string;
|