@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.
- package/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
- package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +25 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +56 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +506 -0
- package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/index.js +7 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
- package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
- package/bundled-plugins/plugins/lark/dist/lark.js +1975 -0
- package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/runtime.js +131328 -0
- package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/settings.js +126971 -0
- package/bundled-plugins/plugins/lark/package.json +57 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-D8Amoe6a.js +201 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/index.html +13 -0
- package/dist/agent-file.js +0 -1
- package/dist/browser-artifact-management.d.ts +4 -1
- package/dist/browser-artifact-management.js +18 -10
- package/dist/browser-artifacts.d.ts +1 -0
- package/dist/browser-artifacts.js +1 -0
- package/dist/bundled-plugins.d.ts +15 -0
- package/dist/bundled-plugins.js +134 -0
- package/dist/chrome-for-testing-runtime.d.ts +27 -0
- package/dist/chrome-for-testing-runtime.js +193 -0
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +9 -0
- package/dist/control-deps.d.ts +6 -2
- package/dist/control-deps.js +88 -82
- package/dist/daemon-server.d.ts +7 -1
- package/dist/daemon-server.js +310 -87
- package/dist/db.js +181 -124
- package/dist/entry-path.d.ts +3 -0
- package/dist/entry-path.js +12 -0
- package/dist/index-daemon.js +3 -66
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.js +16 -0
- package/dist/maintenance-lease.d.ts +39 -0
- package/dist/maintenance-lease.js +250 -0
- package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
- package/dist/migrations/cleanup-legacy-sessions.js +33 -15
- package/dist/official-plugins.d.ts +2 -0
- package/dist/official-plugins.js +2 -0
- package/dist/plugin-cli.d.ts +1 -3
- package/dist/plugin-cli.js +56 -45
- package/dist/plugin-host-leases.d.ts +4 -0
- package/dist/plugin-host-leases.js +7 -0
- package/dist/plugin-host-rpc.d.ts +73 -6
- package/dist/plugin-host-rpc.js +354 -81
- package/dist/plugin-installer.d.ts +24 -55
- package/dist/plugin-installer.js +258 -412
- package/dist/plugin-management-service.d.ts +30 -23
- package/dist/plugin-management-service.js +288 -105
- package/dist/plugin-marketplace.d.ts +35 -0
- package/dist/plugin-marketplace.js +299 -0
- package/dist/plugin-package.d.ts +22 -24
- package/dist/plugin-package.js +520 -571
- package/dist/plugin-skill-context.d.ts +4 -0
- package/dist/plugin-skill-context.js +35 -0
- package/dist/plugin-store.d.ts +36 -37
- package/dist/plugin-store.js +138 -92
- package/dist/plugin-supervisor.d.ts +50 -125
- package/dist/plugin-supervisor.js +678 -1030
- package/dist/pm2.d.ts +11 -2
- package/dist/pm2.js +130 -28
- package/dist/process.d.ts +12 -0
- package/dist/process.js +91 -0
- package/dist/runtime-project-store.d.ts +9 -0
- package/dist/runtime-project-store.js +83 -0
- package/dist/session-fallback-workspace.d.ts +22 -0
- package/dist/session-fallback-workspace.js +120 -0
- package/dist/session-fork-store.d.ts +41 -0
- package/dist/session-fork-store.js +288 -0
- package/dist/session-launch-operations.d.ts +23 -2
- package/dist/session-launch-operations.js +48 -7
- package/dist/session-meta-store.d.ts +12 -9
- package/dist/session-meta-store.js +50 -49
- package/dist/session-portal-grant-store.d.ts +29 -0
- package/dist/session-portal-grant-store.js +84 -0
- package/dist/setup-service.d.ts +21 -0
- package/dist/setup-service.js +39 -0
- package/dist/skills-catalog.d.ts +11 -2
- package/dist/skills-catalog.js +307 -7
- package/dist/system-dependencies.d.ts +26 -0
- package/dist/system-dependencies.js +149 -0
- package/package.json +36 -20
- package/dist/browser-cli-args.d.ts +0 -20
- package/dist/browser-cli-args.js +0 -100
- package/dist/channel-store.d.ts +0 -22
- package/dist/channel-store.js +0 -65
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -732
- package/dist/control-client.d.ts +0 -122
- package/dist/control-client.js +0 -855
- package/dist/control-endpoint.d.ts +0 -14
- package/dist/control-endpoint.js +0 -124
- package/dist/desktop-browser-host-client.d.ts +0 -35
- package/dist/desktop-browser-host-client.js +0 -415
- package/dist/instance-store.d.ts +0 -38
- package/dist/instance-store.js +0 -85
- package/dist/plugin-install-restart.d.ts +0 -13
- package/dist/plugin-install-restart.js +0 -24
- package/dist/plugin-runtime-control.d.ts +0 -2
- package/dist/plugin-runtime-control.js +0 -37
- package/dist/registry.d.ts +0 -10
- package/dist/registry.js +0 -52
- package/dist/setup.d.ts +0 -20
- package/dist/setup.js +0 -374
- package/dist/update.d.ts +0 -25
- 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
|
|
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
|
|
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.
|
|
16
|
-
*
|
|
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 {
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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" && !
|
|
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
|
|
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" && !
|
|
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);
|
package/dist/plugin-cli.d.ts
CHANGED
|
@@ -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
|
|
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 {
|
package/dist/plugin-cli.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
85
|
-
|
|
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.
|
|
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.
|
|
102
|
-
throw new Error(`plugin "${pluginId}" declares
|
|
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.
|
|
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}"
|
|
103
|
+
throw new Error(`plugin "${pluginId}" commands.command resolves outside its package`);
|
|
108
104
|
}
|
|
109
|
-
dataDir =
|
|
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
|
-
|
|
108
|
+
mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
|
|
109
|
+
child = spawn(cliEntry, [...args], {
|
|
112
110
|
cwd: dataDir,
|
|
113
111
|
env: {
|
|
114
|
-
|
|
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.
|
|
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"
|
|
124
|
-
: ["inherit", "inherit", "inherit"
|
|
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.
|
|
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.
|
|
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
|
+
];
|