@cluesmith/codev 3.3.0 → 3.3.2

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 (202) hide show
  1. package/dashboard-dist/assets/{index-JCbkpAcV.js → index-D3XaKpZW.js} +45 -43
  2. package/dashboard-dist/assets/index-D3XaKpZW.js.map +1 -0
  3. package/dashboard-dist/index.html +1 -1
  4. package/dist/agent-farm/cli.d.ts.map +1 -1
  5. package/dist/agent-farm/cli.js +99 -36
  6. package/dist/agent-farm/cli.js.map +1 -1
  7. package/dist/agent-farm/commands/reset/constants.d.ts +119 -1
  8. package/dist/agent-farm/commands/reset/constants.d.ts.map +1 -1
  9. package/dist/agent-farm/commands/reset/constants.js +119 -1
  10. package/dist/agent-farm/commands/reset/constants.js.map +1 -1
  11. package/dist/agent-farm/commands/reset/context.d.ts +20 -2
  12. package/dist/agent-farm/commands/reset/context.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/reset/context.js +44 -3
  14. package/dist/agent-farm/commands/reset/context.js.map +1 -1
  15. package/dist/agent-farm/commands/reset/index.d.ts +7 -7
  16. package/dist/agent-farm/commands/reset/index.js +8 -8
  17. package/dist/agent-farm/commands/reset/index.js.map +1 -1
  18. package/dist/agent-farm/commands/reset/receipt.d.ts +1 -1
  19. package/dist/agent-farm/commands/reset/receipt.d.ts.map +1 -1
  20. package/dist/agent-farm/commands/reset/receipt.js +19 -8
  21. package/dist/agent-farm/commands/reset/receipt.js.map +1 -1
  22. package/dist/agent-farm/commands/reset/reorient.d.ts +1 -1
  23. package/dist/agent-farm/commands/reset/reorient.d.ts.map +1 -1
  24. package/dist/agent-farm/commands/reset/reorient.js +12 -5
  25. package/dist/agent-farm/commands/reset/reorient.js.map +1 -1
  26. package/dist/agent-farm/commands/reset/self.d.ts +335 -0
  27. package/dist/agent-farm/commands/reset/self.d.ts.map +1 -0
  28. package/dist/agent-farm/commands/reset/self.js +636 -0
  29. package/dist/agent-farm/commands/reset/self.js.map +1 -0
  30. package/dist/agent-farm/commands/reset.d.ts +18 -3
  31. package/dist/agent-farm/commands/reset.d.ts.map +1 -1
  32. package/dist/agent-farm/commands/reset.js +55 -33
  33. package/dist/agent-farm/commands/reset.js.map +1 -1
  34. package/dist/agent-farm/commands/self-refresh.d.ts +60 -0
  35. package/dist/agent-farm/commands/self-refresh.d.ts.map +1 -0
  36. package/dist/agent-farm/commands/self-refresh.js +506 -0
  37. package/dist/agent-farm/commands/self-refresh.js.map +1 -0
  38. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  39. package/dist/agent-farm/commands/send.js +27 -2
  40. package/dist/agent-farm/commands/send.js.map +1 -1
  41. package/dist/agent-farm/commands/spawn-worktree.js +1 -1
  42. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  43. package/dist/agent-farm/commands/spawn.d.ts +7 -0
  44. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  45. package/dist/agent-farm/commands/spawn.js +13 -1
  46. package/dist/agent-farm/commands/spawn.js.map +1 -1
  47. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  48. package/dist/agent-farm/commands/tower.js +13 -0
  49. package/dist/agent-farm/commands/tower.js.map +1 -1
  50. package/dist/agent-farm/lib/reconnect-backoff.d.ts +10 -0
  51. package/dist/agent-farm/lib/reconnect-backoff.d.ts.map +1 -1
  52. package/dist/agent-farm/lib/reconnect-backoff.js +10 -0
  53. package/dist/agent-farm/lib/reconnect-backoff.js.map +1 -1
  54. package/dist/agent-farm/lib/tower-client.d.ts +12 -0
  55. package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
  56. package/dist/agent-farm/lib/tower-client.js +12 -0
  57. package/dist/agent-farm/lib/tower-client.js.map +1 -1
  58. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  59. package/dist/agent-farm/lib/tunnel-client.js +7 -3
  60. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  61. package/dist/agent-farm/servers/canvas-relay.d.ts +60 -0
  62. package/dist/agent-farm/servers/canvas-relay.d.ts.map +1 -0
  63. package/dist/agent-farm/servers/canvas-relay.js +376 -0
  64. package/dist/agent-farm/servers/canvas-relay.js.map +1 -0
  65. package/dist/agent-farm/servers/gate-profiles.d.ts +1 -1
  66. package/dist/agent-farm/servers/gate-profiles.js +1 -1
  67. package/dist/agent-farm/servers/mailbox-delivery.d.ts +141 -13
  68. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  69. package/dist/agent-farm/servers/mailbox-delivery.js +172 -16
  70. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  71. package/dist/agent-farm/servers/mailbox-wiring.d.ts +24 -2
  72. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  73. package/dist/agent-farm/servers/mailbox-wiring.js +92 -32
  74. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
  75. package/dist/agent-farm/servers/message-write.d.ts +58 -18
  76. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  77. package/dist/agent-farm/servers/message-write.js +73 -28
  78. package/dist/agent-farm/servers/message-write.js.map +1 -1
  79. package/dist/agent-farm/servers/overview.d.ts +19 -0
  80. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  81. package/dist/agent-farm/servers/overview.js +78 -1
  82. package/dist/agent-farm/servers/overview.js.map +1 -1
  83. package/dist/agent-farm/servers/render-gate.d.ts +13 -0
  84. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  85. package/dist/agent-farm/servers/render-gate.js +21 -0
  86. package/dist/agent-farm/servers/render-gate.js.map +1 -1
  87. package/dist/agent-farm/servers/session-submit.d.ts +221 -28
  88. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
  89. package/dist/agent-farm/servers/session-submit.js +334 -43
  90. package/dist/agent-farm/servers/session-submit.js.map +1 -1
  91. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  92. package/dist/agent-farm/servers/tower-routes.js +269 -64
  93. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  94. package/dist/agent-farm/servers/tower-server.js +25 -3
  95. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  96. package/dist/agent-farm/servers/tower-terminals.d.ts +6 -0
  97. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  98. package/dist/agent-farm/servers/tower-terminals.js +30 -5
  99. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  100. package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
  101. package/dist/agent-farm/servers/tower-tunnel.js +14 -0
  102. package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
  103. package/dist/agent-farm/servers/tower-websocket.d.ts +6 -1
  104. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  105. package/dist/agent-farm/servers/tower-websocket.js +90 -37
  106. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  107. package/dist/agent-farm/types.d.ts +37 -2
  108. package/dist/agent-farm/types.d.ts.map +1 -1
  109. package/dist/agent-farm/utils/harness.d.ts +3 -3
  110. package/dist/agent-farm/utils/harness.js +1 -1
  111. package/dist/agent-farm/utils/message-format.d.ts +67 -3
  112. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  113. package/dist/agent-farm/utils/message-format.js +127 -14
  114. package/dist/agent-farm/utils/message-format.js.map +1 -1
  115. package/dist/agent-farm/utils/server-utils.d.ts +86 -4
  116. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  117. package/dist/agent-farm/utils/server-utils.js +303 -5
  118. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  119. package/dist/commands/porch/context-refresh.d.ts +181 -0
  120. package/dist/commands/porch/context-refresh.d.ts.map +1 -0
  121. package/dist/commands/porch/context-refresh.js +270 -0
  122. package/dist/commands/porch/context-refresh.js.map +1 -0
  123. package/dist/commands/porch/index.d.ts +6 -0
  124. package/dist/commands/porch/index.d.ts.map +1 -1
  125. package/dist/commands/porch/index.js +46 -0
  126. package/dist/commands/porch/index.js.map +1 -1
  127. package/dist/commands/porch/next.d.ts +0 -9
  128. package/dist/commands/porch/next.d.ts.map +1 -1
  129. package/dist/commands/porch/next.js +133 -4
  130. package/dist/commands/porch/next.js.map +1 -1
  131. package/dist/commands/porch/protocol.d.ts.map +1 -1
  132. package/dist/commands/porch/protocol.js +102 -0
  133. package/dist/commands/porch/protocol.js.map +1 -1
  134. package/dist/commands/porch/types.d.ts +61 -0
  135. package/dist/commands/porch/types.d.ts.map +1 -1
  136. package/dist/lib/forge-contracts.d.ts +9 -0
  137. package/dist/lib/forge-contracts.d.ts.map +1 -1
  138. package/dist/lib/forge-contracts.js.map +1 -1
  139. package/dist/lib/github.d.ts +15 -1
  140. package/dist/lib/github.d.ts.map +1 -1
  141. package/dist/lib/github.js +17 -0
  142. package/dist/lib/github.js.map +1 -1
  143. package/dist/lib/self-refresh-invocation.d.ts +60 -0
  144. package/dist/lib/self-refresh-invocation.d.ts.map +1 -0
  145. package/dist/lib/self-refresh-invocation.js +64 -0
  146. package/dist/lib/self-refresh-invocation.js.map +1 -0
  147. package/dist/lib/test-env.d.ts +50 -0
  148. package/dist/lib/test-env.d.ts.map +1 -1
  149. package/dist/lib/test-env.js +76 -0
  150. package/dist/lib/test-env.js.map +1 -1
  151. package/dist/terminal/attach-replay.d.ts +39 -0
  152. package/dist/terminal/attach-replay.d.ts.map +1 -0
  153. package/dist/terminal/attach-replay.js +76 -0
  154. package/dist/terminal/attach-replay.js.map +1 -0
  155. package/dist/terminal/pty-manager.d.ts.map +1 -1
  156. package/dist/terminal/pty-manager.js +35 -17
  157. package/dist/terminal/pty-manager.js.map +1 -1
  158. package/dist/terminal/pty-session.d.ts +81 -12
  159. package/dist/terminal/pty-session.d.ts.map +1 -1
  160. package/dist/terminal/pty-session.js +113 -25
  161. package/dist/terminal/pty-session.js.map +1 -1
  162. package/dist/terminal/session-screen.d.ts +25 -0
  163. package/dist/terminal/session-screen.d.ts.map +1 -1
  164. package/dist/terminal/session-screen.js +53 -8
  165. package/dist/terminal/session-screen.js.map +1 -1
  166. package/package.json +10 -7
  167. package/scripts/forge/gitea/pr-view.sh +4 -1
  168. package/scripts/forge/github/pr-view.sh +1 -1
  169. package/scripts/forge/gitlab/pr-view.sh +4 -1
  170. package/skeleton/.claude/skills/afx/SKILL.md +54 -0
  171. package/skeleton/.claude/skills/arch-save/SKILL.md +8 -2
  172. package/skeleton/.claude/skills/builder-refresh/SKILL.md +138 -0
  173. package/skeleton/.codex/skills/afx/SKILL.md +54 -0
  174. package/skeleton/.codex/skills/arch-save/SKILL.md +8 -2
  175. package/skeleton/.codex/skills/builder-refresh/SKILL.md +138 -0
  176. package/skeleton/protocol-schema.json +17 -0
  177. package/skeleton/protocols/air/protocol.json +1 -1
  178. package/skeleton/protocols/aspir/protocol.json +5 -1
  179. package/skeleton/protocols/bugfix/protocol.json +1 -1
  180. package/skeleton/protocols/experiment/protocol.json +1 -1
  181. package/skeleton/protocols/maintain/protocol.json +1 -1
  182. package/skeleton/protocols/pir/builder-prompt.md +3 -2
  183. package/skeleton/protocols/pir/prompts/implement.md +1 -1
  184. package/skeleton/protocols/pir/prompts/plan.md +1 -1
  185. package/skeleton/protocols/pir/prompts/review.md +1 -1
  186. package/skeleton/protocols/pir/protocol.json +1 -1
  187. package/skeleton/protocols/pir/protocol.md +4 -4
  188. package/skeleton/protocols/protocol-schema.json +17 -0
  189. package/skeleton/protocols/research/protocol.json +1 -1
  190. package/skeleton/protocols/spike/protocol.json +1 -1
  191. package/skeleton/protocols/spir/protocol.json +5 -1
  192. package/skeleton/resources/commands/agent-farm.md +14 -11
  193. package/skeleton/roles/builder.md +3 -4
  194. package/templates/3d-viewer.html +32 -4
  195. package/templates/open.html +58 -12
  196. package/templates/tower.html +26 -7
  197. package/templates/vendor/three-3MFLoader.js +1478 -0
  198. package/templates/vendor/three-STLLoader.js +410 -0
  199. package/templates/vendor/three-TrackballControls.js +828 -0
  200. package/templates/vendor/three-fflate.module.js +2474 -0
  201. package/templates/vendor/three.module.js +53044 -0
  202. package/dashboard-dist/assets/index-JCbkpAcV.js.map +0 -1
