@indigoai-us/hq-cli 5.188.0 → 5.189.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.
@@ -0,0 +1,21 @@
1
+ import { type LaneRecord, type LaneState, type SeniorRef } from "./types.js";
2
+ declare const HIRE_GRANT_CONTEXT_SCHEMA: "hq-lane-hire-grant-context.v1";
3
+ export interface HireGrantLaneSnapshot {
4
+ lane_id: string;
5
+ state: LaneState;
6
+ senior: SeniorRef;
7
+ hire_grant?: number;
8
+ }
9
+ export interface OutpostHireGrantContext {
10
+ schema_version: typeof HIRE_GRANT_CONTEXT_SCHEMA;
11
+ enforcement_enabled: boolean;
12
+ expected_senior: SeniorRef;
13
+ hierarchy: HireGrantLaneSnapshot[];
14
+ }
15
+ /** Encode only the owner-relevant hierarchy from the primary's complete store. */
16
+ export declare function createOutpostHireGrantContext(target: LaneRecord, lanes: readonly LaneRecord[], enforcementEnabled: boolean): string;
17
+ /** Decode and validate an owner grant context, binding it to the target lane. */
18
+ export declare function parseOutpostHireGrantContext(encoded: string, targetLaneId: string): OutpostHireGrantContext;
19
+ export declare function sameHireGrantSenior(a: SeniorRef, b: SeniorRef): boolean;
20
+ export {};
21
+ //# sourceMappingURL=hire-grant-dispatch.d.ts.map
@@ -0,0 +1,167 @@
1
+ import { aliveDirectChildren } from "./hire-grants.js";
2
+ import { LanesError } from "./errors.js";
3
+ import { isLaneState, isSeniorKind, } from "./types.js";
4
+ const HIRE_GRANT_CONTEXT_SCHEMA = "hq-lane-hire-grant-context.v1";
5
+ const MAX_CONTEXT_BYTES = 48 * 1024;
6
+ const MAX_CONTEXT_LANES = 256;
7
+ function contextError(message) {
8
+ return new LanesError("hire_grant_context_invalid", message, {});
9
+ }
10
+ function sameSenior(a, b) {
11
+ return a.kind === b.kind && a.id === b.id;
12
+ }
13
+ function copySenior(senior) {
14
+ return senior.id === undefined
15
+ ? { kind: senior.kind }
16
+ : { kind: senior.kind, id: senior.id };
17
+ }
18
+ function snapshot(lane) {
19
+ return {
20
+ lane_id: lane.lane_id,
21
+ state: lane.state,
22
+ senior: copySenior(lane.senior),
23
+ ...(lane.hire_grant === undefined ? {} : { hire_grant: lane.hire_grant }),
24
+ };
25
+ }
26
+ /** Encode only the owner-relevant hierarchy from the primary's complete store. */
27
+ export function createOutpostHireGrantContext(target, lanes, enforcementEnabled) {
28
+ const current = lanes.find((lane) => lane.lane_id === target.lane_id);
29
+ if (!current || !sameSenior(current.senior, target.senior)) {
30
+ throw new LanesError("hire_grant_hierarchy_unavailable", `Cannot build an authoritative hire-grant hierarchy for lane ${target.lane_id}`, { lane_id: target.lane_id });
31
+ }
32
+ const relevant = new Map();
33
+ relevant.set(current.lane_id, current);
34
+ if (enforcementEnabled) {
35
+ if (current.senior.kind === "lane") {
36
+ const parentId = current.senior.id;
37
+ const parent = parentId ? lanes.find((lane) => lane.lane_id === parentId) : undefined;
38
+ if (!parent) {
39
+ throw new LanesError("hire_grant_senior_invalid", `Lane ${target.lane_id} has no senior lane in the primary hierarchy`, { lane_id: target.lane_id, senior_lane_id: parentId ?? null });
40
+ }
41
+ relevant.set(parent.lane_id, parent);
42
+ for (const sibling of aliveDirectChildren(lanes, parent.lane_id)) {
43
+ relevant.set(sibling.lane_id, sibling);
44
+ }
45
+ }
46
+ for (const child of aliveDirectChildren(lanes, current.lane_id)) {
47
+ relevant.set(child.lane_id, child);
48
+ }
49
+ }
50
+ const hierarchy = [...relevant.values()]
51
+ .map(snapshot)
52
+ .sort((a, b) => a.lane_id.localeCompare(b.lane_id));
53
+ if (hierarchy.length > MAX_CONTEXT_LANES) {
54
+ throw new LanesError("hire_grant_hierarchy_too_large", `Lane ${target.lane_id} has too many related lanes to dispatch a bounded grant check`, { lane_id: target.lane_id, related_lane_count: hierarchy.length });
55
+ }
56
+ const context = {
57
+ schema_version: HIRE_GRANT_CONTEXT_SCHEMA,
58
+ enforcement_enabled: enforcementEnabled,
59
+ expected_senior: copySenior(current.senior),
60
+ hierarchy,
61
+ };
62
+ const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64url");
63
+ if (Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES) {
64
+ throw new LanesError("hire_grant_hierarchy_too_large", `Lane ${target.lane_id} has too much related state to dispatch a bounded grant check`, { lane_id: target.lane_id, encoded_bytes: Buffer.byteLength(encoded, "utf8") });
65
+ }
66
+ return encoded;
67
+ }
68
+ function parseSenior(value) {
69
+ if (value === null || typeof value !== "object" || Array.isArray(value))
70
+ return null;
71
+ const record = value;
72
+ if (Object.keys(record).some((key) => key !== "kind" && key !== "id"))
73
+ return null;
74
+ if (!isSeniorKind(record.kind))
75
+ return null;
76
+ if (record.id !== undefined &&
77
+ (typeof record.id !== "string" || !record.id.trim() || record.id.trim() !== record.id))
78
+ return null;
79
+ if ((record.kind === "session" || record.kind === "lane") && record.id === undefined)
80
+ return null;
81
+ if (record.kind === "user" && record.id !== undefined)
82
+ return null;
83
+ return record.id === undefined
84
+ ? { kind: record.kind }
85
+ : { kind: record.kind, id: record.id };
86
+ }
87
+ function parseLaneSnapshot(value) {
88
+ if (value === null || typeof value !== "object" || Array.isArray(value))
89
+ return null;
90
+ const record = value;
91
+ if (Object.keys(record).some((key) => !["lane_id", "state", "senior", "hire_grant"].includes(key)))
92
+ return null;
93
+ if (typeof record.lane_id !== "string" || !record.lane_id.trim())
94
+ return null;
95
+ if (!isLaneState(record.state))
96
+ return null;
97
+ const senior = parseSenior(record.senior);
98
+ if (!senior)
99
+ return null;
100
+ if (record.hire_grant !== undefined &&
101
+ (typeof record.hire_grant !== "number" || !Number.isSafeInteger(record.hire_grant) || record.hire_grant < 0)) {
102
+ return null;
103
+ }
104
+ return {
105
+ lane_id: record.lane_id,
106
+ state: record.state,
107
+ senior,
108
+ ...(record.hire_grant === undefined ? {} : { hire_grant: record.hire_grant }),
109
+ };
110
+ }
111
+ /** Decode and validate an owner grant context, binding it to the target lane. */
112
+ export function parseOutpostHireGrantContext(encoded, targetLaneId) {
113
+ if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded) ||
114
+ Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES ||
115
+ Buffer.from(encoded, "base64url").toString("base64url") !== encoded) {
116
+ throw contextError("Outpost grant context is not canonical base64url");
117
+ }
118
+ let raw;
119
+ try {
120
+ const decoded = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(encoded, "base64url"));
121
+ raw = JSON.parse(decoded);
122
+ }
123
+ catch {
124
+ throw contextError("Outpost grant context is not valid UTF-8 JSON");
125
+ }
126
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
127
+ throw contextError("Outpost grant context must be an object");
128
+ }
129
+ const record = raw;
130
+ if (Object.keys(record).some((key) => !["schema_version", "enforcement_enabled", "expected_senior", "hierarchy"].includes(key))) {
131
+ throw contextError("Outpost grant context contains unknown fields");
132
+ }
133
+ if (record.schema_version !== HIRE_GRANT_CONTEXT_SCHEMA ||
134
+ typeof record.enforcement_enabled !== "boolean" ||
135
+ !Array.isArray(record.hierarchy) ||
136
+ record.hierarchy.length === 0 || record.hierarchy.length > MAX_CONTEXT_LANES) {
137
+ throw contextError("Outpost grant context has an unsupported or incomplete shape");
138
+ }
139
+ const expectedSenior = parseSenior(record.expected_senior);
140
+ const hierarchy = record.hierarchy.map(parseLaneSnapshot);
141
+ if (!expectedSenior || hierarchy.some((lane) => lane === null)) {
142
+ throw contextError("Outpost grant context contains an invalid senior or lane snapshot");
143
+ }
144
+ const snapshots = hierarchy;
145
+ const byId = new Map(snapshots.map((lane) => [lane.lane_id, lane]));
146
+ if (byId.size !== snapshots.length) {
147
+ throw contextError("Outpost grant context contains duplicate lane snapshots");
148
+ }
149
+ const target = byId.get(targetLaneId);
150
+ if (!target || !sameSenior(target.senior, expectedSenior)) {
151
+ throw contextError("Outpost grant context does not match the target's expected senior");
152
+ }
153
+ if (record.enforcement_enabled && expectedSenior.kind === "lane" &&
154
+ (!expectedSenior.id || !byId.has(expectedSenior.id))) {
155
+ throw contextError("Outpost grant context is missing the target's senior lane");
156
+ }
157
+ return {
158
+ schema_version: HIRE_GRANT_CONTEXT_SCHEMA,
159
+ enforcement_enabled: record.enforcement_enabled,
160
+ expected_senior: expectedSenior,
161
+ hierarchy: snapshots,
162
+ };
163
+ }
164
+ export function sameHireGrantSenior(a, b) {
165
+ return sameSenior(a, b);
166
+ }
167
+ //# sourceMappingURL=hire-grant-dispatch.js.map
@@ -1,7 +1,8 @@
1
1
  import { type LaneRecord } from "./types.js";
