@convertcom/mcp-server 0.0.1-beta.17 → 0.0.1-beta.18

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 (38) hide show
  1. package/README.md +1 -1
  2. package/build/executor.js +271 -17
  3. package/build/executor.js.map +1 -1
  4. package/build/failure-attribution.d.ts +55 -0
  5. package/build/failure-attribution.js +326 -0
  6. package/build/failure-attribution.js.map +1 -0
  7. package/build/image-dimensions.d.ts +5 -0
  8. package/build/image-dimensions.js +203 -0
  9. package/build/image-dimensions.js.map +1 -0
  10. package/build/kb-search.js +16 -1
  11. package/build/kb-search.js.map +1 -1
  12. package/build/namespaced.d.ts +5 -0
  13. package/build/namespaced.js +199 -11
  14. package/build/namespaced.js.map +1 -1
  15. package/build/operator.d.ts +14 -0
  16. package/build/operator.js +128 -19
  17. package/build/operator.js.map +1 -1
  18. package/build/prompts.js +10 -3
  19. package/build/prompts.js.map +1 -1
  20. package/build/runtime.d.ts +15 -2
  21. package/build/runtime.js +186 -20
  22. package/build/runtime.js.map +1 -1
  23. package/build/whats-new.d.ts +24 -0
  24. package/build/whats-new.js +150 -0
  25. package/build/whats-new.js.map +1 -0
  26. package/data/convert-kb-legacy-map.json +24956 -9527
  27. package/data/convert-kb-manifest.json +26 -27
  28. package/data/convert-kb.json +44625 -64753
  29. package/data/kb-vectors/0/embedding.index.3.proxima +0 -0
  30. package/data/kb-vectors/0/scalar.0.ipc +0 -0
  31. package/data/kb-vectors/corpus.json +2 -2
  32. package/data/kb-vectors/del.0 +0 -0
  33. package/data/kb-vectors/idmap.0/000008.sst +0 -0
  34. package/data/kb-vectors/idmap.0/IDENTITY +1 -1
  35. package/data/kb-vectors/idmap.0/MANIFEST-000005 +0 -0
  36. package/data/kb-vectors/manifest.3 +0 -0
  37. package/package.json +1 -1
  38. package/server.json +2 -2
package/build/operator.js CHANGED
@@ -2,6 +2,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { executeNamespacedTool, namespaceToolNameForAction } from './namespaced.js';
3
3
  import { JSON_SCHEMA_DRAFT_2020_12 } from './shared.js';
4
4
  import { scrubDiagnosticText } from './redaction.js';
5
+ import { failureAttribution, failureFromStructured, inputFailure, safeFailureAttribution, } from './failure-attribution.js';
5
6
  import { FEEDBACK_BODY_SCHEMA, FEEDBACK_CONFIRMATION_SCHEMA, FeedbackContractError, feedbackConsentProof, feedbackSubmissionContextOutline, feedbackSubmissionDigest, sanitizeFeedbackBody, validateAndSanitizeFeedback, } from './feedback.js';
