@alpic-ai/tracks 0.4.1 → 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 +116 -63
- 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({
|
|
@@ -170,8 +169,8 @@ const Agent = Schema.Struct({
|
|
|
170
169
|
const repository = Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "URL of a Git repository to clone before the agent starts." }));
|
|
171
170
|
const MAX_SCRIPT_LENGTH = 32768;
|
|
172
171
|
const script = (description) => Schema.OptionFromOptionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(MAX_SCRIPT_LENGTH)).annotate({ description: `${description} At most ${MAX_SCRIPT_LENGTH} characters.` }));
|
|
173
|
-
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
|
|
174
|
-
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.");
|
|
175
174
|
const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
176
175
|
const RESERVED_VARIABLES = /* @__PURE__ */ new Set(["PATH", "HOME"]);
|
|
177
176
|
const RESERVED_VARIABLE_PREFIXES = [
|
|
@@ -215,7 +214,7 @@ const McpServerInput = Schema.Union([Schema.Struct({
|
|
|
215
214
|
description: "An MCP server given by a connection to it, to pick between several accounts."
|
|
216
215
|
})]).annotate({
|
|
217
216
|
identifier: "McpServerInput",
|
|
218
|
-
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`."
|
|
219
218
|
});
|
|
220
219
|
const McpServersInput = Schema.Array(McpServerInput).check(Schema.isMaxLength(MAX_MCP_SERVERS), Schema.makeFilter((servers) => {
|
|
221
220
|
const names = servers.map((server) => server.name);
|
|
@@ -252,6 +251,26 @@ const RunEnvironment = Schema.Struct({
|
|
|
252
251
|
identifier: "Environment",
|
|
253
252
|
description: "What the agent works on."
|
|
254
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
|
+
})));
|
|
255
274
|
const Budget = Schema.Finite.check(Schema.isBetween({
|
|
256
275
|
minimum: .01,
|
|
257
276
|
maximum: 1e4
|
|
@@ -264,6 +283,7 @@ const CreateRunInput = Schema.Struct({
|
|
|
264
283
|
agent: Agent,
|
|
265
284
|
prompt: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do first." }),
|
|
266
285
|
environment: EnvironmentInput,
|
|
286
|
+
labels: Schema.optionalKey(Labels),
|
|
267
287
|
budget_usd: Schema.OptionFromOptionalKey(Budget),
|
|
268
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`." })),
|
|
269
289
|
idle_timeout_seconds: Schema.OptionFromOptionalKey(Schema.Int.check(Schema.isBetween({
|
|
@@ -275,10 +295,10 @@ const CreateRunInput = Schema.Struct({
|
|
|
275
295
|
maximum: MAX_TIME_LIMIT_SECONDS
|
|
276
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.` }))
|
|
277
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" });
|
|
278
|
-
const
|
|
298
|
+
const SendPromptInput = Schema.Struct({
|
|
279
299
|
text: Schema.NonEmptyString.annotate({ description: "What you ask the agent to do next." }),
|
|
280
300
|
model: Schema.OptionFromOptionalKey(Schema.NonEmptyString.annotate({ description: "Model for this prompt only. Defaults to the run's model." }))
|
|
281
|
-
}).annotate({ identifier: "
|
|
301
|
+
}).annotate({ identifier: "SendPromptInput" });
|
|
282
302
|
const PromptIndex = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).annotate({ description: "Position of the prompt in its run, starting at 1." });
|
|
283
303
|
const promptFields = {
|
|
284
304
|
run_id: RunId,
|
|
@@ -330,6 +350,7 @@ const FailureKind = Schema.Literals([
|
|
|
330
350
|
});
|
|
331
351
|
const runFields = {
|
|
332
352
|
id: RunId,
|
|
353
|
+
labels: Labels.annotate({ description: "Labels attached to the run when it was created." }),
|
|
333
354
|
agent: Agent,
|
|
334
355
|
environment: RunEnvironment,
|
|
335
356
|
budget_usd: Schema.Number.annotate({ description: "Maximum the run may spend, in US dollars." }),
|
|
@@ -381,7 +402,7 @@ const Run = Schema.Union([
|
|
|
381
402
|
]).annotate({ description: "A coding agent working on your prompt in an isolated sandbox." });
|
|
382
403
|
var RunNotFound = class extends Schema.TaggedError()("RunNotFound", { message: Schema.String }, {
|
|
383
404
|
httpApiStatus: 404,
|
|
384
|
-
description: "No run with this id exists
|
|
405
|
+
description: "No run with this id exists in your organization."
|
|
385
406
|
}) {};
|
|
386
407
|
var ConnectionUnusable = class extends Schema.TaggedError()("ConnectionUnusable", { message: Schema.String }, {
|
|
387
408
|
httpApiStatus: 422,
|
|
@@ -395,6 +416,17 @@ var ModelProviderUnavailable = class extends Schema.TaggedError()("ModelProvider
|
|
|
395
416
|
httpApiStatus: 503,
|
|
396
417
|
description: "The model provider could not be reached to check the model. Retry later."
|
|
397
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
|
+
}) {};
|
|
398
430
|
var RunNotIdle = class extends Schema.TaggedError()("RunNotIdle", { message: Schema.String }, {
|
|
399
431
|
httpApiStatus: 409,
|
|
400
432
|
description: "The run accepts no prompt now: it is one-shot, has ended, or is still working on a prompt."
|
|
@@ -438,7 +470,7 @@ const eventFields = {
|
|
|
438
470
|
prompt: PromptIndex.annotate({ description: "Index of the prompt the event belongs to." }),
|
|
439
471
|
captured_at: Schema.DateTimeUtc.annotate({ description: "When the output the event comes from was captured." }),
|
|
440
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." })),
|
|
441
|
-
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." })
|
|
442
474
|
};
|
|
443
475
|
const Event = Schema.Union([
|
|
444
476
|
Schema.Struct({
|
|
@@ -534,18 +566,35 @@ const EventStreamItem = Schema.Union([Schema.Struct({
|
|
|
534
566
|
event: Schema.Literal("heartbeat"),
|
|
535
567
|
data: Schema.Literal("")
|
|
536
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
|
+
}));
|
|
537
574
|
var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("create", "/runs", {
|
|
538
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." })) },
|
|
539
|
-
payload:
|
|
576
|
+
payload: CreateRunInputBesidesStartedBy,
|
|
540
577
|
success: Run.pipe(HttpApiSchema.status(201)),
|
|
541
578
|
error: [
|
|
542
579
|
ModelNotOffered,
|
|
543
580
|
ModelProviderUnavailable,
|
|
544
|
-
ConnectionUnusable
|
|
581
|
+
ConnectionUnusable,
|
|
582
|
+
InsufficientBalance,
|
|
583
|
+
OrganizationBlocked
|
|
545
584
|
]
|
|
546
585
|
}).annotateMerge(OpenApi.annotations({
|
|
547
586
|
summary: "Create a run",
|
|
548
|
-
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`."
|
|
549
598
|
})), HttpApiEndpoint.get("get", "/runs/:id", {
|
|
550
599
|
params: { id: RunId },
|
|
551
600
|
success: Run,
|
|
@@ -558,7 +607,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
558
607
|
success: RunUsage,
|
|
559
608
|
error: RunNotFound
|
|
560
609
|
}).annotateMerge(OpenApi.annotations({
|
|
561
|
-
summary: "Get run usage",
|
|
610
|
+
summary: "Get a run's usage",
|
|
562
611
|
description: "Returns what the run has consumed so far, per model and in sandbox time, and what it cost."
|
|
563
612
|
})), HttpApiEndpoint.post("end", "/runs/:id/end", {
|
|
564
613
|
params: { id: RunId },
|
|
@@ -574,9 +623,9 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
574
623
|
}).annotateMerge(OpenApi.annotations({
|
|
575
624
|
summary: "Cancel a run",
|
|
576
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."
|
|
577
|
-
})), HttpApiEndpoint.post("
|
|
626
|
+
})), HttpApiEndpoint.post("sendPrompt", "/runs/:id/prompts", {
|
|
578
627
|
params: { id: RunId },
|
|
579
|
-
payload:
|
|
628
|
+
payload: SendPromptInput,
|
|
580
629
|
success: Prompt.pipe(HttpApiSchema.status(202)),
|
|
581
630
|
error: [
|
|
582
631
|
RunNotFound,
|
|
@@ -593,7 +642,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
593
642
|
success: List(Prompt),
|
|
594
643
|
error: RunNotFound
|
|
595
644
|
}).annotateMerge(OpenApi.annotations({
|
|
596
|
-
summary: "List run prompts",
|
|
645
|
+
summary: "List a run's prompts",
|
|
597
646
|
description: "Returns the run's prompts in order. To get the next page, pass the `index` of the last prompt received as `after`."
|
|
598
647
|
})), HttpApiEndpoint.get("listFrames", "/runs/:id/frames", {
|
|
599
648
|
params: { id: RunId },
|
|
@@ -604,7 +653,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
604
653
|
success: List(Frame),
|
|
605
654
|
error: RunNotFound
|
|
606
655
|
}).annotateMerge(OpenApi.annotations({
|
|
607
|
-
summary: "List run
|
|
656
|
+
summary: "List a run's frames",
|
|
608
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`."
|
|
609
658
|
})), HttpApiEndpoint.get("listEvents", "/runs/:id/events", {
|
|
610
659
|
params: { id: RunId },
|
|
@@ -615,7 +664,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
615
664
|
success: List(Event),
|
|
616
665
|
error: RunNotFound
|
|
617
666
|
}).annotateMerge(OpenApi.annotations({
|
|
618
|
-
summary: "List run events",
|
|
667
|
+
summary: "List a run's events",
|
|
619
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`."
|
|
620
669
|
})), HttpApiEndpoint.get("followFrames", "/runs/:id/frames/stream", {
|
|
621
670
|
params: { id: RunId },
|
|
@@ -623,7 +672,7 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
623
672
|
success: HttpApiSchema.StreamSse({ events: FrameStreamItem }),
|
|
624
673
|
error: RunNotFound
|
|
625
674
|
}).annotateMerge(OpenApi.annotations({
|
|
626
|
-
summary: "Stream run
|
|
675
|
+
summary: "Stream a run's frames",
|
|
627
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."
|
|
628
677
|
})), HttpApiEndpoint.get("followEvents", "/runs/:id/events/stream", {
|
|
629
678
|
params: { id: RunId },
|
|
@@ -631,41 +680,42 @@ var RunsApi = class extends HttpApiGroup.make("runs").add(HttpApiEndpoint.post("
|
|
|
631
680
|
success: HttpApiSchema.StreamSse({ events: EventStreamItem }),
|
|
632
681
|
error: RunNotFound
|
|
633
682
|
}).annotateMerge(OpenApi.annotations({
|
|
634
|
-
summary: "Stream run events",
|
|
683
|
+
summary: "Stream a run's events",
|
|
635
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."
|
|
636
|
-
}))).
|
|
685
|
+
}))).annotateMerge(OpenApi.annotations({
|
|
637
686
|
title: "Runs",
|
|
638
687
|
description: "Start coding agents and follow their work."
|
|
639
|
-
})) {};
|
|
688
|
+
})).middleware(ApiKeyAuthorization).middleware(RequestValidation) {};
|
|
640
689
|
//#endregion
|
|
641
690
|
//#region ../contract/src/system.ts
|
|
642
691
|
const EgressIps = Schema.Struct({ ips: Schema.Array(Schema.String).annotate({ description: "IPv4 addresses, empty when the API runs locally." }) }).annotate({ identifier: "EgressIps" });
|
|
643
692
|
var SystemApi = class extends HttpApiGroup.make("system", { topLevel: true }).add(HttpApiEndpoint.get("health", "/health", { success: HttpApiSchema.NoContent }).annotateMerge(OpenApi.annotations({
|
|
644
693
|
summary: "Check health",
|
|
645
|
-
description: "Returns 204 when the API is up. No
|
|
694
|
+
description: "Returns 204 when the API is up. No key required."
|
|
646
695
|
}))).add(HttpApiEndpoint.get("egressIps", "/egress-ips", { success: EgressIps }).annotateMerge(OpenApi.annotations({
|
|
647
696
|
summary: "List egress IPs",
|
|
648
|
-
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."
|
|
649
698
|
}))).annotateMerge(OpenApi.annotations({ title: "System" })) {};
|
|
650
699
|
//#endregion
|
|
651
700
|
//#region ../contract/src/api.ts
|
|
652
|
-
var
|
|
653
|
-
title: "
|
|
654
|
-
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."
|
|
655
704
|
})) {};
|
|
656
705
|
//#endregion
|
|
657
706
|
//#region ../contract/src/client.ts
|
|
658
707
|
const CLI_USER_AGENT = "tracks";
|
|
659
708
|
var ApiClient = class ApiClient extends Context.Service()("@tracks/contract/ApiClient") {
|
|
660
|
-
static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(
|
|
709
|
+
static layer = (options) => Layer.effect(ApiClient, HttpApiClient.make(TracksApi, { transformClient: HttpClient.mapRequest((request) => {
|
|
661
710
|
const located = HttpClientRequest.prependUrl(request, options.baseUrl.href);
|
|
662
711
|
return options.userAgent === void 0 ? located : HttpClientRequest.setHeader(located, "user-agent", options.userAgent);
|
|
663
|
-
}) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(
|
|
712
|
+
}) })).pipe(Layer.provide(HttpApiMiddleware.layerClient(ApiKeyAuthorization, ({ next, request }) => next(HttpClientRequest.bearerToken(request, Redacted.value(options.apiKey))))));
|
|
664
713
|
};
|
|
665
714
|
//#endregion
|
|
666
715
|
//#region ../env/src/env.ts
|
|
667
716
|
const Secret = Schema.Redacted(Schema.NonEmptyString);
|
|
668
717
|
const AgentRunnerKey = Schema.Redacted(Schema.NonEmptyString.check(Schema.isMinLength(32)));
|
|
718
|
+
const StripeKey = (prefix) => Schema.Redacted(Schema.String.check(Schema.isStartingWith(prefix)));
|
|
669
719
|
const Port = Schema.Int.check(Schema.isBetween({
|
|
670
720
|
minimum: 1,
|
|
671
721
|
maximum: 65535
|
|
@@ -675,7 +725,7 @@ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
|
675
725
|
"127.0.0.1",
|
|
676
726
|
"[::1]"
|
|
677
727
|
]);
|
|
678
|
-
const
|
|
728
|
+
const TRACKS_API_PATH = "/api";
|
|
679
729
|
const SecureUrl = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" || LOOPBACK_HOSTS.has(url.hostname) || "must use https"));
|
|
680
730
|
const ENCRYPTION_KEY_BYTES = 32;
|
|
681
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}`)));
|
|
@@ -691,6 +741,7 @@ const IpAddresses = Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.Strin
|
|
|
691
741
|
const SentryDsn = Schema.URLFromString.check(Schema.makeFilter((url) => url.protocol === "https:" && url.username !== "" || "must be a Sentry DSN"));
|
|
692
742
|
const Secrets = Schema.Struct({
|
|
693
743
|
AGENT_RUNNER_KEY: AgentRunnerKey,
|
|
744
|
+
TRACKS_API_KEY: Secret,
|
|
694
745
|
DAYTONA_API_KEY: Secret,
|
|
695
746
|
E2B_API_KEY: Secret,
|
|
696
747
|
ANTHROPIC_API_KEY: Secret,
|
|
@@ -700,12 +751,13 @@ const Secrets = Schema.Struct({
|
|
|
700
751
|
TYPESAFE_API_KEY: Secret,
|
|
701
752
|
DATABASE_PASSWORD: Secret,
|
|
702
753
|
CONNECTION_ENCRYPTION_KEY: EncryptionKey,
|
|
754
|
+
STRIPE_SECRET_KEY: StripeKey("rk_"),
|
|
755
|
+
STRIPE_WEBHOOK_SECRET: StripeKey("whsec_"),
|
|
703
756
|
SENTRY_DSN: Schema.optionalKey(Schema.Redacted(SentryDsn))
|
|
704
757
|
});
|
|
705
758
|
const Variables = Schema.Struct({
|
|
706
759
|
PORT: Port.pipe(Schema.withDecodingDefaultKey(Effect.succeed(3e3))),
|
|
707
|
-
|
|
708
|
-
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}`))),
|
|
709
761
|
DAYTONA_API_URL: Schema.NonEmptyString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("https://app.daytona.io/api"))),
|
|
710
762
|
AGENT_RUNNER_URL: SecureUrl,
|
|
711
763
|
CONNECTION_RETURN_ORIGINS: Origins.pipe(Schema.withDecodingDefaultKey(Effect.succeed(""))),
|
|
@@ -754,6 +806,7 @@ const CONNECTIONS_ENV = [
|
|
|
754
806
|
"CONNECTION_RETURN_ORIGINS",
|
|
755
807
|
"CONNECTION_ENCRYPTION_KEY"
|
|
756
808
|
];
|
|
809
|
+
const PAYMENTS_ENV = ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"];
|
|
757
810
|
const AGENT_RUNNER_ENV = [
|
|
758
811
|
...DATABASE_ENV,
|
|
759
812
|
...DAYTONA_ENV,
|
|
@@ -773,19 +826,16 @@ const SERVICE_ENV = [
|
|
|
773
826
|
"EGRESS_IPS",
|
|
774
827
|
...LOG_ENV,
|
|
775
828
|
...OBSERVABILITY_ENV,
|
|
776
|
-
...AGENT_RUNNER_ENV
|
|
777
|
-
|
|
778
|
-
const CLI_ENV = [
|
|
779
|
-
"AGENT_RUNNER_KEY",
|
|
780
|
-
"AGENT_RUNNER_ORGANIZATION",
|
|
781
|
-
"AGENT_RUNNER_API_URL"
|
|
829
|
+
...AGENT_RUNNER_ENV,
|
|
830
|
+
...PAYMENTS_ENV
|
|
782
831
|
];
|
|
832
|
+
const CLI_ENV = ["TRACKS_API_KEY", "TRACKS_API_URL"];
|
|
783
833
|
const isDatabaseVariable = (name) => DATABASE_ENV.some((databaseName) => databaseName === name);
|
|
784
834
|
SERVICE_ENV.flatMap((name) => isSecret(name) && !isDatabaseVariable(name) ? [name] : []);
|
|
785
835
|
const readEnv = (...names) => Config.schema(Env.mapFields(Struct.pick(names)));
|
|
786
836
|
//#endregion
|
|
787
837
|
//#region package.json
|
|
788
|
-
var version = "0.4.
|
|
838
|
+
var version = "0.4.2";
|
|
789
839
|
//#endregion
|
|
790
840
|
//#region src/connections.ts
|
|
791
841
|
const encodeConnection = Schema.encodeEffect(Schema.fromJsonString(Connection, { space: 2 }));
|
|
@@ -794,7 +844,7 @@ const connectionId = Argument.String("id").pipe(Argument.withDescription("Connec
|
|
|
794
844
|
const LOOPBACK = "127.0.0.1";
|
|
795
845
|
const CALLBACK_PATH = "/callback";
|
|
796
846
|
const CONSENT_TIMEOUT = "10 minutes";
|
|
797
|
-
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>";
|
|
798
848
|
var ConsentFailed = class extends Schema.TaggedError()("ConsentFailed", { reason: Schema.String }) {
|
|
799
849
|
get message() {
|
|
800
850
|
return `The connection did not complete: ${this.reason}`;
|
|
@@ -850,7 +900,7 @@ const confirmationOf = (params, id) => {
|
|
|
850
900
|
onNone: () => Effect.fail(new ConsentFailed({ reason: params.get("error") ?? "no confirmation came back" }))
|
|
851
901
|
});
|
|
852
902
|
};
|
|
853
|
-
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 }) {
|
|
854
904
|
const client = yield* ApiClient;
|
|
855
905
|
const callback = yield* loopbackCallback;
|
|
856
906
|
const created = yield* client.connections.create({ payload: {
|
|
@@ -878,14 +928,14 @@ const get$1 = Command.make("get", { id: connectionId }, Effect.fn("connections.g
|
|
|
878
928
|
const client = yield* ApiClient;
|
|
879
929
|
yield* Console.log(yield* encodeConnection(yield* client.connections.get({ params: { id } })));
|
|
880
930
|
})).pipe(Command.withDescription("Show a connection"));
|
|
881
|
-
const
|
|
931
|
+
const deleteConnection = Command.make("delete", { id: connectionId }, Effect.fn("connections.delete")(function* ({ id }) {
|
|
882
932
|
yield* (yield* ApiClient).connections.delete({ params: { id } });
|
|
883
933
|
})).pipe(Command.withDescription("Delete a connection and the access it holds"));
|
|
884
934
|
const connections = Command.make("connections").pipe(Command.withDescription("Manage connections to MCP servers"), Command.withSubcommands([
|
|
885
|
-
|
|
935
|
+
create$1,
|
|
886
936
|
list,
|
|
887
937
|
get$1,
|
|
888
|
-
|
|
938
|
+
deleteConnection
|
|
889
939
|
]));
|
|
890
940
|
//#endregion
|
|
891
941
|
//#region src/runs.ts
|
|
@@ -1020,12 +1070,12 @@ const follow = Command.make("follow", {
|
|
|
1020
1070
|
if ((yield* client.runs.get({ params: { id } })).status === "ended") return;
|
|
1021
1071
|
}
|
|
1022
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"));
|
|
1023
|
-
const
|
|
1073
|
+
const sendPrompt = Command.make("send-prompt", {
|
|
1024
1074
|
id: runId,
|
|
1025
1075
|
text: Argument.String("text").pipe(Argument.withDescription("Follow-up prompt sent to the agent")),
|
|
1026
1076
|
model: Flag.String("model").pipe(Flag.withAlias("m"), Flag.withDescription("Model for this prompt only"), Flag.optional)
|
|
1027
|
-
}, Effect.fn("runs.
|
|
1028
|
-
const prompt = yield* (yield* ApiClient).runs.
|
|
1077
|
+
}, Effect.fn("runs.sendPrompt")(function* ({ id, text, model }) {
|
|
1078
|
+
const prompt = yield* (yield* ApiClient).runs.sendPrompt({
|
|
1029
1079
|
params: { id },
|
|
1030
1080
|
payload: {
|
|
1031
1081
|
text,
|
|
@@ -1059,7 +1109,7 @@ const cancel = Command.make("cancel", { id: runId }, Effect.fn("runs.cancel")(fu
|
|
|
1059
1109
|
const runs = Command.make("runs").pipe(Command.withDescription("Manage runs"), Command.withSubcommands([
|
|
1060
1110
|
create,
|
|
1061
1111
|
get,
|
|
1062
|
-
|
|
1112
|
+
sendPrompt,
|
|
1063
1113
|
prompts,
|
|
1064
1114
|
frames,
|
|
1065
1115
|
events,
|
|
@@ -1080,12 +1130,15 @@ const cli = Command.make("tracks").pipe(Command.withDescription("Run coding agen
|
|
|
1080
1130
|
connections
|
|
1081
1131
|
]));
|
|
1082
1132
|
const ApiClientLayer = Layer.unwrap(Effect.map(readEnv(...CLI_ENV), (env) => ApiClient.layer({
|
|
1083
|
-
baseUrl: env.
|
|
1084
|
-
|
|
1085
|
-
organization: env.AGENT_RUNNER_ORGANIZATION,
|
|
1133
|
+
baseUrl: env.TRACKS_API_URL,
|
|
1134
|
+
apiKey: env.TRACKS_API_KEY,
|
|
1086
1135
|
userAgent: `${CLI_USER_AGENT}/${version}`
|
|
1087
1136
|
}))).pipe(Layer.provide(FetchHttpClient.layer));
|
|
1088
1137
|
const MainLayer = Layer.mergeAll(ApiClientLayer, NodeServices.layer);
|
|
1089
|
-
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);
|
|
1090
1143
|
//#endregion
|
|
1091
1144
|
export {};
|