@piagent/platform 1.2.8 → 1.2.10
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 +75 -0
- package/README.md +4 -4
- package/catalog/capabilities.json +2 -2
- package/docs/command-reference-vietnamese.md +7 -4
- package/docs/context-engine.md +30 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/mcp-and-tools.md +7 -0
- package/docs/operator-manual-vietnamese.md +27 -11
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +7 -1
- package/docs/subagents-and-multiagent.md +1 -1
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/context-engine.html +8 -1
- package/docs-site/content/security.html +1 -0
- package/docs-site/context-engine.html +9 -2
- 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 +2 -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 +6 -1
- package/packages/piagent-core/capabilities/capability-core.js +3 -0
- package/packages/piagent-core/extensions/context-engine.js +263 -120
- package/packages/piagent-core/extensions/context-index-policy.js +136 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +258 -101
- package/packages/piagent-core/mcp/mcp-config-layers.js +52 -6
- package/packages/piagent-core/mcp/mcp-tool-naming.js +12 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/subagents/piagent-reviewer.md +3 -3
- package/scripts/context-engine.mjs +22 -20
- package/scripts/update-global.mjs +1 -1
- package/scripts/verify-local.sh +2 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { resolveProjectProfileDocument } from "../capabilities/project-profile.js";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONTEXT_INDEX_PATH = ".pi/context-index.json";
|
|
8
|
+
const CONTEXT_ENGINE_STATE_PATTERN = ".pi/piagent-state/**";
|
|
9
|
+
const EXCLUDE_POLICY_VERSION = 2;
|
|
10
|
+
|
|
11
|
+
function stringArray(value) {
|
|
12
|
+
return Array.isArray(value)
|
|
13
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
14
|
+
: [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function uniqueStrings(values) {
|
|
18
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compareCodePoints(left, right) {
|
|
22
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizePattern(value) {
|
|
26
|
+
return String(value ?? "")
|
|
27
|
+
.trim()
|
|
28
|
+
.replaceAll("\\", "/")
|
|
29
|
+
.replace(/^\.\/+/, "")
|
|
30
|
+
.replace(/\/+/g, "/");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJsonObject(file, label, { optional = false } = {}) {
|
|
34
|
+
if (!fs.existsSync(file)) {
|
|
35
|
+
if (optional) return undefined;
|
|
36
|
+
throw new Error(`${label} is missing at ${file}`);
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`${label} is not valid JSON: ${file}`);
|
|
43
|
+
}
|
|
44
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
45
|
+
throw new Error(`${label} must contain a JSON object: ${file}`);
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function contextIndexStatePath(profile) {
|
|
51
|
+
const configured = profile?.contextIndex?.path;
|
|
52
|
+
return typeof configured === "string" && configured.trim()
|
|
53
|
+
? configured.trim()
|
|
54
|
+
: DEFAULT_CONTEXT_INDEX_PATH;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function effectiveProtectedPaths(policy = {}, profile = {}) {
|
|
58
|
+
const policyProtectedPaths = stringArray(policy.protectedPaths);
|
|
59
|
+
const baseReadProtectedPaths = Array.isArray(policy.shellProtectedPaths)
|
|
60
|
+
? stringArray(policy.shellProtectedPaths)
|
|
61
|
+
: policyProtectedPaths;
|
|
62
|
+
const profileProtectedPaths = stringArray(profile.protectedPaths);
|
|
63
|
+
const profileShellProtectedPaths = Array.isArray(profile.shellProtectedPaths)
|
|
64
|
+
? stringArray(profile.shellProtectedPaths)
|
|
65
|
+
: profileProtectedPaths;
|
|
66
|
+
const readOnlyPaths = stringArray(profile.readOnlyPaths);
|
|
67
|
+
const contextIndexPath = contextIndexStatePath(profile);
|
|
68
|
+
return {
|
|
69
|
+
readProtectedPaths: uniqueStrings([
|
|
70
|
+
...baseReadProtectedPaths,
|
|
71
|
+
...profileProtectedPaths,
|
|
72
|
+
contextIndexPath
|
|
73
|
+
]),
|
|
74
|
+
writeProtectedPaths: uniqueStrings([
|
|
75
|
+
...policyProtectedPaths,
|
|
76
|
+
...profileProtectedPaths,
|
|
77
|
+
...readOnlyPaths,
|
|
78
|
+
contextIndexPath
|
|
79
|
+
]),
|
|
80
|
+
shellProtectedPaths: uniqueStrings([
|
|
81
|
+
...baseReadProtectedPaths,
|
|
82
|
+
...profileShellProtectedPaths,
|
|
83
|
+
...readOnlyPaths,
|
|
84
|
+
contextIndexPath
|
|
85
|
+
]),
|
|
86
|
+
readOnlyPaths: uniqueStrings(readOnlyPaths)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeContextIndexExcludePatterns(patterns) {
|
|
91
|
+
return uniqueStrings(stringArray(patterns).map(normalizePattern)).sort(compareCodePoints);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function contextIndexExcludePatterns(policy, profile) {
|
|
95
|
+
const effective = effectiveProtectedPaths(policy, profile);
|
|
96
|
+
return normalizeContextIndexExcludePatterns([
|
|
97
|
+
...effective.readProtectedPaths,
|
|
98
|
+
...effective.writeProtectedPaths,
|
|
99
|
+
...effective.shellProtectedPaths,
|
|
100
|
+
CONTEXT_ENGINE_STATE_PATTERN
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function contextIndexExcludeDigest(patterns) {
|
|
105
|
+
return crypto
|
|
106
|
+
.createHash("sha256")
|
|
107
|
+
.update(JSON.stringify({
|
|
108
|
+
version: EXCLUDE_POLICY_VERSION,
|
|
109
|
+
patterns: normalizeContextIndexExcludePatterns(patterns)
|
|
110
|
+
}))
|
|
111
|
+
.digest("hex");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function contextIndexExcludePolicyVersion() {
|
|
115
|
+
return EXCLUDE_POLICY_VERSION;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function loadProjectContextIndexPolicy(platformRoot, projectPath, options = {}) {
|
|
119
|
+
const policyPath = path.join(platformRoot, "packages", "piagent-core", "policies", "base-policy.json");
|
|
120
|
+
const policy = readJsonObject(policyPath, "Piagent base policy");
|
|
121
|
+
const explicitProfilePath = typeof options.profilePath === "string" && options.profilePath.trim()
|
|
122
|
+
? path.resolve(options.profilePath)
|
|
123
|
+
: undefined;
|
|
124
|
+
const profilePath = explicitProfilePath ?? path.join(projectPath, ".pi", "piagent-profile.json");
|
|
125
|
+
const stored = readJsonObject(profilePath, "Piagent project profile", { optional: !explicitProfilePath });
|
|
126
|
+
const profile = stored
|
|
127
|
+
? resolveProjectProfileDocument(platformRoot, stored).profile
|
|
128
|
+
: {};
|
|
129
|
+
return {
|
|
130
|
+
policy,
|
|
131
|
+
profile,
|
|
132
|
+
profilePath,
|
|
133
|
+
profileSource: explicitProfilePath ? "env" : stored ? "project" : "fallback",
|
|
134
|
+
excludePatterns: contextIndexExcludePatterns(policy, profile)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -81,10 +81,15 @@ import {
|
|
|
81
81
|
buildTestImpact,
|
|
82
82
|
classifyContextTask,
|
|
83
83
|
contextIndexV2Status,
|
|
84
|
+
ensureContextIndexV2,
|
|
84
85
|
estimateContextTokens,
|
|
85
86
|
searchContextIndexV2,
|
|
86
87
|
toolResultFingerprint
|
|
87
88
|
} from "./context-engine.js";
|
|
89
|
+
import {
|
|
90
|
+
contextIndexExcludePatterns,
|
|
91
|
+
effectiveProtectedPaths
|
|
92
|
+
} from "./context-index-policy.js";
|
|
88
93
|
|
|
89
94
|
import type { ActionClassification } from "./guard-shell-analysis.ts";
|
|
90
95
|
|
|
@@ -99,7 +104,6 @@ import type {
|
|
|
99
104
|
ContextIndexNodeKind,
|
|
100
105
|
ContextIndexSettings,
|
|
101
106
|
ContextPreflight,
|
|
102
|
-
EffectiveProtectedPaths,
|
|
103
107
|
ExecPolicyConfig,
|
|
104
108
|
ExternalActionPolicyConfig,
|
|
105
109
|
FinalGateConfig,
|
|
@@ -259,6 +263,14 @@ type ChatImageAttachmentResult = {
|
|
|
259
263
|
skipped: Array<{ path: string; reason: string }>;
|
|
260
264
|
};
|
|
261
265
|
|
|
266
|
+
type ChatImageAccessPolicy = {
|
|
267
|
+
roots: ReturnType<typeof resolveDocumentRoots>;
|
|
268
|
+
readProtectedPaths: string[];
|
|
269
|
+
filesystemRead?: string[];
|
|
270
|
+
enforceFilesystemRead: boolean;
|
|
271
|
+
onImageInspected?: (path: string) => void;
|
|
272
|
+
};
|
|
273
|
+
|
|
262
274
|
const DEFAULT_MEMORY_SETTINGS: Required<MemorySettings> = {
|
|
263
275
|
enabled: true,
|
|
264
276
|
mode: "manual",
|
|
@@ -1921,34 +1933,6 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
1921
1933
|
return [...new Set(values.filter((value) => typeof value === "string" && value.trim().length > 0))];
|
|
1922
1934
|
}
|
|
1923
1935
|
|
|
1924
|
-
function effectiveProtectedPaths(policy: BasePolicy, profile: ProjectProfile): EffectiveProtectedPaths {
|
|
1925
|
-
const baseReadProtectedPaths = policy.shellProtectedPaths ?? policy.protectedPaths;
|
|
1926
|
-
const profileProtectedPaths = profile.protectedPaths ?? [];
|
|
1927
|
-
const profileShellProtectedPaths = profile.shellProtectedPaths ?? profile.protectedPaths ?? [];
|
|
1928
|
-
const readOnlyPaths = profile.readOnlyPaths ?? [];
|
|
1929
|
-
const contextIndexStatePath = resolveContextIndexSettings(profile).path;
|
|
1930
|
-
return {
|
|
1931
|
-
readProtectedPaths: uniqueStrings([
|
|
1932
|
-
...baseReadProtectedPaths,
|
|
1933
|
-
...profileProtectedPaths,
|
|
1934
|
-
contextIndexStatePath
|
|
1935
|
-
]),
|
|
1936
|
-
writeProtectedPaths: uniqueStrings([
|
|
1937
|
-
...policy.protectedPaths,
|
|
1938
|
-
...profileProtectedPaths,
|
|
1939
|
-
...readOnlyPaths,
|
|
1940
|
-
contextIndexStatePath
|
|
1941
|
-
]),
|
|
1942
|
-
shellProtectedPaths: uniqueStrings([
|
|
1943
|
-
...baseReadProtectedPaths,
|
|
1944
|
-
...profileShellProtectedPaths,
|
|
1945
|
-
...readOnlyPaths,
|
|
1946
|
-
contextIndexStatePath
|
|
1947
|
-
]),
|
|
1948
|
-
readOnlyPaths: uniqueStrings(readOnlyPaths)
|
|
1949
|
-
};
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
1936
|
function classifyExternalAction(toolName: string, input: Record<string, unknown>, policy: BasePolicy): {
|
|
1953
1937
|
decision: "not-external" | "safe-read" | "confirm";
|
|
1954
1938
|
provider?: string;
|
|
@@ -4009,29 +3993,55 @@ function escapeRegExp(input: string): string {
|
|
|
4009
3993
|
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4010
3994
|
}
|
|
4011
3995
|
|
|
4012
|
-
function supportedImageMimeType(
|
|
4013
|
-
if (
|
|
3996
|
+
function supportedImageMimeType(bytes: Buffer): string | undefined {
|
|
3997
|
+
if (
|
|
3998
|
+
bytes.length >= 24
|
|
3999
|
+
&& bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
|
|
4000
|
+
&& bytes.readUInt32BE(8) === 13
|
|
4001
|
+
&& bytes.subarray(12, 16).toString("ascii") === "IHDR"
|
|
4002
|
+
&& bytes.readUInt32BE(16) > 0
|
|
4003
|
+
&& bytes.readUInt32BE(20) > 0
|
|
4004
|
+
) {
|
|
4014
4005
|
return "image/png";
|
|
4015
4006
|
}
|
|
4016
|
-
if (
|
|
4007
|
+
if (
|
|
4008
|
+
bytes.length >= 4
|
|
4009
|
+
&& bytes[0] === 0xff
|
|
4010
|
+
&& bytes[1] === 0xd8
|
|
4011
|
+
&& bytes[2] === 0xff
|
|
4012
|
+
&& bytes[bytes.length - 2] === 0xff
|
|
4013
|
+
&& bytes[bytes.length - 1] === 0xd9
|
|
4014
|
+
) {
|
|
4017
4015
|
return "image/jpeg";
|
|
4018
4016
|
}
|
|
4019
|
-
if (bytes.length >=
|
|
4017
|
+
if (bytes.length >= 10) {
|
|
4020
4018
|
const head = bytes.subarray(0, 6).toString("ascii");
|
|
4021
|
-
if (
|
|
4019
|
+
if (
|
|
4020
|
+
(head === "GIF87a" || head === "GIF89a")
|
|
4021
|
+
&& bytes.readUInt16LE(6) > 0
|
|
4022
|
+
&& bytes.readUInt16LE(8) > 0
|
|
4023
|
+
) {
|
|
4024
|
+
return "image/gif";
|
|
4025
|
+
}
|
|
4022
4026
|
}
|
|
4023
|
-
if (
|
|
4027
|
+
if (
|
|
4028
|
+
bytes.length >= 16
|
|
4029
|
+
&& bytes.subarray(0, 4).toString("ascii") === "RIFF"
|
|
4030
|
+
&& bytes.subarray(8, 12).toString("ascii") === "WEBP"
|
|
4031
|
+
&& ["VP8 ", "VP8L", "VP8X"].includes(bytes.subarray(12, 16).toString("ascii"))
|
|
4032
|
+
&& bytes.readUInt32LE(4) + 8 <= bytes.length
|
|
4033
|
+
) {
|
|
4024
4034
|
return "image/webp";
|
|
4025
4035
|
}
|
|
4026
|
-
if (
|
|
4036
|
+
if (
|
|
4037
|
+
bytes.length >= 26
|
|
4038
|
+
&& bytes[0] === 0x42
|
|
4039
|
+
&& bytes[1] === 0x4d
|
|
4040
|
+
&& bytes.readUInt32LE(2) <= bytes.length
|
|
4041
|
+
&& bytes.readUInt32LE(14) >= 12
|
|
4042
|
+
) {
|
|
4027
4043
|
return "image/bmp";
|
|
4028
4044
|
}
|
|
4029
|
-
const ext = path.extname(filePath).toLowerCase().replace(/^\./, "");
|
|
4030
|
-
if (ext === "png") return "image/png";
|
|
4031
|
-
if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
|
|
4032
|
-
if (ext === "gif") return "image/gif";
|
|
4033
|
-
if (ext === "webp") return "image/webp";
|
|
4034
|
-
if (ext === "bmp") return "image/bmp";
|
|
4035
4045
|
return undefined;
|
|
4036
4046
|
}
|
|
4037
4047
|
|
|
@@ -4085,9 +4095,131 @@ function extractLocalImagePathCandidates(text: string, cwd: string): string[] {
|
|
|
4085
4095
|
return [...candidates];
|
|
4086
4096
|
}
|
|
4087
4097
|
|
|
4088
|
-
|
|
4098
|
+
type ResolvedChatImage =
|
|
4099
|
+
| { status: "ok"; absolutePath: string; bytes: Buffer; mimeType: string }
|
|
4100
|
+
| { status: "error"; reason: string };
|
|
4101
|
+
|
|
4102
|
+
function pathContainedBy(root: string, candidate: string): boolean {
|
|
4103
|
+
if (candidate === root) return true;
|
|
4104
|
+
const relative = path.relative(root, candidate);
|
|
4105
|
+
return relative.length > 0
|
|
4106
|
+
&& relative !== ".."
|
|
4107
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
4108
|
+
&& !path.isAbsolute(relative);
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
export function readChatImage(
|
|
4112
|
+
requestedPath: string,
|
|
4113
|
+
cwd: string,
|
|
4114
|
+
access: ChatImageAccessPolicy
|
|
4115
|
+
): ResolvedChatImage {
|
|
4116
|
+
const lexicalRequestedPath = path.isAbsolute(requestedPath)
|
|
4117
|
+
? path.normalize(requestedPath)
|
|
4118
|
+
: path.resolve(cwd, requestedPath);
|
|
4119
|
+
const lexicalProjectRoot = path.resolve(cwd);
|
|
4120
|
+
const requestedProjectLexicalRelative = pathContainedBy(lexicalProjectRoot, lexicalRequestedPath)
|
|
4121
|
+
? path.relative(lexicalProjectRoot, lexicalRequestedPath).split(path.sep).join("/") || "."
|
|
4122
|
+
: undefined;
|
|
4123
|
+
let canonicalPath: string;
|
|
4124
|
+
let inspected: fs.Stats;
|
|
4125
|
+
try {
|
|
4126
|
+
canonicalPath = fs.realpathSync.native(requestedPath);
|
|
4127
|
+
inspected = fs.statSync(canonicalPath);
|
|
4128
|
+
} catch {
|
|
4129
|
+
return { status: "error", reason: "file does not exist or cannot be inspected" };
|
|
4130
|
+
}
|
|
4131
|
+
if (!inspected.isFile()) return { status: "error", reason: "not a regular file" };
|
|
4132
|
+
if (inspected.size <= 0) return { status: "error", reason: "empty file" };
|
|
4133
|
+
if (inspected.size > MAX_CHAT_IMAGE_BYTES) {
|
|
4134
|
+
return {
|
|
4135
|
+
status: "error",
|
|
4136
|
+
reason: `image is ${formatCount(inspected.size)} bytes > ${formatCount(MAX_CHAT_IMAGE_BYTES)} byte limit`
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
const root = access.roots.find((candidate) => pathContainedBy(candidate.path, canonicalPath));
|
|
4141
|
+
if (!root) return { status: "error", reason: "outside the project and every granted additionalReadRoots directory" };
|
|
4142
|
+
|
|
4143
|
+
const rootRelative = path.relative(root.path, canonicalPath).split(path.sep).join("/") || ".";
|
|
4144
|
+
const projectRoot = access.roots.find((candidate) => candidate.source === "project");
|
|
4145
|
+
let canonicalRequestedLocation = requestedPath;
|
|
4146
|
+
try {
|
|
4147
|
+
canonicalRequestedLocation = path.join(
|
|
4148
|
+
fs.realpathSync.native(path.dirname(requestedPath)),
|
|
4149
|
+
path.basename(requestedPath)
|
|
4150
|
+
);
|
|
4151
|
+
} catch {
|
|
4152
|
+
// The full target already resolved. This location form only preserves the
|
|
4153
|
+
// final symlink's policy position, so failure falls back to the raw path.
|
|
4154
|
+
}
|
|
4155
|
+
const requestedProjectRelative = projectRoot && pathContainedBy(projectRoot.path, canonicalRequestedLocation)
|
|
4156
|
+
? path.relative(projectRoot.path, canonicalRequestedLocation).split(path.sep).join("/") || "."
|
|
4157
|
+
: undefined;
|
|
4158
|
+
const protectedMatch = matchesProtectedPath(canonicalPath, access.readProtectedPaths)
|
|
4159
|
+
?? matchesProtectedPath(rootRelative, access.readProtectedPaths)
|
|
4160
|
+
?? matchesProtectedPath(canonicalRequestedLocation, access.readProtectedPaths)
|
|
4161
|
+
?? matchesProtectedPath(requestedPath, access.readProtectedPaths)
|
|
4162
|
+
?? (requestedProjectLexicalRelative
|
|
4163
|
+
? matchesProtectedPath(requestedProjectLexicalRelative, access.readProtectedPaths)
|
|
4164
|
+
: undefined)
|
|
4165
|
+
?? (requestedProjectRelative ? matchesProtectedPath(requestedProjectRelative, access.readProtectedPaths) : undefined);
|
|
4166
|
+
if (protectedMatch) return { status: "error", reason: `matches protected path ${protectedMatch}` };
|
|
4167
|
+
|
|
4168
|
+
if (
|
|
4169
|
+
root.source === "project"
|
|
4170
|
+
&& access.enforceFilesystemRead
|
|
4171
|
+
&& access.filesystemRead
|
|
4172
|
+
&& !matchesAnyPath(rootRelative, access.filesystemRead)
|
|
4173
|
+
) {
|
|
4174
|
+
return {
|
|
4175
|
+
status: "error",
|
|
4176
|
+
reason: `outside the resolved filesystem read scope (${access.filesystemRead.join(", ")})`
|
|
4177
|
+
};
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
access.onImageInspected?.(canonicalPath);
|
|
4181
|
+
const openFlags = fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | fs.constants.O_NOFOLLOW;
|
|
4182
|
+
let fd: number;
|
|
4183
|
+
try {
|
|
4184
|
+
fd = fs.openSync(canonicalPath, openFlags);
|
|
4185
|
+
} catch {
|
|
4186
|
+
return { status: "error", reason: "file changed before it could be opened" };
|
|
4187
|
+
}
|
|
4188
|
+
try {
|
|
4189
|
+
const opened = fs.fstatSync(fd);
|
|
4190
|
+
if (
|
|
4191
|
+
!opened.isFile()
|
|
4192
|
+
|| opened.dev !== inspected.dev
|
|
4193
|
+
|| opened.ino !== inspected.ino
|
|
4194
|
+
|| opened.size !== inspected.size
|
|
4195
|
+
) {
|
|
4196
|
+
return { status: "error", reason: "file changed between the safety checks and the read" };
|
|
4197
|
+
}
|
|
4198
|
+
const bytes = Buffer.alloc(opened.size);
|
|
4199
|
+
let offset = 0;
|
|
4200
|
+
while (offset < bytes.length) {
|
|
4201
|
+
const read = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
4202
|
+
if (read <= 0) break;
|
|
4203
|
+
offset += read;
|
|
4204
|
+
}
|
|
4205
|
+
if (offset !== bytes.length) return { status: "error", reason: "file changed while it was being read" };
|
|
4206
|
+
const mimeType = supportedImageMimeType(bytes);
|
|
4207
|
+
if (!mimeType) return { status: "error", reason: "file bytes are not a supported image" };
|
|
4208
|
+
return { status: "ok", absolutePath: canonicalPath, bytes, mimeType };
|
|
4209
|
+
} finally {
|
|
4210
|
+
fs.closeSync(fd);
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
function attachLocalImagesFromText(
|
|
4215
|
+
text: string,
|
|
4216
|
+
existingImages: unknown[] | undefined,
|
|
4217
|
+
cwd: string,
|
|
4218
|
+
resolveAccess: () => ChatImageAccessPolicy
|
|
4219
|
+
): ChatImageAttachmentResult | undefined {
|
|
4089
4220
|
const imagePaths = extractLocalImagePathCandidates(text, cwd);
|
|
4090
4221
|
if (imagePaths.length === 0) return undefined;
|
|
4222
|
+
const access = resolveAccess();
|
|
4091
4223
|
|
|
4092
4224
|
const existing = Array.isArray(existingImages) ? existingImages : [];
|
|
4093
4225
|
const images: ChatImageAttachmentResult["images"] = [];
|
|
@@ -4101,28 +4233,14 @@ function attachLocalImagesFromText(text: string, existingImages: unknown[] | und
|
|
|
4101
4233
|
continue;
|
|
4102
4234
|
}
|
|
4103
4235
|
try {
|
|
4104
|
-
const
|
|
4105
|
-
if (
|
|
4106
|
-
skipped.push({ path: imagePath, reason:
|
|
4107
|
-
continue;
|
|
4108
|
-
}
|
|
4109
|
-
if (stat.size <= 0) {
|
|
4110
|
-
skipped.push({ path: imagePath, reason: "empty file" });
|
|
4111
|
-
continue;
|
|
4112
|
-
}
|
|
4113
|
-
if (stat.size > MAX_CHAT_IMAGE_BYTES) {
|
|
4114
|
-
skipped.push({ path: imagePath, reason: `image is ${formatCount(stat.size)} bytes > ${formatCount(MAX_CHAT_IMAGE_BYTES)} byte limit; use read on the file so Pi can resize it` });
|
|
4115
|
-
continue;
|
|
4116
|
-
}
|
|
4117
|
-
const bytes = fs.readFileSync(imagePath);
|
|
4118
|
-
const mimeType = supportedImageMimeType(imagePath, bytes);
|
|
4119
|
-
if (!mimeType) {
|
|
4120
|
-
skipped.push({ path: imagePath, reason: "unsupported image type" });
|
|
4236
|
+
const resolved = readChatImage(imagePath, cwd, access);
|
|
4237
|
+
if (resolved.status === "error") {
|
|
4238
|
+
skipped.push({ path: imagePath, reason: resolved.reason });
|
|
4121
4239
|
continue;
|
|
4122
4240
|
}
|
|
4123
4241
|
const marker = `[image${existing.length + images.length + 1}]`;
|
|
4124
|
-
images.push({ type: "image", mimeType, data: bytes.toString("base64") });
|
|
4125
|
-
attached.push({ marker, path: imagePath, mimeType, bytes:
|
|
4242
|
+
images.push({ type: "image", mimeType: resolved.mimeType, data: resolved.bytes.toString("base64") });
|
|
4243
|
+
attached.push({ marker, path: imagePath, mimeType: resolved.mimeType, bytes: resolved.bytes.length });
|
|
4126
4244
|
nextText = nextText.replace(new RegExp(escapeRegExp(imagePath), "g"), marker);
|
|
4127
4245
|
nextText = nextText.replace(new RegExp(escapeRegExp(`file://${imagePath}`), "g"), marker);
|
|
4128
4246
|
} catch (error) {
|
|
@@ -4480,7 +4598,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4480
4598
|
if (updateNotice) ctx.ui.notify(updateNotice, "info");
|
|
4481
4599
|
let engineStatus: unknown;
|
|
4482
4600
|
try {
|
|
4483
|
-
engineStatus = await contextIndexV2Status(ctx.cwd
|
|
4601
|
+
engineStatus = await contextIndexV2Status(ctx.cwd, {
|
|
4602
|
+
excludePatterns: contextIndexExcludePatterns(policy, profile)
|
|
4603
|
+
});
|
|
4484
4604
|
} catch (error) {
|
|
4485
4605
|
engineStatus = { exists: false, error: error instanceof Error ? error.message : String(error) };
|
|
4486
4606
|
}
|
|
@@ -4506,7 +4626,22 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4506
4626
|
termCount: taskSignal.terms.length
|
|
4507
4627
|
});
|
|
4508
4628
|
|
|
4509
|
-
const imageAttachment = attachLocalImagesFromText(text, event.images, ctx.cwd)
|
|
4629
|
+
const imageAttachment = attachLocalImagesFromText(text, event.images, ctx.cwd, () => {
|
|
4630
|
+
const projectTrusted = ctx.isProjectTrusted();
|
|
4631
|
+
const profile = loadProfileFromContext(ctx);
|
|
4632
|
+
const permissionProfile = resolvePermissionProfile(profile, policy, permissionOverrideFromContext(ctx));
|
|
4633
|
+
const capabilityState = verifyProjectCapabilityState(extensionDir, ctx.cwd, projectTrusted);
|
|
4634
|
+
return {
|
|
4635
|
+
roots: resolveDocumentRoots({
|
|
4636
|
+
cwd: ctx.cwd,
|
|
4637
|
+
profileRoots: profile.additionalReadRoots,
|
|
4638
|
+
environmentRoots: process.env.PIAGENT_ADDITIONAL_READ_ROOTS
|
|
4639
|
+
}),
|
|
4640
|
+
readProtectedPaths: effectiveProtectedPaths(policy, profile).readProtectedPaths,
|
|
4641
|
+
filesystemRead: capabilityState.filesystemRead,
|
|
4642
|
+
enforceFilesystemRead: permissionProfile.mode !== "trusted-full-access"
|
|
4643
|
+
};
|
|
4644
|
+
});
|
|
4510
4645
|
if (imageAttachment?.attached.length) {
|
|
4511
4646
|
ctx.ui.notify(`Piagent image input: attached ${imageAttachment.attached.map((item) => item.marker).join(", ")}`, "info");
|
|
4512
4647
|
} else if (imageAttachment?.skipped.length) {
|
|
@@ -4590,7 +4725,12 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4590
4725
|
autoPackedPrompts.add(signal.promptHash);
|
|
4591
4726
|
if (autoPackedPrompts.size > 50) autoPackedPrompts.delete(autoPackedPrompts.values().next().value as string);
|
|
4592
4727
|
try {
|
|
4593
|
-
const
|
|
4728
|
+
const excludePatterns = contextIndexExcludePatterns(policy, loadProfileFromContext(ctx));
|
|
4729
|
+
const ensured = await ensureContextIndexV2(ctx.cwd, {
|
|
4730
|
+
excludePatterns,
|
|
4731
|
+
rebuildMissing: false
|
|
4732
|
+
});
|
|
4733
|
+
const status = ensured.status;
|
|
4594
4734
|
if (!status.exists || status.stale) {
|
|
4595
4735
|
telemetry(ctx, {
|
|
4596
4736
|
event: "context_pack",
|
|
@@ -4603,7 +4743,12 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4603
4743
|
return;
|
|
4604
4744
|
}
|
|
4605
4745
|
const budgetTokens = signal.lane === "high-risk" ? 1_200 : signal.lane === "tiny" ? 500 : 900;
|
|
4606
|
-
const pack = await buildContextPack(ctx.cwd, query, {
|
|
4746
|
+
const pack = await buildContextPack(ctx.cwd, query, {
|
|
4747
|
+
budgetTokens,
|
|
4748
|
+
includeCode: false,
|
|
4749
|
+
limit: 12,
|
|
4750
|
+
excludePatterns
|
|
4751
|
+
});
|
|
4607
4752
|
telemetry(ctx, {
|
|
4608
4753
|
event: "context_pack",
|
|
4609
4754
|
queryHash: pack.queryHash,
|
|
@@ -5029,17 +5174,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5029
5174
|
}),
|
|
5030
5175
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
5031
5176
|
const profile = loadProfileFromContext(ctx);
|
|
5032
|
-
const
|
|
5033
|
-
const excludePatterns = Array.from(new Set([
|
|
5034
|
-
...pathPolicy.readProtectedPaths,
|
|
5035
|
-
...pathPolicy.writeProtectedPaths,
|
|
5036
|
-
".pi/context-index.json",
|
|
5037
|
-
".pi/piagent-state/**"
|
|
5038
|
-
]));
|
|
5177
|
+
const excludePatterns = contextIndexExcludePatterns(policy, profile);
|
|
5039
5178
|
let result: unknown;
|
|
5040
5179
|
let text: string;
|
|
5041
5180
|
if (params.action === "status") {
|
|
5042
|
-
result = await contextIndexV2Status(ctx.cwd);
|
|
5181
|
+
result = await contextIndexV2Status(ctx.cwd, { excludePatterns });
|
|
5043
5182
|
const status = result as Awaited<ReturnType<typeof contextIndexV2Status>>;
|
|
5044
5183
|
text = [
|
|
5045
5184
|
`indexV2: ${status.exists ? "ready" : "missing"}`,
|
|
@@ -5062,12 +5201,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5062
5201
|
].join("\n");
|
|
5063
5202
|
} else if (params.action === "search") {
|
|
5064
5203
|
if (!params.query?.trim()) throw new Error("query is required for context search");
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
}
|
|
5070
|
-
|
|
5204
|
+
const ensured = await ensureContextIndexV2(ctx.cwd, {
|
|
5205
|
+
excludePatterns,
|
|
5206
|
+
refresh: params.refresh,
|
|
5207
|
+
rebuildMissing: true
|
|
5208
|
+
});
|
|
5209
|
+
const status = ensured.status;
|
|
5210
|
+
result = await searchContextIndexV2(ctx.cwd, params.query, { limit: 15, excludePatterns });
|
|
5071
5211
|
const search = result as Awaited<ReturnType<typeof searchContextIndexV2>>;
|
|
5072
5212
|
text = [
|
|
5073
5213
|
`confidence: ${search.confidence}`,
|
|
@@ -5076,20 +5216,23 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5076
5216
|
].join("\n");
|
|
5077
5217
|
} else if (params.action === "pack") {
|
|
5078
5218
|
if (!params.query?.trim()) throw new Error("query is required for a context pack");
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
}
|
|
5219
|
+
const ensured = await ensureContextIndexV2(ctx.cwd, {
|
|
5220
|
+
excludePatterns,
|
|
5221
|
+
refresh: params.refresh,
|
|
5222
|
+
rebuildMissing: true
|
|
5223
|
+
});
|
|
5224
|
+
const status = ensured.status;
|
|
5084
5225
|
const pack = await buildContextPack(ctx.cwd, params.query, {
|
|
5085
5226
|
budgetTokens: params.budgetTokens ?? 6_000,
|
|
5086
5227
|
includeCode: true,
|
|
5087
|
-
limit: 18
|
|
5228
|
+
limit: 18,
|
|
5229
|
+
excludePatterns
|
|
5088
5230
|
});
|
|
5089
5231
|
result = { ...pack, text: undefined, status };
|
|
5090
5232
|
text = pack.text;
|
|
5091
5233
|
} else if (params.action === "impact") {
|
|
5092
|
-
|
|
5234
|
+
await ensureContextIndexV2(ctx.cwd, { excludePatterns, rebuildMissing: false });
|
|
5235
|
+
result = await buildTestImpact(ctx.cwd, params.files ?? [], { excludePatterns });
|
|
5093
5236
|
const impact = result as Awaited<ReturnType<typeof buildTestImpact>>;
|
|
5094
5237
|
text = [
|
|
5095
5238
|
`changed: ${impact.changedFiles.join(", ") || "none"}`,
|
|
@@ -5139,7 +5282,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
5139
5282
|
const contextIndex = buildContextIndexStatus(ctx.cwd, profile);
|
|
5140
5283
|
let contextEngine: Awaited<ReturnType<typeof contextIndexV2Status>> | { exists: false; warnings: string[] };
|
|
5141
5284
|
try {
|
|
5142
|
-
contextEngine = await contextIndexV2Status(ctx.cwd
|
|
5285
|
+
contextEngine = await contextIndexV2Status(ctx.cwd, {
|
|
5286
|
+
excludePatterns: contextIndexExcludePatterns(policy, profile)
|
|
5287
|
+
});
|
|
5143
5288
|
} catch (error) {
|
|
5144
5289
|
contextEngine = { exists: false, warnings: [error instanceof Error ? error.message : String(error)] };
|
|
5145
5290
|
}
|
|
@@ -7232,7 +7377,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7232
7377
|
|
|
7233
7378
|
async function emitContextEngineStatus(ctx: ExtensionContext): Promise<void> {
|
|
7234
7379
|
try {
|
|
7235
|
-
const status = await contextIndexV2Status(ctx.cwd
|
|
7380
|
+
const status = await contextIndexV2Status(ctx.cwd, {
|
|
7381
|
+
excludePatterns: contextIndexExcludePatterns(policy, loadProfileFromContext(ctx))
|
|
7382
|
+
});
|
|
7236
7383
|
emitRuntimeMessage(ctx, "piagent-context-engine-status", [
|
|
7237
7384
|
`contextEngine: ${status.exists ? "ready" : "missing"}`,
|
|
7238
7385
|
`path: ${status.path}`,
|
|
@@ -7251,15 +7398,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7251
7398
|
|
|
7252
7399
|
async function rebuildContextEngine(ctx: ExtensionContext): Promise<void> {
|
|
7253
7400
|
const profile = loadProfileFromContext(ctx);
|
|
7254
|
-
const
|
|
7401
|
+
const excludePatterns = contextIndexExcludePatterns(policy, profile);
|
|
7255
7402
|
try {
|
|
7256
7403
|
const result = await buildContextIndexV2(ctx.cwd, {
|
|
7257
|
-
excludePatterns
|
|
7258
|
-
...pathPolicy.readProtectedPaths,
|
|
7259
|
-
...pathPolicy.writeProtectedPaths,
|
|
7260
|
-
".pi/context-index.json",
|
|
7261
|
-
".pi/piagent-state/**"
|
|
7262
|
-
]))
|
|
7404
|
+
excludePatterns
|
|
7263
7405
|
});
|
|
7264
7406
|
telemetry(ctx, { event: "context_engine_action", action: "rebuild", ...result });
|
|
7265
7407
|
emitRuntimeMessage(ctx, "piagent-context-engine-rebuild", [
|
|
@@ -7275,9 +7417,13 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7275
7417
|
}
|
|
7276
7418
|
|
|
7277
7419
|
async function emitContextEngineSearch(ctx: ExtensionContext, query: string): Promise<boolean> {
|
|
7278
|
-
const
|
|
7420
|
+
const excludePatterns = contextIndexExcludePatterns(policy, loadProfileFromContext(ctx));
|
|
7421
|
+
const { status } = await ensureContextIndexV2(ctx.cwd, {
|
|
7422
|
+
excludePatterns,
|
|
7423
|
+
rebuildMissing: false
|
|
7424
|
+
});
|
|
7279
7425
|
if (!status.exists || !query.trim()) return false;
|
|
7280
|
-
const search = await searchContextIndexV2(ctx.cwd, query, { limit: 12 });
|
|
7426
|
+
const search = await searchContextIndexV2(ctx.cwd, query, { limit: 12, excludePatterns });
|
|
7281
7427
|
emitRuntimeMessage(ctx, "piagent-context-engine-search", [
|
|
7282
7428
|
`confidence: ${search.confidence}`,
|
|
7283
7429
|
`stale: ${status.stale ? "yes" : "no"}`,
|
|
@@ -7291,12 +7437,21 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7291
7437
|
emitRuntimeMessage(ctx, "piagent-context-pack-help", "Usage: /context pack <task or symbol>");
|
|
7292
7438
|
return;
|
|
7293
7439
|
}
|
|
7294
|
-
const
|
|
7440
|
+
const excludePatterns = contextIndexExcludePatterns(policy, loadProfileFromContext(ctx));
|
|
7441
|
+
const { status } = await ensureContextIndexV2(ctx.cwd, {
|
|
7442
|
+
excludePatterns,
|
|
7443
|
+
rebuildMissing: false
|
|
7444
|
+
});
|
|
7295
7445
|
if (!status.exists) {
|
|
7296
7446
|
emitRuntimeMessage(ctx, "piagent-context-pack-help", "Context Engine index is missing. Run /context rebuild first.");
|
|
7297
7447
|
return;
|
|
7298
7448
|
}
|
|
7299
|
-
const pack = await buildContextPack(ctx.cwd, query, {
|
|
7449
|
+
const pack = await buildContextPack(ctx.cwd, query, {
|
|
7450
|
+
budgetTokens: 1_500,
|
|
7451
|
+
includeCode: false,
|
|
7452
|
+
limit: 15,
|
|
7453
|
+
excludePatterns
|
|
7454
|
+
});
|
|
7300
7455
|
telemetry(ctx, {
|
|
7301
7456
|
event: "context_pack",
|
|
7302
7457
|
queryHash: pack.queryHash,
|
|
@@ -7318,7 +7473,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
7318
7473
|
|
|
7319
7474
|
async function emitTestImpact(ctx: ExtensionContext, raw: string): Promise<void> {
|
|
7320
7475
|
const files = raw.split(/\s+/).map((file) => file.trim()).filter(Boolean);
|
|
7321
|
-
const
|
|
7476
|
+
const excludePatterns = contextIndexExcludePatterns(policy, loadProfileFromContext(ctx));
|
|
7477
|
+
await ensureContextIndexV2(ctx.cwd, { excludePatterns, rebuildMissing: false });
|
|
7478
|
+
const impact = await buildTestImpact(ctx.cwd, files, { excludePatterns });
|
|
7322
7479
|
emitRuntimeMessage(ctx, "piagent-context-impact", [
|
|
7323
7480
|
`changed: ${impact.changedFiles.join(", ") || "none"}`,
|
|
7324
7481
|
`impacted: ${impact.impactedFiles.map((item) => `${item.path} via ${item.via}`).join(", ") || "none"}`,
|