@tonbo/cli 0.3.1 → 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 +1224 -1162
  3. package/package.json +1 -1
package/dist/bin/tonbo.js CHANGED
@@ -32,1232 +32,1290 @@ 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/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";
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
+ }
349
436
  }
350
437
  };
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 });
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
+ }
357
477
  }
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.`);
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));
360
496
  }
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.`);
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));
366
510
  }
367
- return packages.length;
511
+ return candidate;
368
512
  }
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
- };
513
+ function assertManagedDeploymentSpec(value) {
514
+ if (!validateDeploymentSpec(value)) {
515
+ throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
388
516
  }
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
517
  }
436
518
 
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."
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;
468
525
  }
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;
526
+ async listRegions(oauthToken) {
527
+ return parseRegionDiscovery(
528
+ await requestJson(this.fetcher, `${this.accountOrigin}/api/cli/regions`, {
529
+ headers: { authorization: `Bearer ${oauthToken}` }
530
+ })
531
+ );
486
532
  }
487
- @media (prefers-color-scheme: dark) {
488
- :root {
489
- --paper: #1d1b17;
490
- --ink: #f3f2ee;
491
- --rule: #3a362e;
492
- --quiet: #a49e8f;
493
- --signal: #f08b4b;
494
- }
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
+ });
495
542
  }
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;
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);
507
551
  }
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;
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);
524
560
  }
525
- main {
526
- display: flex;
527
- flex-direction: column;
528
- justify-content: center;
529
- max-width: 46ch;
530
- 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);
531
574
  }
532
- h1 {
533
- margin: 0;
534
- font-size: 26px;
535
- font-weight: 300;
536
- line-height: 1.16;
537
- 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);
538
592
  }
539
- p {
540
- margin: 14px 0 0;
541
- font-size: 16px;
542
- line-height: 1.55;
543
- 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);
544
606
  }
545
- hr {
546
- width: 40px;
547
- margin: 28px 0 0;
548
- border: 0;
549
- 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);
550
616
  }
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;
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 };
595
678
  }
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
- )
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
+ }
615
708
  );
616
- if (!refreshed.refresh_token) refreshed.refresh_token = tokens.refresh_token;
617
- await this.credentials.save(refreshed);
618
- return refreshed.access_token;
619
709
  }
620
- 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()
621
727
  }) {
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
- )
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
657
759
  );
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
760
  }
664
- config() {
665
- 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);
666
767
  }
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
- });
768
+ setAgentSecret(agentId, name, value, token) {
769
+ return this.management(
770
+ "PUT",
771
+ `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
772
+ token,
773
+ { value }
774
+ );
673
775
  }
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;
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()
729
791
  }
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
- });
792
+ },
793
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
750
794
  });
751
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
+ }
752
811
  };
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
812
 
771
- // ../../packages/agent-source-bundler/dist/src/inference-default.js
772
- var DEFAULT_INFERENCE_MODEL = inferenceDefault.model;
813
+ // src/agent-source.ts
814
+ import { lstat, readFile } from "node:fs/promises";
815
+ import path from "node:path";
773
816
 
774
- // ../../packages/agent-source-bundler/dist/src/deployment-origin.js
775
- function deploymentOriginLabel(actor) {
776
- 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;
777
859
  }
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 {
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) {
786
916
  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;
917
+ return await lstat(path.join(this.root, filename));
918
+ } catch (error) {
919
+ if (error.code === "ENOENT") return null;
920
+ throw error;
793
921
  }
794
922
  }
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]}`;
923
+ };
924
+ function inspectLocalAgentSource(root) {
925
+ return inspectAgentSource(new LocalAgentSource(root));
800
926
  }
801
927
 
802
- // src/commands.ts
803
- 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";
804
933
 
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
- }
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."
900
959
  }
901
960
  };
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
- }
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;
934
977
  }
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"
978
+ @media (prefers-color-scheme: dark) {
979
+ :root {
980
+ --paper: #1d1b17;
981
+ --ink: #f3f2ee;
982
+ --rule: #3a362e;
983
+ --quiet: #a49e8f;
984
+ --signal: #f08b4b;
1012
985
  }
1013
986
  }
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
- }
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;
1074
998
  }
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
- }
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"
1120
1061
  },
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
- }
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
+ });
1231
1242
  }
1232
1243
  };
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);
1249
+ }
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]);
1255
+ }
1233
1256
 
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}` : "."}`;
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";
1249
1268
  }
1250
- function parseDeclaration(value) {
1251
- const candidate = structuredClone(value);
1252
- if (!validateDeclaration(candidate)) {
1253
- throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
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
+ }
1254
1285
  }
1255
- return candidate;
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]}`;
1256
1291
  }
1257
- function assertManagedDeploymentSpec(value) {
1258
- if (!validateDeploymentSpec(value)) {
1259
- throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
1260
- }
1292
+
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
+ });
1261
1319
  }
1262
1320
 
1263
1321
  // ../../packages/agent-source-bundler/dist/src/native-pi.js
@@ -2647,6 +2705,10 @@ function createProgram(dependencies = createDependencies) {
2647
2705
  async (options) => initCommand(dependencies(jsonOutput(program)), options)
2648
2706
  );
2649
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
+ });
2650
2712
  const machine = program.command("machine").description("manage independent account-owned Machines");
2651
2713
  machine.command("list").option("--account <account>", "account UUID").action(async (options) => {
2652
2714
  const deps = dependencies(jsonOutput(program));
@@ -2657,7 +2719,7 @@ function createProgram(dependencies = createDependencies) {
2657
2719
  )
2658
2720
  );
2659
2721
  });
2660
- 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) => {
2661
2723
  const deps = dependencies(jsonOutput(program));
2662
2724
  deps.output(
2663
2725
  await deps.api.machineRequest(await deps.auth.accessToken(), "", "POST", {