@rynx-ai/daemon 0.1.10 → 0.1.11-beta.10

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 (156) hide show
  1. package/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
  2. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +25 -0
  3. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
  4. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +56 -0
  5. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
  6. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
  7. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
  8. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
  9. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
  10. package/bundled-plugins/plugins/lark/dist/host-adapters.js +506 -0
  11. package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
  12. package/bundled-plugins/plugins/lark/dist/index.js +7 -0
  13. package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
  14. package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
  15. package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
  16. package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
  17. package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
  18. package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
  19. package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
  20. package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
  21. package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
  22. package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
  23. package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
  24. package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
  25. package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
  26. package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
  27. package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
  28. package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
  29. package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
  30. package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
  31. package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
  32. package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
  33. package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
  34. package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
  35. package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
  36. package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
  37. package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
  38. package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
  39. package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
  40. package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
  41. package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
  42. package/bundled-plugins/plugins/lark/dist/lark.js +1975 -0
  43. package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
  44. package/bundled-plugins/plugins/lark/dist/runtime.js +131328 -0
  45. package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
  46. package/bundled-plugins/plugins/lark/dist/settings.js +126971 -0
  47. package/bundled-plugins/plugins/lark/package.json +57 -0
  48. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  49. package/bundled-plugins/plugins/lark/ui/assets/index-D8Amoe6a.js +201 -0
  50. package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  51. package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  52. package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  53. package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  54. package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  55. package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  56. package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  57. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  58. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  59. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  60. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  61. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  62. package/bundled-plugins/plugins/lark/ui/index.html +13 -0
  63. package/dist/agent-file.js +0 -1
  64. package/dist/browser-artifact-management.d.ts +4 -1
  65. package/dist/browser-artifact-management.js +18 -10
  66. package/dist/browser-artifacts.d.ts +1 -0
  67. package/dist/browser-artifacts.js +1 -0
  68. package/dist/bundled-plugins.d.ts +15 -0
  69. package/dist/bundled-plugins.js +134 -0
  70. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  71. package/dist/chrome-for-testing-runtime.js +193 -0
  72. package/dist/chrome-for-testing-store.d.ts +2 -0
  73. package/dist/chrome-for-testing-store.js +9 -0
  74. package/dist/control-deps.d.ts +6 -2
  75. package/dist/control-deps.js +88 -82
  76. package/dist/daemon-server.d.ts +7 -1
  77. package/dist/daemon-server.js +310 -87
  78. package/dist/db.js +181 -124
  79. package/dist/entry-path.d.ts +3 -0
  80. package/dist/entry-path.js +12 -0
  81. package/dist/index-daemon.js +3 -66
  82. package/dist/lifecycle.d.ts +12 -0
  83. package/dist/lifecycle.js +16 -0
  84. package/dist/maintenance-lease.d.ts +39 -0
  85. package/dist/maintenance-lease.js +250 -0
  86. package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
  87. package/dist/migrations/cleanup-legacy-sessions.js +33 -15
  88. package/dist/official-plugins.d.ts +2 -0
  89. package/dist/official-plugins.js +2 -0
  90. package/dist/plugin-cli.d.ts +1 -3
  91. package/dist/plugin-cli.js +56 -45
  92. package/dist/plugin-host-leases.d.ts +4 -0
  93. package/dist/plugin-host-leases.js +7 -0
  94. package/dist/plugin-host-rpc.d.ts +73 -6
  95. package/dist/plugin-host-rpc.js +354 -81
  96. package/dist/plugin-installer.d.ts +24 -55
  97. package/dist/plugin-installer.js +258 -412
  98. package/dist/plugin-management-service.d.ts +30 -23
  99. package/dist/plugin-management-service.js +288 -105
  100. package/dist/plugin-marketplace.d.ts +35 -0
  101. package/dist/plugin-marketplace.js +299 -0
  102. package/dist/plugin-package.d.ts +22 -24
  103. package/dist/plugin-package.js +520 -571
  104. package/dist/plugin-skill-context.d.ts +4 -0
  105. package/dist/plugin-skill-context.js +35 -0
  106. package/dist/plugin-store.d.ts +36 -37
  107. package/dist/plugin-store.js +138 -92
  108. package/dist/plugin-supervisor.d.ts +50 -125
  109. package/dist/plugin-supervisor.js +678 -1030
  110. package/dist/pm2.d.ts +11 -2
  111. package/dist/pm2.js +130 -28
  112. package/dist/process.d.ts +12 -0
  113. package/dist/process.js +91 -0
  114. package/dist/runtime-project-store.d.ts +9 -0
  115. package/dist/runtime-project-store.js +83 -0
  116. package/dist/session-fallback-workspace.d.ts +22 -0
  117. package/dist/session-fallback-workspace.js +120 -0
  118. package/dist/session-fork-store.d.ts +41 -0
  119. package/dist/session-fork-store.js +288 -0
  120. package/dist/session-launch-operations.d.ts +23 -2
  121. package/dist/session-launch-operations.js +48 -7
  122. package/dist/session-meta-store.d.ts +12 -9
  123. package/dist/session-meta-store.js +50 -49
  124. package/dist/session-portal-grant-store.d.ts +29 -0
  125. package/dist/session-portal-grant-store.js +84 -0
  126. package/dist/setup-service.d.ts +21 -0
  127. package/dist/setup-service.js +39 -0
  128. package/dist/skills-catalog.d.ts +11 -2
  129. package/dist/skills-catalog.js +307 -7
  130. package/dist/system-dependencies.d.ts +26 -0
  131. package/dist/system-dependencies.js +149 -0
  132. package/package.json +36 -20
  133. package/dist/browser-cli-args.d.ts +0 -20
  134. package/dist/browser-cli-args.js +0 -100
  135. package/dist/channel-store.d.ts +0 -22
  136. package/dist/channel-store.js +0 -65
  137. package/dist/cli.d.ts +0 -2
  138. package/dist/cli.js +0 -732
  139. package/dist/control-client.d.ts +0 -122
  140. package/dist/control-client.js +0 -855
  141. package/dist/control-endpoint.d.ts +0 -14
  142. package/dist/control-endpoint.js +0 -124
  143. package/dist/desktop-browser-host-client.d.ts +0 -35
  144. package/dist/desktop-browser-host-client.js +0 -415
  145. package/dist/instance-store.d.ts +0 -38
  146. package/dist/instance-store.js +0 -85
  147. package/dist/plugin-install-restart.d.ts +0 -13
  148. package/dist/plugin-install-restart.js +0 -24
  149. package/dist/plugin-runtime-control.d.ts +0 -2
  150. package/dist/plugin-runtime-control.js +0 -37
  151. package/dist/registry.d.ts +0 -10
  152. package/dist/registry.js +0 -52
  153. package/dist/setup.d.ts +0 -20
  154. package/dist/setup.js +0 -374
  155. package/dist/update.d.ts +0 -25
  156. package/dist/update.js +0 -133
