Add web4-governance extension for AI governance with R6 workflow
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 <noreply@anthropic.com>
This commit is contained in:
parent
3fe4b2595a
commit
aa15d95986
21
extensions/web4-governance/LICENSE
Normal file
21
extensions/web4-governance/LICENSE
Normal file
@ -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.
|
||||
179
extensions/web4-governance/README.md
Normal file
179
extensions/web4-governance/README.md
Normal file
@ -0,0 +1,179 @@
|
||||
# Web4 Governance Extension for Moltbot
|
||||
|
||||
Lightweight AI governance with R6 workflow formalism and audit trails.
|
||||
|
||||
[](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)
|
||||
23
extensions/web4-governance/clawdbot.plugin.json
Normal file
23
extensions/web4-governance/clawdbot.plugin.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
extensions/web4-governance/index.ts
Normal file
171
extensions/web4-governance/index.ts
Normal file
@ -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<string, SessionState>();
|
||||
|
||||
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;
|
||||
26
extensions/web4-governance/package.json
Normal file
26
extensions/web4-governance/package.json
Normal file
@ -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 <web4@metalinxx.io>",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"moltbot": "workspace:*"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dp-web4/web4"
|
||||
}
|
||||
}
|
||||
220
extensions/web4-governance/src/audit-command.ts
Normal file
220
extensions/web4-governance/src/audit-command.ts
Normal file
@ -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}` };
|
||||
}
|
||||
}
|
||||
197
extensions/web4-governance/src/r6.ts
Normal file
197
extensions/web4-governance/src/r6.ts
Normal file
@ -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<string, unknown>,
|
||||
): 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<void> {
|
||||
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<void> {
|
||||
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, unknown>,
|
||||
): 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, unknown>,
|
||||
): 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;
|
||||
}
|
||||
115
extensions/web4-governance/src/session.ts
Normal file
115
extensions/web4-governance/src/session.ts
Normal file
@ -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<GovernancePreferences> {
|
||||
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<SessionState> {
|
||||
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<SessionState | null> {
|
||||
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<void> {
|
||||
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"),
|
||||
};
|
||||
}
|
||||
74
extensions/web4-governance/src/types.ts
Normal file
74
extensions/web4-governance/src/types.ts
Normal file
@ -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<string, unknown>;
|
||||
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[];
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user