2
2
  export declare const LANE_HIRE_GRANTS_FLAG = "lanes.hire-grants";
3
- export declare function aliveDirectChildren(lanes: readonly LaneRecord[], parentLaneId: string): LaneRecord[];
4
- export declare function assignedChildHireGrants(children: readonly LaneRecord[], exceptLaneId?: string): number;
3
+ export type LaneHireGrantRow = Pick<LaneRecord, "lane_id" | "state" | "senior" | "hire_grant">;
4
+ export declare function aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string): T[];
5
+ export declare function assignedChildHireGrants(children: readonly LaneHireGrantRow[], exceptLaneId?: string): number;
5
6
  export interface HireGrantAdmissionSnapshot {
6
7
  parent: LaneRecord;
7
8
  children: LaneRecord[];
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
27
27
  stderr: string;
28
28
  }
29
29
  export interface OutpostLaneCommand {
30
- action: "message" | "resume" | "stop" | "show" | "grant";
30
+ action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target";
31
31
  laneId: string;
32
32
  hqBinary?: string;
33
33
  text?: string;
@@ -35,7 +35,10 @@ export interface OutpostLaneCommand {
35
35
  story?: string;
36
36
  account?: string;
37
37
  model?: string;
38
+ effort?: string;
39
+ worker?: string;
38
40
  hire?: string;
41
+ grantContext?: string;
39
42
  compact?: boolean;
40
43
  }
41
44
  export interface OutpostExecPort {
@@ -80,6 +83,8 @@ export declare function buildOutpostBalanceMoveCommand(input: {
80
83
  export declare function buildOutpostIdentityBackfillCommand(expectedId: string): string;
81
84
  /** Build the same `hq outposts exec` remote command used by lane pickup. */
82
85
  export declare function buildOutpostLaneCommand(input: OutpostLaneCommand): string;
86
+ /** True when a remote CLI exited after rejecting the forwarded effort flag. */
87
+ export declare function outpostRejectedEffortOption(result: Pick<OutpostDispatchResult, "exitCode" | "stdout" | "stderr">): boolean;
83
88
  export declare function stageOutpostPayload(port: Pick<OutpostExecPort, "stageExecInput">, outpostId: string, bytes: Uint8Array, put?: typeof fetch): Promise<{
84
89
  getUrl: string;
85
90
  }>;
@@ -104,7 +104,10 @@ export function buildOutpostLaneCommand(input) {
104
104
  ["--story", input.story],
105
105
  ["--account", input.account],
106
106
  ["--model", input.model],
107
+ ["--effort", input.effort],
108
+ ["--worker", input.worker],
107
109
  ["--hire", input.hire],
110
+ ["--grant-context", input.grantContext],
108
111
  ]) {
109
112
  if (value !== undefined)
110
113
  args.push(flag, value);
@@ -113,6 +116,12 @@ export function buildOutpostLaneCommand(input) {
113
116
  args.push("--no-compact");
114
117
  return withRemoteHqDir(joinCommandParts(args));
115
118
  }
119
+ /** True when a remote CLI exited after rejecting the forwarded effort flag. */
120
+ export function outpostRejectedEffortOption(result) {
121
+ if (result.exitCode === 0)
122
+ return false;
123
+ return /(?:unknown|unrecognized) option[^\n]{0,120}--effort/i.test(`${result.stderr}\n${result.stdout}`);
124
+ }
116
125
  export async function stageOutpostPayload(port, outpostId, bytes, put = fetch) {
117
126
  const staged = await port.stageExecInput(outpostId);
118
127
  const upload = await put(staged.putUrl, {
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { PROVIDER_HOME_ENV } from "./accounts.js";
11
11
  import { type RunCompactionFn } from "./compaction.js";
12
+ import { type ResolvedEffort } from "./effort.js";
12
13
  import { type KnownModelsDeps } from "./models.js";
13
14
  import { type LaunchTrigger } from "./delivery.js";
14
15
  import { type MachineIdentity } from "./machine-identity.js";
@@ -283,6 +284,8 @@ export interface SpawnLaunchOptions {
283
284
  * (`hq lanes message --no-compact`). Default true.
284
285
  */
285
286
  compact?: boolean;
287
+ /** Effort resolved by an explicit resume/message request, or pinned from its record. */
288
+ effort?: ResolvedEffort;
286
289
  }
287
290
  /**
288
291
  * Bind the account, spawn the worker and its watcher outside the caller's
@@ -15,7 +15,7 @@ import { PROVIDER_HOME_ENV, accountStatus, isLoggedOut, resolveAccount, selectBa
15
15
  import { COMPACTION_PROMPT, compactionAfterUsage, compactionDue, compactLaneSession, } from "./compaction.js";
16
16
  import { installDrainHook } from "./hooks-install.js";
17
17
  import { loadLanesConfig, providerAutoCompactTokens, providerCompactAfterMs, } from "./config.js";
18
- import { appendEffortFlag, resolveLaneEffort } from "./effort.js";
18
+ import { appendEffortFlag, resolveLaneEffort, } from "./effort.js";
19
19
  import { defaultProbe, validateLaneModel, validateProfileLaneModel, } from "./models.js";
20
20
  import { claimableStatesFor, planLaneLaunch, providerSupportsSessionResume, } from "./delivery.js";
21
21
  import { AccountResolutionError, LanesError } from "./errors.js";
@@ -1719,7 +1719,13 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1719
1719
  accountModel: accountCfg?.model,
1720
1720
  providerModel: config.providers[lane.provider].model,
1721
1721
  });
1722
- const effort = resolveLaneEffort({
1722
+ const pendingResumeEffort = pendingResume !== undefined
1723
+ ? {
1724
+ ...(existing.effort !== undefined ? { effort: existing.effort } : {}),
1725
+ source: existing.effort_source ?? "none",
1726
+ }
1727
+ : undefined;
1728
+ const effort = launch.effort ?? pendingResumeEffort ?? resolveLaneEffort({
1723
1729
  cliEffort: existing.effort_source === "cli" ? existing.effort : undefined,
1724
1730
  profileEffort,
1725
1731
  accountEffort: accountCfg?.effort,
@@ -0,0 +1,12 @@
1
+ export interface SsmRunCommandContextDeps {
2
+ platform?: string;
3
+ parentPid?: number;
4
+ readFileSync?: (path: string, encoding: "utf8") => string;
5
+ }
6
+ /**
7
+ * True only for a Linux process descended from the root-owned SSM Run Command
8
+ * worker chain. Outpost owner-only lane mutations use this to distinguish an
9
+ * SSM dispatch from a normal local CLI invocation.
10
+ */
11
+ export declare function isSsmRunCommandProcess(deps?: SsmRunCommandContextDeps): boolean;
12
+ //# sourceMappingURL=ssm-command-context.d.ts.map
@@ -0,0 +1,52 @@
1
+ import * as fs from "node:fs";
2
+ const SSM_WORKER_CHAIN = [
3
+ "ssm-document-wo",
4
+ "ssm-agent-worke",
5
+ "amazon-ssm-agen",
6
+ ];
7
+ /**
8
+ * True only for a Linux process descended from the root-owned SSM Run Command
9
+ * worker chain. Outpost owner-only lane mutations use this to distinguish an
10
+ * SSM dispatch from a normal local CLI invocation.
11
+ */
12
+ export function isSsmRunCommandProcess(deps = {}) {
13
+ if ((deps.platform ?? process.platform) !== "linux")
14
+ return false;
15
+ const readFileSync = deps.readFileSync ?? ((filePath, encoding) => fs.readFileSync(filePath, encoding));
16
+ let pid = deps.parentPid ?? process.ppid;
17
+ const lineage = [];
18
+ for (let depth = 0; depth < 32 && pid > 1; depth += 1) {
19
+ let status;
20
+ let command;
21
+ try {
22
+ status = readFileSync(`/proc/${pid}/status`, "utf8");
23
+ command = readFileSync(`/proc/${pid}/comm`, "utf8").trim();
24
+ }
25
+ catch (err) {
26
+ const code = err !== null && typeof err === "object" && "code" in err
27
+ ? err.code
28
+ : undefined;
29
+ if (code === "ENOENT" || code === "EACCES" || code === "EPERM")
30
+ return false;
31
+ throw err;
32
+ }
33
+ const parentMatch = /^PPid:\s*(\d+)\s*$/m.exec(status);
34
+ const uidMatch = /^Uid:\s*\d+\s+(\d+)\s+\d+\s+\d+\s*$/m.exec(status);
35
+ if (!parentMatch || !uidMatch || !command)
36
+ return false;
37
+ const parentPid = Number(parentMatch[1]);
38
+ const effectiveUid = Number(uidMatch[1]);
39
+ if (!Number.isSafeInteger(parentPid) || !Number.isSafeInteger(effectiveUid))
40
+ return false;
41
+ lineage.push({ pid, parentPid, command, effectiveUid });
42
+ pid = parentPid;
43
+ }
44
+ for (let start = 0; start <= lineage.length - SSM_WORKER_CHAIN.length; start += 1) {
45
+ const chain = lineage.slice(start, start + SSM_WORKER_CHAIN.length);
46
+ if (chain.every((entry, index) => entry.effectiveUid === 0 && entry.command.startsWith(SSM_WORKER_CHAIN[index]))) {
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+ //# sourceMappingURL=ssm-command-context.js.map
@@ -226,7 +226,7 @@ export const LANE_FIELD_WRITERS = {
226
226
  priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
227
227
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
228
228
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
229
- last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"], fast_mode: ["spawn"],
229
+ last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
230
230
  last_message_at: ["command"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
231
231
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
232
232
  admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.188.0",
3
+ "version": "5.189.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {