@coreplane/switchboard 1.265.1 → 1.266.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 (28) hide show
  1. package/dist/assets/.env.example +6 -0
  2. package/dist/assets/deploy/cloudflare/depotCi.ts +246 -0
  3. package/dist/assets/deploy/cloudflare/worker.ts +16 -0
  4. package/dist/assets/deploy/secrets.manifest.json +13 -1
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +1 -1
  7. package/dist/assets/source.json +3 -3
  8. package/dist/assets/src/core/depotCi.ts +72 -0
  9. package/dist/assets/src/core/refusal.ts +1 -0
  10. package/dist/assets/web/dist/.vite/manifest.json +30 -30
  11. package/dist/assets/web/dist/assets/{HomePage-CBQpcnaW.js → HomePage-B2Y-Fa9V.js} +1 -1
  12. package/dist/assets/web/dist/assets/{ResidentDetailPage-C-SgOUTl.js → ResidentDetailPage-DRNwx2--.js} +1 -1
  13. package/dist/assets/web/dist/assets/{ResidentsIndexPage-CiDhvXok.js → ResidentsIndexPage-EHKMtX69.js} +1 -1
  14. package/dist/assets/web/dist/assets/{RunFoldRow-DZ8huwUU.js → RunFoldRow-CAdpX4Gn.js} +1 -1
  15. package/dist/assets/web/dist/assets/{RunRoutePage-Cp6wicYc.js → RunRoutePage-DppLhu79.js} +3 -3
  16. package/dist/assets/web/dist/assets/{RunsIndexPage-BPXtfwwx.js → RunsIndexPage-C7hx5O1X.js} +1 -1
  17. package/dist/assets/web/dist/assets/{ScheduledPage-DbvP48HI.js → ScheduledPage-Dk2kMnYT.js} +1 -1
  18. package/dist/assets/web/dist/assets/{SettingsPage-B87lZaiN.js → SettingsPage-DAf6R383.js} +1 -1
  19. package/dist/assets/web/dist/assets/{StatusDot-CFLn-DZ2.js → StatusDot-IkfsHGXg.js} +1 -1
  20. package/dist/assets/web/dist/assets/{Tooltip-Dx9YBVEx.js → Tooltip-CmQVfAl2.js} +1 -1
  21. package/dist/assets/web/dist/assets/{UnitRoutePage-d4A2X_o7.js → UnitRoutePage-BjfuZVWr.js} +1 -1
  22. package/dist/assets/web/dist/assets/{dist-Cn51vWpw.js → dist-Dg6HTzpQ.js} +1 -1
  23. package/dist/assets/web/dist/assets/main-Bk1L-bNk.js +28 -0
  24. package/dist/assets/web/dist/assets/main-DPB0UO1-.css +1 -0
  25. package/dist/cli.js +1133 -636
  26. package/package.json +1 -1
  27. package/dist/assets/web/dist/assets/main-DGC6WFqS.css +0 -1
  28. package/dist/assets/web/dist/assets/main-DYwRHh0F.js +0 -28
@@ -33,3 +33,9 @@ E2B_API_KEY=e2b_...
33
33
  # GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
34
34
  # (b) Fallback: a fine-grained PAT scoped to only the repos the bot may touch.
35
35
  # GH_TOKEN=github_pat_...
