@ziggs-ai/agent-sdk 0.10.8 → 0.11.1

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 (104) hide show
  1. package/CHANGELOG.md +11 -11
  2. package/README.md +1 -1
  3. package/dist/AgentHost.d.ts +96 -16
  4. package/dist/AgentHost.js +205 -25
  5. package/dist/adapters/AnthropicAdapter.js +1 -1
  6. package/dist/adapters/McpAdapter.d.ts +10 -10
  7. package/dist/adapters/McpAdapter.js +31 -21
  8. package/dist/adapters/OpenAIAdapter.js +1 -1
  9. package/dist/agent/Agent.d.ts +2 -2
  10. package/dist/agent/ClaudeSdkAgent.d.ts +15 -6
  11. package/dist/agent/ClaudeSdkAgent.js +149 -48
  12. package/dist/agent/claudeCognition.d.ts +3 -3
  13. package/dist/agent/claudeCognition.js +7 -7
  14. package/dist/agent/claudeSdkTools.d.ts +3 -3
  15. package/dist/agent/claudeSdkTools.js +34 -15
  16. package/dist/agent/hostedAgentConnection.d.ts +1 -1
  17. package/dist/agent/hostedAgentConnection.js +1 -1
  18. package/dist/agent/toolManagerToSdkMcp.d.ts +1 -1
  19. package/dist/agent/toolManagerToSdkMcp.js +2 -2
  20. package/dist/context/applyEffects.js +1 -1
  21. package/dist/defineAgent.js +33 -4
  22. package/dist/formatters/HistoryFormatter.js +2 -2
  23. package/dist/index.d.ts +4 -2
  24. package/dist/index.js +8 -5
  25. package/dist/ingress/normalizeIncoming.d.ts +1 -1
  26. package/dist/ingress/normalizeIncoming.js +7 -7
  27. package/dist/pricing/fleetDefaults.d.ts +3 -3
  28. package/dist/pricing/fleetDefaults.js +3 -3
  29. package/dist/pricing/fleetEvalFree.d.ts +1 -1
  30. package/dist/pricing/fleetEvalFree.js +1 -1
  31. package/dist/pricing/fleetFreeTierA.gen.d.ts +1 -1
  32. package/dist/pricing/fleetFreeTierA.gen.js +1 -1
  33. package/dist/pricing/fleetTierByAgentId.gen.d.ts +2 -2
  34. package/dist/pricing/fleetTierByAgentId.gen.js +9 -2
  35. package/dist/runtime/AgentMachine.d.ts +1 -1
  36. package/dist/runtime/AgentMachine.js +8 -8
  37. package/dist/runtime/PromptBuilder.d.ts +2 -2
  38. package/dist/runtime/PromptBuilder.js +3 -3
  39. package/dist/runtime/bootstrapCtx.d.ts +1 -1
  40. package/dist/runtime/bootstrapCtx.js +2 -2
  41. package/dist/runtime/buildOutcome.d.ts +1 -1
  42. package/dist/runtime/buildOutcome.js +4 -4
  43. package/dist/runtime/defaults.d.ts +5 -5
  44. package/dist/runtime/defaults.js +4 -4
  45. package/dist/runtime/deliveryWriteKeys.d.ts +1 -1
  46. package/dist/runtime/deliveryWriteKeys.js +3 -3
  47. package/dist/runtime/kickoff.d.ts +3 -3
  48. package/dist/runtime/kickoff.js +3 -3
  49. package/dist/runtime/runMechanical.d.ts +1 -1
  50. package/dist/runtime/runMechanical.js +1 -1
  51. package/dist/runtime/runTurn.d.ts +1 -1
  52. package/dist/runtime/runTurn.js +10 -10
  53. package/dist/runtime/taskBookkeeping.d.ts +3 -3
  54. package/dist/runtime/taskBookkeeping.js +3 -3
  55. package/dist/runtime/taskIntake.js +2 -2
  56. package/dist/server/ConnectionPool.js +1 -1
  57. package/dist/server/EventQueue.d.ts +1 -1
  58. package/dist/server/EventQueue.js +1 -1
  59. package/dist/server/SeenMessages.d.ts +2 -2
  60. package/dist/server/SeenMessages.js +2 -2
  61. package/dist/server/ZiggsEffectHandler.d.ts +2 -2
  62. package/dist/server/ZiggsEffectHandler.js +18 -9
  63. package/dist/server/agreements/AgreementService.d.ts +3 -8
  64. package/dist/server/agreements/AgreementService.js +4 -7
  65. package/dist/server/inboxLoop.d.ts +5 -5
  66. package/dist/server/inboxLoop.js +27 -11
  67. package/dist/server/polls/PollRunner.d.ts +36 -1
  68. package/dist/server/polls/PollRunner.js +42 -2
  69. package/dist/server/questMatch.d.ts +1 -1
  70. package/dist/server/questMatch.js +1 -1
  71. package/dist/server/runLauncher.js +3 -3
  72. package/dist/server/tasks/TaskService.d.ts +5 -5
  73. package/dist/server/tools/index.d.ts +1 -0
  74. package/dist/server/tools/index.js +7 -6
  75. package/dist/server/tools/tier1/protocolRunner.d.ts +1 -1
  76. package/dist/server/tools/tier1/protocolRunner.js +19 -19
  77. package/dist/server/tools/tier1/protocolTools.js +4 -4
  78. package/dist/server/tools/tier2/artifactTools.js +2 -2
  79. package/dist/server/tools/tier2/chatTools.js +2 -2
  80. package/dist/server/tools/tier2/connectionTools.js +3 -3
  81. package/dist/server/tools/tier2/contextTools.js +1 -1
  82. package/dist/server/tools/tier2/discoveryTools.js +2 -2
  83. package/dist/server/tools/tier2/grantsTools.js +1 -1
  84. package/dist/server/tools/tier2/linkTools.js +3 -3
  85. package/dist/server/tools/tier2/marketplaceTools.js +1 -1
  86. package/dist/server/tools/tier2/mcpConnectionTools.d.ts +4 -0
  87. package/dist/server/tools/tier2/mcpConnectionTools.js +136 -0
  88. package/dist/server/tools/tier2/paymentTools.js +1 -1
  89. package/dist/shared/operatorKeyClaims.d.ts +2 -2
  90. package/dist/shared/operatorKeyClaims.js +1 -1
  91. package/dist/tasks/protocolRegistry.d.ts +1 -1
  92. package/dist/tasks/protocolRegistry.js +1 -1
  93. package/dist/tools/ToolManager.d.ts +1 -1
  94. package/dist/tools/ToolManager.js +3 -2
  95. package/dist/tools/capabilityAdapter.d.ts +1 -1
  96. package/dist/tools/capabilityAdapter.js +3 -3
  97. package/dist/tools/defineTool.d.ts +7 -0
  98. package/dist/tools/defineTool.js +13 -1
  99. package/dist/tools/needs.d.ts +54 -0
  100. package/dist/tools/needs.js +84 -0
  101. package/dist/tools/recordReport.d.ts +3 -3
  102. package/dist/tools/recordReport.js +3 -3
  103. package/dist/types.d.ts +28 -18
  104. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  import { isBroadcastTarget, } from '@ziggs-ai/api-client';
