From aa15d95986e7f76a98d0e7b2eca754fcf97678a2 Mon Sep 17 00:00:00 2001 From: dp-web4 Date: Tue, 27 Jan 2026 12:27:14 -0800 Subject: [PATCH] Add web4-governance extension for AI governance with R6 workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds lightweight AI governance extension with: - R6 workflow formalism (Rules, Role, Request, Reference, Resource → Result) - Session start/end hooks for session tracking and audit initialization - Before/after tool call hooks for R6 request logging and audit trails - /audit command for generating governance reports - Hash-linked audit chain for verifiable provenance The extension provides transparent accountability for AI agent actions without impeding productivity, using a policy-based approach that can be customized per team or organization. Co-Authored-By: Claude Sonnet 4.5 --- extensions/web4-governance/LICENSE | 21 ++ extensions/web4-governance/README.md | 179 ++++++++++++++ .../web4-governance/clawdbot.plugin.json | 23 ++ extensions/web4-governance/index.ts | 171 ++++++++++++++ extensions/web4-governance/package.json | 26 +++ .../web4-governance/src/audit-command.ts | 220 ++++++++++++++++++ extensions/web4-governance/src/r6.ts | 197 ++++++++++++++++ extensions/web4-governance/src/session.ts | 115 +++++++++ extensions/web4-governance/src/types.ts | 74 ++++++ 9 files changed, 1026 insertions(+) create mode 100644 extensions/web4-governance/LICENSE create mode 100644 extensions/web4-governance/README.md create mode 100644 extensions/web4-governance/clawdbot.plugin.json create mode 100644 extensions/web4-governance/index.ts create mode 100644 extensions/web4-governance/package.json create mode 100644 extensions/web4-governance/src/audit-command.ts create mode 100644 extensions/web4-governance/src/r6.ts create mode 100644 extensions/web4-governance/src/session.ts create mode 100644 extensions/web4-governance/src/types.ts diff --git a/extensions/web4-governance/LICENSE b/extensions/web4-governance/LICENSE new file mode 100644 index 000000000..72201028a --- /dev/null +++ b/extensions/web4-governance/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Web4 Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/web4-governance/README.md b/extensions/web4-governance/README.md new file mode 100644 index 000000000..c05dd95c4 --- /dev/null +++ b/extensions/web4-governance/README.md @@ -0,0 +1,179 @@ +# Web4 Governance Extension for Moltbot + +Lightweight AI governance with R6 workflow formalism and audit trails. + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +## Overview + +This extension adds structured governance to Moltbot sessions: + +- **R6 Workflow** - Every tool call follows a formal intent→action→result flow +- **Audit Trail** - Verifiable chain of actions with provenance +- **Session Identity** - Software-bound tokens for session tracking + +No external dependencies. No network calls. Just structured, auditable AI actions. + +## Installation + +The extension is bundled with Moltbot. To enable it, configure in your Moltbot config: + +```json +{ + "plugins": { + "web4-governance": { + "enabled": true, + "auditLevel": "standard", + "showR6Status": true, + "actionBudget": null + } + } +} +``` + +## What It Does + +### Every Tool Call Gets an R6 Record + +The R6 framework captures structured intent: + +``` +R6 = Rules + Role + Request + Reference + Resource → Result +``` + +| Component | What It Captures | +|-----------|------------------| +| **Rules** | Preferences and constraints | +| **Role** | Session identity, action index | +| **Request** | Tool name, category, target | +| **Reference** | Chain position, previous R6 | +| **Resource** | (Optional) Estimated cost | +| **Result** | Status, output hash | + +### Audit Trail with Provenance + +Each action creates an audit record linked to its R6 request: + +```json +{ + "record_id": "audit:f8e9a1b2", + "r6_request_id": "r6:f8e9a1b2", + "tool": "Edit", + "category": "write", + "target": "src/main.rs", + "result": { + "status": "success", + "output_hash": "a1b2c3d4..." + }, + "provenance": { + "session_id": "abc123", + "action_index": 47, + "prev_record_hash": "..." + } +} +``` + +Records form a hash-linked chain, enabling verification. + +### Session Identity + +Sessions get a software-bound token: + +``` +web4:session:a1b2c3d4 +``` + +This is **not** hardware-bound (no TPM/Secure Enclave). Trust interpretation is up to the relying party. For hardware-bound identity, see [Hardbound](https://github.com/dp-web4/hardbound). + +## Commands + +| Command | Description | +|---------|-------------| +| `/audit` | Show session audit summary | +| `/audit last 10` | Show last 10 actions | +| `/audit verify` | Verify chain integrity | +| `/audit export` | Export audit log | + +## Configuration + +Available configuration options: + +```json +{ + "plugins": { + "web4-governance": { + "auditLevel": "standard", + "showR6Status": true, + "actionBudget": null + } + } +} +``` + +**auditLevel**: +- `minimal` - Just record, no output +- `standard` - Session start message +- `verbose` - Show each R6 request + +**showR6Status**: Show session token on session start + +**actionBudget**: Maximum number of actions (null = unlimited) + +## Files + +``` +~/.web4/ +├── preferences.json # User preferences (optional) +├── sessions/ # Session state +│ └── {session_id}.json +├── audit/ # Audit records +│ └── {session_id}.jsonl +└── r6/ # R6 request logs + └── {date}.jsonl +``` + +## Why R6? + +The R6 framework provides: + +1. **Structured Intent** - Every action has documented purpose +2. **Audit Foundation** - Machine-readable action history +3. **Context Preservation** - Reference links maintain history +4. **Trust Basis** - Verifiable record for trust evaluation +5. **Policy Hook** - Rules component enables future enforcement + +R6 is observational by default - it records, doesn't block. This makes it safe to deploy without disrupting workflows. + +## Web4 Ecosystem + +This extension implements a subset of the [Web4 trust infrastructure](https://github.com/dp-web4/web4): + +| Concept | This Extension | Full Web4 | +|---------|----------------|-----------| +| Identity | Software token | LCT (hardware-bound) | +| Workflow | R6 framework | R6 + Policy enforcement | +| Audit | Hash-linked chain | Distributed ledger | +| Trust | (Relying party decides) | T3 Trust Tensor | + +For enterprise features (hardware binding, team governance, policy enforcement), see [Hardbound](https://github.com/dp-web4/hardbound). + +## Contributing + +Contributions welcome! This extension is MIT licensed. + +Areas for contribution: +- Additional audit visualizations +- R6 analytics and insights +- Integration with external audit systems +- Performance optimizations + +## License + +MIT License - see [LICENSE](LICENSE) + +## Links + +- [Web4 Specification](https://github.com/dp-web4/web4) +- [R6 Framework Spec](https://github.com/dp-web4/web4/blob/main/web4-standard/core-spec/r6-framework.md) +- [Hardbound (Enterprise)](https://github.com/dp-web4/hardbound) +- [Moltbot](https://github.com/moltbot/moltbot) diff --git a/extensions/web4-governance/clawdbot.plugin.json b/extensions/web4-governance/clawdbot.plugin.json new file mode 100644 index 000000000..f4e48ad8e --- /dev/null +++ b/extensions/web4-governance/clawdbot.plugin.json @@ -0,0 +1,23 @@ +{ + "id": "web4-governance", + "channels": [], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "auditLevel": { + "type": "string", + "enum": ["minimal", "standard", "verbose"], + "default": "standard" + }, + "showR6Status": { + "type": "boolean", + "default": true + }, + "actionBudget": { + "type": ["number", "null"], + "default": null + } + } + } +} diff --git a/extensions/web4-governance/index.ts b/extensions/web4-governance/index.ts new file mode 100644 index 000000000..8cd80ffd6 --- /dev/null +++ b/extensions/web4-governance/index.ts @@ -0,0 +1,171 @@ +/** + * Web4 Governance Extension for Moltbot + * SPDX-License-Identifier: MIT + * Copyright (c) 2025 Web4 Contributors + * + * Lightweight AI governance with R6 workflow formalism and audit trails. + * https://github.com/dp-web4/web4 + */ + +import type { MoltbotPluginApi, MoltbotPluginDefinition } from "clawdbot/plugin-sdk"; +import { initializeSession, loadSession, saveSession } from "./src/session.js"; +import { + createR6Request, + finalizeR6Request, + createAuditRecord, + persistR6Request, + persistAuditRecord, +} from "./src/r6.js"; +import { handleAuditCommand } from "./src/audit-command.js"; +import type { SessionState } from "./src/types.js"; + +// Active session store (in-memory, per-process) +const activeSessions = new Map(); + +const plugin: MoltbotPluginDefinition = { + id: "web4-governance", + name: "Web4 Governance", + description: + "Lightweight AI governance with R6 workflow formalism and audit trails. Every tool call becomes a structured request with verifiable provenance.", + version: "1.0.0", + + async register(api: MoltbotPluginApi) { + api.logger.info("Web4 Governance extension loaded"); + + // Register session lifecycle hooks + api.on("session_start", async (event, ctx) => { + const sessionId = event.sessionId || ctx.sessionId; + if (!sessionId) return; + + try { + // Try to load existing session, or create new + let session = await loadSession(sessionId); + if (!session) { + session = await initializeSession(sessionId); + + // Show status if preference enabled + if (session.preferences.showR6Status && api.logger.info) { + const tokenShort = session.token.token_id.split(":")[2]; + api.logger.info(`[Web4] Session ${tokenShort} (software-bound)`); + } + } + + // Store in active sessions + activeSessions.set(sessionId, session); + } catch (error) { + api.logger.error(`Failed to initialize Web4 session: ${error}`); + } + }); + + api.on("session_end", async (event, ctx) => { + const sessionId = event.sessionId || ctx.sessionId; + if (!sessionId) return; + + try { + const session = activeSessions.get(sessionId); + if (session) { + // Final save + await saveSession(session); + activeSessions.delete(sessionId); + + if (api.logger.info) { + api.logger.info( + `[Web4] Session ended: ${session.action_count} actions audited`, + ); + } + } + } catch (error) { + api.logger.error(`Failed to finalize Web4 session: ${error}`); + } + }); + + // Register tool call audit hooks + api.on("before_tool_call", async (event, ctx) => { + const sessionId = ctx.sessionKey || ctx.agentId; + if (!sessionId) return; + + try { + let session = activeSessions.get(sessionId); + if (!session) { + // Session might not be in memory, try to load + session = await loadSession(sessionId); + if (!session) { + // Create session on-the-fly if needed + session = await initializeSession(sessionId); + } + activeSessions.set(sessionId, session); + } + + // Create R6 request + const r6 = createR6Request(session, event.toolName, event.params); + + // Store R6 temporarily (will be finalized in after_tool_call) + session.r6_requests.push(r6); + + // Persist R6 request + await persistR6Request(r6); + } catch (error) { + api.logger.error(`Failed to create R6 request: ${error}`); + } + }); + + api.on("after_tool_call", async (event, ctx) => { + const sessionId = ctx.sessionKey || ctx.agentId; + if (!sessionId) return; + + try { + const session = activeSessions.get(sessionId); + if (!session) return; + + // Get the pending R6 request + const pendingR6 = session.r6_requests[session.r6_requests.length - 1]; + if (!pendingR6 || pendingR6.result) { + // Already finalized or missing + return; + } + + // Finalize R6 with result + const finalizedR6 = finalizeR6Request(pendingR6, event.result, event.error); + session.r6_requests[session.r6_requests.length - 1] = finalizedR6; + + // Create audit record + const auditRecord = createAuditRecord(finalizedR6, session); + session.audit_chain.push(auditRecord); + + // Increment action count + session.action_count++; + + // Persist audit record + await persistAuditRecord(auditRecord, sessionId); + + // Save session state + await saveSession(session); + + // Check action budget if configured + if ( + session.preferences.actionBudget && + session.action_count >= session.preferences.actionBudget + ) { + api.logger.warn( + `[Web4] Action budget reached: ${session.action_count}/${session.preferences.actionBudget}`, + ); + } + } catch (error) { + api.logger.error(`Failed to finalize R6 request: ${error}`); + } + }); + + // Register /audit command + api.registerCommand({ + name: "audit", + description: "Show Web4 governance audit summary and reports", + acceptsArgs: true, + requireAuth: true, + handler: handleAuditCommand, + }); + + api.logger.info("Web4 Governance hooks and commands registered"); + }, +}; + +export default plugin; diff --git a/extensions/web4-governance/package.json b/extensions/web4-governance/package.json new file mode 100644 index 000000000..5d0ba9645 --- /dev/null +++ b/extensions/web4-governance/package.json @@ -0,0 +1,26 @@ +{ + "name": "web4-governance", + "version": "1.0.0", + "description": "Lightweight AI governance with R6 workflow formalism and audit trails for Moltbot", + "type": "module", + "main": "index.ts", + "scripts": {}, + "keywords": [ + "governance", + "audit", + "trust", + "r6", + "provenance", + "web4" + ], + "author": "Web4 Contributors ", + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "moltbot": "workspace:*" + }, + "repository": { + "type": "git", + "url": "https://github.com/dp-web4/web4" + } +} diff --git a/extensions/web4-governance/src/audit-command.ts b/extensions/web4-governance/src/audit-command.ts new file mode 100644 index 000000000..e7b00b20b --- /dev/null +++ b/extensions/web4-governance/src/audit-command.ts @@ -0,0 +1,220 @@ +/** + * Web4 Governance Extension - Audit Command + * SPDX-License-Identifier: MIT + * Copyright (c) 2025 Web4 Contributors + */ + +import { createHash } from "node:crypto"; +import { readFile, readdir } from "node:fs/promises"; +import { join } from "node:path"; +import type { PluginCommandHandler } from "clawdbot/plugin-sdk"; +import { getWeb4Paths, loadSession } from "./session.js"; +import type { AuditRecord, SessionState } from "./types.js"; + +/** + * Handle /audit command. + */ +export const handleAuditCommand: PluginCommandHandler = async (ctx) => { + const args = ctx.args?.trim() || ""; + + // Parse command arguments + if (args.startsWith("last ")) { + const count = parseInt(args.slice(5), 10); + return await showLastActions(count); + } + + if (args === "verify") { + return await verifyChainIntegrity(); + } + + if (args === "export") { + return await exportAuditLog(); + } + + // Default: show session summary + return await showSessionSummary(); +}; + +/** + * Show session summary. + */ +async function showSessionSummary() { + try { + const { sessionDir } = getWeb4Paths(); + const sessions = await readdir(sessionDir); + + if (sessions.length === 0) { + return { text: "No Web4 governance sessions found." }; + } + + // Get latest session + const latestSessionFile = sessions.sort().reverse()[0]; + const sessionId = latestSessionFile.replace(".json", ""); + const session = await loadSession(sessionId); + + if (!session) { + return { text: "Failed to load session data." }; + } + + const tokenShort = session.token.token_id.split(":")[2]; + const duration = Date.now() - new Date(session.started_at).getTime(); + const durationMin = Math.floor(duration / 60000); + + const text = [ + "**Web4 Governance Session Summary**", + "", + `Session: ${tokenShort} (software-bound)`, + `Started: ${new Date(session.started_at).toLocaleString()}`, + `Duration: ${durationMin} minutes`, + `Actions: ${session.action_count}`, + `Audit Level: ${session.preferences.auditLevel}`, + "", + "Use `/audit last 10` to see recent actions", + "Use `/audit verify` to verify chain integrity", + "Use `/audit export` to export full audit log", + ].join("\n"); + + return { text }; + } catch (error) { + return { text: `Error reading audit data: ${error}` }; + } +} + +/** + * Show last N actions. + */ +async function showLastActions(count: number) { + try { + const { sessionDir, auditDir } = getWeb4Paths(); + const sessions = await readdir(sessionDir); + + if (sessions.length === 0) { + return { text: "No audit records found." }; + } + + // Get latest session + const latestSessionFile = sessions.sort().reverse()[0]; + const sessionId = latestSessionFile.replace(".json", ""); + + // Read audit log + const auditFile = join(auditDir, `${sessionId}.jsonl`); + const auditData = await readFile(auditFile, "utf-8"); + const records: AuditRecord[] = auditData + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + const lastRecords = records.slice(-count); + + const lines = [ + `**Last ${lastRecords.length} Actions**`, + "", + ]; + + for (const record of lastRecords) { + const time = new Date(record.timestamp).toLocaleTimeString(); + const status = record.result.status === "success" ? "✓" : "✗"; + lines.push( + `${status} [${time}] ${record.tool} (${record.category})${record.target ? ` → ${record.target}` : ""}`, + ); + } + + return { text: lines.join("\n") }; + } catch (error) { + return { text: `Error reading audit log: ${error}` }; + } +} + +/** + * Verify chain integrity. + */ +async function verifyChainIntegrity() { + try { + const { sessionDir, auditDir } = getWeb4Paths(); + const sessions = await readdir(sessionDir); + + if (sessions.length === 0) { + return { text: "No audit chain to verify." }; + } + + // Get latest session + const latestSessionFile = sessions.sort().reverse()[0]; + const sessionId = latestSessionFile.replace(".json", ""); + + // Read audit log + const auditFile = join(auditDir, `${sessionId}.jsonl`); + const auditData = await readFile(auditFile, "utf-8"); + const records: AuditRecord[] = auditData + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + // Verify chain + let isValid = true; + let prevHash: string | undefined; + + for (let i = 0; i < records.length; i++) { + const record = records[i]; + const expectedPrevHash = prevHash; + const actualPrevHash = record.provenance.prev_record_hash; + + if (i === 0) { + if (actualPrevHash !== undefined) { + isValid = false; + break; + } + } else { + if (actualPrevHash !== expectedPrevHash) { + isValid = false; + break; + } + } + + // Compute hash for next iteration + prevHash = createHash("sha256") + .update(JSON.stringify(record)) + .digest("hex") + .slice(0, 16); + } + + if (isValid) { + return { + text: `✓ Audit chain verified: ${records.length} records, integrity intact`, + }; + } else { + return { text: "✗ Audit chain verification failed: integrity compromised" }; + } + } catch (error) { + return { text: `Error verifying chain: ${error}` }; + } +} + +/** + * Export audit log. + */ +async function exportAuditLog() { + try { + const { sessionDir, auditDir } = getWeb4Paths(); + const sessions = await readdir(sessionDir); + + if (sessions.length === 0) { + return { text: "No audit logs to export." }; + } + + // Get latest session + const latestSessionFile = sessions.sort().reverse()[0]; + const sessionId = latestSessionFile.replace(".json", ""); + + const auditFile = join(auditDir, `${sessionId}.jsonl`); + const text = [ + `Audit log exported: ${auditFile}`, + "", + "Copy this file to share with auditors or relying parties.", + "The JSONL format preserves the hash-linked chain structure.", + ].join("\n"); + + return { text }; + } catch (error) { + return { text: `Error exporting audit log: ${error}` }; + } +} diff --git a/extensions/web4-governance/src/r6.ts b/extensions/web4-governance/src/r6.ts new file mode 100644 index 000000000..31dca4f26 --- /dev/null +++ b/extensions/web4-governance/src/r6.ts @@ -0,0 +1,197 @@ +/** + * Web4 Governance Extension - R6 Workflow + * SPDX-License-Identifier: MIT + * Copyright (c) 2025 Web4 Contributors + * + * R6 Framework: Rules + Role + Request + Reference + Resource → Result + */ + +import { createHash, randomBytes } from "node:crypto"; +import { appendFile, mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import type { R6Request, AuditRecord, SessionState } from "./types.js"; +import { getWeb4Paths } from "./session.js"; + +/** + * Create an R6 request for a tool call. + */ +export function createR6Request( + session: SessionState, + toolName: string, + params: Record, +): R6Request { + const requestId = `r6:${randomBytes(4).toString("hex")}`; + const actionIndex = session.action_count; + const prevR6 = session.r6_requests[session.r6_requests.length - 1]; + + // Categorize tool + const category = categorizeToolCall(toolName, params); + const target = extractTarget(toolName, params); + + const r6: R6Request = { + request_id: requestId, + rules: session.preferences, + role: { + session_id: session.session_id, + action_index: actionIndex, + }, + request: { + tool: toolName, + category, + target, + }, + reference: { + chain_position: actionIndex, + prev_r6_id: prevR6?.request_id, + }, + timestamp: new Date().toISOString(), + }; + + return r6; +} + +/** + * Finalize R6 request with result. + */ +export function finalizeR6Request( + r6: R6Request, + result: unknown, + error?: string, +): R6Request { + const status = error ? "error" : "success"; + const outputHash = result + ? createHash("sha256") + .update(JSON.stringify(result)) + .digest("hex") + .slice(0, 16) + : undefined; + + return { + ...r6, + result: { + status, + output_hash: outputHash, + error, + }, + }; +} + +/** + * Create an audit record from R6 request. + */ +export function createAuditRecord( + r6: R6Request, + session: SessionState, +): AuditRecord { + const recordId = `audit:${randomBytes(4).toString("hex")}`; + const prevRecord = session.audit_chain[session.audit_chain.length - 1]; + const prevRecordHash = prevRecord + ? createHash("sha256") + .update(JSON.stringify(prevRecord)) + .digest("hex") + .slice(0, 16) + : undefined; + + return { + record_id: recordId, + r6_request_id: r6.request_id, + tool: r6.request.tool, + category: r6.request.category, + target: r6.request.target, + result: r6.result!, + provenance: { + session_id: session.session_id, + action_index: r6.role.action_index, + prev_record_hash: prevRecordHash, + }, + timestamp: r6.timestamp, + }; +} + +/** + * Persist R6 request to daily log. + */ +export async function persistR6Request(r6: R6Request): Promise { + const { r6Dir } = getWeb4Paths(); + await mkdir(r6Dir, { recursive: true }); + + const date = new Date().toISOString().split("T")[0]; + const logFile = join(r6Dir, `${date}.jsonl`); + + await appendFile(logFile, JSON.stringify(r6) + "\n"); +} + +/** + * Persist audit record to session log. + */ +export async function persistAuditRecord( + record: AuditRecord, + sessionId: string, +): Promise { + const { auditDir } = getWeb4Paths(); + await mkdir(auditDir, { recursive: true }); + + const logFile = join(auditDir, `${sessionId}.jsonl`); + await appendFile(logFile, JSON.stringify(record) + "\n"); +} + +/** + * Categorize tool call for audit trail. + */ +function categorizeToolCall( + toolName: string, + params: Record, +): string { + // Basic categorization - can be extended + if (toolName.toLowerCase().includes("read") || toolName.toLowerCase().includes("get")) { + return "read"; + } + if ( + toolName.toLowerCase().includes("write") || + toolName.toLowerCase().includes("create") || + toolName.toLowerCase().includes("edit") + ) { + return "write"; + } + if ( + toolName.toLowerCase().includes("delete") || + toolName.toLowerCase().includes("remove") + ) { + return "delete"; + } + if ( + toolName.toLowerCase().includes("exec") || + toolName.toLowerCase().includes("run") || + toolName.toLowerCase().includes("bash") + ) { + return "execute"; + } + return "other"; +} + +/** + * Extract target from tool parameters. + */ +function extractTarget( + toolName: string, + params: Record, +): string | undefined { + // Common parameter names for targets + const targetKeys = [ + "file_path", + "path", + "filename", + "url", + "target", + "destination", + "command", + ]; + + for (const key of targetKeys) { + if (params[key]) { + return String(params[key]); + } + } + + return undefined; +} diff --git a/extensions/web4-governance/src/session.ts b/extensions/web4-governance/src/session.ts new file mode 100644 index 000000000..2e65f05b9 --- /dev/null +++ b/extensions/web4-governance/src/session.ts @@ -0,0 +1,115 @@ +/** + * Web4 Governance Extension - Session Management + * SPDX-License-Identifier: MIT + * Copyright (c) 2025 Web4 Contributors + */ + +import { createHash, randomBytes } from "node:crypto"; +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { homedir, userInfo } from "node:os"; +import { join } from "node:path"; +import type { + SessionToken, + GovernancePreferences, + SessionState, +} from "./types.js"; + +const WEB4_DIR = join(homedir(), ".web4"); +const SESSION_DIR = join(WEB4_DIR, "sessions"); +const PREFERENCES_FILE = join(WEB4_DIR, "preferences.json"); + +/** + * Create a software-bound session token. + * In the full Web4 spec, this would be an LCT (Linked Context Token) + * bound to hardware. This light version uses a software-derived token. + */ +export function createSessionToken(): SessionToken { + const uid = userInfo().uid; + const hostname = require("node:os").hostname(); + const timestamp = new Date().toISOString(); + const seed = `${hostname}:${uid}:${timestamp}:${randomBytes(8).toString("hex")}`; + const tokenHash = createHash("sha256").update(seed).digest("hex").slice(0, 12); + + return { + token_id: `web4:session:${tokenHash}`, + binding: "software", + created_at: new Date().toISOString(), + machine_hint: createHash("sha256").update(hostname).digest("hex").slice(0, 8), + }; +} + +/** + * Load user governance preferences. + */ +export async function loadPreferences(): Promise { + try { + const data = await readFile(PREFERENCES_FILE, "utf-8"); + return JSON.parse(data); + } catch { + // Default preferences + return { + auditLevel: "standard", + showR6Status: true, + actionBudget: null, + }; + } +} + +/** + * Initialize Web4 session state. + */ +export async function initializeSession(sessionId: string): Promise { + await mkdir(SESSION_DIR, { recursive: true }); + + const token = createSessionToken(); + const preferences = await loadPreferences(); + + const session: SessionState = { + session_id: sessionId, + token, + preferences, + started_at: new Date().toISOString(), + action_count: 0, + r6_requests: [], + audit_chain: [], + }; + + const sessionFile = join(SESSION_DIR, `${sessionId}.json`); + await writeFile(sessionFile, JSON.stringify(session, null, 2)); + + return session; +} + +/** + * Load session state. + */ +export async function loadSession(sessionId: string): Promise { + try { + const sessionFile = join(SESSION_DIR, `${sessionId}.json`); + const data = await readFile(sessionFile, "utf-8"); + return JSON.parse(data); + } catch { + return null; + } +} + +/** + * Save session state. + */ +export async function saveSession(session: SessionState): Promise { + const sessionFile = join(SESSION_DIR, `${session.session_id}.json`); + await writeFile(sessionFile, JSON.stringify(session, null, 2)); +} + +/** + * Get Web4 directory paths. + */ +export function getWeb4Paths() { + return { + web4Dir: WEB4_DIR, + sessionDir: SESSION_DIR, + preferencesFile: PREFERENCES_FILE, + auditDir: join(WEB4_DIR, "audit"), + r6Dir: join(WEB4_DIR, "r6"), + }; +} diff --git a/extensions/web4-governance/src/types.ts b/extensions/web4-governance/src/types.ts new file mode 100644 index 000000000..ceecb3462 --- /dev/null +++ b/extensions/web4-governance/src/types.ts @@ -0,0 +1,74 @@ +/** + * Web4 Governance Extension - Types + * SPDX-License-Identifier: MIT + * Copyright (c) 2025 Web4 Contributors + */ + +export type SessionToken = { + token_id: string; + binding: "software"; + created_at: string; + machine_hint: string; +}; + +export type GovernancePreferences = { + auditLevel: "minimal" | "standard" | "verbose"; + showR6Status: boolean; + actionBudget: number | null; +}; + +export type R6Request = { + request_id: string; + rules: Record; + role: { + session_id: string; + action_index: number; + }; + request: { + tool: string; + category: string; + target?: string; + }; + reference: { + chain_position: number; + prev_r6_id?: string; + }; + resource?: { + estimated_cost?: number; + }; + result?: { + status: "success" | "error" | "blocked"; + output_hash?: string; + error?: string; + }; + timestamp: string; +}; + +export type AuditRecord = { + record_id: string; + r6_request_id: string; + tool: string; + category: string; + target?: string; + result: { + status: "success" | "error" | "blocked"; + output_hash?: string; + error?: string; + }; + provenance: { + session_id: string; + action_index: number; + prev_record_hash?: string; + }; + timestamp: string; +}; + +export type SessionState = { + session_id: string; + token: SessionToken; + preferences: GovernancePreferences; + started_at: string; + action_count: number; + r6_requests: R6Request[]; + audit_chain: AuditRecord[]; +};