@cluesmith/codev 3.3.3 → 3.3.5

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 (244) hide show
  1. package/dashboard-dist/assets/index-BuflRUi6.js +220 -0
  2. package/dashboard-dist/assets/index-BuflRUi6.js.map +1 -0
  3. package/dashboard-dist/assets/index-r3bZzLmX.css +32 -0
  4. package/dashboard-dist/index.html +2 -2
  5. package/dist/agent-farm/cli.d.ts.map +1 -1
  6. package/dist/agent-farm/cli.js +48 -0
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/ide.d.ts +20 -0
  9. package/dist/agent-farm/commands/ide.d.ts.map +1 -0
  10. package/dist/agent-farm/commands/ide.js +138 -0
  11. package/dist/agent-farm/commands/ide.js.map +1 -0
  12. package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/inbox.js +15 -3
  14. package/dist/agent-farm/commands/inbox.js.map +1 -1
  15. package/dist/agent-farm/commands/send.d.ts +3 -2
  16. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/send.js +40 -6
  18. package/dist/agent-farm/commands/send.js.map +1 -1
  19. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  20. package/dist/agent-farm/commands/spawn-worktree.js +28 -9
  21. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  22. package/dist/agent-farm/commands/tower.d.ts +21 -9
  23. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  24. package/dist/agent-farm/commands/tower.js +90 -30
  25. package/dist/agent-farm/commands/tower.js.map +1 -1
  26. package/dist/agent-farm/db/tower-owner.d.ts +168 -0
  27. package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
  28. package/dist/agent-farm/db/tower-owner.js +369 -0
  29. package/dist/agent-farm/db/tower-owner.js.map +1 -0
  30. package/dist/agent-farm/db/types.d.ts +30 -5
  31. package/dist/agent-farm/db/types.d.ts.map +1 -1
  32. package/dist/agent-farm/db/types.js.map +1 -1
  33. package/dist/agent-farm/lib/ide-record.d.ts +65 -0
  34. package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
  35. package/dist/agent-farm/lib/ide-record.js +118 -0
  36. package/dist/agent-farm/lib/ide-record.js.map +1 -0
  37. package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
  38. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  39. package/dist/agent-farm/lib/tunnel-client.js +53 -5
  40. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  41. package/dist/agent-farm/servers/decisions/file.d.ts +50 -0
  42. package/dist/agent-farm/servers/decisions/file.d.ts.map +1 -0
  43. package/dist/agent-farm/servers/decisions/file.js +146 -0
  44. package/dist/agent-farm/servers/decisions/file.js.map +1 -0
  45. package/dist/agent-farm/servers/decisions/grammar.d.ts +36 -0
  46. package/dist/agent-farm/servers/decisions/grammar.d.ts.map +1 -0
  47. package/dist/agent-farm/servers/decisions/grammar.js +109 -0
  48. package/dist/agent-farm/servers/decisions/grammar.js.map +1 -0
  49. package/dist/agent-farm/servers/decisions/notice.d.ts +11 -0
  50. package/dist/agent-farm/servers/decisions/notice.d.ts.map +1 -0
  51. package/dist/agent-farm/servers/decisions/notice.js +18 -0
  52. package/dist/agent-farm/servers/decisions/notice.js.map +1 -0
  53. package/dist/agent-farm/servers/decisions/parse.d.ts +36 -0
  54. package/dist/agent-farm/servers/decisions/parse.d.ts.map +1 -0
  55. package/dist/agent-farm/servers/decisions/parse.js +308 -0
  56. package/dist/agent-farm/servers/decisions/parse.js.map +1 -0
  57. package/dist/agent-farm/servers/decisions/routes.d.ts +17 -0
  58. package/dist/agent-farm/servers/decisions/routes.d.ts.map +1 -0
  59. package/dist/agent-farm/servers/decisions/routes.js +160 -0
  60. package/dist/agent-farm/servers/decisions/routes.js.map +1 -0
  61. package/dist/agent-farm/servers/decisions/splice.d.ts +26 -0
  62. package/dist/agent-farm/servers/decisions/splice.d.ts.map +1 -0
  63. package/dist/agent-farm/servers/decisions/splice.js +120 -0
  64. package/dist/agent-farm/servers/decisions/splice.js.map +1 -0
  65. package/dist/agent-farm/servers/decisions/stamp.d.ts +4 -0
  66. package/dist/agent-farm/servers/decisions/stamp.d.ts.map +1 -0
  67. package/dist/agent-farm/servers/decisions/stamp.js +171 -0
  68. package/dist/agent-farm/servers/decisions/stamp.js.map +1 -0
  69. package/dist/agent-farm/servers/gate-profiles.d.ts +23 -2
  70. package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
  71. package/dist/agent-farm/servers/gate-profiles.js +43 -2
  72. package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
  73. package/dist/agent-farm/servers/ide-forward.d.ts +62 -0
  74. package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -0
  75. package/dist/agent-farm/servers/ide-forward.js +274 -0
  76. package/dist/agent-farm/servers/ide-forward.js.map +1 -0
  77. package/dist/agent-farm/servers/ide-server.d.ts +68 -0
  78. package/dist/agent-farm/servers/ide-server.d.ts.map +1 -0
  79. package/dist/agent-farm/servers/ide-server.js +281 -0
  80. package/dist/agent-farm/servers/ide-server.js.map +1 -0
  81. package/dist/agent-farm/servers/mailbox-delivery.d.ts +213 -4
  82. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  83. package/dist/agent-farm/servers/mailbox-delivery.js +489 -68
  84. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  85. package/dist/agent-farm/servers/mailbox-wiring.d.ts +18 -4
  86. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  87. package/dist/agent-farm/servers/mailbox-wiring.js +94 -21
  88. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
  89. package/dist/agent-farm/servers/message-write.d.ts +138 -11
  90. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  91. package/dist/agent-farm/servers/message-write.js +144 -35
  92. package/dist/agent-farm/servers/message-write.js.map +1 -1
  93. package/dist/agent-farm/servers/overview.d.ts +32 -15
  94. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  95. package/dist/agent-farm/servers/overview.js +181 -103
  96. package/dist/agent-farm/servers/overview.js.map +1 -1
  97. package/dist/agent-farm/servers/render-gate.d.ts +53 -0
  98. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  99. package/dist/agent-farm/servers/render-gate.js +135 -24
  100. package/dist/agent-farm/servers/render-gate.js.map +1 -1
  101. package/dist/agent-farm/servers/session-submit.d.ts +7 -4
  102. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
  103. package/dist/agent-farm/servers/session-submit.js +7 -4
  104. package/dist/agent-farm/servers/session-submit.js.map +1 -1
  105. package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
  106. package/dist/agent-farm/servers/tower-cron.js +4 -1
  107. package/dist/agent-farm/servers/tower-cron.js.map +1 -1
  108. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  109. package/dist/agent-farm/servers/tower-instances.js +6 -7
  110. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  111. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  112. package/dist/agent-farm/servers/tower-routes.js +116 -18
  113. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  114. package/dist/agent-farm/servers/tower-server.js +64 -7
  115. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  116. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  117. package/dist/agent-farm/servers/tower-terminals.js +53 -8
  118. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  119. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  120. package/dist/agent-farm/servers/tower-websocket.js +18 -1
  121. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  122. package/dist/agent-farm/utils/message-format.d.ts +8 -0
  123. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  124. package/dist/agent-farm/utils/message-format.js +21 -6
  125. package/dist/agent-farm/utils/message-format.js.map +1 -1
  126. package/dist/agent-farm/utils/port.d.ts +22 -0
  127. package/dist/agent-farm/utils/port.d.ts.map +1 -0
  128. package/dist/agent-farm/utils/port.js +35 -0
  129. package/dist/agent-farm/utils/port.js.map +1 -0
  130. package/dist/agent-farm/utils/server-utils.d.ts +26 -0
  131. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  132. package/dist/agent-farm/utils/server-utils.js +36 -0
  133. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  134. package/dist/cli.js +2 -2
  135. package/dist/cli.js.map +1 -1
  136. package/dist/commands/consult/index.d.ts +20 -1
  137. package/dist/commands/consult/index.d.ts.map +1 -1
  138. package/dist/commands/consult/index.js +117 -3
  139. package/dist/commands/consult/index.js.map +1 -1
  140. package/dist/commands/consult/tree-tripwire.d.ts +177 -0
  141. package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
  142. package/dist/commands/consult/tree-tripwire.js +356 -0
  143. package/dist/commands/consult/tree-tripwire.js.map +1 -0
  144. package/dist/commands/doctor.d.ts +69 -0
  145. package/dist/commands/doctor.d.ts.map +1 -1
  146. package/dist/commands/doctor.js +163 -0
  147. package/dist/commands/doctor.js.map +1 -1
  148. package/dist/commands/generate-image.d.ts +5 -0
  149. package/dist/commands/generate-image.d.ts.map +1 -1
  150. package/dist/commands/generate-image.js +225 -4
  151. package/dist/commands/generate-image.js.map +1 -1
  152. package/dist/commands/porch/checks.d.ts +7 -1
  153. package/dist/commands/porch/checks.d.ts.map +1 -1
  154. package/dist/commands/porch/checks.js +77 -14
  155. package/dist/commands/porch/checks.js.map +1 -1
  156. package/dist/commands/porch/index.d.ts +38 -1
  157. package/dist/commands/porch/index.d.ts.map +1 -1
  158. package/dist/commands/porch/index.js +79 -43
  159. package/dist/commands/porch/index.js.map +1 -1
  160. package/dist/commands/porch/protocol.d.ts +4 -3
  161. package/dist/commands/porch/protocol.d.ts.map +1 -1
  162. package/dist/commands/porch/protocol.js +32 -5
  163. package/dist/commands/porch/protocol.js.map +1 -1
  164. package/dist/commands/porch/types.d.ts +2 -0
  165. package/dist/commands/porch/types.d.ts.map +1 -1
  166. package/dist/lib/agent-env.d.ts +95 -0
  167. package/dist/lib/agent-env.d.ts.map +1 -0
  168. package/dist/lib/agent-env.js +137 -0
  169. package/dist/lib/agent-env.js.map +1 -0
  170. package/dist/lib/config.d.ts +8 -0
  171. package/dist/lib/config.d.ts.map +1 -1
  172. package/dist/lib/config.js.map +1 -1
  173. package/dist/lib/forge-contracts.d.ts +10 -0
  174. package/dist/lib/forge-contracts.d.ts.map +1 -1
  175. package/dist/lib/forge-contracts.js.map +1 -1
  176. package/dist/lib/forge-rate-limit.d.ts +45 -0
  177. package/dist/lib/forge-rate-limit.d.ts.map +1 -0
  178. package/dist/lib/forge-rate-limit.js +89 -0
  179. package/dist/lib/forge-rate-limit.js.map +1 -0
  180. package/dist/lib/forge.d.ts +28 -0
  181. package/dist/lib/forge.d.ts.map +1 -1
  182. package/dist/lib/forge.js +63 -10
  183. package/dist/lib/forge.js.map +1 -1
  184. package/dist/lib/github.d.ts +13 -1
  185. package/dist/lib/github.d.ts.map +1 -1
  186. package/dist/lib/github.js +58 -15
  187. package/dist/lib/github.js.map +1 -1
  188. package/dist/lib/test-env.d.ts +17 -0
  189. package/dist/lib/test-env.d.ts.map +1 -1
  190. package/dist/lib/test-env.js +20 -0
  191. package/dist/lib/test-env.js.map +1 -1
  192. package/dist/terminal/pty-session.d.ts +122 -4
  193. package/dist/terminal/pty-session.d.ts.map +1 -1
  194. package/dist/terminal/pty-session.js +142 -7
  195. package/dist/terminal/pty-session.js.map +1 -1
  196. package/dist/terminal/session-manager.d.ts +23 -2
  197. package/dist/terminal/session-manager.d.ts.map +1 -1
  198. package/dist/terminal/session-manager.js +115 -36
  199. package/dist/terminal/session-manager.js.map +1 -1
  200. package/dist/terminal/session-screen.d.ts +34 -5
  201. package/dist/terminal/session-screen.d.ts.map +1 -1
  202. package/dist/terminal/session-screen.js +52 -6
  203. package/dist/terminal/session-screen.js.map +1 -1
  204. package/dist/terminal/terminal-replies.d.ts +102 -0
  205. package/dist/terminal/terminal-replies.d.ts.map +1 -0
  206. package/dist/terminal/terminal-replies.js +181 -0
  207. package/dist/terminal/terminal-replies.js.map +1 -0
  208. package/package.json +4 -4
  209. package/scripts/forge/github/pr-list.sh +6 -2
  210. package/scripts/forge/github/pr-search.sh +4 -2
  211. package/scripts/forge/gitlab/issue-search.sh +1 -0
  212. package/scripts/forge/gitlab/pr-search.sh +35 -2
  213. package/scripts/forge/linear/auth-status.sh +1 -0
  214. package/scripts/forge/linear/issue-comment.sh +1 -0
  215. package/scripts/forge/linear/issue-list.sh +1 -0
  216. package/scripts/forge/linear/issue-search.sh +1 -0
  217. package/scripts/forge/linear/issue-view.sh +1 -0
  218. package/scripts/forge/linear/recently-closed.sh +1 -0
  219. package/scripts/forge/linear/user-identity.sh +1 -0
  220. package/skeleton/.claude/skills/afx/SKILL.md +15 -0
  221. package/skeleton/.claude/skills/arch-init/SKILL.md +56 -9
  222. package/skeleton/.claude/skills/arch-save/SKILL.md +100 -9
  223. package/skeleton/.claude/skills/consult/SKILL.md +9 -0
  224. package/skeleton/.claude/skills/generate-image/SKILL.md +19 -2
  225. package/skeleton/.claude/skills/porch/SKILL.md +18 -0
  226. package/skeleton/.codex/skills/afx/SKILL.md +15 -0
  227. package/skeleton/.codex/skills/arch-init/SKILL.md +56 -9
  228. package/skeleton/.codex/skills/arch-save/SKILL.md +100 -9
  229. package/skeleton/.codex/skills/consult/SKILL.md +9 -0
  230. package/skeleton/.codex/skills/generate-image/SKILL.md +19 -2
  231. package/skeleton/.codex/skills/porch/SKILL.md +18 -0
  232. package/skeleton/protocol-schema.json +6 -0
  233. package/skeleton/protocols/maintain/protocol.md +5 -0
  234. package/skeleton/protocols/pir/prompts/review.md +0 -1
  235. package/skeleton/protocols/protocol-schema.json +6 -0
  236. package/skeleton/resources/commands/agent-farm.md +71 -2
  237. package/skeleton/resources/commands/consult.md +9 -2
  238. package/skeleton/roles/consultant.md +32 -0
  239. package/skeleton/templates/AGENTS.md +1 -1
  240. package/skeleton/templates/CLAUDE.md +1 -1
  241. package/skeleton/templates/cheatsheet.md +1 -1
  242. package/dashboard-dist/assets/index-CUIjS_Vb.js +0 -199
  243. package/dashboard-dist/assets/index-CUIjS_Vb.js.map +0 -1
  244. package/dashboard-dist/assets/index-DnYLy5C_.css +0 -32
