You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: advanced/browser-automation.md
+86-1Lines changed: 86 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -199,6 +199,91 @@ The snapshot returns an accessibility tree. Use `interactive: true` to see only
199
199
200
200
---
201
201
202
+
## Selected Cookie Sync
203
+
204
+
Server-side browser sessions start with no login state. **Selected cookie sync** lets a user pick specific cookies from a site they are logged into and copy them into GoClaw, so an agent's browser can act as that signed-in session — without sharing a password.
205
+
206
+
A small Chrome extension (`chrome-selected-cookie-sync`) does the picking. There is **no automatic background sync**: the user opens the extension on the active tab, checks the exact cookies to share, and clicks **Sync**. GoClaw stores the values encrypted and replays them into the agent's browser only for matching domains and paths.
207
+
208
+
```mermaid
209
+
flowchart LR
210
+
USER["User on logged-in site"] --> EXT["chrome-selected-cookie-sync\nextension"]
| `DELETE` | `/v1/browser/cookies?agent_id=&domain=&name=&path=` | Revoke synced cookies |
225
+
226
+
The client only ever chooses `agent_id`. **Tenant and user are derived from the auth context**, not from the request body — a client cannot spoof another user's cookies. Sync is rejected when the auth context has no user, or when no `agent_id` is supplied.
227
+
228
+
**Sync request body:**
229
+
230
+
```json
231
+
{
232
+
"agent_id": "default",
233
+
"source": "chrome-selected-cookie-sync",
234
+
"cookies": [
235
+
{
236
+
"domain": "example.com",
237
+
"name": "session",
238
+
"path": "/",
239
+
"value": "REDACTED",
240
+
"secure": true,
241
+
"httpOnly": true,
242
+
"sameSite": "lax",
243
+
"expirationDate": 1789999999
244
+
}
245
+
]
246
+
}
247
+
```
248
+
249
+
**Response:** `{ "synced": 1 }`. Limits: max 200 cookies per request, 16 KB per cookie value, 1 MB total body.
250
+
251
+
The `GET` response returns metadata only — `domain`, `name`, `path`, `secure`, `httpOnly`, `sameSite`, `expiresAt`, `source`, `updatedAt`. Cookie **values are never returned**.
252
+
253
+
### Scope and uniqueness
254
+
255
+
Each stored cookie is keyed by `(tenant_id, user_id, agent_id, domain, path, name)`. Re-syncing the same cookie updates the existing row (upsert). This scope is what keeps one user's cookies from leaking into another user's or another agent's browser session.
256
+
257
+
### Security
258
+
259
+
- **Encrypted at rest**: cookie values are encrypted with AES-256-GCM before being written to the `browser_cookies` table. Requires the `GOCLAW_ENCRYPTION_KEY` environment variable — **sync and list fail closed (HTTP 503) when it is unset**, so cookies are never persisted in plaintext.
260
+
- **Write-only values**: the list endpoint and audit logs return metadata only. Cookie values never appear in API responses or logs.
261
+
- **Scoped replay**: the agent browser receives a cookie only when the requested URL's host and path match the stored cookie's domain/path, the cookie has not expired, and the tenant/user/agent scope matches.
262
+
- **Explicit selection**: the extension reads cookies only after the user grants host permission for the active site, and sends only the cookies the user checked.
263
+
- **Revocation**: delete from the extension or call `DELETE /v1/browser/cookies?agent_id=<agent>&domain=<domain>` to remove synced cookies. Omitting `domain` removes all cookies for that agent.
264
+
265
+
### How the agent consumes synced cookies
266
+
267
+
When the agent's browser navigates to an `http(s)` URL, GoClaw's cookie provider looks up cookies for the current browser scope (`tenant_id` / `user_id` / `agent_id`), decrypts them, and injects only those whose domain and path match the target URL (and that have not expired). Non-HTTP schemes get no cookies. The agent never sees raw values — they are applied directly to the Chrome session via CDP.
268
+
269
+
### Install the extension
270
+
271
+
The extension lives in the GoClaw repo at `extensions/chrome-selected-cookie-sync/`.
272
+
273
+
1. Open `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the `extensions/chrome-selected-cookie-sync/` folder.
274
+
2. Open a tab on the site you are logged into, then click the extension icon.
275
+
3. Fill in the popup:
276
+
- **Gateway URL** — e.g. `http://localhost:18790`
277
+
- **Token** — an operator token (sent as `Authorization: Bearer <token>`)
278
+
- **User ID** — sent as the `X-GoClaw-User-Id` header
279
+
- **Agent ID** — e.g. `default`
280
+
4. Click **Grant access** to give the extension host permission for the current site, then **Refresh** to list the site's cookies.
281
+
5. Check the cookies you want to share (or **Select all**), then click **Sync**. The popup confirms `Synced N cookies.`
282
+
283
+
Settings are saved in `chrome.storage.local`. The extension requests gateway-origin permission before sending, and asks for active-tab host permission before reading cookies.
284
+
285
+
---
286
+
202
287
## Security Considerations
203
288
204
289
- **SSRF protection**: GoClaw applies SSRF filtering to tool inputs — agents cannot be trivially directed to internal network addresses.
@@ -256,4 +341,4 @@ Returns:
256
341
-[Exec Approval](/exec-approval) — require human sign-off before running commands
Copy file name to clipboardExpand all lines: advanced/cli-credentials.md
+122-1Lines changed: 122 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -167,6 +167,7 @@ A `400` response on create/update includes the rejected key names in `rejected_k
167
167
"error": "env keys denied: LD_PRELOAD, PATH",
168
168
"rejected_keys": "LD_PRELOAD,PATH"
169
169
}
170
+
```
170
171
171
172
## REST API
172
173
@@ -246,6 +247,126 @@ POST /v1/cli-credentials/{id}/agent-grants/{grantId}/env:reveal
246
247
247
248
Returns the decrypted plaintext env vars. Rate-limited to 10 calls/minute per user. See [Revealing Decrypted Env Vars](#revealing-decrypted-env-vars) for full details.
248
249
250
+
## Typed Credential Adapters
251
+
252
+
The sections above describe the **legacy env-paste model** — you paste arbitrary environment variables and GoClaw injects them verbatim into the child process. That works for tools that read auth from a single stable env var (`GH_TOKEN`, `AWS_ACCESS_KEY_ID`, …), but it fails for tools like `git` that read credentials from config files, credential helpers, or per-remote URLs — pasting a PAT into `GIT_TOKEN` does nothing.
253
+
254
+
**Typed credential adapters** solve this. Instead of pasting raw env vars, you choose a credential *type*, and GoClaw routes the credential through a server-side adapter that knows how to inject it correctly and securely for that specific tool.
255
+
256
+
### Credential types
257
+
258
+
A user credential row carries a `credential_type` (migration `000073`):
259
+
260
+
|`credential_type`| Meaning |
261
+
|-------------------|---------|
262
+
|`NULL` / `env`| Legacy env passthrough — env vars injected verbatim, exactly as before. No host scoping. |
263
+
|`pat`| Personal Access Token, for HTTPS git remotes (GitHub/GitLab/Gitea). Requires a `host_scope`. |
264
+
|`ssh_key`| SSH private key (PEM), for git over SSH. Requires a `host_scope`. |
265
+
266
+
`NULL`/`env` rows are never migrated — existing legacy credentials keep working unchanged. Typed adapters are opt-in per credential.
267
+
268
+
### User credentials vs binary/system credentials
269
+
270
+
Typed adapters operate on **user credentials**, not the binary-level env defaults:
271
+
272
+
-**Binary/system credentials** — the binary definition + its default env vars (and agent-grant overrides) described above. Shared across the binary's grants.
273
+
-**User credentials** — per-user typed secrets stored in `secure_cli_user_credentials`, scoped to a single hostname.
274
+
275
+
Manage user credentials in the dashboard under **Settings → CLI Credentials → User Credentials**. Click **Add**, select the user, choose the credential type (`Personal Access Token` or `SSH Private Key`), enter the **Host Scope**, and paste the secret. The stored secret is AES-256-GCM encrypted and can never be read back — editing the row shows a `••••••••` placeholder; leaving the secret field blank preserves the stored value, typing a new value replaces it.
276
+
277
+
### The git adapter
278
+
279
+
The `git` adapter is the first shipped typed adapter. It injects credentials **only** for network subcommands:
280
+
281
+
```
282
+
clone fetch pull push submodule
283
+
```
284
+
285
+
Any other subcommand (`status`, `log`, `diff`, `commit`, `branch`, …) is a local operation and runs **uncredentialed** — no injection, no audit-log line.
286
+
287
+
**PAT flow.** The token is injected through environment variables, never on `argv`:
288
+
289
+
```
290
+
GIT_CONFIG_COUNT=1
291
+
GIT_CONFIG_KEY_0=http.https://<host>/.extraheader
292
+
GIT_CONFIG_VALUE_0=Authorization: Bearer <token>
293
+
```
294
+
295
+
Because the token lives in an env value (not a command-line flag), it never appears in `ps`, `/proc/<pid>/cmdline`, or shell history. The injected vars are scoped to the spawned `git` process only — GoClaw's own environment and sibling exec calls never see them.
296
+
297
+
**SSH flow.** The PEM key is written to a `0600`-mode tmpfile in the system temp dir (prefix `goclaw-gitkey-*`), and `GIT_SSH_COMMAND` is set to:
`StrictHostKeyChecking=accept-new` accepts unknown host keys on **first contact (TOFU)**. Pre-seed `~/.ssh/known_hosts` to close the window (see [Security Hardening](/deploy-security)). The tmpfile is removed after exec via a deferred cleanup. **Passphrase-protected SSH keys are rejected at save time** — re-export your key without a passphrase, or use a dedicated deploy key.
304
+
305
+
### Host scope
306
+
307
+
Both `pat` and `ssh_key` require a **`host_scope`** — the exact ASCII `host` or `host:port` the credential is valid for. It is normalized to lowercase ASCII (via `idna.ToASCII`) and matched **exactly**. v1 has **no wildcards**, and the port is part of the key:
If you run a self-hosted server on the scheme's default port (443 HTTPS, 22 SSH), omit the port; if on a non-default port, include it (e.g. `gitea.internal:8443`). When no stored credential matches the resolved remote host, the adapter falls through to the uncredentialed path and the remote rejects the operation if it requires auth.
314
+
315
+
### Env visibility: sensitive vs non-sensitive
316
+
317
+
Stored env entries now carry a `kind`. When the dashboard or admin API reads a credential back, the response masks values according to kind:
|`value` (explicitly non-sensitive, e.g. a region or profile name) | plain value returned, `masked: false`|
323
+
324
+
This lets operators see non-secret context (e.g. `AWS_DEFAULT_REGION=us-west-2`) in the UI while secrets stay masked. Secrets are still never returned except via the dedicated `env:reveal` endpoint.
325
+
326
+
### Migrating from legacy env credentials
327
+
328
+
There is no forced migration. A row with `credential_type IS NULL` or `= 'env'` keeps emitting its env vars exactly as before. To upgrade a git credential, open the user-credentials dialog, pick **Personal Access Token** or **SSH Private Key**, enter the host scope, paste the secret, and save — the legacy row is replaced atomically.
329
+
330
+
### v1 limitations
331
+
332
+
-**No wildcard hosts** — one credential per exact `host[:port]`; `*.github.com` is not supported.
333
+
-**No passphrase-protected SSH keys** — rejected at validation time.
334
+
-**No sandbox propagation** — the adapter mutates the forked child's environment, which is incompatible with the bind-mount Docker sandbox path. Credentialed exec runs on the host only in v1.
GoClaw ships a `gws` preset for the Google Workspace CLI (`@googleworkspace/cli`).
340
+
341
+
**Availability.** The `gws` binary is preinstalled **only in the published `full` Docker image**. On `latest`/`base` images, install `@googleworkspace/cli` from the Packages page (requires a Node-enabled build, `ENABLE_NODE=true`; Node.js 18+).
342
+
343
+
**Credentials.** Create a SecureCLI credential from the `gws` preset and provide at least one auth source:
344
+
345
+
| Env var | Purpose |
346
+
|---------|---------|
347
+
|`GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE`| Path to exported `gws` credentials or an OAuth credentials JSON file |
348
+
|`GOOGLE_WORKSPACE_CLI_TOKEN`| Pre-obtained Google OAuth access token (optional) |
349
+
|`GOOGLE_WORKSPACE_CLI_CLIENT_ID`| OAuth client ID for manual auth flows (optional) |
350
+
|`GOOGLE_WORKSPACE_CLI_CLIENT_SECRET`| OAuth client secret for manual auth flows (optional) |
351
+
352
+
**Blocked commands.** The preset blocks interactive and credential-exporting auth flows:
Run those flows outside agent execution, then store the resulting token or credentials-file path in SecureCLI.
359
+
360
+
**Usage.** Default usage is read-oriented. Use `--params` for query parameters, `--json` for request bodies, and `--page-all` for paginated reads:
361
+
362
+
```sh
363
+
gws drive files list --params '{"pageSize": 10}'
364
+
gws gmail users messages list --params '{"userId": "me", "maxResults": 10}'
365
+
gws calendar events list --params '{"calendarId": "primary", "maxResults": 10}'
366
+
```
367
+
368
+
> **Write caution.** Write commands can modify Workspace data. Keep the default preset read-oriented and create a separate, reviewed SecureCLI config for any approved write workflow.
369
+
249
370
## Common Patterns
250
371
251
372
### Allow only one agent to use a sensitive CLI tool
@@ -277,4 +398,4 @@ Update the grant: `{"enabled": false}`. The binary remains accessible to other a
The check runs once per request, before any LLM calls. Sub-agent delegations run under their own agent records with their own budgets.
234
234
235
+
> The agent's `budget_monthly_cents` is also mirrored into the [AI Budget Usage Caps](/usage-quota) system as a managed `month`-window cost-cap policy, so the same limit is enforced there too.
236
+
237
+
---
238
+
239
+
## Model Pricing (Catalog & Overrides)
240
+
241
+
The static `telemetry.model_pricing` map above is the simplest way to price models. For dynamic, database-backed pricing — used by the [AI Budget Usage Caps](/usage-quota) cost caps — GoClaw also keeps a **pricing catalog** and per-tenant **overrides** in PostgreSQL (migration `000070`).
242
+
243
+
### OpenRouter catalog sync
244
+
245
+
GoClaw can pull a full model price list from [OpenRouter](https://openrouter.ai) and store it in the `usage_pricing_catalog` table. Each entry captures per-unit prices for input, output, cache read/write, reasoning, request, image, and web-search units, stored as high-precision decimals.
246
+
247
+
```bash
248
+
curl -X POST -H "Authorization: Bearer your-token" \
This endpoint requires **master scope** (it updates the shared global catalog). The catalog is upserted by `model_id`, so re-running the sync refreshes prices in place.
253
+
254
+
### Per-model price overrides
255
+
256
+
When you want different prices for a specific tenant + provider + model (e.g. a negotiated rate, or a model OpenRouter doesn't list), set an **override** in the `usage_pricing_overrides` table. Overrides are scoped to one tenant and take priority over the global catalog.
257
+
258
+
```bash
259
+
# List the synced catalog (optionally filter by model)
|`POST /v1/model-pricing/sync-openrouter`| master | Refresh the global price catalog from OpenRouter |
284
+
|`GET /v1/model-pricing`| admin | List the synced catalog (`?model=`, `?limit=`) |
285
+
|`PUT /v1/model-pricing/overrides`| tenant-admin | Create or update a per-tenant model price override |
286
+
|`GET /v1/model-pricing/overrides`| admin | List overrides (`?provider_id=`) |
287
+
|`DELETE /v1/model-pricing/overrides/{id}`| tenant-admin | Remove an override |
288
+
289
+
### How pricing resolves
290
+
291
+
When a cost cap needs a price for a model, GoClaw resolves in this order:
292
+
293
+
1.**Tenant override** for the matching provider + model (highest priority)
294
+
2.**Global OpenRouter catalog** entry by `model_id` / canonical id
295
+
296
+
If neither is found, a cost-capped call is blocked with reason `pricing_unknown`. Prices are stored as decimal USD **per token/unit** and converted to micro-dollars when caps compute cost.
297
+
235
298
---
236
299
237
300
## Common Issues
@@ -252,4 +315,4 @@ The check runs once per request, before any LLM calls. Sub-agent delegations run
252
315
-[Observability](/deploy-observability) — OpenTelemetry export for spans including cost fields
253
316
-[Configuration Reference](/config-reference) — full `telemetry` config options
Copy file name to clipboardExpand all lines: advanced/custom-tools.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -67,6 +67,8 @@ curl -X POST http://localhost:8080/v1/tools/custom \
67
67
|`agent_id`| UUID | null | Scope to one agent; omit for global |
68
68
|`enabled`| bool | true | Disable without deleting |
69
69
70
+
> **Three distinct timeouts — don't confuse them.** This per-custom-tool `timeout_seconds` (default 60) applies only to this one custom tool's command. It is separate from the host `exec` builtin tool's own `timeout_seconds` setting (also default 60, max 3600 — see [Tools Overview → Execution Timeout](/tools-overview)) and from the sandbox `sandbox_config.timeout_sec` (agent-level, default 300 — see [Sandbox](/sandbox)). A custom tool routed through the sandbox is still bounded by the sandbox timeout.
71
+
70
72
### Command templates
71
73
72
74
Use `{{.paramName}}` placeholders. GoClaw replaces them with shell-escaped values using simple string replacement — not Go's `text/template` engine, so template functions and pipelines are not supported. Every substituted value is single-quoted with embedded single-quotes escaped, so even a malicious LLM cannot break out of the argument.
@@ -250,4 +252,4 @@ Returns all documents that link to the specified path. Respects team boundaries
250
252
-[Exec Approval](/exec-approval) — require human approval before commands run
251
253
-[Sandbox](/sandbox) — run commands inside Docker for extra isolation
0 commit comments