@siduri-x/core 1.0.5 → 1.0.8

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.
Files changed (85) hide show
  1. package/dist/action-executor.d.ts +12 -0
  2. package/dist/action-executor.js +50 -0
  3. package/dist/action-policy.d.ts +45 -0
  4. package/dist/action-policy.js +219 -0
  5. package/dist/action-policy.test.d.ts +1 -0
  6. package/dist/action-policy.test.js +193 -0
  7. package/dist/action.d.ts +72 -0
  8. package/dist/action.js +2 -0
  9. package/dist/adversarial.test.d.ts +1 -0
  10. package/dist/adversarial.test.js +489 -0
  11. package/dist/architecture-boundary.test.d.ts +1 -0
  12. package/dist/architecture-boundary.test.js +117 -0
  13. package/dist/capability.d.ts +65 -0
  14. package/dist/capability.js +157 -0
  15. package/dist/capability.test.d.ts +1 -0
  16. package/dist/capability.test.js +268 -0
  17. package/dist/chat-contract.d.ts +81 -0
  18. package/dist/chat-contract.js +68 -0
  19. package/dist/cognition-planner.d.ts +15 -0
  20. package/dist/cognition-planner.js +23 -0
  21. package/dist/context-retriever.d.ts +24 -0
  22. package/dist/context-retriever.js +90 -0
  23. package/dist/context.d.ts +44 -0
  24. package/dist/context.js +72 -0
  25. package/dist/context.test.d.ts +1 -0
  26. package/dist/context.test.js +76 -0
  27. package/dist/dispatcher.d.ts +14 -0
  28. package/dist/dispatcher.js +40 -0
  29. package/dist/dispatcher.test.d.ts +1 -0
  30. package/dist/dispatcher.test.js +58 -0
  31. package/dist/ear-types.d.ts +33 -0
  32. package/dist/ear-types.js +2 -0
  33. package/dist/evidence.d.ts +73 -0
  34. package/dist/evidence.js +27 -0
  35. package/dist/evidence.test.d.ts +1 -0
  36. package/dist/evidence.test.js +75 -0
  37. package/dist/experience-emitter.d.ts +21 -0
  38. package/dist/experience-emitter.js +47 -0
  39. package/dist/experience.d.ts +55 -0
  40. package/dist/experience.js +74 -0
  41. package/dist/experience.test.d.ts +1 -0
  42. package/dist/experience.test.js +55 -0
  43. package/dist/gating.d.ts +45 -0
  44. package/dist/gating.js +183 -0
  45. package/dist/gating.test.d.ts +1 -0
  46. package/dist/gating.test.js +186 -0
  47. package/dist/index.d.ts +250 -0
  48. package/dist/index.js +43 -0
  49. package/dist/input-normalizer.d.ts +14 -0
  50. package/dist/input-normalizer.js +58 -0
  51. package/dist/input-normalizer.test.d.ts +1 -0
  52. package/dist/input-normalizer.test.js +39 -0
  53. package/dist/intent-classifier.d.ts +24 -0
  54. package/dist/intent-classifier.js +53 -0
  55. package/dist/intent-classifier.test.d.ts +1 -0
  56. package/dist/intent-classifier.test.js +67 -0
  57. package/dist/memory-settler.d.ts +27 -0
  58. package/dist/memory-settler.js +92 -0
  59. package/dist/mouth-types.d.ts +85 -0
  60. package/dist/mouth-types.js +2 -0
  61. package/dist/perception-cycle.test.d.ts +1 -0
  62. package/dist/perception-cycle.test.js +153 -0
  63. package/dist/prompt-compiler.d.ts +20 -0
  64. package/dist/prompt-compiler.js +54 -0
  65. package/dist/prompt-compiler.test.d.ts +1 -0
  66. package/dist/prompt-compiler.test.js +75 -0
  67. package/dist/proposals.d.ts +29 -0
  68. package/dist/proposals.js +2 -0
  69. package/dist/response-envelope.d.ts +25 -0
  70. package/dist/response-envelope.js +64 -0
  71. package/dist/runtime-facades.test.d.ts +1 -0
  72. package/dist/runtime-facades.test.js +69 -0
  73. package/dist/runtime.d.ts +114 -0
  74. package/dist/runtime.js +412 -0
  75. package/dist/session-history.d.ts +20 -0
  76. package/dist/session-history.js +55 -0
  77. package/dist/session-history.test.d.ts +1 -0
  78. package/dist/session-history.test.js +38 -0
  79. package/dist/sqlite-action-store.d.ts +20 -0
  80. package/dist/sqlite-action-store.js +225 -0
  81. package/dist/sqlite-action-store.test.d.ts +1 -0
  82. package/dist/sqlite-action-store.test.js +251 -0
  83. package/dist/teaching.d.ts +15 -0
  84. package/dist/teaching.js +132 -0
  85. package/package.json +1 -1
