@sealant/sdk 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # @sealant/sdk
2
2
 
3
- The fluent public SDK for Sealant — **create a sandbox, run a harness, replay the record.**
3
+ The fluent public SDK for Sealant — **create a workspace, run a harness, replay the record.**
4
4
 
5
5
  ```ts
6
6
  import { Sealant, opencode } from "@sealant/sdk";
7
7
 
8
8
  const sealant = new Sealant({ baseUrl: "http://localhost:8080" });
9
9
 
10
- const sandbox = await sealant.sandboxes.create({
10
+ const workspace = await sealant.workspaces.create({
11
11
  repository: "github.com/acme/billing-service",
12
12
  harness: opencode(),
13
13
  });
14
14
 
15
- const run = await sandbox.harness.run("Round invoice totals once, after applying the discount.");
15
+ const run = await workspace.harness.run("Round invoice totals once, after applying the discount.");
16
16
 
17
17
  await run.record.replay();
18
18
  ```
@@ -20,8 +20,23 @@ await run.record.replay();
20
20
  ## Design
21
21
 
22
22
  - **Plain-Promise facade over an Effect core.** The default export is ordinary `async`/`await`. The
23
- Effect-native core (services, `Stream`s, typed errors) will be reachable via the
24
- `@sealant/sdk/effect` subpath for power users.
23
+ Effect-native core is reachable via the `@sealant/sdk/effect` subpath for consumers that are
24
+ Effect end-to-end: the contract-derived client as a service, one operation effect per endpoint,
25
+ and the typed contract errors on the failure channel (no squashing) —
26
+
27
+ ```ts
28
+ import { Effect } from "effect";
29
+ import { getRunOp, resolveInternalConfig, sealantApiClientLayer } from "@sealant/sdk/effect";
30
+
31
+ const layer = sealantApiClientLayer(resolveInternalConfig({ baseUrl: "http://localhost:8080" }));
32
+
33
+ const status = getRunOp("run_123").pipe(
34
+ Effect.map((run) => run.status),
35
+ Effect.catchTag("RunNotFoundError", () => Effect.succeed("gone" as const)),
36
+ Effect.provide(layer),
37
+ );
38
+ ```
39
+
25
40
  - **Decoupled public types.** The types in [`src/types.ts`](src/types.ts) are hand-written and kept
26
41
  independent of the Effect-core and `@sealant/telemetry` internal shapes, so the public surface
27
42
  stays stable across internal change. The whole surface is typed now, including operations not yet
@@ -29,13 +44,53 @@ await run.record.replay();
29
44
  - **Harness-neutral.** `opencode()`, `codex()`, `claudeCode()`, and `customHarness()` are thin
30
45
  client values describing how to invoke a harness one-shot.
31
46
 
47
+ ## Deterministic exec
48
+
49
+ Run a command in the workspace with no agent in the loop — recorded into a run record like any other
50
+ process:
51
+
52
+ ```ts
53
+ const check = await workspace.exec(["pnpm", "test"], { cwd: "/workspace/repo" });
54
+ check.exitCode; // the check datum — a NONZERO exit RESOLVES (that's the point)
55
+ check.stdout; // full stdout, decoded
56
+ check.run.record; // the durable evidence
57
+
58
+ // A causal proof is three execs with three recorded exit codes:
59
+ const base = await workspace.exec(["pnpm", "test"]); // fails
60
+ // ...apply the fix...
61
+ const head = await workspace.exec(["pnpm", "test"]); // passes
62
+ ```
63
+
64
+ `exec()` rejects only when the execution machinery itself broke (workspace gone, transport dropped)
65
+ — i.e. when the exit code cannot be trusted. The underlying endpoint
66
+ (`POST /v1/workspaces/:id/exec`) accepts an ordered **list** of commands recorded as one check run;
67
+ the SDK surface starts with the single-command form.
68
+
69
+ ## Typed record events
70
+
71
+ Timeline reads are discriminated by `kind` — switch on it and `data` narrows to the event's typed
72
+ payload (all 12 recorded kinds: process, io, file, network, runtime, and loss events):
73
+
74
+ ```ts
75
+ for await (const entry of run.record.timeline()) {
76
+ if (entry.kind === "networkSourceObserved") {
77
+ entry.data.host; // typed — the raw material of a "sources the agent opened" trail
78
+ entry.data.status;
79
+ }
80
+ }
81
+ ```
82
+
83
+ Forward compatibility is a case, not an error: kinds newer than your SDK version (and payloads that
84
+ fail their schema) arrive as `{ kind: "unknown", rawKind, data }` with everything preserved. Wire
85
+ conventions carry through: uint64 fields are decimal strings, protocol enums are numbers.
86
+
32
87
  ## Connected-account credentials
