@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.
- package/README.md +287 -9
- package/lib/audit-archive.d.ts +1 -1
- package/lib/audit-archive.d.ts.map +1 -1
- package/lib/audit-archive.js +31 -13
- package/lib/audit-archive.js.map +1 -1
- package/lib/cli.d.ts +7 -3
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +104 -23
- package/lib/cli.js.map +1 -1
- package/lib/config.d.ts +15 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +49 -9
- package/lib/config.js.map +1 -1
- package/lib/diagnostics.js +2 -2
- package/lib/diagnostics.js.map +1 -1
- package/lib/index.d.ts +17 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +17 -1
- package/lib/index.js.map +1 -1
- package/lib/ledger.d.ts +19 -2
- package/lib/ledger.d.ts.map +1 -1
- package/lib/ledger.js +371 -44
- package/lib/ledger.js.map +1 -1
- package/lib/linux-cgroups.d.ts +75 -0
- package/lib/linux-cgroups.d.ts.map +1 -0
- package/lib/linux-cgroups.js +517 -0
- package/lib/linux-cgroups.js.map +1 -0
- package/lib/linux-execution-contract.d.ts +123 -0
- package/lib/linux-execution-contract.d.ts.map +1 -0
- package/lib/linux-execution-contract.js +279 -0
- package/lib/linux-execution-contract.js.map +1 -0
- package/lib/linux-execution.d.ts +123 -0
- package/lib/linux-execution.d.ts.map +1 -0
- package/lib/linux-execution.js +1264 -0
- package/lib/linux-execution.js.map +1 -0
- package/lib/linux-job-recovery.d.ts +5 -0
- package/lib/linux-job-recovery.d.ts.map +1 -0
- package/lib/linux-job-recovery.js +63 -0
- package/lib/linux-job-recovery.js.map +1 -0
- package/lib/linux-namespace.d.ts +80 -0
- package/lib/linux-namespace.d.ts.map +1 -0
- package/lib/linux-namespace.js +471 -0
- package/lib/linux-namespace.js.map +1 -0
- package/lib/linux-repository-admission.d.ts +26 -0
- package/lib/linux-repository-admission.d.ts.map +1 -0
- package/lib/linux-repository-admission.js +64 -0
- package/lib/linux-repository-admission.js.map +1 -0
- package/lib/linux-repository-runner.d.ts +54 -0
- package/lib/linux-repository-runner.d.ts.map +1 -0
- package/lib/linux-repository-runner.js +614 -0
- package/lib/linux-repository-runner.js.map +1 -0
- package/lib/linux-runner-state-migration.d.ts +94 -0
- package/lib/linux-runner-state-migration.d.ts.map +1 -0
- package/lib/linux-runner-state-migration.js +463 -0
- package/lib/linux-runner-state-migration.js.map +1 -0
- package/lib/linux-runner-state.d.ts +53 -0
- package/lib/linux-runner-state.d.ts.map +1 -0
- package/lib/linux-runner-state.js +332 -0
- package/lib/linux-runner-state.js.map +1 -0
- package/lib/linux-runtime-source.d.ts +28 -0
- package/lib/linux-runtime-source.d.ts.map +1 -0
- package/lib/linux-runtime-source.js +237 -0
- package/lib/linux-runtime-source.js.map +1 -0
- package/lib/linux-runtime.d.ts +37 -0
- package/lib/linux-runtime.d.ts.map +1 -0
- package/lib/linux-runtime.js +192 -0
- package/lib/linux-runtime.js.map +1 -0
- package/lib/linux-system-snapshot.d.ts +47 -0
- package/lib/linux-system-snapshot.d.ts.map +1 -0
- package/lib/linux-system-snapshot.js +704 -0
- package/lib/linux-system-snapshot.js.map +1 -0
- package/lib/linux-user-namespace.d.ts +79 -0
- package/lib/linux-user-namespace.d.ts.map +1 -0
- package/lib/linux-user-namespace.js +234 -0
- package/lib/linux-user-namespace.js.map +1 -0
- package/lib/linux-verifier-runner.d.ts +27 -0
- package/lib/linux-verifier-runner.d.ts.map +1 -0
- package/lib/linux-verifier-runner.js +346 -0
- package/lib/linux-verifier-runner.js.map +1 -0
- package/lib/native-cgroup-roots.d.ts +20 -0
- package/lib/native-cgroup-roots.d.ts.map +1 -0
- package/lib/native-cgroup-roots.js +121 -0
- package/lib/native-cgroup-roots.js.map +1 -0
- package/lib/native-resources.d.ts +44 -0
- package/lib/native-resources.d.ts.map +1 -0
- package/lib/native-resources.js +354 -0
- package/lib/native-resources.js.map +1 -0
- package/lib/probe.d.ts +10 -0
- package/lib/probe.d.ts.map +1 -1
- package/lib/probe.js +52 -2
- package/lib/probe.js.map +1 -1
- package/lib/service.d.ts +1 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +122 -9
- package/lib/service.js.map +1 -1
- package/lib/storage.d.ts.map +1 -1
- package/lib/storage.js +36 -6
- package/lib/storage.js.map +1 -1
- package/lib/tools.js +1 -1
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +5 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/verifier-runner.d.ts +4 -2
- package/lib/verifier-runner.d.ts.map +1 -1
- package/lib/verifier-runner.js +22 -1
- package/lib/verifier-runner.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +15 -10
- package/runtime/linux-candidate-entry.py +82 -0
- package/runtime/linux-namespace-worker.mjs +561 -0
- package/runtime/linux-seccomp-policy.LICENSE +202 -0
- package/runtime/linux-seccomp-policy.json +886 -0
- package/runtime/linux-seccomp.py +106 -0
- package/runtime/linux-userns-launcher.py +149 -0
- 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
|
|
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,
|
|
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
|
|
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
|
|
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 |
|
|
198
|
-
| Subprocess |
|
|
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
|
|
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.
|
package/lib/audit-archive.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/lib/audit-archive.js
CHANGED
|
@@ -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
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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 !==
|
|
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
|
|
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
|
|
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);
|