@sayknow-cli/coding-agent 0.4.7 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (428) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/cli/read-cli.d.ts +1 -0
  6. package/dist/types/cli.d.ts +9 -1
  7. package/dist/types/commands/daemon.d.ts +2 -2
  8. package/dist/types/commands/deep-interview.d.ts +9 -0
  9. package/dist/types/commands/harness.d.ts +22 -0
  10. package/dist/types/commands/read.d.ts +6 -0
  11. package/dist/types/commands/sdk.d.ts +20 -2
  12. package/dist/types/config/keybindings.d.ts +34 -4
  13. package/dist/types/config/model-profile-contract.d.ts +40 -0
  14. package/dist/types/config/model-profiles.d.ts +2 -3
  15. package/dist/types/config/model-registry.d.ts +5 -0
  16. package/dist/types/config/provider-auth-health.d.ts +14 -0
  17. package/dist/types/config/provider-ranking.d.ts +59 -0
  18. package/dist/types/config/settings-schema.d.ts +156 -10
  19. package/dist/types/config/settings.d.ts +3 -0
  20. package/dist/types/edit/modes/patch.d.ts +13 -0
  21. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  22. package/dist/types/eval/py/executor.d.ts +1 -1
  23. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  24. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  25. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  26. package/dist/types/extensibility/shared-events.d.ts +6 -0
  27. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  28. package/dist/types/main.d.ts +33 -2
  29. package/dist/types/memories/index.d.ts +2 -0
  30. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  31. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  32. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  35. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  36. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  37. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  38. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  39. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  41. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  42. package/dist/types/modes/components/welcome.d.ts +5 -0
  43. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  45. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  46. package/dist/types/modes/interactive-mode.d.ts +13 -2
  47. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  48. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  49. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  50. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  51. package/dist/types/modes/types.d.ts +6 -0
  52. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  53. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  54. package/dist/types/runtime/memory-domain.d.ts +9 -0
  55. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  56. package/dist/types/runtime/memory-guard.d.ts +39 -0
  57. package/dist/types/runtime/memory-limit.d.ts +11 -0
  58. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  59. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
  60. package/dist/types/runtime-mcp/manager.d.ts +9 -1
  61. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  62. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  63. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  64. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  65. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  66. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  67. package/dist/types/sdk/broker/broker.d.ts +2 -0
  68. package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
  69. package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
  70. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  71. package/dist/types/sdk/bus/config.d.ts +2 -0
  72. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  73. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  74. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  75. package/dist/types/sdk/bus/index.d.ts +52 -4
  76. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  77. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  78. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  79. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  80. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  81. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
  82. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  83. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  84. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
  85. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  86. package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
  87. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  88. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  89. package/dist/types/sdk/client/discovery.d.ts +2 -0
  90. package/dist/types/sdk/client/liveness.d.ts +7 -0
  91. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  92. package/dist/types/sdk/host/host.d.ts +3 -1
  93. package/dist/types/sdk/host/query/handlers.d.ts +14 -0
  94. package/dist/types/sdk/index.d.ts +2 -0
  95. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  96. package/dist/types/sdk/prompt-status.d.ts +88 -0
  97. package/dist/types/sdk/session-directory.d.ts +1 -1
  98. package/dist/types/sdk/session.d.ts +7 -0
  99. package/dist/types/sdk/startup-capability.d.ts +9 -0
  100. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  101. package/dist/types/sdk/transport/index.d.ts +12 -0
  102. package/dist/types/sdk/transport/relay.d.ts +43 -0
  103. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  104. package/dist/types/sdk/transport/socket.d.ts +7 -0
  105. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  106. package/dist/types/session/agent-session.d.ts +83 -24
  107. package/dist/types/session/btw-contract.d.ts +16 -0
  108. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  109. package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
  110. package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
  111. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  112. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  113. package/dist/types/session/session-manager.d.ts +89 -18
  114. package/dist/types/session/session-storage.d.ts +31 -0
  115. package/dist/types/session/streaming-output.d.ts +60 -5
  116. package/dist/types/setup/credential-import.d.ts +5 -1
  117. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  118. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
  119. package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
  120. package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
  121. package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
  122. package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
  123. package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
  124. package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
  125. package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
  126. package/dist/types/skc-runtime/state-writer.d.ts +5 -1
  127. package/dist/types/skc-runtime/team-launch.d.ts +1 -1
  128. package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
  129. package/dist/types/skc-runtime/team-store.d.ts +12 -0
  130. package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
  131. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
  132. package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
  133. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
  134. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
  135. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  136. package/dist/types/ssh/utils.d.ts +1 -0
  137. package/dist/types/task/discovery.d.ts +3 -1
  138. package/dist/types/task/executor.d.ts +6 -0
  139. package/dist/types/task/index.d.ts +3 -1
  140. package/dist/types/task/provider-retry-status.d.ts +13 -0
  141. package/dist/types/task/receipt.d.ts +2 -0
  142. package/dist/types/task/types.d.ts +133 -0
  143. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  144. package/dist/types/tools/browser/launch.d.ts +7 -0
  145. package/dist/types/tools/fetch.d.ts +22 -9
  146. package/dist/types/tools/image-gen.d.ts +63 -1
  147. package/dist/types/tools/index.d.ts +7 -1
  148. package/dist/types/tools/output-meta.d.ts +21 -1
  149. package/dist/types/tools/read-internals.d.ts +12 -0
  150. package/dist/types/tools/read.d.ts +45 -1
  151. package/dist/types/tools/resource-gc.d.ts +23 -0
  152. package/dist/types/tools/skill-discovery.d.ts +7 -0
  153. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  154. package/dist/types/tools/tool-result.d.ts +7 -1
  155. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  156. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  157. package/dist/types/web/insane/bridge.d.ts +1 -0
  158. package/dist/types/web/insane/url-guard.d.ts +17 -0
  159. package/package.json +8 -12
  160. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  161. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  162. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  163. package/scripts/compile-args.ts +0 -1
  164. package/scripts/dogfood-repository-binding.ts +209 -0
  165. package/scripts/generate-docs-index.ts +4 -1
  166. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  167. package/scripts/run-test-manifest.ts +6 -4
  168. package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
  169. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  170. package/src/capability/index.ts +58 -42
  171. package/src/capability/types.ts +3 -0
  172. package/src/cli/args.ts +16 -100
  173. package/src/cli/fast-help.ts +1 -1
  174. package/src/cli/mcp-cli.ts +4 -21
  175. package/src/cli/notify-cli.ts +6 -38
  176. package/src/cli/read-cli.ts +2 -1
  177. package/src/cli/setup-cli.ts +10 -4
  178. package/src/cli.ts +133 -11
  179. package/src/commands/daemon.ts +22 -5
  180. package/src/commands/deep-interview.ts +26 -1
  181. package/src/commands/harness.ts +35 -12
  182. package/src/commands/read.ts +11 -2
  183. package/src/commands/sdk.ts +134 -17
  184. package/src/commands/team.ts +1 -1
  185. package/src/commit/agentic/index.ts +3 -3
  186. package/src/commit/map-reduce/index.ts +2 -2
  187. package/src/config/keybindings.ts +158 -25
  188. package/src/config/model-profile-contract.ts +174 -0
  189. package/src/config/model-profiles.ts +51 -10
  190. package/src/config/model-registry.ts +24 -4
  191. package/src/config/model-resolver.ts +2 -2
  192. package/src/config/provider-auth-health.ts +42 -0
  193. package/src/config/provider-ranking.ts +119 -0
  194. package/src/config/settings-schema.ts +157 -10
  195. package/src/config/settings.ts +254 -108
  196. package/src/coordinator-mcp/model-preset.ts +21 -74
  197. package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
  198. package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
  199. package/src/defaults/skc/skills/team/SKILL.md +1 -1
  200. package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
  201. package/src/discovery/ssh.ts +12 -1
  202. package/src/edit/index.ts +3 -6
  203. package/src/edit/modes/patch.ts +38 -1
  204. package/src/edit/modes/replace.ts +42 -0
  205. package/src/edit/path-mutation-lock.ts +112 -0
  206. package/src/eval/js/tool-bridge.ts +1 -1
  207. package/src/eval/py/executor.ts +37 -13
  208. package/src/eval/py/kernel.ts +1 -21
  209. package/src/eval/py/prelude.py +2 -2
  210. package/src/eval/py/runner-artifact.ts +84 -0
  211. package/src/eval/py/tool-bridge.ts +34 -18
  212. package/src/extensibility/extensions/runner.ts +4 -0
  213. package/src/extensibility/extensions/types.ts +42 -4
  214. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  215. package/src/extensibility/shared-events.ts +5 -0
  216. package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
  217. package/src/hooks/native-skill-hook.ts +17 -4
  218. package/src/internal-urls/docs-index.generated.ts +17 -16
  219. package/src/internal-urls/local-protocol.ts +87 -25
  220. package/src/internal-urls/local-root-gc.ts +127 -0
  221. package/src/internal-urls/mcp-protocol.ts +73 -11
  222. package/src/main.ts +120 -42
  223. package/src/memories/index.ts +10 -0
  224. package/src/modes/DESIGN.md +52 -0
  225. package/src/modes/acp/acp-agent.ts +647 -83
  226. package/src/modes/acp/acp-event-mapper.ts +106 -12
  227. package/src/modes/acp/acp-mode.ts +39 -3
  228. package/src/modes/action-registry.ts +1 -1
  229. package/src/modes/components/assistant-message.ts +123 -22
  230. package/src/modes/components/btw-panel.ts +82 -16
  231. package/src/modes/components/irc-sidebar.ts +176 -62
  232. package/src/modes/components/model-selector.ts +61 -4
  233. package/src/modes/components/notifications-settings-editor.ts +1 -1
  234. package/src/modes/components/oauth-selector.ts +42 -12
  235. package/src/modes/components/plan-preview-overlay.ts +1 -1
  236. package/src/modes/components/queue-pane.ts +55 -9
  237. package/src/modes/components/queued-message-selector.ts +53 -10
  238. package/src/modes/components/read-tool-group.ts +82 -14
  239. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  240. package/src/modes/components/sayknow-pet-widget.ts +20 -8
  241. package/src/modes/components/session-observer-overlay.ts +187 -32
  242. package/src/modes/components/settings-selector.ts +60 -18
  243. package/src/modes/components/tool-execution.ts +138 -27
  244. package/src/modes/components/tool-status-header.ts +26 -8
  245. package/src/modes/components/welcome.ts +50 -23
  246. package/src/modes/controllers/btw-controller.ts +119 -36
  247. package/src/modes/controllers/command-controller.ts +101 -54
  248. package/src/modes/controllers/event-controller.ts +119 -27
  249. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  250. package/src/modes/controllers/input-controller.ts +100 -8
  251. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  252. package/src/modes/controllers/selector-controller.ts +4 -1
  253. package/src/modes/interactive-mode.ts +201 -60
  254. package/src/modes/irc-observation-ledger.ts +71 -15
  255. package/src/modes/prompt-action-autocomplete.ts +32 -13
  256. package/src/modes/prompt-suggestion-controller.ts +96 -0
  257. package/src/modes/runtime-init.ts +20 -2
  258. package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
  259. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  260. package/src/modes/types.ts +6 -0
  261. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  262. package/src/modes/utils/ui-helpers.ts +99 -4
  263. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  264. package/src/prompts/agents/architect.md +17 -3
  265. package/src/prompts/agents/critic.md +17 -2
  266. package/src/prompts/agents/executor.md +1 -1
  267. package/src/prompts/agents/planner.md +8 -1
  268. package/src/prompts/system/btw-user.md +3 -8
  269. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  270. package/src/prompts/system/system-prompt.md +6 -3
  271. package/src/prompts/tools/cron.md +3 -1
  272. package/src/prompts/tools/read.md +4 -2
  273. package/src/prompts/tools/skill-discovery.md +1 -0
  274. package/src/runtime/memory-domain.ts +66 -0
  275. package/src/runtime/memory-guard-contract.ts +73 -0
  276. package/src/runtime/memory-guard.ts +292 -0
  277. package/src/runtime/memory-limit.ts +50 -0
  278. package/src/runtime-mcp/client.ts +41 -55
  279. package/src/runtime-mcp/content-limits.ts +71 -0
  280. package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
  281. package/src/runtime-mcp/json-rpc.ts +28 -11
  282. package/src/runtime-mcp/manager.ts +77 -59
  283. package/src/runtime-mcp/oauth-flow.ts +0 -72
  284. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  285. package/src/runtime-mcp/redaction.ts +30 -0
  286. package/src/runtime-mcp/smithery-auth.ts +17 -2
  287. package/src/runtime-mcp/smithery-connect.ts +11 -1
  288. package/src/runtime-mcp/transports/http.ts +63 -22
  289. package/src/sdk/acp/adapter.ts +8 -1
  290. package/src/sdk/acp/final-text.ts +40 -0
  291. package/src/sdk/acp/mcp.ts +26 -0
  292. package/src/sdk/broker/broker.ts +34 -20
  293. package/src/sdk/broker/identity.ts +22 -12
  294. package/src/sdk/broker/lifecycle.ts +293 -43
  295. package/src/sdk/broker/session-index.ts +11 -1
  296. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  297. package/src/sdk/bus/chat-daemon-control.ts +22 -5
  298. package/src/sdk/bus/config-commands.ts +2 -2
  299. package/src/sdk/bus/config.ts +10 -2
  300. package/src/sdk/bus/control-drain-lease.ts +104 -0
  301. package/src/sdk/bus/conversation-store.ts +40 -6
  302. package/src/sdk/bus/daemon-paths.ts +2 -0
  303. package/src/sdk/bus/index.ts +793 -121
  304. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  305. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  306. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  307. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  308. package/src/sdk/bus/notification-orchestration.ts +12 -0
  309. package/src/sdk/bus/notification-service.ts +87 -25
  310. package/src/sdk/bus/operator-runtime.ts +31 -5
  311. package/src/sdk/bus/prompt-reconciliation.ts +240 -0
  312. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  313. package/src/sdk/bus/recent-activity.ts +90 -26
  314. package/src/sdk/bus/reconciliation-store.ts +224 -0
  315. package/src/sdk/bus/telegram-cli.ts +6 -13
  316. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  317. package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
  318. package/src/sdk/bus/telegram-daemon-control.ts +161 -40
  319. package/src/sdk/bus/telegram-daemon.ts +3649 -593
  320. package/src/sdk/bus/telegram-reference.ts +20 -2
  321. package/src/sdk/bus/topic-registry.ts +451 -12
  322. package/src/sdk/cli/session-cli.ts +15 -2
  323. package/src/sdk/client/discovery.ts +19 -3
  324. package/src/sdk/client/liveness.ts +49 -0
  325. package/src/sdk/host/control/dispatch.ts +8 -2
  326. package/src/sdk/host/control/operations.ts +3 -2
  327. package/src/sdk/host/host.ts +27 -9
  328. package/src/sdk/host/query/handlers.ts +125 -5
  329. package/src/sdk/index.ts +12 -0
  330. package/src/sdk/lifecycle-session.ts +19 -3
  331. package/src/sdk/mcp/server.ts +46 -18
  332. package/src/sdk/prompt-status.ts +97 -0
  333. package/src/sdk/protocol/operation-inventory.generated.json +145 -0
  334. package/src/sdk/protocol/operation-registry.ts +24 -4
  335. package/src/sdk/session-directory.ts +5 -1
  336. package/src/sdk/session.ts +69 -8
  337. package/src/sdk/startup-capability.ts +15 -1
  338. package/src/sdk/transport/auth-preface.ts +82 -0
  339. package/src/sdk/transport/index.ts +18 -0
  340. package/src/sdk/transport/relay.ts +252 -0
  341. package/src/sdk/transport/serve-cli.ts +105 -0
  342. package/src/sdk/transport/socket.ts +197 -0
  343. package/src/sdk/transport/stdio.ts +17 -0
  344. package/src/session/agent-session.ts +1112 -314
  345. package/src/session/blob-store.ts +25 -7
  346. package/src/session/btw-contract.ts +59 -0
  347. package/src/session/fallback-chain-controller.ts +47 -8
  348. package/src/session/internal/managed-session-scope.ts +1426 -259
  349. package/src/session/internal/managed-session-storage.ts +414 -238
  350. package/src/session/internal/native-publish-outcome.ts +271 -0
  351. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  352. package/src/session/session-manager.ts +2079 -469
  353. package/src/session/session-storage.ts +151 -11
  354. package/src/session/streaming-output.ts +262 -43
  355. package/src/setup/credential-import.ts +28 -3
  356. package/src/setup/provider-onboarding.ts +49 -5
  357. package/src/setup/provider-presets.json +2 -1
  358. package/src/skc-runtime/deep-interview-runtime.ts +303 -36
  359. package/src/skc-runtime/deep-interview-stage.ts +1016 -0
  360. package/src/skc-runtime/deep-interview-state.ts +8 -5
  361. package/src/skc-runtime/gc-render.ts +1 -0
  362. package/src/skc-runtime/gc-runtime.ts +11 -1
  363. package/src/skc-runtime/linux-proc.ts +70 -36
  364. package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
  365. package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
  366. package/src/skc-runtime/ralplan-runtime.ts +1057 -75
  367. package/src/skc-runtime/repository-binding.ts +267 -0
  368. package/src/skc-runtime/session-state-sidecar.ts +25 -9
  369. package/src/skc-runtime/state-runtime.ts +50 -12
  370. package/src/skc-runtime/state-writer.ts +68 -56
  371. package/src/skc-runtime/team-launch.ts +34 -5
  372. package/src/skc-runtime/team-runtime.ts +41 -1
  373. package/src/skc-runtime/team-store.ts +32 -0
  374. package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
  375. package/src/skc-runtime/team-workers.ts +1 -0
  376. package/src/skc-runtime/tmux-common.ts +13 -2
  377. package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
  378. package/src/skc-runtime/tmux-sessions.ts +45 -6
  379. package/src/skc-runtime/ultragoal-guard.ts +93 -15
  380. package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
  381. package/src/skc-runtime/ultragoal-runtime.ts +469 -38
  382. package/src/skc-runtime/workflow-manifest.generated.json +195 -5
  383. package/src/skc-runtime/workflow-manifest.ts +67 -3
  384. package/src/skill-state/workflow-hud.ts +25 -2
  385. package/src/skill-state/workflow-mutation-guard.ts +104 -12
  386. package/src/slash-commands/builtin-registry.ts +1 -1
  387. package/src/ssh/utils.ts +14 -0
  388. package/src/task/discovery.ts +8 -2
  389. package/src/task/executor.ts +165 -47
  390. package/src/task/index.ts +123 -15
  391. package/src/task/provider-retry-status.ts +88 -0
  392. package/src/task/receipt.ts +3 -0
  393. package/src/task/render.ts +45 -26
  394. package/src/task/skc-command.ts +2 -2
  395. package/src/task/types.ts +55 -10
  396. package/src/task/ultragoal-redteam-activation.ts +75 -0
  397. package/src/tools/ask.ts +1 -1
  398. package/src/tools/ast-edit.ts +2 -2
  399. package/src/tools/bash-allowed-prefixes.ts +45 -1
  400. package/src/tools/bash-pty-selection.ts +2 -2
  401. package/src/tools/browser/launch.ts +41 -6
  402. package/src/tools/browser/screenshot-format.ts +6 -1
  403. package/src/tools/cron.ts +1 -1
  404. package/src/tools/fetch.ts +127 -154
  405. package/src/tools/image-gen.ts +476 -23
  406. package/src/tools/index.ts +7 -1
  407. package/src/tools/output-meta.ts +176 -9
  408. package/src/tools/path-utils.ts +5 -1
  409. package/src/tools/read-internals.ts +44 -0
  410. package/src/tools/read.ts +1544 -310
  411. package/src/tools/resource-gc.ts +665 -125
  412. package/src/tools/skill-discovery.ts +39 -3
  413. package/src/tools/sqlite-reader.ts +63 -5
  414. package/src/tools/subagent-render.ts +74 -16
  415. package/src/tools/subagent.ts +2 -0
  416. package/src/tools/tool-result.ts +14 -1
  417. package/src/utils/edit-mode.ts +2 -2
  418. package/src/utils/pasted-image-path.ts +15 -6
  419. package/src/utils/prompt-suggestion.ts +265 -0
  420. package/src/utils/shell-snapshot.ts +173 -63
  421. package/src/utils/sixel.ts +3 -3
  422. package/src/web/insane/bridge.ts +5 -1
  423. package/src/web/insane/url-guard.ts +140 -27
  424. package/src/web/scrapers/docs-rs.ts +30 -5
  425. package/src/web/scrapers/types.ts +19 -49
  426. package/src/web/scrapers/utils.ts +12 -26
  427. package/src/web/search/providers/codex.ts +1 -1
  428. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -12,20 +12,107 @@ import {
12
12
  openRecoveryFsRoot,
13
13
  type RecoveryFsRoot,
14
14
  renameNoReplacePath,
15
+ repairOwnerOnlyPathSecurityExpected,
15
16
  snapshotDirectoryTree,
16
17
  verifyOwnerOnlyFdSecurity,
17
18
  verifyOwnerOnlyPathSecurity,
19
+ verifyOwnerOnlyPathSecurityExpected,
18
20
  } from "@sayknow-cli/natives";
