@sealant/sdk 0.5.1 → 0.7.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/dist/client.d.ts +8 -1
- package/dist/client.js +26 -1
- package/dist/effect/api-client.d.ts +710 -14
- package/dist/effect/operations.d.ts +226 -6
- package/dist/effect/operations.js +18 -0
- package/dist/effect/run-harness.js +32 -13
- package/dist/facade/session.d.ts +22 -0
- package/dist/facade/session.js +171 -0
- package/dist/facade/workspace.js +109 -6
- package/dist/internal/blueprint.d.ts +1 -0
- package/dist/internal/blueprint.js +23 -8
- package/dist/internal/duration.d.ts +6 -0
- package/dist/internal/duration.js +34 -0
- package/dist/types.d.ts +136 -14
- package/package.json +2 -2
package/dist/facade/workspace.js
CHANGED
|
@@ -1,15 +1,75 @@
|
|
|
1
1
|
import { execWorkspace } from "../effect/exec-workspace.js";
|
|
2
|
-
import { getWorkspaceOp } from "../effect/operations.js";
|
|
2
|
+
import { createSessionOp, expireWorkspaceOp, getSessionOp, getWorkspaceOp, listSessionsOp, restartWorkspaceOp, stopWorkspaceOp, } from "../effect/operations.js";
|
|
3
3
|
import { SealantError, SealantNotImplementedError } from "../errors.js";
|
|
4
|
-
|
|
4
|
+
import { parseTtlSeconds } from "../internal/duration.js";
|
|
5
|
+
import { makeInteractiveSession } from "./session.js";
|
|
6
|
+
// Terminal statuses a workspace can never leave: ready()/events() fail fast (or end the stream)
|
|
7
|
+
// on these instead of polling out their deadline. "stopped" is terminal too — a TTL expiry or a
|
|
8
|
+
// concurrent stop while ready() polls must surface immediately, not as a 10-minute timeout.
|
|
9
|
+
const FAILED_STATUSES = new Set(["failed", "cancelled", "stopped"]);
|
|
5
10
|
const READY_POLL_INTERVAL_MS = 2_000;
|
|
6
11
|
const READY_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
12
|
+
const STOP_POLL_INTERVAL_MS = 1_000;
|
|
13
|
+
const STOP_TIMEOUT_MS = 60 * 1_000;
|
|
7
14
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
15
|
let harnessExecutors;
|
|
9
16
|
export const registerHarnessExecutors = (executors) => {
|
|
10
17
|
harnessExecutors = executors;
|
|
11
18
|
};
|
|
19
|
+
// Launch commands for the built-in harnesses — used when a RE-FETCHED handle (no client harness
|
|
20
|
+
// value) opens a harness session; the workspace's own spec names the harness id.
|
|
21
|
+
const BUILTIN_LAUNCH_COMMANDS = {
|
|
22
|
+
opencode: "opencode",
|
|
23
|
+
codex: "codex",
|
|
24
|
+
"claude-code": "claude",
|
|
25
|
+
};
|
|
12
26
|
export const makeWorkspace = (ctx, init) => {
|
|
27
|
+
const openSession = async (argv, options) => {
|
|
28
|
+
const created = await ctx.runtime.run(createSessionOp({
|
|
29
|
+
workspaceId: init.id,
|
|
30
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
31
|
+
argv: [...argv],
|
|
32
|
+
...(options?.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
33
|
+
...(options?.env === undefined ? {} : { env: options.env }),
|
|
34
|
+
...(options?.cols === undefined ? {} : { cols: options.cols }),
|
|
35
|
+
...(options?.rows === undefined ? {} : { rows: options.rows }),
|
|
36
|
+
...(options?.term === undefined ? {} : { term: options.term }),
|
|
37
|
+
...(options?.metadata === undefined ? {} : { metadata: { ...options.metadata } }),
|
|
38
|
+
}));
|
|
39
|
+
return makeInteractiveSession(ctx, created);
|
|
40
|
+
};
|
|
41
|
+
const sessions = {
|
|
42
|
+
open: (argv, options) => openSession(argv, options),
|
|
43
|
+
get: async (sessionId) => {
|
|
44
|
+
const wire = await ctx.runtime.run(getSessionOp(sessionId, ctx.config.hostLocal.ownerUserId));
|
|
45
|
+
if (wire.workspaceId !== init.id) {
|
|
46
|
+
throw new SealantError(`Session ${sessionId} does not belong to workspace ${init.id}.`, {
|
|
47
|
+
code: "session_not_found",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return makeInteractiveSession(ctx, wire);
|
|
51
|
+
},
|
|
52
|
+
list: async () => {
|
|
53
|
+
const response = await ctx.runtime.run(listSessionsOp({
|
|
54
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
55
|
+
workspaceId: init.id,
|
|
56
|
+
}));
|
|
57
|
+
return response.items.map((item) => makeInteractiveSession(ctx, item));
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
/** The harness's interactive launch argv — client value when present, else from the spec. */
|
|
61
|
+
const resolveHarnessLaunchArgv = async () => {
|
|
62
|
+
if (init.harness !== undefined) {
|
|
63
|
+
return [init.harness.launchCommand ?? init.harness.id];
|
|
64
|
+
}
|
|
65
|
+
const details = await ctx.runtime.run(getWorkspaceOp(init.id));
|
|
66
|
+
const spec = details.spec;
|
|
67
|
+
const harnessId = spec?.harness?.id;
|
|
68
|
+
if (harnessId === undefined) {
|
|
69
|
+
throw new SealantError(`Workspace ${init.id} has no harness in its spec; open a session with workspace.sessions.open(argv) instead.`, { code: "harness_required" });
|
|
70
|
+
}
|
|
71
|
+
return [BUILTIN_LAUNCH_COMMANDS[harnessId] ?? harnessId];
|
|
72
|
+
};
|
|
13
73
|
const harness = {
|
|
14
74
|
run: (prompt, options) => {
|
|
15
75
|
if (harnessExecutors === undefined) {
|
|
@@ -23,7 +83,10 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
23
83
|
}
|
|
24
84
|
return harnessExecutors.start(ctx, init, prompt, options);
|
|
25
85
|
},
|
|
26
|
-
session: () =>
|
|
86
|
+
session: async (options) => {
|
|
87
|
+
const argv = await resolveHarnessLaunchArgv();
|
|
88
|
+
return openSession(argv, options);
|
|
89
|
+
},
|
|
27
90
|
};
|
|
28
91
|
const workspace = {
|
|
29
92
|
id: init.id,
|
|
@@ -54,6 +117,7 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
54
117
|
}
|
|
55
118
|
},
|
|
56
119
|
harness,
|
|
120
|
+
sessions,
|
|
57
121
|
exec: (argv, options) => execWorkspace(ctx, init, argv, options),
|
|
58
122
|
// Poll-backed lifecycle stream: emit a coarse event on each status transition until the workspace
|
|
59
123
|
// reaches a terminal/ready state. Swaps to SSE over Postgres LISTEN/NOTIFY in Stage 5 (same shape).
|
|
@@ -83,9 +147,48 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
83
147
|
}
|
|
84
148
|
return iterate();
|
|
85
149
|
},
|
|
86
|
-
stop:
|
|
87
|
-
|
|
88
|
-
|
|
150
|
+
// BLOCKING stop: the control plane accepts the stop (202) and the worker tears the container
|
|
151
|
+
// down; resolve only once the workspace reports the terminal "stopped" status, so callers can
|
|
152
|
+
// trust the container is gone when this settles.
|
|
153
|
+
stop: async () => {
|
|
154
|
+
const ownerUserId = ctx.config.hostLocal.ownerUserId;
|
|
155
|
+
await ctx.runtime.run(stopWorkspaceOp(init.id, { ownerUserId }));
|
|
156
|
+
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
157
|
+
for (;;) {
|
|
158
|
+
const details = await ctx.runtime.run(getWorkspaceOp(init.id));
|
|
159
|
+
if (details.status === "stopped") {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (Date.now() > deadline) {
|
|
163
|
+
throw new SealantError(`Timed out waiting for workspace ${init.id} to stop.`, {
|
|
164
|
+
code: "workspace_stop_timeout",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
await delay(STOP_POLL_INTERVAL_MS);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
// Restart drives a fresh launch (new attempt, new container, same resolved spec) and returns a
|
|
171
|
+
// handle that resolves readiness against the NEW runtime via the usual ready() gate.
|
|
172
|
+
restart: async () => {
|
|
173
|
+
const ownerUserId = ctx.config.hostLocal.ownerUserId;
|
|
174
|
+
await ctx.runtime.run(restartWorkspaceOp(init.id, { ownerUserId }));
|
|
175
|
+
return makeWorkspace(ctx, {
|
|
176
|
+
id: init.id,
|
|
177
|
+
name: init.name,
|
|
178
|
+
status: "queued",
|
|
179
|
+
...(init.harness === undefined ? {} : { harness: init.harness }),
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
// expire({in: "2h"}) sets the TTL, expire() expires now (the platform reaper stops it on its
|
|
183
|
+
// next tick), expire({in: null}) clears the TTL. Resolves once the expiry is recorded.
|
|
184
|
+
expire: async (options) => {
|
|
185
|
+
const ownerUserId = ctx.config.hostLocal.ownerUserId;
|
|
186
|
+
const ttl = options?.in;
|
|
187
|
+
await ctx.runtime.run(expireWorkspaceOp(init.id, {
|
|
188
|
+
ownerUserId,
|
|
189
|
+
...(ttl === undefined ? {} : { ttlSeconds: ttl === null ? null : parseTtlSeconds(ttl) }),
|
|
190
|
+
}));
|
|
191
|
+
},
|
|
89
192
|
};
|
|
90
193
|
return workspace;
|
|
91
194
|
};
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* plane resolves those account references server-side (never secret material over this path).
|
|
12
12
|
*/
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { SealantError } from "../errors.js";
|
|
14
15
|
import { mapWorkspaceCredentials } from "./credentials.js";
|
|
16
|
+
import { parseTtlSeconds } from "./duration.js";
|
|
15
17
|
const sanitizeRepoSlug = (value) => {
|
|
16
18
|
const slug = value
|
|
17
19
|
.toLowerCase()
|
|
@@ -27,20 +29,32 @@ const toGitUrl = (repository) => {
|
|
|
27
29
|
return `https://${repository}.git`;
|
|
28
30
|
};
|
|
29
31
|
export const buildCreateWorkspaceRequest = (options, config) => {
|
|
30
|
-
|
|
32
|
+
if ((options.repository === undefined) === (options.source === undefined)) {
|
|
33
|
+
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" });
|
|
34
|
+
}
|
|
35
|
+
if (options.source !== undefined && options.ref !== undefined) {
|
|
36
|
+
throw new SealantError("`ref` applies only to `repository` sources, not mounts.", {
|
|
37
|
+
code: "invalid_create_options",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const sourceName = options.repository ?? options.source?.path ?? "workspace";
|
|
41
|
+
const tail = sourceName
|
|
31
42
|
.split("/")
|
|
32
43
|
.filter((s) => s.length > 0)
|
|
33
|
-
.pop() ??
|
|
44
|
+
.pop() ?? sourceName;
|
|
34
45
|
const credentials = mapWorkspaceCredentials(options.credentials);
|
|
35
46
|
const spec = {
|
|
36
47
|
version: "1",
|
|
37
48
|
sources: {
|
|
38
|
-
workspace:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
workspace: options.repository !== undefined
|
|
50
|
+
? {
|
|
51
|
+
kind: "git",
|
|
52
|
+
provider: "generic",
|
|
53
|
+
url: toGitUrl(options.repository),
|
|
54
|
+
// Omitted ref = the repository's default branch, resolved by the clone itself.
|
|
55
|
+
...(options.ref === undefined ? {} : { ref: options.ref }),
|
|
56
|
+
}
|
|
57
|
+
: { kind: "mount", hostPath: options.source?.path },
|
|
44
58
|
},
|
|
45
59
|
harness: { id: options.harness.id },
|
|
46
60
|
customization: { enableSealantd: true },
|
|
@@ -63,6 +77,7 @@ export const buildCreateWorkspaceRequest = (options, config) => {
|
|
|
63
77
|
repository: sanitizeRepoSlug(tail),
|
|
64
78
|
tag: `sdk-${randomUUID().slice(0, 8)}`,
|
|
65
79
|
...(options.name === undefined ? {} : { name: options.name }),
|
|
80
|
+
...(options.ttl === undefined ? {} : { ttlSeconds: parseTtlSeconds(options.ttl) }),
|
|
66
81
|
spec,
|
|
67
82
|
},
|
|
68
83
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a human TTL like `"90m"`, `"2h"`, `"45s"`, `"1d"` into whole seconds (milliseconds round
|
|
3
|
+
* up to at least 1s — the wire TTL is second-granular). Throws a typed `SealantError` on anything
|
|
4
|
+
* else so a typo'd TTL fails at the call site instead of silently never expiring.
|
|
5
|
+
*/
|
|
6
|
+
export declare const parseTtlSeconds: (ttl: string) => number;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SealantError } from "../errors.js";
|
|
2
|
+
const DURATION_PATTERN = /^(\d+)(ms|s|m|h|d)$/;
|
|
3
|
+
const UNIT_SECONDS = {
|
|
4
|
+
s: 1,
|
|
5
|
+
m: 60,
|
|
6
|
+
h: 3600,
|
|
7
|
+
d: 86400,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Parse a human TTL like `"90m"`, `"2h"`, `"45s"`, `"1d"` into whole seconds (milliseconds round
|
|
11
|
+
* up to at least 1s — the wire TTL is second-granular). Throws a typed `SealantError` on anything
|
|
12
|
+
* else so a typo'd TTL fails at the call site instead of silently never expiring.
|
|
13
|
+
*/
|
|
14
|
+
export const parseTtlSeconds = (ttl) => {
|
|
15
|
+
const match = DURATION_PATTERN.exec(ttl.trim());
|
|
16
|
+
if (match === null) {
|
|
17
|
+
throw new SealantError(`Invalid TTL duration "${ttl}". Use a positive integer with a unit: e.g. "45s", "90m", "2h", "1d".`, { code: "invalid_ttl" });
|
|
18
|
+
}
|
|
19
|
+
const value = Number.parseInt(match[1] ?? "", 10);
|
|
20
|
+
const unit = match[2] ?? "";
|
|
21
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
22
|
+
throw new SealantError(`Invalid TTL duration "${ttl}". The value must be a positive integer.`, {
|
|
23
|
+
code: "invalid_ttl",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (unit === "ms") {
|
|
27
|
+
return Math.max(1, Math.ceil(value / 1000));
|
|
28
|
+
}
|
|
29
|
+
const multiplier = UNIT_SECONDS[unit];
|
|
30
|
+
if (multiplier === undefined) {
|
|
31
|
+
throw new SealantError(`Invalid TTL duration "${ttl}".`, { code: "invalid_ttl" });
|
|
32
|
+
}
|
|
33
|
+
return value * multiplier;
|
|
34
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -56,8 +56,8 @@ export interface Harness {
|
|
|
56
56
|
/** Optional launch command for an interactive session (defaults to the executable). */
|
|
57
57
|
readonly launchCommand?: string;
|
|
58
58
|
}
|
|
59
|
-
/** Lifecycle status of a workspace.
|
|
60
|
-
export type WorkspaceStatus = "queued" | "running" | "ready" | "failed" | "cancelled";
|
|
59
|
+
/** Lifecycle status of a workspace. */
|
|
60
|
+
export type WorkspaceStatus = "queued" | "running" | "ready" | "failed" | "cancelled" | "stopped";
|
|
61
61
|
/** A coarse lifecycle event observed while a workspace is being provisioned. */
|
|
62
62
|
export interface WorkspaceEvent {
|
|
63
63
|
readonly type: string;
|
|
@@ -90,12 +90,30 @@ export interface WorkspaceCredentialsOptions {
|
|
|
90
90
|
/** `true` for the caller's default GitHub account, or a string naming a specific one. */
|
|
91
91
|
readonly github?: boolean | string;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* A workspace sourced from a CALLER-OWNED host directory instead of a fresh clone. The platform
|
|
95
|
+
* bind-mounts `path` as the workspace working directory and treats it as caller-owned: writes
|
|
96
|
+
* persist across workspace stop/restart/expiry, and the path is never reprovisioned or deleted.
|
|
97
|
+
* The install must allowlist the path's root (`SEALANT_WORKSPACE_MOUNT_ALLOWED_ROOTS`); paths
|
|
98
|
+
* outside the allowlist are rejected at create. Credentials and dotfiles options compose
|
|
99
|
+
* unchanged. Clone-based workspaces remain the right shape for independent verification.
|
|
100
|
+
*/
|
|
101
|
+
export interface WorkspaceMountSource {
|
|
102
|
+
readonly kind: "mount";
|
|
103
|
+
/** Absolute, normalized host path (no `..` segments). */
|
|
104
|
+
readonly path: string;
|
|
105
|
+
}
|
|
93
106
|
export interface CreateOptions {
|
|
94
|
-
/**
|
|
95
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Source git repository to build the workspace around (e.g. `"github.com/acme/billing-service"`).
|
|
109
|
+
* Exactly one of `repository` or `source` must be provided.
|
|
110
|
+
*/
|
|
111
|
+
readonly repository?: string;
|
|
112
|
+
/** Alternative to `repository`: source the workspace from a caller-owned mount. */
|
|
113
|
+
readonly source?: WorkspaceMountSource;
|
|
96
114
|
/** The harness to run inside the workspace. */
|
|
97
115
|
readonly harness: Harness;
|
|
98
|
-
/** Git ref to check out (defaults to the repository's default branch). */
|
|
116
|
+
/** Git ref to check out (defaults to the repository's default branch; `repository` only). */
|
|
99
117
|
readonly ref?: string;
|
|
100
118
|
/** Human-friendly name for the workspace. */
|
|
101
119
|
readonly name?: string;
|
|
@@ -109,6 +127,12 @@ export interface CreateOptions {
|
|
|
109
127
|
readonly onEvent?: (event: WorkspaceEvent) => void;
|
|
110
128
|
/** Connected-account credentials to attach to the workspace (see `WorkspaceCredentialsOptions`). */
|
|
111
129
|
readonly credentials?: WorkspaceCredentialsOptions;
|
|
130
|
+
/**
|
|
131
|
+
* Time-to-live for the workspace, e.g. `"90m"`, `"2h"` (also `"45s"`, `"1d"`). Once it elapses
|
|
132
|
+
* the platform stops the workspace and removes its container. Omitted = the server default TTL
|
|
133
|
+
* (if the install configures one).
|
|
134
|
+
*/
|
|
135
|
+
readonly ttl?: string;
|
|
112
136
|
}
|
|
113
137
|
export interface ListOptions {
|
|
114
138
|
readonly status?: WorkspaceStatus;
|
|
@@ -150,15 +174,20 @@ export interface Workspace {
|
|
|
150
174
|
* run record like any other process. `argv[0]` is the executable, the rest its arguments.
|
|
151
175
|
*/
|
|
152
176
|
exec(argv: readonly string[], options?: WorkspaceExecOptions): Promise<WorkspaceExecResult>;
|
|
177
|
+
/** Interactive PTY sessions: open new ones, reattach to existing ones by id. */
|
|
178
|
+
readonly sessions: WorkspaceSessions;
|
|
153
179
|
/** Lifecycle events as an async stream. */
|
|
154
180
|
events(): AsyncIterable<WorkspaceEvent>;
|
|
155
|
-
/** Stop the workspace
|
|
181
|
+
/** Stop the workspace: remove its container and settle it in the terminal "stopped" status. */
|
|
156
182
|
stop(): Promise<void>;
|
|
157
|
-
/** Restart the workspace into a fresh runtime
|
|
183
|
+
/** Restart the workspace into a fresh runtime — a new container, no filesystem carry-over. */
|
|
158
184
|
restart(): Promise<Workspace>;
|
|
159
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Schedule the workspace to expire: `expire({ in: "2h" })` sets the TTL, `expire()` expires it
|
|
187
|
+
* now (the platform reaper stops it shortly), `expire({ in: null })` clears the TTL.
|
|
188
|
+
*/
|
|
160
189
|
expire(options?: {
|
|
161
|
-
readonly in?: string;
|
|
190
|
+
readonly in?: string | null;
|
|
162
191
|
}): Promise<void>;
|
|
163
192
|
}
|
|
164
193
|
export interface RunOptions {
|
|
@@ -166,9 +195,23 @@ export interface RunOptions {
|
|
|
166
195
|
readonly signal?: AbortSignal;
|
|
167
196
|
/** Idempotency key so a retried call does not start a duplicate run. */
|
|
168
197
|
readonly idempotencyKey?: string;
|
|
198
|
+
/**
|
|
199
|
+
* Opaque correlation bag ({ projectId, sessionId, ... }): stored verbatim by the platform and
|
|
200
|
+
* echoed on reads. No platform-side semantics.
|
|
201
|
+
*/
|
|
202
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
169
203
|
}
|
|
204
|
+
/** Options for opening an interactive PTY session. */
|
|
170
205
|
export interface SessionOptions {
|
|
171
|
-
|
|
206
|
+
/** Working directory inside the workspace (defaults to the repository root). */
|
|
207
|
+
readonly cwd?: string;
|
|
208
|
+
/** Extra environment for the PTY process (not for secrets — use `credentials`). */
|
|
209
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
210
|
+
readonly cols?: number;
|
|
211
|
+
readonly rows?: number;
|
|
212
|
+
readonly term?: string;
|
|
213
|
+
/** Opaque correlation bag, stored verbatim and echoed on reads. */
|
|
214
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
172
215
|
}
|
|
173
216
|
/** Runs a harness in a workspace, one-shot or interactive. */
|
|
174
217
|
export interface HarnessRunner {
|
|
@@ -176,7 +219,7 @@ export interface HarnessRunner {
|
|
|
176
219
|
run(prompt: string, options?: RunOptions): Promise<Run>;
|
|
177
220
|
/** NON-BLOCKING: returns a live handle immediately for streaming via `run.record.stream()`. */
|
|
178
221
|
start(prompt: string, options?: RunOptions): Promise<Run>;
|
|
179
|
-
/**
|
|
222
|
+
/** Opens an interactive PTY session running the harness's launch command. */
|
|
180
223
|
session(options?: SessionOptions): Promise<InteractiveSession>;
|
|
181
224
|
}
|
|
182
225
|
export type RunOutcome = "completed" | "failed";
|
|
@@ -467,12 +510,91 @@ export interface Run {
|
|
|
467
510
|
/** Resolves once the run has terminally completed (no-op if already settled). */
|
|
468
511
|
wait(): Promise<Run>;
|
|
469
512
|
}
|
|
470
|
-
/**
|
|
513
|
+
/** Lifecycle status of an interactive session. */
|
|
514
|
+
export type SessionStatus = "starting" | "running" | "exited" | "failed";
|
|
515
|
+
/** One recorded output chunk. `sequence` is the durable resume cursor. */
|
|
516
|
+
export interface SessionOutputChunk {
|
|
517
|
+
readonly sequence: bigint;
|
|
518
|
+
readonly data: Uint8Array;
|
|
519
|
+
}
|
|
520
|
+
/** A point-in-time report of an interactive session's lifecycle. */
|
|
521
|
+
export interface InteractiveSessionStatus {
|
|
522
|
+
readonly status: SessionStatus;
|
|
523
|
+
readonly exitCode?: number;
|
|
524
|
+
readonly exitSignal?: number;
|
|
525
|
+
/**
|
|
526
|
+
* Highest recorded output sequence — resume a disconnected reader with
|
|
527
|
+
* `output({ from: outputHighWater + 1n })` (or re-read from `0n` for full history).
|
|
528
|
+
*/
|
|
529
|
+
readonly outputHighWater: bigint;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* An interactive PTY session over a live workspace. Sessions are DURABLE PLATFORM RESOURCES, not
|
|
533
|
+
* client connections: the PTY keeps running when this handle (or the whole process) goes away, and
|
|
534
|
+
* a session can be re-fetched by id from any workspace handle (`workspace.sessions.get(id)`) and
|
|
535
|
+
* driven from there. Output is byte-exact, redacted, and sequence-keyed — `output({ from: 0n })`
|
|
536
|
+
* after a reconnect replays the full recorded history and then live-tails.
|
|
537
|
+
*/
|
|
471
538
|
export interface InteractiveSession {
|
|
472
|
-
|
|
473
|
-
|
|
539
|
+
readonly id: string;
|
|
540
|
+
readonly workspaceId: string;
|
|
541
|
+
/** The run recording this session — its record is the durable, replayable evidence. */
|
|
542
|
+
readonly runId: string;
|
|
543
|
+
/** Send keystrokes. Strings are UTF-8-encoded; bytes pass through untouched. */
|
|
544
|
+
send(input: string | Uint8Array): Promise<void>;
|
|
545
|
+
/**
|
|
546
|
+
* Byte-exact output as a RESUMABLE stream: recorded history from `from` (inclusive; default the
|
|
547
|
+
* beginning), then the live tail until the session settles. Each chunk carries its durable
|
|
548
|
+
* sequence, so a disconnected consumer resumes with `from: lastChunk.sequence + 1n`.
|
|
549
|
+
*/
|
|
550
|
+
output(options?: {
|
|
551
|
+
readonly from?: bigint;
|
|
552
|
+
readonly signal?: AbortSignal;
|
|
553
|
+
}): AsyncIterable<SessionOutputChunk>;
|
|
554
|
+
/** Resize the PTY. */
|
|
555
|
+
resize(cols: number, rows: number): Promise<void>;
|
|
556
|
+
/** Deliver a POSIX signal to the session's process (e.g. 2 = SIGINT). */
|
|
557
|
+
signal(signal: number): Promise<void>;
|
|
558
|
+
/** Current lifecycle + the output high-water mark (the resume cursor). */
|
|
559
|
+
status(): Promise<InteractiveSessionStatus>;
|
|
560
|
+
/** Close the PTY (hang up the terminal). Resolves once the session settles. */
|
|
474
561
|
close(): Promise<void>;
|
|
475
562
|
}
|
|
563
|
+
/** Interactive sessions of one workspace: open new ones, reattach to existing ones. */
|
|
564
|
+
export interface WorkspaceSessions {
|
|
565
|
+
/** Opens a PTY session running `argv` (argv[0] is the program). */
|
|
566
|
+
open(argv: readonly string[], options?: SessionOptions): Promise<InteractiveSession>;
|
|
567
|
+
/** Reattach to a session by id — works from ANY handle, not just the creating one. */
|
|
568
|
+
get(sessionId: string): Promise<InteractiveSession>;
|
|
569
|
+
/** Sessions of this workspace, newest first. */
|
|
570
|
+
list(): Promise<readonly InteractiveSession[]>;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Scopes for the session surface: `session:read` (stream/status/output), `session:input`
|
|
574
|
+
* (input/resize/signal), `workspace:exec` (open sessions/terminals, exec). A client holding only
|
|
575
|
+
* `session:read` can stream output but is rejected for input and exec.
|
|
576
|
+
*/
|
|
577
|
+
export type AccessTokenScope = "session:read" | "session:input" | "workspace:exec";
|
|
578
|
+
export interface CreateAccessTokenOptions {
|
|
579
|
+
readonly scopes: readonly AccessTokenScope[];
|
|
580
|
+
readonly name?: string;
|
|
581
|
+
/** Narrow the token to one workspace. */
|
|
582
|
+
readonly workspaceId?: string;
|
|
583
|
+
/** Time-to-live, e.g. `"15m"`, `"2h"`. Omitted = no expiry. */
|
|
584
|
+
readonly ttl?: string;
|
|
585
|
+
}
|
|
586
|
+
export interface CreatedAccessToken {
|
|
587
|
+
readonly tokenId: string;
|
|
588
|
+
/** The bearer secret — shown exactly once, never retrievable again. Use it as `apiKey`. */
|
|
589
|
+
readonly token: string;
|
|
590
|
+
readonly scopes: readonly AccessTokenScope[];
|
|
591
|
+
readonly workspaceId?: string;
|
|
592
|
+
readonly expiresAt?: string;
|
|
593
|
+
}
|
|
594
|
+
/** Mint scoped bearer tokens (e.g. for a mobile pairing flow's per-scope grants). */
|
|
595
|
+
export interface AccessTokensNamespace {
|
|
596
|
+
create(options: CreateAccessTokenOptions): Promise<CreatedAccessToken>;
|
|
597
|
+
}
|
|
476
598
|
/**
|
|
477
599
|
* Connected-account selection for inference — the same reference shape as workspace creation,
|
|
478
600
|
* minus GitHub (not a model provider). `true` means "my default account"; a string names one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"effect": "^4.0.0-beta.85",
|
|
30
|
-
"@sealant/api-contracts": "^0.
|
|
30
|
+
"@sealant/api-contracts": "^0.7.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@effect/vitest": "^4.0.0-beta.85",
|