@tonbo/cli 0.3.0 → 0.3.2

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/tonbo.js +1258 -1215
  3. package/package.json +1 -1
package/dist/bin/tonbo.js CHANGED
@@ -32,1246 +32,1293 @@ async function requestJson(fetcher, url, init = {}) {
32
32
  return body;
33
33
  }
34
34
 
35
- // src/api.ts
36
- var TonboApi = class {
37
- constructor(fetcher, accountOrigin = "https://tonbo.dev", managementOrigin = "https://api.tonbo.dev") {
38
- this.fetcher = fetcher;
39
- this.accountOrigin = accountOrigin;
40
- this.managementOrigin = managementOrigin;
41
- }
42
- machineRequest(oauthToken, path6 = "", method = "GET", body) {
43
- return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/machines${path6}`, {
44
- method,
45
- headers: {
46
- authorization: `Bearer ${oauthToken}`,
47
- ...body === void 0 ? {} : { "content-type": "application/json" }
48
- },
49
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
50
- });
51
- }
52
- listAgents(oauthToken) {
53
- return requestJson(
54
- this.fetcher,
55
- `${this.accountOrigin}/api/cli/agents`,
56
- {
57
- headers: { authorization: `Bearer ${oauthToken}` }
58
- }
59
- ).then((body) => body.agents);
60
- }
61
- listOrganizations(oauthToken) {
62
- return requestJson(
63
- this.fetcher,
64
- `${this.accountOrigin}/api/cli/accounts`,
65
- {
66
- headers: { authorization: `Bearer ${oauthToken}` }
67
- }
68
- ).then((body) => body.organizations);
69
- }
70
- createAgent(oauthToken, name, organizationId) {
71
- return requestJson(
72
- this.fetcher,
73
- `${this.accountOrigin}/api/cli/agents`,
74
- {
75
- method: "POST",
76
- headers: {
77
- authorization: `Bearer ${oauthToken}`,
78
- "content-type": "application/json"
79
- },
80
- body: JSON.stringify(organizationId ? { name, orgId: organizationId } : { name })
35
+ // src/contracts.ts
36
+ import { Ajv2020 } from "ajv/dist/2020.js";
37
+
38
+ // src/generated/contracts.ts
39
+ var piAgentSchema = {
40
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
41
+ "$id": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json",
42
+ "title": "PI Agent v1",
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": [
46
+ "runtime",
47
+ "driver"
48
+ ],
49
+ "properties": {
50
+ "runtime": {
51
+ "const": "pi"
52
+ },
53
+ "secrets": {
54
+ "type": "array",
55
+ "maxItems": 32,
56
+ "uniqueItems": true,
57
+ "items": {
58
+ "type": "string",
59
+ "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
81
60
  }
82
- ).then((body) => body.agent);
83
- }
84
- registerSshKey(oauthToken, key) {
85
- return requestJson(
86
- this.fetcher,
87
- `${this.accountOrigin}/api/cli/ssh-keys`,
88
- {
89
- method: "POST",
90
- headers: {
91
- authorization: `Bearer ${oauthToken}`,
92
- "content-type": "application/json"
61
+ },
62
+ "driver": {
63
+ "oneOf": [
64
+ {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "required": [
68
+ "kind"
69
+ ],
70
+ "properties": {
71
+ "kind": {
72
+ "const": "native"
73
+ }
74
+ }
93
75
  },
94
- body: JSON.stringify({
95
- algorithm: key.algorithm,
96
- key_base64: key.keyBase64,
97
- label: key.label
98
- })
99
- }
100
- ).then((body) => body.key);
76
+ {
77
+ "type": "object",
78
+ "additionalProperties": false,
79
+ "required": [
80
+ "kind",
81
+ "protocol",
82
+ "command"
83
+ ],
84
+ "properties": {
85
+ "kind": {
86
+ "const": "command"
87
+ },
88
+ "protocol": {
89
+ "const": "pi-rpc-v1"
90
+ },
91
+ "command": {
92
+ "type": "array",
93
+ "minItems": 1,
94
+ "maxItems": 64,
95
+ "items": {
96
+ "type": "string",
97
+ "minLength": 1,
98
+ "maxLength": 1024
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ]
104
+ }
101
105
  }
102
- revokeSshKey(oauthToken, fingerprint) {
103
- return requestJson(
104
- this.fetcher,
105
- `${this.accountOrigin}/api/cli/ssh-keys`,
106
- {
107
- method: "DELETE",
108
- headers: {
109
- authorization: `Bearer ${oauthToken}`,
110
- "content-type": "application/json"
111
- },
112
- body: JSON.stringify({ fingerprint })
106
+ };
107
+ var agentServiceSchema = {
108
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
109
+ "$id": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json",
110
+ "title": "Tonbo Agent application service v1",
111
+ "type": "object",
112
+ "additionalProperties": false,
113
+ "required": [
114
+ "command"
115
+ ],
116
+ "properties": {
117
+ "command": {
118
+ "type": "array",
119
+ "minItems": 1,
120
+ "maxItems": 64,
121
+ "items": {
122
+ "type": "string",
123
+ "minLength": 1,
124
+ "maxLength": 4096
113
125
  }
114
- ).then((body) => body.key);
115
- }
116
- exchangeManagementToken(oauthToken, agentId) {
117
- return requestJson(
118
- this.fetcher,
119
- `${this.accountOrigin}/api/cli/agents/${agentId}/token`,
120
- {
121
- method: "POST",
122
- headers: { authorization: `Bearer ${oauthToken}` }
126
+ },
127
+ "secrets": {
128
+ "type": "array",
129
+ "maxItems": 32,
130
+ "uniqueItems": true,
131
+ "items": {
132
+ "type": "string",
133
+ "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
123
134
  }
124
- ).then((body) => body.access_token);
135
+ },
136
+ "kubernetes": {
137
+ "$ref": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json#/$defs/request"
138
+ }
125
139
  }
126
- async deploy({
127
- bundle,
128
- origin,
129
- agentId,
130
- promote,
131
- spec,
132
- token
133
- }) {
134
- const descriptor = {
135
- format: bundle.format,
136
- sha256: bundle.sha256,
137
- size_bytes: bundle.size_bytes
138
- };
139
- const bundlesPath = `/v1/agents/${agentId}/source-bundles`;
140
- const prepared = await this.management("PUT", `${bundlesPath}/${bundle.sha256}`, token, {
141
- format: descriptor.format,
142
- size_bytes: descriptor.size_bytes
143
- });
144
- if (prepared.status === "upload") {
145
- if (!prepared.upload_url) throw new Error("Tonbo did not return a source upload URL.");
146
- if (!prepared.upload_headers || prepared.upload_headers["if-none-match"] !== "*")
147
- throw new Error("Missing immutable source upload headers.");
148
- const uploaded = await this.fetcher(prepared.upload_url, {
149
- redirect: "error",
150
- method: "PUT",
151
- headers: prepared.upload_headers,
152
- body: new Blob([new Uint8Array(bundle.bytes)])
153
- });
154
- let uploadError = null;
155
- if (!uploaded.ok) {
156
- uploadError = new Error(`Source upload failed with HTTP ${uploaded.status}.`);
157
- }
158
- try {
159
- await this.management("POST", `${bundlesPath}/${bundle.sha256}/complete`, token, {
160
- format: bundle.format,
161
- size_bytes: bundle.size_bytes
162
- });
163
- } catch (error) {
164
- if (uploadError)
165
- throw new AggregateError([uploadError, error], "Source bundle upload did not complete.");
166
- throw error;
140
+ };
141
+ var kubernetesProfilesSchema = {
142
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
143
+ "$id": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json",
144
+ "title": "Tonbo managed Kubernetes profiles v1",
145
+ "$defs": {
146
+ "request": {
147
+ "type": "object",
148
+ "additionalProperties": false,
149
+ "required": [
150
+ "profile"
151
+ ],
152
+ "properties": {
153
+ "profile": false
167
154
  }
168
155
  }
169
- let deployment = await this.createDeployment(agentId, { spec, origin }, token);
170
- const previous = await this.getProduction(agentId, token);
171
- const previousDeployment = previous ? await this.getDeployment(agentId, previous.deployment_id, token) : null;
172
- const unchanged = previousDeployment?.spec_sha256 === deployment.spec_sha256;
173
- let production = previous;
174
- if (promote) {
175
- production = await this.putProduction(
176
- agentId,
177
- {
178
- deployment_id: deployment.id,
179
- desired_state: "running",
180
- expected_generation: previous ? previous.generation : null
181
- },
182
- token
183
- );
184
- deployment = await this.getDeployment(agentId, deployment.id, token);
185
- }
186
- return { deployment, production, unchanged };
187
- }
188
- createDeployment(agentId, body, token) {
189
- return this.management(
190
- "POST",
191
- `/v1/agents/${agentId}/deployments`,
192
- token,
193
- body
194
- ).then((response) => response.data);
195
- }
196
- listDeployments(agentId, token) {
197
- return this.managementList(`/v1/agents/${agentId}/deployments`, token);
198
- }
199
- getDeployment(agentId, deploymentId, token) {
200
- return this.management(
201
- "GET",
202
- `/v1/agents/${agentId}/deployments/${deploymentId}`,
203
- token
204
- ).then((response) => response.data);
205
- }
206
- getProduction(agentId, token) {
207
- return this.management(
208
- "GET",
209
- `/v1/agents/${agentId}/production`,
210
- token
211
- ).then(
212
- (response) => response.data,
213
- (error) => {
214
- if (error.status === 404) return null;
215
- throw error;
156
+ },
157
+ "x-tonbo-profiles": {}
158
+ };
159
+ var declarationSchema = {
160
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
161
+ "$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-v2.schema.json",
162
+ "title": "Tonbo Agent declaration v2",
163
+ "type": "object",
164
+ "additionalProperties": false,
165
+ "required": [
166
+ "version",
167
+ "harness"
168
+ ],
169
+ "properties": {
170
+ "version": {
171
+ "const": 2
172
+ },
173
+ "inference": {
174
+ "type": "object",
175
+ "additionalProperties": false,
176
+ "default": {
177
+ "model": "claude-sonnet-4-5"
178
+ },
179
+ "required": [
180
+ "model"
181
+ ],
182
+ "properties": {
183
+ "model": {
184
+ "type": "string",
185
+ "minLength": 1,
186
+ "maxLength": 160
187
+ }
216
188
  }
217
- );
218
- }
219
- putProduction(agentId, body, token) {
220
- return this.management(
221
- "PUT",
222
- `/v1/agents/${agentId}/production`,
223
- token,
224
- body
225
- ).then((response) => response.data);
226
- }
227
- listRollouts(agentId, token) {
228
- return this.managementList(`/v1/agents/${agentId}/production/rollouts`, token);
229
- }
230
- async run({
231
- agentId,
232
- prompt,
233
- sessionId,
234
- token,
235
- turnId = randomUUID()
236
- }) {
237
- const agentPath = `/v1/agents/${agentId}`;
238
- const production = await this.getProduction(agentId, token);
239
- if (!production)
240
- throw new Error("Agent has no Production Deployment; run `tonbo deploy` first.");
241
- if (production.observed_state !== "running")
242
- throw new Error(
243
- `Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
244
- );
245
- const session = sessionId ? { id: sessionId } : (await this.management("POST", `${agentPath}/sessions`, token, {
246
- deployment_id: production.deployment_id
247
- })).data;
248
- const turn = (await this.management(
249
- "POST",
250
- `${agentPath}/sessions/${session.id}/turns`,
251
- token,
252
- { prompt },
253
- turnId
254
- )).data;
255
- return { production, session, turn };
256
- }
257
- turnEvents({
258
- after = 0,
259
- agentId,
260
- sessionId,
261
- token,
262
- turnId
263
- }) {
264
- return this.management(
265
- "GET",
266
- `/v1/agents/${agentId}/sessions/${sessionId}/turns/${turnId}/events?after=${after}`,
267
- token
268
- );
269
- }
270
- listAgentSecrets(agentId, token) {
271
- return this.management(
272
- "GET",
273
- `/v1/agents/${agentId}/secrets`,
274
- token
275
- ).then((body) => body.data);
276
- }
277
- setAgentSecret(agentId, name, value, token) {
278
- return this.management(
279
- "PUT",
280
- `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
281
- token,
282
- { value }
283
- );
284
- }
285
- deleteAgentSecret(agentId, name, token) {
286
- return this.management(
287
- "DELETE",
288
- `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
289
- token
290
- );
291
- }
292
- management(method, path6, token, body, idempotencyKey) {
293
- return requestJson(this.fetcher, `${this.managementOrigin}${path6}`, {
294
- method,
295
- headers: {
296
- authorization: `Bearer ${token}`,
297
- ...body === void 0 ? {} : {
298
- "content-type": "application/json",
299
- "idempotency-key": idempotencyKey ?? randomUUID()
189
+ },
190
+ "build": {
191
+ "type": "object",
192
+ "additionalProperties": false,
193
+ "required": [
194
+ "command"
195
+ ],
196
+ "properties": {
197
+ "command": {
198
+ "type": "array",
199
+ "minItems": 1,
200
+ "maxItems": 64,
201
+ "items": {
202
+ "type": "string",
203
+ "minLength": 1,
204
+ "maxLength": 1024
205
+ }
300
206
  }
301
- },
302
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
303
- });
207
+ }
208
+ },
209
+ "service": {
210
+ "$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
211
+ },
212
+ "agent": {
213
+ "$ref": "https://contracts.tonbo.dev/agents/agent-name-v1.json#/$defs/publicHostname"
214
+ },
215
+ "harness": {
216
+ "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
217
+ }
304
218
  }
