@dev-mainsequence/codex-plugin 0.7.10 → 0.7.12
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/.codex-plugin/plugin.json +1 -1
- package/README.md +149 -155
- package/build-info.json +3 -3
- package/dist/hosts/workspace.js +5 -2
- package/dist/panel-server.js +6 -1
- package/dist/vendor/devtools/index.js +2 -1
- package/dist/vendor/devtools/package.json +1 -1
- package/dist/vendor/devtools/shared/projectTools.js +88 -44
- package/docs/agents-tools-panel.md +115 -508
- package/docs/conversational-mcp.md +86 -47
- package/package.json +4 -4
- package/docs/codex-verification.md +0 -161
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.projectToolCatalogPath = exports.projectToolConnectionLabel = exports.isProjectToolName = exports.MAX_PROJECT_TOOL_TIMEOUT_MS = exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS = exports.PROJECT_TOOL_TIMEOUT_HINT = exports.MAX_PROJECT_TOOLS_PER_CONNECTION = exports.PROJECT_TOOL_PREFIX = void 0;
|
|
36
|
+
exports.projectToolCatalogPath = exports.projectToolConnectionLabel = exports.isProjectToolName = exports.DEFAULT_PROJECT_REFRESH_BUDGET_MS = exports.MAX_PROJECT_TOOL_TIMEOUT_MS = exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS = exports.PROJECT_TOOL_TIMEOUT_HINT = exports.MAX_PROJECT_TOOLS_PER_CONNECTION = exports.PROJECT_TOOL_PREFIX = void 0;
|
|
37
37
|
exports.projectToolName = projectToolName;
|
|
38
38
|
exports.projectToolConnection = projectToolConnection;
|
|
39
39
|
exports.projectToolDefinition = projectToolDefinition;
|
|
@@ -51,12 +51,16 @@ const path = __importStar(require("node:path"));
|
|
|
51
51
|
const node_crypto_1 = require("node:crypto");
|
|
52
52
|
const models_1 = require("./models.js");
|
|
53
53
|
const stateDirectory_1 = require("./stateDirectory.js");
|
|
54
|
+
const repositories_1 = require("./repositories.js");
|
|
54
55
|
exports.PROJECT_TOOL_PREFIX = 'ms_';
|
|
55
56
|
exports.MAX_PROJECT_TOOLS_PER_CONNECTION = 80;
|
|
56
57
|
/** A project may advertise how long one of its tools can take; the client honours it up to the cap. */
|
|
57
58
|
exports.PROJECT_TOOL_TIMEOUT_HINT = 'mainsequence.ai/timeout-seconds/v1';
|
|
58
59
|
exports.DEFAULT_PROJECT_REQUEST_TIMEOUT_MS = 120_000;
|
|
59
60
|
exports.MAX_PROJECT_TOOL_TIMEOUT_MS = 300_000;
|
|
61
|
+
/** A whole refresh, every connection included: a cold project answers `initialize` in up to 120 s, and a
|
|
62
|
+
* host must not hold the shared lock for the 13 minutes once measured. */
|
|
63
|
+
exports.DEFAULT_PROJECT_REFRESH_BUDGET_MS = 300_000;
|
|
60
64
|
const CATALOG_FILE = 'project-tool-catalog.json';
|
|
61
65
|
const LOCK_STALE_MS = 10 * 60_000;
|
|
62
66
|
const UUID = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i;
|
|
@@ -173,12 +177,15 @@ function success(result) {
|
|
|
173
177
|
return result.data;
|
|
174
178
|
}
|
|
175
179
|
/** Every saved connection in scope and its current tools: `mcp-list` (read-only), then one `mcp-tools`
|
|
176
|
-
* per connection, two at a time (the shared transport's per-origin limit)
|
|
177
|
-
* cannot be read now
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
180
|
+
* per connection, two at a time (the shared transport's per-origin limit), all within one budget. A
|
|
181
|
+
* connection whose catalog cannot be read now, or is not read before the budget runs out, stays in the
|
|
182
|
+
* catalog marked `unavailable` with the reason, keeping its `previous` tools when it had any, so a scope
|
|
183
|
+
* is never written empty and healthy when its connections exist; a removed connection loses its tools;
|
|
184
|
+
* connections outside the scope are carried over from `previous` unchanged. No account snapshot is
|
|
185
|
+
* attached: a registered tool is bound to a saved connection, and the session for that connection's
|
|
186
|
+
* backend is verified when the tool is called. */
|
|
181
187
|
async function readProjectToolCatalog(dispatch, options = {}) {
|
|
188
|
+
const started = Date.now(), budgetMs = options.budgetMs ?? exports.DEFAULT_PROJECT_REFRESH_BUDGET_MS;
|
|
182
189
|
const status = options.session ?? success(await dispatch({ action: 'auth-status', ...(options.backend ? { backend: options.backend } : {}) }));
|
|
183
190
|
if (!status.authenticated || !status.backend)
|
|
184
191
|
throw new models_1.ToolingError('NOT_AUTHENTICATED', 'Sign in to MainSequence to register saved project MCP tools.');
|
|
@@ -189,39 +196,57 @@ async function readProjectToolCatalog(dispatch, options = {}) {
|
|
|
189
196
|
const rows = success(await dispatch({ action: 'mcp-list', backend, readOnly: true, ...environment }));
|
|
190
197
|
const previous = options.previous?.connections ?? [];
|
|
191
198
|
const connections = previous.filter(connection => !inScope(connection, scope)), unavailable = [];
|
|
192
|
-
let retained = 0;
|
|
199
|
+
let retained = 0, expired = false, timer;
|
|
200
|
+
const budget = new Promise(resolve => { timer = setTimeout(() => { expired = true; resolve('budget'); }, Math.max(0, budgetMs - (Date.now() - started))); });
|
|
201
|
+
const seconds = budgetMs >= 1000 ? `${Math.round(budgetMs / 1000)} s` : `${budgetMs} ms`;
|
|
202
|
+
function unreadable(row, code, message) {
|
|
203
|
+
const earlier = previous.find(connection => connection.backend === backend && connection.releaseUid === row.resource_release_uid.toLowerCase());
|
|
204
|
+
if (earlier)
|
|
205
|
+
retained += earlier.tools.length;
|
|
206
|
+
connections.push({ ...(earlier ?? projectToolConnection(row)), unavailable: { at: new Date().toISOString(), code, message } });
|
|
207
|
+
unavailable.push((0, exports.projectToolConnectionLabel)(row));
|
|
208
|
+
}
|
|
193
209
|
const pending = [...rows];
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const row = pending.shift();
|
|
198
|
-
try {
|
|
199
|
-
const output = success(await dispatch({ action: 'mcp-tools', backend, releaseUid: row.resource_release_uid, ...environment }));
|
|
200
|
-
const current = output.connection;
|
|
201
|
-
connections.push(projectToolConnection({ ...row, name: row.name ?? current?.name, display_name: row.display_name ?? current?.display_name,
|
|
202
|
-
mcp_connection: { ...row.mcp_connection, organization_environment_name: row.mcp_connection.organization_environment_name ?? current?.mcp_connection?.organization_environment_name } }, output.tools));
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
if (error instanceof models_1.ToolingError && error.code === 'CANCELLED')
|
|
206
|
-
throw error;
|
|
210
|
+
try {
|
|
211
|
+
await Promise.all(Array.from({ length: Math.min(2, pending.length) }, async () => {
|
|
212
|
+
while (pending.length && !expired) {
|
|
207
213
|
options.signal?.throwIfAborted();
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
const row = pending.shift();
|
|
215
|
+
try {
|
|
216
|
+
const outcome = await Promise.race([Promise.resolve(dispatch({ action: 'mcp-tools', backend, releaseUid: row.resource_release_uid, ...environment })).then(result => ({ result })), budget]);
|
|
217
|
+
if (outcome === 'budget') {
|
|
218
|
+
unreadable(row, 'TIMEOUT', `Not read within the ${seconds} refresh budget; its earlier tools stay registered until a later refresh reads it.`);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
const output = success(outcome.result), current = output.connection;
|
|
222
|
+
connections.push(projectToolConnection({ ...row, name: row.name ?? current?.name, display_name: row.display_name ?? current?.display_name,
|
|
223
|
+
mcp_connection: { ...row.mcp_connection, organization_environment_name: row.mcp_connection.organization_environment_name ?? current?.mcp_connection?.organization_environment_name } }, output.tools));
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof models_1.ToolingError && error.code === 'CANCELLED')
|
|
227
|
+
throw error;
|
|
228
|
+
options.signal?.throwIfAborted();
|
|
229
|
+
unreadable(row, error instanceof models_1.ToolingError ? error.code : 'INTERNAL_ERROR', error instanceof models_1.ToolingError ? error.message : 'The catalog could not be read.');
|
|
212
230
|
}
|
|
213
|
-
unavailable.push((0, exports.projectToolConnectionLabel)(row));
|
|
214
231
|
}
|
|
215
|
-
}
|
|
216
|
-
|
|
232
|
+
}));
|
|
233
|
+
// Whatever the budget left unread is reported, never silently dropped.
|
|
234
|
+
for (const row of pending)
|
|
235
|
+
unreadable(row, 'TIMEOUT', `Not read within the ${seconds} refresh budget; its earlier tools stay registered until a later refresh reads it.`);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
}
|
|
217
240
|
connections.sort((a, b) => a.backend.localeCompare(b.backend) || a.environmentUid.localeCompare(b.environmentUid) || a.releaseUid.localeCompare(b.releaseUid));
|
|
218
|
-
return { catalog: { version: 1, updatedAt: new Date().toISOString(), connections }, backend, unavailable, retained };
|
|
241
|
+
return { catalog: { version: 1, updatedAt: new Date().toISOString(), connections }, backend, unavailable, retained, durationMs: Date.now() - started };
|
|
219
242
|
}
|
|
220
243
|
function connectionRecord(value) {
|
|
221
244
|
if (!object(value) || typeof value.backend !== 'string' || !UUID.test(String(value.releaseUid)) || !UUID.test(String(value.environmentUid)) || !Array.isArray(value.tools))
|
|
222
245
|
return;
|
|
246
|
+
const u = value.unavailable;
|
|
223
247
|
return { backend: value.backend, releaseUid: String(value.releaseUid).toLowerCase(), environmentUid: String(value.environmentUid).toLowerCase(),
|
|
224
|
-
environmentName: text(value.environmentName), name: text(value.name), tools: normalizeTools(value.tools)
|
|
248
|
+
environmentName: text(value.environmentName), name: text(value.name), tools: normalizeTools(value.tools),
|
|
249
|
+
...(object(u) && typeof u.code === 'string' && typeof u.message === 'string' ? { unavailable: { at: text(u.at, 40), code: text(u.code, 60), message: text(u.message, 1000) } } : {}) };
|
|
225
250
|
}
|
|
226
251
|
/** Beside `mcp-connections.json`, shared by every host on the machine. */
|
|
227
252
|
const projectToolCatalogPath = (env = process.env) => path.join((0, stateDirectory_1.mainSequenceStateDirectory)(env), CATALOG_FILE);
|
|
@@ -250,10 +275,12 @@ function createProjectToolCatalog(options) {
|
|
|
250
275
|
const file = options.file ?? (0, exports.projectToolCatalogPath)(options.env), staleMs = options.staleMs ?? 15 * 60_000;
|
|
251
276
|
let catalog = readProjectToolCatalogFile(file), loadedAt = mtime(), running = null;
|
|
252
277
|
let current = null;
|
|
253
|
-
// The
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
278
|
+
// The backend this host's listing is for: given by the host, resolved by ready() the way auth-status
|
|
279
|
+
// resolves it, or learned from a refresh. The file is shared by every host and backend on the machine,
|
|
280
|
+
// so until it is known a listing shows nothing rather than another backend's tools.
|
|
281
|
+
let known = options.backend ? { backend: options.backend } : undefined;
|
|
282
|
+
let readying = null;
|
|
283
|
+
const state = { error: null };
|
|
257
284
|
function mtime() { try {
|
|
258
285
|
return fs.statSync(file).mtimeMs;
|
|
259
286
|
}
|
|
@@ -268,7 +295,9 @@ function createProjectToolCatalog(options) {
|
|
|
268
295
|
catalog = readProjectToolCatalogFile(file);
|
|
269
296
|
loadedAt = now;
|
|
270
297
|
}
|
|
271
|
-
const
|
|
298
|
+
const none = { definitions: [], bindings: new Map() };
|
|
299
|
+
const registration = (scope) => (scope ?? known) ? projectToolRegistration(catalog, scope ?? known) : none;
|
|
300
|
+
const selected = (scope) => (catalog?.connections ?? []).filter(connection => (scope ?? known) ? inScope(connection, scope ?? known) : false);
|
|
272
301
|
async function write(next) {
|
|
273
302
|
await fsp.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
274
303
|
const temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
@@ -330,16 +359,27 @@ function createProjectToolCatalog(options) {
|
|
|
330
359
|
}
|
|
331
360
|
return {
|
|
332
361
|
file,
|
|
333
|
-
/**
|
|
362
|
+
/** The backend this listing is for, resolved once without any network request (the process
|
|
363
|
+
* environment, then the shared preferences, then the default), as `auth-status` resolves it. A
|
|
364
|
+
* host calls this before its first tool list; a refresh or an explicit scope also sets it. */
|
|
365
|
+
ready() {
|
|
366
|
+
if (known)
|
|
367
|
+
return Promise.resolve();
|
|
368
|
+
readying ??= (0, repositories_1.resolveBackend)(undefined, undefined, options.env ?? process.env).then(backend => { known ??= { backend }; }, () => { });
|
|
369
|
+
return readying;
|
|
370
|
+
},
|
|
371
|
+
/** Definitions for the host's tool list, from the newest shared cache, for the known backend only. */
|
|
334
372
|
list(scope) { reload(); return registration(scope).definitions; },
|
|
335
373
|
binding(name) { reload(); return registration({}).bindings.get(name); },
|
|
336
374
|
state(scope) {
|
|
337
375
|
reload();
|
|
338
|
-
const
|
|
376
|
+
const connections = selected(scope), unavailable = connections.filter(connection => connection.unavailable);
|
|
339
377
|
return { count: registration(scope).definitions.length,
|
|
340
|
-
connections:
|
|
341
|
-
|
|
342
|
-
updatedAt: catalog?.updatedAt || null, refreshing: running !== null,
|
|
378
|
+
connections: connections.map(connection => ({ backend: connection.backend, releaseUid: connection.releaseUid, environmentUid: connection.environmentUid, environmentName: connection.environmentName,
|
|
379
|
+
name: connection.name, tools: connection.tools.length, ...(connection.unavailable ? { unavailable: connection.unavailable } : {}) })),
|
|
380
|
+
updatedAt: catalog?.updatedAt || null, refreshing: running !== null,
|
|
381
|
+
// Read from the file, so a host whose process did not run the refresh still sees what it could not read.
|
|
382
|
+
unavailable: unavailable.map(connection => connection.name || connection.releaseUid), retained: unavailable.reduce((sum, connection) => sum + connection.tools.length, 0), error: state.error };
|
|
343
383
|
},
|
|
344
384
|
/** The state plus definitions and bindings, as the `mcp-catalog` action returns them. */
|
|
345
385
|
view(scope) {
|
|
@@ -383,10 +423,8 @@ function createProjectToolCatalog(options) {
|
|
|
383
423
|
current.locked = true;
|
|
384
424
|
try {
|
|
385
425
|
reload();
|
|
386
|
-
const read = await readProjectToolCatalog(dispatch, { ...scope, backend: session.backend, previous: catalog, signal: controller.signal, session });
|
|
426
|
+
const read = await readProjectToolCatalog(dispatch, { ...scope, backend: session.backend, previous: catalog, signal: controller.signal, session, budgetMs: options.budgetMs });
|
|
387
427
|
await write(read.catalog);
|
|
388
|
-
state.unavailable = read.unavailable;
|
|
389
|
-
state.retained = read.retained;
|
|
390
428
|
state.error = null;
|
|
391
429
|
adopt(read.catalog, scope ?? { backend: read.backend });
|
|
392
430
|
return true;
|
|
@@ -400,7 +438,12 @@ function createProjectToolCatalog(options) {
|
|
|
400
438
|
state.error = { code: error instanceof models_1.ToolingError ? error.code : 'INTERNAL_ERROR', message: error instanceof models_1.ToolingError ? error.message : 'The saved project MCP tools could not be refreshed.' };
|
|
401
439
|
return false;
|
|
402
440
|
}
|
|
403
|
-
})().finally(() => {
|
|
441
|
+
})().finally(() => {
|
|
442
|
+
// A read the budget left behind must not keep a socket or the dispatcher alive.
|
|
443
|
+
controller.abort();
|
|
444
|
+
running = null;
|
|
445
|
+
current = null;
|
|
446
|
+
});
|
|
404
447
|
return running;
|
|
405
448
|
},
|
|
406
449
|
/** A host process is ending: stop its refresh and free the shared lock for the next one at once. */
|
|
@@ -425,7 +468,8 @@ function createProjectToolCatalog(options) {
|
|
|
425
468
|
void this.refresh(true);
|
|
426
469
|
return;
|
|
427
470
|
}
|
|
428
|
-
|
|
471
|
+
// A catalog just read is a successful read: the connection is available again.
|
|
472
|
+
const connections = catalog.connections.map((item, i) => i === index ? { ...item, tools: normalizeTools(tools), unavailable: undefined } : item);
|
|
429
473
|
const next = { version: 1, updatedAt: new Date().toISOString(), connections };
|
|
430
474
|
try {
|
|
431
475
|
await write(next);
|