6
7
  const APPROVAL_RULE_CODES = new Set([
7
8
  'experience.activate',
@@ -129,7 +130,7 @@ const READ_ACTIONS = [
129
130
  {
130
131
  action: 'get_defaults',
131
132
  mode: 'read',
132
- description: 'Infer and cache project defaults such as preferred goal, audience, location, URL, and traffic split.',
133
+ description: 'Infer and cache suggested project defaults such as preferred goal, audience, location, URL, and traffic split. Suggestions are not consent to attach or create a resource.',
133
134
  path_parameters: ['account_id', 'project_id'],
134
135
  accepts_body: true,
135
136
  },
@@ -145,14 +146,14 @@ const WRITE_ACTIONS = [
145
146
  {
146
147
  action: 'create_custom_code_ab_test',
147
148
  mode: 'write',
148
- description: 'Create a draft custom-code A/B experience and verify the persisted payload. Required body: url plus non-empty js or css. Optional: name, control_name, variation_name, description, objective, primary_goal, goals, audiences, locations, settings. Canonical custom variations may be supplied as variations[].changes[] with at most 100 changes per variation for bounded verification. Payload verification is independent from attached-goal lifecycle health; goal_configuration reports archived or unresolved goals without encouraging a duplicate create. Returns one authoritative experience plus a compact create receipt. Unknown fields are rejected.',
149
+ description: 'Create a draft custom-code A/B experience and verify the persisted payload. Required body: url plus non-empty js or css. Optional: name, control_name, variation_name, description, objective, primary_goal, goals, audiences, locations, settings. Omitted goals and goals: [] can both trigger Convert goal fallback; resolve user-intended existing goals and primary, send explicit IDs, and inspect the expanded returned experience. A bare URL does not select the UI Experience URL location; attach a user-intended location explicitly. Canonical custom variations may be supplied as variations[].changes[] with at most 100 changes per variation for bounded verification. Payload verification is independent from attached-goal lifecycle health; goal_configuration reports archived or unresolved goals without encouraging a duplicate create. Returns one authoritative experience plus a compact create receipt. Unknown fields are rejected.',
149
150
  path_parameters: ['account_id', 'project_id'],
150
151
  accepts_body: true,
151
152
  },
152
153
  {
153
154
  action: 'create_split_url_test',
154
155
  mode: 'write',
155
- description: 'Create a draft split URL experience with an Original no-redirect control and a redirect treatment, allocated 50/50, and verify both arms, baseline, allocation and redirects. Required body: original_pattern (or url) and variation_pattern (variation_url and redirect_url are accepted aliases). Optional: name, variation_name, case_sensitive, description, objective, primary_goal, goals, audiences, locations, settings, split_regex_support. Custom variations require unique names, exactly one explicit is_baseline: true control with no changes, one defaultRedirect per treatment, and positive numeric traffic_distribution per arm totaling 100; at most 100 changes per variation. goal_configuration separately reports archived or unresolved goals. Returns one authoritative experience plus a create receipt. If verified is false, inspect that existing draft; never repeat create or activate it automatically. Unknown fields are rejected.',
156
+ description: 'Create a draft split URL experience with an Original no-redirect control and a redirect treatment, allocated 50/50, and verify both arms, baseline, allocation and redirects. Required body: original_pattern (or url) and variation_pattern (variation_url and redirect_url are accepted aliases). Optional: name, variation_name, case_sensitive, description, objective, primary_goal, goals, audiences, locations, settings, split_regex_support. Omitted goals and goals: [] can both trigger Convert goal fallback; resolve user-intended existing goals and primary, send explicit IDs, and inspect the expanded returned experience. A bare URL does not select the UI Experience URL location; attach a user-intended location explicitly. Custom variations require unique names, exactly one explicit is_baseline: true control with no changes, one defaultRedirect per treatment, and positive numeric traffic_distribution per arm totaling 100; at most 100 changes per variation. goal_configuration separately reports archived or unresolved goals. Returns one authoritative experience plus a create receipt. If verified is false, inspect that existing draft; never repeat create or activate it automatically. Unknown fields are rejected.',
156
157
  path_parameters: ['account_id', 'project_id'],
157
158
  accepts_body: true,
158
159
  },
@@ -172,6 +173,10 @@ const WRITE_ACTIONS = [
172
173
  },
173
174
  ];
174
175
  const ALL_ACTIONS = [...READ_ACTIONS, ...WRITE_ACTIONS];
176
+ const ALL_ACTION_NAMES = new Set(ALL_ACTIONS.map((action) => action.action));
177
+ export function isRegisteredOperatorAction(action) {
178
+ return typeof action === 'string' && ALL_ACTION_NAMES.has(action);
179
+ }
175
180
  const DEFAULT_RESULTS_PER_PAGE = 50;
176
181
  const MAX_OPERATOR_COLLECTION_PAGES = 20;
177
182
  const MAX_OPERATOR_COLLECTION_BYTES = 8 * 1024 * 1024;
@@ -254,6 +259,9 @@ export const OPERATOR_OUTPUT_SCHEMA = {
254
259
  error: {},
255
260
  issues: { type: 'array', items: { type: 'object' } },
256
261
  verified: { type: 'boolean' },
262
+ failure: { type: 'object', description: 'Closed, identifier-free failure category and provenance for a failed operator call.' },
263
+ partial_failure_count: { type: 'integer', minimum: 1, description: 'Number of failed namespace calls when an operator workflow still returned partial evidence.' },
264
+ partial_failure: { type: 'object', description: 'Closed, identifier-free dominant failure category and provenance for a partially successful workflow.' },
257
265
  },
258
266
  additionalProperties: true,
259
267
  };
@@ -617,7 +625,7 @@ async function prepareFeedback(args, toolSet, readOnlyImpersonation) {
617
625
  approved_payload_digest: digest,
618
626
  },
619
627
  next_step: submitAvailable
620
- ? 'Show the sanitized summary, context outline, context byte count, payload digest, and review_contract to the user. Explain that context values are intentionally not re-echoed and that the digest binds approval to the complete sanitized payload. Only after explicit approval, call submit_feedback with the unchanged body and set all three confirmation_template booleans to true.'
628
+ ? 'Show the sanitized summary, context outline, context byte count, payload digest, and review_contract to the user. Explain that context values are intentionally not re-echoed and that the digest binds approval to the complete sanitized payload. Only after explicit approval, call submit_feedback with the unchanged body and set all three confirmation_template booleans to true. If the host reports no approval while the confirmation matches this template exactly, the host failed to capture approval: re-present the host approval card instead of editing the confirmation.'
621
629
  : readOnlyImpersonation
622
630
  ? 'Show the sanitized summary, context outline, context byte count, payload digest, and review_contract to the user. Staff impersonation is browse-only, so this preparation cannot be submitted or attributed as the customer. Remove as_user and prepare the feedback again as the connected staff member if they explicitly want to submit it.'
623
631
  : 'Show the sanitized summary, context outline, context byte count, payload digest, and review_contract to the user. This read-only MCP profile cannot store feedback; do not claim submission. A write-enabled Convert MCP profile is required after approval.',
@@ -941,15 +949,23 @@ async function experimentPortfolio(args, context) {
941
949
  }
942
950
  async function experimentVelocity(args, context) {
943
951
  await report(context, 'Fetching experiences for velocity report', 1, 3);
944
- const experienceCall = await listExperiences(basePath(args), portfolioFilters(args));
952
+ const [experienceCall, goalCall] = await Promise.all([
953
+ listExperiences(basePath(args), portfolioFilters(args)),
954
+ listResource('goals', basePath(args)),
955
+ ]);
945
956
  const experiences = collectionFrom(experienceCall.data);
946
- const summaries = summarizeExperiences(experiences);
957
+ const summaries = summarizeExperiences(experiences, canonicalGoalDefinitions(collectionFrom(goalCall.data)));
958
+ const goalCoverage = investigationCollectionCoverage(goalCall);
947
959
  const launched = summaries.filter((item) => typeof item.start_time === 'number' && item.start_time > 0);
948
960
  const completed = summaries.filter((item) => typeof item.end_time === 'number' && item.end_time > 0);
949
961
  const coverage = investigationCollectionCoverage(experienceCall);
950
962
  const completeCoverage = coverage.status === 'complete';
951
963
  return {
952
- source_status: { experiences: experienceCall.ok ? 'available' : 'unavailable' },
964
+ source_status: {
965
+ experiences: experienceCall.ok ? 'available' : 'unavailable',
966
+ goals: goalCall.ok ? 'available' : 'unavailable',
967
+ },
968
+ goal_authority_coverage: goalCoverage,
953
969
  coverage,
954
970
  total: experienceCall.ok && completeCoverage ? summaries.length : null,
955
971
  created_or_returned: experienceCall.ok ? summaries.length : null,
@@ -974,7 +990,8 @@ async function experimentVelocity(args, context) {
974
990
  ...(experienceCall.ok && !completeCoverage
975
991
  ? [`Experience velocity coverage is ${coverage.status}; exact totals and averages are null, while returned_* fields describe only fetched rows.`]
976
992
  : []),
977
- ...warningsFromCalls([experienceCall]),
993
+ ...goalAuthorityWarnings(goalCoverage),
994
+ ...warningsFromCalls([experienceCall, goalCall]),
978
995
  ],
979
996
  };
980
997
  }
@@ -1474,9 +1491,13 @@ async function investigate(args, context) {
1474
1491
  }
1475
1492
  async function failedTestAnalysis(args, context) {
1476
1493
  await report(context, 'Fetching completed and paused experiences', 1, 3);
1477
- const experienceCall = await listExperiences(basePath(args), { status: ['completed', 'paused'] });
1494
+ const [experienceCall, goalCall] = await Promise.all([
1495
+ listExperiences(basePath(args), { status: ['completed', 'paused'] }),
1496
+ listResource('goals', basePath(args)),
1497
+ ]);
1478
1498
  const experiences = collectionFrom(experienceCall.data);
1479
- const summaries = summarizeExperiences(experiences);
1499
+ const summaries = summarizeExperiences(experiences, canonicalGoalDefinitions(collectionFrom(goalCall.data)));
1500
+ const goalCoverage = investigationCollectionCoverage(goalCall);
1480
1501
  const applicable = summaries.filter((item) => item.outcome !== 'not_applicable');
1481
1502
  const losses = applicable.filter((item) => [
1482
1503
  'negative',
@@ -1488,7 +1509,11 @@ async function failedTestAnalysis(args, context) {
1488
1509
  const coverage = investigationCollectionCoverage(experienceCall);
1489
1510
  const completeCoverage = coverage.status === 'complete';
1490
1511
  return {
1491
- source_status: { experiences: experienceCall.ok ? 'available' : 'unavailable' },
1512
+ source_status: {
1513
+ experiences: experienceCall.ok ? 'available' : 'unavailable',
1514
+ goals: goalCall.ok ? 'available' : 'unavailable',
1515
+ },
1516
+ goal_authority_coverage: goalCoverage,
1492
1517
  coverage,
1493
1518
  returned_reviewed: experienceCall.ok ? summaries.length : null,
1494
1519
  total_reviewed: experienceCall.ok && completeCoverage ? summaries.length : null,
@@ -1509,7 +1534,8 @@ async function failedTestAnalysis(args, context) {
1509
1534
  ...(experienceCall.ok && !completeCoverage
1510
1535
  ? [`Failed-test analysis coverage is ${coverage.status}; exact totals are null and returned_* fields describe only fetched rows.`]
1511
1536
  : []),
1512
- ...warningsFromCalls([experienceCall]),
1537
+ ...goalAuthorityWarnings(goalCoverage),
1538
+ ...warningsFromCalls([experienceCall, goalCall]),
1513
1539
  ],
1514
1540
  };
1515
1541
  }
@@ -1599,20 +1625,27 @@ async function cleanupCandidates(args, context) {
1599
1625
  await report(context, 'Fetching cleanup candidates', 1, 4);
1600
1626
  const path = basePath(args);
1601
1627
  const staleDraftDays = numberFrom(args.body.stale_draft_days ?? args.query.stale_draft_days, 30);
1602
- const [drafts, allExperiences, goals, audiences, locations, features] = await Promise.all([
1628
+ const [drafts, allExperiences, goals, audiences, locations, features, goalAuthority] = await Promise.all([
1603
1629
  listExperiences(path, { status: ['draft'] }),
1604
1630
  listExperiences(path),
1605
1631
  listResource('goals', path, { usage: 'not_used' }),
1606
1632
  listResource('audiences', path, { usage: 'not_used' }),
1607
1633
  listResource('locations', path, { usage: 'not_used' }),
1608
1634
  listResource('features', path),
1635
+ listResource('goals', path),
1609
1636
  ]);
1610
1637
  const experienceItems = collectionFrom(allExperiences.data);
1611
1638
  const draftItems = collectionFrom(drafts.data);
1639
+ // Shared goals can embed stale or reordered definitions per experience; resolve
1640
+ // every stale-draft summary against the goals authority (draft_qa parity) so a
1641
+ // shared goal reports one consistent type/name/status everywhere. Missing ids
1642
+ // fall back to their embeds, and an unavailable authority is disclosed below.
1643
+ const canonicalGoals = canonicalGoalDefinitions(collectionFrom(goalAuthority.data));
1644
+ const goalAuthorityCoverage = investigationCollectionCoverage(goalAuthority);
1612
1645
  const staleCutoff = Date.now() / 1000 - staleDraftDays * 24 * 60 * 60;
1613
1646
  const staleDrafts = draftItems
1614
1647
  .filter((item) => typeof item.created_at !== 'number' || item.created_at < staleCutoff)
1615
- .map((experience) => summarizeExperience(experience));
1648
+ .map((experience) => summarizeExperience(experience, canonicalGoals));
1616
1649
  const experienceCoverage = investigationCollectionCoverage(allExperiences);
1617
1650
  const draftCoverage = investigationCollectionCoverage(drafts);
1618
1651
  const completeExperienceCoverage = experienceCoverage.status === 'complete';
@@ -1648,12 +1681,13 @@ async function cleanupCandidates(args, context) {
1648
1681
  sdk_search_patterns: feature.key ? [`${feature.key}`, `isFeatureEnabled(\"${feature.key}\")`, `getFeature(\"${feature.key}\")`] : [],
1649
1682
  };
1650
1683
  });
1651
- const cleanupWarnings = warningsFromCalls([drafts, allExperiences, goals, audiences, locations, features]);
1684
+ const cleanupWarnings = warningsFromCalls([drafts, allExperiences, goals, audiences, locations, features, goalAuthority]);
1652
1685
  for (const [resource, status] of Object.entries(candidateSourceStatus)) {
1653
1686
  if (status !== 'complete') {
1654
1687
  cleanupWarnings.push(`${resource} cleanup candidates are unavailable because verification status is ${status}; other resource categories remain independently evaluated.`);
1655
1688
  }
1656
1689
  }
1690
+ cleanupWarnings.push(...goalAuthorityWarnings(goalAuthorityCoverage));
1657
1691
  return {
1658
1692
  stale_draft_days: staleDraftDays,
1659
1693
  source_status: {
@@ -1668,6 +1702,7 @@ async function cleanupCandidates(args, context) {
1668
1702
  drafts: draftCoverage,
1669
1703
  experiences: experienceCoverage,
1670
1704
  ...resourceCoverage,
1705
+ goal_authority: goalAuthorityCoverage,
1671
1706
  },
1672
1707
  returned_stale_drafts: drafts.ok ? staleDrafts.length : null,
1673
1708
  total_stale_drafts: drafts.ok && draftCoverage.status === 'complete' ? staleDrafts.length : null,
@@ -2412,6 +2447,7 @@ async function namespaceCall(namespace, action, path, body, query, confirmation)
2412
2447
  error: structured.error ?? text ?? 'Unknown error',
2413
2448
  details: structured,
2414
2449
  text,
2450
+ failure: failureFromStructured(structured, 'namespace'),
2415
2451
  });
2416
2452
  }
2417
2453
  return recordNamespaceCall({
@@ -2437,6 +2473,13 @@ async function safeNamespaceCall(namespace, action, path, body, query) {
2437
2473
  namespace,
2438
2474
  action,
2439
2475
  error: error instanceof Error ? error.name : 'ThrownError',
2476
+ failure: failureAttribution({
2477
+ category: 'unknown',
2478
+ source: 'operator',
2479
+ stage: 'operator_workflow',
2480
+ dispatchState: 'unknown',
2481
+ code: error instanceof Error ? error.name : 'ThrownError',
2482
+ }),
2440
2483
  });
2441
2484
  }
2442
2485
  }
@@ -2460,6 +2503,11 @@ function basePath(args, extra = [], override = {}) {
2460
2503
  }
2461
2504
  return path;
2462
2505
  }
2506
+ function goalAuthorityWarnings(coverage) {
2507
+ return coverage.status === 'complete' ? [] : [
2508
+ `Goal type/name/status for unresolved IDs fell back to experience embeds because the goals authority is ${coverage.status}; verify shared goal definitions via goals.get before acting on them.`,
2509
+ ];
2510
+ }
2463
2511
  function summarizeExperiences(experiences, canonicalGoals = new Map()) {
2464
2512
  return experiences.map((experience) => summarizeExperience(experience, canonicalGoals));
2465
2513
  }
@@ -3213,11 +3261,25 @@ function stableLifecycleStatus(value) {
3213
3261
  : 'unknown';
3214
3262
  }
3215
3263
  function stableCallErrorClass(call) {
3264
+ if (call.failure?.category === 'input')
3265
+ return 'invalid_input';
3266
+ if (call.failure?.category === 'permission')
3267
+ return call.status === 401 ? 'authentication_failed' : 'permission_denied';
3268
+ if (call.failure?.category === 'not_found')
3269
+ return 'not_found';
3270
+ if (call.failure?.category === 'timeout')
3271
+ return 'upstream_timeout';
3272
+ if (call.failure?.category === 'network')
3273
+ return 'upstream_network_error';
3274
+ if (call.failure?.category === 'cancellation')
3275
+ return 'cancelled';
3276
+ if (call.failure?.category === 'internal')
3277
+ return 'internal_error';
3216
3278
  const status = call.status;
3217
3279
  if (status === 401)
3218
3280
  return 'authentication_failed';
3219
3281
  if (status === 403)
3220
- return 'permission_denied';
3282
+ return 'upstream_request_failed';
3221
3283
  if (status === 404)
3222
3284
  return 'not_found';
3223
3285
  if (status === 422)
@@ -4018,11 +4080,40 @@ async function report(context, message, progress, total) {
4018
4080
  function finalizeOperatorResult(action, data, calls) {
4019
4081
  const allCallsFailed = calls.length > 0 && calls.every((call) => !call.ok);
4020
4082
  if (!allCallsFailed) {
4021
- return okResult(action, data);
4083
+ return okResult(action, data, calls);
4022
4084
  }
4023
4085
  const status = dominantFailureStatus(calls);
4024
4086
  const errors = calls.map((call) => `${call.namespace}.${call.action}: ${stableCallErrorClass(call)}`);
4025
- return errorResult(action, 'AllUpstreamCallsFailed', `All upstream namespace calls failed for operator.${action}.`, { status, data, errors });
4087
+ const partial = operatorFailureSummary(calls);
4088
+ return errorResult(action, 'AllUpstreamCallsFailed', `All upstream namespace calls failed for operator.${action}.`, {
4089
+ status,
4090
+ data,
4091
+ errors,
4092
+ ...(partial ? { failure: partial.failure } : {}),
4093
+ });
4094
+ }
4095
+ function operatorFailureSummary(calls) {
4096
+ const failures = calls
4097
+ .filter((call) => !call.ok)
4098
+ .map((call) => call.failure ?? failureFromStructured({
4099
+ ...(typeof call.status === 'number' ? { status: call.status } : {}),
4100
+ error: typeof call.error === 'string' ? call.error : 'UnknownFailure',
4101
+ }, 'namespace'));
4102
+ if (failures.length === 0)
4103
+ return null;
4104
+ const priority = new Map([
4105
+ ['permission', 0],
4106
+ ['cancellation', 1],
4107
+ ['timeout', 2],
4108
+ ['network', 3],
4109
+ ['not_found', 4],
4110
+ ['upstream', 5],
4111
+ ['input', 6],
4112
+ ['internal', 7],
4113
+ ['unknown', 8],
4114
+ ]);
4115
+ const failure = failures.slice().sort((left, right) => (priority.get(left.category) ?? 99) - (priority.get(right.category) ?? 99))[0];
4116
+ return { count: failures.length, failure };
4026
4117
  }
4027
4118
  /** The most common HTTP status among failed calls, or 502 when no call reported a numeric status. */
4028
4119
  function dominantFailureStatus(calls) {
@@ -4036,11 +4127,12 @@ function dominantFailureStatus(calls) {
4036
4127
  return 502;
4037
4128
  return [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
4038
4129
  }
4039
- function okResult(action, data) {
4130
+ function okResult(action, data, calls = []) {
4040
4131
  const verified = typeof data.verified === 'boolean' ? data.verified : undefined;
4041
4132
  const responseData = verified === undefined ? data : { ...data };
4042
4133
  if (verified !== undefined)
4043
4134
  delete responseData.verified;
4135
+ const partial = operatorFailureSummary(calls);
4044
4136
  const payload = {
4045
4137
  ok: true,
4046
4138
  namespace: 'operator',
@@ -4049,6 +4141,10 @@ function okResult(action, data) {
4049
4141
  status: 200,
4050
4142
  data: responseData,
4051
4143
  ...(verified !== undefined ? { verified } : {}),
4144
+ ...(partial ? {
4145
+ partial_failure_count: partial.count,
4146
+ partial_failure: partial.failure,
4147
+ } : {}),
4052
4148
  };
4053
4149
  return {
4054
4150
  content: [{ type: 'text', text: JSON.stringify(payload) }],
@@ -4059,6 +4155,10 @@ function errorResult(action, code, message, extra = {}) {
4059
4155
  const scrubbedExtra = scrubOperatorErrorValue(extra);
4060
4156
  const safeExtra = isObject(scrubbedExtra) ? scrubbedExtra : {};
4061
4157
  const status = typeof safeExtra.status === 'number' ? safeExtra.status : 400;
4158
+ const existingFailure = safeFailureAttribution(safeExtra.failure);
4159
+ const inferredFailure = code === 'MissingPathParameters'
4160
+ ? inputFailure(code, 'operator', 'required', 'path')
4161
+ : failureFromStructured({ ...safeExtra, error: code }, 'operator');
4062
4162
  const payload = {
4063
4163
  ...safeExtra,
4064
4164
  ok: false,
@@ -4068,6 +4168,7 @@ function errorResult(action, code, message, extra = {}) {
4068
4168
  status,
4069
4169
  error: code,
4070
4170
  message: scrubDiagnosticText(message),
4171
+ failure: existingFailure ?? inferredFailure,
4071
4172
  };
4072
4173
  return {
4073
4174
  isError: true,
@@ -4121,6 +4222,14 @@ function approvalRequiredOperatorErrorResult(action, details) {
4121
4222
  error: 'ApprovalRequired',
4122
4223
  code: 'ApprovalRequired',
4123
4224
  message,
4225
+ failure: failureAttribution({
4226
+ category: 'permission',
4227
+ source: 'operator',
4228
+ stage: 'upstream_response',
4229
+ dispatchState: 'dispatched',
4230
+ code: 'ApprovalRequired',
4231
+ upstreamStatus: 422,
4232
+ }),
4124
4233
  approval_state: approvalState,
4125
4234
  approval_required: {
4126
4235
  rule_code: ruleCode,