@siduri-x/api 1.0.5 → 2.0.2

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