@dev-mainsequence/codex-plugin 0.3.0 → 0.7.9
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 +6 -5
- package/.mcp.json +17 -0
- package/README.md +49 -11
- package/assets/mainsequence-mark.svg +453 -0
- package/build-info.json +3 -3
- package/dist/hosts/codexPresentation.js +1 -0
- package/dist/hosts/discovery.js +252 -0
- package/dist/hosts/messaging.js +63 -0
- package/dist/hosts/panel/panel.html +265 -0
- package/dist/hosts/workspace.js +107 -0
- package/dist/panel-server.js +223 -0
- package/dist/vendor/devtools/index.js +45 -1
- package/dist/vendor/devtools/package.json +1 -1
- package/dist/vendor/devtools/shared/accountScope.js +19 -0
- package/dist/vendor/devtools/shared/actions.js +44 -7
- package/dist/vendor/devtools/shared/agentAssets.js +38 -0
- package/dist/vendor/devtools/shared/agentDelivery.js +477 -0
- package/dist/vendor/devtools/shared/agentIcon.js +43 -0
- package/dist/vendor/devtools/shared/agents.js +18 -7
- package/dist/vendor/devtools/shared/arguments.js +16 -9
- package/dist/vendor/devtools/shared/deployments.js +214 -0
- package/dist/vendor/devtools/shared/environments.js +38 -0
- package/dist/vendor/devtools/shared/mcp.js +237 -46
- package/dist/vendor/devtools/shared/mcpTransport.js +79 -23
- package/dist/vendor/devtools/shared/organizationProjectWorkspace.js +8 -3
- package/dist/vendor/devtools/shared/organizationProjects.js +2 -13
- package/dist/vendor/devtools/shared/projectTools.js +437 -0
- package/dist/vendor/devtools/shared/repositories.js +2 -2
- package/dist/vendor/devtools/shared/session.js +133 -14
- package/dist/vendor/devtools/shared/sharedTransport.js +506 -0
- package/dist/vendor/devtools/shared/stateDirectory.js +44 -0
- package/dist/vendor/devtools/shared/workingDirectory.js +13 -0
- package/dist/vendor/devtools/shared/workspaceSettings.js +96 -0
- package/docs/agents-tools-panel.md +522 -0
- package/docs/codex-verification.md +37 -0
- package/docs/conversational-mcp.md +16 -3
- package/package.json +9 -5
- package/skills/mainsequence-mcp/SKILL.md +19 -4
- package/skills/ms/assets/mainsequence-mark.svg +453 -0
|
@@ -44,6 +44,10 @@ const http = __importStar(require("http"));
|
|
|
44
44
|
const config_1 = require("./config.js");
|
|
45
45
|
const machineSession_1 = require("./machineSession.js");
|
|
46
46
|
const models_1 = require("./models.js");
|
|
47
|
+
const accountScope_1 = require("./accountScope.js");
|
|
48
|
+
class AccountChangedError extends models_1.ToolingError {
|
|
49
|
+
constructor() { super('AUTH_REJECTED', 'The MainSequence account or session changed. Refresh the current view before continuing.'); }
|
|
50
|
+
}
|
|
47
51
|
function requireBackend(input) {
|
|
48
52
|
const backend = (0, config_1.normalizeBackendUrl)(input);
|
|
49
53
|
let url;
|
|
@@ -101,6 +105,7 @@ class MachineSessionService {
|
|
|
101
105
|
env;
|
|
102
106
|
renewals = new Map();
|
|
103
107
|
secrets = new Set();
|
|
108
|
+
verifiedAccounts = new Map();
|
|
104
109
|
constructor(host, options = {}) {
|
|
105
110
|
this.host = host;
|
|
106
111
|
this.options = options;
|
|
@@ -172,6 +177,12 @@ class MachineSessionService {
|
|
|
172
177
|
throw new models_1.ToolingError('NOT_AUTHENTICATED', 'No session is saved for this backend. Run login.');
|
|
173
178
|
return { session, source: 'machine' };
|
|
174
179
|
}
|
|
180
|
+
async selectionIdentity(input) {
|
|
181
|
+
const backend = requireBackend(input), { session, source } = await this.selected(backend);
|
|
182
|
+
const expiresAt = expiry(session.access);
|
|
183
|
+
return { key: crypto.createHash('sha256').update(JSON.stringify([backend, source, session.username, session.access, session.refresh])).digest('hex'),
|
|
184
|
+
...(expiresAt ? { expiresAt: Date.parse(expiresAt) } : {}) };
|
|
185
|
+
}
|
|
175
186
|
async http(backend, method, apiPath, body, access, timeoutMs) {
|
|
176
187
|
this.options.signal?.throwIfAborted();
|
|
177
188
|
if ((apiPath !== '/mcp' && !/^\/(api|auth)\//.test(apiPath)) || apiPath.includes('..'))
|
|
@@ -179,19 +190,25 @@ class MachineSessionService {
|
|
|
179
190
|
const controller = new AbortController();
|
|
180
191
|
const abort = () => controller.abort();
|
|
181
192
|
this.options.signal?.addEventListener('abort', abort, { once: true });
|
|
182
|
-
const
|
|
193
|
+
const budget = this.options.requestTimeoutMs ?? timeoutMs ?? 30_000;
|
|
194
|
+
let timedOut = false;
|
|
195
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort(); }, budget);
|
|
183
196
|
try {
|
|
184
197
|
const response = await (this.options.fetch ?? fetch)(backend + apiPath, {
|
|
185
198
|
method, redirect: 'error', signal: controller.signal,
|
|
199
|
+
// Opt-in shared transports may deduplicate only these stateless platform
|
|
200
|
+
// reads. Never infer read safety from arbitrary project tool annotations.
|
|
201
|
+
...(apiPath === '/mcp' && body && typeof body === 'object' && (['initialize', 'notifications/initialized', 'tools/list'].includes(body.method)
|
|
202
|
+
|| body.method === 'tools/call' && ['agent.list', 'agent.search', 'agent.get', 'resource_release.mcp_endpoints'].includes(body.params?.name)) ? { cache: 'force-cache' } : {}),
|
|
186
203
|
headers: { 'Content-Type': 'application/json', ...(apiPath === '/mcp' ? { Accept: 'application/json, text/event-stream', 'MCP-Protocol-Version': '2025-11-25' } : {}), ...(access ? { Authorization: `Bearer ${access}` } : {}) },
|
|
187
204
|
body: body === undefined ? undefined : JSON.stringify(body)
|
|
188
205
|
});
|
|
189
206
|
if (response.status === 401 || response.status === 403)
|
|
190
207
|
throw new models_1.ToolingError('AUTH_REJECTED', 'The backend rejected the selected credentials.');
|
|
191
208
|
if (!response.ok)
|
|
192
|
-
throw new models_1.ToolingError('API_ERROR', `The backend returned HTTP ${response.status}.`);
|
|
209
|
+
throw new models_1.ToolingError('API_ERROR', `The backend ${backend} returned HTTP ${response.status} for ${method} ${apiPath}.`);
|
|
193
210
|
if (apiPath === '/mcp')
|
|
194
|
-
return await (0, mcpTransport_1.readMcpResponse)(response, body);
|
|
211
|
+
return await (0, mcpTransport_1.readMcpResponse)(response, body, 'platform MCP ' + backend);
|
|
195
212
|
if (response.status === 204)
|
|
196
213
|
return undefined;
|
|
197
214
|
try {
|
|
@@ -206,22 +223,33 @@ class MachineSessionService {
|
|
|
206
223
|
throw new models_1.ToolingError('CANCELLED', 'The action was cancelled.');
|
|
207
224
|
if (error instanceof models_1.ToolingError)
|
|
208
225
|
throw error;
|
|
209
|
-
throw new models_1.ToolingError('BACKEND_UNREACHABLE',
|
|
226
|
+
throw new models_1.ToolingError('BACKEND_UNREACHABLE', `The backend ${backend} could not be reached for ${method} ${apiPath}: `
|
|
227
|
+
+ (timedOut ? `the request timed out after ${(0, mcpTransport_1.describeSeconds)(budget)} s` : (0, mcpTransport_1.failureReason)(error) || 'the connection failed') + '. The saved session has been kept.');
|
|
210
228
|
}
|
|
211
229
|
finally {
|
|
212
230
|
clearTimeout(timer);
|
|
213
231
|
this.options.signal?.removeEventListener('abort', abort);
|
|
214
232
|
}
|
|
215
233
|
}
|
|
216
|
-
renew(backend) {
|
|
217
|
-
const
|
|
234
|
+
renew(backend, expectedAccount, stale) {
|
|
235
|
+
const renewalKey = JSON.stringify([backend, expectedAccount]);
|
|
236
|
+
const running = this.renewals.get(renewalKey);
|
|
218
237
|
if (running)
|
|
219
238
|
return running;
|
|
220
|
-
const
|
|
239
|
+
const run = async () => {
|
|
221
240
|
// Bypass cached state: another SDK process may have rotated the refresh token.
|
|
222
241
|
const previous = await this.saved(backend);
|
|
223
242
|
if (!previous?.refresh)
|
|
224
243
|
throw new models_1.ToolingError('NOT_AUTHENTICATED', 'No refreshable machine session is saved for this backend.');
|
|
244
|
+
if (expectedAccount !== undefined && previous.username && previous.username !== expectedAccount)
|
|
245
|
+
throw this.accountChanged();
|
|
246
|
+
// Another host process may have renewed while this one waited for the
|
|
247
|
+
// shared lease. Reuse its saved pair; never rotate it a second time.
|
|
248
|
+
if (stale && previous.access && previous.access !== stale.access) {
|
|
249
|
+
if (expectedAccount !== undefined)
|
|
250
|
+
await this.verifyAccount(backend, previous, expectedAccount);
|
|
251
|
+
return previous;
|
|
252
|
+
}
|
|
225
253
|
let data;
|
|
226
254
|
try {
|
|
227
255
|
data = await this.http(backend, 'POST', '/auth/jwt-token/token/refresh/', { refresh: previous.refresh });
|
|
@@ -232,11 +260,24 @@ class MachineSessionService {
|
|
|
232
260
|
throw error;
|
|
233
261
|
}
|
|
234
262
|
const session = this.remember(tokenPair(data, previous));
|
|
263
|
+
if (expectedAccount !== undefined) {
|
|
264
|
+
const current = await this.saved(backend);
|
|
265
|
+
// A shared transport can coalesce simultaneous rotations. Another client
|
|
266
|
+
// may already have saved this exact returned pair; that is the same login.
|
|
267
|
+
if (!current || (current.access !== previous.access || current.refresh !== previous.refresh)
|
|
268
|
+
&& (current.access !== session.access || current.refresh !== session.refresh))
|
|
269
|
+
throw this.accountChanged();
|
|
270
|
+
}
|
|
271
|
+
// Refresh may rotate the old token. Keep the new pair even if the subsequent
|
|
272
|
+
// profile check is unavailable; no domain request runs before that check passes.
|
|
235
273
|
await this.persist(backend, session);
|
|
274
|
+
if (expectedAccount !== undefined)
|
|
275
|
+
await this.verifyAccount(backend, session, expectedAccount);
|
|
236
276
|
return session;
|
|
237
|
-
}
|
|
238
|
-
this.
|
|
239
|
-
|
|
277
|
+
};
|
|
278
|
+
const task = this.options.sessionLock ? this.options.sessionLock(backend, run, this.options.signal) : run();
|
|
279
|
+
this.renewals.set(renewalKey, task);
|
|
280
|
+
void task.finally(() => this.renewals.delete(renewalKey)).catch(() => { });
|
|
240
281
|
return task;
|
|
241
282
|
}
|
|
242
283
|
async persist(backend, session) {
|
|
@@ -281,32 +322,110 @@ class MachineSessionService {
|
|
|
281
322
|
};
|
|
282
323
|
}
|
|
283
324
|
async request(input, method, apiPath, body, retrySafe = method === 'GET', options) {
|
|
325
|
+
return this.accountRequest(input, method, apiPath, body, retrySafe, options);
|
|
326
|
+
}
|
|
327
|
+
accountChanged() {
|
|
328
|
+
return new AccountChangedError();
|
|
329
|
+
}
|
|
330
|
+
recordAccount(backend, session, profile, expectedAccount) {
|
|
331
|
+
const username = profile?.user?.username || profile?.username;
|
|
332
|
+
if (username !== expectedAccount) {
|
|
333
|
+
this.verifiedAccounts.delete(JSON.stringify([backend, session.access]));
|
|
334
|
+
throw this.accountChanged();
|
|
335
|
+
}
|
|
336
|
+
if (this.verifiedAccounts.size >= 32)
|
|
337
|
+
this.verifiedAccounts.delete(this.verifiedAccounts.keys().next().value);
|
|
338
|
+
this.verifiedAccounts.set(JSON.stringify([backend, session.access]), username);
|
|
339
|
+
}
|
|
340
|
+
async verifyAccount(backend, session, expectedAccount) {
|
|
341
|
+
if (this.verifiedAccounts.get(JSON.stringify([backend, session.access])) === expectedAccount)
|
|
342
|
+
return;
|
|
343
|
+
this.recordAccount(backend, session, await this.http(backend, 'GET', '/api/v1/users/me/', undefined, session.access), expectedAccount);
|
|
344
|
+
}
|
|
345
|
+
/** The same selected credential is verified and used. A 401 retry must verify its new credential too. */
|
|
346
|
+
async withAccount(input, expectedAccount) {
|
|
347
|
+
const backend = requireBackend(input);
|
|
348
|
+
(0, accountScope_1.validateExpectedAccount)(expectedAccount);
|
|
349
|
+
const target = (value) => { if (requireBackend(value) !== backend)
|
|
350
|
+
throw new models_1.ToolingError('BACKEND_MISMATCH', 'The request belongs to another backend.'); };
|
|
351
|
+
const unsupported = async () => { throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'Account-bound operations cannot change authentication.'); };
|
|
352
|
+
const scoped = {
|
|
353
|
+
request: (value, method, apiPath, body, retrySafe = method === 'GET', options) => {
|
|
354
|
+
target(value);
|
|
355
|
+
return this.accountRequest(backend, method, apiPath, body, retrySafe, options, expectedAccount);
|
|
356
|
+
},
|
|
357
|
+
status: async (value, check = true) => {
|
|
358
|
+
target(value);
|
|
359
|
+
// During one account-bound operation, reuse the identity verified for this
|
|
360
|
+
// exact access token. Still reread shared credentials on every boundary;
|
|
361
|
+
// a changed token must be verified before it can access the remote service.
|
|
362
|
+
if (check)
|
|
363
|
+
await this.accountRequest(backend, 'GET', '/api/v1/users/me/', undefined, true, undefined, expectedAccount);
|
|
364
|
+
const current = await this.selected(backend);
|
|
365
|
+
await this.verifyAccount(backend, current.session, expectedAccount);
|
|
366
|
+
const final = await this.selected(backend);
|
|
367
|
+
if (final.source !== current.source || final.session.access !== current.session.access || final.session.refresh !== current.session.refresh)
|
|
368
|
+
throw this.accountChanged();
|
|
369
|
+
const source = current.source === 'environment' ? 'environment' : this.options.persistence?.mode ?? 'machine';
|
|
370
|
+
return { backend, authenticated: true, source, persistence: source,
|
|
371
|
+
store: source === 'environment' ? null : this.options.persistence?.name ?? this.store?.name ?? null,
|
|
372
|
+
username: expectedAccount, expiresAt: expiry(current.session.access), checked: true };
|
|
373
|
+
},
|
|
374
|
+
login: unsupported, logout: unsupported, refresh: unsupported,
|
|
375
|
+
sanitize: text => this.sanitize(text),
|
|
376
|
+
};
|
|
377
|
+
await scoped.status(backend, true);
|
|
378
|
+
return scoped;
|
|
379
|
+
}
|
|
380
|
+
async accountRequest(input, method, apiPath, body, retrySafe, options, expectedAccount) {
|
|
284
381
|
if (options?.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 300000))
|
|
285
382
|
throw new models_1.ToolingError('INVALID_REQUEST', 'Request timeout must be between 1 and 300000 milliseconds.');
|
|
286
383
|
const backend = requireBackend(input);
|
|
287
384
|
let { session, source } = await this.selected(backend);
|
|
288
385
|
if (!session.access || (expiry(session.access) && Date.parse(expiry(session.access)) <= Date.now() + 30_000)) {
|
|
289
386
|
if (source === 'machine')
|
|
290
|
-
session = await this.renew(backend);
|
|
387
|
+
session = await this.renew(backend, expectedAccount, session);
|
|
291
388
|
else if (!session.access)
|
|
292
389
|
throw new models_1.ToolingError('NOT_AUTHENTICATED', 'The explicit environment has no access token.');
|
|
293
390
|
}
|
|
391
|
+
const send = async (selected) => {
|
|
392
|
+
const checkSelected = async () => {
|
|
393
|
+
if (expectedAccount === undefined)
|
|
394
|
+
return;
|
|
395
|
+
const current = await this.selected(backend);
|
|
396
|
+
if (current.source !== source || current.session.access !== selected.access || current.session.refresh !== selected.refresh)
|
|
397
|
+
throw this.accountChanged();
|
|
398
|
+
};
|
|
399
|
+
if (expectedAccount !== undefined && apiPath !== '/api/v1/users/me/')
|
|
400
|
+
await this.verifyAccount(backend, selected, expectedAccount);
|
|
401
|
+
await checkSelected();
|
|
402
|
+
const value = await this.http(backend, method, apiPath, body, selected.access, options?.timeoutMs);
|
|
403
|
+
if (expectedAccount !== undefined && apiPath === '/api/v1/users/me/')
|
|
404
|
+
this.recordAccount(backend, selected, value, expectedAccount);
|
|
405
|
+
await checkSelected();
|
|
406
|
+
return value;
|
|
407
|
+
};
|
|
294
408
|
try {
|
|
295
|
-
return await
|
|
409
|
+
return await send(session);
|
|
296
410
|
}
|
|
297
411
|
catch (error) {
|
|
298
412
|
if (!(error instanceof models_1.ToolingError) || error.code !== 'AUTH_REJECTED')
|
|
299
413
|
throw error;
|
|
414
|
+
// Scope failures are not expired-token failures and must never trigger a replay.
|
|
415
|
+
if (error instanceof AccountChangedError)
|
|
416
|
+
throw error;
|
|
300
417
|
if (source === 'environment')
|
|
301
418
|
throw refusedCredentials('environment', backend, session.access);
|
|
302
419
|
if (!retrySafe)
|
|
303
420
|
throw error;
|
|
304
421
|
const latest = await this.saved(backend);
|
|
305
|
-
const retry = latest?.access && latest.access !== session.access ? latest : await this.renew(backend);
|
|
422
|
+
const retry = latest?.access && latest.access !== session.access ? latest : await this.renew(backend, expectedAccount, session);
|
|
306
423
|
try {
|
|
307
|
-
return await
|
|
424
|
+
return await send(retry);
|
|
308
425
|
}
|
|
309
426
|
catch (retryError) {
|
|
427
|
+
if (retryError instanceof AccountChangedError)
|
|
428
|
+
throw retryError;
|
|
310
429
|
if (retryError instanceof models_1.ToolingError && retryError.code === 'AUTH_REJECTED')
|
|
311
430
|
throw refusedCredentials('machine', backend, retry.refresh || retry.access);
|
|
312
431
|
throw retryError;
|