@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 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 mount-sourced workspaces.
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 }));
@@ -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: () => {
@@ -29,6 +29,7 @@ export declare const buildCreateWorkspaceRequest: (options: CreateOptions, confi
29
29
  readonly secretEnv?: {
30
30
  readonly [x: string]: string;
31
31
  } | undefined;
32
+ readonly captureToken?: string | undefined;
32
33
  readonly ttlSeconds?: number | undefined;
33
34
  };
34
35
  };
@@ -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" ? options.source.rootPath : options.source?.path) ??
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 === "standby"
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
- : { kind: "mount", hostPath: options.source?.path },
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
- /** Alternative to `repository`: source the workspace from a caller-owned mount, or a standby root. */
235
- readonly source?: WorkspaceMountSource | WorkspaceStandbySource;
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.28.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.28.0"
29
+ "@sealant/api-contracts": "^0.31.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@effect/vitest": "4.0.0-beta.85",