@fungi.computer/mycelium 0.0.2 → 0.2.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 +70 -7
- package/dist/access.d.ts +2 -7
- package/dist/access.js +0 -3
- package/dist/execute-tool.d.ts +3 -2
- package/dist/execute-tool.js +40 -11
- package/dist/image-collector.d.ts +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +2 -1
- package/dist/internal/build-identity.node.js +1 -1
- package/dist/internal/execution-lifecycle.d.ts +4 -2
- package/dist/internal/execution-lifecycle.js +97 -54
- package/dist/internal/host-cores.d.ts +2 -0
- package/dist/internal/host-cores.js +4 -0
- package/dist/internal/host-cores.node.d.ts +2 -0
- package/dist/internal/host-cores.node.js +5 -0
- package/dist/mcp-catalog.d.ts +0 -6
- package/dist/mcp-catalog.js +0 -1
- package/dist/mcp.d.ts +0 -1
- package/dist/mcp.js +0 -1
- package/dist/output-collector.d.ts +46 -0
- package/dist/output-collector.js +145 -0
- package/dist/runtime.js +1 -1
- package/dist/sandbox-admission.d.ts +19 -0
- package/dist/sandbox-admission.js +21 -0
- package/dist/sandbox-boundary.js +32 -23
- package/dist/sandbox-failure-event.d.ts +1 -1
- package/dist/sandbox-failure-event.js +1 -0
- package/dist/sandbox-failure.d.ts +12 -1
- package/dist/sandbox-failure.js +14 -0
- package/dist/sandbox-guest.d.ts +6 -6
- package/dist/sandbox-guest.js +5 -7
- package/dist/sandbox-wire.d.ts +5 -0
- package/dist/sandbox-wire.js +20 -1
- package/dist/source-runtime.js +3 -3
- package/docs/api/index.md +68 -5
- package/docs/api/mcp.md +0 -2
- package/docs/api/sandbox-failure.md +90 -0
- package/docs/api/sandbox-guest.md +36 -30
- package/docs/api/sandbox-wire.md +3 -3
- package/package.json +9 -5
package/dist/mcp-catalog.d.ts
CHANGED
|
@@ -6,12 +6,6 @@ export type McpCatalogSnapshot = Readonly<{
|
|
|
6
6
|
sources: readonly CapabilitySource[];
|
|
7
7
|
/** Every permitted module is named; an omitted module is refused. */
|
|
8
8
|
grants: Grants;
|
|
9
|
-
/**
|
|
10
|
-
* Granted modules whose writes a person already approved for this principal,
|
|
11
|
-
* such as an owner's stored approval of a key's App tools. Their writes run
|
|
12
|
-
* without a per-call elicitation; every other write still asks.
|
|
13
|
-
*/
|
|
14
|
-
approvedWrites?: readonly string[];
|
|
15
9
|
}>;
|
|
16
10
|
/**
|
|
17
11
|
* Adapt principal-scoped capability sources and a sandbox to the shared MCP server.
|
package/dist/mcp-catalog.js
CHANGED
package/dist/mcp.d.ts
CHANGED
|
@@ -28,7 +28,6 @@ export type McpExecutionContext = Readonly<{
|
|
|
28
28
|
}>) => Promise<void>;
|
|
29
29
|
/** Apply to Mycelium access options for this call only; never persist callbacks. */
|
|
30
30
|
access: Pick<AccessOptions, "confirm"> & Readonly<{
|
|
31
|
-
confirmWrites: true;
|
|
32
31
|
/** Recheck the open connection and refresh authority before each operation. */
|
|
33
32
|
beforeCall: (signal: AbortSignal) => Promise<void>;
|
|
34
33
|
}>;
|
package/dist/mcp.js
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Stream from "effect/Stream";
|
|
3
|
+
/** Complete decoded text, in completion order across stdout/stderr, not byte order. */
|
|
4
|
+
export declare class OutputCollector {
|
|
5
|
+
#private;
|
|
6
|
+
private readonly wake;
|
|
7
|
+
private constructor();
|
|
8
|
+
static make(): Effect.Effect<OutputCollector, never, never>;
|
|
9
|
+
/** Never waits for a reader. An incomplete scalar stays with its producer. */
|
|
10
|
+
write(stream: "stdout" | "stderr", bytes: Uint8Array): void;
|
|
11
|
+
snapshot(): {
|
|
12
|
+
tail: string;
|
|
13
|
+
droppedBytes: number;
|
|
14
|
+
totalBytes: number;
|
|
15
|
+
};
|
|
16
|
+
/** EOF flush order is stdout then stderr; the final update precedes terminal. */
|
|
17
|
+
finish(): {
|
|
18
|
+
type: "update";
|
|
19
|
+
result: {
|
|
20
|
+
isError: false;
|
|
21
|
+
content: {
|
|
22
|
+
type: "text";
|
|
23
|
+
text: string;
|
|
24
|
+
}[];
|
|
25
|
+
data: {
|
|
26
|
+
droppedBytes: number;
|
|
27
|
+
totalBytes: number;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
} | undefined;
|
|
31
|
+
get updates(): Stream.Stream<{
|
|
32
|
+
type: "update";
|
|
33
|
+
result: {
|
|
34
|
+
isError: false;
|
|
35
|
+
content: {
|
|
36
|
+
type: "text";
|
|
37
|
+
text: string;
|
|
38
|
+
}[];
|
|
39
|
+
data: {
|
|
40
|
+
droppedBytes: number;
|
|
41
|
+
totalBytes: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}, never, never>;
|
|
45
|
+
dispose(): void;
|
|
46
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Queue from "effect/Queue";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
const capacity = 8192;
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
// fatal: false is the existing default; Cloudflare's type requires the field.
|
|
7
|
+
const decoder = () => new TextDecoder("utf-8", { ignoreBOM: true, fatal: false });
|
|
8
|
+
/** Complete decoded text, in completion order across stdout/stderr, not byte order. */
|
|
9
|
+
export class OutputCollector {
|
|
10
|
+
wake;
|
|
11
|
+
#producers = {
|
|
12
|
+
stdout: { decoder: decoder(), pendingBytes: 0 },
|
|
13
|
+
stderr: { decoder: decoder(), pendingBytes: 0 },
|
|
14
|
+
};
|
|
15
|
+
#probe = decoder();
|
|
16
|
+
// One bounded ring of complete decoder emissions. No UTF-8 slicing/parser.
|
|
17
|
+
#parts = Array.from({ length: capacity }, () => undefined);
|
|
18
|
+
#head = 0;
|
|
19
|
+
#size = 0;
|
|
20
|
+
#textBytes = 0;
|
|
21
|
+
#newlines = 0;
|
|
22
|
+
#droppedBytes = 0;
|
|
23
|
+
#totalBytes = 0;
|
|
24
|
+
#dirty = false;
|
|
25
|
+
#closed = false;
|
|
26
|
+
constructor(wake) {
|
|
27
|
+
this.wake = wake;
|
|
28
|
+
}
|
|
29
|
+
static make() {
|
|
30
|
+
return Queue.make({ capacity: 1, strategy: "dropping" }).pipe(Effect.map((wake) => new OutputCollector(wake)));
|
|
31
|
+
}
|
|
32
|
+
/** Never waits for a reader. An incomplete scalar stays with its producer. */
|
|
33
|
+
write(stream, bytes) {
|
|
34
|
+
if (this.#closed || bytes.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const producer = this.#producers[stream];
|
|
37
|
+
this.#totalBytes += bytes.length;
|
|
38
|
+
// Byte-sized decoder inputs let each retained emission carry its exact raw
|
|
39
|
+
// cost, even when malformed input expands to replacement characters.
|
|
40
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
41
|
+
const byte = bytes.subarray(i, i + 1);
|
|
42
|
+
producer.pendingBytes++;
|
|
43
|
+
const text = producer.decoder.decode(byte, { stream: true });
|
|
44
|
+
if (text.length === 0)
|
|
45
|
+
continue;
|
|
46
|
+
// A decoder may emit a replacement for a prior incomplete sequence while
|
|
47
|
+
// buffering the current byte as the start of the next scalar. Ask the
|
|
48
|
+
// maintained decoder, rather than duplicating UTF-8 byte grammar.
|
|
49
|
+
const pending = producer.pendingBytes > 1 &&
|
|
50
|
+
this.#probe.decode(byte, { stream: true }).length === 0
|
|
51
|
+
? 1
|
|
52
|
+
: 0;
|
|
53
|
+
this.#probe.decode();
|
|
54
|
+
this.#append(text, producer.pendingBytes - pending);
|
|
55
|
+
producer.pendingBytes = pending;
|
|
56
|
+
}
|
|
57
|
+
this.#dirty = true;
|
|
58
|
+
Queue.offerUnsafe(this.wake, undefined);
|
|
59
|
+
}
|
|
60
|
+
#evict() {
|
|
61
|
+
const part = this.#parts[this.#head];
|
|
62
|
+
if (part === undefined)
|
|
63
|
+
throw new Error("Missing output ring entry");
|
|
64
|
+
this.#parts[this.#head] = undefined;
|
|
65
|
+
this.#head = (this.#head + 1) % capacity;
|
|
66
|
+
this.#size--;
|
|
67
|
+
this.#textBytes -= part.textBytes;
|
|
68
|
+
this.#newlines -= part.newlines;
|
|
69
|
+
this.#droppedBytes += part.rawBytes;
|
|
70
|
+
}
|
|
71
|
+
#append(text, rawBytes) {
|
|
72
|
+
if (text.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
const part = {
|
|
75
|
+
text,
|
|
76
|
+
rawBytes,
|
|
77
|
+
textBytes: encoder.encode(text).length,
|
|
78
|
+
newlines: text.split("\n").length - 1,
|
|
79
|
+
};
|
|
80
|
+
if (this.#size === capacity)
|
|
81
|
+
this.#evict();
|
|
82
|
+
this.#parts[(this.#head + this.#size) % capacity] = part;
|
|
83
|
+
this.#size++;
|
|
84
|
+
this.#textBytes += part.textBytes;
|
|
85
|
+
this.#newlines += part.newlines;
|
|
86
|
+
while (this.#textBytes > capacity || this.#newlines >= 200)
|
|
87
|
+
this.#evict();
|
|
88
|
+
}
|
|
89
|
+
snapshot() {
|
|
90
|
+
let tail = "";
|
|
91
|
+
for (let i = 0; i < this.#size; i++) {
|
|
92
|
+
const part = this.#parts[(this.#head + i) % capacity];
|
|
93
|
+
if (part === undefined)
|
|
94
|
+
throw new Error("Missing output ring entry");
|
|
95
|
+
tail += part.text;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
tail,
|
|
99
|
+
droppedBytes: this.#droppedBytes,
|
|
100
|
+
totalBytes: this.#totalBytes,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
#update() {
|
|
104
|
+
if (!this.#dirty)
|
|
105
|
+
return undefined;
|
|
106
|
+
this.#dirty = false;
|
|
107
|
+
const { tail, ...data } = this.snapshot();
|
|
108
|
+
return {
|
|
109
|
+
type: "update",
|
|
110
|
+
result: {
|
|
111
|
+
isError: false,
|
|
112
|
+
content: [{ type: "text", text: tail }],
|
|
113
|
+
data,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** EOF flush order is stdout then stderr; the final update precedes terminal. */
|
|
118
|
+
finish() {
|
|
119
|
+
if (this.#closed)
|
|
120
|
+
return undefined;
|
|
121
|
+
this.#closed = true;
|
|
122
|
+
for (const producer of [this.#producers.stdout, this.#producers.stderr]) {
|
|
123
|
+
const text = producer.decoder.decode();
|
|
124
|
+
this.#append(text, producer.pendingBytes);
|
|
125
|
+
producer.pendingBytes = 0;
|
|
126
|
+
if (text.length > 0)
|
|
127
|
+
this.#dirty = true;
|
|
128
|
+
}
|
|
129
|
+
return this.#update();
|
|
130
|
+
}
|
|
131
|
+
get updates() {
|
|
132
|
+
return Stream.fromQueue(this.wake).pipe(Stream.mapEffect(() => Effect.sleep("100 millis").pipe(Effect.map(() => (this.#closed ? undefined : this.#update())))), Stream.filter((part) => part !== undefined));
|
|
133
|
+
}
|
|
134
|
+
dispose() {
|
|
135
|
+
this.#closed = true;
|
|
136
|
+
this.#parts.fill(undefined);
|
|
137
|
+
this.#head = this.#size = this.#textBytes = this.#newlines = 0;
|
|
138
|
+
this.#totalBytes = this.#droppedBytes = 0;
|
|
139
|
+
this.#dirty = false;
|
|
140
|
+
for (const producer of [this.#producers.stdout, this.#producers.stderr]) {
|
|
141
|
+
producer.decoder.decode();
|
|
142
|
+
producer.pendingBytes = 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/runtime.js
CHANGED
|
@@ -36,13 +36,13 @@ function settleClosePlans(plans, liveRevisions) {
|
|
|
36
36
|
export function openCoreRuntime(options) {
|
|
37
37
|
return Effect.gen(function* () {
|
|
38
38
|
const gate = yield* Semaphore.make(1);
|
|
39
|
-
const backingWork = createBackingWorkTracker();
|
|
40
39
|
const execution = yield* Effect.try({
|
|
41
40
|
try: () => normalizeExecutionPolicy(options.execution),
|
|
42
41
|
catch: (cause) => cause instanceof MyceliumDefinitionError
|
|
43
42
|
? cause
|
|
44
43
|
: new MyceliumDefinitionError("Execution policy is invalid"),
|
|
45
44
|
});
|
|
45
|
+
const backingWork = yield* createBackingWorkTracker(execution.capacity);
|
|
46
46
|
const sandbox = Object.freeze({
|
|
47
47
|
execute: options.sandbox.execute,
|
|
48
48
|
adapter: options.sandbox.adapter,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const sandboxAdmissionOptionsSchema: z.ZodObject<{
|
|
3
|
+
sandboxCapacity: z.ZodOptional<z.ZodInt>;
|
|
4
|
+
admissionWaitMs: z.ZodOptional<z.ZodInt>;
|
|
5
|
+
}, z.core.$strict>;
|
|
6
|
+
/** Host-owned admission inputs; never part of guest execute arguments. */
|
|
7
|
+
export type SandboxAdmissionOptions = Readonly<{
|
|
8
|
+
/** Positive host-selected capacity; omission derives the host default. */
|
|
9
|
+
sandboxCapacity?: number | undefined;
|
|
10
|
+
/** Maximum admission wait in milliseconds, from 1 through 900,000. */
|
|
11
|
+
admissionWaitMs?: number | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
/** Captured once per runtime and shared across backing-work groups. */
|
|
14
|
+
export type SandboxAdmissionPolicy = Readonly<{
|
|
15
|
+
capacity: number;
|
|
16
|
+
waitMs: number;
|
|
17
|
+
}>;
|
|
18
|
+
/** Validate host policy and resolve defaults from a platform-owned core observation. */
|
|
19
|
+
export declare function normalizeSandboxAdmissionPolicy(options: SandboxAdmissionOptions | undefined, availableCores?: number): SandboxAdmissionPolicy;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MyceliumDefinitionError } from "./errors.js";
|
|
3
|
+
export const sandboxAdmissionOptionsSchema = z.strictObject({
|
|
4
|
+
sandboxCapacity: z.int().positive().optional(),
|
|
5
|
+
admissionWaitMs: z.int().min(1).max(900_000).optional(),
|
|
6
|
+
});
|
|
7
|
+
/** Validate host policy and resolve defaults from a platform-owned core observation. */
|
|
8
|
+
export function normalizeSandboxAdmissionPolicy(options, availableCores) {
|
|
9
|
+
const parsed = sandboxAdmissionOptionsSchema.safeParse(options ?? {});
|
|
10
|
+
const cores = z
|
|
11
|
+
.int()
|
|
12
|
+
.positive()
|
|
13
|
+
.safeParse(availableCores ?? 2);
|
|
14
|
+
if (!parsed.success || !cores.success) {
|
|
15
|
+
throw new MyceliumDefinitionError("Sandbox admission policy is invalid");
|
|
16
|
+
}
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
capacity: parsed.data.sandboxCapacity ?? Math.min(32, Math.max(2, cores.data)),
|
|
19
|
+
waitMs: parsed.data.admissionWaitMs ?? 5_000,
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/sandbox-boundary.js
CHANGED
|
@@ -4,11 +4,12 @@ import * as Result from "effect/Result";
|
|
|
4
4
|
import * as Schema from "effect/Schema";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { InvalidOperationInput } from "./errors.js";
|
|
7
|
-
import { SourceInvalid, GuestThrew, DeadlineExceeded, Cancelled, DispatchLost, SandboxCrashed, BadResponse, ResultTooLarge, ResultNotJson, CapabilityFailed, Unknown, referenceSandboxFailure, sandboxFailureMessage, } from "./sandbox-failure.js";
|
|
8
|
-
const ownedFailureSchema = z.union([
|
|
7
|
+
import { SourceInvalid, GuestThrew, DeadlineExceeded, SandboxBusy, Cancelled, DispatchLost, SandboxCrashed, BadResponse, ResultTooLarge, ResultNotJson, CapabilityFailed, Unknown, referenceSandboxFailure, sandboxFailureMessage, } from "./sandbox-failure.js";
|
|
8
|
+
const ownedFailureSchema = z.lazy(() => z.union([
|
|
9
9
|
z.instanceof(SourceInvalid),
|
|
10
10
|
z.instanceof(GuestThrew),
|
|
11
11
|
z.instanceof(DeadlineExceeded),
|
|
12
|
+
z.instanceof(SandboxBusy),
|
|
12
13
|
z.instanceof(Cancelled),
|
|
13
14
|
z.instanceof(DispatchLost),
|
|
14
15
|
z.instanceof(SandboxCrashed),
|
|
@@ -17,9 +18,9 @@ const ownedFailureSchema = z.union([
|
|
|
17
18
|
z.instanceof(ResultNotJson),
|
|
18
19
|
z.instanceof(CapabilityFailed),
|
|
19
20
|
z.instanceof(Unknown),
|
|
20
|
-
]);
|
|
21
|
+
]));
|
|
21
22
|
/** The one interpretation boundary for foreign in-process values and rejections. */
|
|
22
|
-
const foreignValueSchema = z
|
|
23
|
+
const foreignValueSchema = z.lazy(() => z
|
|
23
24
|
.unknown()
|
|
24
25
|
.pipe(z.union([
|
|
25
26
|
z
|
|
@@ -32,7 +33,12 @@ const foreignValueSchema = z
|
|
|
32
33
|
z.string().transform((value) => ({ kind: "string", value })),
|
|
33
34
|
z.boolean().transform((value) => ({ kind: "boolean", value })),
|
|
34
35
|
z
|
|
35
|
-
.union([
|
|
36
|
+
.union([
|
|
37
|
+
z.number(),
|
|
38
|
+
z.nan(),
|
|
39
|
+
z.literal(Infinity),
|
|
40
|
+
z.literal(-Infinity),
|
|
41
|
+
])
|
|
36
42
|
.transform((value) => ({ kind: "number", value })),
|
|
37
43
|
z.bigint().transform((value) => ({ kind: "bigint", value })),
|
|
38
44
|
z.symbol().transform((value) => ({ kind: "symbol", value })),
|
|
@@ -46,7 +52,7 @@ const foreignValueSchema = z
|
|
|
46
52
|
.union([z.instanceof(Object), z.record(z.string(), z.unknown())])
|
|
47
53
|
.transform((value) => ({ kind: "object", value })),
|
|
48
54
|
z.unknown().transform(() => ({ kind: "opaque" })),
|
|
49
|
-
]));
|
|
55
|
+
])));
|
|
50
56
|
/** Admit only errors constructed by the sandbox owner, using the maintained decoder. */
|
|
51
57
|
export function isSandboxFailure(cause) {
|
|
52
58
|
return ownedFailureSchema.safeParse(cause).success;
|
|
@@ -68,7 +74,7 @@ export function sandboxValueKind(value) {
|
|
|
68
74
|
return decoded.kind;
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
|
-
const bindingInputSchema = z.json().optional();
|
|
77
|
+
const bindingInputSchema = z.lazy(() => z.json().optional());
|
|
72
78
|
/** Decode the RPC argument before any capability callback observes it. */
|
|
73
79
|
export function sandboxBindingInput(value) {
|
|
74
80
|
return bindingInputSchema.parse(value);
|
|
@@ -132,7 +138,7 @@ function operatorValue(cause, seen) {
|
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
|
-
const missingVariable = z
|
|
141
|
+
const missingVariable = z.lazy(() => z
|
|
136
142
|
.object({
|
|
137
143
|
name: z.literal("ReferenceError"),
|
|
138
144
|
message: z
|
|
@@ -142,10 +148,13 @@ const missingVariable = z
|
|
|
142
148
|
.transform((value) => ({
|
|
143
149
|
...value,
|
|
144
150
|
missingVariable: value.message.slice(0, -" is not defined".length),
|
|
145
|
-
}));
|
|
146
|
-
const guestDiagnosticSchema = z
|
|
147
|
-
.union([
|
|
148
|
-
|
|
151
|
+
})));
|
|
152
|
+
const guestDiagnosticSchema = z.lazy(() => z
|
|
153
|
+
.union([
|
|
154
|
+
missingVariable,
|
|
155
|
+
z.object({ name: z.string(), message: z.string() }),
|
|
156
|
+
])
|
|
157
|
+
.and(z.object({ transportMessage: z.string().max(512).optional() })));
|
|
149
158
|
/** Decode the engine's guest-owned diagnostic once; renderers receive typed fields. */
|
|
150
159
|
export function sandboxGuestFailure(cause, bindings = {}) {
|
|
151
160
|
const diagnostic = guestDiagnosticSchema.parse(cause);
|
|
@@ -160,13 +169,14 @@ export function sandboxGuestFailure(cause, bindings = {}) {
|
|
|
160
169
|
}
|
|
161
170
|
return new GuestThrew(diagnostic);
|
|
162
171
|
}
|
|
163
|
-
const transportEnvelopeSchema = z.strictObject({
|
|
172
|
+
const transportEnvelopeSchema = z.lazy(() => z.strictObject({
|
|
164
173
|
v: z.literal(1),
|
|
165
174
|
kind: z.literal("sandbox_failure"),
|
|
166
175
|
tag: z.enum([
|
|
167
176
|
"SourceInvalid",
|
|
168
177
|
"GuestThrew",
|
|
169
178
|
"DeadlineExceeded",
|
|
179
|
+
"SandboxBusy",
|
|
170
180
|
"Cancelled",
|
|
171
181
|
"DispatchLost",
|
|
172
182
|
"SandboxCrashed",
|
|
@@ -178,9 +188,9 @@ const transportEnvelopeSchema = z.strictObject({
|
|
|
178
188
|
]),
|
|
179
189
|
ref: z.string().min(1).max(32),
|
|
180
190
|
message: z.string().max(160),
|
|
181
|
-
});
|
|
191
|
+
}));
|
|
182
192
|
const decodeJsonText = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Unknown));
|
|
183
|
-
const jsonTextSchema = z.string().transform((text, context) => {
|
|
193
|
+
const jsonTextSchema = z.lazy(() => z.string().transform((text, context) => {
|
|
184
194
|
const decoded = decodeJsonText(text);
|
|
185
195
|
if (Result.isFailure(decoded)) {
|
|
186
196
|
context.addIssue({
|
|
@@ -190,7 +200,7 @@ const jsonTextSchema = z.string().transform((text, context) => {
|
|
|
190
200
|
return z.NEVER;
|
|
191
201
|
}
|
|
192
202
|
return decoded.success;
|
|
193
|
-
});
|
|
203
|
+
}));
|
|
194
204
|
const transportMessageSchema = jsonTextSchema.pipe(transportEnvelopeSchema);
|
|
195
205
|
const returnedOperationFailure = z.strictObject({
|
|
196
206
|
error: transportMessageSchema,
|
|
@@ -218,16 +228,15 @@ export function recoverSandboxFailure(failure, failures) {
|
|
|
218
228
|
const original = failures.get(decoded.data.ref);
|
|
219
229
|
return original?._tag === decoded.data.tag ? original : failure;
|
|
220
230
|
}
|
|
221
|
-
const executorFailureSchema = z
|
|
222
|
-
.unknown()
|
|
223
|
-
.pipe(z.union([
|
|
231
|
+
const executorFailureSchema = z.lazy(() => z.unknown().pipe(z.union([
|
|
224
232
|
z
|
|
225
233
|
.literal("Execution timed out")
|
|
226
234
|
.transform(() => ({ kind: "deadline" })),
|
|
227
|
-
z
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
235
|
+
z.unknown().transform((rawCause) => ({
|
|
236
|
+
kind: "bad_response",
|
|
237
|
+
rawCause,
|
|
238
|
+
})),
|
|
239
|
+
])));
|
|
231
240
|
/** Decode Codemode's documented error carrier before adapters choose a failure. */
|
|
232
241
|
export function sandboxExecutorFailure(cause, limitMs, elapsedMs) {
|
|
233
242
|
const decoded = executorFailureSchema.parse(cause);
|
|
@@ -7,7 +7,7 @@ export declare const SandboxFailureEventSchema: Schema.Struct<{
|
|
|
7
7
|
readonly executionId: Schema.String;
|
|
8
8
|
readonly adapter: Schema.String;
|
|
9
9
|
readonly buildSha: Schema.NullOr<Schema.String>;
|
|
10
|
-
readonly tag: Schema.Literals<readonly ["SourceInvalid", "GuestThrew", "DeadlineExceeded", "Cancelled", "DispatchLost", "SandboxCrashed", "BadResponse", "ResultTooLarge", "ResultNotJson", "CapabilityFailed", "Unknown"]>;
|
|
10
|
+
readonly tag: Schema.Literals<readonly ["SourceInvalid", "GuestThrew", "DeadlineExceeded", "SandboxBusy", "Cancelled", "DispatchLost", "SandboxCrashed", "BadResponse", "ResultTooLarge", "ResultNotJson", "CapabilityFailed", "Unknown"]>;
|
|
11
11
|
readonly fields: Schema.$Record<Schema.String, Schema.Codec<z.core.util.JSONType, z.core.util.JSONType, never, never>>;
|
|
12
12
|
readonly healthAlert: Schema.Boolean;
|
|
13
13
|
}>;
|
|
@@ -34,6 +34,17 @@ export declare class DeadlineExceeded extends DeadlineExceeded_base<SandboxFailu
|
|
|
34
34
|
readonly acknowledged?: boolean | undefined;
|
|
35
35
|
}> {
|
|
36
36
|
}
|
|
37
|
+
declare const SandboxBusy_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
38
|
+
readonly _tag: "SandboxBusy";
|
|
39
|
+
} & Readonly<A>;
|
|
40
|
+
/** Admission expired before any sandbox work started. */
|
|
41
|
+
export declare class SandboxBusy extends SandboxBusy_base<SandboxFailureReference & {
|
|
42
|
+
readonly capacity: number;
|
|
43
|
+
readonly waitedMs: number;
|
|
44
|
+
}> {
|
|
45
|
+
/** Stable contention code, not a sandbox health failure. */
|
|
46
|
+
readonly code = "sandbox-busy";
|
|
47
|
+
}
|
|
37
48
|
declare const Cancelled_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
38
49
|
readonly _tag: "Cancelled";
|
|
39
50
|
} & Readonly<A>;
|
|
@@ -120,7 +131,7 @@ export declare class Unknown extends Unknown_base<SandboxFailureReference & {
|
|
|
120
131
|
}> {
|
|
121
132
|
}
|
|
122
133
|
/** Closed error channel shared by sandbox adapters and the invocation owner. */
|
|
123
|
-
export type SandboxFailure = SourceInvalid | GuestThrew | DeadlineExceeded | Cancelled | DispatchLost | SandboxCrashed | BadResponse | ResultTooLarge | ResultNotJson | CapabilityFailed | Unknown;
|
|
134
|
+
export type SandboxFailure = SourceInvalid | GuestThrew | DeadlineExceeded | SandboxBusy | Cancelled | DispatchLost | SandboxCrashed | BadResponse | ResultTooLarge | ResultNotJson | CapabilityFailed | Unknown;
|
|
124
135
|
/** Copy the failure to correlate one execution without mutating shared host errors. */
|
|
125
136
|
export declare function referenceSandboxFailure(failure: SandboxFailure, ref: string): SandboxFailure;
|
|
126
137
|
/** The sole model projection. Transport, parser and raw cause fields stay private. */
|
package/dist/sandbox-failure.js
CHANGED
|
@@ -8,6 +8,11 @@ export class GuestThrew extends Data.TaggedError("GuestThrew") {
|
|
|
8
8
|
/** The invocation owner decided its execution budget was spent. */
|
|
9
9
|
export class DeadlineExceeded extends Data.TaggedError("DeadlineExceeded") {
|
|
10
10
|
}
|
|
11
|
+
/** Admission expired before any sandbox work started. */
|
|
12
|
+
export class SandboxBusy extends Data.TaggedError("SandboxBusy") {
|
|
13
|
+
/** Stable contention code, not a sandbox health failure. */
|
|
14
|
+
code = "sandbox-busy";
|
|
15
|
+
}
|
|
11
16
|
/** The invocation owner accepted cancellation. */
|
|
12
17
|
export class Cancelled extends Data.TaggedError("Cancelled") {
|
|
13
18
|
}
|
|
@@ -62,6 +67,12 @@ export function referenceSandboxFailure(failure, ref) {
|
|
|
62
67
|
acknowledged: failure.acknowledged,
|
|
63
68
|
ref,
|
|
64
69
|
});
|
|
70
|
+
case "SandboxBusy":
|
|
71
|
+
return new SandboxBusy({
|
|
72
|
+
capacity: failure.capacity,
|
|
73
|
+
waitedMs: failure.waitedMs,
|
|
74
|
+
ref,
|
|
75
|
+
});
|
|
65
76
|
case "Cancelled":
|
|
66
77
|
return new Cancelled({
|
|
67
78
|
by: failure.by,
|
|
@@ -126,6 +137,9 @@ export function sandboxFailureMessage(failure, includeReference = true) {
|
|
|
126
137
|
case "DeadlineExceeded":
|
|
127
138
|
text = `execute hit its ${String(failure.limitMs)} ms deadline after ${String(failure.elapsedMs)} ms; split the work or persist progress${failure.acknowledged === false ? "; sandbox did not acknowledge cancellation" : ""}`;
|
|
128
139
|
break;
|
|
140
|
+
case "SandboxBusy":
|
|
141
|
+
text = `sandbox-busy: all ${String(failure.capacity)} sandbox slots remained occupied after waiting ${String(failure.waitedMs)} ms. Nothing ran; retry this call after capacity becomes available.`;
|
|
142
|
+
break;
|
|
129
143
|
case "Cancelled":
|
|
130
144
|
text =
|
|
131
145
|
failure.acknowledged === false
|
package/dist/sandbox-guest.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
/** Closed guest envelope: guest exceptions cannot masquerade as host failures. */
|
|
3
|
-
export declare const sandboxGuestResultSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3
|
+
export declare const sandboxGuestResultSchema: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
4
4
|
ok: z.ZodLiteral<true>;
|
|
5
5
|
value: z.ZodUnknown;
|
|
6
6
|
}, z.core.$strict>, z.ZodObject<{
|
|
@@ -11,7 +11,7 @@ export declare const sandboxGuestResultSchema: z.ZodDiscriminatedUnion<[z.ZodObj
|
|
|
11
11
|
message: z.ZodString;
|
|
12
12
|
transportMessage: z.ZodOptional<z.ZodString>;
|
|
13
13
|
}, z.core.$strict>;
|
|
14
|
-
}, z.core.$strict>], "ok"
|
|
14
|
+
}, z.core.$strict>], "ok">>;
|
|
15
15
|
/** Locate syntax in the submitted code, accounting for Codemode's normalization. */
|
|
16
16
|
export declare function submittedSourceLocation(normalized: string, submitted: string): Readonly<{
|
|
17
17
|
line: number;
|
|
@@ -19,13 +19,13 @@ export declare function submittedSourceLocation(normalized: string, submitted: s
|
|
|
19
19
|
excerpt: string;
|
|
20
20
|
}> | null;
|
|
21
21
|
/** Parsed guest namespace segments; callbacks never cross this wire. */
|
|
22
|
-
export declare const sandboxNamespaceSegmentSchema: z.ZodString
|
|
22
|
+
export declare const sandboxNamespaceSegmentSchema: z.ZodLazy<z.ZodString>;
|
|
23
23
|
/** A transport alias is interpolated as a strict JavaScript binding identifier. */
|
|
24
|
-
export declare const sandboxTransportAliasSchema: z.ZodString
|
|
24
|
+
export declare const sandboxTransportAliasSchema: z.ZodLazy<z.ZodString>;
|
|
25
25
|
/** Transport aliases mapped to their logical namespace paths. */
|
|
26
|
-
export declare const sandboxNamespacePathsSchema: z.ZodRecord<z.ZodString
|
|
26
|
+
export declare const sandboxNamespacePathsSchema: z.ZodLazy<z.ZodRecord<z.ZodLazy<z.ZodString>, z.ZodArray<z.ZodLazy<z.ZodString>>>>;
|
|
27
27
|
/** Serializable flat transport table; operation keys may have any namespace depth. */
|
|
28
|
-
export declare const sandboxBindingNamesSchema: z.ZodRecord<z.ZodString
|
|
28
|
+
export declare const sandboxBindingNamesSchema: z.ZodLazy<z.ZodRecord<z.ZodLazy<z.ZodString>, z.ZodArray<z.ZodString>>>;
|
|
29
29
|
/** Serializable operation names; callbacks stay in the host's existing binding table. */
|
|
30
30
|
export declare function sandboxBindingNames<A>(bindings: Readonly<Record<string, Readonly<Record<string, A>>>>): Readonly<Record<string, readonly string[]>>;
|
|
31
31
|
/** Injective provider identifier: Codemode sanitizes dotted function names. */
|
package/dist/sandbox-guest.js
CHANGED
|
@@ -2,7 +2,7 @@ import { parse } from "acorn";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { sandboxSourceError } from "./sandbox-boundary.js";
|
|
4
4
|
/** Closed guest envelope: guest exceptions cannot masquerade as host failures. */
|
|
5
|
-
export const sandboxGuestResultSchema = z.discriminatedUnion("ok", [
|
|
5
|
+
export const sandboxGuestResultSchema = z.lazy(() => z.discriminatedUnion("ok", [
|
|
6
6
|
z.strictObject({ ok: z.literal(true), value: z.unknown() }),
|
|
7
7
|
z.strictObject({
|
|
8
8
|
ok: z.literal(false),
|
|
@@ -13,7 +13,7 @@ export const sandboxGuestResultSchema = z.discriminatedUnion("ok", [
|
|
|
13
13
|
transportMessage: z.string().max(512).optional(),
|
|
14
14
|
}),
|
|
15
15
|
}),
|
|
16
|
-
]);
|
|
16
|
+
]));
|
|
17
17
|
/** Locate syntax in the submitted code, accounting for Codemode's normalization. */
|
|
18
18
|
export function submittedSourceLocation(normalized, submitted) {
|
|
19
19
|
try {
|
|
@@ -45,9 +45,7 @@ export function submittedSourceLocation(normalized, submitted) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
/** Parsed guest namespace segments; callbacks never cross this wire. */
|
|
48
|
-
export const sandboxNamespaceSegmentSchema = z
|
|
49
|
-
.string()
|
|
50
|
-
.regex(/^[A-Za-z][A-Za-z0-9_]*$/u);
|
|
48
|
+
export const sandboxNamespaceSegmentSchema = z.lazy(() => z.string().regex(/^[A-Za-z][A-Za-z0-9_]*$/u));
|
|
51
49
|
/** A transport alias is interpolated as a strict JavaScript binding identifier. */
|
|
52
50
|
export const sandboxTransportAliasSchema = sandboxNamespaceSegmentSchema.refine((alias) => {
|
|
53
51
|
if (alias === "env")
|
|
@@ -63,9 +61,9 @@ export const sandboxTransportAliasSchema = sandboxNamespaceSegmentSchema.refine(
|
|
|
63
61
|
}
|
|
64
62
|
}, "Invalid sandbox transport alias");
|
|
65
63
|
/** Transport aliases mapped to their logical namespace paths. */
|
|
66
|
-
export const sandboxNamespacePathsSchema = z.record(sandboxTransportAliasSchema, z.array(sandboxNamespaceSegmentSchema).min(1));
|
|
64
|
+
export const sandboxNamespacePathsSchema = z.lazy(() => z.record(sandboxTransportAliasSchema, z.array(sandboxNamespaceSegmentSchema).min(1)));
|
|
67
65
|
/** Serializable flat transport table; operation keys may have any namespace depth. */
|
|
68
|
-
export const sandboxBindingNamesSchema = z.record(sandboxTransportAliasSchema, z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)*$/u)));
|
|
66
|
+
export const sandboxBindingNamesSchema = z.lazy(() => z.record(sandboxTransportAliasSchema, z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)*$/u))));
|
|
69
67
|
/** Serializable operation names; callbacks stay in the host's existing binding table. */
|
|
70
68
|
export function sandboxBindingNames(bindings) {
|
|
71
69
|
return Object.fromEntries(Object.entries(bindings).map(([namespace, operations]) => [
|
package/dist/sandbox-wire.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export declare const sandboxWireSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
20
20
|
kind: z.ZodLiteral<"deadline_exceeded">;
|
|
21
21
|
limitMs: z.ZodNumber;
|
|
22
22
|
elapsedMs: z.ZodNumber;
|
|
23
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
24
|
+
kind: z.ZodLiteral<"sandbox_busy">;
|
|
25
|
+
code: z.ZodLiteral<"sandbox-busy">;
|
|
26
|
+
capacity: z.ZodInt;
|
|
27
|
+
waitedMs: z.ZodNumber;
|
|
23
28
|
}, z.core.$strict>, z.ZodObject<{
|
|
24
29
|
kind: z.ZodLiteral<"result_not_json">;
|
|
25
30
|
valueKind: z.ZodString;
|