Dynamic system prompt injection plugin for OpenClaw. Routes agent context through configurable routers, matches against registered rules, and injects prompt files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.1 KiB
6.1 KiB
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
// Every router exports a resolve function
export interface RouterContext {
agentId: string;
}
export function resolve(ctx: RouterContext): string | Promise<string>;
Example routers:
// routers/agent-id.ts — zero dependencies
export function resolve(ctx: { agentId: string }): string {
return ctx.agentId;
}
// 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:
{
"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 <name> --key <key> --file <path> |
Register a rule |
remove --router <name> --key <key> |
Remove a rule |
list |
List all rules |
test --agent <agent-id> |
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
{
"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
// 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/listtoolprompt-rules testtool (preview matches for an agent)prompt-rules reload-routerstool
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
- No hard dependencies — Plugin works with just agent-id router. External system routers are optional.
- Drop-in extensibility — Add a .ts file to routers/ to add a new routing dimension.
- Hot reload — Router changes don't require gateway restart.
- Separation of concerns — Routers resolve context → Rules map keys to files → Plugin injects content.