@mcoda/codali 0.1.87 → 0.1.89

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 (71) hide show
  1. package/dist/cli/EvalCommand.d.ts +8 -0
  2. package/dist/cli/EvalCommand.d.ts.map +1 -1
  3. package/dist/cli/EvalCommand.js +93 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +1 -0
  6. package/dist/docdex/DocdexClient.d.ts +8 -1
  7. package/dist/docdex/DocdexClient.d.ts.map +1 -1
  8. package/dist/docdex/DocdexClient.js +126 -33
  9. package/dist/eval/CodaliGatewayLiveHarness.d.ts +169 -0
  10. package/dist/eval/CodaliGatewayLiveHarness.d.ts.map +1 -0
  11. package/dist/eval/CodaliGatewayLiveHarness.js +824 -0
  12. package/dist/eval/GatewayEvalSuite.d.ts +202 -0
  13. package/dist/eval/GatewayEvalSuite.d.ts.map +1 -0
  14. package/dist/eval/GatewayEvalSuite.js +673 -0
  15. package/dist/gateway/AgentTierResolver.d.ts +74 -0
  16. package/dist/gateway/AgentTierResolver.d.ts.map +1 -0
  17. package/dist/gateway/AgentTierResolver.js +576 -0
  18. package/dist/gateway/AppToolGatewayDispatcher.d.ts +88 -0
  19. package/dist/gateway/AppToolGatewayDispatcher.d.ts.map +1 -0
  20. package/dist/gateway/AppToolGatewayDispatcher.js +381 -0
  21. package/dist/gateway/CodaliGateway.d.ts +73 -0
  22. package/dist/gateway/CodaliGateway.d.ts.map +1 -0
  23. package/dist/gateway/CodaliGateway.js +824 -0
  24. package/dist/gateway/CodaliGatewaySchemas.d.ts +21 -0
  25. package/dist/gateway/CodaliGatewaySchemas.d.ts.map +1 -0
  26. package/dist/gateway/CodaliGatewaySchemas.js +874 -0
  27. package/dist/gateway/CodaliGatewayStore.d.ts +157 -0
  28. package/dist/gateway/CodaliGatewayStore.d.ts.map +1 -0
  29. package/dist/gateway/CodaliGatewayStore.js +206 -0
  30. package/dist/gateway/CodaliGatewayTypes.d.ts +336 -0
  31. package/dist/gateway/CodaliGatewayTypes.d.ts.map +1 -0
  32. package/dist/gateway/CodaliGatewayTypes.js +1 -0
  33. package/dist/gateway/ContextPackBuilder.d.ts +43 -0
  34. package/dist/gateway/ContextPackBuilder.d.ts.map +1 -0
  35. package/dist/gateway/ContextPackBuilder.js +317 -0
  36. package/dist/gateway/EvidenceNormalizer.d.ts +42 -0
  37. package/dist/gateway/EvidenceNormalizer.d.ts.map +1 -0
  38. package/dist/gateway/EvidenceNormalizer.js +488 -0
  39. package/dist/gateway/GatewayPlanner.d.ts +195 -0
  40. package/dist/gateway/GatewayPlanner.d.ts.map +1 -0
  41. package/dist/gateway/GatewayPlanner.js +379 -0
  42. package/dist/gateway/GatewayPolicyCompiler.d.ts +30 -0
  43. package/dist/gateway/GatewayPolicyCompiler.d.ts.map +1 -0
  44. package/dist/gateway/GatewayPolicyCompiler.js +114 -0
  45. package/dist/gateway/GatewaySecurityPolicy.d.ts +14 -0
  46. package/dist/gateway/GatewaySecurityPolicy.d.ts.map +1 -0
  47. package/dist/gateway/GatewaySecurityPolicy.js +350 -0
  48. package/dist/gateway/GatewayStateMachine.d.ts +165 -0
  49. package/dist/gateway/GatewayStateMachine.d.ts.map +1 -0
  50. package/dist/gateway/GatewayStateMachine.js +790 -0
  51. package/dist/gateway/GatewayTraceReplay.d.ts +120 -0
  52. package/dist/gateway/GatewayTraceReplay.d.ts.map +1 -0
  53. package/dist/gateway/GatewayTraceReplay.js +273 -0
  54. package/dist/gateway/ToolCapabilityCompiler.d.ts +50 -0
  55. package/dist/gateway/ToolCapabilityCompiler.d.ts.map +1 -0
  56. package/dist/gateway/ToolCapabilityCompiler.js +442 -0
  57. package/dist/index.d.ts +33 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +16 -0
  60. package/dist/runtime/CodaliJobRuntime.d.ts +211 -0
  61. package/dist/runtime/CodaliJobRuntime.d.ts.map +1 -0
  62. package/dist/runtime/CodaliJobRuntime.js +590 -0
  63. package/dist/runtime/CodaliRuntime.d.ts +81 -1
  64. package/dist/runtime/CodaliRuntime.d.ts.map +1 -1
  65. package/dist/runtime/CodaliRuntime.js +619 -4
  66. package/dist/tools/ToolRegistry.d.ts.map +1 -1
  67. package/dist/tools/ToolRegistry.js +4 -0
  68. package/dist/tools/ToolTypes.d.ts +1 -1
  69. package/dist/tools/ToolTypes.d.ts.map +1 -1
  70. package/dist/tools/ToolTypes.js +5 -1
  71. package/package.json +3 -3
