nawabari 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -7
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +333 -40
- package/dist/cli.js.map +1 -1
- package/dist/contract.js +50 -0
- package/dist/contract.js.map +1 -1
- package/dist/domain/errors.d.ts +1 -1
- package/dist/domain/errors.js +4 -1
- package/dist/domain/errors.js.map +1 -1
- package/dist/domain/sandbox-launcher.d.ts +35 -0
- package/dist/domain/sandbox-launcher.js +679 -0
- package/dist/domain/sandbox-launcher.js.map +1 -0
- package/dist/domain/sandbox.d.ts +190 -0
- package/dist/domain/sandbox.js +363 -0
- package/dist/domain/sandbox.js.map +1 -0
- package/dist/domain/session-backend.d.ts +2 -1
- package/dist/domain/session-backend.js +71 -1
- package/dist/domain/session-backend.js.map +1 -1
- package/dist/domain/session.d.ts +54 -0
- package/dist/domain/session.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/git.d.ts +25 -0
- package/dist/git.js +75 -0
- package/dist/git.js.map +1 -1
- package/dist/registry/atomic.d.ts +24 -0
- package/dist/registry/atomic.js +35 -2
- package/dist/registry/atomic.js.map +1 -1
- package/dist/registry/errors.d.ts +1 -1
- package/dist/registry/errors.js +1 -0
- package/dist/registry/errors.js.map +1 -1
- package/dist/session-registry.d.ts +120 -0
- package/dist/session-registry.js +456 -92
- package/dist/session-registry.js.map +1 -1
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { EVIDENCE_MAX_DIFF_BYTES, EVIDENCE_MAX_DIFF_HUNKS, EVIDENCE_MAX_DIFF_PAT
|
|
|
6
6
|
import { createLocalSessionBackend } from "./domain/session-backend.js";
|
|
7
7
|
import { defaultCliIO, renderFailure, renderSuccess } from "./presentation.js";
|
|
8
8
|
import { MACHINE_CONTRACT_ID, MACHINE_CONTRACT_SCHEMA_VERSION, machineContract } from "./contract.js";
|
|
9
|
+
import { resolveSandboxExecutionRequest, runSandboxedCommand, } from "./domain/sandbox.js";
|
|
9
10
|
const CLI_NAME = "nawabari";
|
|
10
11
|
const packageMetadata = createRequire(import.meta.url)("../package.json");
|
|
11
12
|
const VERSION = packageMetadata.version;
|
|
@@ -25,14 +26,18 @@ const HELP_COMMANDS = [
|
|
|
25
26
|
value: "<name>",
|
|
26
27
|
default: "nawabari/session/<session_id>",
|
|
27
28
|
}),
|
|
28
|
-
option("--worktree", "
|
|
29
|
+
option("--worktree", "Exact managed worktree path override; mutually exclusive with --worktree-root", {
|
|
29
30
|
value: "<path>",
|
|
30
31
|
default: "<managed_worktree_root>/<repository>-<session_id>",
|
|
31
32
|
}),
|
|
33
|
+
option("--worktree-root", "Managed root to place the worktree under; Nawabari derives the final path. Mutually exclusive with --worktree", { value: "<path>", default: "resolved repository-local root" }),
|
|
32
34
|
option("--base", "Commit-resolving base ref for the new worktree", { value: "<ref>", default: "HEAD" }),
|
|
33
35
|
option("--label", "Optional display label; never used as an identity", { value: "<text>", default: "omitted" }),
|
|
34
36
|
],
|
|
35
|
-
notes: [
|
|
37
|
+
notes: [
|
|
38
|
+
"All create options are optional. Use status --json to discover managed_worktree_root.",
|
|
39
|
+
"--worktree and --worktree-root cannot be combined.",
|
|
40
|
+
],
|
|
36
41
|
},
|
|
37
42
|
{
|
|
38
43
|
name: "session id",
|
|
@@ -46,6 +51,30 @@ const HELP_COMMANDS = [
|
|
|
46
51
|
usage: `${CLI_NAME} session show [--session <id>]`,
|
|
47
52
|
options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
|
|
48
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: "session inspect",
|
|
56
|
+
summary: "Report side-effect-free close/cleanup readiness for a session",
|
|
57
|
+
usage: `${CLI_NAME} session inspect [--session <id>] [--integrated-revision <rev>]`,
|
|
58
|
+
options: [
|
|
59
|
+
option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
|
|
60
|
+
option("--integrated-revision", "Externally evidenced revision to test for non-ancestry (squash/rebase) integration; independently re-verified via exact Git tree-object equivalence, never trusted blindly", { value: "<rev>" }),
|
|
61
|
+
],
|
|
62
|
+
notes: [
|
|
63
|
+
"Read-only: never mutates session, claim, Git, worktree, branch, or registry state. Repeated calls are idempotent.",
|
|
64
|
+
"Derived from the same authoritative close/cleanup Git evidence as session close; does not duplicate or diverge from that logic.",
|
|
65
|
+
"Nawabari never queries GitHub or any remote provider; --integrated-revision only names a local revision for Nawabari to independently verify.",
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "session run",
|
|
70
|
+
summary: "Run one command inside the protected session sandbox",
|
|
71
|
+
usage: `${CLI_NAME} session run [--session <id>] -- <command> [args...]`,
|
|
72
|
+
options: [option("--session", "Select the active owned session", { value: "<id>" })],
|
|
73
|
+
notes: [
|
|
74
|
+
"The -- terminator is mandatory. The command is passed as argv without a shell, and protected execution is fail-closed.",
|
|
75
|
+
"session exec is an alias.",
|
|
76
|
+
],
|
|
77
|
+
},
|
|
49
78
|
{
|
|
50
79
|
name: "session list",
|
|
51
80
|
summary: "List bounded repository session records",
|
|
@@ -69,14 +98,25 @@ const HELP_COMMANDS = [
|
|
|
69
98
|
},
|
|
70
99
|
{
|
|
71
100
|
name: "session update",
|
|
72
|
-
summary: "
|
|
73
|
-
usage: `${CLI_NAME} session update --resource <path-or-glob> --mode <read|write|exclusive-write> [--session <id>]`,
|
|
101
|
+
summary: "Atomically replace a session's complete resource claim set",
|
|
102
|
+
usage: `${CLI_NAME} session update --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...] [--session <id>]`,
|
|
74
103
|
options: [
|
|
75
|
-
option("--resource", "Repository-relative resource
|
|
76
|
-
|
|
104
|
+
option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
|
|
105
|
+
value: "<path-or-glob>",
|
|
106
|
+
required: true,
|
|
107
|
+
}),
|
|
108
|
+
option("--mode", "Mode for the --resource immediately before it; repeatable", {
|
|
109
|
+
value: "<read|write|exclusive-write>",
|
|
110
|
+
required: true,
|
|
111
|
+
}),
|
|
77
112
|
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
78
113
|
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
79
114
|
],
|
|
115
|
+
notes: [
|
|
116
|
+
"The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
|
|
117
|
+
"on any invalid or conflicting claim the prior set is left unchanged.",
|
|
118
|
+
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
119
|
+
],
|
|
80
120
|
},
|
|
81
121
|
{
|
|
82
122
|
name: "session claims",
|
|
@@ -101,9 +141,25 @@ const HELP_COMMANDS = [
|
|
|
101
141
|
},
|
|
102
142
|
{
|
|
103
143
|
name: "resource update",
|
|
104
|
-
summary: "
|
|
105
|
-
usage: `${CLI_NAME} resource update --resource <path-or-glob> --mode <read|write|exclusive-write
|
|
106
|
-
options: [
|
|
144
|
+
summary: "Atomically replace a session's complete resource claim set (alias)",
|
|
145
|
+
usage: `${CLI_NAME} resource update --resource <path-or-glob> --mode <read|write|exclusive-write> [--resource <path-or-glob> --mode <read|write|exclusive-write> ...] [--session <id>]`,
|
|
146
|
+
options: [
|
|
147
|
+
option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
|
|
148
|
+
value: "<path-or-glob>",
|
|
149
|
+
required: true,
|
|
150
|
+
}),
|
|
151
|
+
option("--mode", "Mode for the --resource immediately before it; repeatable", {
|
|
152
|
+
value: "<read|write|exclusive-write>",
|
|
153
|
+
required: true,
|
|
154
|
+
}),
|
|
155
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
156
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
157
|
+
],
|
|
158
|
+
notes: [
|
|
159
|
+
"The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
|
|
160
|
+
"on any invalid or conflicting claim the prior set is left unchanged.",
|
|
161
|
+
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
162
|
+
],
|
|
107
163
|
},
|
|
108
164
|
{
|
|
109
165
|
name: "resource list",
|
|
@@ -123,8 +179,15 @@ const HELP_COMMANDS = [
|
|
|
123
179
|
{
|
|
124
180
|
name: "session close",
|
|
125
181
|
summary: "Close the current or selected session",
|
|
126
|
-
usage: `${CLI_NAME} session close [--session <id>]`,
|
|
127
|
-
options: [
|
|
182
|
+
usage: `${CLI_NAME} session close [--session <id>] [--integrated-revision <rev>]`,
|
|
183
|
+
options: [
|
|
184
|
+
option("--session", "Select a session instead of the current worktree owner", { value: "<id>" }),
|
|
185
|
+
option("--integrated-revision", "Externally evidenced revision proving non-ancestry (squash/rebase) integration; independently re-verified via exact Git tree-object equivalence, never trusted blindly", { value: "<rev>" }),
|
|
186
|
+
],
|
|
187
|
+
notes: [
|
|
188
|
+
"Ordinary ancestry-based close remains the cheap/default path and requires no flags.",
|
|
189
|
+
"Nawabari never queries GitHub or any remote provider; --integrated-revision only names a local revision for Nawabari to independently verify.",
|
|
190
|
+
],
|
|
128
191
|
},
|
|
129
192
|
{
|
|
130
193
|
name: "authorize",
|
|
@@ -246,7 +309,9 @@ function helpSpecFor(commandArguments) {
|
|
|
246
309
|
return ROOT_HELP_SPEC;
|
|
247
310
|
let key = commandArguments[0] === "resource"
|
|
248
311
|
? `resource ${commandArguments[1] ?? "list"}`
|
|
249
|
-
: commandArguments
|
|
312
|
+
: commandArguments[0] === "session" && commandArguments[1] === "exec"
|
|
313
|
+
? "session run"
|
|
314
|
+
: commandArguments.slice(0, 2).join(" ");
|
|
250
315
|
if (key === "resource claims")
|
|
251
316
|
key = "resource list";
|
|
252
317
|
const direct = HELP_COMMANDS.find((spec) => spec.name === key);
|
|
@@ -348,7 +413,17 @@ function parseGlobalArguments(argv) {
|
|
|
348
413
|
let help = false;
|
|
349
414
|
let version = false;
|
|
350
415
|
const commandArguments = [];
|
|
416
|
+
let passthrough = false;
|
|
351
417
|
for (const argument of argv) {
|
|
418
|
+
if (passthrough) {
|
|
419
|
+
commandArguments.push(argument);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (argument === "--") {
|
|
423
|
+
passthrough = true;
|
|
424
|
+
commandArguments.push(argument);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
352
427
|
if (argument === "--json") {
|
|
353
428
|
json = true;
|
|
354
429
|
}
|
|
@@ -381,6 +456,7 @@ function parseOptions(arguments_, allowed) {
|
|
|
381
456
|
session_id: null,
|
|
382
457
|
branch: null,
|
|
383
458
|
worktree: null,
|
|
459
|
+
worktree_root: null,
|
|
384
460
|
base: null,
|
|
385
461
|
label: null,
|
|
386
462
|
resource: null,
|
|
@@ -406,6 +482,7 @@ function parseOptions(arguments_, allowed) {
|
|
|
406
482
|
patch: false,
|
|
407
483
|
max_bytes: null,
|
|
408
484
|
max_hunks: null,
|
|
485
|
+
integrated_revision: null,
|
|
409
486
|
};
|
|
410
487
|
let dryRun = false;
|
|
411
488
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
@@ -451,6 +528,8 @@ function parseOptions(arguments_, allowed) {
|
|
|
451
528
|
options.branch = value;
|
|
452
529
|
else if (name === "--worktree")
|
|
453
530
|
options.worktree = value;
|
|
531
|
+
else if (name === "--worktree-root")
|
|
532
|
+
options.worktree_root = value;
|
|
454
533
|
else if (name === "--base")
|
|
455
534
|
options.base = value;
|
|
456
535
|
else if (name === "--label")
|
|
@@ -489,6 +568,8 @@ function parseOptions(arguments_, allowed) {
|
|
|
489
568
|
options.max_bytes = value;
|
|
490
569
|
else if (name === "--max-hunks")
|
|
491
570
|
options.max_hunks = value;
|
|
571
|
+
else if (name === "--integrated-revision")
|
|
572
|
+
options.integrated_revision = value;
|
|
492
573
|
}
|
|
493
574
|
if (options.apply && dryRun) {
|
|
494
575
|
return failure(usageError("INVALID_ARGUMENT", "--apply and --dry-run cannot be used together."));
|
|
@@ -498,6 +579,83 @@ function parseOptions(arguments_, allowed) {
|
|
|
498
579
|
function noOptions(arguments_) {
|
|
499
580
|
return parseOptions(arguments_, new Set());
|
|
500
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* `update` accepts a complete desired claim set as repeated
|
|
584
|
+
* `--resource <path> --mode <mode>` pairs. Pairing is by strict local
|
|
585
|
+
* adjacency (each `--resource` must be immediately followed by its own
|
|
586
|
+
* `--mode`, with no other option able to intervene) rather than by
|
|
587
|
+
* parallel-array position, so argv order can never associate a resource
|
|
588
|
+
* with the wrong mode.
|
|
589
|
+
*/
|
|
590
|
+
function parseClaimReplacementPairs(arguments_) {
|
|
591
|
+
let sessionId = null;
|
|
592
|
+
let repository = null;
|
|
593
|
+
const pairs = [];
|
|
594
|
+
let pendingResource = null;
|
|
595
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
596
|
+
const { name, inlineValue } = optionParts(arguments_[index]);
|
|
597
|
+
if (name !== "--session" && name !== "--repository" && name !== "--resource" && name !== "--mode") {
|
|
598
|
+
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
599
|
+
}
|
|
600
|
+
if (pendingResource !== null && name !== "--mode") {
|
|
601
|
+
return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
|
|
602
|
+
}
|
|
603
|
+
const value = inlineValue ?? arguments_[index + 1];
|
|
604
|
+
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
|
|
605
|
+
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
606
|
+
}
|
|
607
|
+
if (inlineValue === null)
|
|
608
|
+
index += 1;
|
|
609
|
+
if (name === "--session")
|
|
610
|
+
sessionId = value;
|
|
611
|
+
else if (name === "--repository")
|
|
612
|
+
repository = value;
|
|
613
|
+
else if (name === "--resource")
|
|
614
|
+
pendingResource = value;
|
|
615
|
+
else {
|
|
616
|
+
if (pendingResource === null) {
|
|
617
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode must be immediately preceded by its own --resource.", {
|
|
618
|
+
option: "--mode",
|
|
619
|
+
}));
|
|
620
|
+
}
|
|
621
|
+
pairs.push({ resource: pendingResource, mode: value });
|
|
622
|
+
pendingResource = null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (pendingResource !== null) {
|
|
626
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource must be immediately followed by --mode.", { option: "--mode" }));
|
|
627
|
+
}
|
|
628
|
+
if (pairs.length === 0) {
|
|
629
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
630
|
+
}
|
|
631
|
+
return { ok: true, value: { session_id: sessionId, repository, pairs } };
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* `claim` accepts exactly one `--resource`/`--mode` pair. Reuses the same
|
|
635
|
+
* strict-adjacency scan as `update` so a second `--resource`/`--mode` (in
|
|
636
|
+
* any order) is rejected outright instead of silently overwriting the
|
|
637
|
+
* first pair (last-wins).
|
|
638
|
+
*/
|
|
639
|
+
function parseSingleClaimPair(arguments_) {
|
|
640
|
+
const parsed = parseClaimReplacementPairs(arguments_);
|
|
641
|
+
if (!parsed.ok)
|
|
642
|
+
return parsed;
|
|
643
|
+
if (parsed.value.pairs.length > 1) {
|
|
644
|
+
return failure(usageError("INVALID_ARGUMENT", "claim accepts exactly one --resource/--mode pair.", {
|
|
645
|
+
pair_count: parsed.value.pairs.length,
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
const [pair] = parsed.value.pairs;
|
|
649
|
+
return {
|
|
650
|
+
ok: true,
|
|
651
|
+
value: {
|
|
652
|
+
session_id: parsed.value.session_id,
|
|
653
|
+
repository: parsed.value.repository,
|
|
654
|
+
resource: pair.resource,
|
|
655
|
+
mode: pair.mode,
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
}
|
|
501
659
|
function sessionListingOptions(parsed) {
|
|
502
660
|
const parseInteger = (option, value) => {
|
|
503
661
|
if (value === null)
|
|
@@ -557,25 +715,33 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
557
715
|
if (subcommand === undefined) {
|
|
558
716
|
return failure(usageError("MISSING_ARGUMENT", "session requires a subcommand."));
|
|
559
717
|
}
|
|
560
|
-
if (subcommand === "claim"
|
|
561
|
-
const parsed =
|
|
718
|
+
if (subcommand === "claim") {
|
|
719
|
+
const parsed = parseSingleClaimPair(rest);
|
|
562
720
|
if (!parsed.ok)
|
|
563
721
|
return parsed;
|
|
564
|
-
if (
|
|
565
|
-
return
|
|
566
|
-
|
|
567
|
-
if (parsed.value.mode === null) {
|
|
568
|
-
return failure(usageError("MISSING_ARGUMENT", "--mode requires a value.", { option: "--mode" }));
|
|
569
|
-
}
|
|
570
|
-
const options = {
|
|
722
|
+
if (dependencies.backend.claimResources === undefined)
|
|
723
|
+
return claimCapabilityUnavailable(subcommand);
|
|
724
|
+
const result = await dependencies.backend.claimResources(context, {
|
|
571
725
|
session_id: parsed.value.session_id,
|
|
572
726
|
repository: parsed.value.repository,
|
|
573
727
|
claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
|
|
728
|
+
});
|
|
729
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
730
|
+
}
|
|
731
|
+
if (subcommand === "update") {
|
|
732
|
+
const parsed = parseClaimReplacementPairs(rest);
|
|
733
|
+
if (!parsed.ok)
|
|
734
|
+
return parsed;
|
|
735
|
+
if (dependencies.backend.updateClaims === undefined)
|
|
577
736
|
return claimCapabilityUnavailable(subcommand);
|
|
578
|
-
const result = await
|
|
737
|
+
const result = await dependencies.backend.updateClaims(context, {
|
|
738
|
+
session_id: parsed.value.session_id,
|
|
739
|
+
repository: parsed.value.repository,
|
|
740
|
+
claims: parsed.value.pairs.map((pair) => ({
|
|
741
|
+
resource: pair.resource,
|
|
742
|
+
mode: pair.mode,
|
|
743
|
+
})),
|
|
744
|
+
});
|
|
579
745
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
580
746
|
}
|
|
581
747
|
if (subcommand === "claims") {
|
|
@@ -600,18 +766,57 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
600
766
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
601
767
|
}
|
|
602
768
|
if (subcommand === "create") {
|
|
603
|
-
const parsed = parseOptions(rest, new Set(["--branch", "--worktree", "--base", "--label"]));
|
|
769
|
+
const parsed = parseOptions(rest, new Set(["--branch", "--worktree", "--worktree-root", "--base", "--label"]));
|
|
604
770
|
if (!parsed.ok)
|
|
605
771
|
return parsed;
|
|
772
|
+
if (parsed.value.worktree !== null && parsed.value.worktree_root !== null) {
|
|
773
|
+
return failure(usageError("INVALID_ARGUMENT", "--worktree and --worktree-root cannot be used together."));
|
|
774
|
+
}
|
|
606
775
|
const options = {
|
|
607
776
|
branch: parsed.value.branch,
|
|
608
777
|
worktree: parsed.value.worktree,
|
|
778
|
+
worktree_root: parsed.value.worktree_root,
|
|
609
779
|
base: parsed.value.base,
|
|
610
780
|
label: parsed.value.label,
|
|
611
781
|
};
|
|
612
782
|
const result = await dependencies.backend.createSession(context, options);
|
|
613
783
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
614
784
|
}
|
|
785
|
+
if (subcommand === "run" || subcommand === "exec") {
|
|
786
|
+
const delimiter = rest.indexOf("--");
|
|
787
|
+
if (delimiter === -1) {
|
|
788
|
+
return failure(usageError("MISSING_ARGUMENT", "session run requires a -- terminator before the command."));
|
|
789
|
+
}
|
|
790
|
+
const parsed = parseOptions(rest.slice(0, delimiter), new Set(["--session"]));
|
|
791
|
+
if (!parsed.ok)
|
|
792
|
+
return parsed;
|
|
793
|
+
const command = rest[delimiter + 1];
|
|
794
|
+
if (command === undefined || command.length === 0) {
|
|
795
|
+
return failure(usageError("MISSING_ARGUMENT", "session run requires a command after --."));
|
|
796
|
+
}
|
|
797
|
+
const request = await resolveSandboxExecutionRequest(dependencies.backend, context, { session_id: parsed.value.session_id, enforce: true }, dependencies.sandboxProbe, dependencies.sandboxRuntimeLayout);
|
|
798
|
+
if (!request.ok)
|
|
799
|
+
return request;
|
|
800
|
+
const runner = dependencies.sandboxRunner ?? runSandboxedCommand;
|
|
801
|
+
const result = await runner(request.value, {
|
|
802
|
+
command,
|
|
803
|
+
args: rest.slice(delimiter + 2),
|
|
804
|
+
});
|
|
805
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
806
|
+
}
|
|
807
|
+
if (subcommand === "inspect") {
|
|
808
|
+
const parsed = parseOptions(rest, new Set(["--session", "--integrated-revision"]));
|
|
809
|
+
if (!parsed.ok)
|
|
810
|
+
return parsed;
|
|
811
|
+
if (dependencies.backend.sessionDiagnostic === undefined)
|
|
812
|
+
return sessionDiagnosticCapabilityUnavailable();
|
|
813
|
+
const options = {
|
|
814
|
+
session_id: parsed.value.session_id,
|
|
815
|
+
integrated_revision: parsed.value.integrated_revision,
|
|
816
|
+
};
|
|
817
|
+
const result = await dependencies.backend.sessionDiagnostic(context, options);
|
|
818
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
819
|
+
}
|
|
615
820
|
if (subcommand === "id" || subcommand === "show" || subcommand === "close") {
|
|
616
821
|
if (subcommand === "id") {
|
|
617
822
|
const parsed = noOptions(rest);
|
|
@@ -622,11 +827,14 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
622
827
|
return selected;
|
|
623
828
|
return { ok: true, value: { session_id: selected.value.session_id } };
|
|
624
829
|
}
|
|
625
|
-
const parsed = parseOptions(rest, new Set(["--session"]));
|
|
830
|
+
const parsed = parseOptions(rest, new Set(subcommand === "close" ? ["--session", "--integrated-revision"] : ["--session"]));
|
|
626
831
|
if (!parsed.ok)
|
|
627
832
|
return parsed;
|
|
628
833
|
if (subcommand === "close") {
|
|
629
|
-
const closeOptions = {
|
|
834
|
+
const closeOptions = {
|
|
835
|
+
session_id: parsed.value.session_id,
|
|
836
|
+
integrated_revision: parsed.value.integrated_revision,
|
|
837
|
+
};
|
|
630
838
|
const selected = await dependencies.backend.closeSession(context, closeOptions);
|
|
631
839
|
return selected.ok ? { ok: true, value: selected.value } : selected;
|
|
632
840
|
}
|
|
@@ -647,23 +855,35 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
647
855
|
}
|
|
648
856
|
if (command === "resource") {
|
|
649
857
|
const resourceSubcommand = subcommand ?? "list";
|
|
650
|
-
if (resourceSubcommand === "claim"
|
|
651
|
-
const parsed =
|
|
858
|
+
if (resourceSubcommand === "claim") {
|
|
859
|
+
const parsed = parseSingleClaimPair(rest);
|
|
652
860
|
if (!parsed.ok)
|
|
653
861
|
return parsed;
|
|
654
|
-
if (
|
|
655
|
-
return failure(usageError("MISSING_ARGUMENT", "resource claim requires --resource and --mode."));
|
|
656
|
-
}
|
|
657
|
-
const operation = resourceSubcommand === "claim" ? dependencies.backend.claimResources : dependencies.backend.updateClaims;
|
|
658
|
-
if (operation === undefined)
|
|
862
|
+
if (dependencies.backend.claimResources === undefined)
|
|
659
863
|
return claimCapabilityUnavailable(resourceSubcommand);
|
|
660
|
-
const result = await
|
|
864
|
+
const result = await dependencies.backend.claimResources(context, {
|
|
661
865
|
session_id: parsed.value.session_id,
|
|
662
866
|
repository: parsed.value.repository,
|
|
663
867
|
claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
664
868
|
});
|
|
665
869
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
666
870
|
}
|
|
871
|
+
if (resourceSubcommand === "update") {
|
|
872
|
+
const parsed = parseClaimReplacementPairs(rest);
|
|
873
|
+
if (!parsed.ok)
|
|
874
|
+
return parsed;
|
|
875
|
+
if (dependencies.backend.updateClaims === undefined)
|
|
876
|
+
return claimCapabilityUnavailable(resourceSubcommand);
|
|
877
|
+
const result = await dependencies.backend.updateClaims(context, {
|
|
878
|
+
session_id: parsed.value.session_id,
|
|
879
|
+
repository: parsed.value.repository,
|
|
880
|
+
claims: parsed.value.pairs.map((pair) => ({
|
|
881
|
+
resource: pair.resource,
|
|
882
|
+
mode: pair.mode,
|
|
883
|
+
})),
|
|
884
|
+
});
|
|
885
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
886
|
+
}
|
|
667
887
|
if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
|
|
668
888
|
const parsed = parseOptions(rest, new Set(["--session"]));
|
|
669
889
|
if (!parsed.ok)
|
|
@@ -926,6 +1146,11 @@ function checkpointCapabilityUnavailable() {
|
|
|
926
1146
|
operation: "checkpoint",
|
|
927
1147
|
}));
|
|
928
1148
|
}
|
|
1149
|
+
function sessionDiagnosticCapabilityUnavailable() {
|
|
1150
|
+
return failure(new DomainError("BACKEND_UNAVAILABLE", "Session diagnostic capability is not available.", {
|
|
1151
|
+
operation: "session.inspect",
|
|
1152
|
+
}));
|
|
1153
|
+
}
|
|
929
1154
|
function repositoryEvidenceCapabilityUnavailable(operation) {
|
|
930
1155
|
return failure(new DomainError("BACKEND_UNAVAILABLE", "Repository evidence capability is not available.", { operation }));
|
|
931
1156
|
}
|
|
@@ -961,9 +1186,62 @@ function emitFailure(mode, command, error, io) {
|
|
|
961
1186
|
io[mode === "json" ? "stdout" : "stderr"](renderFailure(mode, command, error));
|
|
962
1187
|
return error.exitCode;
|
|
963
1188
|
}
|
|
1189
|
+
/** Extract the raw `--session` value a rejected INVALID_SESSION_ID carried, regardless of which command path threw it. */
|
|
1190
|
+
function invalidSessionIdQuery(error) {
|
|
1191
|
+
const details = error.details;
|
|
1192
|
+
if (details === null)
|
|
1193
|
+
return null;
|
|
1194
|
+
if (typeof details.sessionId === "string")
|
|
1195
|
+
return details.sessionId;
|
|
1196
|
+
const nested = details.details;
|
|
1197
|
+
if (nested === null || typeof nested !== "object" || Array.isArray(nested))
|
|
1198
|
+
return null;
|
|
1199
|
+
const nestedSessionId = nested.sessionId;
|
|
1200
|
+
return typeof nestedSessionId === "string" ? nestedSessionId : null;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* `--session` stays machine-ID based: an invalid value is never silently
|
|
1204
|
+
* reinterpreted as a label. But when it exactly and unambiguously matches
|
|
1205
|
+
* one active session's label, expose that session's canonical ID as a
|
|
1206
|
+
* bounded, non-authoritative hint instead of forcing a separate `session
|
|
1207
|
+
* list` round trip. Ambiguous or absent matches never guess.
|
|
1208
|
+
*/
|
|
1209
|
+
async function enrichInvalidSessionIdError(error, backend, context) {
|
|
1210
|
+
if (error.code !== "INVALID_SESSION_ID")
|
|
1211
|
+
return error;
|
|
1212
|
+
const query = invalidSessionIdQuery(error);
|
|
1213
|
+
if (query === null || query.length === 0)
|
|
1214
|
+
return error;
|
|
1215
|
+
const listing = await backend.listSessions(context, { include_closed: false, limit: MAX_SESSION_LIST_LIMIT });
|
|
1216
|
+
if (!listing.ok)
|
|
1217
|
+
return error;
|
|
1218
|
+
const matches = listing.value.sessions.filter((session) => session.state === "active" && session.label === query);
|
|
1219
|
+
const hint = matches.length === 1
|
|
1220
|
+
? {
|
|
1221
|
+
session_label_query: query,
|
|
1222
|
+
session_label_match: "unique",
|
|
1223
|
+
session_id_hint: matches[0].session_id,
|
|
1224
|
+
safe_actions: ["retry-with-session-id-hint"],
|
|
1225
|
+
}
|
|
1226
|
+
: matches.length > 1
|
|
1227
|
+
? {
|
|
1228
|
+
session_label_query: query,
|
|
1229
|
+
session_label_match: "ambiguous",
|
|
1230
|
+
session_label_match_count: matches.length,
|
|
1231
|
+
safe_actions: ["disambiguate-session-label", "list-sessions"],
|
|
1232
|
+
}
|
|
1233
|
+
: {
|
|
1234
|
+
session_label_query: query,
|
|
1235
|
+
session_label_match: "none",
|
|
1236
|
+
safe_actions: ["list-sessions"],
|
|
1237
|
+
};
|
|
1238
|
+
return new DomainError(error.code, error.message, { ...error.details, ...hint }, error.exitCode);
|
|
1239
|
+
}
|
|
964
1240
|
export async function runCli(argv, dependencies = {}) {
|
|
965
1241
|
const io = dependencies.io ?? defaultCliIO();
|
|
966
|
-
const
|
|
1242
|
+
const delimiter = argv.indexOf("--");
|
|
1243
|
+
const globalArguments = delimiter === -1 ? argv : argv.slice(0, delimiter);
|
|
1244
|
+
const mode = globalArguments.includes("--json") ? "json" : "human";
|
|
967
1245
|
const parsed = parseGlobalArguments(argv);
|
|
968
1246
|
if (!parsed.ok)
|
|
969
1247
|
return emitFailure(mode, "cli", parsed.error, io);
|
|
@@ -1004,10 +1282,25 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
1004
1282
|
const cwd = dependencies.cwd ?? process.cwd();
|
|
1005
1283
|
try {
|
|
1006
1284
|
const backend = dependencies.backend ?? createLocalSessionBackend();
|
|
1007
|
-
const result = await executeCommand(parsed.value.commandArguments, {
|
|
1008
|
-
|
|
1009
|
-
|
|
1285
|
+
const result = await executeCommand(parsed.value.commandArguments, {
|
|
1286
|
+
backend,
|
|
1287
|
+
cwd,
|
|
1288
|
+
sandboxRunner: dependencies.sandboxRunner,
|
|
1289
|
+
sandboxProbe: dependencies.sandboxProbe,
|
|
1290
|
+
sandboxRuntimeLayout: dependencies.sandboxRuntimeLayout,
|
|
1291
|
+
});
|
|
1292
|
+
if (!result.ok) {
|
|
1293
|
+
const enriched = await enrichInvalidSessionIdError(result.error, backend, sessionContext(cwd));
|
|
1294
|
+
return emitFailure(mode, command, enriched, io);
|
|
1295
|
+
}
|
|
1010
1296
|
io.stdout(renderSuccess(mode, command, result.value));
|
|
1297
|
+
if ((command === "session run" || command === "session exec") &&
|
|
1298
|
+
typeof result.value.exit_code === "number" &&
|
|
1299
|
+
result.value.exit_code !== 0) {
|
|
1300
|
+
return result.value.exit_code >= 1 && result.value.exit_code <= 255
|
|
1301
|
+
? result.value.exit_code
|
|
1302
|
+
: EXIT_CODES.rejected;
|
|
1303
|
+
}
|
|
1011
1304
|
return EXIT_CODES.success;
|
|
1012
1305
|
}
|
|
1013
1306
|
catch {
|