@tonbo/cli 0.3.4 → 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 -172
- package/dist/bin/tonbo.js +937 -2244
- package/package.json +7 -13
package/dist/bin/tonbo.js
CHANGED
|
@@ -1,284 +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
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
|
-
|
|
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
|
+
);
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
// src/contracts.ts
|
|
37
|
-
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
38
|
-
|
|
39
44
|
// src/generated/contracts.ts
|
|
40
|
-
var piAgentSchema = {
|
|
41
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
42
|
-
"$id": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json",
|
|
43
|
-
"title": "PI Agent v1",
|
|
44
|
-
"type": "object",
|
|
45
|
-
"additionalProperties": false,
|
|
46
|
-
"required": [
|
|
47
|
-
"runtime",
|
|
48
|
-
"driver"
|
|
49
|
-
],
|
|
50
|
-
"properties": {
|
|
51
|
-
"runtime": {
|
|
52
|
-
"const": "pi"
|
|
53
|
-
},
|
|
54
|
-
"secrets": {
|
|
55
|
-
"type": "array",
|
|
56
|
-
"maxItems": 32,
|
|
57
|
-
"uniqueItems": true,
|
|
58
|
-
"items": {
|
|
59
|
-
"type": "string",
|
|
60
|
-
"pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
"driver": {
|
|
64
|
-
"oneOf": [
|
|
65
|
-
{
|
|
66
|
-
"type": "object",
|
|
67
|
-
"additionalProperties": false,
|
|
68
|
-
"required": [
|
|
69
|
-
"kind"
|
|
70
|
-
],
|
|
71
|
-
"properties": {
|
|
72
|
-
"kind": {
|
|
73
|
-
"const": "native"
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
"type": "object",
|
|
79
|
-
"additionalProperties": false,
|
|
80
|
-
"required": [
|
|
81
|
-
"kind",
|
|
82
|
-
"protocol",
|
|
83
|
-
"command"
|
|
84
|
-
],
|
|
85
|
-
"properties": {
|
|
86
|
-
"kind": {
|
|
87
|
-
"const": "command"
|
|
88
|
-
},
|
|
89
|
-
"protocol": {
|
|
90
|
-
"const": "pi-rpc-v1"
|
|
91
|
-
},
|
|
92
|
-
"command": {
|
|
93
|
-
"type": "array",
|
|
94
|
-
"minItems": 1,
|
|
95
|
-
"maxItems": 64,
|
|
96
|
-
"items": {
|
|
97
|
-
"type": "string",
|
|
98
|
-
"minLength": 1,
|
|
99
|
-
"maxLength": 1024
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
]
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
var agentServiceSchema = {
|
|
109
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
110
|
-
"$id": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json",
|
|
111
|
-
"title": "Tonbo Agent application service v1",
|
|
112
|
-
"type": "object",
|
|
113
|
-
"additionalProperties": false,
|
|
114
|
-
"required": [
|
|
115
|
-
"command"
|
|
116
|
-
],
|
|
117
|
-
"properties": {
|
|
118
|
-
"command": {
|
|
119
|
-
"type": "array",
|
|
120
|
-
"minItems": 1,
|
|
121
|
-
"maxItems": 64,
|
|
122
|
-
"items": {
|
|
123
|
-
"type": "string",
|
|
124
|
-
"minLength": 1,
|
|
125
|
-
"maxLength": 4096
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
"secrets": {
|
|
129
|
-
"type": "array",
|
|
130
|
-
"maxItems": 32,
|
|
131
|
-
"uniqueItems": true,
|
|
132
|
-
"items": {
|
|
133
|
-
"type": "string",
|
|
134
|
-
"pattern": "^[A-Z_][A-Z0-9_]{0,127}$"
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
"kubernetes": {
|
|
138
|
-
"$ref": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json#/$defs/request"
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
var kubernetesProfilesSchema = {
|
|
143
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
144
|
-
"$id": "https://contracts.tonbo.dev/agents/kubernetes-profiles-v1.schema.json",
|
|
145
|
-
"title": "Tonbo managed Kubernetes profiles v1",
|
|
146
|
-
"$defs": {
|
|
147
|
-
"request": {
|
|
148
|
-
"type": "object",
|
|
149
|
-
"additionalProperties": false,
|
|
150
|
-
"required": [
|
|
151
|
-
"profile"
|
|
152
|
-
],
|
|
153
|
-
"properties": {
|
|
154
|
-
"profile": false
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
"x-tonbo-profiles": {}
|
|
159
|
-
};
|
|
160
|
-
var declarationSchema = {
|
|
161
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
162
|
-
"$id": "https://contracts.tonbo.dev/agents/tonbo-declaration-v2.schema.json",
|
|
163
|
-
"title": "Tonbo Agent declaration v2",
|
|
164
|
-
"type": "object",
|
|
165
|
-
"additionalProperties": false,
|
|
166
|
-
"required": [
|
|
167
|
-
"version",
|
|
168
|
-
"harness"
|
|
169
|
-
],
|
|
170
|
-
"properties": {
|
|
171
|
-
"version": {
|
|
172
|
-
"const": 2
|
|
173
|
-
},
|
|
174
|
-
"inference": {
|
|
175
|
-
"type": "object",
|
|
176
|
-
"additionalProperties": false,
|
|
177
|
-
"default": {
|
|
178
|
-
"model": "claude-sonnet-4-5"
|
|
179
|
-
},
|
|
180
|
-
"required": [
|
|
181
|
-
"model"
|
|
182
|
-
],
|
|
183
|
-
"properties": {
|
|
184
|
-
"model": {
|
|
185
|
-
"type": "string",
|
|
186
|
-
"minLength": 1,
|
|
187
|
-
"maxLength": 160
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
"build": {
|
|
192
|
-
"type": "object",
|
|
193
|
-
"additionalProperties": false,
|
|
194
|
-
"required": [
|
|
195
|
-
"command"
|
|
196
|
-
],
|
|
197
|
-
"properties": {
|
|
198
|
-
"command": {
|
|
199
|
-
"type": "array",
|
|
200
|
-
"minItems": 1,
|
|
201
|
-
"maxItems": 64,
|
|
202
|
-
"items": {
|
|
203
|
-
"type": "string",
|
|
204
|
-
"minLength": 1,
|
|
205
|
-
"maxLength": 1024
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
"service": {
|
|
211
|
-
"$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
|
|
212
|
-
},
|
|
213
|
-
"agent": {
|
|
214
|
-
"$ref": "https://contracts.tonbo.dev/agents/agent-name-v1.json#/$defs/publicHostname"
|
|
215
|
-
},
|
|
216
|
-
"harness": {
|
|
217
|
-
"$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
var deploymentSchema = {
|
|
222
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
223
|
-
"$id": "https://contracts.tonbo.dev/agents/managed-deployment-v1.schema.json",
|
|
224
|
-
"title": "Managed Agent Deployment v1",
|
|
225
|
-
"type": "object",
|
|
226
|
-
"additionalProperties": false,
|
|
227
|
-
"required": [
|
|
228
|
-
"version",
|
|
229
|
-
"agent",
|
|
230
|
-
"source",
|
|
231
|
-
"inference"
|
|
232
|
-
],
|
|
233
|
-
"properties": {
|
|
234
|
-
"version": {
|
|
235
|
-
"const": 1
|
|
236
|
-
},
|
|
237
|
-
"agent": {
|
|
238
|
-
"$ref": "https://contracts.tonbo.dev/agents/pi-agent-v1.schema.json"
|
|
239
|
-
},
|
|
240
|
-
"source": {
|
|
241
|
-
"type": "object",
|
|
242
|
-
"additionalProperties": false,
|
|
243
|
-
"required": [
|
|
244
|
-
"format",
|
|
245
|
-
"sha256",
|
|
246
|
-
"size_bytes"
|
|
247
|
-
],
|
|
248
|
-
"properties": {
|
|
249
|
-
"format": {
|
|
250
|
-
"const": "tar-v1"
|
|
251
|
-
},
|
|
252
|
-
"sha256": {
|
|
253
|
-
"type": "string",
|
|
254
|
-
"pattern": "^[0-9a-f]{64}$"
|
|
255
|
-
},
|
|
256
|
-
"size_bytes": {
|
|
257
|
-
"type": "integer",
|
|
258
|
-
"minimum": 1,
|
|
259
|
-
"maximum": 67108864
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
"inference": {
|
|
264
|
-
"type": "object",
|
|
265
|
-
"additionalProperties": false,
|
|
266
|
-
"required": [
|
|
267
|
-
"model"
|
|
268
|
-
],
|
|
269
|
-
"properties": {
|
|
270
|
-
"model": {
|
|
271
|
-
"type": "string",
|
|
272
|
-
"minLength": 1,
|
|
273
|
-
"maxLength": 160
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
},
|
|
277
|
-
"service": {
|
|
278
|
-
"$ref": "https://contracts.tonbo.dev/agents/agent-service-v1.schema.json"
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
45
|
var agentNameContract = {
|
|
283
46
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
284
47
|
"$id": "https://contracts.tonbo.dev/agents/agent-name-v1.json",
|
|
@@ -436,522 +199,298 @@ var agentNameContract = {
|
|
|
436
199
|
}
|
|
437
200
|
}
|
|
438
201
|
};
|
|
439
|
-
var
|
|
440
|
-
|
|
441
|
-
"
|
|
442
|
-
|
|
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
|
-
|
|
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"
|
|
478
346
|
}
|
|
479
347
|
};
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
var validateDeploymentSpec = ajv.compile(deploymentSchema);
|
|
493
|
-
var validateRegionDiscovery = ajv.compile(regionDiscoverySchema);
|
|
494
|
-
function parseRegionDiscovery(value) {
|
|
495
|
-
if (!validateRegionDiscovery(value)) {
|
|
496
|
-
throw new Error(validationMessage("Region discovery", validateRegionDiscovery.errors));
|
|
497
|
-
}
|
|
498
|
-
const result = value;
|
|
499
|
-
if (new Set(result.regions.map((region) => region.id)).size !== result.regions.length)
|
|
500
|
-
throw new Error("Region discovery contains duplicate region identities.");
|
|
501
|
-
return result;
|
|
502
|
-
}
|
|
503
|
-
function validationMessage(label, errors) {
|
|
504
|
-
const detail = errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ");
|
|
505
|
-
return `${label} is invalid${detail ? `: ${detail}` : "."}`;
|
|
506
|
-
}
|
|
507
|
-
function parseDeclaration(value) {
|
|
508
|
-
const candidate = structuredClone(value);
|
|
509
|
-
if (!validateDeclaration(candidate)) {
|
|
510
|
-
throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
|
|
511
|
-
}
|
|
512
|
-
return candidate;
|
|
513
|
-
}
|
|
514
|
-
function assertManagedDeploymentSpec(value) {
|
|
515
|
-
if (!validateDeploymentSpec(value)) {
|
|
516
|
-
throw new Error(validationMessage("Managed Deployment spec", validateDeploymentSpec.errors));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
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
|
+
];
|
|
519
360
|
|
|
520
361
|
// src/api.ts
|
|
521
|
-
var
|
|
522
|
-
|
|
362
|
+
var DEFAULT_API_ORIGIN = "https://api.tonbo.dev";
|
|
363
|
+
var ManagementApi = class {
|
|
364
|
+
constructor(fetcher, apiOrigin2 = DEFAULT_API_ORIGIN) {
|
|
523
365
|
this.fetcher = fetcher;
|
|
524
|
-
this.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
return this.
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
headers: {
|
|
541
|
-
authorization: `Bearer ${oauthToken}`,
|
|
542
|
-
...body === void 0 ? {} : { "content-type": "application/json" }
|
|
543
|
-
},
|
|
544
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
listAgents(oauthToken) {
|
|
548
|
-
return requestJson(
|
|
549
|
-
this.fetcher,
|
|
550
|
-
`${this.accountOrigin}/api/cli/agents`,
|
|
551
|
-
{
|
|
552
|
-
headers: { authorization: `Bearer ${oauthToken}` }
|
|
553
|
-
}
|
|
554
|
-
).then((body) => body.agents);
|
|
555
|
-
}
|
|
556
|
-
listOrganizations(oauthToken) {
|
|
557
|
-
return requestJson(
|
|
558
|
-
this.fetcher,
|
|
559
|
-
`${this.accountOrigin}/api/cli/accounts`,
|
|
560
|
-
{
|
|
561
|
-
headers: { authorization: `Bearer ${oauthToken}` }
|
|
562
|
-
}
|
|
563
|
-
).then((body) => body.organizations);
|
|
564
|
-
}
|
|
565
|
-
createAgent(oauthToken, name, organizationId, requestId = randomUUID()) {
|
|
566
|
-
return requestJson(
|
|
567
|
-
this.fetcher,
|
|
568
|
-
`${this.accountOrigin}/api/cli/agents`,
|
|
569
|
-
{
|
|
570
|
-
method: "POST",
|
|
571
|
-
headers: {
|
|
572
|
-
authorization: `Bearer ${oauthToken}`,
|
|
573
|
-
"content-type": "application/json"
|
|
574
|
-
},
|
|
575
|
-
body: JSON.stringify({
|
|
576
|
-
name,
|
|
577
|
-
requestId,
|
|
578
|
-
...organizationId ? { orgId: organizationId } : {}
|
|
579
|
-
})
|
|
580
|
-
}
|
|
581
|
-
).then((body) => body.agent).catch((cause) => {
|
|
582
|
-
if (cause instanceof Error && "status" in cause && typeof cause.status === "number" && cause.status >= 400 && cause.status < 500)
|
|
583
|
-
throw cause;
|
|
584
|
-
throw new Error(
|
|
585
|
-
`Agent creation was not confirmed. Query \`tonbo agent creation ${requestId}${organizationId ? ` --account ${organizationId}` : ""}\` or retry creation with --request-id ${requestId} and the same name and account.`,
|
|
586
|
-
{ cause }
|
|
587
|
-
);
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
readAgentCreationRequest(oauthToken, requestId, accountId) {
|
|
591
|
-
const account = accountId ? `?account=${encodeURIComponent(accountId)}` : "";
|
|
592
|
-
return requestJson(
|
|
593
|
-
this.fetcher,
|
|
594
|
-
`${this.accountOrigin}/api/cli/agents/creations/${encodeURIComponent(requestId)}${account}`,
|
|
595
|
-
{ headers: { authorization: `Bearer ${oauthToken}` } }
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
registerSshKey(oauthToken, key) {
|
|
599
|
-
return requestJson(
|
|
600
|
-
this.fetcher,
|
|
601
|
-
`${this.accountOrigin}/api/cli/ssh-keys`,
|
|
602
|
-
{
|
|
603
|
-
method: "POST",
|
|
604
|
-
headers: {
|
|
605
|
-
authorization: `Bearer ${oauthToken}`,
|
|
606
|
-
"content-type": "application/json"
|
|
607
|
-
},
|
|
608
|
-
body: JSON.stringify({
|
|
609
|
-
algorithm: key.algorithm,
|
|
610
|
-
key_base64: key.keyBase64,
|
|
611
|
-
label: key.label
|
|
612
|
-
})
|
|
613
|
-
}
|
|
614
|
-
).then((body) => body.key);
|
|
615
|
-
}
|
|
616
|
-
revokeSshKey(oauthToken, fingerprint) {
|
|
617
|
-
return requestJson(
|
|
618
|
-
this.fetcher,
|
|
619
|
-
`${this.accountOrigin}/api/cli/ssh-keys`,
|
|
620
|
-
{
|
|
621
|
-
method: "DELETE",
|
|
622
|
-
headers: {
|
|
623
|
-
authorization: `Bearer ${oauthToken}`,
|
|
624
|
-
"content-type": "application/json"
|
|
625
|
-
},
|
|
626
|
-
body: JSON.stringify({ fingerprint })
|
|
627
|
-
}
|
|
628
|
-
).then((body) => body.key);
|
|
629
|
-
}
|
|
630
|
-
exchangeManagementToken(oauthToken, agentId) {
|
|
631
|
-
return requestJson(
|
|
632
|
-
this.fetcher,
|
|
633
|
-
`${this.accountOrigin}/api/cli/agents/${agentId}/token`,
|
|
634
|
-
{
|
|
635
|
-
method: "POST",
|
|
636
|
-
headers: { authorization: `Bearer ${oauthToken}` }
|
|
637
|
-
}
|
|
638
|
-
).then((body) => body.access_token);
|
|
639
|
-
}
|
|
640
|
-
async deploy({
|
|
641
|
-
bundle,
|
|
642
|
-
origin,
|
|
643
|
-
agentId,
|
|
644
|
-
promote,
|
|
645
|
-
spec,
|
|
646
|
-
token
|
|
647
|
-
}) {
|
|
648
|
-
const descriptor = {
|
|
649
|
-
format: bundle.format,
|
|
650
|
-
sha256: bundle.sha256,
|
|
651
|
-
size_bytes: bundle.size_bytes
|
|
652
|
-
};
|
|
653
|
-
const bundlesPath = `/v1/agents/${agentId}/source-bundles`;
|
|
654
|
-
const prepared = await this.management("PUT", `${bundlesPath}/${bundle.sha256}`, token, {
|
|
655
|
-
format: descriptor.format,
|
|
656
|
-
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);
|
|
657
382
|
});
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
method: "PUT",
|
|
665
|
-
headers: prepared.upload_headers,
|
|
666
|
-
body: new Blob([new Uint8Array(bundle.bytes)])
|
|
667
|
-
});
|
|
668
|
-
let uploadError = null;
|
|
669
|
-
if (!uploaded.ok) {
|
|
670
|
-
uploadError = new Error(`Source upload failed with HTTP ${uploaded.status}.`);
|
|
671
|
-
}
|
|
672
|
-
try {
|
|
673
|
-
await this.management("POST", `${bundlesPath}/${bundle.sha256}/complete`, token, {
|
|
674
|
-
format: bundle.format,
|
|
675
|
-
size_bytes: bundle.size_bytes
|
|
676
|
-
});
|
|
677
|
-
} catch (error) {
|
|
678
|
-
if (uploadError)
|
|
679
|
-
throw new AggregateError([uploadError, error], "Source bundle upload did not complete.");
|
|
680
|
-
throw error;
|
|
681
|
-
}
|
|
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));
|
|
682
389
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
)
|
|
698
|
-
|
|
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;
|
|
699
412
|
}
|
|
700
|
-
return { deployment, production, unchanged };
|
|
701
|
-
}
|
|
702
|
-
createDeployment(agentId, body, token) {
|
|
703
|
-
return this.management(
|
|
704
|
-
"POST",
|
|
705
|
-
`/v1/agents/${agentId}/deployments`,
|
|
706
|
-
token,
|
|
707
|
-
body
|
|
708
|
-
).then((response) => response.data);
|
|
709
|
-
}
|
|
710
|
-
listDeployments(agentId, token) {
|
|
711
|
-
return this.managementList(`/v1/agents/${agentId}/deployments`, token);
|
|
712
|
-
}
|
|
713
|
-
getDeployment(agentId, deploymentId, token) {
|
|
714
|
-
return this.management(
|
|
715
|
-
"GET",
|
|
716
|
-
`/v1/agents/${agentId}/deployments/${deploymentId}`,
|
|
717
|
-
token
|
|
718
|
-
).then((response) => response.data);
|
|
719
413
|
}
|
|
720
|
-
|
|
721
|
-
return this.
|
|
722
|
-
"GET",
|
|
723
|
-
`/v1/agents/${agentId}/production`,
|
|
724
|
-
token
|
|
725
|
-
).then(
|
|
726
|
-
(response) => response.data,
|
|
727
|
-
(error) => {
|
|
728
|
-
if (error.status === 404) return null;
|
|
729
|
-
throw error;
|
|
730
|
-
}
|
|
731
|
-
);
|
|
414
|
+
getAccount(key) {
|
|
415
|
+
return this.call("getAccount", key);
|
|
732
416
|
}
|
|
733
|
-
|
|
734
|
-
return this.
|
|
735
|
-
"PUT",
|
|
736
|
-
`/v1/agents/${agentId}/production`,
|
|
737
|
-
token,
|
|
738
|
-
body
|
|
739
|
-
).then((response) => response.data);
|
|
417
|
+
listRegions(key) {
|
|
418
|
+
return this.call("listRegions", key);
|
|
740
419
|
}
|
|
741
|
-
|
|
742
|
-
return this.
|
|
420
|
+
listMembers(key, page) {
|
|
421
|
+
return this.page("listMembers", key, page);
|
|
743
422
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
prompt,
|
|
747
|
-
sessionId,
|
|
748
|
-
token,
|
|
749
|
-
turnId = randomUUID()
|
|
750
|
-
}) {
|
|
751
|
-
const agentPath = `/v1/agents/${agentId}`;
|
|
752
|
-
const production = await this.getProduction(agentId, token);
|
|
753
|
-
if (!production)
|
|
754
|
-
throw new Error("Agent has no Production Deployment; run `tonbo deploy` first.");
|
|
755
|
-
if (production.observed_state !== "running")
|
|
756
|
-
throw new Error(
|
|
757
|
-
`Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
|
|
758
|
-
);
|
|
759
|
-
const session = sessionId ? { id: sessionId } : (await this.management("POST", `${agentPath}/sessions`, token, {
|
|
760
|
-
deployment_id: production.deployment_id
|
|
761
|
-
})).data;
|
|
762
|
-
const turn = (await this.management(
|
|
763
|
-
"POST",
|
|
764
|
-
`${agentPath}/sessions/${session.id}/turns`,
|
|
765
|
-
token,
|
|
766
|
-
{ prompt },
|
|
767
|
-
turnId
|
|
768
|
-
)).data;
|
|
769
|
-
return { production, session, turn };
|
|
423
|
+
updateMember(key, subject, change) {
|
|
424
|
+
return this.call("updateMember", key, { params: { subject }, body: change });
|
|
770
425
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
return this.management(
|
|
779
|
-
"GET",
|
|
780
|
-
`/v1/agents/${agentId}/sessions/${sessionId}/turns/${turnId}/events?after=${after}`,
|
|
781
|
-
token
|
|
782
|
-
);
|
|
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"));
|
|
783
433
|
}
|
|
784
|
-
|
|
785
|
-
return this.
|
|
786
|
-
"GET",
|
|
787
|
-
`/v1/agents/${agentId}/secrets`,
|
|
788
|
-
token
|
|
789
|
-
).then((body) => body.data);
|
|
434
|
+
listApiKeys(key, page) {
|
|
435
|
+
return this.page("listApiKeys", key, page);
|
|
790
436
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
token,
|
|
796
|
-
{ value }
|
|
797
|
-
);
|
|
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) });
|
|
798
441
|
}
|
|
799
|
-
|
|
800
|
-
return this.
|
|
801
|
-
"DELETE",
|
|
802
|
-
`/v1/agents/${agentId}/secrets/${encodeURIComponent(name)}`,
|
|
803
|
-
token
|
|
804
|
-
);
|
|
442
|
+
getApiKey(key, keyId) {
|
|
443
|
+
return this.call("getApiKey", key, { params: { key_id: keyId } });
|
|
805
444
|
}
|
|
806
|
-
|
|
807
|
-
return
|
|
808
|
-
|
|
809
|
-
headers: {
|
|
810
|
-
authorization: `Bearer ${token}`,
|
|
811
|
-
...body === void 0 ? {} : {
|
|
812
|
-
"content-type": "application/json",
|
|
813
|
-
"idempotency-key": idempotencyKey ?? randomUUID()
|
|
814
|
-
}
|
|
815
|
-
},
|
|
816
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
445
|
+
revokeApiKey(key, keyId) {
|
|
446
|
+
return this.call("revokeApiKey", key, {
|
|
447
|
+
params: { key_id: keyId }
|
|
817
448
|
});
|
|
818
449
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
let cursor = null;
|
|
822
|
-
do {
|
|
823
|
-
const separator = path6.includes("?") ? "&" : "?";
|
|
824
|
-
const page = await this.management(
|
|
825
|
-
"GET",
|
|
826
|
-
`${path6}${separator}limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
|
|
827
|
-
token
|
|
828
|
-
);
|
|
829
|
-
values.push(...page.data);
|
|
830
|
-
cursor = page.next_cursor;
|
|
831
|
-
} while (cursor);
|
|
832
|
-
return values;
|
|
450
|
+
listBuckets(key, page) {
|
|
451
|
+
return this.page("listBuckets", key, page);
|
|
833
452
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
var AGENTS_INSTRUCTIONS_PATH = "AGENTS.md";
|
|
842
|
-
var PI_SETTINGS_PATH = ".pi/settings.json";
|
|
843
|
-
var MAX_HARNESS_CONFIG_BYTES = 256 * 1024;
|
|
844
|
-
var supportedHarnesses = [
|
|
845
|
-
{
|
|
846
|
-
command: "pi",
|
|
847
|
-
id: "pi",
|
|
848
|
-
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"));
|
|
849
460
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
{ driver: "native", harness: "pi" },
|
|
853
|
-
{ driver: "command", harness: "pi" }
|
|
854
|
-
];
|
|
855
|
-
var AgentSourceInspectionError = class extends Error {
|
|
856
|
-
code;
|
|
857
|
-
path;
|
|
858
|
-
constructor(code, path6, message, options) {
|
|
859
|
-
super(message, options);
|
|
860
|
-
this.code = code;
|
|
861
|
-
this.path = path6;
|
|
862
|
-
this.name = "AgentSourceInspectionError";
|
|
461
|
+
getBucket(key, name) {
|
|
462
|
+
return this.call("getBucket", key, { params: { name } });
|
|
863
463
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
let settings;
|
|
867
|
-
try {
|
|
868
|
-
settings = JSON.parse(contents);
|
|
869
|
-
} catch (error) {
|
|
870
|
-
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 });
|
|
871
466
|
}
|
|
872
|
-
|
|
873
|
-
|
|
467
|
+
deleteBucket(key, name) {
|
|
468
|
+
return this.call("deleteBucket", key, { params: { name } });
|
|
874
469
|
}
|
|
875
|
-
|
|
876
|
-
return
|
|
877
|
-
const packages = settings.packages;
|
|
878
|
-
if (!Array.isArray(packages)) {
|
|
879
|
-
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 } });
|
|
880
472
|
}
|
|
881
|
-
|
|
473
|
+
};
|
|
474
|
+
function withoutUndefined(value) {
|
|
475
|
+
return Object.fromEntries(
|
|
476
|
+
Object.entries(value).filter(([, field]) => field !== void 0)
|
|
477
|
+
);
|
|
882
478
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
version: 1,
|
|
891
|
-
instructions: { agentsMd },
|
|
892
|
-
harness: {
|
|
893
|
-
id: "pi",
|
|
894
|
-
reason: "pi_settings_found",
|
|
895
|
-
state: "identified"
|
|
896
|
-
},
|
|
897
|
-
pi: {
|
|
898
|
-
packageCount: parsePiPackageCount(piSettings),
|
|
899
|
-
settingsFound: true
|
|
900
|
-
}
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
return {
|
|
904
|
-
version: 1,
|
|
905
|
-
instructions: { agentsMd },
|
|
906
|
-
harness: {
|
|
907
|
-
candidates: supportedHarnesses.map((harness) => harness.id),
|
|
908
|
-
reason: "no_harness_specific_config",
|
|
909
|
-
state: "selection_required"
|
|
910
|
-
},
|
|
911
|
-
pi: {
|
|
912
|
-
packageCount: 0,
|
|
913
|
-
settingsFound: false
|
|
914
|
-
}
|
|
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
|
+
);
|
|
915
486
|
};
|
|
916
487
|
}
|
|
917
488
|
|
|
918
|
-
// src/agent-source.ts
|
|
919
|
-
var LocalAgentSource = class {
|
|
920
|
-
constructor(root) {
|
|
921
|
-
this.root = root;
|
|
922
|
-
}
|
|
923
|
-
async has(filename) {
|
|
924
|
-
const metadata = await this.metadata(filename);
|
|
925
|
-
return metadata?.isFile() === true;
|
|
926
|
-
}
|
|
927
|
-
async readText(filename, maxBytes) {
|
|
928
|
-
const metadata = await this.metadata(filename);
|
|
929
|
-
if (!metadata) return null;
|
|
930
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
931
|
-
throw new Error(`${filename} must be a regular file.`);
|
|
932
|
-
}
|
|
933
|
-
if (metadata.size > maxBytes) {
|
|
934
|
-
throw new Error(`${filename} exceeds the ${maxBytes}-byte inspection limit.`);
|
|
935
|
-
}
|
|
936
|
-
return readFile(path.join(this.root, filename), "utf8");
|
|
937
|
-
}
|
|
938
|
-
async metadata(filename) {
|
|
939
|
-
try {
|
|
940
|
-
return await lstat(path.join(this.root, filename));
|
|
941
|
-
} catch (error) {
|
|
942
|
-
if (error.code === "ENOENT") return null;
|
|
943
|
-
throw error;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
};
|
|
947
|
-
function inspectLocalAgentSource(root) {
|
|
948
|
-
return inspectAgentSource(new LocalAgentSource(root));
|
|
949
|
-
}
|
|
950
|
-
|
|
951
489
|
// src/auth.ts
|
|
952
490
|
import { createHash, randomBytes } from "node:crypto";
|
|
953
491
|
import { createServer } from "node:http";
|
|
954
492
|
import { execFile } from "node:child_process";
|
|
493
|
+
import os from "node:os";
|
|
955
494
|
import { promisify } from "node:util";
|
|
956
495
|
|
|
957
496
|
// src/callback-page.ts
|
|
@@ -1088,102 +627,78 @@ function callbackResponse(outcome) {
|
|
|
1088
627
|
|
|
1089
628
|
// src/auth.ts
|
|
1090
629
|
var exec = promisify(execFile);
|
|
1091
|
-
var
|
|
1092
|
-
function
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
function withAbsoluteExpiry(tokens) {
|
|
1096
|
-
if (tokens.expires_at || !Number.isInteger(tokens.expires_in) || (tokens.expires_in ?? 0) <= 0)
|
|
1097
|
-
return tokens;
|
|
1098
|
-
return {
|
|
1099
|
-
...tokens,
|
|
1100
|
-
expires_at: Math.floor(Date.now() / 1e3) + (tokens.expires_in ?? 0)
|
|
1101
|
-
};
|
|
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";
|
|
1102
634
|
}
|
|
1103
635
|
var AuthClient = class {
|
|
1104
|
-
constructor(credentials, fetcher,
|
|
636
|
+
constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname()) {
|
|
1105
637
|
this.credentials = credentials;
|
|
1106
638
|
this.fetcher = fetcher;
|
|
1107
|
-
this.
|
|
639
|
+
this.loginOrigin = loginOrigin2;
|
|
1108
640
|
this.browserOpener = browserOpener;
|
|
641
|
+
this.hostname = hostname;
|
|
1109
642
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
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();
|
|
1112
647
|
if (injected) return injected;
|
|
1113
|
-
const
|
|
1114
|
-
if (!
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
client_id: config.client_id,
|
|
1125
|
-
grant_type: "refresh_token",
|
|
1126
|
-
refresh_token: tokens.refresh_token
|
|
1127
|
-
})
|
|
1128
|
-
)
|
|
1129
|
-
);
|
|
1130
|
-
if (!refreshed.refresh_token) refreshed.refresh_token = tokens.refresh_token;
|
|
1131
|
-
await this.credentials.save(refreshed);
|
|
1132
|
-
return refreshed.access_token;
|
|
1133
|
-
}
|
|
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
|
+
*/
|
|
1134
659
|
async login(progress = () => {
|
|
1135
|
-
}) {
|
|
1136
|
-
|
|
1137
|
-
const config = await this.config();
|
|
1138
|
-
progress({ status: "completed", step: "account-config" });
|
|
1139
|
-
if (config.redirect_uri !== LOOPBACK_REDIRECT)
|
|
1140
|
-
throw new Error(`Unsupported OAuth redirect URI: ${config.redirect_uri}`);
|
|
1141
|
-
const verifier = base64url(randomBytes(48));
|
|
660
|
+
}, account) {
|
|
661
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
1142
662
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
1143
|
-
const state =
|
|
1144
|
-
const authorization = new URL(config.authorization_endpoint);
|
|
1145
|
-
authorization.search = new URLSearchParams({
|
|
1146
|
-
client_id: config.client_id,
|
|
1147
|
-
code_challenge: challenge,
|
|
1148
|
-
code_challenge_method: "S256",
|
|
1149
|
-
redirect_uri: config.redirect_uri,
|
|
1150
|
-
response_type: "code",
|
|
1151
|
-
scope: "openid profile email",
|
|
1152
|
-
state
|
|
1153
|
-
}).toString();
|
|
663
|
+
const state = randomBytes(24).toString("base64url");
|
|
1154
664
|
const code = await this.listenForCode(
|
|
1155
665
|
state,
|
|
1156
|
-
() =>
|
|
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
|
+
},
|
|
1157
678
|
progress
|
|
1158
679
|
);
|
|
1159
|
-
progress({ status: "started", step: "
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
redirect_uri: config.redirect_uri
|
|
1169
|
-
})
|
|
1170
|
-
)
|
|
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
|
+
}
|
|
1171
689
|
);
|
|
1172
|
-
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
|
+
};
|
|
1173
698
|
progress({ status: "started", step: "credential-store" });
|
|
1174
|
-
await this.credentials.save(
|
|
699
|
+
await this.credentials.save(login);
|
|
1175
700
|
progress({ status: "completed", step: "credential-store" });
|
|
1176
|
-
return
|
|
1177
|
-
}
|
|
1178
|
-
config() {
|
|
1179
|
-
return requestJson(this.fetcher, `${this.accountOrigin}/api/cli/config`);
|
|
1180
|
-
}
|
|
1181
|
-
exchange(endpoint, body) {
|
|
1182
|
-
return requestJson(this.fetcher, endpoint, {
|
|
1183
|
-
method: "POST",
|
|
1184
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
1185
|
-
body
|
|
1186
|
-
});
|
|
701
|
+
return login;
|
|
1187
702
|
}
|
|
1188
703
|
listenForCode(expectedState, ready, progress) {
|
|
1189
704
|
return new Promise((resolve, reject) => {
|
|
@@ -1225,19 +740,23 @@ var AuthClient = class {
|
|
|
1225
740
|
5 * 60 * 1e3
|
|
1226
741
|
);
|
|
1227
742
|
const server = createServer((request, response) => {
|
|
1228
|
-
const url = new URL(request.url ?? "/",
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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) {
|
|
1232
750
|
const responseSocket2 = response.socket;
|
|
1233
751
|
respond(
|
|
1234
752
|
response,
|
|
1235
753
|
"denied",
|
|
1236
|
-
() => finish({ error: new Error(
|
|
754
|
+
() => finish({ error: new Error(`The login was not approved (${denied}).`) }, responseSocket2)
|
|
1237
755
|
);
|
|
1238
756
|
return;
|
|
1239
757
|
}
|
|
1240
|
-
|
|
758
|
+
const code = url.searchParams.get("code");
|
|
759
|
+
if (!code) {
|
|
1241
760
|
respond(response, "invalid");
|
|
1242
761
|
return;
|
|
1243
762
|
}
|
|
@@ -1252,10 +771,11 @@ var AuthClient = class {
|
|
|
1252
771
|
});
|
|
1253
772
|
server.once("error", (error) => finish({ error }));
|
|
1254
773
|
progress({ status: "started", step: "callback-server" });
|
|
1255
|
-
server.listen(
|
|
774
|
+
server.listen(0, LOOPBACK_HOST, () => {
|
|
775
|
+
const { port } = server.address();
|
|
1256
776
|
progress({ status: "completed", step: "callback-server" });
|
|
1257
777
|
progress({ status: "started", step: "browser" });
|
|
1258
|
-
void ready().then(startAuthorization).catch(
|
|
778
|
+
void ready(`http://${LOOPBACK_HOST}:${port}/callback`).then(startAuthorization).catch(
|
|
1259
779
|
(error) => finish({
|
|
1260
780
|
error: error instanceof Error ? error : new Error("Could not open the login browser.")
|
|
1261
781
|
})
|
|
@@ -1277,420 +797,96 @@ async function openBrowser(url) {
|
|
|
1277
797
|
return void await exec("xdg-open", [url]);
|
|
1278
798
|
}
|
|
1279
799
|
|
|
1280
|
-
//
|
|
1281
|
-
|
|
1282
|
-
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"] };
|
|
1283
|
-
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"] };
|
|
1284
|
-
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"] } };
|
|
1285
|
-
|
|
1286
|
-
// ../../packages/agent-source-bundler/dist/src/inference-default.js
|
|
1287
|
-
var DEFAULT_INFERENCE_MODEL = inferenceDefault.model;
|
|
800
|
+
// src/commands.ts
|
|
801
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1288
802
|
|
|
1289
|
-
//
|
|
1290
|
-
|
|
1291
|
-
|
|
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");
|
|
1292
811
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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;
|
|
820
|
+
try {
|
|
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 });
|
|
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;
|
|
832
|
+
}
|
|
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;
|
|
1301
844
|
try {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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);
|
|
853
|
+
} catch (error) {
|
|
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 });
|
|
1308
857
|
}
|
|
1309
858
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
859
|
+
async remove() {
|
|
860
|
+
await assertExistingDestinationIsSafe(this.filename);
|
|
861
|
+
await rm(this.filename, { force: true });
|
|
862
|
+
}
|
|
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)));
|
|
1315
868
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
// src/build.ts
|
|
1322
|
-
import { spawn } from "node:child_process";
|
|
1323
|
-
async function runBuildCommand(root, command) {
|
|
1324
|
-
if (command.length === 0) throw new Error("Build command must not be empty.");
|
|
1325
|
-
await new Promise((resolve, reject) => {
|
|
1326
|
-
const child = spawn(command[0], command.slice(1), {
|
|
1327
|
-
cwd: root,
|
|
1328
|
-
env: process.env,
|
|
1329
|
-
// Command output is progress, not the CLI result. Keep stdout available
|
|
1330
|
-
// for the single JSON document emitted by `tonbo --json deploy`.
|
|
1331
|
-
stdio: ["inherit", process.stderr, process.stderr]
|
|
1332
|
-
});
|
|
1333
|
-
child.once("error", reject);
|
|
1334
|
-
child.once("exit", (code, signal) => {
|
|
1335
|
-
if (code === 0) resolve();
|
|
1336
|
-
else
|
|
1337
|
-
reject(
|
|
1338
|
-
new Error(
|
|
1339
|
-
`Build command failed${signal ? ` with signal ${signal}` : ` with status ${code}`}.`
|
|
1340
|
-
)
|
|
1341
|
-
);
|
|
1342
|
-
});
|
|
1343
|
-
});
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
// ../../packages/agent-source-bundler/dist/src/native-pi.js
|
|
1347
|
-
import { stringify } from "smol-toml";
|
|
1348
|
-
var PI_SLACK_TEMPLATE_SERVICE = piSlackTemplate.service;
|
|
1349
|
-
function nativePiDeclaration(model, service) {
|
|
1350
|
-
return {
|
|
1351
|
-
version: 2,
|
|
1352
|
-
harness: { runtime: "pi", driver: { kind: "native" } },
|
|
1353
|
-
inference: { model },
|
|
1354
|
-
...service ? { service } : {}
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// src/declaration.ts
|
|
1359
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1360
|
-
import { lstat as lstat2, open, readFile as readFile2, rename, rm } from "node:fs/promises";
|
|
1361
|
-
import path2 from "node:path";
|
|
1362
|
-
import { parse, stringify as stringify2 } from "smol-toml";
|
|
1363
|
-
var DECLARATION_FILENAME = ".tonbo";
|
|
1364
|
-
function createDeclaration(model = DEFAULT_INFERENCE_MODEL, driver = { kind: "native" }, buildCommand, agentHostname) {
|
|
1365
|
-
const base = nativePiDeclaration(model.trim());
|
|
1366
|
-
return parseDeclaration({
|
|
1367
|
-
...base,
|
|
1368
|
-
...agentHostname ? { agent: agentHostname } : {},
|
|
1369
|
-
harness: { ...base.harness, driver },
|
|
1370
|
-
...buildCommand ? { build: { command: buildCommand } } : {}
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
function renderDeclaration(declaration) {
|
|
1374
|
-
return `# Tonbo Agent configuration.
|
|
1375
|
-
# Edit this file directly or run \`tonbo init\` to reconfigure.
|
|
1376
|
-
${stringify2(declaration)}`;
|
|
1377
|
-
}
|
|
1378
|
-
async function bindDeclarationAgent(root, agentHostname) {
|
|
1379
|
-
const declaration = await loadDeclaration(root);
|
|
1380
|
-
const bound = parseDeclaration({ ...declaration, agent: agentHostname });
|
|
1381
|
-
await saveDeclaration(root, bound, true);
|
|
1382
|
-
return bound;
|
|
1383
|
-
}
|
|
1384
|
-
async function declarationExists(root) {
|
|
1385
|
-
const filename = path2.join(root, DECLARATION_FILENAME);
|
|
1386
|
-
try {
|
|
1387
|
-
const metadata = await lstat2(filename);
|
|
1388
|
-
if (metadata.isSymbolicLink() || !metadata.isFile())
|
|
1389
|
-
throw new Error(`${filename} must be a regular file.`);
|
|
1390
|
-
return true;
|
|
1391
|
-
} catch (error) {
|
|
1392
|
-
if (error.code === "ENOENT") return false;
|
|
1393
|
-
throw error;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
async function saveDeclaration(root, declaration, overwrite) {
|
|
1397
|
-
const filename = path2.join(root, DECLARATION_FILENAME);
|
|
1398
|
-
const exists = await declarationExists(root);
|
|
1399
|
-
if (exists && !overwrite)
|
|
1400
|
-
throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
|
|
1401
|
-
const contents = renderDeclaration(declaration);
|
|
1402
|
-
if (!overwrite) {
|
|
1403
|
-
const handle2 = await open(filename, "wx", 420).catch((error) => {
|
|
1404
|
-
if (error.code === "EEXIST")
|
|
1405
|
-
throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
|
|
1406
|
-
throw error;
|
|
1407
|
-
});
|
|
1408
|
-
try {
|
|
1409
|
-
await handle2.writeFile(contents, "utf8");
|
|
1410
|
-
await handle2.sync();
|
|
1411
|
-
} finally {
|
|
1412
|
-
await handle2.close();
|
|
1413
|
-
}
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
const temporary = path2.join(root, `.${DECLARATION_FILENAME}.${process.pid}.${randomUUID2()}.tmp`);
|
|
1417
|
-
let handle;
|
|
1418
|
-
try {
|
|
1419
|
-
handle = await open(temporary, "wx", 420);
|
|
1420
|
-
await handle.writeFile(contents, "utf8");
|
|
1421
|
-
await handle.sync();
|
|
1422
|
-
await handle.close();
|
|
1423
|
-
handle = void 0;
|
|
1424
|
-
await rename(temporary, filename);
|
|
1425
|
-
} catch (error) {
|
|
1426
|
-
await handle?.close().catch(() => void 0);
|
|
1427
|
-
await rm(temporary, { force: true }).catch(() => void 0);
|
|
1428
|
-
throw error;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
async function loadDeclaration(declarationRoot) {
|
|
1432
|
-
const filename = path2.join(declarationRoot, DECLARATION_FILENAME);
|
|
1433
|
-
let parsed;
|
|
1434
|
-
try {
|
|
1435
|
-
parsed = parse(await readFile2(filename, "utf8"));
|
|
1436
|
-
} catch (error) {
|
|
1437
|
-
if (error.code === "ENOENT")
|
|
1438
|
-
throw new Error(`No ${DECLARATION_FILENAME} declaration found at ${filename}.`);
|
|
1439
|
-
throw new Error(`Could not read ${filename} as TOML.`, { cause: error });
|
|
1440
|
-
}
|
|
1441
|
-
return parseDeclaration(parsed);
|
|
1442
|
-
}
|
|
1443
|
-
function buildDeploymentSpec(declaration, source) {
|
|
1444
|
-
const spec = {
|
|
1445
|
-
version: 1,
|
|
1446
|
-
agent: declaration.harness,
|
|
1447
|
-
source: {
|
|
1448
|
-
format: source.format,
|
|
1449
|
-
sha256: source.sha256,
|
|
1450
|
-
size_bytes: source.size_bytes
|
|
1451
|
-
},
|
|
1452
|
-
inference: declaration.inference,
|
|
1453
|
-
...declaration.service ? { service: declaration.service } : {}
|
|
1454
|
-
};
|
|
1455
|
-
assertManagedDeploymentSpec(spec);
|
|
1456
|
-
return spec;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
// ../../packages/agent-source-bundler/dist/src/index.js
|
|
1460
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1461
|
-
import { lstat as lstat3, readFile as readFile3, readdir } from "node:fs/promises";
|
|
1462
|
-
import path3 from "node:path";
|
|
1463
|
-
import ignore from "ignore";
|
|
1464
|
-
import tar from "tar-stream";
|
|
1465
|
-
var MAX_BUNDLE_BYTES = sourceBundleContract.max_bytes;
|
|
1466
|
-
var SESSION_SOURCE_DIRECTORY = piSessionContract.session_directory.replace(/^\/+|\/+$/g, "");
|
|
1467
|
-
var DEFAULT_IGNORES = [
|
|
1468
|
-
".git/",
|
|
1469
|
-
"node_modules/",
|
|
1470
|
-
".DS_Store",
|
|
1471
|
-
".env",
|
|
1472
|
-
".env.*",
|
|
1473
|
-
"!.env.example",
|
|
1474
|
-
".tonbo-cache/",
|
|
1475
|
-
".tonbo-system/",
|
|
1476
|
-
...sourceBundleContract.runtime_cache_directories.map((directory) => `${directory}/`)
|
|
1477
|
-
];
|
|
1478
|
-
var EXACT_NPM_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
1479
|
-
var GIT_COMMIT = /^[0-9a-f]{40}$/i;
|
|
1480
|
-
function packageSource(value, index) {
|
|
1481
|
-
if (typeof value === "string")
|
|
1482
|
-
return value;
|
|
1483
|
-
if (value && typeof value === "object" && "source" in value && typeof value.source === "string")
|
|
1484
|
-
return value.source;
|
|
1485
|
-
throw new Error(`.pi/settings.json packages[${index}] must be a source string or object.`);
|
|
1486
|
-
}
|
|
1487
|
-
async function validatePackageSource(root, source) {
|
|
1488
|
-
if (source.startsWith("npm:")) {
|
|
1489
|
-
const specifier = source.slice(4);
|
|
1490
|
-
const separator = specifier.lastIndexOf("@");
|
|
1491
|
-
if (separator <= 0 || !EXACT_NPM_VERSION.test(specifier.slice(separator + 1))) {
|
|
1492
|
-
throw new Error(`PI package ${source} must pin an exact npm version, for example npm:my-agent@1.2.3.`);
|
|
1493
|
-
}
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
if (source.startsWith("git:")) {
|
|
1497
|
-
const separator = source.lastIndexOf("@");
|
|
1498
|
-
if (separator <= "git:".length || !GIT_COMMIT.test(source.slice(separator + 1))) {
|
|
1499
|
-
throw new Error(`PI package ${source} must pin a full 40-character Git commit.`);
|
|
1500
|
-
}
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
if (source.startsWith("./") || source.startsWith("../")) {
|
|
1504
|
-
const settingsDirectory = path3.join(root, ".pi");
|
|
1505
|
-
const resolved = path3.resolve(settingsDirectory, source);
|
|
1506
|
-
const relative = path3.relative(root, resolved);
|
|
1507
|
-
if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative)) {
|
|
1508
|
-
throw new Error(`Local PI package ${source} resolves outside the deployed agent.`);
|
|
1509
|
-
}
|
|
1510
|
-
let metadata;
|
|
1511
|
-
try {
|
|
1512
|
-
metadata = await lstat3(resolved);
|
|
1513
|
-
} catch (error) {
|
|
1514
|
-
if (error.code === "ENOENT") {
|
|
1515
|
-
throw new Error(`Local PI package ${source} does not exist.`);
|
|
1516
|
-
}
|
|
1517
|
-
throw error;
|
|
1518
|
-
}
|
|
1519
|
-
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
1520
|
-
throw new Error(`Local PI package ${source} must resolve to a real agent directory.`);
|
|
1521
|
-
}
|
|
1522
|
-
return;
|
|
1523
|
-
}
|
|
1524
|
-
throw new Error(`PI package ${source} must use an exact npm version, a full Git commit, or a agent-local path.`);
|
|
1525
|
-
}
|
|
1526
|
-
async function validatePiPackages(root) {
|
|
1527
|
-
const filename = path3.join(root, ".pi", "settings.json");
|
|
1528
|
-
let settings;
|
|
1529
|
-
try {
|
|
1530
|
-
settings = JSON.parse(await readFile3(filename, "utf8"));
|
|
1531
|
-
} catch (error) {
|
|
1532
|
-
if (error.code === "ENOENT")
|
|
1533
|
-
return;
|
|
1534
|
-
throw new Error(`Could not read ${filename} as JSON.`, { cause: error });
|
|
1535
|
-
}
|
|
1536
|
-
if (!settings || typeof settings !== "object" || !("packages" in settings))
|
|
1537
|
-
return;
|
|
1538
|
-
const packages = settings.packages;
|
|
1539
|
-
if (!Array.isArray(packages))
|
|
1540
|
-
throw new Error(`${filename} packages must be an array.`);
|
|
1541
|
-
await Promise.all(packages.map((value, index) => validatePackageSource(root, packageSource(value, index))));
|
|
1542
|
-
}
|
|
1543
|
-
async function findDeclarationRoot(start) {
|
|
1544
|
-
let candidate = path3.resolve(start);
|
|
1545
|
-
for (; ; ) {
|
|
1546
|
-
try {
|
|
1547
|
-
if ((await lstat3(path3.join(candidate, ".tonbo"))).isFile())
|
|
1548
|
-
return candidate;
|
|
1549
|
-
} catch (error) {
|
|
1550
|
-
if (error.code !== "ENOENT")
|
|
1551
|
-
throw error;
|
|
1552
|
-
}
|
|
1553
|
-
const parent = path3.dirname(candidate);
|
|
1554
|
-
if (parent === candidate) {
|
|
1555
|
-
throw new Error(`No .tonbo declaration found above ${path3.resolve(start)}.`);
|
|
1556
|
-
}
|
|
1557
|
-
candidate = parent;
|
|
1558
|
-
}
|
|
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);
|
|
1559
874
|
}
|
|
1560
|
-
async function
|
|
1561
|
-
const matcher = ignore().add(DEFAULT_IGNORES);
|
|
875
|
+
async function assertExistingDestinationIsSafe(filename) {
|
|
1562
876
|
try {
|
|
1563
|
-
|
|
877
|
+
const stat = await lstat(filename);
|
|
878
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
879
|
+
throw new Error("Tonbo credential path must be a regular file.");
|
|
1564
880
|
} catch (error) {
|
|
1565
|
-
if (error.code !== "ENOENT")
|
|
1566
|
-
throw error;
|
|
1567
|
-
}
|
|
1568
|
-
return matcher;
|
|
1569
|
-
}
|
|
1570
|
-
async function collectFiles(root) {
|
|
1571
|
-
const matcher = await sourceIgnore(root);
|
|
1572
|
-
const files = [];
|
|
1573
|
-
async function visit(directory, relativeDirectory) {
|
|
1574
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
1575
|
-
entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
|
|
1576
|
-
for (const entry of entries) {
|
|
1577
|
-
const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
|
|
1578
|
-
const ignored = matcher.ignores(relative + (entry.isDirectory() ? "/" : ""));
|
|
1579
|
-
if (ignored)
|
|
1580
|
-
continue;
|
|
1581
|
-
if (relative === SESSION_SOURCE_DIRECTORY || relative.startsWith(`${SESSION_SOURCE_DIRECTORY}/`)) {
|
|
1582
|
-
throw new Error(`Source path ${SESSION_SOURCE_DIRECTORY}/ is reserved for durable PI history. Rename it or exclude it with .tonboignore.`);
|
|
1583
|
-
}
|
|
1584
|
-
const absolute = path3.join(directory, entry.name);
|
|
1585
|
-
if (entry.isDirectory()) {
|
|
1586
|
-
await visit(absolute, relative);
|
|
1587
|
-
continue;
|
|
1588
|
-
}
|
|
1589
|
-
const metadata = await lstat3(absolute);
|
|
1590
|
-
if (!metadata.isFile()) {
|
|
1591
|
-
throw new Error(`Source path ${relative} is not a regular file. V1 does not follow symlinks or special files.`);
|
|
1592
|
-
}
|
|
1593
|
-
files.push({
|
|
1594
|
-
absolute,
|
|
1595
|
-
mode: metadata.mode & 73 ? 493 : 420,
|
|
1596
|
-
relative,
|
|
1597
|
-
size: metadata.size
|
|
1598
|
-
});
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
await visit(root, "");
|
|
1602
|
-
if (!files.some((file) => file.relative === ".tonbo")) {
|
|
1603
|
-
throw new Error("The source bundle must contain .tonbo.");
|
|
1604
|
-
}
|
|
1605
|
-
return files;
|
|
1606
|
-
}
|
|
1607
|
-
function addEntry(pack, file, contents) {
|
|
1608
|
-
return new Promise((resolve, reject) => {
|
|
1609
|
-
pack.entry({
|
|
1610
|
-
gid: 0,
|
|
1611
|
-
mode: file.mode,
|
|
1612
|
-
mtime: /* @__PURE__ */ new Date(0),
|
|
1613
|
-
name: file.relative,
|
|
1614
|
-
size: contents.length,
|
|
1615
|
-
type: "file",
|
|
1616
|
-
uid: 0
|
|
1617
|
-
}, contents, (error) => error ? reject(error) : resolve());
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
async function buildSourceBundle(root) {
|
|
1621
|
-
const resolvedRoot = path3.resolve(root);
|
|
1622
|
-
await validatePiPackages(resolvedRoot);
|
|
1623
|
-
const files = await collectFiles(resolvedRoot);
|
|
1624
|
-
const payloadBytes = files.reduce((total, file) => total + file.size, 0);
|
|
1625
|
-
if (payloadBytes > MAX_BUNDLE_BYTES) {
|
|
1626
|
-
throw new Error(`Source files exceed ${MAX_BUNDLE_BYTES} bytes after ignore rules.`);
|
|
1627
|
-
}
|
|
1628
|
-
const pack = tar.pack();
|
|
1629
|
-
const chunks = [];
|
|
1630
|
-
let size = 0;
|
|
1631
|
-
pack.on("data", (chunk) => {
|
|
1632
|
-
size += chunk.length;
|
|
1633
|
-
if (size > MAX_BUNDLE_BYTES) {
|
|
1634
|
-
pack.destroy(new Error(`Source bundle exceeds ${MAX_BUNDLE_BYTES} bytes after ignore rules.`));
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
chunks.push(chunk);
|
|
1638
|
-
});
|
|
1639
|
-
const completed = new Promise((resolve, reject) => {
|
|
1640
|
-
pack.on("end", resolve);
|
|
1641
|
-
pack.on("error", reject);
|
|
1642
|
-
});
|
|
1643
|
-
for (const file of files) {
|
|
1644
|
-
await addEntry(pack, file, await readFile3(file.absolute));
|
|
881
|
+
if (error.code !== "ENOENT") throw error;
|
|
1645
882
|
}
|
|
1646
|
-
pack.finalize();
|
|
1647
|
-
await completed;
|
|
1648
|
-
const bytes = Buffer.concat(chunks);
|
|
1649
|
-
return {
|
|
1650
|
-
bytes,
|
|
1651
|
-
format: sourceBundleContract.format,
|
|
1652
|
-
root: resolvedRoot,
|
|
1653
|
-
sha256: createHash2("sha256").update(bytes).digest("hex"),
|
|
1654
|
-
size_bytes: bytes.length
|
|
1655
|
-
};
|
|
1656
883
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
var DEFAULT_PUBLIC_KEYS = ["id_ed25519.pub", "id_ecdsa.pub", "id_rsa.pub"];
|
|
1664
|
-
var ALGORITHMS = /* @__PURE__ */ new Set(["ssh-ed25519", "ecdsa-sha2-nistp256", "ssh-rsa"]);
|
|
1665
|
-
function parseOpenSshPublicKey(value, label) {
|
|
1666
|
-
const fields = value.trim().split(/\s+/);
|
|
1667
|
-
if (fields.length < 2 || !ALGORITHMS.has(fields[0]))
|
|
1668
|
-
throw new Error("SSH public key must be Ed25519, ECDSA P-256, or RSA.");
|
|
1669
|
-
const blob = Buffer.from(fields[1], "base64");
|
|
1670
|
-
if (blob.length < 32 || blob.length > 16384 || blob.toString("base64") !== fields[1])
|
|
1671
|
-
throw new Error("SSH public key is not canonical base64.");
|
|
1672
|
-
return {
|
|
1673
|
-
algorithm: fields[0],
|
|
1674
|
-
fingerprint: `SHA256:${createHash3("sha256").update(blob).digest("base64").replace(/=$/, "")}`,
|
|
1675
|
-
keyBase64: fields[1],
|
|
1676
|
-
label
|
|
1677
|
-
};
|
|
1678
|
-
}
|
|
1679
|
-
async function readSshPublicKey(path6) {
|
|
1680
|
-
return parseOpenSshPublicKey(await readFile4(path6, "utf8"), basename(path6, ".pub"));
|
|
1681
|
-
}
|
|
1682
|
-
async function readDefaultSshPublicKeys(sshDirectory = join(homedir(), ".ssh")) {
|
|
1683
|
-
const keys = [];
|
|
1684
|
-
for (const name of DEFAULT_PUBLIC_KEYS) {
|
|
1685
|
-
const key = await readSshPublicKey(join(sshDirectory, name)).catch(
|
|
1686
|
-
(error) => {
|
|
1687
|
-
if (error.code === "ENOENT") return null;
|
|
1688
|
-
throw error;
|
|
1689
|
-
}
|
|
1690
|
-
);
|
|
1691
|
-
if (key) keys.push(key);
|
|
1692
|
-
}
|
|
1693
|
-
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.");
|
|
1694
890
|
}
|
|
1695
891
|
|
|
1696
892
|
// src/agent-name.ts
|
|
@@ -1705,379 +901,63 @@ function validateAgentName(name) {
|
|
|
1705
901
|
);
|
|
1706
902
|
}
|
|
1707
903
|
}
|
|
1708
|
-
function agentNameSuggestion(value) {
|
|
1709
|
-
let name = value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
1710
|
-
if (!/^[a-z]/.test(name)) name = `agent-${name}`;
|
|
1711
|
-
name = name.slice(0, AGENT_NAME_RULE.maxLength).replace(/-+$/g, "");
|
|
1712
|
-
if (name.length < AGENT_NAME_RULE.minLength) {
|
|
1713
|
-
name = `${name || "agent"}-agent`.slice(0, AGENT_NAME_RULE.maxLength);
|
|
1714
|
-
}
|
|
1715
|
-
if (RESERVED_AGENT_NAMES.has(name)) name = `${name}-agent`;
|
|
1716
|
-
return name;
|
|
1717
|
-
}
|
|
1718
904
|
|
|
1719
|
-
// src/
|
|
1720
|
-
|
|
1721
|
-
const
|
|
1722
|
-
|
|
1723
|
-
if (!declaration.agent) {
|
|
1724
|
-
throw new Error("No Agent is configured in .tonbo. Run `tonbo agent use <agent>` first.");
|
|
1725
|
-
}
|
|
1726
|
-
const oauthToken = await deps.auth.accessToken();
|
|
1727
|
-
return {
|
|
1728
|
-
declaration,
|
|
1729
|
-
oauthToken,
|
|
1730
|
-
agent: selectAgent(await deps.api.listAgents(oauthToken), declaration.agent),
|
|
1731
|
-
root
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
function selectAgent(agents, selector) {
|
|
1735
|
-
const normalized = selector.toLowerCase();
|
|
1736
|
-
const matches = agents.filter(
|
|
1737
|
-
(agent) => agent.id === selector || agent.name === normalized || agent.publicHostname === normalized
|
|
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))
|
|
1738
909
|
);
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
|
|
1756
|
-
const inspection = await deps.inspectSource(root);
|
|
1757
|
-
let harness = options.harness ?? (existingDeclaration ? "pi" : void 0);
|
|
1758
|
-
if (harness !== void 0 && harness !== "pi") {
|
|
1759
|
-
throw new Error("--harness must name a supported Harness: pi.");
|
|
1760
|
-
}
|
|
1761
|
-
let driver = options.driver ?? existingDeclaration?.harness.driver.kind;
|
|
1762
|
-
if (driver !== void 0 && driver !== "native" && driver !== "command") {
|
|
1763
|
-
throw new Error("--driver must be native or command.");
|
|
1764
|
-
}
|
|
1765
|
-
if (!harness) {
|
|
1766
|
-
if (inspection.harness.state === "identified") harness = inspection.harness.id;
|
|
1767
|
-
else if (!interactive) {
|
|
1768
|
-
throw new Error("No Harness-specific configuration found. Pass --harness pi.");
|
|
1769
|
-
} else {
|
|
1770
|
-
deps.output({
|
|
1771
|
-
message: "No Harness-specific configuration was found in this agent."
|
|
1772
|
-
});
|
|
1773
|
-
harness = await selectHarness(deps);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
if (!driver) {
|
|
1777
|
-
if (!interactive) {
|
|
1778
|
-
throw new Error("PI execution mode is required. Pass --driver native or --driver command.");
|
|
1779
|
-
}
|
|
1780
|
-
if (inspection.pi.settingsFound) {
|
|
1781
|
-
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"}.`;
|
|
1782
|
-
deps.output({ message: packageSummary });
|
|
1783
|
-
}
|
|
1784
|
-
driver = "native";
|
|
1785
|
-
}
|
|
1786
|
-
if (driver === "native" && (options.agentEntry || options.buildCommand)) {
|
|
1787
|
-
throw new Error("--agent-entry and --build-command require --driver command.");
|
|
1788
|
-
}
|
|
1789
|
-
const existingEntry = existingDeclaration?.harness.driver.kind === "command" && existingDeclaration.harness.driver.command[0] === "node" ? existingDeclaration.harness.driver.command[1] : void 0;
|
|
1790
|
-
let entry = options.agentEntry?.trim() || existingEntry;
|
|
1791
|
-
if (driver === "command" && !entry && interactive) {
|
|
1792
|
-
entry = (await deps.prompt("PI SDK entry file [dist/agent.mjs]: ")).trim();
|
|
1793
|
-
}
|
|
1794
|
-
entry ||= "dist/agent.mjs";
|
|
1795
|
-
let model = options.model?.trim() || existingDeclaration?.inference.model || DEFAULT_INFERENCE_MODEL;
|
|
1796
|
-
let buildCommand = driver === "command" ? options.buildCommand ?? existingDeclaration?.build?.command ?? ["npm", "run", "build"] : void 0;
|
|
1797
|
-
let agentPlan = existingDeclaration?.agent ? {
|
|
1798
|
-
kind: "existing",
|
|
1799
|
-
publicHostname: existingDeclaration.agent
|
|
1800
|
-
} : { kind: "later" };
|
|
1801
|
-
if (interactive && !existingDeclaration?.agent) {
|
|
1802
|
-
agentPlan = await selectInitAgent(deps, root);
|
|
1803
|
-
}
|
|
1804
|
-
if (interactive) {
|
|
1805
|
-
while (true) {
|
|
1806
|
-
deps.output({
|
|
1807
|
-
message: renderInitSummary(agentPlan, harness, driver, model, entry, buildCommand)
|
|
1808
|
-
});
|
|
1809
|
-
const saveLabel = initSaveLabel(exists, agentPlan);
|
|
1810
|
-
const action = await deps.select(`${saveLabel} with this configuration?`, [
|
|
1811
|
-
{
|
|
1812
|
-
name: saveLabel,
|
|
1813
|
-
value: "save"
|
|
1814
|
-
},
|
|
1815
|
-
{
|
|
1816
|
-
description: "Change the agent, Harness, execution mode, model, or SDK entrypoint.",
|
|
1817
|
-
name: "Edit configuration",
|
|
1818
|
-
value: "edit"
|
|
1819
|
-
},
|
|
1820
|
-
{ name: "Cancel", value: "cancel" }
|
|
1821
|
-
]);
|
|
1822
|
-
if (action === "cancel") {
|
|
1823
|
-
deps.output({
|
|
1824
|
-
message: exists ? `Cancelled. Kept existing ${DECLARATION_FILENAME}.` : `Cancelled. Did not create ${DECLARATION_FILENAME}.`
|
|
1825
|
-
});
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
|
-
if (action === "save") break;
|
|
1829
|
-
const setting = await deps.select("What would you like to change?", [
|
|
1830
|
-
{
|
|
1831
|
-
description: initAgentSummary(agentPlan),
|
|
1832
|
-
name: "Agent",
|
|
1833
|
-
value: "agent"
|
|
1834
|
-
},
|
|
1835
|
-
{ description: harnessName(harness), name: "Harness", value: "harness" },
|
|
1836
|
-
{
|
|
1837
|
-
description: driver === "native" ? "PI CLI (`pi`)" : "PI SDK app",
|
|
1838
|
-
name: "Execution mode",
|
|
1839
|
-
value: "driver"
|
|
1840
|
-
},
|
|
1841
|
-
{ description: model, name: "Inference model", value: "model" },
|
|
1842
|
-
...driver === "command" ? [{ description: entry, name: "PI SDK entry file", value: "entry" }] : [],
|
|
1843
|
-
{ name: "Back to review", value: "back" }
|
|
1844
|
-
]);
|
|
1845
|
-
if (setting === "agent") agentPlan = await selectInitAgent(deps, root);
|
|
1846
|
-
if (setting === "harness") harness = await selectHarness(deps);
|
|
1847
|
-
if (setting === "driver") {
|
|
1848
|
-
driver = await deps.select(
|
|
1849
|
-
"How should Tonbo start PI?",
|
|
1850
|
-
piTargetChoices(inspection.pi.settingsFound)
|
|
1851
|
-
);
|
|
1852
|
-
if (driver === "command") {
|
|
1853
|
-
const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
|
|
1854
|
-
if (answer) entry = answer;
|
|
1855
|
-
buildCommand = options.buildCommand ?? ["npm", "run", "build"];
|
|
1856
|
-
} else {
|
|
1857
|
-
buildCommand = void 0;
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
if (setting === "model") {
|
|
1861
|
-
const answer = (await deps.prompt(`Inference model [${model}]: `)).trim();
|
|
1862
|
-
if (answer) model = answer;
|
|
1863
|
-
}
|
|
1864
|
-
if (setting === "entry") {
|
|
1865
|
-
const answer = (await deps.prompt(`PI SDK entry file [${entry}]: `)).trim();
|
|
1866
|
-
if (answer) entry = answer;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
let agentHostname = agentPlan.kind === "existing" ? agentPlan.publicHostname : void 0;
|
|
1871
|
-
let createdAgent;
|
|
1872
|
-
if (agentPlan.kind === "create") {
|
|
1873
|
-
const oauthToken = await deps.auth.accessToken();
|
|
1874
|
-
createdAgent = await deps.api.createAgent(oauthToken, agentPlan.name, agentPlan.organizationId);
|
|
1875
|
-
agentHostname = createdAgent.publicHostname;
|
|
1876
|
-
}
|
|
1877
|
-
const configuredDeclaration = createDeclaration(
|
|
1878
|
-
model,
|
|
1879
|
-
driver === "native" ? { kind: "native" } : { kind: "command", protocol: "pi-rpc-v1", command: ["node", entry] },
|
|
1880
|
-
buildCommand,
|
|
1881
|
-
agentHostname
|
|
1882
|
-
);
|
|
1883
|
-
const declaration = parseDeclaration({
|
|
1884
|
-
...configuredDeclaration,
|
|
1885
|
-
harness: {
|
|
1886
|
-
...configuredDeclaration.harness,
|
|
1887
|
-
...existingDeclaration?.harness.secrets ? { secrets: existingDeclaration.harness.secrets } : {}
|
|
1888
|
-
},
|
|
1889
|
-
...existingDeclaration?.service ? { service: existingDeclaration.service } : {}
|
|
1890
|
-
});
|
|
1891
|
-
try {
|
|
1892
|
-
await saveDeclaration(root, declaration, overwrite);
|
|
1893
|
-
} catch (error) {
|
|
1894
|
-
if (createdAgent) {
|
|
1895
|
-
throw new Error(
|
|
1896
|
-
`Created agent ${createdAgent.name}, but could not write ${DECLARATION_FILENAME}. Fix the file and run \`tonbo agent use ${createdAgent.publicHostname}\`.`,
|
|
1897
|
-
{ cause: error }
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
1900
|
-
throw error;
|
|
1901
|
-
}
|
|
1902
|
-
const driverSummary = driver === "command" ? `PI SDK app
|
|
1903
|
-
Build: ${buildCommand?.join(" ")}
|
|
1904
|
-
Entrypoint: node ${entry}` : inspection.pi.packageCount > 0 ? `PI CLI with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}` : "PI CLI";
|
|
1905
|
-
const agentSummary = createdAgent ? `Agent: ${createdAgent.name} (${createdAgent.publicHostname})
|
|
1906
|
-
Organization: ${createdAgent.organizationName}
|
|
1907
|
-
Application: https://${createdAgent.publicHostname}
|
|
1908
|
-
SSH after deploy: ssh ${createdAgent.sshDestination}` : agentPlan.kind === "existing" ? `Agent: ${agentPlan.name ? `${agentPlan.name} (${agentPlan.publicHostname})` : agentPlan.publicHostname}${agentPlan.organizationName ? `
|
|
1909
|
-
Organization: ${agentPlan.organizationName}` : ""}${agentPlan.sshDestination ? `
|
|
1910
|
-
SSH after deploy: ssh ${agentPlan.sshDestination}` : ""}` : "Next: tonbo agent create <name>";
|
|
1911
|
-
deps.output({
|
|
1912
|
-
message: `${createdAgent ? `Created agent ${createdAgent.name} and` : exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
|
|
1913
|
-
${agentSummary}
|
|
1914
|
-
Harness: PI
|
|
1915
|
-
Mode: ${driverSummary}${agentPlan.kind === "later" ? "" : "\nNext: tonbo deploy"}`,
|
|
1916
|
-
declaration,
|
|
1917
|
-
path: path4.join(root, DECLARATION_FILENAME)
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
function initSaveLabel(exists, agent) {
|
|
1921
|
-
if (agent.kind === "create") {
|
|
1922
|
-
return exists ? `Create agent and replace ${DECLARATION_FILENAME}` : `Create agent and ${DECLARATION_FILENAME}`;
|
|
1923
|
-
}
|
|
1924
|
-
return exists ? `Replace ${DECLARATION_FILENAME}` : `Create ${DECLARATION_FILENAME}`;
|
|
1925
|
-
}
|
|
1926
|
-
async function selectInitAgent(deps, root) {
|
|
1927
|
-
for (; ; ) {
|
|
1928
|
-
const action = await deps.select("How should this directory connect to Tonbo?", [
|
|
1929
|
-
{
|
|
1930
|
-
description: "Create it only after you confirm the complete configuration.",
|
|
1931
|
-
name: "Create a new agent",
|
|
1932
|
-
value: "create"
|
|
1933
|
-
},
|
|
1934
|
-
{
|
|
1935
|
-
description: "Bind this source tree to one agent you can access.",
|
|
1936
|
-
name: "Use an existing agent",
|
|
1937
|
-
value: "existing"
|
|
1938
|
-
},
|
|
1939
|
-
{
|
|
1940
|
-
description: "Write an unbound declaration and configure its agent later.",
|
|
1941
|
-
name: "Set up later",
|
|
1942
|
-
value: "later"
|
|
1943
|
-
}
|
|
1944
|
-
]);
|
|
1945
|
-
if (action === "later") return { kind: "later" };
|
|
1946
|
-
if (action === "create") {
|
|
1947
|
-
const organization = await selectInitOrganization(deps);
|
|
1948
|
-
const defaultName = agentNameSuggestion(path4.basename(root)) || "tonbo-agent";
|
|
1949
|
-
for (; ; ) {
|
|
1950
|
-
const answer = (await deps.prompt(`Agent name [${defaultName}]: `)).trim().toLowerCase();
|
|
1951
|
-
const name = answer || defaultName;
|
|
1952
|
-
try {
|
|
1953
|
-
validateAgentName(name);
|
|
1954
|
-
return {
|
|
1955
|
-
kind: "create",
|
|
1956
|
-
name,
|
|
1957
|
-
organizationId: organization?.id,
|
|
1958
|
-
organizationName: organization?.name
|
|
1959
|
-
};
|
|
1960
|
-
} catch (error) {
|
|
1961
|
-
deps.output({
|
|
1962
|
-
message: error instanceof Error ? error.message : "Agent name is invalid."
|
|
1963
|
-
});
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
const oauthToken = await deps.auth.accessToken();
|
|
1968
|
-
const agents = (await deps.api.listAgents(oauthToken)).filter(
|
|
1969
|
-
(agent2) => agent2.status === "active"
|
|
1970
|
-
);
|
|
1971
|
-
if (agents.length === 0) {
|
|
1972
|
-
deps.output({ message: "No active agents are available. Create a new agent instead." });
|
|
1973
|
-
continue;
|
|
1974
|
-
}
|
|
1975
|
-
const id = await deps.select(
|
|
1976
|
-
"Which agent should this directory use?",
|
|
1977
|
-
agents.map((agent2) => ({
|
|
1978
|
-
name: agent2.name,
|
|
1979
|
-
description: `Organization: ${agent2.organizationName} \xB7 ${agent2.publicHostname} \xB7 ${agent2.id}`,
|
|
1980
|
-
value: agent2.id
|
|
1981
|
-
}))
|
|
1982
|
-
);
|
|
1983
|
-
const agent = selectAgent(agents, id);
|
|
1984
|
-
return {
|
|
1985
|
-
kind: "existing",
|
|
1986
|
-
name: agent.name,
|
|
1987
|
-
organizationName: agent.organizationName,
|
|
1988
|
-
publicHostname: agent.publicHostname,
|
|
1989
|
-
sshDestination: agent.sshDestination
|
|
1990
|
-
};
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
function initAgentSummary(agent) {
|
|
1994
|
-
if (agent.kind === "create")
|
|
1995
|
-
return `Create ${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""} (permanent address assigned after creation)`;
|
|
1996
|
-
if (agent.kind === "existing") {
|
|
1997
|
-
return agent.name ? `${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""} (${agent.publicHostname})` : `Keep ${agent.publicHostname}`;
|
|
1998
|
-
}
|
|
1999
|
-
return "Set up later";
|
|
2000
|
-
}
|
|
2001
|
-
async function selectInitOrganization(deps) {
|
|
2002
|
-
const oauthToken = await deps.auth.accessToken();
|
|
2003
|
-
const organizations = (await deps.api.listOrganizations(oauthToken)).filter(
|
|
2004
|
-
(organization) => organization.role !== "member"
|
|
2005
|
-
);
|
|
2006
|
-
if (organizations.length === 0)
|
|
2007
|
-
throw new Error("No organization lets you create a agent. Ask an admin or owner.");
|
|
2008
|
-
if (organizations.length === 1) return organizations[0];
|
|
2009
|
-
const id = await deps.select(
|
|
2010
|
-
"Which organization should own the new agent?",
|
|
2011
|
-
organizations.map((organization) => ({
|
|
2012
|
-
description: organization.role,
|
|
2013
|
-
name: organization.name,
|
|
2014
|
-
value: organization.id
|
|
2015
|
-
}))
|
|
2016
|
-
);
|
|
2017
|
-
return organizations.find((organization) => organization.id === id) ?? organizations[0];
|
|
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);
|
|
2018
926
|
}
|
|
2019
|
-
|
|
2020
|
-
return
|
|
2021
|
-
"Which supported Harness should Tonbo use?",
|
|
2022
|
-
supportedHarnesses.map((supported) => ({
|
|
2023
|
-
description: `Run this agent with the ${supported.command} command.`,
|
|
2024
|
-
name: `${supported.name} (\`${supported.command}\`)`,
|
|
2025
|
-
value: supported.id
|
|
2026
|
-
}))
|
|
2027
|
-
);
|
|
927
|
+
function when(timestamp, absent = "never") {
|
|
928
|
+
return timestamp ?? absent;
|
|
2028
929
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
930
|
+
|
|
931
|
+
// src/commands.ts
|
|
932
|
+
function print(deps, json, data, human) {
|
|
933
|
+
deps.output(json ? JSON.stringify(data) : human);
|
|
2032
934
|
}
|
|
2033
|
-
function
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
"Tonbo Agent configuration:",
|
|
2037
|
-
` Agent: ${initAgentSummary(agent)}`,
|
|
2038
|
-
` Harness: ${harnessName(harness)}`,
|
|
2039
|
-
` Execution: ${driver === "native" ? "PI CLI (`pi`)" : "PI SDK app"}`,
|
|
2040
|
-
` Model: ${model}`
|
|
2041
|
-
];
|
|
2042
|
-
if (driver === "command") {
|
|
2043
|
-
lines.push(` Build: ${buildCommand?.join(" ") ?? "none"}`);
|
|
2044
|
-
lines.push(` Entrypoint: node ${entry}`);
|
|
2045
|
-
}
|
|
2046
|
-
return lines.join("\n");
|
|
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}`);
|
|
2047
938
|
}
|
|
2048
|
-
function
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
},
|
|
2055
|
-
native: {
|
|
2056
|
-
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.",
|
|
2057
|
-
name: "PI CLI (`pi`)",
|
|
2058
|
-
value: "native"
|
|
2059
|
-
}
|
|
2060
|
-
};
|
|
2061
|
-
return supportedExecutionTargets.filter((target) => target.harness === "pi").map((target) => choices[target.driver]);
|
|
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.");
|
|
2062
945
|
}
|
|
2063
|
-
async function loginCommand(deps) {
|
|
946
|
+
async function loginCommand(deps, options = {}) {
|
|
2064
947
|
try {
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
deps.
|
|
2070
|
-
|
|
948
|
+
const login = await deps.auth.login(
|
|
949
|
+
(event) => reportLoginProgress(deps.progress, event),
|
|
950
|
+
options.account
|
|
951
|
+
);
|
|
952
|
+
deps.output(
|
|
953
|
+
`Logged in to account ${login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`
|
|
954
|
+
);
|
|
2071
955
|
} catch (error) {
|
|
2072
956
|
deps.progress.fail();
|
|
2073
957
|
throw error;
|
|
2074
958
|
}
|
|
2075
959
|
}
|
|
2076
960
|
var LOGIN_PROGRESS_MESSAGES = {
|
|
2077
|
-
"account-config": {
|
|
2078
|
-
started: "Connecting to Tonbo",
|
|
2079
|
-
completed: "Connected to Tonbo"
|
|
2080
|
-
},
|
|
2081
961
|
"callback-server": {
|
|
2082
962
|
started: "Starting local browser callback",
|
|
2083
963
|
completed: "Local browser callback ready"
|
|
@@ -2087,20 +967,20 @@ var LOGIN_PROGRESS_MESSAGES = {
|
|
|
2087
967
|
completed: "Browser opened"
|
|
2088
968
|
},
|
|
2089
969
|
"browser-authorization": {
|
|
2090
|
-
started: "Waiting for browser
|
|
2091
|
-
completed: "Browser
|
|
970
|
+
started: "Waiting for browser approval",
|
|
971
|
+
completed: "Browser approval received"
|
|
2092
972
|
},
|
|
2093
973
|
"callback-close": {
|
|
2094
974
|
started: "Closing local browser callback",
|
|
2095
975
|
completed: "Local browser callback closed"
|
|
2096
976
|
},
|
|
2097
|
-
"
|
|
2098
|
-
started: "
|
|
2099
|
-
completed: "
|
|
977
|
+
"key-redemption": {
|
|
978
|
+
started: "Receiving the CLI key",
|
|
979
|
+
completed: "CLI key received"
|
|
2100
980
|
},
|
|
2101
981
|
"credential-store": {
|
|
2102
|
-
started: "Saving login
|
|
2103
|
-
completed: "Login
|
|
982
|
+
started: "Saving login",
|
|
983
|
+
completed: "Login saved"
|
|
2104
984
|
}
|
|
2105
985
|
};
|
|
2106
986
|
function reportLoginProgress(progress, event) {
|
|
@@ -2108,517 +988,352 @@ function reportLoginProgress(progress, event) {
|
|
|
2108
988
|
if (event.status === "started") progress.start(messages.started);
|
|
2109
989
|
else progress.succeed(messages.completed);
|
|
2110
990
|
}
|
|
2111
|
-
async function
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
const key = await deps.api.revokeSshKey(oauthToken, fingerprint);
|
|
2120
|
-
deps.output({ message: `Revoked SSH key ${key.fingerprint}.`, key });
|
|
2121
|
-
}
|
|
2122
|
-
async function agentUseCommand(deps, selector, force = false) {
|
|
2123
|
-
const root = await findDeclarationRoot(deps.cwd());
|
|
2124
|
-
const declaration = await loadDeclaration(root);
|
|
2125
|
-
const oauthToken = await deps.auth.accessToken();
|
|
2126
|
-
const agents = await deps.api.listAgents(oauthToken);
|
|
2127
|
-
const agent = selectAgent(agents, selector);
|
|
2128
|
-
if (declaration.agent === agent.publicHostname) {
|
|
2129
|
-
deps.output({
|
|
2130
|
-
message: `.tonbo already uses agent ${agent.publicHostname}.`,
|
|
2131
|
-
agent
|
|
2132
|
-
});
|
|
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.");
|
|
2133
999
|
return;
|
|
2134
1000
|
}
|
|
2135
|
-
if (
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
if (!deps.interactive()) {
|
|
2139
|
-
throw new Error(
|
|
2140
|
-
`.tonbo already uses agent ${currentLabel}. Pass --force to rebind it to ${agent.publicHostname}.`
|
|
2141
|
-
);
|
|
2142
|
-
}
|
|
2143
|
-
const action = await deps.select(
|
|
2144
|
-
`Rebind .tonbo from ${currentLabel} to ${agent.publicHostname}?`,
|
|
2145
|
-
[
|
|
2146
|
-
{ name: `Rebind to ${agent.publicHostname}`, value: "rebind" },
|
|
2147
|
-
{ name: "Cancel", value: "cancel" }
|
|
2148
|
-
]
|
|
2149
|
-
);
|
|
2150
|
-
if (action !== "rebind") {
|
|
2151
|
-
deps.output({ message: `Cancelled. .tonbo still uses agent ${currentLabel}.` });
|
|
2152
|
-
return;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
await bindDeclarationAgent(root, agent.publicHostname);
|
|
2156
|
-
deps.output({ message: `Bound .tonbo to agent ${agent.publicHostname}.`, agent });
|
|
2157
|
-
}
|
|
2158
|
-
async function agentCreateCommand(deps, name, options = {}) {
|
|
2159
|
-
const root = await findDeclarationRoot(deps.cwd());
|
|
2160
|
-
const declaration = await loadDeclaration(root);
|
|
2161
|
-
if (declaration.agent) {
|
|
2162
|
-
throw new Error(
|
|
2163
|
-
`.tonbo is already bound to agent ${declaration.agent}. Run \`tonbo agent use <agent>\` to review a rebind.`
|
|
2164
|
-
);
|
|
1001
|
+
if (!login) {
|
|
1002
|
+
deps.output("Not logged in.");
|
|
1003
|
+
return;
|
|
2165
1004
|
}
|
|
2166
|
-
|
|
2167
|
-
validateAgentName(name);
|
|
2168
|
-
const requestId = options.requestId ?? randomUUID3();
|
|
2169
|
-
const agent = await deps.api.createAgent(oauthToken, name, options.account, requestId);
|
|
1005
|
+
let failure;
|
|
2170
1006
|
try {
|
|
2171
|
-
await
|
|
1007
|
+
await deps.api.revokeApiKey(login.key, login.key_id);
|
|
2172
1008
|
} catch (error) {
|
|
1009
|
+
if (!(error instanceof HttpError && (error.status === 401 || error.status === 404)))
|
|
1010
|
+
failure = error instanceof Error ? error : new Error("The revocation failed.");
|
|
1011
|
+
}
|
|
1012
|
+
await deps.credentials.remove();
|
|
1013
|
+
if (failure)
|
|
2173
1014
|
throw new Error(
|
|
2174
|
-
`
|
|
2175
|
-
{ cause:
|
|
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 }
|
|
2176
1017
|
);
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
1018
|
+
deps.output("Logged out. The CLI key is revoked.");
|
|
1019
|
+
}
|
|
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."
|
|
1030
|
+
);
|
|
2183
1031
|
}
|
|
2184
|
-
async function
|
|
2185
|
-
const
|
|
2186
|
-
if (
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
bundle: source,
|
|
2192
|
-
origin,
|
|
2193
|
-
agentId: agent.id,
|
|
2194
|
-
promote: options.promote,
|
|
2195
|
-
spec: buildDeploymentSpec(declaration, source),
|
|
2196
|
-
token: managementToken
|
|
2197
|
-
});
|
|
2198
|
-
const { deployment } = result;
|
|
2199
|
-
const shortId = shortDeploymentId(deployment.id);
|
|
2200
|
-
const lines = [
|
|
2201
|
-
options.promote ? `Deployed agent ${agent.name}.` : `Created Deployment ${deploymentName(deployment)} (${shortId}) for agent ${agent.name} without promoting it.`,
|
|
2202
|
-
`Organization: ${agent.organizationName}`,
|
|
2203
|
-
`Deployment: ${deploymentName(deployment)} (${shortId})`,
|
|
2204
|
-
`Source: ${deploymentSourceLabel(deployment)}`,
|
|
2205
|
-
`Production: ${options.promote ? productionStateLabel(deployment) : `not promoted; run \`tonbo deployments promote ${shortId}\` to move Production here`}`,
|
|
2206
|
-
`Application: https://${agent.publicHostname}`
|
|
2207
|
-
];
|
|
2208
|
-
if (result.unchanged) {
|
|
2209
|
-
lines.push(
|
|
2210
|
-
`Contents are identical to the ${options.promote ? "previous" : "current"} Production Deployment; the running Agent does not change.`
|
|
2211
|
-
);
|
|
2212
|
-
}
|
|
2213
|
-
if (options.promote) {
|
|
2214
|
-
lines.push(
|
|
2215
|
-
"",
|
|
2216
|
-
"Agent process: starts with the first turn and stays warm while the runtime is active.",
|
|
2217
|
-
"",
|
|
2218
|
-
"Start the Agent:",
|
|
2219
|
-
' tonbo run "<prompt>"',
|
|
2220
|
-
"",
|
|
2221
|
-
"Connect with SSH:",
|
|
2222
|
-
` ssh ${agent.sshDestination}`
|
|
2223
|
-
);
|
|
2224
|
-
}
|
|
2225
|
-
deps.output({ message: lines.join("\n"), agent, ...result });
|
|
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
|
+
);
|
|
2226
1039
|
}
|
|
2227
|
-
async function
|
|
2228
|
-
|
|
2229
|
-
const
|
|
2230
|
-
const
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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()
|
|
1048
|
+
);
|
|
1049
|
+
print(
|
|
1050
|
+
deps,
|
|
1051
|
+
options.json,
|
|
2238
1052
|
agent,
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
agents
|
|
2247
|
-
});
|
|
2248
|
-
}
|
|
2249
|
-
async function agentShowCommand(deps) {
|
|
2250
|
-
const { agent, token } = await agentManagement(deps);
|
|
2251
|
-
const production = await deps.api.getProduction(agent.id, token);
|
|
2252
|
-
const deployment = production ? await deps.api.getDeployment(agent.id, production.deployment_id, token) : null;
|
|
2253
|
-
const lines = [
|
|
2254
|
-
`agent: ${agent.name}`,
|
|
2255
|
-
`Organization: ${agent.organizationName}`,
|
|
2256
|
-
`application: https://${agent.publicHostname}`,
|
|
2257
|
-
`ssh: ssh ${agent.sshDestination}`
|
|
2258
|
-
];
|
|
2259
|
-
if (deployment) {
|
|
2260
|
-
lines.push(
|
|
2261
|
-
`production: ${deploymentName(deployment)} (${shortDeploymentId(deployment.id)}) ${productionStateLabel(deployment)}`
|
|
2262
|
-
);
|
|
2263
|
-
}
|
|
2264
|
-
deps.output({ message: lines.join("\n"), agent, production, deployment });
|
|
2265
|
-
}
|
|
2266
|
-
function shortDeploymentId(id) {
|
|
2267
|
-
return id.replace(/-/g, "").slice(0, 8);
|
|
2268
|
-
}
|
|
2269
|
-
function deploymentName(deployment) {
|
|
2270
|
-
const subject = deployment.origin.git?.subject?.trim();
|
|
2271
|
-
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
|
+
);
|
|
2272
1060
|
}
|
|
2273
|
-
function
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
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
|
+
);
|
|
2278
1076
|
}
|
|
2279
|
-
function
|
|
2280
|
-
|
|
2281
|
-
return generation === null ? state : `${state} (generation ${generation})`;
|
|
1077
|
+
function memberName(member) {
|
|
1078
|
+
return member.name ?? member.display_name ?? "";
|
|
2282
1079
|
}
|
|
2283
|
-
function
|
|
2284
|
-
const
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
|
|
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."
|
|
2290
1099
|
);
|
|
2291
|
-
|
|
2292
|
-
|
|
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)
|
|
2293
1116
|
throw new Error(
|
|
2294
|
-
|
|
1117
|
+
`${member} names several members (${[...subjects].join(", ")}); give the member's subject.`
|
|
2295
1118
|
);
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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")
|
|
2302
1143
|
);
|
|
2303
|
-
const render = (row) => row.map((cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd();
|
|
2304
|
-
return [render(headers), ...rows.map(render)].join("\n");
|
|
2305
|
-
}
|
|
2306
|
-
function formatTimestamp(value) {
|
|
2307
|
-
const date = new Date(value);
|
|
2308
|
-
if (Number.isNaN(date.getTime())) return value;
|
|
2309
|
-
return `${date.toISOString().slice(0, 16)}Z`;
|
|
2310
|
-
}
|
|
2311
|
-
function productionMarker(state) {
|
|
2312
|
-
if (state === "current") return "\u25CF";
|
|
2313
|
-
if (state === "previous") return "\u25CB";
|
|
2314
|
-
return "";
|
|
2315
1144
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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]
|
|
2321
1162
|
]);
|
|
2322
|
-
const message = deployments.length ? renderTable(
|
|
2323
|
-
["ID", "NAME", "STATUS", "PRODUCTION", "SOURCE", "CREATED", "BY"],
|
|
2324
|
-
deployments.map((deployment) => [
|
|
2325
|
-
shortDeploymentId(deployment.id),
|
|
2326
|
-
deploymentName(deployment),
|
|
2327
|
-
deployment.production.state,
|
|
2328
|
-
productionMarker(deployment.production.state),
|
|
2329
|
-
deploymentSourceLabel(deployment),
|
|
2330
|
-
formatTimestamp(deployment.created_at),
|
|
2331
|
-
deployment.created_by_user_id ? shortDeploymentId(deployment.created_by_user_id) : ""
|
|
2332
|
-
])
|
|
2333
|
-
) : `No Deployments exist for agent ${agent.name}. Run \`tonbo deploy\` to create one.`;
|
|
2334
|
-
deps.output({ message, agent, production, deployments });
|
|
2335
1163
|
}
|
|
2336
|
-
async function
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
(rollout) => rollout.deployment_id === deployment.id || rollout.previous_deployment_id === deployment.id
|
|
2341
|
-
);
|
|
2342
|
-
deps.output({
|
|
2343
|
-
message: renderDeploymentDetail(deployment, rollouts),
|
|
2344
|
-
agent,
|
|
2345
|
-
deployment,
|
|
2346
|
-
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
|
|
2347
1168
|
});
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
rows.push(
|
|
2366
|
-
["Created", formatTimestamp(deployment.created_at)],
|
|
2367
|
-
["Created by", deployment.created_by_user_id ?? ""],
|
|
2368
|
-
["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."
|
|
2369
1186
|
);
|
|
2370
|
-
if (typeof inference?.model === "string") rows.push(["Model", inference.model]);
|
|
2371
|
-
if (bundleLabel) rows.push(["Bundle", bundleLabel]);
|
|
2372
|
-
const label = Math.max(...rows.map(([name]) => name.length)) + 1;
|
|
2373
|
-
const lines = rows.map(([name, value]) => `${`${name}:`.padEnd(label)} ${value}`.trimEnd());
|
|
2374
|
-
lines.push("", "Rollouts:");
|
|
2375
|
-
if (rollouts.length === 0) lines.push(" none");
|
|
2376
|
-
for (const rollout of rollouts) {
|
|
2377
|
-
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)}`;
|
|
2378
|
-
lines.push(
|
|
2379
|
-
` generation ${rollout.generation} ${formatTimestamp(rollout.created_at)} ${direction}${rollout.created_by_user_id ? ` by ${shortDeploymentId(rollout.created_by_user_id)}` : ""}`
|
|
2380
|
-
);
|
|
2381
|
-
}
|
|
2382
|
-
return lines.join("\n");
|
|
2383
1187
|
}
|
|
2384
|
-
async function
|
|
2385
|
-
const
|
|
2386
|
-
const
|
|
2387
|
-
const
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
}
|
|
2394
|
-
async function deploymentsRollbackCommand(deps, prefix) {
|
|
2395
|
-
const { agent, token } = await agentManagement(deps);
|
|
2396
|
-
let target;
|
|
2397
|
-
if (prefix !== void 0) {
|
|
2398
|
-
target = selectDeployment(await deps.api.listDeployments(agent.id, token), prefix);
|
|
2399
|
-
} else {
|
|
2400
|
-
const rollouts = await deps.api.listRollouts(agent.id, token);
|
|
2401
|
-
const previousId = rollouts.find(
|
|
2402
|
-
(rollout) => rollout.previous_deployment_id !== null
|
|
2403
|
-
)?.previous_deployment_id;
|
|
2404
|
-
if (!previousId) {
|
|
2405
|
-
throw new Error(
|
|
2406
|
-
`Agent ${agent.name} has no previous Production Deployment to roll back to. Name one with \`tonbo deployments rollback <id>\`.`
|
|
2407
|
-
);
|
|
2408
|
-
}
|
|
2409
|
-
target = await deps.api.getDeployment(agent.id, previousId, token);
|
|
2410
|
-
}
|
|
2411
|
-
const result = await moveProduction(deps, agent.id, target, token);
|
|
2412
|
-
deps.output({
|
|
2413
|
-
message: describeProductionMove(agent, result, "Rolled back"),
|
|
2414
|
-
agent,
|
|
2415
|
-
...result
|
|
1188
|
+
async function apiKeyCreateCommand(deps, name, options = {}) {
|
|
1189
|
+
const key = await deps.auth.apiKey();
|
|
1190
|
+
const member = options.member === void 0 ? void 0 : await resolveMemberSubject(deps, key, options.member);
|
|
1191
|
+
const created = await deps.api.createApiKey(key, {
|
|
1192
|
+
name,
|
|
1193
|
+
member,
|
|
1194
|
+
read_only: options.readOnly,
|
|
1195
|
+
buckets: options.bucket,
|
|
1196
|
+
expires_in: options.expiresIn
|
|
2416
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
|
+
);
|
|
2417
1213
|
}
|
|
2418
|
-
async function
|
|
2419
|
-
const
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
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));
|
|
1217
|
+
}
|
|
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:`;
|
|
2426
1227
|
},
|
|
2427
|
-
|
|
1228
|
+
"yes"
|
|
2428
1229
|
);
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
}
|
|
2435
|
-
function
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
const { oauthToken, agent } = await resolveAgent(deps);
|
|
2445
|
-
return {
|
|
2446
|
-
agent,
|
|
2447
|
-
token: await deps.api.exchangeManagementToken(oauthToken, agent.id)
|
|
2448
|
-
};
|
|
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
|
+
]);
|
|
2449
1245
|
}
|
|
2450
|
-
async function
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
message: secrets.length ? secrets.map((secret) => secret.name).join("\n") : "No agent secrets are configured.",
|
|
2455
|
-
agent,
|
|
2456
|
-
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
|
|
2457
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
|
+
);
|
|
2458
1266
|
}
|
|
2459
|
-
async function
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
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
|
+
);
|
|
2468
1279
|
}
|
|
2469
|
-
async function
|
|
2470
|
-
const
|
|
2471
|
-
|
|
2472
|
-
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));
|
|
2473
1283
|
}
|
|
2474
|
-
async function
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
const
|
|
2478
|
-
|
|
2479
|
-
runtime_session_id: options.runtimeSession,
|
|
2480
|
-
work_keys: options.workKey,
|
|
2481
|
-
reason: options.reason
|
|
2482
|
-
});
|
|
2483
|
-
deps.output({
|
|
2484
|
-
message: "Activity cancellation requested. Physical retirement and application checkpoint cancellation must still be verified.",
|
|
2485
|
-
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
|
|
2486
1289
|
});
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
import os from "node:os";
|
|
2493
|
-
import path5 from "node:path";
|
|
2494
|
-
function defaultConfigDirectory() {
|
|
2495
|
-
const base = process.env.XDG_CONFIG_HOME || path5.join(os.homedir(), ".config");
|
|
2496
|
-
return path5.join(base, "tonbo");
|
|
2497
|
-
}
|
|
2498
|
-
var FileCredentialStore = class {
|
|
2499
|
-
constructor(filename = path5.join(defaultConfigDirectory(), "credentials.json")) {
|
|
2500
|
-
this.filename = filename;
|
|
2501
|
-
}
|
|
2502
|
-
async load() {
|
|
2503
|
-
try {
|
|
2504
|
-
await assertPrivateRegularFile(this.filename);
|
|
2505
|
-
const parsed = JSON.parse(await readFile5(this.filename, "utf8"));
|
|
2506
|
-
if (!isOAuthTokenSet(parsed)) throw new Error("invalid token set");
|
|
2507
|
-
return parsed;
|
|
2508
|
-
} catch (error) {
|
|
2509
|
-
if (error.code === "ENOENT") return null;
|
|
2510
|
-
throw new Error(`Could not read Tonbo credentials at ${this.filename}.`, { cause: error });
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
async save(tokens) {
|
|
2514
|
-
if (!isOAuthTokenSet(tokens)) throw new Error("Refusing to store a malformed Tonbo token set.");
|
|
2515
|
-
const directory = path5.dirname(this.filename);
|
|
2516
|
-
await mkdir(directory, { mode: 448, recursive: true });
|
|
2517
|
-
await preparePrivateDirectory(directory);
|
|
2518
|
-
await assertExistingDestinationIsSafe(this.filename);
|
|
2519
|
-
const temporary = path5.join(
|
|
2520
|
-
directory,
|
|
2521
|
-
`.${path5.basename(this.filename)}.${process.pid}.${randomUUID4()}.tmp`
|
|
2522
|
-
);
|
|
2523
|
-
let handle = null;
|
|
2524
|
-
try {
|
|
2525
|
-
handle = await open2(temporary, "wx", 384);
|
|
2526
|
-
await handle.writeFile(`${JSON.stringify(tokens, null, 2)}
|
|
2527
|
-
`, "utf8");
|
|
2528
|
-
await handle.sync();
|
|
2529
|
-
await handle.close();
|
|
2530
|
-
handle = null;
|
|
2531
|
-
if (process.platform !== "win32") await chmod(temporary, 384);
|
|
2532
|
-
await rename2(temporary, this.filename);
|
|
2533
|
-
} catch (error) {
|
|
2534
|
-
await handle?.close().catch(() => void 0);
|
|
2535
|
-
await rm2(temporary, { force: true }).catch(() => void 0);
|
|
2536
|
-
throw new Error(`Could not store Tonbo credentials at ${this.filename}.`, { cause: error });
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
};
|
|
2540
|
-
function isOAuthTokenSet(value) {
|
|
2541
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2542
|
-
const token = value;
|
|
2543
|
-
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);
|
|
2544
|
-
}
|
|
2545
|
-
async function preparePrivateDirectory(directory) {
|
|
2546
|
-
const stat = await lstat4(directory);
|
|
2547
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
2548
|
-
throw new Error("Tonbo config directory must be a real directory.");
|
|
2549
|
-
if (process.platform !== "win32") await chmod(directory, 448);
|
|
2550
|
-
}
|
|
2551
|
-
async function assertExistingDestinationIsSafe(filename) {
|
|
2552
|
-
try {
|
|
2553
|
-
const stat = await lstat4(filename);
|
|
2554
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
2555
|
-
throw new Error("Tonbo credential path must be a regular file.");
|
|
2556
|
-
} catch (error) {
|
|
2557
|
-
if (error.code !== "ENOENT") throw error;
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
async function assertPrivateRegularFile(filename) {
|
|
2561
|
-
const stat = await lstat4(filename);
|
|
2562
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
2563
|
-
throw new Error("Tonbo credential path must be a regular file.");
|
|
2564
|
-
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
2565
|
-
throw new Error("Tonbo credential file must have mode 0600.");
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
// src/git.ts
|
|
2569
|
-
import { execFile as execFile2 } from "node:child_process";
|
|
2570
|
-
var defaultGitRunner = (args, cwd) => new Promise((resolve, reject) => {
|
|
2571
|
-
execFile2(
|
|
2572
|
-
"git",
|
|
2573
|
-
args,
|
|
2574
|
-
{
|
|
2575
|
-
cwd,
|
|
2576
|
-
encoding: "utf8",
|
|
2577
|
-
// Never take the index lock for a read: a concurrent editor or IDE
|
|
2578
|
-
// must not turn provenance collection into a failure.
|
|
2579
|
-
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
2580
|
-
windowsHide: true
|
|
2581
|
-
},
|
|
2582
|
-
(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.`
|
|
2583
1295
|
);
|
|
2584
|
-
});
|
|
2585
|
-
var COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
2586
|
-
var REF_MAX_LENGTH = 255;
|
|
2587
|
-
var SUBJECT_MAX_LENGTH = 512;
|
|
2588
|
-
var AUTHOR_MAX_LENGTH = 255;
|
|
2589
|
-
async function collectGitProvenance(root, run = defaultGitRunner) {
|
|
2590
|
-
const commitSha = await read(run, ["rev-parse", "HEAD"], root);
|
|
2591
|
-
if (!commitSha || !COMMIT_SHA.test(commitSha)) return null;
|
|
2592
|
-
const [ref, subject, author, status, remote] = await Promise.all([
|
|
2593
|
-
read(run, ["rev-parse", "--abbrev-ref", "HEAD"], root),
|
|
2594
|
-
read(run, ["log", "-1", "--format=%s"], root),
|
|
2595
|
-
read(run, ["log", "-1", "--format=%an"], root),
|
|
2596
|
-
// Only the deployed tree matters: changes elsewhere in the repository do
|
|
2597
|
-
// not alter the uploaded bundle.
|
|
2598
|
-
read(run, ["status", "--porcelain", "--", "."], root),
|
|
2599
|
-
read(run, ["remote", "get-url", "origin"], root)
|
|
2600
|
-
]);
|
|
2601
|
-
return {
|
|
2602
|
-
...githubRepositoryUrl(remote) ? { repository_url: githubRepositoryUrl(remote) } : {},
|
|
2603
|
-
commit_sha: commitSha,
|
|
2604
|
-
ref: ref === "HEAD" ? null : clamp(ref, REF_MAX_LENGTH),
|
|
2605
|
-
subject: clamp(subject, SUBJECT_MAX_LENGTH),
|
|
2606
|
-
author: clamp(author, AUTHOR_MAX_LENGTH),
|
|
2607
|
-
// When the status cannot be read the tree cannot be shown to match the
|
|
2608
|
-
// commit, so the Deployment is recorded as dirty rather than clean.
|
|
2609
|
-
dirty: status === null || status.length > 0
|
|
2610
|
-
};
|
|
2611
1296
|
}
|
|
2612
|
-
async function
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
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
|
+
);
|
|
2618
1314
|
}
|
|
2619
|
-
function
|
|
2620
|
-
|
|
2621
|
-
|
|
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
|
+
);
|
|
2622
1337
|
}
|
|
2623
1338
|
|
|
2624
1339
|
// src/progress.ts
|
|
@@ -2674,26 +1389,10 @@ var TerminalProgress = class {
|
|
|
2674
1389
|
this.lastWidth = value.length;
|
|
2675
1390
|
}
|
|
2676
1391
|
};
|
|
2677
|
-
var silentProgress = {
|
|
2678
|
-
start: () => {
|
|
2679
|
-
},
|
|
2680
|
-
succeed: () => {
|
|
2681
|
-
},
|
|
2682
|
-
fail: () => {
|
|
2683
|
-
}
|
|
2684
|
-
};
|
|
2685
1392
|
|
|
2686
1393
|
// src/prompt.ts
|
|
2687
1394
|
import select from "@inquirer/select";
|
|
2688
1395
|
import { createInterface } from "node:readline/promises";
|
|
2689
|
-
async function terminalPrompt(question) {
|
|
2690
|
-
const prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
2691
|
-
try {
|
|
2692
|
-
return await prompt.question(`? ${question}`);
|
|
2693
|
-
} finally {
|
|
2694
|
-
prompt.close();
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
1396
|
async function terminalSelect(question, choices) {
|
|
2698
1397
|
return select(
|
|
2699
1398
|
{
|
|
@@ -2707,178 +1406,172 @@ async function terminalSelect(question, choices) {
|
|
|
2707
1406
|
}
|
|
2708
1407
|
);
|
|
2709
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
|
+
}
|
|
2710
1417
|
|
|
2711
1418
|
// src/app.ts
|
|
2712
1419
|
var packageVersion = JSON.parse(
|
|
2713
1420
|
readFileSync(new URL("../../package.json", import.meta.url), "utf8")
|
|
2714
1421
|
);
|
|
2715
|
-
function
|
|
2716
|
-
const
|
|
2717
|
-
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();
|
|
2718
1437
|
return {
|
|
2719
|
-
api: new
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
else console.log(value.message ?? value);
|
|
2729
|
-
},
|
|
2730
|
-
progress: json ? silentProgress : new TerminalProgress(process.stderr),
|
|
2731
|
-
prompt: terminalPrompt,
|
|
2732
|
-
select: terminalSelect,
|
|
2733
|
-
secretValue: (name, fromEnvironment) => {
|
|
2734
|
-
const environmentName = fromEnvironment ?? name;
|
|
2735
|
-
const value = process.env[environmentName];
|
|
2736
|
-
if (!value)
|
|
2737
|
-
throw new Error(
|
|
2738
|
-
`Environment variable ${environmentName} is empty. Set it before running tonbo secret set.`
|
|
2739
|
-
);
|
|
2740
|
-
return Promise.resolve(value);
|
|
2741
|
-
}
|
|
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
|
|
2742
1447
|
};
|
|
2743
1448
|
}
|
|
2744
|
-
function
|
|
2745
|
-
|
|
1449
|
+
function parseLimit(value) {
|
|
1450
|
+
if (!/^[1-9][0-9]*$/.test(value)) throw new InvalidArgumentError("Not a positive integer.");
|
|
1451
|
+
return Number(value);
|
|
2746
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");
|
|
1460
|
+
}
|
|
1461
|
+
var roleOption = (description) => new Option("--role <role>", description).choices(accountRoles);
|
|
2747
1462
|
function createProgram(dependencies = createDependencies) {
|
|
2748
|
-
const program = new Command().name("tonbo").description("
|
|
2749
|
-
program.command("
|
|
2750
|
-
|
|
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)
|
|
2751
1481
|
);
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
const deps = dependencies(jsonOutput(program));
|
|
2760
|
-
deps.output(
|
|
2761
|
-
await deps.api.machineRequest(
|
|
2762
|
-
await deps.auth.accessToken(),
|
|
2763
|
-
options.account ? `?account=${encodeURIComponent(options.account)}` : ""
|
|
2764
|
-
)
|
|
2765
|
-
);
|
|
2766
|
-
});
|
|
2767
|
-
machine.command("allocate").requiredOption("--account <account>", "owning account UUID").requiredOption("--region <region>", "compute region (see tonbo region list)").option(
|
|
2768
|
-
"--request-id <uuid>",
|
|
2769
|
-
"stable allocation request UUID; reuse it after an uncertain response"
|
|
2770
|
-
).action(async (options) => {
|
|
2771
|
-
const deps = dependencies(jsonOutput(program));
|
|
2772
|
-
const requestId = options.requestId ?? randomUUID5();
|
|
2773
|
-
try {
|
|
2774
|
-
const result = await deps.api.machineRequest(await deps.auth.accessToken(), "", "POST", {
|
|
2775
|
-
accountId: options.account,
|
|
2776
|
-
region: options.region,
|
|
2777
|
-
requestId
|
|
2778
|
-
});
|
|
2779
|
-
deps.output({ ...result, requestId });
|
|
2780
|
-
} catch (cause) {
|
|
2781
|
-
throw new Error(
|
|
2782
|
-
`Machine allocation was not confirmed. Retry with --request-id ${requestId} and the same account and region.`,
|
|
2783
|
-
{ cause }
|
|
2784
|
-
);
|
|
2785
|
-
}
|
|
2786
|
-
});
|
|
2787
|
-
machine.command("allocation <requestId>").description("query the original Machine for an allocation request").requiredOption("--account <account>", "owning account UUID").action(async (requestId, options) => {
|
|
2788
|
-
const deps = dependencies(jsonOutput(program));
|
|
2789
|
-
deps.output(
|
|
2790
|
-
await deps.api.machineRequest(
|
|
2791
|
-
await deps.auth.accessToken(),
|
|
2792
|
-
`/allocations/${encodeURIComponent(requestId)}?account=${encodeURIComponent(options.account)}`
|
|
2793
|
-
)
|
|
2794
|
-
);
|
|
2795
|
-
});
|
|
2796
|
-
for (const operation of ["show", "metrics", "release"]) {
|
|
2797
|
-
machine.command(`${operation} <machine>`).action(async (id) => {
|
|
2798
|
-
const deps = dependencies(jsonOutput(program));
|
|
2799
|
-
const result = await deps.api.machineRequest(
|
|
2800
|
-
await deps.auth.accessToken(),
|
|
2801
|
-
`/${encodeURIComponent(id)}`,
|
|
2802
|
-
operation === "release" ? "DELETE" : "GET"
|
|
2803
|
-
);
|
|
2804
|
-
deps.output(operation === "metrics" ? result.metrics : result);
|
|
2805
|
-
});
|
|
2806
|
-
}
|
|
2807
|
-
machine.command("bind <machine>").requiredOption("--agent <agent>", "Agent UUID").action(async (id, options) => {
|
|
2808
|
-
const deps = dependencies(jsonOutput(program));
|
|
2809
|
-
deps.output(
|
|
2810
|
-
await deps.api.machineRequest(
|
|
2811
|
-
await deps.auth.accessToken(),
|
|
2812
|
-
`/${encodeURIComponent(id)}/binding`,
|
|
2813
|
-
"POST",
|
|
2814
|
-
{ agentId: options.agent }
|
|
2815
|
-
)
|
|
2816
|
-
);
|
|
2817
|
-
});
|
|
2818
|
-
machine.command("unbind <machine>").description("detach Agent and reset system disk; retain Agent Workspace and Sessions").action(async (id) => {
|
|
2819
|
-
const deps = dependencies(jsonOutput(program));
|
|
2820
|
-
const token = await deps.auth.accessToken();
|
|
2821
|
-
const path6 = `/${encodeURIComponent(id)}`;
|
|
2822
|
-
const current = await deps.api.machineRequest(token, path6);
|
|
2823
|
-
if (!current.binding) throw new Error("Machine is not bound to an Agent.");
|
|
2824
|
-
deps.output(
|
|
2825
|
-
await deps.api.machineRequest(token, `${path6}/binding`, "DELETE", {
|
|
2826
|
-
agentId: current.binding.agent_id,
|
|
2827
|
-
bindingId: current.binding.id,
|
|
2828
|
-
generation: current.binding.generation
|
|
2829
|
-
})
|
|
2830
|
-
);
|
|
2831
|
-
});
|
|
2832
|
-
const agent = program.command("agent").description("manage the Agent bound to this source directory");
|
|
2833
|
-
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("--request-id <uuid>", "reuse the same creation UUID after an uncertain response").action(
|
|
2834
|
-
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)
|
|
2835
1489
|
);
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
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)
|
|
1501
|
+
);
|
|
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)
|
|
1508
|
+
);
|
|
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
|
|
2843
1520
|
)
|
|
2844
|
-
)
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
sshKey.command("add <public-key>").description("register an OpenSSH public key with the current Tonbo account").action(async (path6) => sshKeyAddCommand(dependencies(jsonOutput(program)), path6));
|
|
2849
|
-
sshKey.command("remove <fingerprint>").description("revoke an SSH public key from the current Tonbo account").action(
|
|
2850
|
-
async (fingerprint) => sshKeyRemoveCommand(dependencies(jsonOutput(program)), fingerprint)
|
|
1521
|
+
),
|
|
1522
|
+
"createApiKey"
|
|
1523
|
+
).action(
|
|
1524
|
+
async (name, options) => apiKeyCreateCommand(dependencies(), name, options)
|
|
2851
1525
|
);
|
|
2852
|
-
|
|
2853
|
-
async (
|
|
1526
|
+
operationOptions(apiKey.command("show <key-id>").description("show one key"), "getApiKey").action(
|
|
1527
|
+
async (keyId, options) => apiKeyShowCommand(dependencies(), keyId, options)
|
|
2854
1528
|
);
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
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)
|
|
2858
1534
|
);
|
|
2859
|
-
const
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
async (prefix) => deploymentsShowCommand(dependencies(jsonOutput(program)), prefix)
|
|
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)
|
|
2863
1538
|
);
|
|
2864
|
-
|
|
2865
|
-
|
|
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)
|
|
2866
1550
|
);
|
|
2867
|
-
|
|
2868
|
-
async (options) =>
|
|
1551
|
+
operationOptions(bucket.command("show <name>").description("show a bucket"), "getBucket").action(
|
|
1552
|
+
async (name, options) => bucketShowCommand(dependencies(), name, options)
|
|
2869
1553
|
);
|
|
2870
|
-
|
|
2871
|
-
|
|
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"
|
|
1557
|
+
).action(
|
|
1558
|
+
async (name, options) => bucketUpdateCommand(dependencies(), name, options)
|
|
2872
1559
|
);
|
|
2873
|
-
|
|
2874
|
-
|
|
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)
|
|
2875
1565
|
);
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
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)
|
|
2880
1574
|
);
|
|
2881
|
-
secret.command("remove <name>").action(async (name) => secretRemoveCommand(dependencies(jsonOutput(program)), name));
|
|
2882
1575
|
return program;
|
|
2883
1576
|
}
|
|
2884
1577
|
|