@siduri-x/core 1.0.9 → 2.0.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/dist/action-policy.d.ts +28 -1
- package/dist/action-policy.js +167 -7
- package/dist/action-policy.test.js +104 -0
- package/dist/adversarial.test.js +4 -2
- package/dist/architecture-boundary.test.js +4 -12
- package/dist/capability.d.ts +11 -2
- package/dist/capability.js +8 -1
- package/dist/capability.test.js +43 -1
- package/dist/context-retriever.d.ts +8 -5
- package/dist/context-retriever.js +44 -15
- package/dist/database.d.ts +1 -0
- package/dist/database.js +17 -0
- package/dist/index.d.ts +46 -7
- package/dist/index.js +2 -0
- package/dist/prompt-compiler.d.ts +1 -0
- package/dist/prompt-compiler.js +7 -1
- package/dist/runtime.d.ts +12 -5
- package/dist/runtime.js +26 -8
- package/dist/schema-validator.test.js +2 -2
- package/dist/siduri-db.d.ts +138 -0
- package/dist/siduri-db.js +687 -0
- package/dist/siduri-db.test.d.ts +1 -0
- package/dist/siduri-db.test.js +863 -0
- package/dist/sqlite-action-store.d.ts +3 -2
- package/dist/sqlite-action-store.js +27 -4
- package/dist/sqlite-action-store.test.js +1 -0
- package/package.json +6 -6
package/dist/action-policy.d.ts
CHANGED
|
@@ -15,12 +15,35 @@ export interface ActionPolicyEngineOptions {
|
|
|
15
15
|
defaultRequireApprovalForHighRisk?: boolean;
|
|
16
16
|
store?: ActionStore;
|
|
17
17
|
secretKey?: string;
|
|
18
|
+
allowedApproverRoles?: string[];
|
|
19
|
+
requiredApproverCapabilities?: string[];
|
|
20
|
+
}
|
|
21
|
+
export type ActionApprovalDecisionCode = 'APPROVED' | 'REJECTED_UNAUTHORIZED' | 'REJECTED_UNAUTHENTICATED' | 'REJECTED_ROLE_MISMATCH' | 'REJECTED_MISSING_APPROVER_ID' | 'REJECTED_MISSING_CAPABILITY';
|
|
22
|
+
export interface ActionApprovalResult {
|
|
23
|
+
approved: boolean;
|
|
24
|
+
decisionCode: ActionApprovalDecisionCode;
|
|
25
|
+
reason: string;
|
|
26
|
+
executionId: string;
|
|
27
|
+
approverActorId: string;
|
|
18
28
|
}
|
|
19
29
|
export interface ApproveActionOptions {
|
|
20
30
|
executionId: string;
|
|
21
31
|
approverActorId: string;
|
|
22
32
|
reason?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Optional RequestContext of the approver establishing authenticated identity.
|
|
35
|
+
*/
|
|
36
|
+
context?: RequestContext;
|
|
37
|
+
/**
|
|
38
|
+
* Optional role of the approver (e.g. 'owner', 'administrator', 'operator').
|
|
39
|
+
*/
|
|
40
|
+
approverRole?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Optional capabilities held by the approver.
|
|
43
|
+
*/
|
|
44
|
+
approverCapabilities?: string[];
|
|
23
45
|
}
|
|
46
|
+
export declare function normalizeApproverRole(role?: string, actorId?: string): string;
|
|
24
47
|
export declare class ActionPolicyEngine {
|
|
25
48
|
private readonly toolRegistry;
|
|
26
49
|
private readonly rules;
|
|
@@ -28,7 +51,10 @@ export declare class ActionPolicyEngine {
|
|
|
28
51
|
private readonly defaultRiskLevel;
|
|
29
52
|
private readonly defaultRequireApprovalForHighRisk;
|
|
30
53
|
private readonly secretKey;
|
|
54
|
+
private readonly allowedApproverRoles;
|
|
55
|
+
private readonly requiredApproverCapabilities;
|
|
31
56
|
private readonly approvedExecutions;
|
|
57
|
+
private readonly pendingActions;
|
|
32
58
|
constructor(options?: ActionPolicyEngineOptions);
|
|
33
59
|
registerToolDefinition(tool: ToolDefinition): void;
|
|
34
60
|
unregisterToolDefinition(toolName: string): boolean;
|
|
@@ -38,7 +64,8 @@ export declare class ActionPolicyEngine {
|
|
|
38
64
|
decision: ActionPolicyDecision;
|
|
39
65
|
capability?: AuthorizationCapability;
|
|
40
66
|
}>;
|
|
41
|
-
approveAction(options: ApproveActionOptions): Promise<
|
|
67
|
+
approveAction(options: ApproveActionOptions): Promise<ActionApprovalResult>;
|
|
68
|
+
private recordApprovalAudit;
|
|
42
69
|
recordAudit(action: ActionIntent, context: RequestContext | undefined, decision: ActionPolicyDecision | undefined, lifecycle: ActionLifecycleState, result?: unknown, error?: string, durationMs?: number): Promise<ActionAuditEvent>;
|
|
43
70
|
getAuditLog(): Promise<ActionAuditEvent[]>;
|
|
44
71
|
getStore(): ActionStore;
|
package/dist/action-policy.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ActionPolicyEngine = void 0;
|
|
4
|
+
exports.normalizeApproverRole = normalizeApproverRole;
|
|
4
5
|
const capability_1 = require("./capability");
|
|
6
|
+
function normalizeApproverRole(role, actorId) {
|
|
7
|
+
if (role && role.trim() !== '') {
|
|
8
|
+
const lower = role.trim().toLowerCase();
|
|
9
|
+
if (lower === 'administrator' || lower === 'admin')
|
|
10
|
+
return 'administrator';
|
|
11
|
+
return lower;
|
|
12
|
+
}
|
|
13
|
+
if (actorId) {
|
|
14
|
+
const lower = actorId.toLowerCase();
|
|
15
|
+
if (lower.includes('admin'))
|
|
16
|
+
return 'administrator';
|
|
17
|
+
if (lower.includes('owner'))
|
|
18
|
+
return 'owner';
|
|
19
|
+
if (lower.includes('operator'))
|
|
20
|
+
return 'operator';
|
|
21
|
+
if (lower.includes('viewer') || lower.includes('guest') || lower.includes('visitor'))
|
|
22
|
+
return 'viewer';
|
|
23
|
+
}
|
|
24
|
+
return 'owner'; // Default to single-owner role
|
|
25
|
+
}
|
|
5
26
|
class ActionPolicyEngine {
|
|
6
27
|
toolRegistry = new Map();
|
|
7
28
|
rules = [];
|
|
@@ -9,13 +30,18 @@ class ActionPolicyEngine {
|
|
|
9
30
|
defaultRiskLevel;
|
|
10
31
|
defaultRequireApprovalForHighRisk;
|
|
11
32
|
secretKey;
|
|
12
|
-
|
|
33
|
+
allowedApproverRoles;
|
|
34
|
+
requiredApproverCapabilities;
|
|
35
|
+
approvedExecutions = new Map();
|
|
36
|
+
pendingActions = new Map();
|
|
13
37
|
constructor(options = {}) {
|
|
14
38
|
this.rules = options.rules ?? [];
|
|
15
39
|
this.defaultRiskLevel = options.defaultRiskLevel ?? 'HIGH';
|
|
16
40
|
this.defaultRequireApprovalForHighRisk = options.defaultRequireApprovalForHighRisk ?? true;
|
|
17
41
|
this.store = options.store ?? new capability_1.InMemoryActionStore();
|
|
18
42
|
this.secretKey = (0, capability_1.getOrGenerateLocalActionPolicySecret)(options.secretKey);
|
|
43
|
+
this.allowedApproverRoles = (options.allowedApproverRoles ?? ['owner', 'administrator', 'admin', 'operator']).map((r) => r.toLowerCase());
|
|
44
|
+
this.requiredApproverCapabilities = options.requiredApproverCapabilities ?? [];
|
|
19
45
|
}
|
|
20
46
|
registerToolDefinition(tool) {
|
|
21
47
|
const key = tool.providerId ? `${tool.providerId}/${tool.name}` : tool.name;
|
|
@@ -125,14 +151,38 @@ class ActionPolicyEngine {
|
|
|
125
151
|
// Risk level approval check
|
|
126
152
|
const requiresExplicitApproval = toolDef.requiresApproval ??
|
|
127
153
|
(this.defaultRequireApprovalForHighRisk && (riskLevel === 'HIGH' || riskLevel === 'CRITICAL'));
|
|
128
|
-
let
|
|
154
|
+
let approvalRecord = this.approvedExecutions.get(executionId);
|
|
155
|
+
if (!approvalRecord && typeof this.store.getApproval === 'function') {
|
|
156
|
+
approvalRecord = await this.store.getApproval(executionId);
|
|
157
|
+
if (approvalRecord) {
|
|
158
|
+
this.approvedExecutions.set(executionId, approvalRecord);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
let isApproved = Boolean(approvalRecord);
|
|
129
162
|
if (!isApproved && typeof this.store.isActionApproved === 'function') {
|
|
130
163
|
isApproved = await this.store.isActionApproved(executionId);
|
|
131
|
-
|
|
132
|
-
|
|
164
|
+
}
|
|
165
|
+
if (requiresExplicitApproval && isApproved && approvalRecord) {
|
|
166
|
+
const approverRole = (approvalRecord.approverRole || normalizeApproverRole(undefined, approvalRecord.approverActorId)).toLowerCase();
|
|
167
|
+
const isOwnerOrAdmin = approverRole === 'owner' || approverRole === 'administrator' || approverRole === 'admin';
|
|
168
|
+
if (toolDef.allowedRoles && toolDef.allowedRoles.length > 0 && !isOwnerOrAdmin) {
|
|
169
|
+
const allowed = toolDef.allowedRoles.map((r) => r.toLowerCase());
|
|
170
|
+
if (!allowed.includes(approverRole)) {
|
|
171
|
+
const decision = {
|
|
172
|
+
allowed: false,
|
|
173
|
+
reason: `Approver "${approvalRecord.approverActorId}" with role "${approverRole}" is not authorized to approve tool "${action.toolName}"`,
|
|
174
|
+
riskLevel,
|
|
175
|
+
requiredCapabilities: requiredCaps,
|
|
176
|
+
executionId,
|
|
177
|
+
decisionCode: 'REJECTED_UNAUTHORIZED',
|
|
178
|
+
};
|
|
179
|
+
await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
|
|
180
|
+
return { decision };
|
|
181
|
+
}
|
|
133
182
|
}
|
|
134
183
|
}
|
|
135
184
|
if (requiresExplicitApproval && !isApproved) {
|
|
185
|
+
this.pendingActions.set(executionId, { action, context: effectiveContext, toolDef });
|
|
136
186
|
const decision = {
|
|
137
187
|
allowed: false,
|
|
138
188
|
reason: `Action "${action.toolName}" has risk level ${riskLevel} and requires explicit approval`,
|
|
@@ -181,11 +231,121 @@ class ActionPolicyEngine {
|
|
|
181
231
|
return { decision, capability };
|
|
182
232
|
}
|
|
183
233
|
async approveAction(options) {
|
|
184
|
-
|
|
234
|
+
const actorId = (options.context?.actor.actorId || options.approverActorId || '').trim();
|
|
235
|
+
if (!actorId) {
|
|
236
|
+
return {
|
|
237
|
+
approved: false,
|
|
238
|
+
decisionCode: 'REJECTED_MISSING_APPROVER_ID',
|
|
239
|
+
reason: 'approverActorId is required to approve an action execution',
|
|
240
|
+
executionId: options.executionId,
|
|
241
|
+
approverActorId: '',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// 1. Authenticated boundary check
|
|
245
|
+
if (options.context && options.context.actor.authenticated === false) {
|
|
246
|
+
const result = {
|
|
247
|
+
approved: false,
|
|
248
|
+
decisionCode: 'REJECTED_UNAUTHENTICATED',
|
|
249
|
+
reason: `Approver "${actorId}" is unauthenticated and cannot approve actions`,
|
|
250
|
+
executionId: options.executionId,
|
|
251
|
+
approverActorId: actorId,
|
|
252
|
+
};
|
|
253
|
+
await this.recordApprovalAudit(options.executionId, actorId, 'unauthenticated', false, result.reason);
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
// 2. Resolve effective approver role & capabilities
|
|
257
|
+
const rawRole = options.context?.actor.authorizationRole || options.context?.actor?.role || options.approverRole;
|
|
258
|
+
const approverRole = normalizeApproverRole(rawRole, actorId);
|
|
259
|
+
const capabilities = options.context?.actor.capabilities || options.approverCapabilities || [];
|
|
260
|
+
const isOwnerOrAdmin = approverRole === 'owner' || approverRole === 'administrator' || approverRole === 'admin';
|
|
261
|
+
const isRoleAllowed = isOwnerOrAdmin || this.allowedApproverRoles.includes(approverRole);
|
|
262
|
+
// Reject non-allowed roles or explicit viewer/guest role
|
|
263
|
+
if (!isRoleAllowed || approverRole === 'viewer') {
|
|
264
|
+
const result = {
|
|
265
|
+
approved: false,
|
|
266
|
+
decisionCode: 'REJECTED_UNAUTHORIZED',
|
|
267
|
+
reason: `Actor "${actorId}" with role "${approverRole}" is not authorized to approve actions`,
|
|
268
|
+
executionId: options.executionId,
|
|
269
|
+
approverActorId: actorId,
|
|
270
|
+
};
|
|
271
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason);
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
// 3. Tool-specific authorization check if pending action is registered
|
|
275
|
+
const pending = this.pendingActions.get(options.executionId);
|
|
276
|
+
if (pending) {
|
|
277
|
+
const { toolDef } = pending;
|
|
278
|
+
if (toolDef.allowedRoles && toolDef.allowedRoles.length > 0 && !isOwnerOrAdmin) {
|
|
279
|
+
const normalizedToolRoles = new Set(toolDef.allowedRoles.map((r) => r.toLowerCase()));
|
|
280
|
+
if (!normalizedToolRoles.has(approverRole)) {
|
|
281
|
+
const result = {
|
|
282
|
+
approved: false,
|
|
283
|
+
decisionCode: 'REJECTED_ROLE_MISMATCH',
|
|
284
|
+
reason: `Approver role "${approverRole}" is not authorized to approve tool "${toolDef.name}" (requires: ${toolDef.allowedRoles.join(', ')})`,
|
|
285
|
+
executionId: options.executionId,
|
|
286
|
+
approverActorId: actorId,
|
|
287
|
+
};
|
|
288
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason, toolDef.name);
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (this.requiredApproverCapabilities.length > 0 && !isOwnerOrAdmin) {
|
|
293
|
+
const missing = this.requiredApproverCapabilities.filter((c) => !capabilities.includes(c));
|
|
294
|
+
if (missing.length > 0) {
|
|
295
|
+
const result = {
|
|
296
|
+
approved: false,
|
|
297
|
+
decisionCode: 'REJECTED_MISSING_CAPABILITY',
|
|
298
|
+
reason: `Approver is missing required approval capabilities: [${missing.join(', ')}]`,
|
|
299
|
+
executionId: options.executionId,
|
|
300
|
+
approverActorId: actorId,
|
|
301
|
+
};
|
|
302
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason, toolDef.name);
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// 4. Record verified approval
|
|
308
|
+
const record = {
|
|
309
|
+
executionId: options.executionId,
|
|
310
|
+
approverActorId: actorId,
|
|
311
|
+
reason: options.reason,
|
|
312
|
+
approverRole,
|
|
313
|
+
approvedAt: new Date().toISOString(),
|
|
314
|
+
};
|
|
315
|
+
this.approvedExecutions.set(options.executionId, record);
|
|
185
316
|
if (typeof this.store.saveApproval === 'function') {
|
|
186
|
-
await this.store.saveApproval(options.executionId,
|
|
317
|
+
await this.store.saveApproval(options.executionId, actorId, options.reason, approverRole);
|
|
187
318
|
}
|
|
188
|
-
|
|
319
|
+
const reason = options.reason || 'Action approved by authorized policy approver';
|
|
320
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, true, reason, pending?.toolDef.name);
|
|
321
|
+
return {
|
|
322
|
+
approved: true,
|
|
323
|
+
decisionCode: 'APPROVED',
|
|
324
|
+
reason,
|
|
325
|
+
executionId: options.executionId,
|
|
326
|
+
approverActorId: actorId,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async recordApprovalAudit(executionId, approverActorId, approverRole, approved, reason, toolName) {
|
|
330
|
+
const event = {
|
|
331
|
+
executionId,
|
|
332
|
+
actionId: executionId,
|
|
333
|
+
toolName: toolName || 'action:approve',
|
|
334
|
+
companionId: 'system',
|
|
335
|
+
actorId: approverActorId,
|
|
336
|
+
riskLevel: 'HIGH',
|
|
337
|
+
lifecycle: approved ? 'APPROVED' : 'REJECTED',
|
|
338
|
+
decision: {
|
|
339
|
+
allowed: approved,
|
|
340
|
+
reason: reason || (approved ? 'Approval granted' : 'Approval rejected'),
|
|
341
|
+
riskLevel: 'HIGH',
|
|
342
|
+
executionId,
|
|
343
|
+
decisionCode: approved ? 'ALLOWED_POLICY' : 'REJECTED_UNAUTHORIZED',
|
|
344
|
+
},
|
|
345
|
+
parametersHash: (0, capability_1.computeParametersHash)({ executionId, approverActorId, approverRole, reason }),
|
|
346
|
+
timestamp: new Date().toISOString(),
|
|
347
|
+
};
|
|
348
|
+
await this.store.appendAudit(event);
|
|
189
349
|
}
|
|
190
350
|
async recordAudit(action, context, decision, lifecycle, result, error, durationMs) {
|
|
191
351
|
const event = {
|
|
@@ -190,4 +190,108 @@ describe('ActionPolicyEngine Boundary', () => {
|
|
|
190
190
|
expect(resOp.decision.allowed).toBe(false);
|
|
191
191
|
expect(resOp.decision.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
192
192
|
});
|
|
193
|
+
describe('Approval Authorization Semantics', () => {
|
|
194
|
+
const criticalAction = {
|
|
195
|
+
actionId: 'act-sec-auth-1',
|
|
196
|
+
toolName: 'admin/delete_database',
|
|
197
|
+
parameters: {},
|
|
198
|
+
context: {
|
|
199
|
+
companionId: 'companion-1',
|
|
200
|
+
actor: {
|
|
201
|
+
actorId: 'admin-1',
|
|
202
|
+
sessionId: 'sess-1',
|
|
203
|
+
authorizationRole: 'administrator',
|
|
204
|
+
capabilities: ['admin:delete'],
|
|
205
|
+
authenticated: true,
|
|
206
|
+
},
|
|
207
|
+
conversation: { channel: 'direct', correlationId: 'corr-1' },
|
|
208
|
+
},
|
|
209
|
+
executionId: 'exec-sec-auth-1',
|
|
210
|
+
};
|
|
211
|
+
it('rejects action approval when approverActorId is empty', async () => {
|
|
212
|
+
await engine.evaluateAction(criticalAction);
|
|
213
|
+
const res = await engine.approveAction({
|
|
214
|
+
executionId: 'exec-sec-auth-1',
|
|
215
|
+
approverActorId: '',
|
|
216
|
+
});
|
|
217
|
+
expect(res.approved).toBe(false);
|
|
218
|
+
expect(res.decisionCode).toBe('REJECTED_MISSING_APPROVER_ID');
|
|
219
|
+
});
|
|
220
|
+
it('rejects action approval from unauthenticated approver context', async () => {
|
|
221
|
+
await engine.evaluateAction(criticalAction);
|
|
222
|
+
const res = await engine.approveAction({
|
|
223
|
+
executionId: 'exec-sec-auth-1',
|
|
224
|
+
approverActorId: 'fake-admin',
|
|
225
|
+
context: {
|
|
226
|
+
companionId: 'companion-1',
|
|
227
|
+
actor: {
|
|
228
|
+
actorId: 'fake-admin',
|
|
229
|
+
sessionId: 'sess-fake',
|
|
230
|
+
authorizationRole: 'administrator',
|
|
231
|
+
capabilities: ['admin:delete'],
|
|
232
|
+
authenticated: false, // Unauthenticated!
|
|
233
|
+
},
|
|
234
|
+
conversation: { channel: 'direct', correlationId: 'corr-fake' },
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
expect(res.approved).toBe(false);
|
|
238
|
+
expect(res.decisionCode).toBe('REJECTED_UNAUTHENTICATED');
|
|
239
|
+
});
|
|
240
|
+
it('rejects action approval from viewer role', async () => {
|
|
241
|
+
await engine.evaluateAction(criticalAction);
|
|
242
|
+
const res = await engine.approveAction({
|
|
243
|
+
executionId: 'exec-sec-auth-1',
|
|
244
|
+
approverActorId: 'viewer-user',
|
|
245
|
+
approverRole: 'viewer',
|
|
246
|
+
});
|
|
247
|
+
expect(res.approved).toBe(false);
|
|
248
|
+
expect(res.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
249
|
+
});
|
|
250
|
+
it('rejects approval when approver role does not match tool requirements (operator cannot approve admin tool)', async () => {
|
|
251
|
+
// 1. Initial evaluation stages pending execution
|
|
252
|
+
const eval1 = await engine.evaluateAction(criticalAction);
|
|
253
|
+
expect(eval1.decision.allowed).toBe(false);
|
|
254
|
+
expect(eval1.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
|
|
255
|
+
// 2. Operator attempts to approve an administrator-only tool
|
|
256
|
+
const approvalRes = await engine.approveAction({
|
|
257
|
+
executionId: 'exec-sec-auth-1',
|
|
258
|
+
approverActorId: 'operator-alice',
|
|
259
|
+
approverRole: 'operator',
|
|
260
|
+
});
|
|
261
|
+
expect(approvalRes.approved).toBe(false);
|
|
262
|
+
expect(approvalRes.decisionCode).toBe('REJECTED_ROLE_MISMATCH');
|
|
263
|
+
// 3. Action re-evaluation remains unapproved and denied
|
|
264
|
+
const eval2 = await engine.evaluateAction(criticalAction);
|
|
265
|
+
expect(eval2.decision.allowed).toBe(false);
|
|
266
|
+
expect(eval2.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
|
|
267
|
+
});
|
|
268
|
+
it('authorizes approval and emits capability when approver is administrator or owner', async () => {
|
|
269
|
+
await engine.evaluateAction(criticalAction);
|
|
270
|
+
const approvalRes = await engine.approveAction({
|
|
271
|
+
executionId: 'exec-sec-auth-1',
|
|
272
|
+
approverActorId: 'admin-bob',
|
|
273
|
+
approverRole: 'administrator',
|
|
274
|
+
reason: 'Authorized scheduled database purge',
|
|
275
|
+
});
|
|
276
|
+
expect(approvalRes.approved).toBe(true);
|
|
277
|
+
expect(approvalRes.decisionCode).toBe('APPROVED');
|
|
278
|
+
const evalApproved = await engine.evaluateAction(criticalAction);
|
|
279
|
+
expect(evalApproved.decision.allowed).toBe(true);
|
|
280
|
+
expect(evalApproved.decision.decisionCode).toBe('ALLOWED_POLICY');
|
|
281
|
+
expect(evalApproved.capability).toBeDefined();
|
|
282
|
+
});
|
|
283
|
+
it('records structured tamper-evident audit logs for approval decisions', async () => {
|
|
284
|
+
await engine.evaluateAction(criticalAction);
|
|
285
|
+
await engine.approveAction({
|
|
286
|
+
executionId: 'exec-sec-auth-1',
|
|
287
|
+
approverActorId: 'viewer-tamper',
|
|
288
|
+
approverRole: 'viewer',
|
|
289
|
+
});
|
|
290
|
+
const auditLogs = await engine.getAuditLog();
|
|
291
|
+
const rejectionEvent = auditLogs.find((l) => l.executionId === 'exec-sec-auth-1' && l.lifecycle === 'REJECTED' && l.toolName === 'action:approve');
|
|
292
|
+
expect(rejectionEvent).toBeDefined();
|
|
293
|
+
expect(rejectionEvent?.actorId).toBe('viewer-tamper');
|
|
294
|
+
expect(rejectionEvent?.decision?.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
193
297
|
});
|
package/dist/adversarial.test.js
CHANGED
|
@@ -356,7 +356,7 @@ describe('Adversarial Hardening Verification Suite (Phase 3)', () => {
|
|
|
356
356
|
const failingMemory = {
|
|
357
357
|
initialize: jest.fn().mockResolvedValue(undefined),
|
|
358
358
|
searchClaims: jest.fn().mockRejectedValue(new Error('Connection terminated unexpectedly')),
|
|
359
|
-
getDirectives: jest.fn().mockRejectedValue(new Error('
|
|
359
|
+
getDirectives: jest.fn().mockRejectedValue(new Error('Database read timeout')),
|
|
360
360
|
};
|
|
361
361
|
const runtime = new index_1.SiduriRuntime('companion-adv', { name: 'AdvCompanion' }, {
|
|
362
362
|
brain: mockBrain,
|
|
@@ -454,6 +454,7 @@ describe('Adversarial Hardening Verification Suite (Phase 3)', () => {
|
|
|
454
454
|
'riskLevel',
|
|
455
455
|
'lifecycle',
|
|
456
456
|
'parametersHash',
|
|
457
|
+
'resultHash',
|
|
457
458
|
];
|
|
458
459
|
for (const field of criticalFields) {
|
|
459
460
|
const tamperedEvent = { ...event1, [field]: 'TAMPERED_VALUE' };
|
|
@@ -477,12 +478,13 @@ describe('Adversarial Hardening Verification Suite (Phase 3)', () => {
|
|
|
477
478
|
decisionCode: tamperedEvent.decision.decisionCode,
|
|
478
479
|
} : null,
|
|
479
480
|
parametersHash: tamperedEvent.parametersHash || null,
|
|
481
|
+
resultHash: field === 'resultHash' ? 'TAMPERED_VALUE' : null,
|
|
480
482
|
error: tamperedEvent.error || null,
|
|
481
483
|
timestamp: tamperedEvent.timestamp,
|
|
482
484
|
});
|
|
483
485
|
const crypto = require('node:crypto');
|
|
484
486
|
const brokenHash1 = crypto.createHash('sha256').update(`${initialPrevHash}:${canonical}`, 'utf8').digest('hex');
|
|
485
|
-
expect(brokenHash1).not.toBe(event1.
|
|
487
|
+
expect(brokenHash1).not.toBe(event1.eventHash);
|
|
486
488
|
}
|
|
487
489
|
});
|
|
488
490
|
});
|
|
@@ -41,16 +41,14 @@ describe('Architecture: Core & Organ Package Boundaries (Phase 2)', () => {
|
|
|
41
41
|
const corePackageJsonPath = path.resolve(__dirname, '../package.json');
|
|
42
42
|
const EXPECTED_ORGANS = [
|
|
43
43
|
{ dir: 'brain', name: '@siduri-x/brain', organType: 'brain', configKey: 'brain' },
|
|
44
|
-
{ dir: 'memory', name: '@siduri-x/memory', organType: 'memory', configKey: 'memory' },
|
|
45
|
-
{ dir: 'knowledge', name: '@siduri-x/knowledge', organType: 'knowledge', configKey: 'knowledge' },
|
|
46
|
-
{ dir: 'behavior', name: '@siduri-x/behavior', organType: 'behavior', configKey: 'behavior' },
|
|
47
44
|
{ dir: 'ear', name: '@siduri-x/ear', organType: 'ear', configKey: 'ear' },
|
|
45
|
+
{ dir: 'voice', name: '@siduri-x/voice', organType: 'voice', configKey: 'voice' },
|
|
46
|
+
{ dir: 'mouth', name: '@siduri-x/mouth', organType: 'mouth', configKey: 'mouth' },
|
|
48
47
|
{ dir: 'vision', name: '@siduri-x/vision', organType: 'vision', configKey: 'vision' },
|
|
49
48
|
{ dir: 'hands', name: '@siduri-x/hands', organType: 'hands', configKey: 'hands' },
|
|
50
49
|
{ dir: 'body', name: '@siduri-x/body', organType: 'body', configKey: 'body' },
|
|
51
|
-
{ dir: 'voice', name: '@siduri-x/voice', organType: 'voice', configKey: 'voice' },
|
|
52
50
|
{ dir: 'observation', name: '@siduri-x/observation', organType: 'observation', configKey: 'observation' },
|
|
53
|
-
{ dir: '
|
|
51
|
+
{ dir: 'eknowledge', name: '@siduri-x/eknowledge', organType: 'eknowledge', configKey: 'eknowledge' },
|
|
54
52
|
];
|
|
55
53
|
it('package.json has zero dependencies on @siduri-x organ packages', () => {
|
|
56
54
|
const pkg = JSON.parse(fs.readFileSync(corePackageJsonPath, 'utf8'));
|
|
@@ -78,7 +76,7 @@ describe('Architecture: Core & Organ Package Boundaries (Phase 2)', () => {
|
|
|
78
76
|
}
|
|
79
77
|
expect(forbiddenImports).toEqual([]);
|
|
80
78
|
});
|
|
81
|
-
it('all
|
|
79
|
+
it('all 9 peripheral organ packages have a valid organ-manifest.json', () => {
|
|
82
80
|
for (const organ of EXPECTED_ORGANS) {
|
|
83
81
|
const manifestPath = path.join(rootOrgansDir, organ.dir, 'organ-manifest.json');
|
|
84
82
|
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
@@ -108,10 +106,4 @@ describe('Architecture: Core & Organ Package Boundaries (Phase 2)', () => {
|
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
});
|
|
111
|
-
it('memory organ packages SQL migrations', () => {
|
|
112
|
-
const memoryMigrationsDir = path.join(rootOrgansDir, 'memory', 'migrations');
|
|
113
|
-
expect(fs.existsSync(memoryMigrationsDir)).toBe(true);
|
|
114
|
-
const files = fs.readdirSync(memoryMigrationsDir).filter((f) => f.endsWith('.sql'));
|
|
115
|
-
expect(files.length).toBeGreaterThanOrEqual(1);
|
|
116
|
-
});
|
|
117
109
|
});
|
package/dist/capability.d.ts
CHANGED
|
@@ -37,12 +37,20 @@ export interface PersistentExecutionRecord {
|
|
|
37
37
|
createdAt: string;
|
|
38
38
|
updatedAt: string;
|
|
39
39
|
}
|
|
40
|
+
export interface ActionApprovalRecord {
|
|
41
|
+
executionId: string;
|
|
42
|
+
approverActorId: string;
|
|
43
|
+
reason?: string;
|
|
44
|
+
approvedAt: string;
|
|
45
|
+
approverRole?: string;
|
|
46
|
+
}
|
|
40
47
|
export interface ActionStore {
|
|
41
48
|
reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
|
|
42
49
|
updateExecution(record: PersistentExecutionRecord): Promise<void>;
|
|
43
50
|
getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
|
|
44
|
-
saveApproval(executionId: string, approverActorId: string, reason?: string): Promise<void>;
|
|
51
|
+
saveApproval(executionId: string, approverActorId: string, reason?: string, approverRole?: string): Promise<void>;
|
|
45
52
|
isActionApproved(executionId: string): Promise<boolean>;
|
|
53
|
+
getApproval?(executionId: string): Promise<ActionApprovalRecord | undefined>;
|
|
46
54
|
appendAudit(event: ActionAuditEvent): Promise<void>;
|
|
47
55
|
getAuditLog(executionId?: string): Promise<ActionAuditEvent[]>;
|
|
48
56
|
}
|
|
@@ -54,8 +62,9 @@ export declare class InMemoryActionStore implements ActionStore {
|
|
|
54
62
|
reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
|
|
55
63
|
updateExecution(record: PersistentExecutionRecord): Promise<void>;
|
|
56
64
|
getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
|
|
57
|
-
saveApproval(executionId: string, approverActorId: string, reason?: string): Promise<void>;
|
|
65
|
+
saveApproval(executionId: string, approverActorId: string, reason?: string, approverRole?: string): Promise<void>;
|
|
58
66
|
isActionApproved(executionId: string): Promise<boolean>;
|
|
67
|
+
getApproval(executionId: string): Promise<ActionApprovalRecord | undefined>;
|
|
59
68
|
appendAudit(event: ActionAuditEvent): Promise<void>;
|
|
60
69
|
getAuditLog(executionId?: string): Promise<ActionAuditEvent[]>;
|
|
61
70
|
}
|
package/dist/capability.js
CHANGED
|
@@ -53,16 +53,22 @@ class InMemoryActionStore {
|
|
|
53
53
|
const rec = this.executions.get(executionId);
|
|
54
54
|
return rec ? { ...rec } : undefined;
|
|
55
55
|
}
|
|
56
|
-
async saveApproval(executionId, approverActorId, reason) {
|
|
56
|
+
async saveApproval(executionId, approverActorId, reason, approverRole) {
|
|
57
57
|
this.approvals.set(executionId, {
|
|
58
|
+
executionId,
|
|
58
59
|
approverActorId,
|
|
59
60
|
reason,
|
|
61
|
+
approverRole,
|
|
60
62
|
approvedAt: new Date().toISOString(),
|
|
61
63
|
});
|
|
62
64
|
}
|
|
63
65
|
async isActionApproved(executionId) {
|
|
64
66
|
return this.approvals.has(executionId);
|
|
65
67
|
}
|
|
68
|
+
async getApproval(executionId) {
|
|
69
|
+
const record = this.approvals.get(executionId);
|
|
70
|
+
return record ? { ...record } : undefined;
|
|
71
|
+
}
|
|
66
72
|
async appendAudit(event) {
|
|
67
73
|
const prevHash = this.lastAuditHash;
|
|
68
74
|
// Tamper-evident hash chaining over all security-critical event fields
|
|
@@ -85,6 +91,7 @@ class InMemoryActionStore {
|
|
|
85
91
|
decisionCode: event.decision.decisionCode,
|
|
86
92
|
} : null,
|
|
87
93
|
parametersHash: event.parametersHash || null,
|
|
94
|
+
resultHash: event.resultHash || null,
|
|
88
95
|
error: event.error || null,
|
|
89
96
|
timestamp: event.timestamp,
|
|
90
97
|
};
|
package/dist/capability.test.js
CHANGED
|
@@ -165,6 +165,7 @@ describe('AuthorizationCapability Cryptographic & Tamper Review', () => {
|
|
|
165
165
|
decisionCode: event1.decision.decisionCode,
|
|
166
166
|
} : null,
|
|
167
167
|
parametersHash: event1.parametersHash || null,
|
|
168
|
+
resultHash: null,
|
|
168
169
|
error: event1.error || null,
|
|
169
170
|
timestamp: event1.timestamp,
|
|
170
171
|
});
|
|
@@ -190,6 +191,7 @@ describe('AuthorizationCapability Cryptographic & Tamper Review', () => {
|
|
|
190
191
|
decisionCode: event2.decision.decisionCode,
|
|
191
192
|
} : null,
|
|
192
193
|
parametersHash: event2.parametersHash || null,
|
|
194
|
+
resultHash: null,
|
|
193
195
|
error: event2.error || null,
|
|
194
196
|
timestamp: event2.timestamp,
|
|
195
197
|
});
|
|
@@ -203,7 +205,47 @@ describe('AuthorizationCapability Cryptographic & Tamper Review', () => {
|
|
|
203
205
|
});
|
|
204
206
|
const tamperedHash1 = crypto.createHash('sha256').update(`${initialPrevHash}:${tamperedCanonical1}`, 'utf8').digest('hex');
|
|
205
207
|
const brokenHash2 = crypto.createHash('sha256').update(`${tamperedHash1}:${canonical2}`, 'utf8').digest('hex');
|
|
206
|
-
expect(brokenHash2).not.toBe(event2.
|
|
208
|
+
expect(brokenHash2).not.toBe(event2.eventHash);
|
|
209
|
+
});
|
|
210
|
+
it('detects tampering with resultHash on completed action events', async () => {
|
|
211
|
+
const store = new capability_1.InMemoryActionStore();
|
|
212
|
+
const initialPrevHash = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
213
|
+
const event = {
|
|
214
|
+
executionId: 'exec-1',
|
|
215
|
+
actionId: 'act-1',
|
|
216
|
+
toolName: 'comm/send_email',
|
|
217
|
+
companionId: 'comp-1',
|
|
218
|
+
riskLevel: 'LOW',
|
|
219
|
+
lifecycle: 'COMPLETED',
|
|
220
|
+
parametersHash: 'param-hash-1',
|
|
221
|
+
resultHash: 'result-hash-original',
|
|
222
|
+
timestamp: '2026-09-12T10:00:00.000Z',
|
|
223
|
+
};
|
|
224
|
+
await store.appendAudit(event);
|
|
225
|
+
const log = await store.getAuditLog();
|
|
226
|
+
const recorded = log[0];
|
|
227
|
+
// Tampering with resultHash changes canonical payload and recalculates a mismatched hash
|
|
228
|
+
const tamperedPayload = {
|
|
229
|
+
executionId: recorded.executionId,
|
|
230
|
+
actionId: recorded.actionId,
|
|
231
|
+
toolName: recorded.toolName,
|
|
232
|
+
providerId: recorded.providerId || null,
|
|
233
|
+
companionId: recorded.companionId,
|
|
234
|
+
actorId: recorded.actorId || null,
|
|
235
|
+
sessionId: recorded.sessionId || null,
|
|
236
|
+
channel: recorded.channel || null,
|
|
237
|
+
correlationId: recorded.correlationId || null,
|
|
238
|
+
riskLevel: recorded.riskLevel,
|
|
239
|
+
lifecycle: recorded.lifecycle,
|
|
240
|
+
decision: null,
|
|
241
|
+
parametersHash: recorded.parametersHash || null,
|
|
242
|
+
resultHash: 'result-hash-TAMPERED',
|
|
243
|
+
error: recorded.error || null,
|
|
244
|
+
timestamp: recorded.timestamp,
|
|
245
|
+
};
|
|
246
|
+
const tamperedCanonical = (0, capability_1.canonicalizeJson)(tamperedPayload);
|
|
247
|
+
const recomputedHash = crypto.createHash('sha256').update(`${initialPrevHash}:${tamperedCanonical}`, 'utf8').digest('hex');
|
|
248
|
+
expect(recomputedHash).not.toBe(recorded.eventHash);
|
|
207
249
|
});
|
|
208
250
|
});
|
|
209
251
|
describe('Durable Action Approval Restart Semantics', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KnowledgeOrgan, MemoryOrgan, KnowledgeItem, Claim, BehaviorDirective, EvidenceRecord, ResponseCitation, RequestContext } from './index';
|
|
1
|
+
import { KnowledgeOrgan, MemoryOrgan, KnowledgeItem, Claim, BehaviorDirective, EvidenceRecord, ResponseCitation, RequestContext, SelfRepository, LifeDatabase, EpisodicMemoryStore, EKnowledgeOrgan } from './index';
|
|
2
2
|
export interface ContextRetrievalParams {
|
|
3
3
|
companionId: string;
|
|
4
4
|
perceivedText: string;
|
|
@@ -6,8 +6,10 @@ export interface ContextRetrievalParams {
|
|
|
6
6
|
role: 'OWNER' | 'VIEWER' | 'OPERATOR';
|
|
7
7
|
isContextObject: boolean;
|
|
8
8
|
shouldQueryKnowledge: boolean;
|
|
9
|
-
knowledge?: KnowledgeOrgan;
|
|
10
|
-
memory?: MemoryOrgan;
|
|
9
|
+
knowledge?: KnowledgeOrgan | LifeDatabase;
|
|
10
|
+
memory?: MemoryOrgan | EpisodicMemoryStore;
|
|
11
|
+
self?: SelfRepository;
|
|
12
|
+
externalKnowledge?: EKnowledgeOrgan | KnowledgeOrgan;
|
|
11
13
|
}
|
|
12
14
|
export interface RetrievedContext {
|
|
13
15
|
knowledgeData: KnowledgeItem[];
|
|
@@ -16,9 +18,10 @@ export interface RetrievedContext {
|
|
|
16
18
|
subsystemDiagnostics: Record<string, string>;
|
|
17
19
|
collectedEvidence: EvidenceRecord[];
|
|
18
20
|
citations: ResponseCitation[];
|
|
21
|
+
lifeContext?: string[];
|
|
19
22
|
}
|
|
20
23
|
/**
|
|
21
|
-
* Concurrently queries Knowledge
|
|
22
|
-
*
|
|
24
|
+
* Concurrently queries Self, Knowledge (Life DB), External Knowledge, and Memory
|
|
25
|
+
* in a single parallel pass with graceful degradation.
|
|
23
26
|
*/
|
|
24
27
|
export declare function retrieveRuntimeContext(params: ContextRetrievalParams): Promise<RetrievedContext>;
|