@@ -6,12 +6,14 @@ import { OllamaRemoteProvider } from "../providers/OllamaRemoteProvider.js";
6
6
  import { CodexCliProvider } from "../providers/CodexCliProvider.js";
7
7
  import { MswarmWorkerProvider } from "../providers/MswarmWorkerProvider.js";
8
8
  import { DocdexClient, normalizeDocdexRuntimeOperation, } from "../docdex/DocdexClient.js";
9
+ import { AppToolGatewayDispatchError, dispatchAppToolGateway, } from "../gateway/AppToolGatewayDispatcher.js";
9
10
  import { createDiffTool } from "../tools/diff/DiffTool.js";
10
11
  import { createDocdexTools } from "../tools/docdex/DocdexTools.js";
11
12
  import { createFileTools } from "../tools/filesystem/FileTools.js";
12
13
  import { createSearchTool } from "../tools/search/SearchTool.js";
13
14
  import { createShellTool } from "../tools/shell/ShellTool.js";
14
15
  import { ToolRegistry } from "../tools/ToolRegistry.js";
16
+ import { ToolExecutionError, } from "../tools/ToolTypes.js";
15
17
  import { formatInstructionBlocks, loadInstructionBlocks } from "../session/InstructionLoader.js";
16
18
  import { SessionStore } from "../session/SessionStore.js";
17
19
  import { SubagentOrchestrator, } from "../subagents/SubagentOrchestrator.js";
@@ -30,6 +32,33 @@ const WEB_TOOL_NAMES = new Set(["docdex_web_research"]);
30
32
  const MEMORY_WRITE_TOOL_NAMES = new Set(["docdex_memory_save"]);
31
33
  const PROFILE_WRITE_TOOL_NAMES = new Set(["docdex_save_preference"]);
32
34
  const INDEX_REBUILD_TOOL_NAMES = new Set(["docdex_index_rebuild", "docdex_index_ingest"]);
