@dsh-enhanced/assistant-isolation 0.1.65 → 0.1.67

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 (116) hide show
  1. package/README.md +287 -9
  2. package/lib/audit-archive.d.ts +1 -1
  3. package/lib/audit-archive.d.ts.map +1 -1
  4. package/lib/audit-archive.js +31 -13
  5. package/lib/audit-archive.js.map +1 -1
  6. package/lib/cli.d.ts +7 -3
  7. package/lib/cli.d.ts.map +1 -1
  8. package/lib/cli.js +104 -23
  9. package/lib/cli.js.map +1 -1
  10. package/lib/config.d.ts +15 -2
  11. package/lib/config.d.ts.map +1 -1
  12. package/lib/config.js +49 -9
  13. package/lib/config.js.map +1 -1
  14. package/lib/diagnostics.js +2 -2
  15. package/lib/diagnostics.js.map +1 -1
  16. package/lib/index.d.ts +17 -1
  17. package/lib/index.d.ts.map +1 -1
  18. package/lib/index.js +17 -1
  19. package/lib/index.js.map +1 -1
  20. package/lib/ledger.d.ts +19 -2
  21. package/lib/ledger.d.ts.map +1 -1
  22. package/lib/ledger.js +371 -44
  23. package/lib/ledger.js.map +1 -1
  24. package/lib/linux-cgroups.d.ts +75 -0
  25. package/lib/linux-cgroups.d.ts.map +1 -0
  26. package/lib/linux-cgroups.js +517 -0
  27. package/lib/linux-cgroups.js.map +1 -0
  28. package/lib/linux-execution-contract.d.ts +123 -0
  29. package/lib/linux-execution-contract.d.ts.map +1 -0
  30. package/lib/linux-execution-contract.js +279 -0
  31. package/lib/linux-execution-contract.js.map +1 -0
  32. package/lib/linux-execution.d.ts +123 -0
  33. package/lib/linux-execution.d.ts.map +1 -0
  34. package/lib/linux-execution.js +1264 -0
  35. package/lib/linux-execution.js.map +1 -0
  36. package/lib/linux-job-recovery.d.ts +5 -0
  37. package/lib/linux-job-recovery.d.ts.map +1 -0
  38. package/lib/linux-job-recovery.js +63 -0
  39. package/lib/linux-job-recovery.js.map +1 -0
  40. package/lib/linux-namespace.d.ts +80 -0
  41. package/lib/linux-namespace.d.ts.map +1 -0
  42. package/lib/linux-namespace.js +471 -0
  43. package/lib/linux-namespace.js.map +1 -0
  44. package/lib/linux-repository-admission.d.ts +26 -0
  45. package/lib/linux-repository-admission.d.ts.map +1 -0
  46. package/lib/linux-repository-admission.js +64 -0
  47. package/lib/linux-repository-admission.js.map +1 -0
  48. package/lib/linux-repository-runner.d.ts +54 -0
  49. package/lib/linux-repository-runner.d.ts.map +1 -0
  50. package/lib/linux-repository-runner.js +614 -0
  51. package/lib/linux-repository-runner.js.map +1 -0
  52. package/lib/linux-runner-state-migration.d.ts +94 -0
  53. package/lib/linux-runner-state-migration.d.ts.map +1 -0
  54. package/lib/linux-runner-state-migration.js +463 -0
  55. package/lib/linux-runner-state-migration.js.map +1 -0
  56. package/lib/linux-runner-state.d.ts +53 -0
  57. package/lib/linux-runner-state.d.ts.map +1 -0
  58. package/lib/linux-runner-state.js +332 -0
  59. package/lib/linux-runner-state.js.map +1 -0
  60. package/lib/linux-runtime-source.d.ts +28 -0
  61. package/lib/linux-runtime-source.d.ts.map +1 -0
  62. package/lib/linux-runtime-source.js +237 -0
  63. package/lib/linux-runtime-source.js.map +1 -0
  64. package/lib/linux-runtime.d.ts +37 -0
  65. package/lib/linux-runtime.d.ts.map +1 -0
  66. package/lib/linux-runtime.js +192 -0
  67. package/lib/linux-runtime.js.map +1 -0
  68. package/lib/linux-system-snapshot.d.ts +47 -0
  69. package/lib/linux-system-snapshot.d.ts.map +1 -0
  70. package/lib/linux-system-snapshot.js +704 -0
  71. package/lib/linux-system-snapshot.js.map +1 -0
  72. package/lib/linux-user-namespace.d.ts +79 -0
  73. package/lib/linux-user-namespace.d.ts.map +1 -0
  74. package/lib/linux-user-namespace.js +234 -0
  75. package/lib/linux-user-namespace.js.map +1 -0
  76. package/lib/linux-verifier-runner.d.ts +27 -0
  77. package/lib/linux-verifier-runner.d.ts.map +1 -0
  78. package/lib/linux-verifier-runner.js +346 -0
  79. package/lib/linux-verifier-runner.js.map +1 -0
  80. package/lib/native-cgroup-roots.d.ts +20 -0
  81. package/lib/native-cgroup-roots.d.ts.map +1 -0
  82. package/lib/native-cgroup-roots.js +121 -0
  83. package/lib/native-cgroup-roots.js.map +1 -0
  84. package/lib/native-resources.d.ts +44 -0
  85. package/lib/native-resources.d.ts.map +1 -0
  86. package/lib/native-resources.js +354 -0
  87. package/lib/native-resources.js.map +1 -0
  88. package/lib/probe.d.ts +10 -0
  89. package/lib/probe.d.ts.map +1 -1
  90. package/lib/probe.js +52 -2
  91. package/lib/probe.js.map +1 -1
  92. package/lib/service.d.ts +1 -1
  93. package/lib/service.d.ts.map +1 -1
  94. package/lib/service.js +122 -9
  95. package/lib/service.js.map +1 -1
  96. package/lib/storage.d.ts.map +1 -1
  97. package/lib/storage.js +36 -6
  98. package/lib/storage.js.map +1 -1
  99. package/lib/tools.js +1 -1
  100. package/lib/tools.js.map +1 -1
  101. package/lib/types.d.ts +5 -1
  102. package/lib/types.d.ts.map +1 -1
  103. package/lib/verifier-runner.d.ts +4 -2
  104. package/lib/verifier-runner.d.ts.map +1 -1
  105. package/lib/verifier-runner.js +22 -1
  106. package/lib/verifier-runner.js.map +1 -1
  107. package/lib/version.d.ts +1 -1
  108. package/lib/version.js +1 -1
  109. package/package.json +15 -10
  110. package/runtime/linux-candidate-entry.py +82 -0
  111. package/runtime/linux-namespace-worker.mjs +561 -0
  112. package/runtime/linux-seccomp-policy.LICENSE +202 -0
  113. package/runtime/linux-seccomp-policy.json +886 -0
  114. package/runtime/linux-seccomp.py +106 -0
  115. package/runtime/linux-userns-launcher.py +149 -0
  116. package/runtime/linux-userns-proof.py +299 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Assistant Isolation
