@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.
- package/dist/src/agent/feedback-store.d.ts +39 -0
- package/dist/src/agent/feedback-store.js +98 -0
- package/dist/src/agent/feedback-store.js.map +1 -0
- package/dist/src/agent/local-server.js +9 -2
- package/dist/src/agent/local-server.js.map +1 -1
- package/dist/src/agent/routes/admin-chat.d.ts +1 -0
- package/dist/src/agent/routes/admin-chat.js +1 -1
- package/dist/src/agent/routes/admin-chat.js.map +1 -1
- package/dist/src/agent/routes/evals.js +34 -47
- package/dist/src/agent/routes/evals.js.map +1 -1
- package/dist/src/agent/routes/feedback.d.ts +11 -0
- package/dist/src/agent/routes/feedback.js +72 -0
- package/dist/src/agent/routes/feedback.js.map +1 -0
- package/dist/src/agent/routes/files.js +118 -12
- package/dist/src/agent/routes/files.js.map +1 -1
- package/dist/src/agent/routes/inspect.js +33 -0
- package/dist/src/agent/routes/inspect.js.map +1 -1
- package/dist/src/cron/heartbeat-runner.d.ts +3 -6
- package/dist/src/cron/heartbeat-runner.js +1 -10
- package/dist/src/cron/heartbeat-runner.js.map +1 -1
- package/dist/src/index.d.ts +4 -5
- package/dist/src/index.js +3 -10
- package/dist/src/index.js.map +1 -1
- package/dist/src/middleware/auth.d.ts +3 -19
- package/dist/src/middleware/auth.js +0 -118
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/routes/ai-stream.d.ts +8 -7
- package/dist/src/routes/ai-stream.js +3 -16
- package/dist/src/routes/ai-stream.js.map +1 -1
- package/dist/src/routes/chat-stream.d.ts +4 -3
- package/dist/src/routes/chat-stream.js +3 -16
- package/dist/src/routes/chat-stream.js.map +1 -1
- package/dist/src/routes/chat.d.ts +4 -2
- package/dist/src/routes/chat.js +2 -14
- package/dist/src/routes/chat.js.map +1 -1
- package/dist/src/routes/chat.test.js +2 -2
- package/dist/src/routes/chat.test.js.map +1 -1
- package/dist/src/server.d.ts +16 -3
- package/dist/src/server.js +24 -25
- package/dist/src/server.js.map +1 -1
- package/dist/src/session/admin-file-tools.d.ts +136 -0
- package/dist/src/session/admin-file-tools.js +240 -0
- package/dist/src/session/admin-file-tools.js.map +1 -0
- package/dist/src/session/session-manager.d.ts +37 -3
- package/dist/src/session/session-manager.js +174 -44
- package/dist/src/session/session-manager.js.map +1 -1
- package/dist/src/session/session-manager.test.js +30 -52
- package/dist/src/session/session-manager.test.js.map +1 -1
- package/dist/src/session/session-runner.d.ts +29 -13
- package/dist/src/session/session-runner.js +28 -91
- package/dist/src/session/session-runner.js.map +1 -1
- package/dist/src/session/session-runner.test.js +70 -80
- package/dist/src/session/session-runner.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/dist/src/audit/audit-client.d.ts +0 -46
- package/dist/src/audit/audit-client.js +0 -83
- package/dist/src/audit/audit-client.js.map +0 -1
- package/dist/src/middleware/auth.test.d.ts +0 -6
- package/dist/src/middleware/auth.test.js +0 -260
- package/dist/src/middleware/auth.test.js.map +0 -1
- package/dist/src/routes/sessions.d.ts +0 -14
- package/dist/src/routes/sessions.js +0 -82
- package/dist/src/routes/sessions.js.map +0 -1
- package/dist/src/utils/jwt-verify.d.ts +0 -19
- package/dist/src/utils/jwt-verify.js +0 -32
- package/dist/src/utils/jwt-verify.js.map +0 -1
- package/dist/src/utils/jwt-verify.test.d.ts +0 -6
- package/dist/src/utils/jwt-verify.test.js +0 -150
- 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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
434
|
-
if (this.repo) {
|
|
435
|
-
await this.
|
|
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
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
768
|
+
this.sharedMcpManager = manager;
|
|
639
769
|
}
|
|
640
770
|
}
|
|
641
771
|
catch (err) {
|