36
+
37
+ # Depot CI bridge (optional; bot only). Depot's organization token must NOT be
38
+ # in this file or any container: provision DEPOT_API_TOKEN only on the Worker.
39
+ # Provision a separate internal bearer on that Worker; it forwards this one to
40
+ # the bot. PUBLIC_BASE_URL must be the Worker's HTTPS origin. No Depot CLI needed.
41
+ # DEPOT_CI_BRIDGE_TOKEN=...
@@ -0,0 +1,246 @@
1
+ // Depot's organization token belongs to this Worker alone. The bot sends a
2
+ // repo-bound, typed operation; this adapter resolves ownership before spending
3
+ // that credential on logs or a mutation. Nothing here executes repository code.
4
+ import { z } from "zod";
5
+ import { MINUTE_MS } from "../../src/core/budgets.ts";
6
+ import {
7
+ DEPOT_CI_AUTHORIZATION_PATH,
8
+ DEPOT_CI_MAX_BYTES,
9
+ DEPOT_ERRORS,
10
+ depotCiGrantSchema,
11
+ depotCiTicketSchema,
12
+ depotId,
13
+ depotJson,
14
+ } from "../../src/core/depotCi.ts";
15
+ import { redactSecrets, stripAnsi } from "../../src/core/redact.ts";
16
+ import { sameToken } from "./artifactsCopy.ts";
17
+ import { INTERNAL } from "./shared.ts";
18
+
19
+ const attemptSchema = z.object({ attemptId: depotId, attempt: z.number().int().positive(), status: z.string() });
20
+ const workflowSchema = z.object({
21
+ orgId: depotId,
22
+ runId: depotId,
23
+ repo: z.string(),
24
+ workflowId: depotId,
25
+ headSha: z.string().default(""),
26
+ sha: z.string().default(""),
27
+ workflowStatus: z.string(),
28
+ workflowName: z.string().default(""),
29
+ workflowPath: z.string().default(""),
30
+ workflowErrorMessage: z.string().default(""),
31
+ jobs: z
32
+ .array(
33
+ z.object({
34
+ jobId: depotId,
35
+ jobKey: z.string(),
36
+ jobDisplayName: z.string().default(""),
37
+ status: z.string(),
38
+ attempts: z.array(attemptSchema).default([]),
39
+ }),
40
+ )
41
+ .default([]),
42
+ });
43
+ const logsSchema = z.object({
44
+ lines: z.array(z.object({ body: z.string(), stepName: z.string().default("") })).default([]),
45
+ nextPageToken: z.string().max(8192).default(""),
46
+ });
47
+ const retrySchema = z.object({
48
+ workflowId: depotId,
49
+ jobIds: z.array(depotId),
50
+ jobCount: z.number().int().nonnegative(),
51
+ });
52
+ /** The callback has no caller-controlled origin or path. Worker wiring supplies
53
+ * the singleton container binding, NOT an Internet fetch or a redirect. */
54
+ export async function depotCiAuthorization(
55
+ ticket: string,
56
+ signal: AbortSignal,
57
+ botFetch: (request: Request) => Promise<Response>,
58
+ ): Promise<unknown> {
59
+ const res = await botFetch(
60
+ new Request(`${INTERNAL}${DEPOT_CI_AUTHORIZATION_PATH}`, {
61
+ method: "POST",
62
+ redirect: "error",
63
+ signal,
64
+ headers: { authorization: `Bearer ${ticket}` },
65
+ }),
66
+ );
67
+ if (res.status !== 200) {
68
+ await res.body?.cancel();
69
+ return undefined;
70
+ }
71
+ return depotJson(res, 16 * 1024);
72
+ }
73
+ const json = (status: number, body: unknown) =>
74
+ new Response(JSON.stringify(body), {
75
+ status,
76
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
77
+ });
78
+
79
+ export async function handleDepotCi(
80
+ request: Request,
81
+ deps: {
82
+ depotToken?: string;
83
+ bridgeToken?: string;
84
+ authorize?: (ticket: string, signal: AbortSignal) => Promise<unknown>;
85
+ fetch: typeof fetch;
86
+ },
87
+ ): Promise<Response> {
88
+ const bearer = /^Bearer\s+(\S+)$/.exec(request.headers.get("authorization") ?? "")?.[1];
89
+ if (!deps.bridgeToken || !bearer || !sameToken(bearer, deps.bridgeToken))
90
+ return json(401, { error: "Depot CI bridge bearer refused." });
91
+ if (request.method !== "POST") return json(405, { error: "Depot CI requires POST." });
92
+ let raw: unknown;
93
+ try {
94
+ raw = await depotJson(new Response(request.body), 16 * 1024);
95
+ } catch {
96
+ return json(400, { error: DEPOT_ERRORS.invalid });
97
+ }
98
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return json(400, { error: DEPOT_ERRORS.invalid });
99
+ const parsed = depotCiTicketSchema.safeParse(raw);
100
+ if (!parsed.success) return json(400, { error: DEPOT_ERRORS.invalid });
101
+ const signal = AbortSignal.any([request.signal, AbortSignal.timeout(MINUTE_MS)]);
102
+ let grant;
103
+ try {
104
+ signal.throwIfAborted();
105
+ grant = depotCiGrantSchema.safeParse(await deps.authorize?.(parsed.data.ticket, signal));
106
+ } catch {
107
+ return json(403, { error: DEPOT_ERRORS.unauthorized });
108
+ }
109
+ if (!grant.success) return json(403, { error: DEPOT_ERRORS.unauthorized });
110
+ if (!deps.depotToken) return json(503, { error: DEPOT_ERRORS.unavailable });
111
+ // Both repo and operation came from a one-use permit the authenticated run's
112
+ // tool call registered in the bot. Possessing the bridge bearer cannot mint
113
+ // one, choose another repo, change its head, or replay it.
114
+ const { repo, operation: op } = grant.data;
115
+ // Credentials are redacted as exact values too: not every vendor token has a
116
+ // recognizable prefix. Strip control sequences before either redaction pass.
117
+ const sanitize = (text: string) =>
118
+ redactSecrets(
119
+ stripAnsi(text)
120
+ .split(deps.depotToken!)
121
+ .join("[redacted]")
122
+ .split(deps.bridgeToken!)
123
+ .join("[redacted]")
124
+ .split(parsed.data.ticket)
125
+ .join("[redacted]"),
126
+ );
127
+ const safeJson = (value: unknown): unknown => {
128
+ if (typeof value === "string") return sanitize(value);
129
+ if (Array.isArray(value)) return value.map(safeJson);
130
+ if (value && typeof value === "object")
131
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, safeJson(entry)]));
132
+ return value;
133
+ };
134
+ let mutationStarted = false;
135
+ let upstreamStatus: number | undefined;
136
+ const rpc = async (method: "GetWorkflow" | "GetJobAttemptLogs" | "RetryFailedJobs", body: object) => {
137
+ signal.throwIfAborted();
138
+ if (method === "RetryFailedJobs") mutationStarted = true;
139
+ const res = await deps.fetch(`https://api.depot.dev/depot.ci.v1.CIService/${method}`, {
140
+ method: "POST",
141
+ redirect: "error",
142
+ signal,
143
+ headers: {
144
+ authorization: `Bearer ${deps.depotToken}`,
145
+ "content-type": "application/json",
146
+ "connect-protocol-version": "1",
147
+ },
148
+ body: JSON.stringify(body),
149
+ });
150
+ if (!res.ok) {
151
+ upstreamStatus = res.status;
152
+ await res.body?.cancel();
153
+ throw new Error("upstream refused");
154
+ }
155
+ return depotJson(res);
156
+ };
157
+ try {
158
+ const workflow = workflowSchema.parse(await rpc("GetWorkflow", { workflowId: op.workflowId }));
159
+ if (workflow.repo.toLowerCase() !== repo.toLowerCase() || workflow.workflowId !== op.workflowId)
160
+ return json(404, { error: DEPOT_ERRORS.notFound });
161
+ if (op.operation === "inspect") return json(200, safeJson(workflow));
162
+ if (op.operation === "retry_failed") {
163
+ const head = workflow.headSha || workflow.sha;
164
+ if (
165
+ head.toLowerCase() !== op.expectedHead.toLowerCase() ||
166
+ !["failed", "cancelled"].includes(workflow.workflowStatus)
167
+ )
168
+ return json(409, { error: DEPOT_ERRORS.stale });
169
+ const answer = retrySchema.parse(await rpc("RetryFailedJobs", { workflowId: op.workflowId }));
170
+ if (answer.workflowId !== op.workflowId || answer.jobCount !== answer.jobIds.length)
171
+ throw new Error("inconsistent retry response");
172
+ return json(200, safeJson(answer));
173
+ }
174
+ const jobs = workflow.jobs.filter((j) => j.jobId === op.jobId);
175
+ if (jobs.length !== 1) return json(404, { error: DEPOT_ERRORS.notFound });
176
+ const attempts = jobs[0].attempts;
177
+ const latest = Math.max(...attempts.map((a) => a.attempt));
178
+ const selected = attempts.filter((a) => (op.attemptId ? a.attemptId === op.attemptId : a.attempt === latest));
179
+ if (selected.length !== 1) return json(404, { error: DEPOT_ERRORS.notFound });
180
+ const attemptId = selected[0].attemptId;
181
+ let pageToken: string | undefined;
182
+ const seen = new Set<string>();
183
+ const records: string[] = [];
184
+ let capturedChars = 0;
185
+ let complete = false;
186
+ let truncated = false;
187
+ const lineLimit = op.lines ?? 200;
188
+ for (let page = 0; page < 20; page++) {
189
+ const result = logsSchema.parse(
190
+ await rpc("GetJobAttemptLogs", { attemptId, ...(pageToken ? { pageToken } : {}) }),
191
+ );
192
+ // Keep the finite raw capture until all boundaries can be sanitized.
193
+ // A separate aggregate ceiling bounds Worker memory, not just each page.
194
+ const chars = result.lines.reduce((sum, line) => sum + line.body.length + 1, 0);
195
+ if (capturedChars + chars > 2 * DEPOT_CI_MAX_BYTES) break;
196
+ for (const line of result.lines) records.push(line.body);
197
+ capturedChars += chars;
198
+ if (!result.nextPageToken) {
199
+ complete = true;
200
+ break;
201
+ }
202
+ if (seen.has(result.nextPageToken)) break;
203
+ seen.add(result.nextPageToken);
204
+ pageToken = result.nextPageToken;
205
+ }
206
+ // Depot records/pages may split a credential. Compare both interpretations
207
+ // of their separators BEFORE clipping: if removing newlines changes what is
208
+ // secret, withhold the capture rather than guessing which fragment is safe.
209
+ // Partial captures are withheld too: a later page could finish a credential
210
+ // or a PEM block. This fails closed without retaining unbounded raw logs.
211
+ const rawText = complete ? records.join("\n") : "";
212
+ const sanitized = sanitize(rawText);
213
+ const withheld = !complete
214
+ ? "incomplete capture"
215
+ : sanitized.replace(/\n/g, "") !== sanitize(rawText.replace(/\n/g, ""))
216
+ ? "credential spans log records"
217
+ : undefined;
218
+ let text = "";
219
+ if (!withheld) {
220
+ const lines = sanitized.split("\n");
221
+ truncated = lines.length > lineLimit;
222
+ text = lines.slice(-lineLimit).join("\n");
223
+ if (text.length > 50_000) {
224
+ text = text.slice(-50_000);
225
+ truncated = true;
226
+ }
227
+ }
228
+ return json(200, {
229
+ workflowId: op.workflowId,
230
+ jobId: op.jobId,
231
+ attemptId,
232
+ complete,
233
+ truncated: truncated || !!withheld,
234
+ ...(withheld ? { withheld } : {}),
235
+ text,
236
+ });
237
+ } catch {
238
+ return json(502, {
239
+ error: mutationStarted
240
+ ? DEPOT_ERRORS.unknown
241
+ : upstreamStatus
242
+ ? `Depot CI upstream refused (HTTP ${upstreamStatus}).`
243
+ : DEPOT_ERRORS.failed,
244
+ });
245
+ }
246
+ }
@@ -1,3 +1,5 @@
1
+ import { depotCiAuthorization, handleDepotCi } from "./depotCi.ts";
2
+ import { DEPOT_CI_AUTHORIZATION_PATH, DEPOT_CI_PATH } from "../../src/core/depotCi.ts";
1
3
  // Cloudflare Containers shim: runs the unchanged Switchboard image as a single
