# PrismFacet Dynamic system prompt injection plugin for OpenClaw. Routes agent context through configurable routers, matches against registered rules, and injects corresponding prompt files into the system prompt. ## Concept ``` Agent session starts → before_prompt_build(ctx) → for each router: router.resolve(ctx) → key (e.g. "developer") lookup rules for router:key (e.g. "role:developer") read prompt file (e.g. roles/developer/ROLE.md) → append all matched prompts to system prompt ``` **Routers** resolve context into a key. They are TypeScript files — drop one into `routers/` to register it. **Rules** map `router:key` → prompt file path. Managed via MCP tool at runtime. ## Architecture ``` ~/.openclaw/plugins/prism-facet/ ├── openclaw.plugin.json # Plugin manifest ├── package.json ├── index.ts # Entry point ├── src/ │ ├── router-loader.ts # Load/reload routers from routers/ │ ├── rule-store.ts # CRUD for rules (rules.json) │ └── prompt-injector.ts # before_prompt_build hook logic ├── routers/ # Router functions (drop-in) │ ├── role.ts # Resolves agent role (reads ego.json) │ ├── position.ts # Resolves agent position │ └── agent-id.ts # Returns agentId directly (no deps) └── rules.json # Persisted rules (managed via tool) ``` ## Router Interface ```typescript // Every router exports a resolve function export interface RouterContext { agentId: string; } export function resolve(ctx: RouterContext): string | Promise; ``` Example routers: ```typescript // routers/agent-id.ts — zero dependencies export function resolve(ctx: { agentId: string }): string { return ctx.agentId; } ``` ```typescript // routers/role.ts — reads ego.json (optional dependency on PaddedCell) import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; const EGO_PATH = `${homedir()}/.openclaw/ego.json`; export function resolve(ctx: { agentId: string }): string { try { const ego = JSON.parse(readFileSync(EGO_PATH, 'utf8')); return ego['agent-scope']?.[ctx.agentId]?.role || ''; } catch { return ''; } } ``` ### Hot Reload Routers are loaded via dynamic `import()` with cache-busting. A reload tool clears the module cache and re-imports all routers without restarting the gateway. ## Rules Rules are stored in `rules.json`: ```json { "role:developer": "/path/to/roles/developer/ROLE.md", "role:operator": "/path/to/roles/operator/ROLE.md", "position:tech-leader": "/path/to/positions/tech-leader/POSITION.md", "agent-id:operator": "/path/to/special/operator-extra.md" } ``` Key format: `{router-name}:{resolved-key}` ## MCP Tools ### prompt-rules | Subcommand | Description | |------------|-------------| | `add --router --key --file ` | Register a rule | | `remove --router --key ` | Remove a rule | | `list` | List all rules | | `test --agent ` | Preview which rules match and what prompts would be injected | | `reload-routers` | Hot-reload all router functions | ## Runtime Flow ``` 1. Gateway starts → plugin loads 2. Load all routers from routers/ via dynamic import() 3. Load rules.json On each before_prompt_build(event, ctx): 4. For each loaded router: a. key = router.resolve(ctx) b. If key is empty → skip c. ruleKey = "{routerName}:{key}" d. If rules[ruleKey] exists → read the prompt file 5. Concatenate all matched prompt contents 6. Return { appendSystemContext: concatenated } On reload-routers tool call: 7. Clear import cache for all routers 8. Re-import all routers from routers/ ``` ## Plugin Manifest ```json { "id": "prism-facet", "name": "PrismFacet", "version": "0.1.0", "description": "Dynamic system prompt injection via routers and rules", "main": "dist/index.js", "configSchema": { "type": "object", "properties": { "routersDir": { "type": "string", "description": "Directory containing router .ts/.js files" }, "rulesFile": { "type": "string", "description": "Path to rules.json" } } } } ``` ## Configuration ```json // openclaw.json { "plugins": { "entries": { "prism-facet": { "enabled": true, "hooks": { "allowPromptInjection": true }, "config": { "routersDir": "~/.openclaw/plugins/prism-facet/routers", "rulesFile": "~/.openclaw/plugins/prism-facet/rules.json" } } } } } ``` ## Implementation Phases ### Phase 1: Core Plugin - [ ] Plugin scaffold (manifest, package.json, tsconfig) - [ ] Router loader with dynamic import + cache busting - [ ] Rule store (CRUD on rules.json) - [ ] before_prompt_build hook: resolve routers → match rules → inject prompts - [ ] Built-in routers: agent-id.ts ### Phase 2: MCP Tools - [ ] `prompt-rules add/remove/list` tool - [ ] `prompt-rules test` tool (preview matches for an agent) - [ ] `prompt-rules reload-routers` tool ### Phase 3: Default Routers - [ ] role.ts (reads ego.json) - [ ] position.ts (reads ego.json) - [ ] Documentation for writing custom routers ### Phase 4: Testing & Deployment - [ ] Test on laptop environment - [ ] Deploy to T2 (production) - [ ] Integrate with ClawSkills role/position definitions - [ ] Verify injection for all active agents ## Dependencies | Dependency | Required? | Notes | |-----------|-----------|-------| | OpenClaw v2026.4.9+ | Yes | before_prompt_build + allowPromptInjection | | PaddedCell / ego-mgr | No | Only needed by role.ts / position.ts routers | | Node.js ESM | Yes | Dynamic import() for router loading | ## Design Principles 1. **No hard dependencies** — Plugin works with just agent-id router. External system routers are optional. 2. **Drop-in extensibility** — Add a .ts file to routers/ to add a new routing dimension. 3. **Hot reload** — Router changes don't require gateway restart. 4. **Separation of concerns** — Routers resolve context → Rules map keys to files → Plugin injects content.