@sema-agent/client-core 0.78.2 → 0.79.0

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 (45) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +22 -18
  3. package/dist/adapt/arms.js +7 -0
  4. package/dist/adapt/textStream.d.ts +1 -0
  5. package/dist/adapt/textStream.js +10 -0
  6. package/dist/adapt/toolCards.d.ts +1 -0
  7. package/dist/adapt/toolCards.js +2 -0
  8. package/dist/adapter/downstream/eventToSdkMessage.js +4 -0
  9. package/dist/adapter/downstream/terminalToSdkResult.d.ts +9 -1
  10. package/dist/adapter/downstream/terminalToSdkResult.js +29 -3
  11. package/dist/adapter/runStream.js +60 -3
  12. package/dist/agentSession/contract.d.ts +2 -1
  13. package/dist/engineNoticeCodes.js +2 -0
  14. package/dist/gateVocabulary.d.ts +6 -0
  15. package/dist/gateVocabulary.js +21 -0
  16. package/dist/hitl/askGateWire.d.ts +3 -0
  17. package/dist/hitl/askGateWire.js +2 -0
  18. package/dist/hitl/frameRouter.d.ts +1 -0
  19. package/dist/hitl/frameRouter.js +39 -8
  20. package/dist/hitl/gateLedger.d.ts +9 -4
  21. package/dist/hitl/gateLedger.js +29 -5
  22. package/dist/hitl/parkResolver.js +6 -1
  23. package/dist/hitl/persistedRulesWire.d.ts +12 -1
  24. package/dist/hitl/persistedRulesWire.js +20 -5
  25. package/dist/hitl/toolApprovalWire.d.ts +15 -1
  26. package/dist/hitl/toolApprovalWire.js +45 -2
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +2 -0
  29. package/dist/mcpLiveness.js +3 -0
  30. package/dist/mcpProbeCapability.d.ts +18 -0
  31. package/dist/mcpProbeCapability.js +55 -0
  32. package/dist/mcpProbeWire.d.ts +51 -0
  33. package/dist/mcpProbeWire.js +217 -0
  34. package/dist/printToolResultFrame.d.ts +1 -0
  35. package/dist/printToolResultFrame.js +2 -0
  36. package/dist/request/taskRequest.d.ts +20 -1
  37. package/dist/request/taskRequest.js +198 -52
  38. package/dist/sdkWireTransit.d.ts +2 -1
  39. package/dist/sdkWireTransit.js +1 -0
  40. package/dist/toolResult.d.ts +15 -1
  41. package/dist/toolResult.js +61 -4
  42. package/dist/wireFailureShape.d.ts +1 -0
  43. package/dist/wireFailureShape.js +44 -10
  44. package/docs/INTEGRATION-CLIENTS.md +148 -14
  45. package/package.json +4 -4
