@coreplane/switchboard 1.216.2 → 1.218.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.
- package/dist/assets/config/config.example.yaml +6 -0
- package/dist/assets/deploy/cloudflare/package.json +1 -1
- package/dist/assets/deploy/cloudflare/worker.ts +4 -0
- package/dist/assets/deploy/cloudflare-docs/package.json +1 -1
- package/dist/assets/deploy/cloudflare-memory/package.json +1 -1
- package/dist/assets/deploy/cloudflare-memory/worker.ts +96 -6
- package/dist/assets/deploy/cloudflare-resident/package.json +1 -1
- package/dist/assets/deploy/cloudflare-sandbox/package.json +1 -1
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- package/dist/assets/package-lock.json +5676 -7966
- package/dist/assets/package.json +2 -2
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +18 -0
- package/dist/assets/src/core/runEvents.ts +13 -0
- package/dist/assets/src/core/runFriction.ts +4 -2
- package/dist/assets/src/core/runLedger/sessionLog.ts +22 -0
- package/dist/assets/src/core/runLedger/types.ts +17 -0
- package/dist/assets/src/core/runRecord.ts +4 -2
- package/dist/assets/src/mcp/registry.ts +33 -5
- package/dist/assets/web/dist/.vite/manifest.json +18 -18
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CvwjhjlG.js → ResidentDetailPage-DvCt96Dk.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-DoN6XuNx.js → ResidentsIndexPage-BZaYja1-.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-FzLNexjF.js → RunRoutePage-DI4vzCUu.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-DhSUbF9k.js → RunsIndexPage-Ch9uP4p4.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-Bq0Yg4nT.js → ScheduledPage-DAkAHMCH.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-D8Wwt1KC.js → StatusDot-CfBonGcV.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DpDK7jWZ.js → Tooltip-CBD36E3W.js} +1 -1
- package/dist/assets/web/dist/assets/dist-DQ1U73LF.js +1542 -0
- package/dist/assets/web/dist/assets/{main-BFkEOy3K.js → main-CdLnG-yl.js} +2 -2
- package/dist/cli.js +1066 -343
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/dist-B7BVkB7x.js +0 -1547
|
@@ -473,6 +473,12 @@ workspaceDir: ./workspaces
|
|
|
473
473
|
# auth: bearer
|
|
474
474
|
# tokenEnv: MCP_GITHUB_TOKEN # static bearer from the bot env (startup fails if unset)
|
|
475
475
|
# agents: [general, research, coding] # default [general, research]
|
|
476
|
+
# lake: # a server behind Cloudflare Access: the gate wants a service token,
|
|
477
|
+
# url: https://vega.example.com/mcp # not an Authorization header — name the two headers and the bot
|
|
478
|
+
# auth: none # env vars holding them (manifest secrets on Cloudflare); an unset
|
|
479
|
+
# headersEnv: # one makes the server `unavailable` for that run, naming it
|
|
480
|
+
# CF-Access-Client-Id: MCP_ACCESS_CLIENT_ID
|
|
481
|
+
# CF-Access-Client-Secret: MCP_ACCESS_CLIENT_SECRET
|
|
476
482
|
# channels:
|
|
477
483
|
# "slack:C0123":
|
|
478
484
|
# mcpServers:
|
|
@@ -90,6 +90,8 @@ export interface Env {
|
|
|
90
90
|
ANTHROPIC_ADMIN_KEY?: string; // costs dash (optional): Anthropic Admin API key for the LLM cost report
|
|
91
91
|
MEMORY_TOKEN?: string; // durable memory + friction ledger + schedule firings + MCP registry: bearer for the state Worker
|
|
92
92
|
MCP_CREDENTIAL_KEY?: string; // MCP registry: the bot-only key that seals server credentials before they reach the McpDO
|
|
93
|
+
MCP_ACCESS_CLIENT_ID?: string; // MCP servers behind Cloudflare Access: the service token's client id, named by a server's `headersEnv`
|
|
94
|
+
MCP_ACCESS_CLIENT_SECRET?: string; // MCP servers behind Cloudflare Access: the service token's client secret, named by a server's `headersEnv`
|
|
93
95
|
STATE_WORKER_URL?: string; // var: the state Worker's base URL — where this shim records each scheduled firing
|
|
94
96
|
ARTIFACTS_R2_ACCESS_KEY_ID?: string; // artifact store: the bucket-scoped S3 token the bot signs presigned URLs with
|
|
95
97
|
ARTIFACTS_R2_SECRET_ACCESS_KEY?: string; // artifact store: the secret half of that token
|
|
@@ -124,6 +126,8 @@ const FORWARDED_OPTIONAL = [
|
|
|
124
126
|
"BRAVE_SEARCH_API_KEY",
|
|
125
127
|
"MEMORY_TOKEN",
|
|
126
128
|
"MCP_CREDENTIAL_KEY",
|
|
129
|
+
"MCP_ACCESS_CLIENT_ID",
|
|
130
|
+
"MCP_ACCESS_CLIENT_SECRET",
|
|
127
131
|
"STATE_WORKER_URL",
|
|
128
132
|
"ARTIFACTS_R2_ACCESS_KEY_ID",
|
|
129
133
|
"ARTIFACTS_R2_SECRET_ACCESS_KEY",
|
|
@@ -46,7 +46,10 @@ import {
|
|
|
46
46
|
attachmentRefsOf,
|
|
47
47
|
DEFAULT_SESSION_LOG_MAX_BYTES,
|
|
48
48
|
droppedToolResultRow,
|
|
49
|
+
GAP_MARKER,
|
|
50
|
+
NOTEPAD_MAX_BYTES,
|
|
49
51
|
planSessionTrim,
|
|
52
|
+
roleOfStoredRow,
|
|
50
53
|
rowKind,
|
|
51
54
|
sessionsToDrop,
|
|
52
55
|
tailCut,
|
|
@@ -80,6 +83,7 @@ import {
|
|
|
80
83
|
type LiveRunRow,
|
|
81
84
|
type ReclaimedRun,
|
|
82
85
|
type RunState,
|
|
86
|
+
type SessionHit,
|
|
83
87
|
type StepRecord,
|
|
84
88
|
type StopMode,
|
|
85
89
|
type TranscriptAttachment,
|
|
@@ -210,6 +214,10 @@ const FTS_CANDIDATES_FLOOR = 50;
|
|
|
210
214
|
* tokens and are untouched; the engine still ranks with the FULL query, so the
|
|
211
215
|
* cap only shapes which rows can become candidates. */
|
|
212
216
|
const MAX_MATCH_TOKENS = 24;
|
|
217
|
+
/** A `recall` query's size and the most hits one answers (session-log item 10):
|
|
218
|
+
* a query is a few words, and the tool's default is five. */
|
|
219
|
+
const MAX_SEARCH_QUERY_BYTES = 1_024;
|
|
220
|
+
const MAX_SEARCH_HITS = 50;
|
|
213
221
|
/** Request body ceiling, checked against Content-Length before parsing. A full
|
|
214
222
|
* batch (50 × 4000-char texts + keywords + envelope) fits comfortably. */
|
|
215
223
|
const MAX_BODY_BYTES = 512 * 1024;
|
|
@@ -2892,18 +2900,65 @@ export class SessionLogDO extends DurableObject<Env> {
|
|
|
2892
2900
|
return { ...(await this.read(from)), from };
|
|
2893
2901
|
}
|
|
2894
2902
|
|
|
2895
|
-
/** The rows whose text matches `query`,
|
|
2896
|
-
|
|
2903
|
+
/** The rows whose text matches `query`, in relevance order (FTS5's bm25 rank,
|
|
2904
|
+
* the order the memory store's search uses; the newest first among equals),
|
|
2905
|
+
* each with its turn, part, role, kind and indexed text — what `recall`
|
|
2906
|
+
* answers (session-log item 10). */
|
|
2907
|
+
async search(query: string, limit: number): Promise<SessionHit[]> {
|
|
2897
2908
|
const match = ftsMatchExpr(query);
|
|
2898
2909
|
if (match === null) return [];
|
|
2899
2910
|
return this.sql
|
|
2900
|
-
.exec<{ idx: number; part: number; text: string }>(
|
|
2901
|
-
`SELECT t.idx, t.part, t.text FROM turns_fts f JOIN turns t ON t.id = f.rowid
|
|
2902
|
-
WHERE turns_fts MATCH ? ORDER BY t.idx DESC, t.part DESC LIMIT ?`,
|
|
2911
|
+
.exec<{ idx: number; part: number; kind: string; json: string; text: string }>(
|
|
2912
|
+
`SELECT t.idx, t.part, t.kind, t.json, t.text FROM turns_fts f JOIN turns t ON t.id = f.rowid
|
|
2913
|
+
WHERE turns_fts MATCH ? ORDER BY f.rank, t.idx DESC, t.part DESC LIMIT ?`,
|
|
2903
2914
|
match,
|
|
2904
2915
|
limit,
|
|
2905
2916
|
)
|
|
2906
|
-
.toArray()
|
|
2917
|
+
.toArray()
|
|
2918
|
+
.map(({ idx, part, kind, json, text }) => {
|
|
2919
|
+
const role = roleOfStoredRow(json);
|
|
2920
|
+
return { idx, part, ...(role !== undefined ? { role } : {}), kind, text };
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
/** The gap markers (session-log item 9) whose turn lies in `[from, to]`: the
|
|
2925
|
+
* rows a follow-up appended because the run before it detached, so a search
|
|
2926
|
+
* whose hits straddle one can say the log ends short between them. */
|
|
2927
|
+
async gapsBetween(from: number, to: number): Promise<number[]> {
|
|
2928
|
+
return this.sql
|
|
2929
|
+
.exec<{ idx: number }>(
|
|
2930
|
+
`SELECT DISTINCT idx FROM turns WHERE idx >= ? AND idx <= ? AND kind = 'text' AND text = ? ORDER BY idx`,
|
|
2931
|
+
from,
|
|
2932
|
+
to,
|
|
2933
|
+
GAP_MARKER,
|
|
2934
|
+
)
|
|
2935
|
+
.toArray()
|
|
2936
|
+
.map((r) => r.idx);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
/** The notepad, replaced whole by the live run (session-log item 10): the
|
|
2940
|
+
* same fence as a row write — unknown-run before an owner, fenced for
|
|
2941
|
+
* another generation — and the write's time kept beside the text. */
|
|
2942
|
+
async writeNotepad(gen: string, text: string, now: number): Promise<FenceResult> {
|
|
2943
|
+
let out: FenceResult = { ok: true };
|
|
2944
|
+
this.ctx.storage.transactionSync(() => {
|
|
2945
|
+
const owner = this.owner();
|
|
2946
|
+
if (owner === undefined) {
|
|
2947
|
+
out = { ok: false, reason: "unknown-run" };
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
if (owner.gen !== gen) {
|
|
2951
|
+
out = { ok: false, reason: "fenced" };
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
this.sql.exec(
|
|
2955
|
+
`INSERT INTO notepad (k, text, updated_at) VALUES (1, ?, ?)
|
|
2956
|
+
ON CONFLICT(k) DO UPDATE SET text = excluded.text, updated_at = excluded.updated_at`,
|
|
2957
|
+
text,
|
|
2958
|
+
now,
|
|
2959
|
+
);
|
|
2960
|
+
});
|
|
2961
|
+
return out;
|
|
2907
2962
|
}
|
|
2908
2963
|
|
|
2909
2964
|
async bytes(): Promise<number> {
|
|
@@ -2968,6 +3023,9 @@ const LEDGER_ROUTES = new Set([
|
|
|
2968
3023
|
"/runs/session/read",
|
|
2969
3024
|
"/runs/session/read-tail",
|
|
2970
3025
|
"/runs/session/clear-owner",
|
|
3026
|
+
"/runs/session/search",
|
|
3027
|
+
"/runs/session/notepad",
|
|
3028
|
+
"/runs/session/notepad/write",
|
|
2971
3029
|
]);
|
|
2972
3030
|
|
|
2973
3031
|
/** Routes whose bodies may carry a record, a transcript chunk, or an event batch. */
|
|
@@ -3187,6 +3245,38 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
|
|
|
3187
3245
|
if (!g.ok) return json({ error: g.error }, 400);
|
|
3188
3246
|
return fenced(await stub.clearOwner(runId.value, g.value));
|
|
3189
3247
|
}
|
|
3248
|
+
// `recall` (session-log item 10): the hits in relevance order, and the gap
|
|
3249
|
+
// markers that lie between the oldest and the newest of them.
|
|
3250
|
+
if (pathname === "/runs/session/search") {
|
|
3251
|
+
if (
|
|
3252
|
+
typeof b.query !== "string" ||
|
|
3253
|
+
b.query.trim().length === 0 ||
|
|
3254
|
+
utf8ByteLength(b.query) > MAX_SEARCH_QUERY_BYTES
|
|
3255
|
+
)
|
|
3256
|
+
return json({ error: `query must be a non-empty string of at most ${MAX_SEARCH_QUERY_BYTES} bytes` }, 400);
|
|
3257
|
+
const limit = b.limit;
|
|
3258
|
+
if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > MAX_SEARCH_HITS)
|
|
3259
|
+
return json({ error: `limit must be an integer in 1..${MAX_SEARCH_HITS}` }, 400);
|
|
3260
|
+
const hits = await stub.search(b.query, limit);
|
|
3261
|
+
const gaps =
|
|
3262
|
+
hits.length > 1
|
|
3263
|
+
? await stub.gapsBetween(Math.min(...hits.map((h) => h.idx)), Math.max(...hits.map((h) => h.idx)))
|
|
3264
|
+
: [];
|
|
3265
|
+
console.log(`[runs/session/search] ${key.value} -> ${hits.length} hit(s), ${gaps.length} gap(s)`);
|
|
3266
|
+
return json({ hits, gaps });
|
|
3267
|
+
}
|
|
3268
|
+
if (pathname === "/runs/session/notepad") return json({ notepad: await stub.notepad() });
|
|
3269
|
+
if (pathname === "/runs/session/notepad/write") {
|
|
3270
|
+
const g = gen(b.gen);
|
|
3271
|
+
if (!g.ok) return json({ error: g.error }, 400);
|
|
3272
|
+
if (typeof b.text !== "string") return json({ error: "text must be a string" }, 400);
|
|
3273
|
+
const bytes = utf8ByteLength(b.text);
|
|
3274
|
+
if (bytes > NOTEPAD_MAX_BYTES)
|
|
3275
|
+
return json({ error: `text is ${bytes} bytes; the notepad holds at most ${NOTEPAD_MAX_BYTES}` }, 400);
|
|
3276
|
+
const r = await stub.writeNotepad(g.value, b.text, systemClock());
|
|
3277
|
+
console.log(`[runs/session/notepad/write] ${key.value} <- ${bytes} byte(s), ok=${r.ok}`);
|
|
3278
|
+
return fenced(r);
|
|
3279
|
+
}
|
|
3190
3280
|
return json({ error: "not found" }, 404);
|
|
3191
3281
|
}
|
|
3192
3282
|
|
|
@@ -99,6 +99,18 @@
|
|
|
99
99
|
"optional": true,
|
|
100
100
|
"note": "AES-256-GCM key (32 bytes, base64: `openssl rand -base64 32`) the bot seals MCP server credentials with before they go to the state Worker's McpDO (docs/reference/specs/mcp-tools.md). Bot-only by design: the Worker stores ciphertext. Rotation re-seals every credential — not built yet; rotate = users re-run `mcp connect`."
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"name": "MCP_ACCESS_CLIENT_ID",
|
|
104
|
+
"workers": ["bot"],
|
|
105
|
+
"optional": true,
|
|
106
|
+
"note": "Client id of the Cloudflare Access service token the bot presents to MCP servers behind Access — a pinned server's `headersEnv` names it under `CF-Access-Client-Id` (docs/reference/specs/mcp-tools.md item 11). Minted in Zero Trust → Access → Service Auth of the account that guards the server, and enrolled on that Access application's policy. Optional: without it such a server is `unavailable` naming the variable."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "MCP_ACCESS_CLIENT_SECRET",
|
|
110
|
+
"workers": ["bot"],
|
|
111
|
+
"optional": true,
|
|
112
|
+
"note": "Client secret of that service token — `headersEnv` names it under `CF-Access-Client-Secret`. Shown once at creation; rotate by minting a new token, `deploy secrets bot --only` both names, `deploy restart`, then revoke the old one."
|
|
113
|
+
},
|
|
102
114
|
{
|
|
103
115
|
"name": "MEMORY_TOKEN",
|
|
104
116
|
"workers": ["bot", "resident", "memory"],
|