@piagent/platform 1.2.0 → 1.2.2
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 +149 -1
- package/README.md +1 -1
- package/catalog/capabilities.json +4 -4
- package/docs/command-reference-vietnamese.md +15 -4
- package/docs/context-window-policy.md +1 -0
- package/docs/distribution-standard.md +7 -7
- package/docs/mcp-and-tools.md +49 -3
- package/docs/operator-manual-vietnamese.md +52 -11
- package/docs/publishing-for-teams.md +2 -0
- package/docs/quickstart-vietnamese.md +4 -2
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +4 -3
- package/docs/team-onboarding.md +3 -3
- package/docs/usage-observability.md +49 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/mcp-approval.html +54 -2
- package/docs-site/content/quickstart.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +55 -3
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +2 -2
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/package.json +1 -1
- package/packages/piagent-core/README.md +2 -0
- package/packages/piagent-core/capabilities/capability-core.js +41 -4
- package/packages/piagent-core/capabilities/capability-sources.js +73 -12
- package/packages/piagent-core/extensions/piagent-guard.ts +539 -70
- package/packages/piagent-core/extensions/policy-core.js +1586 -98
- package/packages/piagent-core/mcp/mcp-approval-store.js +56 -15
- package/packages/piagent-core/mcp/mcp-auth-readiness.js +31 -3
- package/packages/piagent-core/mcp/mcp-command-actions.js +139 -30
- package/packages/piagent-core/mcp/mcp-config-layers.js +339 -6
- package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
- package/packages/piagent-core/mcp/mcp-server-entry.js +51 -0
- package/packages/piagent-core/mcp/mcp-session-view.js +55 -4
- package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/prompts/be-to-fe.md +1 -0
- package/packages/piagent-core/prompts/piagent-commands.md +1 -0
- package/packages/piagent-core/prompts/platform-improve.md +1 -0
- package/packages/piagent-core/prompts/review.md +1 -0
- package/packages/piagent-core/prompts/scout.md +1 -0
- package/packages/piagent-core/prompts/task.md +1 -0
- package/scripts/configure-model-scope.sh +19 -3
- package/scripts/configure-subagents.sh +17 -4
- package/scripts/init-project.sh +15 -8
- package/scripts/mcp-manage.mjs +111 -22
- package/scripts/pi-model-catalog.sh +11 -1
- package/scripts/pi-session-stats.sh +27 -1
- package/scripts/pi-usage-history.mjs +567 -0
- package/scripts/quality-benchmark.sh +37 -9
- package/scripts/setup.sh +21 -13
- package/scripts/uninstall-global.sh +10 -4
- package/scripts/verify-local.sh +19 -0
- package/scripts/verify-release-identity.mjs +29 -0
|
@@ -238,8 +238,13 @@ export function validateCapabilityPack(document, options = {}) {
|
|
|
238
238
|
|
|
239
239
|
if (rejectUnknownKeys(spec.activation, new Set(["mode", "profiles", "triggers"]), `${source}.spec.activation`, errors)) {
|
|
240
240
|
if (!ACTIVATION_MODES.has(spec.activation.mode)) pushError(errors, `${source}.spec.activation.mode`, "must be explicit, profile, or trigger");
|
|
241
|
-
validateStringArray(spec.activation.profiles ?? [], `${source}.spec.activation.profiles`, errors, { maxItems: 64, pattern: NAME_PATTERN });
|
|
241
|
+
const profiles = validateStringArray(spec.activation.profiles ?? [], `${source}.spec.activation.profiles`, errors, { maxItems: 64, pattern: NAME_PATTERN });
|
|
242
242
|
const triggers = validateStringArray(spec.activation.triggers ?? [], `${source}.spec.activation.triggers`, errors, { maxItems: 64 });
|
|
243
|
+
// Both modes name the thing that switches them on, so both are checked for
|
|
244
|
+
// it. Only `trigger` was, which let a pack declare profile activation and
|
|
245
|
+
// list no profiles: resolution then read `.includes` off an absent array
|
|
246
|
+
// and died with a TypeError where it should have refused with a reason.
|
|
247
|
+
if (spec.activation.mode === "profile" && profiles.length === 0) pushError(errors, `${source}.spec.activation.profiles`, "must not be empty for profile activation");
|
|
243
248
|
if (spec.activation.mode === "trigger" && triggers.length === 0) pushError(errors, `${source}.spec.activation.triggers`, "must not be empty for trigger activation");
|
|
244
249
|
}
|
|
245
250
|
|
|
@@ -859,7 +864,10 @@ export function resolveCapabilityProfileDocument(root, profile, options = {}) {
|
|
|
859
864
|
const key = `${selector.name}@${selector.version}`;
|
|
860
865
|
const directPack = byKey.get(key);
|
|
861
866
|
if (!directPack) throw new CapabilityValidationError(`profile selects unknown capability pack ${key}`);
|
|
862
|
-
|
|
867
|
+
// Defaulted rather than assumed present. The validator now requires the list
|
|
868
|
+
// for this mode, but this line runs against whatever is on disk, and reading
|
|
869
|
+
// through to a TypeError would turn a refusal into a crash.
|
|
870
|
+
if (directPack.activation.mode === "profile" && !(directPack.activation.profiles ?? []).includes(profile.mode)) throw new CapabilityValidationError(`${key} is not enabled for profile mode ${profile.mode}`);
|
|
863
871
|
select(key);
|
|
864
872
|
}
|
|
865
873
|
|
|
@@ -995,22 +1003,51 @@ export function resolveCapabilityProfile(root, profilePath, options = {}) {
|
|
|
995
1003
|
const GRANTING_PERMISSIONS = ["capabilities", "filesystemRead", "filesystemWrite", "networkDomains", "externalActions"];
|
|
996
1004
|
const RESTRICTING_PERMISSIONS = ["protectedPaths", "readOnlyPaths", "shellProtectedPaths"];
|
|
997
1005
|
|
|
1006
|
+
// Everything above is about artifacts the platform ships. A pack vendored from
|
|
1007
|
+
// somewhere else is a different question, and gets a different answer.
|
|
1008
|
+
//
|
|
1009
|
+
// None of the reasons for leaving artifact content on the build side apply to
|
|
1010
|
+
// it. Its bytes change when that source decides they change, not when this
|
|
1011
|
+
// project updates. The manifest digest pins which artifacts a pack declares, not
|
|
1012
|
+
// what is in them, so a rewritten prompt or policy file keeps the same manifest.
|
|
1013
|
+
// And the argument that the platform can rewrite its own verifier is an argument
|
|
1014
|
+
// about the platform: an external source cannot, which is exactly why its
|
|
1015
|
+
// content is worth pinning. Prompts and policy files are what a hostile source
|
|
1016
|
+
// would edit, and it would rather not be asked again.
|
|
1017
|
+
//
|
|
1018
|
+
// `workspace` is the platform's own origin and is a reserved source name, so a
|
|
1019
|
+
// vendored source cannot claim it.
|
|
1020
|
+
function externalArtifacts(document) {
|
|
1021
|
+
const external = new Set(
|
|
1022
|
+
(document?.packs ?? [])
|
|
1023
|
+
.filter((pack) => pack?.origin && pack.origin !== "workspace")
|
|
1024
|
+
.map((pack) => `${pack.name}@${pack.version}`)
|
|
1025
|
+
);
|
|
1026
|
+
if (external.size === 0) return undefined;
|
|
1027
|
+
return (document?.artifacts ?? []).filter((artifact) => external.has(artifact?.pack));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
998
1030
|
function consentedShape(document) {
|
|
999
1031
|
return {
|
|
1000
1032
|
schemaVersion: document?.schemaVersion,
|
|
1001
1033
|
apiVersion: document?.core?.apiVersion,
|
|
1002
1034
|
packageSource: document?.core?.packageSource,
|
|
1003
1035
|
profile: document?.profile,
|
|
1004
|
-
packs: document?.packs
|
|
1036
|
+
packs: document?.packs,
|
|
1037
|
+
externalArtifacts: externalArtifacts(document)
|
|
1005
1038
|
};
|
|
1006
1039
|
}
|
|
1007
1040
|
|
|
1041
|
+
const CONSENT_DRIFT_REASONS = {
|
|
1042
|
+
externalArtifacts: "an artifact shipped by a vendored capability source changed content"
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1008
1045
|
function consentDrift(expected, actual) {
|
|
1009
1046
|
const before = consentedShape(actual);
|
|
1010
1047
|
const after = consentedShape(expected);
|
|
1011
1048
|
return Object.keys(after)
|
|
1012
1049
|
.filter((key) => stableJson(after[key]) !== stableJson(before[key]))
|
|
1013
|
-
.map((key) => `${key} no longer matches the locked value`);
|
|
1050
|
+
.map((key) => CONSENT_DRIFT_REASONS[key] ?? `${key} no longer matches the locked value`);
|
|
1014
1051
|
}
|
|
1015
1052
|
|
|
1016
1053
|
function permissionDrift(expected, actual) {
|
|
@@ -110,6 +110,39 @@ function assertContainedDirectory(projectRoot, target, label) {
|
|
|
110
110
|
return targetReal;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Verify a path this code is about to create, empty or write sits inside the
|
|
115
|
+
* project, without requiring it to exist yet.
|
|
116
|
+
*
|
|
117
|
+
* The read path can lstat its target and be done with it. A write path cannot:
|
|
118
|
+
* the destination is normally absent, and the failure that matters is an
|
|
119
|
+
* *ancestor* leading somewhere else. A repository shipping `.pi` as a symlink is
|
|
120
|
+
* the whole attack — `mkdir -p` follows it, the directory that appears below is
|
|
121
|
+
* a real directory, so a check on the immediate parent sees nothing wrong, and
|
|
122
|
+
* the recursive delete that comes next lands outside the project.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} rootReal a project root already through realpath
|
|
125
|
+
* @param {string} target
|
|
126
|
+
* @param {string} label
|
|
127
|
+
*/
|
|
128
|
+
function assertContainedWritePath(rootReal, target, label) {
|
|
129
|
+
const relative = path.relative(rootReal, path.resolve(rootReal, target));
|
|
130
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) fail(`${label} resolves outside the project`);
|
|
131
|
+
let current = rootReal;
|
|
132
|
+
for (const segment of relative.split(path.sep)) {
|
|
133
|
+
if (!segment) continue;
|
|
134
|
+
current = path.join(current, segment);
|
|
135
|
+
let stat;
|
|
136
|
+
try {
|
|
137
|
+
stat = fs.lstatSync(current);
|
|
138
|
+
} catch {
|
|
139
|
+
// Absent from here down, so there is nothing further to follow.
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (stat.isSymbolicLink()) fail(`${label} must not resolve through a symbolic link (${segment})`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
113
146
|
function assertNoSymbolicLinks(root, label, budget = { remaining: MAX_VENDOR_ENTRIES }) {
|
|
114
147
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
115
148
|
if (budget.remaining-- <= 0) fail(`${label} contains more than ${MAX_VENDOR_ENTRIES} entries`);
|
|
@@ -212,27 +245,55 @@ export function vendorRemoteSource(projectRoot, entry) {
|
|
|
212
245
|
const [validated] = validateCapabilitySources([entry]);
|
|
213
246
|
if (!validated.source) fail(`capability source ${validated.name} is a local path and does not need vendoring`);
|
|
214
247
|
|
|
215
|
-
|
|
248
|
+
// Resolved the same way the read path resolves it, so the containment check
|
|
249
|
+
// below compares two paths of the same form.
|
|
250
|
+
const rootReal = fs.realpathSync(projectRoot);
|
|
251
|
+
const destination = vendorDirectoryFor(rootReal, validated.name);
|
|
252
|
+
const label = `capability source ${validated.name}`;
|
|
253
|
+
|
|
254
|
+
// Before anything is created and well before anything is removed: every call
|
|
255
|
+
// after this line writes or deletes, and the point of the check is to have
|
|
256
|
+
// happened first.
|
|
257
|
+
assertContainedWritePath(rootReal, destination, label);
|
|
216
258
|
const parent = path.dirname(destination);
|
|
217
259
|
fs.mkdirSync(parent, { recursive: true, mode: 0o755 });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
260
|
+
// Re-checked after the directories exist. The first pass ran against a path
|
|
261
|
+
// that was mostly absent, so it proved nothing about the components this call
|
|
262
|
+
// just created or about one swapped underneath in between.
|
|
263
|
+
assertContainedWritePath(rootReal, destination, label);
|
|
264
|
+
|
|
265
|
+
// Fetched beside the destination and moved into place at the end, so a fetch
|
|
266
|
+
// that fails leaves the reviewed tree that was already there. Emptying the
|
|
267
|
+
// destination first meant a network error or a hostile archive destroyed a
|
|
268
|
+
// working vendor directory and left the project with neither version.
|
|
269
|
+
const staging = `${destination}.incoming.${process.pid}`;
|
|
270
|
+
assertContainedWritePath(rootReal, staging, label);
|
|
271
|
+
for (const sibling of fs.readdirSync(parent)) {
|
|
272
|
+
// Debris from a run that died between creating this and moving it.
|
|
273
|
+
if (sibling.startsWith(`${validated.name}.incoming.`)) fs.rmSync(path.join(parent, sibling), { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
fs.mkdirSync(staging, { mode: 0o755 });
|
|
221
276
|
|
|
222
277
|
try {
|
|
223
|
-
if (validated.source.startsWith("npm:")) fetchNpmSource(validated.source.slice("npm:".length),
|
|
224
|
-
else fetchGitSource(validated.source,
|
|
278
|
+
if (validated.source.startsWith("npm:")) fetchNpmSource(validated.source.slice("npm:".length), staging);
|
|
279
|
+
else fetchGitSource(validated.source, staging);
|
|
225
280
|
|
|
226
281
|
// 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
|
-
|
|
229
|
-
|
|
282
|
+
// hostile archive is rejected before anything reads a manifest out of it,
|
|
283
|
+
// and before it can replace the tree already in place.
|
|
284
|
+
assertNoSymbolicLinks(staging, `capability source ${validated.name}`);
|
|
285
|
+
if (!fs.existsSync(path.join(staging, "packs"))) {
|
|
230
286
|
fail(`capability source ${validated.name} provides no packs directory`);
|
|
231
287
|
}
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
// the failure, so remove it.
|
|
288
|
+
|
|
289
|
+
// Everything above succeeded, so the old tree can go.
|
|
235
290
|
fs.rmSync(destination, { recursive: true, force: true });
|
|
291
|
+
fs.renameSync(staging, destination);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// Only the half-written fetch is removed. The destination is either
|
|
294
|
+
// untouched, because the failure happened before the swap, or already
|
|
295
|
+
// replaced by a tree that passed every check.
|
|
296
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
236
297
|
throw error;
|
|
237
298
|
}
|
|
238
299
|
|