@sealant/api-contracts-next 0.0.0-next.0 → 0.39.0-next.682
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/core-api/access-tokens.d.ts +126 -0
- package/dist/core-api/access-tokens.js +72 -0
- package/dist/core-api/budgets.d.ts +15 -0
- package/dist/core-api/budgets.js +13 -0
- package/dist/core-api/connected-accounts.d.ts +226 -0
- package/dist/core-api/connected-accounts.js +128 -0
- package/dist/core-api/control-plane.d.ts +1318 -0
- package/dist/core-api/control-plane.js +31 -0
- package/dist/core-api/github.d.ts +261 -0
- package/dist/core-api/github.js +153 -0
- package/dist/core-api/inference.d.ts +223 -0
- package/dist/core-api/inference.js +139 -0
- package/dist/core-api/oci-names.d.ts +21 -0
- package/dist/core-api/oci-names.js +44 -0
- package/dist/core-api/packages.d.ts +179 -0
- package/dist/core-api/packages.js +60 -0
- package/dist/core-api/profiles.d.ts +188 -0
- package/dist/core-api/profiles.js +88 -0
- package/dist/core-api/record-events.d.ts +209 -0
- package/dist/core-api/record-events.js +193 -0
- package/dist/core-api/registries.d.ts +106 -0
- package/dist/core-api/registries.js +85 -0
- package/dist/core-api/runs.d.ts +516 -0
- package/dist/core-api/runs.js +284 -0
- package/dist/core-api/sessions.d.ts +390 -0
- package/dist/core-api/sessions.js +264 -0
- package/dist/core-api/ssh-keys.d.ts +120 -0
- package/dist/core-api/ssh-keys.js +108 -0
- package/dist/core-api/system.d.ts +51 -0
- package/dist/core-api/system.js +41 -0
- package/dist/core-api/users.d.ts +71 -0
- package/dist/core-api/users.js +48 -0
- package/dist/core-api/workspaces.d.ts +1880 -0
- package/dist/core-api/workspaces.js +980 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/workspace-environment.d.ts +124 -0
- package/dist/workspace-environment.js +259 -0
- package/package.json +31 -9
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi";
|
|
3
|
+
import { BudgetExceededError } from "./budgets.js";
|
|
4
|
+
import { runCommandSchema, runSchema } from "./runs.js";
|
|
5
|
+
const NonEmptyString = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed());
|
|
6
|
+
/**
|
|
7
|
+
* `retained`: the workspace's executor ended (or its capture launch failed after it started)
|
|
8
|
+
* with work on its disk not confirmed saved. It is kept, not dead: the control plane drains or
|
|
9
|
+
* recovers it with the capture token it was launched with, and reports `stopped` or `failed`
|
|
10
|
+
* again only once the retention ends (saved and removed, discarded, or lost). A caller keeps
|
|
11
|
+
* what the recovery needs — the session's lease and token — while it reads `retained`. See
|
|
12
|
+
* `captureDrain.retained` for why and the recovery attempts.
|
|
13
|
+
*/
|
|
14
|
+
export const workspaceStatusSchema = Schema.Literals([
|
|
15
|
+
"queued",
|
|
16
|
+
"running",
|
|
17
|
+
"ready",
|
|
18
|
+
"failed",
|
|
19
|
+
"cancelled",
|
|
20
|
+
"stopped",
|
|
21
|
+
"retained",
|
|
22
|
+
]);
|
|
23
|
+
export const workspaceRuntimeSchema = Schema.Struct({
|
|
24
|
+
adapter: Schema.Literals(["docker", "k8s", "k3s", "cloudflare", "microvm"]),
|
|
25
|
+
/**
|
|
26
|
+
* The executor's identity on its runtime: the Docker container id, the Pod name, the MicroVM
|
|
27
|
+
* id. This is the id a caller records at launch and names in a stop's `completion`.
|
|
28
|
+
*/
|
|
29
|
+
resourceId: NonEmptyString,
|
|
30
|
+
reference: NonEmptyString,
|
|
31
|
+
/** `retained`: the executor is kept with its disk for recovery (see `workspaceStatusSchema`). */
|
|
32
|
+
status: Schema.Literals(["pending", "running", "ready", "failed", "stopped", "retained"]),
|
|
33
|
+
endpoint: Schema.optional(Schema.String),
|
|
34
|
+
/**
|
|
35
|
+
* ISO-8601 instant the runtime itself ends the executor, whatever anyone asks: a Lambda
|
|
36
|
+
* MicroVM's maximum duration from its start. `null` where the runtime imposes no lifetime
|
|
37
|
+
* (Docker, Kubernetes). A caller holding unsaved work on the executor drains before it. Absent
|
|
38
|
+
* only from control planes that predate it.
|
|
39
|
+
*/
|
|
40
|
+
deadline: Schema.optional(Schema.NullOr(Schema.String)),
|
|
41
|
+
/** The run (launch attempt) this executor belongs to. Absent from older control planes. */
|
|
42
|
+
runId: Schema.optional(NonEmptyString),
|
|
43
|
+
/** The launch identity the create named for this executor (`launchId`), when it named one. */
|
|
44
|
+
launchId: Schema.optional(NonEmptyString),
|
|
45
|
+
});
|
|
46
|
+
export const workspaceSshTargetSchema = Schema.Struct({
|
|
47
|
+
workspaceId: NonEmptyString,
|
|
48
|
+
attemptId: NonEmptyString,
|
|
49
|
+
runtime: Schema.Struct({
|
|
50
|
+
adapter: Schema.Literals(["docker", "k8s", "k3s", "cloudflare", "microvm"]),
|
|
51
|
+
resourceId: NonEmptyString,
|
|
52
|
+
reference: NonEmptyString,
|
|
53
|
+
status: Schema.Literals(["pending", "running", "ready", "failed", "stopped"]),
|
|
54
|
+
endpoint: Schema.String,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
export const workspacePublishedImageSchema = Schema.Struct({
|
|
58
|
+
reference: NonEmptyString,
|
|
59
|
+
digestReference: NonEmptyString,
|
|
60
|
+
digest: NonEmptyString,
|
|
61
|
+
});
|
|
62
|
+
export const workspaceErrorSchema = Schema.Struct({
|
|
63
|
+
message: Schema.String,
|
|
64
|
+
code: Schema.optional(NonEmptyString),
|
|
65
|
+
});
|
|
66
|
+
export const githubWorkspaceSourceSelectionSchema = Schema.Struct({
|
|
67
|
+
provider: Schema.Literal("github"),
|
|
68
|
+
installationId: NonEmptyString,
|
|
69
|
+
installationRepositoryId: NonEmptyString,
|
|
70
|
+
ref: Schema.optional(NonEmptyString),
|
|
71
|
+
});
|
|
72
|
+
// Connected-account selection (mirrors `newWorkspaceCredentialsSchema` in @sealant/validators):
|
|
73
|
+
// values are connected-account ids ("cacc_…") or per-provider account names; explicit per-provider
|
|
74
|
+
// entries win over the profile's bindings. Resolved server-side into opaque blueprint
|
|
75
|
+
// `credentialRefs` — no secret material ever appears in the request or the blueprint.
|
|
76
|
+
export const createWorkspaceCredentialsSchema = Schema.Struct({
|
|
77
|
+
profileId: Schema.optional(NonEmptyString),
|
|
78
|
+
claude: Schema.optional(NonEmptyString),
|
|
79
|
+
codex: Schema.optional(NonEmptyString),
|
|
80
|
+
github: Schema.optional(NonEmptyString),
|
|
81
|
+
});
|
|
82
|
+
export const createWorkspaceRequestSchema = Schema.Struct({
|
|
83
|
+
ownerUserId: NonEmptyString,
|
|
84
|
+
registryId: NonEmptyString,
|
|
85
|
+
repository: NonEmptyString,
|
|
86
|
+
tag: NonEmptyString,
|
|
87
|
+
name: Schema.optional(NonEmptyString),
|
|
88
|
+
sourceSelection: Schema.optional(githubWorkspaceSourceSelectionSchema),
|
|
89
|
+
dotfilesSelection: Schema.optional(githubWorkspaceSourceSelectionSchema),
|
|
90
|
+
credentials: Schema.optional(createWorkspaceCredentialsSchema),
|
|
91
|
+
spec: Schema.Unknown,
|
|
92
|
+
// The transient secret channel: validated by `parseWorkspaceSecretEnv`, encrypted at rest on
|
|
93
|
+
// the build job until launch, delivered to the workspace daemon as a boot file, and NEVER part
|
|
94
|
+
// of the blueprint/spec, the attempt snapshot, or any read response. See the SDK README.
|
|
95
|
+
secretEnv: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
96
|
+
// The capture source's session credential (sealantd ADR-0015): required with a `capture`
|
|
97
|
+
// workspace source and refused with any other. Sealed and delivered exactly like `secretEnv`,
|
|
98
|
+
// reaching the daemon's boot secret file as `SEALANT_CAPTURE_TOKEN`; never the spec, the attempt
|
|
99
|
+
// snapshot, or any read response. Not retained: a capture workspace cannot be restarted in place.
|
|
100
|
+
captureToken: Schema.optional(NonEmptyString),
|
|
101
|
+
// Per-create TTL override in seconds; when omitted the server default TTL (if configured)
|
|
102
|
+
// applies. The reaper stops the workspace once the TTL elapses.
|
|
103
|
+
ttlSeconds: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
104
|
+
/**
|
|
105
|
+
* Makes the create idempotent for this owner: a repeated create with the same key returns the
|
|
106
|
+
* workspace the first one made (`replayed: true`) instead of creating another, so a caller that
|
|
107
|
+
* lost the answer (a crash, a timeout) can repeat it or look the workspace up
|
|
108
|
+
* (`GET /v1/workspaces?idempotencyKey=`). Scoped to `ownerUserId`. The `idempotency-key` header
|
|
109
|
+
* means the same; this field wins when both are sent.
|
|
110
|
+
*/
|
|
111
|
+
idempotencyKey: Schema.optional(NonEmptyString),
|
|
112
|
+
/**
|
|
113
|
+
* The caller's immutable identity for the ONE physical executor this create launches, minted
|
|
114
|
+
* before create (a caller that makes the create idempotent can reuse its idempotency key). It is
|
|
115
|
+
* recorded on the launch attempt and reported with the executor (`runtime.launchId`); a stop's
|
|
116
|
+
* completion attestation that names a launch must name this one.
|
|
117
|
+
*/
|
|
118
|
+
launchId: Schema.optional(NonEmptyString),
|
|
119
|
+
});
|
|
120
|
+
export const createWorkspaceHeadersSchema = Schema.Struct({
|
|
121
|
+
"idempotency-key": Schema.optional(NonEmptyString),
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* The `harnessId` stamped on runs created by the deterministic-exec endpoint, so consumers can tell
|
|
125
|
+
* check runs apart from harness runs when listing/reading runs.
|
|
126
|
+
*/
|
|
127
|
+
export const execRunHarnessId = "exec";
|
|
128
|
+
/**
|
|
129
|
+
* Deterministic exec: run an ORDERED LIST of commands in the workspace, recorded as ONE run (a
|
|
130
|
+
* "check run") — e.g. a causal proof `base fails · head passes · revert fails` as three commands
|
|
131
|
+
* with three recorded exit codes.
|
|
132
|
+
*
|
|
133
|
+
* Semantics differ deliberately from harness runs: every command executes IN ORDER regardless of
|
|
134
|
+
* exit codes (a nonzero exit is a check DATUM, not an execution failure), and the run completes iff
|
|
135
|
+
* every command executed and was recorded. The run's `exitCode` is the LAST command's; per-command
|
|
136
|
+
* exit codes live in the execution record (`processExited` events). The run FAILS only when the
|
|
137
|
+
* execution machinery broke (workspace gone, transport dropped mid-command) — so `status` answers
|
|
138
|
+
* "can I trust these exit codes", not "did the checks pass".
|
|
139
|
+
*/
|
|
140
|
+
export const execWorkspaceRequestSchema = Schema.Struct({
|
|
141
|
+
ownerUserId: NonEmptyString,
|
|
142
|
+
/** Commands execute sequentially in the workspace, each recorded like any other process. */
|
|
143
|
+
commands: Schema.Array(runCommandSchema).check(Schema.isNonEmpty(), Schema.isMaxLength(32)),
|
|
144
|
+
});
|
|
145
|
+
/**
|
|
146
|
+
* Bind a standby workspace's working directory, or a bindable extra mount, to one subdirectory of
|
|
147
|
+
* its root (sealantd ADR-0014). `mountPath` defaults to the working directory; an empty `subpath`
|
|
148
|
+
* unbinds. The reply is the workspace's full set of live bindings, which every relaunch re-applies.
|
|
149
|
+
*/
|
|
150
|
+
export const bindWorkspaceRequestSchema = Schema.Struct({
|
|
151
|
+
ownerUserId: NonEmptyString,
|
|
152
|
+
mountPath: Schema.optional(NonEmptyString),
|
|
153
|
+
subpath: Schema.String,
|
|
154
|
+
});
|
|
155
|
+
export const workspaceBindSchema = Schema.Struct({
|
|
156
|
+
mountPath: NonEmptyString,
|
|
157
|
+
subpath: NonEmptyString,
|
|
158
|
+
});
|
|
159
|
+
export const workspaceBindsSchema = Schema.Struct({
|
|
160
|
+
binds: Schema.Array(workspaceBindSchema),
|
|
161
|
+
});
|
|
162
|
+
/** Which flush sealantd runs: `final` (the executor is ending) or `suspend` (a checkpoint). */
|
|
163
|
+
export const workspaceCaptureFlushKindSchema = Schema.Literals(["final", "suspend"]);
|
|
164
|
+
/**
|
|
165
|
+
* Flush a capture-sourced workspace's captures (sealantd ADR-0015 `capture.flush`): a capture,
|
|
166
|
+
* then everything staged is shipped and registered on the session channel. Synchronous over the
|
|
167
|
+
* daemon's control connection, bounded by `deadlineMs`. The reply is the daemon's capture status.
|
|
168
|
+
*
|
|
169
|
+
* `kind: "final"` says the executor is ending: the daemon stops its managed processes (SIGTERM,
|
|
170
|
+
* then SIGKILL after `graceMs`), snapshots both capture classes, ships, and reports `complete`.
|
|
171
|
+
* Only `complete === true` means the executor's work is saved. After a final flush the daemon
|
|
172
|
+
* refuses new work for good. A final flush that runs past its deadline answers `complete: false`
|
|
173
|
+
* and keeps shipping in the daemon, so later status reads and repeated finals converge.
|
|
174
|
+
* `kind: "suspend"` (the default) is a checkpoint: the executor keeps running.
|
|
175
|
+
*/
|
|
176
|
+
export const flushWorkspaceCaptureRequestSchema = Schema.Struct({
|
|
177
|
+
ownerUserId: NonEmptyString,
|
|
178
|
+
/** `final` or `suspend` (the default). */
|
|
179
|
+
kind: Schema.optional(workspaceCaptureFlushKindSchema),
|
|
180
|
+
/** How long the daemon may take before it answers, in milliseconds. Absent: its own default. */
|
|
181
|
+
deadlineMs: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
182
|
+
/**
|
|
183
|
+
* Final only: how long managed processes get between SIGTERM and SIGKILL, in milliseconds,
|
|
184
|
+
* counted inside `deadlineMs`. Absent: the daemon's default.
|
|
185
|
+
*/
|
|
186
|
+
graceMs: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
187
|
+
});
|
|
188
|
+
/**
|
|
189
|
+
* Read a capture-sourced workspace's capture status (sealantd `capture.status`) without flushing:
|
|
190
|
+
* what a drain polls to show `saving · N left` and to know when the executor may go away.
|
|
191
|
+
*/
|
|
192
|
+
export const getWorkspaceCaptureStatusQuerySchema = Schema.Struct({
|
|
193
|
+
ownerUserId: NonEmptyString,
|
|
194
|
+
});
|
|
195
|
+
export const captureClassSchema = Schema.Literals(["small", "bulk"]);
|
|
196
|
+
/** One capture class's snaps (sealantd `CaptureClassSnaps`). */
|
|
197
|
+
export const captureClassSnapsSchema = Schema.Struct({
|
|
198
|
+
class: captureClassSchema,
|
|
199
|
+
/** Snaps of this class that failed since the daemon started. */
|
|
200
|
+
snapsFailed: Schema.Number,
|
|
201
|
+
/** The last snap's error, while the last snap failed; absent once one succeeds. */
|
|
202
|
+
lastSnapError: Schema.optional(Schema.String),
|
|
203
|
+
/** When the current run of failed snaps began (Unix ms), while the last snap failed. */
|
|
204
|
+
snapFailingSinceUnixMs: Schema.optional(Schema.Number),
|
|
205
|
+
});
|
|
206
|
+
/**
|
|
207
|
+
* Where in an executor's own history it made an answer or a seal (sealantd's stamp, decision 17):
|
|
208
|
+
* its capture lease epoch, the launch it runs as, the daemon boot that answered, how many daemon
|
|
209
|
+
* boots opened its disk (0: unknown), an observation number that only grows within that boot, and
|
|
210
|
+
* the capture head. Evidence about one executor is ordered by it, never by any clock: of the same
|
|
211
|
+
* epoch, launch and boot by `observation`; of the same epoch and launch and different boots whose
|
|
212
|
+
* generations are both above 0 and differ, by (`bootGeneration`, `observation`); anything else
|
|
213
|
+
* cannot be ordered.
|
|
214
|
+
*/
|
|
215
|
+
export const captureExecutorOriginSchema = Schema.Struct({
|
|
216
|
+
epoch: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
217
|
+
launch: NonEmptyString,
|
|
218
|
+
bootId: NonEmptyString,
|
|
219
|
+
bootGeneration: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
220
|
+
observation: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
221
|
+
headN: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
222
|
+
});
|
|
223
|
+
/**
|
|
224
|
+
* A capture step running past its bound (sealantd `CaptureOverdue`): what is running, outermost
|
|
225
|
+
* first and `›`-separated (`small snap › git cat-file --batch-check`), when it started (Unix ms,
|
|
226
|
+
* display only), how long it had been running when the answer was made, and its bound (ms).
|
|
227
|
+
*/
|
|
228
|
+
export const captureOverdueSchema = Schema.Struct({
|
|
229
|
+
step: NonEmptyString,
|
|
230
|
+
startedUnixMs: Schema.Number,
|
|
231
|
+
runningMs: Schema.Number,
|
|
232
|
+
boundMs: Schema.Number,
|
|
233
|
+
});
|
|
234
|
+
export const workspaceCaptureStatusSchema = Schema.Struct({
|
|
235
|
+
epoch: Schema.Number,
|
|
236
|
+
worktreeId: NonEmptyString,
|
|
237
|
+
/** The newest capture the session channel has registered for this worktree. */
|
|
238
|
+
headN: Schema.optional(Schema.Number),
|
|
239
|
+
/** Captures staged on the executor and not yet registered: the unsaved queue. */
|
|
240
|
+
pending: Schema.Number,
|
|
241
|
+
stagedBytes: Schema.Number,
|
|
242
|
+
uploadedObjects: Schema.Number,
|
|
243
|
+
uploadedBytes: Schema.Number,
|
|
244
|
+
registered: Schema.Number,
|
|
245
|
+
fenced: Schema.Boolean,
|
|
246
|
+
paused: Schema.Boolean,
|
|
247
|
+
lastSnapUnixMs: Schema.optional(Schema.Number),
|
|
248
|
+
/**
|
|
249
|
+
* Capture classes the registrar refused for the session's byte quota: nothing of these ships
|
|
250
|
+
* until the next epoch or re-plan, whatever `pending` says. Non-empty means work is NOT being
|
|
251
|
+
* saved. Absent from control planes that predate it.
|
|
252
|
+
*/
|
|
253
|
+
refused: Schema.optional(Schema.Array(captureClassSchema)),
|
|
254
|
+
/**
|
|
255
|
+
* Bytes still to ship, and bulk captures still pending (sealantd's `pending_bytes` /
|
|
256
|
+
* `pending_bulk`). Every field below `refused` is absent until the daemon reports it (the
|
|
257
|
+
* control plane's pinned sealantd wire predates them).
|
|
258
|
+
*/
|
|
259
|
+
pendingBytes: Schema.optional(Schema.Number),
|
|
260
|
+
pendingBulk: Schema.optional(Schema.Number),
|
|
261
|
+
/**
|
|
262
|
+
* The daemon's own account of its last FINAL flush: true only when it quiesced every managed
|
|
263
|
+
* process, snapshotted both capture classes and registered everything. The only proof that an
|
|
264
|
+
* executor may go away; `pending === 0` alone is not. Absent until sealantd reports it (read
|
|
265
|
+
* absent as not complete).
|
|
266
|
+
*/
|
|
267
|
+
complete: Schema.optional(Schema.Boolean),
|
|
268
|
+
/**
|
|
269
|
+
* Why the last final flush is not complete (`not-final`, `in-progress`, `processes-remain`,
|
|
270
|
+
* `sweep-unavailable`, `snapshot-failed`, `unreadable`, `fenced`, `conflict`, `deadline`,
|
|
271
|
+
* `ship-failed`, `pending`, `internal`). A class whose last snap failed is `snapshot-failed`.
|
|
272
|
+
*/
|
|
273
|
+
incompleteReason: Schema.optional(Schema.String),
|
|
274
|
+
/**
|
|
275
|
+
* Paths the last snap of each class could not read (listed, stat'ed or opened), summed over
|
|
276
|
+
* both classes. Never taken as deleted: an automatic snap carries a path's last captured
|
|
277
|
+
* content forward, a final snap fails instead.
|
|
278
|
+
*/
|
|
279
|
+
unreadable: Schema.optional(Schema.Number),
|
|
280
|
+
/** Of `unreadable`, the paths whose last captured content was carried forward. */
|
|
281
|
+
carried: Schema.optional(Schema.Number),
|
|
282
|
+
/**
|
|
283
|
+
* The first unreadable paths (at most 20), virtual: `tree/<path>` under the worktree,
|
|
284
|
+
* `.git/<path>`, `harness/<path>`; small class first.
|
|
285
|
+
*/
|
|
286
|
+
unreadablePaths: Schema.optional(Schema.Array(Schema.String)),
|
|
287
|
+
/**
|
|
288
|
+
* A capture the registrar refused to register that the executor is working through:
|
|
289
|
+
* `missing-objects` (an object it names is not in the store) or `unrestorable` (a section's
|
|
290
|
+
* tree would not restore). Nothing is dropped: its objects are uploaded again and it is
|
|
291
|
+
* rebuilt from disk in its place.
|
|
292
|
+
*/
|
|
293
|
+
registerRefused: Schema.optional(Schema.String),
|
|
294
|
+
/** That refused capture's chain position. */
|
|
295
|
+
registerRefusedN: Schema.optional(Schema.Number),
|
|
296
|
+
/** The first keys (at most 20) the registrar named as missing. */
|
|
297
|
+
registerMissing: Schema.optional(Schema.Array(Schema.String)),
|
|
298
|
+
/** Register refusals the daemon has seen since it started. */
|
|
299
|
+
registerRefusals: Schema.optional(Schema.Number),
|
|
300
|
+
/** The refused capture waits to be rebuilt from disk; nothing behind it registers first. */
|
|
301
|
+
repairing: Schema.optional(Schema.Boolean),
|
|
302
|
+
/**
|
|
303
|
+
* A bulk build is in progress: its capture is not queued yet, so `pending` and `pendingBulk`
|
|
304
|
+
* do not count it (`pendingBytes` counts what it has staged). A drain is not done while true.
|
|
305
|
+
*/
|
|
306
|
+
bulkBuilding: Schema.optional(Schema.Boolean),
|
|
307
|
+
/**
|
|
308
|
+
* Each captured class's snaps: how many failed since the daemon started, and the last one's
|
|
309
|
+
* error while it fails. A snap that fails stages nothing: what changed since the last capture
|
|
310
|
+
* is on the executor's disk only.
|
|
311
|
+
*/
|
|
312
|
+
snaps: Schema.optional(Schema.Array(captureClassSnapsSchema)),
|
|
313
|
+
/**
|
|
314
|
+
* Derived from `snaps`: the error of the class that has been failing longest. Present means the
|
|
315
|
+
* executor's newest work is NOT being captured, whatever `pending` says.
|
|
316
|
+
*/
|
|
317
|
+
lastSnapError: Schema.optional(Schema.String),
|
|
318
|
+
/** Derived from `snaps`: when the earliest current run of failed snaps began (Unix ms). */
|
|
319
|
+
snapFailingSinceUnixMs: Schema.optional(Schema.Number),
|
|
320
|
+
/** Derived from `snaps`: failed snaps of every class since the daemon started. */
|
|
321
|
+
snapsFailed: Schema.optional(Schema.Number),
|
|
322
|
+
/**
|
|
323
|
+
* Where in the executor's own history this answer was made. Absent from a daemon that predates
|
|
324
|
+
* the stamp: such answers cannot be ordered by position.
|
|
325
|
+
*/
|
|
326
|
+
origin: Schema.optional(captureExecutorOriginSchema),
|
|
327
|
+
/**
|
|
328
|
+
* A capture step past its bound, while one is: the executor's capture is stuck there now. Not a
|
|
329
|
+
* verdict: the step's own limit ends it and the snap fails (`snaps`). Absent while nothing is
|
|
330
|
+
* past its bound, and from a daemon or control plane that predates it.
|
|
331
|
+
*/
|
|
332
|
+
overdue: Schema.optional(captureOverdueSchema),
|
|
333
|
+
});
|
|
334
|
+
/**
|
|
335
|
+
* Re-plan a capture-sourced workspace (sealantd 0.15 `capture.replan`, the claim hook): the daemon
|
|
336
|
+
* asks the session channel for its plan again with no worktree named, delta-materialises the
|
|
337
|
+
* answer over what is on disk, and captures under the answered worktree and epoch from then on.
|
|
338
|
+
* Synchronous over the daemon's control connection. Idempotent: `unchanged` is true when the
|
|
339
|
+
* answer named the worktree and epoch already in force.
|
|
340
|
+
*/
|
|
341
|
+
export const replanWorkspaceCaptureRequestSchema = Schema.Struct({
|
|
342
|
+
ownerUserId: NonEmptyString,
|
|
343
|
+
});
|
|
344
|
+
export const workspaceCaptureReplannedSchema = Schema.Struct({
|
|
345
|
+
worktreeId: NonEmptyString,
|
|
346
|
+
epoch: Schema.Number,
|
|
347
|
+
headN: Schema.optional(Schema.Number),
|
|
348
|
+
headCaptureId: Schema.optional(NonEmptyString),
|
|
349
|
+
filesWritten: Schema.Number,
|
|
350
|
+
bytesWritten: Schema.Number,
|
|
351
|
+
filesSkipped: Schema.Number,
|
|
352
|
+
bytesSkipped: Schema.Number,
|
|
353
|
+
removed: Schema.Number,
|
|
354
|
+
unchanged: Schema.Boolean,
|
|
355
|
+
});
|
|
356
|
+
export const renameWorkspaceRequestSchema = Schema.Struct({
|
|
357
|
+
name: NonEmptyString,
|
|
358
|
+
/** The workspace's owner. Required by the control plane: a rename that names none finds nothing. */
|
|
359
|
+
ownerUserId: Schema.optional(NonEmptyString),
|
|
360
|
+
});
|
|
361
|
+
// Lifecycle actions are owner-scoped like execWorkspace: ownerUserId rides in the payload and a
|
|
362
|
+
// mismatch yields a uniform 404 (existence is not leaked).
|
|
363
|
+
export const stopWorkspaceRequestSchema = Schema.Struct({
|
|
364
|
+
ownerUserId: NonEmptyString,
|
|
365
|
+
/**
|
|
366
|
+
* End the workspace WITHOUT saving its unsaved captures. A capture-sourced workspace is
|
|
367
|
+
* otherwise drained before it stops, and kept running for as long as its work cannot be
|
|
368
|
+
* confirmed saved; this is the owner's explicit way out. The request is recorded (who, when)
|
|
369
|
+
* and every stop path honours it: the runtime is terminated at once, and the workspace's
|
|
370
|
+
* `captureDrain` reads `discarded`. Accepted on a workspace already stopped whose runtime is
|
|
371
|
+
* still up (a kept one). Owner only.
|
|
372
|
+
*/
|
|
373
|
+
discardUnsaved: Schema.optional(Schema.Boolean),
|
|
374
|
+
/**
|
|
375
|
+
* The caller's attestation that its capture store holds a SEALED final capture of this
|
|
376
|
+
* workspace's current executor: a FINAL flush that completed (every writer stopped, both
|
|
377
|
+
* classes snapshotted, everything registered) and was recorded durably by the store. It is
|
|
378
|
+
* permission to remove that executor's disk once it has ended, even when this control plane
|
|
379
|
+
* never read `complete: true` from it itself (the FINAL reply was lost, the daemon exited
|
|
380
|
+
* before a drain reached it). Accepted only when `executorId` names the current executor — the
|
|
381
|
+
* run id, or the runtime's `resourceId` / `reference` (`workspace.details().runtime`) — and
|
|
382
|
+
* nothing the control plane observed from the executor contradicts it: an older `epoch` than
|
|
383
|
+
* the executor reported, a capture past `captureN`, or — in the same epoch — a report that its
|
|
384
|
+
* work is NOT saved (incomplete, changed, unreadable, a failed snapshot) that the executor's
|
|
385
|
+
* own history does not place before the seal (`origin`; without it, any such report at or past
|
|
386
|
+
* `captureN` counts against the seal). Otherwise ignored, and the executor is kept as before.
|
|
387
|
+
* Weighed again
|
|
388
|
+
* when it is used: a later report that the work is not saved revokes it. A seal stands in for
|
|
389
|
+
* a lost FINAL answer, never for a received one that said the work is not saved. Never a
|
|
390
|
+
* reason to skip the drain of a running executor.
|
|
391
|
+
*/
|
|
392
|
+
completion: Schema.optional(Schema.Struct({
|
|
393
|
+
/** The sealed capture's chain position (`n`). */
|
|
394
|
+
captureN: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
395
|
+
/** The capture lease epoch the seal was made under. */
|
|
396
|
+
epoch: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
397
|
+
/** The executor the seal names: run id, or runtime `resourceId` / `reference`. */
|
|
398
|
+
executorId: NonEmptyString,
|
|
399
|
+
/**
|
|
400
|
+
* The launch identity the seal names (`final_seal.executor`). Required when the create named
|
|
401
|
+
* a `launchId`, and then it must be that one; an attestation without it, or naming another
|
|
402
|
+
* launch, is ignored (a seal never transfers between executors).
|
|
403
|
+
*/
|
|
404
|
+
launchId: Schema.optional(NonEmptyString),
|
|
405
|
+
/**
|
|
406
|
+
* When the store recorded the seal (ISO 8601). Kept for display: clocks order nothing. It
|
|
407
|
+
* must still read as a time.
|
|
408
|
+
*/
|
|
409
|
+
sealedAt: Schema.optional(NonEmptyString),
|
|
410
|
+
/**
|
|
411
|
+
* Where in the executor's own history the seal was made (sealantd stamps it on the
|
|
412
|
+
* `final_seal`): the one thing that orders the seal against a report of the same capture
|
|
413
|
+
* that its work is not saved. Without it, such a report at or past `captureN` counts
|
|
414
|
+
* against the seal.
|
|
415
|
+
*/
|
|
416
|
+
origin: Schema.optional(captureExecutorOriginSchema),
|
|
417
|
+
})),
|
|
418
|
+
});
|
|
419
|
+
export const stopWorkspaceResponseSchema = Schema.Struct({
|
|
420
|
+
workspaceId: NonEmptyString,
|
|
421
|
+
status: workspaceStatusSchema,
|
|
422
|
+
/**
|
|
423
|
+
* What became of a `completion` attestation on the request: `accepted` (recorded; it lets the
|
|
424
|
+
* executor's disk go once it has ended) or `ignored` (it does not name this executor, or is for
|
|
425
|
+
* an older epoch; `detail` says which). Absent when the request carried none.
|
|
426
|
+
*/
|
|
427
|
+
completion: Schema.optional(Schema.Struct({
|
|
428
|
+
outcome: Schema.Literals(["accepted", "ignored"]),
|
|
429
|
+
detail: Schema.optional(Schema.String),
|
|
430
|
+
})),
|
|
431
|
+
});
|
|
432
|
+
/** Owner-scoped: ask the control plane to recover the workspace's retained executor now. */
|
|
433
|
+
export const recoverWorkspaceRequestSchema = Schema.Struct({
|
|
434
|
+
ownerUserId: NonEmptyString,
|
|
435
|
+
});
|
|
436
|
+
/**
|
|
437
|
+
* `requested`: the workspace's executor is retained (its disk holds work not confirmed saved) and
|
|
438
|
+
* a recovery attempt is due now; `recoverable` says whether its runtime can restart it on its own
|
|
439
|
+
* disk (Docker) or only report it (Kubernetes, MicroVM). `not-retained`: nothing is retained for
|
|
440
|
+
* the workspace's current run; nothing was done.
|
|
441
|
+
*/
|
|
442
|
+
export const recoverWorkspaceResponseSchema = Schema.Struct({
|
|
443
|
+
workspaceId: NonEmptyString,
|
|
444
|
+
state: Schema.Literals(["requested", "not-retained"]),
|
|
445
|
+
recoverable: Schema.optional(Schema.Boolean),
|
|
446
|
+
});
|
|
447
|
+
export const restartWorkspaceRequestSchema = Schema.Struct({
|
|
448
|
+
ownerUserId: NonEmptyString,
|
|
449
|
+
});
|
|
450
|
+
export const restartWorkspaceResponseSchema = Schema.Struct({
|
|
451
|
+
workspaceId: NonEmptyString,
|
|
452
|
+
/** The new attempt driving the fresh launch. */
|
|
453
|
+
runId: NonEmptyString,
|
|
454
|
+
status: workspaceStatusSchema,
|
|
455
|
+
});
|
|
456
|
+
export const expireWorkspaceRequestSchema = Schema.Struct({
|
|
457
|
+
ownerUserId: NonEmptyString,
|
|
458
|
+
// Seconds from now until the workspace expires; null clears the TTL (never expires); omitted =
|
|
459
|
+
// expire immediately (the reaper stops it on its next tick).
|
|
460
|
+
ttlSeconds: Schema.optional(Schema.NullOr(Schema.Int.check(Schema.isGreaterThan(0)))),
|
|
461
|
+
});
|
|
462
|
+
export const expireWorkspaceResponseSchema = Schema.Struct({
|
|
463
|
+
workspaceId: NonEmptyString,
|
|
464
|
+
expiresAt: Schema.NullOr(Schema.String),
|
|
465
|
+
});
|
|
466
|
+
export const renameWorkspaceResponseSchema = Schema.Struct({
|
|
467
|
+
workspaceId: NonEmptyString,
|
|
468
|
+
name: NonEmptyString,
|
|
469
|
+
updatedAt: Schema.String,
|
|
470
|
+
});
|
|
471
|
+
export const createWorkspaceResponseSchema = Schema.Struct({
|
|
472
|
+
workspaceId: NonEmptyString,
|
|
473
|
+
name: NonEmptyString,
|
|
474
|
+
status: workspaceStatusSchema,
|
|
475
|
+
registryId: NonEmptyString,
|
|
476
|
+
repository: NonEmptyString,
|
|
477
|
+
tag: NonEmptyString,
|
|
478
|
+
/** The launch attempt this create started (or, replayed, the workspace's latest). */
|
|
479
|
+
runId: Schema.optional(NonEmptyString),
|
|
480
|
+
/**
|
|
481
|
+
* The executor, once one exists: on a fresh create there is none yet (the launch is
|
|
482
|
+
* asynchronous); a replayed create carries the workspace's current one.
|
|
483
|
+
*/
|
|
484
|
+
runtime: Schema.optional(workspaceRuntimeSchema),
|
|
485
|
+
/** `true` when an earlier create with the same `idempotencyKey` made this workspace. */
|
|
486
|
+
replayed: Schema.optional(Schema.Boolean),
|
|
487
|
+
/** The launch identity the create named (`launchId`), recorded on its attempt. */
|
|
488
|
+
launchId: Schema.optional(NonEmptyString),
|
|
489
|
+
});
|
|
490
|
+
/**
|
|
491
|
+
* What became of an idempotent create, by its key (owner-scoped):
|
|
492
|
+
*
|
|
493
|
+
* - `pending`: a create with the key started and has not committed — it may still be in flight,
|
|
494
|
+
* or it died before it committed. A repeat of the create finishes it; `cancel` makes sure it
|
|
495
|
+
* never does. `workspaceId` names a half-made workspace a create from before this record
|
|
496
|
+
* left, which a repeat of the create completes.
|
|
497
|
+
* - `found`: the create committed; `workspaceId` (and `runId`, `launchId`) name what it made.
|
|
498
|
+
* - `cancelled`: the key was cancelled; no create with it ever commits.
|
|
499
|
+
* - `none`: no create with the key has reached this control plane (yet). Point-in-time only: a
|
|
500
|
+
* delayed request can still arrive — `cancel` is the answer that stays true.
|
|
501
|
+
*/
|
|
502
|
+
export const workspaceCreateStateSchema = Schema.Struct({
|
|
503
|
+
idempotencyKey: NonEmptyString,
|
|
504
|
+
state: Schema.Literals(["pending", "found", "cancelled", "none"]),
|
|
505
|
+
workspaceId: Schema.optional(NonEmptyString),
|
|
506
|
+
runId: Schema.optional(NonEmptyString),
|
|
507
|
+
launchId: Schema.optional(NonEmptyString),
|
|
508
|
+
});
|
|
509
|
+
export const getWorkspaceCreateQuerySchema = Schema.Struct({
|
|
510
|
+
ownerUserId: NonEmptyString,
|
|
511
|
+
});
|
|
512
|
+
/** Owner-scoped: cancel the create with this key, so it never commits. */
|
|
513
|
+
export const cancelWorkspaceCreateRequestSchema = Schema.Struct({
|
|
514
|
+
ownerUserId: NonEmptyString,
|
|
515
|
+
});
|
|
516
|
+
export const workspaceSummarySchema = Schema.Struct({
|
|
517
|
+
workspaceId: NonEmptyString,
|
|
518
|
+
name: NonEmptyString,
|
|
519
|
+
ownerUserId: NonEmptyString,
|
|
520
|
+
status: workspaceStatusSchema,
|
|
521
|
+
registryId: Schema.optional(NonEmptyString),
|
|
522
|
+
repository: Schema.optional(NonEmptyString),
|
|
523
|
+
tag: Schema.optional(NonEmptyString),
|
|
524
|
+
runtime: Schema.optional(workspaceRuntimeSchema),
|
|
525
|
+
publishedImage: Schema.optional(workspacePublishedImageSchema),
|
|
526
|
+
error: Schema.optional(workspaceErrorSchema),
|
|
527
|
+
createdAt: Schema.String,
|
|
528
|
+
updatedAt: Schema.String,
|
|
529
|
+
startedAt: Schema.optional(Schema.String),
|
|
530
|
+
finishedAt: Schema.optional(Schema.String),
|
|
531
|
+
expiresAt: Schema.optional(Schema.String),
|
|
532
|
+
});
|
|
533
|
+
/**
|
|
534
|
+
* What the control plane last OBSERVED of a capture-sourced workspace's drain — the FINAL flush
|
|
535
|
+
* and queue polling every platform stop runs before it removes the runtime. A stop request is
|
|
536
|
+
* not a stop: until the runtime is gone, this is what is happening.
|
|
537
|
+
*
|
|
538
|
+
* - `draining`: the queue is still moving; the stop continues on the server.
|
|
539
|
+
* - `kept`: nothing will stop the runtime — the work is not confirmed saved (the queue stalled,
|
|
540
|
+
* a class was refused, the daemon is silent while the executor runs, or the daemon did not
|
|
541
|
+
* report its final flush complete). `detail` says which.
|
|
542
|
+
* - `saved`: the daemon reported its final flush complete; the runtime is being removed.
|
|
543
|
+
* - `gone`: the daemon is silent and the runtime reports the executor ended.
|
|
544
|
+
* - `stop-failed`: the drain let the stop through but removing the runtime failed (`detail`
|
|
545
|
+
* has the error); the control plane retries the stop.
|
|
546
|
+
* - `stopped`: the runtime was removed after its drain let it go.
|
|
547
|
+
* - `discarded`: the owner discarded the unsaved captures (`stop({ discardUnsaved: true })`);
|
|
548
|
+
* the runtime was terminated without a drain. `discard` records who asked, and when.
|
|
549
|
+
*
|
|
550
|
+
* `preservationStartsAt` is when the control plane starts that drain on its own ahead of the
|
|
551
|
+
* runtime's deadline (`runtime.deadline`), once it has planned one.
|
|
552
|
+
*/
|
|
553
|
+
export const workspaceCaptureDrainSchema = Schema.Struct({
|
|
554
|
+
state: Schema.Literals([
|
|
555
|
+
"draining",
|
|
556
|
+
"kept",
|
|
557
|
+
"saved",
|
|
558
|
+
"gone",
|
|
559
|
+
"stop-failed",
|
|
560
|
+
"stopped",
|
|
561
|
+
"discarded",
|
|
562
|
+
]),
|
|
563
|
+
detail: Schema.optional(Schema.String),
|
|
564
|
+
/** ISO-8601: when this was observed. */
|
|
565
|
+
observedAt: Schema.optional(Schema.String),
|
|
566
|
+
/** ISO-8601: when the deadline sweep starts (or started) the final drain. */
|
|
567
|
+
preservationStartsAt: Schema.optional(Schema.String),
|
|
568
|
+
/** The owner's request to discard the unsaved captures: who asked, and when (ISO-8601). */
|
|
569
|
+
discard: Schema.optional(Schema.Struct({ requestedBy: NonEmptyString, requestedAt: Schema.String })),
|
|
570
|
+
/**
|
|
571
|
+
* The executor is RETAINED: kept because its disk holds work not confirmed saved. `since` and
|
|
572
|
+
* `reason` say when and why; recovery is attempted on a backoff (`recoveryAttempts`,
|
|
573
|
+
* `nextRecoveryAt`, `lastRecoveryError`); `recoverable` says whether its runtime can restart it
|
|
574
|
+
* on its own disk (Docker) or only report it (Kubernetes, MicroVM). Absent when nothing is
|
|
575
|
+
* retained.
|
|
576
|
+
*/
|
|
577
|
+
retained: Schema.optional(Schema.Struct({
|
|
578
|
+
since: Schema.String,
|
|
579
|
+
reason: Schema.String,
|
|
580
|
+
recoverable: Schema.Boolean,
|
|
581
|
+
recoveryAttempts: Schema.Int,
|
|
582
|
+
nextRecoveryAt: Schema.optional(Schema.String),
|
|
583
|
+
lastRecoveryError: Schema.optional(Schema.String),
|
|
584
|
+
})),
|
|
585
|
+
/**
|
|
586
|
+
* The executor this observation is about: the run and its runtime identity (`resourceId` is
|
|
587
|
+
* the id a stop's `completion` names).
|
|
588
|
+
*/
|
|
589
|
+
executor: Schema.optional(Schema.Struct({
|
|
590
|
+
runId: NonEmptyString,
|
|
591
|
+
adapter: NonEmptyString,
|
|
592
|
+
resourceId: NonEmptyString,
|
|
593
|
+
reference: Schema.optional(NonEmptyString),
|
|
594
|
+
/** The launch identity the create named for this executor (`launchId`), when it named one. */
|
|
595
|
+
launchId: Schema.optional(NonEmptyString),
|
|
596
|
+
})),
|
|
597
|
+
/** The latest `completion` attestation accepted for this executor (see `stop`). */
|
|
598
|
+
completion: Schema.optional(Schema.Struct({
|
|
599
|
+
executorId: NonEmptyString,
|
|
600
|
+
epoch: Schema.Int,
|
|
601
|
+
captureN: Schema.Int,
|
|
602
|
+
attestedAt: Schema.String,
|
|
603
|
+
launchId: Schema.optional(NonEmptyString),
|
|
604
|
+
/** When the store recorded the seal, when the attestation said. */
|
|
605
|
+
sealedAt: Schema.optional(Schema.String),
|
|
606
|
+
/** Where in the executor's own history the seal was made, when the attestation said. */
|
|
607
|
+
origin: Schema.optional(captureExecutorOriginSchema),
|
|
608
|
+
})),
|
|
609
|
+
});
|
|
610
|
+
export const workspaceDetailsSchema = Schema.Struct({
|
|
611
|
+
workspaceId: NonEmptyString,
|
|
612
|
+
name: NonEmptyString,
|
|
613
|
+
ownerUserId: NonEmptyString,
|
|
614
|
+
status: workspaceStatusSchema,
|
|
615
|
+
registryId: Schema.optional(NonEmptyString),
|
|
616
|
+
repository: Schema.optional(NonEmptyString),
|
|
617
|
+
tag: Schema.optional(NonEmptyString),
|
|
618
|
+
runtime: Schema.optional(workspaceRuntimeSchema),
|
|
619
|
+
publishedImage: Schema.optional(workspacePublishedImageSchema),
|
|
620
|
+
error: Schema.optional(workspaceErrorSchema),
|
|
621
|
+
createdAt: Schema.String,
|
|
622
|
+
updatedAt: Schema.String,
|
|
623
|
+
startedAt: Schema.optional(Schema.String),
|
|
624
|
+
finishedAt: Schema.optional(Schema.String),
|
|
625
|
+
expiresAt: Schema.optional(Schema.String),
|
|
626
|
+
spec: Schema.optional(Schema.Unknown),
|
|
627
|
+
/**
|
|
628
|
+
* The current runtime's capture drain as last observed (see `workspaceCaptureDrainSchema`).
|
|
629
|
+
* Absent while no drain or preservation schedule exists, and from older control planes.
|
|
630
|
+
*/
|
|
631
|
+
captureDrain: Schema.optional(workspaceCaptureDrainSchema),
|
|
632
|
+
});
|
|
633
|
+
/**
|
|
634
|
+
* Owner scoping on a single read: the workspace must belong to `ownerUserId` (uniform 404).
|
|
635
|
+
* Optional on the wire for older callers; the control plane requires it.
|
|
636
|
+
*/
|
|
637
|
+
export const getWorkspaceQuerySchema = Schema.Struct({
|
|
638
|
+
ownerUserId: Schema.optional(NonEmptyString),
|
|
639
|
+
});
|
|
640
|
+
export const listWorkspacesQuerySchema = Schema.Struct({
|
|
641
|
+
ownerUserId: NonEmptyString,
|
|
642
|
+
status: Schema.optional(workspaceStatusSchema),
|
|
643
|
+
limit: Schema.optional(NonEmptyString),
|
|
644
|
+
/** Only the owner's workspace created with this `idempotencyKey` (none or one item). */
|
|
645
|
+
idempotencyKey: Schema.optional(NonEmptyString),
|
|
646
|
+
});
|
|
647
|
+
export const listWorkspacesResponseSchema = Schema.Struct({
|
|
648
|
+
items: Schema.Array(workspaceSummarySchema),
|
|
649
|
+
});
|
|
650
|
+
export const listWorkspaceAttemptsQuerySchema = Schema.Struct({
|
|
651
|
+
ownerUserId: Schema.optional(NonEmptyString),
|
|
652
|
+
limit: Schema.optional(NonEmptyString),
|
|
653
|
+
});
|
|
654
|
+
export const workspaceAttemptSummarySchema = Schema.Struct({
|
|
655
|
+
attemptId: NonEmptyString,
|
|
656
|
+
relation: Schema.Literals(["launch", "rebuild", "retry", "resume"]),
|
|
657
|
+
status: workspaceStatusSchema,
|
|
658
|
+
triggerType: Schema.Literals(["manual", "schedule", "api", "retry"]),
|
|
659
|
+
triggerRef: Schema.optional(NonEmptyString),
|
|
660
|
+
runtime: Schema.optional(workspaceRuntimeSchema),
|
|
661
|
+
publishedImage: Schema.optional(workspacePublishedImageSchema),
|
|
662
|
+
error: Schema.optional(workspaceErrorSchema),
|
|
663
|
+
spec: Schema.optional(Schema.Unknown),
|
|
664
|
+
queuedAt: Schema.String,
|
|
665
|
+
createdAt: Schema.String,
|
|
666
|
+
updatedAt: Schema.String,
|
|
667
|
+
linkedAt: Schema.String,
|
|
668
|
+
startedAt: Schema.optional(Schema.String),
|
|
669
|
+
finishedAt: Schema.optional(Schema.String),
|
|
670
|
+
durationMs: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
671
|
+
});
|
|
672
|
+
export const listWorkspaceAttemptsResponseSchema = Schema.Struct({
|
|
673
|
+
items: Schema.Array(workspaceAttemptSummarySchema),
|
|
674
|
+
});
|
|
675
|
+
export const listWorkspaceEventsQuerySchema = Schema.Struct({
|
|
676
|
+
ownerUserId: Schema.optional(NonEmptyString),
|
|
677
|
+
limit: Schema.optional(NonEmptyString),
|
|
678
|
+
});
|
|
679
|
+
export const workspaceEventTypeSchema = Schema.Literals([
|
|
680
|
+
"workspace.created",
|
|
681
|
+
"attempt.queued",
|
|
682
|
+
"attempt.running",
|
|
683
|
+
"attempt.succeeded",
|
|
684
|
+
"attempt.failed",
|
|
685
|
+
"attempt.cancelled",
|
|
686
|
+
"image.published",
|
|
687
|
+
"runtime.pending",
|
|
688
|
+
"runtime.running",
|
|
689
|
+
"runtime.ready",
|
|
690
|
+
"runtime.failed",
|
|
691
|
+
"runtime.stopped",
|
|
692
|
+
]);
|
|
693
|
+
export const workspaceEventSchema = Schema.Struct({
|
|
694
|
+
eventId: NonEmptyString,
|
|
695
|
+
workspaceId: NonEmptyString,
|
|
696
|
+
attemptId: Schema.optional(NonEmptyString),
|
|
697
|
+
type: workspaceEventTypeSchema,
|
|
698
|
+
occurredAt: Schema.String,
|
|
699
|
+
message: Schema.optional(Schema.String),
|
|
700
|
+
data: Schema.optional(Schema.Unknown),
|
|
701
|
+
});
|
|
702
|
+
export const listWorkspaceEventsResponseSchema = Schema.Struct({
|
|
703
|
+
items: Schema.Array(workspaceEventSchema),
|
|
704
|
+
});
|
|
705
|
+
export const workspaceGatewayHeadersSchema = Schema.Struct({
|
|
706
|
+
// Authenticates the gateway as a trusted caller of this internal endpoint.
|
|
707
|
+
"x-sealant-gateway-token": Schema.optional(NonEmptyString),
|
|
708
|
+
// Identifies the client principal (the SSH key's owner). The API authorizes principal x workspace
|
|
709
|
+
// before returning a control target (gateway-spec §3.4).
|
|
710
|
+
"x-sealant-principal-id": Schema.optional(NonEmptyString),
|
|
711
|
+
});
|
|
712
|
+
export class WorkspaceBadRequestError extends Schema.TaggedErrorClass()("WorkspaceBadRequestError", {
|
|
713
|
+
message: Schema.String,
|
|
714
|
+
}, { httpApiStatus: 400 }) {
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Kubernetes-only create-time inputs (cluster env sources `runtime.envFrom`, a workspace
|
|
718
|
+
* `kubernetes.serviceAccountName`) on a deployment whose workspaces do not run on Kubernetes.
|
|
719
|
+
* Refused synchronously at POST /v1/workspaces — no workspace row, no build job, no failure
|
|
720
|
+
* minutes later. The stable `code` doubles as the SDK consumer's capability probe: mapping this
|
|
721
|
+
* code is how a caller learns the install cannot resolve cluster bindings, instead of trusting a
|
|
722
|
+
* config flag that can lie.
|
|
723
|
+
*/
|
|
724
|
+
export class WorkspaceRuntimeEnvReferencesUnsupportedError extends Schema.TaggedErrorClass()("WorkspaceRuntimeEnvReferencesUnsupportedError", {
|
|
725
|
+
message: Schema.String,
|
|
726
|
+
code: Schema.Literals(["runtime-env-references-unsupported"]),
|
|
727
|
+
}, { httpApiStatus: 422 }) {
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* `services.docker` requested on an install whose workspace runtime cannot serve it. Kubernetes
|
|
731
|
+
* needs its operator-enabled rootless sidecar. Lambda MicroVMs need a separate Docker-capable
|
|
732
|
+
* image, leaving the default image at its restricted Linux capability set. Refused synchronously
|
|
733
|
+
* at POST /v1/workspaces; the stable `code` is the consumer's capability probe.
|
|
734
|
+
*/
|
|
735
|
+
export class WorkspaceDockerServiceUnsupportedError extends Schema.TaggedErrorClass()("WorkspaceDockerServiceUnsupportedError", {
|
|
736
|
+
message: Schema.String,
|
|
737
|
+
code: Schema.Literals(["workspace-docker-unsupported"]),
|
|
738
|
+
}, { httpApiStatus: 422 }) {
|
|
739
|
+
}
|
|
740
|
+
export class WorkspaceUnauthorizedError extends Schema.TaggedErrorClass()("WorkspaceUnauthorizedError", {
|
|
741
|
+
message: Schema.String,
|
|
742
|
+
}, { httpApiStatus: 401 }) {
|
|
743
|
+
}
|
|
744
|
+
export class WorkspaceForbiddenError extends Schema.TaggedErrorClass()("WorkspaceForbiddenError", {
|
|
745
|
+
message: Schema.String,
|
|
746
|
+
}, { httpApiStatus: 403 }) {
|
|
747
|
+
}
|
|
748
|
+
export class WorkspaceNotFoundError extends Schema.TaggedErrorClass()("WorkspaceNotFoundError", {
|
|
749
|
+
message: Schema.String,
|
|
750
|
+
}, { httpApiStatus: 404 }) {
|
|
751
|
+
}
|
|
752
|
+
export class WorkspaceConflictError extends Schema.TaggedErrorClass()("WorkspaceConflictError", {
|
|
753
|
+
message: Schema.String,
|
|
754
|
+
/** A stable reason, where one applies (`create-cancelled`: the create's key was cancelled). */
|
|
755
|
+
code: Schema.optional(NonEmptyString),
|
|
756
|
+
}, { httpApiStatus: 409 }) {
|
|
757
|
+
}
|
|
758
|
+
export class WorkspaceBadGatewayError extends Schema.TaggedErrorClass()("WorkspaceBadGatewayError", {
|
|
759
|
+
message: Schema.String,
|
|
760
|
+
}, { httpApiStatus: 502 }) {
|
|
761
|
+
}
|
|
762
|
+
export class WorkspaceServiceUnavailableError extends Schema.TaggedErrorClass()("WorkspaceServiceUnavailableError", {
|
|
763
|
+
message: Schema.String,
|
|
764
|
+
}, { httpApiStatus: 503 }) {
|
|
765
|
+
}
|
|
766
|
+
export class WorkspaceInternalServerError extends Schema.TaggedErrorClass()("WorkspaceInternalServerError", {
|
|
767
|
+
message: Schema.String,
|
|
768
|
+
}, { httpApiStatus: 500 }) {
|
|
769
|
+
}
|
|
770
|
+
const workspaceIdParams = Schema.Struct({ workspaceId: NonEmptyString });
|
|
771
|
+
const idempotencyKeyParams = Schema.Struct({ idempotencyKey: NonEmptyString });
|
|
772
|
+
export const WorkspacesGroup = HttpApiGroup.make("workspaces")
|
|
773
|
+
.add(HttpApiEndpoint.post("createWorkspace", "/", {
|
|
774
|
+
headers: createWorkspaceHeadersSchema,
|
|
775
|
+
payload: createWorkspaceRequestSchema,
|
|
776
|
+
success: createWorkspaceResponseSchema.pipe(HttpApiSchema.status(202)),
|
|
777
|
+
error: [
|
|
778
|
+
BudgetExceededError,
|
|
779
|
+
WorkspaceBadRequestError,
|
|
780
|
+
WorkspaceRuntimeEnvReferencesUnsupportedError,
|
|
781
|
+
WorkspaceDockerServiceUnsupportedError,
|
|
782
|
+
WorkspaceForbiddenError,
|
|
783
|
+
WorkspaceNotFoundError,
|
|
784
|
+
// Selected connected account exists but is not usable (status "invalid").
|
|
785
|
+
WorkspaceConflictError,
|
|
786
|
+
WorkspaceBadGatewayError,
|
|
787
|
+
WorkspaceServiceUnavailableError,
|
|
788
|
+
WorkspaceInternalServerError,
|
|
789
|
+
],
|
|
790
|
+
}))
|
|
791
|
+
.add(
|
|
792
|
+
// What became of an idempotent create, by its key (owner-scoped).
|
|
793
|
+
HttpApiEndpoint.get("getWorkspaceCreate", "/idempotency-keys/:idempotencyKey", {
|
|
794
|
+
params: idempotencyKeyParams,
|
|
795
|
+
query: getWorkspaceCreateQuerySchema,
|
|
796
|
+
success: workspaceCreateStateSchema,
|
|
797
|
+
error: [WorkspaceBadRequestError, WorkspaceInternalServerError],
|
|
798
|
+
}))
|
|
799
|
+
.add(
|
|
800
|
+
// Cancel an idempotent create by its key: a create with it never commits afterwards (a
|
|
801
|
+
// delayed original request included). `found` when it had already committed.
|
|
802
|
+
HttpApiEndpoint.post("cancelWorkspaceCreate", "/idempotency-keys/:idempotencyKey/cancel", {
|
|
803
|
+
params: idempotencyKeyParams,
|
|
804
|
+
payload: cancelWorkspaceCreateRequestSchema,
|
|
805
|
+
success: workspaceCreateStateSchema,
|
|
806
|
+
error: [WorkspaceBadRequestError, WorkspaceInternalServerError],
|
|
807
|
+
}))
|
|
808
|
+
.add(
|
|
809
|
+
// Synchronous: the daemon applies the bind over the control connection before this answers.
|
|
810
|
+
HttpApiEndpoint.post("bindWorkspace", "/:workspaceId/bind", {
|
|
811
|
+
params: workspaceIdParams,
|
|
812
|
+
payload: bindWorkspaceRequestSchema,
|
|
813
|
+
success: workspaceBindsSchema,
|
|
814
|
+
error: [
|
|
815
|
+
WorkspaceBadRequestError,
|
|
816
|
+
WorkspaceNotFoundError,
|
|
817
|
+
// No live runtime to bind in (never launched, mid-launch, or the daemon refused).
|
|
818
|
+
WorkspaceConflictError,
|
|
819
|
+
WorkspaceInternalServerError,
|
|
820
|
+
],
|
|
821
|
+
}))
|
|
822
|
+
.add(
|
|
823
|
+
// Synchronous: the daemon flushes over the control connection before this answers.
|
|
824
|
+
HttpApiEndpoint.post("flushWorkspaceCapture", "/:workspaceId/capture/flush", {
|
|
825
|
+
params: workspaceIdParams,
|
|
826
|
+
payload: flushWorkspaceCaptureRequestSchema,
|
|
827
|
+
success: workspaceCaptureStatusSchema,
|
|
828
|
+
error: [
|
|
829
|
+
// Not a capture-sourced workspace.
|
|
830
|
+
WorkspaceBadRequestError,
|
|
831
|
+
WorkspaceNotFoundError,
|
|
832
|
+
// No live runtime to flush (never launched, mid-launch, or the daemon refused).
|
|
833
|
+
WorkspaceConflictError,
|
|
834
|
+
WorkspaceInternalServerError,
|
|
835
|
+
],
|
|
836
|
+
}))
|
|
837
|
+
.add(
|
|
838
|
+
// Synchronous: one `capture.status` round trip over the control connection; flushes nothing.
|
|
839
|
+
HttpApiEndpoint.get("getWorkspaceCaptureStatus", "/:workspaceId/capture", {
|
|
840
|
+
params: workspaceIdParams,
|
|
841
|
+
query: getWorkspaceCaptureStatusQuerySchema,
|
|
842
|
+
success: workspaceCaptureStatusSchema,
|
|
843
|
+
error: [
|
|
844
|
+
// Not a capture-sourced workspace.
|
|
845
|
+
WorkspaceBadRequestError,
|
|
846
|
+
WorkspaceNotFoundError,
|
|
847
|
+
// No live runtime to ask (never launched, mid-launch, gone, or the daemon refused).
|
|
848
|
+
WorkspaceConflictError,
|
|
849
|
+
WorkspaceInternalServerError,
|
|
850
|
+
],
|
|
851
|
+
}))
|
|
852
|
+
.add(
|
|
853
|
+
// Synchronous: the daemon re-plans and delta-materialises over the control connection before
|
|
854
|
+
// this answers.
|
|
855
|
+
HttpApiEndpoint.post("replanWorkspaceCapture", "/:workspaceId/capture/replan", {
|
|
856
|
+
params: workspaceIdParams,
|
|
857
|
+
payload: replanWorkspaceCaptureRequestSchema,
|
|
858
|
+
success: workspaceCaptureReplannedSchema,
|
|
859
|
+
error: [
|
|
860
|
+
// Not a capture-sourced workspace.
|
|
861
|
+
WorkspaceBadRequestError,
|
|
862
|
+
WorkspaceNotFoundError,
|
|
863
|
+
// No live runtime to re-plan (never launched, mid-launch, or the daemon refused).
|
|
864
|
+
WorkspaceConflictError,
|
|
865
|
+
WorkspaceInternalServerError,
|
|
866
|
+
],
|
|
867
|
+
}))
|
|
868
|
+
.add(
|
|
869
|
+
// Async like createWorkspace: 202 + the queued run resource; poll `GET /v1/runs/:runId` to
|
|
870
|
+
// completion, then read exit codes / scrollback from the run record.
|
|
871
|
+
HttpApiEndpoint.post("execWorkspace", "/:workspaceId/exec", {
|
|
872
|
+
params: workspaceIdParams,
|
|
873
|
+
payload: execWorkspaceRequestSchema,
|
|
874
|
+
success: runSchema.pipe(HttpApiSchema.status(202)),
|
|
875
|
+
error: [
|
|
876
|
+
WorkspaceBadRequestError,
|
|
877
|
+
WorkspaceNotFoundError,
|
|
878
|
+
// The workspace has never launched a runtime — nothing to exec in yet.
|
|
879
|
+
WorkspaceConflictError,
|
|
880
|
+
WorkspaceInternalServerError,
|
|
881
|
+
],
|
|
882
|
+
}))
|
|
883
|
+
.add(
|
|
884
|
+
// Async: 202 = the stop was accepted and enqueued; the worker removes the container and the
|
|
885
|
+
// workspace transitions to "stopped". Idempotent — stopping a stopped workspace is a no-op 202.
|
|
886
|
+
HttpApiEndpoint.post("stopWorkspace", "/:workspaceId/stop", {
|
|
887
|
+
params: workspaceIdParams,
|
|
888
|
+
payload: stopWorkspaceRequestSchema,
|
|
889
|
+
success: stopWorkspaceResponseSchema.pipe(HttpApiSchema.status(202)),
|
|
890
|
+
error: [
|
|
891
|
+
WorkspaceBadRequestError,
|
|
892
|
+
WorkspaceNotFoundError,
|
|
893
|
+
// The workspace has never launched a runtime — nothing to stop yet.
|
|
894
|
+
WorkspaceConflictError,
|
|
895
|
+
WorkspaceBadGatewayError,
|
|
896
|
+
WorkspaceInternalServerError,
|
|
897
|
+
],
|
|
898
|
+
}))
|
|
899
|
+
.add(
|
|
900
|
+
// Async: 202 = a recovery attempt of the workspace's retained executor is due now; the worker
|
|
901
|
+
// restarts it on its own disk where the runtime can, drains it with a FINAL flush and only
|
|
902
|
+
// then removes it. Nothing retained = `not-retained`, nothing done.
|
|
903
|
+
HttpApiEndpoint.post("recoverWorkspace", "/:workspaceId/recover", {
|
|
904
|
+
params: workspaceIdParams,
|
|
905
|
+
payload: recoverWorkspaceRequestSchema,
|
|
906
|
+
success: recoverWorkspaceResponseSchema.pipe(HttpApiSchema.status(202)),
|
|
907
|
+
error: [
|
|
908
|
+
WorkspaceBadRequestError,
|
|
909
|
+
WorkspaceNotFoundError,
|
|
910
|
+
WorkspaceConflictError,
|
|
911
|
+
WorkspaceInternalServerError,
|
|
912
|
+
],
|
|
913
|
+
}))
|
|
914
|
+
.add(
|
|
915
|
+
// Async: 202 + the new attempt id. Restart = stop the current runtime (if any) and drive a
|
|
916
|
+
// fresh launch from the same resolved spec — a new container, no filesystem carry-over.
|
|
917
|
+
HttpApiEndpoint.post("restartWorkspace", "/:workspaceId/restart", {
|
|
918
|
+
params: workspaceIdParams,
|
|
919
|
+
payload: restartWorkspaceRequestSchema,
|
|
920
|
+
success: restartWorkspaceResponseSchema.pipe(HttpApiSchema.status(202)),
|
|
921
|
+
error: [
|
|
922
|
+
BudgetExceededError,
|
|
923
|
+
WorkspaceBadRequestError,
|
|
924
|
+
WorkspaceNotFoundError,
|
|
925
|
+
// The workspace has never launched (no spec to relaunch from) or is mid-launch.
|
|
926
|
+
WorkspaceConflictError,
|
|
927
|
+
WorkspaceBadGatewayError,
|
|
928
|
+
WorkspaceInternalServerError,
|
|
929
|
+
],
|
|
930
|
+
}))
|
|
931
|
+
.add(
|
|
932
|
+
// Synchronous: sets (or clears) the workspace TTL column; the worker reaper enforces it.
|
|
933
|
+
HttpApiEndpoint.post("expireWorkspace", "/:workspaceId/expire", {
|
|
934
|
+
params: workspaceIdParams,
|
|
935
|
+
payload: expireWorkspaceRequestSchema,
|
|
936
|
+
success: expireWorkspaceResponseSchema,
|
|
937
|
+
error: [WorkspaceBadRequestError, WorkspaceNotFoundError, WorkspaceInternalServerError],
|
|
938
|
+
}))
|
|
939
|
+
.add(HttpApiEndpoint.patch("renameWorkspace", "/:workspaceId/name", {
|
|
940
|
+
params: workspaceIdParams,
|
|
941
|
+
payload: renameWorkspaceRequestSchema,
|
|
942
|
+
success: renameWorkspaceResponseSchema,
|
|
943
|
+
error: [WorkspaceNotFoundError, WorkspaceInternalServerError],
|
|
944
|
+
}))
|
|
945
|
+
.add(HttpApiEndpoint.get("listWorkspaces", "/", {
|
|
946
|
+
query: listWorkspacesQuerySchema,
|
|
947
|
+
success: listWorkspacesResponseSchema,
|
|
948
|
+
error: [WorkspaceBadRequestError, WorkspaceInternalServerError],
|
|
949
|
+
}))
|
|
950
|
+
.add(HttpApiEndpoint.get("getWorkspace", "/:workspaceId", {
|
|
951
|
+
params: workspaceIdParams,
|
|
952
|
+
query: getWorkspaceQuerySchema,
|
|
953
|
+
success: workspaceDetailsSchema,
|
|
954
|
+
error: [WorkspaceNotFoundError, WorkspaceInternalServerError],
|
|
955
|
+
}))
|
|
956
|
+
.add(HttpApiEndpoint.get("listWorkspaceAttempts", "/:workspaceId/attempts", {
|
|
957
|
+
params: workspaceIdParams,
|
|
958
|
+
query: listWorkspaceAttemptsQuerySchema,
|
|
959
|
+
success: listWorkspaceAttemptsResponseSchema,
|
|
960
|
+
error: [WorkspaceBadRequestError, WorkspaceNotFoundError, WorkspaceInternalServerError],
|
|
961
|
+
}))
|
|
962
|
+
.add(HttpApiEndpoint.get("listWorkspaceEvents", "/:workspaceId/events", {
|
|
963
|
+
params: workspaceIdParams,
|
|
964
|
+
query: listWorkspaceEventsQuerySchema,
|
|
965
|
+
success: listWorkspaceEventsResponseSchema,
|
|
966
|
+
error: [WorkspaceBadRequestError, WorkspaceNotFoundError, WorkspaceInternalServerError],
|
|
967
|
+
}))
|
|
968
|
+
.add(HttpApiEndpoint.get("getWorkspaceSshTarget", "/:workspaceId/ssh-target", {
|
|
969
|
+
params: workspaceIdParams,
|
|
970
|
+
headers: workspaceGatewayHeadersSchema,
|
|
971
|
+
success: workspaceSshTargetSchema,
|
|
972
|
+
error: [
|
|
973
|
+
WorkspaceUnauthorizedError,
|
|
974
|
+
WorkspaceNotFoundError,
|
|
975
|
+
WorkspaceConflictError,
|
|
976
|
+
WorkspaceServiceUnavailableError,
|
|
977
|
+
WorkspaceInternalServerError,
|
|
978
|
+
],
|
|
979
|
+
}))
|
|
980
|
+
.annotate(OpenApi.Description, "Workspace lifecycle, attempts, events, and runtime routing endpoints.");
|