@siduri-x/core 2.0.1 → 2.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.
Files changed (41) hide show
  1. package/dist/action-policy.d.ts +8 -1
  2. package/dist/action-policy.js +84 -24
  3. package/dist/action-policy.test.js +67 -1
  4. package/dist/action.d.ts +1 -1
  5. package/dist/capability.d.ts +6 -2
  6. package/dist/capability.js +5 -1
  7. package/dist/capability.test.js +1 -0
  8. package/dist/chat-contract.d.ts +3 -1
  9. package/dist/chat-contract.js +5 -2
  10. package/dist/container.d.ts +74 -0
  11. package/dist/container.js +81 -0
  12. package/dist/context.d.ts +3 -1
  13. package/dist/context.js +13 -0
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +2 -0
  16. package/dist/input-normalizer.js +3 -1
  17. package/dist/intent-classifier.d.ts +4 -2
  18. package/dist/intent-classifier.js +32 -1
  19. package/dist/intent-classifier.test.js +52 -0
  20. package/dist/memory-settler.d.ts +2 -1
  21. package/dist/memory-settler.js +9 -1
  22. package/dist/perception-cycle.test.js +134 -0
  23. package/dist/perception-pipeline.d.ts +76 -0
  24. package/dist/perception-pipeline.js +258 -0
  25. package/dist/perception-pipeline.test.d.ts +1 -0
  26. package/dist/perception-pipeline.test.js +65 -0
  27. package/dist/prompt-compiler.d.ts +2 -1
  28. package/dist/prompt-compiler.js +7 -1
  29. package/dist/proposals.d.ts +4 -1
  30. package/dist/response-envelope.d.ts +2 -1
  31. package/dist/response-envelope.js +2 -1
  32. package/dist/runtime-facades.test.js +27 -27
  33. package/dist/runtime.d.ts +36 -113
  34. package/dist/runtime.js +58 -403
  35. package/dist/siduri-db.d.ts +25 -9
  36. package/dist/siduri-db.js +114 -15
  37. package/dist/siduri-db.test.js +4 -3
  38. package/dist/sqlite-action-store.d.ts +1 -1
  39. package/dist/sqlite-action-store.js +37 -6
  40. package/dist/sqlite-action-store.test.js +9 -0
  41. package/package.json +1 -1
@@ -18,7 +18,7 @@ export interface ActionPolicyEngineOptions {
18
18
  allowedApproverRoles?: string[];
19
19
  requiredApproverCapabilities?: string[];
20
20
  }
