@neuroverseos/governance 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +190 -0
- package/README.md +472 -115
- package/dist/adapters/express.cjs +874 -0
- package/dist/adapters/express.d.cts +66 -0
- package/dist/adapters/express.d.ts +66 -0
- package/dist/adapters/express.js +10 -0
- package/dist/adapters/index.cjs +1166 -0
- package/dist/adapters/index.d.cts +5 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +40 -0
- package/dist/adapters/langchain.cjs +861 -0
- package/dist/adapters/langchain.d.cts +82 -0
- package/dist/adapters/langchain.d.ts +82 -0
- package/dist/adapters/langchain.js +14 -0
- package/dist/adapters/openai.cjs +891 -0
- package/dist/adapters/openai.d.cts +92 -0
- package/dist/adapters/openai.d.ts +92 -0
- package/dist/adapters/openai.js +14 -0
- package/dist/adapters/openclaw.cjs +883 -0
- package/dist/adapters/openclaw.d.cts +92 -0
- package/dist/adapters/openclaw.d.ts +92 -0
- package/dist/adapters/openclaw.js +14 -0
- package/dist/bootstrap-H4HHKQ5G.js +113 -0
- package/dist/build-73KAVHEY.js +308 -0
- package/dist/chunk-4NGDRRQH.js +10 -0
- package/dist/chunk-B4NF3OLW.js +673 -0
- package/dist/chunk-BUWWN2NX.js +192 -0
- package/dist/chunk-CROPZ75A.js +107 -0
- package/dist/chunk-D7BGWV2J.js +620 -0
- package/dist/chunk-EIUHJXBB.js +340 -0
- package/dist/chunk-EQXFOKH2.js +85 -0
- package/dist/chunk-FYPYZFV5.js +187 -0
- package/dist/chunk-FYS2CBUW.js +304 -0
- package/dist/chunk-FZQCRGUU.js +115 -0
- package/dist/chunk-I3RRAYK2.js +11 -0
- package/dist/chunk-ITJ3LCPG.js +225 -0
- package/dist/chunk-M3TZFGHO.js +79 -0
- package/dist/chunk-MWDQ4MJB.js +11 -0
- package/dist/chunk-O5OMJMIE.js +590 -0
- package/dist/chunk-T4X42QXC.js +121 -0
- package/dist/chunk-XPDMYECO.js +642 -0
- package/dist/chunk-Z2S2HIV5.js +100 -0
- package/dist/cli/neuroverse.cjs +5864 -0
- package/dist/cli/neuroverse.d.cts +1 -0
- package/dist/cli/neuroverse.d.ts +1 -0
- package/dist/cli/neuroverse.js +128 -0
- package/dist/configure-ai-46JVG56I.js +129 -0
- package/dist/derive-6NAEWLM5.js +150 -0
- package/dist/explain-3B3VB6TL.js +69 -0
- package/dist/guard-67Y66P3I.js +82 -0
- package/dist/guard-contract-D_RQz9kt.d.cts +529 -0
- package/dist/guard-contract-D_RQz9kt.d.ts +529 -0
- package/dist/impact-CHERK3O6.js +57 -0
- package/dist/improve-YG6I6ERG.js +84 -0
- package/dist/index.cjs +4227 -0
- package/dist/index.d.cts +1147 -0
- package/dist/index.d.ts +1139 -25
- package/dist/index.js +318 -1159
- package/dist/init-Z66T6TDI.js +142 -0
- package/dist/simulate-ETHHINZ4.js +107 -0
- package/dist/trace-3YODSSIP.js +164 -0
- package/dist/validate-UVE6GKQU.js +72 -0
- package/dist/validate-engine-657D75OG.js +6 -0
- package/dist/world-WLNHL5XC.js +298 -0
- package/dist/worlds/derivation-world.nv-world.md +278 -0
- package/package.json +74 -26
- package/LICENSE +0 -21
- package/dist/audit-logger.d.ts +0 -38
- package/dist/audit-logger.d.ts.map +0 -1
- package/dist/audit-logger.js +0 -100
- package/dist/audit-logger.js.map +0 -1
- package/dist/condition-engine.d.ts +0 -16
- package/dist/condition-engine.d.ts.map +0 -1
- package/dist/condition-engine.js +0 -186
- package/dist/condition-engine.js.map +0 -1
- package/dist/drift-monitor.d.ts +0 -81
- package/dist/drift-monitor.d.ts.map +0 -1
- package/dist/drift-monitor.js +0 -228
- package/dist/drift-monitor.js.map +0 -1
- package/dist/governance-engine.d.ts +0 -131
- package/dist/governance-engine.d.ts.map +0 -1
- package/dist/governance-engine.js +0 -660
- package/dist/governance-engine.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -196
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -9
- package/dist/types.js.map +0 -1
- package/dist/world-bootstrap.d.ts +0 -31
- package/dist/world-bootstrap.d.ts.map +0 -1
- package/dist/world-bootstrap.js +0 -415
- package/dist/world-bootstrap.js.map +0 -1
- package/openclaw.plugin.json +0 -59
|
@@ -1,660 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Governance Engine — Deterministic decision pipeline
|
|
4
|
-
*
|
|
5
|
-
* Evaluates tool calls against world rules in strict order:
|
|
6
|
-
* 1. Session allowlist → ALLOW silently if guard already approved
|
|
7
|
-
* 2. Invariants → any hit = BLOCK (immediate, no override)
|
|
8
|
-
* 3. Guards → condition engine + tool filter → PAUSE or BLOCK
|
|
9
|
-
* 4. Rules → trigger evaluation → verdict override
|
|
10
|
-
* 5. Role constraints → cannotDo = BLOCK, requiresApproval = PAUSE
|
|
11
|
-
* 6. Default → ALLOW
|
|
12
|
-
*
|
|
13
|
-
* Same world + same event = same verdict. Always.
|
|
14
|
-
* No AI. No network calls. Sub-millisecond per evaluation.
|
|
15
|
-
*/
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.GovernanceEngine = void 0;
|
|
18
|
-
const fs_1 = require("fs");
|
|
19
|
-
const crypto_1 = require("crypto");
|
|
20
|
-
const path_1 = require("path");
|
|
21
|
-
const condition_engine_1 = require("./condition-engine");
|
|
22
|
-
const world_bootstrap_1 = require("./world-bootstrap");
|
|
23
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
24
|
-
// Default verdict (spec §4)
|
|
25
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
26
|
-
const DEFAULT_ALLOW = {
|
|
27
|
-
status: 'ALLOW',
|
|
28
|
-
reason: 'No rules matched — default allow',
|
|
29
|
-
ruleId: null,
|
|
30
|
-
guard: null,
|
|
31
|
-
evidence: null,
|
|
32
|
-
};
|
|
33
|
-
const NO_WORLD = {
|
|
34
|
-
status: 'ALLOW',
|
|
35
|
-
reason: 'No world file loaded',
|
|
36
|
-
ruleId: null,
|
|
37
|
-
guard: null,
|
|
38
|
-
evidence: null,
|
|
39
|
-
};
|
|
40
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
41
|
-
// Engine
|
|
42
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
43
|
-
class GovernanceEngine {
|
|
44
|
-
constructor(config) {
|
|
45
|
-
this.world = null;
|
|
46
|
-
// Runtime integrity state (spec §8)
|
|
47
|
-
this.worldMtimeMs = 0;
|
|
48
|
-
this.cachedWorldHash = null;
|
|
49
|
-
this.metaRecord = null;
|
|
50
|
-
this.surfacedAlerts = new Set();
|
|
51
|
-
this.workspaceDir = null;
|
|
52
|
-
this.config = config;
|
|
53
|
-
}
|
|
54
|
-
/** Set the workspace directory for md drift detection */
|
|
55
|
-
setWorkspaceDir(dir) {
|
|
56
|
-
this.workspaceDir = dir;
|
|
57
|
-
}
|
|
58
|
-
/** Clear surfaced alerts (e.g. on session reset) */
|
|
59
|
-
clearAlertHistory() {
|
|
60
|
-
this.surfacedAlerts.clear();
|
|
61
|
-
}
|
|
62
|
-
// ── World Loading ──────────────────────────────────────────────
|
|
63
|
-
loadWorld() {
|
|
64
|
-
if (!(0, fs_1.existsSync)(this.config.worldPath)) {
|
|
65
|
-
this.world = null;
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
const raw = (0, fs_1.readFileSync)(this.config.worldPath, 'utf-8');
|
|
70
|
-
this.world = JSON.parse(raw);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
this.world = null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
getWorld() {
|
|
77
|
-
return this.world;
|
|
78
|
-
}
|
|
79
|
-
setWorld(world) {
|
|
80
|
-
this.world = world;
|
|
81
|
-
}
|
|
82
|
-
saveWorld() {
|
|
83
|
-
if (this.world) {
|
|
84
|
-
(0, fs_1.writeFileSync)(this.config.worldPath, JSON.stringify(this.world, null, 2));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
getStatus() {
|
|
88
|
-
return {
|
|
89
|
-
worldLoaded: this.world !== null,
|
|
90
|
-
invariantCount: this.world?.invariants?.length ?? 0,
|
|
91
|
-
guardCount: this.world?.guards?.length ?? 0,
|
|
92
|
-
ruleCount: this.world?.rules?.length ?? 0,
|
|
93
|
-
roleCount: this.world?.roles?.length ?? 0,
|
|
94
|
-
enforcement: this.config.enforcement,
|
|
95
|
-
observeOnly: this.config.observeOnly,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
// ── World Meta / Integrity ───────────────────────────────────
|
|
99
|
-
/** Load or create world.meta.json for integrity tracking */
|
|
100
|
-
loadMeta() {
|
|
101
|
-
const metaPath = this.getMetaPath();
|
|
102
|
-
if (!metaPath)
|
|
103
|
-
return;
|
|
104
|
-
if ((0, fs_1.existsSync)(metaPath)) {
|
|
105
|
-
try {
|
|
106
|
-
this.metaRecord = JSON.parse((0, fs_1.readFileSync)(metaPath, 'utf-8'));
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
this.metaRecord = null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
else if (this.world) {
|
|
113
|
-
// Migration: world.json exists but no meta yet (spec §16)
|
|
114
|
-
this.metaRecord = {
|
|
115
|
-
world: this.world,
|
|
116
|
-
hash: this.computeWorldHash(this.world),
|
|
117
|
-
activatedAt: Date.now(),
|
|
118
|
-
activatedBy: 'migration',
|
|
119
|
-
version: 1,
|
|
120
|
-
};
|
|
121
|
-
this.saveMeta();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
saveMeta() {
|
|
125
|
-
const metaPath = this.getMetaPath();
|
|
126
|
-
if (!metaPath || !this.metaRecord)
|
|
127
|
-
return;
|
|
128
|
-
(0, fs_1.writeFileSync)(metaPath, JSON.stringify(this.metaRecord, null, 2));
|
|
129
|
-
}
|
|
130
|
-
getMetaRecord() {
|
|
131
|
-
return this.metaRecord;
|
|
132
|
-
}
|
|
133
|
-
setMetaRecord(record) {
|
|
134
|
-
this.metaRecord = record;
|
|
135
|
-
}
|
|
136
|
-
// ── Role Bindings (agentId → roleId) ─────────────────────────
|
|
137
|
-
/**
|
|
138
|
-
* Get all role bindings from the meta record.
|
|
139
|
-
* Returns empty array if no bindings exist.
|
|
140
|
-
*/
|
|
141
|
-
getRoleBindings() {
|
|
142
|
-
return this.metaRecord?.roleBindings ?? [];
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Hydrate a Map<agentId, roleId> from stored bindings.
|
|
146
|
-
* Called at startup and after any binding change.
|
|
147
|
-
*/
|
|
148
|
-
hydrateRoleMap(roleMap) {
|
|
149
|
-
roleMap.clear();
|
|
150
|
-
const bindings = this.getRoleBindings();
|
|
151
|
-
for (const binding of bindings) {
|
|
152
|
-
roleMap.set(binding.agentId, binding.roleId);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Add or update a role binding. Returns the severity of the change.
|
|
157
|
-
* Does NOT auto-save — caller must call saveMeta().
|
|
158
|
-
*/
|
|
159
|
-
setRoleBinding(agentId, roleId, boundBy = 'human') {
|
|
160
|
-
if (!this.metaRecord)
|
|
161
|
-
return 'new_binding';
|
|
162
|
-
if (!this.metaRecord.roleBindings) {
|
|
163
|
-
this.metaRecord.roleBindings = [];
|
|
164
|
-
}
|
|
165
|
-
const existing = this.metaRecord.roleBindings.find(b => b.agentId === agentId);
|
|
166
|
-
let severity;
|
|
167
|
-
if (!existing) {
|
|
168
|
-
severity = 'new_binding';
|
|
169
|
-
this.metaRecord.roleBindings.push({
|
|
170
|
-
agentId,
|
|
171
|
-
roleId,
|
|
172
|
-
boundAt: Date.now(),
|
|
173
|
-
boundBy,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
const oldRoleId = existing.roleId;
|
|
178
|
-
severity = this.classifyBindingChange(oldRoleId, roleId);
|
|
179
|
-
existing.roleId = roleId;
|
|
180
|
-
existing.boundAt = Date.now();
|
|
181
|
-
existing.boundBy = boundBy;
|
|
182
|
-
}
|
|
183
|
-
return severity;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Remove a role binding. Returns 'removal' severity.
|
|
187
|
-
* Does NOT auto-save — caller must call saveMeta().
|
|
188
|
-
*/
|
|
189
|
-
removeRoleBinding(agentId) {
|
|
190
|
-
if (!this.metaRecord?.roleBindings)
|
|
191
|
-
return false;
|
|
192
|
-
const idx = this.metaRecord.roleBindings.findIndex(b => b.agentId === agentId);
|
|
193
|
-
if (idx === -1)
|
|
194
|
-
return false;
|
|
195
|
-
this.metaRecord.roleBindings.splice(idx, 1);
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Classify the severity of changing from one role to another.
|
|
200
|
-
* Uses the role's requiresApproval and cannotDo as proxy for privilege level.
|
|
201
|
-
*/
|
|
202
|
-
classifyBindingChange(oldRoleId, newRoleId) {
|
|
203
|
-
if (oldRoleId === newRoleId)
|
|
204
|
-
return 'reassignment';
|
|
205
|
-
if (!this.world)
|
|
206
|
-
return 'reassignment';
|
|
207
|
-
const oldRole = this.world.roles.find(r => r.id === oldRoleId);
|
|
208
|
-
const newRole = this.world.roles.find(r => r.id === newRoleId);
|
|
209
|
-
if (!oldRole || !newRole)
|
|
210
|
-
return 'reassignment';
|
|
211
|
-
// Privilege proxy: more cannotDo restrictions = lower privilege
|
|
212
|
-
// requiresApproval = lower privilege
|
|
213
|
-
const oldPrivilege = this.estimatePrivilege(oldRole);
|
|
214
|
-
const newPrivilege = this.estimatePrivilege(newRole);
|
|
215
|
-
if (newPrivilege > oldPrivilege)
|
|
216
|
-
return 'escalation';
|
|
217
|
-
if (newPrivilege < oldPrivilege)
|
|
218
|
-
return 'de_escalation';
|
|
219
|
-
return 'reassignment';
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Estimate privilege level of a role (higher = more permissive).
|
|
223
|
-
* Simple heuristic: canDo breadth - cannotDo restrictions - approval overhead.
|
|
224
|
-
*/
|
|
225
|
-
estimatePrivilege(role) {
|
|
226
|
-
let score = role.canDo.length;
|
|
227
|
-
score -= role.cannotDo.length * 2;
|
|
228
|
-
if (role.requiresApproval)
|
|
229
|
-
score -= 3;
|
|
230
|
-
return score;
|
|
231
|
-
}
|
|
232
|
-
getMetaPath() {
|
|
233
|
-
if (!this.config.worldPath)
|
|
234
|
-
return null;
|
|
235
|
-
return (0, path_1.join)((0, path_1.dirname)(this.config.worldPath), 'world.meta.json');
|
|
236
|
-
}
|
|
237
|
-
/** Canonical SHA-256 hash of a world object (sorted keys, no whitespace) */
|
|
238
|
-
computeWorldHash(world) {
|
|
239
|
-
const canonical = JSON.stringify(world, Object.keys(world).sort());
|
|
240
|
-
return (0, crypto_1.createHash)('sha256').update(canonical).digest('hex');
|
|
241
|
-
}
|
|
242
|
-
// ── Runtime Integrity Checks (spec §8) ─────────────────────
|
|
243
|
-
/**
|
|
244
|
-
* System-level protection: intercept modifications to governance source files.
|
|
245
|
-
*
|
|
246
|
-
* This runs BEFORE all user-defined rules and cannot be removed by
|
|
247
|
-
* modifying world.json. It is the engine's self-protection layer.
|
|
248
|
-
*
|
|
249
|
-
* - write_file / edit_file targeting .neuroverseos/ → BLOCK
|
|
250
|
-
* - write_file / edit_file targeting .md files → PAUSE
|
|
251
|
-
* - shell commands referencing .neuroverseos/ → BLOCK
|
|
252
|
-
*
|
|
253
|
-
* The PAUSE verdict flows through the existing interactive prompt in the
|
|
254
|
-
* before_tool_call hook. Session overrides ("always") are handled there.
|
|
255
|
-
*/
|
|
256
|
-
checkGovernanceSourceProtection(event) {
|
|
257
|
-
const toolLower = event.tool.toLowerCase();
|
|
258
|
-
const targetPath = event.scope || event.args?.file_path || '';
|
|
259
|
-
const pathLower = targetPath.toLowerCase();
|
|
260
|
-
// ── File-writing tools ──────────────────────────────────
|
|
261
|
-
const isFileWrite = toolLower.includes('write') || toolLower.includes('edit');
|
|
262
|
-
if (isFileWrite && pathLower) {
|
|
263
|
-
// BLOCK: Direct modification of governance state directory
|
|
264
|
-
if (pathLower.includes('.neuroverseos')) {
|
|
265
|
-
return {
|
|
266
|
-
status: 'BLOCK',
|
|
267
|
-
reason: 'Direct modification of governance state is not permitted. Use /world commands to manage governance.',
|
|
268
|
-
ruleId: 'system:governance-source-protection',
|
|
269
|
-
guard: null,
|
|
270
|
-
evidence: `target: ${targetPath}`,
|
|
271
|
-
severity: 'governance',
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
// PAUSE: Modification of governance source files (.md)
|
|
275
|
-
if (pathLower.endsWith('.md')) {
|
|
276
|
-
return {
|
|
277
|
-
status: 'PAUSE',
|
|
278
|
-
reason: 'Governance source modification detected. Changes to .md files alter your World File on next bootstrap. If you did not request this edit, DENY it.',
|
|
279
|
-
ruleId: 'system:governance-source-protection',
|
|
280
|
-
guard: 'governance-source-write',
|
|
281
|
-
evidence: `target: ${targetPath}`,
|
|
282
|
-
severity: 'governance',
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// ── Shell commands ──────────────────────────────────────
|
|
287
|
-
const isShell = toolLower === 'shell' || toolLower === 'bash';
|
|
288
|
-
if (isShell) {
|
|
289
|
-
const command = (event.scope || event.args?.command || '').toLowerCase();
|
|
290
|
-
// BLOCK: shell commands that target governance state directory
|
|
291
|
-
if (command.includes('.neuroverseos')) {
|
|
292
|
-
return {
|
|
293
|
-
status: 'BLOCK',
|
|
294
|
-
reason: 'Shell command targets governance state directory. Use /world commands to manage governance.',
|
|
295
|
-
ruleId: 'system:governance-source-protection',
|
|
296
|
-
guard: null,
|
|
297
|
-
evidence: `command references .neuroverseos/`,
|
|
298
|
-
severity: 'governance',
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Run all integrity checks before the governance pipeline.
|
|
306
|
-
* Returns accumulated alerts. Critical alerts override the verdict.
|
|
307
|
-
*/
|
|
308
|
-
checkIntegrity() {
|
|
309
|
-
const alerts = [];
|
|
310
|
-
let blocked = null;
|
|
311
|
-
// 1. Hash verification — was world.json tampered?
|
|
312
|
-
if (this.metaRecord && this.world) {
|
|
313
|
-
const hashResult = this.verifyWorldHash();
|
|
314
|
-
if (hashResult) {
|
|
315
|
-
alerts.push(hashResult);
|
|
316
|
-
blocked = {
|
|
317
|
-
status: 'BLOCK',
|
|
318
|
-
reason: 'World file integrity check failed. File was modified outside approval pipeline.',
|
|
319
|
-
ruleId: 'system:integrity',
|
|
320
|
-
guard: null,
|
|
321
|
-
evidence: 'hash mismatch',
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// 2. World missing — meta exists but world.json was deleted
|
|
326
|
-
if (this.metaRecord && !this.world) {
|
|
327
|
-
const alert = {
|
|
328
|
-
level: 'critical',
|
|
329
|
-
code: 'WORLD_MISSING',
|
|
330
|
-
message: 'World file was deleted outside the approval pipeline.',
|
|
331
|
-
action: 'Run /world restore to reload the last approved version.',
|
|
332
|
-
};
|
|
333
|
-
alerts.push(alert);
|
|
334
|
-
blocked = {
|
|
335
|
-
status: 'BLOCK',
|
|
336
|
-
reason: 'World file missing. Was deleted outside approval pipeline.',
|
|
337
|
-
ruleId: 'system:integrity',
|
|
338
|
-
guard: null,
|
|
339
|
-
evidence: 'world.json missing, world.meta.json present',
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
// 3. Pending world detection — non-blocking
|
|
343
|
-
if (!blocked) {
|
|
344
|
-
const pendingPath = (0, path_1.join)((0, path_1.dirname)(this.config.worldPath), 'world.pending.json');
|
|
345
|
-
if ((0, fs_1.existsSync)(pendingPath) && !this.surfacedAlerts.has('PENDING_UNAPPROVED')) {
|
|
346
|
-
alerts.push({
|
|
347
|
-
level: 'info',
|
|
348
|
-
code: 'PENDING_UNAPPROVED',
|
|
349
|
-
message: 'Pending governance changes await approval.',
|
|
350
|
-
action: 'Run /world diff to review.',
|
|
351
|
-
});
|
|
352
|
-
this.surfacedAlerts.add('PENDING_UNAPPROVED');
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
// 4. Source drift detection — non-blocking
|
|
356
|
-
if (!blocked && this.workspaceDir && this.world?.metadata?.mdHashes) {
|
|
357
|
-
const driftKey = 'SOURCE_DRIFT';
|
|
358
|
-
if (!this.surfacedAlerts.has(driftKey)) {
|
|
359
|
-
const drift = (0, world_bootstrap_1.checkMdDrift)(this.workspaceDir, this.world.metadata.mdHashes);
|
|
360
|
-
const hasDrift = drift.changed.length > 0 || drift.added.length > 0 || drift.removed.length > 0;
|
|
361
|
-
if (hasDrift) {
|
|
362
|
-
const parts = [];
|
|
363
|
-
if (drift.changed.length > 0)
|
|
364
|
-
parts.push(`modified: ${drift.changed.join(', ')}`);
|
|
365
|
-
if (drift.added.length > 0)
|
|
366
|
-
parts.push(`new: ${drift.added.join(', ')}`);
|
|
367
|
-
if (drift.removed.length > 0)
|
|
368
|
-
parts.push(`removed: ${drift.removed.join(', ')}`);
|
|
369
|
-
alerts.push({
|
|
370
|
-
level: 'warning',
|
|
371
|
-
code: 'SOURCE_DRIFT',
|
|
372
|
-
message: 'Governance source files have changed since last bootstrap.',
|
|
373
|
-
action: 'Run /world bootstrap to review.',
|
|
374
|
-
details: parts.join('; '),
|
|
375
|
-
});
|
|
376
|
-
this.surfacedAlerts.add(driftKey);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return { alerts, blocked };
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Verify world.json hash against stored hash.
|
|
384
|
-
* Uses mtime cache to avoid re-reading on every call.
|
|
385
|
-
*/
|
|
386
|
-
verifyWorldHash() {
|
|
387
|
-
if (!this.metaRecord || !(0, fs_1.existsSync)(this.config.worldPath))
|
|
388
|
-
return null;
|
|
389
|
-
try {
|
|
390
|
-
const stat = (0, fs_1.statSync)(this.config.worldPath);
|
|
391
|
-
const currentMtime = stat.mtimeMs;
|
|
392
|
-
// Only re-hash if file was modified since last check
|
|
393
|
-
if (currentMtime !== this.worldMtimeMs) {
|
|
394
|
-
const raw = (0, fs_1.readFileSync)(this.config.worldPath, 'utf-8');
|
|
395
|
-
const currentWorld = JSON.parse(raw);
|
|
396
|
-
this.cachedWorldHash = this.computeWorldHash(currentWorld);
|
|
397
|
-
this.worldMtimeMs = currentMtime;
|
|
398
|
-
}
|
|
399
|
-
if (this.cachedWorldHash && this.cachedWorldHash !== this.metaRecord.hash) {
|
|
400
|
-
return {
|
|
401
|
-
level: 'critical',
|
|
402
|
-
code: 'TAMPERED',
|
|
403
|
-
message: 'World file integrity check failed. Modified outside approval pipeline.',
|
|
404
|
-
action: 'Run /world restore to reload the last approved version.',
|
|
405
|
-
details: `expected: ${this.metaRecord.hash.slice(0, 12)}..., found: ${this.cachedWorldHash.slice(0, 12)}...`,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
catch {
|
|
410
|
-
// File unreadable — will be caught by other checks
|
|
411
|
-
}
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
// ── Decision Pipeline (spec §3) ───────────────────────────────
|
|
415
|
-
/**
|
|
416
|
-
* Evaluate a tool call event against the loaded world.
|
|
417
|
-
* Returns a deterministic verdict: ALLOW, PAUSE, or BLOCK.
|
|
418
|
-
* Now includes runtime integrity checks (spec §8).
|
|
419
|
-
*/
|
|
420
|
-
evaluate(event) {
|
|
421
|
-
// Step 0: System-level governance source protection (hardcoded, cannot be removed)
|
|
422
|
-
const sourceProtection = this.checkGovernanceSourceProtection(event);
|
|
423
|
-
if (sourceProtection) {
|
|
424
|
-
// Still surface any integrity alerts alongside the protection verdict
|
|
425
|
-
const { alerts } = this.checkIntegrity();
|
|
426
|
-
return alerts.length > 0 ? { ...sourceProtection, alerts } : sourceProtection;
|
|
427
|
-
}
|
|
428
|
-
// Runtime integrity checks run before governance pipeline
|
|
429
|
-
const { alerts, blocked } = this.checkIntegrity();
|
|
430
|
-
if (blocked) {
|
|
431
|
-
return { ...blocked, alerts: alerts.length > 0 ? alerts : undefined };
|
|
432
|
-
}
|
|
433
|
-
if (!this.world)
|
|
434
|
-
return NO_WORLD;
|
|
435
|
-
// Observe-only: run pipeline but always return ALLOW
|
|
436
|
-
if (this.config.observeOnly) {
|
|
437
|
-
const wouldBe = this.runPipeline(event);
|
|
438
|
-
if (wouldBe.status !== 'ALLOW') {
|
|
439
|
-
return {
|
|
440
|
-
status: 'ALLOW',
|
|
441
|
-
reason: `[OBSERVE] Would have been ${wouldBe.status}: ${wouldBe.reason}`,
|
|
442
|
-
ruleId: wouldBe.ruleId,
|
|
443
|
-
guard: wouldBe.guard,
|
|
444
|
-
evidence: wouldBe.evidence,
|
|
445
|
-
alerts: alerts.length > 0 ? alerts : undefined,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
return { ...DEFAULT_ALLOW, alerts: alerts.length > 0 ? alerts : undefined };
|
|
449
|
-
}
|
|
450
|
-
const verdict = this.runPipeline(event);
|
|
451
|
-
if (alerts.length > 0) {
|
|
452
|
-
return { ...verdict, alerts };
|
|
453
|
-
}
|
|
454
|
-
return verdict;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* The actual ordered pipeline. This is the core of governance.
|
|
458
|
-
*/
|
|
459
|
-
runPipeline(event) {
|
|
460
|
-
const world = this.world;
|
|
461
|
-
// 1. Session allowlist — checked in index.ts before calling evaluate
|
|
462
|
-
// (sessionOverrides on the event). We check here too for defense in depth.
|
|
463
|
-
if (event.sessionOverrides.length > 0) {
|
|
464
|
-
// Will be used by guards step — don't short-circuit entirely
|
|
465
|
-
// because invariants can never be overridden
|
|
466
|
-
}
|
|
467
|
-
// 2. Invariants — always evaluated, never overridable
|
|
468
|
-
const invariantVerdict = this.checkInvariants(event, world.invariants);
|
|
469
|
-
if (invariantVerdict)
|
|
470
|
-
return invariantVerdict;
|
|
471
|
-
// 3. Guards — scoped, condition-based
|
|
472
|
-
const guardVerdict = this.checkGuards(event, world.guards);
|
|
473
|
-
if (guardVerdict) {
|
|
474
|
-
// Check if this guard was session-overridden
|
|
475
|
-
if (world.kernel.sessionOverridesAllowed &&
|
|
476
|
-
guardVerdict.ruleId &&
|
|
477
|
-
event.sessionOverrides.includes(guardVerdict.ruleId)) {
|
|
478
|
-
return {
|
|
479
|
-
...DEFAULT_ALLOW,
|
|
480
|
-
reason: `Session override for ${guardVerdict.ruleId}`,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
return guardVerdict;
|
|
484
|
-
}
|
|
485
|
-
// 4. Rules — contextual triggers with effects
|
|
486
|
-
const ruleVerdict = this.checkRules(event, world.rules);
|
|
487
|
-
if (ruleVerdict)
|
|
488
|
-
return ruleVerdict;
|
|
489
|
-
// 5. Role constraints
|
|
490
|
-
const roleVerdict = this.checkRoles(event, world.roles);
|
|
491
|
-
if (roleVerdict)
|
|
492
|
-
return roleVerdict;
|
|
493
|
-
// 6. Default → ALLOW
|
|
494
|
-
return DEFAULT_ALLOW;
|
|
495
|
-
}
|
|
496
|
-
// ── Pipeline Steps ─────────────────────────────────────────────
|
|
497
|
-
/**
|
|
498
|
-
* Step 2: Invariants. Non-toggleable. Non-overridable. Hard BLOCK.
|
|
499
|
-
*/
|
|
500
|
-
checkInvariants(event, invariants) {
|
|
501
|
-
if (!invariants?.length)
|
|
502
|
-
return null;
|
|
503
|
-
for (const inv of invariants) {
|
|
504
|
-
// Scope check
|
|
505
|
-
if (inv.scope.length > 0 && !inv.scope.includes(event.type))
|
|
506
|
-
continue;
|
|
507
|
-
const result = (0, condition_engine_1.evaluateCondition)(inv.condition, event);
|
|
508
|
-
if (result.matched) {
|
|
509
|
-
return {
|
|
510
|
-
status: 'BLOCK',
|
|
511
|
-
reason: inv.description,
|
|
512
|
-
ruleId: `invariant:${inv.id}`,
|
|
513
|
-
guard: null,
|
|
514
|
-
evidence: result.evidence,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Step 3: Guards. Scoped + tool-filtered. Can PAUSE or BLOCK.
|
|
522
|
-
*/
|
|
523
|
-
checkGuards(event, guards) {
|
|
524
|
-
if (!guards?.length)
|
|
525
|
-
return null;
|
|
526
|
-
for (const guard of guards) {
|
|
527
|
-
// Scope check
|
|
528
|
-
if (guard.scope.length > 0 && !guard.scope.includes(event.type))
|
|
529
|
-
continue;
|
|
530
|
-
// Tool filter — if appliesTo is set, only match those tools
|
|
531
|
-
if (guard.appliesTo.length > 0) {
|
|
532
|
-
const toolLower = event.tool.toLowerCase();
|
|
533
|
-
if (!guard.appliesTo.some(t => t.toLowerCase() === toolLower))
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
const result = (0, condition_engine_1.evaluateCondition)(guard.condition, event);
|
|
537
|
-
if (result.matched) {
|
|
538
|
-
return {
|
|
539
|
-
status: guard.enforcement === 'block' ? 'BLOCK' : 'PAUSE',
|
|
540
|
-
reason: guard.description ?? guard.id,
|
|
541
|
-
ruleId: `guard:${guard.id}`,
|
|
542
|
-
guard: guard.id,
|
|
543
|
-
evidence: result.evidence,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Step 4: Rules. Contextual triggers with verdict effects.
|
|
551
|
-
*/
|
|
552
|
-
checkRules(event, rules) {
|
|
553
|
-
if (!rules?.length)
|
|
554
|
-
return null;
|
|
555
|
-
for (const rule of rules) {
|
|
556
|
-
const result = (0, condition_engine_1.evaluateCondition)(rule.trigger, event);
|
|
557
|
-
if (result.matched) {
|
|
558
|
-
const status = rule.effect.verdict.toUpperCase();
|
|
559
|
-
if (status !== 'ALLOW') {
|
|
560
|
-
return {
|
|
561
|
-
status,
|
|
562
|
-
reason: rule.description ?? rule.id,
|
|
563
|
-
ruleId: `rule:${rule.id}`,
|
|
564
|
-
guard: null,
|
|
565
|
-
evidence: result.evidence,
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* Step 5: Role constraints. cannotDo = BLOCK, requiresApproval = PAUSE.
|
|
574
|
-
*/
|
|
575
|
-
checkRoles(event, roles) {
|
|
576
|
-
if (!roles?.length || !event.role)
|
|
577
|
-
return null;
|
|
578
|
-
const role = roles.find(r => r.id === event.role || r.name.toLowerCase() === event.role?.toLowerCase());
|
|
579
|
-
if (!role)
|
|
580
|
-
return null;
|
|
581
|
-
// Check cannotDo — hard BLOCK
|
|
582
|
-
const toolLower = event.tool.toLowerCase();
|
|
583
|
-
if (role.cannotDo.some(action => toolLower.includes(action.toLowerCase()))) {
|
|
584
|
-
return {
|
|
585
|
-
status: 'BLOCK',
|
|
586
|
-
reason: `Role "${role.name}" cannot perform "${event.tool}"`,
|
|
587
|
-
ruleId: `role:${role.id}`,
|
|
588
|
-
guard: null,
|
|
589
|
-
evidence: `tool "${event.tool}" in cannotDo list`,
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
// Check requiresApproval — PAUSE
|
|
593
|
-
if (role.requiresApproval) {
|
|
594
|
-
return {
|
|
595
|
-
status: 'PAUSE',
|
|
596
|
-
reason: `Role "${role.name}" requires approval for all actions`,
|
|
597
|
-
ruleId: `role:${role.id}`,
|
|
598
|
-
guard: null,
|
|
599
|
-
evidence: `role requires approval`,
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
// ── Amendment Application ──────────────────────────────────────
|
|
605
|
-
/**
|
|
606
|
-
* Apply an approved amendment to the world.
|
|
607
|
-
*/
|
|
608
|
-
applyAmendment(proposal) {
|
|
609
|
-
if (!this.world)
|
|
610
|
-
return;
|
|
611
|
-
switch (proposal.type) {
|
|
612
|
-
case 'add_invariant':
|
|
613
|
-
this.world.invariants.push({
|
|
614
|
-
id: `inv-${Date.now().toString(36)}`,
|
|
615
|
-
description: proposal.reason,
|
|
616
|
-
scope: ['tool_call'],
|
|
617
|
-
condition: {
|
|
618
|
-
field: 'intent',
|
|
619
|
-
operator: 'contains',
|
|
620
|
-
value: proposal.suggestion.toLowerCase(),
|
|
621
|
-
},
|
|
622
|
-
enforcement: 'block',
|
|
623
|
-
});
|
|
624
|
-
break;
|
|
625
|
-
case 'add_guard':
|
|
626
|
-
this.world.guards.push({
|
|
627
|
-
id: `guard-${Date.now().toString(36)}`,
|
|
628
|
-
description: proposal.reason,
|
|
629
|
-
scope: ['tool_call'],
|
|
630
|
-
appliesTo: [],
|
|
631
|
-
condition: {
|
|
632
|
-
field: 'intent',
|
|
633
|
-
operator: 'contains',
|
|
634
|
-
value: proposal.suggestion.toLowerCase(),
|
|
635
|
-
},
|
|
636
|
-
enforcement: 'pause',
|
|
637
|
-
requiresApproval: true,
|
|
638
|
-
});
|
|
639
|
-
break;
|
|
640
|
-
case 'add_role':
|
|
641
|
-
this.world.roles.push({
|
|
642
|
-
id: `role-${Date.now().toString(36)}`,
|
|
643
|
-
name: proposal.suggestion,
|
|
644
|
-
canDo: [],
|
|
645
|
-
cannotDo: [],
|
|
646
|
-
requiresApproval: false,
|
|
647
|
-
});
|
|
648
|
-
break;
|
|
649
|
-
case 'modify_threshold':
|
|
650
|
-
// Convert guard enforcement from pause→block or block→pause
|
|
651
|
-
const targetGuard = this.world.guards.find(g => g.id === proposal.suggestion || g.description === proposal.suggestion);
|
|
652
|
-
if (targetGuard) {
|
|
653
|
-
targetGuard.enforcement = targetGuard.enforcement === 'pause' ? 'block' : 'pause';
|
|
654
|
-
}
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
exports.GovernanceEngine = GovernanceEngine;
|
|
660
|
-
//# sourceMappingURL=governance-engine.js.map
|