@siduri-x/core 1.0.5 → 1.0.7
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 +50 -0
- package/dist/action-policy.d.ts +45 -0
- package/dist/action-policy.js +219 -0
- package/dist/action-policy.test.d.ts +1 -0
- package/dist/action-policy.test.js +194 -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 +493 -0
- package/dist/architecture-boundary.test.d.ts +1 -0
- package/dist/architecture-boundary.test.js +117 -0
- package/dist/capability.d.ts +65 -0
- package/dist/capability.js +157 -0
- package/dist/capability.test.d.ts +1 -0
- package/dist/capability.test.js +269 -0
- package/dist/chat-contract.d.ts +81 -0
- package/dist/chat-contract.js +68 -0
- package/dist/cognition-planner.d.ts +15 -0
- package/dist/cognition-planner.js +23 -0
- package/dist/context-retriever.d.ts +24 -0
- package/dist/context-retriever.js +92 -0
- package/dist/context.d.ts +45 -0
- package/dist/context.js +72 -0
- package/dist/context.test.d.ts +1 -0
- package/dist/context.test.js +76 -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 +60 -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 +47 -0
- package/dist/evidence.test.d.ts +1 -0
- package/dist/evidence.test.js +101 -0
- package/dist/experience-emitter.d.ts +21 -0
- package/dist/experience-emitter.js +48 -0
- package/dist/experience.d.ts +57 -0
- package/dist/experience.js +79 -0
- package/dist/experience.test.d.ts +1 -0
- package/dist/experience.test.js +59 -0
- package/dist/gating.d.ts +46 -0
- package/dist/gating.js +185 -0
- package/dist/gating.test.d.ts +1 -0
- package/dist/gating.test.js +190 -0
- package/dist/index.d.ts +250 -0
- package/dist/index.js +43 -0
- package/dist/input-normalizer.d.ts +14 -0
- package/dist/input-normalizer.js +58 -0
- package/dist/input-normalizer.test.d.ts +1 -0
- package/dist/input-normalizer.test.js +39 -0
- package/dist/intent-classifier.d.ts +24 -0
- package/dist/intent-classifier.js +53 -0
- package/dist/intent-classifier.test.d.ts +1 -0
- package/dist/intent-classifier.test.js +68 -0
- package/dist/memory-settler.d.ts +27 -0
- package/dist/memory-settler.js +95 -0
- package/dist/mouth-types.d.ts +85 -0
- package/dist/mouth-types.js +2 -0
- package/dist/perception-cycle.test.d.ts +1 -0
- package/dist/perception-cycle.test.js +155 -0
- package/dist/prompt-compiler.d.ts +20 -0
- package/dist/prompt-compiler.js +57 -0
- package/dist/prompt-compiler.test.d.ts +1 -0
- package/dist/prompt-compiler.test.js +76 -0
- package/dist/proposals.d.ts +30 -0
- package/dist/proposals.js +2 -0
- package/dist/response-envelope.d.ts +25 -0
- package/dist/response-envelope.js +64 -0
- package/dist/runtime-facades.test.d.ts +1 -0
- package/dist/runtime-facades.test.js +69 -0
- package/dist/runtime.d.ts +114 -0
- package/dist/runtime.js +412 -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/sqlite-action-store.d.ts +20 -0
- package/dist/sqlite-action-store.js +225 -0
- package/dist/sqlite-action-store.test.d.ts +1 -0
- package/dist/sqlite-action-store.test.js +252 -0
- package/dist/teaching.d.ts +15 -0
- package/dist/teaching.js +159 -0
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the action policy secret key.
|
|
3
|
+
* In production (NODE_ENV=production), ACTION_POLICY_SECRET is required.
|
|
4
|
+
* In local/development environments, if no secret is provided via environment
|
|
5
|
+
* or options, generates an ephemeral cryptographically strong secret per-process,
|
|
6
|
+
* avoiding shared hardcoded fallback secrets.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getOrGenerateLocalActionPolicySecret(provided?: string): string;
|
|
9
|
+
import { ActionRiskLevel, ActionLifecycleState, ActionAuditEvent, ActionPolicyDecision } from './action';
|
|
10
|
+
export interface AuthorizationCapability {
|
|
11
|
+
executionId: string;
|
|
12
|
+
actionId: string;
|
|
13
|
+
toolName: string;
|
|
14
|
+
providerId: string;
|
|
15
|
+
parametersHash: string;
|
|
16
|
+
companionId: string;
|
|
17
|
+
actorId?: string;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
channel?: string;
|
|
20
|
+
correlationId?: string;
|
|
21
|
+
riskLevel: ActionRiskLevel;
|
|
22
|
+
issuedAt: string;
|
|
23
|
+
expiresAt: string;
|
|
24
|
+
allowed: true;
|
|
25
|
+
signature: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PersistentExecutionRecord {
|
|
28
|
+
executionId: string;
|
|
29
|
+
actionId: string;
|
|
30
|
+
toolName: string;
|
|
31
|
+
providerId: string;
|
|
32
|
+
parametersHash: string;
|
|
33
|
+
lifecycle: ActionLifecycleState;
|
|
34
|
+
decision?: ActionPolicyDecision;
|
|
35
|
+
result?: unknown;
|
|
36
|
+
error?: string;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ActionStore {
|
|
41
|
+
reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
|
|
42
|
+
updateExecution(record: PersistentExecutionRecord): Promise<void>;
|
|
43
|
+
getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
|
|
44
|
+
saveApproval(executionId: string, approverActorId: string, reason?: string): Promise<void>;
|
|
45
|
+
isActionApproved(executionId: string): Promise<boolean>;
|
|
46
|
+
appendAudit(event: ActionAuditEvent): Promise<void>;
|
|
47
|
+
getAuditLog(executionId?: string): Promise<ActionAuditEvent[]>;
|
|
48
|
+
}
|
|
49
|
+
export declare class InMemoryActionStore implements ActionStore {
|
|
50
|
+
private readonly executions;
|
|
51
|
+
private readonly approvals;
|
|
52
|
+
private readonly auditLog;
|
|
53
|
+
private lastAuditHash;
|
|
54
|
+
reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
|
|
55
|
+
updateExecution(record: PersistentExecutionRecord): Promise<void>;
|
|
56
|
+
getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
|
|
57
|
+
saveApproval(executionId: string, approverActorId: string, reason?: string): Promise<void>;
|
|
58
|
+
isActionApproved(executionId: string): Promise<boolean>;
|
|
59
|
+
appendAudit(event: ActionAuditEvent): Promise<void>;
|
|
60
|
+
getAuditLog(executionId?: string): Promise<ActionAuditEvent[]>;
|
|
61
|
+
}
|
|
62
|
+
export declare function canonicalizeJson(obj: unknown): string;
|
|
63
|
+
export declare function computeParametersHash(params: unknown): string;
|
|
64
|
+
export declare function signCapabilityPayload(payload: Record<string, unknown>, secretKey?: string): string;
|
|
65
|
+
export declare function verifyCapabilitySignature(capability: AuthorizationCapability, secretKey?: string): boolean;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Standard cryptographic implementations avoiding runtime ambient type dependencies
|
|
3
|
+
// using node's built-in crypto module dynamically or via require for universal CommonJS compatibility
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.InMemoryActionStore = void 0;
|
|
6
|
+
exports.getOrGenerateLocalActionPolicySecret = getOrGenerateLocalActionPolicySecret;
|
|
7
|
+
exports.canonicalizeJson = canonicalizeJson;
|
|
8
|
+
exports.computeParametersHash = computeParametersHash;
|
|
9
|
+
exports.signCapabilityPayload = signCapabilityPayload;
|
|
10
|
+
exports.verifyCapabilitySignature = verifyCapabilitySignature;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
let cachedEphemeralSecret = null;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the action policy secret key.
|
|
16
|
+
* In production (NODE_ENV=production), ACTION_POLICY_SECRET is required.
|
|
17
|
+
* In local/development environments, if no secret is provided via environment
|
|
18
|
+
* or options, generates an ephemeral cryptographically strong secret per-process,
|
|
19
|
+
* avoiding shared hardcoded fallback secrets.
|
|
20
|
+
*/
|
|
21
|
+
function getOrGenerateLocalActionPolicySecret(provided) {
|
|
22
|
+
if (provided) {
|
|
23
|
+
return provided;
|
|
24
|
+
}
|
|
25
|
+
const envSecret = typeof process !== 'undefined' && process.env ? process.env.ACTION_POLICY_SECRET : undefined;
|
|
26
|
+
if (envSecret) {
|
|
27
|
+
return envSecret;
|
|
28
|
+
}
|
|
29
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
|
|
30
|
+
throw new Error('FATAL: ACTION_POLICY_SECRET is required in production environment');
|
|
31
|
+
}
|
|
32
|
+
if (!cachedEphemeralSecret) {
|
|
33
|
+
cachedEphemeralSecret = crypto.randomBytes(32).toString('hex');
|
|
34
|
+
}
|
|
35
|
+
return cachedEphemeralSecret;
|
|
36
|
+
}
|
|
37
|
+
class InMemoryActionStore {
|
|
38
|
+
executions = new Map();
|
|
39
|
+
approvals = new Map();
|
|
40
|
+
auditLog = [];
|
|
41
|
+
lastAuditHash = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
42
|
+
async reserveExecution(record) {
|
|
43
|
+
if (this.executions.has(record.executionId)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
this.executions.set(record.executionId, { ...record });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
async updateExecution(record) {
|
|
50
|
+
this.executions.set(record.executionId, { ...record });
|
|
51
|
+
}
|
|
52
|
+
async getExecution(executionId) {
|
|
53
|
+
const rec = this.executions.get(executionId);
|
|
54
|
+
return rec ? { ...rec } : undefined;
|
|
55
|
+
}
|
|
56
|
+
async saveApproval(executionId, approverActorId, reason) {
|
|
57
|
+
this.approvals.set(executionId, {
|
|
58
|
+
approverActorId,
|
|
59
|
+
reason,
|
|
60
|
+
approvedAt: new Date().toISOString(),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async isActionApproved(executionId) {
|
|
64
|
+
return this.approvals.has(executionId);
|
|
65
|
+
}
|
|
66
|
+
async appendAudit(event) {
|
|
67
|
+
const prevHash = this.lastAuditHash;
|
|
68
|
+
// Tamper-evident hash chaining over all security-critical event fields
|
|
69
|
+
const eventPayload = {
|
|
70
|
+
executionId: event.executionId,
|
|
71
|
+
actionId: event.actionId,
|
|
72
|
+
toolName: event.toolName,
|
|
73
|
+
providerId: event.providerId || null,
|
|
74
|
+
companionId: event.companionId,
|
|
75
|
+
actorId: event.actorId || null,
|
|
76
|
+
sessionId: event.sessionId || null,
|
|
77
|
+
channel: event.channel || null,
|
|
78
|
+
correlationId: event.correlationId || null,
|
|
79
|
+
riskLevel: event.riskLevel,
|
|
80
|
+
lifecycle: event.lifecycle,
|
|
81
|
+
decision: event.decision ? {
|
|
82
|
+
allowed: event.decision.allowed,
|
|
83
|
+
reason: event.decision.reason,
|
|
84
|
+
riskLevel: event.decision.riskLevel,
|
|
85
|
+
decisionCode: event.decision.decisionCode,
|
|
86
|
+
} : null,
|
|
87
|
+
parametersHash: event.parametersHash || null,
|
|
88
|
+
error: event.error || null,
|
|
89
|
+
timestamp: event.timestamp,
|
|
90
|
+
};
|
|
91
|
+
const canonical = canonicalizeJson(eventPayload);
|
|
92
|
+
const eventHash = crypto
|
|
93
|
+
.createHash('sha256')
|
|
94
|
+
.update(`${prevHash}:${canonical}`, 'utf8')
|
|
95
|
+
.digest('hex');
|
|
96
|
+
this.lastAuditHash = eventHash;
|
|
97
|
+
const recordWithHash = {
|
|
98
|
+
...event,
|
|
99
|
+
previousEventHash: prevHash,
|
|
100
|
+
eventHash,
|
|
101
|
+
resultHash: event.resultHash || eventHash,
|
|
102
|
+
};
|
|
103
|
+
this.auditLog.push(recordWithHash);
|
|
104
|
+
}
|
|
105
|
+
async getAuditLog(executionId) {
|
|
106
|
+
if (executionId) {
|
|
107
|
+
return this.auditLog.filter((e) => e.executionId === executionId).map((e) => ({ ...e }));
|
|
108
|
+
}
|
|
109
|
+
return this.auditLog.map((e) => ({ ...e }));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
exports.InMemoryActionStore = InMemoryActionStore;
|
|
113
|
+
function canonicalizeJson(obj) {
|
|
114
|
+
if (obj === null || typeof obj !== 'object') {
|
|
115
|
+
return JSON.stringify(obj);
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(obj)) {
|
|
118
|
+
return `[${obj.map(canonicalizeJson).join(',')}]`;
|
|
119
|
+
}
|
|
120
|
+
const keys = Object.keys(obj).sort();
|
|
121
|
+
const pairs = keys.map((k) => `"${k}":${canonicalizeJson(obj[k])}`);
|
|
122
|
+
return `{${pairs.join(',')}}`;
|
|
123
|
+
}
|
|
124
|
+
function computeParametersHash(params) {
|
|
125
|
+
const canonical = canonicalizeJson(params || {});
|
|
126
|
+
return crypto.createHash('sha256').update(canonical, 'utf8').digest('hex');
|
|
127
|
+
}
|
|
128
|
+
function signCapabilityPayload(payload, secretKey) {
|
|
129
|
+
const resolvedKey = getOrGenerateLocalActionPolicySecret(secretKey);
|
|
130
|
+
const canonicalStr = canonicalizeJson(payload);
|
|
131
|
+
return crypto.createHmac('sha256', resolvedKey).update(canonicalStr, 'utf8').digest('hex');
|
|
132
|
+
}
|
|
133
|
+
function verifyCapabilitySignature(capability, secretKey) {
|
|
134
|
+
if (!capability || capability.allowed !== true || typeof capability.signature !== 'string') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const resolvedKey = getOrGenerateLocalActionPolicySecret(secretKey);
|
|
138
|
+
const { signature, allowed, ...rest } = capability;
|
|
139
|
+
const canonicalStr = canonicalizeJson(rest);
|
|
140
|
+
const expectedSigHex = crypto.createHmac('sha256', resolvedKey).update(canonicalStr, 'utf8').digest('hex');
|
|
141
|
+
// Constant-time comparison to prevent timing attacks
|
|
142
|
+
try {
|
|
143
|
+
const sigBuffer = globalThis.Buffer
|
|
144
|
+
? globalThis.Buffer.from(signature, 'hex')
|
|
145
|
+
: new Uint8Array(signature.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []);
|
|
146
|
+
const expectedBuffer = globalThis.Buffer
|
|
147
|
+
? globalThis.Buffer.from(expectedSigHex, 'hex')
|
|
148
|
+
: new Uint8Array(expectedSigHex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []);
|
|
149
|
+
if (sigBuffer.length !== expectedBuffer.length || sigBuffer.length === 0) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return crypto.timingSafeEqual(sigBuffer, expectedBuffer);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const capability_1 = require("./capability");
|
|
6
|
+
const action_policy_1 = require("./action-policy");
|
|
7
|
+
describe('AuthorizationCapability Cryptographic & Tamper Review', () => {
|
|
8
|
+
const secretKey = 'prod_secret_signing_key';
|
|
9
|
+
let engine;
|
|
10
|
+
let sampleContext;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
engine = new action_policy_1.ActionPolicyEngine({
|
|
13
|
+
secretKey,
|
|
14
|
+
defaultRiskLevel: 'LOW',
|
|
15
|
+
});
|
|
16
|
+
sampleContext = {
|
|
17
|
+
companionId: 'comp-alpha',
|
|
18
|
+
actor: {
|
|
19
|
+
actorId: 'user-77',
|
|
20
|
+
sessionId: 'sess-88',
|
|
21
|
+
authorizationRole: 'operator',
|
|
22
|
+
capabilities: ['tool:send_email'],
|
|
23
|
+
authenticated: true,
|
|
24
|
+
},
|
|
25
|
+
conversation: {
|
|
26
|
+
channel: 'direct',
|
|
27
|
+
audienceId: 'aud-alpha',
|
|
28
|
+
correlationId: 'corr-999',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
engine.registerToolDefinition({
|
|
32
|
+
name: 'send_email',
|
|
33
|
+
providerId: 'comm',
|
|
34
|
+
description: 'Send email',
|
|
35
|
+
inputSchema: {},
|
|
36
|
+
riskLevel: 'LOW',
|
|
37
|
+
requiredCapabilities: ['tool:send_email'],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('Field-Level Tampering Invalidation (HMAC-SHA-256)', () => {
|
|
41
|
+
let legitCapability;
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
const { capability } = await engine.evaluateAction({
|
|
44
|
+
actionId: 'act-email-1',
|
|
45
|
+
toolName: 'comm/send_email',
|
|
46
|
+
parameters: { to: 'alice@example.com', subject: 'Hello' },
|
|
47
|
+
context: sampleContext,
|
|
48
|
+
});
|
|
49
|
+
expect(capability).toBeDefined();
|
|
50
|
+
legitCapability = capability;
|
|
51
|
+
});
|
|
52
|
+
it('verifies untouched authentic capability', () => {
|
|
53
|
+
expect((0, capability_1.verifyCapabilitySignature)(legitCapability, secretKey)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it('invalidates signature if executionId is modified', () => {
|
|
56
|
+
const tampered = { ...legitCapability, executionId: 'exec-tampered-999' };
|
|
57
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('invalidates signature if actionId is modified', () => {
|
|
60
|
+
const tampered = { ...legitCapability, actionId: 'act-tampered-999' };
|
|
61
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
it('invalidates signature if toolName is modified', () => {
|
|
64
|
+
const tampered = { ...legitCapability, toolName: 'admin/delete_all' };
|
|
65
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
it('invalidates signature if providerId is modified', () => {
|
|
68
|
+
const tampered = { ...legitCapability, providerId: 'untrusted_plugin' };
|
|
69
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
it('invalidates signature if parametersHash is modified', () => {
|
|
72
|
+
const tampered = { ...legitCapability, parametersHash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' };
|
|
73
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
it('invalidates signature if actorId is modified', () => {
|
|
76
|
+
const tampered = { ...legitCapability, actorId: 'attacker-root' };
|
|
77
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
it('invalidates signature if sessionId is modified', () => {
|
|
80
|
+
const tampered = { ...legitCapability, sessionId: 'hijacked-session' };
|
|
81
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
it('invalidates signature if channel is modified', () => {
|
|
84
|
+
const tampered = { ...legitCapability, channel: 'public' };
|
|
85
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it('invalidates signature if correlationId is modified', () => {
|
|
88
|
+
const tampered = { ...legitCapability, correlationId: 'corr-spoofed' };
|
|
89
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it('invalidates signature if expiresAt is modified to extend lifetime', () => {
|
|
92
|
+
const tampered = { ...legitCapability, expiresAt: new Date(Date.now() + 86400000).toISOString() };
|
|
93
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
it('invalidates signature if issuedAt is modified', () => {
|
|
96
|
+
const tampered = { ...legitCapability, issuedAt: new Date(Date.now() - 100000).toISOString() };
|
|
97
|
+
expect((0, capability_1.verifyCapabilitySignature)(tampered, secretKey)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
it('rejects verification if secretKey is mismatched', () => {
|
|
100
|
+
expect((0, capability_1.verifyCapabilitySignature)(legitCapability, 'wrong_secret_key')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
it('handles malformed, truncated, or non-hex signatures safely without crashing', () => {
|
|
103
|
+
const malformed1 = { ...legitCapability, signature: 'not_a_hex_string' };
|
|
104
|
+
expect((0, capability_1.verifyCapabilitySignature)(malformed1, secretKey)).toBe(false);
|
|
105
|
+
const malformed2 = { ...legitCapability, signature: 'deadbeef' };
|
|
106
|
+
expect((0, capability_1.verifyCapabilitySignature)(malformed2, secretKey)).toBe(false);
|
|
107
|
+
const malformed3 = { ...legitCapability, signature: '' };
|
|
108
|
+
expect((0, capability_1.verifyCapabilitySignature)(malformed3, secretKey)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('Deterministic Canonicalization', () => {
|
|
112
|
+
it('produces identical canonical JSON strings regardless of key insertion order', () => {
|
|
113
|
+
const objA = { b: 2, a: 1, c: { z: 26, y: 25 } };
|
|
114
|
+
const objB = { a: 1, c: { y: 25, z: 26 }, b: 2 };
|
|
115
|
+
expect((0, capability_1.canonicalizeJson)(objA)).toBe((0, capability_1.canonicalizeJson)(objB));
|
|
116
|
+
expect((0, capability_1.computeParametersHash)(objA)).toBe((0, capability_1.computeParametersHash)(objB));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('Audit Trail SHA-256 Hash Chaining and Mutation Detection', () => {
|
|
120
|
+
it('detects tampering or mutation in previous audit events in the chain', async () => {
|
|
121
|
+
const store = new capability_1.InMemoryActionStore();
|
|
122
|
+
const policyEngine = new action_policy_1.ActionPolicyEngine({ store, secretKey });
|
|
123
|
+
policyEngine.registerToolDefinition({
|
|
124
|
+
name: 'send_email',
|
|
125
|
+
providerId: 'comm',
|
|
126
|
+
description: 'Send email',
|
|
127
|
+
inputSchema: {},
|
|
128
|
+
riskLevel: 'LOW',
|
|
129
|
+
});
|
|
130
|
+
// Event 1
|
|
131
|
+
await policyEngine.evaluateAction({
|
|
132
|
+
actionId: 'act-1',
|
|
133
|
+
toolName: 'comm/send_email',
|
|
134
|
+
parameters: { step: 1 },
|
|
135
|
+
context: sampleContext,
|
|
136
|
+
});
|
|
137
|
+
// Event 2
|
|
138
|
+
await policyEngine.evaluateAction({
|
|
139
|
+
actionId: 'act-2',
|
|
140
|
+
toolName: 'comm/send_email',
|
|
141
|
+
parameters: { step: 2 },
|
|
142
|
+
context: sampleContext,
|
|
143
|
+
});
|
|
144
|
+
const auditTrail = await store.getAuditLog();
|
|
145
|
+
expect(auditTrail.length).toBe(2);
|
|
146
|
+
const event1 = auditTrail[0];
|
|
147
|
+
const event2 = auditTrail[1];
|
|
148
|
+
// Recompute expected hash chain using standard SHA-256
|
|
149
|
+
const initialPrevHash = '0000000000000000000000000000000000000000000000000000000000000000';
|
|
150
|
+
const canonical1 = (0, capability_1.canonicalizeJson)({
|
|
151
|
+
executionId: event1.executionId,
|
|
152
|
+
actionId: event1.actionId,
|
|
153
|
+
toolName: event1.toolName,
|
|
154
|
+
providerId: event1.providerId || null,
|
|
155
|
+
companionId: event1.companionId,
|
|
156
|
+
actorId: event1.actorId || null,
|
|
157
|
+
sessionId: event1.sessionId || null,
|
|
158
|
+
channel: event1.channel || null,
|
|
159
|
+
correlationId: event1.correlationId || null,
|
|
160
|
+
riskLevel: event1.riskLevel,
|
|
161
|
+
lifecycle: event1.lifecycle,
|
|
162
|
+
decision: event1.decision ? {
|
|
163
|
+
allowed: event1.decision.allowed,
|
|
164
|
+
reason: event1.decision.reason,
|
|
165
|
+
riskLevel: event1.decision.riskLevel,
|
|
166
|
+
decisionCode: event1.decision.decisionCode,
|
|
167
|
+
} : null,
|
|
168
|
+
parametersHash: event1.parametersHash || null,
|
|
169
|
+
error: event1.error || null,
|
|
170
|
+
timestamp: event1.timestamp,
|
|
171
|
+
});
|
|
172
|
+
const expectedHash1 = crypto.createHash('sha256').update(`${initialPrevHash}:${canonical1}`, 'utf8').digest('hex');
|
|
173
|
+
expect(event1.eventHash).toBe(expectedHash1);
|
|
174
|
+
expect(event1.previousEventHash).toBe(initialPrevHash);
|
|
175
|
+
const canonical2 = (0, capability_1.canonicalizeJson)({
|
|
176
|
+
executionId: event2.executionId,
|
|
177
|
+
actionId: event2.actionId,
|
|
178
|
+
toolName: event2.toolName,
|
|
179
|
+
providerId: event2.providerId || null,
|
|
180
|
+
companionId: event2.companionId,
|
|
181
|
+
actorId: event2.actorId || null,
|
|
182
|
+
sessionId: event2.sessionId || null,
|
|
183
|
+
channel: event2.channel || null,
|
|
184
|
+
correlationId: event2.correlationId || null,
|
|
185
|
+
riskLevel: event2.riskLevel,
|
|
186
|
+
lifecycle: event2.lifecycle,
|
|
187
|
+
decision: event2.decision ? {
|
|
188
|
+
allowed: event2.decision.allowed,
|
|
189
|
+
reason: event2.decision.reason,
|
|
190
|
+
riskLevel: event2.decision.riskLevel,
|
|
191
|
+
decisionCode: event2.decision.decisionCode,
|
|
192
|
+
} : null,
|
|
193
|
+
parametersHash: event2.parametersHash || null,
|
|
194
|
+
error: event2.error || null,
|
|
195
|
+
timestamp: event2.timestamp,
|
|
196
|
+
});
|
|
197
|
+
const expectedHash2 = crypto.createHash('sha256').update(`${expectedHash1}:${canonical2}`, 'utf8').digest('hex');
|
|
198
|
+
expect(event2.eventHash).toBe(expectedHash2);
|
|
199
|
+
expect(event2.previousEventHash).toBe(expectedHash1);
|
|
200
|
+
// If an attacker altered event 1 retrospectively, the hash chain breaks for event 2
|
|
201
|
+
const tamperedCanonical1 = (0, capability_1.canonicalizeJson)({
|
|
202
|
+
...JSON.parse(canonical1),
|
|
203
|
+
actionId: 'act-tampered-1',
|
|
204
|
+
});
|
|
205
|
+
const tamperedHash1 = crypto.createHash('sha256').update(`${initialPrevHash}:${tamperedCanonical1}`, 'utf8').digest('hex');
|
|
206
|
+
const brokenHash2 = crypto.createHash('sha256').update(`${tamperedHash1}:${canonical2}`, 'utf8').digest('hex');
|
|
207
|
+
expect(brokenHash2).not.toBe(event2.resultHash);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe('Durable Action Approval Restart Semantics', () => {
|
|
211
|
+
it('preserves approved execution authorization across ActionPolicyEngine process restart', async () => {
|
|
212
|
+
const sharedStore = new capability_1.InMemoryActionStore();
|
|
213
|
+
// Instance 1: Operator reviews and approves a HIGH risk action
|
|
214
|
+
const engine1 = new action_policy_1.ActionPolicyEngine({
|
|
215
|
+
store: sharedStore,
|
|
216
|
+
secretKey,
|
|
217
|
+
defaultRiskLevel: 'HIGH',
|
|
218
|
+
defaultRequireApprovalForHighRisk: true,
|
|
219
|
+
});
|
|
220
|
+
engine1.registerToolDefinition({
|
|
221
|
+
name: 'database/cleanup',
|
|
222
|
+
providerId: 'db',
|
|
223
|
+
description: 'Cleanup DB',
|
|
224
|
+
inputSchema: {},
|
|
225
|
+
riskLevel: 'HIGH',
|
|
226
|
+
requiresApproval: true,
|
|
227
|
+
});
|
|
228
|
+
const highRiskAction = {
|
|
229
|
+
actionId: 'act-restart-1',
|
|
230
|
+
executionId: 'exec-restart-1',
|
|
231
|
+
toolName: 'db/database/cleanup',
|
|
232
|
+
parameters: { target: 'logs' },
|
|
233
|
+
context: sampleContext,
|
|
234
|
+
};
|
|
235
|
+
// 1. Initial evaluation without approval is rejected
|
|
236
|
+
const eval1 = await engine1.evaluateAction(highRiskAction);
|
|
237
|
+
expect(eval1.decision.allowed).toBe(false);
|
|
238
|
+
expect(eval1.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
|
|
239
|
+
// 2. Operator explicitly approves
|
|
240
|
+
await engine1.approveAction({
|
|
241
|
+
executionId: 'exec-restart-1',
|
|
242
|
+
approverActorId: 'operator-1',
|
|
243
|
+
reason: 'Scheduled maintenance',
|
|
244
|
+
});
|
|
245
|
+
// 3. Process restarts: new ActionPolicyEngine instance with empty in-memory set but shared durable store
|
|
246
|
+
const engine2 = new action_policy_1.ActionPolicyEngine({
|
|
247
|
+
store: sharedStore,
|
|
248
|
+
secretKey,
|
|
249
|
+
defaultRiskLevel: 'HIGH',
|
|
250
|
+
defaultRequireApprovalForHighRisk: true,
|
|
251
|
+
});
|
|
252
|
+
engine2.registerToolDefinition({
|
|
253
|
+
name: 'database/cleanup',
|
|
254
|
+
providerId: 'db',
|
|
255
|
+
description: 'Cleanup DB',
|
|
256
|
+
inputSchema: {},
|
|
257
|
+
riskLevel: 'HIGH',
|
|
258
|
+
requiresApproval: true,
|
|
259
|
+
});
|
|
260
|
+
// 4. Evaluation after restart loads durable approval and successfully authorizes capability
|
|
261
|
+
const eval2 = await engine2.evaluateAction(highRiskAction);
|
|
262
|
+
expect(eval2.decision.allowed).toBe(true);
|
|
263
|
+
expect(eval2.decision.decisionCode).toBe('ALLOWED_POLICY');
|
|
264
|
+
expect(eval2.capability).toBeDefined();
|
|
265
|
+
expect(eval2.capability?.executionId).toBe('exec-restart-1');
|
|
266
|
+
expect((0, capability_1.verifyCapabilitySignature)(eval2.capability, secretKey)).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RequestContext } from './context';
|
|
2
|
+
import { Message, Claim } from './index';
|
|
3
|
+
import { ActionExecutionResult } from './action';
|
|
4
|
+
import { ResponseCitation } from './evidence';
|
|
5
|
+
import { SiduriRuntime } from './runtime';
|
|
6
|
+
import { MouthMedium, FormattedMouthOutput } from './mouth-types';
|
|
7
|
+
export interface ChatRequest {
|
|
8
|
+
id?: string;
|
|
9
|
+
companionId?: string;
|
|
10
|
+
message: string;
|
|
11
|
+
role?: 'OWNER' | 'VIEWER' | 'OPERATOR' | string;
|
|
12
|
+
context?: RequestContext;
|
|
13
|
+
history?: Message[];
|
|
14
|
+
medium?: MouthMedium;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
export interface ChatResponseMetadataEvent {
|
|
19
|
+
event_id: string;
|
|
20
|
+
kind: string;
|
|
21
|
+
lifecycle: string;
|
|
22
|
+
approval?: string;
|
|
23
|
+
expression?: string;
|
|
24
|
+
action?: string;
|
|
25
|
+
durationMs?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ChatResponsePlan {
|
|
28
|
+
speech_id?: string;
|
|
29
|
+
audio_url?: string;
|
|
30
|
+
subtitle_ja: string;
|
|
31
|
+
subtitle_en: string;
|
|
32
|
+
spoken_ja?: string;
|
|
33
|
+
evidence_ids?: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface ChatResponseMetadata {
|
|
36
|
+
language?: string;
|
|
37
|
+
proposals?: Claim[];
|
|
38
|
+
memory_proposals?: Array<{
|
|
39
|
+
proposal_id: string;
|
|
40
|
+
subject?: string;
|
|
41
|
+
predicate?: string;
|
|
42
|
+
value?: string;
|
|
43
|
+
status: string;
|
|
44
|
+
content?: string;
|
|
45
|
+
claim_type?: string;
|
|
46
|
+
}>;
|
|
47
|
+
behavioral_proposals?: Array<{
|
|
48
|
+
directive_id: string;
|
|
49
|
+
memory_class: string;
|
|
50
|
+
domain: string;
|
|
51
|
+
subject: string;
|
|
52
|
+
predicate: string;
|
|
53
|
+
value: string;
|
|
54
|
+
status: string;
|
|
55
|
+
behavior?: any;
|
|
56
|
+
runtime_effect?: string;
|
|
57
|
+
}>;
|
|
58
|
+
action_results?: ActionExecutionResult[];
|
|
59
|
+
evidence_ids?: string[];
|
|
60
|
+
citations?: ResponseCitation[];
|
|
61
|
+
subsystem_diagnostics?: Record<string, string>;
|
|
62
|
+
events?: ChatResponseMetadataEvent[];
|
|
63
|
+
[key: string]: any;
|
|
64
|
+
}
|
|
65
|
+
export interface ChatResponse {
|
|
66
|
+
status: 'APPROVED' | 'REJECTED' | 'STAGED' | string;
|
|
67
|
+
response_id?: string;
|
|
68
|
+
correlation_id?: string;
|
|
69
|
+
response: ChatResponsePlan;
|
|
70
|
+
metadata?: ChatResponseMetadata;
|
|
71
|
+
delivery?: FormattedMouthOutput;
|
|
72
|
+
reply?: string;
|
|
73
|
+
text?: string;
|
|
74
|
+
audioUrl?: string;
|
|
75
|
+
expression?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Canonical helper to dispatch a chat request to a SiduriRuntime instance.
|
|
79
|
+
* Guarantees a 1:1 identical response structure for localweb (apps/api) and standalone CLI.
|
|
80
|
+
*/
|
|
81
|
+
export declare function dispatchCompanionChat(runtime: SiduriRuntime, payload: ChatRequest): Promise<ChatResponse>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dispatchCompanionChat = dispatchCompanionChat;
|
|
4
|
+
/**
|
|
5
|
+
* Canonical helper to dispatch a chat request to a SiduriRuntime instance.
|
|
6
|
+
* Guarantees a 1:1 identical response structure for localweb (apps/api) and standalone CLI.
|
|
7
|
+
*/
|
|
8
|
+
async function dispatchCompanionChat(runtime, payload) {
|
|
9
|
+
const userMessage = payload.message || payload.text || '';
|
|
10
|
+
const history = Array.isArray(payload.history) ? payload.history : [];
|
|
11
|
+
let roleOrContext;
|
|
12
|
+
if (payload.context) {
|
|
13
|
+
roleOrContext = payload.context;
|
|
14
|
+
}
|
|
15
|
+
else if (payload.role) {
|
|
16
|
+
roleOrContext = payload.role;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
roleOrContext = 'OWNER';
|
|
20
|
+
}
|
|
21
|
+
const runtimeResult = (payload.medium || payload.signal)
|
|
22
|
+
? await runtime.handleUserMessage(userMessage, roleOrContext, history, payload.medium, payload.signal)
|
|
23
|
+
: await runtime.handleUserMessage(userMessage, roleOrContext, history);
|
|
24
|
+
const delivery = runtimeResult?.delivery;
|
|
25
|
+
// Normalize response plan
|
|
26
|
+
const speech = delivery?.displayText ||
|
|
27
|
+
delivery?.text ||
|
|
28
|
+
runtimeResult?.response?.subtitle_ja ||
|
|
29
|
+
runtimeResult?.response?.subtitle_en ||
|
|
30
|
+
'';
|
|
31
|
+
const audioUrl = delivery?.audioUrl || runtimeResult?.response?.audio_url;
|
|
32
|
+
// Extract avatar expression if any event was generated or provided by Mouth delivery
|
|
33
|
+
let expression = delivery?.expression || 'neutral';
|
|
34
|
+
if (expression === 'neutral') {
|
|
35
|
+
const events = runtimeResult?.metadata?.events || [];
|
|
36
|
+
const avatarEvent = events.find((e) => e.kind === 'avatar' || e.kind === 'body');
|
|
37
|
+
if (avatarEvent && avatarEvent.expression) {
|
|
38
|
+
expression = avatarEvent.expression;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Ensure both spoken_ja and subtitle_en are accessible alongside speech_id and evidence_ids
|
|
42
|
+
const responsePlan = {
|
|
43
|
+
speech_id: runtimeResult?.response?.speech_id,
|
|
44
|
+
audio_url: audioUrl,
|
|
45
|
+
subtitle_ja: delivery?.subtitles?.ja ?? runtimeResult?.response?.subtitle_ja ?? speech,
|
|
46
|
+
subtitle_en: delivery?.subtitles?.en ?? runtimeResult?.response?.subtitle_en ?? speech,
|
|
47
|
+
spoken_ja: delivery?.subtitles?.spoken ?? runtimeResult?.response?.spoken_ja ?? runtimeResult?.response?.subtitle_ja ?? speech,
|
|
48
|
+
evidence_ids: runtimeResult?.metadata?.evidence_ids ?? runtimeResult?.response?.evidence_ids ?? [],
|
|
49
|
+
};
|
|
50
|
+
const metadata = {
|
|
51
|
+
...(runtimeResult?.metadata || {}),
|
|
52
|
+
};
|
|
53
|
+
delete metadata.internal_monologue;
|
|
54
|
+
delete metadata.internalMonologue;
|
|
55
|
+
return {
|
|
56
|
+
status: runtimeResult?.status || 'APPROVED',
|
|
57
|
+
response_id: runtimeResult?.response_id,
|
|
58
|
+
correlation_id: runtimeResult?.correlation_id,
|
|
59
|
+
response: responsePlan,
|
|
60
|
+
delivery,
|
|
61
|
+
metadata,
|
|
62
|
+
// Convenience fields for legacy/simple consumers
|
|
63
|
+
reply: speech,
|
|
64
|
+
text: speech,
|
|
65
|
+
audioUrl,
|
|
66
|
+
expression,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BrainOrgan, Message, ResponsePlan, MemoryScope } from './index';
|
|
2
|
+
export interface CognitionPlanningParams {
|
|
3
|
+
companionName: string;
|
|
4
|
+
brain?: BrainOrgan;
|
|
5
|
+
systemPrompt: string;
|
|
6
|
+
contextPrompt: string;
|
|
7
|
+
recentMessages: Message[];
|
|
8
|
+
recipient?: MemoryScope;
|
|
9
|
+
perceivedText: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Invokes BrainOrgan to generate a structured ResponsePlan, or provides
|
|
13
|
+
* a graceful baseline response if Brain is absent or in headless passive mode.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateCognitionPlan(params: CognitionPlanningParams): Promise<ResponsePlan>;
|