@piagent/platform 1.46.4 → 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.
Files changed (71) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/company-dev-commands.md +2 -1
  4. package/docs/distribution-standard.md +6 -6
  5. package/docs/mcp-and-tools.md +21 -1
  6. package/docs/operator-manual-vietnamese.md +7 -7
  7. package/docs/publishing-for-teams.md +1 -0
  8. package/docs/release-install-policy.md +2 -2
  9. package/docs/team-onboarding.md +2 -2
  10. package/docs-site/architecture.html +1 -1
  11. package/docs-site/assets/search-en.json +1 -1
  12. package/docs-site/assets/search-vi.json +1 -1
  13. package/docs-site/benchmark.html +1 -1
  14. package/docs-site/commands.html +1 -1
  15. package/docs-site/content/en/mcp.html +8 -0
  16. package/docs-site/content/mcp.html +8 -0
  17. package/docs-site/context-engine.html +1 -1
  18. package/docs-site/ecosystem.html +1 -1
  19. package/docs-site/en/architecture.html +1 -1
  20. package/docs-site/en/benchmark.html +1 -1
  21. package/docs-site/en/commands.html +1 -1
  22. package/docs-site/en/context-engine.html +1 -1
  23. package/docs-site/en/ecosystem.html +1 -1
  24. package/docs-site/en/index.html +9 -9
  25. package/docs-site/en/mcp-approval.html +1 -1
  26. package/docs-site/en/mcp-auth.html +1 -1
  27. package/docs-site/en/mcp-servers.html +1 -1
  28. package/docs-site/en/mcp.html +9 -1
  29. package/docs-site/en/permissions.html +1 -1
  30. package/docs-site/en/profiles.html +1 -1
  31. package/docs-site/en/quickstart.html +1 -1
  32. package/docs-site/en/runtime.html +1 -1
  33. package/docs-site/en/scope.html +1 -1
  34. package/docs-site/en/security.html +1 -1
  35. package/docs-site/en/team.html +1 -1
  36. package/docs-site/en/update.html +3 -3
  37. package/docs-site/en/whats-new.html +29 -17
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +9 -9
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +9 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/releases.json +36 -0
  49. package/docs-site/runtime.html +1 -1
  50. package/docs-site/scope.html +1 -1
  51. package/docs-site/security.html +1 -1
  52. package/docs-site/team.html +1 -1
  53. package/docs-site/update.html +3 -3
  54. package/docs-site/whats-new.html +29 -17
  55. package/docs-site/windows.html +1 -1
  56. package/docs-site/workflows.html +1 -1
  57. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  58. package/package.json +1 -1
  59. package/packages/piagent-core/extensions/piagent-guard.ts +2 -146
  60. package/packages/piagent-core/managed/command-tools.mjs +17 -3
  61. package/packages/piagent-core/managed/member-mcp.mjs +71 -0
  62. package/packages/piagent-core/managed/memory-lessons.mjs +1 -1
  63. package/packages/piagent-core/managed/resource-loader.mjs +19 -7
  64. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +4 -0
  65. package/packages/piagent-core/managed/session.mjs +3 -3
  66. package/packages/piagent-core/managed/simulator-commands.mjs +117 -0
  67. package/packages/piagent-core/mcp/mcp-server-catalog.js +18 -1
  68. package/packages/piagent-core/package.json +1 -1
  69. package/packages/piagent-core/runtime/mcp-repository-gate.d.mts +10 -0
  70. package/packages/piagent-core/runtime/mcp-repository-gate.mjs +150 -0
  71. package/scripts/setup.sh +1 -1