2
2
  import { applyFleetCounterPricing, applyFleetPricingForProtocol, } from '../../../pricing/fleetDefaults.js';
3
- // ZIG-1111: the link-summary rule lives in AgreementClient, at the point the
3
+ // the link-summary rule lives in AgreementClient, at the point the
4
4
  // document is parsed. Every verb here inherits it through the client instead of
5
5
  // each one restating `engagementKind === 'link' ? linkSummary(a) : a` — which is
6
6
  // how counter, fulfill and subcontract came to return the raw document.
@@ -12,12 +12,12 @@ function normalizeState(state) {
12
12
  return 'failed';
13
13
  return state;
14
14
  }
15
- // ZIG-793: the `plan` tool-schema is underspecified (`plan: {type:'object'}`,
15
+ // the `plan` tool-schema is underspecified (`plan: {type:'object'}`,
16
16
  // `planReviewTiming: 'string'`), so the LLM emits steps missing stepId/order and an
17
17
  // out-of-enum planReviewTiming, which the backend DTO rejects. Normalize in-place
18
18
  // before the write.
19
19
  //
20
- // ZIG-1095: task-spawn is the only op left that carries a plan. Propose, counter
20
+ // task-spawn is the only op left that carries a plan. Propose, counter
21
21
  // and subcontract used to run this too — and their plans were then dropped on the
22
22
  // floor, since no agreement route has ever accepted one.
23
23
  // Returns the clamped review timing rather than only writing it back, so the
@@ -46,7 +46,7 @@ function normalizePlanInPlace(payload) {
46
46
  : 'with_proposal';
47
47
  }
48
48
  export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null, writeKeys = null,
49
- /** ZIG-1120 #8 — prefer the cached session snapshot over a fresh HTTP read. */
49
+ /** #8 — prefer the cached session snapshot over a fresh HTTP read. */
50
50
  cachedAgreements) {
51
51
  if (!payload?.operation)
52
52
  return null;
@@ -63,7 +63,7 @@ cachedAgreements) {
63
63
  return null;
64
64
  return agreementService.respond(payload.agreementId, payload.action);
65
65
  case 'agreement-claim': {
66
- // ZIG-1021: the one claim verb — quest, standing offer, or link invite.
66
+ // the one claim verb — quest, standing offer, or link invite.
67
67
  if (!payload.agreementId)
68
68
  throw new Error('agreement-claim requires `agreementId`');
69
69
  return await agreementService.claim(payload.agreementId);
@@ -101,11 +101,11 @@ cachedAgreements) {
101
101
  return null;
102
102
  let spawnAgreementId = payload.agreementId;
103
103
  const ags = await agreementsInChat(services, chatId, cachedAgreements);
104
- // b2 (ZIG-832): if the assignee is not a party on the cited agreement,
104
+ // b2: if the assignee is not a party on the cited agreement,
105
105
  // redirect the spawn to the ACTIVE sub-agreement (same root) where the
106
106
  // assignee IS the provider — the orchestrator often cites the top-level
107
107
  // hire, which the backend rejects with "assignee not a party".
108
- // ZIG-858: also redirect when the cited agreement is no longer active —
108
+ // also redirect when the cited agreement is no longer active —
109
109
  // a count-bound sub fulfills after its quota (default 1), and the LLM
110
110
  // keeps citing that now-terminal sub for the next spawn even when a
111
111
  // fresh active sub for the same executor exists.
@@ -120,14 +120,14 @@ cachedAgreements) {
120
120
  spawnAgreementId = sub.agreementId;
121
121
  }
122
122
  }
123
- // Safety guard (ZIG-832, B): a spawn against an agreement that is not active,
123
+ // Safety guard: a spawn against an agreement that is not active,
124
124
  // or whose assignee is not a party on it, hard-400s at the backend AND makes
125
125
  // the worker invent a "connection needed" message. That happens when the
126
126
  // orchestrator spawns before its sub-agreement is subcontracted/approved.
127
127
  // Skip GRACEFULLY instead — the sub-agreement's activation event will wake
128
128
  // the orchestrator to spawn correctly on a later turn, no 400, no cascade.
129
129
  const target = ags.find((a) => a.agreementId === spawnAgreementId);
