@sidurijs/core 1.0.0
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-executor.d.ts +12 -0
- package/dist/action-executor.js +54 -0
- package/dist/action-policy.d.ts +79 -0
- package/dist/action-policy.js +439 -0
- package/dist/action-policy.test.d.ts +1 -0
- package/dist/action-policy.test.js +363 -0
- package/dist/action.d.ts +72 -0
- package/dist/action.js +2 -0
- package/dist/adversarial.test.d.ts +1 -0
- package/dist/adversarial.test.js +491 -0
- package/dist/architecture-boundary.test.d.ts +1 -0
- package/dist/architecture-boundary.test.js +108 -0
- package/dist/capability.d.ts +78 -0
- package/dist/capability.js +168 -0
- package/dist/capability.test.d.ts +1 -0
- package/dist/capability.test.js +311 -0
- package/dist/chat-contract.d.ts +94 -0
- package/dist/chat-contract.js +104 -0
- package/dist/cognition-planner.d.ts +15 -0
- package/dist/cognition-planner.js +23 -0
- package/dist/container.d.ts +74 -0
- package/dist/container.js +81 -0
- package/dist/context-retriever.d.ts +33 -0
- package/dist/context-retriever.js +260 -0
- package/dist/context.d.ts +46 -0
- package/dist/context.js +85 -0
- package/dist/context.test.d.ts +1 -0
- package/dist/context.test.js +76 -0
- package/dist/conversational-teach.test.d.ts +1 -0
- package/dist/conversational-teach.test.js +941 -0
- package/dist/database.d.ts +1 -0
- package/dist/database.js +17 -0
- package/dist/dispatcher.d.ts +14 -0
- package/dist/dispatcher.js +40 -0
- package/dist/dispatcher.test.d.ts +1 -0
- package/dist/dispatcher.test.js +58 -0
- package/dist/ear-types.d.ts +33 -0
- package/dist/ear-types.js +2 -0
- package/dist/evidence.d.ts +76 -0
- package/dist/evidence.js +27 -0
- package/dist/evidence.test.d.ts +1 -0
- package/dist/evidence.test.js +75 -0
- package/dist/experience-emitter.d.ts +21 -0
- package/dist/experience-emitter.js +47 -0
- package/dist/experience.d.ts +55 -0
- package/dist/experience.js +74 -0
- package/dist/experience.test.d.ts +1 -0
- package/dist/experience.test.js +55 -0
- package/dist/gating.d.ts +45 -0
- package/dist/gating.js +185 -0
- package/dist/gating.test.d.ts +1 -0
- package/dist/gating.test.js +186 -0
- package/dist/index.d.ts +367 -0
- package/dist/index.js +56 -0
- package/dist/input-normalizer.d.ts +14 -0
- package/dist/input-normalizer.js +76 -0
- package/dist/input-normalizer.test.d.ts +1 -0
- package/dist/input-normalizer.test.js +48 -0
- package/dist/intent-classifier.d.ts +37 -0
- package/dist/intent-classifier.js +150 -0
- package/dist/intent-classifier.test.d.ts +1 -0
- package/dist/intent-classifier.test.js +118 -0
- package/dist/memory-settler.d.ts +49 -0
- package/dist/memory-settler.js +161 -0
- package/dist/mouth-types.d.ts +90 -0
- package/dist/mouth-types.js +2 -0
- package/dist/perception-cycle.test.d.ts +1 -0
- package/dist/perception-cycle.test.js +301 -0
- package/dist/perception-pipeline.d.ts +77 -0
- package/dist/perception-pipeline.js +304 -0
- package/dist/perception-pipeline.test.d.ts +1 -0
- package/dist/perception-pipeline.test.js +65 -0
- package/dist/prompt-compiler.d.ts +26 -0
- package/dist/prompt-compiler.js +81 -0
- package/dist/prompt-compiler.test.d.ts +1 -0
- package/dist/prompt-compiler.test.js +90 -0
- package/dist/proposals.d.ts +33 -0
- package/dist/proposals.js +2 -0
- package/dist/response-envelope.d.ts +30 -0
- package/dist/response-envelope.js +90 -0
- package/dist/runtime-facades.test.d.ts +1 -0
- package/dist/runtime-facades.test.js +95 -0
- package/dist/runtime.d.ts +116 -0
- package/dist/runtime.js +778 -0
- package/dist/schema-validator.d.ts +9 -0
- package/dist/schema-validator.js +107 -0
- package/dist/schema-validator.test.d.ts +1 -0
- package/dist/schema-validator.test.js +124 -0
- package/dist/session-history.d.ts +20 -0
- package/dist/session-history.js +55 -0
- package/dist/session-history.test.d.ts +1 -0
- package/dist/session-history.test.js +38 -0
- package/dist/siduri-db.d.ts +235 -0
- package/dist/siduri-db.js +1405 -0
- package/dist/siduri-db.test.d.ts +1 -0
- package/dist/siduri-db.test.js +1021 -0
- package/dist/sqlite-action-store.d.ts +21 -0
- package/dist/sqlite-action-store.js +281 -0
- package/dist/sqlite-action-store.test.d.ts +1 -0
- package/dist/sqlite-action-store.test.js +261 -0
- package/dist/teaching.d.ts +14 -0
- package/dist/teaching.js +14 -0
- package/package.json +43 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const action_policy_1 = require("./action-policy");
|
|
4
|
+
const capability_1 = require("./capability");
|
|
5
|
+
describe('ActionPolicyEngine Boundary', () => {
|
|
6
|
+
let engine;
|
|
7
|
+
let sampleContext;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
engine = new action_policy_1.ActionPolicyEngine({
|
|
10
|
+
defaultRiskLevel: 'HIGH',
|
|
11
|
+
defaultRequireApprovalForHighRisk: true,
|
|
12
|
+
secretKey: 'test_secret_key',
|
|
13
|
+
});
|
|
14
|
+
sampleContext = {
|
|
15
|
+
companionId: 'companion-1',
|
|
16
|
+
actor: {
|
|
17
|
+
actorId: 'user-1',
|
|
18
|
+
sessionId: 'sess-1',
|
|
19
|
+
authorizationRole: 'operator',
|
|
20
|
+
capabilities: ['web:search', 'calc:basic'],
|
|
21
|
+
authenticated: true,
|
|
22
|
+
},
|
|
23
|
+
conversation: {
|
|
24
|
+
channel: 'direct',
|
|
25
|
+
correlationId: 'corr-1',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const searchTool = {
|
|
29
|
+
name: 'search_web',
|
|
30
|
+
providerId: 'builtin',
|
|
31
|
+
description: 'Search the web',
|
|
32
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
|
|
33
|
+
riskLevel: 'LOW',
|
|
34
|
+
requiredCapabilities: ['web:search'],
|
|
35
|
+
allowedChannels: ['direct', 'public', 'private'],
|
|
36
|
+
};
|
|
37
|
+
const deleteTool = {
|
|
38
|
+
name: 'delete_database',
|
|
39
|
+
providerId: 'admin',
|
|
40
|
+
description: 'Delete entire database',
|
|
41
|
+
inputSchema: { type: 'object' },
|
|
42
|
+
riskLevel: 'CRITICAL',
|
|
43
|
+
requiredCapabilities: ['admin:delete'],
|
|
44
|
+
allowedRoles: ['administrator'],
|
|
45
|
+
requiresApproval: true,
|
|
46
|
+
};
|
|
47
|
+
engine.registerToolDefinition(searchTool);
|
|
48
|
+
engine.registerToolDefinition(deleteTool);
|
|
49
|
+
});
|
|
50
|
+
it('approves an authorized low-risk action and issues a signed AuthorizationCapability', async () => {
|
|
51
|
+
const action = {
|
|
52
|
+
actionId: 'act-1',
|
|
53
|
+
toolName: 'builtin/search_web',
|
|
54
|
+
parameters: { query: 'Antigravity' },
|
|
55
|
+
context: sampleContext,
|
|
56
|
+
};
|
|
57
|
+
const { decision, capability } = await engine.evaluateAction(action);
|
|
58
|
+
expect(decision.allowed).toBe(true);
|
|
59
|
+
expect(decision.decisionCode).toBe('ALLOWED_POLICY');
|
|
60
|
+
expect(decision.riskLevel).toBe('LOW');
|
|
61
|
+
expect(capability).toBeDefined();
|
|
62
|
+
expect(capability?.allowed).toBe(true);
|
|
63
|
+
expect(capability?.toolName).toBe('builtin/search_web');
|
|
64
|
+
expect(capability?.actionId).toBe('act-1');
|
|
65
|
+
expect(capability?.signature).toBeDefined();
|
|
66
|
+
expect((0, capability_1.verifyCapabilitySignature)(capability, 'test_secret_key')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('rejects an unknown tool/action by default and issues no capability', async () => {
|
|
69
|
+
const action = {
|
|
70
|
+
actionId: 'act-2',
|
|
71
|
+
toolName: 'unregistered_tool',
|
|
72
|
+
parameters: {},
|
|
73
|
+
context: sampleContext,
|
|
74
|
+
};
|
|
75
|
+
const { decision, capability } = await engine.evaluateAction(action);
|
|
76
|
+
expect(decision.allowed).toBe(false);
|
|
77
|
+
expect(decision.decisionCode).toBe('REJECTED_UNKNOWN_TOOL');
|
|
78
|
+
expect(decision.riskLevel).toBe('CRITICAL');
|
|
79
|
+
expect(capability).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
it('rejects action if request context is missing', async () => {
|
|
82
|
+
const action = {
|
|
83
|
+
actionId: 'act-3',
|
|
84
|
+
toolName: 'builtin/search_web',
|
|
85
|
+
parameters: { query: 'test' },
|
|
86
|
+
// No context provided
|
|
87
|
+
};
|
|
88
|
+
const { decision, capability } = await engine.evaluateAction(action);
|
|
89
|
+
expect(decision.allowed).toBe(false);
|
|
90
|
+
expect(decision.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
91
|
+
expect(decision.reason).toContain('Missing RequestContext');
|
|
92
|
+
expect(capability).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
it('rejects action when actor lacks required capability', async () => {
|
|
95
|
+
const action = {
|
|
96
|
+
actionId: 'act-4',
|
|
97
|
+
toolName: 'admin/delete_database',
|
|
98
|
+
parameters: {},
|
|
99
|
+
context: sampleContext, // has 'web:search' and 'calc:basic', not 'admin:delete'
|
|
100
|
+
};
|
|
101
|
+
const { decision, capability } = await engine.evaluateAction(action);
|
|
102
|
+
expect(decision.allowed).toBe(false);
|
|
103
|
+
expect(decision.decisionCode).toBe('REJECTED_UNAUTHORIZED'); // operator is not administrator
|
|
104
|
+
expect(capability).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
it('rejects high/critical risk action requiring explicit approval before approval is granted, and allows after approval', async () => {
|
|
107
|
+
const adminContext = {
|
|
108
|
+
...sampleContext,
|
|
109
|
+
actor: {
|
|
110
|
+
...sampleContext.actor,
|
|
111
|
+
authorizationRole: 'administrator',
|
|
112
|
+
capabilities: ['admin:delete'],
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const action = {
|
|
116
|
+
actionId: 'act-5',
|
|
117
|
+
toolName: 'admin/delete_database',
|
|
118
|
+
parameters: {},
|
|
119
|
+
context: adminContext,
|
|
120
|
+
executionId: 'exec-admin-delete-1',
|
|
121
|
+
};
|
|
122
|
+
// First attempt: unapproved
|
|
123
|
+
const res1 = await engine.evaluateAction(action);
|
|
124
|
+
expect(res1.decision.allowed).toBe(false);
|
|
125
|
+
expect(res1.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
|
|
126
|
+
expect(res1.capability).toBeUndefined();
|
|
127
|
+
// Grant explicit approval
|
|
128
|
+
await engine.approveAction({
|
|
129
|
+
executionId: 'exec-admin-delete-1',
|
|
130
|
+
approverActorId: 'admin-super-user',
|
|
131
|
+
approverRole: 'administrator',
|
|
132
|
+
});
|
|
133
|
+
// Second attempt: approved
|
|
134
|
+
const res2 = await engine.evaluateAction(action);
|
|
135
|
+
expect(res2.decision.allowed).toBe(true);
|
|
136
|
+
expect(res2.decision.decisionCode).toBe('ALLOWED_POLICY');
|
|
137
|
+
expect(res2.capability).toBeDefined();
|
|
138
|
+
expect((0, capability_1.verifyCapabilitySignature)(res2.capability, 'test_secret_key')).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it('records structured tamper-evident audit log entries with hash chaining', async () => {
|
|
141
|
+
const action = {
|
|
142
|
+
actionId: 'act-audit',
|
|
143
|
+
toolName: 'builtin/search_web',
|
|
144
|
+
parameters: { query: 'Audit Test' },
|
|
145
|
+
context: sampleContext,
|
|
146
|
+
};
|
|
147
|
+
await engine.evaluateAction(action);
|
|
148
|
+
const auditLogs = await engine.getAuditLog();
|
|
149
|
+
expect(auditLogs.length).toBeGreaterThan(0);
|
|
150
|
+
const log = auditLogs.find((l) => l.actionId === 'act-audit');
|
|
151
|
+
expect(log).toBeDefined();
|
|
152
|
+
expect(log?.actorId).toBe('user-1');
|
|
153
|
+
expect(log?.toolName).toBe('builtin/search_web');
|
|
154
|
+
expect(log?.parametersHash).toBeDefined();
|
|
155
|
+
expect(log?.resultHash).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
it('supports single-owner role parity between owner and administrator', async () => {
|
|
158
|
+
const ownerTool = {
|
|
159
|
+
name: 'builtin/owner_tool',
|
|
160
|
+
description: 'Owner only tool',
|
|
161
|
+
inputSchema: {},
|
|
162
|
+
riskLevel: 'LOW',
|
|
163
|
+
allowedRoles: ['owner'],
|
|
164
|
+
};
|
|
165
|
+
engine.registerToolDefinition(ownerTool);
|
|
166
|
+
// Context with authorizationRole: 'administrator'
|
|
167
|
+
const adminContext = {
|
|
168
|
+
...sampleContext,
|
|
169
|
+
actor: {
|
|
170
|
+
...sampleContext.actor,
|
|
171
|
+
authorizationRole: 'administrator',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
const actionWithAdmin = {
|
|
175
|
+
actionId: 'act-owner-admin',
|
|
176
|
+
toolName: 'builtin/owner_tool',
|
|
177
|
+
parameters: {},
|
|
178
|
+
context: adminContext,
|
|
179
|
+
};
|
|
180
|
+
const res = await engine.evaluateAction(actionWithAdmin);
|
|
181
|
+
expect(res.decision.allowed).toBe(true);
|
|
182
|
+
expect(res.capability).toBeDefined();
|
|
183
|
+
// Context with authorizationRole: 'operator' (non-owner) is rejected
|
|
184
|
+
const operatorAction = {
|
|
185
|
+
actionId: 'act-owner-op',
|
|
186
|
+
toolName: 'builtin/owner_tool',
|
|
187
|
+
parameters: {},
|
|
188
|
+
context: sampleContext, // has authorizationRole: 'operator'
|
|
189
|
+
};
|
|
190
|
+
const resOp = await engine.evaluateAction(operatorAction);
|
|
191
|
+
expect(resOp.decision.allowed).toBe(false);
|
|
192
|
+
expect(resOp.decision.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
193
|
+
});
|
|
194
|
+
describe('Approval Authorization Semantics', () => {
|
|
195
|
+
const criticalAction = {
|
|
196
|
+
actionId: 'act-sec-auth-1',
|
|
197
|
+
toolName: 'admin/delete_database',
|
|
198
|
+
parameters: {},
|
|
199
|
+
context: {
|
|
200
|
+
companionId: 'companion-1',
|
|
201
|
+
actor: {
|
|
202
|
+
actorId: 'admin-1',
|
|
203
|
+
sessionId: 'sess-1',
|
|
204
|
+
authorizationRole: 'administrator',
|
|
205
|
+
capabilities: ['admin:delete'],
|
|
206
|
+
authenticated: true,
|
|
207
|
+
},
|
|
208
|
+
conversation: { channel: 'direct', correlationId: 'corr-1' },
|
|
209
|
+
},
|
|
210
|
+
executionId: 'exec-sec-auth-1',
|
|
211
|
+
};
|
|
212
|
+
it('rejects action approval when approverActorId is empty', async () => {
|
|
213
|
+
await engine.evaluateAction(criticalAction);
|
|
214
|
+
const res = await engine.approveAction({
|
|
215
|
+
executionId: 'exec-sec-auth-1',
|
|
216
|
+
approverActorId: '',
|
|
217
|
+
});
|
|
218
|
+
expect(res.approved).toBe(false);
|
|
219
|
+
expect(res.decisionCode).toBe('REJECTED_MISSING_APPROVER_ID');
|
|
220
|
+
});
|
|
221
|
+
it('rejects action approval from unauthenticated approver context', async () => {
|
|
222
|
+
await engine.evaluateAction(criticalAction);
|
|
223
|
+
const res = await engine.approveAction({
|
|
224
|
+
executionId: 'exec-sec-auth-1',
|
|
225
|
+
approverActorId: 'fake-admin',
|
|
226
|
+
context: {
|
|
227
|
+
companionId: 'companion-1',
|
|
228
|
+
actor: {
|
|
229
|
+
actorId: 'fake-admin',
|
|
230
|
+
sessionId: 'sess-fake',
|
|
231
|
+
authorizationRole: 'administrator',
|
|
232
|
+
capabilities: ['admin:delete'],
|
|
233
|
+
authenticated: false, // Unauthenticated!
|
|
234
|
+
},
|
|
235
|
+
conversation: { channel: 'direct', correlationId: 'corr-fake' },
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
expect(res.approved).toBe(false);
|
|
239
|
+
expect(res.decisionCode).toBe('REJECTED_UNAUTHENTICATED');
|
|
240
|
+
});
|
|
241
|
+
it('rejects action approval from viewer role', async () => {
|
|
242
|
+
await engine.evaluateAction(criticalAction);
|
|
243
|
+
const res = await engine.approveAction({
|
|
244
|
+
executionId: 'exec-sec-auth-1',
|
|
245
|
+
approverActorId: 'viewer-user',
|
|
246
|
+
approverRole: 'viewer',
|
|
247
|
+
});
|
|
248
|
+
expect(res.approved).toBe(false);
|
|
249
|
+
expect(res.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
250
|
+
});
|
|
251
|
+
it('rejects approval when approver role does not match tool requirements (operator cannot approve admin tool)', async () => {
|
|
252
|
+
// 1. Initial evaluation stages pending execution
|
|
253
|
+
const eval1 = await engine.evaluateAction(criticalAction);
|
|
254
|
+
expect(eval1.decision.allowed).toBe(false);
|
|
255
|
+
expect(eval1.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
|
|
256
|
+
// 2. Operator attempts to approve an administrator-only tool
|
|
257
|
+
const approvalRes = await engine.approveAction({
|
|
258
|
+
executionId: 'exec-sec-auth-1',
|
|
259
|
+
approverActorId: 'operator-alice',
|
|
260
|
+
approverRole: 'operator',
|
|
261
|
+
});
|
|
262
|
+
expect(approvalRes.approved).toBe(false);
|
|
263
|
+
expect(approvalRes.decisionCode).toBe('REJECTED_ROLE_MISMATCH');
|
|
264
|
+
// 3. Action re-evaluation remains unapproved and denied
|
|
265
|
+
const eval2 = await engine.evaluateAction(criticalAction);
|
|
266
|
+
expect(eval2.decision.allowed).toBe(false);
|
|
267
|
+
expect(eval2.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
|
|
268
|
+
});
|
|
269
|
+
it('authorizes approval and emits capability when approver is administrator or owner', async () => {
|
|
270
|
+
await engine.evaluateAction(criticalAction);
|
|
271
|
+
const approvalRes = await engine.approveAction({
|
|
272
|
+
executionId: 'exec-sec-auth-1',
|
|
273
|
+
approverActorId: 'admin-bob',
|
|
274
|
+
approverRole: 'administrator',
|
|
275
|
+
reason: 'Authorized scheduled database purge',
|
|
276
|
+
});
|
|
277
|
+
expect(approvalRes.approved).toBe(true);
|
|
278
|
+
expect(approvalRes.decisionCode).toBe('APPROVED');
|
|
279
|
+
const evalApproved = await engine.evaluateAction(criticalAction);
|
|
280
|
+
expect(evalApproved.decision.allowed).toBe(true);
|
|
281
|
+
expect(evalApproved.decision.decisionCode).toBe('ALLOWED_POLICY');
|
|
282
|
+
expect(evalApproved.capability).toBeDefined();
|
|
283
|
+
});
|
|
284
|
+
it('records structured tamper-evident audit logs for approval decisions', async () => {
|
|
285
|
+
await engine.evaluateAction(criticalAction);
|
|
286
|
+
await engine.approveAction({
|
|
287
|
+
executionId: 'exec-sec-auth-1',
|
|
288
|
+
approverActorId: 'viewer-tamper',
|
|
289
|
+
approverRole: 'viewer',
|
|
290
|
+
});
|
|
291
|
+
const auditLogs = await engine.getAuditLog();
|
|
292
|
+
const rejectionEvent = auditLogs.find((l) => l.executionId === 'exec-sec-auth-1' && l.lifecycle === 'REJECTED' && l.toolName === 'action:approve');
|
|
293
|
+
expect(rejectionEvent).toBeDefined();
|
|
294
|
+
expect(rejectionEvent?.actorId).toBe('viewer-tamper');
|
|
295
|
+
expect(rejectionEvent?.decision?.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
296
|
+
});
|
|
297
|
+
it('rejects approval attempt for arbitrary unknown execution ID', async () => {
|
|
298
|
+
const res = await engine.approveAction({
|
|
299
|
+
executionId: 'arbitrary-unregistered-id',
|
|
300
|
+
approverActorId: 'admin-bob',
|
|
301
|
+
approverRole: 'administrator',
|
|
302
|
+
});
|
|
303
|
+
expect(res.approved).toBe(false);
|
|
304
|
+
expect(res.decisionCode).toBe('REJECTED_UNKNOWN_EXECUTION');
|
|
305
|
+
});
|
|
306
|
+
it('rejects privilege escalation from actorId strings (attacker-admin is not an administrator without explicit role)', async () => {
|
|
307
|
+
await engine.evaluateAction(criticalAction);
|
|
308
|
+
const res = await engine.approveAction({
|
|
309
|
+
executionId: 'exec-sec-auth-1',
|
|
310
|
+
approverActorId: 'attacker-admin-user',
|
|
311
|
+
// Notice no approverRole is provided!
|
|
312
|
+
});
|
|
313
|
+
expect(res.approved).toBe(false);
|
|
314
|
+
expect(res.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
315
|
+
});
|
|
316
|
+
it('rejects action evaluation if parameters are tampered after approval (REJECTED_APPROVAL_MISMATCH)', async () => {
|
|
317
|
+
await engine.evaluateAction(criticalAction);
|
|
318
|
+
const approval = await engine.approveAction({
|
|
319
|
+
executionId: 'exec-sec-auth-1',
|
|
320
|
+
approverActorId: 'admin-bob',
|
|
321
|
+
approverRole: 'administrator',
|
|
322
|
+
});
|
|
323
|
+
expect(approval.approved).toBe(true);
|
|
324
|
+
// Attacker re-evaluates the same approved executionId but with injected dangerous parameters
|
|
325
|
+
const tamperedAction = {
|
|
326
|
+
...criticalAction,
|
|
327
|
+
parameters: { injected: 'malicious-payload' },
|
|
328
|
+
};
|
|
329
|
+
const evalTampered = await engine.evaluateAction(tamperedAction);
|
|
330
|
+
expect(evalTampered.decision.allowed).toBe(false);
|
|
331
|
+
expect(evalTampered.decision.decisionCode).toBe('REJECTED_APPROVAL_MISMATCH');
|
|
332
|
+
expect(evalTampered.capability).toBeUndefined();
|
|
333
|
+
});
|
|
334
|
+
it('rejects action evaluation if tool name is swapped after approval (REJECTED_APPROVAL_MISMATCH)', async () => {
|
|
335
|
+
await engine.evaluateAction(criticalAction);
|
|
336
|
+
const approval = await engine.approveAction({
|
|
337
|
+
executionId: 'exec-sec-auth-1',
|
|
338
|
+
approverActorId: 'admin-bob',
|
|
339
|
+
approverRole: 'administrator',
|
|
340
|
+
});
|
|
341
|
+
expect(approval.approved).toBe(true);
|
|
342
|
+
// Register another tool
|
|
343
|
+
engine.registerToolDefinition({
|
|
344
|
+
name: 'admin/format_drive',
|
|
345
|
+
providerId: 'admin',
|
|
346
|
+
description: 'Format drive',
|
|
347
|
+
inputSchema: {},
|
|
348
|
+
riskLevel: 'CRITICAL',
|
|
349
|
+
allowedRoles: ['administrator'],
|
|
350
|
+
requiresApproval: true,
|
|
351
|
+
});
|
|
352
|
+
// Attacker re-uses the executionId on a different tool
|
|
353
|
+
const swappedAction = {
|
|
354
|
+
...criticalAction,
|
|
355
|
+
toolName: 'admin/format_drive',
|
|
356
|
+
};
|
|
357
|
+
const evalSwapped = await engine.evaluateAction(swappedAction);
|
|
358
|
+
expect(evalSwapped.decision.allowed).toBe(false);
|
|
359
|
+
expect(evalSwapped.decision.decisionCode).toBe('REJECTED_APPROVAL_MISMATCH');
|
|
360
|
+
expect(evalSwapped.capability).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
package/dist/action.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { RequestContext } from './context';
|
|
2
|
+
export type ActionRiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
3
|
+
export type ActionLifecycleState = 'PROPOSED' | 'VALIDATED' | 'POLICY_CHECKED' | 'APPROVED' | 'EXECUTING' | 'COMPLETED' | 'REJECTED' | 'FAILED' | 'CANCELLED' | 'TIMED_OUT';
|
|
4
|
+
export interface ActionIntent {
|
|
5
|
+
actionId: string;
|
|
6
|
+
toolName: string;
|
|
7
|
+
parameters: Record<string, unknown>;
|
|
8
|
+
description?: string;
|
|
9
|
+
context?: RequestContext;
|
|
10
|
+
executionId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ActionPolicyDecision {
|
|
13
|
+
allowed: boolean;
|
|
14
|
+
reason: string;
|
|
15
|
+
riskLevel: ActionRiskLevel;
|
|
16
|
+
requiredCapabilities?: string[];
|
|
17
|
+
executionId: string;
|
|
18
|
+
decisionCode: 'ALLOWED_AUTO' | 'ALLOWED_POLICY' | 'REJECTED_UNKNOWN_TOOL' | 'REJECTED_UNAUTHORIZED' | 'REJECTED_MISSING_CAPABILITY' | 'REJECTED_HIGH_RISK_UNAPPROVED' | 'REJECTED_CHANNEL_RESTRICTED' | 'REJECTED_COMPANION_MISMATCH' | 'REJECTED_APPROVAL_MISMATCH' | 'REJECTED_POLICY';
|
|
19
|
+
}
|
|
20
|
+
export interface ActionAuditEvent {
|
|
21
|
+
executionId: string;
|
|
22
|
+
actionId: string;
|
|
23
|
+
toolName: string;
|
|
24
|
+
providerId?: string;
|
|
25
|
+
companionId: string;
|
|
26
|
+
actorId?: string;
|
|
27
|
+
sessionId?: string;
|
|
28
|
+
channel?: string;
|
|
29
|
+
correlationId?: string;
|
|
30
|
+
riskLevel: ActionRiskLevel;
|
|
31
|
+
lifecycle: ActionLifecycleState;
|
|
32
|
+
decision?: ActionPolicyDecision;
|
|
33
|
+
parametersHash?: string;
|
|
34
|
+
resultHash?: string;
|
|
35
|
+
eventHash?: string;
|
|
36
|
+
previousEventHash?: string;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
durationMs?: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface ActionExecutionResult {
|
|
42
|
+
actionId: string;
|
|
43
|
+
executionId: string;
|
|
44
|
+
toolName: string;
|
|
45
|
+
lifecycle: ActionLifecycleState;
|
|
46
|
+
success: boolean;
|
|
47
|
+
result?: unknown;
|
|
48
|
+
error?: string;
|
|
49
|
+
decision?: ActionPolicyDecision;
|
|
50
|
+
durationMs?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ToolDefinition {
|
|
53
|
+
name: string;
|
|
54
|
+
description: string;
|
|
55
|
+
inputSchema: Record<string, unknown>;
|
|
56
|
+
providerId?: string;
|
|
57
|
+
riskLevel?: ActionRiskLevel;
|
|
58
|
+
requiredCapabilities?: string[];
|
|
59
|
+
allowedRoles?: string[];
|
|
60
|
+
allowedChannels?: string[];
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
requiresApproval?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface ToolExecutionOptions {
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
}
|
|
68
|
+
export interface HandsOrgan {
|
|
69
|
+
listTools(): Promise<ToolDefinition[]>;
|
|
70
|
+
executeAction(action: ActionIntent, authorization: any, // AuthorizationCapability required
|
|
71
|
+
options?: ToolExecutionOptions): Promise<ActionExecutionResult>;
|
|
72
|
+
}
|
package/dist/action.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|