maka-agent 0.2.0-dev.47.20260922 → 0.2.0-dev.48.20260923

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 (92) hide show
  1. package/dist/runtime-host-capability-provider-command.js +25 -6
  2. package/dist/tui-mcp-control.js +37 -24
  3. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  4. package/node_modules/@maka/acp-executor-plugin/dist/acp-errors.js +26 -0
  5. package/node_modules/@maka/acp-executor-plugin/dist/acp-filesystem.js +103 -0
  6. package/node_modules/@maka/acp-executor-plugin/dist/acp-process.js +119 -0
  7. package/node_modules/@maka/acp-executor-plugin/dist/acp-projection.js +146 -0
  8. package/node_modules/@maka/acp-executor-plugin/dist/index.js +653 -0
  9. package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +24446 -0
  10. package/node_modules/@maka/acp-executor-plugin/package.json +15 -0
  11. package/node_modules/@maka/antigravity-acp-plugin/dist/index.js +88 -0
  12. package/node_modules/@maka/antigravity-acp-plugin/dist/model-presentation.js +53 -0
  13. package/node_modules/@maka/antigravity-acp-plugin/dist/plugin.mjs +115 -0
  14. package/node_modules/@maka/antigravity-acp-plugin/package.json +15 -0
  15. package/node_modules/@maka/core/dist/executor-catalog.js +106 -0
  16. package/node_modules/@maka/core/dist/mcp.js +4 -1
  17. package/node_modules/@maka/core/dist/model-metadata.js +28 -1
  18. package/node_modules/@maka/core/dist/runtime-event-store.js +25 -0
  19. package/node_modules/@maka/core/dist/runtime-invocation.js +5 -0
  20. package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +6 -0
  21. package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +21 -0
  22. package/node_modules/@maka/core/dist/session.js +2 -0
  23. package/node_modules/@maka/core/dist/settings.js +1 -0
  24. package/node_modules/@maka/core/dist/tool-ledger-scanner.js +365 -104
  25. package/node_modules/@maka/core/dist/turn-origin.js +14 -0
  26. package/node_modules/@maka/core/package.json +2 -0
  27. package/node_modules/@maka/mcp/dist/credential-coordinator.js +6 -1
  28. package/node_modules/@maka/mcp/dist/index.js +31 -5
  29. package/node_modules/@maka/mcp/dist/oauth.js +54 -6
  30. package/node_modules/@maka/runtime/dist/agent-run-inspect.js +1 -1
  31. package/node_modules/@maka/runtime/dist/conversation-copy.js +56 -1
  32. package/node_modules/@maka/runtime/dist/mcp-tools.js +9 -2
  33. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +116 -3
  34. package/node_modules/@maka/runtime/dist/plugin-executor-service.js +212 -16
  35. package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +2 -2
  36. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
  37. package/node_modules/@maka/runtime/dist/session-manager.js +170 -125
  38. package/node_modules/@maka/runtime/dist/shell-run-manager.js +3 -0
  39. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +29 -4
  40. package/node_modules/@maka/runtime-host/dist/candidate-entry.js +8 -5
  41. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  42. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +8 -1
  43. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -1
  44. package/node_modules/@maka/runtime-host/dist/composition-identity.js +19 -0
  45. package/node_modules/@maka/runtime-host/dist/protocol/index.js +10 -2
  46. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +34 -0
  47. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +40 -2
  48. package/node_modules/@maka/runtime-host/dist/server/builtin-external-agent-plugins.js +188 -0
  49. package/node_modules/@maka/runtime-host/dist/server/candidate.js +12 -7
  50. package/node_modules/@maka/runtime-host/dist/server/client-capability-recovery.js +44 -46
  51. package/node_modules/@maka/runtime-host/dist/server/context-coordinator.js +2 -0
  52. package/node_modules/@maka/runtime-host/dist/server/execution-composition-factory.js +1 -1
  53. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +159 -19
  54. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
  55. package/node_modules/@maka/runtime-host/dist/server/extension-bundle.js +4 -0
  56. package/node_modules/@maka/runtime-host/dist/server/host-composition-source.js +47 -0
  57. package/node_modules/@maka/runtime-host/dist/server/host-composition.js +1 -29
  58. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +2 -9
  59. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +75 -9
  60. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +4 -0
  61. package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +45 -0
  62. package/node_modules/@maka/runtime-host/dist/server/jev-routing-model.js +157 -0
  63. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +2 -2
  64. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +4 -0
  65. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +11 -1
  66. package/node_modules/@maka/runtime-host/dist/server/process-lifecycle.js +1 -1
  67. package/node_modules/@maka/runtime-host/dist/server/process-termination-error.js +27 -0
  68. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +91 -12
  69. package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +2 -0
  70. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +99 -12
  71. package/node_modules/@maka/runtime-host/dist/server/session-effect-coordinator.js +12 -6
  72. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +33 -0
  73. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +4 -0
  74. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +173 -0
  75. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +293 -0
  76. package/node_modules/@maka/runtime-host/package.json +2 -0
  77. package/node_modules/@maka/storage/dist/agent-run-store-contract.js +8 -0
  78. package/node_modules/@maka/storage/dist/execution-stores.js +15 -3
  79. package/node_modules/@maka/storage/dist/mcp-config-store.js +91 -2
  80. package/node_modules/@maka/storage/dist/operational-state-store.js +15 -1
  81. package/node_modules/@maka/storage/dist/plan-authority.js +2 -0
  82. package/node_modules/@maka/storage/dist/plan-store.js +30 -0
  83. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
  84. package/node_modules/@maka/storage/dist/runtime-policy/credential-vault-document.js +4 -1
  85. package/node_modules/@maka/storage/dist/runtime-policy/policy-document.js +2 -0
  86. package/node_modules/@maka/storage/dist/session-store-values.js +16 -4
  87. package/node_modules/@maka/storage/dist/shell-run-authority.js +1 -0
  88. package/node_modules/@maka/storage/dist/shell-run-store.js +15 -0
  89. package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +74 -1
  90. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +724 -84
  91. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +16 -0
  92. package/package.json +1 -1
