@siduri-x/api 1.0.1 → 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.
Files changed (43) hide show
  1. package/dist/index.js +18 -9
  2. package/package.json +16 -12
  3. package/src/app.ts +294 -95
  4. package/src/auth.test.ts +56 -23
  5. package/src/auth.ts +74 -22
  6. package/src/context-mapper.test.ts +42 -147
  7. package/src/context-mapper.ts +44 -179
  8. package/src/index.test.ts +61 -32
  9. package/src/index.ts +30 -21
  10. package/src/runtime.test.ts +57 -5
  11. package/src/t5-experience.test.ts +0 -1
  12. package/src/t6-security.test.ts +0 -1
  13. package/dist/app.d.ts +0 -9
  14. package/dist/app.js +0 -480
  15. package/dist/auth.d.ts +0 -8
  16. package/dist/auth.js +0 -40
  17. package/dist/auth.test.d.ts +0 -1
  18. package/dist/auth.test.js +0 -48
  19. package/dist/b0-b6.test.d.ts +0 -1
  20. package/dist/b0-b6.test.js +0 -121
  21. package/dist/context-mapper.d.ts +0 -15
  22. package/dist/context-mapper.js +0 -287
  23. package/dist/context-mapper.test.d.ts +0 -1
  24. package/dist/context-mapper.test.js +0 -233
  25. package/dist/cors.d.ts +0 -3
  26. package/dist/cors.js +0 -42
  27. package/dist/index.d.ts +0 -6
  28. package/dist/index.test.d.ts +0 -1
  29. package/dist/index.test.js +0 -115
  30. package/dist/runtime.d.ts +0 -1
  31. package/dist/runtime.js +0 -17
  32. package/dist/runtime.test.d.ts +0 -1
  33. package/dist/runtime.test.js +0 -240
  34. package/dist/smoke.test.d.ts +0 -0
  35. package/dist/smoke.test.js +0 -6
  36. package/dist/t4-gating.test.d.ts +0 -1
  37. package/dist/t4-gating.test.js +0 -193
  38. package/dist/t5-experience.test.d.ts +0 -1
  39. package/dist/t5-experience.test.js +0 -156
  40. package/dist/t6-security.test.d.ts +0 -1
  41. package/dist/t6-security.test.js +0 -424
  42. package/dist/t7-release.test.d.ts +0 -1
  43. package/dist/t7-release.test.js +0 -119
@@ -1,17 +1,12 @@
1
1
  import {
2
2
  RequestContext,
3
- AuthorizationRole,
4
- Channel,
5
3
  DiagnosticCode,
6
4
  ContextError,
7
5
  validateRequestContext,
8
6
  } from '@siduri-x/core';
9
7
 
10
8
  export interface ContextMapperOptions {
11
- endpointPolicy?: 'public' | 'private' | 'operator' | 'direct';
12
- defaultPublicAudience?: string;
13
- defaultPrivateAudience?: string;
14
- defaultOperatorAudience?: string;
9
+ endpointPolicy?: 'public' | 'private' | 'operator' | 'direct' | string;
15
10
  allowAnonymousPublicChat?: boolean;
16
11
  }
17
12
 
@@ -22,13 +17,17 @@ export interface MapRequestContextResult {
22
17
  error?: ContextError;
23
18
  }
24
19
 