@@ -0,0 +1,250 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { rynxHome } from "@rynx-ai/core";
5
+ const LEASE_SCHEMA_VERSION = 1;
6
+ // The detached updater can spend 24h waiting for idle before cutover begins.
7
+ // Once accepted, retain a separate bounded recovery window long enough for the
8
+ // package switch and rollback even if the updater is killed at its outer limit.
9
+ const DEFAULT_LEASE_TTL_MS = 30 * 60 * 60 * 1_000;
10
+ const EXPIRY_RETRY_MS = 1_000;
11
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
12
+ const LEASE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,256}$/;
13
+ export function maintenanceLeaseFile(env = process.env) {
14
+ return join(rynxHome(env), "maintenance-lease.json");
15
+ }
16
+ /**
17
+ * Cross-process admission fence used only during daemon replacement.
18
+ *
19
+ * The raw bearer token is never persisted. The bounded expiry means a crashed
20
+ * upgrader cannot leave the daemon permanently unavailable, while atomic rename
21
+ * ensures a replacement daemon sees either the complete lease or no lease.
22
+ */
23
+ export class MaintenanceLeaseStore {
24
+ filePath;
25
+ now;
26
+ ttlMs;
27
+ syncDirectory;
28
+ onExpired;
29
+ lease;
30
+ expiryTimer;
31
+ constructor(options = {}) {
32
+ this.filePath = options.filePath ?? maintenanceLeaseFile();
33
+ this.now = options.now ?? (() => Date.now());
34
+ this.ttlMs = options.ttlMs ?? DEFAULT_LEASE_TTL_MS;
35
+ this.syncDirectory = options.syncDirectory ?? fsyncDirectory;
36
+ this.onExpired = options.onExpired ?? (() => undefined);
37
+ if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0) {
38
+ throw new Error("maintenance lease TTL must be a positive safe integer");
39
+ }
40
+ this.lease = this.readPersistedLease();
41
+ this.scheduleExpiry();
42
+ }
43
+ isActive() {
44
+ if (!this.lease)
45
+ return false;
46
+ if (this.lease.expiresAt > this.now())
47
+ return true;
48
+ this.expire();
49
+ return false;
50
+ }
51
+ isHeldBy(token) {
52
+ let tokenHash;
53
+ try {
54
+ tokenHash = hashLeaseToken(token);
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ return Boolean(this.isActive()
60
+ && this.lease
61
+ && hashesEqual(this.lease.tokenHash, tokenHash));
62
+ }
63
+ acquire(token) {
64
+ const tokenHash = hashLeaseToken(token);
65
+ if (this.isActive()) {
66
+ if (this.lease && hashesEqual(this.lease.tokenHash, tokenHash)) {
67
+ const createdAt = this.now();
68
+ const renewed = {
69
+ schemaVersion: LEASE_SCHEMA_VERSION,
70
+ tokenHash,
71
+ createdAt,
72
+ expiresAt: createdAt + this.ttlMs,
73
+ };
74
+ // Fail closed in memory if persistence reaches an indeterminate I/O
75
+ // outcome; the caller sees the error, but this process must not reopen
76
+ // admission around a lease file that may already have been renamed.
77
+ this.lease = renewed;
78
+ try {
79
+ this.writePersistedLease(renewed);
80
+ }
81
+ finally {
82
+ this.scheduleExpiry();
83
+ }
84
+ return false;
85
+ }
86
+ throw new Error("a different maintenance lease is already active");
87
+ }
88
+ const createdAt = this.now();
89
+ const lease = {
90
+ schemaVersion: LEASE_SCHEMA_VERSION,
91
+ tokenHash,
92
+ createdAt,
93
+ expiresAt: createdAt + this.ttlMs,
94
+ };
95
+ this.lease = lease;
96
+ try {
97
+ this.writePersistedLease(lease);
98
+ }
99
+ finally {
100
+ this.scheduleExpiry();
101
+ }
102
+ return true;
103
+ }
104
+ release(token) {
105
+ if (!this.isActive())
106
+ return "not_active";
107
+ const lease = this.lease;
108
+ if (!lease)
109
+ return "not_active";
110
+ let tokenHash;
111
+ try {
112
+ tokenHash = hashLeaseToken(token);
113
+ }
114
+ catch {
115
+ return "denied";
116
+ }
117
+ if (!hashesEqual(lease.tokenHash, tokenHash))
118
+ return "denied";
119
+ this.clear();
120
+ return "released";
121
+ }
122
+ readPersistedLease() {
123
+ let parsed;
124
+ try {
125
+ parsed = JSON.parse(readFileSync(this.filePath, "utf8"));
126
+ }
127
+ catch (error) {
128
+ if (error
129
+ && typeof error === "object"
130
+ && "code" in error
131
+ && error.code === "ENOENT")
132
+ return undefined;
133
+ throw new Error("maintenance lease state is unreadable", { cause: error });
134
+ }
135
+ if (!isPersistedLease(parsed, this.ttlMs)) {
136
+ throw new Error("maintenance lease state is invalid");
137
+ }
138
+ if (parsed.expiresAt <= this.now()) {
139
+ this.clearFile();
140
+ return undefined;
141
+ }
142
+ return parsed;
143
+ }
144
+ writePersistedLease(lease) {
145
+ const parent = dirname(this.filePath);
146
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
147
+ const temporary = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
148
+ let descriptor;
149
+ try {
150
+ descriptor = openSync(temporary, "wx", 0o600);
151
+ writeFileSync(descriptor, `${JSON.stringify(lease)}\n`, "utf8");
152
+ fsyncSync(descriptor);
153
+ closeSync(descriptor);
154
+ descriptor = undefined;
155
+ renameSync(temporary, this.filePath);
156
+ this.syncDirectory(parent);
157
+ }
158
+ finally {
159
+ if (descriptor !== undefined)
160
+ closeSync(descriptor);
161
+ rmSync(temporary, { force: true });
162
+ }
163
+ }
164
+ clear() {
165
+ this.clearFile();
166
+ this.lease = undefined;
167
+ this.cancelExpiry();
168
+ }
169
+ clearFile() {
170
+ rmSync(this.filePath, { force: true });
171
+ this.syncDirectory(dirname(this.filePath));
172
+ }
173
+ expire() {
174
+ this.clear();
175
+ this.onExpired();
176
+ }
177
+ scheduleExpiry(delayOverride) {
178
+ this.cancelExpiry();
179
+ if (!this.lease)
180
+ return;
181
+ const delay = delayOverride ?? Math.max(0, this.lease.expiresAt - this.now());
182
+ this.expiryTimer = setTimeout(() => {
183
+ this.expiryTimer = undefined;
184
+ if (!this.lease)
185
+ return;
186
+ if (this.lease.expiresAt > this.now()) {
187
+ this.scheduleExpiry();
188
+ return;
189
+ }
190
+ try {
191
+ this.expire();
192
+ }
193
+ catch {
194
+ // Keep the in-memory fence and retry cleanup. A failed durability
195
+ // operation must not turn an expired lease into open admission.
196
+ this.scheduleExpiry(EXPIRY_RETRY_MS);
197
+ }
198
+ }, Math.min(delay, MAX_TIMER_DELAY_MS));
199
+ this.expiryTimer.unref?.();
200
+ }
201
+ cancelExpiry() {
202
+ if (this.expiryTimer)
203
+ clearTimeout(this.expiryTimer);
204
+ this.expiryTimer = undefined;
205
+ }
206
+ }
207
+ function fsyncDirectory(directory) {
208
+ let descriptor;
209
+ try {
210
+ descriptor = openSync(directory, "r");
211
+ fsyncSync(descriptor);
212
+ }
213
+ catch (error) {
214
+ if (error
215
+ && typeof error === "object"
216
+ && "code" in error
217
+ && error.code === "ENOENT")
218
+ return;
219
+ throw error;
220
+ }
221
+ finally {
222
+ if (descriptor !== undefined)
223
+ closeSync(descriptor);
224
+ }
225
+ }
226
+ function hashLeaseToken(token) {
227
+ if (!LEASE_TOKEN_PATTERN.test(token)) {
228
+ throw new Error("maintenance lease token must be 32-256 base64url characters");
229
+ }
230
+ return createHash("sha256").update(token, "utf8").digest("hex");
231
+ }
232
+ function hashesEqual(left, right) {
233
+ const leftBuffer = Buffer.from(left, "hex");
234
+ const rightBuffer = Buffer.from(right, "hex");
235
+ return leftBuffer.length === rightBuffer.length
236
+ && timingSafeEqual(leftBuffer, rightBuffer);
237
+ }
238
+ function isPersistedLease(value, maximumTtlMs) {
239
+ if (!value || typeof value !== "object")
240
+ return false;
241
+ const candidate = value;
242
+ return candidate.schemaVersion === LEASE_SCHEMA_VERSION
243
+ && typeof candidate.tokenHash === "string"
244
+ && /^[a-f0-9]{64}$/.test(candidate.tokenHash)
245
+ && Number.isSafeInteger(candidate.createdAt)
246
+ && Number.isSafeInteger(candidate.expiresAt)
247
+ && candidate.createdAt >= 0
248
+ && candidate.expiresAt > candidate.createdAt
249
+ && candidate.expiresAt - candidate.createdAt <= maximumTtlMs;
250
+ }
@@ -1,5 +1,5 @@
1
1
  export interface CleanupOptions {
2
- /** CodexSessionStore JSON path; defaults to `<cwd>/.codex-proxy/sessions.json`. */
2
+ /** CodexSessionStore JSON path; defaults to `{RYNX_HOME}/.codex-proxy/sessions.json`. */
3
3
  codexStorePath?: string;
4
4
  /** Report what would be removed without deleting anything. */
5
5
  dryRun?: boolean;
@@ -8,20 +8,21 @@
8
8
  * - canonical-log rows (`session_items`) and identity rows (`sessions`) whose
9
9
  * id is not an opaque `sess_` id,
10
10
  * - the daemon-owned runtime thread→rollout bindings in
11
- * `.codex-proxy/sessions.json` that use a legacy id.
11
+ * `.codex-proxy/sessions.json` that no longer have a Session row. This also
12
+ * removes opaque bindings left behind by the snapshot-schema cutover.
12
13
  *
13
14
  * Runs on every daemon boot (not marker-gated): on a healthy system it deletes
14
15
  * nothing and writes nothing, so it is cheap; if a legacy session ever appears
15
- * the next boot removes it. The `sess_` filter means it can never touch a
16
- * current session. Plugin-private routing stores are deliberately not read.
16
+ * the next boot removes it. Plugin-private routing stores are deliberately not
17
+ * read.
17
18
  */
18
19
  import { mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
19
20
  import path from "node:path";
20
- import { isMachineSessionId } from "@rynx-ai/core";
21
+ import { rynxHome } from "@rynx-ai/core";
21
22
  import { db } from "../db.js";
22
23
  /** Mirrors the daemon runtime store's default resolution. */
23
24
  function defaultProxyDir() {
24
- return path.join(process.cwd(), ".codex-proxy");
25
+ return path.join(rynxHome(), ".codex-proxy");
25
26
  }
26
27
  // Anything whose id isn't an opaque `sess_` id is legacy. GLOB treats `_` as a
27
28
  // literal (unlike LIKE), so `sess_*` matches exactly the new id shape.
@@ -31,11 +32,14 @@ export function cleanupLegacySessions(opts = {}) {
31
32
  const log = opts.log ?? (() => { });
32
33
  const dryRun = Boolean(opts.dryRun);
33
34
  const conn = db();
34
- const codexStorePath = opts.codexStorePath ?? path.join(defaultProxyDir(), "sessions.json");
35
+ const liveSessionIds = new Set(conn.prepare("SELECT id FROM sessions").all().map((row) => row.id));
36
+ const codexStorePath = opts.codexStorePath
37
+ ? path.resolve(rynxHome(), opts.codexStorePath)
38
+ : path.join(defaultProxyDir(), "sessions.json");
35
39
  if (dryRun) {
36
40
  const deletedLogRows = countRows(conn, `SELECT COUNT(*) AS n FROM session_items WHERE ${LEGACY_LOG}`);
37
41
  const deletedSessions = countRows(conn, `SELECT COUNT(*) AS n FROM sessions WHERE ${LEGACY_META}`);
38
- const prunedStoreEntries = countLegacyStoreEntries(readJsonStore(codexStorePath), (key) => key);
42
+ const prunedStoreEntries = countOrphanedStoreEntries(readJsonStore(codexStorePath), (key) => key, liveSessionIds);
39
43
  log({ event: "cleanup.dry_run", deletedLogRows, deletedSessions, prunedStoreEntries });
40
44
  return { dryRun: true, deletedLogRows, deletedSessions, prunedStoreEntries };
41
45
  }
@@ -48,7 +52,7 @@ export function cleanupLegacySessions(opts = {}) {
48
52
  })();
49
53
  // The runtime store is daemon-owned. Plugin routing stores remain private to
50
54
  // their plugins and are never inspected by this migration.
51
- prunedStoreEntries += pruneStore(codexStorePath, (key) => key);
55
+ prunedStoreEntries += pruneStore(codexStorePath, (key) => key, liveSessionIds);
52
56
  if (deletedLogRows || deletedSessions || prunedStoreEntries) {
53
57
  log({ event: "cleanup.done", deletedLogRows, deletedSessions, prunedStoreEntries });
54
58
  }
@@ -57,31 +61,45 @@ export function cleanupLegacySessions(opts = {}) {
57
61
  function countRows(conn, sql) {
58
62
  return conn.prepare(sql).get().n;
59
63
  }
60
- function countLegacyStoreEntries(data, idOf) {
64
+ function countOrphanedStoreEntries(data, idOf, liveSessionIds) {
61
65
  let n = 0;
62
66
  for (const [key, record] of Object.entries(data.sessions ?? {})) {
63
67
  const id = idOf(key, record);
64
- if (typeof id === "string" && !isMachineSessionId(id))
68
+ if (typeof id === "string" && !liveSessionIds.has(id))
69
+ n += 1;
70
+ }
71
+ for (const key of Object.keys(data.claudeForkIntents ?? {})) {
72
+ if (!liveSessionIds.has(key))
65
73
  n += 1;
66
74
  }
67
75
  return n;
68
76
  }
69
- /** Drop every legacy entry from a JSON store; returns how many were removed. */
70
- function pruneStore(file, idOf) {
77
+ /** Drop every binding whose owning Session no longer exists. */
78
+ function pruneStore(file, idOf, liveSessionIds) {
71
79
  const data = readJsonStore(file);
72
- if (!data.sessions)
80
+ if (!data.sessions && !data.claudeForkIntents)
73
81
  return 0;
74
82
  let pruned = 0;
75
83
  const kept = {};
76
- for (const [key, record] of Object.entries(data.sessions)) {
84
+ for (const [key, record] of Object.entries(data.sessions ?? {})) {
77
85
  const id = idOf(key, record);
78
- if (typeof id === "string" && !isMachineSessionId(id)) {
86
+ if (typeof id === "string" && !liveSessionIds.has(id)) {
79
87
  pruned += 1;
80
88
  }
81
89
  else {
82
90
  kept[key] = record;
83
91
  }
84
92
  }
93
+ if (data.claudeForkIntents) {
94
+ const keptIntents = {};
95
+ for (const [targetSessionId, intent] of Object.entries(data.claudeForkIntents)) {
96
+ if (liveSessionIds.has(targetSessionId))
97
+ keptIntents[targetSessionId] = intent;
98
+ else
99
+ pruned += 1;
100
+ }
101
+ data.claudeForkIntents = keptIntents;
102
+ }
85
103
  if (pruned > 0) {
86
104
  data.sessions = kept;
87
105
  writeJsonStore(file, data);
@@ -0,0 +1,2 @@
1
+ export declare const OFFICIAL_MARKETPLACE_ID = "rynx-ai/rynx";
2
+ export declare const DEFAULT_BUNDLED_PLUGINS: readonly ["lark@rynx-ai/rynx"];
@@ -0,0 +1,2 @@
1
+ export const OFFICIAL_MARKETPLACE_ID = "rynx-ai/rynx";
2
+ export const DEFAULT_BUNDLED_PLUGINS = ["lark@rynx-ai/rynx"];
@@ -1,4 +1,3 @@
1
- import { type PluginCliHostEffect } from "@rynx-ai/plugin-sdk";
2
1
  import { type RunnablePluginRecord } from "./plugin-store.js";
3
2
  export interface RunPluginCliOptions {
4
3
  stdio?: "inherit" | "pipe";
@@ -14,7 +13,6 @@ export interface RunPluginCliOptions {
14
13
  }
15
14
  export interface PluginCliCommandResult {
16
15
  code: number;
17
- effects: PluginCliHostEffect[];
18
16
  output?: {
19
17
  stdout: string;
20
18
  stderr: string;
@@ -22,7 +20,7 @@ export interface PluginCliCommandResult {
22
20
  stderrTruncated: boolean;
23
21
  };
24
22
  }
25
- /** Execute one statically declared plugin command in a separate Node process. */
23
+ /** Execute one statically declared, language-neutral plugin command. */
26
24
  export declare function runPluginCliCommand(pluginId: string, args: readonly string[], options?: RunPluginCliOptions): Promise<PluginCliCommandResult>;
27
25
  export declare function pluginCommandUsage(record: RunnablePluginRecord): string;
28
26
  export declare class PluginCliUsageError extends Error {
@@ -1,14 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdirSync, realpathSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { rynxHome } from "@rynx-ai/core";
5
- import { createPluginCliInvocationLeaseId, parsePluginCliHostEffect, removePluginRuntimeLease, writePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
4
+ import { createPluginCliInvocationLeaseId, removePluginRuntimeLease, writePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
6
5
  import { db } from "./db.js";
7
- import { getPlugin, isRunnablePluginRecord } from "./plugin-store.js";
6
+ import { pluginDataDir, pluginRuntimeCacheDir } from "./plugin-installer.js";
7
+ import { validatePluginPackageSnapshot } from "./plugin-package.js";
8
+ import { getPlugin } from "./plugin-store.js";
8
9
  import { loadOrCreateDaemonInstallationIdentitySync } from "./installation-id.js";
9
10
  const DEFAULT_TERMINATION_GRACE_MS = 2_000;
10
11
  const MAX_TERMINATION_GRACE_MS = 30_000;
11
- /** Execute one statically declared plugin command in a separate Node process. */
12
+ /** Execute one statically declared, language-neutral plugin command. */
12
13
  export async function runPluginCliCommand(pluginId, args, options = {}) {
13
14
  const hostEnv = options.env ?? process.env;
14
15
  const terminationGraceMs = options.terminationGraceMs ?? DEFAULT_TERMINATION_GRACE_MS;
@@ -21,7 +22,6 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
21
22
  let terminationReason;
22
23
  let forceKillTimer;
23
24
  let abortListener;
24
- const effects = new Map();
25
25
  if (options.stdin !== undefined && options.stdio !== "pipe") {
26
26
  throw new Error("plugin CLI stdin requires piped stdio");
27
27
  }
@@ -37,6 +37,13 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
37
37
  if (options.signal?.aborted)
38
38
  throw pluginCliAbortError(options.signal.reason);
39
39
  const installationIdentity = loadOrCreateDaemonInstallationIdentitySync(hostEnv);
40
+ const verifiedRecord = getPlugin(pluginId);
41
+ if (!verifiedRecord)
42
+ throw new Error(`plugin "${pluginId}" is not installed`);
43
+ const verifiedIntegrity = await validatePluginPackageSnapshot(verifiedRecord.installPath, options.signal ? { signal: options.signal } : {});
44
+ if (verifiedIntegrity !== verifiedRecord.integrity) {
45
+ throw new Error(`plugin package integrity changed for "${verifiedRecord.canonicalId}"`);
46
+ }
40
47
  const clearForceKill = () => {
41
48
  if (forceKillTimer)
42
49
  clearTimeout(forceKillTimer);
@@ -47,24 +54,14 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
47
54
  terminationReason ??= reason;
48
55
  if (!childIsRunning())
49
56
  return;
50
- try {
51
- child.kill("SIGTERM");
52
- }
53
- catch {
54
- // The close/error event remains authoritative for cleanup.
55
- }
57
+ signalChildTree(child, "SIGTERM");
56
58
  if (forceKillTimer)
57
59
  return;
58
60
  forceKillTimer = setTimeout(() => {
59
61
  forceKillTimer = undefined;
60
62
  if (!childIsRunning())
61
63
  return;
62
- try {
63
- child.kill("SIGKILL");
64
- }
65
- catch {
66
- // Completion still waits for close/error; no lease is removed early.
67
- }
64
+ signalChildTree(child, "SIGKILL");
68
65
  }, terminationGraceMs);
69
66
  forceKillTimer.unref?.();
70
67
  };
@@ -81,14 +78,13 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
81
78
  const record = getPlugin(pluginId);
82
79
  if (!record)
83
80
  throw new Error(`plugin "${pluginId}" is not installed`);
84
- if (!isRunnablePluginRecord(record)) {
85
- throw new Error(`plugin "${pluginId}" uses the removed legacy format; reinstall it`);
81
+ if (record.integrity !== verifiedRecord.integrity ||
82
+ record.installPath !== verifiedRecord.installPath ||
83
+ record.updatedAt !== verifiedRecord.updatedAt) {
84
+ throw new Error(`plugin "${pluginId}" changed while its command was being prepared; retry`);
86
85
  }
87
- // `enabled` controls daemon runtime contributions, not explicit CLI calls.
88
- // Keeping commands available lets a user stop/status a managed service before
89
- // uninstalling a disabled plugin.
90
86
  const command = args[0];
91
- const declared = record.manifest.contributions?.commands?.find((entry) => entry.id === command);
87
+ const declared = record.manifest.commands?.items.find((entry) => entry.id === command);
92
88
  if (!command ||
93
89
  command === "--help" ||
94
90
  command === "-h" ||
@@ -98,41 +94,40 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
98
94
  if (!declared) {
99
95
  throw new PluginCliUsageError(`plugin "${pluginId}" does not declare command "${command}"\n\n${pluginCommandUsage(record)}`);
100
96
  }
101
- if (!record.manifest.cliEntry) {
102
- throw new Error(`plugin "${pluginId}" declares commands but has no cliEntry`);
97
+ if (!record.manifest.commands) {
98
+ throw new Error(`plugin "${pluginId}" declares no CLI command executable`);
103
99
  }
104
100
  const packageRoot = realpathSync(record.installPath);
105
- const cliEntry = realpathSync(path.resolve(packageRoot, record.manifest.cliEntry));
101
+ const cliEntry = realpathSync(path.resolve(packageRoot, record.manifest.commands.command));
106
102
  if (!isPathInside(packageRoot, cliEntry) || !statSync(cliEntry).isFile()) {
107
- throw new Error(`plugin "${pluginId}" cliEntry resolves outside its package`);
103
+ throw new Error(`plugin "${pluginId}" commands.command resolves outside its package`);
108
104
  }
109
- dataDir = path.join(rynxHome(), "plugin-data", record.name);
105
+ dataDir = pluginDataDir(record.marketplaceId, record.pluginId);
106
+ const cacheDir = pluginRuntimeCacheDir(record.marketplaceId, record.pluginId, record.integrity);
110
107
  mkdirSync(dataDir, { recursive: true, mode: 0o700 });
111
- child = spawn(process.execPath, [cliEntry, ...args], {
108
+ mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
109
+ child = spawn(cliEntry, [...args], {
112
110
  cwd: dataDir,
113
111
  env: {
114
- ...hostEnv,
112
+ PATH: hostEnv.PATH,
113
+ HOME: hostEnv.HOME,
114
+ TMPDIR: hostEnv.TMPDIR,
115
+ LANG: hostEnv.LANG,
115
116
  RYNX_HOST_INSTALLATION_ID: installationIdentity.installationId,
116
117
  RYNX_HOST_INSTALLATION_PROOF_ALGORITHM: installationIdentity.algorithm,
117
118
  RYNX_HOST_INSTALLATION_PUBLIC_KEY: installationIdentity.publicKey,
118
- RYNX_PLUGIN_ID: record.name,
119
+ RYNX_PLUGIN_ID: record.pluginId,
120
+ RYNX_PLUGIN_INSTALLATION_ID: record.canonicalId,
119
121
  RYNX_PLUGIN_PACKAGE_ROOT: packageRoot,
120
122
  RYNX_PLUGIN_DATA_DIR: dataDir,
123
+ RYNX_PLUGIN_CACHE_DIR: cacheDir,
121
124
  },
122
125
  stdio: options.stdio === "pipe"
123
- ? ["pipe", "pipe", "pipe", "ipc"]
124
- : ["inherit", "inherit", "inherit", "ipc"],
126
+ ? ["pipe", "pipe", "pipe"]
127
+ : ["inherit", "inherit", "inherit"],
128
+ detached: process.platform !== "win32",
125
129
  });
126
130
  completion = new Promise((resolve, reject) => {
127
- child.on("message", (message) => {
128
- try {
129
- const effect = parsePluginCliHostEffect(message);
130
- effects.set(effect.effect, effect);
131
- }
132
- catch (error) {
133
- terminateChild(error instanceof Error ? error : new Error(String(error)));
134
- }
135
- });
136
131
  child.once("error", (error) => {
137
132
  if (!childIsRunning()) {
138
133
  reject(error);
@@ -172,7 +167,7 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
172
167
  leaseId = createPluginCliInvocationLeaseId(child.pid ?? process.pid);
173
168
  writePluginRuntimeLease({
174
169
  dataDir,
175
- pluginId: record.name,
170
+ pluginId: record.pluginId,
176
171
  leaseId,
177
172
  installPath: packageRoot,
178
173
  });
@@ -193,7 +188,6 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
193
188
  const code = await completion;
194
189
  return {
195
190
  code,
196
- effects: code === 0 ? [...effects.values()] : [],
197
191
  ...(options.captureOutputBytes !== undefined && stdout && stderr
198
192
  ? {
199
193
  output: {
@@ -210,6 +204,23 @@ export async function runPluginCliCommand(pluginId, args, options = {}) {
210
204
  cleanup();
211
205
  }
212
206
  }
207
+ function signalChildTree(child, signal) {
208
+ if (process.platform !== "win32" && child.pid) {
209
+ try {
210
+ process.kill(-child.pid, signal);
211
+ return;
212
+ }
213
+ catch {
214
+ // Fall back to the direct child if its process group already exited.
215
+ }
216
+ }
217
+ try {
218
+ child.kill(signal);
219
+ }
220
+ catch {
221
+ // Completion still waits for close/error; no lease is removed early.
222
+ }
223
+ }
213
224
  function pluginCliAbortError(reason) {
214
225
  const error = new Error(reason instanceof Error && reason.message ? reason.message : "plugin CLI command aborted");
215
226
  error.name = reason instanceof Error && reason.name === "TimeoutError"
@@ -246,7 +257,7 @@ function captureStream(stream, limit) {
246
257
  };
247
258
  }
248
259
  export function pluginCommandUsage(record) {
249
- const commands = record.manifest.contributions?.commands ?? [];
260
+ const commands = record.manifest.commands?.items ?? [];
250
261
  const lines = [`Usage: rynx plugin ${record.name} <command> [args...]`];
251
262
  if (commands.length === 0)
252
263
  return [...lines, "", "This plugin declares no CLI commands."].join("\n");
@@ -0,0 +1,4 @@
1
+ /** Fixed leases owned by the resident daemon's plugin supervisor. A new daemon
2
+ * may reclaim these only after it has acquired the exclusive RYNX_HOME owner
3
+ * lock and before starting any plugin runners. */
4
+ export declare const PLUGIN_HOST_RUNTIME_LEASE_IDS: readonly ["host-runtime", "host-settings"];
@@ -0,0 +1,7 @@
1
+ /** Fixed leases owned by the resident daemon's plugin supervisor. A new daemon
2
+ * may reclaim these only after it has acquired the exclusive RYNX_HOME owner
3
+ * lock and before starting any plugin runners. */
4
+ export const PLUGIN_HOST_RUNTIME_LEASE_IDS = [
5
+ "host-runtime",
6
+ "host-settings",
7
+ ];