@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.
@@ -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
- const FAILED_STATUSES = new Set(["failed", "cancelled"]);
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: () => Promise.reject(new SealantNotImplementedError("harness.session (interactive, Phase 3)")),
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: () => Promise.reject(new SealantNotImplementedError("workspace.stop (lifecycle, Phase 3)")),
87
- restart: () => Promise.reject(new SealantNotImplementedError("workspace.restart (lifecycle, Phase 3)")),
88
- expire: () => Promise.reject(new SealantNotImplementedError("workspace.expire (lifecycle, Phase 3)")),
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
  };
@@ -26,5 +26,6 @@ export declare const buildCreateWorkspaceRequest: (options: CreateOptions, confi
26
26
  readonly github?: string | undefined;
27
27
  } | undefined;
28
28
  readonly spec: unknown;
29
+ readonly ttlSeconds?: number | undefined;
29
30
  };
30
31
  };
@@ -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
- const tail = options.repository
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() ?? options.repository;
44
+ .pop() ?? sourceName;
34
45
  const credentials = mapWorkspaceCredentials(options.credentials);
35
46
  const spec = {
36
47
  version: "1",
37
48
  sources: {
38
- workspace: {
39
- kind: "git",
40
- provider: "generic",
41
- url: toGitUrl(options.repository),
42
- ref: options.ref ?? "main",
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. `stopped`/`expired` arrive with lifecycle close-out (Phase 3). */
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
- /** Source git repository to build the workspace around (e.g. `"github.com/acme/billing-service"`). */
95
- readonly repository: string;
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 now (Phase 3). */
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 (Phase 3). */
183
+ /** Restart the workspace into a fresh runtime a new container, no filesystem carry-over. */
158
184
  restart(): Promise<Workspace>;
159
- /** Schedule the workspace to expire (Phase 3). */
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
- readonly signal?: AbortSignal;
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
- /** Interactive session reusing the live workspace (Phase 3). */
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
- /** An interactive harness session over the live workspace (Phase 3). */
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
- send(input: string): Promise<void>;
473
- output(): AsyncIterable<Uint8Array>;
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.5.1",
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.5.1"
30
+ "@sealant/api-contracts": "^0.7.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@effect/vitest": "^4.0.0-beta.85",