nawabari 0.3.0 → 0.4.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/README.md +85 -4
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +215 -31
- package/dist/cli.js.map +1 -1
- package/dist/contract.js +14 -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.js +1 -0
- package/dist/domain/session-backend.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.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 +25 -0
- package/dist/session-registry.js +47 -47
- package/dist/session-registry.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -5,10 +5,51 @@ agents. It gives each active session one exclusively owned worktree and one
|
|
|
5
5
|
mutable branch. It works without GitHub, `gh`, a network connection, Mottainai,
|
|
6
6
|
or a particular agent runtime.
|
|
7
7
|
|
|
8
|
-
Nawabari governs operations routed through Nawabari.
|
|
9
|
-
ownership boundary, not an operating-system or filesystem
|
|
10
|
-
that already has filesystem permissions can still edit
|
|
11
|
-
directly.
|
|
8
|
+
Nawabari governs operations routed through Nawabari. By default it is an
|
|
9
|
+
authorization and ownership boundary, not an operating-system or filesystem
|
|
10
|
+
sandbox: a process that already has filesystem permissions can still edit
|
|
11
|
+
another worktree directly. This default (legacy) mode is unchanged.
|
|
12
|
+
|
|
13
|
+
The opt-in protected session mode defines a Linux-only OS/filesystem/process
|
|
14
|
+
enforcement boundary underneath the existing session/worktree/resource
|
|
15
|
+
authority (`src/domain/sandbox.ts`, contract
|
|
16
|
+
`nawabari.sandbox-execution.v1`). It binds one existing Nawabari session
|
|
17
|
+
resolved through the authoritative registry/guard path to a typed sandbox
|
|
18
|
+
execution request; it does not create a second session identity. A
|
|
19
|
+
machine-readable capability/doctor report distinguishes required Linux
|
|
20
|
+
primitives (bubblewrap, user/mount/PID/IPC/UTS namespaces) from optional
|
|
21
|
+
defense-in-depth primitives (cgroups v2, Landlock, seccomp, capability
|
|
22
|
+
inspection). When protected execution is requested and a required capability
|
|
23
|
+
is unavailable or the platform is unsupported, resolution fails closed and
|
|
24
|
+
never returns a request that claims the legacy unsandboxed path is
|
|
25
|
+
protected. The lower-level contract remains responsible only for capability
|
|
26
|
+
detection and the typed request/result shape; `resolveSandboxExecutionRequest()`
|
|
27
|
+
returns a request without invoking bubblewrap. The managed `session run`
|
|
28
|
+
launcher consumes that request and establishes the protected boundary. Network
|
|
29
|
+
mode is honestly reported as `inherited` (shared with the host), not isolated.
|
|
30
|
+
|
|
31
|
+
`git nawabari session run --session <id> -- <command> [args...]` is the
|
|
32
|
+
managed protected execution entry point. It resolves the existing session
|
|
33
|
+
through the normal guard authority and compiles a fixed bubblewrap argv; the
|
|
34
|
+
command is passed after an argv terminator and is never interpreted by a
|
|
35
|
+
shell. Resolution or launch failure never falls back to the legacy ambient
|
|
36
|
+
filesystem view.
|
|
37
|
+
|
|
38
|
+
The canonical profile starts from a private root, mounts only the owned
|
|
39
|
+
worktree read-write, gives each session private `/tmp`, `/proc`, HOME and
|
|
40
|
+
cache state, and exposes no sibling worktree or control-plane path. A small
|
|
41
|
+
repository-owned `nawabari/sandbox/shared-home` subtree is the only HOME state
|
|
42
|
+
shared between sessions. Selected host user-tool directories (`~/.local/bin`
|
|
43
|
+
and pnpm's user bin when present) are read-only; credentials and the rest of
|
|
44
|
+
the host HOME are not mounted. `/dev`, system certificates/configuration, and
|
|
45
|
+
the detected runtime are explicit read-only/runtime inputs.
|
|
46
|
+
|
|
47
|
+
On standalone Linux the profile uses existing `/usr`, `/bin`, `/lib*` and
|
|
48
|
+
selected `/etc` paths only when present. On NixOS it additionally selects
|
|
49
|
+
`/nix/store`, `/run/current-system`, `/run/wrappers`, and the per-user profile
|
|
50
|
+
when present; no `/usr` layout is assumed. Missing required paths or namespace
|
|
51
|
+
support produces a stable capability/topology error. Network remains
|
|
52
|
+
inherited by design.
|
|
12
53
|
|
|
13
54
|
## Install
|
|
14
55
|
|
|
@@ -41,6 +82,13 @@ result-schema versions, identity fields, and stable `failure_codes`. The
|
|
|
41
82
|
package version is release metadata; it is not a substitute for the
|
|
42
83
|
machine-contract identifier.
|
|
43
84
|
|
|
85
|
+
The `resource-claims` capability additionally exposes a machine-readable
|
|
86
|
+
`claim_set_replacement` object (`commands`, `atomic: true`,
|
|
87
|
+
`pairing: "adjacent-resource-mode"`, `idempotent_retry: true`,
|
|
88
|
+
`unchanged_on_rejection: true`) describing the atomic multi-claim replacement
|
|
89
|
+
surface documented above, so a caller can discover this contract instead of
|
|
90
|
+
assuming it from the CLI help text.
|
|
91
|
+
|
|
44
92
|
The supported standalone sequence is:
|
|
45
93
|
|
|
46
94
|
```text
|
|
@@ -182,6 +230,28 @@ git nawabari session update --session "$NAWABARI_SESSION_ID" \
|
|
|
182
230
|
git nawabari session release --session "$NAWABARI_SESSION_ID" --json
|
|
183
231
|
```
|
|
184
232
|
|
|
233
|
+
`session update` (and its `resource update` alias) atomically replaces a
|
|
234
|
+
session's _complete_ claim set in one `updateClaims()` transaction, backed by
|
|
235
|
+
the same repository lock as every other mutation. `--resource`/`--mode` are
|
|
236
|
+
repeatable to submit a multi-resource desired set in a single call; each
|
|
237
|
+
`--resource` must be immediately followed by its own `--mode`, so pairing is
|
|
238
|
+
positional adjacency rather than flag order and repeated resources can never
|
|
239
|
+
be associated with the wrong mode:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
git nawabari session update --session "$NAWABARI_SESSION_ID" \
|
|
243
|
+
--resource src/a.ts --mode exclusive-write \
|
|
244
|
+
--resource src/b.ts --mode exclusive-write \
|
|
245
|
+
--json
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
If any requested claim in the set is invalid or conflicts, the whole update
|
|
249
|
+
is rejected and the session's prior claim set is left unchanged; no partial
|
|
250
|
+
or empty intermediate claim state is ever observable. Submitting the same
|
|
251
|
+
complete desired set again is idempotent. A successful replacement's JSON
|
|
252
|
+
exposes the resulting `claims` together with machine-readable `added` and
|
|
253
|
+
`released` claims.
|
|
254
|
+
|
|
185
255
|
The modes have these normative meanings:
|
|
186
256
|
|
|
187
257
|
- `read`: a non-mutating access declaration. It is not a consistency lease,
|
|
@@ -267,6 +337,17 @@ worktree=$(printf '%s' "$created" | jq -r .worktree)
|
|
|
267
337
|
git nawabari session close --session "$session_id" --json
|
|
268
338
|
```
|
|
269
339
|
|
|
340
|
+
For a worker that must receive the protected filesystem/process boundary, use
|
|
341
|
+
the managed launcher instead of starting the worker directly:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
git nawabari session run --session "$session_id" -- node worker.js
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The launcher owns only the child sandbox topology and process attachment. The
|
|
348
|
+
session registry, worktree/branch ownership, resource claims and lifecycle
|
|
349
|
+
remain the authoritative Nawabari domain state.
|
|
350
|
+
|
|
270
351
|
The orchestrator owns scheduling, prompts, and worker lifetime; Nawabari owns
|
|
271
352
|
only local session identity, worktree/branch ownership, and safe cleanup. No
|
|
272
353
|
Mottainai, GitHub, `gh`, network, or agent-runtime dependency is required.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { type DomainResult } from "./domain/errors.js";
|
|
1
2
|
import { type SessionBackend } from "./domain/session.js";
|
|
2
3
|
import { type CliIO } from "./presentation.js";
|
|
4
|
+
import { type SandboxCommand, type SandboxExecutionResult, type SandboxProbe, type SandboxRuntimeLayout } from "./domain/sandbox.js";
|
|
3
5
|
export type CliDependencies = {
|
|
4
6
|
backend?: SessionBackend;
|
|
5
7
|
cwd?: string;
|
|
6
8
|
io?: CliIO;
|
|
7
9
|
version?: string;
|
|
10
|
+
sandboxRunner?: (request: import("./domain/sandbox.js").SandboxExecutionRequest, command: SandboxCommand) => Promise<DomainResult<SandboxExecutionResult>>;
|
|
11
|
+
sandboxProbe?: SandboxProbe;
|
|
12
|
+
sandboxRuntimeLayout?: SandboxRuntimeLayout;
|
|
8
13
|
};
|
|
9
14
|
export declare function runCli(argv: string[], dependencies?: CliDependencies): Promise<number>;
|
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;
|
|
@@ -46,6 +47,16 @@ const HELP_COMMANDS = [
|
|
|
46
47
|
usage: `${CLI_NAME} session show [--session <id>]`,
|
|
47
48
|
options: [option("--session", "Select a session instead of the current worktree owner", { value: "<id>" })],
|
|
48
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: "session run",
|
|
52
|
+
summary: "Run one command inside the protected session sandbox",
|
|
53
|
+
usage: `${CLI_NAME} session run [--session <id>] -- <command> [args...]`,
|
|
54
|
+
options: [option("--session", "Select the active owned session", { value: "<id>" })],
|
|
55
|
+
notes: [
|
|
56
|
+
"The -- terminator is mandatory. The command is passed as argv without a shell, and protected execution is fail-closed.",
|
|
57
|
+
"session exec is an alias.",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
49
60
|
{
|
|
50
61
|
name: "session list",
|
|
51
62
|
summary: "List bounded repository session records",
|
|
@@ -69,14 +80,25 @@ const HELP_COMMANDS = [
|
|
|
69
80
|
},
|
|
70
81
|
{
|
|
71
82
|
name: "session update",
|
|
72
|
-
summary: "
|
|
73
|
-
usage: `${CLI_NAME} session update --resource <path-or-glob> --mode <read|write|exclusive-write> [--session <id>]`,
|
|
83
|
+
summary: "Atomically replace a session's complete resource claim set",
|
|
84
|
+
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
85
|
options: [
|
|
75
|
-
option("--resource", "Repository-relative resource
|
|
76
|
-
|
|
86
|
+
option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
|
|
87
|
+
value: "<path-or-glob>",
|
|
88
|
+
required: true,
|
|
89
|
+
}),
|
|
90
|
+
option("--mode", "Mode for the --resource immediately before it; repeatable", {
|
|
91
|
+
value: "<read|write|exclusive-write>",
|
|
92
|
+
required: true,
|
|
93
|
+
}),
|
|
77
94
|
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
78
95
|
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
79
96
|
],
|
|
97
|
+
notes: [
|
|
98
|
+
"The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
|
|
99
|
+
"on any invalid or conflicting claim the prior set is left unchanged.",
|
|
100
|
+
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
101
|
+
],
|
|
80
102
|
},
|
|
81
103
|
{
|
|
82
104
|
name: "session claims",
|
|
@@ -101,9 +123,25 @@ const HELP_COMMANDS = [
|
|
|
101
123
|
},
|
|
102
124
|
{
|
|
103
125
|
name: "resource update",
|
|
104
|
-
summary: "
|
|
105
|
-
usage: `${CLI_NAME} resource update --resource <path-or-glob> --mode <read|write|exclusive-write
|
|
106
|
-
options: [
|
|
126
|
+
summary: "Atomically replace a session's complete resource claim set (alias)",
|
|
127
|
+
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>]`,
|
|
128
|
+
options: [
|
|
129
|
+
option("--resource", "Repository-relative resource; repeatable, each paired with the --mode immediately after it", {
|
|
130
|
+
value: "<path-or-glob>",
|
|
131
|
+
required: true,
|
|
132
|
+
}),
|
|
133
|
+
option("--mode", "Mode for the --resource immediately before it; repeatable", {
|
|
134
|
+
value: "<read|write|exclusive-write>",
|
|
135
|
+
required: true,
|
|
136
|
+
}),
|
|
137
|
+
option("--session", "Target active session; omitted resolves the current owner", { value: "<id>" }),
|
|
138
|
+
option("--repository", "Expected repository identity", { value: "<id>" }),
|
|
139
|
+
],
|
|
140
|
+
notes: [
|
|
141
|
+
"The desired claim set fully replaces the session's current claims in one updateClaims() transaction; " +
|
|
142
|
+
"on any invalid or conflicting claim the prior set is left unchanged.",
|
|
143
|
+
"Each --resource must be immediately followed by its own --mode; pairing is positional adjacency, not flag order.",
|
|
144
|
+
],
|
|
107
145
|
},
|
|
108
146
|
{
|
|
109
147
|
name: "resource list",
|
|
@@ -246,7 +284,9 @@ function helpSpecFor(commandArguments) {
|
|
|
246
284
|
return ROOT_HELP_SPEC;
|
|
247
285
|
let key = commandArguments[0] === "resource"
|
|
248
286
|
? `resource ${commandArguments[1] ?? "list"}`
|
|
249
|
-
: commandArguments
|
|
287
|
+
: commandArguments[0] === "session" && commandArguments[1] === "exec"
|
|
288
|
+
? "session run"
|
|
289
|
+
: commandArguments.slice(0, 2).join(" ");
|
|
250
290
|
if (key === "resource claims")
|
|
251
291
|
key = "resource list";
|
|
252
292
|
const direct = HELP_COMMANDS.find((spec) => spec.name === key);
|
|
@@ -348,7 +388,17 @@ function parseGlobalArguments(argv) {
|
|
|
348
388
|
let help = false;
|
|
349
389
|
let version = false;
|
|
350
390
|
const commandArguments = [];
|
|
391
|
+
let passthrough = false;
|
|
351
392
|
for (const argument of argv) {
|
|
393
|
+
if (passthrough) {
|
|
394
|
+
commandArguments.push(argument);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (argument === "--") {
|
|
398
|
+
passthrough = true;
|
|
399
|
+
commandArguments.push(argument);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
352
402
|
if (argument === "--json") {
|
|
353
403
|
json = true;
|
|
354
404
|
}
|
|
@@ -498,6 +548,83 @@ function parseOptions(arguments_, allowed) {
|
|
|
498
548
|
function noOptions(arguments_) {
|
|
499
549
|
return parseOptions(arguments_, new Set());
|
|
500
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* `update` accepts a complete desired claim set as repeated
|
|
553
|
+
* `--resource <path> --mode <mode>` pairs. Pairing is by strict local
|
|
554
|
+
* adjacency (each `--resource` must be immediately followed by its own
|
|
555
|
+
* `--mode`, with no other option able to intervene) rather than by
|
|
556
|
+
* parallel-array position, so argv order can never associate a resource
|
|
557
|
+
* with the wrong mode.
|
|
558
|
+
*/
|
|
559
|
+
function parseClaimReplacementPairs(arguments_) {
|
|
560
|
+
let sessionId = null;
|
|
561
|
+
let repository = null;
|
|
562
|
+
const pairs = [];
|
|
563
|
+
let pendingResource = null;
|
|
564
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
565
|
+
const { name, inlineValue } = optionParts(arguments_[index]);
|
|
566
|
+
if (name !== "--session" && name !== "--repository" && name !== "--resource" && name !== "--mode") {
|
|
567
|
+
return failure(usageError("INVALID_ARGUMENT", `Unknown option: ${name}.`, { option: name }));
|
|
568
|
+
}
|
|
569
|
+
if (pendingResource !== null && name !== "--mode") {
|
|
570
|
+
return failure(usageError("INVALID_ARGUMENT", `${name} cannot appear between --resource and its --mode; only --mode is permitted immediately after --resource.`, { option: name }));
|
|
571
|
+
}
|
|
572
|
+
const value = inlineValue ?? arguments_[index + 1];
|
|
573
|
+
if (value === undefined || value === "" || (inlineValue === null && value.startsWith("-"))) {
|
|
574
|
+
return failure(usageError("MISSING_ARGUMENT", `${name} requires a value.`, { option: name }));
|
|
575
|
+
}
|
|
576
|
+
if (inlineValue === null)
|
|
577
|
+
index += 1;
|
|
578
|
+
if (name === "--session")
|
|
579
|
+
sessionId = value;
|
|
580
|
+
else if (name === "--repository")
|
|
581
|
+
repository = value;
|
|
582
|
+
else if (name === "--resource")
|
|
583
|
+
pendingResource = value;
|
|
584
|
+
else {
|
|
585
|
+
if (pendingResource === null) {
|
|
586
|
+
return failure(usageError("INVALID_ARGUMENT", "--mode must be immediately preceded by its own --resource.", {
|
|
587
|
+
option: "--mode",
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
pairs.push({ resource: pendingResource, mode: value });
|
|
591
|
+
pendingResource = null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (pendingResource !== null) {
|
|
595
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource must be immediately followed by --mode.", { option: "--mode" }));
|
|
596
|
+
}
|
|
597
|
+
if (pairs.length === 0) {
|
|
598
|
+
return failure(usageError("MISSING_ARGUMENT", "--resource requires a value.", { option: "--resource" }));
|
|
599
|
+
}
|
|
600
|
+
return { ok: true, value: { session_id: sessionId, repository, pairs } };
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* `claim` accepts exactly one `--resource`/`--mode` pair. Reuses the same
|
|
604
|
+
* strict-adjacency scan as `update` so a second `--resource`/`--mode` (in
|
|
605
|
+
* any order) is rejected outright instead of silently overwriting the
|
|
606
|
+
* first pair (last-wins).
|
|
607
|
+
*/
|
|
608
|
+
function parseSingleClaimPair(arguments_) {
|
|
609
|
+
const parsed = parseClaimReplacementPairs(arguments_);
|
|
610
|
+
if (!parsed.ok)
|
|
611
|
+
return parsed;
|
|
612
|
+
if (parsed.value.pairs.length > 1) {
|
|
613
|
+
return failure(usageError("INVALID_ARGUMENT", "claim accepts exactly one --resource/--mode pair.", {
|
|
614
|
+
pair_count: parsed.value.pairs.length,
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
const [pair] = parsed.value.pairs;
|
|
618
|
+
return {
|
|
619
|
+
ok: true,
|
|
620
|
+
value: {
|
|
621
|
+
session_id: parsed.value.session_id,
|
|
622
|
+
repository: parsed.value.repository,
|
|
623
|
+
resource: pair.resource,
|
|
624
|
+
mode: pair.mode,
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
}
|
|
501
628
|
function sessionListingOptions(parsed) {
|
|
502
629
|
const parseInteger = (option, value) => {
|
|
503
630
|
if (value === null)
|
|
@@ -557,25 +684,33 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
557
684
|
if (subcommand === undefined) {
|
|
558
685
|
return failure(usageError("MISSING_ARGUMENT", "session requires a subcommand."));
|
|
559
686
|
}
|
|
560
|
-
if (subcommand === "claim"
|
|
561
|
-
const parsed =
|
|
687
|
+
if (subcommand === "claim") {
|
|
688
|
+
const parsed = parseSingleClaimPair(rest);
|
|
562
689
|
if (!parsed.ok)
|
|
563
690
|
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 = {
|
|
691
|
+
if (dependencies.backend.claimResources === undefined)
|
|
692
|
+
return claimCapabilityUnavailable(subcommand);
|
|
693
|
+
const result = await dependencies.backend.claimResources(context, {
|
|
571
694
|
session_id: parsed.value.session_id,
|
|
572
695
|
repository: parsed.value.repository,
|
|
573
696
|
claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
|
|
697
|
+
});
|
|
698
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
699
|
+
}
|
|
700
|
+
if (subcommand === "update") {
|
|
701
|
+
const parsed = parseClaimReplacementPairs(rest);
|
|
702
|
+
if (!parsed.ok)
|
|
703
|
+
return parsed;
|
|
704
|
+
if (dependencies.backend.updateClaims === undefined)
|
|
577
705
|
return claimCapabilityUnavailable(subcommand);
|
|
578
|
-
const result = await
|
|
706
|
+
const result = await dependencies.backend.updateClaims(context, {
|
|
707
|
+
session_id: parsed.value.session_id,
|
|
708
|
+
repository: parsed.value.repository,
|
|
709
|
+
claims: parsed.value.pairs.map((pair) => ({
|
|
710
|
+
resource: pair.resource,
|
|
711
|
+
mode: pair.mode,
|
|
712
|
+
})),
|
|
713
|
+
});
|
|
579
714
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
580
715
|
}
|
|
581
716
|
if (subcommand === "claims") {
|
|
@@ -612,6 +747,28 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
612
747
|
const result = await dependencies.backend.createSession(context, options);
|
|
613
748
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
614
749
|
}
|
|
750
|
+
if (subcommand === "run" || subcommand === "exec") {
|
|
751
|
+
const delimiter = rest.indexOf("--");
|
|
752
|
+
if (delimiter === -1) {
|
|
753
|
+
return failure(usageError("MISSING_ARGUMENT", "session run requires a -- terminator before the command."));
|
|
754
|
+
}
|
|
755
|
+
const parsed = parseOptions(rest.slice(0, delimiter), new Set(["--session"]));
|
|
756
|
+
if (!parsed.ok)
|
|
757
|
+
return parsed;
|
|
758
|
+
const command = rest[delimiter + 1];
|
|
759
|
+
if (command === undefined || command.length === 0) {
|
|
760
|
+
return failure(usageError("MISSING_ARGUMENT", "session run requires a command after --."));
|
|
761
|
+
}
|
|
762
|
+
const request = await resolveSandboxExecutionRequest(dependencies.backend, context, { session_id: parsed.value.session_id, enforce: true }, dependencies.sandboxProbe, dependencies.sandboxRuntimeLayout);
|
|
763
|
+
if (!request.ok)
|
|
764
|
+
return request;
|
|
765
|
+
const runner = dependencies.sandboxRunner ?? runSandboxedCommand;
|
|
766
|
+
const result = await runner(request.value, {
|
|
767
|
+
command,
|
|
768
|
+
args: rest.slice(delimiter + 2),
|
|
769
|
+
});
|
|
770
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
771
|
+
}
|
|
615
772
|
if (subcommand === "id" || subcommand === "show" || subcommand === "close") {
|
|
616
773
|
if (subcommand === "id") {
|
|
617
774
|
const parsed = noOptions(rest);
|
|
@@ -647,23 +804,35 @@ async function executeCommand(commandArguments, dependencies) {
|
|
|
647
804
|
}
|
|
648
805
|
if (command === "resource") {
|
|
649
806
|
const resourceSubcommand = subcommand ?? "list";
|
|
650
|
-
if (resourceSubcommand === "claim"
|
|
651
|
-
const parsed =
|
|
807
|
+
if (resourceSubcommand === "claim") {
|
|
808
|
+
const parsed = parseSingleClaimPair(rest);
|
|
652
809
|
if (!parsed.ok)
|
|
653
810
|
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)
|
|
811
|
+
if (dependencies.backend.claimResources === undefined)
|
|
659
812
|
return claimCapabilityUnavailable(resourceSubcommand);
|
|
660
|
-
const result = await
|
|
813
|
+
const result = await dependencies.backend.claimResources(context, {
|
|
661
814
|
session_id: parsed.value.session_id,
|
|
662
815
|
repository: parsed.value.repository,
|
|
663
816
|
claims: [{ resource: parsed.value.resource, mode: parsed.value.mode }],
|
|
664
817
|
});
|
|
665
818
|
return result.ok ? { ok: true, value: result.value } : result;
|
|
666
819
|
}
|
|
820
|
+
if (resourceSubcommand === "update") {
|
|
821
|
+
const parsed = parseClaimReplacementPairs(rest);
|
|
822
|
+
if (!parsed.ok)
|
|
823
|
+
return parsed;
|
|
824
|
+
if (dependencies.backend.updateClaims === undefined)
|
|
825
|
+
return claimCapabilityUnavailable(resourceSubcommand);
|
|
826
|
+
const result = await dependencies.backend.updateClaims(context, {
|
|
827
|
+
session_id: parsed.value.session_id,
|
|
828
|
+
repository: parsed.value.repository,
|
|
829
|
+
claims: parsed.value.pairs.map((pair) => ({
|
|
830
|
+
resource: pair.resource,
|
|
831
|
+
mode: pair.mode,
|
|
832
|
+
})),
|
|
833
|
+
});
|
|
834
|
+
return result.ok ? { ok: true, value: result.value } : result;
|
|
835
|
+
}
|
|
667
836
|
if (resourceSubcommand === "list" || resourceSubcommand === "claims") {
|
|
668
837
|
const parsed = parseOptions(rest, new Set(["--session"]));
|
|
669
838
|
if (!parsed.ok)
|
|
@@ -963,7 +1132,9 @@ function emitFailure(mode, command, error, io) {
|
|
|
963
1132
|
}
|
|
964
1133
|
export async function runCli(argv, dependencies = {}) {
|
|
965
1134
|
const io = dependencies.io ?? defaultCliIO();
|
|
966
|
-
const
|
|
1135
|
+
const delimiter = argv.indexOf("--");
|
|
1136
|
+
const globalArguments = delimiter === -1 ? argv : argv.slice(0, delimiter);
|
|
1137
|
+
const mode = globalArguments.includes("--json") ? "json" : "human";
|
|
967
1138
|
const parsed = parseGlobalArguments(argv);
|
|
968
1139
|
if (!parsed.ok)
|
|
969
1140
|
return emitFailure(mode, "cli", parsed.error, io);
|
|
@@ -1004,10 +1175,23 @@ export async function runCli(argv, dependencies = {}) {
|
|
|
1004
1175
|
const cwd = dependencies.cwd ?? process.cwd();
|
|
1005
1176
|
try {
|
|
1006
1177
|
const backend = dependencies.backend ?? createLocalSessionBackend();
|
|
1007
|
-
const result = await executeCommand(parsed.value.commandArguments, {
|
|
1178
|
+
const result = await executeCommand(parsed.value.commandArguments, {
|
|
1179
|
+
backend,
|
|
1180
|
+
cwd,
|
|
1181
|
+
sandboxRunner: dependencies.sandboxRunner,
|
|
1182
|
+
sandboxProbe: dependencies.sandboxProbe,
|
|
1183
|
+
sandboxRuntimeLayout: dependencies.sandboxRuntimeLayout,
|
|
1184
|
+
});
|
|
1008
1185
|
if (!result.ok)
|
|
1009
1186
|
return emitFailure(mode, command, result.error, io);
|
|
1010
1187
|
io.stdout(renderSuccess(mode, command, result.value));
|
|
1188
|
+
if ((command === "session run" || command === "session exec") &&
|
|
1189
|
+
typeof result.value.exit_code === "number" &&
|
|
1190
|
+
result.value.exit_code !== 0) {
|
|
1191
|
+
return result.value.exit_code >= 1 && result.value.exit_code <= 255
|
|
1192
|
+
? result.value.exit_code
|
|
1193
|
+
: EXIT_CODES.rejected;
|
|
1194
|
+
}
|
|
1011
1195
|
return EXIT_CODES.success;
|
|
1012
1196
|
}
|
|
1013
1197
|
catch {
|