@@ -19,12 +19,94 @@ export declare function readBody(req: http.IncomingMessage, maxSize?: number): P
19
19
  */
20
20
  export declare function parseJsonBody(req: http.IncomingMessage, maxSize?: number): Promise<Record<string, unknown>>;
21
21
  /**
22
- * Security: Validate request origin
23
- * Currently allows all requests - security is handled by the server binding to localhost only.
22
+ * The expected local key, cached after first read. Issues the key if missing
23
+ * (Tower is the owner). Returns null and stays fail-closed if the key cannot be
24
+ * read or created (e.g. an unwritable `~/.agent-farm`).
25
+ */
26
+ export declare function getExpectedKey(): string | null;
27
+ /**
28
+ * Reset the cached key. Test-only seam; also lets a future rotation path force
29
+ * a re-read. Not wired to any runtime rotation in this change.
30
+ */
31
+ export declare function resetExpectedKeyCache(): void;
32
+ /**
33
+ * Constant-time key comparison. `timingSafeEqual` throws on unequal-length
34
+ * buffers, so length is checked first (a length mismatch is an immediate,
35
+ * non-secret reject).
36
+ */
37
+ export declare function keysMatch(presented: string, expected: string): boolean;
38
+ /**
39
+ * Routes intentionally reachable without the key. Kept deliberately narrow:
40
+ * pre-auth liveness/version probes, the Tower launcher shell, the React
41
+ * dashboard's static assets (the page loads keyless, then authenticates its
42
+ * own API/WebSocket calls with the key), the annotator shell + vendor files,
43
+ * and the cloud OAuth callback. Everything else requires the key.
44
+ *
45
+ * The entries here are the paths a *browser navigation* reaches — a top-level
46
+ * redirect, an iframe load, a `<script>`/`<link>` tag — none of which can
47
+ * attach the `codev-tower-key` header. Each one therefore carries either no
48
+ * secret or its own credential; adding a route here means proving that.
49
+ *
50
+ * The privileged workspace `file` reader and every `api/` or `ws/` subpath are
51
+ * explicitly excluded so a static-asset carve-out never exposes a data route.
52
+ */
53
+ export declare function isPublicRoute(method: string, pathname: string): boolean;
54
+ /**
55
+ * CORS origin allowlist (advisory Layer 3). Replaces the previous
56
+ * reflect-any-`https://` behavior. Allowed: loopback origins on any port
57
+ * (`http://localhost[:port]`, `http://127.0.0.1[:port]`) plus any origins an
58
+ * operator lists in `CODEV_TOWER_ALLOWED_ORIGINS` (comma-separated, exact
59
+ * match) for a tunnel/proxy deployment. Secure by default: no wildcard, no
60
+ * scheme-only reflection. CORS is defense-in-depth; the key check is the
61
+ * actual control.
62
+ */
63
+ export declare function isAllowedOrigin(origin: string): boolean;
64
+ /**
65
+ * Host allowlist (advisory GHSA-xvjp-7748-v88v). A DNS-rebinding guard for the
66
+ * key-bearing dashboard shell: the key is injected into the (public) shell, so a
67
+ * browser rebound to Tower's address would carry the attacker's hostname in
68
+ * `Host` and be rejected before the shell is served. Allowed: loopback hostnames
69
+ * and the hostnames of any `CODEV_TOWER_ALLOWED_ORIGINS` (the same var the CORS
70
+ * allowlist uses, so a tunnel/proxy deployment configures both).
71
+ *
72
+ * In BRIDGE_MODE the operator has deliberately exposed Tower on the network, so
73
+ * the allowlist ALSO accepts IP-literal Hosts — a LAN client reaches Tower by IP
74
+ * (`http://192.168.1.5:4100/`), while DNS rebinding requires a host *name*, which
75
+ * stays rejected even in bridge mode. Hostname-based LAN access uses the
76
+ * `CODEV_TOWER_ALLOWED_ORIGINS` escape hatch. The key (mandatory under bridge
77
+ * mode) remains the primary control and is checked separately for keyed routes;
78
+ * this relaxation never weakens it.
79
+ */
80
+ export declare function isAllowedHost(hostHeader: string | undefined): boolean;
81
+ /**
82
+ * Security: decide whether an HTTP request may proceed.
83
+ *
84
+ * Public-allowlisted routes pass keyless; every other route must present a
85
+ * `codev-tower-key` header that constant-time-matches the expected local key.
86
+ * Fails closed when the expected key is unavailable.
87
+ *
24
88
  * @param req - HTTP incoming message
25
- * @returns true (always allowed)
89
+ * @returns true if the request is authorized
90
+ */
91
+ export declare function isRequestAllowed(req: http.IncomingMessage): boolean;
92
+ /**
93
+ * The subprotocol the server echoes back on a terminal WebSocket handshake:
94
+ * the non-secret marker if the client offered it, else none. NEVER echoes the
95
+ * `codev-key.<key>` token — echoing it would leak the key into a response
96
+ * header. Used as the `ws` server's `handleProtocols` (advisory
97
+ * GHSA-xvjp-7748-v88v). Exported so the echo contract is unit-testable.
98
+ */
99
+ export declare function selectWsSubprotocol(offered: Set<string>): string | false;
100
+ /**
101
+ * Security: decide whether a WebSocket upgrade may proceed. Validated at the
102
+ * handshake (before any PTY attach), independent of the `Origin` header so a
103
+ * missing Origin can never degrade into an auth bypass. Fails closed when the
104
+ * expected key is unavailable.
105
+ *
106
+ * @param req - HTTP upgrade request
107
+ * @returns true if the upgrade is authorized
26
108
  */
