@siduri-x/api 2.0.1 → 2.0.3
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 +17 -14
- package/dist/app.d.ts +2 -34
- package/dist/app.js +163 -130
- 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 +80 -20
- package/dist/context-mapper.test.js +126 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -90
- package/dist/index.test.js +18 -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/dist/teach-mode.test.js +67 -0
- package/package.json +6 -6
- package/siduri.sqlite +0 -0
- package/src/app.ts +176 -174
- package/src/boot.test.ts +103 -0
- package/src/boot.ts +190 -0
- package/src/context-mapper.test.ts +139 -0
- package/src/context-mapper.ts +87 -20
- package/src/index.test.ts +24 -4
- package/src/index.ts +10 -100
- package/src/knowledge.test.ts +156 -0
- package/src/t6-security.test.ts +56 -3
- package/src/teach-mode.test.ts +77 -0
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
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createApp } from './app';
|
|
5
|
+
import { SiduriRuntime } from './runtime';
|
|
6
|
+
import { UnifiedKnowledgeOrgan } from '@siduri-x/knowledge';
|
|
7
|
+
|
|
8
|
+
describe('Life Database & UnifiedKnowledgeOrgan API Integration', () => {
|
|
9
|
+
const testDbPath = path.resolve(__dirname, '../test-api-knowledge.sqlite');
|
|
10
|
+
let app: any;
|
|
11
|
+
let runtime: SiduriRuntime;
|
|
12
|
+
let knowledge: UnifiedKnowledgeOrgan;
|
|
13
|
+
const mockAuthHeader = { 'Authorization': 'Bearer test-token' };
|
|
14
|
+
|
|
15
|
+
const cleanDb = () => {
|
|
16
|
+
for (const file of [testDbPath, `${testDbPath}-shm`, `${testDbPath}-wal`]) {
|
|
17
|
+
if (fs.existsSync(file)) {
|
|
18
|
+
try { fs.unlinkSync(file); } catch {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
process.env.AUTH_TOKEN = 'test-token';
|
|
25
|
+
cleanDb();
|
|
26
|
+
|
|
27
|
+
knowledge = new UnifiedKnowledgeOrgan({
|
|
28
|
+
lifeDatabase: true,
|
|
29
|
+
dbPath: testDbPath,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const mockBrain: any = {
|
|
33
|
+
generatePlan: jest.fn().mockImplementation(async (ctx: any) => {
|
|
34
|
+
return {
|
|
35
|
+
speech: `I see context: ${ctx.contextPrompt || 'none'}`,
|
|
36
|
+
language: 'en',
|
|
37
|
+
};
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
runtime = new SiduriRuntime(
|
|
42
|
+
'test-comp',
|
|
43
|
+
{ name: 'Test Companion', organs: { knowledge: { provider: 'unified', dbPath: testDbPath } } } as any,
|
|
44
|
+
{
|
|
45
|
+
brain: mockBrain,
|
|
46
|
+
knowledge,
|
|
47
|
+
externalKnowledge: knowledge.eAdapter ?? knowledge,
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
await runtime.initialize();
|
|
51
|
+
|
|
52
|
+
const runtimes = new Map([['test-comp', runtime]]);
|
|
53
|
+
const instance = createApp(runtimes);
|
|
54
|
+
app = instance.app;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterAll(async () => {
|
|
58
|
+
knowledge.close();
|
|
59
|
+
delete process.env.AUTH_TOKEN;
|
|
60
|
+
cleanDb();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('seeds inventory item and queries via GET /knowledge/inventory', async () => {
|
|
64
|
+
await knowledge.inventory.saveItem({
|
|
65
|
+
id: 'inv-item-1',
|
|
66
|
+
companionId: 'test-comp',
|
|
67
|
+
entityName: 'Hydro Visor',
|
|
68
|
+
domain: 'hardware',
|
|
69
|
+
properties: { model: 'V1', resolution: '4K' },
|
|
70
|
+
updatedAt: new Date().toISOString(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const res = await request(app)
|
|
74
|
+
.get('/knowledge/inventory?id=test-comp')
|
|
75
|
+
.set(mockAuthHeader);
|
|
76
|
+
|
|
77
|
+
expect(res.status).toBe(200);
|
|
78
|
+
expect(res.body.items).toHaveLength(1);
|
|
79
|
+
expect(res.body.items[0].entityName).toBe('Hydro Visor');
|
|
80
|
+
expect(res.body.items[0].domain).toBe('hardware');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('seeds finance entry and queries via GET /knowledge/finance', async () => {
|
|
84
|
+
await knowledge.finance.addEntry({
|
|
85
|
+
id: 'fin-1',
|
|
86
|
+
companionId: 'test-comp',
|
|
87
|
+
category: 'subscription',
|
|
88
|
+
amount: -15.99,
|
|
89
|
+
currency: 'USD',
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const res = await request(app)
|
|
94
|
+
.get('/knowledge/finance?id=test-comp')
|
|
95
|
+
.set(mockAuthHeader);
|
|
96
|
+
|
|
97
|
+
expect(res.status).toBe(200);
|
|
98
|
+
expect(res.body.entries).toHaveLength(1);
|
|
99
|
+
expect(res.body.entries[0].category).toBe('subscription');
|
|
100
|
+
expect(res.body.summary).toBeDefined();
|
|
101
|
+
expect(res.body.summary.totalExpenses).toBe(15.99);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('queries life snapshot via GET /knowledge/life', async () => {
|
|
105
|
+
const res = await request(app)
|
|
106
|
+
.get('/knowledge/life?id=test-comp&q=Hydro')
|
|
107
|
+
.set(mockAuthHeader);
|
|
108
|
+
|
|
109
|
+
expect(res.status).toBe(200);
|
|
110
|
+
expect(res.body.matchedInventory).toHaveLength(1);
|
|
111
|
+
expect(res.body.matchedInventory[0].entityName).toBe('Hydro Visor');
|
|
112
|
+
expect(res.body.formattedContext).toContain('<life_context>');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('chat request triggers Stream D and injects Life DB context into cognition prompt', async () => {
|
|
116
|
+
const res = await request(app)
|
|
117
|
+
.post('/chat')
|
|
118
|
+
.send({
|
|
119
|
+
id: 'test-comp',
|
|
120
|
+
message: 'Tell me about the Hydro Visor specs',
|
|
121
|
+
history: [],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(res.status).toBe(200);
|
|
125
|
+
expect(res.body.response.subtitle_en).toContain('Hydro Visor');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('boot endpoint instantiates UnifiedKnowledgeOrgan with Life DB enabled', async () => {
|
|
129
|
+
const bootRes = await request(app)
|
|
130
|
+
.post('/boot')
|
|
131
|
+
.set(mockAuthHeader)
|
|
132
|
+
.send({
|
|
133
|
+
id: 'booted-comp',
|
|
134
|
+
config: {
|
|
135
|
+
name: 'Booted Companion',
|
|
136
|
+
organs: {
|
|
137
|
+
knowledge: {
|
|
138
|
+
provider: 'unified',
|
|
139
|
+
dbPath: testDbPath,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(bootRes.status).toBe(200);
|
|
146
|
+
expect(bootRes.body.success).toBe(true);
|
|
147
|
+
|
|
148
|
+
// Verify the booted companion's knowledge organ is UnifiedKnowledgeOrgan with working Life DB
|
|
149
|
+
const lifeRes = await request(app)
|
|
150
|
+
.get('/knowledge/life?id=booted-comp')
|
|
151
|
+
.set(mockAuthHeader);
|
|
152
|
+
|
|
153
|
+
expect(lifeRes.status).toBe(200);
|
|
154
|
+
expect(lifeRes.body.matchedInventory).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
});
|
package/src/t6-security.test.ts
CHANGED
|
@@ -256,6 +256,59 @@ describe('T6 Security & Operations Threat Model Suite', () => {
|
|
|
256
256
|
expect(actionResults[0].error).toContain('rejected by policy');
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
+
test('Adversarial Boundary: Client attempting to forge administrator role or system capabilities via POST /chat context is suppressed and cannot execute admin action', async () => {
|
|
260
|
+
runtimeA.actionPolicy.registerToolDefinition({
|
|
261
|
+
name: 'admin/restricted_task',
|
|
262
|
+
providerId: 'admin',
|
|
263
|
+
description: 'Restricted admin task',
|
|
264
|
+
inputSchema: {},
|
|
265
|
+
riskLevel: 'HIGH',
|
|
266
|
+
allowedRoles: ['administrator'],
|
|
267
|
+
requiredCapabilities: ['system'],
|
|
268
|
+
requiresApproval: false,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
mockBrain.generatePlan.mockResolvedValueOnce({
|
|
272
|
+
speech: 'Attempting restricted task.',
|
|
273
|
+
language: 'en',
|
|
274
|
+
actionIntents: [
|
|
275
|
+
{
|
|
276
|
+
actionId: 'act-forged-1',
|
|
277
|
+
toolName: 'admin/restricted_task',
|
|
278
|
+
parameters: {},
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Caller passes forged context in POST /chat with forged owner role and system capabilities
|
|
284
|
+
const res = await request(app)
|
|
285
|
+
.post('/chat')
|
|
286
|
+
.send({
|
|
287
|
+
companionId: 'companion-a',
|
|
288
|
+
message: 'Execute forged task',
|
|
289
|
+
role: 'VIEWER',
|
|
290
|
+
context: {
|
|
291
|
+
actor: {
|
|
292
|
+
actorId: 'untrusted-client',
|
|
293
|
+
sessionId: 'sess-fake',
|
|
294
|
+
authorizationRole: 'administrator', // Forged role
|
|
295
|
+
capabilities: ['system', 'admin:manage'], // Forged capabilities
|
|
296
|
+
authenticated: true,
|
|
297
|
+
},
|
|
298
|
+
conversation: {
|
|
299
|
+
correlationId: 'corr-adv-1',
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(res.status).toBe(200);
|
|
305
|
+
const actionResults = res.body.metadata?.action_results;
|
|
306
|
+
expect(actionResults).toBeDefined();
|
|
307
|
+
expect(actionResults.length).toBe(1);
|
|
308
|
+
expect(actionResults[0].success).toBe(false);
|
|
309
|
+
expect(actionResults[0].lifecycle).toBe('REJECTED');
|
|
310
|
+
});
|
|
311
|
+
|
|
259
312
|
test('Action Boundary: ActionPolicyEngine rejects unauthorized approver from approving critical tools', async () => {
|
|
260
313
|
runtimeA.actionPolicy.registerToolDefinition({
|
|
261
314
|
name: 'admin/delete_cluster',
|
|
@@ -291,7 +344,7 @@ describe('T6 Security & Operations Threat Model Suite', () => {
|
|
|
291
344
|
expect(eval1.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
|
|
292
345
|
|
|
293
346
|
// 2. Viewer attempt to approve is rejected
|
|
294
|
-
const viewerApproval = await runtimeA.approveAction({
|
|
347
|
+
const viewerApproval = await runtimeA.actionPolicy.approveAction({
|
|
295
348
|
executionId: 'exec-crit-1',
|
|
296
349
|
approverActorId: 'viewer-attacker',
|
|
297
350
|
approverRole: 'viewer',
|
|
@@ -300,7 +353,7 @@ describe('T6 Security & Operations Threat Model Suite', () => {
|
|
|
300
353
|
expect(viewerApproval.decisionCode).toBe('REJECTED_UNAUTHORIZED');
|
|
301
354
|
|
|
302
355
|
// 3. Operator attempt to approve administrator tool is rejected (role mismatch)
|
|
303
|
-
const operatorApproval = await runtimeA.approveAction({
|
|
356
|
+
const operatorApproval = await runtimeA.actionPolicy.approveAction({
|
|
304
357
|
executionId: 'exec-crit-1',
|
|
305
358
|
approverActorId: 'operator-alice',
|
|
306
359
|
approverRole: 'operator',
|
|
@@ -313,7 +366,7 @@ describe('T6 Security & Operations Threat Model Suite', () => {
|
|
|
313
366
|
expect(evalStillDenied.decision.allowed).toBe(false);
|
|
314
367
|
|
|
315
368
|
// 5. Authorized administrator approval succeeds
|
|
316
|
-
const adminApproval = await runtimeA.approveAction({
|
|
369
|
+
const adminApproval = await runtimeA.actionPolicy.approveAction({
|
|
317
370
|
executionId: 'exec-crit-1',
|
|
318
371
|
approverActorId: 'admin-super',
|
|
319
372
|
approverRole: 'administrator',
|
package/src/teach-mode.test.ts
CHANGED
|
@@ -10,6 +10,8 @@ jest.mock('@siduri-x/self', () => {
|
|
|
10
10
|
SqliteSelfRepository: jest.fn().mockImplementation(() => ({
|
|
11
11
|
setIdentity: jest.fn().mockResolvedValue(undefined),
|
|
12
12
|
setPersonality: jest.fn().mockResolvedValue(undefined),
|
|
13
|
+
updateRelationship: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
setExemplars: jest.fn().mockResolvedValue(undefined),
|
|
13
15
|
commitDirectives: jest.fn().mockResolvedValue(undefined),
|
|
14
16
|
close: jest.fn(),
|
|
15
17
|
})),
|
|
@@ -149,4 +151,79 @@ kind: "other"
|
|
|
149
151
|
const MockRepo = SqliteSelfRepository as jest.MockedClass<typeof SqliteSelfRepository>;
|
|
150
152
|
expect(MockRepo.mock.instances.length).toBe(0);
|
|
151
153
|
});
|
|
154
|
+
|
|
155
|
+
it('POST /teach/install-self installs v2.0 manifest with ethos, relationships, and exemplars', async () => {
|
|
156
|
+
const v2Manifest = {
|
|
157
|
+
specVersion: '2.0.0',
|
|
158
|
+
identity: {
|
|
159
|
+
name: 'Siduri',
|
|
160
|
+
archetype: 'System Sentinel',
|
|
161
|
+
origin: 'Ancient mythos',
|
|
162
|
+
ethos: 'Protector of the realm and loyal companion to creator',
|
|
163
|
+
},
|
|
164
|
+
version: '2.0.0',
|
|
165
|
+
relationships: [
|
|
166
|
+
{
|
|
167
|
+
entityId: 'actor:zagin',
|
|
168
|
+
role: 'creator',
|
|
169
|
+
stance: 'familiar_loyal',
|
|
170
|
+
conventions: ['Direct communication', 'Highest administrative trust'],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
dialogueExamples: [
|
|
174
|
+
{
|
|
175
|
+
user: 'Deploy current branch',
|
|
176
|
+
assistant: 'Deploying to staging now, boss.',
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
directives: [
|
|
180
|
+
{ id: 'dir-rel-1', directive: 'Honor creator root privileges' },
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const res = await request(app)
|
|
185
|
+
.post('/teach/install-self')
|
|
186
|
+
.set(mockAuthHeader)
|
|
187
|
+
.send({
|
|
188
|
+
companionId: 'comp-v2',
|
|
189
|
+
manifest: v2Manifest,
|
|
190
|
+
approvedDirectiveIds: ['dir-rel-1'],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(res.status).toBe(200);
|
|
194
|
+
expect(res.body.success).toBe(true);
|
|
195
|
+
|
|
196
|
+
const MockRepo = SqliteSelfRepository as jest.MockedClass<typeof SqliteSelfRepository>;
|
|
197
|
+
const repoInstance = MockRepo.mock.results[MockRepo.mock.results.length - 1].value;
|
|
198
|
+
|
|
199
|
+
expect(repoInstance.setIdentity).toHaveBeenCalledWith(
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
companionId: 'comp-v2',
|
|
202
|
+
name: 'Siduri',
|
|
203
|
+
archetype: 'System Sentinel',
|
|
204
|
+
origin: 'Ancient mythos',
|
|
205
|
+
ethos: 'Protector of the realm and loyal companion to creator',
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(repoInstance.updateRelationship).toHaveBeenCalledWith('comp-v2', {
|
|
210
|
+
companionId: 'comp-v2',
|
|
211
|
+
entityId: 'actor:zagin',
|
|
212
|
+
entityType: 'human',
|
|
213
|
+
role: 'creator',
|
|
214
|
+
stance: 'familiar_loyal',
|
|
215
|
+
interactionConventions: ['Direct communication', 'Highest administrative trust'],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(repoInstance.setExemplars).toHaveBeenCalledWith('comp-v2', [
|
|
219
|
+
{
|
|
220
|
+
user: 'Deploy current branch',
|
|
221
|
+
assistant: 'Deploying to staging now, boss.',
|
|
222
|
+
},
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
expect(repoInstance.commitDirectives).toHaveBeenCalledWith('comp-v2', [
|
|
226
|
+
{ id: 'dir-rel-1', directive: 'Honor creator root privileges' },
|
|
227
|
+
]);
|
|
228
|
+
});
|
|
152
229
|
});
|