Files
PrismFacet/PLAN.md
zhi 3a36605238 init: PrismFacet project plan
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>
2026-04-18 11:02:57 +00:00

218 lines
6.1 KiB
Markdown

# 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<string>;
```
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 <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
```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.