27
- export declare function isRequestAllowed(_req: http.IncomingMessage): boolean;
109
+ export declare function isWebSocketAllowed(req: http.IncomingMessage): boolean;
28
110
  /**
29
111
  * Validate a bind host value for server.listen().
30
112
  *
@@ -1 +1 @@
1
- {"version":3,"file":"server-utils.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/utils/server-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAEvC;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB1F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAyBhH;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAEpE;AACD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiCjD"}
1
+ {"version":3,"file":"server-utils.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/utils/server-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAKvC;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB1F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAyBhH;AAsBD;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAS9C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKtE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA2CvE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAWvD;AAwCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CASrE;AAmBD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAsBnE;AAqBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,KAAK,CAExE;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAUrE;AACD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiCjD"}
@@ -3,6 +3,9 @@
3
3
  * Extracted from tower-server.ts and open-server.ts
4
4
  * to eliminate code duplication (Maintenance Run 0004)
5
5
  */
6
+ import { timingSafeEqual } from 'node:crypto';
7
+ import { ensureLocalKey } from '@cluesmith/codev-core/auth';
8
+ import { TOWER_KEY_HEADER, LEGACY_WEB_KEY_HEADER, WS_MARKER_PROTOCOL, WS_KEY_PROTOCOL_PREFIX } from '@cluesmith/codev-types';
6
9
  /**
7
10
  * HTML-escape a string to prevent XSS
8
11
  */