2
4
  // always-on container instance — the shape of any long-lived server on
3
5
  // Cloudflare Containers (singleton DO, cron keep-alive;
@@ -83,6 +85,8 @@ export interface Env {
83
85
  GITHUB_APP_ID?: string;
84
86
  GITHUB_APP_INSTALLATION_ID?: string;
85
87
  GITHUB_APP_PRIVATE_KEY?: string;
88
+ DEPOT_API_TOKEN?: string; // Worker-only organization credential: NEVER forwarded to any container
89
+ DEPOT_CI_BRIDGE_TOKEN?: string; // bot → Worker, narrow repo-bound CI operations only
86
90
  PUBLIC_BASE_URL?: string; // live-view: base for /runs/<id>?t=… links on the status card
87
91
  ACCESS_TEAM_DOMAIN?: string; // live-view SSO gate: Cloudflare Access team domain (JWKS + iss)
88
92
  ACCESS_AUD?: string; // live-view SSO gate: Cloudflare Access application AUD tag
@@ -124,6 +128,7 @@ const FORWARDED_OPTIONAL = [
124
128
  "GITHUB_APP_ID",
125
129
  "GITHUB_APP_INSTALLATION_ID",
126
130
  "GITHUB_APP_PRIVATE_KEY",
131
+ "DEPOT_CI_BRIDGE_TOKEN",
127
132
  "PUBLIC_BASE_URL",
128
133
  "ACCESS_TEAM_DOMAIN",
129
134
  "ACCESS_AUD",
@@ -458,6 +463,17 @@ const withLength = (res: Response): Response => withKnownLength(res, (size) => n
458
463
  export default {
459
464
  async fetch(request: Request, env: Env): Promise<Response> {
460
465
  const pathname = new URL(request.url).pathname;
466
+ // Permits can only be consumed through the Worker's fixed container
467
+ // binding. The public edge must never proxy this callback path.
468
+ if (pathname === DEPOT_CI_AUTHORIZATION_PATH) return new Response("not found", { status: 404 });
469
+ if (pathname === DEPOT_CI_PATH)
470
+ return handleDepotCi(request, {
471
+ depotToken: env.DEPOT_API_TOKEN,
472
+ bridgeToken: env.DEPOT_CI_BRIDGE_TOKEN,
473
+ authorize: (ticket, signal) =>
474
+ depotCiAuthorization(ticket, signal, (req) => getContainer(env.SWITCHBOARD, INSTANCE).fetch(req)),
475
+ fetch: (input, init) => fetch(input, init),
476
+ });
461
477
  // Only deliberately published copies are public. The existing run reader
462
478
  // and the bucket stay private; the write endpoint checks its own bearer.
463
479
  if (pathname.startsWith("/pr-images/")) return withLength(await handlePrImage(request, env.ARTIFACTS));
@@ -1,6 +1,18 @@
1
1
  {
2
- "$comment": "Every Cloudflare Worker secret Switchboard provisions: its name, which Worker(s) hold it, and whether a Worker may go without it (`optional: true` everywhere, or a list of the Workers it is optional on — a shared bearer is required on the Worker that serves the feature and optional on the bot until that feature is configured). This file is the contract — src/core/secretsManifest.test.ts keeps it equal to each worker.ts `Env` interface and to the bot container's forwarding list. WHERE the values live is the deployment profile's `secretsSource` (a directory of <NAME> files, `~/.secrets/switchboard` by default, or a 1Password item `op://Vault/Item` with one field per name); `deploy secrets <worker>` (`npm run secrets` in each deploy/cloudflare*/) puts them, refusing before any upload when a required value is absent. A shared bearer must carry ONE value on every Worker listed for it. Rotation: new value at the source, `deploy secrets` on EVERY listed Worker, then `deploy restart` for the bot (its running container keeps the env it started with). Public config (URLs, ids of no consequence) is a wrangler.jsonc var, not a secret.",
2
+ "$comment": "Every Cloudflare Worker secret Switchboard provisions: its name, which Worker(s) hold it, and whether a Worker may go without it (`optional: true` everywhere, or a list of the Workers it is optional on — a shared bearer is required on the Worker that serves the feature and optional on the bot until that feature is configured). This file is the contract — src/core/secretsManifest.test.ts keeps it equal to each worker.ts `Env` interface and to the bot container's forwarding list, except the explicitly Worker-only DEPOT_API_TOKEN (which must never be forwarded). WHERE the values live is the deployment profile's `secretsSource` (a directory of <NAME> files, `~/.secrets/switchboard` by default, or a 1Password item `op://Vault/Item` with one field per name); `deploy secrets <worker>` (`npm run secrets` in each deploy/cloudflare*/) puts them, refusing before any upload when a required value is absent. A shared bearer must carry ONE value on every Worker listed for it. Rotation: new value at the source, `deploy secrets` on EVERY listed Worker, then `deploy restart` for the bot (its running container keeps the env it started with). Public config (URLs, ids of no consequence) is a wrangler.jsonc var, not a secret.",
3
3
  "secrets": [
4
+ {
5
+ "name": "DEPOT_API_TOKEN",
6
+ "workers": ["bot"],
7
+ "optional": true,
8
+ "note": "Depot organization token, held ONLY by the bot Worker edge. Explicitly excluded from bot container forwarding; never put in a bot env, coding container, resident or shell. Used only for repo-verified workflow inspection, logs and failed-only retry. Rotate in Depot organization settings, provision with deploy secrets bot."
9
+ },
10
+ {
11
+ "name": "DEPOT_CI_BRIDGE_TOKEN",
12
+ "workers": ["bot"],
13
+ "optional": true,
14
+ "note": "Separate self-minted internal bearer: trusted bot → its Worker's narrow /internal/depot-ci operation. Forwarded to the bot only, never model containers. This is NOT a Depot credential. Absent disables CI tools; provision with deploy secrets bot and restart the bot."
15
+ },
4
16
  {
5
17
  "name": "SLACK_BOT_TOKEN",
6
18
  "workers": ["bot"],
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.265.1",
3
+ "version": "1.266.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "switchboard",
9
- "version": "1.265.1",
9
+ "version": "1.266.0",
10
10
  "license": "Apache-2.0",
11
11
  "workspaces": [
12
12
  "web",
@@ -21060,7 +21060,7 @@
21060
21060
  },
21061
21061
  "packages/switchboard": {
21062
21062
  "name": "@coreplane/switchboard",
21063
- "version": "1.265.1",
21063
+ "version": "1.266.0",
21064
21064
  "license": "Apache-2.0",
21065
21065
  "dependencies": {
21066
21066
  "@earendil-works/pi-ai": "0.85.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchboard",
3
- "version": "1.265.1",
3
+ "version": "1.266.0",
4
4
  "private": true,
5
5
  "description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.265.1",
3
- "commit": "dccb10815d97823de2be35d9768379a2d9ff6aaf",
4
- "builtAt": "2026-09-26T14:55:53.149Z"
2
+ "version": "1.266.0",
3
+ "commit": "9b03a2a996929a0e6f8604a7ea830106b0f37e53",
4
+ "builtAt": "2026-09-26T17:09:46.306Z"
5
5
  }
@@ -0,0 +1,72 @@
1
+ // The node-free contract shared by the bot's repo-bound client and the edge.
2
+ // No generic RPC, arbitrary URL or credential can be supplied by a child.
3
+ import { z } from "zod";
4
+
5
+ export const DEPOT_CI_PATH = "/internal/depot-ci";
6
+ export const DEPOT_CI_AUTHORIZATION_PATH = "/internal/depot-ci/authorization";
7
+ export const depotCiTicketSchema = z.object({ ticket: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
8
+ export const DEPOT_CI_MAX_BYTES = 2 * 1024 * 1024;
9
+ export const depotId = z.string().regex(/^[A-Za-z0-9_-]{1,160}$/);
10
+ const common = { workflowId: depotId };
11
+ export const depotOperationSchema = z.discriminatedUnion("operation", [
12
+ z.object({ ...common, operation: z.literal("inspect") }).strict(),
13
+ z
14
+ .object({
15
+ ...common,
16
+ operation: z.literal("logs"),
17
+ jobId: depotId,
18
+ attemptId: depotId.optional(),
19
+ lines: z.number().int().min(1).max(2000).optional(),
20
+ })
21
+ .strict(),
22
+ z
23
+ .object({ ...common, operation: z.literal("retry_failed"), expectedHead: z.string().regex(/^[a-f0-9]{40}$/i) })
24
+ .strict(),
25
+ ]);
26
+ export type DepotOperation = z.infer<typeof depotOperationSchema>;
27
+ /** Read only from the fixed bot callback, never from the bridge request. */
28
+ export const depotCiGrantSchema = z
29
+ .object({
30
+ runId: depotId,
31
+ repo: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/),
32
+ operation: depotOperationSchema,
33
+ })
34
+ .strict();
35
+ export type DepotCiGrant = z.infer<typeof depotCiGrantSchema>;
36
+ export interface DepotCi {
37
+ call(operation: DepotOperation, signal?: AbortSignal): Promise<Record<string, unknown>>;
38
+ }
39
+
40
+ /** Fixed messages shared across the bridge; upstream bodies are never errors. */
41
+ export const DEPOT_ERRORS = {
42
+ invalid: "Depot CI: invalid operation.",
43
+ unauthorized: "Depot CI: run authorization expired or refused.",
44
+ unavailable: "Depot CI unavailable: DEPOT_API_TOKEN is not configured at the Worker.",
45
+ notFound: "Depot CI: workflow, job or attempt not found in the bound repository.",
46
+ stale: "Depot CI: head mismatch or workflow is not failed/cancelled; inspect before retry.",
47
+ unknown: "Depot retry outcome unknown; inspect before another request.",
48
+ failed: "Depot CI upstream response invalid or unavailable.",
49
+ } as const;
50
+
51
+ /** Read through a byte ceiling, including chunked responses. A partial JSON
52
+ * response is never parsed or returned. Callers supply the operation deadline. */
53
+ export async function depotJson(response: Response, limit = DEPOT_CI_MAX_BYTES): Promise<unknown> {
54
+ if (!response.body) throw new Error("empty response");
55
+ const reader = response.body.getReader();
56
+ const decoder = new TextDecoder();
57
+ let bytes = 0;
58
+ let text = "";
59
+ try {
60
+ for (;;) {
61
+ const { done, value } = await reader.read();
62
+ if (done) break;
63
+ bytes += value.byteLength;
64
+ if (bytes > limit) throw new Error("response too large");
65
+ text += decoder.decode(value, { stream: true });
66
+ }
67
+ return JSON.parse(text + decoder.decode()) as unknown;
68
+ } finally {
69
+ await reader.cancel().catch(() => {});
70
+ reader.releaseLock();
71
+ }
72
+ }
@@ -45,6 +45,7 @@ const CAUSE_OF = {
45
45
  repo_not_visible: "policy",
46
46
  repo_unverified: "system",
47
47
  repo_not_onboarded: "request",
48
+ pr_target_conflict: "request",
48
49
  repo_access: "policy",
49
50
  pr_head_unknown: "system",
50
51
  workspace_head_mismatch: "system",
@@ -28,7 +28,7 @@
28
28
  "src": "../node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-normal.woff2"
29
29
  },
30
30
  "../node_modules/@pierre/diffs/dist/index.js": {
31
- "file": "assets/dist-Cn51vWpw.js",
31
+ "file": "assets/dist-Dg6HTzpQ.js",
32
32
  "name": "dist",
33
33
  "src": "../node_modules/@pierre/diffs/dist/index.js",
34
34
  "isDynamicEntry": true,
@@ -2581,8 +2581,8 @@
2581
2581
  "_sseReplay-DGRDRLua.js"
2582
2582
  ]
2583
2583
  },
2584
- "_RunFoldRow-DZ8huwUU.js": {
2585
- "file": "assets/RunFoldRow-DZ8huwUU.js",
2584
+ "_RunFoldRow-CAdpX4Gn.js": {
2585
+ "file": "assets/RunFoldRow-CAdpX4Gn.js",
2586
2586
  "name": "RunFoldRow",
2587
2587
  "imports": [
2588
2588
  "_browser-CCImen5O.js",
@@ -2592,7 +2592,7 @@
2592
2592
  "_SlackMark-DB0MGiF5.js",
2593
2593
  "_sseReplay-DGRDRLua.js",
2594
2594
  "_durationTone-CVpX_yIk.js",
2595
- "_StatusDot-CFLn-DZ2.js"
2595
+ "_StatusDot-IkfsHGXg.js"
2596
2596
  ]
2597
2597
  },
2598
2598
  "_RunsTabs-C7Ua3XX5.js": {
@@ -2632,16 +2632,16 @@
2632
2632
  "_browser-CCImen5O.js"
2633
2633
  ]
2634
2634
  },
2635
- "_StatusDot-CFLn-DZ2.js": {
2636
- "file": "assets/StatusDot-CFLn-DZ2.js",
2635
+ "_StatusDot-IkfsHGXg.js": {
2636
+ "file": "assets/StatusDot-IkfsHGXg.js",
2637
2637
  "name": "StatusDot",
2638
2638
  "imports": [
2639
2639
  "_browser-CCImen5O.js",
2640
- "_Tooltip-Dx9YBVEx.js"
2640
+ "_Tooltip-CmQVfAl2.js"
2641
2641
  ]
2642
2642
  },
2643
- "_Tooltip-Dx9YBVEx.js": {
2644
- "file": "assets/Tooltip-Dx9YBVEx.js",
2643
+ "_Tooltip-CmQVfAl2.js": {
2644
+ "file": "assets/Tooltip-CmQVfAl2.js",
2645
2645
  "name": "Tooltip",
2646
2646
  "imports": [
2647
2647
  "_AppShell-DPJVhUGP.js",
@@ -2888,7 +2888,7 @@
2888
2888
  ]
2889
2889
  },
2890
2890
  "src/main.ts": {
2891
- "file": "assets/main-DYwRHh0F.js",
2891
+ "file": "assets/main-Bk1L-bNk.js",
2892
2892
  "name": "main",
2893
2893
  "src": "src/main.ts",
2894
2894
  "isEntry": true,
@@ -2911,7 +2911,7 @@
2911
2911
  "_NotFoundPage-CDjMfCcj.js"
2912
2912
  ],
2913
2913
  "css": [
2914
- "assets/main-DGC6WFqS.css"
2914
+ "assets/main-DPB0UO1-.css"
2915
2915
  ],
2916
2916
  "assets": [
2917
2917
  "assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2",
@@ -2951,14 +2951,14 @@
2951
2951
  ]
2952
2952
  },
2953
2953
  "src/pages/HomePage.vue": {
2954
- "file": "assets/HomePage-CBQpcnaW.js",
2954
+ "file": "assets/HomePage-B2Y-Fa9V.js",
2955
2955
  "name": "HomePage",
2956
2956
  "src": "src/pages/HomePage.vue",
2957
2957
  "isDynamicEntry": true,
2958
2958
  "imports": [
2959
2959
  "_AppShell-DPJVhUGP.js",
2960
2960
  "_browser-CCImen5O.js",
2961
- "_Tooltip-Dx9YBVEx.js",
2961
+ "_Tooltip-CmQVfAl2.js",
2962
2962
  "_PendingTurnRow-DIC86uWA.js",
2963
2963
  "src/main.ts",
2964
2964
  "_eventSource-Cv0AFlMi.js",
@@ -2999,7 +2999,7 @@
2999
2999
  ]
3000
3000
  },
3001
3001
  "src/pages/ResidentDetailPage.vue": {
3002
- "file": "assets/ResidentDetailPage-C-SgOUTl.js",
3002
+ "file": "assets/ResidentDetailPage-DRNwx2--.js",
3003
3003
  "name": "ResidentDetailPage",
3004
3004
  "src": "src/pages/ResidentDetailPage.vue",
3005
3005
  "isDynamicEntry": true,
@@ -3007,36 +3007,36 @@
3007
3007
  "_AppShell-DPJVhUGP.js",
3008
3008
  "_browser-CCImen5O.js",
3009
3009
  "src/main.ts",
3010
- "_StatusDot-CFLn-DZ2.js"
3010
+ "_StatusDot-IkfsHGXg.js"
3011
3011
  ]
3012
3012
  },
3013
3013
  "src/pages/ResidentsIndexPage.vue": {
3014
- "file": "assets/ResidentsIndexPage-CiDhvXok.js",
3014
+ "file": "assets/ResidentsIndexPage-EHKMtX69.js",
3015
3015
  "name": "ResidentsIndexPage",
3016
3016
  "src": "src/pages/ResidentsIndexPage.vue",
3017
3017
  "isDynamicEntry": true,
3018
3018
  "imports": [
3019
3019
  "_AppShell-DPJVhUGP.js",
3020
3020
  "_browser-CCImen5O.js",
3021
- "_Tooltip-Dx9YBVEx.js",
3021
+ "_Tooltip-CmQVfAl2.js",
3022
3022
  "src/main.ts",
3023
3023
  "_eventSource-Cv0AFlMi.js",
3024
3024
  "_wallClock-x0hjr0J_.js",
3025
3025
  "_indexFormat-B-pd4r7Y.js",
3026
3026
  "_indexRow-BsWcHHVh.js",
3027
3027
  "_format-BldUwl_R.js",
3028
- "_StatusDot-CFLn-DZ2.js"
3028
+ "_StatusDot-IkfsHGXg.js"
3029
3029
  ]
3030
3030
  },
3031
3031
  "src/pages/RunRoutePage.vue": {
3032
- "file": "assets/RunRoutePage-Cp6wicYc.js",
3032
+ "file": "assets/RunRoutePage-DppLhu79.js",
3033
3033
  "name": "RunRoutePage",
3034
3034
  "src": "src/pages/RunRoutePage.vue",
3035
3035
  "isDynamicEntry": true,
3036
3036
  "imports": [
3037
3037
  "_AppShell-DPJVhUGP.js",
3038
3038
  "_browser-CCImen5O.js",
3039
- "_Tooltip-Dx9YBVEx.js",
3039
+ "_Tooltip-CmQVfAl2.js",
3040
3040
  "_PendingTurnRow-DIC86uWA.js",
3041
3041
  "src/main.ts",
3042
3042
  "_eventSource-Cv0AFlMi.js",
@@ -3044,7 +3044,7 @@
3044
3044
  "_indexFormat-B-pd4r7Y.js",
3045
3045
  "_indexRow-BsWcHHVh.js",
3046
3046
  "_format-BldUwl_R.js",
3047
- "_RunFoldRow-DZ8huwUU.js",
3047
+ "_RunFoldRow-CAdpX4Gn.js",
3048
3048
  "_SlackMark-DB0MGiF5.js",
3049
3049
  "_sseReplay-DGRDRLua.js",
3050
3050
  "_durationTone-CVpX_yIk.js",
@@ -3058,14 +3058,14 @@
3058
3058
  ]
3059
3059
  },
3060
3060
  "src/pages/RunsIndexPage.vue": {
3061
- "file": "assets/RunsIndexPage-BPXtfwwx.js",
3061
+ "file": "assets/RunsIndexPage-C7hx5O1X.js",
3062
3062
  "name": "RunsIndexPage",
3063
3063
  "src": "src/pages/RunsIndexPage.vue",
3064
3064
  "isDynamicEntry": true,
3065
3065
  "imports": [
3066
3066
  "_AppShell-DPJVhUGP.js",
3067
3067
  "_browser-CCImen5O.js",
3068
- "_Tooltip-Dx9YBVEx.js",
3068
+ "_Tooltip-CmQVfAl2.js",
3069
3069
  "_InputMenu-BRV35UWP.js",
3070
3070
  "src/main.ts",
3071
3071
  "_eventSource-Cv0AFlMi.js",
@@ -3076,18 +3076,18 @@
3076
3076
  "_SlackMark-DB0MGiF5.js",
3077
3077
  "_durationTone-CVpX_yIk.js",
3078
3078
  "_RunsTabs-C7Ua3XX5.js",
3079
- "_StatusDot-CFLn-DZ2.js"
3079
+ "_StatusDot-IkfsHGXg.js"
3080
3080
  ]
3081
3081
  },
3082
3082
  "src/pages/ScheduledPage.vue": {
3083
- "file": "assets/ScheduledPage-DbvP48HI.js",
3083
+ "file": "assets/ScheduledPage-Dk2kMnYT.js",
3084
3084
  "name": "ScheduledPage",
3085
3085
  "src": "src/pages/ScheduledPage.vue",
3086
3086
  "isDynamicEntry": true,
3087
3087
  "imports": [
3088
3088
  "_AppShell-DPJVhUGP.js",
3089
3089
  "_browser-CCImen5O.js",
3090
- "_Tooltip-Dx9YBVEx.js",
3090
+ "_Tooltip-CmQVfAl2.js",
3091
3091
  "_wallClock-x0hjr0J_.js",
3092
3092
  "_indexFormat-B-pd4r7Y.js",
3093
3093
  "_format-BldUwl_R.js",
@@ -3095,7 +3095,7 @@
3095
3095
  ]
3096
3096
  },
3097
3097
  "src/pages/SettingsPage.vue": {
3098
- "file": "assets/SettingsPage-B87lZaiN.js",
3098
+ "file": "assets/SettingsPage-DAf6R383.js",
3099
3099
  "name": "SettingsPage",
3100
3100
  "src": "src/pages/SettingsPage.vue",
3101
3101
  "isDynamicEntry": true,
@@ -3103,14 +3103,14 @@
3103
3103
  "_AppShell-DPJVhUGP.js",
3104
3104
  "_browser-CCImen5O.js",
3105
3105
  "_SettingSelect-CEeA4iuC.js",
3106
- "_Tooltip-Dx9YBVEx.js",
3106
+ "_Tooltip-CmQVfAl2.js",
3107
3107
  "_InputMenu-BRV35UWP.js",
3108
3108
  "_settingsApi-Dm6eaQcu.js",
3109
3109
  "_indexRow-BsWcHHVh.js"
3110
3110
  ]
3111
3111
  },
3112
3112
  "src/pages/UnitRoutePage.vue": {
3113
- "file": "assets/UnitRoutePage-d4A2X_o7.js",
3113
+ "file": "assets/UnitRoutePage-BjfuZVWr.js",
3114
3114
  "name": "UnitRoutePage",
3115
3115
  "src": "src/pages/UnitRoutePage.vue",
3116
3116
  "isDynamicEntry": true,
@@ -3122,7 +3122,7 @@
3122
3122
  "_indexFormat-B-pd4r7Y.js",
3123
3123
  "_indexRow-BsWcHHVh.js",
3124
3124
  "_format-BldUwl_R.js",
3125
- "_RunFoldRow-DZ8huwUU.js",
3125
+ "_RunFoldRow-CAdpX4Gn.js",
3126
3126
  "_SlackMark-DB0MGiF5.js",
3127
3127
  "_sseReplay-DGRDRLua.js",
3128
3128
  "_NotFoundPage-CDjMfCcj.js"