appduct 0.10.0 → 0.11.0

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 (230) hide show
  1. package/README.md +121 -51
  2. package/dist/artifact-inspect.d.ts +0 -1
  3. package/dist/bin.d.ts +0 -1
  4. package/dist/bin.js +1250 -2
  5. package/dist/cli/command-options.d.ts +8 -1
  6. package/dist/cli/create-cli.d.ts +0 -1
  7. package/dist/cli/dispatch.d.ts +10 -1
  8. package/dist/cli/envelope.d.ts +19 -0
  9. package/dist/cli/global-flags.d.ts +30 -0
  10. package/dist/cli/open-target.d.ts +25 -8
  11. package/dist/cli/result-types.d.ts +30 -5
  12. package/dist/cli/router.d.ts +78 -0
  13. package/dist/cli/routes/daemon/index.d.ts +9 -0
  14. package/dist/cli/routes/daemon/index.js +848 -0
  15. package/dist/cli/routes/daemon/run.d.ts +3 -0
  16. package/dist/cli/routes/daemon/run.js +3246 -0
  17. package/dist/cli/routes/daemon/start.d.ts +3 -0
  18. package/dist/cli/routes/daemon/start.js +1668 -0
  19. package/dist/cli/routes/daemon/status.d.ts +4 -0
  20. package/dist/cli/routes/daemon/status.js +1676 -0
  21. package/dist/cli/routes/daemon/stop.d.ts +4 -0
  22. package/dist/cli/routes/daemon/stop.js +1646 -0
  23. package/dist/cli/routes/doctor.d.ts +3 -0
  24. package/dist/cli/routes/doctor.js +1075 -0
  25. package/dist/cli/routes/events.d.ts +3 -0
  26. package/dist/cli/routes/events.js +1906 -0
  27. package/dist/cli/routes/init.d.ts +3 -0
  28. package/dist/cli/routes/init.js +1486 -0
  29. package/dist/cli/routes/invoke.d.ts +3 -0
  30. package/dist/cli/routes/invoke.js +1884 -0
  31. package/dist/cli/routes/keygen.d.ts +3 -0
  32. package/dist/cli/routes/keygen.js +928 -0
  33. package/dist/cli/routes/link.d.ts +3 -0
  34. package/dist/cli/routes/link.js +2705 -0
  35. package/dist/cli/routes/ls.d.ts +3 -0
  36. package/dist/cli/routes/ls.js +1659 -0
  37. package/dist/cli/routes/mcp.d.ts +5 -0
  38. package/dist/cli/routes/mcp.js +3917 -0
  39. package/dist/cli/routes/revoke.d.ts +3 -0
  40. package/dist/cli/routes/revoke.js +1675 -0
  41. package/dist/cli/routes/tools.d.ts +3 -0
  42. package/dist/cli/routes/tools.js +1851 -0
  43. package/dist/cli/runner.d.ts +3 -11
  44. package/dist/cli/types.d.ts +9 -6
  45. package/dist/cli/version-guard.d.ts +20 -0
  46. package/dist/cli.d.ts +0 -1
  47. package/dist/client/app-client.d.ts +0 -1
  48. package/dist/client/bootstrap.d.ts +4 -4
  49. package/dist/client/connect.d.ts +0 -1
  50. package/dist/client/errors.d.ts +0 -1
  51. package/dist/client/index.d.ts +0 -1
  52. package/dist/client/index.js +2379 -16
  53. package/dist/commands/daemon/context.d.ts +14 -0
  54. package/dist/commands/daemon/run.d.ts +11 -0
  55. package/dist/commands/daemon/start.d.ts +5 -0
  56. package/dist/commands/daemon/status.d.ts +5 -0
  57. package/dist/commands/daemon/stop.d.ts +5 -0
  58. package/dist/commands/doctor.d.ts +0 -1
  59. package/dist/commands/events.d.ts +0 -1
  60. package/dist/commands/init.d.ts +17 -4
  61. package/dist/commands/invoke.d.ts +0 -1
  62. package/dist/commands/keygen.d.ts +0 -1
  63. package/dist/commands/link.d.ts +1 -2
  64. package/dist/commands/ls.d.ts +0 -1
  65. package/dist/commands/mcp.d.ts +2 -4
  66. package/dist/commands/revoke.d.ts +0 -1
  67. package/dist/commands/tools.d.ts +19 -3
  68. package/dist/daemon/address.d.ts +0 -1
  69. package/dist/daemon/audit.d.ts +6 -11
  70. package/dist/daemon/calls.d.ts +0 -1
  71. package/dist/daemon/config.d.ts +13 -16
  72. package/dist/daemon/daemon.d.ts +0 -1
  73. package/dist/daemon/event-bus.d.ts +0 -1
  74. package/dist/daemon/links.d.ts +0 -1
  75. package/dist/daemon/listener.d.ts +0 -1
  76. package/dist/daemon/log-rotation.d.ts +0 -1
  77. package/dist/daemon/pidfile.d.ts +14 -8
  78. package/dist/daemon/policy.d.ts +0 -1
  79. package/dist/daemon/registry.d.ts +0 -1
  80. package/dist/daemon/rpc-errors.d.ts +16 -0
  81. package/dist/daemon/rpc-server.d.ts +0 -11
  82. package/dist/daemon/sessions.d.ts +0 -1
  83. package/dist/daemon/socket-probe.d.ts +0 -1
  84. package/dist/daemon/state-dir.d.ts +0 -1
  85. package/dist/daemon/timers.d.ts +0 -1
  86. package/dist/daemon/tls.d.ts +0 -1
  87. package/dist/errors.d.ts +0 -1
  88. package/dist/host-certificate.d.ts +0 -1
  89. package/dist/index.d.ts +0 -1
  90. package/dist/index.js +1307 -3
  91. package/dist/key-material.d.ts +0 -1
  92. package/dist/link.d.ts +8 -7
  93. package/dist/mcp/app-tools.d.ts +153 -0
  94. package/dist/mcp/connect-tool.d.ts +14 -8
  95. package/dist/mcp/daemon-tools.d.ts +2 -11
  96. package/dist/mcp/events-tool.d.ts +0 -1
  97. package/dist/mcp/server.d.ts +33 -14
  98. package/dist/native-scheme.d.ts +0 -1
  99. package/dist/output.d.ts +8 -11
  100. package/dist/package-root.d.ts +11 -0
  101. package/dist/package-version.d.ts +0 -1
  102. package/dist/qr-terminal.d.ts +0 -1
  103. package/dist/rpc/client.d.ts +0 -1
  104. package/dist/scheme.d.ts +60 -2
  105. package/dist/spki-pin.d.ts +0 -1
  106. package/package.json +4 -3
  107. package/dist/artifact-inspect.d.ts.map +0 -1
  108. package/dist/artifact-inspect.js +0 -334
  109. package/dist/bin.d.ts.map +0 -1
  110. package/dist/cli/command-options.d.ts.map +0 -1
  111. package/dist/cli/command-options.js +0 -95
  112. package/dist/cli/create-cli.d.ts.map +0 -1
  113. package/dist/cli/create-cli.js +0 -56
  114. package/dist/cli/dispatch.d.ts.map +0 -1
  115. package/dist/cli/dispatch.js +0 -275
  116. package/dist/cli/open-target.d.ts.map +0 -1
  117. package/dist/cli/open-target.js +0 -438
  118. package/dist/cli/result-types.d.ts.map +0 -1
  119. package/dist/cli/result-types.js +0 -6
  120. package/dist/cli/runner.d.ts.map +0 -1
  121. package/dist/cli/runner.js +0 -111
  122. package/dist/cli/types.d.ts.map +0 -1
  123. package/dist/cli/types.js +0 -10
  124. package/dist/cli.d.ts.map +0 -1
  125. package/dist/cli.js +0 -2
  126. package/dist/client/app-client.d.ts.map +0 -1
  127. package/dist/client/app-client.js +0 -223
  128. package/dist/client/bootstrap.d.ts.map +0 -1
  129. package/dist/client/bootstrap.js +0 -108
  130. package/dist/client/connect.d.ts.map +0 -1
  131. package/dist/client/connect.js +0 -38
  132. package/dist/client/errors.d.ts.map +0 -1
  133. package/dist/client/errors.js +0 -40
  134. package/dist/client/index.d.ts.map +0 -1
  135. package/dist/commands/daemon.d.ts +0 -22
  136. package/dist/commands/daemon.d.ts.map +0 -1
  137. package/dist/commands/daemon.js +0 -128
  138. package/dist/commands/doctor.d.ts.map +0 -1
  139. package/dist/commands/doctor.js +0 -42
  140. package/dist/commands/events.d.ts.map +0 -1
  141. package/dist/commands/events.js +0 -76
  142. package/dist/commands/init.d.ts.map +0 -1
  143. package/dist/commands/init.js +0 -231
  144. package/dist/commands/invoke.d.ts.map +0 -1
  145. package/dist/commands/invoke.js +0 -54
  146. package/dist/commands/keygen.d.ts.map +0 -1
  147. package/dist/commands/keygen.js +0 -46
  148. package/dist/commands/link.d.ts.map +0 -1
  149. package/dist/commands/link.js +0 -43
  150. package/dist/commands/ls.d.ts.map +0 -1
  151. package/dist/commands/ls.js +0 -7
  152. package/dist/commands/mcp.d.ts.map +0 -1
  153. package/dist/commands/mcp.js +0 -98
  154. package/dist/commands/revoke.d.ts.map +0 -1
  155. package/dist/commands/revoke.js +0 -7
  156. package/dist/commands/tools.d.ts.map +0 -1
  157. package/dist/commands/tools.js +0 -52
  158. package/dist/daemon/address.d.ts.map +0 -1
  159. package/dist/daemon/address.js +0 -71
  160. package/dist/daemon/audit.d.ts.map +0 -1
  161. package/dist/daemon/audit.js +0 -237
  162. package/dist/daemon/calls.d.ts.map +0 -1
  163. package/dist/daemon/calls.js +0 -158
  164. package/dist/daemon/config.d.ts.map +0 -1
  165. package/dist/daemon/config.js +0 -170
  166. package/dist/daemon/daemon.d.ts.map +0 -1
  167. package/dist/daemon/daemon.js +0 -560
  168. package/dist/daemon/event-bus.d.ts.map +0 -1
  169. package/dist/daemon/event-bus.js +0 -111
  170. package/dist/daemon/links.d.ts.map +0 -1
  171. package/dist/daemon/links.js +0 -118
  172. package/dist/daemon/listener.d.ts.map +0 -1
  173. package/dist/daemon/listener.js +0 -132
  174. package/dist/daemon/log-rotation.d.ts.map +0 -1
  175. package/dist/daemon/log-rotation.js +0 -136
  176. package/dist/daemon/pidfile.d.ts.map +0 -1
  177. package/dist/daemon/pidfile.js +0 -90
  178. package/dist/daemon/policy.d.ts.map +0 -1
  179. package/dist/daemon/policy.js +0 -24
  180. package/dist/daemon/registry.d.ts.map +0 -1
  181. package/dist/daemon/registry.js +0 -31
  182. package/dist/daemon/rpc-server.d.ts.map +0 -1
  183. package/dist/daemon/rpc-server.js +0 -203
  184. package/dist/daemon/sessions.d.ts.map +0 -1
  185. package/dist/daemon/sessions.js +0 -429
  186. package/dist/daemon/socket-probe.d.ts.map +0 -1
  187. package/dist/daemon/socket-probe.js +0 -28
  188. package/dist/daemon/state-dir.d.ts.map +0 -1
  189. package/dist/daemon/state-dir.js +0 -43
  190. package/dist/daemon/timers.d.ts.map +0 -1
  191. package/dist/daemon/timers.js +0 -12
  192. package/dist/daemon/tls.d.ts.map +0 -1
  193. package/dist/daemon/tls.js +0 -79
  194. package/dist/errors.d.ts.map +0 -1
  195. package/dist/errors.js +0 -162
  196. package/dist/host-certificate.d.ts.map +0 -1
  197. package/dist/host-certificate.js +0 -111
  198. package/dist/index.d.ts.map +0 -1
  199. package/dist/key-material.d.ts.map +0 -1
  200. package/dist/key-material.js +0 -42
  201. package/dist/link.d.ts.map +0 -1
  202. package/dist/link.js +0 -117
  203. package/dist/mcp/connect-tool.d.ts.map +0 -1
  204. package/dist/mcp/connect-tool.js +0 -396
  205. package/dist/mcp/daemon-tools.d.ts.map +0 -1
  206. package/dist/mcp/daemon-tools.js +0 -29
  207. package/dist/mcp/events-tool.d.ts.map +0 -1
  208. package/dist/mcp/events-tool.js +0 -322
  209. package/dist/mcp/server.d.ts.map +0 -1
  210. package/dist/mcp/server.js +0 -538
  211. package/dist/mcp/tool-mapping.d.ts +0 -58
  212. package/dist/mcp/tool-mapping.d.ts.map +0 -1
  213. package/dist/mcp/tool-mapping.js +0 -143
  214. package/dist/mcp/tool-namespace.d.ts +0 -40
  215. package/dist/mcp/tool-namespace.d.ts.map +0 -1
  216. package/dist/mcp/tool-namespace.js +0 -65
  217. package/dist/native-scheme.d.ts.map +0 -1
  218. package/dist/native-scheme.js +0 -465
  219. package/dist/output.d.ts.map +0 -1
  220. package/dist/output.js +0 -342
  221. package/dist/package-version.d.ts.map +0 -1
  222. package/dist/package-version.js +0 -47
  223. package/dist/qr-terminal.d.ts.map +0 -1
  224. package/dist/qr-terminal.js +0 -45
  225. package/dist/rpc/client.d.ts.map +0 -1
  226. package/dist/rpc/client.js +0 -819
  227. package/dist/scheme.d.ts.map +0 -1
  228. package/dist/scheme.js +0 -321
  229. package/dist/spki-pin.d.ts.map +0 -1
  230. package/dist/spki-pin.js +0 -14
