@pstdio/pocketcoder-sdk 0.3.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/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/index.d.ts +1417 -0
- package/dist/index.js +2335 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2335 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
//#region ../contracts/src/pagination.ts
|
|
4
|
+
const CursorSchema = z.string().min(1).max(2048);
|
|
5
|
+
function CursorPageSchema(item) {
|
|
6
|
+
return z.object({
|
|
7
|
+
items: z.array(item),
|
|
8
|
+
next_cursor: CursorSchema.nullable()
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function CursorQuerySchema(maxLimit, defaultLimit) {
|
|
12
|
+
return z.object({
|
|
13
|
+
cursor: CursorSchema.optional(),
|
|
14
|
+
limit: z.coerce.number().int().positive().max(maxLimit).default(defaultLimit)
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region ../contracts/src/conversation.ts
|
|
19
|
+
const CONVERSATION_ROLES = [
|
|
20
|
+
"user",
|
|
21
|
+
"assistant",
|
|
22
|
+
"system",
|
|
23
|
+
"tool"
|
|
24
|
+
];
|
|
25
|
+
const ConversationMetadataSchema = z.record(z.string().min(1).max(64), z.string().max(512)).refine((value) => Object.keys(value).length <= 32, "conversation metadata has at most 32 keys");
|
|
26
|
+
const ConversationMessageInputSchema = z.object({
|
|
27
|
+
message_id: z.string().min(1).max(256),
|
|
28
|
+
role: z.enum(CONVERSATION_ROLES),
|
|
29
|
+
content: z.string().max(262144),
|
|
30
|
+
occurred_at: z.iso.datetime(),
|
|
31
|
+
metadata: ConversationMetadataSchema.default({})
|
|
32
|
+
});
|
|
33
|
+
const ConversationMessageResourceSchema = ConversationMessageInputSchema.extend({ seq: z.number().int().positive() });
|
|
34
|
+
CursorQuerySchema(200, 100);
|
|
35
|
+
const ConversationPageSchema = CursorPageSchema(ConversationMessageResourceSchema).extend({ retention: z.object({
|
|
36
|
+
status: z.literal("retained"),
|
|
37
|
+
expires_at: z.iso.datetime().nullable()
|
|
38
|
+
}) });
|
|
39
|
+
const ConversationResumeOutcomeSchema = z.object({
|
|
40
|
+
status: z.literal("supported"),
|
|
41
|
+
reason: z.null(),
|
|
42
|
+
source_workspace_id: z.uuid(),
|
|
43
|
+
checkpoint_id: z.uuid()
|
|
44
|
+
});
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region ../contracts/src/duration.ts
|
|
47
|
+
const DURATION_RE = /^(\d+)(ms|s|m|h)$/;
|
|
48
|
+
function isDuration(value) {
|
|
49
|
+
return DURATION_RE.test(value);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region ../contracts/src/persistence.ts
|
|
53
|
+
const DurationValueSchema = z.string().refine(isDuration, { message: "expected a duration like 15s, 20m, or 2h" });
|
|
54
|
+
const LAUNCH_MODES = ["create", "restore"];
|
|
55
|
+
const CONVERSATION_RESTORE_CAPABILITIES = [
|
|
56
|
+
"supported",
|
|
57
|
+
"filesystem_only",
|
|
58
|
+
"unknown"
|
|
59
|
+
];
|
|
60
|
+
const SourceDescriptorSchema = z.object({
|
|
61
|
+
kind: z.literal("git"),
|
|
62
|
+
repository: z.string().min(1).max(64).regex(/^[a-z0-9]([a-z0-9._-]{0,62}[a-z0-9])?$/),
|
|
63
|
+
revision: z.string().min(1).max(256).refine((value) => !value.startsWith("-") && !value.startsWith("/") && !value.endsWith("/") && !value.endsWith(".") && !value.endsWith(".lock") && value !== "@" && !value.includes("..") && !value.includes("//") && !value.includes("@{") && !/[~^:?*[\]\\\s]/u.test(value) && [...value].every((character) => {
|
|
64
|
+
const code = character.codePointAt(0) ?? 0;
|
|
65
|
+
return code >= 32 && code !== 127;
|
|
66
|
+
}), { message: "revision is not a safe branch, tag, or commit name" })
|
|
67
|
+
});
|
|
68
|
+
const ResolvedSourceSchema = z.object({
|
|
69
|
+
kind: z.literal("git"),
|
|
70
|
+
repository: z.string().min(1).max(64),
|
|
71
|
+
requested_revision: z.string().min(1).max(256),
|
|
72
|
+
resolved_commit: z.string().regex(/^[0-9a-f]{40,64}$/)
|
|
73
|
+
});
|
|
74
|
+
const PersistenceMountSchema = z.object({
|
|
75
|
+
name: z.string().min(1).max(64).regex(/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/),
|
|
76
|
+
target: z.string().min(1),
|
|
77
|
+
maxBytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
78
|
+
maxFiles: z.number().int().positive().max(1e7)
|
|
79
|
+
});
|
|
80
|
+
const CheckpointPolicySchema = z.object({
|
|
81
|
+
onIdle: z.enum(["preserve", "destroy"]).default("destroy"),
|
|
82
|
+
onDeadline: z.enum(["preserve", "destroy"]).default("destroy"),
|
|
83
|
+
onCleanExit: z.enum(["preserve", "destroy"]).default("destroy"),
|
|
84
|
+
onFailure: z.enum([
|
|
85
|
+
"preserve",
|
|
86
|
+
"retain-for-recovery",
|
|
87
|
+
"destroy"
|
|
88
|
+
]).default("destroy"),
|
|
89
|
+
retention: DurationValueSchema.default("168h")
|
|
90
|
+
});
|
|
91
|
+
const PersistenceSpecSchema = z.object({
|
|
92
|
+
mounts: z.array(PersistenceMountSchema).max(16).default([]),
|
|
93
|
+
conversationRestore: z.enum(CONVERSATION_RESTORE_CAPABILITIES).default("filesystem_only"),
|
|
94
|
+
conversationRetention: DurationValueSchema.default("168h"),
|
|
95
|
+
sessionCompatibility: z.string().min(1).max(128).optional(),
|
|
96
|
+
checkpoint: CheckpointPolicySchema.prefault({})
|
|
97
|
+
});
|
|
98
|
+
const OutputDeclarationSchema = z.discriminatedUnion("type", [
|
|
99
|
+
z.object({ type: z.literal("gitSha") }),
|
|
100
|
+
z.object({
|
|
101
|
+
type: z.literal("string"),
|
|
102
|
+
maxLength: z.number().int().positive().max(4096).default(512)
|
|
103
|
+
}),
|
|
104
|
+
z.object({
|
|
105
|
+
type: z.literal("httpsUrl"),
|
|
106
|
+
maxLength: z.number().int().positive().max(4096).default(2048)
|
|
107
|
+
})
|
|
108
|
+
]);
|
|
109
|
+
const CHECKPOINT_STATES = [
|
|
110
|
+
"creating",
|
|
111
|
+
"ready",
|
|
112
|
+
"failed",
|
|
113
|
+
"deleting",
|
|
114
|
+
"deleted"
|
|
115
|
+
];
|
|
116
|
+
const OPERATION_KINDS = [
|
|
117
|
+
"preserve",
|
|
118
|
+
"restore",
|
|
119
|
+
"verify",
|
|
120
|
+
"delete"
|
|
121
|
+
];
|
|
122
|
+
const OPERATION_STATES = [
|
|
123
|
+
"pending",
|
|
124
|
+
"running",
|
|
125
|
+
"succeeded",
|
|
126
|
+
"failed"
|
|
127
|
+
];
|
|
128
|
+
const CheckpointResourceSchema = z.object({
|
|
129
|
+
id: z.uuid(),
|
|
130
|
+
workspace_id: z.uuid(),
|
|
131
|
+
state: z.enum(CHECKPOINT_STATES),
|
|
132
|
+
reason_code: z.string().nullable(),
|
|
133
|
+
template: z.object({
|
|
134
|
+
name: z.string(),
|
|
135
|
+
version: z.string(),
|
|
136
|
+
digest: z.string()
|
|
137
|
+
}),
|
|
138
|
+
manifest_digest: z.string().nullable(),
|
|
139
|
+
logical_bytes: z.number().int().nonnegative().nullable(),
|
|
140
|
+
stored_bytes: z.number().int().nonnegative().nullable(),
|
|
141
|
+
file_count: z.number().int().nonnegative().nullable(),
|
|
142
|
+
mounts: z.array(z.string()),
|
|
143
|
+
conversation_restore: z.enum(CONVERSATION_RESTORE_CAPABILITIES),
|
|
144
|
+
label: z.string().nullable(),
|
|
145
|
+
created_at: z.iso.datetime(),
|
|
146
|
+
ready_at: z.iso.datetime().nullable(),
|
|
147
|
+
expires_at: z.iso.datetime().nullable()
|
|
148
|
+
});
|
|
149
|
+
const OperationResourceSchema = z.object({
|
|
150
|
+
id: z.uuid(),
|
|
151
|
+
kind: z.enum(OPERATION_KINDS),
|
|
152
|
+
state: z.enum(OPERATION_STATES),
|
|
153
|
+
workspace_id: z.uuid().nullable(),
|
|
154
|
+
checkpoint_id: z.uuid().nullable(),
|
|
155
|
+
result_workspace_id: z.uuid().nullable(),
|
|
156
|
+
reason_code: z.string().nullable(),
|
|
157
|
+
created_at: z.iso.datetime(),
|
|
158
|
+
updated_at: z.iso.datetime(),
|
|
159
|
+
completed_at: z.iso.datetime().nullable()
|
|
160
|
+
});
|
|
161
|
+
z.object({
|
|
162
|
+
retention: DurationValueSchema.optional(),
|
|
163
|
+
label: z.string().min(1).max(128).optional()
|
|
164
|
+
});
|
|
165
|
+
z.object({
|
|
166
|
+
external_id: z.string().min(1).max(256),
|
|
167
|
+
metadata: z.record(z.string().max(64), z.string().max(512)).optional(),
|
|
168
|
+
launch_input: z.record(z.string(), z.unknown()).optional()
|
|
169
|
+
});
|
|
170
|
+
const CheckpointManifestEntrySchema = z.object({
|
|
171
|
+
path: z.string(),
|
|
172
|
+
kind: z.enum([
|
|
173
|
+
"directory",
|
|
174
|
+
"file",
|
|
175
|
+
"symlink"
|
|
176
|
+
]),
|
|
177
|
+
mode: z.number().int().nonnegative(),
|
|
178
|
+
uid: z.number().int().nonnegative(),
|
|
179
|
+
gid: z.number().int().nonnegative(),
|
|
180
|
+
mtime_ns: z.string().regex(/^\d+$/),
|
|
181
|
+
size: z.number().int().nonnegative(),
|
|
182
|
+
digest: z.string().optional(),
|
|
183
|
+
link_target: z.string().optional()
|
|
184
|
+
});
|
|
185
|
+
z.object({
|
|
186
|
+
format: z.literal("pocketcoder-checkpoint/v1"),
|
|
187
|
+
checkpoint_id: z.uuid(),
|
|
188
|
+
template_digest: z.string(),
|
|
189
|
+
mounts: z.array(z.object({
|
|
190
|
+
name: z.string(),
|
|
191
|
+
entries: z.array(CheckpointManifestEntrySchema)
|
|
192
|
+
})),
|
|
193
|
+
logical_bytes: z.number().int().nonnegative(),
|
|
194
|
+
file_count: z.number().int().nonnegative()
|
|
195
|
+
});
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region ../contracts/src/network.ts
|
|
198
|
+
const DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
199
|
+
function isDomain(value) {
|
|
200
|
+
const domain = value.startsWith("*.") ? value.slice(2) : value;
|
|
201
|
+
return domain.length <= 253 && !domain.endsWith(".") && isIP(domain) === 0 && domain.split(".").length >= 2 && domain.split(".").every((label) => DOMAIN_LABEL.test(label));
|
|
202
|
+
}
|
|
203
|
+
const NetworkRuleSchema = z.object({
|
|
204
|
+
domain: z.string().refine(isDomain, "expected a lowercase ASCII domain or leading *. wildcard"),
|
|
205
|
+
ports: z.array(z.number().int().min(1).max(65535)).min(1).max(32).default([80, 443]),
|
|
206
|
+
allowPrivate: z.boolean().default(false)
|
|
207
|
+
}).superRefine((rule, ctx) => {
|
|
208
|
+
if (new Set(rule.ports).size !== rule.ports.length) ctx.addIssue({
|
|
209
|
+
code: "custom",
|
|
210
|
+
path: ["ports"],
|
|
211
|
+
message: "ports must be unique"
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
const UnrestrictedNetworkPolicySchema = z.object({ mode: z.literal("unrestricted") });
|
|
215
|
+
const RestrictedNetworkPolicySchema = z.object({
|
|
216
|
+
mode: z.literal("restricted"),
|
|
217
|
+
allow: z.array(NetworkRuleSchema).max(256).default([])
|
|
218
|
+
});
|
|
219
|
+
const NetworkPolicySchema = z.preprocess((value) => value ?? { mode: "unrestricted" }, z.discriminatedUnion("mode", [UnrestrictedNetworkPolicySchema, RestrictedNetworkPolicySchema]));
|
|
220
|
+
const NETWORK_STATES = [
|
|
221
|
+
"disabled",
|
|
222
|
+
"starting",
|
|
223
|
+
"ready",
|
|
224
|
+
"degraded"
|
|
225
|
+
];
|
|
226
|
+
const NetworkEventInputSchema = z.object({
|
|
227
|
+
source_seq: z.number().int().positive(),
|
|
228
|
+
occurred_at: z.iso.datetime(),
|
|
229
|
+
decision: z.enum(["allow", "deny"]),
|
|
230
|
+
transport: z.enum(["http", "https"]),
|
|
231
|
+
host: z.string().min(1).max(253),
|
|
232
|
+
port: z.number().int().min(1).max(65535),
|
|
233
|
+
method: z.string().max(32).nullable().default(null),
|
|
234
|
+
path: z.string().max(2048).refine((path) => !path.includes("?") && !path.includes("#"), "path cannot contain query or fragment").nullable().default(null),
|
|
235
|
+
matched_rule: z.string().max(512).nullable().default(null),
|
|
236
|
+
reason: z.string().min(1).max(128)
|
|
237
|
+
});
|
|
238
|
+
z.object({
|
|
239
|
+
source_session_id: z.uuid(),
|
|
240
|
+
events: z.array(NetworkEventInputSchema).min(1).max(100)
|
|
241
|
+
});
|
|
242
|
+
const NetworkEventSchema = NetworkEventInputSchema.omit({ source_seq: true }).extend({
|
|
243
|
+
seq: z.number().int().positive(),
|
|
244
|
+
workspace_id: z.uuid(),
|
|
245
|
+
source_session_id: z.uuid(),
|
|
246
|
+
source_seq: z.number().int().positive()
|
|
247
|
+
});
|
|
248
|
+
CursorQuerySchema(200, 100);
|
|
249
|
+
const NetworkEventPageSchema = CursorPageSchema(NetworkEventSchema);
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region ../contracts/src/workspace.ts
|
|
252
|
+
const WORKSPACE_STATES = [
|
|
253
|
+
"queued",
|
|
254
|
+
"provisioning",
|
|
255
|
+
"connected",
|
|
256
|
+
"ready",
|
|
257
|
+
"preserving",
|
|
258
|
+
"terminating",
|
|
259
|
+
"succeeded",
|
|
260
|
+
"failed",
|
|
261
|
+
"canceled",
|
|
262
|
+
"expired",
|
|
263
|
+
"preserved"
|
|
264
|
+
];
|
|
265
|
+
const TERMINAL_STATES = [
|
|
266
|
+
"succeeded",
|
|
267
|
+
"failed",
|
|
268
|
+
"canceled",
|
|
269
|
+
"expired",
|
|
270
|
+
"preserved"
|
|
271
|
+
];
|
|
272
|
+
const REASON_CODES = [
|
|
273
|
+
"child_exit_success",
|
|
274
|
+
"child_exit_failure",
|
|
275
|
+
"setup_failed",
|
|
276
|
+
"bootstrap_failed",
|
|
277
|
+
"registration_timeout",
|
|
278
|
+
"health_failed",
|
|
279
|
+
"child_crash",
|
|
280
|
+
"provider_lost",
|
|
281
|
+
"disconnect_timeout",
|
|
282
|
+
"canceled_by_caller",
|
|
283
|
+
"deadline_expired",
|
|
284
|
+
"idle_expired",
|
|
285
|
+
"queue_timeout",
|
|
286
|
+
"launch_failed",
|
|
287
|
+
"preserve_requested",
|
|
288
|
+
"preserved_by_policy",
|
|
289
|
+
"checkpoint_created",
|
|
290
|
+
"checkpoint_failed",
|
|
291
|
+
"checkpoint_corrupt",
|
|
292
|
+
"checkpoint_quota_exceeded",
|
|
293
|
+
"checkpoint_storage_lost",
|
|
294
|
+
"restore_requested",
|
|
295
|
+
"restore_failed",
|
|
296
|
+
"image_unavailable",
|
|
297
|
+
"source_resolution_failed",
|
|
298
|
+
"secret_resolution_failed",
|
|
299
|
+
"operation_conflict",
|
|
300
|
+
"network_policy_failed"
|
|
301
|
+
];
|
|
302
|
+
const AGENT_STATES = [
|
|
303
|
+
"unknown",
|
|
304
|
+
"running",
|
|
305
|
+
"stable"
|
|
306
|
+
];
|
|
307
|
+
z.object({
|
|
308
|
+
external_id: z.string().min(1).max(256),
|
|
309
|
+
template: z.object({
|
|
310
|
+
name: z.string().min(1).max(64),
|
|
311
|
+
version: z.string().max(64).optional()
|
|
312
|
+
}),
|
|
313
|
+
launch_input: z.record(z.string(), z.unknown()).optional(),
|
|
314
|
+
source: SourceDescriptorSchema.optional(),
|
|
315
|
+
metadata: z.record(z.string().max(64), z.string().max(512)).optional()
|
|
316
|
+
});
|
|
317
|
+
const TemplateRefSchema = z.object({
|
|
318
|
+
name: z.string(),
|
|
319
|
+
version: z.string(),
|
|
320
|
+
digest: z.string()
|
|
321
|
+
});
|
|
322
|
+
const WorkspaceResourceSchema = z.object({
|
|
323
|
+
id: z.uuid(),
|
|
324
|
+
external_id: z.string(),
|
|
325
|
+
template: TemplateRefSchema,
|
|
326
|
+
state: z.enum(WORKSPACE_STATES),
|
|
327
|
+
reason_code: z.enum(REASON_CODES).nullable(),
|
|
328
|
+
agent_state: z.enum(AGENT_STATES),
|
|
329
|
+
change_cursor: z.number().int().nonnegative(),
|
|
330
|
+
provider_kind: z.string().nullable(),
|
|
331
|
+
provisioning_mode: z.enum(["cold", "warm"]).nullable(),
|
|
332
|
+
network: z.object({ state: z.enum(NETWORK_STATES) }),
|
|
333
|
+
health: z.record(z.string(), z.string()),
|
|
334
|
+
created_at: z.iso.datetime(),
|
|
335
|
+
updated_at: z.iso.datetime(),
|
|
336
|
+
connected_at: z.iso.datetime().nullable(),
|
|
337
|
+
ready_at: z.iso.datetime().nullable(),
|
|
338
|
+
deadline_at: z.iso.datetime(),
|
|
339
|
+
terminal_at: z.iso.datetime().nullable(),
|
|
340
|
+
metadata: z.record(z.string(), z.string()),
|
|
341
|
+
origin_workspace_id: z.uuid().nullable(),
|
|
342
|
+
restored_from_checkpoint_id: z.uuid().nullable(),
|
|
343
|
+
source: SourceDescriptorSchema.extend({
|
|
344
|
+
requested_revision: z.string(),
|
|
345
|
+
resolved_commit: ResolvedSourceSchema.shape.resolved_commit.nullable()
|
|
346
|
+
}).omit({ revision: true }).nullable(),
|
|
347
|
+
persistence: z.object({
|
|
348
|
+
enabled: z.boolean(),
|
|
349
|
+
conversation_restore: z.enum(CONVERSATION_RESTORE_CAPABILITIES),
|
|
350
|
+
conversation_resume: z.object({
|
|
351
|
+
status: z.enum([
|
|
352
|
+
"supported",
|
|
353
|
+
"unsupported",
|
|
354
|
+
"unknown"
|
|
355
|
+
]),
|
|
356
|
+
reason: z.enum(["filesystem_only", "capability_unknown"]).nullable()
|
|
357
|
+
}),
|
|
358
|
+
latest_checkpoint_id: z.uuid().nullable()
|
|
359
|
+
}),
|
|
360
|
+
outputs: z.record(z.string(), z.unknown()),
|
|
361
|
+
failure: z.object({
|
|
362
|
+
reason_code: z.enum(REASON_CODES),
|
|
363
|
+
log_tail: z.string(),
|
|
364
|
+
log_tail_truncated: z.boolean(),
|
|
365
|
+
last_log_seq: z.number().int().nonnegative().nullable()
|
|
366
|
+
}).nullable()
|
|
367
|
+
});
|
|
368
|
+
const WorkspacePageSchema = CursorPageSchema(WorkspaceResourceSchema);
|
|
369
|
+
z.object({
|
|
370
|
+
external_id: z.string().optional(),
|
|
371
|
+
state: z.enum(WORKSPACE_STATES).optional(),
|
|
372
|
+
template: z.string().optional(),
|
|
373
|
+
metadata: z.string().max(4096).optional(),
|
|
374
|
+
created_after: z.iso.datetime().optional(),
|
|
375
|
+
created_before: z.iso.datetime().optional(),
|
|
376
|
+
limit: z.coerce.number().int().positive().max(200).default(50),
|
|
377
|
+
cursor: z.string().optional()
|
|
378
|
+
});
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region ../contracts/src/api.ts
|
|
381
|
+
const TemplatePageSchema = CursorPageSchema(z.object({
|
|
382
|
+
name: z.string(),
|
|
383
|
+
version: z.string(),
|
|
384
|
+
digest: z.string(),
|
|
385
|
+
description: z.string().optional(),
|
|
386
|
+
status: z.enum([
|
|
387
|
+
"active",
|
|
388
|
+
"available",
|
|
389
|
+
"retired"
|
|
390
|
+
])
|
|
391
|
+
}));
|
|
392
|
+
CursorQuerySchema(200, 100);
|
|
393
|
+
const CheckpointPageSchema = CursorPageSchema(CheckpointResourceSchema);
|
|
394
|
+
CursorQuerySchema(200, 50).extend({ state: z.enum(CHECKPOINT_STATES).optional() });
|
|
395
|
+
const WorkspaceChangeSchema = z.object({
|
|
396
|
+
cursor: z.number().int().nonnegative(),
|
|
397
|
+
changed: z.boolean(),
|
|
398
|
+
workspace: WorkspaceResourceSchema
|
|
399
|
+
});
|
|
400
|
+
const PreserveResponseSchema = z.object({
|
|
401
|
+
workspace: WorkspaceResourceSchema,
|
|
402
|
+
checkpoint: CheckpointResourceSchema,
|
|
403
|
+
operation: OperationResourceSchema
|
|
404
|
+
});
|
|
405
|
+
const RestoreResponseSchema = z.object({
|
|
406
|
+
workspace: WorkspaceResourceSchema,
|
|
407
|
+
operation: OperationResourceSchema
|
|
408
|
+
});
|
|
409
|
+
const ResumeResponseSchema = RestoreResponseSchema.extend({ resume: ConversationResumeOutcomeSchema });
|
|
410
|
+
const OutputPageSchema = CursorPageSchema(z.object({
|
|
411
|
+
seq: z.number().int().positive(),
|
|
412
|
+
name: z.string(),
|
|
413
|
+
value: z.unknown(),
|
|
414
|
+
occurred_at: z.iso.datetime()
|
|
415
|
+
}));
|
|
416
|
+
CursorQuerySchema(1e3, 200);
|
|
417
|
+
const WarmPoolInventorySchema = z.object({
|
|
418
|
+
items: z.array(z.object({
|
|
419
|
+
template: z.string(),
|
|
420
|
+
version: z.string(),
|
|
421
|
+
template_digest: z.string(),
|
|
422
|
+
driver: z.string(),
|
|
423
|
+
desired: z.number().int().nonnegative(),
|
|
424
|
+
counts: z.record(z.string(), z.number().int().nonnegative()),
|
|
425
|
+
oldest_ready_age_ms: z.number().nonnegative().nullable()
|
|
426
|
+
})),
|
|
427
|
+
metrics: z.record(z.string(), z.number())
|
|
428
|
+
});
|
|
429
|
+
const StorageInventorySchema = z.object({
|
|
430
|
+
backend: z.string(),
|
|
431
|
+
storage_count: z.number().int().nonnegative(),
|
|
432
|
+
checkpoint_count: z.number().int().nonnegative(),
|
|
433
|
+
unknown_storage: z.array(z.string()),
|
|
434
|
+
unknown_checkpoints: z.array(z.string())
|
|
435
|
+
});
|
|
436
|
+
const StoragePruneResultSchema = z.object({
|
|
437
|
+
deleted: z.number().int().nonnegative(),
|
|
438
|
+
skipped: z.number().int().nonnegative(),
|
|
439
|
+
transcripts_deleted: z.number().int().nonnegative()
|
|
440
|
+
});
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region ../contracts/src/attachment.ts
|
|
443
|
+
const ATTACHMENT_MAX_FILE_BYTES = 26214400;
|
|
444
|
+
const ATTACHMENT_CHUNK_BYTES = 524288;
|
|
445
|
+
const AttachmentMediaTypeSchema = z.string().regex(/^[\w.+-]+\/[\w.+-]+$/).max(255);
|
|
446
|
+
const AttachmentDescriptorSchema = z.object({
|
|
447
|
+
id: z.uuid(),
|
|
448
|
+
name: z.string().min(1).max(255),
|
|
449
|
+
path: z.string().min(1),
|
|
450
|
+
media_type: AttachmentMediaTypeSchema,
|
|
451
|
+
size_bytes: z.number().int().min(0).max(ATTACHMENT_MAX_FILE_BYTES),
|
|
452
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/)
|
|
453
|
+
});
|
|
454
|
+
z.looseObject({
|
|
455
|
+
type: z.literal("user"),
|
|
456
|
+
content: z.string(),
|
|
457
|
+
attachment_ids: z.array(z.uuid()).min(1).max(10).refine((ids) => new Set(ids).size === ids.length, "attachment_ids must be unique").optional()
|
|
458
|
+
});
|
|
459
|
+
const MANIFEST_OPEN = "<pocketcoder-attachments>";
|
|
460
|
+
const MANIFEST_CLOSE = "</pocketcoder-attachments>";
|
|
461
|
+
function splitAttachmentManifest(content) {
|
|
462
|
+
const open = content.lastIndexOf(MANIFEST_OPEN);
|
|
463
|
+
if (open < 0 || !content.trimEnd().endsWith(MANIFEST_CLOSE)) return {
|
|
464
|
+
text: content,
|
|
465
|
+
attachments: null
|
|
466
|
+
};
|
|
467
|
+
const close = content.lastIndexOf(MANIFEST_CLOSE);
|
|
468
|
+
const parsed = z.array(AttachmentDescriptorSchema).safeParse(safeJson(content.slice(open + 25, close)));
|
|
469
|
+
if (!parsed.success) return {
|
|
470
|
+
text: content,
|
|
471
|
+
attachments: null
|
|
472
|
+
};
|
|
473
|
+
return {
|
|
474
|
+
text: content.slice(0, open).trimEnd(),
|
|
475
|
+
attachments: parsed.data
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function safeJson(value) {
|
|
479
|
+
try {
|
|
480
|
+
return JSON.parse(value);
|
|
481
|
+
} catch {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const AttachmentStartPayload = z.object({
|
|
486
|
+
operation_id: z.uuid(),
|
|
487
|
+
attachment_id: z.uuid(),
|
|
488
|
+
name: z.string().min(1).max(255),
|
|
489
|
+
media_type: AttachmentMediaTypeSchema,
|
|
490
|
+
size_bytes: z.number().int().min(0).max(ATTACHMENT_MAX_FILE_BYTES)
|
|
491
|
+
});
|
|
492
|
+
const AttachmentChunkPayload = z.object({
|
|
493
|
+
operation_id: z.uuid(),
|
|
494
|
+
seq: z.number().int().nonnegative(),
|
|
495
|
+
content_b64: z.string().max(Math.ceil(ATTACHMENT_CHUNK_BYTES / 3) * 4 + 4)
|
|
496
|
+
});
|
|
497
|
+
const AttachmentFinishPayload = z.object({ operation_id: z.uuid() });
|
|
498
|
+
const AttachmentAbortPayload = z.object({
|
|
499
|
+
operation_id: z.uuid(),
|
|
500
|
+
reason: z.string().max(512)
|
|
501
|
+
});
|
|
502
|
+
const AttachmentResolvePayload = z.object({
|
|
503
|
+
operation_id: z.uuid(),
|
|
504
|
+
attachment_ids: z.array(z.uuid()).min(1).max(10)
|
|
505
|
+
});
|
|
506
|
+
const AttachmentAckPayload = z.object({
|
|
507
|
+
operation_id: z.uuid(),
|
|
508
|
+
seq: z.number().int().nonnegative(),
|
|
509
|
+
received_bytes: z.number().int().nonnegative()
|
|
510
|
+
});
|
|
511
|
+
const AttachmentResultPayload = z.object({
|
|
512
|
+
operation_id: z.uuid(),
|
|
513
|
+
status: z.enum([
|
|
514
|
+
"created",
|
|
515
|
+
"existing",
|
|
516
|
+
"conflict",
|
|
517
|
+
"failed"
|
|
518
|
+
]),
|
|
519
|
+
descriptor: AttachmentDescriptorSchema.optional(),
|
|
520
|
+
failure_code: z.enum([
|
|
521
|
+
"invalid",
|
|
522
|
+
"too_large",
|
|
523
|
+
"sequence",
|
|
524
|
+
"io",
|
|
525
|
+
"interrupted"
|
|
526
|
+
]).optional(),
|
|
527
|
+
detail: z.string().max(512).optional()
|
|
528
|
+
});
|
|
529
|
+
const AttachmentResolvedPayload = z.object({
|
|
530
|
+
operation_id: z.uuid(),
|
|
531
|
+
descriptors: z.array(AttachmentDescriptorSchema).optional(),
|
|
532
|
+
missing_id: z.uuid().optional()
|
|
533
|
+
});
|
|
534
|
+
const errorCodes = Object.keys({
|
|
535
|
+
"auth.invalid_key": 401,
|
|
536
|
+
"auth.missing_scope": 403,
|
|
537
|
+
"auth.disabled_principal": 403,
|
|
538
|
+
"validation.invalid": 400,
|
|
539
|
+
"idempotency.conflict": 409,
|
|
540
|
+
"capacity.queue_full": 429,
|
|
541
|
+
"capacity.waiters_full": 429,
|
|
542
|
+
"template.not_found": 404,
|
|
543
|
+
"template.version_not_found": 404,
|
|
544
|
+
"template.not_authorized": 403,
|
|
545
|
+
"workspace.not_found": 404,
|
|
546
|
+
"workspace.external_id_conflict": 409,
|
|
547
|
+
"workspace.not_ready": 409,
|
|
548
|
+
"workspace.terminal": 410,
|
|
549
|
+
"workspace.disconnected": 503,
|
|
550
|
+
"terminal.not_declared": 422,
|
|
551
|
+
"terminal.session_limit": 409,
|
|
552
|
+
"terminal.session_not_found": 404,
|
|
553
|
+
"terminal.session_closed": 409,
|
|
554
|
+
"terminal.protocol_unsupported": 426,
|
|
555
|
+
"workspace.persistence_not_enabled": 409,
|
|
556
|
+
"workspace.preserving": 409,
|
|
557
|
+
"checkpoint.not_found": 404,
|
|
558
|
+
"checkpoint.not_ready": 409,
|
|
559
|
+
"checkpoint.none_ready": 409,
|
|
560
|
+
"checkpoint.corrupt": 409,
|
|
561
|
+
"checkpoint.quota_exceeded": 413,
|
|
562
|
+
"checkpoint.in_use": 409,
|
|
563
|
+
"restore.template_not_authorized": 403,
|
|
564
|
+
"restore.image_unavailable": 409,
|
|
565
|
+
"restore.incompatible": 409,
|
|
566
|
+
"resume.unsupported": 409,
|
|
567
|
+
"conversation.expired": 410,
|
|
568
|
+
"conversation.deleted": 410,
|
|
569
|
+
"operation.conflict": 409,
|
|
570
|
+
"source.not_allowed": 422,
|
|
571
|
+
"source.invalid_revision": 422,
|
|
572
|
+
"secret.unavailable": 503,
|
|
573
|
+
"storage.capacity_exhausted": 507,
|
|
574
|
+
"relay.body_too_large": 413,
|
|
575
|
+
"relay.route_not_allowed": 422,
|
|
576
|
+
"relay.streaming_unsupported": 409,
|
|
577
|
+
"relay.deadline_exceeded": 504,
|
|
578
|
+
"relay.upstream_error": 502,
|
|
579
|
+
"attachment.invalid": 400,
|
|
580
|
+
"attachment.too_large": 413,
|
|
581
|
+
"attachment.not_found": 404,
|
|
582
|
+
"attachment.conflict": 409,
|
|
583
|
+
"attachment.unsupported": 409,
|
|
584
|
+
"attachment.interrupted": 503,
|
|
585
|
+
"internal.error": 500
|
|
586
|
+
});
|
|
587
|
+
const ErrorEnvelopeSchema = z.object({ error: z.object({
|
|
588
|
+
code: z.enum(errorCodes),
|
|
589
|
+
message: z.string().min(1),
|
|
590
|
+
request_id: z.string().min(1),
|
|
591
|
+
details: z.record(z.string(), z.unknown()).optional()
|
|
592
|
+
}) });
|
|
593
|
+
WORKSPACE_STATES.map((s) => `workspace.${s}`);
|
|
594
|
+
z.object({
|
|
595
|
+
id: z.uuid(),
|
|
596
|
+
type: z.string(),
|
|
597
|
+
occurred_at: z.iso.datetime(),
|
|
598
|
+
workspace: z.object({
|
|
599
|
+
id: z.uuid(),
|
|
600
|
+
external_id: z.string(),
|
|
601
|
+
state: z.enum(WORKSPACE_STATES),
|
|
602
|
+
reason_code: z.string().nullable(),
|
|
603
|
+
agent_state: z.enum(AGENT_STATES),
|
|
604
|
+
provisioning_mode: z.enum(["cold", "warm"]).nullable().default(null),
|
|
605
|
+
change_cursor: z.number().int().nonnegative(),
|
|
606
|
+
failure: z.object({
|
|
607
|
+
reason_code: z.enum(REASON_CODES),
|
|
608
|
+
log_tail: z.string(),
|
|
609
|
+
log_tail_truncated: z.boolean(),
|
|
610
|
+
last_log_seq: z.number().int().nonnegative().nullable()
|
|
611
|
+
}).nullable(),
|
|
612
|
+
template: z.object({
|
|
613
|
+
name: z.string(),
|
|
614
|
+
version: z.string(),
|
|
615
|
+
digest: z.string()
|
|
616
|
+
}),
|
|
617
|
+
origin_workspace_id: z.uuid().nullable(),
|
|
618
|
+
restored_from_checkpoint_id: z.uuid().nullable(),
|
|
619
|
+
latest_checkpoint_id: z.uuid().nullable(),
|
|
620
|
+
outputs: z.record(z.string(), z.unknown())
|
|
621
|
+
})
|
|
622
|
+
});
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region ../contracts/src/logs.ts
|
|
625
|
+
const LogChunkSchema = z.object({
|
|
626
|
+
seq: z.number().int().positive(),
|
|
627
|
+
stream: z.string(),
|
|
628
|
+
occurred_at: z.iso.datetime(),
|
|
629
|
+
content: z.string()
|
|
630
|
+
});
|
|
631
|
+
CursorQuerySchema(1e3, 200);
|
|
632
|
+
const LogPageSchema = CursorPageSchema(LogChunkSchema);
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region ../contracts/src/protocol-stream.ts
|
|
635
|
+
const ProxyStreamStartPayload = z.object({
|
|
636
|
+
request_id: z.uuid(),
|
|
637
|
+
status: z.number().int().min(100).max(599),
|
|
638
|
+
headers: z.record(z.string(), z.string())
|
|
639
|
+
});
|
|
640
|
+
const ProxyStreamChunkPayload = z.object({
|
|
641
|
+
request_id: z.uuid(),
|
|
642
|
+
seq: z.number().int().nonnegative(),
|
|
643
|
+
content_b64: z.string().max(87400)
|
|
644
|
+
});
|
|
645
|
+
const ProxyStreamEndPayload = z.object({
|
|
646
|
+
request_id: z.uuid(),
|
|
647
|
+
error_code: z.enum([
|
|
648
|
+
"unreachable",
|
|
649
|
+
"deadline",
|
|
650
|
+
"too_large"
|
|
651
|
+
]).optional()
|
|
652
|
+
});
|
|
653
|
+
const ProxyStreamAckPayload = z.object({
|
|
654
|
+
request_id: z.uuid(),
|
|
655
|
+
seq: z.number().int().nonnegative()
|
|
656
|
+
});
|
|
657
|
+
const ProxyStreamCancelPayload = z.object({
|
|
658
|
+
request_id: z.uuid(),
|
|
659
|
+
reason: z.enum([
|
|
660
|
+
"downstream_closed",
|
|
661
|
+
"deadline",
|
|
662
|
+
"too_large",
|
|
663
|
+
"workspace_disconnected"
|
|
664
|
+
])
|
|
665
|
+
});
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region ../contracts/src/template-constants.ts
|
|
668
|
+
function isAbsolutePath(value) {
|
|
669
|
+
return value.startsWith("/") && ![...value].some((character) => character.codePointAt(0) === 0);
|
|
670
|
+
}
|
|
671
|
+
const SECRET_ENV_PATTERN = /(SECRET|TOKEN|PASSWORD|PASSWD|API_?KEY|PRIVATE_?KEY|CREDENTIAL)/i;
|
|
672
|
+
const SECRET_REFERENCE_PREFIX = "secretRef:";
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region ../contracts/src/template-schema.ts
|
|
675
|
+
const NAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
676
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
677
|
+
const IMAGE_DIGEST_RE = /^[^\s@]+@sha256:[0-9a-f]{64}$/;
|
|
678
|
+
function secretMountPath(reference) {
|
|
679
|
+
if (!reference.startsWith("secretRef:")) throw new Error("secret reference must start with secretRef:");
|
|
680
|
+
const name = reference.slice(10);
|
|
681
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/.test(name) || name.includes("..")) throw new Error("invalid secret reference");
|
|
682
|
+
return `/run/pocketcoder/secrets/${name.replaceAll("/", "%2F")}`;
|
|
683
|
+
}
|
|
684
|
+
const DurationSchema = z.string().refine(isDuration, { message: "expected a duration like 15s, 20m, or 2h" });
|
|
685
|
+
const CommandSchema = z.array(z.string().min(1)).min(1);
|
|
686
|
+
const EnvSchema = z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string());
|
|
687
|
+
const SetupStepSchema = z.object({
|
|
688
|
+
name: z.string().regex(NAME_RE),
|
|
689
|
+
command: CommandSchema,
|
|
690
|
+
timeoutSeconds: z.number().int().positive().max(3600).default(300),
|
|
691
|
+
env: EnvSchema.default({}),
|
|
692
|
+
cwd: z.string().refine(isAbsolutePath, "expected an absolute path").optional(),
|
|
693
|
+
runOn: z.array(z.enum(LAUNCH_MODES)).min(1).default(["create"])
|
|
694
|
+
});
|
|
695
|
+
const HarnessSchema = z.object({
|
|
696
|
+
command: CommandSchema,
|
|
697
|
+
env: EnvSchema.default({}),
|
|
698
|
+
cwd: z.string().refine(isAbsolutePath, "expected an absolute path").optional()
|
|
699
|
+
});
|
|
700
|
+
const AgentSchema = HarnessSchema.extend({
|
|
701
|
+
type: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/).default("custom"),
|
|
702
|
+
transport: z.enum(["pty", "acp"]).default("pty"),
|
|
703
|
+
termWidth: z.number().int().min(10).max(65535).optional()
|
|
704
|
+
});
|
|
705
|
+
const TerminalSchema = z.object({
|
|
706
|
+
command: CommandSchema,
|
|
707
|
+
env: EnvSchema.default({}),
|
|
708
|
+
cwd: z.string().refine(isAbsolutePath, "expected an absolute path").optional(),
|
|
709
|
+
maxSessions: z.number().int().min(1).max(8).default(2),
|
|
710
|
+
idleTimeout: DurationSchema.default("10m")
|
|
711
|
+
});
|
|
712
|
+
const ServiceRouteSchema = z.object({
|
|
713
|
+
method: z.enum([
|
|
714
|
+
"GET",
|
|
715
|
+
"POST",
|
|
716
|
+
"PUT",
|
|
717
|
+
"PATCH",
|
|
718
|
+
"DELETE"
|
|
719
|
+
]),
|
|
720
|
+
path: z.string(),
|
|
721
|
+
query: z.array(z.string().min(1)).default([]),
|
|
722
|
+
responseMode: z.enum(["buffered", "stream"]).default("buffered"),
|
|
723
|
+
maxRequestBytes: z.number().int().positive().max(16777216).default(65536),
|
|
724
|
+
maxResponseBytes: z.number().int().positive().max(16777216).default(1048576),
|
|
725
|
+
deadlineSeconds: z.number().int().positive().max(300).default(60)
|
|
726
|
+
});
|
|
727
|
+
const ServiceSchema = z.object({
|
|
728
|
+
baseUrl: z.string(),
|
|
729
|
+
required: z.boolean().default(true),
|
|
730
|
+
healthPath: z.string().default("/status"),
|
|
731
|
+
routes: z.array(ServiceRouteSchema).min(1)
|
|
732
|
+
});
|
|
733
|
+
const SecuritySchema = z.object({
|
|
734
|
+
uid: z.number().int().min(1e3).default(10001),
|
|
735
|
+
gid: z.number().int().min(1e3).default(10001),
|
|
736
|
+
readOnlyRoot: z.boolean().default(true),
|
|
737
|
+
writableMemoryPaths: z.array(z.string().refine(isAbsolutePath, "expected an absolute path")).default(["/tmp"]),
|
|
738
|
+
dropCapabilities: z.array(z.string()).default(["ALL"]),
|
|
739
|
+
allowPrivilegeEscalation: z.literal(false).default(false),
|
|
740
|
+
seccomp: z.literal("RuntimeDefault").default("RuntimeDefault")
|
|
741
|
+
});
|
|
742
|
+
const TimeoutsSchema = z.object({
|
|
743
|
+
start: DurationSchema.default("2m"),
|
|
744
|
+
maxAge: DurationSchema.default("2h"),
|
|
745
|
+
idle: DurationSchema.default("20m"),
|
|
746
|
+
disconnectGrace: DurationSchema.default("5m"),
|
|
747
|
+
terminateGrace: DurationSchema.default("15s")
|
|
748
|
+
});
|
|
749
|
+
const ResourcesSchema = z.object({
|
|
750
|
+
cpu: z.string().regex(/^\d+(\.\d+)?m?$/),
|
|
751
|
+
memory: z.string().regex(/^\d+(Mi|Gi)$/)
|
|
752
|
+
});
|
|
753
|
+
const RepositorySchema = z.object({
|
|
754
|
+
url: z.url().refine((value) => {
|
|
755
|
+
try {
|
|
756
|
+
return new URL(value).username === "" && new URL(value).password === "";
|
|
757
|
+
} catch {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}, "repository URLs must not contain userinfo"),
|
|
761
|
+
credential: z.string().startsWith(SECRET_REFERENCE_PREFIX).max(256).refine((value) => {
|
|
762
|
+
try {
|
|
763
|
+
secretMountPath(value);
|
|
764
|
+
return true;
|
|
765
|
+
} catch {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}, "credential must be a normalized secret reference").optional()
|
|
769
|
+
});
|
|
770
|
+
const SourceSpecSchema = z.object({
|
|
771
|
+
kind: z.literal("git"),
|
|
772
|
+
destinationMount: z.string().regex(NAME_RE),
|
|
773
|
+
repositories: z.record(z.string().regex(NAME_RE), RepositorySchema),
|
|
774
|
+
allowedRevision: z.literal("branch-tag-or-commit").default("branch-tag-or-commit")
|
|
775
|
+
});
|
|
776
|
+
const CheckpointHookSchema = z.object({
|
|
777
|
+
command: CommandSchema,
|
|
778
|
+
timeoutSeconds: z.number().int().positive().max(300).default(30),
|
|
779
|
+
env: EnvSchema.default({}),
|
|
780
|
+
cwd: z.string().refine(isAbsolutePath, "expected an absolute path").optional()
|
|
781
|
+
});
|
|
782
|
+
const TemplateSpecSchema = z.object({
|
|
783
|
+
version: z.string().regex(SEMVER_RE),
|
|
784
|
+
image: z.string().regex(IMAGE_DIGEST_RE, { message: "image must be digest-pinned (repo@sha256:<64 hex>)" }),
|
|
785
|
+
command: CommandSchema.default([
|
|
786
|
+
"/usr/local/bin/pocketcoder-agent",
|
|
787
|
+
"supervise",
|
|
788
|
+
"--launch-input",
|
|
789
|
+
"/run/pocketcoder/input"
|
|
790
|
+
]),
|
|
791
|
+
setup: z.array(SetupStepSchema).max(32).default([]),
|
|
792
|
+
agent: AgentSchema.optional(),
|
|
793
|
+
harness: HarnessSchema.optional(),
|
|
794
|
+
terminal: TerminalSchema.optional(),
|
|
795
|
+
env: EnvSchema.default({}),
|
|
796
|
+
resources: ResourcesSchema,
|
|
797
|
+
timeouts: TimeoutsSchema.prefault({}),
|
|
798
|
+
services: z.record(z.string().regex(NAME_RE), ServiceSchema).optional(),
|
|
799
|
+
security: SecuritySchema.prefault({}),
|
|
800
|
+
network: NetworkPolicySchema,
|
|
801
|
+
maxLaunchInputBytes: z.number().int().positive().max(1048576).default(65536),
|
|
802
|
+
compat: z.object({
|
|
803
|
+
agent: z.string().optional(),
|
|
804
|
+
agentapi: z.string().optional()
|
|
805
|
+
}).default({}),
|
|
806
|
+
persistence: PersistenceSpecSchema.prefault({}),
|
|
807
|
+
source: SourceSpecSchema.nullable().default(null),
|
|
808
|
+
checkpointHook: CheckpointHookSchema.optional(),
|
|
809
|
+
outputs: z.record(z.string().regex(NAME_RE), OutputDeclarationSchema).default({})
|
|
810
|
+
}).superRefine((spec, ctx) => {
|
|
811
|
+
if (!spec.agent && !spec.harness) ctx.addIssue({
|
|
812
|
+
code: "custom",
|
|
813
|
+
path: ["harness"],
|
|
814
|
+
message: "either agent or harness is required"
|
|
815
|
+
});
|
|
816
|
+
if (!spec.agent) return;
|
|
817
|
+
if (spec.agent.transport === "acp" && spec.agent.termWidth !== void 0) ctx.addIssue({
|
|
818
|
+
code: "custom",
|
|
819
|
+
path: ["agent", "termWidth"],
|
|
820
|
+
message: "termWidth is only valid for PTY transport"
|
|
821
|
+
});
|
|
822
|
+
for (const field of [
|
|
823
|
+
"harness",
|
|
824
|
+
"services",
|
|
825
|
+
"checkpointHook"
|
|
826
|
+
]) {
|
|
827
|
+
if (spec[field] === void 0) continue;
|
|
828
|
+
ctx.addIssue({
|
|
829
|
+
code: "custom",
|
|
830
|
+
path: [field],
|
|
831
|
+
message: `agent cannot be combined with ${field}`
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}).transform((spec) => {
|
|
835
|
+
if (spec.agent) {
|
|
836
|
+
const { harness: _harness, services: _services, checkpointHook: _hook, ...native } = spec;
|
|
837
|
+
return {
|
|
838
|
+
...native,
|
|
839
|
+
agent: spec.agent
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const { agent: _agent, ...legacy } = spec;
|
|
843
|
+
return {
|
|
844
|
+
...legacy,
|
|
845
|
+
harness: spec.harness,
|
|
846
|
+
services: spec.services ?? {}
|
|
847
|
+
};
|
|
848
|
+
});
|
|
849
|
+
const TemplateManifestBaseSchema = z.object({
|
|
850
|
+
apiVersion: z.literal("pocketcoder.dev/v1alpha1"),
|
|
851
|
+
kind: z.literal("Template"),
|
|
852
|
+
metadata: z.object({
|
|
853
|
+
name: z.string().regex(NAME_RE),
|
|
854
|
+
description: z.string().max(512).optional()
|
|
855
|
+
}),
|
|
856
|
+
spec: TemplateSpecSchema
|
|
857
|
+
});
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region ../contracts/src/template-runtime.ts
|
|
860
|
+
const LEGACY_AGENTAPI_SERVICE = ServiceSchema.parse({
|
|
861
|
+
baseUrl: "http://127.0.0.1:3284",
|
|
862
|
+
routes: [
|
|
863
|
+
{
|
|
864
|
+
method: "GET",
|
|
865
|
+
path: "/status"
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
method: "GET",
|
|
869
|
+
path: "/messages",
|
|
870
|
+
query: ["after"]
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
method: "POST",
|
|
874
|
+
path: "/message"
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
});
|
|
878
|
+
const AGENTAPI_SERVICE = ServiceSchema.parse({
|
|
879
|
+
...LEGACY_AGENTAPI_SERVICE,
|
|
880
|
+
routes: [...LEGACY_AGENTAPI_SERVICE.routes, {
|
|
881
|
+
method: "GET",
|
|
882
|
+
path: "/events",
|
|
883
|
+
responseMode: "stream",
|
|
884
|
+
maxResponseBytes: 4194304,
|
|
885
|
+
deadlineSeconds: 300
|
|
886
|
+
}]
|
|
887
|
+
});
|
|
888
|
+
function isAgentApiNative(spec) {
|
|
889
|
+
return "agent" in spec && spec.agent !== void 0;
|
|
890
|
+
}
|
|
891
|
+
function agentApiHarness(spec) {
|
|
892
|
+
if (!isAgentApiNative(spec)) return spec.harness;
|
|
893
|
+
return {
|
|
894
|
+
command: [
|
|
895
|
+
"/usr/local/bin/agentapi",
|
|
896
|
+
"server",
|
|
897
|
+
"--type",
|
|
898
|
+
spec.agent.type,
|
|
899
|
+
...spec.agent.transport === "acp" ? ["--experimental-acp"] : [],
|
|
900
|
+
...spec.agent.termWidth === void 0 ? [] : ["--term-width", String(spec.agent.termWidth)],
|
|
901
|
+
"--port",
|
|
902
|
+
"3284",
|
|
903
|
+
"--",
|
|
904
|
+
...spec.agent.command
|
|
905
|
+
],
|
|
906
|
+
env: spec.agent.env,
|
|
907
|
+
...spec.agent.cwd ? { cwd: spec.agent.cwd } : {}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function templateServices(spec) {
|
|
911
|
+
return isAgentApiNative(spec) ? { agent: AGENTAPI_SERVICE } : spec.services;
|
|
912
|
+
}
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region ../contracts/src/template-validation.ts
|
|
915
|
+
function envSources(spec) {
|
|
916
|
+
const sources = [[["spec", "env"], spec.env], [isAgentApiNative(spec) ? [
|
|
917
|
+
"spec",
|
|
918
|
+
"agent",
|
|
919
|
+
"env"
|
|
920
|
+
] : [
|
|
921
|
+
"spec",
|
|
922
|
+
"harness",
|
|
923
|
+
"env"
|
|
924
|
+
], agentApiHarness(spec).env]];
|
|
925
|
+
for (const [i, step] of spec.setup.entries()) sources.push([[
|
|
926
|
+
"spec",
|
|
927
|
+
"setup",
|
|
928
|
+
i,
|
|
929
|
+
"env"
|
|
930
|
+
], step.env]);
|
|
931
|
+
if (!isAgentApiNative(spec) && spec.checkpointHook) sources.push([[
|
|
932
|
+
"spec",
|
|
933
|
+
"checkpointHook",
|
|
934
|
+
"env"
|
|
935
|
+
], spec.checkpointHook.env]);
|
|
936
|
+
if (spec.terminal) sources.push([[
|
|
937
|
+
"spec",
|
|
938
|
+
"terminal",
|
|
939
|
+
"env"
|
|
940
|
+
], spec.terminal.env]);
|
|
941
|
+
return sources;
|
|
942
|
+
}
|
|
943
|
+
const NETWORK_ENV = /* @__PURE__ */ new Set([
|
|
944
|
+
"http_proxy",
|
|
945
|
+
"https_proxy",
|
|
946
|
+
"all_proxy",
|
|
947
|
+
"no_proxy"
|
|
948
|
+
]);
|
|
949
|
+
function validateNetworkEnvironment(spec, ctx) {
|
|
950
|
+
if (spec.network.mode !== "restricted") return;
|
|
951
|
+
for (const [path, env] of envSources(spec)) for (const key of Object.keys(env)) {
|
|
952
|
+
if (!NETWORK_ENV.has(key.toLowerCase())) continue;
|
|
953
|
+
ctx.addIssue({
|
|
954
|
+
code: "custom",
|
|
955
|
+
path: [...path, key],
|
|
956
|
+
message: "proxy variables are reserved by restricted networking"
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function validateServices(spec, ctx) {
|
|
961
|
+
for (const [serviceName, service] of Object.entries(templateServices(spec))) {
|
|
962
|
+
if (!isLoopbackBaseUrl(service.baseUrl)) ctx.addIssue({
|
|
963
|
+
code: "custom",
|
|
964
|
+
path: [
|
|
965
|
+
"spec",
|
|
966
|
+
"services",
|
|
967
|
+
serviceName,
|
|
968
|
+
"baseUrl"
|
|
969
|
+
],
|
|
970
|
+
message: "service baseUrl must be a loopback http URL"
|
|
971
|
+
});
|
|
972
|
+
if (!isNormalizedPath(service.healthPath)) ctx.addIssue({
|
|
973
|
+
code: "custom",
|
|
974
|
+
path: [
|
|
975
|
+
"spec",
|
|
976
|
+
"services",
|
|
977
|
+
serviceName,
|
|
978
|
+
"healthPath"
|
|
979
|
+
],
|
|
980
|
+
message: "healthPath must be a normalized absolute path"
|
|
981
|
+
});
|
|
982
|
+
validateServiceRoutes(serviceName, service.routes, ctx);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function validateServiceRoutes(serviceName, routes, ctx) {
|
|
986
|
+
const seen = /* @__PURE__ */ new Set();
|
|
987
|
+
for (const [index, route] of routes.entries()) {
|
|
988
|
+
if (!isNormalizedPath(route.path)) ctx.addIssue({
|
|
989
|
+
code: "custom",
|
|
990
|
+
path: [
|
|
991
|
+
"spec",
|
|
992
|
+
"services",
|
|
993
|
+
serviceName,
|
|
994
|
+
"routes",
|
|
995
|
+
index,
|
|
996
|
+
"path"
|
|
997
|
+
],
|
|
998
|
+
message: "route path must be normalized, absolute, and exact"
|
|
999
|
+
});
|
|
1000
|
+
const key = `${route.method} ${route.path}`;
|
|
1001
|
+
if (seen.has(key)) ctx.addIssue({
|
|
1002
|
+
code: "custom",
|
|
1003
|
+
path: [
|
|
1004
|
+
"spec",
|
|
1005
|
+
"services",
|
|
1006
|
+
serviceName,
|
|
1007
|
+
"routes",
|
|
1008
|
+
index
|
|
1009
|
+
],
|
|
1010
|
+
message: `duplicate route: ${key}`
|
|
1011
|
+
});
|
|
1012
|
+
seen.add(key);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function validateEnvironment(spec, ctx) {
|
|
1016
|
+
for (const [where, env] of envSources(spec)) for (const [key, value] of Object.entries(env)) {
|
|
1017
|
+
if (SECRET_ENV_PATTERN.test(key) && !value.startsWith("secretRef:") && value !== "") ctx.addIssue({
|
|
1018
|
+
code: "custom",
|
|
1019
|
+
path: where,
|
|
1020
|
+
message: `env ${key} looks like a secret literal; use a "${SECRET_REFERENCE_PREFIX}" reference resolved by the deployment`
|
|
1021
|
+
});
|
|
1022
|
+
if (value.startsWith("secretRef:") && !validSecretReference(value)) ctx.addIssue({
|
|
1023
|
+
code: "custom",
|
|
1024
|
+
path: where,
|
|
1025
|
+
message: `env ${key} has an invalid secret reference`
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
function validateTerminal(spec, ctx) {
|
|
1030
|
+
if (!spec.terminal?.cwd || spec.terminal.cwd === "/" || isNormalizedFilesystemPath(spec.terminal.cwd)) return;
|
|
1031
|
+
ctx.addIssue({
|
|
1032
|
+
code: "custom",
|
|
1033
|
+
path: [
|
|
1034
|
+
"spec",
|
|
1035
|
+
"terminal",
|
|
1036
|
+
"cwd"
|
|
1037
|
+
],
|
|
1038
|
+
message: "cwd must be a normalized absolute filesystem path"
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
function validSecretReference(value) {
|
|
1042
|
+
try {
|
|
1043
|
+
secretMountPath(value);
|
|
1044
|
+
return true;
|
|
1045
|
+
} catch {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function validateOutputs(spec, ctx) {
|
|
1050
|
+
for (const name of Object.keys(spec.outputs)) {
|
|
1051
|
+
if (!SECRET_ENV_PATTERN.test(name)) continue;
|
|
1052
|
+
ctx.addIssue({
|
|
1053
|
+
code: "custom",
|
|
1054
|
+
path: [
|
|
1055
|
+
"spec",
|
|
1056
|
+
"outputs",
|
|
1057
|
+
name
|
|
1058
|
+
],
|
|
1059
|
+
message: "secret-like names are not allowed as durable outputs"
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function validateWritableMemoryPaths(spec, ctx) {
|
|
1064
|
+
for (const path of spec.security.writableMemoryPaths) {
|
|
1065
|
+
if (!path.includes("..")) continue;
|
|
1066
|
+
ctx.addIssue({
|
|
1067
|
+
code: "custom",
|
|
1068
|
+
path: [
|
|
1069
|
+
"spec",
|
|
1070
|
+
"security",
|
|
1071
|
+
"writableMemoryPaths"
|
|
1072
|
+
],
|
|
1073
|
+
message: "writable paths must not contain .."
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function isLoopbackBaseUrl(value) {
|
|
1078
|
+
let url;
|
|
1079
|
+
try {
|
|
1080
|
+
url = new URL(value);
|
|
1081
|
+
} catch {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
if (url.protocol !== "http:") return false;
|
|
1085
|
+
if (url.pathname !== "/" || url.search !== "" || url.hash !== "") return false;
|
|
1086
|
+
return [
|
|
1087
|
+
"127.0.0.1",
|
|
1088
|
+
"localhost",
|
|
1089
|
+
"[::1]"
|
|
1090
|
+
].includes(url.hostname) || url.hostname === "::1";
|
|
1091
|
+
}
|
|
1092
|
+
const FORBIDDEN_PERSISTENCE_ROOTS = [
|
|
1093
|
+
"/",
|
|
1094
|
+
"/run/pocketcoder",
|
|
1095
|
+
"/run/pocketcoder/secrets",
|
|
1096
|
+
"/proc",
|
|
1097
|
+
"/sys",
|
|
1098
|
+
"/dev"
|
|
1099
|
+
];
|
|
1100
|
+
function pathContains(parent, child) {
|
|
1101
|
+
return child === parent || child.startsWith(`${parent}/`);
|
|
1102
|
+
}
|
|
1103
|
+
function validatePersistence(spec, ctx) {
|
|
1104
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1105
|
+
const mounts = spec.persistence.mounts;
|
|
1106
|
+
for (const [index, mount] of mounts.entries()) validatePersistenceMount(spec, mounts, mount, index, seenNames, ctx);
|
|
1107
|
+
validateSourceMount(spec, mounts, ctx);
|
|
1108
|
+
if (spec.persistence.conversationRestore === "supported" && (!spec.persistence.sessionCompatibility || mounts.length < 2)) ctx.addIssue({
|
|
1109
|
+
code: "custom",
|
|
1110
|
+
path: [
|
|
1111
|
+
"spec",
|
|
1112
|
+
"persistence",
|
|
1113
|
+
"conversationRestore"
|
|
1114
|
+
],
|
|
1115
|
+
message: "supported conversation restore requires sessionCompatibility and a separate harness-state mount"
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
function validatePersistenceMount(spec, mounts, mount, index, seenNames, ctx) {
|
|
1119
|
+
const path = [
|
|
1120
|
+
"spec",
|
|
1121
|
+
"persistence",
|
|
1122
|
+
"mounts",
|
|
1123
|
+
index,
|
|
1124
|
+
"target"
|
|
1125
|
+
];
|
|
1126
|
+
if (!isNormalizedFilesystemPath(mount.target)) ctx.addIssue({
|
|
1127
|
+
code: "custom",
|
|
1128
|
+
path,
|
|
1129
|
+
message: "target must be a normalized absolute filesystem path"
|
|
1130
|
+
});
|
|
1131
|
+
if (FORBIDDEN_PERSISTENCE_ROOTS.some((root) => pathContains(root, mount.target))) ctx.addIssue({
|
|
1132
|
+
code: "custom",
|
|
1133
|
+
path,
|
|
1134
|
+
message: "target overlaps a protected runtime or kernel path"
|
|
1135
|
+
});
|
|
1136
|
+
if (seenNames.has(mount.name)) ctx.addIssue({
|
|
1137
|
+
code: "custom",
|
|
1138
|
+
path: [
|
|
1139
|
+
"spec",
|
|
1140
|
+
"persistence",
|
|
1141
|
+
"mounts",
|
|
1142
|
+
index,
|
|
1143
|
+
"name"
|
|
1144
|
+
],
|
|
1145
|
+
message: "persistence mount names must be unique"
|
|
1146
|
+
});
|
|
1147
|
+
seenNames.add(mount.name);
|
|
1148
|
+
validateMountOverlap(mounts, mount, index, path, ctx);
|
|
1149
|
+
validateMemoryPathOverlap(spec, mount, path, ctx);
|
|
1150
|
+
}
|
|
1151
|
+
function validateMountOverlap(mounts, mount, index, path, ctx) {
|
|
1152
|
+
for (const [otherIndex, other] of mounts.entries()) {
|
|
1153
|
+
if (otherIndex >= index) continue;
|
|
1154
|
+
if (!pathContains(other.target, mount.target) && !pathContains(mount.target, other.target)) continue;
|
|
1155
|
+
ctx.addIssue({
|
|
1156
|
+
code: "custom",
|
|
1157
|
+
path,
|
|
1158
|
+
message: `target overlaps persistence mount ${other.name}`
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function validateMemoryPathOverlap(spec, mount, path, ctx) {
|
|
1163
|
+
for (const memoryPath of spec.security.writableMemoryPaths) {
|
|
1164
|
+
if (!pathContains(memoryPath, mount.target) && !pathContains(mount.target, memoryPath)) continue;
|
|
1165
|
+
ctx.addIssue({
|
|
1166
|
+
code: "custom",
|
|
1167
|
+
path,
|
|
1168
|
+
message: `target overlaps writableMemoryPath ${memoryPath}`
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function validateSourceMount(spec, mounts, ctx) {
|
|
1173
|
+
if (!spec.source) return;
|
|
1174
|
+
if (mounts.find((mount) => mount.name === spec.source?.destinationMount)) return;
|
|
1175
|
+
ctx.addIssue({
|
|
1176
|
+
code: "custom",
|
|
1177
|
+
path: [
|
|
1178
|
+
"spec",
|
|
1179
|
+
"source",
|
|
1180
|
+
"destinationMount"
|
|
1181
|
+
],
|
|
1182
|
+
message: "source destinationMount must name a persistence mount"
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
function isNormalizedFilesystemPath(path) {
|
|
1186
|
+
if (!isAbsolutePath(path) || path === "/") return false;
|
|
1187
|
+
if (path.endsWith("/") || path.includes("//") || path.includes("\\") || path.includes(",")) return false;
|
|
1188
|
+
if ([...path].some((character) => {
|
|
1189
|
+
const code = character.codePointAt(0) ?? 0;
|
|
1190
|
+
return code < 32 || code === 127;
|
|
1191
|
+
})) return false;
|
|
1192
|
+
return !path.split("/").some((part) => part === "." || part === "..");
|
|
1193
|
+
}
|
|
1194
|
+
function isNormalizedPath(path) {
|
|
1195
|
+
if (!path.startsWith("/")) return false;
|
|
1196
|
+
if (path.includes("..") || path.includes("//")) return false;
|
|
1197
|
+
if (/[?#\s]/.test(path)) return false;
|
|
1198
|
+
if (/%2e|%2f|%5c/i.test(path)) return false;
|
|
1199
|
+
if ([...path].some((character) => character.charCodeAt(0) <= 31)) return false;
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
function validateTemplateSpec(spec, ctx) {
|
|
1203
|
+
validateServices(spec, ctx);
|
|
1204
|
+
validateEnvironment(spec, ctx);
|
|
1205
|
+
validateOutputs(spec, ctx);
|
|
1206
|
+
validateWritableMemoryPaths(spec, ctx);
|
|
1207
|
+
validatePersistence(spec, ctx);
|
|
1208
|
+
validateNetworkEnvironment(spec, ctx);
|
|
1209
|
+
validateTerminal(spec, ctx);
|
|
1210
|
+
}
|
|
1211
|
+
TemplateManifestBaseSchema.superRefine((manifest, ctx) => {
|
|
1212
|
+
validateTemplateSpec(manifest.spec, ctx);
|
|
1213
|
+
});
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region ../contracts/src/terminal.ts
|
|
1216
|
+
const TERMINAL_CHUNK_BYTES = 32768;
|
|
1217
|
+
const TERMINAL_REPLAY_BUFFER_BYTES = 65536;
|
|
1218
|
+
function base64Schema(maxBytes) {
|
|
1219
|
+
return z.string().regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/).refine((value) => {
|
|
1220
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
1221
|
+
return value.length / 4 * 3 - padding <= maxBytes;
|
|
1222
|
+
}, `decoded payload must not exceed ${maxBytes} bytes`);
|
|
1223
|
+
}
|
|
1224
|
+
const TerminalChunkSchema = base64Schema(TERMINAL_CHUNK_BYTES);
|
|
1225
|
+
const TerminalReplaySchema = base64Schema(TERMINAL_REPLAY_BUFFER_BYTES);
|
|
1226
|
+
const TerminalSizeSchema = z.number().int().min(1).max(1e3);
|
|
1227
|
+
const TerminalOpenPayload = z.object({
|
|
1228
|
+
session_id: z.uuid(),
|
|
1229
|
+
rows: TerminalSizeSchema,
|
|
1230
|
+
cols: TerminalSizeSchema,
|
|
1231
|
+
reattach: z.boolean()
|
|
1232
|
+
});
|
|
1233
|
+
const TerminalInputPayload = z.object({
|
|
1234
|
+
session_id: z.uuid(),
|
|
1235
|
+
data_b64: TerminalChunkSchema
|
|
1236
|
+
});
|
|
1237
|
+
const TerminalResizePayload = z.object({
|
|
1238
|
+
session_id: z.uuid(),
|
|
1239
|
+
rows: TerminalSizeSchema,
|
|
1240
|
+
cols: TerminalSizeSchema
|
|
1241
|
+
});
|
|
1242
|
+
const TerminalClosePayload = z.object({
|
|
1243
|
+
session_id: z.uuid(),
|
|
1244
|
+
reason: z.enum([
|
|
1245
|
+
"checkpoint",
|
|
1246
|
+
"workspace_ended",
|
|
1247
|
+
"closed"
|
|
1248
|
+
])
|
|
1249
|
+
});
|
|
1250
|
+
const TerminalOpenedPayload = z.object({
|
|
1251
|
+
session_id: z.uuid(),
|
|
1252
|
+
replay_b64: TerminalReplaySchema.optional()
|
|
1253
|
+
});
|
|
1254
|
+
const TerminalOutputPayload = z.object({
|
|
1255
|
+
session_id: z.uuid(),
|
|
1256
|
+
data_b64: TerminalChunkSchema
|
|
1257
|
+
});
|
|
1258
|
+
const TerminalClosedPayload = z.object({
|
|
1259
|
+
session_id: z.uuid(),
|
|
1260
|
+
reason: z.enum([
|
|
1261
|
+
"exit",
|
|
1262
|
+
"idle",
|
|
1263
|
+
"checkpoint",
|
|
1264
|
+
"workspace_ended",
|
|
1265
|
+
"error",
|
|
1266
|
+
"closed"
|
|
1267
|
+
]),
|
|
1268
|
+
exit_code: z.number().int().nullable().optional(),
|
|
1269
|
+
detail: z.string().max(512).optional()
|
|
1270
|
+
});
|
|
1271
|
+
z.discriminatedUnion("type", [z.object({
|
|
1272
|
+
type: z.literal("input"),
|
|
1273
|
+
data_b64: TerminalChunkSchema
|
|
1274
|
+
}), z.object({
|
|
1275
|
+
type: z.literal("resize"),
|
|
1276
|
+
rows: TerminalSizeSchema,
|
|
1277
|
+
cols: TerminalSizeSchema
|
|
1278
|
+
})]);
|
|
1279
|
+
const ServerTerminalMessageSchema = z.discriminatedUnion("type", [
|
|
1280
|
+
z.object({
|
|
1281
|
+
type: z.literal("opened"),
|
|
1282
|
+
session_id: z.uuid(),
|
|
1283
|
+
replay_b64: TerminalReplaySchema.optional()
|
|
1284
|
+
}),
|
|
1285
|
+
z.object({
|
|
1286
|
+
type: z.literal("output"),
|
|
1287
|
+
data_b64: TerminalChunkSchema
|
|
1288
|
+
}),
|
|
1289
|
+
z.object({
|
|
1290
|
+
type: z.literal("closed"),
|
|
1291
|
+
reason: z.enum([
|
|
1292
|
+
"exit",
|
|
1293
|
+
"idle",
|
|
1294
|
+
"checkpoint",
|
|
1295
|
+
"workspace_ended",
|
|
1296
|
+
"agent_detached"
|
|
1297
|
+
]),
|
|
1298
|
+
exit_code: z.number().int().nullable().optional()
|
|
1299
|
+
}),
|
|
1300
|
+
z.object({
|
|
1301
|
+
type: z.literal("status"),
|
|
1302
|
+
state: z.enum(["reconnecting", "resumed"])
|
|
1303
|
+
})
|
|
1304
|
+
]);
|
|
1305
|
+
const TerminalSessionSchema = z.object({
|
|
1306
|
+
session_id: z.uuid(),
|
|
1307
|
+
workspace_id: z.uuid(),
|
|
1308
|
+
key_id: z.uuid(),
|
|
1309
|
+
opened_at: z.iso.datetime(),
|
|
1310
|
+
closed_at: z.iso.datetime().nullable(),
|
|
1311
|
+
duration_ms: z.number().int().nonnegative().nullable(),
|
|
1312
|
+
close_reason: z.enum([
|
|
1313
|
+
"exit",
|
|
1314
|
+
"idle",
|
|
1315
|
+
"checkpoint",
|
|
1316
|
+
"workspace_ended",
|
|
1317
|
+
"agent_detached",
|
|
1318
|
+
"client_closed"
|
|
1319
|
+
]).nullable(),
|
|
1320
|
+
exit_code: z.number().int().nullable(),
|
|
1321
|
+
bytes_in: z.number().int().nonnegative(),
|
|
1322
|
+
bytes_out: z.number().int().nonnegative()
|
|
1323
|
+
});
|
|
1324
|
+
CursorQuerySchema(200, 50);
|
|
1325
|
+
const TerminalSessionPageSchema = CursorPageSchema(TerminalSessionSchema);
|
|
1326
|
+
const ProviderInputSchema = z.object({
|
|
1327
|
+
workspace_id: z.uuid(),
|
|
1328
|
+
server_url: z.string(),
|
|
1329
|
+
registration_secret: z.string(),
|
|
1330
|
+
template_digest: z.string(),
|
|
1331
|
+
template_name: z.string().default("unknown"),
|
|
1332
|
+
template_version: z.string().default("unknown"),
|
|
1333
|
+
launch_mode: z.enum(LAUNCH_MODES).default("create"),
|
|
1334
|
+
source: SourceDescriptorSchema.optional(),
|
|
1335
|
+
restore: z.object({
|
|
1336
|
+
checkpoint_id: z.uuid(),
|
|
1337
|
+
origin_workspace_id: z.uuid()
|
|
1338
|
+
}).optional(),
|
|
1339
|
+
launch_input: z.record(z.string(), z.unknown()).optional()
|
|
1340
|
+
});
|
|
1341
|
+
const PoolProviderInputSchema = z.object({
|
|
1342
|
+
pool_runtime_id: z.uuid(),
|
|
1343
|
+
server_url: z.string(),
|
|
1344
|
+
enrollment_secret: z.string().min(1),
|
|
1345
|
+
template_digest: z.string(),
|
|
1346
|
+
template_name: z.string(),
|
|
1347
|
+
template_version: z.string()
|
|
1348
|
+
});
|
|
1349
|
+
z.union([ProviderInputSchema, PoolProviderInputSchema]);
|
|
1350
|
+
z.object({
|
|
1351
|
+
v: z.literal(3),
|
|
1352
|
+
type: z.literal("pool_registered"),
|
|
1353
|
+
pool_runtime_id: z.uuid(),
|
|
1354
|
+
template: z.object({
|
|
1355
|
+
name: z.string(),
|
|
1356
|
+
version: z.string(),
|
|
1357
|
+
digest: z.string()
|
|
1358
|
+
}),
|
|
1359
|
+
agent_version: z.string()
|
|
1360
|
+
});
|
|
1361
|
+
z.object({
|
|
1362
|
+
v: z.literal(3),
|
|
1363
|
+
type: z.literal("lease_assignment"),
|
|
1364
|
+
input: ProviderInputSchema
|
|
1365
|
+
});
|
|
1366
|
+
const SOURCE_CREDENTIAL_MAX_BYTES = 65536;
|
|
1367
|
+
const EnvelopeBase = z.object({
|
|
1368
|
+
v: z.union([
|
|
1369
|
+
z.literal(1),
|
|
1370
|
+
z.literal(2),
|
|
1371
|
+
z.literal(3),
|
|
1372
|
+
z.literal(4),
|
|
1373
|
+
z.literal(5),
|
|
1374
|
+
z.literal(6)
|
|
1375
|
+
]),
|
|
1376
|
+
workspace_id: z.uuid(),
|
|
1377
|
+
connection_id: z.uuid(),
|
|
1378
|
+
seq: z.number().int().nonnegative(),
|
|
1379
|
+
sent_at: z.iso.datetime()
|
|
1380
|
+
});
|
|
1381
|
+
const RegisteredPayload = z.object({
|
|
1382
|
+
agent_version: z.string(),
|
|
1383
|
+
template: z.object({
|
|
1384
|
+
name: z.string(),
|
|
1385
|
+
version: z.string(),
|
|
1386
|
+
digest: z.string()
|
|
1387
|
+
}),
|
|
1388
|
+
agentapi_version: z.string().optional(),
|
|
1389
|
+
services: z.array(z.string()),
|
|
1390
|
+
pid: z.number().int().positive()
|
|
1391
|
+
});
|
|
1392
|
+
const HeartbeatPayload = z.object({
|
|
1393
|
+
child: z.enum([
|
|
1394
|
+
"starting",
|
|
1395
|
+
"setup",
|
|
1396
|
+
"running",
|
|
1397
|
+
"exited",
|
|
1398
|
+
"terminating"
|
|
1399
|
+
]),
|
|
1400
|
+
agentapi_state: z.enum([
|
|
1401
|
+
"unknown",
|
|
1402
|
+
"stable",
|
|
1403
|
+
"running"
|
|
1404
|
+
]).optional()
|
|
1405
|
+
});
|
|
1406
|
+
const ProcessStatePayload = z.object({
|
|
1407
|
+
phase: z.enum([
|
|
1408
|
+
"starting",
|
|
1409
|
+
"setup",
|
|
1410
|
+
"running",
|
|
1411
|
+
"exited",
|
|
1412
|
+
"terminating"
|
|
1413
|
+
]),
|
|
1414
|
+
exit_code: z.number().int().nullable().optional(),
|
|
1415
|
+
setup_step: z.string().optional(),
|
|
1416
|
+
detail: z.string().max(512).optional()
|
|
1417
|
+
});
|
|
1418
|
+
const ServiceHealthPayload = z.object({
|
|
1419
|
+
service: z.string(),
|
|
1420
|
+
health: z.enum([
|
|
1421
|
+
"unknown",
|
|
1422
|
+
"starting",
|
|
1423
|
+
"healthy",
|
|
1424
|
+
"unhealthy"
|
|
1425
|
+
]),
|
|
1426
|
+
detail: z.string().max(512).optional()
|
|
1427
|
+
});
|
|
1428
|
+
const AgentStatePayload = z.object({ state: z.enum(["running", "stable"]) });
|
|
1429
|
+
const NetworkStatePayload = z.object({
|
|
1430
|
+
state: z.enum([
|
|
1431
|
+
"starting",
|
|
1432
|
+
"ready",
|
|
1433
|
+
"degraded"
|
|
1434
|
+
]),
|
|
1435
|
+
detail: z.string().max(512).optional()
|
|
1436
|
+
});
|
|
1437
|
+
const LogChunkPayload = z.object({
|
|
1438
|
+
stream: z.enum([
|
|
1439
|
+
"stdout",
|
|
1440
|
+
"stderr",
|
|
1441
|
+
"runtime"
|
|
1442
|
+
]),
|
|
1443
|
+
content_b64: z.string().max(87400),
|
|
1444
|
+
occurred_at: z.iso.datetime()
|
|
1445
|
+
});
|
|
1446
|
+
const ProxyResponsePayload = z.object({
|
|
1447
|
+
request_id: z.uuid(),
|
|
1448
|
+
status: z.number().int().min(100).max(599).optional(),
|
|
1449
|
+
headers: z.record(z.string(), z.string()).default({}),
|
|
1450
|
+
body_b64: z.string().optional(),
|
|
1451
|
+
error_code: z.enum([
|
|
1452
|
+
"unreachable",
|
|
1453
|
+
"deadline",
|
|
1454
|
+
"too_large"
|
|
1455
|
+
]).optional()
|
|
1456
|
+
});
|
|
1457
|
+
const TerminationAckPayload = z.object({ phase: z.enum([
|
|
1458
|
+
"term_sent",
|
|
1459
|
+
"killed",
|
|
1460
|
+
"exited"
|
|
1461
|
+
]) });
|
|
1462
|
+
const SourceResolvedPayload = z.object({
|
|
1463
|
+
repository: z.string().min(1).max(64),
|
|
1464
|
+
requested_revision: z.string().min(1).max(256),
|
|
1465
|
+
resolved_commit: z.string().regex(/^[0-9a-f]{40,64}$/)
|
|
1466
|
+
});
|
|
1467
|
+
const CheckpointStatusPayload = z.object({
|
|
1468
|
+
operation_id: z.uuid(),
|
|
1469
|
+
phase: z.enum([
|
|
1470
|
+
"quiescing",
|
|
1471
|
+
"quiesced",
|
|
1472
|
+
"failed"
|
|
1473
|
+
]),
|
|
1474
|
+
detail: z.string().max(512).optional()
|
|
1475
|
+
});
|
|
1476
|
+
const OutputPublishedPayload = z.object({
|
|
1477
|
+
name: z.string().min(1).max(64),
|
|
1478
|
+
value: z.unknown()
|
|
1479
|
+
});
|
|
1480
|
+
const RestoreStatusPayload = z.object({
|
|
1481
|
+
phase: z.enum([
|
|
1482
|
+
"validating",
|
|
1483
|
+
"ready",
|
|
1484
|
+
"failed"
|
|
1485
|
+
]),
|
|
1486
|
+
capability: z.enum(CONVERSATION_RESTORE_CAPABILITIES),
|
|
1487
|
+
detail: z.string().max(512).optional()
|
|
1488
|
+
});
|
|
1489
|
+
const ConversationMessagePayload = ConversationMessageInputSchema;
|
|
1490
|
+
z.discriminatedUnion("type", [
|
|
1491
|
+
EnvelopeBase.extend({
|
|
1492
|
+
type: z.literal("registered"),
|
|
1493
|
+
payload: RegisteredPayload
|
|
1494
|
+
}),
|
|
1495
|
+
EnvelopeBase.extend({
|
|
1496
|
+
type: z.literal("heartbeat"),
|
|
1497
|
+
payload: HeartbeatPayload
|
|
1498
|
+
}),
|
|
1499
|
+
EnvelopeBase.extend({
|
|
1500
|
+
type: z.literal("process_state"),
|
|
1501
|
+
payload: ProcessStatePayload
|
|
1502
|
+
}),
|
|
1503
|
+
EnvelopeBase.extend({
|
|
1504
|
+
type: z.literal("service_health"),
|
|
1505
|
+
payload: ServiceHealthPayload
|
|
1506
|
+
}),
|
|
1507
|
+
EnvelopeBase.extend({
|
|
1508
|
+
type: z.literal("agent_state"),
|
|
1509
|
+
payload: AgentStatePayload
|
|
1510
|
+
}),
|
|
1511
|
+
EnvelopeBase.extend({
|
|
1512
|
+
type: z.literal("network_state"),
|
|
1513
|
+
payload: NetworkStatePayload
|
|
1514
|
+
}),
|
|
1515
|
+
EnvelopeBase.extend({
|
|
1516
|
+
type: z.literal("log_chunk"),
|
|
1517
|
+
payload: LogChunkPayload
|
|
1518
|
+
}),
|
|
1519
|
+
EnvelopeBase.extend({
|
|
1520
|
+
type: z.literal("proxy_response"),
|
|
1521
|
+
payload: ProxyResponsePayload
|
|
1522
|
+
}),
|
|
1523
|
+
EnvelopeBase.extend({
|
|
1524
|
+
type: z.literal("proxy_stream_start"),
|
|
1525
|
+
payload: ProxyStreamStartPayload
|
|
1526
|
+
}),
|
|
1527
|
+
EnvelopeBase.extend({
|
|
1528
|
+
type: z.literal("proxy_stream_chunk"),
|
|
1529
|
+
payload: ProxyStreamChunkPayload
|
|
1530
|
+
}),
|
|
1531
|
+
EnvelopeBase.extend({
|
|
1532
|
+
type: z.literal("proxy_stream_end"),
|
|
1533
|
+
payload: ProxyStreamEndPayload
|
|
1534
|
+
}),
|
|
1535
|
+
EnvelopeBase.extend({
|
|
1536
|
+
type: z.literal("terminal_opened"),
|
|
1537
|
+
payload: TerminalOpenedPayload
|
|
1538
|
+
}),
|
|
1539
|
+
EnvelopeBase.extend({
|
|
1540
|
+
type: z.literal("terminal_output"),
|
|
1541
|
+
payload: TerminalOutputPayload
|
|
1542
|
+
}),
|
|
1543
|
+
EnvelopeBase.extend({
|
|
1544
|
+
type: z.literal("terminal_closed"),
|
|
1545
|
+
payload: TerminalClosedPayload
|
|
1546
|
+
}),
|
|
1547
|
+
EnvelopeBase.extend({
|
|
1548
|
+
type: z.literal("termination_ack"),
|
|
1549
|
+
payload: TerminationAckPayload
|
|
1550
|
+
}),
|
|
1551
|
+
EnvelopeBase.extend({
|
|
1552
|
+
type: z.literal("source_resolved"),
|
|
1553
|
+
payload: SourceResolvedPayload
|
|
1554
|
+
}),
|
|
1555
|
+
EnvelopeBase.extend({
|
|
1556
|
+
type: z.literal("checkpoint_status"),
|
|
1557
|
+
payload: CheckpointStatusPayload
|
|
1558
|
+
}),
|
|
1559
|
+
EnvelopeBase.extend({
|
|
1560
|
+
type: z.literal("output_published"),
|
|
1561
|
+
payload: OutputPublishedPayload
|
|
1562
|
+
}),
|
|
1563
|
+
EnvelopeBase.extend({
|
|
1564
|
+
type: z.literal("restore_status"),
|
|
1565
|
+
payload: RestoreStatusPayload
|
|
1566
|
+
}),
|
|
1567
|
+
EnvelopeBase.extend({
|
|
1568
|
+
type: z.literal("conversation_message"),
|
|
1569
|
+
payload: ConversationMessagePayload
|
|
1570
|
+
}),
|
|
1571
|
+
EnvelopeBase.extend({
|
|
1572
|
+
type: z.literal("attachment_ack"),
|
|
1573
|
+
payload: AttachmentAckPayload
|
|
1574
|
+
}),
|
|
1575
|
+
EnvelopeBase.extend({
|
|
1576
|
+
type: z.literal("attachment_result"),
|
|
1577
|
+
payload: AttachmentResultPayload
|
|
1578
|
+
}),
|
|
1579
|
+
EnvelopeBase.extend({
|
|
1580
|
+
type: z.literal("attachment_resolved"),
|
|
1581
|
+
payload: AttachmentResolvedPayload
|
|
1582
|
+
})
|
|
1583
|
+
]);
|
|
1584
|
+
const ExecSpecSchema = z.object({
|
|
1585
|
+
agentapi_native: z.boolean().default(false),
|
|
1586
|
+
setup: z.array(SetupStepSchema),
|
|
1587
|
+
harness: HarnessSchema,
|
|
1588
|
+
env: z.record(z.string(), z.string()),
|
|
1589
|
+
services: z.record(z.string(), ServiceSchema),
|
|
1590
|
+
terminal: z.object({
|
|
1591
|
+
command: z.array(z.string().min(1)).min(1),
|
|
1592
|
+
cwd: z.string().optional(),
|
|
1593
|
+
env: z.record(z.string(), z.string()),
|
|
1594
|
+
max_sessions: z.number().int().min(1).max(8),
|
|
1595
|
+
idle_timeout_seconds: z.number().int().positive(),
|
|
1596
|
+
replay_buffer_bytes: z.number().int().positive()
|
|
1597
|
+
}).nullable().default(null),
|
|
1598
|
+
timeouts: TimeoutsSchema,
|
|
1599
|
+
security: z.object({ writable_memory_paths: z.array(z.string()) }).default({ writable_memory_paths: [] }),
|
|
1600
|
+
network: z.discriminatedUnion("mode", [z.object({ mode: z.literal("unrestricted") }), z.object({
|
|
1601
|
+
mode: z.literal("restricted"),
|
|
1602
|
+
proxy_url: z.url(),
|
|
1603
|
+
health_url: z.url()
|
|
1604
|
+
})]).default({ mode: "unrestricted" }),
|
|
1605
|
+
launch_mode: z.enum(LAUNCH_MODES).default("create"),
|
|
1606
|
+
source: SourceDescriptorSchema.extend({
|
|
1607
|
+
url: z.url(),
|
|
1608
|
+
destination: z.string(),
|
|
1609
|
+
credential: z.string().min(1).max(SOURCE_CREDENTIAL_MAX_BYTES).refine((value) => !value.includes("\0"), "credential must not contain NUL bytes").nullable().default(null)
|
|
1610
|
+
}).nullable().default(null),
|
|
1611
|
+
restore: z.object({
|
|
1612
|
+
checkpoint_id: z.uuid(),
|
|
1613
|
+
origin_workspace_id: z.uuid()
|
|
1614
|
+
}).nullable().default(null),
|
|
1615
|
+
persistence: z.object({
|
|
1616
|
+
mounts: z.array(z.object({
|
|
1617
|
+
name: z.string(),
|
|
1618
|
+
target: z.string()
|
|
1619
|
+
})),
|
|
1620
|
+
conversation_restore: z.enum(CONVERSATION_RESTORE_CAPABILITIES)
|
|
1621
|
+
}),
|
|
1622
|
+
checkpoint_hook: z.object({
|
|
1623
|
+
command: z.array(z.string().min(1)).min(1),
|
|
1624
|
+
timeout_seconds: z.number().int().positive(),
|
|
1625
|
+
env: z.record(z.string(), z.string()),
|
|
1626
|
+
cwd: z.string().optional()
|
|
1627
|
+
}).nullable(),
|
|
1628
|
+
outputs: z.record(z.string(), z.unknown())
|
|
1629
|
+
});
|
|
1630
|
+
const RegisteredAckPayload = z.object({
|
|
1631
|
+
epoch: z.number().int().positive(),
|
|
1632
|
+
reconnect_credential: z.string().optional(),
|
|
1633
|
+
limits: z.object({
|
|
1634
|
+
max_frame_bytes: z.number().int().positive(),
|
|
1635
|
+
max_inflight_relay: z.number().int().positive(),
|
|
1636
|
+
log_chunk_bytes: z.number().int().positive(),
|
|
1637
|
+
heartbeat_seconds: z.number().int().positive()
|
|
1638
|
+
}),
|
|
1639
|
+
exec: ExecSpecSchema
|
|
1640
|
+
});
|
|
1641
|
+
const ProxyRequestPayload = z.object({
|
|
1642
|
+
request_id: z.uuid(),
|
|
1643
|
+
service: z.string(),
|
|
1644
|
+
method: z.enum([
|
|
1645
|
+
"GET",
|
|
1646
|
+
"POST",
|
|
1647
|
+
"PUT",
|
|
1648
|
+
"PATCH",
|
|
1649
|
+
"DELETE"
|
|
1650
|
+
]),
|
|
1651
|
+
path: z.string(),
|
|
1652
|
+
query: z.record(z.string(), z.string()).default({}),
|
|
1653
|
+
headers: z.record(z.string(), z.string()).default({}),
|
|
1654
|
+
body_b64: z.string().optional(),
|
|
1655
|
+
deadline_ms: z.number().int().positive()
|
|
1656
|
+
});
|
|
1657
|
+
const SignalPayload = z.object({ signal: z.enum(["TERM", "KILL"]) });
|
|
1658
|
+
const HealthProbePayload = z.object({ service: z.string() });
|
|
1659
|
+
const ShutdownPayload = z.object({ reason: z.string().max(512) });
|
|
1660
|
+
const PrepareCheckpointPayload = z.object({
|
|
1661
|
+
operation_id: z.uuid(),
|
|
1662
|
+
deadline_ms: z.number().int().positive()
|
|
1663
|
+
});
|
|
1664
|
+
z.discriminatedUnion("type", [
|
|
1665
|
+
EnvelopeBase.extend({
|
|
1666
|
+
type: z.literal("registered_ack"),
|
|
1667
|
+
payload: RegisteredAckPayload
|
|
1668
|
+
}),
|
|
1669
|
+
EnvelopeBase.extend({
|
|
1670
|
+
type: z.literal("proxy_request"),
|
|
1671
|
+
payload: ProxyRequestPayload
|
|
1672
|
+
}),
|
|
1673
|
+
EnvelopeBase.extend({
|
|
1674
|
+
type: z.literal("proxy_stream_ack"),
|
|
1675
|
+
payload: ProxyStreamAckPayload
|
|
1676
|
+
}),
|
|
1677
|
+
EnvelopeBase.extend({
|
|
1678
|
+
type: z.literal("proxy_stream_cancel"),
|
|
1679
|
+
payload: ProxyStreamCancelPayload
|
|
1680
|
+
}),
|
|
1681
|
+
EnvelopeBase.extend({
|
|
1682
|
+
type: z.literal("terminal_open"),
|
|
1683
|
+
payload: TerminalOpenPayload
|
|
1684
|
+
}),
|
|
1685
|
+
EnvelopeBase.extend({
|
|
1686
|
+
type: z.literal("terminal_input"),
|
|
1687
|
+
payload: TerminalInputPayload
|
|
1688
|
+
}),
|
|
1689
|
+
EnvelopeBase.extend({
|
|
1690
|
+
type: z.literal("terminal_resize"),
|
|
1691
|
+
payload: TerminalResizePayload
|
|
1692
|
+
}),
|
|
1693
|
+
EnvelopeBase.extend({
|
|
1694
|
+
type: z.literal("terminal_close"),
|
|
1695
|
+
payload: TerminalClosePayload
|
|
1696
|
+
}),
|
|
1697
|
+
EnvelopeBase.extend({
|
|
1698
|
+
type: z.literal("signal"),
|
|
1699
|
+
payload: SignalPayload
|
|
1700
|
+
}),
|
|
1701
|
+
EnvelopeBase.extend({
|
|
1702
|
+
type: z.literal("health_probe"),
|
|
1703
|
+
payload: HealthProbePayload
|
|
1704
|
+
}),
|
|
1705
|
+
EnvelopeBase.extend({
|
|
1706
|
+
type: z.literal("shutdown"),
|
|
1707
|
+
payload: ShutdownPayload
|
|
1708
|
+
}),
|
|
1709
|
+
EnvelopeBase.extend({
|
|
1710
|
+
type: z.literal("prepare_checkpoint"),
|
|
1711
|
+
payload: PrepareCheckpointPayload
|
|
1712
|
+
}),
|
|
1713
|
+
EnvelopeBase.extend({
|
|
1714
|
+
type: z.literal("attachment_start"),
|
|
1715
|
+
payload: AttachmentStartPayload
|
|
1716
|
+
}),
|
|
1717
|
+
EnvelopeBase.extend({
|
|
1718
|
+
type: z.literal("attachment_chunk"),
|
|
1719
|
+
payload: AttachmentChunkPayload
|
|
1720
|
+
}),
|
|
1721
|
+
EnvelopeBase.extend({
|
|
1722
|
+
type: z.literal("attachment_finish"),
|
|
1723
|
+
payload: AttachmentFinishPayload
|
|
1724
|
+
}),
|
|
1725
|
+
EnvelopeBase.extend({
|
|
1726
|
+
type: z.literal("attachment_abort"),
|
|
1727
|
+
payload: AttachmentAbortPayload
|
|
1728
|
+
}),
|
|
1729
|
+
EnvelopeBase.extend({
|
|
1730
|
+
type: z.literal("attachment_resolve"),
|
|
1731
|
+
payload: AttachmentResolvePayload
|
|
1732
|
+
})
|
|
1733
|
+
]);
|
|
1734
|
+
//#endregion
|
|
1735
|
+
//#region src/admin.ts
|
|
1736
|
+
var AdministrationApi = class {
|
|
1737
|
+
transport;
|
|
1738
|
+
constructor(transport) {
|
|
1739
|
+
this.transport = transport;
|
|
1740
|
+
}
|
|
1741
|
+
warmPools(options = {}) {
|
|
1742
|
+
return this.transport.request("/v1/warm-pools", WarmPoolInventorySchema, options);
|
|
1743
|
+
}
|
|
1744
|
+
storageInventory(options = {}) {
|
|
1745
|
+
return this.transport.request("/v1/storage/inventory", StorageInventorySchema, options);
|
|
1746
|
+
}
|
|
1747
|
+
pruneStorage(options = {}) {
|
|
1748
|
+
return this.transport.request("/v1/storage/prune", StoragePruneResultSchema, {
|
|
1749
|
+
method: "POST",
|
|
1750
|
+
signal: options.signal
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
//#endregion
|
|
1755
|
+
//#region src/errors.ts
|
|
1756
|
+
z.enum(["client.non_json_response", "client.invalid_response"]);
|
|
1757
|
+
var PocketCoderError = class extends Error {
|
|
1758
|
+
code;
|
|
1759
|
+
status;
|
|
1760
|
+
requestId;
|
|
1761
|
+
details;
|
|
1762
|
+
constructor(input) {
|
|
1763
|
+
super(input.message);
|
|
1764
|
+
this.name = "PocketCoderError";
|
|
1765
|
+
this.code = input.code;
|
|
1766
|
+
this.status = input.status;
|
|
1767
|
+
this.requestId = input.requestId;
|
|
1768
|
+
this.details = input.details;
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
var ConversationGoneError = class extends PocketCoderError {};
|
|
1772
|
+
var WorkspaceTerminalError = class extends Error {
|
|
1773
|
+
workspace;
|
|
1774
|
+
constructor(workspace) {
|
|
1775
|
+
const reason = workspace.reason_code ?? workspace.failure?.reason_code ?? "no reason";
|
|
1776
|
+
const tail = workspace.failure?.log_tail.trim();
|
|
1777
|
+
super([`workspace ${workspace.id} reached ${workspace.state} (${reason})`, ...tail ? [`failure log:\n${tail}`] : []].join("\n"));
|
|
1778
|
+
this.name = "WorkspaceTerminalError";
|
|
1779
|
+
this.workspace = workspace;
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
function responseError(response, body) {
|
|
1783
|
+
const parsed = ErrorEnvelopeSchema.safeParse(body);
|
|
1784
|
+
if (!parsed.success) return new PocketCoderError({
|
|
1785
|
+
message: `PocketCoder returned an invalid error response (${response.status})`,
|
|
1786
|
+
code: "client.invalid_response",
|
|
1787
|
+
status: response.status
|
|
1788
|
+
});
|
|
1789
|
+
const { code, message, request_id: requestId, details } = parsed.data.error;
|
|
1790
|
+
return new (code === "conversation.deleted" || code === "conversation.expired" ? ConversationGoneError : PocketCoderError)({
|
|
1791
|
+
message,
|
|
1792
|
+
code,
|
|
1793
|
+
status: response.status,
|
|
1794
|
+
requestId,
|
|
1795
|
+
details
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
//#endregion
|
|
1799
|
+
//#region src/attachments.ts
|
|
1800
|
+
function contentDisposition(name) {
|
|
1801
|
+
const clean = name.replace(/[\r\n]/g, "");
|
|
1802
|
+
if (/^[ -~]*$/.test(clean)) return `attachment; filename="${clean.replace(/(["\\])/g, "\\$1")}"`;
|
|
1803
|
+
return `attachment; filename*=UTF-8''${encodeURIComponent(clean)}`;
|
|
1804
|
+
}
|
|
1805
|
+
function progressStream(body, total, onProgress) {
|
|
1806
|
+
const source = new Response(body).body ?? new ReadableStream({ start: (c) => c.close() });
|
|
1807
|
+
let uploaded = 0;
|
|
1808
|
+
return source.pipeThrough(new TransformStream({ transform(chunk, controller) {
|
|
1809
|
+
uploaded += chunk.byteLength;
|
|
1810
|
+
controller.enqueue(chunk);
|
|
1811
|
+
onProgress(Math.min(uploaded, total), total);
|
|
1812
|
+
} }));
|
|
1813
|
+
}
|
|
1814
|
+
var AttachmentsApi = class {
|
|
1815
|
+
transport;
|
|
1816
|
+
constructor(transport) {
|
|
1817
|
+
this.transport = transport;
|
|
1818
|
+
}
|
|
1819
|
+
async upload(workspaceId, input) {
|
|
1820
|
+
const id = (input.id ?? crypto.randomUUID()).toLowerCase();
|
|
1821
|
+
const body = input.onProgress ? progressStream(input.body, input.sizeBytes, input.onProgress) : input.body;
|
|
1822
|
+
const init = {
|
|
1823
|
+
method: "PUT",
|
|
1824
|
+
...input.signal ? { signal: input.signal } : {},
|
|
1825
|
+
headers: {
|
|
1826
|
+
"content-type": input.mediaType ?? "application/octet-stream",
|
|
1827
|
+
"content-disposition": contentDisposition(input.name),
|
|
1828
|
+
"content-length": String(input.sizeBytes)
|
|
1829
|
+
},
|
|
1830
|
+
body,
|
|
1831
|
+
...body instanceof ReadableStream ? { duplex: "half" } : {}
|
|
1832
|
+
};
|
|
1833
|
+
return await this.transport.request(`/v1/workspaces/${encodeURIComponent(workspaceId)}/attachments/${id}`, AttachmentDescriptorSchema, init);
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
var AgentApi = class {
|
|
1837
|
+
transport;
|
|
1838
|
+
constructor(transport) {
|
|
1839
|
+
this.transport = transport;
|
|
1840
|
+
}
|
|
1841
|
+
async sendMessage(workspaceId, input) {
|
|
1842
|
+
const response = await this.transport.raw(`/v1/workspaces/${encodeURIComponent(workspaceId)}/agent/message`, {
|
|
1843
|
+
method: "POST",
|
|
1844
|
+
...input.signal ? { signal: input.signal } : {},
|
|
1845
|
+
headers: { "content-type": "application/json" },
|
|
1846
|
+
body: JSON.stringify({
|
|
1847
|
+
type: "user",
|
|
1848
|
+
content: input.content,
|
|
1849
|
+
...input.attachmentIds?.length ? { attachment_ids: input.attachmentIds } : {}
|
|
1850
|
+
})
|
|
1851
|
+
});
|
|
1852
|
+
if (response.ok) return;
|
|
1853
|
+
const text = await response.text();
|
|
1854
|
+
let body;
|
|
1855
|
+
try {
|
|
1856
|
+
body = JSON.parse(text);
|
|
1857
|
+
} catch {
|
|
1858
|
+
throw new PocketCoderError({
|
|
1859
|
+
message: `PocketCoder returned a non-JSON response (${response.status})`,
|
|
1860
|
+
code: "client.non_json_response",
|
|
1861
|
+
status: response.status
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
throw responseError(response, body);
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
//#endregion
|
|
1868
|
+
//#region src/common.ts
|
|
1869
|
+
function queryString(values) {
|
|
1870
|
+
const params = new URLSearchParams();
|
|
1871
|
+
for (const [key, value] of Object.entries(values)) if (value !== void 0) params.set(key, String(value));
|
|
1872
|
+
return params.toString();
|
|
1873
|
+
}
|
|
1874
|
+
function page(body) {
|
|
1875
|
+
return {
|
|
1876
|
+
items: body.items,
|
|
1877
|
+
nextCursor: body.next_cursor
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/checkpoints.ts
|
|
1882
|
+
var CheckpointsApi = class {
|
|
1883
|
+
transport;
|
|
1884
|
+
constructor(transport) {
|
|
1885
|
+
this.transport = transport;
|
|
1886
|
+
}
|
|
1887
|
+
async list(workspaceId, query = {}, options = {}) {
|
|
1888
|
+
return page(await this.transport.request(`/v1/workspaces/${encodeURIComponent(workspaceId)}/checkpoints?${queryString({
|
|
1889
|
+
state: query.state,
|
|
1890
|
+
cursor: query.cursor,
|
|
1891
|
+
limit: query.limit ?? 50
|
|
1892
|
+
})}`, CheckpointPageSchema, options));
|
|
1893
|
+
}
|
|
1894
|
+
get(id, options = {}) {
|
|
1895
|
+
return this.transport.request(`/v1/checkpoints/${encodeURIComponent(id)}`, CheckpointResourceSchema, options);
|
|
1896
|
+
}
|
|
1897
|
+
verify(id, key, options = {}) {
|
|
1898
|
+
return this.operationRequest(`/v1/checkpoints/${encodeURIComponent(id)}/verify`, "POST", key, options);
|
|
1899
|
+
}
|
|
1900
|
+
delete(id, key, options = {}) {
|
|
1901
|
+
return this.operationRequest(`/v1/checkpoints/${encodeURIComponent(id)}`, "DELETE", key, options);
|
|
1902
|
+
}
|
|
1903
|
+
restore(id, input, key, options = {}) {
|
|
1904
|
+
return this.transport.request(`/v1/checkpoints/${encodeURIComponent(id)}/restore`, RestoreResponseSchema, {
|
|
1905
|
+
method: "POST",
|
|
1906
|
+
signal: options.signal,
|
|
1907
|
+
headers: { "Idempotency-Key": key },
|
|
1908
|
+
body: JSON.stringify(input)
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
operationRequest(path, method, key, options) {
|
|
1912
|
+
return this.transport.request(path, OperationResourceSchema, {
|
|
1913
|
+
method,
|
|
1914
|
+
signal: options.signal,
|
|
1915
|
+
headers: { "Idempotency-Key": key }
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
var OperationsApi = class {
|
|
1920
|
+
transport;
|
|
1921
|
+
constructor(transport) {
|
|
1922
|
+
this.transport = transport;
|
|
1923
|
+
}
|
|
1924
|
+
get(id, options = {}) {
|
|
1925
|
+
return this.transport.request(`/v1/operations/${encodeURIComponent(id)}`, OperationResourceSchema, options);
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
//#endregion
|
|
1929
|
+
//#region src/conversations.ts
|
|
1930
|
+
var ConversationsApi = class {
|
|
1931
|
+
transport;
|
|
1932
|
+
constructor(transport) {
|
|
1933
|
+
this.transport = transport;
|
|
1934
|
+
}
|
|
1935
|
+
async list(id, query = {}, options = {}) {
|
|
1936
|
+
return page(await this.transport.request(`/v1/workspaces/${encodeURIComponent(id)}/conversation?${queryString({
|
|
1937
|
+
cursor: query.cursor,
|
|
1938
|
+
limit: query.limit ?? 100
|
|
1939
|
+
})}`, ConversationPageSchema, options));
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
//#endregion
|
|
1943
|
+
//#region src/diagnostics.ts
|
|
1944
|
+
var WorkspaceCursorApi = class {
|
|
1945
|
+
transport;
|
|
1946
|
+
resource;
|
|
1947
|
+
schema;
|
|
1948
|
+
constructor(transport, resource, schema) {
|
|
1949
|
+
this.transport = transport;
|
|
1950
|
+
this.resource = resource;
|
|
1951
|
+
this.schema = schema;
|
|
1952
|
+
}
|
|
1953
|
+
async list(workspaceId, query = {}, options = {}) {
|
|
1954
|
+
return page(await this.transport.request(`/v1/workspaces/${encodeURIComponent(workspaceId)}/${this.resource}?${queryString({
|
|
1955
|
+
cursor: query.cursor,
|
|
1956
|
+
limit: query.limit ?? 100
|
|
1957
|
+
})}`, this.schema, options));
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
var LogsApi = class extends WorkspaceCursorApi {
|
|
1961
|
+
constructor(transport) {
|
|
1962
|
+
super(transport, "logs", LogPageSchema);
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
var NetworkEventsApi = class extends WorkspaceCursorApi {
|
|
1966
|
+
constructor(transport) {
|
|
1967
|
+
super(transport, "network-events", NetworkEventPageSchema);
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
var OutputsApi = class extends WorkspaceCursorApi {
|
|
1971
|
+
constructor(transport) {
|
|
1972
|
+
super(transport, "outputs", OutputPageSchema);
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
//#endregion
|
|
1976
|
+
//#region src/templates.ts
|
|
1977
|
+
var TemplatesApi = class {
|
|
1978
|
+
transport;
|
|
1979
|
+
constructor(transport) {
|
|
1980
|
+
this.transport = transport;
|
|
1981
|
+
}
|
|
1982
|
+
async page(query = {}, options = {}) {
|
|
1983
|
+
return page(await this.transport.request(`/v1/templates?${queryString({
|
|
1984
|
+
limit: query.limit ?? 100,
|
|
1985
|
+
cursor: query.cursor
|
|
1986
|
+
})}`, TemplatePageSchema, options));
|
|
1987
|
+
}
|
|
1988
|
+
async list(options = {}) {
|
|
1989
|
+
return (await this.page({}, options)).items;
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
//#endregion
|
|
1993
|
+
//#region src/terminals.ts
|
|
1994
|
+
var TerminalConnection = class {
|
|
1995
|
+
socket;
|
|
1996
|
+
messageListeners = /* @__PURE__ */ new Set();
|
|
1997
|
+
openListeners = /* @__PURE__ */ new Set();
|
|
1998
|
+
closeListeners = /* @__PURE__ */ new Set();
|
|
1999
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
2000
|
+
pendingMessages = [];
|
|
2001
|
+
opened = false;
|
|
2002
|
+
closedEvent = null;
|
|
2003
|
+
errored = false;
|
|
2004
|
+
constructor(socket) {
|
|
2005
|
+
this.socket = socket;
|
|
2006
|
+
this.opened = socket.readyState === WebSocket.OPEN;
|
|
2007
|
+
socket.addEventListener("open", () => {
|
|
2008
|
+
this.opened = true;
|
|
2009
|
+
for (const listener of this.openListeners) listener();
|
|
2010
|
+
});
|
|
2011
|
+
socket.addEventListener("message", (event) => this.handleMessage(event));
|
|
2012
|
+
socket.addEventListener("close", (event) => {
|
|
2013
|
+
this.closedEvent = event;
|
|
2014
|
+
for (const listener of this.closeListeners) listener(event);
|
|
2015
|
+
});
|
|
2016
|
+
socket.addEventListener("error", () => {
|
|
2017
|
+
this.errored = true;
|
|
2018
|
+
for (const listener of this.errorListeners) listener();
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
onMessage(listener) {
|
|
2022
|
+
this.messageListeners.add(listener);
|
|
2023
|
+
for (const message of this.pendingMessages.splice(0)) listener(message);
|
|
2024
|
+
return () => this.messageListeners.delete(listener);
|
|
2025
|
+
}
|
|
2026
|
+
onOpen(listener) {
|
|
2027
|
+
this.openListeners.add(listener);
|
|
2028
|
+
if (this.opened) queueMicrotask(() => this.openListeners.has(listener) && listener());
|
|
2029
|
+
return () => this.openListeners.delete(listener);
|
|
2030
|
+
}
|
|
2031
|
+
onClose(listener) {
|
|
2032
|
+
this.closeListeners.add(listener);
|
|
2033
|
+
if (this.closedEvent) {
|
|
2034
|
+
const event = this.closedEvent;
|
|
2035
|
+
queueMicrotask(() => this.closeListeners.has(listener) && listener(event));
|
|
2036
|
+
}
|
|
2037
|
+
return () => this.closeListeners.delete(listener);
|
|
2038
|
+
}
|
|
2039
|
+
onError(listener) {
|
|
2040
|
+
this.errorListeners.add(listener);
|
|
2041
|
+
if (this.errored) queueMicrotask(() => this.errorListeners.has(listener) && listener());
|
|
2042
|
+
return () => this.errorListeners.delete(listener);
|
|
2043
|
+
}
|
|
2044
|
+
sendInput(value) {
|
|
2045
|
+
const bytes = typeof value === "string" ? Buffer.from(value) : value;
|
|
2046
|
+
for (let offset = 0; offset < bytes.byteLength; offset += TERMINAL_CHUNK_BYTES) this.socket.send(JSON.stringify({
|
|
2047
|
+
type: "input",
|
|
2048
|
+
data_b64: Buffer.from(bytes.subarray(offset, offset + TERMINAL_CHUNK_BYTES)).toString("base64")
|
|
2049
|
+
}));
|
|
2050
|
+
}
|
|
2051
|
+
resize(rows, cols) {
|
|
2052
|
+
this.socket.send(JSON.stringify({
|
|
2053
|
+
type: "resize",
|
|
2054
|
+
rows,
|
|
2055
|
+
cols
|
|
2056
|
+
}));
|
|
2057
|
+
}
|
|
2058
|
+
close(code = 1e3, reason = "client detached") {
|
|
2059
|
+
this.socket.close(code, reason);
|
|
2060
|
+
}
|
|
2061
|
+
handleMessage(event) {
|
|
2062
|
+
if (typeof event.data !== "string") return;
|
|
2063
|
+
let value;
|
|
2064
|
+
try {
|
|
2065
|
+
value = JSON.parse(event.data);
|
|
2066
|
+
} catch {
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
const parsed = ServerTerminalMessageSchema.safeParse(value);
|
|
2070
|
+
if (!parsed.success) return;
|
|
2071
|
+
if (this.messageListeners.size === 0) {
|
|
2072
|
+
this.pendingMessages.push(parsed.data);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
for (const listener of this.messageListeners) listener(parsed.data);
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
var TerminalsApi = class {
|
|
2079
|
+
transport;
|
|
2080
|
+
constructor(transport) {
|
|
2081
|
+
this.transport = transport;
|
|
2082
|
+
}
|
|
2083
|
+
connect(workspaceId, options = {}) {
|
|
2084
|
+
const query = options.sessionId ? `?${queryString({ session: options.sessionId })}` : "";
|
|
2085
|
+
return new TerminalConnection(this.transport.webSocket(`/v1/workspaces/${encodeURIComponent(workspaceId)}/terminal${query}`));
|
|
2086
|
+
}
|
|
2087
|
+
async list(workspaceId, query = {}, options = {}) {
|
|
2088
|
+
return page(await this.transport.request(`/v1/workspaces/${encodeURIComponent(workspaceId)}/terminal-sessions?${queryString({
|
|
2089
|
+
cursor: query.cursor,
|
|
2090
|
+
limit: query.limit ?? 50
|
|
2091
|
+
})}`, TerminalSessionPageSchema, options));
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
//#endregion
|
|
2095
|
+
//#region src/transport.ts
|
|
2096
|
+
function baseUrlOf(value) {
|
|
2097
|
+
let url;
|
|
2098
|
+
try {
|
|
2099
|
+
url = new URL(value);
|
|
2100
|
+
} catch {
|
|
2101
|
+
throw new TypeError("PocketCoder baseUrl must be a valid HTTP(S) URL");
|
|
2102
|
+
}
|
|
2103
|
+
if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("PocketCoder baseUrl must be a valid HTTP(S) URL");
|
|
2104
|
+
url.pathname = url.pathname.replace(/\/$/, "");
|
|
2105
|
+
url.search = "";
|
|
2106
|
+
url.hash = "";
|
|
2107
|
+
return url.toString().replace(/\/$/, "");
|
|
2108
|
+
}
|
|
2109
|
+
var PocketCoderTransport = class {
|
|
2110
|
+
baseUrl;
|
|
2111
|
+
apiKey;
|
|
2112
|
+
timeoutMs;
|
|
2113
|
+
maxRetries;
|
|
2114
|
+
fetchImpl;
|
|
2115
|
+
webSocketFactory;
|
|
2116
|
+
constructor(config, fetchImpl = fetch) {
|
|
2117
|
+
this.baseUrl = baseUrlOf(config.baseUrl);
|
|
2118
|
+
this.apiKey = config.apiKey;
|
|
2119
|
+
this.timeoutMs = config.timeoutMs ?? 6e4;
|
|
2120
|
+
this.maxRetries = config.maxRetries ?? 2;
|
|
2121
|
+
this.fetchImpl = config.fetch ?? fetchImpl;
|
|
2122
|
+
this.webSocketFactory = config.webSocket ?? ((url, headers) => new WebSocket(url, { headers }));
|
|
2123
|
+
}
|
|
2124
|
+
webSocket(path) {
|
|
2125
|
+
const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
|
|
2126
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2127
|
+
return this.webSocketFactory(url.toString(), { authorization: `Bearer ${this.apiKey}` });
|
|
2128
|
+
}
|
|
2129
|
+
async raw(path, init = {}) {
|
|
2130
|
+
const timeout = AbortSignal.timeout(this.timeoutMs);
|
|
2131
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
2132
|
+
const request = {
|
|
2133
|
+
...init,
|
|
2134
|
+
signal,
|
|
2135
|
+
headers: {
|
|
2136
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
2137
|
+
...init.body ? { "content-type": "application/json" } : {},
|
|
2138
|
+
...init.headers
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
const retryable = isIdempotent(request);
|
|
2142
|
+
for (let attempt = 0;; attempt += 1) try {
|
|
2143
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`, request);
|
|
2144
|
+
if (!retryable || attempt >= this.maxRetries || !retryableStatus(response.status)) return response;
|
|
2145
|
+
await delay(retryDelayMs(response, attempt), signal);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
if (!retryable || attempt >= this.maxRetries || signal.aborted) throw error;
|
|
2148
|
+
await delay(100 * 2 ** attempt, signal);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
async request(path, schema, init = {}) {
|
|
2152
|
+
const response = await this.raw(path, init);
|
|
2153
|
+
const text = await response.text();
|
|
2154
|
+
let body;
|
|
2155
|
+
try {
|
|
2156
|
+
body = text ? JSON.parse(text) : void 0;
|
|
2157
|
+
} catch {
|
|
2158
|
+
throw new PocketCoderError({
|
|
2159
|
+
message: `PocketCoder returned a non-JSON response (${response.status})`,
|
|
2160
|
+
code: "client.non_json_response",
|
|
2161
|
+
status: response.status
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
if (!response.ok) throw responseError(response, body);
|
|
2165
|
+
const parsed = schema.safeParse(body);
|
|
2166
|
+
if (!parsed.success) throw new PocketCoderError({
|
|
2167
|
+
message: `PocketCoder returned an invalid response (${response.status})`,
|
|
2168
|
+
code: "client.invalid_response",
|
|
2169
|
+
status: response.status,
|
|
2170
|
+
details: { issues: z.treeifyError(parsed.error) }
|
|
2171
|
+
});
|
|
2172
|
+
return parsed.data;
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
function isIdempotent(init) {
|
|
2176
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
2177
|
+
if ([
|
|
2178
|
+
"GET",
|
|
2179
|
+
"HEAD",
|
|
2180
|
+
"OPTIONS"
|
|
2181
|
+
].includes(method)) return true;
|
|
2182
|
+
return new Headers(init.headers).has("idempotency-key");
|
|
2183
|
+
}
|
|
2184
|
+
function retryableStatus(status) {
|
|
2185
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
2186
|
+
}
|
|
2187
|
+
function retryDelayMs(response, attempt) {
|
|
2188
|
+
const retryAfter = response.headers.get("retry-after");
|
|
2189
|
+
if (retryAfter !== null) {
|
|
2190
|
+
const seconds = Number(retryAfter);
|
|
2191
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
2192
|
+
const date = Date.parse(retryAfter);
|
|
2193
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
2194
|
+
}
|
|
2195
|
+
return 100 * 2 ** attempt;
|
|
2196
|
+
}
|
|
2197
|
+
async function delay(milliseconds, signal) {
|
|
2198
|
+
if (milliseconds === 0) return;
|
|
2199
|
+
await new Promise((resolve, reject) => {
|
|
2200
|
+
const onAbort = () => {
|
|
2201
|
+
clearTimeout(timer);
|
|
2202
|
+
reject(signal.reason);
|
|
2203
|
+
};
|
|
2204
|
+
const timer = setTimeout(() => {
|
|
2205
|
+
signal.removeEventListener("abort", onAbort);
|
|
2206
|
+
resolve();
|
|
2207
|
+
}, milliseconds);
|
|
2208
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
//#endregion
|
|
2212
|
+
//#region src/workspaces.ts
|
|
2213
|
+
const TERMINAL_WORKSPACE_STATES = new Set(TERMINAL_STATES);
|
|
2214
|
+
var WorkspacesApi = class {
|
|
2215
|
+
transport;
|
|
2216
|
+
constructor(transport) {
|
|
2217
|
+
this.transport = transport;
|
|
2218
|
+
}
|
|
2219
|
+
async list(query = {}, options = {}) {
|
|
2220
|
+
return page(await this.transport.request(`/v1/workspaces?${queryString({
|
|
2221
|
+
state: query.state,
|
|
2222
|
+
template: query.template,
|
|
2223
|
+
external_id: query.externalId,
|
|
2224
|
+
limit: query.limit ?? 50,
|
|
2225
|
+
cursor: query.cursor
|
|
2226
|
+
})}`, WorkspacePageSchema, options));
|
|
2227
|
+
}
|
|
2228
|
+
async *all(query = {}, options = {}) {
|
|
2229
|
+
let cursor;
|
|
2230
|
+
do {
|
|
2231
|
+
const current = await this.list({
|
|
2232
|
+
...query,
|
|
2233
|
+
cursor
|
|
2234
|
+
}, options);
|
|
2235
|
+
for (const workspace of current.items) yield workspace;
|
|
2236
|
+
cursor = current.nextCursor ?? void 0;
|
|
2237
|
+
} while (cursor);
|
|
2238
|
+
}
|
|
2239
|
+
get(id, options = {}) {
|
|
2240
|
+
return this.transport.request(`/v1/workspaces/${encodeURIComponent(id)}`, WorkspaceResourceSchema, options);
|
|
2241
|
+
}
|
|
2242
|
+
create(input, options = {}) {
|
|
2243
|
+
const body = {
|
|
2244
|
+
external_id: input.externalId,
|
|
2245
|
+
template: {
|
|
2246
|
+
name: input.templateName,
|
|
2247
|
+
...input.templateVersion ? { version: input.templateVersion } : {}
|
|
2248
|
+
},
|
|
2249
|
+
...input.launchInput ? { launch_input: input.launchInput } : {},
|
|
2250
|
+
...input.metadata ? { metadata: input.metadata } : {},
|
|
2251
|
+
...input.source ? { source: input.source } : {}
|
|
2252
|
+
};
|
|
2253
|
+
return this.transport.request("/v1/workspaces", WorkspaceResourceSchema, {
|
|
2254
|
+
method: "POST",
|
|
2255
|
+
signal: options.signal,
|
|
2256
|
+
headers: { "Idempotency-Key": input.idempotencyKey ?? input.externalId },
|
|
2257
|
+
body: JSON.stringify(body)
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
cancel(id, options = {}) {
|
|
2261
|
+
return this.transport.request(`/v1/workspaces/${encodeURIComponent(id)}/cancel`, WorkspaceResourceSchema, {
|
|
2262
|
+
method: "POST",
|
|
2263
|
+
signal: options.signal
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
change(id, after, wait, options = {}) {
|
|
2267
|
+
return this.transport.request(`/v1/workspaces/${encodeURIComponent(id)}/changes?after=${after}&wait=${wait}`, WorkspaceChangeSchema, options);
|
|
2268
|
+
}
|
|
2269
|
+
async waitForReady(initial, timeoutMs, options = {}) {
|
|
2270
|
+
const deadline = Date.now() + timeoutMs;
|
|
2271
|
+
let workspace = initial;
|
|
2272
|
+
while (workspace.state !== "ready") {
|
|
2273
|
+
if (TERMINAL_STATES.includes(workspace.state)) throw new WorkspaceTerminalError(workspace);
|
|
2274
|
+
const remainingMs = deadline - Date.now();
|
|
2275
|
+
if (remainingMs <= 0) throw new Error(`workspace ${workspace.id} did not become ready within ${timeoutMs}ms`);
|
|
2276
|
+
workspace = (await this.change(workspace.id, workspace.change_cursor, Math.max(1, Math.min(30, Math.ceil(remainingMs / 1e3))), options)).workspace;
|
|
2277
|
+
options.onTick?.(workspace);
|
|
2278
|
+
}
|
|
2279
|
+
return workspace;
|
|
2280
|
+
}
|
|
2281
|
+
preserve(id, input, key, options = {}) {
|
|
2282
|
+
return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/preserve`, input, key, PreserveResponseSchema, options);
|
|
2283
|
+
}
|
|
2284
|
+
recreate(id, input, key, options = {}) {
|
|
2285
|
+
return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/recreate`, input, key, RestoreResponseSchema, options);
|
|
2286
|
+
}
|
|
2287
|
+
resume(id, input, key, options = {}) {
|
|
2288
|
+
return this.jsonOperation(`/v1/workspaces/${encodeURIComponent(id)}/resume`, input, key, ResumeResponseSchema, options);
|
|
2289
|
+
}
|
|
2290
|
+
jsonOperation(path, input, key, schema, options) {
|
|
2291
|
+
return this.transport.request(path, schema, {
|
|
2292
|
+
method: "POST",
|
|
2293
|
+
signal: options.signal,
|
|
2294
|
+
headers: { "Idempotency-Key": key },
|
|
2295
|
+
body: JSON.stringify(input)
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
//#endregion
|
|
2300
|
+
//#region src/client.ts
|
|
2301
|
+
var PocketCoderClient = class {
|
|
2302
|
+
transport;
|
|
2303
|
+
templates;
|
|
2304
|
+
workspaces;
|
|
2305
|
+
attachments;
|
|
2306
|
+
agent;
|
|
2307
|
+
conversations;
|
|
2308
|
+
checkpoints;
|
|
2309
|
+
operations;
|
|
2310
|
+
logs;
|
|
2311
|
+
networkEvents;
|
|
2312
|
+
outputs;
|
|
2313
|
+
administration;
|
|
2314
|
+
terminals;
|
|
2315
|
+
constructor(config, fetchImpl = fetch) {
|
|
2316
|
+
this.transport = new PocketCoderTransport(config, fetchImpl);
|
|
2317
|
+
this.templates = new TemplatesApi(this.transport);
|
|
2318
|
+
this.workspaces = new WorkspacesApi(this.transport);
|
|
2319
|
+
this.attachments = new AttachmentsApi(this.transport);
|
|
2320
|
+
this.agent = new AgentApi(this.transport);
|
|
2321
|
+
this.conversations = new ConversationsApi(this.transport);
|
|
2322
|
+
this.checkpoints = new CheckpointsApi(this.transport);
|
|
2323
|
+
this.operations = new OperationsApi(this.transport);
|
|
2324
|
+
this.logs = new LogsApi(this.transport);
|
|
2325
|
+
this.networkEvents = new NetworkEventsApi(this.transport);
|
|
2326
|
+
this.outputs = new OutputsApi(this.transport);
|
|
2327
|
+
this.administration = new AdministrationApi(this.transport);
|
|
2328
|
+
this.terminals = new TerminalsApi(this.transport);
|
|
2329
|
+
}
|
|
2330
|
+
raw(path, init = {}) {
|
|
2331
|
+
return this.transport.raw(path, init);
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
//#endregion
|
|
2335
|
+
export { AdministrationApi, AgentApi, AttachmentsApi, CheckpointsApi, ConversationGoneError, ConversationsApi, LogsApi, NetworkEventsApi, OperationsApi, OutputsApi, PocketCoderClient, PocketCoderError, TERMINAL_WORKSPACE_STATES, TemplatesApi, TerminalConnection, TerminalsApi, WorkspaceTerminalError, WorkspacesApi, splitAttachmentManifest };
|