@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
package/src/app.ts CHANGED
@@ -4,17 +4,48 @@ import { createCorsOptions } from './cors';
4
4
  import { SiduriRuntime, dispatchCompanionChat } from './runtime';
5
5
  import { OpenAICompatibleBrain, OpenRouterBrain } from '@siduri-x/brain';
6
6
  import { PostgresMemoryOrgan } from '@siduri-x/memory';
7
- import { VoiceAdapter } from '@siduri-x/voice';
8
- import { EKnowledgeAdapter } from '@siduri-x/knowledge';
9
- import { OpenRouterVisionAdapter } from '@siduri-x/vision';
7
+ import { VoiceAdapter, VoiceConfig } from '@siduri-x/voice';
8
+ import { EKnowledgeAdapter, EKnowledgeConfig } from '@siduri-x/knowledge';
9
+ import { OpenRouterVisionAdapter, OpenRouterVisionConfig } from '@siduri-x/vision';
10
10
  import { ActiveSelfCompiler } from '@siduri-x/behavior';
11
- import { Live2DAdapter } from '@siduri-x/body';
11
+ import { Live2DAdapter, Live2DAdapterConfig } from '@siduri-x/body';
12
12
  import { FixtureObservationOrgan } from '@siduri-x/observation';
13
- import { DefaultHandsOrgan } from '@siduri-x/hands';
14
- import { DefaultEarOrgan } from '@siduri-x/ear';
15
- import { attachIdentity, requireRole, Identity } from './auth';
13
+ import { DefaultHandsOrgan, DefaultHandsOrganConfig } from '@siduri-x/hands';
14
+ import { DefaultEarOrgan, EarOrganConfig } from '@siduri-x/ear';
15
+ import { DefaultMouthOrgan, DefaultMouthOrganConfig } from '@siduri-x/mouth';
16
+ import { attachIdentity, requireAuth, Identity } from './auth';
16
17
  import { mapRequestContext } from './context-mapper';
17
18
 
