@effect-agent/sandbox-local 0.1.0-beta.24 → 0.1.0-beta.25

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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Sandbox, SandboxExitError, SandboxExited, SandboxImplementation, SandboxOutput, SandboxOutputLimitError, SandboxResourceUse, SandboxSpawnError, SandboxStarted, SandboxTimeoutError, SandboxUnsupportedRequestError } from "@effect-agent/sandbox";
1
+ import { SANDBOX_DIAGNOSTIC_MAX_LENGTH, Sandbox, SandboxExitError, SandboxExited, SandboxImplementation, SandboxOutput, SandboxOutputLimitError, SandboxResourceUse, SandboxSpawnError, SandboxStarted, SandboxTimeoutError, SandboxUnsupportedRequestError } from "@effect-agent/sandbox";
2
2
  import { NodeServices } from "@effect/platform-node";
3
3
  import { Clock, Config, Duration, Effect, Layer, Option, Ref, Stream } from "effect";
4
4
  import { ChildProcess } from "effect/unstable/process";
@@ -16,25 +16,31 @@ const zeroOutput = {
16
16
  stdout: 0,
17
17
  stderr: 0
18
18
  };
19
+ const boundedDiagnostic = (message) => message.slice(0, SANDBOX_DIAGNOSTIC_MAX_LENGTH);
19
20
  const unsupported = (feature, message) => SandboxUnsupportedRequestError.make({
20
21
  implementation: unisolatedImplementation,
21
22
  feature,
22
- message
23
+ message: boundedDiagnostic(message)
24
+ });
25
+ const validateRequest = Effect.fn("LocalSandbox.validateRequest")(function* (request) {
26
+ 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'.");
27
+ if (request.mounts.length > 0) return yield* unsupported("mounts", "The unisolated local process runner cannot enforce mount access modes.");
28
+ if (request.network._tag !== "NetworkDisabled") return yield* unsupported("network", "The unisolated local process runner cannot enforce workload network policy.");
29
+ if (request.limits.cpuCores !== void 0) return yield* unsupported("cpu-limit", "The unisolated local process runner cannot enforce CPU limits.");
30
+ if (request.limits.memoryBytes !== void 0) return yield* unsupported("memory-limit", "The unisolated local process runner cannot enforce memory limits.");
31
+ if (request.secretHandles.length > 0) return yield* unsupported("secret-handles", "The unisolated local process runner does not resolve secret handles into an environment.");
32
+ if (request.artifactRules.length > 0) return yield* unsupported("artifacts", "The unisolated local process runner does not collect artifacts.");
23
33
  });
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
34
  const spawnError = (request, message, cause) => SandboxSpawnError.make({
35
35
  implementation: unisolatedImplementation,
36
36
  command: request.command,
37
- message,
37
+ message: boundedDiagnostic(message),
38
+ ...cause === void 0 ? {} : { cause }
39
+ });
40
+ const exitError = (message, cause, exitCode = -1) => SandboxExitError.make({
41
+ implementation: unisolatedImplementation,
42
+ exitCode,
43
+ message: boundedDiagnostic(message),
38
44
  ...cause === void 0 ? {} : { cause }
39
45
  });
40
46
  const environmentFromAllowlist = Effect.fn("LocalSandbox.environmentFromAllowlist")(function* (request) {
@@ -96,15 +102,10 @@ const makeExecute = (spawner) => (request) => Stream.unwrap(Effect.gen(function*
96
102
  });
97
103
  const child = yield* spawner.spawn(command).pipe(Effect.mapError((error) => spawnError(request, error.message, error)));
98
104
  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)));
105
+ 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)));
100
106
  };
101
107
  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 exitCode = yield* child.exitCode.pipe(Effect.mapError((error) => exitError(error.message, error)));
108
109
  const endedAt = yield* Clock.currentTimeMillis;
109
110
  const output = yield* Ref.get(counts);
110
111
  const resourceUse = SandboxResourceUse.make({
@@ -119,11 +120,7 @@ const makeExecute = (spawner) => (request) => Stream.unwrap(Effect.gen(function*
119
120
  resourceUse,
120
121
  artifacts: []
121
122
  });
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
- })));
123
+ return exitCode === 0 ? Stream.succeed(exited) : Stream.concat(Stream.succeed(exited), Stream.fail(exitError(`Unisolated local process exited with code ${exitCode}.`, void 0, exitCode)));
127
124
  }));
128
125
  return Stream.concat(Stream.succeed(SandboxStarted.make({
129
126
  eventVersion: 1,
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/local-sandbox.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\";\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 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) => 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 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 exitError(\n `Unisolated local process exited with code ${exitCode}.`,\n undefined,\n 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-agent/sandbox-local",
3
- "version": "0.1.0-beta.24",
3
+ "version": "0.1.0-beta.25",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/index.d.mts",
@@ -8,7 +8,7 @@
8
8
  }
9
9
  },
10
10
  "dependencies": {
11
- "@effect-agent/sandbox": "0.1.0-beta.24",
11
+ "@effect-agent/sandbox": "0.1.0-beta.25",
12
12
  "@effect/platform-node": "4.0.0-rc.110",
13
13
  "effect": "4.0.0-rc.110"
14
14
  },
@@ -1,4 +1,5 @@
1
1
  import {
2
+ SANDBOX_DIAGNOSTIC_MAX_LENGTH,
2
3
  Sandbox,
3
4
  SandboxExited,
4
5
  SandboxExitError,
@@ -10,7 +11,6 @@ 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,
@@ -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
 
@@ -208,7 +215,7 @@ const makeExecute =
208
215
  const streamOutput = (stream: OutputStream, decoder: TextDecoder) => {
209
216
  const source = stream === "stdout" ? child.stdout : child.stderr;
210
217
  return source.pipe(
211
- Stream.mapError((error) => spawnError(request, error.message, error)),
218
+ Stream.mapError((error) => exitError(error.message, error)),
212
219
  Stream.mapEffect((bytes) =>
213
220
  outputEvent(counts, stream, bytes, decoder, request.limits.maxOutputBytes),
214
221
  ),
@@ -219,14 +226,7 @@ const makeExecute =
219
226
  const terminal = Stream.unwrap(
220
227
  Effect.gen(function* () {
221
228
  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
- ),
229
+ Effect.mapError((error) => exitError(error.message, error)),
230
230
  );
231
231
  const endedAt = yield* Clock.currentTimeMillis;
232
232
  const output = yield* Ref.get(counts);
@@ -247,11 +247,11 @@ const makeExecute =
247
247
  : Stream.concat(
248
248
  Stream.succeed<SandboxEvent>(exited),
249
249
  Stream.fail(
250
- SandboxExitError.make({
251
- implementation: unisolatedImplementation,
250
+ exitError(
251
+ `Unisolated local process exited with code ${exitCode}.`,
252
+ undefined,
252
253
  exitCode,
253
- message: `Unisolated local process exited with code ${exitCode}.`,
254
- }),
254
+ ),
255
255
  ),
256
256
  );
257
257
  }),