@coreplane/switchboard 0.0.0 → 1.18.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 (131) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +18 -1
  3. package/dist/assets/.dockerignore +27 -0
  4. package/dist/assets/.env.example +33 -0
  5. package/dist/assets/Dockerfile +111 -0
  6. package/dist/assets/config/config.example.yaml +359 -0
  7. package/dist/assets/deploy/bin/build-stamp.d.mts +15 -0
  8. package/dist/assets/deploy/bin/build-stamp.mjs +98 -0
  9. package/dist/assets/deploy/bin/cf-logs +32 -0
  10. package/dist/assets/deploy/cloudflare/package.json +29 -0
  11. package/dist/assets/deploy/cloudflare/preflight.mjs +243 -0
  12. package/dist/assets/deploy/cloudflare/tsconfig.json +18 -0
  13. package/dist/assets/deploy/cloudflare/worker.ts +382 -0
  14. package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +67 -0
  15. package/dist/assets/deploy/cloudflare/write-build.d.mts +7 -0
  16. package/dist/assets/deploy/cloudflare/write-build.mjs +53 -0
  17. package/dist/assets/deploy/cloudflare-docs/package.json +18 -0
  18. package/dist/assets/deploy/cloudflare-docs/wrangler.template.jsonc +30 -0
  19. package/dist/assets/deploy/cloudflare-memory/package.json +25 -0
  20. package/dist/assets/deploy/cloudflare-memory/tsconfig.json +17 -0
  21. package/dist/assets/deploy/cloudflare-memory/worker.ts +2635 -0
  22. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +50 -0
  23. package/dist/assets/deploy/cloudflare-resident/Dockerfile +91 -0
  24. package/dist/assets/deploy/cloudflare-resident/gc.ts +287 -0
  25. package/dist/assets/deploy/cloudflare-resident/node-async-hooks.d.ts +11 -0
  26. package/dist/assets/deploy/cloudflare-resident/package.json +29 -0
  27. package/dist/assets/deploy/cloudflare-resident/preflight.mjs +224 -0
  28. package/dist/assets/deploy/cloudflare-resident/tsconfig.json +19 -0
  29. package/dist/assets/deploy/cloudflare-resident/worker.ts +6637 -0
  30. package/dist/assets/deploy/cloudflare-resident/wrangler.template.jsonc +120 -0
  31. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +67 -0
  32. package/dist/assets/deploy/cloudflare-sandbox/docker-wrapper.sh +37 -0
  33. package/dist/assets/deploy/cloudflare-sandbox/package.json +26 -0
  34. package/dist/assets/deploy/cloudflare-sandbox/tsconfig.json +20 -0
  35. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +410 -0
  36. package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +67 -0
  37. package/dist/assets/deploy/profile.example.json +13 -0
  38. package/dist/assets/deploy/secrets.manifest.json +108 -0
  39. package/dist/assets/docker-entrypoint.sh +15 -0
  40. package/dist/assets/package-lock.json +18407 -0
  41. package/dist/assets/package.json +104 -0
  42. package/dist/assets/project.json +219 -0
  43. package/dist/assets/source.json +5 -0
  44. package/dist/assets/src/core/authz/actor.ts +100 -0
  45. package/dist/assets/src/core/authz/authorize.ts +169 -0
  46. package/dist/assets/src/core/authz/grants.ts +347 -0
  47. package/dist/assets/src/core/authz/policy.ts +281 -0
  48. package/dist/assets/src/core/authz/resource.ts +147 -0
  49. package/dist/assets/src/core/authz/types.ts +164 -0
  50. package/dist/assets/src/core/drain.ts +54 -0
  51. package/dist/assets/src/core/ingressTokens.ts +64 -0
  52. package/dist/assets/src/core/memory/engine.ts +115 -0
  53. package/dist/assets/src/core/memory/scorer.ts +147 -0
  54. package/dist/assets/src/core/memory/types.ts +120 -0
  55. package/dist/assets/src/core/normalizeSpans.ts +299 -0
  56. package/dist/assets/src/core/prDescriptionTypes.ts +54 -0
  57. package/dist/assets/src/core/redact.ts +113 -0
  58. package/dist/assets/src/core/runEvents.ts +537 -0
  59. package/dist/assets/src/core/runFriction.ts +665 -0
  60. package/dist/assets/src/core/runLedger/decisions.ts +126 -0
  61. package/dist/assets/src/core/runLedger/types.ts +177 -0
  62. package/dist/assets/src/core/runRecord.ts +627 -0
  63. package/dist/assets/src/core/runShape.ts +61 -0
  64. package/dist/assets/src/core/schedules.ts +452 -0
  65. package/dist/assets/src/core/time/formatDuration.ts +61 -0
  66. package/dist/assets/src/core/trace/attrs.ts +203 -0
  67. package/dist/assets/src/core/trace/classify.ts +49 -0
  68. package/dist/assets/src/core/trace/clock.ts +6 -0
  69. package/dist/assets/src/core/trace/context.ts +9 -0
  70. package/dist/assets/src/core/trace/ids.ts +23 -0
  71. package/dist/assets/src/core/trace/partition.ts +235 -0
  72. package/dist/assets/src/core/trace/sinks.ts +68 -0
  73. package/dist/assets/src/core/trace/streamSpans.ts +163 -0
  74. package/dist/assets/src/core/trace/traceparent.ts +29 -0
  75. package/dist/assets/src/core/trace/tracer.ts +247 -0
  76. package/dist/assets/src/core/trace/types.ts +125 -0
  77. package/dist/assets/src/core/trace/workerTrace.ts +97 -0
  78. package/dist/assets/src/deploy/buildStamp.ts +93 -0
  79. package/dist/assets/src/deploy/liveGate.ts +203 -0
  80. package/dist/assets/src/deploy/profile.ts +162 -0
  81. package/dist/assets/src/deploy/restart.ts +393 -0
  82. package/dist/assets/src/effort.ts +17 -0
  83. package/dist/assets/src/execution/bashTimeout.ts +78 -0
  84. package/dist/assets/src/execution/bindingPurge.ts +43 -0
  85. package/dist/assets/src/execution/residentBackupTransfer.ts +50 -0
  86. package/dist/assets/src/execution/residentCleanliness.ts +95 -0
  87. package/dist/assets/src/execution/residentCredentials.ts +81 -0
  88. package/dist/assets/src/execution/residentDepCache.ts +321 -0
  89. package/dist/assets/src/execution/residentDepsStore.ts +326 -0
  90. package/dist/assets/src/execution/residentDetach.ts +48 -0
  91. package/dist/assets/src/execution/residentDisk.ts +107 -0
  92. package/dist/assets/src/execution/residentDiskBudget.ts +448 -0
  93. package/dist/assets/src/execution/residentExecWrap.ts +100 -0
  94. package/dist/assets/src/execution/residentHead.ts +85 -0
  95. package/dist/assets/src/execution/residentReadonly.ts +72 -0
  96. package/dist/assets/src/execution/residentRefresh.ts +429 -0
  97. package/dist/assets/src/execution/residentRestoreExtract.ts +130 -0
  98. package/dist/assets/src/execution/residentState.ts +47 -0
  99. package/dist/assets/src/execution/residentStepReport.ts +98 -0
  100. package/dist/assets/src/execution/residentStepTrace.ts +97 -0
  101. package/dist/assets/src/execution/residentSteps.ts +99 -0
  102. package/dist/assets/src/execution/residentText.ts +83 -0
  103. package/dist/assets/src/execution/residentTrace.ts +119 -0
  104. package/dist/assets/src/execution/sandboxEnv.ts +42 -0
  105. package/dist/assets/src/execution/sandboxErrors.ts +159 -0
  106. package/dist/assets/src/execution/sandboxKeepalive.ts +118 -0
  107. package/dist/assets/src/execution/shellQuote.ts +8 -0
  108. package/dist/assets/src/mcp/registry.ts +242 -0
  109. package/dist/assets/src/providers/types.ts +152 -0
  110. package/dist/assets/web/dist/.vite/manifest.json +176 -0
  111. package/dist/assets/web/dist/assets/AppShell-Bk2gbvet.js +1 -0
  112. package/dist/assets/web/dist/assets/CostsPage-CTZcMYYx.js +1 -0
  113. package/dist/assets/web/dist/assets/NotFoundPage-C-BuaSm8.js +1 -0
  114. package/dist/assets/web/dist/assets/ResidentDetailPage-D3shEnzl.js +1 -0
  115. package/dist/assets/web/dist/assets/ResidentsIndexPage-DWIubQ05.js +1 -0
  116. package/dist/assets/web/dist/assets/RunRoutePage-BMjuE-oX.js +126 -0
  117. package/dist/assets/web/dist/assets/RunRoutePage-XVFj0XDc.css +1 -0
  118. package/dist/assets/web/dist/assets/RunsIndexPage-C3_jYIo0.js +1 -0
  119. package/dist/assets/web/dist/assets/RunsTabs-C4krAL9o.js +1 -0
  120. package/dist/assets/web/dist/assets/ScheduledPage-g1W58mtN.js +1 -0
  121. package/dist/assets/web/dist/assets/StatusDot-DcPRw3zu.js +1 -0
  122. package/dist/assets/web/dist/assets/Tooltip-DJUkMYjo.js +1 -0
  123. package/dist/assets/web/dist/assets/favicon-DL1rdWJt.js +1 -0
  124. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +1 -0
  125. package/dist/assets/web/dist/assets/main-BsBGUyMH.css +2 -0
  126. package/dist/assets/web/dist/assets/main-CyM5f4JC.js +28 -0
  127. package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +1 -0
  128. package/dist/assets/web/dist/assets/seed-BglCRKLA.js +6 -0
  129. package/dist/assets/web/dist/assets/wallClock-Ckv3sKoR.js +1 -0
  130. package/dist/cli.js +34494 -0
  131. package/package.json +43 -10