2
2
 
3
- Host-only `inspectIsolationGrant({ stateRoot, grant, now? })` supplies a read-only schema 6 snapshot for operator diagnostics. It compares every configured grant field with persisted authority and reports revocation, expiry, historical run/duration reservations across revisions, and active/unknown jobs. It never creates or migrates a database, claims a controller, renews authority, refunds reservations, or returns commands/artifacts. Missing, unsupported or corrupt state is unavailable. `available` refers only to these grant checks; the Web owner doctor combines it with owner checks and a real temporary isolation probe. It is not a Policy, resource-admission or complete autonomy verdict.
3
+ Host-only `inspectIsolationGrant({ stateRoot, grant, now? })` supplies a read-only schema 7/8 snapshot for operator diagnostics. It compares every configured grant field with persisted authority and reports revocation, expiry, historical run/duration reservations across revisions, and active/unknown jobs. It never creates or migrates a database, claims a controller, renews authority, refunds reservations, or returns commands/artifacts. Missing, unsupported or corrupt state is unavailable. `available` refers only to these grant checks; the Web owner doctor combines it with owner checks and a real temporary isolation probe. It is not a Policy, resource-admission or complete autonomy verdict.
4
4
 
5
5
  After a Host crash, startup waits up to 31 seconds for the previous controller's
6
6
  30-second lease to expire naturally. The service and its injected consumers stay
@@ -12,7 +12,237 @@ lease interval plus ordinary boot time before treating readiness as a failure.
12
12
 
13
13
  `@dsh-enhanced/assistant-isolation` adds `isolation_run` to the native DSH ToolRuntime. An operator grants a finite number of offline shell jobs to one authenticated owner, workspace and agent preset. Each job receives only explicitly supplied text files in a fresh scratch directory. Results and artifacts are untrusted process output; exit code zero does not establish that a user goal was achieved.
14
14
 
