planr 1.3.0 → 1.4.0
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/README.md +12 -0
- package/docs/ARCHITECTURE.md +5 -0
- package/docs/CLI_REFERENCE.md +19 -3
- package/docs/MCP_CONTRACT.md +5 -1
- package/docs/MODEL_ROUTING.md +2 -2
- package/docs/PRESET_COMPOSITION.md +109 -0
- package/docs/PRESET_EVALUATION.md +118 -0
- package/docs/PRESET_REGISTRY.md +61 -0
- package/docs/fixtures/mcp-contract.json +16 -0
- package/evaluations/preset-suite-v1.toml +127 -0
- package/evaluations/sol-luna-codex-v1.toml +42 -0
- package/npm/native/darwin-arm64/planr +0 -0
- package/npm/native/darwin-x86_64/planr +0 -0
- package/npm/native/linux-arm64/planr +0 -0
- package/npm/native/linux-x86_64/planr +0 -0
- package/package.json +15 -2
- package/plugins/planr/.claude-plugin/plugin.json +1 -1
- package/plugins/planr/.codex-plugin/plugin.json +1 -1
- package/presets/bindings/claude-native.toml +52 -0
- package/presets/bindings/codex-openai.toml +56 -0
- package/presets/bindings/cursor-fable-grok.toml +49 -0
- package/presets/bindings/cursor-openai.toml +49 -0
- package/presets/bindings/mixed-host.toml +64 -0
- package/presets/policies/balanced.toml +50 -0
- package/presets/policies/low-usage.toml +50 -0
- package/presets/policies/max-quality.toml +50 -0
- package/presets/policies/read-only-audit.toml +50 -0
- package/website/README.md +79 -0
- package/website/_headers +7 -0
- package/website/alchemy-runtime.test.mjs +21 -0
- package/website/app.mjs +216 -0
- package/website/build-catalog.mjs +135 -0
- package/website/build-site.test.mjs +69 -0
- package/website/catalog-model.mjs +185 -0
- package/website/catalog-model.test.mjs +124 -0
- package/website/cloudflare-launcher.test.mjs +38 -0
- package/website/data/catalog.json +307 -0
- package/website/index.html +122 -0
- package/website/registry/manifest.toml +48 -0
- package/website/registry/report.md +33 -0
- package/website/registry/trusted-maintainers.toml +6 -0
- package/website/registry/verification.json +7258 -0
- package/website/serve.mjs +41 -0
- package/website/styles.css +201 -0
- package/website/test-fixtures/recommended.json +72 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
assertAlchemyRuntime,
|
|
5
|
+
MINIMUM_ALCHEMY_NODE_MAJOR,
|
|
6
|
+
} from "../scripts/check-alchemy-runtime.mjs";
|
|
7
|
+
|
|
8
|
+
test("accepts the minimum supported Alchemy deployment runtime", () => {
|
|
9
|
+
assert.equal(assertAlchemyRuntime("22.0.0"), MINIMUM_ALCHEMY_NODE_MAJOR);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("rejects Node runtimes supported by the Planr CLI but not by Alchemy", () => {
|
|
13
|
+
assert.throws(
|
|
14
|
+
() => assertAlchemyRuntime("18.16.1"),
|
|
15
|
+
/Cloudflare deployment requires Node\.js 22 or newer; current runtime is 18\.16\.1/,
|
|
16
|
+
);
|
|
17
|
+
assert.throws(
|
|
18
|
+
() => assertAlchemyRuntime("20.19.5"),
|
|
19
|
+
/Cloudflare deployment requires Node\.js 22 or newer/,
|
|
20
|
+
);
|
|
21
|
+
});
|
package/website/app.mjs
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { formatBasisPoints, previewCommand, statusLabel, visibleCompositions } from "./catalog-model.mjs";
|
|
2
|
+
|
|
3
|
+
const nodes = {
|
|
4
|
+
selectA: document.querySelector("#composition-a"),
|
|
5
|
+
selectB: document.querySelector("#composition-b"),
|
|
6
|
+
cardA: document.querySelector("#comparison-a"),
|
|
7
|
+
cardB: document.querySelector("#comparison-b"),
|
|
8
|
+
comparison: document.querySelector("#comparison"),
|
|
9
|
+
empty: document.querySelector("#empty-state"),
|
|
10
|
+
emptyMessage: document.querySelector("#empty-message"),
|
|
11
|
+
filter: document.querySelector("#recommended-only"),
|
|
12
|
+
published: document.querySelector("#stat-published"),
|
|
13
|
+
recommended: document.querySelector("#stat-recommended"),
|
|
14
|
+
trust: document.querySelector("#stat-trust"),
|
|
15
|
+
generated: document.querySelector("#generated-at"),
|
|
16
|
+
copyStatus: document.querySelector("#copy-status"),
|
|
17
|
+
fixtureBanner: document.querySelector("#fixture-banner"),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function catalogLocation() {
|
|
21
|
+
const local = ["127.0.0.1", "localhost"].includes(location.hostname);
|
|
22
|
+
const fixture = new URLSearchParams(location.search).get("fixture");
|
|
23
|
+
if (local && fixture === "recommended") {
|
|
24
|
+
nodes.fixtureBanner.hidden = false;
|
|
25
|
+
return "./test-fixtures/recommended.json";
|
|
26
|
+
}
|
|
27
|
+
return "./data/catalog.json";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function text(parent, element, value, className) {
|
|
31
|
+
const node = document.createElement(element);
|
|
32
|
+
if (className) node.className = className;
|
|
33
|
+
node.textContent = value;
|
|
34
|
+
parent.append(node);
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatDate(unix) {
|
|
39
|
+
if (!Number.isFinite(unix)) return "Not published";
|
|
40
|
+
return new Intl.DateTimeFormat("en", { dateStyle: "medium", timeZone: "UTC" }).format(new Date(unix * 1000));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatNumber(value) {
|
|
44
|
+
return Number.isFinite(Number(value)) ? new Intl.NumberFormat("en").format(Number(value)) : "—";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function optionLabel(entry) {
|
|
48
|
+
return `${entry.policy.id} + ${entry.binding.id} · ${statusLabel(entry.status)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fillSelect(select, entries, preferredIndex) {
|
|
52
|
+
const previous = select.value;
|
|
53
|
+
select.replaceChildren();
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const option = document.createElement("option");
|
|
56
|
+
option.value = entry.id;
|
|
57
|
+
option.textContent = optionLabel(entry);
|
|
58
|
+
select.append(option);
|
|
59
|
+
}
|
|
60
|
+
select.value = entries.some((entry) => entry.id === previous) ? previous : entries[Math.min(preferredIndex, entries.length - 1)]?.id ?? "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function metric(parent, label, value) {
|
|
64
|
+
const wrapper = document.createElement("div");
|
|
65
|
+
wrapper.className = "metric";
|
|
66
|
+
text(wrapper, "span", label);
|
|
67
|
+
text(wrapper, "strong", value);
|
|
68
|
+
parent.append(wrapper);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function section(parent, title) {
|
|
72
|
+
const wrapper = document.createElement("section");
|
|
73
|
+
wrapper.className = "card-section";
|
|
74
|
+
text(wrapper, "h4", title);
|
|
75
|
+
parent.append(wrapper);
|
|
76
|
+
return wrapper;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function definitionRow(list, termValue, detailValue) {
|
|
80
|
+
const row = document.createElement("div");
|
|
81
|
+
text(row, "dt", termValue);
|
|
82
|
+
text(row, "dd", detailValue);
|
|
83
|
+
list.append(row);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderCard(card, entry) {
|
|
87
|
+
card.replaceChildren();
|
|
88
|
+
if (!entry) return;
|
|
89
|
+
card.dataset.compositionId = entry.id;
|
|
90
|
+
|
|
91
|
+
const top = document.createElement("div");
|
|
92
|
+
top.className = "card-top";
|
|
93
|
+
const title = document.createElement("div");
|
|
94
|
+
text(title, "span", `${entry.binding.id} · ${entry.binding.host}`, "binding-name");
|
|
95
|
+
text(title, "h3", entry.policy.id);
|
|
96
|
+
const badge = text(top, "span", statusLabel(entry.status), `badge status-${entry.status}`);
|
|
97
|
+
badge.setAttribute("aria-label", `Status: ${statusLabel(entry.status)}`);
|
|
98
|
+
top.prepend(title);
|
|
99
|
+
card.append(top);
|
|
100
|
+
text(card, "p", `Policy ${entry.policy.version} · Binding ${entry.binding.version} · Entry ${entry.entryVersion}`, "meta-line");
|
|
101
|
+
|
|
102
|
+
const metrics = document.createElement("div");
|
|
103
|
+
metrics.className = "metric-grid";
|
|
104
|
+
metric(metrics, "Active agents", formatNumber(entry.policy.usage.max_active_agents));
|
|
105
|
+
metric(metrics, "Parallel writers", formatNumber(entry.policy.usage.max_parallel_writers));
|
|
106
|
+
metric(metrics, "Max depth", formatNumber(entry.policy.usage.max_depth));
|
|
107
|
+
metric(metrics, "Metering", entry.policy.usage.metering ?? "unavailable");
|
|
108
|
+
card.append(metrics);
|
|
109
|
+
|
|
110
|
+
const compatibility = section(card, "Compatibility");
|
|
111
|
+
const compatibilityList = document.createElement("dl");
|
|
112
|
+
compatibilityList.className = "compatibility-list";
|
|
113
|
+
definitionRow(
|
|
114
|
+
compatibilityList,
|
|
115
|
+
"Supported hosts",
|
|
116
|
+
entry.compatibility.hosts?.length ? entry.compatibility.hosts.join(", ") : "No compatible host declared",
|
|
117
|
+
);
|
|
118
|
+
definitionRow(
|
|
119
|
+
compatibilityList,
|
|
120
|
+
"Planr versions",
|
|
121
|
+
`${entry.compatibility.minPlanrVersion ?? "Any"} through ${entry.compatibility.maxPlanrVersion ?? "latest"}`,
|
|
122
|
+
);
|
|
123
|
+
definitionRow(
|
|
124
|
+
compatibilityList,
|
|
125
|
+
"Lifecycle",
|
|
126
|
+
entry.replacement
|
|
127
|
+
? `${statusLabel(entry.status)}; replace with ${entry.replacement}`
|
|
128
|
+
: statusLabel(entry.status),
|
|
129
|
+
);
|
|
130
|
+
compatibility.append(compatibilityList);
|
|
131
|
+
|
|
132
|
+
const enforcement = section(card, "Enforcement matrix");
|
|
133
|
+
const list = document.createElement("dl");
|
|
134
|
+
list.className = "enforcement-list";
|
|
135
|
+
for (const item of entry.enforcement) {
|
|
136
|
+
const row = document.createElement("div");
|
|
137
|
+
const term = text(row, "dt", item.dimension);
|
|
138
|
+
text(term, "span", item.state.replaceAll("_", " "), "state");
|
|
139
|
+
text(row, "dd", item.detail);
|
|
140
|
+
list.append(row);
|
|
141
|
+
}
|
|
142
|
+
enforcement.append(list);
|
|
143
|
+
|
|
144
|
+
const evidence = section(card, "Evaluation & provenance");
|
|
145
|
+
text(evidence, "p", `${entry.evaluation.suiteId} ${entry.evaluation.suiteVersion} · evaluated ${formatDate(entry.evaluation.evaluatedAtUnix)} · review ${formatDate(entry.evaluation.reviewAtUnix)}`, "meta-line");
|
|
146
|
+
text(evidence, "p", `Quality ${formatBasisPoints(entry.evaluation.metrics?.average_quality_score_bps)} · runs ${formatNumber(entry.evaluation.metrics?.runs)} · oracle passes ${formatNumber(entry.evaluation.metrics?.oracle_passes)}`, "meta-line");
|
|
147
|
+
text(evidence, "p", `Manifest ${entry.registry.manifestSha256}`, "hash");
|
|
148
|
+
text(evidence, "p", `Signer ${entry.registry.signer} · signature verified · trusted maintainer`, "meta-line");
|
|
149
|
+
|
|
150
|
+
const commandSection = section(card, "Preview safely");
|
|
151
|
+
const commandBlock = document.createElement("div");
|
|
152
|
+
commandBlock.className = "command-block";
|
|
153
|
+
const command = previewCommand(entry.policy.id, entry.binding.id);
|
|
154
|
+
text(commandBlock, "code", command);
|
|
155
|
+
const button = text(commandBlock, "button", "Copy preview command", "copy-button");
|
|
156
|
+
button.type = "button";
|
|
157
|
+
button.dataset.command = command;
|
|
158
|
+
commandSection.append(commandBlock);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function wireCopy() {
|
|
162
|
+
document.addEventListener("click", async (event) => {
|
|
163
|
+
const button = event.target.closest("button[data-command]");
|
|
164
|
+
if (!button) return;
|
|
165
|
+
try {
|
|
166
|
+
await navigator.clipboard.writeText(button.dataset.command);
|
|
167
|
+
button.textContent = "Copied";
|
|
168
|
+
nodes.copyStatus.textContent = `Copied preview command for ${button.closest(".preset-card").dataset.compositionId}`;
|
|
169
|
+
setTimeout(() => { button.textContent = "Copy preview command"; }, 1400);
|
|
170
|
+
} catch {
|
|
171
|
+
nodes.copyStatus.textContent = "Clipboard access was unavailable; select the command text to copy it manually.";
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function render(catalog) {
|
|
177
|
+
const entries = visibleCompositions(catalog, nodes.filter.checked);
|
|
178
|
+
nodes.published.textContent = formatNumber(catalog.compositions.length);
|
|
179
|
+
nodes.recommended.textContent = formatNumber(catalog.compositions.filter((entry) => entry.recommended).length);
|
|
180
|
+
nodes.trust.textContent = catalog.source?.state === "verified_registry_projection" ? "Signed registry" : "Publication gated";
|
|
181
|
+
nodes.generated.textContent = Number.isFinite(catalog.generatedAtUnix)
|
|
182
|
+
? `Projection generated ${formatDate(catalog.generatedAtUnix)} · ${catalog.source.trust}`
|
|
183
|
+
: "No signed registry projection published.";
|
|
184
|
+
fillSelect(nodes.selectA, entries, 0);
|
|
185
|
+
fillSelect(nodes.selectB, entries, 1);
|
|
186
|
+
const empty = entries.length === 0;
|
|
187
|
+
nodes.empty.hidden = !empty;
|
|
188
|
+
nodes.comparison.hidden = empty;
|
|
189
|
+
if (empty) {
|
|
190
|
+
nodes.emptyMessage.textContent = catalog.source?.message ?? (nodes.filter.checked ? "No published composition currently carries an evaluation-backed recommendation." : "No trusted compositions are published.");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const find = (id) => entries.find((entry) => entry.id === id);
|
|
194
|
+
renderCard(nodes.cardA, find(nodes.selectA.value));
|
|
195
|
+
renderCard(nodes.cardB, find(nodes.selectB.value));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function start() {
|
|
199
|
+
wireCopy();
|
|
200
|
+
const response = await fetch(catalogLocation(), { cache: "no-store" });
|
|
201
|
+
if (!response.ok) throw new Error(`catalog request failed with HTTP ${response.status}`);
|
|
202
|
+
const catalog = await response.json();
|
|
203
|
+
if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.compositions)) throw new Error("unsupported catalog data");
|
|
204
|
+
const update = () => render(catalog);
|
|
205
|
+
nodes.filter.addEventListener("change", update);
|
|
206
|
+
nodes.selectA.addEventListener("change", update);
|
|
207
|
+
nodes.selectB.addEventListener("change", update);
|
|
208
|
+
update();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
start().catch((error) => {
|
|
212
|
+
nodes.empty.hidden = false;
|
|
213
|
+
nodes.comparison.hidden = true;
|
|
214
|
+
nodes.emptyMessage.textContent = `Catalog unavailable: ${error.message}`;
|
|
215
|
+
nodes.trust.textContent = "Unavailable";
|
|
216
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { projectComposition, safeIdentifier } from "./catalog-model.mjs";
|
|
8
|
+
|
|
9
|
+
function usage(message) {
|
|
10
|
+
if (message) console.error(message);
|
|
11
|
+
console.error(
|
|
12
|
+
"usage: node website/build-catalog.mjs --manifest <registry.toml> --content-root <dir> --trust-store <trusted.toml> --entry <id=host> [--entry <id=host> ...] --at-unix <unix> --output <catalog.json> [--planr-bin <planr>]",
|
|
13
|
+
);
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const values = { entries: [], planrBin: "planr" };
|
|
19
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
20
|
+
const flag = argv[index];
|
|
21
|
+
const value = argv[index + 1];
|
|
22
|
+
if (!value) usage(`missing value for ${flag}`);
|
|
23
|
+
index += 1;
|
|
24
|
+
if (flag === "--entry") values.entries.push(value);
|
|
25
|
+
else if (flag === "--manifest") values.manifest = value;
|
|
26
|
+
else if (flag === "--content-root") values.contentRoot = value;
|
|
27
|
+
else if (flag === "--trust-store") values.trustStore = value;
|
|
28
|
+
else if (flag === "--at-unix") values.atUnix = Number(value);
|
|
29
|
+
else if (flag === "--output") values.output = value;
|
|
30
|
+
else if (flag === "--planr-bin") values.planrBin = value;
|
|
31
|
+
else usage(`unknown argument ${flag}`);
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
!values.manifest ||
|
|
35
|
+
!values.contentRoot ||
|
|
36
|
+
!values.trustStore ||
|
|
37
|
+
!values.output ||
|
|
38
|
+
!Number.isSafeInteger(values.atUnix) ||
|
|
39
|
+
values.entries.length === 0
|
|
40
|
+
) {
|
|
41
|
+
usage("all required arguments must be provided");
|
|
42
|
+
}
|
|
43
|
+
return values;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function runJson(binary, args, cwd) {
|
|
47
|
+
const result = spawnSync(binary, args, { cwd, encoding: "utf8" });
|
|
48
|
+
if (result.error) throw result.error;
|
|
49
|
+
if (result.status !== 0) {
|
|
50
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || `${binary} exited ${result.status}`);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(result.stdout);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(`${binary} returned invalid JSON: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function entrySpec(raw) {
|
|
60
|
+
const separator = raw.lastIndexOf("=");
|
|
61
|
+
if (separator < 1 || separator === raw.length - 1) usage(`invalid --entry ${raw}; expected id=host`);
|
|
62
|
+
return { id: raw.slice(0, separator), host: raw.slice(separator + 1) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function artifactPath(verified, kind, contentRoot) {
|
|
66
|
+
const matches = verified.entry.artifacts.filter((artifact) => artifact.kind === kind);
|
|
67
|
+
if (matches.length !== 1) throw new Error(`entry ${verified.entry.id} must contain one ${kind}`);
|
|
68
|
+
return resolve(contentRoot, matches[0].path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const options = parseArgs(process.argv.slice(2));
|
|
72
|
+
const invocationRoot = process.cwd();
|
|
73
|
+
const planrBin =
|
|
74
|
+
isAbsolute(options.planrBin) || !options.planrBin.includes("/")
|
|
75
|
+
? options.planrBin
|
|
76
|
+
: resolve(invocationRoot, options.planrBin);
|
|
77
|
+
const manifest = resolve(invocationRoot, options.manifest);
|
|
78
|
+
const contentRoot = resolve(invocationRoot, options.contentRoot);
|
|
79
|
+
const trustStore = resolve(invocationRoot, options.trustStore);
|
|
80
|
+
const scratch = mkdtempSync(join(tmpdir(), "planr-preset-site-"));
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const compositions = options.entries.map((raw) => {
|
|
84
|
+
const { id, host } = entrySpec(raw);
|
|
85
|
+
const verified = runJson(
|
|
86
|
+
planrBin,
|
|
87
|
+
[
|
|
88
|
+
"--json",
|
|
89
|
+
"agents",
|
|
90
|
+
"preset",
|
|
91
|
+
"registry",
|
|
92
|
+
"verify",
|
|
93
|
+
manifest,
|
|
94
|
+
"--entry",
|
|
95
|
+
id,
|
|
96
|
+
"--content-root",
|
|
97
|
+
contentRoot,
|
|
98
|
+
"--trust-store",
|
|
99
|
+
trustStore,
|
|
100
|
+
"--at-unix",
|
|
101
|
+
String(options.atUnix),
|
|
102
|
+
"--host",
|
|
103
|
+
host,
|
|
104
|
+
],
|
|
105
|
+
scratch,
|
|
106
|
+
);
|
|
107
|
+
const verificationPath = artifactPath(verified, "verification", contentRoot);
|
|
108
|
+
const policyId = safeIdentifier(verified.entry.evaluation?.policy_id, "policy id");
|
|
109
|
+
const bindingId = safeIdentifier(verified.entry.evaluation?.binding_id, "binding id");
|
|
110
|
+
const preview = runJson(
|
|
111
|
+
planrBin,
|
|
112
|
+
["--json", "agents", "preset", "apply", policyId, "--binding", bindingId],
|
|
113
|
+
scratch,
|
|
114
|
+
);
|
|
115
|
+
const verificationEnvelope = JSON.parse(readFileSync(verificationPath, "utf8"));
|
|
116
|
+
return projectComposition({ verified, preview, verificationEnvelope });
|
|
117
|
+
});
|
|
118
|
+
const catalog = {
|
|
119
|
+
schemaVersion: 1,
|
|
120
|
+
generatedAtUnix: options.atUnix,
|
|
121
|
+
source: {
|
|
122
|
+
state: "verified_registry_projection",
|
|
123
|
+
manifest: options.manifest,
|
|
124
|
+
entryCount: compositions.length,
|
|
125
|
+
trust: "planr_registry_v1",
|
|
126
|
+
},
|
|
127
|
+
compositions,
|
|
128
|
+
};
|
|
129
|
+
const output = resolve(invocationRoot, options.output);
|
|
130
|
+
await mkdir(dirname(output), { recursive: true });
|
|
131
|
+
writeFileSync(output, `${JSON.stringify(catalog, null, 2)}\n`, { mode: 0o644 });
|
|
132
|
+
console.log(`wrote ${compositions.length} verified composition(s) to ${output}`);
|
|
133
|
+
} finally {
|
|
134
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
135
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
buildSite,
|
|
8
|
+
PUBLIC_SITE_FILES,
|
|
9
|
+
verifyPublication,
|
|
10
|
+
} from "../scripts/build-site.mjs";
|
|
11
|
+
|
|
12
|
+
async function fixture() {
|
|
13
|
+
const root = await mkdtemp(join(tmpdir(), "planr-site-build-"));
|
|
14
|
+
const sourceRoot = join(root, "website");
|
|
15
|
+
const outputRoot = join(root, "dist", "website");
|
|
16
|
+
for (const relativePath of PUBLIC_SITE_FILES) {
|
|
17
|
+
const path = join(sourceRoot, relativePath);
|
|
18
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
19
|
+
await writeFile(path, `public:${relativePath}\n`);
|
|
20
|
+
}
|
|
21
|
+
return { root, sourceRoot, outputRoot };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("builds only the public runtime allowlist and preserves nested catalog bytes", async (t) => {
|
|
25
|
+
const paths = await fixture();
|
|
26
|
+
t.after(() => rm(paths.root, { recursive: true, force: true }));
|
|
27
|
+
await writeFile(join(paths.sourceRoot, "build-catalog.mjs"), "must not be public\n");
|
|
28
|
+
await mkdir(join(paths.sourceRoot, "registry"), { recursive: true });
|
|
29
|
+
await writeFile(join(paths.sourceRoot, "registry", "trusted-maintainers.toml"), "not public\n");
|
|
30
|
+
|
|
31
|
+
const files = await buildSite(paths);
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(files, [...PUBLIC_SITE_FILES].sort());
|
|
34
|
+
assert.equal(
|
|
35
|
+
await readFile(join(paths.outputRoot, "data", "catalog.json"), "utf8"),
|
|
36
|
+
"public:data/catalog.json\n",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("fails before replacing output when a required source artifact is missing", async (t) => {
|
|
41
|
+
const paths = await fixture();
|
|
42
|
+
t.after(() => rm(paths.root, { recursive: true, force: true }));
|
|
43
|
+
await mkdir(paths.outputRoot, { recursive: true });
|
|
44
|
+
await writeFile(join(paths.outputRoot, "sentinel"), "keep on validation failure\n");
|
|
45
|
+
await rm(join(paths.sourceRoot, "styles.css"));
|
|
46
|
+
|
|
47
|
+
await assert.rejects(() => buildSite(paths), /missing public website artifact: styles\.css/);
|
|
48
|
+
assert.equal(await readFile(join(paths.outputRoot, "sentinel"), "utf8"), "keep on validation failure\n");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("publication verification rejects unexpected nested output", async (t) => {
|
|
52
|
+
const paths = await fixture();
|
|
53
|
+
t.after(() => rm(paths.root, { recursive: true, force: true }));
|
|
54
|
+
await buildSite(paths);
|
|
55
|
+
await mkdir(join(paths.outputRoot, "registry"), { recursive: true });
|
|
56
|
+
await writeFile(join(paths.outputRoot, "registry", "manifest.toml"), "unexpected\n");
|
|
57
|
+
|
|
58
|
+
await assert.rejects(() => verifyPublication(paths.outputRoot), /publish output mismatch/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("refuses an output directory inside the source tree", async (t) => {
|
|
62
|
+
const paths = await fixture();
|
|
63
|
+
t.after(() => rm(paths.root, { recursive: true, force: true }));
|
|
64
|
+
|
|
65
|
+
await assert.rejects(
|
|
66
|
+
() => buildSite({ sourceRoot: paths.sourceRoot, outputRoot: join(paths.sourceRoot, "dist") }),
|
|
67
|
+
/must not be inside/,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const IDENTIFIER = /^[A-Za-z0-9._-]+$/;
|
|
2
|
+
|
|
3
|
+
export function safeIdentifier(value, label = "identifier") {
|
|
4
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value)) {
|
|
5
|
+
throw new Error(`${label} is not a safe registry identifier`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function previewCommand(policyId, bindingId) {
|
|
11
|
+
return `planr agents preset apply ${safeIdentifier(policyId, "policy id")} --binding ${safeIdentifier(bindingId, "binding id")}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function statusLabel(status) {
|
|
15
|
+
const labels = {
|
|
16
|
+
experimental: "Experimental",
|
|
17
|
+
verified: "Verified",
|
|
18
|
+
recommended: "Recommended",
|
|
19
|
+
stale: "Stale",
|
|
20
|
+
deprecated: "Deprecated",
|
|
21
|
+
};
|
|
22
|
+
return labels[status] ?? "Unverified";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatBasisPoints(value) {
|
|
26
|
+
const basisPoints = Number(value);
|
|
27
|
+
return Number.isFinite(basisPoints) ? `${(basisPoints / 100).toFixed(2)}%` : "—";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findArtifact(entry, kind) {
|
|
31
|
+
const artifacts = entry.artifacts.filter((artifact) => artifact.kind === kind);
|
|
32
|
+
if (artifacts.length !== 1) {
|
|
33
|
+
throw new Error(`registry entry must contain exactly one ${kind} artifact`);
|
|
34
|
+
}
|
|
35
|
+
return artifacts[0];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function proposedConfig(preview, kind) {
|
|
39
|
+
const artifact = preview.artifacts.find((candidate) => candidate.kind === kind);
|
|
40
|
+
const value = artifact?.config_diff?.proposed?.value;
|
|
41
|
+
if (!value || typeof value !== "object") {
|
|
42
|
+
throw new Error(`preset preview is missing proposed ${kind} configuration`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function matchingCandidate(report, policyId, bindingId) {
|
|
48
|
+
const candidate = report.candidates?.find(
|
|
49
|
+
(value) => value.policy?.id === policyId && value.binding?.id === bindingId,
|
|
50
|
+
);
|
|
51
|
+
if (!candidate) {
|
|
52
|
+
throw new Error(`evaluation report has no candidate for ${policyId} + ${bindingId}`);
|
|
53
|
+
}
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recommendationMatches(report, policyId, bindingId) {
|
|
58
|
+
return Boolean(
|
|
59
|
+
report.recommended?.some(
|
|
60
|
+
(value) => value.policy === policyId && value.binding === bindingId && value.status === "recommended",
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function projectComposition({ verified, preview, verificationEnvelope }) {
|
|
66
|
+
if (!verified?.integrity_verified || !verified?.signature_verified || !verified?.trusted_maintainer) {
|
|
67
|
+
throw new Error("website projection requires an integrity-verified entry signed by a trusted maintainer");
|
|
68
|
+
}
|
|
69
|
+
if (!verified.compatible) {
|
|
70
|
+
throw new Error("website projection requires a compatible registry entry");
|
|
71
|
+
}
|
|
72
|
+
if (!preview?.pack?.safe) {
|
|
73
|
+
throw new Error("website projection requires a canonical safe-pack preview");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const entry = verified.entry;
|
|
77
|
+
const evaluation = entry.evaluation;
|
|
78
|
+
if (!evaluation) {
|
|
79
|
+
throw new Error("website projection requires evaluation binding metadata");
|
|
80
|
+
}
|
|
81
|
+
const policyId = safeIdentifier(evaluation.policy_id, "policy id");
|
|
82
|
+
const bindingId = safeIdentifier(evaluation.binding_id, "binding id");
|
|
83
|
+
const report = verificationEnvelope.report ?? verificationEnvelope;
|
|
84
|
+
const candidate = matchingCandidate(report, policyId, bindingId);
|
|
85
|
+
const reportRecommended = recommendationMatches(report, policyId, bindingId);
|
|
86
|
+
if (verified.recommended && (!reportRecommended || candidate.status !== "recommended")) {
|
|
87
|
+
throw new Error("registry recommendation does not match canonical evaluation evidence");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const policy = proposedConfig(preview, "active_policy");
|
|
91
|
+
const agents = proposedConfig(preview, "agent_registry");
|
|
92
|
+
if (policy.id !== policyId || preview.composition?.binding?.id !== bindingId) {
|
|
93
|
+
throw new Error("preset preview provenance does not match registry evaluation binding");
|
|
94
|
+
}
|
|
95
|
+
const runs = Number(candidate.metrics?.runs ?? 0);
|
|
96
|
+
const verifiedRoutes = Number(candidate.metrics?.verified_route_runs ?? 0);
|
|
97
|
+
const policyArtifact = findArtifact(entry, "policy");
|
|
98
|
+
const bindingArtifact = findArtifact(entry, "host-binding");
|
|
99
|
+
const verificationArtifact = findArtifact(entry, "verification");
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
id: `${safeIdentifier(entry.id, "entry id")}@${entry.version}`,
|
|
103
|
+
entryId: entry.id,
|
|
104
|
+
entryVersion: entry.version,
|
|
105
|
+
status: verified.effective_status,
|
|
106
|
+
statusLabel: statusLabel(verified.effective_status),
|
|
107
|
+
recommended: verified.recommended,
|
|
108
|
+
freshness: verified.freshness,
|
|
109
|
+
lifecycle: entry.lifecycle,
|
|
110
|
+
replacement: entry.replacement ?? null,
|
|
111
|
+
policy: {
|
|
112
|
+
id: policyId,
|
|
113
|
+
version: evaluation.policy_version,
|
|
114
|
+
usage: policy.usage,
|
|
115
|
+
transitions: policy.transitions,
|
|
116
|
+
materiality: policy.materiality,
|
|
117
|
+
execution: policy.execution,
|
|
118
|
+
},
|
|
119
|
+
binding: {
|
|
120
|
+
id: bindingId,
|
|
121
|
+
version: evaluation.binding_version,
|
|
122
|
+
host: preview.composition.host,
|
|
123
|
+
profiles: agents.profiles,
|
|
124
|
+
dispatch: preview.composition.dispatch,
|
|
125
|
+
},
|
|
126
|
+
compatibility: {
|
|
127
|
+
hosts: entry.compatible_hosts,
|
|
128
|
+
minPlanrVersion: entry.min_planr_version,
|
|
129
|
+
maxPlanrVersion: entry.max_planr_version,
|
|
130
|
+
},
|
|
131
|
+
enforcement: [
|
|
132
|
+
{
|
|
133
|
+
dimension: "Policy limits",
|
|
134
|
+
state: "verified",
|
|
135
|
+
detail: "Planr validates count, time, concurrency, transition, and safety-stop limits before dispatch.",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
dimension: "Execution permissions",
|
|
139
|
+
state: "verified",
|
|
140
|
+
detail: "Registry-safe policy: no commands, hooks, network/MCP grants, secrets, or overwrite permission.",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
dimension: "Model and effort",
|
|
144
|
+
state: "host_enforced",
|
|
145
|
+
detail: "The host binding requests concrete routes; the host retains final execution authority.",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
dimension: "Effective route evidence",
|
|
149
|
+
state: runs > 0 && verifiedRoutes === runs ? "verified" : "unavailable",
|
|
150
|
+
detail: `${verifiedRoutes} of ${runs} evaluation runs carried verified effective-route evidence.`,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
evaluation: {
|
|
154
|
+
suiteId: report.suite?.id,
|
|
155
|
+
suiteVersion: report.suite?.version,
|
|
156
|
+
evaluatedAtUnix: report.suite?.evaluated_at_unix,
|
|
157
|
+
reviewAtUnix: entry.review_at_unix,
|
|
158
|
+
status: candidate.status,
|
|
159
|
+
metrics: candidate.metrics,
|
|
160
|
+
thresholds: candidate.threshold_results,
|
|
161
|
+
resultHashes: candidate.results?.map((result) => result.result_sha256) ?? [],
|
|
162
|
+
fixtureSha256: report.suite?.fixture_sha256,
|
|
163
|
+
},
|
|
164
|
+
registry: {
|
|
165
|
+
id: verified.registry_id,
|
|
166
|
+
version: verified.registry_version,
|
|
167
|
+
manifestSha256: verified.manifest_sha256,
|
|
168
|
+
signer: entry.signature?.signer,
|
|
169
|
+
signatureVerified: true,
|
|
170
|
+
trustedMaintainer: true,
|
|
171
|
+
artifacts: [policyArtifact, bindingArtifact, verificationArtifact].map((artifact) => ({
|
|
172
|
+
path: artifact.path,
|
|
173
|
+
kind: artifact.kind,
|
|
174
|
+
sha256: artifact.sha256,
|
|
175
|
+
sizeBytes: artifact.size_bytes,
|
|
176
|
+
})),
|
|
177
|
+
},
|
|
178
|
+
command: previewCommand(policyId, bindingId),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function visibleCompositions(catalog, recommendedOnly = false) {
|
|
183
|
+
const compositions = Array.isArray(catalog?.compositions) ? catalog.compositions : [];
|
|
184
|
+
return recommendedOnly ? compositions.filter((entry) => entry.recommended) : compositions;
|
|
185
|
+
}
|