@rynx-ai/daemon 0.1.10 → 0.1.11-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
- package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +37 -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 +54 -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 +504 -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 +1977 -0
- package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/runtime.js +131316 -0
- package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/settings.js +126958 -0
- package/bundled-plugins/plugins/lark/package.json +57 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +1 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-D9N-AkLX.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/bundled-plugins.d.ts +15 -0
- package/dist/bundled-plugins.js +134 -0
- package/dist/cli.js +26 -208
- package/dist/control-deps.js +2 -79
- package/dist/daemon-server.d.ts +4 -1
- package/dist/daemon-server.js +167 -57
- package/dist/db.js +145 -124
- package/dist/index-daemon.js +25 -4
- 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 +42 -4
- package/dist/plugin-host-rpc.js +144 -34
- package/dist/plugin-installer.d.ts +24 -55
- package/dist/plugin-installer.js +202 -415
- package/dist/plugin-management-service.d.ts +30 -23
- package/dist/plugin-management-service.js +290 -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 +140 -92
- package/dist/plugin-supervisor.d.ts +45 -125
- package/dist/plugin-supervisor.js +516 -1053
- package/dist/pm2.js +15 -10
- 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 +14 -2
- package/dist/session-launch-operations.js +34 -2
- package/dist/session-meta-store.d.ts +7 -9
- package/dist/session-meta-store.js +28 -49
- package/dist/setup.js +26 -14
- package/package.json +14 -11
- package/dist/channel-store.d.ts +0 -22
- package/dist/channel-store.js +0 -65
- 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
|
@@ -1,1143 +1,606 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { normalizePluginConfig, } from "@rynx-ai/plugin-sdk";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { removePluginRuntimeLease, writePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
|
|
4
|
+
import { spawnPluginRunner, } from "@rynx-ai/plugin-runner";
|
|
6
5
|
import { MAX_PLUGIN_HOST_EVENT_BYTES, PluginHostRpc, } from "./plugin-host-rpc.js";
|
|
6
|
+
import { pluginDataDir, pluginRuntimeCacheDir, } from "./plugin-installer.js";
|
|
7
|
+
import { PLUGIN_HOST_RUNTIME_LEASE_IDS } from "./plugin-host-leases.js";
|
|
8
|
+
import { isBuiltInPluginMarketplace } from "./plugin-marketplace.js";
|
|
9
|
+
import { validatePluginPackageSnapshot } from "./plugin-package.js";
|
|
10
|
+
import { setPluginRuntimeState, } from "./plugin-store.js";
|
|
7
11
|
const DEFAULT_RESTART_BASE_MS = 1_000;
|
|
8
12
|
const MAX_RESTART_DELAY_MS = 30_000;
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const MAX_STDERR_LINE_BYTES = 64 * 1024;
|
|
13
|
-
const MAX_STATUS_ERROR_BYTES = 16 * 1024;
|
|
14
|
-
const DEFAULT_READY_TIMEOUT_MS = 15_000;
|
|
15
|
-
const MAX_READY_TIMEOUT_MS = 60_000;
|
|
16
|
-
const RUNTIME_STATUS_POLL_MS = 50;
|
|
17
|
-
const TARGET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
13
|
+
const STABLE_RESET_MS = 60_000;
|
|
14
|
+
const DEFAULT_HEALTH_POLL_MS = 30_000;
|
|
15
|
+
const [RUNTIME_LEASE_ID, SETTINGS_LEASE_ID] = PLUGIN_HOST_RUNTIME_LEASE_IDS;
|
|
18
16
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* There is at most one resident child per enabled plugin. Chat-channel
|
|
23
|
-
* instances are independent desired-state entries inside that child, so
|
|
24
|
-
* stopping the final channel does not tear down the plugin runtime itself.
|
|
17
|
+
* Supervises exactly one language-neutral runtime process per enabled plugin.
|
|
18
|
+
* It has no channel-instance API and never interprets plugin-owned state.
|
|
25
19
|
*/
|
|
26
20
|
export class PluginSupervisor {
|
|
27
21
|
states = new Map();
|
|
28
|
-
|
|
22
|
+
queues = new Map();
|
|
29
23
|
spawnRunner;
|
|
30
|
-
|
|
24
|
+
log;
|
|
25
|
+
daemonInstanceId;
|
|
31
26
|
restartBaseMs;
|
|
32
27
|
restartMaxMs;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
healthPollMs;
|
|
29
|
+
verifyPackage;
|
|
30
|
+
services;
|
|
36
31
|
disposed = false;
|
|
37
|
-
constructor(
|
|
32
|
+
constructor(records, options = {}) {
|
|
38
33
|
this.services = options.services;
|
|
39
34
|
this.spawnRunner = options.spawnRunner ?? spawnPluginRunner;
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this.
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/** Static channel catalog. Calling this never loads or executes plugin code. */
|
|
54
|
-
createChannelTypeDescriptors() {
|
|
55
|
-
const descriptors = [];
|
|
56
|
-
for (const state of this.states.values()) {
|
|
57
|
-
for (const contribution of state.record.manifest.contributions?.chatChannels ?? []) {
|
|
58
|
-
descriptors.push(this.channelDescriptor(state, contribution));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return descriptors;
|
|
35
|
+
this.log = options.log ?? (() => { });
|
|
36
|
+
this.daemonInstanceId = options.daemonInstanceId ?? randomUUID();
|
|
37
|
+
this.restartBaseMs = options.restartBaseMs ?? DEFAULT_RESTART_BASE_MS;
|
|
38
|
+
this.restartMaxMs = Math.min(options.restartMaxMs ?? MAX_RESTART_DELAY_MS, MAX_RESTART_DELAY_MS);
|
|
39
|
+
this.healthPollMs = options.healthPollMs ?? DEFAULT_HEALTH_POLL_MS;
|
|
40
|
+
this.verifyPackage = options.verifyPackage ?? verifyInstalledPackage;
|
|
41
|
+
if (!Number.isSafeInteger(this.healthPollMs) || this.healthPollMs <= 0) {
|
|
42
|
+
throw new Error("plugin health poll interval must be a positive safe integer");
|
|
43
|
+
}
|
|
44
|
+
for (const record of records)
|
|
45
|
+
this.upsertState(record);
|
|
62
46
|
}
|
|
63
|
-
/** Bind the trusted host once, after the server composition root constructs it. */
|
|
64
47
|
bindHostServices(services) {
|
|
65
|
-
this.assertOpen();
|
|
66
48
|
if (this.services)
|
|
67
49
|
throw new Error("plugin host services are already bound");
|
|
68
50
|
this.services = services;
|
|
69
|
-
return this;
|
|
70
51
|
}
|
|
71
|
-
/** Cached, non-blocking runner/contribution health for the generic control API. */
|
|
72
|
-
statusSnapshot() {
|
|
73
|
-
const plugins = [];
|
|
74
|
-
for (const state of this.states.values()) {
|
|
75
|
-
if (state.runner)
|
|
76
|
-
void this.refreshRuntimeStatus(state).catch(() => undefined);
|
|
77
|
-
for (const desired of state.desired.values()) {
|
|
78
|
-
if (state.runner && desired.startedGeneration === state.generation) {
|
|
79
|
-
void this.refreshContributionStatus(state, desired).catch(() => undefined);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const contributions = [...state.desired.values()].map((desired) => ({
|
|
83
|
-
key: desired.key,
|
|
84
|
-
target: { ...desired.target },
|
|
85
|
-
desired: true,
|
|
86
|
-
...(desired.startedGeneration === undefined
|
|
87
|
-
? {}
|
|
88
|
-
: { generation: desired.startedGeneration }),
|
|
89
|
-
failureCount: desired.failureCount,
|
|
90
|
-
status: { ...desired.status },
|
|
91
|
-
}));
|
|
92
|
-
plugins.push({
|
|
93
|
-
pluginId: state.record.manifest.id,
|
|
94
|
-
runner: {
|
|
95
|
-
state: runnerSnapshotState(state, this.disposed),
|
|
96
|
-
generation: state.generation,
|
|
97
|
-
failureCount: state.failureCount,
|
|
98
|
-
...(state.lastRunnerError ? { lastError: state.lastRunnerError } : {}),
|
|
99
|
-
},
|
|
100
|
-
runtimeStatus: { ...state.runtimeStatus },
|
|
101
|
-
contributions,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
return { capturedAt: new Date().toISOString(), plugins };
|
|
105
|
-
}
|
|
106
|
-
/** Start one resident runner for every enabled runtime plugin. */
|
|
107
52
|
async startPluginRuntimes() {
|
|
108
|
-
this.
|
|
109
|
-
await Promise.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
53
|
+
const starts = [...this.states.values()].map((state) => this.runSerialized(state.record.canonicalId, () => this.startState(state)));
|
|
54
|
+
await Promise.allSettled(starts);
|
|
55
|
+
}
|
|
56
|
+
async ensurePluginRuntime(record) {
|
|
57
|
+
return await this.runSerialized(record.canonicalId, async () => {
|
|
58
|
+
const state = this.upsertState(record);
|
|
59
|
+
if (!record.enabled) {
|
|
60
|
+
await this.stopState(state);
|
|
61
|
+
return "stopped";
|
|
113
62
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
pluginId: state.record.manifest.id,
|
|
119
|
-
message: messageOf(error),
|
|
120
|
-
});
|
|
63
|
+
if (!record.manifest.runtime) {
|
|
64
|
+
state.state = "absent";
|
|
65
|
+
setPluginRuntimeState(record.canonicalId, "absent");
|
|
66
|
+
return "absent";
|
|
121
67
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* desired state. Concurrent reloads of the same plugin are applied in call
|
|
127
|
-
* order; other plugins continue running independently.
|
|
128
|
-
*/
|
|
129
|
-
reloadPluginRuntime(record, { readyTimeoutMs } = {}) {
|
|
130
|
-
this.assertReady();
|
|
131
|
-
validateRecord(record);
|
|
132
|
-
const pluginId = record.manifest.id;
|
|
133
|
-
return this.enqueueRuntimeOperation(pluginId, () => this.performPluginRuntimeReload(record, createRuntimeReadyDeadline(boundedReadyTimeout(readyTimeoutMs))));
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Make one enabled plugin runtime ready without replacing an already healthy
|
|
137
|
-
* runner. This is the idempotent operation plugin CLIs use when their durable
|
|
138
|
-
* configuration has not changed.
|
|
139
|
-
*/
|
|
140
|
-
ensurePluginRuntime(record, { readyTimeoutMs } = {}) {
|
|
141
|
-
this.assertReady();
|
|
142
|
-
validateRecord(record);
|
|
143
|
-
const pluginId = record.manifest.id;
|
|
144
|
-
return this.enqueueRuntimeOperation(pluginId, () => this.performPluginRuntimeEnsure(record, createRuntimeReadyDeadline(boundedReadyTimeout(readyTimeoutMs))));
|
|
68
|
+
if (!state.runner)
|
|
69
|
+
await this.startState(state);
|
|
70
|
+
return state.state;
|
|
71
|
+
});
|
|
145
72
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
73
|
+
/** Strict cutover: stop current generation, then start the supplied generation. */
|
|
74
|
+
async reloadPluginRuntime(record) {
|
|
75
|
+
return await this.runSerialized(record.canonicalId, async () => {
|
|
76
|
+
const state = this.upsertState(record);
|
|
77
|
+
await this.stopState(state);
|
|
78
|
+
await this.stopSettingsState(state);
|
|
79
|
+
state.record = record;
|
|
80
|
+
state.stopping = false;
|
|
81
|
+
if (!record.enabled || !record.manifest.runtime) {
|
|
82
|
+
state.state = record.manifest.runtime ? "stopped" : "absent";
|
|
83
|
+
setPluginRuntimeState(record.canonicalId, state.state);
|
|
84
|
+
return state.state;
|
|
154
85
|
}
|
|
155
|
-
|
|
156
|
-
|
|
86
|
+
await this.startState(state);
|
|
87
|
+
return state.state;
|
|
88
|
+
});
|
|
157
89
|
}
|
|
158
|
-
async
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
if (!state.runner) {
|
|
165
|
-
return this.performPluginRuntimeReload(record, deadline);
|
|
166
|
-
}
|
|
167
|
-
const status = await this.refreshRuntimeStatus(state, deadline);
|
|
168
|
-
if (status.state === "unconfigured" ||
|
|
169
|
-
status.state === "failed" ||
|
|
170
|
-
status.state === "stopped") {
|
|
171
|
-
return this.performPluginRuntimeReload(record, deadline);
|
|
172
|
-
}
|
|
173
|
-
// A starting or degraded runtime may be recovering its own transport. Do
|
|
174
|
-
// not destroy it (and any active task leases) merely because the user ran
|
|
175
|
-
// the same connect command twice; wait for its bounded readiness result.
|
|
176
|
-
return this.waitForRuntimeReady(state, deadline);
|
|
90
|
+
async stopPluginRuntime(identity) {
|
|
91
|
+
await this.runSerialized(identity, async () => {
|
|
92
|
+
const state = this.states.get(identity);
|
|
93
|
+
if (state)
|
|
94
|
+
await this.stopState(state);
|
|
95
|
+
});
|
|
177
96
|
}
|
|
178
|
-
async
|
|
179
|
-
this.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.cancelRestart(state);
|
|
196
|
-
state.record = record;
|
|
197
|
-
state.failureCount = 0;
|
|
198
|
-
state.lastRunnerError = undefined;
|
|
199
|
-
state.runtimeStatusRefresh = undefined;
|
|
200
|
-
for (const desired of state.desired.values()) {
|
|
201
|
-
desired.startedGeneration = undefined;
|
|
202
|
-
desired.status = { state: "starting", connected: false, lastError: null };
|
|
203
|
-
state.channelStatus.set(desired.key, {
|
|
204
|
-
...(state.channelStatus.get(desired.key) ?? {}),
|
|
205
|
-
connected: false,
|
|
206
|
-
lastError: null,
|
|
207
|
-
});
|
|
97
|
+
async changeEnabled(record, enabled, mutateRegistry) {
|
|
98
|
+
return await this.runSerialized(record.canonicalId, async () => {
|
|
99
|
+
const state = this.upsertState(record);
|
|
100
|
+
if (!enabled) {
|
|
101
|
+
await this.stopState(state);
|
|
102
|
+
let updated;
|
|
103
|
+
try {
|
|
104
|
+
updated = mutateRegistry();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
state.stopping = false;
|
|
108
|
+
if (state.record.enabled)
|
|
109
|
+
await this.startState(state);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
state.record = updated;
|
|
113
|
+
return state.state;
|
|
208
114
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
// ensureRunner already cached the failure and scheduled resident backoff.
|
|
217
|
-
// The bounded readiness loop remains authoritative and can observe a
|
|
218
|
-
// successful retry before the caller's timeout expires.
|
|
219
|
-
}
|
|
220
|
-
return this.waitForRuntimeReady(state, deadline);
|
|
115
|
+
const updated = mutateRegistry();
|
|
116
|
+
state.record = updated;
|
|
117
|
+
state.stopping = false;
|
|
118
|
+
await this.startState(state);
|
|
119
|
+
return state.state;
|
|
120
|
+
});
|
|
221
121
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
for (const state of this.states.values()) {
|
|
229
|
-
state.desired.clear();
|
|
230
|
-
for (const [key, status] of state.channelStatus) {
|
|
231
|
-
state.channelStatus.set(key, { ...status, connected: false });
|
|
122
|
+
async cutoverInstallation(identity, commitRegistry) {
|
|
123
|
+
return await this.runSerialized(identity, async () => {
|
|
124
|
+
const current = this.states.get(identity);
|
|
125
|
+
if (current) {
|
|
126
|
+
await this.stopState(current);
|
|
127
|
+
await this.stopSettingsState(current);
|
|
232
128
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
channelDescriptor(state, contribution) {
|
|
245
|
-
return {
|
|
246
|
-
type: contribution.type,
|
|
247
|
-
factory: (context) => this.createRemoteChannel(state, contribution, context),
|
|
248
|
-
...(contribution.configSchema ? { configSchema: contribution.configSchema } : {}),
|
|
249
|
-
...(contribution.authorize
|
|
250
|
-
? {
|
|
251
|
-
authorize: (emit, signal, context) => this.authorize(state, contribution, emit, signal, context),
|
|
129
|
+
let installed;
|
|
130
|
+
try {
|
|
131
|
+
installed = await commitRegistry();
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (current) {
|
|
135
|
+
current.stopping = false;
|
|
136
|
+
if (current.record.enabled)
|
|
137
|
+
await this.startState(current);
|
|
252
138
|
}
|
|
253
|
-
|
|
254
|
-
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
const state = this.upsertState(installed);
|
|
142
|
+
state.record = installed;
|
|
143
|
+
state.stopping = false;
|
|
144
|
+
if (installed.enabled)
|
|
145
|
+
await this.startState(state);
|
|
146
|
+
return state.state;
|
|
147
|
+
});
|
|
255
148
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
runtime: context.config.AGENT_RUNTIME,
|
|
273
|
-
...(context.config.RYNX_DEFAULT_AGENT
|
|
274
|
-
? { defaultAgent: context.config.RYNX_DEFAULT_AGENT }
|
|
275
|
-
: {}),
|
|
276
|
-
allowedRoots: resolveAllowedRoots(context.config),
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
return {
|
|
280
|
-
id: contribution.type,
|
|
281
|
-
start: async () => {
|
|
282
|
-
this.assertReady();
|
|
283
|
-
const existing = state.desired.get(key);
|
|
284
|
-
const desired = existing ?? this.rememberDesired(state, { key, request, target });
|
|
285
|
-
if (existing && !sameStartRequest(existing.request, request)) {
|
|
286
|
-
throw new Error(`plugin channel instance is already started with different config: ${instanceId}`);
|
|
287
|
-
}
|
|
288
|
-
await this.ensureDesiredStarted(state, desired);
|
|
289
|
-
},
|
|
290
|
-
stop: async () => {
|
|
291
|
-
const desired = state.desired.get(key);
|
|
292
|
-
if (!desired) {
|
|
293
|
-
state.channelStatus.set(key, { connected: false, lastError: null });
|
|
294
|
-
return;
|
|
149
|
+
async uninstallInstallation(identity, removeRegistry) {
|
|
150
|
+
return await this.runSerialized(identity, async () => {
|
|
151
|
+
const state = this.states.get(identity);
|
|
152
|
+
if (state) {
|
|
153
|
+
await this.stopState(state);
|
|
154
|
+
await this.stopSettingsState(state);
|
|
155
|
+
}
|
|
156
|
+
let removed;
|
|
157
|
+
try {
|
|
158
|
+
removed = await removeRegistry();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (state) {
|
|
162
|
+
state.stopping = false;
|
|
163
|
+
if (state.record.enabled)
|
|
164
|
+
await this.startState(state);
|
|
295
165
|
}
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
if (!removed && state) {
|
|
169
|
+
state.stopping = false;
|
|
170
|
+
if (state.record.enabled)
|
|
171
|
+
await this.startState(state);
|
|
172
|
+
}
|
|
173
|
+
if (removed)
|
|
174
|
+
this.states.delete(identity);
|
|
175
|
+
return removed;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async settingsCall(identity, expectedGeneration, method, params, options = {}) {
|
|
179
|
+
return await this.runSerialized(identity, async () => {
|
|
180
|
+
const state = this.states.get(identity);
|
|
181
|
+
if (!state)
|
|
182
|
+
throw new Error(`plugin "${identity}" is not installed`);
|
|
183
|
+
if (state.record.integrity !== expectedGeneration) {
|
|
184
|
+
throw new Error("plugin settings UI generation is stale");
|
|
185
|
+
}
|
|
186
|
+
const runner = await this.startSettingsState(state);
|
|
187
|
+
if (!runner.handshake.features.settings) {
|
|
188
|
+
throw new Error(`plugin "${identity}" does not advertise a settings background`);
|
|
189
|
+
}
|
|
190
|
+
const result = await runner.client.settingsCall(method, params, options);
|
|
191
|
+
await state.runner?.client.emitHostEvent("plugin.settings.changed", {
|
|
192
|
+
method,
|
|
193
|
+
...(params === undefined ? {} : { params }),
|
|
194
|
+
}).catch(() => undefined);
|
|
195
|
+
return result;
|
|
196
|
+
});
|
|
310
197
|
}
|
|
311
|
-
|
|
312
|
-
this.
|
|
313
|
-
const runner = await this.ensureRunner(state);
|
|
314
|
-
return runner.client.authorize({
|
|
315
|
-
kind: "chat-channel",
|
|
316
|
-
contributionId: contribution.type,
|
|
317
|
-
...(context?.instanceId ? { instanceId: context.instanceId } : {}),
|
|
318
|
-
config: clonePluginConfig(context?.config),
|
|
319
|
-
}, (material) => emit(material), { signal });
|
|
198
|
+
supportsSettings(identity) {
|
|
199
|
+
return Boolean(this.states.get(identity)?.record.manifest.settings);
|
|
320
200
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
...
|
|
324
|
-
|
|
325
|
-
|
|
201
|
+
statusSnapshot() {
|
|
202
|
+
return {
|
|
203
|
+
plugins: [...this.states.values()].map((state) => ({
|
|
204
|
+
pluginId: state.record.canonicalId,
|
|
205
|
+
generation: state.generation,
|
|
206
|
+
state: state.state,
|
|
207
|
+
...(state.health ? { health: structuredClone(state.health) } : {}),
|
|
208
|
+
...(state.error ? { error: state.error } : {}),
|
|
209
|
+
settings: Boolean(state.record.manifest.settings),
|
|
210
|
+
})),
|
|
326
211
|
};
|
|
327
|
-
state.desired.set(desired.key, desired);
|
|
328
|
-
if (desired.target.kind === "chat-channel") {
|
|
329
|
-
state.channelStatus.set(desired.key, { connected: false, lastError: null });
|
|
330
|
-
}
|
|
331
|
-
return desired;
|
|
332
212
|
}
|
|
333
|
-
async
|
|
334
|
-
|
|
335
|
-
if (state.desired.get(desired.key) !== desired) {
|
|
213
|
+
async dispose() {
|
|
214
|
+
if (this.disposed)
|
|
336
215
|
return;
|
|
337
|
-
|
|
338
|
-
await this.
|
|
339
|
-
|
|
216
|
+
this.disposed = true;
|
|
217
|
+
await Promise.allSettled([...this.states.values()].map((state) => this.runSerialized(state.record.canonicalId, async () => {
|
|
218
|
+
await this.stopState(state);
|
|
219
|
+
await this.stopSettingsState(state);
|
|
220
|
+
})));
|
|
221
|
+
}
|
|
222
|
+
upsertState(record) {
|
|
223
|
+
const current = this.states.get(record.canonicalId);
|
|
224
|
+
if (current) {
|
|
225
|
+
if (!current.runner && !current.settingsRunner)
|
|
226
|
+
current.record = record;
|
|
227
|
+
return current;
|
|
228
|
+
}
|
|
229
|
+
const state = {
|
|
230
|
+
record,
|
|
231
|
+
state: record.manifest.runtime ? "stopped" : "absent",
|
|
232
|
+
generation: 0,
|
|
233
|
+
restartFailures: 0,
|
|
234
|
+
stopping: false,
|
|
235
|
+
};
|
|
236
|
+
this.states.set(record.canonicalId, state);
|
|
237
|
+
return state;
|
|
340
238
|
}
|
|
341
|
-
async
|
|
342
|
-
|
|
343
|
-
if (desired.startedGeneration === generation)
|
|
239
|
+
async startState(state) {
|
|
240
|
+
if (this.disposed || state.runner || !state.record.enabled)
|
|
344
241
|
return;
|
|
345
|
-
|
|
346
|
-
|
|
242
|
+
const runtime = state.record.manifest.runtime;
|
|
243
|
+
if (!runtime) {
|
|
244
|
+
state.state = "absent";
|
|
245
|
+
setPluginRuntimeState(state.record.canonicalId, "absent");
|
|
347
246
|
return;
|
|
348
247
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
...(state.channelStatus.get(desired.key) ?? {}),
|
|
361
|
-
connected: state.channelStatus.get(desired.key)?.connected ?? false,
|
|
362
|
-
lastError: null,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}, (error) => {
|
|
366
|
-
if (state.runner === runner && state.desired.get(desired.key) === desired) {
|
|
367
|
-
this.markContributionFailed(state, desired, error);
|
|
368
|
-
}
|
|
369
|
-
throw error;
|
|
370
|
-
});
|
|
371
|
-
desired.starting = operation;
|
|
372
|
-
desired.startingGeneration = generation;
|
|
248
|
+
if (!this.services)
|
|
249
|
+
throw new Error("plugin host services are not bound");
|
|
250
|
+
state.stopping = false;
|
|
251
|
+
state.state = "starting";
|
|
252
|
+
state.error = undefined;
|
|
253
|
+
state.generation += 1;
|
|
254
|
+
setPluginRuntimeState(state.record.canonicalId, "starting");
|
|
255
|
+
const dataDir = pluginDataDir(state.record.marketplaceId, state.record.pluginId);
|
|
256
|
+
const cacheDir = pluginRuntimeCacheDir(state.record.marketplaceId, state.record.pluginId, state.record.integrity);
|
|
257
|
+
let runner;
|
|
258
|
+
let hostRpc;
|
|
373
259
|
try {
|
|
374
|
-
await
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
async stopDesired(state, desired) {
|
|
384
|
-
if (state.desired.get(desired.key) !== desired)
|
|
385
|
-
return;
|
|
386
|
-
state.desired.delete(desired.key);
|
|
387
|
-
state.channelStatus.set(desired.key, {
|
|
388
|
-
connected: false,
|
|
389
|
-
lastError: null,
|
|
390
|
-
lastEventAt: state.channelStatus.get(desired.key)?.lastEventAt ?? null,
|
|
391
|
-
});
|
|
392
|
-
let stopError;
|
|
393
|
-
const runner = state.runner;
|
|
394
|
-
if (runner) {
|
|
395
|
-
try {
|
|
396
|
-
await runner.client.stopContribution(desired.target);
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
stopError = error;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (stopError)
|
|
403
|
-
throw stopError;
|
|
404
|
-
}
|
|
405
|
-
async refreshContributionStatus(state, desired) {
|
|
406
|
-
if (desired.statusRefresh)
|
|
407
|
-
return desired.statusRefresh;
|
|
408
|
-
const runner = state.runner;
|
|
409
|
-
if (!runner || desired.startedGeneration !== state.generation)
|
|
410
|
-
return;
|
|
411
|
-
const refresh = runner.client.status(desired.target).then((status) => {
|
|
412
|
-
if (state.runner !== runner || state.desired.get(desired.key) !== desired)
|
|
413
|
-
return;
|
|
414
|
-
desired.status = boundedContributionStatus(status);
|
|
415
|
-
state.channelStatus.set(desired.key, statusToChannelStatus(desired.status));
|
|
416
|
-
}, (error) => {
|
|
417
|
-
if (state.runner !== runner || state.desired.get(desired.key) !== desired)
|
|
418
|
-
return;
|
|
419
|
-
desired.status = {
|
|
420
|
-
state: "degraded",
|
|
421
|
-
connected: false,
|
|
422
|
-
lastError: messageOf(error),
|
|
423
|
-
};
|
|
424
|
-
state.channelStatus.set(desired.key, {
|
|
425
|
-
...(state.channelStatus.get(desired.key) ?? {}),
|
|
426
|
-
connected: false,
|
|
427
|
-
lastError: messageOf(error),
|
|
260
|
+
await this.verifyPackage(state.record);
|
|
261
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
262
|
+
mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
|
|
263
|
+
writePluginRuntimeLease({
|
|
264
|
+
dataDir,
|
|
265
|
+
pluginId: state.record.pluginId,
|
|
266
|
+
leaseId: RUNTIME_LEASE_ID,
|
|
267
|
+
installPath: state.record.installPath,
|
|
428
268
|
});
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
state.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
269
|
+
hostRpc = new PluginHostRpc({
|
|
270
|
+
pluginId: state.record.pluginId,
|
|
271
|
+
principalId: state.record.canonicalId,
|
|
272
|
+
daemonInstanceId: this.daemonInstanceId,
|
|
273
|
+
capabilities: state.record.manifest.capabilities,
|
|
274
|
+
services: this.services,
|
|
275
|
+
...(state.record.pluginId === "workspace"
|
|
276
|
+
&& isBuiltInPluginMarketplace(state.record.marketplaceId)
|
|
277
|
+
? { managedWorkspaceRoot: dataDir }
|
|
278
|
+
: {}),
|
|
279
|
+
emit: async (event) => {
|
|
280
|
+
if (Buffer.byteLength(JSON.stringify(event), "utf8") > MAX_PLUGIN_HOST_EVENT_BYTES) {
|
|
281
|
+
throw new Error("plugin host event exceeds transport budget");
|
|
282
|
+
}
|
|
283
|
+
await runner?.client.emitHostEvent(event.event, event.payload);
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
state.hostRpc = hostRpc;
|
|
287
|
+
runner = await this.spawnRunner({
|
|
288
|
+
pluginId: state.record.pluginId,
|
|
289
|
+
pluginVersion: state.record.version,
|
|
290
|
+
installationId: state.record.canonicalId,
|
|
291
|
+
packageRoot: state.record.installPath,
|
|
292
|
+
...("entry" in runtime
|
|
293
|
+
? {
|
|
294
|
+
entry: runtime.entry,
|
|
295
|
+
}
|
|
296
|
+
: {
|
|
297
|
+
command: runtime.command,
|
|
298
|
+
args: runtime.args,
|
|
299
|
+
}),
|
|
300
|
+
role: "runtime",
|
|
301
|
+
dataDir,
|
|
302
|
+
cacheDir,
|
|
303
|
+
capabilities: state.record.manifest.capabilities,
|
|
304
|
+
handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
|
|
305
|
+
onStderr: (line) => this.log({
|
|
306
|
+
level: "info",
|
|
307
|
+
event: "plugin-stderr",
|
|
308
|
+
pluginId: state.record.canonicalId,
|
|
309
|
+
message: line,
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
state.runner = runner;
|
|
313
|
+
state.health = runner.initialHealth;
|
|
314
|
+
state.state = healthState(runner.initialHealth);
|
|
315
|
+
state.startedAt = Date.now();
|
|
316
|
+
state.error = undefined;
|
|
317
|
+
setPluginRuntimeState(state.record.canonicalId, state.state);
|
|
318
|
+
this.log({
|
|
319
|
+
level: "info",
|
|
320
|
+
event: "plugin-runtime-started",
|
|
321
|
+
pluginId: state.record.canonicalId,
|
|
322
|
+
});
|
|
323
|
+
this.scheduleHealthPoll(state, runner);
|
|
324
|
+
const generation = state.generation;
|
|
325
|
+
void runner.completion.then((exit) => {
|
|
326
|
+
void this.runSerialized(state.record.canonicalId, () => this.handleExit(state, runner, generation, exit));
|
|
327
|
+
}).catch((error) => {
|
|
328
|
+
void this.runSerialized(state.record.canonicalId, () => this.handleExit(state, runner, generation, {
|
|
329
|
+
code: null,
|
|
330
|
+
signal: null,
|
|
331
|
+
error,
|
|
332
|
+
}));
|
|
462
333
|
});
|
|
463
|
-
state.runtimeStatusRefresh = refresh;
|
|
464
|
-
void refresh.finally(() => {
|
|
465
|
-
if (state.runtimeStatusRefresh === refresh)
|
|
466
|
-
state.runtimeStatusRefresh = undefined;
|
|
467
|
-
}).catch(() => undefined);
|
|
468
334
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
this.assertOpen();
|
|
476
|
-
const remainingBeforeRefresh = deadline.expiresAt - Date.now();
|
|
477
|
-
if (remainingBeforeRefresh <= 0) {
|
|
478
|
-
throw this.runtimeReadyTimeoutError(state, deadline.timeoutMs);
|
|
479
|
-
}
|
|
480
|
-
const status = state.runner
|
|
481
|
-
? await this.refreshRuntimeStatus(state, deadline)
|
|
482
|
-
: state.runtimeStatus;
|
|
483
|
-
if (status.state === "running" && status.connected !== false)
|
|
484
|
-
return { ...status };
|
|
485
|
-
const remaining = deadline.expiresAt - Date.now();
|
|
486
|
-
if (remaining <= 0) {
|
|
487
|
-
throw this.runtimeReadyTimeoutError(state, deadline.timeoutMs);
|
|
488
|
-
}
|
|
489
|
-
await delay(Math.min(RUNTIME_STATUS_POLL_MS, remaining));
|
|
335
|
+
catch (error) {
|
|
336
|
+
await hostRpc?.dispose().catch(() => undefined);
|
|
337
|
+
state.hostRpc = undefined;
|
|
338
|
+
removePluginRuntimeLease(dataDir, RUNTIME_LEASE_ID);
|
|
339
|
+
this.failState(state, error);
|
|
340
|
+
this.scheduleRestart(state);
|
|
490
341
|
}
|
|
491
342
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return this.joinExitTransition(state).then(() => this.ensureRunner(state));
|
|
343
|
+
async startSettingsState(state) {
|
|
344
|
+
if (this.disposed)
|
|
345
|
+
throw new Error("plugin supervisor is disposed");
|
|
346
|
+
if (state.settingsRunner)
|
|
347
|
+
return state.settingsRunner;
|
|
348
|
+
const background = state.record.manifest.settings?.background;
|
|
349
|
+
if (!background) {
|
|
350
|
+
throw new Error(`plugin "${state.record.canonicalId}" has no settings background`);
|
|
501
351
|
}
|
|
502
|
-
if (
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
state.runtimeStatus = { state: "starting", connected: false, lastError: null };
|
|
507
|
-
const spawn = this.spawnAndBind(state).catch((error) => {
|
|
508
|
-
if (!this.disposed) {
|
|
509
|
-
state.failureCount += 1;
|
|
510
|
-
this.markRunnerUnavailable(state, `plugin runner failed to start: ${messageOf(error)}`);
|
|
511
|
-
this.scheduleRestart(state);
|
|
512
|
-
}
|
|
513
|
-
throw error;
|
|
514
|
-
});
|
|
515
|
-
state.spawnPromise = spawn;
|
|
516
|
-
void spawn.finally(() => {
|
|
517
|
-
if (state.spawnPromise === spawn)
|
|
518
|
-
state.spawnPromise = undefined;
|
|
519
|
-
}).catch(() => undefined);
|
|
520
|
-
return spawn;
|
|
521
|
-
}
|
|
522
|
-
async spawnAndBind(state) {
|
|
523
|
-
const pluginId = state.record.manifest.id;
|
|
524
|
-
const eventQueue = new BoundedHostEventQueue();
|
|
525
|
-
const host = new PluginHostRpc({
|
|
526
|
-
pluginId,
|
|
527
|
-
daemonInstanceId: this.daemonInstanceId,
|
|
528
|
-
grants: state.record.grants,
|
|
529
|
-
services: this.requireHostServices(),
|
|
530
|
-
emit: (event) => eventQueue.enqueue(event),
|
|
531
|
-
});
|
|
532
|
-
const stderr = this.stderrSink(pluginId);
|
|
352
|
+
if (!this.services)
|
|
353
|
+
throw new Error("plugin host services are not bound");
|
|
354
|
+
const dataDir = pluginDataDir(state.record.marketplaceId, state.record.pluginId);
|
|
355
|
+
const cacheDir = pluginRuntimeCacheDir(state.record.marketplaceId, state.record.pluginId, state.record.integrity);
|
|
533
356
|
let runner;
|
|
357
|
+
let hostRpc;
|
|
534
358
|
try {
|
|
359
|
+
await this.verifyPackage(state.record);
|
|
360
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
361
|
+
mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
|
|
362
|
+
writePluginRuntimeLease({
|
|
363
|
+
dataDir,
|
|
364
|
+
pluginId: state.record.pluginId,
|
|
365
|
+
leaseId: SETTINGS_LEASE_ID,
|
|
366
|
+
installPath: state.record.installPath,
|
|
367
|
+
});
|
|
368
|
+
hostRpc = new PluginHostRpc({
|
|
369
|
+
pluginId: state.record.pluginId,
|
|
370
|
+
principalId: state.record.canonicalId,
|
|
371
|
+
daemonInstanceId: this.daemonInstanceId,
|
|
372
|
+
capabilities: state.record.manifest.capabilities,
|
|
373
|
+
services: this.services,
|
|
374
|
+
...(state.record.pluginId === "workspace"
|
|
375
|
+
&& isBuiltInPluginMarketplace(state.record.marketplaceId)
|
|
376
|
+
? { managedWorkspaceRoot: dataDir }
|
|
377
|
+
: {}),
|
|
378
|
+
emit: async (event) => {
|
|
379
|
+
if (Buffer.byteLength(JSON.stringify(event), "utf8") > MAX_PLUGIN_HOST_EVENT_BYTES) {
|
|
380
|
+
throw new Error("plugin host event exceeds transport budget");
|
|
381
|
+
}
|
|
382
|
+
await runner?.client.emitHostEvent(event.event, event.payload);
|
|
383
|
+
},
|
|
384
|
+
});
|
|
535
385
|
runner = await this.spawnRunner({
|
|
536
|
-
pluginId,
|
|
386
|
+
pluginId: state.record.pluginId,
|
|
387
|
+
pluginVersion: state.record.version,
|
|
388
|
+
installationId: state.record.canonicalId,
|
|
537
389
|
packageRoot: state.record.installPath,
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
390
|
+
...("entry" in background
|
|
391
|
+
? {
|
|
392
|
+
entry: background.entry,
|
|
393
|
+
}
|
|
394
|
+
: {
|
|
395
|
+
command: background.command,
|
|
396
|
+
args: background.args,
|
|
397
|
+
}),
|
|
398
|
+
role: "settings",
|
|
399
|
+
dataDir,
|
|
400
|
+
cacheDir,
|
|
401
|
+
capabilities: state.record.manifest.capabilities,
|
|
402
|
+
handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
|
|
403
|
+
onStderr: (line) => this.log({
|
|
404
|
+
level: "info",
|
|
405
|
+
event: "plugin-stderr",
|
|
406
|
+
pluginId: state.record.canonicalId,
|
|
407
|
+
message: line,
|
|
408
|
+
}),
|
|
544
409
|
});
|
|
545
|
-
|
|
410
|
+
if (!runner.handshake.features.settings) {
|
|
411
|
+
throw new Error(`plugin "${state.record.canonicalId}" does not advertise a settings background`);
|
|
412
|
+
}
|
|
413
|
+
state.settingsRunner = runner;
|
|
414
|
+
state.settingsHostRpc = hostRpc;
|
|
415
|
+
this.log({
|
|
416
|
+
level: "info",
|
|
417
|
+
event: "plugin-settings-started",
|
|
418
|
+
pluginId: state.record.canonicalId,
|
|
419
|
+
});
|
|
420
|
+
void runner.completion.then((exit) => {
|
|
421
|
+
void this.runSerialized(state.record.canonicalId, () => this.handleSettingsExit(state, runner, hostRpc, exit));
|
|
422
|
+
}).catch((error) => {
|
|
423
|
+
void this.runSerialized(state.record.canonicalId, () => this.handleSettingsExit(state, runner, hostRpc, {
|
|
424
|
+
code: null,
|
|
425
|
+
signal: null,
|
|
426
|
+
error,
|
|
427
|
+
}));
|
|
428
|
+
});
|
|
429
|
+
return runner;
|
|
546
430
|
}
|
|
547
431
|
catch (error) {
|
|
548
|
-
eventQueue.close(error);
|
|
549
|
-
stderr.flush();
|
|
550
432
|
await runner?.close().catch(() => undefined);
|
|
551
|
-
await
|
|
433
|
+
await hostRpc?.dispose().catch(() => undefined);
|
|
434
|
+
removePluginRuntimeLease(dataDir, SETTINGS_LEASE_ID);
|
|
552
435
|
throw error;
|
|
553
436
|
}
|
|
554
|
-
if (this.disposed) {
|
|
555
|
-
eventQueue.close(new Error("plugin supervisor is disposed"));
|
|
556
|
-
stderr.flush();
|
|
557
|
-
await runner.close().catch(() => undefined);
|
|
558
|
-
await host.dispose();
|
|
559
|
-
throw new Error("plugin supervisor is disposed");
|
|
560
|
-
}
|
|
561
|
-
eventQueue.bind(runner.client);
|
|
562
|
-
state.runner = runner;
|
|
563
|
-
state.host = host;
|
|
564
|
-
state.eventQueue = eventQueue;
|
|
565
|
-
state.lastRunnerError = undefined;
|
|
566
|
-
state.generation += 1;
|
|
567
|
-
const generation = state.generation;
|
|
568
|
-
clearTimer(state.stableTimer);
|
|
569
|
-
state.stableTimer = setTimeout(() => {
|
|
570
|
-
if (state.runner === runner)
|
|
571
|
-
state.failureCount = 0;
|
|
572
|
-
}, this.stableResetMs);
|
|
573
|
-
state.stableTimer.unref?.();
|
|
574
|
-
void runner.completion.then((outcome) => {
|
|
575
|
-
eventQueue.close(new Error("plugin runner exited"));
|
|
576
|
-
stderr.flush();
|
|
577
|
-
this.trackRunnerExit(state, runner, host, generation, outcome);
|
|
578
|
-
}, (error) => {
|
|
579
|
-
eventQueue.close(error);
|
|
580
|
-
stderr.flush();
|
|
581
|
-
this.trackRunnerExit(state, runner, host, generation, undefined, error);
|
|
582
|
-
}).catch((error) => {
|
|
583
|
-
this.writeLog({
|
|
584
|
-
level: "error",
|
|
585
|
-
event: "plugin.runner.exit_handler_failed",
|
|
586
|
-
pluginId,
|
|
587
|
-
message: messageOf(error),
|
|
588
|
-
});
|
|
589
|
-
});
|
|
590
|
-
return runner;
|
|
591
437
|
}
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
438
|
+
async stopSettingsState(state) {
|
|
439
|
+
const runner = state.settingsRunner;
|
|
440
|
+
const hostRpc = state.settingsHostRpc;
|
|
441
|
+
state.settingsRunner = undefined;
|
|
442
|
+
state.settingsHostRpc = undefined;
|
|
443
|
+
await runner?.close().catch(() => undefined);
|
|
444
|
+
await hostRpc?.dispose().catch(() => undefined);
|
|
445
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), SETTINGS_LEASE_ID);
|
|
446
|
+
}
|
|
447
|
+
async handleSettingsExit(state, runner, hostRpc, exit) {
|
|
448
|
+
if (state.settingsRunner !== runner)
|
|
449
|
+
return;
|
|
450
|
+
state.settingsRunner = undefined;
|
|
451
|
+
if (state.settingsHostRpc === hostRpc)
|
|
452
|
+
state.settingsHostRpc = undefined;
|
|
453
|
+
await runner.terminateTree().catch(() => undefined);
|
|
454
|
+
await hostRpc.dispose().catch(() => undefined);
|
|
455
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), SETTINGS_LEASE_ID);
|
|
456
|
+
if (!this.disposed) {
|
|
457
|
+
this.log({
|
|
458
|
+
level: "warn",
|
|
459
|
+
event: "plugin-settings-exited",
|
|
460
|
+
pluginId: state.record.canonicalId,
|
|
461
|
+
message: exit.error instanceof Error
|
|
462
|
+
? exit.error.message
|
|
463
|
+
: `settings background exited with code ${exit.code ?? "null"}${exit.signal ? ` (${exit.signal})` : ""}`,
|
|
609
464
|
});
|
|
610
|
-
}
|
|
465
|
+
}
|
|
611
466
|
}
|
|
612
|
-
async
|
|
467
|
+
async stopState(state) {
|
|
468
|
+
state.stopping = true;
|
|
469
|
+
if (state.restartTimer)
|
|
470
|
+
clearTimeout(state.restartTimer);
|
|
471
|
+
state.restartTimer = undefined;
|
|
472
|
+
if (state.healthTimer)
|
|
473
|
+
clearTimeout(state.healthTimer);
|
|
474
|
+
state.healthTimer = undefined;
|
|
475
|
+
const runner = state.runner;
|
|
476
|
+
state.runner = undefined;
|
|
477
|
+
await runner?.close().catch(() => undefined);
|
|
478
|
+
await state.hostRpc?.dispose().catch(() => undefined);
|
|
479
|
+
state.hostRpc = undefined;
|
|
480
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
|
|
481
|
+
state.health = undefined;
|
|
482
|
+
state.error = undefined;
|
|
483
|
+
state.state = state.record.manifest.runtime ? "stopped" : "absent";
|
|
484
|
+
setPluginRuntimeState(state.record.canonicalId, state.state);
|
|
485
|
+
}
|
|
486
|
+
async handleExit(state, runner, generation, exit) {
|
|
613
487
|
if (state.runner !== runner || state.generation !== generation)
|
|
614
488
|
return;
|
|
489
|
+
if (state.healthTimer)
|
|
490
|
+
clearTimeout(state.healthTimer);
|
|
491
|
+
state.healthTimer = undefined;
|
|
615
492
|
state.runner = undefined;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
state.
|
|
619
|
-
state.
|
|
620
|
-
state.
|
|
621
|
-
clearTimer(state.stableTimer);
|
|
622
|
-
state.stableTimer = undefined;
|
|
623
|
-
await host.dispose();
|
|
624
|
-
// Reload, explicit start, and dispose all join this transition. Keep the
|
|
625
|
-
// generation guard as a final defence against any future call path that
|
|
626
|
-
// can bind a successor without waiting for the old host cleanup.
|
|
627
|
-
if (this.disposed || state.runner || state.generation !== generation)
|
|
493
|
+
await runner.terminateTree().catch(() => undefined);
|
|
494
|
+
await state.hostRpc?.dispose().catch(() => undefined);
|
|
495
|
+
state.hostRpc = undefined;
|
|
496
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
|
|
497
|
+
if (state.stopping || this.disposed)
|
|
628
498
|
return;
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
this.
|
|
633
|
-
this.writeLog({
|
|
634
|
-
level: outcome?.code === 0 && !error ? "warn" : "error",
|
|
635
|
-
event: "plugin.runner.exited",
|
|
636
|
-
pluginId: state.record.manifest.id,
|
|
637
|
-
message,
|
|
638
|
-
code: outcome?.code,
|
|
639
|
-
signal: outcome?.signal,
|
|
640
|
-
});
|
|
641
|
-
state.failureCount += 1;
|
|
499
|
+
const stable = state.startedAt && Date.now() - state.startedAt >= STABLE_RESET_MS;
|
|
500
|
+
if (stable)
|
|
501
|
+
state.restartFailures = 0;
|
|
502
|
+
this.failState(state, exit.error ?? new Error(`runtime exited with code ${exit.code ?? "null"}${exit.signal ? ` (${exit.signal})` : ""}`));
|
|
642
503
|
this.scheduleRestart(state);
|
|
643
504
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
505
|
+
failState(state, error) {
|
|
506
|
+
state.restartFailures += 1;
|
|
507
|
+
state.health = undefined;
|
|
508
|
+
state.state = "failed";
|
|
509
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
510
|
+
setPluginRuntimeState(state.record.canonicalId, "failed", state.error);
|
|
511
|
+
this.log({
|
|
512
|
+
level: "error",
|
|
513
|
+
event: "plugin-runtime-failed",
|
|
514
|
+
pluginId: state.record.canonicalId,
|
|
515
|
+
message: state.error,
|
|
516
|
+
});
|
|
651
517
|
}
|
|
652
518
|
scheduleRestart(state) {
|
|
653
|
-
if (this.disposed || state.restartTimer)
|
|
519
|
+
if (this.disposed || state.stopping || !state.record.enabled || state.restartTimer)
|
|
654
520
|
return;
|
|
655
|
-
const
|
|
656
|
-
const delayMs = Math.min(this.restartMaxMs, this.restartBaseMs * 2 ** exponent);
|
|
657
|
-
this.writeLog({
|
|
658
|
-
level: "warn",
|
|
659
|
-
event: "plugin.runner.restart_scheduled",
|
|
660
|
-
pluginId: state.record.manifest.id,
|
|
661
|
-
delayMs,
|
|
662
|
-
});
|
|
521
|
+
const delayMs = Math.min(this.restartMaxMs, this.restartBaseMs * 2 ** Math.max(0, state.restartFailures - 1));
|
|
663
522
|
state.restartTimer = setTimeout(() => {
|
|
664
523
|
state.restartTimer = undefined;
|
|
665
|
-
void this.
|
|
524
|
+
void this.runSerialized(state.record.canonicalId, () => this.startState(state));
|
|
666
525
|
}, delayMs);
|
|
667
526
|
state.restartTimer.unref?.();
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
try {
|
|
674
|
-
runner = await this.ensureRunner(state);
|
|
675
|
-
}
|
|
676
|
-
catch (error) {
|
|
677
|
-
this.writeLog({
|
|
678
|
-
level: "error",
|
|
679
|
-
event: "plugin.runner.restart_failed",
|
|
680
|
-
pluginId: state.record.manifest.id,
|
|
681
|
-
message: messageOf(error),
|
|
682
|
-
});
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
await this.refreshRuntimeStatus(state);
|
|
686
|
-
await this.restoreDesiredContributions(state, runner);
|
|
687
|
-
}
|
|
688
|
-
async restoreDesiredContributions(state, runner) {
|
|
689
|
-
for (const desired of [...state.desired.values()]) {
|
|
690
|
-
try {
|
|
691
|
-
await this.startDesiredOnRunner(state, desired, runner);
|
|
692
|
-
await this.refreshContributionStatus(state, desired);
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
this.writeLog({
|
|
696
|
-
level: "error",
|
|
697
|
-
event: "plugin.contribution.restore_failed",
|
|
698
|
-
pluginId: state.record.manifest.id,
|
|
699
|
-
message: messageOf(error),
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
async closeRunner(state) {
|
|
705
|
-
const runner = state.runner;
|
|
706
|
-
const host = state.host;
|
|
707
|
-
const eventQueue = state.eventQueue;
|
|
708
|
-
state.runner = undefined;
|
|
709
|
-
state.host = undefined;
|
|
710
|
-
state.eventQueue = undefined;
|
|
711
|
-
state.runtimeStatusRefresh = undefined;
|
|
712
|
-
eventQueue?.close(new Error("plugin runner is closing"));
|
|
713
|
-
clearTimer(state.stableTimer);
|
|
714
|
-
state.stableTimer = undefined;
|
|
715
|
-
if (runner) {
|
|
716
|
-
try {
|
|
717
|
-
await runner.close();
|
|
718
|
-
}
|
|
719
|
-
catch (error) {
|
|
720
|
-
this.writeLog({
|
|
721
|
-
level: "warn",
|
|
722
|
-
event: "plugin.runner.close_failed",
|
|
723
|
-
pluginId: state.record.manifest.id,
|
|
724
|
-
message: messageOf(error),
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
await host?.dispose();
|
|
729
|
-
}
|
|
730
|
-
async disposeState(state) {
|
|
731
|
-
await this.joinExitTransition(state);
|
|
732
|
-
const spawning = state.spawnPromise;
|
|
733
|
-
await this.closeRunner(state);
|
|
734
|
-
await spawning?.catch(() => undefined);
|
|
735
|
-
// A spawn can bind between the first close and the disposed check inside
|
|
736
|
-
// spawnAndBind. A final idempotent close makes dispose a true join point.
|
|
737
|
-
await this.closeRunner(state);
|
|
738
|
-
await this.joinExitTransition(state);
|
|
739
|
-
}
|
|
740
|
-
cancelRestart(state) {
|
|
741
|
-
clearTimer(state.restartTimer);
|
|
742
|
-
state.restartTimer = undefined;
|
|
743
|
-
}
|
|
744
|
-
markRunnerUnavailable(state, message) {
|
|
745
|
-
state.lastRunnerError = message;
|
|
746
|
-
state.runtimeStatus = {
|
|
747
|
-
state: "failed",
|
|
748
|
-
connected: false,
|
|
749
|
-
lastError: message,
|
|
750
|
-
};
|
|
751
|
-
for (const desired of state.desired.values()) {
|
|
752
|
-
desired.startedGeneration = undefined;
|
|
753
|
-
desired.status = {
|
|
754
|
-
state: "failed",
|
|
755
|
-
connected: false,
|
|
756
|
-
lastError: message,
|
|
757
|
-
};
|
|
758
|
-
state.channelStatus.set(desired.key, {
|
|
759
|
-
...(state.channelStatus.get(desired.key) ?? {}),
|
|
760
|
-
connected: false,
|
|
761
|
-
lastError: message,
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
markContributionFailed(state, desired, error) {
|
|
766
|
-
desired.startedGeneration = undefined;
|
|
767
|
-
desired.failureCount += 1;
|
|
768
|
-
desired.status = {
|
|
769
|
-
state: "failed",
|
|
770
|
-
connected: false,
|
|
771
|
-
lastError: messageOf(error),
|
|
772
|
-
};
|
|
773
|
-
state.channelStatus.set(desired.key, {
|
|
774
|
-
...(state.channelStatus.get(desired.key) ?? {}),
|
|
775
|
-
connected: false,
|
|
776
|
-
lastError: messageOf(error),
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
stderrSink(pluginId) {
|
|
780
|
-
let buffered = "";
|
|
781
|
-
const continuation = " …[continued]";
|
|
782
|
-
const contentLimit = MAX_STDERR_LINE_BYTES - Buffer.byteLength(continuation, "utf8");
|
|
783
|
-
const emitLine = (line) => {
|
|
784
|
-
const message = line.trimEnd();
|
|
785
|
-
if (!message)
|
|
786
|
-
return;
|
|
787
|
-
this.writeLog({
|
|
788
|
-
level: "warn",
|
|
789
|
-
event: "plugin.runner.stderr",
|
|
790
|
-
pluginId,
|
|
791
|
-
message,
|
|
792
|
-
});
|
|
793
|
-
};
|
|
794
|
-
const appendSegment = (segment) => {
|
|
795
|
-
let remaining = segment;
|
|
796
|
-
while (remaining.length > 0) {
|
|
797
|
-
const bufferedBytes = Buffer.byteLength(buffered, "utf8");
|
|
798
|
-
const budget = Math.max(1, contentLimit - bufferedBytes);
|
|
799
|
-
const [prefix, rest] = splitUtf8AtMost(remaining, budget);
|
|
800
|
-
if (!prefix && rest) {
|
|
801
|
-
if (buffered) {
|
|
802
|
-
emitLine(`${buffered}${continuation}`);
|
|
803
|
-
buffered = "";
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
806
|
-
// `contentLimit` is much larger than a single UTF-8 code point, so
|
|
807
|
-
// this branch is defensive only.
|
|
808
|
-
const first = String.fromCodePoint(remaining.codePointAt(0));
|
|
809
|
-
buffered = first;
|
|
810
|
-
remaining = remaining.slice(first.length);
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
buffered += prefix;
|
|
814
|
-
remaining = rest;
|
|
815
|
-
if (remaining.length > 0 || Buffer.byteLength(buffered, "utf8") >= contentLimit) {
|
|
816
|
-
emitLine(`${buffered}${continuation}`);
|
|
817
|
-
buffered = "";
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
return {
|
|
822
|
-
write: (chunk) => {
|
|
823
|
-
let offset = 0;
|
|
824
|
-
while (offset < chunk.length) {
|
|
825
|
-
const newline = chunk.indexOf("\n", offset);
|
|
826
|
-
if (newline < 0) {
|
|
827
|
-
appendSegment(chunk.slice(offset));
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
appendSegment(chunk.slice(offset, newline));
|
|
831
|
-
emitLine(buffered);
|
|
832
|
-
buffered = "";
|
|
833
|
-
offset = newline + 1;
|
|
834
|
-
}
|
|
835
|
-
},
|
|
836
|
-
flush: () => {
|
|
837
|
-
if (buffered)
|
|
838
|
-
emitLine(buffered);
|
|
839
|
-
buffered = "";
|
|
840
|
-
},
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
writeLog(entry) {
|
|
844
|
-
if (this.logger) {
|
|
845
|
-
this.logger(entry);
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
console.error(JSON.stringify({ timestamp: new Date().toISOString(), ...entry }));
|
|
849
|
-
}
|
|
850
|
-
assertOpen() {
|
|
851
|
-
if (this.disposed)
|
|
852
|
-
throw new Error("plugin supervisor is disposed");
|
|
853
|
-
}
|
|
854
|
-
assertReady() {
|
|
855
|
-
this.assertOpen();
|
|
856
|
-
this.requireHostServices();
|
|
857
|
-
}
|
|
858
|
-
requireHostServices() {
|
|
859
|
-
if (!this.services)
|
|
860
|
-
throw new Error("plugin host services are not bound");
|
|
861
|
-
return this.services;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
/** A single-writer queue: bounded buffering plus an awaitable delivery ack. */
|
|
865
|
-
class BoundedHostEventQueue {
|
|
866
|
-
items = [];
|
|
867
|
-
client;
|
|
868
|
-
queuedBytes = 0;
|
|
869
|
-
draining = false;
|
|
870
|
-
closed = false;
|
|
871
|
-
closeError;
|
|
872
|
-
bind(client) {
|
|
873
|
-
if (this.client)
|
|
874
|
-
throw new Error("plugin host event queue is already bound");
|
|
875
|
-
if (this.closed)
|
|
876
|
-
throw this.closeError ?? new Error("plugin host event queue is closed");
|
|
877
|
-
this.client = client;
|
|
878
|
-
void this.drain();
|
|
879
|
-
}
|
|
880
|
-
enqueue(event) {
|
|
881
|
-
if (this.closed) {
|
|
882
|
-
return Promise.reject(this.closeError ?? new Error("plugin host event queue is closed"));
|
|
883
|
-
}
|
|
884
|
-
let bytes;
|
|
885
|
-
try {
|
|
886
|
-
bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
887
|
-
}
|
|
888
|
-
catch {
|
|
889
|
-
return Promise.reject(new Error("plugin host event must be JSON serializable"));
|
|
890
|
-
}
|
|
891
|
-
if (bytes > MAX_PLUGIN_HOST_EVENT_BYTES) {
|
|
892
|
-
return Promise.reject(new Error("plugin host event exceeds the bounded event budget"));
|
|
893
|
-
}
|
|
894
|
-
if (this.items.length + (this.draining ? 1 : 0) >= MAX_HOST_EVENT_QUEUE_ITEMS ||
|
|
895
|
-
this.queuedBytes + bytes > MAX_HOST_EVENT_QUEUE_BYTES) {
|
|
896
|
-
return Promise.reject(new Error("plugin host event queue is full"));
|
|
897
|
-
}
|
|
898
|
-
const promise = new Promise((resolve, reject) => {
|
|
899
|
-
this.items.push({ event, bytes, resolve, reject });
|
|
900
|
-
this.queuedBytes += bytes;
|
|
527
|
+
this.log({
|
|
528
|
+
level: "warn",
|
|
529
|
+
event: "plugin-runtime-restart-scheduled",
|
|
530
|
+
pluginId: state.record.canonicalId,
|
|
531
|
+
delayMs,
|
|
901
532
|
});
|
|
902
|
-
void this.drain();
|
|
903
|
-
return promise;
|
|
904
533
|
}
|
|
905
|
-
|
|
906
|
-
if (this.
|
|
534
|
+
scheduleHealthPoll(state, runner) {
|
|
535
|
+
if (this.disposed || state.stopping || state.runner !== runner || state.healthTimer)
|
|
907
536
|
return;
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
this.queuedBytes -= item.bytes;
|
|
914
|
-
item.reject(this.closeError);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
async drain() {
|
|
918
|
-
if (this.draining || this.closed || !this.client)
|
|
919
|
-
return;
|
|
920
|
-
this.draining = true;
|
|
921
|
-
try {
|
|
922
|
-
while (!this.closed && this.client && this.items.length > 0) {
|
|
923
|
-
const item = this.items.shift();
|
|
537
|
+
state.healthTimer = setTimeout(() => {
|
|
538
|
+
state.healthTimer = undefined;
|
|
539
|
+
void this.runSerialized(state.record.canonicalId, async () => {
|
|
540
|
+
if (state.runner !== runner || state.stopping || this.disposed)
|
|
541
|
+
return;
|
|
924
542
|
try {
|
|
925
|
-
await this.
|
|
926
|
-
|
|
543
|
+
await this.refreshHealth(state);
|
|
544
|
+
this.scheduleHealthPoll(state, runner);
|
|
927
545
|
}
|
|
928
546
|
catch (error) {
|
|
929
|
-
|
|
930
|
-
item.reject(failure);
|
|
931
|
-
this.queuedBytes -= item.bytes;
|
|
932
|
-
this.close(failure);
|
|
933
|
-
return;
|
|
547
|
+
await this.failRunningState(state, runner, error);
|
|
934
548
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
finally {
|
|
939
|
-
this.draining = false;
|
|
940
|
-
if (!this.closed && this.client && this.items.length > 0)
|
|
941
|
-
void this.drain();
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
/** Convenience export for composition roots that only need the static catalog. */
|
|
946
|
-
export function createChannelTypeDescriptors(supervisor) {
|
|
947
|
-
return supervisor.createChannelTypeDescriptors();
|
|
948
|
-
}
|
|
949
|
-
function createPluginState(record) {
|
|
950
|
-
return {
|
|
951
|
-
record,
|
|
952
|
-
desired: new Map(),
|
|
953
|
-
channelStatus: new Map(),
|
|
954
|
-
generation: 0,
|
|
955
|
-
failureCount: 0,
|
|
956
|
-
runtimeStatus: { state: "stopped", connected: false },
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
function validateRecord(record) {
|
|
960
|
-
if (record.name !== record.manifest.id) {
|
|
961
|
-
throw new Error(`runnable plugin id mismatch: ${record.name} != ${record.manifest.id}`);
|
|
962
|
-
}
|
|
963
|
-
if (!record.enabled) {
|
|
964
|
-
throw new Error(`plugin ${record.manifest.id} is disabled`);
|
|
549
|
+
});
|
|
550
|
+
}, this.healthPollMs);
|
|
551
|
+
state.healthTimer.unref?.();
|
|
965
552
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (!
|
|
969
|
-
throw new Error(`plugin ${record.
|
|
553
|
+
async refreshHealth(state) {
|
|
554
|
+
const runner = state.runner;
|
|
555
|
+
if (!runner) {
|
|
556
|
+
throw new Error(`plugin runtime "${state.record.canonicalId}" is not running`);
|
|
970
557
|
}
|
|
558
|
+
const health = await runner.client.health();
|
|
559
|
+
if (state.runner !== runner)
|
|
560
|
+
return;
|
|
561
|
+
const nextState = healthState(health);
|
|
562
|
+
const changed = state.state !== nextState ||
|
|
563
|
+
state.error !== undefined ||
|
|
564
|
+
JSON.stringify(state.health) !== JSON.stringify(health);
|
|
565
|
+
state.health = health;
|
|
566
|
+
state.state = nextState;
|
|
567
|
+
state.error = undefined;
|
|
568
|
+
if (changed)
|
|
569
|
+
setPluginRuntimeState(state.record.canonicalId, state.state);
|
|
570
|
+
}
|
|
571
|
+
async failRunningState(state, runner, error) {
|
|
572
|
+
if (state.runner !== runner)
|
|
573
|
+
return;
|
|
574
|
+
state.runner = undefined;
|
|
575
|
+
if (state.healthTimer)
|
|
576
|
+
clearTimeout(state.healthTimer);
|
|
577
|
+
state.healthTimer = undefined;
|
|
578
|
+
await runner.close().catch(() => undefined);
|
|
579
|
+
await state.hostRpc?.dispose().catch(() => undefined);
|
|
580
|
+
state.hostRpc = undefined;
|
|
581
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
|
|
582
|
+
if (state.stopping || this.disposed)
|
|
583
|
+
return;
|
|
584
|
+
this.failState(state, error);
|
|
585
|
+
this.scheduleRestart(state);
|
|
971
586
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
throw new Error(`plugin runner identity mismatch: ${handshake.pluginId} != ${pluginId}`);
|
|
982
|
-
}
|
|
983
|
-
if (handshake.apiVersion !== PLUGIN_RUNNER_API_VERSION) {
|
|
984
|
-
throw new Error(`plugin runner API mismatch: ${handshake.apiVersion} != ${PLUGIN_RUNNER_API_VERSION}`);
|
|
985
|
-
}
|
|
986
|
-
if (handshake.grantedCapabilities.length !== grants.length ||
|
|
987
|
-
handshake.grantedCapabilities.some((capability, index) => capability !== grants[index])) {
|
|
988
|
-
throw new Error(`plugin runner capability handshake mismatch: ${pluginId}`);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
function statusToChannelStatus(status) {
|
|
992
|
-
return {
|
|
993
|
-
connected: status.connected ?? status.state === "running",
|
|
994
|
-
lastError: status.lastError ?? null,
|
|
995
|
-
lastEventAt: status.lastEventAt ?? null,
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
function boundedContributionStatus(status) {
|
|
999
|
-
return {
|
|
1000
|
-
...status,
|
|
1001
|
-
...(typeof status.lastError === "string"
|
|
1002
|
-
? { lastError: boundedText(status.lastError, MAX_STATUS_ERROR_BYTES) }
|
|
1003
|
-
: {}),
|
|
1004
|
-
...(typeof status.lastEventAt === "string"
|
|
1005
|
-
? { lastEventAt: boundedText(status.lastEventAt, 256) }
|
|
1006
|
-
: {}),
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
function boundedRuntimeStatus(status) {
|
|
1010
|
-
return {
|
|
1011
|
-
...status,
|
|
1012
|
-
...(typeof status.lastError === "string"
|
|
1013
|
-
? { lastError: boundedText(status.lastError, MAX_STATUS_ERROR_BYTES) }
|
|
1014
|
-
: {}),
|
|
1015
|
-
...(typeof status.lastEventAt === "string"
|
|
1016
|
-
? { lastEventAt: boundedText(status.lastEventAt, 256) }
|
|
1017
|
-
: {}),
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
function channelKey(contributionId, instanceId) {
|
|
1021
|
-
return `channel:${contributionId}:${instanceId}`;
|
|
1022
|
-
}
|
|
1023
|
-
function positiveDelay(value, fallback) {
|
|
1024
|
-
return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
1025
|
-
}
|
|
1026
|
-
function boundedReadyTimeout(value) {
|
|
1027
|
-
if (value === undefined)
|
|
1028
|
-
return DEFAULT_READY_TIMEOUT_MS;
|
|
1029
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
1030
|
-
throw new Error("plugin runtime ready timeout must be a non-negative finite number");
|
|
1031
|
-
}
|
|
1032
|
-
return Math.min(value, MAX_READY_TIMEOUT_MS);
|
|
1033
|
-
}
|
|
1034
|
-
function createRuntimeReadyDeadline(timeoutMs) {
|
|
1035
|
-
return { timeoutMs, expiresAt: Date.now() + timeoutMs };
|
|
1036
|
-
}
|
|
1037
|
-
function settleBeforeDeadline(promise, expiresAt, timeoutError) {
|
|
1038
|
-
const remaining = expiresAt - Date.now();
|
|
1039
|
-
if (remaining <= 0)
|
|
1040
|
-
return Promise.reject(timeoutError());
|
|
1041
|
-
return new Promise((resolve, reject) => {
|
|
1042
|
-
const timer = setTimeout(() => reject(timeoutError()), remaining);
|
|
1043
|
-
promise.then((value) => {
|
|
1044
|
-
clearTimeout(timer);
|
|
1045
|
-
resolve(value);
|
|
1046
|
-
}, (error) => {
|
|
1047
|
-
clearTimeout(timer);
|
|
1048
|
-
reject(error);
|
|
1049
|
-
});
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
function delay(milliseconds) {
|
|
1053
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1054
|
-
}
|
|
1055
|
-
function clearTimer(timer) {
|
|
1056
|
-
if (timer)
|
|
1057
|
-
clearTimeout(timer);
|
|
1058
|
-
}
|
|
1059
|
-
function runnerSnapshotState(state, disposed) {
|
|
1060
|
-
if (state.runner)
|
|
1061
|
-
return "running";
|
|
1062
|
-
if (state.spawnPromise)
|
|
1063
|
-
return "starting";
|
|
1064
|
-
if (state.restartTimer)
|
|
1065
|
-
return "backoff";
|
|
1066
|
-
if (state.lastRunnerError && !disposed)
|
|
1067
|
-
return "failed";
|
|
1068
|
-
return "stopped";
|
|
1069
|
-
}
|
|
1070
|
-
function splitUtf8AtMost(value, maxBytes) {
|
|
1071
|
-
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
1072
|
-
return [value, ""];
|
|
1073
|
-
let low = 0;
|
|
1074
|
-
let high = value.length;
|
|
1075
|
-
while (low < high) {
|
|
1076
|
-
const middle = Math.ceil((low + high) / 2);
|
|
1077
|
-
if (Buffer.byteLength(value.slice(0, middle), "utf8") <= maxBytes)
|
|
1078
|
-
low = middle;
|
|
1079
|
-
else
|
|
1080
|
-
high = middle - 1;
|
|
587
|
+
runSerialized(identity, operation) {
|
|
588
|
+
const previous = this.queues.get(identity) ?? Promise.resolve();
|
|
589
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
590
|
+
this.queues.set(identity, current);
|
|
591
|
+
void current.finally(() => {
|
|
592
|
+
if (this.queues.get(identity) === current)
|
|
593
|
+
this.queues.delete(identity);
|
|
594
|
+
}).catch(() => undefined);
|
|
595
|
+
return current;
|
|
1081
596
|
}
|
|
1082
|
-
if (low > 0 && low < value.length && isHighSurrogate(value.charCodeAt(low - 1)))
|
|
1083
|
-
low -= 1;
|
|
1084
|
-
return [value.slice(0, low), value.slice(low)];
|
|
1085
|
-
}
|
|
1086
|
-
function isHighSurrogate(value) {
|
|
1087
|
-
return value >= 0xd800 && value <= 0xdbff;
|
|
1088
|
-
}
|
|
1089
|
-
function messageOf(error) {
|
|
1090
|
-
return boundedText(error instanceof Error ? error.message : String(error), MAX_STATUS_ERROR_BYTES);
|
|
1091
|
-
}
|
|
1092
|
-
function boundedText(value, maxBytes) {
|
|
1093
|
-
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
1094
|
-
return value;
|
|
1095
|
-
const marker = "…[truncated]";
|
|
1096
|
-
const budget = maxBytes - Buffer.byteLength(marker, "utf8");
|
|
1097
|
-
const [prefix] = splitUtf8AtMost(value, Math.max(1, budget));
|
|
1098
|
-
return `${prefix}${marker}`;
|
|
1099
597
|
}
|
|
1100
|
-
function
|
|
1101
|
-
return
|
|
598
|
+
function healthState(health) {
|
|
599
|
+
return health.state;
|
|
1102
600
|
}
|
|
1103
|
-
function
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
function normalizeChatChannelConfig(contribution, value) {
|
|
1109
|
-
const config = clonePluginConfig(value);
|
|
1110
|
-
const schema = contribution.configSchema ?? [];
|
|
1111
|
-
return schema.length > 0 ? normalizePluginConfig(schema, config) : config;
|
|
1112
|
-
}
|
|
1113
|
-
function cloneJsonValue(value, pathLabel, ancestors) {
|
|
1114
|
-
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
1115
|
-
return value;
|
|
1116
|
-
if (typeof value === "number") {
|
|
1117
|
-
if (!Number.isFinite(value))
|
|
1118
|
-
throw new Error(`${pathLabel} must contain finite numbers`);
|
|
1119
|
-
return value;
|
|
1120
|
-
}
|
|
1121
|
-
if (typeof value !== "object") {
|
|
1122
|
-
throw new Error(`${pathLabel} must contain only JSON values`);
|
|
1123
|
-
}
|
|
1124
|
-
if (ancestors.has(value))
|
|
1125
|
-
throw new Error(`${pathLabel} must not be cyclic`);
|
|
1126
|
-
ancestors.add(value);
|
|
1127
|
-
try {
|
|
1128
|
-
if (Array.isArray(value)) {
|
|
1129
|
-
return value.map((item, index) => cloneJsonValue(item, `${pathLabel}[${index}]`, ancestors));
|
|
1130
|
-
}
|
|
1131
|
-
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
1132
|
-
throw new Error(`${pathLabel} must contain only plain objects`);
|
|
1133
|
-
}
|
|
1134
|
-
const output = {};
|
|
1135
|
-
for (const [key, item] of Object.entries(value)) {
|
|
1136
|
-
output[key] = cloneJsonValue(item, `${pathLabel}.${key}`, ancestors);
|
|
1137
|
-
}
|
|
1138
|
-
return output;
|
|
1139
|
-
}
|
|
1140
|
-
finally {
|
|
1141
|
-
ancestors.delete(value);
|
|
601
|
+
async function verifyInstalledPackage(record) {
|
|
602
|
+
const integrity = await validatePluginPackageSnapshot(record.installPath);
|
|
603
|
+
if (integrity !== record.integrity) {
|
|
604
|
+
throw new Error(`plugin package integrity changed for "${record.canonicalId}"`);
|
|
1142
605
|
}
|
|
1143
606
|
}
|