15
- This is the first actual isolation component of autonomy work packages 08–10. External action/credential brokerage, rollback/compensation and installer bootstrap remain separate delivery requirements. It does not isolate arbitrary Host plugins or replace the native AgentLoop. The Host, Docker daemon, configured image and local operator remain trusted. Each workspace uses a private tmpfs volume with byte and inode hard limits. A persistent pool bounds admitted worker/workspace/keeper reservations across this ledger; it is not a machine-wide guarantee against Docker/kernel overhead or unrelated workloads. Do not treat this initial component as a complete hostile-code production platform.
15
+ This is the first actual isolation component of autonomy work packages 08–10. External action/credential brokerage, rollback/compensation and installer bootstrap remain separate delivery requirements. It does not isolate arbitrary Host plugins or replace the native AgentLoop. The Host, local operator, configured runtime and kernel remain trusted; the Docker backend also trusts its daemon and image. Each workspace uses a private tmpfs volume with byte and inode hard limits. A persistent pool bounds admitted worker/workspace/keeper reservations across this ledger; it is not a machine-wide guarantee against runtime/kernel overhead or unrelated workloads. Do not treat this initial component as a complete hostile-code production platform.
16
+
17
+ ## Native Linux building blocks
18
+
19
+ The Host-only `runLinuxNamespace`, `LinuxCgroupLease`, and `LinuxRuntime`
20
+ exports provide Docker-free execution on non-root Linux. The public tool accepts
21
+ explicit `backend: 'linux-native'` configuration; the private verifier also
22
+ accepts `LinuxVerifierRunnerConfig` below. Omitting `backend` retains Docker
23
+ configuration semantics. Production setup still selects its existing backend
24
+ until installation and owner migration are integrated.
25
+
26
+ `runLinuxNamespace` uses fixed `unshare`, mount, bubblewrap and Python/libseccomp
27
+ programs, with a shipped digest-checked syscall policy. It exposes system runtime
28
+ directories and explicit read-only runtime roots, creates bounded workspace/tmp
29
+ tmpfs mounts, and uses a 1 MiB shared-memory mount. The candidate has UID/GID
30
+ 65534, no effective capabilities, no network, a private PID view and a clean
31
+ environment. It cannot see Host Home, sysfs, credentials or control sockets.
32
+ Only the repository profile allows nested user namespaces for repository tests.
33
+ The caller must keep exposed runtime roots free of secrets. Input and exported
34
+ UTF-8 artifacts have separate 1 MiB bounds. The separate repository API below
35
+ provides binary transport without expanding this text API's permissions.
36
+
37
+ The keeper's fixed gate waits for `beforeKeeperStart` before unshare, mounts or
38
+ staging. A separate candidate gate waits until `beforeStart` admits its exact
39
+ PID/start time to resource controls. `afterExit` must prove the candidate group
40
+ quiet before export; `onStop` must reconcile it after cancellation or failure.
41
+ Callback failure or timeout returns `unknown`. The trusted keeper independently
42
+ revokes work on Host disconnect or deadline. `namespaceExited` only proves that
43
+ keeper's close; it is not a durable resource-cleanup or task-success receipt.
44
+
45
+ `LinuxCgroupLease` configures and reads back CPU, memory including swap, and PID
46
+ limits in pre-delegated v1 controllers. Admission is one-shot; shutdown fences
47
+ late admission and uses freezer/pidfd checks before proving all groups empty.
48
+ Recovery checks boot, owner and directory identities. V2 admission is currently
49
+ rejected because its process-placement path has not been verified. The caller
50
+ still owns pre-creation durable intent, controller exclusivity, orphan recovery,
51
+ keeper resource reservations, authorization and budgets. The repository's
52
+ one-time delegation script is an operator provisioning aid, not a privileged
53
+ runtime broker or a sudo rule for a writable checkout.
54
+
55
+ `LinuxRuntime` verification binds Node, native pnpm, frozen dependencies and
56
+ retained store/cache to device/inode and deterministic tree hashes. It rejects
57
+ outside links, writable contents and drift. None of these exports activates a
58
+ service, reads provider credentials, accesses a browser, downloads images or
59
+ runs an install lifecycle script. See the [delivery boundary](../../docs/dockerless-runtime.md)
60
+ for remaining integration and acceptance.
61
+
62
+ ### Public native execution
63
+
64
+ Configure `backend: 'linux-native'` and `linux: { runtime, cgroupRoots }` alongside
65
+ the existing `stateRoot`, finite `grants`, limits and storage policy. `runtime`
66
+ is the digest-pinned `LinuxRuntime` prepared by the native toolchain bootstrap;
67
+ `cgroupRoots` identifies the current user's verified v1 controller delegation.
68
+ The synchronous Cordis Config rejects invalid bindings and insufficient pools;
69
+ service readiness and every newly admitted request recheck the runtime tree.
70
+ This branch does not require an image or resolve a Docker socket.
71
+
72
+ `isolation_run` keeps its existing request, owner/Policy checks, goal artifact
73
+ binding and cancellation contract. Inline files enter a fresh bounded workspace;
74
+ exported artifacts come back only after cleanup proof. Node and pnpm use the
75
+ pinned runtime PATH. The ledger reserves worker, keeper, workspace, temporary
76
+ and shared-memory allowances before dispatch. Changing backend or contract
77
+ under an existing idempotency key is a conflict and never creates a new job.
78
+
79
+ Startup and background recovery use each job's persisted backend. Native cleanup
80
+ uses the original contract and private receipts; a native-configured service
81
+ leaves historical Docker unknown resources occupied for their original cleanup
82
+ path. Switching configuration does not extend grants or reset budgets.
83
+
84
+ `dsh-isolation probe-native /absolute/runtime-config.json` accepts an owned JSON
85
+ file containing exactly `{ runtime, cgroupRoots }`. It tests pinned Node,
86
+ non-root execution, a canary artifact and actual cleanup without Docker or a
87
+ production grant. Unproven cleanup retains its private evidence directory.
88
+ `reconcile /absolute/private/state-root` dispatches cleanup by persisted backend;
89
+ Docker arguments are unused for a native-only ledger. `revoke` first persists
90
+ revocation and lets a live controller stop its work. For pending native jobs it
91
+ also reports `nativeResourcesQuiescent` and `nativeRetained`; exit 2 indicates
92
+ that cleanup is not yet confirmed. It never takes over a live controller.
93
+
94
+ Positive result retention first commits the ledger's result digest, then
95
+ replaces native journal/receipt bodies with digest-only records. Interrupted
96
+ replacement is retried with the same controller and row-version checks. Unknown
97
+ or unproven work retains its recovery evidence; the default zero-retention
98
+ policy keeps all bodies. Cached results continue through the ledger and cannot
99
+ replay execution after body pruning.
100
+
101
+ ### Durable native verification
102
+
103
+ New managed installations can freeze a `stateSeal: { path, sha256 }` on
104
+ `LinuxVerifierRunnerConfig` and `LinuxRepositoryRunnerConfig`. Before issuing
105
+ owner grants, `prepareLinuxRunnerState({ stateRoot, sealPath, kind })` initializes
106
+ an empty schema 8 ledger and publishes a private seal outside the runner root.
107
+ The caller creates both private directories first. First installation must pass
108
+ `requireEmpty: true`, including when an earlier preparation left a valid seal:
109
+ all business tables and history sequences must still be empty, with no pending
110
+ WAL or shared-memory state. Loss of outer installation files cannot renew an
111
+ already used sealed ledger. The seal binds the purpose,
112
+ owner UID, original directory and database inodes, and fixed schema. Freeze its
113
+ reference in the owner configuration and authority digest. Restart and install
114
+ retry use the existing-only `inspectLinuxRunnerState()`; missing or replaced
115
+ roots, ledgers or seals fail before writable startup. A used unsealed ledger or
116
+ incomplete initialization needs explicit reconciliation. The helper never
117
+ renews grants, clears history or replays unknown jobs. Configurations without a
118
+ seal retain their original behavior and have not acquired this protection.
119
+
120
+ An authorized Docker-to-linux-native backend handover runs offline on a staged
121
+ copy of the home. `prepareLinuxVerifierHandoverState({ name, stageStateRoot,
122
+ stageSealPath, liveStateRoot, liveSealPath })` upgrades the staging ledger to
123
+ schema 9 and publishes the seal before the new authority digest is compiled;
124
+ `migrateLinuxVerifierRunnerState({ ..., existingSeal })` then proves that exact
125
+ seal still binds the copy and performs the audited grant/job rewrite without
126
+ resealing, so a digest compiled in between stays valid across the cutover.
127
+
128
+ `IsolatedVerifierRunner` accepts either the existing Docker configuration or an
129
+ explicit `backend: 'linux-native'` configuration with a verified `LinuxRuntime`
130
+ and delegated `cgroupRoots`. `LinuxVerifierRunner` is also exported directly.
131
+ The API remains `run(key, artifact, stdin, signal)` and `close()`; the fixed
132
+ operator command receives `artifact` and `input` files in the private workspace.
133
+ Its PATH uses the pinned runtime's Node/pnpm. Ordinary agent tools do not receive
134
+ this constructor or the verifier's authority.
135
+
136
+ The native backend reuses the same schema 8 ledger, finite verification grant,
137
+ controller lease, idempotency key and resource pool. It keeps execution records
138
+ in a private `linux-executions` child directory. Kernel resource identity and a
139
+ durable create intent precede execution. Worker and keeper have separate groups;
140
+ the keeper has 128 MiB memory including swap, 64 PIDs and 0.5 CPU. Reservations
141
+ include both groups plus workspace, temporary and shared-memory allowances.
142
+ Runtime drift, lost authorization or controller fencing stops new admission.
143
+
144
+ The shared `prepareLinuxExecutionIntent` / `runLinuxExecution` helpers own the
145
+ durable journal, both group lifetimes and private receipts. Recovery reads
146
+ original identities and proves actual group/workspace absence. A missing or
147
+ partial creation record never grants permission to run the command again or to
148
+ signal an unidentified process. Proven cleanup releases active capacity while
149
+ preserving an existing unknown result and its consumed run/duration budget.
150
+ Historical Docker/macOS rows retain their own backend identity; switching this
151
+ API does not reconcile those external resources or reset their grants.
152
+
153
+ These APIs still require non-root Linux with verified v1 delegation and the
154
+ fixed namespace dependencies. Installer migration, behavior certificates and
155
+ release/adoption remain separate integration steps. V2, arm64 execution and a reboot with changed kernel
156
+ resource identity remain outside the current live acceptance.
157
+
158
+ ### Durable repository execution
159
+
160
+ Host-only `prepareLinuxRepositoryIntent`, `runLinuxRepositoryExecution`,
161
+ `readLinuxRepositoryReceipt` and `reconcileLinuxRepositoryExecution` share the
162
+ same durable execution engine. They require an explicit schema 2 or 3 repository
163
+ contract, a frozen command/archive/artifact binding, and caller-owned finite
164
+ authorization and controller admission. This is not an agent-visible tool or a
165
+ complete source-build adapter.
166
+
167
+ `runLinuxRepositoryNamespace` transports a SHA-256-bound archive of at most
168
+ 64 MiB through a dedicated input pipe and a read-only `/input/source.tar` mount.
169
+ It exports at most 32 MiB of binary artifacts through a separate pipe after the
170
+ candidate group is proven quiet. Neither transport pipe reaches the candidate.
171
+ Repository limits allow an 8 GiB workspace, 4 GiB executable temporary mount,
172
+ nested namespaces and a 30-minute deadline. Ordinary execution retains its
173
+ existing bounds and non-executable temporary mount.
174
+
175
+ The durable receipt contains artifact paths, lengths and hashes; bytes remain in
176
+ an owner-only bounded spool. Reading a successful receipt rechecks both kernel
177
+ absence and spool integrity. Failed or unknown executions return no binary
178
+ artifacts, and a recovered identity never dispatches the command again. Partial
179
+ creation and failed writes retain their original recovery fences.
180
+
181
+ `linuxJobResourceReservation` includes every tmpfs, keeper capacity and durable
182
+ spool; the ledger enforces these minima for both repository schemas. At the maximum artifact
183
+ size, the execution spool and journal allowance is 108 MiB. A caller that stages
184
+ its own archive or output pack must reserve that additional storage before
185
+ staging.
186
+
187
+ Schema 3 additionally binds a `userNamespace` mapping, `systemSnapshot` and the
188
+ packaged execution implementation digest. It uses the owner's existing
189
+ `/etc/subuid` and `/etc/subgid` allocation and digest-pinned, system-owned
190
+ `newuidmap`/`newgidmap` helpers; execution never allocates or widens those ranges.
191
+ Namespace UID/GID 0 map to the Host owner, and candidate UID/GID 65534 map to the
192
+ allocated subordinate identity. The fixed entry drops UID/GID privileges, clears
193
+ supplementary groups and all capabilities, enables no-new-privileges and waits for an independent Host
194
+ check before executing candidate code. The Host persists both namespace and
195
+ candidate witnesses before START. An absent witness can prove a failed setup
196
+ quiet, but cannot authorize candidate execution or a successful result.
197
+
198
+ The system snapshot copies a fixed closure of system tools and shared
199
+ libraries from trusted root-owned paths, bounded to 640 MiB and 32,768 entries.
200
+ An optional verified `LinuxRuntime` supplies only its pinned Node executable at
201
+ `/usr/bin/node`; separate provenance binds that copy to the runtime and its
202
+ original Node digest. This supports nested repository tests without depending
203
+ on a Host Node installation. The snapshot hashes the inventory and bytes,
204
+ rewrites only internal symlinks and mounts the result read-only; it does not
205
+ mount the Host's `/etc` or mutable executable search paths. Schema 3 provides a
206
+ fixed `dsh-sandbox` hostname and a read-only synthetic `/etc/hostname`. A separate 8 MiB /
207
+ 16,384-inode identity mount is included in schema 3 reservations. Recovery uses
208
+ the original namespace ancestry, PID start times, freezer membership and pidfds
209
+ before terminating descendants; allowing a subordinate UID alone is insufficient.
210
+ Schema 1/2 jobs retain their original single-identity cleanup interpretation.
211
+
212
+ Host-only `LinuxRepositoryRunner` adds a private, immutable finite authority,
213
+ controller lease and schema 8 ledger to these repository primitives. It reserves
214
+ capacity before calling the archive factory, freezes the archive digest and
215
+ command under the source-job key, and passes a cancellation signal to the
216
+ factory. Factory deadlines, grant expiry and controller loss abort that signal
217
+ before settling the job. A trusted factory must honor cancellation and await its
218
+ child processes; the runner does not isolate arbitrary Host callbacks.
219
+
220
+ The runner reserves another 256 MiB of Host staging memory and 96 MiB of staging
221
+ storage in addition to the execution minima. Reopening the same authority keeps
222
+ its original expiry and consumed run/duration budget. A repeated key never
223
+ reruns the factory or command; binary results require the original successful
224
+ receipt, intact spool and fresh resource-absence proof. Foreign ledgers are
225
+ rejected before migration or controller acquisition. `close()` fences admission
226
+ and waits for active work and recovery.
227
+
228
+ A private, single-use admission refusal can prove that this call never created
229
+ an execution. It is valid only for the original runner, request, controller and
230
+ state-directory/database identities; a concurrent or later call with the same
231
+ key invalidates it. Ledger refusals require a known rejection before any insert
232
+ and a confirmed rollback. Callers consume the proof after their own cleanup,
233
+ immediately before settling their original task. Missing records, uncertain
234
+ database errors and historical interrupted calls do not provide this proof;
235
+ `inspectQuiescence()` still returns `unknown` for a missing key. No spent budget
236
+ is refunded and no command is replayed.
237
+
238
+ Providing both `userNamespace` and `systemSnapshot` selects schema 3; omitting
239
+ both retains schema 2. Mixed or partial configuration is rejected. A ledger
240
+ containing one repository schema cannot be reopened as the other schema.
241
+
242
+ Control Plane's explicit Linux source-build adapter uses this runner for exact
243
+ Git-tree preparation, frozen offline installation, root `pnpm check` and pack
244
+ verification. Behavior verification and release/adoption still need their own
245
+ native integration; repository execution alone does not authorize adoption.
16
246
 