@@ -0,0 +1,369 @@
1
+ /**
2
+ * global.db owner lock (Issue #1629).
3
+ *
4
+ * The port bind in tower-server.ts is a SAME-PORT mutex only: a second Tower on
5
+ * a DIFFERENT port can still open the same global.db. That is exactly how the
6
+ * incident happened — a test Tower on port 14733 exported `AGENT_FARM_DIR`
7
+ * instead of `CODEV_AGENT_FARM_DIR` (#1515), so it resolved to the production
8
+ * `~/.agent-farm` and opened the live `global.db` (port and DB path are set by
9
+ * independent inputs: the port is a CLI arg, the DB comes from AGENT_FARM_DIR).
10
+ * Its startup reconcile then reconnected every live shellper socket (a shellper
11
+ * holds ONE client), stealing them from the real Tower on 4100, and the
12
+ * stale-row sweep deleted the rows.
13
+ *
14
+ * This module makes the second Tower REFUSE to start instead. The owner record
15
+ * is an exclusive LOCK FILE sitting next to the exact global.db being contended
16
+ * (`<db path>.tower-owner`), so it travels with that file: cross-`CODEV_AGENT_FARM_DIR`
17
+ * isolation is untouched (an isolated DB carries its own lock), and two isolated
18
+ * test Towers on different DB files never share a lock. Acquisition is an atomic
19
+ * `O_EXCL` create — the filesystem itself resolves a race between two starters. A
20
+ * second Tower reads the lock at boot, before reconcile; if a live Tower owns the
21
+ * DB it refuses loudly, otherwise it claims. A dead owner self-clears — a crashed
22
+ * Tower's lock has a dead pid, so the legitimate restart just reclaims.
23
+ *
24
+ * The lock is deliberately NOT a table in global.db: ownership is ephemeral
25
+ * process state (a cleanly-stopped Tower leaves nothing behind), so it needs no
26
+ * durable schema, no migration, and it sits naturally alongside the other
27
+ * runtime artifacts in AGENT_FARM_DIR (`tower.log`, `local-key`).
28
+ */
29
+ import http from 'node:http';
30
+ import os from 'node:os';
31
+ import fs from 'node:fs';
32
+ import path from 'node:path';
33
+ import crypto from 'node:crypto';
34
+ import { getGlobalDbPath } from './index.js';
35
+ /**
36
+ * The lock file for the active global.db: the DB path plus `.tower-owner`.
37
+ *
38
+ * The suffix is deliberately NOT `.lock`: SQLite's `unix-dotfile` VFS (a fallback
39
+ * on filesystems without POSIX locks) creates `<db>.lock`, which would collide.
40
+ * `.tower-owner` avoids every SQLite auxiliary name (`-wal`, `-shm`, `-journal`,
41
+ * `-mj*`, and the dotfile VFS's `.lock`).
42
+ */
43
+ export function defaultLockFile() {
44
+ return `${getGlobalDbPath()}.tower-owner`;
45
+ }
46
+ /** Serialize an owner record to the lock file's on-disk form. */
47
+ function serialize(owner) {
48
+ return JSON.stringify(owner);
49
+ }
50
+ /**
51
+ * Read the owner record from the lock file, or null if the lock is absent or its
52
+ * contents are unusable (corrupt / a partial write from a crashed acquire — the
53
+ * caller clears and reclaims those). A genuine IO error other than "not found"
54
+ * (permission, EISDIR) is rethrown so the boot guard fails CLOSED.
55
+ */
56
+ export function readTowerOwner(lockFile) {
57
+ let raw;
58
+ try {
59
+ raw = fs.readFileSync(lockFile, 'utf8');
60
+ }
61
+ catch (err) {
62
+ if (err.code === 'ENOENT')
63
+ return null;
64
+ throw err;
65
+ }
66
+ try {
67
+ const o = JSON.parse(raw);
68
+ if (typeof o.pid !== 'number' || typeof o.port !== 'number')
69
+ return null;
70
+ return {
71
+ pid: o.pid,
72
+ port: o.port,
73
+ hostname: typeof o.hostname === 'string' ? o.hostname : 'unknown',
74
+ startedAt: typeof o.startedAt === 'number' ? o.startedAt : 0,
75
+ dbDir: typeof o.dbDir === 'string' ? o.dbDir : '',
76
+ bindHost: typeof o.bindHost === 'string' ? o.bindHost : '127.0.0.1',
77
+ };
78
+ }
79
+ catch {
80
+ return null; // unparseable → treat as a corrupt lock the caller can clear
81
+ }
82
+ }
83
+ /** Overwrite the lock file with `owner` (non-atomic; for takeover seams / tests). */
84
+ export function writeTowerOwner(lockFile, owner) {
85
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
86
+ fs.writeFileSync(lockFile, serialize(owner));
87
+ }
88
+ /**
89
+ * Remove the lock file, but ONLY if it is still ours (pid match). Best-effort: a
90
+ * hard kill (SIGKILL) never reaches this, and the dead-pid liveness check
91
+ * self-clears the stale lock on the next start regardless. Called on graceful
92
+ * shutdown so the lock reflects reality between a clean stop and the next start.
93
+ */
94
+ export function releaseTowerOwnerIfMine(lockFile, pid) {
95
+ try {
96
+ const cur = readTowerOwner(lockFile);
97
+ if (cur && cur.pid === pid)
98
+ fs.rmSync(lockFile, { force: true });
99
+ }
100
+ catch {
101
+ // Lock gone / unreadable — nothing to release.
102
+ }
103
+ }
104
+ /** Whether a pid is a live process (SIGnal 0 probe, same as processExists). */
105
+ function pidAlive(pid) {
106
+ try {
107
+ process.kill(pid, 0);
108
+ return true;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ }
114
+ /**
115
+ * Probe `GET /health` on `host`:`port` (unauthenticated per server-utils).
116
+ *
117
+ * The budget is generous by default: `/health` calls `getInstances()` and shells
118
+ * out to `ps -A` (a 5s timeout of its own), so under the incident's load — 60+
119
+ * shellpers, a busy event loop — a tight budget would time out and be misread as
120
+ * "no Tower here", failing OPEN on the one guard that prevents data loss. Only
121
+ * paid on the conflict path (a live pid on a different port), so it costs nothing
122
+ * on the common restart.
123
+ */
124
+ export function probeTowerHealth(host, port, timeoutMs = 6000) {
125
+ return new Promise((resolve) => {
126
+ const req = http.request({ hostname: host, port, path: '/health', method: 'GET', timeout: timeoutMs }, (res) => {
127
+ let body = '';
128
+ res.setEncoding('utf8');
129
+ res.on('data', (chunk) => {
130
+ body += chunk;
131
+ // A tower /health payload is small; cap the read so a chatty non-tower
132
+ // process on the port can't stream unboundedly.
133
+ if (body.length > 64_000) {
134
+ res.destroy();
135
+ resolve('gone');
136
+ }
137
+ });
138
+ res.on('end', () => {
139
+ if (res.statusCode === 200) {
140
+ try {
141
+ resolve(JSON.parse(body).status === 'healthy' ? 'tower' : 'gone');
142
+ }
143
+ catch {
144
+ resolve('gone');
145
+ }
146
+ return;
147
+ }
148
+ if (res.statusCode === 503) {
149
+ // A live Tower can answer 503 {"error":"STARTING_UP"} from its readiness
150
+ // gate. In practice a mid-boot Tower usually HOLDS /health past this
151
+ // probe's budget, so that case times out to 'unreachable' (also refused);
152
+ // this branch covers a 503 returned within budget. Either way, a live owner.
153
+ try {
154
+ resolve(JSON.parse(body).error === 'STARTING_UP' ? 'tower' : 'unreachable');
155
+ }
156
+ catch {
157
+ resolve('unreachable');
158
+ }
159
+ return;
160
+ }
161
+ // Any other 5xx: a degraded-but-live server may answer 500 rather than a
162
+ // clean /health. Treat as inconclusive (fail closed), never as absent.
163
+ if (res.statusCode && res.statusCode >= 500) {
164
+ resolve('unreachable');
165
+ return;
166
+ }
167
+ // A non-5xx, non-Tower HTTP response — a process answered, but it is not
168
+ // a Tower serving /health (a squatter on a recycled port).
169
+ resolve('gone');
170
+ });
171
+ });
172
+ req.on('error', (err) => {
173
+ // ECONNREFUSED = nothing is listening on the port, so the recorded owner is
174
+ // not serving it (a live Tower always holds its port) → gone. Any other
175
+ // transport error is inconclusive → unreachable (fail closed).
176
+ resolve(err.code === 'ECONNREFUSED' ? 'gone' : 'unreachable');
177
+ });
178
+ req.on('timeout', () => {
179
+ req.destroy();
180
+ resolve('unreachable');
181
+ });
182
+ req.end();
183
+ });
184
+ }
185
+ /**
186
+ * Resolve the host to probe from the owner's recorded bind interface. A wildcard
187
+ * bind (`0.0.0.0` / `::` / bracketed `[::]` as validateHost emits) accepts
188
+ * loopback, so probe `127.0.0.1`; a specific bridge host is probed directly
189
+ * (probing loopback would falsely read 'gone').
190
+ */
191
+ export function resolveProbeHost(bindHost) {
192
+ if (!bindHost || bindHost === '0.0.0.0' || bindHost === '::' || bindHost === '[::]')
193
+ return '127.0.0.1';
194
+ // A specific bracketed IPv6 literal ([::1], [2001:db8::1]): http.request's
195
+ // `hostname` wants the address WITHOUT brackets, or it rejects it and the probe
196
+ // is never conclusive. Strip them.
197
+ const ipv6 = bindHost.match(/^\[(.+)\]$/);
198
+ if (ipv6)
199
+ return ipv6[1];
200
+ return bindHost;
201
+ }
202
+ /**
203
+ * Decide whether the recorded owner is a live Tower this process must yield to.
204
+ *
205
+ * - same port AND same bind interface as us: our own `server.listen` succeeded
206
+ * on that exact interface:port, and an identical bind is exclusive, so no other
207
+ * process holds it — any prior owner recorded there is gone. Claim. The bind
208
+ * interface must match: a wildcard bind (`0.0.0.0:P`) and a loopback bind
209
+ * (`127.0.0.1:P`) COEXIST on the same port, so `port` alone matching does not
210
+ * prove a differently-bound owner is gone — that path falls through to the probe.
211
+ * - dead owner pid: stale (a crashed Tower / a clean stop that outran the
212
+ * release). Claim — this is the self-clearing legitimate restart.
213
+ * - live pid: probe the owner's port. Only a `gone` result (connection refused,
214
+ * or a non-Tower response) proves the owner is absent → claim. A `tower`
215
+ * response, OR an `unreachable` inconclusive probe against a still-live pid, is
216
+ * treated as a live owner → refuse. This fails CLOSED: a loaded or mid-boot
217
+ * Tower whose `/health` misses the budget is never mistaken for a free DB.
218
+ */
219
+ export async function ownerIsLive(owner, myPort, myBindHost, deps = {}) {
220
+ const isAlive = deps.isAlive ?? pidAlive;
221
+ const probe = deps.probe ?? probeTowerHealth;
222
+ if (owner.port === myPort && owner.bindHost === myBindHost)
223
+ return false;
224
+ if (!isAlive(owner.pid))
225
+ return false;
226
+ return (await probe(resolveProbeHost(owner.bindHost), owner.port)) !== 'gone';
227
+ }
228
+ /**
229
+ * Bound on the acquire attempts under contention. Each losing attempt means
230
+ * another Tower held or rewrote the lock between our checks; a handful of retries
231
+ * converges in every realistic case, and exhausting them means the lock is
232
+ * churning between many simultaneous starts — where the safe answer is to refuse
233
+ * (fail closed), never to force the lock.
234
+ */
235
+ const MAX_CLAIM_ATTEMPTS = 5;
236
+ /**
237
+ * Atomically acquire the lock: write the FULL record to a unique temp file, then
238
+ * hard-link it onto the lock path. `link()` is atomic and fails with EEXIST if
239
+ * the path is already taken, and — unlike `writeFile({flag:'wx'})` — the linked
240
+ * file already has its complete content, so a concurrent reader never sees an
241
+ * empty/partial lock it could misclassify as corrupt and delete mid-write. The
242
+ * temp is always cleaned up (the hard link keeps the inode alive at the lock path).
243
+ * Returns whether we won the link.
244
+ */
245
+ function tryAcquire(lockFile, owner) {
246
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
247
+ const tmp = `${lockFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
248
+ fs.writeFileSync(tmp, serialize(owner));
249
+ try {
250
+ fs.linkSync(tmp, lockFile);
251
+ return true;
252
+ }
253
+ catch (err) {
254
+ const code = err.code;
255
+ if (code === 'EEXIST')
256
+ return false;
257
+ // Some filesystems don't support hard links (ENOSYS/EPERM, or a cross-device
258
+ // temp on EXDEV). Fall back to an O_EXCL create — a hair weaker (a brief
259
+ // empty-file window) but better than failing to boot at all.
260
+ if (code === 'ENOSYS' || code === 'EPERM' || code === 'EXDEV') {
261
+ try {
262
+ fs.writeFileSync(lockFile, serialize(owner), { flag: 'wx' });
263
+ return true;
264
+ }
265
+ catch (fallbackErr) {
266
+ if (fallbackErr.code === 'EEXIST')
267
+ return false;
268
+ throw fallbackErr;
269
+ }
270
+ }
271
+ throw err;
272
+ }
273
+ finally {
274
+ try {
275
+ fs.rmSync(tmp, { force: true });
276
+ }
277
+ catch { /* temp already gone */ }
278
+ }
279
+ }
280
+ /**
281
+ * Remove a stale lock, but ONLY if it still names the holder we probed (a
282
+ * compare-and-set on pid + startedAt). If a contender rewrote it in between, the
283
+ * content differs and we do NOT remove it — we loop and re-evaluate. Returns
284
+ * whether we removed it.
285
+ */
286
+ function removeStaleLock(lockFile, observed) {
287
+ const cur = readTowerOwner(lockFile);
288
+ if (!cur || cur.pid !== observed.pid || cur.startedAt !== observed.startedAt)
289
+ return false;
290
+ try {
291
+ fs.rmSync(lockFile, { force: true });
292
+ return true;
293
+ }
294
+ catch {
295
+ return false;
296
+ }
297
+ }
298
+ /**
299
+ * Boot guard: refuse if the DB already has a LIVE owner, otherwise claim
300
+ * ownership by writing the lock file. A decision plus a lock write — the caller
301
+ * decides how to fail loudly (log + exit).
302
+ *
303
+ * Acquisition is atomic against a concurrent second Tower: {@link tryAcquire}
304
+ * writes a full temp file and hard-links it onto the path, so the filesystem
305
+ * resolves a two-starter race (one link wins, the other gets EEXIST) with no
306
+ * empty-file window. A stale lock (dead / absent owner) is cleared with a
307
+ * compare-and-set on its contents and the link retried; the invariant preserved
308
+ * is "never remove a LIVE owner's lock". Under persistent churn the claim refuses
309
+ * rather than forcing the lock.
310
+ */
311
+ export async function claimGlobalDbOwnership(opts) {
312
+ const lockFile = opts.lockFile ?? defaultLockFile();
313
+ const { pid, port, dbDir, deps } = opts;
314
+ const bindHost = opts.bindHost ?? '127.0.0.1';
315
+ let lastSeen = null;
316
+ for (let attempt = 0; attempt < MAX_CLAIM_ATTEMPTS; attempt++) {
317
+ const mine = { pid, port, hostname: os.hostname(), startedAt: Date.now(), dbDir, bindHost };
318
+ // Atomic acquire: full-content temp + hard link (see tryAcquire).
319
+ if (tryAcquire(lockFile, mine))
320
+ return { ok: true };
321
+ // Lock exists — evaluate the current holder.
322
+ const existing = readTowerOwner(lockFile);
323
+ if (!existing) {
324
+ // Corrupt / vanished under us — clear it and retry the atomic acquire.
325
+ try {
326
+ fs.rmSync(lockFile, { force: true });
327
+ }
328
+ catch { /* already gone */ }
329
+ continue;
330
+ }
331
+ lastSeen = existing;
332
+ if (await ownerIsLive(existing, port, bindHost, deps)) {
333
+ return { ok: false, conflict: existing };
334
+ }
335
+ // Stale — remove ONLY if the lock still names the holder we probed, then loop
336
+ // to re-attempt the atomic acquire (the link is the serialization point). A
337
+ // failed CAS means a contender rewrote it: loop and re-evaluate, never force.
338
+ removeStaleLock(lockFile, existing);
339
+ }
340
+ // Persistent contention: the lock keeps changing under us. Refuse rather than
341
+ // risk forcing over a live claimant that appeared mid-flight. Name a real
342
+ // contender we observed, never ourselves (Claude CMAP).
343
+ const conflict = readTowerOwner(lockFile) ?? lastSeen ?? {
344
+ pid: -1, port, hostname: 'unknown (lock contended)', startedAt: Date.now(), dbDir, bindHost,
345
+ };
346
+ return { ok: false, conflict };
347
+ }
348
+ /**
349
+ * The loud, teaching error for a refused start. Names the live owner (pid/port/
350
+ * host) and the shared DB, and — because the misconfiguration that triggered the
351
+ * incident was a mistyped env var — spells out the `AGENT_FARM_DIR` vs
352
+ * `CODEV_AGENT_FARM_DIR` fix.
353
+ *
354
+ * This string is user-visible at the CLI (surfaced by the launcher on a fast-exit,
355
+ * Issue #1691), so it carries no internal issue numbers — the incident context
356
+ * (#1629's shellper hijack, the #1515 mistyped-env-var trigger) stays here in the
357
+ * comment, not in the text a user reads.
358
+ */
359
+ export function ownershipConflictMessage(conflict, dbDir) {
360
+ return [
361
+ `Refusing to start: this global.db is already owned by a live Tower.`,
362
+ ` owner pid ${conflict.pid} on port ${conflict.port} (host ${conflict.hostname})`,
363
+ ` shared database dir: ${dbDir}`,
364
+ `A second Tower opening a live global.db hijacks and deletes its shellper sessions.`,
365
+ `If you meant to run an isolated test Tower, set CODEV_AGENT_FARM_DIR (NOT AGENT_FARM_DIR) to a throwaway directory.`,
366
+ `Otherwise stop the existing Tower first ('afx tower stop'), or investigate pid ${conflict.pid} if you believe it is stale.`,
367
+ ].join('\n');
368
+ }
369
+ //# sourceMappingURL=tower-owner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tower-owner.js","sourceRoot":"","sources":["../../../src/agent-farm/db/tower-owner.ts"],"names":[],"mappings":"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"}
@@ -94,17 +94,42 @@ export type MailboxReason = 'busy' | 'no-profile' | 'no-live-pty';
94
94
  * dims the real TUI never adopted).
95
95
  * - `no-composer-marker` — no recognized marker at all (a wrapper/boot screen, a drifted
96
96
  * profile, or an unrenderable frame).
97
- * The latter two are the DEFECT class: the classifier could not verify anything, so the mail
98
- * will not deliver on its own. `GateVerdict.detail`'s fourth value, `empty`, is never persisted
99
- * — a clean verdict delivers the row (and delivery nulls both columns).
97
+ * - `recent-input` — the terminal saw INPUT too recently to write onto (Issue #1473):
98
+ * either it landed while the gate was classifying, or the
99
+ * input-settle interval has not elapsed. "Input" is whatever
100
+ * survives `stripTerminalReplies` on a write reaching the PTY
101
+ * under `PtySession.write()`'s default `'external'` origin — a
102
+ * human's keystrokes, and equally an ungated `--interrupt`/
103
+ * `--escape`, which is why the operator wording says "input"
104
+ * rather than "typing"; a chunk that is nothing but a terminal
105
+ * REPLY moves neither signal. Like `user-text` this
106
+ * is a SAFE hold that clears on its own — the same someone-else-is-
107
+ * writing class, caught one beat earlier, before the TUI has echoed
108
+ * anything for the classifier to see.
109
+ * - `composer-redraw` — the composer is EMPTY but its rendered region changed within the
110
+ * settle window (Issue #1664): a turn-end box redraw, a rotating
111
+ * placeholder, a resize, or an interrupt clearing the line. The
112
+ * recipient's own output ELSEWHERE on the screen — a spinner, a
113
+ * streaming transcript — is deliberately NOT this: writing onto an
114
+ * empty composer while the agent talks is safe and intended, since
115
+ * claude/codex queue input received mid-turn. Self-clearing, like
116
+ * `user-text` and `recent-input`; it resolves about a settle after
117
+ * the composer stops moving.
118
+ * The two can't-verify values are the DEFECT class: the classifier could not verify anything,
119
+ * so the mail will not deliver on its own. `GateVerdict.detail`'s fourth value, `empty`, is
120
+ * never persisted — a clean verdict delivers the row (and delivery nulls both columns).
121
+ *
122
+ * `recent-input` and `composer-redraw` are deliberately NOT part of {@link isUnverifiableVerdict}'s
123
+ * set: escalating either would false-alarm on every ordinary typist and every ordinary repaint. That predicate is an allow-list, so the value is
124
+ * correctly inert there by construction rather than by an edit.
100
125
  *
101
126
  * Null for every non-gate hold (`no-live-pty`, `no-profile`) and for the post-classify
102
- * token/settle re-holds, so a stale detail can never outlive the verdict that produced it.
127
+ * token re-hold, so a stale detail can never outlive the verdict that produced it.
103
128
  *
104
129
  * The DB column carries NO CHECK constraint (see `GLOBAL_SCHEMA` / migration v18); this type
105
130
  * is the enforcement.
106
131
  */
107
- export type MailboxGateDetail = 'user-text' | 'no-region-end' | 'no-composer-marker';
132
+ export type MailboxGateDetail = 'user-text' | 'no-region-end' | 'no-composer-marker' | 'recent-input' | 'composer-redraw';
108
133
  /**
109
134
  * Database row type for the mailbox table (Spec 1313).
110
135
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAe,MAAM,aAAa,CAAC;AAElG;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,CAAC;AAE9E;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,YAAY,GAAG,aAAa,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,iBAAiB,CAAC;AAEtB;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,WAAW,GAAG,cAAc,CAQ5E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAe1D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAM9D;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,UAAU,CAStE"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6IH;;GAEG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAAgB;IAC1D,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,EAAE;QACZ,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;QACxC,SAAS,EAAE,GAAG,CAAC,UAAU,IAAI,SAAS;KACvC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAc;IAC/C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE,GAAG,CAAC,MAA2B;QACvC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,IAAI,EAAE,GAAG,CAAC,IAAmB;QAC7B,QAAQ,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS;QACpC,YAAY,EAAE,GAAG,CAAC,aAAa,IAAI,SAAS;QAC5C,WAAW,EAAE,GAAG,CAAC,YAAY,IAAI,SAAS;QAC1C,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;QACxC,kBAAkB,EAAE,GAAG,CAAC,oBAAoB,IAAI,SAAS;KAC1D,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;KACzC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAiB;IACxD,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE;YACN,IAAI,EAAE,GAAG,CAAC,WAA2C;YACrD,EAAE,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS;SAC/B;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/agent-farm/db/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2KH;;GAEG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAAgB;IAC1D,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,EAAE;QACZ,GAAG,EAAE,GAAG,CAAC,GAAG;QACZ,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;QACxC,SAAS,EAAE,GAAG,CAAC,UAAU,IAAI,SAAS;KACvC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAc;IAC/C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE,GAAG,CAAC,MAA2B;QACvC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,IAAI,EAAE,GAAG,CAAC,IAAmB;QAC7B,QAAQ,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS;QACpC,YAAY,EAAE,GAAG,CAAC,aAAa,IAAI,SAAS;QAC5C,WAAW,EAAE,GAAG,CAAC,YAAY,IAAI,SAAS;QAC1C,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;QACxC,kBAAkB,EAAE,GAAG,CAAC,oBAAoB,IAAI,SAAS;KAC1D,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,UAAU,EAAE,GAAG,CAAC,WAAW,IAAI,SAAS;KACzC,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAiB;IACxD,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE;YACN,IAAI,EAAE,GAAG,CAAC,WAA2C;YACrD,EAAE,EAAE,GAAG,CAAC,SAAS,IAAI,SAAS;SAC/B;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * IDE server runtime record (Issue #1668).
3
+ *
4
+ * Tower forwards a fixed `/ide/` prefix to a single local VS Code server-web
5
+ * process. That server's runtime facts — the port it listens on, the binary it
6
+ * was spawned from, and its PID — live here, in a Tower-managed JSON file under
7
+ * `~/.agent-farm/`, exactly as `cloud-config.json` and `machine-id` do.
8
+ *
9
+ * This is deliberately NOT a user-facing config file and NOT a `global.db`
10
+ * table (Issue #1668 Decisions §D3): a single server is a singleton, so its
11
+ * state belongs beside the other `~/.agent-farm` singletons, written by Tower,
12
+ * never hand-edited. Tower is the sole writer; `afx ide` drives it through the
13
+ * local `/api/ide` endpoint.
14
+ */
15
+ /** The fixed Tower prefix that routes to the IDE server (Issue #1668 §D1). */
16
+ export declare const IDE_DEFAULT_PREFIX = "/ide/";
17
+ /** Default port the local IDE server listens on when `--port` is not given. */
18
+ export declare const IDE_DEFAULT_PORT = 8200;
19
+ /**
20
+ * The single running IDE server's runtime facts. `prefix` is stored for status
21
+ * display but is always the fixed constant; the live PID is authoritative only
22
+ * as a cross-check — port-based discovery (`getProcessesOnPort`) is the source
23
+ * of truth for liveness.
24
+ */
25
+ export interface IdeServerRecord {
26
+ prefix: string;
27
+ port: number;
28
+ serverPath: string;
29
+ pid: number;
30
+ defaultFolder?: string;
31
+ startedAt: string;
32
+ }
33
+ /** Path to `~/.agent-farm/ide-server.json`. */
34
+ export declare function getIdeRecordPath(): string;
35
+ /** Path to the Tower-owned IDE connection-token file. */
36
+ export declare function getIdeConnectionTokenPath(): string;
37
+ /**
38
+ * Read the IDE server record, or null if it does not exist or is unreadable /
39
+ * malformed (fail soft — a corrupt record must never wedge request routing;
40
+ * the caller treats null as "no server registered").
41
+ */
42
+ export declare function readIdeRecord(): IdeServerRecord | null;
43
+ /** Write the IDE server record, creating `~/.agent-farm/` if needed. */
44
+ export declare function writeIdeRecord(record: IdeServerRecord): void;
45
+ /** Delete the IDE server record. No-op if it does not exist. */
46
+ export declare function deleteIdeRecord(): void;
47
+ /**
48
+ * Read the Tower-owned connection token, or null if it does not exist yet.
49
+ *
50
+ * The forward presents this token to the IDE server as a `vscode-tkn` cookie
51
+ * (Issue #1668): the server is spawned with `--connection-token-file`, so it
52
+ * 403s any request that does not carry the token. The token cannot ride the
53
+ * browser's own cookie jar through the relay — the server's `Set-Cookie` is
54
+ * scoped `Path=/ide`, which never matches the external `/t/<tower>/ide/` — so
55
+ * Tower injects it at the forward instead.
56
+ */
57
+ export declare function readIdeConnectionToken(): string | null;
58
+ /**
59
+ * Return the Tower-owned connection token, generating and persisting one (0600)
60
+ * on first use. This token is the IDE server's own inner auth layer (passed via
61
+ * `--connection-token-file`); it is NOT a substitute for Tower's key check on
62
+ * the forward (Issue #1668 §D2).
63
+ */
64
+ export declare function ensureIdeConnectionToken(): string;
65
+ //# sourceMappingURL=ide-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ide-record.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/lib/ide-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAcH,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAE1C,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAKrC;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,yDAAyD;AACzD,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,eAAe,GAAG,IAAI,CAyBtD;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAQ5D;AAED,gEAAgE;AAChE,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAKtD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAajD"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * IDE server runtime record (Issue #1668).
3
+ *
4
+ * Tower forwards a fixed `/ide/` prefix to a single local VS Code server-web
5
+ * process. That server's runtime facts — the port it listens on, the binary it
6
+ * was spawned from, and its PID — live here, in a Tower-managed JSON file under
7
+ * `~/.agent-farm/`, exactly as `cloud-config.json` and `machine-id` do.
8
+ *
9
+ * This is deliberately NOT a user-facing config file and NOT a `global.db`
10
+ * table (Issue #1668 Decisions §D3): a single server is a singleton, so its
11
+ * state belongs beside the other `~/.agent-farm` singletons, written by Tower,
12
+ * never hand-edited. Tower is the sole writer; `afx ide` drives it through the
13
+ * local `/api/ide` endpoint.
14
+ */
15
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, chmodSync, } from 'node:fs';
16
+ import { randomBytes } from 'node:crypto';
17
+ import { resolve } from 'node:path';
18
+ import { AGENT_FARM_DIR } from './tower-client.js';
19
+ /** The fixed Tower prefix that routes to the IDE server (Issue #1668 §D1). */
20
+ export const IDE_DEFAULT_PREFIX = '/ide/';
21
+ /** Default port the local IDE server listens on when `--port` is not given. */
22
+ export const IDE_DEFAULT_PORT = 8200;
23
+ const IDE_RECORD_FILENAME = 'ide-server.json';
24
+ const IDE_CONNECTION_TOKEN_FILENAME = 'ide-connection-token';
25
+ /** Path to `~/.agent-farm/ide-server.json`. */
26
+ export function getIdeRecordPath() {
27
+ return resolve(AGENT_FARM_DIR, IDE_RECORD_FILENAME);
28
+ }
29
+ /** Path to the Tower-owned IDE connection-token file. */
30
+ export function getIdeConnectionTokenPath() {
31
+ return resolve(AGENT_FARM_DIR, IDE_CONNECTION_TOKEN_FILENAME);
32
+ }
33
+ /**
34
+ * Read the IDE server record, or null if it does not exist or is unreadable /
35
+ * malformed (fail soft — a corrupt record must never wedge request routing;
36
+ * the caller treats null as "no server registered").
37
+ */
38
+ export function readIdeRecord() {
39
+ const path = getIdeRecordPath();
40
+ if (!existsSync(path))
41
+ return null;
42
+ try {
43
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
44
+ if (typeof parsed.prefix !== 'string' ||
45
+ typeof parsed.port !== 'number' ||
46
+ typeof parsed.serverPath !== 'string' ||
47
+ typeof parsed.pid !== 'number' ||
48
+ typeof parsed.startedAt !== 'string') {
49
+ return null;
50
+ }
51
+ return {
52
+ prefix: parsed.prefix,
53
+ port: parsed.port,
54
+ serverPath: parsed.serverPath,
55
+ pid: parsed.pid,
56
+ defaultFolder: typeof parsed.defaultFolder === 'string' ? parsed.defaultFolder : undefined,
57
+ startedAt: parsed.startedAt,
58
+ };
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /** Write the IDE server record, creating `~/.agent-farm/` if needed. */
65
+ export function writeIdeRecord(record) {
66
+ if (!existsSync(AGENT_FARM_DIR)) {
67
+ mkdirSync(AGENT_FARM_DIR, { recursive: true, mode: 0o700 });
68
+ }
69
+ const path = getIdeRecordPath();
70
+ writeFileSync(path, JSON.stringify(record, null, 2) + '\n', { mode: 0o600 });
71
+ // `mode` only applies on creation; enforce 0600 even if the file pre-existed.
72
+ chmodSync(path, 0o600);
73
+ }
74
+ /** Delete the IDE server record. No-op if it does not exist. */
75
+ export function deleteIdeRecord() {
76
+ const path = getIdeRecordPath();
77
+ if (existsSync(path))
78
+ unlinkSync(path);
79
+ }
80
+ /**
81
+ * Read the Tower-owned connection token, or null if it does not exist yet.
82
+ *
83
+ * The forward presents this token to the IDE server as a `vscode-tkn` cookie
84
+ * (Issue #1668): the server is spawned with `--connection-token-file`, so it
85
+ * 403s any request that does not carry the token. The token cannot ride the
86
+ * browser's own cookie jar through the relay — the server's `Set-Cookie` is
87
+ * scoped `Path=/ide`, which never matches the external `/t/<tower>/ide/` — so
88
+ * Tower injects it at the forward instead.
89
+ */
90
+ export function readIdeConnectionToken() {
91
+ const path = getIdeConnectionTokenPath();
92
+ if (!existsSync(path))
93
+ return null;
94
+ const token = readFileSync(path, 'utf-8').trim();
95
+ return token.length > 0 ? token : null;
96
+ }
97
+ /**
98
+ * Return the Tower-owned connection token, generating and persisting one (0600)
99
+ * on first use. This token is the IDE server's own inner auth layer (passed via
100
+ * `--connection-token-file`); it is NOT a substitute for Tower's key check on
101
+ * the forward (Issue #1668 §D2).
102
+ */
103
+ export function ensureIdeConnectionToken() {
104
+ const path = getIdeConnectionTokenPath();
105
+ if (existsSync(path)) {
106
+ const existing = readFileSync(path, 'utf-8').trim();
107
+ if (existing.length > 0)
108
+ return existing;
109
+ }
110
+ if (!existsSync(AGENT_FARM_DIR)) {
111
+ mkdirSync(AGENT_FARM_DIR, { recursive: true, mode: 0o700 });
112
+ }
113
+ const token = randomBytes(32).toString('hex');
114
+ writeFileSync(path, token + '\n', { mode: 0o600 });
115
+ chmodSync(path, 0o600); // enforce 0600 even if the file pre-existed
116
+ return token;
117
+ }
118
+ //# sourceMappingURL=ide-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ide-record.js","sourceRoot":"","sources":["../../../src/agent-farm/lib/ide-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,YAAY,EACZ,aAAa,EACb,UAAU,EACV,UAAU,EACV,SAAS,EACT,SAAS,GACV,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAE1C,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAErC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAC9C,MAAM,6BAA6B,GAAG,sBAAsB,CAAC;AAiB7D,+CAA+C;AAC/C,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC;AACtD,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,yBAAyB;IACvC,OAAO,OAAO,CAAC,cAAc,EAAE,6BAA6B,CAAC,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAA6B,CAAC;QACnF,IACE,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;YACjC,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;YAC/B,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ;YACrC,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;YAC9B,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,EACpC,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,aAAa,EAAE,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;YAC1F,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,cAAc,CAAC,MAAuB;IACpD,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QAChC,SAAS,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7E,8EAA8E;IAC9E,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACzB,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,eAAe;IAC7B,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,IAAI,UAAU,CAAC,IAAI,CAAC;QAAE,UAAU,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB;IACpC,MAAM,IAAI,GAAG,yBAAyB,EAAE,CAAC;IACzC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB;IACtC,MAAM,IAAI,GAAG,yBAAyB,EAAE,CAAC;IACzC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;QACpD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,QAAQ,CAAC;IAC3C,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC;QAChC,SAAS,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACnD,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,4CAA4C;IACpE,OAAO,KAAK,CAAC;AACf,CAAC"}