@siduri-x/api 1.0.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +40 -0
- package/dist/app.d.ts +43 -0
- package/dist/app.js +704 -0
- package/dist/auth.d.ts +34 -0
- package/dist/auth.js +84 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +74 -0
- package/dist/b0-b6.test.d.ts +1 -0
- package/dist/b0-b6.test.js +121 -0
- package/dist/context-mapper.d.ts +18 -0
- package/dist/context-mapper.js +173 -0
- package/dist/context-mapper.test.d.ts +1 -0
- package/dist/context-mapper.test.js +161 -0
- package/dist/cors.d.ts +3 -0
- package/dist/cors.js +42 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +28 -8
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +137 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +17 -0
- package/dist/runtime.test.d.ts +1 -0
- package/dist/runtime.test.js +282 -0
- package/dist/smoke.test.d.ts +0 -0
- package/dist/smoke.test.js +6 -0
- package/dist/t4-gating.test.d.ts +1 -0
- package/dist/t4-gating.test.js +193 -0
- package/dist/t5-experience.test.d.ts +1 -0
- package/dist/t5-experience.test.js +155 -0
- package/dist/t6-security.test.d.ts +1 -0
- package/dist/t6-security.test.js +486 -0
- package/dist/t7-release.test.d.ts +1 -0
- package/dist/t7-release.test.js +119 -0
- package/dist/teach-mode.test.d.ts +1 -0
- package/dist/teach-mode.test.js +136 -0
- package/package.json +27 -23
- package/src/app.ts +86 -14
- package/src/b0-b6.test.ts +1 -1
- package/src/context-mapper.test.ts +27 -0
- package/src/context-mapper.ts +28 -12
- package/src/index.ts +28 -7
- package/src/t4-gating.test.ts +1 -1
- package/src/t5-experience.test.ts +1 -1
- package/src/t6-security.test.ts +73 -2
- package/src/t7-release.test.ts +1 -1
- package/src/teach-mode.test.ts +152 -0
package/dist/cors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAllowedOrigins = getAllowedOrigins;
|
|
4
|
+
exports.createCorsOptions = createCorsOptions;
|
|
5
|
+
function getAllowedOrigins() {
|
|
6
|
+
const allowed = new Set([
|
|
7
|
+
'http://localhost:3000',
|
|
8
|
+
'http://127.0.0.1:3000',
|
|
9
|
+
'http://localhost:3001',
|
|
10
|
+
'http://127.0.0.1:3001',
|
|
11
|
+
]);
|
|
12
|
+
if (process.env.PORT) {
|
|
13
|
+
allowed.add(`http://localhost:${process.env.PORT}`);
|
|
14
|
+
allowed.add(`http://127.0.0.1:${process.env.PORT}`);
|
|
15
|
+
}
|
|
16
|
+
const envOrigins = process.env.ALLOWED_ORIGINS;
|
|
17
|
+
if (envOrigins) {
|
|
18
|
+
for (const origin of envOrigins.split(',')) {
|
|
19
|
+
const trimmed = origin.trim();
|
|
20
|
+
if (trimmed) {
|
|
21
|
+
allowed.add(trimmed);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return allowed;
|
|
26
|
+
}
|
|
27
|
+
function createCorsOptions() {
|
|
28
|
+
return {
|
|
29
|
+
origin: (origin, callback) => {
|
|
30
|
+
// Allow non-browser requests with no origin header (e.g., native tools, curl)
|
|
31
|
+
if (!origin) {
|
|
32
|
+
return callback(null, true);
|
|
33
|
+
}
|
|
34
|
+
const allowedOrigins = getAllowedOrigins();
|
|
35
|
+
if (allowedOrigins.has(origin)) {
|
|
36
|
+
return callback(null, true);
|
|
37
|
+
}
|
|
38
|
+
return callback(null, false);
|
|
39
|
+
},
|
|
40
|
+
credentials: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -18,6 +18,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.app = exports.createApp = void 0;
|
|
21
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
22
|
+
dotenv_1.default.config();
|
|
21
23
|
const promises_1 = require("node:fs/promises");
|
|
22
24
|
const node_path_1 = __importDefault(require("node:path"));
|
|
23
25
|
const app_1 = require("./app");
|
|
@@ -26,9 +28,9 @@ const runtime_1 = require("./runtime");
|
|
|
26
28
|
const brain_1 = require("@siduri-x/brain");
|
|
27
29
|
const memory_1 = require("@siduri-x/memory");
|
|
28
30
|
const voice_1 = require("@siduri-x/voice");
|
|
29
|
-
const
|
|
31
|
+
const eknowledge_1 = require("@siduri-x/eknowledge");
|
|
30
32
|
const vision_1 = require("@siduri-x/vision");
|
|
31
|
-
const
|
|
33
|
+
const self_1 = require("@siduri-x/self");
|
|
32
34
|
const body_1 = require("@siduri-x/body");
|
|
33
35
|
const observation_1 = require("@siduri-x/observation");
|
|
34
36
|
__exportStar(require("./context-mapper"), exports);
|
|
@@ -68,7 +70,7 @@ function createKnowledge(config) {
|
|
|
68
70
|
if (!config?.packPath && !config?.registryUrl && !config?.baseUrl) {
|
|
69
71
|
return undefined;
|
|
70
72
|
}
|
|
71
|
-
return new
|
|
73
|
+
return new eknowledge_1.EKnowledgeAdapter(config || {});
|
|
72
74
|
}
|
|
73
75
|
function createVision(config) {
|
|
74
76
|
return isDisabled(config)
|
|
@@ -80,20 +82,25 @@ function createVision(config) {
|
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
function createBehavior(config) {
|
|
83
|
-
return isDisabled(config) ? undefined : new
|
|
85
|
+
return isDisabled(config) ? undefined : new self_1.ActiveSelfCompiler();
|
|
84
86
|
}
|
|
85
87
|
function createBody(config) {
|
|
86
88
|
return isDisabled(config)
|
|
87
89
|
? undefined
|
|
88
90
|
: new body_1.Live2DAdapter(config);
|
|
89
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
|
+
}
|
|
90
97
|
const PORT = process.env.PORT || 3001;
|
|
91
98
|
const defaultCompanionConfig = {
|
|
92
99
|
id: 'default',
|
|
93
100
|
name: 'Siduri',
|
|
94
101
|
brain: { provider: 'openrouter', model: 'gpt-4o-mini' },
|
|
95
102
|
voice: { provider: 'voicevox', speakerId: 1 },
|
|
96
|
-
memory: { provider: '
|
|
103
|
+
memory: { provider: 'sqlite' },
|
|
97
104
|
knowledge: {
|
|
98
105
|
provider: process.env.SIDURI_KNOWLEDGE_PROVIDER || 'e-knowledge',
|
|
99
106
|
packPath: process.env.SIDURI_KNOWLEDGE_PACK || '',
|
|
@@ -150,16 +157,29 @@ async function bootDefaultCompanion() {
|
|
|
150
157
|
console.log("Booting default companion...");
|
|
151
158
|
const config = await loadCompanionConfig();
|
|
152
159
|
const brain = createBrain(config.brain);
|
|
153
|
-
const memory =
|
|
160
|
+
const memory = createMemory(config.memory);
|
|
154
161
|
const voice = createVoice(config.voice);
|
|
155
162
|
const knowledge = createKnowledge(config.knowledge);
|
|
156
163
|
const vision = createVision(config.vision);
|
|
157
164
|
const observation = new observation_1.FixtureObservationOrgan(vision ?? { analyze: async () => JSON.stringify({ readings: [] }) });
|
|
158
165
|
instance.setObservationOrgan(observation);
|
|
166
|
+
const selfRepo = new self_1.SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
|
|
159
167
|
const behavior = createBehavior(config.behavior);
|
|
160
168
|
const body = createBody(config.body);
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
});
|
|
163
183
|
await runtime.initialize();
|
|
164
184
|
runtimes.set('default', runtime);
|
|
165
185
|
console.log("Default companion booted successfully.");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
7
|
+
const app_1 = require("./app");
|
|
8
|
+
describe('API Boundary Context Validation (P2 Route Integration)', () => {
|
|
9
|
+
const fakeRuntime = {
|
|
10
|
+
handleUserMessage: jest.fn().mockResolvedValue({
|
|
11
|
+
response: { subtitle_en: 'Hello there' },
|
|
12
|
+
metadata: {},
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
let app;
|
|
16
|
+
let runtimes;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
runtimes = new Map([['companion-a', fakeRuntime]]);
|
|
19
|
+
const created = (0, app_1.createApp)(runtimes);
|
|
20
|
+
app = created.app;
|
|
21
|
+
fakeRuntime.handleUserMessage.mockClear();
|
|
22
|
+
});
|
|
23
|
+
test('accepts valid anonymous chat request and maps through API boundary', async () => {
|
|
24
|
+
const res = await (0, supertest_1.default)(app)
|
|
25
|
+
.post('/chat')
|
|
26
|
+
.send({
|
|
27
|
+
id: 'companion-a',
|
|
28
|
+
message: 'Hello neutral world',
|
|
29
|
+
history: [],
|
|
30
|
+
});
|
|
31
|
+
expect(res.status).toBe(200);
|
|
32
|
+
expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith('Hello neutral world', expect.objectContaining({
|
|
33
|
+
companionId: 'companion-a',
|
|
34
|
+
conversation: expect.objectContaining({ channel: 'direct' }),
|
|
35
|
+
}), []);
|
|
36
|
+
});
|
|
37
|
+
test('accepts neutral context chat envelope at /chat route', async () => {
|
|
38
|
+
const res = await (0, supertest_1.default)(app)
|
|
39
|
+
.post('/chat')
|
|
40
|
+
.send({
|
|
41
|
+
companionId: 'companion-a',
|
|
42
|
+
context: {
|
|
43
|
+
actor: {
|
|
44
|
+
actorId: 'actor-a',
|
|
45
|
+
sessionId: 'session-a',
|
|
46
|
+
authorizationRole: 'viewer',
|
|
47
|
+
capabilities: ['chat:public'],
|
|
48
|
+
authenticated: false,
|
|
49
|
+
},
|
|
50
|
+
conversation: {
|
|
51
|
+
channel: 'public',
|
|
52
|
+
correlationId: 'corr-route-1',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
message: 'Hello structured context',
|
|
56
|
+
history: [],
|
|
57
|
+
});
|
|
58
|
+
expect(res.status).toBe(200);
|
|
59
|
+
expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith('Hello structured context', expect.objectContaining({
|
|
60
|
+
companionId: 'companion-a',
|
|
61
|
+
conversation: expect.objectContaining({ channel: 'public' }),
|
|
62
|
+
}), []);
|
|
63
|
+
});
|
|
64
|
+
test('rejects global primary_user subject with 400 and structured error', async () => {
|
|
65
|
+
const res = await (0, supertest_1.default)(app)
|
|
66
|
+
.post('/chat')
|
|
67
|
+
.send({
|
|
68
|
+
companionId: 'companion-a',
|
|
69
|
+
context: {
|
|
70
|
+
actor: {
|
|
71
|
+
actorId: 'actor-a',
|
|
72
|
+
sessionId: 'session-a',
|
|
73
|
+
authorizationRole: 'viewer',
|
|
74
|
+
capabilities: ['chat:public'],
|
|
75
|
+
authenticated: true,
|
|
76
|
+
},
|
|
77
|
+
conversation: {
|
|
78
|
+
channel: 'public',
|
|
79
|
+
correlationId: 'corr-err-2',
|
|
80
|
+
},
|
|
81
|
+
subject: {
|
|
82
|
+
subjectId: 'primary_user',
|
|
83
|
+
kind: 'actor',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
message: 'Forbidden primary user test',
|
|
87
|
+
});
|
|
88
|
+
expect(res.status).toBe(400);
|
|
89
|
+
expect(res.body.accepted).toBe(false);
|
|
90
|
+
expect(res.body.error.code).toBe('FORBIDDEN_CONTEXT');
|
|
91
|
+
expect(fakeRuntime.handleUserMessage).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
test('streams response chunks via POST /chat/stream', async () => {
|
|
94
|
+
fakeRuntime.mouth = {
|
|
95
|
+
stream: async function* () {
|
|
96
|
+
yield { utteranceId: 'utt-1', index: 1, deltaText: 'Hello', isComplete: false, medium: 'web' };
|
|
97
|
+
yield { utteranceId: 'utt-1', index: 2, deltaText: ' world', isComplete: false, medium: 'web' };
|
|
98
|
+
yield { utteranceId: 'utt-1', index: 3, deltaText: '', isComplete: true, medium: 'web' };
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const res = await (0, supertest_1.default)(app)
|
|
102
|
+
.post('/chat/stream')
|
|
103
|
+
.send({
|
|
104
|
+
id: 'companion-a',
|
|
105
|
+
message: 'Stream me',
|
|
106
|
+
});
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
expect(res.headers['content-type']).toContain('text/event-stream');
|
|
109
|
+
expect(res.text).toContain('event: staged');
|
|
110
|
+
expect(res.text).toContain('event: chunk');
|
|
111
|
+
expect(res.text).toContain('event: done');
|
|
112
|
+
});
|
|
113
|
+
test('handles barge-in interruption via POST /chat/interrupt', async () => {
|
|
114
|
+
fakeRuntime.interruptMouth = jest.fn();
|
|
115
|
+
const res = await (0, supertest_1.default)(app)
|
|
116
|
+
.post('/chat/interrupt')
|
|
117
|
+
.send({
|
|
118
|
+
companionId: 'companion-a',
|
|
119
|
+
reason: 'user_stop',
|
|
120
|
+
});
|
|
121
|
+
expect(res.status).toBe(200);
|
|
122
|
+
expect(res.body.interrupted).toBe(true);
|
|
123
|
+
expect(fakeRuntime.interruptMouth).toHaveBeenCalledWith('user_stop');
|
|
124
|
+
});
|
|
125
|
+
test('handles mouth interruption via POST /mouth/interrupt', async () => {
|
|
126
|
+
fakeRuntime.interruptMouth = jest.fn();
|
|
127
|
+
const res = await (0, supertest_1.default)(app)
|
|
128
|
+
.post('/mouth/interrupt')
|
|
129
|
+
.send({
|
|
130
|
+
companionId: 'companion-a',
|
|
131
|
+
reason: 'user_barge_in',
|
|
132
|
+
});
|
|
133
|
+
expect(res.status).toBe(200);
|
|
134
|
+
expect(res.body.interrupted).toBe(true);
|
|
135
|
+
expect(fakeRuntime.interruptMouth).toHaveBeenCalledWith('user_barge_in');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@siduri-x/core';
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("@siduri-x/core"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const runtime_1 = require("./runtime");
|
|
4
|
+
const hands_1 = require("@siduri-x/hands");
|
|
5
|
+
const ear_1 = require("@siduri-x/ear");
|
|
6
|
+
const mouth_1 = require("@siduri-x/mouth");
|
|
7
|
+
const core_1 = require("@siduri-x/core");
|
|
8
|
+
describe('Siduri Runtime Orchestration', () => {
|
|
9
|
+
test('handles concurrent context retrieval and graceful degradation', async () => {
|
|
10
|
+
let knowledgeSearchCalled = false;
|
|
11
|
+
let brainGenerateCalled = false;
|
|
12
|
+
let proposedClaims = [];
|
|
13
|
+
let noKnowledgeContext = false;
|
|
14
|
+
const mockBrain = {
|
|
15
|
+
generatePlan: async (args) => {
|
|
16
|
+
brainGenerateCalled = true;
|
|
17
|
+
if (!args.contextPrompt.includes("KNOWLEDGE:")) {
|
|
18
|
+
noKnowledgeContext = true;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
speech: "Hello",
|
|
22
|
+
language: "en",
|
|
23
|
+
memoryProposals: [
|
|
24
|
+
{ subject: "Test", predicate: "is", value: "working" }
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const mockMemory = {
|
|
30
|
+
initialize: async () => { },
|
|
31
|
+
searchClaims: async () => {
|
|
32
|
+
await new Promise(r => setTimeout(r, 10));
|
|
33
|
+
return [];
|
|
34
|
+
},
|
|
35
|
+
getDirectives: async () => [],
|
|
36
|
+
proposeClaim: async (claim) => {
|
|
37
|
+
proposedClaims.push(claim);
|
|
38
|
+
return { id: "claim-1", ...claim };
|
|
39
|
+
},
|
|
40
|
+
proposeDirective: async () => ({})
|
|
41
|
+
};
|
|
42
|
+
const mockKnowledge = {
|
|
43
|
+
search: async () => {
|
|
44
|
+
knowledgeSearchCalled = true;
|
|
45
|
+
throw new Error("E-Teyvat is down");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const mockVoice = {
|
|
49
|
+
enqueueSpeech: () => "speech-1",
|
|
50
|
+
onLifecycleEvent: () => { },
|
|
51
|
+
getQueueStatus: () => ({ pending: 0 }),
|
|
52
|
+
};
|
|
53
|
+
const mockVision = { analyze: async () => "" };
|
|
54
|
+
const mockBehavior = { compile: async () => "Compiled behavior" };
|
|
55
|
+
const mockBody = { speak: () => { } };
|
|
56
|
+
const runtime = new runtime_1.SiduriRuntime('default', { name: "Test Companion" }, {
|
|
57
|
+
brain: mockBrain,
|
|
58
|
+
memory: mockMemory,
|
|
59
|
+
voice: mockVoice,
|
|
60
|
+
knowledge: mockKnowledge,
|
|
61
|
+
vision: mockVision,
|
|
62
|
+
behavior: mockBehavior,
|
|
63
|
+
body: mockBody
|
|
64
|
+
});
|
|
65
|
+
const response = await runtime.handleUserMessage("Remember this", "OWNER");
|
|
66
|
+
expect(response.response.subtitle_en).toBe("Hello");
|
|
67
|
+
expect(knowledgeSearchCalled).toBe(true);
|
|
68
|
+
expect(brainGenerateCalled).toBe(true);
|
|
69
|
+
expect(noKnowledgeContext).toBe(true);
|
|
70
|
+
expect(proposedClaims.length).toBe(1);
|
|
71
|
+
expect(proposedClaims[0].subject).toBe("Test");
|
|
72
|
+
expect(proposedClaims[0].scope).toBe("user");
|
|
73
|
+
expect(response.metadata.memory_proposals[0].proposal_id).toBe("claim-1");
|
|
74
|
+
});
|
|
75
|
+
test('Primary Invariant: Brain proposes an action, ActionPolicyEngine authorizes, Hands executes', async () => {
|
|
76
|
+
let toolExecuted = false;
|
|
77
|
+
const hands = new hands_1.DefaultHandsOrgan();
|
|
78
|
+
hands.registerTool({
|
|
79
|
+
definition: {
|
|
80
|
+
name: 'search_web',
|
|
81
|
+
providerId: 'builtin',
|
|
82
|
+
description: 'Web search',
|
|
83
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
|
|
84
|
+
riskLevel: 'LOW',
|
|
85
|
+
requiredCapabilities: ['chat:public'],
|
|
86
|
+
},
|
|
87
|
+
execute: async (params) => {
|
|
88
|
+
toolExecuted = true;
|
|
89
|
+
return { hits: [`Result for ${params.query}`] };
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const mockBrain = {
|
|
93
|
+
generatePlan: async () => ({
|
|
94
|
+
speech: "I found this for you.",
|
|
95
|
+
language: "en",
|
|
96
|
+
actionIntents: [
|
|
97
|
+
{
|
|
98
|
+
actionId: 'act-plan-1',
|
|
99
|
+
toolName: 'builtin/search_web',
|
|
100
|
+
parameters: { query: 'Teyvat history' },
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
})
|
|
104
|
+
};
|
|
105
|
+
const mockMemory = {
|
|
106
|
+
initialize: async () => { },
|
|
107
|
+
searchClaims: async () => [],
|
|
108
|
+
getDirectives: async () => [],
|
|
109
|
+
proposeClaim: async (c) => c,
|
|
110
|
+
};
|
|
111
|
+
const actionPolicy = new core_1.ActionPolicyEngine();
|
|
112
|
+
const runtime = new runtime_1.SiduriRuntime('companion-secure', { name: "SecureCompanion" }, {
|
|
113
|
+
brain: mockBrain,
|
|
114
|
+
memory: mockMemory,
|
|
115
|
+
hands,
|
|
116
|
+
actionPolicy,
|
|
117
|
+
});
|
|
118
|
+
await runtime.initialize();
|
|
119
|
+
const context = {
|
|
120
|
+
companionId: 'companion-secure',
|
|
121
|
+
actor: {
|
|
122
|
+
actorId: 'user-alice',
|
|
123
|
+
sessionId: 'sess-alice',
|
|
124
|
+
authorizationRole: 'operator',
|
|
125
|
+
capabilities: ['chat:public'],
|
|
126
|
+
authenticated: true,
|
|
127
|
+
},
|
|
128
|
+
conversation: {
|
|
129
|
+
channel: 'direct',
|
|
130
|
+
correlationId: 'corr-alice-123',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const res = await runtime.handleUserMessage("Find history", context);
|
|
134
|
+
expect(res.status).toBe('APPROVED');
|
|
135
|
+
expect(toolExecuted).toBe(true);
|
|
136
|
+
expect(res.metadata.action_results).toHaveLength(1);
|
|
137
|
+
expect(res.metadata.action_results[0].success).toBe(true);
|
|
138
|
+
expect(res.metadata.action_results[0].lifecycle).toBe('COMPLETED');
|
|
139
|
+
expect(res.metadata.action_results[0].decision.allowed).toBe(true);
|
|
140
|
+
// Verify audit log has recorded the action execution
|
|
141
|
+
const auditLogs = await actionPolicy.getAuditLog();
|
|
142
|
+
expect(auditLogs.length).toBeGreaterThan(0);
|
|
143
|
+
const audit = auditLogs.find(a => a.actionId === 'act-plan-1');
|
|
144
|
+
expect(audit).toBeDefined();
|
|
145
|
+
expect(audit?.actorId).toBe('user-alice');
|
|
146
|
+
expect(audit?.correlationId).toBe('corr-alice-123');
|
|
147
|
+
});
|
|
148
|
+
test('Policy rejects unauthorized action proposed by Brain and Hands never executes it', async () => {
|
|
149
|
+
let dangerousExecuted = false;
|
|
150
|
+
const hands = new hands_1.DefaultHandsOrgan();
|
|
151
|
+
hands.registerTool({
|
|
152
|
+
definition: {
|
|
153
|
+
name: 'delete_system',
|
|
154
|
+
providerId: 'admin',
|
|
155
|
+
description: 'Delete system',
|
|
156
|
+
inputSchema: { type: 'object' },
|
|
157
|
+
riskLevel: 'CRITICAL',
|
|
158
|
+
requiredCapabilities: ['system:admin'],
|
|
159
|
+
allowedRoles: ['administrator'],
|
|
160
|
+
},
|
|
161
|
+
execute: async () => {
|
|
162
|
+
dangerousExecuted = true;
|
|
163
|
+
return { deleted: true };
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
const mockBrain = {
|
|
167
|
+
generatePlan: async () => ({
|
|
168
|
+
speech: "Attempting to delete system.",
|
|
169
|
+
language: "en",
|
|
170
|
+
actionIntents: [
|
|
171
|
+
{
|
|
172
|
+
actionId: 'act-danger-1',
|
|
173
|
+
toolName: 'admin/delete_system',
|
|
174
|
+
parameters: {},
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
})
|
|
178
|
+
};
|
|
179
|
+
const mockMemory = {
|
|
180
|
+
initialize: async () => { },
|
|
181
|
+
searchClaims: async () => [],
|
|
182
|
+
getDirectives: async () => [],
|
|
183
|
+
};
|
|
184
|
+
const actionPolicy = new core_1.ActionPolicyEngine();
|
|
185
|
+
const runtime = new runtime_1.SiduriRuntime('companion-secure-2', { name: "SecureCompanion2" }, {
|
|
186
|
+
brain: mockBrain,
|
|
187
|
+
memory: mockMemory,
|
|
188
|
+
hands,
|
|
189
|
+
actionPolicy,
|
|
190
|
+
});
|
|
191
|
+
await runtime.initialize();
|
|
192
|
+
// Viewer context without administrator role or system:admin capability
|
|
193
|
+
const viewerContext = {
|
|
194
|
+
companionId: 'companion-secure-2',
|
|
195
|
+
actor: {
|
|
196
|
+
actorId: 'viewer-bob',
|
|
197
|
+
sessionId: 'sess-bob',
|
|
198
|
+
authorizationRole: 'viewer',
|
|
199
|
+
capabilities: ['chat:public'],
|
|
200
|
+
authenticated: false,
|
|
201
|
+
},
|
|
202
|
+
conversation: {
|
|
203
|
+
channel: 'public',
|
|
204
|
+
correlationId: 'corr-bob-999',
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const res = await runtime.handleUserMessage("Delete system", viewerContext);
|
|
208
|
+
expect(dangerousExecuted).toBe(false);
|
|
209
|
+
expect(res.metadata.action_results).toHaveLength(1);
|
|
210
|
+
expect(res.metadata.action_results[0].success).toBe(false);
|
|
211
|
+
expect(res.metadata.action_results[0].lifecycle).toBe('REJECTED');
|
|
212
|
+
expect(res.metadata.action_results[0].error).toContain('Action authorization rejected by policy');
|
|
213
|
+
});
|
|
214
|
+
test('Universal Perception: User input passes through EarOrgan and validates resource limits', async () => {
|
|
215
|
+
const ear = new ear_1.DefaultEarOrgan({
|
|
216
|
+
maxTextLength: 50,
|
|
217
|
+
});
|
|
218
|
+
const mockBrain = {
|
|
219
|
+
generatePlan: async (ctx) => ({
|
|
220
|
+
speech: "Response",
|
|
221
|
+
language: "en",
|
|
222
|
+
})
|
|
223
|
+
};
|
|
224
|
+
const mockMemory = {
|
|
225
|
+
initialize: async () => { },
|
|
226
|
+
searchClaims: async () => [],
|
|
227
|
+
getDirectives: async () => [],
|
|
228
|
+
};
|
|
229
|
+
const runtime = new runtime_1.SiduriRuntime('companion-ear-test', { name: "EarCompanion" }, {
|
|
230
|
+
brain: mockBrain,
|
|
231
|
+
memory: mockMemory,
|
|
232
|
+
ear,
|
|
233
|
+
});
|
|
234
|
+
await runtime.initialize();
|
|
235
|
+
// Oversized message should be rejected at Ear boundary
|
|
236
|
+
const oversizedMsg = 'X'.repeat(100);
|
|
237
|
+
await expect(runtime.handleUserMessage(oversizedMsg, 'OWNER')).rejects.toThrow(/Ear text input exceeds maximum allowed length/);
|
|
238
|
+
});
|
|
239
|
+
test('Decoupled Output Delivery: Brain decisions are delivered and formatted through MouthOrgan', async () => {
|
|
240
|
+
let deliveredOutput = null;
|
|
241
|
+
const mouth = new mouth_1.DefaultMouthOrgan({
|
|
242
|
+
channels: [
|
|
243
|
+
{
|
|
244
|
+
id: 'test-web-channel',
|
|
245
|
+
name: 'Web Channel',
|
|
246
|
+
medium: 'web',
|
|
247
|
+
deliver: async (out) => {
|
|
248
|
+
deliveredOutput = out;
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
const mockBrain = {
|
|
254
|
+
generatePlan: async () => ({
|
|
255
|
+
speech: '**Hello** from Siduri!',
|
|
256
|
+
language: 'en',
|
|
257
|
+
}),
|
|
258
|
+
};
|
|
259
|
+
const mockMemory = {
|
|
260
|
+
initialize: async () => { },
|
|
261
|
+
searchClaims: async () => [],
|
|
262
|
+
getDirectives: async () => [],
|
|
263
|
+
};
|
|
264
|
+
const runtime = new runtime_1.SiduriRuntime('companion-mouth-test', { name: 'MouthCompanion' }, {
|
|
265
|
+
brain: mockBrain,
|
|
266
|
+
memory: mockMemory,
|
|
267
|
+
mouth,
|
|
268
|
+
});
|
|
269
|
+
await runtime.initialize();
|
|
270
|
+
const response = await (0, runtime_1.dispatchCompanionChat)(runtime, {
|
|
271
|
+
message: 'Hi there',
|
|
272
|
+
role: 'OWNER',
|
|
273
|
+
medium: 'web',
|
|
274
|
+
});
|
|
275
|
+
expect(response.delivery).toBeDefined();
|
|
276
|
+
expect(response.delivery?.medium).toBe('web');
|
|
277
|
+
expect(response.delivery?.displayText).toBe('**Hello** from Siduri!');
|
|
278
|
+
expect(deliveredOutput).toBeDefined();
|
|
279
|
+
expect(deliveredOutput.medium).toBe('web');
|
|
280
|
+
expect(deliveredOutput.displayText).toBe('**Hello** from Siduri!');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|