17
247
  ## Native macOS execution primitive
18
248
 
@@ -73,7 +303,7 @@ dsh plugin --profile <profile> add @dsh-enhanced/assistant-isolation
73
303
 
74
304
  Required Host services: `agents`, `tools`, `assistantDelivery`, and `assistantPolicy`. The bundle is independently publishable; installing it does not install or activate peers. Without the services it does not register an execution tool. With no grants it does not execute jobs. No Docker image is downloaded by the plugin.
75
305
 
76
- The supported execution path is a non-root Linux or macOS Host process with access to a local Docker daemon. Linux defaults to `/var/run/docker.sock`; macOS defaults to the current user's Docker Desktop socket at `~/.docker/run/docker.sock`. The socket must exist at runtime and remain the same local Unix socket through creation, execution and cleanup. macOS requires an owner-owned socket and private Docker run directory; a remote `DOCKER_HOST`, Docker context or socket path outside that user directory is never used. Configure an already-present, operator-reviewed image **ID** (`sha256:` plus 64 hexadecimal digits), containing `/bin/sh`, a trusted `/bin/busybox` with `sleep`, `stat` and `cat`, and the commands needed for the task. Image-declared `VOLUME` entries are rejected to prevent extra writable Docker volumes. A tag or remote repository digest is not accepted as a substitute for the local immutable image ID. Failed Docker operations do not fall back to a Host shell.
306
+ The Docker backend supports a non-root Linux or macOS Host process with access to a local Docker daemon. Linux defaults to `/var/run/docker.sock`; macOS defaults to the current user's Docker Desktop socket at `~/.docker/run/docker.sock`. The socket must exist at runtime and remain the same local Unix socket through creation, execution and cleanup. macOS requires an owner-owned socket and private Docker run directory; a remote `DOCKER_HOST`, Docker context or socket path outside that user directory is never used. Configure an already-present, operator-reviewed image **ID** (`sha256:` plus 64 hexadecimal digits), containing `/bin/sh`, a trusted `/bin/busybox` with `sleep`, `stat` and `cat`, and the commands needed for the task. Image-declared `VOLUME` entries are rejected to prevent extra writable Docker volumes. A tag or remote repository digest is not accepted as a substitute for the local immutable image ID. Failed Docker operations do not fall back to a Host shell.
77
307
 
