@sidurijs/api 1.0.0

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 (66) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +78 -0
  3. package/dist/app.d.ts +11 -0
  4. package/dist/app.js +1190 -0
  5. package/dist/auth.d.ts +34 -0
  6. package/dist/auth.js +84 -0
  7. package/dist/auth.test.d.ts +1 -0
  8. package/dist/auth.test.js +74 -0
  9. package/dist/b0-b6.test.d.ts +1 -0
  10. package/dist/b0-b6.test.js +121 -0
  11. package/dist/boot.d.ts +91 -0
  12. package/dist/boot.js +154 -0
  13. package/dist/boot.test.d.ts +1 -0
  14. package/dist/boot.test.js +91 -0
  15. package/dist/context-mapper.d.ts +20 -0
  16. package/dist/context-mapper.js +233 -0
  17. package/dist/context-mapper.test.d.ts +1 -0
  18. package/dist/context-mapper.test.js +287 -0
  19. package/dist/cors.d.ts +3 -0
  20. package/dist/cors.js +42 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.js +116 -0
  23. package/dist/index.test.d.ts +1 -0
  24. package/dist/index.test.js +151 -0
  25. package/dist/knowledge.test.d.ts +1 -0
  26. package/dist/knowledge.test.js +236 -0
  27. package/dist/runtime.d.ts +1 -0
  28. package/dist/runtime.js +17 -0
  29. package/dist/runtime.test.d.ts +1 -0
  30. package/dist/runtime.test.js +282 -0
  31. package/dist/smoke.test.d.ts +0 -0
  32. package/dist/smoke.test.js +6 -0
  33. package/dist/t4-gating.test.d.ts +1 -0
  34. package/dist/t4-gating.test.js +193 -0
  35. package/dist/t5-experience.test.d.ts +1 -0
  36. package/dist/t5-experience.test.js +155 -0
  37. package/dist/t6-security.test.d.ts +1 -0
  38. package/dist/t6-security.test.js +535 -0
  39. package/dist/t7-release.test.d.ts +1 -0
  40. package/dist/t7-release.test.js +119 -0
  41. package/dist/teach-mode.test.d.ts +1 -0
  42. package/dist/teach-mode.test.js +277 -0
  43. package/jest.config.json +5 -0
  44. package/package.json +44 -0
  45. package/siduri.sqlite +0 -0
  46. package/src/app.ts +1209 -0
  47. package/src/auth.test.ts +90 -0
  48. package/src/auth.ts +101 -0
  49. package/src/b0-b6.test.ts +137 -0
  50. package/src/boot.test.ts +112 -0
  51. package/src/boot.ts +197 -0
  52. package/src/context-mapper.test.ts +319 -0
  53. package/src/context-mapper.ts +279 -0
  54. package/src/cors.ts +44 -0
  55. package/src/index.test.ts +178 -0
  56. package/src/index.ts +101 -0
  57. package/src/knowledge.test.ts +267 -0
  58. package/src/runtime.test.ts +336 -0
  59. package/src/runtime.ts +1 -0
  60. package/src/smoke.test.ts +5 -0
  61. package/src/t4-gating.test.ts +219 -0
  62. package/src/t5-experience.test.ts +174 -0
  63. package/src/t6-security.test.ts +589 -0
  64. package/src/t7-release.test.ts +131 -0
  65. package/src/teach-mode.test.ts +312 -0
  66. package/tsconfig.json +16 -0
