@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
package/dist/tools.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
export class ToolArgumentError extends Error {
|
|
2
|
+
tool;
|
|
3
|
+
issues;
|
|
4
|
+
constructor(tool, issues) {
|
|
5
|
+
super(`Invalid arguments for ${tool}: ${issues.join('; ')}`);
|
|
6
|
+
this.name = 'ToolArgumentError';
|
|
7
|
+
this.tool = tool;
|
|
8
|
+
this.issues = issues;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Validates the JSON-Schema subset used by the tool definitions. Returns problems (empty = valid). */
|
|
12
|
+
export function validateArgs(schema, value, path = '$') {
|
|
13
|
+
const issues = [];
|
|
14
|
+
const t = schema.type;
|
|
15
|
+
if (t === 'object') {
|
|
16
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
17
|
+
return [`${path} must be an object`];
|
|
18
|
+
const obj = value;
|
|
19
|
+
for (const r of schema.required ?? [])
|
|
20
|
+
if (obj[r] === undefined)
|
|
21
|
+
issues.push(`${path}.${r} is required`);
|
|
22
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
23
|
+
const sub = schema.properties?.[k];
|
|
24
|
+
if (!sub) {
|
|
25
|
+
if (schema.additionalProperties === false)
|
|
26
|
+
issues.push(`${path}.${k} is not allowed`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (v !== undefined)
|
|
30
|
+
issues.push(...validateArgs(sub, v, `${path}.${k}`));
|
|
31
|
+
}
|
|
32
|
+
return issues;
|
|
33
|
+
}
|
|
34
|
+
if (t === 'string') {
|
|
35
|
+
if (typeof value !== 'string')
|
|
36
|
+
return [`${path} must be a string`];
|
|
37
|
+
if (schema.minLength !== undefined && value.length < schema.minLength)
|
|
38
|
+
issues.push(`${path} is too short`);
|
|
39
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength)
|
|
40
|
+
issues.push(`${path} is too long`);
|
|
41
|
+
}
|
|
42
|
+
else if (t === 'integer' || t === 'number') {
|
|
43
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || (t === 'integer' && !Number.isInteger(value)))
|
|
44
|
+
return [`${path} must be ${t === 'integer' ? 'an integer' : 'a number'}`];
|
|
45
|
+
if (schema.minimum !== undefined && value < schema.minimum)
|
|
46
|
+
issues.push(`${path} must be >= ${schema.minimum}`);
|
|
47
|
+
if (schema.maximum !== undefined && value > schema.maximum)
|
|
48
|
+
issues.push(`${path} must be <= ${schema.maximum}`);
|
|
49
|
+
}
|
|
50
|
+
else if (t === 'boolean') {
|
|
51
|
+
if (typeof value !== 'boolean')
|
|
52
|
+
return [`${path} must be a boolean`];
|
|
53
|
+
}
|
|
54
|
+
else if (t === 'array') {
|
|
55
|
+
if (!Array.isArray(value))
|
|
56
|
+
return [`${path} must be an array`];
|
|
57
|
+
if (schema.minItems !== undefined && value.length < schema.minItems)
|
|
58
|
+
issues.push(`${path} needs at least ${schema.minItems} items`);
|
|
59
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems)
|
|
60
|
+
issues.push(`${path} allows at most ${schema.maxItems} items`);
|
|
61
|
+
if (schema.items)
|
|
62
|
+
value.forEach((v, i) => issues.push(...validateArgs(schema.items, v, `${path}[${i}]`)));
|
|
63
|
+
}
|
|
64
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
65
|
+
issues.push(`${path} must be one of ${schema.enum.join(', ')}`);
|
|
66
|
+
return issues;
|
|
67
|
+
}
|
|
68
|
+
const ALL = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
|
|
69
|
+
const obj = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
70
|
+
const path = (description = 'Absolute path inside the workspace, e.g. /home/user/project/main.py') => ({ type: 'string', minLength: 1, maxLength: 4096, description });
|
|
71
|
+
const signal = { type: 'string', description: 'Signal name such as SIGTERM, SIGINT or SIGKILL.', minLength: 2, maxLength: 12 };
|
|
72
|
+
function clip(text, max) {
|
|
73
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
74
|
+
if (bytes.length <= max)
|
|
75
|
+
return { text, truncated: false };
|
|
76
|
+
return { text: bytes.subarray(0, max).toString('utf8'), truncated: true };
|
|
77
|
+
}
|
|
78
|
+
/** Builds the tool list for a workspace. Synchronous: tokens are fetched on first use. */
|
|
79
|
+
export function workspaceTools(workspace, opts = {}) {
|
|
80
|
+
const cell = () => workspace.cell(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {});
|
|
81
|
+
const max = opts.maxOutputBytes ?? 65_536;
|
|
82
|
+
const prefix = opts.prefix ?? '';
|
|
83
|
+
const allowed = new Set(opts.tools ?? workspace.grantedTools ?? ALL);
|
|
84
|
+
const defs = [
|
|
85
|
+
{
|
|
86
|
+
name: 'exec',
|
|
87
|
+
permission: 'exec',
|
|
88
|
+
description: 'Run a shell command in the persistent remote workspace (Linux; bash -lc) and return its exit code, stdout and stderr. Files, installed packages and background processes persist between calls.',
|
|
89
|
+
parameters: obj({
|
|
90
|
+
command: { type: 'string', minLength: 1, maxLength: 100_000, description: 'Shell command line, e.g. "pip install -r requirements.txt && pytest -q".' },
|
|
91
|
+
cwd: { type: 'string', maxLength: 4096, description: 'Working directory (absolute).' },
|
|
92
|
+
timeout_ms: { type: 'integer', minimum: 1000, maximum: 3_600_000, description: 'Kill the command after this long (default 600000).' },
|
|
93
|
+
stdin: { type: 'string', maxLength: 1_000_000, description: 'Text written to stdin.' },
|
|
94
|
+
}, ['command']),
|
|
95
|
+
run: async (a, o) => {
|
|
96
|
+
const r = await cell().exec.run(['bash', '-lc', String(a.command)], {
|
|
97
|
+
...(typeof a.cwd === 'string' ? { cwd: a.cwd } : opts.defaultCwd ? { cwd: opts.defaultCwd } : {}),
|
|
98
|
+
timeoutMs: typeof a.timeout_ms === 'number' ? a.timeout_ms : 600_000,
|
|
99
|
+
...(typeof a.stdin === 'string' ? { stdin: a.stdin } : {}),
|
|
100
|
+
maxOutputBytes: max,
|
|
101
|
+
...(o.signal ? { signal: o.signal } : {}),
|
|
102
|
+
});
|
|
103
|
+
return { exit_code: r.exitCode, term_signal: r.termSignal, timed_out: r.timedOut, stdout: r.stdout, stderr: r.stderr, truncated: r.truncated, session_id: r.sessionId };
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'read_file',
|
|
108
|
+
permission: 'files',
|
|
109
|
+
description: 'Read a text file from the workspace (UTF-8). Use offset/length for large files.',
|
|
110
|
+
parameters: obj({ path: path(), offset: { type: 'integer', minimum: 0 }, length: { type: 'integer', minimum: 1, maximum: 10_485_760 } }, ['path']),
|
|
111
|
+
run: async (a) => {
|
|
112
|
+
const bytes = await cell().files.read(String(a.path), {
|
|
113
|
+
...(typeof a.offset === 'number' ? { offset: a.offset } : {}),
|
|
114
|
+
length: typeof a.length === 'number' ? Math.min(a.length, max) : max + 1,
|
|
115
|
+
});
|
|
116
|
+
const c = clip(new TextDecoder().decode(bytes), max);
|
|
117
|
+
return { path: a.path, content: c.text, truncated: c.truncated || bytes.length > max };
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'write_file',
|
|
122
|
+
permission: 'files',
|
|
123
|
+
description: 'Create or replace a file in the workspace (atomic, durable once acknowledged). Set append to add to the end instead.',
|
|
124
|
+
parameters: obj({
|
|
125
|
+
path: path(),
|
|
126
|
+
content: { type: 'string', maxLength: 10_485_760, description: 'Full file content (UTF-8).' },
|
|
127
|
+
append: { type: 'boolean', description: 'Append instead of replacing.' },
|
|
128
|
+
create_parents: { type: 'boolean', description: 'Create missing parent directories (default true).' },
|
|
129
|
+
}, ['path', 'content']),
|
|
130
|
+
run: async (a) => {
|
|
131
|
+
const r = await cell().files.write(String(a.path), String(a.content), { append: a.append === true, createParents: a.create_parents !== false });
|
|
132
|
+
return { path: r.path, bytes_written: r.bytes_written, sha256: r.sha256, durable: r.durable };
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'list_files',
|
|
137
|
+
permission: 'files',
|
|
138
|
+
description: 'List a directory in the workspace.',
|
|
139
|
+
parameters: obj({ path: path('Absolute directory path.'), limit: { type: 'integer', minimum: 1, maximum: 10_000 } }, ['path']),
|
|
140
|
+
run: async (a) => {
|
|
141
|
+
const r = await cell().files.list(String(a.path), typeof a.limit === 'number' ? { limit: a.limit } : { limit: 500 });
|
|
142
|
+
return { entries: r.entries.map((e) => ({ name: e.name, path: e.path, type: e.type, size: e.size, modified_at: e.modified_at })), truncated: r.truncated };
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'list_processes',
|
|
147
|
+
permission: 'process',
|
|
148
|
+
description: 'List processes running in the workspace.',
|
|
149
|
+
parameters: obj({}),
|
|
150
|
+
run: async () => {
|
|
151
|
+
const r = await cell().processes.list();
|
|
152
|
+
return { processes: r.data.map((p) => ({ pid: p.pid, ppid: p.ppid, comm: p.comm, cmdline: p.cmdline, state: p.state, rss_bytes: p.rss_bytes })) };
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'signal_process',
|
|
157
|
+
permission: 'process',
|
|
158
|
+
description: 'Send a signal to a process in the workspace (e.g. stop a server).',
|
|
159
|
+
parameters: obj({ pid: { type: 'integer', minimum: 2 }, signal }, ['pid', 'signal']),
|
|
160
|
+
run: async (a) => {
|
|
161
|
+
await cell().processes.signal(Number(a.pid), String(a.signal));
|
|
162
|
+
return { signalled: true };
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'terminal_open',
|
|
167
|
+
permission: 'pty',
|
|
168
|
+
description: 'Open an interactive terminal (PTY) in the workspace; returns a session_id for terminal_send/terminal_read.',
|
|
169
|
+
parameters: obj({ command: { type: 'string', maxLength: 10_000, description: 'Program to run (default: login shell).' }, rows: { type: 'integer', minimum: 1, maximum: 1000 }, cols: { type: 'integer', minimum: 1, maximum: 1000 } }),
|
|
170
|
+
run: async (a) => {
|
|
171
|
+
const s = await cell().pty.open({
|
|
172
|
+
...(typeof a.command === 'string' ? { argv: ['bash', '-lc', a.command] } : {}),
|
|
173
|
+
...(typeof a.rows === 'number' ? { rows: a.rows } : {}),
|
|
174
|
+
...(typeof a.cols === 'number' ? { cols: a.cols } : {}),
|
|
175
|
+
});
|
|
176
|
+
return { session_id: s.session_id, state: s.state, next_offset: s.output_size };
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'terminal_send',
|
|
181
|
+
permission: 'pty',
|
|
182
|
+
description: 'Type input into a terminal session (include "\\n" to press Enter).',
|
|
183
|
+
parameters: obj({ session_id: { type: 'string', minLength: 1, maxLength: 64 }, input: { type: 'string', maxLength: 1_000_000 } }, ['session_id', 'input']),
|
|
184
|
+
run: async (a) => {
|
|
185
|
+
const s = await cell().pty.input(String(a.session_id), String(a.input));
|
|
186
|
+
return { state: s.state, next_offset: s.output_size };
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'terminal_read',
|
|
191
|
+
permission: 'pty',
|
|
192
|
+
description: 'Read terminal output from an offset (returns next_offset to continue). Waits briefly for new output.',
|
|
193
|
+
parameters: obj({ session_id: { type: 'string', minLength: 1, maxLength: 64 }, offset: { type: 'integer', minimum: 0 }, wait_ms: { type: 'integer', minimum: 0, maximum: 60_000 } }, ['session_id']),
|
|
194
|
+
run: async (a) => {
|
|
195
|
+
const r = await cell().pty.read(String(a.session_id), {
|
|
196
|
+
offset: typeof a.offset === 'number' ? a.offset : 0,
|
|
197
|
+
timeoutMs: typeof a.wait_ms === 'number' ? Math.max(100, a.wait_ms) : 3_000,
|
|
198
|
+
maxBytes: max,
|
|
199
|
+
});
|
|
200
|
+
return { output: r.output, next_offset: r.nextOffset, exited: r.exited, exit_code: r.session?.exit_code ?? null };
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'terminal_close',
|
|
205
|
+
permission: 'pty',
|
|
206
|
+
description: 'Close a terminal session.',
|
|
207
|
+
parameters: obj({ session_id: { type: 'string', minLength: 1, maxLength: 64 } }, ['session_id']),
|
|
208
|
+
run: async (a) => {
|
|
209
|
+
const s = await cell().pty.close(String(a.session_id));
|
|
210
|
+
return { state: s.state };
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'git_clone',
|
|
215
|
+
permission: 'git',
|
|
216
|
+
description: 'Clone a git repository (HTTPS) into the workspace.',
|
|
217
|
+
parameters: obj({ url: { type: 'string', minLength: 9, maxLength: 2048, description: 'https:// remote URL.' }, path: path('Destination directory.'), branch: { type: 'string', maxLength: 255 }, depth: { type: 'integer', minimum: 1 } }, ['url', 'path']),
|
|
218
|
+
run: async (a) => {
|
|
219
|
+
const r = await cell().git.clone({
|
|
220
|
+
url: String(a.url),
|
|
221
|
+
path: String(a.path),
|
|
222
|
+
...(typeof a.branch === 'string' ? { branch: a.branch } : {}),
|
|
223
|
+
...(typeof a.depth === 'number' ? { depth: a.depth } : {}),
|
|
224
|
+
});
|
|
225
|
+
return { exit_code: r.exit_code, stdout: clip(r.stdout, max).text, stderr: clip(r.stderr, max).text };
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'git_status',
|
|
230
|
+
permission: 'git',
|
|
231
|
+
description: 'Show the git status of a repository in the workspace.',
|
|
232
|
+
parameters: obj({ path: path('Repository directory.') }, ['path']),
|
|
233
|
+
run: async (a) => cell().git.status(String(a.path)),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'git_commit',
|
|
237
|
+
permission: 'git',
|
|
238
|
+
description: 'Stage all changes and commit in a repository in the workspace.',
|
|
239
|
+
parameters: obj({ path: path('Repository directory.'), message: { type: 'string', minLength: 1, maxLength: 65_536 } }, ['path', 'message']),
|
|
240
|
+
run: async (a) => {
|
|
241
|
+
const r = await cell().git.commit({ path: String(a.path), message: String(a.message), all: true });
|
|
242
|
+
return { exit_code: r.exit_code, commit: r.commit ?? null, stdout: clip(r.stdout, max).text, stderr: clip(r.stderr, max).text };
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'browser_screenshot',
|
|
247
|
+
permission: 'browser',
|
|
248
|
+
description: 'Open a URL in the workspace’s headless browser and return a PNG screenshot (base64).',
|
|
249
|
+
parameters: obj({ url: { type: 'string', minLength: 8, maxLength: 8192 }, width: { type: 'integer', minimum: 100, maximum: 3840 }, height: { type: 'integer', minimum: 100, maximum: 2160 } }, ['url']),
|
|
250
|
+
run: async (a) => {
|
|
251
|
+
const png = await cell().browser.screenshot({
|
|
252
|
+
url: String(a.url),
|
|
253
|
+
...(typeof a.width === 'number' ? { width: a.width } : {}),
|
|
254
|
+
...(typeof a.height === 'number' ? { height: a.height } : {}),
|
|
255
|
+
});
|
|
256
|
+
return { mime_type: 'image/png', bytes: png.length, data_base64: Buffer.from(png).toString('base64') };
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'browser_content',
|
|
261
|
+
permission: 'browser',
|
|
262
|
+
description: 'Open a URL in the workspace’s headless browser and return the rendered page as text or HTML.',
|
|
263
|
+
parameters: obj({ url: { type: 'string', minLength: 8, maxLength: 8192 }, format: { type: 'string', enum: ['text', 'html'] } }, ['url']),
|
|
264
|
+
run: async (a) => {
|
|
265
|
+
const r = await cell().browser.content({ url: String(a.url), format: a.format === 'html' ? 'html' : 'text' });
|
|
266
|
+
const c = clip(r.content, max);
|
|
267
|
+
return { url: r.url, format: r.format, content: c.text, truncated: r.truncated || c.truncated };
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
return defs
|
|
272
|
+
.filter((d) => allowed.has(d.permission))
|
|
273
|
+
.map((d) => ({
|
|
274
|
+
name: `${prefix}${d.name}`,
|
|
275
|
+
description: d.description,
|
|
276
|
+
parameters: d.parameters,
|
|
277
|
+
permission: d.permission,
|
|
278
|
+
execute: async (args, options = {}) => {
|
|
279
|
+
const issues = validateArgs(d.parameters, args);
|
|
280
|
+
if (issues.length > 0)
|
|
281
|
+
throw new ToolArgumentError(`${prefix}${d.name}`, issues);
|
|
282
|
+
return d.run(args, options);
|
|
283
|
+
},
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
/** OpenAI tool definitions: Chat Completions (`{type:'function', function:{...}}`) or Responses API. */
|
|
287
|
+
export function toOpenAITools(tools, opts = {}) {
|
|
288
|
+
return opts.api === 'responses'
|
|
289
|
+
? tools.map((t) => ({ type: 'function', name: t.name, description: t.description, parameters: t.parameters, strict: false }))
|
|
290
|
+
: tools.map((t) => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
291
|
+
}
|
|
292
|
+
/** Anthropic Messages API tool definitions (`{ name, description, input_schema }`). */
|
|
293
|
+
export function toAnthropicTools(tools) {
|
|
294
|
+
return tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters }));
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Runs a model's tool call: `arguments` may be the JSON string (OpenAI) or an object (Anthropic
|
|
298
|
+
* `input`). Throws for unknown tools and invalid arguments (ToolArgumentError).
|
|
299
|
+
*/
|
|
300
|
+
export async function executeToolCall(tools, call, options = {}) {
|
|
301
|
+
const tool = tools.find((t) => t.name === call.name);
|
|
302
|
+
if (!tool)
|
|
303
|
+
throw new ToolArgumentError(call.name, [`unknown tool ${call.name}`]);
|
|
304
|
+
const raw = call.input ?? call.arguments ?? {};
|
|
305
|
+
let args;
|
|
306
|
+
try {
|
|
307
|
+
args = typeof raw === 'string' ? (raw.trim() === '' ? {} : JSON.parse(raw)) : raw;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
throw new ToolArgumentError(call.name, ['arguments are not valid JSON']);
|
|
311
|
+
}
|
|
312
|
+
return tool.execute(args, options);
|
|
313
|
+
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage, allowances, estimates, grants and spend (Phase 9 application API).
|
|
3
|
+
*
|
|
4
|
+
* const s = await cloud.usage.summary(orgId);
|
|
5
|
+
* if (s.allowance_exhausted) ... // opens/resumes answer 402 allowance_exhausted
|
|
6
|
+
*
|
|
7
|
+
* Every response carries `measurement.measured_through` (usage is complete up to it) and both raw
|
|
8
|
+
* (fractional) and billable (whole-unit) quantities. API keys read organization totals but only
|
|
9
|
+
* their own project's workspaces.
|
|
10
|
+
*/
|
|
11
|
+
import type { operations } from './generated/app-api.js';
|
|
12
|
+
import type { ClientContext } from './client.js';
|
|
13
|
+
type JsonOf<R> = R extends {
|
|
14
|
+
content: {
|
|
15
|
+
'application/json': infer T;
|
|
16
|
+
};
|
|
17
|
+
} ? T : never;
|
|
18
|
+
type Ok<Op> = Op extends {
|
|
19
|
+
responses: infer R;
|
|
20
|
+
} ? {
|
|
21
|
+
[K in keyof R]: K extends 200 | 201 | 202 ? JsonOf<R[K]> : never;
|
|
22
|
+
}[keyof R] : never;
|
|
23
|
+
export type UsageSummary = Ok<operations['getV1OrganizationsOrganizationIdUsageSummary']>;
|
|
24
|
+
export type UsageSeries = Ok<operations['getV1OrganizationsOrganizationIdUsage']>;
|
|
25
|
+
export type UsageEstimate = Ok<operations['getV1OrganizationsOrganizationIdUsageEstimate']>;
|
|
26
|
+
export type Grants = Ok<operations['getV1OrganizationsOrganizationIdGrants']>;
|
|
27
|
+
export type Spend = Ok<operations['getV1OrganizationsOrganizationIdSpend']>;
|
|
28
|
+
export type SpendPolicy = Ok<operations['getV1OrganizationsOrganizationIdSpendPolicy']>;
|
|
29
|
+
export type UsageMeter = UsageSummary['meters'][number]['meter'];
|
|
30
|
+
export interface UsageSeriesParams {
|
|
31
|
+
/** RFC 3339; defaults to the current period's start. */
|
|
32
|
+
from?: string;
|
|
33
|
+
to?: string;
|
|
34
|
+
granularity?: 'hour' | 'day';
|
|
35
|
+
meter?: UsageMeter;
|
|
36
|
+
workspaceId?: string;
|
|
37
|
+
projectId?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare class UsageApi {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(ctx: () => ClientContext);
|
|
42
|
+
/** Current-period usage per meter, allowances with enforcement and cap state, measurement freshness. */
|
|
43
|
+
summary(organizationId: string): Promise<UsageSummary>;
|
|
44
|
+
/** Time series from the ledger (hour: <= 31 days, day: <= 400 days per request). */
|
|
45
|
+
series(organizationId: string, params?: UsageSeriesParams): Promise<UsageSeries>;
|
|
46
|
+
/** Usage of one workspace. */
|
|
47
|
+
workspace(workspaceId: string, params?: Omit<UsageSeriesParams, 'workspaceId' | 'projectId'>): Promise<UsageSeries>;
|
|
48
|
+
/** Subscription fee, usage charges (0 while overage is disabled) and projected allowance use. */
|
|
49
|
+
estimate(organizationId: string): Promise<UsageEstimate>;
|
|
50
|
+
/** Quotas, per-workspace ceilings/reservations/grants and the compute budget leases granted to the cell. */
|
|
51
|
+
grants(organizationId: string, params?: {
|
|
52
|
+
limit?: number;
|
|
53
|
+
cursor?: string;
|
|
54
|
+
}): Promise<Grants>;
|
|
55
|
+
/** Spend policy, charges this period, cap state and enforcement (leases, overshoot bound). */
|
|
56
|
+
spend(organizationId: string): Promise<Spend>;
|
|
57
|
+
/** Usage alert thresholds (changing them is an owner/billing browser action). */
|
|
58
|
+
spendPolicy(organizationId: string): Promise<SpendPolicy>;
|
|
59
|
+
}
|
|
60
|
+
export {};
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export class UsageApi {
|
|
2
|
+
#ctx;
|
|
3
|
+
constructor(ctx) {
|
|
4
|
+
this.#ctx = ctx;
|
|
5
|
+
}
|
|
6
|
+
#get(path, query) {
|
|
7
|
+
const c = this.#ctx();
|
|
8
|
+
return c.http.json('GET', path, query ? { query } : {}, c.authorization);
|
|
9
|
+
}
|
|
10
|
+
/** Current-period usage per meter, allowances with enforcement and cap state, measurement freshness. */
|
|
11
|
+
summary(organizationId) {
|
|
12
|
+
return this.#get(`/v1/organizations/${encodeURIComponent(organizationId)}/usage/summary`);
|
|
13
|
+
}
|
|
14
|
+
/** Time series from the ledger (hour: <= 31 days, day: <= 400 days per request). */
|
|
15
|
+
series(organizationId, params = {}) {
|
|
16
|
+
return this.#get(`/v1/organizations/${encodeURIComponent(organizationId)}/usage`, {
|
|
17
|
+
from: params.from,
|
|
18
|
+
to: params.to,
|
|
19
|
+
granularity: params.granularity,
|
|
20
|
+
meter: params.meter,
|
|
21
|
+
workspace_id: params.workspaceId,
|
|
22
|
+
project_id: params.projectId,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Usage of one workspace. */
|
|
26
|
+
workspace(workspaceId, params = {}) {
|
|
27
|
+
return this.#get(`/v1/workspaces/${encodeURIComponent(workspaceId)}/usage`, { from: params.from, to: params.to, granularity: params.granularity, meter: params.meter });
|
|
28
|
+
}
|
|
29
|
+
/** Subscription fee, usage charges (0 while overage is disabled) and projected allowance use. */
|
|
30
|
+
estimate(organizationId) {
|
|
31
|
+
return this.#get(`/v1/organizations/${encodeURIComponent(organizationId)}/usage/estimate`);
|
|
32
|
+
}
|
|
33
|
+
/** Quotas, per-workspace ceilings/reservations/grants and the compute budget leases granted to the cell. */
|
|
34
|
+
grants(organizationId, params = {}) {
|
|
35
|
+
return this.#get(`/v1/organizations/${encodeURIComponent(organizationId)}/grants`, { limit: params.limit, cursor: params.cursor });
|
|
36
|
+
}
|
|
37
|
+
/** Spend policy, charges this period, cap state and enforcement (leases, overshoot bound). */
|
|
38
|
+
spend(organizationId) {
|
|
39
|
+
return this.#get(`/v1/organizations/${encodeURIComponent(organizationId)}/spend`);
|
|
40
|
+
}
|
|
41
|
+
/** Usage alert thresholds (changing them is an owner/billing browser action). */
|
|
42
|
+
spendPolicy(organizationId) {
|
|
43
|
+
return this.#get(`/v1/organizations/${encodeURIComponent(organizationId)}/spend-policy`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared volumes over the application API (/v1, contracts §15). Types come from
|
|
3
|
+
* the generated OpenAPI document (schemas `Volume` and `VolumeAttachment`).
|
|
4
|
+
*
|
|
5
|
+
* A volume is persistent shared storage (EFS-backed) owned by a project. It is
|
|
6
|
+
* attached to workspaces of the same project — or of any project of the
|
|
7
|
+
* organization when `org_shared` — at a guest `mount_path`, read-only or
|
|
8
|
+
* read-write. Every mutation is an asynchronous operation executed by the cell:
|
|
9
|
+
* create/attach/detach/delete return at once with the operation; the `wait`
|
|
10
|
+
* option (default on) polls it to completion like `workspaces.open`.
|
|
11
|
+
*
|
|
12
|
+
* const { volume } = await cloud.volumes.create({ name: 'datasets', quotaGib: 20 });
|
|
13
|
+
* await cloud.volumes.attach(workspace.id, { volumeId: volume.id, mountPath: '/mnt/datasets', mode: 'ro' });
|
|
14
|
+
*
|
|
15
|
+
* Shared data is outside checkpoint/fork atomicity: suspend/fork keep the
|
|
16
|
+
* attachment (a fork gets the volumes its project may use), never a copy of the
|
|
17
|
+
* contents.
|
|
18
|
+
*/
|
|
19
|
+
import type { ClientContext, Operation, Page, WaitOptions } from './client.js';
|
|
20
|
+
import type { components } from './generated/app-api.js';
|
|
21
|
+
export type Volume = components['schemas']['Volume'];
|
|
22
|
+
export type VolumeAttachment = components['schemas']['VolumeAttachment'];
|
|
23
|
+
export type VolumeState = Volume['state'];
|
|
24
|
+
export type VolumeAttachmentState = VolumeAttachment['state'];
|
|
25
|
+
export interface CreateVolumeParams {
|
|
26
|
+
name: string;
|
|
27
|
+
quotaGib: number;
|
|
28
|
+
/** Attachable from every project of the organization (owners/admins only; API keys get 403). */
|
|
29
|
+
orgShared?: boolean;
|
|
30
|
+
/** Defaults to the API key's project. */
|
|
31
|
+
projectId?: string;
|
|
32
|
+
/** `false`: return while `creating`. Default: wait until the cell made it available (or failed). */
|
|
33
|
+
wait?: false | WaitOptions;
|
|
34
|
+
idempotencyKey?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface AttachVolumeParams {
|
|
37
|
+
volumeId: string;
|
|
38
|
+
/** Absolute guest path, e.g. /mnt/data (system directories and nested mounts are refused). */
|
|
39
|
+
mountPath: string;
|
|
40
|
+
/** Default 'rw'. */
|
|
41
|
+
mode?: 'ro' | 'rw';
|
|
42
|
+
wait?: false | WaitOptions;
|
|
43
|
+
idempotencyKey?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface DeleteVolumeParams {
|
|
46
|
+
projectId?: string;
|
|
47
|
+
/** Detach it from every workspace as part of the delete (guests see I/O errors on the mount). */
|
|
48
|
+
force?: boolean;
|
|
49
|
+
wait?: false | WaitOptions;
|
|
50
|
+
idempotencyKey?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface VolumeResult {
|
|
53
|
+
volume: Volume;
|
|
54
|
+
operation: Operation;
|
|
55
|
+
}
|
|
56
|
+
export interface AttachmentResult {
|
|
57
|
+
attachment: VolumeAttachment;
|
|
58
|
+
/** null only when the volume was already attached with the same mount_path and mode. */
|
|
59
|
+
operation: Operation | null;
|
|
60
|
+
}
|
|
61
|
+
export declare class VolumesApi {
|
|
62
|
+
#private;
|
|
63
|
+
constructor(ctx: () => ClientContext);
|
|
64
|
+
/** Creates a volume (202) and, unless `wait: false`, waits until the cell reports it available. */
|
|
65
|
+
create(params: CreateVolumeParams): Promise<VolumeResult>;
|
|
66
|
+
get(volumeId: string, params?: {
|
|
67
|
+
projectId?: string;
|
|
68
|
+
}): Promise<Volume>;
|
|
69
|
+
/** The project's volumes; `includeOrgShared` adds the organization's org-shared volumes (attachable here). */
|
|
70
|
+
list(params?: {
|
|
71
|
+
projectId?: string;
|
|
72
|
+
includeOrgShared?: boolean;
|
|
73
|
+
includeDeleted?: boolean;
|
|
74
|
+
limit?: number;
|
|
75
|
+
cursor?: string;
|
|
76
|
+
}): Promise<Page<Volume>>;
|
|
77
|
+
listAll(params?: {
|
|
78
|
+
projectId?: string;
|
|
79
|
+
includeOrgShared?: boolean;
|
|
80
|
+
includeDeleted?: boolean;
|
|
81
|
+
}): AsyncGenerator<Volume>;
|
|
82
|
+
/** Deletes the volume and its data (refused with 409 volume_attached while attached, unless `force`). */
|
|
83
|
+
delete(volumeId: string, params?: DeleteVolumeParams): Promise<VolumeResult>;
|
|
84
|
+
/** Where a volume is attached (API keys: only their project's workspaces). */
|
|
85
|
+
attachments(volumeId: string, params?: {
|
|
86
|
+
projectId?: string;
|
|
87
|
+
includeDetached?: boolean;
|
|
88
|
+
limit?: number;
|
|
89
|
+
cursor?: string;
|
|
90
|
+
}): Promise<Page<VolumeAttachment>>;
|
|
91
|
+
/** Volumes attached to a workspace. */
|
|
92
|
+
listAttached(workspaceId: string, params?: {
|
|
93
|
+
includeDetached?: boolean;
|
|
94
|
+
limit?: number;
|
|
95
|
+
cursor?: string;
|
|
96
|
+
}): Promise<Page<VolumeAttachment>>;
|
|
97
|
+
/**
|
|
98
|
+
* Attaches a volume at mountPath and, unless `wait: false`, waits until the cell mounted it (or,
|
|
99
|
+
* for a workspace without a running VM, recorded it for the next start: `mounted: false`).
|
|
100
|
+
* 409 operation_in_progress while another lifecycle operation of the workspace runs.
|
|
101
|
+
*/
|
|
102
|
+
attach(workspaceId: string, params: AttachVolumeParams): Promise<AttachmentResult>;
|
|
103
|
+
/** Detaches a volume and, unless `wait: false`, waits until the cell unmounted it. */
|
|
104
|
+
detach(workspaceId: string, volumeId: string, params?: {
|
|
105
|
+
wait?: false | WaitOptions;
|
|
106
|
+
idempotencyKey?: string;
|
|
107
|
+
}): Promise<AttachmentResult>;
|
|
108
|
+
}
|
package/dist/volumes.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { randomId } from "./http.js";
|
|
2
|
+
const enc = encodeURIComponent;
|
|
3
|
+
export class VolumesApi {
|
|
4
|
+
#ctx;
|
|
5
|
+
#projectId = null;
|
|
6
|
+
constructor(ctx) {
|
|
7
|
+
this.#ctx = ctx;
|
|
8
|
+
}
|
|
9
|
+
/** The API key's project (from GET /v1/me, fetched once). */
|
|
10
|
+
#project(projectId) {
|
|
11
|
+
if (projectId !== undefined)
|
|
12
|
+
return Promise.resolve(projectId);
|
|
13
|
+
this.#projectId ??= this.#ctx()
|
|
14
|
+
.http.json('GET', '/v1/me', {}, this.#ctx().authorization)
|
|
15
|
+
.then((me) => {
|
|
16
|
+
if (!me.api_key)
|
|
17
|
+
throw new Error('projectId is required (the credential is not a project API key)');
|
|
18
|
+
return me.api_key.project_id;
|
|
19
|
+
})
|
|
20
|
+
.catch((err) => {
|
|
21
|
+
this.#projectId = null;
|
|
22
|
+
throw err;
|
|
23
|
+
});
|
|
24
|
+
return this.#projectId;
|
|
25
|
+
}
|
|
26
|
+
async #settle(operation, wait) {
|
|
27
|
+
if (wait === false)
|
|
28
|
+
return operation;
|
|
29
|
+
return this.#ctx().workspaces.waitForOperation(operation.id, wait ?? {});
|
|
30
|
+
}
|
|
31
|
+
/** Creates a volume (202) and, unless `wait: false`, waits until the cell reports it available. */
|
|
32
|
+
async create(params) {
|
|
33
|
+
const projectId = await this.#project(params.projectId);
|
|
34
|
+
const body = { name: params.name, quota_gib: params.quotaGib };
|
|
35
|
+
if (params.orgShared !== undefined)
|
|
36
|
+
body.org_shared = params.orgShared;
|
|
37
|
+
const res = await this.#ctx().http.json('POST', `/v1/projects/${enc(projectId)}/volumes`, { json: body, idempotencyKey: params.idempotencyKey ?? randomId('volume-') }, this.#ctx().authorization);
|
|
38
|
+
if (params.wait === false)
|
|
39
|
+
return res;
|
|
40
|
+
const operation = await this.#settle(res.operation, params.wait);
|
|
41
|
+
return { volume: await this.get(res.volume.id, { projectId }), operation };
|
|
42
|
+
}
|
|
43
|
+
async get(volumeId, params = {}) {
|
|
44
|
+
const projectId = await this.#project(params.projectId);
|
|
45
|
+
return this.#ctx().http.json('GET', `/v1/projects/${enc(projectId)}/volumes/${enc(volumeId)}`, {}, this.#ctx().authorization);
|
|
46
|
+
}
|
|
47
|
+
/** The project's volumes; `includeOrgShared` adds the organization's org-shared volumes (attachable here). */
|
|
48
|
+
async list(params = {}) {
|
|
49
|
+
const projectId = await this.#project(params.projectId);
|
|
50
|
+
const page = await this.#ctx().http.json('GET', `/v1/projects/${enc(projectId)}/volumes`, { query: { include_org_shared: params.includeOrgShared, include_deleted: params.includeDeleted, limit: params.limit, cursor: params.cursor } }, this.#ctx().authorization);
|
|
51
|
+
return { data: page.data, nextCursor: page.next_cursor };
|
|
52
|
+
}
|
|
53
|
+
async *listAll(params = {}) {
|
|
54
|
+
let cursor;
|
|
55
|
+
do {
|
|
56
|
+
const page = await this.list({ ...params, limit: 200, ...(cursor === undefined ? {} : { cursor }) });
|
|
57
|
+
yield* page.data;
|
|
58
|
+
cursor = page.nextCursor ?? undefined;
|
|
59
|
+
} while (cursor !== undefined);
|
|
60
|
+
}
|
|
61
|
+
/** Deletes the volume and its data (refused with 409 volume_attached while attached, unless `force`). */
|
|
62
|
+
async delete(volumeId, params = {}) {
|
|
63
|
+
const projectId = await this.#project(params.projectId);
|
|
64
|
+
let query = {};
|
|
65
|
+
if (params.force) {
|
|
66
|
+
const v = await this.get(volumeId, { projectId });
|
|
67
|
+
query = { force: true, confirm_name: v.name };
|
|
68
|
+
}
|
|
69
|
+
const res = await this.#ctx().http.json('DELETE', `/v1/projects/${enc(projectId)}/volumes/${enc(volumeId)}`, { query, idempotencyKey: params.idempotencyKey ?? randomId('volume-delete-') }, this.#ctx().authorization);
|
|
70
|
+
if (params.wait === false)
|
|
71
|
+
return res;
|
|
72
|
+
const operation = await this.#settle(res.operation, params.wait);
|
|
73
|
+
return { volume: await this.get(volumeId, { projectId }), operation };
|
|
74
|
+
}
|
|
75
|
+
/** Where a volume is attached (API keys: only their project's workspaces). */
|
|
76
|
+
async attachments(volumeId, params = {}) {
|
|
77
|
+
const projectId = await this.#project(params.projectId);
|
|
78
|
+
const page = await this.#ctx().http.json('GET', `/v1/projects/${enc(projectId)}/volumes/${enc(volumeId)}/attachments`, { query: { include_detached: params.includeDetached, limit: params.limit, cursor: params.cursor } }, this.#ctx().authorization);
|
|
79
|
+
return { data: page.data, nextCursor: page.next_cursor };
|
|
80
|
+
}
|
|
81
|
+
/** Volumes attached to a workspace. */
|
|
82
|
+
async listAttached(workspaceId, params = {}) {
|
|
83
|
+
const page = await this.#ctx().http.json('GET', `/v1/workspaces/${enc(workspaceId)}/volumes`, { query: { include_detached: params.includeDetached, limit: params.limit, cursor: params.cursor } }, this.#ctx().authorization);
|
|
84
|
+
return { data: page.data, nextCursor: page.next_cursor };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Attaches a volume at mountPath and, unless `wait: false`, waits until the cell mounted it (or,
|
|
88
|
+
* for a workspace without a running VM, recorded it for the next start: `mounted: false`).
|
|
89
|
+
* 409 operation_in_progress while another lifecycle operation of the workspace runs.
|
|
90
|
+
*/
|
|
91
|
+
async attach(workspaceId, params) {
|
|
92
|
+
const body = { volume_id: params.volumeId, mount_path: params.mountPath };
|
|
93
|
+
if (params.mode !== undefined)
|
|
94
|
+
body.mode = params.mode;
|
|
95
|
+
const res = await this.#ctx().http.json('POST', `/v1/workspaces/${enc(workspaceId)}/volumes`, { json: body, idempotencyKey: params.idempotencyKey ?? randomId('volume-attach-') }, this.#ctx().authorization);
|
|
96
|
+
if (params.wait === false || res.operation === null)
|
|
97
|
+
return res;
|
|
98
|
+
const operation = await this.#settle(res.operation, params.wait);
|
|
99
|
+
return { attachment: await this.#attachment(workspaceId, res.attachment.id), operation };
|
|
100
|
+
}
|
|
101
|
+
/** Detaches a volume and, unless `wait: false`, waits until the cell unmounted it. */
|
|
102
|
+
async detach(workspaceId, volumeId, params = {}) {
|
|
103
|
+
const res = await this.#ctx().http.json('DELETE', `/v1/workspaces/${enc(workspaceId)}/volumes/${enc(volumeId)}`, { idempotencyKey: params.idempotencyKey ?? randomId('volume-detach-') }, this.#ctx().authorization);
|
|
104
|
+
if (params.wait === false)
|
|
105
|
+
return res;
|
|
106
|
+
const operation = await this.#settle(res.operation, params.wait);
|
|
107
|
+
return { attachment: await this.#attachment(workspaceId, res.attachment.id), operation };
|
|
108
|
+
}
|
|
109
|
+
async #attachment(workspaceId, attachmentId) {
|
|
110
|
+
let cursor;
|
|
111
|
+
do {
|
|
112
|
+
const page = await this.listAttached(workspaceId, { includeDetached: true, limit: 200, ...(cursor === undefined ? {} : { cursor }) });
|
|
113
|
+
const hit = page.data.find((a) => a.id === attachmentId);
|
|
114
|
+
if (hit)
|
|
115
|
+
return hit;
|
|
116
|
+
cursor = page.nextCursor ?? undefined;
|
|
117
|
+
} while (cursor !== undefined);
|
|
118
|
+
throw new Error(`attachment ${attachmentId} not found on workspace ${workspaceId}`);
|
|
119
|
+
}
|
|
120
|
+
}
|