@shardflux/sdk 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/LICENSE +202 -0
- package/README.md +162 -0
- package/dist/audit.d.ts +74 -0
- package/dist/audit.js +49 -0
- package/dist/cell.d.ts +195 -0
- package/dist/cell.js +408 -0
- package/dist/client.d.ts +212 -0
- package/dist/client.js +238 -0
- package/dist/egress.d.ts +152 -0
- package/dist/egress.js +37 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.js +73 -0
- package/dist/generated/app-api.d.ts +23479 -0
- package/dist/generated/app-api.js +5 -0
- package/dist/generated/cell-api.d.ts +1832 -0
- package/dist/generated/cell-api.js +5 -0
- package/dist/http.d.ts +39 -0
- package/dist/http.js +121 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +13 -0
- package/dist/secrets.d.ts +100 -0
- package/dist/secrets.js +56 -0
- package/dist/templates.d.ts +328 -0
- package/dist/templates.js +117 -0
- package/dist/tokens.d.ts +35 -0
- package/dist/tokens.js +61 -0
- package/dist/tools.d.ts +91 -0
- package/dist/tools.js +313 -0
- package/dist/usage.d.ts +60 -0
- package/dist/usage.js +45 -0
- package/dist/volumes.d.ts +108 -0
- package/dist/volumes.js +120 -0
- package/dist/workspace.d.ts +70 -0
- package/dist/workspace.js +113 -0
- package/package.json +56 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template registry and custom template builds over the application API (/v1).
|
|
3
|
+
* Types are written by hand.
|
|
4
|
+
*
|
|
5
|
+
* Publishing/archiving an organization template version is a browser-only
|
|
6
|
+
* owner/admin action (it changes what `open` resolves for every project), so
|
|
7
|
+
* it is not exposed here.
|
|
8
|
+
*/
|
|
9
|
+
import type { ClientContext, Page } from './client.js';
|
|
10
|
+
export type TemplateOwner = 'platform' | 'organization';
|
|
11
|
+
export type TemplateVersionState = 'unpublished' | 'published' | 'archived';
|
|
12
|
+
export type CeilingSource = 'template' | 'user' | 'plan';
|
|
13
|
+
export interface TemplateVersion {
|
|
14
|
+
id: string;
|
|
15
|
+
version: number;
|
|
16
|
+
state: TemplateVersionState;
|
|
17
|
+
architecture: string;
|
|
18
|
+
/** SHA-256 of the version manifest: its immutable identity. */
|
|
19
|
+
artifact_sha256: string;
|
|
20
|
+
description: string | null;
|
|
21
|
+
created_at: string;
|
|
22
|
+
published_at: string | null;
|
|
23
|
+
archived_at: string | null;
|
|
24
|
+
/** The version `open` picks for a new workspace of this template. */
|
|
25
|
+
is_open_version: boolean;
|
|
26
|
+
caps: {
|
|
27
|
+
cpu_millis_max: number | null;
|
|
28
|
+
memory_mib_max: number | null;
|
|
29
|
+
disk_gib_max: number | null;
|
|
30
|
+
};
|
|
31
|
+
default_caps: {
|
|
32
|
+
cpu_millis_ceiling: number | null;
|
|
33
|
+
memory_mib_base: number | null;
|
|
34
|
+
memory_mib_ceiling: number | null;
|
|
35
|
+
disk_gib: number | null;
|
|
36
|
+
} | null;
|
|
37
|
+
effective_ceilings: {
|
|
38
|
+
cpu_millis: number;
|
|
39
|
+
memory_mib: number;
|
|
40
|
+
disk_gib: number;
|
|
41
|
+
sources: {
|
|
42
|
+
cpu_millis: CeilingSource;
|
|
43
|
+
memory_mib: CeilingSource;
|
|
44
|
+
disk_gib: CeilingSource;
|
|
45
|
+
};
|
|
46
|
+
} | null;
|
|
47
|
+
/** Resources where your plan is below what the template states, with the plan value. */
|
|
48
|
+
plan_clamped: {
|
|
49
|
+
cpu_millis?: number;
|
|
50
|
+
memory_mib?: number;
|
|
51
|
+
disk_gib?: number;
|
|
52
|
+
};
|
|
53
|
+
compatibility: {
|
|
54
|
+
manifest_schema: string | null;
|
|
55
|
+
architecture: string;
|
|
56
|
+
kernel: {
|
|
57
|
+
release: string | null;
|
|
58
|
+
sha256: string | null;
|
|
59
|
+
} | null;
|
|
60
|
+
guest_agent: {
|
|
61
|
+
version: string | null;
|
|
62
|
+
sha256: string | null;
|
|
63
|
+
vsock_port: number | null;
|
|
64
|
+
} | null;
|
|
65
|
+
rootfs: {
|
|
66
|
+
format: string | null;
|
|
67
|
+
bytes: number | null;
|
|
68
|
+
sha256: string | null;
|
|
69
|
+
} | null;
|
|
70
|
+
firecracker: string | null;
|
|
71
|
+
runtime_class: string | null;
|
|
72
|
+
};
|
|
73
|
+
installed_tools: Array<{
|
|
74
|
+
name: string;
|
|
75
|
+
version: string;
|
|
76
|
+
}>;
|
|
77
|
+
build_id: string | null;
|
|
78
|
+
publishable: boolean;
|
|
79
|
+
}
|
|
80
|
+
export interface TemplateSummary {
|
|
81
|
+
id: string;
|
|
82
|
+
slug: string;
|
|
83
|
+
name: string;
|
|
84
|
+
owner: TemplateOwner;
|
|
85
|
+
organization_id: string | null;
|
|
86
|
+
created_at: string;
|
|
87
|
+
archived_at: string | null;
|
|
88
|
+
shadowed_by_organization_template: boolean;
|
|
89
|
+
open_version: TemplateVersion | null;
|
|
90
|
+
}
|
|
91
|
+
export interface TemplateDetail extends TemplateSummary {
|
|
92
|
+
plan: {
|
|
93
|
+
plan_key: string;
|
|
94
|
+
catalog_version: string;
|
|
95
|
+
} | null;
|
|
96
|
+
/** What `open({ template: slug })` resolves to for the organization right now. */
|
|
97
|
+
open_resolves_to: {
|
|
98
|
+
template_id: string;
|
|
99
|
+
template_version_id: string;
|
|
100
|
+
version: number;
|
|
101
|
+
owner: TemplateOwner;
|
|
102
|
+
} | null;
|
|
103
|
+
versions: TemplateVersion[];
|
|
104
|
+
versions_truncated: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* queued -> building -> testing -> publishing -> published (the cell stored the artifact; the API then registers the
|
|
108
|
+
* template version: see `registration`) | failed | canceled. `succeeded` = legacy (pre-0.5) builds.
|
|
109
|
+
*/
|
|
110
|
+
export type TemplateBuildState = 'queued' | 'building' | 'testing' | 'publishing' | 'published' | 'succeeded' | 'failed' | 'canceled';
|
|
111
|
+
export type TemplateBuildRegistrationState = 'not_applicable' | 'pending' | 'registered' | 'failed';
|
|
112
|
+
export interface BuilderAvailability {
|
|
113
|
+
state: 'available' | 'unavailable';
|
|
114
|
+
reason: 'no_builder_registered' | 'no_recent_heartbeat' | null;
|
|
115
|
+
active_builders: number;
|
|
116
|
+
last_heartbeat_at: string | null;
|
|
117
|
+
}
|
|
118
|
+
export interface TemplateBuild {
|
|
119
|
+
id: string;
|
|
120
|
+
organization_id: string;
|
|
121
|
+
template: {
|
|
122
|
+
id: string;
|
|
123
|
+
slug: string;
|
|
124
|
+
name: string;
|
|
125
|
+
};
|
|
126
|
+
state: TemplateBuildState;
|
|
127
|
+
/** Version number this build produces in its organization template (assigned at request time; null for legacy builds). */
|
|
128
|
+
target_version: number | null;
|
|
129
|
+
/** The registered version is published at once (default) or left for the owner/admin publish route. */
|
|
130
|
+
auto_publish: boolean;
|
|
131
|
+
published_at: string | null;
|
|
132
|
+
/** Creation of the immutable template version from a published build (by the API worker, within seconds). */
|
|
133
|
+
registration: {
|
|
134
|
+
state: TemplateBuildRegistrationState;
|
|
135
|
+
registered_at: string | null;
|
|
136
|
+
error: {
|
|
137
|
+
code: string;
|
|
138
|
+
message: string;
|
|
139
|
+
} | null;
|
|
140
|
+
};
|
|
141
|
+
/** The template version this build produced, once registered. */
|
|
142
|
+
template_version: {
|
|
143
|
+
id: string;
|
|
144
|
+
version: number;
|
|
145
|
+
published: boolean;
|
|
146
|
+
} | null;
|
|
147
|
+
cancel_requested_at: string | null;
|
|
148
|
+
created_at: string;
|
|
149
|
+
updated_at: string;
|
|
150
|
+
started_at: string | null;
|
|
151
|
+
completed_at: string | null;
|
|
152
|
+
requested_by: {
|
|
153
|
+
type: 'user' | 'api_key';
|
|
154
|
+
id: string;
|
|
155
|
+
};
|
|
156
|
+
base: {
|
|
157
|
+
ref: string;
|
|
158
|
+
template_id: string;
|
|
159
|
+
template_version_id: string;
|
|
160
|
+
slug: string;
|
|
161
|
+
version: number;
|
|
162
|
+
artifact_sha256: string;
|
|
163
|
+
};
|
|
164
|
+
architecture: string;
|
|
165
|
+
bounds: {
|
|
166
|
+
cpu_millis: number;
|
|
167
|
+
memory_mib: number;
|
|
168
|
+
disk_gib: number;
|
|
169
|
+
timeout_seconds: number;
|
|
170
|
+
};
|
|
171
|
+
requested_bounds: {
|
|
172
|
+
cpu_millis: number | null;
|
|
173
|
+
memory_mib: number | null;
|
|
174
|
+
disk_gib: number | null;
|
|
175
|
+
timeout_seconds: number | null;
|
|
176
|
+
};
|
|
177
|
+
bound_sources: Record<'cpu_millis' | 'memory_mib' | 'disk_gib' | 'timeout_seconds', 'request' | 'plan' | 'platform'>;
|
|
178
|
+
network: {
|
|
179
|
+
mode: 'none' | 'egress_allowlist';
|
|
180
|
+
allow_hosts: string[];
|
|
181
|
+
};
|
|
182
|
+
provenance: {
|
|
183
|
+
recipe_schema: string;
|
|
184
|
+
recipe_sha256: string;
|
|
185
|
+
base_artifact_sha256: string;
|
|
186
|
+
builder_id: string | null;
|
|
187
|
+
attempt: number;
|
|
188
|
+
};
|
|
189
|
+
/** Present on create and get of one build. */
|
|
190
|
+
recipe?: {
|
|
191
|
+
dockerfile: string;
|
|
192
|
+
};
|
|
193
|
+
result: {
|
|
194
|
+
artifact_sha256: string | null;
|
|
195
|
+
scan: ({
|
|
196
|
+
passed: boolean;
|
|
197
|
+
} & Record<string, unknown>) | null;
|
|
198
|
+
compatibility: ({
|
|
199
|
+
passed: boolean;
|
|
200
|
+
} & Record<string, unknown>) | null;
|
|
201
|
+
produced_version: {
|
|
202
|
+
id: string;
|
|
203
|
+
version: number;
|
|
204
|
+
state: TemplateVersionState;
|
|
205
|
+
published_at: string | null;
|
|
206
|
+
archived_at: string | null;
|
|
207
|
+
} | null;
|
|
208
|
+
};
|
|
209
|
+
failure: {
|
|
210
|
+
code: string;
|
|
211
|
+
message: string;
|
|
212
|
+
} | null;
|
|
213
|
+
log: {
|
|
214
|
+
state: 'none' | 'available' | 'expired';
|
|
215
|
+
bytes: number | null;
|
|
216
|
+
sha256: string | null;
|
|
217
|
+
expires_at: string | null;
|
|
218
|
+
downloadable: boolean;
|
|
219
|
+
tail?: string | null;
|
|
220
|
+
};
|
|
221
|
+
publishable: boolean;
|
|
222
|
+
builder_availability: BuilderAvailability;
|
|
223
|
+
}
|
|
224
|
+
export interface TemplateRecipe {
|
|
225
|
+
/** `<template slug>@<version>`, referenced in the Dockerfile as `FROM shardflux-base`. */
|
|
226
|
+
base: string;
|
|
227
|
+
/** Dockerfile (<= 64 KiB, no build context; external images pinned by @sha256 digest; final stage FROM shardflux-base). */
|
|
228
|
+
dockerfile: string;
|
|
229
|
+
architecture?: 'x86_64';
|
|
230
|
+
resources?: {
|
|
231
|
+
cpu_millis?: number;
|
|
232
|
+
memory_mib?: number;
|
|
233
|
+
disk_gib?: number;
|
|
234
|
+
timeout_seconds?: number;
|
|
235
|
+
};
|
|
236
|
+
network?: {
|
|
237
|
+
mode: 'none' | 'egress_allowlist';
|
|
238
|
+
allow_hosts?: string[];
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export interface CreateTemplateBuildParams {
|
|
242
|
+
/** Organization template slug (created by the first build; platform slugs are refused). */
|
|
243
|
+
templateSlug: string;
|
|
244
|
+
displayName?: string;
|
|
245
|
+
recipe: TemplateRecipe;
|
|
246
|
+
/** Publish the produced version as soon as it is registered (default true); false leaves it for the owner/admin publish route. */
|
|
247
|
+
autoPublish?: boolean;
|
|
248
|
+
/** Defaults to a random key so retries never create a second build. */
|
|
249
|
+
idempotencyKey?: string;
|
|
250
|
+
}
|
|
251
|
+
/** Short-lived download of the full build log (a bearer URL until expires_at: do not log or share it). */
|
|
252
|
+
export interface TemplateBuildLogUrl {
|
|
253
|
+
url: string;
|
|
254
|
+
expires_at: string;
|
|
255
|
+
object_key: string;
|
|
256
|
+
bytes: number | null;
|
|
257
|
+
sha256: string | null;
|
|
258
|
+
}
|
|
259
|
+
export interface WaitForBuildOptions {
|
|
260
|
+
timeoutMs?: number;
|
|
261
|
+
pollIntervalMs?: number;
|
|
262
|
+
maxPollIntervalMs?: number;
|
|
263
|
+
signal?: AbortSignal;
|
|
264
|
+
}
|
|
265
|
+
/** Finished from the caller's point of view: failed/canceled/legacy succeeded, or published and its registration settled. */
|
|
266
|
+
export declare function buildSettled(build: Pick<TemplateBuild, 'state' | 'registration'>): boolean;
|
|
267
|
+
/** Thrown by waitForBuild when the build is still running (or awaiting registration) at the deadline (it keeps going). */
|
|
268
|
+
export declare class TemplateBuildTimeoutError extends Error {
|
|
269
|
+
readonly build: TemplateBuild;
|
|
270
|
+
constructor(build: TemplateBuild, waitedMs: number);
|
|
271
|
+
}
|
|
272
|
+
export declare class TemplateBuildsApi {
|
|
273
|
+
#private;
|
|
274
|
+
constructor(ctx: () => ClientContext);
|
|
275
|
+
/** Queues a build (202). Poll with get()/waitForBuild(); `builder_availability` says whether a builder runs. */
|
|
276
|
+
create(organizationId: string, params: CreateTemplateBuildParams): Promise<TemplateBuild>;
|
|
277
|
+
get(organizationId: string, buildId: string): Promise<TemplateBuild>;
|
|
278
|
+
list(organizationId: string, params?: {
|
|
279
|
+
state?: TemplateBuildState;
|
|
280
|
+
template?: string;
|
|
281
|
+
limit?: number;
|
|
282
|
+
cursor?: string;
|
|
283
|
+
}): Promise<Page<TemplateBuild>>;
|
|
284
|
+
/**
|
|
285
|
+
* A presigned URL (<= 15 min) for the full build log `builds/<id>.log`. 404 with details.reason log_not_available
|
|
286
|
+
* (not recorded yet) or log_expired; 503 when the deployment has no build-log bucket.
|
|
287
|
+
*/
|
|
288
|
+
logUrl(organizationId: string, buildId: string): Promise<TemplateBuildLogUrl>;
|
|
289
|
+
/** queued -> canceled; building/testing/publishing -> cancellation requested (honoured until publishing starts). API keys cancel only their own builds. */
|
|
290
|
+
cancel(organizationId: string, buildId: string): Promise<TemplateBuild>;
|
|
291
|
+
builderAvailability(organizationId: string): Promise<BuilderAvailability>;
|
|
292
|
+
/**
|
|
293
|
+
* Polls until the build is settled and returns it: failed | canceled, or published with its registration
|
|
294
|
+
* `registered` (the version exists; `template_version`) or `failed`. Legacy builds end `succeeded`.
|
|
295
|
+
* Throws TemplateBuildTimeoutError at the deadline (default 30 min); the build continues.
|
|
296
|
+
*/
|
|
297
|
+
waitForBuild(organizationId: string, buildId: string, opts?: WaitForBuildOptions): Promise<TemplateBuild>;
|
|
298
|
+
}
|
|
299
|
+
export declare class TemplatesApi {
|
|
300
|
+
#private;
|
|
301
|
+
readonly builds: TemplateBuildsApi;
|
|
302
|
+
constructor(ctx: () => ClientContext);
|
|
303
|
+
/** Templates the API key's organization can use (platform + its own), with the version `open` picks. */
|
|
304
|
+
list(params?: {
|
|
305
|
+
organizationId?: string;
|
|
306
|
+
includeArchived?: boolean;
|
|
307
|
+
owner?: TemplateOwner;
|
|
308
|
+
limit?: number;
|
|
309
|
+
cursor?: string;
|
|
310
|
+
}): Promise<Page<TemplateSummary>>;
|
|
311
|
+
listAll(params?: {
|
|
312
|
+
organizationId?: string;
|
|
313
|
+
includeArchived?: boolean;
|
|
314
|
+
owner?: TemplateOwner;
|
|
315
|
+
}): AsyncGenerator<TemplateSummary>;
|
|
316
|
+
/** One template by slug: versions, compatibility, caps, installed tools, plan clamping and what `open` resolves to. */
|
|
317
|
+
get(slug: string, params?: {
|
|
318
|
+
organizationId?: string;
|
|
319
|
+
includeArchived?: boolean;
|
|
320
|
+
owner?: TemplateOwner;
|
|
321
|
+
}): Promise<TemplateDetail>;
|
|
322
|
+
/** Like get() but resolves to null on 404 (unknown slug or outside the organization). */
|
|
323
|
+
find(slug: string, params?: {
|
|
324
|
+
organizationId?: string;
|
|
325
|
+
includeArchived?: boolean;
|
|
326
|
+
owner?: TemplateOwner;
|
|
327
|
+
}): Promise<TemplateDetail | null>;
|
|
328
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ShardfluxApiError } from "./errors.js";
|
|
2
|
+
import { randomId } from "./http.js";
|
|
3
|
+
const TERMINAL = new Set(['succeeded', 'failed', 'canceled']);
|
|
4
|
+
/** Finished from the caller's point of view: failed/canceled/legacy succeeded, or published and its registration settled. */
|
|
5
|
+
export function buildSettled(build) {
|
|
6
|
+
if (TERMINAL.has(build.state))
|
|
7
|
+
return true;
|
|
8
|
+
return build.state === 'published' && (build.registration.state === 'registered' || build.registration.state === 'failed');
|
|
9
|
+
}
|
|
10
|
+
/** Thrown by waitForBuild when the build is still running (or awaiting registration) at the deadline (it keeps going). */
|
|
11
|
+
export class TemplateBuildTimeoutError extends Error {
|
|
12
|
+
build;
|
|
13
|
+
constructor(build, waitedMs) {
|
|
14
|
+
super(`Template build ${build.id} is still ${build.state} after ${waitedMs} ms${build.builder_availability.state === 'unavailable' ? ` (builder ${build.builder_availability.reason ?? 'unavailable'})` : ''}`);
|
|
15
|
+
this.name = 'TemplateBuildTimeoutError';
|
|
16
|
+
this.build = build;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const enc = encodeURIComponent;
|
|
20
|
+
export class TemplateBuildsApi {
|
|
21
|
+
#ctx;
|
|
22
|
+
constructor(ctx) {
|
|
23
|
+
this.#ctx = ctx;
|
|
24
|
+
}
|
|
25
|
+
/** Queues a build (202). Poll with get()/waitForBuild(); `builder_availability` says whether a builder runs. */
|
|
26
|
+
create(organizationId, params) {
|
|
27
|
+
const body = { template_slug: params.templateSlug, recipe: params.recipe };
|
|
28
|
+
if (params.displayName !== undefined)
|
|
29
|
+
body.display_name = params.displayName;
|
|
30
|
+
if (params.autoPublish !== undefined)
|
|
31
|
+
body.auto_publish = params.autoPublish;
|
|
32
|
+
return this.#ctx().http.json('POST', `/v1/organizations/${enc(organizationId)}/template-builds`, { json: body, idempotencyKey: params.idempotencyKey ?? randomId('template-build-') }, this.#ctx().authorization);
|
|
33
|
+
}
|
|
34
|
+
get(organizationId, buildId) {
|
|
35
|
+
return this.#ctx().http.json('GET', `/v1/organizations/${enc(organizationId)}/template-builds/${enc(buildId)}`, {}, this.#ctx().authorization);
|
|
36
|
+
}
|
|
37
|
+
async list(organizationId, params = {}) {
|
|
38
|
+
const page = await this.#ctx().http.json('GET', `/v1/organizations/${enc(organizationId)}/template-builds`, { query: { state: params.state, template: params.template, limit: params.limit, cursor: params.cursor } }, this.#ctx().authorization);
|
|
39
|
+
return { data: page.data, nextCursor: page.next_cursor };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A presigned URL (<= 15 min) for the full build log `builds/<id>.log`. 404 with details.reason log_not_available
|
|
43
|
+
* (not recorded yet) or log_expired; 503 when the deployment has no build-log bucket.
|
|
44
|
+
*/
|
|
45
|
+
logUrl(organizationId, buildId) {
|
|
46
|
+
return this.#ctx().http.json('GET', `/v1/organizations/${enc(organizationId)}/template-builds/${enc(buildId)}/log-url`, {}, this.#ctx().authorization);
|
|
47
|
+
}
|
|
48
|
+
/** queued -> canceled; building/testing/publishing -> cancellation requested (honoured until publishing starts). API keys cancel only their own builds. */
|
|
49
|
+
cancel(organizationId, buildId) {
|
|
50
|
+
return this.#ctx().http.json('POST', `/v1/organizations/${enc(organizationId)}/template-builds/${enc(buildId)}/cancel`, {}, this.#ctx().authorization);
|
|
51
|
+
}
|
|
52
|
+
builderAvailability(organizationId) {
|
|
53
|
+
return this.#ctx().http.json('GET', `/v1/organizations/${enc(organizationId)}/template-builder-availability`, {}, this.#ctx().authorization);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Polls until the build is settled and returns it: failed | canceled, or published with its registration
|
|
57
|
+
* `registered` (the version exists; `template_version`) or `failed`. Legacy builds end `succeeded`.
|
|
58
|
+
* Throws TemplateBuildTimeoutError at the deadline (default 30 min); the build continues.
|
|
59
|
+
*/
|
|
60
|
+
async waitForBuild(organizationId, buildId, opts = {}) {
|
|
61
|
+
const { sleep } = this.#ctx();
|
|
62
|
+
const timeoutMs = opts.timeoutMs ?? 1_800_000;
|
|
63
|
+
const maxInterval = opts.maxPollIntervalMs ?? 10_000;
|
|
64
|
+
let interval = opts.pollIntervalMs ?? 1_000;
|
|
65
|
+
const started = Date.now();
|
|
66
|
+
for (;;) {
|
|
67
|
+
const build = await this.get(organizationId, buildId);
|
|
68
|
+
if (buildSettled(build))
|
|
69
|
+
return build;
|
|
70
|
+
const waited = Date.now() - started;
|
|
71
|
+
if (waited >= timeoutMs)
|
|
72
|
+
throw new TemplateBuildTimeoutError(build, waited);
|
|
73
|
+
await sleep(Math.max(10, Math.min(interval, timeoutMs - waited)));
|
|
74
|
+
if (opts.signal?.aborted)
|
|
75
|
+
throw opts.signal.reason instanceof Error ? opts.signal.reason : new Error('aborted');
|
|
76
|
+
interval = Math.min(maxInterval, interval * 2);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class TemplatesApi {
|
|
81
|
+
#ctx;
|
|
82
|
+
builds;
|
|
83
|
+
constructor(ctx) {
|
|
84
|
+
this.#ctx = ctx;
|
|
85
|
+
this.builds = new TemplateBuildsApi(ctx);
|
|
86
|
+
}
|
|
87
|
+
/** Templates the API key's organization can use (platform + its own), with the version `open` picks. */
|
|
88
|
+
async list(params = {}) {
|
|
89
|
+
const path = params.organizationId === undefined ? '/v1/templates' : `/v1/organizations/${enc(params.organizationId)}/templates`;
|
|
90
|
+
const page = await this.#ctx().http.json('GET', path, { query: { include_archived: params.includeArchived, owner: params.owner, limit: params.limit, cursor: params.cursor } }, this.#ctx().authorization);
|
|
91
|
+
return { data: page.data, nextCursor: page.next_cursor };
|
|
92
|
+
}
|
|
93
|
+
async *listAll(params = {}) {
|
|
94
|
+
let cursor;
|
|
95
|
+
do {
|
|
96
|
+
const page = await this.list({ ...params, limit: 200, ...(cursor === undefined ? {} : { cursor }) });
|
|
97
|
+
yield* page.data;
|
|
98
|
+
cursor = page.nextCursor ?? undefined;
|
|
99
|
+
} while (cursor !== undefined);
|
|
100
|
+
}
|
|
101
|
+
/** One template by slug: versions, compatibility, caps, installed tools, plan clamping and what `open` resolves to. */
|
|
102
|
+
get(slug, params = {}) {
|
|
103
|
+
const path = params.organizationId === undefined ? `/v1/templates/${enc(slug)}` : `/v1/organizations/${enc(params.organizationId)}/templates/${enc(slug)}`;
|
|
104
|
+
return this.#ctx().http.json('GET', path, { query: { include_archived: params.includeArchived, owner: params.owner } }, this.#ctx().authorization);
|
|
105
|
+
}
|
|
106
|
+
/** Like get() but resolves to null on 404 (unknown slug or outside the organization). */
|
|
107
|
+
async find(slug, params = {}) {
|
|
108
|
+
try {
|
|
109
|
+
return await this.get(slug, params);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (err instanceof ShardfluxApiError && err.status === 404)
|
|
113
|
+
return null;
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace tool-token lifecycle: one manager per (workspace, agent label,
|
|
3
|
+
* tools). Tokens are fetched lazily from POST /v1/workspaces/{id}/tool-tokens,
|
|
4
|
+
* reused until `refreshSkewMs` before expiry, refreshed with single-flight
|
|
5
|
+
* (concurrent callers share one request), and invalidated on the cell's
|
|
6
|
+
* `409 stale_epoch` or `401` so the next call gets a fresh token (and, after a
|
|
7
|
+
* move, a possibly different cell endpoint).
|
|
8
|
+
*/
|
|
9
|
+
import type { components } from './generated/app-api.js';
|
|
10
|
+
import type { HttpClient } from './http.js';
|
|
11
|
+
export type ToolToken = components['schemas']['ToolToken'];
|
|
12
|
+
export type ToolName = ToolToken['tools'][number];
|
|
13
|
+
export interface ToolTokenOptions {
|
|
14
|
+
agentLabel?: string | undefined;
|
|
15
|
+
tools?: readonly ToolName[] | undefined;
|
|
16
|
+
/** Refresh this long before expiry (default 60 s). */
|
|
17
|
+
refreshSkewMs?: number;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
}
|
|
20
|
+
export declare class ToolTokenManager {
|
|
21
|
+
#private;
|
|
22
|
+
readonly workspaceId: string;
|
|
23
|
+
readonly agentLabel: string | undefined;
|
|
24
|
+
readonly tools: readonly ToolName[] | undefined;
|
|
25
|
+
/** Number of tokens fetched from the API (observability / tests). */
|
|
26
|
+
fetched: number;
|
|
27
|
+
constructor(http: HttpClient, authorization: string, workspaceId: string, opts?: ToolTokenOptions);
|
|
28
|
+
/** Accepts a token obtained elsewhere (e.g. the one returned by a 200 open). */
|
|
29
|
+
seed(token: ToolToken): void;
|
|
30
|
+
get current(): ToolToken | undefined;
|
|
31
|
+
get(): Promise<ToolToken>;
|
|
32
|
+
/** Forces a new token (single-flight). */
|
|
33
|
+
refresh(): Promise<ToolToken>;
|
|
34
|
+
invalidate(): void;
|
|
35
|
+
}
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export class ToolTokenManager {
|
|
2
|
+
workspaceId;
|
|
3
|
+
agentLabel;
|
|
4
|
+
tools;
|
|
5
|
+
#http;
|
|
6
|
+
#authorization;
|
|
7
|
+
#skew;
|
|
8
|
+
#now;
|
|
9
|
+
#current;
|
|
10
|
+
#inflight;
|
|
11
|
+
/** Number of tokens fetched from the API (observability / tests). */
|
|
12
|
+
fetched = 0;
|
|
13
|
+
constructor(http, authorization, workspaceId, opts = {}) {
|
|
14
|
+
this.#http = http;
|
|
15
|
+
this.#authorization = authorization;
|
|
16
|
+
this.workspaceId = workspaceId;
|
|
17
|
+
this.agentLabel = opts.agentLabel;
|
|
18
|
+
this.tools = opts.tools;
|
|
19
|
+
this.#skew = opts.refreshSkewMs ?? 60_000;
|
|
20
|
+
this.#now = opts.now ?? Date.now;
|
|
21
|
+
}
|
|
22
|
+
/** Accepts a token obtained elsewhere (e.g. the one returned by a 200 open). */
|
|
23
|
+
seed(token) {
|
|
24
|
+
if (token.workspace_id === this.workspaceId)
|
|
25
|
+
this.#current = token;
|
|
26
|
+
}
|
|
27
|
+
get current() {
|
|
28
|
+
return this.#current;
|
|
29
|
+
}
|
|
30
|
+
#fresh(t) {
|
|
31
|
+
return t !== undefined && Date.parse(t.expires_at) - this.#now() > this.#skew;
|
|
32
|
+
}
|
|
33
|
+
async get() {
|
|
34
|
+
if (this.#fresh(this.#current))
|
|
35
|
+
return this.#current;
|
|
36
|
+
return this.refresh();
|
|
37
|
+
}
|
|
38
|
+
/** Forces a new token (single-flight). */
|
|
39
|
+
refresh() {
|
|
40
|
+
this.#inflight ??= (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const body = {};
|
|
43
|
+
if (this.agentLabel !== undefined)
|
|
44
|
+
body.agent_label = this.agentLabel;
|
|
45
|
+
if (this.tools !== undefined)
|
|
46
|
+
body.tools = [...this.tools];
|
|
47
|
+
const token = await this.#http.json('POST', `/v1/workspaces/${encodeURIComponent(this.workspaceId)}/tool-tokens`, { json: body }, this.#authorization);
|
|
48
|
+
this.fetched += 1;
|
|
49
|
+
this.#current = token;
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
this.#inflight = undefined;
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
return this.#inflight;
|
|
57
|
+
}
|
|
58
|
+
invalidate() {
|
|
59
|
+
this.#current = undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ToolName } from './tokens.js';
|
|
2
|
+
import type { Workspace } from './workspace.js';
|
|
3
|
+
export interface JsonSchema {
|
|
4
|
+
type?: 'object' | 'string' | 'integer' | 'number' | 'boolean' | 'array';
|
|
5
|
+
description?: string;
|
|
6
|
+
properties?: Record<string, JsonSchema>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
additionalProperties?: boolean;
|
|
9
|
+
items?: JsonSchema;
|
|
10
|
+
enum?: readonly (string | number)[];
|
|
11
|
+
minimum?: number;
|
|
12
|
+
maximum?: number;
|
|
13
|
+
minLength?: number;
|
|
14
|
+
maxLength?: number;
|
|
15
|
+
minItems?: number;
|
|
16
|
+
maxItems?: number;
|
|
17
|
+
default?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface WorkspaceTool<A extends Record<string, unknown> = Record<string, unknown>, R = unknown> {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
parameters: JsonSchema & {
|
|
23
|
+
type: 'object';
|
|
24
|
+
};
|
|
25
|
+
/** Which workspace tool permission the call needs (exec, files, pty, process, git, browser). */
|
|
26
|
+
permission: ToolName;
|
|
27
|
+
execute(args: A, options?: {
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
}): Promise<R>;
|
|
30
|
+
}
|
|
31
|
+
export declare class ToolArgumentError extends Error {
|
|
32
|
+
readonly tool: string;
|
|
33
|
+
readonly issues: string[];
|
|
34
|
+
constructor(tool: string, issues: string[]);
|
|
35
|
+
}
|
|
36
|
+
/** Validates the JSON-Schema subset used by the tool definitions. Returns problems (empty = valid). */
|
|
37
|
+
export declare function validateArgs(schema: JsonSchema, value: unknown, path?: string): string[];
|
|
38
|
+
export interface WorkspaceToolsOptions {
|
|
39
|
+
/** Tool permissions to expose (default: the tools of the workspace's last token, else all). */
|
|
40
|
+
tools?: ToolName[];
|
|
41
|
+
/** Attribution label for the tokens these tools use (one agent session per label). */
|
|
42
|
+
agentLabel?: string;
|
|
43
|
+
/** Prefix for tool names, e.g. "workspace_" (default none). */
|
|
44
|
+
prefix?: string;
|
|
45
|
+
/** Bytes of command output / file content returned to the model (default 64 KiB). */
|
|
46
|
+
maxOutputBytes?: number;
|
|
47
|
+
/** Working directory for exec when the model gives none (default the guest user's home). */
|
|
48
|
+
defaultCwd?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Builds the tool list for a workspace. Synchronous: tokens are fetched on first use. */
|
|
51
|
+
export declare function workspaceTools(workspace: Workspace, opts?: WorkspaceToolsOptions): WorkspaceTool[];
|
|
52
|
+
/** OpenAI tool definitions: Chat Completions (`{type:'function', function:{...}}`) or Responses API. */
|
|
53
|
+
export declare function toOpenAITools(tools: readonly WorkspaceTool[], opts?: {
|
|
54
|
+
api?: 'chat' | 'responses';
|
|
55
|
+
}): {
|
|
56
|
+
type: "function";
|
|
57
|
+
name: string;
|
|
58
|
+
description: string;
|
|
59
|
+
parameters: JsonSchema & {
|
|
60
|
+
type: "object";
|
|
61
|
+
};
|
|
62
|
+
strict: boolean;
|
|
63
|
+
}[] | {
|
|
64
|
+
type: "function";
|
|
65
|
+
function: {
|
|
66
|
+
name: string;
|
|
67
|
+
description: string;
|
|
68
|
+
parameters: JsonSchema & {
|
|
69
|
+
type: "object";
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}[];
|
|
73
|
+
/** Anthropic Messages API tool definitions (`{ name, description, input_schema }`). */
|
|
74
|
+
export declare function toAnthropicTools(tools: readonly WorkspaceTool[]): {
|
|
75
|
+
name: string;
|
|
76
|
+
description: string;
|
|
77
|
+
input_schema: JsonSchema & {
|
|
78
|
+
type: "object";
|
|
79
|
+
};
|
|
80
|
+
}[];
|
|
81
|
+
/**
|
|
82
|
+
* Runs a model's tool call: `arguments` may be the JSON string (OpenAI) or an object (Anthropic
|
|
83
|
+
* `input`). Throws for unknown tools and invalid arguments (ToolArgumentError).
|
|
84
|
+
*/
|
|
85
|
+
export declare function executeToolCall(tools: readonly WorkspaceTool[], call: {
|
|
86
|
+
name: string;
|
|
87
|
+
arguments?: string | Record<string, unknown>;
|
|
88
|
+
input?: Record<string, unknown>;
|
|
89
|
+
}, options?: {
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
}): Promise<unknown>;
|