pushary 1.9.143 → 1.9.145

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 (137) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/data/SKILL.md +4 -2
  3. package/data/cursor-plugin/skills/pushary/SKILL.md +3 -1
  4. package/data/vscode-plugin/skills/pushary/SKILL.md +3 -1
  5. package/dist/{api-2YC2KSIM.js → api-KWJQMO6G.js} +11 -11
  6. package/dist/bin/pushary-audit.js +3 -3
  7. package/dist/bin/pushary-bell-hook.js +28 -28
  8. package/dist/bin/pushary-bell.js +10 -10
  9. package/dist/bin/pushary-claude.js +27 -27
  10. package/dist/bin/pushary-clean.js +35 -35
  11. package/dist/bin/pushary-codex-bridge.js +23 -23
  12. package/dist/bin/pushary-codex-hook.js +35 -35
  13. package/dist/bin/pushary-codex-session.js +8 -8
  14. package/dist/bin/pushary-codex.js +16 -16
  15. package/dist/bin/pushary-connect.js +29 -29
  16. package/dist/bin/pushary-cowork.js +5 -5
  17. package/dist/bin/pushary-daemon.js +43 -43
  18. package/dist/bin/pushary-disconnect.js +28 -28
  19. package/dist/bin/pushary-display-hook.js +7 -0
  20. package/dist/bin/pushary-doctor.js +58 -43
  21. package/dist/bin/pushary-elicitation-hook.js +28 -28
  22. package/dist/bin/pushary-gemini-bridge.js +17 -17
  23. package/dist/bin/pushary-gemini-hook.js +32 -32
  24. package/dist/bin/pushary-hook.js +35 -35
  25. package/dist/bin/pushary-login.js +21 -21
  26. package/dist/bin/pushary-logout.js +27 -27
  27. package/dist/bin/pushary-mcp.js +12 -12
  28. package/dist/bin/pushary-mode.js +15 -15
  29. package/dist/bin/pushary-notification-hook.js +27 -27
  30. package/dist/bin/pushary-opencode-hook.js +31 -31
  31. package/dist/bin/pushary-permission-denied-hook.js +35 -35
  32. package/dist/bin/pushary-permission-hook.js +35 -35
  33. package/dist/bin/pushary-post-hook.js +28 -28
  34. package/dist/bin/pushary-prompt-hook.js +27 -27
  35. package/dist/bin/pushary-session-end-hook.js +27 -27
  36. package/dist/bin/pushary-session-start-hook.js +36 -36
  37. package/dist/bin/pushary-setup.js +122 -54
  38. package/dist/bin/pushary-stats.js +5 -5
  39. package/dist/bin/pushary-status.js +33 -33
  40. package/dist/bin/pushary-stop-hook.js +27 -27
  41. package/dist/bin/pushary-stopfailure-hook.js +27 -27
  42. package/dist/bin/pushary-suggestions.js +15 -15
  43. package/dist/bin/pushary-transcript-register.js +17 -17
  44. package/dist/bin/pushary-transcripts.js +25 -25
  45. package/dist/bin/pushary-upgrade.js +41 -41
  46. package/dist/bin/pushary-wait.js +15 -15
  47. package/dist/bin/pushary.js +7 -7
  48. package/dist/{chunk-XWYCFODT.js → chunk-2ELNRGNX.js} +3 -3
  49. package/dist/{chunk-PYSDCPZ3.js → chunk-2LGDEPVL.js} +6 -6
  50. package/dist/{chunk-5TZYAMGW.js → chunk-33BYPZ52.js} +1 -1
  51. package/dist/{chunk-LUVSHGLH.js → chunk-4KDRT3OA.js} +1 -1
  52. package/dist/{chunk-B4VIT5N3.js → chunk-4L5WPDTB.js} +1 -1
  53. package/dist/{chunk-EGUT2MWV.js → chunk-52UXNCZG.js} +3 -3
  54. package/dist/{chunk-NZHRH4RR.js → chunk-5TWW7ZUL.js} +5 -5
  55. package/dist/{chunk-DU4KCXC2.js → chunk-63WX5KZN.js} +3 -3
  56. package/dist/{chunk-DOXH5BV4.js → chunk-6IWOMXUF.js} +4 -4
  57. package/dist/{chunk-R7FSMUHG.js → chunk-6LZ7MXHD.js} +2 -1
  58. package/dist/{chunk-EMODTMHE.js → chunk-6UK4K5RS.js} +1 -1
  59. package/dist/{chunk-IG7NGXBS.js → chunk-72LP4RPC.js} +3 -2
  60. package/dist/{chunk-MMS7R3VO.js → chunk-7LTP2GHO.js} +2 -2
  61. package/dist/{chunk-QESEICW4.js → chunk-A2H256WR.js} +1 -1
  62. package/dist/{chunk-4YH326C5.js → chunk-B4JQIH5M.js} +6 -6
  63. package/dist/{chunk-4EBJ4PTZ.js → chunk-BWREST4F.js} +1 -1
  64. package/dist/{chunk-ZVJIJPQY.js → chunk-BYKRAYGD.js} +1 -1
  65. package/dist/{chunk-3VOUWGFF.js → chunk-CUR2DCZN.js} +4 -4
  66. package/dist/chunk-D3UFDCTQ.js +87 -0
  67. package/dist/{chunk-UIZE4YMU.js → chunk-DZIZ3XFL.js} +2 -2
  68. package/dist/{chunk-QQHUUPPW.js → chunk-EPWOARGR.js} +2 -2
  69. package/dist/{chunk-LWQLM3AO.js → chunk-FCSPMSDC.js} +4 -4
  70. package/dist/{chunk-X2MC2CF3.js → chunk-FNAH3YJC.js} +16 -16
  71. package/dist/{chunk-ZKKZEVNI.js → chunk-G3VSMI7K.js} +2 -2
  72. package/dist/{chunk-SYZF5SDG.js → chunk-GK5X2ZTN.js} +5 -5
  73. package/dist/{chunk-UJETZZG5.js → chunk-GURIOZE7.js} +8 -8
  74. package/dist/{chunk-7VMVMC4H.js → chunk-HOQH5CLZ.js} +1 -1
  75. package/dist/{chunk-LS6BU5RM.js → chunk-IDLOJIIS.js} +1 -1
  76. package/dist/{chunk-P5QXH5QI.js → chunk-IIN26HL7.js} +1 -1
  77. package/dist/{chunk-22CXL7YI.js → chunk-IRPIMQ7U.js} +3 -3
  78. package/dist/{chunk-4JTZZXB3.js → chunk-JKWUF776.js} +3 -3
  79. package/dist/{chunk-7QWH257P.js → chunk-JZT2LPRM.js} +1 -1
  80. package/dist/{chunk-QGJK3NAR.js → chunk-K2SKUBYY.js} +12 -12
  81. package/dist/{chunk-K7MVCPH5.js → chunk-KFALRZIN.js} +4 -4
  82. package/dist/{chunk-44Z6MQZ2.js → chunk-KGFB2LEB.js} +3 -3
  83. package/dist/{chunk-NOTW7GOC.js → chunk-KKPT7ROP.js} +2 -2
  84. package/dist/{chunk-4Z7YVVKQ.js → chunk-KS3PNTRT.js} +1 -1
  85. package/dist/{chunk-6JLANX5L.js → chunk-L4OGTRVW.js} +4 -4
  86. package/dist/{chunk-QLGLUUOP.js → chunk-LD4R2P6N.js} +1 -1
  87. package/dist/{chunk-4OX4SJGA.js → chunk-LJRPWUST.js} +3 -3
  88. package/dist/{chunk-DDJHPMBJ.js → chunk-LMU6NG2G.js} +1 -1
  89. package/dist/{chunk-C5LIS3XM.js → chunk-LQXUWXQP.js} +7 -0
  90. package/dist/{chunk-MOYEHNFP.js → chunk-MKHVLDMN.js} +2 -2
  91. package/dist/{chunk-G3G7DWR3.js → chunk-MNU5N3CQ.js} +11 -11
  92. package/dist/{chunk-BHKODM4L.js → chunk-MOC2DN42.js} +2 -2
  93. package/dist/{chunk-FS47XZCE.js → chunk-NTMKJRQ3.js} +2 -2
  94. package/dist/{chunk-WSKBXADV.js → chunk-OONMKLUF.js} +4 -4
  95. package/dist/{chunk-NQF652IG.js → chunk-PXZTCR4M.js} +2 -2
  96. package/dist/{chunk-V7CS72Q5.js → chunk-PY6CM4NK.js} +6 -6
  97. package/dist/{chunk-G7G2ZPT4.js → chunk-Q6P7G4NY.js} +4 -4
  98. package/dist/{chunk-KRSZFHNS.js → chunk-QKTF7WS6.js} +67 -4
  99. package/dist/{chunk-5RSM5DRT.js → chunk-R34RWVN2.js} +6 -5
  100. package/dist/{chunk-FYB6HBHQ.js → chunk-RO6EVL5C.js} +2 -2
  101. package/dist/{chunk-SMF4TBPU.js → chunk-RWLNTZK5.js} +5 -5
  102. package/dist/{chunk-LTPKSJWY.js → chunk-S3A4PCRR.js} +10 -10
  103. package/dist/{chunk-YCU7Y77H.js → chunk-S7HGPKCL.js} +1 -1
  104. package/dist/{chunk-MH5BRZOQ.js → chunk-SQL5YXUH.js} +1 -1
  105. package/dist/{chunk-MLOYBLO4.js → chunk-TJXJGMSP.js} +4 -4
  106. package/dist/{chunk-MQF6MQMR.js → chunk-TRV3ETGY.js} +5 -5
  107. package/dist/{chunk-42DRRA6N.js → chunk-USFZJAB4.js} +1 -1
  108. package/dist/{chunk-QFZNPZRO.js → chunk-VTJ4WZOC.js} +3 -3
  109. package/dist/{chunk-ZF4CPOO7.js → chunk-WH4ODOCE.js} +6 -6
  110. package/dist/{chunk-3WZMIDGX.js → chunk-WMBWKPKJ.js} +5 -5
  111. package/dist/{chunk-HIIXYZYV.js → chunk-WOSO4YFQ.js} +3 -3
  112. package/dist/{chunk-V53QYJNY.js → chunk-WPBWJMBZ.js} +5 -5
  113. package/dist/{chunk-USNGPJXF.js → chunk-WUT3PO33.js} +8 -8
  114. package/dist/{chunk-R27HXRSK.js → chunk-Y7INHH5G.js} +3 -3
  115. package/dist/{chunk-ZDOTNMGL.js → chunk-YNCVSKUW.js} +10 -8
  116. package/dist/{chunk-DZLLKJJT.js → chunk-YRJZF5RG.js} +3 -3
  117. package/dist/{chunk-CNURORKI.js → chunk-YU5XE6VC.js} +6 -6
  118. package/dist/{chunk-TKFNIDZK.js → chunk-YZA3YBHX.js} +2 -2
  119. package/dist/{chunk-53KLGXFL.js → chunk-ZBMVOMFG.js} +4 -3
  120. package/dist/{chunk-H33CFJOF.js → chunk-ZJZNT2G5.js} +4 -4
  121. package/dist/{chunk-HLWS6ADK.js → chunk-ZORBUQFG.js} +2 -2
  122. package/dist/{chunk-KP6FVUCC.js → chunk-ZSOCJHL7.js} +188 -13
  123. package/dist/{codex-app-cards-switch-EGUHSLTG.js → codex-app-cards-switch-F7WQ7Y3I.js} +11 -11
  124. package/dist/{codex-card-routing-I6YOJBTT.js → codex-card-routing-EVL5L36G.js} +2 -2
  125. package/dist/{codex-desktop-launch-6UN5CWCX.js → codex-desktop-launch-PL6Z2ELZ.js} +1 -1
  126. package/dist/{codex-session-launch-7R3HNIDJ.js → codex-session-launch-7NQEAUDU.js} +20 -20
  127. package/dist/{config-2CHOLFNB.js → config-KFFD4VWH.js} +3 -3
  128. package/dist/{global-upgrade-CCVIYELF.js → global-upgrade-L3CY6EFF.js} +9 -9
  129. package/dist/{identity-7QA3EDOM.js → identity-DPYYIMHL.js} +4 -4
  130. package/dist/{live-capabilities-SDMX6CPZ.js → live-capabilities-QJEXK6GH.js} +4 -4
  131. package/dist/{paths-ZC6XY73Q.js → paths-PEF6C64P.js} +3 -3
  132. package/dist/{reapply-W6AIK5PH.js → reapply-7APCQRRK.js} +29 -29
  133. package/dist/src/index.js +34 -34
  134. package/dist/{upgrade-lock-BRVUBSLD.js → upgrade-lock-A7OKXMRW.js} +4 -4
  135. package/dist/{usage-GWUTCS5A.js → usage-HSS33762.js} +3 -3
  136. package/package.json +1 -1
  137. package/dist/chunk-I6HXZIER.js +0 -31
