@scotthuang/agent-knock-knock 0.12.8 → 0.12.10

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.
@@ -5853,7 +5853,7 @@ function managedListApprovalState(conversation) {
5853
5853
  }
5854
5854
  function listActionContracts() {
5855
5855
  return {
5856
- version: 15,
5856
+ version: 16,
5857
5857
  instructions: [
5858
5858
  "Treat terminals[] as the primary resource and use only actions present in available_actions, except the snapshot-bound terminals[].handoff_decision.choices.take_over_current.action and an exact terminals[].blocking_turns[].recovery_action. Either nested action requires explicit user confirmation; after it succeeds, refresh list before any follow-current send.",
5859
5859
  "The session_exact scope uses session_id only when it is prefilled by the listed send action. A rollout-backed managed Codex pane instead uses the terminal_follow_current scope with its exact selector plus expected_terminal_token because even one materialized rollout does not prove the current TUI foreground thread. A turn id is never an ordinary send target.",
@@ -5864,6 +5864,7 @@ function listActionContracts() {
5864
5864
  "List resumable threads before resume; use only a complete native_thread_id and the action returned for that candidate.",
5865
5865
  "Use a terminal selector only when explicitly named by the user or prefilled by that terminal row's send action. A handoff action also carries expected_terminal_token; never infer, guess, or reuse either value.",
5866
5866
  "Use respond only for an in-flight turn that is explicitly waiting for OpenClaw.",
5867
+ "Approval fingerprint v2 binds stable terminal, process, and action fields plus an unredacted exact prompt-region digest. Whole-screen digests and scrollback excerpts are diagnostic only and never authorize approval. A keys-mode prompt without exact prompt-region evidence is not approvable and exposes no approve action.",
5867
5868
  "Managed controls target turn_id. A raw terminal may be controlled only through its own list-prefilled conversation_id action. When a Codex managed prompt has no usable AKK Turn owner, list may expose one manual terminal-scoped approve carrying expected_terminal_token; copy the complete action after explicit human confirmation. It does not mutate the Turn or Session binding, has no durable dispatch receipt, and is never eligible for automatic approval. If its result is interrupted, refresh status and inspect the live prompt instead of retrying blindly. Never construct, guess, or reuse either selector or token.",
5868
5869
  "Start with the action's prefilled arguments, supply every missing_required field, and consult the top-level action's optional fields only when needed.",
5869
5870
  "Authoritative full IDs are prefilled; short_ref is for display and human input.",
@@ -6023,6 +6024,9 @@ function listActionContracts() {
6023
6024
  required: ["turn_id", "expected_approval_fingerprint"],
6024
6025
  optional: ["expected_terminal_token"],
6025
6026
  terminal_scoped_scope: "Only the latest list-prefilled Codex conversation_id/token action may approve a managed current prompt without a usable Turn owner. It remains human-confirmed, revalidates exact terminal/process/Session/dispatch/prompt state before keys, never mutates managed state, has no durable dispatch receipt, cannot be auto-approved, and must not be blindly retried after an interrupted result.",
6027
+ fingerprint_contract: "v2_prompt_region",
6028
+ fingerprint_authority: "Stable terminal, process, and action fields plus the unredacted exact prompt-region digest; whole-screen digests and scrollback excerpts are diagnostic only.",
6029
+ missing_prompt_region_evidence: "not_approvable",
6026
6030
  requires_explicit_user_confirmation: true,
6027
6031
  requires_fresh_status: true
6028
6032
  },
@@ -12427,6 +12431,10 @@ function terminalBridgeApprovalFingerprint({ terminalControl, terminalStatus })
12427
12431
  if (adapterFingerprint) {
12428
12432
  return adapterFingerprint;
12429
12433
  }
12434
+ if (approval.approvable === true &&
12435
+ (stringValue(approval.decision_mode) ?? "keys") === "keys") {
12436
+ return undefined;
12437
+ }
12430
12438
  const endpoint = terminalEndpointFromControlRef(terminalControl);
12431
12439
  const processAnchorPid = Number(endpoint.processAnchorPid);
12432
12440
  if (!Number.isSafeInteger(processAnchorPid) || processAnchorPid <= 1) {
@@ -13808,331 +13816,337 @@ async function runApprove(options) {
13808
13816
  release();
13809
13817
  }
13810
13818
  };
13811
- releaseStateLock = acquireFileLock(`${statePath}.lock`);
13812
13819
  const writerStoreDir = pathsForConversationDir(path.dirname(statePath)).storeDir;
13813
- try {
13814
- return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
13815
- let approval;
13816
- let lockedConversation = conversation;
13817
- const currentConversation = loadState(statePath);
13818
- const currentTakeover = isRecord(currentConversation.native_session_takeover)
13819
- ? currentConversation.native_session_takeover
13820
- : undefined;
13821
- const currentControl = terminalControlFromTakeover(currentTakeover);
13822
- const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
13823
- ? currentTakeover.terminal_bridge_approval
13824
- : undefined;
13825
- if (currentConversation.status !== conversation.status ||
13826
- currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
13827
- !currentControl ||
13828
- !terminalControlsShareIncarnation(currentControl, terminalControl) ||
13829
- (claudeScreenApproval &&
13830
- currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
13831
- throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
13832
- }
13833
- assertManagedTerminalDispatchOwner({
13834
- storeDir: writerStoreDir,
13835
- conversation: currentConversation,
13836
- terminalControl: currentControl,
13837
- action: "approve"
13838
- });
13839
- lockedConversation = currentConversation;
13840
- const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
13841
- approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
13842
- expectedFingerprint,
13843
- scrollbackLines: Number(options.scrollbackLines ?? 120),
13844
- runtime: currentRuntimeIdentity,
13845
- managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
13846
- requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
13847
- authorize: autoApproved
13848
- ? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
13820
+ const runApprovalWithStateLock = async () => {
13821
+ let approval;
13822
+ let lockedConversation = conversation;
13823
+ const currentConversation = loadState(statePath);
13824
+ const currentTakeover = isRecord(currentConversation.native_session_takeover)
13825
+ ? currentConversation.native_session_takeover
13826
+ : undefined;
13827
+ const currentControl = terminalControlFromTakeover(currentTakeover);
13828
+ const currentApproval = isRecord(currentTakeover?.terminal_bridge_approval)
13829
+ ? currentTakeover.terminal_bridge_approval
13830
+ : undefined;
13831
+ if (currentConversation.status !== conversation.status ||
13832
+ currentTakeover?.terminal_bridge_message_id !== nativeTakeover?.terminal_bridge_message_id ||
13833
+ !currentControl ||
13834
+ !terminalControlsShareIncarnation(currentControl, terminalControl) ||
13835
+ (claudeScreenApproval &&
13836
+ currentApproval?.fingerprint !== monitoredApproval?.fingerprint)) {
13837
+ throw new Error("approval state changed while waiting for terminal control; refresh status and retry");
13838
+ }
13839
+ assertManagedTerminalDispatchOwner({
13840
+ storeDir: writerStoreDir,
13841
+ conversation: currentConversation,
13842
+ terminalControl: currentControl,
13843
+ action: "approve"
13844
+ });
13845
+ lockedConversation = currentConversation;
13846
+ const currentRuntimeIdentity = terminalRuntimeIdentityForConversation(currentConversation, currentControl);
13847
+ approval = await createTerminalAgentBridge(options).approve(executor.kind, currentControl, {
13848
+ expectedFingerprint,
13849
+ scrollbackLines: Number(options.scrollbackLines ?? 120),
13850
+ runtime: currentRuntimeIdentity,
13851
+ managedRequest: terminalDurableRequestForConversation(currentConversation, currentControl),
13852
+ requiredDecisionMode: autoApproved && executor.kind === "claude" ? "keys" : undefined,
13853
+ authorize: autoApproved
13854
+ ? ({ agent, terminalControl: currentTerminalControl, inspection, fingerprint }) => {
13855
+ if (!autoApprovalPolicy) {
13856
+ return {
13857
+ approved: false,
13858
+ reason: "automatic approval requires an executor-side policy"
13859
+ };
13860
+ }
13861
+ const candidate = policyCandidateForInspection({
13862
+ agent,
13863
+ currentTerminalControl,
13864
+ inspection,
13865
+ fingerprint
13866
+ });
13867
+ executorPolicyDecision = evaluateApprovalPolicy({
13868
+ policy: autoApprovalPolicy,
13869
+ candidate
13870
+ });
13871
+ if (executorPolicyDecision.action !== "approve") {
13872
+ return {
13873
+ approved: false,
13874
+ reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
13875
+ };
13876
+ }
13877
+ if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
13878
+ return {
13879
+ approved: false,
13880
+ reason: "executor-side auto-approval rule changed before execution"
13881
+ };
13882
+ }
13883
+ if (policyFingerprint &&
13884
+ executorPolicyDecision.policyFingerprint !== policyFingerprint) {
13885
+ return {
13886
+ approved: false,
13887
+ reason: "executor-side auto-approval policy changed before execution"
13888
+ };
13889
+ }
13890
+ return { approved: true };
13891
+ }
13892
+ : undefined,
13893
+ beforeKeyDispatch: claudeScreenApproval
13894
+ ? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
13895
+ if (autoApproved) {
13849
13896
  if (!autoApprovalPolicy) {
13850
- return {
13851
- approved: false,
13852
- reason: "automatic approval requires an executor-side policy"
13853
- };
13897
+ throw new Error("automatic approval requires an executor-side policy before dispatch");
13854
13898
  }
13855
- const candidate = policyCandidateForInspection({
13856
- agent,
13857
- currentTerminalControl,
13858
- inspection,
13859
- fingerprint
13860
- });
13861
- executorPolicyDecision = evaluateApprovalPolicy({
13899
+ const freshPolicyDecision = evaluateApprovalPolicy({
13862
13900
  policy: autoApprovalPolicy,
13863
- candidate
13901
+ candidate: policyCandidateForInspection({
13902
+ agent: executor.kind,
13903
+ currentTerminalControl: dispatchControl,
13904
+ inspection,
13905
+ fingerprint
13906
+ })
13864
13907
  });
13865
- if (executorPolicyDecision.action !== "approve") {
13866
- return {
13867
- approved: false,
13868
- reason: `executor-side auto-approval policy rejected the current request: ${executorPolicyDecision.reason}`
13869
- };
13908
+ if (freshPolicyDecision.action !== "approve") {
13909
+ throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
13870
13910
  }
13871
- if (policyRuleId && executorPolicyDecision.ruleId !== policyRuleId) {
13872
- return {
13873
- approved: false,
13874
- reason: "executor-side auto-approval rule changed before execution"
13875
- };
13911
+ if (executorPolicyDecision?.ruleId &&
13912
+ freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
13913
+ throw new Error("executor-side auto-approval rule changed after recapture");
13914
+ }
13915
+ if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
13916
+ throw new Error("executor-side auto-approval rule changed before dispatch");
13876
13917
  }
13877
13918
  if (policyFingerprint &&
13878
- executorPolicyDecision.policyFingerprint !== policyFingerprint) {
13879
- return {
13880
- approved: false,
13881
- reason: "executor-side auto-approval policy changed before execution"
13882
- };
13919
+ freshPolicyDecision.policyFingerprint !== policyFingerprint) {
13920
+ throw new Error("executor-side auto-approval policy changed before dispatch");
13883
13921
  }
13884
- return { approved: true };
13922
+ executorPolicyDecision = freshPolicyDecision;
13885
13923
  }
13886
- : undefined,
13887
- beforeKeyDispatch: claudeScreenApproval
13888
- ? ({ fingerprint, terminalControl: dispatchControl, inspection, keys }) => {
13889
- if (autoApproved) {
13890
- if (!autoApprovalPolicy) {
13891
- throw new Error("automatic approval requires an executor-side policy before dispatch");
13892
- }
13893
- const freshPolicyDecision = evaluateApprovalPolicy({
13894
- policy: autoApprovalPolicy,
13895
- candidate: policyCandidateForInspection({
13896
- agent: executor.kind,
13897
- currentTerminalControl: dispatchControl,
13898
- inspection,
13899
- fingerprint
13900
- })
13901
- });
13902
- if (freshPolicyDecision.action !== "approve") {
13903
- throw new Error(`executor-side auto-approval policy rejected the recaptured request: ${freshPolicyDecision.reason}`);
13904
- }
13905
- if (executorPolicyDecision?.ruleId &&
13906
- freshPolicyDecision.ruleId !== executorPolicyDecision.ruleId) {
13907
- throw new Error("executor-side auto-approval rule changed after recapture");
13908
- }
13909
- if (policyRuleId && freshPolicyDecision.ruleId !== policyRuleId) {
13910
- throw new Error("executor-side auto-approval rule changed before dispatch");
13911
- }
13912
- if (policyFingerprint &&
13913
- freshPolicyDecision.policyFingerprint !== policyFingerprint) {
13914
- throw new Error("executor-side auto-approval policy changed before dispatch");
13915
- }
13916
- executorPolicyDecision = freshPolicyDecision;
13917
- }
13918
- if (approvalDispatchReserved) {
13919
- throw new Error("Claude approval dispatch was already reserved");
13920
- }
13921
- approvalDispatchReserved = true;
13922
- if (!releaseStateLock) {
13923
- throw new Error("approval state lock was released before terminal dispatch");
13924
- }
13925
- const latestConversation = loadState(statePath);
13926
- const latestTakeover = isRecord(latestConversation.native_session_takeover)
13927
- ? latestConversation.native_session_takeover
13928
- : undefined;
13929
- const latestControl = terminalControlFromTakeover(latestTakeover);
13930
- const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
13931
- ? latestTakeover.terminal_bridge_approval
13932
- : undefined;
13933
- const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
13934
- const latestApprovalState = isRecord(latestApproval?.approval_state)
13935
- ? latestApproval.approval_state
13936
- : undefined;
13937
- const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
13938
- ? latestApprovalState.policy_evidence
13939
- : undefined;
13940
- const recapturedPolicyEvidence = inspection.approval.approvable
13941
- ? inspection.approval.policyEvidence
13942
- : undefined;
13943
- const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
13944
- ? latestTakeover.terminal_bridge_approval_dispatch
13945
- : undefined;
13946
- if (!latestTakeover ||
13947
- latestConversation.status !== "waiting_for_openclaw" ||
13948
- latestTakeover.terminal_bridge_message_id !==
13949
- nativeTakeover?.terminal_bridge_message_id ||
13950
- latestApproval?.fingerprint !== fingerprint ||
13951
- latestNotifiedAt === undefined ||
13952
- cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
13953
- expectedFingerprint !== fingerprint ||
13954
- !terminalControlsShareIncarnation(latestControl, dispatchControl) ||
13955
- (autoApproved &&
13956
- (latestPolicyEvidence?.source !== "claude_transcript" ||
13957
- latestPolicyEvidence.evidence_fingerprint !==
13958
- recapturedPolicyEvidence?.evidenceFingerprint))) {
13959
- throw new Error("approval state changed before terminal dispatch; refresh status and retry");
13960
- }
13961
- if (latestDispatch?.state === "reserved" &&
13962
- latestDispatch.terminal_bridge_message_id ===
13963
- latestTakeover.terminal_bridge_message_id) {
13964
- throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
13965
- }
13966
- const reservedAt = cliNow().toISOString();
13967
- const reservedConversation = {
13968
- ...latestConversation,
13969
- native_session_takeover: {
13970
- ...latestTakeover,
13971
- terminal_bridge_approval_dispatch: {
13972
- state: "reserved",
13973
- attempt_id: randomUUID(),
13974
- fingerprint,
13975
- keys,
13976
- terminal_target: dispatchControl.target,
13977
- terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
13978
- reserved_at: reservedAt
13979
- }
13980
- },
13981
- updated_at: reservedAt
13982
- };
13983
- saveState(statePath, reservedConversation);
13984
- lockedConversation = reservedConversation;
13924
+ if (approvalDispatchReserved) {
13925
+ throw new Error("Claude approval dispatch was already reserved");
13985
13926
  }
13986
- : undefined
13987
- });
13988
- const actualFingerprint = approval.fingerprint;
13989
- const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
13990
- const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
13991
- if (!approval.approved) {
13992
- releaseApprovalStateLock();
13993
- releaseApprovalTerminalLock();
13994
- if (autoApproved) {
13995
- appendEvent(logPath, {
13996
- ts: cliNow().toISOString(),
13997
- conversation_id: conversation.conversation_id,
13998
- event: "terminal_auto_approval_decision",
13999
- action: "rejected",
14000
- reason: approval.reason,
14001
- terminal_control: terminalControl,
14002
- expected_fingerprint: expectedFingerprint,
14003
- actual_fingerprint: actualFingerprint,
14004
- policy_rule_id: effectivePolicyRuleId,
14005
- policy_fingerprint: effectivePolicyFingerprint
14006
- });
13927
+ approvalDispatchReserved = true;
13928
+ if (!releaseStateLock) {
13929
+ throw new Error("approval state lock was released before terminal dispatch");
13930
+ }
13931
+ const latestConversation = loadState(statePath);
13932
+ const latestTakeover = isRecord(latestConversation.native_session_takeover)
13933
+ ? latestConversation.native_session_takeover
13934
+ : undefined;
13935
+ const latestControl = terminalControlFromTakeover(latestTakeover);
13936
+ const latestApproval = isRecord(latestTakeover?.terminal_bridge_approval)
13937
+ ? latestTakeover.terminal_bridge_approval
13938
+ : undefined;
13939
+ const latestNotifiedAt = validTimestampMs(latestApproval?.notified_at);
13940
+ const latestApprovalState = isRecord(latestApproval?.approval_state)
13941
+ ? latestApproval.approval_state
13942
+ : undefined;
13943
+ const latestPolicyEvidence = isRecord(latestApprovalState?.policy_evidence)
13944
+ ? latestApprovalState.policy_evidence
13945
+ : undefined;
13946
+ const recapturedPolicyEvidence = inspection.approval.approvable
13947
+ ? inspection.approval.policyEvidence
13948
+ : undefined;
13949
+ const latestDispatch = isRecord(latestTakeover?.terminal_bridge_approval_dispatch)
13950
+ ? latestTakeover.terminal_bridge_approval_dispatch
13951
+ : undefined;
13952
+ if (!latestTakeover ||
13953
+ latestConversation.status !== "waiting_for_openclaw" ||
13954
+ latestTakeover.terminal_bridge_message_id !==
13955
+ nativeTakeover?.terminal_bridge_message_id ||
13956
+ latestApproval?.fingerprint !== fingerprint ||
13957
+ latestNotifiedAt === undefined ||
13958
+ cliNowMs() - latestNotifiedAt > CLAUDE_SCREEN_APPROVAL_TTL_MS ||
13959
+ expectedFingerprint !== fingerprint ||
13960
+ !terminalControlsShareIncarnation(latestControl, dispatchControl) ||
13961
+ (autoApproved &&
13962
+ (latestPolicyEvidence?.source !== "claude_transcript" ||
13963
+ latestPolicyEvidence.evidence_fingerprint !==
13964
+ recapturedPolicyEvidence?.evidenceFingerprint))) {
13965
+ throw new Error("approval state changed before terminal dispatch; refresh status and retry");
13966
+ }
13967
+ if (latestDispatch?.state === "reserved" &&
13968
+ latestDispatch.terminal_bridge_message_id ===
13969
+ latestTakeover.terminal_bridge_message_id) {
13970
+ throw new Error("a previous Claude approval dispatch has an uncertain outcome; inspect and resolve the terminal manually");
13971
+ }
13972
+ const reservedAt = cliNow().toISOString();
13973
+ const reservedConversation = {
13974
+ ...latestConversation,
13975
+ native_session_takeover: {
13976
+ ...latestTakeover,
13977
+ terminal_bridge_approval_dispatch: {
13978
+ state: "reserved",
13979
+ attempt_id: randomUUID(),
13980
+ fingerprint,
13981
+ keys,
13982
+ terminal_target: dispatchControl.target,
13983
+ terminal_bridge_message_id: latestTakeover.terminal_bridge_message_id,
13984
+ reserved_at: reservedAt
13985
+ }
13986
+ },
13987
+ updated_at: reservedAt
13988
+ };
13989
+ saveState(statePath, reservedConversation);
13990
+ lockedConversation = reservedConversation;
14007
13991
  }
14008
- printJson({
14009
- conversation,
14010
- approved: false,
14011
- blocked: approval.blocked,
14012
- reason: approval.reason,
14013
- terminal_control: terminalControl,
14014
- expected_approval_fingerprint: expectedFingerprint,
14015
- actual_approval_fingerprint: actualFingerprint,
14016
- screen_excerpt: approval.screenExcerpt
14017
- });
14018
- return;
14019
- }
14020
- appendEvent(logPath, {
14021
- ts: cliNow().toISOString(),
14022
- conversation_id: conversation.conversation_id,
14023
- event: "terminal_approval_send",
14024
- terminal_control: terminalControl,
14025
- key: approval.key,
14026
- keys: approval.keys,
14027
- label: approval.label,
14028
- decision_mode: approval.decisionMode,
14029
- request_id: approval.requestId,
14030
- approval_fingerprint: actualFingerprint,
14031
- auto_approved: autoApproved,
14032
- policy_rule_id: effectivePolicyRuleId,
14033
- policy_fingerprint: effectivePolicyFingerprint
14034
- });
13992
+ : undefined
13993
+ });
13994
+ const actualFingerprint = approval.fingerprint;
13995
+ const effectivePolicyRuleId = executorPolicyDecision?.ruleId ?? policyRuleId;
13996
+ const effectivePolicyFingerprint = executorPolicyDecision?.policyFingerprint ?? policyFingerprint;
13997
+ if (!approval.approved) {
13998
+ releaseApprovalStateLock();
14035
13999
  if (autoApproved) {
14036
14000
  appendEvent(logPath, {
14037
14001
  ts: cliNow().toISOString(),
14038
14002
  conversation_id: conversation.conversation_id,
14039
14003
  event: "terminal_auto_approval_decision",
14040
- action: "approved",
14004
+ action: "rejected",
14005
+ reason: approval.reason,
14041
14006
  terminal_control: terminalControl,
14042
- approval_fingerprint: actualFingerprint,
14007
+ expected_fingerprint: expectedFingerprint,
14008
+ actual_fingerprint: actualFingerprint,
14043
14009
  policy_rule_id: effectivePolicyRuleId,
14044
14010
  policy_fingerprint: effectivePolicyFingerprint
14045
14011
  });
14046
14012
  }
14047
- runtimeLog("info", "terminal_approval_send", {
14048
- conversation_id: conversation.conversation_id,
14049
- terminal_target: terminalControl.target,
14050
- key: approval.key,
14051
- keys: approval.keys,
14052
- label: approval.label,
14053
- decision_mode: approval.decisionMode,
14054
- request_id: approval.requestId,
14055
- approval_fingerprint: actualFingerprint,
14056
- auto_approved: autoApproved,
14057
- policy_rule_id: effectivePolicyRuleId,
14058
- policy_fingerprint: effectivePolicyFingerprint
14059
- });
14060
- const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
14061
- ? { ...lockedConversation.native_session_takeover }
14062
- : {};
14063
- const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
14064
- ? nativeTakeoverForUpdate.terminal_bridge_approval
14065
- : undefined;
14066
- const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
14067
- const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
14068
- ? resolvedApproval.approval_state
14069
- : undefined;
14070
- const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
14071
- const approvalResolvedAt = cliNow().toISOString();
14072
- const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
14073
- nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
14074
- DEFAULT_AGENT_TIMEOUT_MINUTES);
14075
- const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
14076
- nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
14077
- DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
14078
- const nextNativeTakeover = {
14079
- ...nativeTakeoverForUpdate,
14080
- terminal_bridge_approval: undefined,
14081
- terminal_bridge_approval_dispatch: undefined,
14082
- terminal_bridge_approval_resolved_at: approvalResolvedAt,
14083
- terminal_bridge_last_approval_fingerprint: actualFingerprint,
14084
- terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
14085
- terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
14086
- terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
14087
- terminal_bridge_last_approval_prompt_cleared_at: undefined,
14088
- terminal_bridge_last_approval_at: approvalResolvedAt,
14089
- terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
14090
- terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
14091
- terminal_bridge_monitor_started_at: approvalResolvedAt,
14092
- terminal_bridge_last_activity_at: approvalResolvedAt,
14093
- terminal_bridge_last_activity_reason: "approval resolved",
14094
- terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
14095
- terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
14096
- terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
14097
- terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
14098
- };
14099
- delete nextNativeTakeover.terminal_bridge_approval;
14100
- delete nextNativeTakeover.terminal_bridge_approval_dispatch;
14101
- delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
14102
- const nextConversation = {
14103
- ...lockedConversation,
14104
- status: terminalBridgeEnabled(lockedConversation)
14105
- ? "waiting_for_agent"
14106
- : lockedConversation.status,
14107
- native_session_takeover: nextNativeTakeover,
14108
- updated_at: approvalResolvedAt
14109
- };
14110
- saveState(statePath, nextConversation);
14111
- releaseApprovalStateLock();
14112
- releaseApprovalTerminalLock();
14113
- const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
14114
- conversation: nextConversation,
14115
- statePath,
14116
- logPath,
14117
- terminalControl,
14118
- options
14119
- });
14120
14013
  printJson({
14121
- conversation: nextConversation,
14122
- approved: true,
14014
+ conversation,
14015
+ approved: false,
14016
+ blocked: approval.blocked,
14017
+ reason: approval.reason,
14018
+ terminal_control: terminalControl,
14019
+ expected_approval_fingerprint: expectedFingerprint,
14020
+ actual_approval_fingerprint: actualFingerprint,
14021
+ screen_excerpt: approval.screenExcerpt
14022
+ });
14023
+ return;
14024
+ }
14025
+ appendEvent(logPath, {
14026
+ ts: cliNow().toISOString(),
14027
+ conversation_id: conversation.conversation_id,
14028
+ event: "terminal_approval_send",
14029
+ terminal_control: terminalControl,
14030
+ key: approval.key,
14031
+ keys: approval.keys,
14032
+ label: approval.label,
14033
+ decision_mode: approval.decisionMode,
14034
+ request_id: approval.requestId,
14035
+ approval_fingerprint: actualFingerprint,
14036
+ auto_approved: autoApproved,
14037
+ policy_rule_id: effectivePolicyRuleId,
14038
+ policy_fingerprint: effectivePolicyFingerprint
14039
+ });
14040
+ if (autoApproved) {
14041
+ appendEvent(logPath, {
14042
+ ts: cliNow().toISOString(),
14043
+ conversation_id: conversation.conversation_id,
14044
+ event: "terminal_auto_approval_decision",
14045
+ action: "approved",
14123
14046
  terminal_control: terminalControl,
14124
- key: approval.key,
14125
- keys: approval.keys,
14126
- label: approval.label,
14127
- decision_mode: approval.decisionMode,
14128
- request_id: approval.requestId,
14129
14047
  approval_fingerprint: actualFingerprint,
14130
- auto_approved: autoApproved,
14131
14048
  policy_rule_id: effectivePolicyRuleId,
14132
- policy_fingerprint: effectivePolicyFingerprint,
14133
- monitor_pid: bridgeMonitor.monitorPid ?? null,
14134
- monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
14049
+ policy_fingerprint: effectivePolicyFingerprint
14135
14050
  });
14051
+ }
14052
+ runtimeLog("info", "terminal_approval_send", {
14053
+ conversation_id: conversation.conversation_id,
14054
+ terminal_target: terminalControl.target,
14055
+ key: approval.key,
14056
+ keys: approval.keys,
14057
+ label: approval.label,
14058
+ decision_mode: approval.decisionMode,
14059
+ request_id: approval.requestId,
14060
+ approval_fingerprint: actualFingerprint,
14061
+ auto_approved: autoApproved,
14062
+ policy_rule_id: effectivePolicyRuleId,
14063
+ policy_fingerprint: effectivePolicyFingerprint
14064
+ });
14065
+ const nativeTakeoverForUpdate = isRecord(lockedConversation.native_session_takeover)
14066
+ ? { ...lockedConversation.native_session_takeover }
14067
+ : {};
14068
+ const resolvedApproval = isRecord(nativeTakeoverForUpdate.terminal_bridge_approval)
14069
+ ? nativeTakeoverForUpdate.terminal_bridge_approval
14070
+ : undefined;
14071
+ const resolvedApprovalScreenDigest = stringValue(resolvedApproval?.screen_digest);
14072
+ const resolvedApprovalState = isRecord(resolvedApproval?.approval_state)
14073
+ ? resolvedApproval.approval_state
14074
+ : undefined;
14075
+ const resolvedTranscriptIdentity = claudeTranscriptApprovalIdentity(resolvedApprovalState);
14076
+ const approvalResolvedAt = cliNow().toISOString();
14077
+ const agentTimeoutMinutes = Number(options.agentTimeoutMinutes ??
14078
+ nativeTakeoverForUpdate.terminal_bridge_inactivity_timeout_minutes ??
14079
+ DEFAULT_AGENT_TIMEOUT_MINUTES);
14080
+ const agentHardTimeoutMinutes = positiveMinutes(options.agentHardTimeoutMinutes ??
14081
+ nativeTakeoverForUpdate.terminal_bridge_hard_timeout_minutes ??
14082
+ DEFAULT_AGENT_HARD_TIMEOUT_MINUTES, "--agent-hard-timeout-minutes");
14083
+ const nextNativeTakeover = {
14084
+ ...nativeTakeoverForUpdate,
14085
+ terminal_bridge_approval: undefined,
14086
+ terminal_bridge_approval_dispatch: undefined,
14087
+ terminal_bridge_approval_resolved_at: approvalResolvedAt,
14088
+ terminal_bridge_last_approval_fingerprint: actualFingerprint,
14089
+ terminal_bridge_last_approval_screen_digest: resolvedApprovalScreenDigest,
14090
+ terminal_bridge_last_approval_request_id: resolvedTranscriptIdentity?.requestId,
14091
+ terminal_bridge_last_approval_evidence_fingerprint: resolvedTranscriptIdentity?.evidenceFingerprint,
14092
+ terminal_bridge_last_approval_prompt_cleared_at: undefined,
14093
+ terminal_bridge_last_approval_at: approvalResolvedAt,
14094
+ terminal_bridge_last_approval_message_id: nativeTakeoverForUpdate.terminal_bridge_message_id,
14095
+ terminal_bridge_monitor_lock_version: TERMINAL_BRIDGE_MONITOR_LOCK_VERSION,
14096
+ terminal_bridge_monitor_started_at: approvalResolvedAt,
14097
+ terminal_bridge_last_activity_at: approvalResolvedAt,
14098
+ terminal_bridge_last_activity_reason: "approval resolved",
14099
+ terminal_bridge_inactivity_timeout_minutes: agentTimeoutMinutes,
14100
+ terminal_bridge_hard_timeout_minutes: agentHardTimeoutMinutes,
14101
+ terminal_bridge_inactivity_deadline_at: deadlineAt(approvalResolvedAt, agentTimeoutMinutes),
14102
+ terminal_bridge_hard_deadline_at: deadlineAt(stringValue(nativeTakeoverForUpdate.terminal_bridge_started_at) ?? approvalResolvedAt, agentHardTimeoutMinutes)
14103
+ };
14104
+ delete nextNativeTakeover.terminal_bridge_approval;
14105
+ delete nextNativeTakeover.terminal_bridge_approval_dispatch;
14106
+ delete nextNativeTakeover.terminal_bridge_last_approval_prompt_cleared_at;
14107
+ const nextConversation = {
14108
+ ...lockedConversation,
14109
+ status: terminalBridgeEnabled(lockedConversation)
14110
+ ? "waiting_for_agent"
14111
+ : lockedConversation.status,
14112
+ native_session_takeover: nextNativeTakeover,
14113
+ updated_at: approvalResolvedAt
14114
+ };
14115
+ saveState(statePath, nextConversation);
14116
+ releaseApprovalStateLock();
14117
+ const bridgeMonitor = ensureTerminalBridgeMonitorAfterApproval({
14118
+ conversation: nextConversation,
14119
+ statePath,
14120
+ logPath,
14121
+ terminalControl,
14122
+ options
14123
+ });
14124
+ printJson({
14125
+ conversation: nextConversation,
14126
+ approved: true,
14127
+ terminal_control: terminalControl,
14128
+ key: approval.key,
14129
+ keys: approval.keys,
14130
+ label: approval.label,
14131
+ decision_mode: approval.decisionMode,
14132
+ request_id: approval.requestId,
14133
+ approval_fingerprint: actualFingerprint,
14134
+ auto_approved: autoApproved,
14135
+ policy_rule_id: effectivePolicyRuleId,
14136
+ policy_fingerprint: effectivePolicyFingerprint,
14137
+ monitor_pid: bridgeMonitor.monitorPid ?? null,
14138
+ monitor_handoff_pid: bridgeMonitor.handoffWatchdog?.pid ?? null
14139
+ });
14140
+ };
14141
+ try {
14142
+ return await withStoreWriterLeaseAsync(writerStoreDir, async () => {
14143
+ releaseStateLock = acquireFileLock(`${statePath}.lock`);
14144
+ try {
14145
+ return await runApprovalWithStateLock();
14146
+ }
14147
+ finally {
14148
+ releaseApprovalStateLock();
14149
+ }
14136
14150
  });
14137
14151
  }
14138
14152
  finally {
@@ -20341,12 +20355,20 @@ async function runTerminalBridgeMonitorWithLock(options, lifecycle, expectedTerm
20341
20355
  });
20342
20356
  }
20343
20357
  }
20358
+ const approvalPromptEvidenceUnavailable = isRecord(approval) &&
20359
+ approval.blocked === true &&
20360
+ approval.approvable === true &&
20361
+ (stringValue(approval.decision_mode) ?? "keys") === "keys" &&
20362
+ !stringValue(approval.fingerprint);
20344
20363
  if (!suppressApprovalNotification &&
20345
20364
  isRecord(approval) &&
20346
20365
  approval.blocked === true &&
20347
- approval.approvable !== true) {
20348
- const approvalReason = stringValue(approval.reason) ??
20349
- "Claude Code permission state cannot be safely resolved through AKK";
20366
+ (approval.approvable !== true ||
20367
+ approvalPromptEvidenceUnavailable)) {
20368
+ const approvalReason = approvalPromptEvidenceUnavailable
20369
+ ? `${executor.display_name} approval prompt lacks exact prompt-region evidence`
20370
+ : stringValue(approval.reason) ??
20371
+ "Claude Code permission state cannot be safely resolved through AKK";
20350
20372
  appendEvent(logPath, {
20351
20373
  ts: cliNow().toISOString(),
20352
20374
  conversation_id: conversation.conversation_id,