21
+ import {
22
+ classifyNativePublishOutcome,
23
+ formatNativePublishDiagnostic,
24
+ mayCleanCurrentStaging,
25
+ type NativePublishOutcome,
26
+ } from "./native-publish-outcome";
19
27
 
20
28
  export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
21
- export const MANAGED_ARTIFACT_MAX_FILES = 10_000;
29
+ export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
22
30
  export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 64 * 1024 * 1024;
23
31
  export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
32
+ export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
24
33
  const LOCK_LEASE_MS = 60_000;
25
34
  const LOCK_HEARTBEAT_MS = 10_000;
26
35
  const LOCK_WAIT_MS = 5_000;
27
36
 
28
37
  const LOCK_STALE_RECHECK_MS = 100;
38
+ export class ManagedPublishError extends Error {
39
+ readonly classification:
40
+ | "destination_conflict"
41
+ | "atomic_unavailable"
42
+ | "invalid_request"
43
+ | "durability_not_provable"
44
+ | "identity_mismatch"
45
+ | "io_error"
46
+ | "durability_failed";
47
+ readonly diagnostic: string;
48
+
49
+ constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
50
+ super(classification);
51
+ this.name = "ManagedPublishError";
52
+ this.classification = classification;
53
+ this.diagnostic = formatNativePublishDiagnostic(outcome);
54
+ }
55
+ }
56
+
57
+ function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
58
+ const classification =
59
+ outcome.reason === "destination_exists"
60
+ ? "destination_conflict"
61
+ : outcome.reason === "atomic_unavailable"
62
+ ? "atomic_unavailable"
63
+ : outcome.reason === "invalid_request"
64
+ ? "invalid_request"
65
+ : outcome.reason === "durability_not_provable"
66
+ ? "durability_not_provable"
67
+ : outcome.reason === "identity_violation"
68
+ ? "identity_mismatch"
69
+ : outcome.reason === "io_failure"
70
+ ? "io_error"
71
+ : "durability_failed";
72
+ return new ManagedPublishError(classification, outcome);
73
+ }
74
+
75
+ /** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
76
+ function sameDirectoryTreeSnapshotAfterRename(
77
+ left: NativeDirectoryTreeSnapshot,
78
+ right: NativeDirectoryTreeSnapshot,
79
+ ): boolean {
80
+ return (
81
+ left.rootDev === right.rootDev &&
82
+ left.rootIno === right.rootIno &&
83
+ left.entries.length === right.entries.length &&
84
+ left.entries.every((entry, index) => {
85
+ const other = right.entries[index];
86
+ return (
87
+ other !== undefined &&
88
+ entry.relativePath === other.relativePath &&
89
+ entry.kind === other.kind &&
90
+ entry.dev === other.dev &&
91
+ entry.ino === other.ino &&
92
+ entry.size === other.size &&
93
+ entry.mtimeNs === other.mtimeNs &&
94
+ (entry.relativePath === "" || entry.ctimeNs === other.ctimeNs) &&
95
+ entry.sha256 === other.sha256
96
+ );
97
+ })
98
+ );
99
+ }
100
+
101
+ class ManagedTreeMoveOutcomeError extends Error {
102
+ constructor(
103
+ message: string,
104
+ readonly stagingCleanupSafe: boolean,
105
+ ) {
106
+ super(message);
107
+ }
108
+ }
109
+
110
+ /** Cleanup is safe only when the native move reports that it did not commit. */
111
+ export function mayCleanManagedTreeStaging(error: unknown): boolean {
112
+ return !(error instanceof ManagedTreeMoveOutcomeError) || error.stagingCleanupSafe;
113
+ }
114
+
115
+ export type ManagedSessionSecurityPolicy = "default" | "windows-existing-verify-first";
29
116
 
