@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.
- package/README.md +60 -189
- package/dist/bin/tonbo.js +925 -2618
- 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 {
|
|
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
|
|
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 (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
"
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
"
|
|
450
|
-
"
|
|
451
|
-
|
|
452
|
-
"
|
|
453
|
-
"
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
"
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return this.
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
)
|
|
716
|
-
|
|
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
|
-
|
|
729
|
-
return this.
|
|
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
|
-
|
|
785
|
-
|
|
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
|
-
|
|
806
|
-
|
|
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
|
-
|
|
814
|
-
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
834
|
-
return this.
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
849
|
-
return this.
|
|
850
|
-
(response) => response.body
|
|
851
|
-
);
|
|
442
|
+
getApiKey(key, keyId) {
|
|
443
|
+
return this.call("getApiKey", key, { params: { key_id: keyId } });
|
|
852
444
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
869
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
922
|
-
|
|
467
|
+
deleteBucket(key, name) {
|
|
468
|
+
return this.call("deleteBucket", key, { params: { name } });
|
|
923
469
|
}
|
|
924
|
-
|
|
925
|
-
return
|
|
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
|
-
|
|
473
|
+
};
|
|
474
|
+
function withoutUndefined(value) {
|
|
475
|
+
return Object.fromEntries(
|
|
476
|
+
Object.entries(value).filter(([, field]) => field !== void 0)
|
|
477
|
+
);
|
|
931
478
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
|
1141
|
-
function
|
|
1142
|
-
|
|
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,
|
|
636
|
+
constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname()) {
|
|
1154
637
|
this.credentials = credentials;
|
|
1155
638
|
this.fetcher = fetcher;
|
|
1156
|
-
this.
|
|
639
|
+
this.loginOrigin = loginOrigin2;
|
|
1157
640
|
this.browserOpener = browserOpener;
|
|
641
|
+
this.hostname = hostname;
|
|
1158
642
|
}
|
|
1159
|
-
/** The
|
|
1160
|
-
*
|
|
1161
|
-
async
|
|
1162
|
-
const injected = process.env.
|
|
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
|
|
1165
|
-
if (!
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
() =>
|
|
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: "
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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: "
|
|
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(
|
|
699
|
+
await this.credentials.save(login);
|
|
1226
700
|
progress({ status: "completed", step: "credential-store" });
|
|
1227
|
-
return
|
|
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 ?? "/",
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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(
|
|
754
|
+
() => finish({ error: new Error(`The login was not approved (${denied}).`) }, responseSocket2)
|
|
1288
755
|
);
|
|
1289
756
|
return;
|
|
1290
757
|
}
|
|
1291
|
-
|
|
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(
|
|
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
|
-
//
|
|
1332
|
-
|
|
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
|
-
//
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1566
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
|
1612
|
-
const matcher = ignore().add(DEFAULT_IGNORES);
|
|
875
|
+
async function assertExistingDestinationIsSafe(filename) {
|
|
1613
876
|
try {
|
|
1614
|
-
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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/
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1825
|
-
|
|
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
|
-
|
|
1863
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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
|
|
2073
|
-
if (
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
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
|
|
2079
|
-
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
-
|
|
2126
|
-
|
|
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
|
|
2255
|
-
completed: "Browser
|
|
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
|
-
"
|
|
2262
|
-
started: "
|
|
2263
|
-
completed: "
|
|
977
|
+
"key-redemption": {
|
|
978
|
+
started: "Receiving the CLI key",
|
|
979
|
+
completed: "CLI key received"
|
|
2264
980
|
},
|
|
2265
981
|
"credential-store": {
|
|
2266
|
-
started: "Saving login
|
|
2267
|
-
completed: "Login
|
|
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
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
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 (
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
-
|
|
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
|
|
1007
|
+
await deps.api.revokeApiKey(login.key, login.key_id);
|
|
2342
1008
|
} catch (error) {
|
|
2343
|
-
|
|
2344
|
-
|
|
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.
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
`
|
|
2352
|
-
|
|
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
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
|
2546
|
-
|
|
2547
|
-
const
|
|
2548
|
-
const
|
|
2549
|
-
|
|
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
|
-
|
|
2552
|
-
const followed = await submitAndFollow(
|
|
1049
|
+
print(
|
|
2553
1050
|
deps,
|
|
2554
|
-
|
|
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
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
|
|
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
|
|
2649
|
-
const
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
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
|
|
2655
|
-
|
|
2656
|
-
return generation === null ? state : `${state} (generation ${generation})`;
|
|
1077
|
+
function memberName(member) {
|
|
1078
|
+
return member.name ?? member.display_name ?? "";
|
|
2657
1079
|
}
|
|
2658
|
-
function
|
|
2659
|
-
const
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
|
|
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
|
-
|
|
2667
|
-
|
|
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
|
-
|
|
1117
|
+
`${member} names several members (${[...subjects].join(", ")}); give the member's subject.`
|
|
2670
1118
|
);
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
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
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
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
|
|
2712
|
-
const
|
|
2713
|
-
|
|
2714
|
-
|
|
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
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
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
|
|
2760
|
-
const
|
|
2761
|
-
const
|
|
2762
|
-
const
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
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
|
|
2770
|
-
const
|
|
2771
|
-
|
|
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
|
|
2794
|
-
const
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
-
|
|
1228
|
+
"yes"
|
|
2803
1229
|
);
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
}
|
|
2810
|
-
function
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
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
|
|
2826
|
-
const
|
|
2827
|
-
|
|
2828
|
-
|
|
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
|
|
2835
|
-
const
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
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
|
|
2845
|
-
const
|
|
2846
|
-
|
|
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
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
const
|
|
2853
|
-
|
|
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
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
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
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
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
|
|
2995
|
-
|
|
2996
|
-
|
|
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
|
|
3091
|
-
const
|
|
3092
|
-
const
|
|
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
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
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
|
|
3120
|
-
|
|
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("
|
|
3124
|
-
program.command("
|
|
3125
|
-
|
|
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
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
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
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
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
|
-
|
|
3231
|
-
|
|
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
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
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
|
-
|
|
3238
|
-
|
|
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
|
-
|
|
3243
|
-
|
|
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
|
-
|
|
3246
|
-
|
|
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
|
-
|
|
3249
|
-
|
|
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
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
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 (
|
|
1558
|
+
async (name, options) => bucketUpdateCommand(dependencies(), name, options)
|
|
3256
1559
|
);
|
|
3257
|
-
|
|
3258
|
-
"
|
|
3259
|
-
|
|
3260
|
-
|
|
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
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
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
|
|