@indigoai-us/hq-cli 5.188.0 → 5.188.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 +10 -0
- package/dist/command-catalog.generated.d.ts +22 -0
- package/dist/command-catalog.generated.js +29 -0
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +261 -35
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +21 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +167 -0
- package/dist/lib/lanes/hire-grants.d.ts +3 -2
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/ssm-command-context.d.ts +12 -0
- package/dist/lib/lanes/ssm-command-context.js +52 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.188.1] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq lanes grant` now applies grants for Outpost-hosted lanes on their owner.
|
|
10
|
+
The primary sends a bounded hierarchy snapshot and its evaluated hq-flags
|
|
11
|
+
decision; the owner revalidates the authorized senior, applies carve,
|
|
12
|
+
occupancy, and terminal-parent checks, then syncs the changed record to the
|
|
13
|
+
primary. Dispatched lane errors include sanitized owner details.
|
|
14
|
+
|
|
5
15
|
## [5.188.0] — 2026-09-25
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -6915,6 +6915,28 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
6915
6915
|
readonly description: "JSON on stdout (always)";
|
|
6916
6916
|
}];
|
|
6917
6917
|
readonly subcommands: readonly [];
|
|
6918
|
+
}, {
|
|
6919
|
+
readonly name: "grant-target";
|
|
6920
|
+
readonly description: "";
|
|
6921
|
+
readonly aliases: readonly [];
|
|
6922
|
+
readonly hidden: true;
|
|
6923
|
+
readonly usage: "[options] <lane-id>";
|
|
6924
|
+
readonly arguments: readonly [{
|
|
6925
|
+
readonly name: "lane-id";
|
|
6926
|
+
readonly required: true;
|
|
6927
|
+
readonly variadic: false;
|
|
6928
|
+
}];
|
|
6929
|
+
readonly options: readonly [{
|
|
6930
|
+
readonly flags: "--hire <n>";
|
|
6931
|
+
readonly description: "Non-negative safe integer hire grant";
|
|
6932
|
+
}, {
|
|
6933
|
+
readonly flags: "--grant-context <base64url>";
|
|
6934
|
+
readonly description: "Primary-authorized hire hierarchy and gate decision";
|
|
6935
|
+
}, {
|
|
6936
|
+
readonly flags: "--json";
|
|
6937
|
+
readonly description: "JSON on stdout (always)";
|
|
6938
|
+
}];
|
|
6939
|
+
readonly subcommands: readonly [];
|
|
6918
6940
|
}, {
|
|
6919
6941
|
readonly name: "grant";
|
|
6920
6942
|
readonly description: "Set a lane's alive child-lane grant as its senior";
|
|
@@ -8937,6 +8937,35 @@ export const COMMAND_CATALOG = [
|
|
|
8937
8937
|
],
|
|
8938
8938
|
"subcommands": []
|
|
8939
8939
|
},
|
|
8940
|
+
{
|
|
8941
|
+
"name": "grant-target",
|
|
8942
|
+
"description": "",
|
|
8943
|
+
"aliases": [],
|
|
8944
|
+
"hidden": true,
|
|
8945
|
+
"usage": "[options] <lane-id>",
|
|
8946
|
+
"arguments": [
|
|
8947
|
+
{
|
|
8948
|
+
"name": "lane-id",
|
|
8949
|
+
"required": true,
|
|
8950
|
+
"variadic": false
|
|
8951
|
+
}
|
|
8952
|
+
],
|
|
8953
|
+
"options": [
|
|
8954
|
+
{
|
|
8955
|
+
"flags": "--hire <n>",
|
|
8956
|
+
"description": "Non-negative safe integer hire grant"
|
|
8957
|
+
},
|
|
8958
|
+
{
|
|
8959
|
+
"flags": "--grant-context <base64url>",
|
|
8960
|
+
"description": "Primary-authorized hire hierarchy and gate decision"
|
|
8961
|
+
},
|
|
8962
|
+
{
|
|
8963
|
+
"flags": "--json",
|
|
8964
|
+
"description": "JSON on stdout (always)"
|
|
8965
|
+
}
|
|
8966
|
+
],
|
|
8967
|
+
"subcommands": []
|
|
8968
|
+
},
|
|
8940
8969
|
{
|
|
8941
8970
|
"name": "grant",
|
|
8942
8971
|
"description": "Set a lane's alive child-lane grant as its senior",
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
|
12
12
|
export { LANE_HIRE_GRANTS_FLAG };
|
|
13
13
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
14
14
|
import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.js";
|
|
15
|
-
import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
15
|
+
import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
16
16
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
17
17
|
import { type RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
18
18
|
import { type KnownModelsDeps, type ProbeCommand } from "../lib/lanes/models.js";
|
|
@@ -80,6 +80,10 @@ export interface LanesCommandDeps {
|
|
|
80
80
|
lanePlacementEnabled?: () => boolean;
|
|
81
81
|
/** Test seam for the default-off lane hire-grants rollout gate. */
|
|
82
82
|
laneHireGrantsEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
83
|
+
/** Test seam for the SSM-only Outpost owner grant path. */
|
|
84
|
+
isSsmOwnerDispatch?: () => boolean;
|
|
85
|
+
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
|
86
|
+
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
83
87
|
/** Test seam for the default-off Claude account-balancing gate. */
|
|
84
88
|
laneClaudeAccountBalanceEnabled?: () => boolean;
|
|
85
89
|
}
|
package/dist/commands/lanes.js
CHANGED
|
@@ -13,8 +13,10 @@ import * as path from "node:path";
|
|
|
13
13
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
14
14
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
15
15
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
16
|
+
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
16
17
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
17
18
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
19
|
+
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
18
20
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
19
21
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
20
22
|
export { LANE_HIRE_GRANTS_FLAG };
|
|
@@ -81,7 +83,7 @@ function failJson(err) {
|
|
|
81
83
|
if (err.error === "hire_grant_exhausted") {
|
|
82
84
|
process.stderr.write(`hq lanes: ${err.message}\n`);
|
|
83
85
|
}
|
|
84
|
-
printJson(err.toJSON());
|
|
86
|
+
printJson({ ...err.toJSON(), message: err.message });
|
|
85
87
|
process.exit(1);
|
|
86
88
|
}
|
|
87
89
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -255,6 +257,8 @@ function resolveDeps(deps = {}) {
|
|
|
255
257
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
256
258
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
257
259
|
laneHireGrantsEnabled: deps.laneHireGrantsEnabled ?? defaultLaneHireGrantsEnabled,
|
|
260
|
+
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
261
|
+
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
258
262
|
laneClaudeAccountBalanceEnabled: deps.laneClaudeAccountBalanceEnabled ?? defaultLaneClaudeAccountBalanceEnabled,
|
|
259
263
|
};
|
|
260
264
|
}
|
|
@@ -451,6 +455,81 @@ function laneHireFields(lane, lanes) {
|
|
|
451
455
|
hire_summary: hireSummary,
|
|
452
456
|
};
|
|
453
457
|
}
|
|
458
|
+
function outpostCliOverride(ctx, outpostId) {
|
|
459
|
+
const envKey = `HQ_LANES_OUTPOST_CLI_${outpostId.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
|
|
460
|
+
const configured = ctx.env[envKey];
|
|
461
|
+
const hqBinary = configured === undefined ? undefined : configured.trim();
|
|
462
|
+
if (configured !== undefined &&
|
|
463
|
+
(!hqBinary ||
|
|
464
|
+
!path.posix.isAbsolute(hqBinary) ||
|
|
465
|
+
hqBinary.includes("\0") ||
|
|
466
|
+
hqBinary.includes("\r") ||
|
|
467
|
+
hqBinary.includes("\n"))) {
|
|
468
|
+
throw new LanesError("outpost_lane_cli_invalid", `${envKey} must be an absolute remote executable path`, { outpost_id: outpostId });
|
|
469
|
+
}
|
|
470
|
+
return hqBinary;
|
|
471
|
+
}
|
|
472
|
+
function safeOwnerErrorValue(value, key = "") {
|
|
473
|
+
if (/(?:env|environment|token|secret|password|credential|authorization|api[_-]?key|access[_-]?key|private[_-]?key)/i.test(key)) {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
if (typeof value === "string") {
|
|
477
|
+
return value
|
|
478
|
+
.replace(/\b(Bearer|Basic)\s+[^\s,;]+/gi, "$1 [redacted]")
|
|
479
|
+
.replace(/\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[A-Z0-9]{16})\b/g, "[redacted]")
|
|
480
|
+
.replace(/\b["']?([A-Z][A-Z0-9_]{1,})["']?\s*[:=]\s*["']?([^\s"',;}]+)["']?/g, "$1=[redacted]");
|
|
481
|
+
}
|
|
482
|
+
if (Array.isArray(value)) {
|
|
483
|
+
return value.map((item) => safeOwnerErrorValue(item));
|
|
484
|
+
}
|
|
485
|
+
if (value === null || typeof value !== "object")
|
|
486
|
+
return value;
|
|
487
|
+
const safe = {};
|
|
488
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
489
|
+
const nested = safeOwnerErrorValue(childValue, childKey);
|
|
490
|
+
if (nested !== undefined)
|
|
491
|
+
safe[childKey] = nested;
|
|
492
|
+
}
|
|
493
|
+
return safe;
|
|
494
|
+
}
|
|
495
|
+
function ownerErrorFromOutput(stdout) {
|
|
496
|
+
const trimmed = stdout.trim();
|
|
497
|
+
if (!trimmed)
|
|
498
|
+
return undefined;
|
|
499
|
+
let parsed;
|
|
500
|
+
try {
|
|
501
|
+
parsed = JSON.parse(trimmed);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
process.stderr.write("hq lanes: Outpost lanes dispatch returned malformed JSON\n");
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
const owner = parsed;
|
|
511
|
+
if (typeof owner.error !== "string" || !/^[a-z][a-z0-9_]{0,99}$/.test(owner.error)) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
if (safeOwnerErrorValue(owner.error) !== owner.error)
|
|
515
|
+
return undefined;
|
|
516
|
+
if (owner.error === "already_terminal" || owner.error === "outpost_self_target") {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
const rawDetails = {};
|
|
520
|
+
for (const [key, value] of Object.entries(owner)) {
|
|
521
|
+
if (key !== "error" && key !== "message" && key !== "details")
|
|
522
|
+
rawDetails[key] = value;
|
|
523
|
+
}
|
|
524
|
+
if (owner.details !== null && typeof owner.details === "object" && !Array.isArray(owner.details)) {
|
|
525
|
+
Object.assign(rawDetails, owner.details);
|
|
526
|
+
}
|
|
527
|
+
const details = safeOwnerErrorValue(rawDetails);
|
|
528
|
+
const message = typeof owner.message === "string" && owner.message.trim()
|
|
529
|
+
? String(safeOwnerErrorValue(owner.message))
|
|
530
|
+
: `Outpost owner returned ${owner.error}`;
|
|
531
|
+
return { error: owner.error, message, details };
|
|
532
|
+
}
|
|
454
533
|
async function ownerLaneHireFields(ctx, lane) {
|
|
455
534
|
if (lane.host.kind !== "outpost") {
|
|
456
535
|
throw new LanesError("outpost_lane_hire_summary_invalid", "Owner summary requires an Outpost-hosted lane", {
|
|
@@ -458,17 +537,7 @@ async function ownerLaneHireFields(ctx, lane) {
|
|
|
458
537
|
});
|
|
459
538
|
}
|
|
460
539
|
const outpostId = lane.host.id;
|
|
461
|
-
const
|
|
462
|
-
const configuredHqBinary = ctx.env[outpostCliEnvKey];
|
|
463
|
-
const hqBinary = configuredHqBinary === undefined ? undefined : configuredHqBinary.trim();
|
|
464
|
-
if (configuredHqBinary !== undefined &&
|
|
465
|
-
(!hqBinary ||
|
|
466
|
-
!path.posix.isAbsolute(hqBinary) ||
|
|
467
|
-
hqBinary.includes("\0") ||
|
|
468
|
-
hqBinary.includes("\r") ||
|
|
469
|
-
hqBinary.includes("\n"))) {
|
|
470
|
-
throw new LanesError("outpost_lane_cli_invalid", `${outpostCliEnvKey} must be an absolute remote executable path`, { outpost_id: outpostId });
|
|
471
|
-
}
|
|
540
|
+
const hqBinary = outpostCliOverride(ctx, outpostId);
|
|
472
541
|
let dispatched;
|
|
473
542
|
try {
|
|
474
543
|
dispatched = await ctx.dispatchOutpostExec({
|
|
@@ -2783,7 +2852,8 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
2783
2852
|
const staged = await ctx.stageOutpostPayload(outpostId, bytes);
|
|
2784
2853
|
const command = buildOutpostPickupCommand(staged.getUrl);
|
|
2785
2854
|
const dispatched = await ctx.dispatchOutpostExec({ outpostId, command });
|
|
2786
|
-
|
|
2855
|
+
const dispatchedOwnerError = ownerErrorFromOutput(dispatched.stdout);
|
|
2856
|
+
if (preserveQueuedOnFailure && dispatched.done !== true && !dispatchedOwnerError) {
|
|
2787
2857
|
throw new LanesError("outpost_dispatch_failed", "Outpost pickup exec has not finished; its outcome is unknown", {
|
|
2788
2858
|
outpost_id: outpostId,
|
|
2789
2859
|
lane_id: lane.lane_id,
|
|
@@ -2813,6 +2883,40 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
2813
2883
|
await emitLaneTerminalStateSoft(ctx, cancelled);
|
|
2814
2884
|
return cancelled;
|
|
2815
2885
|
}
|
|
2886
|
+
const ownerError = dispatchedOwnerError;
|
|
2887
|
+
if (ownerError) {
|
|
2888
|
+
if (preserveQueuedOnFailure) {
|
|
2889
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected lane pickup: ${ownerError.message}`, {
|
|
2890
|
+
outpost_id: outpostId,
|
|
2891
|
+
lane_id: lane.lane_id,
|
|
2892
|
+
command_id: dispatched.commandId,
|
|
2893
|
+
status: dispatched.status,
|
|
2894
|
+
done: dispatched.done ?? false,
|
|
2895
|
+
exit_code: dispatched.exitCode,
|
|
2896
|
+
...(remote?.pickup_persisted === false ||
|
|
2897
|
+
ownerError.details.pickup_persisted === false
|
|
2898
|
+
? { pickup_persisted: false }
|
|
2899
|
+
: {}),
|
|
2900
|
+
owner_error: ownerError,
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
const failed = markDispatchFailure(hqRoot, lane.lane_id, {
|
|
2904
|
+
outpost_id: outpostId,
|
|
2905
|
+
command_id: dispatched.commandId,
|
|
2906
|
+
exit_code: dispatched.exitCode,
|
|
2907
|
+
status: dispatched.status,
|
|
2908
|
+
owner_error: ownerError,
|
|
2909
|
+
});
|
|
2910
|
+
await emitLaneTerminalStateSoft(ctx, failed);
|
|
2911
|
+
throw new LanesError(DISPATCH_TERMINAL_REASON, `Outpost ${outpostId} rejected lane pickup: ${ownerError.message}`, {
|
|
2912
|
+
outpost_id: outpostId,
|
|
2913
|
+
lane_id: failed.lane_id,
|
|
2914
|
+
state: failed.state,
|
|
2915
|
+
terminal_reason: DISPATCH_TERMINAL_REASON,
|
|
2916
|
+
command_id: dispatched.commandId,
|
|
2917
|
+
owner_error: ownerError,
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2816
2920
|
if (preserveQueuedOnFailure && remote?.pickup_persisted === false) {
|
|
2817
2921
|
throw new LanesError("outpost_dispatch_failed", "Outpost pickup finished and reported that it did not persist the lane", {
|
|
2818
2922
|
outpost_id: outpostId,
|
|
@@ -2980,19 +3084,24 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
2980
3084
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
|
|
2981
3085
|
}
|
|
2982
3086
|
const outpostId = lane.host.id;
|
|
3087
|
+
const grantContext = action === "grant"
|
|
3088
|
+
? createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()), await ctx.laneHireGrantsEnabled(lane.company))
|
|
3089
|
+
: undefined;
|
|
2983
3090
|
let dispatched;
|
|
2984
3091
|
try {
|
|
2985
3092
|
dispatched = await ctx.dispatchOutpostExec({
|
|
2986
3093
|
outpostId,
|
|
2987
3094
|
command: buildOutpostLaneCommand({
|
|
2988
|
-
action,
|
|
3095
|
+
action: action === "grant" ? "grant-target" : action,
|
|
2989
3096
|
laneId: lane.lane_id,
|
|
3097
|
+
hqBinary: action === "grant" ? outpostCliOverride(ctx, outpostId) : undefined,
|
|
2990
3098
|
text: options.text,
|
|
2991
3099
|
project: options.project,
|
|
2992
3100
|
story: options.story,
|
|
2993
3101
|
account: options.account,
|
|
2994
3102
|
model: options.model,
|
|
2995
3103
|
hire: options.hire,
|
|
3104
|
+
grantContext,
|
|
2996
3105
|
compact: options.compact,
|
|
2997
3106
|
}),
|
|
2998
3107
|
});
|
|
@@ -3000,6 +3109,18 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3000
3109
|
catch (err) {
|
|
3001
3110
|
throw new LanesError("outpost_dispatch_failed", `Could not dispatch ${action} for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
3002
3111
|
}
|
|
3112
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
3113
|
+
if (ownerError) {
|
|
3114
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the ${action} command: ${ownerError.message}`, {
|
|
3115
|
+
lane_id: lane.lane_id,
|
|
3116
|
+
outpost_id: outpostId,
|
|
3117
|
+
command_id: dispatched.commandId,
|
|
3118
|
+
status: dispatched.status,
|
|
3119
|
+
done: dispatched.done ?? false,
|
|
3120
|
+
exit_code: dispatched.exitCode,
|
|
3121
|
+
owner_error: ownerError,
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3003
3124
|
if (dispatched.done !== true ||
|
|
3004
3125
|
dispatched.status !== "Success" ||
|
|
3005
3126
|
dispatched.exitCode !== 0) {
|
|
@@ -3014,13 +3135,11 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3014
3135
|
}
|
|
3015
3136
|
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane command");
|
|
3016
3137
|
if (remote?.error !== undefined) {
|
|
3017
|
-
throw new LanesError("
|
|
3018
|
-
? remote.message
|
|
3019
|
-
: `Outpost ${outpostId} rejected the ${action} command`, {
|
|
3138
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the ${action} command`, {
|
|
3020
3139
|
lane_id: lane.lane_id,
|
|
3021
3140
|
outpost_id: outpostId,
|
|
3022
3141
|
command_id: dispatched.commandId,
|
|
3023
|
-
|
|
3142
|
+
owner_error: ownerErrorFromOutput(dispatched.stdout),
|
|
3024
3143
|
});
|
|
3025
3144
|
}
|
|
3026
3145
|
const resultConfirmed = remote !== null &&
|
|
@@ -3642,6 +3761,9 @@ async function commitLaneMove(ctx, laneId, target, account) {
|
|
|
3642
3761
|
outcome: "unknown",
|
|
3643
3762
|
...(commandId !== undefined ? { command_id: commandId } : {}),
|
|
3644
3763
|
check_command: checkCommand,
|
|
3764
|
+
...(dispatchDetails.owner_error !== undefined
|
|
3765
|
+
? { owner_error: dispatchDetails.owner_error }
|
|
3766
|
+
: {}),
|
|
3645
3767
|
});
|
|
3646
3768
|
}
|
|
3647
3769
|
let restored = false;
|
|
@@ -3682,6 +3804,9 @@ async function commitLaneMove(ctx, laneId, target, account) {
|
|
|
3682
3804
|
source_host: before.host,
|
|
3683
3805
|
target_host: target,
|
|
3684
3806
|
source_restored: restored,
|
|
3807
|
+
...(dispatchDetails.owner_error !== undefined
|
|
3808
|
+
? { owner_error: dispatchDetails.owner_error }
|
|
3809
|
+
: {}),
|
|
3685
3810
|
...(rollbackError !== undefined
|
|
3686
3811
|
? { rollback_error: dispatchErrorMessage(rollbackError) }
|
|
3687
3812
|
: {}),
|
|
@@ -3711,6 +3836,18 @@ async function dispatchRemoteLaneMove(ctx, source, target, account) {
|
|
|
3711
3836
|
catch (err) {
|
|
3712
3837
|
throw new LanesError("lane_move_dispatch_failed", `Could not ask Outpost ${source.host.id} to move lane ${source.lane_id}: ${dispatchErrorMessage(err)}`, { lane_id: source.lane_id, source_outpost: source.host.id });
|
|
3713
3838
|
}
|
|
3839
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
3840
|
+
if (ownerError) {
|
|
3841
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${source.host.id} rejected the lane move: ${ownerError.message}`, {
|
|
3842
|
+
lane_id: source.lane_id,
|
|
3843
|
+
source_outpost: source.host.id,
|
|
3844
|
+
command_id: dispatched.commandId,
|
|
3845
|
+
status: dispatched.status,
|
|
3846
|
+
done: dispatched.done ?? false,
|
|
3847
|
+
exit_code: dispatched.exitCode,
|
|
3848
|
+
owner_error: ownerError,
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3714
3851
|
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
3715
3852
|
throw new LanesError("lane_move_dispatch_failed", `Outpost ${source.host.id} did not confirm the lane move`, {
|
|
3716
3853
|
lane_id: source.lane_id,
|
|
@@ -3830,40 +3967,54 @@ function callerMatchesSenior(ctx, seniorRef) {
|
|
|
3830
3967
|
function forceAdmissionAuthorized(ctx, lane) {
|
|
3831
3968
|
return callerMatchesSenior(ctx, lane.senior);
|
|
3832
3969
|
}
|
|
3833
|
-
async function
|
|
3834
|
-
if (opts.hire === undefined) {
|
|
3835
|
-
throw new LanesError("missing_hire_grant", "--hire is required", {});
|
|
3836
|
-
}
|
|
3837
|
-
const hireGrant = parseHireGrant(opts.hire);
|
|
3970
|
+
async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContext) {
|
|
3838
3971
|
const hqRoot = ctx.hqRoot();
|
|
3839
3972
|
const initial = readLane(hqRoot, laneId);
|
|
3840
|
-
if (!callerMatchesSenior(ctx, initial.senior)) {
|
|
3973
|
+
if (authorization === "senior" && !callerMatchesSenior(ctx, initial.senior)) {
|
|
3841
3974
|
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: initial.senior });
|
|
3842
3975
|
}
|
|
3843
|
-
if (
|
|
3844
|
-
|
|
3845
|
-
|
|
3976
|
+
if (authorization === "ssm-owner-dispatch" &&
|
|
3977
|
+
(initial.host.kind !== "outpost" || !laneIsHostedHere(ctx, initial))) {
|
|
3978
|
+
throw new LanesError("lane_remote", `Outpost grant dispatch cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: initial.host });
|
|
3846
3979
|
}
|
|
3847
|
-
|
|
3980
|
+
if (authorization === "ssm-owner-dispatch" &&
|
|
3981
|
+
(!grantContext || !sameHireGrantSenior(initial.senior, grantContext.expected_senior))) {
|
|
3982
|
+
throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed after the primary authorized its grant update`, { lane_id: laneId, current_senior: initial.senior });
|
|
3983
|
+
}
|
|
3984
|
+
const enforce = grantContext?.enforcement_enabled ??
|
|
3985
|
+
await ctx.laneHireGrantsEnabled(initial.company);
|
|
3848
3986
|
const lockedLaneIds = [
|
|
3849
3987
|
laneId,
|
|
3850
3988
|
...(initial.senior.kind === "lane" && initial.senior.id
|
|
3851
3989
|
? [initial.senior.id]
|
|
3852
3990
|
: []),
|
|
3853
3991
|
];
|
|
3854
|
-
const updated =
|
|
3992
|
+
const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
|
|
3855
3993
|
const current = readLane(hqRoot, laneId);
|
|
3856
|
-
if (!callerMatchesSenior(ctx, current.senior)) {
|
|
3994
|
+
if (authorization === "senior" && !callerMatchesSenior(ctx, current.senior)) {
|
|
3857
3995
|
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(current.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: current.senior });
|
|
3858
3996
|
}
|
|
3859
|
-
|
|
3997
|
+
if (authorization === "ssm-owner-dispatch" &&
|
|
3998
|
+
(current.host.kind !== "outpost" || !laneIsHostedHere(ctx, current))) {
|
|
3999
|
+
throw new LanesError("lane_remote", `Outpost grant dispatch cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: current.host });
|
|
4000
|
+
}
|
|
4001
|
+
if (authorization === "ssm-owner-dispatch" &&
|
|
4002
|
+
(!grantContext || !sameHireGrantSenior(current.senior, grantContext.expected_senior))) {
|
|
4003
|
+
throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed before the Outpost applied the grant update`, { lane_id: laneId, current_senior: current.senior });
|
|
4004
|
+
}
|
|
4005
|
+
const hierarchy = grantContext?.hierarchy ?? loadAllLanes(hqRoot);
|
|
3860
4006
|
if (enforce && current.senior.kind === "lane") {
|
|
3861
4007
|
if (!current.senior.id) {
|
|
3862
4008
|
throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
|
|
3863
4009
|
lane_id: laneId,
|
|
3864
4010
|
});
|
|
3865
4011
|
}
|
|
3866
|
-
const parent =
|
|
4012
|
+
const parent = hierarchy.find((lane) => lane.lane_id === current.senior.id);
|
|
4013
|
+
if (!parent) {
|
|
4014
|
+
throw new LanesError(authorization === "ssm-owner-dispatch"
|
|
4015
|
+
? "hire_grant_context_invalid"
|
|
4016
|
+
: "hire_grant_senior_invalid", `Lane ${laneId}'s senior lane is missing from the grant hierarchy`, { lane_id: laneId, senior_lane_id: current.senior.id });
|
|
4017
|
+
}
|
|
3867
4018
|
if (isTerminalLaneState(parent.state)) {
|
|
3868
4019
|
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot change grants`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
3869
4020
|
}
|
|
@@ -3901,9 +4052,44 @@ async function runGrant(ctx, laneId, opts) {
|
|
|
3901
4052
|
hire_grant: hireGrant,
|
|
3902
4053
|
}));
|
|
3903
4054
|
});
|
|
3904
|
-
|
|
4055
|
+
return updated;
|
|
4056
|
+
}
|
|
4057
|
+
function printGrantResult(hqRoot, updated, hierarchy) {
|
|
4058
|
+
const fields = laneHireFields(updated, hierarchy ?? loadAllLanes(hqRoot));
|
|
3905
4059
|
printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
|
|
3906
4060
|
}
|
|
4061
|
+
async function runGrant(ctx, laneId, opts) {
|
|
4062
|
+
if (opts.hire === undefined) {
|
|
4063
|
+
throw new LanesError("missing_hire_grant", "--hire is required", {});
|
|
4064
|
+
}
|
|
4065
|
+
const hireGrant = parseHireGrant(opts.hire);
|
|
4066
|
+
const hqRoot = ctx.hqRoot();
|
|
4067
|
+
const initial = readLane(hqRoot, laneId);
|
|
4068
|
+
if (!callerMatchesSenior(ctx, initial.senior)) {
|
|
4069
|
+
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: initial.senior });
|
|
4070
|
+
}
|
|
4071
|
+
if (initial.host.kind === "outpost" && !laneRunsHere(initial, reconcileHostFromCtx(ctx))) {
|
|
4072
|
+
await dispatchOutpostLaneCommand(ctx, initial, "grant", { hire: String(hireGrant) });
|
|
4073
|
+
return;
|
|
4074
|
+
}
|
|
4075
|
+
const updated = await applyHireGrant(ctx, laneId, hireGrant, "senior");
|
|
4076
|
+
printGrantResult(hqRoot, updated);
|
|
4077
|
+
}
|
|
4078
|
+
async function runGrantOwnerTarget(ctx, laneId, opts) {
|
|
4079
|
+
if (!ctx.isSsmOwnerDispatch()) {
|
|
4080
|
+
throw new LanesError("hire_grant_dispatch_unauthorized", "The Outpost grant target is available only to an SSM Run Command dispatch", { lane_id: laneId });
|
|
4081
|
+
}
|
|
4082
|
+
if (opts.hire === undefined) {
|
|
4083
|
+
throw new LanesError("missing_hire_grant", "--hire is required", {});
|
|
4084
|
+
}
|
|
4085
|
+
if (opts.grantContext === undefined) {
|
|
4086
|
+
throw new LanesError("hire_grant_context_invalid", "--grant-context is required", { lane_id: laneId });
|
|
4087
|
+
}
|
|
4088
|
+
const hireGrant = parseHireGrant(opts.hire);
|
|
4089
|
+
const grantContext = parseOutpostHireGrantContext(opts.grantContext, laneId);
|
|
4090
|
+
const hqRoot = ctx.hqRoot();
|
|
4091
|
+
printGrantResult(hqRoot, await applyHireGrant(ctx, laneId, hireGrant, "ssm-owner-dispatch", grantContext), grantContext.hierarchy);
|
|
4092
|
+
}
|
|
3907
4093
|
async function runBackfillGrants(ctx, opts) {
|
|
3908
4094
|
if (opts.apply && opts.dryRun) {
|
|
3909
4095
|
throw new LanesError("hire_grant_backfill_mode_conflict", "Pass either --dry-run or --apply, not both", {});
|
|
@@ -5718,6 +5904,16 @@ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
|
|
|
5718
5904
|
!commandId ||
|
|
5719
5905
|
dispatched.status !== "Success" ||
|
|
5720
5906
|
dispatched.exitCode !== 0) {
|
|
5907
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
5908
|
+
if (ownerError) {
|
|
5909
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the lane read after stop: ${ownerError.message}`, {
|
|
5910
|
+
lane_id: laneId,
|
|
5911
|
+
outpost_id: outpostId,
|
|
5912
|
+
command_id: stopCommandId,
|
|
5913
|
+
sync_command_id: commandId || null,
|
|
5914
|
+
owner_error: ownerError,
|
|
5915
|
+
});
|
|
5916
|
+
}
|
|
5721
5917
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not confirm a lane read after stop ${laneId}`, {
|
|
5722
5918
|
lane_id: laneId,
|
|
5723
5919
|
outpost_id: outpostId,
|
|
@@ -5741,6 +5937,16 @@ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
|
|
|
5741
5937
|
cause: err instanceof Error ? err.message : String(err),
|
|
5742
5938
|
});
|
|
5743
5939
|
}
|
|
5940
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
5941
|
+
if (ownerError) {
|
|
5942
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the lane read after stop: ${ownerError.message}`, {
|
|
5943
|
+
lane_id: laneId,
|
|
5944
|
+
outpost_id: outpostId,
|
|
5945
|
+
command_id: stopCommandId,
|
|
5946
|
+
sync_command_id: commandId,
|
|
5947
|
+
owner_error: ownerError,
|
|
5948
|
+
});
|
|
5949
|
+
}
|
|
5744
5950
|
if (remote?.ok !== true || remote.action !== "show") {
|
|
5745
5951
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return lane ${laneId} after stop`, {
|
|
5746
5952
|
lane_id: laneId,
|
|
@@ -5771,6 +5977,18 @@ async function dispatchOutpostLaneStop(ctx, lane) {
|
|
|
5771
5977
|
catch (err) {
|
|
5772
5978
|
throw new LanesError("outpost_dispatch_failed", `Could not dispatch stop for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
5773
5979
|
}
|
|
5980
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
5981
|
+
if (ownerError) {
|
|
5982
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the stop command: ${ownerError.message}`, {
|
|
5983
|
+
lane_id: lane.lane_id,
|
|
5984
|
+
outpost_id: outpostId,
|
|
5985
|
+
command_id: dispatched.commandId,
|
|
5986
|
+
status: dispatched.status,
|
|
5987
|
+
done: dispatched.done ?? false,
|
|
5988
|
+
exit_code: dispatched.exitCode,
|
|
5989
|
+
owner_error: ownerError,
|
|
5990
|
+
});
|
|
5991
|
+
}
|
|
5774
5992
|
const commandId = dispatched.commandId?.trim();
|
|
5775
5993
|
if (dispatched.done !== true || !commandId) {
|
|
5776
5994
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a completed stop command for lane ${lane.lane_id}`, {
|
|
@@ -5814,13 +6032,13 @@ async function dispatchOutpostLaneStop(ctx, lane) {
|
|
|
5814
6032
|
syncCommandId = owner.commandId;
|
|
5815
6033
|
}
|
|
5816
6034
|
else if (remote?.error !== undefined) {
|
|
5817
|
-
throw new LanesError("
|
|
6035
|
+
throw new LanesError("outpost_dispatch_failed", typeof remote.message === "string"
|
|
5818
6036
|
? remote.message
|
|
5819
6037
|
: `Outpost ${outpostId} rejected the stop command for lane ${lane.lane_id}`, {
|
|
5820
6038
|
lane_id: lane.lane_id,
|
|
5821
6039
|
outpost_id: outpostId,
|
|
5822
6040
|
command_id: commandId,
|
|
5823
|
-
|
|
6041
|
+
owner_error: ownerErrorFromOutput(dispatched.stdout),
|
|
5824
6042
|
});
|
|
5825
6043
|
}
|
|
5826
6044
|
else {
|
|
@@ -6661,6 +6879,14 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6661
6879
|
.action(wrap(async (opts) => {
|
|
6662
6880
|
await runCreate(ctx, opts);
|
|
6663
6881
|
}));
|
|
6882
|
+
lanes
|
|
6883
|
+
.command("grant-target", { hidden: true })
|
|
6884
|
+
.argument("<lane-id>", "Lane id")
|
|
6885
|
+
.option("--hire <n>", "Non-negative safe integer hire grant")
|
|
6886
|
+
.option("--grant-context <base64url>", "Primary-authorized hire hierarchy and gate decision")
|
|
6887
|
+
.action(wrap(async (laneId, opts) => {
|
|
6888
|
+
await runGrantOwnerTarget(ctx, laneId, opts);
|
|
6889
|
+
}));
|
|
6664
6890
|
lanes
|
|
6665
6891
|
.command("grant")
|
|
6666
6892
|
.description("Set a lane's alive child-lane grant as its senior")
|
|
@@ -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
|
|
4
|
-
export declare function
|
|
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;
|
|
@@ -36,6 +36,7 @@ export interface OutpostLaneCommand {
|
|
|
36
36
|
account?: string;
|
|
37
37
|
model?: string;
|
|
38
38
|
hire?: string;
|
|
39
|
+
grantContext?: string;
|
|
39
40
|
compact?: boolean;
|
|
40
41
|
}
|
|
41
42
|
export interface OutpostExecPort {
|
|
@@ -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
|