@@ -63,14 +66,309 @@ export function parseJsonBody(req, maxSize = 1024 * 1024) {
63
66
  req.on('error', reject);
64
67
  });
65
68
  }
69
+ // ============================================================================
70
+ // Request authentication (advisory GHSA-xvjp-7748-v88v)
71
+ // ============================================================================
72
+ //
73
+ // Tower's local HTTP + WebSocket API reaches privileged local operations, so
74
+ // every request that is not on the narrow public-route allowlist must present
75
+ // the shared local key (`~/.agent-farm/local-key`) in the `codev-tower-key`
76
+ // header. Enforcement is server-side only (server/client isolation, #1189):
77
+ // clients merely transport the key.
78
+ // Wire-contract names (header + WS subprotocols) live in `@cluesmith/codev-types`
79
+ // so the server and every client share one source of truth.
66
80
  /**
67
- * Security: Validate request origin
68
- * Currently allows all requests - security is handled by the server binding to localhost only.
81
+ * Cached expected key. `undefined` = not yet loaded; `null` = load failed
82
+ * (fail closed — reject every authenticated request). Tower owns generation,
83
+ * so under normal operation the key file exists after boot.
84
+ */
85
+ let cachedExpectedKey;
86
+ /**
87
+ * The expected local key, cached after first read. Issues the key if missing
88
+ * (Tower is the owner). Returns null and stays fail-closed if the key cannot be
89
+ * read or created (e.g. an unwritable `~/.agent-farm`).
90
+ */
91
+ export function getExpectedKey() {
92
+ if (cachedExpectedKey === undefined) {
93
+ try {
94
+ cachedExpectedKey = ensureLocalKey() || null;
95
+ }
96
+ catch {
97
+ cachedExpectedKey = null;
98
+ }
99
+ }
100
+ return cachedExpectedKey;
101
+ }
102
+ /**
103
+ * Reset the cached key. Test-only seam; also lets a future rotation path force
104
+ * a re-read. Not wired to any runtime rotation in this change.
105
+ */
106
+ export function resetExpectedKeyCache() {
107
+ cachedExpectedKey = undefined;
108
+ }
109
+ /**
110
+ * Constant-time key comparison. `timingSafeEqual` throws on unequal-length
111
+ * buffers, so length is checked first (a length mismatch is an immediate,
112
+ * non-secret reject).
113
+ */
114
+ export function keysMatch(presented, expected) {
115
+ const a = Buffer.from(presented, 'utf8');
116
+ const b = Buffer.from(expected, 'utf8');
117
+ if (a.length !== b.length)
118
+ return false;
119
+ return timingSafeEqual(a, b);
120
+ }
121
+ /**
122
+ * Routes intentionally reachable without the key. Kept deliberately narrow:
123
+ * pre-auth liveness/version probes, the Tower launcher shell, the React
124
+ * dashboard's static assets (the page loads keyless, then authenticates its
125
+ * own API/WebSocket calls with the key), the annotator shell + vendor files,
126
+ * and the cloud OAuth callback. Everything else requires the key.
127
+ *
128
+ * The entries here are the paths a *browser navigation* reaches — a top-level
129
+ * redirect, an iframe load, a `<script>`/`<link>` tag — none of which can
130
+ * attach the `codev-tower-key` header. Each one therefore carries either no
131
+ * secret or its own credential; adding a route here means proving that.
132
+ *
133
+ * The privileged workspace `file` reader and every `api/` or `ws/` subpath are
134
+ * explicitly excluded so a static-asset carve-out never exposes a data route.
135
+ */
136
+ export function isPublicRoute(method, pathname) {
137
+ if (method !== 'GET')
138
+ return false;
139
+ if (pathname === '/health')
140
+ return true;
141
+ if (pathname === '/api/version')
142
+ return true;
143
+ if (pathname === '/' || pathname === '/index.html')
144
+ return true;
145
+ // Cloud OAuth callback (#1570). cloud.codevos.ai ends registration by
146
+ // redirecting the browser here, and a cross-site top-level navigation cannot
147
+ // carry the key header — so the key check can only ever 401 the flow. The
148
+ // route is self-authenticating instead: the nonce IS the credential. It is
149
+ // single-use with a 5-minute TTL and can only be minted by an authenticated
150
+ // POST /api/tunnel/connect, and handleTunnelEndpoint 400s ("Invalid or
151
+ // expired registration link") without a valid one, so a keyless request
152
+ // registers nothing. Exact match only — never widen this to an /api/tunnel/
153
+ // prefix, which would expose the keyed status/disconnect routes.
154
+ if (pathname === '/api/tunnel/connect/callback')
155
+ return true;
156
+ // React SPA served under /workspace/<encoded>/... — static assets only. The
157
+ // trailing subpath is optional: bare /workspace/<enc> serves the SPA shell,
158
+ // same as /workspace/<enc>/ (handleWorkspaceRoutes treats them identically).
159
+ const workspaceMatch = pathname.match(/^\/workspace\/[^/]+(?:\/(.*))?$/);
160
+ if (workspaceMatch) {
161
+ const subPath = workspaceMatch[1] || '';
162
+ // Annotator: its HTML shell and vendor libraries are loaded by iframe
163
+ // navigation and <script>/<link> tags that cannot carry the key header, so
164
+ // they are public (no secret; the shell gets the key injected same-origin).
165
+ // Every data/media sub-route (file, save, api/mtime, api/image, ...) stays
166
+ // keyed — the shell fetches them with the key.
167
+ const annotate = subPath.match(/^api\/annotate\/[^/]+(?:\/(.*))?$/);
168
+ if (annotate) {
169
+ const annotateSub = annotate[1] || '';
170
+ return annotateSub === '' || annotateSub.startsWith('vendor/');
171
+ }
172
+ if (subPath.startsWith('api/') || subPath === 'api')
173
+ return false;
174
+ if (subPath.startsWith('ws/') || subPath === 'ws')
175
+ return false;
176
+ if (subPath === 'file')
177
+ return false;
178
+ return true;
179
+ }
180
+ return false;
181
+ }
182
+ /**
183
+ * CORS origin allowlist (advisory Layer 3). Replaces the previous
184
+ * reflect-any-`https://` behavior. Allowed: loopback origins on any port
185
+ * (`http://localhost[:port]`, `http://127.0.0.1[:port]`) plus any origins an
186
+ * operator lists in `CODEV_TOWER_ALLOWED_ORIGINS` (comma-separated, exact
187
+ * match) for a tunnel/proxy deployment. Secure by default: no wildcard, no
188
+ * scheme-only reflection. CORS is defense-in-depth; the key check is the
189
+ * actual control.
190
+ */
191
+ export function isAllowedOrigin(origin) {
192
+ if (/^http:\/\/localhost(:\d+)?$/.test(origin))
193
+ return true;
194
+ if (/^http:\/\/127\.0\.0\.1(:\d+)?$/.test(origin))
195
+ return true;
196
+ const configured = process.env.CODEV_TOWER_ALLOWED_ORIGINS;
197
+ if (configured) {
198
+ for (const allowed of configured.split(',')) {
199
+ if (allowed.trim() === origin)
200
+ return true;
201
+ }
202
+ }
203
+ return false;
204
+ }
205
+ /** Extract the hostname (no port) from a `Host` header value. */
206
+ function hostnameOf(hostHeader) {
207
+ const h = hostHeader.trim();
208
+ if (h.startsWith('[')) {
209
+ // IPv6 literal: [::1] or [::1]:port. The remainder after ] must be empty or
210
+ // a :port — otherwise the value is malformed and returned whole (matching
211
+ // nothing on the allowlist) rather than trusting its leading literal.
212
+ const end = h.indexOf(']');
213
+ if (end < 0)
214
+ return h;
215
+ const rest = h.slice(end + 1);
216
+ if (rest === '' || /^:\d+$/.test(rest))
217
+ return h.slice(1, end);
218
+ return h;
219
+ }
220
+ const colon = h.lastIndexOf(':');
221
+ if (colon >= 0 && /^\d+$/.test(h.slice(colon + 1))) {
222
+ return h.slice(0, colon);
223
+ }
224
+ return h;
225
+ }
226
+ /** True if `hostname` is an IPv4 or IPv6 literal (brackets already stripped). */
227
+ function isIpLiteral(hostname) {
228
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname))
229
+ return true; // IPv4
230
+ return hostname.includes(':'); // IPv6 (a DNS name never contains ':')
231
+ }
232
+ /** True if `hostname` matches a `CODEV_TOWER_ALLOWED_ORIGINS` entry's host. */
233
+ function isConfiguredHost(hostname) {
234
+ const configured = process.env.CODEV_TOWER_ALLOWED_ORIGINS;
235
+ if (!configured)
236
+ return false;
237
+ for (const origin of configured.split(',')) {
238
+ try {
239
+ if (new URL(origin.trim()).hostname.toLowerCase() === hostname)
240
+ return true;
241
+ }
242
+ catch { /* ignore malformed entry */ }
243
+ }
244
+ return false;
245
+ }
246
+ /**
247
+ * Host allowlist (advisory GHSA-xvjp-7748-v88v). A DNS-rebinding guard for the
248
+ * key-bearing dashboard shell: the key is injected into the (public) shell, so a
249
+ * browser rebound to Tower's address would carry the attacker's hostname in
250
+ * `Host` and be rejected before the shell is served. Allowed: loopback hostnames
251
+ * and the hostnames of any `CODEV_TOWER_ALLOWED_ORIGINS` (the same var the CORS
252
+ * allowlist uses, so a tunnel/proxy deployment configures both).
253
+ *
254
+ * In BRIDGE_MODE the operator has deliberately exposed Tower on the network, so
255
+ * the allowlist ALSO accepts IP-literal Hosts — a LAN client reaches Tower by IP
256
+ * (`http://192.168.1.5:4100/`), while DNS rebinding requires a host *name*, which
257
+ * stays rejected even in bridge mode. Hostname-based LAN access uses the
258
+ * `CODEV_TOWER_ALLOWED_ORIGINS` escape hatch. The key (mandatory under bridge
259
+ * mode) remains the primary control and is checked separately for keyed routes;
260
+ * this relaxation never weakens it.
261
+ */
262
+ export function isAllowedHost(hostHeader) {
263
+ if (!hostHeader)
264
+ return false;
265
+ const hostname = hostnameOf(hostHeader).toLowerCase();
266
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1')
267
+ return true;
268
+ if (isConfiguredHost(hostname))
269
+ return true;
270
+ if (process.env.BRIDGE_MODE === '1' && isIpLiteral(hostname))
271
+ return true;
272
+ return false;
273
+ }
274
+ /** Read one header value as a non-empty string, or null. */
275
+ function headerValue(raw) {
276
+ if (typeof raw === 'string' && raw.length > 0)
277
+ return raw;
278
+ if (Array.isArray(raw) && raw.length > 0 && raw[0])
279
+ return raw[0];
280
+ return null;
281
+ }
282
+ /**
283
+ * Read the key a client presented on an HTTP request, or null if absent. Prefers
284
+ * the current `codev-tower-key` header and falls back to the legacy
285
+ * `codev-web-key` (dual-accept for one release) so an already-installed client
286
+ * bundling an older sdk still authenticates while it updates.
287
+ */
288
+ function presentedHttpKey(req) {
289
+ return headerValue(req.headers[TOWER_KEY_HEADER]) ?? headerValue(req.headers[LEGACY_WEB_KEY_HEADER]);
290
+ }
291
+ /**
292
+ * Security: decide whether an HTTP request may proceed.
293
+ *
294
+ * Public-allowlisted routes pass keyless; every other route must present a
295
+ * `codev-tower-key` header that constant-time-matches the expected local key.
296
+ * Fails closed when the expected key is unavailable.
297
+ *
69
298
  * @param req - HTTP incoming message
70
- * @returns true (always allowed)
299
+ * @returns true if the request is authorized
300
+ */
301
+ export function isRequestAllowed(req) {
302
+ // Host guard runs first (even for public routes) — the key is injected into the
303
+ // public dashboard shell, so a rebound Host must not reach it.
304
+ if (!isAllowedHost(req.headers.host))
305
+ return false;
306
+ const method = req.method || 'GET';
307
+ let pathname = '/';
308
+ try {
309
+ pathname = new URL(req.url || '/', 'http://localhost').pathname;
310
+ }
311
+ catch {
312
+ return false;
313
+ }
314
+ if (isPublicRoute(method, pathname))
315
+ return true;
316
+ const expected = getExpectedKey();
317
+ if (!expected)
318
+ return false;
319
+ const presented = presentedHttpKey(req);
320
+ if (!presented)
321
+ return false;
322
+ return keysMatch(presented, expected);
323
+ }
324
+ /**
325
+ * Extract the presented key from a WebSocket upgrade's `Sec-WebSocket-Protocol`
326
+ * offer (the `codev-key.<KEY>` token), or null if absent/malformed.
327
+ */
328
+ function presentedWsKey(req) {
329
+ const raw = req.headers['sec-websocket-protocol'];
330
+ if (!raw)
331
+ return null;
332
+ const offered = (Array.isArray(raw) ? raw.join(',') : raw)
333
+ .split(',')
334
+ .map((p) => p.trim());
335
+ for (const proto of offered) {
336
+ if (proto.startsWith(WS_KEY_PROTOCOL_PREFIX)) {
337
+ const key = proto.slice(WS_KEY_PROTOCOL_PREFIX.length);
338
+ return key.length > 0 ? key : null;
339
+ }
340
+ }
341
+ return null;
342
+ }
343
+ /**
344
+ * The subprotocol the server echoes back on a terminal WebSocket handshake:
345
+ * the non-secret marker if the client offered it, else none. NEVER echoes the
346
+ * `codev-key.<key>` token — echoing it would leak the key into a response
347
+ * header. Used as the `ws` server's `handleProtocols` (advisory
348
+ * GHSA-xvjp-7748-v88v). Exported so the echo contract is unit-testable.
349
+ */
350
+ export function selectWsSubprotocol(offered) {
351
+ return offered.has(WS_MARKER_PROTOCOL) ? WS_MARKER_PROTOCOL : false;
352
+ }
353
+ /**
354
+ * Security: decide whether a WebSocket upgrade may proceed. Validated at the
355
+ * handshake (before any PTY attach), independent of the `Origin` header so a
356
+ * missing Origin can never degrade into an auth bypass. Fails closed when the
357
+ * expected key is unavailable.
358
+ *
359
+ * @param req - HTTP upgrade request
360
+ * @returns true if the upgrade is authorized
71
361
  */
