nawabari 0.7.1 → 0.9.2

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.
Files changed (90) hide show
  1. package/README.md +264 -29
  2. package/dist/cli-command-registry.d.ts +50 -0
  3. package/dist/cli-command-registry.js +509 -0
  4. package/dist/cli-command-registry.js.map +1 -0
  5. package/dist/cli.d.ts +18 -0
  6. package/dist/cli.js +302 -583
  7. package/dist/cli.js.map +1 -1
  8. package/dist/contract.d.ts +6 -0
  9. package/dist/contract.js +296 -192
  10. package/dist/contract.js.map +1 -1
  11. package/dist/domain/cgroups-v2.d.ts +77 -0
  12. package/dist/domain/cgroups-v2.js +434 -0
  13. package/dist/domain/cgroups-v2.js.map +1 -0
  14. package/dist/domain/doctor.d.ts +4 -1
  15. package/dist/domain/doctor.js +64 -13
  16. package/dist/domain/doctor.js.map +1 -1
  17. package/dist/domain/errors.d.ts +1 -1
  18. package/dist/domain/errors.js.map +1 -1
  19. package/dist/domain/landlock.d.ts +55 -0
  20. package/dist/domain/landlock.js +279 -0
  21. package/dist/domain/landlock.js.map +1 -0
  22. package/dist/domain/runtime.d.ts +12 -0
  23. package/dist/domain/runtime.js +50 -0
  24. package/dist/domain/runtime.js.map +1 -0
  25. package/dist/domain/sandbox-launcher.d.ts +26 -0
  26. package/dist/domain/sandbox-launcher.js +312 -23
  27. package/dist/domain/sandbox-launcher.js.map +1 -1
  28. package/dist/domain/sandbox-seccomp.d.ts +46 -0
  29. package/dist/domain/sandbox-seccomp.js +302 -0
  30. package/dist/domain/sandbox-seccomp.js.map +1 -0
  31. package/dist/domain/sandbox.d.ts +45 -1
  32. package/dist/domain/sandbox.js +176 -25
  33. package/dist/domain/sandbox.js.map +1 -1
  34. package/dist/domain/session-backend.d.ts +4 -1
  35. package/dist/domain/session-backend.js +256 -3
  36. package/dist/domain/session-backend.js.map +1 -1
  37. package/dist/domain/session.d.ts +207 -3
  38. package/dist/domain/session.js.map +1 -1
  39. package/dist/errors.d.ts +1 -1
  40. package/dist/failure-code-vocabulary.d.ts +20 -0
  41. package/dist/failure-code-vocabulary.js +216 -0
  42. package/dist/failure-code-vocabulary.js.map +1 -0
  43. package/dist/git.js +92 -6
  44. package/dist/git.js.map +1 -1
  45. package/dist/operation-authorization.d.ts +1 -1
  46. package/dist/product-state-manifest.d.ts +152 -0
  47. package/dist/product-state-manifest.js +240 -0
  48. package/dist/product-state-manifest.js.map +1 -0
  49. package/dist/public-contract.d.ts +25 -0
  50. package/dist/public-contract.js +29 -0
  51. package/dist/public-contract.js.map +1 -0
  52. package/dist/public-state.d.ts +75 -0
  53. package/dist/public-state.js +60 -0
  54. package/dist/public-state.js.map +1 -0
  55. package/dist/registry/lock.d.ts +8 -0
  56. package/dist/registry/lock.js +38 -20
  57. package/dist/registry/lock.js.map +1 -1
  58. package/dist/repository-evidence.d.ts +16 -0
  59. package/dist/repository-evidence.js.map +1 -1
  60. package/dist/resource-claims.d.ts +2 -2
  61. package/dist/resource-claims.js +96 -27
  62. package/dist/resource-claims.js.map +1 -1
  63. package/dist/session-lifecycle-actions.d.ts +64 -0
  64. package/dist/session-lifecycle-actions.js +117 -0
  65. package/dist/session-lifecycle-actions.js.map +1 -0
  66. package/dist/session-lifecycle-classification.d.ts +120 -0
  67. package/dist/session-lifecycle-classification.js +88 -0
  68. package/dist/session-lifecycle-classification.js.map +1 -0
  69. package/dist/session-registry.d.ts +210 -2
  70. package/dist/session-registry.js +1330 -87
  71. package/dist/session-registry.js.map +1 -1
  72. package/dist/state/index.d.ts +5 -0
  73. package/dist/state/index.js +4 -0
  74. package/dist/state/index.js.map +1 -0
  75. package/dist/state/session/actors.d.ts +71 -0
  76. package/dist/state/session/actors.js +259 -0
  77. package/dist/state/session/actors.js.map +1 -0
  78. package/dist/state/session/guards.d.ts +25 -0
  79. package/dist/state/session/guards.js +108 -0
  80. package/dist/state/session/guards.js.map +1 -0
  81. package/dist/state/session/index.d.ts +6 -0
  82. package/dist/state/session/index.js +5 -0
  83. package/dist/state/session/index.js.map +1 -0
  84. package/dist/state/session/machine.d.ts +185 -0
  85. package/dist/state/session/machine.js +408 -0
  86. package/dist/state/session/machine.js.map +1 -0
  87. package/dist/state/session/types.d.ts +94 -0
  88. package/dist/state/session/types.js +14 -0
  89. package/dist/state/session/types.js.map +1 -0
  90. package/package.json +46 -26
