@dynamicagents/plugins 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -77
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +53 -0
- package/dist/claude-code/cards.js +272 -0
- package/dist/claude-code/config.d.ts +28 -63
- package/dist/claude-code/config.js +0 -11
- package/dist/claude-code/egress.d.ts +8 -1
- package/dist/claude-code/egress.js +42 -4
- package/dist/claude-code/events.d.ts +106 -9
- package/dist/claude-code/events.js +201 -56
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +16 -90
- package/dist/claude-code/index.js +14 -276
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +342 -0
- package/dist/claude-code/run.d.ts +209 -112
- package/dist/claude-code/run.js +339 -138
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +285 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +1 -5
- package/dist/computer/context.js +7 -10
- package/dist/computer/file-lock.js +5 -4
- package/dist/computer/gate.d.ts +4 -4
- package/dist/computer/gate.js +5 -5
- package/dist/computer/index.d.ts +40 -258
- package/dist/computer/index.js +71 -324
- package/dist/computer/paths.d.ts +36 -16
- package/dist/computer/paths.js +42 -29
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +1 -40
- package/dist/computer/render.js +2 -68
- package/dist/computer/tools-exec.js +12 -13
- package/dist/computer/tools-file.d.ts +9 -1
- package/dist/computer/tools-file.js +22 -82
- package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +4 -5
- package/dist/repo/checkout.js +31 -8
- package/dist/repo/context.d.ts +35 -9
- package/dist/repo/context.js +101 -49
- package/dist/repo/index.d.ts +73 -28
- package/dist/repo/index.js +112 -96
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.js +1 -1
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
- package/dist/repo/tools-review.d.ts +9 -0
- package/dist/repo/tools-review.js +67 -47
- package/dist/repo/tools-worktree.js +144 -60
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +4 -4
- package/dist/{computer → workspace}/advisory.js +2 -2
- package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
- package/dist/{computer/host → workspace}/ca-trust.js +12 -3
- package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
- package/dist/{computer/host → workspace}/container-fault.js +23 -8
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
- package/dist/{computer/host → workspace}/git-host.js +72 -2
- package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
- package/dist/{computer/host → workspace}/git-identity.js +1 -1
- package/dist/workspace/index.d.ts +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/{computer/host → workspace}/install-job.d.ts +4 -4
- package/dist/{computer/host → workspace}/install-job.js +276 -176
- package/dist/{computer → workspace}/install.d.ts +8 -8
- package/dist/{computer → workspace}/install.js +3 -3
- package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +105 -27
- package/dist/{computer/host/workspace.js → workspace/object.js} +217 -48
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/{computer → workspace}/shell.d.ts +5 -5
- package/dist/{computer → workspace}/shell.js +6 -6
- package/dist/{computer/host → workspace}/wake.d.ts +1 -1
- package/package.json +16 -36
- package/dist/arc-agi/analysis.d.ts +0 -322
- package/dist/arc-agi/analysis.js +0 -802
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -24
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -40
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -24
- package/dist/arc-agi/main-agent.js +0 -62
- package/dist/arc-agi/recipe.d.ts +0 -48
- package/dist/arc-agi/recipe.js +0 -97
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -29
- package/dist/arc-agi/soul.js +0 -79
- package/dist/arc-agi/store.d.ts +0 -153
- package/dist/arc-agi/store.js +0 -215
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -574
- package/dist/arc-agi/types.d.ts +0 -211
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -133
- package/dist/computer/read.js +0 -200
- package/dist/computer/tools-find.js +0 -216
- package/dist/recall/index.d.ts +0 -105
- package/dist/recall/index.js +0 -181
- package/dist/repo/tools-forge.d.ts +0 -4
- package/dist/triage/index.d.ts +0 -360
- package/dist/triage/index.js +0 -311
- /package/dist/{computer/host → workspace}/container-deps.d.ts +0 -0
- /package/dist/{computer/host → workspace}/container-deps.js +0 -0
- /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
- /package/dist/{computer/host → workspace}/sync.js +0 -0
- /package/dist/{computer/host → workspace}/wake.js +0 -0
|
@@ -4,20 +4,19 @@ import { DurableObject, tracing } from "cloudflare:workers";
|
|
|
4
4
|
// object owes on waking, which is the callbacks registered on it below.
|
|
5
5
|
import { installScheduler, namedDeadline } from "@dynamicagents/core/alarm";
|
|
6
6
|
import { Workspace } from "@cloudflare/computer";
|
|
7
|
-
import {
|
|
7
|
+
import { ContainerBackend, withWorkspaceContainer } from "@cloudflare/computer/backends/container";
|
|
8
8
|
import { createGitClient } from "@cloudflare/computer/git";
|
|
9
9
|
import { createCloudflareObserver } from "@cloudflare/computer/observe/cloudflare";
|
|
10
|
-
import { MAX_TOOL_CALL_MS } from "@dynamicagents/core";
|
|
11
10
|
import { ContainerTrust } from "./ca-trust.js";
|
|
12
11
|
import { ContainerDeps } from "./container-deps.js";
|
|
13
12
|
import { ContainerGitIdentity } from "./git-identity.js";
|
|
14
13
|
import { InstallJob } from "./install-job.js";
|
|
15
14
|
import { WorkspaceGitHost } from "./git-host.js";
|
|
16
15
|
import { SYNC_DRAIN_BUDGET_MS, SYNC_DRAIN_RESUME_MS, WorkspaceSync, syncRetryDelayMs } from "./sync.js";
|
|
17
|
-
// Leaf modules rather than
|
|
16
|
+
// Leaf modules rather than `./index.ts` — see the same import in
|
|
18
17
|
// {@link file://./install-job.ts} for why the barrel is not reachable from here.
|
|
19
|
-
import { deriveAdvisories } from "
|
|
20
|
-
import { pathExists } from "
|
|
18
|
+
import { deriveAdvisories } from "./advisory.js";
|
|
19
|
+
import { pathExists } from "./read.js";
|
|
21
20
|
/**
|
|
22
21
|
* A workspace: one Durable Object, one container, one repository.
|
|
23
22
|
*
|
|
@@ -30,7 +29,7 @@ import { pathExists } from "../read.js";
|
|
|
30
29
|
* A directory inside the plugin rather than a subpath beside it, so that one
|
|
31
30
|
* capability stays one import path and a consumer cannot take the tools without
|
|
32
31
|
* the object they address. The bundle cost of that is nothing: `sideEffects` is
|
|
33
|
-
* false, so an agent that only calls `
|
|
32
|
+
* false, so an agent that only calls `bash` carries no container backend and
|
|
34
33
|
* no isomorphic-git.
|
|
35
34
|
*
|
|
36
35
|
* What this file must not do is import the plugin barrel. The barrel re-exports
|
|
@@ -43,7 +42,7 @@ import { pathExists } from "../read.js";
|
|
|
43
42
|
* FUSE at `/workspace`. Commands run against the same tree the Worker reads over
|
|
44
43
|
* RPC, and the tree outlives the container.
|
|
45
44
|
*
|
|
46
|
-
* **One repository per object**, because
|
|
45
|
+
* **One repository per object**, because `@cloudflare/computer` is strictly 1 DO ↔ 1
|
|
47
46
|
* container: the id derives from caller *and* repository (see `workspaceName`),
|
|
48
47
|
* so two repositories for one caller are two objects and two containers.
|
|
49
48
|
*
|
|
@@ -66,7 +65,7 @@ import { pathExists } from "../read.js";
|
|
|
66
65
|
* - `./sync.ts` — the host's half of syncing, which nothing else will do.
|
|
67
66
|
* - `./ca-trust.ts` — making a container able to speak TLS.
|
|
68
67
|
* - `./git-identity.ts` — who a container's commits belong to.
|
|
69
|
-
* - `./git-host.ts` — the operations that hold the
|
|
68
|
+
* - `./git-host.ts` — the operations that hold the GitHub credential.
|
|
70
69
|
*
|
|
71
70
|
* Each takes a small explicit seam, and that is the test: a module that needed
|
|
72
71
|
* the whole object back would be a module that did not want extracting.
|
|
@@ -77,7 +76,7 @@ export const WORKSPACE_DIR = "/workspace";
|
|
|
77
76
|
* The Durable Object name for one caller's checkout of one repository.
|
|
78
77
|
*
|
|
79
78
|
* Exported because several places must agree on it and none can see the others:
|
|
80
|
-
* the plugin that resolves the stub, the agent that hands it down to
|
|
79
|
+
* the plugin that resolves the stub, the agent that hands it down to sub-agents,
|
|
81
80
|
* and the cancellation path. A pipe rather than a slash, so the caller half
|
|
82
81
|
* cannot forge a repository boundary by containing one.
|
|
83
82
|
*
|
|
@@ -90,7 +89,7 @@ export const WORKSPACE_DIR = "/workspace";
|
|
|
90
89
|
*
|
|
91
90
|
* `repo` is not always a repository. A host may pass a sentinel through here as
|
|
92
91
|
* one — a scratchpad is modelled that way, which is what keys it to its own
|
|
93
|
-
* object and its own container. Anything that is not a
|
|
92
|
+
* object and its own container. Anything that is not a GitHub name works, as long
|
|
94
93
|
* as no caller could clone something that collides with it.
|
|
95
94
|
*/
|
|
96
95
|
export function workspaceName(callerKey, repo) {
|
|
@@ -123,31 +122,38 @@ const IDLE_RECLAIM_ID = "idle-reclaim-id";
|
|
|
123
122
|
* Source-only workspaces are small (6.3 MB for slack-gatekeeper), which makes
|
|
124
123
|
* this hygiene rather than cost control — but unbounded hygiene is still
|
|
125
124
|
* unbounded.
|
|
125
|
+
*
|
|
126
|
+
* Exported because a host keeping work in a workspace has to say how long that
|
|
127
|
+
* work survives untouched.
|
|
126
128
|
*/
|
|
127
|
-
const IDLE_RECLAIM_MS = 7 * 24 * 60 * 60 * 1000;
|
|
129
|
+
export const IDLE_RECLAIM_MS = 7 * 24 * 60 * 60 * 1000;
|
|
128
130
|
/** Where the container-idle deadline keeps its current schedule id. */
|
|
129
131
|
const CONTAINER_IDLE_ID = "container-idle-id";
|
|
132
|
+
/** The holds on the container — see `hold` — as key → the time each lapses. */
|
|
133
|
+
const HOLDS_KEY = "container-holds";
|
|
134
|
+
/** When a release was asked for while a hold stood, for the last `unhold`. */
|
|
135
|
+
const RELEASE_ASKED_KEY = "container-release-asked";
|
|
130
136
|
/** Where the container-warm wake keeps its current schedule id. */
|
|
131
137
|
const CONTAINER_WARM_ID = "container-warm-id";
|
|
132
138
|
/**
|
|
133
139
|
* How long a container stays up after the last command **started**.
|
|
134
140
|
*
|
|
135
|
-
* Ours to schedule
|
|
136
|
-
*
|
|
137
|
-
*
|
|
141
|
+
* Ours to schedule. The backend replaces a container but never stops one, and
|
|
142
|
+
* there is no `@cloudflare/containers` `Container` here with a `sleepAfter` to
|
|
143
|
+
* lean on: a container nobody is using runs until this deadline stops it.
|
|
138
144
|
*
|
|
139
145
|
* **This must exceed the longest command the agent allows**, and breaking that
|
|
140
146
|
* kills work in flight. Measured from when a command *starts*: `#touch()` arms
|
|
141
147
|
* the clock on the way into this object, and a running command touches nothing
|
|
142
148
|
* again until it finishes — `handle.result()` is one long await and the FUSE
|
|
143
|
-
* traffic under it never surfaces as an RPC. Set equal to
|
|
144
|
-
*
|
|
149
|
+
* traffic under it never surfaces as an RPC. Set equal to `DEFAULT_TIMEOUT_MS`
|
|
150
|
+
* in `./exec.ts`, the two timers race and whichever fires first destroys
|
|
145
151
|
* the container the other depends on. Twenty minutes is double that ceiling;
|
|
146
|
-
* raise it, never lower it, if `
|
|
152
|
+
* raise it, never lower it, if `bash` is given a longer timeout.
|
|
147
153
|
*
|
|
148
154
|
* **The default, not the policy.** "Longest command" is a fact about the agent,
|
|
149
155
|
* so one whose commands run longer must say so via
|
|
150
|
-
* {@link WorkspaceObjectConfig.containerIdleMs} — `claude-
|
|
156
|
+
* {@link WorkspaceObjectConfig.containerIdleMs} — `claude-coordinator` holds a
|
|
151
157
|
* `claude -p` session open for its whole 40-minute timeout.
|
|
152
158
|
*/
|
|
153
159
|
const CONTAINER_IDLE_MS = 20 * 60_000;
|
|
@@ -174,13 +180,13 @@ const SYNC_DRAIN_GRACE_MS = 30 * 60_000;
|
|
|
174
180
|
/**
|
|
175
181
|
* How long a container start may take before the calls waiting on it are refused.
|
|
176
182
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
183
|
+
* Late, because refusing a start that was only slow clears `#readying` while it
|
|
184
|
+
* runs, and the next caller starts a second one beside it — and a start pushing
|
|
185
|
+
* a large tree has taken nine minutes. Not later, because a turn is cut at
|
|
186
|
+
* fifteen: past that the model reads no reason, and its next turn joins the
|
|
187
|
+
* same stuck start.
|
|
182
188
|
*/
|
|
183
|
-
const READY_DEADLINE_MS =
|
|
189
|
+
const READY_DEADLINE_MS = 9 * 60_000;
|
|
184
190
|
/**
|
|
185
191
|
* `work`, refused once `ms` pass without it settling. The work itself goes on;
|
|
186
192
|
* only the wait for it ends — see {@link READY_DEADLINE_MS}.
|
|
@@ -199,6 +205,22 @@ export function readyWithin(work, ms) {
|
|
|
199
205
|
* see {@link WorkspaceObjectBase.#purgeSyncedTrees}.
|
|
200
206
|
*/
|
|
201
207
|
const SYNCED_TREES_PURGED_KEY = "deps:purged";
|
|
208
|
+
/**
|
|
209
|
+
* The `images` key a container starts from: `containers[].images.app` in the
|
|
210
|
+
* consumer's wrangler config. Named to the backend rather than left as its
|
|
211
|
+
* default, because {@link IMAGE_ENV} has to be read under the same key.
|
|
212
|
+
*/
|
|
213
|
+
const IMAGE_NAME = "app";
|
|
214
|
+
/**
|
|
215
|
+
* The env name the prepared image's reference rides under — see `backend` for
|
|
216
|
+
* why it rides at all.
|
|
217
|
+
*
|
|
218
|
+
* Nothing reads it, and the spelling keeps it that way. `computerd` hands a
|
|
219
|
+
* command its allowlist plus every `COMPUTER_VAR_` name, and reads its own
|
|
220
|
+
* configuration from `COMPUTERD_`, where a later release could claim this
|
|
221
|
+
* name. Outside both, it stays in the daemon's environment, unread.
|
|
222
|
+
*/
|
|
223
|
+
const IMAGE_ENV = "DYNAMICAGENTS_IMAGE";
|
|
202
224
|
// --- the object -------------------------------------------------------------
|
|
203
225
|
/**
|
|
204
226
|
* Named rather than the anonymous class expression an app would write here.
|
|
@@ -212,10 +234,9 @@ class WorkspaceContainerHost extends DurableObject {
|
|
|
212
234
|
}
|
|
213
235
|
/**
|
|
214
236
|
* `withWorkspaceContainer` adds one method, `getWorkspaceContainer()`, over
|
|
215
|
-
* `this.ctx.container` — the runtime's own container handle
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* everything else.
|
|
237
|
+
* `this.ctx.container` — the runtime's own container handle, started with the
|
|
238
|
+
* image and instance size this object asks for. Stopping it is this object's
|
|
239
|
+
* job — see {@link CONTAINER_IDLE_MS}.
|
|
219
240
|
*/
|
|
220
241
|
const WorkspaceContainerBase = withWorkspaceContainer(WorkspaceContainerHost);
|
|
221
242
|
/**
|
|
@@ -223,14 +244,13 @@ const WorkspaceContainerBase = withWorkspaceContainer(WorkspaceContainerHost);
|
|
|
223
244
|
*
|
|
224
245
|
* Abstract because a Durable Object class takes no constructor arguments, so
|
|
225
246
|
* per-agent configuration cannot arrive that way. {@link workspaceConfig} is the
|
|
226
|
-
* seam,
|
|
227
|
-
* uses for exactly the same reason.
|
|
247
|
+
* seam, for the same reason Think's own hooks are methods rather than options.
|
|
228
248
|
*
|
|
229
249
|
* ## Why `backend` and `#workspace` are lazy
|
|
230
250
|
*
|
|
231
251
|
* **Base class fields run before subclass fields**, so as plain fields they
|
|
232
252
|
* would read `undefined` from any `workspaceConfig()` that touches a subclass
|
|
233
|
-
* field — which `
|
|
253
|
+
* field — which `ClaudeCoordinatorWorkspace`'s does, for its credential store.
|
|
234
254
|
* Memoised getters remove the hazard rather than documenting it, which is what
|
|
235
255
|
* lets a subclass implement the seam however it likes.
|
|
236
256
|
*/
|
|
@@ -353,7 +373,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
353
373
|
id: () => this.ctx.id.toString()
|
|
354
374
|
});
|
|
355
375
|
/**
|
|
356
|
-
* The
|
|
376
|
+
* The GitHub credential, read at the moment it would be handed over.
|
|
357
377
|
*
|
|
358
378
|
* `#`-private, which is the only spelling actually unreachable over RPC — see
|
|
359
379
|
* {@link WorkspaceGitConfig.tokenBinding} for why that matters here and not
|
|
@@ -392,19 +412,38 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
392
412
|
* Nothing sets `egressHost`; the default `computer.internal` is the host the
|
|
393
413
|
* container's outbound HTTP is intercepted on, internal to that loopback.
|
|
394
414
|
*
|
|
395
|
-
* The
|
|
396
|
-
*
|
|
415
|
+
* **The image rides in the env so that a deploy replaces the container.** The
|
|
416
|
+
* backend reuses a running container whose launch — env, network, size, image
|
|
417
|
+
* key — matches the one it asks for, and the image a key resolves to is not
|
|
418
|
+
* part of that: a container started before a deploy would go on running the
|
|
419
|
+
* old image under the new Worker. The library here and the `computerd` in the
|
|
420
|
+
* image are one release, and a daemon from across a version line fails to
|
|
421
|
+
* authenticate (`[stage=auth]`). With the prepared image's reference in the
|
|
422
|
+
* env as {@link IMAGE_ENV}, a new image is a new launch, and the next connect
|
|
423
|
+
* replaces the container. With no image prepared under {@link IMAGE_NAME}
|
|
424
|
+
* there is nothing to add, and the backend's start says so.
|
|
425
|
+
*
|
|
426
|
+
* The binding name, the egress policy and the instance size are the
|
|
427
|
+
* subclass's — see {@link WorkspaceObjectConfig}, which carries the warnings
|
|
428
|
+
* on each.
|
|
397
429
|
*/
|
|
398
430
|
#backendMemo;
|
|
399
431
|
get backend() {
|
|
400
|
-
return (this.#backendMemo ??=
|
|
432
|
+
return (this.#backendMemo ??= this.#newBackend());
|
|
433
|
+
}
|
|
434
|
+
#newBackend() {
|
|
435
|
+
const image = this.ctx.container?.images?.[IMAGE_NAME];
|
|
436
|
+
return new ContainerBackend({
|
|
401
437
|
container: () => this,
|
|
402
438
|
workspace: {
|
|
403
439
|
binding: this.#cfg.binding,
|
|
404
440
|
id: this.ctx.id.toString()
|
|
405
441
|
},
|
|
406
|
-
egress: this.#cfg.egress
|
|
407
|
-
|
|
442
|
+
egress: this.#cfg.egress,
|
|
443
|
+
name: IMAGE_NAME,
|
|
444
|
+
instance: this.#cfg.instance,
|
|
445
|
+
containerEnv: image === undefined ? {} : { [IMAGE_ENV]: image }
|
|
446
|
+
});
|
|
408
447
|
}
|
|
409
448
|
#workspaceMemo;
|
|
410
449
|
get #workspace() {
|
|
@@ -486,7 +525,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
486
525
|
* method is reachable over RPC — see {@link WorkspaceGitConfig.tokenBinding}.
|
|
487
526
|
*
|
|
488
527
|
* **One start at a time**, because {@link #warmIfCold} makes overlap ordinary:
|
|
489
|
-
* a warm and the first `
|
|
528
|
+
* a warm and the first `bash` want the container at the same moment, as do
|
|
490
529
|
* a warm and the install alarm. Two starts means two trust commands through a
|
|
491
530
|
* half-established connection, the second pushing the tree again behind the
|
|
492
531
|
* first. Cleared on settle, so the next caller asks about the container that
|
|
@@ -581,7 +620,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
581
620
|
observer: createCloudflareObserver({ tracing }),
|
|
582
621
|
// Git, running **here** rather than in the container.
|
|
583
622
|
//
|
|
584
|
-
// This is what lets the
|
|
623
|
+
// This is what lets the GitHub token stay on this side of the boundary.
|
|
585
624
|
// `createGitClient` binds isomorphic-git to `provider()` — the local
|
|
586
625
|
// SQLite store, not the wire — so a clone, fetch or push executes next to
|
|
587
626
|
// the data it writes, and the container never holds a credential at all.
|
|
@@ -657,7 +696,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
657
696
|
*
|
|
658
697
|
* A stub cannot enforce what the caller does with it: `runtime.exec` on this
|
|
659
698
|
* one would run against a container with no CA. The exec paths take the other
|
|
660
|
-
* method — see `openWorkspaceFs` in
|
|
699
|
+
* method — see `openWorkspaceFs` in `./open.ts`.
|
|
661
700
|
*/
|
|
662
701
|
async __getWorkspaceFsStub() {
|
|
663
702
|
await this.#touch();
|
|
@@ -669,7 +708,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
669
708
|
return this.#workspace.stub();
|
|
670
709
|
}
|
|
671
710
|
/**
|
|
672
|
-
* Ask the alarm to start the container, so the first `
|
|
711
|
+
* Ask the alarm to start the container, so the first `bash` finds one that
|
|
673
712
|
* is up and already holding the tree.
|
|
674
713
|
*
|
|
675
714
|
* What this avoids is the push, not the boot — see {@link __getWorkspaceFsStub}.
|
|
@@ -679,7 +718,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
679
718
|
* nothing to push and nothing to run, and a container it never uses still
|
|
680
719
|
* bills.
|
|
681
720
|
*
|
|
682
|
-
* Only the arming is awaited. A warm that fails costs the next `
|
|
721
|
+
* Only the arming is awaited. A warm that fails costs the next `bash` the
|
|
683
722
|
* start it would have paid for anyway.
|
|
684
723
|
*/
|
|
685
724
|
async #warmIfCold() {
|
|
@@ -697,7 +736,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
697
736
|
await this.#containerWarm.set(new Date());
|
|
698
737
|
}
|
|
699
738
|
/**
|
|
700
|
-
* Clone, fetch and push — the operations that need the
|
|
739
|
+
* Clone, fetch and push — the operations that need the GitHub token.
|
|
701
740
|
*
|
|
702
741
|
* The work is `./git-host.ts`, which owns why the credential stays on this
|
|
703
742
|
* side of the boundary. What stays here is what only this object can do: the
|
|
@@ -716,6 +755,14 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
716
755
|
const behind = await this.#beforeGit();
|
|
717
756
|
return behind ?? (await this.#gitHost.push(req));
|
|
718
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* A checkout's `origin`, from this object's storage. Starts no container and
|
|
760
|
+
* moves no deadline, like {@link checkoutDir}: a pull request polled for its
|
|
761
|
+
* review is not the workspace in use.
|
|
762
|
+
*/
|
|
763
|
+
async gitOrigin(dir) {
|
|
764
|
+
return await this.#gitHost.origin(dir);
|
|
765
|
+
}
|
|
719
766
|
/**
|
|
720
767
|
* Bring the workspace up to date, or say why git must not run.
|
|
721
768
|
*
|
|
@@ -725,7 +772,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
725
772
|
* did not finish, the workspace still holds the tree as it was before the
|
|
726
773
|
* commit — and isomorphic-git, running here, would push a branch that does not
|
|
727
774
|
* include it. That is the one failure worth stopping for: the push reports
|
|
728
|
-
* success,
|
|
775
|
+
* success, GitHub shows an older tree, and nothing in either account says
|
|
729
776
|
* why.
|
|
730
777
|
*
|
|
731
778
|
* Refused rather than attempted, because every alternative is worse. Pushing
|
|
@@ -793,7 +840,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
793
840
|
*
|
|
794
841
|
* Every entry point calls this, which is what makes the idle clock measure
|
|
795
842
|
* *use* rather than "when the agent last said this name". The agent hands a
|
|
796
|
-
* workspace name to a
|
|
843
|
+
* workspace name to a sub-agent once and then never sees the traffic; the
|
|
797
844
|
* workspace sees all of it.
|
|
798
845
|
*/
|
|
799
846
|
async #touch() {
|
|
@@ -816,6 +863,117 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
816
863
|
await this.#idleReclaim.set(new Date(now + IDLE_RECLAIM_MS));
|
|
817
864
|
await this.#containerIdle.set(new Date(now + this.#containerIdleMs));
|
|
818
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* Stop the container, and **keep everything on disk**.
|
|
868
|
+
*
|
|
869
|
+
* The counterpart to {@link reclaimIfIdle}, and the difference is the whole
|
|
870
|
+
* reason both exist. A reclaim is for a workspace nobody wants again: it empties
|
|
871
|
+
* storage, so the checkout and the dependency tree go with the container. This is
|
|
872
|
+
* for a workspace that will be worked in again — the next task on the same
|
|
873
|
+
* repository has to skip the clone and the install, which is the cost the whole
|
|
874
|
+
* design is arranged around.
|
|
875
|
+
*
|
|
876
|
+
* **Why a caller needs this at all.** The idle deadline would eventually do it,
|
|
877
|
+
* but it cannot be tuned down to meet a cost target: `containerIdleMs` must
|
|
878
|
+
* exceed the longest command the agent allows, and a container that outlives its
|
|
879
|
+
* work by that much bills for the difference. So a host that knows the work is
|
|
880
|
+
* finished says so, and the deadline goes back to being a backstop for the case
|
|
881
|
+
* where nothing got to say anything.
|
|
882
|
+
*
|
|
883
|
+
* Deliberately does **not** touch: `#touch()` would push the very deadline this
|
|
884
|
+
* is standing in for.
|
|
885
|
+
*
|
|
886
|
+
* `released: false` means the container is still up and the standing deadline
|
|
887
|
+
* still owns it — an install is running, or a pull is still moving blocks. Both
|
|
888
|
+
* resolve themselves: the drain re-arms the deadline to come back and finish,
|
|
889
|
+
* which is the same path `#onContainerIdle` takes. Under a {@link hold} the
|
|
890
|
+
* release is deferred instead, to the last `unhold`.
|
|
891
|
+
*/
|
|
892
|
+
async releaseContainer() {
|
|
893
|
+
// Work this object cannot see. Deferred rather than refused: the last
|
|
894
|
+
// `unhold` releases, unless the workspace is used again first.
|
|
895
|
+
const holds = await this.#liveHolds();
|
|
896
|
+
if (holds.length > 0) {
|
|
897
|
+
await this.ctx.storage.put(RELEASE_ASKED_KEY, Date.now());
|
|
898
|
+
console.info(`[${this.#tag}] release deferred while held`, {
|
|
899
|
+
id: this.ctx.id.toString(),
|
|
900
|
+
holds
|
|
901
|
+
});
|
|
902
|
+
return { released: false };
|
|
903
|
+
}
|
|
904
|
+
await this.ctx.storage.delete(RELEASE_ASKED_KEY);
|
|
905
|
+
// An install in flight is "in use" even though nothing has called in.
|
|
906
|
+
// Same rule, and same reason, as `#onContainerIdle`.
|
|
907
|
+
const install = await this.#install.read();
|
|
908
|
+
if (install.state === "running")
|
|
909
|
+
return { released: false };
|
|
910
|
+
// Never stop a container with a pull still moving: that is what turns an
|
|
911
|
+
// outstanding write from late into lost. `#drainBeforeStop` carries the
|
|
912
|
+
// reasoning and re-arms the deadline when it defers.
|
|
913
|
+
const lastUsedAt = (await this.ctx.storage.get("lastUsedAt")) ?? 0;
|
|
914
|
+
if (await this.#drainBeforeStop(lastUsedAt, 0))
|
|
915
|
+
return { released: false };
|
|
916
|
+
// Dropped rather than left standing: the container is about to be gone, and an
|
|
917
|
+
// alarm that fires afterwards logs a stop against a container that is not
|
|
918
|
+
// running. `#touch` re-arms it the next time this workspace is used.
|
|
919
|
+
await this.#containerIdle.clear();
|
|
920
|
+
await this.#stopContainer("idle");
|
|
921
|
+
return { released: true };
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Keep the container up for work this object cannot see, until `untilMs` or
|
|
925
|
+
* {@link unhold}.
|
|
926
|
+
*
|
|
927
|
+
* A Claude Code session a host starts here runs detached: nothing calls in
|
|
928
|
+
* while it works, and the runtime cannot list
|
|
929
|
+
* what is running. So a host done with the workspace cannot tell that the
|
|
930
|
+
* container is not, and {@link releaseContainer} would stop the session with
|
|
931
|
+
* it. A hold is the host saying so. It defers a release; it is not use, so it
|
|
932
|
+
* does not touch, and the idle deadline already outlasts a session.
|
|
933
|
+
*
|
|
934
|
+
* `untilMs` bounds a hold whose `unhold` never comes — a run cut between its
|
|
935
|
+
* start and its end. Past it the hold is ignored.
|
|
936
|
+
*/
|
|
937
|
+
async hold(key, untilMs) {
|
|
938
|
+
const holds = await this.#holdMap();
|
|
939
|
+
holds[key] = untilMs;
|
|
940
|
+
await this.ctx.storage.put(HOLDS_KEY, holds);
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* The work a {@link hold} stood for is over. When it was the last hold, a
|
|
944
|
+
* release asked for while it stood happens now — unless the workspace was
|
|
945
|
+
* used since, in which case the next release is asked for by whoever used it.
|
|
946
|
+
*/
|
|
947
|
+
async unhold(key) {
|
|
948
|
+
// Read and written with no other I/O between, so the object's input gate
|
|
949
|
+
// keeps another call from interleaving: the map written is the one decided on.
|
|
950
|
+
const holds = await this.#holdMap();
|
|
951
|
+
delete holds[key];
|
|
952
|
+
await this.ctx.storage.put(HOLDS_KEY, holds);
|
|
953
|
+
if (Object.keys(holds).length > 0)
|
|
954
|
+
return { released: false };
|
|
955
|
+
const asked = await this.ctx.storage.get(RELEASE_ASKED_KEY);
|
|
956
|
+
if (asked === undefined)
|
|
957
|
+
return { released: false };
|
|
958
|
+
// A use in the same millisecond as the ask counts as after it: wrongly
|
|
959
|
+
// keeping a container costs its idle window, wrongly stopping one a session.
|
|
960
|
+
const lastUsedAt = (await this.ctx.storage.get("lastUsedAt")) ?? 0;
|
|
961
|
+
if (lastUsedAt >= asked) {
|
|
962
|
+
await this.ctx.storage.delete(RELEASE_ASKED_KEY);
|
|
963
|
+
return { released: false };
|
|
964
|
+
}
|
|
965
|
+
return await this.releaseContainer();
|
|
966
|
+
}
|
|
967
|
+
/** Every hold, lapsed ones pruned. */
|
|
968
|
+
async #holdMap() {
|
|
969
|
+
const now = Date.now();
|
|
970
|
+
const stored = (await this.ctx.storage.get(HOLDS_KEY)) ?? {};
|
|
971
|
+
return Object.fromEntries(Object.entries(stored).filter(([, until]) => until > now));
|
|
972
|
+
}
|
|
973
|
+
/** The keys of the holds still standing. */
|
|
974
|
+
async #liveHolds() {
|
|
975
|
+
return Object.keys(await this.#holdMap());
|
|
976
|
+
}
|
|
819
977
|
/**
|
|
820
978
|
* Throw this workspace away if nothing has touched it for `maxIdleMs`.
|
|
821
979
|
*
|
|
@@ -883,6 +1041,10 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
883
1041
|
reason,
|
|
884
1042
|
running: this.ctx.container?.running ?? false
|
|
885
1043
|
});
|
|
1044
|
+
// Hang up the session to computerd first: a destroy that cuts it live
|
|
1045
|
+
// surfaces as an uncaught `Network connection lost`. The workspace
|
|
1046
|
+
// reconnects on its next use, and `close()` swallows its own failures.
|
|
1047
|
+
await this.#workspaceMemo?.close();
|
|
886
1048
|
try {
|
|
887
1049
|
await this.ctx.container?.destroy();
|
|
888
1050
|
}
|
|
@@ -1013,7 +1175,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
1013
1175
|
}
|
|
1014
1176
|
catch (err) {
|
|
1015
1177
|
// A read of local SQLite that threw says nothing about the tree. Treat it
|
|
1016
|
-
// as absent: refusing a delegation costs a
|
|
1178
|
+
// as absent: refusing a delegation costs a tool call, starting a session in a
|
|
1017
1179
|
// directory that may not exist costs the run.
|
|
1018
1180
|
console.warn(`[${this.#tag}] could not probe a checkout`, {
|
|
1019
1181
|
id: this.ctx.id.toString(),
|
|
@@ -1025,7 +1187,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
1025
1187
|
}
|
|
1026
1188
|
/**
|
|
1027
1189
|
* Everything currently true about this workspace that a caller must not assume
|
|
1028
|
-
* away — the array `
|
|
1190
|
+
* away — the array `bash`, `edit` and the workspace's writes all consult.
|
|
1029
1191
|
*
|
|
1030
1192
|
* The policy is not here. `deriveAdvisories` decides which facts matter and
|
|
1031
1193
|
* how they are worded; this method gathers what only the object can see and
|
|
@@ -1161,7 +1323,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
1161
1323
|
*
|
|
1162
1324
|
* **Swallows its own failure.** Every other wake here is work somebody waits
|
|
1163
1325
|
* for; this one is early, so a container that will not start must cost the
|
|
1164
|
-
* next `
|
|
1326
|
+
* next `bash` a start rather than put the scheduler's retry ladder behind a
|
|
1165
1327
|
* deployment fault that will refuse for as long as it stands.
|
|
1166
1328
|
*/
|
|
1167
1329
|
async #onContainerWarm() {
|
|
@@ -1191,6 +1353,13 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
1191
1353
|
await this.#containerIdle.set(new Date(Date.now() + this.#containerIdleMs));
|
|
1192
1354
|
return;
|
|
1193
1355
|
}
|
|
1356
|
+
// Work nothing calls in for, which the deadline cannot measure: come back
|
|
1357
|
+
// when the first hold lapses, without counting the hold as use.
|
|
1358
|
+
const lapses = Object.values(await this.#holdMap());
|
|
1359
|
+
if (lapses.length > 0) {
|
|
1360
|
+
await this.#containerIdle.set(new Date(Math.min(...lapses)));
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1194
1363
|
// Re-read the clock rather than trusting the alarm, the same way
|
|
1195
1364
|
// `reclaimIfIdle` does. `#touch` moves the deadline forward, but an alarm
|
|
1196
1365
|
// already in flight cannot be recalled — so without this a workspace that
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { WorkspaceClient, WorkspaceStub } from "@cloudflare/computer";
|
|
2
|
+
import type { WorkspaceAdvisory } from "./advisory.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reaching a workspace: which one a call names, and the two ways to open it.
|
|
5
|
+
*
|
|
6
|
+
* A leaf, so the object, `/computer`'s tools and its proxy in
|
|
7
|
+
* `../computer/proxy.ts` can import it without
|
|
8
|
+
* going through the barrel that re-exports them.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Where a parent puts the workspace name so its sub-agents reach the same one.
|
|
12
|
+
*
|
|
13
|
+
* A sub-agent cannot compute this itself. The name is derived from the caller
|
|
14
|
+
* and the repository, and the checkout it should work in is one its parent
|
|
15
|
+
* chose — so a sub-agent that named its own would land beside the parent's
|
|
16
|
+
* checkout, in a workspace with nothing in it.
|
|
17
|
+
*
|
|
18
|
+
* A spec's `prepare` runs on the parent, where the name resolves, and its
|
|
19
|
+
* return value reaches the sub-agent's plugins as `runtime()`. That is the
|
|
20
|
+
* channel core built for exactly this, and it is emphatically *not* the
|
|
21
|
+
* sub-agent's input, which the parent's model writes — a workspace name there
|
|
22
|
+
* would be model-authored, and a model naming another caller's workspace would
|
|
23
|
+
* get that caller's files.
|
|
24
|
+
*/
|
|
25
|
+
export declare const WORKSPACE_RUNTIME_KEY = "workspaceName";
|
|
26
|
+
/**
|
|
27
|
+
* Read a parent-resolved workspace name off a sub-agent's `runtime()`.
|
|
28
|
+
*
|
|
29
|
+
* Returns `undefined` rather than throwing on anything unexpected: a parent has
|
|
30
|
+
* no runtime at all, and falling back to the configured thunk is right there.
|
|
31
|
+
*/
|
|
32
|
+
export declare function workspaceNameFromRuntime(runtime: unknown): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* The Durable Object the workspace lives in, as this plugin needs to see it.
|
|
35
|
+
*
|
|
36
|
+
* Structural rather than imported: the class is the host's — it owns the
|
|
37
|
+
* container binding, the alarm and the install job — and a plugin that imported
|
|
38
|
+
* it would stop a second host from bringing its own. `__getWorkspaceStub` is the
|
|
39
|
+
* one method `getWorkspace()` calls across the boundary, and it is what
|
|
40
|
+
* `withWorkspace` installs (or what a host that constructs `Workspace` itself
|
|
41
|
+
* reimplements, which is the same three lines).
|
|
42
|
+
*/
|
|
43
|
+
export interface WorkspaceHost extends Rpc.DurableObjectBranded {
|
|
44
|
+
__getWorkspaceStub(): Promise<WorkspaceStub>;
|
|
45
|
+
/**
|
|
46
|
+
* The same workspace for filesystem-only work, which the host must serve
|
|
47
|
+
* **without starting a container**.
|
|
48
|
+
*
|
|
49
|
+
* The filesystem is the Durable Object's own SQLite, while the first command
|
|
50
|
+
* in a fresh container waits for the whole tree to be pushed into it — so a
|
|
51
|
+
* file tool served the other way waits minutes for a local `readdir`. See
|
|
52
|
+
* `WorkspaceObjectBase.__getWorkspaceFsStub` in `./object.ts`.
|
|
53
|
+
*
|
|
54
|
+
* Required rather than optional, for the reason {@link advisories} gives. A
|
|
55
|
+
* host with nothing to distinguish returns its own `__getWorkspaceStub()`.
|
|
56
|
+
*/
|
|
57
|
+
__getWorkspaceFsStub(): Promise<WorkspaceStub>;
|
|
58
|
+
/**
|
|
59
|
+
* Everything currently true about the workspace that a caller must not assume
|
|
60
|
+
* away — see {@link file://./advisory.ts}. An empty array is the good case.
|
|
61
|
+
*
|
|
62
|
+
* Required rather than optional, and not only because Workers RPC types an
|
|
63
|
+
* optional method as a union nothing can call. A host with nothing to report
|
|
64
|
+
* returns `[]` in one line; a host that forgot to expose it gets a compile
|
|
65
|
+
* error instead of a `bash` that silently runs against a half-built
|
|
66
|
+
* `node_modules`, or against a workspace whose writes are being dropped.
|
|
67
|
+
*
|
|
68
|
+
* `deriveAdvisories` builds the array from what the host already knows, so
|
|
69
|
+
* implementing this is gathering what the host has rather than writing policy.
|
|
70
|
+
*/
|
|
71
|
+
advisories(): Promise<readonly WorkspaceAdvisory[]>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Open the workspace behind a Durable Object stub.
|
|
75
|
+
*
|
|
76
|
+
* The cast is unavoidable, and narrow enough to be worth isolating here rather
|
|
77
|
+
* than repeating. `getWorkspace` wants a handle whose `__getWorkspaceStub()`
|
|
78
|
+
* resolves to a `WorkspaceStub` — the concrete class, private fields and all.
|
|
79
|
+
* Across a Durable Object boundary Workers RPC hands back a structural
|
|
80
|
+
* `Stub<WorkspaceStub>`, which forwards every method faithfully but carries none
|
|
81
|
+
* of the class's private brand: runtime-compatible, type-incompatible. This is
|
|
82
|
+
* the only place in the plugin that gap is crossed.
|
|
83
|
+
*/
|
|
84
|
+
export declare function openWorkspace(host: DurableObjectStub<WorkspaceHost>): Promise<WorkspaceClient>;
|
|
85
|
+
/**
|
|
86
|
+
* The same workspace, opened for filesystem work alone.
|
|
87
|
+
*
|
|
88
|
+
* `getWorkspace` calls exactly one method on what it is handed, so the host is
|
|
89
|
+
* wrapped in an object answering it from the other side of the seam — see
|
|
90
|
+
* {@link WorkspaceHost.__getWorkspaceFsStub}. Everything past that is identical.
|
|
91
|
+
*
|
|
92
|
+
* **Only the file tools take this.** `bash` and `workspaceExec` need the
|
|
93
|
+
* container started and its CA installed, so they open it the other way.
|
|
94
|
+
*/
|
|
95
|
+
export declare function openWorkspaceFs(host: DurableObjectStub<WorkspaceHost>): Promise<WorkspaceClient>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getWorkspace } from "@cloudflare/computer";
|
|
2
|
+
/**
|
|
3
|
+
* Reaching a workspace: which one a call names, and the two ways to open it.
|
|
4
|
+
*
|
|
5
|
+
* A leaf, so the object, `/computer`'s tools and its proxy in
|
|
6
|
+
* `../computer/proxy.ts` can import it without
|
|
7
|
+
* going through the barrel that re-exports them.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Where a parent puts the workspace name so its sub-agents reach the same one.
|
|
11
|
+
*
|
|
12
|
+
* A sub-agent cannot compute this itself. The name is derived from the caller
|
|
13
|
+
* and the repository, and the checkout it should work in is one its parent
|
|
14
|
+
* chose — so a sub-agent that named its own would land beside the parent's
|
|
15
|
+
* checkout, in a workspace with nothing in it.
|
|
16
|
+
*
|
|
17
|
+
* A spec's `prepare` runs on the parent, where the name resolves, and its
|
|
18
|
+
* return value reaches the sub-agent's plugins as `runtime()`. That is the
|
|
19
|
+
* channel core built for exactly this, and it is emphatically *not* the
|
|
20
|
+
* sub-agent's input, which the parent's model writes — a workspace name there
|
|
21
|
+
* would be model-authored, and a model naming another caller's workspace would
|
|
22
|
+
* get that caller's files.
|
|
23
|
+
*/
|
|
24
|
+
export const WORKSPACE_RUNTIME_KEY = "workspaceName";
|
|
25
|
+
/**
|
|
26
|
+
* Read a parent-resolved workspace name off a sub-agent's `runtime()`.
|
|
27
|
+
*
|
|
28
|
+
* Returns `undefined` rather than throwing on anything unexpected: a parent has
|
|
29
|
+
* no runtime at all, and falling back to the configured thunk is right there.
|
|
30
|
+
*/
|
|
31
|
+
export function workspaceNameFromRuntime(runtime) {
|
|
32
|
+
const value = runtime?.[WORKSPACE_RUNTIME_KEY];
|
|
33
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Open the workspace behind a Durable Object stub.
|
|
37
|
+
*
|
|
38
|
+
* The cast is unavoidable, and narrow enough to be worth isolating here rather
|
|
39
|
+
* than repeating. `getWorkspace` wants a handle whose `__getWorkspaceStub()`
|
|
40
|
+
* resolves to a `WorkspaceStub` — the concrete class, private fields and all.
|
|
41
|
+
* Across a Durable Object boundary Workers RPC hands back a structural
|
|
42
|
+
* `Stub<WorkspaceStub>`, which forwards every method faithfully but carries none
|
|
43
|
+
* of the class's private brand: runtime-compatible, type-incompatible. This is
|
|
44
|
+
* the only place in the plugin that gap is crossed.
|
|
45
|
+
*/
|
|
46
|
+
export function openWorkspace(host) {
|
|
47
|
+
return getWorkspace(host);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The same workspace, opened for filesystem work alone.
|
|
51
|
+
*
|
|
52
|
+
* `getWorkspace` calls exactly one method on what it is handed, so the host is
|
|
53
|
+
* wrapped in an object answering it from the other side of the seam — see
|
|
54
|
+
* {@link WorkspaceHost.__getWorkspaceFsStub}. Everything past that is identical.
|
|
55
|
+
*
|
|
56
|
+
* **Only the file tools take this.** `bash` and `workspaceExec` need the
|
|
57
|
+
* container started and its CA installed, so they open it the other way.
|
|
58
|
+
*/
|
|
59
|
+
export function openWorkspaceFs(host) {
|
|
60
|
+
return getWorkspace({
|
|
61
|
+
__getWorkspaceStub: () => host.__getWorkspaceFsStub()
|
|
62
|
+
});
|
|
63
|
+
}
|