mercury-agent 0.4.26 → 0.4.28
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/README.md +24 -1
- package/docs/ROADMAP.md +3 -2
- package/docs/backlog/applicative-profiles.md +135 -149
- package/docs/backlog/caller-bound-capability-token.md +180 -0
- package/docs/debug/major/2026-07-01-strict-yaml-schema-crash.md +85 -0
- package/docs/ideas/business-extensions.md +48 -0
- package/docs/runbooks/gws-oauth-setup.md +211 -0
- package/package.json +1 -1
- package/resources/templates/mercury.example.yaml +84 -84
- package/src/config-file.ts +118 -18
- package/src/config.ts +1 -1
- package/src/core/conversation.ts +2 -2
- package/src/core/handler.ts +1 -1
- package/src/core/routes/console.ts +32 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Mercury is a personal AI assistant that lives where you chat. It connects to Wha
|
|
|
19
19
|
|
|
20
20
|
- **[Node.js](https://nodejs.org/)** >= 18 — Required for `npm install -g mercury-agent`
|
|
21
21
|
- **[Bun](https://bun.sh)** >= 1.0 — JavaScript runtime used by Mercury
|
|
22
|
-
- **[Docker](https://docs.docker.com/get-docker/)** — Required for running agent containers
|
|
22
|
+
- **[Docker](https://docs.docker.com/get-docker/)** — Required for running agent containers. On Linux, also install `docker-buildx` (`apt-get install docker-buildx`) — Docker Desktop includes it, but Docker Engine on Linux does not.
|
|
23
23
|
- **Windows users:** Mercury runs best under [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install). Install WSL2 with `wsl --install`, then install Bun and Docker inside it.
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
@@ -131,6 +131,7 @@ Each space is a user-defined memory boundary with its own workspace and pi sessi
|
|
|
131
131
|
| **Media** | Images, documents, voice notes | [docs/media/overview.md](docs/media/overview.md) |
|
|
132
132
|
| **KB Distillation** | Extract lasting knowledge from chats | [docs/kb-distillation.md](docs/kb-distillation.md) |
|
|
133
133
|
| **Extensions** | TypeScript plugins for CLIs, skills, jobs, hooks | [docs/extensions.md](docs/extensions.md) |
|
|
134
|
+
| **DM Auto-Space** | Auto-create isolated spaces per customer DM | [docs/rate-limiting.md](docs/rate-limiting.md) |
|
|
134
135
|
|
|
135
136
|
---
|
|
136
137
|
|
|
@@ -377,6 +378,28 @@ Supported OAuth providers: Anthropic, GitHub Copilot, Google Gemini CLI, Antigra
|
|
|
377
378
|
| `MERCURY_TRIGGER_PATTERNS` | `@Mercury,Mercury` | Trigger patterns |
|
|
378
379
|
| `MERCURY_ADMINS` | — | Pre-seeded admin user IDs |
|
|
379
380
|
|
|
381
|
+
### DM Auto-Space
|
|
382
|
+
|
|
383
|
+
Auto-create isolated spaces for each customer who DMs your bot. Each customer gets their own space with persistent context, configurable permissions, and rate limits.
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
# mercury.yaml
|
|
387
|
+
dm_auto_space:
|
|
388
|
+
enabled: true
|
|
389
|
+
admin_ids: # platform IDs — auto-link to "main" space
|
|
390
|
+
- "24417056866472" # WhatsApp LID, Telegram user ID, etc.
|
|
391
|
+
default_system_prompt: "You are a helpful assistant."
|
|
392
|
+
default_member_permissions: "prompt,prefs.get" # restrict customers to chat only
|
|
393
|
+
|
|
394
|
+
runtime:
|
|
395
|
+
rate_limit_daily_member: 20 # global daily cap for members (0 = unlimited)
|
|
396
|
+
rate_limit_daily_admin: 0 # global daily cap for admins (0 = unlimited)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`admin_ids` values are platform-specific identifiers — check the dashboard Conversations page to see the format (e.g. WhatsApp LID digits, Telegram numeric user ID).
|
|
400
|
+
|
|
401
|
+
Auto-created spaces are seeded with `trigger.match=always`, `context.mode=context`, and the configured member permissions. Per-space rate limits are manageable from the dashboard.
|
|
402
|
+
|
|
380
403
|
### Per-space Config
|
|
381
404
|
|
|
382
405
|
Conversations are discovered from incoming traffic. Unlinked conversations stay idle until you attach them to a space via `mercury link <conversation-id> <space-id>` or the dashboard.
|
package/docs/ROADMAP.md
CHANGED
|
@@ -10,7 +10,7 @@ Single source of truth for priority. Rule: ≤15 min/week to maintain.
|
|
|
10
10
|
|
|
11
11
|
| Item | Doc | Notes |
|
|
12
12
|
|------|-----|-------|
|
|
13
|
-
|
|
|
13
|
+
| **business-extensions** | [doc](ideas/business-extensions.md) | Dedicated business extensions wrapping raw capabilities with deterministic logic |
|
|
14
14
|
|
|
15
15
|
## Now
|
|
16
16
|
|
|
@@ -26,7 +26,8 @@ Single source of truth for priority. Rule: ≤15 min/week to maintain.
|
|
|
26
26
|
|---|------|-----|-----------|---------|
|
|
27
27
|
| 1 | **autonomous-planning-tier** | [doc](backlog/autonomous-planning-tier.md) | — | Let agents decompose ideas, keep human gate |
|
|
28
28
|
| 2 | **dm-auto-space** | [doc](backlog/dm-auto-space.md) | — | Auto-create space per customer DM |
|
|
29
|
-
| 3 | **applicative-profiles** | [doc](backlog/applicative-profiles.md) |
|
|
29
|
+
| 3 | **applicative-profiles** | [doc](backlog/applicative-profiles.md) | caller-bound-capability-token | Generic profile layer; host-side broker keeps secrets off customer containers |
|
|
30
|
+
| 4 | **caller-bound-capability-token** | [doc](backlog/caller-bound-capability-token.md) | — | Per-turn token so brokers can trust callerId; also hardens TradeStation |
|
|
30
31
|
|
|
31
32
|
## Later
|
|
32
33
|
|
|
@@ -9,199 +9,184 @@
|
|
|
9
9
|
|
|
10
10
|
## Goal
|
|
11
11
|
|
|
12
|
-
Mercury's existing profile system (`src/core/profiles.ts`) is a static provisioning mechanism — it bundles extensions, AGENTS.md, env vars, and config defaults into a deployable template.
|
|
12
|
+
Mercury's existing profile system (`src/core/profiles.ts`) is a static provisioning mechanism — it bundles extensions, AGENTS.md, env vars, and config defaults into a deployable template. It has no runtime behavior: it can't scope which capabilities members access, keep secrets out of an untrusted member's container, or enforce per-user ownership. Business workflows (appointment booking, customer management) require deterministic logic that the LLM cannot be trusted to enforce.
|
|
13
13
|
|
|
14
|
-
Applicative Profiles extend the
|
|
14
|
+
Applicative Profiles extend the profile system with a **runtime contract**: a profile declares the capabilities it wraps, the commands it exposes to members, and its member permission set — and its privileged work runs **host-side**, behind an authenticated broker, so credentials never enter a customer-controlled container. Mercury (public repo) provides the schema, permission scoping, and broker mechanism; the actual business profiles live in a **separate private repo**. The barber-appointments profile is the first implementation and the design driver.
|
|
15
|
+
|
|
16
|
+
### Security framing (why the naive design fails)
|
|
17
|
+
|
|
18
|
+
Mercury's security model today **trusts whoever it hands a tool to** — appropriate because tool-callers have historically been admins. Two facts make a naive "wrap GWS in an extension" approach unsafe for customer-facing profiles:
|
|
19
|
+
|
|
20
|
+
1. **The permission guard is bypassable.** It's a regex denylist on CLI names (`src/extensions/permission-guard.ts`) that the project documents as defense-in-depth only (`docs/TODOS.md` TODO-2). It cannot stop `printenv`, `cat /proc/self/environ`, `curl`, `python -c`, etc.
|
|
21
|
+
2. **Member role does not remove bash.** Role only controls which CLIs are denied and which env vars are injected (`src/core/runtime.ts:1343-1376`); whether bash exists is decided by *model capability*, not role. So a member on a tool-capable model can read any env var in their container.
|
|
22
|
+
|
|
23
|
+
Therefore **any credential injected into a customer's container is readable by that customer.** The only safe design keeps the credential on the host — which Mercury already does for TradeStation (see Architecture).
|
|
15
24
|
|
|
16
25
|
## User Stories
|
|
17
26
|
|
|
18
|
-
- As a Mercury operator, I want to install a profile
|
|
19
|
-
- As a barber shop owner, I want
|
|
20
|
-
- As a barber shop owner, I want
|
|
21
|
-
- As a profile developer, I want a clear schema
|
|
22
|
-
- As a Mercury admin, I want raw capability extensions (GWS, web search) to
|
|
27
|
+
- As a Mercury operator, I want to install a profile from an external repo so my bot gains domain-specific business logic without modifying Mercury core.
|
|
28
|
+
- As a barber shop owner, I want customers (DM members) to book and cancel only their **own** appointments, never seeing others' schedules.
|
|
29
|
+
- As a barber shop owner, I want customers to have **zero** access to my Gmail/Google account, even though the profile uses my Google credentials for Calendar internally.
|
|
30
|
+
- As a profile developer, I want a clear schema + a host-side broker pattern so I can enforce ownership on the host where the real `callerId` is trustworthy.
|
|
31
|
+
- As a Mercury admin, I want raw capability extensions (GWS, web search) to stay admin-only while the profile's scoped commands are member-facing.
|
|
23
32
|
|
|
24
33
|
## MVP Scope
|
|
25
34
|
|
|
26
|
-
**In scope:**
|
|
27
|
-
- Extend `mercury-profile.yaml` schema with runtime fields: `capabilities` (required extensions), `member_permissions` (exhaustive permission
|
|
35
|
+
**In scope (all in `mercury-public`):**
|
|
36
|
+
- Extend `mercury-profile.yaml` schema with runtime fields: `capabilities` (required host-side extensions), `member_permissions` (exhaustive member permission set), `system_prompt` (agent persona)
|
|
28
37
|
- Profile-aware permission resolution — when a profile is active, member permissions come from the profile, not from extension defaults
|
|
29
|
-
- Capability
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- Barber-appointments profile (separate repo) as the first implementation to validate the design
|
|
38
|
+
- Capability-dependency validation — activation fails if a required capability isn't installed
|
|
39
|
+
- A **host-side capability broker** convention: profile capabilities execute on the host (credentials in host storage), invoked from the container via the existing `mrctl`→control-plane-API channel — following the proven TradeStation pattern
|
|
40
|
+
- Depends on [[caller-bound-capability-token]] so the broker can trust `callerId` for ownership checks
|
|
33
41
|
|
|
34
42
|
**Out of scope (deferred):**
|
|
35
|
-
-
|
|
36
|
-
- Profile
|
|
37
|
-
-
|
|
43
|
+
- The actual barber business logic (availability rules, 2-barber support, per-number appointment limits) — lives in the **private profiles repo**, not here
|
|
44
|
+
- Profile marketplace/registry — profiles are installed manually (git clone / package) for now
|
|
45
|
+
- Profile versioning/migration — no schema-version negotiation yet
|
|
46
|
+
- Multi-profile per space — one active profile per deployment for MVP
|
|
38
47
|
- Profile UI in dashboard — managed via config/YAML only
|
|
39
|
-
- Declarative
|
|
48
|
+
- Declarative ownership rules — ownership is enforced in the profile's host-side route code using the trusted `callerId`, not a generic schema (too domain-specific for v1)
|
|
40
49
|
|
|
41
50
|
---
|
|
42
51
|
|
|
43
52
|
## Context for Claude
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- `src/core/
|
|
48
|
-
- `src/extensions/
|
|
49
|
-
- `src/core/
|
|
50
|
-
- `src/
|
|
51
|
-
- `src/
|
|
54
|
+
- `src/core/profiles.ts` — Existing profile system: `profileSchema`, `loadProfileFromDir()`, `applyProfile()` (project-level, no DB/space). Extended here
|
|
55
|
+
- `src/core/permissions.ts` — `getRolePermissions()`, `registerPermission()`, `resolveRole()`. Role-based (admin/member) per space
|
|
56
|
+
- `src/core/runtime.ts` — Container spawn: denied-CLI computation + per-permission env injection at 1343-1376
|
|
57
|
+
- `src/extensions/permission-guard.ts` — Bypassable CLI-name denylist (see `docs/TODOS.md` TODO-2)
|
|
58
|
+
- `src/core/routes/tradestation.ts` + `src/tradestation/host-api.ts` — **The reference broker.** Container calls `mrctl tradestation order` → host route executes with a token from `db.getExtState(...)`, never exposing it to the container ("keeps order placement off the agent container"). Includes a caller-bound confirm guard (153-158, 245-254)
|
|
59
|
+
- `src/core/api.ts` — Control-plane API + auth middleware (35-75) that broker routes mount onto
|
|
60
|
+
- `src/cli/mrctl.ts` — In-container CLI that calls the host API (the container→host transport)
|
|
52
61
|
|
|
53
62
|
---
|
|
54
63
|
|
|
55
64
|
## Architecture & Data
|
|
56
65
|
|
|
57
|
-
###
|
|
66
|
+
### The four parts (and where each lives)
|
|
67
|
+
|
|
68
|
+
| Part | Repo | What |
|
|
69
|
+
|------|------|------|
|
|
70
|
+
| 1. Profile schema + permission scoping | `mercury-public` | New `mercury-profile.yaml` fields; profile-aware member permissions |
|
|
71
|
+
| 2. Host-side capability broker | `mercury-public` | Profile capabilities run host-side via `mrctl`→API, à la TradeStation; secrets stay on host |
|
|
72
|
+
| 3. Per-turn caller token | `mercury-public` | [[caller-bound-capability-token]] — trustworthy `callerId` on the host (dependency) |
|
|
73
|
+
| 4. Barber business logic | **private profiles repo** | Availability, 2 barbers, per-number limits, ownership rules — the moat |
|
|
58
74
|
|
|
59
|
-
|
|
75
|
+
### Part 1 — Schema + permission scoping
|
|
76
|
+
|
|
77
|
+
Extend `profileSchema` in `src/core/profiles.ts` (all new fields optional → backward compatible):
|
|
60
78
|
|
|
61
79
|
```typescript
|
|
62
|
-
// New fields added to profileSchema
|
|
63
80
|
const profileSchema = z.object({
|
|
64
|
-
// ... existing
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
agents_md: z.string().optional(),
|
|
69
|
-
extensions: z.array(profileExtensionSchema).default([]),
|
|
70
|
-
env: z.array(profileEnvVarSchema).default([]),
|
|
71
|
-
defaults: profileDefaultsSchema.optional(),
|
|
72
|
-
|
|
73
|
-
// NEW — runtime applicative fields
|
|
74
|
-
capabilities: z.array(z.string()).default([]), // required raw extensions (e.g. ["gws"])
|
|
75
|
-
member_permissions: z.array(z.string()).optional(), // exhaustive member permission list when profile is active
|
|
76
|
-
system_prompt: z.string().optional(), // profile-specific agent persona
|
|
81
|
+
// ... existing: name, description, version, agents_md, extensions, env, defaults ...
|
|
82
|
+
capabilities: z.array(z.string()).default([]), // required host-side capability extensions, e.g. ["gws"]
|
|
83
|
+
member_permissions: z.array(z.string()).optional(), // EXHAUSTIVE member permission set when active
|
|
84
|
+
system_prompt: z.string().optional(), // profile persona
|
|
77
85
|
});
|
|
78
86
|
```
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
88
|
+
Activation is **project-wide** (a deployment runs one profile — the "app"), stored in `space_config` on `main` / applied to member-facing spaces:
|
|
89
|
+
- `profile.active` — profile name
|
|
90
|
+
- `profile.member_permissions` — comma-separated list from the manifest
|
|
91
|
+
|
|
92
|
+
Permission resolution (`getRolePermissions()`): when `profile.member_permissions` is set, that is the **complete** member set — no extension defaults merged in. Admin/system unchanged (all). Consequence: raw `gws` is absent from the member set, so its CLI is denied **and** its env vars are withheld from member containers (`runtime.ts:1360`).
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
**`system_prompt` precedence** (highest wins): per-space `system_prompt` override → profile `system_prompt` → global AGENTS.md.
|
|
85
95
|
|
|
86
|
-
###
|
|
96
|
+
### Part 2 — Host-side capability broker (the core security mechanism)
|
|
87
97
|
|
|
88
|
-
|
|
98
|
+
This is the crux, and it reuses an existing, production-proven pattern rather than inventing one.
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
**How TradeStation already does it (the template):**
|
|
101
|
+
- Container runs `mrctl tradestation order …` → `POST /api/tradestation/orders`
|
|
102
|
+
- The route runs **on the host** (`src/core/routes/tradestation.ts`) using an OAuth token read from host storage (`db.getExtState(...)`, `src/tradestation/host-api.ts:27`) — the token **never enters the container**
|
|
91
103
|
|
|
92
|
-
|
|
104
|
+
**The barber broker is the same shape, Calendar-flavored:**
|
|
105
|
+
- New host routes (e.g. `POST /api/barber/availability|book|cancel`, `GET /api/barber/my-appointments`) mounted on the existing API (`src/core/api.ts`)
|
|
106
|
+
- New `mrctl` (or thin profile-CLI) subcommands the member agent invokes
|
|
107
|
+
- Host route calls Google Calendar using credentials in **host storage** (`extension_state` / host env) — never injected into the customer container
|
|
108
|
+
- Ownership enforced host-side: the route filters/authorizes by the **token-derived** `callerId` (Part 3), so a customer can only see/cancel their own appointments
|
|
93
109
|
|
|
94
|
-
|
|
110
|
+
**Why Gmail is safe:** the customer container gets neither the `gws` CLI nor any Google credential. Even if the customer reads every env var in their container, there is no secret there — only a per-turn token scoped to themselves. Calendar access happens on the host; Gmail is never wired into the broker at all.
|
|
95
111
|
|
|
96
|
-
|
|
112
|
+
### Part 3 — Trustworthy caller identity (dependency)
|
|
97
113
|
|
|
98
|
-
|
|
114
|
+
The broker's ownership checks require a `callerId` the customer cannot forge. Today `callerId` arrives as a spoofable header gated only by a shared `API_SECRET` (`api.ts:50-72`). [[caller-bound-capability-token]] mints a per-turn token bound to `{callerId, spaceId}` at spawn and makes it authoritative host-side. **This feature depends on that one landing** (or shipping together).
|
|
115
|
+
|
|
116
|
+
### Part 4 — Barber business logic (private repo)
|
|
117
|
+
|
|
118
|
+
Lives in `tagula-agent-profiles` (or similar). Contains the `mercury-profile.yaml`, the agent persona, and the host-side route implementations' business rules (availability windows, 2-barber scheduling, per-number booking limits, double-booking prevention). Consumes Parts 1-3 as the public contract; never touches `mercury-public`.
|
|
119
|
+
|
|
120
|
+
### Example manifest (in the private repo)
|
|
99
121
|
|
|
100
122
|
```yaml
|
|
101
|
-
# mercury-profile.yaml (in separate repo)
|
|
102
123
|
name: barber-appointments
|
|
103
124
|
description: Appointment booking for a barber shop
|
|
104
125
|
version: 0.1.0
|
|
105
|
-
|
|
106
126
|
agents_md: ./AGENTS.md
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- gws # requires GWS extension installed (for Calendar)
|
|
110
|
-
|
|
111
|
-
extensions:
|
|
112
|
-
- name: barber
|
|
113
|
-
source: ./extensions/barber
|
|
114
|
-
|
|
115
|
-
member_permissions:
|
|
116
|
-
- prompt
|
|
117
|
-
- prefs.get
|
|
118
|
-
- barber # only the barber extension — NOT gws
|
|
119
|
-
|
|
120
|
-
env:
|
|
121
|
-
- key: MERCURY_BARBER_BUSINESS_HOURS
|
|
122
|
-
description: "Business hours (e.g. 09:00-18:00)"
|
|
123
|
-
default: "09:00-18:00"
|
|
124
|
-
- key: MERCURY_BARBER_SLOT_DURATION
|
|
125
|
-
description: "Appointment slot duration in minutes"
|
|
126
|
-
default: "30"
|
|
127
|
-
|
|
127
|
+
capabilities: [gws] # Calendar via host-side broker
|
|
128
|
+
member_permissions: [prompt, prefs.get, barber] # barber only — NOT gws
|
|
128
129
|
system_prompt: |
|
|
129
|
-
You are a barber shop assistant. Help
|
|
130
|
-
their appointments.
|
|
131
|
-
appointments — never reveal other customers' schedules.
|
|
132
|
-
|
|
130
|
+
You are a barber shop assistant. Help each customer book, view, and cancel
|
|
131
|
+
ONLY their own appointments. Never reveal other customers' schedules.
|
|
133
132
|
defaults:
|
|
134
|
-
trigger_patterns:
|
|
133
|
+
trigger_patterns: always
|
|
135
134
|
```
|
|
136
135
|
|
|
137
|
-
The barber extension (`./extensions/barber/index.ts`):
|
|
138
|
-
- Calls `mercury.cli({ name: "barber", ... })` — installs a CLI with subcommands: `barber book`, `barber availability`, `barber cancel`, `barber my-appointments`
|
|
139
|
-
- Calls `mercury.permission({ defaultRoles: ["member"] })` — members can use the `barber` CLI
|
|
140
|
-
- The CLI internally uses GWS Calendar API (via env var credentials) but filters results by `CALLER_ID`
|
|
141
|
-
- Members NEVER see the `gws` CLI — it's blocked by the permission guard
|
|
142
|
-
|
|
143
|
-
### API Contracts
|
|
144
|
-
|
|
145
|
-
No new HTTP API endpoints. Profile activation is done via:
|
|
146
|
-
1. `mercury.yaml` — `profile: barber-appointments` (static config)
|
|
147
|
-
2. `space_config` — `profile.active: barber-appointments` (per-space override)
|
|
148
|
-
|
|
149
136
|
### File & Folder Structure
|
|
150
137
|
|
|
151
138
|
| Path | New / Modified | Purpose |
|
|
152
139
|
|------|---------------|---------|
|
|
153
|
-
| `src/core/profiles.ts` | Modified | Extend `profileSchema
|
|
154
|
-
| `src/core/permissions.ts` | Modified | Profile-aware `getRolePermissions()`
|
|
155
|
-
| `src/config.ts` | Modified |
|
|
156
|
-
| `src/
|
|
157
|
-
| `
|
|
140
|
+
| `src/core/profiles.ts` | Modified | Extend `profileSchema`; validate `capabilities`; write `profile.*` to `space_config` |
|
|
141
|
+
| `src/core/permissions.ts` | Modified | Profile-aware `getRolePermissions()` |
|
|
142
|
+
| `src/config.ts` / `src/config-file.ts` | Modified | `profile` config key for activation |
|
|
143
|
+
| `src/core/api.ts` | Modified | Mount profile broker routes (generic mounting point) |
|
|
144
|
+
| `src/core/routes/` | New (per profile capability) | Host-side broker routes — pattern mirrors `tradestation.ts` |
|
|
145
|
+
| `tests/applicative-profiles.test.ts` | New | Permission scoping + capability validation tests |
|
|
158
146
|
|
|
159
147
|
### Implementation Sequence
|
|
160
148
|
|
|
161
|
-
1. **Extend profile schema** —
|
|
149
|
+
1. **Extend profile schema** — add `capabilities`, `member_permissions`, `system_prompt` (optional).
|
|
162
150
|
- Read first: `src/core/profiles.ts`
|
|
163
|
-
- Verify: `bun run typecheck
|
|
164
|
-
|
|
165
|
-
2. **Add capability validation** — In `applyProfile()`, after copying extensions, check that every entry in `capabilities` has a matching directory in `.mercury/extensions/`. Throw a clear error if a required capability is missing.
|
|
151
|
+
- Verify: `bun run typecheck`; existing profiles still parse
|
|
152
|
+
2. **Capability validation** — in `applyProfile()`, fail loudly if any `capabilities` entry has no matching extension.
|
|
166
153
|
- Read first: `src/core/profiles.ts`, `src/extensions/loader.ts`
|
|
167
|
-
- Verify:
|
|
154
|
+
- Verify: apply throws on missing capability; passes when present
|
|
168
155
|
- Blocker: Step 1
|
|
169
|
-
|
|
170
|
-
3. **Store profile activation in space_config** — When `applyProfile()` runs for a space, write `profile.active` and `profile.member_permissions` to `space_config`. Add `profile` config key to `config.ts` / `config-file.ts` for static activation.
|
|
156
|
+
3. **Activation + config** — write `profile.active` / `profile.member_permissions` to `space_config`; add `profile` config key.
|
|
171
157
|
- Read first: `src/config.ts`, `src/config-file.ts`, `src/storage/db.ts`
|
|
172
|
-
- Verify: `
|
|
158
|
+
- Verify: `space_config` reflects active profile
|
|
173
159
|
- Blocker: Step 1
|
|
174
|
-
|
|
175
|
-
4. **Profile-aware permission resolution** — In `getRolePermissions()`, when the space has `profile.member_permissions` set, use that as the member permission set instead of the default. Admin/system roles unchanged.
|
|
160
|
+
4. **Profile-aware permissions** — `getRolePermissions()` uses `profile.member_permissions` as the exhaustive member set.
|
|
176
161
|
- Read first: `src/core/permissions.ts`
|
|
177
|
-
- Verify:
|
|
178
|
-
- Blocker: Step 3
|
|
179
|
-
|
|
180
|
-
5. **System prompt injection** — When a profile has `system_prompt`, inject it via the existing `before_container` hook mechanism or config seeding (same pattern as `dm-auto-space`'s `default_system_prompt`).
|
|
181
|
-
- Read first: `src/core/runtime.ts`, `src/extensions/hooks.ts`
|
|
182
|
-
- Verify: `bun run typecheck` passes
|
|
162
|
+
- Verify: member gets only profile perms; admin unchanged; `gws` CLI denied + env withheld for members
|
|
183
163
|
- Blocker: Step 3
|
|
184
|
-
|
|
185
|
-
|
|
164
|
+
5. **Caller token dependency** — land [[caller-bound-capability-token]] (or ship together) so brokers can trust `callerId`.
|
|
165
|
+
- Verify: broker route reads token-derived caller; spoofed header ignored
|
|
166
|
+
- Blocker: separate doc
|
|
167
|
+
6. **Broker route pattern** — document + provide the mounting point and a reference so a profile's host-side routes follow `tradestation.ts` (host-held creds, caller-bound authorization).
|
|
168
|
+
- Read first: `src/core/routes/tradestation.ts`, `src/tradestation/host-api.ts`, `src/core/api.ts`
|
|
169
|
+
- Verify: a sample broker route executes host-side, credential never injected into the container
|
|
170
|
+
- Blocker: Steps 4, 5
|
|
171
|
+
7. **Tests** — scoping (member/admin/raw-capability), capability validation, activation.
|
|
186
172
|
- Read first: `tests/permissions.test.ts`
|
|
187
|
-
- Verify: `bun test`
|
|
188
|
-
- Blocker: Steps 1-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
- Read first: `resources/templates/mercury.example.yaml`
|
|
192
|
-
- Verify: file contains the new section
|
|
173
|
+
- Verify: `bun test`
|
|
174
|
+
- Blocker: Steps 1-6
|
|
175
|
+
8. **Example config** — commented `profile` section in `mercury.example.yaml`.
|
|
176
|
+
- Verify: file contains the section
|
|
193
177
|
|
|
194
178
|
---
|
|
195
179
|
|
|
196
180
|
## Non-Negotiable Rules
|
|
197
181
|
|
|
198
|
-
- **Profiles are external** —
|
|
199
|
-
- **
|
|
200
|
-
- **
|
|
201
|
-
- **
|
|
202
|
-
- **
|
|
203
|
-
- **
|
|
204
|
-
- **
|
|
182
|
+
- **Profiles are external** — no specific business profile (barber, etc.) lives in `mercury-public`. Only schema, scoping, and broker mechanism.
|
|
183
|
+
- **Credentials never enter a member container** — capability credentials live in host storage and are used only by host-side broker routes. This is the whole point; violating it re-opens the Gmail leak.
|
|
184
|
+
- **Member permissions are exhaustive** — a profile's `member_permissions` is the COMPLETE set; no extension defaults merged. Prevents capability leakage.
|
|
185
|
+
- **Ownership is enforced host-side with the trusted `callerId`** — never with a container-supplied argument, never by prompting the LLM.
|
|
186
|
+
- **Admin permissions unchanged** — admins always get all permissions; profiles scope members only.
|
|
187
|
+
- **Capability validation is strict** — activation fails loudly if a required capability extension is missing.
|
|
188
|
+
- **Backward compatible** — existing profiles and existing brokers keep working; all new fields optional; token path is additive.
|
|
189
|
+
- **Do not rely on the permission guard as a security boundary** — it is bypassable (TODO-2). Security comes from *not putting secrets in the container*, not from blocking CLIs.
|
|
205
190
|
|
|
206
191
|
---
|
|
207
192
|
|
|
@@ -209,36 +194,37 @@ No new HTTP API endpoints. Profile activation is done via:
|
|
|
209
194
|
|
|
210
195
|
| Scenario | Handling |
|
|
211
196
|
|----------|---------|
|
|
212
|
-
|
|
|
213
|
-
|
|
|
214
|
-
|
|
|
215
|
-
|
|
|
216
|
-
|
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
-
| Existing deployment upgrades
|
|
197
|
+
| Customer asks the agent to `printenv` / read files | Allowed — but the container holds no capability secret, only a self-scoped per-turn token. Nothing sensitive to leak. |
|
|
198
|
+
| Customer tries to `curl` the broker spoofing another `callerId` | Rejected — broker authorizes by the token-derived caller ([[caller-bound-capability-token]]), not the header. |
|
|
199
|
+
| Profile requires `gws` but it isn't installed | `applyProfile()` throws listing missing capabilities. |
|
|
200
|
+
| `member_permissions` lists an invalid permission | Filtered by `isValidPermission()`; warn, don't crash. |
|
|
201
|
+
| Admin overrides `role.member.permissions` per space | Per-space override still wins (existing behavior); profile sets the baseline. |
|
|
202
|
+
| Broker route needs Calendar but customer has no appointment yet | Ownership filter returns empty; booking creates a record keyed to `callerId`. |
|
|
203
|
+
| System-triggered task (`callerId = "system"`) hits a broker | Broker skips per-customer ownership for system caller (or per Part 3's system-token decision). |
|
|
204
|
+
| Existing deployment upgrades to this version | No behavior change: `member_permissions` optional; brokers additive. |
|
|
220
205
|
|
|
221
206
|
---
|
|
222
207
|
|
|
223
208
|
## Implementation Checklist
|
|
224
209
|
|
|
225
|
-
### Phase 1 — Goal
|
|
210
|
+
### Phase 1 — Goal
|
|
226
211
|
- [x] Goal, User Stories, and MVP Scope written and reviewed
|
|
227
212
|
|
|
228
|
-
### Phase 2 — Architecture
|
|
213
|
+
### Phase 2 — Architecture
|
|
229
214
|
- [x] Architecture & Data section complete
|
|
230
215
|
- [x] Non-Negotiable Rules defined
|
|
231
216
|
- [x] Edge Cases covered
|
|
232
217
|
- [x] Context for Claude pointers filled in
|
|
233
218
|
|
|
234
|
-
### Implementation
|
|
219
|
+
### Implementation
|
|
235
220
|
- [ ] Step 1 — Extend profile schema
|
|
236
|
-
- [ ] Step 2 —
|
|
237
|
-
- [ ] Step 3 —
|
|
238
|
-
- [ ] Step 4 — Profile-aware
|
|
239
|
-
- [ ] Step 5 —
|
|
240
|
-
- [ ] Step 6 —
|
|
241
|
-
- [ ] Step 7 —
|
|
221
|
+
- [ ] Step 2 — Capability validation
|
|
222
|
+
- [ ] Step 3 — Activation + config
|
|
223
|
+
- [ ] Step 4 — Profile-aware permissions
|
|
224
|
+
- [ ] Step 5 — Caller token dependency ([[caller-bound-capability-token]])
|
|
225
|
+
- [ ] Step 6 — Broker route pattern (mirror `tradestation.ts`)
|
|
226
|
+
- [ ] Step 7 — Tests
|
|
227
|
+
- [ ] Step 8 — Example config
|
|
242
228
|
- [ ] `bun run check` passes
|
|
243
229
|
- [ ] No secrets or `.env` files committed
|
|
244
230
|
- [ ] No unrelated files modified
|
|
@@ -248,17 +234,17 @@ No new HTTP API endpoints. Profile activation is done via:
|
|
|
248
234
|
|
|
249
235
|
## Open Questions
|
|
250
236
|
|
|
251
|
-
- [ ] Should `dm_auto_space` auto-activate
|
|
252
|
-
- [ ] How does `
|
|
237
|
+
- [ ] Should `dm_auto_space` auto-activate the profile for new customer spaces (`dm_auto_space.profile: barber-appointments`)? — owner: TBD
|
|
238
|
+
- [ ] How does `callerId` map to appointment ownership — phone number as-is, or a registration/name step? — owner: profile developer (private repo)
|
|
239
|
+
- [ ] Broker routes: generic `POST /api/profile/:capability/:action` mounting vs per-profile route files? — owner: TBD
|
|
253
240
|
|
|
254
241
|
---
|
|
255
242
|
|
|
256
243
|
## Retrospective
|
|
257
244
|
|
|
258
|
-
> Fill this section when archiving.
|
|
259
|
-
|
|
260
245
|
**Residual risks / follow-ups:**
|
|
261
|
-
-
|
|
246
|
+
- Per-turn caller token is a hard dependency for safe ownership → backlog/caller-bound-capability-token.md
|
|
247
|
+
- Barber business logic tracked in the private profiles repo (out of this repo's scope) — accepted
|
|
262
248
|
|
|
263
249
|
**What changed from the plan:**
|
|
264
250
|
- ...
|