@@ -1,111 +0,0 @@
1
- /**
2
- * Typed in-process event bus (ARCHITECTURE.md §5 event kinds). The RPC layer (for
3
- * `events.subscribe`/`events.since`) and audit subscribe here; this module owns fan-out plus a
4
- * per-session retention ring buffer, and must never let a throwing subscriber take down the daemon
5
- * or another subscriber.
6
- *
7
- * Retention (issue #6): every session-scoped event (`sessionId` set), except the high-frequency
8
- * kinds in `EXCLUDED_FROM_RETENTION`, is appended to a per-session ring buffer with a
9
- * monotonically increasing `seq`, capped at `bufferSize` entries (oldest dropped first). A
10
- * session's buffer is discarded the moment it emits a terminal event (`session_expired` /
11
- * `session_revoked`) — "terminal states free the alias" (sessions.ts) applies to retained events
12
- * too, matching "no persisted history" (ARCHITECTURE.md §3/§13) — or via an explicit `drop()` for
13
- * the pending-link discard paths that never reach one of those event kinds. Daemon-wide events (no
14
- * `sessionId`, e.g. `daemon_started`) are fanned out but never buffered.
15
- */
16
- /** Terminal event kinds whose arrival for a session discards that session's retained buffer. */
17
- const TERMINAL_EVENT_KINDS = new Set(["session_expired", "session_revoked"]);
18
- /** Fanned out live like any other event, but never appended to the retention buffer: a single
19
- * chatty `tools.call` can emit far more of these than `bufferSize` allows, which would otherwise
20
- * evict every retained `app_event` for the session — defeating the reason the buffer exists.
21
- * `tool_call_started`/`tool_call_finished` (one pair per call, not per progress tick) are still
22
- * retained. */
23
- const EXCLUDED_FROM_RETENTION = new Set(["tool_call_progress"]);
24
- export const createEventBus = (options = {}) => {
25
- const clock = options.clock ?? { now: () => new Date() };
26
- const bufferSize = options.bufferSize ?? 256;
27
- const listeners = new Set();
28
- const buffers = new Map();
29
- const cursors = new Map();
30
- const appendToBuffer = (sessionId, notification) => {
31
- const buffer = buffers.get(sessionId) ?? [];
32
- buffer.push(notification);
33
- if (buffer.length > bufferSize) {
34
- buffer.splice(0, buffer.length - bufferSize);
35
- }
36
- buffers.set(sessionId, buffer);
37
- };
38
- return {
39
- emit: (event) => {
40
- const sessionId = event.sessionId;
41
- const seq = sessionId !== undefined ? (cursors.get(sessionId) ?? 0) + 1 : 0;
42
- if (sessionId !== undefined) {
43
- cursors.set(sessionId, seq);
44
- }
45
- const notification = {
46
- ...event,
47
- ts: event.ts ?? clock.now().getTime(),
48
- seq,
49
- };
50
- if (sessionId !== undefined) {
51
- if (TERMINAL_EVENT_KINDS.has(notification.kind)) {
52
- // The terminal event itself is still delivered live (fan-out below) and reported through
53
- // `since`'s `cursor`, but is not retained — nothing can legitimately ask for it again once
54
- // the session is gone.
55
- buffers.delete(sessionId);
56
- cursors.delete(sessionId);
57
- }
58
- else if (!EXCLUDED_FROM_RETENTION.has(notification.kind)) {
59
- appendToBuffer(sessionId, notification);
60
- }
61
- }
62
- for (const listener of listeners) {
63
- try {
64
- listener(notification);
65
- }
66
- catch {
67
- // A misbehaving subscriber must never crash the daemon or block other subscribers.
68
- }
69
- }
70
- },
71
- subscribe: (listener) => {
72
- listeners.add(listener);
73
- return () => {
74
- listeners.delete(listener);
75
- };
76
- },
77
- since: (sessionId, query = {}) => {
78
- const buffer = buffers.get(sessionId) ?? [];
79
- const sessionCursor = cursors.get(sessionId) ?? 0;
80
- // Never hand back the live buffer array itself — `emit()` keeps pushing into it after this
81
- // call returns, and a caller that gets `events` by reference (the common case: no filter
82
- // narrows it below) would see it mutate underneath it.
83
- let events = buffer.slice();
84
- if (query.since !== undefined) {
85
- events = events.filter((event) => event.seq > query.since);
86
- }
87
- if (query.kinds !== undefined) {
88
- const kinds = new Set(query.kinds);
89
- events = events.filter((event) => kinds.has(event.kind));
90
- }
91
- if (query.limit !== undefined && events.length > query.limit) {
92
- // Keep the OLDEST N, not the newest: `limit` exists to bound one response, not to skip
93
- // ahead — a caller pages forward by re-calling with `since` set to the response's own
94
- // `cursor`. Keeping the newest N instead would make that same re-call return the same
95
- // window forever, silently and unrecoverably losing whatever `limit` cut off.
96
- events = events.slice(0, query.limit);
97
- }
98
- // The cursor a caller should resume from: the last event actually returned, so paging
99
- // through a `limit`-truncated response with `since: cursor` always advances. Only when
100
- // nothing was returned (empty buffer, or every retained event was filtered out) does it fall
101
- // back to the session's true high-water mark, so an empty page still lets a caller skip past
102
- // events it explicitly filtered out (by `kinds`) rather than re-fetching them forever.
103
- const cursor = events.length > 0 ? events[events.length - 1].seq : sessionCursor;
104
- return { events, cursor };
105
- },
106
- drop: (sessionId) => {
107
- buffers.delete(sessionId);
108
- cursors.delete(sessionId);
109
- },
110
- };
111
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/daemon/links.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAuC,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE1F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAe,MAAM,aAAa,CAAC;AAKzD,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,aAAa,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,0FAA0F;IAC1F,WAAW,EAAE,MAAM,aAAa,CAAC;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC;;sCAEkC;IAClC,MAAM,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,KAAK,WAAW,CAAC;IACtE,2GAA2G;IAC3G,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,WAAW,GAAG,SAAS,CAAC;IACpD,+EAA+E;IAC/E,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,WAAW,GAAG,SAAS,CAAC;IACxD,wEAAwE;IACxE,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnD,yFAAyF;IACzF,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;;gCAI4B;IAC5B,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAoBF,eAAO,MAAM,yBAAyB,GAAI,SAAS,0BAA0B,KAAG,mBAkH/E,CAAC"}
@@ -1,118 +0,0 @@
1
- /**
2
- * Pending-link minting (ARCHITECTURE.md §6 PENDING state, §8 bootstrap payload). A link is a
3
- * short-lived, single-use claim ticket: `sessionId` + `token`, encoded into the deep-link
4
- * bootstrap payload and backed by a TTL timer that discards it if nobody claims it in time.
5
- * Pending links are cheap and re-issuable — they are never persisted and never appear in
6
- * `sessions.list`/`sessions.describe` (only claimed sessions do; see sessions.ts).
7
- */
8
- import { randomBytes } from "node:crypto";
9
- import { encodeBootstrap } from "@appduct/shared";
10
- const SESSION_ID_BYTES = 12;
11
- const TOKEN_BYTES = 32;
12
- const generateSessionId = () => {
13
- // Re-rolled if it starts with "-": v1 ids starting with "-" broke CLI flag parsing
14
- // (e.g. `appduct tools -abc123...` was parsed as a flag, not a selector).
15
- let candidate = randomBytes(SESSION_ID_BYTES).toString("base64url");
16
- while (candidate.startsWith("-")) {
17
- candidate = randomBytes(SESSION_ID_BYTES).toString("base64url");
18
- }
19
- return candidate;
20
- };
21
- export const createPendingLinkRegistry = (options) => {
22
- const records = new Map();
23
- const discard = (sessionId) => {
24
- const record = records.get(sessionId);
25
- if (!record) {
26
- return;
27
- }
28
- options.timers.clearTimeout(record.timer);
29
- records.delete(sessionId);
30
- // Covers every discard path uniformly, including the ones with no dedicated event kind
31
- // (attempt-limit exceeded, the TTL free-timer) — `link_expired`/`session_revoked` also reach
32
- // here for the paths that do emit one, so this is a harmless no-op in those cases (the
33
- // corresponding event already dropped the buffer, if `event-bus.ts` treats that kind as
34
- // terminal) or the only thing that does (issue #6: an unclaimed link must not retain events
35
- // forever).
36
- options.eventBus.drop(sessionId);
37
- };
38
- return {
39
- create: (ttlSeconds, addressOverride) => {
40
- const sessionId = generateSessionId();
41
- const token = randomBytes(TOKEN_BYTES);
42
- const createdAt = options.clock.now();
43
- const expiresAt = new Date(createdAt.getTime() + ttlSeconds * 1000);
44
- const detectedEndpoint = options.getEndpoint();
45
- // Overriding only ever changes the address (and its family); the port stays the daemon's
46
- // real wss port, since the listener already binds all interfaces (ARCHITECTURE.md §8).
47
- const endpoint = addressOverride
48
- ? {
49
- family: (addressOverride.includes(":") ? 6 : 4),
50
- address: addressOverride,
51
- port: detectedEndpoint.port,
52
- }
53
- : detectedEndpoint;
54
- // Deliberately does not delete the record the instant the TTL fires: a claim attempt arriving
55
- // just after must still see the (now-expired) link and be told "expired" rather than
56
- // "unknown" — the distinction the rejection matrix requires. `handleClaim`'s own `expiresAt`
57
- // check discards the record (and reports `link_expired`) the moment anything touches it.
58
- // But the record must not live forever if nobody ever touches it again: a second timer frees
59
- // it after an equal further grace window (late claims within that window still see
60
- // `link_expired`; a long-lived daemon with many unclaimed links does not leak memory
61
- // unboundedly). `disposeAll` clears whichever timer (TTL or free) is still outstanding on
62
- // daemon shutdown.
63
- const timer = options.timers.setTimeout(() => {
64
- // `link_expired` is its own event kind, distinct from `session_expired` (reserved for the
65
- // SUSPENDED -> EXPIRED grace-window transition of a *claimed* session — ARCHITECTURE.md §6).
66
- options.eventBus.emit({ kind: "link_expired", sessionId, data: { reason: "link_ttl_expired" } });
67
- const record = records.get(sessionId);
68
- if (record) {
69
- record.timer = options.timers.setTimeout(() => discard(sessionId), ttlSeconds * 1000);
70
- }
71
- }, ttlSeconds * 1000);
72
- const link = { sessionId, token, createdAt, expiresAt };
73
- records.set(sessionId, { link, attempts: 0, timer });
74
- const deepLinkPayload = encodeBootstrap({
75
- ...endpoint,
76
- sessionId,
77
- token: token.toString("base64url"),
78
- expiresAt: Math.floor(expiresAt.getTime() / 1000),
79
- });
80
- options.eventBus.emit({ kind: "link_created", sessionId, data: { expiresAt: link.expiresAt.toISOString() } });
81
- return { link, deepLinkPayload, endpoint };
82
- },
83
- get: (sessionId) => records.get(sessionId)?.link,
84
- consume: (sessionId) => {
85
- const record = records.get(sessionId);
86
- if (!record) {
87
- return undefined;
88
- }
89
- options.timers.clearTimeout(record.timer);
90
- records.delete(sessionId);
91
- return record.link;
92
- },
93
- recordFailedAttempt: (sessionId) => {
94
- const record = records.get(sessionId);
95
- if (!record) {
96
- return 0;
97
- }
98
- record.attempts += 1;
99
- return record.attempts;
100
- },
101
- discard,
102
- claimableCount: () => {
103
- const now = options.clock.now().getTime();
104
- let count = 0;
105
- for (const record of records.values()) {
106
- if (record.link.expiresAt.getTime() > now) {
107
- count += 1;
108
- }
109
- }
110
- return count;
111
- },
112
- disposeAll: () => {
113
- for (const sessionId of Array.from(records.keys())) {
114
- discard(sessionId);
115
- }
116
- },
117
- };
118
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../../src/daemon/listener.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAqC,KAAK,MAAM,IAAI,WAAW,EAAE,MAAM,YAAY,CAAC;AAG3F,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAErC,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAK1D,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,UAAU,CAAC;IAChB,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,QAAQ,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,WAAW,CAAC;IACzB,GAAG,EAAE,eAAe,CAAC;IACrB,IAAI,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAC/B;;6FAEyF;IACzF,QAAQ,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,QAAQ,CAAC,KAAK,IAAI,CAAC;IACtE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAMF,eAAO,MAAM,aAAa,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,cAAc,CA4IpF,CAAC"}
@@ -1,132 +0,0 @@
1
- /**
2
- * The pinned-TLS WebSocket listener (ARCHITECTURE.md §7). A thin frame-level gate in front of
3
- * `SessionManager`: JSON/binary/size hygiene, the pre-claim timeout, and post-claim
4
- * type/session-id routing. All state-machine decisions (claim/resume/registry/close-code choice)
5
- * live in sessions.ts — this module never inspects session state itself.
6
- *
7
- * Every socket gets an `error` listener the instant it is accepted, and the server itself gets one
8
- * too: v1's fatal defect was an unhandled `'error'` event crashing the whole daemon. A socket
9
- * closing — for any reason — must never stop the listener or any other session.
10
- */
11
- import { createServer as createHttpsServer } from "node:https";
12
- import { isSessionClaimMessage, isSessionResumeMessage, isSessionBoundMessage } from "@appduct/shared";
13
- import { WebSocketServer } from "ws";
14
- import { isPostClaimMessageType } from "./sessions.js";
15
- import { systemTimers } from "./timers.js";
16
- const DEFAULT_PRE_CLAIM_TIMEOUT_MS = 10_000;
17
- const MAX_PAYLOAD_BYTES = 256 * 1024;
18
- const isRecord = (value) => {
19
- return typeof value === "object" && value !== null && !Array.isArray(value);
20
- };
21
- export const startListener = async (options) => {
22
- const timers = options.timers ?? systemTimers;
23
- const preClaimTimeoutMs = options.preClaimTimeoutMs ?? DEFAULT_PRE_CLAIM_TIMEOUT_MS;
24
- const material = options.tls.current();
25
- const httpsServer = createHttpsServer({ key: material.keyPem, cert: material.certPem });
26
- httpsServer.on("error", () => {
27
- // A listener-level error (e.g. a transient accept failure) must never crash the daemon.
28
- // Startup failures are still surfaced via the one-shot listener below.
29
- });
30
- const wss = new WebSocketServer({ server: httpsServer, maxPayload: MAX_PAYLOAD_BYTES });
31
- wss.on("error", () => {
32
- // Same contract as the https server: never let a listener-level error take the daemon down.
33
- });
34
- wss.on("connection", (socket) => {
35
- socket.on("error", () => {
36
- // The 'close' event still fires after 'error' and drives session suspend/cleanup.
37
- });
38
- let claimedSessionId = null;
39
- const preClaimTimer = timers.setTimeout(() => {
40
- if (!claimedSessionId) {
41
- socket.close(1008, "pre_claim_timeout");
42
- }
43
- }, preClaimTimeoutMs);
44
- socket.once("close", () => {
45
- timers.clearTimeout(preClaimTimer);
46
- });
47
- socket.on("message", (data, isBinary) => {
48
- if (isBinary) {
49
- socket.close(1003, "binary_frame_not_supported");
50
- return;
51
- }
52
- let parsed;
53
- try {
54
- parsed = JSON.parse(data.toString("utf8"));
55
- }
56
- catch {
57
- socket.close(1008, "invalid_json");
58
- return;
59
- }
60
- if (!isRecord(parsed)) {
61
- socket.close(1008, "invalid_json");
62
- return;
63
- }
64
- if (claimedSessionId === null) {
65
- if (isSessionClaimMessage(parsed)) {
66
- const result = options.sessionManager.handleClaim(socket, parsed);
67
- if (result) {
68
- claimedSessionId = result;
69
- timers.clearTimeout(preClaimTimer);
70
- }
71
- return;
72
- }
73
- if (isSessionResumeMessage(parsed)) {
74
- const result = options.sessionManager.handleResume(socket, parsed);
75
- if (result) {
76
- claimedSessionId = result;
77
- timers.clearTimeout(preClaimTimer);
78
- }
79
- return;
80
- }
81
- socket.close(1008, "expected_claim_or_resume");
82
- return;
83
- }
84
- if (!isPostClaimMessageType(parsed.type)) {
85
- socket.close(1008, "unknown_message_type");
86
- return;
87
- }
88
- if (!isSessionBoundMessage(parsed) || parsed.session_id !== claimedSessionId) {
89
- socket.close(1008, "session_mismatch");
90
- return;
91
- }
92
- options.sessionManager.handlePostClaimMessage(claimedSessionId, socket, parsed);
93
- });
94
- });
95
- await new Promise((resolve, reject) => {
96
- const onError = (error) => {
97
- httpsServer.off("listening", onListening);
98
- reject(error);
99
- };
100
- const onListening = () => {
101
- httpsServer.off("error", onError);
102
- resolve();
103
- };
104
- httpsServer.once("error", onError);
105
- httpsServer.once("listening", onListening);
106
- httpsServer.listen(options.port);
107
- });
108
- return {
109
- httpsServer,
110
- wss,
111
- port: () => {
112
- const address = httpsServer.address();
113
- return address && typeof address !== "string" ? address.port : undefined;
114
- },
115
- applyTls: (nextMaterial) => {
116
- // Keep this guard for compatibility with runtimes whose `node:https` shim omits the method.
117
- const server = httpsServer;
118
- server.setSecureContext?.({ key: nextMaterial.keyPem, cert: nextMaterial.certPem });
119
- },
120
- close: () => new Promise((resolve) => {
121
- for (const client of wss.clients) {
122
- client.terminate();
123
- }
124
- wss.close(() => {
125
- // `server.close()` alone only stops accepting new connections — it waits for existing
126
- // (including idle keep-alive) sockets to end, which could hang shutdown indefinitely.
127
- httpsServer.closeAllConnections();
128
- httpsServer.close(() => resolve());
129
- });
130
- }),
131
- };
132
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"log-rotation.d.ts","sourceRoot":"","sources":["../../src/daemon/log-rotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAYH,eAAO,MAAM,mBAAmB,GAAI,aAAa,MAAM,KAAG,MAA4B,CAAC;AAEvF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB;;mBAEe;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;2CAEuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iGAAiG;IACjG,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B,CAAC;AAcF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,sBAAsB,KAC9B,OAAO,CAAC,qBAAqB,CAsD/B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,aAAa,MAAM,EACnB,OAAM,CAAC,OAAO,EAAE,MAAM,KAAK,IAAwD,KAClF,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAM/E,CAAC"}
@@ -1,136 +0,0 @@
1
- /**
2
- * `daemon.log` rotation (ARCHITECTURE.md §3/§4, issue #32). The daemon's stdout/stderr is appended
3
- * to `daemon.log` for the whole life of the process, so nothing inside a running daemon can safely
4
- * truncate it — the only safe moment is just before a *new* daemon is spawned, while no writer
5
- * holds the file open. {@link rotateDaemonLogIfNeeded} is therefore called from the spawn path
6
- * (`rpc/client.ts`'s `defaultSpawn`, which is also what `appduct daemon start` spawns through),
7
- * never from the daemon itself.
8
- *
9
- * "Nothing answered the socket" is not by itself proof that nothing is writing the log: a daemon
10
- * that is still booting, wedged, or mid-shutdown holds its log fd while its socket is unreachable,
11
- * and the spawn lock is released as soon as the child is spawned rather than when it is ready. A
12
- * rename under such a process would send its output to `daemon.log.1`, and the *next* rotation
13
- * would then unlink the inode it is still writing to — output lost, with nothing on disk to show
14
- * for it. So rotation asks twice, as late as it can, and declines on either answer:
15
- *
16
- * - the pidfile must not name a live process (the same probe the pidfile takeover and the
17
- * stale-socket unlink use); and
18
- * - `daemon.sock` must not accept a connection, re-checked immediately before the rename.
19
- *
20
- * **This narrows the window; it does not close it.** A daemon spawned microseconds ago holds its
21
- * log fd before it has written `daemon.pid` or bound its socket, and no check performed before the
22
- * rename can see it — the file it is writing to is simply not distinguishable, from the outside,
23
- * from one nobody holds. What is left is a race measured in the milliseconds between a child being
24
- * spawned and it writing its pidfile, entered only by a *second* spawner that also finds the log
25
- * over its cap. The consequence is bounded (that daemon's output continues into `daemon.log.1`,
26
- * and is lost if a later rotation replaces that backup), and a just-spawned daemon's log is far
27
- * below any sane cap, so in practice the second spawner does not rotate at all. Closing it
28
- * properly needs an advisory lock held across spawn-and-ready rather than released at spawn.
29
- *
30
- * One backup is enough (`daemon.log.1`): the point is a bounded footprint, not a full history. A
31
- * rotation failure is warned and swallowed — a daemon that cannot rotate its log must still start.
32
- */
33
- import { chmod, rename, stat } from "node:fs/promises";
34
- import { DEFAULT_DAEMON_LOG_MAX_BYTES, loadConfig } from "./config.js";
35
- import { isProcessAlive, readPidFromFile } from "./pidfile.js";
36
- import { isSocketConnectable } from "./socket-probe.js";
37
- import { getStateDirPaths } from "./state-dir.js";
38
- /** Mode every file under the state dir is held at (ARCHITECTURE.md §3). */
39
- const STATE_FILE_MODE = 0o600;
40
- export const daemonLogBackupPath = (logFilePath) => `${logFilePath}.1`;
41
- /** True when `pidFilePath` names a process that is still around. A missing/garbage pidfile, or an
42
- * unreadable one, reads as "nothing is running" — the same conclusion the pidfile takeover and the
43
- * stale-socket unlink draw from it. */
44
- const isDaemonHoldingLog = async (pidFilePath) => {
45
- try {
46
- const pid = await readPidFromFile(pidFilePath);
47
- return pid !== undefined && isProcessAlive(pid);
48
- }
49
- catch {
50
- return false;
51
- }
52
- };
53
- /**
54
- * Moves `daemon.log` to `daemon.log.1` when it exceeds `maxBytes`, replacing any previous backup.
55
- * A missing log, a log within the cap, a log a live daemon still holds, or any failure is a no-op
56
- * — never throws.
57
- */
58
- export const rotateDaemonLogIfNeeded = async (options) => {
59
- const warn = options.warn ??
60
- ((message) => {
61
- process.stderr.write(`${message}\n`);
62
- });
63
- try {
64
- const stats = await stat(options.logFilePath);
65
- if (!stats.isFile() || stats.size <= options.maxBytes) {
66
- return { rotated: false };
67
- }
68
- // Both checks run only once rotation is actually on the table, so the overwhelmingly common
69
- // within-cap spawn stays two syscalls — and as late as possible, to keep the window in which
70
- // a daemon can appear between the check and the rename as small as it can be made.
71
- const held = (options.pidFilePath !== undefined && (await isDaemonHoldingLog(options.pidFilePath))) ||
72
- (options.socketPath !== undefined && (await isSocketConnectable(options.socketPath)));
73
- if (held) {
74
- warn(`appduct: "${options.logFilePath}" is over its size cap but a daemon still holds it; leaving it in place.`);
75
- return { rotated: false, skipped: "daemon_running" };
76
- }
77
- const backupPath = daemonLogBackupPath(options.logFilePath);
78
- // `rename` is atomic within the state dir, so a concurrent spawner either sees the old log or
79
- // the rotated one — never a half-copied file.
80
- await rename(options.logFilePath, backupPath);
81
- // Deliberately its own try/catch, *after* the rename has already succeeded. `rename` preserves
82
- // the log's mode, so this only ever repairs a backup that predates this code (or an operator's
83
- // `cp`) — and a filesystem that refuses the chmod must not make a completed rotation report
84
- // itself as "not rotated" under a warning naming the wrong step.
85
- try {
86
- await chmod(backupPath, STATE_FILE_MODE);
87
- }
88
- catch (error) {
89
- warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
90
- }
91
- return { rotated: true, rotatedBytes: stats.size };
92
- }
93
- catch (error) {
94
- if (error.code === "ENOENT") {
95
- return { rotated: false };
96
- }
97
- warn(`appduct: failed to rotate "${options.logFilePath}": ${error.message}`);
98
- return { rotated: false };
99
- }
100
- };
101
- /**
102
- * Best-effort enforcement of `daemon.log`'s ARCHITECTURE.md §3 mode. `open(path, "a", 0o600)`
103
- * already creates it at `0600` (the umask can only remove bits), so this exists for the file that
104
- * was *not* created by us — an operator's `touch`, a `cp` from elsewhere — and is a no-op
105
- * otherwise.
106
- *
107
- * Never throws. It sits directly on the auto-spawn path, where every CLI and MCP command that
108
- * needs a daemon passes through: a filesystem that answers `chmod` with EPERM or ENOTSUP (a
109
- * drvfs/NFS/SMB-backed state dir, a root-owned log under a non-root user) would otherwise brick
110
- * every one of those commands over a permission bit. A daemon that cannot tighten its log mode
111
- * must still start — loudly, but it must start.
112
- */
113
- export const ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}\n`)) => {
114
- try {
115
- await chmod(logFilePath, STATE_FILE_MODE);
116
- return true;
117
- }
118
- catch (error) {
119
- warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
120
- return false;
121
- }
122
- };
123
- /**
124
- * `config.daemonLogMaxBytes` for a state dir, falling back to the default when `config.json` is
125
- * missing or unreadable. Deliberately forgiving: this runs on the spawn path, where a bad config
126
- * is the daemon's problem to report properly once it starts — not a reason to fail rotation (and
127
- * with it the spawn) with a duplicate error.
128
- */
129
- export const resolveDaemonLogMaxBytes = async (stateDir) => {
130
- try {
131
- return (await loadConfig(getStateDirPaths(stateDir))).daemonLogMaxBytes;
132
- }
133
- catch {
134
- return DEFAULT_DAEMON_LOG_MAX_BYTES;
135
- }
136
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"pidfile.d.ts","sourceRoot":"","sources":["../../src/daemon/pidfile.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,qBAAa,yBAA0B,SAAQ,KAAK;IACtC,QAAQ,CAAC,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM;CAIjC;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,OAQ5C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAgBrF,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,aAAa,MAAM,EACnB,UAAS,qBAA0B,KAClC,OAAO,CAAC,aAAa,CA+CvB,CAAC"}
@@ -1,90 +0,0 @@
1
- /**
2
- * Pidfile single-instancing (ARCHITECTURE.md §4). Acquired with `O_EXCL`; on conflict, liveness
3
- * is checked with `process.kill(pid, 0)` and the pidfile is only taken over if the owning
4
- * process is dead.
5
- */
6
- import { open, readFile, rm } from "node:fs/promises";
7
- export class DaemonAlreadyRunningError extends Error {
8
- pid;
9
- constructor(pid) {
10
- super(`Appduct daemon is already running (pid ${pid}).`);
11
- this.pid = pid;
12
- this.name = "DaemonAlreadyRunningError";
13
- }
14
- }
15
- /**
16
- * Liveness probe for a pid this process does not own (ARCHITECTURE.md §4's `process.kill(pid, 0)`
17
- * check). Exported because every "is a daemon still there?" decision in the codebase must answer
18
- * it the same way — pidfile takeover, the auto-spawn path's stale-socket unlink, and log rotation
19
- * all hinge on it, and a second implementation that disagreed about `EPERM` would mean one of
20
- * them quietly clobbering a live daemon's state.
21
- */
22
- export const isProcessAlive = (pid) => {
23
- try {
24
- process.kill(pid, 0);
25
- return true;
26
- }
27
- catch (error) {
28
- // EPERM means the process exists but we lack permission to signal it — still alive.
29
- return error.code === "EPERM";
30
- }
31
- };
32
- export const readPidFromFile = async (pidFilePath) => {
33
- let contents;
34
- try {
35
- contents = await readFile(pidFilePath, "utf8");
36
- }
37
- catch (error) {
38
- if (error.code === "ENOENT") {
39
- return undefined;
40
- }
41
- throw error;
42
- }
43
- const pid = Number.parseInt(contents.trim(), 10);
44
- return Number.isInteger(pid) && pid > 0 ? pid : undefined;
45
- };
46
- /**
47
- * Acquires the daemon pidfile, taking over a stale one (owner process is dead or the file is
48
- * unparseable) but throwing {@link DaemonAlreadyRunningError} when a live daemon holds it.
49
- */
50
- export const acquirePidfile = async (pidFilePath, options = {}) => {
51
- const pid = options.pid ?? process.pid;
52
- // Bounded retry: another process could win the takeover race between our stale-check and our
53
- // write attempt. A handful of retries is enough to make forward progress deterministic in tests
54
- // without looping forever on a persistently-contended pidfile.
55
- for (let attempt = 0; attempt < 5; attempt += 1) {
56
- try {
57
- const handle = await open(pidFilePath, "wx", 0o600);
58
- try {
59
- await handle.writeFile(String(pid), "utf8");
60
- }
61
- finally {
62
- await handle.close();
63
- }
64
- let released = false;
65
- return {
66
- pid,
67
- release: async () => {
68
- if (released) {
69
- return;
70
- }
71
- released = true;
72
- await rm(pidFilePath, { force: true });
73
- },
74
- };
75
- }
76
- catch (error) {
77
- if (error.code !== "EEXIST") {
78
- throw error;
79
- }
80
- const existingPid = await readPidFromFile(pidFilePath);
81
- if (existingPid !== undefined && isProcessAlive(existingPid)) {
82
- throw new DaemonAlreadyRunningError(existingPid);
83
- }
84
- // Stale pidfile (dead owner, or unparseable contents): remove and retry.
85
- await rm(pidFilePath, { force: true });
86
- await options.onStaleTakeover?.();
87
- }
88
- }
89
- throw new Error(`Failed to acquire the Appduct daemon pidfile at "${pidFilePath}".`);
90
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/daemon/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEvE,0FAA0F;AAC1F,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,EAAE,UAAU,MAAM,KAAG,MAAgC,CAAC;AAErG,eAAO,MAAM,QAAQ,GACnB,YAAY,cAAc,EAC1B,SAAS,aAAa,EACtB,QAAQ,mBAAmB,KAC1B,cAYF,CAAC"}
@@ -1,24 +0,0 @@
1
- /**
2
- * Tool-call policy evaluation (ARCHITECTURE.md §12). Every `tools.call` — CLI and MCP alike —
3
- * passes through {@link evaluate} before the daemon sends anything to the app. Precedence:
4
- *
5
- * 1. a per-tool override, `policy.tools["<alias>/<name>"]`, wins outright;
6
- * 2. else `policy.destructive` applies to tools whose descriptor sets
7
- * `annotations.destructiveHint === true`;
8
- * 3. else `policy.default`.
9
- *
10
- * This module is pure and synchronous — it knows nothing about sockets, audit, or the RPC layer;
11
- * the daemon composition root (`daemon.ts`) is the single seam that calls it and reacts to the
12
- * decision (deny → `policy_denied`, no frame to the app, per ARCHITECTURE.md §5/§12).
13
- */
14
- export const policyOverrideKey = (alias, toolName) => `${alias}/${toolName}`;
15
- export const evaluate = (descriptor, session, policy) => {
16
- const override = policy.tools?.[policyOverrideKey(session.alias, descriptor.name)];
17
- if (override !== undefined) {
18
- return override;
19
- }
20
- if (descriptor.annotations?.destructiveHint === true) {
21
- return policy.destructive;
22
- }
23
- return policy.default;
24
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/daemon/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG;IACzB,0FAA0F;IAC1F,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACvC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,cAAc,EAAE,CAAC;IAC7B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,cAAc,GAAG,SAAS,CAAC;IAClD,KAAK,EAAE,MAAM,MAAM,CAAC;CACrB,CAAC;AAEF,qGAAqG;AACrG,eAAO,MAAM,kBAAkB,GAAI,WAAW,MAAM,IAAI,KAAG,YAyB1D,CAAC"}