@sealant/sdk-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/README.md +366 -0
- package/dist/client.d.ts +65 -0
- package/dist/client.js +261 -0
- package/dist/effect/api-client.d.ts +3404 -0
- package/dist/effect/api-client.js +29 -0
- package/dist/effect/exec-workspace.d.ts +5 -0
- package/dist/effect/exec-workspace.js +78 -0
- package/dist/effect/index.d.ts +27 -0
- package/dist/effect/index.js +31 -0
- package/dist/effect/operations.d.ts +907 -0
- package/dist/effect/operations.js +69 -0
- package/dist/effect/run-harness.d.ts +9 -0
- package/dist/effect/run-harness.js +82 -0
- package/dist/effect/runtime.d.ts +22 -0
- package/dist/effect/runtime.js +63 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +47 -0
- package/dist/facade/context.d.ts +9 -0
- package/dist/facade/context.js +9 -0
- package/dist/facade/record.d.ts +36 -0
- package/dist/facade/record.js +252 -0
- package/dist/facade/run.d.ts +34 -0
- package/dist/facade/run.js +66 -0
- package/dist/facade/session.d.ts +23 -0
- package/dist/facade/session.js +263 -0
- package/dist/facade/workspace.d.ts +41 -0
- package/dist/facade/workspace.js +573 -0
- package/dist/harness.d.ts +36 -0
- package/dist/harness.js +43 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/internal/blueprint.d.ts +37 -0
- package/dist/internal/blueprint.js +248 -0
- package/dist/internal/config.d.ts +24 -0
- package/dist/internal/config.js +17 -0
- package/dist/internal/credentials.d.ts +22 -0
- package/dist/internal/credentials.js +26 -0
- package/dist/internal/duration.d.ts +6 -0
- package/dist/internal/duration.js +34 -0
- package/dist/internal/inference.d.ts +47 -0
- package/dist/internal/inference.js +78 -0
- package/dist/internal/linked-worktree.d.ts +13 -0
- package/dist/internal/linked-worktree.js +74 -0
- package/dist/internal/map-error.d.ts +8 -0
- package/dist/internal/map-error.js +44 -0
- package/dist/internal/ssh.d.ts +17 -0
- package/dist/internal/ssh.js +15 -0
- package/dist/types.d.ts +1630 -0
- package/dist/types.js +17 -0
- package/package.json +34 -9
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# @sealant/sdk
|
|
2
|
+
|
|
3
|
+
The fluent public SDK for Sealant — **create a workspace, run a harness, replay the record.**
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Sealant, opencode } from "@sealant/sdk";
|
|
7
|
+
|
|
8
|
+
const sealant = new Sealant({ baseUrl: "http://localhost:8080" });
|
|
9
|
+
|
|
10
|
+
const workspace = await sealant.workspaces.create({
|
|
11
|
+
repository: "github.com/acme/billing-service",
|
|
12
|
+
harness: opencode(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const run = await workspace.harness.run("Round invoice totals once, after applying the discount.");
|
|
16
|
+
|
|
17
|
+
await run.record.replay();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Design
|
|
21
|
+
|
|
22
|
+
- **Plain-Promise facade over an Effect core.** The default export is ordinary `async`/`await`. The
|
|
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
|
+
|
|
40
|
+
- **Decoupled public types.** The types in [`src/types.ts`](src/types.ts) are hand-written and kept
|
|
41
|
+
independent of the Effect-core and `@sealant/telemetry` internal shapes, so the public surface
|
|
42
|
+
stays stable across internal change. The whole surface is typed now, including operations not yet
|
|
43
|
+
implemented (those reject with `SealantNotImplementedError`).
|
|
44
|
+
- **Harness-neutral.** `opencode()`, `codex()`, `claudeCode()`, and `customHarness()` are thin
|
|
45
|
+
client values describing how to invoke a harness one-shot.
|
|
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
|
+
|
|
87
|
+
## Connected-account credentials
|
|
88
|
+
|
|
89
|
+
Attach the caller's connected Claude / Codex / GitHub accounts to a workspace so the harness
|
|
90
|
+
authenticates as that identity instead of running unauthenticated:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const workspace = await sealant.workspaces.create({
|
|
94
|
+
repository: "github.com/acme/billing-service",
|
|
95
|
+
harness: claudeCode(),
|
|
96
|
+
credentials: { claude: true, github: "bot-account" },
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`true` means "my default account"; a string names a specific connected account. `profile` names a
|
|
101
|
+
profile whose bundled per-provider bindings apply first, and any explicit `claude`/`codex`/`github`
|
|
102
|
+
field wins over the profile's binding for that provider. Only account references cross the SDK
|
|
103
|
+
surface — secret material never does; the control plane resolves references to encrypted credentials
|
|
104
|
+
and injects them at launch.
|
|
105
|
+
|
|
106
|
+
## Workspace tools and services
|
|
107
|
+
|
|
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:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const workspace = await sealant.workspaces.create({
|
|
114
|
+
repository: "github.com/acme/billing-service",
|
|
115
|
+
harness: codex(),
|
|
116
|
+
os: "arch",
|
|
117
|
+
packages: ["pnpm", "python", "uv", "mise", "github-cli", "lazygit", "bat"],
|
|
118
|
+
services: { docker: true },
|
|
119
|
+
credentials: { codex: true, github: true },
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`services.docker` installs the Docker client in the image and starts a disposable, workspace-scoped
|
|
124
|
+
rootless daemon at launch. The workspace receives `DOCKER_HOST`; Sealant never mounts the host
|
|
125
|
+
Docker socket. On Kubernetes the daemon is a sidecar of a user-namespaced workspace Pod and the
|
|
126
|
+
operator has to enable it (`workspaces.docker.enabled`); an install that cannot serve it refuses
|
|
127
|
+
`create` with the stable code `workspace-docker-unsupported` (HTTP 422) — map that code to explain
|
|
128
|
+
the gap beside your Docker switch instead of surfacing a launch failure later. GitHub credentials
|
|
129
|
+
provide both `GH_TOKEN` and `GITHUB_TOKEN` to the workspace.
|
|
130
|
+
|
|
131
|
+
## Workspace environment variables
|
|
132
|
+
|
|
133
|
+
Ordinary (non-secret) configuration set on the workspace at creation and inherited by every process
|
|
134
|
+
the platform starts inside it — the harness, later shells, exec'd commands, and their descendants:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const workspace = await sealant.workspaces.create({
|
|
138
|
+
repository: "github.com/acme/billing-service",
|
|
139
|
+
harness: codex(),
|
|
140
|
+
env: { APP_MODE: "review", FEATURE_FLAGS: "checkout,invoices" },
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The contract, stated plainly:
|
|
145
|
+
|
|
146
|
+
- **Not for secrets.** Values are persisted verbatim in the durable workspace spec and are returned
|
|
147
|
+
by workspace-details APIs to authorized clients for the life of that record. Use `credentials` for
|
|
148
|
+
connected-account material. There is no partial support: secret-looking names — containing
|
|
149
|
+
`TOKEN`, `SECRET`, `PASSWORD`, `PASSWD`, `CREDENTIAL`, or `APIKEY` (as a substring, so
|
|
150
|
+
`TOKENIZER_PATH` counts), ending in `_KEY`, or exactly `KEY` — are rejected at create, because the
|
|
151
|
+
workspace runtime's secret filter would silently drop them before any process could see them. A
|
|
152
|
+
loud rejection beats a variable that never arrives.
|
|
153
|
+
- **Validated, client-side and server-side, with the same policy.** Names are
|
|
154
|
+
`[A-Za-z_][A-Za-z0-9_]*` (max 128 chars); values are any UTF-8 up to 4 KiB (empty and multiline
|
|
155
|
+
included, NUL excluded); at most 128 entries and 32 KiB total per workspace. Platform-owned names
|
|
156
|
+
(`SEALANT_*`, `HOME`, `PATH`, `TERM`, `DOCKER_HOST`, proxy variables, loader/shell/
|
|
157
|
+
runtime-injection controls like `LD_*`, `BASH_ENV`, `NODE_OPTIONS`, `PYTHONPATH`, and Git/SSH
|
|
158
|
+
config controls) are rejected. The policy is exported (`parseWorkspaceEnv`,
|
|
159
|
+
`findWorkspaceEnvReservedRule`, `formatWorkspaceEnvIssue`, `WORKSPACE_ENV_*` constants) so your
|
|
160
|
+
own settings surface can validate with the platform's exact rules.
|
|
161
|
+
- **Fixed at creation.** A live workspace is never mutated; a platform-side restart reuses the
|
|
162
|
+
stored spec. Caller values can never override platform controls or injected connected-account
|
|
163
|
+
credentials.
|
|
164
|
+
- **No nested-container injection.** Docker Compose or `docker run` inside the workspace can use the
|
|
165
|
+
values for interpolation, but child containers receive only what the Compose file or the command
|
|
166
|
+
explicitly passes (`environment`, `env_file`, `-e`). Docker runtime only.
|
|
167
|
+
|
|
168
|
+
## Secret environment variables
|
|
169
|
+
|
|
170
|
+
The half of a real `.env` that `env` deliberately refuses — API keys, database URLs with passwords —
|
|
171
|
+
goes through the **transient secret channel**:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const workspace = await sealant.workspaces.create({
|
|
175
|
+
repository: "github.com/acme/billing-service",
|
|
176
|
+
harness: codex(),
|
|
177
|
+
env: { APP_MODE: "review" },
|
|
178
|
+
secretEnv: {
|
|
179
|
+
DATABASE_URL: "postgres://app:s3cret@db.internal/billing",
|
|
180
|
+
STRIPE_API_KEY: "sk_live_…",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
What the platform guarantees for `secretEnv`, and how it differs from `env`:
|
|
186
|
+
|
|
187
|
+
- **Same grammar and size bounds** (`parseWorkspaceSecretEnv`, exported), and the same
|
|
188
|
+
platform-owned names are reserved — but secret-shaped names are exactly what belongs here.
|
|
189
|
+
Connected-account names (`GITHUB_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `GH_TOKEN`) stay reserved:
|
|
190
|
+
attach those through `credentials`.
|
|
191
|
+
- **Never persisted in the clear.** The map rides the create request beside the spec, is sealed with
|
|
192
|
+
the install's credential key on the build job, is decrypted by the worker just before launch, and
|
|
193
|
+
the sealed row is cleared once the launch settles. It is never in the blueprint, the attempt
|
|
194
|
+
snapshot, `WorkspaceDetails`, or any read API.
|
|
195
|
+
- **Never in `docker run` argv or container env.** The worker stages a `0600` file the workspace
|
|
196
|
+
daemon reads once at boot and removes it the moment the workspace is ready. `docker inspect` shows
|
|
197
|
+
a file path, not values.
|
|
198
|
+
- **Inherited by every process the platform starts in the workspace**, winning over `env` and
|
|
199
|
+
container env for the same name — the harness, later shells, exec'd commands, Services.
|
|
200
|
+
- **Masked in captured output.** Every value seeds the daemon's redactor regardless of its name, so
|
|
201
|
+
a `DATABASE_URL` a process echoes is recorded as `***REDACTED***`, like a token.
|
|
202
|
+
- **Fixed at creation.** A platform-side _restart_ of the workspace runs **without** secret env (the
|
|
203
|
+
sealed copy is gone by design); create a new workspace to re-supply it. Docker runtime only;
|
|
204
|
+
nested containers started by Compose or `docker run` inside the workspace still receive only what
|
|
205
|
+
you explicitly pass.
|
|
206
|
+
|
|
207
|
+
Not covered, and worth saying plainly: a process that _deliberately_ writes a secret to a file in
|
|
208
|
+
the repository or to a mount is producing ordinary workspace state, and the redactor masks captured
|
|
209
|
+
I/O, not files.
|
|
210
|
+
|
|
211
|
+
## Capture-sourced workspaces
|
|
212
|
+
|
|
213
|
+
A `capture` source (sealantd ADR-0015) mounts nothing and clones nothing. The workspace daemon
|
|
214
|
+
registers with a session channel, fetches the worktree's head plan, materialises it onto the
|
|
215
|
+
executor's own disk, claims the lease, and ships captures back over the channel — so a session can
|
|
216
|
+
run where no host path exists (a Cloudflare sandbox, a MicroVM) and outlive any one executor:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const workspace = await sealant.workspaces.create({
|
|
220
|
+
source: {
|
|
221
|
+
kind: "capture",
|
|
222
|
+
endpoint: "https://mend.example.com/session/s1",
|
|
223
|
+
worktreeId: "wt_1",
|
|
224
|
+
token: sessionToken, // the channel credential; secret
|
|
225
|
+
harnessHome: "/workspace/harness-home",
|
|
226
|
+
},
|
|
227
|
+
harness: claudeCode(),
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- `token` is lifted out of the blueprint and rides the request top level as `captureToken`. The
|
|
232
|
+
control plane seals it beside `secretEnv` and delivers it through the same boot file, as
|
|
233
|
+
`SEALANT_CAPTURE_TOKEN`; it is never in the spec, `WorkspaceDetails`, argv, or container env.
|
|
234
|
+
- The channel address and worktree id reach the daemon as `SEALANT_CAPTURE_ENDPOINT` and
|
|
235
|
+
`SEALANT_CAPTURE_WORKTREE_ID`, with `SEALANT_WORKSPACE_SOURCE=capture`.
|
|
236
|
+
- `worktreeId` is optional. A standby executor launched ahead of any worktree omits it;
|
|
237
|
+
`SEALANT_CAPTURE_WORKTREE_ID` stays unset and the daemon takes the worktree from the channel's
|
|
238
|
+
plan answer.
|
|
239
|
+
- `harnessHome` is optional. It names an absolute, normalized directory on the executor's own
|
|
240
|
+
filesystem. When a captured head contains a `harness/` subtree, cold materialization restores it
|
|
241
|
+
there. The option does not create or relocate `HOME`, choose an agent state directory, or route
|
|
242
|
+
harness writes. Configure the harness to write there and, for an empty chain, create the directory
|
|
243
|
+
before the daemon watcher starts. The daemon then captures that existing directory and keeps using
|
|
244
|
+
the same path after a standby claim or capture replan. Omitting the option keeps the legacy
|
|
245
|
+
behavior and captures no harness subtree.
|
|
246
|
+
- `transport` is optional. Without it the daemon dials `endpoint` and every object URL over HTTPS
|
|
247
|
+
with a certificate the public roots verify, and refuses to boot otherwise. Set
|
|
248
|
+
`transport.plaintext: true` only when the executor and the channel share a private network;
|
|
249
|
+
`transport.channelCaPem` and `transport.objectCaPem` name a private CA for each path. The control
|
|
250
|
+
plane refuses a plain-HTTP endpoint beyond loopback that `plaintext` does not account for, and an
|
|
251
|
+
endpoint outside the operator's `SEALANT_CAPTURE_ALLOWED_ENDPOINTS`.
|
|
252
|
+
- `harnessHome` is not a host mount. It cannot overlap the workspace working directory, the daemon
|
|
253
|
+
control directory, or an extra mount target. No mount allowlist applies to the harness root. On
|
|
254
|
+
Kubernetes the workspace root is an `emptyDir`; on Cloudflare the sandbox is kept alive while the
|
|
255
|
+
lease is live and a stop sends SIGTERM so the daemon can flush.
|
|
256
|
+
- Harness files receive the existing capture policy. This option does not add automatic credential
|
|
257
|
+
discovery or change which daemon credentials are excluded from captures.
|
|
258
|
+
- **No restart in place**: the credential is discarded once the launch settles, so `restart()` is
|
|
259
|
+
refused; create a replacement workspace with a fresh token.
|
|
260
|
+
|
|
261
|
+
## Dotfiles and shell
|
|
262
|
+
|
|
263
|
+
Bring your own environment: a login shell and dotfiles applied before the workspace accepts work.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const workspace = await sealant.workspaces.create({
|
|
267
|
+
repository: "github.com/acme/billing-service",
|
|
268
|
+
harness: codex(),
|
|
269
|
+
shell: "zsh",
|
|
270
|
+
dotfiles: {
|
|
271
|
+
// A repo the platform clones and applies (chezmoi/stow layouts detected, else copied):
|
|
272
|
+
repository: { url: "github.com/acme/dotfiles" },
|
|
273
|
+
// And/or caller-resolved archives — a checkout cloned host-side with your own ssh identity,
|
|
274
|
+
// or a scanned selection of home files, sent as gzipped tars (max 4, ~4MB decoded each):
|
|
275
|
+
archives: [{ data: tarGzBase64, manager: "copy", bootstrap: false }],
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`shell` installs the shell package and switches the login shell, so `.zshrc`/`.fishrc` actually take
|
|
281
|
+
effect. The `repository` applies first, then each archive in order, so local selections override
|
|
282
|
+
repo files. A failing apply fails the launch loudly rather than handing the agent a half-prepared
|
|
283
|
+
home. Managed OS families only — with `baseImage`, `dotfiles` and non-bash `shell` are rejected
|
|
284
|
+
client-side with the platform's reasoning.
|
|
285
|
+
|
|
286
|
+
## Custom base images
|
|
287
|
+
|
|
288
|
+
Instead of a managed OS family, a workspace image can be built from any image reference you already
|
|
289
|
+
trust:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const workspace = await sealant.workspaces.create({
|
|
293
|
+
repository: "github.com/acme/billing-service",
|
|
294
|
+
harness: codex(),
|
|
295
|
+
baseImage: "node:22-bookworm",
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Distro package installs are skipped entirely — the build overlays only the `sealantd` supervisor
|
|
300
|
+
(PID 1), the harness CLIs (installed with `npm`), and a fully static `socat` (the control-socket
|
|
301
|
+
relay), all copied in as static binaries. This is the **base-image contract**, checked at build time
|
|
302
|
+
with readable failures:
|
|
303
|
+
|
|
304
|
+
- **Any Linux base, `amd64`/`arm64`**, with a **POSIX shell** at `/bin/sh`. Shells beyond that are
|
|
305
|
+
not assumed: the workspace login shell is `/bin/sh`, and `defaultShell` selection is not supported
|
|
306
|
+
with `baseImage`.
|
|
307
|
+
- **Node.js + npm** at or above the harness CLIs' floor (the CLIs are installed with
|
|
308
|
+
`npm install -g` and run on the base's node).
|
|
309
|
+
- **git**, for clone-, mount- and capture-sourced workspaces.
|
|
310
|
+
|
|
311
|
+
`packages` still works: names pass through **verbatim** (no portable-name resolution) to the base's
|
|
312
|
+
own package manager — `apt`, `apk`, `dnf`, or `pacman`, autodetected — and the build fails with a
|
|
313
|
+
readable error when the base has none. Dotfiles are not supported with `baseImage`. `baseImage` and
|
|
314
|
+
`os` are mutually exclusive.
|
|
315
|
+
|
|
316
|
+
## Inference on connected accounts
|
|
317
|
+
|
|
318
|
+
Run short, tool-calling inference loops on the caller's own subscription — server-side, through the
|
|
319
|
+
official agent SDKs (never raw model-API calls on stored credentials), with the tool loop executed
|
|
320
|
+
on YOUR side:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
let response = await sealant.inference.respond({
|
|
324
|
+
prompt: "Compile a review brief from this run record.",
|
|
325
|
+
tools: [
|
|
326
|
+
{
|
|
327
|
+
name: "get_timeline",
|
|
328
|
+
inputSchema: { type: "object", properties: { runId: { type: "string" } } },
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
responseFormat: { type: "json", schema: briefSchema },
|
|
332
|
+
credentials: { claude: true },
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
while (response.turn.type === "toolCalls") {
|
|
336
|
+
const toolResults = await Promise.all(
|
|
337
|
+
response.turn.calls.map(async (call) => ({
|
|
338
|
+
toolCallId: call.toolCallId,
|
|
339
|
+
content: await runTool(call.name, call.input),
|
|
340
|
+
})),
|
|
341
|
+
);
|
|
342
|
+
response = await sealant.inference.respond({ sessionId: response.sessionId, toolResults });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
response.turn.json; // schema-constrained result
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Only account references cross the surface — the control plane resolves and decrypts server-side and
|
|
349
|
+
invokes the official Claude Agent SDK with the account's own subscription token. Claude accounts
|
|
350
|
+
only for now (Codex inference is a stated follow-up); sessions are held in memory by the control
|
|
351
|
+
plane and expire after a few idle minutes, so handle a 404 on continuation by restarting the
|
|
352
|
+
exchange.
|
|
353
|
+
|
|
354
|
+
## Status
|
|
355
|
+
|
|
356
|
+
The core loop is real: `workspaces.create()`/`get()`/`list()`, `ready()`, blocking `harness.run()`
|
|
357
|
+
and non-blocking `harness.start()` (run execution happens server-side; the SDK is a thin HTTP
|
|
358
|
+
client), deterministic `workspace.exec()`, `inference.respond()` (connected-account inference with a
|
|
359
|
+
caller-executed tool loop), `runs.get()`, and the record read surface — `replay()`, `timeline()`
|
|
360
|
+
(typed, kind-discriminated entries), `scrollback()`, `commands()`, `transcript()`, `stream()`
|
|
361
|
+
(poll-backed), `loss()`, `summary()`, plus captured `changes` (files + diff) settled by
|
|
362
|
+
`run()`/`wait()`. The Effect-native core ships at `@sealant/sdk/effect`.
|
|
363
|
+
|
|
364
|
+
Still typed stubs pending their read models / endpoints: `artifacts.get()` and the time-travel folds
|
|
365
|
+
`fileTreeAt()`/`processTreeAt()` (Phase 1), and `harness.session()` + workspace lifecycle
|
|
366
|
+
`stop()`/`restart()`/`expire()` (Phase 3).
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { AccessTokensNamespace, ConnectedAccountsNamespace, CreateOptions, InferenceNamespace, ListOptions, Run, SshKeysNamespace, Workspace, WorkspaceCreateState, WorkspaceSshNamespace, SealantConfig, UsersNamespace } from "./types.js";
|
|
2
|
+
export declare class Sealant {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(config: SealantConfig);
|
|
5
|
+
/** The configured control-plane base URL. */
|
|
6
|
+
get baseUrl(): string;
|
|
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
|
+
/**
|
|
12
|
+
* The owner's workspace a `create({ idempotencyKey })` made, or `null`: what a caller that
|
|
13
|
+
* lost a create's answer uses to find the executor it started.
|
|
14
|
+
*/
|
|
15
|
+
findByIdempotencyKey: (idempotencyKey: string) => Promise<Workspace | null>;
|
|
16
|
+
/**
|
|
17
|
+
* What became of the create with this idempotency key: `pending` (started, not committed),
|
|
18
|
+
* `found` (committed: `workspaceId`, `runId`, `launchId`), `cancelled`, or `none` (nothing has
|
|
19
|
+
* reached the control plane — as of now only; use `cancelCreate` for an answer that stays
|
|
20
|
+
* true).
|
|
21
|
+
*/
|
|
22
|
+
createState: (idempotencyKey: string) => Promise<WorkspaceCreateState>;
|
|
23
|
+
/**
|
|
24
|
+
* Make sure the create with this idempotency key never launches: a pending or unknown key is
|
|
25
|
+
* cancelled for good (a delayed original request with it is refused, `create-cancelled`). A
|
|
26
|
+
* create that already committed answers `found` — stop that workspace instead.
|
|
27
|
+
*/
|
|
28
|
+
cancelCreate: (idempotencyKey: string) => Promise<WorkspaceCreateState>;
|
|
29
|
+
list: (options?: ListOptions | undefined) => Promise<readonly Workspace[]>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Inference on connected accounts — server-side via the official agent SDKs, never raw model-API
|
|
33
|
+
* calls. Tool calls park server-side; execute them here and `respond()` with the results.
|
|
34
|
+
*/
|
|
35
|
+
readonly inference: InferenceNamespace;
|
|
36
|
+
/**
|
|
37
|
+
* Scoped bearer tokens for the session surface — `session:read` / `session:input` /
|
|
38
|
+
* `workspace:exec`. The returned secret is shown once; hand it to a client as its `apiKey` and
|
|
39
|
+
* the session endpoints enforce exactly those scopes (a read-stream token can stream but is
|
|
40
|
+
* rejected for input and exec).
|
|
41
|
+
*/
|
|
42
|
+
/** Identity rows for products that own their own login (one client per user afterwards). */
|
|
43
|
+
readonly users: UsersNamespace;
|
|
44
|
+
/** This client's owner's Claude / Codex / GitHub accounts. Secrets go in; none come out. */
|
|
45
|
+
readonly connectedAccounts: ConnectedAccountsNamespace;
|
|
46
|
+
/**
|
|
47
|
+
* Where workspace SSH connects for this deployment — how an editor (VS Code Remote-SSH) or
|
|
48
|
+
* plain `ssh` reaches a workspace. Destination: `<usernamePrefix>-<workspaceId>@<host>:<port>`;
|
|
49
|
+
* the gateway authorizes each connection from the offered key's owning account.
|
|
50
|
+
*/
|
|
51
|
+
readonly workspaceSsh: WorkspaceSshNamespace;
|
|
52
|
+
/**
|
|
53
|
+
* The owner's SSH public keys — what the workspace SSH gateway resolves a connection to.
|
|
54
|
+
* `ensure` is idempotent: re-offering the same key returns the existing row.
|
|
55
|
+
*/
|
|
56
|
+
readonly sshKeys: SshKeysNamespace;
|
|
57
|
+
readonly accessTokens: AccessTokensNamespace;
|
|
58
|
+
/** Runs by id — so a record can be replayed long after its workspace is gone. */
|
|
59
|
+
readonly runs: {
|
|
60
|
+
get: (runId: string) => Promise<Run>;
|
|
61
|
+
};
|
|
62
|
+
/** Release resources held by the client (the Effect runtime scope: DB pool, daemon connections). */
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
65
|
+
}
|