33
88
 
34
- Attach the caller's connected Claude / Codex / GitHub accounts to a sandbox so the harness
89
+ Attach the caller's connected Claude / Codex / GitHub accounts to a workspace so the harness
35
90
  authenticates as that identity instead of running unauthenticated:
36
91
 
37
92
  ```ts
38
- const sandbox = await sealant.sandboxes.create({
93
+ const workspace = await sealant.workspaces.create({
39
94
  repository: "github.com/acme/billing-service",
40
95
  harness: claudeCode(),
41
96
  credentials: { claude: true, github: "bot-account" },
@@ -48,14 +103,54 @@ field wins over the profile's binding for that provider. Only account references
48
103
  surface — secret material never does; the control plane resolves references to encrypted credentials
49
104
  and injects them at launch.
50
105
 
106
+ ## Inference on connected accounts
107
+
108
+ Run short, tool-calling inference loops on the caller's own subscription — server-side, through the
109
+ official agent SDKs (never raw model-API calls on stored credentials), with the tool loop executed
110
+ on YOUR side:
111
+
112
+ ```ts
113
+ let response = await sealant.inference.respond({
114
+ prompt: "Compile a review brief from this run record.",
115
+ tools: [
116
+ {
117
+ name: "get_timeline",
118
+ inputSchema: { type: "object", properties: { runId: { type: "string" } } },
119
+ },
120
+ ],
121
+ responseFormat: { type: "json", schema: briefSchema },
122
+ credentials: { claude: true },
123
+ });
124
+
125
+ while (response.turn.type === "toolCalls") {
126
+ const toolResults = await Promise.all(
127
+ response.turn.calls.map(async (call) => ({
128
+ toolCallId: call.toolCallId,
129
+ content: await runTool(call.name, call.input),
130
+ })),
131
+ );
132
+ response = await sealant.inference.respond({ sessionId: response.sessionId, toolResults });
133
+ }
134
+
135
+ response.turn.json; // schema-constrained result
136
+ ```
137
+
138
+ Only account references cross the surface — the control plane resolves and decrypts server-side and
139
+ invokes the official Claude Agent SDK with the account's own subscription token. Claude accounts
140
+ only for now (Codex inference is a stated follow-up); sessions are held in memory by the control
141
+ plane and expire after a few idle minutes, so handle a 404 on continuation by restarting the
142
+ exchange.
143
+
51
144
  ## Status
52
145
 
53
- The core loop is real: `sandboxes.create()`/`get()`/`list()`, `ready()`, blocking `harness.run()`
146
+ The core loop is real: `workspaces.create()`/`get()`/`list()`, `ready()`, blocking `harness.run()`
54
147
  and non-blocking `harness.start()` (run execution happens server-side; the SDK is a thin HTTP
55
- client), `runs.get()`, and the record read surface — `replay()`, `timeline()`, `scrollback()`,
56
- `commands()`, `transcript()`, `stream()` (poll-backed), `loss()`, `summary()`, plus captured
57
- `changes` (files + diff) settled by `run()`/`wait()`.
148
+ client), deterministic `workspace.exec()`, `inference.respond()` (connected-account inference with a
149
+ caller-executed tool loop), `runs.get()`, and the record read surface — `replay()`, `timeline()`
150
+ (typed, kind-discriminated entries), `scrollback()`, `commands()`, `transcript()`, `stream()`
151
+ (poll-backed), `loss()`, `summary()`, plus captured `changes` (files + diff) settled by
152
+ `run()`/`wait()`. The Effect-native core ships at `@sealant/sdk/effect`.
58
153
 
59
154
  Still typed stubs pending their read models / endpoints: `artifacts.get()` and the time-travel folds
60
- `fileTreeAt()`/`processTreeAt()` (Phase 1), and `harness.session()` + sandbox lifecycle
155
+ `fileTreeAt()`/`processTreeAt()` (Phase 1), and `harness.session()` + workspace lifecycle
61
156
  `stop()`/`restart()`/`expire()` (Phase 3).
package/dist/client.d.ts CHANGED
@@ -1,16 +1,21 @@
1
- import type { CreateOptions, ListOptions, Run, Sandbox, SealantConfig } from "./types.js";
1
+ import type { CreateOptions, InferenceNamespace, ListOptions, Run, Workspace, SealantConfig } from "./types.js";
2
2
  export declare class Sealant {
3
3
  #private;
4
4
  constructor(config: SealantConfig);
5
5
  /** The configured control-plane base URL. */
6
6
  get baseUrl(): string;
7
- /** Sandbox lifecycle: create, fetch, and list live environments. */
8
- readonly sandboxes: {
9
- create: (options: CreateOptions) => Promise<Sandbox>;
10
- get: (id: string) => Promise<Sandbox>;
11
- list: (options?: ListOptions | undefined) => Promise<readonly Sandbox[]>;
7
+ /** Workspace lifecycle: create, fetch, and list live environments. */
8
+ readonly workspaces: {
9
+ create: (options: CreateOptions) => Promise<Workspace>;
10
+ get: (id: string) => Promise<Workspace>;
11
+ list: (options?: ListOptions | undefined) => Promise<readonly Workspace[]>;
12
12
  };
13
- /** Runs by id — so a record can be replayed long after its sandbox is gone. */
13
+ /**
14
+ * Inference on connected accounts — server-side via the official agent SDKs, never raw model-API
15
+ * calls. Tool calls park server-side; execute them here and `respond()` with the results.
16
+ */
17
+ readonly inference: InferenceNamespace;
18
+ /** Runs by id — so a record can be replayed long after its workspace is gone. */
14
19
  readonly runs: {
15
20
  get: (runId: string) => Promise<Run>;
16
21
  };
package/dist/client.js CHANGED
@@ -10,16 +10,17 @@
10
10
  * stable surface and reject with `SealantNotImplementedError` so callers can compile and wire
11
11
  * against the final shape today.
12
12
  */
13
- import { createSandboxOp, getRunOp, getSandboxOp, listSandboxesOp } from "./effect/operations.js";
13
+ import { createWorkspaceOp, getRunOp, getWorkspaceOp, inferenceRespondOp, listWorkspacesOp, } from "./effect/operations.js";
14
14
  import { runHarness, startHarness } from "./effect/run-harness.js";
15
15
  import { makeSdkRuntime } from "./effect/runtime.js";
16
16
  import { SealantError } from "./errors.js";
17
17
  import { makeRun } from "./facade/run.js";
18
- import { makeSandbox, registerHarnessExecutors } from "./facade/sandbox.js";
19
- import { buildCreateSandboxRequest } from "./internal/blueprint.js";
18
+ import { makeWorkspace, registerHarnessExecutors } from "./facade/workspace.js";
19
+ import { buildCreateWorkspaceRequest } from "./internal/blueprint.js";
20
20
  import { resolveInternalConfig } from "./internal/config.js";
21
- // Wire the run-execution implementations into the Sandbox facade (the injection point exists to
22
- // break the sandbox <-> run-harness import cycle; the client is the composition root).
21
+ import { buildInferenceRespondRequest, mapInferenceResponse } from "./internal/inference.js";
22
+ // Wire the run-execution implementations into the Workspace facade (the injection point exists to
23
+ // break the workspace <-> run-harness import cycle; the client is the composition root).
23
24
  registerHarnessExecutors({ run: runHarness, start: startHarness });
24
25
  export class Sealant {
25
26
  #config;
@@ -38,19 +39,19 @@ export class Sealant {
38
39
  get baseUrl() {
39
40
  return this.#config.baseUrl;
40
41
  }
41
- /** Sandbox lifecycle: create, fetch, and list live environments. */
42
- sandboxes = {
42
+ /** Workspace lifecycle: create, fetch, and list live environments. */
43
+ workspaces = {
43
44
  create: async (options) => {
44
- const { payload } = buildCreateSandboxRequest(options, this.#ctx.config);
45
- const created = await this.#runtime.run(createSandboxOp(payload));
46
- const sandbox = makeSandbox(this.#ctx, {
47
- id: created.sandboxId,
45
+ const { payload } = buildCreateWorkspaceRequest(options, this.#ctx.config);
46
+ const created = await this.#runtime.run(createWorkspaceOp(payload));
47
+ const workspace = makeWorkspace(this.#ctx, {
48
+ id: created.workspaceId,
48
49
  name: created.name,
49
50
  status: created.status,
50
51
  harness: options.harness,
51
52
  });
52
53
  if (options.wait === false) {
53
- return sandbox;
54
+ return workspace;
54
55
  }
55
56
  // Pump provisioning events to onEvent (best-effort) while we wait for ready. Never let an
56
57
  // event-stream hiccup fail create().
@@ -58,7 +59,7 @@ export class Sealant {
58
59
  const onEvent = options.onEvent;
59
60
  void (async () => {
60
61
  try {
61
- for await (const event of sandbox.events()) {
62
+ for await (const event of workspace.events()) {
62
63
  onEvent(event);
63
64
  }
64
65
  }
@@ -67,30 +68,41 @@ export class Sealant {
67
68
  }
68
69
  })();
69
70
  }
70
- return sandbox.ready();
71
+ return workspace.ready();
71
72
  },
72
73
  get: async (id) => {
73
- const details = await this.#runtime.run(getSandboxOp(id));
74
- return makeSandbox(this.#ctx, {
75
- id: details.sandboxId,
74
+ const details = await this.#runtime.run(getWorkspaceOp(id));
75
+ return makeWorkspace(this.#ctx, {
76
+ id: details.workspaceId,
76
77
  name: details.name,
77
78
  status: details.status,
78
79
  });
79
80
  },
80
81
  list: async (options) => {
81
- const response = await this.#runtime.run(listSandboxesOp({
82
+ const response = await this.#runtime.run(listWorkspacesOp({
82
83
  ownerUserId: this.#ctx.config.hostLocal.ownerUserId,
83
84
  ...(options?.status === undefined ? {} : { status: options.status }),
84
85
  ...(options?.limit === undefined ? {} : { limit: String(options.limit) }),
85
86
  }));
86
- return response.items.map((item) => makeSandbox(this.#ctx, {
87
- id: item.sandboxId,
87
+ return response.items.map((item) => makeWorkspace(this.#ctx, {
88
+ id: item.workspaceId,
88
89
  name: item.name,
89
90
  status: item.status,
90
91
  }));
91
92
  },
92
93
  };
93
- /** Runs by id — so a record can be replayed long after its sandbox is gone. */
94
+ /**
95
+ * Inference on connected accounts — server-side via the official agent SDKs, never raw model-API
96
+ * calls. Tool calls park server-side; execute them here and `respond()` with the results.
97
+ */
98
+ inference = {
99
+ respond: async (options) => {
100
+ const payload = buildInferenceRespondRequest(options, this.#ctx.config.hostLocal.ownerUserId);
101
+ const wire = await this.#runtime.run(inferenceRespondOp(payload));
102
+ return mapInferenceResponse(wire);
103
+ },
104
+ };
105
+ /** Runs by id — so a record can be replayed long after its workspace is gone. */
94
106
  runs = {
95
107
  get: async (runId) => {
96
108
  const wire = await this.#runtime.run(getRunOp(runId));