@sema-agent/client-core 0.78.2 → 0.79.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.
- package/CHANGELOG.md +51 -0
- package/README.md +24 -19
- package/dist/adapt/arms.js +7 -0
- package/dist/adapt/textStream.d.ts +1 -0
- package/dist/adapt/textStream.js +10 -0
- package/dist/adapt/toolCards.d.ts +1 -0
- package/dist/adapt/toolCards.js +2 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +4 -0
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +9 -1
- package/dist/adapter/downstream/terminalToSdkResult.js +34 -7
- package/dist/adapter/runStream.js +60 -3
- package/dist/agentSession/contract.d.ts +2 -1
- package/dist/engineNoticeCodes.js +2 -0
- package/dist/gateVocabulary.d.ts +6 -0
- package/dist/gateVocabulary.js +21 -0
- package/dist/hitl/askGateWire.d.ts +3 -0
- package/dist/hitl/askGateWire.js +2 -0
- package/dist/hitl/frameRouter.d.ts +1 -0
- package/dist/hitl/frameRouter.js +39 -8
- package/dist/hitl/gateLedger.d.ts +9 -4
- package/dist/hitl/gateLedger.js +29 -5
- package/dist/hitl/parkResolver.js +6 -1
- package/dist/hitl/persistedRulesWire.d.ts +12 -1
- package/dist/hitl/persistedRulesWire.js +20 -5
- package/dist/hitl/toolApprovalWire.d.ts +15 -1
- package/dist/hitl/toolApprovalWire.js +56 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcpLiveness.js +3 -0
- package/dist/mcpProbeCapability.d.ts +18 -0
- package/dist/mcpProbeCapability.js +55 -0
- package/dist/mcpProbeWire.d.ts +51 -0
- package/dist/mcpProbeWire.js +217 -0
- package/dist/printToolResultFrame.d.ts +1 -0
- package/dist/printToolResultFrame.js +2 -0
- package/dist/request/taskRequest.d.ts +20 -1
- package/dist/request/taskRequest.js +198 -52
- package/dist/sdkRegistryTransit.d.ts +6 -0
- package/dist/sdkRegistryTransit.js +5 -0
- package/dist/sdkWireTransit.d.ts +2 -1
- package/dist/sdkWireTransit.js +1 -0
- package/dist/toolResult.d.ts +15 -1
- package/dist/toolResult.js +61 -4
- package/dist/wireFailureShape.d.ts +1 -0
- package/dist/wireFailureShape.js +44 -10
- package/docs/INTEGRATION-CLIENTS.md +209 -15
- package/package.json +8 -4
|
@@ -164,7 +164,12 @@ async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
|
|
|
164
164
|
const outcome = await surfaceFsApprovalAndDecide({ client: deps.client, ...(onPresented ? { onPresented } : {}) }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
|
|
165
165
|
const outcomeDenied = 'denied' in outcome ? outcome.denied : undefined;
|
|
166
166
|
if (outcome.kind === 'decided' && outcomeDenied === true && outcome.gatedCallId) {
|
|
167
|
-
|
|
167
|
+
const denySettledBy = 'denySettledBy' in outcome ? outcome.denySettledBy : undefined;
|
|
168
|
+
const denyReason = 'denyReason' in outcome ? outcome.denyReason : undefined;
|
|
169
|
+
led.markDenied(outcome.gatedCallId, {
|
|
170
|
+
...(denySettledBy !== undefined ? { settledBy: denySettledBy } : {}),
|
|
171
|
+
...(denyReason !== undefined ? { reason: denyReason } : {}),
|
|
172
|
+
});
|
|
168
173
|
}
|
|
169
174
|
return outcome;
|
|
170
175
|
}
|
|
@@ -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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -411,11 +447,20 @@ export function isToolApprovalFrame(ev) {
|
|
|
411
447
|
typeof e.approvalId === 'string' &&
|
|
412
448
|
e.approvalId.length > 0);
|
|
413
449
|
}
|
|
414
|
-
const
|
|
450
|
+
const FS_WRITE_GATE_ASK_PREFIX = 'approve write to "';
|
|
451
|
+
const FS_WRITE_GATE_ASK_PATH_CLOSE = '"?';
|
|
415
452
|
function pathFromGateMessage(message) {
|
|
416
453
|
if (typeof message !== 'string')
|
|
417
454
|
return undefined;
|
|
418
|
-
|
|
455
|
+
if (!message.startsWith(FS_WRITE_GATE_ASK_PREFIX))
|
|
456
|
+
return undefined;
|
|
457
|
+
const body = message.slice(FS_WRITE_GATE_ASK_PREFIX.length);
|
|
458
|
+
const close = body.indexOf(FS_WRITE_GATE_ASK_PATH_CLOSE);
|
|
459
|
+
if (close <= 0)
|
|
460
|
+
return undefined;
|
|
461
|
+
if (body.indexOf(FS_WRITE_GATE_ASK_PATH_CLOSE, close + 1) !== -1)
|
|
462
|
+
return undefined;
|
|
463
|
+
return body.slice(0, close);
|
|
419
464
|
}
|
|
420
465
|
function isNonNegativeFinite(v) {
|
|
421
466
|
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
@@ -657,6 +702,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
657
702
|
...(isWireRecordCarrier(frame.probeCause) ? { probeCause: frame.probeCause } : {}),
|
|
658
703
|
...(isWireRecordCarrier(frame.ruleEvidence) ? { ruleEvidence: frame.ruleEvidence } : {}),
|
|
659
704
|
...(frame.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
705
|
+
...(frame.mandated === true ? { mandated: true } : {}),
|
|
660
706
|
...(() => { const rc = readReadRootCandidate(frame.readRootCandidate); return rc !== undefined ? { readRootCandidate: rc } : {}; })(),
|
|
661
707
|
...(frame.inputHasBidi === true ? { inputHasBidi: true } : {}),
|
|
662
708
|
...(typeof frame.ruleOffersAbsence === 'string' && frame.ruleOffersAbsence !== ''
|
|
@@ -834,7 +880,13 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
834
880
|
hostLog('debug', `liveToolApprovalWire: ${frame.approvalId} decision note was sent but not persisted ` +
|
|
835
881
|
`(ack.noteRecorded=${String(ack.noteRecorded)}) — decision stood; the audit note did not land on the ask row`);
|
|
836
882
|
}
|
|
837
|
-
|
|
883
|
+
const denyAttribution = card.kind === 'deny'
|
|
884
|
+
? {
|
|
885
|
+
...((o) => (o !== undefined ? { denySettledBy: o } : {}))(readApprovalDenySettledBy(card)),
|
|
886
|
+
...(typeof card.reason === 'string' && card.reason.trim() !== '' ? { denyReason: card.reason } : {}),
|
|
887
|
+
}
|
|
888
|
+
: {};
|
|
889
|
+
return ack !== undefined ? { decision, ack, ...denyAttribution } : { decision, ...denyAttribution };
|
|
838
890
|
}
|
|
839
891
|
catch (e) {
|
|
840
892
|
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';
|
package/dist/mcpLiveness.js
CHANGED
|
@@ -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
|
+
}
|