a55951f315
- Replace Python-based sanitizer in deploy script with lightweight jq/alpine - Add sync-agents-sanitized.mjs for on-demand and watch-mode sync - Add AgentConfigPath to appsettings.json (explicit default) - Extend /health/live endpoint to report agent count from sanitized config - Document architecture in docs/agent-identity-architecture.md - No openclaw.json secrets ever reach Nexus API containers Verification: - curl /api/v1/agents → 9 agents, zero secrets in response - agents-sanitized.json contains only 'agents' key, no gateway/auth - All C# code paths read from agents-sanitized.json (AgentConfigPath) - Bridge controller resolves agent IDs via AgentService.GetAllowedAgentIdsAsync()
85 lines
3.3 KiB
Markdown
85 lines
3.3 KiB
Markdown
# Agent Identity Architecture (P4)
|
|
|
|
> Status: ✅ Implemented (2026-07-13)
|
|
> Task: `4291d694-dd40-410d-b0d5-4742d334547a`
|
|
|
|
## Problem
|
|
|
|
Nexus needed agent identity data (id, name, role, sub-agents, model) for the board/bridge
|
|
operations, but reading directly from `/home/node/.openclaw/openclaw.json` would expose
|
|
secrets (gateway password, API keys, auth profiles, channel tokens).
|
|
|
|
## Solution: Sanitized Agent Config File
|
|
|
|
### Architecture
|
|
|
|
```
|
|
openclaw.json (full config, SECRETS)
|
|
│
|
|
├── Deploy-time: jq extract → agents-sanitized.json (NO secrets)
|
|
│ └── deploy-nexus.sh: extracts only {"agents": ...} from openclaw.json
|
|
│
|
|
├── Manual sync: scripts/sync-agents-sanitized.mjs
|
|
│ └── node scripts/sync-agents-sanitized.mjs --once
|
|
│
|
|
└── Nexus API reads: agents-sanitized.json (read-only mount in compose)
|
|
├── AgentService.LoadAgentConfigsAsync()
|
|
├── OpenClawGatewayClient.LoadAgentIdsFromConfig()
|
|
└── AgentService.GetAllowedAgentIdsAsync()
|
|
```
|
|
|
|
### Key Design Decisions
|
|
|
|
1. **Single sanitized source**: `agents-sanitized.json` contains ONLY the `agents` key
|
|
(list + defaults) — no `gateway`, `auth`, `channels`, `tools`, `plugins`, etc.
|
|
|
|
2. **Read-only mount**: Compose mounts as `:ro` — no write access from the API container
|
|
|
|
3. **No ACL dependency**: No uid-1654 ACL needed; the sanitized file is root-owned and
|
|
world-readable
|
|
|
|
4. **Graceful fallback**: If the sanitized file is missing, both `AgentService` and
|
|
`OpenClawGatewayClient` fall back to hardcoded agent IDs from `AgentIdentityCatalog`
|
|
|
|
5. **Auto-sync on deploy**: The deploy pipeline (`deploy-nexus.sh`) regenerates the
|
|
sanitized file from `openclaw.json` using `jq` in an alpine container
|
|
|
|
6. **Manual sync available**: `scripts/sync-agents-sanitized.mjs` provides on-demand
|
|
and watch-mode sync
|
|
|
|
### File Layout
|
|
|
|
| File | Location | Purpose |
|
|
|------|----------|---------|
|
|
| `openclaw.json` | `/home/node/.openclaw/openclaw.json` | Full config with secrets (gateway only) |
|
|
| `agents-sanitized.json` | `/home/node/.openclaw/agents-sanitized.json` | Agents-only, no secrets |
|
|
| Compose mount | `compose.yaml` → API container | `agents-sanitized.json:ro` |
|
|
| Deploy sanitizer | `.gitea/scripts/deploy-nexus.sh` | jq extraction on deploy |
|
|
| Sync script | `scripts/sync-agents-sanitized.mjs` | Node.js manual/watch sync |
|
|
| Config path | `backend/appsettings.json` | `AgentConfigPath` key |
|
|
|
|
### Security Guarantees
|
|
|
|
- ✅ No `password`, `token`, `secret`, or `api_key` values in `agents-sanitized.json`
|
|
- ✅ API endpoints (`/api/v1/agents`, `/api/v1/agents/{id}`) return ZERO secrets
|
|
- ✅ Gateway bridge controller (`/api/bridge/*`) uses only agent IDs from sanitized config
|
|
- ✅ No direct `openclaw.json` reads in any C# code path
|
|
- ✅ Agent identity catalog (`AgentIdentityCatalog`) is a hardcoded fallback, not a primary source
|
|
|
|
### Verification
|
|
|
|
```bash
|
|
# Check sanitized file has no secrets
|
|
curl -s http://nexus-api-1:8080/api/v1/agents \
|
|
-H "X-Api-Key: <key>" | grep -i "password\|secret\|token\|apikey"
|
|
# Expected: no output
|
|
|
|
# Verify only "agents" key exists in sanitized file
|
|
python3 -c "
|
|
import json
|
|
with open('agents-sanitized.json') as f:
|
|
data = json.load(f)
|
|
print(list(data.keys())) # Should print ['agents']
|
|
"
|
|
```
|