mercury-agent 0.5.2 → 0.5.6
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/package.json +1 -1
- package/docs/ARCHITECTURE.md +0 -34
- package/docs/DESIGN.md +0 -42
- package/docs/ROADMAP.md +0 -43
- package/docs/TODOS.md +0 -147
- package/docs/VISION.md +0 -32
- package/docs/archive/.gitkeep +0 -0
- package/docs/archive/applicative-profiles/2026-07-01-applicative-profiles.md +0 -270
- package/docs/archive/applicative-profiles/2026-07-01-caller-bound-capability-token.md +0 -184
- package/docs/archive/applicative-profiles/2026-07-01-dm-auto-space.md +0 -273
- package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +0 -71
- package/docs/backlog/.gitkeep +0 -0
- package/docs/bugs/.gitkeep +0 -0
- package/docs/debug/major/.gitkeep +0 -0
- package/docs/debug/major/2026-07-01-strict-yaml-schema-crash.md +0 -85
- package/docs/debug/major/2026-07-02-bwrap-privileged-linux-docker.md +0 -160
- package/docs/debug/major/2026-07-02-e2big-prompt-delivery-fix.md +0 -382
- package/docs/debug/major/2026-07-02-registry-pull-no-local-fallback.md +0 -194
- package/docs/debug/minor/.gitkeep +0 -0
- package/docs/debug/moderate/.gitkeep +0 -0
- package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +0 -72
- package/docs/html-slides/.gitkeep +0 -0
- package/docs/ideas/.gitkeep +0 -0
- package/docs/ideas/business-extensions.md +0 -48
- package/docs/in-progress/.gitkeep +0 -0
- package/docs/notes/.gitkeep +0 -0
- package/docs/pending-updates/.gitkeep +0 -0
- package/docs/runbooks/gws-oauth-setup.md +0 -211
- package/docs/runbooks/publish-checklist.md +0 -54
- package/docs/templates/TEMPLATE-AUTOPILOT-CONFIG.yaml +0 -35
- package/docs/templates/TEMPLATE-BUG.md +0 -71
- package/docs/templates/TEMPLATE-CI.yml +0 -25
- package/docs/templates/TEMPLATE-DECISIONS.md +0 -11
- package/docs/templates/TEMPLATE-FEATURE.md +0 -127
- package/docs/templates/TEMPLATE-GOAL.md +0 -31
- package/docs/templates/TEMPLATE-IDEA.md +0 -31
- package/docs/templates/TEMPLATE-NOTE.md +0 -27
- package/docs/templates/TEMPLATE-ROADMAP.md +0 -21
package/package.json
CHANGED
package/docs/ARCHITECTURE.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# {Project Name} — Architecture Reference
|
|
2
|
-
|
|
3
|
-
> On-demand reference. Do not auto-load. Read when a task is cross-cutting or you need to understand inter-package data flow.
|
|
4
|
-
|
|
5
|
-
## Packages
|
|
6
|
-
|
|
7
|
-
| Package | Purpose | Stack | Runs On |
|
|
8
|
-
|---------|---------|-------|---------|
|
|
9
|
-
| `{package}/` | {purpose} | {stack} | {where it runs} |
|
|
10
|
-
|
|
11
|
-
## Deployment Topology
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
{ASCII diagram of your deployment — servers, services, databases, CDNs, etc.}
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Data Flow
|
|
18
|
-
|
|
19
|
-
> How data moves between packages, services, and external systems. Include request paths, event flows, and storage interactions.
|
|
20
|
-
|
|
21
|
-
## Key Invariants
|
|
22
|
-
|
|
23
|
-
> Rules that must always hold true across the system. Breaking these causes cascading failures.
|
|
24
|
-
|
|
25
|
-
- {Invariant 1}
|
|
26
|
-
- {Invariant 2}
|
|
27
|
-
|
|
28
|
-
## Extension Points
|
|
29
|
-
|
|
30
|
-
> Where the system is designed to be extended — plugin interfaces, hook points, configuration surfaces.
|
|
31
|
-
|
|
32
|
-
- **Applicative profiles** (`src/core/profiles.ts`) — a `mercury-profile.yaml` declares `capabilities`, an exhaustive `member_permissions` set, and a `system_prompt`. `applyProfile` validates capabilities and persists `.mercury/active-profile.json`; `main.ts` activates it at startup (project-wide). Member permission scoping is in `getRolePermissions` (`src/core/permissions.ts`). Authoring contract: `docs/authoring-profiles.md`.
|
|
33
|
-
- **Host-side capability broker** — extensions register `mercury.capability(name, handler)` (`src/extensions/types.ts`, `api.ts`); the agent invokes them from the container via `mrctl capability <name> <action>` → `POST /api/capability/:name/:action` (`src/core/routes/capability.ts`). Handlers run on the host with credentials that never enter the container; identity is the token-derived caller.
|
|
34
|
-
- **Caller-bound capability token** (`src/core/caller-token.ts`) — per-turn HMAC token minted at container spawn, made authoritative over spoofable identity headers in the control-plane API auth middleware (`src/core/api.ts`).
|
package/docs/DESIGN.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# {Project Name} — Design System
|
|
2
|
-
|
|
3
|
-
> Fill this in if your project has a UI. Skip if it's a backend-only project.
|
|
4
|
-
|
|
5
|
-
## 1. Visual Theme
|
|
6
|
-
|
|
7
|
-
> Design principles — what the UI should feel like.
|
|
8
|
-
|
|
9
|
-
## 2. Color Roles
|
|
10
|
-
|
|
11
|
-
> CSS variable inventory. All colors must be defined as variables — never hardcode hex in component files.
|
|
12
|
-
|
|
13
|
-
| Token | Dark Value | Light Value | Role |
|
|
14
|
-
|-------|-----------|-------------|------|
|
|
15
|
-
| `--bg` | {hex} | {hex} | Page background |
|
|
16
|
-
| `--surface` | {hex} | {hex} | Card/panel background |
|
|
17
|
-
| `--text` | {hex} | {hex} | Primary text |
|
|
18
|
-
| `--muted` | {hex} | {hex} | Secondary text |
|
|
19
|
-
| `--accent` | {hex} | {hex} | Interactive elements |
|
|
20
|
-
| `--border` | {hex} | {hex} | Borders and dividers |
|
|
21
|
-
| `--color-success` | {hex} | {hex} | Success states |
|
|
22
|
-
| `--color-error` | {hex} | {hex} | Error states |
|
|
23
|
-
| `--color-warning` | {hex} | {hex} | Warning states |
|
|
24
|
-
|
|
25
|
-
## 3. Typography
|
|
26
|
-
|
|
27
|
-
> Font families, sizes, weights. Which fonts for which contexts.
|
|
28
|
-
|
|
29
|
-
## 4. Layout & Spacing
|
|
30
|
-
|
|
31
|
-
> Grid system, breakpoints, spacing scale. Include RTL/logical property conventions if applicable.
|
|
32
|
-
|
|
33
|
-
## 5. Components
|
|
34
|
-
|
|
35
|
-
> Component conventions — buttons, inputs, cards, modals, status indicators, etc.
|
|
36
|
-
|
|
37
|
-
## 6. Hard Rules
|
|
38
|
-
|
|
39
|
-
> Violations block merge. Examples:
|
|
40
|
-
> - No hardcoded hex colors in component files — use CSS variables
|
|
41
|
-
> - No physical CSS properties (`left`/`right`) — use logical properties
|
|
42
|
-
> - All interactive elements must have visible focus rings
|
package/docs/ROADMAP.md
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# {Project Name} — Roadmap
|
|
2
|
-
|
|
3
|
-
Single source of truth for priority. Rule: ≤15 min/week to maintain.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Inbox
|
|
8
|
-
|
|
9
|
-
> Uncategorized ideas. Triage into Now / Next / Later or drop.
|
|
10
|
-
|
|
11
|
-
| Item | Doc | Notes |
|
|
12
|
-
|------|-----|-------|
|
|
13
|
-
| — | — | — |
|
|
14
|
-
|
|
15
|
-
## Now
|
|
16
|
-
|
|
17
|
-
> Max 2 items. What you're actively working on.
|
|
18
|
-
|
|
19
|
-
| Item | Doc | Started | Notes |
|
|
20
|
-
|------|-----|---------|-------|
|
|
21
|
-
| — | — | — | — |
|
|
22
|
-
|
|
23
|
-
## Next
|
|
24
|
-
|
|
25
|
-
| # | Item | Doc | Depends on | Why now |
|
|
26
|
-
|---|------|-----|-----------|---------|
|
|
27
|
-
| 1 | **autonomous-planning-tier** | [doc](backlog/autonomous-planning-tier.md) | — | Let agents decompose ideas, keep human gate |
|
|
28
|
-
|
|
29
|
-
## Later
|
|
30
|
-
|
|
31
|
-
| Item | Doc | Notes |
|
|
32
|
-
|------|-----|-------|
|
|
33
|
-
| — | — | — |
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Shipped
|
|
38
|
-
|
|
39
|
-
| Item | Shipped | What |
|
|
40
|
-
|------|---------|------|
|
|
41
|
-
| **applicative-profiles** | 2026-07-01 | Generic profile layer: manifest schema, exhaustive member scoping, host-side capability broker |
|
|
42
|
-
| **caller-bound-capability-token** | 2026-07-01 | Per-turn unspoofable caller token for host API auth; also hardens TradeStation |
|
|
43
|
-
| **dm-auto-space** | 2026-07-01 | Auto-create space per customer DM with rate limits and dashboard controls |
|
package/docs/TODOS.md
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
# Mercury — Remaining TODOs
|
|
2
|
-
|
|
3
|
-
This document tracks gaps identified in the comprehensive code review that are not yet addressed. Items are ordered by priority (security first, then reliability, then polish).
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Security
|
|
8
|
-
|
|
9
|
-
### TODO-1: Protect dashboard root routes
|
|
10
|
-
|
|
11
|
-
**Status:** Open
|
|
12
|
-
**Priority:** Medium
|
|
13
|
-
**Files:** `src/server.ts`
|
|
14
|
-
|
|
15
|
-
**Issue:** `GET /` and `GET /dashboard` serve the dashboard HTML without authentication. Only `/dashboard/*` (htmx partials, API, SSE) is protected. Users can load the HTML shell without a token.
|
|
16
|
-
|
|
17
|
-
**Fix:** Apply the same auth middleware to `/` and `/dashboard`, or redirect unauthenticated requests to a login page. When `MERCURY_API_SECRET` is set, require Bearer token or `mercury_token` cookie for these routes.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
### TODO-2: Harden permission guard
|
|
22
|
-
|
|
23
|
-
**Status:** Open
|
|
24
|
-
**Priority:** Medium
|
|
25
|
-
**Files:** `src/extensions/permission-guard.ts`
|
|
26
|
-
|
|
27
|
-
**Issue:** The permission guard blocks CLI names via regex on bash commands. It can be bypassed by:
|
|
28
|
-
- `env napkin search "query"`
|
|
29
|
-
- `` `which napkin` search "query" ``
|
|
30
|
-
- `python3 -c "import subprocess; subprocess.run(['napkin', ...])"`
|
|
31
|
-
- Path-based execution: `/root/.bun/bin/napkin`
|
|
32
|
-
|
|
33
|
-
**Fix options:**
|
|
34
|
-
1. Run denied CLIs through an allowlist/blocklist at the pi tool-call layer (if the pi API supports it)
|
|
35
|
-
2. Use a wrapper script that validates the command before execution
|
|
36
|
-
3. Document that this is defense-in-depth only; rely on extension trust model
|
|
37
|
-
4. Consider seccomp or additional bwrap restrictions to limit subprocess execution
|
|
38
|
-
|
|
39
|
-
**Note:** Bubblewrap limits filesystem access but does not prevent the agent from invoking allowed binaries. The permission guard remains the primary control for which CLIs run.
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
### TODO-3: Sanitize extension install commands
|
|
44
|
-
|
|
45
|
-
**Status:** Open
|
|
46
|
-
**Priority:** Low
|
|
47
|
-
**Files:** `src/extensions/image-builder.ts`
|
|
48
|
-
|
|
49
|
-
**Issue:** Extension `install` commands are interpolated directly into Dockerfile `RUN` statements. A malicious extension could inject shell commands that exfiltrate secrets or modify the image.
|
|
50
|
-
|
|
51
|
-
**Fix options:**
|
|
52
|
-
1. Validate install commands against an allowlist (e.g. `bun add -g X`, `npm install -g X`)
|
|
53
|
-
2. Run install commands in a sandboxed build step
|
|
54
|
-
3. Document that extensions are trusted by design (operator installs them); recommend auditing before `mercury add`
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## Reliability
|
|
59
|
-
|
|
60
|
-
### TODO-4: Database migration system
|
|
61
|
-
|
|
62
|
-
**Status:** Open
|
|
63
|
-
**Priority:** Medium
|
|
64
|
-
**Files:** `src/storage/db.ts`, new `src/storage/migrations/`
|
|
65
|
-
|
|
66
|
-
**Issue:** Schema evolution uses `CREATE TABLE IF NOT EXISTS` only. Adding columns, changing types, or renaming tables has no migration path.
|
|
67
|
-
|
|
68
|
-
**Fix:** Implement a simple migration system:
|
|
69
|
-
1. Create `migrations/` directory with sequential SQL files (`001_initial.sql`, `002_add_mutes.sql`, …)
|
|
70
|
-
2. Add `schema_migrations` table to track applied migrations
|
|
71
|
-
3. On startup, run any migrations not yet applied
|
|
72
|
-
4. Document migration authoring in `docs/`
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
### TODO-5: Outbox scan race condition
|
|
77
|
-
|
|
78
|
-
**Status:** Open
|
|
79
|
-
**Priority:** Low
|
|
80
|
-
**Files:** `src/core/outbox.ts`, `src/agent/container-runner.ts`
|
|
81
|
-
|
|
82
|
-
**Issue:** Outbox scanning uses mtime comparison against `startTime`. Files written at exactly `startTime` or during clock skew could be missed or incorrectly included.
|
|
83
|
-
|
|
84
|
-
**Fix:** Use a more robust approach (e.g. track written files explicitly, or use a small time buffer). Document the current behavior and edge cases.
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## Operations
|
|
89
|
-
|
|
90
|
-
### TODO-6: Run containers as non-root
|
|
91
|
-
|
|
92
|
-
**Status:** Open
|
|
93
|
-
**Priority:** Low
|
|
94
|
-
**Files:** `container/Dockerfile`, `container/Dockerfile.minimal`, `src/agent/container-runner.ts`
|
|
95
|
-
|
|
96
|
-
**Issue:** Containers run as root. Chromium uses `--no-sandbox` because it runs as root. This increases blast radius if the container is compromised.
|
|
97
|
-
|
|
98
|
-
**Fix:**
|
|
99
|
-
1. Create a non-root user in the Dockerfile
|
|
100
|
-
2. Run the entrypoint as that user (`USER mercury` or similar)
|
|
101
|
-
3. Ensure Chromium can run without `--no-sandbox` (may require `--user-data-dir` and other flags, or use a different approach)
|
|
102
|
-
4. Update `container-runner.ts` if `docker run` needs `--user` override
|
|
103
|
-
|
|
104
|
-
**Note:** Bubblewrap runs as the same user as the parent process. Moving the container to non-root would also make bwrap run as non-root, reducing privilege further.
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
### TODO-7: CORS configuration
|
|
109
|
-
|
|
110
|
-
**Status:** Open
|
|
111
|
-
**Priority:** Low
|
|
112
|
-
**Files:** `src/server.ts`
|
|
113
|
-
|
|
114
|
-
**Issue:** No CORS headers are set. When Mercury is behind a reverse proxy or accessed from a browser on another origin, cross-origin requests may fail or behave unexpectedly.
|
|
115
|
-
|
|
116
|
-
**Fix:** Add configurable CORS middleware (e.g. `MERCURY_CORS_ORIGINS`). Default to restrictive (same-origin or empty) for security.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## In-Memory State (Deferred)
|
|
121
|
-
|
|
122
|
-
### TODO-8: Crash-safe rate limiter and queue
|
|
123
|
-
|
|
124
|
-
**Status:** Deferred
|
|
125
|
-
**Priority:** Low
|
|
126
|
-
**Files:** `src/core/rate-limiter.ts`, `src/core/space-queue.ts`
|
|
127
|
-
|
|
128
|
-
**Issue:** Rate limiter and queue are in-memory. On crash, rate limit windows reset (allowing burst abuse) and queued work is lost.
|
|
129
|
-
|
|
130
|
-
**Fix:** Persist rate limit state and queue to SQLite or Redis. Adds complexity; may be acceptable for single-node deployments. Revisit if multi-instance or high-availability is required.
|
|
131
|
-
|
|
132
|
-
---
|
|
133
|
-
|
|
134
|
-
## Bubblewrap — What It Mitigates
|
|
135
|
-
|
|
136
|
-
Bubblewrap adds **defense-in-depth** inside the container:
|
|
137
|
-
|
|
138
|
-
| Risk | Mitigated by bubblewrap? |
|
|
139
|
-
|------|--------------------------|
|
|
140
|
-
| **Cross-space data access** | Partially — with per-space mount, `/spaces` only has one space. Bwrap further restricts the pi process to a minimal mount set. |
|
|
141
|
-
| **Arbitrary filesystem access** | Yes — pi only sees `/usr`, `/app`, `/etc`, `/docs`, `/spaces`, `/root`, `/proc`, `/dev`, `/tmp`. Cannot access other paths. |
|
|
142
|
-
| **Process isolation** | Yes — `--unshare-pid`, `--new-session`, `--die-with-parent` limit process visibility and lifecycle. |
|
|
143
|
-
| **Permission guard bypass** | No — bwrap limits *what* the agent can access, not *which commands* it runs. A bypassed guard could still invoke allowed binaries. |
|
|
144
|
-
| **Extension install injection** | No — that happens at image build time, before bwrap runs. |
|
|
145
|
-
| **Container non-root** | No — bwrap runs as the same user (root) as the container. Moving to non-root would improve both. |
|
|
146
|
-
|
|
147
|
-
**Summary:** Bubblewrap reduces blast radius if the agent is compromised. It does not replace the permission guard, API auth, or other controls. It is a valuable additional layer.
|
package/docs/VISION.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# {Project Name} — Product Vision
|
|
2
|
-
|
|
3
|
-
## What {Project Name} Is
|
|
4
|
-
|
|
5
|
-
> One-sentence pitch.
|
|
6
|
-
|
|
7
|
-
## Target User
|
|
8
|
-
|
|
9
|
-
> Who is this for? Be specific — role, context, what they're trying to accomplish.
|
|
10
|
-
|
|
11
|
-
## What Makes {Project Name} Different
|
|
12
|
-
|
|
13
|
-
| Differentiator | Why it matters |
|
|
14
|
-
|---------------|----------------|
|
|
15
|
-
| {differentiator} | {why} |
|
|
16
|
-
|
|
17
|
-
## Non-Goals
|
|
18
|
-
|
|
19
|
-
> Explicit list of what this project does NOT try to be. Helps filter feature requests.
|
|
20
|
-
|
|
21
|
-
- {Non-goal 1}
|
|
22
|
-
- {Non-goal 2}
|
|
23
|
-
|
|
24
|
-
## Guiding Questions for New Ideas
|
|
25
|
-
|
|
26
|
-
> Use these to evaluate whether a feature idea fits the product:
|
|
27
|
-
|
|
28
|
-
1. Does this serve the target user directly?
|
|
29
|
-
2. Does this align with the differentiators above?
|
|
30
|
-
3. Does this conflict with any non-goal?
|
|
31
|
-
4. Can the user benefit from this without configuration or documentation?
|
|
32
|
-
5. Would removing this make the product worse for the target user?
|
package/docs/archive/.gitkeep
DELETED
|
File without changes
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
# Applicative Profiles
|
|
2
|
-
|
|
3
|
-
**Status**: Done
|
|
4
|
-
**Slug**: applicative-profiles
|
|
5
|
-
**Created**: 2026-07-01
|
|
6
|
-
**Last updated**: 2026-07-01
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## Goal
|
|
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. 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
|
-
|
|
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).
|
|
24
|
-
|
|
25
|
-
## User Stories
|
|
26
|
-
|
|
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.
|
|
32
|
-
|
|
33
|
-
## MVP Scope
|
|
34
|
-
|
|
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)
|
|
37
|
-
- Profile-aware permission resolution — when a profile is active, member permissions come from the profile, not from extension defaults
|
|
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
|
|
41
|
-
|
|
42
|
-
**Out of scope (deferred):**
|
|
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
|
|
47
|
-
- Profile UI in dashboard — managed via config/YAML only
|
|
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)
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
|
|
52
|
-
## Context for Claude
|
|
53
|
-
|
|
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)
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## Architecture & Data
|
|
65
|
-
|
|
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 repo** `tagula-agents/tagula-agent-profiles` | Availability, 2 barbers, per-number limits, ownership rules — the moat |
|
|
74
|
-
|
|
75
|
-
### Part 1 — Schema + permission scoping
|
|
76
|
-
|
|
77
|
-
Extend `profileSchema` in `src/core/profiles.ts` (all new fields optional → backward compatible):
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
const profileSchema = z.object({
|
|
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
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
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`).
|
|
93
|
-
|
|
94
|
-
**`system_prompt` precedence** (highest wins): per-space `system_prompt` override → profile `system_prompt` → global AGENTS.md.
|
|
95
|
-
|
|
96
|
-
### Part 2 — Host-side capability broker (the core security mechanism)
|
|
97
|
-
|
|
98
|
-
This is the crux, and it reuses an existing, production-proven pattern rather than inventing one.
|
|
99
|
-
|
|
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**
|
|
103
|
-
|
|
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
|
|
109
|
-
|
|
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.
|
|
111
|
-
|
|
112
|
-
### Part 3 — Trustworthy caller identity (dependency)
|
|
113
|
-
|
|
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 the private monorepo `Avishai-tsabari/tagula-agents` (`c:/code/agentic/tagula-agents`), under `tagula-agent-profiles/` (the repo also holds the cloud console). Checked out as a sibling of `mercury-public`. 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)
|
|
121
|
-
|
|
122
|
-
```yaml
|
|
123
|
-
name: barber-appointments
|
|
124
|
-
description: Appointment booking for a barber shop
|
|
125
|
-
version: 0.1.0
|
|
126
|
-
agents_md: ./AGENTS.md
|
|
127
|
-
capabilities: [gws] # Calendar via host-side broker
|
|
128
|
-
member_permissions: [prompt, prefs.get, barber] # barber only — NOT gws
|
|
129
|
-
system_prompt: |
|
|
130
|
-
You are a barber shop assistant. Help each customer book, view, and cancel
|
|
131
|
-
ONLY their own appointments. Never reveal other customers' schedules.
|
|
132
|
-
defaults:
|
|
133
|
-
trigger_patterns: always
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### File & Folder Structure
|
|
137
|
-
|
|
138
|
-
| Path | New / Modified | Purpose |
|
|
139
|
-
|------|---------------|---------|
|
|
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 |
|
|
146
|
-
|
|
147
|
-
### Implementation Sequence
|
|
148
|
-
|
|
149
|
-
1. **Extend profile schema** — add `capabilities`, `member_permissions`, `system_prompt` (optional).
|
|
150
|
-
- Read first: `src/core/profiles.ts`
|
|
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.
|
|
153
|
-
- Read first: `src/core/profiles.ts`, `src/extensions/loader.ts`
|
|
154
|
-
- Verify: apply throws on missing capability; passes when present
|
|
155
|
-
- Blocker: Step 1
|
|
156
|
-
3. **Activation + config** — write `profile.active` / `profile.member_permissions` to `space_config`; add `profile` config key.
|
|
157
|
-
- Read first: `src/config.ts`, `src/config-file.ts`, `src/storage/db.ts`
|
|
158
|
-
- Verify: `space_config` reflects active profile
|
|
159
|
-
- Blocker: Step 1
|
|
160
|
-
4. **Profile-aware permissions** — `getRolePermissions()` uses `profile.member_permissions` as the exhaustive member set.
|
|
161
|
-
- Read first: `src/core/permissions.ts`
|
|
162
|
-
- Verify: member gets only profile perms; admin unchanged; `gws` CLI denied + env withheld for members
|
|
163
|
-
- Blocker: Step 3
|
|
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.
|
|
172
|
-
- Read first: `tests/permissions.test.ts`
|
|
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
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## Non-Negotiable Rules
|
|
181
|
-
|
|
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.
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
## Edge Cases & Risks
|
|
194
|
-
|
|
195
|
-
| Scenario | Handling |
|
|
196
|
-
|----------|---------|
|
|
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. |
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
## Implementation Checklist
|
|
209
|
-
|
|
210
|
-
### Phase 1 — Goal
|
|
211
|
-
- [x] Goal, User Stories, and MVP Scope written and reviewed
|
|
212
|
-
|
|
213
|
-
### Phase 2 — Architecture
|
|
214
|
-
- [x] Architecture & Data section complete
|
|
215
|
-
- [x] Non-Negotiable Rules defined
|
|
216
|
-
- [x] Edge Cases covered
|
|
217
|
-
- [x] Context for Claude pointers filled in
|
|
218
|
-
|
|
219
|
-
### Implementation
|
|
220
|
-
- [x] Step 1 — Extend profile schema (`capabilities`, `member_permissions`, `system_prompt`); swapped hand-rolled `parseSimpleYaml` for the `yaml` lib (multiline `system_prompt` support, less fragility)
|
|
221
|
-
- [x] Step 2 — Capability validation (`validateProfileCapabilities`, called in `applyProfile`); tests in `tests/applicative-profiles.test.ts`
|
|
222
|
-
- [x] Step 3 — Activation + config: `profile` config key; `applyProfile` persists `.mercury/active-profile.json`; `main.ts` reads it at startup (after extensions load) and calls `setActiveProfileMemberPermissions`
|
|
223
|
-
- [x] Step 4 — Profile-aware permissions: `getRolePermissions` uses the active profile's member set as exhaustive; precedence = explicit per-space override > profile > defaults; admin/system unaffected
|
|
224
|
-
- [x] Step 5 — Caller token dependency ([[caller-bound-capability-token]]) — implemented and committed
|
|
225
|
-
- [x] Step 5b — Inject `system_prompt` at container spawn (appended to `MERCURY_EXT_SYSTEM_PROMPT` in `runtime.ts`, ahead of per-space prompt)
|
|
226
|
-
- [x] Step 6 — Host-side capability broker: `mercury.capability(name, handler)` extension API; `POST /api/capability/:name/:action` route (auth = token-derived caller, authz = permission named after the capability); `registry.getCapability`; `mrctl capability <name> <action> [json]`
|
|
227
|
-
- [x] Step 7 — Tests: `tests/api.test.ts` capability broker (dispatch with token-derived caller; permission-denied → 403) + schema/scoping tests in `tests/applicative-profiles.test.ts`
|
|
228
|
-
- [x] Step 8 — Example config: profile + `mrctl capability` + `MERCURY_CALLER_TOKEN_KEY` notes in `mercury.example.yaml`
|
|
229
|
-
- [ ] `bun run check` passes
|
|
230
|
-
- [ ] No secrets or `.env` files committed
|
|
231
|
-
- [ ] No unrelated files modified
|
|
232
|
-
- [ ] All user stories verifiably met
|
|
233
|
-
|
|
234
|
-
---
|
|
235
|
-
|
|
236
|
-
## Open Questions
|
|
237
|
-
|
|
238
|
-
- [ ] Should `dm_auto_space` auto-activate the profile for new customer spaces (`dm_auto_space.profile: barber-appointments`)? — owner: TBD
|
|
239
|
-
- [ ] How does `callerId` map to appointment ownership — phone number as-is, or a registration/name step? — owner: profile developer (private repo)
|
|
240
|
-
- [ ] Broker routes: generic `POST /api/profile/:capability/:action` mounting vs per-profile route files? — owner: TBD
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
## Retrospective
|
|
245
|
-
|
|
246
|
-
**Residual risks / follow-ups:**
|
|
247
|
-
- Barber business logic (availability, 2-barber, per-number limits) lives in the private `tagula-agents` repo — out of this repo's scope — accepted
|
|
248
|
-
- Ephemeral caller-token key breaks `mrctl` if the API and container-runner ever run in separate processes without `MERCURY_CALLER_TOKEN_KEY` set; documented in config + `docs/authoring-profiles.md` — accepted
|
|
249
|
-
- `mrctl capability <name>/<action>` are not URL-encoded (path traversal only reaches other RBAC-guarded routes, no escalation) — accepted
|
|
250
|
-
- Duplicate capability-name registration across two extensions silently first-wins in `registry.getCapability` — accepted
|
|
251
|
-
- Empty `member_permissions: []` in a manifest gives members zero permissions (author's explicit exhaustive choice) — accepted
|
|
252
|
-
|
|
253
|
-
**What changed from the plan:**
|
|
254
|
-
- Signing key is NOT derived from `API_SECRET` (the plan's fallback) — `API_SECRET` is readable in the container, so an ephemeral host-only random key is used instead (config `callerTokenKey` for multi-process).
|
|
255
|
-
- Caller token split into its own delivered dependency (`caller-bound-capability-token`) — also hardened TradeStation, which reads identity from the same auth context (no code change needed there).
|
|
256
|
-
- Replaced the hand-rolled `parseSimpleYaml` in `profiles.ts` with the `yaml` lib (multiline `system_prompt` support + less fragility); built-in profiles still parse.
|
|
257
|
-
- Profile activation is project-wide via a module-level registry + persisted `.mercury/active-profile.json`, not per-space `space_config` writes (simpler, works for later-created dm-auto-space spaces).
|
|
258
|
-
- Broker registration model chosen with the user: generic `mercury.capability(name, handler)` + `POST /api/capability/:name/:action` + `mrctl capability`, authorized by the permission named after the capability.
|
|
259
|
-
- Added `docs/authoring-profiles.md` as the contract for the external profiles repo.
|
|
260
|
-
|
|
261
|
-
**Key decisions made during implementation:**
|
|
262
|
-
- Token = HMAC-SHA256 over `base64url(payload).base64url(sig)`, constant-time compare with length guard; TTL = container timeout + 60s.
|
|
263
|
-
- `getRolePermissions` uses `Array.isArray` (not `!== null`) on the active-profile member set so a malformed `active-profile.json` falls back to defaults instead of throwing in the hot path (found in session review).
|
|
264
|
-
- Profile schema uses plain `z.object` (no `.strict()`), so unknown manifest keys are stripped, honoring the `2026-07-01-strict-yaml-schema-crash` post-mortem (warn/ignore unknown keys, fail only on invalid values); startup `loadActiveProfile` is try/catch → null.
|
|
265
|
-
|
|
266
|
-
**Architecture impact** (update `docs/ARCHITECTURE.md` if any of these apply):
|
|
267
|
-
- [x] New package/module added — `src/core/caller-token.ts`, `src/core/routes/capability.ts`
|
|
268
|
-
- [ ] New data model or schema change (uses existing `space_config`; adds a local `.mercury/active-profile.json` file, no DB schema change)
|
|
269
|
-
- [x] New inter-package interaction — extension host-side capability handlers invoked via the control-plane API
|
|
270
|
-
- [ ] Deployment topology changed
|