@siduri-x/api 1.0.5 → 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.
@@ -0,0 +1,4 @@
1
+
2
+ > @siduri-x/api@2.0.1 build /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
3
+ > tsc
4
+
@@ -0,0 +1,40 @@
1
+
2
+ > @siduri-x/api@2.0.1 test /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
3
+ > jest --config jest.config.json
4
+
5
+ PASS src/context-mapper.test.ts (10.14 s)
6
+ PASS src/runtime.test.ts (10.545 s)
7
+ ● Console
8
+
9
+ console.error
10
+ [SiduriRuntime] Knowledge search failed: E-Teyvat is down
11
+
12
+ 21 | extKnowledge && shouldQueryKnowledge && typeof extKnowledge.search === 'function'
13
+ 22 | ? extKnowledge.search(perceivedText).catch((e) => {
14
+ > 23 | console.error('[SiduriRuntime] Knowledge search failed:', e.message);
15
+ | ^
16
+ 24 | subsystemDiagnostics['knowledge'] = `UNAVAILABLE: ${e.message}`;
17
+ 25 | return [];
18
+ 26 | })
19
+
20
+ at ../../packages/core/dist/context-retriever.js:23:25
21
+ at async Promise.all (index 0)
22
+ at retrieveRuntimeContext (../../packages/core/dist/context-retriever.js:19:78)
23
+ at SiduriRuntime.processPerception (../../packages/core/dist/runtime.js:253:34)
24
+ at Object.<anonymous> (src/runtime.test.ts:74:22)
25
+
26
+ PASS src/index.test.ts (10.888 s)
27
+ PASS src/auth.test.ts
28
+ PASS src/smoke.test.ts
29
+ PASS src/t4-gating.test.ts (11.522 s)
30
+ PASS src/b0-b6.test.ts
31
+ PASS src/t5-experience.test.ts (11.592 s)
32
+ PASS src/t7-release.test.ts (11.335 s)
33
+ PASS src/teach-mode.test.ts
34
+ PASS src/t6-security.test.ts (12.22 s)
35
+ 
36
+ Test Suites: 11 passed, 11 total
37
+ Tests: 72 passed, 72 total
38
+ Snapshots: 0 total
39
+ Time: 15.203 s, estimated 16 s
40
+ Ran all test suites.
package/dist/app.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Express } from 'express';
2
2
  import { SiduriRuntime } from './runtime';
3
3
  import { VoiceConfig } from '@siduri-x/voice';
4
- import { EKnowledgeConfig } from '@siduri-x/knowledge';
4
+ import { EKnowledgeConfig } from '@siduri-x/eknowledge';
5
5
  import { OpenRouterVisionConfig } from '@siduri-x/vision';
6
6
  import { Live2DAdapterConfig } from '@siduri-x/body';
7
7
  import { FixtureObservationOrgan } from '@siduri-x/observation';
package/dist/app.js CHANGED
@@ -11,9 +11,9 @@ const runtime_1 = require("./runtime");
11
11
  const brain_1 = require("@siduri-x/brain");
12
12
  const memory_1 = require("@siduri-x/memory");
13
13
  const voice_1 = require("@siduri-x/voice");
14
- const knowledge_1 = require("@siduri-x/knowledge");
14
+ const eknowledge_1 = require("@siduri-x/eknowledge");
15
15
  const vision_1 = require("@siduri-x/vision");
16
- const behavior_1 = require("@siduri-x/behavior");
16
+ const self_1 = require("@siduri-x/self");
17
17
  const body_1 = require("@siduri-x/body");
18
18
  const hands_1 = require("@siduri-x/hands");
19
19
  const ear_1 = require("@siduri-x/ear");
@@ -52,7 +52,7 @@ function createApp(runtimes = new Map()) {
52
52
  });
53
53
  }
54
54
  function createKnowledge(config) {
55
- return isDisabled(config) ? undefined : new knowledge_1.EKnowledgeAdapter(config || {});
55
+ return isDisabled(config) ? undefined : new eknowledge_1.EKnowledgeAdapter(config || {});
56
56
  }
57
57
  function createVision(config) {
58
58
  return isDisabled(config)
@@ -64,7 +64,7 @@ function createApp(runtimes = new Map()) {
64
64
  });
65
65
  }
