@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,265 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { CapabilityValidationError, sha256, validateCapabilityPackageSource } from "./capability-core.js";
|
|
6
|
+
|
|
7
|
+
// A capability source lets a project use packs it does not own without forking
|
|
8
|
+
// the platform. There are two levels, and they end in the same place: a
|
|
9
|
+
// directory on disk that is scanned by exactly the same code that scans the
|
|
10
|
+
// platform's own packs.
|
|
11
|
+
//
|
|
12
|
+
// level 1 — `path`, a directory inside the project (a team's private packs)
|
|
13
|
+
// level 2 — `source`, an npm or git release vendored into the project
|
|
14
|
+
//
|
|
15
|
+
// Fetching is deliberately not part of resolution. A maintainer runs the vendor
|
|
16
|
+
// command, the result is written under the project and committed, and every
|
|
17
|
+
// later resolution reads only what is already on disk. The guard therefore
|
|
18
|
+
// never reaches the network, and a pack cannot change under it between the
|
|
19
|
+
// review and the run.
|
|
20
|
+
//
|
|
21
|
+
// None of this widens the trust model. An external pack still resolves only
|
|
22
|
+
// when the profile already names its owner and lifecycle, so an unknown owner
|
|
23
|
+
// is refused rather than warned about.
|
|
24
|
+
|
|
25
|
+
const VENDOR_DIRECTORY = path.join(".pi", "capability-vendor");
|
|
26
|
+
const SOURCE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
27
|
+
const MAX_SOURCES = 16;
|
|
28
|
+
const MAX_VENDOR_ENTRIES = 4096;
|
|
29
|
+
const NETWORK_TIMEOUT_MS = 120_000;
|
|
30
|
+
|
|
31
|
+
function fail(message, details) {
|
|
32
|
+
throw new CapabilityValidationError(message, details);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate the `capabilitySources` block of a project profile.
|
|
37
|
+
*
|
|
38
|
+
* @param {unknown} value
|
|
39
|
+
* @param {string} source
|
|
40
|
+
* @returns {Array<{name: string, path?: string, source?: string}>}
|
|
41
|
+
*/
|
|
42
|
+
export function validateCapabilitySources(value, source = "project profile") {
|
|
43
|
+
if (value === undefined) return [];
|
|
44
|
+
if (!Array.isArray(value)) fail(`${source}.capabilitySources must be an array`);
|
|
45
|
+
if (value.length > MAX_SOURCES) fail(`${source}.capabilitySources must declare at most ${MAX_SOURCES} sources`);
|
|
46
|
+
|
|
47
|
+
const names = new Set();
|
|
48
|
+
return value.map((entry, index) => {
|
|
49
|
+
const location = `${source}.capabilitySources[${index}]`;
|
|
50
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) fail(`${location} must be an object`);
|
|
51
|
+
const allowed = new Set(["name", "path", "source"]);
|
|
52
|
+
for (const key of Object.keys(entry)) {
|
|
53
|
+
if (!allowed.has(key)) fail(`${location}.${key} is not a supported key`);
|
|
54
|
+
}
|
|
55
|
+
if (typeof entry.name !== "string" || !SOURCE_NAME_PATTERN.test(entry.name)) {
|
|
56
|
+
fail(`${location}.name must be a lowercase identifier`);
|
|
57
|
+
}
|
|
58
|
+
if (names.has(entry.name)) fail(`${location}.name must be unique`);
|
|
59
|
+
names.add(entry.name);
|
|
60
|
+
|
|
61
|
+
// One level per entry. Accepting both would leave it ambiguous which one
|
|
62
|
+
// the lock digest actually covers.
|
|
63
|
+
const hasPath = entry.path !== undefined;
|
|
64
|
+
const hasSource = entry.source !== undefined;
|
|
65
|
+
if (hasPath === hasSource) fail(`${location} must declare exactly one of path or source`);
|
|
66
|
+
|
|
67
|
+
if (hasPath) {
|
|
68
|
+
if (typeof entry.path !== "string" || entry.path.length === 0 || entry.path.length > 512) {
|
|
69
|
+
fail(`${location}.path must be a short relative path`);
|
|
70
|
+
}
|
|
71
|
+
if (path.isAbsolute(entry.path) || entry.path.includes("\\") || entry.path.includes("\0")) {
|
|
72
|
+
fail(`${location}.path must be a project-relative path`);
|
|
73
|
+
}
|
|
74
|
+
if (entry.path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
75
|
+
fail(`${location}.path must not contain traversal segments`);
|
|
76
|
+
}
|
|
77
|
+
return { name: entry.name, path: entry.path };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const remote = validateCapabilityPackageSource(entry.source);
|
|
81
|
+
if (!/^(?:npm|git):/.test(remote)) fail(`${location}.source must be an npm or git source`);
|
|
82
|
+
return { name: entry.name, source: remote };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Where a remote source is vendored. Deterministic so the lock, the vendor
|
|
88
|
+
* command, and the guard all agree without passing paths around.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} projectRoot
|
|
91
|
+
* @param {string} name
|
|
92
|
+
*/
|
|
93
|
+
export function vendorDirectoryFor(projectRoot, name) {
|
|
94
|
+
if (!SOURCE_NAME_PATTERN.test(name)) fail(`${name} is not a valid capability source name`);
|
|
95
|
+
return path.join(projectRoot, VENDOR_DIRECTORY, name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertContainedDirectory(projectRoot, target, label) {
|
|
99
|
+
const rootReal = fs.realpathSync(projectRoot);
|
|
100
|
+
if (!fs.existsSync(target)) fail(`${label} does not exist; run the vendor command or correct the path`);
|
|
101
|
+
const stat = fs.lstatSync(target);
|
|
102
|
+
if (stat.isSymbolicLink()) fail(`${label} must not be a symbolic link`);
|
|
103
|
+
if (!stat.isDirectory()) fail(`${label} must be a directory`);
|
|
104
|
+
|
|
105
|
+
// realpath after the symlink check: the check above rejects the final
|
|
106
|
+
// component, and this rejects a parent that leads outside the project.
|
|
107
|
+
const targetReal = fs.realpathSync(target);
|
|
108
|
+
const relative = path.relative(rootReal, targetReal);
|
|
109
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) fail(`${label} resolves outside the project`);
|
|
110
|
+
return targetReal;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function assertNoSymbolicLinks(root, label, budget = { remaining: MAX_VENDOR_ENTRIES }) {
|
|
114
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
115
|
+
if (budget.remaining-- <= 0) fail(`${label} contains more than ${MAX_VENDOR_ENTRIES} entries`);
|
|
116
|
+
const child = path.join(root, entry.name);
|
|
117
|
+
if (entry.isSymbolicLink()) fail(`${label} must not contain symbolic links (${entry.name})`);
|
|
118
|
+
if (entry.isDirectory()) assertNoSymbolicLinks(child, label, budget);
|
|
119
|
+
else if (!entry.isFile()) fail(`${label} must contain only regular files (${entry.name})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Turn declared sources into roots the pack scanner can read. Every root is
|
|
125
|
+
* verified to sit inside the project and to be free of symbolic links, because
|
|
126
|
+
* this is the boundary where someone else's tree becomes readable.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} projectRoot
|
|
129
|
+
* @param {Array<{name: string, path?: string, source?: string}>} sources
|
|
130
|
+
*/
|
|
131
|
+
export function resolveCapabilitySourceRoots(projectRoot, sources) {
|
|
132
|
+
// Resolved roots come back from realpath, so the project root has to be
|
|
133
|
+
// resolved the same way before the two are compared. On a system where the
|
|
134
|
+
// project sits under a symlinked parent, mixing the two forms produces a
|
|
135
|
+
// relative path that climbs out of the project and looks like an escape.
|
|
136
|
+
const rootReal = fs.realpathSync(projectRoot);
|
|
137
|
+
return validateCapabilitySources(sources).map((entry) => {
|
|
138
|
+
const target = entry.path
|
|
139
|
+
? path.resolve(rootReal, entry.path)
|
|
140
|
+
: vendorDirectoryFor(rootReal, entry.name);
|
|
141
|
+
const label = `capability source ${entry.name}`;
|
|
142
|
+
const root = assertContainedDirectory(rootReal, target, label);
|
|
143
|
+
assertNoSymbolicLinks(root, label);
|
|
144
|
+
if (!fs.existsSync(path.join(root, "packs"))) fail(`${label} must contain a packs directory`);
|
|
145
|
+
return {
|
|
146
|
+
origin: entry.name,
|
|
147
|
+
root,
|
|
148
|
+
source: entry.source ?? `./${path.relative(rootReal, root).split(path.sep).join("/")}`
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runCommand(command, args, options) {
|
|
154
|
+
try {
|
|
155
|
+
return execFileSync(command, args, {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
158
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
159
|
+
...options
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
const detail = error && typeof error === "object" && "stderr" in error ? String(error.stderr ?? "").trim() : "";
|
|
163
|
+
fail(`${command} ${args[0] ?? ""} failed`, detail ? [detail] : undefined);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function fetchNpmSource(reference, destination) {
|
|
168
|
+
// `npm pack` resolves the exact version already validated by the source
|
|
169
|
+
// grammar and writes one tarball; nothing is executed from the package.
|
|
170
|
+
const staging = fs.mkdtempSync(path.join(os.tmpdir(), "piagent-vendor-"));
|
|
171
|
+
try {
|
|
172
|
+
const output = runCommand("npm", ["pack", reference, "--pack-destination", staging, "--ignore-scripts"], { cwd: staging });
|
|
173
|
+
const tarball = output.trim().split("\n").pop()?.trim();
|
|
174
|
+
if (!tarball) fail(`npm pack produced no tarball for ${reference}`);
|
|
175
|
+
const tarballPath = path.join(staging, tarball);
|
|
176
|
+
if (!fs.existsSync(tarballPath)) fail(`npm pack reported ${tarball} but wrote no such file`);
|
|
177
|
+
const extracted = path.join(staging, "extracted");
|
|
178
|
+
fs.mkdirSync(extracted);
|
|
179
|
+
runCommand("tar", ["-xzf", tarballPath, "-C", extracted, "--strip-components=1"]);
|
|
180
|
+
fs.cpSync(extracted, destination, { recursive: true, verbatimSymlinks: true });
|
|
181
|
+
} finally {
|
|
182
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function fetchGitSource(reference, destination) {
|
|
187
|
+
const separator = reference.lastIndexOf("@");
|
|
188
|
+
const location = reference.slice("git:".length, separator);
|
|
189
|
+
const revision = reference.slice(separator + 1);
|
|
190
|
+
const staging = fs.mkdtempSync(path.join(os.tmpdir(), "piagent-vendor-"));
|
|
191
|
+
try {
|
|
192
|
+
const url = `https://${location}.git`;
|
|
193
|
+
// No --depth: a tag or commit is fetched by name, and a shallow clone
|
|
194
|
+
// cannot always reach an arbitrary commit.
|
|
195
|
+
runCommand("git", ["clone", "--quiet", "--no-checkout", url, staging]);
|
|
196
|
+
runCommand("git", ["-C", staging, "checkout", "--quiet", "--detach", revision]);
|
|
197
|
+
fs.rmSync(path.join(staging, ".git"), { recursive: true, force: true });
|
|
198
|
+
fs.cpSync(staging, destination, { recursive: true, verbatimSymlinks: true });
|
|
199
|
+
} finally {
|
|
200
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Fetch a remote source into the project's vendor directory. Only the vendor
|
|
206
|
+
* command calls this; resolution and enforcement never do.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} projectRoot
|
|
209
|
+
* @param {{name: string, source?: string}} entry
|
|
210
|
+
*/
|
|
211
|
+
export function vendorRemoteSource(projectRoot, entry) {
|
|
212
|
+
const [validated] = validateCapabilitySources([entry]);
|
|
213
|
+
if (!validated.source) fail(`capability source ${validated.name} is a local path and does not need vendoring`);
|
|
214
|
+
|
|
215
|
+
const destination = vendorDirectoryFor(projectRoot, validated.name);
|
|
216
|
+
const parent = path.dirname(destination);
|
|
217
|
+
fs.mkdirSync(parent, { recursive: true, mode: 0o755 });
|
|
218
|
+
if (fs.lstatSync(parent).isSymbolicLink()) fail(`${parent} must not be a symbolic link`);
|
|
219
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
220
|
+
fs.mkdirSync(destination, { mode: 0o755 });
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
if (validated.source.startsWith("npm:")) fetchNpmSource(validated.source.slice("npm:".length), destination);
|
|
224
|
+
else fetchGitSource(validated.source, destination);
|
|
225
|
+
|
|
226
|
+
// The fetched tree is untrusted until this passes. Checking here means a
|
|
227
|
+
// hostile archive is rejected before anything reads a manifest out of it.
|
|
228
|
+
assertNoSymbolicLinks(destination, `capability source ${validated.name}`);
|
|
229
|
+
if (!fs.existsSync(path.join(destination, "packs"))) {
|
|
230
|
+
fail(`capability source ${validated.name} provides no packs directory`);
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// A half-written vendor directory would resolve on the next run and hide
|
|
234
|
+
// the failure, so remove it.
|
|
235
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { name: validated.name, source: validated.source, directory: destination, digest: digestDirectory(destination) };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Deterministic digest of a vendored tree: path and content of every regular
|
|
244
|
+
* file, in code-point order. Recorded so a later run can tell that the tree it
|
|
245
|
+
* reads is the tree that was reviewed.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} root
|
|
248
|
+
*/
|
|
249
|
+
export function digestDirectory(root) {
|
|
250
|
+
const files = [];
|
|
251
|
+
function walk(directory, prefix) {
|
|
252
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true })
|
|
253
|
+
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
256
|
+
const child = path.join(directory, entry.name);
|
|
257
|
+
if (entry.isSymbolicLink()) fail(`${root} must not contain symbolic links (${relative})`);
|
|
258
|
+
if (entry.isDirectory()) walk(child, relative);
|
|
259
|
+
else if (entry.isFile()) files.push(`${relative}:${sha256(fs.readFileSync(child))}`);
|
|
260
|
+
else fail(`${root} must contain only regular files (${relative})`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
walk(root, "");
|
|
264
|
+
return sha256(files.join("\n"));
|
|
265
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Filesystem helpers shared by the guard and its domain modules.
|
|
2
|
+
// Deliberately dependency-free apart from node builtins so any domain module
|
|
3
|
+
// can import it without creating a cycle back through the guard.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @template T
|
|
9
|
+
* @param {string} filePath
|
|
10
|
+
* @returns {T | undefined}
|
|
11
|
+
*/
|
|
12
|
+
export function readJsonFile(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
15
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The package root carries the runtime policy; the platform root carries the
|
|
22
|
+
// adapter catalogue. They are usually the same directory but not always, so
|
|
23
|
+
// each is located by the marker that actually matters to the caller.
|
|
24
|
+
/** @param {string} startDir @returns {string} */
|
|
25
|
+
export function findPackageRoot(startDir) {
|
|
26
|
+
return findRootWithMarker(startDir, "policies");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @param {string} startDir @returns {string} */
|
|
30
|
+
export function findPlatformRoot(startDir) {
|
|
31
|
+
return findRootWithMarker(startDir, "adapters");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {string} startDir @param {string} marker @returns {string} */
|
|
35
|
+
function findRootWithMarker(startDir, marker) {
|
|
36
|
+
let current = startDir;
|
|
37
|
+
while (true) {
|
|
38
|
+
if (fs.existsSync(path.join(current, "package.json")) && fs.existsSync(path.join(current, marker))) {
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(current);
|
|
42
|
+
if (parent === current) return startDir;
|
|
43
|
+
current = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|