@piagent/platform 1.2.9 → 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 +42 -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 +23 -4
- 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 +1 -0
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +7 -2
- 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/extensions/context-engine.js +134 -69
- package/packages/piagent-core/extensions/context-index-policy.js +1 -1
- package/packages/piagent-core/extensions/piagent-guard.ts +191 -34
- 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
|
@@ -263,6 +263,14 @@ type ChatImageAttachmentResult = {
|
|
|
263
263
|
skipped: Array<{ path: string; reason: string }>;
|
|
264
264
|
};
|
|
265
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
|
+
|
|
266
274
|
const DEFAULT_MEMORY_SETTINGS: Required<MemorySettings> = {
|
|
267
275
|
enabled: true,
|
|
268
276
|
mode: "manual",
|
|
@@ -3985,29 +3993,55 @@ function escapeRegExp(input: string): string {
|
|
|
3985
3993
|
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3986
3994
|
}
|
|
3987
3995
|
|
|
3988
|
-
function supportedImageMimeType(
|
|
3989
|
-
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
|
+
) {
|
|
3990
4005
|
return "image/png";
|
|
3991
4006
|
}
|
|
3992
|
-
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
|
+
) {
|
|
3993
4015
|
return "image/jpeg";
|
|
3994
4016
|
}
|
|
3995
|
-
if (bytes.length >=
|
|
4017
|
+
if (bytes.length >= 10) {
|
|
3996
4018
|
const head = bytes.subarray(0, 6).toString("ascii");
|
|
3997
|
-
if (
|
|
4019
|
+
if (
|
|
4020
|
+
(head === "GIF87a" || head === "GIF89a")
|
|
4021
|
+
&& bytes.readUInt16LE(6) > 0
|
|
4022
|
+
&& bytes.readUInt16LE(8) > 0
|
|
4023
|
+
) {
|
|
4024
|
+
return "image/gif";
|
|
4025
|
+
}
|
|
3998
4026
|
}
|
|
3999
|
-
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
|
+
) {
|
|
4000
4034
|
return "image/webp";
|
|
4001
4035
|
}
|
|
4002
|
-
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
|
+
) {
|
|
4003
4043
|
return "image/bmp";
|
|
4004
4044
|
}
|
|
4005
|
-
const ext = path.extname(filePath).toLowerCase().replace(/^\./, "");
|
|
4006
|
-
if (ext === "png") return "image/png";
|
|
4007
|
-
if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
|
|
4008
|
-
if (ext === "gif") return "image/gif";
|
|
4009
|
-
if (ext === "webp") return "image/webp";
|
|
4010
|
-
if (ext === "bmp") return "image/bmp";
|
|
4011
4045
|
return undefined;
|
|
4012
4046
|
}
|
|
4013
4047
|
|
|
@@ -4061,9 +4095,131 @@ function extractLocalImagePathCandidates(text: string, cwd: string): string[] {
|
|
|
4061
4095
|
return [...candidates];
|
|
4062
4096
|
}
|
|
4063
4097
|
|
|
4064
|
-
|
|
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 {
|
|
4065
4220
|
const imagePaths = extractLocalImagePathCandidates(text, cwd);
|
|
4066
4221
|
if (imagePaths.length === 0) return undefined;
|
|
4222
|
+
const access = resolveAccess();
|
|
4067
4223
|
|
|
4068
4224
|
const existing = Array.isArray(existingImages) ? existingImages : [];
|
|
4069
4225
|
const images: ChatImageAttachmentResult["images"] = [];
|
|
@@ -4077,28 +4233,14 @@ function attachLocalImagesFromText(text: string, existingImages: unknown[] | und
|
|
|
4077
4233
|
continue;
|
|
4078
4234
|
}
|
|
4079
4235
|
try {
|
|
4080
|
-
const
|
|
4081
|
-
if (
|
|
4082
|
-
skipped.push({ path: imagePath, reason:
|
|
4083
|
-
continue;
|
|
4084
|
-
}
|
|
4085
|
-
if (stat.size <= 0) {
|
|
4086
|
-
skipped.push({ path: imagePath, reason: "empty file" });
|
|
4087
|
-
continue;
|
|
4088
|
-
}
|
|
4089
|
-
if (stat.size > MAX_CHAT_IMAGE_BYTES) {
|
|
4090
|
-
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` });
|
|
4091
|
-
continue;
|
|
4092
|
-
}
|
|
4093
|
-
const bytes = fs.readFileSync(imagePath);
|
|
4094
|
-
const mimeType = supportedImageMimeType(imagePath, bytes);
|
|
4095
|
-
if (!mimeType) {
|
|
4096
|
-
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 });
|
|
4097
4239
|
continue;
|
|
4098
4240
|
}
|
|
4099
4241
|
const marker = `[image${existing.length + images.length + 1}]`;
|
|
4100
|
-
images.push({ type: "image", mimeType, data: bytes.toString("base64") });
|
|
4101
|
-
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 });
|
|
4102
4244
|
nextText = nextText.replace(new RegExp(escapeRegExp(imagePath), "g"), marker);
|
|
4103
4245
|
nextText = nextText.replace(new RegExp(escapeRegExp(`file://${imagePath}`), "g"), marker);
|
|
4104
4246
|
} catch (error) {
|
|
@@ -4484,7 +4626,22 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4484
4626
|
termCount: taskSignal.terms.length
|
|
4485
4627
|
});
|
|
4486
4628
|
|
|
4487
|
-
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
|
+
});
|
|
4488
4645
|
if (imageAttachment?.attached.length) {
|
|
4489
4646
|
ctx.ui.notify(`Piagent image input: attached ${imageAttachment.attached.map((item) => item.marker).join(", ")}`, "info");
|
|
4490
4647
|
} else if (imageAttachment?.skipped.length) {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
importedServers,
|
|
9
9
|
unreadableImportTargets
|
|
10
10
|
} from "./mcp-import-kinds.js";
|
|
11
|
-
import { erasesToolOrigin } from "./mcp-tool-naming.js";
|
|
11
|
+
import { directToolsEnabled, erasesToolOrigin } from "./mcp-tool-naming.js";
|
|
12
12
|
|
|
13
13
|
// Four files can define MCP servers, and which one a change belongs in is the
|
|
14
14
|
// first thing anyone gets wrong. The scope names are the vocabulary the CLI
|
|
@@ -253,13 +253,29 @@ export function mergedServerEntry(options) {
|
|
|
253
253
|
} catch {
|
|
254
254
|
continue;
|
|
255
255
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
let layerEntry = {};
|
|
257
|
+
let layerSeen = false;
|
|
258
|
+
// The adapter expands imports inside each layer before applying that
|
|
259
|
+
// layer's direct server map. First imported definition wins; the direct
|
|
260
|
+
// entry then overrides it field by field.
|
|
261
|
+
for (const kind of config.imports) {
|
|
262
|
+
const imported = importedServers(kind, options).find((server) => server.name === options.name);
|
|
263
|
+
if (!imported) continue;
|
|
264
|
+
layerEntry = { ...imported.entry };
|
|
265
|
+
layerSeen = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
const directEntry = config.mcpServers[options.name];
|
|
269
|
+
if (isRecord(directEntry)) {
|
|
270
|
+
layerEntry = { ...layerEntry, ...directEntry };
|
|
271
|
+
layerSeen = true;
|
|
272
|
+
}
|
|
273
|
+
if (!layerSeen) continue;
|
|
274
|
+
merged = { ...merged, ...layerEntry };
|
|
259
275
|
seen = true;
|
|
260
276
|
}
|
|
261
|
-
// A server
|
|
262
|
-
//
|
|
277
|
+
// A server supplied directly by a caller outside the configured layers keeps
|
|
278
|
+
// its own definition as the fallback.
|
|
263
279
|
return seen ? merged : { ...(options.entry ?? {}) };
|
|
264
280
|
}
|
|
265
281
|
|
|
@@ -430,6 +446,36 @@ export function unverifiableMcpConfig(options = {}) {
|
|
|
430
446
|
});
|
|
431
447
|
}
|
|
432
448
|
|
|
449
|
+
if (merged.settings.toolPrefix === "none" && !erasesToolOrigin(merged.settings)) {
|
|
450
|
+
const prefixScope = merged.contributors.toolPrefix;
|
|
451
|
+
const prefixFile = prefixScope
|
|
452
|
+
? configPathForScope(prefixScope, options)
|
|
453
|
+
: configPathForScope("global", options);
|
|
454
|
+
const affected = new Map();
|
|
455
|
+
for (const server of collectServers(options)) {
|
|
456
|
+
if (!server.requiresApproval || affected.has(server.name)) continue;
|
|
457
|
+
const effective = mergedServerEntry({
|
|
458
|
+
...options,
|
|
459
|
+
name: server.name,
|
|
460
|
+
entry: server.entry
|
|
461
|
+
});
|
|
462
|
+
if (!directToolsEnabled(effective.directTools)) continue;
|
|
463
|
+
affected.set(server.name, server.file);
|
|
464
|
+
}
|
|
465
|
+
if (affected.size > 0) {
|
|
466
|
+
const serverFiles = [...new Set(affected.values())];
|
|
467
|
+
const names = [...affected.keys()].sort();
|
|
468
|
+
problems.push({
|
|
469
|
+
scope: "repository direct tools",
|
|
470
|
+
file: serverFiles[0] ?? prefixFile,
|
|
471
|
+
detail: `${serverFiles.join(" and ")} ${serverFiles.length === 1 ? "enables" : "enable"} effective directTools ` +
|
|
472
|
+
`for repository MCP server(s) ${names.join(", ")} ` +
|
|
473
|
+
`while ${prefixFile} sets toolPrefix "none". This strips the server name off those tools and leaves ` +
|
|
474
|
+
"nothing to check an approval against. Disable directTools for those servers or use a server-bearing tool prefix."
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
433
479
|
for (const scope of REPOSITORY_SCOPES) {
|
|
434
480
|
const file = configPathForScope(scope, options);
|
|
435
481
|
let config;
|
|
@@ -77,6 +77,17 @@ export function attributeDirectTool(toolName, serverNames) {
|
|
|
77
77
|
return best;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Whether a global or per-server direct-tool selection registers at least one
|
|
82
|
+
* direct tool. The adapter accepts `true` or a non-empty list on server entries.
|
|
83
|
+
* @param {unknown} value
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function directToolsEnabled(value) {
|
|
87
|
+
return value === true
|
|
88
|
+
|| (Array.isArray(value) && value.some((item) => typeof item === "string" && item.trim().length > 0));
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
/**
|
|
81
92
|
* Whether a config's settings put tools on the model under names that cannot be
|
|
82
93
|
* traced to a server. Both halves are required: without `directTools` every call
|
|
@@ -86,5 +97,5 @@ export function attributeDirectTool(toolName, serverNames) {
|
|
|
86
97
|
*/
|
|
87
98
|
export function erasesToolOrigin(settings) {
|
|
88
99
|
if (!settings || typeof settings !== "object") return false;
|
|
89
|
-
return settings.directTools
|
|
100
|
+
return directToolsEnabled(settings.directTools) && settings.toolPrefix === "none";
|
|
90
101
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: piagent-reviewer
|
|
3
3
|
description: Piagent reviewer for implementation diff, policy, tests, and scope drift
|
|
4
|
-
tools: read, grep, find, ls
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
5
|
thinking: high
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: true
|
|
@@ -16,7 +16,7 @@ maxSubagentDepth: 0
|
|
|
16
16
|
|
|
17
17
|
You are `piagent-reviewer`, a disciplined review subagent for Pi Agent Platform projects.
|
|
18
18
|
|
|
19
|
-
Your job is to review evidence, not to invent issues.
|
|
19
|
+
Your job is to review evidence, not to invent issues. Never edit files or run mutation commands. Report actionable findings so the parent can assign fixes to an explicit writer.
|
|
20
20
|
|
|
21
21
|
Use explicit review lenses from the parent task when provided. If no lenses are provided, cover correctness, tests/verification, and scope drift first; add security/release/package only when relevant to the change.
|
|
22
22
|
|
|
@@ -33,7 +33,7 @@ Rules:
|
|
|
33
33
|
- Do not run destructive commands.
|
|
34
34
|
- Treat Field Guide/memory as advisory, not as proof.
|
|
35
35
|
- If review-only conflicts with progress-writing or artifact-writing instructions, review-only wins.
|
|
36
|
-
-
|
|
36
|
+
- Put blockers that need a decision in the final review so the parent can route them.
|
|
37
37
|
- Do not spawn other subagents.
|
|
38
38
|
|
|
39
39
|
Final output:
|