@@ -64,6 +64,12 @@ export type RulesFailure = {
64
64
  } | {
65
65
  kind: 'write-indeterminate';
66
66
  message: string;
67
+ } | {
68
+ kind: 'write-not-committed';
69
+ message: string;
70
+ } | {
71
+ kind: 'remove-indeterminate';
72
+ message: string;
67
73
  } | {
68
74
  kind: 'error';
69
75
  message: string;
@@ -93,7 +99,7 @@ export type PersistedRuleWriteBehavior = (typeof PERSISTED_RULE_WRITE_BEHAVIORS)
93
99
  export type PersistedRuleWriteRequest = RuleWriteRequest;
94
100
  export type PersistedRuleWriteWireResult = RuleWriteResult;
95
101
  export type PersistedRuleLiveness = RemovalLiveness;
96
- export declare const PERSISTED_RULE_WRITE_REFUSAL_CAUSES: readonly ["unwritable_behavior", "unreadable_identity", "unusable_principal", "client_too_old", "field_refused", "body_shape", "operator_only", "store_absent", "lane_too_old", "route_absent"];
102
+ export declare const PERSISTED_RULE_WRITE_REFUSAL_CAUSES: readonly ["unwritable_behavior", "unreadable_identity", "unusable_principal", "client_too_old", "field_refused", "body_shape", "operator_only", "store_absent", "lane_too_old", "route_absent", "not_committed"];
97
103
  export type PersistedRuleWriteRefusalCause = (typeof PERSISTED_RULE_WRITE_REFUSAL_CAUSES)[number];
98
104
  export declare const PERSISTED_RULE_WRITE_UNKNOWN_REASONS: readonly ["indeterminate", "malformed_result", "identity_mismatch", "unattributed_method_refusal", "unclassified"];
99
105
  export type PersistedRuleWriteUnknownReason = (typeof PERSISTED_RULE_WRITE_UNKNOWN_REASONS)[number];
@@ -110,6 +116,11 @@ export type PersistedRuleWriteOutcome = {
110
116
  wrote: 'persisted' | 'no_op';
111
117
  rev: number;
112
118
  message: string;
119
+ } | {
120
+ status: 'unverified';
121
+ wrote: 'persisted' | 'no_op';
122
+ rev: number;
123
+ message: string;
113
124
  } | {
114
125
  status: 'refused';
115
126
  cause: PersistedRuleWriteRefusalCause;
@@ -35,7 +35,7 @@ export function readCcImportRedeemCounts(result) {
35
35
  return null;
36
36
  }
37
37
  export function classifyRulesFailure(e) {
38
- const { status, errorCode, message, retryAfterSec } = wireFailureShapeOf(e);
38
+ const { status, errorCode, message, retryAfterSec, committed } = wireFailureShapeOf(e);
39
39
  if (errorCode === 'capability.rule_store_required') {
40
40
  return { kind: 'lane-unavailable', message };
41
41
  }
@@ -50,10 +50,12 @@ export function classifyRulesFailure(e) {
50
50
  const sec = retryAfterSec;
51
51
  return { kind: 'retry-same-ticket', message, ...(sec !== undefined ? { retryAfterSec: sec } : {}) };
52
52
  }
53
- if (errorCode === 'state.rule_remove_failed')
54
- return { kind: 'retryable', message };
55
- if (errorCode === 'state.rule_write_failed')
56
- return { kind: 'write-indeterminate', message };
53
+ if (errorCode === 'state.rule_remove_failed') {
54
+ return committed === 'unknown' || committed === 'unreadable' ? { kind: 'remove-indeterminate', message } : { kind: 'retryable', message };
55
+ }
56
+ if (errorCode === 'state.rule_write_failed') {
57
+ return committed === 'no' ? { kind: 'write-not-committed', message } : { kind: 'write-indeterminate', message };
58
+ }
57
59
  if (status === 400 && errorCode === 'request.query_invalid')
58
60
  return { kind: 'cursor-stale', message };
59
61
  if (status === 400 && errorCode === 'request.field_invalid')
@@ -222,6 +224,7 @@ export const PERSISTED_RULE_WRITE_REFUSAL_CAUSES = Object.freeze([
222
224
  'store_absent',
223
225
  'lane_too_old',
224
226
  'route_absent',
227
+ 'not_committed',
225
228
  ]);
226
229
  export const PERSISTED_RULE_WRITE_UNKNOWN_REASONS = Object.freeze([
227
230
  'indeterminate',
@@ -249,6 +252,8 @@ function writeOutcomeFromFailure(failure) {
249
252
  return { status: 'refused', cause: 'route_absent', message };
250
253
  case 'write-indeterminate':
251
254
  return { status: 'unknown', why: 'indeterminate', message };
255
+ case 'write-not-committed':
256
+ return { status: 'refused', cause: 'not_committed', message };
252
257
  default:
253
258
  return { status: 'unknown', why: 'unclassified', message };
254
259
  }
@@ -361,6 +366,16 @@ export async function writePersistedRule(facade, input, opts) {
361
366
  message: 'the rule write landed (the revision is real) but a concurrent revoke arrived before it could be read back — the requested tightening is not standing; do not retry (that would re-create what was just revoked): list the rules to see the current state',
362
367
  };
363
368
  }
369
+ if (liveness === 'unknown') {
370
+ if (hasRow)
371
+ return malformed();
372
+ return {
373
+ status: 'unverified',
374
+ wrote,
375
+ rev,
376
+ message: 'the rule write landed (the revision is real) but the engine could not observe the rule liveness when reading it back — the requested tightening is not verified as standing; do not blindly retry: list the rules to see the current state',
377
+ };
378
+ }
364
379
  if (liveness !== 'yes')
365
380
  return malformed();
366
381
  }
@@ -17,6 +17,8 @@ export type FsApprovalOutcome = {
17
17
  kind: 'decided';
18
18
  gatedCallId?: string | undefined;
19
19
  denied?: boolean | undefined;
20
+ denySettledBy?: ApprovalDenySettledBy | undefined;
21
+ denyReason?: string | undefined;
20
22
  receipt?: DecideReceiptView | undefined;
21
23
  } | {
22
24
  kind: 'aborted';
@@ -85,7 +87,11 @@ export interface ApprovalCardAllowDecision {
85
87
  export interface ApprovalCardDenyDecision {
86
88
  kind: 'deny';
87
89
  reason?: string;
90
+ settledBy?: ApprovalDenySettledBy;
88
91
  }
92
+ export declare const APPROVAL_DENY_SETTLED_BY_WORDS: readonly ["human", "policy"];
93
+ export type ApprovalDenySettledBy = (typeof APPROVAL_DENY_SETTLED_BY_WORDS)[number];
94
+ export declare function readApprovalDenySettledBy(decision: unknown): ApprovalDenySettledBy | undefined;
89
95
  export type ApprovalCardDecision = ApprovalCardAllowDecision | ApprovalCardDenyDecision | {
90
96
  kind: 'aborted';
91
97
  } | {
@@ -124,6 +130,7 @@ export interface ApprovalCardRequest {
124
130
  serverNowMs?: number;
125
131
  receivedAtMs?: number;
126
132
  ruleOffersAbsence?: string;
133
+ mandated?: true;
127
134
  denialLimitFallback?: DenialLimitFallback;
128
135
  origin?: string;
129
136
  ruleStoreUnreadable?: RuleStoreUnreadableKind;
@@ -162,6 +169,7 @@ export interface ToolApprovalFrame {
162
169
  probeCause?: unknown;
163
170
  ruleEvidence?: unknown;
164
171
  requiresRealApproval?: true;
172
+ mandated?: true;
165
173
  origin?: string;
166
174
  inputHasBidi?: true;
167
175
  parked?: true;
@@ -180,7 +188,11 @@ export interface ToolApprovalDelegation {
180
188
  depth: number;
181
189
  agentName?: string;
182
190
  }
183
- export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "ruleOffersAbsence", "denialLimitFallback", "ruleStoreUnreadable", "readRootCandidate", "origin", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
191
+ export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "ruleOffersAbsence", "denialLimitFallback", "ruleStoreUnreadable", "readRootCandidate", "mandated", "origin", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
192
+ export declare function approvalIsMandated(v: {
193
+ mandated?: true;
194
+ ruleOffersAbsence?: string;
195
+ } | undefined | null): boolean;
184
196
  export declare function isFromSubagent(frame: ToolApprovalFrame): boolean;
185
197
  export type ToolApprovalRespondDecision = 'allow' | 'allow_session' | 'deny';
186
198
  export interface RespondToolApprovalOpts {
@@ -201,6 +213,8 @@ export interface ToolApprovalFrameOutcome {
201
213
  respondRefusal?: ToolApprovalRespondRefusal;
202
214
  editRefused?: true;
203
215
  retracted?: true;
216
+ denySettledBy?: ApprovalDenySettledBy;
217
+ denyReason?: string;
204
218
  }
205
219
  interface ToolApprovalRespondRefusalFields {
206
220
  status?: number;
@@ -49,6 +49,24 @@ export function structurallyEqual(a, b) {
49
49
  }
50
50
  export { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS };
51
51
  export { RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS };
52
+ export const APPROVAL_DENY_SETTLED_BY_WORDS = Object.freeze(['human', 'policy']);
53
+ export function readApprovalDenySettledBy(decision) {
54
+ if (decision === null || typeof decision !== 'object')
55
+ return undefined;
56
+ let d;
57
+ try {
58
+ d = Object.getOwnPropertyDescriptor(decision, 'settledBy');
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ if (d === undefined || !('value' in d))
64
+ return undefined;
65
+ const v = d.value;
66
+ return typeof v === 'string' && APPROVAL_DENY_SETTLED_BY_WORDS.includes(v)
67
+ ? v
68
+ : undefined;
69
+ }
52
70
  export const RETRACTED_CARD_DECISION_KIND = 'retracted';
53
71
  const cardPortByKey = createSessionSlot();
54
72
  const cardPortMissesByKey = new Map();
@@ -191,7 +209,16 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
191
209
  try {
192
210
  const raw = await bridge.decideTool({ decision: 'deny', reason: denyReasonForWire(card.reason, `task ${taskId}`) ?? DEFAULT_DENY_REASON }, gatedCallId, signal ? { signal } : undefined, pending);
193
211
  const receipt = readDecideReceipt(raw);
194
- return { kind: 'decided', gatedCallId, denied: true, ...(receipt !== undefined ? { receipt } : {}) };
212
+ const denySettledBy = readApprovalDenySettledBy(card);
213
+ const denyReason = typeof card.reason === 'string' && card.reason.trim() !== '' ? card.reason : undefined;
214
+ return {
215
+ kind: 'decided',
216
+ gatedCallId,
217
+ denied: true,
218
+ ...(denySettledBy !== undefined ? { denySettledBy } : {}),
219
+ ...(denyReason !== undefined ? { denyReason } : {}),
220
+ ...(receipt !== undefined ? { receipt } : {}),
221
+ };
195
222
  }
196
223
  catch (e) {
197
224
  const currentPending = readDecideCurrentPending(e);
@@ -238,6 +265,7 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
238
265
  'denialLimitFallback',
239
266
  'ruleStoreUnreadable',
240
267
  'readRootCandidate',
268
+ 'mandated',
241
269
  'origin',
242
270
  'inputHasBidi',
243
271
  'parked',
@@ -253,6 +281,14 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
253
281
  ];
254
282
  const _mirrorPin = [true, true];
255
283
  void _mirrorPin;
284
+ const MANDATED_ABSENCE_WORD = 'mandated';
285
+ export function approvalIsMandated(v) {
286
+ if (v === null || v === undefined)
287
+ return false;
288
+ if (v.mandated === true)
289
+ return true;
290
+ return v.ruleOffersAbsence === MANDATED_ABSENCE_WORD;
291
+ }
256
292
  function isToolApprovalDelegation(v) {
257
293
  if (v === null || typeof v !== 'object')
258
294
  return false;
@@ -657,6 +693,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
657
693
  ...(isWireRecordCarrier(frame.probeCause) ? { probeCause: frame.probeCause } : {}),
658
694
  ...(isWireRecordCarrier(frame.ruleEvidence) ? { ruleEvidence: frame.ruleEvidence } : {}),
659
695
  ...(frame.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
696
+ ...(frame.mandated === true ? { mandated: true } : {}),
660
697
  ...(() => { const rc = readReadRootCandidate(frame.readRootCandidate); return rc !== undefined ? { readRootCandidate: rc } : {}; })(),
661
698
  ...(frame.inputHasBidi === true ? { inputHasBidi: true } : {}),
662
699
  ...(typeof frame.ruleOffersAbsence === 'string' && frame.ruleOffersAbsence !== ''
@@ -834,7 +871,13 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
834
871
  hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} decision note was sent but not persisted ` +
835
872
  `(ack.noteRecorded=${String(ack.noteRecorded)}) — decision stood; the audit note did not land on the ask row`);
836
873
  }
837
- return ack !== undefined ? { decision, ack } : { decision };
874
+ const denyAttribution = card.kind === 'deny'
875
+ ? {
876
+ ...((o) => (o !== undefined ? { denySettledBy: o } : {}))(readApprovalDenySettledBy(card)),
877
+ ...(typeof card.reason === 'string' && card.reason.trim() !== '' ? { denyReason: card.reason } : {}),
878
+ }
879
+ : {};
880
+ return ack !== undefined ? { decision, ack, ...denyAttribution } : { decision, ...denyAttribution };
838
881
  }
839
882
  catch (e) {
840
883
  const respondRefusal = readToolApprovalRespondRefusal(e);
package/dist/index.d.ts CHANGED
@@ -40,6 +40,8 @@ export * from './argvFlagValue.js';
40
40
  export * from './readFacePosture.js';
41
41
  export * from './mcpPanel.js';
42
42
  export * from './mcpLiveness.js';
43
+ export * from './mcpProbeCapability.js';
44
+ export * from './mcpProbeWire.js';
43
45
  export * from './effectiveFacts.js';
44
46
  export * from './gateOutcome.js';
45
47
  export * from './postureKnob.js';
package/dist/index.js CHANGED
@@ -40,6 +40,8 @@ export * from './argvFlagValue.js';
40
40
  export * from './readFacePosture.js';
41
41
  export * from './mcpPanel.js';
42
42
  export * from './mcpLiveness.js';
43
+ export * from './mcpProbeCapability.js';
44
+ export * from './mcpProbeWire.js';
43
45
  export * from './effectiveFacts.js';
44
46
  export * from './gateOutcome.js';
45
47
  export * from './postureKnob.js';
@@ -1,4 +1,7 @@
1
1
  export const MCP_LIVENESS_STATES = Object.freeze(['reachable', 'unreachable', 'unknown']);
2
+ void MCP_LIVENESS_STATES;
3
+ void true;
4
+ void true;
2
5
  const isLivenessState = (v) => typeof v === 'string' && MCP_LIVENESS_STATES.includes(v);
3
6
  const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
4
7
  export function readMcpLiveness(row) {
@@ -0,0 +1,18 @@
1
+ export type McpProbeReading = {
2
+ kind: 'unobserved';
3
+ } | {
4
+ kind: 'not_reported';
5
+ } | {
6
+ kind: 'absent';
7
+ } | {
8
+ kind: 'present';
9
+ };
10
+ export declare function projectMcpProbeCapability(caps: unknown): McpProbeReading | undefined;
11
+ export declare function noteEngineCapsForMcpProbe(baseUrl: string, caps: unknown, opts?: {
12
+ generation?: number;
13
+ }): void;
14
+ export declare function observedMcpProbe(baseUrl?: string | undefined): McpProbeReading;
15
+ export declare function mcpProbeAvailable(reading: McpProbeReading): 'yes' | 'no' | 'unknown';
16
+ export declare function mcpProbeDoctorDetail(reading: McpProbeReading): string;
17
+ export declare function forgetMcpProbeReading(baseUrl: string | undefined): void;
18
+ export declare function __resetMcpProbeReadingsForTests(): void;
@@ -0,0 +1,55 @@
1
+ import { engineWireTarget } from './engineWireTarget.js';
2
+ import { createEngineCapReader } from './engineCapReader.js';
3
+ export function projectMcpProbeCapability(caps) {
4
+ if (caps === null || typeof caps !== 'object' || Array.isArray(caps))
5
+ return undefined;
6
+ if (!Object.hasOwn(caps, 'mcpProbe'))
7
+ return { kind: 'not_reported' };
8
+ const v = caps.mcpProbe;
9
+ if (v === undefined)
10
+ return { kind: 'not_reported' };
11
+ if (typeof v !== 'boolean')
12
+ return undefined;
13
+ return v ? { kind: 'present' } : { kind: 'absent' };
14
+ }
15
+ const reader = createEngineCapReader({
16
+ name: 'mcpProbe',
17
+ project: projectMcpProbeCapability,
18
+ makeUnobserved: () => ({ kind: 'unobserved' }),
19
+ });
20
+ export function noteEngineCapsForMcpProbe(baseUrl, caps, opts) {
21
+ reader.note(baseUrl, caps, opts);
22
+ }
23
+ export function observedMcpProbe(baseUrl = engineWireTarget()?.baseUrl) {
24
+ return reader.observed(baseUrl);
25
+ }
26
+ export function mcpProbeAvailable(reading) {
27
+ if (reading.kind === 'present')
28
+ return 'yes';
29
+ if (reading.kind === 'absent')
30
+ return 'no';
31
+ return 'unknown';
32
+ }
33
+ export function mcpProbeDoctorDetail(reading) {
34
+ switch (reading.kind) {
35
+ case 'unobserved':
36
+ return ('per-server MCP status face not observed — the engine reports whether it has one on /v1/capabilities; ' +
37
+ 'this process has no usable capabilities reading cached for it (none received, or the last one was unreadable)');
38
+ case 'not_reported':
39
+ return ('per-server MCP status face not reported by this engine — this binary predates the face, so that path is not expected to exist here ' +
40
+ '(asking it usually answers with a machine-readable route-not-found, though a timeout or an uncoded gateway answer still says nothing either way); ' +
41
+ 'prefer the session panel last-leg roster where that panel carries one, and otherwise show this reason rather than an entry that cannot answer');
42
+ case 'absent':
43
+ return ('per-server MCP status face reported off on this deployment — this engine answers no readings there; ' +
44
+ 'this client does not guess why, and whether caller-supplied declarations are honored is a separate position with its own answer');
45
+ case 'present':
46
+ return ('per-server MCP status face on — this engine dials its own declared MCP servers on request, with no run, ' +
47
+ 'and a probe of caller-supplied declarations answers with the very same body shape');
48
+ }
49
+ }
50
+ export function forgetMcpProbeReading(baseUrl) {
51
+ reader.forget(baseUrl);
52
+ }
53
+ export function __resetMcpProbeReadingsForTests() {
54
+ reader.__resetForTests();
55
+ }
@@ -0,0 +1,51 @@
1
+ import type { McpProbeFace, McpServerSpec } from '@sema-agent/sdk';
2
+ import { type WiringManifestMcpEntryView } from './adapter/downstream/eventToSdkMessage.js';
3
+ import { type McpProbeReading } from './mcpProbeCapability.js';
4
+ export interface McpProbeFacade {
5
+ mcpCapabilities(opts?: {
6
+ signal?: AbortSignal;
7
+ }): Promise<McpProbeFace>;
8
+ probeMcp(mcpServers: McpServerSpec[], opts?: {
9
+ signal?: AbortSignal;
10
+ }): Promise<McpProbeFace>;
11
+ }
12
+ export declare const MCP_PROBE_REFUSAL_CAUSES: readonly ["face_absent", "client_too_old", "unreadable_request", "injection_not_honored", "body_shape", "field_invalid", "body_unparseable", "payload_too_large", "rate_limited", "unauthorized", "principal_required", "principal_unverified"];
13
+ export type McpProbeRefusalCause = (typeof MCP_PROBE_REFUSAL_CAUSES)[number];
14
+ export declare const MCP_PROBE_UNKNOWN_WHYS: readonly ["no_verdict", "probe_incomplete", "server_fault", "result_unreadable", "index_misaligned", "unclassified"];
15
+ export type McpProbeUnknownWhy = (typeof MCP_PROBE_UNKNOWN_WHYS)[number];
16
+ export type McpProbeRefused = {
17
+ status: 'refused';
18
+ cause: McpProbeRefusalCause;
19
+ message: string;
20
+ retryAfterSec?: number;
21
+ };
22
+ export type McpProbeUnknown = {
23
+ status: 'unknown';
24
+ why: McpProbeUnknownWhy;
25
+ message: string;
26
+ retryAfterSec?: number;
27
+ };
28
+ export type McpProbeFailure = McpProbeRefused | McpProbeUnknown;
29
+ export interface McpProbeFaceView {
30
+ probedAt: string;
31
+ ttlSec: number;
32
+ servers: WiringManifestMcpEntryView[];
33
+ }
34
+ export type McpProbeOutcome = {
35
+ status: 'face';
36
+ face: McpProbeFaceView;
37
+ } | McpProbeRefused | McpProbeUnknown;
38
+ export declare function classifyMcpProbeFailure(e: unknown): McpProbeFailure;
39
+ export interface McpCapabilitiesCall {
40
+ capability: McpProbeReading;
41
+ }
42
+ export declare function readMcpCapabilities(facade: McpProbeFacade, call: McpCapabilitiesCall, opts?: {
43
+ signal?: AbortSignal;
44
+ }): Promise<McpProbeOutcome>;
45
+ export interface McpProbeServersCall {
46
+ specs: McpServerSpec[];
47
+ capability: McpProbeReading;
48
+ }
49
+ export declare function probeMcpServers(facade: McpProbeFacade, call: McpProbeServersCall, opts?: {
50
+ signal?: AbortSignal;
51
+ }): Promise<McpProbeOutcome>;
@@ -0,0 +1,217 @@
1
+ import { projectMcpSection } from './adapter/downstream/eventToSdkMessage.js';
2
+ import { wireFailureShapeOf } from './wireFailureShape.js';
3
+ import { mcpProbeAvailable } from './mcpProbeCapability.js';
4
+ export const MCP_PROBE_REFUSAL_CAUSES = Object.freeze([
5
+ 'face_absent',
6
+ 'client_too_old',
7
+ 'unreadable_request',
8
+ 'injection_not_honored',
9
+ 'body_shape',
10
+ 'field_invalid',
11
+ 'body_unparseable',
12
+ 'payload_too_large',
13
+ 'rate_limited',
14
+ 'unauthorized',
15
+ 'principal_required',
16
+ 'principal_unverified',
17
+ ]);
18
+ export const MCP_PROBE_UNKNOWN_WHYS = Object.freeze([
19
+ 'no_verdict',
20
+ 'probe_incomplete',
21
+ 'server_fault',
22
+ 'result_unreadable',
23
+ 'index_misaligned',
24
+ 'unclassified',
25
+ ]);
26
+ const _mcpProbeFaceViewPin = true;
27
+ void _mcpProbeFaceViewPin;
28
+ function refusedBy(cause, message, retryAfterSec) {
29
+ return { status: 'refused', cause, message, ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) };
30
+ }
31
+ function unknownBy(why, message, retryAfterSec) {
32
+ return { status: 'unknown', why, message, ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) };
33
+ }
34
+ export function classifyMcpProbeFailure(e) {
35
+ const { status, errorCode, message, retryAfterSec } = wireFailureShapeOf(e);
36
+ if (errorCode === undefined || errorCode === '')
37
+ return unknownBy('no_verdict', message, retryAfterSec);
38
+ switch (errorCode) {
39
+ case 'not_found.route':
40
+ return refusedBy('face_absent', message);
41
+ case 'capability.mcp_injection_required':
42
+ return refusedBy('injection_not_honored', message);
43
+ case 'request.body_shape':
44
+ return refusedBy('body_shape', message);
45
+ case 'request.field_invalid':
46
+ return refusedBy('field_invalid', message);
47
+ case 'request.rejected':
48
+ return refusedBy('body_unparseable', message);
49
+ case 'request.payload_too_large':
50
+ return refusedBy('payload_too_large', message);
51
+ case 'limit.rate_exceeded':
52
+ return refusedBy('rate_limited', message, retryAfterSec);
53
+ case 'auth.unauthorized':
54
+ return refusedBy('unauthorized', message);
55
+ case 'auth.principal_required':
56
+ return refusedBy('principal_required', message);
57
+ case 'principal_unverified':
58
+ return refusedBy('principal_unverified', message);
59
+ case 'state.mcp_probe_incomplete':
60
+ return unknownBy('probe_incomplete', message, retryAfterSec);
61
+ default:
62
+ break;
63
+ }
64
+ if (errorCode.startsWith('auth.'))
65
+ return refusedBy('unauthorized', message);
66
+ if (status === undefined)
67
+ return unknownBy('no_verdict', message, retryAfterSec);
68
+ if (status >= 500)
69
+ return unknownBy('server_fault', message, retryAfterSec);
70
+ return unknownBy('unclassified', message, retryAfterSec);
71
+ }
72
+ function plainObject(v) {
73
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
74
+ }
75
+ function nonEmptyString(v) {
76
+ return typeof v === 'string' && v !== '';
77
+ }
78
+ const MAX_ROWS = 10_000;
79
+ function snapshotArray(v) {
80
+ try {
81
+ if (!Array.isArray(v))
82
+ return null;
83
+ const n = v.length;
84
+ if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < 0 || n > MAX_ROWS)
85
+ return null;
86
+ const out = [];
87
+ for (let i = 0; i < n; i++)
88
+ out.push(v[i]);
89
+ return out;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ function snapshotSpecs(v) {
96
+ let ownSerializer;
97
+ try {
98
+ ownSerializer = v?.toJSON;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ if (ownSerializer !== undefined)
104
+ return null;
105
+ const rows = snapshotArray(v);
106
+ return rows === null ? null : rows;
107
+ }
108
+ function narrowProbeFace(body) {
109
+ let probedAt;
110
+ let ttlSec;
111
+ let servers;
112
+ try {
113
+ if (!plainObject(body))
114
+ return { ok: false, why: 'result_unreadable' };
115
+ probedAt = body.probedAt;
116
+ ttlSec = body.ttlSec;
117
+ servers = body.servers;
118
+ }
119
+ catch {
120
+ return { ok: false, why: 'result_unreadable' };
121
+ }
122
+ if (!nonEmptyString(probedAt))
123
+ return { ok: false, why: 'result_unreadable' };
124
+ if (typeof ttlSec !== 'number' || !Number.isFinite(ttlSec) || ttlSec < 0)
125
+ return { ok: false, why: 'result_unreadable' };
126
+ const raw = snapshotArray(servers);
127
+ if (raw === null)
128
+ return { ok: false, why: 'result_unreadable' };
129
+ let rows;
130
+ try {
131
+ rows = projectMcpSection(raw);
132
+ }
133
+ catch {
134
+ return { ok: false, why: 'result_unreadable' };
135
+ }
136
+ if (rows === undefined)
137
+ return { ok: false, why: 'result_unreadable' };
138
+ if (rows.length !== raw.length)
139
+ return { ok: false, why: 'result_unreadable' };
140
+ return { ok: true, face: { probedAt, ttlSec, servers: rows } };
141
+ }
142
+ const FACE_ABSENT_MESSAGE = 'this engine reports no per-server MCP status face on this deployment, so nothing was sent; ' +
143
+ 'read the last leg roster from the session panel, or show the real cause, rather than an entry that cannot answer';
144
+ function readCapabilityKind(capability) {
145
+ const kind = plainObject(capability) ? capability.kind : undefined;
146
+ if (!nonEmptyString(kind)) {
147
+ throw new TypeError('mcpProbeWire: `capability` (this call reading of the per-server MCP status face position) is required — ' +
148
+ 'a default reading anchor would silently answer for whichever engine happens to be installed, ' +
149
+ 'and this step decides whether a request goes on the wire at all');
150
+ }
151
+ return kind;
152
+ }
153
+ function faceGate(capability) {
154
+ const kind = readCapabilityKind(capability);
155
+ if (mcpProbeAvailable({ kind }) !== 'no')
156
+ return undefined;
157
+ return refusedBy('face_absent', FACE_ABSENT_MESSAGE);
158
+ }
159
+ export async function readMcpCapabilities(facade, call, opts) {
160
+ if (typeof facade?.mcpCapabilities !== 'function') {
161
+ return refusedBy('client_too_old', 'this client has no per-server MCP status read — nothing was sent; upgrade the client that builds this port');
162
+ }
163
+ const gate = faceGate(call?.capability);
164
+ if (gate !== undefined)
165
+ return gate;
166
+ let res;
167
+ try {
168
+ res = await facade.mcpCapabilities(opts);
169
+ }
170
+ catch (e) {
171
+ return classifyMcpProbeFailure(e);
172
+ }
173
+ const narrowed = narrowProbeFace(res);
174
+ if (!narrowed.ok) {
175
+ return unknownBy(narrowed.why, 'the per-server MCP status call answered 2xx with a body this client cannot stand behind ' +
176
+ '(the reading time, the reuse window, or the per-server rows) — refusing to present it as a reading, ' +
177
+ 'because a normal-looking empty list is exactly the false answer this face exists to remove');
178
+ }
179
+ return { status: 'face', face: narrowed.face };
180
+ }
181
+ export async function probeMcpServers(facade, call, opts) {
182
+ if (typeof facade?.probeMcp !== 'function') {
183
+ return refusedBy('client_too_old', 'this client has no MCP declaration probe — nothing was sent, so nothing was dialed; upgrade the client that builds this port');
184
+ }
185
+ const gate = faceGate(call?.capability);
186
+ if (gate !== undefined)
187
+ return gate;
188
+ let submitted;
189
+ try {
190
+ submitted = snapshotSpecs(call?.specs);
191
+ }
192
+ catch {
193
+ submitted = null;
194
+ }
195
+ if (submitted === null) {
196
+ return refusedBy('unreadable_request', 'a declaration probe needs a plain declaration list this client can count — the rows come back aligned by index with it, ' +
197
+ 'so a list of unknown length, or one that serializes itself (its own toJSON can put a different set of servers on the wire ' +
198
+ 'than the one that was counted), cannot be paired with anything; nothing was sent');
199
+ }
200
+ let res;
201
+ try {
202
+ res = await facade.probeMcp(submitted, opts);
203
+ }
204
+ catch (e) {
205
+ return classifyMcpProbeFailure(e);
206
+ }
207
+ const narrowed = narrowProbeFace(res);
208
+ if (!narrowed.ok) {
209
+ return unknownBy(narrowed.why, 'the declaration probe answered 2xx with a body this client cannot stand behind ' +
210
+ '(the reading time, the reuse window, or the per-server rows) — refusing to present it as a reading');
211
+ }
212
+ if (narrowed.face.servers.length !== submitted.length) {
213
+ return unknownBy('index_misaligned', `the declaration probe answered with ${narrowed.face.servers.length} row(s) for ${submitted.length} declaration(s) — ` +
214
+ 'the rows are paired with the submitted list by index, so refusing to pair any of them');
215
+ }
216
+ return { status: 'face', face: narrowed.face };
217
+ }
@@ -12,6 +12,7 @@ export interface ToolEndResultArmLike {
12
12
  gate?: unknown;
13
13
  delivered?: unknown;
14
14
  gatedCallId?: unknown;
15
+ _sema_denial_kind?: unknown;
15
16
  parentToolCallId?: unknown;
16
17
  uuid?: unknown;
17
18
  session_id?: unknown;
@@ -1,3 +1,4 @@
1
+ import { isCcToolDenialKind } from './gateVocabulary.js';
1
2
  export { flattenWireOutput as flattenToolOutput } from './adapt/wireShapes.js';
2
3
  import { flattenWireOutput } from './adapt/wireShapes.js';
3
4
  function bashExitCodeFailed(toolName, text, structured) {
@@ -30,6 +31,7 @@ export function toolEndResultToUserFrame(arm) {
30
31
  ],
31
32
  },
32
33
  parent_tool_use_id: typeof arm.parentToolCallId === 'string' ? arm.parentToolCallId : null,
34
+ ...(isCcToolDenialKind(arm._sema_denial_kind) ? { _sema_denial_kind: arm._sema_denial_kind } : {}),
33
35
  uuid: arm.uuid,
34
36
  session_id: arm.session_id,
35
37
  };