P4: Agent-Identitäten ohne Secrets — sanitized config feed
- 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()
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* sync-agents-sanitized.mjs
|
||||
*
|
||||
* Keeps agents-sanitized.json in sync with openclaw.json.
|
||||
* Strips all secrets (gateway, channels, auth, tools, plugins, etc.)
|
||||
* and only writes the "agents" key.
|
||||
*
|
||||
* Modes:
|
||||
* --once Run once and exit
|
||||
* --watch Watch openclaw.json and re-generate on changes (default)
|
||||
*
|
||||
* Usage:
|
||||
* node sync-agents-sanitized.mjs --once
|
||||
* node sync-agents-sanitized.mjs --watch
|
||||
*
|
||||
* Paths (defaults, override with OPENCLAW_CONFIG and SANITIZED_OUTPUT env vars):
|
||||
* Source: /home/node/.openclaw/openclaw.json
|
||||
* Output: /home/node/.openclaw/agents-sanitized.json
|
||||
*/
|
||||
|
||||
import { watch } from 'node:fs';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
const SRC = process.env.OPENCLAW_CONFIG || '/home/node/.openclaw/openclaw.json';
|
||||
const OUT = process.env.SANITIZED_OUTPUT || '/home/node/.openclaw/agents-sanitized.json';
|
||||
|
||||
let running = true;
|
||||
let debounceTimer = null;
|
||||
const DEBOUNCE_MS = 500;
|
||||
|
||||
function log(msg) {
|
||||
const ts = new Date().toISOString();
|
||||
process.stderr.write(`[agents-sanitized ${ts}] ${msg}\n`);
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
try {
|
||||
const raw = await readFile(SRC, 'utf-8');
|
||||
const data = JSON.parse(raw);
|
||||
|
||||
const agents = data?.agents;
|
||||
if (!agents) {
|
||||
log(`ERROR: "agents" key not found in ${SRC}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const sanitized = { agents };
|
||||
const json = JSON.stringify(sanitized, null, 2) + '\n';
|
||||
await writeFile(OUT, json, 'utf-8');
|
||||
|
||||
const agentCount = agents?.list?.length ?? 0;
|
||||
log(`Generated ${OUT} with ${agentCount} agents (${json.length} bytes)`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
log(`ERROR: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const mode = process.argv.includes('--once') ? 'once' : 'watch';
|
||||
log(`Starting in ${mode} mode`);
|
||||
log(` Source: ${SRC}`);
|
||||
log(` Output: ${OUT}`);
|
||||
|
||||
// Initial generation
|
||||
const ok = await generate();
|
||||
if (mode === 'once') {
|
||||
process.exit(ok ? 0 : 1);
|
||||
}
|
||||
|
||||
// Watch mode
|
||||
log('Watching for changes...');
|
||||
watch(SRC, (eventType) => {
|
||||
if (eventType !== 'change') return;
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
log(`Detected change in ${SRC}`);
|
||||
await generate();
|
||||
}, DEBOUNCE_MS);
|
||||
});
|
||||
|
||||
// Keep process alive
|
||||
process.on('SIGINT', () => { running = false; process.exit(0); });
|
||||
process.on('SIGTERM', () => { running = false; process.exit(0); });
|
||||
|
||||
// Periodic check every 5 minutes as fallback
|
||||
setInterval(async () => {
|
||||
const now = Date.now();
|
||||
log('Periodic re-sync check');
|
||||
await generate();
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(`FATAL: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user