@@ -6,6 +6,7 @@
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
8
  "exports": {
9
+ "./executor-catalog": "./dist/executor-catalog.js",
9
10
  "./executor-id": "./dist/executor-id.js",
10
11
  "./durable-tool-result-projection": "./dist/durable-tool-result-projection.js",
11
12
  "./model-projection-transition": "./dist/model-projection-transition.js",
@@ -29,6 +30,7 @@
29
30
  "./provider-retry-countdown": "./dist/provider-retry-countdown.js",
30
31
  "./interaction": "./dist/interaction.js",
31
32
  "./session": "./dist/session.js",
33
+ "./turn-origin": "./dist/turn-origin.js",
32
34
  "./session-reference": "./dist/session-reference.js",
33
35
  "./session-revisions": "./dist/session-revisions.js",
34
36
  "./collaboration": "./dist/collaboration.js",
@@ -44,7 +44,10 @@ export class McpCredentialCoordinator {
44
44
  *
45
45
  * The optional signal fences an ABANDONED erase: a logout whose round
46
46
  * timed out must not resume later, adopt whatever record a newer login
47
- * just stored as its basis, and tombstone the fresh tokens. */
47
+ * just stored as its basis, and tombstone the fresh tokens.
48
+ *
49
+ * `spare` must be decided here, on the lane's read: checked outside, a
50
+ * login could land between the check and the erase. */
48
51
  async erase(serverId, options = {}) {
49
52
  this.epochs.set(serverId, this.epoch(serverId) + 1);
50
53
  await this.run(serverId, async () => {
@@ -54,6 +57,8 @@ export class McpCredentialCoordinator {
54
57
  // storage read was in flight — committing past it would erase a
55
58
  // record the caller no longer owns.
56
59
  this.assertNotAbandoned(serverId, options.signal);
60
+ if (basis && options.spare?.(basis))
61
+ return;
57
62
  // Absence still gets a tombstone: a cross-process flow that captured
58
63
  // generation 0 on absence must not CAS its credentials back in after
59
64
  // this revocation.
@@ -17,7 +17,7 @@
17
17
  * under the License.
18
18
  */
19
19
  import { randomBytes } from 'node:crypto';
20
- import { auth, Client, extractWWWAuthenticateParams, LATEST_PROTOCOL_VERSION, CLIENT_CAPABILITIES_META_KEY, isInputRequiredResult, SdkErrorCode, SdkHttpError, SSEClientTransport, StreamableHTTPClientTransport, } from '@modelcontextprotocol/client';
20
+ import { auth, validateAuthorizationResponseIssuer, Client, extractWWWAuthenticateParams, LATEST_PROTOCOL_VERSION, CLIENT_CAPABILITIES_META_KEY, isInputRequiredResult, SdkErrorCode, SdkHttpError, SSEClientTransport, StreamableHTTPClientTransport, } from '@modelcontextprotocol/client';
21
21
  import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
22
22
  import { setTimeout as delay } from 'node:timers/promises';
23
23
  import { containsMcpFormState, prepareMcpForm } from './form-elicitation.js';
@@ -34,7 +34,7 @@ import { McpToolCallPreparer } from './tool-output-validation.js';
34
34
  import { mapMcpToolProgress } from './tool-progress.js';
35
35
  import { McpCredentialCoordinator } from './credential-coordinator.js';
36
36
  import { assertTransportSecurity, urlProvenance, } from './transport-security.js';
37
- import { McpAuthRequiredError, McpOAuthProvider, } from './oauth.js';
37
+ import { McpAuthRequiredError, McpOAuthProvider, authorizationCallbackError, mcpOAuthRecordBoundTo, } from './oauth.js';
38
38
  export { formatMcpDiagnosticText } from './diagnostic-text.js';
39
39
  export { McpToolCallError } from './tool-call-error.js';
40
40
  export { createCredentialMcpOAuthStorage, } from './credential-oauth-storage.js';
@@ -259,7 +259,7 @@ export class McpClientManager {
259
259
  : undefined;
260
260
  if (owed) {
261
261
  try {
262
- await this.forgetAuthorization(serverId, owed);
262
+ await this.forgetAuthorization(serverId, owed, { successor: serverConfig });
263
263
  }
264
264
  catch (error) {
265
265
  await this.blockForCredentialCleanup(serverId, current, owed, error);
@@ -1272,7 +1272,6 @@ export class McpClientManager {
1272
1272
  }
1273
1273
  }
1274
1274
  async finishAuthorizationRound(serverId, callback, options = {}) {
1275
- const authorizationCode = callback.code;
1276
1275
  const { config } = this.requireRemoteEntry(serverId);
1277
1276
  // The immediate read doubles as the flow's generation/version pin.
1278
1277
  const storage = this.flowStorage(serverId, options.signal);
@@ -1298,6 +1297,25 @@ export class McpClientManager {
1298
1297
  if (record?.pendingServerUrl !== config.url) {
1299
1298
  throw new Error(`MCP server "${serverId}" changed its URL during authorization`);
1300
1299
  }
1300
+ const metadata = record.discovery?.authorizationServerMetadata;
1301
+ const expectedIssuer = metadata?.issuer ?? record.discovery?.authorizationServerUrl;
1302
+ if (!expectedIssuer)
1303
+ throw new Error('OAuth callback has no recorded issuer');
1304
+ try {
1305
+ validateAuthorizationResponseIssuer({
1306
+ iss: callback.iss,
1307
+ expectedIssuer: String(expectedIssuer),
1308
+ issParameterSupported: metadata?.authorization_response_iss_parameter_supported === true,
1309
+ });
1310
+ }
1311
+ catch {
1312
+ // SDK errors echo the untrusted iss parameter; it must not cross IPC.
1313
+ throw new Error('OAuth callback issuer validation failed');
1314
+ }
1315
+ if ('error' in callback) {
1316
+ throw authorizationCallbackError(callback.error);
1317
+ }
1318
+ const authorizationCode = callback.code;
1301
1319
  const provider = new McpOAuthProvider({
1302
1320
  serverId,
1303
1321
  serverUrl: config.url,
@@ -1432,7 +1450,15 @@ export class McpClientManager {
1432
1450
  return;
1433
1451
  if (config && isMcpStdioConfig(config) && !(await this.coordinator.read(serverId)))
1434
1452
  return;
1435
- await this.coordinator.erase(serverId, options);
1453
+ const { successor, ...eraseOptions } = options;
1454
+ await this.coordinator.erase(serverId, {
1455
+ ...eraseOptions,
1456
+ // Another process may have applied the same change and signed in first.
1457
+ ...(successor &&
1458
+ !isMcpStdioConfig(successor) && {
1459
+ spare: (record) => mcpOAuthRecordBoundTo(record, successor.url, successor.oauth),
1460
+ }),
1461
+ });
1436
1462
  }
1437
1463
  /** Drops stored tokens and registration, returning the server to needs-auth. */
1438
1464
  async clearAuthorization(serverId, options = {}) {
@@ -16,6 +16,25 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
+ import { createHash } from 'node:crypto';
20
+ const OAUTH_ERROR_CODES = new Set([
21
+ 'invalid_request',
22
+ 'unauthorized_client',
23
+ 'access_denied',
24
+ 'unsupported_response_type',
25
+ 'invalid_scope',
26
+ 'server_error',
27
+ 'temporarily_unavailable',
28
+ 'invalid_client',
29
+ 'invalid_grant',
30
+ 'unsupported_grant_type',
31
+ 'interaction_required',
32
+ 'login_required',
33
+ 'consent_required',
34
+ ]);
35
+ export function authorizationCallbackError(code) {
36
+ return new Error(`Authorization failed: ${OAUTH_ERROR_CODES.has(code) ? code : 'unknown_error'}`);
37
+ }
19
38
  export function createMemoryMcpOAuthStorage() {
20
39
  const records = new Map();
21
40
  return {
@@ -31,6 +50,16 @@ export function createMemoryMcpOAuthStorage() {
31
50
  },
32
51
  };
33
52
  }
53
+ function mcpOAuthClientConfigHash(config) {
54
+ if (!config?.clientId)
55
+ return undefined;
56
+ return createHash('sha256')
57
+ .update(JSON.stringify([config.issuer, config.clientId, config.clientSecret]))
58
+ .digest('hex');
59
+ }
60
+ export function mcpOAuthRecordBoundTo(record, serverUrl, config) {
61
+ return (record.serverUrl === serverUrl && record.clientConfigHash === mcpOAuthClientConfigHash(config));
62
+ }
34
63
  /** Thrown (via the SDK's UnauthorizedError path) when a background connect
35
64
  * would need the user in a browser. The manager maps it to `needs-auth`. */
36
65
  export class McpAuthRequiredError extends Error {
@@ -64,6 +93,9 @@ export class McpOAuthProvider {
64
93
  if (state)
65
94
  this.state = () => state;
66
95
  }
96
+ get clientConfigHash() {
97
+ return mcpOAuthClientConfigHash(this.options.config);
98
+ }
67
99
  get redirectUrl() {
68
100
  return this.options.interactive?.redirectUrl ?? BACKGROUND_REDIRECT_URL;
69
101
  }
@@ -82,10 +114,17 @@ export class McpOAuthProvider {
82
114
  : {}),
83
115
  };
84
116
  }
85
- async clientInformation() {
117
+ async clientInformation(context) {
86
118
  const configured = this.options.config;
87
119
  if (configured?.clientId) {
120
+ if (!configured.issuer) {
121
+ throw new Error(`MCP server "${this.options.serverId}" requires oauth.issuer for its pre-registered client`);
122
+ }
123
+ if (context && context.issuer !== configured.issuer) {
124
+ throw new Error(`MCP server "${this.options.serverId}" discovered a different OAuth issuer; reconfigure its client credentials`);
125
+ }
88
126
  return {
127
+ issuer: configured.issuer,
89
128
  client_id: configured.clientId,
90
129
  ...(configured.clientSecret ? { client_secret: configured.clientSecret } : {}),
91
130
  };
@@ -106,7 +145,13 @@ export class McpOAuthProvider {
106
145
  });
107
146
  }
108
147
  async tokens() {
109
- return (await this.read()).tokens;
148
+ if (this.options.config?.clientId)
149
+ await this.clientInformation();
150
+ const record = await this.read();
151
+ const tokens = record.tokens;
152
+ if (this.options.config?.issuer && tokens?.issuer !== this.options.config.issuer)
153
+ return undefined;
154
+ return tokens;
110
155
  }
111
156
  async saveTokens(tokens) {
112
157
  await this.mutate((record) => {
@@ -148,9 +193,7 @@ export class McpOAuthProvider {
148
193
  // A dynamically registered client belongs to the authorization server
149
194
  // that issued it. When discovery moves the resource to a DIFFERENT
150
195
  // authorization server, carrying the old registration over would send
151
- // one AS's client credentials (and any secret) to another. Static
152
- // config-supplied clients are unaffected — they never live in the
153
- // record.
196
+ // one AS's client credentials (and any secret) to another.
154
197
  const previousIssuer = record.discovery?.authorizationServerUrl;
155
198
  const nextIssuer = state.authorizationServerUrl;
156
199
  if (previousIssuer && nextIssuer && `${previousIssuer}` !== `${nextIssuer}`) {
@@ -217,7 +260,8 @@ export class McpOAuthProvider {
217
260
  record.pendingRedirectUrl ||
218
261
  record.pendingServerUrl ||
219
262
  record.pendingState;
220
- return Boolean(boundable) && record.serverUrl !== this.options.serverUrl;
263
+ return (Boolean(boundable) &&
264
+ !mcpOAuthRecordBoundTo(record, this.options.serverUrl, this.options.config));
221
265
  }
222
266
  /** The same fail-closed binding rule as read(), applied to a mutation
223
267
  * basis: a record bound elsewhere contributes NOTHING to the new write —
@@ -239,6 +283,10 @@ export class McpOAuthProvider {
239
283
  const stamp = (record) => {
240
284
  apply(record);
241
285
  record.serverUrl = this.options.serverUrl;
286
+ if (this.clientConfigHash)
287
+ record.clientConfigHash = this.clientConfigHash;
288
+ else
289
+ delete record.clientConfigHash;
242
290
  return record;
243
291
  };
244
292
  // The coordinator-provided storage view makes read-apply-write one
@@ -38,7 +38,7 @@ export async function inspectAgentRunReadModel(runStore, runtimeEventStore, opti
38
38
  diagnostics.push(inspectDiagnostic(invocation, 'runtime_terminal_missing', 'runtime ledger has no complete terminal RuntimeEvent fact'));
39
39
  }
40
40
  }
41
- const projection = runtimeEvents.length > 0
41
+ const projection = runtimeEvents.length > 0 && options.includeProjection !== false
42
42
  ? projectRuntimeEventsToStoredMessages(runtimeEvents, { invocations: [invocation] })
43
43
  : undefined;
44
44
  if (projection) {
@@ -23,6 +23,7 @@ import { decodePersistedToolResultContent } from '@maka/core/tool-result-record-
23
23
  import { isEmittedAgentRunEventType } from '@maka/core/agent-run';
24
24
  import { decodeModelCallAttempt, MODEL_CALL_ATTEMPT_EVENT_TYPE, } from '@maka/core/model-call-attempt';
25
25
  import { TOOL_RECOVERY_DECISION_FACT_KIND } from '@maka/core/tool-recovery-fact';
26
+ import { canonicalToolArgsHash } from '@maka/core/tool-args-identity';
26
27
  import { buildHistoryCompactCheckpoint, historyCompactSourceDigest, matchHistoryCompactCheckpointPrefix, validateHistoryCompactCheckpointShape, } from './history-compact-checkpoint.js';
27
28
  import { findCheckpointSummaryDefect } from './history-compact-summary-validation.js';
28
29
  import { isHistoryCompactContentEvent } from './history-compaction.js';
@@ -281,6 +282,20 @@ function assertConversationRuntimeLedgerCopySupported(run, runtimeEvents) {
281
282
  error.code = 'branch_runtime_fact_rewrite_unsupported';
282
283
  throw error;
283
284
  }
285
+ /**
286
+ * Mirrors the T1 scanner's identity semantics: a call whose args are not
287
+ * strict JSON cannot authenticate, which the scanner surfaces as a
288
+ * `canonical_args_hash_conflict` rather than an exception — so the copy pass
289
+ * must see `undefined` there, not a thrown error.
290
+ */
291
+ const tryCanonicalToolArgsHash = (toolName, args) => {
292
+ try {
293
+ return canonicalToolArgsHash(toolName, args);
294
+ }
295
+ catch {
296
+ return undefined;
297
+ }
298
+ };
284
299
  export async function cloneConversationRuntimeLedger(input) {
285
300
  if (input.plan.sourceSessionId !== input.referenceMap.sourceSessionId) {
286
301
  throw new Error('Conversation copy plan does not belong to the source Session');
@@ -386,10 +401,22 @@ export async function cloneConversationRuntimeLedger(input) {
386
401
  };
387
402
  for (const sourceId of clonedEventBySourceId.keys())
388
403
  finishTarget(sourceId);
404
+ // The identity rewrite changes a copied call's canonical args, so the paired
405
+ // dispatch's recorded hash stops authenticating them and the copied ledger
406
+ // fails its own T1 re-scan. Collect the source and rewritten identities per
407
+ // (invocation, tool call) and re-authenticate the dispatches after the
408
+ // rewrite pass — but only when the source dispatch actually authenticated
409
+ // the source call. Stamping the rewritten identity over a mismatched hash
410
+ // would launder an already-corrupt source ledger into a valid-looking copy
411
+ // instead of preserving the corruption for the re-scan to reject (#5466
412
+ // review). Identity derivation mirrors the scanner's semantics: args that
413
+ // are not strict JSON cannot authenticate at all.
414
+ const rewrittenArgsIdentities = new Map();
389
415
  for (const event of clonedEventBySourceId.values()) {
390
416
  if (event.content?.kind === 'function_call' && event.content.name === 'Read') {
391
417
  const args = event.content.args;
392
- if (args && typeof args === 'object' && 'path' in args && typeof args.path === 'string')
418
+ if (args && typeof args === 'object' && 'path' in args && typeof args.path === 'string') {
419
+ const sourceArgsHash = tryCanonicalToolArgsHash(event.content.name, args);
393
420
  event.content = {
394
421
  ...event.content,
395
422
  args: {
@@ -397,19 +424,47 @@ export async function cloneConversationRuntimeLedger(input) {
397
424
  ...rewriteReadInput(args, references, clonedEventBySourceId),
398
425
  },
399
426
  };
427
+ rewrittenArgsIdentities.set(`${event.invocationId}\u0000${event.content.id}`, {
428
+ sourceArgsHash,
429
+ rewrittenArgsHash: tryCanonicalToolArgsHash(event.content.name, event.content.args),
430
+ });
431
+ }
400
432
  }
401
433
  if (event.content?.kind === 'text')
402
434
  event.content.text = rewriteCopiedText(event.content.text, references, clonedEventBySourceId);
403
435
  if (event.content?.kind === 'function_call' && event.content.name === 'ArchiveRead') {
404
436
  const args = event.content.args;
405
437
  if (args && typeof args === 'object' && 'ref' in args && typeof args.ref === 'string') {
438
+ const sourceArgsHash = tryCanonicalToolArgsHash(event.content.name, args);
406
439
  event.content = {
407
440
  ...event.content,
408
441
  args: { ...args, ref: rewriteLedgerArchiveText(args.ref, references) },
409
442
  };
443
+ rewrittenArgsIdentities.set(`${event.invocationId}\u0000${event.content.id}`, {
444
+ sourceArgsHash,
445
+ rewrittenArgsHash: tryCanonicalToolArgsHash(event.content.name, event.content.args),
446
+ });
410
447
  }
411
448
  }
412
449
  }
450
+ if (rewrittenArgsIdentities.size > 0) {
451
+ for (const event of clonedEventBySourceId.values()) {
452
+ const dispatch = event.actions?.toolDispatch;
453
+ if (dispatch?.canonicalArgsHash === undefined)
454
+ continue;
455
+ const identity = rewrittenArgsIdentities.get(`${event.invocationId}\u0000${dispatch.providerToolCallId}`);
456
+ if (identity === undefined)
457
+ continue;
458
+ if (identity.sourceArgsHash !== dispatch.canonicalArgsHash)
459
+ continue;
460
+ if (identity.rewrittenArgsHash === undefined)
461
+ continue;
462
+ event.actions = {
463
+ ...event.actions,
464
+ toolDispatch: { ...dispatch, canonicalArgsHash: identity.rewrittenArgsHash },
465
+ };
466
+ }
467
+ }
413
468
  const preparedPlans = flattenedPlans.map((plan) => {
414
469
  const runId = runIds.get(plan.run.runId);
415
470
  const clonedOperationalEvents = plan.operationalEvents.flatMap((event) => {
@@ -136,13 +136,20 @@ function mcpToolDescription(descriptor) {
136
136
  }
137
137
  export function mcpProxyToolName(serverId, toolName) {
138
138
  const raw = `mcp__${sanitizeNamePart(serverId)}__${sanitizeNamePart(toolName)}`;
139
- if (raw.length <= MAX_PROVIDER_TOOL_NAME)
139
+ // Preserve existing simple names; hash identities whose spelling or separators are ambiguous.
140
+ if (raw.length <= MAX_PROVIDER_TOOL_NAME &&
141
+ sanitizeNamePart(serverId) === serverId &&
142
+ sanitizeNamePart(toolName) === toolName &&
143
+ !serverId.includes('__') &&
144
+ !toolName.includes('__'))
140
145
  return raw;
141
146
  const hash = createHash('sha256')
142
147
  .update(`${serverId}\0${toolName}`)
143
148
  .digest('hex')
144
149
  .slice(0, HASH_CHARS);
145
- return `${raw.slice(0, MAX_PROVIDER_TOOL_NAME - HASH_CHARS - 2)}__${hash}`;
150
+ // A truncated name must not become another identity's unmodified name.
151
+ const hashed = `mcp_h__${sanitizeNamePart(serverId)}__${sanitizeNamePart(toolName)}`;
152
+ return `${hashed.slice(0, MAX_PROVIDER_TOOL_NAME - HASH_CHARS - 2)}__${hash}`;
146
153
  }
147
154
  function sanitizeNamePart(value) {
148
155
  const sanitized = value
@@ -17,12 +17,14 @@
17
17
  * under the License.
18
18
  */
19
19
  import { randomUUID } from 'node:crypto';
20
+ import { redactSecrets } from '@maka/core/redaction';
20
21
  import { AsyncEventQueue } from './async-queue.js';
21
22
  /** Converts a small plugin executor contract into Maka's existing Run event stream. */
22
23
  export class PluginExecutorBackend {
23
24
  kind = 'plugin-executor';
24
25
  sessionId;
25
26
  #cwd;
27
+ #configuration;
26
28
  #instructions;
27
29
  #model;
28
30
  #thinkingLevel;
@@ -34,6 +36,7 @@ export class PluginExecutorBackend {
34
36
  constructor(input) {
35
37
  this.sessionId = input.sessionId;
36
38
  this.#cwd = input.cwd;
39
+ this.#configuration = input.configuration;
37
40
  this.#instructions = input.instructions;
38
41
  this.#model = input.model;
39
42
  this.#thinkingLevel = input.thinkingLevel;
@@ -86,6 +89,7 @@ export class PluginExecutorBackend {
86
89
  const turnId = input.turnId;
87
90
  let thinkingText = '';
88
91
  const toolUseIds = new Map();
92
+ const toolOutputSequences = new Map();
89
93
  let result;
90
94
  let failure;
91
95
  let failed = false;
@@ -99,6 +103,7 @@ export class PluginExecutorBackend {
99
103
  cwd: this.#cwd,
100
104
  ...(this.#model ? { model: this.#model } : {}),
101
105
  reasoningEffort: this.#thinkingLevel ?? null,
106
+ ...(this.#configuration ? { configuration: this.#configuration } : {}),
102
107
  ...(this.#instructions ? { instructions: this.#instructions } : {}),
103
108
  ...(input.attachments ? { attachments: input.attachments } : {}),
104
109
  ...(input.directoryReferences ? { directoryReferences: input.directoryReferences } : {}),
@@ -108,8 +113,9 @@ export class PluginExecutorBackend {
108
113
  onEvent: (event) => {
109
114
  if (event.type === 'thinking_delta')
110
115
  thinkingText += event.text;
111
- this.#publishOutputEvent(turnId, messageId, event, toolUseIds, queue);
116
+ this.#publishOutputEvent(turnId, messageId, event, toolUseIds, toolOutputSequences, queue);
112
117
  },
118
+ onPermissionRequest: (request) => this.#requestPermission(input, request, signal, toolUseIds, queue),
113
119
  });
114
120
  }
115
121
  catch (error) {
@@ -130,6 +136,81 @@ export class PluginExecutorBackend {
130
136
  }
131
137
  this.#publishResult(turnId, messageId, result, queue);
132
138
  }
139
+ async #requestPermission(input, request, signal, toolUseIds, queue) {
140
+ const hosted = input.hostedInteraction;
141
+ if (!hosted || signal.aborted)
142
+ return { outcome: 'cancelled' };
143
+ const requestId = this.#newId();
144
+ const event = {
145
+ type: 'form_request',
146
+ id: this.#newId(),
147
+ turnId: input.turnId,
148
+ ts: this.#now(),
149
+ requestId,
150
+ toolUseId: toolUseIds.get(request.toolCallId) ?? request.toolCallId,
151
+ message: request.title,
152
+ requester: {
153
+ name: this.#binding.identity.displayName,
154
+ source: this.#binding.identity.extensionId,
155
+ },
156
+ fields: [
157
+ {
158
+ kind: 'single_select',
159
+ name: 'optionId',
160
+ label: request.kind === 'question' ? 'Question' : 'Permission',
161
+ required: true,
162
+ options: request.options.map((option) => ({
163
+ value: option.optionId,
164
+ label: option.name,
165
+ })),
166
+ },
167
+ ],
168
+ };
169
+ let settle;
170
+ const answer = new Promise((resolve) => {
171
+ settle = resolve;
172
+ });
173
+ let settled = false;
174
+ const finish = (result) => {
175
+ if (settled)
176
+ return;
177
+ settled = true;
178
+ settle(result);
179
+ };
180
+ const settlement = {
181
+ applyAnswer: async (result) => {
182
+ if (result.action !== 'accept')
183
+ return finish({ outcome: 'cancelled' });
184
+ const selected = result.values.optionId;
185
+ if (typeof selected !== 'string' ||
186
+ !request.options.some((option) => option.optionId === selected)) {
187
+ return finish({ outcome: 'cancelled' });
188
+ }
189
+ finish({ outcome: 'selected', optionId: selected });
190
+ },
191
+ applyClosure: async () => finish({ outcome: 'cancelled' }),
192
+ };
193
+ let admission;
194
+ const onAbort = () => {
195
+ finish({ outcome: 'cancelled' });
196
+ void Promise.resolve().then(async () => {
197
+ await admission?.catch(() => undefined);
198
+ await hosted.withdrawFormRequest(requestId).catch(() => undefined);
199
+ });
200
+ };
201
+ signal.addEventListener('abort', onAbort, { once: true });
202
+ try {
203
+ admission = hosted.admitFormRequest({ request: event, settlement });
204
+ await admission;
205
+ if (signal.aborted)
206
+ return { outcome: 'cancelled' };
207
+ queue.push(event);
208
+ return await answer;
209
+ }
210
+ finally {
211
+ signal.removeEventListener('abort', onAbort);
212
+ }
213
+ }
133
214
  #closeOptionalOutput(turnId, messageId, thinkingText, toolUseIds, queue) {
134
215
  if (thinkingText) {
135
216
  queue.push({
@@ -194,9 +275,10 @@ export class PluginExecutorBackend {
194
275
  turnId,
195
276
  ts: this.#now(),
196
277
  stopReason: 'user_stop',
278
+ ...(result.providerStopReason ? { providerStopReason: result.providerStopReason } : {}),
197
279
  });
198
280
  }
199
- #publishOutputEvent(turnId, messageId, event, toolUseIds, queue) {
281
+ #publishOutputEvent(turnId, messageId, event, toolUseIds, toolOutputSequences, queue) {
200
282
  if (event.type === 'output_delta') {
201
283
  if (!event.text)
202
284
  return;
@@ -239,6 +321,7 @@ export class PluginExecutorBackend {
239
321
  }
240
322
  const toolUseId = this.#newId();
241
323
  toolUseIds.set(event.toolCallId, toolUseId);
324
+ toolOutputSequences.set(event.toolCallId, 0);
242
325
  queue.push({
243
326
  type: 'tool_start',
244
327
  id: this.#newId(),
@@ -257,6 +340,31 @@ export class PluginExecutorBackend {
257
340
  const toolUseId = toolUseIds.get(event.toolCallId);
258
341
  if (!toolUseId)
259
342
  return;
343
+ if (event.type === 'tool_output_delta') {
344
+ if (!event.text)
345
+ return;
346
+ const chunk = redactSecrets(event.text);
347
+ if (!chunk)
348
+ return;
349
+ const now = this.#now();
350
+ const seq = (toolOutputSequences.get(event.toolCallId) ?? 0) + 1;
351
+ toolOutputSequences.set(event.toolCallId, seq);
352
+ queue.push({
353
+ type: 'tool_output_delta',
354
+ id: this.#newId(),
355
+ sessionId: this.sessionId,
356
+ turnId,
357
+ ts: now,
358
+ toolCallId: toolUseId,
359
+ toolUseId,
360
+ seq,
361
+ stream: event.stream ?? 'stdout',
362
+ chunk,
363
+ redacted: chunk !== event.text,
364
+ createdAt: now,
365
+ });
366
+ return;
367
+ }
260
368
  if (event.type === 'tool_progress') {
261
369
  if (!event.text)
262
370
  return;
@@ -271,6 +379,7 @@ export class PluginExecutorBackend {
271
379
  return;
272
380
  }
273
381
  toolUseIds.delete(event.toolCallId);
382
+ toolOutputSequences.delete(event.toolCallId);
274
383
  queue.push({
275
384
  type: 'tool_result',
276
385
  id: this.#newId(),
@@ -279,7 +388,9 @@ export class PluginExecutorBackend {
279
388
  toolUseId,
280
389
  providerExecuted: true,
281
390
  isError: event.isError ?? false,
282
- content: { kind: 'text', text: event.text },
391
+ content: event.content.kind === 'text'
392
+ ? event.content
393
+ : { kind: 'file_diff', paths: [...event.content.paths], diff: event.content.diff },
283
394
  });
284
395
  }
285
396
  #publishFailure(turnId, message, code, recoverable, queue) {
@@ -311,6 +422,8 @@ function cancellationEventReason(result) {
311
422
  return 'redirect';
312
423
  if (result.reason === 'timeout')
313
424
  return 'timeout';
425
+ if (result.reason === 'crash')
426
+ return 'crash';
314
427
  if (result.source === 'executor_retired')
315
428
  return 'crash';
316
429
  return 'user_stop';