@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18

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 (357) hide show
  1. package/README.md +134 -15
  2. package/assets/integrations/skills/pappra/SKILL.md +28 -7
  3. package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
  4. package/assets/integrations/skills/pappra/references/environment.md +360 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +20 -0
  6. package/assets/integrations/skills/pappra/references/operations.md +36 -5
  7. package/dist/adapters/pi/recovery-extension.d.ts +65 -0
  8. package/dist/adapters/pi/recovery-extension.js +238 -0
  9. package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
  10. package/dist/adapters/pi/session-transition-extension.js +265 -0
  11. package/dist/app/automatic-handoff-events.d.ts +26 -0
  12. package/dist/app/automatic-handoff-events.js +134 -0
  13. package/dist/app/bounded-file.d.ts +2 -0
  14. package/dist/app/bounded-file.js +2 -0
  15. package/dist/app/capability-evidence/claude.d.ts +37 -0
  16. package/dist/app/capability-evidence/claude.js +206 -0
  17. package/dist/app/capability-evidence/codex.d.ts +35 -0
  18. package/dist/app/capability-evidence/codex.js +193 -0
  19. package/dist/app/capability-evidence/index.d.ts +33 -0
  20. package/dist/app/capability-evidence/index.js +117 -0
  21. package/dist/app/capability-evidence/pi.d.ts +73 -0
  22. package/dist/app/capability-evidence/pi.js +669 -0
  23. package/dist/app/capability-evidence/shared.d.ts +69 -0
  24. package/dist/app/capability-evidence/shared.js +195 -0
  25. package/dist/app/collab-auto-handoff.d.ts +55 -0
  26. package/dist/app/collab-auto-handoff.js +384 -0
  27. package/dist/app/collab-bindings.d.ts +57 -0
  28. package/dist/app/collab-bindings.js +1006 -0
  29. package/dist/app/collab-bootstrap-transport.d.ts +163 -0
  30. package/dist/app/collab-bootstrap-transport.js +769 -0
  31. package/dist/app/collab-bootstrap.d.ts +137 -0
  32. package/dist/app/collab-bootstrap.js +3455 -0
  33. package/dist/app/collab-handoff.d.ts +12 -0
  34. package/dist/app/collab-handoff.js +511 -0
  35. package/dist/app/collab-messages.d.ts +2 -0
  36. package/dist/app/collab-messages.js +2 -0
  37. package/dist/app/collab-notify.d.ts +156 -0
  38. package/dist/app/collab-notify.js +1208 -0
  39. package/dist/app/collab-preflight.d.ts +34 -0
  40. package/dist/app/collab-preflight.js +96 -0
  41. package/dist/app/collab-recovery-executor.d.ts +161 -0
  42. package/dist/app/collab-recovery-executor.js +1153 -0
  43. package/dist/app/collab-recovery-projection.d.ts +103 -0
  44. package/dist/app/collab-recovery-projection.js +659 -0
  45. package/dist/app/collab-recovery.d.ts +25 -0
  46. package/dist/app/collab-recovery.js +429 -0
  47. package/dist/app/collab-resources.d.ts +16 -0
  48. package/dist/app/collab-resources.js +278 -0
  49. package/dist/app/collab-role-events.d.ts +22 -0
  50. package/dist/app/collab-role-events.js +329 -0
  51. package/dist/app/collab-role-orchestration.d.ts +53 -0
  52. package/dist/app/collab-role-orchestration.js +601 -0
  53. package/dist/app/collab-role-plan.d.ts +81 -0
  54. package/dist/app/collab-role-plan.js +504 -0
  55. package/dist/app/collab-role-repair.d.ts +109 -0
  56. package/dist/app/collab-role-repair.js +821 -0
  57. package/dist/app/collab-rollover.d.ts +263 -0
  58. package/dist/app/collab-rollover.js +1112 -0
  59. package/dist/app/collab-route.d.ts +64 -0
  60. package/dist/app/collab-route.js +240 -0
  61. package/dist/app/collab-session-transition.d.ts +221 -0
  62. package/dist/app/collab-session-transition.js +3334 -0
  63. package/dist/app/collab-state.d.ts +7 -0
  64. package/dist/app/collab-state.js +162 -0
  65. package/dist/app/collab-task-store.d.ts +22 -0
  66. package/dist/app/collab-task-store.js +204 -0
  67. package/dist/app/collab-work-execution.d.ts +127 -0
  68. package/dist/app/collab-work-execution.js +1027 -0
  69. package/dist/app/collab-work-plan.d.ts +87 -0
  70. package/dist/app/collab-work-plan.js +437 -0
  71. package/dist/app/collaboration.d.ts +8 -2
  72. package/dist/app/collaboration.js +101 -12
  73. package/dist/app/doctor.js +108 -5
  74. package/dist/app/host-mcp.js +34 -8
  75. package/dist/app/host-probe.js +4 -4
  76. package/dist/app/init.d.ts +58 -4
  77. package/dist/app/init.js +768 -155
  78. package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
  79. package/dist/app/managed-pi-bootstrap-admission.js +104 -0
  80. package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
  81. package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
  82. package/dist/app/managed-pi-delivery.d.ts +8 -0
  83. package/dist/app/managed-pi-delivery.js +110 -0
  84. package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
  85. package/dist/app/managed-pi-handoff-receiver.js +263 -0
  86. package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
  87. package/dist/app/managed-pi-handoff-reference.js +60 -0
  88. package/dist/app/managed-pi-herdr.d.ts +21 -0
  89. package/dist/app/managed-pi-herdr.js +185 -0
  90. package/dist/app/managed-pi-initial-admission.d.ts +28 -0
  91. package/dist/app/managed-pi-initial-admission.js +416 -0
  92. package/dist/app/managed-pi-initial-launch.d.ts +5 -0
  93. package/dist/app/managed-pi-initial-launch.js +61 -0
  94. package/dist/app/managed-pi-native-observation.d.ts +36 -0
  95. package/dist/app/managed-pi-native-observation.js +191 -0
  96. package/dist/app/managed-pi-readiness.d.ts +29 -0
  97. package/dist/app/managed-pi-readiness.js +288 -0
  98. package/dist/app/managed-pi-repair-stop.d.ts +21 -0
  99. package/dist/app/managed-pi-repair-stop.js +371 -0
  100. package/dist/app/managed-pi-resume-admission.d.ts +32 -0
  101. package/dist/app/managed-pi-resume-admission.js +618 -0
  102. package/dist/app/managed-pi-retirement.d.ts +28 -0
  103. package/dist/app/managed-pi-retirement.js +112 -0
  104. package/dist/app/managed-pi-sdk-control.d.ts +7 -0
  105. package/dist/app/managed-pi-sdk-control.js +169 -0
  106. package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
  107. package/dist/app/managed-pi-sdk-runtime.js +483 -0
  108. package/dist/app/managed-pi-source-admission.d.ts +14 -0
  109. package/dist/app/managed-pi-source-admission.js +323 -0
  110. package/dist/app/managed-pi-transition-host.d.ts +36 -0
  111. package/dist/app/managed-pi-transition-host.js +292 -0
  112. package/dist/app/managed-pi.d.ts +54 -0
  113. package/dist/app/managed-pi.js +248 -0
  114. package/dist/app/shell-readiness.d.ts +66 -0
  115. package/dist/app/shell-readiness.js +368 -0
  116. package/dist/app/task-pin.d.ts +2 -23
  117. package/dist/app/task-pin.js +2 -113
  118. package/dist/cli/commands/collaboration-binding.d.ts +13 -0
  119. package/dist/cli/commands/collaboration-binding.js +118 -0
  120. package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
  121. package/dist/cli/commands/collaboration-bootstrap.js +84 -0
  122. package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
  123. package/dist/cli/commands/collaboration-capabilities.js +127 -0
  124. package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
  125. package/dist/cli/commands/collaboration-handoff.js +102 -0
  126. package/dist/cli/commands/collaboration-message.d.ts +17 -0
  127. package/dist/cli/commands/collaboration-message.js +177 -0
  128. package/dist/cli/commands/collaboration-plan.d.ts +8 -0
  129. package/dist/cli/commands/collaboration-plan.js +108 -0
  130. package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
  131. package/dist/cli/commands/collaboration-preflight.js +167 -0
  132. package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
  133. package/dist/cli/commands/collaboration-recovery-run.js +224 -0
  134. package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
  135. package/dist/cli/commands/collaboration-recovery.js +104 -0
  136. package/dist/cli/commands/collaboration-resources.d.ts +11 -0
  137. package/dist/cli/commands/collaboration-resources.js +96 -0
  138. package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
  139. package/dist/cli/commands/collaboration-role-plan.js +69 -0
  140. package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
  141. package/dist/cli/commands/collaboration-role-run.js +128 -0
  142. package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
  143. package/dist/cli/commands/collaboration-rollover.js +103 -0
  144. package/dist/cli/commands/collaboration-state.d.ts +10 -0
  145. package/dist/cli/commands/collaboration-state.js +96 -0
  146. package/dist/cli/commands/collaboration-transition.d.ts +10 -0
  147. package/dist/cli/commands/collaboration-transition.js +75 -0
  148. package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
  149. package/dist/cli/commands/collaboration-work-run.js +178 -0
  150. package/dist/cli/commands/collaboration.d.ts +4 -0
  151. package/dist/cli/commands/collaboration.js +39 -4
  152. package/dist/cli/commands/continuity.js +2 -6
  153. package/dist/cli/commands/docs.d.ts +2 -0
  154. package/dist/cli/commands/docs.js +89 -15
  155. package/dist/cli/commands/host.d.ts +7 -1
  156. package/dist/cli/commands/host.js +122 -7
  157. package/dist/cli/commands/init.d.ts +20 -3
  158. package/dist/cli/commands/init.js +897 -232
  159. package/dist/cli/commands/mcp.d.ts +1 -0
  160. package/dist/cli/commands/mcp.js +11 -5
  161. package/dist/cli/commands/repair.d.ts +12 -0
  162. package/dist/cli/commands/repair.js +41 -0
  163. package/dist/cli/commands/runtime.d.ts +1 -1
  164. package/dist/cli/commands/runtime.js +139 -38
  165. package/dist/cli/commands/session.d.ts +7 -0
  166. package/dist/cli/commands/session.js +82 -0
  167. package/dist/cli/commands/task.js +19 -5
  168. package/dist/cli/run.js +866 -24
  169. package/dist/code/identity.d.ts +7 -0
  170. package/dist/code/identity.js +7 -3
  171. package/dist/code/registration.js +7 -0
  172. package/dist/code/service.d.ts +9 -0
  173. package/dist/code/service.js +16 -3
  174. package/dist/code/text.js +19 -7
  175. package/dist/collab/admission.d.ts +91 -66
  176. package/dist/collab/admission.js +21 -6
  177. package/dist/collab/canonical.d.ts +1 -1
  178. package/dist/collab/canonical.js +44 -3
  179. package/dist/collab/envelope.d.ts +2 -2
  180. package/dist/collab/envelope.js +12 -12
  181. package/dist/collab/first-work.d.ts +3 -0
  182. package/dist/collab/first-work.js +15 -0
  183. package/dist/collab/handoff.d.ts +16 -0
  184. package/dist/collab/handoff.js +39 -5
  185. package/dist/collab/index.d.ts +97 -1
  186. package/dist/collab/index.js +9 -1
  187. package/dist/collab/messages.js +126 -3
  188. package/dist/collab/persisted-messages.d.ts +123 -0
  189. package/dist/collab/persisted-messages.js +496 -0
  190. package/dist/collab/preflight.d.ts +1 -1
  191. package/dist/collab/resources.d.ts +13 -3
  192. package/dist/collab/resources.js +37 -6
  193. package/dist/collab/rollover.d.ts +311 -0
  194. package/dist/collab/rollover.js +1024 -0
  195. package/dist/collab/store.d.ts +1 -0
  196. package/dist/collab/store.js +1 -4
  197. package/dist/collab/types.d.ts +2 -354
  198. package/dist/collab/validation.d.ts +2 -10
  199. package/dist/collab/validation.js +2 -303
  200. package/dist/collab/work-plan.d.ts +105 -0
  201. package/dist/collab/work-plan.js +843 -0
  202. package/dist/continuity/context.d.ts +11 -2
  203. package/dist/continuity/context.js +100 -13
  204. package/dist/continuity/handoff-bundle.d.ts +296 -0
  205. package/dist/continuity/handoff-bundle.js +477 -0
  206. package/dist/continuity/index.d.ts +2 -0
  207. package/dist/continuity/index.js +1 -0
  208. package/dist/continuity/paths.js +10 -2
  209. package/dist/continuity/service.js +91 -15
  210. package/dist/continuity/task-pin.d.ts +24 -0
  211. package/dist/continuity/task-pin.js +590 -0
  212. package/dist/doctor.d.ts +1 -0
  213. package/dist/doctor.js +6 -0
  214. package/dist/experimental/index.d.ts +13 -0
  215. package/dist/experimental/index.js +25 -0
  216. package/dist/experimental/pi.d.ts +4 -0
  217. package/dist/experimental/pi.js +12 -0
  218. package/dist/hosts/claude/native.js +1 -1
  219. package/dist/hosts/codex/native.js +1 -1
  220. package/dist/hosts/codex/protocol.js +1 -1
  221. package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
  222. package/dist/hosts/integration/bootstrap-config.js +254 -0
  223. package/dist/hosts/integration/bundle-export.d.ts +44 -0
  224. package/dist/hosts/integration/bundle-export.js +366 -3
  225. package/dist/hosts/integration/bundle.js +3 -0
  226. package/dist/hosts/integration/hook.js +36 -9
  227. package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
  228. package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
  229. package/dist/hosts/integration/pappra-hook.mjs +61 -24749
  230. package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
  231. package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
  232. package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
  233. package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
  234. package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
  235. package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
  236. package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
  237. package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
  238. package/dist/hosts/integration/pi-native-events.d.ts +112 -0
  239. package/dist/hosts/integration/pi-native-events.js +348 -0
  240. package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
  241. package/dist/hosts/integration/pi-rollover-extension.js +431 -0
  242. package/dist/hosts/integration/recovery-config.d.ts +48 -0
  243. package/dist/hosts/integration/recovery-config.js +117 -0
  244. package/dist/hosts/integration/service.d.ts +6 -1
  245. package/dist/hosts/integration/service.js +485 -35
  246. package/dist/hosts/integration/support.d.ts +7 -0
  247. package/dist/hosts/integration/support.js +9 -1
  248. package/dist/hosts/integration/templates.d.ts +3 -0
  249. package/dist/hosts/integration/templates.js +26 -3
  250. package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
  251. package/dist/hosts/pi/managed-host-identity.js +40 -0
  252. package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
  253. package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
  254. package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
  255. package/dist/hosts/pi/managed-sdk-session.js +1639 -0
  256. package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
  257. package/dist/hosts/pi/managed-session-extension.js +84 -0
  258. package/dist/hosts/pi/managed-session.d.ts +177 -0
  259. package/dist/hosts/pi/managed-session.js +1557 -0
  260. package/dist/hosts/pi/protocol.js +1 -1
  261. package/dist/hosts/pi/query-result.js +1 -1
  262. package/dist/hosts/pi/resume-capability.d.ts +41 -0
  263. package/dist/hosts/pi/resume-capability.js +33 -0
  264. package/dist/hosts/pi/rpc.d.ts +182 -134
  265. package/dist/hosts/pi/rpc.js +230 -37
  266. package/dist/hosts/pilot.d.ts +97 -74
  267. package/dist/hosts/pilot.js +5 -1
  268. package/dist/hosts/version.d.ts +3 -0
  269. package/dist/hosts/version.js +18 -0
  270. package/dist/index.d.ts +0 -1
  271. package/dist/index.js +0 -1
  272. package/dist/infra/bounded-file.d.ts +19 -0
  273. package/dist/infra/bounded-file.js +66 -0
  274. package/dist/infra/fs.js +10 -2
  275. package/dist/infra/git.d.ts +29 -0
  276. package/dist/infra/git.js +45 -9
  277. package/dist/infra/json-property.d.ts +2 -0
  278. package/dist/infra/json-property.js +69 -0
  279. package/dist/infra/json.d.ts +4 -0
  280. package/dist/infra/json.js +24 -0
  281. package/dist/infra/owned-process-group.js +1 -1
  282. package/dist/infra/private-lock.d.ts +16 -0
  283. package/dist/infra/private-lock.js +148 -0
  284. package/dist/infra/private-state.d.ts +7 -0
  285. package/dist/infra/private-state.js +146 -0
  286. package/dist/infra/process-identity.d.ts +12 -0
  287. package/dist/infra/process-identity.js +142 -0
  288. package/dist/infra/validation.d.ts +5 -0
  289. package/dist/infra/validation.js +15 -0
  290. package/dist/init.d.ts +1 -0
  291. package/dist/init.js +6 -0
  292. package/dist/install/apply.d.ts +9 -1
  293. package/dist/install/apply.js +176 -12
  294. package/dist/install/dependencies.d.ts +38 -0
  295. package/dist/install/dependencies.js +282 -0
  296. package/dist/install/ownership.js +30 -1
  297. package/dist/install/references.d.ts +31 -0
  298. package/dist/install/references.js +208 -0
  299. package/dist/install/repair.d.ts +37 -0
  300. package/dist/install/repair.js +798 -0
  301. package/dist/install/reset-local.d.ts +16 -0
  302. package/dist/install/reset-local.js +304 -0
  303. package/dist/install/rollback.d.ts +3 -0
  304. package/dist/install/rollback.js +9 -3
  305. package/dist/install/runtime.d.ts +10 -6
  306. package/dist/install/runtime.js +299 -59
  307. package/dist/install/source-fingerprint.d.ts +2 -0
  308. package/dist/install/source-fingerprint.js +103 -0
  309. package/dist/knowledge/catalog.d.ts +1 -0
  310. package/dist/knowledge/catalog.js +143 -10
  311. package/dist/knowledge/journal.js +17 -7
  312. package/dist/knowledge/paths.js +2 -1
  313. package/dist/knowledge/refs.js +4 -0
  314. package/dist/knowledge/register-batch.d.ts +4 -0
  315. package/dist/knowledge/register-batch.js +225 -0
  316. package/dist/knowledge/relocate.d.ts +5 -0
  317. package/dist/knowledge/relocate.js +92 -0
  318. package/dist/knowledge/scan.js +103 -47
  319. package/dist/knowledge/service.d.ts +3 -0
  320. package/dist/knowledge/service.js +33 -1
  321. package/dist/knowledge/write.d.ts +4 -1
  322. package/dist/knowledge/write.js +1 -1
  323. package/dist/mcp/server.d.ts +4 -2
  324. package/dist/mcp/server.js +87 -136
  325. package/dist/schemas/collaboration-state-validation.d.ts +6 -0
  326. package/dist/schemas/collaboration-state-validation.js +342 -0
  327. package/dist/schemas/collaboration-state.d.ts +436 -0
  328. package/dist/schemas/collaboration-state.js +1 -0
  329. package/dist/schemas/collaboration.d.ts +335 -0
  330. package/dist/schemas/collaboration.js +272 -0
  331. package/dist/schemas/continuity.d.ts +8 -0
  332. package/dist/schemas/continuity.js +13 -3
  333. package/dist/schemas/doctor.d.ts +1 -0
  334. package/dist/schemas/doctor.js +1 -0
  335. package/dist/schemas/document.d.ts +61 -0
  336. package/dist/schemas/document.js +9 -0
  337. package/dist/schemas/errors.d.ts +1 -0
  338. package/dist/schemas/errors.js +1 -0
  339. package/dist/schemas/host.d.ts +24 -0
  340. package/dist/schemas/host.js +19 -0
  341. package/dist/schemas/install.d.ts +8 -2
  342. package/dist/schemas/install.js +11 -1
  343. package/dist/schemas/managed-pi-execution.d.ts +16 -0
  344. package/dist/schemas/managed-pi-execution.js +2 -0
  345. package/dist/schemas/outcome.d.ts +9 -5
  346. package/dist/workspace/config-file.js +1 -3
  347. package/dist/workspace/documents.js +12 -2
  348. package/dist/workspace/repositories.d.ts +7 -0
  349. package/dist/workspace/repositories.js +14 -5
  350. package/package.json +35 -35
  351. package/skills/pappra/SKILL.md +28 -7
  352. package/skills/pappra/references/collaboration.md +633 -0
  353. package/skills/pappra/references/environment.md +360 -0
  354. package/skills/pappra/references/hosts.md +20 -0
  355. package/skills/pappra/references/operations.md +36 -5
  356. package/dist/app/collab-from.d.ts +0 -35
  357. package/dist/app/collab-from.js +0 -84
