@sealant/sdk 0.16.0 → 0.18.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
@@ -105,8 +105,9 @@ and injects them at launch.
105
105
 
106
106
  ## Workspace tools and services
107
107
 
108
- Choose a supported operating-system family, request portable package names, and opt into services
109
- that need runtime support rather than a package install:
108
+ Choose a supported operating-system family `fedora` (the default), `arch`, `nix`, or `ubuntu` —
109
+ request portable package names, and opt into services that need runtime support rather than a
110
+ package install:
110
111
 
111
112
  ```ts
112
113
  const workspace = await sealant.workspaces.create({
@@ -123,6 +124,61 @@ const workspace = await sealant.workspaces.create({
123
124
  rootless daemon at launch. The workspace receives `DOCKER_HOST`; Sealant never mounts the host
124
125
  Docker socket. GitHub credentials provide both `GH_TOKEN` and `GITHUB_TOKEN` to the workspace.
125
126
 
127
+ ## Dotfiles and shell
128
+
129
+ Bring your own environment: a login shell and dotfiles applied before the workspace accepts work.
130
+
131
+ ```ts
132
+ const workspace = await sealant.workspaces.create({
133
+ repository: "github.com/acme/billing-service",
134
+ harness: codex(),
135
+ shell: "zsh",
136
+ dotfiles: {
137
+ // A repo the platform clones and applies (chezmoi/stow layouts detected, else copied):
138
+ repository: { url: "github.com/acme/dotfiles" },
139
+ // And/or caller-resolved archives — a checkout cloned host-side with your own ssh identity,
140
+ // or a scanned selection of home files, sent as gzipped tars (max 4, ~4MB decoded each):
141
+ archives: [{ data: tarGzBase64, manager: "copy", bootstrap: false }],
142
+ },
143
+ });
144
+ ```
145
+
146
+ `shell` installs the shell package and switches the login shell, so `.zshrc`/`.fishrc` actually take
147
+ effect. The `repository` applies first, then each archive in order, so local selections override
148
+ repo files. A failing apply fails the launch loudly rather than handing the agent a half-prepared
149
+ home. Managed OS families only — with `baseImage`, `dotfiles` and non-bash `shell` are rejected
150
+ client-side with the platform's reasoning.
151
+
152
+ ## Custom base images
153
+
154
+ Instead of a managed OS family, a workspace image can be built from any image reference you already
155
+ trust:
156
+
157
+ ```ts
158
+ const workspace = await sealant.workspaces.create({
159
+ repository: "github.com/acme/billing-service",
160
+ harness: codex(),
161
+ baseImage: "node:22-bookworm",
162
+ });
163
+ ```
164
+
165
+ Distro package installs are skipped entirely — the build overlays only the `sealantd` supervisor
166
+ (PID 1), the harness CLIs (installed with `npm`), and a fully static `socat` (the control-socket
167
+ relay), all copied in as static binaries. This is the **base-image contract**, checked at build time
168
+ with readable failures:
169
+
170
+ - **Any Linux base, `amd64`/`arm64`**, with a **POSIX shell** at `/bin/sh`. Shells beyond that are
171
+ not assumed: the workspace login shell is `/bin/sh`, and `defaultShell` selection is not supported
172
+ with `baseImage`.
173
+ - **Node.js + npm** at or above the harness CLIs' floor (the CLIs are installed with
174
+ `npm install -g` and run on the base's node).
175
+ - **git**, for clone- and mount-sourced workspaces.
176
+
177
+ `packages` still works: names pass through **verbatim** (no portable-name resolution) to the base's
178
+ own package manager — `apt`, `apk`, `dnf`, or `pacman`, autodetected — and the build fails with a
179
+ readable error when the base has none. Dotfiles are not supported with `baseImage`. `baseImage` and
180
+ `os` are mutually exclusive.
181
+
126
182
  ## Inference on connected accounts
127
183
 
128
184
  Run short, tool-calling inference loops on the caller's own subscription — server-side, through the
@@ -294,7 +294,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
294
294
  readonly resolvePackage: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
295
295
  readonly query: {
296
296
  readonly query: string;
297
- readonly targetOs?: "arch" | "fedora" | "nix" | undefined;
297
+ readonly targetOs?: "arch" | "fedora" | "nix" | "ubuntu" | undefined;
298
298
  };
299
299
  readonly responseMode?: Mode;
300
300
  }) => Effect.Effect<HttpApiClient.Client.Response<{
@@ -329,6 +329,14 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
329
329
  readonly version?: string | undefined;
330
330
  readonly status?: string | undefined;
331
331
  };
332
+ readonly ubuntu: {
333
+ readonly supported: boolean;
334
+ readonly repo?: string | undefined;
335
+ readonly packageName?: string | undefined;
336
+ readonly projectName?: string | undefined;
337
+ readonly version?: string | undefined;
338
+ readonly status?: string | undefined;
339
+ };
332
340
  };
333
341
  readonly alternatives: readonly {
334
342
  readonly projectName: string;
@@ -1604,7 +1612,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1604
1612
  readonly resolvePackage: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1605
1613
  readonly query: {
1606
1614
  readonly query: string;
1607
- readonly targetOs?: "arch" | "fedora" | "nix" | undefined;
1615
+ readonly targetOs?: "arch" | "fedora" | "nix" | "ubuntu" | undefined;
1608
1616
  };
1609
1617
  readonly responseMode?: Mode;
1610
1618
  }) => Effect.Effect<HttpApiClient.Client.Response<{
@@ -1639,6 +1647,14 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1639
1647
  readonly version?: string | undefined;
1640
1648
  readonly status?: string | undefined;
1641
1649
  };
1650
+ readonly ubuntu: {
1651
+ readonly supported: boolean;
1652
+ readonly repo?: string | undefined;
1653
+ readonly packageName?: string | undefined;
1654
+ readonly projectName?: string | undefined;
1655
+ readonly version?: string | undefined;
1656
+ readonly status?: string | undefined;
1657
+ };
1642
1658
  };
1643
1659
  readonly alternatives: readonly {
1644
1660
  readonly projectName: string;
@@ -38,6 +38,20 @@ export const buildCreateWorkspaceRequest = (options, config) => {
38
38
  code: "invalid_create_options",
39
39
  });
40
40
  }
41
+ if (options.os !== undefined && options.baseImage !== undefined) {
42
+ throw new SealantError("workspaces.create accepts either `os` (a managed OS family) or `baseImage` (a custom base image reference), not both.", { code: "invalid_create_options" });
43
+ }
44
+ if (options.baseImage !== undefined && options.dotfiles !== undefined) {
45
+ throw new SealantError("`dotfiles` is not supported with `baseImage`: custom bases guarantee only a POSIX shell, so the dotfiles managers are not provisioned.", { code: "invalid_create_options" });
46
+ }
47
+ if (options.baseImage !== undefined && options.shell !== undefined && options.shell !== "bash") {
48
+ throw new SealantError("`shell` is not supported with `baseImage`: custom bases run /bin/sh and the login shell cannot be switched.", { code: "invalid_create_options" });
49
+ }
50
+ if (options.dotfiles !== undefined &&
51
+ options.dotfiles.repository === undefined &&
52
+ (options.dotfiles.archives === undefined || options.dotfiles.archives.length === 0)) {
53
+ throw new SealantError("`dotfiles` requires a `repository`, at least one entry in `archives`, or both.", { code: "invalid_create_options" });
54
+ }
41
55
  const sourceName = options.repository ?? options.source?.path ?? "workspace";
42
56
  const tail = sourceName
43
57
  .split("/")
@@ -69,6 +83,8 @@ export const buildCreateWorkspaceRequest = (options, config) => {
69
83
  ...(toolingPackages.length === 0 ? {} : { packages: toolingPackages }),
70
84
  ...(dockerService ? { services: { docker: { enabled: true } } } : {}),
71
85
  };
86
+ const dotfilesRepository = options.dotfiles?.repository;
87
+ const dotfilesArchives = options.dotfiles?.archives ?? [];
72
88
  const spec = {
73
89
  version: "1",
74
90
  sources: {
@@ -81,6 +97,21 @@ export const buildCreateWorkspaceRequest = (options, config) => {
81
97
  ...(options.ref === undefined ? {} : { ref: options.ref }),
82
98
  }
83
99
  : { kind: "mount", hostPath: options.source?.path },
100
+ ...(dotfilesRepository === undefined
101
+ ? {}
102
+ : {
103
+ inputs: [
104
+ {
105
+ id: "dotfiles",
106
+ kind: "git",
107
+ purpose: "dotfiles",
108
+ provider: "generic",
109
+ url: toGitUrl(dotfilesRepository.url),
110
+ // Omitted ref = the remote's default branch — never assumed to be `main`.
111
+ ...(dotfilesRepository.ref === undefined ? {} : { ref: dotfilesRepository.ref }),
112
+ },
113
+ ],
114
+ }),
84
115
  ...(mounts.length === 0
85
116
  ? {}
86
117
  : {
@@ -93,9 +124,39 @@ export const buildCreateWorkspaceRequest = (options, config) => {
93
124
  }),
94
125
  },
95
126
  harness: { id: options.harness.id },
96
- customization: { enableSealantd: true },
127
+ customization: {
128
+ enableSealantd: true,
129
+ ...(options.shell === undefined ? {} : { defaultShell: options.shell }),
130
+ // The repository path's knobs live at customization level; archives carry their own.
131
+ ...(dotfilesRepository?.manager === undefined
132
+ ? {}
133
+ : { dotfilesManager: dotfilesRepository.manager }),
134
+ ...(dotfilesRepository?.bootstrap === undefined
135
+ ? {}
136
+ : { dotfilesBootstrap: dotfilesRepository.bootstrap }),
137
+ ...(dotfilesRepository?.bootstrapCommand === undefined
138
+ ? {}
139
+ : { dotfilesBootstrapCommand: dotfilesRepository.bootstrapCommand }),
140
+ },
141
+ ...(dotfilesArchives.length === 0
142
+ ? {}
143
+ : {
144
+ runtime: {
145
+ dotfilesArchives: dotfilesArchives.map((archive) => ({
146
+ data: archive.data,
147
+ ...(archive.manager === undefined ? {} : { manager: archive.manager }),
148
+ ...(archive.target === undefined ? {} : { target: archive.target }),
149
+ ...(archive.bootstrap === undefined ? {} : { bootstrap: archive.bootstrap }),
150
+ ...(archive.bootstrapCommand === undefined
151
+ ? {}
152
+ : { bootstrapCommand: archive.bootstrapCommand }),
153
+ })),
154
+ },
155
+ }),
97
156
  target: {
98
- os: { family: options.os ?? "fedora", mode: "prefer" },
157
+ os: options.baseImage !== undefined
158
+ ? { family: "custom", mode: "require", baseImage: options.baseImage }
159
+ : { family: options.os ?? "fedora", mode: "prefer" },
99
160
  runtime: { family: "docker", mode: "require" },
100
161
  },
101
162
  lifecycle: {
package/dist/types.d.ts CHANGED
@@ -65,7 +65,8 @@ export interface WorkspaceEvent {
65
65
  readonly message?: string;
66
66
  }
67
67
  /** The supported workspace OS families (maps to the blueprint target). */
68
- export type WorkspaceOs = "fedora" | "arch" | "nix";
68
+ /** Supported workspace image OS families. `fedora` is the default when `os` is omitted. */
69
+ export type WorkspaceOs = "fedora" | "arch" | "nix" | "ubuntu";
69
70
  /**
70
71
  * Connected-account credentials to attach to a workspace at creation time, per provider — so the
71
72
  * harness inside the workspace authenticates as the caller's own Claude / Codex / GitHub identity
@@ -122,6 +123,53 @@ export interface WorkspaceExtraMount {
122
123
  /** Defaults to `true`. Pass `false` deliberately — writes to extra mounts are unrecorded. */
123
124
  readonly readOnly?: boolean;
124
125
  }
126
+ /** How a dotfiles tree is applied inside the workspace. */
127
+ export type WorkspaceDotfilesManager = "auto" | "chezmoi" | "stow" | "copy";
128
+ /**
129
+ * A dotfiles repository the platform clones and applies before the workspace accepts work. Public
130
+ * or GitHub-App-reachable repos work as-is; for a repo only the caller's own ssh identity can
131
+ * reach, resolve it host-side and send the checkout as an archive instead (see
132
+ * `WorkspaceDotfilesArchive`).
133
+ */
134
+ export interface WorkspaceDotfilesRepository {
135
+ /** Clone URL (or `"github.com/acme/dotfiles"` shorthand). */
136
+ readonly url: string;
137
+ /** Git ref. Omitted = the remote's default branch — never assumed to be `main`. */
138
+ readonly ref?: string;
139
+ /** Defaults to `"auto"`: chezmoi/stow layouts are detected, everything else is copied. */
140
+ readonly manager?: WorkspaceDotfilesManager;
141
+ /** Run the repo's bootstrap command after applying (skipped when absent). Defaults to true. */
142
+ readonly bootstrap?: boolean;
143
+ /** Bootstrap command, relative to the checkout. Defaults to `./install.sh`. */
144
+ readonly bootstrapCommand?: string;
145
+ }
146
+ /**
147
+ * A caller-resolved dotfiles tree: a gzipped tar the daemon extracts and applies at boot through
148
+ * the same manager dispatch as a cloned repo. This is the shape for dotfiles resolved host-side —
149
+ * a checkout cloned with the caller's own ssh identity, or a scanned selection of home files —
150
+ * so no URL or credential ever has to reach the workspace. Max 4 archives, ~4MB decoded each.
151
+ */
152
+ export interface WorkspaceDotfilesArchive {
153
+ /** base64 of a `.tar.gz` whose contents apply relative to the target. */
154
+ readonly data: string;
155
+ /** Defaults to `"auto"`. Scanned home files usually want `"copy"`. */
156
+ readonly manager?: WorkspaceDotfilesManager;
157
+ /** Where the tree lands: `"home"` (default) or `"config"` (`$HOME/.config`, copy manager only). */
158
+ readonly target?: "home" | "config";
159
+ /** Run `./install.sh` (or `bootstrapCommand`) after applying when present. Defaults to true. */
160
+ readonly bootstrap?: boolean;
161
+ readonly bootstrapCommand?: string;
162
+ }
163
+ /**
164
+ * Dotfiles for the workspace: a repository the platform clones, caller-resolved archives, or both
165
+ * (the repository applies first, archives after — in order — so local selections override repo
166
+ * files). Applied before the workspace reports ready; a failing apply fails the launch loudly.
167
+ * Not supported with `baseImage` (custom bases guarantee only a POSIX shell).
168
+ */
169
+ export interface WorkspaceDotfilesOptions {
170
+ readonly repository?: WorkspaceDotfilesRepository;
171
+ readonly archives?: readonly WorkspaceDotfilesArchive[];
172
+ }
125
173
  /** Runtime-managed services attached only to this workspace. */
126
174
  export interface WorkspaceServicesOptions {
127
175
  /**
@@ -146,10 +194,27 @@ export interface CreateOptions {
146
194
  readonly ref?: string;
147
195
  /** Human-friendly name for the workspace. */
148
196
  readonly name?: string;
149
- /** OS family for the workspace image. */
197
+ /** OS family for the workspace image. Mutually exclusive with `baseImage`. */
150
198
  readonly os?: WorkspaceOs;
199
+ /**
200
+ * Build the workspace image FROM this arbitrary OCI image reference instead of a managed OS
201
+ * family (e.g. `"node:22-bookworm"`). Distro package installs are skipped; the build overlays
202
+ * only the sealantd supervisor, the harness CLIs (npm), and a static socat relay. See "Custom
203
+ * base images" in the SDK README for the base-image contract. Mutually exclusive with `os`;
204
+ * `packages` install through the base's own package manager (apt/apk/dnf/pacman) and fail the
205
+ * build readable when it has none.
206
+ */
207
+ readonly baseImage?: string;
151
208
  /** Extra OS packages to install in the workspace. */
152
209
  readonly packages?: readonly string[];
210
+ /**
211
+ * Login shell for the workspace user (`"bash"` default). The shell package is installed and the
212
+ * login shell switched, so dotfiles like `.zshrc` actually take effect. Managed OS families
213
+ * only — custom bases guarantee just a POSIX shell.
214
+ */
215
+ readonly shell?: "bash" | "zsh" | "fish";
216
+ /** Dotfiles applied before the workspace accepts work (see `WorkspaceDotfilesOptions`). */
217
+ readonly dotfiles?: WorkspaceDotfilesOptions;
153
218
  /** Runtime-managed services that need more than installing an OS package. */
154
219
  readonly services?: WorkspaceServicesOptions;
155
220
  /** When true (default), resolve only once the workspace runtime is live. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/sdk",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "The fluent public SDK for Sealant — create a workspace, run a harness, replay the record.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@sealant/api-contracts": "^0.16.0"
29
+ "@sealant/api-contracts": "^0.18.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@effect/vitest": "4.0.0-beta.85",