package/README.md CHANGED
@@ -17,9 +17,11 @@ authority (`src/domain/sandbox.ts`, contract
17
17
  resolved through the authoritative registry/guard path to a typed sandbox
18
18
  execution request; it does not create a second session identity. A
19
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
20
+ primitives (bubblewrap, user/mount/PID/IPC/UTS namespaces, the versioned
21
+ seccomp baseline, and capability reduction) from optional defense-in-depth
22
+ primitives (cgroups v2 and Landlock). The sandbox report exposes Landlock's
23
+ observed ABI, support, and effective state (`available`, `enforced`,
24
+ `reduced-defense`, `incompatible`, or `error`). When protected execution is requested and a required capability
23
25
  is unavailable or the platform is unsupported, resolution fails closed and
24
26
  never returns a request that claims the legacy unsandboxed path is
25
27
  protected. The lower-level contract remains responsible only for capability
@@ -44,15 +46,43 @@ and pnpm's user bin when present) are read-only; credentials and the rest of
44
46
  the host HOME are not mounted. `/dev`, system certificates/configuration, and
45
47
  the detected runtime are explicit read-only/runtime inputs.
46
48
 
49
+ When the host exposes a compatible Landlock ABI, the protected launcher applies
50
+ a rule set derived from this same topology inside bubblewrap. An unavailable
51
+ or incompatible optional ABI leaves bubblewrap active and reports
52
+ `reduced-defense`; an adapter setup failure fails closed, and a profile that
53
+ explicitly requires Landlock also fails closed when its adapter cannot be
54
+ established. Setup diagnostics are bounded and no ambient/unsandboxed retry is
55
+ attempted.
56
+
47
57
  On standalone Linux the profile uses existing `/usr`, `/bin`, `/lib*` and
48
58
  selected `/etc` paths only when present. On NixOS it additionally selects
49
59
  `/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.
60
+ when present; broad FHS views (`/usr`, `/bin`, `/lib*`) are not selected when
61
+ the NixOS closure roots are present. Missing required paths or namespace
62
+ support produces a stable capability/topology error. The protected child uses
63
+ `nawabari.seccomp.v1`, a compatibility-first deny-list whose policy denials
64
+ return `EPERM` rather than hanging or terminating ordinary development
65
+ subprocess trees. Ambient capabilities are empty (`--cap-drop ALL`). Network
66
+ remains inherited by design.
67
+
68
+ The canonical Mottainai NixOS Runtime fixture can run the opt-in compatibility
69
+ conformance matrix as its unprivileged repository principal:
70
+
71
+ ```bash
72
+ NAWABARI_NIXOS_RUNTIME_CONFORMANCE=1 \
73
+ node --test --import tsx src/domain/nixos-runtime-compat.test.ts
74
+ ```
75
+
76
+ This invokes the normal `session run` CLI route and production launcher. It
77
+ checks explicit NixOS closure paths, private versus repository-shared HOME
78
+ state, Git/checkpoint authority, representative toolchain subprocesses, and
79
+ sequential/concurrent session isolation. It does not create a test-only
80
+ sandbox or treat Mottainai state as Nawabari authority.
53
81
 
54
82
  ## Install
55
83
 
84
+ Nawabari requires Node.js >=24.
85
+
56
86
  ```bash
57
87
  npm install -g nawabari
58
88
  ```
@@ -82,6 +112,17 @@ result-schema versions, identity fields, and stable `failure_codes`. The
82
112
  package version is release metadata; it is not a substitute for the
83
113
  machine-contract identifier.
84
114
 
115
+ The `protected-execution` capability separately advertises the versioned
116
+ `nawabari.sandbox-execution.v1` contract, its required and optional host
117
+ capabilities, the canonical `session run` entry point and `session exec` alias,
118
+ `network_mode: "inherited"`, and fail-closed behavior. Use
119
+ `nawabari doctor --json` to inspect the current host without creating or
120
+ selecting a session. Its `sandbox` report is produced by the same
121
+ `sandboxDoctorReport` authority used when resolving protected execution and
122
+ contains `platform_supported`, per-capability status, `ready`, and
123
+ `missing_required`. An unavailable required capability never implies an
124
+ ambient protected fallback.
125
+
85
126
  The `resource-claims` capability additionally exposes a machine-readable
86
127
  `claim_set_replacement` object (`commands`, `atomic: true`,
87
128
  `pairing: "adjacent-resource-mode"`, `idempotent_retry: true`,
@@ -126,8 +167,8 @@ The result schemas expose the following identities:
126
167
  | claims | `claim_id`, `session_id`, `resource`, `mode`, `claim_set_generation`, `previous_claim_set_generation` |
127
168
  | authorization | `operation`, `allowed`, `code`, `claim_ids` |
128
169
  | checkpoint evidence | `head`, `changed`, `staged`, `unstaged`, `untracked`, `in_claim`, `out_of_claim` |
129
- | repository evidence | `session_id`, `base_revision`, `head`, `clean`, `paths.stats`, `evidence_hash` |
130
- | bounded diff | `from_revision`, `to_revision`, `paths`, `stats`, `patch`, `evidence_hash` |
170
+ | repository evidence | `session_id`, `session_updated_at`, `base_revision`, `head`, `clean`, `paths.stats`, `evidence_hash` |
171
+ | bounded diff | `from_revision`, `to_revision`, `paths`, `stats`, `diagnostics`, `patch`, `evidence_hash` |
131
172
  | commit/push | `commit_sha`, `remote`, `branch`, `target`, `relation` |
132
173
  | reconciliation/cleanup | `clean`, `issues`, `candidates`, `cleaned`, `blocked`, `recovery_hints` |
133
174
 
@@ -138,6 +179,41 @@ The local lifecycle requires Git and the repository-local registry/lock only;
138
179
  it does not require Mottainai, GitHub, `gh`, network access, an LLM, or a
139
180
  coding-agent runtime.
140
181
 
182
+ ## Package state/contract API
183
+
184
+ The CLI/JSON surface above remains the primary integration boundary. For a
185
+ Node caller that wants public lifecycle observation/snapshot,
186
+ transition-decision data, or machine-contract discovery without spawning the
187
+ CLI and parsing its output, the package additionally exports two explicit,
188
+ stable entry points:
189
+
190
+ ```js
191
+ import { classifyNawabariState, getNawabariSessionStateSnapshot } from "nawabari/state";
192
+ import { nawabariMachineContract } from "nawabari/contract";
193
+ ```
194
+
195
+ `nawabari/state` exposes the public lifecycle vocabulary
196
+ (`NawabariLifecycleState`, `NawabariCommand`), the public observation input
197
+ (`NawabariObservation`), and the public projection produced from it
198
+ (`NawabariStateSnapshot`, `NawabariTransitionDecision`). `classifyNawabariState`
199
+ is a pure, transport-neutral projection function; `getNawabariSessionStateSnapshot`
200
+ observes one real, already-provisioned session through the same
201
+ `SessionRegistry` Git/filesystem/session-registry authority `session inspect`
202
+ uses, without mutating anything. `nawabari/contract` exposes
203
+ `nawabariMachineContract()`, a zero-argument wrapper over the same
204
+ `capabilities --json` contract that defaults to the installed package's own
205
+ version.
206
+
207
+ These are the only supported package entry points beyond the CLI binaries.
208
+ Raw XState machine/actor internals, actor refs, internal state-node ids, and
209
+ private machine context are not exported — `package.json#exports` declares no
210
+ other subpath, so a deep import such as `nawabari/dist/state/session/machine.js`
211
+ is rejected. `classifyNawabariState` only ever projects a caller-supplied observation into
212
+ a decision; it never mutates anything, and no observation or transition
213
+ decision it returns grants mutation authority. Mutating a session
214
+ (close/discard/claim/commit/push/...) still requires the existing CLI or the
215
+ `SessionRegistry` authority directly.
216
+
141
217
  ## Read-only repository evidence
142
218
 
143
219
  The evidence family is session-addressed and has no task, Issue, semantic, or
@@ -159,13 +235,24 @@ New sessions persist the exact creation/base revision as `base_revision`;
159
235
  legacy records that lack this field report `base_revision: null` and
160
236
  `base_revision_proven: false` rather than inferring it from a mutable ref.
161
237
 
238
+ `session_updated_at` is the UTC timestamp of the last authoritative
239
+ session-state mutation, not a general Git filesystem mtime. A successful
240
+ Nawabari-managed `commit` is such a mutation, so the registry timestamp is
241
+ persisted before the command returns and a subsequent snapshot reports it with
242
+ the resulting `head`. Git changes made outside Nawabari do not advance this
243
+ field; consumers must use the Git-observed fields for those changes.
244
+
162
245
  `diff` requires at least one explicit concrete path and never accepts a glob or
163
246
  an empty repository-wide selection. Stats are returned by default. Patch text
164
247
  requires `--patch` and is bounded to at most 64 paths, 64 KiB, and 128 hunks;
165
248
  the caller may request smaller limits. Unrepresentable Git observations fail
166
249
  with `GIT_STATE_AMBIGUOUS`; a requested path whose stat is not exposed by Git
167
250
  remains in the result with `available: false` and makes snapshot evidence
168
- `complete: false`, so no path silently disappears.
251
+ `complete: false`, so no path silently disappears. Such paths include a
252
+ bounded `diagnostics` entry; a target directly observed as untracked reports
253
+ `reason: UNTRACKED_TARGET`, while other unavailable-stat causes remain
254
+ `STAT_UNAVAILABLE`. These diagnostics are read-only and do not stage or add
255
+ files.
169
256
 
170
257
  ## Session lifecycle
171
258
 
@@ -216,8 +303,9 @@ destructive cleanup. A clean close releases only the owned worktree and
216
303
  branch, and repeating close is idempotent. `gc` detects stale or interrupted
217
304
  sessions; `--apply` uses the same close safety checks and reports blocked
218
305
  sessions instead of guessing. `gc --dry-run` performs the same non-mutating
219
- cleanup preflight and includes stable blocker codes and `recovery_hints` for
220
- every candidate that is not safe. Cleanup revalidates the physical worktree,
306
+ cleanup preflight, reports age/physical/lifecycle suspicion separately from
307
+ destructive eligibility, and includes stable blocker codes and
308
+ `recovery_hints` for every eligible candidate that is not safe. Cleanup revalidates the physical worktree,
221
309
  branch, and `HEAD` observations immediately before each destructive Git
222
310
  operation.
223
311
 
@@ -238,12 +326,15 @@ history view; closed records remain persisted and are never silently deleted
238
326
  by listing or cleanup.
239
327
 
240
328
  `gc` stale eligibility is separate from closed-history retention. Its default
241
- threshold is 24 hours (`86,400,000` ms), measured from persisted `updated_at`;
242
- records already in `stale` or `closing` state are eligible, and an otherwise
243
- live record is also eligible when Git reports its registered worktree as
244
- missing or prunable. Physical Git/worktree state is authoritative for that
245
- check. `gc --dry-run` and `gc --apply` do not treat a closed record as a stale
246
- cleanup candidate.
329
+ threshold is 24 hours (`86,400,000` ms), measured from persisted `updated_at`.
330
+ Elapsed age is diagnostic suspicion only: it never authorizes destructive
331
+ cleanup for a physically healthy active session. Records already in `stale` or
332
+ `closing` state are eligible, as is an otherwise live record when Git reports
333
+ its registered worktree as safely prunable and missing. Ambiguous physical
334
+ state remains ineligible and fail-closed. `gc --dry-run` exposes suspicion and
335
+ destructive eligibility/reason separately for each candidate; `gc --apply`
336
+ uses only eligible candidates. Closed records are never stale cleanup
337
+ candidates.
247
338
 
248
339
  `doctor` includes a non-destructive `reconciliation` check. It reports
249
340
  registry/Git ownership drift, including missing or prunable worktrees and
@@ -258,8 +349,18 @@ code. Claim JSON exposes `schema_version`, `claim_id`, `session_id`, the
258
349
  repository/worktree identities, canonical `resource`, `mode`, and timestamps.
259
350
  The claim schema version is `2` and supports `read`, `write`, and
260
351
  `exclusive-write`. Schema v1 records use different overlap semantics and are
261
- not interpreted implicitly: an embedding caller must explicitly run
262
- `SessionRegistry.migrate()` before using them.
352
+ not interpreted implicitly. If an upgraded repository reports
353
+ `UNSUPPORTED_CLAIM_SCHEMA_VERSION`, run the public migration command:
354
+
355
+ ```bash
356
+ git nawabari migrate --json
357
+ ```
358
+
359
+ Migration validates the complete legacy registry under the registry lock and
360
+ rewrites it with an atomic replace. It is idempotent and retry-safe; ambiguous
361
+ or corrupt state is rejected with bounded diagnostics. Do not hand-edit or
362
+ delete the registry. Embedding callers may use the same authority through
363
+ `SessionRegistry.migrate()`.
263
364
 
264
365
  ```bash
265
366
  git nawabari session claim --session "$NAWABARI_SESSION_ID" \
@@ -337,6 +438,14 @@ garbage-collecting a session releases its claims; no separate claim registry
337
438
  or claim lock exists. Claims describe ownership state only and do not provide
338
439
  OS-level filesystem observation or a filesystem sandbox.
339
440
 
441
+ For an existing path, canonicalization follows the physical directory entry,
442
+ so a case-insensitive filesystem cannot give `README.md` and `readme.md`
443
+ independent claim identities. This is determined from the filesystem entry
444
+ itself; paths that do not exist yet, and wildcard portions of a claim, retain
445
+ their exact lexical case because their future physical identity is unknown.
446
+ On a case-sensitive filesystem an alternate-case spelling is therefore a
447
+ distinct (possibly future) path, and no global lowercasing is applied.
448
+
340
449
  An ordinary source change uses `write` and can proceed while another session
341
450
  holds a `read` declaration:
342
451
 
@@ -389,6 +498,16 @@ identifiers, in addition to the human-readable `message`. JSON and human
389
498
  output always render the identical underlying result; only the formatting
390
499
  differs.
391
500
 
501
+ Lifecycle diagnostics additionally expose `next_action` (and the bounded
502
+ `next_actions` list) as typed, non-mutating caller actions. The action schema
503
+ is versioned independently and currently includes `retain-session`,
504
+ `supply-exact-integrated-revision`,
505
+ `retry-close-with-bounded-integration-fetch`, `discard-session`, and
506
+ `reconcile-physical-state`. A discard action always carries explicit intent;
507
+ ambiguous or terminal states never advertise destructive actions. These
508
+ fields are additive to `session-diagnostic.v1`, so existing consumers may
509
+ continue using `safe_actions`.
510
+
392
511
  - **`RESOURCE_CLAIM_CONFLICT`** (`session claim`/`session update`,
393
512
  `authorize`, `guard --operation`) reports the blocking claim
394
513
  (`ownerClaimId`, `ownerResource`, `ownerMode`) and the blocking session's
@@ -521,6 +640,16 @@ successful commit — it fails with `COMMIT_RESULT_DIVERGED`, which retains the
521
640
  resulting `commitSha` (the Git commit already happened) alongside the
522
641
  authorized, actual, and divergent path sets for recovery/reconciliation.
523
642
 
643
+ If Git reports a bounded transport failure (timeout, output limit, or spawn
644
+ failure) after the commit invocation, Nawabari re-reads the local `HEAD` and
645
+ that commit's bounded changed-path set before classifying the outcome. A
646
+ failure whose `HEAD` is unchanged is reported with `outcome: "proven-absent"`
647
+ and `retrySafe: true`; a matching new commit is returned as a successful
648
+ result with `reconciliation.outcome: "proven-committed"` and its resulting
649
+ SHA; if either observation is unavailable or does not match the authorized
650
+ paths, the failure carries `outcome: "unresolved"` and `retrySafe: false`.
651
+ Unresolved outcomes never authorize a blind retry.
652
+
524
653
  ```bash
525
654
  git nawabari commit --session "$NAWABARI_SESSION_ID" \
526
655
  --message 'record the local change' --resource src/example.ts --json
@@ -546,12 +675,19 @@ Governed push requires explicit claim-covered resources and an explicit
546
675
  `--remote`/`--branch` target. Existing upstream and local/remote relation are
547
676
  inspected before mutation. A missing upstream requires `--create-upstream`;
548
677
  behind or diverged history requires explicit `--force`. Force pushes use an
549
- exact `--force-with-lease` bound to the observed remote branch SHA; non-force
550
- pushes rely on `--no-force` without any lease option. Nawabari fetches only
551
- the explicit remote branch into a disposable ref when local ancestry is
552
- missing; it does not update tracking refs or fetch unrelated branches/tags.
678
+ exact `--force-with-lease` bound to the observed remote branch SHA. Ordinary
679
+ pushes use the same exact-generation lease, while force authorization remains
680
+ separate. A new remote branch uses an empty lease, requiring the target ref to
681
+ remain absent. Nawabari fetches only the explicit remote branch into a
682
+ disposable ref when local ancestry is missing; it does not update tracking
683
+ refs or fetch unrelated branches/tags.
553
684
  The JSON result includes the immutable `source_sha`, explicit `target_ref`,
554
- observed `observed_remote_sha`, and relation.
685
+ observed `observed_remote_sha`, and relation. If a bounded transport failure
686
+ may have happened after the remote mutation, Nawabari re-observes only that
687
+ exact remote ref. It reports `reconciliation.outcome` as `proven-pushed`,
688
+ `proven-absent`, or `unresolved`, with the exact precondition and post-failure
689
+ generation evidence. Only a proven-absent outcome is retry-safe; unresolved
690
+ outcomes never authorize a blind retry.
555
691
 
556
692
  ```bash
557
693
  git nawabari push --session "$NAWABARI_SESSION_ID" \
@@ -610,11 +746,18 @@ ID, canonical worktree and branch identities, lifecycle state, and timestamps.
610
746
 
611
747
  Ownership-changing writes use an exclusive repository-local lock and a synced
612
748
  temporary file followed by atomic replacement. Concurrent creation cannot
613
- silently duplicate an active worktree or branch. Lock recovery is conservative:
614
- the lock records a random token, PID, host, and process-start identity; an
615
- owner is reclaimed only when the same host proves that exact process identity
616
- is dead. Invalid, remote, or otherwise unverifiable lock metadata is never
617
- stolen and fails closed so an operator can inspect or remove it deliberately.
749
+ silently duplicate an active worktree or branch. Lock recovery is conservative
750
+ and platform-qualified: stale-lock reclamation is supported only on Linux,
751
+ where the lock records a random token, PID, host, and the exact process-start
752
+ token from `/proc/<pid>/stat`. An owner is reclaimed only when the same host
753
+ proves that exact process identity is dead; elapsed age and PID liveness alone
754
+ are never reclaim authority. On non-Linux platforms, Node can run ordinary
755
+ Nawabari operations but does not provide a safe process-generation identity,
756
+ so stale local locks remain `LOCK_STALE` and require deliberate operator
757
+ remediation. Invalid, remote, or otherwise unverifiable lock metadata is never
758
+ stolen and fails closed. The same limitation is machine-readable under
759
+ `capabilities --json` at the `session-lifecycle.registry_lock_recovery`
760
+ contract.
618
761
 
619
762
  ### Conformance and extraction boundary
620
763
 
@@ -639,6 +782,98 @@ The relevant Mottainai #28 execution cases are mapped as follows:
639
782
  Run `pnpm run test:package` to validate the exact packed tarball and its
640
783
  installed CLI, or `pnpm run verify` for the complete local conformance gate.
641
784
 
785
+ ## Exact packed standalone protected-execution evidence
786
+
787
+ `pnpm run test:package:protected` is the package/evidence gate for the
788
+ standalone protected product. It creates one exact `pnpm pack` archive, records its
789
+ package/version, filename, byte size, SHA-256, source revision, and host
790
+ identity in `test-artifacts/packed-standalone-protected-execution.json`,
791
+ validates the archive contents, and installs that archive into a fresh
792
+ temporary consumer with `npm install --offline`. The default evidence report
793
+ is ignored by Git; pass `--evidence-output <path>` to retain it elsewhere and
794
+ `--keep-tarball` to retain the exact archive for inspection.
795
+
796
+ The smoke test invokes only the installed `nawabari` bin. Its package allowlist
797
+ contains `dist` runtime artifacts, `README.md`, `LICENSE`, and `package.json`;
798
+ source, test, and script modules are rejected, and the installed manifest must
799
+ not declare runtime dependencies. The lifecycle proof goes through the
800
+ installed public CLI for `capabilities --json`, `doctor --json`, session
801
+ creation/resolution, `session run`, resource claims, checkpoint, commit,
802
+ local-bare-remote push, and close. The gate requires the protected doctor
803
+ report to be ready; unavailable protected execution is a failure, not a skip
804
+ or an ambient fallback. The fixture uses only a temporary local repository and
805
+ local bare remote and has no Mottainai, GitHub, `gh`, LLM, or network
806
+ dependency.
807
+
808
+ The ordinary `pnpm run test:package` command runs the package/install smoke
809
+ without requiring a real protected host, so it retains the existing
810
+ compatibility CI job; it still verifies fail-closed rejection when protection
811
+ is unavailable. The `pnpm run test:package:protected` command is the #149
812
+ evidence command and must be run on a supported Linux host. It requires a
813
+ ready protected profile and fails when that prerequisite is unavailable.
814
+
815
+ The evidence supports these boundaries only:
816
+
817
+ - Process: on Linux with the required capabilities, the canonical launcher
818
+ establishes bubblewrap user, mount, PID, IPC, and UTS namespaces, applies
819
+ the versioned seccomp profile, and drops ambient capabilities. This is not a
820
+ VM or a claim about every host process.
821
+ - Filesystem: the protected child receives the authoritative session worktree
822
+ read-write, private session HOME/cache/`/tmp`/`/proc`, repository-owned
823
+ shared HOME state, and the fixed read-only runtime/tool inputs selected by
824
+ the profile. Sibling worktrees and unselected host HOME paths are not part
825
+ of the selected topology.
826
+ - HOME/cache: the child sees `/home/nawabari`; private state is per session,
827
+ while only the repository's selected shared-home subtree is shared. Selected
828
+ host tool directories are read-only inputs, not the host HOME.
829
+ - Network: `network_mode` is `inherited`; this evidence does not prove egress
830
+ isolation.
831
+ - Linux prerequisites: use `nawabari doctor --json` on a supported Linux host.
832
+ The package's supported Node.js engine, Git, and required bubblewrap,
833
+ namespace, seccomp, and capability support must be available and ready;
834
+ cgroups v2 and Landlock remain profile-reported optional defenses.
835
+ - Failure behavior: protected resolution uses `enforce: true`; missing
836
+ required capability, unsupported topology, or launch failure returns a
837
+ bounded failure and never retries through ambient execution.
838
+
839
+ ## Packed Mottainai preselected-UID handoff evidence
840
+
841
+ `pnpm run test:package:mottainai` is the required-CI external-handoff gate for
842
+ Issue #150. `scripts/run-mottainai-uid-handoff-gate.mjs` runs the #149
843
+ protected package gate exactly once, retains that exact tarball and its
844
+ `nawabari.packed-standalone-protected-execution.v1` evidence, and passes only
845
+ those exact paths into `scripts/run-mottainai-uid-handoff.mjs`, which installs
846
+ that same tarball into a disposable consumer. `run-mottainai-uid-handoff.mjs`
847
+ itself has no self-pack path: it always requires an already-produced
848
+ `--tarball`/`--artifact-evidence` pair, so #150 can never hide a stale or
849
+ mismatched artifact behind a second pack. The fixture invokes the installed
850
+ `session run` contract under two distinct preselected unprivileged UIDs
851
+ (23001 and 23002 by default) and records bounded
852
+ artifact/fixture/UID/session/worktree/resource/Git evidence in
853
+ `test-artifacts/mottainai-packed-uid-handoff.json`. This gate runs in required
854
+ CI on a Linux host with bubblewrap (`mottainai-uid-handoff` job).
855
+
856
+ The checked-in `scripts/test-fixtures/mottainai-preselected-uid-runner.sh` is a
857
+ UID-only execution adapter. A real Mottainai Runtime may supply another
858
+ executable with the same `--uid`, `--root`, `--cwd`, `--` argv contract:
859
+
860
+ ```bash
861
+ node scripts/run-mottainai-uid-handoff.mjs \
862
+ --tarball nawabari-0.7.1.tgz \
863
+ --artifact-evidence test-artifacts/packed-standalone-protected-execution.json \
864
+ --fixture-runner /path/to/mottainai-uid-runner \
865
+ --uids 23001,23002 \
866
+ --output test-artifacts/mottainai-packed-uid-handoff.json
867
+ ```
868
+
869
+ The runner supplies only the caller's OS UID/GID view. Nawabari remains the
870
+ sole authority for session, worktree, resource claims, protected execution,
871
+ checkpoint, commit, push, local Git, and close; the fixture creates no
872
+ repository-principal registry and passes no Mottainai task, policy, or
873
+ credential semantics. Identical display names, branch names, and resource
874
+ paths are used under different canonical repository paths, and the protected
875
+ child's session/cwd marker is checked to prevent principal confusion.
876
+
642
877
  ## Development
643
878
 
644
879
  ```bash
@@ -0,0 +1,50 @@
1
+ export type CliHelpOptionSpec = {
2
+ readonly name: string;
3
+ readonly aliases?: readonly string[];
4
+ readonly alias_of?: string;
5
+ readonly value?: string;
6
+ readonly required?: boolean;
7
+ readonly default?: string;
8
+ readonly minimum?: number;
9
+ readonly maximum?: number;
10
+ readonly repeatable?: boolean;
11
+ /** Closed values projected from the authority that validates this option. */
12
+ readonly values?: readonly string[];
13
+ /** This option remains required unless one of these additive selectors is present. */
14
+ readonly required_unless?: readonly string[];
15
+ /** Selectors that cannot be combined with this option. */
16
+ readonly mutually_exclusive_with?: readonly string[];
17
+ readonly description: string;
18
+ };
19
+ export type CliCommandDefinition = {
20
+ readonly name: string;
21
+ readonly aliases?: readonly string[];
22
+ readonly summary: string;
23
+ readonly usage: string;
24
+ readonly options: readonly CliHelpOptionSpec[];
25
+ readonly notes?: readonly string[];
26
+ };
27
+ export declare const GLOBAL_HELP_OPTIONS: readonly CliHelpOptionSpec[];
28
+ /**
29
+ * Canonical public command/discovery registry.
30
+ *
31
+ * Dispatcher implementation deliberately remains below in this module. This
32
+ * registry only describes the public discovery surface; aliases point at the
33
+ * canonical command so option metadata cannot drift between projections.
34
+ */
35
+ export declare const CLI_COMMAND_REGISTRY: readonly CliCommandDefinition[];
36
+ export declare const ROOT_HELP_SPEC: CliCommandDefinition;
37
+ /** Resolve a public name through the one canonical registry. */
38
+ export declare function canonicalCommandForName(name: string): CliCommandDefinition | undefined;
39
+ /**
40
+ * Materialize public names for projections without copying option metadata.
41
+ * Alias entries are generated from their canonical definition, including the
42
+ * usage, options, and parser notes.
43
+ */
44
+ export declare function publicCliCommandDefinitions(): readonly CliCommandDefinition[];
45
+ /** Stable alias for consumers such as the future dispatcher parity layer. */
46
+ export declare const COMMAND_REGISTRY: readonly CliCommandDefinition[];
47
+ /** Resolve either a canonical command or one of its public aliases. */
48
+ export declare function resolveCliCommandDefinition(name: string): CliCommandDefinition | undefined;
49
+ /** Return the complete public discovery name list in registry order. */
50
+ export declare function publicCliCommandNames(): readonly string[];