20
+ /**
21
+ * Maps incoming HTTP requests to a canonical RequestContext.
22
+ * In a single-owner, single-machine model:
23
+ * - Security is enforced at the external boundary, not internally between roles.
24
+ * - No internal audience or viewer/operator/owner role hierarchies.
25
+ */
25
26
  export function mapRequestContext(
26
27
  input: any,
27
28
  options: ContextMapperOptions = {}
28
29
  ): MapRequestContextResult {
29
30
  const diagnostics: DiagnosticCode[] = [];
30
- const endpointPolicy = options.endpointPolicy || 'public';
31
- const defaultPublicAudience = options.defaultPublicAudience || 'audience-public';
32
31
 
33
32
  if (!input || typeof input !== 'object') {
34
33
  return {
@@ -40,27 +39,7 @@ export function mapRequestContext(
40
39
  };
41
40
  }
42
41
 
43
- // Check for legacy MASTER_PRIVATE in any audience field or request
44
- const rawAudience =
45
- input?.context?.conversation?.audienceId ??
46
- input?.conversation?.audienceId ??
47
- input?.audienceId ??
48
- input?.audience;
49
-
50
- if (rawAudience === 'MASTER_PRIVATE' || input?.scope === 'MASTER_PRIVATE') {
51
- if (endpointPolicy === 'public' || input?.channel === 'public' || input?.context?.conversation?.channel === 'public') {
52
- return {
53
- accepted: false,
54
- error: {
55
- code: 'LEGACY_PERSONAL_AUDIENCE',
56
- field: 'audienceId',
57
- correlationId: input?.context?.conversation?.correlationId || input?.correlationId,
58
- },
59
- };
60
- }
61
- }
62
-
63
- // 1. If incoming input already has a full neutral context structure
42
+ // 1. If incoming input already has a context structure
64
43
  if (input.context && typeof input.context === 'object') {
65
44
  const rawCtx = input.context;
66
45
  const companionId = input.companionId || rawCtx.companionId || input.id;
@@ -77,48 +56,6 @@ export function mapRequestContext(
77
56
  };
78
57
  }
79
58
 
80
- // Role cannot select audience or subject
81
- const rawRole = input.role || rawCtx.actor?.authorizationRole;
82
- if (input.role && !rawCtx.conversation?.channel && !rawCtx.conversation?.audienceId) {
83
- return {
84
- accepted: false,
85
- error: {
86
- code: 'AMBIGUOUS_CONTEXT',
87
- conflicts: ['role_does_not_select_audience', 'role_does_not_select_subject'],
88
- correlationId,
89
- },
90
- };
91
- }
92
-
93
- const channel: Channel = rawCtx.conversation?.channel || (endpointPolicy as Channel);
94
- let audienceId: string | undefined = rawCtx.conversation?.audienceId;
95
-
96
- if (!audienceId) {
97
- if (channel === 'public' || endpointPolicy === 'public') {
98
- audienceId = defaultPublicAudience;
99
- diagnostics.push('audience_defaulted_by_public_policy');
100
- } else {
101
- return {
102
- accepted: false,
103
- error: {
104
- code: 'MISSING_CONTEXT',
105
- fields: ['conversation.audienceId'],
106
- correlationId,
107
- },
108
- };
109
- }
110
- }
111
-
112
- if (!correlationId) {
113
- return {
114
- accepted: false,
115
- error: {
116
- code: 'MISSING_CONTEXT',
117
- fields: ['conversation.correlationId'],
118
- },
119
- };
120
- }
121
-
122
59
  const actor = rawCtx.actor;
123
60
  if (!actor || typeof actor !== 'object') {
124
61
  return {
@@ -131,64 +68,37 @@ export function mapRequestContext(
131
68
  };
132
69
  }
133
70
 
134
- // Check required capabilities for private/operator/direct channels
135
- const capabilities: string[] = Array.isArray(actor.capabilities) ? actor.capabilities : [];
136
- if (channel === 'private' && !capabilities.includes('chat:private')) {
137
- return {
138
- accepted: false,
139
- error: {
140
- code: 'UNAUTHORIZED_CHANNEL_OR_CAPABILITY',
141
- message: 'Private channel requires explicit chat:private capability',
142
- fields: ['actor.capabilities'],
143
- correlationId,
144
- },
145
- };
146
- }
147
- if (channel === 'operator' && !capabilities.includes('memory:inspect') && !capabilities.includes('operator:access')) {
71
+ // Reject invalid primary_user subject
72
+ if (rawCtx.subject && (rawCtx.subject.subjectId === 'primary_user' || rawCtx.subject === 'primary_user')) {
148
73
  return {
149
74
  accepted: false,
150
75
  error: {
151
- code: 'UNAUTHORIZED_CHANNEL_OR_CAPABILITY',
152
- message: 'Operator channel requires explicit operator capability',
153
- fields: ['actor.capabilities'],
76
+ code: 'FORBIDDEN_CONTEXT',
77
+ message: 'Global primary_user subject is forbidden',
78
+ field: 'subject.subjectId',
154
79
  correlationId,
155
80
  },
156
81
  };
157
82
  }
158
83
 
159
- // Check subject policy
160
- let subject = rawCtx.subject;
161
- if (subject) {
162
- if (subject.subjectId === 'primary_user' || subject === 'primary_user') {
163
- // Global primary_user is rejected or quarantined
164
- return {
165
- accepted: false,
166
- error: {
167
- code: 'FORBIDDEN_CONTEXT',
168
- message: 'Global primary_user subject is forbidden',
169
- field: 'subject.subjectId',
170
- correlationId,
171
- },
172
- };
173
- }
174
- }
175
-
176
84
  const constructed: RequestContext = {
177
85
  companionId,
178
86
  actor: {
179
87
  actorId: actor.actorId,
180
88
  sessionId: actor.sessionId,
89
+ capabilities: Array.isArray(actor.capabilities) ? actor.capabilities : ['chat'],
90
+ authenticated: actor.authenticated !== undefined ? Boolean(actor.authenticated) : true,
181
91
  authorizationRole: actor.authorizationRole,
182
- capabilities,
183
- authenticated: Boolean(actor.authenticated),
92
+ ...actor,
184
93
  },
185
94
  conversation: {
186
- channel,
187
- audienceId,
188
- isLive: rawCtx.conversation?.isLive,
189
95
  correlationId,
96
+ channel: rawCtx.conversation?.channel || input.channel || 'direct',
97
+ isLive: rawCtx.conversation?.isLive,
98
+ ...rawCtx.conversation,
190
99
  },
191
- subject,
100
+ source: input.source || rawCtx.source || 'local',
101
+ subject: rawCtx.subject,
192
102
  };
193
103
 
194
104
  const validated = validateRequestContext(constructed);
@@ -203,9 +113,9 @@ export function mapRequestContext(
203
113
  };
204
114
  }
205
115
 
206
- // 2. Legacy compatibility envelope mapping
116
+ // 2. Synthesize clean RequestContext from request envelope
207
117
  const companionId = input.companionId || input.id;
208
- const correlationId = input.correlationId || input.conversation?.correlationId;
118
+ const correlationId = input.correlationId || input.conversation?.correlationId || (input.generateCorrelationId ? `corr-${Date.now()}` : undefined);
209
119
 
210
120
  if (!companionId) {
211
121
  return {
@@ -222,49 +132,16 @@ export function mapRequestContext(
222
132
  diagnostics.push('companion_default_mapped_for_bootstrap');
223
133
  }
224
134
 
225
- // Map legacy role to authorizationRole
226
- const legacyRole = input.role?.toString().toUpperCase();
227
- let authRole: AuthorizationRole = 'viewer';
228
- if (legacyRole === 'OWNER') {
229
- authRole = 'administrator';
230
- diagnostics.push('legacy_role_mapped_to_authorization');
231
- } else if (legacyRole === 'OPERATOR') {
232
- authRole = 'operator';
233
- diagnostics.push('legacy_role_mapped_to_authorization');
234
- } else if (legacyRole === 'VIEWER') {
235
- authRole = 'viewer';
236
- diagnostics.push('legacy_role_mapped_to_authorization');
237
- } else if (input.role) {
238
- return {
239
- accepted: false,
240
- error: {
241
- code: 'INVALID_CONTEXT',
242
- field: 'role',
243
- correlationId,
244
- },
245
- };
246
- }
247
-
248
- // Check endpoint policy vs legacy request
249
- if (endpointPolicy === 'private' || endpointPolicy === 'operator' || endpointPolicy === 'direct') {
250
- // Missing explicit channel, audience, or capability on private/operator endpoint is an error
251
- const fields: string[] = [];
252
- if (!input.channel) fields.push('conversation.channel');
253
- if (!input.audienceId) fields.push('conversation.audienceId');
254
- if (!input.capabilities && !input.actor?.capabilities) fields.push('actor.capabilities');
255
- if (!correlationId) fields.push('conversation.correlationId');
256
-
135
+ if (!correlationId) {
257
136
  return {
258
137
  accepted: false,
259
138
  error: {
260
139
  code: 'MISSING_CONTEXT',
261
- fields: fields.length > 0 ? fields : ['conversation.audienceId', 'actor.capabilities'],
262
- correlationId,
140
+ fields: ['conversation.correlationId'],
263
141
  },
264
142
  };
265
143
  }
266
144
 
267
- // Ambiguity check: if legacy input specifies role without correlationId or endpoint context for stateful op
268
145
  if (input.subject === 'primary_user' || input.subjectId === 'primary_user') {
269
146
  return {
270
147
  accepted: false,
@@ -277,55 +154,43 @@ export function mapRequestContext(
277
154
  };
278
155
  }
279
156
 
280
- // Missing correlationId for stateful requests
281
- const finalCorrelationId = correlationId || (input.generateCorrelationId ? `corr-${Date.now()}` : undefined);
282
- if (!finalCorrelationId) {
283
- return {
284
- accepted: false,
285
- error: {
286
- code: 'MISSING_CONTEXT',
287
- fields: ['conversation.correlationId'],
288
- },
289
- };
290
- }
291
-
292
- // Build anonymous public context
293
- const actorId = input.actorId || input.actor?.actorId || 'anonymous-session-a';
294
- const sessionId = input.sessionId || input.actor?.sessionId || 'session-a';
157
+ const actorId = input.actorId || input.actor?.actorId || 'local-user';
158
+ const sessionId = input.sessionId || input.actor?.sessionId || `session-${Date.now()}`;
295
159
  if (!input.actorId && !input.actor?.actorId) {
296
160
  diagnostics.push('anonymous_session_generated');
297
161
  }
298
162
 
299
- const channel: Channel = 'public';
300
- const audienceId = defaultPublicAudience;
301
- diagnostics.push('audience_defaulted_by_public_policy');
302
-
303
- const capabilities = authRole === 'administrator'
304
- ? ['chat:public', 'admin:access']
305
- : authRole === 'operator'
306
- ? ['chat:public', 'operator:access']
307
- : ['chat:public'];
163
+ const capabilities = Array.isArray(input.capabilities)
164
+ ? input.capabilities
165
+ : Array.isArray(input.actor?.capabilities)
166
+ ? input.actor.capabilities
167
+ : ['chat', 'system'];
308
168
 
309
169
  const mappedContext: RequestContext = {
310
170
  companionId,
311
171
  actor: {
312
172
  actorId,
313
173
  sessionId,
314
- authorizationRole: authRole,
315
174
  capabilities,
316
- authenticated: Boolean(input.authenticated),
175
+ authenticated: input.authenticated !== undefined ? Boolean(input.authenticated) : true,
176
+ authorizationRole: input.role ? (input.role.toLowerCase() === 'viewer' ? 'viewer' : 'administrator') : undefined,
317
177
  },
318
178
  conversation: {
319
- channel,
320
- audienceId,
321
- correlationId: finalCorrelationId,
179
+ channel: input.channel || input.conversation?.channel || 'direct',
180
+ correlationId,
322
181
  },
323
- subject: undefined, // Anonymous public chat has no subject
182
+ source: input.source || 'local',
183
+ subject: input.subject,
324
184
  };
325
185
 
186
+ const validated = validateRequestContext(mappedContext);
187
+ if (!validated.accepted) {
188
+ return validated;
189
+ }
190
+
326
191
  return {
327
192
  accepted: true,
328
- context: mappedContext,
193
+ context: validated.context,
329
194
  diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
330
195
  };
331
196
  }
package/src/index.test.ts CHANGED
@@ -31,7 +31,10 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
31
31
  expect(res.status).toBe(200);
32
32
  expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith(
33
33
  'Hello neutral world',
34
- 'OWNER',
34
+ expect.objectContaining({
35
+ companionId: 'companion-a',
36
+ conversation: expect.objectContaining({ channel: 'direct' }),
37
+ }),
35
38
  []
36
39
  );
37
40
  });
@@ -51,7 +54,6 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
51
54
  },
52
55
  conversation: {
53
56
  channel: 'public',
54
- audienceId: 'audience-public',
55
57
  correlationId: 'corr-route-1',
56
58
  },
57
59
  },
@@ -62,39 +64,14 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
62
64
  expect(res.status).toBe(200);
63
65
  expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith(
64
66
  'Hello structured context',
65
- 'VIEWER',
67
+ expect.objectContaining({
68
+ companionId: 'companion-a',
69
+ conversation: expect.objectContaining({ channel: 'public' }),
70
+ }),
66
71
  []
67
72
  );
68
73
  });