30
117
  export type ManagedStorageFailure =
31
118
  | "migration_busy"
@@ -34,7 +121,10 @@ export type ManagedStorageFailure =
34
121
  | "destination_conflict"
35
122
  | "source_changed"
36
123
  | "unsafe_artifacts"
124
+ | "artifact_capacity_exceeded"
37
125
  | "durability_failed"
126
+ | "atomic_unavailable"
127
+ | "durability_not_provable"
38
128
  | "migration_retired"
39
129
  | "managed_storage_unsupported";
40
130
 
@@ -93,6 +183,9 @@ export function validateNativeSecurityResult(
93
183
  ) {
94
184
  throw new Error("Malformed ACL security failure evidence");
95
185
  }
186
+ if (operation === "verify" && result.operation !== "query") {
187
+ throw new Error("Verify failure unexpectedly reports ACL mutation");
188
+ }
96
189
  } else if (result.operation !== undefined || result.attribute !== undefined) {
97
190
  throw new Error("Unexpected ACL fields on owner-only security failure");
98
191
  }
@@ -179,6 +272,40 @@ export interface ManagedDirectoryRoot {
179
272
  readonly ino: bigint;
180
273
  }
181
274
 
275
+ /**
276
+ * Establish the first managed directory beneath a potentially shared ancestor.
277
+ * The ancestor is used only to create the managed chain; every component from
278
+ * `configuredRoot` downward is independently type, symlink, owner, mode, and
279
+ * ACL checked. This deliberately does not impose owner-only policy on `/tmp`.
280
+ */
281
+ export function prepareManagedDirectoryRoot(
282
+ configuredRoot: string,
283
+ policy: ManagedSessionSecurityPolicy = "default",
284
+ ): ManagedDirectoryRoot {
285
+ const target = path.resolve(configuredRoot);
286
+ const missing: string[] = [];
287
+ let existing = target;
288
+ for (;;) {
289
+ try {
290
+ assertSafeDirectory(existing);
291
+ break;
292
+ } catch (error) {
293
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
294
+ const parent = path.dirname(existing);
295
+ if (parent === existing) throw new Error(`Managed root is unavailable: ${configuredRoot}`);
296
+ missing.unshift(path.basename(existing));
297
+ existing = parent;
298
+ }
299
+ }
300
+ let current = fs.realpathSync.native(existing);
301
+ for (const component of missing) {
302
+ current = path.join(current, component);
303
+ const created = ensureDirectoryComponent(current);
304
+ secureManagedDirectory(current, created, policy);
305
+ }
306
+ if (missing.length === 0) secureManagedDirectory(current, false, policy);
307
+ return managedDirectoryRoot(current);
308
+ }
182
309
  export function managedDirectoryRoot(configuredRoot: string): ManagedDirectoryRoot {
183
310
  const canonicalPath = fs.realpathSync.native(configuredRoot);
184
311
  const stat = fs.lstatSync(canonicalPath, { bigint: true });
@@ -216,14 +343,16 @@ export function retainManagedDirectoryAuthority(
216
343
  }
217
344
  }