130
- // ZIG-858: a TERMINAL target is not the same as a not-yet-active one — no
130
+ // a TERMINAL target is not the same as a not-yet-active one — no
131
131
  // activation event will ever arrive, so the "wait for a later turn" skip
132
132
  // below would stall the run. Tell the caller the actionable next step:
133
133
  // subcontract a fresh sub for this executor, then spawn under it.
@@ -176,7 +176,7 @@ cachedAgreements) {
176
176
  });
177
177
  }
178
178
  case 'task-update': {
179
- // ZIG-1057: `state` only — legacy `status` alias removed. Fleet prompts
179
+ // `state` only — legacy `status` alias removed. Fleet prompts
180
180
  // must emit state (coffee / fruit / ledger / agent-definer).
181
181
  const terminalRaw = typeof payload.state === 'string' && payload.state ? payload.state : '';
182
182
  if (!payload.taskId || !terminalRaw)
@@ -193,7 +193,7 @@ cachedAgreements) {
193
193
  throw new Error('task-update-plan-step requires `stepId`');
194
194
  if (!payload.status)
195
195
  throw new Error('task-update-plan-step requires `status`');
196
- // ZIG-793: a plan-step tick is cosmetic progress. Worker subtasks usually
196
+ // a plan-step tick is cosmetic progress. Worker subtasks usually
197
197
  // have NO plan (only the orchestrator's job task does), and the LLM may tick
198
198
  // a stepId that isn't in the plan — the backend 400s on both. That must never
199
199
  // hard-fail the tool → fail the task → wake-loop. Make it best-effort.
@@ -213,7 +213,7 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
213
213
  if (!payload.proposedTo)
214
214
  throw new Error('agreement-propose requires `proposedTo` (userId or agentId). To create a task under an existing agreement, use `task_create` instead.');
215
215
  const isBroadcast = isBroadcastTarget(payload.proposedTo);
216
- // ZIG-1022: a link proposal is an agreement with engagementKind 'link' —
216
+ // a link proposal is an agreement with engagementKind 'link' —
217
217
  // no chat, no money; the target owner approves. Folds the old request_link.
218
218
  if (payload.engagementKind === 'link') {
219
219
  if (isBroadcast) {
@@ -225,7 +225,7 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
225
225
  throw new Error('agreement-propose requires `providerId` — name who does the work: your own id (you are offering to work) or the proposedTo id (you are commissioning the recipient).');
226
226
  }
227
227
  if (isBroadcast && payload.providerId) {
228
- // ZIG-1022: providerId = self on a broadcast publishes a STANDING OFFER
228
+ // providerId = self on a broadcast publishes a STANDING OFFER
229
229
  // (seller side open to any claimer). Any other providerId stays an error.
230
230
  if (actingAgentId && payload.providerId === actingAgentId) {
231
231
  const priced = applyFleetPricingForProtocol('agreement-propose', payload, actingAgentId);
@@ -256,7 +256,7 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
256
256
  });
257
257
  return services.agreementService.propose({ ...fields, chatId, idempotencyKey });
258
258
  }
259
- // ── ZIG-832: deterministic sub-agreement resolution ─────────────────────────
259
+ // ── deterministic sub-agreement resolution ─────────────────────────
260
260
  // The orchestrator delegates one task per worker, but the LLM inconsistently
261
261
  // (a) re-subcontracts the same worker (duplicate sub-agreements) and (b)
262
262
  // task_spawns a worker under the TOP-LEVEL hire — where the worker is not a
@@ -267,7 +267,7 @@ const TERMINAL_STATUS = new Set(['fulfilled', 'cancelled', 'refunded', 'rejected
267
267
  async function agreementsInChat(services, chatId, cachedAgreements) {
268
268
  if (!chatId)
269
269
  return [];
270
- // ZIG-1120 #8 — createZiggsEffectHandler already cached the session snapshot;
270
+ // #8 — createZiggsEffectHandler already cached the session snapshot;
271
271
  // prefer that over another GET /context/snapshot just for the agreements array.
272
272
  // Empty array is a real answer ("none in this chat") — do not re-fetch.
273
273
  if (cachedAgreements !== undefined) {
@@ -297,14 +297,14 @@ function agActive(a) {
297
297
  // Spawnable = actually ACTIVE. A freshly-subcontracted sub is `draft` until its
298
298
  // counterparty auto-approves; spawning under a draft (or citing an agreement the
299
299
  // assignee isn't a party on) 400s and cascades into a bogus worker "connection
300
- // needed" message. We treat only 'active' as spawnable (ZIG-832).
300
+ // needed" message. We treat only 'active' as spawnable.
301
301
  function agIsActiveStatus(a) {
302
302
  return String(a.status ?? '') === 'active';
303
303
  }
304
304
  async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId, writeKeys = null, cachedAgreements) {
305
305
  if (!payload.description || !payload.executorId)
306
306
  return null;
307
- // b1 (ZIG-832): idempotent subcontract — if a non-terminal sub-agreement for
307
+ // b1: idempotent subcontract — if a non-terminal sub-agreement for
308
308
  // this executor already exists under the same parent, reuse it instead of
309
309
  // creating a duplicate (which bloats the chain and blocks the fulfill cascade).
310
310
  if (chatId) {
@@ -316,7 +316,7 @@ async function subcontractAgreement(services, payload, chatId, _agents, actingAg
316
316
  }
317
317
  const priced = applyFleetPricingForProtocol('agreement-subcontract', payload, actingAgentId);
318
318
  const { operation: _op, ...fields } = priced;
319
- // ZIG-832: a subcontract must carry a working close condition, or it hangs
319
+ // a subcontract must carry a working close condition, or it hangs
320
320
  // 'active' forever — and because the backend only auto-fulfills a parent hire
321
321
  // once every child sub-agreement is terminal (the fulfill cascade), one
322
322
  // never-closing child leaves the whole engagement open. Deciding lifecycle is
@@ -328,7 +328,7 @@ async function subcontractAgreement(services, payload, chatId, _agents, actingAg
328
328
  // sub — which then cascades the parent closed. A genuinely durable subcontract
329
329
  // still opts out by setting maxExecutions>1 or a time-bound expiresAt.
330
330
  const f = fields;
331
- // ZIG-858: repair incomplete terms instead of clobbering them. The model
331
+ // repair incomplete terms instead of clobbering them. The model
332
332
  // often emits a real quota or deadline but gets the envelope wrong — a
333
333
  // numeric-string maxExecutions, or a quota/deadline with the lifecycle field
334
334
  // missing or mismatched. The old check treated all of those as "no close
@@ -2,7 +2,7 @@ import { defineTool } from '../../../tools/defineTool.js';
2
2
  import { requireProtocolOperation } from '../../../tasks/protocolRegistry.js';
3
3
  import { dispatchProtocolOp } from './protocolRunner.js';
4
4
  import { AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION, } from '@ziggs-ai/api-client';
5
- /** ZIG-957 — keep these enums in lockstep with MCP ziggs_agreement_* / ziggs_task_create. */
5
+ /** keep these enums in lockstep with MCP ziggs_agreement_* / ziggs_task_create. */
6
6
  const LIFECYCLE_ENUM = ['open', 'time-bound', 'count-bound'];
7
7
  const PLAN_REVIEW_TIMING_ENUM = ['with_proposal', 'before_execution'];
8
8
  const lifecycleParam = {
@@ -17,7 +17,7 @@ const planReviewTimingParam = {
17
17
  };
18
18
  /**
19
19
  * The handler for a protocol verb, looked up by tool name in the registry
20
- * (ZIG-1110). Keying off the tool name rather than restating the operation is
20
+ *. Keying off the tool name rather than restating the operation is
21
21
  * what makes half-registration impossible: a verb missing from
22
22
  * PROTOCOL_TOOL_TO_OPERATION throws here, while this module loads, instead of
23
23
  * dispatching correctly and then being unrecognised as protocol at every
@@ -43,13 +43,13 @@ function handlerFor(toolName) {
43
43
  return result;
44
44
  };
45
45
  }
46
- export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service', 'link'], description: "Contract type. Defaults to service. 'link' proposes a bilateral trust link (no work, no money)." }, parentAgreementId: 'string', providerId: { type: 'string', description: AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', billing: { type: 'string', enum: ['total', 'per_task'], description: "How price reads: 'total' (default, escrowed and paid at the end) or 'per_task' (a rate settled as each task completes — standing agreements only, and the default for a hire)." }, agreementDescription: 'string' }, handlerFor('agreement_propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. Engagement precedence (posted-first): 1) REUSE an active agreement on matching terms, 2) CLAIM the counterparty\'s listing (marketplace_view → agreement_claim; never counter a listing), 3) POST a quest broadcast here when nothing listed fits, 4) direct-propose ONLY for bespoke terms, renegotiation, or commissioning a named counterparty with no listing — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay") on direct proposals. Broadcast with proposedTo="everyone" (fully public) or "org" (your org only): no providerId publishes a QUEST any agent may claim; providerId = your own id publishes a STANDING OFFER (claiming is agreement_claim; browsing is marketplace_view). engagementKind "link" with proposedTo = an agent id proposes bilateral trust. engagementKind defaults to service. Agreements are STANDING by default (no expiry, unlimited tasks under one agreement); set expiresAt or maxExecutions only when the engagement should end on its own.', isAgreementCreation: true });
46
+ export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service. For a bilateral trust link, use link_propose.' }, parentAgreementId: 'string', providerId: { type: 'string', description: AGREEMENT_PROPOSE_PROVIDER_ID_DESCRIPTION }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', billing: { type: 'string', enum: ['total', 'per_task'], description: "How price reads: 'total' (default, escrowed and paid at the end) or 'per_task' (a rate settled as each task completes — standing agreements only, and the default for a hire)." }, agreementDescription: 'string' }, handlerFor('agreement_propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. Engagement precedence (posted-first): 1) REUSE an active agreement on matching terms, 2) CLAIM the counterparty\'s listing (marketplace_view → agreement_claim; never counter a listing), 3) POST a quest broadcast here when nothing listed fits, 4) direct-propose ONLY for bespoke terms, renegotiation, or commissioning a named counterparty with no listing — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay") on direct proposals. Broadcast with proposedTo="everyone" (fully public) or "org" (your org only): no providerId publishes a QUEST any agent may claim; providerId = your own id publishes a STANDING OFFER (claiming is agreement_claim; browsing is marketplace_view). For a bilateral trust link, use link_propose (or link_create_invite when you lack the agent id). engagementKind defaults to service. Agreements are STANDING by default (no expiry, unlimited tasks under one agreement); set expiresAt or maxExecutions only when the engagement should end on its own.', isAgreementCreation: true });
47
47
  export const agreementSubcontractTool = defineTool('agreement_subcontract', { description: { type: 'string', required: true }, executorId: { type: 'string', required: true }, parentAgreementId: { type: 'string', required: true }, payerId: 'string', price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', agreementDescription: 'string' }, handlerFor('agreement_subcontract'), { description: 'Delegate work to another agent under an existing parent agreement. Requires parentAgreementId.', isAgreementCreation: true });
48
48
  export const agreementRespondTool = defineTool('agreement_respond', { agreementId: { type: 'string', required: true }, action: { type: 'string', required: true, enum: ['approve', 'reject'] } }, handlerFor('agreement_respond'), { description: 'Approve or reject a pending DIRECT agreement proposal addressed to you. Open broadcasts (quests, standing offers, link invites) have no personal approval slot — claim those with agreement_claim instead, or ignore them to pass. ONE exception: a hand-off that pins YOU as provider carries your pending approval even as an open broadcast — approving consents to serving whoever claims it (the row stays open for claims); rejecting cancels the hand-off. Ignoring it leaves the hand-off unclaimable, so always respond.' });
49
49
  export const agreementClaimTool = defineTool('agreement_claim', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (quest / offer / link invite id)' } }, handlerFor('agreement_claim'), { description: 'Claim an open broadcast agreement by id — the DEFAULT way to engage: terms are already posted, consent is the claim, activation is instant, no negotiation turns. Claims a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; listings are take-it-or-leave-it — never counter one. Direct proposals are approved with agreement_respond instead, not claimed.' });
50
50
  export const taskSetResultTool = defineTool('task_set_result', {
51
51
  taskId: { type: 'string', required: true },
52
- // ZIG-957: same param name as MCP ziggs_task_set_result / PATCH /tasks/:id/state.
52
+ // same param name as MCP ziggs_task_set_result / PATCH /tasks/:id/state.
53
53
  state: { type: 'string', required: true, enum: ['completed', 'failed', 'cancelled'] },
54
54
  // Same object contract as MCP ({ summary, status, links, … }).
55
55
  result: {
@@ -1,7 +1,7 @@
1
1
  import { ARTIFACT_CAPABILITIES, recordArtifactCapability } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // ZIG-899 / ZIG-956 — deliberate artifact recording, defined once in
4
- // api-client's shared capability layer (including the ZIG-560 result-slot
3
+ // deliberate artifact recording, defined once in
4
+ // api-client's shared capability layer (including the result-slot
5
5
  // teaching, worded per surface task grammar).
6
6
  export const recordArtifactTool = toolFromCapability(recordArtifactCapability);
7
7
  export const ARTIFACT_TOOLS = toolsFromCapabilities(ARTIFACT_CAPABILITIES);
@@ -1,7 +1,7 @@
1
1
  import { CHAT_CAPABILITIES, openConversationCapability } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // ZIG-900 / ZIG-956 — chat_open only (chat listing stays a
4
- // context-rail read, ZIG-626 rule); defined once in api-client's shared
3
+ // chat_open only (chat listing stays a
4
+ // context-rail read, rule); defined once in api-client's shared
5
5
  // capability layer.
6
6
  export const openConversationTool = toolFromCapability(openConversationCapability);
7
7
  export const CHAT_TOOLS = toolsFromCapabilities(CHAT_CAPABILITIES);
@@ -1,9 +1,9 @@
1
1
  import { connectionProxyCapability, requestConnectionCapability, ConnectionsClient, } from '@ziggs-ai/api-client';
2
2
  import { defineTool } from '../../../tools/defineTool.js';
3
3
  import { toolFromCapability } from '../../../tools/capabilityAdapter.js';
4
- // ZIG-956 — connection_proxy / connection_request are shared capability
4
+ // connection_proxy / connection_request are shared capability
5
5
  // definitions (api-client). connection_list_grants stays SDK-only: the MCP
6
- // surface folds connection listing into ziggs_connection_list (ZIG-893
6
+ // surface folds connection listing into ziggs_connection_list (
7
7
  // allowance in the parity table).
8
8
  export const connectionProxyTool = toolFromCapability(connectionProxyCapability);
9
9
  export const requestConnectionTool = toolFromCapability(requestConnectionCapability);
@@ -35,7 +35,7 @@ export const connectionListGrantsTool = defineTool('connection_list_grants', {
35
35
  throw wrapped;
36
36
  }
37
37
  }, {
38
- // ZIG-1128: this shipped with no description at all — the model was handed a
38
+ // this shipped with no description at all — the model was handed a
39
39
  // name and a parameter and left to guess what the tool was for. Found by the
40
40
  // new SDK tool-text guard.
41
41
  description: "List the grants issued to this agent on one stored connection. Returns the grantId that connection_proxy needs. Use it when you have a connectionId but not the grantId — grant_list (scopeKind=connection) is the wider read across every connection.",
@@ -1,6 +1,6 @@
1
1
  import { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // ZIG-869 / ZIG-775 / ZIG-956 — context grant tools ride the same api-client
3
+ // context grant tools ride the same api-client
4
4
  // clients as ziggs-mcp; the whole definition (schema + handler) is now shared
5
5
  // via the capability layer, so the two surfaces cannot fork.
6
6
  export const contextReadTool = toolFromCapability(contextReadCapability);
@@ -1,7 +1,7 @@
1
1
  import { agentSearchCapability, agentGetCapability } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability } from '../../../tools/capabilityAdapter.js';
3
- // ZIG-956 — agent discovery is a shared capability definition (api-client),
4
- // so search shaping (ZIG-664 zero-hit hint, reachability rows) is identical
3
+ // agent discovery is a shared capability definition (api-client),
4
+ // so search shaping (zero-hit hint, reachability rows) is identical
5
5
  // on both surfaces.
6
6
  export const agentSearchTool = toolFromCapability(agentSearchCapability);
7
7
  export const agentGetTool = toolFromCapability(agentGetCapability);
@@ -1,6 +1,6 @@
1
1
  import { GRANTS_CAPABILITIES, listGrantsCapability } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // ZIG-893 / ZIG-956 — the single "what authority do I hold?" tool, defined
3
+ // the single "what authority do I hold?" tool, defined
4
4
  // once in api-client's shared capability layer.
5
5
  export const listGrantsTool = toolFromCapability(listGrantsCapability);
6
6
  export const GRANTS_TOOLS = toolsFromCapabilities(GRANTS_CAPABILITIES);
@@ -1,11 +1,11 @@
1
1
  import { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // ZIG-898 / ZIG-956 — links are agreements with engagementKind 'link':
3
+ // links are agreements with engagementKind 'link':
4
4
  // bilateral agent-to-agent trust that gives any unpublished agent cross-org
5
5
  // reach. Defined once in api-client's shared capability layer (including the
6
- // ZIG-670 linkSummary shaping); this file only adapts them onto the SDK.
6
+ // linkSummary shaping); this file only adapts them onto the SDK.
7
7
  //
8
- // ZIG-1021/1022 shrank the rail to two tools. The agreement verbs carry the
8
+ ///1022 shrank the rail to two tools. The agreement verbs carry the
9
9
  // rest: request a direct link with agreement_propose (engagementKind 'link'),
10
10
  // claim an invite with agreement_claim, end a link with agreement_revoke.
11
11
  export const linkCreateInviteTool = toolFromCapability(createLinkInviteCapability);
@@ -1,6 +1,6 @@
1
1
  import { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // ZIG-1023 / ZIG-956 — the marketplace read (quests AND standing offers),
3
+ // the marketplace read (quests AND standing offers),
4
4
  // defined once in api-client's shared capability layer. Publishing is NOT
5
5
  // here: it is the tier-1 verb agreement_propose with proposedTo
6
6
  // "everyone" | "org" (providerId = own id for an offer, omitted for a quest);
@@ -0,0 +1,4 @@
1
+ import { type ToolDefinition } from '../../../tools/defineTool.js';
2
+ export declare const mcpToolsListTool: ToolDefinition;
3
+ export declare const mcpToolCallTool: ToolDefinition;
4
+ export declare const MCP_CONNECTION_TOOLS: ToolDefinition[];
@@ -0,0 +1,136 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import { ConnectionsClient } from '@ziggs-ai/api-client';
4
+ import { defineTool } from '../../../tools/defineTool.js';
5
+ import { resolveZiggsGatewayTarget } from '../../../adapters/McpAdapter.js';
6
+ /**
7
+ * Calling a tool on a connected MCP server, one shot.
8
+ *
9
+ * `connection_proxy` cannot do this. That path resolves a NAMED connector from
10
+ * `connection.provider` and checks the action against its fixed action list; a
11
+ * remote MCP server is stored with `provider: 'mcp'`, which no connector matches,
12
+ * so every attempt came back "Unknown provider: mcp". A hosted agent therefore had
13
+ * no way to use an MCP connection at all — found when a Linear grant was issued to
14
+ * `shipped-digest` and its task still failed after two proxy attempts.
15
+ *
16
+ * The gateway (`POST /connections/:id/mcp`) has always been the right door and is
17
+ * already in production; what was missing was a tool that walks through it. The
18
+ * transport, headers and MCP handshake are the same ones `McpAdapter`'s `ziggs`
19
+ * transport uses — reused rather than re-derived, so a change to the gateway
20
+ * contract lands in one place.
21
+ *
22
+ * Discovery is deliberate: with no `connectionId`/`grantId` the tool asks
23
+ * `GET /grants` for the agent's own live connection grants. One connection means
24
+ * no argument to guess; several means the caller must name one, because picking
25
+ * for them is how an agent reads the wrong workspace and reports it as fact.
26
+ */
27
+ async function resolveTarget(ctx, connectionId, grantId) {
28
+ if (connectionId && grantId)
29
+ return { connectionId, grantId };
30
+ const operatorKey = ctx['operatorKey'];
31
+ const agentId = ctx['agentId'];
32
+ if (!operatorKey)
33
+ throw new Error('operatorKey missing from tool context');
34
+ if (!agentId)
35
+ throw new Error('agentId missing from tool context');
36
+ // No explicit baseUrl: ConnectionsClient already resolves it the way the fleet
37
+ // configures it (HTTP_URL). Passing BACKEND_URL here was the same mistake as in
38
+ // the transport — undefined in deployment.
39
+ const groups = await new ConnectionsClient(operatorKey, agentId).listForHolder();
40
+ const live = groups.filter((g) => (g.grants ?? []).length > 0);
41
+ if (live.length === 0) {
42
+ throw new Error('No live connection grant. Ask the connection owner to issue one for this agent, then retry.');
43
+ }
44
+ const chosen = connectionId
45
+ ? live.find((g) => g.connectionId === connectionId)
46
+ : live.length === 1
47
+ ? live[0]
48
+ : undefined;
49
+ if (!chosen) {
50
+ throw new Error(`Name the connection to use — grants exist on ${live
51
+ .map((g) => g.connectionId)
52
+ .join(', ')}`);
53
+ }
54
+ const grant = grantId
55
+ ? (chosen.grants ?? []).find((g) => g.grantId === grantId)
56
+ : (chosen.grants ?? [])[0];
57
+ if (!grant) {
58
+ throw new Error(`No live grant on ${chosen.connectionId} for this agent`);
59
+ }
60
+ return { connectionId: chosen.connectionId, grantId: grant.grantId };
61
+ }
62
+ async function withClient(ctx, connectionId, grantId, run) {
63
+ const target = resolveZiggsGatewayTarget({
64
+ type: 'ziggs',
65
+ connectionId,
66
+ grantId,
67
+ operatorKey: ctx['operatorKey'],
68
+ agentId: ctx['agentId'],
69
+ }, { agentId: ctx['agentId'] });
70
+ const transport = new StreamableHTTPClientTransport(target.url, {
71
+ requestInit: { headers: target.headers },
72
+ });
73
+ const client = new Client({ name: 'ziggs-agent-sdk', version: '1.0.0' });
74
+ try {
75
+ await client.connect(transport);
76
+ return await run(client);
77
+ }
78
+ finally {
79
+ // A leaked session holds an upstream connection open on the gateway.
80
+ await client.close().catch(() => { });
81
+ }
82
+ }
83
+ export const mcpToolsListTool = defineTool('mcp_tools_list', {
84
+ connectionId: {
85
+ type: 'string',
86
+ description: 'Connection to inspect. Omit when you hold a grant on exactly one — it is then resolved for you.',
87
+ },
88
+ grantId: {
89
+ type: 'string',
90
+ description: 'Grant to use. Omit to use the live grant found on that connection.',
91
+ },
92
+ }, async (args, ctx) => {
93
+ const t = await resolveTarget(ctx, args['connectionId'], args['grantId']);
94
+ const tools = await withClient(ctx, t.connectionId, t.grantId, async (c) => {
95
+ const res = await c.listTools();
96
+ return (res.tools ?? []).map((tool) => ({
97
+ name: tool.name,
98
+ description: tool.description ?? null,
99
+ }));
100
+ });
101
+ return { connectionId: t.connectionId, grantId: t.grantId, tools };
102
+ }, {
103
+ description: "List the tools a connected MCP server exposes, through the Ziggs gateway — the owner's credential stays server-side. The grant may allow only some of them; a call outside the grant is refused by the gateway, not by you.",
104
+ });
105
+ export const mcpToolCallTool = defineTool('mcp_tool_call', {
106
+ tool: {
107
+ type: 'string',
108
+ required: true,
109
+ description: 'Tool name on the connected MCP server, e.g. list_issues.',
110
+ },
111
+ args: {
112
+ type: 'object',
113
+ description: "Arguments for that tool, as the server's schema defines them.",
114
+ },
115
+ connectionId: {
116
+ type: 'string',
117
+ description: 'Connection to call through. Omit when you hold a grant on exactly one.',
118
+ },
119
+ grantId: {
120
+ type: 'string',
121
+ description: 'Grant to use. Omit to use the live grant found on that connection.',
122
+ },
123
+ }, async (args, ctx) => {
124
+ const name = args['tool'];
125
+ if (!name)
126
+ throw new Error('tool is required');
127
+ const t = await resolveTarget(ctx, args['connectionId'], args['grantId']);
128
+ const result = await withClient(ctx, t.connectionId, t.grantId, (c) => c.callTool({
129
+ name,
130
+ arguments: args['args'] ?? {},
131
+ }));
132
+ return { connectionId: t.connectionId, tool: name, result };
133
+ }, {
134
+ description: "Call one tool on a connected MCP server through the Ziggs gateway. Use this for a connection whose provider is a remote MCP server — connection_proxy is for named connectors (github, jira) and refuses these with \"Unknown provider: mcp\". The gateway injects the owner's token upstream and checks the tool name against your grant, so you never hold the credential. Discover names with mcp_tools_list, or read allowed_actions off the grant in grant_list.",
135
+ });
136
+ export const MCP_CONNECTION_TOOLS = [mcpToolsListTool, mcpToolCallTool];
@@ -1,6 +1,6 @@
1
1
  import { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from '@ziggs-ai/api-client';
2
2
  import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
3
- // ZIG-956 — the wallet toolset is defined once in api-client's shared
3
+ // the wallet toolset is defined once in api-client's shared
4
4
  // capability layer; this file only adapts it onto the SDK surface. Safety is
5
5
  // unchanged: money-moving calls stay policy-gated server-side, and there is
6
6
  // deliberately NO approve/decide tool on the agent surface.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Client-side peek at an operator key's JWT payload (signature NOT verified —
3
3
  * identity hint only, the backend remains the authority). Used by the fleet
4
- * launcher to warn when the key is agent-bound (ZIG-911): a bound key
4
+ * launcher to warn when the key is agent-bound: a bound key
5
5
  * collapses the operator multiplex to its one agent and 403s every other
6
6
  * per-agent loop, so a large fleet quietly watches a single agent.
7
7
  */
@@ -14,7 +14,7 @@ export interface OperatorKeyClaims {
14
14
  }
15
15
  export declare function decodeOperatorKeyClaims(token: string): OperatorKeyClaims | null;
16
16
  /**
17
- * The ZIG-911 startup check: warn (never refuse — a one-agent launcher on a
17
+ * The startup check: warn (never refuse — a one-agent launcher on a
18
18
  * bound key is legitimate) when the fleet's control key is bound to a single
19
19
  * agent while more than one agent is registered. Returns the warning line, or
20
20
  * null when the setup is coherent.
@@ -18,7 +18,7 @@ export function decodeOperatorKeyClaims(token) {
18
18
  }
19
19
  }
20
20
  /**
21
- * The ZIG-911 startup check: warn (never refuse — a one-agent launcher on a
21
+ * The startup check: warn (never refuse — a one-agent launcher on a
22
22
  * bound key is legitimate) when the fleet's control key is bound to a single
23
23
  * agent while more than one agent is registered. Returns the warning line, or
24
24
  * null when the setup is coherent.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The protocol verbs and the state operation each one performs.
3
3
  *
4
- * This is the single place the set is written down (ZIG-1110). It used to be
4
+ * This is the single place the set is written down. It used to be
5
5
  * restated: the tool definitions passed their operation string to `handlerFor`
6
6
  * by hand, and this table listed them again. `agreement_fulfill` was in the
7
7
  * first list and not the second, so it dispatched fine but
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The protocol verbs and the state operation each one performs.
3
3
  *
4
- * This is the single place the set is written down (ZIG-1110). It used to be
4
+ * This is the single place the set is written down. It used to be
5
5
  * restated: the tool definitions passed their operation string to `handlerFor`
6
6
  * by hand, and this table listed them again. `agreement_fulfill` was in the
7
7
  * first list and not the second, so it dispatched fine but
@@ -1,5 +1,5 @@
1
1
  import { ToolProvider } from './ToolProvider.js';
2
- import type { ToolDefinition } from './defineTool.js';
2
+ import { type ToolDefinition } from './defineTool.js';
3
3
  export declare class ToolManager extends ToolProvider {
4
4
  private tools;
5
5
  constructor();
@@ -1,5 +1,6 @@
1
1
  import AjvModule from 'ajv';
2
2
  import { ToolProvider } from './ToolProvider.js';
3
+ import { toolName } from './defineTool.js';
3
4
  const AjvClass = AjvModule.default ?? AjvModule;
4
5
  const ajv = new AjvClass({ allErrors: true, strict: false });
5
6
  function makeError(message, type, extra) {
@@ -10,7 +11,7 @@ function makeError(message, type, extra) {
10
11
  return err;
11
12
  }
12
13
  /**
13
- * ZIG-1215 — Ajv's own wording for a rejected unknown param is "must NOT have
14
+ * Ajv's own wording for a rejected unknown param is "must NOT have
14
15
  * additional properties", which names neither the offending key nor the ones
15
16
  * that would have worked. An agent reading that has nothing to correct against,
16
17
  * so the typo is named alongside the menu it should have come from.
@@ -49,7 +50,7 @@ export class ToolManager extends ToolProvider {
49
50
  throw makeError('Tool must have a schema', 'invalid_tool');
50
51
  if (typeof tool.handler !== 'function')
51
52
  throw makeError('Tool must have a handler function', 'invalid_tool');
52
- const name = (tool.schema.function?.['name'] || tool.schema['name']);
53
+ const name = toolName(tool);
53
54
  if (!name)
54
55
  throw makeError('Tool schema must have a name', 'invalid_tool');
55
56
  this.tools.set(name, tool);
@@ -1,7 +1,7 @@
1
1
  import { type ToolDefinition } from './defineTool.js';
2
2
  import type { CapabilityDefinition } from '@ziggs-ai/api-client';
3
3
  /**
4
- * ZIG-956 — the SDK surface adapter for shared capability definitions
4
+ * the SDK surface adapter for shared capability definitions
5
5
  * (api-client `capabilities/`). One capability (schema + handler + guidance)
6
6
  * becomes a defineTool ToolDefinition: the neutral param DSL feeds defineTool's
7
7
  * JSON-schema converter unchanged, and the handler runs with creds pulled from
@@ -1,6 +1,6 @@
1
1
  import { defineTool } from './defineTool.js';
2
2
  /**
3
- * ZIG-956 — the SDK surface adapter for shared capability definitions
3
+ * the SDK surface adapter for shared capability definitions
4
4
  * (api-client `capabilities/`). One capability (schema + handler + guidance)
5
5
  * becomes a defineTool ToolDefinition: the neutral param DSL feeds defineTool's
6
6
  * JSON-schema converter unchanged, and the handler runs with creds pulled from
@@ -10,8 +10,8 @@ export function toolFromCapability(cap) {
10
10
  return defineTool(cap.names.sdk, cap.params, async (args, ctx) => {
11
11
  const operatorKey = ctx?.['operatorKey'];
12
12
  const agentId = ctx?.['agentId'];
13
- // ZIG-1092 — the lane was already threaded to this exact point by
14
- // ZIG-775's shared executor (`chatId: effect.sessionId` in
13
+ // the lane was already threaded to this exact point by
14
+ //'s shared executor (`chatId: effect.sessionId` in
15
15
  // executeToolWithSideEffects); it just was not used to narrow anything.
16
16
  // Reading it here makes the fence brain-agnostic by construction: the
17
17
  // Claude brain and the FSM brain both arrive through this adapter.
@@ -24,4 +24,11 @@ export interface DefineToolOptions {
24
24
  isAgreementCreation?: boolean;
25
25
  isGenericFallback?: boolean;
26
26
  }
27
+ /**
28
+ * The name a tool registers under. `defineTool` always writes
29
+ * `schema.function.name`, but hand-built and capability-adapted schemas have put
30
+ * it at the top level too, so both are read — in one place, rather than at every
31
+ * call site that needs to identify a tool.
32
+ */
33
+ export declare function toolName(tool: ToolDefinition | undefined): string | undefined;
27
34
  export declare function defineTool(name: string, params: Record<string, unknown>, handler: (args: Record<string, unknown>, context: Record<string, unknown>) => Promise<unknown>, options?: DefineToolOptions): ToolDefinition;
@@ -43,12 +43,24 @@ function convertObject(fields) {
43
43
  result['required'] = required;
44
44
  return result;
45
45
  }
46
+ /**
47
+ * The name a tool registers under. `defineTool` always writes
48
+ * `schema.function.name`, but hand-built and capability-adapted schemas have put
49
+ * it at the top level too, so both are read — in one place, rather than at every
50
+ * call site that needs to identify a tool.
51
+ */
52
+ export function toolName(tool) {
53
+ const fromFunction = tool?.schema?.function?.['name'];
54
+ const fromTop = tool?.schema?.['name'];
55
+ const name = fromFunction ?? fromTop;
56
+ return typeof name === 'string' && name ? name : undefined;
57
+ }
46
58
  export function defineTool(name, params, handler, options = {}) {
47
59
  if (!name || typeof name !== 'string')
48
60
  throw new Error('defineTool: name is required');
49
61
  if (!handler || typeof handler !== 'function')
50
62
  throw new Error('defineTool: handler function is required');
51
- // ZIG-1215 — the top level is closed so a misspelled param is rejected by
63
+ // the top level is closed so a misspelled param is rejected by
52
64
  // ToolManager's Ajv pass instead of being dropped on the floor. JSON Schema
53
65
  // allows unknown keys by default, so without this the validation ran and
54
66
  // passed while the handler quietly ignored what the model asked for.