305
- async managementList(path6, token) {
306
- const values = [];
307
- let cursor = null;
308
- do {
309
- const separator = path6.includes("?") ? "&" : "?";
310
- const page = await this.management(
311
- "GET",
312
- `${path6}${separator}limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
313
- token
314
- );
315
- values.push(...page.data);
316
- cursor = page.next_cursor;
317
- } while (cursor);
318
- return values;
219
+ };
220
+ var deploymentSchema = {
221
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
222
+ "$id": "https://contracts.tonbo.dev/agents/managed-deployment-v1.schema.json",
223
+ "title": "Managed Agent Deployment v1",
224
+ "type": "object",
225
+ "additionalProperties": false,
226
+ "required": [
227
+ "version",
228
+ "agent",
229
+ "source",
230
+ "inference"
231
+ ],
232
+ "properties": {
233
+ "version": {
234
+ "const": 1
235
+ },
236
+ "agent": {
237
+ "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
238
+ },
239
+ "source": {
240
+ "type": "object",
241
+ "additionalProperties": false,
242
+ "required": [
243
+ "format",
244
+ "sha256",
245
+ "size_bytes"
246
+ ],
247
+ "properties": {
248
+ "format": {
249
+ "const": "tar-v1"
250
+ },
251
+ "sha256": {
252
+ "type": "string",
253
+ "pattern": "^[0-9a-f]{64}$"
254
+ },
255
+ "size_bytes": {
256
+ "type": "integer",
257
+ "minimum": 1,
258
+ "maximum": 67108864
259
+ }
260
+ }
261
+ },
262
+ "inference": {
263
+ "type": "object",
264
+ "additionalProperties": false,
265
+ "required": [
266
+ "model"
267
+ ],
268
+ "properties": {
269
+ "model": {
270
+ "type": "string",
271
+ "minLength": 1,
272
+ "maxLength": 160
273
+ }
274
+ }
275
+ },
276
+ "service": {
277
+ "$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
278
+ }
319
279
  }
320
280
  };
321
-
322
- // src/agent-source.ts
323
- import { lstat, readFile } from "node:fs/promises";
324
- import path from "node:path";
325
-
326
- // ../../packages/agent-source-inspector/src/index.ts
327
- var AGENTS_INSTRUCTIONS_PATH = "AGENTS.md";
328
- var PI_SETTINGS_PATH = ".pi/settings.json";
329
- var MAX_HARNESS_CONFIG_BYTES = 256 * 1024;
330
- var supportedHarnesses = [
331
- {
332
- command: "pi",
333
- id: "pi",
334
- name: "PI"
335
- }
336
- ];
337
- var supportedExecutionTargets = [
338
- { driver: "native", harness: "pi" },
339
- { driver: "command", harness: "pi" }
340
- ];
341
- var AgentSourceInspectionError = class extends Error {
342
- constructor(code, path6, message, options) {
343
- super(message, options);
344
- this.code = code;
345
- this.path = path6;
346
- this.name = "AgentSourceInspectionError";
281
+ var agentNameContract = {
282
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
283
+ "$id": "https://contracts.tonbo.dev/agents/agent-name-v1.json",
284
+ "x-tonbo-version": 1,
285
+ "x-tonbo-public-hostname-apex": "tonbo.sh",
286
+ "x-tonbo-public-hostname-template": "<agent>-<organization>-<word>-<word>.tonbo.sh",
287
+ "$defs": {
288
+ "name": {
289
+ "type": "string",
290
+ "minLength": 1,
291
+ "maxLength": 40,
292
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
293
+ "not": {
294
+ "enum": [
295
+ "api",
296
+ "artifacts",
297
+ "auth",
298
+ "inference",
299
+ "network-health",
300
+ "sandbox-control",
301
+ "status",
302
+ "streams",
303
+ "www"
304
+ ]
305
+ }
306
+ },
307
+ "publicHostname": {
308
+ "type": "string",
309
+ "minLength": 16,
310
+ "maxLength": 72,
311
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)+\\.tonbo\\.sh$",
312
+ "not": {
313
+ "enum": [
314
+ "api.tonbo.sh",
315
+ "artifacts.tonbo.sh",
316
+ "auth.tonbo.sh",
317
+ "inference.tonbo.sh",
318
+ "network-health.tonbo.sh",
319
+ "sandbox-control.tonbo.sh",
320
+ "status.tonbo.sh",
321
+ "streams.tonbo.sh",
322
+ "www.tonbo.sh"
323
+ ]
324
+ }
325
+ },
326
+ "hostnameWord": {
327
+ "type": "string",
328
+ "enum": [
329
+ "arch",
330
+ "bark",
331
+ "beam",
332
+ "bell",
333
+ "bird",
334
+ "blue",
335
+ "boat",
336
+ "bold",
337
+ "book",
338
+ "calm",
339
+ "cave",
340
+ "clay",
341
+ "coal",
342
+ "cool",
343
+ "cove",
344
+ "dawn",
345
+ "deer",
346
+ "dove",
347
+ "dune",
348
+ "dusk",
349
+ "echo",
350
+ "fern",
351
+ "fire",
352
+ "flax",
353
+ "flow",
354
+ "foam",
355
+ "fawn",
356
+ "frog",
357
+ "gate",
358
+ "glow",
359
+ "gold",
360
+ "gray",
361
+ "gulf",
362
+ "hail",
363
+ "halo",
364
+ "hare",
365
+ "hawk",
366
+ "hill",
367
+ "iris",
368
+ "jade",
369
+ "kite",
370
+ "lake",
371
+ "lamb",
372
+ "leaf",
373
+ "lime",
374
+ "lion",
375
+ "lobe",
376
+ "loft",
377
+ "loom",
378
+ "luma",
379
+ "lynx",
380
+ "mint",
381
+ "mist",
382
+ "moon",
383
+ "moss",
384
+ "navy",
385
+ "nest",
386
+ "nova",
387
+ "oak",
388
+ "ocean",
389
+ "onyx",
390
+ "opal",
391
+ "otter",
392
+ "palm",
393
+ "peak",
394
+ "pine",
395
+ "pond",
396
+ "pool",
397
+ "rain",
398
+ "reed",
399
+ "reef",
400
+ "rice",
401
+ "ring",
402
+ "rise",
403
+ "road",
404
+ "rock",
405
+ "rose",
406
+ "ruby",
407
+ "sage",
408
+ "sand",
409
+ "seal",
410
+ "seed",
411
+ "silk",
412
+ "sky",
413
+ "snow",
414
+ "soft",
415
+ "star",
416
+ "stem",
417
+ "stone",
418
+ "surf",
419
+ "swan",
420
+ "teal",
421
+ "tide",
422
+ "tree",
423
+ "vale",
424
+ "vine",
425
+ "wave",
426
+ "west",
427
+ "wind",
428
+ "wing",
429
+ "wolf",
430
+ "wood",
431
+ "wren",
432
+ "yarn",
433
+ "zest"
434
+ ]
435
+ }
347
436
  }
348
437
  };
349
- function parsePiPackageCount(contents) {
350
- let settings;
351
- try {
352
- settings = JSON.parse(contents);
353
- } catch (error) {
354
- throw new AgentSourceInspectionError(
355
- "invalid_pi_settings",
356
- PI_SETTINGS_PATH,
357
- `${PI_SETTINGS_PATH} must contain valid JSON.`,
358
- { cause: error }
359
- );
438
+ var regionDiscoverySchema = {
439
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
440
+ "$id": "https://contracts.tonbo.dev/regions/discovery-v1.schema.json",
441
+ "title": "Regional allocation discovery v1",
442
+ "type": "object",
443
+ "additionalProperties": false,
444
+ "required": [
445
+ "version",
446
+ "regions"
447
+ ],
448
+ "properties": {
449
+ "version": {
450
+ "const": 1
451
+ },
452
+ "regions": {
453
+ "type": "array",
454
+ "items": {
455
+ "type": "object",
456
+ "additionalProperties": false,
457
+ "required": [
458
+ "id",
459
+ "display_name",
460
+ "allocation_enabled"
461
+ ],
462
+ "properties": {
463
+ "id": {
464
+ "type": "string",
465
+ "pattern": "^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$"
466
+ },
467
+ "display_name": {
468
+ "type": "string",
469
+ "minLength": 1
470
+ },
471
+ "allocation_enabled": {
472
+ "type": "boolean"
473
+ }
474
+ }
475
+ }
476
+ }
360
477
  }
361
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
362
- throw new AgentSourceInspectionError(
363
- "invalid_pi_settings",
364
- PI_SETTINGS_PATH,
365
- `${PI_SETTINGS_PATH} must contain a JSON object.`
366
- );
478
+ };
479
+
480
+ // src/contracts.ts
481
+ var ajv = new Ajv2020({ allErrors: true, useDefaults: true });
482
+ ajv.addKeyword({ keyword: "x-tonbo-profiles" });
483
+ ajv.addKeyword({ keyword: "x-tonbo-version" });
484
+ ajv.addKeyword({ keyword: "x-tonbo-public-hostname-apex" });
485
+ ajv.addKeyword({ keyword: "x-tonbo-public-hostname-template" });
486
+ ajv.addSchema(kubernetesProfilesSchema);
487
+ ajv.addSchema(piAgentSchema);
488
+ ajv.addSchema(agentNameContract);
489
+ ajv.addSchema(agentServiceSchema);
490
+ var validateDeclaration = ajv.compile(declarationSchema);
491
+ var validateDeploymentSpec = ajv.compile(deploymentSchema);
492
+ var validateRegionDiscovery = ajv.compile(regionDiscoverySchema);
493
+ function parseRegionDiscovery(value) {
494
+ if (!validateRegionDiscovery(value)) {
495
+ throw new Error(validationMessage("Region discovery", validateRegionDiscovery.errors));
367
496
  }
368
- if (!("packages" in settings)) return 0;
369
- const packages = settings.packages;
370
- if (!Array.isArray(packages)) {
371
- throw new AgentSourceInspectionError(
372
- "invalid_pi_settings",
373
- PI_SETTINGS_PATH,
374
- `${PI_SETTINGS_PATH} packages must be an array.`
375
- );
497
+ const result = value;
498
+ if (new Set(result.regions.map((region) => region.id)).size !== result.regions.length)
499
+ throw new Error("Region discovery contains duplicate region identities.");
500
+ return result;
501
+ }
502
+ function validationMessage(label, errors) {
503
+ const detail = errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
504
+ return `${label} is invalid${detail ? `: ${detail}` : "."}`;
505
+ }
506
+ function parseDeclaration(value) {
507
+ const candidate = structuredClone(value);
508
+ if (!validateDeclaration(candidate)) {
509
+ throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
376
510
  }
377
- return packages.length;
511
+ return candidate;
378
512
  }
379
- async function inspectAgentSource(source) {
380
- const [agentsMd, piSettings] = await Promise.all([
381
- source.has(AGENTS_INSTRUCTIONS_PATH),
382
- source.readText(PI_SETTINGS_PATH, MAX_HARNESS_CONFIG_BYTES)
383
- ]);
384
- if (piSettings !== null) {
385
- return {
386
- version: 1,
387
- instructions: { agentsMd },
388
- harness: {
389
- id: "pi",
390
- reason: "pi_settings_found",
391
- state: "identified"
392
- },
393
- pi: {
394
- packageCount: parsePiPackageCount(piSettings),
395
- settingsFound: true
396
- }
397
- };
513
+ function assertManagedDeploymentSpec(value) {
514
+ if (!validateDeploymentSpec(value)) {
515
+ throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
398
516
  }
399
- return {
400
- version: 1,
401
- instructions: { agentsMd },
402
- harness: {
403
- candidates: supportedHarnesses.map((harness) => harness.id),
404
- reason: "no_harness_specific_config",
405
- state: "selection_required"
406
- },
407
- pi: {
408
- packageCount: 0,
409
- settingsFound: false
410
- }
411
- };
412
- }
413
-
414
- // src/agent-source.ts
415
- var LocalAgentSource = class {
416
- constructor(root) {
417
- this.root = root;
418
- }
419
- async has(filename) {
420
- const metadata = await this.metadata(filename);
421
- return metadata?.isFile() === true;
422
- }
423
- async readText(filename, maxBytes) {
424
- const metadata = await this.metadata(filename);
425
- if (!metadata) return null;
426
- if (!metadata.isFile() || metadata.isSymbolicLink()) {
427
- throw new Error(`${filename} must be a regular file.`);
428
- }
429
- if (metadata.size > maxBytes) {
430
- throw new Error(`${filename} exceeds the ${maxBytes}-byte inspection limit.`);
431
- }
432
- return readFile(path.join(this.root, filename), "utf8");
433
- }
434
- async metadata(filename) {
435
- try {
436
- return await lstat(path.join(this.root, filename));
437
- } catch (error) {
438
- if (error.code === "ENOENT") return null;
439
- throw error;
440
- }
441
- }
442
- };
443
- function inspectLocalAgentSource(root) {
444
- return inspectAgentSource(new LocalAgentSource(root));
445
517
  }
446
518
 
447
- // src/auth.ts
448
- import { createHash, randomBytes } from "node:crypto";
449
- import { createServer } from "node:http";
450
- import { execFile } from "node:child_process";
451
- import { promisify } from "node:util";
452
-
453
- // src/callback-page.ts
454
- var WORDMARK = [
455
- "\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
456
- "\u2588\u2588\u2584 \u2584\u2588\u2588\u2580\u2584 \u2588\u2588 \u2584\u2580\u2588\u2588 \u2584 \u2588\u2588 \u2584\u2580\u2588",
457
- "\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2584\u2588\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2588"
458
- ].join("\n");
459
- var OUTCOMES = {
460
- complete: {
461
- status: 200,
462
- title: "You are signed in.",
463
- body: "Return to your terminal. This tab can be closed."
464
- },
465
- denied: {
466
- status: 400,
467
- title: "Login was not completed.",
468
- body: "Nothing was authorized. Return to your terminal to try again."
469
- },
470
- invalid: {
471
- // The state did not match, the code was missing, or the path was wrong.
472
- // Say that it could not be verified rather than which check failed: the
473
- // person who can read this page is not necessarily the one who started
474
- // the login.
475
- status: 400,
476
- title: "This callback could not be verified.",
477
- body: "Return to your terminal and start the login again."
519
+ // src/api.ts
520
+ var TonboApi = class {
521
+ constructor(fetcher, accountOrigin = "https://tonbo.dev", managementOrigin = "https://api.tonbo.dev") {
522
+ this.fetcher = fetcher;
523
+ this.accountOrigin = accountOrigin;
524
+ this.managementOrigin = managementOrigin;
478
525
  }
479
- };
480
- function renderCallbackPage(outcome) {
481
- const { title, body } = OUTCOMES[outcome];
482
- return `<!doctype html>
483
- <html lang="en">
484
- <head>
485
- <meta charset="utf-8">
486
- <meta name="viewport" content="width=device-width, initial-scale=1">
487
- <meta name="robots" content="noindex">
488
- <title>Tonbo CLI</title>
489
- <style>
490
- :root {
491
- --paper: #f3f2ee;
492
- --ink: #1d1b17;
493
- --rule: #c9c5ba;
494
- --quiet: #5c564a;
495
- --signal: #d64a03;
526
+ async listRegions(oauthToken) {
527
+ return parseRegionDiscovery(
528
+ await requestJson(this.fetcher, `${this.accountOrigin}/api/cli/regions`, {
529
+ headers: { authorization: `Bearer ${oauthToken}` }
530
+ })
531
+ );
496
532
  }
497
- @media (prefers-color-scheme: dark) {
498
- :root {
499
- --paper: #1d1b17;
500
- --ink: #f3f2ee;
501
- --rule: #3a362e;
502
- --quiet: #a49e8f;
503
- --signal: #f08b4b;
504
- }
533
+ machineRequest(oauthToken, path6 = "", method = "GET", body) {
534
+ return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/machines${path6}`, {
535
+ method,
536
+ headers: {
537
+ authorization: `Bearer ${oauthToken}`,
538
+ ...body === void 0 ? {} : { "content-type": "application/json" }
539
+ },
540
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
541
+ });
505
542
  }
506
- * { box-sizing: border-box; }
507
- body {
508
- margin: 0;
509
- min-height: 100vh;
510
- display: grid;
511
- grid-template-rows: auto 1fr;
512
- padding: 0 clamp(20px, 5vw, 72px);
513
- background: var(--paper);
514
- color: var(--ink);
515
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
516
- -webkit-font-smoothing: antialiased;
543
+ listAgents(oauthToken) {
544
+ return requestJson(
545
+ this.fetcher,
546
+ `${this.accountOrigin}/api/cli/agents`,
547
+ {
548
+ headers: { authorization: `Bearer ${oauthToken}` }
549
+ }
550
+ ).then((body) => body.agents);
517
551
  }
518
- header { padding: 28px 0; }
519
- /* Same metrics as .auth-wordmark in the app: the glyphs are the ground
520
- and the letters are the gaps, so the 8px/10px/scaleY(0.8) combination is
521
- what makes the rows meet without seams. Iosevka is not available here,
522
- so it falls back to the platform monospace. */
523
- pre.wordmark {
524
- margin: 0;
525
- font-family: 'Iosevka', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
526
- font-size: 8px;
527
- font-weight: 400;
528
- line-height: 10px;
529
- letter-spacing: 0;
530
- white-space: pre;
531
- color: var(--signal);
532
- transform: scaleY(0.8);
533
- transform-origin: left top;
552
+ listOrganizations(oauthToken) {
553
+ return requestJson(
554
+ this.fetcher,
555
+ `${this.accountOrigin}/api/cli/accounts`,
556
+ {
557
+ headers: { authorization: `Bearer ${oauthToken}` }
558
+ }
559
+ ).then((body) => body.organizations);
534
560
  }
535
- main {
536
- display: flex;
537
- flex-direction: column;
538
- justify-content: center;
539
- max-width: 46ch;
540
- padding-bottom: 12vh;
561
+ createAgent(oauthToken, name, organizationId) {
562
+ return requestJson(
563
+ this.fetcher,
564
+ `${this.accountOrigin}/api/cli/agents`,
565
+ {
566
+ method: "POST",
567
+ headers: {
568
+ authorization: `Bearer ${oauthToken}`,
569
+ "content-type": "application/json"
570
+ },
571
+ body: JSON.stringify(organizationId ? { name, orgId: organizationId } : { name })
572
+ }
573
+ ).then((body) => body.agent);
541
574
  }
542
- h1 {
543
- margin: 0;
544
- font-size: 26px;
545
- font-weight: 300;
546
- line-height: 1.16;
547
- letter-spacing: -0.018em;
575
+ registerSshKey(oauthToken, key) {
576
+ return requestJson(
577
+ this.fetcher,
578
+ `${this.accountOrigin}/api/cli/ssh-keys`,
579
+ {
580
+ method: "POST",
581
+ headers: {
582
+ authorization: `Bearer ${oauthToken}`,
583
+ "content-type": "application/json"
584
+ },
585
+ body: JSON.stringify({
586
+ algorithm: key.algorithm,
587
+ key_base64: key.keyBase64,
588
+ label: key.label
589
+ })
590
+ }
591
+ ).then((body) => body.key);
548
592
  }
549
- p {
550
- margin: 14px 0 0;
551
- font-size: 16px;
552
- line-height: 1.55;
553
- color: var(--quiet);
593
+ revokeSshKey(oauthToken, fingerprint) {
594
+ return requestJson(
595
+ this.fetcher,
596
+ `${this.accountOrigin}/api/cli/ssh-keys`,
597
+ {
598
+ method: "DELETE",
599
+ headers: {
600
+ authorization: `Bearer ${oauthToken}`,
601
+ "content-type": "application/json"
602
+ },
603
+ body: JSON.stringify({ fingerprint })
604
+ }
605
+ ).then((body) => body.key);
554
606
  }
555
- hr {
556
- width: 40px;
557
- margin: 28px 0 0;
558
- border: 0;
559
- border-top: 1px solid var(--rule);
607
+ exchangeManagementToken(oauthToken, agentId) {
608
+ return requestJson(
609
+ this.fetcher,
610
+ `${this.accountOrigin}/api/cli/agents/${agentId}/token`,
611
+ {
612
+ method: "POST",
613
+ headers: { authorization: `Bearer ${oauthToken}` }
614
+ }
615
+ ).then((body) => body.access_token);
560
616
  }
561
- </style>
562
- </head>
563
- <body>
564
- <header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
565
- <main>
566
- <h1>${title}</h1>
567
- <p>${body}</p>
568
- <hr>
569
- </main>
570
- </body>
571
- </html>
572
- `;
573
- }
574
- function callbackResponse(outcome) {
575
- return {
576
- status: OUTCOMES[outcome].status,
577
- headers: {
578
- "content-type": "text/html; charset=utf-8",
579
- "cache-control": "no-store"
580
- },
581
- body: renderCallbackPage(outcome)
582
- };
583
- }
584
-
585
- // src/auth.ts
586
- var exec = promisify(execFile);
587
- var LOOPBACK_REDIRECT = "http://localhost:17655/callback";
588
- function base64url(value) {
589
- return value.toString("base64url");
590
- }
591
- function withAbsoluteExpiry(tokens) {
592
- if (tokens.expires_at || !Number.isInteger(tokens.expires_in) || (tokens.expires_in ?? 0) <= 0)
593
- return tokens;
594
- return {
595
- ...tokens,
596
- expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 0)
597
- };
598
- }
599
- var AuthClient = class {
600
- constructor(credentials, fetcher, accountOrigin = "https://tonbo.dev", browserOpener = openBrowser) {
601
- this.credentials = credentials;
602
- this.fetcher = fetcher;
603
- this.accountOrigin = accountOrigin;
604
- this.browserOpener = browserOpener;
617
+ async deploy({
618
+ bundle,
619
+ origin,
620
+ agentId,
621
+ promote,
622
+ spec,
623
+ token
624
+ }) {
625
+ const descriptor = {
626
+ format: bundle.format,
627
+ sha256: bundle.sha256,
628
+ size_bytes: bundle.size_bytes
629
+ };
630
+ const bundlesPath = `/v1/agents/${agentId}/source-bundles`;
631
+ const prepared = await this.management("PUT", `${bundlesPath}/${bundle.sha256}`, token, {
632
+ format: descriptor.format,
633
+ size_bytes: descriptor.size_bytes
634
+ });
635
+ if (prepared.status === "upload") {
636
+ if (!prepared.upload_url) throw new Error("Tonbo did not return a source upload URL.");
637
+ if (!prepared.upload_headers || prepared.upload_headers["if-none-match"] !== "*")
638
+ throw new Error("Missing immutable source upload headers.");
639
+ const uploaded = await this.fetcher(prepared.upload_url, {
640
+ redirect: "error",
641
+ method: "PUT",
642
+ headers: prepared.upload_headers,
643
+ body: new Blob([new Uint8Array(bundle.bytes)])
644
+ });
645
+ let uploadError = null;
646
+ if (!uploaded.ok) {
647
+ uploadError = new Error(`Source upload failed with HTTP ${uploaded.status}.`);
648
+ }
649
+ try {
650
+ await this.management("POST", `${bundlesPath}/${bundle.sha256}/complete`, token, {
651
+ format: bundle.format,
652
+ size_bytes: bundle.size_bytes
653
+ });
654
+ } catch (error) {
655
+ if (uploadError)
656
+ throw new AggregateError([uploadError, error], "Source bundle upload did not complete.");
657
+ throw error;
658
+ }
659
+ }
660
+ let deployment = await this.createDeployment(agentId, { spec, origin }, token);
661
+ const previous = await this.getProduction(agentId, token);
662
+ const previousDeployment = previous ? await this.getDeployment(agentId, previous.deployment_id, token) : null;
663
+ const unchanged = previousDeployment?.spec_sha256 === deployment.spec_sha256;
664
+ let production = previous;
665
+ if (promote) {
666
+ production = await this.putProduction(
667
+ agentId,
668
+ {
669
+ deployment_id: deployment.id,
670
+ desired_state: "running",
671
+ expected_generation: previous ? previous.generation : null
672
+ },
673
+ token
674
+ );
675
+ deployment = await this.getDeployment(agentId, deployment.id, token);
676
+ }
677
+ return { deployment, production, unchanged };
605
678
  }
606
- async accessToken() {
607
- const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
608
- if (injected) return injected;
609
- const tokens = await this.credentials.load();
610
- if (!tokens) throw new Error("Not logged in. Run `tonbo login`.");
611
- const legacyRelativeExpiry = !tokens.expires_at && tokens.expires_in;
612
- if (!legacyRelativeExpiry && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1e3) + 30))
613
- return tokens.access_token;
614
- if (!tokens.refresh_token) throw new Error("Tonbo login expired. Run `tonbo login` again.");
615
- const config = await this.config();
616
- const refreshed = withAbsoluteExpiry(
617
- await this.exchange(
618
- config.token_endpoint,
619
- new URLSearchParams({
620
- client_id: config.client_id,
621
- grant_type: "refresh_token",
622
- refresh_token: tokens.refresh_token
623
- })
624
- )
679
+ createDeployment(agentId, body, token) {
680
+ return this.management(
681
+ "POST",
682
+ `/v1/agents/${agentId}/deployments`,
683
+ token,
684
+ body
685
+ ).then((response) => response.data);
686
+ }
687
+ listDeployments(agentId, token) {
688
+ return this.managementList(`/v1/agents/${agentId}/deployments`, token);
689
+ }
690
+ getDeployment(agentId, deploymentId, token) {
691
+ return this.management(
692
+ "GET",
693
+ `/v1/agents/${agentId}/deployments/${deploymentId}`,
694
+ token
695
+ ).then((response) => response.data);
696
+ }
697
+ getProduction(agentId, token) {
698
+ return this.management(
699
+ "GET",
700
+ `/v1/agents/${agentId}/production`,
701
+ token
702
+ ).then(
703
+ (response) => response.data,
704
+ (error) => {
705
+ if (error.status === 404) return null;
706
+ throw error;
707
+ }
625
708
  );
626
- if (!refreshed.refresh_token) refreshed.refresh_token = tokens.refresh_token;
627
- await this.credentials.save(refreshed);
628
- return refreshed.access_token;
629
709
  }
630
- async login(progress = () => {
710
+ putProduction(agentId, body, token) {
711
+ return this.management(
712
+ "PUT",
713
+ `/v1/agents/${agentId}/production`,
714
+ token,
715
+ body
716
+ ).then((response) => response.data);
717
+ }
718
+ listRollouts(agentId, token) {
719
+ return this.managementList(`/v1/agents/${agentId}/production/rollouts`, token);
720
+ }
721
+ async run({
722
+ agentId,
723
+ prompt,
724
+ sessionId,
725
+ token,
726
+ turnId = randomUUID()
631
727
  }) {
632
- progress({ status: "started", step: "account-config" });
633
- const config = await this.config();
634
- progress({ status: "completed", step: "account-config" });
635
- if (config.redirect_uri !== LOOPBACK_REDIRECT)
636
- throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
637
- const verifier = base64url(randomBytes(48));
638
- const challenge = createHash("sha256").update(verifier).digest("base64url");
639
- const state = base64url(randomBytes(24));
640
- const authorization = new URL(config.authorization_endpoint);
641
- authorization.search = new URLSearchParams({
642
- client_id: config.client_id,
643
- code_challenge: challenge,
644
- code_challenge_method: "S256",
645
- redirect_uri: config.redirect_uri,
646
- response_type: "code",
647
- scope: "openid profile email",
648
- state
649
- }).toString();
650
- const code = await this.listenForCode(
651
- state,
652
- () => this.browserOpener(authorization.toString()),
653
- progress
654
- );
655
- progress({ status: "started", step: "token-exchange" });
656
- const tokens = withAbsoluteExpiry(
657
- await this.exchange(
658
- config.token_endpoint,
659
- new URLSearchParams({
660
- client_id: config.client_id,
661
- code,
662
- code_verifier: verifier,
663
- grant_type: "authorization_code",
664
- redirect_uri: config.redirect_uri
665
- })
666
- )
728
+ const agentPath = `/v1/agents/${agentId}`;
729
+ const production = await this.getProduction(agentId, token);
730
+ if (!production)
731
+ throw new Error("Agent has no Production Deployment; run `tonbo deploy` first.");
732
+ if (production.observed_state !== "running")
733
+ throw new Error(
734
+ `Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
735
+ );
736
+ const session = sessionId ? { id: sessionId } : (await this.management("POST", `${agentPath}/sessions`, token, {
737
+ deployment_id: production.deployment_id
738
+ })).data;
739
+ const turn = (await this.management(
740
+ "POST",
741
+ `${agentPath}/sessions/${session.id}/turns`,
742
+ token,
743
+ { prompt },
744
+ turnId
745
+ )).data;
746
+ return { production, session, turn };
747
+ }
748
+ turnEvents({
749
+ after = 0,
750
+ agentId,
751
+ sessionId,
752
+ token,
753
+ turnId
754
+ }) {
755
+ return this.management(
756
+ "GET",
757
+ `/v1/agents/${agentId}/sessions/${sessionId}/turns/${turnId}/events?after=${after}`,
758
+ token
667
759
  );
668
- progress({ status: "completed", step: "token-exchange" });
669
- progress({ status: "started", step: "credential-store" });
670
- await this.credentials.save(tokens);
671
- progress({ status: "completed", step: "credential-store" });
672
- return tokens;
673
760
  }
674
- config() {
675
- return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/config`);
761
+ listAgentSecrets(agentId, token) {
762
+ return this.management(
763
+ "GET",
764
+ `/v1/agents/${agentId}/secrets`,
765
+ token
766
+ ).then((body) => body.data);
676
767
  }
677
- exchange(endpoint, body) {
678
- return requestJson(this.fetcher, endpoint, {
679
- method: "POST",
680
- headers: { "content-type": "application/x-www-form-urlencoded" },
681
- body
682
- });
768
+ setAgentSecret(agentId, name, value, token) {
769
+ return this.management(
770
+ "PUT",
771
+ `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
772
+ token,
773
+ { value }
774
+ );
683
775
  }
684
- listenForCode(expectedState, ready, progress) {
685
- return new Promise((resolve, reject) => {
686
- let settled = false;
687
- const sockets = /* @__PURE__ */ new Set();
688
- let authorizationStarted = false;
689
- const startAuthorization = () => {
690
- if (authorizationStarted) return;
691
- authorizationStarted = true;
692
- progress({ status: "completed", step: "browser" });
693
- progress({ status: "started", step: "browser-authorization" });
694
- };
695
- const finish = (result, completedResponseSocket) => {
696
- if (settled) return;
697
- settled = true;
698
- clearTimeout(timeout);
699
- const complete = (closeError) => {
700
- if (closeError) reject(closeError);
701
- else if ("code" in result) {
702
- progress({ status: "completed", step: "callback-close" });
703
- resolve(result.code);
704
- } else reject(result.error);
705
- };
706
- if ("code" in result) progress({ status: "started", step: "callback-close" });
707
- if (!server.listening) {
708
- complete();
709
- return;
710
- }
711
- server.close(complete);
712
- for (const socket of sockets) {
713
- if (socket !== completedResponseSocket) socket.destroy();
714
- }
715
- server.closeIdleConnections();
716
- };
717
- const timeout = setTimeout(
718
- () => {
719
- finish({ error: new Error("Timed out waiting for browser login.") });
720
- },
721
- 5 * 60 * 1e3
722
- );
723
- const server = createServer((request, response) => {
724
- const url = new URL(request.url ?? "/", LOOPBACK_REDIRECT);
725
- const code = url.searchParams.get("code");
726
- const oauthError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
727
- if (oauthError) {
728
- const responseSocket2 = response.socket;
729
- respond(
730
- response,
731
- "denied",
732
- () => finish({ error: new Error(oauthError) }, responseSocket2)
733
- );
734
- return;
735
- }
736
- if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState || !code) {
737
- respond(response, "invalid");
738
- return;
776
+ deleteAgentSecret(agentId, name, token) {
777
+ return this.management(
778
+ "DELETE",
779
+ `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
780
+ token
781
+ );
782
+ }
783
+ management(method, path6, token, body, idempotencyKey) {
784
+ return requestJson(this.fetcher, `${this.managementOrigin}${path6}`, {
785
+ method,
786
+ headers: {
787
+ authorization: `Bearer ${token}`,
788
+ ...body === void 0 ? {} : {
789
+ "content-type": "application/json",
790
+ "idempotency-key": idempotencyKey ?? randomUUID()
739
791
  }
740
- startAuthorization();
741
- progress({ status: "completed", step: "browser-authorization" });
742
- const responseSocket = response.socket;
743
- respond(response, "complete", () => finish({ code }, responseSocket));
744
- });
745
- server.on("connection", (socket) => {
746
- sockets.add(socket);
747
- socket.once("close", () => sockets.delete(socket));
748
- });
749
- server.once("error", (error) => finish({ error }));
750
- progress({ status: "started", step: "callback-server" });
751
- server.listen(17655, "localhost", () => {
752
- progress({ status: "completed", step: "callback-server" });
753
- progress({ status: "started", step: "browser" });
754
- void ready().then(startAuthorization).catch(
755
- (error) => finish({
756
- error: error instanceof Error ? error : new Error("Could not open the login browser.")
757
- })
758
- );
759
- });
792
+ },
793
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
760
794
  });
761
795
  }
796
+ async managementList(path6, token) {
797
+ const values = [];
798
+ let cursor = null;
799
+ do {
800
+ const separator = path6.includes("?") ? "&" : "?";
801
+ const page = await this.management(
802
+ "GET",
803
+ `${path6}${separator}limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
804
+ token
805
+ );
806
+ values.push(...page.data);
807
+ cursor = page.next_cursor;
808
+ } while (cursor);
809
+ return values;
810
+ }
762
811
  };
763
- function respond(response, outcome, finished) {
764
- const page = callbackResponse(outcome);
765
- if (finished) response.once("finish", finished);
766
- response.writeHead(page.status, { ...page.headers, connection: "close" });
767
- response.end(page.body);
768
- }
769
- async function openBrowser(url) {
770
- if (process.platform === "darwin") return void await exec("open", [url]);
771
- if (process.platform === "win32")
772
- return void await exec("rundll32", ["url.dll,FileProtocolHandler", url]);
773
- return void await exec("xdg-open", [url]);
774
- }
775
812
 
776
- // src/commands.ts
777
- import { DEFAULT_INFERENCE_MODEL as DEFAULT_INFERENCE_MODEL2 } from "@tonbo/agent-source-bundler/inference-default";
778
-
779
- // ../../packages/agent-source-bundler/src/generated/contracts.ts
780
- var sourceBundleContract = { "version": 1, "format": "tar-v1", "key_prefix": "agent-source-bundles", "content_type": "application/vnd.tonbo.source+tar", "max_bytes": 67108864 };
781
- var piSessionContract = { "version": 1, "adapter": "pi-jsonl-v3", "format_version": 3, "durable_completion_timeout_seconds": 30, "session_directory": "/sessions", "path_template": "/sessions/{session_id}.jsonl", "preflight_command": ["/usr/local/bin/artifacts", "runtime", "session-preflight"] };
813
+ // src/agent-source.ts
814
+ import { lstat, readFile } from "node:fs/promises";
815
+ import path from "node:path";
782
816
 
783
- // ../../packages/agent-source-bundler/src/deployment-origin.ts
784
- function deploymentOriginLabel(actor) {
785
- return actor === "onboarding" ? "Setup guide" : "tonbo deploy";
817
+ // ../../packages/agent-source-inspector/dist/src/index.js
818
+ var AGENTS_INSTRUCTIONS_PATH = "AGENTS.md";
819
+ var PI_SETTINGS_PATH = ".pi/settings.json";
820
+ var MAX_HARNESS_CONFIG_BYTES = 256 * 1024;
821
+ var supportedHarnesses = [
822
+ {
823
+ command: "pi",
824
+ id: "pi",
825
+ name: "PI"
826
+ }
827
+ ];
828
+ var supportedExecutionTargets = [
829
+ { driver: "native", harness: "pi" },
830
+ { driver: "command", harness: "pi" }
831
+ ];
832
+ var AgentSourceInspectionError = class extends Error {
833
+ code;
834
+ path;
835
+ constructor(code, path6, message, options) {
836
+ super(message, options);
837
+ this.code = code;
838
+ this.path = path6;
839
+ this.name = "AgentSourceInspectionError";
840
+ }
841
+ };
842
+ function parsePiPackageCount(contents) {
843
+ let settings;
844
+ try {
845
+ settings = JSON.parse(contents);
846
+ } catch (error) {
847
+ throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} must contain valid JSON.`, { cause: error });
848
+ }
849
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
850
+ throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} must contain a JSON object.`);
851
+ }
852
+ if (!("packages" in settings))
853
+ return 0;
854
+ const packages = settings.packages;
855
+ if (!Array.isArray(packages)) {
856
+ throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} packages must be an array.`);
857
+ }
858
+ return packages.length;
786
859
  }
787
- function githubRepositoryUrl(remote) {
788
- if (typeof remote !== "string" || !remote || /\s/.test(remote) || [...remote].some((character) => character.charCodeAt(0) < 32))
789
- return null;
790
- let path6;
791
- const ssh = /^git@github\.com:([^?#]+)$/.exec(remote);
792
- if (ssh) path6 = ssh[1];
793
- else {
860
+ async function inspectAgentSource(source) {
861
+ const [agentsMd, piSettings] = await Promise.all([
862
+ source.has(AGENTS_INSTRUCTIONS_PATH),
863
+ source.readText(PI_SETTINGS_PATH, MAX_HARNESS_CONFIG_BYTES)
864
+ ]);
865
+ if (piSettings !== null) {
866
+ return {
867
+ version: 1,
868
+ instructions: { agentsMd },
869
+ harness: {
870
+ id: "pi",
871
+ reason: "pi_settings_found",
872
+ state: "identified"
873
+ },
874
+ pi: {
875
+ packageCount: parsePiPackageCount(piSettings),
876
+ settingsFound: true
877
+ }
878
+ };
879
+ }
880
+ return {
881
+ version: 1,
882
+ instructions: { agentsMd },
883
+ harness: {
884
+ candidates: supportedHarnesses.map((harness) => harness.id),
885
+ reason: "no_harness_specific_config",
886
+ state: "selection_required"
887
+ },
888
+ pi: {
889
+ packageCount: 0,
890
+ settingsFound: false
891
+ }
892
+ };
893
+ }
894
+
895
+ // src/agent-source.ts
896
+ var LocalAgentSource = class {
897
+ constructor(root) {
898
+ this.root = root;
899
+ }
900
+ async has(filename) {
901
+ const metadata = await this.metadata(filename);
902
+ return metadata?.isFile() === true;
903
+ }
904
+ async readText(filename, maxBytes) {
905
+ const metadata = await this.metadata(filename);
906
+ if (!metadata) return null;
907
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
908
+ throw new Error(`${filename} must be a regular file.`);
909
+ }
910
+ if (metadata.size > maxBytes) {
911
+ throw new Error(`${filename} exceeds the ${maxBytes}-byte inspection limit.`);
912
+ }
913
+ return readFile(path.join(this.root, filename), "utf8");
914
+ }
915
+ async metadata(filename) {
794
916
  try {
795
- const url = new URL(remote);
796
- if (url.hostname !== "github.com" || url.port || !["https:", "ssh:"].includes(url.protocol))
797
- return null;
798
- path6 = url.pathname.slice(1);
799
- } catch {
800
- return null;
917
+ return await lstat(path.join(this.root, filename));
918
+ } catch (error) {
919
+ if (error.code === "ENOENT") return null;
920
+ throw error;
801
921
  }
802
922
  }
803
- path6 = path6.replace(/\/$/, "").replace(/\.git$/, "");
804
- const parts = path6.split("/");
805
- if (parts.length !== 2 || !/^[a-z\d](?:[a-z\d-]{0,38})$/i.test(parts[0]) || !/^[a-z\d._-]{1,100}$/i.test(parts[1]) || [".", ".."].includes(parts[1]))
806
- return null;
807
- return `https://github.com/${parts[0]}/${parts[1]}`;
923
+ };
924
+ function inspectLocalAgentSource(root) {
925
+ return inspectAgentSource(new LocalAgentSource(root));
808
926
  }
809
927
 
810
- // src/commands.ts
811
- import path4 from "node:path";
928
+ // src/auth.ts
929
+ import { createHash, randomBytes } from "node:crypto";
930
+ import { createServer } from "node:http";
931
+ import { execFile } from "node:child_process";
932
+ import { promisify } from "node:util";
812
933
 
813
- // src/build.ts
814
- import { spawn } from "node:child_process";
815
- async function runBuildCommand(root, command) {
816
- if (command.length === 0) throw new Error("Build command must not be empty.");
817
- await new Promise((resolve, reject) => {
818
- const child = spawn(command[0], command.slice(1), {
819
- cwd: root,
820
- env: process.env,
821
- // Command output is progress, not the CLI result. Keep stdout available
822
- // for the single JSON document emitted by `tonbo --json deploy`.
823
- stdio: ["inherit", process.stderr, process.stderr]
824
- });
825
- child.once("error", reject);
826
- child.once("exit", (code, signal) => {
827
- if (code === 0) resolve();
828
- else
829
- reject(
830
- new Error(
831
- `Build command failed${signal ? ` with signal ${signal}` : ` with status ${code}`}.`
832
- )
833
- );
834
- });
835
- });
836
- }
837
-
838
- // src/contracts.ts
839
- import { Ajv2020 } from "ajv/dist/2020.js";
840
-
841
- // src/generated/contracts.ts
842
- var piAgentSchema = {
843
- "$schema": "https://json-schema.org/draft/2020-12/schema",
844
- "$id": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json",
845
- "title": "PI Agent v1",
846
- "type": "object",
847
- "additionalProperties": false,
848
- "required": [
849
- "runtime",
850
- "driver"
851
- ],
852
- "properties": {
853
- "runtime": {
854
- "const": "pi"
855
- },
856
- "secrets": {
857
- "type": "array",
858
- "maxItems": 32,
859
- "uniqueItems": true,
860
- "items": {
861
- "type": "string",
862
- "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
863
- }
864
- },
865
- "driver": {
866
- "oneOf": [
867
- {
868
- "type": "object",
869
- "additionalProperties": false,
870
- "required": [
871
- "kind"
872
- ],
873
- "properties": {
874
- "kind": {
875
- "const": "native"
876
- }
877
- }
878
- },
879
- {
880
- "type": "object",
881
- "additionalProperties": false,
882
- "required": [
883
- "kind",
884
- "protocol",
885
- "command"
886
- ],
887
- "properties": {
888
- "kind": {
889
- "const": "command"
890
- },
891
- "protocol": {
892
- "const": "pi-rpc-v1"
893
- },
894
- "command": {
895
- "type": "array",
896
- "minItems": 1,
897
- "maxItems": 64,
898
- "items": {
899
- "type": "string",
900
- "minLength": 1,
901
- "maxLength": 1024
902
- }
903
- }
904
- }
905
- }
906
- ]
907
- }
934
+ // src/callback-page.ts
935
+ var WORDMARK = [
936
+ "\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588",
937
+ "\u2588\u2588\u2584 \u2584\u2588\u2588\u2580\u2584 \u2588\u2588 \u2584\u2580\u2588\u2588 \u2584 \u2588\u2588 \u2584\u2580\u2588",
938
+ "\u2588\u2588\u2588\u2584\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2584\u2588\u2584\u2588\u2588\u2584\u2584\u2588\u2588\u2588\u2588\u2584\u2584\u2588"
939
+ ].join("\n");
940
+ var OUTCOMES = {
941
+ complete: {
942
+ status: 200,
943
+ title: "You are signed in.",
944
+ body: "Return to your terminal. This tab can be closed."
945
+ },
946
+ denied: {
947
+ status: 400,
948
+ title: "Login was not completed.",
949
+ body: "Nothing was authorized. Return to your terminal to try again."
950
+ },
951
+ invalid: {
952
+ // The state did not match, the code was missing, or the path was wrong.
953
+ // Say that it could not be verified rather than which check failed: the
954
+ // person who can read this page is not necessarily the one who started
955
+ // the login.
956
+ status: 400,
957
+ title: "This callback could not be verified.",
958
+ body: "Return to your terminal and start the login again."
908
959
  }
909
960
  };
910
- var agentServiceSchema = {
911
- "$schema": "https://json-schema.org/draft/2020-12/schema",
912
- "$id": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json",
913
- "title": "Tonbo Agent application service v1",
914
- "type": "object",
915
- "additionalProperties": false,
916
- "required": [
917
- "command"
918
- ],
919
- "properties": {
920
- "command": {
921
- "type": "array",
922
- "minItems": 1,
923
- "maxItems": 64,
924
- "items": {
925
- "type": "string",
926
- "minLength": 1,
927
- "maxLength": 4096
928
- }
929
- },
930
- "secrets": {
931
- "type": "array",
932
- "maxItems": 32,
933
- "uniqueItems": true,
934
- "items": {
935
- "type": "string",
936
- "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
937
- }
938
- },
939
- "kubernetes": {
940
- "$ref": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json#/$defs/request"
941
- }
961
+ function renderCallbackPage(outcome) {
962
+ const { title, body } = OUTCOMES[outcome];
963
+ return `<!doctype html>
964
+ <html lang="en">
965
+ <head>
966
+ <meta charset="utf-8">
967
+ <meta name="viewport" content="width=device-width, initial-scale=1">
968
+ <meta name="robots" content="noindex">
969
+ <title>Tonbo CLI</title>
970
+ <style>
971
+ :root {
972
+ --paper: #f3f2ee;
973
+ --ink: #1d1b17;
974
+ --rule: #c9c5ba;
975
+ --quiet: #5c564a;
976
+ --signal: #d64a03;
942
977
  }
943
- };
944
- var kubernetesProfilesSchema = {
945
- "$schema": "https://json-schema.org/draft/2020-12/schema",
946
- "$id": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json",
947
- "title": "Tonbo managed Kubernetes profiles v1",
948
- "$defs": {
949
- "request": {
950
- "type": "object",
951
- "additionalProperties": false,
952
- "required": [
953
- "profile"
954
- ],
955
- "properties": {
956
- "profile": false
957
- }
958
- }
959
- },
960
- "x-tonbo-profiles": {}
961
- };
962
- var declarationSchema = {
963
- "$schema": "https://json-schema.org/draft/2020-12/schema",
964
- "$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-v2.schema.json",
965
- "title": "Tonbo Agent declaration v2",
966
- "type": "object",
967
- "additionalProperties": false,
968
- "required": [
969
- "version",
970
- "harness"
971
- ],
972
- "properties": {
973
- "version": {
974
- "const": 2
975
- },
976
- "inference": {
977
- "type": "object",
978
- "additionalProperties": false,
979
- "default": {
980
- "model": "claude-sonnet-4-5"
981
- },
982
- "required": [
983
- "model"
984
- ],
985
- "properties": {
986
- "model": {
987
- "type": "string",
988
- "minLength": 1,
989
- "maxLength": 160
990
- }
991
- }
992
- },
993
- "build": {
994
- "type": "object",
995
- "additionalProperties": false,
996
- "required": [
997
- "command"
998
- ],
999
- "properties": {
1000
- "command": {
1001
- "type": "array",
1002
- "minItems": 1,
1003
- "maxItems": 64,
1004
- "items": {
1005
- "type": "string",
1006
- "minLength": 1,
1007
- "maxLength": 1024
1008
- }
1009
- }
1010
- }
1011
- },
1012
- "service": {
1013
- "$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
1014
- },
1015
- "agent": {
1016
- "$ref": "https://contracts.tonbo.dev/agents/agent-name-v1.json#/$defs/publicHostname"
1017
- },
1018
- "harness": {
1019
- "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
978
+ @media (prefers-color-scheme: dark) {
979
+ :root {
980
+ --paper: #1d1b17;
981
+ --ink: #f3f2ee;
982
+ --rule: #3a362e;
983
+ --quiet: #a49e8f;
984
+ --signal: #f08b4b;
1020
985
  }
1021
986
  }
1022
- };
1023
- var deploymentSchema = {
1024
- "$schema": "https://json-schema.org/draft/2020-12/schema",
1025
- "$id": "https://contracts.tonbo.dev/agents/managed-deployment-v1.schema.json",
1026
- "title": "Managed Agent Deployment v1",
1027
- "type": "object",
1028
- "additionalProperties": false,
1029
- "required": [
1030
- "version",
1031
- "agent",
1032
- "source",
1033
- "inference"
1034
- ],
1035
- "properties": {
1036
- "version": {
1037
- "const": 1
1038
- },
1039
- "agent": {
1040
- "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
1041
- },
1042
- "source": {
1043
- "type": "object",
1044
- "additionalProperties": false,
1045
- "required": [
1046
- "format",
1047
- "sha256",
1048
- "size_bytes"
1049
- ],
1050
- "properties": {
1051
- "format": {
1052
- "const": "tar-v1"
1053
- },
1054
- "sha256": {
1055
- "type": "string",
1056
- "pattern": "^[0-9a-f]{64}$"
1057
- },
1058
- "size_bytes": {
1059
- "type": "integer",
1060
- "minimum": 1,
1061
- "maximum": 67108864
1062
- }
1063
- }
1064
- },
1065
- "inference": {
1066
- "type": "object",
1067
- "additionalProperties": false,
1068
- "required": [
1069
- "model"
1070
- ],
1071
- "properties": {
1072
- "model": {
1073
- "type": "string",
1074
- "minLength": 1,
1075
- "maxLength": 160
1076
- }
1077
- }
1078
- },
1079
- "service": {
1080
- "$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
1081
- }
987
+ * { box-sizing: border-box; }
988
+ body {
989
+ margin: 0;
990
+ min-height: 100vh;
991
+ display: grid;
992
+ grid-template-rows: auto 1fr;
993
+ padding: 0 clamp(20px, 5vw, 72px);
994
+ background: var(--paper);
995
+ color: var(--ink);
996
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
997
+ -webkit-font-smoothing: antialiased;
1082
998
  }
1083
- };
1084
- var agentNameContract = {
1085
- "$schema": "https://json-schema.org/draft/2020-12/schema",
1086
- "$id": "https://contracts.tonbo.dev/agents/agent-name-v1.json",
1087
- "x-tonbo-version": 1,
1088
- "x-tonbo-public-hostname-apex": "tonbo.sh",
1089
- "x-tonbo-public-hostname-template": "<agent>-<organization>-<word>-<word>.tonbo.sh",
1090
- "$defs": {
1091
- "name": {
1092
- "type": "string",
1093
- "minLength": 3,
1094
- "maxLength": 40,
1095
- "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
1096
- "not": {
1097
- "enum": [
1098
- "api",
1099
- "artifacts",
1100
- "auth",
1101
- "inference",
1102
- "network-health",
1103
- "sandbox-control",
1104
- "status",
1105
- "streams",
1106
- "www"
1107
- ]
1108
- }
1109
- },
1110
- "publicHostname": {
1111
- "type": "string",
1112
- "minLength": 16,
1113
- "maxLength": 72,
1114
- "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)+\\.tonbo\\.sh$",
1115
- "not": {
1116
- "enum": [
1117
- "api.tonbo.sh",
1118
- "artifacts.tonbo.sh",
1119
- "auth.tonbo.sh",
1120
- "inference.tonbo.sh",
1121
- "network-health.tonbo.sh",
1122
- "sandbox-control.tonbo.sh",
1123
- "status.tonbo.sh",
1124
- "streams.tonbo.sh",
1125
- "www.tonbo.sh"
1126
- ]
1127
- }
999
+ header { padding: 28px 0; }
1000
+ /* Same metrics as .auth-wordmark in the app: the glyphs are the ground
1001
+ and the letters are the gaps, so the 8px/10px/scaleY(0.8) combination is
1002
+ what makes the rows meet without seams. Iosevka is not available here,
1003
+ so it falls back to the platform monospace. */
1004
+ pre.wordmark {
1005
+ margin: 0;
1006
+ font-family: 'Iosevka', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
1007
+ font-size: 8px;
1008
+ font-weight: 400;
1009
+ line-height: 10px;
1010
+ letter-spacing: 0;
1011
+ white-space: pre;
1012
+ color: var(--signal);
1013
+ transform: scaleY(0.8);
1014
+ transform-origin: left top;
1015
+ }
1016
+ main {
1017
+ display: flex;
1018
+ flex-direction: column;
1019
+ justify-content: center;
1020
+ max-width: 46ch;
1021
+ padding-bottom: 12vh;
1022
+ }
1023
+ h1 {
1024
+ margin: 0;
1025
+ font-size: 26px;
1026
+ font-weight: 300;
1027
+ line-height: 1.16;
1028
+ letter-spacing: -0.018em;
1029
+ }
1030
+ p {
1031
+ margin: 14px 0 0;
1032
+ font-size: 16px;
1033
+ line-height: 1.55;
1034
+ color: var(--quiet);
1035
+ }
1036
+ hr {
1037
+ width: 40px;
1038
+ margin: 28px 0 0;
1039
+ border: 0;
1040
+ border-top: 1px solid var(--rule);
1041
+ }
1042
+ </style>
1043
+ </head>
1044
+ <body>
1045
+ <header><pre class="wordmark" aria-hidden="true">${WORDMARK}</pre></header>
1046
+ <main>
1047
+ <h1>${title}</h1>
1048
+ <p>${body}</p>
1049
+ <hr>
1050
+ </main>
1051
+ </body>
1052
+ </html>
1053
+ `;
1054
+ }
1055
+ function callbackResponse(outcome) {
1056
+ return {
1057
+ status: OUTCOMES[outcome].status,
1058
+ headers: {
1059
+ "content-type": "text/html; charset=utf-8",
1060
+ "cache-control": "no-store"
1128
1061
  },
1129
- "hostnameWord": {
1130
- "type": "string",
1131
- "enum": [
1132
- "arch",
1133
- "bark",
1134
- "beam",
1135
- "bell",
1136
- "bird",
1137
- "blue",
1138
- "boat",
1139
- "bold",
1140
- "book",
1141
- "calm",
1142
- "cave",
1143
- "clay",
1144
- "coal",
1145
- "cool",
1146
- "cove",
1147
- "dawn",
1148
- "deer",
1149
- "dove",
1150
- "dune",
1151
- "dusk",
1152
- "echo",
1153
- "fern",
1154
- "fire",
1155
- "flax",
1156
- "flow",
1157
- "foam",
1158
- "fawn",
1159
- "frog",
1160
- "gate",
1161
- "glow",
1162
- "gold",
1163
- "gray",
1164
- "gulf",
1165
- "hail",
1166
- "halo",
1167
- "hare",
1168
- "hawk",
1169
- "hill",
1170
- "iris",
1171
- "jade",
1172
- "kite",
1173
- "lake",
1174
- "lamb",
1175
- "leaf",
1176
- "lime",
1177
- "lion",
1178
- "lobe",
1179
- "loft",
1180
- "loom",
1181
- "luma",
1182
- "lynx",
1183
- "mint",
1184
- "mist",
1185
- "moon",
1186
- "moss",
1187
- "navy",
1188
- "nest",
1189
- "nova",
1190
- "oak",
1191
- "ocean",
1192
- "onyx",
1193
- "opal",
1194
- "otter",
1195
- "palm",
1196
- "peak",
1197
- "pine",
1198
- "pond",
1199
- "pool",
1200
- "rain",
1201
- "reed",
1202
- "reef",
1203
- "rice",
1204
- "ring",
1205
- "rise",
1206
- "road",
1207
- "rock",
1208
- "rose",
1209
- "ruby",
1210
- "sage",
1211
- "sand",
1212
- "seal",
1213
- "seed",
1214
- "silk",
1215
- "sky",
1216
- "snow",
1217
- "soft",
1218
- "star",
1219
- "stem",
1220
- "stone",
1221
- "surf",
1222
- "swan",
1223
- "teal",
1224
- "tide",
1225
- "tree",
1226
- "vale",
1227
- "vine",
1228
- "wave",
1229
- "west",
1230
- "wind",
1231
- "wing",
1232
- "wolf",
1233
- "wood",
1234
- "wren",
1235
- "yarn",
1236
- "zest"
1237
- ]
1238
- }
1062
+ body: renderCallbackPage(outcome)
1063
+ };
1064
+ }
1065
+
1066
+ // src/auth.ts
1067
+ var exec = promisify(execFile);
1068
+ var LOOPBACK_REDIRECT = "http://localhost:17655/callback";
1069
+ function base64url(value) {
1070
+ return value.toString("base64url");
1071
+ }
1072
+ function withAbsoluteExpiry(tokens) {
1073
+ if (tokens.expires_at || !Number.isInteger(tokens.expires_in) || (tokens.expires_in ?? 0) <= 0)
1074
+ return tokens;
1075
+ return {
1076
+ ...tokens,
1077
+ expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 0)
1078
+ };
1079
+ }
1080
+ var AuthClient = class {
1081
+ constructor(credentials, fetcher, accountOrigin = "https://tonbo.dev", browserOpener = openBrowser) {
1082
+ this.credentials = credentials;
1083
+ this.fetcher = fetcher;
1084
+ this.accountOrigin = accountOrigin;
1085
+ this.browserOpener = browserOpener;
1086
+ }
1087
+ async accessToken() {
1088
+ const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
1089
+ if (injected) return injected;
1090
+ const tokens = await this.credentials.load();
1091
+ if (!tokens) throw new Error("Not logged in. Run `tonbo login`.");
1092
+ const legacyRelativeExpiry = !tokens.expires_at && tokens.expires_in;
1093
+ if (!legacyRelativeExpiry && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1e3) + 30))
1094
+ return tokens.access_token;
1095
+ if (!tokens.refresh_token) throw new Error("Tonbo login expired. Run `tonbo login` again.");
1096
+ const config = await this.config();
1097
+ const refreshed = withAbsoluteExpiry(
1098
+ await this.exchange(
1099
+ config.token_endpoint,
1100
+ new URLSearchParams({
1101
+ client_id: config.client_id,
1102
+ grant_type: "refresh_token",
1103
+ refresh_token: tokens.refresh_token
1104
+ })
1105
+ )
1106
+ );
1107
+ if (!refreshed.refresh_token) refreshed.refresh_token = tokens.refresh_token;
1108
+ await this.credentials.save(refreshed);
1109
+ return refreshed.access_token;
1110
+ }
1111
+ async login(progress = () => {
1112
+ }) {
1113
+ progress({ status: "started", step: "account-config" });
1114
+ const config = await this.config();
1115
+ progress({ status: "completed", step: "account-config" });
1116
+ if (config.redirect_uri !== LOOPBACK_REDIRECT)
1117
+ throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
1118
+ const verifier = base64url(randomBytes(48));
1119
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
1120
+ const state = base64url(randomBytes(24));
1121
+ const authorization = new URL(config.authorization_endpoint);
1122
+ authorization.search = new URLSearchParams({
1123
+ client_id: config.client_id,
1124
+ code_challenge: challenge,
1125
+ code_challenge_method: "S256",
1126
+ redirect_uri: config.redirect_uri,
1127
+ response_type: "code",
1128
+ scope: "openid profile email",
1129
+ state
1130
+ }).toString();
1131
+ const code = await this.listenForCode(
1132
+ state,
1133
+ () => this.browserOpener(authorization.toString()),
1134
+ progress
1135
+ );
1136
+ progress({ status: "started", step: "token-exchange" });
1137
+ const tokens = withAbsoluteExpiry(
1138
+ await this.exchange(
1139
+ config.token_endpoint,
1140
+ new URLSearchParams({
1141
+ client_id: config.client_id,
1142
+ code,
1143
+ code_verifier: verifier,
1144
+ grant_type: "authorization_code",
1145
+ redirect_uri: config.redirect_uri
1146
+ })
1147
+ )
1148
+ );
1149
+ progress({ status: "completed", step: "token-exchange" });
1150
+ progress({ status: "started", step: "credential-store" });
1151
+ await this.credentials.save(tokens);
1152
+ progress({ status: "completed", step: "credential-store" });
1153
+ return tokens;
1154
+ }
1155
+ config() {
1156
+ return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/config`);
1157
+ }
1158
+ exchange(endpoint, body) {
1159
+ return requestJson(this.fetcher, endpoint, {
1160
+ method: "POST",
1161
+ headers: { "content-type": "application/x-www-form-urlencoded" },
1162
+ body
1163
+ });
1164
+ }
1165
+ listenForCode(expectedState, ready, progress) {
1166
+ return new Promise((resolve, reject) => {
1167
+ let settled = false;
1168
+ const sockets = /* @__PURE__ */ new Set();
1169
+ let authorizationStarted = false;
1170
+ const startAuthorization = () => {
1171
+ if (authorizationStarted) return;
1172
+ authorizationStarted = true;
1173
+ progress({ status: "completed", step: "browser" });
1174
+ progress({ status: "started", step: "browser-authorization" });
1175
+ };
1176
+ const finish = (result, completedResponseSocket) => {
1177
+ if (settled) return;
1178
+ settled = true;
1179
+ clearTimeout(timeout);
1180
+ const complete = (closeError) => {
1181
+ if (closeError) reject(closeError);
1182
+ else if ("code" in result) {
1183
+ progress({ status: "completed", step: "callback-close" });
1184
+ resolve(result.code);
1185
+ } else reject(result.error);
1186
+ };
1187
+ if ("code" in result) progress({ status: "started", step: "callback-close" });
1188
+ if (!server.listening) {
1189
+ complete();
1190
+ return;
1191
+ }
1192
+ server.close(complete);
1193
+ for (const socket of sockets) {
1194
+ if (socket !== completedResponseSocket) socket.destroy();
1195
+ }
1196
+ server.closeIdleConnections();
1197
+ };
1198
+ const timeout = setTimeout(
1199
+ () => {
1200
+ finish({ error: new Error("Timed out waiting for browser login.") });
1201
+ },
1202
+ 5 * 60 * 1e3
1203
+ );
1204
+ const server = createServer((request, response) => {
1205
+ const url = new URL(request.url ?? "/", LOOPBACK_REDIRECT);
1206
+ const code = url.searchParams.get("code");
1207
+ const oauthError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
1208
+ if (oauthError) {
1209
+ const responseSocket2 = response.socket;
1210
+ respond(
1211
+ response,
1212
+ "denied",
1213
+ () => finish({ error: new Error(oauthError) }, responseSocket2)
1214
+ );
1215
+ return;
1216
+ }
1217
+ if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState || !code) {
1218
+ respond(response, "invalid");
1219
+ return;
1220
+ }
1221
+ startAuthorization();
1222
+ progress({ status: "completed", step: "browser-authorization" });
1223
+ const responseSocket = response.socket;
1224
+ respond(response, "complete", () => finish({ code }, responseSocket));
1225
+ });
1226
+ server.on("connection", (socket) => {
1227
+ sockets.add(socket);
1228
+ socket.once("close", () => sockets.delete(socket));
1229
+ });
1230
+ server.once("error", (error) => finish({ error }));
1231
+ progress({ status: "started", step: "callback-server" });
1232
+ server.listen(17655, "localhost", () => {
1233
+ progress({ status: "completed", step: "callback-server" });
1234
+ progress({ status: "started", step: "browser" });
1235
+ void ready().then(startAuthorization).catch(
1236
+ (error) => finish({
1237
+ error: error instanceof Error ? error : new Error("Could not open the login browser.")
1238
+ })
1239
+ );
1240
+ });
1241
+ });
1239
1242
  }
1240
1243
  };
1241
-
1242
- // src/contracts.ts
1243
- var ajv = new Ajv2020({ allErrors: true, useDefaults: true });
1244
- ajv.addKeyword({ keyword: "x-tonbo-profiles" });
1245
- ajv.addKeyword({ keyword: "x-tonbo-version" });
1246
- ajv.addKeyword({ keyword: "x-tonbo-public-hostname-apex" });
1247
- ajv.addKeyword({ keyword: "x-tonbo-public-hostname-template" });
1248
- ajv.addSchema(kubernetesProfilesSchema);
1249
- ajv.addSchema(piAgentSchema);
1250
- ajv.addSchema(agentNameContract);
1251
- ajv.addSchema(agentServiceSchema);
1252
- var validateDeclaration = ajv.compile(declarationSchema);
1253
- var validateDeploymentSpec = ajv.compile(deploymentSchema);
1254
- function validationMessage(label, errors) {
1255
- const detail = errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
1256
- return `${label} is invalid${detail ? `: ${detail}` : "."}`;
1244
+ function respond(response, outcome, finished) {
1245
+ const page = callbackResponse(outcome);
1246
+ if (finished) response.once("finish", finished);
1247
+ response.writeHead(page.status, { ...page.headers, connection: "close" });
1248
+ response.end(page.body);
1257
1249
  }
1258
- function parseDeclaration(value) {
1259
- const candidate = structuredClone(value);
1260
- if (!validateDeclaration(candidate)) {
1261
- throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
1262
- }
1263
- return candidate;
1250
+ async function openBrowser(url) {
1251
+ if (process.platform === "darwin") return void await exec("open", [url]);
1252
+ if (process.platform === "win32")
1253
+ return void await exec("rundll32", ["url.dll,FileProtocolHandler", url]);
1254
+ return void await exec("xdg-open", [url]);
1264
1255
  }
1265
- function assertManagedDeploymentSpec(value) {
1266
- if (!validateDeploymentSpec(value)) {
1267
- throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
1256
+
1257
+ // ../../packages/agent-source-bundler/dist/src/generated/contracts.js
1258
+ var inferenceDefault = { "model": "claude-sonnet-5" };
1259
+ var sourceBundleContract = { "version": 1, "format": "tar-v1", "key_prefix": "agent-source-bundles", "content_type": "application/vnd.tonbo.source+tar", "max_bytes": 67108864 };
1260
+ var piSessionContract = { "version": 1, "adapter": "pi-jsonl-v3", "format_version": 3, "durable_completion_timeout_seconds": 30, "turn_claim_seconds": 30, "turn_claim_renew_seconds": 10, "session_directory": "/sessions", "path_template": "/sessions/{session_id}.jsonl", "preflight_command": ["/usr/local/bin/artifacts", "runtime", "session-preflight"] };
1261
+
1262
+ // ../../packages/agent-source-bundler/dist/src/inference-default.js
1263
+ var DEFAULT_INFERENCE_MODEL = inferenceDefault.model;
1264
+
1265
+ // ../../packages/agent-source-bundler/dist/src/deployment-origin.js
1266
+ function deploymentOriginLabel(actor) {
1267
+ return actor === "onboarding" ? "Setup guide" : "tonbo deploy";
1268
+ }
1269
+ function githubRepositoryUrl(remote) {
1270
+ if (typeof remote !== "string" || !remote || /\s/.test(remote) || [...remote].some((character) => character.charCodeAt(0) < 32))
1271
+ return null;
1272
+ let path6;
1273
+ const ssh = /^git@github\.com:([^?#]+)$/.exec(remote);
1274
+ if (ssh)
1275
+ path6 = ssh[1];
1276
+ else {
1277
+ try {
1278
+ const url = new URL(remote);
1279
+ if (url.hostname !== "github.com" || url.port || !["https:", "ssh:"].includes(url.protocol))
1280
+ return null;
1281
+ path6 = url.pathname.slice(1);
1282
+ } catch {
1283
+ return null;
1284
+ }
1268
1285
  }
1286
+ path6 = path6.replace(/\/$/, "").replace(/\.git$/, "");
1287
+ const parts = path6.split("/");
1288
+ if (parts.length !== 2 || !/^[a-z\d](?:[a-z\d-]{0,38})$/i.test(parts[0]) || !/^[a-z\d._-]{1,100}$/i.test(parts[1]) || [".", ".."].includes(parts[1]))
1289
+ return null;
1290
+ return `https://github.com/${parts[0]}/${parts[1]}`;
1269
1291
  }
1270
1292
 
1271
- // src/declaration.ts
1272
- import { DEFAULT_INFERENCE_MODEL } from "@tonbo/agent-source-bundler/inference-default";
1293
+ // src/commands.ts
1294
+ import path4 from "node:path";
1295
+
1296
+ // src/build.ts
1297
+ import { spawn } from "node:child_process";
1298
+ async function runBuildCommand(root, command) {
1299
+ if (command.length === 0) throw new Error("Build command must not be empty.");
1300
+ await new Promise((resolve, reject) => {
1301
+ const child = spawn(command[0], command.slice(1), {
1302
+ cwd: root,
1303
+ env: process.env,
1304
+ // Command output is progress, not the CLI result. Keep stdout available
1305
+ // for the single JSON document emitted by `tonbo --json deploy`.
1306
+ stdio: ["inherit", process.stderr, process.stderr]
1307
+ });
1308
+ child.once("error", reject);
1309
+ child.once("exit", (code, signal) => {
1310
+ if (code === 0) resolve();
1311
+ else
1312
+ reject(
1313
+ new Error(
1314
+ `Build command failed${signal ? ` with signal ${signal}` : ` with status ${code}`}.`
1315
+ )
1316
+ );
1317
+ });
1318
+ });
1319
+ }
1273
1320
 
1274
- // ../../packages/agent-source-bundler/src/native-pi.ts
1321
+ // ../../packages/agent-source-bundler/dist/src/native-pi.js
1275
1322
  import { stringify } from "smol-toml";
1276
1323
  function nativePiDeclaration(model) {
1277
1324
  return {
@@ -1382,7 +1429,7 @@ function buildDeploymentSpec(declaration, source) {
1382
1429
  return spec;
1383
1430
  }
1384
1431
 
1385
- // ../../packages/agent-source-bundler/src/index.ts
1432
+ // ../../packages/agent-source-bundler/dist/src/index.js
1386
1433
  import { createHash as createHash2 } from "node:crypto";
1387
1434
  import { lstat as lstat3, readFile as readFile3, readdir } from "node:fs/promises";
1388
1435
  import path3 from "node:path";
@@ -1405,7 +1452,8 @@ var DEFAULT_IGNORES = [
1405
1452
  var EXACT_NPM_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
1406
1453
  var GIT_COMMIT = /^[0-9a-f]{40}$/i;
1407
1454
  function packageSource(value, index) {
1408
- if (typeof value === "string") return value;
1455
+ if (typeof value === "string")
1456
+ return value;
1409
1457
  if (value && typeof value === "object" && "source" in value && typeof value.source === "string")
1410
1458
  return value.source;
1411
1459
  throw new Error(`.pi/settings.json packages[${index}] must be a source string or object.`);
@@ -1415,9 +1463,7 @@ async function validatePackageSource(root, source) {
1415
1463
  const specifier = source.slice(4);
1416
1464
  const separator = specifier.lastIndexOf("@");
1417
1465
  if (separator <= 0 || !EXACT_NPM_VERSION.test(specifier.slice(separator + 1))) {
1418
- throw new Error(
1419
- `PI package ${source} must pin an exact npm version, for example npm:my-agent@1.2.3.`
1420
- );
1466
+ throw new Error(`PI package ${source} must pin an exact npm version, for example npm:my-agent@1.2.3.`);
1421
1467
  }
1422
1468
  return;
1423
1469
  }
@@ -1449,9 +1495,7 @@ async function validatePackageSource(root, source) {
1449
1495
  }
1450
1496
  return;
1451
1497
  }
1452
- throw new Error(
1453
- `PI package ${source} must use an exact npm version, a full Git commit, or a agent-local path.`
1454
- );
1498
+ throw new Error(`PI package ${source} must use an exact npm version, a full Git commit, or a agent-local path.`);
1455
1499
  }
1456
1500
  async function validatePiPackages(root) {
1457
1501
  const filename = path3.join(root, ".pi", "settings.json");
@@ -1459,23 +1503,26 @@ async function validatePiPackages(root) {
1459
1503
  try {
1460
1504
  settings = JSON.parse(await readFile3(filename, "utf8"));
1461
1505
  } catch (error) {
1462
- if (error.code === "ENOENT") return;
1506
+ if (error.code === "ENOENT")
1507
+ return;
1463
1508
  throw new Error(`Could not read ${filename} as JSON.`, { cause: error });
1464
1509
  }
1465
- if (!settings || typeof settings !== "object" || !("packages" in settings)) return;
1510
+ if (!settings || typeof settings !== "object" || !("packages" in settings))
1511
+ return;
1466
1512
  const packages = settings.packages;
1467
- if (!Array.isArray(packages)) throw new Error(`${filename} packages must be an array.`);
1468
- await Promise.all(
1469
- packages.map((value, index) => validatePackageSource(root, packageSource(value, index)))
1470
- );
1513
+ if (!Array.isArray(packages))
1514
+ throw new Error(`${filename} packages must be an array.`);
1515
+ await Promise.all(packages.map((value, index) => validatePackageSource(root, packageSource(value, index))));
1471
1516
  }
1472
1517
  async function findDeclarationRoot(start) {
1473
1518
  let candidate = path3.resolve(start);
1474
1519
  for (; ; ) {
1475
1520
  try {
1476
- if ((await lstat3(path3.join(candidate, ".tonbo"))).isFile()) return candidate;
1521
+ if ((await lstat3(path3.join(candidate, ".tonbo"))).isFile())
1522
+ return candidate;
1477
1523
  } catch (error) {
1478
- if (error.code !== "ENOENT") throw error;
1524
+ if (error.code !== "ENOENT")
1525
+ throw error;
1479
1526
  }
1480
1527
  const parent = path3.dirname(candidate);
1481
1528
  if (parent === candidate) {
@@ -1489,7 +1536,8 @@ async function sourceIgnore(root) {
1489
1536
  try {
1490
1537
  matcher.add(await readFile3(path3.join(root, ".tonboignore"), "utf8"));
1491
1538
  } catch (error) {
1492
- if (error.code !== "ENOENT") throw error;
1539
+ if (error.code !== "ENOENT")
1540
+ throw error;
1493
1541
  }
1494
1542
  return matcher;
1495
1543
  }
@@ -1502,11 +1550,10 @@ async function collectFiles(root) {
1502
1550
  for (const entry of entries) {
1503
1551
  const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
1504
1552
  const ignored = matcher.ignores(relative + (entry.isDirectory() ? "/" : ""));
1505
- if (ignored) continue;
1553
+ if (ignored)
1554
+ continue;
1506
1555
  if (relative === SESSION_SOURCE_DIRECTORY || relative.startsWith(`${SESSION_SOURCE_DIRECTORY}/`)) {
1507
- throw new Error(
1508
- `Source path ${SESSION_SOURCE_DIRECTORY}/ is reserved for durable PI history. Rename it or exclude it with .tonboignore.`
1509
- );
1556
+ throw new Error(`Source path ${SESSION_SOURCE_DIRECTORY}/ is reserved for durable PI history. Rename it or exclude it with .tonboignore.`);
1510
1557
  }
1511
1558
  const absolute = path3.join(directory, entry.name);
1512
1559
  if (entry.isDirectory()) {
@@ -1515,9 +1562,7 @@ async function collectFiles(root) {
1515
1562
  }
1516
1563
  const metadata = await lstat3(absolute);
1517
1564
  if (!metadata.isFile()) {
1518
- throw new Error(
1519
- `Source path ${relative} is not a regular file. V1 does not follow symlinks or special files.`
1520
- );
1565
+ throw new Error(`Source path ${relative} is not a regular file. V1 does not follow symlinks or special files.`);
1521
1566
  }
1522
1567
  files.push({
1523
1568
  absolute,
@@ -1535,19 +1580,15 @@ async function collectFiles(root) {
1535
1580
  }
1536
1581
  function addEntry(pack, file, contents) {
1537
1582
  return new Promise((resolve, reject) => {
1538
- pack.entry(
1539
- {
1540
- gid: 0,
1541
- mode: file.mode,
1542
- mtime: /* @__PURE__ */ new Date(0),
1543
- name: file.relative,
1544
- size: contents.length,
1545
- type: "file",
1546
- uid: 0
1547
- },
1548
- contents,
1549
- (error) => error ? reject(error) : resolve()
1550
- );
1583
+ pack.entry({
1584
+ gid: 0,
1585
+ mode: file.mode,
1586
+ mtime: /* @__PURE__ */ new Date(0),
1587
+ name: file.relative,
1588
+ size: contents.length,
1589
+ type: "file",
1590
+ uid: 0
1591
+ }, contents, (error) => error ? reject(error) : resolve());
1551
1592
  });
1552
1593
  }
1553
1594
  async function buildSourceBundle(root) {
@@ -1564,9 +1605,7 @@ async function buildSourceBundle(root) {
1564
1605
  pack.on("data", (chunk) => {
1565
1606
  size += chunk.length;
1566
1607
  if (size > MAX_BUNDLE_BYTES) {
1567
- pack.destroy(
1568
- new Error(`Source bundle exceeds ${MAX_BUNDLE_BYTES} bytes after ignore rules.`)
1569
- );
1608
+ pack.destroy(new Error(`Source bundle exceeds ${MAX_BUNDLE_BYTES} bytes after ignore rules.`));
1570
1609
  return;
1571
1610
  }
1572
1611
  chunks.push(chunk);
@@ -1727,7 +1766,7 @@ async function initCommand(deps, options) {
1727
1766
  entry = (await deps.prompt("PI SDK entry file [dist/agent.mjs]: ")).trim();
1728
1767
  }
1729
1768
  entry ||= "dist/agent.mjs";
1730
- let model = options.model?.trim() || existingDeclaration?.inference.model || DEFAULT_INFERENCE_MODEL2;
1769
+ let model = options.model?.trim() || existingDeclaration?.inference.model || DEFAULT_INFERENCE_MODEL;
1731
1770
  let buildCommand = driver === "command" ? options.buildCommand ?? existingDeclaration?.build?.command ?? ["npm", "run", "build"] : void 0;
1732
1771
  let agentPlan = existingDeclaration?.agent ? {
1733
1772
  kind: "existing",
@@ -2666,6 +2705,10 @@ function createProgram(dependencies = createDependencies) {
2666
2705
  async (options) => initCommand(dependencies(jsonOutput(program)), options)
2667
2706
  );
2668
2707
  program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(jsonOutput(program))));
2708
+ program.command("region").description("discover regions and Machine allocation availability").command("list").action(async () => {
2709
+ const deps = dependencies(jsonOutput(program));
2710
+ deps.output(await deps.api.listRegions(await deps.auth.accessToken()));
2711
+ });
2669
2712
  const machine = program.command("machine").description("manage independent account-owned Machines");
2670
2713
  machine.command("list").option("--account <account>", "account UUID").action(async (options) => {
2671
2714
  const deps = dependencies(jsonOutput(program));
@@ -2676,7 +2719,7 @@ function createProgram(dependencies = createDependencies) {
2676
2719
  )
2677
2720
  );
2678
2721
  });
2679
- machine.command("allocate").requiredOption("--account <account>", "owning account UUID").requiredOption("--region <region>", "compute region (currently us-east-1)").action(async (options) => {
2722
+ machine.command("allocate").requiredOption("--account <account>", "owning account UUID").requiredOption("--region <region>", "compute region (see tonbo region list)").action(async (options) => {
2680
2723
  const deps = dependencies(jsonOutput(program));
2681
2724
  deps.output(
2682
2725
  await deps.api.machineRequest(await deps.auth.accessToken(), "", "POST", {