@piagent/platform 1.0.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/.gitattributes +7 -0
- package/AGENTS.md +31 -0
- package/CHANGELOG.md +296 -0
- package/CODE_OF_CONDUCT.md +36 -0
- package/CONTRIBUTING.md +66 -0
- package/LICENSE +21 -0
- package/README.md +588 -0
- package/SECURITY.md +38 -0
- package/adapters/backend-api/profile.json +88 -0
- package/adapters/be-readonly-fe/profile.json +120 -0
- package/adapters/data/profile.json +88 -0
- package/adapters/devops/profile.json +92 -0
- package/adapters/docs/profile.json +74 -0
- package/adapters/fullstack/profile.json +94 -0
- package/adapters/generic/profile.json +95 -0
- package/adapters/mobile/profile.json +87 -0
- package/adapters/node-typescript/profile.json +76 -0
- package/adapters/python/profile.json +75 -0
- package/adapters/web-frontend/profile.json +101 -0
- package/catalog/capabilities.json +206 -0
- package/docs/architecture.md +64 -0
- package/docs/auto-delegation-policy.md +192 -0
- package/docs/capability-packs.md +234 -0
- package/docs/command-reference-vietnamese.md +484 -0
- package/docs/context-window-policy.md +85 -0
- package/docs/distribution-standard.md +229 -0
- package/docs/herdr-workflow.md +58 -0
- package/docs/mcp-and-tools.md +316 -0
- package/docs/memory-policy.md +164 -0
- package/docs/model-options.md +165 -0
- package/docs/oauth-providers.md +53 -0
- package/docs/operator-manual-vietnamese.md +1080 -0
- package/docs/package-architecture-notes.md +101 -0
- package/docs/project-adapters.md +174 -0
- package/docs/project-onboarding.md +135 -0
- package/docs/publishing-for-teams.md +64 -0
- package/docs/quality-benchmark.md +74 -0
- package/docs/quickstart-vietnamese.md +350 -0
- package/docs/release-install-policy.md +223 -0
- package/docs/runtime-harness-standard.md +118 -0
- package/docs/runtime-policy-design.md +132 -0
- package/docs/runtime-quality-baseline.md +257 -0
- package/docs/security-redaction-benchmark.md +56 -0
- package/docs/security-threat-model.md +66 -0
- package/docs/subagent-orchestration-capabilities.md +104 -0
- package/docs/subagents-and-multiagent.md +346 -0
- package/docs/task-implementation-contract.md +200 -0
- package/docs/team-onboarding.md +333 -0
- package/docs/usage-observability.md +130 -0
- package/docs/vercel-docs-site.md +115 -0
- package/docs/workflow-recipes.md +125 -0
- package/docs-site/assets/piagent-logo.svg +35 -0
- package/docs-site/favicon.svg +18 -0
- package/docs-site/index.html +1613 -0
- package/docs-site/vercel.json +24 -0
- package/evals/fixtures/action-proposal.invalid.json +34 -0
- package/evals/fixtures/action-proposal.valid.json +34 -0
- package/evals/fixtures/capability-pack.invalid.json +123 -0
- package/evals/fixtures/capability-pack.valid.json +123 -0
- package/evals/fixtures/capability-recipe.invalid.json +81 -0
- package/evals/fixtures/capability-recipe.valid.json +82 -0
- package/evals/fixtures/context-index.invalid.json +25 -0
- package/evals/fixtures/context-index.valid.json +25 -0
- package/evals/fixtures/eval-scenario.invalid.json +40 -0
- package/evals/fixtures/eval-scenario.valid.json +45 -0
- package/evals/fixtures/project-profile.invalid.json +88 -0
- package/evals/fixtures/project-profile.valid.json +95 -0
- package/evals/fixtures/task-contract.invalid.json +73 -0
- package/evals/fixtures/task-contract.valid.json +74 -0
- package/evals/golden/enforcement-decisions.json +329 -0
- package/evals/scenarios/capability-resolution.json +45 -0
- package/package.json +132 -0
- package/packages/piagent-core/README.md +147 -0
- package/packages/piagent-core/capabilities/capability-core.js +1023 -0
- package/packages/piagent-core/capabilities/capability-sources.js +265 -0
- package/packages/piagent-core/extensions/guard-io.js +45 -0
- package/packages/piagent-core/extensions/guard-shell-analysis.ts +558 -0
- package/packages/piagent-core/extensions/guard-types.ts +406 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +5760 -0
- package/packages/piagent-core/extensions/policy-core.js +1070 -0
- package/packages/piagent-core/extensions/redaction-core.js +4 -0
- package/packages/piagent-core/extensions/runtime-evidence.js +215 -0
- package/packages/piagent-core/package.json +46 -0
- package/packages/piagent-core/policies/base-policy.json +249 -0
- package/packages/piagent-core/prompts/be-to-fe.md +82 -0
- package/packages/piagent-core/prompts/commit.md +27 -0
- package/packages/piagent-core/prompts/discuss.md +35 -0
- package/packages/piagent-core/prompts/memory-policy.md +52 -0
- package/packages/piagent-core/prompts/model-options.md +68 -0
- package/packages/piagent-core/prompts/onboard-project.md +147 -0
- package/packages/piagent-core/prompts/piagent-commands.md +68 -0
- package/packages/piagent-core/prompts/plan.md +35 -0
- package/packages/piagent-core/prompts/platform-improve.md +69 -0
- package/packages/piagent-core/prompts/pr.md +26 -0
- package/packages/piagent-core/prompts/review.md +38 -0
- package/packages/piagent-core/prompts/scout.md +50 -0
- package/packages/piagent-core/prompts/task.md +69 -0
- package/packages/piagent-core/security/sensitive-data.js +187 -0
- package/packages/piagent-core/skills/piagent-ops/SKILL.md +44 -0
- package/packages/piagent-core/skills/piagent-source-cache/SKILL.md +59 -0
- package/packages/piagent-core/skills/piagent-source-cache/checkout-source-repo.sh +144 -0
- package/packages/piagent-core/subagents/piagent-oracle.md +29 -0
- package/packages/piagent-core/subagents/piagent-planner.md +46 -0
- package/packages/piagent-core/subagents/piagent-reviewer.md +48 -0
- package/packages/piagent-core/subagents/piagent-scout.md +37 -0
- package/packages/piagent-core/subagents/piagent-worker.md +44 -0
- package/packs/engineering-base/pack.json +123 -0
- package/packs/engineering-base/recipes/bounded-change.json +82 -0
- package/packs/web-delivery/pack.json +72 -0
- package/packs/web-delivery/recipes/verified-web-change.json +57 -0
- package/schemas/action-proposal.schema.json +90 -0
- package/schemas/capability-pack.schema.json +190 -0
- package/schemas/capability-recipe.schema.json +72 -0
- package/schemas/context-index.schema.json +258 -0
- package/schemas/eval-scenario.schema.json +67 -0
- package/schemas/project-profile.schema.json +555 -0
- package/schemas/task-contract.schema.json +314 -0
- package/scripts/audit-runtime-host.sh +78 -0
- package/scripts/bootstrap-local.sh +25 -0
- package/scripts/capability-catalog.mjs +290 -0
- package/scripts/check-runtime-advisories.mjs +114 -0
- package/scripts/configure-mcp.sh +301 -0
- package/scripts/configure-model-scope.sh +143 -0
- package/scripts/configure-subagents.sh +241 -0
- package/scripts/import-agent-instructions.mjs +199 -0
- package/scripts/init-project.sh +369 -0
- package/scripts/install-global.sh +532 -0
- package/scripts/link-project.sh +25 -0
- package/scripts/migrate-project-state.mjs +176 -0
- package/scripts/pi-auto.sh +79 -0
- package/scripts/pi-model-catalog.sh +154 -0
- package/scripts/pi-session-stats.sh +135 -0
- package/scripts/piagent-cli.mjs +53 -0
- package/scripts/profile-doctor.sh +238 -0
- package/scripts/quality-benchmark.sh +185 -0
- package/scripts/redaction-benchmark.mjs +227 -0
- package/scripts/runtime-policy-smoke.sh +48 -0
- package/scripts/setup.sh +425 -0
- package/scripts/team-doctor.sh +461 -0
- package/scripts/verify-local.sh +540 -0
- package/scripts/verify-release-identity.mjs +85 -0
- package/scripts/verify-vercel-link.mjs +64 -0
- package/templates/global/mcp.json +9 -0
- package/templates/global/settings.json +37 -0
- package/templates/project/.mcp.json +9 -0
- package/templates/project/.pi/context-index.json +25 -0
- package/templates/project/.pi/gitignore.template +22 -0
- package/templates/project/.pi/mcp.json +9 -0
- package/templates/project/.pi/memory/MEMORY.md +11 -0
- package/templates/project/.pi/memory/memory_summary.md +9 -0
- package/templates/project/.pi/piagent-profile.json +107 -0
- package/templates/project/.pi/project-context.md +30 -0
- package/templates/project/.pi/settings.json +17 -0
- package/templates/project/.pi/task-contract.template.json +74 -0
- package/templates/project/.pi/tech-context/README.md +7 -0
- package/templates/project/.pi/tech-stack.json +10 -0
- package/templates/project/AGENTS.md +34 -0
- package/templates/project/REVIEW_GUIDELINES.md +23 -0
- package/types/pi-runtime-shims.d.ts +13 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import {
|
|
7
|
+
CapabilityValidationError,
|
|
8
|
+
buildCapabilityCatalog,
|
|
9
|
+
resolveCapabilityProfile,
|
|
10
|
+
resolveCapabilityProfileDocument,
|
|
11
|
+
stableJson,
|
|
12
|
+
validateCapabilityPackageSource,
|
|
13
|
+
validateExternalActionProposal,
|
|
14
|
+
verifyCapabilityLock,
|
|
15
|
+
writeJsonAtomic,
|
|
16
|
+
writeProfileLockAtomic
|
|
17
|
+
} from "../packages/piagent-core/capabilities/capability-core.js";
|
|
18
|
+
import {
|
|
19
|
+
resolveCapabilitySourceRoots,
|
|
20
|
+
validateCapabilitySources,
|
|
21
|
+
vendorRemoteSource
|
|
22
|
+
} from "../packages/piagent-core/capabilities/capability-sources.js";
|
|
23
|
+
|
|
24
|
+
const platformRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
|
+
const catalogPath = path.join(platformRoot, "catalog", "capabilities.json");
|
|
26
|
+
|
|
27
|
+
function usage() {
|
|
28
|
+
process.stdout.write(`Usage:
|
|
29
|
+
piagent-capabilities catalog [--check | --write]
|
|
30
|
+
piagent-capabilities resolve --profile <profile.json> [--output <lock.json>] [--package-source <source>]
|
|
31
|
+
piagent-capabilities apply-profile --profile <source.json> --target <piagent-profile.json> [--package-source <source>] [--force]
|
|
32
|
+
piagent-capabilities doctor [--profile <profile.json>] [--lock <lock.json>] [--package-source <source>]
|
|
33
|
+
piagent-capabilities vendor --profile <profile.json> [--source <name>]
|
|
34
|
+
piagent-capabilities validate-source --package-source <source>
|
|
35
|
+
piagent-capabilities validate-action --file <proposal.json>
|
|
36
|
+
|
|
37
|
+
Commands:
|
|
38
|
+
catalog Build the deterministic capability catalog.
|
|
39
|
+
resolve Resolve a profile to an auditable lock document.
|
|
40
|
+
apply-profile Validate and update a profile with its lock as one fail-closed operation.
|
|
41
|
+
doctor Validate packs, profile grants, and an optional lock document.
|
|
42
|
+
vendor Fetch remote capability sources into the project. The only command that uses the network.
|
|
43
|
+
validate-source Require a local source or an exact remote version, tag, or commit.
|
|
44
|
+
validate-action Validate a dry-run external action proposal.
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseArguments(values) {
|
|
49
|
+
const output = { positional: [], flags: new Map() };
|
|
50
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
51
|
+
const value = values[index];
|
|
52
|
+
if (!value.startsWith("--")) {
|
|
53
|
+
output.positional.push(value);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (["--check", "--write", "--force", "--help"].includes(value)) {
|
|
57
|
+
if (output.flags.has(value)) throw new CapabilityValidationError(`duplicate option ${value}`);
|
|
58
|
+
output.flags.set(value, true);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!["--profile", "--output", "--target", "--lock", "--file", "--package-source", "--source"].includes(value)) throw new CapabilityValidationError(`unknown option ${value}`);
|
|
62
|
+
if (output.flags.has(value)) throw new CapabilityValidationError(`duplicate option ${value}`);
|
|
63
|
+
const next = values[index + 1];
|
|
64
|
+
if (!next || next.startsWith("--")) throw new CapabilityValidationError(`${value} requires a value`);
|
|
65
|
+
output.flags.set(value, next);
|
|
66
|
+
index += 1;
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertAllowedFlags(flags, allowed) {
|
|
72
|
+
for (const flag of flags.keys()) {
|
|
73
|
+
if (!allowed.has(flag)) throw new CapabilityValidationError(`${flag} is not valid for this command`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function requireExistingFile(value, option) {
|
|
78
|
+
if (!value) throw new CapabilityValidationError(`${option} is required`);
|
|
79
|
+
const absolute = path.resolve(value);
|
|
80
|
+
const stat = fs.lstatSync(absolute);
|
|
81
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new CapabilityValidationError(`${option} must identify a regular file`);
|
|
82
|
+
return fs.realpathSync(absolute);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readJson(file) {
|
|
86
|
+
const stat = fs.statSync(file);
|
|
87
|
+
if (stat.size > 256 * 1024) throw new CapabilityValidationError(`${file} exceeds the 262144-byte limit`);
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new CapabilityValidationError(`${file} contains invalid JSON`, [error instanceof Error ? error.message : String(error)]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertOutputBesideProfile(output, profile) {
|
|
96
|
+
const absolute = path.resolve(output);
|
|
97
|
+
const profileDirectory = path.dirname(profile);
|
|
98
|
+
const outputDirectory = fs.realpathSync(path.dirname(absolute));
|
|
99
|
+
if (outputDirectory !== profileDirectory) throw new CapabilityValidationError("--output must be in the same directory as --profile");
|
|
100
|
+
if (path.basename(absolute) !== "piagent-profile.lock.json") throw new CapabilityValidationError("--output filename must be piagent-profile.lock.json");
|
|
101
|
+
return path.join(outputDirectory, path.basename(absolute));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function packageSourceBesideProfile(profile) {
|
|
105
|
+
const settingsPath = path.join(path.dirname(profile), "settings.json");
|
|
106
|
+
if (!fs.existsSync(settingsPath)) return "workspace";
|
|
107
|
+
const settings = readJson(settingsPath);
|
|
108
|
+
const source = Array.isArray(settings.packages) ? settings.packages.find((item) => typeof item === "string" && item.length > 0) : undefined;
|
|
109
|
+
return source ?? "workspace";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A profile lives at <project>/.pi/piagent-profile.json, so its project root is
|
|
113
|
+
// two levels up. Local source paths and the vendor directory are resolved from
|
|
114
|
+
// there, never from the platform, which is the whole point: a project uses its
|
|
115
|
+
// own packs without forking this repository.
|
|
116
|
+
function projectRootForProfile(profileFile) {
|
|
117
|
+
return path.dirname(path.dirname(profileFile));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sourceRootsForProfile(profileFile) {
|
|
121
|
+
const declared = readJson(profileFile).capabilitySources;
|
|
122
|
+
if (declared === undefined) return undefined;
|
|
123
|
+
return resolveCapabilitySourceRoots(projectRootForProfile(profileFile), declared);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function runCatalog(flags) {
|
|
127
|
+
assertAllowedFlags(flags, new Set(["--check", "--write"]));
|
|
128
|
+
if (flags.has("--check") && flags.has("--write")) throw new CapabilityValidationError("--check and --write are mutually exclusive");
|
|
129
|
+
const catalog = buildCapabilityCatalog(platformRoot);
|
|
130
|
+
if (flags.has("--check")) {
|
|
131
|
+
if (!fs.existsSync(catalogPath)) throw new CapabilityValidationError("catalog/capabilities.json is missing");
|
|
132
|
+
const existing = fs.readFileSync(catalogPath, "utf8");
|
|
133
|
+
if (existing !== stableJson(catalog)) throw new CapabilityValidationError("catalog/capabilities.json is stale");
|
|
134
|
+
process.stdout.write(`${JSON.stringify({ ok: true, catalog: "catalog/capabilities.json", packs: catalog.packs.length })}\n`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (flags.has("--write")) {
|
|
138
|
+
writeJsonAtomic(catalogPath, catalog);
|
|
139
|
+
process.stdout.write(`${JSON.stringify({ ok: true, catalog: "catalog/capabilities.json", packs: catalog.packs.length })}\n`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
process.stdout.write(stableJson(catalog));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function runResolve(flags) {
|
|
146
|
+
assertAllowedFlags(flags, new Set(["--profile", "--output", "--package-source"]));
|
|
147
|
+
const profile = requireExistingFile(flags.get("--profile"), "--profile");
|
|
148
|
+
const packageSource = flags.get("--package-source") ?? "workspace";
|
|
149
|
+
const lock = resolveCapabilityProfile(platformRoot, profile, { packageSource, extraRoots: sourceRootsForProfile(profile) });
|
|
150
|
+
if (flags.has("--output")) {
|
|
151
|
+
const output = assertOutputBesideProfile(flags.get("--output"), profile);
|
|
152
|
+
writeJsonAtomic(output, lock);
|
|
153
|
+
process.stdout.write(`${JSON.stringify({ ok: true, output, packs: lock.packs.length })}\n`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
process.stdout.write(stableJson(lock));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runApplyProfile(flags) {
|
|
160
|
+
assertAllowedFlags(flags, new Set(["--profile", "--target", "--package-source", "--force"]));
|
|
161
|
+
const source = requireExistingFile(flags.get("--profile"), "--profile");
|
|
162
|
+
const targetValue = flags.get("--target");
|
|
163
|
+
if (!targetValue) throw new CapabilityValidationError("--target is required");
|
|
164
|
+
const target = path.resolve(targetValue);
|
|
165
|
+
if (path.basename(target) !== "piagent-profile.json") throw new CapabilityValidationError("--target filename must be piagent-profile.json");
|
|
166
|
+
const parent = fs.realpathSync(path.dirname(target));
|
|
167
|
+
const normalizedTarget = path.join(parent, path.basename(target));
|
|
168
|
+
if (fs.existsSync(normalizedTarget) && !flags.has("--force")) throw new CapabilityValidationError("target profile already exists; pass --force to replace it");
|
|
169
|
+
const profile = readJson(source);
|
|
170
|
+
const packageSource = flags.get("--package-source") ?? "workspace";
|
|
171
|
+
const lock = resolveCapabilityProfileDocument(platformRoot, profile, {
|
|
172
|
+
profileFile: path.basename(normalizedTarget),
|
|
173
|
+
packageSource,
|
|
174
|
+
extraRoots: profile.capabilitySources === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: resolveCapabilitySourceRoots(projectRootForProfile(normalizedTarget), profile.capabilitySources)
|
|
177
|
+
});
|
|
178
|
+
const lockTarget = path.join(parent, "piagent-profile.lock.json");
|
|
179
|
+
writeProfileLockAtomic(normalizedTarget, profile, lockTarget, lock);
|
|
180
|
+
process.stdout.write(`${JSON.stringify({ ok: true, profile: normalizedTarget, lock: lockTarget, packs: lock.packs.length })}\n`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function runDoctor(flags) {
|
|
184
|
+
assertAllowedFlags(flags, new Set(["--profile", "--lock", "--package-source"]));
|
|
185
|
+
const catalog = buildCapabilityCatalog(platformRoot);
|
|
186
|
+
const report = {
|
|
187
|
+
ok: true,
|
|
188
|
+
catalog: {
|
|
189
|
+
path: "catalog/capabilities.json",
|
|
190
|
+
packs: catalog.packs.length,
|
|
191
|
+
current: fs.existsSync(catalogPath) && fs.readFileSync(catalogPath, "utf8") === stableJson(catalog)
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
if (!report.catalog.current) throw new CapabilityValidationError("catalog/capabilities.json is missing or stale");
|
|
195
|
+
if (flags.has("--profile")) {
|
|
196
|
+
const profile = requireExistingFile(flags.get("--profile"), "--profile");
|
|
197
|
+
const packageSource = flags.get("--package-source") ?? packageSourceBesideProfile(profile);
|
|
198
|
+
const extraRoots = sourceRootsForProfile(profile);
|
|
199
|
+
const resolved = resolveCapabilityProfile(platformRoot, profile, { packageSource, extraRoots });
|
|
200
|
+
report.profile = {
|
|
201
|
+
path: profile,
|
|
202
|
+
projectId: resolved.profile.projectId,
|
|
203
|
+
packs: resolved.packs.map((pack) => `${pack.name}@${pack.version}`),
|
|
204
|
+
sources: (extraRoots ?? []).map((entry) => ({ name: entry.origin, source: entry.source })),
|
|
205
|
+
permissions: resolved.permissions
|
|
206
|
+
};
|
|
207
|
+
if (flags.has("--lock")) {
|
|
208
|
+
const lockFile = requireExistingFile(flags.get("--lock"), "--lock");
|
|
209
|
+
if (path.dirname(lockFile) !== path.dirname(profile)) throw new CapabilityValidationError("--lock must be in the same directory as --profile");
|
|
210
|
+
const verification = verifyCapabilityLock(platformRoot, profile, readJson(lockFile), { packageSource, extraRoots });
|
|
211
|
+
report.lock = {
|
|
212
|
+
path: lockFile,
|
|
213
|
+
current: verification.ok,
|
|
214
|
+
expectedDigest: verification.expectedDigest,
|
|
215
|
+
actualDigest: verification.actualDigest
|
|
216
|
+
};
|
|
217
|
+
if (!verification.ok) throw new CapabilityValidationError("capability lock is stale or has been modified");
|
|
218
|
+
}
|
|
219
|
+
} else if (flags.has("--lock")) {
|
|
220
|
+
throw new CapabilityValidationError("--lock requires --profile");
|
|
221
|
+
}
|
|
222
|
+
process.stdout.write(stableJson(report));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function runVendor(flags) {
|
|
226
|
+
assertAllowedFlags(flags, new Set(["--profile", "--source"]));
|
|
227
|
+
const profileFile = requireExistingFile(flags.get("--profile"), "--profile");
|
|
228
|
+
const projectRoot = projectRootForProfile(profileFile);
|
|
229
|
+
const declared = validateCapabilitySources(readJson(profileFile).capabilitySources, profileFile);
|
|
230
|
+
const remote = declared.filter((entry) => entry.source !== undefined);
|
|
231
|
+
const only = flags.get("--source");
|
|
232
|
+
if (only && !declared.some((entry) => entry.name === only)) {
|
|
233
|
+
throw new CapabilityValidationError(`${profileFile} declares no capability source named ${only}`);
|
|
234
|
+
}
|
|
235
|
+
const selected = only ? remote.filter((entry) => entry.name === only) : remote;
|
|
236
|
+
|
|
237
|
+
// Fetching replaces the vendored tree, so the digests are reported for the
|
|
238
|
+
// maintainer to compare against the commit they are about to make. This is
|
|
239
|
+
// the review step; nothing downstream reaches the network.
|
|
240
|
+
const vendored = selected.map((entry) => {
|
|
241
|
+
const result = vendorRemoteSource(projectRoot, entry);
|
|
242
|
+
return { name: result.name, source: result.source, digest: result.digest };
|
|
243
|
+
});
|
|
244
|
+
process.stdout.write(`${JSON.stringify({
|
|
245
|
+
ok: true,
|
|
246
|
+
projectRoot,
|
|
247
|
+
vendored,
|
|
248
|
+
localSources: declared.filter((entry) => entry.path !== undefined).map((entry) => entry.name)
|
|
249
|
+
})}\n`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function runValidateAction(flags) {
|
|
253
|
+
assertAllowedFlags(flags, new Set(["--file"]));
|
|
254
|
+
const file = requireExistingFile(flags.get("--file"), "--file");
|
|
255
|
+
validateExternalActionProposal(readJson(file), { source: file });
|
|
256
|
+
process.stdout.write(`${JSON.stringify({ ok: true, file })}\n`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function runValidateSource(flags) {
|
|
260
|
+
assertAllowedFlags(flags, new Set(["--package-source"]));
|
|
261
|
+
const source = flags.get("--package-source");
|
|
262
|
+
if (!source) throw new CapabilityValidationError("--package-source is required");
|
|
263
|
+
validateCapabilityPackageSource(source);
|
|
264
|
+
process.stdout.write(`${JSON.stringify({ ok: true, packageSource: source })}\n`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const parsed = parseArguments(process.argv.slice(2));
|
|
269
|
+
if (parsed.flags.has("--help") || parsed.positional.length === 0) {
|
|
270
|
+
usage();
|
|
271
|
+
process.exit(parsed.flags.has("--help") ? 0 : 2);
|
|
272
|
+
}
|
|
273
|
+
if (parsed.positional.length !== 1) throw new CapabilityValidationError("exactly one command is required");
|
|
274
|
+
const command = parsed.positional[0];
|
|
275
|
+
if (command === "catalog") runCatalog(parsed.flags);
|
|
276
|
+
else if (command === "resolve") runResolve(parsed.flags);
|
|
277
|
+
else if (command === "apply-profile") runApplyProfile(parsed.flags);
|
|
278
|
+
else if (command === "doctor") runDoctor(parsed.flags);
|
|
279
|
+
else if (command === "vendor") runVendor(parsed.flags);
|
|
280
|
+
else if (command === "validate-source") runValidateSource(parsed.flags);
|
|
281
|
+
else if (command === "validate-action") runValidateAction(parsed.flags);
|
|
282
|
+
else throw new CapabilityValidationError(`unknown command ${command}`);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
285
|
+
process.stderr.write(`FAIL: ${message}\n`);
|
|
286
|
+
if (error instanceof CapabilityValidationError) {
|
|
287
|
+
for (const detail of error.errors) process.stderr.write(`- ${detail}\n`);
|
|
288
|
+
}
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Applies the runtime advisory policy to `npm audit --json` read from stdin.
|
|
4
|
+
//
|
|
5
|
+
// The rule is still "no high or critical advisory in the pinned Pi host tree".
|
|
6
|
+
// What this adds is a way to accept one named advisory when there is provably
|
|
7
|
+
// nothing to do about it, without lowering the bar for everything else.
|
|
8
|
+
//
|
|
9
|
+
// An accepted advisory is not forgotten:
|
|
10
|
+
// - it must still be present, so the entry disappears the moment upstream
|
|
11
|
+
// ships a fix rather than lingering as scar tissue;
|
|
12
|
+
// - it expires on a date, so accepting it can never become permanent by
|
|
13
|
+
// inattention.
|
|
14
|
+
// Both of those fail the audit. A gate that quietly stops checking is worse
|
|
15
|
+
// than no gate, because it reads as a pass.
|
|
16
|
+
|
|
17
|
+
const ACCEPTED = [
|
|
18
|
+
{
|
|
19
|
+
id: "GHSA-mh99-v99m-4gvg",
|
|
20
|
+
package: "brace-expansion",
|
|
21
|
+
reviewBy: "2026-08-25",
|
|
22
|
+
reason: [
|
|
23
|
+
"Denial of service only: brace expansion can exhaust memory and crash the",
|
|
24
|
+
"process. No privilege escalation and no data exposure.",
|
|
25
|
+
"Nothing can fix it from here. The Pi host publishes an npm-shrinkwrap.json",
|
|
26
|
+
"that pins brace-expansion 5.0.7, and a published shrinkwrap takes",
|
|
27
|
+
"precedence over consumer overrides, so the resolution cannot be changed",
|
|
28
|
+
"by this repository. Every released Pi host carries a high brace-expansion",
|
|
29
|
+
"advisory: 0.81.1 and 0.82.0 carry this one, and 0.80.x carry the previous",
|
|
30
|
+
"GHSA-3jxr-9vmj-r5cp, so pinning an older host only trades one for another.",
|
|
31
|
+
"The fix is a regenerated shrinkwrap in a Pi release."
|
|
32
|
+
].join(" ")
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function readStdin() {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let text = "";
|
|
39
|
+
process.stdin.setEncoding("utf8");
|
|
40
|
+
process.stdin.on("data", (chunk) => { text += chunk; });
|
|
41
|
+
process.stdin.on("end", () => resolve(text));
|
|
42
|
+
process.stdin.on("error", reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function advisoryIdsFor(vulnerability) {
|
|
47
|
+
// `via` mixes advisory objects with plain package names for transitive
|
|
48
|
+
// effects; only the objects carry an advisory URL.
|
|
49
|
+
return (vulnerability.via ?? [])
|
|
50
|
+
.filter((item) => item && typeof item === "object" && typeof item.url === "string")
|
|
51
|
+
.map((item) => item.url.match(/(GHSA-[a-z0-9-]+)/i)?.[1])
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function today() {
|
|
56
|
+
// Date-only comparison in UTC keeps the expiry independent of the runner's
|
|
57
|
+
// timezone.
|
|
58
|
+
return new Date().toISOString().slice(0, 10);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const raw = await readStdin();
|
|
62
|
+
let report;
|
|
63
|
+
try {
|
|
64
|
+
report = JSON.parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
console.error("FAIL: npm audit did not produce parseable JSON");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (report.auditReportVersion !== 2) {
|
|
71
|
+
// The shape this reads is version 2. Guessing at another shape could report
|
|
72
|
+
// a clean tree because it looked in the wrong place.
|
|
73
|
+
console.error(`FAIL: unsupported npm audit report version ${report.auditReportVersion ?? "(none)"}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const blocking = [];
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
for (const [name, vulnerability] of Object.entries(report.vulnerabilities ?? {})) {
|
|
80
|
+
if (!["high", "critical"].includes(vulnerability.severity)) continue;
|
|
81
|
+
const ids = advisoryIdsFor(vulnerability);
|
|
82
|
+
if (ids.length === 0) {
|
|
83
|
+
blocking.push({ name, severity: vulnerability.severity, id: "(no advisory id)" });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const id of ids) {
|
|
87
|
+
seen.add(id);
|
|
88
|
+
const accepted = ACCEPTED.find((entry) => entry.id === id && entry.package === name);
|
|
89
|
+
if (!accepted) blocking.push({ name, severity: vulnerability.severity, id });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const failures = [];
|
|
94
|
+
for (const entry of blocking) {
|
|
95
|
+
failures.push(`${entry.severity} advisory ${entry.id} in ${entry.name} is not accepted`);
|
|
96
|
+
}
|
|
97
|
+
for (const entry of ACCEPTED) {
|
|
98
|
+
if (!seen.has(entry.id)) {
|
|
99
|
+
failures.push(`accepted advisory ${entry.id} is no longer reported; remove it from ACCEPTED in ${"scripts/check-runtime-advisories.mjs"}`);
|
|
100
|
+
} else if (today() > entry.reviewBy) {
|
|
101
|
+
failures.push(`accepted advisory ${entry.id} passed its ${entry.reviewBy} review date; confirm it still applies or remove it`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (failures.length > 0) {
|
|
106
|
+
console.error("FAIL: runtime advisory policy");
|
|
107
|
+
for (const failure of failures) console.error(`- ${failure}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const counts = report.metadata?.vulnerabilities ?? {};
|
|
112
|
+
const acceptedList = ACCEPTED.map((entry) => `${entry.id} (review by ${entry.reviewBy})`).join(", ");
|
|
113
|
+
console.log(`PASS: no unaccepted high or critical advisory (${counts.high ?? 0} high, ${counts.critical ?? 0} critical)`);
|
|
114
|
+
if (acceptedList) console.log(`Accepted, with reasons recorded in scripts/check-runtime-advisories.mjs: ${acceptedList}`);
|