@@ -5,10 +5,10 @@ import {
5
5
  hermesAppWiringRecord,
6
6
  hermesConfigReceipt,
7
7
  hermesSetupReceipt
8
- } from "./chunk-IG7NGXBS.js";
8
+ } from "./chunk-72LP4RPC.js";
9
9
  import {
10
10
  resolvePathBinary
11
- } from "./chunk-QFZNPZRO.js";
11
+ } from "./chunk-VTJ4WZOC.js";
12
12
  import {
13
13
  readJsonSafe,
14
14
  writeJsonAtomic
@@ -19,7 +19,7 @@ import {
19
19
  } from "./chunk-WIIMW5YO.js";
20
20
  import {
21
21
  isRecord
22
- } from "./chunk-C5LIS3XM.js";
22
+ } from "./chunk-LQXUWXQP.js";
23
23
 
24
24
  // src/hermes/plugin-payload.ts
25
25
  import { createHash } from "crypto";
@@ -32,7 +32,7 @@ import os
32
32
  import threading
33
33
  from pathlib import Path
34
34
 
35
- from . import api, approval, history, host, identity, installation, schemas, tools
35
+ from . import api, approval, approval_coverage, history, host, identity, installation, schemas, tools
36
36
 
37
37
  logger = logging.getLogger(__name__)
38
38
 
@@ -125,6 +125,7 @@ def register(ctx):
125
125
  ctx.register_skill(child.name, skill_md)
126
126
 
127
127
  installation.record_plugin_load(transport)
128
+ _warn_when_hermes_skips_the_shell()
128
129
 
