@tonbo/cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +60 -189
  2. package/dist/bin/tonbo.js +925 -2618
  3. package/package.json +7 -13
package/dist/bin/tonbo.js CHANGED
@@ -1,287 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/app.ts
4
- import { randomUUID as randomUUID5 } from "node:crypto";
5
- import { Command } from "commander";
4
+ import { Command, InvalidArgumentError, Option } from "commander";
6
5
  import { readFileSync } from "node:fs";
7
6
 
8
- // src/api.ts
9
- import { randomUUID } from "node:crypto";
10
-
11
7
  // src/http.ts
12
8
  var HttpError = class extends Error {
13
- constructor(message, status, body) {
9
+ constructor(message, status, body, requestId) {
14
10
  super(message);
15
11
  this.status = status;
16
12
  this.body = body;
13
+ this.requestId = requestId;
14
+ }
15
+ /** The problem `code` of a Management API refusal, such as `unauthenticated`. */
16
+ get code() {
17
+ const record = this.body && typeof this.body === "object" ? this.body : {};
18
+ const code = record.code;
19
+ return typeof code === "string" ? code : null;
17
20
  }
18
21
  };
19
- async function requestJsonResponse(fetcher, url, init = {}) {
22
+ async function requestJson(fetcher, url, init = {}) {
20
23
  const response = await fetcher(url, init);
21
24
  const body = await response.json().catch(() => null);
22
- if (!response.ok) {
23
- const record = body && typeof body === "object" ? body : {};
24
- const message = [record.detail, record.message, record.error, record.title].find(
25
- (value) => typeof value === "string"
26
- );
27
- throw new HttpError(
28
- typeof message === "string" ? message : `Request failed with HTTP ${response.status}.`,
29
- response.status,
30
- body
31
- );
32
- }
33
- return { body, headers: response.headers, status: response.status };
34
- }
35
- async function requestJson(fetcher, url, init = {}) {
36
- return (await requestJsonResponse(fetcher, url, init)).body;
25
+ if (response.ok) return body;
26
+ const record = body && typeof body === "object" ? body : {};
27
+ const reason = [record.detail, record.title, record.error].find(
28
+ (value) => typeof value === "string" && value.length > 0
29
+ );
30
+ const requestId = response.headers.get("x-request-id") ?? (typeof record.request_id === "string" ? record.request_id : null);
31
+ const issues = Array.isArray(record.errors) ? record.errors.filter((issue) => typeof issue?.field === "string" && typeof issue.code === "string").map((issue) => `${issue.field}: ${issue.code}`) : [];
32
+ const message = [
33
+ reason ?? `Request failed with HTTP ${response.status}.`,
34
+ ...issues.length ? [`(${issues.join(", ")})`] : []
35
+ ].join(" ");
36
+ throw new HttpError(
37
+ requestId ? `${message} (request ${requestId})` : message,
38
+ response.status,
39
+ body,
40
+ requestId
41
+ );
37
42
  }
38
43
 
39
- // src/contracts.ts
40
- import { Ajv2020 } from "ajv/dist/2020.js";
41
-
42
44
  // src/generated/contracts.ts
43
- var piAgentSchema = {
44
- "$schema": "https://json-schema.org/draft/2020-12/schema",
45
- "$id": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json",
46
- "title": "PI Agent v1",
47
- "type": "object",
48
- "additionalProperties": false,
49
- "required": [
50
- "runtime",
51
- "driver"
52
- ],
53
- "properties": {
54
- "runtime": {
55
- "const": "pi"
56
- },
57
- "secrets": {
58
- "type": "array",
59
- "maxItems": 32,
60
- "uniqueItems": true,
61
- "items": {
62
- "type": "string",
63
- "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
64
- }
65
- },
66
- "driver": {
67
- "oneOf": [
68
- {
69
- "type": "object",
70
- "additionalProperties": false,
71
- "required": [
72
- "kind"
73
- ],
74
- "properties": {
75
- "kind": {
76
- "const": "native"
77
- }
78
- }
79
- },
80
- {
81
- "type": "object",
82
- "additionalProperties": false,
83
- "required": [
84
- "kind",
85
- "protocol",
86
- "command"
87
- ],
88
- "properties": {
89
- "kind": {
90
- "const": "command"
91
- },
92
- "protocol": {
93
- "const": "pi-rpc-v1"
94
- },
95
- "command": {
96
- "type": "array",
97
- "minItems": 1,
98
- "maxItems": 64,
99
- "items": {
100
- "type": "string",
101
- "minLength": 1,
102
- "maxLength": 1024
103
- }
104
- }
105
- }
106
- }
107
- ]
108
- }
109
- }
110
- };
111
- var agentServiceSchema = {
112
- "$schema": "https://json-schema.org/draft/2020-12/schema",
113
- "$id": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json",
114
- "title": "Tonbo Agent application service v1",
115
- "type": "object",
116
- "additionalProperties": false,
117
- "required": [
118
- "command"
119
- ],
120
- "properties": {
121
- "command": {
122
- "type": "array",
123
- "minItems": 1,
124
- "maxItems": 64,
125
- "items": {
126
- "type": "string",
127
- "minLength": 1,
128
- "maxLength": 4096
129
- }
130
- },
131
- "secrets": {
132
- "type": "array",
133
- "maxItems": 32,
134
- "uniqueItems": true,
135
- "items": {
136
- "type": "string",
137
- "pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
138
- }
139
- },
140
- "kubernetes": {
141
- "$ref": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json#/$defs/request"
142
- }
143
- }
144
- };
145
- var kubernetesProfilesSchema = {
146
- "$schema": "https://json-schema.org/draft/2020-12/schema",
147
- "$id": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json",
148
- "title": "Tonbo managed Kubernetes profiles v1",
149
- "$defs": {
150
- "request": {
151
- "type": "object",
152
- "additionalProperties": false,
153
- "required": [
154
- "profile"
155
- ],
156
- "properties": {
157
- "profile": false
158
- }
159
- }
160
- },
161
- "x-tonbo-profiles": {}
162
- };
163
- var declarationSchema = {
164
- "$schema": "https://json-schema.org/draft/2020-12/schema",
165
- "$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-v2.schema.json",
166
- "title": "Tonbo Agent declaration v2",
167
- "type": "object",
168
- "additionalProperties": false,
169
- "required": [
170
- "version",
171
- "harness"
172
- ],
173
- "properties": {
174
- "version": {
175
- "const": 2
176
- },
177
- "inference": {
178
- "type": "object",
179
- "additionalProperties": false,
180
- "default": {
181
- "model": "claude-sonnet-4-5"
182
- },
183
- "required": [
184
- "model"
185
- ],
186
- "properties": {
187
- "model": {
188
- "type": "string",
189
- "minLength": 1,
190
- "maxLength": 160
191
- }
192
- }
193
- },
194
- "build": {
195
- "type": "object",
196
- "additionalProperties": false,
197
- "required": [
198
- "command"
199
- ],
200
- "properties": {
201
- "command": {
202
- "type": "array",
203
- "minItems": 1,
204
- "maxItems": 64,
205
- "items": {
206
- "type": "string",
207
- "minLength": 1,
208
- "maxLength": 1024
209
- }
210
- }
211
- }
212
- },
213
- "service": {
214
- "$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
215
- },
216
- "agent": {
217
- "$ref": "https://contracts.tonbo.dev/agents/agent-name-v1.json#/$defs/publicHostname"
218
- },
219
- "harness": {
220
- "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
221
- }
222
- }
223
- };
224
- var deploymentSchema = {
225
- "$schema": "https://json-schema.org/draft/2020-12/schema",
226
- "$id": "https://contracts.tonbo.dev/agents/managed-deployment-v1.schema.json",
227
- "title": "Managed Agent Deployment v1",
228
- "type": "object",
229
- "additionalProperties": false,
230
- "required": [
231
- "version",
232
- "agent",
233
- "source",
234
- "inference"
235
- ],
236
- "properties": {
237
- "version": {
238
- "const": 1
239
- },
240
- "agent": {
241
- "$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
242
- },
243
- "source": {
244
- "type": "object",
245
- "additionalProperties": false,
246
- "required": [
247
- "format",
248
- "sha256",
249
- "size_bytes"
250
- ],
251
- "properties": {
252
- "format": {
253
- "const": "tar-v1"
254
- },
255
- "sha256": {
256
- "type": "string",
257
- "pattern": "^[0-9a-f]{64}$"
258
- },
259
- "size_bytes": {
260
- "type": "integer",
261
- "minimum": 1,
262
- "maximum": 67108864
263
- }
264
- }
265
- },
266
- "inference": {
267
- "type": "object",
268
- "additionalProperties": false,
269
- "required": [
270
- "model"
271
- ],
272
- "properties": {
273
- "model": {
274
- "type": "string",
275
- "minLength": 1,
276
- "maxLength": 160
277
- }
278
- }
279
- },
280
- "service": {
281
- "$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
282
- }
283
- }
284
- };
285
45
  var agentNameContract = {
286
46
  "$schema": "https://json-schema.org/draft/2020-12/schema",
287
47
  "$id": "https://contracts.tonbo.dev/agents/agent-name-v1.json",
@@ -439,568 +199,298 @@ var agentNameContract = {
439
199
  }
440
200
  }
441
201
  };
442
- var regionDiscoverySchema = {
443
- "$schema": "https://json-schema.org/draft/2020-12/schema",
444
- "$id": "https://contracts.tonbo.dev/regions/discovery-v1.schema.json",
445
- "title": "Regional allocation discovery v1",
446
- "type": "object",
447
- "additionalProperties": false,
448
- "required": [
449
- "version",
450
- "regions"
451
- ],
452
- "properties": {
453
- "version": {
454
- "const": 1
455
- },
456
- "regions": {
457
- "type": "array",
458
- "items": {
459
- "type": "object",
460
- "additionalProperties": false,
461
- "required": [
462
- "id",
463
- "display_name",
464
- "allocation_enabled"
465
- ],
466
- "properties": {
467
- "id": {
468
- "type": "string",
469
- "pattern": "^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$"
470
- },
471
- "display_name": {
472
- "type": "string",
473
- "minLength": 1
474
- },
475
- "allocation_enabled": {
476
- "type": "boolean"
477
- }
478
- }
479
- }
480
- }
202
+ var managementApiBasePath = "/v1";
203
+ var managementOperations = {
204
+ "getAccount": {
205
+ "method": "GET",
206
+ "path": "/account",
207
+ "pathParams": [],
208
+ "query": [],
209
+ "idempotencyKey": false,
210
+ "cli": "tonbo account show"
211
+ },
212
+ "listRegions": {
213
+ "method": "GET",
214
+ "path": "/regions",
215
+ "pathParams": [],
216
+ "query": [],
217
+ "idempotencyKey": false,
218
+ "cli": "tonbo region list"
219
+ },
220
+ "listMembers": {
221
+ "method": "GET",
222
+ "path": "/members",
223
+ "pathParams": [],
224
+ "query": [
225
+ "cursor",
226
+ "limit"
227
+ ],
228
+ "idempotencyKey": false,
229
+ "cli": "tonbo member list"
230
+ },
231
+ "updateMember": {
232
+ "method": "PATCH",
233
+ "path": "/members/{subject}",
234
+ "pathParams": [
235
+ "subject"
236
+ ],
237
+ "query": [],
238
+ "idempotencyKey": false,
239
+ "cli": "tonbo member update"
240
+ },
241
+ "createAgent": {
242
+ "method": "POST",
243
+ "path": "/agents",
244
+ "pathParams": [],
245
+ "query": [],
246
+ "idempotencyKey": true,
247
+ "cli": "tonbo agent create"
248
+ },
249
+ "listApiKeys": {
250
+ "method": "GET",
251
+ "path": "/api-keys",
252
+ "pathParams": [],
253
+ "query": [
254
+ "cursor",
255
+ "limit"
256
+ ],
257
+ "idempotencyKey": false,
258
+ "cli": "tonbo api-key list"
259
+ },
260
+ "createApiKey": {
261
+ "method": "POST",
262
+ "path": "/api-keys",
263
+ "pathParams": [],
264
+ "query": [],
265
+ "idempotencyKey": false,
266
+ "cli": "tonbo api-key create"
267
+ },
268
+ "getApiKey": {
269
+ "method": "GET",
270
+ "path": "/api-keys/{key_id}",
271
+ "pathParams": [
272
+ "key_id"
273
+ ],
274
+ "query": [],
275
+ "idempotencyKey": false,
276
+ "cli": "tonbo api-key show"
277
+ },
278
+ "revokeApiKey": {
279
+ "method": "DELETE",
280
+ "path": "/api-keys/{key_id}",
281
+ "pathParams": [
282
+ "key_id"
283
+ ],
284
+ "query": [],
285
+ "idempotencyKey": false,
286
+ "cli": "tonbo api-key revoke"
287
+ },
288
+ "listBuckets": {
289
+ "method": "GET",
290
+ "path": "/buckets",
291
+ "pathParams": [],
292
+ "query": [
293
+ "cursor",
294
+ "limit"
295
+ ],
296
+ "idempotencyKey": false,
297
+ "cli": "tonbo bucket list"
298
+ },
299
+ "createBucket": {
300
+ "method": "POST",
301
+ "path": "/buckets",
302
+ "pathParams": [],
303
+ "query": [],
304
+ "idempotencyKey": true,
305
+ "cli": "tonbo bucket create"
306
+ },
307
+ "getBucket": {
308
+ "method": "GET",
309
+ "path": "/buckets/{name}",
310
+ "pathParams": [
311
+ "name"
312
+ ],
313
+ "query": [],
314
+ "idempotencyKey": false,
315
+ "cli": "tonbo bucket show"
316
+ },
317
+ "updateBucket": {
318
+ "method": "PATCH",
319
+ "path": "/buckets/{name}",
320
+ "pathParams": [
321
+ "name"
322
+ ],
323
+ "query": [],
324
+ "idempotencyKey": false,
325
+ "cli": "tonbo bucket update"
326
+ },
327
+ "deleteBucket": {
328
+ "method": "DELETE",
329
+ "path": "/buckets/{name}",
330
+ "pathParams": [
331
+ "name"
332
+ ],
333
+ "query": [],
334
+ "idempotencyKey": false,
335
+ "cli": "tonbo bucket delete"
336
+ },
337
+ "getUsage": {
338
+ "method": "GET",
339
+ "path": "/usage",
340
+ "pathParams": [],
341
+ "query": [
342
+ "period"
343
+ ],
344
+ "idempotencyKey": false,
345
+ "cli": "tonbo usage show"
481
346
  }
482
347
  };
483
-
484
- // src/contracts.ts
485
- var ajv = new Ajv2020({ allErrors: true, useDefaults: true });
486
- ajv.addKeyword({ keyword: "x-tonbo-profiles" });
487
- ajv.addKeyword({ keyword: "x-tonbo-version" });
488
- ajv.addKeyword({ keyword: "x-tonbo-public-hostname-apex" });
489
- ajv.addKeyword({ keyword: "x-tonbo-public-hostname-template" });
490
- ajv.addSchema(kubernetesProfilesSchema);
491
- ajv.addSchema(piAgentSchema);
492
- ajv.addSchema(agentNameContract);
493
- ajv.addSchema(agentServiceSchema);
494
- var validateDeclaration = ajv.compile(declarationSchema);
495
- var validateDeploymentSpec = ajv.compile(deploymentSchema);
496
- var validateRegionDiscovery = ajv.compile(regionDiscoverySchema);
497
- function parseRegionDiscovery(value) {
498
- if (!validateRegionDiscovery(value)) {
499
- throw new Error(validationMessage("Region discovery", validateRegionDiscovery.errors));
500
- }
501
- const result = value;
502
- if (new Set(result.regions.map((region) => region.id)).size !== result.regions.length)
503
- throw new Error("Region discovery contains duplicate region identities.");
504
- return result;
505
- }
506
- function validationMessage(label, errors) {
507
- const detail = errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
508
- return `${label} is invalid${detail ? `: ${detail}` : "."}`;
509
- }
510
- function parseDeclaration(value) {
511
- const candidate = structuredClone(value);
512
- if (!validateDeclaration(candidate)) {
513
- throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
514
- }
515
- return candidate;
516
- }
517
- function assertManagedDeploymentSpec(value) {
518
- if (!validateDeploymentSpec(value)) {
519
- throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
520
- }
521
- }
348
+ var accountRoles = [
349
+ "owner",
350
+ "admin",
351
+ "member"
352
+ ];
353
+ var apiKeyExpirations = [
354
+ "15_days",
355
+ "1_month",
356
+ "6_months",
357
+ "1_year",
358
+ "never"
359
+ ];
522
360
 
523
361
  // src/api.ts
524
- function turnSubmission(response) {
525
- if (response.status === 202) return { state: "pending" };
526
- const body = response.body ?? {};
527
- if (!body.data || typeof body.data.assistant_text !== "string")
528
- throw new Error("Tonbo answered the Turn without its result.");
529
- return { state: "completed", turn: body.data };
530
- }
531
- var TonboApi = class {
532
- constructor(fetcher, accountOrigin = "https://tonbo.dev", managementOrigin = "https://api.tonbo.dev") {
362
+ var DEFAULT_API_ORIGIN = "https://api.tonbo.dev";
363
+ var ManagementApi = class {
364
+ constructor(fetcher, apiOrigin2 = DEFAULT_API_ORIGIN) {
533
365
  this.fetcher = fetcher;
534
- this.accountOrigin = accountOrigin;
535
- this.managementOrigin = managementOrigin;
536
- }
537
- cancelActivities(agentId, token, body) {
538
- return this.management("POST", `/v1/agents/${agentId}/production/cancellations`, token, body);
539
- }
540
- async listRegions(oauthToken) {
541
- return parseRegionDiscovery(
542
- await requestJson(this.fetcher, `${this.accountOrigin}/api/cli/regions`, {
543
- headers: { authorization: `Bearer ${oauthToken}` }
544
- })
545
- );
546
- }
547
- machineRequest(oauthToken, path6 = "", method = "GET", body) {
548
- return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/machines${path6}`, {
549
- method,
550
- headers: {
551
- authorization: `Bearer ${oauthToken}`,
552
- ...body === void 0 ? {} : { "content-type": "application/json" }
553
- },
554
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
555
- });
556
- }
557
- listAgents(oauthToken) {
558
- return requestJson(
559
- this.fetcher,
560
- `${this.accountOrigin}/api/cli/agents`,
561
- {
562
- headers: { authorization: `Bearer ${oauthToken}` }
563
- }
564
- ).then((body) => body.agents);
565
- }
566
- listOrganizations(oauthToken) {
567
- return requestJson(
568
- this.fetcher,
569
- `${this.accountOrigin}/api/cli/accounts`,
570
- {
571
- headers: { authorization: `Bearer ${oauthToken}` }
572
- }
573
- ).then((body) => body.organizations);
574
- }
575
- /** An omitted region lets the server choose the only region that accepts
576
- * new Agents; once more than one does, it refuses the request. */
577
- createAgent(oauthToken, {
578
- name,
579
- organizationId,
580
- region,
581
- requestId = randomUUID()
582
- }) {
583
- return requestJson(
584
- this.fetcher,
585
- `${this.accountOrigin}/api/cli/agents`,
586
- {
587
- method: "POST",
588
- headers: {
589
- authorization: `Bearer ${oauthToken}`,
590
- "content-type": "application/json"
591
- },
592
- body: JSON.stringify({
593
- name,
594
- requestId,
595
- ...organizationId ? { orgId: organizationId } : {},
596
- ...region ? { region } : {}
597
- })
598
- }
599
- ).then((body) => body.agent).catch((cause) => {
600
- if (cause instanceof Error && "status" in cause && typeof cause.status === "number" && cause.status >= 400 && cause.status < 500)
601
- throw cause;
602
- throw new Error(
603
- `Agent creation was not confirmed. Query \`tonbo agent creation ${requestId}${organizationId ? ` --account ${organizationId}` : ""}\` or retry creation with --request-id ${requestId} and the same name, account and region.`,
604
- { cause }
605
- );
606
- });
607
- }
608
- readAgentCreationRequest(oauthToken, requestId, accountId) {
609
- const account = accountId ? `?account=${encodeURIComponent(accountId)}` : "";
610
- return requestJson(
611
- this.fetcher,
612
- `${this.accountOrigin}/api/cli/agents/creations/${encodeURIComponent(requestId)}${account}`,
613
- { headers: { authorization: `Bearer ${oauthToken}` } }
614
- );
615
- }
616
- registerSshKey(oauthToken, key) {
617
- return requestJson(
618
- this.fetcher,
619
- `${this.accountOrigin}/api/cli/ssh-keys`,
620
- {
621
- method: "POST",
622
- headers: {
623
- authorization: `Bearer ${oauthToken}`,
624
- "content-type": "application/json"
625
- },
626
- body: JSON.stringify({
627
- algorithm: key.algorithm,
628
- key_base64: key.keyBase64,
629
- label: key.label
630
- })
631
- }
632
- ).then((body) => body.key);
633
- }
634
- revokeSshKey(oauthToken, fingerprint) {
635
- return requestJson(
636
- this.fetcher,
637
- `${this.accountOrigin}/api/cli/ssh-keys`,
638
- {
639
- method: "DELETE",
640
- headers: {
641
- authorization: `Bearer ${oauthToken}`,
642
- "content-type": "application/json"
643
- },
644
- body: JSON.stringify({ fingerprint })
645
- }
646
- ).then((body) => body.key);
647
- }
648
- exchangeManagementToken(oauthToken, agentId) {
649
- return requestJson(
650
- this.fetcher,
651
- `${this.accountOrigin}/api/cli/agents/${agentId}/token`,
652
- {
653
- method: "POST",
654
- headers: { authorization: `Bearer ${oauthToken}` }
655
- }
656
- ).then((body) => body.access_token);
657
- }
658
- async deploy({
659
- bundle,
660
- origin,
661
- agentId,
662
- promote,
663
- spec,
664
- token
665
- }) {
666
- const descriptor = {
667
- format: bundle.format,
668
- sha256: bundle.sha256,
669
- size_bytes: bundle.size_bytes
670
- };
671
- const bundlesPath = `/v1/agents/${agentId}/source-bundles`;
672
- const prepared = await this.management("PUT", `${bundlesPath}/${bundle.sha256}`, token, {
673
- format: descriptor.format,
674
- size_bytes: descriptor.size_bytes
366
+ this.apiOrigin = apiOrigin2;
367
+ }
368
+ /** The answer's `data`. A refusal throws an HttpError naming the request id. */
369
+ async call(operationId, key, request = {}) {
370
+ return (await this.request(operationId, key, request)).data;
371
+ }
372
+ /** One page of a list operation: its `data` and `next_cursor`. */
373
+ page(operationId, key, { limit, cursor } = {}) {
374
+ return this.request(operationId, key, { query: { limit, cursor } });
375
+ }
376
+ async request(operationId, key, { params = {}, query = {}, body, idempotencyKey }) {
377
+ const operation = managementOperations[operationId];
378
+ const path2 = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
379
+ const value = params[name];
380
+ if (value === void 0) throw new Error(`${operationId} needs the path parameter ${name}.`);
381
+ return encodeURIComponent(value);
675
382
  });
676
- if (prepared.status === "upload") {
677
- if (!prepared.upload_url) throw new Error("Tonbo did not return a source upload URL.");
678
- if (!prepared.upload_headers || prepared.upload_headers["if-none-match"] !== "*")
679
- throw new Error("Missing immutable source upload headers.");
680
- const uploaded = await this.fetcher(prepared.upload_url, {
681
- redirect: "error",
682
- method: "PUT",
683
- headers: prepared.upload_headers,
684
- body: new Blob([new Uint8Array(bundle.bytes)])
685
- });
686
- let uploadError = null;
687
- if (!uploaded.ok) {
688
- uploadError = new Error(`Source upload failed with HTTP ${uploaded.status}.`);
689
- }
690
- try {
691
- await this.management("POST", `${bundlesPath}/${bundle.sha256}/complete`, token, {
692
- format: bundle.format,
693
- size_bytes: bundle.size_bytes
694
- });
695
- } catch (error) {
696
- if (uploadError)
697
- throw new AggregateError([uploadError, error], "Source bundle upload did not complete.");
698
- throw error;
699
- }
383
+ const search = new URLSearchParams();
384
+ for (const [name, value] of Object.entries(query)) {
385
+ if (value === void 0) continue;
386
+ if (!operation.query.includes(name))
387
+ throw new Error(`${operationId} takes no query parameter ${name}.`);
388
+ search.set(name, String(value));
700
389
  }
701
- let deployment = await this.createDeployment(agentId, { spec, origin }, token);
702
- const previous = await this.getProduction(agentId, token);
703
- const previousDeployment = previous ? await this.getDeployment(agentId, previous.deployment_id, token) : null;
704
- const unchanged = previousDeployment?.spec_sha256 === deployment.spec_sha256;
705
- let production = previous;
706
- if (promote) {
707
- production = await this.putProduction(
708
- agentId,
709
- {
710
- deployment_id: deployment.id,
711
- desired_state: "running",
712
- expected_generation: previous ? previous.generation : null
713
- },
714
- token
715
- );
716
- deployment = await this.getDeployment(agentId, deployment.id, token);
390
+ if (operation.idempotencyKey && !idempotencyKey)
391
+ throw new Error(`${operationId} needs an Idempotency-Key.`);
392
+ const headers = { authorization: `Bearer ${key}` };
393
+ if (body !== void 0) headers["content-type"] = "application/json";
394
+ if (idempotencyKey) headers["idempotency-key"] = idempotencyKey;
395
+ const queryString = search.toString();
396
+ const url = `${this.apiOrigin}${managementApiBasePath}${path2}${queryString ? `?${queryString}` : ""}`;
397
+ try {
398
+ return await requestJson(this.fetcher, url, {
399
+ method: operation.method,
400
+ headers,
401
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
402
+ });
403
+ } catch (error) {
404
+ if (error instanceof HttpError && error.status === 401)
405
+ throw new HttpError(
406
+ `${error.message} Run \`tonbo login\`.`,
407
+ error.status,
408
+ error.body,
409
+ error.requestId
410
+ );
411
+ throw error;
717
412
  }
718
- return { deployment, production, unchanged };
719
- }
720
- createDeployment(agentId, body, token) {
721
- return this.management(
722
- "POST",
723
- `/v1/agents/${agentId}/deployments`,
724
- token,
725
- body
726
- ).then((response) => response.data);
727
413
  }
728
- listDeployments(agentId, token) {
729
- return this.managementList(`/v1/agents/${agentId}/deployments`, token);
730
- }
731
- getDeployment(agentId, deploymentId, token) {
732
- return this.management(
733
- "GET",
734
- `/v1/agents/${agentId}/deployments/${deploymentId}`,
735
- token
736
- ).then((response) => response.data);
737
- }
738
- getProduction(agentId, token) {
739
- return this.management(
740
- "GET",
741
- `/v1/agents/${agentId}/production`,
742
- token
743
- ).then(
744
- (response) => response.data,
745
- (error) => {
746
- if (error.status === 404) return null;
747
- throw error;
748
- }
749
- );
750
- }
751
- putProduction(agentId, body, token) {
752
- return this.management(
753
- "PUT",
754
- `/v1/agents/${agentId}/production`,
755
- token,
756
- body
757
- ).then((response) => response.data);
758
- }
759
- listRollouts(agentId, token) {
760
- return this.managementList(`/v1/agents/${agentId}/production/rollouts`, token);
761
- }
762
- /** Checks that Production is running and opens a durable Session, or
763
- * reuses the given one. */
764
- async openSession({
765
- agentId,
766
- sessionId,
767
- token
768
- }) {
769
- const production = await this.getProduction(agentId, token);
770
- if (!production)
771
- throw new Error("Agent has no Production Deployment; run `tonbo deploy` first.");
772
- if (production.observed_state !== "running")
773
- throw new Error(
774
- `Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
775
- );
776
- const session = sessionId ? { id: sessionId } : (await this.management(
777
- "POST",
778
- `/v1/agents/${agentId}/sessions`,
779
- token,
780
- { deployment_id: production.deployment_id }
781
- )).data;
782
- return { production, session };
414
+ getAccount(key) {
415
+ return this.call("getAccount", key);
783
416
  }
784
- /** Submits one Turn under its own id; sending it again under the same id is
785
- * answered from the same operation. The Management API holds the request
786
- * for up to five minutes: it answers the result, 409 `turn_failed`, a
787
- * refusal, or 202 while the Turn waits or runs. */
788
- async submitTurn({
789
- agentId,
790
- prompt,
791
- sessionId,
792
- signal,
793
- token,
794
- turnId
795
- }) {
796
- return turnSubmission(
797
- await this.managementResponse(
798
- "POST",
799
- `/v1/agents/${agentId}/sessions/${sessionId}/turns`,
800
- token,
801
- { body: { prompt }, idempotencyKey: turnId, signal }
802
- )
803
- );
417
+ listRegions(key) {
418
+ return this.call("listRegions", key);
804
419
  }
805
- /** One durable Turn; reading it never affects its execution. */
806
- getTurn(agentId, sessionId, turnId, token) {
807
- return this.management(
808
- "GET",
809
- `/v1/agents/${agentId}/sessions/${sessionId}/turns/${turnId}`,
810
- token
811
- ).then((response) => response.data);
420
+ listMembers(key, page) {
421
+ return this.page("listMembers", key, page);
812
422
  }
813
- turnEvents({
814
- after = 0,
815
- agentId,
816
- sessionId,
817
- token,
818
- turnId
819
- }) {
820
- return this.management(
821
- "GET",
822
- `/v1/agents/${agentId}/sessions/${sessionId}/turns/${turnId}/events?after=${after}`,
823
- token
824
- );
423
+ updateMember(key, subject, change) {
424
+ return this.call("updateMember", key, { params: { subject }, body: change });
825
425
  }
826
- listAgentSecrets(agentId, token) {
827
- return this.management(
828
- "GET",
829
- `/v1/agents/${agentId}/secrets`,
830
- token
831
- ).then((body) => body.data);
426
+ /** A replay with the same idempotency key returns the Agent the first
427
+ * request created, so an unconfirmed creation is retried with that key. */
428
+ createAgent(key, request, idempotencyKey) {
429
+ return this.call("createAgent", key, {
430
+ body: withoutUndefined(request),
431
+ idempotencyKey
432
+ }).catch(unconfirmed("Agent creation", idempotencyKey, "the same name, role and region"));
832
433
  }
833
- setAgentSecret(agentId, name, value, token) {
834
- return this.management(
835
- "PUT",
836
- `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
837
- token,
838
- { value }
839
- );
434
+ listApiKeys(key, page) {
435
+ return this.page("listApiKeys", key, page);
840
436
  }
841
- deleteAgentSecret(agentId, name, token) {
842
- return this.management(
843
- "DELETE",
844
- `/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
845
- token
846
- );
437
+ /** Not idempotent: the secret is never stored, so a replay could not
438
+ * return it, and a blind retry of a live name answers 409. */
439
+ createApiKey(key, request) {
440
+ return this.call("createApiKey", key, { body: withoutUndefined(request) });
847
441
  }
848
- management(method, path6, token, body) {
849
- return this.managementResponse(method, path6, token, { body }).then(
850
- (response) => response.body
851
- );
442
+ getApiKey(key, keyId) {
443
+ return this.call("getApiKey", key, { params: { key_id: keyId } });
852
444
  }
853
- managementResponse(method, path6, token, {
854
- body,
855
- idempotencyKey = randomUUID(),
856
- signal
857
- } = {}) {
858
- return requestJsonResponse(this.fetcher, `${this.managementOrigin}${path6}`, {
859
- method,
860
- headers: {
861
- authorization: `Bearer ${token}`,
862
- ...body === void 0 ? {} : { "content-type": "application/json", "idempotency-key": idempotencyKey }
863
- },
864
- ...body === void 0 ? {} : { body: JSON.stringify(body) },
865
- ...signal ? { signal } : {}
445
+ revokeApiKey(key, keyId) {
446
+ return this.call("revokeApiKey", key, {
447
+ params: { key_id: keyId }
866
448
  });
867
449
  }
868
- async managementList(path6, token) {
869
- const values = [];
870
- let cursor = null;
871
- do {
872
- const separator = path6.includes("?") ? "&" : "?";
873
- const page = await this.management(
874
- "GET",
875
- `${path6}${separator}limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
876
- token
877
- );
878
- values.push(...page.data);
879
- cursor = page.next_cursor;
880
- } while (cursor);
881
- return values;
450
+ listBuckets(key, page) {
451
+ return this.page("listBuckets", key, page);
882
452
  }
883
- };
884
-
885
- // src/agent-source.ts
886
- import { lstat, readFile } from "node:fs/promises";
887
- import path from "node:path";
888
-
889
- // ../../packages/agent-source-inspector/dist/src/index.js
890
- var AGENTS_INSTRUCTIONS_PATH = "AGENTS.md";
891
- var PI_SETTINGS_PATH = ".pi/settings.json";
892
- var MAX_HARNESS_CONFIG_BYTES = 256 * 1024;
893
- var supportedHarnesses = [
894
- {
895
- command: "pi",
896
- id: "pi",
897
- name: "PI"
453
+ /** A replay with the same idempotency key returns the bucket the first
454
+ * request created. */
455
+ createBucket(key, request, idempotencyKey) {
456
+ return this.call("createBucket", key, {
457
+ body: withoutUndefined(request),
458
+ idempotencyKey
459
+ }).catch(unconfirmed("Bucket creation", idempotencyKey, "the same name and region"));
898
460
  }
899
- ];
900
- var supportedExecutionTargets = [
901
- { driver: "native", harness: "pi" },
902
- { driver: "command", harness: "pi" }
903
- ];
904
- var AgentSourceInspectionError = class extends Error {
905
- code;
906
- path;
907
- constructor(code, path6, message, options) {
908
- super(message, options);
909
- this.code = code;
910
- this.path = path6;
911
- this.name = "AgentSourceInspectionError";
461
+ getBucket(key, name) {
462
+ return this.call("getBucket", key, { params: { name } });
912
463
  }
913
- };
914
- function parsePiPackageCount(contents) {
915
- let settings;
916
- try {
917
- settings = JSON.parse(contents);
918
- } catch (error) {
919
- throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} must contain valid JSON.`, { cause: error });
464
+ updateBucket(key, name, change) {
465
+ return this.call("updateBucket", key, { params: { name }, body: change });
920
466
  }
921
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
922
- throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} must contain a JSON object.`);
467
+ deleteBucket(key, name) {
468
+ return this.call("deleteBucket", key, { params: { name } });
923
469
  }
924
- if (!("packages" in settings))
925
- return 0;
926
- const packages = settings.packages;
927
- if (!Array.isArray(packages)) {
928
- throw new AgentSourceInspectionError("invalid_pi_settings", PI_SETTINGS_PATH, `${PI_SETTINGS_PATH} packages must be an array.`);
470
+ getUsage(key, period) {
471
+ return this.call("getUsage", key, { query: { period } });
929
472
  }
930
- return packages.length;
473
+ };
474
+ function withoutUndefined(value) {
475
+ return Object.fromEntries(
476
+ Object.entries(value).filter(([, field]) => field !== void 0)
477
+ );
931
478
  }
932
- async function inspectAgentSource(source) {
933
- const [agentsMd, piSettings] = await Promise.all([
934
- source.has(AGENTS_INSTRUCTIONS_PATH),
935
- source.readText(PI_SETTINGS_PATH, MAX_HARNESS_CONFIG_BYTES)
936
- ]);
937
- if (piSettings !== null) {
938
- return {
939
- version: 1,
940
- instructions: { agentsMd },
941
- harness: {
942
- id: "pi",
943
- reason: "pi_settings_found",
944
- state: "identified"
945
- },
946
- pi: {
947
- packageCount: parsePiPackageCount(piSettings),
948
- settingsFound: true
949
- }
950
- };
951
- }
952
- return {
953
- version: 1,
954
- instructions: { agentsMd },
955
- harness: {
956
- candidates: supportedHarnesses.map((harness) => harness.id),
957
- reason: "no_harness_specific_config",
958
- state: "selection_required"
959
- },
960
- pi: {
961
- packageCount: 0,
962
- settingsFound: false
963
- }
479
+ function unconfirmed(what, idempotencyKey, same) {
480
+ return (cause) => {
481
+ if (cause instanceof HttpError && cause.status < 500) throw cause;
482
+ throw new Error(
483
+ `${what} was not confirmed: ${cause instanceof Error ? cause.message : String(cause)} Retry with --idempotency-key ${idempotencyKey} and ${same}.`,
484
+ { cause }
485
+ );
964
486
  };
965
487
  }
966
488
 
967
- // src/agent-source.ts
968
- var LocalAgentSource = class {
969
- constructor(root) {
970
- this.root = root;
971
- }
972
- async has(filename) {
973
- const metadata = await this.metadata(filename);
974
- return metadata?.isFile() === true;
975
- }
976
- async readText(filename, maxBytes) {
977
- const metadata = await this.metadata(filename);
978
- if (!metadata) return null;
979
- if (!metadata.isFile() || metadata.isSymbolicLink()) {
980
- throw new Error(`${filename} must be a regular file.`);
981
- }
982
- if (metadata.size > maxBytes) {
983
- throw new Error(`${filename} exceeds the ${maxBytes}-byte inspection limit.`);
984
- }
985
- return readFile(path.join(this.root, filename), "utf8");
986
- }
987
- async metadata(filename) {
988
- try {
989
- return await lstat(path.join(this.root, filename));
990
- } catch (error) {
991
- if (error.code === "ENOENT") return null;
992
- throw error;
993
- }
994
- }
995
- };
996
- function inspectLocalAgentSource(root) {
997
- return inspectAgentSource(new LocalAgentSource(root));
998
- }
999
-
1000
489
  // src/auth.ts
1001
490
  import { createHash, randomBytes } from "node:crypto";
1002
491
  import { createServer } from "node:http";
1003
492
  import { execFile } from "node:child_process";
493
+ import os from "node:os";
1004
494
  import { promisify } from "node:util";
1005
495
 
1006
496
  // src/callback-page.ts
@@ -1137,104 +627,78 @@ function callbackResponse(outcome) {
1137
627
 
1138
628
  // src/auth.ts
1139
629
  var exec = promisify(execFile);
1140
- var LOOPBACK_REDIRECT = "http://localhost:17655/callback";
1141
- function base64url(value) {
1142
- return value.toString("base64url");
1143
- }
1144
- function withAbsoluteExpiry(tokens) {
1145
- if (tokens.expires_at || !Number.isInteger(tokens.expires_in) || (tokens.expires_in ?? 0) <= 0)
1146
- return tokens;
1147
- return {
1148
- ...tokens,
1149
- expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 0)
1150
- };
630
+ var LOOPBACK_HOST = "127.0.0.1";
631
+ function loginHostname(hostname = os.hostname()) {
632
+ const cleaned = hostname.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 64);
633
+ return cleaned || "computer";
1151
634
  }
1152
635
  var AuthClient = class {
1153
- constructor(credentials, fetcher, accountOrigin = "https://tonbo.dev", browserOpener = openBrowser) {
636
+ constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname()) {
1154
637
  this.credentials = credentials;
1155
638
  this.fetcher = fetcher;
1156
- this.accountOrigin = accountOrigin;
639
+ this.loginOrigin = loginOrigin2;
1157
640
  this.browserOpener = browserOpener;
641
+ this.hostname = hostname;
1158
642
  }
1159
- /** The stored access token, refreshed first when it has less than
1160
- * `minimumLifetimeSeconds` left. */
1161
- async accessToken(minimumLifetimeSeconds = 30) {
1162
- const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
643
+ /** The personal key requests are made with: TONBO_API_KEY, or the saved
644
+ * login while it has not expired. */
645
+ async apiKey() {
646
+ const injected = process.env.TONBO_API_KEY?.trim();
1163
647
  if (injected) return injected;
1164
- const tokens = await this.credentials.load();
1165
- if (!tokens) throw new Error("Not logged in. Run `tonbo login`.");
1166
- const legacyRelativeExpiry = !tokens.expires_at && tokens.expires_in;
1167
- if (!legacyRelativeExpiry && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1e3) + minimumLifetimeSeconds))
1168
- return tokens.access_token;
1169
- if (!tokens.refresh_token) throw new Error("Tonbo login expired. Run `tonbo login` again.");
1170
- const config = await this.config();
1171
- const refreshed = withAbsoluteExpiry(
1172
- await this.exchange(
1173
- config.token_endpoint,
1174
- new URLSearchParams({
1175
- client_id: config.client_id,
1176
- grant_type: "refresh_token",
1177
- refresh_token: tokens.refresh_token
1178
- })
1179
- )
1180
- );
1181
- if (!refreshed.refresh_token) refreshed.refresh_token = tokens.refresh_token;
1182
- await this.credentials.save(refreshed);
1183
- return refreshed.access_token;
1184
- }
648
+ const login = await this.credentials.load();
649
+ if (!login) throw new Error("Not logged in. Run `tonbo login`.");
650
+ if (login.expires_at && Date.parse(login.expires_at) <= Date.now())
651
+ throw new Error("Your Tonbo login has expired. Run `tonbo login`.");
652
+ return login.key;
653
+ }
654
+ /**
655
+ * The browser handoff of /cli/login: PKCE S256 with a loopback redirect on
656
+ * a free port, then the one-time code is redeemed for a personal key named
657
+ * "CLI on <hostname>", which replaces that host's earlier login key.
658
+ */
1185
659
  async login(progress = () => {
1186
- }) {
1187
- progress({ status: "started", step: "account-config" });
1188
- const config = await this.config();
1189
- progress({ status: "completed", step: "account-config" });
1190
- if (config.redirect_uri !== LOOPBACK_REDIRECT)
1191
- throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
1192
- const verifier = base64url(randomBytes(48));
660
+ }, account) {
661
+ const verifier = randomBytes(48).toString("base64url");
1193
662
  const challenge = createHash("sha256").update(verifier).digest("base64url");
1194
- const state = base64url(randomBytes(24));
1195
- const authorization = new URL(config.authorization_endpoint);
1196
- authorization.search = new URLSearchParams({
1197
- client_id: config.client_id,
1198
- code_challenge: challenge,
1199
- code_challenge_method: "S256",
1200
- redirect_uri: config.redirect_uri,
1201
- response_type: "code",
1202
- scope: "openid profile email",
1203
- state
1204
- }).toString();
663
+ const state = randomBytes(24).toString("base64url");
1205
664
  const code = await this.listenForCode(
1206
665
  state,
1207
- () => this.browserOpener(authorization.toString()),
666
+ (redirectUri) => {
667
+ const authorization = new URL("/cli/login", this.loginOrigin);
668
+ authorization.search = new URLSearchParams({
669
+ code_challenge: challenge,
670
+ code_challenge_method: "S256",
671
+ redirect_uri: redirectUri,
672
+ state,
673
+ hostname: this.hostname,
674
+ ...account ? { account } : {}
675
+ }).toString();
676
+ return this.browserOpener(authorization.toString());
677
+ },
1208
678
  progress
1209
679
  );
1210
- progress({ status: "started", step: "token-exchange" });
1211
- const tokens = withAbsoluteExpiry(
1212
- await this.exchange(
1213
- config.token_endpoint,
1214
- new URLSearchParams({
1215
- client_id: config.client_id,
1216
- code,
1217
- code_verifier: verifier,
1218
- grant_type: "authorization_code",
1219
- redirect_uri: config.redirect_uri
1220
- })
1221
- )
680
+ progress({ status: "started", step: "key-redemption" });
681
+ const redeemed = await requestJson(
682
+ this.fetcher,
683
+ new URL("/api/cli/login/token", this.loginOrigin).toString(),
684
+ {
685
+ method: "POST",
686
+ headers: { "content-type": "application/json" },
687
+ body: JSON.stringify({ code, code_verifier: verifier })
688
+ }
1222
689
  );
1223
- progress({ status: "completed", step: "token-exchange" });
690
+ progress({ status: "completed", step: "key-redemption" });
691
+ const login = {
692
+ version: 1,
693
+ account_id: redeemed.account_id,
694
+ key_id: redeemed.api_key.id,
695
+ key: redeemed.key,
696
+ expires_at: redeemed.api_key.expires_at
697
+ };
1224
698
  progress({ status: "started", step: "credential-store" });
1225
- await this.credentials.save(tokens);
699
+ await this.credentials.save(login);
1226
700
  progress({ status: "completed", step: "credential-store" });
1227
- return tokens;
1228
- }
1229
- config() {
1230
- return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/config`);
1231
- }
1232
- exchange(endpoint, body) {
1233
- return requestJson(this.fetcher, endpoint, {
1234
- method: "POST",
1235
- headers: { "content-type": "application/x-www-form-urlencoded" },
1236
- body
1237
- });
701
+ return login;
1238
702
  }
1239
703
  listenForCode(expectedState, ready, progress) {
1240
704
  return new Promise((resolve, reject) => {
@@ -1276,19 +740,23 @@ var AuthClient = class {
1276
740
  5 * 60 * 1e3
1277
741
  );
1278
742
  const server = createServer((request, response) => {
1279
- const url = new URL(request.url ?? "/", LOOPBACK_REDIRECT);
1280
- const code = url.searchParams.get("code");
1281
- const oauthError = url.searchParams.get("error_description") ?? url.searchParams.get("error");
1282
- if (oauthError) {
743
+ const url = new URL(request.url ?? "/", `http://${LOOPBACK_HOST}`);
744
+ if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState) {
745
+ respond(response, "invalid");
746
+ return;
747
+ }
748
+ const denied = url.searchParams.get("error");
749
+ if (denied) {
1283
750
  const responseSocket2 = response.socket;
1284
751
  respond(
1285
752
  response,
1286
753
  "denied",
1287
- () => finish({ error: new Error(oauthError) }, responseSocket2)
754
+ () => finish({ error: new Error(`The login was not approved (${denied}).`) }, responseSocket2)
1288
755
  );
1289
756
  return;
1290
757
  }
1291
- if (url.pathname !== "/callback" || url.searchParams.get("state") !== expectedState || !code) {
758
+ const code = url.searchParams.get("code");
759
+ if (!code) {
1292
760
  respond(response, "invalid");
1293
761
  return;
1294
762
  }
@@ -1303,10 +771,11 @@ var AuthClient = class {
1303
771
  });
1304
772
  server.once("error", (error) => finish({ error }));
1305
773
  progress({ status: "started", step: "callback-server" });
1306
- server.listen(17655, "localhost", () => {
774
+ server.listen(0, LOOPBACK_HOST, () => {
775
+ const { port } = server.address();
1307
776
  progress({ status: "completed", step: "callback-server" });
1308
777
  progress({ status: "started", step: "browser" });
1309
- void ready().then(startAuthorization).catch(
778
+ void ready(`http://${LOOPBACK_HOST}:${port}/callback`).then(startAuthorization).catch(
1310
779
  (error) => finish({
1311
780
  error: error instanceof Error ? error : new Error("Could not open the login browser.")
1312
781
  })
@@ -1328,420 +797,96 @@ async function openBrowser(url) {
1328
797
  return void await exec("xdg-open", [url]);
1329
798
  }
1330
799
 
1331
- // ../../packages/agent-source-bundler/dist/src/generated/contracts.js
1332
- var inferenceDefault = { "model": "claude-sonnet-5" };
1333
- var sourceBundleContract = { "version": 1, "format": "tar-v1", "key_prefix": "agent-source-bundles", "content_type": "application/vnd.tonbo.source+tar", "max_bytes": 67108864, "runtime_cache_directories": [".pi/npm", ".pi/git"] };
1334
- 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"] };
1335
- var piSlackTemplate = { "repository": "tonbo-io/pi-slack-agent", "source_directory": "examples/pi-slack-agent", "service": { "command": ["node", "slack/server.mjs"], "secrets": ["SLACK_SIGNING_SECRET", "SLACK_BOT_TOKEN", "SLACK_BOT_USER_ID", "SLACK_TEAM_ID", "TONBO_AGENT_API_KEY", "TONBO_AGENT_API_ORIGIN", "TONBO_IAM_ORIGIN"] } };
1336
-
1337
- // ../../packages/agent-source-bundler/dist/src/inference-default.js
1338
- var DEFAULT_INFERENCE_MODEL = inferenceDefault.model;
800
+ // src/commands.ts
801
+ import { randomUUID as randomUUID2 } from "node:crypto";
1339
802
 
1340
- // ../../packages/agent-source-bundler/dist/src/deployment-origin.js
1341
- function deploymentOriginLabel(actor) {
1342
- return actor === "onboarding" ? "Setup guide" : "tonbo deploy";
803
+ // src/credentials.ts
804
+ import { randomUUID } from "node:crypto";
805
+ import { chmod, lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
806
+ import os2 from "node:os";
807
+ import path from "node:path";
808
+ function defaultConfigDirectory() {
809
+ const base = process.env.XDG_CONFIG_HOME || path.join(os2.homedir(), ".config");
810
+ return path.join(base, "tonbo");
1343
811
  }
1344
- function githubRepositoryUrl(remote) {
1345
- if (typeof remote !== "string" || !remote || /\s/.test(remote) || [...remote].some((character) => character.charCodeAt(0) < 32))
1346
- return null;
1347
- let path6;
1348
- const ssh = /^git@github\.com:([^?#]+)$/.exec(remote);
1349
- if (ssh)
1350
- path6 = ssh[1];
1351
- else {
812
+ var UnusableLoginError = class extends Error {
813
+ };
814
+ var FileCredentialStore = class {
815
+ constructor(filename = path.join(defaultConfigDirectory(), "credentials.json")) {
816
+ this.filename = filename;
817
+ }
818
+ async load() {
819
+ let parsed;
1352
820
  try {
1353
- const url = new URL(remote);
1354
- if (url.hostname !== "github.com" || url.port || !["https:", "ssh:"].includes(url.protocol))
1355
- return null;
1356
- path6 = url.pathname.slice(1);
1357
- } catch {
1358
- return null;
821
+ await assertPrivateRegularFile(this.filename);
822
+ parsed = JSON.parse(await readFile(this.filename, "utf8"));
823
+ } catch (error) {
824
+ if (error.code === "ENOENT") return null;
825
+ throw new Error(`Could not read Tonbo credentials at ${this.filename}.`, { cause: error });
1359
826
  }
827
+ if (!isStoredLogin(parsed))
828
+ throw new UnusableLoginError(
829
+ `The saved login at ${this.filename} is not usable by this CLI. Run \`tonbo login\`.`
830
+ );
831
+ return parsed;
1360
832
  }
1361
- path6 = path6.replace(/\/$/, "").replace(/\.git$/, "");
1362
- const parts = path6.split("/");
1363
- 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]))
1364
- return null;
1365
- return `https://github.com/${parts[0]}/${parts[1]}`;
1366
- }
1367
-
1368
- // src/commands.ts
1369
- import path4 from "node:path";
1370
- import { randomUUID as randomUUID3 } from "node:crypto";
1371
-
1372
- // src/build.ts
1373
- import { spawn } from "node:child_process";
1374
- async function runBuildCommand(root, command) {
1375
- if (command.length === 0) throw new Error("Build command must not be empty.");
1376
- await new Promise((resolve, reject) => {
1377
- const child = spawn(command[0], command.slice(1), {
1378
- cwd: root,
1379
- env: process.env,
1380
- // Command output is progress, not the CLI result. Keep stdout available
1381
- // for the single JSON document emitted by `tonbo --json deploy`.
1382
- stdio: ["inherit", process.stderr, process.stderr]
1383
- });
1384
- child.once("error", reject);
1385
- child.once("exit", (code, signal) => {
1386
- if (code === 0) resolve();
1387
- else
1388
- reject(
1389
- new Error(
1390
- `Build command failed${signal ? ` with signal ${signal}` : ` with status ${code}`}.`
1391
- )
1392
- );
1393
- });
1394
- });
1395
- }
1396
-
1397
- // ../../packages/agent-source-bundler/dist/src/native-pi.js
1398
- import { stringify } from "smol-toml";
1399
- var PI_SLACK_TEMPLATE_SERVICE = piSlackTemplate.service;
1400
- function nativePiDeclaration(model, service) {
1401
- return {
1402
- version: 2,
1403
- harness: { runtime: "pi", driver: { kind: "native" } },
1404
- inference: { model },
1405
- ...service ? { service } : {}
1406
- };
1407
- }
1408
-
1409
- // src/declaration.ts
1410
- import { randomUUID as randomUUID2 } from "node:crypto";
1411
- import { lstat as lstat2, open, readFile as readFile2, rename, rm } from "node:fs/promises";
1412
- import path2 from "node:path";
1413
- import { parse, stringify as stringify2 } from "smol-toml";
1414
- var DECLARATION_FILENAME = ".tonbo";
1415
- function createDeclaration(model = DEFAULT_INFERENCE_MODEL, driver = { kind: "native" }, buildCommand, agentHostname) {
1416
- const base = nativePiDeclaration(model.trim());
1417
- return parseDeclaration({
1418
- ...base,
1419
- ...agentHostname ? { agent: agentHostname } : {},
1420
- harness: { ...base.harness, driver },
1421
- ...buildCommand ? { build: { command: buildCommand } } : {}
1422
- });
1423
- }
1424
- function renderDeclaration(declaration) {
1425
- return `# Tonbo Agent configuration.
1426
- # Edit this file directly or run \`tonbo init\` to reconfigure.
1427
- ${stringify2(declaration)}`;
1428
- }
1429
- async function bindDeclarationAgent(root, agentHostname) {
1430
- const declaration = await loadDeclaration(root);
1431
- const bound = parseDeclaration({ ...declaration, agent: agentHostname });
1432
- await saveDeclaration(root, bound, true);
1433
- return bound;
1434
- }
1435
- async function declarationExists(root) {
1436
- const filename = path2.join(root, DECLARATION_FILENAME);
1437
- try {
1438
- const metadata = await lstat2(filename);
1439
- if (metadata.isSymbolicLink() || !metadata.isFile())
1440
- throw new Error(`${filename} must be a regular file.`);
1441
- return true;
1442
- } catch (error) {
1443
- if (error.code === "ENOENT") return false;
1444
- throw error;
1445
- }
1446
- }
1447
- async function saveDeclaration(root, declaration, overwrite) {
1448
- const filename = path2.join(root, DECLARATION_FILENAME);
1449
- const exists = await declarationExists(root);
1450
- if (exists && !overwrite)
1451
- throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1452
- const contents = renderDeclaration(declaration);
1453
- if (!overwrite) {
1454
- const handle2 = await open(filename, "wx", 420).catch((error) => {
1455
- if (error.code === "EEXIST")
1456
- throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1457
- throw error;
1458
- });
1459
- try {
1460
- await handle2.writeFile(contents, "utf8");
1461
- await handle2.sync();
1462
- } finally {
1463
- await handle2.close();
1464
- }
1465
- return;
1466
- }
1467
- const temporary = path2.join(root, `.${DECLARATION_FILENAME}.${process.pid}.${randomUUID2()}.tmp`);
1468
- let handle;
1469
- try {
1470
- handle = await open(temporary, "wx", 420);
1471
- await handle.writeFile(contents, "utf8");
1472
- await handle.sync();
1473
- await handle.close();
1474
- handle = void 0;
1475
- await rename(temporary, filename);
1476
- } catch (error) {
1477
- await handle?.close().catch(() => void 0);
1478
- await rm(temporary, { force: true }).catch(() => void 0);
1479
- throw error;
1480
- }
1481
- }
1482
- async function loadDeclaration(declarationRoot) {
1483
- const filename = path2.join(declarationRoot, DECLARATION_FILENAME);
1484
- let parsed;
1485
- try {
1486
- parsed = parse(await readFile2(filename, "utf8"));
1487
- } catch (error) {
1488
- if (error.code === "ENOENT")
1489
- throw new Error(`No ${DECLARATION_FILENAME} declaration found at ${filename}.`);
1490
- throw new Error(`Could not read ${filename} as TOML.`, { cause: error });
1491
- }
1492
- return parseDeclaration(parsed);
1493
- }
1494
- function buildDeploymentSpec(declaration, source) {
1495
- const spec = {
1496
- version: 1,
1497
- agent: declaration.harness,
1498
- source: {
1499
- format: source.format,
1500
- sha256: source.sha256,
1501
- size_bytes: source.size_bytes
1502
- },
1503
- inference: declaration.inference,
1504
- ...declaration.service ? { service: declaration.service } : {}
1505
- };
1506
- assertManagedDeploymentSpec(spec);
1507
- return spec;
1508
- }
1509
-
1510
- // ../../packages/agent-source-bundler/dist/src/index.js
1511
- import { createHash as createHash2 } from "node:crypto";
1512
- import { lstat as lstat3, readFile as readFile3, readdir } from "node:fs/promises";
1513
- import path3 from "node:path";
1514
- import ignore from "ignore";
1515
- import tar from "tar-stream";
1516
- var MAX_BUNDLE_BYTES = sourceBundleContract.max_bytes;
1517
- var SESSION_SOURCE_DIRECTORY = piSessionContract.session_directory.replace(/^\/+|\/+$/g, "");
1518
- var DEFAULT_IGNORES = [
1519
- ".git/",
1520
- "node_modules/",
1521
- ".DS_Store",
1522
- ".env",
1523
- ".env.*",
1524
- "!.env.example",
1525
- ".tonbo-cache/",
1526
- ".tonbo-system/",
1527
- ...sourceBundleContract.runtime_cache_directories.map((directory) => `${directory}/`)
1528
- ];
1529
- var EXACT_NPM_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
1530
- var GIT_COMMIT = /^[0-9a-f]{40}$/i;
1531
- function packageSource(value, index) {
1532
- if (typeof value === "string")
1533
- return value;
1534
- if (value && typeof value === "object" && "source" in value && typeof value.source === "string")
1535
- return value.source;
1536
- throw new Error(`.pi/settings.json packages[${index}] must be a source string or object.`);
1537
- }
1538
- async function validatePackageSource(root, source) {
1539
- if (source.startsWith("npm:")) {
1540
- const specifier = source.slice(4);
1541
- const separator = specifier.lastIndexOf("@");
1542
- if (separator <= 0 || !EXACT_NPM_VERSION.test(specifier.slice(separator + 1))) {
1543
- throw new Error(`PI package ${source} must pin an exact npm version, for example npm:my-agent@1.2.3.`);
1544
- }
1545
- return;
1546
- }
1547
- if (source.startsWith("git:")) {
1548
- const separator = source.lastIndexOf("@");
1549
- if (separator <= "git:".length || !GIT_COMMIT.test(source.slice(separator + 1))) {
1550
- throw new Error(`PI package ${source} must pin a full 40-character Git commit.`);
1551
- }
1552
- return;
1553
- }
1554
- if (source.startsWith("./") || source.startsWith("../")) {
1555
- const settingsDirectory = path3.join(root, ".pi");
1556
- const resolved = path3.resolve(settingsDirectory, source);
1557
- const relative = path3.relative(root, resolved);
1558
- if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative)) {
1559
- throw new Error(`Local PI package ${source} resolves outside the deployed agent.`);
1560
- }
1561
- let metadata;
833
+ async save(login) {
834
+ if (!isStoredLogin(login)) throw new Error("Refusing to store a malformed Tonbo login.");
835
+ const directory = path.dirname(this.filename);
836
+ await mkdir(directory, { mode: 448, recursive: true });
837
+ await preparePrivateDirectory(directory);
838
+ await assertExistingDestinationIsSafe(this.filename);
839
+ const temporary = path.join(
840
+ directory,
841
+ `.${path.basename(this.filename)}.${process.pid}.${randomUUID()}.tmp`
842
+ );
843
+ let handle = null;
1562
844
  try {
1563
- metadata = await lstat3(resolved);
845
+ handle = await open(temporary, "wx", 384);
846
+ await handle.writeFile(`${JSON.stringify(login, null, 2)}
847
+ `, "utf8");
848
+ await handle.sync();
849
+ await handle.close();
850
+ handle = null;
851
+ if (process.platform !== "win32") await chmod(temporary, 384);
852
+ await rename(temporary, this.filename);
1564
853
  } catch (error) {
1565
- if (error.code === "ENOENT") {
1566
- throw new Error(`Local PI package ${source} does not exist.`);
1567
- }
1568
- throw error;
1569
- }
1570
- if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
1571
- throw new Error(`Local PI package ${source} must resolve to a real agent directory.`);
854
+ await handle?.close().catch(() => void 0);
855
+ await rm(temporary, { force: true }).catch(() => void 0);
856
+ throw new Error(`Could not store Tonbo credentials at ${this.filename}.`, { cause: error });
1572
857
  }
1573
- return;
1574
858
  }
1575
- throw new Error(`PI package ${source} must use an exact npm version, a full Git commit, or a agent-local path.`);
1576
- }
1577
- async function validatePiPackages(root) {
1578
- const filename = path3.join(root, ".pi", "settings.json");
1579
- let settings;
1580
- try {
1581
- settings = JSON.parse(await readFile3(filename, "utf8"));
1582
- } catch (error) {
1583
- if (error.code === "ENOENT")
1584
- return;
1585
- throw new Error(`Could not read ${filename} as JSON.`, { cause: error });
859
+ async remove() {
860
+ await assertExistingDestinationIsSafe(this.filename);
861
+ await rm(this.filename, { force: true });
1586
862
  }
1587
- if (!settings || typeof settings !== "object" || !("packages" in settings))
1588
- return;
1589
- const packages = settings.packages;
1590
- if (!Array.isArray(packages))
1591
- throw new Error(`${filename} packages must be an array.`);
1592
- await Promise.all(packages.map((value, index) => validatePackageSource(root, packageSource(value, index))));
863
+ };
864
+ function isStoredLogin(value) {
865
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
866
+ const login = value;
867
+ return login.version === 1 && typeof login.account_id === "string" && login.account_id.length > 0 && typeof login.key_id === "string" && login.key_id.length > 0 && typeof login.key === "string" && login.key.startsWith("tbo_") && (login.expires_at === null || typeof login.expires_at === "string" && !Number.isNaN(Date.parse(login.expires_at)));
1593
868
  }
1594
- async function findDeclarationRoot(start) {
1595
- let candidate = path3.resolve(start);
1596
- for (; ; ) {
1597
- try {
1598
- if ((await lstat3(path3.join(candidate, ".tonbo"))).isFile())
1599
- return candidate;
1600
- } catch (error) {
1601
- if (error.code !== "ENOENT")
1602
- throw error;
1603
- }
1604
- const parent = path3.dirname(candidate);
1605
- if (parent === candidate) {
1606
- throw new Error(`No .tonbo declaration found above ${path3.resolve(start)}.`);
1607
- }
1608
- candidate = parent;
1609
- }
869
+ async function preparePrivateDirectory(directory) {
870
+ const stat = await lstat(directory);
871
+ if (stat.isSymbolicLink() || !stat.isDirectory())
872
+ throw new Error("Tonbo config directory must be a real directory.");
873
+ if (process.platform !== "win32") await chmod(directory, 448);
1610
874
  }
1611
- async function sourceIgnore(root) {
1612
- const matcher = ignore().add(DEFAULT_IGNORES);
875
+ async function assertExistingDestinationIsSafe(filename) {
1613
876
  try {
1614
- matcher.add(await readFile3(path3.join(root, ".tonboignore"), "utf8"));
877
+ const stat = await lstat(filename);
878
+ if (stat.isSymbolicLink() || !stat.isFile())
879
+ throw new Error("Tonbo credential path must be a regular file.");
1615
880
  } catch (error) {
1616
- if (error.code !== "ENOENT")
1617
- throw error;
1618
- }
1619
- return matcher;
1620
- }
1621
- async function collectFiles(root) {
1622
- const matcher = await sourceIgnore(root);
1623
- const files = [];
1624
- async function visit(directory, relativeDirectory) {
1625
- const entries = await readdir(directory, { withFileTypes: true });
1626
- entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
1627
- for (const entry of entries) {
1628
- const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
1629
- const ignored = matcher.ignores(relative + (entry.isDirectory() ? "/" : ""));
1630
- if (ignored)
1631
- continue;
1632
- if (relative === SESSION_SOURCE_DIRECTORY || relative.startsWith(`${SESSION_SOURCE_DIRECTORY}/`)) {
1633
- throw new Error(`Source path ${SESSION_SOURCE_DIRECTORY}/ is reserved for durable PI history. Rename it or exclude it with .tonboignore.`);
1634
- }
1635
- const absolute = path3.join(directory, entry.name);
1636
- if (entry.isDirectory()) {
1637
- await visit(absolute, relative);
1638
- continue;
1639
- }
1640
- const metadata = await lstat3(absolute);
1641
- if (!metadata.isFile()) {
1642
- throw new Error(`Source path ${relative} is not a regular file. V1 does not follow symlinks or special files.`);
1643
- }
1644
- files.push({
1645
- absolute,
1646
- mode: metadata.mode & 73 ? 493 : 420,
1647
- relative,
1648
- size: metadata.size
1649
- });
1650
- }
1651
- }
1652
- await visit(root, "");
1653
- if (!files.some((file) => file.relative === ".tonbo")) {
1654
- throw new Error("The source bundle must contain .tonbo.");
1655
- }
1656
- return files;
1657
- }
1658
- function addEntry(pack, file, contents) {
1659
- return new Promise((resolve, reject) => {
1660
- pack.entry({
1661
- gid: 0,
1662
- mode: file.mode,
1663
- mtime: /* @__PURE__ */ new Date(0),
1664
- name: file.relative,
1665
- size: contents.length,
1666
- type: "file",
1667
- uid: 0
1668
- }, contents, (error) => error ? reject(error) : resolve());
1669
- });
1670
- }
1671
- async function buildSourceBundle(root) {
1672
- const resolvedRoot = path3.resolve(root);
1673
- await validatePiPackages(resolvedRoot);
1674
- const files = await collectFiles(resolvedRoot);
1675
- const payloadBytes = files.reduce((total, file) => total + file.size, 0);
1676
- if (payloadBytes > MAX_BUNDLE_BYTES) {
1677
- throw new Error(`Source files exceed ${MAX_BUNDLE_BYTES} bytes after ignore rules.`);
1678
- }
1679
- const pack = tar.pack();
1680
- const chunks = [];
1681
- let size = 0;
1682
- pack.on("data", (chunk) => {
1683
- size += chunk.length;
1684
- if (size > MAX_BUNDLE_BYTES) {
1685
- pack.destroy(new Error(`Source bundle exceeds ${MAX_BUNDLE_BYTES} bytes after ignore rules.`));
1686
- return;
1687
- }
1688
- chunks.push(chunk);
1689
- });
1690
- const completed = new Promise((resolve, reject) => {
1691
- pack.on("end", resolve);
1692
- pack.on("error", reject);
1693
- });
1694
- for (const file of files) {
1695
- await addEntry(pack, file, await readFile3(file.absolute));
881
+ if (error.code !== "ENOENT") throw error;
1696
882
  }
1697
- pack.finalize();
1698
- await completed;
1699
- const bytes = Buffer.concat(chunks);
1700
- return {
1701
- bytes,
1702
- format: sourceBundleContract.format,
1703
- root: resolvedRoot,
1704
- sha256: createHash2("sha256").update(bytes).digest("hex"),
1705
- size_bytes: bytes.length
1706
- };
1707
883
  }
1708
-
1709
- // src/ssh-key.ts
1710
- import { createHash as createHash3 } from "node:crypto";
1711
- import { readFile as readFile4 } from "node:fs/promises";
1712
- import { homedir } from "node:os";
1713
- import { basename, join } from "node:path";
1714
- var DEFAULT_PUBLIC_KEYS = ["id_ed25519.pub", "id_ecdsa.pub", "id_rsa.pub"];
1715
- var ALGORITHMS = /* @__PURE__ */ new Set(["ssh-ed25519", "ecdsa-sha2-nistp256", "ssh-rsa"]);
1716
- function parseOpenSshPublicKey(value, label) {
1717
- const fields = value.trim().split(/\s+/);
1718
- if (fields.length < 2 || !ALGORITHMS.has(fields[0]))
1719
- throw new Error("SSH public key must be Ed25519, ECDSA P-256, or RSA.");
1720
- const blob = Buffer.from(fields[1], "base64");
1721
- if (blob.length < 32 || blob.length > 16384 || blob.toString("base64") !== fields[1])
1722
- throw new Error("SSH public key is not canonical base64.");
1723
- return {
1724
- algorithm: fields[0],
1725
- fingerprint: `SHA256:${createHash3("sha256").update(blob).digest("base64").replace(/=$/, "")}`,
1726
- keyBase64: fields[1],
1727
- label
1728
- };
1729
- }
1730
- async function readSshPublicKey(path6) {
1731
- return parseOpenSshPublicKey(await readFile4(path6, "utf8"), basename(path6, ".pub"));
1732
- }
1733
- async function readDefaultSshPublicKeys(sshDirectory = join(homedir(), ".ssh")) {
1734
- const keys = [];
1735
- for (const name of DEFAULT_PUBLIC_KEYS) {
1736
- const key = await readSshPublicKey(join(sshDirectory, name)).catch(
1737
- (error) => {
1738
- if (error.code === "ENOENT") return null;
1739
- throw error;
1740
- }
1741
- );
1742
- if (key) keys.push(key);
1743
- }
1744
- return keys;
884
+ async function assertPrivateRegularFile(filename) {
885
+ const stat = await lstat(filename);
886
+ if (stat.isSymbolicLink() || !stat.isFile())
887
+ throw new Error("Tonbo credential path must be a regular file.");
888
+ if (process.platform !== "win32" && (stat.mode & 63) !== 0)
889
+ throw new Error("Tonbo credential file must have mode 0600.");
1745
890
  }
1746
891
 
1747
892
  // src/agent-name.ts
@@ -1756,492 +901,63 @@ function validateAgentName(name) {
1756
901
  );
1757
902
  }
1758
903
  }
1759
- function agentNameSuggestion(value) {
1760
- let name = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1761
- if (!/^[a-z]/.test(name)) name = `agent-${name}`;
1762
- name = name.slice(0, AGENT_NAME_RULE.maxLength).replace(/-+$/g, "");
1763
- if (name.length < AGENT_NAME_RULE.minLength) {
1764
- name = `${name || "agent"}-agent`.slice(0, AGENT_NAME_RULE.maxLength);
1765
- }
1766
- if (RESERVED_AGENT_NAMES.has(name)) name = `${name}-agent`;
1767
- return name;
1768
- }
1769
904
 
1770
- // src/turns.ts
1771
- var TURN_OUTCOME_GRACE_MS = 3 * 60 * 1e3;
1772
- var POLL_MIN_MS = 1e3;
1773
- var POLL_MAX_MS = 1e4;
1774
- var READ_FAILURE_LIMIT = 10;
1775
- var TurnFailedError = class extends Error {
1776
- constructor(turnId, failureCode, detail) {
1777
- super(
1778
- `Turn ${turnId} failed${failureCode ? ` (${failureCode})` : ""}${detail ? `: ${detail}` : "."}`
1779
- );
1780
- this.turnId = turnId;
1781
- this.failureCode = failureCode;
1782
- this.detail = detail;
1783
- }
1784
- };
1785
- function problemCode(error) {
1786
- if (!(error instanceof HttpError)) return null;
1787
- const body = error.body && typeof error.body === "object" ? error.body : {};
1788
- const code = body.code;
1789
- return typeof code === "string" ? code : null;
1790
- }
1791
- function submissionProblem(error, turnId) {
1792
- if (!(error instanceof HttpError)) return error;
1793
- const body = error.body ?? {};
1794
- const code = problemCode(error);
1795
- if (error.status === 409 && code === "turn_failed")
1796
- return new TurnFailedError(
1797
- turnId,
1798
- typeof body.failure_code === "string" ? body.failure_code : null,
1799
- typeof body.detail === "string" ? body.detail : null
1800
- );
1801
- if (error.status === 409 && code === "agent_busy")
1802
- return new Error(
1803
- `Turn ${turnId} was not admitted: the Agent already has the most Turns waiting to start (agent_busy). Try again with a new Turn later.`,
1804
- { cause: error }
1805
- );
1806
- return new Error(`Turn ${turnId} was not admitted: ${error.message}`, { cause: error });
1807
- }
1808
- function isUnauthenticated(error) {
1809
- return error instanceof HttpError && error.status === 401;
1810
- }
1811
- function isNotFound(error) {
1812
- return error instanceof HttpError && error.status === 404;
1813
- }
1814
- function isTransient(error) {
1815
- if (error instanceof HttpError) return error.status === 429 || error.status >= 500;
1816
- return error instanceof TypeError;
1817
- }
1818
- function turnWaitBound(turn) {
1819
- const deadline = turn.deadline_at ?? turn.start_deadline_at;
1820
- if (!deadline) return null;
1821
- const at = Date.parse(deadline);
1822
- return Number.isFinite(at) ? at + TURN_OUTCOME_GRACE_MS : null;
905
+ // src/format.ts
906
+ function table(headings, rows) {
907
+ const widths = headings.map(
908
+ (heading, column) => Math.max(heading.length, ...rows.map((row) => (row[column] ?? "").length))
909
+ );
910
+ return [headings, ...rows].map(
911
+ (row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd()
912
+ ).join("\n");
913
+ }
914
+ function fields(pairs) {
915
+ const width = Math.max(...pairs.map(([label]) => label.length + 1));
916
+ return pairs.map(([label, value]) => `${`${label}:`.padEnd(width)} ${value}`).join("\n");
917
+ }
918
+ var USD = new Intl.NumberFormat("en-US", {
919
+ style: "currency",
920
+ currency: "USD",
921
+ minimumFractionDigits: 2,
922
+ maximumFractionDigits: 6
923
+ });
924
+ function usd(micros) {
925
+ return USD.format(micros / 1e6);
1823
926
  }
1824
- async function followTurn({
1825
- now,
1826
- read: read2,
1827
- resume,
1828
- sleep,
1829
- turnId
1830
- }) {
1831
- let delay = POLL_MIN_MS;
1832
- let failures = 0;
1833
- for (; ; ) {
1834
- let state;
1835
- try {
1836
- state = await read2();
1837
- failures = 0;
1838
- } catch (error) {
1839
- const transient = isTransient(error);
1840
- if (transient && (failures += 1) < READ_FAILURE_LIMIT) {
1841
- await sleep(POLL_MAX_MS);
1842
- continue;
1843
- }
1844
- if (!transient && !(error instanceof HttpError)) throw error;
1845
- throw new Error(
1846
- `${error instanceof Error ? error.message : String(error)} Waiting stopped, which does not stop the Turn; ${resume}`,
1847
- { cause: error }
1848
- );
1849
- }
1850
- if ("result" in state) return state.result;
1851
- const bound = state.pending ? turnWaitBound(state.pending) : null;
1852
- if (bound !== null && now() > bound)
1853
- throw new Error(
1854
- `Turn ${turnId} is still pending after its deadline, and the platform has not recorded its outcome. Waiting stopped, which does not stop the Turn; ${resume}`
1855
- );
1856
- await sleep(delay);
1857
- delay = Math.min(delay * 2, POLL_MAX_MS);
1858
- }
927
+ function when(timestamp, absent = "never") {
928
+ return timestamp ?? absent;
1859
929
  }
1860
930
 
1861
931
  // src/commands.ts
1862
- async function resolveAgent(deps, minimumTokenLifetimeSeconds) {
1863
- const root = await findDeclarationRoot(deps.cwd());
1864
- const declaration = await loadDeclaration(root);
1865
- if (!declaration.agent) {
1866
- throw new Error("No Agent is configured in .tonbo. Run `tonbo agent use <agent>` first.");
1867
- }
1868
- const oauthToken = await deps.auth.accessToken(minimumTokenLifetimeSeconds);
1869
- return {
1870
- declaration,
1871
- oauthToken,
1872
- agent: selectAgent(await deps.api.listAgents(oauthToken), declaration.agent),
1873
- root
1874
- };
1875
- }
1876
- function selectAgent(agents, selector) {
1877
- const normalized = selector.toLowerCase();
1878
- const matches = agents.filter(
1879
- (agent) => agent.id === selector || agent.name === normalized || agent.publicHostname === normalized
1880
- );
1881
- if (matches.length === 0) throw new Error(`Agent ${selector} was not found in your account.`);
1882
- if (matches.length > 1)
1883
- throw new Error(`Agent name ${selector} is ambiguous; use its full hostname or ID.`);
1884
- if (matches[0].status !== "active") throw new Error(`Agent ${selector} is not active.`);
1885
- return matches[0];
932
+ function print(deps, json, data, human) {
933
+ deps.output(json ? JSON.stringify(data) : human);
1886
934
  }
1887
- async function initCommand(deps, options) {
1888
- const root = deps.cwd();
1889
- const exists = await declarationExists(root);
1890
- let existingDeclaration;
1891
- if (exists) {
1892
- existingDeclaration = await loadDeclaration(root).catch(() => void 0);
1893
- }
1894
- const interactive = deps.interactive();
1895
- const overwrite = options.force === true || exists && interactive;
1896
- if (exists && !overwrite)
1897
- throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1898
- if (options.region !== void 0 && !interactive)
1899
- throw new Error(
1900
- "--region applies only to an agent that interactive init creates. Run `tonbo agent create <name> --region <region>` instead."
1901
- );
1902
- const inspection = await deps.inspectSource(root);
1903
- let harness = options.harness ?? (existingDeclaration ? "pi" : void 0);
1904
- if (harness !== void 0 && harness !== "pi") {
1905
- throw new Error("--harness must name a supported Harness: pi.");
1906
- }
1907
- let driver = options.driver ?? existingDeclaration?.harness.driver.kind;
1908
- if (driver !== void 0 && driver !== "native" && driver !== "command") {
1909
- throw new Error("--driver must be native or command.");
1910
- }
1911
- if (!harness) {
1912
- if (inspection.harness.state === "identified") harness = inspection.harness.id;
1913
- else if (!interactive) {
1914
- throw new Error("No Harness-specific configuration found. Pass --harness pi.");
1915
- } else {
1916
- deps.output({
1917
- message: "No Harness-specific configuration was found in this agent."
1918
- });
1919
- harness = await selectHarness(deps);
1920
- }
1921
- }
1922
- if (!driver) {
1923
- if (!interactive) {
1924
- throw new Error("PI execution mode is required. Pass --driver native or --driver command.");
1925
- }
1926
- if (inspection.pi.settingsFound) {
1927
- const packageSummary = inspection.pi.packageCount === 0 ? "Found PI configuration at .pi/settings.json." : `Found PI configuration at .pi/settings.json with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}.`;
1928
- deps.output({ message: packageSummary });
1929
- }
1930
- driver = "native";
1931
- }
1932
- if (driver === "native" && (options.agentEntry || options.buildCommand)) {
1933
- throw new Error("--agent-entry and --build-command require --driver command.");
1934
- }
1935
- const existingEntry = existingDeclaration?.harness.driver.kind === "command" && existingDeclaration.harness.driver.command[0] === "node" ? existingDeclaration.harness.driver.command[1] : void 0;
1936
- let entry = options.agentEntry?.trim() || existingEntry;
1937
- if (driver === "command" && !entry && interactive) {
1938
- entry = (await deps.prompt("PI SDK entry file [dist/agent.mjs]: ")).trim();
1939
- }
1940
- entry ||= "dist/agent.mjs";
1941
- let model = options.model?.trim() || existingDeclaration?.inference.model || DEFAULT_INFERENCE_MODEL;
1942
- let buildCommand = driver === "command" ? options.buildCommand ?? existingDeclaration?.build?.command ?? ["npm", "run", "build"] : void 0;
1943
- let agentPlan = existingDeclaration?.agent ? {
1944
- kind: "existing",
1945
- publicHostname: existingDeclaration.agent
1946
- } : { kind: "later" };
1947
- if (interactive && !existingDeclaration?.agent) {
1948
- agentPlan = await selectInitAgent(deps, root, options.region);
1949
- }
1950
- if (interactive) {
1951
- while (true) {
1952
- deps.output({
1953
- message: renderInitSummary(agentPlan, harness, driver, model, entry, buildCommand)
1954
- });
1955
- const saveLabel = initSaveLabel(exists, agentPlan);
1956
- const action = await deps.select(`${saveLabel} with this configuration?`, [
1957
- {
1958
- name: saveLabel,
1959
- value: "save"
1960
- },
1961
- {
1962
- description: "Change the agent, Harness, execution mode, model, or SDK entrypoint.",
1963
- name: "Edit configuration",
1964
- value: "edit"
1965
- },
1966
- { name: "Cancel", value: "cancel" }
1967
- ]);
1968
- if (action === "cancel") {
1969
- deps.output({
1970
- message: exists ? `Cancelled. Kept existing ${DECLARATION_FILENAME}.` : `Cancelled. Did not create ${DECLARATION_FILENAME}.`
1971
- });
1972
- return;
1973
- }
1974
- if (action === "save") break;
1975
- const setting = await deps.select("What would you like to change?", [
1976
- {
1977
- description: initAgentSummary(agentPlan),
1978
- name: "Agent",
1979
- value: "agent"
1980
- },
1981
- { description: harnessName(harness), name: "Harness", value: "harness" },
1982
- {
1983
- description: driver === "native" ? "PI CLI (`pi`)" : "PI SDK app",
1984
- name: "Execution mode",
1985
- value: "driver"
1986
- },
1987
- { description: model, name: "Inference model", value: "model" },
1988
- ...driver === "command" ? [{ description: entry, name: "PI SDK entry file", value: "entry" }] : [],
1989
- { name: "Back to review", value: "back" }
1990
- ]);
1991
- if (setting === "agent") agentPlan = await selectInitAgent(deps, root, options.region);
1992
- if (setting === "harness") harness = await selectHarness(deps);
1993
- if (setting === "driver") {
1994
- driver = await deps.select(
1995
- "How should Tonbo start PI?",
1996
- piTargetChoices(inspection.pi.settingsFound)
1997
- );
1998
- if (driver === "command") {
1999
- const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
2000
- if (answer) entry = answer;
2001
- buildCommand = options.buildCommand ?? ["npm", "run", "build"];
2002
- } else {
2003
- buildCommand = void 0;
2004
- }
2005
- }
2006
- if (setting === "model") {
2007
- const answer = (await deps.prompt(`Inference model [${model}]: `)).trim();
2008
- if (answer) model = answer;
2009
- }
2010
- if (setting === "entry") {
2011
- const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
2012
- if (answer) entry = answer;
2013
- }
2014
- }
2015
- }
2016
- let agentHostname = agentPlan.kind === "existing" ? agentPlan.publicHostname : void 0;
2017
- let createdAgent;
2018
- if (agentPlan.kind === "create") {
2019
- const oauthToken = await deps.auth.accessToken();
2020
- createdAgent = await deps.api.createAgent(oauthToken, {
2021
- name: agentPlan.name,
2022
- organizationId: agentPlan.organizationId,
2023
- region: agentPlan.region
2024
- });
2025
- agentHostname = createdAgent.publicHostname;
2026
- }
2027
- const configuredDeclaration = createDeclaration(
2028
- model,
2029
- driver === "native" ? { kind: "native" } : { kind: "command", protocol: "pi-rpc-v1", command: ["node", entry] },
2030
- buildCommand,
2031
- agentHostname
2032
- );
2033
- const declaration = parseDeclaration({
2034
- ...configuredDeclaration,
2035
- harness: {
2036
- ...configuredDeclaration.harness,
2037
- ...existingDeclaration?.harness.secrets ? { secrets: existingDeclaration.harness.secrets } : {}
2038
- },
2039
- ...existingDeclaration?.service ? { service: existingDeclaration.service } : {}
2040
- });
2041
- try {
2042
- await saveDeclaration(root, declaration, overwrite);
2043
- } catch (error) {
2044
- if (createdAgent) {
2045
- throw new Error(
2046
- `Created agent ${createdAgent.name}, but could not write ${DECLARATION_FILENAME}. Fix the file and run \`tonbo agent use ${createdAgent.publicHostname}\`.`,
2047
- { cause: error }
2048
- );
2049
- }
2050
- throw error;
2051
- }
2052
- const driverSummary = driver === "command" ? `PI SDK app
2053
- Build: ${buildCommand?.join(" ")}
2054
- Entrypoint: node ${entry}` : inspection.pi.packageCount > 0 ? `PI CLI with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}` : "PI CLI";
2055
- const agentSummary = createdAgent ? `Agent: ${createdAgent.name} (${createdAgent.publicHostname})
2056
- Organization: ${createdAgent.organizationName}
2057
- Region: ${createdAgent.region}
2058
- Application: https://${createdAgent.publicHostname}
2059
- SSH after deploy: ssh ${createdAgent.sshDestination}` : agentPlan.kind === "existing" ? `Agent: ${agentPlan.name ? `${agentPlan.name} (${agentPlan.publicHostname})` : agentPlan.publicHostname}${agentPlan.organizationName ? `
2060
- Organization: ${agentPlan.organizationName}` : ""}${agentPlan.region ? `
2061
- Region: ${agentPlan.region}` : ""}${agentPlan.sshDestination ? `
2062
- SSH after deploy: ssh ${agentPlan.sshDestination}` : ""}` : "Next: tonbo agent create <name>";
2063
- deps.output({
2064
- message: `${createdAgent ? `Created agent ${createdAgent.name} and` : exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
2065
- ${agentSummary}
2066
- Harness: PI
2067
- Mode: ${driverSummary}${agentPlan.kind === "later" ? "" : "\nNext: tonbo deploy"}`,
2068
- declaration,
2069
- path: path4.join(root, DECLARATION_FILENAME)
2070
- });
935
+ function printPage(deps, json, page, human) {
936
+ print(deps, json, page.data, human(page.data));
937
+ if (page.next_cursor) deps.notice(`More results: run again with --cursor ${page.next_cursor}`);
2071
938
  }
2072
- function initSaveLabel(exists, agent) {
2073
- if (agent.kind === "create") {
2074
- return exists ? `Create agent and replace ${DECLARATION_FILENAME}` : `Create agent and ${DECLARATION_FILENAME}`;
2075
- }
2076
- return exists ? `Replace ${DECLARATION_FILENAME}` : `Create ${DECLARATION_FILENAME}`;
939
+ async function confirm(deps, yes, action, question, expected) {
940
+ if (yes) return;
941
+ if (!deps.interactive())
942
+ throw new Error(`Refusing to ${action} without confirmation: pass --yes.`);
943
+ if (await deps.ask(await question()) !== expected)
944
+ throw new Error("Cancelled; nothing was changed.");
2077
945
  }
2078
- async function selectInitAgent(deps, root, requestedRegion) {
2079
- for (; ; ) {
2080
- const action = await deps.select("How should this directory connect to Tonbo?", [
2081
- {
2082
- description: "Create it only after you confirm the complete configuration.",
2083
- name: "Create a new agent",
2084
- value: "create"
2085
- },
2086
- {
2087
- description: "Bind this source tree to one agent you can access.",
2088
- name: "Use an existing agent",
2089
- value: "existing"
2090
- },
2091
- {
2092
- description: "Write an unbound declaration and configure its agent later.",
2093
- name: "Set up later",
2094
- value: "later"
2095
- }
2096
- ]);
2097
- if (action === "later") return { kind: "later" };
2098
- if (action === "create") {
2099
- const organization = await selectInitOrganization(deps);
2100
- const region = requestedRegion ?? await selectAgentRegion(deps);
2101
- const defaultName = agentNameSuggestion(path4.basename(root)) || "tonbo-agent";
2102
- for (; ; ) {
2103
- const answer = (await deps.prompt(`Agent name [${defaultName}]: `)).trim().toLowerCase();
2104
- const name = answer || defaultName;
2105
- try {
2106
- validateAgentName(name);
2107
- return {
2108
- kind: "create",
2109
- name,
2110
- organizationId: organization?.id,
2111
- organizationName: organization?.name,
2112
- ...region ? { region } : {}
2113
- };
2114
- } catch (error) {
2115
- deps.output({
2116
- message: error instanceof Error ? error.message : "Agent name is invalid."
2117
- });
2118
- }
2119
- }
2120
- }
2121
- const oauthToken = await deps.auth.accessToken();
2122
- const agents = (await deps.api.listAgents(oauthToken)).filter(
2123
- (agent2) => agent2.status === "active"
946
+ async function loginCommand(deps, options = {}) {
947
+ try {
948
+ const login = await deps.auth.login(
949
+ (event) => reportLoginProgress(deps.progress, event),
950
+ options.account
2124
951
  );
2125
- if (agents.length === 0) {
2126
- deps.output({ message: "No active agents are available. Create a new agent instead." });
2127
- continue;
2128
- }
2129
- const id = await deps.select(
2130
- "Which agent should this directory use?",
2131
- agents.map((agent2) => ({
2132
- name: agent2.name,
2133
- description: `Organization: ${agent2.organizationName} \xB7 ${agent2.region} \xB7 ${agent2.publicHostname} \xB7 ${agent2.id}`,
2134
- value: agent2.id
2135
- }))
952
+ deps.output(
953
+ `Logged in to account ${login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`
2136
954
  );
2137
- const agent = selectAgent(agents, id);
2138
- return {
2139
- kind: "existing",
2140
- name: agent.name,
2141
- organizationName: agent.organizationName,
2142
- publicHostname: agent.publicHostname,
2143
- region: agent.region,
2144
- sshDestination: agent.sshDestination
2145
- };
2146
- }
2147
- }
2148
- async function selectAgentRegion(deps) {
2149
- const { regions } = await deps.api.listRegions(await deps.auth.accessToken());
2150
- const open3 = regions.filter((region) => region.allocation_enabled);
2151
- if (open3.length < 2) return void 0;
2152
- return deps.select(
2153
- "Which region should hold the new agent? It stays there permanently.",
2154
- open3.map((region) => ({ description: region.id, name: region.display_name, value: region.id }))
2155
- );
2156
- }
2157
- function initAgentSummary(agent) {
2158
- if (agent.kind === "create")
2159
- return `Create ${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""}${agent.region ? `, region ${agent.region}` : ""} (permanent address assigned after creation)`;
2160
- if (agent.kind === "existing") {
2161
- return agent.name ? `${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""} (${agent.publicHostname})` : `Keep ${agent.publicHostname}`;
2162
- }
2163
- return "Set up later";
2164
- }
2165
- async function selectInitOrganization(deps) {
2166
- const oauthToken = await deps.auth.accessToken();
2167
- const organizations = (await deps.api.listOrganizations(oauthToken)).filter(
2168
- (organization) => organization.role !== "member"
2169
- );
2170
- if (organizations.length === 0)
2171
- throw new Error("No organization lets you create a agent. Ask an admin or owner.");
2172
- if (organizations.length === 1) return organizations[0];
2173
- const id = await deps.select(
2174
- "Which organization should own the new agent?",
2175
- organizations.map((organization) => ({
2176
- description: organization.role,
2177
- name: organization.name,
2178
- value: organization.id
2179
- }))
2180
- );
2181
- return organizations.find((organization) => organization.id === id) ?? organizations[0];
2182
- }
2183
- async function selectHarness(deps) {
2184
- return await deps.select(
2185
- "Which supported Harness should Tonbo use?",
2186
- supportedHarnesses.map((supported) => ({
2187
- description: `Run this agent with the ${supported.command} command.`,
2188
- name: `${supported.name} (\`${supported.command}\`)`,
2189
- value: supported.id
2190
- }))
2191
- );
2192
- }
2193
- function harnessName(harness) {
2194
- const supported = supportedHarnesses.find((candidate) => candidate.id === harness);
2195
- return supported?.name ?? harness;
2196
- }
2197
- function renderInitSummary(agent, harness, driver, model, entry, buildCommand) {
2198
- const lines = [
2199
- "",
2200
- "Tonbo Agent configuration:",
2201
- ` Agent: ${initAgentSummary(agent)}`,
2202
- ` Harness: ${harnessName(harness)}`,
2203
- ` Execution: ${driver === "native" ? "PI CLI (`pi`)" : "PI SDK app"}`,
2204
- ` Model: ${model}`
2205
- ];
2206
- if (driver === "command") {
2207
- lines.push(` Build: ${buildCommand?.join(" ") ?? "none"}`);
2208
- lines.push(` Entrypoint: node ${entry}`);
2209
- }
2210
- return lines.join("\n");
2211
- }
2212
- function piTargetChoices(settingsFound) {
2213
- const choices = {
2214
- command: {
2215
- description: "Run a custom Node.js entrypoint built on the PI SDK.",
2216
- name: "PI SDK app",
2217
- value: "command"
2218
- },
2219
- native: {
2220
- description: settingsFound ? "Run pi directly and load the detected agent-local PI configuration." : "Run pi directly with AGENTS.md and optional agent-local .pi resources.",
2221
- name: "PI CLI (`pi`)",
2222
- value: "native"
2223
- }
2224
- };
2225
- return supportedExecutionTargets.filter((target) => target.harness === "pi").map((target) => choices[target.driver]);
2226
- }
2227
- async function loginCommand(deps) {
2228
- try {
2229
- const tokens = await deps.auth.login((event) => reportLoginProgress(deps.progress, event));
2230
- deps.progress.start("Preparing SSH access");
2231
- const keys = await deps.defaultSshPublicKeys();
2232
- await Promise.all(keys.map((key) => deps.api.registerSshKey(tokens.access_token, key)));
2233
- deps.progress.succeed("SSH access ready");
2234
- deps.output({ message: "Logged in to Tonbo." });
2235
955
  } catch (error) {
2236
956
  deps.progress.fail();
2237
957
  throw error;
2238
958
  }
2239
959
  }
2240
960
  var LOGIN_PROGRESS_MESSAGES = {
2241
- "account-config": {
2242
- started: "Connecting to Tonbo",
2243
- completed: "Connected to Tonbo"
2244
- },
2245
961
  "callback-server": {
2246
962
  started: "Starting local browser callback",
2247
963
  completed: "Local browser callback ready"
@@ -2251,20 +967,20 @@ var LOGIN_PROGRESS_MESSAGES = {
2251
967
  completed: "Browser opened"
2252
968
  },
2253
969
  "browser-authorization": {
2254
- started: "Waiting for browser authorization",
2255
- completed: "Browser authorization received"
970
+ started: "Waiting for browser approval",
971
+ completed: "Browser approval received"
2256
972
  },
2257
973
  "callback-close": {
2258
974
  started: "Closing local browser callback",
2259
975
  completed: "Local browser callback closed"
2260
976
  },
2261
- "token-exchange": {
2262
- started: "Exchanging authorization code",
2263
- completed: "Authorization code exchanged"
977
+ "key-redemption": {
978
+ started: "Receiving the CLI key",
979
+ completed: "CLI key received"
2264
980
  },
2265
981
  "credential-store": {
2266
- started: "Saving login session",
2267
- completed: "Login session saved"
982
+ started: "Saving login",
983
+ completed: "Login saved"
2268
984
  }
2269
985
  };
2270
986
  function reportLoginProgress(progress, event) {
@@ -2272,728 +988,352 @@ function reportLoginProgress(progress, event) {
2272
988
  if (event.status === "started") progress.start(messages.started);
2273
989
  else progress.succeed(messages.completed);
2274
990
  }
2275
- async function sshKeyAddCommand(deps, path6) {
2276
- const key = await readSshPublicKey(path6);
2277
- const oauthToken = await deps.auth.accessToken();
2278
- await deps.api.registerSshKey(oauthToken, key);
2279
- deps.output({ message: `Registered SSH key ${key.fingerprint}.`, key });
2280
- }
2281
- async function sshKeyRemoveCommand(deps, fingerprint) {
2282
- const oauthToken = await deps.auth.accessToken();
2283
- const key = await deps.api.revokeSshKey(oauthToken, fingerprint);
2284
- deps.output({ message: `Revoked SSH key ${key.fingerprint}.`, key });
2285
- }
2286
- async function agentUseCommand(deps, selector, force = false) {
2287
- const root = await findDeclarationRoot(deps.cwd());
2288
- const declaration = await loadDeclaration(root);
2289
- const oauthToken = await deps.auth.accessToken();
2290
- const agents = await deps.api.listAgents(oauthToken);
2291
- const agent = selectAgent(agents, selector);
2292
- if (declaration.agent === agent.publicHostname) {
2293
- deps.output({
2294
- message: `.tonbo already uses agent ${agent.publicHostname}.`,
2295
- agent
2296
- });
991
+ async function logoutCommand(deps) {
992
+ let login;
993
+ try {
994
+ login = await deps.credentials.load();
995
+ } catch (error) {
996
+ if (!(error instanceof UnusableLoginError)) throw error;
997
+ await deps.credentials.remove();
998
+ deps.output("Removed a saved login this CLI cannot use; it had no key to revoke.");
2297
999
  return;
2298
1000
  }
2299
- if (declaration.agent && !force) {
2300
- const current = agents.find((candidate) => candidate.publicHostname === declaration.agent);
2301
- const currentLabel = current?.publicHostname ?? declaration.agent;
2302
- if (!deps.interactive()) {
2303
- throw new Error(
2304
- `.tonbo already uses agent ${currentLabel}. Pass --force to rebind it to ${agent.publicHostname}.`
2305
- );
2306
- }
2307
- const action = await deps.select(
2308
- `Rebind .tonbo from ${currentLabel} to ${agent.publicHostname}?`,
2309
- [
2310
- { name: `Rebind to ${agent.publicHostname}`, value: "rebind" },
2311
- { name: "Cancel", value: "cancel" }
2312
- ]
2313
- );
2314
- if (action !== "rebind") {
2315
- deps.output({ message: `Cancelled. .tonbo still uses agent ${currentLabel}.` });
2316
- return;
2317
- }
2318
- }
2319
- await bindDeclarationAgent(root, agent.publicHostname);
2320
- deps.output({ message: `Bound .tonbo to agent ${agent.publicHostname}.`, agent });
2321
- }
2322
- async function agentCreateCommand(deps, name, options = {}) {
2323
- const root = await findDeclarationRoot(deps.cwd());
2324
- const declaration = await loadDeclaration(root);
2325
- if (declaration.agent) {
2326
- throw new Error(
2327
- `.tonbo is already bound to agent ${declaration.agent}. Run \`tonbo agent use <agent>\` to review a rebind.`
2328
- );
1001
+ if (!login) {
1002
+ deps.output("Not logged in.");
1003
+ return;
2329
1004
  }
2330
- const oauthToken = await deps.auth.accessToken();
2331
- validateAgentName(name);
2332
- const requestId = options.requestId ?? randomUUID3();
2333
- const region = options.region ?? (deps.interactive() ? await selectAgentRegion(deps) : void 0);
2334
- const agent = await deps.api.createAgent(oauthToken, {
2335
- name,
2336
- organizationId: options.account,
2337
- region,
2338
- requestId
2339
- });
1005
+ let failure;
2340
1006
  try {
2341
- await bindDeclarationAgent(root, agent.publicHostname);
1007
+ await deps.api.revokeApiKey(login.key, login.key_id);
2342
1008
  } catch (error) {
2343
- throw new Error(
2344
- `Created agent ${agent.name}, but could not bind ${DECLARATION_FILENAME}. Fix the file and run \`tonbo agent use ${agent.publicHostname}\`.`,
2345
- { cause: error }
2346
- );
1009
+ if (!(error instanceof HttpError && (error.status === 401 || error.status === 404)))
1010
+ failure = error instanceof Error ? error : new Error("The revocation failed.");
2347
1011
  }
2348
- deps.output({
2349
- message: [
2350
- `Created agent ${agent.name} and bound it in .tonbo.`,
2351
- `Organization: ${agent.organizationName}`,
2352
- `Region: ${agent.region}`,
2353
- `Application: https://${agent.publicHostname}`,
2354
- `SSH after deploy: ssh ${agent.sshDestination}`
2355
- ].join("\n"),
2356
- agent,
2357
- requestId
2358
- });
2359
- }
2360
- async function deployCommand(deps, options = { promote: true }) {
2361
- const { declaration, oauthToken, agent, root } = await resolveAgent(deps);
2362
- if (declaration.build) await runBuildCommand(root, declaration.build.command);
2363
- const source = await buildSourceBundle(root);
2364
- const origin = { actor: "cli", git: await deps.gitProvenance(root) };
2365
- const managementToken = await deps.api.exchangeManagementToken(oauthToken, agent.id);
2366
- const result = await deps.api.deploy({
2367
- bundle: source,
2368
- origin,
2369
- agentId: agent.id,
2370
- promote: options.promote,
2371
- spec: buildDeploymentSpec(declaration, source),
2372
- token: managementToken
2373
- });
2374
- const { deployment } = result;
2375
- const shortId = shortDeploymentId(deployment.id);
2376
- const lines = [
2377
- options.promote ? `Deployed agent ${agent.name}.` : `Created Deployment ${deploymentName(deployment)} (${shortId}) for agent ${agent.name} without promoting it.`,
2378
- `Organization: ${agent.organizationName}`,
2379
- `Deployment: ${deploymentName(deployment)} (${shortId})`,
2380
- `Source: ${deploymentSourceLabel(deployment)}`,
2381
- `Production: ${options.promote ? productionStateLabel(deployment) : `not promoted; run \`tonbo deployments promote ${shortId}\` to move Production here`}`,
2382
- `Application: https://${agent.publicHostname}`
2383
- ];
2384
- if (result.unchanged) {
2385
- lines.push(
2386
- `Contents are identical to the ${options.promote ? "previous" : "current"} Production Deployment; the running Agent does not change.`
2387
- );
2388
- }
2389
- if (options.promote) {
2390
- lines.push(
2391
- "",
2392
- "Agent process: starts with the first turn and stays warm while the runtime is active.",
2393
- "",
2394
- "Start the Agent:",
2395
- ' tonbo run "<prompt>"',
2396
- "",
2397
- "Connect with SSH:",
2398
- ` ssh ${agent.sshDestination}`
1012
+ await deps.credentials.remove();
1013
+ if (failure)
1014
+ throw new Error(
1015
+ `Removed the saved login, but could not revoke its key ${login.key_id}: ${failure.message} Revoke it under API keys in the Tonbo console.`,
1016
+ { cause: failure }
2399
1017
  );
2400
- }
2401
- deps.output({ message: lines.join("\n"), agent, ...result });
2402
- }
2403
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2404
- var ADMISSION_WAIT_MS = 15 * 60 * 1e3;
2405
- var RESUBMIT_MS = 5e3;
2406
- var SUBMISSION_TOKEN_LIFETIME_SECONDS = 240;
2407
- function managementTokens(deps, agentId, oauthToken) {
2408
- let current = deps.api.exchangeManagementToken(oauthToken, agentId);
2409
- return async function withToken(call) {
2410
- const token = await current;
2411
- try {
2412
- return await call(token);
2413
- } catch (error) {
2414
- if (!isUnauthenticated(error)) throw error;
2415
- current = deps.auth.accessToken().then((oauth) => deps.api.exchangeManagementToken(oauth, agentId));
2416
- return call(await current);
2417
- }
2418
- };
2419
- }
2420
- function waitCommand(turnId, sessionId) {
2421
- return `tonbo turn wait ${turnId} --session ${sessionId}`;
1018
+ deps.output("Logged out. The CLI key is revoked.");
2422
1019
  }
2423
- function turnNotFound(turnId, sessionId, agentName) {
2424
- return new Error(`Turn ${turnId} was not found in Session ${sessionId} of agent ${agentName}.`);
2425
- }
2426
- function sessionTurn(turn) {
2427
- return {
2428
- assistant_text: turn.assistant_text ?? "",
2429
- session_id: turn.session_id,
2430
- turn_id: turn.id
2431
- };
2432
- }
2433
- function holdSubmission(deps, withToken, submission) {
2434
- const controller = new AbortController();
2435
- const held = { controller, settled: Promise.resolve() };
2436
- held.settled = withToken(
2437
- (token) => deps.api.submitTurn({ ...submission, signal: controller.signal, token })
2438
- ).then(
2439
- (value) => {
2440
- held.answer = { value };
2441
- },
2442
- (error) => {
2443
- held.answer = { error };
2444
- }
1020
+ async function regionListCommand(deps, options = {}) {
1021
+ const regions = await deps.api.listRegions(await deps.auth.apiKey());
1022
+ print(
1023
+ deps,
1024
+ options.json,
1025
+ regions,
1026
+ regions.length ? table(
1027
+ ["REGION", "URL"],
1028
+ regions.map((region) => [region.id, region.url])
1029
+ ) : "No region accepts new resources."
2445
1030
  );
2446
- return held;
2447
- }
2448
- async function submitAndFollow(deps, withToken, submission, { agentName, outcome }) {
2449
- const { agentId, sessionId, turnId } = submission;
2450
- const now = deps.now ?? Date.now;
2451
- const sleep = deps.sleep ?? pause;
2452
- const submittedAt = now();
2453
- const state = {
2454
- admitted: false,
2455
- announced: false,
2456
- held: holdSubmission(deps, withToken, submission),
2457
- resubmitAt: 0,
2458
- unanswered: null
2459
- };
2460
- const read2 = async () => {
2461
- const answer = state.held?.answer;
2462
- if (answer) {
2463
- state.held = null;
2464
- if ("error" in answer) {
2465
- const problem = submissionProblem(answer.error, turnId);
2466
- if (problem instanceof TurnFailedError) throw problem;
2467
- if (!state.admitted) {
2468
- if (!isTransient(answer.error)) throw problem;
2469
- state.unanswered = answer.error;
2470
- state.resubmitAt = now() + RESUBMIT_MS;
2471
- }
2472
- } else if (answer.value.state === "completed") {
2473
- return { result: { completed: answer.value.turn } };
2474
- }
2475
- }
2476
- let turn = null;
2477
- try {
2478
- turn = await withToken((token) => deps.api.getTurn(agentId, sessionId, turnId, token));
2479
- } catch (error) {
2480
- if (!isNotFound(error)) throw error;
2481
- if (state.admitted) throw turnNotFound(turnId, sessionId, agentName);
2482
- }
2483
- if (turn) {
2484
- state.admitted = true;
2485
- if (turn.status === "completed") return { result: { completed: sessionTurn(turn) } };
2486
- if (turn.status === "failed")
2487
- throw new TurnFailedError(turn.id, turn.failure_code, turn.failure);
2488
- if (!outcome) return { result: { admitted: turn } };
2489
- if (!state.announced) {
2490
- state.announced = true;
2491
- deps.progress.start(
2492
- `Turn ${turnId} is running on the platform in Session ${sessionId}; waiting for its result. Interrupting the wait does not stop the Turn.`
2493
- );
2494
- }
2495
- return { pending: turn };
2496
- }
2497
- if (now() - submittedAt > ADMISSION_WAIT_MS) {
2498
- const cause = state.unanswered;
2499
- throw new Error(
2500
- `Turn ${turnId} was not admitted within ${ADMISSION_WAIT_MS / 6e4} minutes${cause instanceof Error ? ` (the last submission got no answer: ${cause.message})` : ""}. Waiting stopped; if the platform admits it later, \`${waitCommand(turnId, sessionId)}\` follows it.`,
2501
- { cause }
2502
- );
2503
- }
2504
- if (!state.held && now() >= state.resubmitAt)
2505
- state.held = holdSubmission(deps, withToken, submission);
2506
- return { pending: null };
2507
- };
2508
- try {
2509
- const followed = await followTurn({
2510
- now,
2511
- read: read2,
2512
- resume: `wait for it with \`${waitCommand(turnId, sessionId)}\`.`,
2513
- // An answer to the held submission ends a pause at once.
2514
- sleep: async (ms) => {
2515
- const held = state.held;
2516
- if (!held) return sleep(ms);
2517
- const wake = new AbortController();
2518
- await Promise.race([sleep(ms, wake.signal), held.settled]);
2519
- wake.abort();
2520
- },
2521
- turnId
2522
- });
2523
- if (state.announced) deps.progress.succeed(`Turn ${turnId} completed.`);
2524
- return followed;
2525
- } catch (error) {
2526
- if (state.announced) deps.progress.fail();
2527
- throw error;
2528
- } finally {
2529
- state.held?.controller.abort();
2530
- }
2531
1031
  }
2532
- function pause(ms, signal) {
2533
- return new Promise((resolve) => {
2534
- const timer = setTimeout(resolve, ms);
2535
- signal?.addEventListener(
2536
- "abort",
2537
- () => {
2538
- clearTimeout(timer);
2539
- resolve();
2540
- },
2541
- { once: true }
2542
- );
2543
- });
1032
+ async function selectAgentRegion(deps, key) {
1033
+ const regions = await deps.api.listRegions(key);
1034
+ if (regions.length < 2) return void 0;
1035
+ return deps.select(
1036
+ "Which region should hold the new agent? It stays there permanently.",
1037
+ regions.map((region) => ({ name: region.id, value: region.id }))
1038
+ );
2544
1039
  }
2545
- async function runCommand(deps, prompt, options) {
2546
- const { oauthToken, agent } = await resolveAgent(deps, SUBMISSION_TOKEN_LIFETIME_SECONDS);
2547
- const withToken = managementTokens(deps, agent.id, oauthToken);
2548
- const { production, session } = await withToken(
2549
- (token) => deps.api.openSession({ agentId: agent.id, sessionId: options.session, token })
1040
+ async function agentCreateCommand(deps, name, options = {}) {
1041
+ validateAgentName(name);
1042
+ const key = await deps.auth.apiKey();
1043
+ const region = options.region ?? (!options.json && deps.interactive() ? await selectAgentRegion(deps, key) : void 0);
1044
+ const agent = await deps.api.createAgent(
1045
+ key,
1046
+ { name, region, ...options.role === void 0 ? {} : { role: options.role } },
1047
+ options.idempotencyKey ?? randomUUID2()
2550
1048
  );
2551
- const turnId = randomUUID3();
2552
- const followed = await submitAndFollow(
1049
+ print(
2553
1050
  deps,
2554
- withToken,
2555
- { agentId: agent.id, prompt, sessionId: session.id, turnId },
2556
- { agentName: agent.name, outcome: options.wait !== false }
2557
- );
2558
- if ("admitted" in followed) {
2559
- deps.output({
2560
- message: [
2561
- `Turn ${turnId} was admitted in Session ${session.id}; the platform runs it to its end.`,
2562
- "Wait for its result:",
2563
- ` ${waitCommand(turnId, session.id)}`
2564
- ].join("\n"),
2565
- agent,
2566
- production,
2567
- session,
2568
- turn_id: turnId,
2569
- turn: followed.admitted
2570
- });
2571
- return;
2572
- }
2573
- deps.output({
2574
- message: followed.completed.assistant_text,
1051
+ options.json,
2575
1052
  agent,
2576
- production,
2577
- session,
2578
- turn: followed.completed
2579
- });
2580
- }
2581
- async function turnWaitCommand(deps, turnId, options) {
2582
- if (!UUID.test(turnId) || !UUID.test(options.session))
2583
- throw new Error("The Turn and its Session are identified by UUIDs.");
2584
- const { oauthToken, agent } = await resolveAgent(deps);
2585
- const withToken = managementTokens(deps, agent.id, oauthToken);
2586
- deps.progress.start(`Waiting for Turn ${turnId}. Interrupting the wait does not stop the Turn.`);
2587
- try {
2588
- const turn = await followTurn({
2589
- now: deps.now ?? Date.now,
2590
- read: async () => {
2591
- let current;
2592
- try {
2593
- current = await withToken(
2594
- (token) => deps.api.getTurn(agent.id, options.session, turnId, token)
2595
- );
2596
- } catch (error) {
2597
- if (!isNotFound(error)) throw error;
2598
- throw turnNotFound(turnId, options.session, agent.name);
2599
- }
2600
- if (current.status === "completed") return { result: current };
2601
- if (current.status === "failed")
2602
- throw new TurnFailedError(current.id, current.failure_code, current.failure);
2603
- return { pending: current };
2604
- },
2605
- resume: `run \`${waitCommand(turnId, options.session)}\` again.`,
2606
- sleep: deps.sleep ?? pause,
2607
- turnId
2608
- });
2609
- deps.progress.succeed(`Turn ${turnId} completed.`);
2610
- deps.output({ message: turn.assistant_text ?? "", agent, turn });
2611
- } catch (error) {
2612
- deps.progress.fail();
2613
- throw error;
2614
- }
2615
- }
2616
- async function agentListCommand(deps) {
2617
- const agents = await deps.api.listAgents(await deps.auth.accessToken());
2618
- deps.output({
2619
- message: agents.length ? agents.map((agent) => `${agent.name} \xB7 ${agent.publicHostname} \xB7 ${agent.region}`).join("\n") : "No agents are available.",
2620
- agents
2621
- });
2622
- }
2623
- async function agentShowCommand(deps) {
2624
- const { agent, token } = await agentManagement(deps);
2625
- const production = await deps.api.getProduction(agent.id, token);
2626
- const deployment = production ? await deps.api.getDeployment(agent.id, production.deployment_id, token) : null;
2627
- const lines = [
2628
- `agent: ${agent.name}`,
2629
- `Organization: ${agent.organizationName}`,
2630
- `region: ${agent.region}`,
2631
- `application: https://${agent.publicHostname}`,
2632
- `ssh: ssh ${agent.sshDestination}`
2633
- ];
2634
- if (deployment) {
2635
- lines.push(
2636
- `production: ${deploymentName(deployment)} (${shortDeploymentId(deployment.id)}) ${productionStateLabel(deployment)}`
2637
- );
2638
- }
2639
- deps.output({ message: lines.join("\n"), agent, production, deployment });
2640
- }
2641
- function shortDeploymentId(id) {
2642
- return id.replace(/-/g, "").slice(0, 8);
2643
- }
2644
- function deploymentName(deployment) {
2645
- const subject = deployment.origin.git?.subject?.trim();
2646
- return subject ? subject : shortDeploymentId(deployment.id);
1053
+ [
1054
+ `Created agent ${agent.name}.`,
1055
+ `ID: ${agent.id}`,
1056
+ `Role: ${agent.role}`,
1057
+ `Region: ${agent.region}`
1058
+ ].join("\n")
1059
+ );
2647
1060
  }
2648
- function deploymentSourceLabel(deployment) {
2649
- const git = deployment.origin.git;
2650
- if (!git) return deploymentOriginLabel(deployment.origin.actor);
2651
- const sha = git.commit_sha.slice(0, 7);
2652
- return git.ref ? `${sha} \xB7 ${git.ref}` : sha;
1061
+ async function accountShowCommand(deps, options = {}) {
1062
+ const account = await deps.api.getAccount(await deps.auth.apiKey());
1063
+ print(
1064
+ deps,
1065
+ options.json,
1066
+ account,
1067
+ fields([
1068
+ ["ID", account.id],
1069
+ ["Name", account.name],
1070
+ ["Kind", account.kind],
1071
+ ["Your role", account.member.role],
1072
+ ["Your subject", account.member.subject],
1073
+ ["Bucket limit", String(account.limits.buckets)]
1074
+ ])
1075
+ );
2653
1076
  }
2654
- function productionStateLabel(deployment) {
2655
- const { generation, state } = deployment.production;
2656
- return generation === null ? state : `${state} (generation ${generation})`;
1077
+ function memberName(member) {
1078
+ return member.name ?? member.display_name ?? "";
2657
1079
  }
2658
- function selectDeployment(deployments, prefix) {
2659
- const normalized = prefix.trim().toLowerCase().replace(/-/g, "");
2660
- if (!/^[0-9a-f]+$/.test(normalized)) {
2661
- throw new Error(`Deployment id ${prefix} must be a hexadecimal id prefix.`);
2662
- }
2663
- const matches = deployments.filter(
2664
- (deployment) => deployment.id.replace(/-/g, "").startsWith(normalized)
1080
+ async function memberListCommand(deps, options = {}) {
1081
+ const page = await deps.api.listMembers(await deps.auth.apiKey(), {
1082
+ limit: options.limit,
1083
+ cursor: options.cursor
1084
+ });
1085
+ printPage(
1086
+ deps,
1087
+ options.json,
1088
+ page,
1089
+ (members) => members.length ? table(
1090
+ ["SUBJECT", "KIND", "NAME", "ROLE", "CREATED"],
1091
+ members.map((member) => [
1092
+ member.subject,
1093
+ member.kind,
1094
+ memberName(member),
1095
+ member.role,
1096
+ member.created_at
1097
+ ])
1098
+ ) : "No members."
2665
1099
  );
2666
- if (matches.length === 0) throw new Error(`Deployment ${prefix} was not found in this agent.`);
2667
- if (matches.length > 1) {
1100
+ }
1101
+ async function resolveMemberSubject(deps, key, member) {
1102
+ if (member.startsWith("agent.")) return member;
1103
+ const subjects = /* @__PURE__ */ new Set();
1104
+ let cursor;
1105
+ do {
1106
+ const page = await deps.api.listMembers(key, { cursor });
1107
+ for (const candidate of page.data)
1108
+ if (candidate.subject === member || candidate.subject === `agent.${member}` || candidate.kind === "agent" && candidate.name === member)
1109
+ subjects.add(candidate.subject);
1110
+ cursor = page.next_cursor ?? void 0;
1111
+ } while (cursor);
1112
+ const [subject, ...others] = subjects;
1113
+ if (!subject)
1114
+ throw new Error(`No member of this account is ${member}. See \`tonbo member list\`.`);
1115
+ if (others.length)
2668
1116
  throw new Error(
2669
- `Deployment id ${prefix} is ambiguous; it matches ${matches.map((deployment) => shortDeploymentId(deployment.id)).join(", ")}. Use more characters.`
1117
+ `${member} names several members (${[...subjects].join(", ")}); give the member's subject.`
2670
1118
  );
2671
- }
2672
- return matches[0];
2673
- }
2674
- function renderTable(headers, rows) {
2675
- const widths = headers.map(
2676
- (header, column) => Math.max(header.length, ...rows.map((row) => row[column].length))
1119
+ return subject;
1120
+ }
1121
+ async function memberUpdateCommand(deps, member, options = {}) {
1122
+ if (options.role === void 0 && options.name === void 0)
1123
+ throw new Error("Nothing to change: pass --role, --name or both.");
1124
+ if (options.name !== void 0) validateAgentName(options.name);
1125
+ const key = await deps.auth.apiKey();
1126
+ const subject = await resolveMemberSubject(deps, key, member);
1127
+ const updated = await deps.api.updateMember(key, subject, {
1128
+ ...options.role === void 0 ? {} : { role: options.role },
1129
+ ...options.name === void 0 ? {} : { name: options.name }
1130
+ });
1131
+ print(
1132
+ deps,
1133
+ options.json,
1134
+ updated,
1135
+ [
1136
+ `Updated ${updated.kind === "agent" ? "Agent" : "member"} ${memberName(updated) || updated.subject}.`,
1137
+ fields([
1138
+ ["Subject", updated.subject],
1139
+ ["Name", memberName(updated)],
1140
+ ["Role", updated.role]
1141
+ ])
1142
+ ].join("\n")
2677
1143
  );
2678
- const render = (row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd();
2679
- return [render(headers), ...rows.map(render)].join("\n");
2680
1144
  }
2681
- function formatTimestamp(value) {
2682
- const date = new Date(value);
2683
- if (Number.isNaN(date.getTime())) return value;
2684
- return `${date.toISOString().slice(0, 16)}Z`;
2685
- }
2686
- function productionMarker(state) {
2687
- if (state === "current") return "\u25CF";
2688
- if (state === "previous") return "\u25CB";
2689
- return "";
2690
- }
2691
- async function deploymentsListCommand(deps) {
2692
- const { agent, token } = await agentManagement(deps);
2693
- const [deployments, production] = await Promise.all([
2694
- deps.api.listDeployments(agent.id, token),
2695
- deps.api.getProduction(agent.id, token)
1145
+ function keyAccess(apiKey) {
1146
+ return apiKey.read_only ? "read-only" : "read-write";
1147
+ }
1148
+ function keyBuckets(apiKey) {
1149
+ return apiKey.buckets.length ? apiKey.buckets.join(", ") : "all";
1150
+ }
1151
+ function apiKeyFields(apiKey) {
1152
+ return fields([
1153
+ ["ID", apiKey.id],
1154
+ ["Name", apiKey.name],
1155
+ ["Member", `${apiKey.member} (${apiKey.member_kind})`],
1156
+ ["Ends in", apiKey.last4],
1157
+ ["Access", keyAccess(apiKey)],
1158
+ ["Buckets", keyBuckets(apiKey)],
1159
+ ["Expires", when(apiKey.expires_at)],
1160
+ ["Last used", when(apiKey.last_used_at)],
1161
+ ["Created", apiKey.created_at]
2696
1162
  ]);
2697
- const message = deployments.length ? renderTable(
2698
- ["ID", "NAME", "STATUS", "PRODUCTION", "SOURCE", "CREATED", "BY"],
2699
- deployments.map((deployment) => [
2700
- shortDeploymentId(deployment.id),
2701
- deploymentName(deployment),
2702
- deployment.production.state,
2703
- productionMarker(deployment.production.state),
2704
- deploymentSourceLabel(deployment),
2705
- formatTimestamp(deployment.created_at),
2706
- deployment.created_by_user_id ? shortDeploymentId(deployment.created_by_user_id) : ""
2707
- ])
2708
- ) : `No Deployments exist for agent ${agent.name}. Run \`tonbo deploy\` to create one.`;
2709
- deps.output({ message, agent, production, deployments });
2710
1163
  }
2711
- async function deploymentsShowCommand(deps, prefix) {
2712
- const { agent, token } = await agentManagement(deps);
2713
- const deployment = selectDeployment(await deps.api.listDeployments(agent.id, token), prefix);
2714
- const rollouts = (await deps.api.listRollouts(agent.id, token)).filter(
2715
- (rollout) => rollout.deployment_id === deployment.id || rollout.previous_deployment_id === deployment.id
2716
- );
2717
- deps.output({
2718
- message: renderDeploymentDetail(deployment, rollouts),
2719
- agent,
2720
- deployment,
2721
- rollouts
1164
+ async function apiKeyListCommand(deps, options = {}) {
1165
+ const page = await deps.api.listApiKeys(await deps.auth.apiKey(), {
1166
+ limit: options.limit,
1167
+ cursor: options.cursor
2722
1168
  });
2723
- }
2724
- function renderDeploymentDetail(deployment, rollouts) {
2725
- const git = deployment.origin.git;
2726
- const source = deployment.spec.source;
2727
- const bundleLabel = typeof source?.sha256 === "string" ? [
2728
- typeof source.format === "string" ? source.format : "",
2729
- source.sha256,
2730
- typeof source.size_bytes === "number" ? `(${source.size_bytes} bytes)` : ""
2731
- ].filter(Boolean).join(" ") : null;
2732
- const inference = deployment.spec.inference;
2733
- const rows = [
2734
- ["Deployment", `${deploymentName(deployment)} (${shortDeploymentId(deployment.id)})`],
2735
- ["ID", deployment.id],
2736
- ["Status", productionStateLabel(deployment)],
2737
- ["Source", `${deploymentSourceLabel(deployment)}${git?.dirty ? " (dirty)" : ""}`]
2738
- ];
2739
- if (git) rows.push(["Commit", git.commit_sha], ["Author", git.author ?? ""]);
2740
- rows.push(
2741
- ["Created", formatTimestamp(deployment.created_at)],
2742
- ["Created by", deployment.created_by_user_id ?? ""],
2743
- ["Spec sha256", deployment.spec_sha256]
1169
+ printPage(
1170
+ deps,
1171
+ options.json,
1172
+ page,
1173
+ (keys) => keys.length ? table(
1174
+ ["ID", "NAME", "MEMBER", "ENDS IN", "ACCESS", "BUCKETS", "EXPIRES", "LAST USED"],
1175
+ keys.map((apiKey) => [
1176
+ apiKey.id,
1177
+ apiKey.name,
1178
+ apiKey.member,
1179
+ apiKey.last4,
1180
+ keyAccess(apiKey),
1181
+ keyBuckets(apiKey),
1182
+ when(apiKey.expires_at),
1183
+ when(apiKey.last_used_at)
1184
+ ])
1185
+ ) : "No API keys."
2744
1186
  );
2745
- if (typeof inference?.model === "string") rows.push(["Model", inference.model]);
2746
- if (bundleLabel) rows.push(["Bundle", bundleLabel]);
2747
- const label = Math.max(...rows.map(([name]) => name.length)) + 1;
2748
- const lines = rows.map(([name, value]) => `${`${name}:`.padEnd(label)} ${value}`.trimEnd());
2749
- lines.push("", "Rollouts:");
2750
- if (rollouts.length === 0) lines.push(" none");
2751
- for (const rollout of rollouts) {
2752
- const direction = rollout.deployment_id === deployment.id ? `${rollout.kind} to this Deployment${rollout.previous_deployment_id ? ` from ${shortDeploymentId(rollout.previous_deployment_id)}` : ""}` : `${rollout.kind} away to ${shortDeploymentId(rollout.deployment_id)}`;
2753
- lines.push(
2754
- ` generation ${rollout.generation} ${formatTimestamp(rollout.created_at)} ${direction}${rollout.created_by_user_id ? ` by ${shortDeploymentId(rollout.created_by_user_id)}` : ""}`
2755
- );
2756
- }
2757
- return lines.join("\n");
2758
1187
  }
2759
- async function deploymentsPromoteCommand(deps, prefix) {
2760
- const { agent, token } = await agentManagement(deps);
2761
- const target = selectDeployment(await deps.api.listDeployments(agent.id, token), prefix);
2762
- const result = await moveProduction(deps, agent.id, target, token);
2763
- deps.output({
2764
- message: describeProductionMove(agent, result, "Promoted"),
2765
- agent,
2766
- ...result
1188
+ async function apiKeyCreateCommand(deps, name, options = {}) {
1189
+ const key = await deps.auth.apiKey();
1190
+ const member = options.member === void 0 ? void 0 : await resolveMemberSubject(deps, key, options.member);
1191
+ const created = await deps.api.createApiKey(key, {
1192
+ name,
1193
+ member,
1194
+ read_only: options.readOnly,
1195
+ buckets: options.bucket,
1196
+ expires_in: options.expiresIn
2767
1197
  });
1198
+ print(
1199
+ deps,
1200
+ options.json,
1201
+ created,
1202
+ [
1203
+ `Created API key ${created.name} for ${created.member}.`,
1204
+ "",
1205
+ ` ${created.key}`,
1206
+ "",
1207
+ apiKeyFields(created)
1208
+ ].join("\n")
1209
+ );
1210
+ deps.notice(
1211
+ "This is the only time the key's secret is shown: store it now. Anyone holding it acts as its member."
1212
+ );
2768
1213
  }
2769
- async function deploymentsRollbackCommand(deps, prefix) {
2770
- const { agent, token } = await agentManagement(deps);
2771
- let target;
2772
- if (prefix !== void 0) {
2773
- target = selectDeployment(await deps.api.listDeployments(agent.id, token), prefix);
2774
- } else {
2775
- const rollouts = await deps.api.listRollouts(agent.id, token);
2776
- const previousId = rollouts.find(
2777
- (rollout) => rollout.previous_deployment_id !== null
2778
- )?.previous_deployment_id;
2779
- if (!previousId) {
2780
- throw new Error(
2781
- `Agent ${agent.name} has no previous Production Deployment to roll back to. Name one with \`tonbo deployments rollback <id>\`.`
2782
- );
2783
- }
2784
- target = await deps.api.getDeployment(agent.id, previousId, token);
2785
- }
2786
- const result = await moveProduction(deps, agent.id, target, token);
2787
- deps.output({
2788
- message: describeProductionMove(agent, result, "Rolled back"),
2789
- agent,
2790
- ...result
2791
- });
1214
+ async function apiKeyShowCommand(deps, keyId, options = {}) {
1215
+ const apiKey = await deps.api.getApiKey(await deps.auth.apiKey(), keyId);
1216
+ print(deps, options.json, apiKey, apiKeyFields(apiKey));
2792
1217
  }
2793
- async function moveProduction(deps, agentId, target, token) {
2794
- const previous = await deps.api.getProduction(agentId, token);
2795
- const production = await deps.api.putProduction(
2796
- agentId,
2797
- {
2798
- deployment_id: target.id,
2799
- desired_state: "running",
2800
- expected_generation: previous ? previous.generation : null
1218
+ async function apiKeyRevokeCommand(deps, keyId, options = {}) {
1219
+ const key = await deps.auth.apiKey();
1220
+ await confirm(
1221
+ deps,
1222
+ options.yes,
1223
+ `revoke API key ${keyId}`,
1224
+ async () => {
1225
+ const apiKey = await deps.api.getApiKey(key, keyId);
1226
+ return `Revoke API key ${apiKey.name} (${apiKey.member}, ends in ${apiKey.last4})? Every token minted from it stops working. Type yes to confirm:`;
2801
1227
  },
2802
- token
1228
+ "yes"
2803
1229
  );
2804
- return {
2805
- deployment: await deps.api.getDeployment(agentId, target.id, token),
2806
- previous,
2807
- production
2808
- };
2809
- }
2810
- function describeProductionMove(agent, move, verb) {
2811
- const { deployment, previous } = move;
2812
- const name = `${deploymentName(deployment)} (${shortDeploymentId(deployment.id)})`;
2813
- const headline = previous?.deployment_id === deployment.id ? `Deployment ${name} is already Production for agent ${agent.name}; requested it to be running.` : `${verb} ${name} to Production for agent ${agent.name}${previous ? ` (from ${shortDeploymentId(previous.deployment_id)})` : ""}.`;
2814
- return `${headline}
2815
- Production: ${productionStateLabel(deployment)}
2816
- Application: https://${agent.publicHostname}`;
2817
- }
2818
- async function agentManagement(deps) {
2819
- const { oauthToken, agent } = await resolveAgent(deps);
2820
- return {
2821
- agent,
2822
- token: await deps.api.exchangeManagementToken(oauthToken, agent.id)
2823
- };
1230
+ const revoked = await deps.api.revokeApiKey(key, keyId);
1231
+ print(deps, options.json, revoked, `Revoked API key ${revoked.id} at ${revoked.revoked_at}.`);
1232
+ }
1233
+ function bucketAccess(bucket) {
1234
+ return bucket.public_read ? "public" : "private";
1235
+ }
1236
+ function bucketFields(bucket) {
1237
+ return fields([
1238
+ ["Name", bucket.name],
1239
+ ["Region", bucket.region],
1240
+ ["State", bucket.state],
1241
+ ["Read access", bucketAccess(bucket)],
1242
+ ["URL", bucket.url],
1243
+ ["Created", bucket.created_at]
1244
+ ]);
2824
1245
  }
2825
- async function secretListCommand(deps) {
2826
- const { agent, token } = await agentManagement(deps);
2827
- const secrets = await deps.api.listAgentSecrets(agent.id, token);
2828
- deps.output({
2829
- message: secrets.length ? secrets.map((secret) => secret.name).join("\n") : "No agent secrets are configured.",
2830
- agent,
2831
- secrets
1246
+ async function bucketListCommand(deps, options = {}) {
1247
+ const page = await deps.api.listBuckets(await deps.auth.apiKey(), {
1248
+ limit: options.limit,
1249
+ cursor: options.cursor
2832
1250
  });
1251
+ printPage(
1252
+ deps,
1253
+ options.json,
1254
+ page,
1255
+ (buckets) => buckets.length ? table(
1256
+ ["NAME", "REGION", "STATE", "READ ACCESS", "URL"],
1257
+ buckets.map((bucket) => [
1258
+ bucket.name,
1259
+ bucket.region,
1260
+ bucket.state,
1261
+ bucketAccess(bucket),
1262
+ bucket.url
1263
+ ])
1264
+ ) : "No buckets."
1265
+ );
2833
1266
  }
2834
- async function secretSetCommand(deps, name, options) {
2835
- const value = await deps.secretValue(name, options.fromEnv);
2836
- const { agent, token } = await agentManagement(deps);
2837
- const secret = await deps.api.setAgentSecret(agent.id, name, value, token);
2838
- deps.output({
2839
- message: `Set agent secret ${secret.name}. Redeploy to replace the active runtime with this value.`,
2840
- agent,
2841
- secret
2842
- });
1267
+ async function bucketCreateCommand(deps, name, options = {}) {
1268
+ const bucket = await deps.api.createBucket(
1269
+ await deps.auth.apiKey(),
1270
+ { name, region: options.region },
1271
+ options.idempotencyKey ?? randomUUID2()
1272
+ );
1273
+ print(
1274
+ deps,
1275
+ options.json,
1276
+ bucket,
1277
+ [`Created bucket ${bucket.name}.`, bucketFields(bucket)].join("\n")
1278
+ );
2843
1279
  }
2844
- async function secretRemoveCommand(deps, name) {
2845
- const { agent, token } = await agentManagement(deps);
2846
- await deps.api.deleteAgentSecret(agent.id, name, token);
2847
- deps.output({ message: `Removed agent secret ${name}.`, agent, name });
1280
+ async function bucketShowCommand(deps, name, options = {}) {
1281
+ const bucket = await deps.api.getBucket(await deps.auth.apiKey(), name);
1282
+ print(deps, options.json, bucket, bucketFields(bucket));
2848
1283
  }
2849
- async function cancelActivitiesCommand(deps, options) {
2850
- const { oauthToken, agent } = await resolveAgent(deps);
2851
- const token = await deps.api.exchangeManagementToken(oauthToken, agent.id);
2852
- const result = await deps.api.cancelActivities(agent.id, token, {
2853
- sandbox_id: options.sandbox,
2854
- runtime_session_id: options.runtimeSession,
2855
- work_keys: options.workKey,
2856
- reason: options.reason
2857
- });
2858
- deps.output({
2859
- message: "Activity cancellation requested. Physical retirement and application checkpoint cancellation must still be verified.",
2860
- cancellation: result
1284
+ async function bucketUpdateCommand(deps, name, options = {}) {
1285
+ if (options.public === options.private)
1286
+ throw new Error("Pass exactly one of --public and --private.");
1287
+ const bucket = await deps.api.updateBucket(await deps.auth.apiKey(), name, {
1288
+ public_read: options.public === true
2861
1289
  });
2862
- }
2863
-
2864
- // src/credentials.ts
2865
- import { randomUUID as randomUUID4 } from "node:crypto";
2866
- import { chmod, lstat as lstat4, mkdir, open as open2, readFile as readFile5, rename as rename2, rm as rm2 } from "node:fs/promises";
2867
- import os from "node:os";
2868
- import path5 from "node:path";
2869
- function defaultConfigDirectory() {
2870
- const base = process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config");
2871
- return path5.join(base, "tonbo");
2872
- }
2873
- var FileCredentialStore = class {
2874
- constructor(filename = path5.join(defaultConfigDirectory(), "credentials.json")) {
2875
- this.filename = filename;
2876
- }
2877
- async load() {
2878
- try {
2879
- await assertPrivateRegularFile(this.filename);
2880
- const parsed = JSON.parse(await readFile5(this.filename, "utf8"));
2881
- if (!isOAuthTokenSet(parsed)) throw new Error("invalid token set");
2882
- return parsed;
2883
- } catch (error) {
2884
- if (error.code === "ENOENT") return null;
2885
- throw new Error(`Could not read Tonbo credentials at ${this.filename}.`, { cause: error });
2886
- }
2887
- }
2888
- async save(tokens) {
2889
- if (!isOAuthTokenSet(tokens)) throw new Error("Refusing to store a malformed Tonbo token set.");
2890
- const directory = path5.dirname(this.filename);
2891
- await mkdir(directory, { mode: 448, recursive: true });
2892
- await preparePrivateDirectory(directory);
2893
- await assertExistingDestinationIsSafe(this.filename);
2894
- const temporary = path5.join(
2895
- directory,
2896
- `.${path5.basename(this.filename)}.${process.pid}.${randomUUID4()}.tmp`
2897
- );
2898
- let handle = null;
2899
- try {
2900
- handle = await open2(temporary, "wx", 384);
2901
- await handle.writeFile(`${JSON.stringify(tokens, null, 2)}
2902
- `, "utf8");
2903
- await handle.sync();
2904
- await handle.close();
2905
- handle = null;
2906
- if (process.platform !== "win32") await chmod(temporary, 384);
2907
- await rename2(temporary, this.filename);
2908
- } catch (error) {
2909
- await handle?.close().catch(() => void 0);
2910
- await rm2(temporary, { force: true }).catch(() => void 0);
2911
- throw new Error(`Could not store Tonbo credentials at ${this.filename}.`, { cause: error });
2912
- }
2913
- }
2914
- };
2915
- function isOAuthTokenSet(value) {
2916
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2917
- const token = value;
2918
- return typeof token.access_token === "string" && token.access_token.length > 0 && (token.refresh_token === void 0 || typeof token.refresh_token === "string") && (token.expires_at === void 0 || Number.isInteger(token.expires_at) && (token.expires_at ?? 0) > 0);
2919
- }
2920
- async function preparePrivateDirectory(directory) {
2921
- const stat = await lstat4(directory);
2922
- if (stat.isSymbolicLink() || !stat.isDirectory())
2923
- throw new Error("Tonbo config directory must be a real directory.");
2924
- if (process.platform !== "win32") await chmod(directory, 448);
2925
- }
2926
- async function assertExistingDestinationIsSafe(filename) {
2927
- try {
2928
- const stat = await lstat4(filename);
2929
- if (stat.isSymbolicLink() || !stat.isFile())
2930
- throw new Error("Tonbo credential path must be a regular file.");
2931
- } catch (error) {
2932
- if (error.code !== "ENOENT") throw error;
2933
- }
2934
- }
2935
- async function assertPrivateRegularFile(filename) {
2936
- const stat = await lstat4(filename);
2937
- if (stat.isSymbolicLink() || !stat.isFile())
2938
- throw new Error("Tonbo credential path must be a regular file.");
2939
- if (process.platform !== "win32" && (stat.mode & 63) !== 0)
2940
- throw new Error("Tonbo credential file must have mode 0600.");
2941
- }
2942
-
2943
- // src/git.ts
2944
- import { execFile as execFile2 } from "node:child_process";
2945
- var defaultGitRunner = (args, cwd) => new Promise((resolve, reject) => {
2946
- execFile2(
2947
- "git",
2948
- args,
2949
- {
2950
- cwd,
2951
- encoding: "utf8",
2952
- // Never take the index lock for a read: a concurrent editor or IDE
2953
- // must not turn provenance collection into a failure.
2954
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
2955
- windowsHide: true
2956
- },
2957
- (error, stdout) => error ? reject(new Error(`git ${args.join(" ")} failed.`, { cause: error })) : resolve(stdout)
1290
+ print(
1291
+ deps,
1292
+ options.json,
1293
+ bucket,
1294
+ bucket.public_read ? `Bucket ${bucket.name} is public: anyone can read it at ${bucket.url}.` : `Bucket ${bucket.name} is private: only the account's keys can read it.`
2958
1295
  );
2959
- });
2960
- var COMMIT_SHA = /^[0-9a-f]{40}$/;
2961
- var REF_MAX_LENGTH = 255;
2962
- var SUBJECT_MAX_LENGTH = 512;
2963
- var AUTHOR_MAX_LENGTH = 255;
2964
- async function collectGitProvenance(root, run = defaultGitRunner) {
2965
- const commitSha = await read(run, ["rev-parse", "HEAD"], root);
2966
- if (!commitSha || !COMMIT_SHA.test(commitSha)) return null;
2967
- const [ref, subject, author, status, remote] = await Promise.all([
2968
- read(run, ["rev-parse", "--abbrev-ref", "HEAD"], root),
2969
- read(run, ["log", "-1", "--format=%s"], root),
2970
- read(run, ["log", "-1", "--format=%an"], root),
2971
- // Only the deployed tree matters: changes elsewhere in the repository do
2972
- // not alter the uploaded bundle.
2973
- read(run, ["status", "--porcelain", "--", "."], root),
2974
- read(run, ["remote", "get-url", "origin"], root)
2975
- ]);
2976
- return {
2977
- ...githubRepositoryUrl(remote) ? { repository_url: githubRepositoryUrl(remote) } : {},
2978
- commit_sha: commitSha,
2979
- ref: ref === "HEAD" ? null : clamp(ref, REF_MAX_LENGTH),
2980
- subject: clamp(subject, SUBJECT_MAX_LENGTH),
2981
- author: clamp(author, AUTHOR_MAX_LENGTH),
2982
- // When the status cannot be read the tree cannot be shown to match the
2983
- // commit, so the Deployment is recorded as dirty rather than clean.
2984
- dirty: status === null || status.length > 0
2985
- };
2986
1296
  }
2987
- async function read(run, args, cwd) {
2988
- try {
2989
- return (await run(args, cwd)).trim();
2990
- } catch {
2991
- return null;
2992
- }
1297
+ async function bucketDeleteCommand(deps, name, options = {}) {
1298
+ await confirm(
1299
+ deps,
1300
+ options.yes,
1301
+ `delete bucket ${name}`,
1302
+ () => Promise.resolve(
1303
+ `Delete bucket ${name} and every stream in it? This cannot be undone, and the name is never used again. Type the bucket's name to confirm:`
1304
+ ),
1305
+ name
1306
+ );
1307
+ const bucket = await deps.api.deleteBucket(await deps.auth.apiKey(), name);
1308
+ print(
1309
+ deps,
1310
+ options.json,
1311
+ bucket,
1312
+ `Bucket ${bucket.name} is ${bucket.state}; its streams are being purged.`
1313
+ );
2993
1314
  }
2994
- function clamp(value, maxLength) {
2995
- if (!value) return null;
2996
- return value.length > maxLength ? value.slice(0, maxLength) : value;
1315
+ async function usageShowCommand(deps, options = {}) {
1316
+ const usage = await deps.api.getUsage(await deps.auth.apiKey(), options.period);
1317
+ print(
1318
+ deps,
1319
+ options.json,
1320
+ usage,
1321
+ [
1322
+ `Usage in ${usage.period} (${usage.currency.toUpperCase()})`,
1323
+ usage.lines.length ? table(
1324
+ ["PRODUCT", "REGION", "DIMENSION", "MODEL", "QUANTITY", "AMOUNT"],
1325
+ usage.lines.map((line) => [
1326
+ line.product,
1327
+ line.region,
1328
+ line.dimension,
1329
+ line.model ?? "",
1330
+ line.quantity,
1331
+ usd(line.amount_micros)
1332
+ ])
1333
+ ) : "No rated usage.",
1334
+ `Total: ${usd(usage.total_micros)}`
1335
+ ].join("\n")
1336
+ );
2997
1337
  }
2998
1338
 
2999
1339
  // src/progress.ts
@@ -3049,26 +1389,10 @@ var TerminalProgress = class {
3049
1389
  this.lastWidth = value.length;
3050
1390
  }
3051
1391
  };
3052
- var silentProgress = {
3053
- start: () => {
3054
- },
3055
- succeed: () => {
3056
- },
3057
- fail: () => {
3058
- }
3059
- };
3060
1392
 
3061
1393
  // src/prompt.ts
3062
1394
  import select from "@inquirer/select";
3063
1395
  import { createInterface } from "node:readline/promises";
3064
- async function terminalPrompt(question) {
3065
- const prompt = createInterface({ input: process.stdin, output: process.stderr });
3066
- try {
3067
- return await prompt.question(`? ${question}`);
3068
- } finally {
3069
- prompt.close();
3070
- }
3071
- }
3072
1396
  async function terminalSelect(question, choices) {
3073
1397
  return select(
3074
1398
  {
@@ -3082,189 +1406,172 @@ async function terminalSelect(question, choices) {
3082
1406
  }
3083
1407
  );
3084
1408
  }
1409
+ async function terminalAsk(question) {
1410
+ const terminal = createInterface({ input: process.stdin, output: process.stderr });
1411
+ try {
1412
+ return (await terminal.question(`${question} `)).trim();
1413
+ } finally {
1414
+ terminal.close();
1415
+ }
1416
+ }
3085
1417
 
3086
1418
  // src/app.ts
3087
1419
  var packageVersion = JSON.parse(
3088
1420
  readFileSync(new URL("../../package.json", import.meta.url), "utf8")
3089
1421
  );
3090
- function createDependencies(json = false) {
3091
- const accountOrigin = process.env.TONBO_ACCOUNT_ORIGIN || "https://tonbo.dev";
3092
- const managementOrigin = process.env.TONBO_API_ORIGIN || "https://api.tonbo.dev";
1422
+ function apiOrigin(configured = process.env.TONBO_API_ORIGIN) {
1423
+ const url = new URL(configured?.trim() || DEFAULT_API_ORIGIN);
1424
+ const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
1425
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
1426
+ throw new Error("TONBO_API_ORIGIN must be an https origin.");
1427
+ return url.origin;
1428
+ }
1429
+ function loginOrigin(api) {
1430
+ const url = new URL(api);
1431
+ if (url.hostname.startsWith("api.")) url.hostname = url.hostname.slice("api.".length);
1432
+ return url.origin;
1433
+ }
1434
+ function createDependencies() {
1435
+ const api = apiOrigin();
1436
+ const credentials = new FileCredentialStore();
3093
1437
  return {
3094
- api: new TonboApi(fetch, accountOrigin, managementOrigin),
3095
- auth: new AuthClient(new FileCredentialStore(), fetch, accountOrigin),
3096
- cwd: () => process.cwd(),
3097
- defaultSshPublicKeys: readDefaultSshPublicKeys,
3098
- gitProvenance: collectGitProvenance,
3099
- interactive: () => !json && process.stdin.isTTY === true && process.stderr.isTTY === true,
3100
- inspectSource: inspectLocalAgentSource,
3101
- output: (value) => {
3102
- if (json) console.log(JSON.stringify(value));
3103
- else console.log(value.message ?? value);
3104
- },
3105
- progress: json ? silentProgress : new TerminalProgress(process.stderr),
3106
- prompt: terminalPrompt,
3107
- select: terminalSelect,
3108
- secretValue: (name, fromEnvironment) => {
3109
- const environmentName = fromEnvironment ?? name;
3110
- const value = process.env[environmentName];
3111
- if (!value)
3112
- throw new Error(
3113
- `Environment variable ${environmentName} is empty. Set it before running tonbo secret set.`
3114
- );
3115
- return Promise.resolve(value);
3116
- }
1438
+ api: new ManagementApi(fetch, api),
1439
+ ask: terminalAsk,
1440
+ auth: new AuthClient(credentials, fetch, loginOrigin(api)),
1441
+ credentials,
1442
+ interactive: () => process.stdin.isTTY === true && process.stderr.isTTY === true,
1443
+ output: (text) => console.log(text),
1444
+ notice: (text) => console.error(text),
1445
+ progress: new TerminalProgress(process.stderr),
1446
+ select: terminalSelect
3117
1447
  };
3118
1448
  }
3119
- function jsonOutput(program) {
3120
- return program.opts().json === true;
1449
+ function parseLimit(value) {
1450
+ if (!/^[1-9][0-9]*$/.test(value)) throw new InvalidArgumentError("Not a positive integer.");
1451
+ return Number(value);
1452
+ }
1453
+ function operationOptions(command, operationId) {
1454
+ const query = managementOperations[operationId].query;
1455
+ if (query.includes("limit"))
1456
+ command.option("--limit <count>", "the most items to return", parseLimit);
1457
+ if (query.includes("cursor"))
1458
+ command.option("--cursor <cursor>", "continue from the cursor a previous page printed");
1459
+ return command.option("--json", "print the API data as JSON");
3121
1460
  }
1461
+ var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
3122
1462
  function createProgram(dependencies = createDependencies) {
3123
- const program = new Command().name("tonbo").description("Deploy a persistent Agent to Tonbo.").version(packageVersion.version).option("--json", "print machine-readable JSON");
3124
- program.command("init").description("interactively create a Tonbo Agent declaration in this directory").option("--harness <harness>", "Agent Harness (currently pi)").option("--model <model>", "inference model").option("--driver <driver>", "PI driver: native or command").option("--agent-entry <file>", "Node entry file for the command driver").option("--build-command <argv...>", "build command argv for the command driver").option("--region <region>", "home region of an agent init creates (see tonbo region list)").option("--force", `replace an existing ${DECLARATION_FILENAME}`).action(
3125
- async (options) => initCommand(dependencies(jsonOutput(program)), options)
1463
+ const program = new Command().name("tonbo").description("Sign in to Tonbo and manage account resources.").version(packageVersion.version);
1464
+ program.command("login").description("sign in through the browser and save a personal key for this computer").option("--account <name>", "the account to log in to; defaults to your personal account").action(async (options) => loginCommand(dependencies(), options));
1465
+ program.command("logout").description("revoke this computer's key and remove the saved login").action(async () => logoutCommand(dependencies()));
1466
+ const account = program.command("account").description("the account you are logged in to");
1467
+ operationOptions(
1468
+ account.command("show").description("show the account, your role in it and its limits"),
1469
+ "getAccount"
1470
+ ).action(async (options) => accountShowCommand(dependencies(), options));
1471
+ operationOptions(
1472
+ program.command("region").description("regions new resources can be created in").command("list").description("list the regions"),
1473
+ "listRegions"
1474
+ ).action(async (options) => regionListCommand(dependencies(), options));
1475
+ const member = program.command("member").description("the people and Agents of the account");
1476
+ operationOptions(
1477
+ member.command("list").description("list the members and their roles"),
1478
+ "listMembers"
1479
+ ).action(
1480
+ async (options) => memberListCommand(dependencies(), options)
3126
1481
  );
3127
- program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(jsonOutput(program))));
3128
- program.command("region").description("discover regions and Machine allocation availability").command("list").action(async () => {
3129
- const deps = dependencies(jsonOutput(program));
3130
- deps.output(await deps.api.listRegions(await deps.auth.accessToken()));
3131
- });
3132
- const machine = program.command("machine").description("manage independent account-owned Machines");
3133
- machine.command("list").option("--account <account>", "account UUID").action(async (options) => {
3134
- const deps = dependencies(jsonOutput(program));
3135
- deps.output(
3136
- await deps.api.machineRequest(
3137
- await deps.auth.accessToken(),
3138
- options.account ? `?account=${encodeURIComponent(options.account)}` : ""
3139
- )
3140
- );
3141
- });
3142
- machine.command("allocate").requiredOption("--account <account>", "owning account UUID").requiredOption("--region <region>", "compute region (see tonbo region list)").option(
3143
- "--request-id <uuid>",
3144
- "stable allocation request UUID; reuse it after an uncertain response"
3145
- ).action(async (options) => {
3146
- const deps = dependencies(jsonOutput(program));
3147
- const requestId = options.requestId ?? randomUUID5();
3148
- try {
3149
- const result = await deps.api.machineRequest(await deps.auth.accessToken(), "", "POST", {
3150
- accountId: options.account,
3151
- region: options.region,
3152
- requestId
3153
- });
3154
- deps.output({ ...result, requestId });
3155
- } catch (cause) {
3156
- throw new Error(
3157
- `Machine allocation was not confirmed. Retry with --request-id ${requestId} and the same account and region.`,
3158
- { cause }
3159
- );
3160
- }
3161
- });
3162
- machine.command("allocation <requestId>").description("query the original Machine for an allocation request").requiredOption("--account <account>", "owning account UUID").action(async (requestId, options) => {
3163
- const deps = dependencies(jsonOutput(program));
3164
- deps.output(
3165
- await deps.api.machineRequest(
3166
- await deps.auth.accessToken(),
3167
- `/allocations/${encodeURIComponent(requestId)}?account=${encodeURIComponent(options.account)}`
3168
- )
3169
- );
3170
- });
3171
- for (const operation of ["show", "metrics", "release"]) {
3172
- machine.command(`${operation} <machine>`).action(async (id) => {
3173
- const deps = dependencies(jsonOutput(program));
3174
- const result = await deps.api.machineRequest(
3175
- await deps.auth.accessToken(),
3176
- `/${encodeURIComponent(id)}`,
3177
- operation === "release" ? "DELETE" : "GET"
3178
- );
3179
- deps.output(operation === "metrics" ? result.metrics : result);
3180
- });
3181
- }
3182
- machine.command("bind <machine>").requiredOption("--agent <agent>", "Agent UUID").action(async (id, options) => {
3183
- const deps = dependencies(jsonOutput(program));
3184
- deps.output(
3185
- await deps.api.machineRequest(
3186
- await deps.auth.accessToken(),
3187
- `/${encodeURIComponent(id)}/binding`,
3188
- "POST",
3189
- { agentId: options.agent }
3190
- )
3191
- );
3192
- });
3193
- machine.command("unbind <machine>").description("detach Agent and reset system disk; retain Agent Workspace and Sessions").action(async (id) => {
3194
- const deps = dependencies(jsonOutput(program));
3195
- const token = await deps.auth.accessToken();
3196
- const path6 = `/${encodeURIComponent(id)}`;
3197
- const current = await deps.api.machineRequest(token, path6);
3198
- if (!current.binding) throw new Error("Machine is not bound to an Agent.");
3199
- deps.output(
3200
- await deps.api.machineRequest(token, `${path6}/binding`, "DELETE", {
3201
- agentId: current.binding.agent_id,
3202
- bindingId: current.binding.id,
3203
- generation: current.binding.generation
3204
- })
3205
- );
3206
- });
3207
- const agent = program.command("agent").description("manage the Agent bound to this source directory");
3208
- agent.command("create <name>").description("create an agent and bind this directory to it").option("--account <account>", "owning account UUID; defaults to your personal account").option(
3209
- "--region <region>",
3210
- "permanent home region (see tonbo region list); required once several regions accept new agents"
3211
- ).option("--request-id <uuid>", "reuse the same creation UUID after an uncertain response").action(
3212
- async (name, options) => agentCreateCommand(dependencies(jsonOutput(program)), name, options)
1482
+ operationOptions(
1483
+ member.command("update <member>").description(
1484
+ "change a member's role, or rename an Agent; <member> is a subject, or an Agent's id or name"
1485
+ ).addOption(roleOption("the member's new role; only an owner grants owner")).option("--name <name>", "the Agent's new name"),
1486
+ "updateMember"
1487
+ ).action(
1488
+ async (subject, options) => memberUpdateCommand(dependencies(), subject, options)
3213
1489
  );
3214
- agent.command("creation <requestId>").description("query the original Agent for a creation request").option("--account <account>", "owning account UUID; defaults to your personal account").action(async (requestId, options) => {
3215
- const deps = dependencies(jsonOutput(program));
3216
- deps.output(
3217
- await deps.api.readAgentCreationRequest(
3218
- await deps.auth.accessToken(),
3219
- requestId,
3220
- options.account
3221
- )
3222
- );
3223
- });
3224
- agent.command("list").description("list the agents available to your account").action(async () => agentListCommand(dependencies(jsonOutput(program))));
3225
- const sshKey = program.command("ssh-key").description("manage public keys used by native agent SSH");
3226
- sshKey.command("add <public-key>").description("register an OpenSSH public key with the current Tonbo account").action(async (path6) => sshKeyAddCommand(dependencies(jsonOutput(program)), path6));
3227
- sshKey.command("remove <fingerprint>").description("revoke an SSH public key from the current Tonbo account").action(
3228
- async (fingerprint) => sshKeyRemoveCommand(dependencies(jsonOutput(program)), fingerprint)
1490
+ operationOptions(
1491
+ program.command("agent").description("manage the Agents of your account").command("create <name>").description("create an agent").addOption(roleOption("the Agent's role; member by default")).option(
1492
+ "--region <region>",
1493
+ "permanent home region (see tonbo region list); required once several regions accept new agents"
1494
+ ).option(
1495
+ "--idempotency-key <key>",
1496
+ "reuse the key of an unconfirmed creation to get the Agent it created"
1497
+ ),
1498
+ "createAgent"
1499
+ ).action(
1500
+ async (name, options) => agentCreateCommand(dependencies(), name, options)
3229
1501
  );
3230
- agent.command("use <agent>").description("write the agent hostname into this directory's .tonbo declaration").option("--force", "replace an existing agent binding without confirmation").action(
3231
- async (selector, options) => agentUseCommand(dependencies(jsonOutput(program)), selector, options.force === true)
1502
+ const apiKey = program.command("api-key").description("the keys of the account's members");
1503
+ operationOptions(
1504
+ apiKey.command("list").description("list your keys, or every key of the account for an owner or admin"),
1505
+ "listApiKeys"
1506
+ ).action(
1507
+ async (options) => apiKeyListCommand(dependencies(), options)
3232
1508
  );
3233
- agent.command("show").description("show the agent bound to this directory and its connection addresses").action(async () => agentShowCommand(dependencies(jsonOutput(program))));
3234
- program.command("deploy").description("upload this directory as a new Deployment and promote it to Production").option("--no-promote", "create the Deployment without moving Production").action(
3235
- async (options) => deployCommand(dependencies(jsonOutput(program)), { promote: options.promote })
1509
+ operationOptions(
1510
+ apiKey.command("create <name>").description("create a key and print its secret, once").option(
1511
+ "--member <member>",
1512
+ "an Agent (subject, id or name) to create the key for; yourself by default"
1513
+ ).option("--read-only", "narrow the key to read-only actions, for its whole life").option(
1514
+ "--bucket <name>",
1515
+ "narrow the key to this bucket, for its whole life; repeat for several",
1516
+ (value, previous = []) => [...previous, value]
1517
+ ).addOption(
1518
+ new Option("--expires-in <period>", "when the key expires; never by default").choices(
1519
+ apiKeyExpirations
1520
+ )
1521
+ ),
1522
+ "createApiKey"
1523
+ ).action(
1524
+ async (name, options) => apiKeyCreateCommand(dependencies(), name, options)
3236
1525
  );
3237
- const deployments = program.command("deployments").description("list and move between the immutable Deployments of the bound agent");
3238
- deployments.command("list", { isDefault: true }).description("list Deployments, newest first").action(async () => deploymentsListCommand(dependencies(jsonOutput(program))));
3239
- deployments.command("show <deployment>").description("show one Deployment and its Rollouts by id prefix").action(
3240
- async (prefix) => deploymentsShowCommand(dependencies(jsonOutput(program)), prefix)
1526
+ operationOptions(apiKey.command("show <key-id>").description("show one key"), "getApiKey").action(
1527
+ async (keyId, options) => apiKeyShowCommand(dependencies(), keyId, options)
3241
1528
  );
3242
- deployments.command("promote <deployment>").description("move Production to a Deployment by id prefix").action(
3243
- async (prefix) => deploymentsPromoteCommand(dependencies(jsonOutput(program)), prefix)
1529
+ operationOptions(
1530
+ apiKey.command("revoke <key-id>").description("revoke a key and every token minted from it").option("--yes", "revoke without asking; required when stdin is not a terminal"),
1531
+ "revokeApiKey"
1532
+ ).action(
1533
+ async (keyId, options) => apiKeyRevokeCommand(dependencies(), keyId, options)
3244
1534
  );
3245
- deployments.command("cancel-activities").description("cancel selected named work on an exact retired runtime").requiredOption("--sandbox <id>", "retired Sandbox UUID").requiredOption("--runtime-session <id>", "exact activation UUID").requiredOption("--work-key <keys...>", "named work keys to cancel").requiredOption("--reason <reason>", "reason retained in the cancellation audit").action(
3246
- async (options) => cancelActivitiesCommand(dependencies(jsonOutput(program)), options)
1535
+ const bucket = program.command("bucket").description("the account's Streams buckets");
1536
+ operationOptions(bucket.command("list").description("list the buckets"), "listBuckets").action(
1537
+ async (options) => bucketListCommand(dependencies(), options)
3247
1538
  );
3248
- deployments.command("rollback [deployment]").description("move Production back to the previous Production Deployment, or to a named one").action(
3249
- async (prefix) => deploymentsRollbackCommand(dependencies(jsonOutput(program)), prefix)
1539
+ operationOptions(
1540
+ bucket.command("create <name>").description("create a bucket").option(
1541
+ "--region <region>",
1542
+ "the bucket's region (see tonbo region list); the account's default region by default"
1543
+ ).option(
1544
+ "--idempotency-key <key>",
1545
+ "reuse the key of an unconfirmed creation to get the bucket it created"
1546
+ ),
1547
+ "createBucket"
1548
+ ).action(
1549
+ async (name, options) => bucketCreateCommand(dependencies(), name, options)
3250
1550
  );
3251
- program.command("run <prompt>").description("run one prompt as a durable Turn in an agent session and wait for its result").option("--session <session>", "resume an existing Agent Session UUID").option(
3252
- "--no-wait",
3253
- "return once the Turn is admitted; the platform runs it to its end (see tonbo turn wait)"
1551
+ operationOptions(bucket.command("show <name>").description("show a bucket"), "getBucket").action(
1552
+ async (name, options) => bucketShowCommand(dependencies(), name, options)
1553
+ );
1554
+ operationOptions(
1555
+ bucket.command("update <name>").description("publish a bucket for anonymous reads, or take it private").addOption(new Option("--public", "serve anonymous reads").conflicts("private")).addOption(new Option("--private", "serve only the account's keys").conflicts("public")),
1556
+ "updateBucket"
3254
1557
  ).action(
3255
- async (prompt, options) => runCommand(dependencies(jsonOutput(program)), prompt, options)
1558
+ async (name, options) => bucketUpdateCommand(dependencies(), name, options)
3256
1559
  );
3257
- program.command("turn").description("follow durable Turns of the bound agent").command("wait <turn>").description(
3258
- "wait for a Turn's result; the Turn runs on the platform whether or not anyone waits"
3259
- ).requiredOption("--session <session>", "the Turn's Agent Session UUID").action(
3260
- async (turnId, options) => turnWaitCommand(dependencies(jsonOutput(program)), turnId, options)
1560
+ operationOptions(
1561
+ bucket.command("delete <name>").description("delete a bucket and every stream in it").option("--yes", "delete without asking; required when stdin is not a terminal"),
1562
+ "deleteBucket"
1563
+ ).action(
1564
+ async (name, options) => bucketDeleteCommand(dependencies(), name, options)
3261
1565
  );
3262
- const secret = program.command("secret").description("manage encrypted environment secrets for the agent service");
3263
- secret.command("list").action(async () => secretListCommand(dependencies(jsonOutput(program))));
3264
- secret.command("set <name>").description("set a secret from an environment variable (the same name by default)").option("--from-env <name>", "read the value from another environment variable").action(
3265
- async (name, options) => secretSetCommand(dependencies(jsonOutput(program)), name, options)
1566
+ operationOptions(
1567
+ program.command("usage").description("the account's rated usage").command("show").description("show one month's rated usage").option(
1568
+ "--period <month>",
1569
+ "the calendar month in UTC, as YYYY-MM; the current one by default"
1570
+ ),
1571
+ "getUsage"
1572
+ ).action(
1573
+ async (options) => usageShowCommand(dependencies(), options)
3266
1574
  );
3267
- secret.command("remove <name>").action(async (name) => secretRemoveCommand(dependencies(jsonOutput(program)), name));
3268
1575
  return program;
3269
1576
  }
3270
1577