package/src/app.ts ADDED
@@ -0,0 +1,1209 @@
1
+ import express, { Express } from 'express';
2
+ import cors from 'cors';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { createCorsOptions } from './cors';
6
+ import { SiduriRuntime, dispatchCompanionChat } from './runtime';
7
+ import { SelfPackageParser, SqliteSelfRepository, scanDirective, compilePersonaDocument } from '@sidurijs/self';
8
+ import { FixtureObservationOrgan } from '@sidurijs/observation';
9
+ import { attachIdentity, requireAuth, Identity, isLocalRequest } from './auth';
10
+ import { mapRequestContext } from './context-mapper';
11
+ import {
12
+ bootCompanion,
13
+ AppBrainConfig,
14
+ AppBehaviorConfig,
15
+ AppBootCompanionConfig,
16
+ BootCompanionOptions,
17
+ } from './boot';
18
+
19
+ export {
20
+ AppBrainConfig,
21
+ AppBehaviorConfig,
22
+ AppBootCompanionConfig,
23
+ BootCompanionOptions,
24
+ bootCompanion,
25
+ };
26
+
27
+ export interface AppInstance {
28
+ app: Express;
29
+ runtimes: Map<string, SiduriRuntime>;
30
+ setObservationOrgan: (org: FixtureObservationOrgan) => void;
31
+ }
32
+
33
+ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): AppInstance {
34
+ const app: Express = express();
35
+ app.use(cors(createCorsOptions()));
36
+ app.use(express.json());
37
+
38
+ let observationOrgan: FixtureObservationOrgan | undefined;
39
+
40
+ app.post('/boot', requireAuth, async (req, res) => {
41
+ try {
42
+ const { id, config } = req.body;
43
+ if (runtimes.has(id)) {
44
+ return res.status(400).json({ error: "Already booted" });
45
+ }
46
+
47
+ const runtime = await bootCompanion(id, config, { observationOrgan });
48
+ runtimes.set(id, runtime);
49
+
50
+ res.json({ success: true, id });
51
+ } catch (e: any) {
52
+ res.status(500).json({ error: e.message });
53
+ }
54
+ });
55
+
56
+ // STATUS / HEALTH ENDPOINTS
57
+ app.get('/health', (req, res) => res.json({ status: "ok" }));
58
+ app.get('/version', (req, res) => res.json({ name: "siduri-x-api", version: "0.2.0-x" }));
59
+ app.get('/ready', (req, res) => {
60
+ const isReady = runtimes.size > 0;
61
+ res.status(isReady ? 200 : 503).json({
62
+ status: isReady ? "ready" : "not_ready",
63
+ companionCount: runtimes.size,
64
+ });
65
+ });
66
+ app.get('/voice/health', (req, res) => {
67
+ const hasVoice = Array.from(runtimes.values()).some((r) => Boolean(r.voice));
68
+ res.json({ provider: "siduri-voice", configured: hasVoice });
69
+ });
70
+ app.get('/obs/health', (req, res) => {
71
+ const connected = Boolean(observationOrgan) || Array.from(runtimes.values()).some((r) => Boolean(r.observation));
72
+ res.json({ connected });
73
+ });
74
+ app.get('/mouth/health', (req, res) => {
75
+ const hasMouth = Array.from(runtimes.values()).some((r) => Boolean(r.mouth));
76
+ res.json({ provider: "siduri-mouth", configured: hasMouth });
77
+ });
78
+ app.get('/body/health', (req, res) => {
79
+ const hasBody = Array.from(runtimes.values()).some((r) => Boolean(r.body));
80
+ res.json({ provider: "siduri-body", configured: hasBody });
81
+ });
82
+ app.get('/body/snapshot', (req, res) => {
83
+ const id = (req.query.id as string) || Array.from(runtimes.keys())[0];
84
+ const runtime = runtimes.get(id);
85
+ if (!runtime || !runtime.body) {
86
+ return res.status(404).json({ error: "Body organ not configured for companion" });
87
+ }
88
+ const snapshot = typeof (runtime.body as any).getSnapshot === 'function'
89
+ ? (runtime.body as any).getSnapshot()
90
+ : null;
91
+ res.json({ snapshot });
92
+ });
93
+ app.get('/mouth/channels', (req, res) => {
94
+ const id = (req.query.id as string) || Array.from(runtimes.keys())[0];
95
+ const runtime = runtimes.get(id);
96
+ if (!runtime || !runtime.mouth || typeof runtime.mouth.getRegisteredChannels !== 'function') {
97
+ return res.json({ channels: [] });
98
+ }
99
+ res.json({ channels: runtime.mouth.getRegisteredChannels() });
100
+ });
101
+ app.get('/me', attachIdentity, (req, res) => {
102
+ const identity = (req as any).identity as Identity;
103
+ res.json({
104
+ actorId: identity.actorId || (identity.authenticated ? 'owner-user' : 'anonymous-session'),
105
+ role: identity.role || (identity.authenticated ? 'OWNER' : 'VIEWER'),
106
+ authenticated: Boolean(identity.authenticated),
107
+ });
108
+ });
109
+ app.put('/me', requireAuth, (req, res) => res.json({ success: true }));
110
+
111
+ // TEACH MODE ENDPOINTS
112
+ app.get('/teach/detected-self', attachIdentity, async (req, res) => {
113
+ try {
114
+ const companionId = (req.query.companionId as string) || Array.from(runtimes.keys())[0] || 'default';
115
+ const runtime = runtimes.get(companionId);
116
+
117
+ const explicitPath = (req.query.path as string) || undefined;
118
+ const envPath = process.env.SIDURI_SELF_PATH || process.env.SELF_PATH;
119
+ const configPath = (runtime?.config as any)?.organs?.behavior?.selfPath ||
120
+ (runtime?.config as any)?.behavior?.selfPath;
121
+
122
+ const candidates: string[] = [];
123
+ if (explicitPath) candidates.push(path.resolve(process.cwd(), explicitPath));
124
+ if (envPath) candidates.push(path.resolve(process.cwd(), envPath));
125
+ if (configPath) candidates.push(path.resolve(process.cwd(), configPath));
126
+
127
+ candidates.push(path.resolve(process.cwd(), 'assets', 'self', `${companionId}.self`));
128
+ candidates.push(path.resolve(process.cwd(), `${companionId}.self`));
129
+
130
+ const searchDirs = [
131
+ path.resolve(process.cwd(), 'assets', 'self'),
132
+ path.resolve(process.cwd(), 'assets'),
133
+ path.resolve(process.cwd()),
134
+ path.resolve(process.cwd(), '..', '..', 'assets', 'self'),
135
+ path.resolve(process.cwd(), '..', '..'),
136
+ ];
137
+
138
+ let matchedFilePath: string | undefined;
139
+
140
+ for (const candidate of candidates) {
141
+ if (fs.existsSync(candidate)) {
142
+ try {
143
+ const stat = fs.statSync(candidate);
144
+ if (stat.isFile()) {
145
+ matchedFilePath = candidate;
146
+ break;
147
+ }
148
+ } catch {}
149
+ }
150
+ }
151
+
152
+ if (!matchedFilePath) {
153
+ for (const dir of searchDirs) {
154
+ if (fs.existsSync(dir)) {
155
+ try {
156
+ const entries = fs.readdirSync(dir);
157
+ const selfFiles = entries.filter((f) => f.endsWith('.self'));
158
+ if (selfFiles.length > 0) {
159
+ const companionSelf = selfFiles.find((f) => f === `${companionId}.self`);
160
+ matchedFilePath = path.join(dir, companionSelf || selfFiles[0]);
161
+ break;
162
+ }
163
+ } catch {}
164
+ }
165
+ }
166
+ }
167
+
168
+ if (!matchedFilePath) {
169
+ return res.json({ detected: false });
170
+ }
171
+
172
+ const content = await fs.promises.readFile(matchedFilePath, 'utf8');
173
+ const parsed = await compilePersonaDocument(content, {
174
+ brain: runtime?.brain,
175
+ companionId,
176
+ fallbackToParser: true,
177
+ });
178
+
179
+ let alreadyInstalled = false;
180
+ const repo = new SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
181
+ try {
182
+ const existingIdentity = await repo.getIdentity(companionId);
183
+ const existingDirectives = await repo.getActiveDirectives(companionId);
184
+ if (
185
+ existingIdentity &&
186
+ parsed.manifest?.identity?.name &&
187
+ existingIdentity.name.toLowerCase() === parsed.manifest.identity.name.toLowerCase() &&
188
+ existingDirectives.length > 0
189
+ ) {
190
+ alreadyInstalled = true;
191
+ }
192
+ } catch {} finally {
193
+ repo.close();
194
+ }
195
+
196
+ res.json({
197
+ detected: true,
198
+ filename: path.basename(matchedFilePath),
199
+ path: matchedFilePath,
200
+ content,
201
+ parsed,
202
+ alreadyInstalled,
203
+ });
204
+ } catch (e: any) {
205
+ res.status(500).json({ error: e.message });
206
+ }
207
+ });
208
+
209
+ app.post('/teach/upload-self', requireAuth, async (req, res) => {
210
+ try {
211
+ const { content, companionId } = req.body;
212
+ if (!content) return res.status(400).json({ error: "Missing content" });
213
+ const targetId = companionId || Array.from(runtimes.keys())[0] || 'default';
214
+ const runtime = runtimes.get(targetId);
215
+ const parsed = await compilePersonaDocument(content, {
216
+ brain: runtime?.brain,
217
+ companionId: targetId,
218
+ fallbackToParser: true,
219
+ });
220
+ res.json(parsed);
221
+ } catch (e: any) {
222
+ res.status(500).json({ error: e.message });
223
+ }
224
+ });
225
+
226
+ app.post('/teach/install-self', requireAuth, async (req, res) => {
227
+ try {
228
+ const { companionId, manifest, approvedDirectiveIds } = req.body;
229
+ if (!companionId || !manifest || !Array.isArray(approvedDirectiveIds)) {
230
+ return res.status(400).json({ error: "Missing required fields" });
231
+ }
232
+
233
+ if (!manifest.identity || !manifest.identity.name) {
234
+ return res.status(400).json({ error: "Invalid manifest: missing identity.name" });
235
+ }
236
+
237
+ const directivesToCommit = manifest.directives?.filter((d: any) => approvedDirectiveIds.includes(d.id)) || [];
238
+ for (const d of directivesToCommit) {
239
+ if (!d || typeof d.directive !== 'string') {
240
+ return res.status(400).json({ error: "Invalid directive entry: missing directive string" });
241
+ }
242
+ const scan = scanDirective(d.directive);
243
+ if (!scan.safe) {
244
+ return res.status(400).json({
245
+ error: `Safety check failed for directive: ${scan.reason}`,
246
+ directiveId: d.id,
247
+ reason: scan.reason,
248
+ });
249
+ }
250
+ }
251
+
252
+ const repo = new SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
253
+
254
+ try {
255
+ await repo.setIdentity({
256
+ companionId,
257
+ name: manifest.identity.name,
258
+ archetype: manifest.identity.archetype,
259
+ origin: manifest.identity.origin,
260
+ ethos: manifest.identity.ethos,
261
+ version: manifest.version || '1.0.0',
262
+ updatedAt: new Date().toISOString(),
263
+ });
264
+
265
+ if (manifest.personality) {
266
+ await repo.setPersonality(companionId, manifest.personality);
267
+ }
268
+
269
+ if (Array.isArray(manifest.relationships)) {
270
+ for (const rel of manifest.relationships) {
271
+ if (rel && rel.entityId) {
272
+ await repo.updateRelationship(companionId, {
273
+ companionId,
274
+ entityId: rel.entityId,
275
+ entityType: 'human',
276
+ role: rel.role || 'user',
277
+ stance: rel.stance || 'neutral',
278
+ interactionConventions: rel.conventions || [],
279
+ });
280
+ }
281
+ }
282
+ }
283
+
284
+ if (Array.isArray(manifest.dialogueExamples) && repo.setExemplars) {
285
+ await repo.setExemplars(companionId, manifest.dialogueExamples);
286
+ }
287
+
288
+ if (directivesToCommit.length > 0) {
289
+ await repo.commitDirectives(companionId, directivesToCommit);
290
+ }
291
+ } finally {
292
+ repo.close();
293
+ }
294
+
295
+ res.json({ success: true });
296
+ } catch (e: any) {
297
+ res.status(500).json({ error: e.message });
298
+ }
299
+ });
300
+
301
+ app.get('/teach/identity', attachIdentity, async (req, res) => {
302
+ try {
303
+ const companionId = (req.query.companionId as string) || Array.from(runtimes.keys())[0] || 'default';
304
+ const runtime = runtimes.get(companionId);
305
+ let identity: any = null;
306
+
307
+ if (runtime?.self && typeof (runtime.self as any).getIdentity === 'function') {
308
+ try {
309
+ identity = await (runtime.self as any).getIdentity(companionId);
310
+ } catch {}
311
+ }
312
+
313
+ if (!identity) {
314
+ const repo = new SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
315
+ try {
316
+ identity = await repo.getIdentity(companionId);
317
+ } catch {} finally {
318
+ repo.close();
319
+ }
320
+ }
321
+
322
+ const configuredName = runtime?.config?.name || 'Siduri';
323
+ res.json({
324
+ companionId,
325
+ name: identity?.name || configuredName,
326
+ archetype: identity?.archetype || identity?.role,
327
+ origin: identity?.origin,
328
+ ethos: identity?.ethos,
329
+ version: identity?.version,
330
+ });
331
+ } catch (e: any) {
332
+ res.status(500).json({ error: e.message });
333
+ }
334
+ });
335
+
336
+ // CHAT (API context boundary validation)
337
+ app.post('/chat', attachIdentity, async (req, res) => {
338
+ const { id, message, history } = req.body;
339
+ const identity = (req as any).identity as Identity;
340
+ const local = isLocalRequest(req);
341
+ const hasConfiguredToken = Boolean(process.env.AUTH_TOKEN || process.env.OPERATOR_TOKEN || process.env.OWNER_TOKEN);
342
+ const isLocalDirectOwner = local && !hasConfiguredToken;
343
+
344
+ const authenticated = identity?.authenticated || isLocalDirectOwner;
345
+ const serverRole = identity?.authenticated ? identity.role : (isLocalDirectOwner ? 'OWNER' : 'VIEWER');
346
+
347
+ // Single-owner companion model: map request context directly with server identity enforcement
348
+ const mappingResult = mapRequestContext({
349
+ ...req.body,
350
+ id: id || req.body.companionId,
351
+ authenticated,
352
+ serverRole,
353
+ source: identity?.source ?? (local ? 'local' : 'external'),
354
+ generateCorrelationId: true,
355
+ });
356
+
357
+ if (!mappingResult.accepted) {
358
+ return res.status(400).json({
359
+ accepted: false,
360
+ error: mappingResult.error,
361
+ });
362
+ }
363
+
364
+ const companionId = mappingResult.context!.companionId;
365
+ const runtime = runtimes.get(companionId);
366
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
367
+
368
+ try {
369
+ const response = await dispatchCompanionChat(runtime, {
370
+ id: companionId,
371
+ companionId,
372
+ message,
373
+ context: mappingResult.context,
374
+ history,
375
+ ...(req.body?.medium ? { medium: req.body.medium } : {}),
376
+ subtitleLanguage: req.body?.subtitleLanguage || req.body?.subtitle_language,
377
+ });
378
+ res.json(response);
379
+ } catch (e: any) {
380
+ res.status(500).json({ error: e.message });
381
+ }
382
+ });
383
+
384
+ // REAL-TIME SSE STREAMING (Mouth transport)
385
+ app.post('/chat/stream', attachIdentity, async (req, res) => {
386
+ const { id, message, history } = req.body;
387
+ const identity = (req as any).identity as Identity;
388
+ const local = isLocalRequest(req);
389
+ const hasConfiguredToken = Boolean(process.env.AUTH_TOKEN || process.env.OPERATOR_TOKEN || process.env.OWNER_TOKEN);
390
+ const isLocalDirectOwner = local && !hasConfiguredToken;
391
+
392
+ const authenticated = identity?.authenticated || isLocalDirectOwner;
393
+ const serverRole = identity?.authenticated ? identity.role : (isLocalDirectOwner ? 'OWNER' : 'VIEWER');
394
+
395
+ const mappingResult = mapRequestContext({
396
+ ...req.body,
397
+ id: id || req.body.companionId,
398
+ authenticated,
399
+ serverRole,
400
+ source: identity?.source ?? (local ? 'local' : 'external'),
401
+ generateCorrelationId: true,
402
+ });
403
+
404
+ if (!mappingResult.accepted) {
405
+ return res.status(400).json({
406
+ accepted: false,
407
+ error: mappingResult.error,
408
+ });
409
+ }
410
+
411
+ const companionId = mappingResult.context!.companionId;
412
+ const runtime = runtimes.get(companionId);
413
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
414
+
415
+ res.setHeader('Content-Type', 'text/event-stream');
416
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
417
+ res.setHeader('Connection', 'keep-alive');
418
+ res.flushHeaders?.();
419
+
420
+ const abortController = new AbortController();
421
+ const onClose = () => {
422
+ if (!res.writableEnded) {
423
+ abortController.abort('client_disconnect');
424
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
425
+ runtime.mouth.interrupt('client_disconnect');
426
+ }
427
+ }
428
+ };
429
+ res.on('close', onClose);
430
+
431
+ try {
432
+ const response = await dispatchCompanionChat(runtime, {
433
+ id: companionId,
434
+ companionId,
435
+ message,
436
+ context: mappingResult.context,
437
+ history,
438
+ medium: 'web',
439
+ signal: abortController.signal,
440
+ subtitleLanguage: req.body?.subtitleLanguage || req.body?.subtitle_language,
441
+ });
442
+
443
+ res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status, mode: response.metadata?.mode })}\n\n`);
444
+
445
+ const avatarEvent = response.metadata?.events?.find(
446
+ (e: any) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval?.toLowerCase() === 'approved' || e.approval === 'APPROVED' || !e.approval)
447
+ );
448
+ if (avatarEvent) {
449
+ res.write(`event: avatar\ndata: ${JSON.stringify(avatarEvent)}\n\n`);
450
+ }
451
+
452
+ const maxStreamLen = Number(process.env.SIDURI_MAX_RESPONSE_CHARS || 8000);
453
+ const rawSpeechText = response.delivery?.text || response.response?.subtitle_en || response.response?.spoken_ja || '';
454
+ const speechText = rawSpeechText.slice(0, maxStreamLen);
455
+ const utterance = {
456
+ utteranceId: response.response_id || 'utt-stream',
457
+ companionId,
458
+ responseId: response.response_id,
459
+ correlationId: response.correlation_id,
460
+ text: speechText,
461
+ medium: 'web' as const,
462
+ expression: avatarEvent?.expression,
463
+ action: avatarEvent?.action,
464
+ signal: abortController.signal,
465
+ };
466
+
467
+ if (runtime.mouth && typeof runtime.mouth.stream === 'function') {
468
+ for await (const chunk of runtime.mouth.stream(utterance)) {
469
+ if (abortController.signal.aborted) {
470
+ res.write(`event: chunk\ndata: ${JSON.stringify({ ...chunk, interrupted: true })}\n\n`);
471
+ break;
472
+ }
473
+ res.write(`event: chunk\ndata: ${JSON.stringify(chunk)}\n\n`);
474
+ }
475
+ } else {
476
+ res.write(`event: chunk\ndata: ${JSON.stringify({ utteranceId: utterance.utteranceId, index: 1, deltaText: speechText, isComplete: true, medium: 'web' })}\n\n`);
477
+ }
478
+
479
+ res.write(`event: done\ndata: ${JSON.stringify(response)}\n\n`);
480
+ res.end();
481
+ } catch (e: any) {
482
+ if (abortController.signal.aborted) {
483
+ res.write(`event: interrupted\ndata: ${JSON.stringify({ reason: abortController.signal.reason })}\n\n`);
484
+ } else {
485
+ res.write(`event: error\ndata: ${JSON.stringify({ error: e.message })}\n\n`);
486
+ }
487
+ res.end();
488
+ } finally {
489
+ res.removeListener('close', onClose);
490
+ }
491
+ });
492
+
493
+ // INTERRUPTION / BARGE-IN
494
+ app.post('/chat/interrupt', attachIdentity, (req, res) => {
495
+ const companionId = req.body?.companionId || req.body?.id || Array.from(runtimes.keys())[0];
496
+ const runtime = companionId ? runtimes.get(companionId) : undefined;
497
+ const reason = req.body?.reason || 'user_barge_in';
498
+
499
+ if (runtime) {
500
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
501
+ runtime.mouth.interrupt(reason);
502
+ }
503
+ return res.json({ success: true, interrupted: true, companionId, reason });
504
+ }
505
+
506
+ for (const r of runtimes.values()) {
507
+ if (r.mouth && typeof r.mouth.interrupt === 'function') {
508
+ r.mouth.interrupt(reason);
509
+ }
510
+ }
511
+ return res.json({ success: true, interrupted: true, reason });
512
+ });
513
+
514
+ app.post('/mouth/interrupt', attachIdentity, (req, res) => {
515
+ const companionId = req.body?.companionId || req.body?.id || Array.from(runtimes.keys())[0];
516
+ const runtime = companionId ? runtimes.get(companionId) : undefined;
517
+ const reason = req.body?.reason || 'user_barge_in';
518
+
519
+ if (runtime) {
520
+ if (runtime.mouth && typeof runtime.mouth.interrupt === 'function') {
521
+ runtime.mouth.interrupt(reason);
522
+ }
523
+ return res.json({ success: true, interrupted: true, companionId, reason });
524
+ }
525
+
526
+ for (const r of runtimes.values()) {
527
+ if (r.mouth && typeof r.mouth.interrupt === 'function') {
528
+ r.mouth.interrupt(reason);
529
+ }
530
+ }
531
+ return res.json({ success: true, interrupted: true, reason });
532
+ });
533
+
534
+ // MEMORY GETTERS
535
+ app.get('/memory/proposals', requireAuth, async (req, res) => {
536
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
537
+ const runtime = runtimes.get(id);
538
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
539
+ if (!runtime.memory) return res.json({ proposals: [] });
540
+ try {
541
+ const proposals = await runtime.memory.getPendingClaims();
542
+ res.json({ proposals });
543
+ } catch (e: any) {
544
+ res.status(500).json({ error: e.message });
545
+ }
546
+ });
547
+
548
+ app.get('/memory', requireAuth, async (req, res) => {
549
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
550
+ const runtime = runtimes.get(id);
551
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
552
+ if (!runtime.memory) return res.json({ items: [] });
553
+ try {
554
+ const items = await runtime.memory.getClaims();
555
+ res.json({ items });
556
+ } catch (e: any) {
557
+ res.status(500).json({ error: e.message });
558
+ }
559
+ });
560
+
561
+ app.get('/memory/claims', requireAuth, async (req, res) => {
562
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
563
+ const runtime = runtimes.get(id);
564
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
565
+ if (!runtime.memory) return res.json({ claims: [] });
566
+ try {
567
+ const claims = typeof (runtime.memory as any).getAllClaims === 'function'
568
+ ? await (runtime.memory as any).getAllClaims()
569
+ : await runtime.memory.getClaims();
570
+ res.json({ claims });
571
+ } catch (e: any) {
572
+ res.status(500).json({ error: e.message });
573
+ }
574
+ });
575
+
576
+ app.get('/memory/behavioral', requireAuth, async (req, res) => {
577
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
578
+ const runtime = runtimes.get(id);
579
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
580
+ if (!runtime.memory) return res.json({ directives: [] });
581
+ try {
582
+ const directives = typeof (runtime.memory as any).getAllDirectives === 'function'
583
+ ? await (runtime.memory as any).getAllDirectives()
584
+ : await runtime.memory.getDirectives();
585
+ res.json({ directives });
586
+ } catch (e: any) {
587
+ res.status(500).json({ error: e.message });
588
+ }
589
+ });
590
+
591
+ // KNOWLEDGE / LIFE DB GETTERS
592
+ app.get('/knowledge/life', requireAuth, async (req, res) => {
593
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
594
+ const runtime = runtimes.get(id);
595
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
596
+ const query = (req.query.q as string) || '';
597
+ if (!runtime.knowledge || typeof (runtime.knowledge as any).queryLifeContext !== 'function') {
598
+ return res.json({ matchedInventory: [], recentFinances: [], upcomingSchedule: [], preferences: [], formattedContext: '' });
599
+ }
600
+ try {
601
+ const result = await (runtime.knowledge as any).queryLifeContext(id, query);
602
+ res.json(result);
603
+ } catch (e: any) {
604
+ res.status(500).json({ error: e.message });
605
+ }
606
+ });
607
+
608
+ app.get('/knowledge/inventory', requireAuth, async (req, res) => {
609
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
610
+ const runtime = runtimes.get(id);
611
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
612
+ const domain = req.query.domain as string | undefined;
613
+ if (!runtime.knowledge || !(runtime.knowledge as any).inventory) {
614
+ return res.json({ items: [] });
615
+ }
616
+ try {
617
+ const items = await (runtime.knowledge as any).inventory.getItems(id, domain);
618
+ res.json({ items });
619
+ } catch (e: any) {
620
+ res.status(500).json({ error: e.message });
621
+ }
622
+ });
623
+
624
+ app.get('/knowledge/finance', requireAuth, async (req, res) => {
625
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
626
+ const runtime = runtimes.get(id);
627
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
628
+ if (!runtime.knowledge || !(runtime.knowledge as any).finance) {
629
+ return res.json({ summary: null, entries: [] });
630
+ }
631
+ try {
632
+ const limit = Number(req.query.limit || 20);
633
+ const [summary, entries] = await Promise.all([
634
+ (runtime.knowledge as any).finance.getSummary(id),
635
+ (runtime.knowledge as any).finance.getEntries(id, limit),
636
+ ]);
637
+ res.json({ summary, entries });
638
+ } catch (e: any) {
639
+ res.status(500).json({ error: e.message });
640
+ }
641
+ });
642
+
643
+ app.get('/knowledge/schedule', requireAuth, async (req, res) => {
644
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
645
+ const runtime = runtimes.get(id);
646
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
647
+ if (!runtime.knowledge || !(runtime.knowledge as any).schedule) {
648
+ return res.json({ items: [] });
649
+ }
650
+ try {
651
+ const items = await (runtime.knowledge as any).schedule.getUpcoming(id);
652
+ res.json({ items });
653
+ } catch (e: any) {
654
+ res.status(500).json({ error: e.message });
655
+ }
656
+ });
657
+
658
+ app.get('/knowledge/preferences', requireAuth, async (req, res) => {
659
+ const id = req.query.id as string || Array.from(runtimes.keys())[0];
660
+ const runtime = runtimes.get(id);
661
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
662
+ const category = req.query.category as string | undefined;
663
+ if (!runtime.knowledge || !(runtime.knowledge as any).preferences) {
664
+ return res.json({ preferences: [] });
665
+ }
666
+ try {
667
+ const preferences = await (runtime.knowledge as any).preferences.getPreferences(id, category);
668
+ res.json({ preferences });
669
+ } catch (e: any) {
670
+ res.status(500).json({ error: e.message });
671
+ }
672
+ });
673
+
674
+ app.get('/knowledge/entities', requireAuth, async (req, res) => {
675
+ const id = (req.query.id as string) || Array.from(runtimes.keys())[0];
676
+ const runtime = runtimes.get(id);
677
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
678
+ const entityType = req.query.type as string | undefined;
679
+ const domain = req.query.domain as string | undefined;
680
+ if (!runtime.knowledge || !(runtime.knowledge as any).entities) {
681
+ return res.json({ entities: [] });
682
+ }
683
+ try {
684
+ const entities = await (runtime.knowledge as any).entities.getEntities(id, entityType, domain);
685
+ res.json({ entities });
686
+ } catch (e: any) {
687
+ res.status(500).json({ error: e.message });
688
+ }
689
+ });
690
+
691
+ app.get('/knowledge/events', requireAuth, async (req, res) => {
692
+ const id = (req.query.id as string) || Array.from(runtimes.keys())[0];
693
+ const runtime = runtimes.get(id);
694
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
695
+ const stream = req.query.stream as string | undefined;
696
+ const limit = Number(req.query.limit || 50);
697
+ if (!runtime.knowledge || !(runtime.knowledge as any).events) {
698
+ return res.json({ events: [] });
699
+ }
700
+ try {
701
+ const events = await (runtime.knowledge as any).events.getEvents(id, stream, limit);
702
+ res.json({ events });
703
+ } catch (e: any) {
704
+ res.status(500).json({ error: e.message });
705
+ }
706
+ });
707
+
708
+ app.get('/knowledge/tasks', requireAuth, async (req, res) => {
709
+ const id = (req.query.id as string) || Array.from(runtimes.keys())[0];
710
+ const runtime = runtimes.get(id);
711
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
712
+ const status = req.query.status as string | undefined;
713
+ if (!runtime.knowledge || !(runtime.knowledge as any).tasks) {
714
+ return res.json({ tasks: [] });
715
+ }
716
+ try {
717
+ const tasks = await (runtime.knowledge as any).tasks.getTasks(id, status);
718
+ res.json({ tasks });
719
+ } catch (e: any) {
720
+ res.status(500).json({ error: e.message });
721
+ }
722
+ });
723
+
724
+ app.post('/knowledge/proposals/approve', requireAuth, async (req, res) => {
725
+ const id = (req.body.companionId as string) || Array.from(runtimes.keys())[0];
726
+ const runtime = runtimes.get(id);
727
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
728
+ try {
729
+ if (typeof (runtime as any).approveProposal === 'function') {
730
+ const result = await (runtime as any).approveProposal(req.body.id, { companionId: id });
731
+ res.json({ approved: true, target: result?.target || 'knowledge', status: 'approved' });
732
+ } else {
733
+ res.status(400).json({ error: 'Runtime does not support proposal approval' });
734
+ }
735
+ } catch (e: any) {
736
+ res.status(500).json({ error: e.message });
737
+ }
738
+ });
739
+
740
+ app.post('/knowledge/entities', requireAuth, async (req, res) => {
741
+ const id = (req.body.companionId as string) || (req.query.id as string) || Array.from(runtimes.keys())[0];
742
+ const runtime = runtimes.get(id);
743
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
744
+ if (!runtime.knowledge || !(runtime.knowledge as any).entities) {
745
+ return res.status(400).json({ error: 'Knowledge organ does not support entities' });
746
+ }
747
+ try {
748
+ const entity = {
749
+ id: req.body.id || `ent-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
750
+ companionId: id,
751
+ name: req.body.name,
752
+ entityType: req.body.entityType || req.body.type || 'entity',
753
+ domain: req.body.domain || 'general',
754
+ properties: req.body.properties || {},
755
+ };
756
+ await (runtime.knowledge as any).entities.saveEntity(entity);
757
+ res.json({ saved: true, entity });
758
+ } catch (e: any) {
759
+ res.status(500).json({ error: e.message });
760
+ }
761
+ });
762
+
763
+ app.post('/knowledge/entities/delete', requireAuth, async (req, res) => {
764
+ const id = (req.body.companionId as string) || (req.query.id as string) || Array.from(runtimes.keys())[0];
765
+ const runtime = runtimes.get(id);
766
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
767
+ if (!runtime.knowledge || !(runtime.knowledge as any).entities) {
768
+ return res.status(400).json({ error: 'Knowledge organ does not support entities' });
769
+ }
770
+ try {
771
+ const success = await (runtime.knowledge as any).entities.deleteEntity(req.body.id);
772
+ res.json({ deleted: success, id: req.body.id });
773
+ } catch (e: any) {
774
+ res.status(500).json({ error: e.message });
775
+ }
776
+ });
777
+
778
+ app.post('/knowledge/tasks', requireAuth, async (req, res) => {
779
+ const id = (req.body.companionId as string) || (req.query.id as string) || Array.from(runtimes.keys())[0];
780
+ const runtime = runtimes.get(id);
781
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
782
+ if (!runtime.knowledge || !(runtime.knowledge as any).tasks) {
783
+ return res.status(400).json({ error: 'Knowledge organ does not support tasks' });
784
+ }
785
+ try {
786
+ const task = {
787
+ id: req.body.id || `task-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
788
+ companionId: id,
789
+ title: req.body.title,
790
+ status: req.body.status || 'todo',
791
+ priority: req.body.priority !== undefined ? Number(req.body.priority) : 1,
792
+ targetDate: req.body.targetDate || null,
793
+ metadata: req.body.metadata || {},
794
+ };
795
+ await (runtime.knowledge as any).tasks.saveTask(task);
796
+ res.json({ saved: true, task });
797
+ } catch (e: any) {
798
+ res.status(500).json({ error: e.message });
799
+ }
800
+ });
801
+
802
+ app.post('/knowledge/tasks/delete', requireAuth, async (req, res) => {
803
+ const id = (req.body.companionId as string) || (req.query.id as string) || Array.from(runtimes.keys())[0];
804
+ const runtime = runtimes.get(id);
805
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
806
+ if (!runtime.knowledge || !(runtime.knowledge as any).tasks) {
807
+ return res.status(400).json({ error: 'Knowledge organ does not support tasks' });
808
+ }
809
+ try {
810
+ const success = await (runtime.knowledge as any).tasks.deleteTask(req.body.id);
811
+ res.json({ deleted: success, id: req.body.id });
812
+ } catch (e: any) {
813
+ res.status(500).json({ error: e.message });
814
+ }
815
+ });
816
+
817
+ app.post('/knowledge/events', requireAuth, async (req, res) => {
818
+ const id = (req.body.companionId as string) || (req.query.id as string) || Array.from(runtimes.keys())[0];
819
+ const runtime = runtimes.get(id);
820
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
821
+ if (!runtime.knowledge || !(runtime.knowledge as any).events) {
822
+ return res.status(400).json({ error: 'Knowledge organ does not support events' });
823
+ }
824
+ try {
825
+ const event = {
826
+ id: req.body.id || `evt-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
827
+ companionId: id,
828
+ stream: req.body.stream || 'default',
829
+ timestamp: req.body.timestamp || new Date().toISOString(),
830
+ metricValue: req.body.metricValue !== undefined && req.body.metricValue !== null ? Number(req.body.metricValue) : undefined,
831
+ metadata: req.body.metadata || {},
832
+ };
833
+ await (runtime.knowledge as any).events.addEvent(event);
834
+ res.json({ saved: true, event });
835
+ } catch (e: any) {
836
+ res.status(500).json({ error: e.message });
837
+ }
838
+ });
839
+
840
+ app.post('/knowledge/schedule', requireAuth, async (req, res) => {
841
+ const id = (req.body.companionId as string) || (req.query.id as string) || Array.from(runtimes.keys())[0];
842
+ const runtime = runtimes.get(id);
843
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
844
+ if (!runtime.knowledge || !(runtime.knowledge as any).schedule) {
845
+ return res.status(400).json({ error: 'Knowledge organ does not support schedule' });
846
+ }
847
+ try {
848
+ const item = {
849
+ id: req.body.id || `sched-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
850
+ companionId: id,
851
+ title: req.body.title,
852
+ startTime: req.body.startTime,
853
+ endTime: req.body.endTime || null,
854
+ isRecurring: Boolean(req.body.isRecurring),
855
+ status: req.body.status || 'active',
856
+ };
857
+ await (runtime.knowledge as any).schedule.saveItem(item);
858
+ res.json({ saved: true, item });
859
+ } catch (e: any) {
860
+ res.status(500).json({ error: e.message });
861
+ }
862
+ });
863
+
864
+ app.post('/knowledge/schedule/delete', requireAuth, async (req, res) => {
865
+ const id = (req.body.companionId as string) || (req.query.id as string) || Array.from(runtimes.keys())[0];
866
+ const runtime = runtimes.get(id);
867
+ if (!runtime) return res.status(404).json({ error: 'Companion not found' });
868
+ if (!runtime.knowledge || !(runtime.knowledge as any).schedule) {
869
+ return res.status(400).json({ error: 'Knowledge organ does not support schedule' });
870
+ }
871
+ try {
872
+ const success = typeof (runtime.knowledge as any).schedule.deleteItem === 'function'
873
+ ? await (runtime.knowledge as any).schedule.deleteItem(req.body.id)
874
+ : false;
875
+ res.json({ deleted: success, id: req.body.id });
876
+ } catch (e: any) {
877
+ res.status(500).json({ error: e.message });
878
+ }
879
+ });
880
+
881
+ // MEMORY MUTATIONS - PROPOSALS
882
+ app.post('/memory/proposals/update', requireAuth, async (req, res) => {
883
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
884
+ const runtime = runtimes.get(id);
885
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
886
+ if (!runtime.memory || typeof runtime.memory.updateClaim !== 'function') {
887
+ return res.status(400).json({ error: "Memory organ does not support updating claims" });
888
+ }
889
+ try {
890
+ const claimId = req.body.id || req.body.claimId;
891
+ if (!claimId) {
892
+ return res.status(400).json({ error: "Missing required claim id" });
893
+ }
894
+ const updated = await runtime.memory.updateClaim(claimId, req.body.updates || req.body);
895
+ res.json({ success: true, claim: updated });
896
+ } catch (e: any) {
897
+ res.status(500).json({ error: e.message });
898
+ }
899
+ });
900
+
901
+ app.post('/memory/proposals/approve', requireAuth, async (req, res) => {
902
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
903
+ const runtime = runtimes.get(id);
904
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
905
+ if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
906
+ try {
907
+ let name: string | undefined;
908
+ if (typeof (runtime as any).approveProposal === 'function') {
909
+ const pRes = await (runtime as any).approveProposal(req.body.id, { companionId: id });
910
+ name = pRes?.name;
911
+ } else {
912
+ await runtime.memory.approveClaim(req.body.id);
913
+ }
914
+ if (!name && runtime.self && typeof (runtime.self as any).getIdentity === 'function') {
915
+ try {
916
+ const ident = await (runtime.self as any).getIdentity(id);
917
+ name = ident?.name;
918
+ } catch {}
919
+ }
920
+ res.json({ approved: true, status: 'approved', name });
921
+ } catch (e: any) {
922
+ res.status(500).json({ error: e.message });
923
+ }
924
+ });
925
+
926
+ app.post('/memory/proposals/reject', requireAuth, async (req, res) => {
927
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
928
+ const runtime = runtimes.get(id);
929
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
930
+ if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
931
+ try {
932
+ if (typeof (runtime as any).rejectProposal === 'function') {
933
+ await (runtime as any).rejectProposal(req.body.id, { companionId: id });
934
+ } else {
935
+ await runtime.memory.rejectClaim(req.body.id);
936
+ }
937
+ res.json({ rejected: true, status: 'rejected' });
938
+ } catch (e: any) {
939
+ res.status(500).json({ error: e.message });
940
+ }
941
+ });
942
+
943
+ // MEMORY MUTATIONS - BEHAVIORAL
944
+ app.post('/memory/behavioral/approve', requireAuth, async (req, res) => {
945
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
946
+ const runtime = runtimes.get(id);
947
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
948
+ if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
949
+ try {
950
+ let name: string | undefined;
951
+ if (typeof (runtime as any).approveDirective === 'function') {
952
+ const bRes = await (runtime as any).approveDirective(req.body.id, { companionId: id });
953
+ name = bRes?.name;
954
+ } else {
955
+ await runtime.memory.approveDirective(req.body.id);
956
+ }
957
+ if (!name && runtime.self && typeof (runtime.self as any).getIdentity === 'function') {
958
+ try {
959
+ const ident = await (runtime.self as any).getIdentity(id);
960
+ name = ident?.name;
961
+ } catch {}
962
+ }
963
+ res.json({ approved: true, status: 'active', name });
964
+ } catch (e: any) {
965
+ res.status(500).json({ error: e.message });
966
+ }
967
+ });
968
+
969
+ app.post('/memory/behavioral/reject', requireAuth, async (req, res) => {
970
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
971
+ const runtime = runtimes.get(id);
972
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
973
+ if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
974
+ try {
975
+ if (typeof (runtime as any).rejectDirective === 'function') {
976
+ await (runtime as any).rejectDirective(req.body.id, { companionId: id });
977
+ } else {
978
+ await runtime.memory.rejectDirective(req.body.id);
979
+ }
980
+ res.json({ rejected: true, status: 'rejected' });
981
+ } catch (e: any) {
982
+ res.status(500).json({ error: e.message });
983
+ }
984
+ });
985
+
986
+ app.post('/memory/behavioral/revoke', requireAuth, async (req, res) => {
987
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
988
+ const runtime = runtimes.get(id);
989
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
990
+ if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
991
+ try {
992
+ if (typeof (runtime as any).revokeDirective === 'function') {
993
+ await (runtime as any).revokeDirective(req.body.id, { companionId: id });
994
+ } else {
995
+ await runtime.memory.revokeDirective(req.body.id);
996
+ }
997
+ res.json({ revoked: true, status: 'revoked' });
998
+ } catch (e: any) {
999
+ res.status(500).json({ error: e.message });
1000
+ }
1001
+ });
1002
+
1003
+ app.post('/memory/behavioral/disable', requireAuth, async (req, res) => {
1004
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
1005
+ const runtime = runtimes.get(id);
1006
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
1007
+ if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
1008
+ try {
1009
+ await runtime.memory.disableDirective(req.body.id);
1010
+ res.json({ disabled: true, status: 'disabled' });
1011
+ } catch (e: any) {
1012
+ res.status(500).json({ error: e.message });
1013
+ }
1014
+ });
1015
+
1016
+ // SYSTEM LOGS
1017
+ app.get('/system/logs', requireAuth, async (req, res) => {
1018
+ const id = (req.query.id as string) || Array.from(runtimes.keys())[0];
1019
+ const runtime = runtimes.get(id);
1020
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
1021
+ const level = req.query.level as string | undefined;
1022
+ const subsystem = req.query.subsystem as string | undefined;
1023
+ const q = req.query.q as string | undefined;
1024
+ const limit = parseInt((req.query.limit as string) || '100', 10);
1025
+ const offset = parseInt((req.query.offset as string) || '0', 10);
1026
+ try {
1027
+ const logs = typeof runtime.queryLogs === 'function'
1028
+ ? runtime.queryLogs({ companionId: id, level, subsystem, q, limit, offset })
1029
+ : [];
1030
+ res.json({ logs });
1031
+ } catch (e: any) {
1032
+ res.status(500).json({ error: e.message, logs: [] });
1033
+ }
1034
+ });
1035
+
1036
+ app.post('/system/logs/clear', requireAuth, async (req, res) => {
1037
+ const id = (req.body.companionId as string) || Array.from(runtimes.keys())[0];
1038
+ const runtime = runtimes.get(id);
1039
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
1040
+ try {
1041
+ if (typeof runtime.clearLogs === 'function') {
1042
+ runtime.clearLogs(id);
1043
+ }
1044
+ res.json({ cleared: true });
1045
+ } catch (e: any) {
1046
+ res.status(500).json({ error: e.message });
1047
+ }
1048
+ });
1049
+
1050
+ const isDevMode = process.env.NODE_ENV !== 'production' || process.env.SIDURI_DEV_MODE === 'true';
1051
+ if (isDevMode) {
1052
+ app.post('/dev/memory/reset', requireAuth, async (req, res) => {
1053
+ const id = req.body.companionId as string || Array.from(runtimes.keys())[0];
1054
+ const runtime = runtimes.get(id);
1055
+ if (!runtime) return res.status(404).json({ error: "Companion not found" });
1056
+ if (!runtime.memory || typeof runtime.memory.resetMemory !== 'function') {
1057
+ return res.status(400).json({ error: "Memory organ does not support reset" });
1058
+ }
1059
+ try {
1060
+ await runtime.memory.resetMemory();
1061
+ res.json({ reset: true });
1062
+ } catch (e: any) {
1063
+ res.status(500).json({ error: e.message });
1064
+ }
1065
+ });
1066
+
1067
+ app.post('/dev/mock-response', async (req, res) => {
1068
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
1069
+ const runtime = runtimes.get(companionId);
1070
+ if (!runtime) return res.status(404).json({ accepted: false, error: 'Companion not found' });
1071
+ const staged = runtime.gating.stageResponse({
1072
+ requestContext: {
1073
+ companionId,
1074
+ actor: {
1075
+ actorId: 'local-user',
1076
+ sessionId: 'sess-local',
1077
+ capabilities: ['chat', 'memory:approve'],
1078
+ authenticated: true,
1079
+ },
1080
+ conversation: {
1081
+ correlationId: req.body?.correlation_id || `corr-${Date.now()}`,
1082
+ },
1083
+ },
1084
+ candidateSpeech: req.body?.speech || 'Mocked staged response for review',
1085
+ candidateLanguage: req.body?.language || 'en',
1086
+ requiresApproval: req.body?.requiresApproval ?? true,
1087
+ });
1088
+ res.json({
1089
+ accepted: true,
1090
+ staged: true,
1091
+ status: staged.status,
1092
+ response_id: staged.responseId,
1093
+ correlation_id: staged.correlationId,
1094
+ speech: staged.speech,
1095
+ });
1096
+ });
1097
+
1098
+ app.post('/dev/approve-response', async (req, res) => {
1099
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
1100
+ const runtime = runtimes.get(companionId);
1101
+ if (!runtime) return res.status(404).json({ approved: false, error: 'Companion not found' });
1102
+
1103
+ let responseId = req.body?.responseId;
1104
+ let correlationId = req.body?.correlation_id;
1105
+
1106
+ if (!responseId && correlationId) {
1107
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
1108
+ if (found) {
1109
+ responseId = found.responseId;
1110
+ }
1111
+ } else if (responseId && !correlationId) {
1112
+ const found = runtime.gating.getStagedPlan(responseId);
1113
+ if (found) {
1114
+ correlationId = found.correlationId;
1115
+ }
1116
+ }
1117
+
1118
+ if (!responseId) {
1119
+ return res.status(400).json({ approved: false, error: 'UNKNOWN_APPROVAL_ID' });
1120
+ }
1121
+
1122
+ const result = runtime.gating.approveResponse({
1123
+ responseId,
1124
+ companionId,
1125
+ correlationId: correlationId || '',
1126
+ });
1127
+
1128
+ if (!result.success) {
1129
+ return res.status(400).json({ approved: false, error: result.reason });
1130
+ }
1131
+
1132
+ // Now evaluated as approved
1133
+ const evaluation = runtime.gating.evaluateGate(result.plan!);
1134
+ res.json({
1135
+ approved: true,
1136
+ status: evaluation.disposition,
1137
+ response_id: result.plan!.responseId,
1138
+ speech: result.plan!.speech,
1139
+ language: result.plan!.language,
1140
+ });
1141
+ });
1142
+
1143
+ app.post('/dev/reject-response', async (req, res) => {
1144
+ const companionId = req.body?.companionId || Array.from(runtimes.keys())[0] || 'default';
1145
+ const runtime = runtimes.get(companionId);
1146
+ if (!runtime) return res.status(404).json({ rejected: false, error: 'Companion not found' });
1147
+
1148
+ let responseId = req.body?.responseId;
1149
+ let correlationId = req.body?.correlation_id;
1150
+
1151
+ if (!responseId && correlationId) {
1152
+ const found = runtime.gating.findStagedPlanByCorrelation(companionId, correlationId);
1153
+ if (found) {
1154
+ responseId = found.responseId;
1155
+ }
1156
+ } else if (responseId && !correlationId) {
1157
+ const found = runtime.gating.getStagedPlan(responseId);
1158
+ if (found) {
1159
+ correlationId = found.correlationId;
1160
+ }
1161
+ }
1162
+
1163
+ if (!responseId) {
1164
+ return res.status(400).json({ rejected: false, error: 'UNKNOWN_APPROVAL_ID' });
1165
+ }
1166
+
1167
+ const result = runtime.gating.rejectResponse({
1168
+ responseId,
1169
+ companionId,
1170
+ correlationId: correlationId || '',
1171
+ reason: req.body?.reason,
1172
+ });
1173
+
1174
+ if (!result.success) {
1175
+ return res.status(400).json({ rejected: false, error: result.reason });
1176
+ }
1177
+
1178
+ res.json({
1179
+ rejected: true,
1180
+ status: result.plan!.status,
1181
+ response_id: result.plan!.responseId,
1182
+ });
1183
+ });
1184
+
1185
+ app.post('/dev/mock-observation', async (req, res) => {
1186
+ const activeRuntime = Array.from(runtimes.values())[0];
1187
+ const targetObs = activeRuntime?.observation || observationOrgan;
1188
+ if (!targetObs) return res.status(503).json({ accepted: false, reason: 'observation_unavailable' });
1189
+ const result = await targetObs.ingest(
1190
+ new Uint8Array([115, 121, 110, 116, 104, 101, 116, 105, 99]),
1191
+ 'fixture-observation',
1192
+ 'configured-vision',
1193
+ );
1194
+ if (!result.observation) return res.status(result.duplicate ? 200 : 409).json({ accepted: false, ...result });
1195
+ res.status(202).json({ accepted: true, observation: result.observation });
1196
+ });
1197
+ }
1198
+
1199
+ return {
1200
+ app,
1201
+ runtimes,
1202
+ setObservationOrgan: (org: FixtureObservationOrgan) => {
1203
+ observationOrgan = org;
1204
+ for (const r of runtimes.values()) {
1205
+ r.observation = org;
1206
+ }
1207
+ },
1208
+ };
1209
+ }