browsermesh-kernel 0.1.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/LICENSE +21 -0
- package/README.md +59 -0
- package/package.json +25 -0
- package/src/byte-stream.mjs +258 -0
- package/src/caps.mjs +102 -0
- package/src/chaos.mjs +151 -0
- package/src/clock.mjs +80 -0
- package/src/constants.mjs +85 -0
- package/src/env.mjs +56 -0
- package/src/errors.mjs +148 -0
- package/src/index.d.ts +826 -0
- package/src/index.mjs +73 -0
- package/src/kernel.mjs +155 -0
- package/src/logger.mjs +182 -0
- package/src/message-port.mjs +93 -0
- package/src/resource-table.mjs +164 -0
- package/src/rng.mjs +76 -0
- package/src/service-registry.mjs +167 -0
- package/src/signal.mjs +122 -0
- package/src/stdio.mjs +62 -0
- package/src/tracer.mjs +117 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clawser Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# browsermesh-kernel
|
|
2
|
+
|
|
3
|
+
Capability-secure browser microkernel: resource handles, ByteStreams, IPC,
|
|
4
|
+
service mesh, structured tracing, chaos engineering, and tenant isolation —
|
|
5
|
+
zero npm dependencies, pure ES modules.
|
|
6
|
+
|
|
7
|
+
## Modules
|
|
8
|
+
|
|
9
|
+
| Module | Key Exports |
|
|
10
|
+
|--------|-------------|
|
|
11
|
+
| constants / errors | `KERNEL_DEFAULTS`, `KERNEL_CAP`, `KERNEL_ERROR`, `KernelError` + 7 subclasses |
|
|
12
|
+
| resource-table | `ResourceTable` — handle-based `res_N` resource allocation |
|
|
13
|
+
| byte-stream | `BYTE_STREAM`, `isByteStream`, `asByteStream`, `createPipe`, `pipe`, `devNull`, `compose` |
|
|
14
|
+
| clock / rng | `Clock` (fixed for testing), `RNG` (seeded xorshift128+) |
|
|
15
|
+
| caps | `buildCaps`, `requireCap`, `CapsBuilder` — capability enforcement |
|
|
16
|
+
| message-port | `KernelMessagePort`, `createChannel` — IPC |
|
|
17
|
+
| service-registry | `ServiceRegistry` — `svc://` service lookup with `onLookupMiss` |
|
|
18
|
+
| tracer | `Tracer` — ring-buffer, `AsyncIterable` trace event stream |
|
|
19
|
+
| logger | `Logger`, `LOG_LEVEL` |
|
|
20
|
+
| chaos | `ChaosEngine` — fault injection |
|
|
21
|
+
| env | `Environment` — immutable env vars |
|
|
22
|
+
| signal / stdio | `SIGNAL`, `SignalController` (TERM/INT/HUP + `AbortSignal`), `Stdio` |
|
|
23
|
+
| kernel | `Kernel` — the facade tying every subsystem together |
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install browsermesh-kernel
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { Kernel, KERNEL_CAP } from 'browsermesh-kernel'
|
|
35
|
+
|
|
36
|
+
const kernel = new Kernel()
|
|
37
|
+
|
|
38
|
+
// Create a tenant with scoped capabilities
|
|
39
|
+
const tenant = kernel.createTenant({
|
|
40
|
+
capabilities: [KERNEL_CAP.CLOCK, KERNEL_CAP.IPC, KERNEL_CAP.STDIO],
|
|
41
|
+
env: { MODE: 'sandbox' },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Use kernel subsystems
|
|
45
|
+
const handle = kernel.resources.allocate('stream', myStream, tenant.id)
|
|
46
|
+
kernel.tracer.emit({ type: 'custom', tenant: tenant.id })
|
|
47
|
+
|
|
48
|
+
// Clean up
|
|
49
|
+
kernel.destroyTenant(tenant.id)
|
|
50
|
+
kernel.close()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Origin
|
|
54
|
+
|
|
55
|
+
Extracted from the [clawser](https://github.com/johnhenry/clawser) browser
|
|
56
|
+
agent workspace, where it underpins workspace tenants, shell pipes, MCP
|
|
57
|
+
service registration, provider cost tracing, sandboxed code execution, and
|
|
58
|
+
daemon IPC — all as opt-in hooks (`clawser-kernel-integration.js`) that are
|
|
59
|
+
no-ops when the kernel isn't active.
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browsermesh-kernel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Capability-secure browser microkernel with resource handles, IPC, tracing, and chaos engineering",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"microkernel",
|
|
17
|
+
"capabilities",
|
|
18
|
+
"ipc",
|
|
19
|
+
"tracing",
|
|
20
|
+
"chaos",
|
|
21
|
+
"browser"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": { "type": "git", "url": "https://github.com/johnhenry/clawser" }
|
|
25
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ByteStream — duck-typed stream protocol with utilities.
|
|
3
|
+
*
|
|
4
|
+
* Provides a protocol symbol, type checking, pipe utilities, and an in-memory
|
|
5
|
+
* pipe factory built on an internal AsyncBuffer (matching netway's pattern).
|
|
6
|
+
* EOF propagation on error ensures paired endpoints close cleanly.
|
|
7
|
+
*
|
|
8
|
+
* @module byte-stream
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { StreamClosedError } from './errors.mjs';
|
|
12
|
+
import { KERNEL_DEFAULTS } from './constants.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Symbol used to tag objects as ByteStream-compliant.
|
|
16
|
+
*/
|
|
17
|
+
export const BYTE_STREAM = Symbol.for('kernel.ByteStream');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check whether an object conforms to the ByteStream protocol.
|
|
21
|
+
* A ByteStream must have `read`, `write`, and `close` methods.
|
|
22
|
+
*
|
|
23
|
+
* @param {*} obj - The object to check.
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
export function isByteStream(obj) {
|
|
27
|
+
return obj != null &&
|
|
28
|
+
typeof obj.read === 'function' &&
|
|
29
|
+
typeof obj.write === 'function' &&
|
|
30
|
+
typeof obj.close === 'function';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Tag an object with the ByteStream symbol. Idempotent — already tagged
|
|
35
|
+
* objects are returned as-is.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} obj - The object to tag (must have read/write/close).
|
|
38
|
+
* @returns {Object} The tagged object.
|
|
39
|
+
*/
|
|
40
|
+
export function asByteStream(obj) {
|
|
41
|
+
if (obj[BYTE_STREAM]) return obj;
|
|
42
|
+
obj[BYTE_STREAM] = true;
|
|
43
|
+
return obj;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Simple asynchronous FIFO buffer with blocking pull semantics.
|
|
48
|
+
* Matches netway's AsyncBuffer pattern. EOF propagation on error.
|
|
49
|
+
*
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
class AsyncBuffer {
|
|
53
|
+
#queue = [];
|
|
54
|
+
#waiters = [];
|
|
55
|
+
#writeClosed = false;
|
|
56
|
+
#readClosed = false;
|
|
57
|
+
#paused = false;
|
|
58
|
+
#highWaterMark;
|
|
59
|
+
|
|
60
|
+
constructor({ highWaterMark = 0 } = {}) {
|
|
61
|
+
this.#highWaterMark = highWaterMark;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
push(data) {
|
|
65
|
+
if (this.#writeClosed || this.#readClosed) return false;
|
|
66
|
+
if (this.#waiters.length > 0) {
|
|
67
|
+
this.#waiters.shift()(data);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
this.#queue.push(data);
|
|
71
|
+
if (this.#highWaterMark > 0 && this.#queue.length >= this.#highWaterMark) {
|
|
72
|
+
this.#paused = true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pull() {
|
|
79
|
+
if (this.#queue.length > 0) {
|
|
80
|
+
const item = this.#queue.shift();
|
|
81
|
+
// Resume writes when queue drains below high-water mark
|
|
82
|
+
if (this.#paused && this.#queue.length < this.#highWaterMark) {
|
|
83
|
+
this.#paused = false;
|
|
84
|
+
}
|
|
85
|
+
return Promise.resolve(item);
|
|
86
|
+
}
|
|
87
|
+
if (this.#writeClosed || this.#readClosed) return Promise.resolve(null);
|
|
88
|
+
return new Promise(resolve => this.#waiters.push(resolve));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @returns {boolean} Whether the buffer is accepting writes */
|
|
92
|
+
get writable() { return !this.#writeClosed && !this.#readClosed && !this.#paused; }
|
|
93
|
+
|
|
94
|
+
/** Signal no more writes — existing buffered data can still be drained. */
|
|
95
|
+
closeWrite() {
|
|
96
|
+
this.#writeClosed = true;
|
|
97
|
+
this.#paused = false;
|
|
98
|
+
// Resolve waiters so blocked reads get null after queue is drained
|
|
99
|
+
for (const w of this.#waiters) w(null);
|
|
100
|
+
this.#waiters.length = 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Hard close — discard everything. */
|
|
104
|
+
closeRead() {
|
|
105
|
+
this.#readClosed = true;
|
|
106
|
+
this.#writeClosed = true;
|
|
107
|
+
this.#paused = false;
|
|
108
|
+
for (const w of this.#waiters) w(null);
|
|
109
|
+
this.#waiters.length = 0;
|
|
110
|
+
this.#queue.length = 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get isClosed() { return this.#writeClosed && this.#readClosed; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create an in-memory pipe returning `[reader, writer]` ByteStreams.
|
|
118
|
+
* Data written to `writer` can be read from `reader`. Closing either
|
|
119
|
+
* endpoint propagates EOF to the other.
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} [opts={}]
|
|
122
|
+
* @param {number} [opts.highWaterMark=1024] - Maximum queue depth.
|
|
123
|
+
* @returns {[Object, Object]} `[reader, writer]` ByteStream pair.
|
|
124
|
+
*/
|
|
125
|
+
export function createPipe({ highWaterMark = KERNEL_DEFAULTS.DEFAULT_STREAM_BUFFER_SIZE } = {}) {
|
|
126
|
+
const buffer = new AsyncBuffer({ highWaterMark });
|
|
127
|
+
let readerClosed = false;
|
|
128
|
+
let writerClosed = false;
|
|
129
|
+
|
|
130
|
+
const reader = {
|
|
131
|
+
[BYTE_STREAM]: true,
|
|
132
|
+
async read() {
|
|
133
|
+
if (readerClosed) return null;
|
|
134
|
+
return buffer.pull();
|
|
135
|
+
},
|
|
136
|
+
async write() {
|
|
137
|
+
throw new StreamClosedError();
|
|
138
|
+
},
|
|
139
|
+
async close() {
|
|
140
|
+
if (readerClosed) return;
|
|
141
|
+
readerClosed = true;
|
|
142
|
+
buffer.closeRead();
|
|
143
|
+
},
|
|
144
|
+
get closed() { return readerClosed; },
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const writer = {
|
|
148
|
+
[BYTE_STREAM]: true,
|
|
149
|
+
async read() {
|
|
150
|
+
throw new StreamClosedError();
|
|
151
|
+
},
|
|
152
|
+
async write(data) {
|
|
153
|
+
if (writerClosed) throw new StreamClosedError();
|
|
154
|
+
if (!buffer.push(data)) {
|
|
155
|
+
writerClosed = true;
|
|
156
|
+
throw new StreamClosedError();
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
async close() {
|
|
160
|
+
if (writerClosed) return;
|
|
161
|
+
writerClosed = true;
|
|
162
|
+
// Signal EOF — existing data can still be drained by reader
|
|
163
|
+
buffer.closeWrite();
|
|
164
|
+
},
|
|
165
|
+
get closed() { return writerClosed; },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return [reader, writer];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Pipe all data from a source ByteStream to a destination ByteStream.
|
|
173
|
+
* Reads from src until null (EOF), writes each chunk to dst.
|
|
174
|
+
* On transport error, closes both endpoints for clean EOF propagation.
|
|
175
|
+
*
|
|
176
|
+
* @param {Object} src - Source ByteStream.
|
|
177
|
+
* @param {Object} dst - Destination ByteStream.
|
|
178
|
+
* @returns {Promise<void>}
|
|
179
|
+
*/
|
|
180
|
+
export async function pipe(src, dst) {
|
|
181
|
+
try {
|
|
182
|
+
let chunk;
|
|
183
|
+
while ((chunk = await src.read()) !== null) {
|
|
184
|
+
await dst.write(chunk);
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
// EOF propagation on error — close both endpoints
|
|
188
|
+
await src.close().catch(() => {});
|
|
189
|
+
await dst.close().catch(() => {});
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create a sink ByteStream that discards all writes and returns null on read.
|
|
196
|
+
*
|
|
197
|
+
* @returns {Object} A ByteStream that acts as `/dev/null`.
|
|
198
|
+
*/
|
|
199
|
+
export function devNull() {
|
|
200
|
+
return {
|
|
201
|
+
[BYTE_STREAM]: true,
|
|
202
|
+
async read() { return null; },
|
|
203
|
+
async write() {},
|
|
204
|
+
async close() {},
|
|
205
|
+
get closed() { return false; },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Transform protocol. A transform has a single method:
|
|
211
|
+
* `transform(chunk: Uint8Array) → Uint8Array | Promise<Uint8Array>`
|
|
212
|
+
*
|
|
213
|
+
* Transforms can be composed onto any ByteStream to create processing
|
|
214
|
+
* pipelines (compression, encryption, tracing, rate-limiting, etc.).
|
|
215
|
+
*/
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Compose one or more transforms onto a ByteStream, returning a new
|
|
219
|
+
* ByteStream that applies the transforms in order on read and in
|
|
220
|
+
* reverse order on write.
|
|
221
|
+
*
|
|
222
|
+
* @param {Object} stream - The underlying ByteStream.
|
|
223
|
+
* @param {...{ transform: function(Uint8Array): Uint8Array|Promise<Uint8Array> }} transforms - Transform objects.
|
|
224
|
+
* @returns {Object} A new ByteStream with transforms applied.
|
|
225
|
+
*/
|
|
226
|
+
export function compose(stream, ...transforms) {
|
|
227
|
+
if (transforms.length === 0) return stream;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
[BYTE_STREAM]: true,
|
|
231
|
+
|
|
232
|
+
async read() {
|
|
233
|
+
const chunk = await stream.read();
|
|
234
|
+
if (chunk === null) return null;
|
|
235
|
+
let result = chunk;
|
|
236
|
+
for (const t of transforms) {
|
|
237
|
+
result = await t.transform(result);
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async write(data) {
|
|
243
|
+
let result = data;
|
|
244
|
+
// Apply inverse transforms in reverse for writes (fall back to transform if no untransform)
|
|
245
|
+
for (let i = transforms.length - 1; i >= 0; i--) {
|
|
246
|
+
const t = transforms[i];
|
|
247
|
+
result = await (t.untransform ? t.untransform(result) : t.transform(result));
|
|
248
|
+
}
|
|
249
|
+
await stream.write(result);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
async close() {
|
|
253
|
+
await stream.close();
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
get closed() { return stream.closed; },
|
|
257
|
+
};
|
|
258
|
+
}
|
package/src/caps.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caps — capability builder and enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Builds a frozen capabilities object from a kernel instance and a set
|
|
5
|
+
* of granted capability tags. The `requireCap` function enforces access
|
|
6
|
+
* control by throwing CapabilityDeniedError for missing capabilities.
|
|
7
|
+
*
|
|
8
|
+
* @module caps
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { KERNEL_CAP } from './constants.mjs';
|
|
12
|
+
import { CapabilityDeniedError } from './errors.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build a frozen capabilities object from granted capability tags.
|
|
16
|
+
* Each granted tag maps to the corresponding kernel subsystem reference.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} kernel - Kernel instance with subsystem accessors.
|
|
19
|
+
* @param {string[]} grantedCaps - Array of KERNEL_CAP tags to grant.
|
|
20
|
+
* @returns {Readonly<Object>} Frozen capabilities object.
|
|
21
|
+
*/
|
|
22
|
+
export function buildCaps(kernel, grantedCaps) {
|
|
23
|
+
const caps = {};
|
|
24
|
+
const granted = new Set(grantedCaps);
|
|
25
|
+
const hasAll = granted.has(KERNEL_CAP.ALL);
|
|
26
|
+
|
|
27
|
+
if (hasAll || granted.has(KERNEL_CAP.CLOCK)) {
|
|
28
|
+
caps.clock = kernel.clock;
|
|
29
|
+
}
|
|
30
|
+
if (hasAll || granted.has(KERNEL_CAP.RNG)) {
|
|
31
|
+
caps.rng = kernel.rng;
|
|
32
|
+
}
|
|
33
|
+
if (hasAll || granted.has(KERNEL_CAP.NET)) {
|
|
34
|
+
caps.net = true; // Network access marker — actual net object provided by netway
|
|
35
|
+
}
|
|
36
|
+
if (hasAll || granted.has(KERNEL_CAP.FS)) {
|
|
37
|
+
caps.fs = true; // FS access marker
|
|
38
|
+
}
|
|
39
|
+
if (hasAll || granted.has(KERNEL_CAP.IPC)) {
|
|
40
|
+
caps.ipc = kernel.services;
|
|
41
|
+
}
|
|
42
|
+
if (hasAll || granted.has(KERNEL_CAP.STDIO)) {
|
|
43
|
+
caps.stdio = true; // Stdio access marker — actual stdio is per-tenant
|
|
44
|
+
}
|
|
45
|
+
if (hasAll || granted.has(KERNEL_CAP.TRACE)) {
|
|
46
|
+
caps.trace = kernel.tracer;
|
|
47
|
+
}
|
|
48
|
+
if (hasAll || granted.has(KERNEL_CAP.CHAOS)) {
|
|
49
|
+
caps.chaos = kernel.chaos;
|
|
50
|
+
}
|
|
51
|
+
if (hasAll || granted.has(KERNEL_CAP.ENV)) {
|
|
52
|
+
caps.env = true; // Env access marker — actual env is per-tenant
|
|
53
|
+
}
|
|
54
|
+
if (hasAll || granted.has(KERNEL_CAP.SIGNAL)) {
|
|
55
|
+
caps.signal = true; // Signal access marker
|
|
56
|
+
}
|
|
57
|
+
if (hasAll || granted.has(KERNEL_CAP.MESH)) {
|
|
58
|
+
caps.mesh = true; // Mesh networking access marker
|
|
59
|
+
}
|
|
60
|
+
if (hasAll || granted.has(KERNEL_CAP.PAYMENT)) {
|
|
61
|
+
caps.payment = true; // Payment channel access marker
|
|
62
|
+
}
|
|
63
|
+
if (hasAll || granted.has(KERNEL_CAP.CONSENSUS)) {
|
|
64
|
+
caps.consensus = true; // Consensus protocol access marker
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Store the granted set for requireCap checks
|
|
68
|
+
caps._granted = Object.freeze([...granted]);
|
|
69
|
+
|
|
70
|
+
return Object.freeze(caps);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Require that a capability tag is present in a caps object.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} caps - Capabilities object from buildCaps.
|
|
77
|
+
* @param {string} capTag - The required KERNEL_CAP tag.
|
|
78
|
+
* @throws {CapabilityDeniedError} If the capability is not granted.
|
|
79
|
+
*/
|
|
80
|
+
export function requireCap(caps, capTag) {
|
|
81
|
+
if (!caps || !caps._granted) throw new CapabilityDeniedError(capTag);
|
|
82
|
+
const granted = new Set(caps._granted);
|
|
83
|
+
if (granted.has(KERNEL_CAP.ALL)) return;
|
|
84
|
+
if (granted.has(capTag)) return;
|
|
85
|
+
throw new CapabilityDeniedError(capTag);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builder class for constructing capabilities (alternative to buildCaps).
|
|
90
|
+
*/
|
|
91
|
+
export class CapsBuilder {
|
|
92
|
+
/**
|
|
93
|
+
* Build capabilities from kernel and granted tags.
|
|
94
|
+
*
|
|
95
|
+
* @param {Object} kernel - Kernel instance.
|
|
96
|
+
* @param {string[]} grantedCaps - Granted capability tags.
|
|
97
|
+
* @returns {Readonly<Object>} Frozen capabilities object.
|
|
98
|
+
*/
|
|
99
|
+
build(kernel, grantedCaps) {
|
|
100
|
+
return buildCaps(kernel, grantedCaps);
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/chaos.mjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChaosEngine — fault injection for testing and chaos engineering.
|
|
3
|
+
*
|
|
4
|
+
* Injects configurable latency, packet drops, disconnects, and network
|
|
5
|
+
* partitions. Supports global defaults and per-scope overrides. Uses RNG
|
|
6
|
+
* for deterministic fault patterns in replay mode.
|
|
7
|
+
*
|
|
8
|
+
* @module chaos
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fault injection engine with global and per-scope configuration.
|
|
13
|
+
*/
|
|
14
|
+
export class ChaosEngine {
|
|
15
|
+
#enabled = false;
|
|
16
|
+
#globalConfig = { latencyMs: 0, dropRate: 0, disconnectRate: 0, partitionTargets: [] };
|
|
17
|
+
#scopeConfigs = new Map();
|
|
18
|
+
#rng;
|
|
19
|
+
#clock;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} [opts={}]
|
|
23
|
+
* @param {Object} [opts.rng] - RNG instance for deterministic fault patterns.
|
|
24
|
+
* @param {Object} [opts.clock] - Clock instance for delay implementation.
|
|
25
|
+
*/
|
|
26
|
+
constructor({ rng, clock } = {}) {
|
|
27
|
+
this.#rng = rng || null;
|
|
28
|
+
this.#clock = clock || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Enable the chaos engine. When disabled, all injection points are no-ops.
|
|
33
|
+
*/
|
|
34
|
+
enable() {
|
|
35
|
+
this.#enabled = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Disable the chaos engine.
|
|
40
|
+
*/
|
|
41
|
+
disable() {
|
|
42
|
+
this.#enabled = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Whether the engine is enabled. */
|
|
46
|
+
get enabled() { return this.#enabled; }
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Configure global fault injection defaults.
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} config
|
|
52
|
+
* @param {number} [config.latencyMs=0] - Added latency in ms.
|
|
53
|
+
* @param {number} [config.dropRate=0] - Drop probability (0-1).
|
|
54
|
+
* @param {number} [config.disconnectRate=0] - Disconnect probability (0-1).
|
|
55
|
+
* @param {string[]} [config.partitionTargets=[]] - Addresses to partition.
|
|
56
|
+
*/
|
|
57
|
+
configure(config) {
|
|
58
|
+
this.#globalConfig = { ...this.#globalConfig, ...config };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Configure fault injection for a specific scope (overrides global).
|
|
63
|
+
*
|
|
64
|
+
* @param {string} scopeId - Scope identifier.
|
|
65
|
+
* @param {Object} config - Scope-specific configuration.
|
|
66
|
+
*/
|
|
67
|
+
configureScope(scopeId, config) {
|
|
68
|
+
this.#scopeConfigs.set(scopeId, { ...config });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Remove scope-specific configuration, falling back to global defaults.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} scopeId - Scope identifier.
|
|
75
|
+
*/
|
|
76
|
+
removeScopeConfig(scopeId) {
|
|
77
|
+
this.#scopeConfigs.delete(scopeId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Maybe inject latency delay.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} [scopeId] - Optional scope for override lookup.
|
|
84
|
+
* @returns {Promise<void>}
|
|
85
|
+
*/
|
|
86
|
+
async maybeDelay(scopeId) {
|
|
87
|
+
if (!this.#enabled) return;
|
|
88
|
+
const config = this.#getConfig(scopeId);
|
|
89
|
+
if (config.latencyMs <= 0) return;
|
|
90
|
+
if (this.#clock) {
|
|
91
|
+
await this.#clock.sleep(config.latencyMs);
|
|
92
|
+
} else {
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, config.latencyMs));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check whether a packet/message should be dropped.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} [scopeId] - Optional scope for override lookup.
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
shouldDrop(scopeId) {
|
|
104
|
+
if (!this.#enabled) return false;
|
|
105
|
+
const config = this.#getConfig(scopeId);
|
|
106
|
+
if (config.dropRate <= 0) return false;
|
|
107
|
+
return this.#random() < config.dropRate;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check whether a connection should be forcibly disconnected.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} [scopeId] - Optional scope for override lookup.
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
shouldDisconnect(scopeId) {
|
|
117
|
+
if (!this.#enabled) return false;
|
|
118
|
+
const config = this.#getConfig(scopeId);
|
|
119
|
+
if (config.disconnectRate <= 0) return false;
|
|
120
|
+
return this.#random() < config.disconnectRate;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check whether an address is partitioned (unreachable).
|
|
125
|
+
*
|
|
126
|
+
* @param {string} addr - Target address.
|
|
127
|
+
* @param {string} [scopeId] - Optional scope for override lookup.
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
isPartitioned(addr, scopeId) {
|
|
131
|
+
if (!this.#enabled) return false;
|
|
132
|
+
const config = this.#getConfig(scopeId);
|
|
133
|
+
return (config.partitionTargets || []).includes(addr);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#getConfig(scopeId) {
|
|
137
|
+
if (scopeId && this.#scopeConfigs.has(scopeId)) {
|
|
138
|
+
return this.#scopeConfigs.get(scopeId);
|
|
139
|
+
}
|
|
140
|
+
return this.#globalConfig;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#random() {
|
|
144
|
+
if (this.#rng) {
|
|
145
|
+
const bytes = this.#rng.get(4);
|
|
146
|
+
const val = (bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24)) >>> 0;
|
|
147
|
+
return val / 0x100000000;
|
|
148
|
+
}
|
|
149
|
+
return Math.random();
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/clock.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock — monotonic and wall-clock time abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Provides both real and deterministic (fixed) clocks. The fixed clock
|
|
5
|
+
* is useful for testing — scheduled job logic becomes unit-testable
|
|
6
|
+
* without real timers.
|
|
7
|
+
*
|
|
8
|
+
* @module clock
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Clock providing monotonic and wall-clock time plus async sleep.
|
|
13
|
+
*/
|
|
14
|
+
export class Clock {
|
|
15
|
+
#monoFn;
|
|
16
|
+
#wallFn;
|
|
17
|
+
#sleepFn;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} [opts={}]
|
|
21
|
+
* @param {function(): number} [opts.monoFn] - Monotonic time source (ms).
|
|
22
|
+
* @param {function(): number} [opts.wallFn] - Wall-clock time source (ms since epoch).
|
|
23
|
+
* @param {function(number): Promise<void>} [opts.sleepFn] - Async sleep implementation.
|
|
24
|
+
*/
|
|
25
|
+
constructor({ monoFn, wallFn, sleepFn } = {}) {
|
|
26
|
+
this.#monoFn = monoFn || (() => performance.now());
|
|
27
|
+
this.#wallFn = wallFn || (() => Date.now());
|
|
28
|
+
this.#sleepFn = sleepFn || (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the current monotonic time in milliseconds.
|
|
33
|
+
* Monotonic time only moves forward and is not affected by clock adjustments.
|
|
34
|
+
*
|
|
35
|
+
* @returns {number} Monotonic timestamp in milliseconds.
|
|
36
|
+
*/
|
|
37
|
+
nowMonotonic() {
|
|
38
|
+
return this.#monoFn();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the current wall-clock time in milliseconds since the Unix epoch.
|
|
43
|
+
*
|
|
44
|
+
* @returns {number} Wall-clock timestamp (ms since epoch).
|
|
45
|
+
*/
|
|
46
|
+
nowWall() {
|
|
47
|
+
return this.#wallFn();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sleep for the given number of milliseconds.
|
|
52
|
+
*
|
|
53
|
+
* @param {number} ms - Duration to sleep.
|
|
54
|
+
* @returns {Promise<void>}
|
|
55
|
+
*/
|
|
56
|
+
async sleep(ms) {
|
|
57
|
+
return this.#sleepFn(ms);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a fixed (deterministic) clock for testing.
|
|
62
|
+
* The clock always returns the same values unless manually advanced.
|
|
63
|
+
*
|
|
64
|
+
* @param {number} mono - Fixed monotonic time value.
|
|
65
|
+
* @param {number} wall - Fixed wall-clock time value.
|
|
66
|
+
* @returns {Clock}
|
|
67
|
+
*/
|
|
68
|
+
static fixed(mono, wall) {
|
|
69
|
+
let currentMono = mono;
|
|
70
|
+
let currentWall = wall;
|
|
71
|
+
return new Clock({
|
|
72
|
+
monoFn: () => currentMono,
|
|
73
|
+
wallFn: () => currentWall,
|
|
74
|
+
sleepFn: async (ms) => {
|
|
75
|
+
currentMono += ms;
|
|
76
|
+
currentWall += ms;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|