@@ -0,0 +1,12 @@
1
+ import { ActionIntent, ActionExecutionResult, ActionPolicyEngine, HandsOrgan, RequestContext } from './index';
2
+ export interface ActionExecutionParams {
3
+ actionIntents?: ActionIntent[];
4
+ requestContext: RequestContext;
5
+ actionPolicy: ActionPolicyEngine;
6
+ hands?: HandsOrgan;
7
+ }
8
+ /**
9
+ * Executes proposed actions under the primary security invariant:
10
+ * "Brain proposes; the policy layer authorizes; Hands executes; the audit layer records."
11
+ */
12
+ export declare function executeActionIntents(params: ActionExecutionParams): Promise<ActionExecutionResult[]>;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeActionIntents = executeActionIntents;
4
+ /**
5
+ * Executes proposed actions under the primary security invariant:
6
+ * "Brain proposes; the policy layer authorizes; Hands executes; the audit layer records."
7
+ */
8
+ async function executeActionIntents(params) {
9
+ const { actionIntents, requestContext, actionPolicy, hands } = params;
10
+ const actionResults = [];
11
+ if (!hands ||
12
+ !actionIntents ||
13
+ actionIntents.length === 0 ||
14
+ typeof hands.executeAction !== 'function') {
15
+ return actionResults;
16
+ }
17
+ for (const rawAction of actionIntents) {
18
+ // 1. Context Propagation: Attach request provenance to ActionIntent
19
+ const actionWithContext = {
20
+ ...rawAction,
21
+ context: requestContext,
22
+ executionId: rawAction.executionId ||
23
+ `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
24
+ };
25
+ // 2. Action Policy Authorization Boundary Check
26
+ const { decision, capability } = await actionPolicy.evaluateAction(actionWithContext, requestContext);
27
+ if (!decision.allowed || !capability) {
28
+ // Action was rejected by policy
29
+ actionResults.push({
30
+ actionId: actionWithContext.actionId,
31
+ executionId: decision.executionId,
32
+ toolName: actionWithContext.toolName,
33
+ lifecycle: 'REJECTED',
34
+ success: false,
35
+ error: `Action authorization rejected by policy: ${decision.reason}`,
36
+ decision,
37
+ });
38
+ continue;
39
+ }
40
+ // 3. Hands Execution (only authorized actions execute with AuthorizationCapability)
41
+ const res = await hands.executeAction(actionWithContext, capability);
42
+ actionResults.push({
43
+ ...res,
44
+ decision,
45
+ });
46
+ // 4. Audit recording for execution outcome
47
+ await actionPolicy.recordAudit(actionWithContext, requestContext, decision, res.lifecycle, res.result, res.error, res.durationMs);
48
+ }
49
+ return actionResults;
50
+ }
@@ -0,0 +1,45 @@
1
+ import { RequestContext } from './context';
2
+ import { ActionIntent, ActionPolicyDecision, ActionAuditEvent, ToolDefinition, ActionRiskLevel, ActionLifecycleState } from './action';
3
+ import { AuthorizationCapability, ActionStore } from './capability';
4
+ export interface ActionPolicyRule {
5
+ toolNamePattern: string | RegExp;
6
+ riskLevel?: ActionRiskLevel;
7
+ requiredCapabilities?: string[];
8
+ allowedRoles?: string[];
9
+ allowedChannels?: string[];
10
+ requiresExplicitApproval?: boolean;
11
+ }
12
+ export interface ActionPolicyEngineOptions {
13
+ rules?: ActionPolicyRule[];
14
+ defaultRiskLevel?: ActionRiskLevel;
15
+ defaultRequireApprovalForHighRisk?: boolean;
16
+ store?: ActionStore;
17
+ secretKey?: string;
18
+ }
19
+ export interface ApproveActionOptions {
20
+ executionId: string;
21
+ approverActorId: string;
22
+ reason?: string;
23
+ }
24
+ export declare class ActionPolicyEngine {
25
+ private readonly toolRegistry;
26
+ private readonly rules;
27
+ private readonly store;
28
+ private readonly defaultRiskLevel;
29
+ private readonly defaultRequireApprovalForHighRisk;
30
+ private readonly secretKey;
31
+ private readonly approvedExecutions;
32
+ constructor(options?: ActionPolicyEngineOptions);
33
+ registerToolDefinition(tool: ToolDefinition): void;
34
+ unregisterToolDefinition(toolName: string): boolean;
35
+ getRegisteredTools(): ToolDefinition[];
36
+ findToolDefinition(toolName: string): ToolDefinition | undefined;
37
+ evaluateAction(action: ActionIntent, context?: RequestContext): Promise<{
38
+ decision: ActionPolicyDecision;
39
+ capability?: AuthorizationCapability;
40
+ }>;
41
+ approveAction(options: ApproveActionOptions): Promise<boolean>;
42
+ recordAudit(action: ActionIntent, context: RequestContext | undefined, decision: ActionPolicyDecision | undefined, lifecycle: ActionLifecycleState, result?: unknown, error?: string, durationMs?: number): Promise<ActionAuditEvent>;
43
+ getAuditLog(): Promise<ActionAuditEvent[]>;
44
+ getStore(): ActionStore;
45
+ }
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActionPolicyEngine = void 0;
4
+ const capability_1 = require("./capability");
5
+ class ActionPolicyEngine {
6
+ toolRegistry = new Map();
7
+ rules = [];
8
+ store;
9
+ defaultRiskLevel;
10
+ defaultRequireApprovalForHighRisk;
11
+ secretKey;
12
+ approvedExecutions = new Set();
13
+ constructor(options = {}) {
14
+ this.rules = options.rules ?? [];
15
+ this.defaultRiskLevel = options.defaultRiskLevel ?? 'HIGH';
16
+ this.defaultRequireApprovalForHighRisk = options.defaultRequireApprovalForHighRisk ?? true;
17
+ this.store = options.store ?? new capability_1.InMemoryActionStore();
18
+ this.secretKey = (0, capability_1.getOrGenerateLocalActionPolicySecret)(options.secretKey);
19
+ }
20
+ registerToolDefinition(tool) {
21
+ const key = tool.providerId ? `${tool.providerId}/${tool.name}` : tool.name;
22
+ this.toolRegistry.set(key, tool);
23
+ // Also index by bare name if unique
24
+ if (!this.toolRegistry.has(tool.name)) {
25
+ this.toolRegistry.set(tool.name, tool);
26
+ }
27
+ }
28
+ unregisterToolDefinition(toolName) {
29
+ return this.toolRegistry.delete(toolName);
30
+ }
31
+ getRegisteredTools() {
32
+ const set = new Set(this.toolRegistry.values());
33
+ return Array.from(set);
34
+ }
35
+ findToolDefinition(toolName) {
36
+ return this.toolRegistry.get(toolName);
37
+ }
38
+ async evaluateAction(action, context) {
39
+ const effectiveContext = action.context || context;
40
+ const executionId = action.executionId || `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
41
+ action.executionId = executionId;
42
+ const toolDef = this.findToolDefinition(action.toolName);
43
+ const paramsHash = (0, capability_1.computeParametersHash)(action.parameters);
44
+ // Rule: Unknown tools/actions must not be implicitly authorized
45
+ if (!toolDef) {
46
+ const decision = {
47
+ allowed: false,
48
+ reason: `Tool "${action.toolName}" is not registered or known in action policy`,
49
+ riskLevel: 'CRITICAL',
50
+ executionId,
51
+ decisionCode: 'REJECTED_UNKNOWN_TOOL',
52
+ };
53
+ await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
54
+ return { decision };
55
+ }
56
+ const riskLevel = toolDef.riskLevel || this.defaultRiskLevel;
57
+ const requiredCaps = toolDef.requiredCapabilities || [];
58
+ // Check request context presence
59
+ if (!effectiveContext) {
60
+ const decision = {
61
+ allowed: false,
62
+ reason: 'Missing RequestContext: Actions cannot be authorized without request context',
63
+ riskLevel,
64
+ requiredCapabilities: requiredCaps,
65
+ executionId,
66
+ decisionCode: 'REJECTED_UNAUTHORIZED',
67
+ };
68
+ await this.recordAudit(action, undefined, decision, 'REJECTED');
69
+ return { decision };
70
+ }
71
+ // Role check if tool restricts roles (supports administrator/owner role parity)
72
+ if (toolDef.allowedRoles && toolDef.allowedRoles.length > 0) {
73
+ const actorRole = effectiveContext.actor.authorizationRole || effectiveContext.actor.role || 'owner';
74
+ const normalizedActorRoles = new Set([actorRole.toLowerCase()]);
75
+ if (actorRole.toLowerCase() === 'administrator' || actorRole.toLowerCase() === 'owner') {
76
+ normalizedActorRoles.add('administrator');
77
+ normalizedActorRoles.add('owner');
78
+ normalizedActorRoles.add('admin');
79
+ }
80
+ const isAuthorizedRole = toolDef.allowedRoles.some((r) => normalizedActorRoles.has(r.toLowerCase()));
81
+ if (!isAuthorizedRole) {
82
+ const decision = {
83
+ allowed: false,
84
+ reason: `Actor role "${actorRole}" is not authorized to execute tool "${action.toolName}"`,
85
+ riskLevel,
86
+ requiredCapabilities: requiredCaps,
87
+ executionId,
88
+ decisionCode: 'REJECTED_UNAUTHORIZED',
89
+ };
90
+ await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
91
+ return { decision };
92
+ }
93
+ }
94
+ // Channel check if tool restricts channels
95
+ if (toolDef.allowedChannels && toolDef.allowedChannels.length > 0) {
96
+ const channel = effectiveContext.conversation.channel || 'direct';
97
+ if (!toolDef.allowedChannels.includes(channel)) {
98
+ const decision = {
99
+ allowed: false,
100
+ reason: `Tool "${action.toolName}" cannot be executed in channel "${channel}"`,
101
+ riskLevel,
102
+ requiredCapabilities: requiredCaps,
103
+ executionId,
104
+ decisionCode: 'REJECTED_CHANNEL_RESTRICTED',
105
+ };
106
+ await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
107
+ return { decision };
108
+ }
109
+ }
110
+ // Capability check
111
+ const actorCaps = effectiveContext.actor.capabilities || [];
112
+ const missingCaps = requiredCaps.filter((cap) => !actorCaps.includes(cap));
113
+ if (missingCaps.length > 0) {
114
+ const decision = {
115
+ allowed: false,
116
+ reason: `Actor is missing required capabilities: [${missingCaps.join(', ')}]`,
117
+ riskLevel,
118
+ requiredCapabilities: requiredCaps,
119
+ executionId,
120
+ decisionCode: 'REJECTED_MISSING_CAPABILITY',
121
+ };
122
+ await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
123
+ return { decision };
124
+ }
125
+ // Risk level approval check
126
+ const requiresExplicitApproval = toolDef.requiresApproval ??
127
+ (this.defaultRequireApprovalForHighRisk && (riskLevel === 'HIGH' || riskLevel === 'CRITICAL'));
128
+ let isApproved = this.approvedExecutions.has(executionId);
129
+ if (!isApproved && typeof this.store.isActionApproved === 'function') {
130
+ isApproved = await this.store.isActionApproved(executionId);
131
+ if (isApproved) {
132
+ this.approvedExecutions.add(executionId);
133
+ }
134
+ }
135
+ if (requiresExplicitApproval && !isApproved) {
136
+ const decision = {
137
+ allowed: false,
138
+ reason: `Action "${action.toolName}" has risk level ${riskLevel} and requires explicit approval`,
139
+ riskLevel,
140
+ requiredCapabilities: requiredCaps,
141
+ executionId,
142
+ decisionCode: 'REJECTED_HIGH_RISK_UNAPPROVED',
143
+ };
144
+ await this.recordAudit(action, effectiveContext, decision, 'POLICY_CHECKED');
145
+ return { decision };
146
+ }
147
+ const decision = {
148
+ allowed: true,
149
+ reason: 'Action authorized by policy',
150
+ riskLevel,
151
+ requiredCapabilities: requiredCaps,
152
+ executionId,
153
+ decisionCode: 'ALLOWED_POLICY',
154
+ };
155
+ // Issue cryptographic/structural AuthorizationCapability
156
+ const issuedAt = new Date().toISOString();
157
+ const expiresAt = new Date(Date.now() + 60_000).toISOString();
158
+ const providerId = toolDef.providerId || 'builtin';
159
+ const capabilityPayload = {
160
+ executionId,
161
+ actionId: action.actionId,
162
+ toolName: action.toolName,
163
+ providerId,
164
+ parametersHash: paramsHash,
165
+ companionId: effectiveContext.companionId,
166
+ actorId: effectiveContext.actor.actorId,
167
+ sessionId: effectiveContext.actor.sessionId,
168
+ channel: effectiveContext.conversation.channel,
169
+ correlationId: effectiveContext.conversation.correlationId,
170
+ riskLevel,
171
+ issuedAt,
172
+ expiresAt,
173
+ };
174
+ const signature = (0, capability_1.signCapabilityPayload)(capabilityPayload, this.secretKey);
175
+ const capability = {
176
+ ...capabilityPayload,
177
+ allowed: true,
178
+ signature,
179
+ };
180
+ await this.recordAudit(action, effectiveContext, decision, 'APPROVED');
181
+ return { decision, capability };
182
+ }
183
+ async approveAction(options) {
184
+ this.approvedExecutions.add(options.executionId);
185
+ if (typeof this.store.saveApproval === 'function') {
186
+ await this.store.saveApproval(options.executionId, options.approverActorId, options.reason);
187
+ }
188
+ return true;
189
+ }
190
+ async recordAudit(action, context, decision, lifecycle, result, error, durationMs) {
191
+ const event = {
192
+ executionId: action.executionId || decision?.executionId || 'unknown',
193
+ actionId: action.actionId,
194
+ toolName: action.toolName,
195
+ companionId: context?.companionId || 'unknown',
196
+ actorId: context?.actor.actorId,
197
+ sessionId: context?.actor.sessionId,
198
+ channel: context?.conversation.channel,
199
+ correlationId: context?.conversation.correlationId,
200
+ riskLevel: decision?.riskLevel || 'LOW',
201
+ lifecycle,
202
+ decision,
203
+ parametersHash: (0, capability_1.computeParametersHash)(action.parameters),
204
+ resultHash: result !== undefined ? (0, capability_1.computeParametersHash)(result) : undefined,
205
+ timestamp: new Date().toISOString(),
206
+ durationMs,
207
+ error,
208
+ };
209
+ await this.store.appendAudit(event);
210
+ return event;
211
+ }
212
+ async getAuditLog() {
213
+ return this.store.getAuditLog();
214
+ }
215
+ getStore() {
216
+ return this.store;
217
+ }
218
+ }
219
+ exports.ActionPolicyEngine = ActionPolicyEngine;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,193 @@
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
+ engine.approveAction({
129
+ executionId: 'exec-admin-delete-1',
130
+ approverActorId: 'admin-super-user',
131
+ });
132
+ // Second attempt: approved
133
+ const res2 = await engine.evaluateAction(action);
134
+ expect(res2.decision.allowed).toBe(true);
135
+ expect(res2.decision.decisionCode).toBe('ALLOWED_POLICY');
136
+ expect(res2.capability).toBeDefined();
137
+ expect((0, capability_1.verifyCapabilitySignature)(res2.capability, 'test_secret_key')).toBe(true);
138
+ });
139
+ it('records structured tamper-evident audit log entries with hash chaining', async () => {
140
+ const action = {
141
+ actionId: 'act-audit',
142
+ toolName: 'builtin/search_web',
143
+ parameters: { query: 'Audit Test' },
144
+ context: sampleContext,
145
+ };
146
+ await engine.evaluateAction(action);
147
+ const auditLogs = await engine.getAuditLog();
148
+ expect(auditLogs.length).toBeGreaterThan(0);
149
+ const log = auditLogs.find((l) => l.actionId === 'act-audit');
150
+ expect(log).toBeDefined();
151
+ expect(log?.actorId).toBe('user-1');
152
+ expect(log?.toolName).toBe('builtin/search_web');
153
+ expect(log?.parametersHash).toBeDefined();
154
+ expect(log?.resultHash).toBeDefined();
155
+ });
156
+ it('supports single-owner role parity between owner and administrator', async () => {
157
+ const ownerTool = {
158
+ name: 'builtin/owner_tool',
159
+ description: 'Owner only tool',
160
+ inputSchema: {},
161
+ riskLevel: 'LOW',
162
+ allowedRoles: ['owner'],
163
+ };
164
+ engine.registerToolDefinition(ownerTool);
165
+ // Context with authorizationRole: 'administrator'
166
+ const adminContext = {
167
+ ...sampleContext,
168
+ actor: {
169
+ ...sampleContext.actor,
170
+ authorizationRole: 'administrator',
171
+ },
172
+ };
173
+ const actionWithAdmin = {
174
+ actionId: 'act-owner-admin',
175
+ toolName: 'builtin/owner_tool',
176
+ parameters: {},
177
+ context: adminContext,
178
+ };
179
+ const res = await engine.evaluateAction(actionWithAdmin);
180
+ expect(res.decision.allowed).toBe(true);
181
+ expect(res.capability).toBeDefined();
182
+ // Context with authorizationRole: 'operator' (non-owner) is rejected
183
+ const operatorAction = {
184
+ actionId: 'act-owner-op',
185
+ toolName: 'builtin/owner_tool',
186
+ parameters: {},
187
+ context: sampleContext, // has authorizationRole: 'operator'
188
+ };
189
+ const resOp = await engine.evaluateAction(operatorAction);
190
+ expect(resOp.decision.allowed).toBe(false);
191
+ expect(resOp.decision.decisionCode).toBe('REJECTED_UNAUTHORIZED');
192
+ });
193
+ });
@@ -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_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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export {};