@uniqbit/mate-core 0.17.2-canary.1 → 0.17.2-canary.3
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/package.json +1 -1
- package/src/cli/commands/install.ts +1 -3
- package/src/cli/commands/plugin/verify.ts +1 -1
- package/src/tools/setup/dynamic-plugins/frozen.ts +3 -24
- package/src/tools/setup/dynamic-plugins/loader.ts +0 -16
- package/src/tools/setup/dynamic-plugins/staging.ts +5 -11
- package/src/tools/setup/dynamic-plugins/verify.ts +3 -22
- package/src/tools/setup/dynamic-plugins/policy.ts +0 -55
package/package.json
CHANGED
|
@@ -166,11 +166,9 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
166
166
|
);
|
|
167
167
|
if (drift.length > 0) {
|
|
168
168
|
process.stderr.write(
|
|
169
|
-
`${FRAMEWORK_NAME}: plugin-generated files differ from the committed checkout: ${drift.join(", ")}\n` +
|
|
169
|
+
`${FRAMEWORK_NAME}: warning: plugin-generated files differ from the committed checkout: ${drift.join(", ")}\n` +
|
|
170
170
|
`Prepare and commit them in the companion's authoring flow; the checkout was left unchanged.\n`,
|
|
171
171
|
);
|
|
172
|
-
process.exitCode = 1;
|
|
173
|
-
return false;
|
|
174
172
|
}
|
|
175
173
|
} else {
|
|
176
174
|
await reconcileInstalledCompanion(plan);
|
|
@@ -4,7 +4,7 @@ import { inspectDeclaredPlugins } from "../../../tools/setup/dynamic-plugins/ver
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @command mate plugin verify
|
|
7
|
-
* @description Strict, installation-free check that every plugin declared by the companion is
|
|
7
|
+
* @description Strict, installation-free check that every plugin declared by the companion is installed, and loadable with the current environment. Exits non-zero naming each failing package. With `--json`, prints `{"capabilities": [...]}` listing the capability IDs the verified plugins provide.
|
|
8
8
|
*/
|
|
9
9
|
export async function runPluginVerifyCommand(
|
|
10
10
|
argv: string[] = [],
|
|
@@ -5,14 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import type { PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
6
6
|
import type { NpmInstallRunner, PluginInstallResult } from "./install";
|
|
7
7
|
import { dynamicPluginsWorkspaceRoot } from "./paths";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
isPluginAllowed,
|
|
11
|
-
PluginPolicyError,
|
|
12
|
-
readPluginPolicy,
|
|
13
|
-
} from "./policy";
|
|
14
|
-
|
|
15
|
-
/** Raised before anything is installed: policy, manifest or lockfile inputs are unusable. */
|
|
8
|
+
|
|
9
|
+
/** Raised before anything is installed: manifest or lockfile inputs are unusable. */
|
|
16
10
|
export class FrozenInstallError extends Error {}
|
|
17
11
|
|
|
18
12
|
export interface FrozenInstallDeps {
|
|
@@ -112,8 +106,7 @@ async function treeMismatches(workspaceRoot: string, lock: Lockfile): Promise<st
|
|
|
112
106
|
|
|
113
107
|
/**
|
|
114
108
|
* Deployment restore: installs exactly what the committed lockfile records.
|
|
115
|
-
* Inputs are validated before npm runs — the
|
|
116
|
-
* workspace manifest against the declarations, and the lockfile against the
|
|
109
|
+
* Inputs are validated before npm runs — the workspace manifest against the declarations, and the lockfile against the
|
|
117
110
|
* manifest — and neither tracked file is ever rewritten. An installed tree is
|
|
118
111
|
* reused only when it matches the lockfile's versions and integrity.
|
|
119
112
|
*/
|
|
@@ -127,20 +120,6 @@ export async function installDeclaredPluginsFrozen(
|
|
|
127
120
|
const desired: Record<string, string> = {};
|
|
128
121
|
for (const declaration of sorted) desired[declaration.package] = declaration.version;
|
|
129
122
|
|
|
130
|
-
let policy: ReturnType<typeof readPluginPolicy>;
|
|
131
|
-
try {
|
|
132
|
-
policy = readPluginPolicy(deps.env ?? process.env);
|
|
133
|
-
} catch (error) {
|
|
134
|
-
if (error instanceof PluginPolicyError) throw new FrozenInstallError(error.message);
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
137
|
-
const refused = sorted.filter((declaration) => !isPluginAllowed(policy, declaration.package));
|
|
138
|
-
if (refused.length > 0) {
|
|
139
|
-
throw new FrozenInstallError(
|
|
140
|
-
refused.map((declaration) => disallowedPluginMessage(declaration.package)).join("\n"),
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
123
|
const manifestFile = path.join(workspaceRoot, "package.json");
|
|
145
124
|
const lockFile = path.join(workspaceRoot, "package-lock.json");
|
|
146
125
|
const [manifestText, lockText] = await Promise.all([readText(manifestFile), readText(lockFile)]);
|
|
@@ -14,12 +14,6 @@ import {
|
|
|
14
14
|
type PluginHost,
|
|
15
15
|
} from "./host";
|
|
16
16
|
import { dynamicPluginsWorkspaceRoot, pluginPackageRoot } from "./paths";
|
|
17
|
-
import {
|
|
18
|
-
disallowedPluginMessage,
|
|
19
|
-
isPluginAllowed,
|
|
20
|
-
PluginPolicyError,
|
|
21
|
-
readPluginPolicy,
|
|
22
|
-
} from "./policy";
|
|
23
17
|
|
|
24
18
|
interface PluginManifest {
|
|
25
19
|
mate?: { pluginApiVersion?: unknown };
|
|
@@ -53,16 +47,6 @@ export async function loadDynamicPlugin(
|
|
|
53
47
|
const name = declaration.package;
|
|
54
48
|
const packageRoot = pluginPackageRoot(companionPath, name);
|
|
55
49
|
|
|
56
|
-
/** Fails closed: a malformed policy allows nothing. Checked before any plugin file is touched. */
|
|
57
|
-
try {
|
|
58
|
-
if (!isPluginAllowed(readPluginPolicy(deps.env ?? process.env), name)) {
|
|
59
|
-
return { ok: false, warning: `${disallowedPluginMessage(name)}; not loaded.` };
|
|
60
|
-
}
|
|
61
|
-
} catch (error) {
|
|
62
|
-
if (!(error instanceof PluginPolicyError)) throw error;
|
|
63
|
-
return { ok: false, warning: `plugin "${name}" not loaded: ${error.message}` };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
50
|
let manifest: PluginManifest;
|
|
67
51
|
try {
|
|
68
52
|
manifest = JSON.parse(
|
|
@@ -25,22 +25,16 @@ function changedPaths(cwd: string): string[] {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* drift. Nothing is written back to the live checkout.
|
|
32
|
-
* paths (empty when the
|
|
28
|
+
* Reports what plugins would generate that is not already committed. The
|
|
29
|
+
* projection runs against a disposable clone of the checkout's commit, so edits
|
|
30
|
+
* in the live checkout neither matter nor are touched, and any file that
|
|
31
|
+
* differs afterwards is drift. Nothing is written back to the live checkout.
|
|
32
|
+
* Returns the drifting paths (empty when the commit is complete).
|
|
33
33
|
*/
|
|
34
34
|
export async function verifyTrackedPluginOutputs(
|
|
35
35
|
companionPath: string,
|
|
36
36
|
project: StagedProjection,
|
|
37
37
|
): Promise<string[]> {
|
|
38
|
-
const pending = changedPaths(companionPath);
|
|
39
|
-
if (pending.length > 0) {
|
|
40
|
-
throw new TrackedOutputError(
|
|
41
|
-
`the checkout has uncommitted changes, which frozen setup will not overwrite: ${pending.join(", ")}`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
38
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "mate-staging-"));
|
|
45
39
|
try {
|
|
46
40
|
const staged = path.join(root, "companion");
|
|
@@ -5,12 +5,6 @@ import type { PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
|
5
5
|
import { readDeclarations, validateDeclaration } from "./declarations";
|
|
6
6
|
import { loadDynamicPlugin, type DynamicPluginLoadDeps } from "./loader";
|
|
7
7
|
import { pluginPackageRoot } from "./paths";
|
|
8
|
-
import {
|
|
9
|
-
disallowedPluginMessage,
|
|
10
|
-
isPluginAllowed,
|
|
11
|
-
PluginPolicyError,
|
|
12
|
-
readPluginPolicy,
|
|
13
|
-
} from "./policy";
|
|
14
8
|
|
|
15
9
|
export interface PluginVerificationFailure {
|
|
16
10
|
package: string;
|
|
@@ -19,15 +13,14 @@ export interface PluginVerificationFailure {
|
|
|
19
13
|
|
|
20
14
|
export interface PluginInspection {
|
|
21
15
|
failures: PluginVerificationFailure[];
|
|
22
|
-
/** IDs of the capabilities the loaded
|
|
16
|
+
/** IDs of the capabilities the loaded plugins provide. */
|
|
23
17
|
capabilities: string[];
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
/**
|
|
27
|
-
* Strict, installation-free check of every declared plugin:
|
|
21
|
+
* Strict, installation-free check of every declared plugin: installed
|
|
28
22
|
* and loadable with the effective environment. Unlike hydration it fails
|
|
29
|
-
* closed on the first-class problems ordinary commands only warn about.
|
|
30
|
-
* allowlisted packages are imported.
|
|
23
|
+
* closed on the first-class problems ordinary commands only warn about.
|
|
31
24
|
*/
|
|
32
25
|
export async function verifyDeclaredPlugins(
|
|
33
26
|
companionPath: string,
|
|
@@ -43,14 +36,6 @@ export async function inspectDeclaredPlugins(
|
|
|
43
36
|
const env = deps.env ?? process.env;
|
|
44
37
|
const failures: PluginVerificationFailure[] = [];
|
|
45
38
|
const capabilities: string[] = [];
|
|
46
|
-
let policy: ReturnType<typeof readPluginPolicy>;
|
|
47
|
-
try {
|
|
48
|
-
policy = readPluginPolicy(env);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if (!(error instanceof PluginPolicyError)) throw error;
|
|
51
|
-
return { failures: [{ package: "(allowlist)", reason: error.message }], capabilities };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
39
|
const declarations: PluginDeclaration[] = [];
|
|
55
40
|
for (const entry of await readDeclarations(companionPath)) {
|
|
56
41
|
const { declaration, error } = validateDeclaration(entry);
|
|
@@ -60,10 +45,6 @@ export async function inspectDeclaredPlugins(
|
|
|
60
45
|
|
|
61
46
|
for (const declaration of declarations) {
|
|
62
47
|
const name = declaration.package;
|
|
63
|
-
if (!isPluginAllowed(policy, name)) {
|
|
64
|
-
failures.push({ package: name, reason: disallowedPluginMessage(name) });
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
48
|
// oxlint-disable-next-line no-await-in-loop -- declared order is part of the contract
|
|
68
49
|
const installed = await fs
|
|
69
50
|
.access(pluginPackageRoot(companionPath, name))
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/** Environment variable carrying the appliance's declared-plugin package allowlist. */
|
|
2
|
-
export const ALLOWED_PLUGINS_ENV = "MATE_ALLOWED_PLUGINS";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Parsed allowlist: exact package names and scope-wide `@scope/*` patterns.
|
|
6
|
-
* An empty list permits no package; the policy's absence permits all.
|
|
7
|
-
*/
|
|
8
|
-
export interface PluginPolicy {
|
|
9
|
-
exact: string[];
|
|
10
|
-
scopes: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class PluginPolicyError extends Error {}
|
|
14
|
-
|
|
15
|
-
const EXACT_PATTERN = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
16
|
-
const SCOPE_PATTERN = /^@[a-z0-9][a-z0-9._~-]*\/\*$/;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Parses a comma-separated allowlist. `undefined` means no policy; an empty
|
|
20
|
-
* or blank string is an explicit empty policy. Malformed entries throw so a
|
|
21
|
-
* typo can never widen or silently narrow the trust decision.
|
|
22
|
-
*/
|
|
23
|
-
export function parseAllowedPlugins(raw: string | undefined): PluginPolicy | null {
|
|
24
|
-
if (raw === undefined) return null;
|
|
25
|
-
const policy: PluginPolicy = { exact: [], scopes: [] };
|
|
26
|
-
if (raw.trim() === "") return policy;
|
|
27
|
-
for (const part of raw.split(",")) {
|
|
28
|
-
const entry = part.trim();
|
|
29
|
-
if (SCOPE_PATTERN.test(entry)) policy.scopes.push(entry.slice(0, entry.indexOf("/") + 1));
|
|
30
|
-
else if (EXACT_PATTERN.test(entry)) policy.exact.push(entry);
|
|
31
|
-
else {
|
|
32
|
-
throw new PluginPolicyError(
|
|
33
|
-
`${ALLOWED_PLUGINS_ENV}: malformed entry ${JSON.stringify(entry)}; expected an exact package name or a scope pattern such as "@acme/*"`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return policy;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Reads the effective policy from an environment; throws on a malformed value. */
|
|
41
|
-
export function readPluginPolicy(
|
|
42
|
-
env: Record<string, string | undefined> = process.env,
|
|
43
|
-
): PluginPolicy | null {
|
|
44
|
-
return parseAllowedPlugins(env[ALLOWED_PLUGINS_ENV]);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function isPluginAllowed(policy: PluginPolicy | null, packageName: string): boolean {
|
|
48
|
-
if (policy === null) return true;
|
|
49
|
-
if (policy.exact.includes(packageName)) return true;
|
|
50
|
-
return policy.scopes.some((scope) => packageName.startsWith(scope));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function disallowedPluginMessage(packageName: string): string {
|
|
54
|
-
return `plugin "${packageName}" is not allowed by ${ALLOWED_PLUGINS_ENV}`;
|
|
55
|
-
}
|