@alpic-ai/tracks 0.4.0 → 0.4.2
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/dist/main.js +138 -68
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -7,17 +7,16 @@ import { isIPv4 } from "node:net";
|
|
|
7
7
|
import { Argument, Command, Flag } from "effect/cli";
|
|
8
8
|
import { createServer } from "node:http";
|
|
9
9
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var Unauthorized = class extends Schema.TaggedError()("Unauthorized", { message: Schema.String }, {
|
|
10
|
+
Context.Service()("@tracks/contract/RequestOrganization");
|
|
11
|
+
Context.Service()("@tracks/contract/CurrentApiKey");
|
|
12
|
+
var ApiKeyRefused = class extends Schema.TaggedError()("ApiKeyRefused", { message: Schema.String }, {
|
|
14
13
|
httpApiStatus: 401,
|
|
15
|
-
description:
|
|
14
|
+
description: "The API key is missing, malformed or revoked, or its organization is closed."
|
|
16
15
|
}) {};
|
|
17
|
-
var
|
|
16
|
+
var ApiKeyAuthorization = class extends HttpApiMiddleware.Service()("@tracks/contract/ApiKeyAuthorization", {
|
|
18
17
|
requiredForClient: true,
|
|
19
|
-
security: { bearer: HttpApiSecurity.http({ scheme: "bearer" }).pipe(HttpApiSecurity.annotate(OpenApi.Description,
|
|
20
|
-
error:
|
|
18
|
+
security: { bearer: HttpApiSecurity.http({ scheme: "bearer" }).pipe(HttpApiSecurity.annotate(OpenApi.Description, "An API key of your organization, starting with `trk_`, sent as `Authorization: Bearer <key>`.")) },
|
|
19
|
+
error: ApiKeyRefused
|
|
21
20
|
}) {};
|
|
22
21
|
const OptionalQueryParam = (schema, description) => Schema.optionalKey(schema).annotateKey({ description }).pipe(Schema.decodeTo(Schema.Option(Schema.toType(schema)), SchemaTransformation.optionFromOptionalKey()));
|
|
23
22
|
const ListQuery = {
|
|
@@ -88,18 +87,18 @@ const Connection = Schema.Union([Schema.Struct({
|
|
|
88
87
|
}).annotate({
|
|
89
88
|
identifier: "FailedConnection",
|
|
90
89
|
description: "A connection whose consent did not complete."
|
|
91
|
-
})]).annotate({ description: "Access to an MCP server as one of its users, kept by
|
|
90
|
+
})]).annotate({ description: "Access to an MCP server as one of its users, kept by us." });
|
|
92
91
|
const CreatedConnection = Schema.Struct({
|
|
93
92
|
connection: Connection,
|
|
94
93
|
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
94
|
}).annotate({ identifier: "CreatedConnection" });
|
|
96
95
|
var ConnectionNotFound = class extends Schema.TaggedError()("ConnectionNotFound", { message: Schema.String }, {
|
|
97
96
|
httpApiStatus: 404,
|
|
98
|
-
description: "No connection with this id exists
|
|
97
|
+
description: "No connection with this id exists in your organization."
|
|
99
98
|
}) {};
|
|
100
99
|
var ConnectionRefused = class extends Schema.TaggedError()("ConnectionRefused", { message: Schema.String }, {
|
|
101
100
|
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
|
|
101
|
+
description: "The connection cannot be started: the return URL is not allowed, or the MCP server cannot be authorized in a way we support."
|
|
103
102
|
}) {};
|
|
104
103
|
var ServerUnreachable = class extends Schema.TaggedError()("ServerUnreachable", { message: Schema.String }, {
|
|
105
104
|
httpApiStatus: 502,
|
|
@@ -141,10 +140,10 @@ var ConnectionsApi = class extends HttpApiGroup.make("connections").add(HttpApiE
|
|
|
141
140
|
}).annotateMerge(OpenApi.annotations({
|
|
142
141
|
summary: "Delete a connection",
|
|
143
142
|
description: "Deletes a connection and the access it holds."
|
|
144
|
-
}))).
|
|
143
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
145
144
|
title: "Connections",
|
|
146
145
|
description: "Let agents use MCP servers as one of their users, without their credentials entering the sandbox."
|
|
147
|
-
})) {};
|
|
146
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
148
147
|
//#endregion
|
|
149
148
|
//#region ../contract/src/runs.ts
|
|
150
149
|
const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
|
|
@@ -154,14 +153,15 @@ const RunId = Schema.String.pipe(Schema.brand("RunId")).annotate({
|
|
|
154
153
|
const Harness = Schema.Literals([
|
|
155
154
|
"claude-code",
|
|
156
155
|
"codex",
|
|
157
|
-
"codex-app"
|
|
156
|
+
"codex-app",
|
|
157
|
+
"cursor"
|
|
158
158
|
]).annotate({
|
|
159
159
|
identifier: "Harness",
|
|
160
|
-
description: "The coding agent to run: `claude-code` (Claude Code), `codex` (OpenAI's Codex CLI)
|
|
160
|
+
description: "The coding agent to run: `claude-code` (Claude Code), `codex` (OpenAI's Codex CLI), `codex-app` (the Codex mode of OpenAI's ChatGPT desktop app, with its browser, computer-use and document tools) or `cursor` (Cursor's CLI)."
|
|
161
161
|
});
|
|
162
162
|
const Agent = Schema.Struct({
|
|
163
163
|
harness: Harness,
|
|
164
|
-
model: Schema.NonEmptyString.annotate({ description: "Model the agent uses, such as `claude-sonnet-5-5`." })
|
|
164
|
+
model: Schema.NonEmptyString.annotate({ description: "Model the agent uses, such as `claude-sonnet-5-5`, or a Cursor model id such as `gpt-5.6-luna-low`." })
|
|
165
165
|
}).annotate({
|
|
166
166
|
identifier: "Agent",
|
|
167
167
|
description: "The coding agent and the model it uses."
|
|
@@ -169,8 +169,8 @@ const Agent = Schema.Struct({
|
|
|
169
169
|
const repository = Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "URL of a Git repository to clone before the agent starts." }));
|
|
170
170
|
const MAX_SCRIPT_LENGTH = 32768;
|
|
171
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
|
|
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
|
|
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 after a restart on our side: 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 after a restart on our side: make it idempotent.");
|
|
174
174
|
const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
175
175
|
const RESERVED_VARIABLES = /* @__PURE__ */ new Set(["PATH", "HOME"]);
|
|
176
176
|
const RESERVED_VARIABLE_PREFIXES = [
|
|
@@ -214,7 +214,7 @@ const McpServerInput = Schema.Union([Schema.Struct({
|
|
|
214
214
|
description: "An MCP server given by a connection to it, to pick between several accounts."
|
|
215
215
|
})]).annotate({
|
|
216
216
|
identifier: "McpServerInput",
|
|
217
|
-
description: "An MCP server the agent can use, through
|
|
217
|
+
description: "An MCP server the agent can use, reached through us: credentials never enter the sandbox. Set `url` or `connection_id`."
|
|
218
218
|
});
|
|
219
219
|
const McpServersInput = Schema.Array(McpServerInput).check(Schema.isMaxLength(MAX_MCP_SERVERS), Schema.makeFilter((servers) => {
|
|
220
220
|
const names = servers.map((server) => server.name);
|
|
@@ -251,6 +251,26 @@ const RunEnvironment = Schema.Struct({
|
|
|
251
251
|
identifier: "Environment",
|
|
252
252
|
description: "What the agent works on."
|
|
253
253
|
});
|
|
254
|
+
const LABEL_KEY_PATTERN = "[A-Za-z0-9_.-]{1,64}";
|
|
255
|
+
const LABEL_KEY = new RegExp(`^${LABEL_KEY_PATTERN}$`);
|
|
256
|
+
const MAX_LABELS = 20;
|
|
257
|
+
const MAX_LABEL_VALUE_LENGTH = 256;
|
|
258
|
+
const Labels = Schema.Record(Schema.String, Schema.String.check(Schema.isMaxLength(MAX_LABEL_VALUE_LENGTH))).annotate({ description: `Labels you attach to the run to find it again, as keys and values, such as \`{ "ticket": "ENG-42" }\`. We never read them. Keys match \`${LABEL_KEY.source}\`; at most ${MAX_LABELS} labels with values of up to ${MAX_LABEL_VALUE_LENGTH} characters.` }).check(Schema.isMaxProperties(MAX_LABELS), Schema.makeFilter((labels) => Object.keys(labels).flatMap((key) => LABEL_KEY.test(key) ? [] : [{
|
|
259
|
+
path: [key],
|
|
260
|
+
issue: `Expected a key matching ${LABEL_KEY.source}`
|
|
261
|
+
}])));
|
|
262
|
+
const LABEL_FILTER = new RegExp(`^${LABEL_KEY_PATTERN}:`);
|
|
263
|
+
const keyAndValueOf = (filter) => {
|
|
264
|
+
const separator = filter.indexOf(":");
|
|
265
|
+
return [filter.slice(0, separator), filter.slice(separator + 1)];
|
|
266
|
+
};
|
|
267
|
+
const LabelFilters = Schema.Array(Schema.String.check(Schema.isPattern(LABEL_FILTER))).check(Schema.makeFilter((filters) => {
|
|
268
|
+
const keys = filters.map((filter) => keyAndValueOf(filter)[0]);
|
|
269
|
+
return new Set(keys).size === keys.length || "Expected each label key once";
|
|
270
|
+
})).pipe(Schema.decodeTo(Labels, SchemaTransformation.transform({
|
|
271
|
+
decode: (filters) => Object.fromEntries(filters.map(keyAndValueOf)),
|
|
272
|
+
encode: (labels) => Object.entries(labels).map(([key, value]) => `${key}:${value}`)
|
|
273
|
+
})));
|
|
254
274
|
const Budget = Schema.Finite.check(Schema.isBetween({
|
|
255
275
|
minimum: .01,
|
|
256
276
|
maximum: 1e4
|
|
@@ -263,6 +283,7 @@ const CreateRunInput = Schema.Struct({
|
|
|
263
283
|
agent: Agent,
|
|
264
284
|
prompt: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do first." }),
|
|
265
285
|
environment: EnvironmentInput,
|
|
286
|
+
labels: Schema.optionalKey(Labels),
|
|
266
287
|
budget_usd: Schema.OptionFromOptionalKey(Budget),
|
|
267
288
|
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
289
|
idle_timeout_seconds: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
|
|
@@ -274,10 +295,10 @@ const CreateRunInput = Schema.Struct({
|
|
|
274
295
|
maximum: MAX_TIME_LIMIT_SECONDS
|
|
275
296
|
})).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
297
|
}).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
|
|
298
|
+
const SendPromptInput = Schema.Struct({
|
|
278
299
|
text: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do next." }),
|
|
279
300
|
model: Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "Model for this prompt only. Defaults to the run's model." }))
|
|
280
|
-
}).annotate({ identifier: "
|
|
301
|
+
}).annotate({ identifier: "SendPromptInput" });
|
|
281
302
|
const PromptIndex = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "Position of the prompt in its run, starting at 1." });
|
|
282
303
|
const promptFields = {
|
|
283
304
|
run_id: RunId,
|
|
@@ -329,6 +350,7 @@ const FailureKind = Schema.Literals([
|
|
|
329
350
|
});
|
|
330
351
|
const runFields = {
|
|
331
352
|
id: RunId,
|
|
353
|
+
labels: Labels.annotate({ description: "Labels attached to the run when it was created." }),
|
|
332
354
|
agent: Agent,
|
|
333
355
|
environment: RunEnvironment,
|
|
334
356
|
budget_usd: Schema.Number.annotate({ description: "Maximum the run may spend, in US dollars." }),
|
|
@@ -380,7 +402,7 @@ const Run = Schema.Union([
|
|
|
380
402
|
]).annotate({ description: "A coding agent working on your prompt in an isolated sandbox." });
|
|
381
403
|
var RunNotFound = class extends Schema.TaggedError()("RunNotFound", { message: Schema.String }, {
|
|
382
404
|
httpApiStatus: 404,
|
|
383
|
-
description: "No run with this id exists
|
|
405
|
+
description: "No run with this id exists in your organization."
|
|
384
406
|
}) {};
|
|
385
407
|
var ConnectionUnusable = class extends Schema.TaggedError()("ConnectionUnusable", { message: Schema.String }, {
|
|
386
408
|
httpApiStatus: 422,
|
|
@@ -390,6 +412,21 @@ var ModelNotOffered = class extends Schema.TaggedError()("ModelNotOffered", { me
|
|
|
390
412
|
httpApiStatus: 422,
|
|
391
413
|
description: "The agent cannot use this model. The message lists the models it can use."
|
|
392
414
|
}) {};
|
|
415
|
+
var ModelProviderUnavailable = class extends Schema.TaggedError()("ModelProviderUnavailable", { message: Schema.String }, {
|
|
416
|
+
httpApiStatus: 503,
|
|
417
|
+
description: "The model provider could not be reached to check the model. Retry later."
|
|
418
|
+
}) {};
|
|
419
|
+
var InsufficientBalance = class extends Schema.TaggedError()("InsufficientBalance", {
|
|
420
|
+
message: Schema.String,
|
|
421
|
+
top_up_url: Schema.String.annotate({ description: "Where to add funds to your organization's balance." })
|
|
422
|
+
}, {
|
|
423
|
+
httpApiStatus: 402,
|
|
424
|
+
description: "Your organization's balance is $0 or below: add funds to start runs again."
|
|
425
|
+
}) {};
|
|
426
|
+
var OrganizationBlocked = class extends Schema.TaggedError()("OrganizationBlocked", { message: Schema.String }, {
|
|
427
|
+
httpApiStatus: 403,
|
|
428
|
+
description: "Your organization cannot start runs after a dispute on one of its payments: contact us."
|
|
429
|
+
}) {};
|
|
393
430
|
var RunNotIdle = class extends Schema.TaggedError()("RunNotIdle", { message: Schema.String }, {
|
|
394
431
|
httpApiStatus: 409,
|
|
395
432
|
description: "The run accepts no prompt now: it is one-shot, has ended, or is still working on a prompt."
|
|
@@ -433,7 +470,7 @@ const eventFields = {
|
|
|
433
470
|
prompt: PromptIndex.annotate({ description: "Index of the prompt the event belongs to." }),
|
|
434
471
|
captured_at: Schema.DateTimeUtc.annotate({ description: "When the output the event comes from was captured." }),
|
|
435
472
|
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
|
|
473
|
+
frame_seqs: Schema.Array(Schema.Int).annotate({ description: "The `seq` of each output line the event was read from. Empty for events we record ourselves." })
|
|
437
474
|
};
|
|
438
475
|
const Event = Schema.Union([
|
|
439
476
|
Schema.Struct({
|
|
@@ -529,14 +566,35 @@ const EventStreamItem = Schema.Union([Schema.Struct({
|
|
|
529
566
|
event: Schema.Literal("heartbeat"),
|
|
530
567
|
data: Schema.Literal("")
|
|
531
568
|
})]);
|
|
569
|
+
const STARTED_BY_LABEL = "started_by";
|
|
570
|
+
const CreateRunInputBesidesStartedBy = CreateRunInput.check(Schema.makeFilter((input) => Object.keys(input.labels ?? {}).filter((key) => key !== STARTED_BY_LABEL).length < MAX_LABELS || {
|
|
571
|
+
path: ["labels"],
|
|
572
|
+
issue: `Expected at most 19 labels besides ${STARTED_BY_LABEL}, which is set to the API key`
|
|
573
|
+
}));
|
|
532
574
|
var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("create", "/runs", {
|
|
533
575
|
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:
|
|
576
|
+
payload: CreateRunInputBesidesStartedBy,
|
|
535
577
|
success: Run.pipe(HttpApiSchema.status(201)),
|
|
536
|
-
error: [
|
|
578
|
+
error: [
|
|
579
|
+
ModelNotOffered,
|
|
580
|
+
ModelProviderUnavailable,
|
|
581
|
+
ConnectionUnusable,
|
|
582
|
+
InsufficientBalance,
|
|
583
|
+
OrganizationBlocked
|
|
584
|
+
]
|
|
537
585
|
}).annotateMerge(OpenApi.annotations({
|
|
538
586
|
summary: "Create a run",
|
|
539
|
-
description:
|
|
587
|
+
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. The run gets the label \`${STARTED_BY_LABEL}: key:<id>\`, with the id of the API key that created it, in place of any \`${STARTED_BY_LABEL}\` you send. Runs start only while your organization's balance is above $0.`
|
|
588
|
+
})), HttpApiEndpoint.get("list", "/runs", {
|
|
589
|
+
query: {
|
|
590
|
+
label: OptionalQueryParam(LabelFilters, "Return only runs with this label, written `key:value`, such as `ticket:ENG-42`. Repeat it to require several labels."),
|
|
591
|
+
after: OptionalQueryParam(RunId, "Return only runs listed after the run with this id."),
|
|
592
|
+
limit: ListQuery.limit
|
|
593
|
+
},
|
|
594
|
+
success: List(Run)
|
|
595
|
+
}).annotateMerge(OpenApi.annotations({
|
|
596
|
+
summary: "List runs",
|
|
597
|
+
description: "Returns your runs, most recently started first. To get the next page, pass the `id` of the last run received as `after`."
|
|
540
598
|
})), HttpApiEndpoint.get("get", "/runs/:id", {
|
|
541
599
|
params: { id: RunId },
|
|
542
600
|
success: Run,
|
|
@@ -549,7 +607,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
549
607
|
success: RunUsage,
|
|
550
608
|
error: RunNotFound
|
|
551
609
|
}).annotateMerge(OpenApi.annotations({
|
|
552
|
-
summary: "Get run usage",
|
|
610
|
+
summary: "Get a run's usage",
|
|
553
611
|
description: "Returns what the run has consumed so far, per model and in sandbox time, and what it cost."
|
|
554
612
|
})), HttpApiEndpoint.post("end", "/runs/:id/end", {
|
|
555
613
|
params: { id: RunId },
|
|
@@ -565,14 +623,15 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
565
623
|
}).annotateMerge(OpenApi.annotations({
|
|
566
624
|
summary: "Cancel a run",
|
|
567
625
|
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("
|
|
626
|
+
})), HttpApiEndpoint.post("sendPrompt", "/runs/:id/prompts", {
|
|
569
627
|
params: { id: RunId },
|
|
570
|
-
payload:
|
|
628
|
+
payload: SendPromptInput,
|
|
571
629
|
success: Prompt.pipe(HttpApiSchema.status(202)),
|
|
572
630
|
error: [
|
|
573
631
|
RunNotFound,
|
|
574
632
|
RunNotIdle,
|
|
575
|
-
ModelNotOffered
|
|
633
|
+
ModelNotOffered,
|
|
634
|
+
ModelProviderUnavailable
|
|
576
635
|
]
|
|
577
636
|
}).annotateMerge(OpenApi.annotations({
|
|
578
637
|
summary: "Send a follow-up prompt",
|
|
@@ -583,7 +642,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
583
642
|
success: List(Prompt),
|
|
584
643
|
error: RunNotFound
|
|
585
644
|
}).annotateMerge(OpenApi.annotations({
|
|
586
|
-
summary: "List run prompts",
|
|
645
|
+
summary: "List a run's prompts",
|
|
587
646
|
description: "Returns the run's prompts in order. To get the next page, pass the `index` of the last prompt received as `after`."
|
|
588
647
|
})), HttpApiEndpoint.get("listFrames", "/runs/:id/frames", {
|
|
589
648
|
params: { id: RunId },
|
|
@@ -594,7 +653,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
594
653
|
success: List(Frame),
|
|
595
654
|
error: RunNotFound
|
|
596
655
|
}).annotateMerge(OpenApi.annotations({
|
|
597
|
-
summary: "List run
|
|
656
|
+
summary: "List a run's frames",
|
|
598
657
|
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
658
|
})), HttpApiEndpoint.get("listEvents", "/runs/:id/events", {
|
|
600
659
|
params: { id: RunId },
|
|
@@ -605,7 +664,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
605
664
|
success: List(Event),
|
|
606
665
|
error: RunNotFound
|
|
607
666
|
}).annotateMerge(OpenApi.annotations({
|
|
608
|
-
summary: "List run events",
|
|
667
|
+
summary: "List a run's events",
|
|
609
668
|
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
669
|
})), HttpApiEndpoint.get("followFrames", "/runs/:id/frames/stream", {
|
|
611
670
|
params: { id: RunId },
|
|
@@ -613,7 +672,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
613
672
|
success: HttpApiSchema.StreamSse({ events: FrameStreamItem }),
|
|
614
673
|
error: RunNotFound
|
|
615
674
|
}).annotateMerge(OpenApi.annotations({
|
|
616
|
-
summary: "Stream run
|
|
675
|
+
summary: "Stream a run's frames",
|
|
617
676
|
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
677
|
})), HttpApiEndpoint.get("followEvents", "/runs/:id/events/stream", {
|
|
619
678
|
params: { id: RunId },
|
|
@@ -621,41 +680,42 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
621
680
|
success: HttpApiSchema.StreamSse({ events: EventStreamItem }),
|
|
622
681
|
error: RunNotFound
|
|
623
682
|
}).annotateMerge(OpenApi.annotations({
|
|
624
|
-
summary: "Stream run events",
|
|
683
|
+
summary: "Stream a run's events",
|
|
625
684
|
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
|
-
}))).
|
|
685
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
627
686
|
title: "Runs",
|
|
628
687
|
description: "Start coding agents and follow their work."
|
|
629
|
-
})) {};
|
|
688
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
630
689
|
//#endregion
|
|
631
690
|
//#region ../contract/src/system.ts
|
|
632
691
|
const EgressIps = Schema.Struct({ ips: Schema.Array(Schema.String).annotate({ description: "IPv4 addresses, empty when the API runs locally." }) }).annotate({ identifier: "EgressIps" });
|
|
633
692
|
var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).add(HttpApiEndpoint.get("health", "/health", { success: HttpApiSchema.NoContent }).annotateMerge(OpenApi.annotations({
|
|
634
693
|
summary: "Check health",
|
|
635
|
-
description: "Returns 204 when the API is up. No
|
|
694
|
+
description: "Returns 204 when the API is up. No key required."
|
|
636
695
|
}))).add(HttpApiEndpoint.get("egressIps", "/egress-ips", { success: EgressIps }).annotateMerge(OpenApi.annotations({
|
|
637
696
|
summary: "List egress IPs",
|
|
638
|
-
description: "The fixed addresses
|
|
697
|
+
description: "The fixed addresses we call 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 key required."
|
|
639
698
|
}))).annotateMerge(OpenApi.annotations({ title: "System" })) {};
|
|
640
699
|
//#endregion
|
|
641
700
|
//#region ../contract/src/api.ts
|
|
642
|
-
var
|
|
643
|
-
title: "
|
|
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."
|
|
701
|
+
var TracksApi = class extends HttpApi.make("tracks").add(RunsApi).add(ConnectionsApi).add(SystemApi).annotate(HttpApi.ParseOptions, { errors: "all" }).annotateMerge(OpenApi.annotations({
|
|
702
|
+
title: "Tracks API",
|
|
703
|
+
description: "Run coding agents such as Claude Code in isolated sandboxes with an API key of your organization. Create a run with a prompt, then follow its output and what it costs."
|
|
645
704
|
})) {};
|
|
646
705
|
//#endregion
|
|
647
706
|
//#region ../contract/src/client.ts
|
|
648
707
|
const CLI_USER_AGENT = "tracks";
|
|
649
708
|
var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiClient") {
|
|
650
|
-
static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(
|
|
709
|
+
static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(TracksApi, { transformClient: HttpClient.mapRequest((request) => {
|
|
651
710
|
const located = HttpClientRequest.prependUrl(request, options.baseUrl.href);
|
|
652
711
|
return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
|
|
653
|
-
}) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(
|
|
712
|
+
}) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(ApiKeyAuthorization, ({ next, request }) => next(HttpClientRequest.bearerToken(request, Redacted.value(options.apiKey))))));
|
|
654
713
|
};
|
|
655
714
|
//#endregion
|
|
656
715
|
//#region ../env/src/env.ts
|
|
657
716
|
const Secret = Schema.Redacted(Schema.NonEmptyString);
|
|
658
717
|
const AgentRunnerKey = Schema.Redacted(Schema.NonEmptyString.check(Schema.isMinLength(32)));
|
|
718
|
+
const StripeKey = (prefix) => Schema.Redacted(Schema.String.check(Schema.isStartingWith(prefix)));
|
|
659
719
|
const Port = Schema.Int.check(Schema.isBetween({
|
|
660
720
|
minimum: 1,
|
|
661
721
|
maximum: 65535
|
|
@@ -665,7 +725,7 @@ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
|
665
725
|
"127.0.0.1",
|
|
666
726
|
"[::1]"
|
|
667
727
|
]);
|
|
668
|
-
const
|
|
728
|
+
const TRACKS_API_PATH = "/api";
|
|
669
729
|
const SecureUrl = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" || LOOPBACK_HOSTS.has(url.hostname) || "must use https"));
|
|
670
730
|
const ENCRYPTION_KEY_BYTES = 32;
|
|
671
731
|
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}`)));
|
|
@@ -681,19 +741,23 @@ const IpAddresses = Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.Strin
|
|
|
681
741
|
const SentryDsn = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" && url.username !== "" || "must be a Sentry DSN"));
|
|
682
742
|
const Secrets = Schema.Struct({
|
|
683
743
|
AGENT_RUNNER_KEY: AgentRunnerKey,
|
|
744
|
+
TRACKS_API_KEY: Secret,
|
|
684
745
|
DAYTONA_API_KEY: Secret,
|
|
685
746
|
E2B_API_KEY: Secret,
|
|
686
747
|
ANTHROPIC_API_KEY: Secret,
|
|
687
748
|
OPENAI_API_KEY: Secret,
|
|
749
|
+
CURSOR_API_KEY: Secret,
|
|
750
|
+
CURSOR_ADMIN_KEY: Secret,
|
|
688
751
|
TYPESAFE_API_KEY: Secret,
|
|
689
752
|
DATABASE_PASSWORD: Secret,
|
|
690
753
|
CONNECTION_ENCRYPTION_KEY: EncryptionKey,
|
|
754
|
+
STRIPE_SECRET_KEY: StripeKey("rk_"),
|
|
755
|
+
STRIPE_WEBHOOK_SECRET: StripeKey("whsec_"),
|
|
691
756
|
SENTRY_DSN: Schema.optionalKey(Schema.Redacted(SentryDsn))
|
|
692
757
|
});
|
|
693
758
|
const Variables = Schema.Struct({
|
|
694
759
|
PORT: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(3e3))),
|
|
695
|
-
|
|
696
|
-
AGENT_RUNNER_API_URL: Schema.URLFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed(`https://agent-runner.lab.alpic.ai${AGENT_RUNNER_PATH}`))),
|
|
760
|
+
TRACKS_API_URL: Schema.URLFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed(`https://agent-runner.lab.alpic.ai${TRACKS_API_PATH}`))),
|
|
697
761
|
DAYTONA_API_URL: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://app.daytona.io/api"))),
|
|
698
762
|
AGENT_RUNNER_URL: SecureUrl,
|
|
699
763
|
CONNECTION_RETURN_ORIGINS: Origins.pipe(Schema.withDecodingDefaultKey(Effect.succeed(""))),
|
|
@@ -731,12 +795,18 @@ const DATABASE_ENV = [
|
|
|
731
795
|
];
|
|
732
796
|
const DAYTONA_ENV = ["DAYTONA_API_KEY", "DAYTONA_API_URL"];
|
|
733
797
|
const E2B_ENV = ["E2B_API_KEY"];
|
|
734
|
-
const GATEWAY_ENV = [
|
|
798
|
+
const GATEWAY_ENV = [
|
|
799
|
+
"ANTHROPIC_API_KEY",
|
|
800
|
+
"OPENAI_API_KEY",
|
|
801
|
+
"CURSOR_API_KEY",
|
|
802
|
+
"CURSOR_ADMIN_KEY"
|
|
803
|
+
];
|
|
735
804
|
const CONNECTIONS_ENV = [
|
|
736
805
|
"AGENT_RUNNER_URL",
|
|
737
806
|
"CONNECTION_RETURN_ORIGINS",
|
|
738
807
|
"CONNECTION_ENCRYPTION_KEY"
|
|
739
808
|
];
|
|
809
|
+
const PAYMENTS_ENV = ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"];
|
|
740
810
|
const AGENT_RUNNER_ENV = [
|
|
741
811
|
...DATABASE_ENV,
|
|
742
812
|
...DAYTONA_ENV,
|
|
@@ -756,19 +826,16 @@ const SERVICE_ENV = [
|
|
|
756
826
|
"EGRESS_IPS",
|
|
757
827
|
...LOG_ENV,
|
|
758
828
|
...OBSERVABILITY_ENV,
|
|
759
|
-
...AGENT_RUNNER_ENV
|
|
760
|
-
|
|
761
|
-
const CLI_ENV = [
|
|
762
|
-
"AGENT_RUNNER_KEY",
|
|
763
|
-
"AGENT_RUNNER_ORGANIZATION",
|
|
764
|
-
"AGENT_RUNNER_API_URL"
|
|
829
|
+
...AGENT_RUNNER_ENV,
|
|
830
|
+
...PAYMENTS_ENV
|
|
765
831
|
];
|
|
832
|
+
const CLI_ENV = ["TRACKS_API_KEY", "TRACKS_API_URL"];
|
|
766
833
|
const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
|
|
767
834
|
SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
|
|
768
835
|
const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
|
|
769
836
|
//#endregion
|
|
770
837
|
//#region package.json
|
|
771
|
-
var version = "0.4.
|
|
838
|
+
var version = "0.4.2";
|
|
772
839
|
//#endregion
|
|
773
840
|
//#region src/connections.ts
|
|
774
841
|
const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
|
|
@@ -777,7 +844,7 @@ const connectionId = Argument.String("id").pipe(Argument.withDescription("Connec
|
|
|
777
844
|
const LOOPBACK = "127.0.0.1";
|
|
778
845
|
const CALLBACK_PATH = "/callback";
|
|
779
846
|
const CONSENT_TIMEOUT = "10 minutes";
|
|
780
|
-
const RETURNED_PAGE = "<!doctype html><title>
|
|
847
|
+
const RETURNED_PAGE = "<!doctype html><title>Tracks</title><p>Back to your terminal: you can close this tab.</p>";
|
|
781
848
|
var ConsentFailed = class extends Schema.TaggedError()("ConsentFailed", { reason: Schema.String }) {
|
|
782
849
|
get message() {
|
|
783
850
|
return `The connection did not complete: ${this.reason}`;
|
|
@@ -833,7 +900,7 @@ const confirmationOf = (params, id) => {
|
|
|
833
900
|
onNone: () => Effect.fail(new ConsentFailed({ reason: params.get("error") ?? "no confirmation came back" }))
|
|
834
901
|
});
|
|
835
902
|
};
|
|
836
|
-
const
|
|
903
|
+
const create$1 = Command.make("create", { server: Argument.String("server-url").pipe(Argument.withDescription("URL of the MCP server, in https")) }, Effect.fn("connections.create")(function* ({ server }) {
|
|
837
904
|
const client = yield* ApiClient;
|
|
838
905
|
const callback = yield* loopbackCallback;
|
|
839
906
|
const created = yield* client.connections.create({ payload: {
|
|
@@ -861,14 +928,14 @@ const get$1 = Command.make("get", { id: connectionId }, Effect.fn("connections.g
|
|
|
861
928
|
const client = yield* ApiClient;
|
|
862
929
|
yield* Console.log(yield* encodeConnection(yield* client.connections.get({ params: { id } })));
|
|
863
930
|
})).pipe(Command.withDescription("Show a connection"));
|
|
864
|
-
const
|
|
931
|
+
const deleteConnection = Command.make("delete", { id: connectionId }, Effect.fn("connections.delete")(function* ({ id }) {
|
|
865
932
|
yield* (yield* ApiClient).connections.delete({ params: { id } });
|
|
866
933
|
})).pipe(Command.withDescription("Delete a connection and the access it holds"));
|
|
867
934
|
const connections = Command.make("connections").pipe(Command.withDescription("Manage connections to MCP servers"), Command.withSubcommands([
|
|
868
|
-
|
|
935
|
+
create$1,
|
|
869
936
|
list,
|
|
870
937
|
get$1,
|
|
871
|
-
|
|
938
|
+
deleteConnection
|
|
872
939
|
]));
|
|
873
940
|
//#endregion
|
|
874
941
|
//#region src/runs.ts
|
|
@@ -1003,12 +1070,12 @@ const follow = Command.make("follow", {
|
|
|
1003
1070
|
if ((yield* client.runs.get({ params: { id } })).status === "ended") return;
|
|
1004
1071
|
}
|
|
1005
1072
|
})).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
|
|
1073
|
+
const sendPrompt = Command.make("send-prompt", {
|
|
1007
1074
|
id: runId,
|
|
1008
1075
|
text: Argument.String("text").pipe(Argument.withDescription("Follow-up prompt sent to the agent")),
|
|
1009
1076
|
model: Flag.String("model").pipe(Flag.withAlias("m"), Flag.withDescription("Model for this prompt only"), Flag.optional)
|
|
1010
|
-
}, Effect.fn("runs.
|
|
1011
|
-
const prompt = yield* (yield* ApiClient).runs.
|
|
1077
|
+
}, Effect.fn("runs.sendPrompt")(function* ({ id, text, model }) {
|
|
1078
|
+
const prompt = yield* (yield* ApiClient).runs.sendPrompt({
|
|
1012
1079
|
params: { id },
|
|
1013
1080
|
payload: {
|
|
1014
1081
|
text,
|
|
@@ -1042,7 +1109,7 @@ const cancel = Command.make("cancel", { id: runId }, Effect.fn("runs.cancel")(fu
|
|
|
1042
1109
|
const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), Command.withSubcommands([
|
|
1043
1110
|
create,
|
|
1044
1111
|
get,
|
|
1045
|
-
|
|
1112
|
+
sendPrompt,
|
|
1046
1113
|
prompts,
|
|
1047
1114
|
frames,
|
|
1048
1115
|
events,
|
|
@@ -1063,12 +1130,15 @@ const cli = Command.make("tracks").pipe(Command.withDescription("Run coding agen
|
|
|
1063
1130
|
connections
|
|
1064
1131
|
]));
|
|
1065
1132
|
const ApiClientLayer = Layer.unwrap(Effect.map(readEnv(...CLI_ENV), (env) => ApiClient.layer({
|
|
1066
|
-
baseUrl: env.
|
|
1067
|
-
|
|
1068
|
-
organization: env.AGENT_RUNNER_ORGANIZATION,
|
|
1133
|
+
baseUrl: env.TRACKS_API_URL,
|
|
1134
|
+
apiKey: env.TRACKS_API_KEY,
|
|
1069
1135
|
userAgent: `${CLI_USER_AGENT}/${version}`
|
|
1070
1136
|
}))).pipe(Layer.provide(FetchHttpClient.layer));
|
|
1071
1137
|
const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer);
|
|
1072
|
-
Command.run(cli, { version }).pipe(Effect.
|
|
1138
|
+
Command.run(cli, { version }).pipe(Effect.catch((error) => {
|
|
1139
|
+
const explainedByHelp = error._tag === "ShowHelp";
|
|
1140
|
+
const line = "message" in error ? `${error._tag}: ${error.message}` : error._tag;
|
|
1141
|
+
return Effect.andThen(explainedByHelp ? Effect.void : Console.error(line), Effect.sync(() => void (process.exitCode = 1)));
|
|
1142
|
+
}), Effect.provide(MainLayer), NodeRuntime.runMain);
|
|
1073
1143
|
//#endregion
|
|
1074
1144
|
export {};
|