@cjhyy/code-shell-core 0.8.9 → 0.8.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/dist/automation/scheduler.js +49 -0
- package/dist/automation/store.d.ts +1 -1
- package/dist/automation/store.js +184 -10
- package/dist/cli/agent-server-stdio.js +7 -0
- package/dist/credentials/store.d.ts +14 -0
- package/dist/credentials/store.js +245 -42
- package/dist/engine/engine.js +45 -6
- package/dist/engine/file-history-hook.js +24 -5
- package/dist/engine/run-types.d.ts +9 -0
- package/dist/engine/turn-loop.js +9 -8
- package/dist/goal/lifecycle.d.ts +2 -0
- package/dist/goal/lifecycle.js +56 -33
- package/dist/index.d.ts +2 -3
- package/dist/index.internal.d.ts +1 -0
- package/dist/index.internal.js +1 -0
- package/dist/index.js +2 -2
- package/dist/links/cli.d.ts +2 -0
- package/dist/links/cli.js +11 -4
- package/dist/model-catalog/index.js +19 -4
- package/dist/model-catalog/save-entry.js +122 -61
- package/dist/model-catalog/types.js +27 -23
- package/dist/panel-apps/installer.js +27 -14
- package/dist/panel-apps/registry.js +60 -12
- package/dist/plugins/installedPlugins.d.ts +4 -0
- package/dist/plugins/installedPlugins.js +70 -30
- package/dist/plugins/installer/types.d.ts +12 -12
- package/dist/plugins/installer/update.js +37 -38
- package/dist/plugins/knownMarketplaces.d.ts +7 -3
- package/dist/plugins/knownMarketplaces.js +127 -23
- package/dist/plugins/pluginCatalog.js +18 -4
- package/dist/plugins/pluginHookApproval.js +56 -60
- package/dist/plugins/pluginMcpApproval.js +50 -52
- package/dist/profile/catalog-store.js +39 -4
- package/dist/profile/catalog.js +55 -15
- package/dist/profile/store.js +51 -21
- package/dist/protocol/chat-session-manager.d.ts +9 -0
- package/dist/protocol/chat-session-manager.js +13 -0
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/server.d.ts +2 -0
- package/dist/protocol/server.js +75 -29
- package/dist/protocol/types.d.ts +8 -0
- package/dist/run/FileRunStore.d.ts +2 -0
- package/dist/run/FileRunStore.js +153 -18
- package/dist/run/Heartbeat.js +63 -4
- package/dist/services/auto-dream.js +39 -17
- package/dist/services/session-memory.js +107 -8
- package/dist/session/file-history.d.ts +63 -2
- package/dist/session/file-history.js +593 -86
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/session-manager.js +52 -21
- package/dist/session/transcript.js +33 -3
- package/dist/session/undo-target.d.ts +15 -6
- package/dist/session/undo-target.js +26 -9
- package/dist/settings/manager.d.ts +22 -3
- package/dist/settings/manager.js +185 -50
- package/dist/settings/schema.d.ts +3 -3
- package/dist/sources/adapters/local-files.js +49 -4
- package/dist/sources/catalog.js +64 -18
- package/dist/sources/types.d.ts +3 -3
- package/dist/sources/types.js +7 -4
- package/dist/themes/installer.js +192 -28
- package/dist/tool-system/builtin/add-marketplace.js +21 -1
- package/dist/tool-system/builtin/cron.d.ts +2 -1
- package/dist/tool-system/builtin/cron.js +20 -6
- package/dist/tool-system/builtin/index.js +44 -0
- package/dist/tool-system/builtin/install-capability.d.ts +52 -0
- package/dist/tool-system/builtin/install-capability.js +1057 -0
- package/dist/tool-system/builtin/skill.js +3 -1
- package/dist/tool-system/executor.js +1 -0
- package/dist/tool-system/registry.js +5 -0
- package/dist/utils/file-mutex.d.ts +2 -0
- package/dist/utils/file-mutex.js +29 -4
- package/package.json +2 -1
|
@@ -7,7 +7,7 @@ import { pluginInstallDir } from "./paths.js";
|
|
|
7
7
|
import { installPluginFromPath } from "./install.js";
|
|
8
8
|
import { installPluginFromSource } from "./installFromSource.js";
|
|
9
9
|
import { parseSource } from "./parseSource.js";
|
|
10
|
-
import { pluginInstallKey, readInstalledPlugins, removeInstallEntries,
|
|
10
|
+
import { pluginInstallKey, mutateInstalledPlugins, readInstalledPlugins, removeInstallEntries, } from "../installedPlugins.js";
|
|
11
11
|
import { pluginHookInstallRecord } from "../pluginHookIntegrity.js";
|
|
12
12
|
import { pluginMcpInstallRecord } from "../pluginMcpIntegrity.js";
|
|
13
13
|
import { readPluginMcp } from "./loadPluginMcp.js";
|
|
@@ -40,37 +40,37 @@ export async function reinstallAtomic(name, source, doInstall) {
|
|
|
40
40
|
try {
|
|
41
41
|
await doInstall();
|
|
42
42
|
if (savedEntries) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
43
|
+
mutateInstalledPlugins((data) => {
|
|
44
|
+
const installed = data.plugins[key] ?? [];
|
|
45
|
+
let changed = false;
|
|
46
|
+
for (const entry of installed) {
|
|
47
|
+
const next = pluginHookInstallRecord(entry.installPath, savedEntries);
|
|
48
|
+
const nextMcp = pluginMcpInstallRecord(entry.installPath, Object.keys(readPluginMcp(entry.installPath, name)).length > 0, savedEntries);
|
|
49
|
+
if (entry.hookDigest !== next.hookDigest ||
|
|
50
|
+
entry.approvedHookDigest !== next.approvedHookDigest ||
|
|
51
|
+
JSON.stringify(entry.approvedHookSnapshot) !==
|
|
52
|
+
JSON.stringify(next.approvedHookSnapshot) ||
|
|
53
|
+
entry.mcpDigest !== nextMcp.mcpDigest ||
|
|
54
|
+
entry.approvedMcpDigest !== nextMcp.approvedMcpDigest) {
|
|
55
|
+
entry.hookDigest = next.hookDigest;
|
|
56
|
+
if (next.approvedHookDigest)
|
|
57
|
+
entry.approvedHookDigest = next.approvedHookDigest;
|
|
58
|
+
else
|
|
59
|
+
delete entry.approvedHookDigest;
|
|
60
|
+
if (next.approvedHookSnapshot)
|
|
61
|
+
entry.approvedHookSnapshot = next.approvedHookSnapshot;
|
|
62
|
+
else
|
|
63
|
+
delete entry.approvedHookSnapshot;
|
|
64
|
+
entry.mcpDigest = nextMcp.mcpDigest;
|
|
65
|
+
if (nextMcp.approvedMcpDigest)
|
|
66
|
+
entry.approvedMcpDigest = nextMcp.approvedMcpDigest;
|
|
67
|
+
else
|
|
68
|
+
delete entry.approvedMcpDigest;
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
writeInstalledPlugins(data);
|
|
72
|
+
return changed ? { value: data } : {};
|
|
73
|
+
});
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
catch (err) {
|
|
@@ -78,13 +78,12 @@ export async function reinstallAtomic(name, source, doInstall) {
|
|
|
78
78
|
await rm(dir, { recursive: true, force: true });
|
|
79
79
|
await rename(backup, dir);
|
|
80
80
|
if (savedEntries) {
|
|
81
|
-
// Restore
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
writeInstalledPlugins(data);
|
|
81
|
+
// Restore under the registry lock so a concurrent install of a different
|
|
82
|
+
// plugin cannot be lost while this plugin rolls back.
|
|
83
|
+
mutateInstalledPlugins((data) => {
|
|
84
|
+
data.plugins[key] = savedEntries;
|
|
85
|
+
return { value: data };
|
|
86
|
+
});
|
|
88
87
|
}
|
|
89
88
|
throw new PluginInstallError(`update failed; the old version of '${name}' was kept: ` +
|
|
90
89
|
`${err instanceof Error ? err.message : String(err)}. Source: ${source}.`);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
/** Read/write ~/.code-shell/plugins/known_marketplaces.json. */
|
|
2
|
+
import type { KnownMarketplaces, KnownMarketplace } from "./types.js";
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
+
* The single source of truth for what a marketplace name may be.
|
|
5
|
+
*
|
|
6
|
+
* Exported so entry points (AddMarketplace) reject a bad name BEFORE cloning,
|
|
7
|
+
* rather than discovering it at persist time and orphaning the clone.
|
|
4
8
|
*/
|
|
5
|
-
|
|
9
|
+
export declare function isValidMarketplaceName(name: string): boolean;
|
|
6
10
|
export declare function knownMarketplacesPath(): string;
|
|
7
11
|
export declare function readKnownMarketplaces(): KnownMarketplaces;
|
|
8
12
|
export declare function writeKnownMarketplaces(data: KnownMarketplaces): void;
|
|
@@ -1,46 +1,150 @@
|
|
|
1
|
+
/** Read/write ~/.code-shell/plugins/known_marketplaces.json. */
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { mutateJsonFile } from "../utils/file-mutex.js";
|
|
6
|
+
const MAX_MARKETPLACE_FILE_BYTES = 4 * 1024 * 1024;
|
|
7
|
+
const MAX_MARKETPLACES = 256;
|
|
8
|
+
const MAX_NAME_LENGTH = 128;
|
|
9
|
+
const MAX_PATH_LENGTH = 4_096;
|
|
10
|
+
const MAX_TIMESTAMP_LENGTH = 128;
|
|
11
|
+
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
12
|
+
const GITHUB_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
1
13
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
14
|
+
* The single source of truth for what a marketplace name may be.
|
|
15
|
+
*
|
|
16
|
+
* Exported so entry points (AddMarketplace) reject a bad name BEFORE cloning,
|
|
17
|
+
* rather than discovering it at persist time and orphaning the clone.
|
|
4
18
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
19
|
+
export function isValidMarketplaceName(name) {
|
|
20
|
+
return name.length <= MAX_NAME_LENGTH && NAME_RE.test(name);
|
|
21
|
+
}
|
|
8
22
|
function userHome() {
|
|
9
23
|
return process.env.HOME ?? homedir();
|
|
10
24
|
}
|
|
11
25
|
export function knownMarketplacesPath() {
|
|
12
26
|
return join(userHome(), ".code-shell", "plugins", "known_marketplaces.json");
|
|
13
27
|
}
|
|
28
|
+
function marketplaceOf(value) {
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
30
|
+
return undefined;
|
|
31
|
+
const raw = value;
|
|
32
|
+
if (!raw.source || typeof raw.source !== "object" || Array.isArray(raw.source))
|
|
33
|
+
return undefined;
|
|
34
|
+
const source = raw.source;
|
|
35
|
+
const normalizedSource = source.source === "github" &&
|
|
36
|
+
typeof source.repo === "string" &&
|
|
37
|
+
GITHUB_REPO_RE.test(source.repo)
|
|
38
|
+
? { source: "github", repo: source.repo }
|
|
39
|
+
: source.source === "git" &&
|
|
40
|
+
typeof source.url === "string" &&
|
|
41
|
+
source.url.length > 0 &&
|
|
42
|
+
source.url.length <= MAX_PATH_LENGTH &&
|
|
43
|
+
!source.url.includes("\0")
|
|
44
|
+
? { source: "git", url: source.url }
|
|
45
|
+
: undefined;
|
|
46
|
+
if (!normalizedSource ||
|
|
47
|
+
typeof raw.installLocation !== "string" ||
|
|
48
|
+
raw.installLocation.length === 0 ||
|
|
49
|
+
raw.installLocation.length > MAX_PATH_LENGTH ||
|
|
50
|
+
raw.installLocation.includes("\0") ||
|
|
51
|
+
typeof raw.lastUpdated !== "string" ||
|
|
52
|
+
raw.lastUpdated.length === 0 ||
|
|
53
|
+
raw.lastUpdated.length > MAX_TIMESTAMP_LENGTH) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const format = raw.format === "claude-code" || raw.format === "codex" || raw.format === "universal"
|
|
57
|
+
? raw.format
|
|
58
|
+
: undefined;
|
|
59
|
+
if (raw.format !== undefined && !format)
|
|
60
|
+
return undefined;
|
|
61
|
+
return {
|
|
62
|
+
source: normalizedSource,
|
|
63
|
+
installLocation: raw.installLocation,
|
|
64
|
+
lastUpdated: raw.lastUpdated,
|
|
65
|
+
...(format ? { format } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function parseKnownMarketplaces(raw) {
|
|
69
|
+
if (raw === undefined)
|
|
70
|
+
return Object.create(null);
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
73
|
+
throw new Error("known marketplaces registry must be an object");
|
|
74
|
+
}
|
|
75
|
+
const entries = Object.entries(parsed);
|
|
76
|
+
if (entries.length > MAX_MARKETPLACES)
|
|
77
|
+
throw new Error("too many known marketplaces");
|
|
78
|
+
const result = Object.create(null);
|
|
79
|
+
for (const [name, value] of entries) {
|
|
80
|
+
// ISOLATE, don't throw. NAME_RE is stricter than the writers that produced
|
|
81
|
+
// these keys (AddMarketplace accepts any non-empty string, and
|
|
82
|
+
// assertSafePluginName permits spaces/non-ASCII), so a single legacy name
|
|
83
|
+
// like "My Market" used to make the whole registry read as empty AND make
|
|
84
|
+
// every subsequent write throw — unrecoverable, because the read reported
|
|
85
|
+
// nothing for the user to fix. Skipping matches registryOf() in
|
|
86
|
+
// installedPlugins.ts, which is the correct shape for this file family.
|
|
87
|
+
if (name.length > MAX_NAME_LENGTH || !NAME_RE.test(name))
|
|
88
|
+
continue;
|
|
89
|
+
const marketplace = marketplaceOf(value);
|
|
90
|
+
if (!marketplace)
|
|
91
|
+
continue;
|
|
92
|
+
result[name] = marketplace;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
14
96
|
export function readKnownMarketplaces() {
|
|
15
97
|
const path = knownMarketplacesPath();
|
|
16
|
-
|
|
17
|
-
return {};
|
|
98
|
+
let descriptor;
|
|
18
99
|
try {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
|
|
100
|
+
const metadata = lstatSync(path);
|
|
101
|
+
if (metadata.isSymbolicLink() ||
|
|
102
|
+
!metadata.isFile() ||
|
|
103
|
+
metadata.size > MAX_MARKETPLACE_FILE_BYTES) {
|
|
104
|
+
return Object.create(null);
|
|
105
|
+
}
|
|
106
|
+
descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
107
|
+
const opened = fstatSync(descriptor);
|
|
108
|
+
if (!opened.isFile() || opened.size > MAX_MARKETPLACE_FILE_BYTES) {
|
|
109
|
+
return Object.create(null);
|
|
22
110
|
}
|
|
111
|
+
return parseKnownMarketplaces(readFileSync(descriptor, "utf8"));
|
|
23
112
|
}
|
|
24
113
|
catch {
|
|
25
|
-
|
|
114
|
+
return Object.create(null);
|
|
26
115
|
}
|
|
27
|
-
|
|
116
|
+
finally {
|
|
117
|
+
if (descriptor !== undefined)
|
|
118
|
+
closeSync(descriptor);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function mutateKnownMarketplaces(change) {
|
|
122
|
+
return mutateJsonFile(knownMarketplacesPath(), {
|
|
123
|
+
parse: parseKnownMarketplaces,
|
|
124
|
+
serialize: (value) => `${JSON.stringify(value, null, 2)}\n`,
|
|
125
|
+
mutation: change,
|
|
126
|
+
mode: 0o600,
|
|
127
|
+
maxBytes: MAX_MARKETPLACE_FILE_BYTES,
|
|
128
|
+
});
|
|
28
129
|
}
|
|
29
130
|
export function writeKnownMarketplaces(data) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
131
|
+
const validated = parseKnownMarketplaces(JSON.stringify(data));
|
|
132
|
+
mutateKnownMarketplaces(() => ({ value: validated }));
|
|
33
133
|
}
|
|
34
134
|
export function upsertKnownMarketplace(name, entry) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
135
|
+
const marketplace = marketplaceOf(entry);
|
|
136
|
+
if (!NAME_RE.test(name) || !marketplace)
|
|
137
|
+
throw new Error("invalid known marketplace");
|
|
138
|
+
mutateKnownMarketplaces((current) => ({ value: { ...current, [name]: marketplace } }));
|
|
38
139
|
}
|
|
39
140
|
export function removeKnownMarketplace(name) {
|
|
40
|
-
|
|
41
|
-
if (!(name in data))
|
|
141
|
+
if (!NAME_RE.test(name))
|
|
42
142
|
return false;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
143
|
+
return (mutateKnownMarketplaces((current) => {
|
|
144
|
+
if (!Object.prototype.hasOwnProperty.call(current, name))
|
|
145
|
+
return { result: false };
|
|
146
|
+
const next = { ...current };
|
|
147
|
+
delete next[name];
|
|
148
|
+
return { value: next, result: true };
|
|
149
|
+
}) ?? false);
|
|
46
150
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { readInstalledPlugins } from "./installedPlugins.js";
|
|
5
5
|
import { pluginsRoot } from "./installer/paths.js";
|
|
6
6
|
import { CANONICAL_PLUGIN_MANIFEST_FILE, CanonicalPluginManifest, } from "./installer/types.js";
|
|
7
7
|
import { resolveSafePluginPath } from "./pluginInstaller.js";
|
|
8
|
+
const MAX_CANONICAL_MANIFEST_BYTES = 4 * 1024 * 1024;
|
|
8
9
|
/**
|
|
9
10
|
* Stable content revision used to bind a user review to the exact template
|
|
10
11
|
* instantiated later. Install paths and timestamps are intentionally excluded.
|
|
@@ -20,14 +21,27 @@ export function pluginAutomationTemplateRevision(installKey, template) {
|
|
|
20
21
|
}
|
|
21
22
|
function readCanonicalManifest(installPath) {
|
|
22
23
|
const file = join(installPath, CANONICAL_PLUGIN_MANIFEST_FILE);
|
|
23
|
-
|
|
24
|
-
return null;
|
|
24
|
+
let descriptor;
|
|
25
25
|
try {
|
|
26
|
-
|
|
26
|
+
const metadata = lstatSync(file);
|
|
27
|
+
if (metadata.isSymbolicLink() ||
|
|
28
|
+
!metadata.isFile() ||
|
|
29
|
+
metadata.size > MAX_CANONICAL_MANIFEST_BYTES) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
33
|
+
const opened = fstatSync(descriptor);
|
|
34
|
+
if (!opened.isFile() || opened.size > MAX_CANONICAL_MANIFEST_BYTES)
|
|
35
|
+
return null;
|
|
36
|
+
return CanonicalPluginManifest.parse(JSON.parse(readFileSync(descriptor, "utf-8")));
|
|
27
37
|
}
|
|
28
38
|
catch {
|
|
29
39
|
return null;
|
|
30
40
|
}
|
|
41
|
+
finally {
|
|
42
|
+
if (descriptor !== undefined)
|
|
43
|
+
closeSync(descriptor);
|
|
44
|
+
}
|
|
31
45
|
}
|
|
32
46
|
function identityFromInstallKey(installKey, manifest) {
|
|
33
47
|
const at = installKey.lastIndexOf("@");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mutateInstalledPlugins, readInstalledPlugins } from "./installedPlugins.js";
|
|
2
2
|
import { inspectPluginHooks, pluginHookReviewSnapshot, pluginHookApprovalState, } from "./pluginHookIntegrity.js";
|
|
3
3
|
function pluginNameFromKey(key) {
|
|
4
4
|
const at = key.lastIndexOf("@");
|
|
@@ -86,75 +86,71 @@ export function reviewPluginHooks(target) {
|
|
|
86
86
|
}
|
|
87
87
|
/** Approve the exact install-time digest; changed-on-disk hooks cannot be blessed. */
|
|
88
88
|
export function approvePluginHooks(target) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
89
|
+
return (mutateInstalledPlugins((data) => {
|
|
90
|
+
const keys = matchingInstallKeys(data, target);
|
|
91
|
+
const out = [];
|
|
92
|
+
let writeNeeded = false;
|
|
93
|
+
for (const installKey of keys) {
|
|
94
|
+
for (const entry of data.plugins[installKey] ?? []) {
|
|
95
|
+
let entryChanged = false;
|
|
96
|
+
const snapshot = inspectPluginHooks(entry.installPath);
|
|
97
|
+
if (snapshot.state === "invalid") {
|
|
98
|
+
throw new Error(`plugin "${installKey}" hooks definition is invalid and cannot be approved: ${snapshot.error ?? "unknown error"}`);
|
|
99
|
+
}
|
|
100
|
+
if (entry.hookDigest && snapshot.digest !== entry.hookDigest) {
|
|
101
|
+
throw new Error(`plugin "${installKey}" hooks changed after install; reinstall or update before approving`);
|
|
102
|
+
}
|
|
103
|
+
if (!entry.hookDigest) {
|
|
104
|
+
entry.hookDigest = snapshot.digest;
|
|
105
|
+
entryChanged = true;
|
|
106
|
+
}
|
|
107
|
+
if (entry.approvedHookDigest !== entry.hookDigest) {
|
|
108
|
+
entry.approvedHookDigest = entry.hookDigest;
|
|
109
|
+
entryChanged = true;
|
|
110
|
+
}
|
|
111
|
+
const nextReview = pluginHookReviewSnapshot(snapshot);
|
|
112
|
+
if (JSON.stringify(entry.approvedHookSnapshot) !== JSON.stringify(nextReview)) {
|
|
113
|
+
entry.approvedHookSnapshot = nextReview;
|
|
114
|
+
entryChanged = true;
|
|
115
|
+
}
|
|
116
|
+
writeNeeded ||= entryChanged;
|
|
117
|
+
out.push(result(installKey, entry, entryChanged, snapshot));
|
|
118
118
|
}
|
|
119
|
-
out.push(result(installKey, entry, entryChanged, snapshot));
|
|
120
119
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
writeInstalledPlugins(data);
|
|
124
|
-
return out;
|
|
120
|
+
return { ...(writeNeeded ? { value: data } : {}), result: out };
|
|
121
|
+
}) ?? []);
|
|
125
122
|
}
|
|
126
123
|
/**
|
|
127
124
|
* Revoke hook execution. Legacy entries are first migrated to a recorded
|
|
128
125
|
* digest so revocation fails closed instead of falling back to legacy trust.
|
|
129
126
|
*/
|
|
130
127
|
export function revokePluginHooks(target) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
entryChanged = true;
|
|
142
|
-
}
|
|
143
|
-
if (snapshot.hasExecutableHooks || snapshot.state === "invalid") {
|
|
144
|
-
if (entry.approvedHookDigest !== undefined) {
|
|
145
|
-
delete entry.approvedHookDigest;
|
|
128
|
+
return (mutateInstalledPlugins((data) => {
|
|
129
|
+
const keys = matchingInstallKeys(data, target);
|
|
130
|
+
const out = [];
|
|
131
|
+
let writeNeeded = false;
|
|
132
|
+
for (const installKey of keys) {
|
|
133
|
+
for (const entry of data.plugins[installKey] ?? []) {
|
|
134
|
+
let entryChanged = false;
|
|
135
|
+
const snapshot = inspectPluginHooks(entry.installPath);
|
|
136
|
+
if (!entry.hookDigest) {
|
|
137
|
+
entry.hookDigest = snapshot.digest;
|
|
146
138
|
entryChanged = true;
|
|
147
139
|
}
|
|
140
|
+
if (snapshot.hasExecutableHooks || snapshot.state === "invalid") {
|
|
141
|
+
if (entry.approvedHookDigest !== undefined) {
|
|
142
|
+
delete entry.approvedHookDigest;
|
|
143
|
+
entryChanged = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else if (entry.approvedHookDigest !== entry.hookDigest) {
|
|
147
|
+
entry.approvedHookDigest = entry.hookDigest;
|
|
148
|
+
entryChanged = true;
|
|
149
|
+
}
|
|
150
|
+
writeNeeded ||= entryChanged;
|
|
151
|
+
out.push(result(installKey, entry, entryChanged, snapshot));
|
|
148
152
|
}
|
|
149
|
-
else if (entry.approvedHookDigest !== entry.hookDigest) {
|
|
150
|
-
entry.approvedHookDigest = entry.hookDigest;
|
|
151
|
-
entryChanged = true;
|
|
152
|
-
}
|
|
153
|
-
writeNeeded ||= entryChanged;
|
|
154
|
-
out.push(result(installKey, entry, entryChanged, snapshot));
|
|
155
153
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
writeInstalledPlugins(data);
|
|
159
|
-
return out;
|
|
154
|
+
return { ...(writeNeeded ? { value: data } : {}), result: out };
|
|
155
|
+
}) ?? []);
|
|
160
156
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mutateInstalledPlugins, readInstalledPlugins } from "./installedPlugins.js";
|
|
2
2
|
import { readPluginMcp } from "./installer/loadPluginMcp.js";
|
|
3
3
|
import { pluginMcpApprovalState, pluginMcpDigest, } from "./pluginMcpIntegrity.js";
|
|
4
4
|
function pluginNameFromKey(key) {
|
|
@@ -44,67 +44,65 @@ export function listPluginMcpTrust() {
|
|
|
44
44
|
}
|
|
45
45
|
/** Approve only the install-time MCP bytes; tampered bytes require update/reinstall. */
|
|
46
46
|
export function approvePluginMcp(target) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
47
|
+
return (mutateInstalledPlugins((data) => {
|
|
48
|
+
const keys = matchingInstallKeys(data, target);
|
|
49
|
+
const out = [];
|
|
50
|
+
let writeNeeded = false;
|
|
51
|
+
for (const installKey of keys) {
|
|
52
|
+
for (const entry of data.plugins[installKey] ?? []) {
|
|
53
|
+
let entryChanged = false;
|
|
54
|
+
const currentDigest = pluginMcpDigest(entry.installPath);
|
|
55
|
+
if (entry.mcpDigest && currentDigest !== entry.mcpDigest) {
|
|
56
|
+
throw new Error(`plugin "${installKey}" MCP config changed after install; reinstall or update before approving`);
|
|
57
|
+
}
|
|
58
|
+
if (!entry.mcpDigest) {
|
|
59
|
+
entry.mcpDigest = currentDigest;
|
|
60
|
+
entryChanged = true;
|
|
61
|
+
}
|
|
62
|
+
if (entry.approvedMcpDigest !== entry.mcpDigest) {
|
|
63
|
+
entry.approvedMcpDigest = entry.mcpDigest;
|
|
64
|
+
entryChanged = true;
|
|
65
|
+
}
|
|
66
|
+
writeNeeded ||= entryChanged;
|
|
67
|
+
out.push(trustEntry(installKey, entry, entryChanged));
|
|
65
68
|
}
|
|
66
|
-
writeNeeded ||= entryChanged;
|
|
67
|
-
out.push(trustEntry(installKey, entry, entryChanged));
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
writeInstalledPlugins(data);
|
|
72
|
-
return out;
|
|
70
|
+
return { ...(writeNeeded ? { value: data } : {}), result: out };
|
|
71
|
+
}) ?? []);
|
|
73
72
|
}
|
|
74
73
|
/**
|
|
75
74
|
* Revoke plugin MCP execution. Legacy entries first record their current
|
|
76
75
|
* digest, preventing revocation from falling back to legacy compatibility.
|
|
77
76
|
*/
|
|
78
77
|
export function revokePluginMcp(target) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
entryChanged = true;
|
|
92
|
-
}
|
|
93
|
-
if (hasServers || currentDigest !== entry.mcpDigest) {
|
|
94
|
-
if (entry.approvedMcpDigest !== undefined) {
|
|
95
|
-
delete entry.approvedMcpDigest;
|
|
78
|
+
return (mutateInstalledPlugins((data) => {
|
|
79
|
+
const keys = matchingInstallKeys(data, target);
|
|
80
|
+
const out = [];
|
|
81
|
+
let writeNeeded = false;
|
|
82
|
+
for (const installKey of keys) {
|
|
83
|
+
const plugin = pluginNameFromKey(installKey);
|
|
84
|
+
for (const entry of data.plugins[installKey] ?? []) {
|
|
85
|
+
let entryChanged = false;
|
|
86
|
+
const hasServers = Object.keys(readPluginMcp(entry.installPath, plugin)).length > 0;
|
|
87
|
+
const currentDigest = pluginMcpDigest(entry.installPath);
|
|
88
|
+
if (!entry.mcpDigest) {
|
|
89
|
+
entry.mcpDigest = currentDigest;
|
|
96
90
|
entryChanged = true;
|
|
97
91
|
}
|
|
92
|
+
if (hasServers || currentDigest !== entry.mcpDigest) {
|
|
93
|
+
if (entry.approvedMcpDigest !== undefined) {
|
|
94
|
+
delete entry.approvedMcpDigest;
|
|
95
|
+
entryChanged = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (entry.approvedMcpDigest !== entry.mcpDigest) {
|
|
99
|
+
entry.approvedMcpDigest = entry.mcpDigest;
|
|
100
|
+
entryChanged = true;
|
|
101
|
+
}
|
|
102
|
+
writeNeeded ||= entryChanged;
|
|
103
|
+
out.push(trustEntry(installKey, entry, entryChanged));
|
|
98
104
|
}
|
|
99
|
-
else if (entry.approvedMcpDigest !== entry.mcpDigest) {
|
|
100
|
-
entry.approvedMcpDigest = entry.mcpDigest;
|
|
101
|
-
entryChanged = true;
|
|
102
|
-
}
|
|
103
|
-
writeNeeded ||= entryChanged;
|
|
104
|
-
out.push(trustEntry(installKey, entry, entryChanged));
|
|
105
105
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
writeInstalledPlugins(data);
|
|
109
|
-
return out;
|
|
106
|
+
return { ...(writeNeeded ? { value: data } : {}), result: out };
|
|
107
|
+
}) ?? []);
|
|
110
108
|
}
|