@usine/executor 0.2.0 → 0.3.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 +115 -37
- package/dist/child-command.d.ts +4 -7
- package/dist/child-command.d.ts.map +1 -1
- package/dist/child-command.js +6 -12
- package/dist/child-command.js.map +1 -1
- package/dist/claude-login-lock.d.ts +8 -0
- package/dist/claude-login-lock.d.ts.map +1 -0
- package/dist/claude-login-lock.js +9 -0
- package/dist/claude-login-lock.js.map +1 -0
- package/dist/credential-environment.d.ts +20 -0
- package/dist/credential-environment.d.ts.map +1 -0
- package/dist/credential-environment.js +20 -0
- package/dist/credential-environment.js.map +1 -0
- package/dist/durable-session.d.ts +63 -0
- package/dist/durable-session.d.ts.map +1 -0
- package/dist/durable-session.js +491 -0
- package/dist/durable-session.js.map +1 -0
- package/dist/exe-dev/commands.d.ts +23 -13
- package/dist/exe-dev/commands.d.ts.map +1 -1
- package/dist/exe-dev/commands.js +11 -57
- package/dist/exe-dev/commands.js.map +1 -1
- package/dist/exe-dev/exe-dev.d.ts +9 -3
- package/dist/exe-dev/exe-dev.d.ts.map +1 -1
- package/dist/exe-dev/exe-dev.js +53 -127
- package/dist/exe-dev/exe-dev.js.map +1 -1
- package/dist/exe-dev/grants.d.ts.map +1 -1
- package/dist/exe-dev/grants.js +35 -15
- package/dist/exe-dev/grants.js.map +1 -1
- package/dist/exe-dev/model.d.ts +15 -19
- package/dist/exe-dev/model.d.ts.map +1 -1
- package/dist/exe-dev/model.js +9 -12
- package/dist/exe-dev/model.js.map +1 -1
- package/dist/exe-dev/preflight.d.ts +8 -4
- package/dist/exe-dev/preflight.d.ts.map +1 -1
- package/dist/exe-dev/preflight.js +23 -33
- package/dist/exe-dev/preflight.js.map +1 -1
- package/dist/executor.d.ts +141 -38
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +90 -17
- package/dist/executor.js.map +1 -1
- package/dist/guard.d.ts +61 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +288 -0
- package/dist/guard.js.map +1 -0
- package/dist/harness-preflight.d.ts +25 -4
- package/dist/harness-preflight.d.ts.map +1 -1
- package/dist/harness-preflight.js +25 -29
- package/dist/harness-preflight.js.map +1 -1
- package/dist/keyed-lock.d.ts +2 -3
- package/dist/keyed-lock.d.ts.map +1 -1
- package/dist/keyed-lock.js +2 -3
- package/dist/keyed-lock.js.map +1 -1
- package/dist/leftovers.d.ts +9 -0
- package/dist/leftovers.d.ts.map +1 -0
- package/dist/leftovers.js +14 -0
- package/dist/leftovers.js.map +1 -0
- package/dist/local/credentials.d.ts +21 -0
- package/dist/local/credentials.d.ts.map +1 -0
- package/dist/local/credentials.js +39 -0
- package/dist/local/credentials.js.map +1 -0
- package/dist/local/leftovers.d.ts +8 -0
- package/dist/local/leftovers.d.ts.map +1 -0
- package/dist/local/leftovers.js +23 -0
- package/dist/local/leftovers.js.map +1 -0
- package/dist/local/local.d.ts +16 -10
- package/dist/local/local.d.ts.map +1 -1
- package/dist/local/local.js +164 -129
- package/dist/local/local.js.map +1 -1
- package/dist/memory-preflight.d.ts +25 -0
- package/dist/memory-preflight.d.ts.map +1 -0
- package/dist/memory-preflight.js +83 -0
- package/dist/memory-preflight.js.map +1 -0
- package/dist/pre-push-hook.d.ts +20 -0
- package/dist/pre-push-hook.d.ts.map +1 -0
- package/dist/pre-push-hook.js +33 -0
- package/dist/pre-push-hook.js.map +1 -0
- package/dist/prepared-attempt.d.ts +33 -3
- package/dist/prepared-attempt.d.ts.map +1 -1
- package/dist/prepared-attempt.js +109 -39
- package/dist/prepared-attempt.js.map +1 -1
- package/dist/provision.d.ts +77 -14
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +357 -55
- package/dist/provision.js.map +1 -1
- package/dist/remote/commands.d.ts +51 -0
- package/dist/remote/commands.d.ts.map +1 -0
- package/dist/remote/commands.js +94 -0
- package/dist/remote/commands.js.map +1 -0
- package/dist/remote/leftovers.d.ts +14 -0
- package/dist/remote/leftovers.d.ts.map +1 -0
- package/dist/remote/leftovers.js +22 -0
- package/dist/remote/leftovers.js.map +1 -0
- package/dist/remote/liveness.d.ts +33 -0
- package/dist/remote/liveness.d.ts.map +1 -0
- package/dist/remote/liveness.js +194 -0
- package/dist/remote/liveness.js.map +1 -0
- package/dist/remote/machine.d.ts +64 -0
- package/dist/remote/machine.d.ts.map +1 -0
- package/dist/remote/machine.js +2 -0
- package/dist/remote/machine.js.map +1 -0
- package/dist/remote/remote.d.ts +24 -0
- package/dist/remote/remote.d.ts.map +1 -0
- package/dist/remote/remote.js +308 -0
- package/dist/remote/remote.js.map +1 -0
- package/dist/remote/sweep.d.ts +15 -0
- package/dist/remote/sweep.d.ts.map +1 -0
- package/dist/remote/sweep.js +116 -0
- package/dist/remote/sweep.js.map +1 -0
- package/dist/renewable-credential.d.ts +17 -0
- package/dist/renewable-credential.d.ts.map +1 -0
- package/dist/renewable-credential.js +58 -0
- package/dist/renewable-credential.js.map +1 -0
- package/dist/salvage.d.ts +30 -0
- package/dist/salvage.d.ts.map +1 -0
- package/dist/salvage.js +104 -0
- package/dist/salvage.js.map +1 -0
- package/dist/ssh/credentials.d.ts +40 -0
- package/dist/ssh/credentials.d.ts.map +1 -0
- package/dist/ssh/credentials.js +83 -0
- package/dist/ssh/credentials.js.map +1 -0
- package/dist/ssh/gateway/admin.d.ts +45 -0
- package/dist/ssh/gateway/admin.d.ts.map +1 -0
- package/dist/ssh/gateway/admin.js +129 -0
- package/dist/ssh/gateway/admin.js.map +1 -0
- package/dist/ssh/gateway/attachments.d.ts +22 -0
- package/dist/ssh/gateway/attachments.d.ts.map +1 -0
- package/dist/ssh/gateway/attachments.js +154 -0
- package/dist/ssh/gateway/attachments.js.map +1 -0
- package/dist/ssh/gateway/environment.d.ts +52 -0
- package/dist/ssh/gateway/environment.d.ts.map +1 -0
- package/dist/ssh/gateway/environment.js +78 -0
- package/dist/ssh/gateway/environment.js.map +1 -0
- package/dist/ssh/gateway/forward.d.ts +74 -0
- package/dist/ssh/gateway/forward.d.ts.map +1 -0
- package/dist/ssh/gateway/forward.js +247 -0
- package/dist/ssh/gateway/forward.js.map +1 -0
- package/dist/ssh/gateway/preflight.d.ts +28 -0
- package/dist/ssh/gateway/preflight.d.ts.map +1 -0
- package/dist/ssh/gateway/preflight.js +234 -0
- package/dist/ssh/gateway/preflight.js.map +1 -0
- package/dist/ssh/model.d.ts +65 -0
- package/dist/ssh/model.d.ts.map +1 -0
- package/dist/ssh/model.js +2 -0
- package/dist/ssh/model.js.map +1 -0
- package/dist/ssh/preflight.d.ts +16 -0
- package/dist/ssh/preflight.d.ts.map +1 -0
- package/dist/ssh/preflight.js +47 -0
- package/dist/ssh/preflight.js.map +1 -0
- package/dist/ssh/ssh.d.ts +20 -0
- package/dist/ssh/ssh.d.ts.map +1 -0
- package/dist/ssh/ssh.js +78 -0
- package/dist/ssh/ssh.js.map +1 -0
- package/dist/ssh/workers.d.ts +53 -0
- package/dist/ssh/workers.d.ts.map +1 -0
- package/dist/ssh/workers.js +173 -0
- package/dist/ssh/workers.js.map +1 -0
- package/package.json +12 -4
- package/dist/exe-dev/channel.d.ts +0 -11
- package/dist/exe-dev/channel.d.ts.map +0 -1
- package/dist/exe-dev/channel.js +0 -41
- package/dist/exe-dev/channel.js.map +0 -1
- package/dist/exe-dev/shell.d.ts +0 -12
- package/dist/exe-dev/shell.d.ts.map +0 -1
- package/dist/exe-dev/shell.js +0 -31
- package/dist/exe-dev/shell.js.map +0 -1
- package/dist/exe-dev/sweep.d.ts +0 -10
- package/dist/exe-dev/sweep.d.ts.map +0 -1
- package/dist/exe-dev/sweep.js +0 -117
- package/dist/exe-dev/sweep.js.map +0 -1
- package/dist/invalid-frame.d.ts +0 -17
- package/dist/invalid-frame.d.ts.map +0 -1
- package/dist/invalid-frame.js +0 -111
- package/dist/invalid-frame.js.map +0 -1
- package/dist/run-host-session.d.ts +0 -26
- package/dist/run-host-session.d.ts.map +0 -1
- package/dist/run-host-session.js +0 -241
- package/dist/run-host-session.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# @usine/executor
|
|
2
2
|
|
|
3
|
-
Worker execution for [usine](https://github.com/basaltbytes/usine). The platform-free
|
|
3
|
+
Worker execution for [usine](https://github.com/basaltbytes/usine). The platform-free root defines worker preflight, scoped attempt preparation, and cancellation, plus the credential provider, worker capability errors, and adapter-neutral request types. Each prepared attempt runs its Pi parent in this process as a durable task, tells it where the worker runs children (local processes, or SSH on an exe.dev VM or any Linux machine), and releases its resources at completion or when its scope closes. [Attempt execution](https://docs.usine.dev/architecture/execution/) and [Workers and the gateway](https://docs.usine.dev/architecture/workers-and-gateway/) cover the executors.
|
|
4
|
+
|
|
5
|
+
0.x APIs can change. Supply `effect` at the workspace's pinned version.
|
|
4
6
|
|
|
5
7
|
## Attempt lifetime
|
|
6
8
|
|
|
7
|
-
Call `Executor.prepare(request)` inside `Effect.scoped`, then
|
|
8
|
-
once. The returned `PreparedAttempt` owns the checkout and repository access. Its
|
|
9
|
-
`workspace` is an `AttemptWorkspace` descriptor containing the Worker, RunSpec, and
|
|
10
|
-
worktree path; it can be serialized without exposing credentials or live handles.
|
|
9
|
+
Call `Executor.prepare(request)` inside `Effect.scoped`, then `prepared.launch(io)` once. The `PreparedAttempt` owns the checkout and repo access. Its `workspace`, an `AttemptWorkspace` with the worker, run spec, and worktree path, serializes without credentials or live handles.
|
|
11
10
|
|
|
12
11
|
```ts
|
|
13
12
|
const result = yield* Effect.scoped(
|
|
@@ -18,56 +17,86 @@ const result = yield* Effect.scoped(
|
|
|
18
17
|
)
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
Preparation
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
Preparation stays interruptible during clone, fetch, credential requests, and lock waits. A failed prepare releases partial acquisitions at once, and an unlaunched attempt when the scope closes. Launch finishes cleanup before it returns on success, failure, or interruption, and closing the owner scope stops a launch in another fiber. Each attempt launches at most once. Cleanup failures are logged and never make a successful result retryable.
|
|
21
|
+
|
|
22
|
+
`CheckoutRequest.maxAttemptMinutes` bounds live execution, excluding preparation and cleanup. Both runaway ceilings are optional: an absent one leaves that half of the guard off, and an armed one steers the parent once at 80%. The clock starts once launch registers the session. The guard delivers its warning apart from its checks, once an `execute` relays the parent's channel (`AttemptRuntime.attached`): no trip waits for delivery, and the append that reaches the token warning returns before the steer is delivered, since the durable runtime records the steer in order with its envelopes.
|
|
23
|
+
|
|
24
|
+
`CheckoutRequest.permissions` is the loop's permission level. A `read` attempt's credential has `contents: read`, so it can't push. An exe.dev worker can't grant that, and refuses the attempt with a `credentials` `WorkerCapabilityError` before attaching the integration. A `read` checkout also gets a pre-push hook that stops `git push` with a message naming the loop, instead of GitHub's 403; `git push --no-verify` skips it, and the token still refuses. A `read` attempt is never salvaged.
|
|
25
|
+
|
|
26
|
+
### Salvage
|
|
27
|
+
|
|
28
|
+
An attempt that ends without success is salvaged before its worktree is removed: uncommitted changes are committed as `wip(usine): salvage attempt <id>`, and HEAD is pushed to `CheckoutRequest.salvageRef` with the worker's credentials. The result reaches the engine through `Sink.salvage`. A failed push keeps the worktree and reports its path and the failure: git's exit code and stderr, so a deleted remote reads differently from a revoked app, or a push stalled past two minutes. Local and SSH adapters scrub the token from that stderr; an exe.dev VM never has one. The startup sweep leaves such a worktree alone. Each later startup retries the push with fresh credentials, for open and closed runs, and rewrites the record from `Retained` to `Pushed` on success; until then the record shows the latest failure in the run drawer.
|
|
29
|
+
|
|
30
|
+
A request whose `resume` names a pushed salvage fetches the revision and applies the salvage on top. A salvage forked from the fetched commit is checked out as is; one from an older base is rebased onto it. A salvage with no changes beyond the revision is dropped. One that doesn't rebase cleanly is left out: the worktree starts from the revision, `RunSpec.conflictingSalvage` names it, and its branch keeps the work. A missing ref falls back to the revision.
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
Startup reconciliation lists each worker's `attempts/` once per executor instance, and adapters skip historical attempts missing from the listing without checking paths. A failed listing stays cached for that startup and is retried after a restart. Existing checkouts keep the record-before-remove salvage order; succeeded attempts only need removal.
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
34
|
+
### Cancel
|
|
35
|
+
|
|
36
|
+
`cancel(attemptId)` applies from the start of preparation: harness probes, credential acquisition, repo lock waits, and checkout creation. It stops preparation before releasing acquired resources and blocks a later launch. `cancel` and the canceled `prepare` or `launch` return only after release. For a live attempt, it cancels the parent task and waits the configured grace before forcing termination. Concurrent and repeated cancels wait for cleanup and keep the launch's typed failure; after release, cancel does nothing.
|
|
37
|
+
|
|
38
|
+
This replaces the 0.x `Executor.launch(prepared, io)` and `Executor.cleanup(prepared)`; the engine's `StepExecutor` contract is unchanged.
|
|
35
39
|
|
|
36
40
|
## Worker preflight
|
|
37
41
|
|
|
38
|
-
Call `executor.preflight(worker, brains)` with the
|
|
42
|
+
Call `executor.preflight(worker, brains, maxConcurrentAttempts)` with the roster's `BrainSpec` values and the worker's `max_concurrent_attempts` before scheduling.
|
|
43
|
+
|
|
44
|
+
Every adapter compares the machine's memory with `maxConcurrentAttempts`. The local adapter reads `/proc/meminfo` on Linux and physical memory on macOS, where swap grows on demand; exe.dev and SSH read `/proc/meminfo` over SSH. Under 1.5 GiB per concurrent attempt, or no swap with more than one, is a `memory` `WorkerCapabilityError`; under 3 GiB logs a warning ([#270](https://github.com/basaltbytes/usine/issues/270)).
|
|
45
|
+
|
|
46
|
+
Every adapter runs the `@usine/agents/preflight` checks where children run: local on this machine (override with `LocalExecutorOptions.harnessReadiness`), exe.dev and SSH over SSH (`SshExecutorOptions.harnessReadiness`). Codex needs CLI 0.144.0 or 0.153.4 and a ChatGPT login, Claude its CLI and subscription login, and Pi children API keys for their models. Pi children run tools in the orchestrator, so exe.dev and SSH workers report Pi brains unavailable.
|
|
47
|
+
|
|
48
|
+
`prepare` repeats the assigned brain's probe before acquiring credentials or a checkout, to catch roster changes and logouts. A capability gap returns `WorkerCapabilityError`, which needs the operator. Transport failures, such as an SSH client that can't reach the VM, and probe timeouts are retryable infrastructure failures.
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
A Claude check holds the worker's `ClaudeLoginLock` (`@usine/agents/runtime`) until it returns, so two preparing attempts, or a preparing attempt and a starting Claude child, never refresh the login at once. The local, exe.dev, and SSH layers need that service: build `claudeLoginLockLayer` from the root once and give it to the executor and the attempt runtime.
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
It replaces exe.dev's `HarnessLoginCheck` and `harnessLoginChecks` options.
|
|
43
53
|
|
|
44
|
-
## Local
|
|
54
|
+
## Local worker
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
For each attempt, `@usine/executor/local`:
|
|
47
57
|
|
|
48
|
-
- checks Git
|
|
49
|
-
- updates a per-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
58
|
+
- checks Git and the assigned harness before accepting work;
|
|
59
|
+
- updates a per-repo bare cache, then creates a detached worktree at the resolved revision;
|
|
60
|
+
- renews repo credentials before expiry. Git reads an attempt token file and `gh` its private `GH_CONFIG_DIR` on each call, so a salvage push after hours uses the latest credential; startup reconciliation issues a fresh one;
|
|
61
|
+
- runs the Pi parent here through `AttemptRuntime` (`@usine/durable`) with the attempt's environment: allowlisted variables, credential settings, and `TURBO_CACHE_DIR`, never the orchestrator's environment;
|
|
62
|
+
- harvests commits and pull requests from the worktree, and the attempt's comments and reviews on the item, with `@usine/core/step/harvest`, running `git` and `gh` in that environment. A failed or canceled attempt (unless this process stopped) gets only its comments harvested, after its parent task stops, through `Sink.comments`. `Sink.windows` reads the attempt windows after GitHub lists the comments, and the harvest keeps comments by the window rule of `@usine/core` (#429). If the windows can't be read, a finished attempt's harvest fails and a failed attempt records no comment;
|
|
63
|
+
- cancels the parent task before forced interruption, then removes the worktree and wipes the credential.
|
|
53
64
|
|
|
54
|
-
`
|
|
65
|
+
`TURBO_CACHE_DIR` is `.usine/workers/<worker>/turbo`. Startup cache sweeps prune only the `*.git` directories the adapter creates under `repos/`, skip and log anything else at debug, and end with an info log of pruned, skipped, and failed counts.
|
|
55
66
|
|
|
56
|
-
The
|
|
67
|
+
The local layer needs Effect `FileSystem`, `Path`, and `ChildProcessSpawner`, a `CredentialProvider`, and an `AttemptRuntime`.
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
### Restarts
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
If the orchestrator stops during an attempt, the parent task stays live in durable storage and the worktree on disk. After a restart, `isLive` lets the engine reattach, and `prepare` with `reattach` takes over the worktree with fresh credentials, reading the run spec and base commit from `AttemptRuntime.request`. A parent that reported success before its harvest also counts as live, so the reattachment harvests it.
|
|
61
72
|
|
|
62
|
-
|
|
73
|
+
The reattached launch resumes its runaway guard (#178) from `Sink.transcript`: each session's highest token report, finished children included; the parent's hold, if still placed; and the warnings already sent, which aren't resent. The guard checks the restored total at start, so a total past a lowered ceiling trips at once. A warning that the restored totals owe waits until `execute` relays the parent's channel.
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
Unsupervised time comes from `AttemptRuntime.unsupervised`. A wall-clock guard records it every 30 seconds while it grows and once when it stops, within a five-second grace. Only a running guard counts: quiet time before a restart counts, downtime doesn't. Each unclean stop loses at most the last 30 seconds, so a crash lets an attempt run that much past its ceiling.
|
|
65
76
|
|
|
66
|
-
|
|
77
|
+
A reattachment reads only what its armed ceilings use: the transcript for either, the unsupervised time for the clock. If one can't be read after the store's retries, the guard doesn't start over with a new allowance. The durable session cancels the parent task, the launch salvages the worktree through `Sink.salvage`, and the attempt fails with an `InfraFailure` naming what couldn't be read, so the next attempt starts from the salvage.
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
After the harvest, the result is recorded in durable storage before the worktree is released, and `outcome` returns it, or the parent's failure, to an engine that stopped before recording the attempt. If storage has the parent's failure, `recover` first records the open attempt's comments, harvested in its worktree with a fresh credential, through the `RecoverySink`'s `comments`, filtered by the windows that its `windows` reads after GitHub lists them. A missing worktree or credential, or unreadable windows, is logged and records none.
|
|
69
80
|
|
|
70
|
-
|
|
81
|
+
Every other end, whether failure, cancel, or tripped ceiling, cancels the parent task. The engine calls `recover` only for attempts that no run reattaches, so `recover` cancels a live parent task first and reconciles the worktree even if `isLive` still reports the attempt, as for an unharvested report. If the stop can't be confirmed (liveness unreadable, or the cancel fails or outlasts the interrupt grace), `recover` and `cancel` fail with a `cancel` `ExecutorOperationError`, and `recover` leaves the worktree, which the task might still write.
|
|
82
|
+
|
|
83
|
+
## exe.dev worker
|
|
84
|
+
|
|
85
|
+
`@usine/executor/exe-dev` runs the same contract on a persistent exe.dev VM whose name is the worker name. Its image must already have Git, the roster's `claude` and `codex` CLIs, and every coding-agent login the instance uses.
|
|
86
|
+
|
|
87
|
+
Preflight creates no checkouts or grants. It reads VM metadata with `ssh exe.dev ls --json`, checks Git and memory over SSH, probes the roster on the VM, and asks exe.dev to test each GitHub integration.
|
|
88
|
+
|
|
89
|
+
Prepare attaches the repo's integration to the VM with a bounded `--for` grant, fetches a bare cached clone from `github.int.exe.xyz`, and creates a detached worktree. Launch runs the Pi parent here through `AttemptRuntime` with a `RemoteShell` host: each Claude Code or Codex child is one SSH command that enters the worktree, exports `GH_HOST=github.int.exe.xyz` over the VM's environment, and runs the harness. Harvesting runs `git` and `gh` the same way. Cleanup removes the worktree, prunes its cache registration, and detaches the integration after the last concurrent attempt that uses it.
|
|
90
|
+
|
|
91
|
+
An exited SSH client doesn't prove its remote command stopped, so each remote child writes its pid to `<worktree>.child-<n>.pid`. Before a workspace is removed or kept, cleanup stops every recorded child on the VM and confirms it's dead; cancel, recovery, and reattach do the same.
|
|
92
|
+
|
|
93
|
+
After a restart, `prepare` with `reattach` checks that the stored request names this worktree and that it still exists on the VM, stops leftover children, and takes it over with a fresh grant. If the VM or worktree is gone, the reattachment fails and cancels the parent task, and the engine retries with a new attempt.
|
|
94
|
+
|
|
95
|
+
Startup reconciles unsuccessful attempts before the engine recovers their runs: each checkout is snapshotted and pushed, and its salvage recorded, before removal. Failed pushes and unknown crash leftovers stay on disk. Cache sweeps prune registrations of missing directories and never delete checkouts. One-shot lobby and SSH commands time out after ten minutes, and the SSH client sends keepalives, so a dead link fails the operation.
|
|
96
|
+
|
|
97
|
+
Concurrent attempts on different repos keep their integrations attached together. Each grant renews at half its lifetime while its attempt runs, a failed renewal retries every minute, and the refcounted release detaches an integration after its last attempt. The grant length, `credentialGrantMinutes` (65 by default), is a renewal window, unrelated to `max_attempt_minutes`; an attempt without a ceiling keeps its grant for its whole life. A failed detach is logged and lapses with its time-box. If stopping the remote children can't be confirmed, cleanup keeps the checkout and its Git registration, logs the location, and still releases the grant. Partial preparation rolls back without stopping children, since none started.
|
|
98
|
+
|
|
99
|
+
The exe.dev layer needs a `ChildProcessSpawner` and an `AttemptRuntime`.
|
|
71
100
|
|
|
72
101
|
```ts
|
|
73
102
|
import { RepositoryName } from "@usine/core/repository"
|
|
@@ -85,4 +114,53 @@ const executorLayer = layer({
|
|
|
85
114
|
})
|
|
86
115
|
```
|
|
87
116
|
|
|
88
|
-
The
|
|
117
|
+
The integration keeps the GitHub credential at exe.dev's network edge: no GitHub token enters the run spec, the durable request, the local SSH environment, or the VM. Pushes from this worker use `exe-dev-github-integration[bot]`; agent-authored PRs and comments should carry the run's agent signature.
|
|
118
|
+
|
|
119
|
+
Salvage inspection, staging, or snapshot failures keep the checkout, uncommitted files included. Restart recovery uses fresh credentials and records the recovered ref before releasing the worktree.
|
|
120
|
+
|
|
121
|
+
## SSH worker
|
|
122
|
+
|
|
123
|
+
`@usine/executor/ssh` runs the same contract on any Linux machine that the orchestrator reaches over SSH ([ADR 0026](../../docs/adr/0026-eu-ssh-worker.md)); `infra/hetzner` creates one on Hetzner Cloud. Each worker has an SSH destination, an optional identity file and port, and a root under which it keeps `workers/<name>/repos`, `attempts`, and `credentials`. The machine needs Git, gh, the roster's `claude` and `codex` CLIs, and their logins for the SSH user.
|
|
124
|
+
|
|
125
|
+
Preflight changes nothing. It checks the SSH connection, a writable root, Git, memory for `maxConcurrentAttempts`, and the roster's harnesses where children run.
|
|
126
|
+
|
|
127
|
+
Prepare mints the attempt's single-repo token through the `CredentialProvider`, writes it with a gh `hosts.yml` to `<root>/workers/<name>/credentials/<attempt>`, and fetches the bare cached clone from `https://github.com/<repository>.git` with git's credential helper reading that file. The token goes over the writing command's stdin, so it's in no command line, process environment, or log, and git and gh output is scrubbed of it. As on the local worker, git and harvest commands renew a token that expires within five minutes; children, harvesting, and the salvage push export the same file paths, so they read the latest token. Cleanup stops the children, removes the checkout, then the credential directory. A process's first use of a worker, for an attempt or startup reconciliation, removes credentials an earlier process left; a reattached attempt gets a fresh one.
|
|
128
|
+
|
|
129
|
+
Children, pid files, the kill script, sweeps, salvage, recovery, and reattachment share the exe.dev worker's code.
|
|
130
|
+
|
|
131
|
+
### Behind a gateway
|
|
132
|
+
|
|
133
|
+
A worker whose address has a `gateway` is on a private network behind a gateway ([ADR 0027](../../docs/adr/0027-gateway-credential-proxy.md)), with no GitHub token on its machine. Prepare mints the same token, attaches it through the gateway's admin API for the worker and repo (`POST /v1/attachments` with the expiry), and keeps the attachment id and returned caller token. Renewal replaces the token on that attachment (`PUT /v1/attachments/<id>`) before expiry, and every two minutes the same request stores the attachment again, so a restarted gateway gets it back. Cleanup detaches after the checkout is cleaned up; a failed detach logs a warning, since the attachment expires with its token. Admin requests time out after 30 seconds. A process's first use of a worker detaches each attachment the gateway has for it, one by one, so a reattached attempt attaches again with a new caller token.
|
|
134
|
+
|
|
135
|
+
The cached clone fetches `http://git.<domain>/<repository>.git`. Children, harvesting, and the executor's git commands export settings that point git, gh, and package managers at the gateway: git sends the caller token only to `git.<domain>` and rewrites `https://github.com/` there, gh uses `git.<domain>` through the worker's socket with its config in `<root>/workers/<name>/gh`, and `HTTPS_PROXY` is the forward proxy.
|
|
136
|
+
|
|
137
|
+
If the gateway has the Anthropic credential, Claude Code calls `llm.<domain>/anthropic`, and preflight checks only its binary. Otherwise Claude Code, and Codex always, use the operator's login on the machine through the forward proxy: preflight checks the login as without a gateway, then that each of the login's hosts answers HTTP through the proxy (`api.anthropic.com` and `platform.claude.com`, or `chatgpt.com` and `auth.openai.com`). A refused host is a `gateway` capability error naming the host to add to the egress allowlist. The caller token is in no command line: each SSH command sends `GIT_CONFIG_VALUE_0`, `GH_ENTERPRISE_TOKEN`, `GH_TOKEN`, and `ANTHROPIC_AUTH_TOKEN` from the client's environment (`SendEnv`), and the machine's sshd must accept them (`AcceptEnv`).
|
|
138
|
+
|
|
139
|
+
The admin API listens on the gateway's loopback. With the first worker behind a gateway, the executor opens its own forward, `ssh -N -L 127.0.0.1:<port>:127.0.0.1:<adminPort> <destination>`, with the same SSH command and environment as its other clients, on a local port the kernel picks; workers behind that gateway share it. When the client exits, the executor starts another after a pause of one second, doubling up to one minute, and logs each open, exit, and reopen with the gateway's destination. An admin request that finds the forward closed, or gets no answer, reopens it at once and is sent once more. A second failure fails the request, with a message giving since when the forward has been closed, or, if it opens but the admin API doesn't answer, since when the API last answered through it. The forward closes when the last worker behind the gateway is removed, or with the layer.
|
|
140
|
+
|
|
141
|
+
Preflight also checks, without attaching, that the forward opens (giving the `ssh` command to run if it doesn't), that the admin API accepts the admin token, that sshd accepts the caller token variables, and that the machine reaches `git.<domain>`, which refuses requests without a caller token. A gap is a `gateway` `WorkerCapabilityError`.
|
|
142
|
+
|
|
143
|
+
If the worker's `llmProviders` lists `anthropic`, preflight also reads the gateway's Anthropic credential kind (`GET /v1/providers`) and records it with the worker. A gateway with none is a `gateway` capability error. For a Claude Code brain, each child reads the recorded kind as `AttemptRequest.gatewayCredential` when it starts (none for Codex and Pi), so its session records the credential, and usage counts a Claude Code child on the gateway's API key in the API keys pool ([ADR 0013](../../docs/adr/0013-cost-accounting-native-meters-guardrails.md)).
|
|
144
|
+
|
|
145
|
+
A gateway released before that endpoint answers 404: the worker's first such preflight logs `SSH Worker's Gateway does not report the kind of its Anthropic credential` at warning level, later ones at debug, and its Claude Code children count in the Claude plan pool. Each preflight asks again, so the first one after an upgrade records the kind. A gateway reload that changes the credential reaches the record at the worker's next preflight: at orchestrator start, or when a configuration reload adds the worker, gives it more slots, or adds a brain. A running attempt's next Claude Code child records the new kind; earlier children keep theirs. Local and exe.dev workers have no gateway, so their children record none.
|
|
146
|
+
|
|
147
|
+
The SSH layer needs a `ChildProcessSpawner`, a `CredentialProvider`, an `AttemptRuntime`, and an `HttpClient` for admin APIs. It provides `SshWorkers` beside the `Executor`, starting with the workers of `workers`. `addWorker(address)` adds a worker at runtime; behind a gateway, the worker shares that gateway's forward, or opens one if it's the first. `removeWorker(name)` removes a worker once no attempt runs on it, and closes its gateway's forward if no other worker uses it. Adding a worker the executor has with the same address changes nothing; with another address, it fails with a `machine` capability error until the worker is removed.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { SshDestination, SshIdentityFile, WorkerName, WorkerRoot } from "@usine/core/worker"
|
|
151
|
+
import { layer } from "@usine/executor/ssh"
|
|
152
|
+
import { Option } from "effect"
|
|
153
|
+
|
|
154
|
+
const executorLayer = layer({
|
|
155
|
+
workers: [
|
|
156
|
+
{
|
|
157
|
+
name: WorkerName.make("eu-one"),
|
|
158
|
+
destination: SshDestination.make("usine@203.0.113.7"),
|
|
159
|
+
identityFile: Option.some(SshIdentityFile.make("~/.ssh/usine_worker")),
|
|
160
|
+
port: Option.none(),
|
|
161
|
+
root: WorkerRoot.make("/home/usine/.usine"),
|
|
162
|
+
gateway: Option.none(),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
})
|
|
166
|
+
```
|
package/dist/child-command.d.ts
CHANGED
|
@@ -6,16 +6,13 @@ export interface CommandResult {
|
|
|
6
6
|
readonly stderr: string;
|
|
7
7
|
readonly stdout: string;
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Returns only the named process variables, so the App private key and every
|
|
11
|
-
* other orchestrator secret stay out of the child.
|
|
12
|
-
*/
|
|
9
|
+
/** Returns the variables in `names` that are set in the process environment. */
|
|
13
10
|
export declare const allowlistedEnvironment: (names: ReadonlyArray<string>) => Effect.Effect<Record<string, string>>;
|
|
14
11
|
/**
|
|
15
|
-
* Runs
|
|
16
|
-
*
|
|
12
|
+
* Runs `command` with a 10-minute timeout and returns its exit code and output without checking
|
|
13
|
+
* the exit code. Fails with `ExecutorOperationError` if the command can't run or times out.
|
|
17
14
|
*/
|
|
18
15
|
export declare const collectCommand: (spawner: ChildProcessSpawner.ChildProcessSpawner["Service"], operation: ExecutorOperation, command: string, args: ReadonlyArray<string>, options: ChildProcess.CommandOptions) => Effect.Effect<CommandResult, ExecutorOperationError>;
|
|
19
|
-
/** Returns the collected stdout
|
|
16
|
+
/** Returns the collected stdout. If the exit code is non-zero, logs a warning and fails with `ExecutorOperationError`. */
|
|
20
17
|
export declare const requireSuccess: (operation: "cancel" | "cleanup" | "launch" | "preflight" | "prepare", host: string, command: string, args: readonly string[], result: CommandResult) => Effect.Effect<string, ExecutorOperationError, never>;
|
|
21
18
|
//# sourceMappingURL=child-command.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"child-command.d.ts","sourceRoot":"","sources":["../src/child-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,MAAM,EAAyB,MAAM,QAAQ,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE/E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"child-command.d.ts","sourceRoot":"","sources":["../src/child-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,MAAM,EAAyB,MAAM,QAAQ,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE/E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAID,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,UAC1B,aAAa,CAAC,MAAM,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQpC,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,cAAc,YAChB,mBAAmB,CAAC,mBAAmB,CAAC,SAAS,CAAC,aAChD,iBAAiB,WACnB,MAAM,QACT,aAAa,CAAC,MAAM,CAAC,WAClB,YAAY,CAAC,cAAc,KACnC,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,sBAAsB,CA+BnD,CAAC;AAEJ,0HAA0H;AAC1H,eAAO,MAAM,cAAc,+MAsBzB,CAAC"}
|
package/dist/child-command.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { Array as Arr, Config, Effect, Fiber, Option, Stream } from "effect";
|
|
2
2
|
import { ChildProcess } from "effect/unstable/process";
|
|
3
3
|
import { ExecutorOperationError } from "./executor.js";
|
|
4
|
-
/** Bounds every adapter command so a hung clone or SSH hop stays cancellable. */
|
|
5
4
|
const COMMAND_TIMEOUT = "10 minutes";
|
|
5
|
+
/** Returns the variables in `names` that are set in the process environment. */
|
|
6
|
+
export const allowlistedEnvironment = (names) => Effect.forEach(names, (name) => Effect.map(Config.option(Config.string(name)), (value) => Option.map(value, (found) => [name, found]))).pipe(Effect.map((entries) => Object.fromEntries(Arr.getSomes(entries))), Effect.orDie);
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*/
|
|
10
|
-
export const allowlistedEnvironment = (names) => Effect.forEach(names, (name) => Effect.map(Config.option(Config.string(name)), (value) => Option.map(value, (found) => [name, found]))).pipe(Effect.map((entries) => Object.fromEntries(Arr.getSomes(entries))),
|
|
11
|
-
// An unreadable process environment at layer build is a defect, not a failure to handle.
|
|
12
|
-
Effect.orDie);
|
|
13
|
-
/**
|
|
14
|
-
* Runs one child process under the timeout and returns its whole output; the
|
|
15
|
-
* caller decides what the exit code means.
|
|
8
|
+
* Runs `command` with a 10-minute timeout and returns its exit code and output without checking
|
|
9
|
+
* the exit code. Fails with `ExecutorOperationError` if the command can't run or times out.
|
|
16
10
|
*/
|
|
17
11
|
export const collectCommand = (spawner, operation, command, args, options) => Effect.scoped(Effect.gen(function* () {
|
|
18
12
|
const handle = yield* spawner.spawn(ChildProcess.make(command, args, options));
|
|
@@ -26,10 +20,10 @@ export const collectCommand = (spawner, operation, command, args, options) => Ef
|
|
|
26
20
|
return { exitCode: Number(exitCode), stderr, stdout };
|
|
27
21
|
})).pipe(Effect.timeout(COMMAND_TIMEOUT), Effect.mapError((cause) => new ExecutorOperationError({
|
|
28
22
|
operation,
|
|
29
|
-
message:
|
|
23
|
+
message: `Can't run ${command}`,
|
|
30
24
|
cause,
|
|
31
25
|
})));
|
|
32
|
-
/** Returns the collected stdout
|
|
26
|
+
/** Returns the collected stdout. If the exit code is non-zero, logs a warning and fails with `ExecutorOperationError`. */
|
|
33
27
|
export const requireSuccess = Effect.fn("Executor.requireSuccess")(function* (operation, host, command, args, result) {
|
|
34
28
|
if (result.exitCode !== 0) {
|
|
35
29
|
yield* Effect.logWarning("Executor command exited non-zero", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"child-command.js","sourceRoot":"","sources":["../src/child-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC7E,OAAO,EAAE,YAAY,EAA4B,MAAM,yBAAyB,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAA0B,MAAM,eAAe,CAAC;AAQ/E,
|
|
1
|
+
{"version":3,"file":"child-command.js","sourceRoot":"","sources":["../src/child-command.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ClaudeLoginLock } from "@usine/agents/runtime";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* A `ClaudeLoginLock` with one mutex per Worker name. Each build creates new locks, so build it
|
|
5
|
+
* once per process and provide it to both the executor and the Attempt runtime.
|
|
6
|
+
*/
|
|
7
|
+
export declare const claudeLoginLockLayer: Layer.Layer<ClaudeLoginLock>;
|
|
8
|
+
//# sourceMappingURL=claude-login-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-login-lock.d.ts","sourceRoot":"","sources":["../src/claude-login-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAG/B;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,KAAK,CAAC,eAAe,CAG7D,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ClaudeLoginLock } from "@usine/agents/runtime";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
import { keyedLockFor } from "./keyed-lock.js";
|
|
4
|
+
/**
|
|
5
|
+
* A `ClaudeLoginLock` with one mutex per Worker name. Each build creates new locks, so build it
|
|
6
|
+
* once per process and provide it to both the executor and the Attempt runtime.
|
|
7
|
+
*/
|
|
8
|
+
export const claudeLoginLockLayer = Layer.effect(ClaudeLoginLock, keyedLockFor());
|
|
9
|
+
//# sourceMappingURL=claude-login-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-login-lock.js","sourceRoot":"","sources":["../src/claude-login-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAiC,KAAK,CAAC,MAAM,CAC5E,eAAe,EACf,YAAY,EAAc,CAC3B,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Returns the gh `hosts.yml` content that authenticates github.com with `token`. */
|
|
2
|
+
export declare const ghHostsConfig: (token: string) => string;
|
|
3
|
+
/** The gh `config.yml` content, required next to `hosts.yml`. */
|
|
4
|
+
export declare const GH_CONFIG = "version: \"1\"\n";
|
|
5
|
+
/**
|
|
6
|
+
* Returns the environment variables that point git and gh at the credential files in
|
|
7
|
+
* `directory`. git reads `directory/token` and gh reads `directory/hosts.yml` on each use, so
|
|
8
|
+
* renewing the token only requires rewriting those files.
|
|
9
|
+
*/
|
|
10
|
+
export declare const credentialEnvironment: (directory: string) => {
|
|
11
|
+
GH_CONFIG_DIR: string;
|
|
12
|
+
GH_PROMPT_DISABLED: string;
|
|
13
|
+
USINE_GITHUB_TOKEN_FILE: string;
|
|
14
|
+
GIT_CONFIG_COUNT: string;
|
|
15
|
+
GIT_CONFIG_KEY_0: string;
|
|
16
|
+
GIT_CONFIG_VALUE_0: string;
|
|
17
|
+
GIT_CONFIG_KEY_1: string;
|
|
18
|
+
GIT_CONFIG_VALUE_1: string;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=credential-environment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-environment.d.ts","sourceRoot":"","sources":["../src/credential-environment.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,eAAO,MAAM,aAAa,UAAW,MAAM,KAAG,MACyJ,CAAC;AAExM,iEAAiE;AACjE,eAAO,MAAM,SAAS,qBAAmB,CAAC;AAE1C;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,cAAe,MAAM;;;;;;;;;CAWlB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Returns the gh `hosts.yml` content that authenticates github.com with `token`. */
|
|
2
|
+
export const ghHostsConfig = (token) => `github.com:\n user: x-access-token\n oauth_token: ${JSON.stringify(token)}\n git_protocol: https\n users:\n x-access-token:\n oauth_token: ${JSON.stringify(token)}\n`;
|
|
3
|
+
/** The gh `config.yml` content, required next to `hosts.yml`. */
|
|
4
|
+
export const GH_CONFIG = 'version: "1"\n';
|
|
5
|
+
/**
|
|
6
|
+
* Returns the environment variables that point git and gh at the credential files in
|
|
7
|
+
* `directory`. git reads `directory/token` and gh reads `directory/hosts.yml` on each use, so
|
|
8
|
+
* renewing the token only requires rewriting those files.
|
|
9
|
+
*/
|
|
10
|
+
export const credentialEnvironment = (directory) => ({
|
|
11
|
+
GH_CONFIG_DIR: directory,
|
|
12
|
+
GH_PROMPT_DISABLED: "1",
|
|
13
|
+
USINE_GITHUB_TOKEN_FILE: `${directory}/token`,
|
|
14
|
+
GIT_CONFIG_COUNT: "2",
|
|
15
|
+
GIT_CONFIG_KEY_0: "credential.helper",
|
|
16
|
+
GIT_CONFIG_VALUE_0: "",
|
|
17
|
+
GIT_CONFIG_KEY_1: "credential.helper",
|
|
18
|
+
GIT_CONFIG_VALUE_1: '!f() { if [ "$1" = get ]; then echo username=x-access-token; printf "password="; cat "$USINE_GITHUB_TOKEN_FILE"; echo; fi; }; f',
|
|
19
|
+
});
|
|
20
|
+
//# sourceMappingURL=credential-environment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-environment.js","sourceRoot":"","sources":["../src/credential-environment.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAa,EAAU,EAAE,CACrD,2DAA2D,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,4FAA4F,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;AAExM,iEAAiE;AACjE,MAAM,CAAC,MAAM,SAAS,GAAG,gBAAgB,CAAC;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,SAAiB,EAAE,EAAE,CACzD,CAAC;IACC,aAAa,EAAE,SAAS;IACxB,kBAAkB,EAAE,GAAG;IACvB,uBAAuB,EAAE,GAAG,SAAS,QAAQ;IAC7C,gBAAgB,EAAE,GAAG;IACrB,gBAAgB,EAAE,mBAAmB;IACrC,kBAAkB,EAAE,EAAE;IACtB,gBAAgB,EAAE,mBAAmB;IACrC,kBAAkB,EAChB,iIAAiI;CACpI,CAAkC,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ChildHost } from "@usine/agents/child-host";
|
|
2
|
+
import * as AttemptChannel from "@usine/core/attempt/channel";
|
|
3
|
+
import { AgentFailure, type AttemptFailure, InfraFailure, RunawayFailure } from "@usine/core/attempt/failure";
|
|
4
|
+
import type { AttemptRecovery, RecoverySink } from "@usine/core/attempt/salvage";
|
|
5
|
+
import type { GatewayCredentialKind } from "@usine/core/worker";
|
|
6
|
+
import type { AttemptWindowsRead, HarvestSpec } from "@usine/core/step/harvest";
|
|
7
|
+
import type { Comment, StepResult, StepResultFact } from "@usine/core/step/result";
|
|
8
|
+
import { AttemptRuntime } from "@usine/durable/attempt-runtime";
|
|
9
|
+
import { Effect, Option, Result, type Scope } from "effect";
|
|
10
|
+
import { type AttemptWorkspace, type CheckoutRequest, type ExecutorError, ExecutorOperationError } from "./executor.ts";
|
|
11
|
+
import { type AcquiredAttempt } from "./prepared-attempt.ts";
|
|
12
|
+
export interface DurableCheckout {
|
|
13
|
+
/** The host with the checkout, where the children run. */
|
|
14
|
+
readonly host: ChildHost;
|
|
15
|
+
/** The complete environment for the children and harvesting commands (ADR 0011). */
|
|
16
|
+
readonly environment: Record<string, string>;
|
|
17
|
+
/**
|
|
18
|
+
* Returns the credential kind the Gateway injects for the children, from the Worker's latest
|
|
19
|
+
* preflight. Returns `None` if the children use their harness's own login.
|
|
20
|
+
*/
|
|
21
|
+
readonly gatewayCredential: Effect.Effect<Option.Option<GatewayCredentialKind>>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns the checkout's commits and pull requests, and the comments the Attempt posted on its
|
|
24
|
+
* Item within its own window and outside the windows of the Agent's other Attempts. Reads
|
|
25
|
+
* `windows` after GitHub lists the comments, and fails if they can't be read.
|
|
26
|
+
*/
|
|
27
|
+
readonly harvest: (windows: AttemptWindowsRead<AttemptFailure>) => Effect.Effect<ReadonlyArray<StepResultFact>, AttemptFailure>;
|
|
28
|
+
/**
|
|
29
|
+
* Returns only the comments, filtered as in `harvest`, for an unsuccessful Attempt. Returns none
|
|
30
|
+
* if GitHub fails or the windows can't be read.
|
|
31
|
+
*/
|
|
32
|
+
readonly harvestComments: (windows: AttemptWindowsRead<AttemptFailure>) => Effect.Effect<ReadonlyArray<Comment>>;
|
|
33
|
+
/** Keeps the checkout on disk so a restart can reattach the parent or repeat an unrecorded harvest. */
|
|
34
|
+
readonly keepForReattach: Effect.Effect<void>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A function that returns the comments the Attempt described by `spec` posted on its Item, read
|
|
38
|
+
* from its checkout with a new credential and filtered by `windows`. Fails only if the
|
|
39
|
+
* credential can't be acquired.
|
|
40
|
+
*/
|
|
41
|
+
export type PostedCommentsHarvest = <W>(spec: HarvestSpec, windows: AttemptWindowsRead<W>) => Effect.Effect<ReadonlyArray<Comment>, ExecutorError>;
|
|
42
|
+
export interface DurableSessionOptions {
|
|
43
|
+
/** The timeout for each cancellation stage: the parent task's abort, then the Attempt's end. */
|
|
44
|
+
readonly interruptGrace: Parameters<typeof Effect.timeoutOption>[1];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns the session that runs Attempts whose Pi parent is a durable task in this process
|
|
48
|
+
* (`AttemptRuntime`), for every Worker kind. If a launch is interrupted without a stop, the
|
|
49
|
+
* parent task stays live and the checkout stays on disk for reattachment after restart; every
|
|
50
|
+
* other ending stops the parent task.
|
|
51
|
+
*/
|
|
52
|
+
export declare const durableSessionFor: (options: DurableSessionOptions) => Effect.Effect<{
|
|
53
|
+
readonly launch: (prepared: AttemptWorkspace, io: AttemptChannel.Sink, checkout: DurableCheckout) => Effect.Effect<StepResult, AgentFailure | import("@usine/core/attempt/failure").BlockedFailure | import("@usine/core/attempt/failure").ChildSessionBudgetFailure | import("@usine/core/attempt/failure").DefectFailure | InfraFailure | import("@usine/core/attempt/failure").QuestionFailure | RunawayFailure, never>;
|
|
54
|
+
readonly prepare: (acquire: Effect.Effect<AcquiredAttempt, ExecutorError, Scope.Scope>, request: CheckoutRequest) => Effect.Effect<import("./executor.ts").PreparedAttempt, import("./executor.ts").CredentialUnavailableError | ExecutorOperationError | import("./executor.ts").WorkerCapabilityError, Scope.Scope>;
|
|
55
|
+
readonly cancel: (attemptId: string & import("effect/Brand").Brand<"AttemptId">) => Effect.Effect<void, import("./executor.ts").CredentialUnavailableError | ExecutorOperationError | import("./executor.ts").WorkerCapabilityError, never>;
|
|
56
|
+
readonly intervene: (attemptId: string & import("effect/Brand").Brand<"AttemptId">, intervention: AttemptChannel.Intervention) => Effect.Effect<undefined, AgentFailure | import("@usine/core/attempt/failure").BlockedFailure | import("@usine/core/attempt/failure").ChildSessionBudgetFailure | import("@usine/core/attempt/failure").DefectFailure | InfraFailure | import("@usine/core/attempt/failure").QuestionFailure | RunawayFailure, never>;
|
|
57
|
+
readonly isLive: (attemptId: string & import("effect/Brand").Brand<"AttemptId">) => Effect.Effect<boolean, AgentFailure | import("@usine/core/attempt/failure").BlockedFailure | import("@usine/core/attempt/failure").ChildSessionBudgetFailure | import("@usine/core/attempt/failure").DefectFailure | InfraFailure | import("@usine/core/attempt/failure").QuestionFailure | RunawayFailure, never>;
|
|
58
|
+
readonly outcome: (attemptId: string & import("effect/Brand").Brand<"AttemptId">) => Effect.Effect<Option.Option<Result.Result<StepResult, AgentFailure | import("@usine/core/attempt/failure").BlockedFailure | import("@usine/core/attempt/failure").ChildSessionBudgetFailure | import("@usine/core/attempt/failure").DefectFailure | InfraFailure | import("@usine/core/attempt/failure").QuestionFailure | RunawayFailure>>, AgentFailure | import("@usine/core/attempt/failure").BlockedFailure | import("@usine/core/attempt/failure").ChildSessionBudgetFailure | import("@usine/core/attempt/failure").DefectFailure | InfraFailure | import("@usine/core/attempt/failure").QuestionFailure | RunawayFailure, never>;
|
|
59
|
+
readonly stopForRecovery: (attempt: AttemptRecovery) => Effect.Effect<void, ExecutorOperationError, never>;
|
|
60
|
+
readonly recoverPostedComments: <E>(attempt: AttemptRecovery, harvest: Option.Option<PostedCommentsHarvest>, record: Pick<RecoverySink<E>, "comments" | "windows">) => Effect.Effect<void, never, never>;
|
|
61
|
+
readonly storedRequest: (attemptId: string & import("effect/Brand").Brand<"AttemptId">) => Effect.Effect<import("@usine/durable/attempt-runtime").StoredRequest, ExecutorOperationError, never>;
|
|
62
|
+
}, never, AttemptRuntime>;
|
|
63
|
+
//# sourceMappingURL=durable-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-session.d.ts","sourceRoot":"","sources":["../src/durable-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,KAAK,cAAc,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,YAAY,EACZ,cAAc,EACf,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AACjF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACnF,OAAO,EAGL,cAAc,EAEf,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAIL,MAAM,EAIN,MAAM,EAEN,MAAM,EACN,KAAK,KAAK,EACX,MAAM,QAAQ,CAAC;AAChB,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,sBAAsB,EACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,eAAe,EAAyC,MAAM,uBAAuB,CAAC;AAcpG,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAAC;IAChF;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,CAChB,OAAO,EAAE,kBAAkB,CAAC,cAAc,CAAC,KACxC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC,CAAC;IAClE;;;OAGG;IACH,QAAQ,CAAC,eAAe,EAAE,CACxB,OAAO,EAAE,kBAAkB,CAAC,cAAc,CAAC,KACxC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3C,uGAAuG;IACvG,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CAC/C;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,EACpC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC;AAE1D,MAAM,WAAW,qBAAqB;IACpC,gGAAgG;IAChG,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC,OAAO,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;CACrE;AASD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;gCA8bjB,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC,WAC1D,eAAe;;;;;;qCAmGiE,CAAC;;yBAmG5F,CAAC"}
|