@sema-agent/client-core 0.82.7 → 0.83.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +15 -7
  3. package/dist/adapt/arms.d.ts +3 -2
  4. package/dist/adapt/arms.js +17 -11
  5. package/dist/adapt/ids.js +12 -8
  6. package/dist/adapt/textStream.d.ts +3 -2
  7. package/dist/adapt/toolCards.d.ts +2 -3
  8. package/dist/adapt/toolCards.js +1 -1
  9. package/dist/adapt.d.ts +1 -1
  10. package/dist/adapt.js +13 -6
  11. package/dist/adapter/activeRunSelfHeal.js +34 -17
  12. package/dist/adapter/downstream/eventToSdkMessage.js +1 -1
  13. package/dist/adapter/downstream/terminalToSdkResult.d.ts +2 -4
  14. package/dist/adapter/downstream/terminalToSdkResult.js +46 -8
  15. package/dist/adapter/runStream.js +25 -8
  16. package/dist/clientSlice.d.ts +0 -5
  17. package/dist/displayUntrusted.d.ts +24 -0
  18. package/dist/displayUntrusted.js +1437 -0
  19. package/dist/engineNoticeCodes.js +4 -0
  20. package/dist/fleet/fleetRowAgentType.d.ts +0 -1
  21. package/dist/fleet/fleetRowAgentType.js +0 -3
  22. package/dist/fleetTaskDesc.js +3 -3
  23. package/dist/gateVocabulary.js +1 -1
  24. package/dist/hitl/approvalDecisionNoteAudit.js +4 -2
  25. package/dist/hitl/approvalResolution.js +2 -0
  26. package/dist/hitl/frameRouter.d.ts +3 -2
  27. package/dist/hitl/sessionPolicyDeliverable.d.ts +14 -0
  28. package/dist/hitl/sessionPolicyDeliverable.js +80 -0
  29. package/dist/hitl/toolApprovalWire.d.ts +1 -0
  30. package/dist/hitl/toolApprovalWire.js +56 -10
  31. package/dist/hooksWireCaps.d.ts +6 -1
  32. package/dist/hooksWireCaps.js +220 -13
  33. package/dist/host.d.ts +25 -0
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.js +3 -0
  36. package/dist/liveInitToolFace.d.ts +4 -2
  37. package/dist/liveInitToolFace.js +2 -2
  38. package/dist/model/catalogLoader.d.ts +1 -6
  39. package/dist/panelRunningHistory.d.ts +3 -2
  40. package/dist/peerFrames.d.ts +0 -1
  41. package/dist/peerFrames.js +2 -1
  42. package/dist/pluginHooksWire.d.ts +92 -0
  43. package/dist/pluginHooksWire.js +429 -0
  44. package/dist/printInitToolFace.js +1 -1
  45. package/dist/request/printNotification.js +26 -26
  46. package/dist/seam.d.ts +8 -2
  47. package/dist/seam.js +18 -7
  48. package/dist/subagent/engineSubagentResume.js +2 -1
  49. package/dist/systemReminderTag.d.ts +0 -1
  50. package/dist/systemReminderTag.js +0 -3
  51. package/dist/toolResult.js +1 -0
  52. package/dist/uuidV5.d.ts +3 -0
  53. package/dist/uuidV5.js +178 -0
  54. package/dist/webSearchWireCaps.d.ts +0 -3
  55. package/dist/webSearchWireCaps.js +21 -51
  56. package/dist/wireFailureShape.d.ts +2 -1
  57. package/docs/INTEGRATION-CLIENTS.md +794 -13
  58. package/package.json +2 -2
@@ -3,6 +3,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
3
3
  'config.env_timeout_discarded',
4
4
  'config.materialize_env_discarded',
5
5
  'config.models_swapped',
6
+ 'config.context_settings_swapped',
6
7
  'config.read_face_swapped',
7
8
  'config.read_face_deployment_clamped',
