@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.
Files changed (39) hide show
  1. package/.codex-plugin/plugin.json +6 -5
  2. package/.mcp.json +17 -0
  3. package/README.md +49 -11
  4. package/assets/mainsequence-mark.svg +453 -0
  5. package/build-info.json +3 -3
  6. package/dist/hosts/codexPresentation.js +1 -0
  7. package/dist/hosts/discovery.js +252 -0
  8. package/dist/hosts/messaging.js +63 -0
  9. package/dist/hosts/panel/panel.html +265 -0
  10. package/dist/hosts/workspace.js +107 -0
  11. package/dist/panel-server.js +223 -0
  12. package/dist/vendor/devtools/index.js +45 -1
  13. package/dist/vendor/devtools/package.json +1 -1
  14. package/dist/vendor/devtools/shared/accountScope.js +19 -0
  15. package/dist/vendor/devtools/shared/actions.js +44 -7
  16. package/dist/vendor/devtools/shared/agentAssets.js +38 -0
  17. package/dist/vendor/devtools/shared/agentDelivery.js +477 -0
  18. package/dist/vendor/devtools/shared/agentIcon.js +43 -0
  19. package/dist/vendor/devtools/shared/agents.js +18 -7
  20. package/dist/vendor/devtools/shared/arguments.js +16 -9
  21. package/dist/vendor/devtools/shared/deployments.js +214 -0
  22. package/dist/vendor/devtools/shared/environments.js +38 -0
  23. package/dist/vendor/devtools/shared/mcp.js +237 -46
  24. package/dist/vendor/devtools/shared/mcpTransport.js +79 -23
  25. package/dist/vendor/devtools/shared/organizationProjectWorkspace.js +8 -3
  26. package/dist/vendor/devtools/shared/organizationProjects.js +2 -13
  27. package/dist/vendor/devtools/shared/projectTools.js +437 -0
  28. package/dist/vendor/devtools/shared/repositories.js +2 -2
  29. package/dist/vendor/devtools/shared/session.js +133 -14
  30. package/dist/vendor/devtools/shared/sharedTransport.js +506 -0
  31. package/dist/vendor/devtools/shared/stateDirectory.js +44 -0
  32. package/dist/vendor/devtools/shared/workingDirectory.js +13 -0
  33. package/dist/vendor/devtools/shared/workspaceSettings.js +96 -0
  34. package/docs/agents-tools-panel.md +522 -0
  35. package/docs/codex-verification.md +37 -0
  36. package/docs/conversational-mcp.md +16 -3
  37. package/package.json +9 -5
  38. package/skills/mainsequence-mcp/SKILL.md +19 -4
  39. 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 timer = setTimeout(abort, this.options.requestTimeoutMs ?? timeoutMs ?? 30_000);
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', 'The backend could not be reached. The saved session has been kept.');
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 running = this.renewals.get(backend);
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 task = (async () => {
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.renewals.set(backend, task);
239
- void task.finally(() => this.renewals.delete(backend)).catch(() => { });
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 this.http(backend, method, apiPath, body, session.access, options?.timeoutMs);
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 this.http(backend, method, apiPath, body, retry.access, options?.timeoutMs);
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;