78
308
  ## Configuration
79
309
 
@@ -171,7 +401,7 @@ The default command keeps bodies. Maintenance rejects a competing live controlle
171
401
 
172
402
  ## Worker-protected audit archive
173
403
 
174
- The Host-only archive command copies the schema 6 lifecycle audit into canonical, hash-linked NDJSON batches. The archive is immutable by protocol and content address: a published batch is never overwritten, and any conflicting content is rejected; this is not an operating-system immutable-bit guarantee. The command freezes a SQLite audit high-water mark and reads only through that fixed snapshot. The main database schema and data, including audit, jobs, grants and controller state, remain unchanged; a SQLite read-only snapshot may create or use WAL/SHM coordination sidecars. Repeating an interrupted or completed export is idempotent: already committed matching batches are retained. Files are committed with private permissions using a synced exclusive temporary file, a no-overwrite hard link, temporary unlink and directory sync; a matching crash residue is recovered under the archive lock. The verifier checks the complete local batch chain, including canonical records, contiguous sequence numbers and the previous-batch digest, and reports the source binding plus external anchor tuple `{ archiveInstanceId, highestSequence, headDigest }`. An empty or control-only directory is not a verifiable archive.
404
+ The Host-only archive command copies the supported schema 7/8 lifecycle audit into canonical, hash-linked NDJSON batches. The archive is immutable by protocol and content address: a published batch is never overwritten, and any conflicting content is rejected; this is not an operating-system immutable-bit guarantee. The command freezes a SQLite audit high-water mark and reads only through that fixed snapshot. The main database schema and data, including audit, jobs, grants and controller state, remain unchanged; a SQLite read-only snapshot may create or use WAL/SHM coordination sidecars. Repeating an interrupted or completed export is idempotent: already committed matching batches are retained. Files are committed with private permissions using a synced exclusive temporary file, a no-overwrite hard link, temporary unlink and directory sync; a matching crash residue is recovered under the archive lock. The verifier checks the complete local batch chain, including canonical records, contiguous sequence numbers and the previous-batch digest, and reports the source binding plus external anchor tuple `{ archiveInstanceId, highestSequence, headDigest }`. An empty or control-only directory is not a verifiable archive.
175
405
 
176
406
  Prepare two distinct canonical, operator-owned directories before running the command; neither directory may be a symlink, group/other-accessible, or the same directory. The archive command does not create or loosen the state root. For a new archive directory, use an owner-only umask and mode explicitly:
177
407
 
@@ -193,10 +423,10 @@ The local hash chain proves integrity and linkage relative to a trusted digest;
193
423
 
194
424
  | Surface | Authority |
195
425
  | --- | --- |
196
- | Filesystem | Host owns a `0700` state directory, `0600` ledger and per-job staging. A separate operator-owned `0700` audit archive may contain canonical hash-linked NDJSON batches; neither state nor archive root is mounted or bind-mounted into a worker. The worker sees its byte/inode-limited tmpfs workspace plus its configured image. Console/artifact results and diagnostic witnesses persist in the private ledger. The Host reads Linux `/proc`, the protected `/run/docker.pid`, and socket metadata for best-effort diagnostics; missing metadata never grants release. No writable Host bind is exposed. Observed storage admission and opt-in result retention are described below; they are not a hard filesystem quota. |
197
- | Network | Host uses the fixed local Docker Unix socket. The worker has `--network none`, no Host socket/config mount and no external action endpoint. |
198
- | Subprocess | Host starts the fixed configured Docker executable and packaged detached Node supervisor, and may read fixed docker.service/docker.socket properties through /usr/bin/systemctl for diagnostics. Workers run `/bin/sh` with non-root UID/GID, read-only image filesystem, dropped capabilities, `no-new-privileges`, memory/CPU/PID limits, bounded `/tmp` and `/dev/shm`, and disabled Docker log storage. A fixed BusyBox keeper retains the job volume across execution-container removal and does not execute job code. |
199
- | Credentials | Worker receives no Host environment or credential files. The Docker daemon access itself is a privileged trusted Host capability. Images must contain no embedded secrets. |
426
+ | Filesystem | Host owns a `0700` state directory, `0600` ledger and per-job staging. A separate operator-owned `0700` audit archive may contain canonical hash-linked NDJSON batches; neither state nor archive root is mounted or bind-mounted into a worker. The worker sees its byte/inode-limited tmpfs workspace plus its configured image or approved native runtime roots. Console/artifact results and diagnostic witnesses persist in the private ledger. The Host reads Linux `/proc`, the protected `/run/docker.pid`, and socket metadata for best-effort diagnostics; missing metadata never grants release. No writable Host bind is exposed. Observed storage admission and opt-in result retention are described below; they are not a hard filesystem quota. |
427
+ | Network | Docker uses its fixed local Unix socket; native Linux does not contact Docker. Workers have no network, Host socket/config mount or external action endpoint. |
428
+ | Subprocess | Native Linux uses the fixed packaged keeper, unshare, mount, bubblewrap and Python/libseccomp programs described above. Docker starts the fixed configured Docker executable and packaged detached Node supervisor, and may read fixed docker.service/docker.socket properties through /usr/bin/systemctl for diagnostics. Workers run `/bin/sh` with non-root UID/GID, read-only image filesystem, dropped capabilities, `no-new-privileges`, memory/CPU/PID limits, bounded `/tmp` and `/dev/shm`, and disabled Docker log storage. A fixed BusyBox keeper retains the job volume across execution-container removal and does not execute job code. |
429
+ | Credentials | Worker receives no Host environment or credential files. The Docker daemon access itself is a privileged trusted Host capability. Images and approved runtime roots must contain no embedded secrets. |
200
430
  | Browser | None. |
