@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,104 @@
1
+ {
2
+ "name": "switchboard",
3
+ "version": "1.18.0",
4
+ "private": true,
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
+ "license": "Apache-2.0",
7
+ "homepage": "https://github.com/coreplanelabs/switchboard#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/coreplanelabs/switchboard.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/coreplanelabs/switchboard/issues"
14
+ },
15
+ "type": "module",
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "main": "dist/index.js",
20
+ "bin": {
21
+ "switchboard": "dist/cli.js"
22
+ },
23
+ "workspaces": [
24
+ "web",
25
+ "docs",
26
+ "deploy/cloudflare",
27
+ "deploy/cloudflare-memory",
28
+ "deploy/cloudflare-resident",
29
+ "deploy/cloudflare-sandbox",
30
+ "deploy/cloudflare-docs",
31
+ "packages/switchboard"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "start": "node dist/index.js",
36
+ "dev": "tsx src/index.ts",
37
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.scripts.json",
38
+ "test": "npm run --silent deploy:gen && vitest run",
39
+ "cli": "tsx src/cli.ts",
40
+ "verify": "npm run verify:root && npm run verify --workspaces --if-present && npm run check:site",
41
+ "verify:root": "npm run check:consistency && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:dist",
42
+ "check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run licenses:check && npm run docs:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run agents:check && npm run clock:check && npm run screenshots:check",
43
+ "ci:gate": "node scripts/ci-gate.mjs",
44
+ "fix": "npm run docs:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
45
+ "deploy:gen": "npm run --silent cli -- deploy init",
46
+ "deploy:check": "npm run --silent cli -- deploy init --check",
47
+ "check:lockfile": "node scripts/check-lockfile.mjs",
48
+ "check:sandbox-pair": "node scripts/check-sandbox-pair.mjs",
49
+ "check:pr-title": "node scripts/check-pr-title.mjs",
50
+ "check:project-facts": "node scripts/check-project-facts.mjs",
51
+ "agents:gen": "tsx scripts/agents-gen.ts",
52
+ "clock:gen": "tsx scripts/clock-allowlist.mts --write",
53
+ "clock:check": "tsx scripts/clock-allowlist.mts",
54
+ "agents:check": "tsx scripts/agents-gen.ts --check",
55
+ "lint": "eslint .",
56
+ "lint:fix": "eslint . --fix",
57
+ "format": "prettier --write .",
58
+ "format:check": "prettier --check .",
59
+ "check:dist": "npm run build && node scripts/check-dist.mjs",
60
+ "check:site": "node scripts/check-site.mjs",
61
+ "check:image": "npm run check:image --workspaces --if-present",
62
+ "skills:sync": "tsx scripts/skills-sync.ts",
63
+ "skills:check": "tsx scripts/skills-sync.ts --check",
64
+ "licenses:check": "node scripts/licenses-check.mjs",
65
+ "docs:gen": "tsx scripts/docs-gen.ts",
66
+ "docs:check": "tsx scripts/docs-gen.ts --check",
67
+ "specs:check": "node scripts/specs-check.mjs",
68
+ "specs:coverage": "tsx scripts/specs-coverage.ts",
69
+ "decisions:check": "tsx scripts/decisions-check.ts",
70
+ "hygiene:check": "node scripts/public-hygiene.mjs",
71
+ "hygiene:gen": "node scripts/public-hygiene.mjs --write",
72
+ "docs:changed": "node scripts/docs-changed.mjs",
73
+ "deploy:targets": "node scripts/deploy-targets.mjs",
74
+ "docs:dev": "npm run dev -w docs --",
75
+ "docs:build": "npm run build -w docs",
76
+ "web:preview": "tsx scripts/web-preview.ts",
77
+ "screenshots:gen": "tsx scripts/screenshots.mts",
78
+ "screenshots:check": "tsx scripts/screenshots.mts --check",
79
+ "load": "tsx scripts/load.ts"
80
+ },
81
+ "dependencies": {
82
+ "@anthropic-ai/sdk": "^0.124.0",
83
+ "@slack/bolt": "^5.1.0",
84
+ "e2b": "^2.46.1",
85
+ "mdast-util-from-markdown": "^2.0.3",
86
+ "undici": "^8.10.2",
87
+ "yaml": "^2.7.0",
88
+ "zod": "^4.5.4"
89
+ },
90
+ "devDependencies": {
91
+ "@eslint/js": "^10.0.1",
92
+ "@types/node": "^24.0.0",
93
+ "eslint": "^10.10.0",
94
+ "eslint-config-prettier": "^10.1.8",
95
+ "eslint-plugin-vue": "^10.11.0",
96
+ "globals": "^17.12.0",
97
+ "prettier": "^3.9.6",
98
+ "tsx": "^4.23.13",
99
+ "typescript": "^5.9.3",
100
+ "typescript-eslint": "^8.69.0",
101
+ "vitest": "^5.0.0",
102
+ "vue-eslint-parser": "^10.4.1"
103
+ }
104
+ }
@@ -0,0 +1,219 @@
1
+ {
2
+ "$comment": "The one place the project says who it is. Everything that names the project, its home, its docs, or its contact reads this file or is checked against it (npm run check:project-facts). Change a value here and the check names every copy that has to follow.",
3
+ "name": "switchboard",
4
+ "displayName": "OpenSwitchboard",
5
+ "npmPackage": "@coreplane/switchboard",
6
+ "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.",
7
+ "organization": "coreplanelabs",
8
+ "repository": "https://github.com/coreplanelabs/switchboard",
9
+ "image": "ghcr.io/coreplanelabs/switchboard",
10
+ "images": {
11
+ "bot": "ghcr.io/coreplanelabs/switchboard",
12
+ "resident": "ghcr.io/coreplanelabs/switchboard-resident",
13
+ "sandbox": "ghcr.io/coreplanelabs/switchboard-sandbox"
14
+ },
15
+ "docs": "https://openswitchboard.dev",
16
+ "topics": [
17
+ "ai-agents",
18
+ "coding-agent",
19
+ "code-review",
20
+ "slack-bot",
21
+ "github-app",
22
+ "mcp-server",
23
+ "cloudflare-workers",
24
+ "llm",
25
+ "typescript"
26
+ ],
27
+ "contact": "dev@coreplane.ai",
28
+ "steward": {
29
+ "name": "Coreplane Labs",
30
+ "url": "https://coreplane.ai"
31
+ },
32
+ "commands": {
33
+ "$comment": "What each root npm script does and when to run it — the source of the Commands table in AGENTS.md (npm run agents:gen). A script without an entry fails agents:check, so a new script cannot ship undocumented.",
34
+ "verify": {
35
+ "does": "The whole gate: every root check, every workspace's verify, the site check — exactly what CI runs.",
36
+ "when": "Before requesting review. ~4 min."
37
+ },
38
+ "verify:root": {
39
+ "does": "The bot package's gate: consistency checks, typecheck, lint, format, tests, dist.",
40
+ "when": "When only the bot changed."
41
+ },
42
+ "check:consistency": {
43
+ "does": "The sub-second checks that generated and declared things equal the code (lockfile, sandbox pairs, skills, licenses, docs tables, specs, project facts, this table).",
44
+ "when": "After touching a generated or declared artifact; one CI leg."
45
+ },
46
+ "test": {
47
+ "does": "The whole vitest suite (bot, web, the plain-Node Worker tests) from one entry, after `deploy:gen`.",
48
+ "when": "Before pushing."
49
+ },
50
+ "typecheck": {
51
+ "does": "TypeScript over the bot and its scripts, no emit.",
52
+ "when": "After type-level changes; `verify:root` runs it."
53
+ },
54
+ "lint": {
55
+ "does": "ESLint over the whole tree.",
56
+ "when": "`npm run fix` repairs what it can."
57
+ },
58
+ "lint:fix": {
59
+ "does": "ESLint with autofix.",
60
+ "when": "Part of `fix`."
61
+ },
62
+ "format": {
63
+ "does": "Prettier over the whole tree (not Markdown).",
64
+ "when": "Part of `fix`."
65
+ },
66
+ "format:check": {
67
+ "does": "Prettier in check mode.",
68
+ "when": "Part of `verify:root`."
69
+ },
70
+ "fix": {
71
+ "does": "Regenerates every generated artifact and repairs lint and formatting.",
72
+ "when": "Before committing; whenever `check:consistency` reports drift."
73
+ },
74
+ "build": {
75
+ "does": "Compiles the bot to `dist/`.",
76
+ "when": "`check:dist` and the Docker image run it; rarely by hand."
77
+ },
78
+ "start": {
79
+ "does": "Runs the compiled bot from `dist/`.",
80
+ "when": "Production entry (the container's CMD)."
81
+ },
82
+ "dev": {
83
+ "does": "Runs the bot from source with tsx.",
84
+ "when": "Local development against a real Slack app."
85
+ },
86
+ "cli": {
87
+ "does": "The operator CLI over the command registry (`-- <group> <verb> …`), plus `ask` to drive the full pipeline without Slack.",
88
+ "when": "Smoke tests, deploys, config, run history."
89
+ },
90
+ "ci:gate": {
91
+ "does": "Reads the `needs` context of a CI fan-out and passes only when every leg succeeded.",
92
+ "when": "CI only — the `bot` and `workers` gate jobs."
93
+ },
94
+ "check:lockfile": {
95
+ "does": "Native packages carry Linux x64 and macOS arm64 variants; records mirror their `package.json`.",
96
+ "when": "After a manifest edit or `npm install`; failures name the fix."
97
+ },
98
+ "check:sandbox-pair": {
99
+ "does": "Each Worker on the `cloudflare/sandbox` image pins `@cloudflare/sandbox` to exactly its Dockerfile tag.",
100
+ "when": "After bumping either half of a pair."
101
+ },
102
+ "check:pr-title": {
103
+ "does": "Judges one PR title as the changelog line it becomes: grammar, type, scope, the migration note behind `!`.",
104
+ "when": "`-- \"feat(scope): …\"` before opening a PR; CI's `title` check runs it."
105
+ },
106
+ "check:dist": {
107
+ "does": "Builds, then proves the compiled entry points and the web bundle the image needs exist.",
108
+ "when": "Part of `verify:root`."
109
+ },
110
+ "check:site": {
111
+ "does": "Every page the dashboard links to is in the built site, and its home page reads `displayName`.",
112
+ "when": "After a docs change; part of `verify`."
113
+ },
114
+ "check:image": {
115
+ "does": "Builds every Worker's image.",
116
+ "when": "After touching a Dockerfile; needs Docker. One CI leg per image."
117
+ },
118
+ "check:project-facts": {
119
+ "does": "Every copy of the project's names, repository, docs URL and contact address equals `project.json`; its description, topics and npm package fit their rules.",
120
+ "when": "After editing `project.json` or a community file; part of `check:consistency`."
121
+ },
122
+ "skills:sync": {
123
+ "does": "Vendors the skills listed in `skills/manifest.yaml`.",
124
+ "when": "After changing the manifest; part of `fix`."
125
+ },
126
+ "skills:check": {
127
+ "does": "The vendored skills match the manifest byte for byte.",
128
+ "when": "Part of `check:consistency`."
129
+ },
130
+ "licenses:check": {
131
+ "does": "Every production dependency's license is on the allowlist.",
132
+ "when": "After adding a dependency."
133
+ },
134
+ "docs:gen": {
135
+ "does": "Writes the generated regions of the reference docs from the command registry.",
136
+ "when": "After changing a command, flag, route, or config key; part of `fix`."
137
+ },
138
+ "docs:check": {
139
+ "does": "The generated doc regions equal what the code would generate.",
140
+ "when": "Part of `check:consistency`."
141
+ },
142
+ "specs:check": {
143
+ "does": "Every `file::describe::it` proof in `docs/reference/specs/*.md` names a real test; header paths exist.",
144
+ "when": "After renaming a test or editing a spec; `-- --fix` makes truncated titles explicit."
145
+ },
146
+ "specs:coverage": {
147
+ "does": "Maps a change's paths to the specs whose `Code`/`Tests` headers cover them, then lists changed source paths no spec covers.",
148
+ "when": "`-- --changed origin/main...HEAD` before review; `-- --require` fails on an uncovered path (warn-only today); `-- --json` for machines."
149
+ },
150
+ "decisions:check": {
151
+ "does": "Every record under `docs/decisions/` and `docs/plans/` carries a valid `status`, a superseded one names what replaced it, and an accepted record's body is unchanged against `origin/main`.",
152
+ "when": "Part of `check:consistency`; a failing record is superseded by a new one, never edited."
153
+ },
154
+ "hygiene:check": {
155
+ "does": "The public tree's imprint (company, people, trackers, plan ids, ids, dates) equals the recorded list, which only shrinks.",
156
+ "when": "Part of `check:consistency`. New hit: rewrite the line or allow it by name in `scripts/public-hygiene.allow`; `-- --list <prefix>` shows the rest."
157
+ },
158
+ "hygiene:gen": {
159
+ "does": "Records the tree's remaining imprint after a scrub; refuses growth unless `-- --force`.",
160
+ "when": "Part of `fix`; new imprint fails it like `hygiene:check`."
161
+ },
162
+ "agents:gen": {
163
+ "does": "Writes the Commands table in AGENTS.md from `package.json` and this file.",
164
+ "when": "After adding or changing a script; part of `fix`."
165
+ },
166
+ "agents:check": {
167
+ "does": "AGENTS.md is under its size budget, its Commands table is current, and every root script is described here.",
168
+ "when": "Part of `check:consistency`."
169
+ },
170
+ "clock:gen": {
171
+ "does": "Regenerates the clock-read allowlist (`src/core/trace/clockAllowlist.json`) from the tree — empty since the ratchet reached zero; a result that is not `{}` names a new direct read.",
172
+ "when": "Part of `fix`."
173
+ },
174
+ "clock:check": {
175
+ "does": "No production file reads the wall clock directly: the allowlist is empty and the tree agrees.",
176
+ "when": "Part of `check:consistency`."
177
+ },
178
+ "docs:changed": {
179
+ "does": "Says whether the last push touched the docs or their build (a CI job output).",
180
+ "when": "CI only — gates the docs deploy."
181
+ },
182
+ "deploy:gen": {
183
+ "does": "Renders each Worker's gitignored `wrangler.jsonc` from its template and the profile in force.",
184
+ "when": "`test`, each Worker's `verify` and `deploy all` run it; by hand before `wrangler dev`."
185
+ },
186
+ "deploy:check": {
187
+ "does": "The rendered `wrangler.jsonc` files match `deploy:gen`.",
188
+ "when": "When one looks hand-edited; change the template."
189
+ },
190
+ "deploy:targets": {
191
+ "does": "Which Workers a PR's diff would deploy, as a job summary; on the release PR, a sticky comment.",
192
+ "when": "CI only — the `deploy targets` job."
193
+ },
194
+ "docs:dev": {
195
+ "does": "Serves the docs site locally with live reload.",
196
+ "when": "Writing docs; `-- --port <n>` picks the port."
197
+ },
198
+ "docs:build": {
199
+ "does": "Builds the docs site to `docs/.vitepress/dist`.",
200
+ "when": "Rarely by hand; `verify -w docs` and the docs Worker's deploy run it."
201
+ },
202
+ "web:preview": {
203
+ "does": "Serves the dashboard bundle over fixtures for a visual check.",
204
+ "when": "After a `web/` change."
205
+ },
206
+ "screenshots:gen": {
207
+ "does": "Renders the dashboard's screenshots from the fixture preview, both themes, and records their inputs' hashes in `docs/public/screenshots/manifest.json`.",
208
+ "when": "After a `web/` or fixture change, once `screenshots:check` names it; needs `npx playwright-core install chromium`, so it is not part of `fix`."
209
+ },
210
+ "screenshots:check": {
211
+ "does": "The dashboard's source and fixtures still hash to what the screenshots were rendered from — no browser.",
212
+ "when": "Part of `check:consistency`."
213
+ },
214
+ "load": {
215
+ "does": "Load harness: `-- history|resident|sandbox|e2e|cards|provider`.",
216
+ "when": "Capacity receipts (docs/reference/specs/load-harness.md)."
217
+ }
218
+ }
219
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": "1.18.0",
3
+ "commit": "30eddf412e567d1989a3d01e7f926536dc54d932",
4
+ "builtAt": "2026-09-10T20:01:13.759Z"
5
+ }
@@ -0,0 +1,100 @@
1
+ import { ALL_GRANTS } from "./grants.js";
2
+ import type { Actor, Grants } from "./types.js";
3
+
4
+ // Actor resolution — adapters resolve identity, never authority: each adapter
5
+ // proves WHO is asking (a Slack user id, an ingress token's subject, an Access
6
+ // `sub` or service token `common_name`, the local CLI, a schedule registry
7
+ // entry) and hands it here; this module gives it a kind, its
8
+ // platform-namespaced id (invariant 4) and its grants — looked up by id through
9
+ // the one `GrantsLookup` config owns. No decision lives here: nothing below
10
+ // says what an actor MAY do.
11
+
12
+ /** What an adapter can prove about a caller's surface. */
13
+ export type ActorSurface = "slack" | "http" | "mcp" | "access-browser" | "access-service" | "cli" | "schedule";
14
+
15
+ export interface ActorInput {
16
+ surface: ActorSurface;
17
+ /** The bare subject the surface authenticated: `U…` for Slack, the token's
18
+ * `subject`, the Access `sub` / `common_name`, the schedule's `name`. */
19
+ subjectId: string;
20
+ /** Where a chat actor is speaking from; both or neither (context, never authority). */
21
+ channelId?: string;
22
+ threadKey?: string;
23
+ }
24
+
25
+ /** `ConfigStore.grantsFor` — or `grantsFor(id, source)` for callers without a store. */
26
+ export type GrantsLookup = (actorId: string) => Grants;
27
+
28
+ /** The local CLI: one operator at a keyboard, every grant, no config consulted. */
29
+ export const CLI_ACTOR: Actor = Object.freeze({ kind: "user", id: "cli:local", grants: ALL_GRANTS });
30
+
31
+ /** The one id form per surface. */
32
+ export function actorIdFor(surface: ActorSurface, subjectId: string): string {
33
+ switch (surface) {
34
+ case "slack":
35
+ return `slack:${subjectId}`;
36
+ case "http":
37
+ return `http:${subjectId}`;
38
+ case "mcp":
39
+ return `mcp:${subjectId}`;
40
+ case "access-browser":
41
+ return `access:${subjectId}`;
42
+ case "access-service":
43
+ return `access:svc:${subjectId}`;
44
+ case "cli":
45
+ return CLI_ACTOR.id;
46
+ case "schedule":
47
+ return `schedule:${subjectId}`;
48
+ }
49
+ }
50
+
51
+ function kindFor(surface: ActorSurface): Actor["kind"] {
52
+ switch (surface) {
53
+ case "slack":
54
+ case "access-browser":
55
+ case "cli":
56
+ return "user";
57
+ case "http":
58
+ case "mcp":
59
+ case "access-service":
60
+ return "service";
61
+ case "schedule":
62
+ return "schedule";
63
+ }
64
+ }
65
+
66
+ /** The `Actor` for what an adapter proved: kind from the surface, namespaced
67
+ * id, grants from config by that id (the CLI always holds everything), and
68
+ * `origin` when the input names both a channel and a thread. */
69
+ export function resolveActor(input: ActorInput, grantsFor: GrantsLookup): Actor {
70
+ if (input.surface === "cli") return CLI_ACTOR;
71
+ const id = actorIdFor(input.surface, input.subjectId);
72
+ const origin =
73
+ input.channelId !== undefined && input.threadKey !== undefined
74
+ ? { channelId: input.channelId, threadKey: input.threadKey }
75
+ : undefined;
76
+ return { kind: kindFor(input.surface), id, grants: grantsFor(id), ...(origin ? { origin } : {}) };
77
+ }
78
+
79
+ const CHAT_SURFACES: Readonly<Record<string, ActorSurface>> = {
80
+ slack: "slack",
81
+ http: "http",
82
+ mcp: "mcp",
83
+ cli: "cli",
84
+ schedule: "schedule",
85
+ };
86
+
87
+ /** A chat message's `userId` is already namespaced by its adapter (`slack:U…`,
88
+ * `http:<subject>`, `mcp:<subject>`, `cli:local`, `schedule:<name>`): the prefix picks the surface.
89
+ * A namespace this module does not know stays a `user` with the id as given —
90
+ * its grants are whatever config names for that id, never a guess. */
91
+ export function resolveChatActor(
92
+ msg: { userId: string; channelId: string; threadKey: string },
93
+ grantsFor: GrantsLookup,
94
+ ): Actor {
95
+ const colon = msg.userId.indexOf(":");
96
+ const surface = colon > 0 ? CHAT_SURFACES[msg.userId.slice(0, colon)] : undefined;
97
+ const origin = { channelId: msg.channelId, threadKey: msg.threadKey };
98
+ if (surface === undefined) return { kind: "user", id: msg.userId, grants: grantsFor(msg.userId), origin };
99
+ return resolveActor({ surface, subjectId: msg.userId.slice(colon + 1), ...origin }, grantsFor);
100
+ }
@@ -0,0 +1,169 @@
1
+ // The ONE decision (docs/decisions/0007-authorization-policy-table.md):
2
+ // `authorize(actor, action, resource)`.
3
+ //
4
+ // Rows for the (action, target) OR; a row's conditions AND; no row → deny.
5
+ // Deny reasons are short machine-readable tokens carrying no resource id or
6
+ // attribute — an audit line may log them, a reply never renders them.
7
+ //
8
+ // Grant arithmetic lives here too. Action grants may be wildcards: `<prefix>:*`
9
+ // covers every action under the prefix (`agent:run:*` ⊇ `agent:run:coding`).
10
+ // Channel and repo sets are literal ids — never wildcarded — so a
11
+ // `channels-in` predicate is always a plain `IN (...)` a store can run.
12
+
13
+ import { ACTOR_KINDS, POLICY, resolveGrant, ruleTarget } from "./policy.js";
14
+ import { attributesOf, targetOfResource, type ResourceAttributes } from "./resource.js";
15
+ import type { Action, Actor, Condition, Decision, Grants, GrantSet, Resource, Rule } from "./types.js";
16
+
17
+ // ── grants ───────────────────────────────────────────────────────────────────
18
+
19
+ /** Does an action grant set hold `action`, literally or through a `<prefix>:*` wildcard? */
20
+ export function hasAction(actions: GrantSet, action: string): boolean {
21
+ if (actions === "all") return true;
22
+ if (actions.has(action)) return true;
23
+ for (let i = action.lastIndexOf(":"); i > 0; i = action.lastIndexOf(":", i - 1)) {
24
+ if (actions.has(`${action.slice(0, i)}:*`)) return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ /** Does a literal id set (channels, repos) hold `id`? */
30
+ export function holds(set: GrantSet, id: string): boolean {
31
+ return set === "all" || set.has(id);
32
+ }
33
+
34
+ function intersectSets(a: GrantSet, b: GrantSet, covers: (set: GrantSet, member: string) => boolean): GrantSet {
35
+ if (a === "all") return b;
36
+ if (b === "all") return a;
37
+ const out = new Set<string>();
38
+ for (const member of a) if (covers(b, member)) out.add(member);
39
+ for (const member of b) if (covers(a, member)) out.add(member);
40
+ return out;
41
+ }
42
+
43
+ /** `a ∩ b`, never a superset of either side. Wildcards intersect by coverage:
44
+ * `{agent:run:*} ∩ {agent:run:coding}` is `{agent:run:coding}`. */
45
+ export function intersectGrants(a: Grants, b: Grants): Grants {
46
+ return {
47
+ actions: intersectSets(a.actions, b.actions, hasAction),
48
+ channels: intersectSets(a.channels, b.channels, holds),
49
+ repos: intersectSets(a.repos, b.repos, holds),
50
+ };
51
+ }
52
+
53
+ /** The grants a decision is made against: an actor acting on behalf of a
54
+ * principal gets the intersection with that principal's effective grants. */
55
+ export function effectiveGrants(actor: Actor): Grants {
56
+ return actor.onBehalfOf ? intersectGrants(actor.grants, effectiveGrants(actor.onBehalfOf)) : actor.grants;
57
+ }
58
+
59
+ /** The identity `is-self` compares against: the root principal of an on-behalf-of chain. */
60
+ export function principalOf(actor: Actor): Actor {
61
+ let current = actor;
62
+ while (current.onBehalfOf) current = current.onBehalfOf;
63
+ return current;
64
+ }
65
+
66
+ // ── decision ─────────────────────────────────────────────────────────────────
67
+
68
+ const ALLOW: Decision = Object.freeze({ allow: true });
69
+
70
+ /** Every deny reason `authorize` can return. */
71
+ export type DenyReason =
72
+ | "unknown-actor-kind" // actor.kind is outside the vocabulary
73
+ | "no-rule" // nothing in the table names this action on this resource
74
+ | "actor-kind" // rows exist, none admits this actor kind
75
+ | "origin-visibility" // rows exist, none admits the resource's origin visibility
76
+ | "missing-grant"
77
+ | "not-member"
78
+ | "not-self"
79
+ | "not-owner"
80
+ | "not-all-channels";
81
+
82
+ const FAILURE_REASON: Readonly<Record<Condition["kind"], DenyReason>> = {
83
+ "has-grant": "missing-grant",
84
+ "member-of": "not-member",
85
+ "is-self": "not-self",
86
+ "owner-of": "not-owner",
87
+ "all-channels": "not-all-channels",
88
+ };
89
+
90
+ function deny(reason: DenyReason): Decision {
91
+ return { allow: false, reason };
92
+ }
93
+
94
+ export function isKnownActorKind(kind: string): boolean {
95
+ return (ACTOR_KINDS as readonly string[]).includes(kind);
96
+ }
97
+
98
+ export function evaluateCondition(
99
+ condition: Condition,
100
+ grants: Grants,
101
+ selfId: string,
102
+ attributes: ResourceAttributes,
103
+ ): boolean {
104
+ switch (condition.kind) {
105
+ case "has-grant": {
106
+ const grant = resolveGrant(condition.grant, attributes);
107
+ return grant !== undefined && hasAction(grants.actions, grant);
108
+ }
109
+ case "member-of":
110
+ // Granted the channel, or the channel is public (a run's stamped
111
+ // visibility). `unknown` — no stamp, a directory failure — is never
112
+ // public: fail-closed.
113
+ return (
114
+ (attributes.channelId !== undefined && holds(grants.channels, attributes.channelId)) ||
115
+ attributes.channelVisibility === "public"
116
+ );
117
+ case "is-self":
118
+ return attributes.userId !== undefined && attributes.userId === selfId;
119
+ case "owner-of":
120
+ return attributes.repo !== undefined && holds(grants.repos, attributes.repo);
121
+ case "all-channels":
122
+ return grants.channels === "all";
123
+ }
124
+ return false;
125
+ }
126
+
127
+ /** Does ONE row allow this actor on this resource? Selectors (`actorKinds`,
128
+ * `originVisibility`) are checked as well as the conditions, so a row is
129
+ * evaluated exactly as `authorize` would. Exported for the per-row table tests. */
130
+ export function evaluateRule(rule: Rule, actor: Actor, resource: Resource): boolean {
131
+ if (!isKnownActorKind(actor.kind)) return false;
132
+ if (ruleTarget(rule) !== targetOfResource(resource)) return false;
133
+ if (rule.actorKinds && !rule.actorKinds.includes(actor.kind)) return false;
134
+ const attributes = attributesOf(resource);
135
+ if (rule.originVisibility && !rule.originVisibility.includes(attributes.visibility)) return false;
136
+ const grants = effectiveGrants(actor);
137
+ const selfId = principalOf(actor).id;
138
+ return rule.when.every((condition) => evaluateCondition(condition, grants, selfId, attributes));
139
+ }
140
+
141
+ /** `authorize` over an explicit (validated) table. Tests use it to drive
142
+ * alternative tables; production code calls `authorize`. */
143
+ export function authorizeWith(rules: readonly Rule[], actor: Actor, action: Action, resource: Resource): Decision {
144
+ if (!isKnownActorKind(actor.kind)) return deny("unknown-actor-kind");
145
+ const target = targetOfResource(resource);
146
+ const named = rules.filter((rule) => rule.action === action && ruleTarget(rule) === target);
147
+ if (named.length === 0) return deny("no-rule");
148
+ const forKind = named.filter((rule) => !rule.actorKinds || rule.actorKinds.includes(actor.kind));
149
+ if (forKind.length === 0) return deny("actor-kind");
150
+ const attributes = attributesOf(resource);
151
+ const forOrigin = forKind.filter(
152
+ (rule) => !rule.originVisibility || rule.originVisibility.includes(attributes.visibility),
153
+ );
154
+ if (forOrigin.length === 0) return deny("origin-visibility");
155
+ const grants = effectiveGrants(actor);
156
+ const selfId = principalOf(actor).id;
157
+ let reason: DenyReason | undefined;
158
+ for (const rule of forOrigin) {
159
+ const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfId, attributes));
160
+ if (!failed) return ALLOW;
161
+ reason ??= FAILURE_REASON[failed.kind];
162
+ }
163
+ // Unreachable: a row with no failing condition returned above, and every row here has conditions.
164
+ return deny(reason ?? "no-rule");
165
+ }
166
+
167
+ export function authorize(actor: Actor, action: Action, resource: Resource): Decision {
168
+ return authorizeWith(POLICY, actor, action, resource);
169
+ }