@@ -0,0 +1,1557 @@
1
+ import { createHash, randomBytes, randomUUID, timingSafeEqual, } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import net, {} from 'node:net';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+ export const MANAGED_PI_SESSION_SCHEMA = 'harness-collab/pi-managed-session/1';
8
+ const CONTROL_SCHEMA = 'harness-collab/pi-managed-control/1';
9
+ const METADATA_ENV = 'PAPPRA_MANAGED_PI_METADATA_FILE';
10
+ const START_TIMEOUT_MS = 10_000;
11
+ const DEFAULT_SHUTDOWN_GRACE_MS = 5_000;
12
+ const KILL_GRACE_MS = 2_000;
13
+ const MAX_CONTROL_BYTES = 8 * 1024;
14
+ // Native queue inventories may contain admitted input. Keep responses bounded
15
+ // independently from the much smaller authenticated command envelope.
16
+ const MAX_CONTROL_RESPONSE_BYTES = 1024 * 1024;
17
+ const MAX_SOCKET_PATH_BYTES = 100;
18
+ export class ManagedPiSessionError extends Error {
19
+ code;
20
+ constructor(code, message = code) {
21
+ super(message);
22
+ this.code = code;
23
+ this.name = 'ManagedPiSessionError';
24
+ }
25
+ }
26
+ /** Fsync-backed, hash-chained journal. */
27
+ export class ManagedPiJournal {
28
+ path;
29
+ fd;
30
+ runId;
31
+ sequence = 0;
32
+ headDigest = '0'.repeat(64);
33
+ failure = null;
34
+ isClosed = false;
35
+ isFenced = false;
36
+ constructor(path, fd, runId) {
37
+ this.path = path;
38
+ this.fd = fd;
39
+ this.runId = runId;
40
+ }
41
+ get healthy() {
42
+ return this.failure === null && !this.isClosed;
43
+ }
44
+ get fenced() {
45
+ return this.isFenced;
46
+ }
47
+ get currentSequence() {
48
+ return this.sequence;
49
+ }
50
+ get currentDigest() {
51
+ return this.headDigest;
52
+ }
53
+ append(kind, fields = {}) {
54
+ if (!this.healthy)
55
+ throw new ManagedPiSessionError('journal_unavailable', this.failure ?? 'journal_closed');
56
+ const body = {
57
+ schema: MANAGED_PI_SESSION_SCHEMA,
58
+ run_id: this.runId,
59
+ sequence: this.sequence + 1,
60
+ at: new Date().toISOString(),
61
+ kind,
62
+ previous_digest: this.headDigest,
63
+ ...fields,
64
+ };
65
+ const digest = createHash('sha256')
66
+ .update(JSON.stringify(body))
67
+ .digest('hex');
68
+ const record = { ...body, digest };
69
+ const bytes = Buffer.from(`${JSON.stringify(record)}\n`);
70
+ try {
71
+ writeAll(this.fd, bytes);
72
+ fs.fsyncSync(this.fd);
73
+ }
74
+ catch (error) {
75
+ this.failure = error instanceof Error ? error.message : String(error);
76
+ this.isFenced = true;
77
+ throw new ManagedPiSessionError('journal_write_failed', this.failure);
78
+ }
79
+ this.sequence += 1;
80
+ this.headDigest = digest;
81
+ return record;
82
+ }
83
+ fence(reason) {
84
+ if (this.isFenced)
85
+ return;
86
+ this.isFenced = true;
87
+ try {
88
+ this.append('ingress_fenced', { reason });
89
+ }
90
+ catch {
91
+ /* fail closed in memory */
92
+ }
93
+ }
94
+ close() {
95
+ if (this.isClosed)
96
+ return;
97
+ this.isClosed = true;
98
+ fs.closeSync(this.fd);
99
+ }
100
+ }
101
+ /** Write-ahead admission metadata, without retaining terminal input or secrets. */
102
+ export function forwardJournaledManagedInput(journal, chunk, forward) {
103
+ if (journal.fenced)
104
+ return false;
105
+ try {
106
+ journal.append('terminal_input', {
107
+ input_digest: `sha256:${createHash('sha256').update(chunk).digest('hex')}`,
108
+ byte_length: chunk.byteLength,
109
+ });
110
+ }
111
+ catch {
112
+ journal.fence('journal_write_failed');
113
+ return false;
114
+ }
115
+ try {
116
+ forward(chunk);
117
+ return true;
118
+ }
119
+ catch {
120
+ journal.fence('terminal_forward_failed');
121
+ return false;
122
+ }
123
+ }
124
+ export function buildManagedPiArgv(input) {
125
+ if (!input.model.trim())
126
+ throw new ManagedPiSessionError('model_required');
127
+ if (!path.isAbsolute(input.extensionPath))
128
+ throw new ManagedPiSessionError('extension_path_must_be_absolute');
129
+ return [
130
+ '--provider',
131
+ 'openrouter',
132
+ '--model',
133
+ input.model,
134
+ '--session-id',
135
+ input.sessionId,
136
+ '--extension',
137
+ input.extensionPath,
138
+ ];
139
+ }
140
+ /** Exit receipts from the bridge count only when all child identity fields match. */
141
+ export function isOwnedManagedPiExit(event, identity) {
142
+ return (event.runId === identity.runId &&
143
+ event.helperPid === identity.helperPid &&
144
+ event.piPid === identity.piPid);
145
+ }
146
+ const PTY_BRIDGE_SOURCE = String.raw `import errno
147
+ import fcntl
148
+ import json
149
+ import os
150
+ import pty
151
+ import select
152
+ import signal
153
+ import time
154
+ import struct
155
+ import sys
156
+ import termios
157
+
158
+ run_id = sys.argv[1]
159
+ pi_argv = sys.argv[2:]
160
+ if not pi_argv:
161
+ os.write(2, b"managed Pi bridge: missing executable\n")
162
+ sys.exit(64)
163
+ for fd in (3, 4):
164
+ try:
165
+ os.set_inheritable(fd, False)
166
+ except OSError:
167
+ pass
168
+
169
+ def write_all(fd, data):
170
+ view = memoryview(data)
171
+ while view:
172
+ count = os.write(fd, view)
173
+ view = view[count:]
174
+
175
+ def report(value):
176
+ try:
177
+ write_all(3, (json.dumps(value, separators=(",", ":")) + "\n").encode())
178
+ except OSError:
179
+ pass
180
+
181
+ def signal_owned(pid, name):
182
+ sig = {"TERM": signal.SIGTERM, "KILL": signal.SIGKILL}.get(name)
183
+ if sig is not None:
184
+ try:
185
+ os.kill(pid, sig)
186
+ except ProcessLookupError:
187
+ pass
188
+
189
+ try:
190
+ pi_pid, master_fd = pty.fork()
191
+ except OSError as error:
192
+ os.write(2, ("managed Pi bridge: pty.fork failed: " + str(error) + "\n").encode())
193
+ sys.exit(70)
194
+ if pi_pid == 0:
195
+ try:
196
+ os.execvpe(pi_argv[0], pi_argv, os.environ)
197
+ except BaseException as error:
198
+ os.write(2, ("managed Pi bridge: exec failed: " + str(error) + "\n").encode())
199
+ os._exit(127)
200
+
201
+ identity = {"runId": run_id, "helperPid": os.getpid(), "piPid": pi_pid}
202
+ report({"kind": "pi_started", **identity})
203
+ try:
204
+ rows = max(1, int(os.environ.get("PAPPRA_MANAGED_PI_ROWS", "24")))
205
+ columns = max(1, int(os.environ.get("PAPPRA_MANAGED_PI_COLUMNS", "80")))
206
+ fcntl.ioctl(master_fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, columns, 0, 0))
207
+ except (OSError, ValueError):
208
+ pass
209
+
210
+ control_buffer = b""
211
+ control_open = True
212
+ stdin_open = True
213
+ output_open = True
214
+ master_open = True
215
+ orphan_deadline = None
216
+ reaped = False
217
+ status = 0
218
+ while True:
219
+ read_fds = [master_fd] if master_open and output_open else []
220
+ if control_open:
221
+ read_fds.append(4)
222
+ if stdin_open:
223
+ read_fds.append(0)
224
+ try:
225
+ ready, _, _ = select.select(read_fds, [], [], 0.05)
226
+ except InterruptedError:
227
+ ready = []
228
+ if master_fd in ready and output_open:
229
+ try:
230
+ data = os.read(master_fd, 65536)
231
+ if data:
232
+ try:
233
+ write_all(1, data)
234
+ except OSError:
235
+ output_open = False
236
+ signal_owned(pi_pid, "TERM")
237
+ if orphan_deadline is None:
238
+ orphan_deadline = time.monotonic() + 1.0
239
+ else:
240
+ master_open = False
241
+ except OSError as error:
242
+ if error.errno not in (errno.EIO, errno.EBADF):
243
+ os.write(2, ("managed Pi bridge: PTY read failed: " + str(error) + "\n").encode())
244
+ output_open = False
245
+ master_open = False
246
+ if 0 in ready and stdin_open:
247
+ try:
248
+ data = os.read(0, 65536)
249
+ if not data:
250
+ stdin_open = False
251
+ else:
252
+ view = memoryview(data)
253
+ while view:
254
+ try:
255
+ count = os.write(master_fd, view)
256
+ view = view[count:]
257
+ except OSError:
258
+ stdin_open = False
259
+ break
260
+ except OSError:
261
+ stdin_open = False
262
+ if 4 in ready and control_open:
263
+ try:
264
+ chunk = os.read(4, 4096)
265
+ except OSError:
266
+ chunk = b""
267
+ if not chunk:
268
+ control_open = False
269
+ signal_owned(pi_pid, "TERM")
270
+ if orphan_deadline is None:
271
+ orphan_deadline = time.monotonic() + 1.0
272
+ else:
273
+ control_buffer += chunk
274
+ while b"\n" in control_buffer:
275
+ line, control_buffer = control_buffer.split(b"\n", 1)
276
+ try:
277
+ command = json.loads(line.decode())
278
+ if command.get("command") == "signal":
279
+ signal_owned(pi_pid, command.get("signal", ""))
280
+ elif command.get("command") == "resize":
281
+ rows = max(1, int(command.get("rows", 24)))
282
+ columns = max(1, int(command.get("columns", 80)))
283
+ fcntl.ioctl(master_fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, columns, 0, 0))
284
+ try:
285
+ os.kill(pi_pid, signal.SIGWINCH)
286
+ except ProcessLookupError:
287
+ pass
288
+ except (ValueError, OSError, TypeError):
289
+ pass
290
+ if orphan_deadline is not None and time.monotonic() >= orphan_deadline:
291
+ signal_owned(pi_pid, "KILL")
292
+ orphan_deadline = None
293
+ if not reaped:
294
+ try:
295
+ waited, status = os.waitpid(pi_pid, os.WNOHANG)
296
+ reaped = waited == pi_pid
297
+ except ChildProcessError:
298
+ # A missing waitpid result is not a successful child-exit receipt.
299
+ # Closing the status channel makes the controller report unknown.
300
+ os.close(master_fd)
301
+ sys.exit(70)
302
+ if reaped:
303
+ if output_open:
304
+ while True:
305
+ try:
306
+ ready, _, _ = select.select([master_fd], [], [], 0)
307
+ if not ready:
308
+ break
309
+ data = os.read(master_fd, 65536)
310
+ if not data:
311
+ break
312
+ write_all(1, data)
313
+ except OSError:
314
+ break
315
+ exit_code = os.waitstatus_to_exitcode(status)
316
+ child_signal = -exit_code if exit_code < 0 else None
317
+ report({"kind": "pi_exit", **identity,
318
+ "exitCode": exit_code if exit_code >= 0 else None,
319
+ "signal": signal.Signals(child_signal).name if child_signal else None})
320
+ break
321
+ try:
322
+ os.close(master_fd)
323
+ except OSError:
324
+ pass
325
+ `;
326
+ /** @internal: exercised by the owned PTY bridge regression test. */
327
+ export const managedPiPtyBridgeSourceForTests = PTY_BRIDGE_SOURCE;
328
+ function writeAll(fd, bytes) {
329
+ let offset = 0;
330
+ while (offset < bytes.byteLength)
331
+ offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
332
+ }
333
+ function fsyncDirectory(directory) {
334
+ const fd = fs.openSync(directory, 'r');
335
+ try {
336
+ fs.fsyncSync(fd);
337
+ }
338
+ finally {
339
+ fs.closeSync(fd);
340
+ }
341
+ }
342
+ function requirePrivateParent(file) {
343
+ const parent = path.dirname(file);
344
+ const stat = fs.lstatSync(parent, { throwIfNoEntry: false });
345
+ if (!stat?.isDirectory() || stat.isSymbolicLink())
346
+ throw new ManagedPiSessionError('artifact_parent_not_directory');
347
+ if (fs.realpathSync(parent) !== parent)
348
+ throw new ManagedPiSessionError('artifact_parent_not_canonical');
349
+ }
350
+ function createArtifacts(journalPath) {
351
+ if (!path.isAbsolute(journalPath))
352
+ throw new ManagedPiSessionError('journal_path_must_be_absolute');
353
+ const file = path.resolve(journalPath);
354
+ requirePrivateParent(file);
355
+ const metadataPath = `${file}.pi-events.jsonl`;
356
+ let journalFd;
357
+ let metadataFd;
358
+ try {
359
+ journalFd = fs.openSync(file, 'wx', 0o600);
360
+ metadataFd = fs.openSync(metadataPath, 'wx', 0o600);
361
+ fs.fsyncSync(journalFd);
362
+ fs.fsyncSync(metadataFd);
363
+ fs.closeSync(metadataFd);
364
+ metadataFd = undefined;
365
+ fsyncDirectory(path.dirname(file));
366
+ return { journalPath: file, metadataPath, journalFd };
367
+ }
368
+ catch (error) {
369
+ if (metadataFd !== undefined)
370
+ try {
371
+ fs.closeSync(metadataFd);
372
+ }
373
+ catch {
374
+ /* best effort */
375
+ }
376
+ if (journalFd !== undefined)
377
+ try {
378
+ fs.closeSync(journalFd);
379
+ }
380
+ catch {
381
+ /* best effort */
382
+ }
383
+ for (const created of [metadataPath, file]) {
384
+ try {
385
+ fs.unlinkSync(created);
386
+ }
387
+ catch {
388
+ /* only paths created by this attempt */
389
+ }
390
+ }
391
+ throw error;
392
+ }
393
+ }
394
+ /** Create private, exclusive journal and Pi observer metadata artifacts for SDK mode. */
395
+ export function createManagedPiJournalArtifacts(journalPath, runId = randomUUID()) {
396
+ const artifacts = createArtifacts(journalPath);
397
+ return {
398
+ journal: new ManagedPiJournal(artifacts.journalPath, artifacts.journalFd, runId),
399
+ metadataPath: artifacts.metadataPath,
400
+ };
401
+ }
402
+ function extensionPath() {
403
+ const current = fileURLToPath(import.meta.url);
404
+ const ext = path.extname(current);
405
+ const stem = path.join(path.dirname(current), 'managed-session-extension');
406
+ const choices = ext === '.ts' ? [`${stem}.ts`, `${stem}.js`] : [`${stem}.js`, `${stem}.ts`];
407
+ const found = choices.find((candidate) => fs.statSync(candidate, { throwIfNoEntry: false })?.isFile());
408
+ if (!found)
409
+ throw new ManagedPiSessionError('managed_extension_missing');
410
+ return found;
411
+ }
412
+ function parseBridgeEvent(line) {
413
+ try {
414
+ const value = JSON.parse(line);
415
+ if (typeof value !== 'object' || value === null)
416
+ return null;
417
+ const record = value;
418
+ if ((record['kind'] !== 'pi_started' && record['kind'] !== 'pi_exit') ||
419
+ typeof record['runId'] !== 'string' ||
420
+ !Number.isSafeInteger(record['helperPid']) ||
421
+ !Number.isSafeInteger(record['piPid']))
422
+ return null;
423
+ return record;
424
+ }
425
+ catch {
426
+ return null;
427
+ }
428
+ }
429
+ function metadataRecords(file) {
430
+ try {
431
+ return fs
432
+ .readFileSync(file, 'utf8')
433
+ .split('\n')
434
+ .flatMap((line) => {
435
+ try {
436
+ const record = JSON.parse(line);
437
+ return typeof record === 'object' && record !== null
438
+ ? [record]
439
+ : [];
440
+ }
441
+ catch {
442
+ return [];
443
+ }
444
+ });
445
+ }
446
+ catch {
447
+ return [];
448
+ }
449
+ }
450
+ function hashFile(file) {
451
+ const fd = fs.openSync(file, 'r');
452
+ const hash = createHash('sha256');
453
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
454
+ let bytes = 0;
455
+ try {
456
+ for (;;) {
457
+ const count = fs.readSync(fd, buffer, 0, buffer.length, null);
458
+ if (!count)
459
+ break;
460
+ hash.update(buffer.subarray(0, count));
461
+ bytes += count;
462
+ }
463
+ return { sha256: hash.digest('hex'), bytes };
464
+ }
465
+ finally {
466
+ fs.closeSync(fd);
467
+ }
468
+ }
469
+ function inspectTranscript(metadataPath, sessionId, requestedCwd) {
470
+ const observation = metadataRecords(metadataPath).find((record) => record['type'] === 'session_start' && record['session_id'] === sessionId);
471
+ if (!observation || typeof observation['session_file'] !== 'string') {
472
+ return {
473
+ status: 'not_observed',
474
+ path: null,
475
+ sessionId: null,
476
+ cwd: null,
477
+ sha256: null,
478
+ bytes: null,
479
+ detail: 'Pi session_start did not report the new transcript path',
480
+ };
481
+ }
482
+ const file = observation['session_file'];
483
+ const observedCwd = typeof observation['cwd'] === 'string' ? observation['cwd'] : null;
484
+ if (!fs.existsSync(file)) {
485
+ return {
486
+ status: 'not_observed',
487
+ path: file,
488
+ sessionId,
489
+ cwd: observedCwd,
490
+ sha256: null,
491
+ bytes: null,
492
+ detail: 'Pi has not persisted the session transcript yet',
493
+ };
494
+ }
495
+ try {
496
+ const stat = fs.lstatSync(file);
497
+ if (!stat.isFile() || stat.isSymbolicLink())
498
+ throw new ManagedPiSessionError('transcript_not_regular');
499
+ const fd = fs.openSync(file, 'r');
500
+ let firstLine;
501
+ try {
502
+ const header = Buffer.alloc(64 * 1024);
503
+ const count = fs.readSync(fd, header, 0, header.length, 0);
504
+ firstLine =
505
+ header.subarray(0, count).toString('utf8').split('\n', 1)[0] ?? '';
506
+ }
507
+ finally {
508
+ fs.closeSync(fd);
509
+ }
510
+ const entry = JSON.parse(firstLine);
511
+ if (typeof entry !== 'object' || entry === null)
512
+ throw new ManagedPiSessionError('transcript_header_invalid');
513
+ const header = entry;
514
+ const content = hashFile(file);
515
+ const sameCwd = observedCwd !== null &&
516
+ (observedCwd === requestedCwd ||
517
+ path.resolve(observedCwd) === path.resolve(requestedCwd));
518
+ if (header['type'] !== 'session' ||
519
+ header['id'] !== sessionId ||
520
+ header['cwd'] !== observedCwd ||
521
+ !sameCwd) {
522
+ return {
523
+ status: 'mismatch',
524
+ path: file,
525
+ sessionId,
526
+ cwd: typeof header['cwd'] === 'string' ? header['cwd'] : observedCwd,
527
+ sha256: content.sha256,
528
+ bytes: content.bytes,
529
+ detail: 'transcript header identity/cwd mismatch',
530
+ };
531
+ }
532
+ return {
533
+ status: 'verified',
534
+ path: file,
535
+ sessionId,
536
+ cwd: observedCwd,
537
+ sha256: content.sha256,
538
+ bytes: content.bytes,
539
+ detail: null,
540
+ };
541
+ }
542
+ catch (error) {
543
+ return {
544
+ status: 'unreadable',
545
+ path: file,
546
+ sessionId,
547
+ cwd: observedCwd,
548
+ sha256: null,
549
+ bytes: null,
550
+ detail: error instanceof Error ? error.message : String(error),
551
+ };
552
+ }
553
+ }
554
+ function validTime(value) {
555
+ const parsed = Date.parse(value);
556
+ return Number.isFinite(parsed) ? parsed : null;
557
+ }
558
+ export function validateManagedPiArchiveEvidence(input) {
559
+ if (!input.sourceExited)
560
+ return 'source_child_not_confirmed_exited';
561
+ if (input.transcript.status !== 'verified' ||
562
+ !input.transcript.path ||
563
+ !input.transcript.sha256)
564
+ return 'original_transcript_not_verified';
565
+ const confirmation = input.receiverConfirmation;
566
+ const work = input.firstWork;
567
+ if (!confirmation.receiverSessionId.trim() ||
568
+ !confirmation.confirmationId.trim())
569
+ return 'receiver_confirmation_missing';
570
+ if (!work.receiverSessionId.trim() || !work.workId.trim())
571
+ return 'first_work_evidence_missing';
572
+ if (confirmation.receiverSessionId !== work.receiverSessionId)
573
+ return 'receiver_identity_mismatch';
574
+ const confirmedAt = validTime(confirmation.confirmedAt);
575
+ const workedAt = validTime(work.observedAt);
576
+ if (confirmedAt === null || workedAt === null)
577
+ return 'receipt_timestamp_invalid';
578
+ if (workedAt < confirmedAt)
579
+ return 'first_work_precedes_receiver_confirmation';
580
+ return null;
581
+ }
582
+ function copyTranscript(source, destination) {
583
+ if (!path.isAbsolute(destination))
584
+ return { ok: false, reason: 'archive_path_must_be_absolute' };
585
+ const target = path.resolve(destination);
586
+ if (target === source.path)
587
+ return { ok: false, reason: 'archive_is_original_path' };
588
+ try {
589
+ requirePrivateParent(target);
590
+ if (fs.lstatSync(target, { throwIfNoEntry: false }))
591
+ return { ok: false, reason: 'archive_target_exists' };
592
+ }
593
+ catch (error) {
594
+ return {
595
+ ok: false,
596
+ reason: error instanceof Error ? error.message : String(error),
597
+ };
598
+ }
599
+ let sourceFd;
600
+ let destinationFd;
601
+ let created = false;
602
+ try {
603
+ const before = fs.statSync(source.path, { bigint: true });
604
+ if (before.dev !== source.dev ||
605
+ before.ino !== source.ino ||
606
+ before.size !== BigInt(source.bytes) ||
607
+ before.mtimeNs !== source.mtimeNs)
608
+ return { ok: false, reason: 'original_transcript_changed' };
609
+ sourceFd = fs.openSync(source.path, 'r');
610
+ destinationFd = fs.openSync(target, 'wx', 0o600);
611
+ created = true;
612
+ const hash = createHash('sha256');
613
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
614
+ let bytes = 0;
615
+ for (;;) {
616
+ const count = fs.readSync(sourceFd, buffer, 0, buffer.length, null);
617
+ if (!count)
618
+ break;
619
+ const chunk = buffer.subarray(0, count);
620
+ writeAll(destinationFd, chunk);
621
+ hash.update(chunk);
622
+ bytes += count;
623
+ }
624
+ const sha256 = hash.digest('hex');
625
+ fs.fsyncSync(destinationFd);
626
+ fs.closeSync(destinationFd);
627
+ destinationFd = undefined;
628
+ fs.closeSync(sourceFd);
629
+ sourceFd = undefined;
630
+ const after = fs.statSync(source.path, { bigint: true });
631
+ if (sha256 !== source.sha256 ||
632
+ bytes !== source.bytes ||
633
+ after.dev !== source.dev ||
634
+ after.ino !== source.ino ||
635
+ after.size !== BigInt(source.bytes) ||
636
+ after.mtimeNs !== source.mtimeNs) {
637
+ fs.unlinkSync(target);
638
+ created = false;
639
+ return { ok: false, reason: 'original_transcript_changed' };
640
+ }
641
+ fsyncDirectory(path.dirname(target));
642
+ return { ok: true, archivePath: target, sha256, bytes };
643
+ }
644
+ catch (error) {
645
+ if (destinationFd !== undefined)
646
+ try {
647
+ fs.closeSync(destinationFd);
648
+ }
649
+ catch {
650
+ /* best effort */
651
+ }
652
+ if (sourceFd !== undefined)
653
+ try {
654
+ fs.closeSync(sourceFd);
655
+ }
656
+ catch {
657
+ /* best effort */
658
+ }
659
+ if (created)
660
+ try {
661
+ fs.unlinkSync(target);
662
+ }
663
+ catch {
664
+ /* only this call's target */
665
+ }
666
+ return {
667
+ ok: false,
668
+ reason: error instanceof Error ? error.message : String(error),
669
+ };
670
+ }
671
+ }
672
+ function boundedWait(promise, milliseconds) {
673
+ let timer;
674
+ return Promise.race([
675
+ promise,
676
+ new Promise((resolve) => {
677
+ timer = setTimeout(() => resolve(null), milliseconds);
678
+ }),
679
+ ]).finally(() => {
680
+ if (timer)
681
+ clearTimeout(timer);
682
+ });
683
+ }
684
+ function tokenMatches(received, expected) {
685
+ if (typeof received !== 'string')
686
+ return false;
687
+ const left = Buffer.from(received);
688
+ const right = Buffer.from(expected);
689
+ return left.length === right.length && timingSafeEqual(left, right);
690
+ }
691
+ function respond(socket, value) {
692
+ if (!socket.destroyed)
693
+ socket.end(`${JSON.stringify(value)}\n`);
694
+ }
695
+ async function handleControl(socket, token, getSession) {
696
+ let text = '';
697
+ let handled = false;
698
+ // A disconnected client cannot crash or fence the owned session.
699
+ socket.on('error', () => socket.destroy());
700
+ socket.setTimeout(75_000, () => socket.destroy());
701
+ socket.on('data', (chunk) => {
702
+ if (handled)
703
+ return;
704
+ text += chunk.toString('utf8');
705
+ if (Buffer.byteLength(text) > MAX_CONTROL_BYTES) {
706
+ handled = true;
707
+ respond(socket, { ok: false, reason: 'request_too_large' });
708
+ return;
709
+ }
710
+ const newline = text.indexOf('\n');
711
+ if (newline < 0)
712
+ return;
713
+ handled = true;
714
+ let value;
715
+ try {
716
+ value = JSON.parse(text.slice(0, newline));
717
+ }
718
+ catch {
719
+ respond(socket, { ok: false, reason: 'invalid_request' });
720
+ return;
721
+ }
722
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
723
+ respond(socket, { ok: false, reason: 'invalid_request' });
724
+ return;
725
+ }
726
+ const request = value;
727
+ if (!tokenMatches(request.token, token)) {
728
+ respond(socket, { ok: false, reason: 'unauthorized' });
729
+ return;
730
+ }
731
+ const session = getSession();
732
+ if (!session) {
733
+ respond(socket, { ok: false, reason: 'session_not_ready' });
734
+ return;
735
+ }
736
+ try {
737
+ if (request.command === 'snapshot') {
738
+ respond(socket, { ok: true, snapshot: session.snapshot() });
739
+ return;
740
+ }
741
+ if (request.command === 'fence') {
742
+ session.fenceIngress('authenticated_control_request');
743
+ respond(socket, { ok: true, snapshot: session.snapshot() });
744
+ return;
745
+ }
746
+ if (request.command === 'stop') {
747
+ void session.stopOwnedChild().then((exit) => respond(socket, { ok: true, exit }), () => respond(socket, { ok: false, reason: 'stop_failed' }));
748
+ return;
749
+ }
750
+ respond(socket, { ok: false, reason: 'unsupported_command' });
751
+ }
752
+ catch {
753
+ respond(socket, { ok: false, reason: 'control_operation_failed' });
754
+ }
755
+ });
756
+ }
757
+ /** Cross-process client for the private descriptor written beside the journal. */
758
+ function currentUserOwns(stat) {
759
+ return typeof process.getuid !== 'function' || stat.uid === process.getuid();
760
+ }
761
+ function readControlDescriptor(file) {
762
+ if (!path.isAbsolute(file))
763
+ throw new ManagedPiSessionError('descriptor_path_must_be_absolute');
764
+ let stat;
765
+ try {
766
+ stat = fs.lstatSync(file);
767
+ }
768
+ catch {
769
+ throw new ManagedPiSessionError('control_descriptor_unavailable');
770
+ }
771
+ if (!stat.isFile() ||
772
+ stat.isSymbolicLink() ||
773
+ (stat.mode & 0o077) !== 0 ||
774
+ !currentUserOwns(stat) ||
775
+ stat.size > 4096)
776
+ throw new ManagedPiSessionError('control_descriptor_not_private');
777
+ let fd;
778
+ let parsed;
779
+ try {
780
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
781
+ fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
782
+ const opened = fs.fstatSync(fd);
783
+ if (!opened.isFile() ||
784
+ opened.dev !== stat.dev ||
785
+ opened.ino !== stat.ino ||
786
+ opened.size > 4096)
787
+ throw new ManagedPiSessionError('control_descriptor_not_private');
788
+ parsed = JSON.parse(fs.readFileSync(fd, 'utf8'));
789
+ }
790
+ catch {
791
+ throw new ManagedPiSessionError('control_descriptor_invalid');
792
+ }
793
+ finally {
794
+ if (fd !== undefined)
795
+ fs.closeSync(fd);
796
+ }
797
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
798
+ throw new ManagedPiSessionError('control_descriptor_invalid');
799
+ const value = parsed;
800
+ if (value['schema'] !== CONTROL_SCHEMA ||
801
+ typeof value['socketPath'] !== 'string' ||
802
+ !path.isAbsolute(value['socketPath']) ||
803
+ Buffer.byteLength(value['socketPath']) > MAX_SOCKET_PATH_BYTES ||
804
+ typeof value['token'] !== 'string' ||
805
+ !/^[a-f0-9]{64}$/.test(value['token']) ||
806
+ typeof value['runId'] !== 'string' ||
807
+ !value['runId'] ||
808
+ value['runId'].length > 256 ||
809
+ typeof value['sessionId'] !== 'string' ||
810
+ !value['sessionId'] ||
811
+ value['sessionId'].length > 256)
812
+ throw new ManagedPiSessionError('control_descriptor_invalid');
813
+ let socketStat;
814
+ try {
815
+ socketStat = fs.lstatSync(value['socketPath']);
816
+ }
817
+ catch {
818
+ throw new ManagedPiSessionError('control_socket_unavailable');
819
+ }
820
+ if (!socketStat.isSocket() ||
821
+ (socketStat.mode & 0o077) !== 0 ||
822
+ !currentUserOwns(socketStat))
823
+ throw new ManagedPiSessionError('control_socket_not_private');
824
+ return value;
825
+ }
826
+ /** Cross-process client for the private descriptor written beside the journal. */
827
+ export async function requestManagedPiControl(descriptorPath, action, payload = {}) {
828
+ const descriptor = readControlDescriptor(descriptorPath);
829
+ return await new Promise((resolve, reject) => {
830
+ const socket = net.createConnection(descriptor.socketPath);
831
+ const responseChunks = [];
832
+ let responseBytes = 0;
833
+ let settled = false;
834
+ const fail = (code) => {
835
+ if (settled)
836
+ return;
837
+ settled = true;
838
+ socket.destroy();
839
+ reject(new ManagedPiSessionError(code));
840
+ };
841
+ socket.setTimeout(75_000, () => fail('control_request_timeout'));
842
+ socket.once('connect', () => {
843
+ socket.write(`${JSON.stringify({ ...payload, token: descriptor.token, command: action })}\n`);
844
+ });
845
+ socket.on('data', (chunk) => {
846
+ if (settled)
847
+ return;
848
+ responseBytes += chunk.length;
849
+ if (responseBytes > MAX_CONTROL_RESPONSE_BYTES) {
850
+ fail('control_response_too_large');
851
+ return;
852
+ }
853
+ responseChunks.push(chunk);
854
+ if (!chunk.includes(0x0a))
855
+ return;
856
+ const response = Buffer.concat(responseChunks).toString('utf8');
857
+ const newline = response.indexOf('\n');
858
+ let value;
859
+ try {
860
+ value = JSON.parse(response.slice(0, newline));
861
+ }
862
+ catch {
863
+ fail('control_response_invalid');
864
+ return;
865
+ }
866
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
867
+ fail('control_response_invalid');
868
+ return;
869
+ }
870
+ settled = true;
871
+ resolve(value);
872
+ socket.end();
873
+ });
874
+ socket.once('end', () => {
875
+ if (!settled)
876
+ fail('control_response_incomplete');
877
+ });
878
+ socket.once('close', () => {
879
+ if (!settled)
880
+ fail('control_response_incomplete');
881
+ });
882
+ socket.once('error', () => fail('control_socket_unavailable'));
883
+ });
884
+ }
885
+ class ManagedPiSessionController {
886
+ journalPath;
887
+ controlEndpoint;
888
+ processIdentity;
889
+ journal;
890
+ metadataPath;
891
+ options;
892
+ child;
893
+ status;
894
+ control;
895
+ server;
896
+ socketStat;
897
+ statusText = '';
898
+ fenced = false;
899
+ helperExit = null;
900
+ piExit = null;
901
+ resolveStarted;
902
+ rejectStarted;
903
+ startSettled = false;
904
+ resolvePiExit;
905
+ resolveHelperExit;
906
+ started;
907
+ piExitPromise;
908
+ helperExitPromise;
909
+ inputData;
910
+ resize;
911
+ outputDrain;
912
+ terminalError;
913
+ rawBefore;
914
+ closed = false;
915
+ signalStopping = false;
916
+ terminationSignal = () => {
917
+ if (this.signalStopping)
918
+ return;
919
+ this.signalStopping = true;
920
+ this.restoreTerminal();
921
+ void this.stopOwnedChild().catch(() => this.failIo('signal_shutdown_failed'));
922
+ };
923
+ constructor(input) {
924
+ this.options = input.options;
925
+ this.processIdentity = input.identity;
926
+ this.metadataPath = input.metadataPath;
927
+ this.journal = input.journal;
928
+ this.journalPath = input.journal.path;
929
+ this.child = input.child;
930
+ this.server = input.server;
931
+ this.socketStat = input.socketStat;
932
+ this.controlEndpoint = input.controlEndpoint;
933
+ const status = input.child.stdio[3];
934
+ const control = input.child.stdio[4];
935
+ if (!status || !control)
936
+ throw new ManagedPiSessionError('pty_control_channel_missing');
937
+ this.status = status;
938
+ this.control = control;
939
+ this.rawBefore = process.stdin.isRaw === true;
940
+ this.started = new Promise((resolve, reject) => {
941
+ this.resolveStarted = resolve;
942
+ this.rejectStarted = reject;
943
+ });
944
+ this.piExitPromise = new Promise((resolve) => {
945
+ this.resolvePiExit = resolve;
946
+ });
947
+ this.helperExitPromise = new Promise((resolve) => {
948
+ this.resolveHelperExit = resolve;
949
+ });
950
+ this.inputData = (chunk) => {
951
+ if (this.fenced)
952
+ return;
953
+ const bytes = Buffer.from(chunk);
954
+ const sent = forwardJournaledManagedInput(this.journal, bytes, (recorded) => {
955
+ if (!this.child.stdin?.writable)
956
+ throw new ManagedPiSessionError('pty_input_closed');
957
+ if (!this.child.stdin.write(recorded))
958
+ process.stdin.pause();
959
+ });
960
+ if (!sent)
961
+ this.fenceIngress('journal_or_pty_input_failed');
962
+ };
963
+ this.resize = () => this.sendResize();
964
+ this.outputDrain = () => this.child.stdout?.resume();
965
+ this.terminalError = () => this.failIo('terminal_stream_failed');
966
+ for (const stream of [
967
+ this.child.stdin,
968
+ this.child.stdout,
969
+ this.child.stderr,
970
+ this.status,
971
+ this.control,
972
+ ]) {
973
+ stream?.on('error', () => this.failIo('pty_stream_failed'));
974
+ }
975
+ process.stdin.on('error', this.terminalError);
976
+ process.stdout.on('error', this.terminalError);
977
+ process.stderr.on('error', this.terminalError);
978
+ this.child.stdin?.on('drain', () => {
979
+ if (!this.fenced)
980
+ process.stdin.resume();
981
+ });
982
+ this.child.stdout?.on('data', (chunk) => {
983
+ if (!process.stdout.write(chunk))
984
+ this.child.stdout?.pause();
985
+ });
986
+ process.stdout.on('drain', this.outputDrain);
987
+ this.child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
988
+ this.status.on('data', (chunk) => this.onStatus(chunk));
989
+ this.status.on('end', () => {
990
+ if (this.processIdentity.piPid === 0) {
991
+ this.failStart(new ManagedPiSessionError('pi_child_identity_unavailable'));
992
+ this.finishUnknown('bridge_status_ended_before_child_identity');
993
+ }
994
+ else if (this.piExit === null) {
995
+ this.finishUnknown('bridge_status_ended_without_pi_exit');
996
+ }
997
+ });
998
+ this.child.on('error', () => this.failIo('pty_bridge_failed'));
999
+ this.child.once('close', () => {
1000
+ process.off('SIGTERM', this.terminationSignal);
1001
+ process.off('SIGHUP', this.terminationSignal);
1002
+ process.stdout.removeListener('drain', this.outputDrain);
1003
+ process.stdin.removeListener('error', this.terminalError);
1004
+ process.stdout.removeListener('error', this.terminalError);
1005
+ process.stderr.removeListener('error', this.terminalError);
1006
+ });
1007
+ this.child.on('exit', (code, signal) => {
1008
+ this.helperExit = { code: code ?? null, signal: signal ?? null };
1009
+ this.resolveHelperExit();
1010
+ if (this.piExit !== null) {
1011
+ this.piExit = {
1012
+ ...this.piExit,
1013
+ helperExitCode: code ?? null,
1014
+ helperSignal: signal ?? null,
1015
+ };
1016
+ }
1017
+ this.restoreTerminal();
1018
+ });
1019
+ process.stdin.setRawMode(true);
1020
+ process.stdin.resume();
1021
+ process.stdin.on('data', this.inputData);
1022
+ process.stdout.on('resize', this.resize);
1023
+ process.on('SIGWINCH', this.resize);
1024
+ process.on('SIGTERM', this.terminationSignal);
1025
+ process.on('SIGHUP', this.terminationSignal);
1026
+ }
1027
+ get identity() {
1028
+ return { ...this.processIdentity };
1029
+ }
1030
+ async waitUntilReady(timeoutMs = 30_000) {
1031
+ await this.started;
1032
+ const deadline = Date.now() + timeoutMs;
1033
+ while (Date.now() < deadline) {
1034
+ if (this.piExit !== null)
1035
+ throw new ManagedPiSessionError('pi_exited_before_session_ready');
1036
+ const initialized = metadataRecords(this.metadataPath).some((record) => record['type'] === 'session_start' &&
1037
+ record['session_id'] === this.processIdentity.sessionId &&
1038
+ typeof record['cwd'] === 'string' &&
1039
+ (record['cwd'] === this.options.cwd ||
1040
+ path.resolve(record['cwd']) === path.resolve(this.options.cwd)));
1041
+ if (initialized)
1042
+ return;
1043
+ await new Promise((resolve) => setTimeout(resolve, 50));
1044
+ }
1045
+ throw new ManagedPiSessionError('pi_session_start_not_observed');
1046
+ }
1047
+ settleStart(error) {
1048
+ if (this.startSettled)
1049
+ return;
1050
+ this.startSettled = true;
1051
+ if (error)
1052
+ this.rejectStarted(error);
1053
+ else
1054
+ this.resolveStarted();
1055
+ }
1056
+ failStart(error) {
1057
+ this.settleStart(error);
1058
+ this.fenceIngress('startup_or_bridge_failure');
1059
+ }
1060
+ failIo(reason) {
1061
+ this.failStart(new ManagedPiSessionError(reason));
1062
+ this.finishUnknown(reason);
1063
+ }
1064
+ finishUnknown(reason) {
1065
+ if (this.piExit !== null)
1066
+ return;
1067
+ this.piExit = {
1068
+ status: 'unknown',
1069
+ piPid: this.processIdentity.piPid,
1070
+ exitCode: null,
1071
+ signal: null,
1072
+ helperExitCode: this.helperExit?.code ?? null,
1073
+ helperSignal: this.helperExit?.signal ?? null,
1074
+ reason,
1075
+ };
1076
+ this.resolvePiExit(this.piExit);
1077
+ }
1078
+ onStatus(chunk) {
1079
+ this.statusText += chunk.toString('utf8');
1080
+ if (Buffer.byteLength(this.statusText) > MAX_CONTROL_BYTES) {
1081
+ this.failStart(new ManagedPiSessionError('pty_status_too_large'));
1082
+ this.statusText = '';
1083
+ return;
1084
+ }
1085
+ let at = this.statusText.indexOf('\n');
1086
+ while (at >= 0) {
1087
+ const line = this.statusText.slice(0, at).trim();
1088
+ this.statusText = this.statusText.slice(at + 1);
1089
+ if (line)
1090
+ this.acceptStatusEvent(line);
1091
+ at = this.statusText.indexOf('\n');
1092
+ }
1093
+ }
1094
+ acceptStatusEvent(line) {
1095
+ const event = parseBridgeEvent(line);
1096
+ if (!event ||
1097
+ event['runId'] !== this.processIdentity.runId ||
1098
+ event['helperPid'] !== this.processIdentity.helperPid) {
1099
+ this.failStart(new ManagedPiSessionError('pty_identity_mismatch'));
1100
+ return;
1101
+ }
1102
+ if (event['kind'] === 'pi_started') {
1103
+ if (this.processIdentity.piPid !== 0 ||
1104
+ !Number.isSafeInteger(event['piPid']) ||
1105
+ Number(event['piPid']) <= 0) {
1106
+ this.failStart(new ManagedPiSessionError('pty_identity_mismatch'));
1107
+ return;
1108
+ }
1109
+ this.processIdentity = {
1110
+ ...this.processIdentity,
1111
+ piPid: Number(event['piPid']),
1112
+ };
1113
+ try {
1114
+ this.journal.append('owned_pi_child_started', {
1115
+ helper_pid: this.processIdentity.helperPid,
1116
+ pi_pid: this.processIdentity.piPid,
1117
+ session_id: this.processIdentity.sessionId,
1118
+ });
1119
+ this.settleStart();
1120
+ this.sendResize();
1121
+ }
1122
+ catch (error) {
1123
+ this.failStart(error instanceof Error ? error : new Error(String(error)));
1124
+ }
1125
+ return;
1126
+ }
1127
+ const exitEvent = {
1128
+ runId: String(event['runId']),
1129
+ helperPid: Number(event['helperPid']),
1130
+ piPid: Number(event['piPid']),
1131
+ exitCode: typeof event['exitCode'] === 'number' ? event['exitCode'] : null,
1132
+ signal: typeof event['signal'] === 'string' ? event['signal'] : null,
1133
+ };
1134
+ if (this.processIdentity.piPid <= 0 ||
1135
+ !isOwnedManagedPiExit(exitEvent, this.processIdentity))
1136
+ return;
1137
+ this.piExit = {
1138
+ status: 'exited',
1139
+ piPid: exitEvent.piPid,
1140
+ exitCode: exitEvent.exitCode,
1141
+ signal: exitEvent.signal,
1142
+ helperExitCode: this.helperExit?.code ?? null,
1143
+ helperSignal: this.helperExit?.signal ?? null,
1144
+ reason: null,
1145
+ };
1146
+ try {
1147
+ this.journal.append('owned_pi_child_exited', {
1148
+ pi_pid: exitEvent.piPid,
1149
+ exit_code: exitEvent.exitCode,
1150
+ signal: exitEvent.signal,
1151
+ });
1152
+ }
1153
+ catch {
1154
+ /* exact waitpid exit remains distinguishable from journal health */
1155
+ }
1156
+ this.resolvePiExit(this.piExit);
1157
+ this.fenceIngress('owned_pi_child_exited');
1158
+ }
1159
+ sendControl(record) {
1160
+ if (this.control.destroyed || !this.control.writable)
1161
+ throw new ManagedPiSessionError('control_pipe_closed');
1162
+ this.control.write(`${JSON.stringify(record)}\n`);
1163
+ }
1164
+ sendResize() {
1165
+ if (this.processIdentity.piPid <= 0 || this.piExit !== null || this.closed)
1166
+ return;
1167
+ try {
1168
+ this.sendControl({
1169
+ command: 'resize',
1170
+ rows: process.stdout.rows ?? 24,
1171
+ columns: process.stdout.columns ?? 80,
1172
+ });
1173
+ }
1174
+ catch {
1175
+ /* presentation resize failure does not change process-exit evidence */
1176
+ }
1177
+ }
1178
+ restoreTerminal() {
1179
+ this.fenceIngress('owned_process_finished');
1180
+ try {
1181
+ process.stdin.setRawMode(this.rawBefore);
1182
+ }
1183
+ catch {
1184
+ /* terminal may already be detached */
1185
+ }
1186
+ }
1187
+ fenceIngress(reason = 'controller_requested') {
1188
+ if (this.fenced)
1189
+ return;
1190
+ this.fenced = true;
1191
+ process.stdin.removeListener('data', this.inputData);
1192
+ process.stdin.pause();
1193
+ process.stdout.removeListener('resize', this.resize);
1194
+ process.removeListener('SIGWINCH', this.resize);
1195
+ this.journal.fence(reason);
1196
+ try {
1197
+ process.stdin.setRawMode(this.rawBefore);
1198
+ }
1199
+ catch {
1200
+ /* terminal may already be detached */
1201
+ }
1202
+ }
1203
+ snapshot() {
1204
+ const transcript = inspectTranscript(this.metadataPath, this.processIdentity.sessionId, this.options.cwd);
1205
+ const blockers = [
1206
+ 'native_queue_payloads_unknown',
1207
+ 'native_approval_state_unknown',
1208
+ 'in_flight_work_inventory_unknown',
1209
+ 'external_writers_not_fenced',
1210
+ 'descendant_processes_not_owned_or_stopped',
1211
+ ];
1212
+ if (!this.journal.healthy)
1213
+ blockers.push('ingress_journal_unhealthy');
1214
+ if (transcript.status !== 'verified')
1215
+ blockers.push('original_transcript_unverified');
1216
+ return {
1217
+ schema: MANAGED_PI_SESSION_SCHEMA,
1218
+ runId: this.processIdentity.runId,
1219
+ provider: 'openrouter',
1220
+ model: this.options.model,
1221
+ cwd: this.options.cwd,
1222
+ sessionId: this.processIdentity.sessionId,
1223
+ process: this.identity,
1224
+ processExit: this.piExit,
1225
+ readiness: {
1226
+ ownedPiChild: 'started',
1227
+ extensionSessionStart: 'observed',
1228
+ terminalUiRender: 'not_observed',
1229
+ },
1230
+ ingress: {
1231
+ state: this.fenced ? 'fenced' : 'open',
1232
+ journalPath: this.journalPath,
1233
+ sequence: this.journal.currentSequence,
1234
+ headDigest: this.journal.currentDigest,
1235
+ healthy: this.journal.healthy,
1236
+ rawTerminalInputJournaled: false,
1237
+ terminalInputMetadataJournaled: true,
1238
+ },
1239
+ transcript,
1240
+ nativeInventory: {
1241
+ queuedMessages: 'unknown',
1242
+ approvals: 'unknown',
1243
+ inFlightWork: 'unknown',
1244
+ externalWriters: 'unknown',
1245
+ descendants: 'uncontrolled',
1246
+ },
1247
+ transferReady: false,
1248
+ blockers,
1249
+ };
1250
+ }
1251
+ async waitForExit() {
1252
+ const exit = await this.piExitPromise;
1253
+ if (exit.status === 'unknown')
1254
+ return exit;
1255
+ const helperWaited = await boundedWait(this.helperExitPromise, 2_000);
1256
+ return {
1257
+ ...this.piExit,
1258
+ helperExitCode: this.helperExit?.code ?? null,
1259
+ helperSignal: this.helperExit?.signal ?? null,
1260
+ reason: helperWaited === null
1261
+ ? 'pi_exit_confirmed_helper_exit_not_observed'
1262
+ : null,
1263
+ };
1264
+ }
1265
+ async stopOwnedChild() {
1266
+ this.fenceIngress('owned_child_stop_requested');
1267
+ if (this.piExit !== null)
1268
+ return this.waitForExit();
1269
+ if (this.processIdentity.piPid <= 0)
1270
+ return {
1271
+ status: 'unknown',
1272
+ piPid: 0,
1273
+ exitCode: null,
1274
+ signal: null,
1275
+ helperExitCode: this.helperExit?.code ?? null,
1276
+ helperSignal: this.helperExit?.signal ?? null,
1277
+ reason: 'pi_child_identity_unavailable',
1278
+ };
1279
+ try {
1280
+ this.sendControl({ command: 'signal', signal: 'TERM' });
1281
+ }
1282
+ catch {
1283
+ return this.unknown('owned_child_signal_unavailable');
1284
+ }
1285
+ let exit = await boundedWait(this.piExitPromise, this.options.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS);
1286
+ if (exit === null) {
1287
+ try {
1288
+ this.sendControl({ command: 'signal', signal: 'KILL' });
1289
+ }
1290
+ catch {
1291
+ return this.unknown('owned_child_kill_unavailable');
1292
+ }
1293
+ exit = await boundedWait(this.piExitPromise, KILL_GRACE_MS);
1294
+ }
1295
+ if (exit === null)
1296
+ return this.unknown('owned_child_exit_not_observed');
1297
+ return this.waitForExit();
1298
+ }
1299
+ unknown(reason) {
1300
+ return {
1301
+ status: 'unknown',
1302
+ piPid: this.processIdentity.piPid,
1303
+ exitCode: null,
1304
+ signal: null,
1305
+ helperExitCode: this.helperExit?.code ?? null,
1306
+ helperSignal: this.helperExit?.signal ?? null,
1307
+ reason,
1308
+ };
1309
+ }
1310
+ archiveOriginalTranscript(input) {
1311
+ const snapshot = this.snapshot();
1312
+ const reason = validateManagedPiArchiveEvidence({
1313
+ sourceExited: snapshot.processExit?.status === 'exited' && this.helperExit !== null,
1314
+ transcript: snapshot.transcript,
1315
+ receiverConfirmation: input.receiverConfirmation,
1316
+ firstWork: input.firstWork,
1317
+ });
1318
+ if (reason)
1319
+ return { ok: false, reason };
1320
+ const observed = snapshot.transcript;
1321
+ if (!observed.path || !observed.sha256 || observed.bytes === null)
1322
+ return { ok: false, reason: 'original_transcript_not_verified' };
1323
+ const stat = fs.statSync(observed.path, { bigint: true });
1324
+ const source = {
1325
+ path: observed.path,
1326
+ sha256: observed.sha256,
1327
+ bytes: observed.bytes,
1328
+ dev: stat.dev,
1329
+ ino: stat.ino,
1330
+ mtimeNs: stat.mtimeNs,
1331
+ };
1332
+ try {
1333
+ this.journal.append('transcript_archive_authorized', {
1334
+ receiver_session_id: input.receiverConfirmation.receiverSessionId,
1335
+ confirmation_id: input.receiverConfirmation.confirmationId,
1336
+ confirmed_at: input.receiverConfirmation.confirmedAt,
1337
+ first_work_id: input.firstWork.workId,
1338
+ first_work_observed_at: input.firstWork.observedAt,
1339
+ original_transcript_sha256: observed.sha256,
1340
+ });
1341
+ }
1342
+ catch {
1343
+ return { ok: false, reason: 'archive_authorization_not_journaled' };
1344
+ }
1345
+ const result = copyTranscript(source, input.destinationPath);
1346
+ if (result.ok) {
1347
+ try {
1348
+ this.journal.append('transcript_archive_created', result);
1349
+ }
1350
+ catch {
1351
+ return {
1352
+ ok: false,
1353
+ reason: 'archive_created_but_completion_not_journaled',
1354
+ };
1355
+ }
1356
+ }
1357
+ return result;
1358
+ }
1359
+ close() {
1360
+ if (this.closed)
1361
+ return;
1362
+ if (!this.piExit || !this.helperExit)
1363
+ throw new ManagedPiSessionError('cannot_close_live_session');
1364
+ this.closed = true;
1365
+ process.off('SIGTERM', this.terminationSignal);
1366
+ process.off('SIGHUP', this.terminationSignal);
1367
+ this.fenceIngress('controller_closed');
1368
+ this.server.close();
1369
+ try {
1370
+ const current = fs.lstatSync(this.controlEndpoint.socketPath);
1371
+ if (current.isSocket() &&
1372
+ current.dev === this.socketStat.dev &&
1373
+ current.ino === this.socketStat.ino)
1374
+ fs.unlinkSync(this.controlEndpoint.socketPath);
1375
+ }
1376
+ catch {
1377
+ /* never remove a path replaced after bind */
1378
+ }
1379
+ this.journal.close();
1380
+ }
1381
+ }
1382
+ /** Start Pi as an owned visible TUI; its native screen is passed directly through the PTY. */
1383
+ export async function startManagedPiSession(options) {
1384
+ if (!process.stdin.isTTY ||
1385
+ !process.stdout.isTTY ||
1386
+ typeof process.stdin.setRawMode !== 'function')
1387
+ throw new ManagedPiSessionError('native_tui_requires_terminal');
1388
+ if (!path.isAbsolute(options.cwd) ||
1389
+ !fs.statSync(options.cwd, { throwIfNoEntry: false })?.isDirectory())
1390
+ throw new ManagedPiSessionError('cwd_must_be_an_absolute_directory');
1391
+ if (!options.model.trim())
1392
+ throw new ManagedPiSessionError('model_required');
1393
+ if (options.shutdownGraceMs !== undefined &&
1394
+ (!Number.isSafeInteger(options.shutdownGraceMs) ||
1395
+ options.shutdownGraceMs < 0 ||
1396
+ options.shutdownGraceMs > 60_000))
1397
+ throw new ManagedPiSessionError('shutdown_grace_out_of_range');
1398
+ const artifacts = createArtifacts(options.journalPath);
1399
+ const runId = randomUUID();
1400
+ const sessionId = randomUUID();
1401
+ const journal = new ManagedPiJournal(artifacts.journalPath, artifacts.journalFd, runId);
1402
+ journal.append('launch_requested', {
1403
+ provider: 'openrouter',
1404
+ model: options.model,
1405
+ cwd: options.cwd,
1406
+ session_id: sessionId,
1407
+ terminal_mode: 'native-pi-tui-over-pty',
1408
+ });
1409
+ const socketPath = options.controlSocketPath ??
1410
+ path.join(path.dirname(artifacts.journalPath), `.pappra-${runId.slice(0, 10)}.sock`);
1411
+ if (!path.isAbsolute(socketPath) ||
1412
+ Buffer.byteLength(socketPath) > MAX_SOCKET_PATH_BYTES) {
1413
+ journal.close();
1414
+ throw new ManagedPiSessionError('control_socket_path_invalid');
1415
+ }
1416
+ requirePrivateParent(socketPath);
1417
+ if (fs.lstatSync(socketPath, { throwIfNoEntry: false })) {
1418
+ journal.close();
1419
+ throw new ManagedPiSessionError('control_socket_already_exists');
1420
+ }
1421
+ const token = randomBytes(32).toString('hex');
1422
+ const descriptorPath = `${artifacts.journalPath}.control.json`;
1423
+ if (fs.lstatSync(descriptorPath, { throwIfNoEntry: false })) {
1424
+ journal.close();
1425
+ throw new ManagedPiSessionError('control_descriptor_already_exists');
1426
+ }
1427
+ let controller = null;
1428
+ const server = net.createServer((socket) => {
1429
+ void handleControl(socket, token, () => controller);
1430
+ });
1431
+ server.on('error', () => controller?.fenceIngress('control_server_failed'));
1432
+ await new Promise((resolve, reject) => {
1433
+ server.once('error', reject);
1434
+ server.listen(socketPath, () => {
1435
+ server.removeListener('error', reject);
1436
+ resolve();
1437
+ });
1438
+ });
1439
+ fs.chmodSync(socketPath, 0o600);
1440
+ const socketStat = fs.lstatSync(socketPath);
1441
+ if (!socketStat.isSocket())
1442
+ throw new ManagedPiSessionError('control_socket_not_socket');
1443
+ const env = {
1444
+ ...process.env,
1445
+ [METADATA_ENV]: artifacts.metadataPath,
1446
+ PAPPRA_MANAGED_PI_ROWS: String(process.stdout.rows ?? 24),
1447
+ PAPPRA_MANAGED_PI_COLUMNS: String(process.stdout.columns ?? 80),
1448
+ };
1449
+ const piBin = options.piBin ?? process.env['PAPPRA_PI_BIN'] ?? 'pi';
1450
+ const argv = [
1451
+ piBin,
1452
+ ...buildManagedPiArgv({
1453
+ model: options.model,
1454
+ sessionId,
1455
+ extensionPath: extensionPath(),
1456
+ }),
1457
+ ];
1458
+ let child;
1459
+ try {
1460
+ child = spawn(options.pythonBin ?? 'python3', ['-u', '-c', PTY_BRIDGE_SOURCE, runId, ...argv], {
1461
+ cwd: options.cwd,
1462
+ env,
1463
+ stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
1464
+ });
1465
+ }
1466
+ catch (error) {
1467
+ server.close();
1468
+ try {
1469
+ fs.unlinkSync(socketPath);
1470
+ }
1471
+ catch {
1472
+ /* socket created by this launch */
1473
+ }
1474
+ try {
1475
+ journal.append('launch_failed', { reason: String(error) });
1476
+ }
1477
+ catch {
1478
+ /* keep original error */
1479
+ }
1480
+ journal.close();
1481
+ throw new ManagedPiSessionError('pty_bridge_spawn_failed', String(error));
1482
+ }
1483
+ try {
1484
+ const inputIdentity = {
1485
+ runId,
1486
+ helperPid: child.pid ?? 0,
1487
+ piPid: 0,
1488
+ sessionId,
1489
+ };
1490
+ controller = new ManagedPiSessionController({
1491
+ options,
1492
+ identity: inputIdentity,
1493
+ metadataPath: artifacts.metadataPath,
1494
+ journal,
1495
+ child,
1496
+ server,
1497
+ socketStat: { dev: socketStat.dev, ino: socketStat.ino },
1498
+ controlEndpoint: { socketPath, descriptorPath },
1499
+ });
1500
+ await waitForStart(controller, START_TIMEOUT_MS);
1501
+ const descriptor = {
1502
+ schema: CONTROL_SCHEMA,
1503
+ socketPath,
1504
+ token,
1505
+ runId,
1506
+ sessionId,
1507
+ };
1508
+ const descriptorFd = fs.openSync(descriptorPath, 'wx', 0o600);
1509
+ try {
1510
+ writeAll(descriptorFd, Buffer.from(`${JSON.stringify(descriptor)}\n`));
1511
+ fs.fsyncSync(descriptorFd);
1512
+ }
1513
+ finally {
1514
+ fs.closeSync(descriptorFd);
1515
+ }
1516
+ fsyncDirectory(path.dirname(descriptorPath));
1517
+ journal.append('controller_ready', {
1518
+ process: controller.identity,
1519
+ control_descriptor: descriptorPath,
1520
+ });
1521
+ return controller;
1522
+ }
1523
+ catch (error) {
1524
+ controller?.fenceIngress('startup_failed');
1525
+ if (controller && controller.identity.piPid > 0)
1526
+ await controller.stopOwnedChild();
1527
+ server.close();
1528
+ try {
1529
+ fs.unlinkSync(socketPath);
1530
+ }
1531
+ catch {
1532
+ /* only our socket */
1533
+ }
1534
+ try {
1535
+ journal.close();
1536
+ }
1537
+ catch {
1538
+ /* already closed */
1539
+ }
1540
+ throw error;
1541
+ }
1542
+ }
1543
+ async function waitForStart(session, timeoutMs) {
1544
+ let timer;
1545
+ try {
1546
+ await Promise.race([
1547
+ session.waitUntilReady(),
1548
+ new Promise((_resolve, reject) => {
1549
+ timer = setTimeout(() => reject(new ManagedPiSessionError('pi_start_timeout')), timeoutMs);
1550
+ }),
1551
+ ]);
1552
+ }
1553
+ finally {
1554
+ if (timer)
1555
+ clearTimeout(timer);
1556
+ }
1557
+ }