@coreplane/switchboard 1.266.0 → 1.266.1
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/assets/deploy/cloudflare/worker.ts +7 -1
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +5 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +48 -3
- package/dist/assets/deploy/cloudflare-resident/legacyCredentials.ts +43 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +102 -141
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/channels/githubDoorPaths.ts +36 -0
- package/dist/assets/src/core/coordinator/contract.ts +2 -0
- package/dist/assets/src/core/refusal.ts +1 -0
- package/dist/assets/src/core/runEvents.ts +1 -0
- package/dist/assets/src/core/runRecord.ts +41 -0
- package/dist/assets/src/execution/residentReadonly.ts +9 -8
- package/dist/assets/src/execution/residentSteps.ts +2 -1
- package/dist/assets/src/execution/seedPlan.ts +2 -2
- package/dist/assets/web/dist/.vite/manifest.json +49 -49
- package/dist/assets/web/dist/assets/{HomePage-B2Y-Fa9V.js → HomePage-BfpIrKDX.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-DIC86uWA.js → PendingTurnRow-C6hroa00.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-CfrbMh8t.js → PlanePage-C2Cb1mkT.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DRNwx2--.js → ResidentDetailPage-Bd9_Q87l.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-EHKMtX69.js → ResidentsIndexPage-C8eS7hxp.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-CAdpX4Gn.js → RunFoldRow-DrfFauYJ.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-DppLhu79.js → RunRoutePage-B_eXfNAF.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-C7hx5O1X.js → RunsIndexPage-BpQu36Dm.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-Dk2kMnYT.js → ScheduledPage-DGHcjcSt.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DAf6R383.js → SettingsPage-IfQ71QjN.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-CPED_XCV.js → SilentTurn-D9ON15a8.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-IkfsHGXg.js → StatusDot-DyFjGg4V.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CmQVfAl2.js → Tooltip-BvPkV4kZ.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-BjfuZVWr.js → UnitRoutePage-BNe3nBIG.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-Dg6HTzpQ.js → dist-v3Py3_QR.js} +1 -1
- package/dist/assets/web/dist/assets/{main-Bk1L-bNk.js → main-ks3kBojV.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-DGRDRLua.js → sseReplay-DuALaBPq.js} +1 -1
- package/dist/cli.js +1834 -641
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { depotCiAuthorization, handleDepotCi } from "./depotCi.ts";
|
|
2
2
|
import { DEPOT_CI_AUTHORIZATION_PATH, DEPOT_CI_PATH } from "../../src/core/depotCi.ts";
|
|
3
|
+
import { githubDoorEdgeRoute } from "../../src/channels/githubDoorPaths.ts";
|
|
3
4
|
// Cloudflare Containers shim: runs the unchanged Switchboard image as a single
|
|
4
5
|
// always-on container instance — the shape of any long-lived server on
|
|
5
6
|
// Cloudflare Containers (singleton DO, cron keep-alive;
|
|
@@ -88,6 +89,7 @@ export interface Env {
|
|
|
88
89
|
DEPOT_API_TOKEN?: string; // Worker-only organization credential: NEVER forwarded to any container
|
|
89
90
|
DEPOT_CI_BRIDGE_TOKEN?: string; // bot → Worker, narrow repo-bound CI operations only
|
|
90
91
|
PUBLIC_BASE_URL?: string; // live-view: base for /runs/<id>?t=… links on the status card
|
|
92
|
+
PUBLIC_GIT_BASE_URL?: string; // GitHub run-bearer door on a separate custom domain without dashboard Access
|
|
91
93
|
ACCESS_TEAM_DOMAIN?: string; // live-view SSO gate: Cloudflare Access team domain (JWKS + iss)
|
|
92
94
|
ACCESS_AUD?: string; // live-view SSO gate: Cloudflare Access application AUD tag
|
|
93
95
|
DASHBOARD_TOKEN?: string; // dashboard auth `token` strategy: the bearer (the default env name; config may name another)
|
|
@@ -130,6 +132,7 @@ const FORWARDED_OPTIONAL = [
|
|
|
130
132
|
"GITHUB_APP_PRIVATE_KEY",
|
|
131
133
|
"DEPOT_CI_BRIDGE_TOKEN",
|
|
132
134
|
"PUBLIC_BASE_URL",
|
|
135
|
+
"PUBLIC_GIT_BASE_URL",
|
|
133
136
|
"ACCESS_TEAM_DOMAIN",
|
|
134
137
|
"ACCESS_AUD",
|
|
135
138
|
"DASHBOARD_TOKEN",
|
|
@@ -462,7 +465,10 @@ const withLength = (res: Response): Response => withKnownLength(res, (size) => n
|
|
|
462
465
|
|
|
463
466
|
export default {
|
|
464
467
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
465
|
-
const
|
|
468
|
+
const requestUrl = new URL(request.url);
|
|
469
|
+
const pathname = requestUrl.pathname;
|
|
470
|
+
if (githubDoorEdgeRoute(requestUrl, request.method, env.PUBLIC_GIT_BASE_URL) === "refuse")
|
|
471
|
+
return new Response("not found", { status: 404 });
|
|
466
472
|
// Permits can only be consumed through the Worker's fixed container
|
|
467
473
|
// binding. The public edge must never proxy this callback path.
|
|
468
474
|
if (pathname === DEPOT_CI_AUTHORIZATION_PATH) return new Response("not found", { status: 404 });
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
// Cloudflare Access (SSO), and Access can only front a real zone domain —
|
|
14
14
|
// leaving the workers.dev URL up would expose an un-SSO'd copy of /runs.
|
|
15
15
|
"workers_dev": false,
|
|
16
|
-
"routes": [
|
|
16
|
+
"routes": [
|
|
17
|
+
{ "pattern": "{{hostname}}", "custom_domain": true },
|
|
18
|
+
{ "pattern": "git.{{hostname}}", "custom_domain": true }
|
|
19
|
+
],
|
|
17
20
|
// Non-secret, deploy-constant config for the container. PUBLIC_BASE_URL: the
|
|
18
21
|
// live-view link base — the dispatcher (in the container) reads
|
|
19
22
|
// process.env.PUBLIC_BASE_URL to put the /runs/<id>?t=… link on the status
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
// every dashboard request is denied.
|
|
28
31
|
"vars": {
|
|
29
32
|
"PUBLIC_BASE_URL": "{{urls.publicBaseUrl}}",
|
|
33
|
+
"PUBLIC_GIT_BASE_URL": "https://git.{{hostname}}",
|
|
30
34
|
// {{#if access}}
|
|
31
35
|
"ACCESS_TEAM_DOMAIN": "{{access.teamDomain}}",
|
|
32
36
|
"ACCESS_AUD": "{{access.aud}}",
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
applyRetention,
|
|
24
24
|
clampRetentionPolicy,
|
|
25
25
|
isRunRecord,
|
|
26
|
+
isRecoveryEvidenceScope,
|
|
26
27
|
isRunSession,
|
|
27
28
|
isRunVisibilityFilter,
|
|
28
29
|
normalizeStored,
|
|
@@ -4163,6 +4164,12 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
4163
4164
|
* walked until the page fills; the kept set is the newest prefix of the
|
|
4164
4165
|
* in-cutoff order, so the walk stops at the first row outside it.
|
|
4165
4166
|
*
|
|
4167
|
+
* Recovery evidence additionally streams every retained summary through the
|
|
4168
|
+
* record parser before trusting identity filters: valid JSON can still be
|
|
4169
|
+
* unreadable evidence whose missing identity would hide a competing child.
|
|
4170
|
+
* This does not load the retained summaries into memory together or change
|
|
4171
|
+
* the relevant-row cap; ordinary list queries keep the fast path above.
|
|
4172
|
+
*
|
|
4166
4173
|
* `visibleTo` — the caller's authorization predicate (authorization.md item
|
|
4167
4174
|
* 6) — is compiled into the same WHERE clause (`visibilitySql`): its leaves
|
|
4168
4175
|
* become `channel_id IN (…)`, `channel_visibility IN (…)`, `user_id = ?`,
|
|
@@ -4170,7 +4177,9 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
4170
4177
|
* indexed filter on the page query, never a post-filter. `none` answers an
|
|
4171
4178
|
* empty page without a query.
|
|
4172
4179
|
*/
|
|
4173
|
-
async list(
|
|
4180
|
+
async list(
|
|
4181
|
+
q: RunListOptions,
|
|
4182
|
+
): Promise<{ items: RunListItem[]; nextBefore?: { finishedAt: number; id: string }; evidenceComplete?: boolean }> {
|
|
4174
4183
|
const now = systemClock();
|
|
4175
4184
|
const limit = clampListLimit(q.limit);
|
|
4176
4185
|
if (q.visibleTo?.kind === "none") return { items: [] };
|
|
@@ -4204,6 +4213,29 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
4204
4213
|
where.push(`parent_run_id = ?`);
|
|
4205
4214
|
params.push(q.parentRunId);
|
|
4206
4215
|
}
|
|
4216
|
+
if (q.recoveryEvidence !== undefined) {
|
|
4217
|
+
// Identity comparisons cannot prove an unreadable record is unrelated.
|
|
4218
|
+
// Use the same parser as the result loop, not a weaker SQL shape check.
|
|
4219
|
+
for (const row of this.sql.exec<Pick<RunRow, "run_id" | "summary_json">>(
|
|
4220
|
+
`SELECT run_id, summary_json FROM runs WHERE finished_at >= ? ORDER BY finished_at DESC, run_id DESC`,
|
|
4221
|
+
cutoff,
|
|
4222
|
+
)) {
|
|
4223
|
+
if (kept !== null && !kept.has(row.run_id)) break; // only the retained newest-first prefix
|
|
4224
|
+
if (!parseSummary(row)) return { items: [], evidenceComplete: false };
|
|
4225
|
+
}
|
|
4226
|
+
const scope = q.recoveryEvidence;
|
|
4227
|
+
const unitKey = `${scope.instanceId}:${scope.unit}`;
|
|
4228
|
+
const prefix = `${unitKey}/`;
|
|
4229
|
+
// Literal prefix, not LIKE: instance ids can contain SQL wildcard chars.
|
|
4230
|
+
// Bare unit keys remain candidates, never proof of absence.
|
|
4231
|
+
where.push(`(CASE WHEN json_valid(summary_json) THEN (
|
|
4232
|
+
json_extract(summary_json, '$.parentInstanceId') = ? OR
|
|
4233
|
+
json_extract(summary_json, '$.idempotencyKey') = ? OR
|
|
4234
|
+
substr(json_extract(summary_json, '$.idempotencyKey'), 1, ?) = ? OR
|
|
4235
|
+
thread_key IN (${scope.threadKeys.map(() => "?").join(",")})
|
|
4236
|
+
) ELSE 1 END)`);
|
|
4237
|
+
params.push(scope.instanceId, unitKey, prefix.length, prefix, ...scope.threadKeys);
|
|
4238
|
+
}
|
|
4207
4239
|
if (q.pr !== undefined) {
|
|
4208
4240
|
// `namesPullRequest` in SQL: the row's repository and the number it names.
|
|
4209
4241
|
where.push(`repo = ?`, `pr_number = ?`);
|
|
@@ -4216,13 +4248,18 @@ export class RunHistoryDO extends DurableObject<Env> {
|
|
|
4216
4248
|
// the walk stops early at the first evicted row) — never a full table load.
|
|
4217
4249
|
const rows = this.sql.exec<RunRow>(`${select} LIMIT ?`, ...params, limit).toArray();
|
|
4218
4250
|
const items: RunListItem[] = [];
|
|
4251
|
+
let malformed = false;
|
|
4219
4252
|
for (const row of rows) {
|
|
4220
4253
|
if (items.length >= limit) break;
|
|
4221
4254
|
if (kept !== null && !kept.has(row.run_id)) break; // kept is a newest-first prefix: nothing older is kept either
|
|
4222
4255
|
const summary = parseSummary(row);
|
|
4223
4256
|
if (summary) items.push({ ...summary, bytes: row.bytes });
|
|
4257
|
+
else malformed = true;
|
|
4224
4258
|
}
|
|
4225
|
-
const out: { items: RunListItem[]; nextBefore?: { finishedAt: number; id: string } } = {
|
|
4259
|
+
const out: { items: RunListItem[]; nextBefore?: { finishedAt: number; id: string }; evidenceComplete?: boolean } = {
|
|
4260
|
+
items,
|
|
4261
|
+
};
|
|
4262
|
+
if (q.recoveryEvidence !== undefined) out.evidenceComplete = !malformed && rows.length < limit;
|
|
4226
4263
|
if (items.length === limit) {
|
|
4227
4264
|
const last = items[items.length - 1];
|
|
4228
4265
|
out.nextBefore = { finishedAt: last.finishedAt, id: last.id };
|
|
@@ -4322,7 +4359,7 @@ function identityOfSummary(raw: string): { userName?: string; parentRunId?: stri
|
|
|
4322
4359
|
}
|
|
4323
4360
|
}
|
|
4324
4361
|
|
|
4325
|
-
function parseSummary(row: RunRow): Omit<RunRecord, "events"> | null {
|
|
4362
|
+
function parseSummary(row: Pick<RunRow, "summary_json">): Omit<RunRecord, "events"> | null {
|
|
4326
4363
|
try {
|
|
4327
4364
|
const parsed: unknown = JSON.parse(row.summary_json);
|
|
4328
4365
|
return isRunRecord({ ...(parsed as object), events: [] })
|
|
@@ -4492,6 +4529,14 @@ function parseRunList(body: unknown): Validated<{ storeKey: string; query: RunLi
|
|
|
4492
4529
|
return invalid("pr must be { repo: owner/name, number: a positive integer }");
|
|
4493
4530
|
query.pr = { repo: pr.repo, number: pr.number };
|
|
4494
4531
|
}
|
|
4532
|
+
if (b.recoveryEvidence !== undefined) {
|
|
4533
|
+
if (!isRecoveryEvidenceScope(b.recoveryEvidence))
|
|
4534
|
+
return invalid("recoveryEvidence must name an instance, unit and original threads");
|
|
4535
|
+
// No paged or narrowed response can attest complete recovery evidence.
|
|
4536
|
+
if (Object.keys(query).some((field) => field !== "limit") || b.visibleTo !== undefined)
|
|
4537
|
+
return invalid("recoveryEvidence cannot be combined with list filters or cursors");
|
|
4538
|
+
query.recoveryEvidence = b.recoveryEvidence;
|
|
4539
|
+
}
|
|
4495
4540
|
if (b.visibleTo !== undefined) {
|
|
4496
4541
|
// A malformed filter is a 400, never "all": the bot degrades to live rows
|
|
4497
4542
|
// rather than the DO widening what an actor may see.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Root-side cleanup for credentials left by an earlier resident release.
|
|
2
|
+
* Open every directory component without following symlinks, then unlink
|
|
3
|
+
* only the named file relative to its pinned directory descriptor. A model
|
|
4
|
+
* owns the old tree and stage directory, so a pathname-based root `rm` can
|
|
5
|
+
* otherwise traverse a replacement symlink outside that tree. */
|
|
6
|
+
export const LEGACY_CREDENTIAL_SCRUB = `import os, sys
|
|
7
|
+
|
|
8
|
+
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
|
|
9
|
+
|
|
10
|
+
def open_dir(path):
|
|
11
|
+
fd = os.open('/', flags)
|
|
12
|
+
try:
|
|
13
|
+
for part in path.strip('/').split('/'):
|
|
14
|
+
if not part:
|
|
15
|
+
continue
|
|
16
|
+
child = os.open(part, flags, dir_fd=fd)
|
|
17
|
+
os.close(fd)
|
|
18
|
+
fd = child
|
|
19
|
+
return fd
|
|
20
|
+
except BaseException:
|
|
21
|
+
os.close(fd)
|
|
22
|
+
raise
|
|
23
|
+
|
|
24
|
+
def remove(parent, name):
|
|
25
|
+
try:
|
|
26
|
+
fd = open_dir(parent)
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
return
|
|
29
|
+
try:
|
|
30
|
+
try:
|
|
31
|
+
os.unlink(name, dir_fd=fd)
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
pass
|
|
34
|
+
finally:
|
|
35
|
+
os.close(fd)
|
|
36
|
+
|
|
37
|
+
remove(sys.argv[1], 'github-credentials')
|
|
38
|
+
remove(sys.argv[2], 'cred')
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
export function legacyCredentialScrubCommand(worktreePath: string, stageDir: string): string[] {
|
|
42
|
+
return ["python3", "-c", LEGACY_CREDENTIAL_SCRUB, `${worktreePath}/.git`, stageDir];
|
|
43
|
+
}
|