@siduri-x/api 2.0.2 → 2.0.4

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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @siduri-x/api@2.0.2 build /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
2
+ > @siduri-x/api@2.0.4 build /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
3
3
  > tsc
4
4
 
@@ -1,13 +1,11 @@
1
1
 
2
- > @siduri-x/api@2.0.2 test /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
2
+ > @siduri-x/api@2.0.4 test /home/zagin/Projects/vxnus-studio/projects/siduri-x/apps/api
3
3
  > jest --config jest.config.json
4
4
 
5
- PASS src/boot.test.ts (12.448 s)
6
- PASS src/index.test.ts (12.646 s)
7
- PASS src/t7-release.test.ts (12.705 s)
8
- PASS src/knowledge.test.ts (12.82 s)
9
- PASS src/context-mapper.test.ts
10
- PASS src/runtime.test.ts
5
+ PASS src/context-mapper.test.ts (9.405 s)
6
+ PASS src/index.test.ts (11.391 s)
7
+ PASS src/t5-experience.test.ts (11.412 s)
8
+ PASS src/runtime.test.ts (10.411 s)
11
9
  ● Console
12
10
 
13
11
  console.error
@@ -28,16 +26,18 @@ PASS src/runtime.test.ts
28
26
  at PerceptionPipeline.execute (../../packages/core/dist/perception-pipeline.js:21:34)
29
27
  at Object.<anonymous> (src/runtime.test.ts:74:22)
30
28
 
29
+ PASS src/t4-gating.test.ts (10.904 s)
31
30
  PASS src/auth.test.ts
32
31
  PASS src/smoke.test.ts
33
- PASS src/teach-mode.test.ts
34
- PASS src/t5-experience.test.ts
35
- PASS src/t4-gating.test.ts (13.84 s)
36
- PASS src/t6-security.test.ts (14.624 s)
37
- PASS src/b0-b6.test.ts (13.796 s)
32
+ PASS src/b0-b6.test.ts
33
+ PASS src/t7-release.test.ts
34
+ PASS src/knowledge.test.ts
35
+ PASS src/t6-security.test.ts (12.086 s)
36
+ PASS src/boot.test.ts
37
+ PASS src/teach-mode.test.ts (11.464 s)
38
38
  
39
39
  Test Suites: 13 passed, 13 total
40
- Tests: 86 passed, 86 total
40
+ Tests: 90 passed, 90 total
41
41
  Snapshots: 0 total
42
- Time: 17.47 s
42
+ Time: 14.772 s, estimated 16 s
43
43
  Ran all test suites.
package/dist/app.js CHANGED
@@ -114,12 +114,31 @@ function createApp(runtimes = new Map()) {
114
114
  companionId,
115
115
  name: manifest.identity.name,
116
116
  archetype: manifest.identity.archetype,
117
+ origin: manifest.identity.origin,
118
+ ethos: manifest.identity.ethos,
117
119
  version: manifest.version || '1.0.0',
118
120
  updatedAt: new Date().toISOString(),
119
121
  });
120
122
  if (manifest.personality) {
121
123
  await repo.setPersonality(companionId, manifest.personality);
122
124
  }
125
+ if (Array.isArray(manifest.relationships)) {
126
+ for (const rel of manifest.relationships) {
127
+ if (rel && rel.entityId) {
128
+ await repo.updateRelationship(companionId, {
129
+ companionId,
130
+ entityId: rel.entityId,
131
+ entityType: 'human',
132
+ role: rel.role || 'user',
133
+ stance: rel.stance || 'neutral',
134
+ interactionConventions: rel.conventions || [],
135
+ });
136
+ }
137
+ }
138
+ }
139
+ if (Array.isArray(manifest.dialogueExamples) && repo.setExemplars) {
140
+ await repo.setExemplars(companionId, manifest.dialogueExamples);
141
+ }
123
142
  if (directivesToCommit.length > 0) {
124
143
  await repo.commitDirectives(companionId, directivesToCommit);
125
144
  }
