@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
|
@@ -1,1143 +1,791 @@
|
|
|
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;
|
|
26
|
+
productVersion;
|
|
27
|
+
cliVersion;
|
|
31
28
|
restartBaseMs;
|
|
32
29
|
restartMaxMs;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
healthPollMs;
|
|
31
|
+
verifyPackage;
|
|
32
|
+
services;
|
|
36
33
|
disposed = false;
|
|
37
|
-
|
|
34
|
+
disposePromise;
|
|
35
|
+
constructor(records, options = {}) {
|
|
38
36
|
this.services = options.services;
|
|
39
37
|
this.spawnRunner = options.spawnRunner ?? spawnPluginRunner;
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this.
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
this.log = options.log ?? (() => { });
|
|
39
|
+
this.daemonInstanceId = options.daemonInstanceId ?? randomUUID();
|
|
40
|
+
this.productVersion = options.productVersion ?? "development";
|
|
41
|
+
this.cliVersion = options.cliVersion ?? "development";
|
|
42
|
+
this.restartBaseMs = options.restartBaseMs ?? DEFAULT_RESTART_BASE_MS;
|
|
43
|
+
this.restartMaxMs = Math.min(options.restartMaxMs ?? MAX_RESTART_DELAY_MS, MAX_RESTART_DELAY_MS);
|
|
44
|
+
this.healthPollMs = options.healthPollMs ?? DEFAULT_HEALTH_POLL_MS;
|
|
45
|
+
this.verifyPackage = options.verifyPackage ?? verifyInstalledPackage;
|
|
46
|
+
if (!Number.isSafeInteger(this.healthPollMs) || this.healthPollMs <= 0) {
|
|
47
|
+
throw new Error("plugin health poll interval must be a positive safe integer");
|
|
48
|
+
}
|
|
49
|
+
for (const record of records)
|
|
50
|
+
this.upsertState(record);
|
|
52
51
|
}
|
|
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;
|
|
62
|
-
}
|
|
63
|
-
/** Bind the trusted host once, after the server composition root constructs it. */
|
|
64
52
|
bindHostServices(services) {
|
|
65
|
-
this.assertOpen();
|
|
66
53
|
if (this.services)
|
|
67
54
|
throw new Error("plugin host services are already bound");
|
|
68
55
|
this.services = services;
|
|
69
|
-
return this;
|
|
70
56
|
}
|
|
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
57
|
async startPluginRuntimes() {
|
|
108
|
-
this.
|
|
109
|
-
await Promise.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
58
|
+
const starts = [...this.states.values()].map((state) => this.runSerialized(state.record.canonicalId, () => this.startState(state)));
|
|
59
|
+
await Promise.allSettled(starts);
|
|
60
|
+
}
|
|
61
|
+
async ensurePluginRuntime(record) {
|
|
62
|
+
return await this.runSerialized(record.canonicalId, async () => {
|
|
63
|
+
const state = this.upsertState(record);
|
|
64
|
+
if (!record.enabled) {
|
|
65
|
+
await this.stopState(state);
|
|
66
|
+
return "stopped";
|
|
113
67
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
pluginId: state.record.manifest.id,
|
|
119
|
-
message: messageOf(error),
|
|
120
|
-
});
|
|
68
|
+
if (!record.manifest.runtime) {
|
|
69
|
+
state.state = "absent";
|
|
70
|
+
setPluginRuntimeState(record.canonicalId, "absent");
|
|
71
|
+
return "absent";
|
|
121
72
|
}
|
|
122
|
-
|
|
73
|
+
if (!state.runner)
|
|
74
|
+
await this.startState(state);
|
|
75
|
+
return state.state;
|
|
76
|
+
});
|
|
123
77
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
78
|
+
/** Strict cutover: stop current generation, then start the supplied generation. */
|
|
79
|
+
async reloadPluginRuntime(record) {
|
|
80
|
+
return await this.runSerialized(record.canonicalId, async () => {
|
|
81
|
+
const state = this.upsertState(record);
|
|
82
|
+
await this.stopState(state);
|
|
83
|
+
await this.stopSettingsState(state);
|
|
84
|
+
state.record = record;
|
|
85
|
+
state.stopping = false;
|
|
86
|
+
if (!record.enabled || !record.manifest.runtime) {
|
|
87
|
+
state.state = record.manifest.runtime ? "stopped" : "absent";
|
|
88
|
+
setPluginRuntimeState(record.canonicalId, state.state);
|
|
89
|
+
return state.state;
|
|
90
|
+
}
|
|
91
|
+
await this.startState(state);
|
|
92
|
+
return state.state;
|
|
93
|
+
});
|
|
134
94
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.assertReady();
|
|
142
|
-
validateRecord(record);
|
|
143
|
-
const pluginId = record.manifest.id;
|
|
144
|
-
return this.enqueueRuntimeOperation(pluginId, () => this.performPluginRuntimeEnsure(record, createRuntimeReadyDeadline(boundedReadyTimeout(readyTimeoutMs))));
|
|
95
|
+
async stopPluginRuntime(identity) {
|
|
96
|
+
await this.runSerialized(identity, async () => {
|
|
97
|
+
const state = this.states.get(identity);
|
|
98
|
+
if (state)
|
|
99
|
+
await this.stopState(state);
|
|
100
|
+
});
|
|
145
101
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
102
|
+
async changeEnabled(record, enabled, mutateRegistry) {
|
|
103
|
+
return await this.runSerialized(record.canonicalId, async () => {
|
|
104
|
+
const state = this.upsertState(record);
|
|
105
|
+
if (!enabled) {
|
|
106
|
+
await this.stopState(state);
|
|
107
|
+
let updated;
|
|
108
|
+
try {
|
|
109
|
+
updated = mutateRegistry();
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
state.stopping = false;
|
|
113
|
+
if (state.record.enabled)
|
|
114
|
+
await this.startState(state);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
state.record = updated;
|
|
118
|
+
return state.state;
|
|
154
119
|
}
|
|
155
|
-
|
|
156
|
-
|
|
120
|
+
const updated = mutateRegistry();
|
|
121
|
+
state.record = updated;
|
|
122
|
+
state.stopping = false;
|
|
123
|
+
await this.startState(state);
|
|
124
|
+
return state.state;
|
|
125
|
+
});
|
|
157
126
|
}
|
|
158
|
-
async
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
127
|
+
async cutoverInstallation(identity, commitRegistry) {
|
|
128
|
+
return await this.runSerialized(identity, async () => {
|
|
129
|
+
const current = this.states.get(identity);
|
|
130
|
+
if (current) {
|
|
131
|
+
await this.stopState(current);
|
|
132
|
+
await this.stopSettingsState(current);
|
|
133
|
+
}
|
|
134
|
+
let installed;
|
|
135
|
+
try {
|
|
136
|
+
installed = await commitRegistry();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (current) {
|
|
140
|
+
current.stopping = false;
|
|
141
|
+
if (current.record.enabled)
|
|
142
|
+
await this.startState(current);
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
const state = this.upsertState(installed);
|
|
147
|
+
state.record = installed;
|
|
148
|
+
state.stopping = false;
|
|
149
|
+
if (installed.enabled)
|
|
150
|
+
await this.startState(state);
|
|
151
|
+
return state.state;
|
|
152
|
+
});
|
|
177
153
|
}
|
|
178
|
-
async
|
|
179
|
-
this.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.states.set(pluginId, state);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
await this.joinExitTransition(state);
|
|
188
|
-
const spawning = state.spawnPromise;
|
|
189
|
-
this.cancelRestart(state);
|
|
190
|
-
await this.closeRunner(state);
|
|
191
|
-
await spawning?.catch(() => undefined);
|
|
192
|
-
// A runner that was already spawning may have bound after the first
|
|
193
|
-
// close. Close once more before switching the immutable package record.
|
|
194
|
-
await this.closeRunner(state);
|
|
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
|
-
});
|
|
154
|
+
async uninstallInstallation(identity, removeRegistry) {
|
|
155
|
+
return await this.runSerialized(identity, async () => {
|
|
156
|
+
const state = this.states.get(identity);
|
|
157
|
+
if (state) {
|
|
158
|
+
await this.stopState(state);
|
|
159
|
+
await this.stopSettingsState(state);
|
|
208
160
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
161
|
+
let removed;
|
|
162
|
+
try {
|
|
163
|
+
removed = await removeRegistry();
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (state) {
|
|
167
|
+
state.stopping = false;
|
|
168
|
+
if (state.record.enabled)
|
|
169
|
+
await this.startState(state);
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
if (!removed && state) {
|
|
174
|
+
state.stopping = false;
|
|
175
|
+
if (state.record.enabled)
|
|
176
|
+
await this.startState(state);
|
|
177
|
+
}
|
|
178
|
+
if (removed)
|
|
179
|
+
this.states.delete(identity);
|
|
180
|
+
return removed;
|
|
181
|
+
});
|
|
221
182
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
state.desired.clear();
|
|
230
|
-
for (const [key, status] of state.channelStatus) {
|
|
231
|
-
state.channelStatus.set(key, { ...status, connected: false });
|
|
183
|
+
async settingsCall(identity, expectedGeneration, method, params, options = {}) {
|
|
184
|
+
return await this.runSerialized(identity, async () => {
|
|
185
|
+
const state = this.states.get(identity);
|
|
186
|
+
if (!state)
|
|
187
|
+
throw new Error(`plugin "${identity}" is not installed`);
|
|
188
|
+
if (state.record.integrity !== expectedGeneration) {
|
|
189
|
+
throw new Error("plugin settings UI generation is stale");
|
|
232
190
|
}
|
|
233
|
-
this.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
191
|
+
const runner = await this.startSettingsState(state);
|
|
192
|
+
if (!runner.handshake.features.settings) {
|
|
193
|
+
throw new Error(`plugin "${identity}" does not advertise a settings background`);
|
|
194
|
+
}
|
|
195
|
+
const result = await runner.client.settingsCall(method, params, options);
|
|
196
|
+
await state.runner?.client.emitHostEvent("plugin.settings.changed", {
|
|
197
|
+
method,
|
|
198
|
+
...(params === undefined ? {} : { params }),
|
|
199
|
+
}).catch(() => undefined);
|
|
200
|
+
return result;
|
|
201
|
+
});
|
|
243
202
|
}
|
|
244
|
-
|
|
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),
|
|
252
|
-
}
|
|
253
|
-
: {}),
|
|
254
|
-
};
|
|
203
|
+
supportsSettings(identity) {
|
|
204
|
+
return Boolean(this.states.get(identity)?.record.manifest.settings);
|
|
255
205
|
}
|
|
256
|
-
|
|
257
|
-
const instanceId = context.instanceId ?? contribution.type;
|
|
258
|
-
if (!TARGET_ID_PATTERN.test(instanceId)) {
|
|
259
|
-
throw new Error(`invalid plugin channel instance id: ${instanceId}`);
|
|
260
|
-
}
|
|
261
|
-
const key = channelKey(contribution.type, instanceId);
|
|
262
|
-
const target = {
|
|
263
|
-
kind: "chat-channel",
|
|
264
|
-
contributionId: contribution.type,
|
|
265
|
-
instanceId,
|
|
266
|
-
};
|
|
267
|
-
const request = {
|
|
268
|
-
...target,
|
|
269
|
-
config: normalizeChatChannelConfig(contribution, context.options),
|
|
270
|
-
...(context.defaultAgent ? { defaultAgent: context.defaultAgent } : {}),
|
|
271
|
-
executionDefaults: {
|
|
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
|
-
};
|
|
206
|
+
statusSnapshot() {
|
|
279
207
|
return {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
|
295
|
-
}
|
|
296
|
-
await this.stopDesired(state, desired);
|
|
297
|
-
},
|
|
298
|
-
getStatus: () => {
|
|
299
|
-
const cached = state.channelStatus.get(key) ?? {
|
|
300
|
-
connected: false,
|
|
301
|
-
lastError: null,
|
|
302
|
-
lastEventAt: null,
|
|
303
|
-
};
|
|
304
|
-
const desired = state.desired.get(key);
|
|
305
|
-
if (desired)
|
|
306
|
-
void this.refreshContributionStatus(state, desired).catch(() => undefined);
|
|
307
|
-
return { ...cached };
|
|
308
|
-
},
|
|
208
|
+
plugins: [...this.states.values()].map((state) => ({
|
|
209
|
+
pluginId: state.record.canonicalId,
|
|
210
|
+
generation: state.generation,
|
|
211
|
+
state: state.state,
|
|
212
|
+
...(state.health ? { health: structuredClone(state.health) } : {}),
|
|
213
|
+
...(state.error ? { error: state.error } : {}),
|
|
214
|
+
settings: Boolean(state.record.manifest.settings),
|
|
215
|
+
})),
|
|
309
216
|
};
|
|
310
217
|
}
|
|
311
|
-
async
|
|
312
|
-
this.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
218
|
+
async dispose() {
|
|
219
|
+
if (this.disposePromise)
|
|
220
|
+
return this.disposePromise;
|
|
221
|
+
this.disposed = true;
|
|
222
|
+
const attempt = (async () => {
|
|
223
|
+
const stopped = await Promise.allSettled([...this.states.values()].map((state) => this.runSerialized(state.record.canonicalId, async () => {
|
|
224
|
+
await this.stopState(state);
|
|
225
|
+
await this.stopSettingsState(state);
|
|
226
|
+
})));
|
|
227
|
+
const failures = stopped
|
|
228
|
+
.filter((result) => result.status === "rejected")
|
|
229
|
+
.map((result) => result.reason);
|
|
230
|
+
if (failures.length > 0) {
|
|
231
|
+
throw new AggregateError(failures, "plugin supervisor could not stop every runtime");
|
|
232
|
+
}
|
|
233
|
+
})();
|
|
234
|
+
this.disposePromise = attempt;
|
|
235
|
+
void attempt.catch(() => {
|
|
236
|
+
if (this.disposePromise === attempt)
|
|
237
|
+
this.disposePromise = undefined;
|
|
238
|
+
});
|
|
239
|
+
return attempt;
|
|
240
|
+
}
|
|
241
|
+
upsertState(record) {
|
|
242
|
+
const current = this.states.get(record.canonicalId);
|
|
243
|
+
if (current) {
|
|
244
|
+
if (!current.runner && !current.settingsRunner)
|
|
245
|
+
current.record = record;
|
|
246
|
+
return current;
|
|
247
|
+
}
|
|
248
|
+
const state = {
|
|
249
|
+
record,
|
|
250
|
+
state: record.manifest.runtime ? "stopped" : "absent",
|
|
251
|
+
generation: 0,
|
|
252
|
+
restartFailures: 0,
|
|
253
|
+
stopping: false,
|
|
326
254
|
};
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (state.desired.get(desired.key) !== desired) {
|
|
255
|
+
this.states.set(record.canonicalId, state);
|
|
256
|
+
return state;
|
|
257
|
+
}
|
|
258
|
+
async startState(state) {
|
|
259
|
+
if (this.disposed
|
|
260
|
+
|| state.runner
|
|
261
|
+
|| blockingCleanupError(state)
|
|
262
|
+
|| !state.record.enabled)
|
|
336
263
|
return;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
async startDesiredOnRunner(state, desired, runner) {
|
|
342
|
-
const generation = state.generation;
|
|
343
|
-
if (desired.startedGeneration === generation)
|
|
344
|
-
return;
|
|
345
|
-
if (desired.starting && desired.startingGeneration === generation) {
|
|
346
|
-
await desired.starting;
|
|
264
|
+
const runtime = state.record.manifest.runtime;
|
|
265
|
+
if (!runtime) {
|
|
266
|
+
state.state = "absent";
|
|
267
|
+
setPluginRuntimeState(state.record.canonicalId, "absent");
|
|
347
268
|
return;
|
|
348
269
|
}
|
|
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;
|
|
270
|
+
if (!this.services)
|
|
271
|
+
throw new Error("plugin host services are not bound");
|
|
272
|
+
state.stopping = false;
|
|
273
|
+
state.state = "starting";
|
|
274
|
+
state.error = undefined;
|
|
275
|
+
state.generation += 1;
|
|
276
|
+
setPluginRuntimeState(state.record.canonicalId, "starting");
|
|
277
|
+
const dataDir = pluginDataDir(state.record.marketplaceId, state.record.pluginId);
|
|
278
|
+
const cacheDir = pluginRuntimeCacheDir(state.record.marketplaceId, state.record.pluginId, state.record.integrity);
|
|
279
|
+
let runner;
|
|
280
|
+
let hostRpc;
|
|
373
281
|
try {
|
|
374
|
-
await
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
282
|
+
await this.verifyPackage(state.record);
|
|
283
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
284
|
+
mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
|
|
285
|
+
writePluginRuntimeLease({
|
|
286
|
+
dataDir,
|
|
287
|
+
pluginId: state.record.pluginId,
|
|
288
|
+
leaseId: RUNTIME_LEASE_ID,
|
|
289
|
+
installPath: state.record.installPath,
|
|
290
|
+
});
|
|
291
|
+
hostRpc = new PluginHostRpc({
|
|
292
|
+
pluginId: state.record.pluginId,
|
|
293
|
+
principalId: state.record.canonicalId,
|
|
294
|
+
daemonInstanceId: this.daemonInstanceId,
|
|
295
|
+
productVersion: this.productVersion,
|
|
296
|
+
cliVersion: this.cliVersion,
|
|
297
|
+
services: this.services,
|
|
298
|
+
...(state.record.pluginId === "workspace"
|
|
299
|
+
&& isBuiltInPluginMarketplace(state.record.marketplaceId)
|
|
300
|
+
? { managedWorkspaceRoot: dataDir }
|
|
301
|
+
: {}),
|
|
302
|
+
emit: async (event) => {
|
|
303
|
+
if (Buffer.byteLength(JSON.stringify(event), "utf8") > MAX_PLUGIN_HOST_EVENT_BYTES) {
|
|
304
|
+
throw new Error("plugin host event exceeds transport budget");
|
|
305
|
+
}
|
|
306
|
+
await runner?.client.emitHostEvent(event.event, event.payload);
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
state.hostRpc = hostRpc;
|
|
310
|
+
runner = await this.spawnRunner({
|
|
311
|
+
pluginId: state.record.pluginId,
|
|
312
|
+
pluginVersion: state.record.version,
|
|
313
|
+
installationId: state.record.canonicalId,
|
|
314
|
+
packageRoot: state.record.installPath,
|
|
315
|
+
...("entry" in runtime
|
|
316
|
+
? {
|
|
317
|
+
entry: runtime.entry,
|
|
318
|
+
}
|
|
319
|
+
: {
|
|
320
|
+
command: runtime.command,
|
|
321
|
+
args: runtime.args,
|
|
322
|
+
}),
|
|
323
|
+
role: "runtime",
|
|
324
|
+
dataDir,
|
|
325
|
+
cacheDir,
|
|
326
|
+
handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
|
|
327
|
+
onStderr: (line) => this.log({
|
|
328
|
+
level: "info",
|
|
329
|
+
event: "plugin-stderr",
|
|
330
|
+
pluginId: state.record.canonicalId,
|
|
331
|
+
message: line,
|
|
332
|
+
}),
|
|
333
|
+
});
|
|
334
|
+
state.runner = runner;
|
|
335
|
+
state.health = runner.initialHealth;
|
|
336
|
+
state.state = healthState(runner.initialHealth);
|
|
337
|
+
state.startedAt = Date.now();
|
|
338
|
+
state.error = undefined;
|
|
339
|
+
setPluginRuntimeState(state.record.canonicalId, state.state);
|
|
340
|
+
this.log({
|
|
341
|
+
level: "info",
|
|
342
|
+
event: "plugin-runtime-started",
|
|
343
|
+
pluginId: state.record.canonicalId,
|
|
344
|
+
});
|
|
345
|
+
this.scheduleHealthPoll(state, runner);
|
|
346
|
+
const generation = state.generation;
|
|
347
|
+
void runner.completion.then((exit) => {
|
|
348
|
+
void this.runSerialized(state.record.canonicalId, () => this.handleExit(state, runner, generation, exit));
|
|
349
|
+
}).catch((error) => {
|
|
350
|
+
void this.runSerialized(state.record.canonicalId, () => this.handleExit(state, runner, generation, {
|
|
351
|
+
code: null,
|
|
352
|
+
signal: null,
|
|
353
|
+
error,
|
|
354
|
+
}));
|
|
355
|
+
});
|
|
381
356
|
}
|
|
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) {
|
|
357
|
+
catch (error) {
|
|
395
358
|
try {
|
|
396
|
-
await
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
stopError = error;
|
|
359
|
+
await hostRpc?.dispose();
|
|
400
360
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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)
|
|
361
|
+
catch (cleanupError) {
|
|
362
|
+
const failure = new AggregateError([error, cleanupError], "plugin runtime startup failed and active-run cleanup was unproven");
|
|
363
|
+
state.runtimeCleanupError = failure;
|
|
364
|
+
this.failState(state, failure);
|
|
418
365
|
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),
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
desired.statusRefresh = refresh;
|
|
431
|
-
try {
|
|
432
|
-
await refresh;
|
|
433
|
-
}
|
|
434
|
-
finally {
|
|
435
|
-
if (desired.statusRefresh === refresh)
|
|
436
|
-
desired.statusRefresh = undefined;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
async refreshRuntimeStatus(state, deadline) {
|
|
440
|
-
if (deadline && deadline.expiresAt - Date.now() <= 0) {
|
|
441
|
-
throw this.runtimeReadyTimeoutError(state, deadline.timeoutMs);
|
|
442
|
-
}
|
|
443
|
-
const runner = state.runner;
|
|
444
|
-
if (!runner)
|
|
445
|
-
return state.runtimeStatus;
|
|
446
|
-
let refresh = state.runtimeStatusRefresh;
|
|
447
|
-
if (!refresh) {
|
|
448
|
-
refresh = runner.client.runtimeStatus().then((status) => {
|
|
449
|
-
if (state.runner !== runner)
|
|
450
|
-
return state.runtimeStatus;
|
|
451
|
-
state.runtimeStatus = boundedRuntimeStatus(status);
|
|
452
|
-
return state.runtimeStatus;
|
|
453
|
-
}, (error) => {
|
|
454
|
-
if (state.runner !== runner)
|
|
455
|
-
return state.runtimeStatus;
|
|
456
|
-
state.runtimeStatus = {
|
|
457
|
-
state: "degraded",
|
|
458
|
-
connected: false,
|
|
459
|
-
lastError: messageOf(error),
|
|
460
|
-
};
|
|
461
|
-
return state.runtimeStatus;
|
|
462
|
-
});
|
|
463
|
-
state.runtimeStatusRefresh = refresh;
|
|
464
|
-
void refresh.finally(() => {
|
|
465
|
-
if (state.runtimeStatusRefresh === refresh)
|
|
466
|
-
state.runtimeStatusRefresh = undefined;
|
|
467
|
-
}).catch(() => undefined);
|
|
468
|
-
}
|
|
469
|
-
return deadline
|
|
470
|
-
? await settleBeforeDeadline(refresh, deadline.expiresAt, () => this.runtimeReadyTimeoutError(state, deadline.timeoutMs))
|
|
471
|
-
: await refresh;
|
|
472
|
-
}
|
|
473
|
-
async waitForRuntimeReady(state, deadline) {
|
|
474
|
-
while (true) {
|
|
475
|
-
this.assertOpen();
|
|
476
|
-
const remainingBeforeRefresh = deadline.expiresAt - Date.now();
|
|
477
|
-
if (remainingBeforeRefresh <= 0) {
|
|
478
|
-
throw this.runtimeReadyTimeoutError(state, deadline.timeoutMs);
|
|
479
366
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return
|
|
485
|
-
const remaining = deadline.expiresAt - Date.now();
|
|
486
|
-
if (remaining <= 0) {
|
|
487
|
-
throw this.runtimeReadyTimeoutError(state, deadline.timeoutMs);
|
|
367
|
+
state.hostRpc = undefined;
|
|
368
|
+
if (isUnprovenProcessCleanup(error)) {
|
|
369
|
+
state.runtimeCleanupError = error;
|
|
370
|
+
this.failState(state, error);
|
|
371
|
+
return;
|
|
488
372
|
}
|
|
489
|
-
|
|
373
|
+
removePluginRuntimeLease(dataDir, RUNTIME_LEASE_ID);
|
|
374
|
+
this.failState(state, error);
|
|
375
|
+
this.scheduleRestart(state);
|
|
490
376
|
}
|
|
491
377
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
378
|
+
async startSettingsState(state) {
|
|
379
|
+
if (this.disposed)
|
|
380
|
+
throw new Error("plugin supervisor is disposed");
|
|
381
|
+
const cleanupError = blockingCleanupError(state);
|
|
382
|
+
if (cleanupError)
|
|
383
|
+
throw cleanupError;
|
|
384
|
+
if (state.settingsRunner)
|
|
385
|
+
return state.settingsRunner;
|
|
386
|
+
const background = state.record.manifest.settings?.background;
|
|
387
|
+
if (!background) {
|
|
388
|
+
throw new Error(`plugin "${state.record.canonicalId}" has no settings background`);
|
|
501
389
|
}
|
|
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);
|
|
390
|
+
if (!this.services)
|
|
391
|
+
throw new Error("plugin host services are not bound");
|
|
392
|
+
const dataDir = pluginDataDir(state.record.marketplaceId, state.record.pluginId);
|
|
393
|
+
const cacheDir = pluginRuntimeCacheDir(state.record.marketplaceId, state.record.pluginId, state.record.integrity);
|
|
533
394
|
let runner;
|
|
395
|
+
let hostRpc;
|
|
534
396
|
try {
|
|
397
|
+
await this.verifyPackage(state.record);
|
|
398
|
+
mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
399
|
+
mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
|
|
400
|
+
writePluginRuntimeLease({
|
|
401
|
+
dataDir,
|
|
402
|
+
pluginId: state.record.pluginId,
|
|
403
|
+
leaseId: SETTINGS_LEASE_ID,
|
|
404
|
+
installPath: state.record.installPath,
|
|
405
|
+
});
|
|
406
|
+
hostRpc = new PluginHostRpc({
|
|
407
|
+
pluginId: state.record.pluginId,
|
|
408
|
+
principalId: state.record.canonicalId,
|
|
409
|
+
daemonInstanceId: this.daemonInstanceId,
|
|
410
|
+
productVersion: this.productVersion,
|
|
411
|
+
cliVersion: this.cliVersion,
|
|
412
|
+
services: this.services,
|
|
413
|
+
...(state.record.pluginId === "workspace"
|
|
414
|
+
&& isBuiltInPluginMarketplace(state.record.marketplaceId)
|
|
415
|
+
? { managedWorkspaceRoot: dataDir }
|
|
416
|
+
: {}),
|
|
417
|
+
emit: async (event) => {
|
|
418
|
+
if (Buffer.byteLength(JSON.stringify(event), "utf8") > MAX_PLUGIN_HOST_EVENT_BYTES) {
|
|
419
|
+
throw new Error("plugin host event exceeds transport budget");
|
|
420
|
+
}
|
|
421
|
+
await runner?.client.emitHostEvent(event.event, event.payload);
|
|
422
|
+
},
|
|
423
|
+
});
|
|
535
424
|
runner = await this.spawnRunner({
|
|
536
|
-
pluginId,
|
|
425
|
+
pluginId: state.record.pluginId,
|
|
426
|
+
pluginVersion: state.record.version,
|
|
427
|
+
installationId: state.record.canonicalId,
|
|
537
428
|
packageRoot: state.record.installPath,
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
429
|
+
...("entry" in background
|
|
430
|
+
? {
|
|
431
|
+
entry: background.entry,
|
|
432
|
+
}
|
|
433
|
+
: {
|
|
434
|
+
command: background.command,
|
|
435
|
+
args: background.args,
|
|
436
|
+
}),
|
|
437
|
+
role: "settings",
|
|
438
|
+
dataDir,
|
|
439
|
+
cacheDir,
|
|
440
|
+
handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
|
|
441
|
+
onStderr: (line) => this.log({
|
|
442
|
+
level: "info",
|
|
443
|
+
event: "plugin-stderr",
|
|
444
|
+
pluginId: state.record.canonicalId,
|
|
445
|
+
message: line,
|
|
446
|
+
}),
|
|
544
447
|
});
|
|
545
|
-
|
|
448
|
+
if (!runner.handshake.features.settings) {
|
|
449
|
+
throw new Error(`plugin "${state.record.canonicalId}" does not advertise a settings background`);
|
|
450
|
+
}
|
|
451
|
+
state.settingsRunner = runner;
|
|
452
|
+
state.settingsHostRpc = hostRpc;
|
|
453
|
+
this.log({
|
|
454
|
+
level: "info",
|
|
455
|
+
event: "plugin-settings-started",
|
|
456
|
+
pluginId: state.record.canonicalId,
|
|
457
|
+
});
|
|
458
|
+
void runner.completion.then((exit) => {
|
|
459
|
+
void this.runSerialized(state.record.canonicalId, () => this.handleSettingsExit(state, runner, hostRpc, exit));
|
|
460
|
+
}).catch((error) => {
|
|
461
|
+
void this.runSerialized(state.record.canonicalId, () => this.handleSettingsExit(state, runner, hostRpc, {
|
|
462
|
+
code: null,
|
|
463
|
+
signal: null,
|
|
464
|
+
error,
|
|
465
|
+
}));
|
|
466
|
+
});
|
|
467
|
+
return runner;
|
|
546
468
|
}
|
|
547
469
|
catch (error) {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
470
|
+
const cleanupErrors = [];
|
|
471
|
+
if (runner) {
|
|
472
|
+
try {
|
|
473
|
+
await runner.close();
|
|
474
|
+
}
|
|
475
|
+
catch (cleanupError) {
|
|
476
|
+
state.settingsRunner = runner;
|
|
477
|
+
cleanupErrors.push(cleanupError);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
await hostRpc?.dispose();
|
|
482
|
+
}
|
|
483
|
+
catch (cleanupError) {
|
|
484
|
+
state.settingsHostRpc = hostRpc;
|
|
485
|
+
cleanupErrors.push(cleanupError);
|
|
486
|
+
}
|
|
487
|
+
if (cleanupErrors.length > 0) {
|
|
488
|
+
const failure = new AggregateError([error, ...cleanupErrors], "plugin settings startup failed and cleanup was unproven");
|
|
489
|
+
state.settingsCleanupError = failure;
|
|
490
|
+
throw failure;
|
|
491
|
+
}
|
|
492
|
+
if (isUnprovenProcessCleanup(error)) {
|
|
493
|
+
state.settingsCleanupError = error;
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
removePluginRuntimeLease(dataDir, SETTINGS_LEASE_ID);
|
|
552
497
|
throw error;
|
|
553
498
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
throw
|
|
499
|
+
}
|
|
500
|
+
async stopSettingsState(state) {
|
|
501
|
+
const runner = state.settingsRunner;
|
|
502
|
+
const hostRpc = state.settingsHostRpc;
|
|
503
|
+
if (state.settingsCleanupError && !runner && !hostRpc) {
|
|
504
|
+
throw state.settingsCleanupError;
|
|
560
505
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
});
|
|
590
|
-
return runner;
|
|
506
|
+
const errors = [];
|
|
507
|
+
if (runner) {
|
|
508
|
+
try {
|
|
509
|
+
await runner.close();
|
|
510
|
+
if (state.settingsRunner === runner)
|
|
511
|
+
state.settingsRunner = undefined;
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
errors.push(error);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (hostRpc) {
|
|
518
|
+
try {
|
|
519
|
+
await hostRpc.dispose();
|
|
520
|
+
if (state.settingsHostRpc === hostRpc)
|
|
521
|
+
state.settingsHostRpc = undefined;
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
errors.push(error);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (errors.length > 0) {
|
|
528
|
+
const failure = cleanupFailure(errors, "plugin settings cleanup was unproven");
|
|
529
|
+
state.settingsCleanupError = failure;
|
|
530
|
+
throw failure;
|
|
531
|
+
}
|
|
532
|
+
state.settingsCleanupError = undefined;
|
|
533
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), SETTINGS_LEASE_ID);
|
|
591
534
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}, (transitionError) => {
|
|
602
|
-
if (state.exitTransition === transition)
|
|
603
|
-
state.exitTransition = undefined;
|
|
604
|
-
this.writeLog({
|
|
535
|
+
async handleSettingsExit(state, runner, hostRpc, exit) {
|
|
536
|
+
if (state.settingsRunner !== runner)
|
|
537
|
+
return;
|
|
538
|
+
try {
|
|
539
|
+
await runner.terminateTree();
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
state.settingsCleanupError = error;
|
|
543
|
+
this.log({
|
|
605
544
|
level: "error",
|
|
606
|
-
event: "plugin
|
|
607
|
-
pluginId: state.record.
|
|
608
|
-
message:
|
|
545
|
+
event: "plugin-settings-exited",
|
|
546
|
+
pluginId: state.record.canonicalId,
|
|
547
|
+
message: error instanceof Error ? error.message : String(error),
|
|
609
548
|
});
|
|
610
|
-
}).catch(() => undefined);
|
|
611
|
-
}
|
|
612
|
-
async runnerExited(state, runner, host, generation, outcome, error) {
|
|
613
|
-
if (state.runner !== runner || state.generation !== generation)
|
|
614
549
|
return;
|
|
615
|
-
state.runner = undefined;
|
|
616
|
-
if (state.host === host)
|
|
617
|
-
state.host = undefined;
|
|
618
|
-
state.eventQueue?.close(error ?? new Error("plugin runner exited"));
|
|
619
|
-
state.eventQueue = undefined;
|
|
620
|
-
state.runtimeStatusRefresh = undefined;
|
|
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)
|
|
628
|
-
return;
|
|
629
|
-
const message = error
|
|
630
|
-
? messageOf(error)
|
|
631
|
-
: `plugin runner exited (code=${String(outcome?.code)}, signal=${String(outcome?.signal)})`;
|
|
632
|
-
this.markRunnerUnavailable(state, message);
|
|
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;
|
|
642
|
-
this.scheduleRestart(state);
|
|
643
|
-
}
|
|
644
|
-
async joinExitTransition(state) {
|
|
645
|
-
while (state.exitTransition) {
|
|
646
|
-
const transition = state.exitTransition;
|
|
647
|
-
await transition.catch(() => undefined);
|
|
648
|
-
if (state.exitTransition === transition)
|
|
649
|
-
state.exitTransition = undefined;
|
|
650
550
|
}
|
|
651
|
-
|
|
652
|
-
scheduleRestart(state) {
|
|
653
|
-
if (this.disposed || state.restartTimer)
|
|
654
|
-
return;
|
|
655
|
-
const exponent = Math.min(Math.max(state.failureCount - 1, 0), 30);
|
|
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
|
-
});
|
|
663
|
-
state.restartTimer = setTimeout(() => {
|
|
664
|
-
state.restartTimer = undefined;
|
|
665
|
-
void this.restart(state);
|
|
666
|
-
}, delayMs);
|
|
667
|
-
state.restartTimer.unref?.();
|
|
668
|
-
}
|
|
669
|
-
async restart(state) {
|
|
670
|
-
if (this.disposed)
|
|
671
|
-
return;
|
|
672
|
-
let runner;
|
|
551
|
+
state.settingsRunner = undefined;
|
|
673
552
|
try {
|
|
674
|
-
|
|
553
|
+
await hostRpc.dispose();
|
|
554
|
+
if (state.settingsHostRpc === hostRpc)
|
|
555
|
+
state.settingsHostRpc = undefined;
|
|
675
556
|
}
|
|
676
557
|
catch (error) {
|
|
677
|
-
|
|
558
|
+
state.settingsCleanupError = error;
|
|
559
|
+
this.log({
|
|
678
560
|
level: "error",
|
|
679
|
-
event: "plugin
|
|
680
|
-
pluginId: state.record.
|
|
681
|
-
message:
|
|
561
|
+
event: "plugin-settings-exited",
|
|
562
|
+
pluginId: state.record.canonicalId,
|
|
563
|
+
message: error instanceof Error ? error.message : String(error),
|
|
682
564
|
});
|
|
683
565
|
return;
|
|
684
566
|
}
|
|
685
|
-
|
|
686
|
-
|
|
567
|
+
state.settingsCleanupError = undefined;
|
|
568
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), SETTINGS_LEASE_ID);
|
|
569
|
+
if (!this.disposed) {
|
|
570
|
+
this.log({
|
|
571
|
+
level: "warn",
|
|
572
|
+
event: "plugin-settings-exited",
|
|
573
|
+
pluginId: state.record.canonicalId,
|
|
574
|
+
message: exit.error instanceof Error
|
|
575
|
+
? exit.error.message
|
|
576
|
+
: `settings background exited with code ${exit.code ?? "null"}${exit.signal ? ` (${exit.signal})` : ""}`,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
687
579
|
}
|
|
688
|
-
async
|
|
689
|
-
|
|
580
|
+
async stopState(state) {
|
|
581
|
+
state.stopping = true;
|
|
582
|
+
if (state.restartTimer)
|
|
583
|
+
clearTimeout(state.restartTimer);
|
|
584
|
+
state.restartTimer = undefined;
|
|
585
|
+
if (state.healthTimer)
|
|
586
|
+
clearTimeout(state.healthTimer);
|
|
587
|
+
state.healthTimer = undefined;
|
|
588
|
+
const runner = state.runner;
|
|
589
|
+
const hostRpc = state.hostRpc;
|
|
590
|
+
if (state.runtimeCleanupError && !runner && !hostRpc) {
|
|
591
|
+
throw state.runtimeCleanupError;
|
|
592
|
+
}
|
|
593
|
+
const errors = [];
|
|
594
|
+
if (runner) {
|
|
690
595
|
try {
|
|
691
|
-
await
|
|
692
|
-
|
|
596
|
+
await runner.close();
|
|
597
|
+
if (state.runner === runner)
|
|
598
|
+
state.runner = undefined;
|
|
693
599
|
}
|
|
694
600
|
catch (error) {
|
|
695
|
-
|
|
696
|
-
level: "error",
|
|
697
|
-
event: "plugin.contribution.restore_failed",
|
|
698
|
-
pluginId: state.record.manifest.id,
|
|
699
|
-
message: messageOf(error),
|
|
700
|
-
});
|
|
601
|
+
errors.push(error);
|
|
701
602
|
}
|
|
702
603
|
}
|
|
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) {
|
|
604
|
+
if (hostRpc) {
|
|
716
605
|
try {
|
|
717
|
-
await
|
|
606
|
+
await hostRpc.dispose();
|
|
607
|
+
if (state.hostRpc === hostRpc)
|
|
608
|
+
state.hostRpc = undefined;
|
|
718
609
|
}
|
|
719
610
|
catch (error) {
|
|
720
|
-
|
|
721
|
-
level: "warn",
|
|
722
|
-
event: "plugin.runner.close_failed",
|
|
723
|
-
pluginId: state.record.manifest.id,
|
|
724
|
-
message: messageOf(error),
|
|
725
|
-
});
|
|
611
|
+
errors.push(error);
|
|
726
612
|
}
|
|
727
613
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
});
|
|
614
|
+
if (errors.length > 0) {
|
|
615
|
+
const failure = cleanupFailure(errors, "plugin runtime cleanup was unproven");
|
|
616
|
+
state.runtimeCleanupError = failure;
|
|
617
|
+
this.failState(state, failure);
|
|
618
|
+
throw failure;
|
|
763
619
|
}
|
|
620
|
+
state.runtimeCleanupError = undefined;
|
|
621
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
|
|
622
|
+
state.health = undefined;
|
|
623
|
+
state.error = undefined;
|
|
624
|
+
state.state = state.record.manifest.runtime ? "stopped" : "absent";
|
|
625
|
+
setPluginRuntimeState(state.record.canonicalId, state.state);
|
|
764
626
|
}
|
|
765
|
-
|
|
766
|
-
|
|
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);
|
|
627
|
+
async handleExit(state, runner, generation, exit) {
|
|
628
|
+
if (state.runner !== runner || state.generation !== generation)
|
|
846
629
|
return;
|
|
847
|
-
|
|
848
|
-
|
|
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;
|
|
630
|
+
if (state.healthTimer)
|
|
631
|
+
clearTimeout(state.healthTimer);
|
|
632
|
+
state.healthTimer = undefined;
|
|
885
633
|
try {
|
|
886
|
-
|
|
634
|
+
await runner.terminateTree();
|
|
887
635
|
}
|
|
888
|
-
catch {
|
|
889
|
-
|
|
636
|
+
catch (error) {
|
|
637
|
+
state.runtimeCleanupError = error;
|
|
638
|
+
this.failState(state, error);
|
|
639
|
+
return;
|
|
890
640
|
}
|
|
891
|
-
|
|
892
|
-
|
|
641
|
+
state.runner = undefined;
|
|
642
|
+
try {
|
|
643
|
+
await state.hostRpc?.dispose();
|
|
644
|
+
state.hostRpc = undefined;
|
|
893
645
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
646
|
+
catch (error) {
|
|
647
|
+
state.runtimeCleanupError = error;
|
|
648
|
+
if (!state.stopping && !this.disposed)
|
|
649
|
+
this.failState(state, error);
|
|
650
|
+
return;
|
|
897
651
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
652
|
+
state.runtimeCleanupError = undefined;
|
|
653
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
|
|
654
|
+
if (state.stopping || this.disposed)
|
|
655
|
+
return;
|
|
656
|
+
const stable = state.startedAt && Date.now() - state.startedAt >= STABLE_RESET_MS;
|
|
657
|
+
if (stable)
|
|
658
|
+
state.restartFailures = 0;
|
|
659
|
+
this.failState(state, exit.error ?? new Error(`runtime exited with code ${exit.code ?? "null"}${exit.signal ? ` (${exit.signal})` : ""}`));
|
|
660
|
+
this.scheduleRestart(state);
|
|
661
|
+
}
|
|
662
|
+
failState(state, error) {
|
|
663
|
+
state.restartFailures += 1;
|
|
664
|
+
state.health = undefined;
|
|
665
|
+
state.state = "failed";
|
|
666
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
667
|
+
setPluginRuntimeState(state.record.canonicalId, "failed", state.error);
|
|
668
|
+
this.log({
|
|
669
|
+
level: "error",
|
|
670
|
+
event: "plugin-runtime-failed",
|
|
671
|
+
pluginId: state.record.canonicalId,
|
|
672
|
+
message: state.error,
|
|
901
673
|
});
|
|
902
|
-
void this.drain();
|
|
903
|
-
return promise;
|
|
904
674
|
}
|
|
905
|
-
|
|
906
|
-
if (this.
|
|
675
|
+
scheduleRestart(state) {
|
|
676
|
+
if (this.disposed || state.stopping || !state.record.enabled || state.restartTimer)
|
|
907
677
|
return;
|
|
908
|
-
this.
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
678
|
+
const delayMs = Math.min(this.restartMaxMs, this.restartBaseMs * 2 ** Math.max(0, state.restartFailures - 1));
|
|
679
|
+
state.restartTimer = setTimeout(() => {
|
|
680
|
+
state.restartTimer = undefined;
|
|
681
|
+
void this.runSerialized(state.record.canonicalId, () => this.startState(state));
|
|
682
|
+
}, delayMs);
|
|
683
|
+
state.restartTimer.unref?.();
|
|
684
|
+
this.log({
|
|
685
|
+
level: "warn",
|
|
686
|
+
event: "plugin-runtime-restart-scheduled",
|
|
687
|
+
pluginId: state.record.canonicalId,
|
|
688
|
+
delayMs,
|
|
689
|
+
});
|
|
916
690
|
}
|
|
917
|
-
|
|
918
|
-
if (this.
|
|
691
|
+
scheduleHealthPoll(state, runner) {
|
|
692
|
+
if (this.disposed || state.stopping || state.runner !== runner || state.healthTimer)
|
|
919
693
|
return;
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
694
|
+
state.healthTimer = setTimeout(() => {
|
|
695
|
+
state.healthTimer = undefined;
|
|
696
|
+
void this.runSerialized(state.record.canonicalId, async () => {
|
|
697
|
+
if (state.runner !== runner || state.stopping || this.disposed)
|
|
698
|
+
return;
|
|
924
699
|
try {
|
|
925
|
-
await this.
|
|
926
|
-
|
|
700
|
+
await this.refreshHealth(state);
|
|
701
|
+
this.scheduleHealthPoll(state, runner);
|
|
927
702
|
}
|
|
928
703
|
catch (error) {
|
|
929
|
-
|
|
930
|
-
item.reject(failure);
|
|
931
|
-
this.queuedBytes -= item.bytes;
|
|
932
|
-
this.close(failure);
|
|
933
|
-
return;
|
|
704
|
+
await this.failRunningState(state, runner, error);
|
|
934
705
|
}
|
|
935
|
-
|
|
936
|
-
|
|
706
|
+
});
|
|
707
|
+
}, this.healthPollMs);
|
|
708
|
+
state.healthTimer.unref?.();
|
|
709
|
+
}
|
|
710
|
+
async refreshHealth(state) {
|
|
711
|
+
const runner = state.runner;
|
|
712
|
+
if (!runner) {
|
|
713
|
+
throw new Error(`plugin runtime "${state.record.canonicalId}" is not running`);
|
|
937
714
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
715
|
+
const health = await runner.client.health();
|
|
716
|
+
if (state.runner !== runner)
|
|
717
|
+
return;
|
|
718
|
+
const nextState = healthState(health);
|
|
719
|
+
const changed = state.state !== nextState ||
|
|
720
|
+
state.error !== undefined ||
|
|
721
|
+
JSON.stringify(state.health) !== JSON.stringify(health);
|
|
722
|
+
state.health = health;
|
|
723
|
+
state.state = nextState;
|
|
724
|
+
state.error = undefined;
|
|
725
|
+
if (changed)
|
|
726
|
+
setPluginRuntimeState(state.record.canonicalId, state.state);
|
|
727
|
+
}
|
|
728
|
+
async failRunningState(state, runner, error) {
|
|
729
|
+
if (state.runner !== runner)
|
|
730
|
+
return;
|
|
731
|
+
if (state.healthTimer)
|
|
732
|
+
clearTimeout(state.healthTimer);
|
|
733
|
+
state.healthTimer = undefined;
|
|
734
|
+
try {
|
|
735
|
+
await runner.close();
|
|
942
736
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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`);
|
|
965
|
-
}
|
|
966
|
-
const requested = new Set(record.manifest.requestedCapabilities);
|
|
967
|
-
for (const grant of record.grants) {
|
|
968
|
-
if (!requested.has(grant)) {
|
|
969
|
-
throw new Error(`plugin ${record.manifest.id} was granted an unrequested capability: ${grant}`);
|
|
737
|
+
catch (cleanupError) {
|
|
738
|
+
state.runtimeCleanupError = cleanupError;
|
|
739
|
+
this.failState(state, cleanupError);
|
|
740
|
+
return;
|
|
970
741
|
}
|
|
742
|
+
state.runner = undefined;
|
|
743
|
+
try {
|
|
744
|
+
await state.hostRpc?.dispose();
|
|
745
|
+
state.hostRpc = undefined;
|
|
746
|
+
}
|
|
747
|
+
catch (cleanupError) {
|
|
748
|
+
state.runtimeCleanupError = cleanupError;
|
|
749
|
+
if (!state.stopping && !this.disposed)
|
|
750
|
+
this.failState(state, cleanupError);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
state.runtimeCleanupError = undefined;
|
|
754
|
+
removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
|
|
755
|
+
if (state.stopping || this.disposed)
|
|
756
|
+
return;
|
|
757
|
+
this.failState(state, error);
|
|
758
|
+
this.scheduleRestart(state);
|
|
971
759
|
}
|
|
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;
|
|
760
|
+
runSerialized(identity, operation) {
|
|
761
|
+
const previous = this.queues.get(identity) ?? Promise.resolve();
|
|
762
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
763
|
+
this.queues.set(identity, current);
|
|
764
|
+
void current.finally(() => {
|
|
765
|
+
if (this.queues.get(identity) === current)
|
|
766
|
+
this.queues.delete(identity);
|
|
767
|
+
}).catch(() => undefined);
|
|
768
|
+
return current;
|
|
1081
769
|
}
|
|
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
770
|
}
|
|
1089
|
-
function
|
|
1090
|
-
return
|
|
771
|
+
function cleanupFailure(errors, message) {
|
|
772
|
+
return errors.length === 1 ? errors[0] : new AggregateError(errors, message);
|
|
1091
773
|
}
|
|
1092
|
-
function
|
|
1093
|
-
|
|
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}`;
|
|
774
|
+
function blockingCleanupError(state) {
|
|
775
|
+
return state.runtimeCleanupError ?? state.settingsCleanupError;
|
|
1099
776
|
}
|
|
1100
|
-
function
|
|
1101
|
-
return
|
|
777
|
+
function isUnprovenProcessCleanup(error) {
|
|
778
|
+
return Boolean(error
|
|
779
|
+
&& typeof error === "object"
|
|
780
|
+
&& "code" in error
|
|
781
|
+
&& error.code === "PROCESS_TREE_STUCK");
|
|
1102
782
|
}
|
|
1103
|
-
function
|
|
1104
|
-
|
|
1105
|
-
return {};
|
|
1106
|
-
return cloneJsonValue(value, "options", new WeakSet());
|
|
783
|
+
function healthState(health) {
|
|
784
|
+
return health.state;
|
|
1107
785
|
}
|
|
1108
|
-
function
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1111
|
-
|
|
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);
|
|
786
|
+
async function verifyInstalledPackage(record) {
|
|
787
|
+
const integrity = await validatePluginPackageSnapshot(record.installPath);
|
|
788
|
+
if (integrity !== record.integrity) {
|
|
789
|
+
throw new Error(`plugin package integrity changed for "${record.canonicalId}"`);
|
|
1142
790
|
}
|
|
1143
791
|
}
|