@lensagent/core 0.1.1 → 0.1.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/README.md +63 -4
- package/config/lens.example.jsonc +36 -13
- package/dist/adapters/executor-router.d.ts +20 -0
- package/dist/adapters/executor-router.js +86 -0
- package/dist/adapters/executor.d.ts +9 -3
- package/dist/adapters/executor.js +196 -61
- package/dist/adapters/lsp.d.ts +2 -1
- package/dist/adapters/lsp.js +98 -41
- package/dist/adapters/mcp.js +52 -17
- package/dist/adapters/revert.d.ts +5 -4
- package/dist/adapters/revert.js +6 -64
- package/dist/adapters/web.d.ts +9 -0
- package/dist/adapters/web.js +244 -0
- package/dist/config/files.d.ts +14 -0
- package/dist/config/files.js +57 -0
- package/dist/config/runtime.d.ts +3 -4
- package/dist/config/runtime.js +116 -97
- package/dist/config/schema.d.ts +56 -10
- package/dist/config/schema.js +48 -8
- package/dist/contracts/capability.d.ts +27 -1
- package/dist/contracts/domain.d.ts +1 -0
- package/dist/contracts/model.d.ts +33 -6
- package/dist/contracts/review.d.ts +17 -0
- package/dist/contracts/review.js +1 -0
- package/dist/core/access-policy.d.ts +40 -0
- package/dist/core/access-policy.js +68 -0
- package/dist/core/access.d.ts +30 -2
- package/dist/core/access.js +159 -26
- package/dist/core/context-boundaries.d.ts +25 -0
- package/dist/core/context-boundaries.js +106 -0
- package/dist/core/context.d.ts +3 -0
- package/dist/core/context.js +11 -0
- package/dist/core/host.js +261 -173
- package/dist/entry/pair.d.ts +1 -0
- package/dist/entry/pair.js +14 -0
- package/dist/entry/run.js +1 -1
- package/dist/execution/lsp-process.d.ts +3 -2
- package/dist/execution/lsp-process.js +46 -28
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +262 -80
- package/dist/runtime/agent.d.ts +9 -4
- package/dist/runtime/agent.js +361 -286
- package/dist/runtime/artifacts.d.ts +13 -1
- package/dist/runtime/artifacts.js +137 -10
- package/dist/runtime/cache.d.ts +59 -16
- package/dist/runtime/cache.js +124 -43
- package/dist/runtime/calls.d.ts +1 -1
- package/dist/runtime/calls.js +164 -48
- package/dist/runtime/capabilities.d.ts +2 -2
- package/dist/runtime/capabilities.js +4 -5
- package/dist/runtime/cleanup.d.ts +5 -2
- package/dist/runtime/cleanup.js +48 -17
- package/dist/runtime/compaction.d.ts +9 -0
- package/dist/runtime/compaction.js +52 -0
- package/dist/runtime/context.d.ts +8 -4
- package/dist/runtime/context.js +115 -128
- package/dist/runtime/events.d.ts +7 -0
- package/dist/runtime/events.js +83 -0
- package/dist/runtime/images.d.ts +14 -0
- package/dist/runtime/images.js +82 -0
- package/dist/runtime/kernel-capabilities.d.ts +7 -1
- package/dist/runtime/kernel-capabilities.js +146 -68
- package/dist/runtime/model-audit.d.ts +9 -0
- package/dist/runtime/model-audit.js +39 -0
- package/dist/runtime/model.d.ts +109 -4
- package/dist/runtime/model.js +368 -143
- package/dist/runtime/observability.d.ts +1 -0
- package/dist/runtime/observability.js +60 -15
- package/dist/runtime/policy.d.ts +1 -1
- package/dist/runtime/policy.js +2 -2
- package/dist/runtime/profile.d.ts +2 -1
- package/dist/runtime/profile.js +12 -15
- package/dist/runtime/projects.d.ts +3 -1
- package/dist/runtime/projects.js +38 -17
- package/dist/runtime/ptc-worker.d.ts +1 -0
- package/dist/runtime/ptc-worker.js +97 -0
- package/dist/runtime/ptc.d.ts +16 -0
- package/dist/runtime/ptc.js +128 -0
- package/dist/runtime/results.d.ts +5 -4
- package/dist/runtime/results.js +29 -51
- package/dist/runtime/review.d.ts +4 -1
- package/dist/runtime/review.js +101 -22
- package/dist/runtime/scheduler.js +1 -0
- package/dist/runtime/skills.js +34 -5
- package/dist/runtime/task-audit.d.ts +21 -0
- package/dist/runtime/task-audit.js +122 -0
- package/dist/runtime/time.d.ts +9 -0
- package/dist/runtime/time.js +18 -0
- package/dist/runtime/tokenizer.d.ts +2 -0
- package/dist/runtime/tokenizer.js +26 -1
- package/dist/store/database.js +26 -1
- package/dist/store/event-log.d.ts +1 -0
- package/dist/store/event-log.js +22 -5
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +50 -1
- package/dist/store/store.d.ts +65 -19
- package/dist/store/store.js +986 -188
- package/package.json +14 -6
- package/proto/executor.proto +69 -55
package/dist/http/server.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { ConfigFiles } from '../config/files.js';
|
|
4
|
+
import { applyPurge, commitPurge } from '../runtime/cleanup.js';
|
|
2
5
|
import Fastify from 'fastify';
|
|
3
6
|
import { digest, expectedRevision, integer, jsonValue, optionalText, record, text } from '../core/codec.js';
|
|
4
7
|
import { LensError, fail } from '../core/domain.js';
|
|
8
|
+
import { loopbackAddress } from '../core/access-policy.js';
|
|
5
9
|
import { ExecutorCallError } from '../adapters/executor.js';
|
|
6
|
-
import { resolveReasoningEffort } from '../runtime/profile.js';
|
|
10
|
+
import { modelReasoningEfforts, resolveReasoningEffort } from '../runtime/profile.js';
|
|
7
11
|
import { componentIdentity } from '../core/identity.js';
|
|
8
12
|
export function createServer(dependencies) {
|
|
9
13
|
const app = Fastify({ logger: false, bodyLimit: 4 * 1024 * 1024, requestIdHeader: 'x-correlation-id', ajv: { customOptions: { removeAdditional: false } }, ...(dependencies.https === undefined ? {} : { https: dependencies.https }) });
|
|
14
|
+
let managedFiles;
|
|
15
|
+
const files = () => managedFiles ??= new ConfigFiles(dirname(dependencies.config.path), [dependencies.config.path, dependencies.pair.path]);
|
|
10
16
|
const contexts = new WeakMap();
|
|
11
17
|
const starts = new WeakMap();
|
|
12
18
|
const specs = [];
|
|
@@ -20,19 +26,34 @@ export function createServer(dependencies) {
|
|
|
20
26
|
error_code: typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : null
|
|
21
27
|
});
|
|
22
28
|
}
|
|
29
|
+
dependencies.observability.record('http', 'request_error', 'failed', 0, { status: failure.status, error_code: failure.code });
|
|
23
30
|
void reply.status(failure.status).send({
|
|
24
31
|
error: { code: failure.code, message: failure.message, retryable: retryableFailure(failure.status), details: failure.details },
|
|
25
32
|
correlation_id: request.id
|
|
26
33
|
});
|
|
27
34
|
});
|
|
28
|
-
app.addHook('onRequest', async (request) => {
|
|
35
|
+
app.addHook('onRequest', async (request, reply) => {
|
|
29
36
|
starts.set(request, performance.now());
|
|
30
37
|
if (!request.url.startsWith('/api/v1/'))
|
|
31
38
|
return;
|
|
39
|
+
if (!dependencies.config.current.config.tls.enabled && !loopbackAddress(request.socket.remoteAddress)) {
|
|
40
|
+
fail('HTTPS_REQUIRED', 403, 'Remote clients must use HTTPS.');
|
|
41
|
+
}
|
|
32
42
|
const readiness = dependencies.ready();
|
|
33
43
|
if (readiness.mode !== 'full')
|
|
34
44
|
fail('RUNTIME_NOT_READY', 503, 'Lens Core is not ready.', { blockers: readiness.blockers });
|
|
35
|
-
const ingress = await dependencies.pair.authorize(request.headers, dependencies.config.current.config.tls.enabled ? 'https' : 'http', request.id
|
|
45
|
+
const ingress = await dependencies.pair.authorize(request.headers, dependencies.config.current.config.tls.enabled ? 'https' : 'http', request.id, true,
|
|
46
|
+
// The raw socket address, never `request.ip`: the latter honours forwarded headers, and a caller-supplied
|
|
47
|
+
// header must not be able to make a remote caller look local enough to admit itself.
|
|
48
|
+
request.socket.remoteAddress);
|
|
49
|
+
const path = request.url.split('?')[0] ?? '';
|
|
50
|
+
const sensitive = path === '/api/v1/config' || path === '/api/v1/config/validate' || path.startsWith('/api/v1/config/files') || /^\/api\/v1\/tasks\/[^/]+\/audit$/.test(path);
|
|
51
|
+
if (sensitive || !['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
|
|
52
|
+
if (!ingress.strongTrust)
|
|
53
|
+
fail('STRONG_TRUST_REQUIRED', 403, 'This operation requires a strongly trusted client.');
|
|
54
|
+
}
|
|
55
|
+
if (sensitive)
|
|
56
|
+
reply.header('Cache-Control', 'no-store');
|
|
36
57
|
const projectId = resolveProjectId(request, dependencies.defaultProjectId);
|
|
37
58
|
// Name the Project that was not found. An anonymous 404 here is indistinguishable from a
|
|
38
59
|
// missing Route, and the id is supplied by the caller in a header, so echoing it back is
|
|
@@ -86,6 +107,7 @@ export function createServer(dependencies) {
|
|
|
86
107
|
const bodySchema = requestBody(normalizedMethod, path);
|
|
87
108
|
app.route({ method, url: path, handler, ...(bodySchema === null ? {} : { schema: { body: bodySchema } }) });
|
|
88
109
|
};
|
|
110
|
+
const page = (request) => pageOptions(request, dependencies.config.current.config.snapshot);
|
|
89
111
|
const project = (request) => {
|
|
90
112
|
const context = contexts.get(request);
|
|
91
113
|
if (context === undefined)
|
|
@@ -113,7 +135,7 @@ export function createServer(dependencies) {
|
|
|
113
135
|
// in the 503 the two look alike, and offering a hard kill on that basis can terminate an
|
|
114
136
|
// unrelated process. `pid` is what the endpoint-owner lookup verifies against.
|
|
115
137
|
if (ready.mode !== 'full')
|
|
116
|
-
return reply.status(503).send({ error: { code: 'RUNTIME_NOT_READY', message: 'Lens Core is not ready.', details: { blockers: ready.blockers, ...componentSummary() } } });
|
|
138
|
+
return reply.status(503).send({ error: { code: 'RUNTIME_NOT_READY', message: 'Lens Core is not ready.', retryable: true, details: { blockers: ready.blockers, ...componentSummary() } } });
|
|
117
139
|
return {
|
|
118
140
|
data: {
|
|
119
141
|
status: 'ready',
|
|
@@ -156,9 +178,35 @@ export function createServer(dependencies) {
|
|
|
156
178
|
});
|
|
157
179
|
route('PUT', '/api/v1/config', async (request) => {
|
|
158
180
|
const input = body(request);
|
|
159
|
-
await dependencies.config.replace(text(input.content, 'content', 4 * 1024 * 1024), text(input.expected_file_digest, 'expected_file_digest'));
|
|
181
|
+
await dependencies.config.replace(text(input.content, 'content', 4 * 1024 * 1024), text(input.expected_file_digest, 'expected_file_digest'), () => dependencies.pair.requireStrong(request.headers));
|
|
160
182
|
return { data: dependencies.config.document(), meta: configMeta(dependencies) };
|
|
161
183
|
});
|
|
184
|
+
route('GET', '/api/v1/access', (request) => ({ data: { client_id: contexts.get(request)?.ingress.clientId ?? null, strong_trust: contexts.get(request)?.ingress.strongTrust === true } }));
|
|
185
|
+
route('GET', '/api/v1/config/files', async () => ({ data: [{ id: 'lens', format: 'jsonc' }, { id: 'pair', format: 'json' }, ...(await files().list()).map(id => ({ id, format: id.endsWith('jsonc') ? 'jsonc' : 'json' }))] }));
|
|
186
|
+
route('GET', '/api/v1/config/files/:file_id', async (request) => {
|
|
187
|
+
const fileId = param(request, 'file_id');
|
|
188
|
+
if (fileId === 'lens')
|
|
189
|
+
return { data: dependencies.config.document() };
|
|
190
|
+
if (fileId === 'pair')
|
|
191
|
+
return { data: await dependencies.pair.document() };
|
|
192
|
+
return { data: await files().document(fileId) };
|
|
193
|
+
});
|
|
194
|
+
route('PUT', '/api/v1/config/files/:file_id', async (request) => {
|
|
195
|
+
const input = body(request);
|
|
196
|
+
const source = text(input.content, 'content', 4 * 1024 * 1024);
|
|
197
|
+
const expected = text(input.expected_file_digest, 'expected_file_digest');
|
|
198
|
+
const fileId = param(request, 'file_id');
|
|
199
|
+
if (fileId === 'lens') {
|
|
200
|
+
await dependencies.config.replace(source, expected, () => dependencies.pair.requireStrong(request.headers));
|
|
201
|
+
return { data: dependencies.config.document() };
|
|
202
|
+
}
|
|
203
|
+
if (fileId === 'pair') {
|
|
204
|
+
await dependencies.pair.replace(source, expected, request.headers);
|
|
205
|
+
return { data: { saved: true } };
|
|
206
|
+
}
|
|
207
|
+
await files().replace(fileId, source, expected, () => dependencies.pair.requireStrong(request.headers));
|
|
208
|
+
return { data: await files().document(fileId) };
|
|
209
|
+
});
|
|
162
210
|
route('GET', '/api/v1/config/options', (request) => ({ data: configOptions(dependencies, project(request)) }));
|
|
163
211
|
route('GET', '/api/v1/capabilities', (request) => {
|
|
164
212
|
// 02A: Page<CapabilityCatalogEntry>. A bare array shares no shape with that, so a client
|
|
@@ -167,15 +215,58 @@ export function createServer(dependencies) {
|
|
|
167
215
|
return { data: wholePage(dependencies.runtime(project(request)).registry.catalog(), dependencies) };
|
|
168
216
|
});
|
|
169
217
|
route('GET', '/api/v1/capabilities/:capability_id', (request) => ({ data: dependencies.runtime(project(request)).registry.get(param(request, 'capability_id')) }));
|
|
218
|
+
async function projectExecutorOperation(operation) {
|
|
219
|
+
try {
|
|
220
|
+
return await operation();
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof ExecutorCallError) {
|
|
224
|
+
if ([7, 16].includes(error.grpcCode))
|
|
225
|
+
fail('EXECUTOR_ACCESS_DENIED', 403, 'Executor denied access. Check Core approval, directory authorization and whether this operation is allowed before retrying.');
|
|
226
|
+
if ([3, 5].includes(error.grpcCode))
|
|
227
|
+
fail('EXECUTOR_PATH_INVALID', 400, 'Executor could not browse the requested server directory.');
|
|
228
|
+
fail('EXECUTOR_UNAVAILABLE', 503, 'Executor is unavailable.');
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
170
233
|
route('GET', '/api/v1/projects', (request) => ({ data: dependencies.store.listProjects(page(request), archiveMode(request)) }));
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
234
|
+
const endpoint = (value) => {
|
|
235
|
+
const selected = executorId(value);
|
|
236
|
+
if (dependencies.executorRouter)
|
|
237
|
+
return dependencies.executorRouter.endpoint(selected);
|
|
238
|
+
if (selected === 'remote')
|
|
239
|
+
return dependencies.executor;
|
|
240
|
+
if (selected !== 'local')
|
|
241
|
+
fail('EXECUTOR_NOT_CONFIGURED', 409, 'Executor is not configured.');
|
|
242
|
+
if (!dependencies.localExecutor)
|
|
243
|
+
fail('LOCAL_EXECUTOR_NOT_CONFIGURED', 409, 'Local Executor is not configured.');
|
|
244
|
+
return dependencies.localExecutor;
|
|
245
|
+
};
|
|
246
|
+
route('GET', '/api/v1/executors', () => {
|
|
247
|
+
const config = dependencies.config.current.config;
|
|
248
|
+
const entries = [
|
|
249
|
+
{ executor_id: 'remote', name: config.executor.sharedFilesystem === true && !dependencies.localExecutor ? 'This computer' : 'Primary Executor', is_local: config.executor.sharedFilesystem === true && !dependencies.localExecutor, default_workspace_path: config.executor.sharedFilesystem === true ? resolve(dirname(dependencies.config.path), '../workspace') : null },
|
|
250
|
+
...(dependencies.localExecutor ? [{ executor_id: 'local', name: 'This computer', is_local: true, default_workspace_path: resolve(dirname(dependencies.config.path), '../workspace') }] : []),
|
|
251
|
+
...Object.entries(config.executors ?? {}).map(([id, value]) => ({ executor_id: id, name: value.name, is_local: false, default_workspace_path: null }))
|
|
252
|
+
];
|
|
253
|
+
return { data: { items: entries } };
|
|
254
|
+
});
|
|
255
|
+
route('GET', '/api/v1/projects/browse', async (request, reply) => ({ data: await projectExecutorOperation(() => endpoint(query(request).executor_id).browse(optionalText(query(request).path, 'path') ?? '.', clientSignal(request, reply))) }));
|
|
256
|
+
route('POST', '/api/v1/projects/browse/open', async (request, reply) => ({ data: await endpoint(body(request).executor_id).open(paths(body(request).workspace_paths), text(body(request).path, 'path'), clientSignal(request, reply)) }));
|
|
257
|
+
route('POST', '/api/v1/projects/choose-folder', async (request, reply) => {
|
|
258
|
+
const selected = executorId(body(request).executor_id);
|
|
259
|
+
const isLocal = selected === 'local' ? dependencies.localExecutor !== undefined : selected === 'remote' && dependencies.config.current.config.executor.sharedFilesystem === true && !dependencies.localExecutor;
|
|
260
|
+
if (!isLocal)
|
|
261
|
+
fail('NATIVE_FOLDER_PICKER_LOCAL_ONLY', 400, 'Native folder selection is only available on this computer. Enter a remote folder path manually.');
|
|
262
|
+
return { data: await projectExecutorOperation(() => endpoint(selected).chooseFolder(optionalText(body(request).initial_path, 'initial_path'), clientSignal(request, reply))) };
|
|
263
|
+
});
|
|
174
264
|
route('POST', '/api/v1/projects', async (request, reply) => {
|
|
175
265
|
const input = body(request);
|
|
176
266
|
const workspacePaths = paths(input.workspace_paths ?? input.paths);
|
|
177
|
-
const
|
|
178
|
-
|
|
267
|
+
const selected = executorId(input.executor_id);
|
|
268
|
+
const validation = await validateWorkspacePaths({ executor: endpoint(selected) }, workspacePaths, input.create_if_missing === true);
|
|
269
|
+
return reply.status(201).send({ data: dependencies.store.createProject(text(input.name, 'name', 256), validatedWorkspacePaths(validation), selected) });
|
|
179
270
|
});
|
|
180
271
|
route('GET', '/api/v1/projects/:project_id', (request) => ({ data: dependencies.store.projectDetail(param(request, 'project_id')) }));
|
|
181
272
|
route('PATCH', '/api/v1/projects/:project_id', (request) => ({ data: dependencies.store.updateProject(param(request, 'project_id'), expectedRevision(body(request).expected_revision), text(body(request).name, 'name', 256)) }));
|
|
@@ -186,8 +277,9 @@ export function createServer(dependencies) {
|
|
|
186
277
|
const projectId = param(request, 'project_id');
|
|
187
278
|
const input = body(request);
|
|
188
279
|
const workspacePaths = paths(input.paths);
|
|
189
|
-
const
|
|
190
|
-
const
|
|
280
|
+
const selected = executorId(input.executor_id);
|
|
281
|
+
const validation = await validateWorkspacePaths({ executor: endpoint(selected) }, workspacePaths, input.create_if_missing === true);
|
|
282
|
+
const workspace = dependencies.store.createWorkspace(projectId, text(input.name, 'name', 256), validatedWorkspacePaths(validation), input.is_default === true, selected);
|
|
191
283
|
dependencies.refreshProject(projectId);
|
|
192
284
|
return reply.status(201).send({ data: workspace });
|
|
193
285
|
});
|
|
@@ -196,7 +288,10 @@ export function createServer(dependencies) {
|
|
|
196
288
|
const projectId = param(request, 'project_id');
|
|
197
289
|
const input = body(request);
|
|
198
290
|
const workspacePaths = paths(input.paths);
|
|
199
|
-
const
|
|
291
|
+
const existing = dependencies.store.getWorkspace(projectId, param(request, 'workspace_id'));
|
|
292
|
+
if (input.executor_id !== undefined && input.executor_id !== existing.executor_id)
|
|
293
|
+
fail('WORKSPACE_EXECUTOR_IMMUTABLE', 409, 'Create another Workspace to change execution endpoint.');
|
|
294
|
+
const validation = await validateWorkspacePaths({ executor: endpoint(existing.executor_id) }, workspacePaths, input.create_if_missing === true);
|
|
200
295
|
const workspace = dependencies.store.updateWorkspace(projectId, param(request, 'workspace_id'), expectedRevision(input.expected_revision), text(input.name, 'name', 256), validatedWorkspacePaths(validation), input.is_default === true);
|
|
201
296
|
dependencies.refreshProject(projectId);
|
|
202
297
|
return { data: workspace };
|
|
@@ -212,12 +307,50 @@ export function createServer(dependencies) {
|
|
|
212
307
|
route('GET', '/api/v1/tasks', (request) => ({ data: dependencies.store.listAllTasks(project(request), page(request), query(request).archived === 'all') }));
|
|
213
308
|
route('POST', '/api/v1/tasks', (request, reply) => reply.status(201).send({ data: dependencies.store.createTask(project(request), text(body(request).title, 'title', 256)) }));
|
|
214
309
|
route('GET', '/api/v1/tasks/:task_id', (request) => ({ data: dependencies.store.taskDetail(project(request), param(request, 'task_id')) }));
|
|
310
|
+
route('GET', '/api/v1/tasks/:task_id/audit', async (request) => {
|
|
311
|
+
const projectId = project(request);
|
|
312
|
+
const taskId = param(request, 'task_id');
|
|
313
|
+
dependencies.store.getTask(projectId, taskId);
|
|
314
|
+
const options = page(request);
|
|
315
|
+
const source = optionalText(query(request).source, 'source') ?? 'core';
|
|
316
|
+
if (source !== 'core' && source !== 'executor')
|
|
317
|
+
fail('AUDIT_SOURCE_INVALID', 400, 'Audit source must be core or executor.');
|
|
318
|
+
if (source === 'executor') {
|
|
319
|
+
try {
|
|
320
|
+
return { data: await endpoint(query(request).executor_id).taskAudit(projectId, taskId, options.limit, options.cursor) };
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (error instanceof ExecutorCallError) {
|
|
324
|
+
if (error.grpcCode === 3)
|
|
325
|
+
fail('AUDIT_CURSOR_INVALID', 400, 'Executor rejected the audit query or cursor.');
|
|
326
|
+
if (error.grpcCode === 9)
|
|
327
|
+
fail('AUDIT_CURSOR_STALE', 409, 'Executor audit cursor is stale.');
|
|
328
|
+
if ([7, 16].includes(error.grpcCode))
|
|
329
|
+
fail('EXECUTOR_AUDIT_FORBIDDEN', 403, 'Executor refused the audit query.');
|
|
330
|
+
if (error.grpcCode === 15)
|
|
331
|
+
fail('AUDIT_INTEGRITY_ERROR', 503, 'Executor audit integrity check failed.');
|
|
332
|
+
if (error.grpcCode === 8)
|
|
333
|
+
fail('AUDIT_QUERY_LIMIT', 503, 'Executor audit query exceeded its scan or response limits.');
|
|
334
|
+
if (error.grpcCode === 12)
|
|
335
|
+
fail('EXECUTOR_AUDIT_UNSUPPORTED', 503, 'Executor does not support audit queries; deploy a matching protocol version.');
|
|
336
|
+
}
|
|
337
|
+
if (error instanceof LensError)
|
|
338
|
+
throw error;
|
|
339
|
+
fail('EXECUTOR_AUDIT_UNAVAILABLE', 503, 'Executor audit storage or transport is unavailable.');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (dependencies.taskAudit === undefined)
|
|
343
|
+
fail('AUDIT_UNAVAILABLE', 503, 'Task audit storage is not configured.');
|
|
344
|
+
return { data: await dependencies.taskAudit.query(projectId, taskId, options.limit, options.cursor) };
|
|
345
|
+
});
|
|
215
346
|
route('PATCH', '/api/v1/tasks/:task_id', (request) => ({ data: dependencies.store.updateTask(project(request), param(request, 'task_id'), expectedRevision(body(request).expected_revision), text(body(request).title, 'title', 256)) }));
|
|
216
347
|
route('POST', '/api/v1/tasks/:task_id/archive', (request) => ({ data: dependencies.store.archiveTask(project(request), param(request, 'task_id'), expectedRevision(body(request).expected_revision), true) }));
|
|
217
348
|
route('POST', '/api/v1/tasks/:task_id/restore', (request) => ({ data: dependencies.store.archiveTask(project(request), param(request, 'task_id'), expectedRevision(body(request).expected_revision), false) }));
|
|
218
349
|
route('DELETE', '/api/v1/tasks/:task_id', async (request) => {
|
|
219
|
-
const purged = dependencies.store.deleteTask(project(request), param(request, 'task_id'), queryRevision(request));
|
|
220
|
-
|
|
350
|
+
const purged = await commitPurge(dependencies.executorRouter ?? dependencies.executor, stateId => dependencies.store.deleteTask(project(request), param(request, 'task_id'), queryRevision(request), stateId));
|
|
351
|
+
if ([purged.purged_calls, purged.orphaned_artifact_paths].some(value => Array.isArray(value) && value.length > 0)) {
|
|
352
|
+
await applyPurge(project(request), dependencies.store, dependencies.artifacts, dependencies.executorRouter ?? dependencies.executor);
|
|
353
|
+
}
|
|
221
354
|
return { data: { ok: true } };
|
|
222
355
|
});
|
|
223
356
|
route('GET', '/api/v1/tasks/:task_id/sessions', (request) => ({ data: dependencies.store.listSessions(project(request), param(request, 'task_id'), page(request), query(request).archived === 'all') }));
|
|
@@ -238,7 +371,8 @@ export function createServer(dependencies) {
|
|
|
238
371
|
model_reasoning_effort: defaults.modelReasoningEffort,
|
|
239
372
|
policy_preset: defaults.policyPreset,
|
|
240
373
|
initial_prompt: initialPrompt ?? null,
|
|
241
|
-
initial_message: initialMessage ?? null
|
|
374
|
+
initial_message: initialMessage ?? null,
|
|
375
|
+
agent_name: optionalText(input.agent_name, 'agent_name', 256) ?? title
|
|
242
376
|
};
|
|
243
377
|
const command = ingressCommand(request, 'session.create', payload, 'SESSION_REQUEST_KEY_REQUIRED');
|
|
244
378
|
const created = dependencies.store.createSessionWithAgent(projectId, {
|
|
@@ -273,10 +407,14 @@ export function createServer(dependencies) {
|
|
|
273
407
|
route('PATCH', '/api/v1/agents/:agent_id', (request) => {
|
|
274
408
|
const input = body(request);
|
|
275
409
|
const current = dependencies.store.getAgent(project(request), param(request, 'agent_id'));
|
|
410
|
+
const model = optionalText(input.model_reference, 'model_reference') ?? stringValue(current.model);
|
|
411
|
+
const effort = optionalText(input.model_reasoning_effort, 'model_reasoning_effort') ?? stringValue(current.model_reasoning_effort);
|
|
412
|
+
dependencies.models.resolve(model);
|
|
413
|
+
resolveReasoningEffort(dependencies.config.current.config, model, effort);
|
|
276
414
|
return { data: dependencies.store.updateAgent(project(request), param(request, 'agent_id'), expectedRevision(input.expected_revision), {
|
|
277
415
|
name: optionalText(input.name, 'name', 256) ?? stringValue(current.name),
|
|
278
|
-
model
|
|
279
|
-
modelReasoningEffort:
|
|
416
|
+
model,
|
|
417
|
+
modelReasoningEffort: effort,
|
|
280
418
|
policyPreset: optionalText(input.policy_preset, 'policy_preset') ?? stringValue(current.policy_preset)
|
|
281
419
|
}) };
|
|
282
420
|
});
|
|
@@ -322,6 +460,7 @@ export function createServer(dependencies) {
|
|
|
322
460
|
const runId = stringValue(dependencies.store.getStep(project(request), param(request, 'step_id')).run_id);
|
|
323
461
|
return { data: dependencies.store.getRunGoal(project(request), runId) };
|
|
324
462
|
});
|
|
463
|
+
route('GET', '/api/v1/agents/:agent_id/cache-usage', (request) => ({ data: dependencies.store.cacheUsage(project(request), param(request, 'agent_id')) }));
|
|
325
464
|
route('GET', '/api/v1/agents/:agent_id/model-attempts', (request) => ({ data: dependencies.store.listAttempts(project(request), 'agent_id', param(request, 'agent_id'), page(request)) }));
|
|
326
465
|
route('GET', '/api/v1/steps/:step_id/model-attempts', (request) => ({ data: dependencies.store.listAttempts(project(request), 'step_id', param(request, 'step_id'), page(request)) }));
|
|
327
466
|
route('GET', '/api/v1/model-attempts/:attempt_id', (request) => ({ data: dependencies.store.getAttempt(project(request), param(request, 'attempt_id')) }));
|
|
@@ -345,19 +484,21 @@ export function createServer(dependencies) {
|
|
|
345
484
|
route('POST', '/api/v1/agents/:agent_id/context/preview', (request) => {
|
|
346
485
|
const agent = dependencies.store.getAgent(project(request), param(request, 'agent_id'));
|
|
347
486
|
const model = dependencies.models.resolve(stringValue(agent.model));
|
|
348
|
-
|
|
349
|
-
if (prepared.sourceRevision !== expectedRevision(body(request).expected_context_revision))
|
|
487
|
+
if (agent.context_revision !== expectedRevision(body(request).expected_context_revision))
|
|
350
488
|
fail('REVISION_CONFLICT', 409, 'The Context changed before preview.');
|
|
489
|
+
const source = dependencies.store.contextSource(project(request), param(request, 'agent_id'));
|
|
490
|
+
const prepared = dependencies.runtime(project(request)).context.prepareSource(source, dependencies.config.current.config, model.contextWindowTokens, model.profile);
|
|
351
491
|
return { data: prepared };
|
|
352
492
|
});
|
|
353
|
-
route('POST', '/api/v1/agents/:agent_id/context/compact', async (request, reply) => ({ data: await dependencies.runtime(project(request)).agents.compact(project(request), param(request, 'agent_id'), expectedRevision(body(request).expected_context_revision), optionalText(body(request).
|
|
493
|
+
route('POST', '/api/v1/agents/:agent_id/context/compact', async (request, reply) => ({ data: await dependencies.runtime(project(request)).agents.compact(project(request), param(request, 'agent_id'), expectedRevision(body(request).expected_context_revision), optionalText(body(request).focus, 'focus'), clientSignal(request, reply)) }));
|
|
354
494
|
route('POST', '/api/v1/agents/:agent_id/context/fork', (request, reply) => {
|
|
355
495
|
const projectId = project(request);
|
|
356
496
|
const source = dependencies.store.getAgent(projectId, param(request, 'agent_id'));
|
|
357
497
|
const input = body(request);
|
|
358
498
|
const model = optionalText(input.model_reference, 'model_reference') ?? stringValue(source.model);
|
|
359
499
|
const effort = optionalText(input.model_reasoning_effort, 'model_reasoning_effort') ?? stringValue(source.model_reasoning_effort);
|
|
360
|
-
|
|
500
|
+
dependencies.models.resolve(model);
|
|
501
|
+
resolveReasoningEffort(dependencies.config.current.config, model, effort);
|
|
361
502
|
return reply.status(201).send({ data: dependencies.store.createContextFork(projectId, param(request, 'agent_id'), {
|
|
362
503
|
throughTurnGroupId: text(input.through_turn_group_id, 'through_turn_group_id'),
|
|
363
504
|
expectedContextRevision: expectedRevision(input.expected_context_revision),
|
|
@@ -368,6 +509,8 @@ export function createServer(dependencies) {
|
|
|
368
509
|
});
|
|
369
510
|
route('GET', '/api/v1/agents/:agent_id/context/turn-groups', (request) => ({ data: dependencies.store.listTurnGroups(project(request), param(request, 'agent_id'), page(request)) }));
|
|
370
511
|
route('GET', '/api/v1/agents/:agent_id/context/projection/decisions', (request) => ({ data: dependencies.store.listProjectionDecisions(project(request), param(request, 'agent_id'), page(request)) }));
|
|
512
|
+
route('GET', '/api/v1/agents/:agent_id/context/working', (request) => ({ data: dependencies.store.workingContext(project(request), param(request, 'agent_id')) }));
|
|
513
|
+
route('GET', '/api/v1/context/:context_item_id/sources', (request) => ({ data: dependencies.store.compactionSources(project(request), param(request, 'context_item_id')) }));
|
|
371
514
|
route('GET', '/api/v1/calls', (request) => ({ data: dependencies.store.listCalls(project(request), page(request), { ...(typeof query(request).state === 'string' ? { state: query(request).state } : {}) }) }));
|
|
372
515
|
route('POST', '/api/v1/calls', (request, reply) => {
|
|
373
516
|
const input = body(request);
|
|
@@ -382,7 +525,7 @@ export function createServer(dependencies) {
|
|
|
382
525
|
}, 'DIRECT_REQUEST_KEY_REQUIRED');
|
|
383
526
|
const replay = dependencies.store.ingressReplay(command);
|
|
384
527
|
if (replay !== null)
|
|
385
|
-
return reply.status(202).send({ data: replay });
|
|
528
|
+
return reply.status(202).send({ data: dependencies.store.callSummary(project(request), stringValue(replay.id)) });
|
|
386
529
|
const effect = dependencies.runtime(project(request)).registry.effect(capabilityId, argumentsValue);
|
|
387
530
|
const call = dependencies.store.createCall(project(request), {
|
|
388
531
|
workspaceId,
|
|
@@ -468,7 +611,7 @@ export function createServer(dependencies) {
|
|
|
468
611
|
fail('INVALID_INPUT', 400, 'call_ids must all belong to one Workspace.');
|
|
469
612
|
const workspaceId = [...workspaceIds][0] ?? '';
|
|
470
613
|
const paths = dependencies.store.projectWorkspacePaths(projectId, workspaceId);
|
|
471
|
-
return { data: await dependencies.executor.describeCallHistory(workspaceId, callIds, paths, false) };
|
|
614
|
+
return { data: await (dependencies.executorRouter?.workspace(project(request), workspaceId) ?? dependencies.executor).describeCallHistory(workspaceId, callIds, paths, false) };
|
|
472
615
|
});
|
|
473
616
|
route('POST', '/api/v1/agents/:agent_id/subagents', async (request, reply) => {
|
|
474
617
|
const projectId = project(request);
|
|
@@ -482,6 +625,9 @@ export function createServer(dependencies) {
|
|
|
482
625
|
model: defaults.model,
|
|
483
626
|
model_reasoning_effort: defaults.modelReasoningEffort,
|
|
484
627
|
policy_preset: defaults.policyPreset,
|
|
628
|
+
focus: typeof input.focus === 'string' ? input.focus : null,
|
|
629
|
+
fork_context: jsonValue(input.fork_context),
|
|
630
|
+
initial_prompt: optionalText(input.initial_prompt, 'initial_prompt') ?? null,
|
|
485
631
|
message: message ?? null
|
|
486
632
|
}, 'IDEMPOTENCY_KEY_INVALID');
|
|
487
633
|
const forkContext = input.fork_context;
|
|
@@ -494,6 +640,7 @@ export function createServer(dependencies) {
|
|
|
494
640
|
model: defaults.model,
|
|
495
641
|
modelReasoningEffort: defaults.modelReasoningEffort,
|
|
496
642
|
...(typeof input.initial_prompt === 'string' ? { initialPrompt: input.initial_prompt } : {}),
|
|
643
|
+
...(typeof input.focus === 'string' ? { compactFocus: input.focus } : {}),
|
|
497
644
|
...(message === undefined ? {} : { initialMessage: message }),
|
|
498
645
|
ingress: command
|
|
499
646
|
}, clientSignal(request, reply));
|
|
@@ -537,8 +684,8 @@ export function createServer(dependencies) {
|
|
|
537
684
|
meta: runtimeMeta(dependencies, projectId)
|
|
538
685
|
};
|
|
539
686
|
});
|
|
540
|
-
route('GET', '/api/v1/events', async (request, reply) => streamEvents(dependencies, contexts.get(request), reply
|
|
541
|
-
route('GET', '/api/v1/runtime/diagnostics', () => ({ data: { readiness: dependencies.ready(), config: configMeta(dependencies), state_revision: dependencies.store.stateRevision(), storage: dependencies.store.storageStats() } }));
|
|
687
|
+
route('GET', '/api/v1/events', async (request, reply) => streamEvents(dependencies, contexts.get(request), reply));
|
|
688
|
+
route('GET', '/api/v1/runtime/diagnostics', () => ({ data: { readiness: dependencies.ready(), config: configMeta(dependencies), state_revision: dependencies.store.stateRevision(), storage: dependencies.store.storageStats(), observability: dependencies.observability.status() } }));
|
|
542
689
|
// The three diagnostic windows are documented as RuntimeRecordWindow<T>, not bare arrays.
|
|
543
690
|
// They are bounded in-process buffers with no replay, so `next_sequence` is the highest
|
|
544
691
|
// sequence returned and `has_more` reports whether the window filled the requested limit.
|
|
@@ -548,13 +695,14 @@ export function createServer(dependencies) {
|
|
|
548
695
|
route('GET', '/api/v1/runtime/metrics', (_request, reply) => reply.type('text/plain; version=0.0.4').send(dependencies.observability.metrics()));
|
|
549
696
|
route('GET', '/api/v1/runtime/operations', (request) => ({ data: { calls: dependencies.store.listCalls(project(request), { limit: dependencies.config.current.config.snapshot.pageSizeMax }, { state: 'RUNNING' }) } }));
|
|
550
697
|
route('POST', '/api/v1/runtime/maintenance/run', async (request) => {
|
|
551
|
-
const
|
|
552
|
-
const
|
|
698
|
+
const before = optionalText(body(request).before, 'before') ?? new Date().toISOString();
|
|
699
|
+
const purged = await commitPurge(dependencies.executorRouter ?? dependencies.executor, stateId => dependencies.store.purgeArchived(project(request), before, stateId));
|
|
700
|
+
const files = await applyPurge(project(request), dependencies.store, dependencies.artifacts, dependencies.executorRouter ?? dependencies.executor);
|
|
553
701
|
// purged_calls is an instruction for applyPurge, not part of the Route response.
|
|
554
702
|
return { data: { purged_tasks: purged.purged_tasks, orphaned_artifact_paths: purged.orphaned_artifact_paths, files } };
|
|
555
703
|
});
|
|
556
704
|
route('GET', '/api/v1/runtime/storage', () => ({ data: dependencies.store.storageStats() }));
|
|
557
|
-
app.setNotFoundHandler((_request, reply) => reply.status(404).send({ error: { code: 'ROUTE_NOT_FOUND', message: 'The requested Route does not exist.', details: {} } }));
|
|
705
|
+
app.setNotFoundHandler((_request, reply) => reply.status(404).send({ error: { code: 'ROUTE_NOT_FOUND', message: 'The requested Route does not exist.', retryable: false, details: {} } }));
|
|
558
706
|
return app;
|
|
559
707
|
}
|
|
560
708
|
function createAgent(dependencies, projectId, taskId, input) {
|
|
@@ -583,8 +731,8 @@ function agentDefaults(dependencies, projectId, roleName, input) {
|
|
|
583
731
|
const preset = dependencies.config.current.config.policyPresets[policyPreset];
|
|
584
732
|
if (preset === undefined)
|
|
585
733
|
fail('POLICY_PRESET_MISSING', 409, 'The selected Policy preset does not exist.');
|
|
586
|
-
|
|
587
|
-
resolveReasoningEffort(
|
|
734
|
+
dependencies.models.resolve(model);
|
|
735
|
+
resolveReasoningEffort(dependencies.config.current.config, model, modelReasoningEffort);
|
|
588
736
|
const registry = dependencies.runtime(projectId).registry;
|
|
589
737
|
const capabilityIds = [...new Set([
|
|
590
738
|
...registry.list().filter((definition) => definition.defaultAssignment === 'all_new_agents').map((definition) => definition.id),
|
|
@@ -631,14 +779,23 @@ function refreshCapabilities(dependencies, projectId, agentId) {
|
|
|
631
779
|
});
|
|
632
780
|
return dependencies.store.replaceCapabilities(projectId, agentId, capabilities, loaded);
|
|
633
781
|
}
|
|
634
|
-
async function streamEvents(dependencies, context, reply
|
|
782
|
+
async function streamEvents(dependencies, context, reply) {
|
|
635
783
|
if (context === undefined)
|
|
636
784
|
fail('TRUSTED_INGRESS_REQUIRED', 500, 'Trusted ingress is missing.');
|
|
637
785
|
const subscription = dependencies.events.subscribe(context.ingress.clientId, context.projectId);
|
|
638
786
|
reply.hijack();
|
|
639
787
|
reply.raw.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive' });
|
|
640
788
|
const controller = new AbortController();
|
|
641
|
-
|
|
789
|
+
const abort = () => controller.abort();
|
|
790
|
+
reply.raw.once('close', abort);
|
|
791
|
+
reply.raw.once('error', abort);
|
|
792
|
+
const drain = async () => {
|
|
793
|
+
if (reply.raw.writableNeedDrain)
|
|
794
|
+
await once(reply.raw, 'drain', {
|
|
795
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(dependencies.config.current.config.sse.heartbeatIntervalMs)])
|
|
796
|
+
});
|
|
797
|
+
controller.signal.throwIfAborted();
|
|
798
|
+
};
|
|
642
799
|
let sequence = 0;
|
|
643
800
|
/**
|
|
644
801
|
* Writes one SSE frame. `kind` and `occurred_at` go in the JSON body, not only in the
|
|
@@ -666,12 +823,17 @@ async function streamEvents(dependencies, context, reply, request) {
|
|
|
666
823
|
state_revision: String(dependencies.store.stateRevision()),
|
|
667
824
|
...runtimeMeta(dependencies, context.projectId)
|
|
668
825
|
});
|
|
669
|
-
const heartbeat = setInterval(() =>
|
|
826
|
+
const heartbeat = setInterval(() => {
|
|
827
|
+
if (!reply.raw.writableNeedDrain && !controller.signal.aborted)
|
|
828
|
+
send('runtime.notice', { code: 'heartbeat', state_revision: String(dependencies.store.stateRevision()), ...runtimeMeta(dependencies, context.projectId) });
|
|
829
|
+
}, dependencies.config.current.config.sse.heartbeatIntervalMs);
|
|
670
830
|
try {
|
|
671
831
|
for (;;) {
|
|
832
|
+
await drain();
|
|
672
833
|
const event = await subscription.next(controller.signal);
|
|
673
834
|
if (event === null)
|
|
674
835
|
break;
|
|
836
|
+
await drain();
|
|
675
837
|
send(event.type, {
|
|
676
838
|
event_id: event.id,
|
|
677
839
|
project_id: event.projectId,
|
|
@@ -682,10 +844,19 @@ async function streamEvents(dependencies, context, reply, request) {
|
|
|
682
844
|
});
|
|
683
845
|
}
|
|
684
846
|
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
if (!controller.signal.aborted)
|
|
849
|
+
dependencies.observability.record('http', 'sse', 'failed', 0, { error_code: error instanceof Error && error.name === 'TimeoutError' ? 'SSE_BACKPRESSURE_TIMEOUT' : 'SSE_STREAM_FAILED' });
|
|
850
|
+
}
|
|
685
851
|
finally {
|
|
686
852
|
clearInterval(heartbeat);
|
|
687
853
|
subscription.close();
|
|
688
|
-
reply.raw.
|
|
854
|
+
reply.raw.off('close', abort);
|
|
855
|
+
reply.raw.off('error', abort);
|
|
856
|
+
if (reply.raw.writableNeedDrain)
|
|
857
|
+
reply.raw.destroy();
|
|
858
|
+
else
|
|
859
|
+
reply.raw.end();
|
|
689
860
|
}
|
|
690
861
|
}
|
|
691
862
|
function runtimeMeta(dependencies, projectId) {
|
|
@@ -694,6 +865,7 @@ function runtimeMeta(dependencies, projectId) {
|
|
|
694
865
|
config_digest: dependencies.config.current.digest,
|
|
695
866
|
pair_digest: dependencies.pair.pairDigest,
|
|
696
867
|
runtime_contract_digest: dependencies.runtime(projectId).contractDigest(),
|
|
868
|
+
project_capabilities_digest: dependencies.runtime(projectId).capabilitiesDigest(),
|
|
697
869
|
access_contract: accessContract(dependencies)
|
|
698
870
|
};
|
|
699
871
|
}
|
|
@@ -736,14 +908,6 @@ function requireLoopback(request) {
|
|
|
736
908
|
fail('SHUTDOWN_FORBIDDEN', 403, 'Shutdown may only be requested from the local machine.');
|
|
737
909
|
}
|
|
738
910
|
}
|
|
739
|
-
function loopbackAddress(address) {
|
|
740
|
-
// Absent for a non-IP socket. Refusing is the safe answer: the caller cannot be shown to be local.
|
|
741
|
-
if (address === undefined)
|
|
742
|
-
return false;
|
|
743
|
-
// A dual-stack listener reports an IPv4 peer as `::ffff:127.0.0.1`.
|
|
744
|
-
const normalized = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address;
|
|
745
|
-
return normalized === '::1' || /^127\./.test(normalized);
|
|
746
|
-
}
|
|
747
911
|
/**
|
|
748
912
|
* CORE-15. 02A defines this as the catalog a client needs to offer model, reasoning-effort,
|
|
749
913
|
* Runtime Role and Policy choices: `models`, `model_profiles`, `roles`, `policy_presets` and
|
|
@@ -758,7 +922,6 @@ function loopbackAddress(address) {
|
|
|
758
922
|
* truth. Reasoning efforts follow the same rule the config validator applies: a Model Harness
|
|
759
923
|
* Profile constrains them when one is attached, otherwise the full ladder is allowed.
|
|
760
924
|
*/
|
|
761
|
-
const EFFORT_LADDER = ['none', 'minimal', 'low', 'medium', 'high'];
|
|
762
925
|
const EFFECT_SCOPE_DESCRIPTIONS = {
|
|
763
926
|
read_only: 'Reads Workspace state without changing it.',
|
|
764
927
|
workspace_write: 'Creates or modifies files inside the Workspace.',
|
|
@@ -776,22 +939,18 @@ const POLICY_VALUE_DESCRIPTIONS = {
|
|
|
776
939
|
function configOptions(dependencies, projectId) {
|
|
777
940
|
const config = dependencies.config.current.config;
|
|
778
941
|
const profiles = config.modelHarnessProfiles ?? {};
|
|
779
|
-
const efforts = (harnessProfile) => harnessProfile === undefined
|
|
780
|
-
? EFFORT_LADDER
|
|
781
|
-
: profiles[harnessProfile]?.effort.allowed ?? [];
|
|
782
942
|
const models = Object.entries(config.providers).flatMap(([providerId, provider]) => Object.entries(provider.models).map(([modelId, model]) => {
|
|
783
|
-
const allowed =
|
|
943
|
+
const allowed = modelReasoningEfforts(config, `${providerId}/${modelId}`);
|
|
784
944
|
return {
|
|
785
945
|
model_reference: `${providerId}/${modelId}`,
|
|
786
946
|
allowed_reasoning_efforts: allowed,
|
|
787
|
-
default_reasoning_effort:
|
|
788
|
-
? 'medium'
|
|
789
|
-
: profiles[model.harnessProfile]?.effort.default ?? allowed[0] ?? 'medium',
|
|
947
|
+
default_reasoning_effort: allowed[0] ?? '',
|
|
790
948
|
harness_profile_id: model.harnessProfile ?? '',
|
|
791
949
|
harness_profile_version: model.harnessProfile === undefined ? 0 : profiles[model.harnessProfile]?.version ?? 0,
|
|
792
950
|
context_window_tokens: model.contextWindowTokens,
|
|
793
951
|
usage_reliable: model.usageReliable ?? null,
|
|
794
|
-
cache_supported: model.cacheSupported ?? null
|
|
952
|
+
cache_supported: (model.promptCache ?? provider.promptCache)?.mode !== undefined ? (model.promptCache ?? provider.promptCache)?.mode !== 'off' : model.cacheSupported ?? null,
|
|
953
|
+
prompt_cache: model.promptCache ?? provider.promptCache ?? null
|
|
795
954
|
};
|
|
796
955
|
}));
|
|
797
956
|
return {
|
|
@@ -801,8 +960,6 @@ function configOptions(dependencies, projectId) {
|
|
|
801
960
|
model_profiles: Object.entries(profiles).map(([id, profile]) => ({
|
|
802
961
|
id,
|
|
803
962
|
version: profile.version,
|
|
804
|
-
allowed_reasoning_efforts: profile.effort.allowed,
|
|
805
|
-
default_reasoning_effort: profile.effort.default,
|
|
806
963
|
reasoning_state_round_trip: profile.semantics.reasoningStateRoundTrip,
|
|
807
964
|
thinking_continuation: profile.semantics.thinkingContinuation,
|
|
808
965
|
structured_output: profile.semantics.structuredOutput,
|
|
@@ -856,11 +1013,20 @@ function schemaDetails(error) {
|
|
|
856
1013
|
* failure to answer is the Executor's (`EXECUTOR_UNAVAILABLE`, 503, retryable). Collapsing both
|
|
857
1014
|
* into the latter told the user to retry a path that would always be rejected, and hid the reason.
|
|
858
1015
|
*/
|
|
1016
|
+
function executorId(value) {
|
|
1017
|
+
if (value === undefined || value === 'remote')
|
|
1018
|
+
return 'remote';
|
|
1019
|
+
if (typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/u.test(value))
|
|
1020
|
+
return value;
|
|
1021
|
+
fail('EXECUTOR_INVALID', 400, 'executor_id must be a configured Executor identifier.');
|
|
1022
|
+
}
|
|
859
1023
|
async function validateWorkspacePaths(dependencies, workspacePaths, createIfMissing) {
|
|
860
1024
|
try {
|
|
861
1025
|
return await dependencies.executor.validatePaths(workspacePaths, undefined, createIfMissing);
|
|
862
1026
|
}
|
|
863
1027
|
catch (error) {
|
|
1028
|
+
if (error instanceof LensError && error.code === 'SHARED_WORKSPACE_REQUIRED')
|
|
1029
|
+
throw error;
|
|
864
1030
|
if (error instanceof ExecutorCallError && error.refused) {
|
|
865
1031
|
fail('WORKSPACE_PATH_INVALID', 422, error.message, { workspace_paths: [...workspacePaths] });
|
|
866
1032
|
}
|
|
@@ -957,6 +1123,7 @@ function openapi(specs) {
|
|
|
957
1123
|
[spec.method]: {
|
|
958
1124
|
operationId: operationId(spec.method, spec.path),
|
|
959
1125
|
parameters: parameters(spec.method, spec.path),
|
|
1126
|
+
...(spec.path.startsWith('/api/v1/') ? { security: [{ PairBearer: [] }] } : {}),
|
|
960
1127
|
...(body === null ? {} : { requestBody: { required: true, content: { 'application/json': { schema: body } } } }),
|
|
961
1128
|
responses: responses(spec.method, spec.path)
|
|
962
1129
|
}
|
|
@@ -967,24 +1134,31 @@ function openapi(specs) {
|
|
|
967
1134
|
info: { title: 'Lens Core API', version: '1.0.0' },
|
|
968
1135
|
paths,
|
|
969
1136
|
components: {
|
|
1137
|
+
securitySchemes: {
|
|
1138
|
+
PairBearer: {
|
|
1139
|
+
type: 'http',
|
|
1140
|
+
scheme: 'bearer',
|
|
1141
|
+
description: 'Pair credential issued by the local administrator. Send the client identity headers alongside Authorization: Bearer <credential>.'
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
970
1144
|
schemas: {
|
|
971
1145
|
Success: {
|
|
972
1146
|
type: 'object',
|
|
973
|
-
required: ['data'],
|
|
974
|
-
properties: { data: {}, meta: { type: 'object', additionalProperties: true } },
|
|
1147
|
+
required: ['data', 'correlation_id'],
|
|
1148
|
+
properties: { data: {}, correlation_id: { type: 'string' }, meta: { type: 'object', additionalProperties: true } },
|
|
975
1149
|
additionalProperties: false
|
|
976
1150
|
},
|
|
977
1151
|
Error: {
|
|
978
1152
|
type: 'object',
|
|
979
|
-
required: ['error'],
|
|
1153
|
+
required: ['error', 'correlation_id'],
|
|
980
1154
|
properties: {
|
|
981
1155
|
error: {
|
|
982
1156
|
type: 'object',
|
|
983
|
-
required: ['code', 'message', 'details'],
|
|
984
|
-
properties: { code: { type: 'string' }, message: { type: 'string' }, details: {} },
|
|
1157
|
+
required: ['code', 'message', 'details', 'retryable'],
|
|
1158
|
+
properties: { code: { type: 'string' }, message: { type: 'string' }, retryable: { type: 'boolean' }, details: {} },
|
|
985
1159
|
additionalProperties: false
|
|
986
1160
|
},
|
|
987
|
-
|
|
1161
|
+
correlation_id: { type: 'string' }
|
|
988
1162
|
},
|
|
989
1163
|
additionalProperties: false
|
|
990
1164
|
}
|
|
@@ -1005,20 +1179,20 @@ function requestBody(method, path) {
|
|
|
1005
1179
|
return null;
|
|
1006
1180
|
if (path === '/api/v1/config/validate')
|
|
1007
1181
|
return objectSchema({ content: { type: 'string', minLength: 1 } }, ['content']);
|
|
1008
|
-
if (path === '/api/v1/config')
|
|
1182
|
+
if (path === '/api/v1/config' || path === '/api/v1/config/files/:file_id')
|
|
1009
1183
|
return objectSchema({ content: { type: 'string', minLength: 1 }, expected_file_digest: stringSchema }, ['content', 'expected_file_digest']);
|
|
1010
1184
|
if (path === '/api/v1/projects')
|
|
1011
|
-
return objectSchema({ name: stringSchema, workspace_paths: stringArraySchema, create_if_missing: booleanSchema }, ['name', 'workspace_paths']);
|
|
1185
|
+
return objectSchema({ name: stringSchema, workspace_paths: stringArraySchema, executor_id: stringSchema, create_if_missing: booleanSchema }, ['name', 'workspace_paths']);
|
|
1012
1186
|
if (path === '/api/v1/projects/:project_id')
|
|
1013
1187
|
return objectSchema({ name: stringSchema, expected_revision: revisionSchema }, ['name', 'expected_revision']);
|
|
1014
1188
|
if (path === '/api/v1/projects/browse/open')
|
|
1015
|
-
return objectSchema({ workspace_paths: stringArraySchema, path: stringSchema }, ['workspace_paths', 'path']);
|
|
1189
|
+
return objectSchema({ workspace_paths: stringArraySchema, path: stringSchema, executor_id: stringSchema }, ['workspace_paths', 'path']);
|
|
1016
1190
|
if (path === '/api/v1/projects/choose-folder')
|
|
1017
|
-
return objectSchema({ initial_path: stringSchema });
|
|
1191
|
+
return objectSchema({ initial_path: stringSchema, executor_id: stringSchema });
|
|
1018
1192
|
if (path === '/api/v1/calls/revert-preview')
|
|
1019
1193
|
return objectSchema({ call_ids: stringArraySchema }, ['call_ids']);
|
|
1020
1194
|
if (/\/projects\/:project_id\/workspaces(?:\/:workspace_id)?$/.test(path))
|
|
1021
|
-
return objectSchema({ name: stringSchema, paths: stringArraySchema, is_default: booleanSchema, create_if_missing: booleanSchema, expected_revision: revisionSchema }, method === 'patch' ? ['name', 'paths', 'expected_revision'] : ['name', 'paths']);
|
|
1195
|
+
return objectSchema({ name: stringSchema, paths: stringArraySchema, executor_id: stringSchema, is_default: booleanSchema, create_if_missing: booleanSchema, expected_revision: revisionSchema }, method === 'patch' ? ['name', 'paths', 'expected_revision'] : ['name', 'paths']);
|
|
1022
1196
|
if (path.endsWith('/archive') || path.endsWith('/restore'))
|
|
1023
1197
|
return objectSchema({ expected_revision: revisionSchema }, ['expected_revision']);
|
|
1024
1198
|
if (path === '/api/v1/tasks' || path === '/api/v1/projects/:project_id/tasks')
|
|
@@ -1065,12 +1239,12 @@ function requestBody(method, path) {
|
|
|
1065
1239
|
if (path.endsWith('/context/projection/reset'))
|
|
1066
1240
|
return objectSchema({
|
|
1067
1241
|
expected_context_revision: revisionSchema,
|
|
1068
|
-
stages: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'string', enum: ['
|
|
1242
|
+
stages: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'string', enum: ['snip'] } }
|
|
1069
1243
|
}, ['expected_context_revision']);
|
|
1070
1244
|
if (path.endsWith('/context/preview'))
|
|
1071
1245
|
return objectSchema({ expected_context_revision: revisionSchema }, ['expected_context_revision']);
|
|
1072
1246
|
if (path.endsWith('/context/compact'))
|
|
1073
|
-
return objectSchema({ expected_context_revision: revisionSchema,
|
|
1247
|
+
return objectSchema({ expected_context_revision: revisionSchema, focus: stringSchema }, ['expected_context_revision']);
|
|
1074
1248
|
if (path.endsWith('/context/fork'))
|
|
1075
1249
|
return objectSchema({
|
|
1076
1250
|
through_turn_group_id: stringSchema,
|
|
@@ -1095,6 +1269,7 @@ function requestBody(method, path) {
|
|
|
1095
1269
|
return objectSchema({
|
|
1096
1270
|
name: stringSchema,
|
|
1097
1271
|
fork_context: { type: 'string', enum: ['full', 'compact', 'none'] },
|
|
1272
|
+
focus: stringSchema,
|
|
1098
1273
|
model_reference: stringSchema,
|
|
1099
1274
|
model_reasoning_effort: stringSchema,
|
|
1100
1275
|
initial_prompt: { type: 'string' },
|
|
@@ -1124,13 +1299,19 @@ function objectSchema(properties, required = []) {
|
|
|
1124
1299
|
}
|
|
1125
1300
|
function parameters(method, path) {
|
|
1126
1301
|
const result = [];
|
|
1302
|
+
if (method === 'get' && path === '/api/v1/tasks/:task_id/audit') {
|
|
1303
|
+
result.push({ name: 'source', in: 'query', required: false, schema: { type: 'string', enum: ['core', 'executor'], default: 'core' } });
|
|
1304
|
+
}
|
|
1305
|
+
if (method === 'get' && (path === '/api/v1/projects/browse' || path === '/api/v1/tasks/:task_id/audit')) {
|
|
1306
|
+
result.push({ name: 'executor_id', in: 'query', required: false, schema: stringSchema });
|
|
1307
|
+
}
|
|
1127
1308
|
for (const match of path.matchAll(/:([a-zA-Z0-9_]+)/g)) {
|
|
1128
1309
|
result.push({ name: match[1] ?? '', in: 'path', required: true, schema: stringSchema });
|
|
1129
1310
|
}
|
|
1130
1311
|
if (path.startsWith('/api/v1/')) {
|
|
1131
1312
|
result.push({ name: 'X-Lens-Client-Kind', in: 'header', required: true, schema: { type: 'string', enum: ['native', 'browser'] } }, { name: 'X-Lens-Client-Mac', in: 'header', required: false, schema: stringSchema }, { name: 'X-Lens-Hardware-Id', in: 'header', required: false, schema: stringSchema }, { name: 'X-Lens-Browser-Fingerprint', in: 'header', required: false, schema: stringSchema }, { name: 'X-Lens-Project-Id', in: 'header', required: false, schema: stringSchema });
|
|
1132
1313
|
}
|
|
1133
|
-
if (method === 'get' && (/\/tasks$|\/sessions$|\/runs$|\/steps$|\/calls$|\/artifacts$|\/context$|\/timeline$|\/model-attempts$|\/partitions$|\/turn-groups$|\/decisions$|\/logs$|\/traces$|\/agent-ops$/.test(path))) {
|
|
1314
|
+
if (method === 'get' && (/\/audit$|\/tasks$|\/sessions$|\/runs$|\/steps$|\/calls$|\/artifacts$|\/context$|\/timeline$|\/model-attempts$|\/partitions$|\/turn-groups$|\/decisions$|\/logs$|\/traces$|\/agent-ops$/.test(path))) {
|
|
1134
1315
|
result.push({ name: 'cursor', in: 'query', required: false, schema: stringSchema }, { name: 'limit', in: 'query', required: false, schema: { type: 'integer', minimum: 1, maximum: 500 } });
|
|
1135
1316
|
}
|
|
1136
1317
|
if (method === 'delete')
|
|
@@ -1148,26 +1329,27 @@ function idempotentRoute(method, path) {
|
|
|
1148
1329
|
|| path.endsWith('/subagent-broadcasts'));
|
|
1149
1330
|
}
|
|
1150
1331
|
function responses(method, path) {
|
|
1332
|
+
const errors = errorResponses(path.startsWith('/api/v1/'));
|
|
1151
1333
|
if (path === '/api/v1/events')
|
|
1152
|
-
return { '200': { description: 'Server-sent RuntimeEvent stream', content: { 'text/event-stream': { schema: { type: 'string' } } } }, ...
|
|
1334
|
+
return { '200': { description: 'Server-sent RuntimeEvent stream', content: { 'text/event-stream': { schema: { type: 'string' } } } }, ...errors };
|
|
1153
1335
|
if (path.endsWith('/runtime/metrics'))
|
|
1154
|
-
return { '200': { description: 'Prometheus metrics', content: { 'text/plain': { schema: { type: 'string' } } } }, ...
|
|
1336
|
+
return { '200': { description: 'Prometheus metrics', content: { 'text/plain': { schema: { type: 'string' } } } }, ...errors };
|
|
1155
1337
|
if (path === '/api/v1/artifacts/:artifact_id') {
|
|
1156
1338
|
const content = { 'application/octet-stream': { schema: { type: 'string', contentEncoding: 'binary' } } };
|
|
1157
1339
|
return {
|
|
1158
1340
|
'200': { description: 'Artifact bytes', content },
|
|
1159
1341
|
'206': { description: 'Artifact byte range', content },
|
|
1160
|
-
...
|
|
1342
|
+
...errors
|
|
1161
1343
|
};
|
|
1162
1344
|
}
|
|
1163
1345
|
const status = method === 'post' && (/\/projects$|\/workspaces$|\/tasks$|\/sessions$|\/agents$|\/subagents$|\/context-overrides$|\/context\/fork$|\/results\/:result_id\/include$/.test(path)) ? '201'
|
|
1164
1346
|
: method === 'post' && (path === '/health/shutdown' || path.endsWith('/messages') || path === '/api/v1/calls' || path.endsWith('/subagent-messages') || path.endsWith('/subagent-broadcasts')) ? '202'
|
|
1165
1347
|
: '200';
|
|
1166
|
-
return { [status]: { description: 'Success', content: { 'application/json': { schema: { '$ref': '#/components/schemas/Success' } } } }, ...
|
|
1348
|
+
return { [status]: { description: 'Success', content: { 'application/json': { schema: { '$ref': '#/components/schemas/Success' } } } }, ...errors };
|
|
1167
1349
|
}
|
|
1168
|
-
function errorResponses() {
|
|
1350
|
+
function errorResponses(protectedRoute) {
|
|
1169
1351
|
const response = { description: 'Error', content: { 'application/json': { schema: { '$ref': '#/components/schemas/Error' } } } };
|
|
1170
|
-
return Object.fromEntries(['400', '403', '404', '409', '413', '416', '422', '428', '500', '502', '503'].map((status) => [status, response]));
|
|
1352
|
+
return Object.fromEntries(['400', ...(protectedRoute ? ['401'] : []), '403', '404', '409', '413', '416', '422', '428', '500', '502', '503'].map((status) => [status, response]));
|
|
1171
1353
|
}
|
|
1172
1354
|
function operationId(method, path) {
|
|
1173
1355
|
return `${method}_${path.replace(/^\/api\/v1\//, '').replace(/^\//, '').replace(/[:/.-]+/g, '_') || 'root'}`;
|
|
@@ -1241,7 +1423,7 @@ function resolveProjectId(request, fallback) {
|
|
|
1241
1423
|
const OPAQUE_KEYS = new Set([
|
|
1242
1424
|
'inputSchema', 'outputSchema', 'input_schema', 'output_schema',
|
|
1243
1425
|
'input', 'content', 'details', 'payload', 'preparation', 'usage', 'metadata',
|
|
1244
|
-
'answer', 'prompt', 'arguments', 'structured', 'document', 'config'
|
|
1426
|
+
'answer', 'prompt', 'arguments', 'structured', 'document', 'config', 'request', 'observation'
|
|
1245
1427
|
]);
|
|
1246
1428
|
// Core mints identifiers as `<entity>_<uuid>`, so the documented field name is derivable from
|
|
1247
1429
|
// the value itself rather than from a per-Route table.
|
|
@@ -1270,7 +1452,7 @@ function retryableFailure(status) {
|
|
|
1270
1452
|
return status === 408 || status === 429 || status >= 500;
|
|
1271
1453
|
}
|
|
1272
1454
|
function routePath(request) {
|
|
1273
|
-
return request.routeOptions.url ??
|
|
1455
|
+
return request.routeOptions.url ?? 'unmatched';
|
|
1274
1456
|
}
|
|
1275
1457
|
function idempotencyKey(request, missingCode) {
|
|
1276
1458
|
const value = request.headers['idempotency-key'];
|
|
@@ -1294,11 +1476,11 @@ function queryRevision(request) {
|
|
|
1294
1476
|
function param(request, name) {
|
|
1295
1477
|
return text(request.params[name], name, 256);
|
|
1296
1478
|
}
|
|
1297
|
-
function
|
|
1479
|
+
function pageOptions(request, limits) {
|
|
1298
1480
|
const values = query(request);
|
|
1299
1481
|
const cursor = optionalText(values.cursor, 'cursor', 4096);
|
|
1300
|
-
const rawLimit = values.limit === undefined ?
|
|
1301
|
-
const limit = Math.min(integer(rawLimit, 'limit', 1),
|
|
1482
|
+
const rawLimit = values.limit === undefined ? limits.pageSizeDefault : typeof values.limit === 'string' ? Number(values.limit) : values.limit;
|
|
1483
|
+
const limit = Math.min(integer(rawLimit, 'limit', 1), limits.pageSizeMax);
|
|
1302
1484
|
return { ...(cursor === undefined ? {} : { cursor }), limit };
|
|
1303
1485
|
}
|
|
1304
1486
|
function paths(value) {
|