@@ -0,0 +1,117 @@
1
+ import path from 'node:path';
2
+
3
+ // What run_on_simulator runs on the member's machine: the iOS Simulator and
4
+ // Android emulator, and the builds that install an app on them (the sandbox
5
+ // reaches neither CoreSimulator nor adb). Only these programs, joined by &&,
6
+ // || or a pipe into an output filter; output may go to a file in the project
7
+ // (a screenshot), nothing else leaves it. Anything wider is run_on_host.
8
+
9
+ const XCRUN = new Set(['simctl', 'xcodebuild', 'devicectl', 'xcresulttool', 'xctrace']);
10
+ // `simctl spawn` starts a program of the simulator's runtime as the member:
11
+ // only `log` (the app's logs).
12
+ const simctl = words => words[1] !== 'simctl' || words[2] !== 'spawn' || words.slice(3).filter(word => !word.startsWith('-'))[1] === 'log';
13
+ const NPX = /^(expo|react-native|@react-native-community\/cli|cap|@capacitor\/cli)(@[\w.-]+)?$/;
14
+ // Program → whether its arguments are allowed (`words` without assignments,
15
+ // `inProject` a path check against the project folder).
16
+ const PROGRAMS = {
17
+ xcrun: words => XCRUN.has(words[1]) && simctl(words),
18
+ xcodebuild: () => true,
19
+ open: words => words.length === 3 && words[1] === '-a' && words[2] === 'Simulator',
20
+ adb: () => true, emulator: () => true, avdmanager: () => true, sdkmanager: () => true,
21
+ gradlew: (words, inProject) => inProject(words[0]),
22
+ // Taps, typing and the screen's accessibility tree; `axe init` writes into other tools' settings.
23
+ axe: words => !!words[1] && words[1] !== 'init',
24
+ xcodegen: words => words[1] === 'generate',
25
+ flutter: () => true,
26
+ pod: words => words[1] === 'install',
27
+ npx: words => NPX.test(words[1] === '--yes' || words[1] === '-y' ? words[2] ?? '' : words[1] ?? ''),
28
+ // Waiting for a boot, and reading what a build printed.
29
+ sleep: words => words.length === 2 && /^\d{1,3}$/.test(words[1]),
30
+ xcbeautify: () => true, xcpretty: () => true,
31
+ grep: () => true, head: () => true, tail: () => true, wc: () => true, sort: () => true, uniq: () => true, jq: () => true,
32
+ cd: (words, inProject) => words.length === 2 && inProject(words[1]),
33
+ mkdir: (words, inProject) => { const paths = words.slice(1).filter(word => word !== '-p'); return paths.length > 0 && paths.every(inProject); },
34
+ };
35
+ // Variables a command may set for itself; never what changes how programs load.
36
+ const ASSIGNMENT = /^([A-Z][A-Z0-9_]*)=/;
37
+ const UNSAFE_VARIABLE = /^(PATH|DYLD_\w*|LD_\w*|NODE_OPTIONS|BASH_ENV|ENV|ZDOTDIR|SHELL|HOME)$/;
38
+
39
+ // A path in the project folder: relative without climbing out of it, or
40
+ // absolute inside it (agents name files by their full path). Without the
41
+ // folder, only a relative one.
42
+ function insideProject(cwd) {
43
+ return target => {
44
+ if (typeof target !== 'string' || !target || target.startsWith('~') || target.includes('$') || target.split('/').includes('..')) return false;
45
+ if (!target.startsWith('/')) return true;
46
+ if (!cwd) return false;
47
+ const relative = path.relative(path.resolve(cwd), path.resolve(target));
48
+ return !relative.startsWith('..') && !path.isAbsolute(relative);
49
+ };
50
+ }
51
+
52
+ // Shell words with their quotes resolved, and the operators between them.
53
+ // null for what this reader does not follow (an unclosed quote).
54
+ function tokens(command) {
55
+ const out = []; let word = null, quote = null;
56
+ const push = () => { if (word !== null) { out.push({ word }); word = null; } };
57
+ for (let i = 0; i < command.length; i += 1) {
58
+ const c = command[i];
59
+ if (quote) { if (c === quote) quote = null; else if (c === '\\' && quote === '"' && i + 1 < command.length) word += command[++i]; else word += c; continue; }
60
+ if (c === "'" || c === '"') { quote = c; word ??= ''; continue; }
61
+ if (c === '\\' && i + 1 < command.length) { word = (word ?? '') + command[++i]; continue; }
62
+ if (/\s/.test(c)) { push(); continue; }
63
+ const two = command.slice(i, i + 2);
64
+ if (['&&', '||', '>>', '2>'].includes(two)) {
65
+ push();
66
+ if (two === '2>' && command[i + 2] === '&' && command[i + 3] === '1') { out.push({ op: '2>&1' }); i += 3; continue; }
67
+ out.push({ op: two }); i += 1; continue;
68
+ }
69
+ if (c === '|' || c === '>') { push(); out.push({ op: c }); continue; }
70
+ word = (word ?? '') + c;
71
+ }
72
+ if (quote) return null;
73
+ push();
74
+ return out;
75
+ }
76
+
77
+ // Why the command is not a simulator command, or null when it is. `cwd`:
78
+ // the project folder, where output files and `cd` may go.
79
+ export function simulatorCommandReason(command, cwd = null) {
80
+ const text = String(command ?? ''), inProject = insideProject(cwd);
81
+ if (!text.trim()) return 'the command is empty';
82
+ if (/[`;\n]|\$\(|<\(|>\(|</.test(text) || text.replaceAll('&&', '').replaceAll('2>&1', '').includes('&')) return 'only commands joined by &&, || or | are run (no ;, &, $( ), backticks or subshells)';
83
+ const list = tokens(text);
84
+ if (!list) return 'an unclosed quote';
85
+ let words = [];
86
+ const close = () => {
87
+ if (!words.length) return 'an empty part between operators';
88
+ while (words.length && ASSIGNMENT.test(words[0])) {
89
+ const name = ASSIGNMENT.exec(words[0])[1];
90
+ if (UNSAFE_VARIABLE.test(name)) return `${name} cannot be set here`;
91
+ words = words.slice(1);
92
+ }
93
+ const name = words[0]?.split('/').pop();
94
+ const allowed = Object.hasOwn(PROGRAMS, name) && PROGRAMS[name](words, inProject);
95
+ words = [];
96
+ return allowed ? null : `${name ?? 'this part'} is not a simulator or app build command (use run_on_host for other commands)`;
97
+ };
98
+ for (let i = 0; i < list.length; i += 1) {
99
+ const { word, op } = list[i];
100
+ if (word !== undefined) { words.push(word); continue; }
101
+ if (op === '2>&1') continue;
102
+ if (op === '>' || op === '>>' || op === '2>') {
103
+ const target = list[i + 1]?.word;
104
+ if (!(target === '/dev/null' || inProject(target))) return 'output may go only to a file inside the project or /dev/null';
105
+ i += 1; continue;
106
+ }
107
+ const reason = close(); if (reason) return reason;
108
+ }
109
+ return close();
110
+ }
111
+
112
+ // A lone `emulator …` keeps running while the virtual device is up: it is
113
+ // started detached and the tool returns once it is launched.
114
+ export function detachedEmulator(command) {
115
+ const list = tokens(String(command ?? ''));
116
+ return !!list && list.length > 0 && list.every(token => token.word !== undefined) && list[0].word.split('/').pop() === 'emulator';
117
+ }
@@ -151,6 +151,18 @@ export const CATALOG_SERVERS = {
151
151
  directTools: false
152
152
  }
153
153
  },
154
+ // Atlassian's own remote server (Jira and Confluence Cloud). It registers its
155
+ // OAuth client itself (the metadata sits at the origin, not at the protected
156
+ // resource), so the member signs in with /mcp-auth and their own Jira access.
157
+ atlassian: {
158
+ description: "Atlassian Rovo MCP server: Jira and Confluence Cloud, with the member's own Atlassian login (OAuth).",
159
+ config: {
160
+ url: "https://mcp.atlassian.com/v1/mcp",
161
+ auth: "oauth",
162
+ lifecycle: "lazy",
163
+ directTools: false
164
+ }
165
+ },
154
166
  // Converts .docx, .xlsx, .pptx and .pdf to Markdown locally. Local matters:
155
167
  // the alternative to this is uploading a document somewhere to read it, and a
156
168
  // document worth converting is usually one worth not uploading.
@@ -202,6 +214,10 @@ export const CATALOG_PREREQUISITES = {
202
214
  "Developer Preview: no Pi OAuth end-to-end release proof exists yet",
203
215
  "Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified"
204
216
  ],
217
+ atlassian: [
218
+ "a Jira or Confluence Cloud site (Server and Data Center are not served by this endpoint)",
219
+ "sign in once with /mcp-auth atlassian; a site admin may have to allow the Atlassian MCP connection"
220
+ ],
205
221
  markitdown: [
206
222
  "uv on PATH (https://docs.astral.sh/uv); the first run downloads a Python runtime and roughly 120 MB of dependencies",
207
223
  "note: markitdown-mcp is published as an alpha, so pin changes deliberately"
@@ -244,11 +260,12 @@ export const CATALOG_PRESETS = {
244
260
  "google-mail": ["gmail"],
245
261
  google: ["google-drive", "google-docs", "google-sheets"],
246
262
  "google-all": ["google-drive", "gmail", "google-docs", "google-sheets"],
263
+ atlassian: ["atlassian"],
247
264
  core: ["context7", "chrome-devtools", "github"],
248
265
  popular: ["context7", "chrome-devtools", "playwright", "github", "figma-desktop"],
249
266
  all: [
250
267
  "context7", "chrome-devtools", "playwright", "github", "figma-desktop", "figma",
251
- "markitdown", "google-drive", "gmail", "google-docs", "google-sheets"
268
+ "markitdown", "atlassian", "google-drive", "gmail", "google-docs", "google-sheets"
252
269
  ]
253
270
  };
254
271
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.46.4",
3
+ "version": "1.47.0",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -0,0 +1,10 @@
1
+ export interface RepositoryMcpGate {
2
+ blocked: Map<string, { state: string; origin: string }>;
3
+ serverNames: Set<string>;
4
+ unverifiable: string[];
5
+ }
6
+
7
+ export const mcpApprovalCache: Map<string, { signature: string; gate: RepositoryMcpGate }>;
8
+ export function mcpServerFromToolCall(toolName: string, input: Record<string, unknown>, candidates: Iterable<string>): string | undefined;
9
+ export function repositoryMcpGate(cwd: string): RepositoryMcpGate;
10
+ export function evaluateMcpApproval(cwd: string, toolName: string, input: Record<string, unknown>): { block: boolean; reason?: string };
@@ -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
+ }
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)