8
9
  'config.write_face_swapped',
@@ -22,6 +23,7 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
22
23
  'config.durable_gate_unavailable',
23
24
  'config.peer_admission_out_of_range',
24
25
  'config.peer_lane_unmounted',
26
+ 'config.secret_env_scrubbed',
25
27
  'peer.inbound_disposition',
26
28
  'peer.held_settled',
27
29
  'peer.idle_subscription',
@@ -78,6 +80,7 @@ export const ENGINE_NOTICE_AUDIENCE = Object.freeze({
78
80
  'config.env_timeout_discarded': 'operator',
79
81
  'config.materialize_env_discarded': 'operator',
80
82
  'config.models_swapped': 'operator',
83
+ 'config.context_settings_swapped': 'operator',
81
84
  'config.read_face_swapped': 'operator',
82
85
  'config.read_face_deployment_clamped': 'operator',
83
86
  'config.write_face_swapped': 'operator',
@@ -97,6 +100,7 @@ export const ENGINE_NOTICE_AUDIENCE = Object.freeze({
97
100
  'config.durable_gate_unavailable': 'user',
98
101
  'config.peer_admission_out_of_range': 'operator',
99
102
  'config.peer_lane_unmounted': 'operator',
103
+ 'config.secret_env_scrubbed': 'operator',
100
104
  'peer.inbound_disposition': 'user',
101
105
  'peer.held_settled': 'user',
102
106
  'peer.idle_subscription': 'user',
@@ -1,3 +1,2 @@
1
1
  export declare function recordFleetRowAgentType(taskIdTail: string, agentType: string | undefined): void;
2
2
  export declare function fleetRowAgentType(taskIdTail: string | undefined): string | undefined;
3
- export declare function __resetFleetRowAgentTypeForTests(): void;
@@ -23,6 +23,3 @@ export function fleetRowAgentType(taskIdTail) {
23
23
  agentTypeByTail.set(taskIdTail, v);
24
24
  return v;
25
25
  }
26
- export function __resetFleetRowAgentTypeForTests() {
27
- agentTypeByTail.clear();
28
- }
@@ -1,6 +1,6 @@
1
- const DISPLAY_UNSAFE = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
1
+ import { applyDisplayFace, LEGACY_ESCAPE_FACE, LEGACY_LABEL_FACE } from './displayUntrusted.js';
2
2
  export function escapeDisplayControlChars(s) {
3
- return s.replace(DISPLAY_UNSAFE, ch => `\\u${(ch.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, '0')}`);
3
+ return applyDisplayFace(s, LEGACY_ESCAPE_FACE);
4
4
  }
5
5
  export function capForDisplay(raw, max) {
6
6
  const escaped = escapeDisplayControlChars(raw);
@@ -19,7 +19,7 @@ export function capForDisplay(raw, max) {
19
19
  return cut > 0 ? escaped.slice(0, cut) : '';
20
20
  }
21
21
  export function collapseLabel(s) {
22
- return escapeDisplayControlChars(s.replace(/\s+/g, ' ').trim());
22
+ return applyDisplayFace(s, LEGACY_LABEL_FACE);
23
23
  }
24
24
  export function taskDescFromName(name, agentType, agentName) {
25
25
  if (!name || !agentType)
@@ -116,5 +116,5 @@ export function ruleStoreUnreadableDetail(kind) {
116
116
  return `your standing deny/ask rules could not be applied to this call; the reported reason ${word} is not one of the mechanisms this client's closed set knows, so answer this ask as usual`;
117
117
  }
118
118
  export function mandatedApprovalDetail() {
119
- return 'no saved allow rule and no standing posture can retire this question, so it is asked every time — answering it here settles this one call';
119
+ return 'no saved allow rule and no remembered answer can retire this question; each call is settled on its own — by you here, by a hook, or by an automatic check the deployment runs — and an answer covers only that call';
120
120
  }
@@ -1,10 +1,12 @@
1
+ import { displayUntrusted } from '../displayUntrusted.js';
1
2
  const NOTE_DISPLAY_MAX = 200;
2
3
  export const DECISION_NOTE_NOTICE_KEY = 'approval-decision-note';
3
4
  function cleanNote(raw) {
4
- const flat = raw
5
- .replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ')
5
+ const flat = displayUntrusted(raw, { credentialUrls: false, credentialWords: false, foldLines: false, mark: 'space' })
6
6
  .replace(/ {2,}/g, ' ')
7
7
  .trim();
8
+ if (flat === '')
9
+ return undefined;
8
10
  return flat.length <= NOTE_DISPLAY_MAX ? flat : `${flat.slice(0, NOTE_DISPLAY_MAX - 1)}…`;
9
11
  }
10
12
  export function readDecisionNoteAudit(ack) {
@@ -70,6 +70,8 @@ function fsLegResolution(outcome, kind) {
70
70
  }
71
71
  if (stage === 'input')
72
72
  return { kind: 'not_sent', cause: 'no_input', ...call, ...detail };
73
+ if (stage === 'card' && pick(outcome, 'editRefused') === true)
74
+ return { kind: 'not_sent', cause: 'edit_refused', ...call, ...detail };
73
75
  if (stage === 'card')
74
76
  return { kind: 'not_sent', cause: 'card_unavailable', ...call, ...detail };
75
77
  if (stage === 'decide') {
@@ -24,7 +24,7 @@ export interface AskGateWireDeps {
24
24
  onToolApprovalOutcome?: (frame: ToolApprovalFrame, outcome: ToolApprovalFrameOutcome) => void | Promise<void>;
25
25
  onDecideOperation?: DecideOperationObserver;
26
26
  }
27
- export type GateKind = 'ask' | 'fs';
27
+ type GateKind = 'ask' | 'fs';
28
28
  export interface GatePark {
29
29
  gate: GateKind;
30
30
  pendingDone?: AgentEvent | undefined;
@@ -55,10 +55,11 @@ export interface FrameRouterCtx {
55
55
  }
56
56
  export declare function isAskTool(toolName: string | undefined): boolean;
57
57
  export declare function isHostProgressFrame(ev: AgentEvent): boolean;
58
- export interface HeldFlushInterruptEvidence {
58
+ interface HeldFlushInterruptEvidence {
59
59
  terminal?: AgentEvent | undefined;
60
60
  signal?: AbortSignal | undefined;
61
61
  gateAbortedByUser?: boolean | undefined;
62
62
  }
63
63
  export declare function flushHeldWithInterruptRewrite(led: GateLedger, evidence: HeldFlushInterruptEvidence): Generator<AgentEvent>;
64
64
  export declare function routeFrame(ev: AgentEvent, ctx: FrameRouterCtx): Promise<FrameAction>;
65
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { SessionPermissionRules } from '@sema-agent/sdk';
2
+ export type SessionPolicyWithheldWhy = 'ask_no_bucket' | 'qualified_deny' | 'peer_wide' | 'wildcard' | 'not_a_tool_name';
3
+ export declare const SESSION_POLICY_WITHHELD_WHY: readonly SessionPolicyWithheldWhy[];
4
+ export interface SessionPolicyWithheldRule {
5
+ readonly rule: string;
6
+ readonly why: SessionPolicyWithheldWhy;
7
+ }
8
+ export interface SessionPolicyDeliverability {
9
+ readonly deliverable: SessionPermissionRules;
10
+ readonly withheld: readonly SessionPolicyWithheldRule[];
11
+ readonly unreadable?: true;
12
+ }
13
+ export declare function sessionPolicyDeliverable(behavior: 'deny' | 'ask', rules: readonly string[]): SessionPolicyDeliverability;
14
+ export declare function sessionPolicyWithheldNotice(why: SessionPolicyWithheldWhy): string;
@@ -0,0 +1,80 @@
1
+ const WITHHELD_NOTICE = {
2
+ ask_no_bucket: 'Not written to this session’s rule record: that record only holds rules that block outright, and this rule asks before running instead — recording it as a block would change what it means.',
3
+ qualified_deny: 'Not written to this session’s rule record: that record holds whole tool names and command names only, and this rule has a part in parentheses it cannot carry — recording just the name could block more than this rule does.',
4
+ peer_wide: 'Not written to this session’s rule record: this rule covers every tool of one server or agent peer, and an engine that matches that record by exact tool name would block none of them.',
5
+ wildcard: 'Not written to this session’s rule record: this rule uses a wildcard (*), and an engine that matches that record by exact tool name would block none of the tools it covers.',
6
+ not_a_tool_name: 'Not written to this session’s rule record: this entry is not exactly one tool name — it is empty, not text, or has spaces around the name.',
7
+ };
8
+ export const SESSION_POLICY_WITHHELD_WHY = Object.freeze(Object.keys(WITHHELD_NOTICE));
9
+ const MAX_RULES = 10_000;
10
+ function unreadable() {
11
+ return { deliverable: {}, withheld: [], unreadable: true };
12
+ }
13
+ const COVERING_NAMESPACE_PREFIXES = ['mcp__', 'a2a__'];
14
+ function withheldWhyOf(rule) {
15
+ if (rule.length === 0)
16
+ return 'not_a_tool_name';
17
+ if (/(?:^|[^\\])\(/.test(rule))
18
+ return 'qualified_deny';
19
+ if (rule.includes('*'))
20
+ return 'wildcard';
21
+ for (const prefix of COVERING_NAMESPACE_PREFIXES) {
22
+ if (!rule.startsWith(prefix))
23
+ continue;
24
+ const segments = rule.slice(prefix.length).split('__');
25
+ if (segments[0] !== '' && segments.slice(1).join('__') === '')
26
+ return 'peer_wide';
27
+ }
28
+ if (rule.trim() !== rule)
29
+ return 'not_a_tool_name';
30
+ return undefined;
31
+ }
32
+ export function sessionPolicyDeliverable(behavior, rules) {
33
+ const entries = [];
34
+ try {
35
+ if (behavior !== 'deny' && behavior !== 'ask')
36
+ return unreadable();
37
+ if (!Array.isArray(rules))
38
+ return unreadable();
39
+ const n = rules.length;
40
+ if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < 0 || n > MAX_RULES)
41
+ return unreadable();
42
+ for (let i = 0; i < n; i++) {
43
+ if (!Object.hasOwn(rules, i)) {
44
+ entries.push(undefined);
45
+ continue;
46
+ }
47
+ const x = rules[i];
48
+ entries.push(typeof x === 'string' ? x : undefined);
49
+ }
50
+ }
51
+ catch {
52
+ return unreadable();
53
+ }
54
+ const withheld = [];
55
+ if (behavior === 'ask') {
56
+ for (const r of entries)
57
+ withheld.push({ rule: r ?? '', why: 'ask_no_bucket' });
58
+ return { deliverable: {}, withheld };
59
+ }
60
+ const exact = [];
61
+ for (const r of entries) {
62
+ if (r === undefined) {
63
+ withheld.push({ rule: '', why: 'not_a_tool_name' });
64
+ continue;
65
+ }
66
+ const why = withheldWhyOf(r);
67
+ if (why === undefined)
68
+ exact.push(r);
69
+ else
70
+ withheld.push({ rule: r, why });
71
+ }
72
+ if (withheld.length > 0 || exact.length === 0)
73
+ return { deliverable: {}, withheld };
74
+ return { deliverable: { toolDeny: exact }, withheld };
75
+ }
76
+ export function sessionPolicyWithheldNotice(why) {
77
+ return typeof why === 'string' && Object.hasOwn(WITHHELD_NOTICE, why)
78
+ ? WITHHELD_NOTICE[why]
79
+ : 'Not written to this session’s rule record.';
80
+ }
@@ -39,6 +39,7 @@ export type FsApprovalOutcome = {
39
39
  retryExhausted?: true;
40
40
  currentPending?: GateCurrentPending | undefined;
41
41
  denyAttributionRefusal?: DenyAttributionRefusalDetail | undefined;
42
+ editRefused?: true;
42
43
  };
43
44
  export interface FsApprovalWireDeps {
44
45
  client: HitlClientLike;
@@ -140,15 +140,46 @@ function logAfterDecided(msg) {
140
140
  catch {
141
141
  }
142
142
  }
143
+ const ARGS_OMITTED_PATH_RECOVERED_NOTE = 'tool arguments exceeded the wire cap and were omitted — only the target path was recovered (from the approval question); ' +
144
+ 'the rest of the arguments, such as the content to write, is not shown, so you are deciding without seeing it. ' +
145
+ 'Edits are not accepted on this card: there is no original input to edit';
146
+ const ARGS_OMITTED_NOTHING_RECOVERED_NOTE = 'tool arguments exceeded the wire cap and were omitted, and nothing could be recovered from the approval question — ' +
147
+ 'you are deciding without seeing what this call will do; deny it if you are not sure. ' +
148
+ 'Edits are not accepted on this card: there is no original input to edit';
149
+ const PARK_ROW_ARGS_ABSENT_NOTE = "this request's tool arguments are not available on this surface — neither the event stream nor the pending approval record " +
150
+ 'carries them, so you are deciding without seeing them; deny it if you are not sure what it will do. ' +
151
+ 'Edits are not accepted on this card: there is no original input to edit';
152
+ const PARK_ROW_ARGS_TRUNCATED_NOTE = "this request's tool arguments were too large to keep on the pending approval record and were replaced by a size marker, " +
153
+ 'so you are deciding without seeing them; deny it if you are not sure what it will do. ' +
154
+ 'Edits are not accepted on this card: there is no original input to edit';
155
+ const PARK_EDIT_REFUSED_REASON = "the card returned an edited approval, but this request's tool arguments are not available on this surface " +
156
+ '(missing, or replaced by a size marker on the pending approval record), so the edit cannot be applied — nothing was sent; ' +
157
+ 'approve or deny it as it is';
158
+ function isParkRowInputSizeMarker(v) {
159
+ if (typeof v !== 'object' || v === null || Array.isArray(v))
160
+ return false;
161
+ const keys = Object.keys(v);
162
+ if (keys.length !== 2 || !keys.includes('truncated') || !keys.includes('bytes'))
163
+ return false;
164
+ const o = v;
165
+ return o.truncated === true && typeof o.bytes === 'number' && Number.isFinite(o.bytes) && o.bytes >= 0;
166
+ }
143
167
  export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signal, parkGatedCallId) {
144
168
  const found = await findPendingForTask(deps.client, taskId, (toolName) => toolName !== undefined && (toolNameIsFsWrite(toolName) || toolNameIsShellExec(toolName)), signal ? { signal } : undefined, parkGatedCallId, (row) => askParkRowArm(row) === 'tool-gate');
145
169
  if (!found.ok)
146
170
  return { kind: 'failed', stage: 'fetch', reason: found.reason, ...(found.code !== undefined ? { code: found.code } : {}) };
147
171
  const { pending, gatedCallId } = found;
148
172
  const toolName = typeof pending.toolName === 'string' ? pending.toolName : '';
149
- const args = (gatedCallId !== undefined ? argsByCall.get(gatedCallId) : undefined) ??
150
- pending.input ??
151
- {};
173
+ const streamArgs = gatedCallId !== undefined ? argsByCall.get(gatedCallId) : undefined;
174
+ const rowInput = pending.input;
175
+ const rowArgsGap = streamArgs !== undefined && streamArgs !== null
176
+ ? undefined
177
+ : rowInput === undefined || rowInput === null
178
+ ? 'absent'
179
+ : isParkRowInputSizeMarker(rowInput)
180
+ ? 'truncated'
181
+ : undefined;
182
+ const args = streamArgs !== undefined && streamArgs !== null ? streamArgs : rowArgsGap !== undefined ? {} : rowInput;
152
183
  if (typeof args !== 'object' || args === null) {
153
184
  return { kind: 'failed', stage: 'input', gatedCallId, reason: 'gate has no tool input payload' };
154
185
  }
@@ -165,8 +196,15 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
165
196
  args,
166
197
  callKey,
167
198
  ...(signal ? { signal } : {}),
199
+ ...(rowArgsGap !== undefined
200
+ ? {
201
+ argsUnavailable: true,
202
+ wireNote: rowArgsGap === 'truncated' ? PARK_ROW_ARGS_TRUNCATED_NOTE : PARK_ROW_ARGS_ABSENT_NOTE,
203
+ }
204
+ : {}),
168
205
  ...(pending.governanceForced === true ? { governanceForced: true } : {}),
169
206
  ...(pending.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
207
+ ...(pending.mandated === true ? { mandated: true } : {}),
170
208
  ...(pending.gateKind === 'irreversible_ask' ? { irreversibleParkGate: true } : {}),
171
209
  ...(pending.hasBidiControls === true ? { hasBidiControls: true } : {}),
172
210
  ...(ruleOffersReadOnly !== undefined ? { ruleOffersReadOnly } : {}),
@@ -184,6 +222,11 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
184
222
  ? { toolCallId: pending.toolCallId }
185
223
  : {}),
186
224
  });
225
+ if (rowArgsGap !== undefined && card.kind === 'allow' && card.updatedInput !== undefined) {
226
+ hostLog('error', `liveToolApprovalWire: approval card for ${gatedCallId ?? `task ${taskId}`} returned an edited approval, but the tool input is ${rowArgsGap === 'truncated' ? 'a size marker on the pending row' : 'missing from both the stream and the pending row'} — nothing sent (the park stays pending)`);
227
+ surfaceEditRefusedOnBlindAsk();
228
+ return { kind: 'failed', stage: 'card', gatedCallId, reason: PARK_EDIT_REFUSED_REASON, editRefused: true };
229
+ }
187
230
  const decideSpan = bridge.openDecideOperation();
188
231
  const decideOpts = { ...(signal ? { signal } : {}), operation: decideSpan };
189
232
  try {
@@ -733,10 +776,14 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
733
776
  ? frame.args
734
777
  : undefined;
735
778
  let wireNote;
779
+ let argsFallback = false;
736
780
  if (typeof args !== 'object' || args === null) {
781
+ argsFallback = true;
782
+ const p = pathFromGateMessage(frame.message);
783
+ args = p !== undefined ? { file_path: p } : {};
737
784
  if (frame.argsOmitted === true) {
738
- wireNote = 'tool arguments exceeded the wire cap and were omitted — the diff below is reconstructed from the gate message, not the full payload';
739
- hostLog('debug', `liveToolApprovalWire: frame ${approvalId} args omitted (>16KiB wire cap) — card falls back to message-derived path`);
785
+ wireNote = p !== undefined ? ARGS_OMITTED_PATH_RECOVERED_NOTE : ARGS_OMITTED_NOTHING_RECOVERED_NOTE;
786
+ hostLog('debug', `liveToolApprovalWire: frame ${approvalId} args omitted (>16KiB wire cap) — card falls back to ${p !== undefined ? 'the message-derived path' : 'an empty input (no path could be derived from the message)'}`);
740
787
  }
741
788
  if (lane?.argsUnavailable === true) {
742
789
  wireNote =
@@ -744,9 +791,8 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
744
791
  'so you are deciding without seeing them; deny it if you are not sure what it will do. ' +
745
792
  'Edits are not accepted on this card: there is no original input to edit';
746
793
  }
747
- const p = pathFromGateMessage(frame.message);
748
- args = p !== undefined ? { file_path: p } : {};
749
794
  }
795
+ const argsNotRealInput = argsFallback || lane?.argsUnavailable === true;
750
796
  const ruleOffers = readRuleOfferSupply(frame.ruleOffers, frame.ruleSuggestions);
751
797
  const denialLimitFallback = readDenialLimitFallback(frame.denialLimitFallback);
752
798
  const card = await surfaceApprovalCard({
@@ -759,7 +805,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
759
805
  ...(signal ? { signal } : {}),
760
806
  ...(isFromSubagent(frame) ? { workerBadge: subagentBadgeFor(frame) } : {}),
761
807
  ...(wireNote !== undefined ? { wireNote } : {}),
762
- ...(lane?.argsUnavailable === true ? { argsUnavailable: true } : {}),
808
+ ...(argsNotRealInput ? { argsUnavailable: true } : {}),
763
809
  ...(frame.governanceForced === true ? { governanceForced: true } : {}),
764
810
  ...(lane?.windowIsCurrent === true
765
811
  ? {
@@ -791,8 +837,8 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
791
837
  ...((k) => (k !== undefined ? { ruleStoreUnreadable: k } : {}))(readRuleStoreUnreadable(frame.ruleStoreUnreadable)),
792
838
  ...(typeof frame.origin === 'string' && frame.origin !== '' ? { origin: frame.origin } : {}),
793
839
  });
794
- if (lane?.argsUnavailable === true && card.kind === 'allow' && card.updatedInput !== undefined) {
795
- hostLog('error', `liveToolApprovalWire: ${approvalId} card returned an edited approval on an args-unavailable ask — nothing sent (the ask stays pending)`);
840
+ if (argsNotRealInput && card.kind === 'allow' && card.updatedInput !== undefined) {
841
+ hostLog('error', `liveToolApprovalWire: ${approvalId} card returned an edited approval on a card whose arguments are not the tool's real input (${lane?.argsUnavailable === true ? 'args-unavailable ask' : 'arguments omitted or absent on the frame'}) — nothing sent (the ask stays pending)`);
796
842
  surfaceEditRefusedOnBlindAsk();
797
843
  return { decision: 'unresolved', editRefused: true };
798
844
  }
@@ -1,8 +1,13 @@
1
+ import { type HooksWirePlan, type HooksWirePlanOptions } from './pluginHooksWire.js';
1
2
  import { type HookFailureNotice } from './notifications.js';
2
3
  import type { WireHooksConfig } from './finalVerifyWire.js';
3
4
  export { GOAL_STOP_HOOK_WIRE_ENV, CC_STOP_SEMANTICS_MIN_SERVER, ccStopSemanticsFromVersion, engineCcStopSemantics, isGoalStopHookWireArmed, setWireSessionStopHook, getWireSessionStopHook, getWireSessionStopHookCcSemantics, buildGoalStopHookPrompt, } from './goalStopHook.js';
4
5
  export type { WireHooksConfig };
5
- export declare function hooksForWire(): WireHooksConfig | undefined;
6
+ export interface HooksForWireOptions {
7
+ readonly plan?: HooksWirePlan;
8
+ }
9
+ export declare function hooksForWire(opts?: HooksForWireOptions): WireHooksConfig | undefined;
10
+ export declare function hooksWirePlan(opts?: HooksWirePlanOptions): HooksWirePlan;
6
11
  export declare function __resetHooksWireCapsForTests(): void;
7
12
  export declare function fleetHookFailureNoticeCapable(baseUrl: string | undefined): boolean;
8
13
  export declare const HOOK_FAILURE_WARN_PREFIX = "Hook error";