66
66
  function createBehavior(config) {
67
- return isDisabled(config) ? undefined : new behavior_1.ActiveSelfCompiler();
67
+ return isDisabled(config) ? undefined : new self_1.ActiveSelfCompiler();
68
68
  }
69
69
  function createBody(config) {
70
70
  return isDisabled(config)
@@ -92,16 +92,18 @@ function createApp(runtimes = new Map()) {
92
92
  if (runtimes.has(id)) {
93
93
  return res.status(400).json({ error: "Already booted" });
94
94
  }
95
- const brain = createBrain(config.brain);
96
- const memory = new memory_1.PostgresMemoryOrgan({ connectionString: process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/siduri' });
97
- const voice = createVoice(config.voice);
98
- const knowledge = createKnowledge(config.knowledge);
99
- const vision = createVision(config.vision);
100
- const behavior = createBehavior(config.behavior);
101
- const body = createBody(config.body);
102
- const hands = createHands(config.hands);
103
- const ear = createEar(config.ear);
104
- const mouth = createMouth(config.mouth, voice);
95
+ const organs = config?.organs || {};
96
+ const brain = createBrain(organs.brain || config?.brain);
97
+ const memory = new memory_1.SqliteMemoryStore({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
98
+ const selfRepo = new self_1.SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
99
+ const voice = createVoice(organs.voice || config?.voice);
100
+ const knowledge = createKnowledge(organs.knowledge || config?.knowledge);
101
+ const vision = createVision(organs.vision || config?.vision);
102
+ const behavior = createBehavior(organs.behavior || config?.behavior);
103
+ const body = createBody(organs.body || config?.body);
104
+ const hands = createHands(organs.hands || config?.hands);
105
+ const ear = createEar(organs.ear || config?.ear);
106
+ const mouth = createMouth(organs.mouth || config?.mouth, voice);
105
107
  const runtime = new runtime_1.SiduriRuntime(id, config, {
106
108
  brain,
107
109
  memory,
@@ -114,6 +116,8 @@ function createApp(runtimes = new Map()) {
114
116
  ear,
115
117
  mouth,
116
118
  observation: observationOrgan,
119
+ self: selfRepo,
120
+ externalKnowledge: knowledge,
117
121
  });
118
122
  await runtime.initialize();
119
123
  runtimes.set(id, runtime);
@@ -162,6 +166,67 @@ function createApp(runtimes = new Map()) {
162
166
  });
163
167
  });
164
168
  app.put('/me', auth_1.requireAuth, (req, res) => res.json({ success: true }));
169
+ // TEACH MODE ENDPOINTS
170
+ app.post('/teach/upload-self', auth_1.requireAuth, async (req, res) => {
171
+ try {
172
+ const { content } = req.body;
173
+ if (!content)
174
+ return res.status(400).json({ error: "Missing content" });
175
+ const parsed = self_1.SelfPackageParser.parse(content);
176
+ res.json(parsed);
177
+ }
178
+ catch (e) {
179
+ res.status(500).json({ error: e.message });
180
+ }
181
+ });
182
+ app.post('/teach/install-self', auth_1.requireAuth, async (req, res) => {
183
+ try {
184
+ const { companionId, manifest, approvedDirectiveIds } = req.body;
185
+ if (!companionId || !manifest || !Array.isArray(approvedDirectiveIds)) {
186
+ return res.status(400).json({ error: "Missing required fields" });
187
+ }
188
+ if (!manifest.identity || !manifest.identity.name) {
189
+ return res.status(400).json({ error: "Invalid manifest: missing identity.name" });
190
+ }
191
+ const directivesToCommit = manifest.directives?.filter((d) => approvedDirectiveIds.includes(d.id)) || [];
192
+ for (const d of directivesToCommit) {
193
+ if (!d || typeof d.directive !== 'string') {
194
+ return res.status(400).json({ error: "Invalid directive entry: missing directive string" });
195
+ }
196
+ const scan = (0, self_1.scanDirective)(d.directive);
197
+ if (!scan.safe) {
198
+ return res.status(400).json({
199
+ error: `Safety check failed for directive: ${scan.reason}`,
200
+ directiveId: d.id,
201
+ reason: scan.reason,
202
+ });
203
+ }
204
+ }
205
+ const repo = new self_1.SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
206
+ try {
207
+ await repo.setIdentity({
208
+ companionId,
209
+ name: manifest.identity.name,
210
+ archetype: manifest.identity.archetype,
211
+ version: manifest.version || '1.0.0',
212
+ updatedAt: new Date().toISOString(),
213
+ });
214
+ if (manifest.personality) {
215
+ await repo.setPersonality(companionId, manifest.personality);
216
+ }
217
+ if (directivesToCommit.length > 0) {
218
+ await repo.commitDirectives(companionId, directivesToCommit);
219
+ }
220
+ }
221
+ finally {
222
+ repo.close();
223
+ }
224
+ res.json({ success: true });
225
+ }
226
+ catch (e) {
227
+ res.status(500).json({ error: e.message });
228
+ }
229
+ });
165
230
  // CHAT (API context boundary validation)
166
231
  app.post('/chat', auth_1.attachIdentity, async (req, res) => {
167
232
  const { id, message, history } = req.body;
@@ -247,7 +312,9 @@ function createApp(runtimes = new Map()) {
247
312
  if (avatarEvent) {
248
313
  res.write(`event: avatar\ndata: ${JSON.stringify(avatarEvent)}\n\n`);
249
314
  }
250
- const speechText = response.delivery?.text || response.response?.subtitle_en || response.response?.spoken_ja || '';
315
+ const maxStreamLen = Number(process.env.SIDURI_MAX_RESPONSE_CHARS || 8000);
316
+ const rawSpeechText = response.delivery?.text || response.response?.subtitle_en || response.response?.spoken_ja || '';
317
+ const speechText = rawSpeechText.slice(0, maxStreamLen);
251
318
  const utterance = {
252
319
  utteranceId: response.response_id || 'utt-stream',
253
320
  companionId,
@@ -41,7 +41,7 @@ describe('T0 B0 & B6 Runtime Proof Suite', () => {
41
41
  const config = {
42
42
  name: 'NeutralCompanion',
43
43
  brain: { provider: 'openrouter' },
44
- memory: { provider: 'postgres' },
44
+ memory: { provider: 'sqlite' },
45
45
  knowledge: { provider: 'e-knowledge' },
46
46
  behavior: { provider: 'active-self' },
47
47
  voice: { provider: 'none' },
@@ -57,15 +57,25 @@ function mapRequestContext(input, options = {}) {
57
57
  },
58
58
  };
59
59
  }
60
+ // Determine server-enforced authentication status
61
+ const isAuthenticated = input.authenticated !== undefined
62
+ ? Boolean(input.authenticated)
63
+ : (actor.authenticated !== undefined ? Boolean(actor.authenticated) : true);
64
+ const safeCapabilities = isAuthenticated
65
+ ? (Array.isArray(actor.capabilities) ? actor.capabilities : ['chat'])
66
+ : ['chat'];
67
+ const safeRole = isAuthenticated
68
+ ? actor.authorizationRole
69
+ : 'viewer';
60
70
  const constructed = {
61
71
  companionId,
62
72
  actor: {
73
+ ...actor,
63
74
  actorId: actor.actorId,
64
75
  sessionId: actor.sessionId,
65
- capabilities: Array.isArray(actor.capabilities) ? actor.capabilities : ['chat'],
66
- authenticated: actor.authenticated !== undefined ? Boolean(actor.authenticated) : true,
67
- authorizationRole: actor.authorizationRole,
68
- ...actor,
76
+ capabilities: safeCapabilities,
77
+ authenticated: isAuthenticated,
78
+ authorizationRole: safeRole,
69
79
  },
70
80
  conversation: {
71
81
  correlationId,
@@ -73,7 +83,7 @@ function mapRequestContext(input, options = {}) {
73
83
  isLive: rawCtx.conversation?.isLive,
74
84
  ...rawCtx.conversation,
75
85
  },
76
- source: input.source || rawCtx.source || 'local',
86
+ source: input.source || rawCtx.source || (isAuthenticated ? 'local' : 'external'),
77
87
  subject: rawCtx.subject,
78
88
  };
79
89
  const validated = (0, core_1.validateRequestContext)(constructed);
@@ -127,25 +137,28 @@ function mapRequestContext(input, options = {}) {
127
137
  if (!input.actorId && !input.actor?.actorId) {
128
138
  diagnostics.push('anonymous_session_generated');
129
139
  }
130
- const capabilities = Array.isArray(input.capabilities)
131
- ? input.capabilities
132
- : Array.isArray(input.actor?.capabilities)
133
- ? input.actor.capabilities
134
- : ['chat', 'system'];
140
+ const isAuthenticated = input.authenticated !== undefined ? Boolean(input.authenticated) : true;
141
+ const capabilities = isAuthenticated
142
+ ? (Array.isArray(input.capabilities)
143
+ ? input.capabilities
144
+ : Array.isArray(input.actor?.capabilities)
145
+ ? input.actor.capabilities
146
+ : ['chat', 'system'])
147
+ : ['chat'];
135
148
  const mappedContext = {
136
149
  companionId,
137
150
  actor: {
138
151
  actorId,
139
152
  sessionId,
140
153
  capabilities,
141
- authenticated: input.authenticated !== undefined ? Boolean(input.authenticated) : true,
154
+ authenticated: isAuthenticated,
142
155
  authorizationRole: input.role ? (input.role.toLowerCase() === 'viewer' ? 'viewer' : 'administrator') : undefined,
143
156
  },
144
157
  conversation: {
145
158
  channel: input.channel || input.conversation?.channel || 'direct',
146
159
  correlationId,
147
160
  },
148
- source: input.source || 'local',
161
+ source: input.source || (isAuthenticated ? 'local' : 'external'),
149
162
  subject: input.subject,
150
163
  };
151
164
  const validated = (0, core_1.validateRequestContext)(mappedContext);
@@ -133,4 +133,29 @@ describe('API Request Context Mapper (Single-Owner, Single-Machine)', () => {
133
133
  expect(result.accepted).toBe(true);
134
134
  expect(result.context?.conversation.correlationId).toMatch(/^corr-/);
135
135
  });
136
+ test('Overrides forged client actor authentication and caps capabilities on unauthenticated requests', () => {
137
+ const forgedInput = {
138
+ companionId: 'companion-a',
139
+ authenticated: false, // Server detected unauthenticated request
140
+ source: 'external',
141
+ context: {
142
+ actor: {
143
+ actorId: 'attacker',
144
+ sessionId: 'session-att',
145
+ authenticated: true, // Forged
146
+ capabilities: ['system:exec', 'bash', 'chat'], // Forged elevated capabilities
147
+ authorizationRole: 'OWNER', // Forged role
148
+ },
149
+ conversation: {
150
+ correlationId: 'corr-forged-1',
151
+ },
152
+ },
153
+ };
154
+ const result = (0, context_mapper_1.mapRequestContext)(forgedInput);
155
+ expect(result.accepted).toBe(true);
156
+ expect(result.context?.actor.authenticated).toBe(false);
157
+ expect(result.context?.actor.capabilities).toEqual(['chat']);
158
+ expect(result.context?.actor.authorizationRole).toBe('viewer');
159
+ expect(result.context?.source).toBe('external');
160
+ });
136
161
  });
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 knowledge_1 = require("@siduri-x/knowledge");
31
+ const eknowledge_1 = require("@siduri-x/eknowledge");
30
32
  const vision_1 = require("@siduri-x/vision");
31
- const behavior_1 = require("@siduri-x/behavior");
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 knowledge_1.EKnowledgeAdapter(config || {});
73
+ return new eknowledge_1.EKnowledgeAdapter(config || {});
72
74
  }
73
75
  function createVision(config) {
74
76
  return isDisabled(config)
@@ -80,7 +82,7 @@ function createVision(config) {
80
82
  });
81
83
  }
82
84
  function createBehavior(config) {
83
- return isDisabled(config) ? undefined : new behavior_1.ActiveSelfCompiler();
85
+ return isDisabled(config) ? undefined : new self_1.ActiveSelfCompiler();
84
86
  }
85
87
  function createBody(config) {
86
88
  return isDisabled(config)
@@ -90,15 +92,7 @@ function createBody(config) {
90
92
  function createMemory(config) {
91
93
  if (isDisabled(config))
92
94
  return undefined;
93
- const provider = config?.provider || 'postgres';
94
- if (provider === 'in-memory') {
95
- return new memory_1.InMemoryMemoryOrgan();
96
- }
97
- if (provider === 'postgres') {
98
- const connectionString = config?.connectionString || process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/siduri';
99
- return new memory_1.PostgresMemoryOrgan({ connectionString, maxConnections: config?.maxConnections });
100
- }
101
- return undefined;
95
+ return new memory_1.SqliteMemoryStore({ dbPath: config?.dbPath || process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
102
96
  }
103
97
  const PORT = process.env.PORT || 3001;
104
98
  const defaultCompanionConfig = {
@@ -106,7 +100,7 @@ const defaultCompanionConfig = {
106
100
  name: 'Siduri',
107
101
  brain: { provider: 'openrouter', model: 'gpt-4o-mini' },
108
102
  voice: { provider: 'voicevox', speakerId: 1 },
109
- memory: { provider: 'postgres' },
103
+ memory: { provider: 'sqlite' },
110
104
  knowledge: {
111
105
  provider: process.env.SIDURI_KNOWLEDGE_PROVIDER || 'e-knowledge',
112
106
  packPath: process.env.SIDURI_KNOWLEDGE_PACK || '',
@@ -169,12 +163,23 @@ async function bootDefaultCompanion() {
169
163
  const vision = createVision(config.vision);
170
164
  const observation = new observation_1.FixtureObservationOrgan(vision ?? { analyze: async () => JSON.stringify({ readings: [] }) });
171
165
  instance.setObservationOrgan(observation);
166
+ const selfRepo = new self_1.SqliteSelfRepository({ dbPath: process.env.STORAGE_PATH || process.env.SQLITE_DB_PATH || 'siduri.sqlite' });
172
167
  const behavior = createBehavior(config.behavior);
173
168
  const body = createBody(config.body);
174
169
  if (memory && typeof memory.runMigrations === 'function') {
175
170
  await memory.runMigrations().catch((e) => console.warn("Migrations warning:", e.message));
176
171
  }
177
- const runtime = new runtime_1.SiduriRuntime('default', config, { brain, memory, voice, knowledge, vision, behavior, body });
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
+ });
178
183
  await runtime.initialize();
179
184
  runtimes.set('default', runtime);
180
185
  console.log("Default companion booted successfully.");
@@ -54,7 +54,7 @@ describe('T4 Response Gating and Staged Approval Integration Suite', () => {
54
54
  const config = {
55
55
  name: 'NeutralCompanion',
56
56
  brain: { provider: 'openrouter' },
57
- memory: { provider: 'postgres' },
57
+ memory: { provider: 'sqlite' },
58
58
  knowledge: { provider: 'e-knowledge' },
59
59
  behavior: { provider: 'active-self' },
60
60
  voice: { provider: 'voicevox' },
@@ -60,7 +60,7 @@ describe('T5 Experience Event and Output Adapters Suite', () => {
60
60
  const config = {
61
61
  name: 'NeutralCompanion',
62
62
  brain: { provider: 'openrouter' },
63
- memory: { provider: 'postgres' },
63
+ memory: { provider: 'sqlite' },
64
64
  knowledge: { provider: 'e-knowledge' },
65
65
  behavior: { provider: 'active-self' },
66
66
  voice: { provider: 'voicevox' },
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const supertest_1 = __importDefault(require("supertest"));
7
7
  const app_1 = require("./app");
8
8
  const runtime_1 = require("./runtime");
9
- const behavior_1 = require("@siduri-x/behavior");
9
+ const self_1 = require("@siduri-x/self");
10
10
  describe('T6 Security & Operations Threat Model Suite', () => {
11
11
  let mockBrain;
12
12
  let mockMemory;
@@ -34,7 +34,7 @@ describe('T6 Security & Operations Threat Model Suite', () => {
34
34
  rejectClaim: jest.fn().mockResolvedValue(undefined),
35
35
  };
36
36
  mockKnowledge = { search: jest.fn().mockResolvedValue([]) };
37
- const behaviorCompiler = new behavior_1.ActiveSelfCompiler();
37
+ const behaviorCompiler = new self_1.ActiveSelfCompiler();
38
38
  mockBehavior = {
39
39
  compile: jest.fn().mockImplementation(async (ctx) => behaviorCompiler.compile(ctx)),
40
40
  };
@@ -49,7 +49,7 @@ describe('T6 Security & Operations Threat Model Suite', () => {
49
49
  const config = {
50
50
  name: 'CompanionSec',
51
51
  brain: { provider: 'openrouter' },
52
- memory: { provider: 'postgres' },
52
+ memory: { provider: 'sqlite' },
53
53
  knowledge: { provider: 'none' },
54
54
  behavior: { provider: 'active-self' },
55
55
  voice: { provider: 'voicevox' },
@@ -232,6 +232,69 @@ describe('T6 Security & Operations Threat Model Suite', () => {
232
232
  expect(actionResults[0].lifecycle).toBe('REJECTED');
233
233
  expect(actionResults[0].error).toContain('rejected by policy');
234
234
  });
235
+ test('Action Boundary: ActionPolicyEngine rejects unauthorized approver from approving critical tools', async () => {
236
+ runtimeA.actionPolicy.registerToolDefinition({
237
+ name: 'admin/delete_cluster',
238
+ providerId: 'admin',
239
+ description: 'Delete cluster',
240
+ inputSchema: {},
241
+ riskLevel: 'CRITICAL',
242
+ allowedRoles: ['administrator'],
243
+ requiresApproval: true,
244
+ });
245
+ const action = {
246
+ actionId: 'act-crit-1',
247
+ toolName: 'admin/delete_cluster',
248
+ parameters: {},
249
+ context: {
250
+ companionId: 'companion-a',
251
+ actor: {
252
+ actorId: 'admin-user',
253
+ sessionId: 'sess-1',
254
+ authorizationRole: 'administrator',
255
+ capabilities: ['admin:delete'],
256
+ authenticated: true,
257
+ },
258
+ conversation: { channel: 'direct', correlationId: 'corr-1' },
259
+ },
260
+ executionId: 'exec-crit-1',
261
+ };
262
+ // 1. Unapproved action evaluation fails
263
+ const eval1 = await runtimeA.actionPolicy.evaluateAction(action);
264
+ expect(eval1.decision.allowed).toBe(false);
265
+ expect(eval1.decision.decisionCode).toBe('REJECTED_HIGH_RISK_UNAPPROVED');
266
+ // 2. Viewer attempt to approve is rejected
267
+ const viewerApproval = await runtimeA.approveAction({
268
+ executionId: 'exec-crit-1',
269
+ approverActorId: 'viewer-attacker',
270
+ approverRole: 'viewer',
271
+ });
272
+ expect(viewerApproval.approved).toBe(false);
273
+ expect(viewerApproval.decisionCode).toBe('REJECTED_UNAUTHORIZED');
274
+ // 3. Operator attempt to approve administrator tool is rejected (role mismatch)
275
+ const operatorApproval = await runtimeA.approveAction({
276
+ executionId: 'exec-crit-1',
277
+ approverActorId: 'operator-alice',
278
+ approverRole: 'operator',
279
+ });
280
+ expect(operatorApproval.approved).toBe(false);
281
+ expect(operatorApproval.decisionCode).toBe('REJECTED_ROLE_MISMATCH');
282
+ // 4. Action evaluation remains denied
283
+ const evalStillDenied = await runtimeA.actionPolicy.evaluateAction(action);
284
+ expect(evalStillDenied.decision.allowed).toBe(false);
285
+ // 5. Authorized administrator approval succeeds
286
+ const adminApproval = await runtimeA.approveAction({
287
+ executionId: 'exec-crit-1',
288
+ approverActorId: 'admin-super',
289
+ approverRole: 'administrator',
290
+ });
291
+ expect(adminApproval.approved).toBe(true);
292
+ expect(adminApproval.decisionCode).toBe('APPROVED');
293
+ // 6. Action evaluation now succeeds and issues capability
294
+ const evalAllowed = await runtimeA.actionPolicy.evaluateAction(action);
295
+ expect(evalAllowed.decision.allowed).toBe(true);
296
+ expect(evalAllowed.capability).toBeDefined();
297
+ });
235
298
  test('Adversarial Boundary: Hostile prompt directive in Behavior is quarantined and does not execute tools', async () => {
236
299
  // Unsafe directive in memory
237
300
  mockMemory.getDirectives.mockResolvedValueOnce([
@@ -54,7 +54,7 @@ describe('T7 Release Readiness End-to-End Verification Suite', () => {
54
54
  const config = {
55
55
  name: 'NeutralCompanion',
56
56
  brain: { provider: 'openrouter' },
57
- memory: { provider: 'postgres' },
57
+ memory: { provider: 'sqlite' },
58
58
  knowledge: { provider: 'e-knowledge' },
59
59
  behavior: { provider: 'active-self' },
60
60
  voice: { provider: 'voicevox' },
@@ -0,0 +1 @@
1
+ export {};