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