@effect-agent/sandbox-local 0.1.0-beta.8 → 0.1.0-beta.80

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.
@@ -0,0 +1,22 @@
1
+ import { Sandbox, SandboxImplementation } from "@effect-agent/sandbox/Sandbox";
2
+ import { Layer } from "effect";
3
+ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
4
+ declare namespace LocalSandbox_d_exports {
5
+ export { layer, sandboxLayer, unisolatedImplementation };
6
+ }
7
+ /**
8
+ * The only implementation identity produced by this package. It deliberately states that local
9
+ * process execution is unisolated development tooling, not a security sandbox.
10
+ */
11
+ declare const unisolatedImplementation: SandboxImplementation;
12
+ /**
13
+ * Development-only local process adapter with its `ChildProcessSpawner` requirement kept visible
14
+ * so composition roots and tests can inject a spawner double. It is unisolated and must never be
15
+ * used as a security boundary for untrusted code or commands.
16
+ */
17
+ declare const sandboxLayer: Layer.Layer<Sandbox, never, ChildProcessSpawner>;
18
+ /** A scoped Node process implementation whose events are always labeled `unisolated`. */
19
+ declare const layer: Layer.Layer<Sandbox>;
20
+ //#endregion
21
+ export { layer, sandboxLayer, LocalSandbox_d_exports as t, unisolatedImplementation };
22
+ //# sourceMappingURL=LocalSandbox.d.mts.map
@@ -0,0 +1,157 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { SANDBOX_DIAGNOSTIC_MAX_LENGTH, Sandbox, SandboxExitError, SandboxExited, SandboxImplementation, SandboxOutput, SandboxOutputLimitError, SandboxResourceUse, SandboxSpawnError, SandboxStarted, SandboxTimeoutError, SandboxUnsupportedRequestError } from "@effect-agent/sandbox/Sandbox";
3
+ import { NodeServices } from "@effect/platform-node";
4
+ import { Clock, Config, Duration, Effect, Layer, Option, Ref, Stream } from "effect";
5
+ import { ChildProcess } from "effect/unstable/process";
6
+ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
7
+ //#region src/LocalSandbox.ts
8
+ var LocalSandbox_exports = /* @__PURE__ */ __exportAll({
9
+ layer: () => layer,
10
+ sandboxLayer: () => sandboxLayer,
11
+ unisolatedImplementation: () => unisolatedImplementation
12
+ });
13
+ /**
14
+ * The only implementation identity produced by this package. It deliberately states that local
15
+ * process execution is unisolated development tooling, not a security sandbox.
16
+ */
17
+ const unisolatedImplementation = SandboxImplementation.make({
18
+ isolation: "unisolated",
19
+ identity: "local-process"
20
+ });
21
+ const zeroOutput = {
22
+ stdout: 0,
23
+ stderr: 0
24
+ };
25
+ const boundedDiagnostic = (message) => message.slice(0, SANDBOX_DIAGNOSTIC_MAX_LENGTH);
26
+ const unsupported = (feature, message) => SandboxUnsupportedRequestError.make({
27
+ implementation: unisolatedImplementation,
28
+ feature,
29
+ message: boundedDiagnostic(message)
30
+ });
31
+ const validateRequest = Effect.fn("LocalSandbox.validateRequest")(function* (request) {
32
+ if (request.runtime.kind !== "unisolated-process" || request.runtime.identity !== unisolatedImplementation.identity) return yield* unsupported("runtime", "The local process runner only accepts runtime kind 'unisolated-process' with identity 'local-process'.");
33
+ if (request.mounts.length > 0) return yield* unsupported("mounts", "The unisolated local process runner cannot enforce mount access modes.");
34
+ if (request.network._tag !== "NetworkDisabled") return yield* unsupported("network", "The unisolated local process runner cannot enforce workload network policy.");
35
+ if (request.limits.cpuCores !== void 0) return yield* unsupported("cpu-limit", "The unisolated local process runner cannot enforce CPU limits.");
36
+ if (request.limits.memoryBytes !== void 0) return yield* unsupported("memory-limit", "The unisolated local process runner cannot enforce memory limits.");
37
+ if (request.secretHandles.length > 0) return yield* unsupported("secret-handles", "The unisolated local process runner does not resolve secret handles into an environment.");
38
+ if (request.artifactRules.length > 0) return yield* unsupported("artifacts", "The unisolated local process runner does not collect artifacts.");
39
+ });
40
+ const spawnError = (request, message, cause) => SandboxSpawnError.make({
41
+ implementation: unisolatedImplementation,
42
+ command: request.command,
43
+ message: boundedDiagnostic(message),
44
+ ...cause === void 0 ? {} : { cause }
45
+ });
46
+ const exitError = (message, cause, exitCode = -1) => SandboxExitError.make({
47
+ implementation: unisolatedImplementation,
48
+ exitCode,
49
+ message: boundedDiagnostic(message),
50
+ ...cause === void 0 ? {} : { cause }
51
+ });
52
+ const environmentFromAllowlist = Effect.fn("LocalSandbox.environmentFromAllowlist")(function* (request) {
53
+ const environment = {};
54
+ for (const name of request.environment.allow) {
55
+ const value = yield* Config.option(Config.string(name)).pipe(Effect.mapError((error) => spawnError(request, `Could not read allowed environment variable '${name}': ${error.message}`, error)));
56
+ if (Option.isSome(value)) environment[name] = value.value;
57
+ }
58
+ return environment;
59
+ });
60
+ const outputEvent = (counts, stream, bytes, decoder, limit) => Ref.modify(counts, (current) => {
61
+ const streamBytes = current[stream] + bytes.byteLength;
62
+ const next = {
63
+ ...current,
64
+ [stream]: streamBytes
65
+ };
66
+ return [next.stdout + next.stderr, next];
67
+ }).pipe(Effect.flatMap((observed) => observed > limit ? Effect.fail(SandboxOutputLimitError.make({
68
+ implementation: unisolatedImplementation,
69
+ stream,
70
+ limit,
71
+ observed
72
+ })) : Effect.succeed(SandboxOutput.make({
73
+ eventVersion: 1,
74
+ implementation: unisolatedImplementation,
75
+ stream,
76
+ text: decoder.decode(bytes, { stream: true }),
77
+ bytes: bytes.byteLength
78
+ }))));
79
+ /**
80
+ * Emits any text the streaming decoder still holds once its source stream ends. The final decode
81
+ * must run at end-of-stream, not at pipeline construction, so the flush is suspended; a trailing
82
+ * incomplete UTF-8 sequence surfaces as replacement text. Its raw bytes were already counted when
83
+ * the chunk arrived, so the flush event carries zero bytes.
84
+ */
85
+ const flushDecoder = (stream, decoder) => Stream.suspend(() => {
86
+ const text = decoder.decode();
87
+ return text.length === 0 ? Stream.empty : Stream.succeed(SandboxOutput.make({
88
+ eventVersion: 1,
89
+ implementation: unisolatedImplementation,
90
+ stream,
91
+ text,
92
+ bytes: 0
93
+ }));
94
+ });
95
+ const makeExecute = (spawner) => (request) => Stream.unwrap(Effect.gen(function* () {
96
+ yield* validateRequest(request);
97
+ const startedAt = yield* Clock.currentTimeMillis;
98
+ const counts = yield* Ref.make(zeroOutput);
99
+ const stdoutDecoder = new TextDecoder();
100
+ const stderrDecoder = new TextDecoder();
101
+ const environment = yield* environmentFromAllowlist(request);
102
+ const command = ChildProcess.make(request.command, request.args, {
103
+ cwd: request.cwd,
104
+ env: environment,
105
+ extendEnv: false,
106
+ stdout: "pipe",
107
+ stderr: "pipe"
108
+ });
109
+ const child = yield* spawner.spawn(command).pipe(Effect.mapError((error) => spawnError(request, error.message, error)));
110
+ const streamOutput = (stream, decoder) => {
111
+ return (stream === "stdout" ? child.stdout : child.stderr).pipe(Stream.mapError((error) => exitError(error.message, error)), Stream.mapEffect((bytes) => outputEvent(counts, stream, bytes, decoder, request.limits.maxOutputBytes)), Stream.concat(flushDecoder(stream, decoder)));
112
+ };
113
+ const terminal = Stream.unwrap(Effect.gen(function* () {
114
+ const exitCode = yield* child.exitCode.pipe(Effect.mapError((error) => exitError(error.message, error)));
115
+ const endedAt = yield* Clock.currentTimeMillis;
116
+ const output = yield* Ref.get(counts);
117
+ const resourceUse = SandboxResourceUse.make({
118
+ wallTime: Duration.millis(endedAt - startedAt),
119
+ stdoutBytes: output.stdout,
120
+ stderrBytes: output.stderr
121
+ });
122
+ const exited = SandboxExited.make({
123
+ eventVersion: 1,
124
+ implementation: unisolatedImplementation,
125
+ exitCode,
126
+ resourceUse,
127
+ artifacts: []
128
+ });
129
+ return exitCode === 0 ? Stream.succeed(exited) : Stream.concat(Stream.succeed(exited), Stream.fail(exitError(`Unisolated local process exited with code ${exitCode}.`, void 0, exitCode)));
130
+ }));
131
+ return Stream.concat(Stream.succeed(SandboxStarted.make({
132
+ eventVersion: 1,
133
+ implementation: unisolatedImplementation,
134
+ runtime: request.runtime
135
+ })), Stream.concat(Stream.merge(streamOutput("stdout", stdoutDecoder), streamOutput("stderr", stderrDecoder)), terminal));
136
+ })).pipe(Stream.interruptWhen(Effect.gen(function* () {
137
+ yield* Effect.sleep(request.limits.maxWallTime);
138
+ return yield* SandboxTimeoutError.make({
139
+ implementation: unisolatedImplementation,
140
+ maxWallTime: request.limits.maxWallTime
141
+ });
142
+ })), Stream.withSpan("LocalSandbox.execute"));
143
+ /**
144
+ * Development-only local process adapter with its `ChildProcessSpawner` requirement kept visible
145
+ * so composition roots and tests can inject a spawner double. It is unisolated and must never be
146
+ * used as a security boundary for untrusted code or commands.
147
+ */
148
+ const sandboxLayer = Layer.effect(Sandbox)(Effect.gen(function* () {
149
+ const spawner = yield* ChildProcessSpawner;
150
+ return Sandbox.of({ execute: makeExecute(spawner) });
151
+ }));
152
+ /** A scoped Node process implementation whose events are always labeled `unisolated`. */
153
+ const layer = sandboxLayer.pipe(Layer.provide(NodeServices.layer));
154
+ //#endregion
155
+ export { layer, sandboxLayer, LocalSandbox_exports as t, unisolatedImplementation };
156
+
157
+ //# sourceMappingURL=LocalSandbox.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalSandbox.mjs","names":[],"sources":["../src/LocalSandbox.ts"],"sourcesContent":["import {\n SANDBOX_DIAGNOSTIC_MAX_LENGTH,\n Sandbox,\n SandboxExited,\n SandboxExitError,\n SandboxImplementation,\n SandboxOutput,\n SandboxOutputLimitError,\n SandboxResourceUse,\n SandboxSpawnError,\n SandboxStarted,\n SandboxTimeoutError,\n SandboxUnsupportedRequestError,\n type SandboxEvent,\n type SandboxExecute,\n type SandboxRequest,\n} from \"@effect-agent/sandbox/Sandbox\";\nimport { NodeServices } from \"@effect/platform-node\";\nimport { Clock, Config, Duration, Effect, Layer, Option, Ref, Stream } from \"effect\";\nimport { ChildProcess } from \"effect/unstable/process\";\nimport { ChildProcessSpawner } from \"effect/unstable/process/ChildProcessSpawner\";\n\n/**\n * The only implementation identity produced by this package. It deliberately states that local\n * process execution is unisolated development tooling, not a security sandbox.\n */\nexport const unisolatedImplementation = SandboxImplementation.make({\n isolation: \"unisolated\",\n identity: \"local-process\",\n});\n\ntype OutputStream = \"stdout\" | \"stderr\";\ntype OutputCounts = Readonly<Record<OutputStream, number>>;\n\nconst zeroOutput: OutputCounts = { stdout: 0, stderr: 0 };\n\nconst boundedDiagnostic = (message: string): string =>\n message.slice(0, SANDBOX_DIAGNOSTIC_MAX_LENGTH);\n\nconst unsupported = (\n feature: Parameters<typeof SandboxUnsupportedRequestError.make>[0][\"feature\"],\n message: string,\n) =>\n SandboxUnsupportedRequestError.make({\n implementation: unisolatedImplementation,\n feature,\n message: boundedDiagnostic(message),\n });\n\nconst validateRequest = Effect.fn(\"LocalSandbox.validateRequest\")(function* (\n request: SandboxRequest,\n) {\n if (\n request.runtime.kind !== \"unisolated-process\" ||\n request.runtime.identity !== unisolatedImplementation.identity\n ) {\n return yield* unsupported(\n \"runtime\",\n \"The local process runner only accepts runtime kind 'unisolated-process' with identity 'local-process'.\",\n );\n }\n if (request.mounts.length > 0) {\n return yield* unsupported(\n \"mounts\",\n \"The unisolated local process runner cannot enforce mount access modes.\",\n );\n }\n if (request.network._tag !== \"NetworkDisabled\") {\n return yield* unsupported(\n \"network\",\n \"The unisolated local process runner cannot enforce workload network policy.\",\n );\n }\n if (request.limits.cpuCores !== undefined) {\n return yield* unsupported(\n \"cpu-limit\",\n \"The unisolated local process runner cannot enforce CPU limits.\",\n );\n }\n if (request.limits.memoryBytes !== undefined) {\n return yield* unsupported(\n \"memory-limit\",\n \"The unisolated local process runner cannot enforce memory limits.\",\n );\n }\n if (request.secretHandles.length > 0) {\n return yield* unsupported(\n \"secret-handles\",\n \"The unisolated local process runner does not resolve secret handles into an environment.\",\n );\n }\n if (request.artifactRules.length > 0) {\n return yield* unsupported(\n \"artifacts\",\n \"The unisolated local process runner does not collect artifacts.\",\n );\n }\n});\n\nconst spawnError = (request: SandboxRequest, message: string, cause?: unknown) =>\n SandboxSpawnError.make({\n implementation: unisolatedImplementation,\n command: request.command,\n message: boundedDiagnostic(message),\n ...(cause === undefined ? {} : { cause }),\n });\n\nconst exitError = (message: string, cause?: unknown, exitCode = -1) =>\n SandboxExitError.make({\n implementation: unisolatedImplementation,\n exitCode,\n message: boundedDiagnostic(message),\n ...(cause === undefined ? {} : { cause }),\n });\n\nconst environmentFromAllowlist = Effect.fn(\"LocalSandbox.environmentFromAllowlist\")(function* (\n request: SandboxRequest,\n) {\n const environment: Record<string, string> = {};\n\n for (const name of request.environment.allow) {\n const value = yield* Config.option(Config.string(name)).pipe(\n Effect.mapError((error) =>\n spawnError(\n request,\n `Could not read allowed environment variable '${name}': ${error.message}`,\n error,\n ),\n ),\n );\n\n if (Option.isSome(value)) {\n environment[name] = value.value;\n }\n }\n\n return environment;\n});\n\nconst outputEvent = (\n counts: Ref.Ref<OutputCounts>,\n stream: OutputStream,\n bytes: Uint8Array,\n decoder: TextDecoder,\n limit: number,\n): Effect.Effect<SandboxOutput, SandboxOutputLimitError> =>\n Ref.modify(counts, (current) => {\n const streamBytes = current[stream] + bytes.byteLength;\n const next = { ...current, [stream]: streamBytes };\n\n return [next.stdout + next.stderr, next] as const;\n }).pipe(\n Effect.flatMap((observed) =>\n observed > limit\n ? Effect.fail(\n SandboxOutputLimitError.make({\n implementation: unisolatedImplementation,\n stream,\n limit,\n observed,\n }),\n )\n : Effect.succeed(\n SandboxOutput.make({\n eventVersion: 1,\n implementation: unisolatedImplementation,\n stream,\n text: decoder.decode(bytes, { stream: true }),\n bytes: bytes.byteLength,\n }),\n ),\n ),\n );\n\n/**\n * Emits any text the streaming decoder still holds once its source stream ends. The final decode\n * must run at end-of-stream, not at pipeline construction, so the flush is suspended; a trailing\n * incomplete UTF-8 sequence surfaces as replacement text. Its raw bytes were already counted when\n * the chunk arrived, so the flush event carries zero bytes.\n */\nconst flushDecoder = (stream: OutputStream, decoder: TextDecoder): Stream.Stream<SandboxOutput> =>\n Stream.suspend(() => {\n const text = decoder.decode();\n\n return text.length === 0\n ? Stream.empty\n : Stream.succeed(\n SandboxOutput.make({\n eventVersion: 1,\n implementation: unisolatedImplementation,\n stream,\n text,\n bytes: 0,\n }),\n );\n });\n\nconst makeExecute =\n (spawner: ChildProcessSpawner[\"Service\"]): SandboxExecute =>\n (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* validateRequest(request);\n const startedAt = yield* Clock.currentTimeMillis;\n const counts = yield* Ref.make(zeroOutput);\n const stdoutDecoder = new TextDecoder();\n const stderrDecoder = new TextDecoder();\n const environment = yield* environmentFromAllowlist(request);\n\n const command = ChildProcess.make(request.command, request.args, {\n cwd: request.cwd,\n env: environment,\n extendEnv: false,\n stdout: \"pipe\",\n stderr: \"pipe\",\n });\n\n const child = yield* spawner\n .spawn(command)\n .pipe(Effect.mapError((error) => spawnError(request, error.message, error)));\n\n const streamOutput = (stream: OutputStream, decoder: TextDecoder) => {\n const source = stream === \"stdout\" ? child.stdout : child.stderr;\n\n return source.pipe(\n Stream.mapError((error) => exitError(error.message, error)),\n Stream.mapEffect((bytes) =>\n outputEvent(counts, stream, bytes, decoder, request.limits.maxOutputBytes),\n ),\n Stream.concat(flushDecoder(stream, decoder)),\n );\n };\n\n const terminal = Stream.unwrap(\n Effect.gen(function* () {\n const exitCode = yield* child.exitCode.pipe(\n Effect.mapError((error) => exitError(error.message, error)),\n );\n\n const endedAt = yield* Clock.currentTimeMillis;\n const output = yield* Ref.get(counts);\n\n const resourceUse = SandboxResourceUse.make({\n wallTime: Duration.millis(endedAt - startedAt),\n stdoutBytes: output.stdout,\n stderrBytes: output.stderr,\n });\n\n const exited = SandboxExited.make({\n eventVersion: 1,\n implementation: unisolatedImplementation,\n exitCode,\n resourceUse,\n artifacts: [],\n });\n\n return exitCode === 0\n ? Stream.succeed<SandboxEvent>(exited)\n : Stream.concat(\n Stream.succeed<SandboxEvent>(exited),\n Stream.fail(\n exitError(\n `Unisolated local process exited with code ${exitCode}.`,\n undefined,\n exitCode,\n ),\n ),\n );\n }),\n );\n\n return Stream.concat(\n Stream.succeed<SandboxEvent>(\n SandboxStarted.make({\n eventVersion: 1,\n implementation: unisolatedImplementation,\n runtime: request.runtime,\n }),\n ),\n Stream.concat(\n Stream.merge(\n streamOutput(\"stdout\", stdoutDecoder),\n streamOutput(\"stderr\", stderrDecoder),\n ),\n terminal,\n ),\n );\n }),\n ).pipe(\n Stream.interruptWhen(\n Effect.gen(function* () {\n yield* Effect.sleep(request.limits.maxWallTime);\n\n return yield* SandboxTimeoutError.make({\n implementation: unisolatedImplementation,\n maxWallTime: request.limits.maxWallTime,\n });\n }),\n ),\n Stream.withSpan(\"LocalSandbox.execute\"),\n );\n\n/**\n * Development-only local process adapter with its `ChildProcessSpawner` requirement kept visible\n * so composition roots and tests can inject a spawner double. It is unisolated and must never be\n * used as a security boundary for untrusted code or commands.\n */\nexport const sandboxLayer: Layer.Layer<Sandbox, never, ChildProcessSpawner> = Layer.effect(Sandbox)(\n Effect.gen(function* () {\n const spawner = yield* ChildProcessSpawner;\n\n return Sandbox.of({ execute: makeExecute(spawner) });\n }),\n);\n\n/** A scoped Node process implementation whose events are always labeled `unisolated`. */\nexport const layer: Layer.Layer<Sandbox> = sandboxLayer.pipe(Layer.provide(NodeServices.layer));\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,20 +1,2 @@
1
- import { Sandbox, SandboxImplementation } from "@effect-agent/sandbox";
2
- import { Layer } from "effect";
3
- import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
4
- //#region src/local-sandbox.d.ts
5
- /**
6
- * The only implementation identity produced by this package. It deliberately states that local
7
- * process execution is unisolated development tooling, not a security sandbox.
8
- */
9
- declare const unisolatedImplementation: SandboxImplementation;
10
- /**
11
- * Development-only local process adapter with its `ChildProcessSpawner` requirement kept visible
12
- * so composition roots and tests can inject a spawner double. It is unisolated and must never be
13
- * used as a security boundary for untrusted code or commands.
14
- */
15
- declare const sandboxLayer: Layer.Layer<Sandbox, never, ChildProcessSpawner>;
16
- /** A scoped Node process implementation whose events are always labeled `unisolated`. */
17
- declare const layer: Layer.Layer<Sandbox>;
18
- //#endregion
19
- export { layer, sandboxLayer, unisolatedImplementation };
20
- //# sourceMappingURL=index.d.mts.map
1
+ import { t as LocalSandbox_d_exports } from "./LocalSandbox.mjs";
2
+ export { LocalSandbox_d_exports as LocalSandbox };
package/dist/index.mjs CHANGED
@@ -1,151 +1,2 @@
1
- import { Sandbox, SandboxExitError, SandboxExited, SandboxImplementation, SandboxOutput, SandboxOutputLimitError, SandboxResourceUse, SandboxSpawnError, SandboxStarted, SandboxTimeoutError, SandboxUnsupportedRequestError } from "@effect-agent/sandbox";
2
- import { NodeServices } from "@effect/platform-node";
3
- import { Clock, Config, Duration, Effect, Layer, Option, Ref, Stream } from "effect";
4
- import { ChildProcess } from "effect/unstable/process";
5
- import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
6
- //#region src/local-sandbox.ts
7
- /**
8
- * The only implementation identity produced by this package. It deliberately states that local
9
- * process execution is unisolated development tooling, not a security sandbox.
10
- */
11
- const unisolatedImplementation = SandboxImplementation.make({
12
- isolation: "unisolated",
13
- identity: "local-process"
14
- });
15
- const zeroOutput = {
16
- stdout: 0,
17
- stderr: 0
18
- };
19
- const unsupported = (feature, message) => SandboxUnsupportedRequestError.make({
20
- implementation: unisolatedImplementation,
21
- feature,
22
- message
23
- });
24
- const validateRequest = (request) => {
25
- if (request.runtime.kind !== "unisolated-process") return Effect.fail(unsupported("runtime", "The local process runner only accepts runtime.kind 'unisolated-process'."));
26
- if (request.mounts.length > 0) return Effect.fail(unsupported("mounts", "The unisolated local process runner cannot enforce mount access modes."));
27
- if (request.network._tag !== "NetworkDisabled") return Effect.fail(unsupported("network", "The unisolated local process runner cannot enforce workload network policy."));
28
- if (request.limits.cpuCores !== void 0) return Effect.fail(unsupported("cpu-limit", "The unisolated local process runner cannot enforce CPU limits."));
29
- if (request.limits.memoryBytes !== void 0) return Effect.fail(unsupported("memory-limit", "The unisolated local process runner cannot enforce memory limits."));
30
- if (request.secretHandles.length > 0) return Effect.fail(unsupported("secret-handles", "The unisolated local process runner does not resolve secret handles into an environment."));
31
- if (request.artifactRules.length > 0) return Effect.fail(unsupported("artifacts", "The unisolated local process runner does not collect artifacts."));
32
- return Effect.void;
33
- };
34
- const spawnError = (request, message, cause) => SandboxSpawnError.make({
35
- implementation: unisolatedImplementation,
36
- command: request.command,
37
- message,
38
- ...cause === void 0 ? {} : { cause }
39
- });
40
- const environmentFromAllowlist = Effect.fn("LocalSandbox.environmentFromAllowlist")(function* (request) {
41
- const environment = {};
42
- for (const name of request.environment.allow) {
43
- const value = yield* Config.option(Config.string(name)).pipe(Effect.mapError((error) => spawnError(request, `Could not read allowed environment variable '${name}': ${error.message}`, error)));
44
- if (Option.isSome(value)) environment[name] = value.value;
45
- }
46
- return environment;
47
- });
48
- const outputEvent = (counts, stream, bytes, decoder, limit) => Ref.modify(counts, (current) => {
49
- const streamBytes = current[stream] + bytes.byteLength;
50
- const next = {
51
- ...current,
52
- [stream]: streamBytes
53
- };
54
- return [next.stdout + next.stderr, next];
55
- }).pipe(Effect.flatMap((observed) => observed > limit ? Effect.fail(SandboxOutputLimitError.make({
56
- implementation: unisolatedImplementation,
57
- stream,
58
- limit,
59
- observed
60
- })) : Effect.succeed(SandboxOutput.make({
61
- eventVersion: 1,
62
- implementation: unisolatedImplementation,
63
- stream,
64
- text: decoder.decode(bytes, { stream: true }),
65
- bytes: bytes.byteLength
66
- }))));
67
- /**
68
- * Emits any text the streaming decoder still holds once its source stream ends. The final decode
69
- * must run at end-of-stream, not at pipeline construction, so the flush is suspended; a trailing
70
- * incomplete UTF-8 sequence surfaces as replacement text. Its raw bytes were already counted when
71
- * the chunk arrived, so the flush event carries zero bytes.
72
- */
73
- const flushDecoder = (stream, decoder) => Stream.suspend(() => {
74
- const text = decoder.decode();
75
- return text.length === 0 ? Stream.empty : Stream.succeed(SandboxOutput.make({
76
- eventVersion: 1,
77
- implementation: unisolatedImplementation,
78
- stream,
79
- text,
80
- bytes: 0
81
- }));
82
- });
83
- const makeExecute = (spawner) => (request) => Stream.unwrap(Effect.gen(function* () {
84
- yield* validateRequest(request);
85
- const startedAt = yield* Clock.currentTimeMillis;
86
- const counts = yield* Ref.make(zeroOutput);
87
- const stdoutDecoder = new TextDecoder();
88
- const stderrDecoder = new TextDecoder();
89
- const environment = yield* environmentFromAllowlist(request);
90
- const command = ChildProcess.make(request.command, request.args, {
91
- cwd: request.cwd,
92
- env: environment,
93
- extendEnv: false,
94
- stdout: "pipe",
95
- stderr: "pipe"
96
- });
97
- const child = yield* spawner.spawn(command).pipe(Effect.mapError((error) => spawnError(request, error.message, error)));
98
- const streamOutput = (stream, decoder) => {
99
- return (stream === "stdout" ? child.stdout : child.stderr).pipe(Stream.mapError((error) => spawnError(request, error.message, error)), Stream.mapEffect((bytes) => outputEvent(counts, stream, bytes, decoder, request.limits.maxOutputBytes)), Stream.concat(flushDecoder(stream, decoder)));
100
- };
101
- const terminal = Stream.unwrap(Effect.gen(function* () {
102
- const exitCode = yield* child.exitCode.pipe(Effect.mapError((error) => SandboxExitError.make({
103
- cause: error,
104
- implementation: unisolatedImplementation,
105
- exitCode: -1,
106
- message: error.message
107
- })));
108
- const endedAt = yield* Clock.currentTimeMillis;
109
- const output = yield* Ref.get(counts);
110
- const resourceUse = SandboxResourceUse.make({
111
- wallTime: Duration.millis(endedAt - startedAt),
112
- stdoutBytes: output.stdout,
113
- stderrBytes: output.stderr
114
- });
115
- const exited = SandboxExited.make({
116
- eventVersion: 1,
117
- implementation: unisolatedImplementation,
118
- exitCode,
119
- resourceUse,
120
- artifacts: []
121
- });
122
- return exitCode === 0 ? Stream.succeed(exited) : Stream.concat(Stream.succeed(exited), Stream.fail(SandboxExitError.make({
123
- implementation: unisolatedImplementation,
124
- exitCode,
125
- message: `Unisolated local process exited with code ${exitCode}.`
126
- })));
127
- }));
128
- return Stream.concat(Stream.succeed(SandboxStarted.make({
129
- eventVersion: 1,
130
- implementation: unisolatedImplementation,
131
- runtime: request.runtime
132
- })), Stream.concat(Stream.merge(streamOutput("stdout", stdoutDecoder), streamOutput("stderr", stderrDecoder)), terminal)).pipe(Stream.interruptWhen(Effect.sleep(request.limits.maxWallTime).pipe(Effect.andThen(Effect.fail(SandboxTimeoutError.make({
133
- implementation: unisolatedImplementation,
134
- maxWallTime: request.limits.maxWallTime
135
- }))))));
136
- })).pipe(Stream.withSpan("LocalSandbox.execute"));
137
- /**
138
- * Development-only local process adapter with its `ChildProcessSpawner` requirement kept visible
139
- * so composition roots and tests can inject a spawner double. It is unisolated and must never be
140
- * used as a security boundary for untrusted code or commands.
141
- */
142
- const sandboxLayer = Layer.effect(Sandbox)(Effect.gen(function* () {
143
- const spawner = yield* ChildProcessSpawner;
144
- return Sandbox.of({ execute: makeExecute(spawner) });
145
- }));
146
- /** A scoped Node process implementation whose events are always labeled `unisolated`. */
147
- const layer = sandboxLayer.pipe(Layer.provide(NodeServices.layer));
148
- //#endregion
149
- export { layer, sandboxLayer, unisolatedImplementation };
150
-
151
- //# sourceMappingURL=index.mjs.map
1
+ import { t as LocalSandbox_exports } from "./LocalSandbox.mjs";
2
+ export { LocalSandbox_exports as LocalSandbox };
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
package/package.json CHANGED
@@ -1,41 +1 @@
1
- {
2
- "name": "@effect-agent/sandbox-local",
3
- "version": "0.1.0-beta.8",
4
- "exports": {
5
- ".": {
6
- "types": "./dist/index.d.mts",
7
- "default": "./dist/index.mjs"
8
- }
9
- },
10
- "dependencies": {
11
- "@effect-agent/sandbox": "0.1.0-beta.8",
12
- "@effect/platform-node": "4.0.0-beta.107",
13
- "effect": "4.0.0-beta.107"
14
- },
15
- "description": "Node-local child-process Sandbox adapter for Effect Agent; honestly labeled unisolated development tooling, not a security boundary.",
16
- "license": "MIT",
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/danieljvdm/effect-agent.git",
20
- "directory": "packages/sandbox-local"
21
- },
22
- "files": [
23
- "dist",
24
- "src"
25
- ],
26
- "type": "module",
27
- "publishConfig": {
28
- "access": "public"
29
- },
30
- "scripts": {
31
- "build": "vp pack",
32
- "check": "tsc --noEmit -p tsconfig.json",
33
- "test": "vp test --passWithNoTests"
34
- },
35
- "devDependencies": {
36
- "@effect/vitest": "4.0.0-beta.107",
37
- "@types/node": "26.1.2",
38
- "typescript": "7.0.2",
39
- "vite-plus": "0.2.6"
40
- }
41
- }
1
+ {"name":"@effect-agent/sandbox-local","version":"0.1.0-beta.80","dependencies":{"@effect-agent/sandbox":"0.1.0-beta.80","@effect/platform-node":"4.0.0-rc.112"},"devDependencies":{"@effect/vitest":"4.0.0-rc.112","@types/node":"26.1.2","effect":"4.0.0-rc.112","typescript":"7.0.2","vite-plus":"0.3.0"},"peerDependencies":{"effect":"^4.0.0-rc.112"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"./LocalSandbox":{"types":"./dist/LocalSandbox.d.mts","default":"./dist/LocalSandbox.mjs"}},"description":"Node-local child-process Sandbox adapter for Effect Agent; honestly labeled unisolated development tooling, not a security boundary.","license":"MIT","repository":{"type":"git","url":"git+https://github.com/danieljvdm/effect-agent.git","directory":"packages/sandbox-local"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
@@ -1,4 +1,5 @@
1
1
  import {
2
+ SANDBOX_DIAGNOSTIC_MAX_LENGTH,
2
3
  Sandbox,
3
4
  SandboxExited,
4
5
  SandboxExitError,
@@ -10,11 +11,10 @@ import {
10
11
  SandboxStarted,
11
12
  SandboxTimeoutError,
12
13
  SandboxUnsupportedRequestError,
13
- type SandboxError,
14
14
  type SandboxEvent,
15
15
  type SandboxExecute,
16
16
  type SandboxRequest,
17
- } from "@effect-agent/sandbox";
17
+ } from "@effect-agent/sandbox/Sandbox";
18
18
  import { NodeServices } from "@effect/platform-node";
19
19
  import { Clock, Config, Duration, Effect, Layer, Option, Ref, Stream } from "effect";
20
20
  import { ChildProcess } from "effect/unstable/process";
@@ -34,6 +34,9 @@ type OutputCounts = Readonly<Record<OutputStream, number>>;
34
34
 
35
35
  const zeroOutput: OutputCounts = { stdout: 0, stderr: 0 };
36
36
 
37
+ const boundedDiagnostic = (message: string): string =>
38
+ message.slice(0, SANDBOX_DIAGNOSTIC_MAX_LENGTH);
39
+
37
40
  const unsupported = (
38
41
  feature: Parameters<typeof SandboxUnsupportedRequestError.make>[0]["feature"],
39
42
  message: string,
@@ -41,68 +44,72 @@ const unsupported = (
41
44
  SandboxUnsupportedRequestError.make({
42
45
  implementation: unisolatedImplementation,
43
46
  feature,
44
- message,
47
+ message: boundedDiagnostic(message),
45
48
  });
46
49
 
47
- const validateRequest = (request: SandboxRequest): Effect.Effect<void, SandboxError> => {
48
- if (request.runtime.kind !== "unisolated-process") {
49
- return Effect.fail(
50
- unsupported(
51
- "runtime",
52
- "The local process runner only accepts runtime.kind 'unisolated-process'.",
53
- ),
50
+ const validateRequest = Effect.fn("LocalSandbox.validateRequest")(function* (
51
+ request: SandboxRequest,
52
+ ) {
53
+ if (
54
+ request.runtime.kind !== "unisolated-process" ||
55
+ request.runtime.identity !== unisolatedImplementation.identity
56
+ ) {
57
+ return yield* unsupported(
58
+ "runtime",
59
+ "The local process runner only accepts runtime kind 'unisolated-process' with identity 'local-process'.",
54
60
  );
55
61
  }
56
62
  if (request.mounts.length > 0) {
57
- return Effect.fail(
58
- unsupported(
59
- "mounts",
60
- "The unisolated local process runner cannot enforce mount access modes.",
61
- ),
63
+ return yield* unsupported(
64
+ "mounts",
65
+ "The unisolated local process runner cannot enforce mount access modes.",
62
66
  );
63
67
  }
64
68
  if (request.network._tag !== "NetworkDisabled") {
65
- return Effect.fail(
66
- unsupported(
67
- "network",
68
- "The unisolated local process runner cannot enforce workload network policy.",
69
- ),
69
+ return yield* unsupported(
70
+ "network",
71
+ "The unisolated local process runner cannot enforce workload network policy.",
70
72
  );
71
73
  }
72
74
  if (request.limits.cpuCores !== undefined) {
73
- return Effect.fail(
74
- unsupported("cpu-limit", "The unisolated local process runner cannot enforce CPU limits."),
75
+ return yield* unsupported(
76
+ "cpu-limit",
77
+ "The unisolated local process runner cannot enforce CPU limits.",
75
78
  );
76
79
  }
77
80
  if (request.limits.memoryBytes !== undefined) {
78
- return Effect.fail(
79
- unsupported(
80
- "memory-limit",
81
- "The unisolated local process runner cannot enforce memory limits.",
82
- ),
81
+ return yield* unsupported(
82
+ "memory-limit",
83
+ "The unisolated local process runner cannot enforce memory limits.",
83
84
  );
84
85
  }
85
86
  if (request.secretHandles.length > 0) {
86
- return Effect.fail(
87
- unsupported(
88
- "secret-handles",
89
- "The unisolated local process runner does not resolve secret handles into an environment.",
90
- ),
87
+ return yield* unsupported(
88
+ "secret-handles",
89
+ "The unisolated local process runner does not resolve secret handles into an environment.",
91
90
  );
92
91
  }
93
92
  if (request.artifactRules.length > 0) {
94
- return Effect.fail(
95
- unsupported("artifacts", "The unisolated local process runner does not collect artifacts."),
93
+ return yield* unsupported(
94
+ "artifacts",
95
+ "The unisolated local process runner does not collect artifacts.",
96
96
  );
97
97
  }
98
- return Effect.void;
99
- };
98
+ });
100
99
 
101
100
  const spawnError = (request: SandboxRequest, message: string, cause?: unknown) =>
102
101
  SandboxSpawnError.make({
103
102
  implementation: unisolatedImplementation,
104
103
  command: request.command,
105
- message,
104
+ message: boundedDiagnostic(message),
105
+ ...(cause === undefined ? {} : { cause }),
106
+ });
107
+
108
+ const exitError = (message: string, cause?: unknown, exitCode = -1) =>
109
+ SandboxExitError.make({
110
+ implementation: unisolatedImplementation,
111
+ exitCode,
112
+ message: boundedDiagnostic(message),
106
113
  ...(cause === undefined ? {} : { cause }),
107
114
  });
108
115
 
@@ -110,6 +117,7 @@ const environmentFromAllowlist = Effect.fn("LocalSandbox.environmentFromAllowlis
110
117
  request: SandboxRequest,
111
118
  ) {
112
119
  const environment: Record<string, string> = {};
120
+
113
121
  for (const name of request.environment.allow) {
114
122
  const value = yield* Config.option(Config.string(name)).pipe(
115
123
  Effect.mapError((error) =>
@@ -120,10 +128,12 @@ const environmentFromAllowlist = Effect.fn("LocalSandbox.environmentFromAllowlis
120
128
  ),
121
129
  ),
122
130
  );
131
+
123
132
  if (Option.isSome(value)) {
124
133
  environment[name] = value.value;
125
134
  }
126
135
  }
136
+
127
137
  return environment;
128
138
  });
129
139
 
@@ -137,6 +147,7 @@ const outputEvent = (
137
147
  Ref.modify(counts, (current) => {
138
148
  const streamBytes = current[stream] + bytes.byteLength;
139
149
  const next = { ...current, [stream]: streamBytes };
150
+
140
151
  return [next.stdout + next.stderr, next] as const;
141
152
  }).pipe(
142
153
  Effect.flatMap((observed) =>
@@ -170,6 +181,7 @@ const outputEvent = (
170
181
  const flushDecoder = (stream: OutputStream, decoder: TextDecoder): Stream.Stream<SandboxOutput> =>
171
182
  Stream.suspend(() => {
172
183
  const text = decoder.decode();
184
+
173
185
  return text.length === 0
174
186
  ? Stream.empty
175
187
  : Stream.succeed(
@@ -194,6 +206,7 @@ const makeExecute =
194
206
  const stdoutDecoder = new TextDecoder();
195
207
  const stderrDecoder = new TextDecoder();
196
208
  const environment = yield* environmentFromAllowlist(request);
209
+
197
210
  const command = ChildProcess.make(request.command, request.args, {
198
211
  cwd: request.cwd,
199
212
  env: environment,
@@ -201,14 +214,16 @@ const makeExecute =
201
214
  stdout: "pipe",
202
215
  stderr: "pipe",
203
216
  });
217
+
204
218
  const child = yield* spawner
205
219
  .spawn(command)
206
220
  .pipe(Effect.mapError((error) => spawnError(request, error.message, error)));
207
221
 
208
222
  const streamOutput = (stream: OutputStream, decoder: TextDecoder) => {
209
223
  const source = stream === "stdout" ? child.stdout : child.stderr;
224
+
210
225
  return source.pipe(
211
- Stream.mapError((error) => spawnError(request, error.message, error)),
226
+ Stream.mapError((error) => exitError(error.message, error)),
212
227
  Stream.mapEffect((bytes) =>
213
228
  outputEvent(counts, stream, bytes, decoder, request.limits.maxOutputBytes),
214
229
  ),
@@ -219,22 +234,18 @@ const makeExecute =
219
234
  const terminal = Stream.unwrap(
220
235
  Effect.gen(function* () {
221
236
  const exitCode = yield* child.exitCode.pipe(
222
- Effect.mapError((error) =>
223
- SandboxExitError.make({
224
- cause: error,
225
- implementation: unisolatedImplementation,
226
- exitCode: -1,
227
- message: error.message,
228
- }),
229
- ),
237
+ Effect.mapError((error) => exitError(error.message, error)),
230
238
  );
239
+
231
240
  const endedAt = yield* Clock.currentTimeMillis;
232
241
  const output = yield* Ref.get(counts);
242
+
233
243
  const resourceUse = SandboxResourceUse.make({
234
244
  wallTime: Duration.millis(endedAt - startedAt),
235
245
  stdoutBytes: output.stdout,
236
246
  stderrBytes: output.stderr,
237
247
  });
248
+
238
249
  const exited = SandboxExited.make({
239
250
  eventVersion: 1,
240
251
  implementation: unisolatedImplementation,
@@ -242,22 +253,23 @@ const makeExecute =
242
253
  resourceUse,
243
254
  artifacts: [],
244
255
  });
256
+
245
257
  return exitCode === 0
246
258
  ? Stream.succeed<SandboxEvent>(exited)
247
259
  : Stream.concat(
248
260
  Stream.succeed<SandboxEvent>(exited),
249
261
  Stream.fail(
250
- SandboxExitError.make({
251
- implementation: unisolatedImplementation,
262
+ exitError(
263
+ `Unisolated local process exited with code ${exitCode}.`,
264
+ undefined,
252
265
  exitCode,
253
- message: `Unisolated local process exited with code ${exitCode}.`,
254
- }),
266
+ ),
255
267
  ),
256
268
  );
257
269
  }),
258
270
  );
259
271
 
260
- const execution = Stream.concat(
272
+ return Stream.concat(
261
273
  Stream.succeed<SandboxEvent>(
262
274
  SandboxStarted.make({
263
275
  eventVersion: 1,
@@ -273,23 +285,20 @@ const makeExecute =
273
285
  terminal,
274
286
  ),
275
287
  );
276
-
277
- return execution.pipe(
278
- Stream.interruptWhen(
279
- Effect.sleep(request.limits.maxWallTime).pipe(
280
- Effect.andThen(
281
- Effect.fail(
282
- SandboxTimeoutError.make({
283
- implementation: unisolatedImplementation,
284
- maxWallTime: request.limits.maxWallTime,
285
- }),
286
- ),
287
- ),
288
- ),
289
- ),
290
- );
291
288
  }),
292
- ).pipe(Stream.withSpan("LocalSandbox.execute"));
289
+ ).pipe(
290
+ Stream.interruptWhen(
291
+ Effect.gen(function* () {
292
+ yield* Effect.sleep(request.limits.maxWallTime);
293
+
294
+ return yield* SandboxTimeoutError.make({
295
+ implementation: unisolatedImplementation,
296
+ maxWallTime: request.limits.maxWallTime,
297
+ });
298
+ }),
299
+ ),
300
+ Stream.withSpan("LocalSandbox.execute"),
301
+ );
293
302
 
294
303
  /**
295
304
  * Development-only local process adapter with its `ChildProcessSpawner` requirement kept visible
@@ -299,6 +308,7 @@ const makeExecute =
299
308
  export const sandboxLayer: Layer.Layer<Sandbox, never, ChildProcessSpawner> = Layer.effect(Sandbox)(
300
309
  Effect.gen(function* () {
301
310
  const spawner = yield* ChildProcessSpawner;
311
+
302
312
  return Sandbox.of({ execute: makeExecute(spawner) });
303
313
  }),
304
314
  );
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export * from "./local-sandbox.ts";
1
+ export * as LocalSandbox from "./LocalSandbox.ts";
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/local-sandbox.ts"],"sourcesContent":["import {\n Sandbox,\n SandboxExited,\n SandboxExitError,\n SandboxImplementation,\n SandboxOutput,\n SandboxOutputLimitError,\n SandboxResourceUse,\n SandboxSpawnError,\n SandboxStarted,\n SandboxTimeoutError,\n SandboxUnsupportedRequestError,\n type SandboxError,\n type SandboxEvent,\n type SandboxExecute,\n type SandboxRequest,\n} from \"@effect-agent/sandbox\";\nimport { NodeServices } from \"@effect/platform-node\";\nimport { Clock, Config, Duration, Effect, Layer, Option, Ref, Stream } from \"effect\";\nimport { ChildProcess } from \"effect/unstable/process\";\nimport { ChildProcessSpawner } from \"effect/unstable/process/ChildProcessSpawner\";\n\n/**\n * The only implementation identity produced by this package. It deliberately states that local\n * process execution is unisolated development tooling, not a security sandbox.\n */\nexport const unisolatedImplementation = SandboxImplementation.make({\n isolation: \"unisolated\",\n identity: \"local-process\",\n});\n\ntype OutputStream = \"stdout\" | \"stderr\";\ntype OutputCounts = Readonly<Record<OutputStream, number>>;\n\nconst zeroOutput: OutputCounts = { stdout: 0, stderr: 0 };\n\nconst unsupported = (\n feature: Parameters<typeof SandboxUnsupportedRequestError.make>[0][\"feature\"],\n message: string,\n) =>\n SandboxUnsupportedRequestError.make({\n implementation: unisolatedImplementation,\n feature,\n message,\n });\n\nconst validateRequest = (request: SandboxRequest): Effect.Effect<void, SandboxError> => {\n if (request.runtime.kind !== \"unisolated-process\") {\n return Effect.fail(\n unsupported(\n \"runtime\",\n \"The local process runner only accepts runtime.kind 'unisolated-process'.\",\n ),\n );\n }\n if (request.mounts.length > 0) {\n return Effect.fail(\n unsupported(\n \"mounts\",\n \"The unisolated local process runner cannot enforce mount access modes.\",\n ),\n );\n }\n if (request.network._tag !== \"NetworkDisabled\") {\n return Effect.fail(\n unsupported(\n \"network\",\n \"The unisolated local process runner cannot enforce workload network policy.\",\n ),\n );\n }\n if (request.limits.cpuCores !== undefined) {\n return Effect.fail(\n unsupported(\"cpu-limit\", \"The unisolated local process runner cannot enforce CPU limits.\"),\n );\n }\n if (request.limits.memoryBytes !== undefined) {\n return Effect.fail(\n unsupported(\n \"memory-limit\",\n \"The unisolated local process runner cannot enforce memory limits.\",\n ),\n );\n }\n if (request.secretHandles.length > 0) {\n return Effect.fail(\n unsupported(\n \"secret-handles\",\n \"The unisolated local process runner does not resolve secret handles into an environment.\",\n ),\n );\n }\n if (request.artifactRules.length > 0) {\n return Effect.fail(\n unsupported(\"artifacts\", \"The unisolated local process runner does not collect artifacts.\"),\n );\n }\n return Effect.void;\n};\n\nconst spawnError = (request: SandboxRequest, message: string, cause?: unknown) =>\n SandboxSpawnError.make({\n implementation: unisolatedImplementation,\n command: request.command,\n message,\n ...(cause === undefined ? {} : { cause }),\n });\n\nconst environmentFromAllowlist = Effect.fn(\"LocalSandbox.environmentFromAllowlist\")(function* (\n request: SandboxRequest,\n) {\n const environment: Record<string, string> = {};\n for (const name of request.environment.allow) {\n const value = yield* Config.option(Config.string(name)).pipe(\n Effect.mapError((error) =>\n spawnError(\n request,\n `Could not read allowed environment variable '${name}': ${error.message}`,\n error,\n ),\n ),\n );\n if (Option.isSome(value)) {\n environment[name] = value.value;\n }\n }\n return environment;\n});\n\nconst outputEvent = (\n counts: Ref.Ref<OutputCounts>,\n stream: OutputStream,\n bytes: Uint8Array,\n decoder: TextDecoder,\n limit: number,\n): Effect.Effect<SandboxOutput, SandboxOutputLimitError> =>\n Ref.modify(counts, (current) => {\n const streamBytes = current[stream] + bytes.byteLength;\n const next = { ...current, [stream]: streamBytes };\n return [next.stdout + next.stderr, next] as const;\n }).pipe(\n Effect.flatMap((observed) =>\n observed > limit\n ? Effect.fail(\n SandboxOutputLimitError.make({\n implementation: unisolatedImplementation,\n stream,\n limit,\n observed,\n }),\n )\n : Effect.succeed(\n SandboxOutput.make({\n eventVersion: 1,\n implementation: unisolatedImplementation,\n stream,\n text: decoder.decode(bytes, { stream: true }),\n bytes: bytes.byteLength,\n }),\n ),\n ),\n );\n\n/**\n * Emits any text the streaming decoder still holds once its source stream ends. The final decode\n * must run at end-of-stream, not at pipeline construction, so the flush is suspended; a trailing\n * incomplete UTF-8 sequence surfaces as replacement text. Its raw bytes were already counted when\n * the chunk arrived, so the flush event carries zero bytes.\n */\nconst flushDecoder = (stream: OutputStream, decoder: TextDecoder): Stream.Stream<SandboxOutput> =>\n Stream.suspend(() => {\n const text = decoder.decode();\n return text.length === 0\n ? Stream.empty\n : Stream.succeed(\n SandboxOutput.make({\n eventVersion: 1,\n implementation: unisolatedImplementation,\n stream,\n text,\n bytes: 0,\n }),\n );\n });\n\nconst makeExecute =\n (spawner: ChildProcessSpawner[\"Service\"]): SandboxExecute =>\n (request) =>\n Stream.unwrap(\n Effect.gen(function* () {\n yield* validateRequest(request);\n const startedAt = yield* Clock.currentTimeMillis;\n const counts = yield* Ref.make(zeroOutput);\n const stdoutDecoder = new TextDecoder();\n const stderrDecoder = new TextDecoder();\n const environment = yield* environmentFromAllowlist(request);\n const command = ChildProcess.make(request.command, request.args, {\n cwd: request.cwd,\n env: environment,\n extendEnv: false,\n stdout: \"pipe\",\n stderr: \"pipe\",\n });\n const child = yield* spawner\n .spawn(command)\n .pipe(Effect.mapError((error) => spawnError(request, error.message, error)));\n\n const streamOutput = (stream: OutputStream, decoder: TextDecoder) => {\n const source = stream === \"stdout\" ? child.stdout : child.stderr;\n return source.pipe(\n Stream.mapError((error) => spawnError(request, error.message, error)),\n Stream.mapEffect((bytes) =>\n outputEvent(counts, stream, bytes, decoder, request.limits.maxOutputBytes),\n ),\n Stream.concat(flushDecoder(stream, decoder)),\n );\n };\n\n const terminal = Stream.unwrap(\n Effect.gen(function* () {\n const exitCode = yield* child.exitCode.pipe(\n Effect.mapError((error) =>\n SandboxExitError.make({\n cause: error,\n implementation: unisolatedImplementation,\n exitCode: -1,\n message: error.message,\n }),\n ),\n );\n const endedAt = yield* Clock.currentTimeMillis;\n const output = yield* Ref.get(counts);\n const resourceUse = SandboxResourceUse.make({\n wallTime: Duration.millis(endedAt - startedAt),\n stdoutBytes: output.stdout,\n stderrBytes: output.stderr,\n });\n const exited = SandboxExited.make({\n eventVersion: 1,\n implementation: unisolatedImplementation,\n exitCode,\n resourceUse,\n artifacts: [],\n });\n return exitCode === 0\n ? Stream.succeed<SandboxEvent>(exited)\n : Stream.concat(\n Stream.succeed<SandboxEvent>(exited),\n Stream.fail(\n SandboxExitError.make({\n implementation: unisolatedImplementation,\n exitCode,\n message: `Unisolated local process exited with code ${exitCode}.`,\n }),\n ),\n );\n }),\n );\n\n const execution = Stream.concat(\n Stream.succeed<SandboxEvent>(\n SandboxStarted.make({\n eventVersion: 1,\n implementation: unisolatedImplementation,\n runtime: request.runtime,\n }),\n ),\n Stream.concat(\n Stream.merge(\n streamOutput(\"stdout\", stdoutDecoder),\n streamOutput(\"stderr\", stderrDecoder),\n ),\n terminal,\n ),\n );\n\n return execution.pipe(\n Stream.interruptWhen(\n Effect.sleep(request.limits.maxWallTime).pipe(\n Effect.andThen(\n Effect.fail(\n SandboxTimeoutError.make({\n implementation: unisolatedImplementation,\n maxWallTime: request.limits.maxWallTime,\n }),\n ),\n ),\n ),\n ),\n );\n }),\n ).pipe(Stream.withSpan(\"LocalSandbox.execute\"));\n\n/**\n * Development-only local process adapter with its `ChildProcessSpawner` requirement kept visible\n * so composition roots and tests can inject a spawner double. It is unisolated and must never be\n * used as a security boundary for untrusted code or commands.\n */\nexport const sandboxLayer: Layer.Layer<Sandbox, never, ChildProcessSpawner> = Layer.effect(Sandbox)(\n Effect.gen(function* () {\n const spawner = yield* ChildProcessSpawner;\n return Sandbox.of({ execute: makeExecute(spawner) });\n }),\n);\n\n/** A scoped Node process implementation whose events are always labeled `unisolated`. */\nexport const layer: Layer.Layer<Sandbox> = sandboxLayer.pipe(Layer.provide(NodeServices.layer));\n"],"mappings":"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"}