@alpic-ai/tracks 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/main.js +1074 -0
  2. package/package.json +34 -3
  3. package/README.md +0 -3
package/dist/main.js ADDED
@@ -0,0 +1,1074 @@
1
+ #!/usr/bin/env node
2
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
3
+ import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, OpenApi } from "effect/http-api";
4
+ import { Config, Console, Context, Deferred, Effect, Layer, Option, Redacted, Result, Schema, SchemaTransformation, Stream, Struct } from "effect";
5
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
6
+ import { isIPv4 } from "node:net";
7
+ import { Argument, Command, Flag } from "effect/cli";
8
+ import { createServer } from "node:http";
9
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
10
+ //#region ../contract/src/authorization.ts
11
+ const ORGANIZATION_HEADER = "x-organization";
12
+ Context.Service()("@tracks/contract/CurrentOrganization");
13
+ var Unauthorized = class extends Schema.TaggedError()("Unauthorized", { message: Schema.String }, {
14
+ httpApiStatus: 401,
15
+ description: `The Agent Runner key is missing or invalid, or no \`${ORGANIZATION_HEADER}\` header names the Organization.`
16
+ }) {};
17
+ var Authorization = class extends HttpApiMiddleware.Service()("@tracks/contract/Authorization", {
18
+ requiredForClient: true,
19
+ security: { bearer: HttpApiSecurity.http({ scheme: "bearer" }).pipe(HttpApiSecurity.annotate(OpenApi.Description, `The Agent Runner key, sent as \`Authorization: Bearer <key>\`, with the Organization the request is made for in the \`${ORGANIZATION_HEADER}\` header.`)) },
20
+ error: Unauthorized
21
+ }) {};
22
+ const OptionalQueryParam = (schema, description) => Schema.optionalKey(schema).annotateKey({ description }).pipe(Schema.decodeTo(Schema.Option(Schema.toType(schema)), SchemaTransformation.optionFromOptionalKey()));
23
+ const ListQuery = {
24
+ after: OptionalQueryParam(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), "Return only items after this position."),
25
+ limit: OptionalQueryParam(Schema.Int.check(Schema.isBetween({
26
+ minimum: 1,
27
+ maximum: 1e3
28
+ })), `Maximum number of items to return, from 1 to 1000. Defaults to 100.`)
29
+ };
30
+ const List = (item) => Schema.Struct({
31
+ data: Schema.Array(item),
32
+ has_more: Schema.Boolean.annotate({ description: "Whether more items are available after this page." })
33
+ });
34
+ //#endregion
35
+ //#region ../contract/src/validation.ts
36
+ const RequestIssue = Schema.Struct({
37
+ path: Schema.Array(Schema.Union([Schema.String, Schema.Number])).annotate({ description: "Keys leading to the wrong value inside that part of the request, such as `[\"agent\", \"model\"]`. Empty when the problem concerns that part as a whole." }),
38
+ message: Schema.String.annotate({ description: "What is wrong with the value." })
39
+ }).annotate({
40
+ identifier: "RequestIssue",
41
+ description: "One problem found in the request."
42
+ });
43
+ var InvalidRequest = class extends Schema.TaggedError()("InvalidRequest", {
44
+ message: Schema.String.annotate({ description: "Every problem found, in one sentence." }),
45
+ location: Schema.Literals([
46
+ "path",
47
+ "query",
48
+ "header",
49
+ "body"
50
+ ]).annotate({ description: "The part of the request that is wrong." }),
51
+ issues: Schema.Array(RequestIssue).annotate({ description: "Each problem found, with where it is." })
52
+ }, {
53
+ httpApiStatus: 400,
54
+ description: "The request is invalid: a value is missing, has the wrong type or is out of range. Fix it before sending the request again."
55
+ }) {};
56
+ var RequestValidation = class extends HttpApiMiddleware.Service()("@tracks/contract/RequestValidation", { error: InvalidRequest }) {};
57
+ //#endregion
58
+ //#region ../contract/src/connections.ts
59
+ const ConnectionId = Schema.String.pipe(Schema.brand("ConnectionId")).annotate({
60
+ identifier: "ConnectionId",
61
+ description: "Unique identifier of the connection."
62
+ });
63
+ const HttpsUrl = Schema.String.check(Schema.makeFilter((url) => URL.parse(url)?.protocol === "https:" || "must be an https URL"));
64
+ const CreateConnectionInput = Schema.Struct({
65
+ server_url: HttpsUrl.annotate({ description: "URL of the MCP server to connect to." }),
66
+ return_url: Schema.OptionFromOptionalKey(Schema.String.check(Schema.makeFilter((url) => URL.canParse(url) || "must be a URL")).annotate({ description: "Optional. Omit it when you cannot receive the user's browser, as an agent or a script: the user then sees a code such as `K7QM-4XPD` to give back to you. Otherwise, where the browser is sent back once they consented or refused, on an allowed origin or on `http://127.0.0.1`, `http://[::1]` or `http://localhost` at any port for a program running on the user's machine. It gets `connection_id`, then `confirmation` on success or `error` otherwise." }))
67
+ }).annotate({ identifier: "CreateConnectionInput" });
68
+ const ConfirmConnectionInput = Schema.Struct({ confirmation: Schema.NonEmptyString.annotate({ description: "The `confirmation` your return URL received, or the code the user gave you, proving the consent came back to you. Valid for 10 minutes after the consent." }) }).annotate({ identifier: "ConfirmConnectionInput" });
69
+ const connectionFields = {
70
+ id: ConnectionId,
71
+ server_url: Schema.String.annotate({ description: "URL of the MCP server." }),
72
+ created_at: Schema.DateTimeUtc.annotate({ description: "When the connection was created." })
73
+ };
74
+ const Connection = Schema.Union([Schema.Struct({
75
+ ...connectionFields,
76
+ status: Schema.Literals([
77
+ "authorizing",
78
+ "unconfirmed",
79
+ "active"
80
+ ]).annotate({ description: "`authorizing` until the user consents on the MCP server, `unconfirmed` until you confirm it, `active` once it can be used." })
81
+ }).annotate({
82
+ identifier: "WorkingConnection",
83
+ description: "A connection that has not failed."
84
+ }), Schema.Struct({
85
+ ...connectionFields,
86
+ status: Schema.Literal("failed"),
87
+ failure: Schema.String.annotate({ description: "Why the consent did not complete." })
88
+ }).annotate({
89
+ identifier: "FailedConnection",
90
+ description: "A connection whose consent did not complete."
91
+ })]).annotate({ description: "Access to an MCP server as one of its users, kept by Agent Runner." });
92
+ const CreatedConnection = Schema.Struct({
93
+ connection: Connection,
94
+ consent_url: Schema.String.annotate({ description: "Send the user's browser there to consent on the MCP server. Valid for 10 minutes, once. The browser then comes back to your `return_url`, or shows the confirmation code without one." })
95
+ }).annotate({ identifier: "CreatedConnection" });
96
+ var ConnectionNotFound = class extends Schema.TaggedError()("ConnectionNotFound", { message: Schema.String }, {
97
+ httpApiStatus: 404,
98
+ description: "No connection with this id exists for this Organization."
99
+ }) {};
100
+ var ConnectionRefused = class extends Schema.TaggedError()("ConnectionRefused", { message: Schema.String }, {
101
+ httpApiStatus: 422,
102
+ description: "The connection cannot be started: the return URL is not allowed, or the MCP server cannot be authorized in a way Agent Runner supports."
103
+ }) {};
104
+ var ServerUnreachable = class extends Schema.TaggedError()("ServerUnreachable", { message: Schema.String }, {
105
+ httpApiStatus: 502,
106
+ description: "The MCP server or its authorization server could not be reached."
107
+ }) {};
108
+ var ConfirmationRefused = class extends Schema.TaggedError()("ConfirmationRefused", { message: Schema.String }, {
109
+ httpApiStatus: 409,
110
+ description: "The connection is not waiting for this confirmation."
111
+ }) {};
112
+ var ConnectionsApi = class extends HttpApiGroup.make("connections").add(HttpApiEndpoint.post("create", "/connections", {
113
+ payload: CreateConnectionInput,
114
+ success: CreatedConnection.pipe(HttpApiSchema.status(201)),
115
+ error: [ConnectionRefused, ServerUnreachable]
116
+ }).annotateMerge(OpenApi.annotations({
117
+ summary: "Create a connection",
118
+ description: "Starts connecting to an MCP server that acts as one of its users. Send the user's browser to `consent_url`. Once they consent, they get a `confirmation`: as a code shown to them, to give back to you, or, if you passed the optional `return_url`, sent to it with `connection_id`. Confirm the connection with it within 10 minutes to make it `active`, then give its MCP server's URL to runs."
119
+ })), HttpApiEndpoint.get("list", "/connections", { success: List(Connection) }).annotateMerge(OpenApi.annotations({
120
+ summary: "List connections",
121
+ description: "Returns every connection."
122
+ })), HttpApiEndpoint.get("get", "/connections/:id", {
123
+ params: { id: ConnectionId },
124
+ success: Connection,
125
+ error: ConnectionNotFound
126
+ }).annotateMerge(OpenApi.annotations({
127
+ summary: "Get a connection",
128
+ description: "Returns a connection."
129
+ })), HttpApiEndpoint.post("confirm", "/connections/:id/confirm", {
130
+ params: { id: ConnectionId },
131
+ payload: ConfirmConnectionInput,
132
+ success: Connection,
133
+ error: [ConnectionNotFound, ConfirmationRefused]
134
+ }).annotateMerge(OpenApi.annotations({
135
+ summary: "Confirm a connection",
136
+ description: "Makes an `unconfirmed` connection `active`. Confirm it only when the browser that came back to your return URL belongs to the user who started the connection."
137
+ })), HttpApiEndpoint.delete("delete", "/connections/:id", {
138
+ params: { id: ConnectionId },
139
+ success: HttpApiSchema.NoContent,
140
+ error: ConnectionNotFound
141
+ }).annotateMerge(OpenApi.annotations({
142
+ summary: "Delete a connection",
143
+ description: "Deletes a connection and the access it holds."
144
+ }))).middleware(Authorization).middleware(RequestValidation).annotateMerge(OpenApi.annotations({
145
+ title: "Connections",
146
+ description: "Let agents use MCP servers as one of their users, without their credentials entering the sandbox."
147
+ })) {};
148
+ //#endregion
149
+ //#region ../contract/src/runs.ts
150
+ const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
151
+ identifier: "RunId",
152
+ description: "Unique identifier of the run."
153
+ });
154
+ const Harness = Schema.Literals([
155
+ "claude-code",
156
+ "codex",
157
+ "codex-app"
158
+ ]).annotate({
159
+ identifier: "Harness",
160
+ description: "The coding agent to run: `claude-code` (Claude Code), `codex` (OpenAI's Codex CLI) or `codex-app` (the Codex mode of OpenAI's ChatGPT desktop app, with its browser, computer-use and document tools)."
161
+ });
162
+ const Agent = Schema.Struct({
163
+ harness: Harness,
164
+ model: Schema.NonEmptyString.annotate({ description: "Model the agent uses, such as `claude-sonnet-5-5`." })
165
+ }).annotate({
166
+ identifier: "Agent",
167
+ description: "The coding agent and the model it uses."
168
+ });
169
+ const repository = Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "URL of a Git repository to clone before the agent starts." }));
170
+ const MAX_SCRIPT_LENGTH = 32768;
171
+ const script = (description) => Schema.OptionFromOptionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(MAX_SCRIPT_LENGTH)).annotate({ description: `${description} At most ${MAX_SCRIPT_LENGTH} characters.` }));
172
+ const setup = script("Shell script run with bash in the repository directory after checkout and before the first prompt, with the run's variables set, while the run is `preparing`. A non-zero exit fails the run. It may run again if Agent Runner restarts mid-way: make it idempotent.");
173
+ const teardown = script("Shell script run with bash in the repository directory after the run ends and before its sandbox is destroyed, with the run's variables set. Its exit code doesn't change how the run ended. It may run again if Agent Runner restarts mid-way: make it idempotent.");
174
+ const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
175
+ const RESERVED_VARIABLES = /* @__PURE__ */ new Set(["PATH", "HOME"]);
176
+ const RESERVED_VARIABLE_PREFIXES = [
177
+ "ANTHROPIC_",
178
+ "CLAUDE_",
179
+ "OPENAI_",
180
+ "CODEX_",
181
+ "AGENT_RUNNER_"
182
+ ];
183
+ const isReservedVariable = (name) => RESERVED_VARIABLES.has(name) || RESERVED_VARIABLE_PREFIXES.some((prefix) => name.startsWith(prefix));
184
+ const MAX_VARIABLES = 100;
185
+ const MAX_VARIABLE_VALUE_LENGTH = 32768;
186
+ const Variables$1 = Schema.Record(Schema.String, Schema.String.check(Schema.isMaxLength(MAX_VARIABLE_VALUE_LENGTH))).annotate({ description: `Environment variables set in every process of the agent, by name. Their values are never returned. Names match \`^[A-Za-z_][A-Za-z0-9_]*$\`; at most ${MAX_VARIABLES} variables of up to ${MAX_VARIABLE_VALUE_LENGTH} characters each. \`PATH\`, \`HOME\` and names starting with \`ANTHROPIC_\`, \`CLAUDE_\`, \`OPENAI_\`, \`CODEX_\` or \`AGENT_RUNNER_\` are reserved for running the agent.` }).check(Schema.isMaxProperties(MAX_VARIABLES), Schema.makeFilter((variables) => Object.keys(variables).flatMap((name) => {
187
+ if (!VARIABLE_NAME.test(name)) return [{
188
+ path: [name],
189
+ issue: `Expected a name matching ${VARIABLE_NAME.source}`
190
+ }];
191
+ if (isReservedVariable(name)) return [{
192
+ path: [name],
193
+ issue: "Expected a name not reserved for the agent"
194
+ }];
195
+ return [];
196
+ })));
197
+ const MCP_SERVER_NAME = /^[A-Za-z0-9_-]{1,64}$/;
198
+ const MAX_MCP_SERVERS = 20;
199
+ const RESERVED_MCP_SERVER_NAMES = /* @__PURE__ */ new Set(["chatgpt_plugins"]);
200
+ const McpServerName = Schema.String.check(Schema.isPattern(MCP_SERVER_NAME)).annotate({ description: "Name the agent knows the MCP server by, unique in the run, matching `^[A-Za-z0-9_-]{1,64}$`, such as `linear`. `chatgpt_plugins` is reserved for running the agent." });
201
+ const McpServerInput = Schema.Union([Schema.Struct({
202
+ name: McpServerName,
203
+ url: HttpsUrl.annotate({ description: "URL of the MCP server. When you have one `active` connection to this exact URL, the agent uses it; otherwise the server is used without one. Requests come from the addresses listed at `GET /egress-ips`." }),
204
+ connection_id: Schema.optionalKey(Schema.Never)
205
+ }).annotate({
206
+ identifier: "McpServerByUrlInput",
207
+ description: "An MCP server given by its URL."
208
+ }), Schema.Struct({
209
+ name: McpServerName,
210
+ connection_id: ConnectionId.annotate({ description: "An `active` connection: the agent uses its MCP server as the user who consented." }),
211
+ url: Schema.optionalKey(Schema.Never)
212
+ }).annotate({
213
+ identifier: "McpServerByConnectionInput",
214
+ description: "An MCP server given by a connection to it, to pick between several accounts."
215
+ })]).annotate({
216
+ identifier: "McpServerInput",
217
+ description: "An MCP server the agent can use, through Agent Runner: credentials never enter the sandbox. Set `url` or `connection_id`."
218
+ });
219
+ const McpServersInput = Schema.Array(McpServerInput).check(Schema.isMaxLength(MAX_MCP_SERVERS), Schema.makeFilter((servers) => {
220
+ const names = servers.map((server) => server.name);
221
+ return new Set(names).size === names.length || "MCP server names must be unique";
222
+ }), Schema.makeFilter((servers) => servers.flatMap((server, index) => RESERVED_MCP_SERVER_NAMES.has(server.name) ? [{
223
+ path: [index, "name"],
224
+ issue: "Expected a name not reserved for the agent"
225
+ }] : []))).annotate({ description: `MCP servers the agent can use, at most ${MAX_MCP_SERVERS}.` });
226
+ const EnvironmentInput = Schema.Struct({
227
+ repository,
228
+ variables: Schema.OptionFromOptionalKey(Variables$1),
229
+ setup,
230
+ teardown,
231
+ mcp_servers: Schema.OptionFromOptionalKey(McpServersInput)
232
+ }).annotate({
233
+ identifier: "EnvironmentInput",
234
+ description: "What the agent works on."
235
+ });
236
+ const RunMcpServer = Schema.Struct({
237
+ name: Schema.String.annotate({ description: "Name the agent knows the MCP server by." }),
238
+ url: Schema.String.annotate({ description: "URL of the MCP server." }),
239
+ connection_id: Schema.OptionFromOptionalKey(ConnectionId.annotate({ description: "The connection the agent uses it through, if any." }))
240
+ }).annotate({
241
+ identifier: "McpServer",
242
+ description: "An MCP server the agent can use."
243
+ });
244
+ const RunEnvironment = Schema.Struct({
245
+ repository,
246
+ variables: Schema.Array(Schema.String).annotate({ description: "Names of the environment variables set in every process of the agent. Their values are never returned." }),
247
+ setup,
248
+ teardown,
249
+ mcp_servers: Schema.Array(RunMcpServer)
250
+ }).annotate({
251
+ identifier: "Environment",
252
+ description: "What the agent works on."
253
+ });
254
+ const Budget = Schema.Finite.check(Schema.isBetween({
255
+ minimum: .01,
256
+ maximum: 1e4
257
+ })).annotate({ description: "Maximum the run may spend, in US dollars. Defaults to a standard budget when omitted. The run ends with `budget_exceeded` once it is reached." });
258
+ const MIN_IDLE_TIMEOUT_SECONDS = 120;
259
+ const MAX_IDLE_TIMEOUT_SECONDS = 900;
260
+ const MIN_TIME_LIMIT_SECONDS = 60;
261
+ const MAX_TIME_LIMIT_SECONDS = 43200;
262
+ const CreateRunInput = Schema.Struct({
263
+ agent: Agent,
264
+ prompt: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do first." }),
265
+ environment: EnvironmentInput,
266
+ budget_usd: Schema.OptionFromOptionalKey(Budget),
267
+ one_shot: Schema.OptionFromOptionalKey(Schema.Boolean.annotate({ description: "Whether the run ends as soon as its first prompt finishes. Defaults to `true`. Set it to `false` to send follow-up prompts while the run is `idle`." })),
268
+ idle_timeout_seconds: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
269
+ minimum: MIN_IDLE_TIMEOUT_SECONDS,
270
+ maximum: MAX_IDLE_TIMEOUT_SECONDS
271
+ })).annotate({ description: `How long the run waits for a follow-up prompt before it ends with \`idle_timeout\`, in seconds, from ${MIN_IDLE_TIMEOUT_SECONDS} to ${MAX_IDLE_TIMEOUT_SECONDS}. Defaults to ${MAX_IDLE_TIMEOUT_SECONDS}. Only allowed when \`one_shot\` is \`false\`.` })),
272
+ time_limit_seconds: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
273
+ minimum: MIN_TIME_LIMIT_SECONDS,
274
+ maximum: MAX_TIME_LIMIT_SECONDS
275
+ })).annotate({ description: `Longest the run may last, in seconds from when it starts, from ${MIN_TIME_LIMIT_SECONDS} to ${MAX_TIME_LIMIT_SECONDS}. Defaults to 7200. Once reached, the run ends with \`time_limit\`, even in the middle of a prompt.` }))
276
+ }).check(Schema.makeFilter((input) => Option.contains(input.one_shot, false) || Option.isNone(input.idle_timeout_seconds) || "idle_timeout_seconds is only allowed when one_shot is false")).annotate({ identifier: "CreateRunInput" });
277
+ const SubmitPromptInput = Schema.Struct({
278
+ text: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do next." }),
279
+ model: Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "Model for this prompt only. Defaults to the run's model." }))
280
+ }).annotate({ identifier: "SubmitPromptInput" });
281
+ const PromptIndex = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "Position of the prompt in its run, starting at 1." });
282
+ const promptFields = {
283
+ run_id: RunId,
284
+ index: PromptIndex,
285
+ text: Schema.String.annotate({ description: "What you asked the agent to do." }),
286
+ model: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "Model for this prompt, when not the run's." })),
287
+ submitted_at: Schema.DateTimeUtc.annotate({ description: "When the prompt was submitted." })
288
+ };
289
+ const Prompt = Schema.Union([Schema.Struct({
290
+ ...promptFields,
291
+ status: Schema.Literals([
292
+ "working",
293
+ "failed",
294
+ "canceled"
295
+ ]).annotate({ description: "`working` while the agent works on it. `failed` or `canceled` when the run ended before the prompt finished." })
296
+ }).annotate({
297
+ identifier: "UnfinishedPrompt",
298
+ description: "A prompt the agent has not finished."
299
+ }), Schema.Struct({
300
+ ...promptFields,
301
+ status: Schema.Literal("finished").annotate({ description: "The agent stopped, whatever its exit code." }),
302
+ finished_at: Schema.DateTimeUtc.annotate({ description: "When the agent stopped." }),
303
+ exit_code: Schema.Int.annotate({ description: "Exit code of the agent." })
304
+ }).annotate({
305
+ identifier: "FinishedPrompt",
306
+ description: "A prompt the agent has finished."
307
+ })]).annotate({ description: "One instruction to the agent and the work it did in response." });
308
+ const EndReason = Schema.Literals([
309
+ "completed",
310
+ "idle_timeout",
311
+ "time_limit",
312
+ "budget_exceeded",
313
+ "failed",
314
+ "canceled"
315
+ ]).annotate({
316
+ identifier: "EndReason",
317
+ description: "Why the run ended. When `failed`, `failure` explains what went wrong."
318
+ });
319
+ const FailureKind = Schema.Literals([
320
+ "repository_checkout_failed",
321
+ "setup_failed",
322
+ "sandbox_not_found",
323
+ "process_lost",
324
+ "sandbox_unavailable",
325
+ "unexpected"
326
+ ]).annotate({
327
+ identifier: "FailureKind",
328
+ description: "What kind of thing went wrong: the repository could not be checked out, the environment's setup command failed, the sandbox disappeared, the agent's process was lost, the sandbox provider failed, or something unexpected."
329
+ });
330
+ const runFields = {
331
+ id: RunId,
332
+ agent: Agent,
333
+ environment: RunEnvironment,
334
+ budget_usd: Schema.Number.annotate({ description: "Maximum the run may spend, in US dollars." }),
335
+ one_shot: Schema.Boolean.annotate({ description: "Whether the run ends as soon as its first prompt finishes." }),
336
+ idle_timeout_seconds: Schema.OptionFromOptionalKey(Schema.Int.annotate({ description: "How long the run waits for a follow-up prompt before it ends, in seconds. Absent for a one-shot run." })),
337
+ time_limit_seconds: Schema.Int.annotate({ description: "Longest the run may last, in seconds from when it starts." }),
338
+ started_at: Schema.DateTimeUtc.annotate({ description: "When the run started." }),
339
+ last_prompt: Prompt
340
+ };
341
+ const endedRunFields = {
342
+ ...runFields,
343
+ status: Schema.Literal("ended"),
344
+ ended_at: Schema.DateTimeUtc.annotate({ description: "When the run ended." })
345
+ };
346
+ const Run = Schema.Union([
347
+ Schema.Struct({
348
+ ...runFields,
349
+ status: Schema.Literals([
350
+ "preparing",
351
+ "working",
352
+ "idle"
353
+ ]).annotate({ description: "`preparing` while its sandbox gets ready, `working` while the agent works on a prompt, `idle` while it waits for a follow-up prompt." })
354
+ }).annotate({
355
+ identifier: "ActiveRun",
356
+ description: "A run that has not ended."
357
+ }),
358
+ Schema.Struct({
359
+ ...endedRunFields,
360
+ end_reason: EndReason.pick([
361
+ "completed",
362
+ "idle_timeout",
363
+ "time_limit",
364
+ "budget_exceeded",
365
+ "canceled"
366
+ ])
367
+ }).annotate({
368
+ identifier: "EndedRun",
369
+ description: "A run that ended without failing."
370
+ }),
371
+ Schema.Struct({
372
+ ...endedRunFields,
373
+ end_reason: Schema.Literal("failed"),
374
+ failure: Schema.String.annotate({ description: "What went wrong." }),
375
+ failure_kind: FailureKind
376
+ }).annotate({
377
+ identifier: "FailedRun",
378
+ description: "A run that ended because something went wrong."
379
+ })
380
+ ]).annotate({ description: "A coding agent working on your prompt in an isolated sandbox." });
381
+ var RunNotFound = class extends Schema.TaggedError()("RunNotFound", { message: Schema.String }, {
382
+ httpApiStatus: 404,
383
+ description: "No run with this id exists for this Organization."
384
+ }) {};
385
+ var ConnectionUnusable = class extends Schema.TaggedError()("ConnectionUnusable", { message: Schema.String }, {
386
+ httpApiStatus: 422,
387
+ description: "A `connection_id` in `environment.mcp_servers` does not exist or is not `active`, or a `url` matches several `active` connections: pick one with `connection_id`."
388
+ }) {};
389
+ var ModelNotOffered = class extends Schema.TaggedError()("ModelNotOffered", { message: Schema.String }, {
390
+ httpApiStatus: 422,
391
+ description: "The agent cannot use this model. The message lists the models it can use."
392
+ }) {};
393
+ var RunNotIdle = class extends Schema.TaggedError()("RunNotIdle", { message: Schema.String }, {
394
+ httpApiStatus: 409,
395
+ description: "The run accepts no prompt now: it is one-shot, has ended, or is still working on a prompt."
396
+ }) {};
397
+ const Count = Schema.Natural;
398
+ const RunUsage = Schema.Struct({
399
+ cost_usd: Schema.Number.annotate({ description: "Total cost of the run so far, in US dollars." }),
400
+ models: Schema.Array(Schema.Struct({
401
+ provider: Schema.String.annotate({ description: "Company serving the model, such as `anthropic`." }),
402
+ model: Schema.String,
403
+ quantities: Schema.Record(Schema.String, Count).annotate({ description: "Units consumed, keyed by unit as the provider counts them, such as input tokens or web searches." }),
404
+ cost_usd: Schema.Number.annotate({ description: "Cost of this model's usage, in US dollars." })
405
+ })).annotate({ description: "Model usage, one entry per model." }),
406
+ sandbox: Schema.Struct({
407
+ seconds: Schema.Number.annotate({ description: "Time the sandbox ran, in seconds." }),
408
+ cost_usd: Schema.Number.annotate({ description: "Cost of the sandbox time, in US dollars." })
409
+ })
410
+ }).annotate({
411
+ identifier: "RunUsage",
412
+ description: "What a run consumed and what it cost."
413
+ });
414
+ const Frame = Schema.Struct({
415
+ run_id: RunId,
416
+ seq: Schema.Int.annotate({ description: "Position of the line in the run's output, starting at 1. Always increasing, but some numbers may be skipped." }),
417
+ prompt: PromptIndex.annotate({ description: "Index of the prompt the agent was working on." }),
418
+ captured_at: Schema.DateTimeUtc.annotate({ description: "When the line was captured." }),
419
+ channel: Schema.Literals(["stdout", "stderr"]).annotate({ description: "Output stream the line came from." }),
420
+ body: Schema.String.annotate({ description: "The line, exactly as the agent printed it." })
421
+ }).annotate({
422
+ identifier: "Frame",
423
+ description: "One line of the agent's output."
424
+ });
425
+ const Outcome = Schema.Literals(["succeeded", "failed"]).annotate({
426
+ identifier: "Outcome",
427
+ description: "Whether the tool call or subagent succeeded."
428
+ });
429
+ const ToolCallId = Schema.String.annotate({ description: "Identifier of the tool call, unique in its run." });
430
+ const eventFields = {
431
+ run_id: RunId,
432
+ seq: Schema.Int.annotate({ description: "Position of the event in the run, starting at 1. Always increasing, but some numbers may be skipped." }),
433
+ prompt: PromptIndex.annotate({ description: "Index of the prompt the event belongs to." }),
434
+ captured_at: Schema.DateTimeUtc.annotate({ description: "When the output the event comes from was captured." }),
435
+ subagent_id: Schema.OptionFromOptionalKey(ToolCallId.annotate({ description: "The subagent that did it: the `tool_call_id` of the `tool_started` event that launched it. Absent when the run's agent did it." })),
436
+ frame_seqs: Schema.Array(Schema.Int).annotate({ description: "The `seq` of each output line the event was read from. Empty for events Agent Runner records itself." })
437
+ };
438
+ const Event = Schema.Union([
439
+ Schema.Struct({
440
+ ...eventFields,
441
+ type: Schema.Literal("prompt"),
442
+ text: Schema.String.annotate({ description: "What you asked the agent to do." })
443
+ }).annotate({
444
+ identifier: "PromptEvent",
445
+ description: "A prompt given to the agent; the first event of its prompt."
446
+ }),
447
+ Schema.Struct({
448
+ ...eventFields,
449
+ type: Schema.Literal("message"),
450
+ text: Schema.String.annotate({ description: "What the agent wrote." })
451
+ }).annotate({
452
+ identifier: "MessageEvent",
453
+ description: "Text the agent wrote, apart from reasoning and tool calls."
454
+ }),
455
+ Schema.Struct({
456
+ ...eventFields,
457
+ type: Schema.Literal("reasoning"),
458
+ text: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "The reasoning, or a summary of it. Absent when the harness hides it." }))
459
+ }).annotate({
460
+ identifier: "ReasoningEvent",
461
+ description: "The agent reasoning before it acts."
462
+ }),
463
+ Schema.Struct({
464
+ ...eventFields,
465
+ type: Schema.Literal("tool_started"),
466
+ tool_call_id: ToolCallId,
467
+ tool_name: Schema.String.annotate({ description: "Name of the tool, as the harness calls it." }),
468
+ input: Schema.Json.annotate({ description: "What the agent passed to the tool." })
469
+ }).annotate({
470
+ identifier: "ToolStartedEvent",
471
+ description: "The agent called a tool."
472
+ }),
473
+ Schema.Struct({
474
+ ...eventFields,
475
+ type: Schema.Literal("tool_ended"),
476
+ tool_call_id: ToolCallId,
477
+ outcome: Outcome,
478
+ output: Schema.String.annotate({ description: "What the tool returned, possibly truncated." }),
479
+ structured: Schema.OptionFromOptionalKey(Schema.Json.annotate({ description: "The structured content an MCP tool returned beside its text, unchanged. Absent when the tool returned none or it was too large." }))
480
+ }).annotate({
481
+ identifier: "ToolEndedEvent",
482
+ description: "A tool call ended; its `tool_call_id` is the one of the `tool_started` event."
483
+ }),
484
+ Schema.Struct({
485
+ ...eventFields,
486
+ type: Schema.Literal("subagent_started"),
487
+ description: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "What the agent asked the subagent to do, in short." }))
488
+ }).annotate({
489
+ identifier: "SubagentStartedEvent",
490
+ description: "A subagent started; its `subagent_id` is the `tool_call_id` of the `tool_started` event that launched it. That tool call may end before the subagent does: the subagent is done at its `subagent_ended`."
491
+ }),
492
+ Schema.Struct({
493
+ ...eventFields,
494
+ type: Schema.Literal("subagent_ended"),
495
+ outcome: Outcome
496
+ }).annotate({
497
+ identifier: "SubagentEndedEvent",
498
+ description: "A subagent ended."
499
+ }),
500
+ Schema.Struct({
501
+ ...eventFields,
502
+ type: Schema.Literal("stop"),
503
+ stop_reason: Schema.Literals([
504
+ "completed",
505
+ "failed",
506
+ "canceled"
507
+ ]).annotate({ description: "Why the agent stopped. When `failed`, `error` may explain what went wrong." }),
508
+ error: Schema.OptionFromOptionalKey(Schema.String.annotate({ description: "What went wrong." }))
509
+ }).annotate({
510
+ identifier: "StopEvent",
511
+ description: "The agent stopped working on the prompt; the last event of its prompt. It ends everything still open in the prompt: any tool call or subagent without its own end event ended with it."
512
+ })
513
+ ]).annotate({ description: "One thing the agent did, read from its output so that runs of every harness read alike." });
514
+ const FrameStreamItem = Schema.Union([Schema.Struct({
515
+ id: Schema.optional(Schema.String),
516
+ event: Schema.Literal("frame"),
517
+ data: Schema.fromJsonString(Schema.toCodecJson(Frame))
518
+ }), Schema.Struct({
519
+ id: Schema.optional(Schema.String),
520
+ event: Schema.Literal("heartbeat"),
521
+ data: Schema.Literal("")
522
+ })]);
523
+ const EventStreamItem = Schema.Union([Schema.Struct({
524
+ id: Schema.optional(Schema.String),
525
+ event: Schema.Literal("event"),
526
+ data: Schema.fromJsonString(Schema.toCodecJson(Event))
527
+ }), Schema.Struct({
528
+ id: Schema.optional(Schema.String),
529
+ event: Schema.Literal("heartbeat"),
530
+ data: Schema.Literal("")
531
+ })]);
532
+ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("create", "/runs", {
533
+ headers: { "idempotency-key": Schema.OptionFromOptionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(255)).annotate({ description: "A unique value you choose, up to 255 characters, to retry safely: a request with a key already used in your organization returns the run the first request created, without starting another, whatever its body." })) },
534
+ payload: CreateRunInput,
535
+ success: Run.pipe(HttpApiSchema.status(201)),
536
+ error: [ModelNotOffered, ConnectionUnusable]
537
+ }).annotateMerge(OpenApi.annotations({
538
+ summary: "Create a run",
539
+ description: "Starts a coding agent on your prompt in a fresh sandbox. A one-shot run, the default, ends when the agent finishes; otherwise the run waits `idle` for follow-up prompts until you end it or it times out. Send an `Idempotency-Key` header to retry without creating a second run."
540
+ })), HttpApiEndpoint.get("get", "/runs/:id", {
541
+ params: { id: RunId },
542
+ success: Run,
543
+ error: RunNotFound
544
+ }).annotateMerge(OpenApi.annotations({
545
+ summary: "Get a run",
546
+ description: "Returns a run and its current status."
547
+ })), HttpApiEndpoint.get("getUsage", "/runs/:id/usage", {
548
+ params: { id: RunId },
549
+ success: RunUsage,
550
+ error: RunNotFound
551
+ }).annotateMerge(OpenApi.annotations({
552
+ summary: "Get run usage",
553
+ description: "Returns what the run has consumed so far, per model and in sandbox time, and what it cost."
554
+ })), HttpApiEndpoint.post("end", "/runs/:id/end", {
555
+ params: { id: RunId },
556
+ success: Run.pipe(HttpApiSchema.status(202)),
557
+ error: RunNotFound
558
+ }).annotateMerge(OpenApi.annotations({
559
+ summary: "End a run",
560
+ description: "Ends the run with `completed` once its current prompt finishes, then destroys its sandbox. Ending a run that has ended does nothing."
561
+ })), HttpApiEndpoint.post("cancel", "/runs/:id/cancel", {
562
+ params: { id: RunId },
563
+ success: Run.pipe(HttpApiSchema.status(202)),
564
+ error: RunNotFound
565
+ }).annotateMerge(OpenApi.annotations({
566
+ summary: "Cancel a run",
567
+ description: "Stops the agent right away, even mid-prompt, ends the run with `canceled`, then destroys its sandbox. Canceling a run that has ended does nothing."
568
+ })), HttpApiEndpoint.post("submitPrompt", "/runs/:id/prompts", {
569
+ params: { id: RunId },
570
+ payload: SubmitPromptInput,
571
+ success: Prompt.pipe(HttpApiSchema.status(202)),
572
+ error: [
573
+ RunNotFound,
574
+ RunNotIdle,
575
+ ModelNotOffered
576
+ ]
577
+ }).annotateMerge(OpenApi.annotations({
578
+ summary: "Send a follow-up prompt",
579
+ description: "Asks the agent to continue where its last prompt stopped. Accepted only while the run is `idle`; the run is `working` again until the agent stops."
580
+ })), HttpApiEndpoint.get("listPrompts", "/runs/:id/prompts", {
581
+ params: { id: RunId },
582
+ query: ListQuery,
583
+ success: List(Prompt),
584
+ error: RunNotFound
585
+ }).annotateMerge(OpenApi.annotations({
586
+ summary: "List run prompts",
587
+ description: "Returns the run's prompts in order. To get the next page, pass the `index` of the last prompt received as `after`."
588
+ })), HttpApiEndpoint.get("listFrames", "/runs/:id/frames", {
589
+ params: { id: RunId },
590
+ query: {
591
+ ...ListQuery,
592
+ prompt: OptionalQueryParam(PromptIndex, "Return only this prompt's output.")
593
+ },
594
+ success: List(Frame),
595
+ error: RunNotFound
596
+ }).annotateMerge(OpenApi.annotations({
597
+ summary: "List run output",
598
+ description: "Returns the agent's output, line by line, in order. To get the next page, pass the `seq` of the last line received as `after`."
599
+ })), HttpApiEndpoint.get("listEvents", "/runs/:id/events", {
600
+ params: { id: RunId },
601
+ query: {
602
+ ...ListQuery,
603
+ prompt: OptionalQueryParam(PromptIndex, "Return only this prompt's events.")
604
+ },
605
+ success: List(Event),
606
+ error: RunNotFound
607
+ }).annotateMerge(OpenApi.annotations({
608
+ summary: "List run events",
609
+ description: "Returns what the agent did, in order: each prompt, then its messages, reasoning, tool calls and subagents, then a `stop` once the agent stopped. Events read alike whatever the harness, and are read from the run's output: `frame_seqs` points at the lines each one comes from. To get the next page, pass the `seq` of the last event received as `after`."
610
+ })), HttpApiEndpoint.get("followFrames", "/runs/:id/frames/stream", {
611
+ params: { id: RunId },
612
+ query: { after: OptionalQueryParam(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), "Send only lines whose `seq` is greater than this.") },
613
+ success: HttpApiSchema.StreamSse({ events: FrameStreamItem }),
614
+ error: RunNotFound
615
+ }).annotateMerge(OpenApi.annotations({
616
+ summary: "Stream run output",
617
+ description: "Sends the agent's output as server-sent events as it is produced: a `frame` event per line, its data the line as JSON, and a `heartbeat` event every 30 seconds to keep the connection open. Closes once the run has ended and every line was sent. The connection may also close earlier; to pick up where it stopped, call again with the `seq` of the last line received as `after`, or let an SSE client reconnect: the `Last-Event-ID` header it sends is used when `after` is absent."
618
+ })), HttpApiEndpoint.get("followEvents", "/runs/:id/events/stream", {
619
+ params: { id: RunId },
620
+ query: { after: OptionalQueryParam(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), "Send only events whose `seq` is greater than this.") },
621
+ success: HttpApiSchema.StreamSse({ events: EventStreamItem }),
622
+ error: RunNotFound
623
+ }).annotateMerge(OpenApi.annotations({
624
+ summary: "Stream run events",
625
+ description: "Sends what the agent did as server-sent events as it happens: an `event` event per event, its data the event as JSON, and a `heartbeat` event every 30 seconds to keep the connection open. Closes once the run has ended and every event was sent. The connection may also close earlier; to pick up where it stopped, call again with the `seq` of the last event received as `after`, or let an SSE client reconnect: the `Last-Event-ID` header it sends is used when `after` is absent."
626
+ }))).middleware(Authorization).middleware(RequestValidation).annotateMerge(OpenApi.annotations({
627
+ title: "Runs",
628
+ description: "Start coding agents and follow their work."
629
+ })) {};
630
+ //#endregion
631
+ //#region ../contract/src/system.ts
632
+ const EgressIps = Schema.Struct({ ips: Schema.Array(Schema.String).annotate({ description: "IPv4 addresses, empty when the API runs locally." }) }).annotate({ identifier: "EgressIps" });
633
+ var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).add(HttpApiEndpoint.get("health", "/health", { success: HttpApiSchema.NoContent }).annotateMerge(OpenApi.annotations({
634
+ summary: "Check health",
635
+ description: "Returns 204 when the API is up. No Agent Runner key required."
636
+ }))).add(HttpApiEndpoint.get("egressIps", "/egress-ips", { success: EgressIps }).annotateMerge(OpenApi.annotations({
637
+ summary: "List egress IPs",
638
+ description: "The fixed addresses Agent Runner calls MCP servers and their authorization servers from. Allow them on an MCP server that only accepts known addresses. Commands the agent runs in its sandbox, such as cloning the repository, come from other addresses. No Agent Runner key required."
639
+ }))).annotateMerge(OpenApi.annotations({ title: "System" })) {};
640
+ //#endregion
641
+ //#region ../contract/src/api.ts
642
+ var Api = class extends HttpApi.make("agent-runner").add(RunsApi).add(ConnectionsApi).prefix("/v1").add(SystemApi).annotate(HttpApi.ParseOptions, { errors: "all" }).annotateMerge(OpenApi.annotations({
643
+ title: "Agent Runner API",
644
+ description: "Run coding agents such as Claude Code in isolated sandboxes. Create a run with a prompt, then follow its output and what it costs."
645
+ })) {};
646
+ //#endregion
647
+ //#region ../contract/src/client.ts
648
+ const CLI_USER_AGENT = "tracks";
649
+ var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiClient") {
650
+ static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(Api, { transformClient: HttpClient.mapRequest((request) => {
651
+ const located = HttpClientRequest.prependUrl(request, options.baseUrl.href);
652
+ return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
653
+ }) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(Authorization, ({ next, request }) => next(HttpClientRequest.bearerToken(request, Redacted.value(options.agentRunnerKey)).pipe(HttpClientRequest.setHeader(ORGANIZATION_HEADER, options.organization))))));
654
+ };
655
+ //#endregion
656
+ //#region ../env/src/env.ts
657
+ const Secret = Schema.Redacted(Schema.NonEmptyString);
658
+ const AgentRunnerKey = Schema.Redacted(Schema.NonEmptyString.check(Schema.isMinLength(32)));
659
+ const Port = Schema.Int.check(Schema.isBetween({
660
+ minimum: 1,
661
+ maximum: 65535
662
+ }));
663
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
664
+ "localhost",
665
+ "127.0.0.1",
666
+ "[::1]"
667
+ ]);
668
+ const AGENT_RUNNER_PATH = "/agent-runner";
669
+ const SecureUrl = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" || LOOPBACK_HOSTS.has(url.hostname) || "must use https"));
670
+ const ENCRYPTION_KEY_BYTES = 32;
671
+ const EncryptionKey = Schema.Redacted(Schema.NonEmptyString.check(Schema.makeFilter((key) => Buffer.from(key, "base64").length === ENCRYPTION_KEY_BYTES || `must be ${ENCRYPTION_KEY_BYTES} random bytes in base64: openssl rand -base64 ${ENCRYPTION_KEY_BYTES}`)));
672
+ const Origin = Schema.String.check(Schema.makeFilter((origin) => URL.parse(origin)?.origin === origin || `${origin} must be an origin, such as https://app.example.com`));
673
+ const Origins = Schema.String.pipe(Schema.decodeTo(Schema.Array(Origin), SchemaTransformation.transform({
674
+ decode: (list) => list.split(",").map((origin) => origin.trim()).filter((origin) => origin !== ""),
675
+ encode: (origins) => origins.join(",")
676
+ })));
677
+ const IpAddresses = Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.String.check(Schema.makeFilter((address) => isIPv4(address) || `${address} must be an IPv4 address`))), SchemaTransformation.transform({
678
+ decode: (list) => list.split(",").map((address) => address.trim()).filter((address) => address !== ""),
679
+ encode: (addresses) => addresses.join(",")
680
+ })));
681
+ const SentryDsn = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" && url.username !== "" || "must be a Sentry DSN"));
682
+ const Secrets = Schema.Struct({
683
+ AGENT_RUNNER_KEY: AgentRunnerKey,
684
+ DAYTONA_API_KEY: Secret,
685
+ E2B_API_KEY: Secret,
686
+ ANTHROPIC_API_KEY: Secret,
687
+ OPENAI_API_KEY: Secret,
688
+ TYPESAFE_API_KEY: Secret,
689
+ DATABASE_PASSWORD: Secret,
690
+ CONNECTION_ENCRYPTION_KEY: EncryptionKey,
691
+ SENTRY_DSN: Schema.optionalKey(Schema.Redacted(SentryDsn))
692
+ });
693
+ const Variables = Schema.Struct({
694
+ PORT: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(3e3))),
695
+ AGENT_RUNNER_ORGANIZATION: Schema.NonEmptyString,
696
+ AGENT_RUNNER_API_URL: Schema.URLFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed(`https://agent-runner.lab.alpic.ai${AGENT_RUNNER_PATH}`))),
697
+ DAYTONA_API_URL: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://app.daytona.io/api"))),
698
+ AGENT_RUNNER_URL: SecureUrl,
699
+ CONNECTION_RETURN_ORIGINS: Origins.pipe(Schema.withDecodingDefaultKey(Effect.succeed(""))),
700
+ EGRESS_IPS: IpAddresses.pipe(Schema.withDecodingDefaultKey(Effect.succeed(""))),
701
+ DATABASE_HOST: Schema.NonEmptyString,
702
+ DATABASE_PORT: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(5432))),
703
+ DATABASE_NAME: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("app"))),
704
+ DATABASE_USER: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("app"))),
705
+ DATABASE_SSL: Schema.Boolean,
706
+ LOG_FORMAT: Schema.Literals(["pretty", "json"]).pipe(Schema.withDecodingDefaultKey(Effect.succeed("pretty"))),
707
+ LOG_LEVEL: Schema.Literals([
708
+ "Trace",
709
+ "Debug",
710
+ "Info",
711
+ "Warn",
712
+ "Error",
713
+ "Fatal"
714
+ ]).pipe(Schema.withDecodingDefaultKey(Effect.succeed("Info"))),
715
+ DEPLOYMENT_ENVIRONMENT: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("local"))),
716
+ OTLP_METRICS_URL: Schema.optionalKey(Schema.URLFromString)
717
+ });
718
+ const Env = Schema.Struct({
719
+ ...Secrets.fields,
720
+ ...Variables.fields
721
+ });
722
+ const isSecret = (name) => Object.hasOwn(Secrets.fields, name);
723
+ Struct.keys(Env.fields);
724
+ const DATABASE_ENV = [
725
+ "DATABASE_HOST",
726
+ "DATABASE_PORT",
727
+ "DATABASE_NAME",
728
+ "DATABASE_USER",
729
+ "DATABASE_PASSWORD",
730
+ "DATABASE_SSL"
731
+ ];
732
+ const DAYTONA_ENV = ["DAYTONA_API_KEY", "DAYTONA_API_URL"];
733
+ const E2B_ENV = ["E2B_API_KEY"];
734
+ const GATEWAY_ENV = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
735
+ const CONNECTIONS_ENV = [
736
+ "AGENT_RUNNER_URL",
737
+ "CONNECTION_RETURN_ORIGINS",
738
+ "CONNECTION_ENCRYPTION_KEY"
739
+ ];
740
+ const AGENT_RUNNER_ENV = [
741
+ ...DATABASE_ENV,
742
+ ...DAYTONA_ENV,
743
+ ...E2B_ENV,
744
+ ...GATEWAY_ENV,
745
+ ...CONNECTIONS_ENV
746
+ ];
747
+ const LOG_ENV = ["LOG_FORMAT", "LOG_LEVEL"];
748
+ const OBSERVABILITY_ENV = [
749
+ "SENTRY_DSN",
750
+ "DEPLOYMENT_ENVIRONMENT",
751
+ "OTLP_METRICS_URL"
752
+ ];
753
+ const SERVICE_ENV = [
754
+ "AGENT_RUNNER_KEY",
755
+ "PORT",
756
+ "EGRESS_IPS",
757
+ ...LOG_ENV,
758
+ ...OBSERVABILITY_ENV,
759
+ ...AGENT_RUNNER_ENV
760
+ ];
761
+ const CLI_ENV = [
762
+ "AGENT_RUNNER_KEY",
763
+ "AGENT_RUNNER_ORGANIZATION",
764
+ "AGENT_RUNNER_API_URL"
765
+ ];
766
+ const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
767
+ SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
768
+ const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
769
+ //#endregion
770
+ //#region package.json
771
+ var version = "0.4.0";
772
+ //#endregion
773
+ //#region src/connections.ts
774
+ const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
775
+ const encodeConnections = Schema.encodeEffect(Schema.fromJsonString(List(Connection), { space: 2 }));
776
+ const connectionId = Argument.String("id").pipe(Argument.withDescription("Connection id"), Argument.withSchema(ConnectionId));
777
+ const LOOPBACK = "127.0.0.1";
778
+ const CALLBACK_PATH = "/callback";
779
+ const CONSENT_TIMEOUT = "10 minutes";
780
+ const RETURNED_PAGE = "<!doctype html><title>Agent Runner</title><p>Back to your terminal: you can close this tab.</p>";
781
+ var ConsentFailed = class extends Schema.TaggedError()("ConsentFailed", { reason: Schema.String }) {
782
+ get message() {
783
+ return `The connection did not complete: ${this.reason}`;
784
+ }
785
+ };
786
+ const listening = (server) => Effect.callback((resume) => {
787
+ server.listen(0, LOOPBACK, () => {
788
+ const address = server.address();
789
+ resume(address !== null && typeof address === "object" ? Effect.succeed(address.port) : Effect.die("The callback server has no port"));
790
+ });
791
+ });
792
+ const closed = (server) => Effect.callback((resume) => {
793
+ server.closeAllConnections();
794
+ server.close(() => resume(Effect.void));
795
+ });
796
+ const loopbackCallback = Effect.gen(function* () {
797
+ const returned = yield* Deferred.make();
798
+ const server = createServer((request, response) => {
799
+ const url = new URL(request.url ?? "/", `http://${LOOPBACK}`);
800
+ if (url.pathname !== CALLBACK_PATH) {
801
+ response.writeHead(404).end();
802
+ return;
803
+ }
804
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(RETURNED_PAGE);
805
+ Deferred.doneUnsafe(returned, Effect.succeed(url.searchParams));
806
+ });
807
+ const port = yield* Effect.acquireRelease(listening(server), () => closed(server));
808
+ return {
809
+ url: `http://${LOOPBACK}:${port}${CALLBACK_PATH}`,
810
+ returned: Deferred.await(returned)
811
+ };
812
+ });
813
+ const BROWSER_OPENERS = {
814
+ darwin: (url) => ChildProcess.make("open", [url]),
815
+ linux: (url) => ChildProcess.make("xdg-open", [url]),
816
+ win32: (url) => ChildProcess.make("cmd", [
817
+ "/c",
818
+ "start",
819
+ "\"\"",
820
+ url
821
+ ])
822
+ };
823
+ const openInBrowser = Effect.fnUntraced(function* (url) {
824
+ const opener = BROWSER_OPENERS[process.platform];
825
+ if (opener === void 0) return;
826
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
827
+ yield* Effect.ignore(spawner.exitCode(opener(url)));
828
+ });
829
+ const confirmationOf = (params, id) => {
830
+ if (params.get("connection_id") !== id) return Effect.fail(new ConsentFailed({ reason: "the browser came back for another connection" }));
831
+ return Option.match(Option.fromNullOr(params.get("confirmation")), {
832
+ onSome: Effect.succeed,
833
+ onNone: () => Effect.fail(new ConsentFailed({ reason: params.get("error") ?? "no confirmation came back" }))
834
+ });
835
+ };
836
+ const add = Command.make("add", { server: Argument.String("server-url").pipe(Argument.withDescription("URL of the MCP server, in https")) }, Effect.fn("connections.add")(function* ({ server }) {
837
+ const client = yield* ApiClient;
838
+ const callback = yield* loopbackCallback;
839
+ const created = yield* client.connections.create({ payload: {
840
+ server_url: server,
841
+ return_url: Option.some(callback.url)
842
+ } });
843
+ yield* Console.error(`Consent in your browser, opening: ${created.consent_url}`);
844
+ yield* openInBrowser(created.consent_url);
845
+ const returned = yield* callback.returned.pipe(Effect.timeoutOrElse({
846
+ duration: CONSENT_TIMEOUT,
847
+ orElse: () => Effect.fail(new ConsentFailed({ reason: "no consent within 10 minutes" }))
848
+ }));
849
+ const confirmation = yield* confirmationOf(returned, created.connection.id);
850
+ const connection = yield* client.connections.confirm({
851
+ params: { id: created.connection.id },
852
+ payload: { confirmation }
853
+ });
854
+ yield* Console.log(yield* encodeConnection(connection));
855
+ }, Effect.scoped)).pipe(Command.withDescription("Connect to an MCP server as one of its users: consent in your browser, then use the connection in runs"));
856
+ const list = Command.make("list", {}, Effect.fn("connections.list")(function* () {
857
+ const client = yield* ApiClient;
858
+ yield* Console.log(yield* encodeConnections(yield* client.connections.list()));
859
+ })).pipe(Command.withDescription("List connections"));
860
+ const get$1 = Command.make("get", { id: connectionId }, Effect.fn("connections.get")(function* ({ id }) {
861
+ const client = yield* ApiClient;
862
+ yield* Console.log(yield* encodeConnection(yield* client.connections.get({ params: { id } })));
863
+ })).pipe(Command.withDescription("Show a connection"));
864
+ const remove = Command.make("remove", { id: connectionId }, Effect.fn("connections.remove")(function* ({ id }) {
865
+ yield* (yield* ApiClient).connections.delete({ params: { id } });
866
+ })).pipe(Command.withDescription("Delete a connection and the access it holds"));
867
+ const connections = Command.make("connections").pipe(Command.withDescription("Manage connections to MCP servers"), Command.withSubcommands([
868
+ add,
869
+ list,
870
+ get$1,
871
+ remove
872
+ ]));
873
+ //#endregion
874
+ //#region src/runs.ts
875
+ const encodeRun = Schema.encodeEffect(Schema.fromJsonString(Run, { space: 2 }));
876
+ const encodeFrames = Schema.encodeEffect(Schema.fromJsonString(List(Frame), { space: 2 }));
877
+ const encodeEvents = Schema.encodeEffect(Schema.fromJsonString(List(Event), { space: 2 }));
878
+ const encodeFrame = Schema.encodeEffect(Schema.fromJsonString(Frame));
879
+ const encodeEvent = Schema.encodeEffect(Schema.fromJsonString(Event));
880
+ const encodeUsage = Schema.encodeEffect(Schema.fromJsonString(RunUsage, { space: 2 }));
881
+ const encodePrompt = Schema.encodeEffect(Schema.fromJsonString(Prompt, { space: 2 }));
882
+ const encodePrompts = Schema.encodeEffect(Schema.fromJsonString(List(Prompt), { space: 2 }));
883
+ const runId = Argument.String("id").pipe(Argument.withDescription("Run id"), Argument.withSchema(RunId));
884
+ const variableOf = (entry) => {
885
+ const equals = entry.indexOf("=");
886
+ if (equals !== -1) return Option.some({
887
+ name: entry.slice(0, equals),
888
+ value: entry.slice(equals + 1)
889
+ });
890
+ return Option.map(Option.fromUndefinedOr(process.env[entry]), (value) => ({
891
+ name: entry,
892
+ value
893
+ }));
894
+ };
895
+ const mcpServerOf = (entry) => {
896
+ const equals = entry.indexOf("=");
897
+ if (equals === -1) return Option.none();
898
+ const name = entry.slice(0, equals);
899
+ const target = entry.slice(equals + 1);
900
+ return Option.some(URL.canParse(target) ? {
901
+ name,
902
+ url: target
903
+ } : {
904
+ name,
905
+ connection_id: ConnectionId.make(target)
906
+ });
907
+ };
908
+ const create = Command.make("create", {
909
+ prompt: Argument.String("prompt").pipe(Argument.withDescription("Prompt sent to the agent")),
910
+ harness: Flag.Literals("harness", Harness.literals).pipe(Flag.withAlias("H"), Flag.withDescription("Harness running the agent")),
911
+ model: Flag.String("model").pipe(Flag.withAlias("m"), Flag.withDescription("Model used by the harness")),
912
+ repository: Flag.String("repository").pipe(Flag.withAlias("r"), Flag.withDescription("Git repository cloned into the sandbox"), Flag.optional),
913
+ env: Flag.String("env").pipe(Flag.withAlias("e"), Flag.withDescription("Environment variable set in the agent's processes, as KEY=VALUE, or KEY to pass its local value"), Flag.filterMap(variableOf, (entry) => `${entry} is not set in the local environment`), Flag.atLeast(0)),
914
+ setup: Flag.String("setup").pipe(Flag.withDescription("Shell script run in the sandbox after checkout, before the first prompt; from a file: --setup \"$(cat setup.sh)\""), Flag.optional),
915
+ teardown: Flag.String("teardown").pipe(Flag.withDescription("Shell script run in the sandbox after the run ends, before the sandbox is destroyed; from a file: --teardown \"$(cat teardown.sh)\""), Flag.optional),
916
+ mcp: Flag.String("mcp").pipe(Flag.withDescription("MCP server the agent can use, as NAME=URL, through your connection to that URL if you have one, or NAME=CONNECTION_ID"), Flag.filterMap(mcpServerOf, (entry) => `${entry} is not NAME=URL or NAME=CONNECTION_ID`), Flag.atLeast(0)),
917
+ budget: Flag.Finite("budget").pipe(Flag.withAlias("b"), Flag.withDescription("Most the run may spend, in US dollars"), Flag.optional),
918
+ oneShot: Flag.Boolean("one-shot").pipe(Flag.withDescription("End the run once its first prompt finishes (default); --no-one-shot waits for follow-ups"), Flag.optional),
919
+ idleTimeout: Flag.Int("idle-timeout").pipe(Flag.withDescription("Seconds an idle run waits for a follow-up before it ends"), Flag.optional),
920
+ timeLimit: Flag.Int("time-limit").pipe(Flag.withDescription("Longest the run may last, in seconds, before it is stopped"), Flag.optional),
921
+ idempotencyKey: Flag.String("idempotency-key").pipe(Flag.withDescription("Retry safely: running again with the same key returns the run already created"), Flag.optional)
922
+ }, Effect.fn("runs.create")(function* ({ prompt, harness, model, repository, env, setup, teardown, mcp, budget, oneShot, idleTimeout, timeLimit, idempotencyKey }) {
923
+ const run = yield* (yield* ApiClient).runs.create({
924
+ headers: { "idempotency-key": idempotencyKey },
925
+ payload: {
926
+ agent: {
927
+ harness,
928
+ model
929
+ },
930
+ prompt,
931
+ environment: {
932
+ repository,
933
+ variables: Option.some(Object.fromEntries(env.map(({ name, value }) => [name, value]))),
934
+ setup,
935
+ teardown,
936
+ mcp_servers: Option.some(mcp)
937
+ },
938
+ budget_usd: budget,
939
+ one_shot: oneShot,
940
+ idle_timeout_seconds: idleTimeout,
941
+ time_limit_seconds: timeLimit
942
+ }
943
+ });
944
+ yield* Console.log(yield* encodeRun(run));
945
+ })).pipe(Command.withDescription("Start a run"));
946
+ const get = Command.make("get", { id: runId }, Effect.fn("runs.get")(function* ({ id }) {
947
+ const run = yield* (yield* ApiClient).runs.get({ params: { id } });
948
+ yield* Console.log(yield* encodeRun(run));
949
+ })).pipe(Command.withDescription("Show a run"));
950
+ const usage = Command.make("usage", { id: runId }, Effect.fn("runs.usage")(function* ({ id }) {
951
+ const client = yield* ApiClient;
952
+ yield* Console.log(yield* encodeUsage(yield* client.runs.getUsage({ params: { id } })));
953
+ })).pipe(Command.withDescription("Show what a run consumed and what it cost"));
954
+ const frames = Command.make("frames", {
955
+ id: runId,
956
+ after: Flag.Int("after").pipe(Flag.withAlias("a"), Flag.withDescription("Only frames with a greater seq"), Flag.optional),
957
+ limit: Flag.Int("limit").pipe(Flag.withAlias("l"), Flag.withDescription("Maximum frames returned"), Flag.optional),
958
+ prompt: Flag.Int("prompt").pipe(Flag.withAlias("p"), Flag.withDescription("Only frames of this prompt, by index"), Flag.optional)
959
+ }, Effect.fn("runs.frames")(function* ({ id, after, limit, prompt }) {
960
+ const page = yield* (yield* ApiClient).runs.listFrames({
961
+ params: { id },
962
+ query: {
963
+ after,
964
+ limit,
965
+ prompt
966
+ }
967
+ });
968
+ yield* Console.log(yield* encodeFrames(page));
969
+ })).pipe(Command.withDescription("List a run's frames"));
970
+ const events = Command.make("events", {
971
+ id: runId,
972
+ after: Flag.Int("after").pipe(Flag.withAlias("a"), Flag.withDescription("Only events with a greater seq"), Flag.optional),
973
+ limit: Flag.Int("limit").pipe(Flag.withAlias("l"), Flag.withDescription("Maximum events returned"), Flag.optional),
974
+ prompt: Flag.Int("prompt").pipe(Flag.withAlias("p"), Flag.withDescription("Only events of this prompt, by index"), Flag.optional)
975
+ }, Effect.fn("runs.events")(function* ({ id, after, limit, prompt }) {
976
+ const page = yield* (yield* ApiClient).runs.listEvents({
977
+ params: { id },
978
+ query: {
979
+ after,
980
+ limit,
981
+ prompt
982
+ }
983
+ });
984
+ yield* Console.log(yield* encodeEvents(page));
985
+ })).pipe(Command.withDescription("List a run's events"));
986
+ const printed = (sent, itemOf, encode) => sent.pipe(Stream.filterMap(itemOf), Stream.tap((item) => Effect.flatMap(encode(item), Console.log)), Stream.map((item) => item.seq), Stream.runLast);
987
+ const follow = Command.make("follow", {
988
+ id: runId,
989
+ after: Flag.Int("after").pipe(Flag.withAlias("a"), Flag.withDescription("Only frames, or events with --events, with a greater seq"), Flag.optional),
990
+ events: Flag.Boolean("events").pipe(Flag.withAlias("e"), Flag.withDescription("Follow the run's events instead of its frames"), Flag.withDefault(false))
991
+ }, Effect.fn("runs.follow")(function* ({ id, after, events: followingEvents }) {
992
+ const client = yield* ApiClient;
993
+ const followOnce = (afterSeq) => followingEvents ? Effect.flatMap(client.runs.followEvents({
994
+ params: { id },
995
+ query: { after: afterSeq }
996
+ }), (sent) => printed(sent, (item) => item.event === "event" ? Result.succeed(item.data) : Result.failVoid, encodeEvent)) : Effect.flatMap(client.runs.followFrames({
997
+ params: { id },
998
+ query: { after: afterSeq }
999
+ }), (sent) => printed(sent, (item) => item.event === "frame" ? Result.succeed(item.data) : Result.failVoid, encodeFrame));
1000
+ let lastSeen = after;
1001
+ while (true) {
1002
+ lastSeen = Option.orElse(yield* followOnce(lastSeen), () => lastSeen);
1003
+ if ((yield* client.runs.get({ params: { id } })).status === "ended") return;
1004
+ }
1005
+ })).pipe(Command.withDescription("Print a run's frames, or its events with --events, as they come, one JSON object per line, until the run ends"));
1006
+ const submitPrompt = Command.make("prompt", {
1007
+ id: runId,
1008
+ text: Argument.String("text").pipe(Argument.withDescription("Follow-up prompt sent to the agent")),
1009
+ model: Flag.String("model").pipe(Flag.withAlias("m"), Flag.withDescription("Model for this prompt only"), Flag.optional)
1010
+ }, Effect.fn("runs.prompt")(function* ({ id, text, model }) {
1011
+ const prompt = yield* (yield* ApiClient).runs.submitPrompt({
1012
+ params: { id },
1013
+ payload: {
1014
+ text,
1015
+ model
1016
+ }
1017
+ });
1018
+ yield* Console.log(yield* encodePrompt(prompt));
1019
+ })).pipe(Command.withDescription("Send a follow-up prompt to an idle run"));
1020
+ const prompts = Command.make("prompts", {
1021
+ id: runId,
1022
+ after: Flag.Int("after").pipe(Flag.withAlias("a"), Flag.withDescription("Only prompts with a greater index"), Flag.optional),
1023
+ limit: Flag.Int("limit").pipe(Flag.withAlias("l"), Flag.withDescription("Maximum prompts returned"), Flag.optional)
1024
+ }, Effect.fn("runs.prompts")(function* ({ id, after, limit }) {
1025
+ const page = yield* (yield* ApiClient).runs.listPrompts({
1026
+ params: { id },
1027
+ query: {
1028
+ after,
1029
+ limit
1030
+ }
1031
+ });
1032
+ yield* Console.log(yield* encodePrompts(page));
1033
+ })).pipe(Command.withDescription("List a run's prompts"));
1034
+ const end = Command.make("end", { id: runId }, Effect.fn("runs.end")(function* ({ id }) {
1035
+ const client = yield* ApiClient;
1036
+ yield* Console.log(yield* encodeRun(yield* client.runs.end({ params: { id } })));
1037
+ })).pipe(Command.withDescription("End a run once its current prompt finishes"));
1038
+ const cancel = Command.make("cancel", { id: runId }, Effect.fn("runs.cancel")(function* ({ id }) {
1039
+ const client = yield* ApiClient;
1040
+ yield* Console.log(yield* encodeRun(yield* client.runs.cancel({ params: { id } })));
1041
+ })).pipe(Command.withDescription("Stop a run right away, even mid-prompt"));
1042
+ const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), Command.withSubcommands([
1043
+ create,
1044
+ get,
1045
+ submitPrompt,
1046
+ prompts,
1047
+ frames,
1048
+ events,
1049
+ follow,
1050
+ usage,
1051
+ end,
1052
+ cancel
1053
+ ]));
1054
+ //#endregion
1055
+ //#region src/main.ts
1056
+ const health = Command.make("health", {}, Effect.fn("health")(function* () {
1057
+ yield* (yield* ApiClient).health();
1058
+ yield* Console.log("ok");
1059
+ })).pipe(Command.withDescription("Check that the API is reachable"));
1060
+ const cli = Command.make("tracks").pipe(Command.withDescription("Run coding agents in sandboxes"), Command.withSubcommands([
1061
+ health,
1062
+ runs,
1063
+ connections
1064
+ ]));
1065
+ const ApiClientLayer = Layer.unwrap(Effect.map(readEnv(...CLI_ENV), (env) => ApiClient.layer({
1066
+ baseUrl: env.AGENT_RUNNER_API_URL,
1067
+ agentRunnerKey: env.AGENT_RUNNER_KEY,
1068
+ organization: env.AGENT_RUNNER_ORGANIZATION,
1069
+ userAgent: `${CLI_USER_AGENT}/${version}`
1070
+ }))).pipe(Layer.provide(FetchHttpClient.layer));
1071
+ const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer);
1072
+ Command.run(cli, { version }).pipe(Effect.provide(MainLayer), NodeRuntime.runMain);
1073
+ //#endregion
1074
+ export {};
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@alpic-ai/tracks",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.4.0",
4
+ "description": "Run coding agents in sandboxes from the command line",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/alpic-ai/tracks.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "bin": {
12
+ "tracks": "./dist/main.js"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "@effect/platform-node": "^4.0.2",
23
+ "effect": "^4.0.2"
24
+ },
25
+ "devDependencies": {
26
+ "@tracks/contract": "0.0.0",
27
+ "@tracks/env": "0.0.0",
28
+ "rolldown": "^1.2.13"
29
+ },
30
+ "engines": {
31
+ "node": ">=24"
32
+ },
33
+ "scripts": {
34
+ "start": "node --import @tracks/env/preload src/main.ts",
35
+ "build": "rolldown --config rolldown.config.ts"
36
+ }
6
37
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.