@neuroverseos/governance 0.4.3 → 0.5.1
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/README.md +189 -0
- package/dist/adapters/autoresearch.js +2 -2
- package/dist/adapters/deep-agents.js +2 -2
- package/dist/adapters/express.js +2 -2
- package/dist/adapters/github.js +2 -2
- package/dist/adapters/index.js +23 -21
- package/dist/adapters/langchain.js +2 -2
- package/dist/adapters/mentraos.js +8 -6
- package/dist/adapters/openai.js +2 -2
- package/dist/adapters/openclaw.js +2 -2
- package/dist/{add-XSANI3FK.js → add-JP7TC2K3.js} +1 -1
- package/dist/admin/index.cjs +2214 -0
- package/dist/admin/index.d.cts +362 -0
- package/dist/admin/index.d.ts +362 -0
- package/dist/admin/index.js +703 -0
- package/dist/{build-EGBGZFIJ.js → build-THUEYMVT.js} +5 -5
- package/dist/{chunk-YJ34R5NB.js → chunk-5RAQ5DZW.js} +3 -3
- package/dist/{chunk-RDA7ISWC.js → chunk-6UPEUMJ2.js} +3 -3
- package/dist/chunk-7UU7V3AD.js +447 -0
- package/dist/{chunk-ZEIT2QLM.js → chunk-EK77AJAH.js} +22 -4
- package/dist/{chunk-3S5AD4AB.js → chunk-FGOSKQDE.js} +3 -3
- package/dist/{chunk-GTPV2XGO.js → chunk-GJ6LM4JZ.js} +1 -441
- package/dist/chunk-H3REGQRI.js +107 -0
- package/dist/{chunk-J2IZBHXJ.js → chunk-LAKUB76X.js} +3 -3
- package/dist/{chunk-FVOGUCB6.js → chunk-R23T5SZG.js} +3 -3
- package/dist/{chunk-A7SHG75T.js → chunk-RF2L5SYG.js} +3 -3
- package/dist/{chunk-QMVQ6KPL.js → chunk-TL4DLMMW.js} +3 -3
- package/dist/{chunk-AV7XJJWK.js → chunk-TZBERHFM.js} +3 -3
- package/dist/{chunk-3AYKQHYI.js → chunk-UZBW44KD.js} +3 -3
- package/dist/{chunk-FS2UUJJO.js → chunk-XPMZB46F.js} +3 -3
- package/dist/cli/neuroverse.cjs +962 -284
- package/dist/cli/neuroverse.js +46 -22
- package/dist/cli/plan.js +1 -1
- package/dist/cli/run.cjs +242 -139
- package/dist/cli/run.js +23 -3
- package/dist/{demo-6OQYWRR6.js → demo-N5K4VXJW.js} +3 -3
- package/dist/{derive-7Y7YWVLU.js → derive-5LOMN7GO.js} +4 -4
- package/dist/{equity-penalties-NVBAB5WL.js → equity-penalties-PYCJ3Q4U.js} +6 -6
- package/dist/{explain-HDFN4ION.js → explain-42TVC3QD.js} +1 -1
- package/dist/{guard-6KSCWT2W.js → guard-TPYDFG6V.js} +16 -4
- package/dist/{improve-2PWGGO5B.js → improve-HLZGJ54Z.js} +3 -3
- package/dist/index.cjs +19 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -27
- package/dist/keygen-BSZH3NM2.js +77 -0
- package/dist/{lens-MHMUDCMQ.js → lens-NFGZHD76.js} +1 -1
- package/dist/{mcp-server-TNIWZ7B5.js → mcp-server-5XXNG6VC.js} +2 -2
- package/dist/migrate-NH5PVMX4.js +221 -0
- package/dist/{playground-3FLDGBET.js → playground-2EU5CFIH.js} +4 -4
- package/dist/{redteam-HV6LMKEH.js → redteam-VK6OVHAE.js} +3 -3
- package/dist/{session-XZP2754M.js → session-NGA4DUPL.js} +2 -2
- package/dist/sign-RRELHKWM.js +11 -0
- package/dist/{simulate-VT437EEL.js → simulate-4YNOBMES.js} +1 -1
- package/dist/{test-4WTX6RKQ.js → test-HDBPMQTG.js} +3 -3
- package/dist/{validate-M52DX22Y.js → validate-6MFQZ2EG.js} +1 -1
- package/dist/verify-6AVTWX75.js +151 -0
- package/dist/{world-O4HTQPDP.js → world-H5WVURKU.js} +1 -1
- package/dist/{world-loader-YTYFOP7D.js → world-loader-J47PCPDZ.js} +1 -1
- package/package.json +22 -10
- package/dist/{behavioral-SLW7ALEK.js → behavioral-SPWPGYXL.js} +3 -3
- package/dist/{bootstrap-2OW5ZLBL.js → bootstrap-IP5QMC3Q.js} +3 -3
- package/dist/{chunk-I4RTIMLX.js → chunk-EQUAWNXW.js} +0 -0
- package/dist/{chunk-DA5MHFRR.js → chunk-NTHXZAW4.js} +3 -3
- package/dist/{chunk-FHXXD2TI.js → chunk-QZ666FCV.js} +6 -6
- package/dist/{configure-ai-LL3VAPQW.js → configure-ai-5MP5DWTT.js} +3 -3
- package/dist/{decision-flow-3K4D72G4.js → decision-flow-IJPNMVQK.js} +3 -3
- /package/dist/{doctor-EC5OYTI3.js → doctor-Q5APJOTS.js} +0 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MENTRA_INTENT_TAXONOMY
|
|
3
|
+
} from "../chunk-GJ6LM4JZ.js";
|
|
4
|
+
import {
|
|
5
|
+
evaluateGuard
|
|
6
|
+
} from "../chunk-ZAF6JH23.js";
|
|
7
|
+
import "../chunk-QLPTHTVB.js";
|
|
8
|
+
import "../chunk-QWGCMQQD.js";
|
|
9
|
+
|
|
10
|
+
// src/admin/simulator.ts
|
|
11
|
+
function zonePolicyToVerdict(policy) {
|
|
12
|
+
switch (policy) {
|
|
13
|
+
case "allow":
|
|
14
|
+
return "allow";
|
|
15
|
+
case "block":
|
|
16
|
+
return "block";
|
|
17
|
+
case "confirm_each":
|
|
18
|
+
return "pause";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function intentMatchesPattern(intent, pattern) {
|
|
22
|
+
if (pattern.endsWith("*")) {
|
|
23
|
+
return intent.startsWith(pattern.slice(0, -1));
|
|
24
|
+
}
|
|
25
|
+
return intent === pattern;
|
|
26
|
+
}
|
|
27
|
+
function evaluateIntentAgainstZone(intentDef, zone) {
|
|
28
|
+
const rules = zone.rules;
|
|
29
|
+
const domain = intentDef.domain;
|
|
30
|
+
for (const custom of rules.customRules) {
|
|
31
|
+
if (intentMatchesPattern(intentDef.intent, custom.intentPattern)) {
|
|
32
|
+
return custom.action === "confirm" ? "pause" : custom.action;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (domain === "camera") return zonePolicyToVerdict(rules.camera);
|
|
36
|
+
if (domain === "microphone") return zonePolicyToVerdict(rules.microphone);
|
|
37
|
+
if (domain === "ai_data" || domain === "ai_action") {
|
|
38
|
+
if (intentDef.intent.includes("auto_purchase") || intentDef.intent.includes("auto_respond")) {
|
|
39
|
+
return zonePolicyToVerdict(rules.aiActions);
|
|
40
|
+
}
|
|
41
|
+
if (intentDef.intent.includes("retain") || intentDef.intent.includes("share")) {
|
|
42
|
+
return zonePolicyToVerdict(rules.dataRetention);
|
|
43
|
+
}
|
|
44
|
+
return zonePolicyToVerdict(rules.aiDataSend);
|
|
45
|
+
}
|
|
46
|
+
if (domain === "location") return zonePolicyToVerdict(rules.locationSharing);
|
|
47
|
+
return "allow";
|
|
48
|
+
}
|
|
49
|
+
function evaluateIntentAgainstRole(intentDef, role) {
|
|
50
|
+
const def = role.definition;
|
|
51
|
+
for (const pattern of def.cannotDo) {
|
|
52
|
+
if (intentDef.intent.includes(pattern) || intentDef.description.toLowerCase().includes(pattern.toLowerCase()) || intentDef.domain === pattern) {
|
|
53
|
+
return "block";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (def.requiresApproval) {
|
|
57
|
+
return "pause";
|
|
58
|
+
}
|
|
59
|
+
if (def.canDo.length > 0) {
|
|
60
|
+
const allowed = def.canDo.some(
|
|
61
|
+
(pattern) => intentDef.intent.includes(pattern) || intentDef.description.toLowerCase().includes(pattern.toLowerCase()) || intentDef.domain === pattern
|
|
62
|
+
);
|
|
63
|
+
if (!allowed) return "block";
|
|
64
|
+
}
|
|
65
|
+
return "allow";
|
|
66
|
+
}
|
|
67
|
+
function mergeVerdicts(...verdicts) {
|
|
68
|
+
if (verdicts.includes("block")) return "block";
|
|
69
|
+
if (verdicts.includes("pause")) return "pause";
|
|
70
|
+
if (verdicts.includes("modify")) return "modify";
|
|
71
|
+
return "allow";
|
|
72
|
+
}
|
|
73
|
+
function simulatePolicy(request, currentRoles, currentZones, platformWorld) {
|
|
74
|
+
const intentsToTest = request.intents ? MENTRA_INTENT_TAXONOMY.filter((i) => request.intents.includes(i.intent)) : MENTRA_INTENT_TAXONOMY;
|
|
75
|
+
const verdicts = [];
|
|
76
|
+
const conflicts = [];
|
|
77
|
+
for (const intentDef of intentsToTest) {
|
|
78
|
+
let currentVerdict;
|
|
79
|
+
if (request.type !== "full_matrix") {
|
|
80
|
+
const currentVerdictParts = [];
|
|
81
|
+
if (request.targetZoneId) {
|
|
82
|
+
const currentZone = currentZones.find((z) => z.id === request.targetZoneId);
|
|
83
|
+
if (currentZone) {
|
|
84
|
+
currentVerdictParts.push(evaluateIntentAgainstZone(intentDef, currentZone));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (request.targetRoleId) {
|
|
88
|
+
const currentRole = currentRoles.find((r) => r.id === request.targetRoleId);
|
|
89
|
+
if (currentRole) {
|
|
90
|
+
currentVerdictParts.push(evaluateIntentAgainstRole(intentDef, currentRole));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
currentVerdict = currentVerdictParts.length > 0 ? mergeVerdicts(...currentVerdictParts) : "allow";
|
|
94
|
+
}
|
|
95
|
+
const proposedParts = [];
|
|
96
|
+
if (request.proposed?.zone) {
|
|
97
|
+
proposedParts.push(evaluateIntentAgainstZone(intentDef, request.proposed.zone));
|
|
98
|
+
} else if (request.targetZoneId) {
|
|
99
|
+
const existingZone = currentZones.find((z) => z.id === request.targetZoneId);
|
|
100
|
+
if (existingZone) {
|
|
101
|
+
proposedParts.push(evaluateIntentAgainstZone(intentDef, existingZone));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (request.proposed?.role) {
|
|
105
|
+
proposedParts.push(evaluateIntentAgainstRole(intentDef, request.proposed.role));
|
|
106
|
+
} else if (request.targetRoleId) {
|
|
107
|
+
const existingRole = currentRoles.find((r) => r.id === request.targetRoleId);
|
|
108
|
+
if (existingRole) {
|
|
109
|
+
proposedParts.push(evaluateIntentAgainstRole(intentDef, existingRole));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (request.type === "full_matrix") {
|
|
113
|
+
for (const zone of currentZones) {
|
|
114
|
+
proposedParts.push(evaluateIntentAgainstZone(intentDef, zone));
|
|
115
|
+
}
|
|
116
|
+
for (const role of currentRoles) {
|
|
117
|
+
proposedParts.push(evaluateIntentAgainstRole(intentDef, role));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (platformWorld) {
|
|
121
|
+
try {
|
|
122
|
+
const event = {
|
|
123
|
+
intent: intentDef.intent,
|
|
124
|
+
scope: intentDef.domain,
|
|
125
|
+
actionCategory: intentDef.action_category
|
|
126
|
+
};
|
|
127
|
+
const guardResult = evaluateGuard(event, platformWorld, {});
|
|
128
|
+
if (guardResult.status !== "ALLOW") {
|
|
129
|
+
proposedParts.push(
|
|
130
|
+
guardResult.status === "BLOCK" ? "block" : guardResult.status === "PAUSE" ? "pause" : guardResult.status === "MODIFY" ? "modify" : "allow"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const proposedVerdict = proposedParts.length > 0 ? mergeVerdicts(...proposedParts) : "allow";
|
|
137
|
+
const changed = currentVerdict !== void 0 && currentVerdict !== proposedVerdict;
|
|
138
|
+
verdicts.push({
|
|
139
|
+
intent: intentDef.intent,
|
|
140
|
+
description: intentDef.description,
|
|
141
|
+
currentVerdict,
|
|
142
|
+
proposedVerdict,
|
|
143
|
+
changed,
|
|
144
|
+
reason: buildReason(intentDef, proposedVerdict, request)
|
|
145
|
+
});
|
|
146
|
+
if (changed && currentVerdict === "allow" && proposedVerdict === "block") {
|
|
147
|
+
conflicts.push({
|
|
148
|
+
description: `"${intentDef.description}" was allowed but would now be blocked`,
|
|
149
|
+
severity: intentDef.base_risk === "low" ? "warning" : "error",
|
|
150
|
+
intent: intentDef.intent,
|
|
151
|
+
suggestion: `Verify that blocking ${intentDef.intent} won't break apps that depend on it`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const summary = {
|
|
156
|
+
total: verdicts.length,
|
|
157
|
+
allowed: verdicts.filter((v) => v.proposedVerdict === "allow").length,
|
|
158
|
+
blocked: verdicts.filter((v) => v.proposedVerdict === "block").length,
|
|
159
|
+
paused: verdicts.filter((v) => v.proposedVerdict === "pause").length,
|
|
160
|
+
modified: verdicts.filter((v) => v.proposedVerdict === "modify").length
|
|
161
|
+
};
|
|
162
|
+
const diff = request.type !== "full_matrix" ? verdicts.filter((v) => v.changed).map((v) => ({
|
|
163
|
+
intent: v.intent,
|
|
164
|
+
from: v.currentVerdict,
|
|
165
|
+
to: v.proposedVerdict,
|
|
166
|
+
explanation: `${v.description}: ${v.currentVerdict} \u2192 ${v.proposedVerdict}`
|
|
167
|
+
})) : void 0;
|
|
168
|
+
return {
|
|
169
|
+
request,
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
verdicts,
|
|
172
|
+
summary,
|
|
173
|
+
conflicts,
|
|
174
|
+
diff
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function simulateFullMatrix(roles, zones, platformWorld) {
|
|
178
|
+
const matrix = /* @__PURE__ */ new Map();
|
|
179
|
+
for (const role of roles) {
|
|
180
|
+
const roleResults = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const zone of zones) {
|
|
182
|
+
const result = simulatePolicy(
|
|
183
|
+
{
|
|
184
|
+
type: "full_matrix",
|
|
185
|
+
proposed: { role, zone }
|
|
186
|
+
},
|
|
187
|
+
[],
|
|
188
|
+
[],
|
|
189
|
+
platformWorld
|
|
190
|
+
);
|
|
191
|
+
roleResults.set(zone.id, result.verdicts);
|
|
192
|
+
}
|
|
193
|
+
matrix.set(role.id, roleResults);
|
|
194
|
+
}
|
|
195
|
+
return matrix;
|
|
196
|
+
}
|
|
197
|
+
function buildReason(intentDef, verdict, request) {
|
|
198
|
+
if (verdict === "block") {
|
|
199
|
+
if (request.proposed?.zone) {
|
|
200
|
+
return `Blocked by zone "${request.proposed.zone.name}" policy`;
|
|
201
|
+
}
|
|
202
|
+
if (request.proposed?.role) {
|
|
203
|
+
return `Blocked by role "${request.proposed.role.name}" restrictions`;
|
|
204
|
+
}
|
|
205
|
+
return "Blocked by combined policy";
|
|
206
|
+
}
|
|
207
|
+
if (verdict === "pause") {
|
|
208
|
+
return "Requires user confirmation before proceeding";
|
|
209
|
+
}
|
|
210
|
+
if (verdict === "modify") {
|
|
211
|
+
return "Allowed with modifications";
|
|
212
|
+
}
|
|
213
|
+
return "Allowed by current policy";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/admin/manager.ts
|
|
217
|
+
var AUTHORITY_RANK = {
|
|
218
|
+
viewer: 0,
|
|
219
|
+
operator: 1,
|
|
220
|
+
supervisor: 2,
|
|
221
|
+
manager: 3,
|
|
222
|
+
admin: 4
|
|
223
|
+
};
|
|
224
|
+
function hasAuthority(actorLevel, requiredLevel) {
|
|
225
|
+
return AUTHORITY_RANK[actorLevel] >= AUTHORITY_RANK[requiredLevel];
|
|
226
|
+
}
|
|
227
|
+
var GovernanceAdmin = class {
|
|
228
|
+
storage;
|
|
229
|
+
platformWorld;
|
|
230
|
+
constructor(storage, platformWorld) {
|
|
231
|
+
this.storage = storage;
|
|
232
|
+
this.platformWorld = platformWorld;
|
|
233
|
+
}
|
|
234
|
+
// ── Authority Check ───────────────────────────────────────────────────
|
|
235
|
+
async getActorAuthority(actorRoleId) {
|
|
236
|
+
const chain = await this.storage.getAuthorityChain();
|
|
237
|
+
const grant = chain.grants.find((g) => g.roleId === actorRoleId);
|
|
238
|
+
return grant?.authorityLevel ?? "viewer";
|
|
239
|
+
}
|
|
240
|
+
async checkAuthority(actorRoleId, requiredLevel, locationId) {
|
|
241
|
+
const chain = await this.storage.getAuthorityChain();
|
|
242
|
+
const grant = chain.grants.find((g) => g.roleId === actorRoleId);
|
|
243
|
+
if (!grant || !hasAuthority(grant.authorityLevel, requiredLevel)) {
|
|
244
|
+
throw new GovernanceAdminError(
|
|
245
|
+
`Insufficient authority. Requires ${requiredLevel}, role "${actorRoleId}" has ${grant?.authorityLevel ?? "viewer"}`,
|
|
246
|
+
"INSUFFICIENT_AUTHORITY"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (locationId && grant.locationScope && grant.locationScope.length > 0) {
|
|
250
|
+
if (!grant.locationScope.includes(locationId)) {
|
|
251
|
+
throw new GovernanceAdminError(
|
|
252
|
+
`Role "${actorRoleId}" does not have authority over location "${locationId}"`,
|
|
253
|
+
"LOCATION_SCOPE_DENIED"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async audit(action, actorId, actorRole, targetType, targetId, summary, changes) {
|
|
259
|
+
await this.storage.appendAudit({
|
|
260
|
+
id: `audit_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
261
|
+
action,
|
|
262
|
+
actorId,
|
|
263
|
+
actorRole,
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
targetType,
|
|
266
|
+
targetId,
|
|
267
|
+
changes,
|
|
268
|
+
summary
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// ── Roles ─────────────────────────────────────────────────────────────
|
|
272
|
+
async createRole(role, actorId, actorRoleId) {
|
|
273
|
+
await this.checkAuthority(actorRoleId, "manager");
|
|
274
|
+
const existing = await this.storage.getRole(role.id);
|
|
275
|
+
if (existing) {
|
|
276
|
+
throw new GovernanceAdminError(
|
|
277
|
+
`Role "${role.id}" already exists`,
|
|
278
|
+
"ROLE_EXISTS"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
const newRole = {
|
|
283
|
+
...role,
|
|
284
|
+
createdBy: actorId,
|
|
285
|
+
createdAt: now,
|
|
286
|
+
updatedAt: now,
|
|
287
|
+
active: true
|
|
288
|
+
};
|
|
289
|
+
await this.storage.saveRole(newRole);
|
|
290
|
+
await this.audit(
|
|
291
|
+
"role_created",
|
|
292
|
+
actorId,
|
|
293
|
+
actorRoleId,
|
|
294
|
+
"role",
|
|
295
|
+
role.id,
|
|
296
|
+
`Created role "${role.name}"`,
|
|
297
|
+
{ after: newRole }
|
|
298
|
+
);
|
|
299
|
+
return newRole;
|
|
300
|
+
}
|
|
301
|
+
async updateRole(roleId, updates, actorId, actorRoleId) {
|
|
302
|
+
await this.checkAuthority(actorRoleId, "manager");
|
|
303
|
+
const existing = await this.storage.getRole(roleId);
|
|
304
|
+
if (!existing) {
|
|
305
|
+
throw new GovernanceAdminError(`Role "${roleId}" not found`, "ROLE_NOT_FOUND");
|
|
306
|
+
}
|
|
307
|
+
const updated = {
|
|
308
|
+
...existing,
|
|
309
|
+
...updates,
|
|
310
|
+
id: roleId,
|
|
311
|
+
// prevent ID mutation
|
|
312
|
+
updatedAt: Date.now()
|
|
313
|
+
};
|
|
314
|
+
await this.storage.saveRole(updated);
|
|
315
|
+
await this.audit(
|
|
316
|
+
"role_updated",
|
|
317
|
+
actorId,
|
|
318
|
+
actorRoleId,
|
|
319
|
+
"role",
|
|
320
|
+
roleId,
|
|
321
|
+
`Updated role "${roleId}"`,
|
|
322
|
+
{
|
|
323
|
+
before: existing,
|
|
324
|
+
after: updated
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
return updated;
|
|
328
|
+
}
|
|
329
|
+
async deleteRole(roleId, actorId, actorRoleId) {
|
|
330
|
+
await this.checkAuthority(actorRoleId, "admin");
|
|
331
|
+
const existing = await this.storage.getRole(roleId);
|
|
332
|
+
if (!existing) {
|
|
333
|
+
throw new GovernanceAdminError(`Role "${roleId}" not found`, "ROLE_NOT_FOUND");
|
|
334
|
+
}
|
|
335
|
+
const assignments = await this.storage.getAssignmentsByRole(roleId);
|
|
336
|
+
if (assignments.length > 0) {
|
|
337
|
+
throw new GovernanceAdminError(
|
|
338
|
+
`Cannot delete role "${roleId}" \u2014 ${assignments.length} device(s) still assigned. Unassign them first.`,
|
|
339
|
+
"ROLE_IN_USE"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
await this.storage.deleteRole(roleId);
|
|
343
|
+
await this.audit(
|
|
344
|
+
"role_deleted",
|
|
345
|
+
actorId,
|
|
346
|
+
actorRoleId,
|
|
347
|
+
"role",
|
|
348
|
+
roleId,
|
|
349
|
+
`Deleted role "${existing.name}"`,
|
|
350
|
+
{ before: existing }
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
async listRoles() {
|
|
354
|
+
return this.storage.getRoles();
|
|
355
|
+
}
|
|
356
|
+
async getRole(roleId) {
|
|
357
|
+
return this.storage.getRole(roleId);
|
|
358
|
+
}
|
|
359
|
+
// ── Assignments ───────────────────────────────────────────────────────
|
|
360
|
+
async assignRole(assignment, actorId, actorRoleId) {
|
|
361
|
+
await this.checkAuthority(actorRoleId, "operator");
|
|
362
|
+
const role = await this.storage.getRole(assignment.roleId);
|
|
363
|
+
if (!role) {
|
|
364
|
+
throw new GovernanceAdminError(
|
|
365
|
+
`Role "${assignment.roleId}" not found`,
|
|
366
|
+
"ROLE_NOT_FOUND"
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const full = {
|
|
370
|
+
...assignment,
|
|
371
|
+
id: `assign_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
372
|
+
assignedAt: Date.now()
|
|
373
|
+
};
|
|
374
|
+
await this.storage.saveAssignment(full);
|
|
375
|
+
await this.audit(
|
|
376
|
+
"role_assigned",
|
|
377
|
+
actorId,
|
|
378
|
+
actorRoleId,
|
|
379
|
+
"assignment",
|
|
380
|
+
full.id,
|
|
381
|
+
`Assigned role "${assignment.roleId}" to device "${assignment.deviceId}"` + (assignment.employeeName ? ` (${assignment.employeeName})` : "")
|
|
382
|
+
);
|
|
383
|
+
return full;
|
|
384
|
+
}
|
|
385
|
+
async unassignRole(assignmentId, actorId, actorRoleId) {
|
|
386
|
+
await this.checkAuthority(actorRoleId, "operator");
|
|
387
|
+
await this.storage.deleteAssignment(assignmentId);
|
|
388
|
+
await this.audit(
|
|
389
|
+
"role_unassigned",
|
|
390
|
+
actorId,
|
|
391
|
+
actorRoleId,
|
|
392
|
+
"assignment",
|
|
393
|
+
assignmentId,
|
|
394
|
+
`Removed role assignment "${assignmentId}"`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
async getDeviceRole(deviceId) {
|
|
398
|
+
const assignments = await this.storage.getAssignmentsByDevice(deviceId);
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
const active = assignments.filter(
|
|
401
|
+
(a) => !a.expiresAt || a.expiresAt > now
|
|
402
|
+
);
|
|
403
|
+
return active.sort((a, b) => b.assignedAt - a.assignedAt)[0] ?? null;
|
|
404
|
+
}
|
|
405
|
+
async listAssignments() {
|
|
406
|
+
return this.storage.getAssignments();
|
|
407
|
+
}
|
|
408
|
+
// ── Zones ─────────────────────────────────────────────────────────────
|
|
409
|
+
async createZone(zone, actorId, actorRoleId) {
|
|
410
|
+
await this.checkAuthority(actorRoleId, "manager", zone.locationId);
|
|
411
|
+
const existing = await this.storage.getZone(zone.id);
|
|
412
|
+
if (existing) {
|
|
413
|
+
throw new GovernanceAdminError(
|
|
414
|
+
`Zone "${zone.id}" already exists`,
|
|
415
|
+
"ZONE_EXISTS"
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
const newZone = {
|
|
420
|
+
...zone,
|
|
421
|
+
createdBy: actorId,
|
|
422
|
+
createdAt: now,
|
|
423
|
+
updatedAt: now,
|
|
424
|
+
active: true
|
|
425
|
+
};
|
|
426
|
+
await this.storage.saveZone(newZone);
|
|
427
|
+
await this.audit(
|
|
428
|
+
"zone_created",
|
|
429
|
+
actorId,
|
|
430
|
+
actorRoleId,
|
|
431
|
+
"zone",
|
|
432
|
+
zone.id,
|
|
433
|
+
`Created zone "${zone.name}" at location "${zone.locationId}"`,
|
|
434
|
+
{ after: newZone }
|
|
435
|
+
);
|
|
436
|
+
return newZone;
|
|
437
|
+
}
|
|
438
|
+
async updateZone(zoneId, updates, actorId, actorRoleId) {
|
|
439
|
+
const existing = await this.storage.getZone(zoneId);
|
|
440
|
+
if (!existing) {
|
|
441
|
+
throw new GovernanceAdminError(`Zone "${zoneId}" not found`, "ZONE_NOT_FOUND");
|
|
442
|
+
}
|
|
443
|
+
await this.checkAuthority(actorRoleId, "manager", existing.locationId);
|
|
444
|
+
const updated = {
|
|
445
|
+
...existing,
|
|
446
|
+
...updates,
|
|
447
|
+
id: zoneId,
|
|
448
|
+
updatedAt: Date.now()
|
|
449
|
+
};
|
|
450
|
+
await this.storage.saveZone(updated);
|
|
451
|
+
await this.audit(
|
|
452
|
+
"zone_updated",
|
|
453
|
+
actorId,
|
|
454
|
+
actorRoleId,
|
|
455
|
+
"zone",
|
|
456
|
+
zoneId,
|
|
457
|
+
`Updated zone "${zoneId}"`,
|
|
458
|
+
{
|
|
459
|
+
before: existing,
|
|
460
|
+
after: updated
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
return updated;
|
|
464
|
+
}
|
|
465
|
+
async deleteZone(zoneId, actorId, actorRoleId) {
|
|
466
|
+
const existing = await this.storage.getZone(zoneId);
|
|
467
|
+
if (!existing) {
|
|
468
|
+
throw new GovernanceAdminError(`Zone "${zoneId}" not found`, "ZONE_NOT_FOUND");
|
|
469
|
+
}
|
|
470
|
+
await this.checkAuthority(actorRoleId, "admin", existing.locationId);
|
|
471
|
+
await this.storage.deleteZone(zoneId);
|
|
472
|
+
await this.audit(
|
|
473
|
+
"zone_deleted",
|
|
474
|
+
actorId,
|
|
475
|
+
actorRoleId,
|
|
476
|
+
"zone",
|
|
477
|
+
zoneId,
|
|
478
|
+
`Deleted zone "${existing.name}"`,
|
|
479
|
+
{ before: existing }
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
async listZones(locationId) {
|
|
483
|
+
if (locationId) {
|
|
484
|
+
return this.storage.getZonesByLocation(locationId);
|
|
485
|
+
}
|
|
486
|
+
return this.storage.getZones();
|
|
487
|
+
}
|
|
488
|
+
async getZone(zoneId) {
|
|
489
|
+
return this.storage.getZone(zoneId);
|
|
490
|
+
}
|
|
491
|
+
// ── Authority ─────────────────────────────────────────────────────────
|
|
492
|
+
async updateAuthority(chain, actorId, actorRoleId) {
|
|
493
|
+
await this.checkAuthority(actorRoleId, "admin");
|
|
494
|
+
const previous = await this.storage.getAuthorityChain();
|
|
495
|
+
const enforced = {
|
|
496
|
+
...chain,
|
|
497
|
+
emergencyOverrideAlwaysAllowed: true
|
|
498
|
+
};
|
|
499
|
+
await this.storage.saveAuthorityChain(enforced);
|
|
500
|
+
await this.audit(
|
|
501
|
+
"authority_updated",
|
|
502
|
+
actorId,
|
|
503
|
+
actorRoleId,
|
|
504
|
+
"authority",
|
|
505
|
+
"chain",
|
|
506
|
+
"Updated authority chain",
|
|
507
|
+
{
|
|
508
|
+
before: previous,
|
|
509
|
+
after: enforced
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
return enforced;
|
|
513
|
+
}
|
|
514
|
+
async getAuthority() {
|
|
515
|
+
return this.storage.getAuthorityChain();
|
|
516
|
+
}
|
|
517
|
+
// ── Simulation ────────────────────────────────────────────────────────
|
|
518
|
+
async simulate(request, actorId, actorRoleId) {
|
|
519
|
+
const roles = await this.storage.getRoles();
|
|
520
|
+
const zones = await this.storage.getZones();
|
|
521
|
+
const result = simulatePolicy(
|
|
522
|
+
request,
|
|
523
|
+
roles,
|
|
524
|
+
zones,
|
|
525
|
+
this.platformWorld
|
|
526
|
+
);
|
|
527
|
+
await this.audit(
|
|
528
|
+
"simulation_run",
|
|
529
|
+
actorId,
|
|
530
|
+
actorRoleId,
|
|
531
|
+
"simulation",
|
|
532
|
+
request.type,
|
|
533
|
+
`Ran ${request.type} simulation: ${result.summary.total} intents evaluated, ${result.summary.blocked} blocked, ${result.conflicts.length} conflicts`
|
|
534
|
+
);
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
async simulateMatrix(actorId, actorRoleId) {
|
|
538
|
+
const roles = await this.storage.getRoles();
|
|
539
|
+
const zones = await this.storage.getZones();
|
|
540
|
+
const result = simulateFullMatrix(roles, zones, this.platformWorld);
|
|
541
|
+
await this.audit(
|
|
542
|
+
"simulation_run",
|
|
543
|
+
actorId,
|
|
544
|
+
actorRoleId,
|
|
545
|
+
"simulation",
|
|
546
|
+
"full_matrix",
|
|
547
|
+
`Ran full matrix simulation: ${roles.length} roles \xD7 ${zones.length} zones`
|
|
548
|
+
);
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Simulate a proposed role change and return only what would break.
|
|
553
|
+
* The "measure twice" before deploying.
|
|
554
|
+
*/
|
|
555
|
+
async simulateRoleChange(roleId, proposedDefinitionUpdates, actorId, actorRoleId) {
|
|
556
|
+
const existing = await this.storage.getRole(roleId);
|
|
557
|
+
if (!existing) {
|
|
558
|
+
throw new GovernanceAdminError(`Role "${roleId}" not found`, "ROLE_NOT_FOUND");
|
|
559
|
+
}
|
|
560
|
+
const proposedRole = {
|
|
561
|
+
...existing,
|
|
562
|
+
definition: { ...existing.definition, ...proposedDefinitionUpdates }
|
|
563
|
+
};
|
|
564
|
+
return this.simulate(
|
|
565
|
+
{
|
|
566
|
+
type: "role_change",
|
|
567
|
+
proposed: { role: proposedRole },
|
|
568
|
+
targetRoleId: roleId
|
|
569
|
+
},
|
|
570
|
+
actorId,
|
|
571
|
+
actorRoleId
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Simulate a proposed zone rule change and return impact.
|
|
576
|
+
*/
|
|
577
|
+
async simulateZoneChange(zoneId, proposedRuleUpdates, targetRoleId, actorId, actorRoleId) {
|
|
578
|
+
const existing = await this.storage.getZone(zoneId);
|
|
579
|
+
if (!existing) {
|
|
580
|
+
throw new GovernanceAdminError(`Zone "${zoneId}" not found`, "ZONE_NOT_FOUND");
|
|
581
|
+
}
|
|
582
|
+
const proposedZone = {
|
|
583
|
+
...existing,
|
|
584
|
+
rules: {
|
|
585
|
+
...existing.rules,
|
|
586
|
+
...proposedRuleUpdates,
|
|
587
|
+
customRules: proposedRuleUpdates.customRules ?? existing.rules.customRules
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
return this.simulate(
|
|
591
|
+
{
|
|
592
|
+
type: "zone_change",
|
|
593
|
+
proposed: { zone: proposedZone },
|
|
594
|
+
targetRoleId,
|
|
595
|
+
targetZoneId: zoneId
|
|
596
|
+
},
|
|
597
|
+
actorId,
|
|
598
|
+
actorRoleId
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
// ── Audit Log ─────────────────────────────────────────────────────────
|
|
602
|
+
async getAuditLog(options) {
|
|
603
|
+
return this.storage.getAuditLog(options);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
var GovernanceAdminError = class extends Error {
|
|
607
|
+
code;
|
|
608
|
+
constructor(message, code) {
|
|
609
|
+
super(message);
|
|
610
|
+
this.name = "GovernanceAdminError";
|
|
611
|
+
this.code = code;
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/admin/storage.ts
|
|
616
|
+
var InMemoryStorage = class {
|
|
617
|
+
roles = /* @__PURE__ */ new Map();
|
|
618
|
+
assignments = /* @__PURE__ */ new Map();
|
|
619
|
+
zones = /* @__PURE__ */ new Map();
|
|
620
|
+
authority = {
|
|
621
|
+
grants: [],
|
|
622
|
+
emergencyOverrideAlwaysAllowed: true
|
|
623
|
+
};
|
|
624
|
+
audit = [];
|
|
625
|
+
// ── Roles ───────────────────────────────────────────────────────────────
|
|
626
|
+
async getRoles() {
|
|
627
|
+
return Array.from(this.roles.values());
|
|
628
|
+
}
|
|
629
|
+
async getRole(id) {
|
|
630
|
+
return this.roles.get(id) ?? null;
|
|
631
|
+
}
|
|
632
|
+
async saveRole(role) {
|
|
633
|
+
this.roles.set(role.id, role);
|
|
634
|
+
}
|
|
635
|
+
async deleteRole(id) {
|
|
636
|
+
this.roles.delete(id);
|
|
637
|
+
}
|
|
638
|
+
// ── Assignments ─────────────────────────────────────────────────────────
|
|
639
|
+
async getAssignments() {
|
|
640
|
+
return Array.from(this.assignments.values());
|
|
641
|
+
}
|
|
642
|
+
async getAssignmentsByDevice(deviceId) {
|
|
643
|
+
return Array.from(this.assignments.values()).filter(
|
|
644
|
+
(a) => a.deviceId === deviceId
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
async getAssignmentsByRole(roleId) {
|
|
648
|
+
return Array.from(this.assignments.values()).filter(
|
|
649
|
+
(a) => a.roleId === roleId
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
async saveAssignment(assignment) {
|
|
653
|
+
this.assignments.set(assignment.id, assignment);
|
|
654
|
+
}
|
|
655
|
+
async deleteAssignment(id) {
|
|
656
|
+
this.assignments.delete(id);
|
|
657
|
+
}
|
|
658
|
+
// ── Zones ───────────────────────────────────────────────────────────────
|
|
659
|
+
async getZones() {
|
|
660
|
+
return Array.from(this.zones.values());
|
|
661
|
+
}
|
|
662
|
+
async getZone(id) {
|
|
663
|
+
return this.zones.get(id) ?? null;
|
|
664
|
+
}
|
|
665
|
+
async getZonesByLocation(locationId) {
|
|
666
|
+
return Array.from(this.zones.values()).filter(
|
|
667
|
+
(z) => z.locationId === locationId
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
async saveZone(zone) {
|
|
671
|
+
this.zones.set(zone.id, zone);
|
|
672
|
+
}
|
|
673
|
+
async deleteZone(id) {
|
|
674
|
+
this.zones.delete(id);
|
|
675
|
+
}
|
|
676
|
+
// ── Authority ───────────────────────────────────────────────────────────
|
|
677
|
+
async getAuthorityChain() {
|
|
678
|
+
return this.authority;
|
|
679
|
+
}
|
|
680
|
+
async saveAuthorityChain(chain) {
|
|
681
|
+
this.authority = chain;
|
|
682
|
+
}
|
|
683
|
+
// ── Audit ───────────────────────────────────────────────────────────────
|
|
684
|
+
async getAuditLog(options) {
|
|
685
|
+
let entries = this.audit;
|
|
686
|
+
if (options?.action) {
|
|
687
|
+
entries = entries.filter((e) => e.action === options.action);
|
|
688
|
+
}
|
|
689
|
+
const offset = options?.offset ?? 0;
|
|
690
|
+
const limit = options?.limit ?? 100;
|
|
691
|
+
return entries.slice(offset, offset + limit);
|
|
692
|
+
}
|
|
693
|
+
async appendAudit(entry) {
|
|
694
|
+
this.audit.push(entry);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
export {
|
|
698
|
+
GovernanceAdmin,
|
|
699
|
+
GovernanceAdminError,
|
|
700
|
+
InMemoryStorage,
|
|
701
|
+
simulateFullMatrix,
|
|
702
|
+
simulatePolicy
|
|
703
|
+
};
|