72
- export function isRequestAllowed(_req) {
73
- return true;
362
+ export function isWebSocketAllowed(req) {
363
+ if (!isAllowedHost(req.headers.host))
364
+ return false;
365
+ const expected = getExpectedKey();
366
+ if (!expected)
367
+ return false;
368
+ const presented = presentedWsKey(req);
369
+ if (!presented)
370
+ return false;
371
+ return keysMatch(presented, expected);
74
372
  }
75
373
  /**
76
374
  * Validate a bind host value for server.listen().
@@ -1 +1 @@
1
- {"version":3,"file":"server-utils.js","sourceRoot":"","sources":["../../../src/agent-farm/utils/server-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server-utils.js","sourceRoot":"","sources":["../../../src/agent-farm/utils/server-utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Context-refresh boundary resolution and task emission (Spec 1470).
3
+ *
4
+ * ## What a boundary is, and why it is recorded rather than inferred
5
+ *
6
+ * A boundary is a moment porch already transitions the state machine, at which a
7
+ * builder's context can be discarded without losing anything: the spec, the plan,
8
+ * `status.yaml`, the thread narrative and git carry the durable state, so a
9
+ * refreshed builder re-orients from disk rather than from memory.
10
+ *
11
+ * The refresh is DESTRUCTIVE — it ends in `/clear`, which has no undo — so it must
12
+ * fire at most once per boundary. Porch transitions can loop: issue #1408 saw
13
+ * verify-approval reset every plan phase back to `pending`, which would replay
14
+ * every plan-phase advance. A side effect that wipes context cannot be guarded by
15
+ * inferring "we must already have done this" from phase or iteration; it has to be
16
+ * a RECORDED FACT. That is what `context_refreshes` in `status.yaml` is.
17
+ *
18
+ * ## Where at-most-once actually comes from
19
+ *
20
+ * Not from a lock or a guard, but from control flow. Each transition site mutates
21
+ * the phase fields AND appends the boundary record in ONE `writeStateAndCommit`,
22
+ * then returns the refresh task INSTEAD of recursing into `next()`. The next
23
+ * `porch next` reads a state where the boundary is already recorded, so
24
+ * {@link hasRefreshed} is true and the normal path runs. There is no window in
25
+ * which the record and the transition disagree, because they are the same write.
26
+ *
27
+ * ## Failure semantics: consumed means consumed
28
+ *
29
+ * The boundary is recorded when the task is EMITTED, not when the refresh
30
+ * succeeds. A refresh that fails, or that the builder never runs, is never
31
+ * retried. That is deliberate:
32
+ *
33
+ * - The builder-side command never writes `status.yaml` (only porch does), so
34
+ * there is no completion signal to model and no way for a failed refresh to
35
+ * corrupt protocol state.
36
+ * - A missed refresh costs context and nothing else. A repeated one costs a
37
+ * builder's working memory, over and over, with nobody watching.
38
+ *
39
+ * Asymmetric costs, so the design fails toward doing nothing.
40
+ */
41
+ import type { PorchTask, ProjectState, Protocol } from './types.js';
42
+ /**
43
+ * Identifier for entering a protocol phase.
44
+ *
45
+ * Derived from the transition rather than from a configured label, so the record
46
+ * in `status.yaml` cannot drift from the event that produced it.
47
+ */
48
+ export declare function enterBoundary(phaseId: string): string;
49
+ /** Identifier for advancing INTO a given plan phase. */
50
+ export declare function planPhaseBoundary(planPhaseId: string): string;
51
+ /**
52
+ * Is a phase-entry boundary declared for this protocol?
53
+ *
54
+ * Inspects the FIELDS, never the presence of the `context_refresh` object.
55
+ * `context_refresh: {}` is valid and declares nothing, so a truthiness check on
56
+ * the object would report every boundary as declared for a protocol that opted
57
+ * into none of them.
58
+ */
59
+ export declare function declaresEnter(protocol: Protocol, phaseId: string): boolean;
60
+ /** Is the plan-phase-advance boundary declared for this protocol? */
61
+ export declare function declaresPlanPhaseAdvance(protocol: Protocol): boolean;
62
+ /** Has this boundary already been refreshed for this project? */
63
+ export declare function hasRefreshed(state: ProjectState, boundary: string): boolean;
64
+ /**
65
+ * Record a boundary on the state object.
66
+ *
67
+ * Mutates rather than writing: the caller folds this into the SAME
68
+ * `writeStateAndCommit` as the transition itself, which is what makes the record
69
+ * and the transition atomic. A separate write here would open exactly the window
70
+ * this design closes.
71
+ */
72
+ export declare function recordRefresh(state: ProjectState, boundary: string, now: string): void;
73
+ /**
74
+ * Mark every recorded boundary as acknowledged, returning true if anything changed.
75
+ *
76
+ * ## Why an acknowledgment exists at all
77
+ *
78
+ * A refresh is unattended, and its worst failure is silent: the builder clears,
79
+ * the re-entry never arrives, and nothing ever says so. The plan's first design
80
+ * tried to DERIVE that from `updated_at` — but the normal task-emission path in
81
+ * `next()` writes no state, so `updated_at` stays pinned at the transition for
82
+ * the whole of a healthy build. A threshold long enough to avoid false positives
83
+ * would be far too long to catch the stall it exists for.
84
+ *
85
+ * So porch records the fact instead. This runs on the first `porch next` that
86
+ * reaches the NORMAL path after a boundary — which only happens if a builder came
87
+ * back and asked for work. A boundary recorded but never acknowledged therefore
88
+ * means exactly one thing: nobody returned.
89
+ *
90
+ * ## Why porch owns it
91
+ *
92
+ * The builder-side command is forbidden from writing `status.yaml`, and that
93
+ * prohibition is load-bearing — it is what makes a failed refresh incapable of
94
+ * corrupting protocol state. So the acknowledgment cannot come from the side that
95
+ * would most naturally report "I'm back"; it has to be inferred by the only
96
+ * writer, from the only evidence it has: the builder asked for its next task.
97
+ *
98
+ * Writes at most once per boundary, so this costs one extra state write per
99
+ * refresh rather than one per `porch next`.
100
+ *
101
+ * ## What this cannot distinguish, and why that is accepted
102
+ *
103
+ * Any normal-path pass acknowledges EVERY outstanding boundary. So a refresh
104
+ * that was refused before clearing — after which the builder simply runs `porch
105
+ * next` — reads as acknowledged, as does a stall a human recovered by hand.
106
+ *
107
+ * That is not a bug to fix here, because porch has exactly one piece of
108
+ * evidence: the builder asked for work. It cannot tell "cleared and came back"
109
+ * from "never cleared" from "was rescued", and inventing a distinction it cannot
110
+ * observe would be worse than admitting the limit. What the signal genuinely
111
+ * means is narrow and still useful: **no builder has asked for work since this
112
+ * boundary was recorded.**
113
+ */
114
+ export declare function acknowledgeRefreshes(state: ProjectState, now: string): boolean;
115
+ /**
116
+ * How long a refresh may legitimately take before silence means trouble.
117
+ *
118
+ * A refresh is not instant: the builder writes a save (minutes, for a real one),
119
+ * the re-entry is held by Tower for {@link DEFAULT_REENTRY_DELAY_SECONDS}, the
120
+ * mailbox waits for a clean prompt, and only then does the builder run `porch
121
+ * next`. Ten minutes is comfortably past all of that and still far sooner than a
122
+ * human would otherwise notice a builder sitting cleared and idle.
123
+ */
124
+ export declare const REFRESH_STALL_GRACE_MS: number;
125
+ /**
126
+ * Boundaries recorded but never acknowledged — the raw fact, no timing applied.
127
+ *
128
+ * Use this for HISTORY (what has and has not been acknowledged). For deciding
129
+ * whether to warn a human, use {@link stalledRefreshes}, which adds the grace
130
+ * period.
131
+ */
132
+ export declare function unacknowledgedRefreshes(state: ProjectState): Array<{
133
+ boundary: string;
134
+ at: string;
135
+ }>;
136
+ /**
137
+ * Unacknowledged boundaries old enough that silence is a problem.
138
+ *
139
+ * ## Why a grace period after all
140
+ *
141
+ * An earlier version of this warned on ANY unacknowledged boundary, with a
142
+ * comment arguing that time-based rules do not work here. That argument
143
+ * conflated two different things, and the result was a warning on every healthy
144
+ * refresh — a false positive during completely normal operation, every single
145
+ * time.
146
+ *
147
+ * What genuinely does not work is deriving the stall from `updated_at`, because
148
+ * that timestamp does not move during a healthy build. But the ACKNOWLEDGMENT
149
+ * does move, exactly when the builder returns. Given a reliable event, a grace
150
+ * period is simply the answer to "has enough time passed that silence is
151
+ * suspicious?" — which is the question, and which the plan asked for in the
152
+ * words "past a threshold".
153
+ *
154
+ * A signal that fires during normal operation is not a signal.
155
+ */
156
+ export declare function stalledRefreshes(state: ProjectState, now: number, graceMs?: number): Array<{
157
+ boundary: string;
158
+ at: string;
159
+ ageMs: number;
160
+ }>;
161
+ /**
162
+ * Should a refresh fire for `boundary`, and is it declared?
163
+ *
164
+ * Deliberately does NOT consider how much context the builder has used. The spec
165
+ * fixes always-fire-at-configured-boundaries over threshold triggering:
166
+ * thresholds would need harness introspection porch does not have, and would make
167
+ * the behavior non-deterministic and untestable. An unnecessary refresh at a
168
+ * clean boundary costs a re-read of artifacts the builder can already see.
169
+ */
170
+ export declare function shouldRefresh(state: ProjectState, declared: boolean, boundary: string): boolean;
171
+ /**
172
+ * The single task emitted at a boundary.
173
+ *
174
+ * Deliberately does NOT instruct `porch done`. A refresh is not a build: `porch
175
+ * done` would validate checks and advance the build state, which is wrong at a
176
+ * moment when nothing was built. The builder refreshes and then re-enters through
177
+ * `porch next`, which is also what the post-clear re-entry frame says — so both
178
+ * paths converge on the same next command whether or not the clear lands.
179
+ */
180
+ export declare function buildRefreshTask(boundary: string): PorchTask;
181
+ //# sourceMappingURL=context-refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-refresh.d.ts","sourceRoot":"","sources":["../../../src/commands/porch/context-refresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,KAAK,EAAwB,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAM1F;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7D;AAMD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAG1E;AAED,qEAAqE;AACrE,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAEpE;AAMD,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE3E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAItF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAS9E;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAAiB,CAAC;AAErD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,YAAY,GAClB,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,CAIzC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,MAAM,EACX,OAAO,SAAyB,GAC/B,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAOxD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,OAAO,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAET;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CA0C5D"}