@codeworksh/harness 0.0.1-canary.10.1.g65d5c167

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sanchit Kudari <0xsanchit@gmail.com> & Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,333 @@
1
+ # @codeworksh/harness
2
+
3
+ > **Beta:** This package is an early release. Expect breaking changes.
4
+
5
+ `@codeworksh/harness` is an agent harness built with [Effect v4 beta](https://github.com/Effect-TS/effect) and powered by [`@codeworksh/aikit`](https://www.npmjs.com/package/@codeworksh/aikit).
6
+ It leverages Effect's typed services, layers, scopes, streams, and structured errors to agent loops, durable sessions, tool execution, and local or remote sandboxes.
7
+
8
+ The initial public surface is the Effect SDK at `@codeworksh/harness/effect`.
9
+
10
+ ## Features
11
+
12
+ - **Durable Sessions:** create, attach, resume, interrupt, and inspect agent sessions without rebuilding orchestration around every turn.
13
+ - **Streaming Agent Loop:** consume durable session events while the loop coordinates model output, tool calls, and continuations.
14
+ - **Tool Execution:** use the built-in Bash tool or register typed tools with sequential or parallel execution.
15
+ - **Model Flexibility:** select any provider and model available in Aikit's generated catalog, including its supported thinking levels.
16
+ - **Pluggable Sandboxes:** run the same workflow against the host machine, a virtual filesystem, or a remote sandbox.
17
+
18
+ ## Plugins
19
+
20
+ Third-party plugins build against [`@codeworksh/plugin`](../plugin/README.md), the published SDK
21
+ that owns the plugin, tool and sandbox contract this package consumes — a plugin package depends
22
+ on it rather than on the whole harness. The same types are re-exported here as `Plugin` and `Tool`
23
+ for embedders who already hold the harness.
24
+
25
+ Pass a plugin list when constructing the harness. Each plugin declares an ID and the domain it extends — `tool` or `prompt` — and contributes during setup. Setup runs once per exchange, after model resolution; its tools, hooks, and prompt remain pinned through tool continuations.
26
+
27
+ ```ts
28
+ import { Effect, Schema } from "effect";
29
+ import { Harness, Plugin, Tool } from "@codeworksh/harness/effect";
30
+
31
+ const echo = Plugin.define({
32
+ id: "acme.tool.echo",
33
+ kind: "tool",
34
+ setup(ctx) {
35
+ ctx.plugin.tools.add(
36
+ Tool.register(
37
+ Tool.make({
38
+ name: "echo",
39
+ description: "Echo a message",
40
+ parameters: Schema.Struct({ text: Schema.String }),
41
+ success: Schema.String,
42
+ handler: ({ text }) => Effect.succeed(text),
43
+ }),
44
+ ),
45
+ {
46
+ beforeToolCall(call) {
47
+ // Arguments have already been decoded. Return { block: true, reason: "..." }
48
+ // to skip this handler and its after hook.
49
+ },
50
+ afterToolCall({ terminal }) {
51
+ // Completed/error results can be patched through content, details, isError.
52
+ // Aborted results are observation-only; keep cancellation cleanup short.
53
+ },
54
+ },
55
+ );
56
+ },
57
+ });
58
+
59
+ // A prompt plugin renders the system prompt, and indexes every tool registered before it.
60
+ const prompt = Plugin.define({
61
+ id: "acme.prompt.main",
62
+ kind: "prompt",
63
+ setup(ctx) {
64
+ ctx.plugin.prompt.set(
65
+ `You have: ${ctx.plugin.tools
66
+ .list()
67
+ .map((tool) => tool.name)
68
+ .join(", ")}`,
69
+ );
70
+ },
71
+ });
72
+
73
+ // Omit `plugins` for the built-ins plus whatever settings add; passing it owns the whole
74
+ // selection, which is why this one supplies its own prompt plugin.
75
+ const runtime = Harness.layer({ plugins: [echo, prompt] });
76
+ ```
77
+
78
+ Hooks belong to the tool registration. Sequential or parallel scheduling, selected with `Session.create({ tools: { execution: "parallel" } })`, covers the entire hook/handler pipeline. After runs for a started, interrupted tool if it has not already started, with a one-second cooperative cleanup grace period. The kernel owns result settlement.
79
+
80
+ `ctx.plugin.tools.update(name, patch)` rewrites a registration's model-facing prose without replacing the tool or its hooks. A read sees only earlier contributions, so a plugin patching `promptSnippet` or `promptGuidelines` must run _before_ the tool it patches is indexed. Declaring `kind: "tool"` puts it ahead of every prompt plugin already; what it still has to get right is its position among the other tool plugins, which is the order their entries are written in.
81
+
82
+ Prompt plugins use `ctx.plugin.prompt.get()` and `set(string)`. Each `set` replaces the entire prompt, including with an empty string. Place a prompt plugin after the tools or prompt contributors it needs. Contributions close after setup; plugins receive event publication but no subscription or background lifecycle.
83
+
84
+ Omitting `plugins` selects Bash then the default prompt, followed by the host settings' `plugins` block. An explicit array replaces all of that, and an empty one runs nothing — which is not a usable harness: `freeze` requires a system prompt, so a selection without a prompt plugin fails every exchange with `SnapshotError("no prompt plugin set a system prompt")`. Every working selection ends with a prompt plugin, whether `codework.prompt.default` or your own.
85
+
86
+ An entry is a **module** or a **configuration object**.
87
+
88
+ A module is a definition object, a local path, a `file:` URL, or a package spec. It is loaded if it is not already, and it takes the position it is written at — naming a module again moves it:
89
+
90
+ ```ts
91
+ plugins: [echo, "./plugins/local.ts", "file:///opt/plugin.mjs", "@acme/codework-tool-proc@1.2.0"];
92
+ ```
93
+
94
+ A configuration object addresses a plugin **something else already selected**, and names it one of two ways:
95
+
96
+ ```jsonc
97
+ { "plugin": "acme.tool.proc", "options": { "limit": 40 } } // by ID, when you know it
98
+ { "package": "@acme/codework-tool-proc", "options": { "limit": 40 } } // by the package it came from
99
+ { "plugin": "codework.tool.bash", "enabled": false } // drop a built-in
100
+ ```
101
+
102
+ Exactly one of `plugin` and `package` per entry. `plugin` is a plugin's ID; `package` is any name the module answers to — the package name, the exact spec, the path it was loaded from, or the name a local package's own manifest declares.
103
+
104
+ An **ID is a key**. The last module to claim one owns it, and the configuration written against it stays with the key rather than with whichever module is currently behind it. Two modules exporting one ID is the author's conflict to resolve — the replacement is logged at debug, not arbitrated here.
105
+
106
+ A configuration object **never loads, installs or reorders anything**. If its name matches nothing in the selection — a typo, a package you have not added, a plugin this build does not ship — the entry is **ignored**: one debug line, no failure, and nothing fetched. So adding a package and configuring it is two entries, in that order:
107
+
108
+ ```jsonc
109
+ { "plugins": ["@acme/codework-tool-proc@1.2.0", { "package": "@acme/codework-tool-proc", "options": { "limit": 40 } }] }
110
+ ```
111
+
112
+ Repeated configuration replaces rather than merges — the block is opaque, so the last entry owns it whole. `options` reaches that plugin as the second argument to `setup`, `{}` when nothing configured it. The harness never looks inside it, including the `null`s it strips everywhere else in a settings file, since inside an opaque block a `null` is a value its plugin may need:
113
+
114
+ ```ts
115
+ Plugin.define({
116
+ id: "acme.tool.proc",
117
+ setup(ctx, options) {
118
+ const limit = typeof options.limit === "number" ? options.limit : 10;
119
+ },
120
+ });
121
+ ```
122
+
123
+ Source modules must default-export one plugin object.
124
+
125
+ Settings entries take the same two forms, and they extend the built-in selection instead of standing in for it, so naming one plugin cannot silently drop Bash or the prompt:
126
+
127
+ ```jsonc
128
+ // <project>/.codework/settings.jsonc, ~/.codework/settings.jsonc, or a --user-config-dir
129
+ {
130
+ "plugins": [
131
+ "@acme/codework-prompt-life",
132
+ "@acme/codework-tool-proc@1.2.0",
133
+ "./plugins/local.ts",
134
+ { "package": "@acme/codework-tool-proc", "options": { "limit": 20 } },
135
+ { "plugin": "codework.tool.bash", "enabled": false },
136
+ ],
137
+ }
138
+ ```
139
+
140
+ Settings files are JSONC: comments and a trailing comma are part of the format, and a syntax error names what the parser expected and where (`PropertyNameExpected at 2:38`). Entries accumulate across settings layers, lowest priority first: a project's list extends the user's rather than standing in for it, the way every other key in the document merges. A project drops an inherited plugin the same way it drops a built-in, with `{ "plugin": "<id>", "enabled": false }`. A leading `~` expands to the home directory. A `./` or `../` path resolves against the directory of the file that declared it — inside `<project>/.codework/`, or beside `~/.codework/settings.jsonc` — so one entry means one file in every project; a `package` naming a relative path is anchored the same way. `file:` URLs, absolute paths and package specs are taken as written.
141
+
142
+ Setup order comes from the **domain a plugin declares**, not from where its entry sits. Every `kind: "tool"` plugin is set up before any `kind: "prompt"` plugin, so a prompt plugin always sees the complete tool set — including tools added from a settings file, which land after the built-ins in the array. Within one domain, entries keep the order they were written in, which is where composition actually happens: a plugin patching another's tool, or appending to the prompt a previous one rendered, is written after it on purpose.
143
+
144
+ That is the reason `kind` is part of the definition rather than something the harness guesses. A user's settings file and a project's are edited by different people at different times, and neither can see the other's ordering; what each plugin _is_ remains knowable in both.
145
+
146
+ A plugin package declares `@codeworksh/harness` and `effect` as **exact peer dependencies**, never as dependencies:
147
+
148
+ ```jsonc
149
+ "peerDependencies": { "@codeworksh/harness": "0.0.1", "effect": "4.0.0-rc.115" },
150
+ "devDependencies": { "effect": "4.0.0-rc.115" }
151
+ ```
152
+
153
+ A plugin is installed into its own directory, so its Effect is a separate module instance from the harness's. Two instances of the _same_ version interoperate completely — service tags resolve by their string id, and schemas, generators and handlers all cross the boundary. Two different _versions_ do not: a tool's schema then encodes a result the harness cannot commit. Declaring the peer moves that from a runtime failure to a line during `npm install`, and `test/plugin.foreign.test.ts` holds the interop itself in place.
154
+
155
+ Package sources install with pnpm, with lifecycle scripts disabled, under the harness home cache. The installer inherits stderr, so a first install prints pnpm's own progress and errors to the terminal — and a `plugins` entry in a settings file means that can happen during `Harness.layer` construction, before any session exists. An omitted version means `latest` on the first installation; subsequent constructions reuse that completed installation. The selection is read once per `Harness.layer`, so an edited `plugins` block applies at the next construction; hot reload and daemon lifecycles are not implemented.
156
+
157
+ Failures are attributed: a bad reference, unreadable module, or malformed plugin fails `Harness.layer` construction with `PluginPreparationError`, which carries the failing phase (`source`, `install`, `import`, or `definition`) and the index of the offending reference. A failing package install reports `PluginInstallError`; a plugin's `setup` failure becomes `Plugin.SetupError` with the plugin id, surfacing as a `SnapshotError` for that exchange. Plugins are trusted in-process code — local paths and `file:` references import whatever they point at, so only load sources you trust.
158
+
159
+ ## Pluggable Sandboxes
160
+
161
+ Harness uses a driver-based sandbox architecture. Drivers share a common lifecycle and I/O surface, keeping provider details out of session and agent-loop code.
162
+
163
+ | Backend | Environment | Good for |
164
+ | -------------- | ------------------------------------------- | ----------------------------------------------------- |
165
+ | Local host | Real filesystem and processes | Working directly in the current machine or repository |
166
+ | In-memory VFS | Ephemeral virtual filesystem | Fast tests and isolated experiments |
167
+ | SQLite VFS | In-memory or file-backed virtual filesystem | Reproducible sandboxes with optional persistence |
168
+ | Vercel Sandbox | Remote sandbox | Isolated cloud execution on Vercel |
169
+ | Daytona | Remote sandbox | Managed development environments on Daytona |
170
+
171
+ The SDK exposes sandbox creation, registration, discovery, refresh, wake, stop, and destroy operations. Built-in drivers can be selected when constructing the Harness layer:
172
+
173
+ ```ts
174
+ import { Effect } from "effect";
175
+ import { Harness, Sandbox, Session } from "@codeworksh/harness/effect";
176
+
177
+ const program = Effect.gen(function* () {
178
+ const sandbox = yield* Sandbox.create({ driver: "memory", config: { defaultCwd: "/workspace" } });
179
+ const session = yield* Session.create({ sandbox });
180
+ const info = yield* session.info;
181
+ console.log(`${sandbox.driver}:${info.directory}`);
182
+ });
183
+
184
+ await program.pipe(
185
+ Effect.provide(
186
+ Harness.layer({
187
+ database: ":memory:",
188
+ home: ".codework",
189
+ }),
190
+ ),
191
+ Effect.scoped,
192
+ Effect.runPromise,
193
+ );
194
+ ```
195
+
196
+ `local` always exists, while `memory` and `sqldb` are registered automatically. Install third-party drivers with pnpm and load their package specifiers when constructing the layer:
197
+
198
+ ```sh
199
+ pnpm install @acme/codework-sandbox-e2b
200
+ ```
201
+
202
+ ```ts
203
+ Harness.layer({
204
+ sandboxes: ["@acme/codework-sandbox-e2b"],
205
+ });
206
+ ```
207
+
208
+ Vercel and Daytona are the first remote drivers. More providers can be added behind the same lifecycle and I/O contracts without changing session or agent-loop code.
209
+
210
+ ## Requirements
211
+
212
+ - Node.js 24.14.1 or newer
213
+ - An API key for the model provider you select, or OAuth credentials created with
214
+ `codework auth login --openai-codex` / `codework auth login --github-copilot`
215
+ - Network access to models.dev the first time: the model catalog is downloaded into `<home>/models.gen.json` on demand
216
+
217
+ ## CLI
218
+
219
+ Run the current development release without installing it globally:
220
+
221
+ ```sh
222
+ export OPENAI_API_KEY="..."
223
+
224
+ pnpm dlx @codeworksh/harness@dev \
225
+ --home .codework-beta \
226
+ run --cwd "$PWD" --provider openai --model gpt-5.5 --thinking high \
227
+ "Inspect this repository"
228
+ ```
229
+
230
+ The streamed response remains clean on stdout for piping. Session context and the per-run model usage summary are written to stderr:
231
+
232
+ ```text
233
+ session ses_...
234
+ sandbox local · /workspace/project
235
+
236
+ ── response ─────────────────────────────────────────────────────────────
237
+ The response streams here.
238
+ ── usage ────────────────────────────────────────────────────────────────
239
+ model openai/gpt-5.5
240
+ tokens 12,400 input · 820 output · 13,220 total
241
+ cache 9,600 read · 0 write
242
+ cost $0.014200 · 1 turn
243
+ ```
244
+
245
+ The model catalog lives at `<home>/models.gen.json` and is refreshed once it is older than 15 minutes; `codework models refresh --force` downloads it now. Set `CODEWORK_MODELS_FILE` to pin a catalog you manage yourself.
246
+
247
+ The CLI prints the session ID to stderr. Provider, model, and thinking settings are stored with the session, so use the same home directory and session ID to continue it:
248
+
249
+ ```sh
250
+ pnpm dlx @codeworksh/harness@dev \
251
+ --home .codework-beta \
252
+ run --session <session-id> \
253
+ "Continue with the implementation"
254
+ ```
255
+
256
+ Persisted settings are the fallback. A new CLI process can explicitly re-bind or change them while attaching; supplied values are recorded as the session's latest configuration:
257
+
258
+ ```sh
259
+ pnpm dlx @codeworksh/harness@dev \
260
+ --home .codework-beta \
261
+ run --session <session-id> \
262
+ --provider openai --model gpt-5.6-luna --thinking high \
263
+ "Continue with this model"
264
+ ```
265
+
266
+ Local execution is the default. To create a session in a new Daytona sandbox, set `DAYTONA_API_KEY` and select the Daytona driver:
267
+
268
+ ```sh
269
+ export DAYTONA_API_KEY="..."
270
+
271
+ pnpm dlx @codeworksh/harness@dev \
272
+ --home .codework-beta \
273
+ run --sandbox-driver daytona --provider openai --model gpt-5.5 \
274
+ "Inspect this repository"
275
+ ```
276
+
277
+ Pass the provider's sandbox ID to connect a new Harness session to an existing Daytona sandbox:
278
+
279
+ ```sh
280
+ pnpm dlx @codeworksh/harness@dev \
281
+ --home .codework-beta \
282
+ run --sandbox-driver daytona --sandbox-provider-id <daytona-sandbox-id> \
283
+ "Continue work in this sandbox"
284
+ ```
285
+
286
+ `--cwd` overrides the selected sandbox's default working directory. When continuing with `--session`, omit the sandbox flags: the durable session already references its sandbox.
287
+
288
+ The same flags work with Vercel Sandbox by using `--sandbox-driver vercel`; `--sandbox-provider-id` then accepts the existing Vercel sandbox name.
289
+
290
+ Use `codework --help` or `pnpm dlx @codeworksh/harness@dev --help` for all options.
291
+
292
+ ## Effect SDK
293
+
294
+ ```sh
295
+ pnpm add @codeworksh/harness@dev effect
296
+ ```
297
+
298
+ ```ts
299
+ import { Effect } from "effect";
300
+ import { Harness, Session } from "@codeworksh/harness/effect";
301
+
302
+ const program = Effect.gen(function* () {
303
+ const session = yield* Session.create({
304
+ title: "Review Harness",
305
+ directory: process.cwd(),
306
+ model: { provider: "openai", id: "gpt-5.5" },
307
+ thinkingLevel: "high",
308
+ });
309
+ const info = yield* session.info;
310
+
311
+ console.log(`session ${info.id} in ${info.directory}`);
312
+ });
313
+
314
+ await program.pipe(
315
+ Effect.provide(Harness.layer({ database: ":memory:", home: ".codework-readme" })),
316
+ Effect.scoped,
317
+ Effect.runPromise,
318
+ );
319
+ ```
320
+
321
+ Creating a session does not contact the provider. Call `session.run(prompt)` to execute a turn, and call `session.events()` directly to obtain its Effect `Stream`.
322
+
323
+ The Effect SDK currently includes:
324
+
325
+ - `Harness.layer` for process configuration and service wiring
326
+ - `Session` handles for create, attach, prompt, run, resume, interrupt, events, and history
327
+ - `Sandbox` drivers and lifecycle operations for in-memory, SQLite, Vercel, and Daytona environments
328
+ - Local host execution for sessions without a configured sandbox
329
+ - Effect-native errors, layers, streams, and resource scopes
330
+
331
+ ## Status
332
+
333
+ This release is intended for quick iteration and feedback. It is not yet a stable production API. Please report issues through the [Codework repository](https://github.com/codeworksh/codework/issues).
@@ -0,0 +1,313 @@
1
+ import { a as io_exports, n as shell_exports, o as instance_exports, r as filesystem_exports, s as posix, t as resource_exports } from "./resource-DFZh0e-s.mjs";
2
+ import { SandboxDriver, SandboxProvider } from "./sandbox.mjs";
3
+ import { Context, DateTime, Effect, Layer, Option, Schema } from "effect";
4
+ import { Buffer } from "node:buffer";
5
+ //#region src/sandboxes/daytona/fs.ts
6
+ const resolvePath = (path, options) => {
7
+ const normalized = posix.normalize(path);
8
+ if (options?.cwd === void 0 || posix.isAbsolute(normalized)) return normalized;
9
+ return posix.normalize(posix.join(options.cwd, normalized));
10
+ };
11
+ /** Resolve provider paths without adding policy or mutable working-directory state. */
12
+ const make$1 = (provider, options) => {
13
+ const resolve = (path) => resolvePath(path, options);
14
+ return {
15
+ readFile: (path) => provider.readFile(resolve(path)),
16
+ readFileBuffer: (path) => provider.readFileBuffer(resolve(path)),
17
+ writeFile: (path, content) => provider.writeFile(resolve(path), content),
18
+ stat: (path) => provider.stat(resolve(path)),
19
+ ...provider.lstat === void 0 ? {} : { lstat: (path) => provider.lstat(resolve(path)) },
20
+ readdir: (path) => provider.readdir(resolve(path)),
21
+ exists: (path) => provider.exists(resolve(path)),
22
+ mkdir: (path, mkdirOptions) => provider.mkdir(resolve(path), mkdirOptions),
23
+ rm: (path, rmOptions) => provider.rm(resolve(path), rmOptions),
24
+ realpath: (path) => provider.realpath(resolve(path))
25
+ };
26
+ };
27
+ //#endregion
28
+ //#region src/sandboxes/daytona/provider.ts
29
+ /** Fallback when Daytona cannot report a snapshot/image-specific work directory. */
30
+ const DEFAULT_CWD = "/home/daytona";
31
+ /**
32
+ * The mount cwd for a Daytona namespace.
33
+ *
34
+ * An absolute override replaces the namespace default outright, so the
35
+ * `getWorkDir()` round-trip is skipped: it would discover a value we then throw
36
+ * away. Otherwise the sandbox's own work directory is the default, and
37
+ * {@link DEFAULT_CWD} covers a snapshot or image that reports none.
38
+ *
39
+ * Taken as a thunk rather than read off the sandbox so all three branches are
40
+ * testable without provisioning one — this decides where every Daytona mount
41
+ * roots, and §8.1 makes a wrong answer here resolve silently rather than fail.
42
+ */
43
+ const mountCwd = async (cwd, getWorkDir) => {
44
+ const defaultCwd = posix.isAbsolute(cwd ?? "") ? DEFAULT_CWD : await getWorkDir() ?? "/home/daytona";
45
+ return io_exports.SandboxIO.resolveMountCwd(defaultCwd, cwd);
46
+ };
47
+ Schema.TaggedError()("DaytonaError", { sanitized: instance_exports.SandboxInstance.PersistedError });
48
+ var Remote = class extends Context.Service()("@codeworksh/harness/sandboxes/daytona/provider/Remote") {};
49
+ const assertCommandSucceeded = (command, result) => {
50
+ if (result.exitCode !== 0) throw new Error(result.result || `command failed (${result.exitCode}): ${command}`);
51
+ };
52
+ const dateFrom = (value) => {
53
+ if (value === void 0) return void 0;
54
+ return Option.getOrUndefined(DateTime.make(value).pipe(Option.map(DateTime.toDateUtc)));
55
+ };
56
+ const statsFrom = (info) => {
57
+ const symlink = info.mode === void 0 ? void 0 : info.mode.startsWith("l");
58
+ const mtime = dateFrom(info.modifiedAt ?? info.modTime);
59
+ return {
60
+ isFile: !info.isDir && symlink !== true,
61
+ isDirectory: info.isDir,
62
+ ...symlink === void 0 ? {} : { isSymbolicLink: symlink },
63
+ ...info.size === void 0 ? {} : { size: info.size },
64
+ ...mtime === void 0 ? {} : { mtime }
65
+ };
66
+ };
67
+ const providerFrom = (sandbox, options) => {
68
+ const filesystem = {
69
+ readFile: async (path) => (await sandbox.fs.downloadFile(path)).toString("utf8"),
70
+ readFileBuffer: async (path) => new Uint8Array(await sandbox.fs.downloadFile(path)),
71
+ writeFile: (path, content) => sandbox.fs.uploadFile(typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content), path),
72
+ stat: async (path) => statsFrom(await sandbox.fs.getFileDetails(path)),
73
+ lstat: async (path) => {
74
+ const command = `test -L ${(0, shell_exports.quote)(path)}`;
75
+ const result = await sandbox.process.executeCommand(command, options.cwd, void 0, options.execTimeout);
76
+ if (result.exitCode === 0) return {
77
+ isFile: false,
78
+ isDirectory: false,
79
+ isSymbolicLink: true
80
+ };
81
+ if (result.exitCode === 1) return statsFrom(await sandbox.fs.getFileDetails(path));
82
+ assertCommandSucceeded(command, result);
83
+ throw new Error(`unreachable lstat result for ${path}`);
84
+ },
85
+ readdir: async (path) => (await sandbox.fs.listFiles(path)).map((entry) => entry.name),
86
+ exists: async (path) => {
87
+ try {
88
+ await sandbox.fs.getFileDetails(path);
89
+ return true;
90
+ } catch (cause) {
91
+ const { DaytonaNotFoundError } = await import("@daytona/sdk");
92
+ if (cause instanceof DaytonaNotFoundError) return false;
93
+ throw cause;
94
+ }
95
+ },
96
+ mkdir: async (path, mkdirOptions) => {
97
+ if (!mkdirOptions?.recursive) {
98
+ await sandbox.fs.createFolder(path, "755");
99
+ return;
100
+ }
101
+ const command = `mkdir -p ${(0, shell_exports.quote)(path)}`;
102
+ const result = await sandbox.process.executeCommand(command, options.cwd, void 0, options.execTimeout);
103
+ assertCommandSucceeded(command, result);
104
+ },
105
+ rm: async (path, rmOptions) => {
106
+ if (rmOptions?.force && !await filesystem.exists(path)) return;
107
+ try {
108
+ await sandbox.fs.deleteFile(path, rmOptions?.recursive);
109
+ } catch (cause) {
110
+ if (rmOptions?.force && !await filesystem.exists(path)) return;
111
+ throw cause;
112
+ }
113
+ },
114
+ realpath: async (path) => {
115
+ const run = async (script) => {
116
+ const command = (0, shell_exports.quoteArgv)([
117
+ "sh",
118
+ "-c",
119
+ script,
120
+ "_",
121
+ path
122
+ ]);
123
+ return {
124
+ command,
125
+ result: await sandbox.process.executeCommand(command, options.cwd, void 0, options.execTimeout)
126
+ };
127
+ };
128
+ const asDirectory = await run(filesystem_exports.SandboxFileSystem.realpathScripts[0]);
129
+ if (asDirectory.result.exitCode === 0) return (asDirectory.result.result ?? "").trimEnd();
130
+ const asFile = await run(filesystem_exports.SandboxFileSystem.realpathScripts[1]);
131
+ assertCommandSucceeded(asFile.command, asFile.result);
132
+ return (asFile.result.result ?? "").trimEnd();
133
+ }
134
+ };
135
+ return filesystem;
136
+ };
137
+ const runCommand = (sandbox, options, command, opts) => Effect.tryPromise({
138
+ try: () => sandbox.process.executeCommand(command, (0, shell_exports.resolveCwd)(options.cwd, opts?.cwd), opts?.env, options.execTimeout),
139
+ catch: (cause) => new shell_exports.ShellError({
140
+ command,
141
+ cause
142
+ })
143
+ }).pipe(Effect.map((response) => ({
144
+ stdout: response.result ?? "",
145
+ stderr: "",
146
+ exitCode: response.exitCode
147
+ })));
148
+ const exec = (sandbox, options) => (command, opts) => runCommand(sandbox, options, command, opts);
149
+ const execArgv = (sandbox, options) => (argv, opts) => runCommand(sandbox, options, (0, shell_exports.quoteArgv)(argv), opts);
150
+ /**
151
+ * Cwd-neutral IO attachment for a lifecycle driver.
152
+ *
153
+ * Mount wrappers supply an absolute cwd to every public operation. Internal
154
+ * filesystem helper commands already receive absolute paths, so the transport
155
+ * itself keeps no mutable working-directory state and owns no resource
156
+ * finalizer.
157
+ */
158
+ const transport = (sandbox, options = {}) => Layer.merge(Layer.succeed(filesystem_exports.SandboxFileSystem.Service, filesystem_exports.SandboxFileSystem.fromProvider(make$1(providerFrom(sandbox, options)))), Layer.succeed(shell_exports.Shell, shell_exports.Shell.of({
159
+ exec: exec(sandbox, options),
160
+ execArgv: execArgv(sandbox, options)
161
+ })));
162
+ Layer.effect(resource_exports.SandboxResource.Service, Effect.map(Remote, ({ sandbox }) => ({ providerResourceId: sandbox.id })));
163
+ //#endregion
164
+ //#region src/sandboxes/daytona/index.ts
165
+ const Options = Schema.Struct({
166
+ apiKey: Schema.optional(Schema.String),
167
+ apiUrl: Schema.optional(Schema.String),
168
+ target: Schema.optional(Schema.String)
169
+ });
170
+ const ClientOptions = Options;
171
+ const ResourcesConfig = Schema.Struct({
172
+ cpu: Schema.optional(Schema.Finite),
173
+ gpu: Schema.optional(Schema.Finite),
174
+ memory: Schema.optional(Schema.Finite),
175
+ disk: Schema.optional(Schema.Finite)
176
+ });
177
+ const CreateConfig = Schema.Struct({
178
+ snapshot: Schema.optional(Schema.String),
179
+ image: Schema.optional(Schema.String),
180
+ language: Schema.optional(Schema.String),
181
+ envVars: Schema.optional(Schema.Record(Schema.String, Schema.String)),
182
+ resources: Schema.optional(ResourcesConfig),
183
+ user: Schema.optional(Schema.String),
184
+ cwd: Schema.optional(Schema.String),
185
+ autoStopInterval: Schema.optional(Schema.Finite),
186
+ execTimeout: Schema.optional(Schema.Finite)
187
+ });
188
+ const RuntimeConfig = Schema.Struct({
189
+ defaultCwd: SandboxDriver.AbsolutePath,
190
+ user: Schema.optional(Schema.String),
191
+ execTimeout: Schema.optional(Schema.Finite)
192
+ });
193
+ const name = SandboxDriver.Name.make("daytona");
194
+ const statusFrom = (state) => {
195
+ return {
196
+ status: state === "stopped" || state === "archived" ? "offline" : state === "stopping" || state === "archiving" || state === "snapshotting" || state === "destroying" ? "suspending" : state === "destroyed" ? "unavail" : state === "error" || state === "build_failed" || state === "unknown" ? "faulted" : "online",
197
+ providerStatus: state ?? "unknown"
198
+ };
199
+ };
200
+ const shouldWake = (sandbox) => sandbox.state === "stopped" || sandbox.state === "archived";
201
+ const make = (client = {}) => {
202
+ const redact = SandboxProvider.makeRedactor([client.apiKey ?? ""]);
203
+ const daytona = (sdk) => new sdk.Daytona({
204
+ ...client.apiKey === void 0 ? {} : { apiKey: client.apiKey },
205
+ ...client.apiUrl === void 0 ? {} : { apiUrl: client.apiUrl },
206
+ ...client.target === void 0 ? {} : { target: client.target }
207
+ });
208
+ const attempt = (operation, run) => Effect.suspend(() => {
209
+ let sdk;
210
+ return Effect.tryPromise({
211
+ try: () => import("@daytona/sdk").then((loaded) => {
212
+ sdk = loaded;
213
+ return run(loaded);
214
+ }),
215
+ catch: (cause) => SandboxProvider.providerError({
216
+ driver: name,
217
+ operation,
218
+ cause,
219
+ redact,
220
+ notFound: sdk !== void 0 && cause instanceof sdk.DaytonaNotFoundError
221
+ })
222
+ });
223
+ });
224
+ const get = (providerResourceId, operation) => attempt(operation, (sdk) => daytona(sdk).get(providerResourceId));
225
+ const refresh = (sandbox, operation) => attempt(operation, () => sandbox.refreshData()).pipe(Effect.as(sandbox));
226
+ const observed = (sandbox) => ({
227
+ ...statusFrom(sandbox.state),
228
+ metadata: { target: sandbox.target }
229
+ });
230
+ const wake = (sandbox, operation) => shouldWake(sandbox) ? attempt(operation, () => sandbox.start()).pipe(Effect.map(() => sandbox)) : Effect.succeed(sandbox);
231
+ const runtime = (defaultCwd, input) => ({
232
+ defaultCwd: SandboxDriver.AbsolutePath.make(defaultCwd),
233
+ ...input.user === void 0 ? {} : { user: input.user },
234
+ ...input.execTimeout === void 0 ? {} : { execTimeout: input.execTimeout }
235
+ });
236
+ return SandboxDriver.driver({
237
+ name,
238
+ kind: "remote",
239
+ capabilities: {
240
+ inspect: true,
241
+ reattach: true,
242
+ wake: true,
243
+ stop: true,
244
+ destroy: true,
245
+ cancels: false
246
+ },
247
+ createConfigCodec: CreateConfig,
248
+ runtimeConfigCodec: RuntimeConfig,
249
+ create: ({ instanceId, config }) => Effect.gen(function* () {
250
+ const base = {
251
+ language: config.language ?? "typescript",
252
+ ...config.envVars === void 0 ? {} : { envVars: config.envVars },
253
+ ...config.user === void 0 ? {} : { user: config.user },
254
+ ...config.autoStopInterval === void 0 ? {} : { autoStopInterval: config.autoStopInterval },
255
+ autoDeleteInterval: -1,
256
+ labels: {
257
+ "codework-instance": instanceId,
258
+ "codework-managed": "true"
259
+ }
260
+ };
261
+ const sandbox = yield* attempt("create", (loaded) => {
262
+ const sdk = daytona(loaded);
263
+ return config.image === void 0 ? sdk.create({
264
+ ...base,
265
+ ...config.snapshot === void 0 ? {} : { snapshot: config.snapshot }
266
+ }) : sdk.create({
267
+ ...base,
268
+ image: config.image,
269
+ ...config.resources === void 0 ? {} : { resources: config.resources }
270
+ });
271
+ });
272
+ const defaultCwd = yield* attempt("create.cwd", () => mountCwd(config.cwd, () => sandbox.getWorkDir()));
273
+ const state = statusFrom(sandbox.state);
274
+ return {
275
+ providerResourceId: sandbox.id,
276
+ providerStatus: state.providerStatus,
277
+ runtimeConfig: runtime(defaultCwd, config),
278
+ metadata: { target: sandbox.target }
279
+ };
280
+ }),
281
+ runtimeConfigFor: ({ providerResourceId, overrides }) => Effect.gen(function* () {
282
+ const sandbox = yield* get(providerResourceId, "runtimeConfigFor");
283
+ return {
284
+ defaultCwd: overrides?.defaultCwd ?? SandboxDriver.AbsolutePath.make(yield* attempt("runtimeConfigFor.cwd", () => mountCwd(void 0, () => sandbox.getWorkDir()))),
285
+ ...overrides?.user === void 0 ? { user: sandbox.user } : { user: overrides.user },
286
+ ...overrides?.execTimeout === void 0 ? {} : { execTimeout: overrides.execTimeout }
287
+ };
288
+ }),
289
+ attach: (input) => Layer.unwrap(Effect.gen(function* () {
290
+ const sandbox = yield* get(Option.getOrElse(input.providerResourceId, () => input.id), "attach");
291
+ yield* wake(sandbox, "attach.wake");
292
+ return transport(sandbox, input.runtimeConfig.execTimeout === void 0 ? void 0 : { execTimeout: input.runtimeConfig.execTimeout });
293
+ })),
294
+ inspect: (input) => Effect.flatMap(get(Option.getOrElse(input.providerResourceId, () => input.id), "inspect"), (sandbox) => Effect.map(refresh(sandbox, "inspect.refresh"), observed)),
295
+ wake: (input) => Effect.flatMap(get(Option.getOrElse(input.providerResourceId, () => input.id), "wake"), (sandbox) => Effect.map(wake(sandbox, "wake.start"), observed)),
296
+ stop: (input) => Effect.flatMap(get(Option.getOrElse(input.providerResourceId, () => input.id), "stop"), (sandbox) => attempt("stop", () => sandbox.stop()).pipe(Effect.map(() => observed(sandbox)))),
297
+ destroy: (input) => Effect.flatMap(get(Option.getOrElse(input.providerResourceId, () => input.id), "destroy"), (sandbox) => attempt("destroy", () => sandbox.delete()))
298
+ });
299
+ };
300
+ const sandbox = SandboxDriver.module({
301
+ apiVersion: SandboxDriver.apiVersion,
302
+ name,
303
+ options: Options,
304
+ make
305
+ });
306
+ const config = (value) => ({
307
+ driver: "daytona",
308
+ config: value
309
+ });
310
+ //#endregion
311
+ export { RuntimeConfig as a, sandbox as c, ResourcesConfig as i, CreateConfig as n, config as o, Options as r, make as s, ClientOptions as t };
312
+
313
+ //# sourceMappingURL=daytona-BAurx6Gp.mjs.map