@amodalai/runtime 0.1.16 → 0.1.17

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.
Files changed (70) hide show
  1. package/dist/src/agent/feedback-store.d.ts +39 -0
  2. package/dist/src/agent/feedback-store.js +98 -0
  3. package/dist/src/agent/feedback-store.js.map +1 -0
  4. package/dist/src/agent/local-server.js +9 -2
  5. package/dist/src/agent/local-server.js.map +1 -1
  6. package/dist/src/agent/routes/admin-chat.d.ts +1 -0
  7. package/dist/src/agent/routes/admin-chat.js +1 -1
  8. package/dist/src/agent/routes/admin-chat.js.map +1 -1
  9. package/dist/src/agent/routes/evals.js +34 -47
  10. package/dist/src/agent/routes/evals.js.map +1 -1
  11. package/dist/src/agent/routes/feedback.d.ts +11 -0
  12. package/dist/src/agent/routes/feedback.js +72 -0
  13. package/dist/src/agent/routes/feedback.js.map +1 -0
  14. package/dist/src/agent/routes/files.js +118 -12
  15. package/dist/src/agent/routes/files.js.map +1 -1
  16. package/dist/src/agent/routes/inspect.js +33 -0
  17. package/dist/src/agent/routes/inspect.js.map +1 -1
  18. package/dist/src/cron/heartbeat-runner.d.ts +3 -6
  19. package/dist/src/cron/heartbeat-runner.js +1 -10
  20. package/dist/src/cron/heartbeat-runner.js.map +1 -1
  21. package/dist/src/index.d.ts +4 -5
  22. package/dist/src/index.js +3 -10
  23. package/dist/src/index.js.map +1 -1
  24. package/dist/src/middleware/auth.d.ts +3 -19
  25. package/dist/src/middleware/auth.js +0 -118
  26. package/dist/src/middleware/auth.js.map +1 -1
  27. package/dist/src/routes/ai-stream.d.ts +8 -7
  28. package/dist/src/routes/ai-stream.js +3 -16
  29. package/dist/src/routes/ai-stream.js.map +1 -1
  30. package/dist/src/routes/chat-stream.d.ts +4 -3
  31. package/dist/src/routes/chat-stream.js +3 -16
  32. package/dist/src/routes/chat-stream.js.map +1 -1
  33. package/dist/src/routes/chat.d.ts +4 -2
  34. package/dist/src/routes/chat.js +2 -14
  35. package/dist/src/routes/chat.js.map +1 -1
  36. package/dist/src/routes/chat.test.js +2 -2
  37. package/dist/src/routes/chat.test.js.map +1 -1
  38. package/dist/src/server.d.ts +16 -3
  39. package/dist/src/server.js +24 -25
  40. package/dist/src/server.js.map +1 -1
  41. package/dist/src/session/admin-file-tools.d.ts +136 -0
  42. package/dist/src/session/admin-file-tools.js +240 -0
  43. package/dist/src/session/admin-file-tools.js.map +1 -0
  44. package/dist/src/session/session-manager.d.ts +37 -3
  45. package/dist/src/session/session-manager.js +174 -44
  46. package/dist/src/session/session-manager.js.map +1 -1
  47. package/dist/src/session/session-manager.test.js +30 -52
  48. package/dist/src/session/session-manager.test.js.map +1 -1
  49. package/dist/src/session/session-runner.d.ts +29 -13
  50. package/dist/src/session/session-runner.js +28 -91
  51. package/dist/src/session/session-runner.js.map +1 -1
  52. package/dist/src/session/session-runner.test.js +70 -80
  53. package/dist/src/session/session-runner.test.js.map +1 -1
  54. package/dist/tsconfig.tsbuildinfo +1 -1
  55. package/package.json +2 -2
  56. package/dist/src/audit/audit-client.d.ts +0 -46
  57. package/dist/src/audit/audit-client.js +0 -83
  58. package/dist/src/audit/audit-client.js.map +0 -1
  59. package/dist/src/middleware/auth.test.d.ts +0 -6
  60. package/dist/src/middleware/auth.test.js +0 -260
  61. package/dist/src/middleware/auth.test.js.map +0 -1
  62. package/dist/src/routes/sessions.d.ts +0 -14
  63. package/dist/src/routes/sessions.js +0 -82
  64. package/dist/src/routes/sessions.js.map +0 -1
  65. package/dist/src/utils/jwt-verify.d.ts +0 -19
  66. package/dist/src/utils/jwt-verify.js +0 -32
  67. package/dist/src/utils/jwt-verify.js.map +0 -1
  68. package/dist/src/utils/jwt-verify.test.d.ts +0 -6
  69. package/dist/src/utils/jwt-verify.test.js +0 -150
  70. package/dist/src/utils/jwt-verify.test.js.map +0 -1
