@piagent/platform 1.1.5 → 1.1.7
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/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/adapters/be-readonly-fe/profile.json +12 -4
- package/docs/capability-packs.md +21 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/project-adapters.md +44 -0
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +3 -3
- package/docs-site/index.html +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/capabilities/capability-core.js +109 -8
- package/packages/piagent-core/capabilities/project-profile.js +83 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +97 -95
- package/packages/piagent-core/extensions/project-shape.js +285 -0
- package/packages/piagent-core/package.json +1 -1
- package/schemas/project-profile.schema.json +25 -7
- package/scripts/capability-catalog.mjs +28 -7
- package/scripts/init-project.sh +19 -89
- package/scripts/install-global.sh +3 -3
- package/scripts/profile-doctor.sh +13 -3
- package/scripts/runtime-policy-smoke.sh +12 -3
- package/scripts/team-doctor.sh +12 -3
|
@@ -6,19 +6,37 @@
|
|
|
6
6
|
"required": [
|
|
7
7
|
"schemaVersion",
|
|
8
8
|
"projectId",
|
|
9
|
-
"displayName"
|
|
10
|
-
"mode",
|
|
11
|
-
"rootMarkers",
|
|
12
|
-
"protectedPaths",
|
|
13
|
-
"requiredContext",
|
|
14
|
-
"verifyCommands",
|
|
15
|
-
"mcpCapabilities"
|
|
9
|
+
"displayName"
|
|
16
10
|
],
|
|
11
|
+
"if": {
|
|
12
|
+
"required": [
|
|
13
|
+
"extends"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"then": {
|
|
17
|
+
"description": "A profile that names an adapter stores identity plus overrides. Every policy field it omits comes from the installed adapter at runtime, so none of them are required here."
|
|
18
|
+
},
|
|
19
|
+
"else": {
|
|
20
|
+
"required": [
|
|
21
|
+
"mode",
|
|
22
|
+
"rootMarkers",
|
|
23
|
+
"protectedPaths",
|
|
24
|
+
"requiredContext",
|
|
25
|
+
"verifyCommands",
|
|
26
|
+
"mcpCapabilities"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
17
29
|
"properties": {
|
|
18
30
|
"schemaVersion": {
|
|
19
31
|
"type": "integer",
|
|
20
32
|
"const": 1
|
|
21
33
|
},
|
|
34
|
+
"extends": {
|
|
35
|
+
"description": "Name of a built-in adapter under adapters/ in the installed platform. Its profile is the base; every other key in this file replaces the adapter's key of the same name.",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"minLength": 1,
|
|
38
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
39
|
+
},
|
|
22
40
|
"projectId": {
|
|
23
41
|
"type": "string",
|
|
24
42
|
"minLength": 1,
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
validateCapabilitySources,
|
|
21
21
|
vendorRemoteSource
|
|
22
22
|
} from "../packages/piagent-core/capabilities/capability-sources.js";
|
|
23
|
+
import {
|
|
24
|
+
buildExtendingProfile,
|
|
25
|
+
resolveProjectProfileDocument
|
|
26
|
+
} from "../packages/piagent-core/capabilities/project-profile.js";
|
|
23
27
|
|
|
24
28
|
const platformRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
29
|
const catalogPath = path.join(platformRoot, "catalog", "capabilities.json");
|
|
@@ -28,7 +32,7 @@ function usage() {
|
|
|
28
32
|
process.stdout.write(`Usage:
|
|
29
33
|
piagent-capabilities catalog [--check | --write]
|
|
30
34
|
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]
|
|
35
|
+
piagent-capabilities apply-profile --profile <source.json> --target <piagent-profile.json> [--extends <adapter>] [--package-source <source>] [--force]
|
|
32
36
|
piagent-capabilities doctor [--profile <profile.json>] [--lock <lock.json>] [--package-source <source>]
|
|
33
37
|
piagent-capabilities vendor --profile <profile.json> [--source <name>]
|
|
34
38
|
piagent-capabilities validate-source --package-source <source>
|
|
@@ -58,7 +62,7 @@ function parseArguments(values) {
|
|
|
58
62
|
output.flags.set(value, true);
|
|
59
63
|
continue;
|
|
60
64
|
}
|
|
61
|
-
if (!["--profile", "--output", "--target", "--lock", "--file", "--package-source", "--source"].includes(value)) throw new CapabilityValidationError(`unknown option ${value}`);
|
|
65
|
+
if (!["--profile", "--extends", "--output", "--target", "--lock", "--file", "--package-source", "--source"].includes(value)) throw new CapabilityValidationError(`unknown option ${value}`);
|
|
62
66
|
if (output.flags.has(value)) throw new CapabilityValidationError(`duplicate option ${value}`);
|
|
63
67
|
const next = values[index + 1];
|
|
64
68
|
if (!next || next.startsWith("--")) throw new CapabilityValidationError(`${value} requires a value`);
|
|
@@ -157,7 +161,7 @@ function runResolve(flags) {
|
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
function runApplyProfile(flags) {
|
|
160
|
-
assertAllowedFlags(flags, new Set(["--profile", "--target", "--package-source", "--force"]));
|
|
164
|
+
assertAllowedFlags(flags, new Set(["--profile", "--extends", "--target", "--package-source", "--force"]));
|
|
161
165
|
const source = requireExistingFile(flags.get("--profile"), "--profile");
|
|
162
166
|
const targetValue = flags.get("--target");
|
|
163
167
|
if (!targetValue) throw new CapabilityValidationError("--target is required");
|
|
@@ -166,17 +170,27 @@ function runApplyProfile(flags) {
|
|
|
166
170
|
const parent = fs.realpathSync(path.dirname(target));
|
|
167
171
|
const normalizedTarget = path.join(parent, path.basename(target));
|
|
168
172
|
if (fs.existsSync(normalizedTarget) && !flags.has("--force")) throw new CapabilityValidationError("target profile already exists; pass --force to replace it");
|
|
169
|
-
const
|
|
173
|
+
const adapter = readJson(source);
|
|
174
|
+
const base = flags.get("--extends");
|
|
175
|
+
// With --extends the project records which adapter it follows and keeps only
|
|
176
|
+
// its own identity, so a later platform correction reaches it. Without it the
|
|
177
|
+
// source document is copied whole, which is what a caller supplying its own
|
|
178
|
+
// profile file wants.
|
|
179
|
+
const stored = base === undefined
|
|
180
|
+
? adapter
|
|
181
|
+
: buildExtendingProfile(base, adapter.projectId, adapter.displayName);
|
|
182
|
+
const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
|
|
170
183
|
const packageSource = flags.get("--package-source") ?? "workspace";
|
|
171
184
|
const lock = resolveCapabilityProfileDocument(platformRoot, profile, {
|
|
172
185
|
profileFile: path.basename(normalizedTarget),
|
|
186
|
+
storedProfile: stored,
|
|
173
187
|
packageSource,
|
|
174
188
|
extraRoots: profile.capabilitySources === undefined
|
|
175
189
|
? undefined
|
|
176
190
|
: resolveCapabilitySourceRoots(projectRootForProfile(normalizedTarget), profile.capabilitySources)
|
|
177
191
|
});
|
|
178
192
|
const lockTarget = path.join(parent, "piagent-profile.lock.json");
|
|
179
|
-
writeProfileLockAtomic(normalizedTarget,
|
|
193
|
+
writeProfileLockAtomic(normalizedTarget, stored, lockTarget, lock);
|
|
180
194
|
process.stdout.write(`${JSON.stringify({ ok: true, profile: normalizedTarget, lock: lockTarget, packs: lock.packs.length })}\n`);
|
|
181
195
|
}
|
|
182
196
|
|
|
@@ -210,11 +224,18 @@ function runDoctor(flags) {
|
|
|
210
224
|
const verification = verifyCapabilityLock(platformRoot, profile, readJson(lockFile), { packageSource, extraRoots });
|
|
211
225
|
report.lock = {
|
|
212
226
|
path: lockFile,
|
|
213
|
-
current: verification.
|
|
227
|
+
current: verification.status === "current",
|
|
228
|
+
status: verification.status,
|
|
229
|
+
reasons: verification.reasons,
|
|
214
230
|
expectedDigest: verification.expectedDigest,
|
|
215
231
|
actualDigest: verification.actualDigest
|
|
216
232
|
};
|
|
217
|
-
|
|
233
|
+
// A re-pinnable lock is behind the installed build but grants exactly what
|
|
234
|
+
// it agreed to, so it is reported rather than treated as a failure. The
|
|
235
|
+
// guard rewrites it on the next session.
|
|
236
|
+
if (verification.status === "blocked") {
|
|
237
|
+
throw new CapabilityValidationError(`capability lock no longer matches what the project agreed to: ${verification.reasons.join("; ")}`);
|
|
238
|
+
}
|
|
218
239
|
}
|
|
219
240
|
} else if (flags.has("--lock")) {
|
|
220
241
|
throw new CapabilityValidationError("--lock requires --profile");
|
package/scripts/init-project.sh
CHANGED
|
@@ -102,97 +102,20 @@ fi
|
|
|
102
102
|
|
|
103
103
|
PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
[[ -f "$project/package.json" ]] && grep -Eiq "$pattern" "$project/package.json"
|
|
109
|
-
}
|
|
110
|
-
|
|
105
|
+
# Profile detection lives in packages/piagent-core/extensions/project-shape.js so
|
|
106
|
+
# that this script and the runtime `piagent_profile_options` tool recommend the
|
|
107
|
+
# same profile. They used to hold separate copies of the rules and drifted apart.
|
|
111
108
|
detect_profile() {
|
|
112
109
|
local project="$1"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
[[ -f "$project/package.json" ]] && has_package=true
|
|
123
|
-
|
|
124
|
-
if [[ -f "$project/pubspec.yaml" ]] || { [[ -d "$project/android" ]] && [[ -d "$project/ios" ]]; }; then
|
|
125
|
-
mobile=true
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
if [[ -f "$project/dbt_project.yml" ]] || [[ -f "$project/dvc.yaml" ]] || [[ -d "$project/notebooks" ]] || [[ -d "$project/data" ]]; then
|
|
129
|
-
data=true
|
|
130
|
-
fi
|
|
131
|
-
|
|
132
|
-
if [[ -f "$project/Dockerfile" ]] || [[ -f "$project/docker-compose.yml" ]] || [[ -f "$project/compose.yml" ]] || [[ -f "$project/compose.yaml" ]] || [[ -d "$project/terraform" ]] || [[ -d "$project/infra" ]] || [[ -d "$project/k8s" ]] || [[ -d "$project/helm" ]]; then
|
|
133
|
-
infra=true
|
|
134
|
-
devops=true
|
|
135
|
-
elif [[ -d "$project/.github/workflows" ]]; then
|
|
136
|
-
devops=true
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
if [[ -d "$project/docs" ]] || [[ -f "$project/mkdocs.yml" ]] || [[ -f "$project/mint.json" ]] || [[ -f "$project/docusaurus.config.js" ]]; then
|
|
140
|
-
docs=true
|
|
141
|
-
fi
|
|
142
|
-
|
|
143
|
-
if [[ "$has_package" == true ]]; then
|
|
144
|
-
if [[ -d "$project/frontend" ]] || [[ -d "$project/apps/web" ]] || [[ -d "$project/apps/frontend" ]]; then
|
|
145
|
-
frontend=true
|
|
146
|
-
fi
|
|
147
|
-
|
|
148
|
-
if [[ -d "$project/backend" ]] || [[ -d "$project/apps/api" ]] || [[ -d "$project/apps/server" ]]; then
|
|
149
|
-
backend=true
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
if package_has_any "$project" '"(next|react|vite|vue|svelte|astro|@angular/core|remix)"' \
|
|
153
|
-
|| [[ -f "$project/next.config.js" ]] || [[ -f "$project/next.config.mjs" ]] || [[ -f "$project/next.config.ts" ]] \
|
|
154
|
-
|| [[ -f "$project/vite.config.js" ]] || [[ -f "$project/vite.config.ts" ]] \
|
|
155
|
-
|| [[ -d "$project/src/app" ]] || [[ -d "$project/pages" ]] || [[ -d "$project/public" ]]; then
|
|
156
|
-
frontend=true
|
|
157
|
-
fi
|
|
158
|
-
|
|
159
|
-
if package_has_any "$project" '"(@nestjs|express|fastify|hono|koa|apollo-server|graphql-yoga|prisma|typeorm|sequelize|drizzle-orm)"' \
|
|
160
|
-
|| [[ -f "$project/nest-cli.json" ]] || [[ -d "$project/prisma" ]] || [[ -d "$project/src/server" ]] || [[ -d "$project/src/api" ]]; then
|
|
161
|
-
backend=true
|
|
162
|
-
fi
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
if [[ -f "$project/pom.xml" ]] || [[ -f "$project/build.gradle" ]] || [[ -f "$project/build.gradle.kts" ]] || [[ -d "$project/src/main/java" ]] || [[ -d "$project/src/main/kotlin" ]]; then
|
|
166
|
-
backend=true
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
if [[ -f "$project/pyproject.toml" ]] && grep -Eiq "(fastapi|flask|django|litestar|starlite)" "$project/pyproject.toml"; then
|
|
170
|
-
backend=true
|
|
171
|
-
fi
|
|
172
|
-
|
|
173
|
-
if [[ "$mobile" == true ]]; then
|
|
174
|
-
printf '%s\n' "mobile"
|
|
175
|
-
elif [[ "$frontend" == true && "$backend" == true ]]; then
|
|
176
|
-
printf '%s\n' "fullstack"
|
|
177
|
-
elif [[ "$frontend" == true ]]; then
|
|
178
|
-
printf '%s\n' "web-frontend"
|
|
179
|
-
elif [[ "$backend" == true ]]; then
|
|
180
|
-
printf '%s\n' "backend-api"
|
|
181
|
-
elif [[ "$data" == true ]]; then
|
|
182
|
-
printf '%s\n' "data"
|
|
183
|
-
elif [[ -f "$project/pyproject.toml" ]]; then
|
|
184
|
-
printf '%s\n' "python"
|
|
185
|
-
elif [[ "$has_package" == true && -f "$project/tsconfig.json" ]]; then
|
|
186
|
-
printf '%s\n' "node-typescript"
|
|
187
|
-
elif [[ "$infra" == true ]]; then
|
|
188
|
-
printf '%s\n' "devops"
|
|
189
|
-
elif [[ "$docs" == true ]]; then
|
|
190
|
-
printf '%s\n' "docs"
|
|
191
|
-
elif [[ "$devops" == true ]]; then
|
|
192
|
-
printf '%s\n' "devops"
|
|
193
|
-
else
|
|
194
|
-
printf '%s\n' "generic"
|
|
195
|
-
fi
|
|
110
|
+
node --input-type=module - "$PLATFORM_ROOT" "$project" <<'NODE'
|
|
111
|
+
import path from "node:path";
|
|
112
|
+
import { pathToFileURL } from "node:url";
|
|
113
|
+
|
|
114
|
+
const [platformRoot, project] = process.argv.slice(2);
|
|
115
|
+
const modulePath = path.join(platformRoot, "packages/piagent-core/extensions/project-shape.js");
|
|
116
|
+
const { detectProfileName } = await import(pathToFileURL(modulePath).href);
|
|
117
|
+
process.stdout.write(`${detectProfileName(project).name}\n`);
|
|
118
|
+
NODE
|
|
196
119
|
}
|
|
197
120
|
|
|
198
121
|
resolve_profile_path() {
|
|
@@ -296,6 +219,13 @@ if [[ "$FORCE_PROFILE" == true || ! -f "$PROFILE_TARGET" ]]; then
|
|
|
296
219
|
--target "$PROFILE_TARGET"
|
|
297
220
|
--package-source "$PACKAGE_SOURCE"
|
|
298
221
|
)
|
|
222
|
+
# A built-in adapter is referenced, not copied, so a later platform correction
|
|
223
|
+
# reaches this project. A caller supplying its own profile file still gets that
|
|
224
|
+
# file written whole.
|
|
225
|
+
if [[ "$PROFILE_PATH" == "$PLATFORM_ROOT/adapters/"*"/profile.json" ]]; then
|
|
226
|
+
adapter_name="${PROFILE_PATH#"$PLATFORM_ROOT/adapters/"}"
|
|
227
|
+
apply_profile_args+=(--extends "${adapter_name%/profile.json}")
|
|
228
|
+
fi
|
|
299
229
|
if [[ "$FORCE_PROFILE" == true ]]; then
|
|
300
230
|
apply_profile_args+=(--force)
|
|
301
231
|
fi
|
|
@@ -15,7 +15,7 @@ Package source examples:
|
|
|
15
15
|
# Installs the stable tag after resolving it to a commit SHA.
|
|
16
16
|
|
|
17
17
|
# Pin a specific release:
|
|
18
|
-
scripts/install-global.sh --version v1.1.
|
|
18
|
+
scripts/install-global.sh --version v1.1.7 --resolve-tag
|
|
19
19
|
|
|
20
20
|
# Preview the planned install/update without changing user config:
|
|
21
21
|
scripts/install-global.sh --stable --dry-run
|
|
@@ -127,7 +127,7 @@ detect_runtime_surface() {
|
|
|
127
127
|
printf 'native-windows/%s (not release-gated; terminal helpers require Bash semantics)' "$arch_name"
|
|
128
128
|
;;
|
|
129
129
|
*)
|
|
130
|
-
printf '%s/%s (outside v1.1.
|
|
130
|
+
printf '%s/%s (outside v1.1.7 release matrix)' "$os_name" "$arch_name"
|
|
131
131
|
;;
|
|
132
132
|
esac
|
|
133
133
|
}
|
|
@@ -135,7 +135,7 @@ detect_runtime_surface() {
|
|
|
135
135
|
warn_runtime_surface_if_needed() {
|
|
136
136
|
local surface="$1"
|
|
137
137
|
case "$surface" in
|
|
138
|
-
*"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.
|
|
138
|
+
*"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.7 release matrix"*)
|
|
139
139
|
echo "WARN: runtime surface is $surface." >&2
|
|
140
140
|
echo "WARN: For team rollout, run piagent-doctor plus the project smoke/verify suite on this machine before relying on it." >&2
|
|
141
141
|
;;
|
|
@@ -19,12 +19,22 @@ if [[ ! -f "$PROFILE_PATH" ]]; then
|
|
|
19
19
|
exit 1
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
PLATFORM_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
23
|
+
|
|
24
|
+
node --input-type=module - "$PROJECT_PATH" "$PROFILE_PATH" "$PLATFORM_ROOT" <<'NODE'
|
|
23
25
|
import fs from "node:fs";
|
|
24
26
|
import path from "node:path";
|
|
27
|
+
import { pathToFileURL } from "node:url";
|
|
28
|
+
|
|
29
|
+
const [projectPath, profilePath, platformRoot] = process.argv.slice(2);
|
|
30
|
+
const stored = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
// A profile that names an adapter is checked as the document that will actually
|
|
33
|
+
// be enforced, not as the few lines the project happens to store.
|
|
34
|
+
const { resolveProjectProfileDocument } = await import(
|
|
35
|
+
pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
|
|
36
|
+
);
|
|
37
|
+
const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
|
|
28
38
|
|
|
29
39
|
const errors = [];
|
|
30
40
|
const warnings = [];
|
|
@@ -25,11 +25,20 @@ bash "$ROOT/scripts/profile-doctor.sh" "$PROJECT" >/dev/null
|
|
|
25
25
|
bash "$ROOT/scripts/quality-benchmark.sh" "$PROJECT" --init >/dev/null
|
|
26
26
|
bash "$ROOT/scripts/quality-benchmark.sh" "$PROJECT" --record --scenario smoke --surface pi --result pass --tokens 1 --verify "test -s README.md" >/dev/null
|
|
27
27
|
|
|
28
|
-
node --input-type=module - "$PROJECT/.pi/piagent-profile.json" "$PROJECT/.pi/benchmarks/quality-runs.jsonl" <<'NODE'
|
|
28
|
+
node --input-type=module - "$PROJECT/.pi/piagent-profile.json" "$PROJECT/.pi/benchmarks/quality-runs.jsonl" "$ROOT" <<'NODE'
|
|
29
29
|
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { pathToFileURL } from "node:url";
|
|
30
32
|
|
|
31
|
-
const [profilePath, benchmarkPath] = process.argv.slice(2);
|
|
32
|
-
const
|
|
33
|
+
const [profilePath, benchmarkPath, platformRoot] = process.argv.slice(2);
|
|
34
|
+
const stored = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
35
|
+
|
|
36
|
+
// A project that names an adapter stores identity and overrides only. The policy
|
|
37
|
+
// the runtime enforces is the resolved document, so that is what gets checked.
|
|
38
|
+
const { resolveProjectProfileDocument } = await import(
|
|
39
|
+
pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
|
|
40
|
+
);
|
|
41
|
+
const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
|
|
33
42
|
for (const capability of ["filesystem-readonly", "filesystem-write", "shell", "memory"]) {
|
|
34
43
|
if (!profile.mcpCapabilities.includes(capability)) {
|
|
35
44
|
throw new Error(`missing capability ${capability}`);
|
package/scripts/team-doctor.sh
CHANGED
|
@@ -137,7 +137,7 @@ function runtimeMatrixStatus() {
|
|
|
137
137
|
surface: `${platform}/${arch}`,
|
|
138
138
|
status: "outside-release-matrix",
|
|
139
139
|
teamRolloutReady: false,
|
|
140
|
-
note: "This runtime is outside the documented v1.1.
|
|
140
|
+
note: "This runtime is outside the documented v1.1.7 support matrix."
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -313,7 +313,12 @@ if (foreignAgentConfig.length > 0) {
|
|
|
313
313
|
const projectProfilePath = path.join(projectPath, ".pi", "piagent-profile.json");
|
|
314
314
|
let projectProfile = null;
|
|
315
315
|
if (fs.existsSync(projectProfilePath)) {
|
|
316
|
-
|
|
316
|
+
// Check the document that will be enforced. A profile that names an adapter
|
|
317
|
+
// stores only its own identity and overrides.
|
|
318
|
+
const { resolveProjectProfileDocument } = await import(
|
|
319
|
+
pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
|
|
320
|
+
);
|
|
321
|
+
const profile = resolveProjectProfileDocument(platformRoot, readJson(projectProfilePath)).profile;
|
|
317
322
|
projectProfile = profile;
|
|
318
323
|
for (const field of ["schemaVersion", "projectId", "displayName", "mode"]) {
|
|
319
324
|
if (profile[field] === undefined || profile[field] === "") errors.push(`project profile missing ${field}`);
|
|
@@ -333,7 +338,11 @@ if (fs.existsSync(projectProfilePath)) {
|
|
|
333
338
|
try {
|
|
334
339
|
const lock = readJson(projectLockPath);
|
|
335
340
|
const verification = verifyCapabilityLock(platformRoot, projectProfilePath, lock, { packageSource: projectPackageSource });
|
|
336
|
-
if (
|
|
341
|
+
if (verification.status === "blocked") {
|
|
342
|
+
errors.push(`project capability lock no longer matches what the project agreed to: ${verification.reasons.join("; ")}`);
|
|
343
|
+
} else if (verification.status === "repin") {
|
|
344
|
+
warnings.push(`project capability lock is behind the installed platform and will be re-pinned on the next Pi session (${verification.reasons.join("; ")})`);
|
|
345
|
+
}
|
|
337
346
|
} catch (error) {
|
|
338
347
|
errors.push(`project capability lock validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
339
348
|
}
|