@siduri-x/api 1.0.0 → 1.0.1

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.
package/src/app.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import express, { Express } from 'express';
2
2
  import cors from 'cors';
3
- import { SiduriRuntime } from './runtime';
3
+ import { createCorsOptions } from './cors';
4
+ import { SiduriRuntime, dispatchCompanionChat } from './runtime';
4
5
  import { OpenAICompatibleBrain, OpenRouterBrain } from '@siduri-x/brain';
5
6
  import { PostgresMemoryOrgan } from '@siduri-x/memory';
6
- import { VoicevoxAdapter } from '@siduri-x/voice';
7
+ import { VoiceAdapter } from '@siduri-x/voice';
7
8
  import { EKnowledgeAdapter } from '@siduri-x/knowledge';
8
9
  import { OpenRouterVisionAdapter } from '@siduri-x/vision';
9
10
  import { ActiveSelfCompiler } from '@siduri-x/behavior';
@@ -22,7 +23,7 @@ export interface AppInstance {
22
23
 
23
24
  export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): AppInstance {
24
25
  const app: Express = express();
25
- app.use(cors());
26
+ app.use(cors(createCorsOptions()));
26
27
  app.use(express.json());
27
28
 
28
29
  let observationOrgan: FixtureObservationOrgan | undefined;
@@ -48,7 +49,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
48
49
  function createVoice(config: any) {
49
50
  return isDisabled(config)
50
51
  ? undefined
51
- : new VoicevoxAdapter({ baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
52
+ : new VoiceAdapter({ provider: config.provider || 'voicevox', baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
52
53
  }
53
54
 
54
55
  function createKnowledge(config: any) {
@@ -113,11 +114,19 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
113
114
 
114
115
  // STATUS / HEALTH ENDPOINTS
115
116
  app.get('/health', (req, res) => res.json({ status: "ok" }));
116
- app.get('/version', (req, res) => res.json({ name: "siduri-y-api", version: "0.2.0-y" }));
117
- app.get('/ready', (req, res) => res.json({ status: "ready", dependencies: {} }));
118
- app.get('/voice/health', (req, res) => res.json({ provider: "voicevox", healthy: true }));
119
- app.get('/obs/health', (req, res) => res.json({ connected: true }));
120
- app.get('/platforms/status', (req, res) => res.json({ platforms: {} }));
117
+ app.get('/version', (req, res) => res.json({ name: "siduri-x-api", version: "0.2.0-x" }));
118
+ app.get('/ready', (req, res) => {
119
+ const isReady = runtimes.size > 0;
120
+ res.status(isReady ? 200 : 503).json({
121
+ status: isReady ? "ready" : "not_ready",
122
+ companionCount: runtimes.size,
123
+ });
124
+ });
125
+ app.get('/voice/health', (req, res) => {
126
+ const hasVoice = Array.from(runtimes.values()).some((r) => Boolean(r.voice));
127
+ res.json({ provider: "siduri-voice", configured: hasVoice });
128
+ });
129
+ app.get('/obs/health', (req, res) => res.json({ connected: Boolean(observationOrgan) }));
121
130
  app.get('/me', attachIdentity, (req, res) => {
122
131
  const identity = (req as any).identity as Identity;
123
132
  res.json({
@@ -133,12 +142,20 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
133
142
  const { id, message, history } = req.body;
134
143
  const identity = (req as any).identity as Identity;
135
144
 
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
+
136
152
  // Call context mapper at the API boundary
137
153
  const mappingResult = mapRequestContext(
138
154
  {
139
155
  ...req.body,
140
156
  id: id || req.body.companionId,
141
- role: req.body.role || identity?.role,
157
+ role: effectiveRole,
158
+ authenticated: isOwner,
142
159
  generateCorrelationId: true,
143
160
  },
144
161
  {
@@ -159,15 +176,13 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
159
176
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
160
177
 
161
178
  try {
162
- // Map authorization role to legacy memory scope for backwards-compatible runtime call
163
- const legacyScope =
164
- mappingResult.context!.actor.authorizationRole === 'administrator'
165
- ? 'OWNER'
166
- : mappingResult.context!.actor.authorizationRole === 'operator'
167
- ? 'OPERATOR'
168
- : 'VIEWER';
169
-
170
- const response = await runtime.handleUserMessage(message, legacyScope, history);
179
+ const response = await dispatchCompanionChat(runtime, {
180
+ id: companionId,
181
+ companionId,
182
+ message,
183
+ context: mappingResult.context,
184
+ history,
185
+ });
171
186
  res.json(response);
172
187
  } catch (e: any) {
173
188
  res.status(500).json({ error: e.message });
@@ -228,7 +243,24 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
228
243
  });
229
244
 
230
245
  // MEMORY MUTATIONS - PROPOSALS
231
- app.post('/memory/proposals/update', requireRole(['OWNER', 'OPERATOR']), async (req, res) => res.json({ success: true }));
246
+ app.post('/memory/proposals/update', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
247
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
248
+ const runtime = runtimes.get(id);
249
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
250
+ if (!runtime.memory || typeof runtime.memory.updateClaim !== 'function') {
251
+ return res.status(400).json({ error: "Memory organ does not support updating claims" });
252
+ }
253
+ try {
254
+ const claimId = req.body.id || req.body.claimId;
255
+ if (!claimId) {
256
+ return res.status(400).json({ error: "Missing required claim id" });
257
+ }
258
+ const updated = await runtime.memory.updateClaim(claimId, req.body.updates || req.body);
259
+ res.json({ success: true, claim: updated });
260
+ } catch (e: any) {
261
+ res.status(500).json({ error: e.message });
262
+ }
263
+ });
232
264
 
233
265
  app.post('/memory/proposals/approve', requireRole(['OWNER', 'OPERATOR']), async (req, res) => {
234
266
  const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
@@ -309,151 +341,156 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
309
341
  }
310
342
  });
311
343
 
312
- app.post('/dev/memory/reset', requireRole(['OWNER']), async (req, res) => res.json({ reset: true }));
313
-
314
- // MOCKS / DEV / EVIDENCE / PLATFORMS
315
- app.get('/platforms/events', (req, res) => res.json({ events: [] }));
316
- app.get('/platforms/actions', (req, res) => res.json({ actions: [] }));
317
- app.get('/evidence', (req, res) => res.json({ results: [] }));
318
- app.get('/observations', (req, res) => res.json({ observations: observationOrgan?.current() ?? [] }));
344
+ const isDevMode = process.env.NODE_ENV !== 'production' || process.env.SIDURI_DEV_MODE === 'true';
345
+ if (isDevMode) {
346
+ app.post('/dev/memory/reset', requireRole(['OWNER']), async (req, res) => {
347
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
348
+ const runtime = runtimes.get(id);
349
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
350
+ if (!runtime.memory || typeof runtime.memory.resetMemory !== 'function') {
351
+ return res.status(400).json({ error: "Memory organ does not support reset" });
352
+ }
353
+ try {
354
+ await runtime.memory.resetMemory();
355
+ res.json({ reset: true });
356
+ } catch (e: any) {
357
+ res.status(500).json({ error: e.message });
358
+ }
359
+ });
319
360
 
320
- app.post('/dev/mock-response', async (req, res) => {
321
- const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
322
- const runtime = runtimes.get(companionId);
323
- if (!runtime) return res.status(404).json({ accepted: false, error: 'Companion not found' });
324
- const staged = runtime.gating.stageResponse({
325
- requestContext: {
326
- companionId,
327
- actor: {
328
- actorId: 'operator-a',
329
- sessionId: 'sess-op',
330
- authorizationRole: 'operator',
331
- capabilities: ['chat:public', 'memory:approve'],
332
- authenticated: true,
361
+ app.post('/dev/mock-response', async (req, res) => {
362
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
363
+ const runtime = runtimes.get(companionId);
364
+ if (!runtime) return res.status(404).json({ accepted: false, error: 'Companion not found' });
365
+ const staged = runtime.gating.stageResponse({
366
+ requestContext: {
367
+ companionId,
368
+ actor: {
369
+ actorId: 'operator-a',
370
+ sessionId: 'sess-op',
371
+ authorizationRole: 'operator',
372
+ capabilities: ['chat:public', 'memory:approve'],
373
+ authenticated: true,
374
+ },
375
+ conversation: {
376
+ channel: 'public',
377
+ audienceId: 'audience-public',
378
+ correlationId: req.body?.correlation_id || `corr-${Date.now()}`,
379
+ },
333
380
  },
334
- conversation: {
335
- channel: 'public',
336
- audienceId: 'audience-public',
337
- correlationId: req.body?.correlation_id || `corr-${Date.now()}`,
338
- },
339
- },
340
- candidateSpeech: req.body?.speech || 'Mocked staged response for review',
341
- candidateLanguage: req.body?.language || 'en',
342
- requiresApproval: req.body?.requiresApproval ?? true,
343
- });
344
- res.json({
345
- accepted: true,
346
- staged: true,
347
- status: staged.status,
348
- response_id: staged.responseId,
349
- correlation_id: staged.correlationId,
350
- speech: staged.speech,
381
+ candidateSpeech: req.body?.speech || 'Mocked staged response for review',
382
+ candidateLanguage: req.body?.language || 'en',
383
+ requiresApproval: req.body?.requiresApproval ?? true,
384
+ });
385
+ res.json({
386
+ accepted: true,
387
+ staged: true,
388
+ status: staged.status,
389
+ response_id: staged.responseId,
390
+ correlation_id: staged.correlationId,
391
+ speech: staged.speech,
392
+ });
351
393
  });
352
- });
353
-
354
- app.post('/dev/approve-response', async (req, res) => {
355
- const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
356
- const runtime = runtimes.get(companionId);
357
- if (!runtime) return res.status(404).json({ approved: false, error: 'Companion not found' });
358
-
359
- let responseId = req.body?.responseId;
360
- let correlationId = req.body?.correlation_id;
361
394
 
362
- if (!responseId && correlationId) {
363
- const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
364
- if (found) {
365
- responseId = found.responseId;
395
+ app.post('/dev/approve-response', async (req, res) => {
396
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
397
+ const runtime = runtimes.get(companionId);
398
+ if (!runtime) return res.status(404).json({ approved: false, error: 'Companion not found' });
399
+
400
+ let responseId = req.body?.responseId;
401
+ let correlationId = req.body?.correlation_id;
402
+
403
+ if (!responseId && correlationId) {
404
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
405
+ if (found) {
406
+ responseId = found.responseId;
407
+ }
408
+ } else if (responseId && !correlationId) {
409
+ const found = runtime.gating.getStagedPlan(responseId);
410
+ if (found) {
411
+ correlationId = found.correlationId;
412
+ }
366
413
  }
367
- } else if (responseId && !correlationId) {
368
- const found = runtime.gating.getStagedPlan(responseId);
369
- if (found) {
370
- correlationId = found.correlationId;
371
- }
372
- }
373
414
 
374
- if (!responseId) {
375
- return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
376
- }
415
+ if (!responseId) {
416
+ return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
417
+ }
377
418
 
378
- const result = runtime.gating.approveResponse({
379
- responseId,
380
- companionId,
381
- correlationId: correlationId || '',
382
- audienceId: req.body?.audienceId,
383
- });
419
+ const result = runtime.gating.approveResponse({
420
+ responseId,
421
+ companionId,
422
+ correlationId: correlationId || '',
423
+ audienceId: req.body?.audienceId,
424
+ });
384
425
 
385
- if (!result.success) {
386
- return res.status(400).json({ approved: false, error: result.reason });
387
- }
426
+ if (!result.success) {
427
+ return res.status(400).json({ approved: false, error: result.reason });
428
+ }
388
429
 
389
- // Now evaluated as approved
390
- const evaluation = runtime.gating.evaluateGate(result.plan!);
391
- res.json({
392
- approved: true,
393
- status: evaluation.disposition,
394
- response_id: result.plan!.responseId,
395
- speech: result.plan!.speech,
396
- language: result.plan!.language,
430
+ // Now evaluated as approved
431
+ const evaluation = runtime.gating.evaluateGate(result.plan!);
432
+ res.json({
433
+ approved: true,
434
+ status: evaluation.disposition,
435
+ response_id: result.plan!.responseId,
436
+ speech: result.plan!.speech,
437
+ language: result.plan!.language,
438
+ });
397
439
  });
398
- });
399
440
 
400
- app.post('/dev/reject-response', async (req, res) => {
401
- const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
402
- const runtime = runtimes.get(companionId);
403
- if (!runtime) return res.status(404).json({ rejected: false, error: 'Companion not found' });
404
-
405
- let responseId = req.body?.responseId;
406
- let correlationId = req.body?.correlation_id;
407
-
408
- if (!responseId && correlationId) {
409
- const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
410
- if (found) {
411
- responseId = found.responseId;
412
- }
413
- } else if (responseId && !correlationId) {
414
- const found = runtime.gating.getStagedPlan(responseId);
415
- if (found) {
416
- correlationId = found.correlationId;
441
+ app.post('/dev/reject-response', async (req, res) => {
442
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
443
+ const runtime = runtimes.get(companionId);
444
+ if (!runtime) return res.status(404).json({ rejected: false, error: 'Companion not found' });
445
+
446
+ let responseId = req.body?.responseId;
447
+ let correlationId = req.body?.correlation_id;
448
+
449
+ if (!responseId && correlationId) {
450
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
451
+ if (found) {
452
+ responseId = found.responseId;
453
+ }
454
+ } else if (responseId && !correlationId) {
455
+ const found = runtime.gating.getStagedPlan(responseId);
456
+ if (found) {
457
+ correlationId = found.correlationId;
458
+ }
417
459
  }
418
- }
419
460
 
420
- if (!responseId) {
421
- return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
422
- }
461
+ if (!responseId) {
462
+ return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
463
+ }
423
464
 
424
- const result = runtime.gating.rejectResponse({
425
- responseId,
426
- companionId,
427
- correlationId: correlationId || '',
428
- reason: req.body?.reason,
429
- });
465
+ const result = runtime.gating.rejectResponse({
466
+ responseId,
467
+ companionId,
468
+ correlationId: correlationId || '',
469
+ reason: req.body?.reason,
470
+ });
430
471
 
431
- if (!result.success) {
432
- return res.status(400).json({ rejected: false, error: result.reason });
433
- }
472
+ if (!result.success) {
473
+ return res.status(400).json({ rejected: false, error: result.reason });
474
+ }
434
475
 
435
- res.json({
436
- rejected: true,
437
- status: result.plan!.status,
438
- response_id: result.plan!.responseId,
476
+ res.json({
477
+ rejected: true,
478
+ status: result.plan!.status,
479
+ response_id: result.plan!.responseId,
480
+ });
439
481
  });
440
- });
441
-
442
- app.post('/dev/mock-observation', async (req, res) => {
443
- if (!observationOrgan) return res.status(503).json({ accepted: false, reason: 'observation_unavailable' });
444
- const result = await observationOrgan.ingest(
445
- new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]),
446
- 'fixture-observation',
447
- 'configured-vision',
448
- );
449
- if (!result.observation) return res.status(result.duplicate ? 200 : 409).json({ accepted: false, ...result });
450
- res.status(202).json({ accepted: true, observation: result.observation });
451
- });
452
482
 
453
- app.post('/platforms/actions/suggest', (req, res) => res.json({ suggested: true }));
454
- app.post('/platforms/actions/approve', (req, res) => res.json({ approved: true }));
455
- app.post('/platforms/actions/reject', (req, res) => res.json({ rejected: true }));
456
- app.post('/platforms/actions/send', (req, res) => res.json({ sent: true }));
483
+ 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(
486
+ new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]),
487
+ 'fixture-observation',
488
+ 'configured-vision',
489
+ );
490
+ if (!result.observation) return res.status(result.duplicate ? 200 : 409).json({ accepted: false, ...result });
491
+ res.status(202).json({ accepted: true, observation: result.observation });
492
+ });
493
+ }
457
494
 
458
495
  return { app, runtimes, setObservationOrgan: (org: FixtureObservationOrgan) => { observationOrgan = org; } };
459
496
  }
package/src/b0-b6.test.ts CHANGED
@@ -87,7 +87,7 @@ describe('T0 B0 & B6 Runtime Proof Suite', () => {
87
87
  expect(mockBrain.generatePlan).toHaveBeenCalledWith(
88
88
  expect.objectContaining({
89
89
  contextPrompt: '',
90
- recipient: 'VIEWER',
90
+ recipient: 'OWNER',
91
91
  })
92
92
  );
93
93
  });
package/src/cors.ts ADDED
@@ -0,0 +1,44 @@
1
+ import cors from 'cors';
2
+
3
+ export function getAllowedOrigins(): Set<string> {
4
+ const allowed = new Set<string>([
5
+ 'http://localhost:3000',
6
+ 'http://127.0.0.1:3000',
7
+ 'http://localhost:3001',
8
+ 'http://127.0.0.1:3001',
9
+ ]);
10
+
11
+ if (process.env.PORT) {
12
+ allowed.add(`http://localhost:${process.env.PORT}`);
13
+ allowed.add(`http://127.0.0.1:${process.env.PORT}`);
14
+ }
15
+
16
+ const envOrigins = process.env.ALLOWED_ORIGINS;
17
+ if (envOrigins) {
18
+ for (const origin of envOrigins.split(',')) {
19
+ const trimmed = origin.trim();
20
+ if (trimmed) {
21
+ allowed.add(trimmed);
22
+ }
23
+ }
24
+ }
25
+
26
+ return allowed;
27
+ }
28
+
29
+ export function createCorsOptions(): cors.CorsOptions {
30
+ return {
31
+ origin: (origin, callback) => {
32
+ // Allow non-browser requests with no origin header (e.g., native tools, curl)
33
+ if (!origin) {
34
+ return callback(null, true);
35
+ }
36
+ const allowedOrigins = getAllowedOrigins();
37
+ if (allowedOrigins.has(origin)) {
38
+ return callback(null, true);
39
+ }
40
+ return callback(null, false);
41
+ },
42
+ credentials: true,
43
+ };
44
+ }
package/src/index.test.ts CHANGED
@@ -31,7 +31,7 @@ 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
- 'VIEWER',
34
+ 'OWNER',
35
35
  []
36
36
  );
37
37
  });
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@ import { createApp, AppInstance } 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 { VoicevoxAdapter } from '@siduri-x/voice';
8
+ import { VoiceAdapter } from '@siduri-x/voice';
9
9
  import { EKnowledgeAdapter } from '@siduri-x/knowledge';
10
10
  import { OpenRouterVisionAdapter } from '@siduri-x/vision';
11
11
  import { ActiveSelfCompiler } from '@siduri-x/behavior';
@@ -41,7 +41,7 @@ function isDisabled(config: any): boolean {
41
41
  function createVoice(config: any) {
42
42
  return isDisabled(config)
43
43
  ? undefined
44
- : new VoicevoxAdapter({ baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
44
+ : new VoiceAdapter({ provider: config.provider || 'voicevox', baseUrl: process.env.VOICEVOX_URL || 'http://localhost:50021', speakerId: config.speakerId || 1 });
45
45
  }
46
46
 
47
47
  function createKnowledge(config: any) {
@@ -151,8 +151,8 @@ async function bootDefaultCompanion() {
151
151
 
152
152
  if (process.env.NODE_ENV !== 'test') {
153
153
  bootDefaultCompanion().then(() => {
154
- app.listen(PORT, () => {
155
- console.log(`Siduri-Y API running on port ${PORT}`);
154
+ app.listen(Number(PORT), '127.0.0.1', () => {
155
+ console.log(`Siduri-X API running on port ${PORT} (127.0.0.1)`);
156
156
  });
157
157
  }).catch(e => {
158
158
  console.error("Failed to boot default companion:", e);