@siduri-x/api 2.0.1 → 2.0.2

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/boot.ts ADDED
@@ -0,0 +1,190 @@
1
+ import { OpenAICompatibleBrain, OpenRouterBrain } from '@siduri-x/brain';
2
+ import { SqliteMemoryStore } from '@siduri-x/memory';
3
+ import { VoiceAdapter, VoiceConfig } from '@siduri-x/voice';
4
+ import { UnifiedKnowledgeOrgan, UnifiedKnowledgeConfig } from '@siduri-x/knowledge';
5
+ import { OpenRouterVisionAdapter, OpenRouterVisionConfig } from '@siduri-x/vision';
6
+ import { ActiveSelfCompiler, SqliteSelfRepository } from '@siduri-x/self';
7
+ import { Live2DAdapter, Live2DAdapterConfig } from '@siduri-x/body';
8
+ import { FixtureObservationOrgan } from '@siduri-x/observation';
9
+ import { DefaultHandsOrgan, DefaultHandsOrganConfig } from '@siduri-x/hands';
10
+ import { DefaultEarOrgan, EarOrganConfig } from '@siduri-x/ear';
11
+ import { DefaultMouthOrgan, DefaultMouthOrganConfig } from '@siduri-x/mouth';
12
+ import { SiduriRuntime } from './runtime';
13
+
14
+ export interface AppBrainConfig {
15
+ provider?: 'openrouter' | 'openai-compatible' | string;
16
+ model?: string;
17
+ apiKey?: string;
18
+ apiKeyEnv?: string;
19
+ baseUrl?: string;
20
+ timeoutMs?: number;
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ export interface AppBehaviorConfig {
25
+ provider?: 'active_self' | 'none' | string;
26
+ preset?: string;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export interface AppBootCompanionConfig {
31
+ name: string;
32
+ id?: string;
33
+ brain?: AppBrainConfig;
34
+ voice?: VoiceConfig;
35
+ memory?: { provider?: string; connectionString?: string; maxConnections?: number; dbPath?: string; [key: string]: unknown };
36
+ knowledge?: UnifiedKnowledgeConfig;
37
+ vision?: OpenRouterVisionConfig;
38
+ behavior?: AppBehaviorConfig;
39
+ body?: Live2DAdapterConfig;
40
+ hands?: DefaultHandsOrganConfig;
41
+ ear?: EarOrganConfig;
42
+ mouth?: DefaultMouthOrganConfig;
43
+ self?: { dbPath?: string; [key: string]: unknown };
44
+ organs?: Record<string, any>;
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ export interface BootCompanionOptions {
49
+ observationOrgan?: FixtureObservationOrgan;
50
+ }
51
+
52
+ export function isDisabled(config?: { provider?: string }): boolean {
53
+ return !config || config.provider === 'none';
54
+ }
55
+
56
+ export function createBrain(config?: AppBrainConfig) {
57
+ const provider = config?.provider || 'openrouter';
58
+ const defaultKeyEnv = provider === 'openai-compatible' ? 'OPENAI_COMPATIBLE_API_KEY' : 'OPENROUTER_API_KEY';
59
+ const apiKey = config?.apiKey || process.env[config?.apiKeyEnv || defaultKeyEnv] || '';
60
+ if (provider === 'openai-compatible') {
61
+ return new OpenAICompatibleBrain({
62
+ apiKey,
63
+ model: config?.model || 'local-model',
64
+ baseUrl: config?.baseUrl || 'http://127.0.0.1:1234/v1',
65
+ });
66
+ }
67
+ return new OpenRouterBrain({ apiKey, model: config?.model || 'gpt-4o-mini' });
68
+ }
69
+
70
+ export function createVoice(config?: VoiceConfig) {
71
+ return isDisabled(config)
72
+ ? undefined
73
+ : new VoiceAdapter({
74
+ provider: (config?.provider as any) || 'voicevox',
75
+ baseUrl: config?.baseUrl || process.env.VOICEVOX_URL || 'http://localhost:50021',
76
+ speakerId: config?.speakerId || 1,
77
+ ...config,
78
+ });
79
+ }
80
+
81
+ export function createKnowledge(config?: UnifiedKnowledgeConfig) {
82
+ if (isDisabled(config)) return undefined;
83
+ const dbPath = (config?.dbPath as string) || process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite';
84
+ return new UnifiedKnowledgeOrgan({
85
+ ...config,
86
+ dbPath,
87
+ lifeDatabase: config?.lifeDatabase ?? true,
88
+ });
89
+ }
90
+
91
+ export function createVision(config?: OpenRouterVisionConfig & { provider?: string }) {
92
+ return isDisabled(config)
93
+ ? undefined
94
+ : new OpenRouterVisionAdapter({
95
+ apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY || '',
96
+ model: config?.model || 'gpt-4-vision',
97
+ ...config,
98
+ });
99
+ }
100
+
101
+ export function createBehavior(config?: AppBehaviorConfig) {
102
+ return isDisabled(config) ? undefined : new ActiveSelfCompiler();
103
+ }
104
+
105
+ export function createBody(config?: Live2DAdapterConfig & { provider?: string }) {
106
+ return isDisabled(config)
107
+ ? undefined
108
+ : new Live2DAdapter(config);
109
+ }
110
+
111
+ export function createHands(config?: DefaultHandsOrganConfig & { provider?: string }) {
112
+ return isDisabled(config)
113
+ ? new DefaultHandsOrgan()
114
+ : new DefaultHandsOrgan(config);
115
+ }
116
+
117
+ export function createEar(config?: EarOrganConfig & { provider?: string }) {
118
+ return isDisabled(config)
119
+ ? new DefaultEarOrgan()
120
+ : new DefaultEarOrgan(config);
121
+ }
122
+
123
+ export function createMouth(config?: DefaultMouthOrganConfig & { provider?: string }, voice?: any) {
124
+ return isDisabled(config)
125
+ ? new DefaultMouthOrgan({ voice })
126
+ : new DefaultMouthOrgan({ ...config, voice });
127
+ }
128
+
129
+ export function createMemory(config?: { provider?: string; connectionString?: string; maxConnections?: number; dbPath?: string }) {
130
+ if (isDisabled(config)) return undefined;
131
+ return new SqliteMemoryStore({ dbPath: config?.dbPath || process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
132
+ }
133
+
134
+ export function createSelf(config?: { dbPath?: string }) {
135
+ return new SqliteSelfRepository({ dbPath: config?.dbPath || process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
136
+ }
137
+
138
+ export function createObservation(vision?: any): FixtureObservationOrgan {
139
+ return new FixtureObservationOrgan(
140
+ vision ?? { analyze: async () => JSON.stringify({ readings: [] }) }
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Canonical companion bootstrapper.
146
+ * Wires all 10 organs, runs memory migrations, and initializes the SiduriRuntime.
147
+ */
148
+ export async function bootCompanion(
149
+ id: string,
150
+ config: AppBootCompanionConfig,
151
+ options?: BootCompanionOptions
152
+ ): Promise<SiduriRuntime> {
153
+ const organs = (config as any)?.organs || {};
154
+
155
+ const brain = createBrain(organs.brain || config?.brain);
156
+ const memory = createMemory(organs.memory || (config as any)?.memory);
157
+ const selfRepo = createSelf(organs.self || (config as any)?.self);
158
+ const voice = createVoice(organs.voice || config?.voice);
159
+ const knowledge = createKnowledge(organs.knowledge || config?.knowledge);
160
+ const vision = createVision(organs.vision || config?.vision);
161
+ const behavior = createBehavior(organs.behavior || config?.behavior);
162
+ const body = createBody(organs.body || config?.body);
163
+ const hands = createHands(organs.hands || config?.hands);
164
+ const ear = createEar(organs.ear || config?.ear);
165
+ const mouth = createMouth(organs.mouth || config?.mouth, voice);
166
+ const observation = options?.observationOrgan;
167
+
168
+ if (memory && typeof (memory as any).runMigrations === 'function') {
169
+ await (memory as any).runMigrations().catch((e: any) => console.warn("Migrations warning:", e.message));
170
+ }
171
+
172
+ const runtime = new SiduriRuntime(id, config as any, {
173
+ brain,
174
+ memory,
175
+ voice,
176
+ knowledge,
177
+ vision,
178
+ behavior,
179
+ body,
180
+ hands,
181
+ ear,
182
+ mouth,
183
+ observation,
184
+ self: selfRepo,
185
+ externalKnowledge: (knowledge as any)?.eAdapter ?? knowledge,
186
+ });
187
+
188
+ await runtime.initialize();
189
+ return runtime;
190
+ }
@@ -176,5 +176,105 @@ describe('API Request Context Mapper (Single-Owner, Single-Machine)', () => {
176
176
  expect(result.context?.actor.capabilities).toEqual(['chat']);
177
177
  expect(result.context?.actor.authorizationRole).toBe('viewer');
178
178
  expect(result.context?.source).toBe('external');
179
+ expect(result.diagnostics).toContain('role_escalation_attempt_suppressed');
180
+ expect(result.diagnostics).toContain('capability_escalation_attempt_suppressed');
181
+ });
182
+
183
+ test('Prevents attenuated caller from escalating to administrator role or forging elevated capabilities', () => {
184
+ const attenuatedPayload = {
185
+ companionId: 'companion-a',
186
+ authenticated: true,
187
+ role: 'VIEWER',
188
+ source: 'external',
189
+ context: {
190
+ actor: {
191
+ actorId: 'visitor-bob',
192
+ sessionId: 'session-vis-1',
193
+ authorizationRole: 'administrator', // Forged owner/admin role
194
+ capabilities: ['chat', 'system', 'root:manage', 'action:execute'], // Forged system/root capabilities
195
+ },
196
+ conversation: {
197
+ correlationId: 'corr-att-escalate',
198
+ },
199
+ },
200
+ };
201
+
202
+ const result = mapRequestContext(attenuatedPayload);
203
+ expect(result.accepted).toBe(true);
204
+ expect(result.context?.actor.authenticated).toBe(true);
205
+ expect(result.context?.actor.authorizationRole).toBe('viewer');
206
+ expect(result.context?.actor.capabilities).toEqual(['chat']);
207
+ expect(result.diagnostics).toContain('role_escalation_attempt_suppressed');
208
+ expect(result.diagnostics).toContain('capability_escalation_attempt_suppressed');
209
+ });
210
+
211
+ test('Strips unknown capabilities for authenticated owner while preserving canonical companion capabilities', () => {
212
+ const ownerPayload = {
213
+ companionId: 'companion-a',
214
+ authenticated: true,
215
+ source: 'local',
216
+ context: {
217
+ actor: {
218
+ actorId: 'owner-user',
219
+ sessionId: 'session-owner-1',
220
+ capabilities: ['chat', 'system', 'root:unauthorized', 'arbitrary:hack'],
221
+ },
222
+ conversation: {
223
+ correlationId: 'corr-owner-caps',
224
+ },
225
+ },
226
+ };
227
+
228
+ const result = mapRequestContext(ownerPayload);
229
+ expect(result.accepted).toBe(true);
230
+ expect(result.context?.actor.authorizationRole).toBe('administrator');
231
+ expect(result.context?.actor.capabilities).toEqual(['chat', 'system']);
232
+ expect(result.diagnostics).toContain('capability_escalation_attempt_suppressed');
233
+ });
234
+
235
+ test('Flat envelope: suppresses role and capability escalation when serverRole is VIEWER', () => {
236
+ const viewerPayload = {
237
+ companionId: 'companion-a',
238
+ authenticated: false,
239
+ serverRole: 'VIEWER',
240
+ role: 'OWNER', // Forged in flat envelope
241
+ capabilities: ['chat', 'system', 'action:execute'],
242
+ generateCorrelationId: true,
243
+ };
244
+
245
+ const result = mapRequestContext(viewerPayload);
246
+ expect(result.accepted).toBe(true);
247
+ expect(result.context?.actor.authorizationRole).toBe('viewer');
248
+ expect(result.context?.actor.capabilities).toEqual(['chat']);
249
+ expect(result.diagnostics).toContain('role_escalation_attempt_suppressed');
250
+ });
251
+
252
+ test('Authenticated owner receives canonical administrator role and capabilities, and can safely attenuate to viewer', () => {
253
+ const ownerPayload = {
254
+ companionId: 'companion-a',
255
+ authenticated: true,
256
+ serverRole: 'OWNER',
257
+ generateCorrelationId: true,
258
+ };
259
+
260
+ const ownerResult = mapRequestContext(ownerPayload);
261
+ expect(ownerResult.accepted).toBe(true);
262
+ expect(ownerResult.context?.actor.authorizationRole).toBe('administrator');
263
+ expect(ownerResult.context?.actor.capabilities).toEqual([
264
+ 'chat',
265
+ 'memory:approve',
266
+ 'action:execute',
267
+ 'system',
268
+ ]);
269
+
270
+ // Attenuation to viewer
271
+ const attenuatedPayload = {
272
+ ...ownerPayload,
273
+ role: 'VIEWER',
274
+ };
275
+ const attenuatedResult = mapRequestContext(attenuatedPayload);
276
+ expect(attenuatedResult.accepted).toBe(true);
277
+ expect(attenuatedResult.context?.actor.authorizationRole).toBe('viewer');
278
+ expect(attenuatedResult.diagnostics).not.toContain('role_escalation_attempt_suppressed');
179
279
  });
180
280
  });
@@ -20,8 +20,10 @@ export interface MapRequestContextResult {
20
20
  /**
21
21
  * Maps incoming HTTP requests to a canonical RequestContext.
22
22
  * In a single-owner, single-machine model:
23
- * - Security is enforced at the external boundary, not internally between roles.
23
+ * - Security is enforced at the external machine boundary, NOT internally between roles.
24
24
  * - No internal audience or viewer/operator/owner role hierarchies.
25
+ * - Authenticated callers are the single owner with full companion access.
26
+ * - Unauthenticated callers are bounded to public chat.
25
27
  */
26
28
  export function mapRequestContext(
27
29
  input: any,
@@ -43,7 +45,10 @@ export function mapRequestContext(
43
45
  if (input.context && typeof input.context === 'object') {
44
46
  const rawCtx = input.context;
45
47
  const companionId = input.companionId || rawCtx.companionId || input.id;
46
- const correlationId = rawCtx.conversation?.correlationId || input.correlationId;
48
+ const correlationId =
49
+ rawCtx.conversation?.correlationId ||
50
+ input.correlationId ||
51
+ (input.generateCorrelationId ? `corr-${Date.now()}` : undefined);
47
52
 
48
53
  if (!companionId) {
49
54
  return {
@@ -81,18 +86,46 @@ export function mapRequestContext(
81
86
  };
82
87
  }
83
88
 
84
- // Determine server-enforced authentication status
89
+ const isViewerRequested =
90
+ (typeof input.role === 'string' && input.role.toLowerCase() === 'viewer') ||
91
+ (typeof input.serverRole === 'string' && input.serverRole.toLowerCase() === 'viewer') ||
92
+ (typeof actor.authorizationRole === 'string' && actor.authorizationRole.toLowerCase() === 'viewer') ||
93
+ (typeof (actor as any).role === 'string' && (actor as any).role.toLowerCase() === 'viewer');
94
+
95
+ // Determine server-enforced authentication status at the machine boundary
85
96
  const isAuthenticated = input.authenticated !== undefined
86
97
  ? Boolean(input.authenticated)
87
98
  : (actor.authenticated !== undefined ? Boolean(actor.authenticated) : true);
88
99
 
89
- const safeCapabilities = isAuthenticated
90
- ? (Array.isArray(actor.capabilities) ? actor.capabilities : ['chat'])
91
- : ['chat'];
100
+ const isViewer = !isAuthenticated || isViewerRequested;
101
+
102
+ const CANONICAL_CAPABILITIES = new Set(['chat', 'memory:approve', 'action:execute', 'system']);
103
+ const DEFAULT_OWNER_CAPABILITIES = ['chat', 'memory:approve', 'action:execute', 'system'];
92
104
 
93
- const safeRole = isAuthenticated
94
- ? actor.authorizationRole
95
- : 'viewer';
105
+ let safeCapabilities: string[];
106
+ let authorizationRole: string;
107
+
108
+ if (isViewer) {
109
+ safeCapabilities = ['chat'];
110
+ authorizationRole = 'viewer';
111
+ if (Array.isArray(actor.capabilities) && actor.capabilities.some((c: string) => c !== 'chat' && c !== 'chat:public')) {
112
+ diagnostics.push('capability_escalation_attempt_suppressed');
113
+ }
114
+ if (actor.authorizationRole && actor.authorizationRole.toLowerCase() !== 'viewer') {
115
+ diagnostics.push('role_escalation_attempt_suppressed');
116
+ }
117
+ } else {
118
+ authorizationRole = 'administrator';
119
+ if (Array.isArray(actor.capabilities) && actor.capabilities.length > 0) {
120
+ const filtered = actor.capabilities.filter((c: string) => CANONICAL_CAPABILITIES.has(c));
121
+ if (filtered.length < actor.capabilities.length) {
122
+ diagnostics.push('capability_escalation_attempt_suppressed');
123
+ }
124
+ safeCapabilities = filtered.length > 0 ? filtered : [...DEFAULT_OWNER_CAPABILITIES];
125
+ } else {
126
+ safeCapabilities = [...DEFAULT_OWNER_CAPABILITIES];
127
+ }
128
+ }
96
129
 
97
130
  const constructed: RequestContext = {
98
131
  companionId,
@@ -102,7 +135,7 @@ export function mapRequestContext(
102
135
  sessionId: actor.sessionId,
103
136
  capabilities: safeCapabilities,
104
137
  authenticated: isAuthenticated,
105
- authorizationRole: safeRole,
138
+ authorizationRole,
106
139
  },
107
140
  conversation: {
108
141
  correlationId,
@@ -167,20 +200,52 @@ export function mapRequestContext(
167
200
  };
168
201
  }
169
202
 
170
- const actorId = input.actorId || input.actor?.actorId || 'local-user';
203
+ const isViewerRequested =
204
+ (typeof input.role === 'string' && input.role.toLowerCase() === 'viewer') ||
205
+ (typeof input.serverRole === 'string' && input.serverRole.toLowerCase() === 'viewer');
206
+
207
+ const isAuthenticated = input.authenticated !== undefined ? Boolean(input.authenticated) : true;
208
+ const isViewer = !isAuthenticated || isViewerRequested;
209
+
210
+ const actorId = input.actorId || input.actor?.actorId || (isAuthenticated && !isViewer ? 'local-user' : 'anonymous-session');
171
211
  const sessionId = input.sessionId || input.actor?.sessionId || `session-${Date.now()}`;
172
212
  if (!input.actorId && !input.actor?.actorId) {
173
213
  diagnostics.push('anonymous_session_generated');
174
214
  }
175
215
 
176
- const isAuthenticated = input.authenticated !== undefined ? Boolean(input.authenticated) : true;
177
- const capabilities = isAuthenticated
178
- ? (Array.isArray(input.capabilities)
179
- ? input.capabilities
180
- : Array.isArray(input.actor?.capabilities)
181
- ? input.actor.capabilities
182
- : ['chat', 'system'])
183
- : ['chat'];
216
+ const CANONICAL_CAPABILITIES = new Set(['chat', 'memory:approve', 'action:execute', 'system']);
217
+ const DEFAULT_OWNER_CAPABILITIES = ['chat', 'memory:approve', 'action:execute', 'system'];
218
+
219
+ const rawCaps = Array.isArray(input.capabilities)
220
+ ? input.capabilities
221
+ : Array.isArray(input.actor?.capabilities)
222
+ ? input.actor.capabilities
223
+ : undefined;
224
+
225
+ let capabilities: string[];
226
+ let authorizationRole: string;
227
+
228
+ if (isViewer) {
229
+ capabilities = ['chat'];
230
+ authorizationRole = 'viewer';
231
+ if (rawCaps && rawCaps.some((c: string) => c !== 'chat' && c !== 'chat:public')) {
232
+ diagnostics.push('capability_escalation_attempt_suppressed');
233
+ }
234
+ if (input.role && input.role.toLowerCase() !== 'viewer') {
235
+ diagnostics.push('role_escalation_attempt_suppressed');
236
+ }
237
+ } else {
238
+ authorizationRole = 'administrator';
239
+ if (rawCaps && rawCaps.length > 0) {
240
+ const filtered = rawCaps.filter((c: string) => CANONICAL_CAPABILITIES.has(c));
241
+ if (filtered.length < rawCaps.length) {
242
+ diagnostics.push('capability_escalation_attempt_suppressed');
243
+ }
244
+ capabilities = filtered.length > 0 ? filtered : [...DEFAULT_OWNER_CAPABILITIES];
245
+ } else {
246
+ capabilities = [...DEFAULT_OWNER_CAPABILITIES];
247
+ }
248
+ }
184
249
 
185
250
  const mappedContext: RequestContext = {
186
251
  companionId,
@@ -189,7 +254,7 @@ export function mapRequestContext(
189
254
  sessionId,
190
255
  capabilities,
191
256
  authenticated: isAuthenticated,
192
- authorizationRole: input.role ? (input.role.toLowerCase() === 'viewer' ? 'viewer' : 'administrator') : undefined,
257
+ authorizationRole,
193
258
  },
194
259
  conversation: {
195
260
  channel: input.channel || input.conversation?.channel || 'direct',
package/src/index.test.ts CHANGED
@@ -127,7 +127,7 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
127
127
  });
128
128
 
129
129
  test('handles barge-in interruption via POST /chat/interrupt', async () => {
130
- fakeRuntime.interruptMouth = jest.fn();
130
+ fakeRuntime.mouth = { interrupt: jest.fn() };
131
131
 
132
132
  const res = await request(app)
133
133
  .post('/chat/interrupt')
@@ -138,11 +138,11 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
138
138
 
139
139
  expect(res.status).toBe(200);
140
140
  expect(res.body.interrupted).toBe(true);
141
- expect(fakeRuntime.interruptMouth).toHaveBeenCalledWith('user_stop');
141
+ expect(fakeRuntime.mouth.interrupt).toHaveBeenCalledWith('user_stop');
142
142
  });
143
143
 
144
144
  test('handles mouth interruption via POST /mouth/interrupt', async () => {
145
- fakeRuntime.interruptMouth = jest.fn();
145
+ fakeRuntime.mouth = { interrupt: jest.fn() };
146
146
 
147
147
  const res = await request(app)
148
148
  .post('/mouth/interrupt')
@@ -153,6 +153,6 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
153
153
 
154
154
  expect(res.status).toBe(200);
155
155
  expect(res.body.interrupted).toBe(true);
156
- expect(fakeRuntime.interruptMouth).toHaveBeenCalledWith('user_barge_in');
156
+ expect(fakeRuntime.mouth.interrupt).toHaveBeenCalledWith('user_barge_in');
157
157
  });
158
158
  });
package/src/index.ts CHANGED
@@ -4,87 +4,19 @@ dotenv.config();
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { Express } from 'express';
7
- import { createApp, AppInstance, AppBrainConfig, AppBehaviorConfig } from './app';
7
+ import { createApp, AppInstance } from './app';
8
8
  import { SiduriRuntime } from './runtime';
9
- import { OpenAICompatibleBrain, OpenRouterBrain } from '@siduri-x/brain';
10
- import { SqliteMemoryStore } from '@siduri-x/memory';
11
- import { VoiceAdapter, VoiceConfig } from '@siduri-x/voice';
12
- import { EKnowledgeAdapter, EKnowledgeConfig } from '@siduri-x/eknowledge';
13
- import { OpenRouterVisionAdapter, OpenRouterVisionConfig } from '@siduri-x/vision';
14
- import { ActiveSelfCompiler, SqliteSelfRepository } from '@siduri-x/self';
15
- import { Live2DAdapter, Live2DAdapterConfig } from '@siduri-x/body';
16
- import { FixtureObservationOrgan } from '@siduri-x/observation';
9
+ import { bootCompanion, createVision, createObservation } from './boot';
17
10
 
18
11
  export { createApp, AppInstance };
19
12
  export * from './context-mapper';
13
+ export * from './boot';
20
14
 
21
15
  const runtimes = new Map<string, SiduriRuntime>();
22
16
  const instance: AppInstance = createApp(runtimes);
23
17
  export const app: Express = instance.app;
24
18
  export default app;
25
19
 
26
- function createBrain(config?: AppBrainConfig) {
27
- const provider = config?.provider || 'openrouter';
28
- const defaultKeyEnv = provider === 'openai-compatible' ? 'OPENAI_COMPATIBLE_API_KEY' : 'OPENROUTER_API_KEY';
29
- const apiKey = config?.apiKey || process.env[config?.apiKeyEnv || defaultKeyEnv] || '';
30
- if (provider === 'openai-compatible') {
31
- return new OpenAICompatibleBrain({
32
- apiKey,
33
- model: config?.model || 'local-model',
34
- baseUrl: config?.baseUrl || 'http://127.0.0.1:1234/v1',
35
- });
36
- }
37
- return new OpenRouterBrain({ apiKey, model: config?.model || 'gpt-4o-mini' });
38
- }
39
-
40
- function isDisabled(config?: { provider?: string }): boolean {
41
- return !config || config.provider === 'none';
42
- }
43
-
44
- function createVoice(config?: VoiceConfig) {
45
- return isDisabled(config)
46
- ? undefined
47
- : new VoiceAdapter({
48
- provider: (config?.provider as any) || 'voicevox',
49
- baseUrl: config?.baseUrl || process.env.VOICEVOX_URL || 'http://localhost:50021',
50
- speakerId: config?.speakerId || 1,
51
- ...config,
52
- });
53
- }
54
-
55
- function createKnowledge(config?: EKnowledgeConfig) {
56
- if (isDisabled(config)) return undefined;
57
- if (!config?.packPath && !config?.registryUrl && !config?.baseUrl) {
58
- return undefined;
59
- }
60
- return new EKnowledgeAdapter(config || {});
61
- }
62
-
63
- function createVision(config?: OpenRouterVisionConfig & { provider?: string }) {
64
- return isDisabled(config)
65
- ? undefined
66
- : new OpenRouterVisionAdapter({
67
- apiKey: config?.apiKey || process.env.OPENROUTER_API_KEY || '',
68
- model: config?.model || 'gpt-4-vision',
69
- ...config,
70
- });
71
- }
72
-
73
- function createBehavior(config?: AppBehaviorConfig) {
74
- return isDisabled(config) ? undefined : new ActiveSelfCompiler();
75
- }
76
-
77
- function createBody(config?: Live2DAdapterConfig & { provider?: string }) {
78
- return isDisabled(config)
79
- ? undefined
80
- : new Live2DAdapter(config);
81
- }
82
-
83
- function createMemory(config?: { provider?: string; connectionString?: string; maxConnections?: number; dbPath?: string }) {
84
- if (isDisabled(config)) return undefined;
85
- return new SqliteMemoryStore({ dbPath: config?.dbPath || process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
86
- }
87
-
88
20
  const PORT = process.env.PORT || 3001;
89
21
 
90
22
  const defaultCompanionConfig = {
@@ -94,7 +26,9 @@ const defaultCompanionConfig = {
94
26
  voice: { provider: 'voicevox', speakerId: 1 },
95
27
  memory: { provider: 'sqlite' },
96
28
  knowledge: {
97
- provider: (process.env.SIDURI_KNOWLEDGE_PROVIDER as 'e-knowledge' | 'e-remote' | 'e-hub') || 'e-knowledge',
29
+ provider: (process.env.SIDURI_KNOWLEDGE_PROVIDER as any) || 'unified',
30
+ lifeDatabase: true,
31
+ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite',
98
32
  packPath: process.env.SIDURI_KNOWLEDGE_PACK || '',
99
33
  registryUrl: process.env.SIDURI_KNOWLEDGE_REGISTRY_URL || '',
100
34
  packId: process.env.SIDURI_KNOWLEDGE_PACK_ID || '',
@@ -137,6 +71,7 @@ async function loadCompanionConfig() {
137
71
  if (process.env.SIDURI_KNOWLEDGE_REGISTRY_URL) config.knowledge.registryUrl = process.env.SIDURI_KNOWLEDGE_REGISTRY_URL;
138
72
  if (process.env.SIDURI_KNOWLEDGE_PACK_ID) config.knowledge.packId = process.env.SIDURI_KNOWLEDGE_PACK_ID;
139
73
  if (process.env.SIDURI_KNOWLEDGE_MODE) config.knowledge.preferredMode = process.env.SIDURI_KNOWLEDGE_MODE;
74
+ if (process.env.SIDURI_KNOWLEDGE_DB_PATH) config.knowledge.dbPath = process.env.SIDURI_KNOWLEDGE_DB_PATH;
140
75
  return config;
141
76
  }
142
77
 
@@ -144,37 +79,12 @@ async function bootDefaultCompanion() {
144
79
  if (runtimes.has('default')) return;
145
80
  console.log("Booting default companion...");
146
81
  const config: any = await loadCompanionConfig();
147
-
148
- const brain = createBrain(config.brain);
149
- const memory = createMemory(config.memory);
150
- const voice = createVoice(config.voice);
151
- const knowledge = createKnowledge(config.knowledge);
82
+
152
83
  const vision = createVision(config.vision);
153
- const observation = new FixtureObservationOrgan(
154
- vision ?? { analyze: async () => JSON.stringify({ readings: [] }) },
155
- );
84
+ const observation = createObservation(vision);
156
85
  instance.setObservationOrgan(observation);
157
- const selfRepo = new SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
158
- const behavior = createBehavior(config.behavior);
159
- const body = createBody(config.body);
160
-
161
- if (memory && typeof (memory as any).runMigrations === 'function') {
162
- await (memory as any).runMigrations().catch((e: any) => console.warn("Migrations warning:", e.message));
163
- }
164
-
165
- const runtime = new SiduriRuntime('default', config as any, {
166
- brain,
167
- memory,
168
- voice,
169
- knowledge,
170
- vision,
171
- behavior,
172
- body,
173
- self: selfRepo,
174
- externalKnowledge: knowledge
175
- });
176
- await runtime.initialize();
177
86
 
87
+ const runtime = await bootCompanion('default', config, { observationOrgan: observation });
178
88
  runtimes.set('default', runtime);
179
89
  console.log("Default companion booted successfully.");
180
90
  }