@xioflow/kernel 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -15
- package/dist/capability/check.d.ts +32 -0
- package/dist/capability/check.d.ts.map +1 -0
- package/dist/capability/check.js +136 -0
- package/dist/capability/check.js.map +1 -0
- package/dist/capability/index.d.ts +3 -0
- package/dist/capability/index.d.ts.map +1 -0
- package/dist/capability/index.js +3 -0
- package/dist/capability/index.js.map +1 -0
- package/dist/capability/path-utils.d.ts +11 -0
- package/dist/capability/path-utils.d.ts.map +1 -0
- package/dist/capability/path-utils.js +46 -0
- package/dist/capability/path-utils.js.map +1 -0
- package/dist/confinement/bubblewrap.d.ts +8 -0
- package/dist/confinement/bubblewrap.d.ts.map +1 -0
- package/dist/confinement/bubblewrap.js +45 -0
- package/dist/confinement/bubblewrap.js.map +1 -0
- package/dist/confinement/detector.d.ts +4 -0
- package/dist/confinement/detector.d.ts.map +1 -0
- package/dist/confinement/detector.js +35 -0
- package/dist/confinement/detector.js.map +1 -0
- package/dist/confinement/index.d.ts +5 -0
- package/dist/confinement/index.d.ts.map +1 -0
- package/dist/confinement/index.js +5 -0
- package/dist/confinement/index.js.map +1 -0
- package/dist/confinement/sandbox-exec.d.ts +8 -0
- package/dist/confinement/sandbox-exec.d.ts.map +1 -0
- package/dist/confinement/sandbox-exec.js +30 -0
- package/dist/confinement/sandbox-exec.js.map +1 -0
- package/dist/confinement/srt.d.ts +8 -0
- package/dist/confinement/srt.d.ts.map +1 -0
- package/dist/confinement/srt.js +41 -0
- package/dist/confinement/srt.js.map +1 -0
- package/dist/domain.d.ts +46 -2
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +209 -18
- package/dist/domain.js.map +1 -1
- package/dist/driver/node-driver.d.ts.map +1 -1
- package/dist/driver/node-driver.js +46 -36
- package/dist/driver/node-driver.js.map +1 -1
- package/dist/driver/types.d.ts +13 -0
- package/dist/driver/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/quick-run.d.ts +27 -0
- package/dist/quick-run.d.ts.map +1 -0
- package/dist/quick-run.js +75 -0
- package/dist/quick-run.js.map +1 -0
- package/dist/recovery/engine.d.ts +7 -0
- package/dist/recovery/engine.d.ts.map +1 -1
- package/dist/recovery/engine.js +174 -4
- package/dist/recovery/engine.js.map +1 -1
- package/dist/snapshot/git-shadow.d.ts +49 -0
- package/dist/snapshot/git-shadow.d.ts.map +1 -0
- package/dist/snapshot/git-shadow.js +359 -0
- package/dist/snapshot/git-shadow.js.map +1 -0
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +31 -0
- package/dist/store/schema.js.map +1 -1
- package/dist/store/sqlite.d.ts +17 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +193 -58
- package/dist/store/sqlite.js.map +1 -1
- package/dist/supervisor/drainer.d.ts +17 -0
- package/dist/supervisor/drainer.d.ts.map +1 -0
- package/dist/supervisor/drainer.js +200 -0
- package/dist/supervisor/drainer.js.map +1 -0
- package/dist/supervisor/service.d.ts +24 -0
- package/dist/supervisor/service.d.ts.map +1 -0
- package/dist/supervisor/service.js +588 -0
- package/dist/supervisor/service.js.map +1 -0
- package/dist/supervisor/supervisor.d.ts +62 -4
- package/dist/supervisor/supervisor.d.ts.map +1 -1
- package/dist/supervisor/supervisor.js +1037 -562
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +748 -2
- package/dist/testing/contract-suite.js.map +1 -1
- package/dist/types.d.ts +175 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +39 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Xio-Shark/xioflow/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@xioflow/kernel)
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
-
[](./ARCHITECTURE.md)
|
|
7
|
+
[](./ARCHITECTURE.en.md)
|
|
8
|
+
[](./ARCHITECTURE.md)
|
|
8
9
|
|
|
9
|
-
A supervised execution kernel for AI agent runtimes. Zero runtime dependencies.
|
|
10
|
+
A supervised execution kernel for AI agent runtimes. Zero runtime dependencies. (See [Architecture & Protocol Specification (EN)](./ARCHITECTURE.en.md) / [中文规范](./ARCHITECTURE.md)).
|
|
10
11
|
|
|
11
12
|
Agent runtimes usually call `spawn()` (or `exec()`) and hope for the best. When the host crashes mid-tool-call, or a cancel cannot be confirmed, they are left with orphan processes, double-applied side effects, and no honest record of what actually happened. This kernel makes those states first-class instead of silent.
|
|
12
13
|
|
|
13
|
-
[xiocode](https://github.com/Xio-Shark/xiocode), the reference distribution, runs its supervised commands on this kernel by default
|
|
14
|
+
[xiocode](https://github.com/Xio-Shark/xiocode), the reference distribution, runs its supervised commands on this kernel by default — the equivalence suite below is what made that switch safe to make.
|
|
15
|
+
|
|
16
|
+
Besides one-shot commands, the kernel supervises long-running services (MCP stdio servers, dev servers), makes operations idempotent by `opId` for durable engines (Temporal, LangGraph), and snapshots, rolls back and forks the workspace so an agent's file changes can be undone per step.
|
|
14
17
|
|
|
15
18
|
## Guarantees
|
|
16
19
|
|
|
@@ -21,14 +24,17 @@ Agent runtimes usually call `spawn()` (or `exec()`) and hope for the best. When
|
|
|
21
24
|
| No premature release | Exclusive resource leases are released only after the platform driver confirms the process is gone. An unconfirmed stop keeps the lease and escalates to `indeterminate`. |
|
|
22
25
|
| No silent output loss | In-memory output is capped by `maxOutputBytes` (default 10 MiB); each stream is spilled to `<domain>/artifacts/<opId>-stdout.log` / `-stderr.log`, `fsync`ed, and hashed. Truncation is reported per stream (`stdoutTruncated` / `stderrTruncated`) with `stdoutRef` / `stderrRef`, `stdoutBytes` / `stderrBytes` and `stdoutHash` / `stderrHash`; `isTruncated` / `outputRef` / `outputHash` remain as aggregate compatibility fields. |
|
|
23
26
|
| No implicit environment | `envWhiteList` is exact: whatever you pass is what the child gets, with no injected `PATH`. Without a whitelist the child inherits `process.env`, unless you pass `inheritEnv: false` to get an empty environment. |
|
|
24
|
-
| No orphan leak | Termination escalates SIGINT to SIGTERM to SIGKILL across the process group, then re-enumerates descendants. Escaped (`setsid`) survivors are reported honestly as `{ stopped:
|
|
27
|
+
| No orphan leak | Termination escalates SIGINT to SIGTERM to SIGKILL across the process group, then re-enumerates descendants. Escaped (`setsid`) survivors are reported honestly as `{ stopped: 'cannot_determine', residualPids: [...] }` instead of a fake success. If the group is confirmed empty but the output pipes are still held by a process the driver never saw, the operation is `indeterminate`, not `succeeded`. |
|
|
28
|
+
| No blind retry | Resubmitting an `opId` with the same input fingerprint joins the in-flight execution or replays the recorded result (`replayed: true`); an `indeterminate` result is returned as-is and never re-executed. A different fingerprint throws `OperationIdConflictError`. |
|
|
29
|
+
| No out-of-scope rollback | A rollback only rewrites and deletes files inside the snapshot's declared roots, never touches ignored files unless they were captured, and is verified by fingerprint. `coverage: 'complete'` is claimed only when every operation since the snapshot ran under a confinement driver. |
|
|
25
30
|
| No split brain | A domain has one active owner, held by an exclusive lock file plus a heartbeat lease. Stale owners are fenced by an epoch counter; their writes are rejected. |
|
|
26
31
|
|
|
27
32
|
## Requirements
|
|
28
33
|
|
|
29
|
-
- Node.js >= 22.
|
|
34
|
+
- Node.js >= 22.13 (`node:sqlite` without a flag). CI covers Node 22.13 and 24 on Ubuntu and macOS. `node:sqlite` is still marked experimental upstream, so Node prints an `ExperimentalWarning`; that is expected.
|
|
30
35
|
- Linux and macOS. Descendant enumeration and group termination use `ps(1)` and POSIX process groups.
|
|
31
|
-
-
|
|
36
|
+
- Snapshots need `git` on `PATH` and a git working tree. Confinement is optional and uses `sandbox-exec` (macOS), `bubblewrap` (Linux) or `srt` when available.
|
|
37
|
+
- Windows is not supported. There the driver reports `processGroupKill: false` and `descendantEnumeration: 'none'` rather than pretending it can contain processes.
|
|
32
38
|
|
|
33
39
|
## Install
|
|
34
40
|
|
|
@@ -38,6 +44,36 @@ npm install @xioflow/kernel
|
|
|
38
44
|
|
|
39
45
|
## Quickstart
|
|
40
46
|
|
|
47
|
+
### 1. Simplest Usage (`quickRun`)
|
|
48
|
+
|
|
49
|
+
For standard tool calls and durable activity execution, `quickRun` automatically manages domain acquisition, Task and Run records, and lock lifecycles:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { quickRun } from '@xioflow/kernel';
|
|
53
|
+
|
|
54
|
+
// 1. One-line supervised execution
|
|
55
|
+
const result = await quickRun({
|
|
56
|
+
execPath: 'npm',
|
|
57
|
+
args: ['test'],
|
|
58
|
+
cwd: '/path/to/workspace',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(result.status, result.exitCode, result.stdout);
|
|
62
|
+
|
|
63
|
+
// 2. Idempotent execution (safe retry for Temporal / LangGraph / durable workflows)
|
|
64
|
+
const idemResult = await quickRun(
|
|
65
|
+
{ execPath: 'git', args: ['commit', '-m', 'chore: update'], cwd: '/path/to/workspace' },
|
|
66
|
+
{ opId: 'workflow-step-42' } // Passing opId guarantees at-most-once execution across retries
|
|
67
|
+
);
|
|
68
|
+
if (idemResult.replayed) {
|
|
69
|
+
console.log('Result retrieved from journal without re-executing process');
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Full Architecture & Fine-Grained Supervision
|
|
74
|
+
|
|
75
|
+
When you need granular control over Tasks, Runs, resource locks, streaming output, or multi-phase workflows:
|
|
76
|
+
|
|
41
77
|
```js
|
|
42
78
|
import { ExecutionDomain, NodePlatformDriver, ProcessSupervisor } from '@xioflow/kernel';
|
|
43
79
|
|
|
@@ -89,6 +125,9 @@ domain.close(); // releases the owner lease and the domain lock
|
|
|
89
125
|
|
|
90
126
|
To cancel, call `await supervisor.cancelOperation('op-1', graceMs)`. It returns `{ stopped, scope, residualPids? }`. The pending `executeProcess` promise then resolves with `status: 'cancelled'`, or with `status: 'indeterminate'` when the driver cannot confirm that the process actually stopped, in which case its leases stay locked.
|
|
91
127
|
|
|
128
|
+
> **Note on In-Flight Join and Cancellation**:
|
|
129
|
+
> When an in-flight operation with the same `opId` and fingerprint is joined concurrently, passing an `abortSignal` to the secondary caller only cancels the secondary caller's own wait promise—it never aborts the underlying process or the primary caller's execution. To deliberately terminate the underlying process, explicitly call `supervisor.cancelOperation(opId)`.
|
|
130
|
+
|
|
92
131
|
## Crash recovery
|
|
93
132
|
|
|
94
133
|
After a restart, reacquire the domain and run the recovery engine:
|
|
@@ -111,11 +150,73 @@ For every unfinished operation, recovery verifies the recorded process identity
|
|
|
111
150
|
|
|
112
151
|
The kernel records execution facts (state, output evidence, artifact references) and refuses to guess. Whether a failed test that was later fixed counts as business success is the distribution's decision, not the kernel's.
|
|
113
152
|
|
|
153
|
+
An `indeterminate` operation leaves the kernel only through `domain.adjudicate(opId, verdict, actor, note)`, which re-scans for residual processes, refuses `confirmed_stopped` while any are alive (or while the scan itself fails), and journals the decision before releasing leases.
|
|
154
|
+
|
|
155
|
+
## Long-running services
|
|
156
|
+
|
|
157
|
+
MCP stdio servers, dev servers and watchers are supervised as services. Each instance is a kernel operation (`<serviceId>#<n>`), so a host crash leaves a record that recovery can act on:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const service = await supervisor.startService({
|
|
161
|
+
serviceId: 'mcp-fs',
|
|
162
|
+
runId: 'run-1',
|
|
163
|
+
command: { execPath: 'node', args: ['server.mjs'], cwd: '/path/to/workspace' },
|
|
164
|
+
readiness: { stdoutLine: /listening/, timeoutMs: 5000 }, // or 'spawned'
|
|
165
|
+
restart: { policy: 'on-failure', maxRestarts: 3, backoffMs: 500 },
|
|
166
|
+
requiredResources: ['port:3000'],
|
|
167
|
+
});
|
|
168
|
+
await service.ready; // rejects if the readiness line never appears; the instance is then stopped
|
|
169
|
+
service.stdin.write('...'); // stdin stays open (stdinMode: 'stream'); stdout is passed through
|
|
170
|
+
await service.stop(2000);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Leases are held by the service across restart backoff. A stop the driver cannot confirm records the instance as `indeterminate`, journals `SERVICE_FAILED { reason: 'stop_unconfirmed' }` and keeps the leases. After a host crash, `recover()` stops surviving instances and never restarts them automatically. See [`examples/mcp-stdio-transport`](./examples/mcp-stdio-transport) for an MCP SDK `Transport` on top of this.
|
|
174
|
+
|
|
175
|
+
## Snapshots, rollback and forks
|
|
176
|
+
|
|
177
|
+
The git-shadow driver snapshots declared roots without touching the user's index, HEAD or branch, and pins each snapshot under `refs/xioflow/snapshots/<id>`:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const snap = await supervisor.captureSnapshot({ runId: 'run-1', opId: 'snap-1', roots: ['/path/to/workspace/src'] });
|
|
181
|
+
|
|
182
|
+
// ... the agent edits files ...
|
|
183
|
+
|
|
184
|
+
const rb = await supervisor.rollback({ runId: 'run-1', opId: 'rb-1', snapshotId: snap.snapshot.id });
|
|
185
|
+
console.log(rb.status, rb.coverage, rb.outOfScopeEffects); // e.g. 'restored', 'declared_roots', 'possible'
|
|
186
|
+
|
|
187
|
+
// Fork the snapshot into an independent worktree for a parallel candidate:
|
|
188
|
+
await supervisor.materialize(snap.snapshot.id, '/tmp/candidate-a');
|
|
189
|
+
await supervisor.dematerialize('/tmp/candidate-a');
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Rollback only rewrites and deletes files inside the snapshot's roots and leaves ignored files alone unless the snapshot used `includeIgnored: true`. It is verified by fingerprint against the snapshot tree. `coverage` is `complete` only when every operation since the snapshot ran confined; otherwise it is `declared_roots` with `outOfScopeEffects: 'possible'`.
|
|
193
|
+
|
|
194
|
+
To make `complete` reachable, bind operations to a capability and run them confined:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
const cap = domain.issueCapability(
|
|
198
|
+
{ write: ['/path/to/workspace/src'], exclusive: ['workspace:write:/path/to/workspace/src'] },
|
|
199
|
+
'my-agent-policy',
|
|
200
|
+
10 * 60_000,
|
|
201
|
+
);
|
|
202
|
+
await supervisor.executeProcess({
|
|
203
|
+
runId: 'run-1',
|
|
204
|
+
opId: 'op-edit',
|
|
205
|
+
name: 'codegen',
|
|
206
|
+
command: { execPath: 'node', args: ['../codegen.mjs'], cwd: '/path/to/workspace/src' },
|
|
207
|
+
// mutation roots default to cwd; pass mutationRoots to declare others
|
|
208
|
+
capabilityId: cap.id, // out-of-scope resources or paths are rejected at admission
|
|
209
|
+
confinement: true, // sandbox-exec / bubblewrap / srt, whichever is available
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A capability can be narrowed with `domain.attenuate`, and revoked with `domain.revokeCapability`. Confinement exists so the rollback coverage claim is true. It is not a security boundary.
|
|
214
|
+
|
|
114
215
|
## Design notes
|
|
115
216
|
|
|
116
217
|
### Verify your own runtime
|
|
117
218
|
|
|
118
|
-
The package ships the same
|
|
219
|
+
The package ships the same 44-item contract suite that the kernel itself is tested against, so an embedding runtime can prove its consumer honors these guarantees:
|
|
119
220
|
|
|
120
221
|
```js
|
|
121
222
|
import { defineContractTestSuite } from '@xioflow/kernel/testing';
|
|
@@ -137,31 +238,31 @@ defineContractTestSuite('My Agent Runtime', async () => ({
|
|
|
137
238
|
- When the root process exits while a descendant still holds its pipes, the supervisor reaps the process group and reports the root's real exit facts with `residualProcessesReaped: true`, instead of blocking the operation until its timeout.
|
|
138
239
|
- A spawn failure is reported both as `status: 'failed'`/`exitCode: 127` and as `spawnFailure` with the underlying error message, so embedders can tell "the binary could not start" apart from "the child exited 127".
|
|
139
240
|
- `onStreamChunk(stream, chunk)` forwards raw stdout/stderr chunks while the process runs, so a caller can render live output. A throwing callback never aborts the drain; the first error is recorded as `streamCallbackError`.
|
|
140
|
-
- Hard resource requests (`maxMemoryBytes`, `maxPids`, `maxCpuTimeMs` with `enforcement: 'hard'`) are rejected at admission with `UnsupportedCapabilityError` on platforms that cannot enforce them, instead of degrading silently. The remaining capability flags are reported truthfully for callers to inspect, but are not enforced by admission
|
|
141
|
-
- Protocol specification: [`ARCHITECTURE.md`](./ARCHITECTURE.md) (Chinese).
|
|
241
|
+
- Hard resource requests (`maxMemoryBytes`, `maxPids`, `maxCpuTimeMs` with `enforcement: 'hard'`) are rejected at admission with `UnsupportedCapabilityError` on platforms that cannot enforce them, instead of degrading silently. The remaining capability flags are reported truthfully for callers to inspect, but are not enforced by admission yet.
|
|
242
|
+
- Protocol specification: [`ARCHITECTURE.md`](./ARCHITECTURE.md) (Chinese, complete) and [`ARCHITECTURE.en.md`](./ARCHITECTURE.en.md) (English, §0, §3 and §7). They describe the target design; the phased plan and the spec-vs-implementation gap table live in [`ROADMAP.md`](./ROADMAP.md).
|
|
142
243
|
|
|
143
244
|
## Status
|
|
144
245
|
|
|
145
|
-
0.
|
|
246
|
+
0.4.0 on npm (services, snapshots/rollback/forks, capabilities and confinement; see [`CHANGELOG.md`](./CHANGELOG.md)), pre-1.0: the API may change. Not implemented yet: Linux cgroup v2 backend (hard memory/CPU/PID enforcement), enforced domain-wide memory budgets, artifact retrieval helpers, daemon mode and non-TypeScript bindings. Remaining gaps are tracked in [`ROADMAP.md`](./ROADMAP.md).
|
|
146
247
|
|
|
147
248
|
## Releasing
|
|
148
249
|
|
|
149
250
|
Releases are tag-driven. A tag always produces a GitHub Release; the npm upload is switched on separately, so a tag can never ship an artifact that is not traceable to this repository:
|
|
150
251
|
|
|
151
252
|
1. Bump `version` in `package.json`, commit, and push to `main`.
|
|
152
|
-
2. Push the matching tag, e.g. `git tag v0.
|
|
253
|
+
2. Push the matching tag, e.g. `git tag v0.3.0 && git push origin v0.3.0`.
|
|
153
254
|
3. `.github/workflows/release.yml` re-runs typecheck, tests and the pack smoke test, refuses a tag that does not match `package.json`, creates the GitHub Release with the packed tarball, `SHA256SUMS` and a build provenance attestation attached, and — only when the repository variable `NPM_TRUSTED_PUBLISHING_ENABLED` is `true` — publishes with `npm publish --provenance` and verifies the attestation.
|
|
154
255
|
|
|
155
256
|
One-time setup, in this order: (1) npmjs.com → package settings → Trusted Publisher → GitHub Actions: organization `Xio-Shark`, repository `xioflow`, workflow filename `release.yml`, environment left empty; (2) set the repository variable `NPM_TRUSTED_PUBLISHING_ENABLED=true`. Until both exist the publish job is skipped and the reason is printed in the `verify` job's log. npm's registry index can lag several minutes behind an upload, so the verification step polls and may need a job re-run.
|
|
156
257
|
|
|
157
|
-
To attach assets to an existing tag without publishing to npm, run the workflow manually: `gh workflow run release.yml -f tag=v0.
|
|
258
|
+
To attach assets to an existing tag without publishing to npm, run the workflow manually: `gh workflow run release.yml -f tag=v0.3.0`. An asset that is already attached is kept if identical and fails the run if it differs; published assets are never overwritten.
|
|
158
259
|
|
|
159
260
|
### Verifying a release
|
|
160
261
|
|
|
161
262
|
```bash
|
|
162
|
-
gh release download v0.
|
|
263
|
+
gh release download v0.3.0 --repo Xio-Shark/xioflow
|
|
163
264
|
shasum -a 256 -c SHA256SUMS
|
|
164
|
-
gh attestation verify xioflow-kernel-0.
|
|
265
|
+
gh attestation verify xioflow-kernel-0.3.0.tgz --repo Xio-Shark/xioflow
|
|
165
266
|
npm audit signatures # inside a project that installed @xioflow/kernel from npm
|
|
166
267
|
```
|
|
167
268
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Capability, CapabilityScope } from '../types.js';
|
|
2
|
+
import { resolveRealPath, isPathContained } from './path-utils.js';
|
|
3
|
+
import { SqliteStore } from '../store/sqlite.js';
|
|
4
|
+
export { resolveRealPath, isPathContained };
|
|
5
|
+
/**
|
|
6
|
+
* 规范化 Capability 作用域
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeScope(scope: CapabilityScope): CapabilityScope;
|
|
9
|
+
/**
|
|
10
|
+
* 校验并生成收窄的作用域(禁止放宽)
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateScopeNarrowing(parentScope: CapabilityScope, narrowerScope: Partial<CapabilityScope>): CapabilityScope;
|
|
13
|
+
export interface CheckCapabilityAdmissionOptions {
|
|
14
|
+
domainId: string;
|
|
15
|
+
currentEpoch: number;
|
|
16
|
+
store: SqliteStore;
|
|
17
|
+
capabilityId: string;
|
|
18
|
+
requiredResources?: string[];
|
|
19
|
+
mutationRoots?: string[];
|
|
20
|
+
runId?: string;
|
|
21
|
+
opId?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface CapabilityAdmissionResult {
|
|
24
|
+
capability: Capability;
|
|
25
|
+
effectiveResources: string[];
|
|
26
|
+
effectiveMutationRoots: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 准入校验:在意图登记前校验结构完整性与作用域合法性(契约 #55)
|
|
30
|
+
*/
|
|
31
|
+
export declare function checkCapabilityAdmission(options: CheckCapabilityAdmissionOptions): CapabilityAdmissionResult;
|
|
32
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/capability/check.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,eAAe,EAEhB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,CAAC;AAE5C;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,eAAe,CAKtE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,eAAe,EAC5B,aAAa,EAAE,OAAO,CAAC,eAAe,CAAC,GACtC,eAAe,CAmCjB;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,WAAW,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,sBAAsB,EAAE,MAAM,EAAE,CAAC;CAClC;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,+BAA+B,GACvC,yBAAyB,CAwG3B"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { CapabilityViolationError, } from '../types.js';
|
|
2
|
+
import { resolveRealPath, isPathContained } from './path-utils.js';
|
|
3
|
+
export { resolveRealPath, isPathContained };
|
|
4
|
+
/**
|
|
5
|
+
* 规范化 Capability 作用域
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeScope(scope) {
|
|
8
|
+
return {
|
|
9
|
+
write: scope.write.map((w) => resolveRealPath(w)),
|
|
10
|
+
exclusive: [...new Set(scope.exclusive)],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 校验并生成收窄的作用域(禁止放宽)
|
|
15
|
+
*/
|
|
16
|
+
export function validateScopeNarrowing(parentScope, narrowerScope) {
|
|
17
|
+
const normParent = normalizeScope(parentScope);
|
|
18
|
+
let newWrite = normParent.write;
|
|
19
|
+
if (narrowerScope.write !== undefined) {
|
|
20
|
+
const resolvedWrites = narrowerScope.write.map((w) => resolveRealPath(w));
|
|
21
|
+
for (const rw of resolvedWrites) {
|
|
22
|
+
const allowed = normParent.write.some((pw) => isPathContained(pw, rw));
|
|
23
|
+
if (!allowed) {
|
|
24
|
+
throw new CapabilityViolationError('attenuation_widened', `Cannot widen writable scope: '${rw}' is not contained within parent scope [${normParent.write.join(', ')}]`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
newWrite = resolvedWrites;
|
|
28
|
+
}
|
|
29
|
+
let newExclusive = normParent.exclusive;
|
|
30
|
+
if (narrowerScope.exclusive !== undefined) {
|
|
31
|
+
for (const res of narrowerScope.exclusive) {
|
|
32
|
+
if (!normParent.exclusive.includes(res)) {
|
|
33
|
+
throw new CapabilityViolationError('attenuation_widened', `Cannot widen exclusive resource scope: '${res}' is not in parent exclusive scope [${normParent.exclusive.join(', ')}]`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
newExclusive = [...new Set(narrowerScope.exclusive)];
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
write: newWrite,
|
|
40
|
+
exclusive: newExclusive,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 准入校验:在意图登记前校验结构完整性与作用域合法性(契约 #55)
|
|
45
|
+
*/
|
|
46
|
+
export function checkCapabilityAdmission(options) {
|
|
47
|
+
const { domainId, currentEpoch, store, capabilityId, runId, opId } = options;
|
|
48
|
+
const reject = (reason, message) => {
|
|
49
|
+
const err = new CapabilityViolationError(reason, message);
|
|
50
|
+
try {
|
|
51
|
+
store.recordJournalEvent({
|
|
52
|
+
domainId,
|
|
53
|
+
runId,
|
|
54
|
+
operationId: opId,
|
|
55
|
+
type: 'CAPABILITY_REJECTED',
|
|
56
|
+
payload: {
|
|
57
|
+
capabilityId,
|
|
58
|
+
reason,
|
|
59
|
+
message,
|
|
60
|
+
requiredResources: options.requiredResources,
|
|
61
|
+
mutationRoots: options.mutationRoots,
|
|
62
|
+
},
|
|
63
|
+
timestamp: new Date().toISOString(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// 保证即便 journal 写入异常也必须抛出 CapabilityViolationError
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
};
|
|
71
|
+
const cap = store.getCapability(capabilityId);
|
|
72
|
+
if (!cap) {
|
|
73
|
+
return reject('revoked', `Capability '${capabilityId}' not found`);
|
|
74
|
+
}
|
|
75
|
+
if (cap.epoch !== currentEpoch) {
|
|
76
|
+
return reject('epoch_mismatch', `Capability '${capabilityId}' epoch ${cap.epoch} does not match current domain epoch ${currentEpoch}`);
|
|
77
|
+
}
|
|
78
|
+
if (new Date(cap.expiresAt).getTime() <= Date.now()) {
|
|
79
|
+
return reject('expired', `Capability '${capabilityId}' expired at ${cap.expiresAt}`);
|
|
80
|
+
}
|
|
81
|
+
if (cap.revokedAt) {
|
|
82
|
+
return reject('revoked', `Capability '${capabilityId}' was revoked at ${cap.revokedAt}`);
|
|
83
|
+
}
|
|
84
|
+
// 递归校验祖先链,父撤销/过期/换代导致子级联失效
|
|
85
|
+
let ancestorId = cap.parentId;
|
|
86
|
+
while (ancestorId) {
|
|
87
|
+
const ancestor = store.getCapability(ancestorId);
|
|
88
|
+
if (!ancestor) {
|
|
89
|
+
return reject('revoked', `Capability ancestor '${ancestorId}' not found`);
|
|
90
|
+
}
|
|
91
|
+
if (ancestor.revokedAt) {
|
|
92
|
+
return reject('revoked', `Capability ancestor '${ancestorId}' was revoked`);
|
|
93
|
+
}
|
|
94
|
+
if (ancestor.epoch !== currentEpoch) {
|
|
95
|
+
return reject('epoch_mismatch', `Capability ancestor '${ancestorId}' epoch mismatch`);
|
|
96
|
+
}
|
|
97
|
+
if (new Date(ancestor.expiresAt).getTime() <= Date.now()) {
|
|
98
|
+
return reject('expired', `Capability ancestor '${ancestorId}' has expired`);
|
|
99
|
+
}
|
|
100
|
+
ancestorId = ancestor.parentId;
|
|
101
|
+
}
|
|
102
|
+
// 1. 资源推导与越界检查
|
|
103
|
+
let effectiveResources;
|
|
104
|
+
if (options.requiredResources !== undefined && options.requiredResources.length > 0) {
|
|
105
|
+
for (const res of options.requiredResources) {
|
|
106
|
+
if (!cap.scope.exclusive.includes(res)) {
|
|
107
|
+
return reject('out_of_scope_resource', `Resource '${res}' is not permitted by capability '${capabilityId}' exclusive scope [${cap.scope.exclusive.join(', ')}]`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
effectiveResources = [...options.requiredResources];
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
effectiveResources = [...cap.scope.exclusive];
|
|
114
|
+
}
|
|
115
|
+
// 2. 写入根推导与越界检查 (按路径段真实路径包含判定)
|
|
116
|
+
let effectiveMutationRoots;
|
|
117
|
+
if (options.mutationRoots !== undefined && options.mutationRoots.length > 0) {
|
|
118
|
+
const resolvedRoots = options.mutationRoots.map((r) => resolveRealPath(r));
|
|
119
|
+
for (const rr of resolvedRoots) {
|
|
120
|
+
const allowed = cap.scope.write.some((cw) => isPathContained(cw, rr));
|
|
121
|
+
if (!allowed) {
|
|
122
|
+
return reject('out_of_scope_path', `Mutation root '${rr}' is outside capability '${capabilityId}' writable scope [${cap.scope.write.join(', ')}]`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
effectiveMutationRoots = resolvedRoots;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
effectiveMutationRoots = [...cap.scope.write];
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
capability: cap,
|
|
132
|
+
effectiveResources,
|
|
133
|
+
effectiveMutationRoots,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/capability/check.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/capability/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/capability/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 规范化真实路径(处理软链接、.. 与大小写)
|
|
3
|
+
* 若路径不存在,回溯寻找最近存在的祖先目录 realpath 后拼接剩余段
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolveRealPath(p: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* 路径包含判定:按路径段严格比对(/a/b 不包含 /a/bc)
|
|
8
|
+
* 双方均做 realpath,杜绝符号链接逃逸与 .. 越界
|
|
9
|
+
*/
|
|
10
|
+
export declare function isPathContained(parentPath: string, childPath: string): boolean;
|
|
11
|
+
//# sourceMappingURL=path-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../src/capability/path-utils.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAqBjD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAS9E"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* 规范化真实路径(处理软链接、.. 与大小写)
|
|
5
|
+
* 若路径不存在,回溯寻找最近存在的祖先目录 realpath 后拼接剩余段
|
|
6
|
+
*/
|
|
7
|
+
export function resolveRealPath(p) {
|
|
8
|
+
const resolved = path.resolve(p);
|
|
9
|
+
try {
|
|
10
|
+
return fs.realpathSync(resolved);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
const parts = [];
|
|
14
|
+
let cur = resolved;
|
|
15
|
+
while (!fs.existsSync(cur)) {
|
|
16
|
+
const parent = path.dirname(cur);
|
|
17
|
+
if (parent === cur)
|
|
18
|
+
break;
|
|
19
|
+
parts.unshift(path.basename(cur));
|
|
20
|
+
cur = parent;
|
|
21
|
+
}
|
|
22
|
+
let realBase = cur;
|
|
23
|
+
try {
|
|
24
|
+
realBase = fs.realpathSync(cur);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
realBase = cur;
|
|
28
|
+
}
|
|
29
|
+
return parts.length > 0 ? path.join(realBase, ...parts) : realBase;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 路径包含判定:按路径段严格比对(/a/b 不包含 /a/bc)
|
|
34
|
+
* 双方均做 realpath,杜绝符号链接逃逸与 .. 越界
|
|
35
|
+
*/
|
|
36
|
+
export function isPathContained(parentPath, childPath) {
|
|
37
|
+
const realParent = resolveRealPath(parentPath);
|
|
38
|
+
const realChild = resolveRealPath(childPath);
|
|
39
|
+
if (realParent === realChild)
|
|
40
|
+
return true;
|
|
41
|
+
if (realParent === path.sep)
|
|
42
|
+
return true;
|
|
43
|
+
const parentWithSep = realParent.endsWith(path.sep) ? realParent : realParent + path.sep;
|
|
44
|
+
return realChild.startsWith(parentWithSep);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=path-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../../src/capability/path-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,CAAS;IACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,GAAG,GAAG,QAAQ,CAAC;QACnB,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,MAAM,KAAK,GAAG;gBAAE,MAAM;YAC1B,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;YAClC,GAAG,GAAG,MAAM,CAAC;QACf,CAAC;QACD,IAAI,QAAQ,GAAG,GAAG,CAAC;QACnB,IAAI,CAAC;YACH,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,GAAG,GAAG,CAAC;QACjB,CAAC;QACD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACrE,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,SAAiB;IACnE,MAAM,UAAU,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;IAE7C,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,UAAU,KAAK,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,aAAa,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC;IACzF,OAAO,SAAS,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ConfinementDriver } from '../types.js';
|
|
2
|
+
import { StructuredCommand } from '../driver/types.js';
|
|
3
|
+
export declare class BubblewrapConfinementDriver implements ConfinementDriver {
|
|
4
|
+
readonly name = "bubblewrap";
|
|
5
|
+
static isAvailable(): boolean;
|
|
6
|
+
wrap(command: StructuredCommand, writableRoots: string[]): StructuredCommand;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=bubblewrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bubblewrap.d.ts","sourceRoot":"","sources":["../../src/confinement/bubblewrap.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAiBvD,qBAAa,2BAA4B,YAAW,iBAAiB;IACnE,SAAgB,IAAI,gBAAgB;IAEpC,OAAc,WAAW,IAAI,OAAO,CAEnC;IAEM,IAAI,CAAC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAwBlF;CACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveRealPath } from '../capability/path-utils.js';
|
|
4
|
+
function findExecutable(name) {
|
|
5
|
+
const envPath = process.env.PATH || '';
|
|
6
|
+
const dirs = envPath.split(path.delimiter);
|
|
7
|
+
for (const dir of dirs) {
|
|
8
|
+
const candidate = path.join(dir, name);
|
|
9
|
+
try {
|
|
10
|
+
if (fs.existsSync(candidate) && (fs.statSync(candidate).mode & 0o111)) {
|
|
11
|
+
return candidate;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
catch { }
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
export class BubblewrapConfinementDriver {
|
|
19
|
+
name = 'bubblewrap';
|
|
20
|
+
static isAvailable() {
|
|
21
|
+
return process.platform === 'linux' && findExecutable('bwrap') !== null;
|
|
22
|
+
}
|
|
23
|
+
wrap(command, writableRoots) {
|
|
24
|
+
const bwrapPath = findExecutable('bwrap');
|
|
25
|
+
if (!bwrapPath || process.platform !== 'linux') {
|
|
26
|
+
throw new Error("Confinement driver 'bubblewrap' is not available on this platform");
|
|
27
|
+
}
|
|
28
|
+
const realRoots = writableRoots.map((r) => resolveRealPath(r));
|
|
29
|
+
const bwrapArgs = [
|
|
30
|
+
'--ro-bind', '/', '/',
|
|
31
|
+
'--dev', '/dev',
|
|
32
|
+
'--proc', '/proc',
|
|
33
|
+
];
|
|
34
|
+
for (const root of realRoots) {
|
|
35
|
+
bwrapArgs.push('--bind', root, root);
|
|
36
|
+
}
|
|
37
|
+
bwrapArgs.push('--', command.execPath, ...command.args);
|
|
38
|
+
return {
|
|
39
|
+
...command,
|
|
40
|
+
execPath: bwrapPath,
|
|
41
|
+
args: bwrapArgs,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=bubblewrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bubblewrap.js","sourceRoot":"","sources":["../../src/confinement/bubblewrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,SAAS,cAAc,CAAC,IAAY;IAClC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IACvC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC3C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC;YACH,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;gBACtE,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,OAAO,2BAA2B;IACtB,IAAI,GAAG,YAAY,CAAC;IAE7B,MAAM,CAAC,WAAW;QACvB,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,cAAc,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IAC1E,CAAC;IAEM,IAAI,CAAC,OAA0B,EAAE,aAAuB;QAC7D,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;QAC1C,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;QACvF,CAAC;QAED,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/D,MAAM,SAAS,GAAa;YAC1B,WAAW,EAAE,GAAG,EAAE,GAAG;YACrB,OAAO,EAAE,MAAM;YACf,QAAQ,EAAE,OAAO;SAClB,CAAC;QAEF,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACvC,CAAC;QAED,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAExD,OAAO;YACL,GAAG,OAAO;YACV,QAAQ,EAAE,SAAS;YACnB,IAAI,EAAE,SAAS;SAChB,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../../src/confinement/detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKhD,wBAAgB,iCAAiC,IAAI,MAAM,EAAE,CAY5D;AAED,wBAAgB,uBAAuB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAa/E"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SandboxExecConfinementDriver } from './sandbox-exec.js';
|
|
2
|
+
import { BubblewrapConfinementDriver } from './bubblewrap.js';
|
|
3
|
+
import { SrtConfinementDriver } from './srt.js';
|
|
4
|
+
export function detectAvailableConfinementDrivers() {
|
|
5
|
+
const drivers = [];
|
|
6
|
+
if (SrtConfinementDriver.isAvailable()) {
|
|
7
|
+
drivers.push('srt');
|
|
8
|
+
}
|
|
9
|
+
if (SandboxExecConfinementDriver.isAvailable()) {
|
|
10
|
+
drivers.push('sandbox-exec');
|
|
11
|
+
}
|
|
12
|
+
if (BubblewrapConfinementDriver.isAvailable()) {
|
|
13
|
+
drivers.push('bubblewrap');
|
|
14
|
+
}
|
|
15
|
+
return drivers;
|
|
16
|
+
}
|
|
17
|
+
export function createConfinementDriver(name) {
|
|
18
|
+
if (name === 'sandbox-exec')
|
|
19
|
+
return new SandboxExecConfinementDriver();
|
|
20
|
+
if (name === 'bubblewrap')
|
|
21
|
+
return new BubblewrapConfinementDriver();
|
|
22
|
+
if (name === 'srt')
|
|
23
|
+
return new SrtConfinementDriver();
|
|
24
|
+
if (!name) {
|
|
25
|
+
if (SandboxExecConfinementDriver.isAvailable())
|
|
26
|
+
return new SandboxExecConfinementDriver();
|
|
27
|
+
if (BubblewrapConfinementDriver.isAvailable())
|
|
28
|
+
return new BubblewrapConfinementDriver();
|
|
29
|
+
if (SrtConfinementDriver.isAvailable())
|
|
30
|
+
return new SrtConfinementDriver();
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Unknown confinement driver: '${name}'`);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.js","sourceRoot":"","sources":["../../src/confinement/detector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEhD,MAAM,UAAU,iCAAiC;IAC/C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,oBAAoB,CAAC,WAAW,EAAE,EAAE,CAAC;QACvC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IACD,IAAI,4BAA4B,CAAC,WAAW,EAAE,EAAE,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC/B,CAAC;IACD,IAAI,2BAA2B,CAAC,WAAW,EAAE,EAAE,CAAC;QAC9C,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,IAAa;IACnD,IAAI,IAAI,KAAK,cAAc;QAAE,OAAO,IAAI,4BAA4B,EAAE,CAAC;IACvE,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,2BAA2B,EAAE,CAAC;IACpE,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,IAAI,oBAAoB,EAAE,CAAC;IAEtD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,4BAA4B,CAAC,WAAW,EAAE;YAAE,OAAO,IAAI,4BAA4B,EAAE,CAAC;QAC1F,IAAI,2BAA2B,CAAC,WAAW,EAAE;YAAE,OAAO,IAAI,2BAA2B,EAAE,CAAC;QACxF,IAAI,oBAAoB,CAAC,WAAW,EAAE;YAAE,OAAO,IAAI,oBAAoB,EAAE,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,GAAG,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/confinement/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/confinement/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ConfinementDriver } from '../types.js';
|
|
2
|
+
import { StructuredCommand } from '../driver/types.js';
|
|
3
|
+
export declare class SandboxExecConfinementDriver implements ConfinementDriver {
|
|
4
|
+
readonly name = "sandbox-exec";
|
|
5
|
+
static isAvailable(): boolean;
|
|
6
|
+
wrap(command: StructuredCommand, writableRoots: string[]): StructuredCommand;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=sandbox-exec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-exec.d.ts","sourceRoot":"","sources":["../../src/confinement/sandbox-exec.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvD,qBAAa,4BAA6B,YAAW,iBAAiB;IACpE,SAAgB,IAAI,kBAAkB;IAEtC,OAAc,WAAW,IAAI,OAAO,CAEnC;IAEM,IAAI,CAAC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,iBAAiB,CAwBlF;CACF"}
|