69
74
 
70
- test('rejects MASTER_PRIVATE in public request with 400 and structured error', async () => {
71
- const res = await request(app)
72
- .post('/chat')
73
- .send({
74
- companionId: 'companion-a',
75
- context: {
76
- actor: {
77
- actorId: 'actor-a',
78
- sessionId: 'session-a',
79
- authorizationRole: 'viewer',
80
- capabilities: ['chat:public'],
81
- authenticated: false,
82
- },
83
- conversation: {
84
- channel: 'public',
85
- audienceId: 'MASTER_PRIVATE',
86
- correlationId: 'corr-err-1',
87
- },
88
- },
89
- message: 'Forbidden audience test',
90
- });
91
-
92
- expect(res.status).toBe(400);
93
- expect(res.body.accepted).toBe(false);
94
- expect(res.body.error.code).toBe('LEGACY_PERSONAL_AUDIENCE');
95
- expect(fakeRuntime.handleUserMessage).not.toHaveBeenCalled();
96
- });
97
-
98
75
  test('rejects global primary_user subject with 400 and structured error', async () => {
99
76
  const res = await request(app)
100
77
  .post('/chat')
@@ -110,7 +87,6 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
110
87
  },
111
88
  conversation: {
112
89
  channel: 'public',
113
- audienceId: 'audience-public',
114
90
  correlationId: 'corr-err-2',
115
91
  },
116
92
  subject: {
@@ -126,4 +102,57 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
126
102
  expect(res.body.error.code).toBe('FORBIDDEN_CONTEXT');
127
103
  expect(fakeRuntime.handleUserMessage).not.toHaveBeenCalled();
128
104
  });
