@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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +16 -13
- package/dist/app.d.ts +2 -34
- package/dist/app.js +143 -129
- package/dist/boot.d.ts +92 -0
- package/dist/boot.js +146 -0
- package/dist/boot.test.d.ts +1 -0
- package/dist/boot.test.js +84 -0
- package/dist/context-mapper.d.ts +3 -1
- package/dist/context-mapper.js +78 -20
- package/dist/context-mapper.test.js +91 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -90
- package/dist/index.test.js +4 -4
- package/dist/knowledge.test.d.ts +1 -0
- package/dist/knowledge.test.js +138 -0
- package/dist/t6-security.test.js +52 -3
- package/package.json +6 -6
- package/src/app.ts +154 -173
- package/src/boot.test.ts +103 -0
- package/src/boot.ts +190 -0
- package/src/context-mapper.test.ts +100 -0
- package/src/context-mapper.ts +85 -20
- package/src/index.test.ts +4 -4
- package/src/index.ts +10 -100
- package/src/knowledge.test.ts +156 -0
- package/src/t6-security.test.ts +56 -3
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
|
});
|
package/src/context-mapper.ts
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
:
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
7
|
+
import { createApp, AppInstance } from './app';
|
|
8
8
|
import { SiduriRuntime } from './runtime';
|
|
9
|
-
import {
|
|
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
|
|
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 =
|
|
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
|
}
|