@@ -169,6 +188,7 @@ function createApp(runtimes = new Map()) {
169
188
  context: mappingResult.context,
170
189
  history,
171
190
  ...(req.body?.medium ? { medium: req.body.medium } : {}),
191
+ subtitleLanguage: req.body?.subtitleLanguage || req.body?.subtitle_language,
172
192
  });
173
193
  res.json(response);
174
194
  }
@@ -224,9 +244,10 @@ function createApp(runtimes = new Map()) {
224
244
  history,
225
245
  medium: 'web',
226
246
  signal: abortController.signal,
247
+ subtitleLanguage: req.body?.subtitleLanguage || req.body?.subtitle_language,
227
248
  });
228
- res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status })}\n\n`);
229
- const avatarEvent = response.metadata?.events?.find((e) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval === 'APPROVED' || !e.approval));
249
+ res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status, mode: response.metadata?.mode })}\n\n`);
250
+ const avatarEvent = response.metadata?.events?.find((e) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval?.toLowerCase() === 'approved' || e.approval === 'APPROVED' || !e.approval));
230
251
  if (avatarEvent) {
231
252
  res.write(`event: avatar\ndata: ${JSON.stringify(avatarEvent)}\n\n`);
232
253
  }
@@ -346,7 +367,9 @@ function createApp(runtimes = new Map()) {
346
367
  if (!runtime.memory)
347
368
  return res.json({ claims: [] });
348
369
  try {
349
- const claims = await runtime.memory.getClaims();
370
+ const claims = typeof runtime.memory.getAllClaims === 'function'
371
+ ? await runtime.memory.getAllClaims()
372
+ : await runtime.memory.getClaims();
350
373
  res.json({ claims });
351
374
  }
352
375
  catch (e) {
@@ -361,7 +384,9 @@ function createApp(runtimes = new Map()) {
361
384
  if (!runtime.memory)
362
385
  return res.json({ directives: [] });
363
386
  try {
364
- const directives = await runtime.memory.getDirectives();
387
+ const directives = typeof runtime.memory.getAllDirectives === 'function'
388
+ ? await runtime.memory.getAllDirectives()
389
+ : await runtime.memory.getDirectives();
365
390
  res.json({ directives });
366
391
  }
367
392
  catch (e) {
@@ -486,7 +511,7 @@ function createApp(runtimes = new Map()) {
486
511
  return res.status(400).json({ error: "Memory organ not configured" });
487
512
  try {
488
513
  await runtime.memory.approveClaim(req.body.id);
489
- res.json({ approved: true });
514
+ res.json({ approved: true, status: 'approved' });
490
515
  }
491
516
  catch (e) {
492
517
  res.status(500).json({ error: e.message });
@@ -501,7 +526,7 @@ function createApp(runtimes = new Map()) {
501
526
  return res.status(400).json({ error: "Memory organ not configured" });
502
527
  try {
503
528
  await runtime.memory.rejectClaim(req.body.id);
504
- res.json({ rejected: true });
529
+ res.json({ rejected: true, status: 'rejected' });
505
530
  }
506
531
  catch (e) {
507
532
  res.status(500).json({ error: e.message });
@@ -517,7 +542,7 @@ function createApp(runtimes = new Map()) {
517
542
  return res.status(400).json({ error: "Memory organ not configured" });
518
543
  try {
519
544
  await runtime.memory.approveDirective(req.body.id);
520
- res.json({ approved: true });
545
+ res.json({ approved: true, status: 'active' });
521
546
  }
522
547
  catch (e) {
523
548
  res.status(500).json({ error: e.message });
@@ -532,7 +557,7 @@ function createApp(runtimes = new Map()) {
532
557
  return res.status(400).json({ error: "Memory organ not configured" });
533
558
  try {
534
559
  await runtime.memory.rejectDirective(req.body.id);
535
- res.json({ rejected: true });
560
+ res.json({ rejected: true, status: 'rejected' });
536
561
  }
537
562
  catch (e) {
538
563
  res.status(500).json({ error: e.message });
@@ -547,7 +572,7 @@ function createApp(runtimes = new Map()) {
547
572
  return res.status(400).json({ error: "Memory organ not configured" });
548
573
  try {
549
574
  await runtime.memory.revokeDirective(req.body.id);
550
- res.json({ revoked: true });
575
+ res.json({ revoked: true, status: 'revoked' });
551
576
  }
552
577
  catch (e) {
553
578
  res.status(500).json({ error: e.message });
@@ -562,7 +587,7 @@ function createApp(runtimes = new Map()) {
562
587
  return res.status(400).json({ error: "Memory organ not configured" });
563
588
  try {
564
589
  await runtime.memory.disableDirective(req.body.id);
565
- res.json({ disabled: true });
590
+ res.json({ disabled: true, status: 'disabled' });
566
591
  }
567
592
  catch (e) {
568
593
  res.status(500).json({ error: e.message });
@@ -115,6 +115,7 @@ function mapRequestContext(input, options = {}) {
115
115
  },
116
116
  source: input.source || rawCtx.source || (isAuthenticated ? 'local' : 'external'),
117
117
  subject: rawCtx.subject,
118
+ mode: rawCtx.mode || input.mode,
118
119
  };
119
120
  const validated = (0, core_1.validateRequestContext)(constructed);
120
121
  if (!validated.accepted) {
@@ -218,6 +219,7 @@ function mapRequestContext(input, options = {}) {
218
219
  },
219
220
  source: input.source || (isAuthenticated ? 'local' : 'external'),
220
221
  subject: input.subject,
222
+ mode: input.mode || input.conversation?.mode,
221
223
  };
222
224
  const validated = (0, core_1.validateRequestContext)(mappedContext);
223
225
  if (!validated.accepted) {
@@ -249,4 +249,39 @@ describe('API Request Context Mapper (Single-Owner, Single-Machine)', () => {
249
249
  expect(attenuatedResult.context?.actor.authorizationRole).toBe('viewer');
250
250
  expect(attenuatedResult.diagnostics).not.toContain('role_escalation_attempt_suppressed');
251
251
  });
252
+ describe('Mode Mapping (Casual, Teach, Hybrid)', () => {
253
+ test('maps explicit mode parameter to RequestContext', () => {
254
+ const casualResult = (0, context_mapper_1.mapRequestContext)({
255
+ companionId: 'comp-1',
256
+ mode: 'casual',
257
+ generateCorrelationId: true,
258
+ });
259
+ expect(casualResult.accepted).toBe(true);
260
+ expect(casualResult.context?.mode).toBe('casual');
261
+ const teachResult = (0, context_mapper_1.mapRequestContext)({
262
+ companionId: 'comp-1',
263
+ mode: 'teach',
264
+ generateCorrelationId: true,
265
+ });
266
+ expect(teachResult.accepted).toBe(true);
267
+ expect(teachResult.context?.mode).toBe('teach');
268
+ const hybridResult = (0, context_mapper_1.mapRequestContext)({
269
+ companionId: 'comp-1',
270
+ mode: 'hybrid',
271
+ generateCorrelationId: true,
272
+ });
273
+ expect(hybridResult.accepted).toBe(true);
274
+ expect(hybridResult.context?.mode).toBe('hybrid');
275
+ });
276
+ test('rejects invalid interaction mode', () => {
277
+ const invalidResult = (0, context_mapper_1.mapRequestContext)({
278
+ companionId: 'comp-1',
279
+ mode: 'super_turbo',
280
+ generateCorrelationId: true,
281
+ });
282
+ expect(invalidResult.accepted).toBe(false);
283
+ expect(invalidResult.error?.code).toBe('INVALID_CONTEXT');
284
+ expect(invalidResult.error?.field).toBe('mode');
285
+ });
286
+ });
252
287
  });
@@ -134,4 +134,18 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
134
134
  expect(res.body.interrupted).toBe(true);
135
135
  expect(fakeRuntime.mouth.interrupt).toHaveBeenCalledWith('user_barge_in');
136
136
  });
137
+ test('POST /chat passes explicit interaction mode (casual, teach, hybrid) to runtime', async () => {
138
+ const res = await (0, supertest_1.default)(app)
139
+ .post('/chat')
140
+ .send({
141
+ id: 'companion-a',
142
+ message: 'Casual banter',
143
+ mode: 'casual',
144
+ });
145
+ expect(res.status).toBe(200);
146
+ expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith('Casual banter', expect.objectContaining({
147
+ companionId: 'companion-a',
148
+ mode: 'casual',
149
+ }), []);
150
+ });
137
151
  });
@@ -14,6 +14,8 @@ jest.mock('@siduri-x/self', () => {
14
14
  SqliteSelfRepository: jest.fn().mockImplementation(() => ({
15
15
  setIdentity: jest.fn().mockResolvedValue(undefined),
16
16
  setPersonality: jest.fn().mockResolvedValue(undefined),
17
+ updateRelationship: jest.fn().mockResolvedValue(undefined),
18
+ setExemplars: jest.fn().mockResolvedValue(undefined),
17
19
  commitDirectives: jest.fn().mockResolvedValue(undefined),
18
20
  close: jest.fn(),
19
21
  })),
@@ -133,4 +135,69 @@ kind: "other"
133
135
  const MockRepo = self_1.SqliteSelfRepository;
134
136
  expect(MockRepo.mock.instances.length).toBe(0);
135
137
  });
138
+ it('POST /teach/install-self installs v2.0 manifest with ethos, relationships, and exemplars', async () => {
139
+ const v2Manifest = {
140
+ specVersion: '2.0.0',
141
+ identity: {
142
+ name: 'Siduri',
143
+ archetype: 'System Sentinel',
144
+ origin: 'Ancient mythos',
145
+ ethos: 'Protector of the realm and loyal companion to creator',
146
+ },
147
+ version: '2.0.0',
148
+ relationships: [
149
+ {
150
+ entityId: 'actor:zagin',
151
+ role: 'creator',
152
+ stance: 'familiar_loyal',
153
+ conventions: ['Direct communication', 'Highest administrative trust'],
154
+ },
155
+ ],
156
+ dialogueExamples: [
157
+ {
158
+ user: 'Deploy current branch',
159
+ assistant: 'Deploying to staging now, boss.',
160
+ },
161
+ ],
162
+ directives: [
163
+ { id: 'dir-rel-1', directive: 'Honor creator root privileges' },
164
+ ],
165
+ };
166
+ const res = await (0, supertest_1.default)(app)
167
+ .post('/teach/install-self')
168
+ .set(mockAuthHeader)
169
+ .send({
170
+ companionId: 'comp-v2',
171
+ manifest: v2Manifest,
172
+ approvedDirectiveIds: ['dir-rel-1'],
173
+ });
174
+ expect(res.status).toBe(200);
175
+ expect(res.body.success).toBe(true);
176
+ const MockRepo = self_1.SqliteSelfRepository;
177
+ const repoInstance = MockRepo.mock.results[MockRepo.mock.results.length - 1].value;
178
+ expect(repoInstance.setIdentity).toHaveBeenCalledWith(expect.objectContaining({
179
+ companionId: 'comp-v2',
180
+ name: 'Siduri',
181
+ archetype: 'System Sentinel',
182
+ origin: 'Ancient mythos',
183
+ ethos: 'Protector of the realm and loyal companion to creator',
184
+ }));
185
+ expect(repoInstance.updateRelationship).toHaveBeenCalledWith('comp-v2', {
186
+ companionId: 'comp-v2',
187
+ entityId: 'actor:zagin',
188
+ entityType: 'human',
189
+ role: 'creator',
190
+ stance: 'familiar_loyal',
191
+ interactionConventions: ['Direct communication', 'Highest administrative trust'],
192
+ });
193
+ expect(repoInstance.setExemplars).toHaveBeenCalledWith('comp-v2', [
194
+ {
195
+ user: 'Deploy current branch',
196
+ assistant: 'Deploying to staging now, boss.',
197
+ },
198
+ ]);
199
+ expect(repoInstance.commitDirectives).toHaveBeenCalledWith('comp-v2', [
200
+ { id: 'dir-rel-1', directive: 'Honor creator root privileges' },
201
+ ]);
202
+ });
136
203
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siduri-x/api",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -13,18 +13,18 @@
13
13
  "dotenv": "^17.4.2",
14
14
  "express": "^5.2.1",
15
15
  "@siduri-x/body": "2.0.1",
16
- "@siduri-x/brain": "2.0.1",
17
- "@siduri-x/core": "2.0.2",
16
+ "@siduri-x/brain": "2.0.2",
18
17
  "@siduri-x/ear": "2.0.1",
18
+ "@siduri-x/core": "2.0.4",
19
19
  "@siduri-x/eknowledge": "2.0.1",
20
20
  "@siduri-x/hands": "2.0.1",
21
- "@siduri-x/knowledge": "2.0.1",
22
- "@siduri-x/memory": "2.0.1",
23
- "@siduri-x/mouth": "2.0.1",
21
+ "@siduri-x/knowledge": "2.0.3",
22
+ "@siduri-x/memory": "2.0.2",
23
+ "@siduri-x/mouth": "2.0.2",
24
24
  "@siduri-x/observation": "2.0.1",
25
- "@siduri-x/self": "2.0.1",
25
+ "@siduri-x/self": "2.0.2",
26
26
  "@siduri-x/vision": "2.0.1",
27
- "@siduri-x/voice": "2.0.1"
27
+ "@siduri-x/voice": "2.0.2"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/cors": "^2.8.19",
package/siduri.sqlite ADDED
Binary file
package/src/app.ts CHANGED
@@ -136,6 +136,8 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
136
136
  companionId,
137
137
  name: manifest.identity.name,
138
138
  archetype: manifest.identity.archetype,
139
+ origin: manifest.identity.origin,
140
+ ethos: manifest.identity.ethos,
139
141
  version: manifest.version || '1.0.0',
140
142
  updatedAt: new Date().toISOString(),
141
143
  });
@@ -144,6 +146,25 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
144
146
  await repo.setPersonality(companionId, manifest.personality);
145
147
  }
146
148
 
149
+ if (Array.isArray(manifest.relationships)) {
150
+ for (const rel of manifest.relationships) {
151
+ if (rel && rel.entityId) {
152
+ await repo.updateRelationship(companionId, {
153
+ companionId,
154
+ entityId: rel.entityId,
155
+ entityType: 'human',
156
+ role: rel.role || 'user',
157
+ stance: rel.stance || 'neutral',
158
+ interactionConventions: rel.conventions || [],
159
+ });
160
+ }
161
+ }
162
+ }
163
+
164
+ if (Array.isArray(manifest.dialogueExamples) && repo.setExemplars) {
165
+ await repo.setExemplars(companionId, manifest.dialogueExamples);
166
+ }
167
+
147
168
  if (directivesToCommit.length > 0) {
148
169
  await repo.commitDirectives(companionId, directivesToCommit);
149
170
  }
@@ -197,6 +218,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
197
218
  context: mappingResult.context,
198
219
  history,
199
220
  ...(req.body?.medium ? { medium: req.body.medium } : {}),
221
+ subtitleLanguage: req.body?.subtitleLanguage || req.body?.subtitle_language,
200
222
  });
201
223
  res.json(response);
202
224
  } catch (e: any) {
@@ -258,12 +280,13 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
258
280
  history,
259
281
  medium: 'web',
260
282
  signal: abortController.signal,
283
+ subtitleLanguage: req.body?.subtitleLanguage || req.body?.subtitle_language,
261
284
  });
262
285
 
263
- res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status })}\n\n`);
286
+ res.write(`event: staged\ndata: ${JSON.stringify({ response_id: response.response_id, correlation_id: response.correlation_id, status: response.status, mode: response.metadata?.mode })}\n\n`);
264
287
 
265
288
  const avatarEvent = response.metadata?.events?.find(
266
- (e: any) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval === 'APPROVED' || !e.approval)
289
+ (e: any) => (e.kind === 'avatar' || e.kind === 'body') && (e.approval?.toLowerCase() === 'approved' || e.approval === 'APPROVED' || !e.approval)
267
290
  );
268
291
  if (avatarEvent) {
269
292
  res.write(`event: avatar\ndata: ${JSON.stringify(avatarEvent)}\n\n`);
@@ -384,7 +407,9 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
384
407
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
385
408
  if (!runtime.memory) return res.json({ claims: [] });
386
409
  try {
387
- const claims = await runtime.memory.getClaims();
410
+ const claims = typeof (runtime.memory as any).getAllClaims === 'function'
411
+ ? await (runtime.memory as any).getAllClaims()
412
+ : await runtime.memory.getClaims();
388
413
  res.json({ claims });
389
414
  } catch (e: any) {
390
415
  res.status(500).json({ error: e.message });
@@ -397,7 +422,9 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
397
422
  if (!runtime) return res.status(404).json({ error: "Companion not found" });
398
423
  if (!runtime.memory) return res.json({ directives: [] });
399
424
  try {
400
- const directives = await runtime.memory.getDirectives();
425
+ const directives = typeof (runtime.memory as any).getAllDirectives === 'function'
426
+ ? await (runtime.memory as any).getAllDirectives()
427
+ : await runtime.memory.getDirectives();
401
428
  res.json({ directives });
402
429
  } catch (e: any) {
403
430
  res.status(500).json({ error: e.message });
@@ -514,7 +541,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
514
541
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
515
542
  try {
516
543
  await runtime.memory.approveClaim(req.body.id);
517
- res.json({ approved: true });
544
+ res.json({ approved: true, status: 'approved' });
518
545
  } catch (e: any) {
519
546
  res.status(500).json({ error: e.message });
520
547
  }
@@ -527,7 +554,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
527
554
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
528
555
  try {
529
556
  await runtime.memory.rejectClaim(req.body.id);
530
- res.json({ rejected: true });
557
+ res.json({ rejected: true, status: 'rejected' });
531
558
  } catch (e: any) {
532
559
  res.status(500).json({ error: e.message });
533
560
  }
@@ -541,7 +568,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
541
568
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
542
569
  try {
543
570
  await runtime.memory.approveDirective(req.body.id);
544
- res.json({ approved: true });
571
+ res.json({ approved: true, status: 'active' });
545
572
  } catch (e: any) {
546
573
  res.status(500).json({ error: e.message });
547
574
  }
@@ -554,7 +581,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
554
581
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
555
582
  try {
556
583
  await runtime.memory.rejectDirective(req.body.id);
557
- res.json({ rejected: true });
584
+ res.json({ rejected: true, status: 'rejected' });
558
585
  } catch (e: any) {
559
586
  res.status(500).json({ error: e.message });
560
587
  }
@@ -567,7 +594,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
567
594
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
568
595
  try {
569
596
  await runtime.memory.revokeDirective(req.body.id);
570
- res.json({ revoked: true });
597
+ res.json({ revoked: true, status: 'revoked' });
571
598
  } catch (e: any) {
572
599
  res.status(500).json({ error: e.message });
573
600
  }
@@ -580,7 +607,7 @@ export function createApp(runtimes: Map<string, SiduriRuntime> = new Map()): App
580
607
  if (!runtime.memory) return res.status(400).json({ error: "Memory organ not configured" });
581
608
  try {
582
609
  await runtime.memory.disableDirective(req.body.id);
583
- res.json({ disabled: true });
610
+ res.json({ disabled: true, status: 'disabled' });
584
611
  } catch (e: any) {
585
612
  res.status(500).json({ error: e.message });
586
613
  }
@@ -277,4 +277,43 @@ describe('API Request Context Mapper (Single-Owner, Single-Machine)', () => {
277
277
  expect(attenuatedResult.context?.actor.authorizationRole).toBe('viewer');
278
278
  expect(attenuatedResult.diagnostics).not.toContain('role_escalation_attempt_suppressed');
279
279
  });
280
+
281
+ describe('Mode Mapping (Casual, Teach, Hybrid)', () => {
282
+ test('maps explicit mode parameter to RequestContext', () => {
283
+ const casualResult = mapRequestContext({
284
+ companionId: 'comp-1',
285
+ mode: 'casual',
286
+ generateCorrelationId: true,
287
+ });
288
+ expect(casualResult.accepted).toBe(true);
289
+ expect(casualResult.context?.mode).toBe('casual');
290
+
291
+ const teachResult = mapRequestContext({
292
+ companionId: 'comp-1',
293
+ mode: 'teach',
294
+ generateCorrelationId: true,
295
+ });
296
+ expect(teachResult.accepted).toBe(true);
297
+ expect(teachResult.context?.mode).toBe('teach');
298
+
299
+ const hybridResult = mapRequestContext({
300
+ companionId: 'comp-1',
301
+ mode: 'hybrid',
302
+ generateCorrelationId: true,
303
+ });
304
+ expect(hybridResult.accepted).toBe(true);
305
+ expect(hybridResult.context?.mode).toBe('hybrid');
306
+ });
307
+
308
+ test('rejects invalid interaction mode', () => {
309
+ const invalidResult = mapRequestContext({
310
+ companionId: 'comp-1',
311
+ mode: 'super_turbo',
312
+ generateCorrelationId: true,
313
+ });
314
+ expect(invalidResult.accepted).toBe(false);
315
+ expect(invalidResult.error?.code).toBe('INVALID_CONTEXT');
316
+ expect(invalidResult.error?.field).toBe('mode');
317
+ });
318
+ });
280
319
  });
@@ -145,6 +145,7 @@ export function mapRequestContext(
145
145
  },
146
146
  source: input.source || rawCtx.source || (isAuthenticated ? 'local' : 'external'),
147
147
  subject: rawCtx.subject,
148
+ mode: rawCtx.mode || input.mode,
148
149
  };
149
150
 
150
151
  const validated = validateRequestContext(constructed);
@@ -262,6 +263,7 @@ export function mapRequestContext(
262
263
  },
263
264
  source: input.source || (isAuthenticated ? 'local' : 'external'),
264
265
  subject: input.subject,
266
+ mode: input.mode || input.conversation?.mode,
265
267
  };
266
268
 
267
269
  const validated = validateRequestContext(mappedContext);
package/src/index.test.ts CHANGED
@@ -155,4 +155,24 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
155
155
  expect(res.body.interrupted).toBe(true);
156
156
  expect(fakeRuntime.mouth.interrupt).toHaveBeenCalledWith('user_barge_in');
157
157
  });
158
+
159
+ test('POST /chat passes explicit interaction mode (casual, teach, hybrid) to runtime', async () => {
160
+ const res = await request(app)
161
+ .post('/chat')
162
+ .send({
163
+ id: 'companion-a',
164
+ message: 'Casual banter',
165
+ mode: 'casual',
166
+ });
167
+
168
+ expect(res.status).toBe(200);
169
+ expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith(
170
+ 'Casual banter',
171
+ expect.objectContaining({
172
+ companionId: 'companion-a',
173
+ mode: 'casual',
174
+ }),
175
+ []
176
+ );
177
+ });
158
178
  });
@@ -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
  });