105
+
106
+ test('streams response chunks via POST /chat/stream', async () => {
107
+ fakeRuntime.mouth = {
108
+ stream: async function* () {
109
+ yield { utteranceId: 'utt-1', index: 1, deltaText: 'Hello', isComplete: false, medium: 'web' };
110
+ yield { utteranceId: 'utt-1', index: 2, deltaText: ' world', isComplete: false, medium: 'web' };
111
+ yield { utteranceId: 'utt-1', index: 3, deltaText: '', isComplete: true, medium: 'web' };
112
+ },
113
+ };
114
+
115
+ const res = await request(app)
116
+ .post('/chat/stream')
117
+ .send({
118
+ id: 'companion-a',
119
+ message: 'Stream me',
120
+ });
121
+
122
+ expect(res.status).toBe(200);
123
+ expect(res.headers['content-type']).toContain('text/event-stream');
124
+ expect(res.text).toContain('event: staged');
125
+ expect(res.text).toContain('event: chunk');
126
+ expect(res.text).toContain('event: done');
127
+ });
128
+
129
+ test('handles barge-in interruption via POST /chat/interrupt', async () => {
130
+ fakeRuntime.interruptMouth = jest.fn();
131
+
132
+ const res = await request(app)
133
+ .post('/chat/interrupt')
134
+ .send({
135
+ companionId: 'companion-a',
136
+ reason: 'user_stop',
137
+ });
138
+
139
+ expect(res.status).toBe(200);
140
+ expect(res.body.interrupted).toBe(true);
141
+ expect(fakeRuntime.interruptMouth).toHaveBeenCalledWith('user_stop');
142
+ });
143
+
144
+ test('handles mouth interruption via POST /mouth/interrupt', async () => {
145
+ fakeRuntime.interruptMouth = jest.fn();
146
+
147
+ const res = await request(app)
148
+ .post('/mouth/interrupt')
149
+ .send({
150
+ companionId: 'companion-a',
151
+ reason: 'user_barge_in',
152
+ });
153
+
154
+ expect(res.status).toBe(200);
155
+ expect(res.body.interrupted).toBe(true);
156
+ expect(fakeRuntime.interruptMouth).toHaveBeenCalledWith('user_barge_in');
157
+ });
129
158
  });
