chainlesschain 0.162.184 → 0.162.185

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 (183) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-DzAl5fGx.js → AIOps-Co4ipAKx.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-DAHtMGeC.js → ActionButton-BpsoDrDq.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-BHQaexv5.js → Analytics-DMUqeBik.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-Phz_pD0n.js → AppLayout-C6fD_dXr.js} +5 -5
  7. package/src/assets/web-panel/assets/{Artifacts-DHt47kCA.js → Artifacts-DW0YlQlV.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit--Y_8FULl.js → Audit-DXtHiiFK.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-N1zchH7D.js → BackgroundAgents-BgmF62U-.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-B_BoyN30.js → Backup-D8vhVLQC.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-Gl4uQHcT.js → BaseInput-D7nCtY2X.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-Cg-eIyAZ.js → Chat-CBZLEV2G.js} +5 -5
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-CtDb-2Qu.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-D0MGR2wA.js → Checkbox-DJLkeD4M.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-BdObJEFn.js → Codegen-Cs6xO8Fd.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col--ghxXT3z.js → Col-Dtx9wxfT.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-BUFeXOqz.js → Community-DiigJg4n.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-DhQO2_Mr.js → Compact-B67oMDDL.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-3HoTqKYd.js → Compliance-BBDl-Jos.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-B3zmO4VW.js → Cowork-Ba98hSsa.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-CJDrZOcc.js → Cron-DBWQZIz7.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-SMl15obd.js → Crosschain-OcrKzxAt.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-CgTrTWrl.js → DID-mW2eSbjM.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-dXFKaKol.js → Dashboard-BmsDPmKJ.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-qQHEhCvI.js → Dropdown-CVzmCyRx.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-C_ej0MWH.js → EmailListRenderer-vVgqKzhh.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D6ajoE7I.js → FamilyGuardDashboard-CWY5t_ZJ.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-Cio2y49h.js → Federation-ZGomhjxr.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-ClJz1cYp.js → FormItemContext-DyaknbLr.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-Dr_8Maca.js → GenericCardRenderer-CmIvN1qr.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-CevLwp5u.js → Git-DQOJIYkG.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CW9YyQUY.js → Governance-CMiY-EEp.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-CQqeAyER.js → Inference-D6IXuucf.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-cuGVxHrw.js → KnowledgeGraph-6jkTL0hV.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-DIuHri5d.js → Logs-N0jD-349.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-BL1IVXm-.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-CY1VhFGx.js → Marketplace-WhYuhBxm.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-BBAbi1vt.js → McpTools-DTeO28xy.js} +5 -5
  39. package/src/assets/web-panel/assets/{Memory-BWYGHxJA.js → Memory-MSHzPSE-.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-CPIFkW7A.js → MobileBridge-Cyqs3J3W.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-DQyWxCho.js → MobileProjects-7IN2hWHg.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-Bo3GnQbx.js → Mtc-MWBnezNM.js} +5 -5
  43. package/src/assets/web-panel/assets/{MtcAudit-C8-c4rSW.js → MtcAudit-DQu35Nu-.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-ChqMTtoq.js → Multisig-CQS_M-DP.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-C48TyhvI.js → NLProgramming-WD16vQvY.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-zogUFWxp.js → Notes-DqsiSwOq.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-v-K9XpSB.js → NotificationSettings-D3LoxO-P.js} +1 -1
  48. package/src/assets/web-panel/assets/OrderTableRenderer-C5jPoTd6.js +1 -0
  49. package/src/assets/web-panel/assets/{Organization-Boi1dQfR.js → Organization-48aMBJso.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-DM5YWwgA.js → Overflow-j-a2XkhR.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-C57LKpKK.js → P2P-BYpBNYnh.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-BFkPqu6Q.js → PdhVaultBrowser-CYCsrrLg.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-D6fB9YSp.js → Permissions-ZRXGXDFb.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-Bp7EefEo.js → PersonalDataHub-6OWUPHyL.js} +5 -5
  55. package/src/assets/web-panel/assets/{Pipeline-BVln3hZf.js → Pipeline-FcjlDK9E.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-Bf8P3nF6.js → Privacy-vD_6Tldp.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-DM6wA2E2.js → ProjectInit-B_CHOLXp.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-CGbc5Iho.js → ProjectSettings-rEiqQ2da.js} +2 -2
  59. package/src/assets/web-panel/assets/{Projects-D0p9EOxm.js → Projects-Bg8obRAM.js} +1 -1
  60. package/src/assets/web-panel/assets/{Providers-B2kJcxS6.js → Providers-Dgtt97B4.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-Cxu7dXYy.js → QrScannerModal-DaGVylZO.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-DTlrTPms.js → QuickAsk-DP2_2x3u.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-Cm3X-cCj.js → Recommend-D29yj4ki.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DNb2kjvt.js → RemoteSession-Ue2wnBLp.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-CZUFFjc1.js → Reputation-CrxIHqeC.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-DAPgloxA.js → Row-BgSzJPCn.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-dsx9S793.js → RssFeed-AwhYOrP9.js} +2 -2
  68. package/src/assets/web-panel/assets/{Search-DnnC8kz-.js → Search-D2shpEcm.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-VuZknRMK.js → Security-BtKzhTbT.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-B3XNX8Vx.js → Services-BY0DsUSm.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-Dl1nVtCj.js → Skeleton-B3Sb0UI9.js} +1 -1
  72. package/src/assets/web-panel/assets/Skills-Ne5w0bDs.js +1 -0
  73. package/src/assets/web-panel/assets/{Sla-HzU56n1z.js → Sla-C8VkaP4T.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-CRle7Jl2.js → SpeechSettings-BQdIEYP9.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-DWadz7Sl.js → SyncSettings-CwFafrUg.js} +2 -2
  76. package/src/assets/web-panel/assets/Tasks-yJuKoDlR.js +1 -0
  77. package/src/assets/web-panel/assets/{Templates-BBAgvrlM.js → Templates-DYn9qCmx.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-BsbbvGN8.js → Tenant-DkKReHvn.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-uscpZI4q.js → Terminal-r6bd-owt.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-mTyjNE6r.js → TimelineRenderer-Mq2juUC3.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-Bz97oBgs.js → Tokens-BnMw7MRi.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-BTRKC140.js → Trigger-CTzKp4o9.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-B4A_KuyX.js → Trust-BBrSZm9U.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-DES9TmLU.js → UkeySign-D9fDvEMq.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-ClXs7Qxp.js → VideoEditing-DUi5ve-0.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-IzZqFz-7.js → Wallet-C8Fi1P8_.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-BtOP04g_.js → WebAuthn-s1LppYi2.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-_0cqnopY.js → WorkflowEditor-DJL2C4lj.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-ax7193uU.js → chat-D3F2ndOB.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-Bu4WGJah.js → colors-B7739-e3.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-BX1dlTW0.js → compact-item-CcJbFTF4.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CktA_Dj7.js → createContext-CuP_DEFS.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-Cj_36GHq.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-D5wd817l.js → hasIn-6YEXfIik.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-C3HmzCLt.js → index-4h3lpWJ3.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CjEJjl5C.js → index-BIID4XcP.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BQtoRyTO.js → index-BOHUo3aM.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CP8ELJBc.js → index-BYYSLEjn.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CfzxR61U.js → index-Bh4hcNWW.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DmPJ6T6F.js → index-BhIKnQAW.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C8DkWyNr.js → index-Bru_kpZl.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-iHcPtQ2e.js → index-BsdSnHQE.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D9op8poE.js → index-BxSqVMGh.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BcGdL3sG.js → index-C3ZA_h1j.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DmVUeWUk.js → index-C3pctdCb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Cv7bVWJ5.js → index-CRuoRapN.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-D-o4Rwjk.js → index-CY0zd_jc.js} +1 -1
  108. package/src/assets/web-panel/assets/index-CezFrAmD.js +1 -0
  109. package/src/assets/web-panel/assets/index-Cxgguez6.js +1 -0
  110. package/src/assets/web-panel/assets/{index-Ci3y1ZAv.js → index-CxradkS6.js} +19 -19
  111. package/src/assets/web-panel/assets/{index-CUForzsG.js → index-D0K_rFC1.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BLLsUNDc.js → index-D0zdPyTD.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CeVZaJSC.js → index-D3haorUt.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-o14NNnxw.js → index-D6Ij_2uw.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Db3CpWTr.js → index-DDRlBaDt.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-h3KJZr95.js → index-DGsE7MQz.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BAtBNfkr.js → index-DHvxXXNA.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DF1TysIF.js → index-DV11jpeO.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-5PYYkr8S.js → index-DXdnfDVK.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-tYPSfGbd.js → index-D_75jYWk.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-jzlzm4VK.js → index-DjI9MkId.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D_cycOET.js → index-DlSvpNMf.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Dzdr0zDf.js → index-Dox44j29.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CYRE0NJA.js → index-DpEKmjoa.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Bad2CYeH.js → index-Dsy7_x4s.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-C8tLR2TG.js → index-Gxw09e9R.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-ytp2H5Zx.js → index-LfRlSmqe.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DkD1K-fF.js → index-Nz-MUHD1.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BRfXoRuh.js → index-b9cuvFEv.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DYo9S5OT.js → index-eR6H6NR7.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-A4uBPQCz.js → index-fWgUQZ9d.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-PBLzwYnu.js → index-mfWQRSLn.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-bFCaM7Eq.js → index-yOZYSNmG.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-D2Iut83B.js → initDefaultProps-CJ1Bq2L3.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BuaJVdK9.js → motion-rYJN_vzf.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-CaEuM92y.js → move-DnDK2gTP.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-wuKNBwys.js → mtc-parser-CKnXy7ES.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-7a3eklaq.js → omit-3nRc3hWT.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-Dn4z7KvJ.js → pickAttrs-Ddjj3sLu.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-DVn_qT4-.js → placementArrow-CbsbphQY.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-Dodz1OMQ.js → responsiveObserve-D9aZ4VDJ.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-CxoGskbp.js → slide-D4pvdFj0.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-DBh_epQH.js → statusUtils-C2W-vY-S.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-RXQ4gG5P.js → styleChecker-Cbq5150d.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-1Wxm0HEG.js → useFlexGapSupport-BA2B7R82.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-CI_ySNxJ.js → useFs-CLrwg3xg.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-C6973rZP.js → usePersonalDataHub-DQeAI5qs.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-CalynwKr.js → vnode-8BDGy-YU.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-CM1cd-Q0.js → zoom-B2SzFprl.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/data/changelog.json +13 -0
  152. package/src/gateways/terminal/PtyManager.js +45 -15
  153. package/src/gateways/ws/ws-agent-handler.js +15 -1
  154. package/src/gateways/ws/ws-session-gateway.js +88 -0
  155. package/src/harness/background-task-command-runner.js +234 -0
  156. package/src/harness/background-task-manager.js +150 -20
  157. package/src/harness/background-task-worker.js +17 -11
  158. package/src/lib/background-agent-supervisor.js +46 -0
  159. package/src/lib/event-runtime-host.js +12 -10
  160. package/src/lib/hook-runner.cjs +7 -1
  161. package/src/lib/hook-runner.js +2 -0
  162. package/src/lib/hooks-v2-runtime.js +94 -6
  163. package/src/lib/hooks-v2-workspace-context.js +258 -0
  164. package/src/lib/lsp/__tests__/lsp-client.test.js +1 -4
  165. package/src/lib/plugin-runtime/bin.js +20 -4
  166. package/src/lib/plugin-runtime/sandbox-policy.js +1 -4
  167. package/src/lib/process-execution-broker/index.js +706 -40
  168. package/src/lib/process-execution-broker/linux-generic-bwrap-runtime.js +212 -2
  169. package/src/lib/process-execution-broker/linux-generic-bwrap.js +124 -6
  170. package/src/lib/process-execution-broker/platform-sandbox.js +6 -2
  171. package/src/repl/agent-repl.js +8 -0
  172. package/src/runtime/agent-core.js +272 -38
  173. package/src/runtime/headless-runner.js +11 -1
  174. package/src/runtime/headless-stream.js +8 -0
  175. package/src/workers/background-agent-worker.js +5 -11
  176. package/src/assets/web-panel/assets/ChatBubbleRenderer-BzeUi3JH.js +0 -1
  177. package/src/assets/web-panel/assets/MarkdownRenderer-R91Qw5JA.js +0 -1
  178. package/src/assets/web-panel/assets/OrderTableRenderer-P94JGGNz.js +0 -1
  179. package/src/assets/web-panel/assets/Skills-CY09CZ_W.js +0 -1
  180. package/src/assets/web-panel/assets/Tasks-CNVTDGg8.js +0 -1
  181. package/src/assets/web-panel/assets/devWarning-x877iKKS.js +0 -1
  182. package/src/assets/web-panel/assets/index-D7pfPouB.js +0 -1
  183. package/src/assets/web-panel/assets/index-e4i-VLBd.js +0 -1
@@ -27,6 +27,7 @@ import crypto from "node:crypto";
27
27
  import os from "node:os";
28
28
  import path from "node:path";
29
29
  import * as fs from "node:fs";
30
+ import * as tty from "node:tty";
30
31
  import { createRequire } from "node:module";
31
32
 
32
33
  const require = createRequire(import.meta.url);
@@ -56,7 +57,6 @@ const SUPPORTED_SANDBOX_PROFILES = new Set([
56
57
 
57
58
  // 延迟导入避免循环依赖
58
59
  let _traceCtx = null;
59
- let _hooksV2 = null;
60
60
  const _ipcBus = null;
61
61
 
62
62
  function getTraceCtx() {
@@ -74,15 +74,428 @@ function getRpl() {
74
74
  return runtimeProvenanceLedger;
75
75
  }
76
76
 
77
- function getHooksV2() {
78
- if (!_hooksV2) {
77
+ function createNativePtyAdapter() {
78
+ const MAX_PENDING_WRITE_BYTES = 1024 * 1024;
79
+ const terminalStates = new WeakMap();
80
+ const blockingSlaveOwners = new Map();
81
+ const closeStream = (stream) => {
79
82
  try {
80
- _hooksV2 = require("../hooks-v2-runtime.js");
83
+ stream?.destroy?.();
81
84
  } catch {
82
- _hooksV2 = null;
85
+ // PTY cleanup is best-effort after the owning process has terminated.
86
+ }
87
+ };
88
+ const terminalState = (terminal) => {
89
+ const state = terminalStates.get(terminal);
90
+ if (!state) {
91
+ throw new Error("pty_terminal_state_unavailable");
92
+ }
93
+ return state;
94
+ };
95
+ const invalidateMasterState = (state) => {
96
+ state.disposed = true;
97
+ state.fd = null;
98
+ state.writeQueue.length = 0;
99
+ state.pendingWriteBytes = 0;
100
+ if (state.writeImmediate) {
101
+ clearImmediate(state.writeImmediate);
102
+ state.writeImmediate = null;
103
+ }
104
+ };
105
+ const hasLiveMaster = (state) =>
106
+ !state.disposed &&
107
+ Number.isInteger(state.fd) &&
108
+ state.master?.destroyed !== true &&
109
+ state.master?.closed !== true;
110
+ const processWriteQueue = (state) => {
111
+ if (!hasLiveMaster(state)) {
112
+ invalidateMasterState(state);
113
+ return;
83
114
  }
84
- }
85
- return _hooksV2;
115
+ if (state.writeQueue.length === 0) {
116
+ return;
117
+ }
118
+ const task = state.writeQueue[0];
119
+ let written = 0;
120
+ try {
121
+ // The node-pty master is non-blocking. Keep the syscall synchronous so
122
+ // releaseTerminal() can never close and recycle this numeric fd while a
123
+ // libuv fs.write request still refers to it. Bound each turn for event
124
+ // loop fairness; EAGAIN is retried from the next turn.
125
+ written = fs.writeSync(
126
+ state.fd,
127
+ task.buffer,
128
+ task.offset,
129
+ Math.min(task.buffer.byteLength - task.offset, 64 * 1024),
130
+ null,
131
+ );
132
+ } catch (error) {
133
+ if (error.code !== "EAGAIN" && error.code !== "EWOULDBLOCK") {
134
+ invalidateMasterState(state);
135
+ }
136
+ }
137
+ if (written > 0) {
138
+ task.offset += written;
139
+ state.pendingWriteBytes -= written;
140
+ if (task.offset >= task.buffer.byteLength) {
141
+ state.writeQueue.shift();
142
+ }
143
+ }
144
+ if (
145
+ !state.disposed &&
146
+ state.writeQueue.length > 0 &&
147
+ !state.writeImmediate
148
+ ) {
149
+ state.writeImmediate = setImmediate(() => {
150
+ state.writeImmediate = null;
151
+ processWriteQueue(state);
152
+ });
153
+ }
154
+ };
155
+ return {
156
+ allocate(ptyModule, options) {
157
+ if (
158
+ typeof ptyModule?.native?.open !== "function" ||
159
+ typeof ptyModule.native.resize !== "function"
160
+ ) {
161
+ throw new TypeError("pty_module_native_open_unavailable");
162
+ }
163
+ const cols = options?.cols || 80;
164
+ const rows = options?.rows || 24;
165
+ if (
166
+ !Number.isInteger(cols) ||
167
+ cols <= 0 ||
168
+ cols > 65_535 ||
169
+ !Number.isInteger(rows) ||
170
+ rows <= 0 ||
171
+ rows > 65_535
172
+ ) {
173
+ throw new TypeError("pty_dimensions_invalid");
174
+ }
175
+ let opened;
176
+ let master = null;
177
+ try {
178
+ opened = ptyModule.native.open(cols, rows);
179
+ if (
180
+ !Number.isInteger(opened?.master) ||
181
+ opened.master < 0 ||
182
+ !Number.isInteger(opened?.slave) ||
183
+ opened.slave < 0 ||
184
+ opened.master === opened.slave ||
185
+ typeof opened?.pty !== "string" ||
186
+ !/^\/dev\/pts\/\d+$/.test(opened.pty)
187
+ ) {
188
+ throw new Error("pty_native_identity_unavailable");
189
+ }
190
+ const masterStat = fs.fstatSync(opened.master);
191
+ const slaveStat = fs.fstatSync(opened.slave);
192
+ const ptmxStat = fs.statSync("/dev/ptmx");
193
+ const pathStat = fs.statSync(opened.pty);
194
+ if (
195
+ !masterStat.isCharacterDevice() ||
196
+ !slaveStat.isCharacterDevice() ||
197
+ !ptmxStat.isCharacterDevice() ||
198
+ !pathStat.isCharacterDevice() ||
199
+ String(masterStat.dev) !== String(ptmxStat.dev) ||
200
+ String(masterStat.rdev) !== String(ptmxStat.rdev) ||
201
+ String(pathStat.dev) !== String(slaveStat.dev) ||
202
+ String(pathStat.rdev) !== String(slaveStat.rdev)
203
+ ) {
204
+ throw new Error("pty_native_identity_invalid");
205
+ }
206
+ master = new tty.ReadStream(opened.master);
207
+ const encoding =
208
+ options?.encoding === null
209
+ ? null
210
+ : typeof options?.encoding === "string"
211
+ ? options.encoding
212
+ : "utf8";
213
+ if (encoding !== null) {
214
+ master.setEncoding(encoding);
215
+ }
216
+ const terminal = {
217
+ master,
218
+ ptsName: opened.pty,
219
+ };
220
+ const state = {
221
+ fd: opened.master,
222
+ rawSlaveFd: opened.slave,
223
+ blockingSlaveFd: null,
224
+ native: ptyModule.native,
225
+ cols,
226
+ rows,
227
+ encoding: encoding || "utf8",
228
+ writeQueue: [],
229
+ pendingWriteBytes: 0,
230
+ writeImmediate: null,
231
+ disposed: false,
232
+ readError: null,
233
+ master,
234
+ };
235
+ terminalStates.set(terminal, state);
236
+ master.on("error", (error) => {
237
+ state.readError = error;
238
+ invalidateMasterState(state);
239
+ });
240
+ master.once("close", () => invalidateMasterState(state));
241
+ return terminal;
242
+ } catch (error) {
243
+ if (master) {
244
+ closeStream(master);
245
+ } else if (Number.isInteger(opened?.master)) {
246
+ try {
247
+ fs.closeSync(opened.master);
248
+ } catch {
249
+ // Preserve the allocation failure.
250
+ }
251
+ }
252
+ if (Number.isInteger(opened?.slave)) {
253
+ try {
254
+ fs.closeSync(opened.slave);
255
+ } catch {
256
+ // Preserve the allocation failure.
257
+ }
258
+ }
259
+ throw error;
260
+ }
261
+ },
262
+ openBlockingSlave(terminal) {
263
+ const ptsName = terminal?.ptsName;
264
+ const state = terminalState(terminal);
265
+ const originalFd = state.rawSlaveFd;
266
+ if (
267
+ typeof ptsName !== "string" ||
268
+ !/^\/dev\/pts\/\d+$/.test(ptsName) ||
269
+ !Number.isInteger(originalFd)
270
+ ) {
271
+ throw new Error("pty_slave_identity_unavailable");
272
+ }
273
+ const originalStat = fs.fstatSync(originalFd);
274
+ if (!originalStat.isCharacterDevice()) {
275
+ throw new Error("pty_slave_not_character_device");
276
+ }
277
+ const flags =
278
+ Number(fs.constants.O_RDWR) |
279
+ Number(fs.constants.O_NOCTTY || 0) |
280
+ Number(fs.constants.O_CLOEXEC || 0);
281
+ const fd = fs.openSync(ptsName, flags);
282
+ try {
283
+ const openedStat = fs.fstatSync(fd);
284
+ const pathStat = fs.statSync(ptsName);
285
+ if (
286
+ !openedStat.isCharacterDevice() ||
287
+ !pathStat.isCharacterDevice() ||
288
+ String(openedStat.dev) !== String(originalStat.dev) ||
289
+ String(openedStat.rdev) !== String(originalStat.rdev) ||
290
+ String(pathStat.dev) !== String(originalStat.dev) ||
291
+ String(pathStat.rdev) !== String(originalStat.rdev)
292
+ ) {
293
+ throw new Error("pty_slave_identity_changed");
294
+ }
295
+ fs.closeSync(originalFd);
296
+ state.rawSlaveFd = null;
297
+ state.blockingSlaveFd = fd;
298
+ blockingSlaveOwners.set(fd, state);
299
+ return fd;
300
+ } catch (error) {
301
+ fs.closeSync(fd);
302
+ throw error;
303
+ }
304
+ },
305
+ closeFd(fd) {
306
+ fs.closeSync(fd);
307
+ const state = blockingSlaveOwners.get(fd);
308
+ if (state) {
309
+ state.blockingSlaveFd = null;
310
+ blockingSlaveOwners.delete(fd);
311
+ }
312
+ },
313
+ getCols(terminal) {
314
+ return terminalState(terminal).cols;
315
+ },
316
+ getRows(terminal) {
317
+ return terminalState(terminal).rows;
318
+ },
319
+ onData(terminal, listener) {
320
+ const master = terminal?.master;
321
+ if (typeof master?.on !== "function") {
322
+ throw new Error("pty_master_stream_unavailable");
323
+ }
324
+ master.on("data", listener);
325
+ return {
326
+ dispose() {
327
+ if (typeof master.off === "function") {
328
+ master.off("data", listener);
329
+ } else {
330
+ master.removeListener?.("data", listener);
331
+ }
332
+ },
333
+ };
334
+ },
335
+ write(terminal, data) {
336
+ const state = terminalState(terminal);
337
+ if (!hasLiveMaster(state)) {
338
+ invalidateMasterState(state);
339
+ return;
340
+ }
341
+ const buffer =
342
+ typeof data === "string"
343
+ ? Buffer.from(data, state.encoding)
344
+ : Buffer.from(data);
345
+ if (buffer.byteLength === 0) return;
346
+ if (
347
+ buffer.byteLength > MAX_PENDING_WRITE_BYTES ||
348
+ state.pendingWriteBytes > MAX_PENDING_WRITE_BYTES - buffer.byteLength
349
+ ) {
350
+ const error = new Error("pty_write_queue_limit_exceeded");
351
+ error.code = "ERR_PTY_WRITE_BACKPRESSURE";
352
+ throw error;
353
+ }
354
+ state.writeQueue.push({ buffer, offset: 0 });
355
+ state.pendingWriteBytes += buffer.byteLength;
356
+ processWriteQueue(state);
357
+ },
358
+ resize(terminal, cols, rows) {
359
+ const state = terminalState(terminal);
360
+ if (!hasLiveMaster(state)) {
361
+ invalidateMasterState(state);
362
+ const error = new Error("pty_terminal_closed");
363
+ error.code = "ERR_PTY_TERMINAL_CLOSED";
364
+ throw error;
365
+ }
366
+ if (
367
+ !Number.isInteger(cols) ||
368
+ cols <= 0 ||
369
+ cols > 65_535 ||
370
+ !Number.isInteger(rows) ||
371
+ rows <= 0 ||
372
+ rows > 65_535
373
+ ) {
374
+ throw new TypeError("pty_dimensions_invalid");
375
+ }
376
+ try {
377
+ state.native.resize(state.fd, cols, rows);
378
+ } catch (error) {
379
+ invalidateMasterState(state);
380
+ throw error;
381
+ }
382
+ state.cols = cols;
383
+ state.rows = rows;
384
+ },
385
+ clear() {},
386
+ pause(terminal) {
387
+ return terminal?.master?.pause?.();
388
+ },
389
+ resume(terminal) {
390
+ return terminal?.master?.resume?.();
391
+ },
392
+ setEncoding(terminal, encoding) {
393
+ const master = terminal?.master;
394
+ if (master?._decoder) {
395
+ delete master._decoder;
396
+ }
397
+ if (encoding) {
398
+ master?.setEncoding?.(encoding);
399
+ }
400
+ },
401
+ releaseTerminal(terminal) {
402
+ const state = terminalStates.get(terminal);
403
+ if (state) {
404
+ invalidateMasterState(state);
405
+ if (Number.isInteger(state.rawSlaveFd)) {
406
+ try {
407
+ fs.closeSync(state.rawSlaveFd);
408
+ } catch {
409
+ // Closing the master below remains authoritative.
410
+ }
411
+ state.rawSlaveFd = null;
412
+ }
413
+ if (Number.isInteger(state.blockingSlaveFd)) {
414
+ try {
415
+ fs.closeSync(state.blockingSlaveFd);
416
+ } catch {
417
+ // The descriptor may already have been closed by the launch path.
418
+ }
419
+ blockingSlaveOwners.delete(state.blockingSlaveFd);
420
+ state.blockingSlaveFd = null;
421
+ }
422
+ }
423
+ closeStream(terminal?.master);
424
+ },
425
+ };
426
+ }
427
+
428
+ function wrapSandboxedPty(terminal, child, command, ptyAdapter) {
429
+ const exitEmitter = new EventEmitter();
430
+ let exitState = null;
431
+ let closed = false;
432
+ const finish = (exitCode, signal) => {
433
+ if (closed) return;
434
+ closed = true;
435
+ exitState = {
436
+ exitCode: Number.isInteger(exitCode) ? exitCode : 1,
437
+ signal: signal ?? null,
438
+ };
439
+ exitEmitter.emit("exit", exitState);
440
+ };
441
+ child.once("exit", finish);
442
+ child.once("error", () => finish(1, null));
443
+
444
+ return {
445
+ pid: child.pid,
446
+ process: command,
447
+ childProcess: child,
448
+ get cols() {
449
+ return ptyAdapter.getCols(terminal);
450
+ },
451
+ get rows() {
452
+ return ptyAdapter.getRows(terminal);
453
+ },
454
+ onData(listener) {
455
+ return ptyAdapter.onData(terminal, listener);
456
+ },
457
+ onExit(listener) {
458
+ if (exitState) {
459
+ queueMicrotask(() => listener(exitState));
460
+ return { dispose() {} };
461
+ }
462
+ exitEmitter.once("exit", listener);
463
+ return {
464
+ dispose() {
465
+ exitEmitter.off("exit", listener);
466
+ },
467
+ };
468
+ },
469
+ write(data) {
470
+ return ptyAdapter.write(terminal, data);
471
+ },
472
+ resize(cols, rows) {
473
+ if (closed) {
474
+ const error = new Error("pty_terminal_closed");
475
+ error.code = "ERR_PTY_TERMINAL_CLOSED";
476
+ throw error;
477
+ }
478
+ return ptyAdapter.resize(terminal, cols, rows);
479
+ },
480
+ clear() {
481
+ return ptyAdapter.clear(terminal);
482
+ },
483
+ pause() {
484
+ return ptyAdapter.pause(terminal);
485
+ },
486
+ resume() {
487
+ return ptyAdapter.resume(terminal);
488
+ },
489
+ setEncoding(encoding) {
490
+ return ptyAdapter.setEncoding(terminal, encoding);
491
+ },
492
+ kill(signal = "SIGHUP") {
493
+ return child.kill(signal);
494
+ },
495
+ destroy() {
496
+ return child.kill("SIGHUP");
497
+ },
498
+ };
86
499
  }
87
500
 
88
501
  class ProcessExecutionBroker extends EventEmitter {
@@ -122,6 +535,8 @@ class ProcessExecutionBroker extends EventEmitter {
122
535
  applySandbox: _applySandbox,
123
536
  postSpawnSandbox: _postSpawnSandbox,
124
537
  };
538
+ this._ptyAdapter = createNativePtyAdapter();
539
+ this._hooksEventSink = null;
125
540
 
126
541
  this._ensureLogDir();
127
542
  this._loadPermissions();
@@ -277,6 +692,7 @@ class ProcessExecutionBroker extends EventEmitter {
277
692
  cwd: options.cwd || process.cwd(),
278
693
  shell: options.shell,
279
694
  sync: launch?.sync === true,
695
+ pty: launch?.pty === true,
280
696
  stdio: options.stdio,
281
697
  requiredBoundaries,
282
698
  });
@@ -435,7 +851,7 @@ class ProcessExecutionBroker extends EventEmitter {
435
851
  args = [],
436
852
  options = {},
437
853
  trustedWorkspaceRoot = process.cwd(),
438
- { sync = false } = {},
854
+ { sync = false, pty = false } = {},
439
855
  ) {
440
856
  const requiredBoundaries = [
441
857
  ...new Set([
@@ -465,6 +881,7 @@ class ProcessExecutionBroker extends EventEmitter {
465
881
  workspaceRoot: trustedWorkspaceRoot,
466
882
  shell: options.shell,
467
883
  sync,
884
+ pty,
468
885
  stdio: options.stdio,
469
886
  requiredBoundaries,
470
887
  detached: options.detached,
@@ -707,6 +1124,7 @@ class ProcessExecutionBroker extends EventEmitter {
707
1124
  let genericWorkspaceProbe = false;
708
1125
  let sanitizedFilesystemPolicy = null;
709
1126
  let sanitizedNetworkPolicy = null;
1127
+ let sanitizedPtyPolicy = null;
710
1128
  if (plan.runtimeProbe !== null && plan.runtimeProbe !== undefined) {
711
1129
  if (
712
1130
  typeof plan.runtimeProbe !== "object" ||
@@ -1085,6 +1503,7 @@ class ProcessExecutionBroker extends EventEmitter {
1085
1503
  "contractDigest",
1086
1504
  "policyDigest",
1087
1505
  "mountTopologyDigest",
1506
+ "sourceMountSetDigest",
1088
1507
  "emptyRoot",
1089
1508
  "undeclaredRootReadOnly",
1090
1509
  "workspaceReadWrite",
@@ -1099,11 +1518,13 @@ class ProcessExecutionBroker extends EventEmitter {
1099
1518
  "socketCreationDenied",
1100
1519
  "descriptorMounts",
1101
1520
  "mountTopologyAtomic",
1521
+ "sourceMountPropagationPrivateAtAttestation",
1102
1522
  ];
1103
1523
  for (const field of [
1104
1524
  "contractDigest",
1105
1525
  "policyDigest",
1106
1526
  "mountTopologyDigest",
1527
+ "sourceMountSetDigest",
1107
1528
  ]) {
1108
1529
  if (
1109
1530
  plan.runtimeProbe[field] !== undefined &&
@@ -1116,7 +1537,7 @@ class ProcessExecutionBroker extends EventEmitter {
1116
1537
  );
1117
1538
  }
1118
1539
  }
1119
- for (const field of genericWorkspaceEvidenceFields.slice(3)) {
1540
+ for (const field of genericWorkspaceEvidenceFields.slice(4)) {
1120
1541
  if (
1121
1542
  plan.runtimeProbe[field] !== undefined &&
1122
1543
  typeof plan.runtimeProbe[field] !== "boolean"
@@ -1143,6 +1564,30 @@ class ProcessExecutionBroker extends EventEmitter {
1143
1564
  if (genericWorkspaceKind) {
1144
1565
  const filesystemPolicy = plan.filesystemPolicy;
1145
1566
  const networkPolicy = plan.networkPolicy;
1567
+ const ptyPolicy = plan.ptyPolicy ?? null;
1568
+ const ptyPlan =
1569
+ ptyPolicy !== null &&
1570
+ typeof ptyPolicy === "object" &&
1571
+ !Array.isArray(ptyPolicy);
1572
+ const ptyPlanValid = ptyPlan
1573
+ ? ptyPolicy.mode === "dedicated-controlling-terminal" &&
1574
+ ptyPolicy.launcherPath === "/usr/bin/setsid" &&
1575
+ typeof ptyPolicy.launcherSha256 === "string" &&
1576
+ /^[a-f0-9]{64}$/.test(ptyPolicy.launcherSha256) &&
1577
+ Number.isSafeInteger(ptyPolicy.launcherBytes) &&
1578
+ ptyPolicy.launcherBytes > 0 &&
1579
+ ptyPolicy.launcherBytes <= 256 * 1024 * 1024 &&
1580
+ ptyPolicy.launcherDescriptorBound === true &&
1581
+ ptyPolicy.launcherExecutablePinned === true &&
1582
+ ptyPolicy.launcherDescriptorConsumedBeforeTarget === true &&
1583
+ ptyPolicy.launcherStagingPathHidden === true &&
1584
+ ptyPolicy.bwrapNewSession === false &&
1585
+ plan.args[0] === "--ctty" &&
1586
+ /^\/proc\/self\/fd\/\d+$/.test(plan.args[1] || "") &&
1587
+ !plan.args.includes("--new-session")
1588
+ : ptyPolicy === null &&
1589
+ plan.args[0] !== "--ctty" &&
1590
+ plan.args.includes("--new-session");
1146
1591
  const workspaceRoot = filesystemPolicy?.workspaceRoot;
1147
1592
  const workingDirectory = filesystemPolicy?.workingDirectory;
1148
1593
  const workspaceRelative =
@@ -1172,6 +1617,10 @@ class ProcessExecutionBroker extends EventEmitter {
1172
1617
  plan.runtimeProbe.mountTopologyAtomic === false &&
1173
1618
  plan.runtimeProbe.mountTopologyDigest ===
1174
1619
  filesystemPolicy?.mountTopologyDigest &&
1620
+ plan.runtimeProbe.sourceMountSetDigest ===
1621
+ filesystemPolicy?.sourceMountSetDigest &&
1622
+ plan.runtimeProbe.sourceMountPropagationPrivateAtAttestation ===
1623
+ true &&
1175
1624
  [
1176
1625
  "emptyRoot",
1177
1626
  "undeclaredRootReadOnly",
@@ -1208,6 +1657,9 @@ class ProcessExecutionBroker extends EventEmitter {
1208
1657
  "no-strict-descendants-or-forbidden-root-aliases-at-attestation" &&
1209
1658
  filesystemPolicy.mountTopologySource === "proc-self-mountinfo" &&
1210
1659
  /^[a-f0-9]{64}$/.test(filesystemPolicy.mountTopologyDigest || "") &&
1660
+ /^[a-f0-9]{64}$/.test(filesystemPolicy.sourceMountSetDigest || "") &&
1661
+ filesystemPolicy.sourceMountPropagationPrivateAtAttestation ===
1662
+ true &&
1211
1663
  filesystemPolicy.mountTopologyAtomic === false &&
1212
1664
  Array.isArray(filesystemPolicy.anonymousWritablePaths) &&
1213
1665
  filesystemPolicy.anonymousWritablePaths.length === 5 &&
@@ -1227,7 +1679,8 @@ class ProcessExecutionBroker extends EventEmitter {
1227
1679
  plan.args.includes("--remount-ro") &&
1228
1680
  plan.args.includes("--unshare-net") &&
1229
1681
  plan.args.includes("--seccomp") &&
1230
- !plan.args.includes("--ro-bind");
1682
+ !plan.args.includes("--ro-bind") &&
1683
+ ptyPlanValid;
1231
1684
  if (!genericWorkspaceProbe) {
1232
1685
  throw this._sandboxError(
1233
1686
  "invalid_sandbox_plan",
@@ -1252,6 +1705,9 @@ class ProcessExecutionBroker extends EventEmitter {
1252
1705
  workspaceMountTopology: filesystemPolicy.workspaceMountTopology,
1253
1706
  mountTopologySource: filesystemPolicy.mountTopologySource,
1254
1707
  mountTopologyDigest: filesystemPolicy.mountTopologyDigest,
1708
+ sourceMountSetDigest: filesystemPolicy.sourceMountSetDigest,
1709
+ sourceMountPropagationPrivateAtAttestation:
1710
+ filesystemPolicy.sourceMountPropagationPrivateAtAttestation,
1255
1711
  mountTopologyAtomic: filesystemPolicy.mountTopologyAtomic,
1256
1712
  });
1257
1713
  sanitizedNetworkPolicy = Object.freeze({
@@ -1259,6 +1715,20 @@ class ProcessExecutionBroker extends EventEmitter {
1259
1715
  namespaceIdentityChanged: networkPolicy.namespaceIdentityChanged,
1260
1716
  seccomp: networkPolicy.seccomp,
1261
1717
  });
1718
+ sanitizedPtyPolicy = ptyPlan
1719
+ ? Object.freeze({
1720
+ mode: ptyPolicy.mode,
1721
+ launcherPath: ptyPolicy.launcherPath,
1722
+ launcherSha256: ptyPolicy.launcherSha256,
1723
+ launcherBytes: ptyPolicy.launcherBytes,
1724
+ launcherDescriptorBound: ptyPolicy.launcherDescriptorBound,
1725
+ launcherExecutablePinned: ptyPolicy.launcherExecutablePinned,
1726
+ launcherDescriptorConsumedBeforeTarget:
1727
+ ptyPolicy.launcherDescriptorConsumedBeforeTarget,
1728
+ launcherStagingPathHidden: ptyPolicy.launcherStagingPathHidden,
1729
+ bwrapNewSession: ptyPolicy.bwrapNewSession,
1730
+ })
1731
+ : null;
1262
1732
  }
1263
1733
  runtimeProbe = {
1264
1734
  kind: plan.runtimeProbe.kind,
@@ -1365,6 +1835,7 @@ class ProcessExecutionBroker extends EventEmitter {
1365
1835
  ? {
1366
1836
  filesystemPolicy: sanitizedFilesystemPolicy,
1367
1837
  networkPolicy: sanitizedNetworkPolicy,
1838
+ ptyPolicy: sanitizedPtyPolicy,
1368
1839
  }
1369
1840
  : {}),
1370
1841
  postSpawn: { ...postSpawn },
@@ -1378,9 +1849,18 @@ class ProcessExecutionBroker extends EventEmitter {
1378
1849
  (boundary) => !guaranteed.has(boundary),
1379
1850
  );
1380
1851
  if (missingBoundaries.length > 0) {
1852
+ const candidateReason =
1853
+ typeof plan.reason === "string" && plan.reason
1854
+ ? `; reason=${plan.reason}${
1855
+ typeof plan.runtimeProbe?.reason === "string" &&
1856
+ plan.runtimeProbe.reason
1857
+ ? `:${plan.runtimeProbe.reason}`
1858
+ : ""
1859
+ }`
1860
+ : "";
1381
1861
  throw this._sandboxBoundaryError(
1382
1862
  "required_boundaries_unsatisfied",
1383
- `Sandbox backend ${plan.backend || plan.candidateBackend || "unavailable"} cannot satisfy required boundaries: ${missingBoundaries.join(", ")}`,
1863
+ `Sandbox backend ${plan.backend || plan.candidateBackend || "unavailable"} cannot satisfy required boundaries: ${missingBoundaries.join(", ")}${candidateReason}`,
1384
1864
  {
1385
1865
  requiredBoundaries,
1386
1866
  actualGuarantees,
@@ -1404,7 +1884,7 @@ class ProcessExecutionBroker extends EventEmitter {
1404
1884
  command,
1405
1885
  args,
1406
1886
  options,
1407
- { sync = false, sandboxPolicy = {} } = {},
1887
+ { sync = false, pty = false, sandboxPolicy = {} } = {},
1408
1888
  ) {
1409
1889
  const strict = this._sandboxStrictEnabled();
1410
1890
  const requiredBoundaries = sandboxPolicy.requiredBoundaries || [];
@@ -1443,6 +1923,7 @@ class ProcessExecutionBroker extends EventEmitter {
1443
1923
  profile,
1444
1924
  requiredBoundaries: Object.freeze([...requiredBoundaries]),
1445
1925
  sync,
1926
+ ...(pty ? { pty: true } : {}),
1446
1927
  ...(sandboxPolicy.executionContract
1447
1928
  ? { executionContract: sandboxPolicy.executionContract }
1448
1929
  : {}),
@@ -1544,6 +2025,9 @@ class ProcessExecutionBroker extends EventEmitter {
1544
2025
  workspaceMountTopology: plan.filesystemPolicy.workspaceMountTopology,
1545
2026
  mountTopologySource: plan.filesystemPolicy.mountTopologySource,
1546
2027
  mountTopologyDigest: plan.filesystemPolicy.mountTopologyDigest,
2028
+ sourceMountSetDigest: plan.filesystemPolicy.sourceMountSetDigest,
2029
+ sourceMountPropagationPrivateAtAttestation:
2030
+ plan.filesystemPolicy.sourceMountPropagationPrivateAtAttestation,
1547
2031
  mountTopologyAtomic: plan.filesystemPolicy.mountTopologyAtomic,
1548
2032
  }
1549
2033
  : null;
@@ -1554,6 +2038,21 @@ class ProcessExecutionBroker extends EventEmitter {
1554
2038
  seccomp: plan.networkPolicy.seccomp,
1555
2039
  }
1556
2040
  : null;
2041
+ auditEntry.sandboxPtyPolicy =
2042
+ genericPolicy && plan.ptyPolicy
2043
+ ? {
2044
+ mode: plan.ptyPolicy.mode,
2045
+ launcherPath: plan.ptyPolicy.launcherPath,
2046
+ launcherSha256: plan.ptyPolicy.launcherSha256,
2047
+ launcherBytes: plan.ptyPolicy.launcherBytes,
2048
+ launcherDescriptorBound: plan.ptyPolicy.launcherDescriptorBound,
2049
+ launcherExecutablePinned: plan.ptyPolicy.launcherExecutablePinned,
2050
+ launcherDescriptorConsumedBeforeTarget:
2051
+ plan.ptyPolicy.launcherDescriptorConsumedBeforeTarget,
2052
+ launcherStagingPathHidden: plan.ptyPolicy.launcherStagingPathHidden,
2053
+ bwrapNewSession: plan.ptyPolicy.bwrapNewSession,
2054
+ }
2055
+ : null;
1557
2056
  auditEntry.sandboxCandidateReason = plan?.candidateBackend
1558
2057
  ? plan?.reason || null
1559
2058
  : null;
@@ -1865,15 +2364,22 @@ class ProcessExecutionBroker extends EventEmitter {
1865
2364
  }
1866
2365
 
1867
2366
  _emitHooksEvent(event, data) {
1868
- const hooks = getHooksV2();
1869
- if (!hooks || !hooks.hooksV2) return;
2367
+ const hooks = this._hooksEventSink;
2368
+ if (!hooks) return;
1870
2369
  try {
1871
- hooks.hooksV2.emit(event, data);
2370
+ hooks.emit(event, data);
1872
2371
  } catch {
1873
2372
  // Hook reporting must not hide the process execution result.
1874
2373
  }
1875
2374
  }
1876
2375
 
2376
+ _setHooksEventSink(hooks) {
2377
+ if (hooks !== null && typeof hooks?.emit !== "function") {
2378
+ throw new TypeError("hooks event sink must expose emit()");
2379
+ }
2380
+ this._hooksEventSink = hooks;
2381
+ }
2382
+
1877
2383
  spawn(command, args, options = {}) {
1878
2384
  const executionId = crypto.randomUUID();
1879
2385
  const startTime = Date.now();
@@ -2038,14 +2544,16 @@ class ProcessExecutionBroker extends EventEmitter {
2038
2544
  );
2039
2545
  if (
2040
2546
  sandboxPlan.applied === true &&
2041
- sandboxPlan.backend === "linux-bwrap-workspace" &&
2547
+ (sandboxPlan.backend === "linux-bwrap-workspace" ||
2548
+ sandboxPlan.backend === "linux-bwrap") &&
2042
2549
  sandboxPlan.postSpawn.required === false
2043
2550
  ) {
2044
2551
  // child_process.spawn() has synchronously duplicated every stdio entry
2045
- // into the launched bwrap process before returning. Generic workspace
2046
- // plans do not need a parent-side post-spawn handle, so retaining their
2047
- // pinned mount/seccomp descriptors until a long-lived MCP/LSP/monitor
2048
- // exits would turn each child into an avoidable FD leak.
2552
+ // into the launched bwrap process before returning. Both the generic
2553
+ // workspace backend and direct Plugin-bin bwrap backend are complete
2554
+ // pre-spawn plans with no parent-side enforcement handle. Retaining
2555
+ // their pinned mount/seccomp/supervisor descriptors until a long-lived
2556
+ // process exits would turn each child into an avoidable FD leak.
2049
2557
  cleanupSandbox();
2050
2558
  }
2051
2559
  try {
@@ -2271,9 +2779,10 @@ class ProcessExecutionBroker extends EventEmitter {
2271
2779
 
2272
2780
  /**
2273
2781
  * Route a node-pty session through the same provenance and credential
2274
- * boundary as child_process execution. node-pty owns the native PTY
2275
- * allocation, so the platform sandbox is reported as unavailable here
2276
- * rather than being falsely marked as applied.
2782
+ * boundary as child_process execution. Policy-free sessions retain native
2783
+ * node-pty semantics. A policy-bearing Linux session uses node-pty only to
2784
+ * allocate a dedicated terminal; child_process then duplicates its slave
2785
+ * together with the descriptor-pinned generic bwrap plan.
2277
2786
  */
2278
2787
  spawnPty(ptyModule, command, args = [], options = {}) {
2279
2788
  if (!ptyModule || typeof ptyModule.spawn !== "function") {
@@ -2315,6 +2824,7 @@ class ProcessExecutionBroker extends EventEmitter {
2315
2824
  command,
2316
2825
  args,
2317
2826
  sync: false,
2827
+ pty: true,
2318
2828
  });
2319
2829
  auditEntry.sandboxRequired = [...sandboxPolicy.requiredBoundaries];
2320
2830
  } catch (sandboxPolicyError) {
@@ -2336,27 +2846,19 @@ class ProcessExecutionBroker extends EventEmitter {
2336
2846
  error.auditEntry = auditEntry;
2337
2847
  throw error;
2338
2848
  }
2339
- if (sandboxPolicy.requiredBoundaries.length > 0) {
2340
- const sandboxError = this._sandboxBoundaryError(
2341
- "required_boundaries_unsatisfied",
2342
- `Native PTY host cannot satisfy required boundaries: ${sandboxPolicy.requiredBoundaries.join(", ")}`,
2343
- {
2344
- requiredBoundaries: sandboxPolicy.requiredBoundaries,
2345
- missingBoundaries: sandboxPolicy.requiredBoundaries,
2346
- },
2347
- );
2348
- this._recordSandboxDenial(auditEntry, sandboxError, startTime);
2349
- sandboxError.auditEntry = auditEntry;
2350
- throw sandboxError;
2351
- }
2352
2849
 
2353
2850
  const spawnOptions = { ...options, args: [...auditEntry.args] };
2354
2851
  this._stripSandboxControlOptions(spawnOptions);
2355
2852
  this._stripPluginControlOptions(spawnOptions);
2356
- delete spawnOptions.origin;
2357
- delete spawnOptions.policy;
2358
- delete spawnOptions.scope;
2359
2853
  try {
2854
+ const terminalName =
2855
+ typeof options.name === "string" && options.name
2856
+ ? options.name
2857
+ : "xterm";
2858
+ spawnOptions.env = {
2859
+ ...(spawnOptions.env || process.env),
2860
+ TERM: terminalName,
2861
+ };
2360
2862
  if (this._credentialBoundaryEnabled()) {
2361
2863
  spawnOptions.credentialContext = this._createCredentialContext(
2362
2864
  command,
@@ -2378,7 +2880,170 @@ class ProcessExecutionBroker extends EventEmitter {
2378
2880
  }
2379
2881
  const filteredArgs = spawnOptions.args || [];
2380
2882
  delete spawnOptions.args;
2381
- const proc = ptyModule.spawn(command, filteredArgs, spawnOptions);
2883
+
2884
+ if (sandboxPolicy.requiredBoundaries.length === 0) {
2885
+ delete spawnOptions.origin;
2886
+ delete spawnOptions.policy;
2887
+ delete spawnOptions.scope;
2888
+ const proc = ptyModule.spawn(command, filteredArgs, spawnOptions);
2889
+ auditEntry.pid = proc?.pid ?? null;
2890
+ auditEntry.endTime = Date.now();
2891
+ auditEntry.durationMs = auditEntry.endTime - startTime;
2892
+ this._recordAudit(auditEntry);
2893
+ this._writeRplEntry(auditEntry, "started");
2894
+ return proc;
2895
+ }
2896
+
2897
+ let sandboxPlan;
2898
+ try {
2899
+ sandboxPlan = this._prepareSandboxPlan(
2900
+ command,
2901
+ filteredArgs,
2902
+ spawnOptions,
2903
+ {
2904
+ pty: true,
2905
+ sandboxPolicy,
2906
+ },
2907
+ );
2908
+ if (
2909
+ sandboxPlan.applied !== true ||
2910
+ sandboxPlan.backend !== "linux-bwrap-workspace" ||
2911
+ sandboxPlan.ptyPolicy?.mode !== "dedicated-controlling-terminal"
2912
+ ) {
2913
+ throw this._sandboxBoundaryError(
2914
+ "required_boundaries_unsatisfied",
2915
+ `PTY backend ${sandboxPlan.backend || sandboxPlan.candidateBackend || "unavailable"} cannot satisfy the dedicated terminal contract`,
2916
+ {
2917
+ requiredBoundaries: sandboxPolicy.requiredBoundaries,
2918
+ actualGuarantees: sandboxPlan.guarantees,
2919
+ missingBoundaries: sandboxPolicy.requiredBoundaries.filter(
2920
+ (boundary) => !sandboxPlan.guarantees.includes(boundary),
2921
+ ),
2922
+ sandboxBackend: sandboxPlan.backend,
2923
+ sandboxCandidateBackend: sandboxPlan.candidateBackend,
2924
+ sandboxRuntimeProbe: sandboxPlan.runtimeProbe,
2925
+ sandboxPolicyAttested: sandboxPlan.policyAttested,
2926
+ sandboxPolicyDigest: sandboxPlan.policyDigest,
2927
+ },
2928
+ );
2929
+ }
2930
+ } catch (sandboxError) {
2931
+ sandboxPlan?.cleanup?.();
2932
+ this._recordSandboxDenial(auditEntry, sandboxError, startTime);
2933
+ sandboxError.auditEntry = auditEntry;
2934
+ throw sandboxError;
2935
+ }
2936
+
2937
+ let terminal;
2938
+ let slaveFd = null;
2939
+ let child;
2940
+ let proc;
2941
+ let terminalReleased = false;
2942
+ const releaseTerminal = () => {
2943
+ if (terminalReleased) return;
2944
+ terminalReleased = true;
2945
+ try {
2946
+ this._ptyAdapter.releaseTerminal(terminal);
2947
+ } catch {
2948
+ // The sandbox process exit remains authoritative.
2949
+ }
2950
+ };
2951
+ try {
2952
+ terminal = this._ptyAdapter.allocate(ptyModule, {
2953
+ cols: options.cols,
2954
+ rows: options.rows,
2955
+ encoding: options.encoding,
2956
+ });
2957
+ slaveFd = this._ptyAdapter.openBlockingSlave(terminal);
2958
+ const nativeSpawnFn = this._native?.spawn || nativeSpawn;
2959
+ child = nativeSpawnFn(sandboxPlan.command, sandboxPlan.args, {
2960
+ ...sandboxPlan.options,
2961
+ stdio: [
2962
+ slaveFd,
2963
+ slaveFd,
2964
+ slaveFd,
2965
+ ...sandboxPlan.options.stdio.slice(3),
2966
+ ],
2967
+ });
2968
+ proc = wrapSandboxedPty(terminal, child, command, this._ptyAdapter);
2969
+ } catch (ptyError) {
2970
+ if (child && typeof child.kill === "function") {
2971
+ try {
2972
+ child.kill();
2973
+ } catch {
2974
+ // Continue fail-closed cleanup after a partial child launch.
2975
+ }
2976
+ }
2977
+ if (Number.isInteger(slaveFd)) {
2978
+ try {
2979
+ this._ptyAdapter.closeFd(slaveFd);
2980
+ } catch {
2981
+ // Preserve the allocation/spawn failure.
2982
+ }
2983
+ }
2984
+ sandboxPlan.cleanup?.();
2985
+ releaseTerminal();
2986
+ const sandboxError = this._sandboxBoundaryError(
2987
+ "pty_allocation_or_spawn_failed",
2988
+ `Strong Linux PTY launch failed: ${ptyError.message}`,
2989
+ {
2990
+ requiredBoundaries: sandboxPolicy.requiredBoundaries,
2991
+ actualGuarantees: [],
2992
+ missingBoundaries: sandboxPolicy.requiredBoundaries,
2993
+ sandboxBackend: sandboxPlan.backend,
2994
+ sandboxRuntimeProbe: sandboxPlan.runtimeProbe,
2995
+ sandboxPolicyAttested: sandboxPlan.policyAttested,
2996
+ sandboxPolicyDigest: sandboxPlan.policyDigest,
2997
+ },
2998
+ );
2999
+ this._recordSandboxDenial(auditEntry, sandboxError, startTime);
3000
+ sandboxError.auditEntry = auditEntry;
3001
+ throw sandboxError;
3002
+ }
3003
+ try {
3004
+ this._ptyAdapter.closeFd(slaveFd);
3005
+ } catch {
3006
+ try {
3007
+ child.kill();
3008
+ } catch {
3009
+ // Continue fail-closed cleanup even if the partial child already died.
3010
+ }
3011
+ sandboxPlan.cleanup?.();
3012
+ releaseTerminal();
3013
+ const sandboxError = this._sandboxBoundaryError(
3014
+ "pty_parent_slave_cleanup_failed",
3015
+ "Strong Linux PTY could not close its parent-side slave descriptor",
3016
+ {
3017
+ requiredBoundaries: sandboxPolicy.requiredBoundaries,
3018
+ missingBoundaries: sandboxPolicy.requiredBoundaries,
3019
+ sandboxBackend: sandboxPlan.backend,
3020
+ },
3021
+ );
3022
+ this._recordSandboxDenial(auditEntry, sandboxError, startTime);
3023
+ sandboxError.auditEntry = auditEntry;
3024
+ throw sandboxError;
3025
+ }
3026
+ sandboxPlan.cleanup?.();
3027
+ this._applySandboxAudit(auditEntry, sandboxPlan, true);
3028
+ this._stats.sandboxed++;
3029
+
3030
+ child.once("exit", (code, signal) => {
3031
+ releaseTerminal();
3032
+ auditEntry.exitCode = code;
3033
+ auditEntry.signal = signal;
3034
+ auditEntry.endTime = Date.now();
3035
+ auditEntry.durationMs = auditEntry.endTime - startTime;
3036
+ this._writeRplEntry(auditEntry, "completed");
3037
+ this.emit("exit", auditEntry);
3038
+ });
3039
+ child.once("error", (error) => {
3040
+ releaseTerminal();
3041
+ auditEntry.error = error.message;
3042
+ auditEntry.endTime = Date.now();
3043
+ auditEntry.durationMs = auditEntry.endTime - startTime;
3044
+ this._writeRplEntry(auditEntry, "error", error);
3045
+ if (this.listenerCount("error") > 0) this.emit("error", auditEntry);
3046
+ });
2382
3047
  auditEntry.pid = proc?.pid ?? null;
2383
3048
  auditEntry.endTime = Date.now();
2384
3049
  auditEntry.durationMs = auditEntry.endTime - startTime;
@@ -2386,6 +3051,7 @@ class ProcessExecutionBroker extends EventEmitter {
2386
3051
  this._writeRplEntry(auditEntry, "started");
2387
3052
  return proc;
2388
3053
  } catch (error) {
3054
+ if (error.auditEntry === auditEntry) throw error;
2389
3055
  auditEntry.error = error.message;
2390
3056
  auditEntry.endTime = Date.now();
2391
3057
  auditEntry.durationMs = auditEntry.endTime - startTime;