@@ -0,0 +1,359 @@
1
+ # Switchboard configuration.
2
+ # Copy to config/config.yaml and adjust. API keys come from env vars only.
3
+
4
+ # The GitHub organization (or user) this installation serves — the account the
5
+ # GitHub App is installed on. Required. It names the shared memory scope
6
+ # (`org:<organization>`) and the About block every model run carries; nothing
7
+ # in the code assumes a particular organization.
8
+ organization: acme
9
+
10
+ providers:
11
+ anthropic:
12
+ type: anthropic
13
+ apiKeyEnv: ANTHROPIC_API_KEY
14
+ # Any OpenAI-compatible endpoint works with the same adapter:
15
+ openai:
16
+ type: openai-compatible
17
+ baseUrl: https://api.openai.com/v1
18
+ apiKeyEnv: OPENAI_API_KEY
19
+ # groq:
20
+ # type: openai-compatible
21
+ # baseUrl: https://api.groq.com/openai/v1
22
+ # apiKeyEnv: GROQ_API_KEY
23
+ # local:
24
+ # type: openai-compatible
25
+ # baseUrl: http://localhost:11434/v1 # Ollama
26
+
27
+ defaults:
28
+ # Agent used when none is specified (per-channel/user/request can override).
29
+ agent: general
30
+ # Default model per agent, as <provider>/<model>.
31
+ models:
32
+ general: anthropic/claude-haiku-4-5
33
+ coding: anthropic/claude-opus-5
34
+ review: anthropic/claude-opus-5
35
+ # Default model effort per agent: low | medium | high (how hard the model
36
+ # thinks per turn; lower = much faster turns). Same layering as models —
37
+ # per-channel/user `effort` / `efforts.<agent>` and a per-request `effort:`
38
+ # directive override these; unset → the agent's built-in effort, else the
39
+ # provider default. Skipped for models without effort support.
40
+ # efforts:
41
+ # coding: medium
42
+
43
+ # PR reviews and the reading diff (docs/reference/specs/reading-diff.md). Every
44
+ # review records its full `git diff`; the ABRIDGED version (meat.dev, run on
45
+ # the bot host over the complete diff GitHub serves, with this bot's own
46
+ # Anthropic key) is on demand — `review abridge <run id>`, grant `review:write`
47
+ # — unless `provider: meat` makes it automatic after every review's record is
48
+ # written (one Opus-class call per review; meat caches by model + diff). `off`
49
+ # records no reading diff at all. `SWITCHBOARD_READING_DIFF=git|meat|off` in
50
+ # the environment overrides `provider` on a deployed bot. Needs `runHistory`.
51
+ # review:
52
+ # readingDiff:
53
+ # provider: git # git (default) | meat | off
54
+ # meatModel: claude-opus-5 # meat's -model; Opus-class is the floor that actually abridges
55
+ # meatTimeoutS: 240 # meat's own budget on the host; past it the abridging fails by name
56
+
57
+ # Static per-channel defaults (channel ID -> scope). Runtime overrides set via
58
+ # "config set channel ..." are stored in data/overrides.json and win over these.
59
+ # channels:
60
+ # slack:C012345:
61
+ # agent: review
62
+ # models:
63
+ # review: anthropic/claude-opus-5
64
+ # efforts:
65
+ # coding: medium
66
+
67
+ # Static per-user defaults (user ID -> scope).
68
+ # users:
69
+ # slack:U012345:
70
+ # model: openai/gpt-5
71
+
72
+ # Authorization (docs/reference/specs/authorization.md item 9, docs/reference/authorization.md):
73
+ # two blocks. `restrict` names what is CLOSED unless granted — an agent listed
74
+ # here runs only for an actor whose grants hold agent:run:<name> (or `all`); a
75
+ # repo listed here is used only by an actor whose `repos` axis names it (or
76
+ # `all`). Everything unlisted is open to everyone who can reach the bot.
77
+ # Enforcement is at run time against the resolved agent/repo, so it can't be
78
+ # bypassed via directives or config scopes. Names must be registered agents /
79
+ # owner/name slugs, or the load fails.
80
+ # restrict:
81
+ # agents: [coding]
82
+ # repos: [acme/api]
83
+ #
84
+ # `grants`: what ANY actor holds, in one shape, keyed by its platform-namespaced
85
+ # id — `slack:U…`, `http:<subject>`, `mcp:<subject>`, `access:<sub>`,
86
+ # `access:svc:<common_name>`, `schedule:<name>` — or by a whole surface,
87
+ # `<ns>:*`: `access:*` is every Cloudflare Access browser session (the org,
88
+ # granted once instead of enumerated), `slack:*` every workspace member the bot
89
+ # hears, `http:*` / `mcp:*` every ingress token. Each entry has up to three
90
+ # axes; every axis is a list of names or the explicit word `all`, and an ABSENT
91
+ # axis is the empty set (fail-closed). A `slack:` user holds the open chat
92
+ # commands and every unrestricted agent whether listed or not — an entry ADDS to
93
+ # that; an Access browser session holds every group's read the same way;
94
+ # credentials (`http:`, `mcp:`, `access:svc:`) hold exactly their entry and an
95
+ # unlisted one holds nothing, not even `dispatch`. A surface entry is unioned
96
+ # into every actor of its surface on top of its own entry — a person's entry
97
+ # never narrows it; `access:*` never reaches an `access:svc:` token. `config:write`
98
+ # (channel config), `repo:write` (repo onboard/offboard/…, friction propose) and
99
+ # every `runs:*` are never a baseline: nobody holds them until granted, and
100
+ # nobody is an admin without an `all` entry. An unknown id prefix, a misspelled
101
+ # `all` or a `*` that is not a whole surface (`slack:U*`, `schedule:*`,
102
+ # `agent:*`) fails the load, as does any top-level key this file does not document.
103
+ # grants:
104
+ # access:*: # everyone in the org: every Access browser session
105
+ # actions: all # holds everything — the org-wide pattern
106
+ # channels: all
107
+ # repos: all
108
+ # slack:U0ADMIN: # an admin, spelled out
109
+ # actions: all
110
+ # channels: all
111
+ # repos: all
112
+ # slack:U0456DEV:
113
+ # actions: [agent:run:coding, repo:write, friction:write]
114
+ # repos: [acme/api] # the repos this user may use
115
+ # access:svc:ops-bot: # an Access service token (its common_name)
116
+ # actions: [runs:read, runs:write, friction:read]
117
+ # channels: all # sees runs from every channel
118
+ # http:ci: # an ingress token's subject, as http: …
119
+ # actions: [dispatch, runs:read]
120
+ # channels: [http:ops] # … the channels whose runs it may read (a
121
+ # # token with no `channel` key and no entry
122
+ # # here reads none — the fail-closed default)
123
+ # mcp:ci: # … and the same subject over MCP
124
+ # actions: [dispatch, runs:read]
125
+ # channels: [mcp:ops]
126
+ # http:cron: # the identity the shim fires schedules as
127
+ # actions: [friction:read, friction:write, repo:write]
128
+ # channels: all # the weekly pass analyzes the whole fleet
129
+ # schedule:self-improvement: # the weekly cron's own actor (src/core/schedules.ts
130
+ # actions: [friction:write] # declares its grants; an entry here REPLACES them)
131
+ # channels: all # analyzes the whole fleet, not one channel
132
+
133
+ # Where agent tools execute. "local" runs them on the bot host (dev/CLI);
134
+ # "e2b" runs each thread's tools in its own E2B micro-VM — the repo checkout
135
+ # and GH_TOKEN live in the sandbox, never on the bot host. Recommended for
136
+ # any deployment where untrusted users can reach the coding agent.
137
+ execution:
138
+ type: local
139
+ # Cloudflare Sandbox via the proxy Worker (deploy/cloudflare-sandbox/):
140
+ # type: cloudflare
141
+ # url: https://switchboard-sandbox.<account>.workers.dev
142
+ # apiKeyEnv: SANDBOX_TOKEN # bearer secret shared with the Worker
143
+ # E2B:
144
+ # type: e2b
145
+ # apiKeyEnv: E2B_API_KEY
146
+ # timeoutMinutes: 30 # sandbox idle lifetime; thread follow-ups reconnect
147
+ # Resident repo environments (deploy/cloudflare-resident/): when set, a
148
+ # request whose target repo is onboarded and warm runs against the resident
149
+ # Worker; any other resident state falls back to the backend above with a
150
+ # named reason on the status card. Works alongside any per-thread type.
151
+ # Repos are onboarded from chat: `repo onboard <owner/name>` (see
152
+ # docs/how-to/onboard-a-repo.md; needs the `repo:write` grant — never a
153
+ # baseline, admins only until granted). `repo list` shows lifecycle;
154
+ # `repo offboard`/`repo rebuild` accept --dry-run for an itemized plan
155
+ # without executing.
156
+ # Deterministic ops (no model turn): `repo test <owner/name> [<ref>]` /
157
+ # `repo build <owner/name> [<ref>]` — and natural asks like "run the tests
158
+ # on main in acme/api" — run the repo's onboarded command in a disposable
159
+ # resident checkout and post the result with zero model calls. Operator-
160
+ # level: gated like a coding run — `restrict.agents` / `restrict.repos`
161
+ # against the caller's grants (NOT `repo:write`). Anything ambiguous falls through to the agent. With
162
+ # local execution the same asks run fixed Node commands in the thread's
163
+ # local workspace (dev-only).
164
+ # resident:
165
+ # baseUrl: https://switchboard-resident.<account>.workers.dev
166
+ # tokenEnv: RESIDENT_OPERATOR_TOKEN # operator bearer secret (default)
167
+ # adminTokenEnv: RESIDENT_ADMIN_TOKEN # admin bearer for `repo ...` commands (default)
168
+ # probeTimeoutMs: 2000 # /status probe budget; timeout = not warm
169
+
170
+ # Where per-conversation agent workspaces are created (local execution only).
171
+ # Under docker compose this directory is a named volume, so checkouts survive a restart.
172
+ workspaceDir: ./workspaces
173
+
174
+ # Cross-session self-learning memory (docs/reference/specs/memory.md). DEFAULT OFF: absent, or
175
+ # enabled:false, means the dispatcher uses a NullMemoryStore and the model input
176
+ # is byte-identical to memory-off — zero behavior change. When enabled, before
177
+ # each model turn the dispatcher retrieves scope-relevant distilled records
178
+ # (keyword + recency ranked, hard-budgeted) and injects them as a DEDICATED
179
+ # advisory context block on the system prompt ("Background memory for <resource>
180
+ # (may be outdated — verify before acting):"), never mixed into history. After
181
+ # each reply, a run that did real work (used tools, or a thread ≥4 turns deep)
182
+ # fires ONE async reflection call on `model` that distills ≤5 durable facts + 1
183
+ # summary into the store (secret-redacted, confidence-gated, dedup/supersede on
184
+ # write) — fire-and-forget, so it never delays or fails the reply. Records live
185
+ # in the Memory Worker (deploy/cloudflare-memory/: one SQLite Durable Object per
186
+ # scope, survives bot restarts); with no `worker` configured the bot falls back
187
+ # to an IN-PROCESS store and warns at startup — dev only. See docs/reference/specs/memory.md.
188
+ # memory:
189
+ # enabled: false # master switch (default false)
190
+ # worker:
191
+ # baseUrl: https://switchboard-memory.example.com
192
+ # tokenEnv: MEMORY_TOKEN # env var holding the Worker's bearer (default)
193
+ # # Scopes are derived per request, not configured: every request reads the
194
+ # # shared org scope (org:acme), the channel's scope (channel:slack:C…)
195
+ # # and — when the run is bound to a repo — that repo's scope (repo:owner/name)
196
+ # # plus the requesting user's own scope (user:slack:U…) — a
197
+ # # person's records never surface for anyone else.
198
+ # limit: 8 # max records retrieved/injected per request (default 8)
199
+ # maxTokens: 800 # hard token budget for the injected block (default 800)
200
+ # maxRecordsPerScope: 500
201
+ # # per-scope cap on ACTIVE records (default 500): a write
202
+ # # that would exceed it evicts the least recently used
203
+ # # records (soft delete, status `evicted`) in the same write.
204
+ # model: anthropic/claude-haiku-4-5
205
+ # # <provider>/<model> for the reflection pass — pick a
206
+ # # cheap tier. Absent → the run's own resolved model.
207
+
208
+ # Self-improvement proposals (docs/reference/specs/run-friction.md). Every finished run's friction
209
+ # diagnosis (docs/reference/specs/run-friction.md) is stored with its run record, so the
210
+ # runs these commands see are run history's (`runHistory` below; its retention
211
+ # bounds them — without `runHistory` there are no recent runs to analyze).
212
+ # `friction report` (open to everyone) shows the patterns that recur across
213
+ # recent runs; `friction propose [--dry-run]` (admin-gated like repo management)
214
+ # files the top patterns as labeled GitHub issues — evidence, affected runs, a
215
+ # concrete suggested fix — deduped against the proposals already open. It only
216
+ # ever PROPOSES: no PRs, nothing merged. Without `repo`, `friction propose`
217
+ # refuses. See docs/reference/specs/self-improvement.md.
218
+ # selfImprovement:
219
+ # repo: acme/switchboard # owner/name the proposals are filed against
220
+ # label: self-improvement # triage label (created if missing; default shown)
221
+ # minRuns: 2 # a pattern must recur in ≥ this many distinct runs
222
+ # top: 3 # proposals filed per pass
223
+
224
+ # Scheduled jobs. The schedule registry (src/core/schedules.ts) lists every
225
+ # cron the Cloudflare Worker shim runs; a `run` schedule fires as an ordinary run
226
+ # through /ingress as the `cron` identity — give that identity a token in
227
+ # SWITCHBOARD_INGRESS_TOKENS ({"…": {"subject": "cron", "channel": "cron"}}) and
228
+ # grant it what its command needs in `grants.http:cron` (`friction propose` →
229
+ # `friction:write`; a text command through /ingress is decided from the actor's
230
+ # grants like a tool call) with `channels: all` so the pass reads the fleet's
231
+ # runs, not the cron channel's own. The shim records each firing on the state Worker's
232
+ # ScheduleDO; this block tells the /runs "Scheduled" panel where to read them.
233
+ # Without it the panel lists the schedules with no firing history.
234
+ # schedules:
235
+ # worker:
236
+ # baseUrl: https://switchboard-memory.example.com
237
+ # tokenEnv: MEMORY_TOKEN # env var holding the Worker's bearer (default)
238
+
239
+ # agent:ship pipeline caps (docs/reference/specs/agent-ship.md). `agent:ship in owner/repo:
240
+ # <task>` runs the coding → review → fix loop to LGTM as one pipeline: at most
241
+ # `maxRounds` review rounds and `maxMinutes` minutes of wall clock — whichever
242
+ # hits first ends the loop, and each child round's own budget is clipped to the
243
+ # remaining pipeline time. Defaults shown; both must be integers >= 1.
244
+ # ship:
245
+ # maxRounds: 3 # review rounds per pipeline
246
+ # maxMinutes: 120 # pipeline wall-clock budget in minutes
247
+
248
+ # Spend reporting: GET /costs (Access-gated). Optional. See docs/reference/specs/costs.md.
249
+ # costs:
250
+ # cloudflareAccountId: <32-hex account id>
251
+ # cloudflareTokenEnv: CF_ANALYTICS_TOKEN # token with Account Analytics:Read
252
+ # anthropicAdminKeyEnv: ANTHROPIC_ADMIN_KEY # optional: sk-ant-admin… for LLM spend
253
+ # groups:
254
+ # myapp:
255
+ # label: My App
256
+ # workers: [myapp, myapp-worker] # Worker script names — the attribution root: their Durable
257
+ # # Objects, their requests/CPU, and the R2 buckets named <worker>-cache
258
+ # containerApps: # container application id → label (no dataset ties an app to a Worker)
259
+ # a0000000-0000-0000-0000-000000000000: server
260
+ # durableObjectNamespaces: # optional: DO namespace id → label; a namespace hosted by a listed
261
+ # <32-hex namespace id>: server DO # Worker is attributed anyway, labelled by that Worker
262
+ # r2Buckets: # optional: bucket name → label for buckets not named <worker>-cache
263
+ # some-other-bucket: uploads
264
+ # anthropicWorkspaceId: wrkspc_... # this app's Anthropic workspace
265
+
266
+ # Dashboard authentication (docs/reference/specs/access-gate.md). Everything the dashboard
267
+ # serves — /runs*, /residents*, /costs*, /mcp/connect/*, and every /api/* command
268
+ # — sits behind ONE identity gate; `auth` picks which credential it checks:
269
+ # access — the Cloudflare Access JWT the edge injects (ACCESS_TEAM_DOMAIN and
270
+ # ACCESS_AUD in the environment). Production behind an Access app.
271
+ # token — `Authorization: Bearer <token>`, the secret read from the env var
272
+ # `token.env` names (default DASHBOARD_TOKEN); a match is the one
273
+ # actor `token.actor` names, granted under `grants` like any browser
274
+ # session. For an Access-free deployment fronted by a proxy, curl or
275
+ # a script — a bare browser cannot send the header.
276
+ # none — no credential; served ONLY to loopback callers on a localhost
277
+ # deployment (PUBLIC_BASE_URL unset or naming localhost). A remote
278
+ # caller is refused, and an explicit `none` on a deployment with a
279
+ # public PUBLIC_BASE_URL is a startup error. Local development.
280
+ # Default when the block is absent: `access` if both ACCESS_* are set, else `none`.
281
+ # A strategy whose inputs are missing fails at startup, naming the piece.
282
+ # dashboard:
283
+ # auth: token
284
+ # token:
285
+ # env: DASHBOARD_TOKEN # env var holding the bearer (default)
286
+ # actor: access:ops # the actor the bearer resolves to — key its grants entry by this id
287
+
288
+ # Slack adapter — reconnect catch-up (docs/decisions/0012-reconnect-catch-up-as-recovery.md). Socket Mode drops every mention
289
+ # posted while the websocket is down (each bot deploy = drain + cold start).
290
+ # On every (re)connect the bot re-reads recent history of the channels it is in
291
+ # and runs whatever has no receipt from it (no :eyes:, no bot reply after it).
292
+ # Defaults: enabled, 30-minute window. The drain closes the socket on SIGTERM
293
+ # and the next container starts only after this one exits, so a deploy over a
294
+ # run in flight blacks Slack out for up to the 15-minute drain deadline plus a
295
+ # cold start — the window must be >= 20 minutes to cover that; a smaller
296
+ # value is kept but warned about at startup. Needs channels:read + groups:read
297
+ # (channel listing) on top of the history scopes.
298
+ # slack:
299
+ # catchUp:
300
+ # enabled: true
301
+ # windowMinutes: 30
302
+
303
+ # Where chat-set runtime overrides (`config set`, `config instructions`, `config clear`)
304
+ # persist (docs/reference/specs/routing-and-config.md item 12). Without this block they go to
305
+ # data/overrides.json on the host disk — EPHEMERAL on Cloudflare Containers. In
306
+ # production name the state Worker's ConfigDO (same Worker + bearer as memory and
307
+ # run history); the bot and the CLI then write one versioned document.
308
+ # runtimeOverrides:
309
+ # worker:
310
+ # baseUrl: https://switchboard-memory.example.com
311
+ # tokenEnv: MEMORY_TOKEN # default
312
+
313
+ # Persistent run history (docs/reference/specs/run-history.md). Every finished run's
314
+ # record — identity, timing, terminal status, the redacted event stream, the
315
+ # friction diagnosis — is kept for a retention window instead of evicted 60 s
316
+ # after finish, and the friction ledger's "recent runs" are read from it.
317
+ # Without this block history is OFF (runs stay live-only, as before).
318
+ # runHistory:
319
+ # retentionDays: 30 # days a finished run stays readable (1–365; default 30)
320
+ # maxRuns: 5000 # newest runs kept (1–20000; default 5000)
321
+ # maxBytes: 2147483648 # total stored bytes kept (16 MiB–8 GiB; default 2 GiB)
322
+ # includeContext: true # include the thread-context turns fed to the model in the
323
+ # # run stream — live page and persisted record (default true)
324
+ # store: worker # `worker` (default when `worker` is set) or `file` — an
325
+ # # explicit opt-in that writes data/runs/<id>.json on the
326
+ # # host disk (ephemeral on Cloudflare Containers)
327
+ # # The durable store: a RunHistoryDO on the state Worker (deploy/cloudflare-memory/,
328
+ # # the same Worker + bearer as memory and the friction ledger). Must be https:.
329
+ # worker:
330
+ # baseUrl: https://switchboard-memory.example.com
331
+ # tokenEnv: MEMORY_TOKEN # env var holding the Worker's bearer (default)
332
+
333
+ # External MCP servers as agent tools (docs/reference/specs/mcp-tools.md). Servers are a
334
+ # SETTING ON THE CONFIG SCOPES, not a list here: `defaults.mcpServers` (org-wide),
335
+ # `channels.<id>.mcpServers`, `users.<id>.mcpServers` — static below, or added at run
336
+ # time with `mcp add` (persisted with the other runtime overrides). A run gets the
337
+ # union of the three tiers; a name in more than one tier resolves to the highest-
338
+ # trust tier (org > channel > user). Only org entries may name coding/review/ship.
339
+ # Tools appear as `mcp__<name>__<tool>`; descriptions and results are untrusted
340
+ # data; every call is budgeted and recorded. URLs are SSRF-checked at load and at
341
+ # connect. This block turns the feature on and names the deployment knobs; without
342
+ # it MCP is off and the `mcp.*` commands answer `unavailable`.
343
+ # mcp:
344
+ # credentialKeyEnv: MCP_CREDENTIAL_KEY # default; 32 bytes base64 (`openssl rand -base64 32`),
345
+ # # bot-only — seals tokens entered on the connect page
346
+ # # secretsPath: ./data/mcp-secrets.json # sealed blobs + tickets when no runtimeOverrides.worker
347
+ #
348
+ # defaults:
349
+ # mcpServers:
350
+ # github:
351
+ # url: https://api.githubcopilot.com/mcp/
352
+ # auth: bearer
353
+ # tokenEnv: MCP_GITHUB_TOKEN # static bearer from the bot env (startup fails if unset)
354
+ # agents: [general, research, coding] # default [general, research]
355
+ # channels:
356
+ # "slack:C0123":
357
+ # mcpServers:
358
+ # notion: { url: https://mcp.notion.so/mcp, auth: none } # general/research only
359
+ # vanta: { url: https://mcp.vanta.com/mcp, auth: oauth } # OAuth 2.1: a channel-config holder signs in once via `mcp connect vanta --scope channel`
@@ -0,0 +1,15 @@
1
+ // Types for build-stamp.mjs (plain-Node ESM so a Worker's `npm run deploy`
2
+ // needs no build step). The runtime shape it produces is read back by
3
+ // `src/deploy/buildStamp.ts` (`BuildStamp`).
4
+ export type Stamp = { commit: string; builtAt: string };
5
+ export const DEFINE_COMMIT: "SWITCHBOARD_BUILD_COMMIT";
6
+ export const DEFINE_BUILT_AT: "SWITCHBOARD_BUILT_AT";
7
+ export function buildStamp(input: { commit: string; dirty: boolean; now?: Date }): Stamp;
8
+ export function stampFromEnv(env: Record<string, string | undefined>, now?: Date): Stamp | undefined;
9
+ export function defineArgs(stamp: Stamp): string[];
10
+ export function spawnOutcome(res: {
11
+ error?: { message: string };
12
+ signal?: NodeJS.Signals | null;
13
+ status?: number | null;
14
+ }): { code: number; message?: string };
15
+ export function main(extraArgs?: string[]): number;
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ // Stamp the build identity into a Worker SCRIPT deploy (docs/reference/specs/execution.md
3
+ // item 13) — the resident, memory and sandbox Workers' `npm run deploy` runs
4
+ // this instead of bare `wrangler deploy`.
5
+ //
6
+ // It derives `{commit, builtAt}` from the tree being deployed and hands them to
7
+ // `wrangler deploy --define`, so the bundle carries the commit and the Worker
8
+ // answers it on `/healthz` (`src/deploy/buildStamp.ts` is the reader). The bot
9
+ // does the same thing through a COPYed `build.json` (`../cloudflare/write-build.mjs`)
10
+ // because a container needs a file; a Worker script needs a substitution — the
11
+ // reader module explains why a generated file cannot work here.
12
+ //
13
+ // Dependency-free Node. `buildStamp` and `defineArgs` are pure given their
14
+ // inputs and unit-tested from `src/deploy/buildStamp.test.ts`; `main()` does
15
+ // the git reads and the spawn.
16
+ import { execFileSync, spawnSync } from "node:child_process";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+
20
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
21
+
22
+ /** The identifiers substituted into the bundle. `src/deploy/buildStamp.ts`
23
+ * declares and reads exactly these two. */
24
+ export const DEFINE_COMMIT = "SWITCHBOARD_BUILD_COMMIT";
25
+ export const DEFINE_BUILT_AT = "SWITCHBOARD_BUILT_AT";
26
+
27
+ /** `{ commit, builtAt }` — `commit` gets a `-dirty` suffix when the tree has
28
+ * uncommitted changes, because wrangler bundles the TREE, not the commit. */
29
+ export function buildStamp({ commit, dirty, now = new Date() }) {
30
+ return { commit: `${commit}${dirty ? "-dirty" : ""}`, builtAt: now.toISOString() };
31
+ }
32
+
33
+ /** The `--define` argv for a stamp. `JSON.stringify` is what makes each value
34
+ * a JS string LITERAL rather than an expression: esbuild substitutes the text
35
+ * verbatim, so an unquoted value would be parsed as code. */
36
+ export function defineArgs(stamp) {
37
+ return [
38
+ "--define",
39
+ `${DEFINE_COMMIT}:${JSON.stringify(stamp.commit)}`,
40
+ "--define",
41
+ `${DEFINE_BUILT_AT}:${JSON.stringify(stamp.builtAt)}`,
42
+ ];
43
+ }
44
+
45
+ /** The identity the environment hands a deploy that has no tree to read: a
46
+ * deploy from the published npm package runs this script in a materialised
47
+ * copy of the Worker's directory, where git knows nothing, and sets
48
+ * `SWITCHBOARD_BUILD_COMMIT` to the commit the package was built from
49
+ * (src/deploy/run.ts). `builtAt` is still now — each deploy is its own build.
50
+ * Undefined when the variable is unset or blank: git decides. Pure. */
51
+ export function stampFromEnv(env, now = new Date()) {
52
+ const commit = env[DEFINE_COMMIT]?.trim();
53
+ return commit ? { commit, builtAt: now.toISOString() } : undefined;
54
+ }
55
+
56
+ /** Read the tree's identity: the environment's when it names one, else git. A
57
+ * git failure (deploying from an export, no git on PATH) warns and stamps
58
+ * `unknown` rather than blocking the deploy — the Worker then reports
59
+ * `commit: "unknown"`, which is the honest answer. */
60
+ function readStamp() {
61
+ const given = stampFromEnv(process.env);
62
+ if (given) return given;
63
+ try {
64
+ const git = (args) => execFileSync("git", args, { cwd: REPO_ROOT, encoding: "utf8" }).trim();
65
+ return buildStamp({ commit: git(["rev-parse", "HEAD"]), dirty: git(["status", "--porcelain"]) !== "" });
66
+ } catch (err) {
67
+ console.warn(`[build-stamp] git failed (${err?.message ?? err}) — deploying with commit "unknown"`);
68
+ return { commit: "unknown", builtAt: new Date().toISOString() };
69
+ }
70
+ }
71
+
72
+ /** How a finished spawn becomes this script's exit code, plus the line to
73
+ * print. A signal kill (Ctrl-C, a CI timeout) reports `status: null`, so
74
+ * without naming the signal an interrupted deploy is indistinguishable from
75
+ * a wrangler that simply failed. */
76
+ export function spawnOutcome({ error, signal, status }) {
77
+ if (error) return { code: 1, message: `could not run wrangler: ${error.message}` };
78
+ if (signal) return { code: 1, message: `wrangler was killed by ${signal} — the deploy did not finish` };
79
+ if (typeof status !== "number") return { code: 1, message: "wrangler exited with no status" };
80
+ return { code: status };
81
+ }
82
+
83
+ export function main(extraArgs = []) {
84
+ const stamp = readStamp();
85
+ const args = ["deploy", ...defineArgs(stamp), ...extraArgs];
86
+ console.log(`[build-stamp] ${stamp.commit} @ ${stamp.builtAt}`);
87
+ // `wrangler` resolves through the calling package's node_modules/.bin, which
88
+ // npm puts on PATH for a `npm run deploy`. shell:false — no argument of this
89
+ // command may ever be interpreted by a shell.
90
+ const outcome = spawnOutcome(spawnSync("wrangler", args, { stdio: "inherit", cwd: process.cwd() }));
91
+ if (outcome.message) console.error(`[build-stamp] ${outcome.message}`);
92
+ return outcome.code;
93
+ }
94
+
95
+ // Run only when executed directly, not when imported by the tests.
96
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
97
+ process.exitCode = main(process.argv.slice(2));
98
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Query historical Workers Logs for one of the installation's Workers (the
3
+ # "what did the sandbox/resident/bot Worker log at 15:26?" question `wrangler
4
+ # tail` can't answer — tail is live-only, and the wrangler OAuth login has no
5
+ # observability scope). Needs an API token with Account → Workers
6
+ # Observability: Read (+ Workers Scripts: Read) in CLOUDFLARE_OBS_TOKEN —
7
+ # deliberately NOT CLOUDFLARE_API_TOKEN, which wrangler would pick up. The
8
+ # account is the deployment profile's (deploy/profile.json, or the file
9
+ # SWITCHBOARD_DEPLOY_PROFILE names).
10
+ #
11
+ # deploy/bin/cf-logs <service> [minutes-back=60] [grep-pattern]
12
+ # deploy/bin/cf-logs switchboard-sandbox 240 'exit|OOM|error'
13
+ set -euo pipefail
14
+ PROFILE=${SWITCHBOARD_DEPLOY_PROFILE:-"$(dirname "$0")/../profile.json"}
15
+ ACCOUNT=$(node -p 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).account' "$PROFILE")
16
+ SERVICE=${1:?service (switchboard | switchboard-sandbox | switchboard-resident | switchboard-memory)}
17
+ MINUTES=${2:-60}
18
+ PATTERN=${3:-}
19
+ : "${CLOUDFLARE_OBS_TOKEN:?set CLOUDFLARE_OBS_TOKEN (Workers Observability: Read on the profile's account)}"
20
+ NOW=$(date +%s)
21
+ FROM=$(( (NOW - MINUTES * 60) * 1000 )); TO=$(( NOW * 1000 ))
22
+ BODY=$(cat <<JSON
23
+ {"queryId":"cf-logs","timeframe":{"from":$FROM,"to":$TO},
24
+ "parameters":{"datasets":["cloudflare-workers"],
25
+ "filters":[{"key":"\$metadata.service","operation":"eq","value":"$SERVICE","type":"string"}]},
26
+ "view":"events","limit":500}
27
+ JSON
28
+ )
29
+ curl -sS -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT/workers/observability/telemetry/query" \
30
+ -H "Authorization: Bearer $CLOUDFLARE_OBS_TOKEN" -H 'content-type: application/json' -d "$BODY" \
31
+ | jq -r '.result.events.events[]? | "\(.timestamp | tostring | .[0:10] | tonumber | todate) \(.["$metadata"].level // "log") \(.["$metadata"].message // (.["$workers"].event // "" | tostring))"' \
32
+ | { if [ -n "$PATTERN" ]; then grep -E "$PATTERN" || true; else cat; fi; }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "switchboard-worker",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=22"
7
+ },
8
+ "scripts": {
9
+ "check:image": "docker build --quiet ../..",
10
+ "deploy": "node preflight.mjs && node write-build.mjs && wrangler deploy",
11
+ "preflight": "node preflight.mjs",
12
+ "test": "vitest run",
13
+ "verify": "npm --prefix ../.. run --silent deploy:gen && npm run typecheck && npm test",
14
+ "dev": "wrangler dev",
15
+ "typecheck": "tsc --noEmit -p tsconfig.json",
16
+ "typegen": "wrangler types",
17
+ "tail": "wrangler tail",
18
+ "secrets": "npm --prefix ../.. run --silent cli -- deploy secrets bot"
19
+ },
20
+ "dependencies": {
21
+ "@cloudflare/containers": "^0.3.7"
22
+ },
23
+ "devDependencies": {
24
+ "@cloudflare/workers-types": "^5.20260904.1",
25
+ "typescript": "^5.9.3",
26
+ "vitest": "^5.0.0",
27
+ "wrangler": "^4.129.0"
28
+ }
29
+ }