package/src/index.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { Express } from 'express';
4
- import { createApp, AppInstance } from './app';
4
+ import { createApp, AppInstance, AppBrainConfig, AppBehaviorConfig } from './app';
5
5
  import { SiduriRuntime } from './runtime';
6
6
  import { OpenAICompatibleBrain, OpenRouterBrain } from '@siduri-x/brain';
7
7
  import { PostgresMemoryOrgan } from '@siduri-x/memory';
8
- import { VoiceAdapter } from '@siduri-x/voice';
9
- import { EKnowledgeAdapter } from '@siduri-x/knowledge';
10
- import { OpenRouterVisionAdapter } from '@siduri-x/vision';
8
+ import { VoiceAdapter, VoiceConfig } from '@siduri-x/voice';
9
+ import { EKnowledgeAdapter, EKnowledgeConfig } from '@siduri-x/knowledge';
10
+ import { OpenRouterVisionAdapter, OpenRouterVisionConfig } from '@siduri-x/vision';
11
11
  import { ActiveSelfCompiler } from '@siduri-x/behavior';
12
- import { Live2DAdapter } from '@siduri-x/body';
12
+ import { Live2DAdapter, Live2DAdapterConfig } from '@siduri-x/body';
13
13
  import { FixtureObservationOrgan } from '@siduri-x/observation';
