@tonbo/cli 0.3.1 → 0.3.3

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