21
- export type ActionApprovalDecisionCode = 'APPROVED' | 'REJECTED_UNAUTHORIZED' | 'REJECTED_UNAUTHENTICATED' | 'REJECTED_ROLE_MISMATCH' | 'REJECTED_MISSING_APPROVER_ID' | 'REJECTED_MISSING_CAPABILITY';
21
+ export type ActionApprovalDecisionCode = 'APPROVED' | 'REJECTED_UNAUTHORIZED' | 'REJECTED_UNAUTHENTICATED' | 'REJECTED_ROLE_MISMATCH' | 'REJECTED_MISSING_APPROVER_ID' | 'REJECTED_MISSING_CAPABILITY' | 'REJECTED_UNKNOWN_EXECUTION';
22
22
  export interface ActionApprovalResult {
23
23
  approved: boolean;
24
24
  decisionCode: ActionApprovalDecisionCode;
@@ -42,6 +42,13 @@ export interface ApproveActionOptions {
42
42
  * Optional capabilities held by the approver.
43
43
  */
44
44
  approverCapabilities?: string[];
45
+ /**
46
+ * Optional explicit action binding parameters when pre-authorizing a known action
47
+ */
48
+ toolName?: string;
49
+ parametersHash?: string;
50
+ companionId?: string;
51
+ actorId?: string;
45
52
  }
46
53
  export declare function normalizeApproverRole(role?: string, actorId?: string): string;
47
54
  export declare class ActionPolicyEngine {
@@ -10,18 +10,10 @@ function normalizeApproverRole(role, actorId) {
10
10
  return 'administrator';
11
11
  return lower;
12
12
  }
13
- if (actorId) {
14
- const lower = actorId.toLowerCase();
15
- if (lower.includes('admin'))
16
- return 'administrator';
17
- if (lower.includes('owner'))
18
- return 'owner';
19
- if (lower.includes('operator'))
20
- return 'operator';
21
- if (lower.includes('viewer') || lower.includes('guest') || lower.includes('visitor'))
22
- return 'viewer';
23
- }
24
- return 'owner'; // Default to single-owner role
13
+ // Principle of least privilege: Never derive privileged roles (administrator, owner, operator)
14
+ // from arbitrary substring matching on unverified actorId strings.
15
+ // When an explicit authorization role is not provided, default to 'viewer'.
16
+ return 'viewer';
25
17
  }
26
18
  class ActionPolicyEngine {
27
19
  toolRegistry = new Map();
@@ -96,7 +88,9 @@ class ActionPolicyEngine {
96
88
  }
97
89
  // Role check if tool restricts roles (supports administrator/owner role parity)
98
90
  if (toolDef.allowedRoles && toolDef.allowedRoles.length > 0) {
99
- const actorRole = effectiveContext.actor.authorizationRole || effectiveContext.actor.role || 'owner';
91
+ const actorRole = effectiveContext.actor.authorizationRole ||
92
+ effectiveContext.actor.role ||
93
+ (effectiveContext.actor.authenticated === false ? 'viewer' : 'owner');
100
94
  const normalizedActorRoles = new Set([actorRole.toLowerCase()]);
101
95
  if (actorRole.toLowerCase() === 'administrator' || actorRole.toLowerCase() === 'owner') {
102
96
  normalizedActorRoles.add('administrator');
@@ -158,6 +152,26 @@ class ActionPolicyEngine {
158
152
  this.approvedExecutions.set(executionId, approvalRecord);
159
153
  }
160
154
  }
155
+ // Cryptographic / structural binding verification:
156
+ // Ensure the approval record matches the exact tool, parameters, companion, and actor
157
+ if (approvalRecord) {
158
+ const toolMatch = !approvalRecord.toolName || approvalRecord.toolName === action.toolName;
159
+ const paramsMatch = !approvalRecord.parametersHash || approvalRecord.parametersHash === paramsHash;
160
+ const companionMatch = !approvalRecord.companionId || approvalRecord.companionId === effectiveContext.companionId;
161
+ const actorMatch = !approvalRecord.actorId || approvalRecord.actorId === effectiveContext.actor.actorId;
162
+ if (!toolMatch || !paramsMatch || !companionMatch || !actorMatch) {
163
+ const decision = {
164
+ allowed: false,
165
+ reason: `Approval record for execution "${executionId}" does not match the requested tool, parameters, companion, or actor`,
166
+ riskLevel,
167
+ requiredCapabilities: requiredCaps,
168
+ executionId,
169
+ decisionCode: 'REJECTED_APPROVAL_MISMATCH',
170
+ };
171
+ await this.recordAudit(action, effectiveContext, decision, 'REJECTED');
172
+ return { decision };
173
+ }
174
+ }
161
175
  let isApproved = Boolean(approvalRecord);
162
176
  if (!isApproved && typeof this.store.isActionApproved === 'function') {
163
177
  isApproved = await this.store.isActionApproved(executionId);
@@ -183,6 +197,27 @@ class ActionPolicyEngine {
183
197
  }
184
198
  if (requiresExplicitApproval && !isApproved) {
185
199
  this.pendingActions.set(executionId, { action, context: effectiveContext, toolDef });
200
+ if (typeof this.store.reserveExecution === 'function') {
201
+ const record = {
202
+ executionId,
203
+ actionId: action.actionId,
204
+ toolName: action.toolName,
205
+ providerId: toolDef.providerId || 'builtin',
206
+ parametersHash: paramsHash,
207
+ lifecycle: 'POLICY_CHECKED',
208
+ decision: {
209
+ allowed: false,
210
+ reason: `Action "${action.toolName}" has risk level ${riskLevel} and requires explicit approval`,
211
+ riskLevel,
212
+ requiredCapabilities: requiredCaps,
213
+ executionId,
214
+ decisionCode: 'REJECTED_HIGH_RISK_UNAPPROVED',
215
+ },
216
+ createdAt: new Date().toISOString(),
217
+ updatedAt: new Date().toISOString(),
218
+ };
219
+ await this.store.reserveExecution(record).catch(() => { });
220
+ }
186
221
  const decision = {
187
222
  allowed: false,
188
223
  reason: `Action "${action.toolName}" has risk level ${riskLevel} and requires explicit approval`,
@@ -271,21 +306,42 @@ class ActionPolicyEngine {
271
306
  await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason);
272
307
  return result;
273
308
  }
274
- // 3. Tool-specific authorization check if pending action is registered
309
+ // 3. Locate pending in-memory action or persisted execution record
275
310
  const pending = this.pendingActions.get(options.executionId);
276
- if (pending) {
277
- const { toolDef } = pending;
278
- if (toolDef.allowedRoles && toolDef.allowedRoles.length > 0 && !isOwnerOrAdmin) {
279
- const normalizedToolRoles = new Set(toolDef.allowedRoles.map((r) => r.toLowerCase()));
311
+ let executionRecord;
312
+ if (!pending && typeof this.store.getExecution === 'function') {
313
+ executionRecord = await this.store.getExecution(options.executionId);
314
+ }
315
+ if (!pending && !executionRecord && !options.toolName) {
316
+ const result = {
317
+ approved: false,
318
+ decisionCode: 'REJECTED_UNKNOWN_EXECUTION',
319
+ reason: `Cannot approve execution "${options.executionId}": execution is not pending or recognized`,
320
+ executionId: options.executionId,
321
+ approverActorId: actorId,
322
+ };
323
+ await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason);
324
+ return result;
325
+ }
326
+ const targetToolName = pending?.action.toolName || executionRecord?.toolName || options.toolName || '';
327
+ const targetToolDef = pending?.toolDef || (targetToolName ? this.findToolDefinition(targetToolName) : undefined);
328
+ const targetParamsHash = (pending ? (0, capability_1.computeParametersHash)(pending.action.parameters) : undefined) ||
329
+ executionRecord?.parametersHash ||
330
+ options.parametersHash;
331
+ const targetCompanionId = pending?.context?.companionId || pending?.action.context?.companionId || options.companionId;
332
+ const targetActorId = pending?.context?.actor.actorId || pending?.action.context?.actor.actorId || options.actorId;
333
+ if (targetToolDef) {
334
+ if (targetToolDef.allowedRoles && targetToolDef.allowedRoles.length > 0 && !isOwnerOrAdmin) {
335
+ const normalizedToolRoles = new Set(targetToolDef.allowedRoles.map((r) => r.toLowerCase()));
280
336
  if (!normalizedToolRoles.has(approverRole)) {
281
337
  const result = {
282
338
  approved: false,
283
339
  decisionCode: 'REJECTED_ROLE_MISMATCH',
284
- reason: `Approver role "${approverRole}" is not authorized to approve tool "${toolDef.name}" (requires: ${toolDef.allowedRoles.join(', ')})`,
340
+ reason: `Approver role "${approverRole}" is not authorized to approve tool "${targetToolDef.name}" (requires: ${targetToolDef.allowedRoles.join(', ')})`,
285
341
  executionId: options.executionId,
286
342
  approverActorId: actorId,
287
343
  };
288
- await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason, toolDef.name);
344
+ await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason, targetToolDef.name);
289
345
  return result;
290
346
  }
291
347
  }
@@ -299,25 +355,29 @@ class ActionPolicyEngine {
299
355
  executionId: options.executionId,
300
356
  approverActorId: actorId,
301
357
  };
302
- await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason, toolDef.name);
358
+ await this.recordApprovalAudit(options.executionId, actorId, approverRole, false, result.reason, targetToolDef.name);
303
359
  return result;
304
360
  }
305
361
  }
306
362
  }
307
- // 4. Record verified approval
363
+ // 4. Record verified approval with exact tool, parameter, companion, and actor binding
308
364
  const record = {
309
365
  executionId: options.executionId,
310
366
  approverActorId: actorId,
311
367
  reason: options.reason,
312
368
  approverRole,
313
369
  approvedAt: new Date().toISOString(),
370
+ toolName: targetToolName,
371
+ parametersHash: targetParamsHash,
372
+ companionId: targetCompanionId,
373
+ actorId: targetActorId,
314
374
  };
315
375
  this.approvedExecutions.set(options.executionId, record);
316
376
  if (typeof this.store.saveApproval === 'function') {
317
- await this.store.saveApproval(options.executionId, actorId, options.reason, approverRole);
377
+ await this.store.saveApproval(options.executionId, actorId, options.reason, approverRole, targetToolName, targetParamsHash, targetCompanionId, targetActorId);
318
378
  }
319
379
  const reason = options.reason || 'Action approved by authorized policy approver';
320
- await this.recordApprovalAudit(options.executionId, actorId, approverRole, true, reason, pending?.toolDef.name);
380
+ await this.recordApprovalAudit(options.executionId, actorId, approverRole, true, reason, targetToolName);
321
381
  return {
322
382
  approved: true,
323
383
  decisionCode: 'APPROVED',
@@ -125,9 +125,10 @@ describe('ActionPolicyEngine Boundary', () => {
125
125
  expect(res1.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
126
126
  expect(res1.capability).toBeUndefined();
127
127
  // Grant explicit approval
128
- engine.approveAction({
128
+ await engine.approveAction({
129
129
  executionId: 'exec-admin-delete-1',
130
130
  approverActorId: 'admin-super-user',
131
+ approverRole: 'administrator',
131
132
  });
132
133
  // Second attempt: approved
133
134
  const res2 = await engine.evaluateAction(action);
@@ -293,5 +294,70 @@ describe('ActionPolicyEngine Boundary', () => {
293
294
  expect(rejectionEvent?.actorId).toBe('viewer-tamper');
294
295
  expect(rejectionEvent?.decision?.decisionCode).toBe('REJECTED_UNAUTHORIZED');
295
296
  });
297
+ it('rejects approval attempt for arbitrary unknown execution ID', async () => {
298
+ const res = await engine.approveAction({
299
+ executionId: 'arbitrary-unregistered-id',
300
+ approverActorId: 'admin-bob',
301
+ approverRole: 'administrator',
302
+ });
303
+ expect(res.approved).toBe(false);
304
+ expect(res.decisionCode).toBe('REJECTED_UNKNOWN_EXECUTION');
305
+ });
306
+ it('rejects privilege escalation from actorId strings (attacker-admin is not an administrator without explicit role)', async () => {
307
+ await engine.evaluateAction(criticalAction);
308
+ const res = await engine.approveAction({
309
+ executionId: 'exec-sec-auth-1',
310
+ approverActorId: 'attacker-admin-user',
311
+ // Notice no approverRole is provided!
312
+ });
313
+ expect(res.approved).toBe(false);
314
+ expect(res.decisionCode).toBe('REJECTED_UNAUTHORIZED');
315
+ });
316
+ it('rejects action evaluation if parameters are tampered after approval (REJECTED_APPROVAL_MISMATCH)', async () => {
317
+ await engine.evaluateAction(criticalAction);
318
+ const approval = await engine.approveAction({
319
+ executionId: 'exec-sec-auth-1',
320
+ approverActorId: 'admin-bob',
321
+ approverRole: 'administrator',
322
+ });
323
+ expect(approval.approved).toBe(true);
324
+ // Attacker re-evaluates the same approved executionId but with injected dangerous parameters
325
+ const tamperedAction = {
326
+ ...criticalAction,
327
+ parameters: { injected: 'malicious-payload' },
328
+ };
329
+ const evalTampered = await engine.evaluateAction(tamperedAction);
330
+ expect(evalTampered.decision.allowed).toBe(false);
331
+ expect(evalTampered.decision.decisionCode).toBe('REJECTED_APPROVAL_MISMATCH');
332
+ expect(evalTampered.capability).toBeUndefined();
333
+ });
334
+ it('rejects action evaluation if tool name is swapped after approval (REJECTED_APPROVAL_MISMATCH)', async () => {
335
+ await engine.evaluateAction(criticalAction);
336
+ const approval = await engine.approveAction({
337
+ executionId: 'exec-sec-auth-1',
338
+ approverActorId: 'admin-bob',
339
+ approverRole: 'administrator',
340
+ });
341
+ expect(approval.approved).toBe(true);
342
+ // Register another tool
343
+ engine.registerToolDefinition({
344
+ name: 'admin/format_drive',
345
+ providerId: 'admin',
346
+ description: 'Format drive',
347
+ inputSchema: {},
348
+ riskLevel: 'CRITICAL',
349
+ allowedRoles: ['administrator'],
350
+ requiresApproval: true,
351
+ });
352
+ // Attacker re-uses the executionId on a different tool
353
+ const swappedAction = {
354
+ ...criticalAction,
355
+ toolName: 'admin/format_drive',
356
+ };
357
+ const evalSwapped = await engine.evaluateAction(swappedAction);
358
+ expect(evalSwapped.decision.allowed).toBe(false);
359
+ expect(evalSwapped.decision.decisionCode).toBe('REJECTED_APPROVAL_MISMATCH');
360
+ expect(evalSwapped.capability).toBeUndefined();
361
+ });
296
362
  });
297
363
  });
package/dist/action.d.ts CHANGED
@@ -15,7 +15,7 @@ export interface ActionPolicyDecision {
15
15
  riskLevel: ActionRiskLevel;
16
16
  requiredCapabilities?: string[];
17
17
  executionId: string;
18
- decisionCode: 'ALLOWED_AUTO' | 'ALLOWED_POLICY' | 'REJECTED_UNKNOWN_TOOL' | 'REJECTED_UNAUTHORIZED' | 'REJECTED_MISSING_CAPABILITY' | 'REJECTED_HIGH_RISK_UNAPPROVED' | 'REJECTED_CHANNEL_RESTRICTED' | 'REJECTED_COMPANION_MISMATCH' | 'REJECTED_POLICY';
18
+ decisionCode: 'ALLOWED_AUTO' | 'ALLOWED_POLICY' | 'REJECTED_UNKNOWN_TOOL' | 'REJECTED_UNAUTHORIZED' | 'REJECTED_MISSING_CAPABILITY' | 'REJECTED_HIGH_RISK_UNAPPROVED' | 'REJECTED_CHANNEL_RESTRICTED' | 'REJECTED_COMPANION_MISMATCH' | 'REJECTED_APPROVAL_MISMATCH' | 'REJECTED_POLICY';
19
19
  }
20
20
  export interface ActionAuditEvent {
21
21
  executionId: string;
@@ -43,12 +43,16 @@ export interface ActionApprovalRecord {
43
43
  reason?: string;
44
44
  approvedAt: string;
45
45
  approverRole?: string;
46
+ toolName?: string;
47
+ parametersHash?: string;
48
+ companionId?: string;
49
+ actorId?: string;
46
50
  }
47
51
  export interface ActionStore {
48
52
  reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
49
53
  updateExecution(record: PersistentExecutionRecord): Promise<void>;
50
54
  getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
51
- saveApproval(executionId: string, approverActorId: string, reason?: string, approverRole?: string): Promise<void>;
55
+ saveApproval(executionId: string, approverActorId: string, reason?: string, approverRole?: string, toolName?: string, parametersHash?: string, companionId?: string, actorId?: string): Promise<void>;
52
56
  isActionApproved(executionId: string): Promise<boolean>;
53
57
  getApproval?(executionId: string): Promise<ActionApprovalRecord | undefined>;
54
58
  appendAudit(event: ActionAuditEvent): Promise<void>;
@@ -62,7 +66,7 @@ export declare class InMemoryActionStore implements ActionStore {
62
66
  reserveExecution(record: PersistentExecutionRecord): Promise<boolean>;
63
67
  updateExecution(record: PersistentExecutionRecord): Promise<void>;
64
68
  getExecution(executionId: string): Promise<PersistentExecutionRecord | undefined>;
65
- saveApproval(executionId: string, approverActorId: string, reason?: string, approverRole?: string): Promise<void>;
69
+ saveApproval(executionId: string, approverActorId: string, reason?: string, approverRole?: string, toolName?: string, parametersHash?: string, companionId?: string, actorId?: string): Promise<void>;
66
70
  isActionApproved(executionId: string): Promise<boolean>;
67
71
  getApproval(executionId: string): Promise<ActionApprovalRecord | undefined>;
68
72
  appendAudit(event: ActionAuditEvent): Promise<void>;
@@ -53,13 +53,17 @@ class InMemoryActionStore {
53
53
  const rec = this.executions.get(executionId);
54
54
  return rec ? { ...rec } : undefined;
55
55
  }
56
- async saveApproval(executionId, approverActorId, reason, approverRole) {
56
+ async saveApproval(executionId, approverActorId, reason, approverRole, toolName, parametersHash, companionId, actorId) {
57
57
  this.approvals.set(executionId, {
58
58
  executionId,
59
59
  approverActorId,
60
60
  reason,
61
61
  approverRole,
62
62
  approvedAt: new Date().toISOString(),
63
+ toolName,
64
+ parametersHash,
65
+ companionId,
66
+ actorId,
63
67
  });
64
68
  }
65
69
  async isActionApproved(executionId) {
@@ -281,6 +281,7 @@ describe('AuthorizationCapability Cryptographic & Tamper Review', () => {
281
281
  await engine1.approveAction({
282
282
  executionId: 'exec-restart-1',
283
283
  approverActorId: 'operator-1',
284
+ approverRole: 'operator',
284
285
  reason: 'Scheduled maintenance',
285
286
  });
286
287
  // 3. Process restarts: new ActionPolicyEngine instance with empty in-memory set but shared durable store
@@ -78,4 +78,6 @@ export interface ChatResponse {
78
78
  * Canonical helper to dispatch a chat request to a SiduriRuntime instance.
79
79
  * Guarantees a 1:1 identical response structure for localweb (apps/api) and standalone CLI.
80
80
  */
81
- export declare function dispatchCompanionChat(runtime: SiduriRuntime, payload: ChatRequest): Promise<ChatResponse>;
81
+ export declare function dispatchCompanionChat(runtime: SiduriRuntime | {
82
+ runtime: SiduriRuntime;
83
+ }, payload: ChatRequest): Promise<ChatResponse>;
@@ -6,6 +6,9 @@ exports.dispatchCompanionChat = dispatchCompanionChat;
6
6
  * Guarantees a 1:1 identical response structure for localweb (apps/api) and standalone CLI.
7
7
  */
8
8
  async function dispatchCompanionChat(runtime, payload) {
9
+ const runner = 'runtime' in runtime && typeof runtime.handleUserMessage !== 'function'
10
+ ? runtime.runtime
11
+ : runtime;
9
12
  const userMessage = payload.message || payload.text || '';
10
13
  const history = Array.isArray(payload.history) ? payload.history : [];
11
14
  let roleOrContext;
@@ -19,8 +22,8 @@ async function dispatchCompanionChat(runtime, payload) {
19
22
  roleOrContext = 'OWNER';
20
23
  }
21
24
  const runtimeResult = (payload.medium || payload.signal)
22
- ? await runtime.handleUserMessage(userMessage, roleOrContext, history, payload.medium, payload.signal)
23
- : await runtime.handleUserMessage(userMessage, roleOrContext, history);
25
+ ? await runner.handleUserMessage(userMessage, roleOrContext, history, payload.medium, payload.signal)
26
+ : await runner.handleUserMessage(userMessage, roleOrContext, history);
24
27
  const delivery = runtimeResult?.delivery;
25
28
  // Normalize response plan
26
29
  const speech = delivery?.displayText ||
@@ -0,0 +1,74 @@
1
+ import { BrainOrgan, MemoryOrgan, VoiceOrgan, KnowledgeOrgan, VisionOrgan, BehaviorOrgan, BodyOrgan, HandsOrgan, EarOrgan, ObservationOrgan, MouthOrgan, SelfRepository, EKnowledgeOrgan, OrganConfig, ActionStore, ActionPolicyEngine, ResponseGatingEngine, ExperienceDispatcher, ExperienceAdapter } from './index';
2
+ import { SessionHistoryManager } from './session-history';
3
+ import { SiduriRuntime } from './runtime';
4
+ export interface SiduriRuntimeConfig {
5
+ name: string;
6
+ brain?: OrganConfig | Record<string, unknown>;
7
+ voice?: OrganConfig | Record<string, unknown>;
8
+ memory?: OrganConfig | Record<string, unknown>;
9
+ knowledge?: OrganConfig | Record<string, unknown>;
10
+ behavior?: OrganConfig | Record<string, unknown>;
11
+ body?: OrganConfig | Record<string, unknown>;
12
+ vision?: OrganConfig | Record<string, unknown>;
13
+ hands?: OrganConfig | Record<string, unknown>;
14
+ ear?: OrganConfig | Record<string, unknown>;
15
+ observation?: OrganConfig | Record<string, unknown>;
16
+ mouth?: OrganConfig | Record<string, unknown>;
17
+ self?: OrganConfig | Record<string, unknown>;
18
+ externalKnowledge?: OrganConfig | Record<string, unknown>;
19
+ actionPolicy?: Record<string, unknown>;
20
+ actionStore?: 'in-memory' | 'sqlite' | {
21
+ type: 'sqlite' | 'in-memory';
22
+ dbPath?: string;
23
+ };
24
+ actionStorePath?: string;
25
+ [key: string]: unknown;
26
+ }
27
+ export interface RuntimeOrgans {
28
+ brain?: BrainOrgan;
29
+ memory?: MemoryOrgan;
30
+ voice?: VoiceOrgan | ExperienceAdapter;
31
+ knowledge?: KnowledgeOrgan;
32
+ vision?: VisionOrgan;
33
+ behavior?: BehaviorOrgan;
34
+ body?: BodyOrgan | ExperienceAdapter;
35
+ hands?: HandsOrgan;
36
+ ear?: EarOrgan;
37
+ observation?: ObservationOrgan;
38
+ mouth?: MouthOrgan;
39
+ self?: SelfRepository;
40
+ externalKnowledge?: EKnowledgeOrgan;
41
+ actionStore?: ActionStore;
42
+ actionPolicy?: ActionPolicyEngine;
43
+ }
44
+ /**
45
+ * CompanionContainer manages the lifecycle, discovery, wiring, and dependency injection
46
+ * of organs and infrastructure for a companion instance.
47
+ */
48
+ export declare class CompanionContainer {
49
+ readonly id: string;
50
+ readonly config: SiduriRuntimeConfig;
51
+ readonly organs: RuntimeOrgans;
52
+ readonly gating: ResponseGatingEngine;
53
+ readonly actionPolicy: ActionPolicyEngine;
54
+ readonly dispatcher: ExperienceDispatcher;
55
+ readonly sessionHistory: SessionHistoryManager;
56
+ private _runtime?;
57
+ constructor(id: string, config: SiduriRuntimeConfig, organs?: RuntimeOrgans);
58
+ get brain(): BrainOrgan | undefined;
59
+ get memory(): MemoryOrgan | undefined;
60
+ get voice(): ExperienceAdapter | VoiceOrgan | undefined;
61
+ get knowledge(): KnowledgeOrgan | undefined;
62
+ get vision(): VisionOrgan | undefined;
63
+ get behavior(): BehaviorOrgan | undefined;
64
+ get body(): ExperienceAdapter | BodyOrgan | undefined;
65
+ get hands(): HandsOrgan | undefined;
66
+ get ear(): EarOrgan | undefined;
67
+ get observation(): ObservationOrgan | undefined;
68
+ set observation(org: ObservationOrgan | undefined);
69
+ get mouth(): MouthOrgan | undefined;
70
+ get self(): SelfRepository | undefined;
71
+ get externalKnowledge(): EKnowledgeOrgan | undefined;
72
+ initialize(): Promise<void>;
73
+ get runtime(): SiduriRuntime;
74
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CompanionContainer = void 0;
4
+ const index_1 = require("./index");
5
+ const session_history_1 = require("./session-history");
6
+ const runtime_1 = require("./runtime");
7
+ /**
8
+ * CompanionContainer manages the lifecycle, discovery, wiring, and dependency injection
9
+ * of organs and infrastructure for a companion instance.
10
+ */
11
+ class CompanionContainer {
12
+ id;
13
+ config;
14
+ organs;
15
+ gating;
16
+ actionPolicy;
17
+ dispatcher;
18
+ sessionHistory;
19
+ _runtime;
20
+ constructor(id, config, organs = {}) {
21
+ this.id = id;
22
+ this.config = config;
23
+ this.organs = organs;
24
+ this.gating = new index_1.ResponseGatingEngine();
25
+ let actionStore = organs.actionStore;
26
+ if (!actionStore) {
27
+ const storeOpt = config.actionStore;
28
+ const storePath = config.actionStorePath || (typeof storeOpt === 'object' ? storeOpt.dbPath : undefined);
29
+ if (storeOpt === 'sqlite' || (typeof storeOpt === 'object' && storeOpt.type === 'sqlite') || storePath) {
30
+ actionStore = new index_1.SqliteActionStore({ dbPath: storePath });
31
+ }
32
+ }
33
+ this.actionPolicy = organs.actionPolicy || new index_1.ActionPolicyEngine({
34
+ store: actionStore,
35
+ });
36
+ this.dispatcher = new index_1.ExperienceDispatcher();
37
+ if (this.organs.voice && typeof this.organs.voice.handleEvent === 'function') {
38
+ this.dispatcher.registerAdapter(this.organs.voice);
39
+ }
40
+ if (this.organs.body && typeof this.organs.body.handleEvent === 'function') {
41
+ this.dispatcher.registerAdapter(this.organs.body);
42
+ }
43
+ if (this.organs.mouth && typeof this.organs.mouth.handleEvent === 'function') {
44
+ this.dispatcher.registerAdapter(this.organs.mouth);
45
+ }
46
+ this.sessionHistory = new session_history_1.SessionHistoryManager();
47
+ }
48
+ // Direct organ accessors
49
+ get brain() { return this.organs.brain; }
50
+ get memory() { return this.organs.memory; }
51
+ get voice() { return this.organs.voice; }
52
+ get knowledge() { return this.organs.knowledge; }
53
+ get vision() { return this.organs.vision; }
54
+ get behavior() { return this.organs.behavior; }
55
+ get body() { return this.organs.body; }
56
+ get hands() { return this.organs.hands; }
57
+ get ear() { return this.organs.ear; }
58
+ get observation() { return this.organs.observation; }
59
+ set observation(org) { this.organs.observation = org; }
60
+ get mouth() { return this.organs.mouth; }
61
+ get self() { return this.organs.self; }
62
+ get externalKnowledge() { return this.organs.externalKnowledge; }
63
+ async initialize() {
64
+ if (this.organs.memory && typeof this.organs.memory.initialize === 'function') {
65
+ await this.organs.memory.initialize(this.id);
66
+ }
67
+ if (this.organs.hands && typeof this.organs.hands.listTools === 'function') {
68
+ const tools = await this.organs.hands.listTools();
69
+ for (const tool of tools) {
70
+ this.actionPolicy.registerToolDefinition(tool);
71
+ }
72
+ }
73
+ }
74
+ get runtime() {
75
+ if (!this._runtime) {
76
+ this._runtime = new runtime_1.SiduriRuntime(this.id, this.config, this);
77
+ }
78
+ return this._runtime;
79
+ }
80
+ }
81
+ exports.CompanionContainer = CompanionContainer;
package/dist/context.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface ConversationContext {
12
12
  [key: string]: unknown;
13
13
  }
14
14
  export type SubjectKind = 'actor' | 'companion' | 'configured';
15
+ export type InteractionMode = 'casual' | 'teach' | 'hybrid';
15
16
  export interface SubjectRef {
16
17
  subjectId: string;
17
18
  kind: SubjectKind;
@@ -23,9 +24,10 @@ export interface RequestContext {
23
24
  conversation: ConversationContext;
24
25
  source?: 'local' | 'external' | string;
25
26
  subject?: SubjectRef;
27
+ mode?: InteractionMode;
26
28
  metadata?: Record<string, unknown>;
27
29
  }
28
- export type DiagnosticCode = 'legacy_role_removed' | 'anonymous_session_generated' | 'companion_default_mapped_for_bootstrap' | 'actor_scoped_subject_mapped';
30
+ export type DiagnosticCode = 'legacy_role_removed' | 'anonymous_session_generated' | 'companion_default_mapped_for_bootstrap' | 'actor_scoped_subject_mapped' | 'role_escalation_attempt_suppressed' | 'capability_escalation_attempt_suppressed';
29
31
  export type ContextErrorCode = 'MISSING_CONTEXT' | 'INVALID_CONTEXT' | 'FORBIDDEN_CONTEXT' | 'AMBIGUOUS_CONTEXT' | 'UNAUTHORIZED_CAPABILITY';
30
32
  export interface ContextError {
31
33
  code: ContextErrorCode;
package/dist/context.js CHANGED
@@ -55,6 +55,19 @@ function validateRequestContext(context) {
55
55
  }
56
56
  }
57
57
  }
58
+ if (ctx.mode !== undefined) {
59
+ if (ctx.mode !== 'casual' && ctx.mode !== 'teach' && ctx.mode !== 'hybrid') {
60
+ return {
61
+ accepted: false,
62
+ error: {
63
+ code: 'INVALID_CONTEXT',
64
+ message: `Invalid interaction mode: '${ctx.mode}' (expected 'casual', 'teach', or 'hybrid')`,
65
+ field: 'mode',
66
+ correlationId: ctx.conversation?.correlationId,
67
+ },
68
+ };
69
+ }
70
+ }
58
71
  if (missingFields.length > 0) {
59
72
  return {
60
73
  accepted: false,
package/dist/index.d.ts CHANGED
@@ -25,6 +25,8 @@ export * from './session-history';
25
25
  export * from './schema-validator';
26
26
  export * from './siduri-db';
27
27
  export * from './database';
28
+ export * from './container';
29
+ export * from './perception-pipeline';
28
30
  import { EvidenceRecord } from './evidence';
29
31
  import { ActionIntent } from './action';
30
32
  import { RequestContext } from './context';
@@ -253,19 +255,25 @@ export interface HealthProbeResult {
253
255
  }
254
256
  export type HealthProbeFn = (context: HealthProbeContext) => Promise<HealthProbeResult> | HealthProbeResult;
255
257
  export * from './mouth-types';
256
- import type { SelfIdentity, PersonalityTraits, SelfDirective, SelfRelationship, LifeInventoryItem, LifeScheduleItem, LifePreference, MemoryClaim, EpisodicEvent } from './siduri-db';
258
+ import type { SelfIdentity, PersonalityTraits, SelfDirective, SelfRelationship, SelfDialogueExample, LifeInventoryItem, LifeScheduleItem, LifePreference, MemoryClaim, EpisodicEvent } from './siduri-db';
259
+ export type { SelfIdentity, PersonalityTraits, SelfDirective, SelfRelationship, SelfDialogueExample, LifeInventoryItem, LifeScheduleItem, LifePreference, MemoryClaim, EpisodicEvent, };
257
260
  export interface SelfRepository {
258
261
  getIdentity(companionId: string): Promise<SelfIdentity | undefined>;
259
- getPersonality(companionId: string): Promise<PersonalityTraits>;
262
+ getPersonality?(companionId: string): Promise<PersonalityTraits>;
260
263
  getActiveDirectives(companionId: string): Promise<SelfDirective[]>;
261
264
  getRelationship(companionId: string, entityId: string): Promise<SelfRelationship | null>;
265
+ getRelationships?(companionId: string): Promise<SelfRelationship[]>;
266
+ getExemplars?(companionId: string): Promise<SelfDialogueExample[]>;
267
+ setExemplars?(companionId: string, exemplars: SelfDialogueExample[]): Promise<void>;
262
268
  commitDirectives(companionId: string, directives: SelfDirective[]): Promise<void>;
263
269
  updateRelationship(companionId: string, rel: SelfRelationship): Promise<void>;
264
270
  disableDirective?(id: string): Promise<void>;
265
271
  getActiveSelf?(companionId: string): Promise<{
266
272
  identity?: SelfIdentity;
267
- personality: PersonalityTraits;
273
+ personality?: PersonalityTraits;
268
274
  directives: SelfDirective[];
275
+ relationships?: SelfRelationship[];
276
+ exemplars?: SelfDialogueExample[];
269
277
  }>;
270
278
  }
271
279
  export interface LifeDatabase {
package/dist/index.js CHANGED
@@ -42,5 +42,7 @@ __exportStar(require("./session-history"), exports);
42
42
  __exportStar(require("./schema-validator"), exports);
43
43
  __exportStar(require("./siduri-db"), exports);
44
44
  __exportStar(require("./database"), exports);
45
+ __exportStar(require("./container"), exports);
46
+ __exportStar(require("./perception-pipeline"), exports);
45
47
  // Mouth (Communication & Output Delivery)
46
48
  __exportStar(require("./mouth-types"), exports);
@@ -16,7 +16,9 @@ async function normalizeUserInput(message, roleOrContext = 'OWNER', history = []
16
16
  }
17
17
  const isContextObject = typeof roleOrContext === 'object' && roleOrContext !== null;
18
18
  const role = isContextObject
19
- ? (roleOrContext.actor?.authorizationRole === 'viewer' ? 'VIEWER' : 'OWNER')
19
+ ? (roleOrContext.actor?.authorizationRole === 'viewer' || roleOrContext.actor?.authenticated === false
20
+ ? 'VIEWER'
21
+ : 'OWNER')
20
22
  : roleOrContext;
21
23
  const requestContext = isContextObject
22
24
  ? roleOrContext