mercury-agent 0.4.27 → 0.5.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.
@@ -0,0 +1,174 @@
1
+ # Authoring Applicative Profiles
2
+
3
+ The contract for building a **profile** — a package of deterministic business
4
+ logic that wraps raw capabilities (Calendar, email, …) and scopes what members
5
+ can do. Mercury (this repo) provides the schema, loader, permission scoping, and
6
+ the host-side capability broker; **profiles live in a separate repo** and depend
7
+ only on this contract.
8
+
9
+ > Layers: **Mercury** = platform (messaging, spaces, permissions, tokens,
10
+ > broker). **Capability extensions** = raw API access (e.g. `gws`), admin-only.
11
+ > **Profiles** = deterministic domain logic wrapping capabilities, member-facing.
12
+
13
+ ## 1. Profile manifest — `mercury-profile.yaml`
14
+
15
+ ```yaml
16
+ name: barber-appointments # lowercase-kebab; required
17
+ description: Appointment booking for a barber shop
18
+ version: 0.1.0
19
+
20
+ agents_md: ./AGENTS.md # optional; copied to the global AGENTS.md
21
+
22
+ # Raw capability extensions this profile requires to be installed. Validated at
23
+ # apply time — activation fails loudly if any are missing.
24
+ capabilities:
25
+ - gws
26
+
27
+ # Extensions this profile ships (copied into .mercury/extensions).
28
+ extensions:
29
+ - name: barber
30
+ source: ./extensions/barber
31
+
32
+ # EXHAUSTIVE member permission set while active. When present it REPLACES the
33
+ # default member permissions — nothing is merged in — so raw capabilities stay
34
+ # admin-only unless listed here. Omit entirely to keep built-in defaults.
35
+ member_permissions:
36
+ - prompt
37
+ - prefs.get
38
+ - barber # the profile's own capability, NOT gws
39
+
40
+ # Optional env the profile needs (host-side credentials for capabilities).
41
+ env:
42
+ - key: MERCURY_BARBER_BUSINESS_HOURS
43
+ description: "Business hours"
44
+ default: "09:00-18:00"
45
+
46
+ # Project-wide agent persona, injected into every container's system prompt.
47
+ system_prompt: |
48
+ You are a barber shop assistant. Help each customer book, view, and cancel
49
+ ONLY their own appointments. Never reveal other customers' schedules.
50
+
51
+ defaults:
52
+ trigger_patterns: always
53
+ ```
54
+
55
+ Apply it with `mercury profile apply <name|path|git-url>`. That validates
56
+ capabilities, copies extensions/AGENTS.md, and persists activation to
57
+ `.mercury/active-profile.json`, which Mercury loads at startup.
58
+
59
+ ## 2. The capability broker (where the business logic runs)
60
+
61
+ A profile's privileged work runs **on the host**, so credentials never enter the
62
+ customer-controlled agent container. The profile's extension registers a
63
+ handler; the agent invokes it through a thin CLI.
64
+
65
+ ### Register a handler (in the extension's `index.ts`)
66
+
67
+ ```ts
68
+ import type { ExtensionSetupFn } from "mercury-agent"; // types from this repo
69
+
70
+ const setup: ExtensionSetupFn = (mercury) => {
71
+ // Make the capability gate-able. Members get it via member_permissions.
72
+ mercury.permission({ defaultRoles: [] });
73
+
74
+ // Host-side credentials for the wrapped capability (never sent to the container).
75
+ mercury.env({ from: "MERCURY_BARBER_GOOGLE_CREDENTIALS" });
76
+
77
+ mercury.capability("barber", async (req, ctx) => {
78
+ // req.callerId is the TOKEN-DERIVED, unspoofable caller — safe for ownership.
79
+ switch (req.action) {
80
+ case "availability":
81
+ return { data: await listOpenSlots(req.body, ctx) };
82
+ case "book":
83
+ return { data: await book(req.callerId, req.body, ctx) };
84
+ case "cancel":
85
+ return { data: await cancelOwn(req.callerId, req.body, ctx) };
86
+ case "my-appointments":
87
+ return { data: await listOwn(req.callerId, ctx) };
88
+ default:
89
+ return { status: 400, data: { error: `unknown action: ${req.action}` } };
90
+ }
91
+ });
92
+ };
93
+
94
+ export default setup;
95
+ ```
96
+
97
+ ### The contract types (exported from this repo)
98
+
99
+ ```ts
100
+ type CapabilityHandler = (
101
+ req: CapabilityRequest,
102
+ ctx: MercuryExtensionContext,
103
+ ) => Promise<CapabilityResult>;
104
+
105
+ interface CapabilityRequest {
106
+ name: string; // capability name
107
+ action: string; // sub-action (book, cancel, …)
108
+ callerId: string; // token-derived, TRUSTWORTHY — use for ownership checks
109
+ spaceId: string;
110
+ body: unknown; // parsed JSON request body (or null)
111
+ }
112
+
113
+ interface CapabilityResult {
114
+ status?: number; // default 200
115
+ data: unknown; // JSON-serializable response
116
+ }
117
+ ```
118
+
119
+ `ctx` (`MercuryExtensionContext`) gives you `db`, `config`, `log`, and
120
+ `hasCallerPermission(spaceId, callerId, permission)`. Store per-profile state via
121
+ `mercury.store` (namespaced KV) or your own tables through `ctx.db`.
122
+
123
+ ### How the agent invokes it
124
+
125
+ Inside the container the agent runs:
126
+
127
+ ```
128
+ mrctl capability barber book '{"slot":"2026-07-02T15:00"}'
129
+ ```
130
+
131
+ → `POST /api/capability/barber/book`. Document these commands for the agent in
132
+ the extension's `SKILL.md` or the profile's `AGENTS.md`.
133
+
134
+ ## 3. Permission & security model (non-negotiable)
135
+
136
+ - **`member_permissions` is exhaustive.** List every permission a member may
137
+ hold, including the capability name (e.g. `barber`). Anything not listed —
138
+ including raw capabilities like `gws` — is unavailable to members.
139
+ - **Authorization = permission named after the capability.** The broker route
140
+ requires the caller to hold the `<name>` permission; the same grant gates both
141
+ the `mrctl capability <name> …` CLI and the route. Keep capability name ==
142
+ permission name == the entry in `member_permissions`.
143
+ - **Credentials stay on the host.** Put capability credentials in host env
144
+ (`mercury.env`) / `mercury.store` and use them only inside the handler. Never
145
+ expose a raw capability CLI (e.g. `gws`) to members — its secret would be
146
+ readable inside the container.
147
+ - **Enforce ownership with `req.callerId`.** It is token-derived and cannot be
148
+ spoofed by the container. Never trust an id passed in `req.body`.
149
+ - **`callerId === "system"`** for scheduled/system runs — handle it explicitly
150
+ (e.g. skip per-customer ownership).
151
+
152
+ ## 4. Suggested profile repo layout
153
+
154
+ ```
155
+ <profile-repo>/
156
+ barber/ # one folder per profile
157
+ mercury-profile.yaml
158
+ AGENTS.md
159
+ extensions/
160
+ barber/
161
+ index.ts # registers permission + capability
162
+ package.json
163
+ ```
164
+
165
+ Depend on this repo (`mercury-agent`) for the types
166
+ (`ExtensionSetupFn`, `CapabilityRequest`, `CapabilityResult`,
167
+ `MercuryExtensionContext`). See `src/extensions/types.ts` for the full surface.
168
+
169
+ ## 5. Local test loop
170
+
171
+ 1. `mercury profile apply ./barber`
172
+ 2. `mercury service install` (builds the derived image with the extension CLI)
173
+ 3. DM the bot as a non-admin number; confirm you can `book`/`cancel` only your
174
+ own appointments and cannot reach `gws`/Gmail.
@@ -0,0 +1,85 @@
1
+ # Bug: Strict YAML schema crashes Mercury on unknown config keys
2
+
3
+ **Status**: Fixed
4
+ **Severity**: major
5
+ **Slug**: strict-yaml-schema-crash
6
+ **Reported**: 2026-07-01
7
+ **Last updated**: 2026-07-01
8
+
9
+ ---
10
+
11
+ ## Summary
12
+ `mercuryFileSchema` uses `.strict()` on all Zod objects, causing Mercury to crash on startup when `mercury.yaml` contains any unrecognized key — a typo, a key from a newer version, or a renamed field. Combined with PM2's default unlimited restarts and Baileys' session-based auth, this creates a cascade: config error → crash loop → WhatsApp session revoked → extended downtime requiring manual QR re-scan.
13
+
14
+ ## Steps to Reproduce
15
+ 1. Add any unrecognized key to `mercury.yaml`, e.g. `dm_auto_space: { admin_numbers: ["123"] }` (renamed to `admin_ids` in 0.4.27)
16
+ 2. Start Mercury (`mercury run` or `mercury service install`)
17
+ 3. Mercury crashes with `Invalid mercury.yaml: dm_auto_space: Unrecognized key: "admin_numbers"`
18
+ 4. PM2 restarts Mercury in a crash loop, each restart fails identically
19
+ 5. Crash loop causes WhatsApp session to expire (reason=401), requiring re-authentication
20
+
21
+ ## Expected Behavior
22
+ Mercury should log a warning about the unknown key and continue starting normally. Invalid *values* (wrong type, out of range) should still crash — the distinction is between "unknown key" (safe to ignore) and "invalid value" (can't run correctly).
23
+
24
+ ## Actual Behavior
25
+ Mercury throws an unhandled error and exits. PM2 restart loop compounds the damage by expiring the WhatsApp session.
26
+
27
+ ## Impact
28
+ - **Service downtime** — entire Mercury instance goes down, no messages processed
29
+ - **WhatsApp session loss** — crash loop causes Baileys auth to expire, requiring manual QR re-scan
30
+ - **Version upgrade risk** — any field rename between versions (like `admin_numbers` → `admin_ids`) crashes deployments that haven't updated their yaml
31
+ - **Typo intolerance** — a single typo in mercury.yaml takes down the whole service
32
+
33
+ ## Root Cause Analysis
34
+
35
+ Two separate concerns are conflated by `.strict()`:
36
+
37
+ 1. **Unknown keys** (typo, version mismatch, renamed field) — should warn, not crash. The config isn't invalid, it's just unrecognized. Even with WABA, crashing because of a typo is wrong.
38
+ 2. **Invalid values** (wrong type, out of range) — should crash. If `port: "abc"`, the service can't run correctly. Fail fast is correct.
39
+
40
+ The Baileys crash-loop cascade is a separate but compounding problem — on the unofficial WhatsApp protocol, rapid reconnect attempts trigger session revocation by Meta. This makes *any* crash loop catastrophic, not just config-related ones.
41
+
42
+ ## Suspected Location
43
+ - `src/config-file.ts` — `mercuryFileSchema` (line 25) and all nested objects use `.strict()`, which rejects unknown keys
44
+ - `src/config-file.ts` — `mergeRawMercuryConfig()` (line 352) calls `safeParse` but throws on failure instead of warning
45
+
46
+ ## Fix Plan
47
+
48
+ ### Phase 1 — Short term (now)
49
+
50
+ **Config resilience:** Remove `.strict()` from `mercuryFileSchema` and all nested objects (Zod default behavior strips unknown keys silently). Add a post-parse diff that logs warnings for any stripped keys so operators notice typos without crashing.
51
+
52
+ **PM2 restart limit:** Configure `max_restarts` and `min_uptime` in the PM2 ecosystem config so a crash loop stops after N attempts instead of running forever. Send an alert when the limit is hit.
53
+
54
+ ### Phase 2 — Medium term
55
+
56
+ **Circuit breaker for container errors:** Container failures (model API errors, malformed responses) should not crash the host process. The host should log the error, reply to the user with a friendly message, and continue.
57
+
58
+ ### Phase 3 — Long term (WABA)
59
+
60
+ With WABA, auth is a permanent API token — crash loops no longer revoke auth. At that point, strict validation on values remains correct (fail fast on truly invalid config), while unknown keys should still warn-not-crash regardless of the WhatsApp transport.
61
+
62
+ ## Notes
63
+ - Industry standard (Kubernetes, OpenCode/SST) is to warn on unknown keys, not crash. Zod's default behavior is `.strip()` (silently remove unknown keys) — Mercury explicitly opted into `.strict()` which is the most aggressive mode.
64
+ - OpenCode hit the same bug and fixed it by switching to passthrough + warnings (github.com/sst/opencode/issues/6145).
65
+ - The Baileys session loss is a known risk of the unofficial protocol — Meta actively discourages bot usage on consumer WhatsApp. WABA eliminates this class of risk entirely.
66
+
67
+ ---
68
+
69
+ ## Post-Mortem
70
+
71
+ ### Investigation
72
+ Read `src/config-file.ts` — `mercuryFileSchema` and all 13 nested Zod object schemas used `.strict()`. The `mergeRawMercuryConfig()` function called `safeParse()` correctly but threw unconditionally on any parse failure, including unknown keys. The Zod `.strict()` mode treats unknown keys the same as type errors — both produce parse failures.
73
+
74
+ ### Root Cause
75
+ `.strict()` on Zod object schemas rejects unknown keys with the same error severity as invalid values. There was no distinction between "unrecognized key" (safe to ignore) and "invalid value" (can't run). A renamed field (`admin_numbers` → `admin_ids`) caused Mercury to crash on startup, PM2's unlimited restarts amplified this into a crash loop, and the rapid reconnection pattern caused WhatsApp/Baileys to revoke the session.
76
+
77
+ ### Fix
78
+ - **`src/config-file.ts`**: Replaced all 13 `.strict()` calls with `.strip()` (Zod default — silently removes unknown keys). Added `KNOWN_TOP_KEYS` and `KNOWN_SECTION_KEYS` static key sets, and a `warnUnknownKeys()` function that runs before parsing to log `[WARN]` messages for any unrecognized keys. Invalid values (wrong types, out-of-range numbers) still throw — only unknown keys are tolerated.
79
+ - **`mergeRawMercuryConfig()`**: Added an optional `log` parameter (defaults to `console.warn`) so the warning function is testable. Called `warnUnknownKeys()` before `safeParse()`.
80
+ - **`tests/config.test.ts`**: Added 4 tests: unknown top-level key warns but doesn't crash, unknown nested key warns, invalid value type still throws, valid config produces no warnings.
81
+
82
+ ### Lessons
83
+ - Separate "unknown key" from "invalid value" in config validation — they have different severity and different correct responses.
84
+ - Any feature that adds config fields must consider version mismatch — a user on version N with a yaml from version N+1 should not crash.
85
+ - On Baileys, any crash loop is catastrophic — PM2 restart limits should be configured.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: business-extensions
3
+ description: Dedicated business extensions that wrap raw capabilities (GWS, etc.) with deterministic domain logic — e.g. a barber-appointments extension
4
+ metadata:
5
+ type: idea
6
+ ---
7
+
8
+ # Business Extensions
9
+
10
+ **Status**: Idea
11
+ **Created**: 2026-07-01
12
+
13
+ ## Problem
14
+
15
+ Raw capability extensions (GWS Calendar, Gmail, web search) expose generic APIs. Business workflows (appointment booking, customer management) require deterministic logic — availability checks, double-booking prevention, confirmation flows — that can't be reliably delegated to the LLM via prompting alone.
16
+
17
+ Today the only options are: (1) let the LLM orchestrate raw APIs via system prompt instructions (not deterministic enough), or (2) build a full standalone application outside Mercury. Neither is ideal.
18
+
19
+ ## Idea
20
+
21
+ A pattern for **business extensions** that wrap raw capabilities with domain-specific, deterministic logic and expose simplified CLIs to the agent.
22
+
23
+ Example: `barber-appointments` extension
24
+ - Wraps GWS Calendar internally
25
+ - Exposes: `book-appointment`, `list-availability`, `cancel-appointment`
26
+ - Enforces business rules in code: no double-booking, business hours only, minimum slot duration, confirmation required
27
+ - Registers as a single Mercury permission (`barber-appointments`)
28
+ - The LLM calls the simplified CLI; the extension handles the hard logic deterministically
29
+
30
+ This separates concerns:
31
+ - **Mercury** = platform (messaging, spaces, permissions, rate limits)
32
+ - **Capability extensions** = raw API access (GWS, web search, etc.) — admin-only or building blocks
33
+ - **Business extensions** = deterministic domain logic wrapping capabilities — customer-facing
34
+
35
+ ## Why not just prompt engineering?
36
+
37
+ LLMs are non-deterministic. For a barber shop, a double-booked appointment or a cancelled-without-confirmation is a real-world problem. Business rules must be enforced in code, not hoped for via prompts.
38
+
39
+ ## Open questions
40
+
41
+ - Should business extensions be a separate category in the extension system, or just a convention?
42
+ - How do business extensions declare their dependency on capability extensions (e.g., `barber-appointments` needs `gws`)?
43
+ - Should there be a template/scaffold for creating business extensions?
44
+ - How does this relate to "profiles" — is a profile = business extension + config preset?
45
+
46
+ ## First candidate
47
+
48
+ `barber-appointments` — Calendar-based appointment booking for a barber shop. Quick win for first customer impression. Gmail integration deferred.
@@ -0,0 +1,15 @@
1
+ ---
2
+ slug: applicative-profiles
3
+ shipped: 2026-07-01
4
+ ---
5
+
6
+ ## ROADMAP changes
7
+
8
+ **Remove from:** Next (table row where Item = applicative-profiles)
9
+
10
+ **Also pull:** none
11
+
12
+ **Add to Shipped table** (insert at top, after header row):
13
+ | **applicative-profiles** | 2026-07-01 | Generic profile layer: manifest schema, exhaustive member scoping, host-side capability broker |
14
+
15
+ **Milestone Map:** none
@@ -0,0 +1,15 @@
1
+ ---
2
+ slug: caller-bound-capability-token
3
+ shipped: 2026-07-01
4
+ ---
5
+
6
+ ## ROADMAP changes
7
+
8
+ **Remove from:** Next (table row where Item = caller-bound-capability-token)
9
+
10
+ **Also pull:** none
11
+
12
+ **Add to Shipped table** (insert at top, after header row):
13
+ | **caller-bound-capability-token** | 2026-07-01 | Per-turn unspoofable caller token for host API auth; also hardens TradeStation |
14
+
15
+ **Milestone Map:** none
@@ -0,0 +1,211 @@
1
+ # GWS OAuth Setup Runbook
2
+
3
+ How to connect Google Calendar to a customer's Mercury assistant.
4
+
5
+ ## Prerequisites
6
+
7
+ - Access to the Tagula GCP project (console.cloud.google.com, project "Tagula")
8
+ - SSH access to the customer's VPS
9
+ - The customer's `MERCURY_API_SECRET` (from their `.env`)
10
+ - The customer's Google account email
11
+
12
+ ## Option A: Auth Gateway (preferred — works on mobile + desktop)
13
+
14
+ Use this when `auth.tagula.ai` is set up. Customer just clicks a link.
15
+
16
+ ### One-time gateway setup
17
+
18
+ > Skip if the gateway is already running at `auth.tagula.ai`.
19
+
20
+ 1. **DNS**: Add an A record for `auth.tagula.ai` → gateway server IP (GoDaddy DNS panel)
21
+
22
+ 2. **Server**: SSH into the gateway server and install nginx + certbot:
23
+ ```bash
24
+ apt-get update && apt-get install -y nginx certbot python3-certbot-nginx
25
+ ```
26
+
27
+ 3. **Nginx config** (`/etc/nginx/sites-available/auth-tagula`):
28
+ ```nginx
29
+ server {
30
+ listen 80;
31
+ server_name auth.tagula.ai;
32
+ location / {
33
+ proxy_pass http://127.0.0.1:3456;
34
+ proxy_set_header Host $host;
35
+ proxy_set_header X-Forwarded-For $remote_addr;
36
+ }
37
+ }
38
+ ```
39
+ ```bash
40
+ ln -s /etc/nginx/sites-available/auth-tagula /etc/nginx/sites-enabled/
41
+ nginx -t && systemctl reload nginx
42
+ ```
43
+
44
+ 4. **SSL**:
45
+ ```bash
46
+ certbot --nginx -d auth.tagula.ai
47
+ ```
48
+
49
+ 5. **Gateway config**: Copy `gateway-config.example.json` → `gateway-config.json`, fill in:
50
+ - `google.clientId` and `google.clientSecret` from the Tagula GCP project
51
+ - `google.redirectUri` = `https://auth.tagula.ai/oauth/callback`
52
+
53
+ 6. **GCP redirect URI**: In GCP Console → APIs & Services → Credentials → Web client → add `https://auth.tagula.ai/oauth/callback` to Authorized redirect URIs
54
+
55
+ 7. **Start gateway**:
56
+ ```bash
57
+ npm install -g pm2 # if not installed
58
+ pm2 start auth-gateway.js --name auth-gateway
59
+ pm2 save
60
+ ```
61
+
62
+ ### Per-customer setup
63
+
64
+ 1. **Add customer to gateway config** (`gateway-config.json`):
65
+ ```json
66
+ "assistants": {
67
+ "customer-name": {
68
+ "apiUrl": "http://<VPS_IP>:8787",
69
+ "apiSecret": "<MERCURY_API_SECRET from customer's .env>",
70
+ "envVar": "MERCURY_GWS_CREDENTIALS_JSON"
71
+ }
72
+ }
73
+ ```
74
+ Restart gateway: `pm2 restart auth-gateway`
75
+
76
+ 2. **Add test user in GCP**: GCP Console → Google Auth Platform → Audience → Add users → add customer's Gmail
77
+
78
+ 3. **Install gws extension on customer VPS** (if not already):
79
+ ```bash
80
+ ssh root@<VPS_IP>
81
+ # Via dashboard: go to http://<VPS_IP>:8787/dashboard/page/features → Install gws
82
+ # Or via API:
83
+ curl -X POST http://localhost:8787/api/console/extensions/install \
84
+ -H "Authorization: Bearer <API_SECRET>" \
85
+ -H "Content-Type: application/json" \
86
+ -d '{"catalogName": "gws"}'
87
+ ```
88
+
89
+ 4. **Send the link to customer**:
90
+ ```
91
+ https://auth.tagula.ai/connect/gws?assistant=customer-name
92
+ ```
93
+ Customer clicks → signs in with Google → sees "Connected!" → done.
94
+
95
+ 5. **Verify**: Check Mercury logs on the VPS:
96
+ ```bash
97
+ pm2 logs 'mercury run' --lines 20 --nostream | grep -i gws
98
+ ```
99
+ Should show: `Loaded extension: gws` and `Installed skill: gws`
100
+
101
+ ---
102
+
103
+ ## Option B: Manual (ngrok — no gateway needed)
104
+
105
+ Use this before the gateway is set up, or as a one-off.
106
+
107
+ ### Setup
108
+
109
+ 1. **Install ngrok on customer VPS**:
110
+ ```bash
111
+ ssh root@<VPS_IP>
112
+ curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc \
113
+ | tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
114
+ echo 'deb https://ngrok-agent.s3.amazonaws.com buster main' \
115
+ | tee /etc/apt/sources.list.d/ngrok.list
116
+ apt-get update && apt-get install -y ngrok
117
+ ngrok config add-authtoken <YOUR_NGROK_TOKEN>
118
+ ```
119
+
120
+ 2. **Write callback script** (`/tmp/oauth-callback.js`):
121
+ ```bash
122
+ # Copy from tagula-agents-cloud-console/oauth-callback-standalone.js
123
+ # Update CLIENT_ID and CLIENT_SECRET from GCP
124
+ scp oauth-callback-standalone.js root@<VPS_IP>:/tmp/oauth-callback.js
125
+ ```
126
+
127
+ 3. **Start callback + ngrok**:
128
+ ```bash
129
+ ssh root@<VPS_IP>
130
+ nohup node /tmp/oauth-callback.js > /tmp/oauth-callback.log 2>&1 &
131
+ ngrok http 9999
132
+ ```
133
+ Note the ngrok URL (e.g., `https://abc123.ngrok-free.dev`)
134
+
135
+ 4. **Configure redirect URI**:
136
+ - Set redirect URI in the callback script:
137
+ ```bash
138
+ curl "http://localhost:9999/set-redirect?uri=https://<NGROK_URL>/oauth/callback"
139
+ ```
140
+ - Add same URL to GCP: APIs & Services → Credentials → Web client → Authorized redirect URIs
141
+
142
+ 5. **Add test user**: GCP → Google Auth Platform → Audience → Add customer's Gmail
143
+
144
+ 6. **Send OAuth link to customer**:
145
+ ```
146
+ https://accounts.google.com/o/oauth2/v2/auth?client_id=<CLIENT_ID>&redirect_uri=https://<NGROK_URL>/oauth/callback&response_type=code&scope=https://www.googleapis.com/auth/calendar.events&access_type=offline&prompt=consent
147
+ ```
148
+
149
+ 7. **After customer authorizes**:
150
+ - ngrok free tier shows an interstitial — customer clicks "Visit Site"
151
+ - Check logs: `cat /tmp/oauth-callback.log`
152
+ - Should show `SUCCESS - credentials saved`
153
+
154
+ 8. **Move credentials to correct .env** (the callback script writes to `.mercury/.env` but Mercury reads from project root `.env`):
155
+ ```bash
156
+ grep MERCURY_GWS /opt/path/to/assistant/.mercury/.env >> /opt/path/to/assistant/.env
157
+ sed -i '/MERCURY_GWS/d' /opt/path/to/assistant/.mercury/.env
158
+ ```
159
+
160
+ 9. **Restart Mercury and clean up**:
161
+ ```bash
162
+ pm2 restart 'mercury run'
163
+ # Kill callback server and ngrok
164
+ kill $(lsof -ti:9999) 2>/dev/null
165
+ pkill ngrok
166
+ rm /tmp/oauth-callback.js /tmp/oauth-callback.log
167
+ ```
168
+
169
+ 10. **Verify**:
170
+ ```bash
171
+ pm2 logs 'mercury run' --lines 20 --nostream | grep -i gws
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Option C: Copy-paste URL (desktop only)
177
+
178
+ Simplest, no infrastructure needed. Customer must be on a computer (not mobile).
179
+
180
+ 1. **Create Desktop-type OAuth client** in GCP (not Web) — allows `http://localhost` redirect
181
+ 2. **Generate OAuth URL** with `redirect_uri=http://localhost:1`
182
+ 3. **Send link to customer** — they click, authorize, Google redirects to localhost (fails)
183
+ 4. **Customer copies the full URL** from their browser bar and sends it back
184
+ 5. **Extract code and exchange**:
185
+ ```bash
186
+ # Extract code from the URL the customer sent
187
+ CODE="the_code_from_url"
188
+ curl -s -X POST https://oauth2.googleapis.com/token \
189
+ -d "code=$CODE&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>&redirect_uri=http://localhost:1&grant_type=authorization_code"
190
+ ```
191
+ 6. **Set env var and restart** — copy the tokens JSON into `.env` as `MERCURY_GWS_CREDENTIALS_JSON='...'`
192
+
193
+ ---
194
+
195
+ ## Troubleshooting
196
+
197
+ | Symptom | Cause | Fix |
198
+ |---------|-------|-----|
199
+ | `invalid_client` on Google consent | Client not propagated yet, or created in new Auth Platform UI | Wait 10 min, or recreate from legacy APIs & Services → Credentials |
200
+ | Extension installs but doesn't load | `MERCURY_GWS_CREDENTIALS_JSON` not set or in wrong `.env` | Check `grep MERCURY_GWS .env` in project root |
201
+ | `Removed stale skill: gws` in logs | Extension skipped by credential gate | Same as above — credential missing |
202
+ | `Failed to build derived image` | `docker-buildx` not installed | `apt-get install docker-buildx` |
203
+ | Customer's wrong Google account used | Browser auto-selects default account | Add all their accounts as test users, or use incognito |
204
+ | ngrok "Visit Site" interstitial | Free tier warning | Customer clicks "Visit Site" — shows once |
205
+ | `refresh_token_expires_in: 604799` | Token expires in 7 days (testing mode) | Publish the OAuth app in GCP to get non-expiring refresh tokens |
206
+
207
+ ## Important notes
208
+
209
+ - **Test mode tokens expire in 7 days.** While the GCP app is in "Testing" status, refresh tokens expire after 7 days. To get permanent tokens, publish the app (requires Google verification for sensitive scopes). For now, re-run the OAuth flow when tokens expire.
210
+ - **docker-buildx** is required on Linux servers for Mercury to build derived images with extension CLIs. Install with `apt-get install docker-buildx`.
211
+ - **Rotate credentials** after sharing them in chat. Go to GCP → Credentials → Web client → rotate client secret. Update `gateway-config.json` and any callback scripts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mercury-agent",
3
- "version": "0.4.27",
3
+ "version": "0.5.0",
4
4
  "description": "Personal AI assistant for chat platforms (WhatsApp, Slack, Discord, Telegram)",
5
5
  "license": "MIT",
6
6
  "author": "Avishai Tsabari",