201
431
  | Install scripts | No runtime provisioning, image pull or privileged install hook. Standard package build/prepack only. |
202
432
 
@@ -224,10 +454,58 @@ The default export is the Cordis plugin object with stable name, Config and appl
224
454
 
225
455
  ### 可信目标产物与独立验收
226
456
 
227
- schema v6 新增可空的 artifact admission 绑定,v1–v5 行迁移后保留且不会被追认为目标产物。Host 在真实 Goals native round 内,于 Docker 派发前固定 step acceptance ID/digest、run、turn 与声明输出路径;相同幂等键不会重新绑定。运行中若原 admission 改变则取消。私有 `readAcceptedArtifact` 只读取同一 v4 step scope/owner/Session 的成功、已静止、有效期内且正文未清理的产物;较新的同路径失败尝试禁止回退旧结果。
457
+ schema v6 新增可空的 artifact admission 绑定,v1–v5 行迁移后保留且不会被追认为目标产物。Host 在真实 Goals native round 内,于隔离任务派发前固定 step acceptance ID/digest、run、turn 与声明输出路径;相同幂等键不会重新绑定。运行中若原 admission 改变则取消。私有 `readAcceptedArtifact` 只读取同一 v4 step scope/owner/Session 的成功、已静止、有效期内且正文未清理的产物;较新的同路径失败尝试禁止回退旧结果。
228
458
 
229
459
  Host Verifier 可使用导出的 `IsolatedVerifierRunner` 在独立私有 stateRoot 中验收复制产物。该类没有模型工具入口,固定 authority/镜像/命令、次数、累计预留时长及期限,可选 `socketPath` 与普通隔离任务遵循相同的本地 Docker socket 约束,并复用相同 Docker supervisor 和资源边界。验收任务与原产物 job 是不同 ID;验收不在宿主执行模型代码。未知派发仍保留资源占用且不自动重放,重复 setup 或重启也不会重置预算。
230
460
 
231
461
  The read-only `isolation_grants` tool exposes usable grant IDs and remaining limits only for the current authenticated owner and workspace/preset. It applies live Policy, principal lineage, expiry and revocation checks; it cannot create or extend grants and returns no credentials or Host paths. When Policy allows discovery and at least one grant remains usable for that exact owner, the Host registers its exact tool definition for finite preauthorization so this read-only query skips the redundant native risk prompt. Revocation, expiry, owner/scope mismatch, Policy denial, tool replacement, or bundle unload removes that preauthorization. Use a returned ID with `isolation_run`.
232
462
 
233
463
  Native skill benchmark cells may expose `skill_run` and `skill_status` through the same preauthorization gate. Only the exact Skills definitions with a current, owner-bound isolation-only delegation can execute. This does not enable Host filesystem/shell tools or ordinary saved workflows in an isolated scope; every nested isolation job still needs its own finite grant. See [native skill reuse](../../docs/native-skill-reuse.md).