14
14
 
15
15
  export { createApp, AppInstance };
@@ -20,49 +20,58 @@ const instance: AppInstance = createApp(runtimes);
20
20
  export const app: Express = instance.app;
21
21
  export default app;
22
22
 
23
- function createBrain(config: any) {
24
- const provider = config.provider || 'openrouter';
23
+ function createBrain(config?: AppBrainConfig) {
24
+ const provider = config?.provider || 'openrouter';
25
25
  const defaultKeyEnv = provider === 'openai-compatible' ? 'OPENAI_COMPATIBLE_API_KEY' : 'OPENROUTER_API_KEY';
26
- const apiKey = config.apiKey || process.env[config.apiKeyEnv || defaultKeyEnv] || '';
26
+ const apiKey = config?.apiKey || process.env[config?.apiKeyEnv || defaultKeyEnv] || '';
27
27
  if (provider === 'openai-compatible') {
28
28
  return new OpenAICompatibleBrain({
29
29
  apiKey,
30
- model: config.model || 'local-model',
31
- baseUrl: config.baseUrl || 'http://127.0.0.1:1234/v1',
30
+ model: config?.model || 'local-model',
31
+ baseUrl: config?.baseUrl || 'http://127.0.0.1:1234/v1',
32
32
  });
33
33
  }
34
- return new OpenRouterBrain({ apiKey, model: config.model || 'gpt-4o-mini' });
34
+ return new OpenRouterBrain({ apiKey, model: config?.model || 'gpt-4o-mini' });
35
35
  }
36
36
 
37
- function isDisabled(config: any): boolean {
37
+ function isDisabled(config?: { provider?: string }): boolean {
38
38
  return !config || config.provider === 'none';
39
39
  }
40
40
 
41
- function createVoice(config: any) {
41
+ function createVoice(config?: VoiceConfig) {
42
42
  return isDisabled(config)
43
43
  ? undefined
44
- : new VoiceAdapter({ provider: config.provider || 'voicevox', baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
44
+ : new VoiceAdapter({
45
+ provider: (config?.provider as any) || 'voicevox',
46
+ baseUrl: config?.baseUrl || process.env.VOICEVOX_URL || 'http://localhost:50021',
47
+ speakerId: config?.speakerId || 1,
48
+ ...config,
49
+ });
45
50
  }
46
51
 
47
- function createKnowledge(config: any) {
52
+ function createKnowledge(config?: EKnowledgeConfig) {
48
53
  if (isDisabled(config)) return undefined;
49
- if (!config?.packPath && !config?.registryUrl && !config?.baseUrl && !config?.hubUrl) {
54
+ if (!config?.packPath && !config?.registryUrl && !config?.baseUrl) {
50
55
  return undefined;
51
56
  }
52
- return new EKnowledgeAdapter(config);
57
+ return new EKnowledgeAdapter(config || {});
53
58
  }
54
59
 
55
- function createVision(config: any) {
60
+ function createVision(config?: OpenRouterVisionConfig & { provider?: string }) {
56
61
  return isDisabled(config)
57
62
  ? undefined
58
- : new OpenRouterVisionAdapter({ apiKey: process.env.OPENROUTER_API_KEY || '', model: config.model || 'gpt-4-vision' });
63
+ : new OpenRouterVisionAdapter({
64
+ apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY || '',
65
+ model: config?.model || 'gpt-4-vision',
66
+ ...config,
67
+ });
59
68
  }
60
69
 
61
- function createBehavior(config: any) {
70
+ function createBehavior(config?: AppBehaviorConfig) {
62
71
  return isDisabled(config) ? undefined : new ActiveSelfCompiler();
63
72
  }
64
73
 
65
- function createBody(config: any) {
74
+ function createBody(config?: Live2DAdapterConfig & { provider?: string }) {
66
75
  return isDisabled(config)
67
76
  ? undefined
68
77
  : new Live2DAdapter(config);
@@ -1,6 +1,7 @@
1
- import { SiduriRuntime } from './runtime';
1
+ import { SiduriRuntime, dispatchCompanionChat } from './runtime';
2
2
  import { DefaultHandsOrgan } from '@siduri-x/hands';
3
3
  import { DefaultEarOrgan } from '@siduri-x/ear';
4
+ import { DefaultMouthOrgan } from '@siduri-x/mouth';
4
5
  import { ActionPolicyEngine, RequestContext } from '@siduri-x/core';
5
6
 
6
7
  describe('Siduri Runtime Orchestration', () => {
@@ -78,7 +79,7 @@ describe('Siduri Runtime Orchestration', () => {
78
79
  expect(noKnowledgeContext).toBe(true);
79
80
  expect(proposedClaims.length).toBe(1);
80
81
  expect(proposedClaims[0].subject).toBe("Test");
81
- expect(proposedClaims[0].scope).toBe("OWNER");
82
+ expect(proposedClaims[0].scope).toBe("user");
82
83
  expect(response.metadata.memory_proposals[0].proposal_id).toBe("claim-1");
83
84
  });
84
85
 
@@ -94,7 +95,7 @@ describe('Siduri Runtime Orchestration', () => {
94
95
  riskLevel: 'LOW',
95
96
  requiredCapabilities: ['chat:public'],
96
97
  },
97
- execute: async (params) => {
98
+ execute: async (params: any) => {
98
99
  toolExecuted = true;
99
100
  return { hits: [`Result for ${params.query}`] };
100
101
  },
@@ -146,7 +147,6 @@ describe('Siduri Runtime Orchestration', () => {
146
147
  },
147
148
  conversation: {
148
149
  channel: 'direct',
149
- audienceId: 'audience-direct',
150
150
  correlationId: 'corr-alice-123',
151
151
  },
152
152
  };
@@ -233,7 +233,6 @@ describe('Siduri Runtime Orchestration', () => {
233
233
  },
234
234
  conversation: {
235
235
  channel: 'public',
236
- audienceId: 'audience-public',
237
236
  correlationId: 'corr-bob-999',
238
237
  },
239
238
  };
@@ -281,4 +280,57 @@ describe('Siduri Runtime Orchestration', () => {
281
280
  /Ear text input exceeds maximum allowed length/
282
281
  );
283
282
  });
283
+
284
+ test('Decoupled Output Delivery: Brain decisions are delivered and formatted through MouthOrgan', async () => {
285
+ let deliveredOutput: any = null;
286
+ const mouth = new DefaultMouthOrgan({
287
+ channels: [
288
+ {
289
+ id: 'test-web-channel',
290
+ name: 'Web Channel',
291
+ medium: 'web',
292
+ deliver: async (out: any) => {
293
+ deliveredOutput = out;
294
+ },
295
+ },
296
+ ],
297
+ });
298
+
299
+ const mockBrain = {
300
+ generatePlan: async () => ({
301
+ speech: '**Hello** from Siduri!',
302
+ language: 'en',
303
+ }),
304
+ };
305
+
306
+ const mockMemory = {
307
+ initialize: async () => {},
308
+ searchClaims: async () => [],
309
+ getDirectives: async () => [],
310
+ };
311
+
312
+ const runtime = new SiduriRuntime(
313
+ 'companion-mouth-test',
314
+ { name: 'MouthCompanion' } as any,
315
+ {
316
+ brain: mockBrain as any,
317
+ memory: mockMemory as any,
318
+ mouth,
319
+ }
320
+ );
321
+ await runtime.initialize();
322
+
323
+ const response = await dispatchCompanionChat(runtime, {
324
+ message: 'Hi there',
325
+ role: 'OWNER',
326
+ medium: 'web',
327
+ });
328
+
329
+ expect(response.delivery).toBeDefined();
330
+ expect(response.delivery?.medium).toBe('web');
331
+ expect(response.delivery?.displayText).toBe('**Hello** from Siduri!');
332
+ expect(deliveredOutput).toBeDefined();
333
+ expect(deliveredOutput.medium).toBe('web');
334
+ expect(deliveredOutput.displayText).toBe('**Hello** from Siduri!');
335
+ });
284
336
  });
@@ -161,7 +161,6 @@ describe('T5 Experience Event and Output Adapters Suite', () => {
161
161
  responseId: 'resp-1',
162
162
  correlationId: 'corr-1',
163
163
  channel: 'public' as const,
164
- audienceId: 'audience-public',
165
164
  approval: 'STAGED' as any, // Not approved!
166
165
  kind: 'voice' as const,
167
166
  lifecycle: 'STARTED' as const,
@@ -168,7 +168,6 @@ describe('T6 Security & Operations Threat Model Suite', () => {
168
168
  responseId: 'resp-1',
169
169
  correlationId: 'corr-1',
170
170
  channel: 'public',
171
- audienceId: 'audience-public',
172
171
  approval: 'APPROVED',
173
172
  kind: 'voice',
174
173
  lifecycle: 'STARTED',
package/dist/app.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import { Express } from 'express';
2
- import { SiduriRuntime } from './runtime';
3
- import { FixtureObservationOrgan } from '@siduri-x/observation';
4
- export interface AppInstance {
5
- app: Express;
6
- runtimes: Map<string, SiduriRuntime>;
7
- setObservationOrgan: (org: FixtureObservationOrgan) => void;
8
- }
9
- export declare function createApp(runtimes?: Map<string, SiduriRuntime>): AppInstance;