35
+ const READ_ONLY_DYNAMIC_BACKING_TOOLS = new Set([
36
+ "docdex_search",
37
+ "docdex_batch_search",
38
+ "docdex_open",
39
+ "docdex_files",
40
+ "docdex_tree",
41
+ "docdex_stats",
42
+ ]);
43
+ const FORBIDDEN_DYNAMIC_ARG_KEYS = new Set([
44
+ "authorization",
45
+ "apiKey",
46
+ "api_key",
47
+ "baseUrl",
48
+ "base_url",
49
+ "credential",
50
+ "credentials",
51
+ "credentialSource",
52
+ "credential_source",
53
+ "repoId",
54
+ "repo_id",
55
+ "repoRoot",
56
+ "repo_root",
57
+ "tenantId",
58
+ "tenant_id",
59
+ "workspaceRoot",
60
+ "workspace_root",
61
+ ]);
33
62
  const DOCDEX_TOOL_OPERATIONS = new Map([
34
63
  ["docdex_health", ["health"]],
35
64
  ["docdex_initialize", ["initialize"]],
@@ -103,6 +132,424 @@ const stripUndefined = (input) => {
103
132
  const isRecord = (value) => {
104
133
  return typeof value === "object" && value !== null && !Array.isArray(value);
105
134
  };
135
+ const createDynamicToolRegistryState = () => ({
136
+ consideredTools: [],
137
+ registeredDynamicTools: [],
138
+ skippedDynamicTools: [],
139
+ dynamicToolCalls: [],
140
+ });
141
+ const pushUnique = (target, value) => {
142
+ if (!target.includes(value))
143
+ target.push(value);
144
+ };
145
+ const recordSkippedDynamicTool = (state, name, reason) => {
146
+ if (!state.skippedDynamicTools.some((entry) => entry.name === name && entry.reason === reason)) {
147
+ state.skippedDynamicTools.push({ name, reason });
148
+ }
149
+ };
150
+ const stringArrayFromUnknown = (value) => {
151
+ if (Array.isArray(value)) {
152
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
153
+ }
154
+ if (typeof value === "string" && value.trim()) {
155
+ return [value.trim()];
156
+ }
157
+ return [];
158
+ };
159
+ const toolNameFromRecord = (value) => {
160
+ for (const key of ["name", "tool", "toolName", "tool_name", "id"]) {
161
+ const entry = value[key];
162
+ if (typeof entry === "string" && entry.trim())
163
+ return entry.trim();
164
+ }
165
+ return undefined;
166
+ };
167
+ const toolNamesFromManifestEntries = (entries) => {
168
+ if (!Array.isArray(entries))
169
+ return [];
170
+ const names = [];
171
+ for (const entry of entries) {
172
+ if (typeof entry === "string" && entry.trim()) {
173
+ pushUnique(names, entry.trim());
174
+ continue;
175
+ }
176
+ if (isRecord(entry)) {
177
+ const name = toolNameFromRecord(entry);
178
+ if (name)
179
+ pushUnique(names, name);
180
+ }
181
+ }
182
+ return names;
183
+ };
184
+ const toolNamesFromManifest = (manifest) => {
185
+ if (!manifest)
186
+ return [];
187
+ const names = [];
188
+ for (const list of [
189
+ manifest.actualTools,
190
+ manifest.virtualTools,
191
+ manifest.actual_tools,
192
+ manifest.virtual_tools,
193
+ ]) {
194
+ for (const name of toolNamesFromManifestEntries(list))
195
+ pushUnique(names, name);
196
+ }
197
+ return names;
198
+ };
199
+ const normalizeRuntimeToolContractEntries = (contracts) => {
200
+ if (!contracts)
201
+ return [];
202
+ if (Array.isArray(contracts)) {
203
+ const entries = [];
204
+ for (const contract of contracts) {
205
+ if (!isRecord(contract))
206
+ continue;
207
+ const name = toolNameFromRecord(contract);
208
+ if (name)
209
+ entries.push([name, contract]);
210
+ }
211
+ return entries;
212
+ }
213
+ if (!isRecord(contracts))
214
+ return [];
215
+ const entries = [];
216
+ for (const [key, value] of Object.entries(contracts)) {
217
+ if (!key.trim())
218
+ continue;
219
+ const contract = isRecord(value)
220
+ ? { name: key, ...value }
221
+ : { name: key, metadata: { valueType: typeof value } };
222
+ entries.push([key, contract]);
223
+ }
224
+ return entries;
225
+ };
226
+ const contractString = (contract, camelKey, snakeKey) => {
227
+ const value = contract[camelKey] ?? contract[snakeKey];
228
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
229
+ };
230
+ const contractBoolean = (contract, camelKey, snakeKey) => {
231
+ const value = contract[camelKey] ?? contract[snakeKey];
232
+ return typeof value === "boolean" ? value : undefined;
233
+ };
234
+ const contractStringArray = (contract, camelKey, snakeKey) => {
235
+ return stringArrayFromUnknown(contract[camelKey] ?? contract[snakeKey]);
236
+ };
237
+ const contractCallSchema = (contract) => {
238
+ const schema = contract.callSchema ?? contract.call_schema;
239
+ if (isRecord(schema) && (schema.type === undefined || schema.type === "object")) {
240
+ return {
241
+ ...schema,
242
+ type: "object",
243
+ };
244
+ }
245
+ return { type: "object", additionalProperties: true };
246
+ };
247
+ const findForbiddenDynamicArgKeys = (value, path = "$", matches = []) => {
248
+ if (Array.isArray(value)) {
249
+ value.forEach((entry, index) => findForbiddenDynamicArgKeys(entry, `${path}[${index}]`, matches));
250
+ return matches;
251
+ }
252
+ if (!isRecord(value))
253
+ return matches;
254
+ for (const [key, entry] of Object.entries(value)) {
255
+ const childPath = `${path}.${key}`;
256
+ if (FORBIDDEN_DYNAMIC_ARG_KEYS.has(key)) {
257
+ matches.push(childPath);
258
+ }
259
+ findForbiddenDynamicArgKeys(entry, childPath, matches);
260
+ }
261
+ return matches;
262
+ };
263
+ const assertDynamicArgsStayInScope = (toolName, args) => {
264
+ const forbidden = findForbiddenDynamicArgKeys(args);
265
+ if (forbidden.length) {
266
+ throw new ToolExecutionError("tool_permission_denied", "Dynamic tool arguments cannot override tenant or repo scope", {
267
+ retryable: false,
268
+ details: { tool: toolName, forbidden },
269
+ });
270
+ }
271
+ };
272
+ const readStringArg = (args, keys) => {
273
+ for (const key of keys) {
274
+ const value = args[key];
275
+ if (typeof value === "string" && value.trim())
276
+ return value.trim();
277
+ }
278
+ return undefined;
279
+ };
280
+ const readStringArrayArg = (args, keys) => {
281
+ for (const key of keys) {
282
+ const values = stringArrayFromUnknown(args[key]);
283
+ if (values.length)
284
+ return values;
285
+ }
286
+ return [];
287
+ };
288
+ const readNumberArg = (args, keys) => {
289
+ for (const key of keys) {
290
+ const value = args[key];
291
+ if (typeof value === "number" && Number.isFinite(value))
292
+ return value;
293
+ }
294
+ return undefined;
295
+ };
296
+ const readBooleanArg = (args, keys) => {
297
+ for (const key of keys) {
298
+ const value = args[key];
299
+ if (typeof value === "boolean")
300
+ return value;
301
+ }
302
+ return undefined;
303
+ };
304
+ const selectDocdexBackingCall = (toolName, backingTools, args) => {
305
+ const record = isRecord(args) ? args : {};
306
+ const limit = readNumberArg(record, ["limit", "maxResults", "max_results"]);
307
+ const queries = readStringArrayArg(record, ["queries"]);
308
+ if (queries.length && backingTools.includes("docdex_batch_search")) {
309
+ return {
310
+ toolName: "docdex_batch_search",
311
+ args: stripUndefined({
312
+ queries,
313
+ limit,
314
+ includeLibs: readBooleanArg(record, ["includeLibs", "include_libs"]),
315
+ }),
316
+ };
317
+ }
318
+ const query = readStringArg(record, ["query", "q", "search", "question", "prompt", "text"]);
319
+ if (query && backingTools.includes("docdex_search")) {
320
+ return { toolName: "docdex_search", args: stripUndefined({ query, limit }) };
321
+ }
322
+ const path = readStringArg(record, ["path", "relPath", "rel_path", "file", "sourcePath", "source_path"]);
323
+ const docId = readStringArg(record, ["docId", "doc_id"]);
324
+ if ((path || docId) && backingTools.includes("docdex_open")) {
325
+ return {
326
+ toolName: "docdex_open",
327
+ args: stripUndefined({
328
+ path,
329
+ docId,
330
+ window: readNumberArg(record, ["window"]),
331
+ textOnly: readBooleanArg(record, ["textOnly", "text_only"]),
332
+ startLine: readNumberArg(record, ["startLine", "start_line"]),
333
+ endLine: readNumberArg(record, ["endLine", "end_line"]),
334
+ head: readNumberArg(record, ["head"]),
335
+ clamp: readBooleanArg(record, ["clamp"]),
336
+ }),
337
+ };
338
+ }
339
+ if (backingTools.includes("docdex_tree")) {
340
+ return {
341
+ toolName: "docdex_tree",
342
+ args: stripUndefined({
343
+ path,
344
+ maxDepth: readNumberArg(record, ["maxDepth", "max_depth"]),
345
+ dirsOnly: readBooleanArg(record, ["dirsOnly", "dirs_only"]),
346
+ includeHidden: readBooleanArg(record, ["includeHidden", "include_hidden"]),
347
+ }),
348
+ };
349
+ }
350
+ if (backingTools.includes("docdex_files")) {
351
+ return {
352
+ toolName: "docdex_files",
353
+ args: stripUndefined({
354
+ limit,
355
+ offset: readNumberArg(record, ["offset"]),
356
+ }),
357
+ };
358
+ }
359
+ if (backingTools.includes("docdex_stats")) {
360
+ return { toolName: "docdex_stats", args: {} };
361
+ }
362
+ throw new ToolExecutionError("tool_invalid_args", "Dynamic tool arguments do not match its read-only backing tools", {
363
+ retryable: false,
364
+ details: { tool: toolName, backingTools },
365
+ });
366
+ };
367
+ const createDocdexBackedDynamicTool = (options) => {
368
+ const { name, contract, registry, backingTools, state } = options;
369
+ const executionMode = contractString(contract, "executionMode", "execution_mode") ?? "server_supplied_snapshot_plus_docdex";
370
+ return {
371
+ name,
372
+ description: contract.description ??
373
+ `Read-only runtime tool. Uses ${backingTools.join(", ")} and returns ${contract.resultContract ?? contract.result_contract ?? "contracted app context"}.`,
374
+ inputSchema: contractCallSchema(contract),
375
+ handler: async (args, context) => {
376
+ const startedAt = Date.now();
377
+ let backingTool;
378
+ let recorded = false;
379
+ try {
380
+ assertDynamicArgsStayInScope(name, args);
381
+ const backingCall = selectDocdexBackingCall(name, backingTools, args);
382
+ backingTool = backingCall.toolName;
383
+ const result = await registry.execute(backingCall.toolName, backingCall.args, context);
384
+ if (!result.ok) {
385
+ state.dynamicToolCalls.push({
386
+ name,
387
+ backingTool,
388
+ status: "failed",
389
+ latencyMs: Date.now() - startedAt,
390
+ errorCode: result.error?.code,
391
+ errorMessage: result.error?.message,
392
+ });
393
+ recorded = true;
394
+ throw new ToolExecutionError(result.error?.code ?? "tool_execution_failed", result.error?.message ?? "Backing tool failed", {
395
+ retryable: result.error?.retryable,
396
+ details: { tool: name, backingTool, backingError: result.error?.details },
397
+ });
398
+ }
399
+ const data = {
400
+ tool: name,
401
+ executionMode,
402
+ resultContract: contract.resultContract ?? contract.result_contract,
403
+ resultSources: contractStringArray(contract, "resultSources", "result_sources"),
404
+ sourcePaths: contractStringArray(contract, "sourcePaths", "source_paths"),
405
+ sourceTypes: contractStringArray(contract, "sourceTypes", "source_types"),
406
+ suppliedSnapshots: contractStringArray(contract, "suppliedSnapshots", "supplied_snapshots"),
407
+ backingTool,
408
+ result: payloadForToolResult(result),
409
+ };
410
+ state.dynamicToolCalls.push({
411
+ name,
412
+ backingTool,
413
+ status: "success",
414
+ latencyMs: Date.now() - startedAt,
415
+ });
416
+ recorded = true;
417
+ return {
418
+ output: JSON.stringify(data, null, 2),
419
+ data,
420
+ };
421
+ }
422
+ catch (error) {
423
+ if (!recorded) {
424
+ state.dynamicToolCalls.push({
425
+ name,
426
+ backingTool,
427
+ status: "failed",
428
+ latencyMs: Date.now() - startedAt,
429
+ errorCode: error instanceof ToolExecutionError ? error.code : "tool_execution_failed",
430
+ errorMessage: error instanceof Error ? error.message : String(error),
431
+ });
432
+ }
433
+ throw error;
434
+ }
435
+ },
436
+ };
437
+ };
438
+ const resolveGatewayContract = (contract, policyGateway) => {
439
+ const gateway = contract.gateway;
440
+ if (!gateway && !policyGateway)
441
+ return undefined;
442
+ return { ...(policyGateway ?? {}), ...(gateway ?? {}) };
443
+ };
444
+ const gatewayString = (gateway, keys) => {
445
+ if (!gateway)
446
+ return undefined;
447
+ for (const key of keys) {
448
+ const value = gateway[key];
449
+ if (typeof value === "string" && value.trim()) {
450
+ return value.trim();
451
+ }
452
+ }
453
+ return undefined;
454
+ };
455
+ const gatewayBoolean = (gateway, keys) => {
456
+ if (!gateway)
457
+ return undefined;
458
+ for (const key of keys) {
459
+ const value = gateway[key];
460
+ if (typeof value === "boolean") {
461
+ return value;
462
+ }
463
+ }
464
+ return undefined;
465
+ };
466
+ const gatewayEndpoint = (gateway) => gatewayString(gateway, ["endpoint"]);
467
+ const gatewaySigningSecret = (gateway) => gatewayString(gateway, [
468
+ "signatureSecret",
469
+ "signature_secret",
470
+ "signingSecret",
471
+ "signing_secret",
472
+ "secret",
473
+ "signature",
474
+ ]);
475
+ const appToolGatewayTenantScope = (input) => stripUndefined({
476
+ tenant_id: input.metadata?.tenantId,
477
+ docdex_repo_id: input.docdex?.repoId,
478
+ });
479
+ const appToolGatewayRequesterScope = (input) => stripUndefined({
480
+ request_id: input.metadata?.requestId,
481
+ owner_user_id: input.metadata?.ownerUserId,
482
+ api_key_id: input.metadata?.apiKeyId,
483
+ agent_slug: input.metadata?.agentSlug,
484
+ });
485
+ const gatewayDispatchToolErrorCode = (error) => {
486
+ if (error.code === "GATEWAY_INVALID_ARGS")
487
+ return "tool_invalid_args";
488
+ if (error.code === "GATEWAY_HTTP_FAILED" ||
489
+ error.code === "GATEWAY_RESPONSE_MALFORMED") {
490
+ return "tool_execution_failed";
491
+ }
492
+ return "tool_permission_denied";
493
+ };
494
+ const createGatewayDynamicTool = (options) => {
495
+ const { name, contract, gateway, input, state } = options;
496
+ return {
497
+ name,
498
+ description: contract.description ??
499
+ `Read-only app tool dispatched through the runtime app_tool_gateway for ${name}.`,
500
+ inputSchema: contractCallSchema(contract),
501
+ handler: async (args, context) => {
502
+ const startedAt = Date.now();
503
+ try {
504
+ assertDynamicArgsStayInScope(name, args);
505
+ const dispatched = await dispatchAppToolGateway({
506
+ runId: context.runId ?? input.metadata?.requestId ?? input.metadata?.jobId ?? "codali-runtime",
507
+ sessionId: input.session?.id,
508
+ requestId: input.metadata?.requestId,
509
+ tenantScope: appToolGatewayTenantScope(input),
510
+ requesterScope: appToolGatewayRequesterScope(input),
511
+ toolName: name,
512
+ args,
513
+ contract,
514
+ gateway,
515
+ allowedTools: input.policy.allowedTools,
516
+ deniedTools: input.policy.deniedTools,
517
+ });
518
+ state.dynamicToolCalls.push({
519
+ name,
520
+ backingTool: "app_tool_gateway",
521
+ status: "success",
522
+ latencyMs: Date.now() - startedAt,
523
+ });
524
+ return {
525
+ output: JSON.stringify(dispatched.evidencePayload, null, 2),
526
+ data: dispatched.evidencePayload,
527
+ };
528
+ }
529
+ catch (error) {
530
+ const toolError = error instanceof AppToolGatewayDispatchError
531
+ ? new ToolExecutionError(gatewayDispatchToolErrorCode(error), error.message, {
532
+ retryable: error.retryable,
533
+ details: stripUndefined({
534
+ tool: name,
535
+ gatewayErrorCode: error.code,
536
+ gatewayDetails: error.details,
537
+ }),
538
+ })
539
+ : error;
540
+ state.dynamicToolCalls.push({
541
+ name,
542
+ backingTool: "app_tool_gateway",
543
+ status: "failed",
544
+ latencyMs: Date.now() - startedAt,
545
+ errorCode: toolError instanceof ToolExecutionError ? toolError.code : "tool_execution_failed",
546
+ errorMessage: toolError instanceof Error ? toolError.message : String(toolError),
547
+ });
548
+ throw toolError;
549
+ }
550
+ },
551
+ };
552
+ };
106
553
  const parseJsonPayload = (output) => {
107
554
  const trimmed = output.trim();
108
555
  if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("[")))
@@ -619,13 +1066,137 @@ const isDocdexToolAllowed = (toolName, docdex) => {
619
1066
  }
620
1067
  return true;
621
1068
  };
1069
+ const isImmutableDocdexRuntimeContext = (docdex) => docdex?.immutableRuntimeContext === true ||
1070
+ docdex?.credentialSource === "attached_mswarm_api_key";
622
1071
  const registerRuntimeTool = (registry, tool, policy, docdex) => {
623
1072
  if (isRuntimeToolAllowed(tool.name, policy) && isDocdexToolAllowed(tool.name, docdex)) {
624
1073
  registry.register(tool);
625
1074
  }
626
1075
  };
627
- const buildRuntimeToolRegistry = (input) => {
1076
+ const collectRuntimeToolContractCandidates = (input) => {
1077
+ const contracts = new Map();
1078
+ for (const [name, contract] of normalizeRuntimeToolContractEntries(input.policy.okacamToolContracts)) {
1079
+ contracts.set(name, contract);
1080
+ }
1081
+ for (const [name, contract] of normalizeRuntimeToolContractEntries(input.policy.appToolContracts)) {
1082
+ contracts.set(name, contract);
1083
+ }
1084
+ const names = new Set();
1085
+ for (const name of toolNamesFromManifest(input.docdex?.toolManifest))
1086
+ names.add(name);
1087
+ for (const name of stringArrayFromUnknown(input.policy.okacamVirtualTools))
1088
+ names.add(name);
1089
+ for (const name of stringArrayFromUnknown(input.policy.appVirtualTools))
1090
+ names.add(name);
1091
+ for (const name of contracts.keys())
1092
+ names.add(name);
1093
+ return Array.from(names)
1094
+ .sort()
1095
+ .map((name) => ({ name, contract: contracts.get(name) }));
1096
+ };
1097
+ const registerRuntimeContractTools = (registry, input, state, warnings) => {
1098
+ const candidates = collectRuntimeToolContractCandidates(input);
1099
+ if (!candidates.length)
1100
+ return;
1101
+ const registryNames = new Set(registry.list().map((tool) => tool.name));
1102
+ for (const candidate of candidates) {
1103
+ const name = candidate.name.trim();
1104
+ if (!name)
1105
+ continue;
1106
+ pushUnique(state.consideredTools, name);
1107
+ if (!candidate.contract) {
1108
+ if (!registryNames.has(name))
1109
+ recordSkippedDynamicTool(state, name, "missing_contract");
1110
+ continue;
1111
+ }
1112
+ if (candidate.contract.enabled === false) {
1113
+ recordSkippedDynamicTool(state, name, "contract_disabled");
1114
+ continue;
1115
+ }
1116
+ if (!isRuntimeToolAllowed(name, input.policy) || !isDocdexToolAllowed(name, input.docdex)) {
1117
+ recordSkippedDynamicTool(state, name, "policy_disallowed");
1118
+ continue;
1119
+ }
1120
+ if (registryNames.has(name)) {
1121
+ recordSkippedDynamicTool(state, name, "already_registered");
1122
+ continue;
1123
+ }
1124
+ const readOnly = contractBoolean(candidate.contract, "readOnly", "read_only");
1125
+ if (readOnly === false) {
1126
+ recordSkippedDynamicTool(state, name, "not_read_only");
1127
+ continue;
1128
+ }
1129
+ const executionMode = contractString(candidate.contract, "executionMode", "execution_mode") ??
1130
+ "server_supplied_snapshot_plus_docdex";
1131
+ const gateway = resolveGatewayContract(candidate.contract, input.policy.appToolGateway);
1132
+ const resolvedGatewayEndpoint = gatewayEndpoint(gateway);
1133
+ if (executionMode === "app_tool_gateway" || resolvedGatewayEndpoint) {
1134
+ if (!gateway) {
1135
+ recordSkippedDynamicTool(state, name, "gateway_not_configured");
1136
+ continue;
1137
+ }
1138
+ if (readOnly !== true) {
1139
+ recordSkippedDynamicTool(state, name, "not_read_only");
1140
+ continue;
1141
+ }
1142
+ if (gatewayBoolean(gateway, ["readOnly", "read_only"]) !== true) {
1143
+ recordSkippedDynamicTool(state, name, "gateway_not_read_only");
1144
+ continue;
1145
+ }
1146
+ if (!resolvedGatewayEndpoint) {
1147
+ recordSkippedDynamicTool(state, name, "gateway_endpoint_missing");
1148
+ continue;
1149
+ }
1150
+ if (!gatewaySigningSecret(gateway)) {
1151
+ recordSkippedDynamicTool(state, name, "gateway_signature_missing");
1152
+ continue;
1153
+ }
1154
+ registerRuntimeTool(registry, createGatewayDynamicTool({
1155
+ name,
1156
+ contract: candidate.contract,
1157
+ gateway: { ...gateway, endpoint: resolvedGatewayEndpoint },
1158
+ input,
1159
+ state,
1160
+ }), input.policy, input.docdex);
1161
+ registryNames.add(name);
1162
+ pushUnique(state.registeredDynamicTools, name);
1163
+ continue;
1164
+ }
1165
+ const declaredBackingTools = contractStringArray(candidate.contract, "backingTools", "backing_tools");
1166
+ if (!declaredBackingTools.length) {
1167
+ recordSkippedDynamicTool(state, name, "missing_backing_tools");
1168
+ continue;
1169
+ }
1170
+ const unsafeBackingTool = declaredBackingTools.find((toolName) => !READ_ONLY_DYNAMIC_BACKING_TOOLS.has(toolName));
1171
+ if (unsafeBackingTool) {
1172
+ recordSkippedDynamicTool(state, name, `unsafe_backing_tool:${unsafeBackingTool}`);
1173
+ continue;
1174
+ }
1175
+ const availableBackingTools = declaredBackingTools.filter((toolName) => registryNames.has(toolName));
1176
+ if (!availableBackingTools.length) {
1177
+ recordSkippedDynamicTool(state, name, "backing_tool_unavailable");
1178
+ continue;
1179
+ }
1180
+ registerRuntimeTool(registry, createDocdexBackedDynamicTool({
1181
+ name,
1182
+ contract: candidate.contract,
1183
+ registry,
1184
+ backingTools: availableBackingTools,
1185
+ state,
1186
+ }), input.policy, input.docdex);
1187
+ registryNames.add(name);
1188
+ pushUnique(state.registeredDynamicTools, name);
1189
+ }
1190
+ const warningSkips = state.skippedDynamicTools.filter((entry) => entry.reason !== "already_registered");
1191
+ if (warningSkips.length) {
1192
+ warnings.push(`Runtime tool contracts skipped: ${warningSkips
1193
+ .map((entry) => `${entry.name}:${entry.reason}`)
1194
+ .join(", ")}`);
1195
+ }
1196
+ };
1197
+ const buildRuntimeToolRegistry = (input, dynamicToolState, warnings) => {
628
1198
  if (input.toolRegistry) {
1199
+ registerRuntimeContractTools(input.toolRegistry, input, dynamicToolState, warnings);
629
1200
  return input.toolRegistry;
630
1201
  }
631
1202
  const registry = new ToolRegistry();
@@ -634,6 +1205,7 @@ const buildRuntimeToolRegistry = (input) => {
634
1205
  if (explicitTools) {
635
1206
  for (const tool of explicitTools)
636
1207
  register(tool);
1208
+ registerRuntimeContractTools(registry, input, dynamicToolState, warnings);
637
1209
  return registry;
638
1210
  }
639
1211
  for (const tool of createFileTools())
@@ -643,19 +1215,26 @@ const buildRuntimeToolRegistry = (input) => {
643
1215
  if (input.policy.allowShell) {
644
1216
  register(createShellTool());
645
1217
  }
1218
+ const immutableDocdexContext = isImmutableDocdexRuntimeContext(input.docdex);
646
1219
  const docdexClient = new DocdexClient({
647
- baseUrl: input.docdex?.baseUrl ?? DEFAULT_DOCDEX_BASE_URL,
1220
+ baseUrl: immutableDocdexContext
1221
+ ? input.docdex?.baseUrl ?? ""
1222
+ : input.docdex?.baseUrl ?? DEFAULT_DOCDEX_BASE_URL,
648
1223
  repoId: input.docdex?.repoId,
649
- repoRoot: input.docdex?.repoRoot ?? input.workspace.root,
1224
+ repoRoot: immutableDocdexContext
1225
+ ? undefined
1226
+ : input.docdex?.repoRoot ?? input.workspace.root,
650
1227
  dagSessionId: input.docdex?.dagSessionId ?? input.metadata?.requestId,
651
1228
  apiKey: input.docdex?.apiKey,
652
1229
  credentialSource: input.docdex?.credentialSource,
653
1230
  required: input.docdex?.required,
654
1231
  allowedOperations: input.docdex?.allowedOperations,
655
1232
  capabilities: input.docdex?.capabilities,
1233
+ immutableRuntimeContext: immutableDocdexContext,
656
1234
  });
657
1235
  for (const tool of createDocdexTools(docdexClient))
658
1236
  register(tool);
1237
+ registerRuntimeContractTools(registry, input, dynamicToolState, warnings);
659
1238
  return registry;
660
1239
  };
661
1240
  const buildResponseFormat = (response) => {
@@ -930,6 +1509,25 @@ const emitRuntimeEvent = (event, sink, warnings) => {
930
1509
  warnings.push(error instanceof Error ? error.message : String(error));
931
1510
  }
932
1511
  };
1512
+ const buildRuntimeTelemetry = (options) => {
1513
+ const calledTools = [];
1514
+ for (const event of options.events) {
1515
+ if (event.type === "tool_call")
1516
+ pushUnique(calledTools, event.name);
1517
+ }
1518
+ return {
1519
+ runId: options.runId,
1520
+ runtime: "codali",
1521
+ mode: options.input.policy.mode,
1522
+ toolCallCount: options.toolCallsExecuted,
1523
+ calledTools,
1524
+ consideredTools: [...options.dynamicToolState.consideredTools],
1525
+ registeredDynamicTools: [...options.dynamicToolState.registeredDynamicTools],
1526
+ skippedDynamicTools: [...options.dynamicToolState.skippedDynamicTools],
1527
+ dynamicToolCalls: [...options.dynamicToolState.dynamicToolCalls],
1528
+ warnings: [...options.warnings],
1529
+ };
1530
+ };
933
1531
  const createSubagentRunner = (options) => {
934
1532
  const { input, registry, toolContext, runId, remainingToolBudget } = options;
935
1533
  return async ({ spec }) => {
@@ -1322,6 +1920,7 @@ export const runCodaliTask = async (input) => {
1322
1920
  const touchedFiles = new Set();
1323
1921
  const warnings = [];
1324
1922
  const events = [];
1923
+ const dynamicToolState = createDynamicToolRegistryState();
1325
1924
  let eventSequence = 0;
1326
1925
  const session = await initializeRuntimeSession(input, runId, warnings);
1327
1926
  const emit = (event) => {
@@ -1329,7 +1928,7 @@ export const runCodaliTask = async (input) => {
1329
1928
  emitRuntimeEvent(event, input.onEvent, warnings);
1330
1929
  };
1331
1930
  const provider = input.providerInstance ?? createRuntimeProvider(input.provider);
1332
- const registry = buildRuntimeToolRegistry(input);
1931
+ const registry = buildRuntimeToolRegistry(input, dynamicToolState, warnings);
1333
1932
  const originalRecordTouchedFile = input.toolContext?.recordTouchedFile;
1334
1933
  const toolContext = {
1335
1934
  ...input.toolContext,
@@ -1385,6 +1984,14 @@ export const runCodaliTask = async (input) => {
1385
1984
  warnings,
1386
1985
  events,
1387
1986
  runId,
1987
+ telemetry: buildRuntimeTelemetry({
1988
+ input,
1989
+ runId,
1990
+ toolCallsExecuted: result.toolCallsExecuted,
1991
+ events,
1992
+ warnings,
1993
+ dynamicToolState,
1994
+ }),
1388
1995
  session: toSessionSummary(finalSession),
1389
1996
  };
1390
1997
  }
@@ -1436,6 +2043,14 @@ export const runCodaliTask = async (input) => {
1436
2043
  warnings,
1437
2044
  events,
1438
2045
  runId,
2046
+ telemetry: buildRuntimeTelemetry({
2047
+ input,
2048
+ runId,
2049
+ toolCallsExecuted: result.toolCallsExecuted,
2050
+ events,
2051
+ warnings,
2052
+ dynamicToolState,
2053
+ }),
1439
2054
  session: toSessionSummary(finalSession),
1440
2055
  };
1441
2056
  }