129
130
  logger.info(
130
131
  "[pushary] Plugin registered: %d tools, %d hooks, approval transport %s",
@@ -157,8 +158,14 @@ def _register_system_prompt_section(ctx):
157
158
 
158
159
 
159
160
  def _gated_tools():
160
- raw = os.environ.get("PUSHARY_GATE_TOOLS", "")
161
- return {name.strip().lower() for name in raw.split(",") if name.strip()}
161
+ raw = os.environ.get(approval_coverage.GATE_TOOLS_ENV, "")
162
+ return {name.strip().lower() for name in raw.split(approval_coverage.GATE_TOOLS_SEPARATOR) if name.strip()}
163
+
164
+
165
+ def _warn_when_hermes_skips_the_shell():
166
+ warning = approval_coverage.shell_gate_warning(os.environ, host.terminal_config(), _gated_tools())
167
+ if warning:
168
+ logger.warning(warning)
162
169
 
163
170
 
164
171
  def _egress_redactor():
@@ -949,6 +956,7 @@ def get_end_user_decision(decision_id, wait_seconds=45):
949
956
  seg = urllib.parse.quote(str(decision_id), safe="")
950
957
  return _rest_call("GET", f"/decisions/{seg}", params={"wait": wait_seconds})
951
958
  ` },
959
+ { path: "approval_coverage.py", contents: 'import json\nimport re\nfrom typing import AbstractSet, Any, List, Mapping, Optional\n\nHERMES_SHELL_TOOL = "terminal"\nGATE_TOOLS_ENV = "PUSHARY_GATE_TOOLS"\nTERMINAL_BACKEND_ENV = "TERMINAL_ENV"\nDOCKER_MOUNT_CWD_ENV = "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE"\nDOCKER_VOLUMES_ENV = "TERMINAL_DOCKER_VOLUMES"\n\nDEFAULT_BACKEND = "local"\nDOCKER_BACKEND = "docker"\nBACKENDS_THAT_SKIP_APPROVALS = frozenset({"modal", "daytona", "vercel_sandbox"})\n\nBACKEND_NAME = re.compile(r"^[a-z0-9_]+$")\nTRUE_WORDS = frozenset({"true", "1", "yes"})\nHOST_PATH_PREFIXES = ("/", "~", "./", "../")\nWINDOWS_DRIVE_SEPARATORS = ("/", "\\\\")\nGATE_TOOLS_SEPARATOR = ","\n\n\ndef _is_blank(value: Any) -> bool:\n return value is None or (isinstance(value, str) and not value.strip())\n\n\ndef _backend_name(value: Any) -> Optional[str]:\n if not isinstance(value, str):\n return None\n name = value.strip()\n return name if BACKEND_NAME.match(name) else None\n\n\ndef terminal_backend(environ: Mapping[str, str], terminal_config: Mapping[str, Any]) -> Optional[str]:\n for value in (environ.get(TERMINAL_BACKEND_ENV), terminal_config.get("backend")):\n if not _is_blank(value):\n return _backend_name(value)\n return DEFAULT_BACKEND\n\n\ndef _is_true(value: Any) -> bool:\n if isinstance(value, bool):\n return value\n if isinstance(value, int):\n return value == 1\n return isinstance(value, str) and value.strip().lower() in TRUE_WORDS\n\n\ndef volume_binds_host_path(spec: Any) -> bool:\n if not isinstance(spec, str):\n return False\n volume = spec.strip()\n if not volume:\n return False\n if volume.startswith(HOST_PATH_PREFIXES):\n return True\n return len(volume) >= 3 and volume[1] == ":" and volume[2] in WINDOWS_DRIVE_SEPARATORS\n\n\ndef _docker_volumes(environ: Mapping[str, str], terminal_config: Mapping[str, Any]) -> List[Any]:\n raw = environ.get(DOCKER_VOLUMES_ENV)\n volumes: Any = terminal_config.get("docker_volumes")\n if raw is not None:\n try:\n volumes = json.loads(raw)\n except ValueError:\n return []\n return volumes if isinstance(volumes, list) else []\n\n\ndef _docker_may_reach_host(environ: Mapping[str, str], terminal_config: Mapping[str, Any]) -> bool:\n mount = environ.get(DOCKER_MOUNT_CWD_ENV, terminal_config.get("docker_mount_cwd_to_workspace"))\n return _is_true(mount) or any(volume_binds_host_path(volume) for volume in _docker_volumes(environ, terminal_config))\n\n\ndef skips_dangerous_command_approvals(environ: Mapping[str, str], terminal_config: Mapping[str, Any]) -> bool:\n backend = terminal_backend(environ, terminal_config)\n if backend == DOCKER_BACKEND:\n return not _docker_may_reach_host(environ, terminal_config)\n return backend in BACKENDS_THAT_SKIP_APPROVALS\n\n\ndef _gate_tools_with_shell(environ: Mapping[str, str]) -> str:\n listed = [entry.strip() for entry in environ.get(GATE_TOOLS_ENV, "").split(GATE_TOOLS_SEPARATOR) if entry.strip()]\n return GATE_TOOLS_SEPARATOR.join([*listed, HERMES_SHELL_TOOL])\n\n\ndef shell_gate_warning(\n environ: Mapping[str, str],\n terminal_config: Mapping[str, Any],\n gated_tools: AbstractSet[str],\n) -> Optional[str]:\n if HERMES_SHELL_TOOL in gated_tools or not skips_dangerous_command_approvals(environ, terminal_config):\n return None\n backend = terminal_backend(environ, terminal_config)\n return (\n f"[pushary] Hermes skips its dangerous-command approvals on the {backend} terminal backend, "\n f"because it treats the container as the boundary. Pushary only gates the tools listed in "\n f"{GATE_TOOLS_ENV}, and {HERMES_SHELL_TOOL} is not one of them, so no shell command reaches "\n f"your phone. To gate them, run: hermes config set {GATE_TOOLS_ENV} {_gate_tools_with_shell(environ)} "\n f"and restart Hermes."\n )\n' },
952
960
  { path: "approval_ledger.py", contents: 'from __future__ import annotations\n\nimport json\nimport os\nimport re\nimport secrets\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Literal, Mapping, Optional, Set, Tuple, Union\n\nif TYPE_CHECKING:\n from typing_extensions import TypeGuard\n\nApprovalChoice = Literal["once", "session", "always", "deny"]\nRecordState = Literal["waiting", "settled"]\nClaimState = Literal["claiming", "won", "lost"]\nHandedBack = Literal["handed_to_hermes"]\nEnded = Literal["ended"]\nSettledOutcome = Union[ApprovalChoice, HandedBack]\nRecentOutcome = Union[SettledOutcome, Ended]\nDecider = Literal["hermes_desktop", "pushary_transport"]\nRefusedClaim = Literal["not_found", "not_waiting", "not_answerable_here", "not_offered", "already_claimed"]\nClaimResult = Union[Literal["claimed"], RefusedClaim]\nProcessIsRunning = Callable[[int], bool]\n\nAPPROVAL_CHOICES: Tuple[ApprovalChoice, ...] = ("once", "session", "always", "deny")\nWAITING: RecordState = "waiting"\nSETTLED: RecordState = "settled"\nCLAIMING: ClaimState = "claiming"\nWON: ClaimState = "won"\nLOST: ClaimState = "lost"\nCLAIM_STATES: Tuple[ClaimState, ...] = (CLAIMING, WON, LOST)\nHANDED_TO_HERMES: HandedBack = "handed_to_hermes"\nENDED: Ended = "ended"\nSETTLED_OUTCOMES: Tuple[SettledOutcome, ...] = (*APPROVAL_CHOICES, HANDED_TO_HERMES)\nRECENT_OUTCOMES: Tuple[RecentOutcome, ...] = (*SETTLED_OUTCOMES, ENDED)\nDECIDED_BY_DESKTOP: Decider = "hermes_desktop"\nDECIDED_BY_TRANSPORT: Decider = "pushary_transport"\n\nCLAIMED: Literal["claimed"] = "claimed"\nNOT_FOUND: RefusedClaim = "not_found"\nNOT_WAITING: RefusedClaim = "not_waiting"\nNOT_ANSWERABLE_HERE: RefusedClaim = "not_answerable_here"\nNOT_OFFERED: RefusedClaim = "not_offered"\nALREADY_CLAIMED: RefusedClaim = "already_claimed"\nREFUSED_CLAIMS: Tuple[RefusedClaim, ...] = (NOT_FOUND, NOT_WAITING, NOT_ANSWERABLE_HERE, NOT_OFFERED, ALREADY_CLAIMED)\n\nDESKTOP_ANSWER_SOURCE = "hermes_desktop"\nUNNAMED_ANSWER_SOURCE = "pushary"\n\nLEDGER_PATH = ("plugin-data", "pushary", "approvals")\nRECORD_SUFFIX = ".json"\nHOST_OUTCOME_PREFIX = "host-"\nTEMPORARY_PREFIX = "."\n\nCORRELATION_ID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")\nREQUEST_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,64}$")\nTOKEN_PATTERN = re.compile(r"^[a-z][a-z0-9_]{0,63}$")\n\nRETENTION_SECONDS = 24 * 60 * 60\nRECENT_LIMIT = 6\nSUBJECT_MAX = 200\nLABEL_MAX = 80\n\n\n@dataclass(frozen=True)\nclass OfferedChoice:\n choice: ApprovalChoice\n label: str\n\n\n@dataclass(frozen=True)\nclass OpenedApproval:\n correlation_id: str\n request_id: Optional[str]\n subject: str\n choices: Tuple[OfferedChoice, ...]\n pid: int\n opened_at_ms: int\n deadline_at_ms: int\n\n\n@dataclass(frozen=True)\nclass WaitingRecord:\n opened: OpenedApproval\n\n\n@dataclass(frozen=True)\nclass SettledRecord:\n opened: OpenedApproval\n outcome: SettledOutcome\n answer_source: Optional[str]\n settled_at_ms: int\n\n\nApprovalRecord = Union[WaitingRecord, SettledRecord]\n\n\n@dataclass(frozen=True)\nclass HostOutcome:\n request_id: str\n choice: str\n at_ms: int\n\n\n@dataclass(frozen=True)\nclass WaitingApproval:\n correlation_id: str\n subject: str\n choices: Tuple[OfferedChoice, ...]\n opened_at_ms: int\n deadline_at_ms: int\n answerable_here: bool\n routed_to: Optional[str]\n claim: Optional[ClaimState]\n\n\n@dataclass(frozen=True)\nclass RecentApproval:\n correlation_id: str\n subject: str\n opened_at_ms: int\n settled_at_ms: int\n outcome: RecentOutcome\n answer_source: Optional[str]\n host_outcome: Optional[str]\n\n\n@dataclass(frozen=True)\nclass ApprovalsView:\n waiting: Tuple[WaitingApproval, ...]\n recent: Tuple[RecentApproval, ...]\n\n\ndef is_correlation_id(value: object) -> TypeGuard[str]:\n return isinstance(value, str) and CORRELATION_ID_PATTERN.fullmatch(value) is not None\n\n\ndef is_request_id(value: object) -> TypeGuard[str]:\n return isinstance(value, str) and REQUEST_ID_PATTERN.fullmatch(value) is not None\n\n\ndef is_token(value: object) -> TypeGuard[str]:\n return isinstance(value, str) and TOKEN_PATTERN.fullmatch(value) is not None\n\n\ndef approval_choice(value: object) -> Optional[ApprovalChoice]:\n for choice in APPROVAL_CHOICES:\n if value == choice:\n return choice\n return None\n\n\ndef _record_state(value: object) -> Optional[RecordState]:\n for state in (WAITING, SETTLED):\n if value == state:\n return state\n return None\n\n\ndef _settled_outcome(value: object) -> Optional[SettledOutcome]:\n for outcome in SETTLED_OUTCOMES:\n if value == outcome:\n return outcome\n return None\n\n\ndef _instant(value: object) -> Optional[int]:\n return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None\n\n\ndef _bounded_text(value: object, limit: int) -> Optional[str]:\n return value if isinstance(value, str) and len(value) <= limit else None\n\n\ndef _fields(value: object) -> Optional[Mapping[object, object]]:\n return value if isinstance(value, dict) else None\n\n\ndef offered_choices(pairs: Iterable[Tuple[str, str]]) -> Tuple[OfferedChoice, ...]:\n offered = []\n for raw_choice, raw_label in pairs:\n choice = approval_choice(raw_choice)\n if choice is not None:\n offered.append(OfferedChoice(choice, str(raw_label)[:LABEL_MAX]))\n return tuple(offered)\n\n\ndef _parse_offered(value: object) -> Optional[OfferedChoice]:\n fields = _fields(value)\n if fields is None:\n return None\n choice = approval_choice(fields.get("choice"))\n label = _bounded_text(fields.get("label"), LABEL_MAX)\n return OfferedChoice(choice, label) if choice is not None and label is not None else None\n\n\ndef _parse_choices(value: object) -> Optional[Tuple[OfferedChoice, ...]]:\n if not isinstance(value, list):\n return None\n parsed: List[OfferedChoice] = []\n for item in value:\n offered = _parse_offered(item)\n if offered is None:\n return None\n parsed.append(offered)\n return tuple(parsed)\n\n\ndef _parse_opened(fields: Mapping[object, object]) -> Optional[OpenedApproval]:\n correlation_id = fields.get("correlationId")\n request_id = fields.get("requestId")\n subject = _bounded_text(fields.get("subject"), SUBJECT_MAX)\n choices = _parse_choices(fields.get("choices"))\n pid = fields.get("pid")\n opened_at_ms = _instant(fields.get("openedAtMs"))\n deadline_at_ms = _instant(fields.get("deadlineAtMs"))\n if not is_correlation_id(correlation_id):\n return None\n if request_id is not None and not is_request_id(request_id):\n return None\n if (\n subject is None\n or not choices\n or not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0\n or opened_at_ms is None\n or deadline_at_ms is None\n ):\n return None\n return OpenedApproval(\n correlation_id=correlation_id,\n request_id=request_id,\n subject=subject,\n choices=choices,\n pid=pid,\n opened_at_ms=opened_at_ms,\n deadline_at_ms=deadline_at_ms,\n )\n\n\ndef parse_record(value: object) -> Optional[ApprovalRecord]:\n fields = _fields(value)\n if fields is None:\n return None\n opened = _parse_opened(fields)\n state = _record_state(fields.get("state"))\n if opened is None or state is None:\n return None\n if state == WAITING:\n return WaitingRecord(opened)\n outcome = _settled_outcome(fields.get("outcome"))\n settled_at_ms = _instant(fields.get("settledAtMs"))\n answer_source = fields.get("answerSource")\n if outcome is None or settled_at_ms is None:\n return None\n if answer_source is not None and not is_token(answer_source):\n return None\n return SettledRecord(opened, outcome, answer_source, settled_at_ms)\n\n\ndef parse_host_outcome(value: object) -> Optional[HostOutcome]:\n fields = _fields(value)\n if fields is None:\n return None\n request_id = fields.get("requestId")\n choice = fields.get("choice")\n at_ms = _instant(fields.get("atMs"))\n if not is_request_id(request_id) or not is_token(choice) or at_ms is None:\n return None\n return HostOutcome(request_id, choice, at_ms)\n\n\ndef _offered_json(choice: OfferedChoice) -> Dict[str, object]:\n return {"choice": choice.choice, "label": choice.label}\n\n\ndef _opened_json(opened: OpenedApproval) -> Dict[str, object]:\n return {\n "correlationId": opened.correlation_id,\n "requestId": opened.request_id,\n "subject": opened.subject,\n "choices": [_offered_json(choice) for choice in opened.choices],\n "pid": opened.pid,\n "openedAtMs": opened.opened_at_ms,\n "deadlineAtMs": opened.deadline_at_ms,\n }\n\n\ndef _record_json(record: ApprovalRecord) -> Dict[str, object]:\n stored = _opened_json(record.opened)\n if isinstance(record, WaitingRecord):\n stored["state"] = WAITING\n return stored\n stored.update(state=SETTLED, outcome=record.outcome, answerSource=record.answer_source,\n settledAtMs=record.settled_at_ms)\n return stored\n\n\ndef _host_outcome_json(outcome: HostOutcome) -> Dict[str, object]:\n return {"requestId": outcome.request_id, "choice": outcome.choice, "atMs": outcome.at_ms}\n\n\ndef ledger_directory(home: Path) -> Path:\n return Path(home).joinpath(*LEDGER_PATH)\n\n\ndef _record_path(home: Path, correlation_id: str) -> Path:\n return ledger_directory(home) / f"{correlation_id}{RECORD_SUFFIX}"\n\n\ndef _host_outcome_path(home: Path, request_id: str) -> Path:\n return ledger_directory(home) / f"{HOST_OUTCOME_PREFIX}{request_id}{RECORD_SUFFIX}"\n\n\ndef _ensure_parent(path: Path) -> None:\n path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)\n\n\ndef _staged_json(path: Path, value: Dict[str, object]) -> Path:\n _ensure_parent(path)\n staged = path.with_name(f"{TEMPORARY_PREFIX}{path.name}.{secrets.token_hex(8)}.tmp")\n try:\n with open(os.open(staged, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w", encoding="utf-8") as handle:\n json.dump(value, handle)\n except BaseException:\n staged.unlink(missing_ok=True)\n raise\n return staged\n\n\ndef _replace_json(path: Path, value: Dict[str, object]) -> None:\n staged = _staged_json(path, value)\n try:\n os.replace(staged, path)\n finally:\n staged.unlink(missing_ok=True)\n\n\ndef _read_json(path: Path) -> object:\n try:\n return json.loads(path.read_text(encoding="utf-8"))\n except (OSError, ValueError):\n return None\n\n\ndef read_record(home: Path, correlation_id: str) -> Optional[ApprovalRecord]:\n if not is_correlation_id(correlation_id):\n return None\n record = parse_record(_read_json(_record_path(home, correlation_id)))\n return record if record is not None and record.opened.correlation_id == correlation_id else None\n\n\ndef read_host_outcome(home: Path, request_id: Optional[str]) -> Optional[HostOutcome]:\n if not is_request_id(request_id):\n return None\n outcome = parse_host_outcome(_read_json(_host_outcome_path(home, request_id)))\n return outcome if outcome is not None and outcome.request_id == request_id else None\n\n\ndef open_approval(\n home: Path,\n *,\n correlation_id: str,\n request_id: Optional[str],\n subject: str,\n choices: Tuple[OfferedChoice, ...],\n pid: int,\n opened_at_ms: int,\n deadline_at_ms: int,\n) -> bool:\n if not is_correlation_id(correlation_id) or pid <= 0 or not choices:\n return False\n opened = OpenedApproval(\n correlation_id=correlation_id,\n request_id=request_id if is_request_id(request_id) else None,\n subject=subject[:SUBJECT_MAX],\n choices=choices,\n pid=pid,\n opened_at_ms=opened_at_ms,\n deadline_at_ms=deadline_at_ms,\n )\n _replace_json(_record_path(home, correlation_id), _record_json(WaitingRecord(opened)))\n return True\n\n\ndef settle_approval(\n home: Path,\n correlation_id: str,\n *,\n outcome: SettledOutcome,\n answer_source: Optional[str],\n settled_at_ms: int,\n) -> bool:\n record = read_record(home, correlation_id)\n if not isinstance(record, WaitingRecord):\n return False\n source = answer_source if is_token(answer_source) else None\n _replace_json(_record_path(home, correlation_id), _record_json(SettledRecord(record.opened, outcome, source,\n settled_at_ms)))\n return True\n\n\ndef record_host_outcome(home: Path, request_id: str, choice: str, at_ms: int) -> bool:\n if not is_request_id(request_id) or not is_token(choice):\n return False\n _replace_json(_host_outcome_path(home, request_id), _host_outcome_json(HostOutcome(request_id, choice, at_ms)))\n return True\n\n\ndef host_has_finished(home: Path, request_id: Optional[str]) -> bool:\n return read_host_outcome(home, request_id) is not None\n\n\ndef _owner_is_running(opened: OpenedApproval, process_is_running: ProcessIsRunning) -> bool:\n return opened.pid > 0 and process_is_running(opened.pid)\n\n\ndef _is_live(record: ApprovalRecord, host_outcome: Optional[HostOutcome], now_ms: int,\n process_is_running: ProcessIsRunning) -> bool:\n return (\n isinstance(record, WaitingRecord)\n and host_outcome is None\n and now_ms < record.opened.deadline_at_ms\n and _owner_is_running(record.opened, process_is_running)\n )\n\n\ndef _waiting_elsewhere(record: WaitingRecord) -> WaitingApproval:\n opened = record.opened\n return WaitingApproval(\n correlation_id=opened.correlation_id,\n subject=opened.subject,\n choices=opened.choices,\n opened_at_ms=opened.opened_at_ms,\n deadline_at_ms=opened.deadline_at_ms,\n answerable_here=False,\n routed_to=None,\n claim=None,\n )\n\n\ndef _recent(record: ApprovalRecord, host_outcome: Optional[HostOutcome]) -> RecentApproval:\n opened = record.opened\n if isinstance(record, SettledRecord):\n outcome: RecentOutcome = record.outcome\n settled_at_ms = record.settled_at_ms\n answer_source = record.answer_source\n else:\n outcome = ENDED\n settled_at_ms = host_outcome.at_ms if host_outcome else opened.deadline_at_ms\n answer_source = None\n return RecentApproval(\n correlation_id=opened.correlation_id,\n subject=opened.subject,\n opened_at_ms=opened.opened_at_ms,\n settled_at_ms=settled_at_ms,\n outcome=outcome,\n answer_source=answer_source,\n host_outcome=host_outcome.choice if host_outcome else None,\n )\n\n\ndef approvals_view(\n records: Iterable[ApprovalRecord],\n host_outcomes: Mapping[str, HostOutcome],\n now_ms: int,\n process_is_running: ProcessIsRunning,\n) -> ApprovalsView:\n waiting = []\n recent = []\n for record in records:\n request_id = record.opened.request_id\n host_outcome = host_outcomes.get(request_id) if request_id else None\n if isinstance(record, WaitingRecord) and _is_live(record, host_outcome, now_ms, process_is_running):\n waiting.append(_waiting_elsewhere(record))\n else:\n recent.append(_recent(record, host_outcome))\n waiting.sort(key=lambda item: item.opened_at_ms)\n recent.sort(key=lambda item: item.settled_at_ms, reverse=True)\n return ApprovalsView(waiting=tuple(waiting), recent=tuple(recent[:RECENT_LIMIT]))\n\n\ndef _ledger_files(home: Path) -> List[Path]:\n try:\n return [path for path in ledger_directory(home).iterdir() if path.is_file()]\n except OSError:\n return []\n\n\ndef _correlation_id_of(name: str) -> Optional[str]:\n candidate = name[: -len(RECORD_SUFFIX)] if name.endswith(RECORD_SUFFIX) else None\n return candidate if is_correlation_id(candidate) else None\n\n\ndef _ledger_entries(home: Path) -> List[Tuple[Path, object]]:\n return [(path, _read_json(path)) for path in _ledger_files(home) if not path.name.startswith(TEMPORARY_PREFIX)]\n\n\ndef read_approvals(home: Path, now_ms: int, process_is_running: ProcessIsRunning) -> ApprovalsView:\n records: List[ApprovalRecord] = []\n host_outcomes: Dict[str, HostOutcome] = {}\n for path, value in _ledger_entries(home):\n name = path.name\n if name.startswith(HOST_OUTCOME_PREFIX):\n host_outcome = parse_host_outcome(value)\n if host_outcome is not None:\n host_outcomes[host_outcome.request_id] = host_outcome\n else:\n record = parse_record(value)\n if record is not None and name == f"{record.opened.correlation_id}{RECORD_SUFFIX}":\n records.append(record)\n return approvals_view(records, host_outcomes, now_ms, process_is_running)\n\n\ndef _unexpired_waiting(home: Path, now_ms: int) -> Set[str]:\n kept: Set[str] = set()\n for path, value in _ledger_entries(home):\n if path.name.startswith(HOST_OUTCOME_PREFIX) or not path.name.endswith(RECORD_SUFFIX):\n continue\n record = parse_record(value)\n if isinstance(record, WaitingRecord) and record.opened.deadline_at_ms > now_ms:\n kept.add(record.opened.correlation_id)\n return kept\n\n\ndef prune(home: Path, now_seconds: float) -> None:\n kept = _unexpired_waiting(home, int(now_seconds * 1000))\n for path in _ledger_files(home):\n if _correlation_id_of(path.name) in kept:\n continue\n try:\n if now_seconds - path.stat().st_mtime > RETENTION_SECONDS:\n path.unlink(missing_ok=True)\n except OSError:\n continue\n' },
953
961
  { path: "approval.py", contents: `import logging
954
962
  import os
@@ -957,10 +965,10 @@ import time
957
965
  from typing import Literal, Optional, Tuple
958
966
 
959
967
  from . import api, approval_ledger, cancellation, desktop_answers, desktop_contract, identity, installation
968
+ from .approval_coverage import HERMES_SHELL_TOOL
960
969
 
961
970
  logger = logging.getLogger(__name__)
962
971
 
963
- HERMES_SHELL_TOOL = "terminal"
964
972
  DECISION_LINE_MAX = 500
965
973
  ACTION_BODY_MAX = 4000
966
974
 
@@ -1313,7 +1321,7 @@ def _await_decision(request, created, correlation_id: str, offered, uses_select:
1313
1321
  return _decided(request, correlation_id, "deny", None)
1314
1322
  ` },
1315
1323
  { path: "cancellation.py", contents: 'from __future__ import annotations\n\nfrom typing import Literal, Mapping, Tuple\n\nCancellation = Literal[\n "withdrawn_by_this_call",\n "cancelled_unproven",\n "already_cancelled",\n "not_pending",\n "unreachable",\n]\n\nWITHDRAWN_BY_THIS_CALL: Cancellation = "withdrawn_by_this_call"\nCANCELLED_UNPROVEN: Cancellation = "cancelled_unproven"\nALREADY_CANCELLED: Cancellation = "already_cancelled"\nNOT_PENDING: Cancellation = "not_pending"\nUNREACHABLE: Cancellation = "unreachable"\nCANCELLATIONS: Tuple[Cancellation, ...] = (\n WITHDRAWN_BY_THIS_CALL,\n CANCELLED_UNPROVEN,\n ALREADY_CANCELLED,\n NOT_PENDING,\n UNREACHABLE,\n)\n\nUNAVAILABLE_STATUS = "unavailable"\nSTOP_HANDOFF = "stop"\n\n\ndef classify_cancellation(response: object) -> Cancellation:\n if not isinstance(response, Mapping):\n return UNREACHABLE\n fields: Mapping[object, object] = response\n cancelled = fields.get("cancelled")\n by_this_call = fields.get("cancelledByThisCall")\n if (\n not isinstance(cancelled, bool)\n or fields.get("status") == UNAVAILABLE_STATUS\n or fields.get("handoffAction") == STOP_HANDOFF\n or (by_this_call is not None and not isinstance(by_this_call, bool))\n or (by_this_call is True and not cancelled)\n ):\n return UNREACHABLE\n if not cancelled:\n return NOT_PENDING\n if by_this_call is None:\n return CANCELLED_UNPROVEN\n return WITHDRAWN_BY_THIS_CALL if by_this_call else ALREADY_CANCELLED\n' },
1316
- { path: "dashboard/manifest.json", contents: '{\n "name": "pushary",\n "label": "Pushary",\n "description": "Approvals Pushary is carrying to your phone, answerable from Hermes Desktop",\n "version": "1.0.1",\n "tab": { "hidden": true },\n "entry": "web.js",\n "api": "plugin_api.py"\n}\n' },
1324
+ { path: "dashboard/manifest.json", contents: '{\n "name": "pushary",\n "label": "Pushary",\n "description": "Approvals Pushary is carrying to your phone, answerable from Hermes Desktop",\n "version": "1.0.2",\n "tab": { "hidden": true },\n "entry": "web.js",\n "api": "plugin_api.py"\n}\n' },
1317
1325
  { path: "dashboard/plugin_api.py", contents: `import importlib
1318
1326
  import sys
1319
1327
  import time
@@ -1967,7 +1975,7 @@ def stop_history(session_id: str) -> None:
1967
1975
  if thread.is_alive():
1968
1976
  logger.warning("[pushary] Hermes final history export is still pending")
1969
1977
  ` },
1970
- { path: "host.py", contents: 'import os\n\nTRANSPORT_NAME = "pushary"\n\nHOOK_TIMEOUT_DEFAULT_SECONDS = 30.0\nHOOK_TIMEOUT_MAX_SECONDS = 600.0\n\nGATE_HEADROOM_SECONDS = 5.0\nGATE_MIN_SECONDS = 5.0\nGATE_MAX_SECONDS = 55.0\n\n# The last Hermes that runs a pre_tool_call callback to completion. Anything\n# after this abandons one that outlives plugins.hook_callback_timeout.\nUNBOUNDED_THROUGH_VERSION = (0, 20, 5)\n\n\ndef _config():\n try:\n from hermes_cli.config import load_config_readonly\n\n loaded = load_config_readonly()\n except Exception:\n return {}\n return loaded if isinstance(loaded, dict) else {}\n\n\ndef _section(node, *path):\n for key in path:\n node = node.get(key) if isinstance(node, dict) else None\n if node is None:\n return {}\n return node if isinstance(node, dict) else {}\n\n\ndef profile_secret(name):\n try:\n from agent.secret_scope import get_secret\n except Exception:\n return os.environ.get(name) or None\n try:\n return get_secret(name) or None\n except Exception:\n return None\n\n\ndef hermes_version_label():\n try:\n from importlib.metadata import version\n\n return version("hermes-cli")\n except Exception:\n try:\n import hermes_cli\n\n label = getattr(hermes_cli, "__version__", None)\n except Exception:\n return None\n return str(label) if label else None\n\n\ndef _hermes_version():\n """This Hermes as a comparable tuple, or None when it will not say."""\n raw = hermes_version_label()\n if not raw:\n return None\n parts = []\n for piece in str(raw).split(".")[:3]:\n digits = ""\n for char in piece:\n if not char.isdigit():\n break\n digits += char\n if not digits:\n return None\n parts.append(int(digits))\n while len(parts) < 3:\n parts.append(0)\n return tuple(parts)\n\n\ndef bounds_pre_tool_call():\n """Whether this Hermes abandons a slow pre_tool_call callback.\n\n The timeout was added after 0.20.5, which runs plugin hooks to completion.\n Clamping the wait on a host that would never have cut it off just shortens\n the window the user has to answer, for nothing.\n\n Unreadable means assume bounded. A wait that is shorter than it had to be\n costs the user seconds; a wait longer than the host allows costs them the\n answer they already gave.\n\n That rule is why the private symbol is not asked on its own. `hasattr` on\n `_HOOK_TIMEOUT_FAIL_CLOSED_HOOKS` cannot tell "this host runs hooks to\n completion" apart from "the private name moved", and those two answers want\n opposite behaviour: the first earns the full window, the second spends 55\n seconds against a budget of 30 and loses an answer the user already gave.\n A rename is the likelier of the two, because a private name is the one thing\n a host is free to change. So the symbol is taken as proof of bounded, and\n only a version this side of the timeout landing is taken as proof of the\n other. Anything unreadable falls to bounded.\n """\n try:\n from hermes_cli import plugins\n except Exception:\n return True\n if hasattr(plugins, "_HOOK_TIMEOUT_FAIL_CLOSED_HOOKS"):\n return True\n version = _hermes_version()\n if version is None:\n return True\n return version > UNBOUNDED_THROUGH_VERSION\n\n\ndef hook_callback_timeout_seconds():\n raw = _section(_config(), "plugins").get("hook_callback_timeout")\n if raw is None:\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n try:\n timeout = float(raw)\n except (TypeError, ValueError):\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n if timeout < 0:\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n return min(timeout, HOOK_TIMEOUT_MAX_SECONDS)\n\n\ndef gate_budget_seconds(requested_seconds):\n if not bounds_pre_tool_call():\n return max(1.0, min(float(requested_seconds), GATE_MAX_SECONDS))\n timeout = hook_callback_timeout_seconds()\n ceiling = (\n GATE_MAX_SECONDS\n if timeout <= 0\n else max(GATE_MIN_SECONDS, timeout - GATE_HEADROOM_SECONDS)\n )\n return max(1.0, min(float(requested_seconds), ceiling, GATE_MAX_SECONDS))\n\n\ndef pushary_is_selected_transport():\n selected = _section(_config(), "security", "approval").get("transport")\n return str(selected or "").strip().lower() == TRANSPORT_NAME\n' },
1978
+ { path: "host.py", contents: 'import os\n\nTRANSPORT_NAME = "pushary"\n\nHOOK_TIMEOUT_DEFAULT_SECONDS = 30.0\nHOOK_TIMEOUT_MAX_SECONDS = 600.0\n\nGATE_HEADROOM_SECONDS = 5.0\nGATE_MIN_SECONDS = 5.0\nGATE_MAX_SECONDS = 55.0\n\n# The last Hermes that runs a pre_tool_call callback to completion. Anything\n# after this abandons one that outlives plugins.hook_callback_timeout.\nUNBOUNDED_THROUGH_VERSION = (0, 20, 5)\n\n\ndef _config():\n try:\n from hermes_cli.config import load_config_readonly\n\n loaded = load_config_readonly()\n except Exception:\n return {}\n return loaded if isinstance(loaded, dict) else {}\n\n\ndef _section(node, *path):\n for key in path:\n node = node.get(key) if isinstance(node, dict) else None\n if node is None:\n return {}\n return node if isinstance(node, dict) else {}\n\n\ndef profile_secret(name):\n try:\n from agent.secret_scope import get_secret\n except Exception:\n return os.environ.get(name) or None\n try:\n return get_secret(name) or None\n except Exception:\n return None\n\n\ndef hermes_version_label():\n try:\n from importlib.metadata import version\n\n return version("hermes-cli")\n except Exception:\n try:\n import hermes_cli\n\n label = getattr(hermes_cli, "__version__", None)\n except Exception:\n return None\n return str(label) if label else None\n\n\ndef _hermes_version():\n """This Hermes as a comparable tuple, or None when it will not say."""\n raw = hermes_version_label()\n if not raw:\n return None\n parts = []\n for piece in str(raw).split(".")[:3]:\n digits = ""\n for char in piece:\n if not char.isdigit():\n break\n digits += char\n if not digits:\n return None\n parts.append(int(digits))\n while len(parts) < 3:\n parts.append(0)\n return tuple(parts)\n\n\ndef bounds_pre_tool_call():\n """Whether this Hermes abandons a slow pre_tool_call callback.\n\n The timeout was added after 0.20.5, which runs plugin hooks to completion.\n Clamping the wait on a host that would never have cut it off just shortens\n the window the user has to answer, for nothing.\n\n Unreadable means assume bounded. A wait that is shorter than it had to be\n costs the user seconds; a wait longer than the host allows costs them the\n answer they already gave.\n\n That rule is why the private symbol is not asked on its own. `hasattr` on\n `_HOOK_TIMEOUT_FAIL_CLOSED_HOOKS` cannot tell "this host runs hooks to\n completion" apart from "the private name moved", and those two answers want\n opposite behaviour: the first earns the full window, the second spends 55\n seconds against a budget of 30 and loses an answer the user already gave.\n A rename is the likelier of the two, because a private name is the one thing\n a host is free to change. So the symbol is taken as proof of bounded, and\n only a version this side of the timeout landing is taken as proof of the\n other. Anything unreadable falls to bounded.\n """\n try:\n from hermes_cli import plugins\n except Exception:\n return True\n if hasattr(plugins, "_HOOK_TIMEOUT_FAIL_CLOSED_HOOKS"):\n return True\n version = _hermes_version()\n if version is None:\n return True\n return version > UNBOUNDED_THROUGH_VERSION\n\n\ndef hook_callback_timeout_seconds():\n raw = _section(_config(), "plugins").get("hook_callback_timeout")\n if raw is None:\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n try:\n timeout = float(raw)\n except (TypeError, ValueError):\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n if timeout < 0:\n return HOOK_TIMEOUT_DEFAULT_SECONDS\n return min(timeout, HOOK_TIMEOUT_MAX_SECONDS)\n\n\ndef gate_budget_seconds(requested_seconds):\n if not bounds_pre_tool_call():\n return max(1.0, min(float(requested_seconds), GATE_MAX_SECONDS))\n timeout = hook_callback_timeout_seconds()\n ceiling = (\n GATE_MAX_SECONDS\n if timeout <= 0\n else max(GATE_MIN_SECONDS, timeout - GATE_HEADROOM_SECONDS)\n )\n return max(1.0, min(float(requested_seconds), ceiling, GATE_MAX_SECONDS))\n\n\ndef terminal_config():\n return _section(_config(), "terminal")\n\n\ndef pushary_is_selected_transport():\n selected = _section(_config(), "security", "approval").get("transport")\n return str(selected or "").strip().lower() == TRANSPORT_NAME\n' },
1971
1979
  { path: "identity.py", contents: 'import hashlib\nimport os\nimport re\nimport socket\nfrom pathlib import Path\n\nDEFAULT_AGENT_NAME = "Hermes"\nMAX_ID_LENGTH = 128\nSHARED_MACHINE_ID_FILE = Path.home() / ".pushary" / "machine-id"\nSHARED_MACHINE_ID_PATTERN = re.compile(r"^[A-Za-z0-9-]{1,128}$")\n\n_session_id = None\n\n\ndef agent_name():\n return os.environ.get("PUSHARY_AGENT_NAME", DEFAULT_AGENT_NAME)\n\n\ndef labelled_agent_name(requested):\n project = str(requested or "").partition(" - ")[2].strip()\n return f"{agent_name()} - {project}" if project else agent_name()\n\n\ndef shared_machine_id(path=None):\n try:\n value = (path or SHARED_MACHINE_ID_FILE).read_text(encoding="utf-8").strip()\n except OSError:\n return None\n return value if SHARED_MACHINE_ID_PATTERN.match(value) else None\n\n\ndef hostname_machine_id():\n try:\n host = socket.gethostname()\n except Exception:\n return None\n if not host:\n return None\n return hashlib.sha256(host.encode("utf-8")).hexdigest()[:8]\n\n\ndef machine_id():\n return shared_machine_id() or hostname_machine_id()\n\n\ndef remember_session(value):\n global _session_id\n if value:\n _session_id = str(value)[:MAX_ID_LENGTH]\n\n\ndef forget_session():\n global _session_id\n _session_id = None\n\n\ndef session_id():\n return _session_id\n' },
1972
1980
  { path: "installation.py", contents: `import json
1973
1981
  import logging
@@ -2098,7 +2106,7 @@ def record_plugin_load(approval_transport_registered, home=None, pid=None, now=N
2098
2106
  logger.debug("[pushary] could not record the plugin load: %s", exc)
2099
2107
  return None
2100
2108
  ` },
2101
- { path: "plugin.yaml", contents: 'name: pushary\nversion: 1.0.1\ndescription: Push notifications and human-in-the-loop for Hermes Agent via Pushary\nauthor: RalphNex OU\nrequires_env:\n - name: PUSHARY_API_KEY\n description: Your Pushary API key\n url: https://pushary.com/sign-up?from=hermes\n secret: true\nconfig_schema:\n api_key:\n type: secret\n env: PUSHARY_API_KEY\n label: Pushary API key\n description: "Get one at pushary.com/sign-up?from=hermes"\n required: true\nprovides_tools:\n - pushary_remind\n - pushary_notify\n - pushary_ask\n - pushary_wait\n - pushary_cancel\n - pushary_propose_scope\n - pushary_enroll\n - pushary_ask_end_user\nprovides_hooks:\n - pre_llm_call\n - pre_tool_call\n - post_tool_call\n - on_session_start\n - on_session_end\n - on_session_finalize\n - post_approval_response\nprovides_approval_transports:\n - pushary\n' },
2109
+ { path: "plugin.yaml", contents: 'name: pushary\nversion: 1.0.2\ndescription: Push notifications and human-in-the-loop for Hermes Agent via Pushary\nauthor: RalphNex OU\nrequires_env:\n - name: PUSHARY_API_KEY\n description: Your Pushary API key\n url: https://pushary.com/sign-up?from=hermes\n secret: true\nconfig_schema:\n api_key:\n type: secret\n env: PUSHARY_API_KEY\n label: Pushary API key\n description: "Get one at pushary.com/sign-up?from=hermes"\n required: true\nprovides_tools:\n - pushary_remind\n - pushary_notify\n - pushary_ask\n - pushary_wait\n - pushary_cancel\n - pushary_propose_scope\n - pushary_enroll\n - pushary_ask_end_user\nprovides_hooks:\n - pre_llm_call\n - pre_tool_call\n - post_tool_call\n - on_session_start\n - on_session_end\n - on_session_finalize\n - post_approval_response\nprovides_approval_transports:\n - pushary\n' },
2102
2110
  { path: "schemas.py", contents: `PUSHARY_REMIND = {
2103
2111
  "name": "pushary_remind",
2104
2112
  "description": "Schedule a one-time personal reminder on Pushary, even after Hermes exits. Use body plus in_minutes or at (ISO timestamp with explicit UTC offset, within 30 days). Never guess the user's timezone. Confirm the returned time. No arguments lists pending reminders; cancel_reminder_id cancels a scheduled one. Use Hermes cron for agent work; use this for reminding the operator.",
@@ -2743,6 +2751,135 @@ var shippedHermesPlugin = (files = HERMES_PLUGIN_FILES) => {
2743
2751
  return { files, version, digest: hermesPluginDigest(files) };
2744
2752
  };
2745
2753
 
2754
+ // src/hermes/approval-coverage.ts
2755
+ var HERMES_TERMINAL_SECTION = "terminal";
2756
+ var HERMES_TERMINAL_ENV_KEY = "TERMINAL_ENV";
2757
+ var HERMES_GATE_TOOLS_KEY = "PUSHARY_GATE_TOOLS";
2758
+ var HERMES_SHELL_TOOL = "terminal";
2759
+ var HERMES_DOCKER_VOLUMES_KEY = "TERMINAL_DOCKER_VOLUMES";
2760
+ var HERMES_DOCKER_MOUNT_CWD_KEY = "TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE";
2761
+ var HERMES_COMMAND_ALLOWLIST_KEY = "command_allowlist";
2762
+ var HERMES_ALWAYS_ALLOWED_SHELL_RULE = `plugin_rule:${HERMES_SHELL_TOOL}`;
2763
+ var HERMES_DEFAULT_BACKEND = "local";
2764
+ var HERMES_DOCKER_BACKEND = "docker";
2765
+ var BACKENDS_WHERE_HERMES_ASKS = /* @__PURE__ */ new Set(["local", "ssh"]);
2766
+ var BACKENDS_WHERE_HERMES_SKIPS = /* @__PURE__ */ new Set(["modal", "daytona", "vercel_sandbox"]);
2767
+ var BACKEND_NAME = /^[a-z0-9_]+$/;
2768
+ var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "1", "yes"]);
2769
+ var HOST_PATH_PREFIXES = ["/", "~", "./", "../"];
2770
+ var WINDOWS_DRIVE_PATH = /^.:[/\\]/;
2771
+ var GATE_TOOLS_SEPARATOR = ",";
2772
+ var ABSENT = { kind: "absent" };
2773
+ var MALFORMED = { kind: "malformed" };
2774
+ var backendNameOf = (value) => {
2775
+ if (value === void 0 || value === null) return ABSENT;
2776
+ if (typeof value !== "string") return MALFORMED;
2777
+ const name = value.trim();
2778
+ if (name === "") return ABSENT;
2779
+ return BACKEND_NAME.test(name) ? { kind: "named", name } : MALFORMED;
2780
+ };
2781
+ var terminalSectionOf = (reading) => {
2782
+ if (reading.kind === "unset") return { kind: "read", section: {} };
2783
+ return isRecord(reading.value) ? { kind: "read", section: reading.value } : { kind: "malformed" };
2784
+ };
2785
+ var environmentBackendOf = (reading) => reading.kind === "unset" ? ABSENT : backendNameOf(reading.value);
2786
+ var effectiveBackend = (configured, environment) => {
2787
+ if (configured.kind === "malformed") return MALFORMED;
2788
+ if (configured.kind === "named" && configured.name !== HERMES_DEFAULT_BACKEND) return configured;
2789
+ if (environment.kind !== "absent") return environment;
2790
+ return { kind: "named", name: HERMES_DEFAULT_BACKEND };
2791
+ };
2792
+ var isTrueSetting = (value) => value === true || value === 1 || typeof value === "string" && TRUE_WORDS.has(value.trim().toLowerCase());
2793
+ var bindsHostPath = (volume) => {
2794
+ if (typeof volume !== "string") return false;
2795
+ const spec = volume.trim();
2796
+ return spec !== "" && (HOST_PATH_PREFIXES.some((prefix) => spec.startsWith(prefix)) || WINDOWS_DRIVE_PATH.test(spec));
2797
+ };
2798
+ var effectiveValue = (reading, configured) => reading.kind === "set" ? reading.value : configured;
2799
+ var volumeListOf = (value) => {
2800
+ if (typeof value !== "string") return value;
2801
+ try {
2802
+ return JSON.parse(value);
2803
+ } catch {
2804
+ return null;
2805
+ }
2806
+ };
2807
+ var mountsHostVolume = (access) => {
2808
+ const volumes = volumeListOf(access.volumes);
2809
+ return Array.isArray(volumes) && volumes.some(bindsHostPath);
2810
+ };
2811
+ var dockerCoverage = (access) => {
2812
+ const backend = HERMES_DOCKER_BACKEND;
2813
+ if (mountsHostVolume(access)) return { kind: "hermes_asks", backend };
2814
+ if (isTrueSetting(access.mountCwd)) return { kind: "hermes_may_skip", backend, doubt: "docker_start_folder" };
2815
+ return { kind: "hermes_skips", backend };
2816
+ };
2817
+ var dockerCoverageFrom = (section, readings) => {
2818
+ if (readings.dockerVolumes === null || readings.dockerMountCwd === null) return { kind: "unreadable" };
2819
+ return dockerCoverage({
2820
+ volumes: effectiveValue(readings.dockerVolumes, section.docker_volumes),
2821
+ mountCwd: effectiveValue(readings.dockerMountCwd, section.docker_mount_cwd_to_workspace)
2822
+ });
2823
+ };
2824
+ var coverageOn = (backend, section, readings) => {
2825
+ if (backend === HERMES_DOCKER_BACKEND) return dockerCoverageFrom(section, readings);
2826
+ if (BACKENDS_WHERE_HERMES_ASKS.has(backend)) return { kind: "hermes_asks", backend };
2827
+ if (BACKENDS_WHERE_HERMES_SKIPS.has(backend)) return { kind: "hermes_skips", backend };
2828
+ return { kind: "hermes_may_skip", backend, doubt: "plugin_backend" };
2829
+ };
2830
+ var hermesApprovalCoverage = (readings) => {
2831
+ if (readings.terminal === null || readings.terminalEnv === null) return { kind: "unreadable" };
2832
+ const terminal = terminalSectionOf(readings.terminal);
2833
+ if (terminal.kind === "malformed") return { kind: "unreadable" };
2834
+ const backend = effectiveBackend(backendNameOf(terminal.section.backend), environmentBackendOf(readings.terminalEnv));
2835
+ return backend.kind === "named" ? coverageOn(backend.name, terminal.section, readings) : { kind: "unreadable" };
2836
+ };
2837
+ var listedEntries = (raw) => raw.split(GATE_TOOLS_SEPARATOR).map((entry) => entry.trim()).filter((entry) => entry !== "");
2838
+ var hermesGateToolsFrom = (reading) => {
2839
+ if (reading === null) return { kind: "unreadable" };
2840
+ if (reading.kind === "unset") return { kind: "unset" };
2841
+ if (typeof reading.value !== "string") return { kind: "unreadable" };
2842
+ const entries = listedEntries(reading.value);
2843
+ return { kind: "listed", entries, tools: entries.map((entry) => entry.toLowerCase()) };
2844
+ };
2845
+ var gatesHermesShell = (gate) => gate.kind === "listed" && gate.tools.includes(HERMES_SHELL_TOOL);
2846
+ var gateToolsWithShell = (gate) => {
2847
+ if (gate.kind === "unreadable" || gatesHermesShell(gate)) return null;
2848
+ const entries = gate.kind === "listed" ? gate.entries : [];
2849
+ return [...entries, HERMES_SHELL_TOOL].join(GATE_TOOLS_SEPARATOR);
2850
+ };
2851
+ var LEGACY_FLOW_LIST = /^\[(.*)\]$/s;
2852
+ var LEGACY_LIST_SEPARATOR = ",";
2853
+ var QUOTED_ENTRY = /^(['"])(.*)\1$/s;
2854
+ var unquotedEntry = (entry) => {
2855
+ const trimmed = entry.trim();
2856
+ return QUOTED_ENTRY.exec(trimmed)?.[2] ?? trimmed;
2857
+ };
2858
+ var legacyListEntries = (raw) => {
2859
+ const list = LEGACY_FLOW_LIST.exec(raw.trim());
2860
+ return list ? list[1].split(LEGACY_LIST_SEPARATOR).map(unquotedEntry) : [];
2861
+ };
2862
+ var allowlistEntries = (allowlist) => {
2863
+ if (allowlist?.kind !== "set") return [];
2864
+ if (Array.isArray(allowlist.value)) return allowlist.value;
2865
+ return typeof allowlist.value === "string" ? legacyListEntries(allowlist.value) : [];
2866
+ };
2867
+ var escalatesToHermesApprovals = (transport) => transport?.kind === "set" && transport.value === HERMES_PUSHARY_TRANSPORT;
2868
+ var hermesAlwaysAllowsShell = ({ allowlist, transport }) => escalatesToHermesApprovals(transport) && allowlistEntries(allowlist).includes(HERMES_ALWAYS_ALLOWED_SHELL_RULE);
2869
+ var skipOf = (coverage) => coverage.kind === "hermes_skips" ? { kind: "skips" } : { kind: "may_skip", doubt: coverage.doubt };
2870
+ var hermesShellGateState = ({ coverage, gate, alwaysAllowed }) => {
2871
+ if (coverage.kind === "unreadable") return { kind: "unreadable" };
2872
+ if (coverage.kind === "hermes_asks") return { kind: "hermes_asks", backend: coverage.backend };
2873
+ const skip = skipOf(coverage);
2874
+ const backend = coverage.backend;
2875
+ if (!gatesHermesShell(gate)) return { kind: "ungated", backend, skip };
2876
+ return alwaysAllowed ? { kind: "always_allowed", backend, skip } : { kind: "pushary_gates", backend, skip };
2877
+ };
2878
+ var hermesShellGateChoice = (input) => {
2879
+ if (input.requested) return "apply";
2880
+ return input.canAsk ? "ask" : "report";
2881
+ };
2882
+
2746
2883
  // src/hermes/runtime.ts
2747
2884
  import { execFile } from "child_process";
2748
2885
  import { accessSync, closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
@@ -2793,9 +2930,18 @@ var PUSHARY_VALUES = {
2793
2930
  [HERMES_APPROVAL_TRANSPORT_KEY]: HERMES_PUSHARY_TRANSPORT,
2794
2931
  [HERMES_APPROVAL_FALLBACK_KEY]: HERMES_PUSHARY_FALLBACK
2795
2932
  };
2933
+ var readingBefore = (snapshot) => snapshot.wasSet ? { kind: "set", value: snapshot.before } : { kind: "unset" };
2934
+ var valueSetupWrote = (snapshot) => {
2935
+ if (snapshot.key === HERMES_GATE_TOOLS_KEY) return gateToolsWithShell(hermesGateToolsFrom(readingBefore(snapshot)));
2936
+ return Object.hasOwn(PUSHARY_VALUES, snapshot.key) ? PUSHARY_VALUES[snapshot.key] : null;
2937
+ };
2938
+ var receiptWithSnapshot = (receipt, snapshot) => ({
2939
+ ...receipt,
2940
+ settings: [...receipt.settings.filter((setting) => setting.key !== snapshot.key), snapshot]
2941
+ });
2796
2942
  var hermesSettingRestore = (snapshot, current) => {
2797
- const ours = PUSHARY_VALUES[snapshot.key];
2798
- const stillOurs = current.kind === "set" && current.value === ours;
2943
+ const ours = valueSetupWrote(snapshot);
2944
+ const stillOurs = ours !== null && current.kind === "set" && current.value === ours;
2799
2945
  const oursBeforeSetup = snapshot.wasSet && snapshot.before === ours;
2800
2946
  return stillOurs && !oursBeforeSetup ? { kind: "restore", key: snapshot.key, snapshot } : { kind: "leave", key: snapshot.key };
2801
2947
  };
@@ -2880,13 +3026,20 @@ var commandOutput = (error) => {
2880
3026
  };
2881
3027
  var stoppedAtTimeout = (error) => isRecord(error) && error.killed === true;
2882
3028
  var runFileAsync = promisify(execFile);
3029
+ var SETTINGS_A_SERVICE_GATEWAY_NEVER_INHERITS = [
3030
+ HERMES_GATE_TOOLS_KEY,
3031
+ HERMES_TERMINAL_ENV_KEY,
3032
+ HERMES_DOCKER_VOLUMES_KEY,
3033
+ HERMES_DOCKER_MOUNT_CWD_KEY
3034
+ ];
3035
+ var environmentAsTheGatewaySeesIt = (environment) => Object.fromEntries(Object.entries(environment).filter(([name]) => !SETTINGS_A_SERVICE_GATEWAY_NEVER_INHERITS.includes(name)));
2883
3036
  var runHermesCommand = async (launcher, args, timeoutMs = HERMES_COMMAND_TIMEOUT_MS) => {
2884
3037
  const shell = needsShell(launcher.path);
2885
3038
  try {
2886
3039
  const running = runFileAsync(
2887
3040
  shell ? quotedForWindowsShell(launcher.path) : launcher.path,
2888
3041
  shell ? args.map(quotedForWindowsShell) : [...args],
2889
- { encoding: "utf-8", timeout: timeoutMs, maxBuffer: HERMES_OUTPUT_LIMIT_BYTES, shell }
3042
+ { encoding: "utf-8", timeout: timeoutMs, maxBuffer: HERMES_OUTPUT_LIMIT_BYTES, shell, env: environmentAsTheGatewaySeesIt(process.env) }
2890
3043
  );
2891
3044
  running.child.stdin?.end();
2892
3045
  const { stdout } = await running;
@@ -3362,8 +3515,28 @@ var removeHermesWiring = async (request) => {
3362
3515
 
3363
3516
  export {
3364
3517
  HERMES_INSTALL_MARKER_FILE,
3518
+ HERMES_APPROVAL_TRANSPORT_KEY,
3365
3519
  shippedHermesPlugin,
3520
+ HERMES_TERMINAL_SECTION,
3521
+ HERMES_TERMINAL_ENV_KEY,
3522
+ HERMES_GATE_TOOLS_KEY,
3523
+ HERMES_SHELL_TOOL,
3524
+ HERMES_DOCKER_VOLUMES_KEY,
3525
+ HERMES_DOCKER_MOUNT_CWD_KEY,
3526
+ HERMES_COMMAND_ALLOWLIST_KEY,
3527
+ HERMES_ALWAYS_ALLOWED_SHELL_RULE,
3528
+ hermesApprovalCoverage,
3529
+ hermesGateToolsFrom,
3530
+ gatesHermesShell,
3531
+ gateToolsWithShell,
3532
+ hermesAlwaysAllowsShell,
3533
+ hermesShellGateState,
3534
+ hermesShellGateChoice,
3535
+ settingSnapshot,
3536
+ receiptWithSnapshot,
3366
3537
  findHermesLauncher,
3538
+ readHermesSetting,
3539
+ writeHermesSetting,
3367
3540
  hermesPluginDirectory,
3368
3541
  HERMES_CONFIG_FILE,
3369
3542
  defaultHermesHome,
@@ -3378,6 +3551,8 @@ export {
3378
3551
  hermesReceiptPaths,
3379
3552
  hermesSetupWasRecorded,
3380
3553
  hermesSetupPredatesThePluginFolder,
3554
+ readSetupReceipt,
3555
+ writeSetupReceipt,
3381
3556
  installHermesPlugin,
3382
3557
  refreshHermesPluginFiles,
3383
3558
  removeHermesWiring
@@ -22,22 +22,22 @@ import {
22
22
  turnCodexAppCardsOff,
23
23
  turnCodexAppCardsOn,
24
24
  whyLaunchctlFailed
25
- } from "./chunk-FYB6HBHQ.js";
26
- import "./chunk-44Z6MQZ2.js";
25
+ } from "./chunk-RO6EVL5C.js";
26
+ import "./chunk-KGFB2LEB.js";
27
27
  import "./chunk-XCEN33UP.js";
28
28
  import "./chunk-QCGNGEGE.js";
29
- import "./chunk-ZF4CPOO7.js";
29
+ import "./chunk-WH4ODOCE.js";
30
30
  import "./chunk-H6CL3T2S.js";
31
- import "./chunk-4Z7YVVKQ.js";
32
- import "./chunk-HIIXYZYV.js";
33
- import "./chunk-BHKODM4L.js";
34
- import "./chunk-IG7NGXBS.js";
35
- import "./chunk-QFZNPZRO.js";
36
- import "./chunk-4EBJ4PTZ.js";
31
+ import "./chunk-KS3PNTRT.js";
32
+ import "./chunk-WOSO4YFQ.js";
33
+ import "./chunk-MOC2DN42.js";
34
+ import "./chunk-72LP4RPC.js";
35
+ import "./chunk-VTJ4WZOC.js";
36
+ import "./chunk-BWREST4F.js";
37
37
  import "./chunk-TODDEIYG.js";
38
- import "./chunk-R7FSMUHG.js";
38
+ import "./chunk-6LZ7MXHD.js";
39
39
  import "./chunk-WIIMW5YO.js";
40
- import "./chunk-C5LIS3XM.js";
40
+ import "./chunk-LQXUWXQP.js";
41
41
  import "./chunk-U4P4NDVB.js";
42
42
  import "./chunk-MLKGABMK.js";
43
43
  export {
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  deriveToolTarget
3
- } from "./chunk-7QWH257P.js";
3
+ } from "./chunk-JZT2LPRM.js";
4
4
  import {
5
5
  TOOL_NAME_MAX_LENGTH,
6
6
  codexElicitationToolName
7
- } from "./chunk-C5LIS3XM.js";
7
+ } from "./chunk-LQXUWXQP.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
 
10
10
  // src/codex-card-routing.ts
@@ -11,7 +11,7 @@ import {
11
11
  installedCodexApp,
12
12
  shellQuoted,
13
13
  writeDesktopProxy
14
- } from "./chunk-44Z6MQZ2.js";
14
+ } from "./chunk-KGFB2LEB.js";
15
15
  import "./chunk-XCEN33UP.js";
16
16
  import {
17
17
  CODEX_APP_LAUNCH_VARIABLE
@@ -11,38 +11,38 @@ import {
11
11
  isCodexQuestionLeftWithTheLocalClient,
12
12
  planCodexElicitation,
13
13
  reportOwnedQuestion
14
- } from "./chunk-USNGPJXF.js";
15
- import "./chunk-B4VIT5N3.js";
16
- import "./chunk-R27HXRSK.js";
17
- import "./chunk-UIZE4YMU.js";
18
- import "./chunk-DDJHPMBJ.js";
19
- import "./chunk-NQF652IG.js";
14
+ } from "./chunk-WUT3PO33.js";
15
+ import "./chunk-4L5WPDTB.js";
16
+ import "./chunk-Y7INHH5G.js";
17
+ import "./chunk-DZIZ3XFL.js";
18
+ import "./chunk-LMU6NG2G.js";
19
+ import "./chunk-PXZTCR4M.js";
20
20
  import "./chunk-CYTPQDQY.js";
21
- import "./chunk-22CXL7YI.js";
21
+ import "./chunk-IRPIMQ7U.js";
22
22
  import "./chunk-XMSDA5WA.js";
23
- import "./chunk-EMODTMHE.js";
23
+ import "./chunk-6UK4K5RS.js";
24
24
  import {
25
25
  writeDegraded
26
- } from "./chunk-CNURORKI.js";
26
+ } from "./chunk-YU5XE6VC.js";
27
27
  import "./chunk-POFBE6ZY.js";
28
- import "./chunk-V53QYJNY.js";
29
- import "./chunk-DZLLKJJT.js";
30
- import "./chunk-4OX4SJGA.js";
28
+ import "./chunk-WPBWJMBZ.js";
29
+ import "./chunk-YRJZF5RG.js";
30
+ import "./chunk-LJRPWUST.js";
31
31
  import "./chunk-H6CL3T2S.js";
32
- import "./chunk-ZVJIJPQY.js";
33
- import "./chunk-YCU7Y77H.js";
34
- import "./chunk-4Z7YVVKQ.js";
32
+ import "./chunk-BYKRAYGD.js";
33
+ import "./chunk-S7HGPKCL.js";
34
+ import "./chunk-KS3PNTRT.js";
35
35
  import "./chunk-YU2EUD7O.js";
36
- import "./chunk-IG7NGXBS.js";
37
- import "./chunk-QFZNPZRO.js";
38
- import "./chunk-4EBJ4PTZ.js";
36
+ import "./chunk-72LP4RPC.js";
37
+ import "./chunk-VTJ4WZOC.js";
38
+ import "./chunk-BWREST4F.js";
39
39
  import "./chunk-BG4HHB7E.js";
40
40
  import "./chunk-TODDEIYG.js";
41
- import "./chunk-R7FSMUHG.js";
41
+ import "./chunk-6LZ7MXHD.js";
42
42
  import "./chunk-WIIMW5YO.js";
43
43
  import {
44
44
  canonicalJsonText
45
- } from "./chunk-C5LIS3XM.js";
45
+ } from "./chunk-LQXUWXQP.js";
46
46
  import "./chunk-MLKGABMK.js";
47
47
 
48
48
  // src/codex-session-launch.ts
@@ -3,9 +3,9 @@ import {
3
3
  getApiKey,
4
4
  getBaseUrl,
5
5
  resetKeyDriftWarning
6
- } from "./chunk-4EBJ4PTZ.js";
7
- import "./chunk-R7FSMUHG.js";
8
- import "./chunk-C5LIS3XM.js";
6
+ } from "./chunk-BWREST4F.js";
7
+ import "./chunk-6LZ7MXHD.js";
8
+ import "./chunk-LQXUWXQP.js";
9
9
  import "./chunk-MLKGABMK.js";
10
10
  export {
11
11
  configFilePath,
@@ -7,20 +7,20 @@ import {
7
7
  recordUnfinishedUpgrade,
8
8
  upgradeInstalledPackage,
9
9
  withUpgradeLock
10
- } from "./chunk-PYSDCPZ3.js";
11
- import "./chunk-QQHUUPPW.js";
12
- import "./chunk-R27HXRSK.js";
13
- import "./chunk-UIZE4YMU.js";
10
+ } from "./chunk-2LGDEPVL.js";
11
+ import "./chunk-EPWOARGR.js";
12
+ import "./chunk-Y7INHH5G.js";
13
+ import "./chunk-DZIZ3XFL.js";
14
14
  import "./chunk-CYTPQDQY.js";
15
- import "./chunk-MOYEHNFP.js";
15
+ import "./chunk-MKHVLDMN.js";
16
16
  import "./chunk-POFBE6ZY.js";
17
- import "./chunk-IG7NGXBS.js";
18
- import "./chunk-QFZNPZRO.js";
17
+ import "./chunk-72LP4RPC.js";
18
+ import "./chunk-VTJ4WZOC.js";
19
19
  import "./chunk-BG4HHB7E.js";
20
20
  import "./chunk-TODDEIYG.js";
21
- import "./chunk-R7FSMUHG.js";
21
+ import "./chunk-6LZ7MXHD.js";
22
22
  import "./chunk-WIIMW5YO.js";
23
- import "./chunk-C5LIS3XM.js";
23
+ import "./chunk-LQXUWXQP.js";
24
24
  import "./chunk-MLKGABMK.js";
25
25
  export {
26
26
  finishGlobalUpgrade,
@@ -2,12 +2,12 @@ import {
2
2
  getMachineId,
3
3
  getOrCreateMachineId,
4
4
  readExistingMachineId
5
- } from "./chunk-4Z7YVVKQ.js";
6
- import "./chunk-IG7NGXBS.js";
7
- import "./chunk-QFZNPZRO.js";
5
+ } from "./chunk-KS3PNTRT.js";
6
+ import "./chunk-72LP4RPC.js";
7
+ import "./chunk-VTJ4WZOC.js";
8
8
  import "./chunk-TODDEIYG.js";
9
9
  import "./chunk-WIIMW5YO.js";
10
- import "./chunk-C5LIS3XM.js";
10
+ import "./chunk-LQXUWXQP.js";
11
11
  import "./chunk-MLKGABMK.js";
12
12
  export {
13
13
  getMachineId,
@@ -16,10 +16,10 @@ import {
16
16
  resolveControlPolicy,
17
17
  resolveProviderBinary,
18
18
  trackProviderInventory
19
- } from "./chunk-R27HXRSK.js";
20
- import "./chunk-UIZE4YMU.js";
19
+ } from "./chunk-Y7INHH5G.js";
20
+ import "./chunk-DZIZ3XFL.js";
21
21
  import "./chunk-CYTPQDQY.js";
22
- import "./chunk-QFZNPZRO.js";
22
+ import "./chunk-VTJ4WZOC.js";
23
23
  import "./chunk-TODDEIYG.js";
24
24
  import "./chunk-WIIMW5YO.js";
25
25
  import {
@@ -30,7 +30,7 @@ import {
30
30
  GEMINI_BRIDGE_CAPABILITY,
31
31
  GEMINI_RESUME_CAPABILITY,
32
32
  SPAWN_CAPABILITY
33
- } from "./chunk-C5LIS3XM.js";
33
+ } from "./chunk-LQXUWXQP.js";
34
34
  import "./chunk-MLKGABMK.js";
35
35
  export {
36
36
  CLAUDE_BRIDGE_CAPABILITY,
@@ -39,11 +39,11 @@ import {
39
39
  serviceOwedBackMark,
40
40
  vscodePluginDir,
41
41
  vscodePluginMcp
42
- } from "./chunk-IG7NGXBS.js";
43
- import "./chunk-QFZNPZRO.js";
42
+ } from "./chunk-72LP4RPC.js";
43
+ import "./chunk-VTJ4WZOC.js";
44
44
  import "./chunk-TODDEIYG.js";
45
45
  import "./chunk-WIIMW5YO.js";
46
- import "./chunk-C5LIS3XM.js";
46
+ import "./chunk-LQXUWXQP.js";
47
47
  import "./chunk-MLKGABMK.js";
48
48
  export {
49
49
  autoUpdateLock,