@xioflow/kernel 0.3.0 → 0.5.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 +174 -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/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +94 -0
- package/dist/cli.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 +26 -2
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +192 -18
- package/dist/domain.js.map +1 -1
- package/dist/driver/node-driver.d.ts.map +1 -1
- package/dist/driver/node-driver.js +42 -61
- package/dist/driver/node-driver.js.map +1 -1
- package/dist/driver/reaper-driver.d.ts +62 -0
- package/dist/driver/reaper-driver.d.ts.map +1 -0
- package/dist/driver/reaper-driver.js +371 -0
- package/dist/driver/reaper-driver.js.map +1 -0
- package/dist/driver/spawn-support.d.ts +4 -0
- package/dist/driver/spawn-support.d.ts.map +1 -0
- package/dist/driver/spawn-support.js +31 -0
- package/dist/driver/spawn-support.js.map +1 -0
- package/dist/driver/types.d.ts +19 -2
- package/dist/driver/types.d.ts.map +1 -1
- package/dist/fault/crashpoint.d.ts +8 -0
- package/dist/fault/crashpoint.d.ts.map +1 -0
- package/dist/fault/crashpoint.js +37 -0
- package/dist/fault/crashpoint.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +233 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +32 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +255 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/native/darwin-arm64/xioflow-reaper +0 -0
- package/dist/native/darwin-x64/xioflow-reaper +0 -0
- package/dist/native/linux-arm64/xioflow-reaper +0 -0
- package/dist/native/linux-x64/xioflow-reaper +0 -0
- package/dist/otel/otlp.d.ts +37 -0
- package/dist/otel/otlp.d.ts.map +1 -0
- package/dist/otel/otlp.js +224 -0
- package/dist/otel/otlp.js.map +1 -0
- package/dist/quick-run.d.ts.map +1 -1
- package/dist/quick-run.js +5 -12
- package/dist/quick-run.js.map +1 -1
- package/dist/recovery/engine.d.ts +7 -0
- package/dist/recovery/engine.d.ts.map +1 -1
- package/dist/recovery/engine.js +176 -4
- package/dist/recovery/engine.js.map +1 -1
- package/dist/snapshot/git-shadow.d.ts +57 -0
- package/dist/snapshot/git-shadow.d.ts.map +1 -0
- package/dist/snapshot/git-shadow.js +364 -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 +14 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +150 -58
- package/dist/store/sqlite.js.map +1 -1
- package/dist/supervisor/admission.d.ts +26 -0
- package/dist/supervisor/admission.d.ts.map +1 -0
- package/dist/supervisor/admission.js +98 -0
- package/dist/supervisor/admission.js.map +1 -0
- 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/finalize.d.ts +21 -0
- package/dist/supervisor/finalize.d.ts.map +1 -0
- package/dist/supervisor/finalize.js +66 -0
- package/dist/supervisor/finalize.js.map +1 -0
- package/dist/supervisor/fingerprint.d.ts +8 -0
- package/dist/supervisor/fingerprint.d.ts.map +1 -0
- package/dist/supervisor/fingerprint.js +54 -0
- package/dist/supervisor/fingerprint.js.map +1 -0
- package/dist/supervisor/process-monitor.d.ts +9 -0
- package/dist/supervisor/process-monitor.d.ts.map +1 -0
- package/dist/supervisor/process-monitor.js +230 -0
- package/dist/supervisor/process-monitor.js.map +1 -0
- package/dist/supervisor/process-output.d.ts +27 -0
- package/dist/supervisor/process-output.d.ts.map +1 -0
- package/dist/supervisor/process-output.js +102 -0
- package/dist/supervisor/process-output.js.map +1 -0
- package/dist/supervisor/process-run.d.ts +30 -0
- package/dist/supervisor/process-run.d.ts.map +1 -0
- package/dist/supervisor/process-run.js +191 -0
- package/dist/supervisor/process-run.js.map +1 -0
- package/dist/supervisor/replay.d.ts +14 -0
- package/dist/supervisor/replay.d.ts.map +1 -0
- package/dist/supervisor/replay.js +70 -0
- package/dist/supervisor/replay.js.map +1 -0
- package/dist/supervisor/rollback.d.ts +8 -0
- package/dist/supervisor/rollback.d.ts.map +1 -0
- package/dist/supervisor/rollback.js +153 -0
- package/dist/supervisor/rollback.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/snapshot-ops.d.ts +36 -0
- package/dist/supervisor/snapshot-ops.d.ts.map +1 -0
- package/dist/supervisor/snapshot-ops.js +243 -0
- package/dist/supervisor/snapshot-ops.js.map +1 -0
- package/dist/supervisor/supervisor.d.ts +62 -30
- package/dist/supervisor/supervisor.d.ts.map +1 -1
- package/dist/supervisor/supervisor.js +164 -842
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/supervisor/types.d.ts +64 -0
- package/dist/supervisor/types.d.ts.map +1 -0
- package/dist/supervisor/types.js +2 -0
- package/dist/supervisor/types.js.map +1 -0
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +619 -12
- package/dist/testing/contract-suite.js.map +1 -1
- package/dist/types.d.ts +161 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/dist/workspace/read-tracking.d.ts +23 -0
- package/dist/workspace/read-tracking.d.ts.map +1 -0
- package/dist/workspace/read-tracking.js +66 -0
- package/dist/workspace/read-tracking.js.map +1 -0
- package/dist/workspace/transactions.d.ts +89 -0
- package/dist/workspace/transactions.d.ts.map +1 -0
- package/dist/workspace/transactions.js +277 -0
- package/dist/workspace/transactions.js.map +1 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
7
|
[](./ARCHITECTURE.en.md)
|
|
8
8
|
[](./ARCHITECTURE.md)
|
|
@@ -11,7 +11,9 @@ A supervised execution kernel for AI agent runtimes. Zero runtime dependencies.
|
|
|
11
11
|
|
|
12
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.
|
|
13
13
|
|
|
14
|
-
[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.
|
|
15
17
|
|
|
16
18
|
## Guarantees
|
|
17
19
|
|
|
@@ -22,14 +24,17 @@ Agent runtimes usually call `spawn()` (or `exec()`) and hope for the best. When
|
|
|
22
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`. |
|
|
23
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. |
|
|
24
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. |
|
|
25
|
-
| 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. |
|
|
26
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. |
|
|
27
31
|
|
|
28
32
|
## Requirements
|
|
29
33
|
|
|
30
|
-
- Node.js >= 22.
|
|
31
|
-
- Linux and macOS.
|
|
32
|
-
-
|
|
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.
|
|
35
|
+
- Linux and macOS. The default driver enumerates descendants with `ps(1)` and terminates POSIX process groups; the optional native reaper holds the whole tree instead (see [Holding the process tree](#holding-the-process-tree)).
|
|
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.
|
|
33
38
|
|
|
34
39
|
## Install
|
|
35
40
|
|
|
@@ -123,6 +128,70 @@ To cancel, call `await supervisor.cancelOperation('op-1', graceMs)`. It returns
|
|
|
123
128
|
> **Note on In-Flight Join and Cancellation**:
|
|
124
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)`.
|
|
125
130
|
|
|
131
|
+
## Holding the process tree
|
|
132
|
+
|
|
133
|
+
`NodePlatformDriver` observes a process tree from the outside, so a descendant that calls `setsid()` and loses its parent can outlive a stop; the kernel then reports `indeterminate` instead of lying. `ReaperPlatformDriver` runs each operation under a small native helper (`xioflow-reaper`, shipped prebuilt in the package) that holds the tree:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { ExecutionDomain, ProcessSupervisor, ReaperPlatformDriver } from '@xioflow/kernel';
|
|
137
|
+
|
|
138
|
+
const supervisor = new ProcessSupervisor(domain, new ReaperPlatformDriver());
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| | Linux | macOS |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| How the tree is held | child subreaper: orphans are reparented to the helper | kqueue `NOTE_FORK`, session membership, µs start times |
|
|
144
|
+
| Stop signals | `pidfd_send_signal` after re-checking the start time | `kill` after re-checking the start time |
|
|
145
|
+
| `confirmed_stopped` means | `waitpid` returned `ECHILD` (`scope: 'subreaper_tree'`) | every tracked process is gone (`scope: 'tracked_tree'`) |
|
|
146
|
+
| Supervisor process dies | helper stops the whole tree | helper stops the whole tree |
|
|
147
|
+
|
|
148
|
+
`ReaperPlatformDriver.isAvailable()` tells whether this platform's helper is present; the constructor throws rather than falling back to another driver. Build one from source with `node scripts/build-native.mjs`, or point `XIOFLOW_REAPER_PATH` at a binary.
|
|
149
|
+
|
|
150
|
+
## Use it from any agent: MCP server
|
|
151
|
+
|
|
152
|
+
The package ships a `xioflow` command that serves the kernel over MCP (stdio), so an agent that speaks MCP can run commands under supervision without writing an integration:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"xioflow": { "command": "npx", "args": ["-y", "@xioflow/kernel", "mcp", "--domain", "/path/to/repo/.xioflow-kernel"] }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The server acquires the domain and runs crash recovery before it answers anything. It then offers these tools:
|
|
163
|
+
|
|
164
|
+
- `run_command`: supervised execution; reusing an `opId` replays the recorded result instead of running again.
|
|
165
|
+
- `operation_status` and `cancel_operation`.
|
|
166
|
+
- `snapshot_workspace` and `rollback_workspace`.
|
|
167
|
+
- `begin_transaction`, `commit_transaction` and `abort_transaction`.
|
|
168
|
+
|
|
169
|
+
Client features map onto kernel semantics:
|
|
170
|
+
|
|
171
|
+
- A request with a `progressToken` streams stdout/stderr as progress notifications.
|
|
172
|
+
- `notifications/cancelled` stops the operation through the stop pipeline.
|
|
173
|
+
- A result that is `indeterminate` is presented as "stop and ask a human".
|
|
174
|
+
|
|
175
|
+
Adjudication is intentionally not exposed to the model. `--driver auto` (the default) uses the native reaper when its helper is present and says which driver it chose in the server instructions and in every result.
|
|
176
|
+
|
|
177
|
+
## OpenTelemetry
|
|
178
|
+
|
|
179
|
+
The journal exports as OTLP/HTTP JSON traces, with no dependencies:
|
|
180
|
+
|
|
181
|
+
- One trace per Run.
|
|
182
|
+
- One span per settled operation, with status transitions, replays, capability use and adjudication as span events.
|
|
183
|
+
- One span per workspace transaction.
|
|
184
|
+
- `indeterminate` and `failed` are `ERROR`.
|
|
185
|
+
|
|
186
|
+
Ids are derived deterministically, so exporting the same journal twice produces the same spans.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
xioflow mcp --otlp-endpoint http://localhost:4318 # export every 5s while serving
|
|
190
|
+
xioflow otel-export --otlp-endpoint http://localhost:4318 # one-shot, read-only, works next to a running server
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
From code: `exportJournalToOtlp(domain, { endpoint, fromSeq })` returns the cursor for the next incremental export; `journalToOtlpTraces(...)` builds the payload without sending it.
|
|
194
|
+
|
|
126
195
|
## Crash recovery
|
|
127
196
|
|
|
128
197
|
After a restart, reacquire the domain and run the recovery engine:
|
|
@@ -139,17 +208,105 @@ For every unfinished operation, recovery verifies the recorded process identity
|
|
|
139
208
|
| Observed state | Action | Leases |
|
|
140
209
|
| --- | --- | --- |
|
|
141
210
|
| Intent persisted, never spawned | cleaned up as failed | released |
|
|
142
|
-
| Process confirmed dead
|
|
211
|
+
| Process confirmed dead, exit never observed | `failed` with `terminationReason: 'exit_unobserved'`: the outcome is unknown, so do not retry under a new `opId` | released |
|
|
143
212
|
| Process alive and identity confirmed | stopped through the stop pipeline | released |
|
|
144
213
|
| Identity cannot be determined | left `indeterminate` | retained, manual decision required |
|
|
145
214
|
|
|
146
215
|
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.
|
|
147
216
|
|
|
217
|
+
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.
|
|
218
|
+
|
|
219
|
+
## Long-running services
|
|
220
|
+
|
|
221
|
+
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:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
const service = await supervisor.startService({
|
|
225
|
+
serviceId: 'mcp-fs',
|
|
226
|
+
runId: 'run-1',
|
|
227
|
+
command: { execPath: 'node', args: ['server.mjs'], cwd: '/path/to/workspace' },
|
|
228
|
+
readiness: { stdoutLine: /listening/, timeoutMs: 5000 }, // or 'spawned'
|
|
229
|
+
restart: { policy: 'on-failure', maxRestarts: 3, backoffMs: 500 },
|
|
230
|
+
requiredResources: ['port:3000'],
|
|
231
|
+
});
|
|
232
|
+
await service.ready; // rejects if the readiness line never appears; the instance is then stopped
|
|
233
|
+
service.stdin.write('...'); // stdin stays open (stdinMode: 'stream'); stdout is passed through
|
|
234
|
+
await service.stop(2000);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
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.
|
|
238
|
+
|
|
239
|
+
## Snapshots, rollback and forks
|
|
240
|
+
|
|
241
|
+
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>`:
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
const snap = await supervisor.captureSnapshot({ runId: 'run-1', opId: 'snap-1', roots: ['/path/to/workspace/src'] });
|
|
245
|
+
|
|
246
|
+
// ... the agent edits files ...
|
|
247
|
+
|
|
248
|
+
const rb = await supervisor.rollback({ runId: 'run-1', opId: 'rb-1', snapshotId: snap.snapshot.id });
|
|
249
|
+
console.log(rb.status, rb.coverage, rb.outOfScopeEffects); // e.g. 'restored', 'declared_roots', 'possible'
|
|
250
|
+
|
|
251
|
+
// Fork the snapshot into an independent worktree for a parallel candidate:
|
|
252
|
+
await supervisor.materialize(snap.snapshot.id, '/tmp/candidate-a');
|
|
253
|
+
await supervisor.dematerialize('/tmp/candidate-a');
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
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'`.
|
|
257
|
+
|
|
258
|
+
To make `complete` reachable, bind operations to a capability and run them confined:
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
const cap = domain.issueCapability(
|
|
262
|
+
{ write: ['/path/to/workspace/src'], exclusive: ['workspace:write:/path/to/workspace/src'] },
|
|
263
|
+
'my-agent-policy',
|
|
264
|
+
10 * 60_000,
|
|
265
|
+
);
|
|
266
|
+
await supervisor.executeProcess({
|
|
267
|
+
runId: 'run-1',
|
|
268
|
+
opId: 'op-edit',
|
|
269
|
+
name: 'codegen',
|
|
270
|
+
command: { execPath: 'node', args: ['../codegen.mjs'], cwd: '/path/to/workspace/src' },
|
|
271
|
+
// mutation roots default to cwd; pass mutationRoots to declare others
|
|
272
|
+
capabilityId: cap.id, // out-of-scope resources or paths are rejected at admission
|
|
273
|
+
confinement: true, // sandbox-exec / bubblewrap / srt, whichever is available
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
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.
|
|
278
|
+
|
|
279
|
+
## Parallel agents: workspace transactions
|
|
280
|
+
|
|
281
|
+
Locks make parallel agents wait for each other. Workspace transactions let them work at the same time and check for conflicts when they commit, the way optimistic concurrency control works in a database:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
const tx = await supervisor.beginWorkspaceTransaction({
|
|
285
|
+
txId: 'agent-a-1',
|
|
286
|
+
runId: 'run-a',
|
|
287
|
+
root: '/path/to/repo',
|
|
288
|
+
forkPath: '/tmp/forks/agent-a-1',
|
|
289
|
+
});
|
|
290
|
+
// the agent works in its own fork
|
|
291
|
+
await supervisor.executeProcess({ runId: 'run-a', opId: 'a-edit', name: 'edit', command: { execPath: 'node', args: ['edit.mjs'], cwd: tx.forkRoot } });
|
|
292
|
+
|
|
293
|
+
const res = await supervisor.commitWorkspaceTransaction('agent-a-1');
|
|
294
|
+
if (res.status === 'conflict') {
|
|
295
|
+
// e.g. [{ path: 'config.json', kind: 'read_write', otherTxId: 'agent-b-7' }]
|
|
296
|
+
await supervisor.abortWorkspaceTransaction('agent-a-1');
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- **Write set**: the exact per-file diff between the base snapshot and the fork.
|
|
301
|
+
- **Read set**: observed without privileges through access times. Each entry in the fork gets an atime just before its mtime, so any later read (file contents or directory listings) moves the atime past the mtime on both Linux (`relatime`) and macOS (APFS only updates an atime that is older than the mtime). On a `noatime` filesystem the result says `readTracking: 'unobserved'` and `readSet: null`.
|
|
302
|
+
- **Validation**: a commit fails with `write_write` if a transaction committed since this one began wrote the same file, and with `read_write` if it changed something this one read. A listed directory only conflicts when entries were added to it or removed from it. A write that bypassed transactions and went straight to the workspace fails with `external_write`.
|
|
303
|
+
- **Apply**: only a validated transaction is applied to the workspace. `TX_COMMITTING` is journaled first, so a commit interrupted by a crash finishes when it is called again after restart.
|
|
304
|
+
|
|
148
305
|
## Design notes
|
|
149
306
|
|
|
150
307
|
### Verify your own runtime
|
|
151
308
|
|
|
152
|
-
The package ships the same
|
|
309
|
+
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:
|
|
153
310
|
|
|
154
311
|
```js
|
|
155
312
|
import { defineContractTestSuite } from '@xioflow/kernel/testing';
|
|
@@ -166,36 +323,38 @@ defineContractTestSuite('My Agent Runtime', async () => ({
|
|
|
166
323
|
|
|
167
324
|
`vitest` is an optional peer dependency, and the subpath is only loaded if you import it.
|
|
168
325
|
|
|
326
|
+
Beyond the contracts, the repository kills its own supervisor at every durable step (before and after each store commit, and while the process runs), recovers in a fresh process and checks the invariants again (`tests/fault/crash-matrix.test.ts`), and model-checks the same invariants for every interleaving of launch, stop, crash and recovery with TLA+ (`spec/tla/`, `pnpm check:tla`).
|
|
327
|
+
|
|
169
328
|
- Persistence is SQLite with WAL and `synchronous = FULL`, so committed facts survive power loss.
|
|
170
329
|
- One execution domain is one workspace-scoped store plus one active owner. Isolation is rebuilt from the store before any new operation is admitted.
|
|
171
330
|
- 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.
|
|
172
331
|
- 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".
|
|
173
332
|
- `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`.
|
|
174
|
-
- 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
|
|
175
|
-
- Protocol specification: [`ARCHITECTURE.md`](./ARCHITECTURE.md) (Chinese).
|
|
333
|
+
- 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.
|
|
334
|
+
- 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).
|
|
176
335
|
|
|
177
336
|
## Status
|
|
178
337
|
|
|
179
|
-
0.
|
|
338
|
+
0.5.0 on npm (native tree-holding reaper, workspace transactions for parallel agents, MCP server, OpenTelemetry export, crash-point matrix and TLA+ model; 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).
|
|
180
339
|
|
|
181
340
|
## Releasing
|
|
182
341
|
|
|
183
342
|
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:
|
|
184
343
|
|
|
185
344
|
1. Bump `version` in `package.json`, commit, and push to `main`.
|
|
186
|
-
2. Push the matching tag, e.g. `git tag v0.
|
|
345
|
+
2. Push the matching tag, e.g. `git tag v0.3.0 && git push origin v0.3.0`.
|
|
187
346
|
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.
|
|
188
347
|
|
|
189
348
|
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.
|
|
190
349
|
|
|
191
|
-
To attach assets to an existing tag without publishing to npm, run the workflow manually: `gh workflow run release.yml -f tag=v0.
|
|
350
|
+
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.
|
|
192
351
|
|
|
193
352
|
### Verifying a release
|
|
194
353
|
|
|
195
354
|
```bash
|
|
196
|
-
gh release download v0.
|
|
355
|
+
gh release download v0.3.0 --repo Xio-Shark/xioflow
|
|
197
356
|
shasum -a 256 -c SHA256SUMS
|
|
198
|
-
gh attestation verify xioflow-kernel-0.
|
|
357
|
+
gh attestation verify xioflow-kernel-0.3.0.tgz --repo Xio-Shark/xioflow
|
|
199
358
|
npm audit signatures # inside a project that installed @xioflow/kernel from npm
|
|
200
359
|
```
|
|
201
360
|
|
|
@@ -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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|