464
+
465
+ ## Non-activating native resources
466
+
467
+ `@dsh-enhanced/assistant-isolation/native-resources` is a programmatic resource
468
+ entry, with no Cordis plugin/default export. Importing it does not mount Isolation
469
+ or create grants. `freezeLinuxRuntimeSource({kind:'git-object', repository, commit,
470
+ version})` reads exact commit objects from an existing checkout or bare repository;
471
+ `local-head` captures HEAD even when the working tree is dirty. Neither executes
472
+ repository scripts nor creates an RSI workspace or repair branch. `official-tag`
473
+ fetches only `refs/tags/v<exact-version>` from the fixed official GitHub repository
474
+ into a private temporary bare object database and removes it afterwards. This
475
+ requires HTTPS Git access; cancellation and each command are bounded.
476
+
477
+ The resulting `{version,sourceCommit,manifests:[{path,bytes}]}` is a manifest byte
478
+ snapshot, not an authenticity certificate. The trusted caller selects the source.
479
+ Preparation copies and validates the root manifests, complete lockfile importer
480
+ inventory, workspace manifests, exact cohort version/package manager and workspace
481
+ links; caller-provided hashes are never accepted as evidence. Workspace YAML only
482
+ permits the reviewed declarative `packages`, `catalog`, `linkWorkspacePackages`,
483
+ `allowBuilds` and `minimumReleaseAgeExclude` fields with strict value shapes.
484
+ Unknown pnpm settings, package-level `pnpm`/`devEngines`/`engines.runtime`, and
485
+ lockfile configuration dependencies are rejected rather than stripped; normal
486
+ `engines.node` constraints remain supported. No source code or
487
+ package lifecycle script is executed. Arbitrary workspace patterns and nonregistry
488
+ resolutions are unsupported.
489
+
490
+ `prepareLinuxRuntime({dshHome,profile,source,signal?,nodeArchivePath?,
491
+ pnpmArchivePath?,pnpmExeArchivePath?,pnpmNativeArchivePath?,revalidateSource?})`
492
+ prepares pinned official Node/pnpm archives and the frozen dependency closure with
493
+ scripts and pnpmfile hooks explicitly disabled. Cache archives are still checked against fixed SHA-256 values.
494
+ The optional programmatic `revalidateSource` fence authenticates caller-specific
495
+ provenance before/after preparation and retained-tree verification. It is trusted
496
+ Host code, not a serialized config or model callback.
497
+
498
+ `readLinuxRuntime({dshHome,profile,source,signal?,revalidateSource?})` only reopens
499
+ and verifies an existing private runtime; it does not download, create, repair or
500
+ rebuild it. Both return `PreparedLinuxRuntime` schema 3 and retain the prior RSI
501
+ receipt location `rsi-linux-runtimes/<profile>`, exact receipt field order/digests,
502
+ archive pins and whole-tree/inode validation. The path name is compatibility
503
+ storage, not a requirement for an RSI source workspace.
504
+
505
+ `discoverLinuxCgroupRoots({signal?})` independently reads existing delegated v1
506
+ roots. It never prepares or grants a cgroup and is deliberately not a prerequisite
507
+ of the base resource builder. `probeLinuxIsolationRuntime` is re-exported unchanged:
508
+ calling it explicitly performs the existing finite isolation probe. Resources do
509
+ not start Host services, configure owners/grants, renew budgets or run sudo. Linux
510
+ x64/arm64 is required for prepare/read/discovery; importing and freezing manifests
511
+ also work on macOS. Offline fixtures do not establish Linux live readiness.
@@ -9,7 +9,7 @@ export interface IsolationAuditArchiveManifest {
9
9
  readonly count: number;
10
10
  readonly firstSequence: number;
11
11
  readonly lastSequence: number;
12
- readonly ledgerSchema: 6 | 7;
12
+ readonly ledgerSchema: 6 | 7 | 8 | 9;
13
13
  readonly previousDigest: string | null;
14
14
  readonly protocol: typeof ISOLATION_AUDIT_ARCHIVE_PROTOCOL;
15
15
  readonly snapshotThroughSequence: number;
@@ -1 +1 @@
1
- {"version":3,"file":"audit-archive.d.ts","sourceRoot":"","sources":["../src/audit-archive.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,gCAAgC,EAAG,gCAAyC,CAAA;AAgBzF,MAAM,MAAM,8BAA8B,GAAG,UAAU,GAAG,eAAe,GAAG,cAAc,GAAG,gBAAgB,GAAG,iBAAiB,GAAG,IAAI,CAAA;AAExI,qBAAa,0BAA2B,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,8BAA8B;gBAApC,IAAI,EAAE,8BAA8B,EAAE,OAAO,GAAE,MAAa;CAIlF;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,CAAC,GAAG,CAAC,CAAA;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,gCAAgC,CAAA;IAC1D,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAA;IACxC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;IACtC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;CACpB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,OAAO,gCAAgC,CAAA;IAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAA;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;CACvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IACxC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,YAAY,EAAE,iCAAiC,CAAA;CACzD;AA+PD,8HAA8H;AAC9H,wBAAgB,2BAA2B,CAAC,gBAAgB,EAAE,MAAM,GAAG,iCAAiC,CAOvG;AA4KD,iGAAiG;AACjG,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,2BAA2B,CAqE/I"}
1
+ {"version":3,"file":"audit-archive.d.ts","sourceRoot":"","sources":["../src/audit-archive.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,gCAAgC,EAAG,gCAAyC,CAAA;AAezF,MAAM,MAAM,8BAA8B,GAAG,UAAU,GAAG,eAAe,GAAG,cAAc,GAAG,gBAAgB,GAAG,iBAAiB,GAAG,IAAI,CAAA;AAExI,qBAAa,0BAA2B,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,8BAA8B;gBAApC,IAAI,EAAE,8BAA8B,EAAE,OAAO,GAAE,MAAa;CAIlF;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,gCAAgC,CAAA;IAC1D,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAA;IACxC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;IACtC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAA;CACpB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,OAAO,gCAAgC,CAAA;IAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAA;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAA;CACvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IACxC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,YAAY,EAAE,iCAAiC,CAAA;CACzD;AA+PD,8HAA8H;AAC9H,wBAAgB,2BAA2B,CAAC,gBAAgB,EAAE,MAAM,GAAG,iCAAiC,CAOvG;AA4LD,iGAAiG;AACjG,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,2BAA2B,CAsE/I"}
@@ -3,7 +3,6 @@ import { closeSync, constants, existsSync, fchmodSync, fstatSync, fsyncSync, lin
3
3
  import { isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
5
  export const ISOLATION_AUDIT_ARCHIVE_PROTOCOL = 'dsh-isolation-audit-archive/v1';
6
- const LEDGER_SCHEMA = 7;
7
6
  const DEFAULT_BATCH_SIZE = 100;
8
7
  const MAX_BATCH_SIZE = 1_000;
9
8
  const MAX_TEXT_LENGTH = 16_384;
@@ -197,7 +196,7 @@ function validateLockedControlPath(identity) {
197
196
  function decodeManifest(value) {
198
197
  const keys = ['archiveInstanceId', 'count', 'firstSequence', 'lastSequence', 'ledgerSchema', 'previousDigest', 'protocol', 'snapshotThroughSequence', 'sourceStateRootDigest', 'type', 'version'];
199
198
  if (!plain(value) || !exactKeys(value, keys) || typeof value.archiveInstanceId !== 'string' || !ARCHIVE_INSTANCE_ID.test(value.archiveInstanceId) || typeof value.sourceStateRootDigest !== 'string' || !DIGEST.test(value.sourceStateRootDigest) || !safeNatural(value.count) || value.count > MAX_BATCH_SIZE
200
- || !safePositive(value.firstSequence) || !safeNatural(value.lastSequence) || ![6, 7].includes(value.ledgerSchema)
199
+ || !safePositive(value.firstSequence) || !safeNatural(value.lastSequence) || ![6, 7, 8, 9].includes(value.ledgerSchema)
201
200
  || (value.previousDigest !== null && (typeof value.previousDigest !== 'string' || !DIGEST.test(value.previousDigest)))
202
201
  || value.protocol !== ISOLATION_AUDIT_ARCHIVE_PROTOCOL || !safeNatural(value.snapshotThroughSequence) || value.type !== 'manifest' || value.version !== 1)
203
202
  fail('invalid-archive', 'invalid audit archive manifest');
@@ -512,21 +511,39 @@ function beginArchiveControl(directory, sourceStateRootDigest) {
512
511
  }
513
512
  function validateLedgerFile(state) {
514
513
  const path = join(state.path, 'ledger.sqlite');
514
+ let descriptor;
515
515
  try {
516
- const inspected = inspectPrivateFile(path, 'isolation ledger');
517
- return { path, descriptor: inspected.descriptor };
516
+ // The source ledger is append-only while a live writer holds it, so its size
517
+ // may grow between stat and open. Identity (device/inode/owner/mode/link
518
+ // count) must still be stable: a replacement remains invalid-source.
519
+ const before = lstatSync(path, { bigint: true });
520
+ const owner = BigInt(process.getuid?.() ?? -1);
521
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.uid !== owner
522
+ || (before.mode & 4095n) !== 384n)
523
+ fail('invalid-source', 'isolation ledger must be one private owned regular file');
524
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
525
+ const opened = fstatSync(descriptor, { bigint: true });
526
+ const after = lstatSync(path, { bigint: true });
527
+ if (!opened.isFile() || opened.nlink !== 1n || opened.uid !== owner || (opened.mode & 4095n) !== 384n
528
+ || !after.isFile() || after.isSymbolicLink() || after.nlink !== 1n || after.uid !== owner
529
+ || (after.mode & 4095n) !== 384n || !sameInode(before, opened) || !sameInode(opened, after)) {
530
+ fail('invalid-source', 'isolation ledger changed while being opened');
531
+ }
532
+ return { path, descriptor };
518
533
  }
519
534
  catch (error) {
535
+ if (descriptor !== undefined)
536
+ closeSync(descriptor);
520
537
  if (error instanceof IsolationAuditArchiveError)
521
538
  fail('invalid-source', error.message);
522
- throw error;
539
+ fail('invalid-source', 'isolation ledger is unavailable');
523
540
  }
524
541
  }
525
542
  function validateLedgerSnapshot(database) {
526
543
  const userVersion = database.prepare('PRAGMA user_version').get().user_version;
527
544
  const schema = database.prepare("SELECT value FROM schema_meta WHERE key='schema-version'").get();
528
- if (userVersion !== LEDGER_SCHEMA || schema?.value !== String(LEDGER_SCHEMA))
529
- fail('invalid-source', 'isolation ledger schema 7 is required');
545
+ if (userVersion !== 7 && userVersion !== 8 && userVersion !== 9 || schema?.value !== String(userVersion))
546
+ fail('invalid-source', 'isolation ledger schema 7, 8 or 9 is required');
530
547
  const highWater = database.prepare("SELECT COALESCE((SELECT seq FROM sqlite_sequence WHERE name='isolation_audit'), 0) AS sequence").get().sequence;
531
548
  if (!safeNatural(highWater))
532
549
  fail('invalid-source', 'isolation audit high-water is invalid');
@@ -579,8 +596,8 @@ function rowRecord(row, expectedSequence) {
579
596
  return freeze({ action: row.action, detail: row.detail, grantId: row.grant_id, jobId: row.job_id, occurredAt: row.occurred_at, sequence: row.sequence });
580
597
  }
581
598
  function recordsEqual(left, right) { return canonical(left) === canonical(right); }
582
- function makeManifest(archiveInstanceId, sourceStateRootDigest, firstSequence, lastSequence, count, snapshotThroughSequence, previousDigest) {
583
- return freeze({ archiveInstanceId, count, firstSequence, lastSequence, ledgerSchema: LEDGER_SCHEMA, previousDigest, protocol: ISOLATION_AUDIT_ARCHIVE_PROTOCOL, snapshotThroughSequence, sourceStateRootDigest, type: 'manifest', version: 1 });
599
+ function makeManifest(archiveInstanceId, sourceStateRootDigest, firstSequence, lastSequence, count, snapshotThroughSequence, previousDigest, ledgerSchema) {
600
+ return freeze({ archiveInstanceId, count, firstSequence, lastSequence, ledgerSchema, previousDigest, protocol: ISOLATION_AUDIT_ARCHIVE_PROTOCOL, snapshotThroughSequence, sourceStateRootDigest, type: 'manifest', version: 1 });
584
601
  }
585
602
  function encodeFile(manifest, records) {
586
603
  return Buffer.from(`${[canonical(manifest), ...records.map(record => canonical(record))].join('\n')}\n`, 'utf8');
@@ -674,7 +691,7 @@ export function archiveIsolationAudit(options) {
674
691
  if (!disjoint(state.path, archive.path) || (state.device === archive.device && state.inode === archive.inode))
675
692
  fail('invalid-path', 'state root and archive directory must be disjoint');
676
693
  // Do not initialize or bind an archive when the requested source cannot
677
- // first prove that it is a complete schema-6 audit ledger. The locked
694
+ // first prove that it is a complete supported audit ledger. The locked
678
695
  // snapshot below repeats these checks authoritatively.
679
696
  preflightLedger(state);
680
697
  const controlLock = beginArchiveControl(archive, digest(state.path));
@@ -686,6 +703,7 @@ export function archiveIsolationAudit(options) {
686
703
  ledgerDescriptor = snapshot.descriptor;
687
704
  transaction = true;
688
705
  const highWater = snapshot.highWater;
706
+ const ledgerSchema = database.prepare('PRAGMA user_version').get().user_version;
689
707
  if (existing.verification.highestSequence > highWater || existing.verification.snapshotThroughSequence > highWater)
690
708
  fail('conflict', 'archive does not match this isolation ledger');
691
709
  let archivedIndex = 0;
@@ -703,7 +721,7 @@ export function archiveIsolationAudit(options) {
703
721
  let afterSequence = existing.verification.highestSequence;
704
722
  let previousDigest = existing.verification.headDigest;
705
723
  if (highWater === 0 && previousDigest === null) {
706
- const publication = publish(archive, encodeFile(makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, 1, 0, 0, 0, null), []));
724
+ const publication = publish(archive, encodeFile(makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, 1, 0, 0, 0, null, ledgerSchema), []));
707
725
  if (publication.created)
708
726
  createdFiles.push(publication.name);
709
727
  previousDigest = publication.digest;
@@ -719,7 +737,7 @@ export function archiveIsolationAudit(options) {
719
737
  while (cursor < decoded.length && records.length < input.batchSize) {
720
738
  const candidate = decoded[cursor];
721
739
  const next = [...records, candidate];
722
- const provisional = makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, next[0].sequence, next.at(-1).sequence, next.length, highWater, previousDigest);
740
+ const provisional = makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, next[0].sequence, next.at(-1).sequence, next.length, highWater, previousDigest, ledgerSchema);
723
741
  if (encodeFile(provisional, next).byteLength > MAX_ARCHIVE_FILE_BYTES) {
724
742
  if (records.length === 0)
725
743
  fail('invalid-source', 'one audit record exceeds the archive file bound');
@@ -728,7 +746,7 @@ export function archiveIsolationAudit(options) {
728
746
  records.push(candidate);
729
747
  cursor += 1;
730
748
  }
731
- const manifest = makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, records[0].sequence, records.at(-1).sequence, records.length, highWater, previousDigest);
749
+ const manifest = makeManifest(controlLock.archiveInstanceId, controlLock.sourceStateRootDigest, records[0].sequence, records.at(-1).sequence, records.length, highWater, previousDigest, ledgerSchema);
732
750
  const publication = publish(archive, encodeFile(manifest, records));
733
751
  if (publication.created) {
734
752
  createdFiles.push(publication.name);