@piagent/platform 1.3.0-ie.9 → 1.3.1
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 +36 -3
- package/README.md +1 -1
- package/docs/capability-packs.md +9 -2
- 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/release-install-policy.md +29 -21
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +1 -1
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- 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 +1 -1
- 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/whats-new.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/package.json +2 -3
- package/packages/piagent-core/capabilities/capability-verification-cache.js +280 -0
- package/packages/piagent-core/capabilities/runtime-integrity.js +10 -1
- package/packages/piagent-core/extensions/document-intake.ts +24 -10
- package/packages/piagent-core/extensions/piagent-guard.ts +28 -36
- package/packages/piagent-core/extensions/state-retention.js +77 -44
- package/packages/piagent-core/package.json +1 -1
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/mcp-approval" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/mcp-approval" hreflang="en" lang="en">EN</a>
|
package/docs-site/mcp-auth.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/mcp-auth" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/mcp-auth" hreflang="en" lang="en">EN</a>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/mcp-servers" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/mcp-servers" hreflang="en" lang="en">EN</a>
|
package/docs-site/mcp.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/mcp" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/mcp" hreflang="en" lang="en">EN</a>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/permissions" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/permissions" hreflang="en" lang="en">EN</a>
|
package/docs-site/profiles.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/profiles" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/profiles" hreflang="en" lang="en">EN</a>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/quickstart" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/quickstart" hreflang="en" lang="en">EN</a>
|
package/docs-site/runtime.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/runtime" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/runtime" hreflang="en" lang="en">EN</a>
|
package/docs-site/scope.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/scope" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/scope" hreflang="en" lang="en">EN</a>
|
package/docs-site/security.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/security" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/security" hreflang="en" lang="en">EN</a>
|
package/docs-site/team.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/team" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/team" hreflang="en" lang="en">EN</a>
|
package/docs-site/whats-new.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/whats-new" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/whats-new" hreflang="en" lang="en">EN</a>
|
package/docs-site/workflows.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<span>Pi Agent Platform</span>
|
|
31
31
|
</a>
|
|
32
32
|
<div class="top-actions">
|
|
33
|
-
<span class="pill"><span class="pill-dot"></span>v1.3.
|
|
33
|
+
<span class="pill"><span class="pill-dot"></span>v1.3.1 docs · VI</span>
|
|
34
34
|
<nav class="language-switch" aria-label="Ngôn ngữ">
|
|
35
35
|
<a class="language-option current" href="/workflows" hreflang="vi" lang="vi" aria-current="true">VI</a>
|
|
36
36
|
<a class="language-option" href="/en/workflows" hreflang="en" lang="en">EN</a>
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piagent/platform",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
|
-
"provenance": true
|
|
8
|
-
"tag": "next"
|
|
7
|
+
"provenance": true
|
|
9
8
|
},
|
|
10
9
|
"type": "module",
|
|
11
10
|
"description": "Reusable Pi agent platform, project adapters, MCP/tool policy, and coding workflow harness.",
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { sha256, stableJson, verifyCapabilityLock } from "./capability-core.js";
|
|
5
|
+
import { resolveProjectProfileDocument } from "./project-profile.js";
|
|
6
|
+
|
|
7
|
+
const compare = (left, right) => String(left).localeCompare(String(right), "en");
|
|
8
|
+
const MAX_PACK_ENTRIES = 256;
|
|
9
|
+
const MAX_ARTIFACTS_PER_KIND = 128;
|
|
10
|
+
const MAX_JSON_BYTES = 256 * 1024;
|
|
11
|
+
|
|
12
|
+
// Full capability resolution deliberately hashes every executable runtime file.
|
|
13
|
+
// That is the right cold-path check, but not useful per-call work. This cache
|
|
14
|
+
// remembers only a `current` full verification. Reuse still walks strong
|
|
15
|
+
// metadata for every input that produced it; any change or error falls back to
|
|
16
|
+
// the cryptographic verifier.
|
|
17
|
+
export function createCapabilityVerificationCache() {
|
|
18
|
+
return new Map();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cacheRootForPack(rootReal, pack, extraRoots) {
|
|
22
|
+
return pack.origin === "workspace"
|
|
23
|
+
? rootReal
|
|
24
|
+
: extraRoots.find((entry) => entry.origin === pack.origin)?.root;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function addTarget(targets, target, kind, optional = false) {
|
|
28
|
+
const absolute = path.resolve(target);
|
|
29
|
+
const previous = targets.get(absolute);
|
|
30
|
+
if (!previous || previous.kind !== "directory") targets.set(absolute, { path: absolute, kind, optional });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function containedTarget(root, relativePath) {
|
|
34
|
+
if (typeof relativePath !== "string" || path.isAbsolute(relativePath)) return undefined;
|
|
35
|
+
const absolute = path.resolve(root, relativePath);
|
|
36
|
+
const relative = path.relative(root, absolute);
|
|
37
|
+
return relative.startsWith("..") || path.isAbsolute(relative) ? undefined : absolute;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readBoundedJson(file) {
|
|
41
|
+
const stat = fs.lstatSync(file);
|
|
42
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_JSON_BYTES) return undefined;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function addEvalInputs(targets, packRoot, artifactPath) {
|
|
51
|
+
const artifact = readBoundedJson(artifactPath);
|
|
52
|
+
const profile = containedTarget(packRoot, artifact?.spec?.profile);
|
|
53
|
+
const fixture = containedTarget(packRoot, artifact?.spec?.fixture?.path);
|
|
54
|
+
if (!profile || !fixture) return false;
|
|
55
|
+
addTarget(targets, profile, "file");
|
|
56
|
+
addTarget(targets, fixture, "file");
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function addCatalogTargets(targets, packRoot) {
|
|
61
|
+
const packsDirectory = path.join(packRoot, "packs");
|
|
62
|
+
addTarget(targets, packsDirectory, "directory");
|
|
63
|
+
const entries = fs.readdirSync(packsDirectory, { withFileTypes: true })
|
|
64
|
+
.filter((entry) => !entry.name.startsWith("."));
|
|
65
|
+
if (entries.length > MAX_PACK_ENTRIES) return false;
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (!entry.isDirectory() || entry.isSymbolicLink?.()) return false;
|
|
68
|
+
const packDirectory = path.join(packsDirectory, entry.name);
|
|
69
|
+
addTarget(targets, packDirectory, "directory");
|
|
70
|
+
const manifestPath = path.join(packDirectory, "pack.json");
|
|
71
|
+
addTarget(targets, manifestPath, "file");
|
|
72
|
+
const manifest = readBoundedJson(manifestPath);
|
|
73
|
+
const provides = manifest?.spec?.provides;
|
|
74
|
+
if (!provides || typeof provides !== "object" || Array.isArray(provides)) return false;
|
|
75
|
+
for (const [kind, artifacts] of Object.entries(provides)) {
|
|
76
|
+
if (!Array.isArray(artifacts) || artifacts.length > MAX_ARTIFACTS_PER_KIND) return false;
|
|
77
|
+
for (const artifact of artifacts) {
|
|
78
|
+
const target = containedTarget(packRoot, artifact?.path);
|
|
79
|
+
if (!target) return false;
|
|
80
|
+
addTarget(targets, target, "file");
|
|
81
|
+
if (kind === "evals" && !addEvalInputs(targets, packRoot, target)) return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function verificationTargets(root, profilePath, lockPath, document, options = {}) {
|
|
89
|
+
const rootReal = fs.realpathSync(root);
|
|
90
|
+
const targets = new Map();
|
|
91
|
+
addTarget(targets, profilePath, "file");
|
|
92
|
+
addTarget(targets, lockPath, "file");
|
|
93
|
+
for (const target of options.identityFiles ?? []) addTarget(targets, target, "file", true);
|
|
94
|
+
|
|
95
|
+
const extraRoots = options.extraRoots ?? [];
|
|
96
|
+
const packRoots = new Map();
|
|
97
|
+
for (const pack of document?.packs ?? []) {
|
|
98
|
+
const packRoot = cacheRootForPack(rootReal, pack, extraRoots);
|
|
99
|
+
if (!packRoot) continue;
|
|
100
|
+
packRoots.set(`${pack.name}@${pack.version}`, packRoot);
|
|
101
|
+
addTarget(targets, path.join(packRoot, "packs"), "directory");
|
|
102
|
+
addTarget(targets, path.join(packRoot, "packs", pack.name), "directory");
|
|
103
|
+
addTarget(targets, path.join(packRoot, "packs", pack.name, "pack.json"), "file");
|
|
104
|
+
}
|
|
105
|
+
for (const entry of extraRoots) {
|
|
106
|
+
addTarget(targets, entry.root, "directory");
|
|
107
|
+
addTarget(targets, path.join(entry.root, "packs"), "directory");
|
|
108
|
+
}
|
|
109
|
+
for (const packRoot of [rootReal, ...extraRoots.map((entry) => entry.root)]) {
|
|
110
|
+
if (!addCatalogTargets(targets, packRoot)) return undefined;
|
|
111
|
+
}
|
|
112
|
+
for (const entry of document?.core?.runtimeFiles ?? []) {
|
|
113
|
+
const target = containedTarget(rootReal, entry.path);
|
|
114
|
+
if (!target) return undefined;
|
|
115
|
+
addTarget(targets, target, "file");
|
|
116
|
+
}
|
|
117
|
+
for (const artifact of document?.artifacts ?? []) {
|
|
118
|
+
const artifactRoot = packRoots.get(artifact.pack);
|
|
119
|
+
if (!artifactRoot) continue;
|
|
120
|
+
const target = containedTarget(artifactRoot, artifact.path);
|
|
121
|
+
if (!target) return undefined;
|
|
122
|
+
addTarget(targets, target, "file");
|
|
123
|
+
}
|
|
124
|
+
return [...targets.values()].sort((left, right) => compare(left.path, right.path));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function statIdentity(stat, includeTimes) {
|
|
128
|
+
const values = [stat.dev, stat.ino, stat.mode, stat.nlink, stat.uid, stat.gid, stat.rdev, stat.size];
|
|
129
|
+
if (includeTimes) values.push(stat.mtimeNs, stat.ctimeNs);
|
|
130
|
+
return values.map(String).join(":");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ancestorIdentity(stat) {
|
|
134
|
+
return [stat.dev, stat.ino, stat.mode, stat.uid, stat.gid, stat.rdev]
|
|
135
|
+
.map(String)
|
|
136
|
+
.join(":");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function lexicalAncestors(target) {
|
|
140
|
+
const parsed = path.parse(target);
|
|
141
|
+
const segments = target.slice(parsed.root.length).split(path.sep).filter(Boolean);
|
|
142
|
+
const ancestors = [];
|
|
143
|
+
let current = parsed.root;
|
|
144
|
+
for (const segment of segments.slice(0, -1)) {
|
|
145
|
+
current = path.join(current, segment);
|
|
146
|
+
ancestors.push(current);
|
|
147
|
+
}
|
|
148
|
+
return ancestors;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function metadataSignature(targets) {
|
|
152
|
+
if (!targets) return undefined;
|
|
153
|
+
const records = [];
|
|
154
|
+
const ancestors = new Set(targets.flatMap((target) => lexicalAncestors(target.path)));
|
|
155
|
+
try {
|
|
156
|
+
for (const ancestor of [...ancestors].sort(compare)) {
|
|
157
|
+
const stat = fs.lstatSync(ancestor, { bigint: true });
|
|
158
|
+
const symbolic = stat.isSymbolicLink();
|
|
159
|
+
const identity = symbolic ? statIdentity(stat, true) : ancestorIdentity(stat);
|
|
160
|
+
records.push(`a:${ancestor}:${identity}:${symbolic ? fs.readlinkSync(ancestor) : "directory"}`);
|
|
161
|
+
}
|
|
162
|
+
for (const target of targets) {
|
|
163
|
+
let lexical;
|
|
164
|
+
try {
|
|
165
|
+
lexical = fs.lstatSync(target.path, { bigint: true });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (target.optional && error?.code === "ENOENT") {
|
|
168
|
+
records.push(`t:${target.path}:absent`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const expectedType = target.kind === "directory" ? lexical.isDirectory() : lexical.isFile();
|
|
174
|
+
if (!expectedType || lexical.isSymbolicLink()) return undefined;
|
|
175
|
+
records.push(`t:${target.path}:${statIdentity(lexical, true)}`);
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
return sha256(records.join("\n"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const cacheKey = (root, profilePath, lockPath) => (
|
|
184
|
+
`${path.resolve(root)}\0${path.resolve(profilePath)}\0${path.resolve(lockPath)}`
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
function cacheIdentity(lockDocument, options = {}) {
|
|
188
|
+
return sha256(stableJson({
|
|
189
|
+
lockDocument,
|
|
190
|
+
packageSource: options.packageSource,
|
|
191
|
+
extraRoots: (options.extraRoots ?? []).map((entry) => ({ origin: entry.origin, root: entry.root, source: entry.source })),
|
|
192
|
+
identityFiles: (options.identityFiles ?? []).map((entry) => path.resolve(entry)).sort(compare)
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function snapshotTargets(targets) {
|
|
197
|
+
const signature = metadataSignature(targets);
|
|
198
|
+
return signature ? { signature, targets } : undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function stableSnapshot(snapshot) {
|
|
202
|
+
if (!snapshot) return false;
|
|
203
|
+
const current = metadataSignature(snapshot.targets);
|
|
204
|
+
return Boolean(current && current === snapshot.signature);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function verifyCapabilityLockCached(cache, root, profilePath, lockPath, lockDocument, options = {}) {
|
|
208
|
+
const key = cacheKey(root, profilePath, lockPath);
|
|
209
|
+
const identity = cacheIdentity(lockDocument, options);
|
|
210
|
+
const cached = options.forceFull === true ? undefined : cache?.get(key);
|
|
211
|
+
if (cached?.identity === identity) {
|
|
212
|
+
const signature = metadataSignature(cached.targets);
|
|
213
|
+
if (signature && signature === cached.signature) return { ...cached.verification, cacheStatus: "hit" };
|
|
214
|
+
}
|
|
215
|
+
cache?.delete(key);
|
|
216
|
+
|
|
217
|
+
const initialTargets = verificationTargets(root, profilePath, lockPath, lockDocument, options);
|
|
218
|
+
const before = snapshotTargets(initialTargets);
|
|
219
|
+
const verification = verifyCapabilityLock(root, profilePath, lockDocument, options);
|
|
220
|
+
options.afterFullVerify?.();
|
|
221
|
+
if (verification.status === "current" && before) {
|
|
222
|
+
const after = snapshotTargets(verificationTargets(root, profilePath, lockPath, verification.expected, options));
|
|
223
|
+
if (after && before.signature === after.signature && stableSnapshot(after)) {
|
|
224
|
+
cache?.set(key, { identity, ...after, verification });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { ...verification, cacheStatus: "verified" };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function verifyProjectCapabilityStateCached(input) {
|
|
231
|
+
const { cache, cwd, platformRoot, profilePath, lockPath, lockDocument, storedProfile } = input;
|
|
232
|
+
const profile = resolveProjectProfileDocument(platformRoot, storedProfile).profile;
|
|
233
|
+
if (!Array.isArray(profile.capabilityPacks)) return { ok: true };
|
|
234
|
+
const packageSource = input.packageSource(cwd);
|
|
235
|
+
const extraRoots = input.extraRoots(cwd, profile);
|
|
236
|
+
const identityFiles = [path.join(cwd, ".pi", "settings.json")];
|
|
237
|
+
if (typeof storedProfile.extends === "string" && /^[a-z0-9][a-z0-9-]{0,63}$/.test(storedProfile.extends)) {
|
|
238
|
+
identityFiles.push(path.join(platformRoot, "adapters", storedProfile.extends, "profile.json"));
|
|
239
|
+
}
|
|
240
|
+
const verificationOptions = { packageSource, extraRoots, identityFiles, forceFull: input.forceFull === true };
|
|
241
|
+
const verification = verifyCapabilityLockCached(cache, platformRoot, profilePath, lockPath, lockDocument, verificationOptions);
|
|
242
|
+
const runtimeDigest = verification.expected?.core?.packageDigest;
|
|
243
|
+
if (input.allowRepin !== true && input.sessionDigest && runtimeDigest !== input.sessionDigest) {
|
|
244
|
+
return { ok: false, reason: "Installed Piagent runtime changed during this session. Restart the session to verify and re-pin the capability lock before using tools." };
|
|
245
|
+
}
|
|
246
|
+
if (verification.status === "repin" && input.allowRepin !== true) {
|
|
247
|
+
return { ok: false, reason: "Installed Piagent runtime changed during this session. Restart the session to verify and re-pin the capability lock before using tools." };
|
|
248
|
+
}
|
|
249
|
+
if (verification.status === "blocked") {
|
|
250
|
+
return { ok: false, reason: `Capability lock does not match what this project agreed to: ${verification.reasons.join("; ")}. Reapply the project profile.` };
|
|
251
|
+
}
|
|
252
|
+
const granted = {
|
|
253
|
+
filesystemRead: verification.expected.permissions.filesystemRead,
|
|
254
|
+
filesystemWrite: verification.expected.permissions.filesystemWrite
|
|
255
|
+
};
|
|
256
|
+
if (verification.status !== "repin") {
|
|
257
|
+
if (input.allowRepin === true) input.rememberSessionDigest?.(runtimeDigest);
|
|
258
|
+
return { ok: true, ...granted };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
input.writeLock(lockPath, verification.expected);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return { ok: false, reason: `Capability lock is behind the installed platform and could not be rewritten: ${error instanceof Error ? error.message : String(error)}` };
|
|
265
|
+
}
|
|
266
|
+
input.afterRepinWrite?.();
|
|
267
|
+
const confirmed = verifyCapabilityLockCached(
|
|
268
|
+
cache,
|
|
269
|
+
platformRoot,
|
|
270
|
+
profilePath,
|
|
271
|
+
lockPath,
|
|
272
|
+
verification.expected,
|
|
273
|
+
{ ...verificationOptions, forceFull: true }
|
|
274
|
+
);
|
|
275
|
+
if (confirmed.status !== "current") {
|
|
276
|
+
return { ok: false, reason: "Installed Piagent runtime changed while the capability lock was being re-pinned. Restart the session and try again." };
|
|
277
|
+
}
|
|
278
|
+
input.rememberSessionDigest?.(runtimeDigest);
|
|
279
|
+
return { ok: true, repinned: verification.reasons.join("; "), ...granted };
|
|
280
|
+
}
|
|
@@ -7,17 +7,23 @@ export const CORE_RUNTIME_INTEGRITY_FILES = Object.freeze([
|
|
|
7
7
|
"packages/piagent-core/extensions/acceptance-receipt.js",
|
|
8
8
|
"packages/piagent-core/extensions/criterion-graph.js",
|
|
9
9
|
"packages/piagent-core/extensions/criterion-context-pack.js",
|
|
10
|
+
"packages/piagent-core/extensions/document-intake.ts",
|
|
11
|
+
"packages/piagent-core/extensions/execution-backend.js",
|
|
10
12
|
"packages/piagent-core/extensions/failure-types.ts",
|
|
11
13
|
"packages/piagent-core/extensions/repository-memory.js",
|
|
14
|
+
"packages/piagent-core/extensions/state-retention.js",
|
|
12
15
|
"packages/piagent-core/extensions/task-contract-view.js",
|
|
13
16
|
"packages/piagent-core/extensions/task-authority-contract.js",
|
|
14
17
|
"packages/piagent-core/extensions/task-digest-migration.js",
|
|
15
18
|
"packages/piagent-core/extensions/task-journal.js",
|
|
16
19
|
"packages/piagent-core/extensions/task-runtime-audit.js",
|
|
20
|
+
"packages/piagent-core/extensions/task-state.js",
|
|
17
21
|
"packages/piagent-core/extensions/tenant-contract.js",
|
|
22
|
+
"packages/piagent-core/extensions/verification-intelligence.js",
|
|
18
23
|
"packages/piagent-core/extensions/working-tree-digest.js",
|
|
19
24
|
"packages/piagent-core/policy/authority-manifest.v1.json",
|
|
20
25
|
"packages/piagent-core/capabilities/authority-manifest.ts",
|
|
26
|
+
"packages/piagent-core/capabilities/capability-verification-cache.js",
|
|
21
27
|
"packages/piagent-core/runtime/hooks/agent-start-hook.ts",
|
|
22
28
|
"packages/piagent-core/runtime/hooks/completion-hook.ts",
|
|
23
29
|
"packages/piagent-core/runtime/hooks/input-hook.ts",
|
|
@@ -95,5 +101,8 @@ export const CORE_RUNTIME_INTEGRITY_FILES = Object.freeze([
|
|
|
95
101
|
"packages/piagent-core/runtime/tools/phase-tools.ts",
|
|
96
102
|
"packages/piagent-core/runtime/tools/phase-tool-runtime.ts",
|
|
97
103
|
"packages/piagent-core/runtime/workflows/input-routing.ts",
|
|
98
|
-
"packages/piagent-core/runtime/workflows/task-intake.ts"
|
|
104
|
+
"packages/piagent-core/runtime/workflows/task-intake.ts",
|
|
105
|
+
"scripts/piagent-cli.mjs",
|
|
106
|
+
"scripts/register-typescript-loader.mjs",
|
|
107
|
+
"scripts/typescript-loader.mjs"
|
|
99
108
|
]);
|
|
@@ -51,6 +51,8 @@ export type DocumentExtraction =
|
|
|
51
51
|
| { status: "ok"; text: string; truncated: boolean; kind: "text" | "docx" | "pdf" }
|
|
52
52
|
| { status: "error"; reason: string };
|
|
53
53
|
|
|
54
|
+
type DocumentCommandResult = { status: number | null; stdout: string; stderr: string; error?: Error };
|
|
55
|
+
|
|
54
56
|
export function expandHome(candidate: string, home: string | undefined): string {
|
|
55
57
|
if (candidate === "~") return home ?? candidate;
|
|
56
58
|
if (candidate.startsWith("~/")) {
|
|
@@ -435,7 +437,7 @@ export function extractTextDocument(buffer: Buffer): DocumentExtraction {
|
|
|
435
437
|
// missing tool would be a lie in the direction that wastes the most time.
|
|
436
438
|
export function extractPdfFromDescriptor(
|
|
437
439
|
fd: number,
|
|
438
|
-
run: (command: string, args: string[], input?: number) =>
|
|
440
|
+
run: (command: string, args: string[], input?: number) => DocumentCommandResult = defaultRun
|
|
439
441
|
): DocumentExtraction {
|
|
440
442
|
const probe = run("command", ["-v", "pdftotext"]);
|
|
441
443
|
if (probe.error || probe.status !== 0) {
|
|
@@ -471,19 +473,31 @@ export function extractPdfFromDescriptor(
|
|
|
471
473
|
// returns would hang the session rather than fail the read.
|
|
472
474
|
const PDF_TIMEOUT_MS = 20_000;
|
|
473
475
|
|
|
476
|
+
export function probeExecutableOnPath(executable: string): DocumentCommandResult {
|
|
477
|
+
const result = spawnSync("/bin/sh", ["-c", 'command -v "$1"', "piagent-command-probe", executable], {
|
|
478
|
+
encoding: "utf8",
|
|
479
|
+
timeout: PDF_TIMEOUT_MS
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
status: result.status,
|
|
483
|
+
stdout: result.stdout ?? "",
|
|
484
|
+
stderr: result.stderr ?? "",
|
|
485
|
+
error: result.error
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
474
489
|
function defaultRun(
|
|
475
490
|
command: string,
|
|
476
491
|
args: string[],
|
|
477
492
|
input?: number
|
|
478
|
-
):
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
});
|
|
493
|
+
): DocumentCommandResult {
|
|
494
|
+
if (command === "command") return probeExecutableOnPath(args[1] ?? "");
|
|
495
|
+
const result = spawnSync(command, args, {
|
|
496
|
+
encoding: "utf8",
|
|
497
|
+
maxBuffer: MAX_DOCUMENT_BYTES,
|
|
498
|
+
timeout: PDF_TIMEOUT_MS,
|
|
499
|
+
stdio: [input ?? "ignore", "pipe", "pipe"]
|
|
500
|
+
});
|
|
487
501
|
return {
|
|
488
502
|
status: result.status,
|
|
489
503
|
stdout: result.stdout ?? "",
|
|
@@ -49,10 +49,13 @@ import * as mcpActions from "../mcp/mcp-command-actions.js";
|
|
|
49
49
|
import { buildExtendingProfile, resolveProjectProfileDocument } from "../capabilities/project-profile.js";
|
|
50
50
|
import {
|
|
51
51
|
resolveCapabilityProfileDocument,
|
|
52
|
-
verifyCapabilityLock,
|
|
53
52
|
writeJsonAtomic,
|
|
54
53
|
writeProfileLockAtomic
|
|
55
54
|
} from "../capabilities/capability-core.js";
|
|
55
|
+
import {
|
|
56
|
+
createCapabilityVerificationCache,
|
|
57
|
+
verifyProjectCapabilityStateCached
|
|
58
|
+
} from "../capabilities/capability-verification-cache.js";
|
|
56
59
|
import { resolveCapabilitySourceRoots } from "../capabilities/capability-sources.js";
|
|
57
60
|
import {
|
|
58
61
|
actionTextMatchesAny, actionTokens, classifyActionTokenSequence, classifyExplicitActionValues, classifyToolNameAction,
|
|
@@ -715,7 +718,15 @@ type ProjectCapabilityState = {
|
|
|
715
718
|
filesystemWrite?: string[];
|
|
716
719
|
};
|
|
717
720
|
|
|
718
|
-
|
|
721
|
+
const capabilityVerificationCache = createCapabilityVerificationCache();
|
|
722
|
+
const sessionCapabilityDigests = new Map<string, string>();
|
|
723
|
+
|
|
724
|
+
function verifyProjectCapabilityState(
|
|
725
|
+
extensionDir: string,
|
|
726
|
+
cwd: string,
|
|
727
|
+
projectTrusted: boolean,
|
|
728
|
+
options: { allowRepin?: boolean; forceFull?: boolean; sessionId?: string } = {}
|
|
729
|
+
): ProjectCapabilityState {
|
|
719
730
|
if (process.env.PIAGENT_PROFILE?.trim()) return { ok: true };
|
|
720
731
|
if (!projectTrusted) return { ok: true };
|
|
721
732
|
const profilePath = projectProfilePath(cwd);
|
|
@@ -725,46 +736,24 @@ function verifyProjectCapabilityState(extensionDir: string, cwd: string, project
|
|
|
725
736
|
// would skip the lock entirely for every project that references an adapter.
|
|
726
737
|
const stored = readJsonFile<ProjectProfile>(profilePath);
|
|
727
738
|
if (!stored) return { ok: true };
|
|
728
|
-
let profile: ProjectProfile;
|
|
729
739
|
try {
|
|
730
|
-
profile = resolveProjectProfileDocument(
|
|
740
|
+
const profile = resolveProjectProfileDocument(findPlatformRoot(extensionDir), stored).profile;
|
|
741
|
+
if (!Array.isArray(profile.capabilityPacks)) return { ok: true };
|
|
731
742
|
} catch (error) {
|
|
732
743
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
733
744
|
}
|
|
734
|
-
if (!Array.isArray(profile.capabilityPacks)) return { ok: true };
|
|
735
745
|
const lockPath = path.join(cwd, ".pi", "piagent-profile.lock.json");
|
|
736
746
|
if (!fs.existsSync(lockPath)) return { ok: false, reason: "Capability lock is missing. Reapply the project profile." };
|
|
737
747
|
const lock = readJsonFile<Record<string, unknown>>(lockPath);
|
|
738
748
|
if (!lock) return { ok: false, reason: "Capability lock is unreadable. Reapply the project profile." };
|
|
739
749
|
try {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
extraRoots: capabilitySourceRoots
|
|
750
|
+
return verifyProjectCapabilityStateCached({
|
|
751
|
+
cache: capabilityVerificationCache, cwd, platformRoot: findPlatformRoot(extensionDir), profilePath, lockPath,
|
|
752
|
+
lockDocument: lock, storedProfile: stored, packageSource: projectPackageSource, extraRoots: capabilitySourceRoots,
|
|
753
|
+
writeLock: writeJsonAtomic, allowRepin: options.allowRepin, forceFull: options.forceFull,
|
|
754
|
+
sessionDigest: options.sessionId ? sessionCapabilityDigests.get(`${cwd}\0${options.sessionId}`) : undefined,
|
|
755
|
+
rememberSessionDigest: options.sessionId ? (digest: string) => sessionCapabilityDigests.set(`${cwd}\0${options.sessionId}`, digest) : undefined
|
|
743
756
|
});
|
|
744
|
-
if (verification.status === "blocked") {
|
|
745
|
-
return {
|
|
746
|
-
ok: false,
|
|
747
|
-
reason: `Capability lock does not match what this project agreed to: ${verification.reasons.join("; ")}. Reapply the project profile.`
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
const granted = {
|
|
751
|
-
filesystemRead: verification.expected.permissions.filesystemRead,
|
|
752
|
-
filesystemWrite: verification.expected.permissions.filesystemWrite
|
|
753
|
-
};
|
|
754
|
-
if (verification.status !== "repin") return { ok: true, ...granted };
|
|
755
|
-
|
|
756
|
-
// The grant is unchanged and only the platform build moved. Record the new
|
|
757
|
-
// build instead of stopping the session, which is what turned every release
|
|
758
|
-
// into a chore in every project.
|
|
759
|
-
try {
|
|
760
|
-
writeJsonAtomic(lockPath, verification.expected);
|
|
761
|
-
} catch (error) {
|
|
762
|
-
return {
|
|
763
|
-
ok: false,
|
|
764
|
-
reason: `Capability lock is behind the installed platform and could not be rewritten: ${error instanceof Error ? error.message : String(error)}`
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
return { ok: true, repinned: verification.reasons.join("; "), ...granted };
|
|
768
757
|
} catch (error) {
|
|
769
758
|
return { ok: false, reason: `Capability validation failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
770
759
|
}
|
|
@@ -4273,7 +4262,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4273
4262
|
resolveTaskAny: (cwd, reference) => resolveTaskContract(cwd, reference, undefined) as TaskContract | undefined,
|
|
4274
4263
|
bindTask: bindSessionTask,
|
|
4275
4264
|
writeTask,
|
|
4276
|
-
capabilityState: (ctx) => verifyProjectCapabilityState(extensionDir, ctx.cwd, ctx.isProjectTrusted()),
|
|
4265
|
+
capabilityState: (ctx) => verifyProjectCapabilityState(extensionDir, ctx.cwd, ctx.isProjectTrusted(), { allowRepin: true, forceFull: true, sessionId: ctx.sessionManager.getSessionId() }),
|
|
4277
4266
|
permissionProfile: (ctx, profile) => resolvePermissionProfile(profile, policy, permissionOverrideFromContext(ctx)),
|
|
4278
4267
|
legacyProjectWarning: legacyProjectStateWarning,
|
|
4279
4268
|
mcpReadinessNotice,
|
|
@@ -4311,7 +4300,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4311
4300
|
flushObservedTaskContext,
|
|
4312
4301
|
onTurnEnd: activityInspector.refresh,
|
|
4313
4302
|
onAgentSettled: activityInspector.refresh,
|
|
4314
|
-
beforeShutdown:
|
|
4303
|
+
beforeShutdown: (ctx) => {
|
|
4304
|
+
activityInspector.dispose(ctx);
|
|
4305
|
+
sessionCapabilityDigests.delete(`${ctx.cwd}\0${ctx.sessionManager.getSessionId()}`);
|
|
4306
|
+
}
|
|
4315
4307
|
});
|
|
4316
4308
|
|
|
4317
4309
|
registerInputHook(pi, {
|
|
@@ -4322,7 +4314,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4322
4314
|
const projectTrusted = ctx.isProjectTrusted();
|
|
4323
4315
|
const profile = loadProfileFromContext(ctx);
|
|
4324
4316
|
const permissionProfile = resolvePermissionProfile(profile, policy, permissionOverrideFromContext(ctx));
|
|
4325
|
-
const capabilityState = verifyProjectCapabilityState(extensionDir, ctx.cwd, projectTrusted);
|
|
4317
|
+
const capabilityState = verifyProjectCapabilityState(extensionDir, ctx.cwd, projectTrusted, { sessionId: ctx.sessionManager.getSessionId() });
|
|
4326
4318
|
return {
|
|
4327
4319
|
roots: resolveDocumentRoots({
|
|
4328
4320
|
cwd: ctx.cwd,
|
|
@@ -4572,7 +4564,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4572
4564
|
const eventInput = isPlainRecord(event.input) ? event.input : {};
|
|
4573
4565
|
const backendDecision = executionBackendToolDecision(isTaskMutationTool(event.toolName, eventInput));
|
|
4574
4566
|
if (!backendDecision.allowed) return { block: true, reason: backendDecision.reason };
|
|
4575
|
-
const capabilityState = verifyProjectCapabilityState(extensionDir, ctx.cwd, projectTrusted);
|
|
4567
|
+
const capabilityState = verifyProjectCapabilityState(extensionDir, ctx.cwd, projectTrusted, { sessionId: ctx.sessionManager.getSessionId() });
|
|
4576
4568
|
const recoveryTools = new Set(["piagent_profile_options", "piagent_profile_apply", "piagent_context", "read", "grep", "find", "ls"]);
|
|
4577
4569
|
if (!capabilityState.ok && !recoveryTools.has(event.toolName)) {
|
|
4578
4570
|
return { block: true, reason: capabilityState.reason ?? "Capability lock validation failed." };
|