@event4u/agent-config 1.37.0 → 1.39.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/.agent-src/commands/onboard.md +131 -50
- package/.agent-src/templates/agents/agent-project-settings.example.yml +9 -2
- package/.agent-src/templates/scripts/work_engine/_lib/__init__.py +7 -0
- package/.agent-src/templates/scripts/work_engine/_lib/agent_settings.py +168 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +18 -19
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +64 -0
- package/README.md +36 -1
- package/docs/contracts/mcp-cloud-scope.md +182 -0
- package/docs/contracts/mcp-phase-1-scope.md +8 -3
- package/docs/customization.md +45 -0
- package/docs/guidelines/agent-infra/layered-settings.md +54 -17
- package/docs/guidelines/agent-infra/mcp-request-signing.md +4 -0
- package/docs/mcp-server.md +11 -3
- package/docs/setup/mcp-client-config.md +152 -0
- package/docs/setup/mcp-cloud-endpoints.md +109 -0
- package/docs/setup/mcp-cloud-registry-listing.md +99 -0
- package/docs/setup/mcp-cloud-setup.md +152 -0
- package/docs/setup/mcp-r2-bootstrap.md +82 -0
- package/package.json +1 -1
- package/scripts/_lib/agent_settings.py +168 -0
- package/scripts/mcp_parity_smoke.py +146 -0
- package/scripts/pack_mcp_content.py +274 -0
- package/scripts/readme_linter.py +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# MCP Cloud Endpoints — URL shapes & DNS
|
|
2
|
+
|
|
3
|
+
Public URL shapes for the hosted `agent-config-mcp` Worker. Governed
|
|
4
|
+
by `docs/contracts/mcp-cloud-scope.md` (A0-cloud) and Phase 5.2 of
|
|
5
|
+
`agents/roadmaps/road-to-cloudflare-mcp-hosting.md`.
|
|
6
|
+
|
|
7
|
+
## Stability
|
|
8
|
+
|
|
9
|
+
**Experimental.** Inherits the label from `mcp-phase-1-scope.md`. URL
|
|
10
|
+
shapes below are pinned for the lifetime of the *experimental* window;
|
|
11
|
+
breaking changes require a stability-label bump.
|
|
12
|
+
|
|
13
|
+
## Scope — Lite surface
|
|
14
|
+
|
|
15
|
+
The hosted endpoint exposes the **read-only governance surface**
|
|
16
|
+
(skills, commands, rules, guidelines, contexts) as MCP prompts +
|
|
17
|
+
resources. `tools/list` returns two **deprecated stubs**
|
|
18
|
+
(`lint_skills`, `chat_history_append`) that point at their local-stdio
|
|
19
|
+
successors; `tools/call` against either returns `isError=true`. No
|
|
20
|
+
script execution, no FS access, no shell.
|
|
21
|
+
|
|
22
|
+
Full power — the ~112 Python scripts (linters, audits, `task ci`,
|
|
23
|
+
work-engine hooks) — requires the local install. See
|
|
24
|
+
[`../contracts/mcp-cloud-scope.md`](../contracts/mcp-cloud-scope.md)
|
|
25
|
+
for the execution-safety boundary and the Phase-7-DEFERRED gate that
|
|
26
|
+
governs any future tool restoration.
|
|
27
|
+
|
|
28
|
+
## URL shapes (pinned)
|
|
29
|
+
|
|
30
|
+
Two surfaces, both serve identical wire contracts (JSON-RPC over POST,
|
|
31
|
+
SSE on GET — A0-cloud invariant 1):
|
|
32
|
+
|
|
33
|
+
| Shape | Resolves to | Use case |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `https://mcp.<domain>/latest/sse` | the release currently pointed at by `releases/latest.txt` in R2 | client wants the rolling cutting-edge build |
|
|
36
|
+
| `https://mcp.<domain>/v<X.Y.Z>/sse` | the immutable release `<X.Y.Z>` from the R2 archive | client wants a pinned, reproducible build |
|
|
37
|
+
|
|
38
|
+
The `<domain>` placeholder is operator-configured; the package itself
|
|
39
|
+
does not own DNS. Pin the chosen domain in your fork's
|
|
40
|
+
`mcp-cloud-scope.md` § Bucket / DNS.
|
|
41
|
+
|
|
42
|
+
For JSON-RPC, drop the `/sse` suffix:
|
|
43
|
+
|
|
44
|
+
| JSON-RPC | SSE |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `POST https://mcp.<domain>/latest` | `GET https://mcp.<domain>/latest/sse` |
|
|
47
|
+
| `POST https://mcp.<domain>/v1.37.0` | `GET https://mcp.<domain>/v1.37.0/sse` |
|
|
48
|
+
|
|
49
|
+
The Worker reads its bundled blob at module init (per A0-cloud
|
|
50
|
+
invariant 2); the path prefix in MVP-1 is a routing artefact, not a
|
|
51
|
+
content selector. Multi-version routing lands in MVP-2.
|
|
52
|
+
|
|
53
|
+
## DNS setup (operator-side)
|
|
54
|
+
|
|
55
|
+
One-time, requires Cloudflare account + zone access:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# 1. Add the Worker custom domain in Cloudflare dashboard:
|
|
59
|
+
# Workers & Pages → agent-config-mcp → Settings → Domains & Routes
|
|
60
|
+
# → Add Custom Domain → "mcp.<your-domain>"
|
|
61
|
+
#
|
|
62
|
+
# 2. Cloudflare creates the AAAA + A records automatically. No manual
|
|
63
|
+
# CNAME — Custom Domains is the supported path (not "Routes").
|
|
64
|
+
#
|
|
65
|
+
# 3. Verify:
|
|
66
|
+
curl -s -X POST https://mcp.<your-domain>/ \
|
|
67
|
+
-H "content-type: application/json" \
|
|
68
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"ping","params":{}}'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
After DNS is live, uncomment the `routes` block in
|
|
72
|
+
`workers/mcp/wrangler.toml` and redeploy via `wrangler deploy` (or let
|
|
73
|
+
the GitHub Action pick it up on the next release).
|
|
74
|
+
|
|
75
|
+
The fallback `*.workers.dev` URL stays live for free; the custom
|
|
76
|
+
domain is only the public stability promise.
|
|
77
|
+
|
|
78
|
+
## Health probe
|
|
79
|
+
|
|
80
|
+
Every URL accepts `GET /` with no body and returns release identity:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"ok": true,
|
|
85
|
+
"name": "agent-config-mcp",
|
|
86
|
+
"release_key": "v1.37.0-2fc5084",
|
|
87
|
+
"package_version": "1.37.0",
|
|
88
|
+
"signature": "35bc3c5e8b83",
|
|
89
|
+
"schema_version": 1
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `signature` field is the wire-surface `skillSetSignature` — same
|
|
94
|
+
hash that MCP clients see under `_meta.skillSetSignature` on the
|
|
95
|
+
identity surface.
|
|
96
|
+
|
|
97
|
+
## Parity smoke
|
|
98
|
+
|
|
99
|
+
Post-deploy CI runs `scripts/mcp_parity_smoke.py` against the new
|
|
100
|
+
deployment with `--target https://mcp.<domain>`. A non-zero exit
|
|
101
|
+
aborts the `latest.txt` repoint, so the previous release keeps
|
|
102
|
+
serving on `/latest/`.
|
|
103
|
+
|
|
104
|
+
## See also
|
|
105
|
+
|
|
106
|
+
- Per-client config snippets: [`mcp-client-config.md`](mcp-client-config.md)
|
|
107
|
+
- A0-cloud contract: `docs/contracts/mcp-cloud-scope.md`
|
|
108
|
+
- R2 bootstrap: `docs/setup/mcp-r2-bootstrap.md`
|
|
109
|
+
- Local stdio fallback: `scripts/mcp_server/` (unchanged)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# MCP Registry Listing — submission package
|
|
2
|
+
|
|
3
|
+
Single source of truth for every MCP-registry submission of the hosted
|
|
4
|
+
`agent-config-mcp` Worker. Copy-paste sections from this file into the
|
|
5
|
+
target registry's template; do not maintain per-registry forks.
|
|
6
|
+
|
|
7
|
+
Phase 6.1 of `agents/roadmaps/road-to-cloudflare-mcp-hosting.md`.
|
|
8
|
+
|
|
9
|
+
## One-liner
|
|
10
|
+
|
|
11
|
+
> Read-only governance surface for AI coding agents — 174 skills, 104
|
|
12
|
+
> commands, 60 rules, 100 guidelines + contexts. Hosted MCP bridge
|
|
13
|
+
> over the `event4u/agent-config` package.
|
|
14
|
+
|
|
15
|
+
## Endpoints
|
|
16
|
+
|
|
17
|
+
| Shape | URL | Use |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Rolling latest | `https://mcp.<operator-domain>/latest/sse` | clients tracking the live build |
|
|
20
|
+
| Pinned release | `https://mcp.<operator-domain>/v<X.Y.Z>/sse` | clients pinning a reproducible version |
|
|
21
|
+
| Liveness | `GET https://mcp.<operator-domain>/` | release identity + signature |
|
|
22
|
+
|
|
23
|
+
The `<operator-domain>` placeholder reflects the package's design —
|
|
24
|
+
every operator hosts their own Worker. The package upstream does not
|
|
25
|
+
run a public reference deployment; consumers point their Worker at
|
|
26
|
+
their own R2 bucket per `docs/setup/mcp-r2-bootstrap.md`.
|
|
27
|
+
|
|
28
|
+
## Wire surface (MVP-1)
|
|
29
|
+
|
|
30
|
+
| Method | Status |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `initialize` | implemented |
|
|
33
|
+
| `ping` | implemented |
|
|
34
|
+
| `prompts/list`, `prompts/get` | implemented |
|
|
35
|
+
| `resources/list`, `resources/read` | implemented |
|
|
36
|
+
| `tools/list` | implemented (returns deprecated stubs only) |
|
|
37
|
+
| `tools/call` | **not implemented** — returns `-32601 Method not found` |
|
|
38
|
+
| `notifications/*` | not implemented |
|
|
39
|
+
|
|
40
|
+
No mutation, no auth, no subrequests at runtime. Full contract:
|
|
41
|
+
`docs/contracts/mcp-cloud-scope.md` § A0-cloud.
|
|
42
|
+
|
|
43
|
+
## Stability
|
|
44
|
+
|
|
45
|
+
**Experimental.** Wire surface, URL shapes, and the `_meta.signature`
|
|
46
|
+
field are pinned for the lifetime of the *experimental* window.
|
|
47
|
+
Breaking changes require a stability-label bump (see
|
|
48
|
+
`docs/contracts/mcp-phase-1-scope.md`).
|
|
49
|
+
|
|
50
|
+
## Identity & reproducibility
|
|
51
|
+
|
|
52
|
+
Every response carries `_meta.skillSetSignature` — a 12-char SHA-256
|
|
53
|
+
prefix over the sorted `(uri, body)` pairs of the bundled content.
|
|
54
|
+
Identical content → identical signature, across machines and builds.
|
|
55
|
+
R2 archives every release indefinitely under
|
|
56
|
+
`releases/v<X.Y.Z>-<sha>/`.
|
|
57
|
+
|
|
58
|
+
## Categories (for registry tagging)
|
|
59
|
+
|
|
60
|
+
- `governance`
|
|
61
|
+
- `meta-prompting`
|
|
62
|
+
- `skills`
|
|
63
|
+
- `agent-infrastructure`
|
|
64
|
+
- `code-review`
|
|
65
|
+
- `experimental`
|
|
66
|
+
|
|
67
|
+
## License & contact
|
|
68
|
+
|
|
69
|
+
| Field | Value |
|
|
70
|
+
|---|---|
|
|
71
|
+
| License | MIT |
|
|
72
|
+
| Source | `https://github.com/event4u-app/agent-config` |
|
|
73
|
+
| Maintainer | event4u-app (org) |
|
|
74
|
+
| Contact | GitHub issues |
|
|
75
|
+
|
|
76
|
+
## Links to upstream contracts
|
|
77
|
+
|
|
78
|
+
- A0-cloud safety contract: `docs/contracts/mcp-cloud-scope.md`
|
|
79
|
+
- Phase-1 scope (inherited): `docs/contracts/mcp-phase-1-scope.md`
|
|
80
|
+
- URL shapes & DNS: `docs/setup/mcp-cloud-endpoints.md`
|
|
81
|
+
- Local stdio kernel (predecessor): `scripts/mcp_server/`
|
|
82
|
+
|
|
83
|
+
## Submission targets
|
|
84
|
+
|
|
85
|
+
| Target | Status | Notes |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| [`awesome-mcp-servers`](https://github.com/punkpeye/awesome-mcp-servers) | ready for PR | low-friction listing, accepts experimental |
|
|
88
|
+
| [`modelcontextprotocol.io` catalog](https://modelcontextprotocol.io/servers) | ready for PR after `awesome-mcp-servers` merges | needs evidence of community uptake |
|
|
89
|
+
|
|
90
|
+
Both submissions reuse the **One-liner**, **Endpoints**, **Wire
|
|
91
|
+
surface**, **Stability**, and **License & contact** sections verbatim
|
|
92
|
+
from this file.
|
|
93
|
+
|
|
94
|
+
## Out of scope for this roadmap
|
|
95
|
+
|
|
96
|
+
npm-launcher listing (`npx @event4u/agent-config-mcp`) targets the
|
|
97
|
+
**local stdio** server, not the hosted Worker. Different transport,
|
|
98
|
+
different installation pattern, different audience — captured in
|
|
99
|
+
`agents/roadmaps/road-to-mcp-server.md` if revived.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Cloudflare MCP — Operator Setup
|
|
2
|
+
|
|
3
|
+
One-stop landing for onboarding a Cloudflare account to host the
|
|
4
|
+
`agent-config-mcp` Worker. Combines bucket bootstrap, DNS, GitHub
|
|
5
|
+
secrets, and troubleshooting in one place.
|
|
6
|
+
|
|
7
|
+
Governed by [`docs/contracts/mcp-cloud-scope.md`](../contracts/mcp-cloud-scope.md)
|
|
8
|
+
(A0-cloud). Deploys run **only** in CI
|
|
9
|
+
([`.github/workflows/deploy-mcp-worker.yml`](../../.github/workflows/deploy-mcp-worker.yml))
|
|
10
|
+
— per A0-cloud invariant 7, never `wrangler deploy` from a developer
|
|
11
|
+
machine.
|
|
12
|
+
|
|
13
|
+
## TL;DR — the happy path
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
task mcp:cloud:login # one-time, opens browser
|
|
17
|
+
task mcp:cloud:setup # check → r2-create → r2-verify → whoami
|
|
18
|
+
# Copy account id from output → GitHub Secrets (see § GitHub Secrets)
|
|
19
|
+
# Create scoped API token (see § API Token) → GitHub Secrets
|
|
20
|
+
# Done. First `release: published` triggers the deploy.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
| Tool | Version | Install |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Node.js | ≥ 20 | <https://nodejs.org/> or `nvm install 20` |
|
|
28
|
+
| `npx`/`wrangler` | wrangler ≥ 4.0 (auto-fetched on first run) | bundled with Node |
|
|
29
|
+
| `gh` CLI | latest | <https://cli.github.com/> (only for manual `deploy-dispatch`) |
|
|
30
|
+
| Cloudflare account | any plan | <https://dash.cloudflare.com/sign-up> |
|
|
31
|
+
|
|
32
|
+
Run `task mcp:cloud:check` to verify all four in one shot.
|
|
33
|
+
|
|
34
|
+
## Step 1 — Cloudflare login
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
task mcp:cloud:login
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Opens a browser to authorize wrangler against your Cloudflare account.
|
|
41
|
+
One-time per developer machine; the token is stored in
|
|
42
|
+
`~/Library/Preferences/.wrangler/` (macOS) or `~/.wrangler/` (Linux).
|
|
43
|
+
|
|
44
|
+
## Step 2 — Enable R2
|
|
45
|
+
|
|
46
|
+
Cloudflare requires a one-time plan activation before R2 buckets can
|
|
47
|
+
be created. **You will hit error `code: 10042` on the first attempt
|
|
48
|
+
if you skip this.**
|
|
49
|
+
|
|
50
|
+
1. <https://dash.cloudflare.com/?to=/:account/r2/overview>
|
|
51
|
+
2. Click **Purchase R2 Plan**
|
|
52
|
+
3. Select **Free Tier** — 10 GB storage / 1 M Class-A / 10 M Class-B
|
|
53
|
+
ops per month at **$0** (credit card required, $0 within quota,
|
|
54
|
+
$0 egress)
|
|
55
|
+
4. Confirm; wait ~30 s for activation
|
|
56
|
+
|
|
57
|
+
## Step 3 — Create the R2 bucket
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
task mcp:cloud:r2-create # idempotent — safe to re-run
|
|
61
|
+
task mcp:cloud:r2-verify # ✅ if present
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The bucket is named `agent-config-mcp` and configured per
|
|
65
|
+
[`docs/setup/mcp-r2-bootstrap.md`](mcp-r2-bootstrap.md) (private,
|
|
66
|
+
indefinite retention, Worker reads via binding).
|
|
67
|
+
|
|
68
|
+
`task mcp:cloud:setup` chains steps 1, 3, and the account-id readout
|
|
69
|
+
in one shot.
|
|
70
|
+
|
|
71
|
+
## Step 4 — API Token
|
|
72
|
+
|
|
73
|
+
The CI deploy pipeline needs a **scoped** token — never reuse the
|
|
74
|
+
Global API Key or a production-tenant token.
|
|
75
|
+
|
|
76
|
+
Dashboard → **My Profile → API Tokens → Create Token → Custom token**:
|
|
77
|
+
|
|
78
|
+
| Permission | Resource | Access |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| Account · Workers Scripts | your account | Edit |
|
|
81
|
+
| Account · Workers R2 Storage | your account | Edit |
|
|
82
|
+
| User · User Details | — | Read |
|
|
83
|
+
|
|
84
|
+
If you uncomment the `routes` block in `workers/mcp/wrangler.toml`
|
|
85
|
+
(custom domain cutover, Phase 5.2), add **Zone · DNS · Edit** on the
|
|
86
|
+
relevant zone.
|
|
87
|
+
|
|
88
|
+
Copy the generated token immediately — Cloudflare shows it once.
|
|
89
|
+
|
|
90
|
+
## Step 5 — GitHub Secrets
|
|
91
|
+
|
|
92
|
+
Repository → **Settings → Secrets and variables → Actions → New
|
|
93
|
+
repository secret**:
|
|
94
|
+
|
|
95
|
+
| Secret | Value | Source |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `CLOUDFLARE_API_TOKEN` | scoped token from Step 4 | dashboard |
|
|
98
|
+
| `CLOUDFLARE_ACCOUNT_ID` | account id | `task mcp:cloud:whoami` |
|
|
99
|
+
|
|
100
|
+
Set them in **Actions** secrets, not **Codespaces** or **Dependabot**
|
|
101
|
+
scopes.
|
|
102
|
+
|
|
103
|
+
## Step 6 — Validate
|
|
104
|
+
|
|
105
|
+
Trigger the deploy workflow manually against an existing tag — no
|
|
106
|
+
release event needed:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
task mcp:cloud:deploy-dispatch TAG=v1.37.0
|
|
110
|
+
gh run watch
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Green smoke step → `latest.txt` repoints → release is live on
|
|
114
|
+
`*.workers.dev`. A red smoke step leaves `latest.txt` on the previous
|
|
115
|
+
release.
|
|
116
|
+
|
|
117
|
+
## Step 7 — DNS (optional, Phase 5.2)
|
|
118
|
+
|
|
119
|
+
Custom domain `mcp.<your-domain>` setup lives in
|
|
120
|
+
[`docs/setup/mcp-cloud-endpoints.md`](mcp-cloud-endpoints.md) § DNS
|
|
121
|
+
setup. Until cutover, the Worker serves on the free
|
|
122
|
+
`agent-config-mcp.<account>.workers.dev` URL.
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
| Symptom | Cause | Fix |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `code: 10042` on bucket create | R2 not enabled on account | Step 2 — Enable R2 |
|
|
129
|
+
| `wrangler whoami` shows no Account ID | not logged in | `task mcp:cloud:login` |
|
|
130
|
+
| Workflow fails on `wrangler deploy` with auth error | secret missing/wrong scope | re-check Step 4 token permissions |
|
|
131
|
+
| Smoke step red after deploy | bundle vs. SDK mismatch | check `compatibility_date` in `wrangler.toml` |
|
|
132
|
+
| Bucket create returns `already exists` | bucket present | not an error — `task mcp:cloud:r2-verify` to confirm |
|
|
133
|
+
|
|
134
|
+
## Available tasks
|
|
135
|
+
|
|
136
|
+
| Task | Purpose |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `task mcp:cloud:check` | Preflight — tools + login status |
|
|
139
|
+
| `task mcp:cloud:login` | Interactive wrangler login |
|
|
140
|
+
| `task mcp:cloud:whoami` | Print account id for GitHub Secret |
|
|
141
|
+
| `task mcp:cloud:r2-create` | Create R2 bucket (idempotent) |
|
|
142
|
+
| `task mcp:cloud:r2-verify` | Verify R2 bucket exists |
|
|
143
|
+
| `task mcp:cloud:setup` | Full chain — check → r2 → whoami |
|
|
144
|
+
| `task mcp:cloud:dev` | Local `wrangler dev` on :8787 |
|
|
145
|
+
| `task mcp:cloud:deploy-dispatch TAG=v…` | Manual workflow trigger |
|
|
146
|
+
|
|
147
|
+
## See also
|
|
148
|
+
|
|
149
|
+
- [`docs/contracts/mcp-cloud-scope.md`](../contracts/mcp-cloud-scope.md) — A0-cloud contract
|
|
150
|
+
- [`docs/setup/mcp-r2-bootstrap.md`](mcp-r2-bootstrap.md) — R2 layout & break-glass
|
|
151
|
+
- [`docs/setup/mcp-cloud-endpoints.md`](mcp-cloud-endpoints.md) — URL shapes & DNS
|
|
152
|
+
- [`workers/mcp/README.md`](../../workers/mcp/README.md) — Worker source overview
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# R2 Bootstrap — agent-config MCP
|
|
2
|
+
|
|
3
|
+
One-time setup for the R2 bucket that holds the Worker's release archive.
|
|
4
|
+
Owned by `mcp-cloud-scope.md` §3.3 and consumed by the deploy pipeline
|
|
5
|
+
(`.github/workflows/deploy-mcp-worker.yml`).
|
|
6
|
+
|
|
7
|
+
## Bucket
|
|
8
|
+
|
|
9
|
+
| Field | Value |
|
|
10
|
+
|---|---|
|
|
11
|
+
| Bucket name | `agent-config-mcp` |
|
|
12
|
+
| Location hint | `auto` (Cloudflare-chosen) |
|
|
13
|
+
| Public access | **off** — Worker reads via binding, never via signed URL |
|
|
14
|
+
| Object retention | indefinite — every release stays for forensics |
|
|
15
|
+
|
|
16
|
+
## Layout
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
agent-config-mcp/
|
|
20
|
+
├─ releases/
|
|
21
|
+
│ ├─ v<X.Y.Z>-<sha>/
|
|
22
|
+
│ │ ├─ content.json.gz # gzipped content blob (R2 archival copy)
|
|
23
|
+
│ │ └─ manifest.json # uncompressed manifest sidecar
|
|
24
|
+
│ └─ latest.txt # one-line: v<X.Y.Z>-<sha>
|
|
25
|
+
└─ (no other prefixes)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`latest.txt` is the only mutable object. Every `releases/v*/` directory
|
|
29
|
+
is **immutable** once written; the pipeline refuses to overwrite an
|
|
30
|
+
existing release prefix.
|
|
31
|
+
|
|
32
|
+
## Bootstrap — one-time
|
|
33
|
+
|
|
34
|
+
Requires `wrangler` ≥ 4.0 and a Cloudflare API token with R2 admin scope.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
# 1. Authenticate (interactive, opens browser).
|
|
38
|
+
npx wrangler login
|
|
39
|
+
|
|
40
|
+
# 2. Create the bucket.
|
|
41
|
+
npx wrangler r2 bucket create agent-config-mcp
|
|
42
|
+
|
|
43
|
+
# 3. Verify.
|
|
44
|
+
npx wrangler r2 bucket list | grep agent-config-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The Worker binding is declared in `workers/mcp/wrangler.toml` under
|
|
48
|
+
`[[r2_buckets]]`. The pipeline reads/writes via the wrangler CLI in CI,
|
|
49
|
+
not via the Worker — A0-cloud invariant 2 forbids the Worker from
|
|
50
|
+
issuing R2 writes.
|
|
51
|
+
|
|
52
|
+
## Secrets in CI
|
|
53
|
+
|
|
54
|
+
The deploy pipeline needs two GitHub secrets:
|
|
55
|
+
|
|
56
|
+
| Secret | Scope |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `CLOUDFLARE_API_TOKEN` | `Workers Scripts:Edit` + `Workers R2 Storage:Edit` |
|
|
59
|
+
| `CLOUDFLARE_ACCOUNT_ID` | account id (not a token) |
|
|
60
|
+
|
|
61
|
+
The token must be scoped to **this account only**; do not reuse a
|
|
62
|
+
production-tenant token.
|
|
63
|
+
|
|
64
|
+
## Manual fixes — break-glass
|
|
65
|
+
|
|
66
|
+
If `latest.txt` ends up pointing to a broken release, the recovery is to
|
|
67
|
+
repoint it manually:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
# Repoint latest to a known-good release.
|
|
71
|
+
echo -n "v1.36.0-abc1234" | \
|
|
72
|
+
npx wrangler r2 object put agent-config-mcp/releases/latest.txt --pipe
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Past `releases/v*/` directories are not deleted in recovery — leave the
|
|
76
|
+
forensics intact.
|
|
77
|
+
|
|
78
|
+
## Terraform note
|
|
79
|
+
|
|
80
|
+
The repo does not yet manage CF resources via Terraform. When TF lands,
|
|
81
|
+
this bucket should move into a `cloudflare_r2_bucket` resource and this
|
|
82
|
+
doc shrinks to a pointer.
|
package/package.json
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Centralized loader for ``.agent-settings.yml`` with user-global fallback.
|
|
2
|
+
|
|
3
|
+
Phase 1 of road-to-portable-dev-preferences. Single source of truth for
|
|
4
|
+
how scripts read agent settings — replaces ~15 ad-hoc loaders in P3.
|
|
5
|
+
|
|
6
|
+
Resolution order (project wins, user-global fills gaps for whitelisted
|
|
7
|
+
keys only):
|
|
8
|
+
|
|
9
|
+
1. Project ``./.agent-settings.yml`` (full file, all keys)
|
|
10
|
+
2. ``~/.config/agent-config/agent-settings.yml`` (whitelist only)
|
|
11
|
+
3. Built-in defaults (currently empty)
|
|
12
|
+
|
|
13
|
+
Whitelisted keys (``MERGEABLE_KEYS``) are exact dotted paths. A
|
|
14
|
+
non-whitelisted key in the user-global file is silently ignored — the
|
|
15
|
+
``verbose=True`` flag surfaces ignored paths via ``logging.info`` for
|
|
16
|
+
debugging.
|
|
17
|
+
|
|
18
|
+
Contract — pure, read-only, tolerant:
|
|
19
|
+
|
|
20
|
+
* Lazy PyYAML import; no yaml installed → defaults returned.
|
|
21
|
+
* Missing project file → user-global + defaults.
|
|
22
|
+
* Missing user-global file → project + defaults.
|
|
23
|
+
* Both missing → defaults.
|
|
24
|
+
* Malformed YAML / unreadable file → defaults, logged at WARNING.
|
|
25
|
+
* No file is ever created or written by this module.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
DEFAULT_PROJECT_FILE = ".agent-settings.yml"
|
|
36
|
+
DEFAULT_USER_GLOBAL_FILE = (
|
|
37
|
+
Path.home() / ".config" / "agent-config" / "agent-settings.yml"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
#: Exact dotted paths allowed to cascade from user-global into the merged
|
|
41
|
+
#: settings. Anything not listed here is silently ignored when present in
|
|
42
|
+
#: the user-global file. Adding a key requires an ADR — see
|
|
43
|
+
#: ``agents/roadmaps/road-to-portable-dev-preferences.md``.
|
|
44
|
+
MERGEABLE_KEYS: tuple[str, ...] = (
|
|
45
|
+
"name",
|
|
46
|
+
"ide",
|
|
47
|
+
"cost_profile",
|
|
48
|
+
"personal.bot_icon",
|
|
49
|
+
"personal.autonomy",
|
|
50
|
+
"caveman.speak_scope",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_DEFAULTS: dict[str, Any] = {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_agent_settings(
|
|
57
|
+
project_path: Path | str | None = None,
|
|
58
|
+
user_global_path: Path | str | None = None,
|
|
59
|
+
verbose: bool = False,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Return the merged settings dict.
|
|
62
|
+
|
|
63
|
+
``project_path`` defaults to ``./.agent-settings.yml`` (CWD-relative).
|
|
64
|
+
``user_global_path`` defaults to
|
|
65
|
+
``~/.config/agent-config/agent-settings.yml``. Both arguments accept
|
|
66
|
+
``Path`` or ``str``. Pass ``verbose=True`` to log keys present in
|
|
67
|
+
user-global that are not on the whitelist.
|
|
68
|
+
"""
|
|
69
|
+
project = _read_yaml(
|
|
70
|
+
Path(project_path) if project_path else Path(DEFAULT_PROJECT_FILE),
|
|
71
|
+
) or {}
|
|
72
|
+
user_global_raw = _read_yaml(
|
|
73
|
+
Path(user_global_path) if user_global_path else DEFAULT_USER_GLOBAL_FILE,
|
|
74
|
+
) or {}
|
|
75
|
+
|
|
76
|
+
user_global_filtered, ignored = _filter_whitelist(
|
|
77
|
+
user_global_raw, MERGEABLE_KEYS,
|
|
78
|
+
)
|
|
79
|
+
if verbose and ignored:
|
|
80
|
+
logger.info(
|
|
81
|
+
"agent_settings: ignored non-whitelisted user-global keys: %s",
|
|
82
|
+
sorted(ignored),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
merged: dict[str, Any] = _deep_copy_defaults(_DEFAULTS)
|
|
86
|
+
_deep_merge(merged, user_global_filtered)
|
|
87
|
+
_deep_merge(merged, project)
|
|
88
|
+
return merged
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _read_yaml(path: Path) -> dict[str, Any] | None:
|
|
92
|
+
"""Best-effort YAML read; never raises. Returns ``None`` on any failure."""
|
|
93
|
+
if not path.is_file():
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
import yaml # type: ignore[import-untyped]
|
|
97
|
+
except ImportError:
|
|
98
|
+
return None
|
|
99
|
+
try:
|
|
100
|
+
with path.open(encoding="utf-8") as fh:
|
|
101
|
+
data = yaml.safe_load(fh) or {}
|
|
102
|
+
except (OSError, yaml.YAMLError):
|
|
103
|
+
logger.warning("agent_settings: unreadable or malformed YAML at %s", path)
|
|
104
|
+
return None
|
|
105
|
+
return data if isinstance(data, dict) else None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _filter_whitelist(
|
|
109
|
+
raw: dict[str, Any], allowed: tuple[str, ...],
|
|
110
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
111
|
+
"""Return ``(filtered_dict, ignored_paths)`` from a user-global blob."""
|
|
112
|
+
filtered: dict[str, Any] = {}
|
|
113
|
+
for dotted in allowed:
|
|
114
|
+
value = _get_dotted(raw, dotted)
|
|
115
|
+
if value is not None:
|
|
116
|
+
_set_dotted(filtered, dotted, value)
|
|
117
|
+
ignored = [p for p in _leaf_paths(raw) if p not in allowed]
|
|
118
|
+
return filtered, ignored
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_dotted(data: dict[str, Any], dotted: str) -> Any:
|
|
122
|
+
cursor: Any = data
|
|
123
|
+
for part in dotted.split("."):
|
|
124
|
+
if not isinstance(cursor, dict) or part not in cursor:
|
|
125
|
+
return None
|
|
126
|
+
cursor = cursor[part]
|
|
127
|
+
return cursor
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _set_dotted(target: dict[str, Any], dotted: str, value: Any) -> None:
|
|
131
|
+
parts = dotted.split(".")
|
|
132
|
+
cursor = target
|
|
133
|
+
for part in parts[:-1]:
|
|
134
|
+
nxt = cursor.setdefault(part, {})
|
|
135
|
+
if not isinstance(nxt, dict):
|
|
136
|
+
nxt = {}
|
|
137
|
+
cursor[part] = nxt
|
|
138
|
+
cursor = nxt
|
|
139
|
+
cursor[parts[-1]] = value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _leaf_paths(data: dict[str, Any], prefix: str = "") -> list[str]:
|
|
143
|
+
paths: list[str] = []
|
|
144
|
+
for key, value in data.items():
|
|
145
|
+
path = f"{prefix}.{key}" if prefix else key
|
|
146
|
+
if isinstance(value, dict) and value:
|
|
147
|
+
paths.extend(_leaf_paths(value, path))
|
|
148
|
+
else:
|
|
149
|
+
paths.append(path)
|
|
150
|
+
return paths
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _deep_merge(dst: dict[str, Any], src: dict[str, Any]) -> None:
|
|
154
|
+
"""Merge ``src`` into ``dst`` in-place; nested dicts are merged recursively."""
|
|
155
|
+
for key, value in src.items():
|
|
156
|
+
if (
|
|
157
|
+
isinstance(value, dict)
|
|
158
|
+
and isinstance(dst.get(key), dict)
|
|
159
|
+
):
|
|
160
|
+
_deep_merge(dst[key], value)
|
|
161
|
+
else:
|
|
162
|
+
dst[key] = value
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _deep_copy_defaults(src: dict[str, Any]) -> dict[str, Any]:
|
|
166
|
+
out: dict[str, Any] = {}
|
|
167
|
+
_deep_merge(out, src)
|
|
168
|
+
return out
|