@sealant/sdk 0.28.0 → 0.31.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 +34 -1
- package/dist/effect/api-client.d.ts +92 -8
- package/dist/effect/operations.d.ts +32 -2
- package/dist/effect/operations.js +2 -0
- package/dist/facade/workspace.js +34 -1
- package/dist/internal/blueprint.d.ts +1 -0
- package/dist/internal/blueprint.js +26 -4
- package/dist/types.d.ts +88 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -208,6 +208,39 @@ Not covered, and worth saying plainly: a process that _deliberately_ writes a se
|
|
|
208
208
|
the repository or to a mount is producing ordinary workspace state, and the redactor masks captured
|
|
209
209
|
I/O, not files.
|
|
210
210
|
|
|
211
|
+
## Capture-sourced workspaces
|
|
212
|
+
|
|
213
|
+
A `capture` source (sealantd ADR-0015) mounts nothing and clones nothing. The workspace daemon
|
|
214
|
+
registers with a session channel, fetches the worktree's head plan, materialises it onto the
|
|
215
|
+
executor's own disk, claims the lease, and ships captures back over the channel — so a session can
|
|
216
|
+
run where no host path exists (a Cloudflare sandbox, a MicroVM) and outlive any one executor:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const workspace = await sealant.workspaces.create({
|
|
220
|
+
source: {
|
|
221
|
+
kind: "capture",
|
|
222
|
+
endpoint: "https://mend.example.com/session/s1",
|
|
223
|
+
worktreeId: "wt_1",
|
|
224
|
+
token: sessionToken, // the channel credential; secret
|
|
225
|
+
},
|
|
226
|
+
harness: claudeCode(),
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `token` is lifted out of the blueprint and rides the request top level as `captureToken`. The
|
|
231
|
+
control plane seals it beside `secretEnv` and delivers it through the same boot file, as
|
|
232
|
+
`SEALANT_CAPTURE_TOKEN`; it is never in the spec, `WorkspaceDetails`, argv, or container env.
|
|
233
|
+
- The channel address and worktree id reach the daemon as `SEALANT_CAPTURE_ENDPOINT` and
|
|
234
|
+
`SEALANT_CAPTURE_WORKTREE_ID`, with `SEALANT_WORKSPACE_SOURCE=capture`.
|
|
235
|
+
- `worktreeId` is optional. A standby executor — launched ahead of any worktree so it can
|
|
236
|
+
materialise the project base and a dependency cache, then be bound to a worktree at claim — omits
|
|
237
|
+
it; `SEALANT_CAPTURE_WORKTREE_ID` stays unset and the daemon takes the worktree from the channel's
|
|
238
|
+
plan answer.
|
|
239
|
+
- No mount allowlist applies. On Kubernetes the workspace root is an `emptyDir`; on Cloudflare the
|
|
240
|
+
sandbox is kept alive while the lease is live and a stop sends SIGTERM so the daemon can flush.
|
|
241
|
+
- **No restart in place**: the credential is discarded once the launch settles, so `restart()` is
|
|
242
|
+
refused; create a replacement workspace with a fresh token.
|
|
243
|
+
|
|
211
244
|
## Dotfiles and shell
|
|
212
245
|
|
|
213
246
|
Bring your own environment: a login shell and dotfiles applied before the workspace accepts work.
|
|
@@ -256,7 +289,7 @@ with readable failures:
|
|
|
256
289
|
with `baseImage`.
|
|
257
290
|
- **Node.js + npm** at or above the harness CLIs' floor (the CLIs are installed with
|
|
258
291
|
`npm install -g` and run on the base's node).
|
|
259
|
-
- **git**, for clone- and
|
|
292
|
+
- **git**, for clone-, mount- and capture-sourced workspaces.
|
|
260
293
|
|
|
261
294
|
`packages` still works: names pass through **verbatim** (no portable-name resolution) to the base's
|
|
262
295
|
own package manager — `apt`, `apk`, `dnf`, or `pacman`, autodetected — and the build fails with a
|
|
@@ -1128,6 +1128,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
|
|
|
1128
1128
|
readonly secretEnv?: {
|
|
1129
1129
|
readonly [x: string]: string;
|
|
1130
1130
|
} | undefined;
|
|
1131
|
+
readonly captureToken?: string | undefined;
|
|
1131
1132
|
readonly ttlSeconds?: number | undefined;
|
|
1132
1133
|
};
|
|
1133
1134
|
readonly responseMode?: Mode;
|
|
@@ -1189,6 +1190,27 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
|
|
|
1189
1190
|
readonly workspaceId: string;
|
|
1190
1191
|
readonly expiresAt: string | null;
|
|
1191
1192
|
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
1193
|
+
readonly flushWorkspaceCapture: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
1194
|
+
readonly params: {
|
|
1195
|
+
readonly workspaceId: string;
|
|
1196
|
+
};
|
|
1197
|
+
readonly payload: {
|
|
1198
|
+
readonly ownerUserId: string;
|
|
1199
|
+
};
|
|
1200
|
+
readonly responseMode?: Mode;
|
|
1201
|
+
}) => Effect.Effect<HttpApiClient.Client.Response<{
|
|
1202
|
+
readonly epoch: number;
|
|
1203
|
+
readonly worktreeId: string;
|
|
1204
|
+
readonly headN?: number | undefined;
|
|
1205
|
+
readonly pending: number;
|
|
1206
|
+
readonly stagedBytes: number;
|
|
1207
|
+
readonly uploadedObjects: number;
|
|
1208
|
+
readonly uploadedBytes: number;
|
|
1209
|
+
readonly registered: number;
|
|
1210
|
+
readonly fenced: boolean;
|
|
1211
|
+
readonly paused: boolean;
|
|
1212
|
+
readonly lastSnapUnixMs?: number | undefined;
|
|
1213
|
+
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
1192
1214
|
readonly getWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
1193
1215
|
readonly params: {
|
|
1194
1216
|
readonly workspaceId: string;
|
|
@@ -1206,7 +1228,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
|
|
|
1206
1228
|
readonly repository?: string | undefined;
|
|
1207
1229
|
readonly tag?: string | undefined;
|
|
1208
1230
|
readonly runtime?: {
|
|
1209
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
1231
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
1210
1232
|
readonly resourceId: string;
|
|
1211
1233
|
readonly reference: string;
|
|
1212
1234
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -1241,7 +1263,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
|
|
|
1241
1263
|
readonly workspaceId: string;
|
|
1242
1264
|
readonly attemptId: string;
|
|
1243
1265
|
readonly runtime: {
|
|
1244
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
1266
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
1245
1267
|
readonly resourceId: string;
|
|
1246
1268
|
readonly reference: string;
|
|
1247
1269
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -1264,7 +1286,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
|
|
|
1264
1286
|
readonly triggerType: "api" | "manual" | "retry" | "schedule";
|
|
1265
1287
|
readonly triggerRef?: string | undefined;
|
|
1266
1288
|
readonly runtime?: {
|
|
1267
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
1289
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
1268
1290
|
readonly resourceId: string;
|
|
1269
1291
|
readonly reference: string;
|
|
1270
1292
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -1325,7 +1347,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
|
|
|
1325
1347
|
readonly repository?: string | undefined;
|
|
1326
1348
|
readonly tag?: string | undefined;
|
|
1327
1349
|
readonly runtime?: {
|
|
1328
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
1350
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
1329
1351
|
readonly resourceId: string;
|
|
1330
1352
|
readonly reference: string;
|
|
1331
1353
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -1360,6 +1382,26 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
|
|
|
1360
1382
|
readonly name: string;
|
|
1361
1383
|
readonly updatedAt: string;
|
|
1362
1384
|
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
1385
|
+
readonly replanWorkspaceCapture: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
1386
|
+
readonly params: {
|
|
1387
|
+
readonly workspaceId: string;
|
|
1388
|
+
};
|
|
1389
|
+
readonly payload: {
|
|
1390
|
+
readonly ownerUserId: string;
|
|
1391
|
+
};
|
|
1392
|
+
readonly responseMode?: Mode;
|
|
1393
|
+
}) => Effect.Effect<HttpApiClient.Client.Response<{
|
|
1394
|
+
readonly worktreeId: string;
|
|
1395
|
+
readonly epoch: number;
|
|
1396
|
+
readonly headN?: number | undefined;
|
|
1397
|
+
readonly headCaptureId?: string | undefined;
|
|
1398
|
+
readonly filesWritten: number;
|
|
1399
|
+
readonly bytesWritten: number;
|
|
1400
|
+
readonly filesSkipped: number;
|
|
1401
|
+
readonly bytesSkipped: number;
|
|
1402
|
+
readonly removed: number;
|
|
1403
|
+
readonly unchanged: boolean;
|
|
1404
|
+
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
1363
1405
|
readonly restartWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
1364
1406
|
readonly params: {
|
|
1365
1407
|
readonly workspaceId: string;
|
|
@@ -2514,6 +2556,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
|
|
|
2514
2556
|
readonly secretEnv?: {
|
|
2515
2557
|
readonly [x: string]: string;
|
|
2516
2558
|
} | undefined;
|
|
2559
|
+
readonly captureToken?: string | undefined;
|
|
2517
2560
|
readonly ttlSeconds?: number | undefined;
|
|
2518
2561
|
};
|
|
2519
2562
|
readonly responseMode?: Mode;
|
|
@@ -2575,6 +2618,27 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
|
|
|
2575
2618
|
readonly workspaceId: string;
|
|
2576
2619
|
readonly expiresAt: string | null;
|
|
2577
2620
|
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
2621
|
+
readonly flushWorkspaceCapture: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
2622
|
+
readonly params: {
|
|
2623
|
+
readonly workspaceId: string;
|
|
2624
|
+
};
|
|
2625
|
+
readonly payload: {
|
|
2626
|
+
readonly ownerUserId: string;
|
|
2627
|
+
};
|
|
2628
|
+
readonly responseMode?: Mode;
|
|
2629
|
+
}) => Effect.Effect<HttpApiClient.Client.Response<{
|
|
2630
|
+
readonly epoch: number;
|
|
2631
|
+
readonly worktreeId: string;
|
|
2632
|
+
readonly headN?: number | undefined;
|
|
2633
|
+
readonly pending: number;
|
|
2634
|
+
readonly stagedBytes: number;
|
|
2635
|
+
readonly uploadedObjects: number;
|
|
2636
|
+
readonly uploadedBytes: number;
|
|
2637
|
+
readonly registered: number;
|
|
2638
|
+
readonly fenced: boolean;
|
|
2639
|
+
readonly paused: boolean;
|
|
2640
|
+
readonly lastSnapUnixMs?: number | undefined;
|
|
2641
|
+
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
2578
2642
|
readonly getWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
2579
2643
|
readonly params: {
|
|
2580
2644
|
readonly workspaceId: string;
|
|
@@ -2592,7 +2656,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
|
|
|
2592
2656
|
readonly repository?: string | undefined;
|
|
2593
2657
|
readonly tag?: string | undefined;
|
|
2594
2658
|
readonly runtime?: {
|
|
2595
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
2659
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
2596
2660
|
readonly resourceId: string;
|
|
2597
2661
|
readonly reference: string;
|
|
2598
2662
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -2627,7 +2691,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
|
|
|
2627
2691
|
readonly workspaceId: string;
|
|
2628
2692
|
readonly attemptId: string;
|
|
2629
2693
|
readonly runtime: {
|
|
2630
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
2694
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
2631
2695
|
readonly resourceId: string;
|
|
2632
2696
|
readonly reference: string;
|
|
2633
2697
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -2650,7 +2714,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
|
|
|
2650
2714
|
readonly triggerType: "api" | "manual" | "retry" | "schedule";
|
|
2651
2715
|
readonly triggerRef?: string | undefined;
|
|
2652
2716
|
readonly runtime?: {
|
|
2653
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
2717
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
2654
2718
|
readonly resourceId: string;
|
|
2655
2719
|
readonly reference: string;
|
|
2656
2720
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -2711,7 +2775,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
|
|
|
2711
2775
|
readonly repository?: string | undefined;
|
|
2712
2776
|
readonly tag?: string | undefined;
|
|
2713
2777
|
readonly runtime?: {
|
|
2714
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
2778
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
2715
2779
|
readonly resourceId: string;
|
|
2716
2780
|
readonly reference: string;
|
|
2717
2781
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -2746,6 +2810,26 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
|
|
|
2746
2810
|
readonly name: string;
|
|
2747
2811
|
readonly updatedAt: string;
|
|
2748
2812
|
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
2813
|
+
readonly replanWorkspaceCapture: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
2814
|
+
readonly params: {
|
|
2815
|
+
readonly workspaceId: string;
|
|
2816
|
+
};
|
|
2817
|
+
readonly payload: {
|
|
2818
|
+
readonly ownerUserId: string;
|
|
2819
|
+
};
|
|
2820
|
+
readonly responseMode?: Mode;
|
|
2821
|
+
}) => Effect.Effect<HttpApiClient.Client.Response<{
|
|
2822
|
+
readonly worktreeId: string;
|
|
2823
|
+
readonly epoch: number;
|
|
2824
|
+
readonly headN?: number | undefined;
|
|
2825
|
+
readonly headCaptureId?: string | undefined;
|
|
2826
|
+
readonly filesWritten: number;
|
|
2827
|
+
readonly bytesWritten: number;
|
|
2828
|
+
readonly filesSkipped: number;
|
|
2829
|
+
readonly bytesSkipped: number;
|
|
2830
|
+
readonly removed: number;
|
|
2831
|
+
readonly unchanged: boolean;
|
|
2832
|
+
}, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
|
|
2749
2833
|
readonly restartWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
|
|
2750
2834
|
readonly params: {
|
|
2751
2835
|
readonly workspaceId: string;
|
|
@@ -28,6 +28,7 @@ export declare const createWorkspaceOp: (payload: {
|
|
|
28
28
|
readonly secretEnv?: {
|
|
29
29
|
readonly [x: string]: string;
|
|
30
30
|
} | undefined;
|
|
31
|
+
readonly captureToken?: string | undefined;
|
|
31
32
|
readonly ttlSeconds?: number | undefined;
|
|
32
33
|
}, idempotencyKey?: string | undefined) => Effect.Effect<{
|
|
33
34
|
readonly workspaceId: string;
|
|
@@ -46,7 +47,7 @@ export declare const getWorkspaceOp: (workspaceId: string, ownerUserId?: string
|
|
|
46
47
|
readonly repository?: string | undefined;
|
|
47
48
|
readonly tag?: string | undefined;
|
|
48
49
|
readonly runtime?: {
|
|
49
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
50
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
50
51
|
readonly resourceId: string;
|
|
51
52
|
readonly reference: string;
|
|
52
53
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -82,7 +83,7 @@ export declare const listWorkspacesOp: (query: {
|
|
|
82
83
|
readonly repository?: string | undefined;
|
|
83
84
|
readonly tag?: string | undefined;
|
|
84
85
|
readonly runtime?: {
|
|
85
|
-
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s";
|
|
86
|
+
readonly adapter: "cloudflare" | "docker" | "k3s" | "k8s" | "microvm";
|
|
86
87
|
readonly resourceId: string;
|
|
87
88
|
readonly reference: string;
|
|
88
89
|
readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
|
|
@@ -145,6 +146,35 @@ export declare const bindWorkspaceOp: (workspaceId: string, payload: {
|
|
|
145
146
|
readonly subpath: string;
|
|
146
147
|
}[];
|
|
147
148
|
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
|
|
149
|
+
export declare const flushWorkspaceCaptureOp: (workspaceId: string, payload: {
|
|
150
|
+
readonly ownerUserId: string;
|
|
151
|
+
}) => Effect.Effect<{
|
|
152
|
+
readonly epoch: number;
|
|
153
|
+
readonly worktreeId: string;
|
|
154
|
+
readonly headN?: number | undefined;
|
|
155
|
+
readonly pending: number;
|
|
156
|
+
readonly stagedBytes: number;
|
|
157
|
+
readonly uploadedObjects: number;
|
|
158
|
+
readonly uploadedBytes: number;
|
|
159
|
+
readonly registered: number;
|
|
160
|
+
readonly fenced: boolean;
|
|
161
|
+
readonly paused: boolean;
|
|
162
|
+
readonly lastSnapUnixMs?: number | undefined;
|
|
163
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
|
|
164
|
+
export declare const replanWorkspaceCaptureOp: (workspaceId: string, payload: {
|
|
165
|
+
readonly ownerUserId: string;
|
|
166
|
+
}) => Effect.Effect<{
|
|
167
|
+
readonly worktreeId: string;
|
|
168
|
+
readonly epoch: number;
|
|
169
|
+
readonly headN?: number | undefined;
|
|
170
|
+
readonly headCaptureId?: string | undefined;
|
|
171
|
+
readonly filesWritten: number;
|
|
172
|
+
readonly bytesWritten: number;
|
|
173
|
+
readonly filesSkipped: number;
|
|
174
|
+
readonly bytesSkipped: number;
|
|
175
|
+
readonly removed: number;
|
|
176
|
+
readonly unchanged: boolean;
|
|
177
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
|
|
148
178
|
export declare const stopWorkspaceOp: (workspaceId: string, payload: {
|
|
149
179
|
readonly ownerUserId: string;
|
|
150
180
|
}) => Effect.Effect<{
|
|
@@ -12,6 +12,8 @@ export const getWorkspaceOp = (workspaceId, ownerUserId) => Effect.flatMap(Seala
|
|
|
12
12
|
export const listWorkspacesOp = (query) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.listWorkspaces({ query }));
|
|
13
13
|
export const execWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.execWorkspace({ params: { workspaceId }, payload }));
|
|
14
14
|
export const bindWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.bindWorkspace({ params: { workspaceId }, payload }));
|
|
15
|
+
export const flushWorkspaceCaptureOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.flushWorkspaceCapture({ params: { workspaceId }, payload }));
|
|
16
|
+
export const replanWorkspaceCaptureOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.replanWorkspaceCapture({ params: { workspaceId }, payload }));
|
|
15
17
|
export const stopWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.stopWorkspace({ params: { workspaceId }, payload }));
|
|
16
18
|
export const restartWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.restartWorkspace({ params: { workspaceId }, payload }));
|
|
17
19
|
export const expireWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.expireWorkspace({ params: { workspaceId }, payload }));
|
package/dist/facade/workspace.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execWorkspace } from "../effect/exec-workspace.js";
|
|
2
|
-
import { bindWorkspaceOp, createSessionOp, expireWorkspaceOp, getSessionOp, getWorkspaceOp, listSessionsOp, restartWorkspaceOp, stopWorkspaceOp, } from "../effect/operations.js";
|
|
2
|
+
import { bindWorkspaceOp, flushWorkspaceCaptureOp, createSessionOp, expireWorkspaceOp, getSessionOp, getWorkspaceOp, listSessionsOp, replanWorkspaceCaptureOp, restartWorkspaceOp, stopWorkspaceOp, } from "../effect/operations.js";
|
|
3
3
|
import { SealantError, SealantNotImplementedError } from "../errors.js";
|
|
4
4
|
import { parseTtlSeconds } from "../internal/duration.js";
|
|
5
5
|
import { makeInteractiveSession } from "./session.js";
|
|
@@ -128,6 +128,39 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
128
128
|
}));
|
|
129
129
|
return result.binds.map((bind) => ({ mountPath: bind.mountPath, subpath: bind.subpath }));
|
|
130
130
|
},
|
|
131
|
+
capture: {
|
|
132
|
+
flush: async () => {
|
|
133
|
+
const status = await ctx.runtime.run(flushWorkspaceCaptureOp(init.id, { ownerUserId: ctx.config.hostLocal.ownerUserId }));
|
|
134
|
+
return {
|
|
135
|
+
epoch: status.epoch,
|
|
136
|
+
worktreeId: status.worktreeId,
|
|
137
|
+
...(status.headN === undefined ? {} : { headN: status.headN }),
|
|
138
|
+
pending: status.pending,
|
|
139
|
+
stagedBytes: status.stagedBytes,
|
|
140
|
+
uploadedObjects: status.uploadedObjects,
|
|
141
|
+
uploadedBytes: status.uploadedBytes,
|
|
142
|
+
registered: status.registered,
|
|
143
|
+
fenced: status.fenced,
|
|
144
|
+
paused: status.paused,
|
|
145
|
+
...(status.lastSnapUnixMs === undefined ? {} : { lastSnapUnixMs: status.lastSnapUnixMs }),
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
replan: async () => {
|
|
149
|
+
const result = await ctx.runtime.run(replanWorkspaceCaptureOp(init.id, { ownerUserId: ctx.config.hostLocal.ownerUserId }));
|
|
150
|
+
return {
|
|
151
|
+
worktreeId: result.worktreeId,
|
|
152
|
+
epoch: result.epoch,
|
|
153
|
+
...(result.headN === undefined ? {} : { headN: result.headN }),
|
|
154
|
+
...(result.headCaptureId === undefined ? {} : { headCaptureId: result.headCaptureId }),
|
|
155
|
+
filesWritten: result.filesWritten,
|
|
156
|
+
bytesWritten: result.bytesWritten,
|
|
157
|
+
filesSkipped: result.filesSkipped,
|
|
158
|
+
bytesSkipped: result.bytesSkipped,
|
|
159
|
+
removed: result.removed,
|
|
160
|
+
unchanged: result.unchanged,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
},
|
|
131
164
|
// Poll-backed lifecycle stream: emit a coarse event on each status transition until the workspace
|
|
132
165
|
// reaches a terminal/ready state. Swaps to SSE over Postgres LISTEN/NOTIFY in Stage 5 (same shape).
|
|
133
166
|
events: () => {
|
|
@@ -38,10 +38,13 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
38
38
|
throw new SealantError("workspaces.create requires exactly one of `repository` (a git remote to clone) or `source` (a caller-owned mount).", { code: "invalid_create_options" });
|
|
39
39
|
}
|
|
40
40
|
if (options.source !== undefined && options.ref !== undefined) {
|
|
41
|
-
throw new SealantError("`ref` applies only to `repository` sources, not mounts.", {
|
|
41
|
+
throw new SealantError("`ref` applies only to `repository` sources, not mounts or captures.", {
|
|
42
42
|
code: "invalid_create_options",
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
+
if (options.source?.kind === "capture" && options.source.token.trim().length === 0) {
|
|
46
|
+
throw new SealantError("A capture source needs a non-empty `token`: the session credential the daemon registers with.", { code: "invalid_create_options" });
|
|
47
|
+
}
|
|
45
48
|
if (options.os !== undefined && options.baseImage !== undefined) {
|
|
46
49
|
throw new SealantError("workspaces.create accepts either `os` (a managed OS family) or `baseImage` (a custom base image reference), not both.", { code: "invalid_create_options" });
|
|
47
50
|
}
|
|
@@ -57,14 +60,18 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
57
60
|
throw new SealantError("`dotfiles` requires a `repository`, at least one entry in `archives`, or both.", { code: "invalid_create_options" });
|
|
58
61
|
}
|
|
59
62
|
const sourceName = options.repository ??
|
|
60
|
-
(options.source?.kind === "standby"
|
|
63
|
+
(options.source?.kind === "standby"
|
|
64
|
+
? options.source.rootPath
|
|
65
|
+
: options.source?.kind === "capture"
|
|
66
|
+
? options.source.worktreeId
|
|
67
|
+
: options.source?.path) ??
|
|
61
68
|
"workspace";
|
|
62
69
|
const tail = sourceName
|
|
63
70
|
.split("/")
|
|
64
71
|
.filter((s) => s.length > 0)
|
|
65
72
|
.pop() ?? sourceName;
|
|
66
73
|
const credentials = mapWorkspaceCredentials(options.credentials);
|
|
67
|
-
const linkedWorktreeMount = options.source === undefined || options.source.kind
|
|
74
|
+
const linkedWorktreeMount = options.source === undefined || options.source.kind !== "mount"
|
|
68
75
|
? null
|
|
69
76
|
: discoverLinkedWorktreeMetadataMount(options.source.path);
|
|
70
77
|
const explicitMounts = options.mounts ?? [];
|
|
@@ -150,7 +157,19 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
150
157
|
}
|
|
151
158
|
: options.source?.kind === "standby"
|
|
152
159
|
? { kind: "standby", rootPath: options.source.rootPath }
|
|
153
|
-
:
|
|
160
|
+
: options.source?.kind === "capture"
|
|
161
|
+
? // The token is NOT here: it rides the request top level as `captureToken`.
|
|
162
|
+
{
|
|
163
|
+
kind: "capture",
|
|
164
|
+
endpoint: options.source.endpoint,
|
|
165
|
+
...(options.source.worktreeId === undefined
|
|
166
|
+
? {}
|
|
167
|
+
: { worktreeId: options.source.worktreeId }),
|
|
168
|
+
...(options.source.platform === undefined
|
|
169
|
+
? {}
|
|
170
|
+
: { platform: options.source.platform }),
|
|
171
|
+
}
|
|
172
|
+
: { kind: "mount", hostPath: options.source?.path },
|
|
154
173
|
...(dotfilesRepository === undefined
|
|
155
174
|
? {}
|
|
156
175
|
: {
|
|
@@ -218,6 +237,9 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
218
237
|
...(options.ttl === undefined ? {} : { ttlSeconds: parseTtlSeconds(options.ttl) }),
|
|
219
238
|
spec,
|
|
220
239
|
...(secretEnv === undefined ? {} : { secretEnv }),
|
|
240
|
+
// The capture credential is the one secret outside `secretEnv`: sealed and delivered the
|
|
241
|
+
// same way by the control plane, under its platform-owned name.
|
|
242
|
+
...(options.source?.kind === "capture" ? { captureToken: options.source.token } : {}),
|
|
221
243
|
},
|
|
222
244
|
};
|
|
223
245
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -131,6 +131,30 @@ export interface WorkspaceStandbySource {
|
|
|
131
131
|
/** Absolute, normalized deployment root (no `..` segments); container-visible in volume mode. */
|
|
132
132
|
readonly rootPath: string;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* A CAPTURE workspace (sealantd ADR-0015): the platform mounts nothing and clones nothing. The
|
|
136
|
+
* workspace daemon registers with the session channel at `endpoint` using `token`, fetches the
|
|
137
|
+
* worktree's head plan, materialises it onto the executor's own disk, claims the lease and ships
|
|
138
|
+
* captures back over the channel — so the same session can run on a host that has no path to
|
|
139
|
+
* offer (Cloudflare, a MicroVM) and outlive any one executor. `token` is delivered once, through
|
|
140
|
+
* the secret env channel (`SEALANT_CAPTURE_TOKEN`), never into the blueprint or any read; a
|
|
141
|
+
* capture workspace therefore cannot be restarted in place — create a replacement with a fresh
|
|
142
|
+
* token. `platform` is a hint recorded for placement bookkeeping. No allowlist applies.
|
|
143
|
+
*/
|
|
144
|
+
export interface WorkspaceCaptureSource {
|
|
145
|
+
readonly kind: "capture";
|
|
146
|
+
/** The session channel URL the daemon registers with. */
|
|
147
|
+
readonly endpoint: string;
|
|
148
|
+
/**
|
|
149
|
+
* The worktree whose captures this workspace materialises and extends. Omit for a standby
|
|
150
|
+
* executor launched before its worktree exists: the daemon then takes the worktree from the
|
|
151
|
+
* channel's plan answer, which names the one it is bound to at claim.
|
|
152
|
+
*/
|
|
153
|
+
readonly worktreeId?: string;
|
|
154
|
+
/** The session-scoped channel credential. Secret: sealed for the launch, then discarded. */
|
|
155
|
+
readonly token: string;
|
|
156
|
+
readonly platform?: string;
|
|
157
|
+
}
|
|
134
158
|
/**
|
|
135
159
|
* An ADDITIONAL caller-owned host directory bind-mounted beside the primary source — sibling
|
|
136
160
|
* repositories, reference clones, scratch material the workspace should see without adopting.
|
|
@@ -164,6 +188,63 @@ export interface WorkspaceBindOptions {
|
|
|
164
188
|
/** Relative path under the mount's root; an empty string unbinds. */
|
|
165
189
|
readonly subpath: string;
|
|
166
190
|
}
|
|
191
|
+
/** The daemon's capture status after `workspace.capture.flush()` (sealantd ADR-0015). */
|
|
192
|
+
export interface WorkspaceCaptureStatus {
|
|
193
|
+
/** The lease epoch the captures were shipped under. */
|
|
194
|
+
readonly epoch: number;
|
|
195
|
+
readonly worktreeId: string;
|
|
196
|
+
/** The head sequence registered on the session channel, once anything has been. */
|
|
197
|
+
readonly headN?: number;
|
|
198
|
+
/** Captures still staged and not yet shipped; zero after a complete flush. */
|
|
199
|
+
readonly pending: number;
|
|
200
|
+
readonly stagedBytes: number;
|
|
201
|
+
readonly uploadedObjects: number;
|
|
202
|
+
readonly uploadedBytes: number;
|
|
203
|
+
readonly registered: number;
|
|
204
|
+
/** The channel fenced this executor: nothing it captures from now on is accepted. */
|
|
205
|
+
readonly fenced: boolean;
|
|
206
|
+
/** The harness is paused (lease lost); see the daemon's lease semantics. */
|
|
207
|
+
readonly paused: boolean;
|
|
208
|
+
readonly lastSnapUnixMs?: number;
|
|
209
|
+
}
|
|
210
|
+
/** The daemon's answer to `workspace.capture.replan()` (sealantd 0.15 `capture.replan`). */
|
|
211
|
+
export interface WorkspaceCaptureReplanned {
|
|
212
|
+
/** The worktree the session channel's plan answered; the executor captures under it from now on. */
|
|
213
|
+
readonly worktreeId: string;
|
|
214
|
+
/** The lease epoch the plan answered. */
|
|
215
|
+
readonly epoch: number;
|
|
216
|
+
/** The head sequence the plan carried, once the worktree has one. */
|
|
217
|
+
readonly headN?: number;
|
|
218
|
+
/** The head capture the plan carried, once the worktree has one. */
|
|
219
|
+
readonly headCaptureId?: string;
|
|
220
|
+
/** What the delta materialise wrote: files whose bytes, mode or links differed from disk. */
|
|
221
|
+
readonly filesWritten: number;
|
|
222
|
+
readonly bytesWritten: number;
|
|
223
|
+
/** What it left alone: files already matching the plan on disk. */
|
|
224
|
+
readonly filesSkipped: number;
|
|
225
|
+
readonly bytesSkipped: number;
|
|
226
|
+
/** Files, symlinks and emptied directories the plan dropped, swept from the tree. */
|
|
227
|
+
readonly removed: number;
|
|
228
|
+
/** The plan named the worktree and epoch already in force; nothing moved. */
|
|
229
|
+
readonly unchanged: boolean;
|
|
230
|
+
}
|
|
231
|
+
/** Capture operations of a capture-sourced workspace. */
|
|
232
|
+
export interface WorkspaceCapture {
|
|
233
|
+
/**
|
|
234
|
+
* Final capture, then ship and register everything staged. Synchronous: resolves once the
|
|
235
|
+
* daemon has flushed (bounded by its grace window). Gate an executor's retirement on
|
|
236
|
+
* `pending === 0 && !fenced`. Refused on workspaces that are not capture-sourced.
|
|
237
|
+
*/
|
|
238
|
+
flush(): Promise<WorkspaceCaptureStatus>;
|
|
239
|
+
/**
|
|
240
|
+
* Re-plan: the daemon asks the session channel for its plan again with no worktree named,
|
|
241
|
+
* delta-materialises the answer over what is on disk, and captures under the answered worktree
|
|
242
|
+
* and epoch from then on (the fence lifts, foreign queue entries drop). The claim hook for a
|
|
243
|
+
* standby executor. Synchronous and idempotent (`unchanged: true`). Refused on workspaces that
|
|
244
|
+
* are not capture-sourced.
|
|
245
|
+
*/
|
|
246
|
+
replan(): Promise<WorkspaceCaptureReplanned>;
|
|
247
|
+
}
|
|
167
248
|
/** How a dotfiles tree is applied inside the workspace. */
|
|
168
249
|
export type WorkspaceDotfilesManager = "auto" | "chezmoi" | "stow" | "copy";
|
|
169
250
|
/**
|
|
@@ -231,8 +312,11 @@ export interface CreateOptions {
|
|
|
231
312
|
* Exactly one of `repository` or `source` must be provided.
|
|
232
313
|
*/
|
|
233
314
|
readonly repository?: string;
|
|
234
|
-
/**
|
|
235
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Alternative to `repository`: source the workspace from a caller-owned mount, a standby root,
|
|
317
|
+
* or a capture channel (see each source type).
|
|
318
|
+
*/
|
|
319
|
+
readonly source?: WorkspaceMountSource | WorkspaceStandbySource | WorkspaceCaptureSource;
|
|
236
320
|
/** Additional read-only-by-default mounts beside the primary source (see `WorkspaceExtraMount`). */
|
|
237
321
|
readonly mounts?: readonly WorkspaceExtraMount[];
|
|
238
322
|
/** The harness to run inside the workspace. */
|
|
@@ -378,6 +462,8 @@ export interface Workspace {
|
|
|
378
462
|
* binding, which each relaunch re-applies.
|
|
379
463
|
*/
|
|
380
464
|
bind(options: WorkspaceBindOptions): Promise<readonly WorkspaceBind[]>;
|
|
465
|
+
/** Capture flush for capture-sourced workspaces (sealantd ADR-0015). */
|
|
466
|
+
readonly capture: WorkspaceCapture;
|
|
381
467
|
/** Interactive PTY sessions: open new ones, reattach to existing ones by id. */
|
|
382
468
|
readonly sessions: WorkspaceSessions;
|
|
383
469
|
/** Lifecycle events as an async stream. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "The fluent public SDK for Sealant — create a workspace, run a harness, replay the record.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sealant/api-contracts": "^0.
|
|
29
|
+
"@sealant/api-contracts": "^0.31.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@effect/vitest": "4.0.0-beta.85",
|