@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,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,54 @@
|
|
|
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
|
+
parameters: {
|
|
23
|
+
...(requestContext.companionId ? { companionId: requestContext.companionId } : {}),
|
|
24
|
+
...(rawAction.parameters || {}),
|
|
25
|
+
},
|
|
26
|
+
executionId: rawAction.executionId ||
|
|
27
|
+
`exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
28
|
+
};
|
|
29
|
+
// 2. Action Policy Authorization Boundary Check
|
|
30
|
+
const { decision, capability } = await actionPolicy.evaluateAction(actionWithContext, requestContext);
|
|
31
|
+
if (!decision.allowed || !capability) {
|
|
32
|
+
// Action was rejected by policy
|
|
33
|
+
actionResults.push({
|
|
34
|
+
actionId: actionWithContext.actionId,
|
|
35
|
+
executionId: decision.executionId,
|
|
36
|
+
toolName: actionWithContext.toolName,
|
|
37
|
+
lifecycle: 'REJECTED',
|
|
38
|
+
success: false,
|
|
39
|
+
error: `Action authorization rejected by policy: ${decision.reason}`,
|
|
40
|
+
decision,
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// 3. Hands Execution (only authorized actions execute with AuthorizationCapability)
|
|
45
|
+
const res = await hands.executeAction(actionWithContext, capability);
|
|
46
|
+
actionResults.push({
|
|
47
|
+
...res,
|
|
48
|
+
decision,
|
|
49
|
+
});
|
|
50
|
+
// 4. Audit recording for execution outcome
|
|
51
|
+
await actionPolicy.recordAudit(actionWithContext, requestContext, decision, res.lifecycle, res.result, res.error, res.durationMs);
|
|
52
|
+
}
|
|
53
|
+
return actionResults;
|
|
54
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
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' | 'REJECTED_UNKNOWN_EXECUTION';
|
|
22
|
+
export interface ActionApprovalResult {
|
|
23
|
+
approved: boolean;
|
|
24
|
+
decisionCode: ActionApprovalDecisionCode;
|
|
25
|
+
reason: string;
|
|
26
|
+
executionId: string;
|
|
27
|
+
approverActorId: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ApproveActionOptions {
|
|
30
|
+
executionId: string;
|
|
31
|
+
approverActorId: string;
|
|
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[];
|
|
45
|
+
/**
|
|
46
|
+
* Optional explicit action binding parameters when pre-authorizing a known action
|
|
47
|
+
*/
|
|
48
|
+
toolName?: string;
|
|
49
|
+
parametersHash?: string;
|
|
50
|
+
companionId?: string;
|
|
51
|
+
actorId?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function normalizeApproverRole(role?: string, actorId?: string): string;
|
|
54
|
+
export declare class ActionPolicyEngine {
|
|
55
|
+
private readonly toolRegistry;
|
|
56
|
+
private readonly rules;
|
|
57
|
+
private readonly store;
|
|
58
|
+
private readonly defaultRiskLevel;
|
|
59
|
+
private readonly defaultRequireApprovalForHighRisk;
|
|
60
|
+
private readonly secretKey;
|
|
61
|
+
private readonly allowedApproverRoles;
|
|
62
|
+
private readonly requiredApproverCapabilities;
|
|
63
|
+
private readonly approvedExecutions;
|
|
64
|
+
private readonly pendingActions;
|
|
65
|
+
constructor(options?: ActionPolicyEngineOptions);
|
|
66
|
+
registerToolDefinition(tool: ToolDefinition): void;
|
|
67
|
+
unregisterToolDefinition(toolName: string): boolean;
|
|
68
|
+
getRegisteredTools(): ToolDefinition[];
|
|
69
|
+
findToolDefinition(toolName: string): ToolDefinition | undefined;
|
|
70
|
+
evaluateAction(action: ActionIntent, context?: RequestContext): Promise<{
|
|
71
|
+
decision: ActionPolicyDecision;
|
|
72
|
+
capability?: AuthorizationCapability;
|
|
73
|
+
}>;
|
|
74
|
+
approveAction(options: ApproveActionOptions): Promise<ActionApprovalResult>;
|
|
75
|
+
private recordApprovalAudit;
|
|
76
|
+
recordAudit(action: ActionIntent, context: RequestContext | undefined, decision: ActionPolicyDecision | undefined, lifecycle: ActionLifecycleState, result?: unknown, error?: string, durationMs?: number): Promise<ActionAuditEvent>;
|
|
77
|
+
getAuditLog(): Promise<ActionAuditEvent[]>;
|
|
78
|
+
getStore(): ActionStore;
|
|
79
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ActionPolicyEngine = void 0;
|
|
4
|
+
exports.normalizeApproverRole = normalizeApproverRole;
|
|
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
|
+
// Principle of least privilege: Never derive privileged roles (administrator, owner, operator)
|
|
14
|
+
// from arbitrary substring matching on unverified actorId strings.
|
|
15
|
+
// When an explicit authorization role is not provided, default to 'viewer'.
|
|
16
|
+
return 'viewer';
|
|
17
|
+
}
|
|
18
|
+
class ActionPolicyEngine {
|
|
19
|
+
toolRegistry = new Map();
|
|
20
|
+
rules = [];
|
|
21
|
+
store;
|
|
22
|
+
defaultRiskLevel;
|
|
23
|
+
defaultRequireApprovalForHighRisk;
|
|
24
|
+
secretKey;
|
|
25
|
+
allowedApproverRoles;
|
|
26
|
+
requiredApproverCapabilities;
|
|
27
|
+
approvedExecutions = new Map();
|
|
28
|
+
pendingActions = new Map();
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.rules = options.rules ?? [];
|
|
31
|
+
this.defaultRiskLevel = options.defaultRiskLevel ?? 'HIGH';
|
|
32
|
+
this.defaultRequireApprovalForHighRisk = options.defaultRequireApprovalForHighRisk ?? true;
|
|
33
|
+
this.store = options.store ?? new capability_1.InMemoryActionStore();
|
|
34
|
+
this.secretKey = (0, capability_1.getOrGenerateLocalActionPolicySecret)(options.secretKey);
|
|
35
|
+
this.allowedApproverRoles = (options.allowedApproverRoles ?? ['owner', 'administrator', 'admin', 'operator']).map((r) => r.toLowerCase());
|
|
36
|
+
this.requiredApproverCapabilities = options.requiredApproverCapabilities ?? [];
|
|
37
|
+
}
|
|
38
|
+
registerToolDefinition(tool) {
|
|
39
|
+
const key = tool.providerId ? `${tool.providerId}/${tool.name}` : tool.name;
|
|
40
|
+
this.toolRegistry.set(key, tool);
|
|
41
|
+
// Also index by bare name if unique
|
|
42
|
+
if (!this.toolRegistry.has(tool.name)) {
|
|
43
|
+
this.toolRegistry.set(tool.name, tool);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
unregisterToolDefinition(toolName) {
|
|
47
|
+
return this.toolRegistry.delete(toolName);
|
|
48
|
+
}
|
|
49
|
+
getRegisteredTools() {
|
|
50
|
+
const set = new Set(this.toolRegistry.values());
|
|
51
|
+
return Array.from(set);
|
|
52
|
+
}
|
|
53
|
+
findToolDefinition(toolName) {
|
|
54
|
+
return this.toolRegistry.get(toolName);
|
|
55
|
+
}
|
|
56
|
+
async evaluateAction(action, context) {
|
|
57
|
+
const effectiveContext = action.context || context;
|
|
58
|
+
const executionId = action.executionId || `exec-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
59
|
+
action.executionId = executionId;
|
|
60
|
+
const toolDef = this.findToolDefinition(action.toolName);
|
|
61
|
+
const paramsHash = (0, capability_1.computeParametersHash)(action.parameters);
|
|
62
|
+
// Rule: Unknown tools/actions must not be implicitly authorized
|
|
63
|
+
if (!toolDef) {
|
|
64
|
+
const decision = {
|
|
65
|
+
allowed: false,
|
|
66
|
+
reason: `Tool "${action.toolName}" is not registered or known in action policy`,
|
|
67
|
+
riskLevel: 'CRITICAL',
|
|
68
|
+
executionId,
|
|
69
|
+
decisionCode: 'REJECTED_UNKNOWN_TOOL',
|
|
70
|
+
};
|
|
71
|
+
await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
|
|
72
|
+
return { decision };
|
|
73
|
+
}
|
|
74
|
+
const riskLevel = toolDef.riskLevel || this.defaultRiskLevel;
|
|
75
|
+
const requiredCaps = toolDef.requiredCapabilities || [];
|
|
76
|
+
// Check request context presence
|
|
77
|
+
if (!effectiveContext) {
|
|
78
|
+
const decision = {
|
|
79
|
+
allowed: false,
|
|
80
|
+
reason: 'Missing RequestContext: Actions cannot be authorized without request context',
|
|
81
|
+
riskLevel,
|
|
82
|
+
requiredCapabilities: requiredCaps,
|
|
83
|
+
executionId,
|
|
84
|
+
decisionCode: 'REJECTED_UNAUTHORIZED',
|
|
85
|
+
};
|
|
86
|
+
await this.recordAudit(action, undefined, decision, 'REJECTED');
|
|
87
|
+
return { decision };
|
|
88
|
+
}
|
|
89
|
+
// Role check if tool restricts roles (supports administrator/owner role parity)
|
|
90
|
+
if (toolDef.allowedRoles && toolDef.allowedRoles.length > 0) {
|
|
91
|
+
const actorRole = effectiveContext.actor.authorizationRole ||
|
|
92
|
+
effectiveContext.actor.role ||
|
|
93
|
+
(effectiveContext.actor.authenticated === false ? 'viewer' : 'owner');
|
|
94
|
+
const normalizedActorRoles = new Set([actorRole.toLowerCase()]);
|
|
95
|
+
if (actorRole.toLowerCase() === 'administrator' || actorRole.toLowerCase() === 'owner') {
|
|
96
|
+
normalizedActorRoles.add('administrator');
|
|
97
|
+
normalizedActorRoles.add('owner');
|
|
98
|
+
normalizedActorRoles.add('admin');
|
|
99
|
+
}
|
|
100
|
+
const isAuthorizedRole = toolDef.allowedRoles.some((r) => normalizedActorRoles.has(r.toLowerCase()));
|
|
101
|
+
if (!isAuthorizedRole) {
|
|
102
|
+
const decision = {
|
|
103
|
+
allowed: false,
|
|
104
|
+
reason: `Actor role "${actorRole}" is not authorized to execute tool "${action.toolName}"`,
|
|
105
|
+
riskLevel,
|
|
106
|
+
requiredCapabilities: requiredCaps,
|
|
107
|
+
executionId,
|
|
108
|
+
decisionCode: 'REJECTED_UNAUTHORIZED',
|
|
109
|
+
};
|
|
110
|
+
await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
|
|
111
|
+
return { decision };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Channel check if tool restricts channels
|
|
115
|
+
if (toolDef.allowedChannels && toolDef.allowedChannels.length > 0) {
|
|
116
|
+
const channel = effectiveContext.conversation?.channel || 'direct';
|
|
117
|
+
if (!toolDef.allowedChannels.includes(channel)) {
|
|
118
|
+
const decision = {
|
|
119
|
+
allowed: false,
|
|
120
|
+
reason: `Tool "${action.toolName}" cannot be executed in channel "${channel}"`,
|
|
121
|
+
riskLevel,
|
|
122
|
+
requiredCapabilities: requiredCaps,
|
|
123
|
+
executionId,
|
|
124
|
+
decisionCode: 'REJECTED_CHANNEL_RESTRICTED',
|
|
125
|
+
};
|
|
126
|
+
await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
|
|
127
|
+
return { decision };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Capability check
|
|
131
|
+
const actorCaps = effectiveContext.actor.capabilities || [];
|
|
132
|
+
const missingCaps = requiredCaps.filter((cap) => !actorCaps.includes(cap));
|
|
133
|
+
if (missingCaps.length > 0) {
|
|
134
|
+
const decision = {
|
|
135
|
+
allowed: false,
|
|
136
|
+
reason: `Actor is missing required capabilities: [${missingCaps.join(', ')}]`,
|
|
137
|
+
riskLevel,
|
|
138
|
+
requiredCapabilities: requiredCaps,
|
|
139
|
+
executionId,
|
|
140
|
+
decisionCode: 'REJECTED_MISSING_CAPABILITY',
|
|
141
|
+
};
|
|
142
|
+
await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
|
|
143
|
+
return { decision };
|
|
144
|
+
}
|
|
145
|
+
// Risk level approval check
|
|
146
|
+
const requiresExplicitApproval = toolDef.requiresApproval ??
|
|
147
|
+
(this.defaultRequireApprovalForHighRisk && (riskLevel === 'HIGH' || riskLevel === 'CRITICAL'));
|
|
148
|
+
let approvalRecord = this.approvedExecutions.get(executionId);
|
|
149
|
+
if (!approvalRecord && typeof this.store.getApproval === 'function') {
|
|
150
|
+
approvalRecord = await this.store.getApproval(executionId);
|
|
151
|
+
if (approvalRecord) {
|
|
152
|
+
this.approvedExecutions.set(executionId, approvalRecord);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Cryptographic / structural binding verification:
|
|
156
|
+
// Ensure the approval record matches the exact tool, parameters, companion, and actor
|
|
157
|
+
if (approvalRecord) {
|
|
158
|
+
const toolMatch = !approvalRecord.toolName || approvalRecord.toolName === action.toolName;
|
|
159
|
+
const paramsMatch = !approvalRecord.parametersHash || approvalRecord.parametersHash === paramsHash;
|
|
160
|
+
const companionMatch = !approvalRecord.companionId || approvalRecord.companionId === effectiveContext.companionId;
|
|
161
|
+
const actorMatch = !approvalRecord.actorId || approvalRecord.actorId === effectiveContext.actor.actorId;
|
|
162
|
+
if (!toolMatch || !paramsMatch || !companionMatch || !actorMatch) {
|
|
163
|
+
const decision = {
|
|
164
|
+
allowed: false,
|
|
165
|
+
reason: `Approval record for execution "${executionId}" does not match the requested tool, parameters, companion, or actor`,
|
|
166
|
+
riskLevel,
|
|
167
|
+
requiredCapabilities: requiredCaps,
|
|
168
|
+
executionId,
|
|
169
|
+
decisionCode: 'REJECTED_APPROVAL_MISMATCH',
|
|
170
|
+
};
|
|
171
|
+
await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
|
|
172
|
+
return { decision };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let isApproved = Boolean(approvalRecord);
|
|
176
|
+
if (!isApproved && typeof this.store.isActionApproved === 'function') {
|
|
177
|
+
isApproved = await this.store.isActionApproved(executionId);
|
|
178
|
+
}
|
|
179
|
+
if (requiresExplicitApproval && isApproved && approvalRecord) {
|
|
180
|
+
const approverRole = (approvalRecord.approverRole || normalizeApproverRole(undefined, approvalRecord.approverActorId)).toLowerCase();
|
|
181
|
+
const isOwnerOrAdmin = approverRole === 'owner' || approverRole === 'administrator' || approverRole === 'admin';
|
|
182
|
+
if (toolDef.allowedRoles && toolDef.allowedRoles.length > 0 && !isOwnerOrAdmin) {
|
|
183
|
+
const allowed = toolDef.allowedRoles.map((r) => r.toLowerCase());
|
|
184
|
+
if (!allowed.includes(approverRole)) {
|
|
185
|
+
const decision = {
|
|
186
|
+
allowed: false,
|
|
187
|
+
reason: `Approver "${approvalRecord.approverActorId}" with role "${approverRole}" is not authorized to approve tool "${action.toolName}"`,
|
|
188
|
+
riskLevel,
|
|
189
|
+
requiredCapabilities: requiredCaps,
|
|
190
|
+
executionId,
|
|
191
|
+
decisionCode: 'REJECTED_UNAUTHORIZED',
|
|
192
|
+
};
|
|
193
|
+
await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
|
|
194
|
+
return { decision };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (requiresExplicitApproval && !isApproved) {
|
|
199
|
+
this.pendingActions.set(executionId, { action, context: effectiveContext, toolDef });
|
|
200
|
+
if (typeof this.store.reserveExecution === 'function') {
|
|
201
|
+
const record = {
|
|
202
|
+
executionId,
|
|
203
|
+
actionId: action.actionId,
|
|
204
|
+
toolName: action.toolName,
|
|
205
|
+
providerId: toolDef.providerId || 'builtin',
|
|
206
|
+
parametersHash: paramsHash,
|
|
207
|
+
lifecycle: 'POLICY_CHECKED',
|
|
208
|
+
decision: {
|
|
209
|
+
allowed: false,
|
|
210
|
+
reason: `Action "${action.toolName}" has risk level ${riskLevel} and requires explicit approval`,
|
|
211
|
+
riskLevel,
|
|
212
|
+
requiredCapabilities: requiredCaps,
|
|
213
|
+
executionId,
|
|
214
|
+
decisionCode: 'REJECTED_HIGH_RISK_UNAPPROVED',
|
|
215
|
+
},
|
|
216
|
+
createdAt: new Date().toISOString(),
|
|
217
|
+
updatedAt: new Date().toISOString(),
|
|
218
|
+
};
|
|
219
|
+
await this.store.reserveExecution(record).catch(() => { });
|
|
220
|
+
}
|
|
221
|
+
const decision = {
|
|
222
|
+
allowed: false,
|
|
223
|
+
reason: `Action "${action.toolName}" has risk level ${riskLevel} and requires explicit approval`,
|
|
224
|
+
riskLevel,
|
|
225
|
+
requiredCapabilities: requiredCaps,
|
|
226
|
+
executionId,
|
|
227
|
+
decisionCode: 'REJECTED_HIGH_RISK_UNAPPROVED',
|
|
228
|
+
};
|
|
229
|
+
await this.recordAudit(action, effectiveContext, decision, 'POLICY_CHECKED');
|
|
230
|
+
return { decision };
|
|
231
|
+
}
|
|
232
|
+
const decision = {
|
|
233
|
+
allowed: true,
|
|
234
|
+
reason: 'Action authorized by policy',
|
|
235
|
+
riskLevel,
|
|
236
|
+
requiredCapabilities: requiredCaps,
|
|
237
|
+
executionId,
|
|
238
|
+
decisionCode: 'ALLOWED_POLICY',
|
|
239
|
+
};
|
|
240
|
+
// Issue cryptographic/structural AuthorizationCapability
|
|
241
|
+
const issuedAt = new Date().toISOString();
|
|
242
|
+
const expiresAt = new Date(Date.now() + 60_000).toISOString();
|
|
243
|
+
const providerId = toolDef.providerId || 'builtin';
|
|
244
|
+
const capabilityPayload = {
|
|
245
|
+
executionId,
|
|
246
|
+
actionId: action.actionId,
|
|
247
|
+
toolName: action.toolName,
|
|
248
|
+
providerId,
|
|
249
|
+
parametersHash: paramsHash,
|
|
250
|
+
companionId: effectiveContext.companionId,
|
|
251
|
+
actorId: effectiveContext.actor?.actorId || 'owner-user',
|
|
252
|
+
sessionId: effectiveContext.actor?.sessionId || `sess-${effectiveContext.companionId}`,
|
|
253
|
+
channel: effectiveContext.conversation?.channel || 'direct',
|
|
254
|
+
correlationId: effectiveContext.conversation?.correlationId || `corr-${Date.now()}`,
|
|
255
|
+
riskLevel,
|
|
256
|
+
issuedAt,
|
|
257
|
+
expiresAt,
|
|
258
|
+
};
|
|
259
|
+
const signature = (0, capability_1.signCapabilityPayload)(capabilityPayload, this.secretKey);
|
|
260
|
+
const capability = {
|
|
261
|
+
...capabilityPayload,
|
|
262
|
+
allowed: true,
|
|
263
|
+
signature,
|
|
264
|
+
};
|
|
265
|
+
await this.recordAudit(action, effectiveContext, decision, 'APPROVED');
|
|
266
|
+
return { decision, capability };
|
|
267
|
+
}
|
|
268
|
+
async approveAction(options) {
|
|
269
|
+
const actorId = (options.context?.actor.actorId || options.approverActorId || '').trim();
|
|
270
|
+
if (!actorId) {
|
|
271
|
+
return {
|
|
272
|
+
approved: false,
|
|
273
|
+
decisionCode: 'REJECTED_MISSING_APPROVER_ID',
|
|
274
|
+
reason: 'approverActorId is required to approve an action execution',
|
|
275
|
+
executionId: options.executionId,
|
|
276
|
+
approverActorId: '',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
// 1. Authenticated boundary check
|
|
280
|
+
if (options.context && options.context.actor.authenticated === false) {
|
|
281
|
+
const result = {
|
|
282
|
+
approved: false,
|
|
283
|
+
decisionCode: 'REJECTED_UNAUTHENTICATED',
|
|
284
|
+
reason: `Approver "${actorId}" is unauthenticated and cannot approve actions`,
|
|
285
|
+
executionId: options.executionId,
|
|
286
|
+
approverActorId: actorId,
|
|
287
|
+
};
|
|
288
|
+
await this.recordApprovalAudit(options.executionId, actorId, 'unauthenticated', false, result.reason);
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
// 2. Resolve effective approver role & capabilities
|
|
292
|
+
const rawRole = options.context?.actor.authorizationRole || options.context?.actor?.role || options.approverRole;
|
|
293
|
+
const approverRole = normalizeApproverRole(rawRole, actorId);
|
|
294
|
+
const capabilities = options.context?.actor.capabilities || options.approverCapabilities || [];
|
|
295
|
+
const isOwnerOrAdmin = approverRole === 'owner' || approverRole === 'administrator' || approverRole === 'admin';
|
|
296
|
+
const isRoleAllowed = isOwnerOrAdmin || this.allowedApproverRoles.includes(approverRole);
|
|
297
|
+
// Reject non-allowed roles or explicit viewer/guest role
|
|
298
|
+
if (!isRoleAllowed || approverRole === 'viewer') {
|
|
299
|
+
const result = {
|
|
300
|
+
approved: false,
|
|
301
|
+
decisionCode: 'REJECTED_UNAUTHORIZED',
|
|
302
|
+
reason: `Actor "${actorId}" with role "${approverRole}" is not authorized to approve actions`,
|
|
303
|
+
executionId: options.executionId,
|
|
304
|
+
approverActorId: actorId,
|
|
305
|
+
};
|
|
306
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason);
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
// 3. Locate pending in-memory action or persisted execution record
|
|
310
|
+
const pending = this.pendingActions.get(options.executionId);
|
|
311
|
+
let executionRecord;
|
|
312
|
+
if (!pending && typeof this.store.getExecution === 'function') {
|
|
313
|
+
executionRecord = await this.store.getExecution(options.executionId);
|
|
314
|
+
}
|
|
315
|
+
if (!pending && !executionRecord && !options.toolName) {
|
|
316
|
+
const result = {
|
|
317
|
+
approved: false,
|
|
318
|
+
decisionCode: 'REJECTED_UNKNOWN_EXECUTION',
|
|
319
|
+
reason: `Cannot approve execution "${options.executionId}": execution is not pending or recognized`,
|
|
320
|
+
executionId: options.executionId,
|
|
321
|
+
approverActorId: actorId,
|
|
322
|
+
};
|
|
323
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason);
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
const targetToolName = pending?.action.toolName || executionRecord?.toolName || options.toolName || '';
|
|
327
|
+
const targetToolDef = pending?.toolDef || (targetToolName ? this.findToolDefinition(targetToolName) : undefined);
|
|
328
|
+
const targetParamsHash = (pending ? (0, capability_1.computeParametersHash)(pending.action.parameters) : undefined) ||
|
|
329
|
+
executionRecord?.parametersHash ||
|
|
330
|
+
options.parametersHash;
|
|
331
|
+
const targetCompanionId = pending?.context?.companionId || pending?.action.context?.companionId || options.companionId;
|
|
332
|
+
const targetActorId = pending?.context?.actor.actorId || pending?.action.context?.actor.actorId || options.actorId;
|
|
333
|
+
if (targetToolDef) {
|
|
334
|
+
if (targetToolDef.allowedRoles && targetToolDef.allowedRoles.length > 0 && !isOwnerOrAdmin) {
|
|
335
|
+
const normalizedToolRoles = new Set(targetToolDef.allowedRoles.map((r) => r.toLowerCase()));
|
|
336
|
+
if (!normalizedToolRoles.has(approverRole)) {
|
|
337
|
+
const result = {
|
|
338
|
+
approved: false,
|
|
339
|
+
decisionCode: 'REJECTED_ROLE_MISMATCH',
|
|
340
|
+
reason: `Approver role "${approverRole}" is not authorized to approve tool "${targetToolDef.name}" (requires: ${targetToolDef.allowedRoles.join(', ')})`,
|
|
341
|
+
executionId: options.executionId,
|
|
342
|
+
approverActorId: actorId,
|
|
343
|
+
};
|
|
344
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason, targetToolDef.name);
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (this.requiredApproverCapabilities.length > 0 && !isOwnerOrAdmin) {
|
|
349
|
+
const missing = this.requiredApproverCapabilities.filter((c) => !capabilities.includes(c));
|
|
350
|
+
if (missing.length > 0) {
|
|
351
|
+
const result = {
|
|
352
|
+
approved: false,
|
|
353
|
+
decisionCode: 'REJECTED_MISSING_CAPABILITY',
|
|
354
|
+
reason: `Approver is missing required approval capabilities: [${missing.join(', ')}]`,
|
|
355
|
+
executionId: options.executionId,
|
|
356
|
+
approverActorId: actorId,
|
|
357
|
+
};
|
|
358
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason, targetToolDef.name);
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// 4. Record verified approval with exact tool, parameter, companion, and actor binding
|
|
364
|
+
const record = {
|
|
365
|
+
executionId: options.executionId,
|
|
366
|
+
approverActorId: actorId,
|
|
367
|
+
reason: options.reason,
|
|
368
|
+
approverRole,
|
|
369
|
+
approvedAt: new Date().toISOString(),
|
|
370
|
+
toolName: targetToolName,
|
|
371
|
+
parametersHash: targetParamsHash,
|
|
372
|
+
companionId: targetCompanionId,
|
|
373
|
+
actorId: targetActorId,
|
|
374
|
+
};
|
|
375
|
+
this.approvedExecutions.set(options.executionId, record);
|
|
376
|
+
if (typeof this.store.saveApproval === 'function') {
|
|
377
|
+
await this.store.saveApproval(options.executionId, actorId, options.reason, approverRole, targetToolName, targetParamsHash, targetCompanionId, targetActorId);
|
|
378
|
+
}
|
|
379
|
+
const reason = options.reason || 'Action approved by authorized policy approver';
|
|
380
|
+
await this.recordApprovalAudit(options.executionId, actorId, approverRole, true, reason, targetToolName);
|
|
381
|
+
return {
|
|
382
|
+
approved: true,
|
|
383
|
+
decisionCode: 'APPROVED',
|
|
384
|
+
reason,
|
|
385
|
+
executionId: options.executionId,
|
|
386
|
+
approverActorId: actorId,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
async recordApprovalAudit(executionId, approverActorId, approverRole, approved, reason, toolName) {
|
|
390
|
+
const event = {
|
|
391
|
+
executionId,
|
|
392
|
+
actionId: executionId,
|
|
393
|
+
toolName: toolName || 'action:approve',
|
|
394
|
+
companionId: 'system',
|
|
395
|
+
actorId: approverActorId,
|
|
396
|
+
riskLevel: 'HIGH',
|
|
397
|
+
lifecycle: approved ? 'APPROVED' : 'REJECTED',
|
|
398
|
+
decision: {
|
|
399
|
+
allowed: approved,
|
|
400
|
+
reason: reason || (approved ? 'Approval granted' : 'Approval rejected'),
|
|
401
|
+
riskLevel: 'HIGH',
|
|
402
|
+
executionId,
|
|
403
|
+
decisionCode: approved ? 'ALLOWED_POLICY' : 'REJECTED_UNAUTHORIZED',
|
|
404
|
+
},
|
|
405
|
+
parametersHash: (0, capability_1.computeParametersHash)({ executionId, approverActorId, approverRole, reason }),
|
|
406
|
+
timestamp: new Date().toISOString(),
|
|
407
|
+
};
|
|
408
|
+
await this.store.appendAudit(event);
|
|
409
|
+
}
|
|
410
|
+
async recordAudit(action, context, decision, lifecycle, result, error, durationMs) {
|
|
411
|
+
const event = {
|
|
412
|
+
executionId: action.executionId || decision?.executionId || 'unknown',
|
|
413
|
+
actionId: action.actionId,
|
|
414
|
+
toolName: action.toolName,
|
|
415
|
+
companionId: context?.companionId || 'unknown',
|
|
416
|
+
actorId: context?.actor?.actorId,
|
|
417
|
+
sessionId: context?.actor?.sessionId,
|
|
418
|
+
channel: context?.conversation?.channel,
|
|
419
|
+
correlationId: context?.conversation?.correlationId,
|
|
420
|
+
riskLevel: decision?.riskLevel || 'LOW',
|
|
421
|
+
lifecycle,
|
|
422
|
+
decision,
|
|
423
|
+
parametersHash: (0, capability_1.computeParametersHash)(action.parameters),
|
|
424
|
+
resultHash: result !== undefined ? (0, capability_1.computeParametersHash)(result) : undefined,
|
|
425
|
+
timestamp: new Date().toISOString(),
|
|
426
|
+
durationMs,
|
|
427
|
+
error,
|
|
428
|
+
};
|
|
429
|
+
await this.store.appendAudit(event);
|
|
430
|
+
return event;
|
|
431
|
+
}
|
|
432
|
+
async getAuditLog() {
|
|
433
|
+
return this.store.getAuditLog();
|
|
434
|
+
}
|
|
435
|
+
getStore() {
|
|
436
|
+
return this.store;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
exports.ActionPolicyEngine = ActionPolicyEngine;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|