@piagent/platform 1.46.2 → 1.47.0
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 +40 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/company-dev-commands.md +2 -1
- package/docs/distribution-standard.md +6 -6
- package/docs/mcp-and-tools.md +21 -1
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +3 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/mcp.html +8 -0
- package/docs-site/content/mcp.html +8 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +15 -15
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +9 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/update.html +3 -3
- package/docs-site/en/whats-new.html +79 -47
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +15 -15
- 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 +9 -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/releases.json +98 -0
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/update.html +3 -3
- package/docs-site/whats-new.html +79 -47
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/extensions/piagent-guard.ts +2 -146
- package/packages/piagent-core/managed/command-tools.mjs +17 -3
- package/packages/piagent-core/managed/copies.mjs +14 -4
- package/packages/piagent-core/managed/member-mcp.mjs +71 -0
- package/packages/piagent-core/managed/memory-lessons.mjs +1 -1
- package/packages/piagent-core/managed/resource-loader.mjs +19 -7
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +4 -0
- package/packages/piagent-core/managed/session.mjs +5 -5
- package/packages/piagent-core/managed/simulator-commands.mjs +117 -0
- package/packages/piagent-core/mcp/mcp-server-catalog.js +18 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/managed-failure.mjs +0 -1
- package/packages/piagent-core/runtime/mcp-repository-gate.d.mts +10 -0
- package/packages/piagent-core/runtime/mcp-repository-gate.mjs +150 -0
- package/scripts/claude-model-additions.mjs +8 -1
- package/scripts/configure-model-scope.sh +6 -1
- package/scripts/install-global.sh +10 -4
- package/scripts/personal-pi-config.mjs +5 -2
- package/scripts/pi-models-json.mjs +13 -0
- package/scripts/setup.sh +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
REPOSITORY_SCOPES,
|
|
6
|
+
collectServers,
|
|
7
|
+
configPathForScope,
|
|
8
|
+
mcpDecisionInputs,
|
|
9
|
+
unverifiableMcpConfig
|
|
10
|
+
} from "../mcp/mcp-config-layers.js";
|
|
11
|
+
import { approvalState } from "../mcp/mcp-approval-store.js";
|
|
12
|
+
import { attributeDirectTool } from "../mcp/mcp-tool-naming.js";
|
|
13
|
+
|
|
14
|
+
// Servers a repository defines are not usable until somebody on this machine has
|
|
15
|
+
// approved them. This is the enforcement point, shared by personal Piagent
|
|
16
|
+
// (piagent-guard) and company sessions (managed/member-mcp.mjs): the adapter
|
|
17
|
+
// owns the connection and neither can prevent one being opened, but no tool
|
|
18
|
+
// call reaches an unapproved server. The residual exposure is the connection
|
|
19
|
+
// itself, which is worth stating plainly rather than describing this as a full
|
|
20
|
+
// block.
|
|
21
|
+
|
|
22
|
+
// The same token form the guard's action policy uses: `MCP`, `mcp` and `Mcp`
|
|
23
|
+
// all name the adapter's proxy tool.
|
|
24
|
+
const actionToken = (value) => String(value)
|
|
25
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
28
|
+
.replace(/^-+|-+$/g, "");
|
|
29
|
+
|
|
30
|
+
// Which MCP server a tool call is addressed to. The adapter exposes one proxy
|
|
31
|
+
// tool named `mcp` that carries the server in `input.server`, and, under
|
|
32
|
+
// directTools, one tool per server tool named `<prefix>_<tool>` where the prefix
|
|
33
|
+
// is derived from the server name. Direct names are matched against the servers
|
|
34
|
+
// this gate is holding rather than against a fixed pattern, because the prefix
|
|
35
|
+
// is the server's own name and there is nothing constant to anchor on.
|
|
36
|
+
export function mcpServerFromToolCall(toolName, input, candidates) {
|
|
37
|
+
if (actionToken(toolName) === "mcp") {
|
|
38
|
+
return typeof input?.server === "string" && input.server.trim() ? input.server.trim() : undefined;
|
|
39
|
+
}
|
|
40
|
+
return attributeDirectTool(toolName, candidates);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Recomputed only when one of the files behind the decision changes, because
|
|
44
|
+
// this runs on every tool call. Approving or rejecting a server clears the
|
|
45
|
+
// project's entry.
|
|
46
|
+
export const mcpApprovalCache = new Map();
|
|
47
|
+
|
|
48
|
+
function fileSignature(file) {
|
|
49
|
+
try {
|
|
50
|
+
const stat = fs.statSync(file);
|
|
51
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
52
|
+
} catch {
|
|
53
|
+
return "absent";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// `blocked`: the servers a call must not reach, with why. `unverifiable`:
|
|
58
|
+
// conditions under which no tool call can be checked at all, each with the
|
|
59
|
+
// sentence to show. Those are not "a server is unapproved" but "this
|
|
60
|
+
// repository's config has put the gate in a state where it cannot tell which
|
|
61
|
+
// server a call belongs to", and the only sound answer to that is to stop.
|
|
62
|
+
export function repositoryMcpGate(cwd) {
|
|
63
|
+
// Repository scopes plus anything an `imports` key drags in from a file the
|
|
64
|
+
// clone carries. Collecting those here is the whole point: a server the gate
|
|
65
|
+
// never enumerates is a server it silently permits.
|
|
66
|
+
const servers = collectServers({ projectPath: cwd, scopes: [...REPOSITORY_SCOPES] });
|
|
67
|
+
const repositoryFiles = [...REPOSITORY_SCOPES].map((scope) => ({
|
|
68
|
+
scope,
|
|
69
|
+
file: configPathForScope(scope, { projectPath: cwd })
|
|
70
|
+
}));
|
|
71
|
+
// Every file the decision reads, listed by the module that reads them rather
|
|
72
|
+
// than restated here. Building this by hand is what went wrong: the scan grew
|
|
73
|
+
// to read merged settings from all four scopes and to stat import targets
|
|
74
|
+
// outside the repository, the hand-written list did not, and an already-loaded
|
|
75
|
+
// guard went on permitting calls after a personal global config had put the
|
|
76
|
+
// session in the state this gate exists to refuse.
|
|
77
|
+
const signature = [
|
|
78
|
+
...servers.map((server) => `${server.origin}:${server.name}:${fileSignature(server.file)}`),
|
|
79
|
+
...repositoryFiles.map((layer) => `layer:${layer.scope}:${fileSignature(layer.file)}`),
|
|
80
|
+
...mcpDecisionInputs({ projectPath: cwd }).map((file) => `input:${file}:${fileSignature(file)}`),
|
|
81
|
+
`store:${fileSignature(path.join(os.homedir(), ".pi", "piagent-mcp-approvals.json"))}`
|
|
82
|
+
].join("|");
|
|
83
|
+
const cached = mcpApprovalCache.get(cwd);
|
|
84
|
+
if (cached?.signature === signature) return cached.gate;
|
|
85
|
+
|
|
86
|
+
const blocked = new Map();
|
|
87
|
+
for (const server of servers) {
|
|
88
|
+
const state = approvalState({ projectPath: cwd, name: server.name, entry: server.entry });
|
|
89
|
+
if (state.state === "approved") continue;
|
|
90
|
+
// A name defined more than once keeps its blocking entry: the approved copy
|
|
91
|
+
// does not vouch for the one nobody has looked at.
|
|
92
|
+
blocked.set(server.name, { state: state.state, origin: server.origin });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Two things a repository-carried config can do that leave nothing to check.
|
|
96
|
+
//
|
|
97
|
+
// It can ask for tools with no prefix, so a direct tool arrives under a bare
|
|
98
|
+
// name carrying no evidence of its server. Refusing only the proxy there was
|
|
99
|
+
// not a fix: the proxy is the one form that names its server, so blocking it
|
|
100
|
+
// and allowing the bare names left the hole exactly where it was.
|
|
101
|
+
//
|
|
102
|
+
// It can also import a kind whose config this platform cannot enumerate, and
|
|
103
|
+
// then the set of servers reaching the session is simply unknown. Blocking the
|
|
104
|
+
// servers that could be listed says nothing about the ones that could not.
|
|
105
|
+
//
|
|
106
|
+
// Both stop every tool call. A repository can make a session refuse to run;
|
|
107
|
+
// it must not be able to make one run a server nobody approved.
|
|
108
|
+
// Read from the shared module rather than decided here, so `piagent-mcp
|
|
109
|
+
// doctor` reports exactly what this refuses. The two disagreeing is how an
|
|
110
|
+
// operator ends up reading "PASS" while every tool call is being stopped.
|
|
111
|
+
const unverifiable = unverifiableMcpConfig({ projectPath: cwd }).map((problem) => problem.detail);
|
|
112
|
+
|
|
113
|
+
const gate = { blocked, serverNames: new Set(servers.map((server) => server.name)), unverifiable };
|
|
114
|
+
mcpApprovalCache.set(cwd, { signature, gate });
|
|
115
|
+
return gate;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Whether a tool call may go ahead in `cwd`: `{ block: false }`, or the
|
|
119
|
+
// reason it may not, naming the commands that resolve it.
|
|
120
|
+
export function evaluateMcpApproval(cwd, toolName, input) {
|
|
121
|
+
const gate = repositoryMcpGate(cwd);
|
|
122
|
+
|
|
123
|
+
// Every tool call, not only the ones that look like MCP. Under either of these
|
|
124
|
+
// conditions a call cannot be traced to a server, so there is no such thing as
|
|
125
|
+
// a call this gate can clear.
|
|
126
|
+
if (gate.unverifiable.length > 0) {
|
|
127
|
+
return { block: true, reason: `Blocked every tool call: ${gate.unverifiable.join(" ")}` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const server = mcpServerFromToolCall(toolName, input, gate.blocked.keys());
|
|
131
|
+
if (!server) return { block: false };
|
|
132
|
+
const record = gate.blocked.get(server);
|
|
133
|
+
if (!record) return { block: false };
|
|
134
|
+
const explanation = record.state === "rejected"
|
|
135
|
+
? "it was rejected for this project"
|
|
136
|
+
: record.state === "changed"
|
|
137
|
+
? "its definition changed since it was approved"
|
|
138
|
+
: record.origin.startsWith("import:")
|
|
139
|
+
? `this repository imports it from ${record.origin.slice("import:".length)} config and nobody on this machine has approved it here`
|
|
140
|
+
: "this repository defines it and nobody on this machine has approved it";
|
|
141
|
+
return {
|
|
142
|
+
block: true,
|
|
143
|
+
// Named as commands, because this is read inside a session: the reader can
|
|
144
|
+
// run these where they already are, without shelling out.
|
|
145
|
+
reason:
|
|
146
|
+
`Blocked MCP server ${server}: ${explanation}. ` +
|
|
147
|
+
`Review it with \`/piagent-mcp get ${server}\`, then \`/piagent-mcp approve ${server}\` to allow it ` +
|
|
148
|
+
`or \`/piagent-mcp reject ${server}\` to refuse it.`
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parsePiModelsJson } from "./pi-models-json.mjs";
|
|
3
4
|
|
|
4
5
|
const additions = JSON.parse(fs.readFileSync(new URL("../catalog/claude-model-additions.json", import.meta.url), "utf8"));
|
|
5
6
|
const object = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -9,7 +10,11 @@ const object = value => value !== null && typeof value === "object" && !Array.is
|
|
|
9
10
|
export function prepareClaudeModelAdditions(file) {
|
|
10
11
|
if (fs.existsSync(file) && (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink())) throw new Error("Unsafe model configuration path");
|
|
11
12
|
const before = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
|
|
12
|
-
|
|
13
|
+
let data = {}, strict = true;
|
|
14
|
+
if (before !== null) {
|
|
15
|
+
try { ({ data, strict } = parsePiModelsJson(before)); }
|
|
16
|
+
catch (error) { throw new Error(`Cannot read model configuration ${file}: ${error.message}`); }
|
|
17
|
+
}
|
|
13
18
|
if (!object(data) || (data.providers !== undefined && !object(data.providers))) throw new Error("Unsupported model configuration");
|
|
14
19
|
data.providers ??= {};
|
|
15
20
|
const provider = data.providers.anthropic ?? {};
|
|
@@ -19,6 +24,8 @@ export function prepareClaudeModelAdditions(file) {
|
|
|
19
24
|
const models = provider.models ?? [];
|
|
20
25
|
const missing = additions.models.filter(model => !models.some(existing => existing.id === model.id));
|
|
21
26
|
if (missing.length === 0) return null;
|
|
27
|
+
// Rewriting would drop the member's comments; leave the file to them.
|
|
28
|
+
if (!strict) return { skipped: `${file} has comments; not adding ${missing.map(model => model.id).join(", ")} (add them by hand if wanted)`, apply() {} };
|
|
22
29
|
data.providers.anthropic = { ...provider, models: [...models, ...missing] };
|
|
23
30
|
return { apply() {
|
|
24
31
|
const current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
|
|
@@ -168,7 +168,11 @@ function parseDefaultModel(input) {
|
|
|
168
168
|
|
|
169
169
|
const parsedDefault = parseDefaultModel(defaultModelInput);
|
|
170
170
|
const settingsBefore = fs.existsSync(settingsPath) ? fs.readFileSync(settingsPath, "utf8") : null;
|
|
171
|
-
|
|
171
|
+
let settings = {};
|
|
172
|
+
if (settingsBefore !== null) {
|
|
173
|
+
try { settings = JSON.parse(settingsBefore); }
|
|
174
|
+
catch (error) { console.error(`FAIL: cannot read Pi settings ${settingsPath}: ${error.message}`); process.exit(1); }
|
|
175
|
+
}
|
|
172
176
|
|
|
173
177
|
// What Piagent last offered, so an update adds only models new to this
|
|
174
178
|
// release and never puts back one the member removed.
|
|
@@ -213,6 +217,7 @@ if (dryRun) {
|
|
|
213
217
|
console.log(` preset: ${preset}`);
|
|
214
218
|
console.log(` default: ${settings.defaultProvider}/${settings.defaultModel}:${settings.defaultThinkingLevel}`);
|
|
215
219
|
console.log(` enabledModels: ${settings.enabledModels.length}`);
|
|
220
|
+
if (catalogEdit?.skipped) console.log(` note: ${catalogEdit.skipped}`);
|
|
216
221
|
for (const note of notes) console.log(` ${note}`);
|
|
217
222
|
}
|
|
218
223
|
NODE
|
|
@@ -881,10 +881,16 @@ fi
|
|
|
881
881
|
|
|
882
882
|
if [[ "$CONFIGURE_MODEL_SCOPE" == true ]]; then
|
|
883
883
|
echo "Configuring Pi model selector scope:"
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
884
|
+
MODEL_SCOPE_ARGS=(--preset "$MODEL_SCOPE_PRESET" --default-model "$DEFAULT_MODEL")
|
|
885
|
+
if [[ "$RESET_MODEL_SCOPE" != true ]]; then
|
|
886
|
+
MODEL_SCOPE_ARGS+=(--keep-member-choices)
|
|
887
|
+
fi
|
|
888
|
+
# The model list is a convenience: a member's settings or models.json that the
|
|
889
|
+
# script cannot read must not stop the install (Pi and the package are already
|
|
890
|
+
# in place). The script restores settings.json when it fails part way.
|
|
891
|
+
if ! run_cmd bash "$PLATFORM_ROOT/scripts/configure-model-scope.sh" "${MODEL_SCOPE_ARGS[@]}"; then
|
|
892
|
+
echo "WARN: the Pi model selector scope was not changed; the install continues." >&2
|
|
893
|
+
echo " Fix the file named above, then run: piagent-model-scope ${MODEL_SCOPE_ARGS[*]}" >&2
|
|
888
894
|
fi
|
|
889
895
|
fi
|
|
890
896
|
|
|
@@ -15,11 +15,14 @@ import fs from "node:fs";
|
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { parsePiModelsJson } from "./pi-models-json.mjs";
|
|
18
19
|
|
|
19
20
|
const COMPANY_PROVIDER = /^agent_watch_/;
|
|
20
21
|
const SESSION_HEADER_EXTENSION = "agentwatch-studio-session.mjs";
|
|
21
22
|
|
|
22
23
|
const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
|
|
24
|
+
// models.json may carry comments, as Pi allows; a strict read would miss company providers in it.
|
|
25
|
+
const readModels = (file) => { try { return parsePiModelsJson(fs.readFileSync(file, "utf8")).data; } catch { return null; } };
|
|
23
26
|
const real = (file) => { try { return fs.realpathSync(file); } catch { return null; } };
|
|
24
27
|
|
|
25
28
|
// The Piagent package a path belongs to (its root), or null.
|
|
@@ -47,7 +50,7 @@ export function packageVersion(agentDir, source) {
|
|
|
47
50
|
|
|
48
51
|
export function inspect(agentDir) {
|
|
49
52
|
const settingsFile = path.join(agentDir, "settings.json"), modelsFile = path.join(agentDir, "models.json");
|
|
50
|
-
const settings = readJson(settingsFile) ?? {}, models =
|
|
53
|
+
const settings = readJson(settingsFile) ?? {}, models = readModels(modelsFile) ?? {};
|
|
51
54
|
const managed = fs.existsSync(path.join(agentDir, "agent-watch-managed.json"));
|
|
52
55
|
const packages = Array.isArray(settings.packages) ? settings.packages : [];
|
|
53
56
|
const piagentPackages = packages.filter(isPiagentSource);
|
|
@@ -109,7 +112,7 @@ export function repair(agentDir, now = new Date()) {
|
|
|
109
112
|
backups.push(backup(settingsFile, stamp)); writeJson(settingsFile, settings); changed.push(settingsFile);
|
|
110
113
|
}
|
|
111
114
|
if (kinds.has("company-providers")) {
|
|
112
|
-
const models =
|
|
115
|
+
const models = readModels(modelsFile);
|
|
113
116
|
for (const name of Object.keys(models.providers ?? {})) if (COMPANY_PROVIDER.test(name)) delete models.providers[name];
|
|
114
117
|
backups.push(backup(modelsFile, stamp)); writeJson(modelsFile, models); changed.push(modelsFile);
|
|
115
118
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Pi reads models.json leniently: a BOM, `//` line comments and trailing
|
|
2
|
+
// commas are allowed (Pi's stripJsonComments). Piagent reads it the same way,
|
|
3
|
+
// and says whether the text was plain JSON, so a writer knows it would drop
|
|
4
|
+
// the member's comments.
|
|
5
|
+
const stripComments = text => text
|
|
6
|
+
.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, m => (m[0] === '"' ? m : ""))
|
|
7
|
+
.replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : ""));
|
|
8
|
+
|
|
9
|
+
export function parsePiModelsJson(text) {
|
|
10
|
+
const body = text.replace(/^\uFEFF/, "");
|
|
11
|
+
try { return { data: JSON.parse(body), strict: true }; }
|
|
12
|
+
catch { return { data: JSON.parse(stripComments(body)), strict: false }; }
|
|
13
|
+
}
|
package/scripts/setup.sh
CHANGED
|
@@ -22,7 +22,7 @@ Options:
|
|
|
22
22
|
--project-only, --no-global Only initialize project, skip global install
|
|
23
23
|
--no-project Skip project initialization
|
|
24
24
|
--with-mcp / --no-mcp Install/skip Pi MCP adapter during global install (default: install)
|
|
25
|
-
--mcp-preset <minimal|core|popular|all|docs|browser|github|design|design-local|web|documents|google|google-mail|google-all>
|
|
25
|
+
--mcp-preset <minimal|core|popular|all|docs|browser|github|design|design-local|web|documents|google|google-mail|google-all|atlassian>
|
|
26
26
|
Configure shared global MCP baseline when --with-mcp is enabled (default: core)
|
|
27
27
|
--with-subagents / --no-subagents
|
|
28
28
|
Install/skip pi-subagents during global install (default: install)
|