218
345
 
219
- function ensureDirectoryComponent(pathname: string): void {
346
+ function ensureDirectoryComponent(pathname: string): boolean {
347
+ let created = false;
220
348
  try {
221
349
  fs.mkdirSync(pathname, { mode: 0o700 });
350
+ created = true;
222
351
  } catch (error) {
223
352
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
224
353
  }
225
354
  assertSafeDirectory(pathname);
226
- secure(pathname, "directory");
355
+ return created;
227
356
  }
228
357
 
229
358
  export function assertManagedDirectoryRoot(root: ManagedDirectoryRoot): void {
@@ -263,10 +392,66 @@ function secure(pathname: string, kind: "directory" | "file"): void {
263
392
  if (!verified.ok) throw securityError(pathname, verified);
264
393
  }
265
394
 
395
+ function windowsExistingVerifyFirst(policy: ManagedSessionSecurityPolicy): boolean {
396
+ return process.platform === "win32" && policy === "windows-existing-verify-first";
397
+ }
398
+
399
+ function assertManagedPathIdentity(pathname: string, kind: "directory" | "file", expected: fs.BigIntStats): void {
400
+ const current = fs.lstatSync(pathname, { bigint: true });
401
+ if (current.isSymbolicLink()) throw new Error("reparse_point");
402
+ const expectedKind = kind === "directory" ? current.isDirectory() : current.isFile();
403
+ if (!expectedKind) throw new Error(kind === "directory" ? "not_directory" : "not_file");
404
+ if (current.dev !== expected.dev || current.ino !== expected.ino) throw new Error("identity_mismatch");
405
+ }
406
+
407
+ function verifyExistingManagedPathSecurity(
408
+ pathname: string,
409
+ kind: "directory" | "file",
410
+ expected: fs.BigIntStats,
411
+ ): void {
412
+ const verified = validateNativeSecurityResult(
413
+ verifyOwnerOnlyPathSecurityExpected(pathname, kind, expected.dev, expected.ino),
414
+ "verify",
415
+ kind,
416
+ );
417
+ assertManagedPathIdentity(pathname, kind, expected);
418
+ if (!verified.ok) throw securityError(pathname, verified);
419
+ }
420
+
421
+ function secureExistingManagedDirectory(pathname: string, kind: "directory" | "file"): void {
422
+ const named = fs.lstatSync(pathname, { bigint: true });
423
+ const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
424
+ if (!safeKind || named.isSymbolicLink()) throw new Error(`Unsafe managed ${kind}: ${pathname}`);
425
+ const verified = validateNativeSecurityResult(
426
+ verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
427
+ "verify",
428
+ kind,
429
+ );
430
+ assertManagedPathIdentity(pathname, kind, named);
431
+ if (verified.ok) return;
432
+ if (verified.code !== "acl_verify_failed") throw securityError(pathname, verified);
433
+ const repaired = validateNativeSecurityResult(
434
+ repairOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
435
+ "verify",
436
+ kind,
437
+ );
438
+ if (!repaired.ok) throw securityError(pathname, repaired);
439
+ assertManagedPathIdentity(pathname, kind, named);
440
+ }
441
+
442
+ function secureManagedDirectory(pathname: string, created: boolean, policy: ManagedSessionSecurityPolicy): void {
443
+ if (!created && windowsExistingVerifyFirst(policy)) {
444
+ secureExistingManagedDirectory(pathname, "directory");
445
+ return;
446
+ }
447
+ secure(pathname, "directory");
448
+ }
449
+
266
450
  /** Internal retained-root capability for one managed descendant subtree. */
267
451
  export class ManagedSessionDescendantStore {
268
452
  readonly #root: ManagedDirectoryRoot;
269
453
  readonly #baseDir: string;
454
+ readonly #policy: ManagedSessionSecurityPolicy;
270
455
  readonly #authority: RecoveryFsRoot | undefined;
271
456
  readonly #authorityBaseDir: string;
272
457
  readonly #subtreeRoot: ManagedDirectoryRoot;
@@ -275,32 +460,31 @@ export class ManagedSessionDescendantStore {
275
460
  root: ManagedDirectoryRoot,
276
461
  baseDir: string,
277
462
  retained?: { authority: RecoveryFsRoot; authorityBaseDir: string },
463
+ policy?: ManagedSessionSecurityPolicy,
278
464
  ) {
279
- assertManagedDirectoryRoot(root);
280
465
  managedRelativePath(root, baseDir);
466
+
281
467
  this.#root = root;
282
468
  this.#baseDir = path.resolve(baseDir);
469
+ this.#policy = policy ?? "default";
283
470
  this.#authorityBaseDir = retained?.authorityBaseDir ?? this.#baseDir;
284
471
  if (retained) {
285
472
  const relative = path.relative(retained.authorityBaseDir, this.#baseDir).split(path.sep).join("/");
286
473
  const captured = retained.authority.snapshotManagedTree(relative);
287
474
  if (!captured.ok || !captured.snapshot)
288
475
  throw new Error(captured.code ?? "Managed subtree identity unavailable");
289
- const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
290
- if (
291
- captured.snapshot.rootDev !== subtreeStat.dev.toString() ||
292
- captured.snapshot.rootIno !== subtreeStat.ino.toString()
293
- )
294
- throw new Error("Managed descendant root identity changed");
295
476
  this.#subtreeRoot = Object.freeze({
296
477
  canonicalPath: this.#baseDir,
297
- dev: subtreeStat.dev,
298
- ino: subtreeStat.ino,
478
+ dev: BigInt(captured.snapshot.rootDev),
479
+ ino: BigInt(captured.snapshot.rootIno),
299
480
  });
300
481
  this.#authority = retained.authority;
482
+ this.#assertBound();
483
+
301
484
  return;
302
485
  }
303
- ensureManagedDirectory(this.#baseDir, root);
486
+ assertManagedDirectoryRoot(root);
487
+ ensureManagedDirectory(this.#baseDir, root, this.#policy);
304
488
  const subtreeStat = fs.lstatSync(this.#baseDir, { bigint: true });
305
489
  this.#subtreeRoot = Object.freeze({ canonicalPath: this.#baseDir, dev: subtreeStat.dev, ino: subtreeStat.ino });
306
490
  if (process.platform === "linux") {
@@ -332,19 +516,28 @@ export class ManagedSessionDescendantStore {
332
516
  return this.#subtreeRoot;
333
517
  }
334
518
 
519
+ get securityPolicy(): ManagedSessionSecurityPolicy {
520
+ return this.#policy;
521
+ }
522
+
335
523
  deriveSubtree(relativePath: string): ManagedSessionDescendantStore {
336
524
  const child = this.ensureDirectory(relativePath);
337
525
  const resolved = this.#resolve(relativePath);
338
- if (!this.#authority) return new ManagedSessionDescendantStore(this.#root, resolved);
526
+ if (!this.#authority) return new ManagedSessionDescendantStore(this.#root, resolved, undefined, this.#policy);
339
527
  const retainedChild = this.#authority.retainManagedDirectory(
340
528
  this.#relative(resolved),
341
529
  child.dev.toString(),
342
530
  child.ino.toString(),
343
531
  );
344
- return new ManagedSessionDescendantStore(this.#root, resolved, {
345
- authority: retainedChild,
346
- authorityBaseDir: resolved,
347
- });
532
+ return new ManagedSessionDescendantStore(
533
+ this.#root,
534
+ resolved,
535
+ {
536
+ authority: retainedChild,
537
+ authorityBaseDir: resolved,
538
+ },
539
+ this.#policy,
540
+ );
348
541
  }
349
542
 
350
543
  retainAuthority(): RecoveryFsRoot | undefined {
@@ -371,12 +564,19 @@ export class ManagedSessionDescendantStore {
371
564
  return;
372
565
  }
373
566
  } else {
374
- const verified = validateNativeSecurityResult(
375
- verifyOwnerOnlyPathSecurity(this.#baseDir, "directory"),
376
- "verify",
377
- "directory",
378
- );
379
- if (!verified.ok) throw securityError(this.#baseDir, verified);
567
+ const named = fs.lstatSync(this.#baseDir, { bigint: true });
568
+ if (!named.isDirectory() || named.isSymbolicLink())
569
+ throw new Error(`Unsafe managed directory: ${this.#baseDir}`);
570
+ if (windowsExistingVerifyFirst(this.#policy))
571
+ verifyExistingManagedPathSecurity(this.#baseDir, "directory", named);
572
+ else {
573
+ const verified = validateNativeSecurityResult(
574
+ verifyOwnerOnlyPathSecurity(this.#baseDir, "directory"),
575
+ "verify",
576
+ "directory",
577
+ );
578
+ if (!verified.ok) throw securityError(this.#baseDir, verified);
579
+ }
380
580
  }
381
581
  this.#assertBound();
382
582
  }
@@ -421,7 +621,7 @@ export class ManagedSessionDescendantStore {
421
621
  ino: BigInt(ensured.identity.ino),
422
622
  });
423
623
  }
424
- ensureManagedDirectory(this.#resolve(relativePath), this.#root);
624
+ ensureManagedDirectory(this.#resolve(relativePath), this.#root, this.#policy);
425
625
  const named = fs.lstatSync(this.#resolve(relativePath), { bigint: true });
426
626
  return Object.freeze({ canonicalPath: this.#resolve(relativePath), dev: named.dev, ino: named.ino });
427
627
  }
@@ -434,13 +634,13 @@ export class ManagedSessionDescendantStore {
434
634
  this.#assertBound();
435
635
  return;
436
636
  }
437
- await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root);
637
+ await publishManagedFileNoReplace(resolved, bytes, undefined, this.#root, this.#policy);
438
638
  }
439
639
 
440
640
  publishNoReplaceSync(relativePath: string, bytes: Uint8Array): void {
441
641
  const resolved = this.#resolve(relativePath);
442
642
  if (!this.#authority) {
443
- publishManagedFileNoReplaceSync(resolved, bytes, this.#root);
643
+ publishManagedFileNoReplaceSync(resolved, bytes, this.#root, this.#policy);
444
644
  this.#assertBound();
445
645
  return;
446
646
  }
@@ -453,7 +653,7 @@ export class ManagedSessionDescendantStore {
453
653
  this.#assertBound();
454
654
  const resolved = this.#resolve(relativePath);
455
655
  if (!this.#authority) {
456
- await replaceManagedFile(resolved, bytes, this.#subtreeRoot);
656
+ await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
457
657
  this.#assertBound();
458
658
  return;
459
659
  }
@@ -466,7 +666,7 @@ export class ManagedSessionDescendantStore {
466
666
  this.#assertBound();
467
667
  const resolved = this.#resolve(relativePath);
468
668
  if (!this.#authority) {
469
- replaceManagedFileSync(resolved, bytes, this.#subtreeRoot);
669
+ replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
470
670
  this.#assertBound();
471
671
  return;
472
672
  }
@@ -477,7 +677,7 @@ export class ManagedSessionDescendantStore {
477
677
  appendSync(relativePath: string, bytes: Uint8Array): void {
478
678
  this.#assertBound();
479
679
  const resolved = this.#resolve(relativePath);
480
- const existing = this.readExpected(relativePath);
680
+ let existing = this.readExpected(relativePath);
481
681
  if (!existing) throw new Error("managed_append_missing");
482
682
  if (this.#authority) {
483
683
  const appended = this.#authority.appendManaged(
@@ -494,20 +694,49 @@ export class ManagedSessionDescendantStore {
494
694
  this.#assertBound();
495
695
  return;
496
696
  }
697
+ // On Darwin, the first write-append open can change only ctime (e.g. com.apple.provenance)
698
+ // while leaving dev/ino/size/mtime/content intact. Allow one bounded refresh+retry for that
699
+ // transition only; any other identity change or a second ctime transition stays fail-closed.
700
+ // See https://github.com/jaybeyond/Sayknow_CLI/issues/2944
497
701
  let fd: number | undefined;
702
+ let darwinCtimeRefreshConsumed = false;
498
703
  try {
499
- fd = fs.openSync(resolved, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_NOFOLLOW);
500
- const before = identity(fs.fstatSync(fd, { bigint: true }));
501
- if (!sameIdentity(before, existing.identity)) throw new Error("identity_mismatch");
502
- secureFileDescriptor(resolved, fd, "verify");
503
- let offset = 0;
504
- while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
505
- fs.fsyncSync(fd);
506
- secureFileDescriptor(resolved, fd, "verify");
507
- const after = identity(fs.fstatSync(fd, { bigint: true }));
508
- const named = identity(fs.lstatSync(resolved, { bigint: true }));
509
- if (!sameIdentity(after, named) || after.dev !== before.dev || after.ino !== before.ino)
510
- throw new Error("identity_mismatch");
704
+ for (;;) {
705
+ fd = fs.openSync(resolved, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_NOFOLLOW);
706
+ const before = identity(fs.fstatSync(fd, { bigint: true }));
707
+ if (!sameIdentity(before, existing.identity)) {
708
+ const allowDarwinCtimeRefresh =
709
+ process.platform === "darwin" &&
710
+ !darwinCtimeRefreshConsumed &&
711
+ isCtimeOnlyIdentityMismatch(before, existing.identity);
712
+ if (!allowDarwinCtimeRefresh) throw new Error("identity_mismatch");
713
+ fs.closeSync(fd);
714
+ fd = undefined;
715
+ darwinCtimeRefreshConsumed = true;
716
+ const original = existing;
717
+ const refreshed = this.readExpected(relativePath);
718
+ if (
719
+ !refreshed ||
720
+ !sameStableIdentityIgnoringCtime(refreshed.identity, original.identity) ||
721
+ refreshed.identity.sha256 !== original.identity.sha256 ||
722
+ !refreshed.bytes.equals(original.bytes)
723
+ ) {
724
+ throw new Error("identity_mismatch");
725
+ }
726
+ existing = refreshed;
727
+ continue;
728
+ }
729
+ secureFileDescriptor(resolved, fd, "verify");
730
+ let offset = 0;
731
+ while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
732
+ fs.fsyncSync(fd);
733
+ secureFileDescriptor(resolved, fd, "verify");
734
+ const after = identity(fs.fstatSync(fd, { bigint: true }));
735
+ const named = identity(fs.lstatSync(resolved, { bigint: true }));
736
+ if (!sameIdentity(after, named) || after.dev !== before.dev || after.ino !== before.ino)
737
+ throw new Error("identity_mismatch");
738
+ break;
739
+ }
511
740
  } finally {
512
741
  if (fd !== undefined) fs.closeSync(fd);
513
742
  }
@@ -528,6 +757,7 @@ export class ManagedSessionDescendantStore {
528
757
  if (!created.ok) throw new Error(created.code ?? "managed_publish_failed");
529
758
  if (!created.identity) throw new Error("managed_publish_identity_unavailable");
530
759
  let captured: ReturnType<RecoveryFsRoot["readManaged"]> | undefined;
760
+ let published: unknown;
531
761
  try {
532
762
  captured = this.#authority.readManaged(temporary);
533
763
  if (!captured.ok || !captured.identity || !captured.data)
@@ -553,7 +783,7 @@ export class ManagedSessionDescendantStore {
553
783
  digest,
554
784
  );
555
785
  if (!synced.ok) throw new Error(synced.code ?? "managed_publish_fsync_failed");
556
- const published = this.#authority.renameManagedFileNoReplace(
786
+ published = this.#authority.renameManagedFileNoReplace(
557
787
  temporary,
558
788
  relative,
559
789
  captured.identity.dev,
@@ -563,35 +793,14 @@ export class ManagedSessionDescendantStore {
563
793
  captured.identity.ctimeNs,
564
794
  digest,
565
795
  );
566
- if (!published.ok)
567
- throw new Error(
568
- published.code === "already_exists"
569
- ? "destination_conflict"
570
- : (published.code ?? "managed_publish_failed"),
571
- );
796
+ const outcome = classifyNativePublishOutcome(published, "retained_file");
797
+ if (!outcome.ok) throw publishFailure(outcome);
572
798
  } catch (error) {
573
- if (captured?.ok && captured.identity && captured.data) {
574
- const digest = captured.identity.sha256 ?? createHash("sha256").update(captured.data).digest("hex");
575
- const installed = this.#authority.readManaged(relative);
576
- if (
577
- installed.ok &&
578
- installed.identity &&
579
- installed.data &&
580
- installed.identity.dev === captured.identity.dev &&
581
- installed.identity.ino === captured.identity.ino &&
582
- (installed.identity.sha256 ?? createHash("sha256").update(installed.data).digest("hex")) === digest
583
- ) {
584
- const removed = this.#authority.removeManaged(
585
- relative,
586
- installed.identity.dev,
587
- installed.identity.ino,
588
- installed.identity.size,
589
- installed.identity.mtimeNs,
590
- installed.identity.ctimeNs,
591
- digest,
592
- );
593
- if (!removed.ok) throw new Error(removed.code ?? "managed_publish_reconcile_failed");
594
- }
799
+ // A committed or unknown native outcome is evidence, not authorization to
800
+ // probe or remove the destination. Only a validated pre-mutation result
801
+ // may clean this operation's still-named staging object.
802
+ const outcome = captured ? classifyNativePublishOutcome(published, "retained_file") : undefined;
803
+ if (captured?.ok && captured.identity && captured.data && outcome && mayCleanCurrentStaging(outcome)) {
595
804
  const staged = this.#authority.readManaged(temporary);
596
805
  if (
597
806
  staged.ok &&
@@ -609,7 +818,8 @@ export class ManagedSessionDescendantStore {
609
818
  staged.identity.ctimeNs,
610
819
  staged.identity.sha256 ?? createHash("sha256").update(staged.data).digest("hex"),
611
820
  );
612
- if (!removed.ok) throw new Error(removed.code ?? "managed_publish_reconcile_failed");
821
+ if (!removed.ok && removed.code !== "cleanup_pending")
822
+ throw new Error(removed.code ?? "managed_publish_reconcile_failed");
613
823
  }
614
824
  }
615
825
  throw error;
@@ -682,7 +892,17 @@ export class ManagedSessionDescendantStore {
682
892
  sha256: expected.identity.sha256,
683
893
  quarantineName: `.skc-remove-${process.pid}-${randomUUID()}`,
684
894
  });
685
- if (!removed.ok) throw new Error(removed.code ?? "managed_remove_failed");
895
+ if (
896
+ !removed.ok &&
897
+ !(
898
+ removed.code === "cleanup_pending" &&
899
+ (removed.detachedPath ??
900
+ removed.retainedSuccessorPath ??
901
+ removed.retainedPlaceholderPath ??
902
+ removed.retainedUnknownPath) !== undefined
903
+ )
904
+ )
905
+ throw new Error(removed.code ?? "managed_remove_failed");
686
906
  this.#assertBound();
687
907
  return;
688
908
  }
@@ -695,7 +915,8 @@ export class ManagedSessionDescendantStore {
695
915
  expected.identity.ctimeNs.toString(),
696
916
  expected.identity.sha256,
697
917
  );
698
- if (!removed.ok) throw new Error(removed.code ?? "managed_remove_failed");
918
+ if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
919
+ throw new Error(removed.code ?? "managed_remove_failed");
699
920
  this.#assertBound();
700
921
  }
701
922
  /** Read and remove one managed descendant through retained authority. */
@@ -719,7 +940,8 @@ export class ManagedSessionDescendantStore {
719
940
  existing.identity.ctimeNs,
720
941
  existing.identity.sha256 ?? createHash("sha256").update(existing.data).digest("hex"),
721
942
  );
722
- if (!removed.ok) throw new Error(removed.code ?? "managed_consume_failed");
943
+ if (!removed.ok && !(removed.code === "cleanup_pending" && removed.recoveryPath !== undefined))
944
+ throw new Error(removed.code ?? "managed_consume_failed");
723
945
  this.#assertBound();
724
946
  return existing.data;
725
947
  }
@@ -805,92 +1027,31 @@ export class ManagedSessionDescendantStore {
805
1027
  sourceRelativePath: string,
806
1028
  destinationRelativePath: string,
807
1029
  expected: NativeDirectoryTreeSnapshot,
808
- ): void {
1030
+ ): NativeDirectoryTreeSnapshot {
809
1031
  this.#assertBound();
810
- if (!this.#authority) {
811
- const source = this.#resolve(sourceRelativePath);
812
- const destination = this.#resolve(destinationRelativePath);
813
- if (fs.existsSync(destination)) throw new Error("already_exists");
814
- let destinationSnapshot: NativeDirectoryTreeSnapshot | undefined;
815
- try {
816
- this.ensureDirectory(destinationRelativePath);
817
- destinationSnapshot = this.captureTree(destinationRelativePath);
818
- for (const entry of expected.entries) {
819
- if (entry.relativePath === "") continue;
820
- const target = path.posix.join(destinationRelativePath.replaceAll(path.sep, "/"), entry.relativePath);
821
- if (entry.kind === "directory") {
822
- this.ensureDirectory(target);
823
- destinationSnapshot = this.captureTree(destinationRelativePath);
824
- continue;
825
- }
826
- const sourceEntry = this.readExpected(
827
- path.posix.join(sourceRelativePath.replaceAll(path.sep, "/"), entry.relativePath),
828
- );
829
- if (
830
- !sourceEntry ||
831
- sourceEntry.identity.dev.toString() !== entry.dev ||
832
- sourceEntry.identity.ino.toString() !== entry.ino ||
833
- sourceEntry.identity.size.toString() !== entry.size ||
834
- sourceEntry.identity.mtimeNs.toString() !== entry.mtimeNs ||
835
- sourceEntry.identity.sha256 !== entry.sha256
836
- )
837
- throw new Error("artifact_source_changed");
838
- this.publishNoReplaceSync(target, sourceEntry.bytes);
839
- destinationSnapshot = this.captureTree(destinationRelativePath);
840
- }
841
- destinationSnapshot = this.captureTree(destinationRelativePath);
842
- const comparable = (tree: NativeDirectoryTreeSnapshot) =>
843
- tree.entries.map(entry => ({
844
- relativePath: entry.relativePath,
845
- kind: entry.kind,
846
- size: entry.size,
847
- sha256: entry.sha256,
848
- }));
849
- if (JSON.stringify(comparable(destinationSnapshot)) !== JSON.stringify(comparable(expected)))
850
- throw new Error("artifact_destination_mismatch");
851
- const terminalSource = snapshotDirectoryTree(source);
852
- if (
853
- !terminalSource.ok ||
854
- !terminalSource.snapshot ||
855
- JSON.stringify(terminalSource.snapshot) !== JSON.stringify(expected)
1032
+ const moved = this.#authority
1033
+ ? this.#authority.renameManagedTreeNoReplace(
1034
+ this.#relative(this.#resolve(sourceRelativePath)),
1035
+ this.#relative(this.#resolve(destinationRelativePath)),
1036
+ expected,
856
1037
  )
857
- throw new Error("artifact_source_changed");
858
- const retired = exactRemoveDirectoryTree(source, expected);
859
- if (!retired.ok) throw new Error(retired.code ?? "managed_remove_failed");
860
- } catch (error) {
861
- if (destinationSnapshot) {
862
- const removed = exactRemoveDirectoryTree(destination, destinationSnapshot);
863
- if (!removed.ok) throw new Error(removed.code ?? "managed_publish_reconcile_failed");
864
- }
865
- throw error;
866
- }
867
- this.#assertBound();
868
- return;
869
- }
870
- const moved = this.#authority.renameManagedTreeNoReplace(
871
- this.#relative(this.#resolve(sourceRelativePath)),
872
- this.#relative(this.#resolve(destinationRelativePath)),
873
- expected,
874
- );
875
- if (!moved.ok) {
876
- try {
877
- const installed = this.captureTree(destinationRelativePath);
878
- const comparable = (tree: NativeDirectoryTreeSnapshot) =>
879
- tree.entries.map(entry => ({
880
- relativePath: entry.relativePath,
881
- kind: entry.kind,
882
- size: entry.size,
883
- sha256: entry.sha256,
884
- }));
885
- if (JSON.stringify(comparable(installed)) !== JSON.stringify(comparable(expected)))
886
- throw new Error("managed_publish_reconcile_failed");
887
- this.removeTreeExpected(destinationRelativePath, installed);
888
- } catch (error) {
889
- if (!(error instanceof Error) || error.message !== "not_found") throw error;
890
- }
891
- throw new Error(moved.code ?? "managed_move_failed");
892
- }
1038
+ : renameNoReplacePath(this.#resolve(sourceRelativePath), this.#resolve(destinationRelativePath));
1039
+ const outcome = classifyNativePublishOutcome(moved, this.#authority ? "retained_tree" : "direct_rename");
1040
+ if (!outcome.ok)
1041
+ throw new ManagedTreeMoveOutcomeError(publishFailure(outcome).message, mayCleanCurrentStaging(outcome));
1042
+ const movedSnapshot = this.#authority
1043
+ ? this.#authority.snapshotManagedTree(this.#relative(this.#resolve(destinationRelativePath)))
1044
+ : snapshotDirectoryTree(this.#resolve(destinationRelativePath));
1045
+ if (
1046
+ !movedSnapshot.ok ||
1047
+ !movedSnapshot.snapshot ||
1048
+ !sameDirectoryTreeSnapshotAfterRename(movedSnapshot.snapshot, expected)
1049
+ )
1050
+ throw new ManagedTreeMoveOutcomeError("artifact_destination_mismatch", false);
1051
+ if (!this.#authority) fsyncDirectory(this.#baseDir);
1052
+
893
1053
  this.#assertBound();
1054
+ return movedSnapshot.snapshot;
894
1055
  }
895
1056
 
896
1057
  removeTreeExpected(relativePath: string, expected: NativeDirectoryTreeSnapshot): void {
@@ -1018,6 +1179,24 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
1018
1179
  );
1019
1180
  }
1020
1181
 
1182
+ /** Content/path identity that remains meaningful across a Darwin write-open ctime-only transition. */
1183
+ function sameStableIdentityIgnoringCtime(
1184
+ left: ManagedFileSnapshot["identity"],
1185
+ right: ManagedFileSnapshot["identity"],
1186
+ ): boolean {
1187
+ return (
1188
+ left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs
1189
+ );
1190
+ }
1191
+
1192
+ /** True when observed identity matches expected on every field except ctimeNs. */
1193
+ function isCtimeOnlyIdentityMismatch(
1194
+ observed: ManagedFileSnapshot["identity"],
1195
+ expected: ManagedFileSnapshot["identity"],
1196
+ ): boolean {
1197
+ return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
1198
+ }
1199
+
1021
1200
  function parseLock(pathname: string): LockRecord | undefined {
1022
1201
  try {
1023
1202
  const stat = fs.lstatSync(pathname);
@@ -1060,25 +1239,43 @@ function sameFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean
1060
1239
  }
1061
1240
 
1062
1241
  /** Create a managed directory and fail closed unless its owner-only mode/ACL verifies. */
1063
- export function ensureManagedDirectory(pathname: string, root?: ManagedDirectoryRoot): void {
1242
+ export function ensureManagedDirectory(
1243
+ pathname: string,
1244
+ root?: ManagedDirectoryRoot,
1245
+ policy: ManagedSessionSecurityPolicy = "default",
1246
+ ): void {
1064
1247
  if (!root) {
1248
+ let existed = false;
1249
+ try {
1250
+ const named = fs.lstatSync(pathname);
1251
+ existed = true;
1252
+ if (!named.isDirectory() || named.isSymbolicLink()) throw new Error(`Unsafe managed directory: ${pathname}`);
1253
+ } catch (error) {
1254
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
1255
+ }
1065
1256
  fs.mkdirSync(pathname, { recursive: true, mode: 0o700 });
1066
1257
  assertSafeDirectory(pathname);
1067
- secure(pathname, "directory");
1258
+ if (existed && windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(pathname, "directory");
1259
+ else secure(pathname, "directory");
1068
1260
  return;
1069
1261
  }
1070
1262
  ensureManagedRoot(root);
1263
+ const components = managedRelativePath(root, pathname);
1264
+ if (components.length === 0) {
1265
+ if (windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(root.canonicalPath, "directory");
1266
+ return;
1267
+ }
1071
1268
  let current = root.canonicalPath;
1072
- for (const component of managedRelativePath(root, pathname)) {
1269
+ if (windowsExistingVerifyFirst(policy)) secureExistingManagedDirectory(current, "directory");
1270
+ else secure(current, "directory");
1271
+ for (const component of components) {
1073
1272
  // Re-inspect the captured root or already-secured descendant before every
1074
1273
  // descent; a replaced component cannot grant authority to the next one.
1075
1274
  if (current === root.canonicalPath) assertManagedDirectoryRoot(root);
1076
- else {
1077
- assertSafeDirectory(current);
1078
- secure(current, "directory");
1079
- }
1275
+ else assertSafeDirectory(current);
1080
1276
  current = path.join(current, component);
1081
- ensureDirectoryComponent(current);
1277
+ const created = ensureDirectoryComponent(current);
1278
+ secureManagedDirectory(current, created, policy);
1082
1279
  }
1083
1280
  }
1084
1281
 
@@ -1123,15 +1320,17 @@ export async function publishManagedFileNoReplace(
1123
1320
  bytes: Uint8Array,
1124
1321
  assertOwned?: () => void,
1125
1322
  root?: ManagedDirectoryRoot,
1323
+ policy: ManagedSessionSecurityPolicy = "default",
1126
1324
  ): Promise<void> {
1127
1325
  const parent = path.dirname(destination);
1128
- ensureManagedDirectory(parent, root);
1326
+ ensureManagedDirectory(parent, root, policy);
1129
1327
  const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
1130
1328
  let fd: number | undefined;
1131
1329
  let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
1132
- let stagingSnapshot: ManagedFileSnapshot | undefined;
1133
- let committed = false;
1134
1330
  let failure: unknown;
1331
+ let outcome: NativePublishOutcome | undefined;
1332
+ let renameAttempted = false;
1333
+
1135
1334
  try {
1136
1335
  assertOwned?.();
1137
1336
  fd = fs.openSync(
@@ -1146,17 +1345,13 @@ export async function publishManagedFileNoReplace(
1146
1345
  secureFileDescriptor(staging, fd, "verify");
1147
1346
  const staged = fs.fstatSync(fd, { bigint: true });
1148
1347
  stagingIdentity = { dev: staged.dev, ino: staged.ino };
1149
- stagingSnapshot = {
1150
- bytes: Buffer.from(bytes),
1151
- identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
1152
- };
1153
1348
  assertOwned?.();
1154
1349
 
1155
- const published = renameNoReplacePath(staging, destination);
1156
- if (!published.ok) {
1157
- if (published.code === "quarantine_collision") throw new Error("destination_conflict");
1158
- throw new Error("durability_failed");
1159
- }
1350
+ renameAttempted = true;
1351
+ outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
1352
+
1353
+ if (!outcome.ok) throw publishFailure(outcome);
1354
+
1160
1355
  const named = fs.lstatSync(destination, { bigint: true });
1161
1356
  if (
1162
1357
  !named.isFile() ||
@@ -1171,25 +1366,12 @@ export async function publishManagedFileNoReplace(
1171
1366
  fd = undefined;
1172
1367
 
1173
1368
  fsyncDirectory(parent);
1174
- committed = true;
1175
1369
  } catch (error) {
1176
1370
  failure = error;
1177
1371
  } finally {
1178
1372
  if (fd !== undefined) fs.closeSync(fd);
1179
- if (!committed && stagingSnapshot) {
1180
- try {
1181
- const installed = captureManagedFileNoFollow(destination);
1182
- if (
1183
- installed.identity.dev === stagingSnapshot.identity.dev &&
1184
- installed.identity.ino === stagingSnapshot.identity.ino &&
1185
- installed.identity.sha256 === stagingSnapshot.identity.sha256
1186
- )
1187
- await unlinkManagedFileVerified(destination, installed.identity);
1188
- } catch (error) {
1189
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
1190
- }
1191
- }
1192
- if (stagingIdentity) {
1373
+
1374
+ if (stagingIdentity && (!renameAttempted || (outcome && mayCleanCurrentStaging(outcome)))) {
1193
1375
  await fsp
1194
1376
  .lstat(staging, { bigint: true })
1195
1377
  .then(stat => {
@@ -1208,15 +1390,16 @@ export function publishManagedFileNoReplaceSync(
1208
1390
  destination: string,
1209
1391
  bytes: Uint8Array,
1210
1392
  root?: ManagedDirectoryRoot,
1393
+ policy: ManagedSessionSecurityPolicy = "default",
1211
1394
  ): void {
1212
1395
  const parent = path.dirname(destination);
1213
- ensureManagedDirectory(parent, root);
1396
+ ensureManagedDirectory(parent, root, policy);
1214
1397
  const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
1215
1398
  let fd: number | undefined;
1216
1399
  let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
1217
- let stagingSnapshot: ManagedFileSnapshot | undefined;
1218
- let committed = false;
1219
1400
  let failure: unknown;
1401
+ let outcome: NativePublishOutcome | undefined;
1402
+
1220
1403
  try {
1221
1404
  fd = fs.openSync(
1222
1405
  staging,
@@ -1230,16 +1413,10 @@ export function publishManagedFileNoReplaceSync(
1230
1413
  secureFileDescriptor(staging, fd, "verify");
1231
1414
  const staged = fs.fstatSync(fd, { bigint: true });
1232
1415
  stagingIdentity = { dev: staged.dev, ino: staged.ino };
1233
- stagingSnapshot = {
1234
- bytes: Buffer.from(bytes),
1235
- identity: identity(staged, createHash("sha256").update(bytes).digest("hex")),
1236
- };
1237
1416
 
1238
- const published = renameNoReplacePath(staging, destination);
1239
- if (!published.ok) {
1240
- if (published.code === "quarantine_collision") throw new Error("destination_conflict");
1241
- throw new Error("durability_failed");
1242
- }
1417
+ outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
1418
+ if (!outcome.ok) throw publishFailure(outcome);
1419
+
1243
1420
  const named = fs.lstatSync(destination, { bigint: true });
1244
1421
  if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
1245
1422
  throw new Error("destination_identity_changed");
@@ -1248,35 +1425,12 @@ export function publishManagedFileNoReplaceSync(
1248
1425
  fs.closeSync(fd);
1249
1426
  fd = undefined;
1250
1427
  fsyncDirectory(parent);
1251
- committed = true;
1252
1428
  } catch (error) {
1253
1429
  failure = error;
1254
1430
  } finally {
1255
1431
  if (fd !== undefined) fs.closeSync(fd);
1256
1432
  }
1257
- if (stagingIdentity) {
1258
- if (!committed && stagingSnapshot) {
1259
- try {
1260
- const installed = captureManagedFileNoFollow(destination);
1261
- if (
1262
- installed.identity.dev === stagingSnapshot.identity.dev &&
1263
- installed.identity.ino === stagingSnapshot.identity.ino &&
1264
- installed.identity.sha256 === stagingSnapshot.identity.sha256
1265
- ) {
1266
- const removed = exactUnlink(destination, {
1267
- dev: installed.identity.dev,
1268
- ino: installed.identity.ino,
1269
- size: BigInt(installed.identity.size),
1270
- mtimeNs: installed.identity.mtimeNs,
1271
- sha256: installed.identity.sha256,
1272
- quarantineName: `.skc-remove-${process.pid}-${randomUUID()}`,
1273
- });
1274
- if (!removed.ok) failure ??= new Error(removed.code ?? "managed_publish_reconcile_failed");
1275
- }
1276
- } catch (error) {
1277
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") failure ??= error;
1278
- }
1279
- }
1433
+ if (stagingIdentity && outcome && mayCleanCurrentStaging(outcome)) {
1280
1434
  try {
1281
1435
  const named = fs.lstatSync(staging, { bigint: true });
1282
1436
  if (named.dev !== stagingIdentity.dev || named.ino !== stagingIdentity.ino) {
@@ -1291,9 +1445,14 @@ export function publishManagedFileNoReplaceSync(
1291
1445
  }
1292
1446
 
1293
1447
  /** Replace one managed regular file while retaining the secured staging fd through publication. */
1294
- export function replaceManagedFileSync(destination: string, bytes: Uint8Array, root: ManagedDirectoryRoot): void {
1448
+ export function replaceManagedFileSync(
1449
+ destination: string,
1450
+ bytes: Uint8Array,
1451
+ root: ManagedDirectoryRoot,
1452
+ policy: ManagedSessionSecurityPolicy = "default",
1453
+ ): void {
1295
1454
  const parent = path.dirname(destination);
1296
- ensureManagedDirectory(parent, root);
1455
+ ensureManagedDirectory(parent, root, policy);
1297
1456
  const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
1298
1457
  let fd: number | undefined;
1299
1458
  let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
@@ -1338,8 +1497,9 @@ export async function replaceManagedFile(
1338
1497
  destination: string,
1339
1498
  bytes: Uint8Array,
1340
1499
  root: ManagedDirectoryRoot,
1500
+ policy: ManagedSessionSecurityPolicy = "default",
1341
1501
  ): Promise<void> {
1342
- replaceManagedFileSync(destination, bytes, root);
1502
+ replaceManagedFileSync(destination, bytes, root, policy);
1343
1503
  }
1344
1504
 
1345
1505
  /** Copy the exact bytes captured from one no-follow source descriptor. */
@@ -1348,11 +1508,12 @@ export async function copyManagedFileNoReplace(
1348
1508
  destination: string,
1349
1509
  snapshot = captureManagedFileNoFollow(source),
1350
1510
  root?: ManagedDirectoryRoot,
1511
+ policy: ManagedSessionSecurityPolicy = "default",
1351
1512
  ): Promise<void> {
1352
1513
  const named = captureManagedFileNoFollow(source);
1353
1514
  if (!sameIdentity(snapshot.identity, named.identity) || !snapshot.bytes.equals(named.bytes))
1354
1515
  throw new Error("source_changed");
1355
- await publishManagedFileNoReplace(destination, snapshot.bytes, undefined, root);
1516
+ await publishManagedFileNoReplace(destination, snapshot.bytes, undefined, root, policy);
1356
1517
  const destinationSnapshot = captureManagedFileNoFollow(destination);
1357
1518
  if (!destinationSnapshot.bytes.equals(snapshot.bytes)) throw new Error("durability_failed");
1358
1519
  }
@@ -1362,8 +1523,9 @@ export async function acquireManagedLock(
1362
1523
  locksDirectory: string,
1363
1524
  name: string,
1364
1525
  root?: ManagedDirectoryRoot,
1526
+ policy: ManagedSessionSecurityPolicy = "default",
1365
1527
  ): Promise<ManagedStorageLock> {
1366
- ensureManagedDirectory(locksDirectory, root);
1528
+ ensureManagedDirectory(locksDirectory, root, policy);
1367
1529
  const lockPath = path.join(locksDirectory, `${name}.lock`);
1368
1530
  const deadline = Date.now() + LOCK_WAIT_MS;
1369
1531
  let staleObservedAt: number | undefined;
@@ -1478,8 +1640,20 @@ export async function acquireManagedLock(
1478
1640
  }
1479
1641
  }
1480
1642
 
1643
+ export interface ManagedArtifactTreeLimits {
1644
+ maxFiles?: number;
1645
+ maxTotalBytes?: number;
1646
+ }
1647
+
1481
1648
  /** Bounds a no-follow artifact tree before it can be copied or deleted. */
1482
- export function validateManagedArtifactTree(root: string): void {
1649
+ export function validateManagedArtifactTree(root: string, limits: ManagedArtifactTreeLimits = {}): void {
1650
+ const clampLimit = (limit: number | undefined, maximum: number): number => {
1651
+ if (limit === undefined) return maximum;
1652
+ if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit <= 0) throw new Error("unsafe_artifacts");
1653
+ return Math.min(limit, maximum);
1654
+ };
1655
+ const maxFiles = clampLimit(limits.maxFiles, MANAGED_ARTIFACT_MAX_FILES);
1656
+ const maxTotalBytes = clampLimit(limits.maxTotalBytes, MANAGED_ARTIFACT_MAX_TOTAL_BYTES);
1483
1657
  let files = 0;
1484
1658
  let bytes = 0;
1485
1659
  const visit = (directory: string, depth: number): void => {
@@ -1496,8 +1670,7 @@ export function validateManagedArtifactTree(root: string): void {
1496
1670
  if (!stat.isFile() || stat.size > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("unsafe_artifacts");
1497
1671
  files++;
1498
1672
  bytes += stat.size;
1499
- if (files > MANAGED_ARTIFACT_MAX_FILES || bytes > MANAGED_ARTIFACT_MAX_TOTAL_BYTES)
1500
- throw new Error("unsafe_artifacts");
1673
+ if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
1501
1674
  }
1502
1675
  };
1503
1676
  const rootStat = fs.lstatSync(root);
@@ -1513,7 +1686,10 @@ export function fsyncManagedArtifactTree(root: string): NativeDirectoryTreeSnaps
1513
1686
  const stat = fs.lstatSync(pathname);
1514
1687
  if (stat.isSymbolicLink()) throw new Error("unsafe_artifacts");
1515
1688
  if (stat.isFile()) {
1516
- const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
1689
+ const fd = fs.openSync(
1690
+ pathname,
1691
+ (process.platform === "win32" ? fs.constants.O_WRONLY : fs.constants.O_RDONLY) | fs.constants.O_NOFOLLOW,
1692
+ );
1517
1693
  try {
1518
1694
  fs.fsyncSync(fd);
1519
1695
  } finally {