@@ -0,0 +1,240 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Amodal Labs, Inc.
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ /**
7
+ * Admin-only file tools for reading, writing, and deleting agent config files.
8
+ * These tools are registered on admin sessions only, and only for local repos.
9
+ *
10
+ * Ported from the old agent-runner.ts admin tool implementation.
11
+ */
12
+ import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
13
+ import * as path from 'node:path';
14
+ // ---------------------------------------------------------------------------
15
+ // Path validation
16
+ // ---------------------------------------------------------------------------
17
+ const ALLOWED_REPO_DIRS = [
18
+ 'skills/',
19
+ 'knowledge/',
20
+ 'connections/',
21
+ 'stores/',
22
+ 'pages/',
23
+ 'automations/',
24
+ 'evals/',
25
+ 'agents/',
26
+ 'tools/',
27
+ 'amodal_packages/', // installed packages (read-only)
28
+ ];
29
+ const BLOCKED_FILENAMES = [
30
+ '.env',
31
+ 'amodal.json',
32
+ 'package.json',
33
+ 'pnpm-lock.yaml',
34
+ 'tsconfig.json',
35
+ ];
36
+ const READ_ONLY_DIRS = [
37
+ 'amodal_packages/',
38
+ ];
39
+ /** @internal Exported for testing */
40
+ export function isAllowedRepoPath(relPath) {
41
+ const basename = path.basename(relPath);
42
+ if (BLOCKED_FILENAMES.includes(basename))
43
+ return false;
44
+ return ALLOWED_REPO_DIRS.some((dir) => relPath.startsWith(dir));
45
+ }
46
+ function isReadOnlyPath(relPath) {
47
+ return READ_ONLY_DIRS.some((dir) => relPath.startsWith(dir));
48
+ }
49
+ function validatePath(repoRoot, rawPath) {
50
+ if (!rawPath || rawPath.startsWith('/')) {
51
+ return { error: 'Path must be relative to the repo root (no leading /)' };
52
+ }
53
+ if (rawPath.includes('..')) {
54
+ return { error: 'Path traversal (..) is not allowed' };
55
+ }
56
+ const normalized = path.normalize(rawPath);
57
+ if (!isAllowedRepoPath(normalized)) {
58
+ return { error: `Path "${normalized}" is not in an allowed directory. Allowed: ${ALLOWED_REPO_DIRS.join(', ')}. Blocked files: ${BLOCKED_FILENAMES.join(', ')}` };
59
+ }
60
+ const resolved = path.resolve(repoRoot, normalized);
61
+ if (!resolved.startsWith(repoRoot)) {
62
+ return { error: 'Resolved path escapes the repo directory' };
63
+ }
64
+ return { resolved, relative: normalized };
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Base adapter matching upstream DeclarativeTool interface
68
+ // ---------------------------------------------------------------------------
69
+ function createToolAdapter(opts) {
70
+ return {
71
+ name: opts.name,
72
+ displayName: opts.name,
73
+ description: opts.description,
74
+ kind: 'declarative',
75
+ parameterSchema: opts.parameters,
76
+ get isReadOnly() { return opts.name === 'read_repo_file'; },
77
+ get toolAnnotations() { return undefined; },
78
+ get schema() { return this.getSchema(); },
79
+ getSchema() {
80
+ return {
81
+ name: opts.name,
82
+ description: opts.description,
83
+ parametersJsonSchema: opts.parameters,
84
+ };
85
+ },
86
+ build(params) {
87
+ return {
88
+ name: opts.name,
89
+ params,
90
+ execute: async () => opts.execute(params),
91
+ };
92
+ },
93
+ silentBuild(params) {
94
+ return this.build(params);
95
+ },
96
+ async validateBuildAndExecute(params) {
97
+ return opts.execute(params);
98
+ },
99
+ };
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Tool factories
103
+ // ---------------------------------------------------------------------------
104
+ export function createReadRepoFileTool(repoRoot) {
105
+ return createToolAdapter({
106
+ name: 'read_repo_file',
107
+ description: 'Read a file from the agent repo. Path is relative to repo root. Allowed directories: skills/, knowledge/, connections/, stores/, pages/, automations/, evals/, agents/, tools/.',
108
+ parameters: {
109
+ type: 'object',
110
+ properties: {
111
+ path: { type: 'string', description: 'File path relative to repo root (e.g. "knowledge/formatting-rules.md")' },
112
+ },
113
+ required: ['path'],
114
+ },
115
+ async execute(params) {
116
+ const rawPath = String(params['path'] ?? '');
117
+ const validation = validatePath(repoRoot, rawPath);
118
+ if ('error' in validation) {
119
+ return { llmContent: `Error: ${validation.error}`, error: { message: validation.error, type: 'VALIDATION_ERROR' } };
120
+ }
121
+ try {
122
+ const content = await readFile(validation.resolved, 'utf-8');
123
+ return { llmContent: content, returnDisplay: `Read ${validation.relative}` };
124
+ }
125
+ catch (err) {
126
+ const isNotFound = err instanceof Error && 'code' in err && (err.code) === 'ENOENT'; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion -- checking errno;
127
+ const msg = isNotFound ? `File not found: ${validation.relative}` : (err instanceof Error ? err.message : String(err));
128
+ return { llmContent: `Error: ${msg}`, error: { message: msg, type: 'EXECUTION_FAILED' } };
129
+ }
130
+ },
131
+ });
132
+ }
133
+ export function createWriteRepoFileTool(repoRoot) {
134
+ return createToolAdapter({
135
+ name: 'write_repo_file',
136
+ description: 'Create or update a file in the agent repo. Use this to add skills, knowledge, pages, automations, tools, store schemas, evals, connection docs, or agent overrides. Path is relative to repo root. Allowed directories: skills/, knowledge/, connections/, stores/, pages/, automations/, evals/, agents/, tools/.',
137
+ parameters: {
138
+ type: 'object',
139
+ properties: {
140
+ path: { type: 'string', description: 'File path relative to repo root (e.g. "knowledge/formatting-rules.md")' },
141
+ content: { type: 'string', description: 'Full file content to write' },
142
+ },
143
+ required: ['path', 'content'],
144
+ },
145
+ async execute(params) {
146
+ const rawPath = String(params['path'] ?? '');
147
+ const content = String(params['content'] ?? '');
148
+ const validation = validatePath(repoRoot, rawPath);
149
+ if ('error' in validation) {
150
+ return { llmContent: `Error: ${validation.error}`, error: { message: validation.error, type: 'VALIDATION_ERROR' } };
151
+ }
152
+ if (isReadOnlyPath(validation.relative)) {
153
+ return { llmContent: `Error: ${validation.relative} is read-only (installed package)`, error: { message: 'Cannot write to installed packages', type: 'VALIDATION_ERROR' } };
154
+ }
155
+ if (!content) {
156
+ return { llmContent: 'Error: Content must not be empty', error: { message: 'Content must not be empty', type: 'VALIDATION_ERROR' } };
157
+ }
158
+ try {
159
+ await mkdir(path.dirname(validation.resolved), { recursive: true });
160
+ await writeFile(validation.resolved, content, 'utf-8');
161
+ return { llmContent: `Wrote ${validation.relative} (${String(content.length)} bytes)`, returnDisplay: `Wrote ${validation.relative}` };
162
+ }
163
+ catch (err) {
164
+ const msg = err instanceof Error ? err.message : String(err);
165
+ return { llmContent: `Error: ${msg}`, error: { message: msg, type: 'EXECUTION_FAILED' } };
166
+ }
167
+ },
168
+ });
169
+ }
170
+ export function createDeleteRepoFileTool(repoRoot) {
171
+ return createToolAdapter({
172
+ name: 'delete_repo_file',
173
+ description: 'Delete a file from the agent repo. Always confirm with the user before deleting. Path is relative to repo root. Same directory restrictions as write_repo_file.',
174
+ parameters: {
175
+ type: 'object',
176
+ properties: {
177
+ path: { type: 'string', description: 'File path relative to repo root (e.g. "evals/old-test.md")' },
178
+ },
179
+ required: ['path'],
180
+ },
181
+ async execute(params) {
182
+ const rawPath = String(params['path'] ?? '');
183
+ const validation = validatePath(repoRoot, rawPath);
184
+ if ('error' in validation) {
185
+ return { llmContent: `Error: ${validation.error}`, error: { message: validation.error, type: 'VALIDATION_ERROR' } };
186
+ }
187
+ if (isReadOnlyPath(validation.relative)) {
188
+ return { llmContent: `Error: ${validation.relative} is read-only (installed package)`, error: { message: 'Cannot delete installed packages', type: 'VALIDATION_ERROR' } };
189
+ }
190
+ try {
191
+ await unlink(validation.resolved);
192
+ return { llmContent: `Deleted ${validation.relative}`, returnDisplay: `Deleted ${validation.relative}` };
193
+ }
194
+ catch (err) {
195
+ const isNotFound = err instanceof Error && 'code' in err && (err.code) === 'ENOENT'; // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion -- checking errno;
196
+ const msg = isNotFound ? `File not found: ${validation.relative}` : (err instanceof Error ? err.message : String(err));
197
+ return { llmContent: `Error: ${msg}`, error: { message: msg, type: 'EXECUTION_FAILED' } };
198
+ }
199
+ },
200
+ });
201
+ }
202
+ // ---------------------------------------------------------------------------
203
+ // Internal API tool — lets admin query the local runtime's own endpoints
204
+ // ---------------------------------------------------------------------------
205
+ export function createInternalApiTool(getPort) {
206
+ return createToolAdapter({
207
+ name: 'internal_api',
208
+ description: `Query the amodal runtime's internal API. Use this to check eval results, connection health, agent context, store data, and automation status. The endpoint is relative to the local server (e.g. "/api/evals/runs" or "/inspect/health"). Only GET requests are supported.`,
209
+ parameters: {
210
+ type: 'object',
211
+ properties: {
212
+ endpoint: { type: 'string', description: 'API path (e.g. "/api/evals/runs", "/inspect/health", "/api/stores")' },
213
+ },
214
+ required: ['endpoint'],
215
+ },
216
+ async execute(params) {
217
+ const endpoint = String(params['endpoint'] ?? '');
218
+ const port = getPort();
219
+ if (!port) {
220
+ return { llmContent: 'Error: Server not ready', error: { message: 'Server not ready', type: 'EXECUTION_FAILED' } };
221
+ }
222
+ try {
223
+ const resp = await fetch(`http://127.0.0.1:${port}${endpoint}`);
224
+ const text = await resp.text();
225
+ try {
226
+ const json = JSON.parse(text);
227
+ return { llmContent: JSON.stringify(json, null, 2), returnDisplay: `GET ${endpoint} → ${resp.status}` };
228
+ }
229
+ catch {
230
+ return { llmContent: text, returnDisplay: `GET ${endpoint} → ${resp.status}` };
231
+ }
232
+ }
233
+ catch (err) {
234
+ const msg = err instanceof Error ? err.message : String(err);
235
+ return { llmContent: `Error: ${msg}`, error: { message: msg, type: 'EXECUTION_FAILED' } };
236
+ }
237
+ },
238
+ });
239
+ }
240
+ //# sourceMappingURL=admin-file-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-file-tools.js","sourceRoot":"","sources":["../../../src/session/admin-file-tools.ts"],"names":[],"mappings":"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"}
@@ -54,6 +54,23 @@ export interface ManagedSession {
54
54
  /** App ID for this session */
55
55
  appId?: string;
56
56
  }
57
+ /** Shape of a stored session record (from platform API or session store). */
58
+ export interface StoredSessionRecord {
59
+ id: string;
60
+ app_id: string;
61
+ messages: SessionMessage[];
62
+ status: string;
63
+ model?: string;
64
+ provider?: string;
65
+ }
66
+ /**
67
+ * Pluggable session store for loading stored session history.
68
+ * Implementations are provided by the hosting layer.
69
+ */
70
+ export interface SessionStore {
71
+ /** Fetch a stored session record by ID. Returns null if not found. */
72
+ getSession(appId: string, sessionId: string, token: string): Promise<StoredSessionRecord | null>;
73
+ }
57
74
  export interface SessionManagerOptions {
58
75
  /** Base config parameters to clone for each session */
59
76
  baseParams: AmodalConfigParameters;
@@ -71,7 +88,13 @@ export interface SessionManagerOptions {
71
88
  shellExecutor?: CustomShellExecutor;
72
89
  /** Shared store backend (local dev) */
73
90
  storeBackend?: StoreBackend;
91
+ /** Pluggable session store for hydration (if not provided, falls back to platform API) */
92
+ sessionStore?: SessionStore;
74
93
  }
94
+ /**
95
+ * Manages per-request sessions: creates Config + GeminiClient + Scheduler
96
+ * instances, tracks them by ID, and cleans up expired sessions.
97
+ */
75
98
  export declare class SessionManager {
76
99
  private readonly sessions;
77
100
  private baseParams;
@@ -81,9 +104,12 @@ export declare class SessionManager {
81
104
  private readonly toolExecutor?;
82
105
  private readonly shellExecutor?;
83
106
  private readonly sharedStoreBackend?;
107
+ private readonly sessionStore?;
84
108
  private cleanupTimer;
85
109
  /** Deduplicates concurrent hydration requests for the same conversation */
86
110
  private readonly pendingHydrations;
111
+ /** Shared MCP manager for all sessions (lazy-initialized, reused) */
112
+ private sharedMcpManager?;
87
113
  /** Persistent MCP manager for inspect operations (lazy-initialized) */
88
114
  private inspectMcp?;
89
115
  private inspectMcpInitialized;
@@ -134,9 +160,13 @@ export declare class SessionManager {
134
160
  reregister(session: ManagedSession, newId: string): void;
135
161
  /**
136
162
  * Create an admin session for the config chat.
137
- * Uses admin agent skills/knowledge but the current repo's connections.
163
+ * Uses admin agent skills/knowledge but the current repo's connections/stores.
164
+ *
165
+ * Temporarily swaps repo fields so create() builds the prompt with admin
166
+ * content, then restores the original repo. This mirrors the old approach
167
+ * of building an adminRepo overlay.
138
168
  */
139
- createAdminSession(): Promise<ManagedSession>;
169
+ createAdminSession(getPort?: () => number | null): Promise<ManagedSession>;
140
170
  /**
141
171
  * Get a persistent MCP manager for inspect/health operations.
142
172
  * Lazy-initialized on first call, reused across requests.
@@ -145,7 +175,11 @@ export declare class SessionManager {
145
175
  /**
146
176
  * Initialize MCP servers for a session from repo connections.
147
177
  */
148
- private initMcp;
178
+ /**
179
+ * Initialize the shared MCP manager (once, reused across sessions).
180
+ * Avoids reconnecting MCP servers for every eval/judge/admin session.
181
+ */
182
+ private initSharedMcp;
149
183
  /**
150
184
  * Build the list of upstream core tools to enable based on repo config.
151
185
  * Only tools relevant to the Amodal runtime are included.
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
6
  import { randomUUID } from 'node:crypto';
7
- import { AmodalConfig, Scheduler, ROOT_SCHEDULER_ID, ApprovalMode, PolicyDecision, AgentSDK, buildDefaultPrompt, PlanModeManager, McpManager, ensureAdminAgent, loadAdminAgent, } from '@amodalai/core';
7
+ import { AmodalConfig, Scheduler, ROOT_SCHEDULER_ID, ApprovalMode, PolicyDecision, AgentSDK, buildDefaultPrompt, resolveScopeLabels, generateFieldGuidance, generateAlternativeLookupGuidance, PlanModeManager, McpManager, ensureAdminAgent, loadAdminAgent, } from '@amodalai/core';
8
8
  import { convertSessionMessagesToHistory } from './history-converter.js';
9
9
  /**
10
10
  * Resolve env: references in a string record.
@@ -26,6 +26,10 @@ function resolveEnvRefs(record) {
26
26
  const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
27
27
  const DEFAULT_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
28
28
  const ASK_USER_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
29
+ /**
30
+ * Manages per-request sessions: creates Config + GeminiClient + Scheduler
31
+ * instances, tracks them by ID, and cleans up expired sessions.
32
+ */
29
33
  export class SessionManager {
30
34
  sessions = new Map();
31
35
  baseParams;
@@ -35,9 +39,12 @@ export class SessionManager {
35
39
  toolExecutor;
36
40
  shellExecutor;
37
41
  sharedStoreBackend;
42
+ sessionStore;
38
43
  cleanupTimer = null;
39
44
  /** Deduplicates concurrent hydration requests for the same conversation */
40
45
  pendingHydrations = new Map();
46
+ /** Shared MCP manager for all sessions (lazy-initialized, reused) */
47
+ sharedMcpManager;
41
48
  /** Persistent MCP manager for inspect operations (lazy-initialized) */
42
49
  inspectMcp;
43
50
  inspectMcpInitialized = false;
@@ -49,6 +56,7 @@ export class SessionManager {
49
56
  this.toolExecutor = options.toolExecutor;
50
57
  this.shellExecutor = options.shellExecutor;
51
58
  this.sharedStoreBackend = options.storeBackend;
59
+ this.sessionStore = options.sessionStore;
52
60
  const cleanupIntervalMs = options.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
53
61
  this.cleanupTimer = setInterval(() => void this.cleanup(), cleanupIntervalMs);
54
62
  // Don't keep the process alive just for cleanup
@@ -107,7 +115,7 @@ export class SessionManager {
107
115
  }));
108
116
  sessionParams.basePrompt = this.repo.config.basePrompt;
109
117
  sessionParams.agentName = this.repo.config.name;
110
- sessionParams.agentContext = this.repo.config.description;
118
+ sessionParams.agentContext = this.repo.config.userContext ?? this.repo.config.description;
111
119
  // Model config from repo
112
120
  const mainModel = this.repo.config.models?.main;
113
121
  if (mainModel) {
@@ -238,11 +246,6 @@ export class SessionManager {
238
246
  const connections = config.getConnections();
239
247
  const connKeys = Object.keys(connections).filter((k) => k !== '_secrets');
240
248
  process.stderr.write(`[SESSION] connections: ${connKeys.join(', ') || '(none)'}\n`);
241
- if (sessionType === 'onboarding' && !connections['platform-api']) {
242
- process.stderr.write(`[SESSION] WARNING: onboarding session has no platform-api connection — ` +
243
- `the request tool cannot create resources. Check that ADMIN_APP_ID is set ` +
244
- `and the seed has run.\n`);
245
- }
246
249
  // App secrets are available to tools via session-scoped getSessionEnv()
247
250
  // (through ToolContext). They are NOT injected into process.env to prevent
248
251
  // cross-app secret leakage in multi-session runtimes.
@@ -308,7 +311,36 @@ export class SessionManager {
308
311
  name: config.getAgentName() ?? 'Amodal Agent',
309
312
  description: config.getAgentDescription(),
310
313
  agentContext: config.getAgentContext(),
311
- connectionNames: Object.keys(config.getConnections()).filter((k) => k !== '_secrets'),
314
+ agentOverride: this.repo?.agents?.main,
315
+ connections: this.repo?.connections ? Array.from(this.repo.connections.values()).map((conn) => ({
316
+ name: conn.name,
317
+ endpoints: (conn.surface ?? [])
318
+ .filter((ep) => ep.included)
319
+ .map((ep) => ({ method: ep.method, path: ep.path, description: ep.description })),
320
+ entities: conn.entities,
321
+ rules: conn.rules,
322
+ })) : undefined,
323
+ skills: this.repo?.skills?.map((s) => ({
324
+ name: s.name,
325
+ description: s.description ?? '',
326
+ trigger: s.trigger,
327
+ body: s.body,
328
+ })),
329
+ knowledge: this.repo?.knowledge?.map((k) => ({
330
+ name: k.name,
331
+ title: k.title,
332
+ body: k.body,
333
+ })),
334
+ ...(this.repo?.connections ? (() => {
335
+ const { scopeLabels } = resolveScopeLabels(this.repo.connections, []);
336
+ const fieldGuidance = generateFieldGuidance(this.repo.connections, []);
337
+ const altLookup = generateAlternativeLookupGuidance(this.repo.connections);
338
+ return {
339
+ fieldGuidance: fieldGuidance || undefined,
340
+ scopeLabels: Object.keys(scopeLabels).length > 0 ? scopeLabels : undefined,
341
+ alternativeLookupGuidance: altLookup || undefined,
342
+ };
343
+ })() : {}),
312
344
  });
313
345
  try {
314
346
  geminiClient.getChat().setSystemInstruction(systemPrompt);
@@ -430,9 +462,73 @@ export class SessionManager {
430
462
  shellExecutor: this.shellExecutor,
431
463
  appId: auth?.applicationId,
432
464
  };
433
- // Initialize MCP servers if repo has MCP connections
434
- if (this.repo) {
435
- await this.initMcp(session, this.repo);
465
+ // Share MCP connection across sessions connect once, reuse for all
466
+ if (this.repo && !this.sharedMcpManager) {
467
+ await this.initSharedMcp(this.repo);
468
+ }
469
+ if (this.sharedMcpManager) {
470
+ session.mcpManager = this.sharedMcpManager;
471
+ // Register MCP tools on the upstream tool registry so the Gemini client can see them
472
+ try {
473
+ const upstream = config.getUpstreamConfig();
474
+ const toolRegistry = upstream.getToolRegistry();
475
+ const mcpTools = session.mcpManager.getDiscoveredTools();
476
+ for (const mcpTool of mcpTools) {
477
+ // Adapter matching upstream DeclarativeTool interface (build/silentBuild/schema/getSchema)
478
+ const mcpSession = session;
479
+ const adapter = {
480
+ name: mcpTool.name,
481
+ displayName: mcpTool.name,
482
+ description: mcpTool.description,
483
+ kind: 'declarative',
484
+ parameterSchema: mcpTool.parameters,
485
+ get isReadOnly() { return true; },
486
+ get toolAnnotations() { return undefined; },
487
+ get schema() { return this.getSchema(); },
488
+ getSchema() {
489
+ return {
490
+ name: mcpTool.name,
491
+ description: mcpTool.description,
492
+ parametersJsonSchema: mcpTool.parameters,
493
+ };
494
+ },
495
+ build(params) {
496
+ return {
497
+ name: mcpTool.name,
498
+ params,
499
+ execute: async () => adapter.validateBuildAndExecute(params),
500
+ };
501
+ },
502
+ silentBuild(params) {
503
+ return this.build(params);
504
+ },
505
+ async validateBuildAndExecute(params) {
506
+ try {
507
+ const result = await mcpSession.mcpManager.callTool(mcpTool.name, params);
508
+ const output = result.content
509
+ .map((c) => c.type === 'text' && c.text ? c.text : `[${c.type}]`)
510
+ .join('\n');
511
+ return {
512
+ llmContent: result.isError ? `Error: ${output}` : output,
513
+ returnDisplay: output.slice(0, 200),
514
+ ...(result.isError ? { error: { message: output, type: 'EXECUTION_FAILED' } } : {}),
515
+ };
516
+ }
517
+ catch (err) {
518
+ const msg = err instanceof Error ? err.message : String(err);
519
+ return { llmContent: `Error: ${msg}`, returnDisplay: msg, error: { message: msg, type: 'EXECUTION_FAILED' } };
520
+ }
521
+ },
522
+ };
523
+ toolRegistry.registerTool(adapter); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
524
+ }
525
+ await geminiClient.setTools();
526
+ process.stderr.write(`[MCP] Registered ${String(mcpTools.length)} MCP tools on tool registry\n`);
527
+ }
528
+ catch (err) {
529
+ const msg = err instanceof Error ? err.message : String(err);
530
+ process.stderr.write(`[MCP] Failed to register MCP tools: ${msg}\n`);
531
+ }
436
532
  }
437
533
  this.sessions.set(sessionId, session);
438
534
  return session;
@@ -479,31 +575,20 @@ export class SessionManager {
479
575
  }
480
576
  }
481
577
  async doHydrate(conversationId, role, auth, sessionType) {
482
- // Fetch stored conversation from platform-api
483
- const url = `${this.platformApiUrl}/api/applications/${auth.applicationId}/sessions/${conversationId}`;
578
+ // Fetch stored conversation via pluggable session store
579
+ if (!this.sessionStore || !auth?.applicationId || !auth.token) {
580
+ return null;
581
+ }
484
582
  let record;
485
583
  try {
486
- const controller = new AbortController();
487
- const timer = setTimeout(() => controller.abort(), 10_000);
488
- const response = await fetch(url, {
489
- signal: controller.signal,
490
- headers: {
491
- Authorization: `Bearer ${auth.token}`,
492
- Accept: 'application/json',
493
- },
494
- });
495
- clearTimeout(timer);
496
- if (!response.ok) {
497
- process.stderr.write(`[HYDRATE] Failed to fetch conversation ${conversationId}: HTTP ${response.status}\n`);
498
- return null;
499
- }
500
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- API response shape
501
- record = (await response.json());
584
+ record = await this.sessionStore.getSession(auth.applicationId, conversationId, auth.token);
502
585
  }
503
586
  catch (err) {
504
587
  process.stderr.write(`[HYDRATE] Error fetching conversation ${conversationId}: ${err instanceof Error ? err.message : String(err)}\n`);
505
588
  return null;
506
589
  }
590
+ if (!record)
591
+ return null;
507
592
  // No messages → nothing to hydrate
508
593
  if (!record.messages || record.messages.length === 0)
509
594
  return null;
@@ -574,27 +659,68 @@ export class SessionManager {
574
659
  }
575
660
  /**
576
661
  * Create an admin session for the config chat.
577
- * Uses admin agent skills/knowledge but the current repo's connections.
662
+ * Uses admin agent skills/knowledge but the current repo's connections/stores.
663
+ *
664
+ * Temporarily swaps repo fields so create() builds the prompt with admin
665
+ * content, then restores the original repo. This mirrors the old approach
666
+ * of building an adminRepo overlay.
578
667
  */
579
- async createAdminSession() {
668
+ async createAdminSession(getPort) {
580
669
  if (!this.repo) {
581
670
  throw new Error('Admin sessions require a repo');
582
671
  }
672
+ if (this.repo.source !== 'local') {
673
+ throw new Error('Admin sessions are only available for local repos');
674
+ }
583
675
  const agentDir = await ensureAdminAgent(this.repo.origin);
584
676
  const adminContent = await loadAdminAgent(agentDir);
585
- // Create a session with admin context
586
- // The admin session uses the same create() flow but with overridden prompts
587
- const session = await this.create('admin');
588
- // Override system prompt with admin agent prompt
589
- if (adminContent.agentPrompt) {
590
- try {
591
- session.geminiClient.getChat().setSystemInstruction(adminContent.agentPrompt);
592
- }
593
- catch {
594
- // Non-fatal
595
- }
677
+ // Save original repo fields
678
+ const origSkills = this.repo.skills;
679
+ const origKnowledge = this.repo.knowledge;
680
+ const origAgents = this.repo.agents;
681
+ const origAutomations = this.repo.automations;
682
+ // Swap in admin content so create() builds the prompt correctly
683
+ this.repo.skills = adminContent.skills;
684
+ this.repo.knowledge = adminContent.knowledge;
685
+ this.repo.agents = {
686
+ main: adminContent.agentPrompt ?? undefined,
687
+ simple: undefined,
688
+ subagents: [],
689
+ };
690
+ this.repo.automations = [];
691
+ let session;
692
+ try {
693
+ session = await this.create('admin');
694
+ }
695
+ finally {
696
+ // Restore original repo fields
697
+ this.repo.skills = origSkills;
698
+ this.repo.knowledge = origKnowledge;
699
+ this.repo.agents = origAgents;
700
+ this.repo.automations = origAutomations;
596
701
  }
597
702
  session.appId = 'admin';
703
+ // Register admin file tools (read/write/delete repo files)
704
+ try {
705
+ const { createReadRepoFileTool, createWriteRepoFileTool, createDeleteRepoFileTool } = await import('./admin-file-tools.js');
706
+ const repoRoot = this.repo.origin;
707
+ const upstream = session.config.getUpstreamConfig();
708
+ const toolRegistry = upstream.getToolRegistry();
709
+ toolRegistry.registerTool(createReadRepoFileTool(repoRoot)); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
710
+ toolRegistry.registerTool(createWriteRepoFileTool(repoRoot)); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
711
+ toolRegistry.registerTool(createDeleteRepoFileTool(repoRoot)); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
712
+ // Internal API tool — lets admin query eval results, health, context, etc.
713
+ if (getPort) {
714
+ const { createInternalApiTool } = await import('./admin-file-tools.js');
715
+ toolRegistry.registerTool(createInternalApiTool(getPort)); // eslint-disable-line @typescript-eslint/no-unsafe-type-assertion
716
+ }
717
+ await session.geminiClient.setTools();
718
+ process.stderr.write('[ADMIN] Registered admin tools (file tools + internal_api)\n');
719
+ }
720
+ catch (err) {
721
+ const msg = err instanceof Error ? err.message : String(err);
722
+ process.stderr.write(`[ADMIN] Failed to register file tools: ${msg}\n`);
723
+ }
598
724
  return session;
599
725
  }
600
726
  /**
@@ -627,7 +753,11 @@ export class SessionManager {
627
753
  /**
628
754
  * Initialize MCP servers for a session from repo connections.
629
755
  */
630
- async initMcp(session, repo) {
756
+ /**
757
+ * Initialize the shared MCP manager (once, reused across sessions).
758
+ * Avoids reconnecting MCP servers for every eval/judge/admin session.
759
+ */
760
+ async initSharedMcp(repo) {
631
761
  const mcpServers = this.buildMcpConfigs(repo);
632
762
  if (Object.keys(mcpServers).length === 0)
633
763
  return;
@@ -635,7 +765,7 @@ export class SessionManager {
635
765
  try {
636
766
  await manager.startServers(mcpServers);
637
767
  if (manager.connectedCount > 0) {
638
- session.mcpManager = manager;
768
+ this.sharedMcpManager = manager;
639
769
  }
640
770
  }
641
771
  catch (err) {