@siduri-x/core 1.0.2 → 1.0.4

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.
@@ -38,7 +38,7 @@ export declare class ActionPolicyEngine {
38
38
  decision: ActionPolicyDecision;
39
39
  capability?: AuthorizationCapability;
40
40
  }>;
41
- approveAction(options: ApproveActionOptions): boolean;
41
+ approveAction(options: ApproveActionOptions): Promise<boolean>;
42
42
  recordAudit(action: ActionIntent, context: RequestContext | undefined, decision: ActionPolicyDecision | undefined, lifecycle: ActionLifecycleState, result?: unknown, error?: string, durationMs?: number): Promise<ActionAuditEvent>;
43
43
  getAuditLog(): Promise<ActionAuditEvent[]>;
44
44
  getStore(): ActionStore;
@@ -73,10 +73,17 @@ class ActionPolicyEngine {
73
73
  await this.recordAudit(action, undefined, decision, 'REJECTED');
74
74
  return { decision };
75
75
  }
76
- // Role check if tool restricts roles
76
+ // Role check if tool restricts roles (supports administrator/owner role parity)
77
77
  if (toolDef.allowedRoles && toolDef.allowedRoles.length > 0) {
78
78
  const actorRole = effectiveContext.actor.authorizationRole;
79
- if (!toolDef.allowedRoles.includes(actorRole)) {
79
+ const normalizedActorRoles = new Set([actorRole.toLowerCase()]);
80
+ if (actorRole === 'administrator' || actorRole === 'owner') {
81
+ normalizedActorRoles.add('administrator');
82
+ normalizedActorRoles.add('owner');
83
+ normalizedActorRoles.add('admin');
84
+ }
85
+ const isAuthorizedRole = toolDef.allowedRoles.some((r) => normalizedActorRoles.has(r.toLowerCase()));
86
+ if (!isAuthorizedRole) {
80
87
  const decision = {
81
88
  allowed: false,
82
89
  reason: `Actor role "${actorRole}" is not authorized to execute tool "${action.toolName}"`,
@@ -123,7 +130,14 @@ class ActionPolicyEngine {
123
130
  // Risk level approval check
124
131
  const requiresExplicitApproval = toolDef.requiresApproval ??
125
132
  (this.defaultRequireApprovalForHighRisk && (riskLevel === 'HIGH' || riskLevel === 'CRITICAL'));
126
- if (requiresExplicitApproval && !this.approvedExecutions.has(executionId)) {
133
+ let isApproved = this.approvedExecutions.has(executionId);
134
+ if (!isApproved && typeof this.store.isActionApproved === 'function') {
135
+ isApproved = await this.store.isActionApproved(executionId);
136
+ if (isApproved) {
137
+ this.approvedExecutions.add(executionId);
138
+ }
139
+ }
140
+ if (requiresExplicitApproval && !isApproved) {
127
141
  const decision = {
128
142
  allowed: false,
129
143
  reason: `Action "${action.toolName}" has risk level ${riskLevel} and requires explicit approval`,
@@ -171,8 +185,11 @@ class ActionPolicyEngine {
171
185
  await this.recordAudit(action, effectiveContext, decision, 'APPROVED');
172
186
  return { decision, capability };
173
187
  }
174
- approveAction(options) {
188
+ async approveAction(options) {
175
189
  this.approvedExecutions.add(options.executionId);
190
+ if (typeof this.store.saveApproval === 'function') {
191
+ await this.store.saveApproval(options.executionId, options.approverActorId, options.reason);
192
+ }
176
193
  return true;
177
194
  }
178
195
  async recordAudit(action, context, decision, lifecycle, result, error, durationMs) {
@@ -154,4 +154,41 @@ describe('ActionPolicyEngine Boundary', () => {
154
154
  expect(log?.parametersHash).toBeDefined();
155
155
  expect(log?.resultHash).toBeDefined();
156
156
  });
157
+ it('supports single-owner role parity between owner and administrator', async () => {
158
+ const ownerTool = {
159
+ name: 'builtin/owner_tool',
160
+ description: 'Owner only tool',
161
+ inputSchema: {},
162
+ riskLevel: 'LOW',
163
+ allowedRoles: ['owner'],
164
+ };
165
+ engine.registerToolDefinition(ownerTool);
166
+ // Context with authorizationRole: 'administrator'
167
+ const adminContext = {
168
+ ...sampleContext,
169
+ actor: {
170
+ ...sampleContext.actor,
171
+ authorizationRole: 'administrator',
172
+ },
173
+ };
174
+ const actionWithAdmin = {
175
+ actionId: 'act-owner-admin',
176
+ toolName: 'builtin/owner_tool',
177
+ parameters: {},
178
+ context: adminContext,
179
+ };
180
+ const res = await engine.evaluateAction(actionWithAdmin);
181
+ expect(res.decision.allowed).toBe(true);
182
+ expect(res.capability).toBeDefined();
183
+ // Context with authorizationRole: 'operator' (non-owner) is rejected
184
+ const operatorAction = {
185
+ actionId: 'act-owner-op',
186
+ toolName: 'builtin/owner_tool',
187
+ parameters: {},
188
+ context: sampleContext, // has authorizationRole: 'operator'
189
+ };
190
+ const resOp = await engine.evaluateAction(operatorAction);
191
+ expect(resOp.decision.allowed).toBe(false);
192
+ expect(resOp.decision.decisionCode).toBe('REJECTED_UNAUTHORIZED');
193
+ });
157
194
  });
package/dist/action.d.ts CHANGED
@@ -32,6 +32,8 @@ export interface ActionAuditEvent {
32
32
  decision?: ActionPolicyDecision;
33
33
  parametersHash?: string;
34
34
  resultHash?: string;
35
+ eventHash?: string;
36
+ previousEventHash?: string;
35
37
  timestamp: string;
36
38
  durationMs?: number;
37
39
  error?: string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,493 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("./index");
4
+ describe('Adversarial Hardening Verification Suite (Phase 3)', () => {
5
+ const secretKey = 'test_policy_secret_key_123';
6
+ const baseOwnerContext = {
7
+ companionId: 'companion-adv',
8
+ actor: {
9
+ actorId: 'owner-user',
10
+ sessionId: 'sess-owner',
11
+ authorizationRole: 'administrator',
12
+ capabilities: ['chat:public', 'chat:private', 'system:manage', 'tools:all'],
13
+ authenticated: true,
14
+ },
15
+ conversation: {
16
+ channel: 'private',
17
+ audienceId: 'audience-owner',
18
+ correlationId: 'corr-adv-1',
19
+ },
20
+ };
21
+ const baseViewerContext = {
22
+ companionId: 'companion-adv',
23
+ actor: {
24
+ actorId: 'anonymous-viewer',
25
+ sessionId: 'sess-viewer',
26
+ authorizationRole: 'viewer',
27
+ capabilities: ['chat:public'],
28
+ authenticated: false,
29
+ },
30
+ conversation: {
31
+ channel: 'public',
32
+ audienceId: 'audience-public',
33
+ correlationId: 'corr-adv-2',
34
+ },
35
+ };
36
+ // INVARIANT 1: Memory Truth & Scoping Boundaries
37
+ describe('Invariant 1: Memory Truth & Cognition Filtering', () => {
38
+ test('expired, future, and below-threshold claims are not injected into Brain contextPrompt', async () => {
39
+ const mockBrain = {
40
+ generatePlan: jest.fn().mockResolvedValue({ speech: 'Cognition received context', language: 'en' }),
41
+ };
42
+ const now = new Date();
43
+ const pastTime = new Date(now.getTime() - 100_000).toISOString();
44
+ const futureTime = new Date(now.getTime() + 100_000).toISOString();
45
+ // Memory mock simulating search output containing valid claim only after organ filtering
46
+ const validClaim = {
47
+ id: 'c-valid',
48
+ companionId: 'companion-adv',
49
+ subject: 'User',
50
+ predicate: 'favoriteColor',
51
+ value: 'Azure',
52
+ status: 'APPROVED',
53
+ scope: 'OWNER',
54
+ confidence: 0.95,
55
+ validFrom: pastTime,
56
+ validUntil: futureTime,
57
+ };
58
+ const mockMemory = {
59
+ initialize: jest.fn().mockResolvedValue(undefined),
60
+ searchClaims: jest.fn().mockResolvedValue([validClaim]),
61
+ getDirectives: jest.fn().mockResolvedValue([]),
62
+ };
63
+ const runtime = new index_1.SiduriRuntime('companion-adv', { name: 'AdvCompanion' }, {
64
+ brain: mockBrain,
65
+ memory: mockMemory,
66
+ });
67
+ await runtime.handleUserMessage('What is my favorite color?', baseOwnerContext);
68
+ expect(mockMemory.searchClaims).toHaveBeenCalled();
69
+ const brainCall = mockBrain.generatePlan.mock.calls[0][0];
70
+ expect(brainCall.contextPrompt).toContain('User favoriteColor Azure');
71
+ });
72
+ test('companion isolation: runtime passes only companionId matching context', async () => {
73
+ const mockBrain = {
74
+ generatePlan: jest.fn().mockResolvedValue({ speech: 'OK', language: 'en' }),
75
+ };
76
+ const mockMemory = {
77
+ initialize: jest.fn().mockResolvedValue(undefined),
78
+ searchClaims: jest.fn().mockResolvedValue([]),
79
+ getDirectives: jest.fn().mockResolvedValue([]),
80
+ };
81
+ const runtime = new index_1.SiduriRuntime('companion-A', { name: 'AdvA' }, {
82
+ brain: mockBrain,
83
+ memory: mockMemory,
84
+ });
85
+ await runtime.handleUserMessage('Query', {
86
+ ...baseOwnerContext,
87
+ companionId: 'companion-A',
88
+ });
89
+ expect(mockMemory.searchClaims).toHaveBeenCalledWith('Query', expect.objectContaining({ channel: 'private', audienceId: 'audience-owner' }), 5);
90
+ });
91
+ });
92
+ // INVARIANT 2: Authority & Request Boundary
93
+ describe('Invariant 2: Authority & Request Context Boundary', () => {
94
+ test('viewer cannot execute admin action intents even if request context is maliciously populated', async () => {
95
+ const store = new index_1.InMemoryActionStore();
96
+ const policyEngine = new index_1.ActionPolicyEngine({ store, secretKey });
97
+ policyEngine.registerToolDefinition({
98
+ name: 'database/drop_tables',
99
+ providerId: 'db',
100
+ description: 'Drop DB tables',
101
+ inputSchema: {},
102
+ riskLevel: 'CRITICAL',
103
+ allowedRoles: ['administrator'],
104
+ requiredCapabilities: ['system:manage'],
105
+ });
106
+ const intent = {
107
+ actionId: 'act-drop-1',
108
+ toolName: 'db/database/drop_tables',
109
+ parameters: {},
110
+ context: baseViewerContext, // Viewer role
111
+ };
112
+ const { decision, capability } = await policyEngine.evaluateAction(intent);
113
+ expect(decision.allowed).toBe(false);
114
+ expect(decision.decisionCode).toBe('REJECTED_UNAUTHORIZED');
115
+ expect(capability).toBeUndefined();
116
+ });
117
+ test('missing request context strictly blocks authorization', async () => {
118
+ const store = new index_1.InMemoryActionStore();
119
+ const policyEngine = new index_1.ActionPolicyEngine({ store, secretKey });
120
+ policyEngine.registerToolDefinition({
121
+ name: 'test_tool',
122
+ providerId: 'sys',
123
+ description: 'Test Tool',
124
+ inputSchema: {},
125
+ riskLevel: 'LOW',
126
+ });
127
+ const intent = {
128
+ actionId: 'act-no-ctx',
129
+ toolName: 'sys/test_tool',
130
+ parameters: {},
131
+ };
132
+ const { decision, capability } = await policyEngine.evaluateAction(intent, undefined);
133
+ expect(decision.allowed).toBe(false);
134
+ expect(decision.decisionCode).toBe('REJECTED_UNAUTHORIZED');
135
+ expect(capability).toBeUndefined();
136
+ });
137
+ });
138
+ // INVARIANT 4: Action Replay & Concurrency Protection
139
+ describe('Invariant 4: Action Idempotency, Concurrency & Signature Replay', () => {
140
+ let store;
141
+ let mockHandlerExecute;
142
+ class TestHandsOrgan {
143
+ store;
144
+ secretKey;
145
+ constructor(store, secretKey) {
146
+ this.store = store;
147
+ this.secretKey = secretKey;
148
+ }
149
+ async listTools() {
150
+ return [{
151
+ name: 'transfer_funds',
152
+ providerId: 'bank',
153
+ description: 'Transfer money',
154
+ inputSchema: {},
155
+ riskLevel: 'CRITICAL',
156
+ }];
157
+ }
158
+ async executeAction(action, authorization) {
159
+ const actionId = action?.actionId || 'unknown';
160
+ const toolName = action?.toolName || 'unknown';
161
+ if (!authorization || authorization.allowed !== true) {
162
+ return { actionId, executionId: 'unauthorized', toolName, lifecycle: 'REJECTED', success: false, error: 'Unauthorized' };
163
+ }
164
+ if (!(0, index_1.verifyCapabilitySignature)(authorization, this.secretKey)) {
165
+ return { actionId, executionId: authorization.executionId, toolName, lifecycle: 'REJECTED', success: false, error: 'Invalid or forged AuthorizationCapability signature' };
166
+ }
167
+ if (authorization.expiresAt && new Date(authorization.expiresAt).getTime() <= Date.now()) {
168
+ return { actionId, executionId: authorization.executionId, toolName, lifecycle: 'REJECTED', success: false, error: 'AuthorizationCapability has expired' };
169
+ }
170
+ const currentParamsHash = (0, index_1.computeParametersHash)(action.parameters);
171
+ if (authorization.parametersHash !== currentParamsHash) {
172
+ return { actionId, executionId: authorization.executionId, toolName, lifecycle: 'REJECTED', success: false, error: 'Parameters hash mismatch' };
173
+ }
174
+ const executionId = authorization.executionId;
175
+ const existing = await this.store.getExecution(executionId);
176
+ if (existing && existing.lifecycle === 'COMPLETED') {
177
+ return { actionId, executionId, toolName, lifecycle: 'COMPLETED', success: true, result: existing.result };
178
+ }
179
+ const reserved = await this.store.reserveExecution({
180
+ executionId,
181
+ actionId,
182
+ toolName,
183
+ providerId: authorization.providerId,
184
+ parametersHash: currentParamsHash,
185
+ lifecycle: 'EXECUTING',
186
+ createdAt: new Date().toISOString(),
187
+ updatedAt: new Date().toISOString(),
188
+ });
189
+ if (!reserved) {
190
+ return { actionId, executionId, toolName, lifecycle: 'FAILED', success: false, error: 'Reservation conflict' };
191
+ }
192
+ const result = await mockHandlerExecute(action.parameters);
193
+ await this.store.updateExecution({
194
+ executionId,
195
+ actionId,
196
+ toolName,
197
+ providerId: authorization.providerId,
198
+ parametersHash: currentParamsHash,
199
+ lifecycle: 'COMPLETED',
200
+ result,
201
+ createdAt: new Date().toISOString(),
202
+ updatedAt: new Date().toISOString(),
203
+ });
204
+ return { actionId, executionId, toolName, lifecycle: 'COMPLETED', success: true, result };
205
+ }
206
+ }
207
+ let hands;
208
+ beforeEach(() => {
209
+ store = new index_1.InMemoryActionStore();
210
+ mockHandlerExecute = jest.fn().mockResolvedValue({ transactionId: 'tx-12345', status: 'CONFIRMED' });
211
+ hands = new TestHandsOrgan(store, secretKey);
212
+ });
213
+ test('replaying a completed capability returns cached result without re-executing handler', async () => {
214
+ const mockExecute = mockHandlerExecute;
215
+ const action = {
216
+ actionId: 'act-tx-1',
217
+ executionId: 'exec-tx-1',
218
+ toolName: 'bank/transfer_funds',
219
+ parameters: { amount: 100, to: 'Alice' },
220
+ };
221
+ const paramsHash = (0, index_1.computeParametersHash)(action.parameters);
222
+ const capabilityPayload = {
223
+ executionId: 'exec-tx-1',
224
+ actionId: 'act-tx-1',
225
+ toolName: 'bank/transfer_funds',
226
+ providerId: 'bank',
227
+ parametersHash: paramsHash,
228
+ companionId: 'companion-adv',
229
+ actorId: 'owner-user',
230
+ sessionId: 'sess-owner',
231
+ channel: 'private',
232
+ correlationId: 'corr-adv-1',
233
+ riskLevel: 'CRITICAL',
234
+ issuedAt: new Date().toISOString(),
235
+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
236
+ };
237
+ const signature = (0, index_1.signCapabilityPayload)(capabilityPayload, secretKey);
238
+ const capability = {
239
+ ...capabilityPayload,
240
+ allowed: true,
241
+ signature,
242
+ };
243
+ // 1. First execution succeeds
244
+ const res1 = await hands.executeAction(action, capability);
245
+ expect(res1.success).toBe(true);
246
+ expect(res1.lifecycle).toBe('COMPLETED');
247
+ expect(mockExecute).toHaveBeenCalledTimes(1);
248
+ // 2. Replay execution with identical capability
249
+ const res2 = await hands.executeAction(action, capability);
250
+ expect(res2.success).toBe(true);
251
+ expect(res2.lifecycle).toBe('COMPLETED');
252
+ expect(res2.result).toEqual({ transactionId: 'tx-12345', status: 'CONFIRMED' });
253
+ // Handler was NOT called a second time (replay defended)
254
+ expect(mockExecute).toHaveBeenCalledTimes(1);
255
+ });
256
+ test('tampering with action parameters invalidates cryptographic capability', async () => {
257
+ const action = {
258
+ actionId: 'act-tx-2',
259
+ executionId: 'exec-tx-2',
260
+ toolName: 'bank/transfer_funds',
261
+ parameters: { amount: 100, to: 'Alice' },
262
+ };
263
+ const paramsHash = (0, index_1.computeParametersHash)(action.parameters);
264
+ const capabilityPayload = {
265
+ executionId: 'exec-tx-2',
266
+ actionId: 'act-tx-2',
267
+ toolName: 'bank/transfer_funds',
268
+ providerId: 'bank',
269
+ parametersHash: paramsHash,
270
+ companionId: 'companion-adv',
271
+ actorId: 'owner-user',
272
+ sessionId: 'sess-owner',
273
+ channel: 'private',
274
+ correlationId: 'corr-adv-1',
275
+ riskLevel: 'CRITICAL',
276
+ issuedAt: new Date().toISOString(),
277
+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
278
+ };
279
+ const signature = (0, index_1.signCapabilityPayload)(capabilityPayload, secretKey);
280
+ const capability = { ...capabilityPayload, allowed: true, signature };
281
+ // Attacker tampers with parameters from $100 to $10,000
282
+ const tamperedAction = {
283
+ ...action,
284
+ parameters: { amount: 10_000, to: 'Attacker' },
285
+ };
286
+ const res = await hands.executeAction(tamperedAction, capability);
287
+ expect(res.success).toBe(false);
288
+ expect(res.lifecycle).toBe('REJECTED');
289
+ expect(res.error).toContain('Parameters hash mismatch');
290
+ });
291
+ test('forged signature on capability is rejected by constant-time verification', async () => {
292
+ const action = {
293
+ actionId: 'act-tx-3',
294
+ executionId: 'exec-tx-3',
295
+ toolName: 'bank/transfer_funds',
296
+ parameters: { amount: 100, to: 'Alice' },
297
+ };
298
+ const capability = {
299
+ executionId: 'exec-tx-3',
300
+ actionId: 'act-tx-3',
301
+ toolName: 'bank/transfer_funds',
302
+ providerId: 'bank',
303
+ parametersHash: (0, index_1.computeParametersHash)(action.parameters),
304
+ companionId: 'companion-adv',
305
+ actorId: 'owner-user',
306
+ sessionId: 'sess-owner',
307
+ channel: 'private',
308
+ correlationId: 'corr-adv-1',
309
+ riskLevel: 'CRITICAL',
310
+ issuedAt: new Date().toISOString(),
311
+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
312
+ allowed: true,
313
+ signature: '0000000000000000000000000000000000000000000000000000000000000000', // Forged signature
314
+ };
315
+ const res = await hands.executeAction(action, capability);
316
+ expect(res.success).toBe(false);
317
+ expect(res.lifecycle).toBe('REJECTED');
318
+ expect(res.error).toContain('Invalid or forged AuthorizationCapability signature');
319
+ });
320
+ test('expired authorization capability is rejected', async () => {
321
+ const action = {
322
+ actionId: 'act-tx-4',
323
+ executionId: 'exec-tx-4',
324
+ toolName: 'bank/transfer_funds',
325
+ parameters: { amount: 100, to: 'Alice' },
326
+ };
327
+ const paramsHash = (0, index_1.computeParametersHash)(action.parameters);
328
+ const expiredTime = new Date(Date.now() - 5000).toISOString();
329
+ const capabilityPayload = {
330
+ executionId: 'exec-tx-4',
331
+ actionId: 'act-tx-4',
332
+ toolName: 'bank/transfer_funds',
333
+ providerId: 'bank',
334
+ parametersHash: paramsHash,
335
+ companionId: 'companion-adv',
336
+ actorId: 'owner-user',
337
+ sessionId: 'sess-owner',
338
+ channel: 'private',
339
+ correlationId: 'corr-adv-1',
340
+ riskLevel: 'CRITICAL',
341
+ issuedAt: new Date(Date.now() - 10000).toISOString(),
342
+ expiresAt: expiredTime,
343
+ };
344
+ const signature = (0, index_1.signCapabilityPayload)(capabilityPayload, secretKey);
345
+ const capability = { ...capabilityPayload, allowed: true, signature };
346
+ const res = await hands.executeAction(action, capability);
347
+ expect(res.success).toBe(false);
348
+ expect(res.lifecycle).toBe('REJECTED');
349
+ expect(res.error).toContain('AuthorizationCapability has expired');
350
+ });
351
+ });
352
+ // INVARIANT 6: Failure Semantics & Degradation
353
+ describe('Invariant 6: Subsystem Failure Diagnostics & Non-Empty Propagation', () => {
354
+ test('database/memory query failure surfaces diagnostic in contextPrompt and metadata', async () => {
355
+ const mockBrain = {
356
+ generatePlan: jest.fn().mockResolvedValue({ speech: 'Graceful fallback response', language: 'en' }),
357
+ };
358
+ const failingMemory = {
359
+ initialize: jest.fn().mockResolvedValue(undefined),
360
+ searchClaims: jest.fn().mockRejectedValue(new Error('Connection terminated unexpectedly')),
361
+ getDirectives: jest.fn().mockRejectedValue(new Error('PostgreSQL read timeout')),
362
+ };
363
+ const runtime = new index_1.SiduriRuntime('companion-adv', { name: 'AdvCompanion' }, {
364
+ brain: mockBrain,
365
+ memory: failingMemory,
366
+ });
367
+ const response = await runtime.handleUserMessage('Hello companion', baseOwnerContext);
368
+ expect(response.status).toBe('APPROVED');
369
+ expect(response.metadata.subsystem_diagnostics).toBeDefined();
370
+ expect(response.metadata.subsystem_diagnostics.memory_claims).toContain('UNAVAILABLE');
371
+ expect(response.metadata.subsystem_diagnostics.memory_directives).toContain('UNAVAILABLE');
372
+ const brainCall = mockBrain.generatePlan.mock.calls[0][0];
373
+ expect(brainCall.contextPrompt).toContain('SUBSYSTEM STATUS (DEGRADED):');
374
+ expect(brainCall.contextPrompt).toContain('memory_claims');
375
+ });
376
+ });
377
+ // INVARIANT 8: Truth Gate Admissibility vs Factuality
378
+ describe('Invariant 8: Response Gating Evidence Admissibility Semantics', () => {
379
+ test('gate strictly enforces evidence admissibility and disclosure without claiming unverified factuality', () => {
380
+ const gating = new index_1.ResponseGatingEngine();
381
+ const publicEvidence = {
382
+ evidenceId: 'ev-pub-1',
383
+ sourceId: 'src-facts',
384
+ origin: 'knowledge',
385
+ trust: 'configured',
386
+ sensitivity: 'public',
387
+ allowedAudiences: ['audience-public'],
388
+ companionId: 'companion-adv',
389
+ correlationId: 'corr-adv-1',
390
+ createdAt: new Date().toISOString(),
391
+ };
392
+ const privateEvidence = {
393
+ evidenceId: 'ev-priv-1',
394
+ sourceId: 'src-secrets',
395
+ origin: 'knowledge',
396
+ trust: 'configured',
397
+ sensitivity: 'restricted',
398
+ allowedAudiences: ['audience-owner'],
399
+ companionId: 'companion-adv',
400
+ correlationId: 'corr-adv-1',
401
+ createdAt: new Date().toISOString(),
402
+ };
403
+ // Staged for public channel with both public and restricted evidence attached
404
+ const staged = gating.stageResponse({
405
+ requestContext: baseViewerContext, // Public channel
406
+ candidateSpeech: 'Siduri was created in 1840 by aliens.',
407
+ candidateLanguage: 'en',
408
+ evidenceRecords: [publicEvidence, privateEvidence],
409
+ });
410
+ const evaluation = gating.evaluateGate(staged, [publicEvidence, privateEvidence]);
411
+ expect(evaluation.admissible).toBe(true);
412
+ expect(evaluation.reasonCode).toBe('APPROVED_DIRECT');
413
+ // Public evidence admitted, restricted private evidence excluded from public emission
414
+ expect(evaluation.filteredEvidenceIds).toEqual(['ev-pub-1']);
415
+ expect(evaluation.filteredEvidenceIds).not.toContain('ev-priv-1');
416
+ });
417
+ });
418
+ // INVARIANT 9: Full-Field Tamper-Evident Audit Trail
419
+ describe('Invariant 9: Full-Field SHA-256 Audit Trail Chaining', () => {
420
+ test('mutating any security-critical field breaks cryptographic hash chain', async () => {
421
+ const store = new index_1.InMemoryActionStore();
422
+ const policyEngine = new index_1.ActionPolicyEngine({ store, secretKey });
423
+ policyEngine.registerToolDefinition({
424
+ name: 'test_tool',
425
+ providerId: 'sys',
426
+ description: 'Test Tool',
427
+ inputSchema: {},
428
+ riskLevel: 'LOW',
429
+ });
430
+ // Event 1
431
+ await policyEngine.evaluateAction({
432
+ actionId: 'act-1',
433
+ toolName: 'sys/test_tool',
434
+ parameters: { step: 1 },
435
+ context: baseOwnerContext,
436
+ });
437
+ // Event 2
438
+ await policyEngine.evaluateAction({
439
+ actionId: 'act-2',
440
+ toolName: 'sys/test_tool',
441
+ parameters: { step: 2 },
442
+ context: baseOwnerContext,
443
+ });
444
+ const auditTrail = await store.getAuditLog();
445
+ expect(auditTrail.length).toBe(2);
446
+ const event1 = auditTrail[0];
447
+ const event2 = auditTrail[1];
448
+ // Mutate security-critical fields in event1 and verify chain discrepancy
449
+ const criticalFields = [
450
+ 'executionId',
451
+ 'actionId',
452
+ 'toolName',
453
+ 'companionId',
454
+ 'actorId',
455
+ 'sessionId',
456
+ 'channel',
457
+ 'correlationId',
458
+ 'riskLevel',
459
+ 'lifecycle',
460
+ 'parametersHash',
461
+ ];
462
+ for (const field of criticalFields) {
463
+ const tamperedEvent = { ...event1, [field]: 'TAMPERED_VALUE' };
464
+ const initialPrevHash = '0000000000000000000000000000000000000000000000000000000000000000';
465
+ const canonical = (0, index_1.canonicalizeJson)({
466
+ executionId: tamperedEvent.executionId,
467
+ actionId: tamperedEvent.actionId,
468
+ toolName: tamperedEvent.toolName,
469
+ providerId: tamperedEvent.providerId || null,
470
+ companionId: tamperedEvent.companionId,
471
+ actorId: tamperedEvent.actorId || null,
472
+ sessionId: tamperedEvent.sessionId || null,
473
+ channel: tamperedEvent.channel || null,
474
+ correlationId: tamperedEvent.correlationId || null,
475
+ riskLevel: tamperedEvent.riskLevel,
476
+ lifecycle: tamperedEvent.lifecycle,
477
+ decision: tamperedEvent.decision ? {
478
+ allowed: tamperedEvent.decision.allowed,
479
+ reason: tamperedEvent.decision.reason,
480
+ riskLevel: tamperedEvent.decision.riskLevel,
481
+ decisionCode: tamperedEvent.decision.decisionCode,
482
+ } : null,
483
+ parametersHash: tamperedEvent.parametersHash || null,
484
+ error: tamperedEvent.error || null,
485
+ timestamp: tamperedEvent.timestamp,
486
+ });
487
+ const crypto = require('node:crypto');
488
+ const brokenHash1 = crypto.createHash('sha256').update(`${initialPrevHash}:${canonical}`, 'utf8').digest('hex');
489
+ expect(brokenHash1).not.toBe(event1.resultHash);
490
+ }
491
+ });
492
+ });
493
+ });
@@ -33,16 +33,21 @@ export interface ActionStore {
33
33
  reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
34
34
  updateExecution(record: PersistentExecutionRecord): Promise<void>;
35
35
  getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
36
+ saveApproval(executionId: string, approverActorId: string, reason?: string): Promise<void>;
37
+ isActionApproved(executionId: string): Promise<boolean>;
36
38
  appendAudit(event: ActionAuditEvent): Promise<void>;
37
39
  getAuditLog(executionId?: string): Promise<ActionAuditEvent[]>;
38
40
  }
39
41
  export declare class InMemoryActionStore implements ActionStore {
40
42
  private readonly executions;
43
+ private readonly approvals;
41
44
  private readonly auditLog;
42
45
  private lastAuditHash;
43
46
  reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
44
47
  updateExecution(record: PersistentExecutionRecord): Promise<void>;
45
48
  getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
49
+ saveApproval(executionId: string, approverActorId: string, reason?: string): Promise<void>;
50
+ isActionApproved(executionId: string): Promise<boolean>;
46
51
  appendAudit(event: ActionAuditEvent): Promise<void>;
47
52
  getAuditLog(executionId?: string): Promise<ActionAuditEvent[]>;
48
53
  }
@@ -11,6 +11,7 @@ exports.verifyCapabilitySignature = verifyCapabilitySignature;
11
11
  const crypto = require('crypto');
12
12
  class InMemoryActionStore {
13
13
  executions = new Map();
14
+ approvals = new Map();
14
15
  auditLog = [];
15
16
  lastAuditHash = '0000000000000000000000000000000000000000000000000000000000000000';
16
17
  async reserveExecution(record) {
@@ -27,24 +28,51 @@ class InMemoryActionStore {
27
28
  const rec = this.executions.get(executionId);
28
29
  return rec ? { ...rec } : undefined;
29
30
  }
31
+ async saveApproval(executionId, approverActorId, reason) {
32
+ this.approvals.set(executionId, {
33
+ approverActorId,
34
+ reason,
35
+ approvedAt: new Date().toISOString(),
36
+ });
37
+ }
38
+ async isActionApproved(executionId) {
39
+ return this.approvals.has(executionId);
40
+ }
30
41
  async appendAudit(event) {
31
- // Tamper-evident hash chaining: SHA-256(prevHash + ":" + canonicalEvent)
42
+ const prevHash = this.lastAuditHash;
43
+ // Tamper-evident hash chaining over all security-critical event fields
32
44
  const eventPayload = {
33
45
  executionId: event.executionId,
34
46
  actionId: event.actionId,
35
47
  toolName: event.toolName,
48
+ providerId: event.providerId || null,
49
+ companionId: event.companionId,
50
+ actorId: event.actorId || null,
51
+ sessionId: event.sessionId || null,
52
+ channel: event.channel || null,
53
+ correlationId: event.correlationId || null,
54
+ riskLevel: event.riskLevel,
36
55
  lifecycle: event.lifecycle,
37
- parametersHash: event.parametersHash,
56
+ decision: event.decision ? {
57
+ allowed: event.decision.allowed,
58
+ reason: event.decision.reason,
59
+ riskLevel: event.decision.riskLevel,
60
+ decisionCode: event.decision.decisionCode,
61
+ } : null,
62
+ parametersHash: event.parametersHash || null,
63
+ error: event.error || null,
38
64
  timestamp: event.timestamp,
39
65
  };
40
66
  const canonical = canonicalizeJson(eventPayload);
41
67
  const eventHash = crypto
42
68
  .createHash('sha256')
43
- .update(`${this.lastAuditHash}:${canonical}`, 'utf8')
69
+ .update(`${prevHash}:${canonical}`, 'utf8')
44
70
  .digest('hex');
45
71
  this.lastAuditHash = eventHash;
46
72
  const recordWithHash = {
47
73
  ...event,
74
+ previousEventHash: prevHash,
75
+ eventHash,
48
76
  resultHash: event.resultHash || eventHash,
49
77
  };
50
78
  this.auditLog.push(recordWithHash);
@@ -151,22 +151,52 @@ describe('AuthorizationCapability Cryptographic & Tamper Review', () => {
151
151
  executionId: event1.executionId,
152
152
  actionId: event1.actionId,
153
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,
154
161
  lifecycle: event1.lifecycle,
155
- parametersHash: event1.parametersHash,
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,
156
170
  timestamp: event1.timestamp,
157
171
  });
158
172
  const expectedHash1 = crypto.createHash('sha256').update(`${initialPrevHash}:${canonical1}`, 'utf8').digest('hex');
159
- expect(event1.resultHash).toBe(expectedHash1);
173
+ expect(event1.eventHash).toBe(expectedHash1);
174
+ expect(event1.previousEventHash).toBe(initialPrevHash);
160
175
  const canonical2 = (0, capability_1.canonicalizeJson)({
161
176
  executionId: event2.executionId,
162
177
  actionId: event2.actionId,
163
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,
164
186
  lifecycle: event2.lifecycle,
165
- parametersHash: event2.parametersHash,
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,
166
195
  timestamp: event2.timestamp,
167
196
  });
168
197
  const expectedHash2 = crypto.createHash('sha256').update(`${expectedHash1}:${canonical2}`, 'utf8').digest('hex');
169
- expect(event2.resultHash).toBe(expectedHash2);
198
+ expect(event2.eventHash).toBe(expectedHash2);
199
+ expect(event2.previousEventHash).toBe(expectedHash1);
170
200
  // If an attacker altered event 1 retrospectively, the hash chain breaks for event 2
171
201
  const tamperedCanonical1 = (0, capability_1.canonicalizeJson)({
172
202
  ...JSON.parse(canonical1),
@@ -177,4 +207,63 @@ describe('AuthorizationCapability Cryptographic & Tamper Review', () => {
177
207
  expect(brokenHash2).not.toBe(event2.resultHash);
178
208
  });
179
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
+ });
180
269
  });
@@ -0,0 +1,77 @@
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
+ export interface ChatRequest {
7
+ id?: string;
8
+ companionId?: string;
9
+ message: string;
10
+ role?: 'OWNER' | 'VIEWER' | 'OPERATOR';
11
+ context?: RequestContext;
12
+ history?: Message[];
13
+ [key: string]: any;
14
+ }
15
+ export interface ChatResponseMetadataEvent {
16
+ event_id: string;
17
+ kind: string;
18
+ lifecycle: string;
19
+ approval?: string;
20
+ expression?: string;
21
+ action?: string;
22
+ durationMs?: number;
23
+ }
24
+ export interface ChatResponsePlan {
25
+ speech_id?: string;
26
+ audio_url?: string;
27
+ subtitle_ja: string;
28
+ subtitle_en: string;
29
+ spoken_ja?: string;
30
+ evidence_ids?: string[];
31
+ }
32
+ export interface ChatResponseMetadata {
33
+ language?: string;
34
+ proposals?: Claim[];
35
+ memory_proposals?: Array<{
36
+ proposal_id: string;
37
+ subject?: string;
38
+ predicate?: string;
39
+ value?: string;
40
+ status: string;
41
+ content?: string;
42
+ claim_type?: string;
43
+ }>;
44
+ behavioral_proposals?: Array<{
45
+ directive_id: string;
46
+ memory_class: string;
47
+ domain: string;
48
+ subject: string;
49
+ predicate: string;
50
+ value: string;
51
+ status: string;
52
+ behavior?: any;
53
+ runtime_effect?: string;
54
+ }>;
55
+ action_results?: ActionExecutionResult[];
56
+ evidence_ids?: string[];
57
+ citations?: ResponseCitation[];
58
+ subsystem_diagnostics?: Record<string, string>;
59
+ events?: ChatResponseMetadataEvent[];
60
+ [key: string]: any;
61
+ }
62
+ export interface ChatResponse {
63
+ status: 'APPROVED' | 'REJECTED' | 'STAGED' | string;
64
+ response_id?: string;
65
+ correlation_id?: string;
66
+ response: ChatResponsePlan;
67
+ metadata?: ChatResponseMetadata;
68
+ reply?: string;
69
+ text?: string;
70
+ audioUrl?: string;
71
+ expression?: string;
72
+ }
73
+ /**
74
+ * Canonical helper to dispatch a chat request to a SiduriRuntime instance.
75
+ * Guarantees a 1:1 identical response structure for localweb (apps/api) and standalone CLI.
76
+ */
77
+ export declare function dispatchCompanionChat(runtime: SiduriRuntime, payload: ChatRequest): Promise<ChatResponse>;
@@ -0,0 +1,65 @@
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.role) {
13
+ roleOrContext = payload.role;
14
+ }
15
+ else if (payload.context) {
16
+ // Map authorization role to legacy memory scope for backwards-compatible runtime calls
17
+ const authRole = payload.context.actor?.authorizationRole;
18
+ roleOrContext =
19
+ authRole === 'administrator'
20
+ ? 'OWNER'
21
+ : authRole === 'operator'
22
+ ? 'OPERATOR'
23
+ : (authRole === 'viewer' ? 'VIEWER' : 'OWNER');
24
+ }
25
+ else {
26
+ roleOrContext = 'OWNER';
27
+ }
28
+ const runtimeResult = await runtime.handleUserMessage(userMessage, roleOrContext, history);
29
+ // Normalize response plan
30
+ const speech = runtimeResult?.response?.subtitle_ja || runtimeResult?.response?.subtitle_en || '';
31
+ const audioUrl = runtimeResult?.response?.audio_url;
32
+ // Extract avatar expression if any event was generated
33
+ let expression = 'neutral';
34
+ const events = runtimeResult?.metadata?.events || [];
35
+ const avatarEvent = events.find((e) => e.kind === 'avatar' || e.kind === 'body');
36
+ if (avatarEvent && avatarEvent.expression) {
37
+ expression = avatarEvent.expression;
38
+ }
39
+ // Ensure both spoken_ja and subtitle_en are accessible alongside speech_id and evidence_ids
40
+ const responsePlan = {
41
+ speech_id: runtimeResult?.response?.speech_id,
42
+ audio_url: audioUrl,
43
+ subtitle_ja: runtimeResult?.response?.subtitle_ja ?? speech,
44
+ subtitle_en: runtimeResult?.response?.subtitle_en ?? speech,
45
+ spoken_ja: runtimeResult?.response?.spoken_ja ?? runtimeResult?.response?.subtitle_ja ?? speech,
46
+ evidence_ids: runtimeResult?.metadata?.evidence_ids ?? runtimeResult?.response?.evidence_ids ?? [],
47
+ };
48
+ const metadata = {
49
+ ...(runtimeResult?.metadata || {}),
50
+ };
51
+ delete metadata.internal_monologue;
52
+ delete metadata.internalMonologue;
53
+ return {
54
+ status: runtimeResult?.status || 'APPROVED',
55
+ response_id: runtimeResult?.response_id,
56
+ correlation_id: runtimeResult?.correlation_id,
57
+ response: responsePlan,
58
+ metadata,
59
+ // Convenience fields for legacy/simple consumers
60
+ reply: speech,
61
+ text: speech,
62
+ audioUrl,
63
+ expression,
64
+ };
65
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from './ear-types';
9
9
  export * from './capability';
10
10
  export * from './teaching';
11
11
  export * from './runtime';
12
+ export * from './chat-contract';
12
13
  import { ActionIntent } from './action';
13
14
  import { EarIngestOptions } from './ear-types';
14
15
  export interface OrganConfig {
@@ -123,6 +124,8 @@ export interface MemoryQueryOptions {
123
124
  audienceId?: string;
124
125
  sensitivity?: string;
125
126
  limit?: number;
127
+ minConfidence?: number;
128
+ now?: string | Date;
126
129
  }
127
130
  export interface MemoryOrgan {
128
131
  initialize(companionId: string): Promise<void>;
@@ -143,6 +146,8 @@ export interface MemoryOrgan {
143
146
  disableDirective(id: string): Promise<void>;
144
147
  expireDirective?(id: string): Promise<void>;
145
148
  supersedeClaim?(id: string, replacement: Omit<Claim, 'id' | 'status' | 'companionId'>): Promise<Claim>;
149
+ updateClaim?(id: string, updates: Partial<Pick<Claim, 'subject' | 'predicate' | 'value' | 'scope' | 'sensitivity' | 'confidence' | 'validFrom' | 'validUntil' | 'allowedAudiences'>>): Promise<Claim>;
150
+ resetMemory?(): Promise<void>;
146
151
  addSourceEvent?(event: SourceEvent): Promise<SourceEvent>;
147
152
  getSourceEvent?(id: string): Promise<SourceEvent | undefined>;
148
153
  }
package/dist/index.js CHANGED
@@ -26,3 +26,4 @@ __exportStar(require("./ear-types"), exports);
26
26
  __exportStar(require("./capability"), exports);
27
27
  __exportStar(require("./teaching"), exports);
28
28
  __exportStar(require("./runtime"), exports);
29
+ __exportStar(require("./chat-contract"), exports);
package/dist/runtime.js CHANGED
@@ -111,17 +111,21 @@ class SiduriRuntime {
111
111
  limit: 5,
112
112
  }
113
113
  : role;
114
+ const subsystemDiagnostics = {};
114
115
  const [knowledgeData, memoryData, activeDirectives] = await Promise.all([
115
116
  this.knowledge && shouldQueryKnowledge && typeof this.knowledge.search === 'function' ? this.knowledge.search(perceivedText).catch(e => {
116
117
  console.error("[SiduriRuntime] Knowledge search failed:", e.message);
118
+ subsystemDiagnostics['knowledge'] = `UNAVAILABLE: ${e.message}`;
117
119
  return [];
118
120
  }) : Promise.resolve([]),
119
121
  this.memory && typeof this.memory.searchClaims === 'function' ? this.memory.searchClaims(perceivedText, queryOptions, 5).catch(e => {
120
122
  console.error("[SiduriRuntime] Memory search failed:", e.message);
123
+ subsystemDiagnostics['memory_claims'] = `UNAVAILABLE: ${e.message}`;
121
124
  return [];
122
125
  }) : Promise.resolve([]),
123
126
  this.memory && typeof this.memory.getDirectives === 'function' ? this.memory.getDirectives().catch(e => {
124
127
  console.error("[SiduriRuntime] Memory directives failed:", e.message);
128
+ subsystemDiagnostics['memory_directives'] = `UNAVAILABLE: ${e.message}`;
125
129
  return [];
126
130
  }) : Promise.resolve([])
127
131
  ]);
@@ -154,6 +158,9 @@ class SiduriRuntime {
154
158
  }
155
159
  }
156
160
  let contextPrompt = "";
161
+ if (Object.keys(subsystemDiagnostics).length > 0) {
162
+ contextPrompt += "SUBSYSTEM STATUS (DEGRADED):\n" + Object.entries(subsystemDiagnostics).map(([k, v]) => `- [${k}] ${v}`).join("\n") + "\n";
163
+ }
157
164
  if (knowledgeData.length > 0) {
158
165
  contextPrompt += "KNOWLEDGE:\n" + knowledgeData.map(k => `- [revision:${k.revision} source:${k.provenance}] ${k.content}`).join("\n") + "\n";
159
166
  }
@@ -383,12 +390,12 @@ class SiduriRuntime {
383
390
  },
384
391
  metadata: {
385
392
  language: plan.language,
386
- internal_monologue: plan.internalMonologue,
387
393
  proposals: createdMemoryProposals,
388
394
  memory_proposals: memoryProposalReceipts,
389
395
  action_results: actionResults,
390
396
  evidence_ids: gateEval.filteredEvidenceIds,
391
397
  citations: gateEval.filteredCitations,
398
+ subsystem_diagnostics: Object.keys(subsystemDiagnostics).length > 0 ? subsystemDiagnostics : undefined,
392
399
  events: experienceEvents.map(e => ({
393
400
  event_id: e.eventId,
394
401
  kind: e.kind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siduri-x/core",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Core runtime types, evidence protocol, action dispatcher, capability validation, and SiduriRuntime protocol",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {