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