@sema-agent/client-core 0.79.1 → 0.80.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.
- package/CHANGELOG.md +23 -0
- package/README.md +11 -9
- package/dist/adapter/downstream/eventToSdkMessage.js +2 -4
- package/dist/adapter/downstream/terminalToSdkResult.js +1 -1
- package/dist/adapter/runStream.js +25 -8
- package/dist/gateOutcome.d.ts +4 -0
- package/dist/gateOutcome.js +36 -0
- package/dist/gateVocabulary.d.ts +1 -0
- package/dist/gateVocabulary.js +7 -0
- package/dist/hitl/frameRouter.js +2 -5
- package/dist/hitl/hitlBridge.d.ts +6 -4
- package/dist/hitl/hitlBridge.js +1 -0
- package/dist/hitl/toolApprovalWire.d.ts +6 -0
- package/dist/hitl/toolApprovalWire.js +105 -36
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcpEngineLeg.d.ts +23 -0
- package/dist/mcpEngineLeg.js +135 -0
- package/dist/mcpLiveness.d.ts +1 -0
- package/dist/mcpLiveness.js +2 -1
- package/dist/ruleRemovalConsequence.d.ts +3 -0
- package/dist/ruleRemovalConsequence.js +15 -0
- package/dist/wireRefusalCopy.d.ts +9 -0
- package/dist/wireRefusalCopy.js +68 -0
- package/docs/INTEGRATION-CLIENTS.md +101 -8
- package/package.json +4 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike } from './hitlBridge.js';
|
|
2
2
|
import { type RuleStoreUnreadableKind } from '../gateVocabulary.js';
|
|
3
|
+
import { type DenyAttributionRefusalDetail } from '../wireRefusalCopy.js';
|
|
3
4
|
import { type DecideReceiptView } from '../decideReceipt.js';
|
|
4
5
|
import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS } from '@sema-agent/sdk';
|
|
5
6
|
import type { RuleSuggestion, ToolApprovalRespondAck, PersistedRuleAnchor as SdkPersistedRuleAnchor, RuleOfferMatch as SdkRuleOfferMatch, RuleOfferBatchMember as SdkRuleOfferBatchMember, RuleOfferUncoveredDetail as SdkRuleOfferUncoveredDetail, RuleOfferUncoveredReason as SdkRuleOfferUncoveredReason, AskOrigin as SdkAskOrigin, RuleOffersAbsence as SdkRuleOffersAbsence, DenialLimitKind as SdkDenialLimitKind, DenialLimitFallback as SdkDenialLimitFallback } from '@sema-agent/sdk';
|
|
@@ -19,6 +20,7 @@ export type FsApprovalOutcome = {
|
|
|
19
20
|
denied?: boolean | undefined;
|
|
20
21
|
denySettledBy?: ApprovalDenySettledBy | undefined;
|
|
21
22
|
denyReason?: string | undefined;
|
|
23
|
+
denyAttributionDropped?: true;
|
|
22
24
|
receipt?: DecideReceiptView | undefined;
|
|
23
25
|
} | {
|
|
24
26
|
kind: 'aborted';
|
|
@@ -36,6 +38,7 @@ export type FsApprovalOutcome = {
|
|
|
36
38
|
safetyCode?: HitlSafetyCode | undefined;
|
|
37
39
|
retryExhausted?: true;
|
|
38
40
|
currentPending?: GateCurrentPending | undefined;
|
|
41
|
+
denyAttributionRefusal?: DenyAttributionRefusalDetail | undefined;
|
|
39
42
|
};
|
|
40
43
|
export interface FsApprovalWireDeps {
|
|
41
44
|
client: HitlClientLike;
|
|
@@ -202,6 +205,7 @@ export interface RespondToolApprovalOpts {
|
|
|
202
205
|
persistRuleEdited?: true;
|
|
203
206
|
persistRuleBatchOfferIndex?: number;
|
|
204
207
|
note?: string;
|
|
208
|
+
settledBy?: 'policy';
|
|
205
209
|
}
|
|
206
210
|
export type RespondToolApprovalFn = (approvalId: string, decision: ToolApprovalRespondDecision, opts?: RespondToolApprovalOpts) => Promise<ToolApprovalRespondAck | void>;
|
|
207
211
|
export type ToolApprovalRespondAckView = ToolApprovalRespondAck;
|
|
@@ -215,6 +219,8 @@ export interface ToolApprovalFrameOutcome {
|
|
|
215
219
|
retracted?: true;
|
|
216
220
|
denySettledBy?: ApprovalDenySettledBy;
|
|
217
221
|
denyReason?: string;
|
|
222
|
+
denyAttributionRefusal?: DenyAttributionRefusalDetail;
|
|
223
|
+
denyAttributionDropped?: true;
|
|
218
224
|
}
|
|
219
225
|
interface ToolApprovalRespondRefusalFields {
|
|
220
226
|
status?: number;
|
|
@@ -6,6 +6,7 @@ import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
|
|
|
6
6
|
import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded, surfaceEditRefusedOnBlindAsk, surfaceRuleArmNotSent, surfaceRuleArmRejected } from './hitlHostSurface.js';
|
|
7
7
|
import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
|
|
8
8
|
import { isRuleStoreUnreadableKind } from '../gateVocabulary.js';
|
|
9
|
+
import { denyAttributionRefusalFromError } from '../wireRefusalCopy.js';
|
|
9
10
|
import { readDecideReceipt } from '../decideReceipt.js';
|
|
10
11
|
import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS, } from '@sema-agent/sdk';
|
|
11
12
|
export function isFsApprovalGate(gate) {
|
|
@@ -205,18 +206,44 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
205
206
|
...(safetyCode !== undefined ? { safetyCode } : {}),
|
|
206
207
|
};
|
|
207
208
|
}
|
|
208
|
-
case 'deny':
|
|
209
|
+
case 'deny': {
|
|
210
|
+
const denySettledByForWire = readApprovalDenySettledBy(card);
|
|
211
|
+
const denyReasonSnapshot = typeof card.reason === 'string' ? card.reason : undefined;
|
|
212
|
+
let lastSendCarriedAttribution = false;
|
|
209
213
|
try {
|
|
210
|
-
const
|
|
214
|
+
const denyBody = {
|
|
215
|
+
decision: 'deny',
|
|
216
|
+
reason: denyReasonForWire(denyReasonSnapshot, `task ${taskId}`) ?? DEFAULT_DENY_REASON,
|
|
217
|
+
...(denySettledByForWire === 'policy' ? { settledBy: 'policy' } : {}),
|
|
218
|
+
};
|
|
219
|
+
let attributionDropped = false;
|
|
220
|
+
let raw;
|
|
221
|
+
lastSendCarriedAttribution = denySettledByForWire === 'policy';
|
|
222
|
+
try {
|
|
223
|
+
raw = await bridge.decideTool(denyBody, gatedCallId, signal ? { signal } : undefined, pending);
|
|
224
|
+
}
|
|
225
|
+
catch (first) {
|
|
226
|
+
if (!shouldResendWithoutAttribution(first, lastSendCarriedAttribution, signal))
|
|
227
|
+
throw first;
|
|
228
|
+
const { settledBy: _droppedKey, ...withoutAttribution } = denyBody;
|
|
229
|
+
void _droppedKey;
|
|
230
|
+
lastSendCarriedAttribution = false;
|
|
231
|
+
hostLog('debug', `liveToolApprovalWire: decide(deny) for task ${taskId} was refused because the settlement note is not part of ` +
|
|
232
|
+
'what this deployment signs — re-sending the SAME decision exactly once with that one key removed ' +
|
|
233
|
+
'(the refusal happened before the approval was judged, so nothing was consumed and this is a re-delivery, not a second act)');
|
|
234
|
+
raw = await bridge.decideTool(withoutAttribution, gatedCallId, signal ? { signal } : undefined, pending);
|
|
235
|
+
attributionDropped = true;
|
|
236
|
+
}
|
|
211
237
|
const receipt = readDecideReceipt(raw);
|
|
212
|
-
const denySettledBy =
|
|
213
|
-
const denyReason =
|
|
238
|
+
const denySettledBy = denySettledByForWire;
|
|
239
|
+
const denyReason = denyReasonSnapshot !== undefined && denyReasonSnapshot.trim() !== '' ? denyReasonSnapshot : undefined;
|
|
214
240
|
return {
|
|
215
241
|
kind: 'decided',
|
|
216
242
|
gatedCallId,
|
|
217
243
|
denied: true,
|
|
218
244
|
...(denySettledBy !== undefined ? { denySettledBy } : {}),
|
|
219
245
|
...(denyReason !== undefined ? { denyReason } : {}),
|
|
246
|
+
...(attributionDropped ? { denyAttributionDropped: true } : {}),
|
|
220
247
|
...(receipt !== undefined ? { receipt } : {}),
|
|
221
248
|
};
|
|
222
249
|
}
|
|
@@ -224,6 +251,7 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
224
251
|
const currentPending = readDecideCurrentPending(e);
|
|
225
252
|
const wireCode = readWireErrorCode(e);
|
|
226
253
|
const safetyCode = e instanceof HitlSafetyError ? e.code : undefined;
|
|
254
|
+
const attributionRefusal = denyAttributionRefusalFromError(e, lastSendCarriedAttribution);
|
|
227
255
|
return {
|
|
228
256
|
kind: 'failed',
|
|
229
257
|
stage: 'decide',
|
|
@@ -233,8 +261,10 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
|
|
|
233
261
|
...(currentPending !== undefined ? { currentPending } : {}),
|
|
234
262
|
...(wireCode !== undefined ? { errorCode: wireCode } : {}),
|
|
235
263
|
...(safetyCode !== undefined ? { safetyCode } : {}),
|
|
264
|
+
...(attributionRefusal !== null ? { denyAttributionRefusal: attributionRefusal } : {}),
|
|
236
265
|
};
|
|
237
266
|
}
|
|
267
|
+
}
|
|
238
268
|
}
|
|
239
269
|
}
|
|
240
270
|
function readReadRootCandidate(v) {
|
|
@@ -392,6 +422,13 @@ function readPersistedRuleAnchors(v) {
|
|
|
392
422
|
function isNonNegativeSafeInt(v) {
|
|
393
423
|
return typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
|
|
394
424
|
}
|
|
425
|
+
function shouldResendWithoutAttribution(e, sentSettledBy, signal) {
|
|
426
|
+
if (!sentSettledBy)
|
|
427
|
+
return false;
|
|
428
|
+
if (signal?.aborted === true)
|
|
429
|
+
return false;
|
|
430
|
+
return denyAttributionRefusalFromError(e, true)?.kind === 'not_in_proof';
|
|
431
|
+
}
|
|
395
432
|
export function readToolApprovalRespondRefusal(err) {
|
|
396
433
|
const pick = (key) => {
|
|
397
434
|
try {
|
|
@@ -647,6 +684,8 @@ function subagentBadgeFor(frame) {
|
|
|
647
684
|
}
|
|
648
685
|
const MAX_RESPOND_NOTE_CHARS = 2048;
|
|
649
686
|
export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamArgs, signal, lane) {
|
|
687
|
+
const approvalId = frame.approvalId;
|
|
688
|
+
const frameToolCallId = typeof frame.toolCallId === 'string' && frame.toolCallId !== '' ? frame.toolCallId : undefined;
|
|
650
689
|
const toolName = typeof frame.toolName === 'string' ? frame.toolName : 'Write';
|
|
651
690
|
let args = streamArgs !== undefined && streamArgs !== null
|
|
652
691
|
? streamArgs
|
|
@@ -657,7 +696,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
657
696
|
if (typeof args !== 'object' || args === null) {
|
|
658
697
|
if (frame.argsOmitted === true) {
|
|
659
698
|
wireNote = 'tool arguments exceeded the wire cap and were omitted — the diff below is reconstructed from the gate message, not the full payload';
|
|
660
|
-
hostLog('debug', `liveToolApprovalWire: frame ${
|
|
699
|
+
hostLog('debug', `liveToolApprovalWire: frame ${approvalId} args omitted (>16KiB wire cap) — card falls back to message-derived path`);
|
|
661
700
|
}
|
|
662
701
|
if (lane?.argsUnavailable === true) {
|
|
663
702
|
wireNote =
|
|
@@ -673,9 +712,9 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
673
712
|
const card = await surfaceApprovalCard({
|
|
674
713
|
toolName,
|
|
675
714
|
args: args,
|
|
676
|
-
callKey: liveFrameCallKey(
|
|
677
|
-
...(
|
|
678
|
-
? { toolCallId:
|
|
715
|
+
callKey: liveFrameCallKey(approvalId),
|
|
716
|
+
...(frameToolCallId !== undefined
|
|
717
|
+
? { toolCallId: frameToolCallId }
|
|
679
718
|
: {}),
|
|
680
719
|
...(signal ? { signal } : {}),
|
|
681
720
|
...(isFromSubagent(frame) ? { workerBadge: subagentBadgeFor(frame) } : {}),
|
|
@@ -713,32 +752,36 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
713
752
|
...(typeof frame.origin === 'string' && frame.origin !== '' ? { origin: frame.origin } : {}),
|
|
714
753
|
});
|
|
715
754
|
if (lane?.argsUnavailable === true && card.kind === 'allow' && card.updatedInput !== undefined) {
|
|
716
|
-
hostLog('error', `liveToolApprovalWire: ${
|
|
755
|
+
hostLog('error', `liveToolApprovalWire: ${approvalId} card returned an edited approval on an args-unavailable ask — nothing sent (the ask stays pending)`);
|
|
717
756
|
surfaceEditRefusedOnBlindAsk();
|
|
718
757
|
return { decision: 'unresolved', editRefused: true };
|
|
719
758
|
}
|
|
720
759
|
if (card.kind === RETRACTED_CARD_DECISION_KIND) {
|
|
721
|
-
hostLog('debug', `liveToolApprovalWire: ${
|
|
760
|
+
hostLog('debug', `liveToolApprovalWire: ${approvalId} card retracted without a decision${card.reason ? ` (${card.reason})` : ''} — nothing sent`);
|
|
722
761
|
return { decision: 'unresolved', retracted: true };
|
|
723
762
|
}
|
|
724
763
|
const decision = card.kind === 'allow' ? (card.allowSession ? 'allow_session' : 'allow') : 'deny';
|
|
764
|
+
const denySettledByForWire = card.kind === 'deny' ? readApprovalDenySettledBy(card) : undefined;
|
|
765
|
+
const sentSettledBy = decision === 'deny' && denySettledByForWire === 'policy';
|
|
766
|
+
let lastSendCarriedAttribution = sentSettledBy;
|
|
767
|
+
const denyReasonSnapshot = card.kind === 'deny' && typeof card.reason === 'string' ? card.reason : undefined;
|
|
725
768
|
if (card.kind === 'failed') {
|
|
726
|
-
hostLog('debug', `liveToolApprovalWire: approval card unavailable (${card.reason}) — fail-closed deny for ${
|
|
769
|
+
hostLog('debug', `liveToolApprovalWire: approval card unavailable (${card.reason}) — fail-closed deny for ${approvalId}`);
|
|
727
770
|
}
|
|
728
771
|
let note;
|
|
729
|
-
if (
|
|
772
|
+
if (denyReasonSnapshot !== undefined && denyReasonSnapshot.trim() !== '') {
|
|
730
773
|
if (lane?.approvalDecisionNoteCapable !== true) {
|
|
731
|
-
hostLog('debug', `liveToolApprovalWire: ${
|
|
774
|
+
hostLog('debug', `liveToolApprovalWire: ${approvalId} card supplied a deny reason (len=${denyReasonSnapshot.length}) but the ` +
|
|
732
775
|
'approvalDecisionNote capability is not confirmed for this engine — note not sent (an unknown key would be ' +
|
|
733
776
|
'silently swallowed by an older server while still acking 200)');
|
|
734
777
|
}
|
|
735
|
-
else if (
|
|
736
|
-
hostLog('debug', `liveToolApprovalWire: ${
|
|
737
|
-
`(len=${
|
|
778
|
+
else if (denyReasonSnapshot.length > MAX_RESPOND_NOTE_CHARS) {
|
|
779
|
+
hostLog('debug', `liveToolApprovalWire: ${approvalId} deny reason exceeds the decision_note cap ` +
|
|
780
|
+
`(len=${denyReasonSnapshot.length} > ${MAX_RESPOND_NOTE_CHARS}) — note not sent at all (the server rejects the WHOLE ` +
|
|
738
781
|
'respond with 400 over an oversize note, and a silently halved audit reason is worse than none)');
|
|
739
782
|
}
|
|
740
783
|
else {
|
|
741
|
-
note =
|
|
784
|
+
note = denyReasonSnapshot;
|
|
742
785
|
}
|
|
743
786
|
}
|
|
744
787
|
try {
|
|
@@ -751,7 +794,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
751
794
|
const wantsTextArm = card.kind === 'allow' && typeof card.persistRule === 'string' && card.persistRule !== '';
|
|
752
795
|
const wantsBatchArm = card.kind === 'allow' && card.persistRuleBatchOfferIndex !== undefined;
|
|
753
796
|
if (wantsTextArm && wantsBatchArm) {
|
|
754
|
-
hostLog('error', `liveToolApprovalWire: DROPPING the whole persistRule arm for ${
|
|
797
|
+
hostLog('error', `liveToolApprovalWire: DROPPING the whole persistRule arm for ${approvalId} — the card returned BOTH a rule ` +
|
|
755
798
|
'text and a batchOfferIndex; the server rejects that combination with a 400 that would take the decision down ' +
|
|
756
799
|
'with it, and picking one arm here would be deciding on the user\'s behalf (the decision itself still goes through)');
|
|
757
800
|
ruleArmDroppedForCheck = true;
|
|
@@ -769,14 +812,14 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
769
812
|
batchArmTarget = target;
|
|
770
813
|
}
|
|
771
814
|
else {
|
|
772
|
-
hostLog('debug', `liveToolApprovalWire: DROPPING persistRuleBatchOfferIndex(${idx}) for ${
|
|
815
|
+
hostLog('debug', `liveToolApprovalWire: DROPPING persistRuleBatchOfferIndex(${idx}) for ${approvalId} — the ` +
|
|
773
816
|
'respondBatchRuleOffers capability is not confirmed for this engine, so the batch arm is not sent at all ' +
|
|
774
817
|
'(the decision itself still goes through unchanged; the offer stays on the card for local/display use)');
|
|
775
818
|
ruleArmDroppedForCaps = true;
|
|
776
819
|
}
|
|
777
820
|
}
|
|
778
821
|
else {
|
|
779
|
-
hostLog('error', `liveToolApprovalWire: DROPPING persistRuleBatchOfferIndex(${String(idx)}) for ${
|
|
822
|
+
hostLog('error', `liveToolApprovalWire: DROPPING persistRuleBatchOfferIndex(${String(idx)}) for ${approvalId} — that wire ` +
|
|
780
823
|
'index is not a batch offer on this frame (out of range, a single offer, or not a non-negative integer); ' +
|
|
781
824
|
'never redeeming an index the engine did not offer as a batch');
|
|
782
825
|
ruleArmDroppedForCheck = true;
|
|
@@ -790,7 +833,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
790
833
|
persistRuleEdited = true;
|
|
791
834
|
}
|
|
792
835
|
else {
|
|
793
|
-
hostLog('debug', `liveToolApprovalWire: DROPPING edited persistRule for ${
|
|
836
|
+
hostLog('debug', `liveToolApprovalWire: DROPPING edited persistRule for ${approvalId} (len=${card.persistRule.length}) — the ` +
|
|
794
837
|
'respondFreeFormRules capability is not confirmed for this engine, so the free-form arm is not sent at all ' +
|
|
795
838
|
'(the decision itself still goes through unchanged)');
|
|
796
839
|
ruleArmDroppedForCaps = true;
|
|
@@ -800,22 +843,40 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
800
843
|
persistRule = card.persistRule;
|
|
801
844
|
}
|
|
802
845
|
else {
|
|
803
|
-
hostLog('error', `liveToolApprovalWire: DROPPING persistRule for ${
|
|
846
|
+
hostLog('error', `liveToolApprovalWire: DROPPING persistRule for ${approvalId} — the card returned a rule that is not among the frame's SINGLE offers (a batch member's text is not a selectable candidate either: a conjunction batch is redeemed by index, all-or-nothing) — never sending un-offered text to the rule store`);
|
|
804
847
|
ruleArmDroppedForCheck = true;
|
|
805
848
|
}
|
|
806
849
|
}
|
|
807
|
-
const
|
|
850
|
+
const respondOpts = {
|
|
808
851
|
...(signal && !signal.aborted ? { signal } : {}),
|
|
809
852
|
...(card.kind === 'allow' && card.updatedInput !== undefined ? { updatedInput: card.updatedInput } : {}),
|
|
810
853
|
...(persistRule !== undefined ? { persistRule } : {}),
|
|
811
854
|
...(persistRuleEdited !== undefined ? { persistRuleEdited } : {}),
|
|
812
855
|
...(persistRuleBatchOfferIndex !== undefined ? { persistRuleBatchOfferIndex } : {}),
|
|
813
856
|
...(note !== undefined ? { note } : {}),
|
|
814
|
-
|
|
857
|
+
...(sentSettledBy ? { settledBy: 'policy' } : {}),
|
|
858
|
+
};
|
|
859
|
+
let attributionDropped = false;
|
|
860
|
+
let raw;
|
|
861
|
+
try {
|
|
862
|
+
raw = await respond(approvalId, decision, respondOpts);
|
|
863
|
+
}
|
|
864
|
+
catch (first) {
|
|
865
|
+
if (!shouldResendWithoutAttribution(first, lastSendCarriedAttribution, signal))
|
|
866
|
+
throw first;
|
|
867
|
+
const { settledBy: _droppedKey, ...withoutAttribution } = respondOpts;
|
|
868
|
+
void _droppedKey;
|
|
869
|
+
lastSendCarriedAttribution = false;
|
|
870
|
+
hostLog('debug', `liveToolApprovalWire: respond(deny) for ${approvalId} was refused because the settlement note is not part of ` +
|
|
871
|
+
'what this deployment signs — re-sending the SAME decision exactly once with that one key removed ' +
|
|
872
|
+
'(the refusal happened before the approval was judged, so nothing was consumed and this is a re-delivery, not a second act)');
|
|
873
|
+
raw = await respond(approvalId, decision, withoutAttribution);
|
|
874
|
+
attributionDropped = true;
|
|
875
|
+
}
|
|
815
876
|
const parsed = readToolApprovalRespondAck(raw);
|
|
816
877
|
let ack = parsed;
|
|
817
|
-
if (parsed !== undefined && (parsed.approvalId !==
|
|
818
|
-
hostLog('error', `liveToolApprovalWire: DISCARDING respond ack for ${
|
|
878
|
+
if (parsed !== undefined && (parsed.approvalId !== approvalId || parsed.decision !== decision)) {
|
|
879
|
+
hostLog('error', `liveToolApprovalWire: DISCARDING respond ack for ${approvalId} — it does not correlate ` +
|
|
819
880
|
`(ack.approvalId=${parsed.approvalId} ack.decision=${parsed.decision}, sent decision=${decision}); ` +
|
|
820
881
|
'treating as "no ack" (unknown) — never surfacing a safety notice off an unrelated receipt');
|
|
821
882
|
ack = undefined;
|
|
@@ -846,7 +907,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
846
907
|
strayEchoes.push('persistedRuleAnchors');
|
|
847
908
|
if (strayEchoes.length > 0) {
|
|
848
909
|
hostLog('error', `liveToolApprovalWire: DISCARDING persisted-rule echo(es) [${strayEchoes.join(', ')}] on the ack for ` +
|
|
849
|
-
`${
|
|
910
|
+
`${approvalId} — they do not correlate with the persistence arm actually sent ` +
|
|
850
911
|
`(editedArm=${String(sentEditedArm)} batchArmMembers=${String(expectedBatchCount ?? 'none')} ` +
|
|
851
912
|
`sentOfferIndex=${String(persistRuleBatchOfferIndex ?? 'none')} ` +
|
|
852
913
|
`echoedRules=${String(ack.persistedRules?.length ?? 'none')} ` +
|
|
@@ -869,31 +930,39 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
|
|
|
869
930
|
if (ruleArmDroppedForCheck)
|
|
870
931
|
surfaceRuleArmRejected();
|
|
871
932
|
if (decision === 'allow_session' && ack?.rememberApplied === false) {
|
|
872
|
-
hostLog('debug', `liveToolApprovalWire: ${
|
|
933
|
+
hostLog('debug', `liveToolApprovalWire: ${approvalId} allow_session ack rememberApplied=false — grant not stored, surfacing honest notice`);
|
|
873
934
|
surfaceRememberNotApplied();
|
|
874
935
|
}
|
|
875
|
-
if (
|
|
876
|
-
hostLog('debug', `liveToolApprovalWire: ${
|
|
936
|
+
if (respondOpts.updatedInput !== undefined && ack?.updatedInputForwarded === false) {
|
|
937
|
+
hostLog('debug', `liveToolApprovalWire: ${approvalId} edited args were NOT forwarded (ack.updatedInputForwarded=false) — the tool runs on the ORIGINAL input`);
|
|
877
938
|
surfaceEditNotForwarded();
|
|
878
939
|
}
|
|
879
940
|
if (note !== undefined && ack !== undefined && ack.noteRecorded !== true) {
|
|
880
|
-
hostLog('debug', `liveToolApprovalWire: ${
|
|
941
|
+
hostLog('debug', `liveToolApprovalWire: ${approvalId} decision note was sent but not persisted ` +
|
|
881
942
|
`(ack.noteRecorded=${String(ack.noteRecorded)}) — decision stood; the audit note did not land on the ask row`);
|
|
882
943
|
}
|
|
883
|
-
const denyAttribution =
|
|
944
|
+
const denyAttribution = decision === 'deny'
|
|
884
945
|
? {
|
|
885
|
-
...(
|
|
886
|
-
...(
|
|
946
|
+
...(attributionDropped ? { denyAttributionDropped: true } : {}),
|
|
947
|
+
...(denySettledByForWire !== undefined ? { denySettledBy: denySettledByForWire } : {}),
|
|
948
|
+
...(denyReasonSnapshot !== undefined && denyReasonSnapshot.trim() !== ''
|
|
949
|
+
? { denyReason: denyReasonSnapshot }
|
|
950
|
+
: {}),
|
|
887
951
|
}
|
|
888
952
|
: {};
|
|
889
953
|
return ack !== undefined ? { decision, ack, ...denyAttribution } : { decision, ...denyAttribution };
|
|
890
954
|
}
|
|
891
955
|
catch (e) {
|
|
892
956
|
const respondRefusal = readToolApprovalRespondRefusal(e);
|
|
893
|
-
|
|
957
|
+
const attributionRefusal = denyAttributionRefusalFromError(e, lastSendCarriedAttribution);
|
|
958
|
+
hostLog('debug', `liveToolApprovalWire: respond(${decision}) failed for ${approvalId} ` +
|
|
894
959
|
`(status=${respondRefusal?.status ?? 'none'} errorCode=${logSafeErrorCode(respondRefusal?.errorCode)} ` +
|
|
895
960
|
`messageLen=${respondRefusal?.message?.length ?? 0}) — engine self-settles (TTL/abort); ` +
|
|
896
961
|
'the refusal text is handed back on outcome.respondRefusal for the host to surface');
|
|
897
|
-
return
|
|
962
|
+
return {
|
|
963
|
+
decision: 'unresolved',
|
|
964
|
+
...(respondRefusal !== undefined ? { respondRefusal } : {}),
|
|
965
|
+
...(attributionRefusal !== null ? { denyAttributionRefusal: attributionRefusal } : {}),
|
|
966
|
+
};
|
|
898
967
|
}
|
|
899
968
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ 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 './mcpEngineLeg.js';
|
|
43
44
|
export * from './mcpProbeCapability.js';
|
|
44
45
|
export * from './mcpProbeWire.js';
|
|
45
46
|
export * from './effectiveFacts.js';
|
|
@@ -174,3 +175,4 @@ export * from './env/localeGeo.js';
|
|
|
174
175
|
export * from './env/localeTag.js';
|
|
175
176
|
export * from './env/uiLanguage.js';
|
|
176
177
|
export type { PendingCheckpoint, RuleSuggestion } from '@sema-agent/sdk';
|
|
178
|
+
export * from './ruleRemovalConsequence.js';
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,7 @@ 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 './mcpEngineLeg.js';
|
|
43
44
|
export * from './mcpProbeCapability.js';
|
|
44
45
|
export * from './mcpProbeWire.js';
|
|
45
46
|
export * from './effectiveFacts.js';
|
|
@@ -171,3 +172,4 @@ export * from './websearch/searchProviderPresets.js';
|
|
|
171
172
|
export * from './env/localeGeo.js';
|
|
172
173
|
export * from './env/localeTag.js';
|
|
173
174
|
export * from './env/uiLanguage.js';
|
|
175
|
+
export * from './ruleRemovalConsequence.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WiringManifestMcpEntryView } from './adapter/downstream/eventToSdkMessage.js';
|
|
2
|
+
export type McpEngineLegLiveness = {
|
|
3
|
+
kind: 'unobserved';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'not-listed';
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'ambiguous';
|
|
8
|
+
rows: number;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'unreadable';
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'absent';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'observed';
|
|
15
|
+
state: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function mcpEngineLegLivenessOf(serverName: string, rows: readonly WiringManifestMcpEntryView[] | null | undefined): McpEngineLegLiveness;
|
|
18
|
+
export interface McpDetailLegNoteInput {
|
|
19
|
+
localClientFailed: boolean;
|
|
20
|
+
engineHostedToolCount: number | undefined;
|
|
21
|
+
liveness: McpEngineLegLiveness;
|
|
22
|
+
}
|
|
23
|
+
export declare function mcpDetailLegNote(input: McpDetailLegNoteInput): string | undefined;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { isMcpLivenessState } from './mcpLiveness.js';
|
|
2
|
+
void true;
|
|
3
|
+
void true;
|
|
4
|
+
void true;
|
|
5
|
+
const MCP_LEG_ROSTER_SCAN_LIMIT = 4096;
|
|
6
|
+
const ABSENT_CELL = Symbol('mcpEngineLeg.absent');
|
|
7
|
+
const UNREADABLE_CELL = Symbol('mcpEngineLeg.unreadable');
|
|
8
|
+
const readKey = (o, k) => {
|
|
9
|
+
if (o === null || typeof o !== 'object')
|
|
10
|
+
return ABSENT_CELL;
|
|
11
|
+
try {
|
|
12
|
+
if (!Object.hasOwn(o, k))
|
|
13
|
+
return ABSENT_CELL;
|
|
14
|
+
return o[k];
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return UNREADABLE_CELL;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export function mcpEngineLegLivenessOf(serverName, rows) {
|
|
21
|
+
let isArray = false;
|
|
22
|
+
try {
|
|
23
|
+
isArray = Array.isArray(rows);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { kind: 'unobserved' };
|
|
27
|
+
}
|
|
28
|
+
if (!isArray)
|
|
29
|
+
return { kind: 'unobserved' };
|
|
30
|
+
const len = readKey(rows, 'length');
|
|
31
|
+
if (typeof len !== 'number' || !Number.isInteger(len) || len < 0)
|
|
32
|
+
return { kind: 'unobserved' };
|
|
33
|
+
if (len > MCP_LEG_ROSTER_SCAN_LIMIT)
|
|
34
|
+
return { kind: 'unobserved' };
|
|
35
|
+
let first;
|
|
36
|
+
let matches = 0;
|
|
37
|
+
let incomplete = false;
|
|
38
|
+
for (let i = 0; i < len; i++) {
|
|
39
|
+
const r = readKey(rows, i);
|
|
40
|
+
if (r === UNREADABLE_CELL) {
|
|
41
|
+
incomplete = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (r === null || typeof r !== 'object')
|
|
45
|
+
continue;
|
|
46
|
+
const nm = readKey(r, 'name');
|
|
47
|
+
if (nm === UNREADABLE_CELL) {
|
|
48
|
+
incomplete = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (nm !== serverName)
|
|
52
|
+
continue;
|
|
53
|
+
matches++;
|
|
54
|
+
if (matches === 1)
|
|
55
|
+
first = r;
|
|
56
|
+
}
|
|
57
|
+
if (incomplete)
|
|
58
|
+
return { kind: 'unobserved' };
|
|
59
|
+
if (matches >= 2)
|
|
60
|
+
return { kind: 'ambiguous', rows: matches };
|
|
61
|
+
if (matches === 0)
|
|
62
|
+
return { kind: 'not-listed' };
|
|
63
|
+
const flag = readKey(first, 'livenessUnreadable');
|
|
64
|
+
if (flag === UNREADABLE_CELL || flag === true)
|
|
65
|
+
return { kind: 'unreadable' };
|
|
66
|
+
const cell = readKey(first, 'liveness');
|
|
67
|
+
if (cell === ABSENT_CELL)
|
|
68
|
+
return { kind: 'absent' };
|
|
69
|
+
if (cell === UNREADABLE_CELL || cell === null || typeof cell !== 'object')
|
|
70
|
+
return { kind: 'unreadable' };
|
|
71
|
+
const state = readKey(cell, 'state');
|
|
72
|
+
if (typeof state !== 'string' || state === '')
|
|
73
|
+
return { kind: 'unreadable' };
|
|
74
|
+
return { kind: 'observed', state };
|
|
75
|
+
}
|
|
76
|
+
const rosterReadingOf = (v) => {
|
|
77
|
+
if (v === UNREADABLE_CELL)
|
|
78
|
+
return 'unreadable';
|
|
79
|
+
if (v === ABSENT_CELL || v === undefined)
|
|
80
|
+
return 'silent';
|
|
81
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0)
|
|
82
|
+
return 'unreadable';
|
|
83
|
+
return v > 0 ? 'listed' : 'zero';
|
|
84
|
+
};
|
|
85
|
+
const NOTE_ENGINE_REACHES = "This client's own connection is down; the engine still reaches it.";
|
|
86
|
+
const NOTE_ENGINE_UNREACHABLE = "This client's own connection is down, and the engine could not reach it when it last looked.";
|
|
87
|
+
const NOTE_ENGINE_CANNOT_TELL = "This client's own connection is down; the engine could not tell whether it reaches this server.";
|
|
88
|
+
const NOTE_ENGINE_WORD_UNRECOGNISED = "This client's own connection is down; the engine reported a liveness state this client does not recognise.";
|
|
89
|
+
const NOTE_LIVENESS_UNREADABLE = "This client's own connection is down; the engine sent a liveness record for this server that this client could not read.";
|
|
90
|
+
const NOTE_ROSTER_LISTS_TOOLS = "This client's own connection is down; the engine listed this server's tools for the last run.";
|
|
91
|
+
const NOTE_ENGINE_SILENT = "This status is this client's own connection. This client has neither a liveness reading nor a tool count from the engine for this server.";
|
|
92
|
+
const NOTE_ROSTER_ZERO = "This status is this client's own connection. The engine reported no tools from this server either.";
|
|
93
|
+
const NOTE_ROSTER_UNREADABLE = "This status is this client's own connection. The engine's tool roster for this server could not be read.";
|
|
94
|
+
export function mcpDetailLegNote(input) {
|
|
95
|
+
const raw = input;
|
|
96
|
+
if (raw === null || typeof raw !== 'object')
|
|
97
|
+
return undefined;
|
|
98
|
+
if (readKey(raw, 'localClientFailed') !== true)
|
|
99
|
+
return undefined;
|
|
100
|
+
const lv = readKey(raw, 'liveness');
|
|
101
|
+
const kind = lv !== null && typeof lv === 'object' ? readKey(lv, 'kind') : undefined;
|
|
102
|
+
if (lv !== null && typeof lv === 'object' && kind === 'observed') {
|
|
103
|
+
const state = readKey(lv, 'state');
|
|
104
|
+
if (isMcpLivenessState(state)) {
|
|
105
|
+
switch (state) {
|
|
106
|
+
case 'reachable':
|
|
107
|
+
return NOTE_ENGINE_REACHES;
|
|
108
|
+
case 'unreachable':
|
|
109
|
+
return NOTE_ENGINE_UNREACHABLE;
|
|
110
|
+
case 'unknown':
|
|
111
|
+
return NOTE_ENGINE_CANNOT_TELL;
|
|
112
|
+
default: {
|
|
113
|
+
const exhaustive = state;
|
|
114
|
+
void exhaustive;
|
|
115
|
+
return NOTE_ENGINE_WORD_UNRECOGNISED;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (typeof state === 'string' && state !== '')
|
|
120
|
+
return NOTE_ENGINE_WORD_UNRECOGNISED;
|
|
121
|
+
return NOTE_LIVENESS_UNREADABLE;
|
|
122
|
+
}
|
|
123
|
+
if (kind === 'unreadable')
|
|
124
|
+
return NOTE_LIVENESS_UNREADABLE;
|
|
125
|
+
switch (rosterReadingOf(readKey(raw, 'engineHostedToolCount'))) {
|
|
126
|
+
case 'listed':
|
|
127
|
+
return NOTE_ROSTER_LISTS_TOOLS;
|
|
128
|
+
case 'zero':
|
|
129
|
+
return NOTE_ROSTER_ZERO;
|
|
130
|
+
case 'unreadable':
|
|
131
|
+
return NOTE_ROSTER_UNREADABLE;
|
|
132
|
+
default:
|
|
133
|
+
return NOTE_ENGINE_SILENT;
|
|
134
|
+
}
|
|
135
|
+
}
|
package/dist/mcpLiveness.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const MCP_LIVENESS_STATES: readonly ["reachable", "unreachable", "unknown"];
|
|
2
2
|
export type McpLivenessState = (typeof MCP_LIVENESS_STATES)[number];
|
|
3
|
+
export declare const isMcpLivenessState: (v: unknown) => v is McpLivenessState;
|
|
3
4
|
export interface McpLivenessView {
|
|
4
5
|
state: McpLivenessState;
|
|
5
6
|
observedAt: number;
|
package/dist/mcpLiveness.js
CHANGED
|
@@ -2,7 +2,8 @@ export const MCP_LIVENESS_STATES = Object.freeze(['reachable', 'unreachable', 'u
|
|
|
2
2
|
void MCP_LIVENESS_STATES;
|
|
3
3
|
void true;
|
|
4
4
|
void true;
|
|
5
|
-
const
|
|
5
|
+
export const isMcpLivenessState = (v) => typeof v === 'string' && MCP_LIVENESS_STATES.includes(v);
|
|
6
|
+
const isLivenessState = isMcpLivenessState;
|
|
6
7
|
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
7
8
|
export function readMcpLiveness(row) {
|
|
8
9
|
if (!isRecord(row))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function removalConsequenceLineForBehavior(behavior) {
|
|
2
|
+
switch (behavior) {
|
|
3
|
+
case 'allow':
|
|
4
|
+
return 'Commands it covers will be asked about again.';
|
|
5
|
+
case 'deny':
|
|
6
|
+
return 'Commands it covers will no longer be refused by this rule — removing it widens what can run; it does not tighten anything.';
|
|
7
|
+
case 'ask':
|
|
8
|
+
return 'Commands it covers will no longer be held for approval by this rule.';
|
|
9
|
+
default:
|
|
10
|
+
return 'Whatever this rule does for the commands it covers will stop applying.';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function ruleRemovalBehaviorOf(raw) {
|
|
14
|
+
return raw === 'allow' || raw === 'deny' || raw === 'ask' ? raw : undefined;
|
|
15
|
+
}
|
|
@@ -14,3 +14,12 @@ export interface UsageWindowExhaustedDetail {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function usageWindowExhaustedFromError(err: unknown): UsageWindowExhaustedDetail | null;
|
|
16
16
|
export declare function usageWindowExhaustedContent(detail: UsageWindowExhaustedDetail): string;
|
|
17
|
+
export type DenyAttributionRefusalKind = 'not_in_proof' | 'decision_conflict' | 'attribution_rejected';
|
|
18
|
+
export interface DenyAttributionRefusalDetail {
|
|
19
|
+
kind: DenyAttributionRefusalKind;
|
|
20
|
+
code: string;
|
|
21
|
+
status: number;
|
|
22
|
+
field?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function denyAttributionRefusalFromError(err: unknown, sentSettledBy: boolean): DenyAttributionRefusalDetail | null;
|
|
25
|
+
export declare function denyAttributionRefusalContent(detail: Pick<DenyAttributionRefusalDetail, 'kind' | 'field'>): string;
|