19
+ export interface AppBrainConfig {
20
+ provider?: 'openrouter' | 'openai-compatible' | string;
21
+ model?: string;
22
+ apiKey?: string;
23
+ apiKeyEnv?: string;
24
+ baseUrl?: string;
25
+ timeoutMs?: number;
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ export interface AppBehaviorConfig {
30
+ provider?: 'active_self' | 'none' | string;
31
+ preset?: string;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ export interface AppBootCompanionConfig {
36
+ name: string;
37
+ brain?: AppBrainConfig;
38
+ voice?: VoiceConfig;
39
+ knowledge?: EKnowledgeConfig;
40
+ vision?: OpenRouterVisionConfig;
41
+ behavior?: AppBehaviorConfig;
42
+ body?: Live2DAdapterConfig;
43
+ hands?: DefaultHandsOrganConfig;
44
+ ear?: EarOrganConfig;
45
+ mouth?: DefaultMouthOrganConfig;
46
+ [key: string]: unknown;
47
+ }
48
+
18
49
  export interface AppInstance {
19
50
  app: Express;
20
51
  runtimes: Map<string, SiduriRuntime>;
@@ -28,63 +59,78 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
28
59
 
29
60
  let observationOrgan: FixtureObservationOrgan | undefined;
30
61
 
31
- function createBrain(config: any) {
32
- const provider = config.provider || 'openrouter';
62
+ function createBrain(config?: AppBrainConfig) {
63
+ const provider = config?.provider || 'openrouter';
33
64
  const defaultKeyEnv = provider === 'openai-compatible' ? 'OPENAI_COMPATIBLE_API_KEY' : 'OPENROUTER_API_KEY';
34
- const apiKey = config.apiKey || process.env[config.apiKeyEnv || defaultKeyEnv] || '';
65
+ const apiKey = config?.apiKey || process.env[config?.apiKeyEnv || defaultKeyEnv] || '';
35
66
  if (provider === 'openai-compatible') {
36
67
  return new OpenAICompatibleBrain({
37
68
  apiKey,
38
- model: config.model || 'local-model',
39
- baseUrl: config.baseUrl || 'http://127.0.0.1:1234/v1',
69
+ model: config?.model || 'local-model',
70
+ baseUrl: config?.baseUrl || 'http://127.0.0.1:1234/v1',
40
71
  });
41
72
  }
42
- return new OpenRouterBrain({ apiKey, model: config.model || 'gpt-4o-mini' });
73
+ return new OpenRouterBrain({ apiKey, model: config?.model || 'gpt-4o-mini' });
43
74
  }
44
75
 
45
- function isDisabled(config: any): boolean {
76
+ function isDisabled(config?: { provider?: string }): boolean {
46
77
  return !config || config.provider === 'none';
47
78
  }
48
79
 
49
- function createVoice(config: any) {
80
+ function createVoice(config?: VoiceConfig) {
50
81
  return isDisabled(config)
51
82
  ? undefined
52
- : new VoiceAdapter({ provider: config.provider || 'voicevox', baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
83
+ : new VoiceAdapter({
84
+ provider: (config?.provider as any) || 'voicevox',
85
+ baseUrl: config?.baseUrl || process.env.VOICEVOX_URL || 'http://localhost:50021',
86
+ speakerId: config?.speakerId || 1,
87
+ ...config,
88
+ });
53
89
  }
54
90
 
55
- function createKnowledge(config: any) {
56
- return isDisabled(config) ? undefined : new EKnowledgeAdapter(config);
91
+ function createKnowledge(config?: EKnowledgeConfig) {
92
+ return isDisabled(config) ? undefined : new EKnowledgeAdapter(config || {});
57
93
  }
58
94
 
59
- function createVision(config: any) {
95
+ function createVision(config?: OpenRouterVisionConfig & { provider?: string }) {
60
96
  return isDisabled(config)
61
97
  ? undefined
62
- : new OpenRouterVisionAdapter({ apiKey: process.env.OPENROUTER_API_KEY || '', model: config.model || 'gpt-4-vision' });
98
+ : new OpenRouterVisionAdapter({
99
+ apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY || '',
100
+ model: config?.model || 'gpt-4-vision',
101
+ ...config,
102
+ });
63
103
  }
64
104
 
65
- function createBehavior(config: any) {
105
+ function createBehavior(config?: AppBehaviorConfig) {
66
106
  return isDisabled(config) ? undefined : new ActiveSelfCompiler();
67
107
  }
68
108
 
69
- function createBody(config: any) {
109
+ function createBody(config?: Live2DAdapterConfig & { provider?: string }) {
70
110
  return isDisabled(config)
71
111
  ? undefined
72
112
  : new Live2DAdapter(config);
73
113
  }
74
114
 
75
- function createHands(config: any) {
115
+ function createHands(config?: DefaultHandsOrganConfig & { provider?: string }) {
76
116
  return isDisabled(config)
77
117
  ? new DefaultHandsOrgan()
78
118
  : new DefaultHandsOrgan(config);
79
119
  }
80
120
 
81
- function createEar(config: any) {
121
+ function createEar(config?: EarOrganConfig & { provider?: string }) {
82
122
  return isDisabled(config)
83
123
  ? new DefaultEarOrgan()
84
124
  : new DefaultEarOrgan(config);
85
125
  }
86
126
 
87
- app.post('/boot', requireRole(['OWNER']), async (req, res) => {
127
+ function createMouth(config?: DefaultMouthOrganConfig & { provider?: string }, voice?: any) {
128
+ return isDisabled(config)
129
+ ? new DefaultMouthOrgan({ voice })
130
+ : new DefaultMouthOrgan({ ...config, voice });
131
+ }
132
+
133
+ app.post('/boot', requireAuth, async (req, res) => {
88
134
  try {
89
135
  const { id, config } = req.body;
90
136
  if (runtimes.has(id)) {
@@ -100,8 +146,21 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
100
146
  const body = createBody(config.body);
101
147
  const hands = createHands(config.hands);
102
148
  const ear = createEar(config.ear);
103
-
104
- const runtime = new SiduriRuntime(id, config, { brain, memory, voice, knowledge, vision, behavior, body, hands, ear });
149
+ const mouth = createMouth(config.mouth, voice);
150
+
151
+ const runtime = new SiduriRuntime(id, config, {
152
+ brain,
153
+ memory,
154
+ voice,
155
+ knowledge,
156
+ vision,
157
+ behavior,
158
+ body,
159
+ hands,
160
+ ear,
161
+ mouth,
162
+ observation: observationOrgan,
163
+ });
105
164
  await runtime.initialize();
106
165
 
107
166
  runtimes.set(id, runtime);
@@ -126,43 +185,45 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
126
185
  const hasVoice = Array.from(runtimes.values()).some((r) => Boolean(r.voice));
127
186
  res.json({ provider: "siduri-voice", configured: hasVoice });
128
187
  });
129
- app.get('/obs/health', (req, res) => res.json({ connected: Boolean(observationOrgan) }));
188
+ app.get('/obs/health', (req, res) => {
189
+ const connected = Boolean(observationOrgan) || Array.from(runtimes.values()).some((r) => Boolean(r.observation));
190
+ res.json({ connected });
191
+ });
192
+ app.get('/mouth/health', (req, res) => {
193
+ const hasMouth = Array.from(runtimes.values()).some((r) => Boolean(r.mouth));
194
+ res.json({ provider: "siduri-mouth", configured: hasMouth });
195
+ });
196
+ app.get('/mouth/channels', (req, res) => {
197
+ const id = (req.query.id as string) || Array.from(runtimes.keys())[0];
198
+ const runtime = runtimes.get(id);
199
+ if (!runtime || !runtime.mouth || typeof runtime.mouth.getRegisteredChannels !== 'function') {
200
+ return res.json({ channels: [] });
201
+ }
202
+ res.json({ channels: runtime.mouth.getRegisteredChannels() });
203
+ });
130
204
  app.get('/me', attachIdentity, (req, res) => {
131
205
  const identity = (req as any).identity as Identity;
132
206
  res.json({
133
- actorId: identity.role === 'OWNER' ? 'owner-user' : 'anonymous-session',
134
- role: identity.role,
135
- authenticated: identity.role === 'OWNER',
207
+ actorId: identity.actorId || (identity.authenticated ? 'owner-user' : 'anonymous-session'),
208
+ role: identity.role || (identity.authenticated ? 'OWNER' : 'VIEWER'),
209
+ authenticated: Boolean(identity.authenticated),
136
210
  });
137
211
  });
138
- app.put('/me', requireRole(['OWNER']), (req, res) => res.json({ success: true }));
212
+ app.put('/me', requireAuth, (req, res) => res.json({ success: true }));
139
213
 
140
214
  // CHAT (API context boundary validation)
141
215
  app.post('/chat', attachIdentity, async (req, res) => {
142
216
  const { id, message, history } = req.body;
143
217
  const identity = (req as any).identity as Identity;
144
218
 
145
- // Single-owner companion model: /chat defaults to OWNER identity.
146
- // Explicit non-owner role in request body or context (e.g. adversarial test suites) is respected.
147
- const effectiveRole = req.body?.role === 'VIEWER' || req.body?.context?.actor?.authorizationRole === 'viewer'
148
- ? 'VIEWER'
149
- : (identity?.role === 'OPERATOR' ? 'OPERATOR' : 'OWNER');
150
- const isOwner = effectiveRole === 'OWNER';
151
-
152
- // Call context mapper at the API boundary
153
- const mappingResult = mapRequestContext(
154
- {
155
- ...req.body,
156
- id: id || req.body.companionId,
157
- role: effectiveRole,
158
- authenticated: isOwner,
159
- generateCorrelationId: true,
160
- },
161
- {
162
- endpointPolicy: 'public',
163
- defaultPublicAudience: 'audience-public',
164
- }
165
- );
219
+ // Single-owner companion model: map request context directly
220
+ const mappingResult = mapRequestContext({
221
+ ...req.body,
222
+ id: id || req.body.companionId,
223
+ authenticated: identity?.authenticated ?? true,
224
+ source: identity?.source ?? 'local',
225
+ generateCorrelationId: true,
226
+ });
166
227
 
167
228
  if (!mappingResult.accepted) {
168
229
  return res.status(400).json({
@@ -182,6 +243,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
182
243
  message,
183
244
  context: mappingResult.context,
184
245
  history,
246
+ ...(req.body?.medium ? { medium: req.body.medium } : {}),
185
247
  });
186
248
  res.json(response);
187
249
  } catch (e: any) {
@@ -189,53 +251,183 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
189
251
  }
190
252
  });
191
253
 
254
+ // REAL-TIME SSE STREAMING (Mouth transport)
255
+ app.post('/chat/stream', attachIdentity, async (req, res) => {
256
+ const { id, message, history } = req.body;
257
+ const identity = (req as any).identity as Identity;
258
+
259
+ const mappingResult = mapRequestContext({
260
+ ...req.body,
261
+ id: id || req.body.companionId,
262
+ authenticated: identity?.authenticated ?? true,
263
+ source: identity?.source ?? 'local',
264
+ generateCorrelationId: true,
265
+ });
266
+
267
+ if (!mappingResult.accepted) {
268
+ return res.status(400).json({
269
+ accepted: false,
270
+ error: mappingResult.error,
271
+ });
272
+ }
273
+
274
+ const companionId = mappingResult.context!.companionId;
275
+ const runtime = runtimes.get(companionId);
276
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
277
+
278
+ res.setHeader('Content-Type', 'text/event-stream');
279
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
280
+ res.setHeader('Connection', 'keep-alive');
281
+ res.flushHeaders?.();
282
+
283
+ const abortController = new AbortController();
284
+ const onClose = () => {
285
+ abortController.abort('client_disconnect');
286
+ if (typeof runtime.interruptMouth === 'function') {
287
+ runtime.interruptMouth('client_disconnect');
288
+ }
289
+ };
290
+ req.on('close', onClose);
291
+
292
+ try {
293
+ const response = await dispatchCompanionChat(runtime, {
294
+ id: companionId,
295
+ companionId,
296
+ message,
297
+ context: mappingResult.context,
298
+ history,
299
+ medium: 'web',
300
+ signal: abortController.signal,
301
+ });
302
+
303
+ res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status })}\n\n`);
304
+
305
+ const avatarEvent = response.metadata?.events?.find(
306
+ (e: any) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval === 'APPROVED' || !e.approval)
307
+ );
308
+ if (avatarEvent) {
309
+ res.write(`event: avatar\ndata: ${JSON.stringify(avatarEvent)}\n\n`);
310
+ }
311
+
312
+ const speechText = response.delivery?.text || response.response?.subtitle_en || response.response?.spoken_ja || '';
313
+ const utterance = {
314
+ utteranceId: response.response_id || 'utt-stream',
315
+ companionId,
316
+ responseId: response.response_id,
317
+ correlationId: response.correlation_id,
318
+ text: speechText,
319
+ medium: 'web' as const,
320
+ expression: avatarEvent?.expression,
321
+ action: avatarEvent?.action,
322
+ signal: abortController.signal,
323
+ };
324
+
325
+ if (runtime.mouth && typeof runtime.mouth.stream === 'function') {
326
+ for await (const chunk of runtime.mouth.stream(utterance)) {
327
+ if (abortController.signal.aborted) {
328
+ res.write(`event: chunk\ndata: ${JSON.stringify({ ...chunk, interrupted: true })}\n\n`);
329
+ break;
330
+ }
331
+ res.write(`event: chunk\ndata: ${JSON.stringify(chunk)}\n\n`);
332
+ }
333
+ } else {
334
+ res.write(`event: chunk\ndata: ${JSON.stringify({ utteranceId: utterance.utteranceId, index: 1, deltaText: speechText, isComplete: true, medium: 'web' })}\n\n`);
335
+ }
336
+
337
+ res.write(`event: done\ndata: ${JSON.stringify(response)}\n\n`);
338
+ res.end();
339
+ } catch (e: any) {
340
+ if (abortController.signal.aborted) {
341
+ res.write(`event: interrupted\ndata: ${JSON.stringify({ reason: abortController.signal.reason })}\n\n`);
342
+ } else {
343
+ res.write(`event: error\ndata: ${JSON.stringify({ error: e.message })}\n\n`);
344
+ }
345
+ res.end();
346
+ } finally {
347
+ req.removeListener('close', onClose);
348
+ }
349
+ });
350
+
351
+ // INTERRUPTION / BARGE-IN
352
+ app.post('/chat/interrupt', attachIdentity, (req, res) => {
353
+ const companionId = req.body?.companionId || req.body?.id || Array.from(runtimes.keys())[0];
354
+ const runtime = companionId ? runtimes.get(companionId) : undefined;
355
+ const reason = req.body?.reason || 'user_barge_in';
356
+
357
+ if (runtime) {
358
+ runtime.interruptMouth(reason);
359
+ return res.json({ success: true, interrupted: true, companionId, reason });
360
+ }
361
+
362
+ for (const r of runtimes.values()) {
363
+ r.interruptMouth(reason);
364
+ }
365
+ return res.json({ success: true, interrupted: true, reason });
366
+ });
367
+
368
+ app.post('/mouth/interrupt', attachIdentity, (req, res) => {
369
+ const companionId = req.body?.companionId || req.body?.id || Array.from(runtimes.keys())[0];
370
+ const runtime = companionId ? runtimes.get(companionId) : undefined;
371
+ const reason = req.body?.reason || 'user_barge_in';
372
+
373
+ if (runtime) {
374
+ runtime.interruptMouth(reason);
375
+ return res.json({ success: true, interrupted: true, companionId, reason });
376
+ }
377
+
378
+ for (const r of runtimes.values()) {
379
+ r.interruptMouth(reason);
380
+ }
381
+ return res.json({ success: true, interrupted: true, reason });
382
+ });
383
+
192
384
  // MEMORY GETTERS
193
- app.get('/memory/proposals', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
385
+ app.get('/memory/proposals', requireAuth, async (req, res) => {
194
386
  const id = req.query.id as string || Array.from(runtimes.keys())[0];
195
387
  const runtime = runtimes.get(id);
196
388
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
197
389
  if (!runtime.memory) return res.json({ proposals: [] });
198
390
  try {
199
- const proposals = await runtime.memory.getPendingClaims();
391
+ const proposals = await runtime.getPendingClaims();
200
392
  res.json({ proposals });
201
393
  } catch (e: any) {
202
394
  res.status(500).json({ error: e.message });
203
395
  }
204
396
  });
205
397
 
206
- app.get('/memory', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
398
+ app.get('/memory', requireAuth, async (req, res) => {
207
399
  const id = req.query.id as string || Array.from(runtimes.keys())[0];
208
400
  const runtime = runtimes.get(id);
209
401
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
210
402
  if (!runtime.memory) return res.json({ items: [] });
211
403
  try {
212
- const items = await runtime.memory.getClaims();
404
+ const items = await runtime.getClaims();
213
405
  res.json({ items });
214
406
  } catch (e: any) {
215
407
  res.status(500).json({ error: e.message });
216
408
  }
217
409
  });
218
410
 
219
- app.get('/memory/claims', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
411
+ app.get('/memory/claims', requireAuth, async (req, res) => {
220
412
  const id = req.query.id as string || Array.from(runtimes.keys())[0];
221
413
  const runtime = runtimes.get(id);
222
414
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
223
415
  if (!runtime.memory) return res.json({ claims: [] });
224
416
  try {
225
- const claims = await runtime.memory.getClaims();
417
+ const claims = await runtime.getClaims();
226
418
  res.json({ claims });
227
419
  } catch (e: any) {
228
420
  res.status(500).json({ error: e.message });
229
421
  }
230
422
  });
231
423
 
232
- app.get('/memory/behavioral', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
424
+ app.get('/memory/behavioral', requireAuth, async (req, res) => {
233
425
  const id = req.query.id as string || Array.from(runtimes.keys())[0];
234
426
  const runtime = runtimes.get(id);
235
427
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
236
428
  if (!runtime.memory) return res.json({ directives: [] });
237
429
  try {
238
- const directives = await runtime.memory.getDirectives();
430
+ const directives = await runtime.getDirectives();
239
431
  res.json({ directives });
240
432
  } catch (e: any) {
241
433
  res.status(500).json({ error: e.message });
@@ -243,7 +435,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
243
435
  });
244
436
 
245
437
  // MEMORY MUTATIONS - PROPOSALS
246
- app.post('/memory/proposals/update', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
438
+ app.post('/memory/proposals/update', requireAuth, async (req, res) => {
247
439
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
248
440
  const runtime = runtimes.get(id);
249
441
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
@@ -255,33 +447,33 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
255
447
  if (!claimId) {
256
448
  return res.status(400).json({ error: "Missing required claim id" });
257
449
  }
258
- const updated = await runtime.memory.updateClaim(claimId, req.body.updates || req.body);
450
+ const updated = await runtime.updateClaim(claimId, req.body.updates || req.body);
259
451
  res.json({ success: true, claim: updated });
260
452
  } catch (e: any) {
261
453
  res.status(500).json({ error: e.message });
262
454
  }
263
455
  });
264
456
 
265
- app.post('/memory/proposals/approve', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
457
+ app.post('/memory/proposals/approve', requireAuth, async (req, res) => {
266
458
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
267
459
  const runtime = runtimes.get(id);
268
460
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
269
461
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
270
462
  try {
271
- await runtime.memory.approveClaim(req.body.id);
463
+ await runtime.approveClaim(req.body.id);
272
464
  res.json({ approved: true });
273
465
  } catch (e: any) {
274
466
  res.status(500).json({ error: e.message });
275
467
  }
276
468
  });
277
469
 
278
- app.post('/memory/proposals/reject', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
470
+ app.post('/memory/proposals/reject', requireAuth, async (req, res) => {
279
471
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
280
472
  const runtime = runtimes.get(id);
281
473
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
282
474
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
283
475
  try {
284
- await runtime.memory.rejectClaim(req.body.id);
476
+ await runtime.rejectClaim(req.body.id);
285
477
  res.json({ rejected: true });
286
478
  } catch (e: any) {
287
479
  res.status(500).json({ error: e.message });
@@ -289,52 +481,52 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
289
481
  });
290
482
 
291
483
  // MEMORY MUTATIONS - BEHAVIORAL
292
- app.post('/memory/behavioral/approve', requireRole(['OWNER']), async (req, res) => {
484
+ app.post('/memory/behavioral/approve', requireAuth, async (req, res) => {
293
485
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
294
486
  const runtime = runtimes.get(id);
295
487
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
296
488
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
297
489
  try {
298
- await runtime.memory.approveDirective(req.body.id);
490
+ await runtime.approveDirective(req.body.id);
299
491
  res.json({ approved: true });
300
492
  } catch (e: any) {
301
493
  res.status(500).json({ error: e.message });
302
494
  }
303
495
  });
304
496
 
305
- app.post('/memory/behavioral/reject', requireRole(['OWNER']), async (req, res) => {
497
+ app.post('/memory/behavioral/reject', requireAuth, async (req, res) => {
306
498
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
307
499
  const runtime = runtimes.get(id);
308
500
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
309
501
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
310
502
  try {
311
- await runtime.memory.rejectDirective(req.body.id);
503
+ await runtime.rejectDirective(req.body.id);
312
504
  res.json({ rejected: true });
313
505
  } catch (e: any) {
314
506
  res.status(500).json({ error: e.message });
315
507
  }
316
508
  });
317
509
 
318
- app.post('/memory/behavioral/revoke', requireRole(['OWNER']), async (req, res) => {
510
+ app.post('/memory/behavioral/revoke', requireAuth, async (req, res) => {
319
511
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
320
512
  const runtime = runtimes.get(id);
321
513
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
322
514
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
323
515
  try {
324
- await runtime.memory.revokeDirective(req.body.id);
516
+ await runtime.revokeDirective(req.body.id);
325
517
  res.json({ revoked: true });
326
518
  } catch (e: any) {
327
519
  res.status(500).json({ error: e.message });
328
520
  }
329
521
  });
330
522
 
331
- app.post('/memory/behavioral/disable', requireRole(['OWNER']), async (req, res) => {
523
+ app.post('/memory/behavioral/disable', requireAuth, async (req, res) => {
332
524
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
333
525
  const runtime = runtimes.get(id);
334
526
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
335
527
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
336
528
  try {
337
- await runtime.memory.disableDirective(req.body.id);
529
+ await runtime.disableDirective(req.body.id);
338
530
  res.json({ disabled: true });
339
531
  } catch (e: any) {
340
532
  res.status(500).json({ error: e.message });
@@ -343,7 +535,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
343
535
 
344
536
  const isDevMode = process.env.NODE_ENV !== 'production' || process.env.SIDURI_DEV_MODE === 'true';
345
537
  if (isDevMode) {
346
- app.post('/dev/memory/reset', requireRole(['OWNER']), async (req, res) => {
538
+ app.post('/dev/memory/reset', requireAuth, async (req, res) => {
347
539
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
348
540
  const runtime = runtimes.get(id);
349
541
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
@@ -351,7 +543,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
351
543
  return res.status(400).json({ error: "Memory organ does not support reset" });
352
544
  }
353
545
  try {
354
- await runtime.memory.resetMemory();
546
+ await runtime.resetMemory();
355
547
  res.json({ reset: true });
356
548
  } catch (e: any) {
357
549
  res.status(500).json({ error: e.message });
@@ -362,19 +554,16 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
362
554
  const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
363
555
  const runtime = runtimes.get(companionId);
364
556
  if (!runtime) return res.status(404).json({ accepted: false, error: 'Companion not found' });
365
- const staged = runtime.gating.stageResponse({
557
+ const staged = runtime.stageResponse({
366
558
  requestContext: {
367
559
  companionId,
368
560
  actor: {
369
- actorId: 'operator-a',
370
- sessionId: 'sess-op',
371
- authorizationRole: 'operator',
372
- capabilities: ['chat:public', 'memory:approve'],
561
+ actorId: 'local-user',
562
+ sessionId: 'sess-local',
563
+ capabilities: ['chat', 'memory:approve'],
373
564
  authenticated: true,
374
565
  },
375
566
  conversation: {
376
- channel: 'public',
377
- audienceId: 'audience-public',
378
567
  correlationId: req.body?.correlation_id || `corr-${Date.now()}`,
379
568
  },
380
569
  },
@@ -401,12 +590,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
401
590
  let correlationId = req.body?.correlation_id;
402
591
 
403
592
  if (!responseId && correlationId) {
404
- const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
593
+ const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
405
594
  if (found) {
406
595
  responseId = found.responseId;
407
596
  }
408
597
  } else if (responseId && !correlationId) {
409
- const found = runtime.gating.getStagedPlan(responseId);
598
+ const found = runtime.getStagedPlan(responseId);
410
599
  if (found) {
411
600
  correlationId = found.correlationId;
412
601
  }
@@ -416,11 +605,10 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
416
605
  return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
417
606
  }
418
607
 
419
- const result = runtime.gating.approveResponse({
608
+ const result = runtime.approveResponse({
420
609
  responseId,
421
610
  companionId,
422
611
  correlationId: correlationId || '',
423
- audienceId: req.body?.audienceId,
424
612
  });
425
613
 
426
614
  if (!result.success) {
@@ -428,7 +616,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
428
616
  }
429
617
 
430
618
  // Now evaluated as approved
431
- const evaluation = runtime.gating.evaluateGate(result.plan!);
619
+ const evaluation = runtime.evaluateGate(result.plan!);
432
620
  res.json({
433
621
  approved: true,
434
622
  status: evaluation.disposition,
@@ -447,12 +635,12 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
447
635
  let correlationId = req.body?.correlation_id;
448
636
 
449
637
  if (!responseId && correlationId) {
450
- const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
638
+ const found = runtime.findStagedPlanByCorrelation(companionId, correlationId);
451
639
  if (found) {
452
640
  responseId = found.responseId;
453
641
  }
454
642
  } else if (responseId && !correlationId) {
455
- const found = runtime.gating.getStagedPlan(responseId);
643
+ const found = runtime.getStagedPlan(responseId);
456
644
  if (found) {
457
645
  correlationId = found.correlationId;
458
646
  }
@@ -462,7 +650,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
462
650
  return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
463
651
  }
464
652
 
465
- const result = runtime.gating.rejectResponse({
653
+ const result = runtime.rejectResponse({
466
654
  responseId,
467
655
  companionId,
468
656
  correlationId: correlationId || '',
@@ -481,8 +669,10 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
481
669
  });
482
670
 
483
671
  app.post('/dev/mock-observation', async (req, res) => {
484
- if (!observationOrgan) return res.status(503).json({ accepted: false, reason: 'observation_unavailable' });
485
- const result = await observationOrgan.ingest(
672
+ const activeRuntime = Array.from(runtimes.values())[0];
673
+ const targetObs = activeRuntime?.observation || observationOrgan;
674
+ if (!targetObs) return res.status(503).json({ accepted: false, reason: 'observation_unavailable' });
675
+ const result = await targetObs.ingest(
486
676
  new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]),
487
677
  'fixture-observation',
488
678
  'configured-vision',
@@ -492,5 +682,14 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
492
682
  });
493
683
  }
494
684
 
495
- return { app, runtimes, setObservationOrgan: (org: FixtureObservationOrgan) => { observationOrgan = org; } };
685
+ return {
686
+ app,
687
+ runtimes,
688
+ setObservationOrgan: (org: FixtureObservationOrgan) => {
689
+ observationOrgan = org;
690
+ for (const r of runtimes.values()) {
691
+ r.observation = org;
692
+ }
693
+ },
694
+ };
496
695
  }