@siduri-x/core 1.0.1 → 1.0.3

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;
@@ -123,7 +123,14 @@ class ActionPolicyEngine {
123
123
  // Risk level approval check
124
124
  const requiresExplicitApproval = toolDef.requiresApproval ??
125
125
  (this.defaultRequireApprovalForHighRisk && (riskLevel === 'HIGH' || riskLevel === 'CRITICAL'));
126
- if (requiresExplicitApproval && !this.approvedExecutions.has(executionId)) {
126
+ let isApproved = this.approvedExecutions.has(executionId);
127
+ if (!isApproved && typeof this.store.isActionApproved === 'function') {
128
+ isApproved = await this.store.isActionApproved(executionId);
129
+ if (isApproved) {
130
+ this.approvedExecutions.add(executionId);
131
+ }
132
+ }
133
+ if (requiresExplicitApproval && !isApproved) {
127
134
  const decision = {
128
135
  allowed: false,
129
136
  reason: `Action "${action.toolName}" has risk level ${riskLevel} and requires explicit approval`,
@@ -171,8 +178,11 @@ class ActionPolicyEngine {
171
178
  await this.recordAudit(action, effectiveContext, decision, 'APPROVED');
172
179
  return { decision, capability };
173
180
  }
174
- approveAction(options) {
181
+ async approveAction(options) {
175
182
  this.approvedExecutions.add(options.executionId);
183
+ if (typeof this.store.saveApproval === 'function') {
184
+ await this.store.saveApproval(options.executionId, options.approverActorId, options.reason);
185
+ }
176
186
  return true;
177
187
  }
178
188
  async recordAudit(action, context, decision, lifecycle, result, error, durationMs) {
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
+ });
@@ -51,7 +51,7 @@ describe('Architecture: Core & Organ Package Boundaries (Phase 2)', () => {
51
51
  { dir: 'voice', name: '@siduri-x/voice', organType: 'voice', configKey: 'voice' },
52
52
  { dir: 'observation', name: '@siduri-x/observation', organType: 'observation', configKey: 'observation' },
53
53
  ];
54
- it('package.json has zero dependencies on @siduri-y organ packages', () => {
54
+ it('package.json has zero dependencies on @siduri-x organ packages', () => {
55
55
  const pkg = JSON.parse(fs.readFileSync(corePackageJsonPath, 'utf8'));
56
56
  const allDeps = {
57
57
  ...(pkg.dependencies || {}),
@@ -61,7 +61,7 @@ describe('Architecture: Core & Organ Package Boundaries (Phase 2)', () => {
61
61
  const organDeps = Object.keys(allDeps).filter((dep) => dep.startsWith('@siduri-x/') && dep !== '@siduri-x/core');
62
62
  expect(organDeps).toEqual([]);
63
63
  });
64
- it('source files in packages/core have zero imports referencing @siduri-y organ packages', () => {
64
+ it('source files in packages/core have zero imports referencing @siduri-x organ packages', () => {
65
65
  const files = fs.readdirSync(coreSrcDir).filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts'));
66
66
  const forbiddenImports = [];
67
67
  for (const file of files) {
@@ -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,78 @@
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
+ internal_monologue?: string;
35
+ proposals?: Claim[];
36
+ memory_proposals?: Array<{
37
+ proposal_id: string;
38
+ subject?: string;
39
+ predicate?: string;
40
+ value?: string;
41
+ status: string;
42
+ content?: string;
43
+ claim_type?: string;
44
+ }>;
45
+ behavioral_proposals?: Array<{
46
+ directive_id: string;
47
+ memory_class: string;
48
+ domain: string;
49
+ subject: string;
50
+ predicate: string;
51
+ value: string;
52
+ status: string;
53
+ behavior?: any;
54
+ runtime_effect?: string;
55
+ }>;
56
+ action_results?: ActionExecutionResult[];
57
+ evidence_ids?: string[];
58
+ citations?: ResponseCitation[];
59
+ subsystem_diagnostics?: Record<string, string>;
60
+ events?: ChatResponseMetadataEvent[];
61
+ [key: string]: any;
62
+ }
63
+ export interface ChatResponse {
64
+ status: 'APPROVED' | 'REJECTED' | 'STAGED' | string;
65
+ response_id?: string;
66
+ correlation_id?: string;
67
+ response: ChatResponsePlan;
68
+ metadata?: ChatResponseMetadata;
69
+ reply?: string;
70
+ text?: string;
71
+ audioUrl?: string;
72
+ expression?: string;
73
+ }
74
+ /**
75
+ * Canonical helper to dispatch a chat request to a SiduriRuntime instance.
76
+ * Guarantees a 1:1 identical response structure for localweb (apps/api) and standalone CLI.
77
+ */
78
+ export declare function dispatchCompanionChat(runtime: SiduriRuntime, payload: ChatRequest): Promise<ChatResponse>;
@@ -0,0 +1,63 @@
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
+ : 'VIEWER';
24
+ }
25
+ else {
26
+ roleOrContext = 'VIEWER';
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
+ return {
52
+ status: runtimeResult?.status || 'APPROVED',
53
+ response_id: runtimeResult?.response_id,
54
+ correlation_id: runtimeResult?.correlation_id,
55
+ response: responsePlan,
56
+ metadata,
57
+ // Convenience fields for legacy/simple consumers
58
+ reply: speech,
59
+ text: speech,
60
+ audioUrl,
61
+ expression,
62
+ };
63
+ }
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
  }
@@ -389,6 +396,7 @@ class SiduriRuntime {
389
396
  action_results: actionResults,
390
397
  evidence_ids: gateEval.filteredEvidenceIds,
391
398
  citations: gateEval.filteredCitations,
399
+ subsystem_diagnostics: Object.keys(subsystemDiagnostics).length > 0 ? subsystemDiagnostics : undefined,
392
400
  events: experienceEvents.map(e => ({
393
401
  event_id: e.eventId,
394
402
  kind: e.kind,
package/dist/teaching.js CHANGED
@@ -52,7 +52,7 @@ function extractDeterministicTeaching(message, context, sourceEventId) {
52
52
  }
53
53
  // 2. Actor's Name: "my name is X"
54
54
  const myNameMatch = text.match(/\bmy name is\s+(.+?)(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
55
- if (myNameMatch && !/\b(?:private|public|stream|everywhere)\b/i.test(text)) {
55
+ if (myNameMatch && !/\b(?:private|public|everywhere)\b/i.test(text)) {
56
56
  const name = cleanValue(myNameMatch[1], 80);
57
57
  claims.push({
58
58
  subject: actorSubject,
@@ -66,8 +66,8 @@ function extractDeterministicTeaching(message, context, sourceEventId) {
66
66
  sourceEventId,
67
67
  });
68
68
  }
69
- // 3. Preferred Address / Call me X: "call me X [in private / on stream / in public]"
70
- const callMeMatch = text.match(/\b(?:(?:from now on|only),?\s*)?call me\s+(.+?)(?:\s+(in private|privately|on stream|in public|publicly|everywhere|in direct conversations))?(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
69
+ // 3. Preferred Address / Call me X: "call me X"
70
+ const callMeMatch = text.match(/\b(?:(?:from now on|only),?\s*)?call me\s+(.+?)(?:\s+(in private|privately|in public|publicly|everywhere|in direct conversations))?(?=\s+and\s+(?:i\b|my\b|you\b)|[.;,]|$)/i);
71
71
  if (callMeMatch) {
72
72
  const address = cleanValue(callMeMatch[1], 80);
73
73
  const scopePhrase = (callMeMatch[2] || '').toLowerCase();
@@ -79,7 +79,7 @@ function extractDeterministicTeaching(message, context, sourceEventId) {
79
79
  claimAudiences = [context?.conversation?.audienceId || `audience-private-${actorId}`];
80
80
  directiveInstruction += ' in private conversations';
81
81
  }
82
- else if (scopePhrase.includes('stream') || scopePhrase.includes('public') || scopePhrase.includes('publicly')) {
82
+ else if (scopePhrase.includes('public') || scopePhrase.includes('publicly')) {
83
83
  claimSensitivity = 'public';
84
84
  claimAudiences = ['audience-public'];
85
85
  directiveInstruction += ' in public conversations';
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@siduri-x/core",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Core runtime types, evidence protocol, action dispatcher, capability validation, and SiduriRuntime protocol",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/vxnuslabs/siduri-y",
8
+ "url": "https://github.com/vxnus-studio/siduri-x",
9
9
  "directory": "packages/core"
10
10
  },
11
11
  "publishConfig": {