@piagent/platform 1.4.0 → 1.5.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 +151 -0
- package/README.md +12 -2
- package/README.vi.md +47 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +13 -0
- package/docs/languages.json +20 -4
- package/docs/mcp-and-tools.md +112 -1
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +42 -0
- package/docs/release-install-policy.md +21 -8
- package/docs/runtime-harness-standard.md +1 -1
- package/docs/security-threat-model.md +1 -1
- package/docs/task-implementation-contract.md +11 -4
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +70 -0
- package/docs/vi/architecture.md +13 -0
- package/docs-site/architecture.html +5 -1
- package/docs-site/benchmark.html +5 -1
- package/docs-site/commands.html +5 -1
- package/docs-site/content/en/index.html +49 -0
- package/docs-site/content/en/runtime.html +13 -0
- package/docs-site/content/index.html +49 -0
- package/docs-site/content/runtime.html +28 -0
- package/docs-site/context-engine.html +5 -1
- package/docs-site/en/architecture.html +5 -1
- package/docs-site/en/benchmark.html +5 -1
- package/docs-site/en/commands.html +5 -1
- package/docs-site/en/context-engine.html +5 -1
- package/docs-site/en/index.html +54 -1
- package/docs-site/en/mcp-approval.html +5 -1
- package/docs-site/en/mcp-auth.html +5 -1
- package/docs-site/en/mcp-servers.html +5 -1
- package/docs-site/en/mcp.html +5 -1
- package/docs-site/en/permissions.html +5 -1
- package/docs-site/en/profiles.html +5 -1
- package/docs-site/en/quickstart.html +5 -1
- package/docs-site/en/runtime.html +18 -1
- package/docs-site/en/scope.html +5 -1
- package/docs-site/en/security.html +5 -1
- package/docs-site/en/team.html +5 -1
- package/docs-site/en/whats-new.html +5 -1
- package/docs-site/en/workflows.html +5 -1
- package/docs-site/index.html +54 -1
- package/docs-site/mcp-approval.html +5 -1
- package/docs-site/mcp-auth.html +5 -1
- package/docs-site/mcp-servers.html +5 -1
- package/docs-site/mcp.html +5 -1
- package/docs-site/permissions.html +5 -1
- package/docs-site/profiles.html +5 -1
- package/docs-site/quickstart.html +5 -1
- package/docs-site/runtime.html +33 -1
- package/docs-site/scope.html +5 -1
- package/docs-site/security.html +5 -1
- package/docs-site/team.html +5 -1
- package/docs-site/whats-new.html +5 -1
- package/docs-site/workflows.html +5 -1
- package/evals/fixtures/piagent-webui/document-workspace-v1.invalid.json +17 -0
- package/evals/fixtures/piagent-webui/document-workspace-v1.valid.json +42 -0
- package/evals/fixtures/piagent-webui/session-command-v1.valid.json +2 -1
- package/package.json +14 -2
- package/packages/piagent-core/README.md +3 -2
- package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
- package/packages/piagent-core/capabilities/runtime-integrity.js +51 -4
- package/packages/piagent-core/extensions/document-intake.ts +6 -3
- package/packages/piagent-core/extensions/piagent-guard.ts +20 -237
- package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
- package/packages/piagent-core/extensions/shell-reach.ts +250 -0
- package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
- package/packages/piagent-core/mcp/mcp-server-catalog.js +135 -5
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +9 -2
- package/packages/piagent-core/runtime/input/attachment-document-command.ts +39 -0
- package/packages/piagent-core/runtime/input/attachment-reservation.ts +36 -0
- package/packages/{piagent-webui/extension → piagent-core/runtime/input}/attachment-store.ts +221 -44
- package/packages/piagent-core/runtime/inspection/document-workspace-projection.ts +276 -0
- package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +20 -5
- package/packages/piagent-core/runtime/inspection/session-identity.ts +20 -0
- package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +25 -2
- package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +9 -13
- package/packages/piagent-core/runtime/registration/workflow-commands.ts +5 -4
- package/packages/piagent-core/runtime/session/token-economics.js +34 -0
- package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/input-routing.ts +13 -2
- package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
- package/packages/piagent-core/security/sensitive-data.js +7 -0
- package/packages/piagent-webui/dist/client/assets/MarkdownMessage-Uouhx1QO.js +29 -0
- package/packages/piagent-webui/dist/client/assets/index-BV-_03yl.css +1 -0
- package/packages/piagent-webui/dist/client/assets/index-MBZq8XLV.js +174 -0
- package/packages/piagent-webui/dist/client/index.html +2 -2
- package/packages/piagent-webui/extension/lifecycle-controller.ts +15 -0
- package/packages/piagent-webui/extension/piagent-webui.ts +19 -6
- package/packages/piagent-webui/extension/same-session-bridge.ts +12 -12
- package/packages/piagent-webui/gateway/ai-capability-inspection.ts +53 -0
- package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
- package/packages/piagent-webui/gateway/gateway-service.ts +23 -3
- package/packages/piagent-webui/gateway/native-project-picker.ts +63 -7
- package/packages/piagent-webui/gateway/session-attachment-registry.ts +165 -0
- package/packages/piagent-webui/gateway/session-catalog.ts +62 -9
- package/packages/piagent-webui/gateway/session-command-controller.ts +47 -14
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +67 -12
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +24 -11
- package/packages/piagent-webui/package.json +3 -1
- package/packages/piagent-webui/server/core-inspection-provider.ts +52 -15
- package/packages/piagent-webui/server/ipc-read-model-client.ts +2 -0
- package/packages/piagent-webui/server/loopback-server.ts +63 -25
- package/packages/piagent-webui/server/read-model-provider.ts +2 -0
- package/packages/piagent-webui/server/read-only-router.ts +6 -0
- package/packages/piagent-webui/server/transcript-projection.ts +58 -2
- package/schemas/piagent-webui/attachment-v1.schema.json +24 -4
- package/schemas/piagent-webui/capabilities-v1.schema.json +2 -2
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/document-workspace-v1.schema.json +155 -0
- package/schemas/piagent-webui/session-command-v1.schema.json +14 -3
- package/schemas/piagent-webui/transcript-v1.schema.json +27 -0
- package/scripts/build-docs-site.mjs +21 -1
- package/scripts/explain-command.mjs +240 -0
- package/scripts/install-global.sh +104 -3
- package/scripts/mcp-manage.mjs +17 -4
- package/scripts/pi-session-stats.sh +10 -2
- package/scripts/piagent-cli.mjs +14 -1
- package/scripts/piagent-dashboard.mjs +55 -2
- package/scripts/setup.sh +69 -33
- package/scripts/traffic-snapshot.mjs +214 -0
- package/scripts/typescript-loader.mjs +20 -4
- package/scripts/uninstall-global.sh +53 -2
- package/scripts/update-global.mjs +33 -1
- package/scripts/verify-local.sh +20 -1
- package/scripts/verify-translation-pairing.mjs +121 -0
- package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +0 -172
- package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +0 -1
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
evaluateExecPolicyCore, extractShellGlobCandidates, extractShellPathCandidates, findProtectedPathInCommand, unresolvedPathExpansions,
|
|
12
12
|
globMatchesPath, matchesProtectedPath, normalizePathCandidate, matchesAnyPath
|
|
13
13
|
} from "./policy-core.js";
|
|
14
|
+
import {
|
|
15
|
+
expandSimpleGlobAlternatives, findResolvedProtectedPathInCommand, normalizeRelative, protectedPatternExamples,
|
|
16
|
+
resolveRepositoryPathCandidate, shellGlobMatchesPath, shellGlobSegmentMatches, shellGlobTargetsProtectedPath,
|
|
17
|
+
unresolvedExpansionReason
|
|
18
|
+
} from "./shell-reach.ts";
|
|
14
19
|
import { extractShellWritePathCandidates, shellHasFileWriteRedirection } from "./shell-write-targets.js";
|
|
15
20
|
import { commandMatchesVerifyPlan, createBashResultLedger, findMatchingObservedBashResult, readObservedBashResults } from "./runtime-evidence.js";
|
|
16
21
|
import { findPackageRoot, findPlatformRoot, readJsonFile } from "./guard-io.js";
|
|
@@ -110,7 +115,7 @@ import {
|
|
|
110
115
|
activeTaskToolGroups
|
|
111
116
|
} from "../runtime/tools/tool-groups.ts";
|
|
112
117
|
import type { PiagentToolGroup } from "../runtime/tools/tool-groups.ts";
|
|
113
|
-
import { shortTaskLabel } from "../runtime/workflows/input-routing.ts";
|
|
118
|
+
import { freshRequestParts, shortTaskLabel } from "../runtime/workflows/input-routing.ts";
|
|
114
119
|
import {
|
|
115
120
|
automaticAcceptanceCriteria,
|
|
116
121
|
automaticReviewLenses,
|
|
@@ -374,10 +379,16 @@ const SHELL_TOOL_NAMES = new Set(["bash", "shell", "exec"]);
|
|
|
374
379
|
const MAX_MCP_PROXY_ARGS_CHARS = 131_072;
|
|
375
380
|
const SESSION_PERMISSION_OVERRIDES = new Map<string, PermissionProfileMode>();
|
|
376
381
|
|
|
382
|
+
// The policy used when `policies/base-policy.json` cannot be read. It runs in
|
|
383
|
+
// exactly the case where something is already wrong, so it must never be the
|
|
384
|
+
// looser of the two: this copy had drifted behind the file and silently dropped
|
|
385
|
+
// `.pi/piagent-state/**` -- the guard's own state -- along with the `sudo ` and
|
|
386
|
+
// `chmod -R 777` blocks. A missing policy file is a reason to be stricter, not a
|
|
387
|
+
// quiet downgrade. `tests/policy-core.test.mjs` fails if this drifts again.
|
|
377
388
|
const DEFAULT_POLICY: BasePolicy = {
|
|
378
|
-
protectedPaths: [".git/**", "**/auth.json", "**/.env", "**/.env.*", ".pi/settings.json", ".pi/piagent-profile.json", ".pi/piagent-profile.lock.json", CONTEXT_INDEX_FILE],
|
|
379
|
-
shellProtectedPaths: [".git/**", "**/auth.json", "**/.env", "**/.env.*", ".pi/settings.json", ".pi/piagent-profile.json", ".pi/piagent-profile.lock.json", CONTEXT_INDEX_FILE],
|
|
380
|
-
blockedCommandPatterns: ["rm -rf /", "rm -rf ~", "rm -rf $HOME", "git reset --hard", "git clean -fd"],
|
|
389
|
+
protectedPaths: [".git/**", "**/auth.json", "**/.env", "**/.env.*", "**/node_modules/**", "**/dist/**", ".pi/piagent-state/**", ".pi/settings.json", ".pi/piagent-profile.json", ".pi/piagent-profile.lock.json", CONTEXT_INDEX_FILE],
|
|
390
|
+
shellProtectedPaths: [".git/**", "**/auth.json", "**/.env", "**/.env.*", ".pi/piagent-state/**", ".pi/settings.json", ".pi/piagent-profile.json", ".pi/piagent-profile.lock.json", CONTEXT_INDEX_FILE],
|
|
391
|
+
blockedCommandPatterns: ["rm -rf /", "rm -rf ~", "rm -rf $HOME", "git reset --hard", "git clean -fd", "sudo ", "chmod -R 777"],
|
|
381
392
|
requireConfirmationPatterns: ["deploy", "release", "publish", "migration", "gh pr merge", "git push"],
|
|
382
393
|
defaultRequiredContext: ["AGENTS.md", "README.md"],
|
|
383
394
|
permissionProfiles: {
|
|
@@ -646,26 +657,6 @@ function legacyProjectStateWarning(cwd: string): string | undefined {
|
|
|
646
657
|
: `${detail} No .pi/piagent-profile.json exists, so this project is running unprofiled and its protected paths and verify commands are NOT enforced. Convert it with \`piagent-migrate . --apply\`.`;
|
|
647
658
|
}
|
|
648
659
|
|
|
649
|
-
function normalizeRelative(cwd: string, candidate: unknown): string | undefined {
|
|
650
|
-
if (typeof candidate !== "string" || candidate.trim().length === 0) return undefined;
|
|
651
|
-
let raw = candidate.trim();
|
|
652
|
-
try {
|
|
653
|
-
raw = decodeURIComponent(raw);
|
|
654
|
-
} catch {
|
|
655
|
-
return undefined;
|
|
656
|
-
}
|
|
657
|
-
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(raw)) {
|
|
658
|
-
if (!raw.toLowerCase().startsWith("file://")) return undefined;
|
|
659
|
-
try {
|
|
660
|
-
raw = fileURLToPath(raw);
|
|
661
|
-
} catch {
|
|
662
|
-
return undefined;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
666
|
-
return path.relative(cwd, absolute).split(path.sep).join("/");
|
|
667
|
-
}
|
|
668
|
-
|
|
669
660
|
function slugify(input: string): string {
|
|
670
661
|
return input
|
|
671
662
|
.trim()
|
|
@@ -957,60 +948,6 @@ function inspectRepositoryPathBoundary(cwd: string, candidate: string): { reason
|
|
|
957
948
|
return {};
|
|
958
949
|
}
|
|
959
950
|
|
|
960
|
-
function resolveRepositoryPathCandidate(cwd: string, candidate: string): string | undefined {
|
|
961
|
-
const normalized = normalizePathCandidate(candidate);
|
|
962
|
-
if (normalized === ".." || normalized.startsWith("../")) return undefined;
|
|
963
|
-
|
|
964
|
-
const relative = path.posix.isAbsolute(normalized)
|
|
965
|
-
? path.relative(cwd, normalized).split(path.sep).join("/")
|
|
966
|
-
: normalized;
|
|
967
|
-
if (relative === ".." || relative.startsWith("../")) return undefined;
|
|
968
|
-
|
|
969
|
-
const pending = relative.split("/").filter((item) => item && item !== ".");
|
|
970
|
-
let current = cwd;
|
|
971
|
-
let resolvedDepth = 0;
|
|
972
|
-
for (let index = 0; index < pending.length; index += 1) {
|
|
973
|
-
const next = path.join(current, pending[index]);
|
|
974
|
-
try {
|
|
975
|
-
fs.lstatSync(next);
|
|
976
|
-
current = next;
|
|
977
|
-
resolvedDepth = index + 1;
|
|
978
|
-
} catch (error) {
|
|
979
|
-
const code = error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
980
|
-
if (code !== "ENOENT") return undefined;
|
|
981
|
-
break;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
let canonicalBase: string;
|
|
986
|
-
try {
|
|
987
|
-
canonicalBase = fs.realpathSync.native(current);
|
|
988
|
-
} catch {
|
|
989
|
-
return undefined;
|
|
990
|
-
}
|
|
991
|
-
const canonical = path.resolve(canonicalBase, ...pending.slice(resolvedDepth));
|
|
992
|
-
const canonicalRoot = fs.realpathSync.native(cwd);
|
|
993
|
-
const canonicalRelative = path.relative(canonicalRoot, canonical).split(path.sep).join("/");
|
|
994
|
-
if (canonicalRelative === ".." || canonicalRelative.startsWith("../") || path.isAbsolute(canonicalRelative)) return undefined;
|
|
995
|
-
return canonicalRelative || ".";
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function findResolvedProtectedPathInCommand(
|
|
999
|
-
cwd: string,
|
|
1000
|
-
command: string,
|
|
1001
|
-
protectedPatterns: string[]
|
|
1002
|
-
): { candidate: string; resolved: string; pattern: string } | undefined {
|
|
1003
|
-
for (const candidate of extractShellPathCandidates(command)) {
|
|
1004
|
-
const relative = normalizeRelative(cwd, candidate);
|
|
1005
|
-
if (!relative) continue;
|
|
1006
|
-
const resolved = resolveRepositoryPathCandidate(cwd, relative);
|
|
1007
|
-
if (!resolved || resolved === relative) continue;
|
|
1008
|
-
const pattern = matchesProtectedPath(resolved, protectedPatterns);
|
|
1009
|
-
if (pattern) return { candidate, resolved, pattern };
|
|
1010
|
-
}
|
|
1011
|
-
return undefined;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
951
|
const MAX_TOOL_INPUT_INSPECTION_DEPTH = 32;
|
|
1015
952
|
|
|
1016
953
|
type StringInputWalkResult = {
|
|
@@ -1088,130 +1025,6 @@ function extractLikelyPathFromInput(cwd: string, input: Record<string, unknown>)
|
|
|
1088
1025
|
return extractPathInputsFromInput(cwd, input)[0]?.path;
|
|
1089
1026
|
}
|
|
1090
1027
|
|
|
1091
|
-
function expandSimpleGlobAlternatives(pattern: string, max = 24): { values: string[]; complete: boolean } {
|
|
1092
|
-
let results = [pattern];
|
|
1093
|
-
let changed = true;
|
|
1094
|
-
let complete = true;
|
|
1095
|
-
|
|
1096
|
-
while (changed) {
|
|
1097
|
-
changed = false;
|
|
1098
|
-
const expanded: string[] = [];
|
|
1099
|
-
for (const item of results) {
|
|
1100
|
-
const match = item.match(/\{([^{}]+)\}/);
|
|
1101
|
-
if (!match) {
|
|
1102
|
-
expanded.push(item);
|
|
1103
|
-
continue;
|
|
1104
|
-
}
|
|
1105
|
-
changed = true;
|
|
1106
|
-
const options = match[1].split(",").map((option) => option.trim());
|
|
1107
|
-
for (const option of options) {
|
|
1108
|
-
expanded.push(`${item.slice(0, match.index)}${option}${item.slice((match.index ?? 0) + match[0].length)}`);
|
|
1109
|
-
if (expanded.length >= max) {
|
|
1110
|
-
complete = false;
|
|
1111
|
-
break;
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
if (expanded.length >= max) break;
|
|
1115
|
-
}
|
|
1116
|
-
results = expanded.slice(0, max);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
return { values: results, complete };
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
function shellGlobSegmentMatches(patternSegment: string, candidateSegment: string): boolean {
|
|
1123
|
-
if (candidateSegment.startsWith(".") && !patternSegment.startsWith(".")) return false;
|
|
1124
|
-
let source = "";
|
|
1125
|
-
for (let index = 0; index < patternSegment.length; index += 1) {
|
|
1126
|
-
const char = patternSegment[index];
|
|
1127
|
-
if (char === "*") {
|
|
1128
|
-
source += "[^/]*";
|
|
1129
|
-
continue;
|
|
1130
|
-
}
|
|
1131
|
-
if (char === "?") {
|
|
1132
|
-
source += "[^/]";
|
|
1133
|
-
continue;
|
|
1134
|
-
}
|
|
1135
|
-
if (char === "[") {
|
|
1136
|
-
const end = patternSegment.indexOf("]", index + 1);
|
|
1137
|
-
const body = end > index + 1 ? patternSegment.slice(index + 1, end) : "";
|
|
1138
|
-
if (body && /^[!^A-Za-z0-9_-]+$/.test(body)) {
|
|
1139
|
-
const negated = body.startsWith("!") ? `^${body.slice(1)}` : body;
|
|
1140
|
-
source += `[${negated}]`;
|
|
1141
|
-
index = end;
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
1146
|
-
}
|
|
1147
|
-
return new RegExp(`^${source}$`, "i").test(candidateSegment);
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
function shellGlobMatchesPath(pattern: string, candidate: string): boolean {
|
|
1151
|
-
const patternSegments = normalizePathCandidate(pattern).split("/").filter(Boolean);
|
|
1152
|
-
const candidateSegments = normalizePathCandidate(candidate).split("/").filter(Boolean);
|
|
1153
|
-
|
|
1154
|
-
function match(patternIndex: number, candidateIndex: number): boolean {
|
|
1155
|
-
if (patternIndex === patternSegments.length) return candidateIndex === candidateSegments.length;
|
|
1156
|
-
const patternSegment = patternSegments[patternIndex];
|
|
1157
|
-
if (patternSegment === "**") {
|
|
1158
|
-
if (match(patternIndex + 1, candidateIndex)) return true;
|
|
1159
|
-
for (let next = candidateIndex; next < candidateSegments.length; next += 1) {
|
|
1160
|
-
if (match(patternIndex + 1, next + 1)) return true;
|
|
1161
|
-
}
|
|
1162
|
-
return false;
|
|
1163
|
-
}
|
|
1164
|
-
if (candidateIndex >= candidateSegments.length) return false;
|
|
1165
|
-
return shellGlobSegmentMatches(patternSegment, candidateSegments[candidateIndex])
|
|
1166
|
-
&& match(patternIndex + 1, candidateIndex + 1);
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
return patternSegments.length > 0 && candidateSegments.length > 0 && match(0, 0);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
function protectedPatternExamples(pattern: string): string[] {
|
|
1173
|
-
const normalized = normalizePathCandidate(pattern);
|
|
1174
|
-
if (!normalized) return [];
|
|
1175
|
-
|
|
1176
|
-
const examples = new Set<string>();
|
|
1177
|
-
const add = (value: string | undefined) => {
|
|
1178
|
-
const normalizedValue = normalizePathCandidate(value ?? "");
|
|
1179
|
-
if (normalizedValue) examples.add(normalizedValue);
|
|
1180
|
-
};
|
|
1181
|
-
|
|
1182
|
-
add(normalized);
|
|
1183
|
-
|
|
1184
|
-
if (normalized.endsWith("/**")) {
|
|
1185
|
-
const base = normalized.slice(0, -3);
|
|
1186
|
-
add(base);
|
|
1187
|
-
add(`${base}/probe`);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
if (normalized.startsWith("**/")) {
|
|
1191
|
-
const tail = normalized.slice(3);
|
|
1192
|
-
const concreteTail = tail
|
|
1193
|
-
.replace(/\*\*/g, "nested")
|
|
1194
|
-
.replace(/\*/g, tail.includes(".env.") ? "local" : "probe");
|
|
1195
|
-
add(tail);
|
|
1196
|
-
add(concreteTail);
|
|
1197
|
-
add(`nested/${concreteTail}`);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const concrete = normalized
|
|
1201
|
-
.replace(/^\*\*\//, "")
|
|
1202
|
-
.replace(/\/\*\*$/, "/probe")
|
|
1203
|
-
.replace(/\*\*/g, "nested")
|
|
1204
|
-
.replace(/\*/g, normalized.includes(".env.") ? "local" : "probe");
|
|
1205
|
-
add(concrete);
|
|
1206
|
-
|
|
1207
|
-
for (const example of [...examples]) {
|
|
1208
|
-
const base = path.posix.basename(example);
|
|
1209
|
-
if (base && base !== "probe") examples.add(base);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
return [...examples];
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
1028
|
function protectedLiteralHints(pattern: string): string[] {
|
|
1216
1029
|
const normalized = normalizePathCandidate(pattern).toLowerCase();
|
|
1217
1030
|
if (!normalized) return [];
|
|
@@ -1271,32 +1084,6 @@ function globTargetsProtectedPath(glob: unknown, protectedPatterns: string[]): {
|
|
|
1271
1084
|
return undefined;
|
|
1272
1085
|
}
|
|
1273
1086
|
|
|
1274
|
-
function shellGlobTargetsProtectedPath(
|
|
1275
|
-
command: string,
|
|
1276
|
-
protectedPatterns: string[]
|
|
1277
|
-
): { glob: string; pattern: string; example: string } | undefined {
|
|
1278
|
-
for (const candidate of extractShellGlobCandidates(command)) {
|
|
1279
|
-
if (!/[*?{\[]/.test(candidate)) continue;
|
|
1280
|
-
const expanded = expandSimpleGlobAlternatives(candidate);
|
|
1281
|
-
if (!expanded.complete) return { glob: candidate, pattern: "bounded glob expansion", example: "a protected path" };
|
|
1282
|
-
for (const candidateGlob of expanded.values) {
|
|
1283
|
-
for (const pattern of protectedPatterns) {
|
|
1284
|
-
for (const example of protectedPatternExamples(pattern)) {
|
|
1285
|
-
if (/[*?{\[\]]/.test(example)) continue;
|
|
1286
|
-
if (
|
|
1287
|
-
shellGlobMatchesPath(candidateGlob, example)
|
|
1288
|
-
|| shellGlobMatchesPath(`**/${candidateGlob}`, example)
|
|
1289
|
-
|| shellGlobMatchesPath(candidateGlob, path.posix.basename(example))
|
|
1290
|
-
) {
|
|
1291
|
-
return { glob: candidate, pattern, example };
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
return undefined;
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
1087
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
1301
1088
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1302
1089
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -2550,13 +2337,6 @@ function evaluateMcpApproval(
|
|
|
2550
2337
|
// running the substitution. Matching the literal half against the protected
|
|
2551
2338
|
// patterns answers a different question than the one being asked, so refuse
|
|
2552
2339
|
// rather than let an unknown filename through unchecked.
|
|
2553
|
-
function unresolvedExpansionReason(subject: string, words: string[]): string {
|
|
2554
|
-
const listed = words.map((word) => `\`${word}\``).join(", ");
|
|
2555
|
-
return `${subject} builds a filename this guard cannot resolve: ${listed}. `
|
|
2556
|
-
+ "The literal text around the expansion makes it a path, but its value is only known at run time, "
|
|
2557
|
-
+ "so it cannot be checked against the protected paths. Write the path out, or put the expansion in its own argument.";
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
2340
|
function evaluateMcpProxyShellProtectedAccess(
|
|
2561
2341
|
cwd: string,
|
|
2562
2342
|
prepared: PreparedToolInput,
|
|
@@ -4829,7 +4609,10 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4829
4609
|
if (!sessionTask && taskContractRequired && (shellProjectMutation || directProjectMutation)) {
|
|
4830
4610
|
return {
|
|
4831
4611
|
block: true,
|
|
4832
|
-
|
|
4612
|
+
// Naming the tool matters: this is the first wall a new operator hits,
|
|
4613
|
+
// and "start the task first" does not say with what.
|
|
4614
|
+
reason: "A session-bound Task Implementation Contract is required before project files can be mutated."
|
|
4615
|
+
+ " Call `piagent_task_start` once with explicit project-relative scope, then retry."
|
|
4833
4616
|
};
|
|
4834
4617
|
}
|
|
4835
4618
|
if (sessionTask?.trace.outcome === "pending" && sessionTask.changeMode === "source-change") {
|
|
@@ -4922,7 +4705,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4922
4705
|
safeTaskId, searchContextIndex, searchContextIndexV2, searchMemoryFiles, selectRuntimeAction,
|
|
4923
4706
|
selectValueFromUi, selectVerificationPlan, semanticCompactionInstructions, sendWorkflowFollowUp, setPermissionOverrideForContext,
|
|
4924
4707
|
semanticRepairCompletionBlock: (cwd: string, taskRunId: string) => semanticRepairRuntime.completionBlock(cwd, taskRunId),
|
|
4925
|
-
shellArg, shortTaskLabel, solverShadow, summarizeAttempt, taskChangedFileEvidence,
|
|
4708
|
+
freshRequestParts, shellArg, shortTaskLabel, solverShadow, summarizeAttempt, taskChangedFileEvidence,
|
|
4926
4709
|
techContextDirPath, techContextFilePath, techContextRelativePath, techOptionById, techStackPath,
|
|
4927
4710
|
telemetry, toolRegistryConfig, trajectoryRuntime, uniqueStrings, usageExactCommands,
|
|
4928
4711
|
validTaskScopePattern, validateNewWorkPlan, verifierCommandInstructions, verifyProjectCapabilityState, workingTreeEvidenceDigest,
|
|
@@ -58,11 +58,22 @@ function numericExitCode(value) {
|
|
|
58
58
|
return undefined;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// `isError: false` is Pi stating that the command succeeded, so it is evidence
|
|
62
|
+
// and a claim of zero matches it. An observation that states neither an exit code
|
|
63
|
+
// nor an error flag says nothing at all, and nothing is not evidence of success:
|
|
64
|
+
// without this an empty record satisfied a claim of zero, which is the one
|
|
65
|
+
// direction a ledger built to catch forged verification must never fail in.
|
|
66
|
+
//
|
|
67
|
+
// Both shapes are unreachable through `observedBashResultFromToolResultEvent` and
|
|
68
|
+
// `readObservedBashResults`, which always set a boolean flag. The guard is here
|
|
69
|
+
// because a reader of this function should not have to prove that to know the
|
|
70
|
+
// answer is safe.
|
|
61
71
|
export function claimedExitMatchesObserved(exitCode, observed) {
|
|
62
72
|
const claimed = numericExitCode(exitCode);
|
|
63
73
|
if (claimed === undefined) return false;
|
|
64
74
|
if (typeof observed?.exitCode === "number") return observed.exitCode === claimed;
|
|
65
|
-
|
|
75
|
+
if (typeof observed?.isError !== "boolean") return false;
|
|
76
|
+
return observed.isError ? claimed !== 0 : claimed === 0;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
export function extractBashCommandFromToolResultEvent(event) {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// What a shell command can reach, and how to say so.
|
|
2
|
+
//
|
|
3
|
+
// These checks used to live inside the guard entrypoint, which imports the Pi
|
|
4
|
+
// host runtime and therefore cannot be loaded anywhere else. `piagent explain`
|
|
5
|
+
// needs the same answers the guard gives, and a second implementation of "does
|
|
6
|
+
// this command reach a protected path" is exactly the defect this project keeps
|
|
7
|
+
// closing: one segment, many readers, each with its own view. So the readers
|
|
8
|
+
// live here and both the guard and the CLI take this one.
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
extractShellGlobCandidates,
|
|
16
|
+
extractShellPathCandidates,
|
|
17
|
+
matchesProtectedPath,
|
|
18
|
+
normalizePathCandidate
|
|
19
|
+
} from "./policy-core.js";
|
|
20
|
+
|
|
21
|
+
export function normalizeRelative(cwd: string, candidate: unknown): string | undefined {
|
|
22
|
+
if (typeof candidate !== "string" || candidate.trim().length === 0) return undefined;
|
|
23
|
+
let raw = candidate.trim();
|
|
24
|
+
try {
|
|
25
|
+
raw = decodeURIComponent(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(raw)) {
|
|
30
|
+
if (!raw.toLowerCase().startsWith("file://")) return undefined;
|
|
31
|
+
try {
|
|
32
|
+
raw = fileURLToPath(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
38
|
+
return path.relative(cwd, absolute).split(path.sep).join("/");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveRepositoryPathCandidate(cwd: string, candidate: string): string | undefined {
|
|
42
|
+
const normalized = normalizePathCandidate(candidate);
|
|
43
|
+
if (normalized === ".." || normalized.startsWith("../")) return undefined;
|
|
44
|
+
|
|
45
|
+
const relative = path.posix.isAbsolute(normalized)
|
|
46
|
+
? path.relative(cwd, normalized).split(path.sep).join("/")
|
|
47
|
+
: normalized;
|
|
48
|
+
if (relative === ".." || relative.startsWith("../")) return undefined;
|
|
49
|
+
|
|
50
|
+
const pending = relative.split("/").filter((item) => item && item !== ".");
|
|
51
|
+
let current = cwd;
|
|
52
|
+
let resolvedDepth = 0;
|
|
53
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
54
|
+
const next = path.join(current, pending[index]);
|
|
55
|
+
try {
|
|
56
|
+
fs.lstatSync(next);
|
|
57
|
+
current = next;
|
|
58
|
+
resolvedDepth = index + 1;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const code = error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
61
|
+
if (code !== "ENOENT") return undefined;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let canonicalBase: string;
|
|
67
|
+
try {
|
|
68
|
+
canonicalBase = fs.realpathSync.native(current);
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const canonical = path.resolve(canonicalBase, ...pending.slice(resolvedDepth));
|
|
73
|
+
const canonicalRoot = fs.realpathSync.native(cwd);
|
|
74
|
+
const canonicalRelative = path.relative(canonicalRoot, canonical).split(path.sep).join("/");
|
|
75
|
+
if (canonicalRelative === ".." || canonicalRelative.startsWith("../") || path.isAbsolute(canonicalRelative)) return undefined;
|
|
76
|
+
return canonicalRelative || ".";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function findResolvedProtectedPathInCommand(
|
|
80
|
+
cwd: string,
|
|
81
|
+
command: string,
|
|
82
|
+
protectedPatterns: string[]
|
|
83
|
+
): { candidate: string; resolved: string; pattern: string } | undefined {
|
|
84
|
+
for (const candidate of extractShellPathCandidates(command)) {
|
|
85
|
+
const relative = normalizeRelative(cwd, candidate);
|
|
86
|
+
if (!relative) continue;
|
|
87
|
+
const resolved = resolveRepositoryPathCandidate(cwd, relative);
|
|
88
|
+
if (!resolved || resolved === relative) continue;
|
|
89
|
+
const pattern = matchesProtectedPath(resolved, protectedPatterns);
|
|
90
|
+
if (pattern) return { candidate, resolved, pattern };
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function expandSimpleGlobAlternatives(pattern: string, max = 24): { values: string[]; complete: boolean } {
|
|
96
|
+
let results = [pattern];
|
|
97
|
+
let changed = true;
|
|
98
|
+
let complete = true;
|
|
99
|
+
|
|
100
|
+
while (changed) {
|
|
101
|
+
changed = false;
|
|
102
|
+
const expanded: string[] = [];
|
|
103
|
+
for (const item of results) {
|
|
104
|
+
const match = item.match(/\{([^{}]+)\}/);
|
|
105
|
+
if (!match) {
|
|
106
|
+
expanded.push(item);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
changed = true;
|
|
110
|
+
const options = match[1].split(",").map((option) => option.trim());
|
|
111
|
+
for (const option of options) {
|
|
112
|
+
expanded.push(`${item.slice(0, match.index)}${option}${item.slice((match.index ?? 0) + match[0].length)}`);
|
|
113
|
+
if (expanded.length >= max) {
|
|
114
|
+
complete = false;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (expanded.length >= max) break;
|
|
119
|
+
}
|
|
120
|
+
results = expanded.slice(0, max);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { values: results, complete };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function shellGlobSegmentMatches(patternSegment: string, candidateSegment: string): boolean {
|
|
127
|
+
if (candidateSegment.startsWith(".") && !patternSegment.startsWith(".")) return false;
|
|
128
|
+
let source = "";
|
|
129
|
+
for (let index = 0; index < patternSegment.length; index += 1) {
|
|
130
|
+
const char = patternSegment[index];
|
|
131
|
+
if (char === "*") {
|
|
132
|
+
source += "[^/]*";
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === "?") {
|
|
136
|
+
source += "[^/]";
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (char === "[") {
|
|
140
|
+
const end = patternSegment.indexOf("]", index + 1);
|
|
141
|
+
const body = end > index + 1 ? patternSegment.slice(index + 1, end) : "";
|
|
142
|
+
if (body && /^[!^A-Za-z0-9_-]+$/.test(body)) {
|
|
143
|
+
const negated = body.startsWith("!") ? `^${body.slice(1)}` : body;
|
|
144
|
+
source += `[${negated}]`;
|
|
145
|
+
index = end;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
source += char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
150
|
+
}
|
|
151
|
+
return new RegExp(`^${source}$`, "i").test(candidateSegment);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function shellGlobMatchesPath(pattern: string, candidate: string): boolean {
|
|
155
|
+
const patternSegments = normalizePathCandidate(pattern).split("/").filter(Boolean);
|
|
156
|
+
const candidateSegments = normalizePathCandidate(candidate).split("/").filter(Boolean);
|
|
157
|
+
|
|
158
|
+
function match(patternIndex: number, candidateIndex: number): boolean {
|
|
159
|
+
if (patternIndex === patternSegments.length) return candidateIndex === candidateSegments.length;
|
|
160
|
+
const patternSegment = patternSegments[patternIndex];
|
|
161
|
+
if (patternSegment === "**") {
|
|
162
|
+
if (match(patternIndex + 1, candidateIndex)) return true;
|
|
163
|
+
for (let next = candidateIndex; next < candidateSegments.length; next += 1) {
|
|
164
|
+
if (match(patternIndex + 1, next + 1)) return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (candidateIndex >= candidateSegments.length) return false;
|
|
169
|
+
return shellGlobSegmentMatches(patternSegment, candidateSegments[candidateIndex])
|
|
170
|
+
&& match(patternIndex + 1, candidateIndex + 1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return patternSegments.length > 0 && candidateSegments.length > 0 && match(0, 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function protectedPatternExamples(pattern: string): string[] {
|
|
177
|
+
const normalized = normalizePathCandidate(pattern);
|
|
178
|
+
if (!normalized) return [];
|
|
179
|
+
|
|
180
|
+
const examples = new Set<string>();
|
|
181
|
+
const add = (value: string | undefined) => {
|
|
182
|
+
const normalizedValue = normalizePathCandidate(value ?? "");
|
|
183
|
+
if (normalizedValue) examples.add(normalizedValue);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
add(normalized);
|
|
187
|
+
|
|
188
|
+
if (normalized.endsWith("/**")) {
|
|
189
|
+
const base = normalized.slice(0, -3);
|
|
190
|
+
add(base);
|
|
191
|
+
add(`${base}/probe`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (normalized.startsWith("**/")) {
|
|
195
|
+
const tail = normalized.slice(3);
|
|
196
|
+
const concreteTail = tail
|
|
197
|
+
.replace(/\*\*/g, "nested")
|
|
198
|
+
.replace(/\*/g, tail.includes(".env.") ? "local" : "probe");
|
|
199
|
+
add(tail);
|
|
200
|
+
add(concreteTail);
|
|
201
|
+
add(`nested/${concreteTail}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const concrete = normalized
|
|
205
|
+
.replace(/^\*\*\//, "")
|
|
206
|
+
.replace(/\/\*\*$/, "/probe")
|
|
207
|
+
.replace(/\*\*/g, "nested")
|
|
208
|
+
.replace(/\*/g, normalized.includes(".env.") ? "local" : "probe");
|
|
209
|
+
add(concrete);
|
|
210
|
+
|
|
211
|
+
for (const example of [...examples]) {
|
|
212
|
+
const base = path.posix.basename(example);
|
|
213
|
+
if (base && base !== "probe") examples.add(base);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return [...examples];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function shellGlobTargetsProtectedPath(
|
|
220
|
+
command: string,
|
|
221
|
+
protectedPatterns: string[]
|
|
222
|
+
): { glob: string; pattern: string; example: string } | undefined {
|
|
223
|
+
for (const candidate of extractShellGlobCandidates(command)) {
|
|
224
|
+
if (!/[*?{\[]/.test(candidate)) continue;
|
|
225
|
+
const expanded = expandSimpleGlobAlternatives(candidate);
|
|
226
|
+
if (!expanded.complete) return { glob: candidate, pattern: "bounded glob expansion", example: "a protected path" };
|
|
227
|
+
for (const candidateGlob of expanded.values) {
|
|
228
|
+
for (const pattern of protectedPatterns) {
|
|
229
|
+
for (const example of protectedPatternExamples(pattern)) {
|
|
230
|
+
if (/[*?{\[\]]/.test(example)) continue;
|
|
231
|
+
if (
|
|
232
|
+
shellGlobMatchesPath(candidateGlob, example)
|
|
233
|
+
|| shellGlobMatchesPath(`**/${candidateGlob}`, example)
|
|
234
|
+
|| shellGlobMatchesPath(candidateGlob, path.posix.basename(example))
|
|
235
|
+
) {
|
|
236
|
+
return { glob: candidate, pattern, example };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function unresolvedExpansionReason(subject: string, words: string[]): string {
|
|
246
|
+
const listed = words.map((word) => `\`${word}\``).join(", ");
|
|
247
|
+
return `${subject} builds a filename this guard cannot resolve: ${listed}. `
|
|
248
|
+
+ "The literal text around the expansion makes it a path, but its value is only known at run time, "
|
|
249
|
+
+ "so it cannot be checked against the protected paths. Write the path out, or put the expansion in its own argument.";
|
|
250
|
+
}
|
|
@@ -37,7 +37,7 @@ export const SERVER_ACTIONS = new Set(["get", "approve", "reject", "reset", "ena
|
|
|
37
37
|
export const TERMINAL_ONLY = new Map([
|
|
38
38
|
["add", "piagent-mcp add <name> [--scope <scope>] --url <url> | -- <command> [args...]"],
|
|
39
39
|
["remove", "piagent-mcp remove <name> --scope <scope>"],
|
|
40
|
-
["preset", "piagent-mcp --preset <core|popular|design> --scope <scope>"]
|
|
40
|
+
["preset", "piagent-mcp --preset <core|popular|documents|google|design> --scope <scope>"]
|
|
41
41
|
]);
|
|
42
42
|
|
|
43
43
|
export const HELP_LINES = [
|