@piagent/platform 1.1.9 → 1.2.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +175 -1
  2. package/README.md +2 -2
  3. package/docs/command-reference-vietnamese.md +19 -5
  4. package/docs/distribution-standard.md +7 -7
  5. package/docs/mcp-and-tools.md +154 -2
  6. package/docs/model-options.md +10 -8
  7. package/docs/operator-manual-vietnamese.md +25 -13
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/quickstart-vietnamese.md +2 -2
  10. package/docs/release-install-policy.md +6 -6
  11. package/docs/security-threat-model.md +4 -3
  12. package/docs/team-onboarding.md +4 -4
  13. package/docs/usage-observability.md +29 -0
  14. package/docs/vercel-docs-site.md +34 -4
  15. package/docs-site/architecture.html +152 -0
  16. package/docs-site/assets/docs.css +1001 -0
  17. package/docs-site/assets/docs.js +84 -0
  18. package/docs-site/commands.html +203 -0
  19. package/docs-site/content/architecture.html +29 -0
  20. package/docs-site/content/commands.html +80 -0
  21. package/docs-site/content/index.html +80 -0
  22. package/docs-site/content/mcp-approval.html +133 -0
  23. package/docs-site/content/mcp-auth.html +76 -0
  24. package/docs-site/content/mcp-servers.html +111 -0
  25. package/docs-site/content/mcp.html +120 -0
  26. package/docs-site/content/permissions.html +54 -0
  27. package/docs-site/content/profiles.html +68 -0
  28. package/docs-site/content/quickstart.html +144 -0
  29. package/docs-site/content/runtime.html +45 -0
  30. package/docs-site/content/scope.html +71 -0
  31. package/docs-site/content/security.html +51 -0
  32. package/docs-site/content/team.html +23 -0
  33. package/docs-site/content/workflows.html +47 -0
  34. package/docs-site/index.html +131 -1534
  35. package/docs-site/mcp-approval.html +256 -0
  36. package/docs-site/mcp-auth.html +199 -0
  37. package/docs-site/mcp-servers.html +234 -0
  38. package/docs-site/mcp.html +243 -0
  39. package/docs-site/permissions.html +177 -0
  40. package/docs-site/profiles.html +191 -0
  41. package/docs-site/quickstart.html +267 -0
  42. package/docs-site/runtime.html +168 -0
  43. package/docs-site/scope.html +194 -0
  44. package/docs-site/security.html +174 -0
  45. package/docs-site/team.html +142 -0
  46. package/docs-site/workflows.html +170 -0
  47. package/package.json +4 -2
  48. package/packages/piagent-core/capabilities/capability-core.js +58 -5
  49. package/packages/piagent-core/capabilities/capability-sources.js +73 -12
  50. package/packages/piagent-core/extensions/document-intake.ts +5 -5
  51. package/packages/piagent-core/extensions/piagent-guard.ts +432 -2
  52. package/packages/piagent-core/extensions/policy-core.js +1586 -98
  53. package/packages/piagent-core/mcp/mcp-approval-store.js +216 -0
  54. package/packages/piagent-core/mcp/mcp-auth-readiness.js +225 -0
  55. package/packages/piagent-core/mcp/mcp-command-actions.js +400 -0
  56. package/packages/piagent-core/mcp/mcp-config-layers.js +466 -0
  57. package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
  58. package/packages/piagent-core/mcp/mcp-server-catalog.js +150 -0
  59. package/packages/piagent-core/mcp/mcp-server-entry.js +293 -0
  60. package/packages/piagent-core/mcp/mcp-session-view.js +236 -0
  61. package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
  62. package/packages/piagent-core/package.json +7 -3
  63. package/packages/piagent-core/prompts/model-options.md +8 -8
  64. package/scripts/build-docs-site.mjs +421 -0
  65. package/scripts/configure-model-scope.sh +21 -5
  66. package/scripts/configure-subagents.sh +18 -5
  67. package/scripts/init-project.sh +15 -8
  68. package/scripts/install-global.sh +5 -5
  69. package/scripts/mcp-manage.mjs +656 -0
  70. package/scripts/pi-model-catalog.sh +11 -1
  71. package/scripts/pi-session-stats.sh +27 -1
  72. package/scripts/pi-usage-history.mjs +567 -0
  73. package/scripts/piagent-cli.mjs +1 -1
  74. package/scripts/preview-docs-site.mjs +88 -0
  75. package/scripts/quality-benchmark.sh +37 -9
  76. package/scripts/setup.sh +23 -15
  77. package/scripts/team-doctor.sh +18 -2
  78. package/scripts/uninstall-global.sh +10 -4
  79. package/scripts/verify-local.sh +65 -19
  80. package/scripts/verify-release-identity.mjs +37 -2
  81. package/scripts/configure-mcp.sh +0 -349
@@ -0,0 +1,400 @@
1
+ import {
2
+ REPOSITORY_SCOPES,
3
+ collectServers,
4
+ isRecord,
5
+ mergedServerEntry,
6
+ readMcpConfig,
7
+ writeMcpConfig
8
+ } from "./mcp-config-layers.js";
9
+ import { approvalStorePath, approvalState, clearApproval, recordApproval } from "./mcp-approval-store.js";
10
+ import { maskServerEntry } from "./mcp-server-entry.js";
11
+ import {
12
+ McpViewError,
13
+ collectServerRows,
14
+ describeServer,
15
+ formatDoctorReport,
16
+ formatServerDetail,
17
+ formatServerTable,
18
+ sessionMcpState
19
+ } from "./mcp-session-view.js";
20
+
21
+ // What `/piagent-mcp` does, separated from how a session shows it.
22
+ //
23
+ // Each action takes plain options and returns a report: a one-line notice, the
24
+ // lines to display, and the structured detail behind them. Nothing here knows
25
+ // about `pi`, a UI, or a terminal, so the menu path and the typed-subcommand
26
+ // path cannot drift into doing different things — they call the same function.
27
+
28
+ export { McpViewError };
29
+
30
+ /** Actions that only read. The rest write a config file or an approval. */
31
+ export const READ_ACTIONS = new Set(["status", "get", "doctor"]);
32
+
33
+ /** Actions that name a single server. `reset` accepts one but does not need it. */
34
+ export const SERVER_ACTIONS = new Set(["get", "approve", "reject", "reset", "enable", "disable"]);
35
+
36
+ /** Writes that belong in a shell, with the command to run there. */
37
+ export const TERMINAL_ONLY = new Map([
38
+ ["add", "piagent-mcp add <name> [--scope <scope>] --url <url> | -- <command> [args...]"],
39
+ ["remove", "piagent-mcp remove <name> --scope <scope>"],
40
+ ["preset", "piagent-mcp --preset <core|popular|design> --scope <scope>"]
41
+ ]);
42
+
43
+ export const HELP_LINES = [
44
+ "/piagent-mcp open the menu",
45
+ "/piagent-mcp status every configured server, with scope and readiness",
46
+ "/piagent-mcp get <name> one server in detail, credential values masked",
47
+ "/piagent-mcp doctor what each server still needs before it can answer",
48
+ "/piagent-mcp approve <name> allow a server this repository defines",
49
+ "/piagent-mcp reject <name> refuse it",
50
+ "/piagent-mcp reset [name] forget the decision, or every decision",
51
+ "/piagent-mcp enable|disable <name>",
52
+ "",
53
+ "Adding, removing and presets are terminal commands: run `piagent-mcp --help`.",
54
+ "Live connection state and OAuth sign-in belong to the adapter: /mcp and /mcp-auth."
55
+ ];
56
+
57
+ /**
58
+ * The menu, built from what this project actually has.
59
+ *
60
+ * An entry is offered only when it would do something: approving is not offered
61
+ * when no server here needs a decision, and a menu that lists impossible actions
62
+ * teaches the reader to stop reading it. Counts go in the labels so the menu
63
+ * answers part of the question before anything is picked.
64
+ *
65
+ * @param {{projectPath: string}} options
66
+ */
67
+ export function menuOptions(options) {
68
+ const state = sessionMcpState({ projectPath: options.projectPath });
69
+ const { rows, blocked: stuck } = state;
70
+ // Not `REPOSITORY_SCOPES.has(row.scope)`: a repository-relative import
71
+ // declared by a `global` config produces a row whose scope is `global` and
72
+ // whose definition came out of the clone. Those were the rows the menu never
73
+ // offered to approve, so the only route to a decision was knowing to type the
74
+ // subcommand -- for exactly the servers whose origin is hardest to see.
75
+ const repositoryRows = rows.filter((row) => row.requiresApproval);
76
+ const decisions = repositoryRows.map((row) => ({
77
+ row,
78
+ state: approvalState({ projectPath: options.projectPath, name: row.name, entry: row.entry }).state
79
+ }));
80
+ const undecided = decisions.filter((item) => item.state !== "approved");
81
+ const decided = decisions.filter((item) => item.state !== "pending");
82
+ const disabled = rows.filter((row) => row.readiness.state === "disabled");
83
+ const enabled = rows.filter((row) => row.readiness.state !== "disabled");
84
+
85
+ const entries = [
86
+ {
87
+ value: "status",
88
+ label: state.usable
89
+ ? (rows.length === 0 ? "Servers — none configured" : `Servers — ${rows.length - stuck.length}/${rows.length} ready`)
90
+ : "Servers — every tool call blocked",
91
+ description: state.usable
92
+ ? "Every server visible from this project, with its scope and state"
93
+ : "Config here leaves nothing to check an approval against"
94
+ }
95
+ ];
96
+ // Ahead of everything per-server: while this stands, no decision about an
97
+ // individual server changes whether anything runs.
98
+ if (!state.usable) {
99
+ entries.push({
100
+ value: "doctor",
101
+ label: `Doctor — ${state.blockingConfig.length} config problem${state.blockingConfig.length === 1 ? "" : "s"} blocking every call`,
102
+ description: "Which file to edit before any server can answer",
103
+ recommended: true
104
+ });
105
+ } else if (stuck.length > 0) {
106
+ entries.push({
107
+ value: "doctor",
108
+ label: `Doctor — ${stuck.length} need attention`,
109
+ description: "What each blocked server still needs",
110
+ recommended: true
111
+ });
112
+ } else if (rows.length > 0) {
113
+ entries.push({ value: "doctor", label: "Doctor — all reachable", description: "Re-check what each server needs" });
114
+ }
115
+ if (rows.length > 0) {
116
+ entries.push({ value: "get", label: "Inspect a server", description: "Full definition, credential values masked" });
117
+ }
118
+ if (undecided.length > 0) {
119
+ entries.push({
120
+ value: "approve",
121
+ label: `Approve a repository server — ${undecided.length} waiting`,
122
+ description: "This repository defines it; nobody on this machine has allowed it yet",
123
+ recommended: stuck.length === 0
124
+ });
125
+ entries.push({ value: "reject", label: "Reject a repository server", description: "Refuse it for this project" });
126
+ } else if (repositoryRows.length > 0) {
127
+ entries.push({ value: "reject", label: "Reject a repository server", description: "Withdraw a server this project had allowed" });
128
+ }
129
+ if (decided.length > 0) {
130
+ entries.push({ value: "reset", label: "Forget a decision", description: "Return a server to pending so it is asked again" });
131
+ }
132
+ if (enabled.length > 0) {
133
+ entries.push({ value: "disable", label: "Turn a server off", description: "Stop using it without losing its definition" });
134
+ }
135
+ if (disabled.length > 0) {
136
+ entries.push({ value: "enable", label: `Turn a server back on — ${disabled.length} off`, description: "Restore a disabled server" });
137
+ }
138
+ entries.push({ value: "add", label: "Add a server", description: "Shows the terminal command; writing config is not a slash command" });
139
+ entries.push({ value: "help", label: "All subcommands", description: "Everything /piagent-mcp accepts" });
140
+ return { entries, rows };
141
+ }
142
+
143
+ /**
144
+ * Servers an action can be applied to. Approving a server the repository does
145
+ * not define is not a thing, so it is not offered.
146
+ *
147
+ * @param {{projectPath: string, action: string}} options
148
+ */
149
+ export function serverChoices(options) {
150
+ const { projectPath, action } = options;
151
+ const rows = collectServerRows({ projectPath });
152
+ const filtered = rows.filter((row) => {
153
+ if (action === "approve" || action === "reject" || action === "reset") return row.requiresApproval;
154
+ // A server reached through `imports` lives in a file this command does not
155
+ // own. Offering it here produced a write against the declaring layer that
156
+ // reported success and changed nothing the adapter reads.
157
+ if (action === "enable" || action === "disable") {
158
+ if (row.origin?.startsWith("import:")) return false;
159
+ }
160
+ if (action === "enable") return row.readiness.state === "disabled";
161
+ if (action === "disable") return row.readiness.state !== "disabled";
162
+ return true;
163
+ });
164
+ return filtered.map((row) => ({
165
+ value: row.name,
166
+ label: `${row.name} (${row.scope})`,
167
+ description: row.readiness.detail
168
+ }));
169
+ }
170
+
171
+ /** @param {{projectPath: string, scope?: string}} options */
172
+ export function status(options) {
173
+ const state = sessionMcpState({
174
+ projectPath: options.projectPath,
175
+ scopes: options.scope ? [options.scope] : undefined
176
+ });
177
+ const { rows, blocked: stuck } = state;
178
+ // Reported before the per-server table, and it decides the headline. A status
179
+ // that says "0 servers configured" in a session where every tool call is being
180
+ // refused is not a smaller version of the truth, it points at the wrong file.
181
+ const blocking = state.blockingConfig.map((problem) => `BLOCKED every tool call: ${problem.detail}`);
182
+ const summary = !state.usable
183
+ ? "MCP: every tool call blocked by config"
184
+ : rows.length === 0
185
+ ? "MCP: no servers configured"
186
+ : `MCP: ${rows.length - stuck.length}/${rows.length} ready`;
187
+ return {
188
+ notify: { message: summary, level: state.usable && stuck.length === 0 ? "info" : "warning" },
189
+ lines: [
190
+ ...blocking,
191
+ ...(blocking.length > 0 ? [""] : []),
192
+ ...(rows.length === 0
193
+ ? (state.usable
194
+ ? ["No MCP servers configured.", "Seed the pinned baseline in a terminal: piagent-mcp --preset core --scope global"]
195
+ : [])
196
+ : [formatServerTable(rows), ...(stuck.length > 0 ? ["", `${stuck.length} not usable yet. Run /piagent-mcp doctor.`] : [])])
197
+ ],
198
+ details: {
199
+ project: options.projectPath,
200
+ usable: state.usable,
201
+ blockingConfig: state.blockingConfig,
202
+ servers: rows.map((row) => ({
203
+ name: row.name,
204
+ scope: row.scope,
205
+ origin: row.origin,
206
+ requiresApproval: row.requiresApproval,
207
+ state: row.readiness.state,
208
+ detail: row.readiness.detail
209
+ }))
210
+ }
211
+ };
212
+ }
213
+
214
+ /** @param {{projectPath: string, name: string, scope?: string}} options */
215
+ export function detail(options) {
216
+ const found = describeServer({
217
+ projectPath: options.projectPath,
218
+ name: options.name,
219
+ scope: options.scope,
220
+ listHint: "/piagent-mcp",
221
+ scopeHint: "--scope"
222
+ });
223
+ return {
224
+ notify: { message: `MCP ${options.name}: ${found.readiness.state}`, level: "info" },
225
+ lines: formatServerDetail(found, { inSession: true }),
226
+ details: { name: options.name, scope: found.scope, file: found.file, state: found.readiness.state, entry: found.masked }
227
+ };
228
+ }
229
+
230
+ /**
231
+ * The Docker daemon probe is deliberately absent: it spawns a process, and this
232
+ * runs inside the session's own turn. `piagent-mcp doctor` in a terminal does it.
233
+ *
234
+ * @param {{projectPath: string}} options
235
+ */
236
+ export function doctor(options) {
237
+ const state = sessionMcpState({ projectPath: options.projectPath });
238
+ const report = formatDoctorReport(state.rows, { rerun: "/piagent-mcp doctor", inSession: true });
239
+ // A layer nobody can parse counts as a problem: it is hiding whatever it
240
+ // defines. An import declaration does not — the servers it brings in are in
241
+ // the table above with their own approval state — but it is named, because
242
+ // reading the repository's config alone would not reveal where they came from.
243
+ const problems = report.problems + state.unreadable.length + state.blockingConfig.length;
244
+ return {
245
+ notify: {
246
+ message: problems === 0 ? "MCP: every server can be reached" : `MCP: ${problems} need attention`,
247
+ level: problems === 0 ? "info" : "warning"
248
+ },
249
+ lines: [
250
+ // First. No per-server remedy below is worth acting on while one of these
251
+ // stands, because none of them makes a tool call succeed.
252
+ ...state.blockingConfig.map((problem) => `BLOCKED every tool call: ${problem.detail}`),
253
+ ...(state.blockingConfig.length > 0 ? [""] : []),
254
+ ...report.lines,
255
+ ...state.unreadable.map((layer) => `Unreadable ${layer.scope} config: ${layer.detail}`),
256
+ ...state.imports.map((layer) =>
257
+ `${layer.file} imports servers from ${layer.kinds.join(", ")} config; ` +
258
+ "they need approval here like any server the repository defines."),
259
+ ...(problems === 0 ? [] : ["", "The Docker daemon check is terminal-only: piagent-mcp doctor"])
260
+ ],
261
+ details: {
262
+ project: options.projectPath,
263
+ problems,
264
+ usable: state.usable,
265
+ blockingConfig: state.blockingConfig,
266
+ unreadableLayers: state.unreadable.map((layer) => layer.scope),
267
+ repositoryImports: state.imports.map((layer) => ({ scope: layer.scope, kinds: layer.kinds }))
268
+ }
269
+ };
270
+ }
271
+
272
+ /** @param {{projectPath: string, name: string, decision: "approved"|"rejected"}} options */
273
+ export function decide(options) {
274
+ const { projectPath, name, decision } = options;
275
+ const matches = collectServers({ projectPath, scopes: [...REPOSITORY_SCOPES] }).filter((server) => server.name === name);
276
+ if (matches.length === 0) {
277
+ throw new McpViewError(
278
+ `${name} is not defined by this repository. Approval only applies to servers in ${[...REPOSITORY_SCOPES].join(" or ")} scope.`
279
+ );
280
+ }
281
+ if (matches.length > 1) {
282
+ throw new McpViewError(`${name} is defined in ${matches.map((server) => server.scope).join(" and ")}; remove one.`);
283
+ }
284
+
285
+ const server = matches[0];
286
+ const before = approvalState({ projectPath, name, entry: server.entry });
287
+ // Approving without reading it is the failure this gate exists to prevent, so
288
+ // the definition is put in front of the operator alongside the decision.
289
+ //
290
+ // What is shown is the merged entry, not this layer's fragment. The adapter
291
+ // merges a same-named server key by key across layers, so a repository that
292
+ // declares only `{"args": ["--evil"]}` gets the command from a lower layer --
293
+ // and a preview of the fragment showed the operator an argument list with no
294
+ // command attached while consenting to a full command line.
295
+ const preview = decision === "approved" && before.state !== "approved"
296
+ ? [JSON.stringify(maskServerEntry(mergedServerEntry({ projectPath, name, entry: server.entry })), null, 2), ""]
297
+ : [];
298
+ // A decision that was not written is not a decision. Reporting success here
299
+ // would leave the operator believing a server is approved while the gate keeps
300
+ // blocking it, or worse, believing one is rejected while nothing recorded it.
301
+ if (!recordApproval({ projectPath, name, entry: server.entry, decision })) {
302
+ throw new McpViewError(
303
+ `could not write ${approvalStorePath() ?? "the approval store"}; ${name} is still ${before.state}`
304
+ );
305
+ }
306
+ const after = approvalState({ projectPath, name, entry: server.entry });
307
+ return {
308
+ notify: { message: `MCP ${name}: ${decision}`, level: "info" },
309
+ lines: [
310
+ ...preview,
311
+ `${decision === "approved" ? "Approved" : "Rejected"} MCP server ${name} (${server.scope}) for ${projectPath}`,
312
+ `Pinned to this definition: ${after.digest}`,
313
+ "Changing the server's command, URL or arguments returns it to pending."
314
+ ],
315
+ details: { name, scope: server.scope, decision, digest: after.digest, previous: before.state }
316
+ };
317
+ }
318
+
319
+ /** @param {{projectPath: string, name?: string}} options */
320
+ export function reset(options) {
321
+ const { projectPath, name } = options;
322
+ if (!clearApproval({ projectPath, name })) {
323
+ throw new McpViewError(`could not write ${approvalStorePath() ?? "the approval store"}; nothing was forgotten`);
324
+ }
325
+ return {
326
+ notify: { message: name ? `MCP ${name}: decision forgotten` : "MCP: all decisions forgotten", level: "info" },
327
+ lines: [name
328
+ ? `Forgot the decision for ${name} in ${projectPath}`
329
+ : `Forgot every MCP approval decision for ${projectPath}`],
330
+ details: { name: name ?? null, decision: "reset" }
331
+ };
332
+ }
333
+
334
+ /**
335
+ * A server this command has no file of its own to edit: it was read out of
336
+ * another tool's config through `imports`. The write used to land in whichever
337
+ * layer declared the import, where the adapter never looks for it, so the
338
+ * command reported the server disabled and the next tool call still reached it.
339
+ *
340
+ * @param {{name: string, origin?: string, scope: string, file: string}} found
341
+ * @param {string} action
342
+ */
343
+ function assertOwnedByThisTool(found, action) {
344
+ if (!found.origin?.startsWith("import:")) return;
345
+ const kind = found.origin.slice("import:".length);
346
+ throw new McpViewError(
347
+ `${found.name} is not defined by any config this command owns. The ${found.scope} config imports ${kind}, ` +
348
+ `and the definition lives in ${found.file}. Edit that file to ${action} it, or drop "${kind}" from the imports list.`
349
+ );
350
+ }
351
+
352
+ /** @param {{projectPath: string, name: string, scope?: string, enabled: boolean}} options */
353
+ export function toggle(options) {
354
+ const { projectPath, name, scope, enabled } = options;
355
+ const found = describeServer({ projectPath, name, scope, listHint: "/piagent-mcp", scopeHint: "--scope" });
356
+ assertOwnedByThisTool(found, enabled ? "enable" : "disable");
357
+ const config = readMcpConfig(found.file);
358
+ const entry = { ...(isRecord(config.mcpServers[name]) ? config.mcpServers[name] : {}) };
359
+ if (enabled) delete entry.enabled;
360
+ else entry.enabled = false;
361
+ config.mcpServers[name] = entry;
362
+ writeMcpConfig(found.file, config);
363
+ return {
364
+ notify: { message: `MCP ${name}: ${enabled ? "enabled" : "disabled"}`, level: "info" },
365
+ lines: [
366
+ `${enabled ? "Enabled" : "Disabled"} MCP server ${name} in ${found.scope} scope`,
367
+ `File modified: ${found.file}`,
368
+ ...(enabled ? [] : ["The definition is kept, so /piagent-mcp enable restores it."]),
369
+ "The adapter reads this at startup: run /reload for it to take effect in this session."
370
+ ],
371
+ details: { name, scope: found.scope, file: found.file, enabled }
372
+ };
373
+ }
374
+
375
+ /** @param {string} action */
376
+ export function terminalOnly(action) {
377
+ const command = TERMINAL_ONLY.get(action);
378
+ if (!command) return undefined;
379
+ return {
380
+ notify: { message: `MCP: ${action} is a terminal command`, level: "warning" },
381
+ // Why this line sits where it does belongs in the docs. What an operator
382
+ // needs here is the command to run and the reason it is not this one.
383
+ lines: [
384
+ `${action} writes a server definition, so it stays in the terminal:`,
385
+ ` ${command}`,
386
+ "",
387
+ "Shell quoting, ${VAR} references and a `--` command line are a shell's job.",
388
+ "Re-reading them here would be a second, worse parser."
389
+ ],
390
+ details: { action, command }
391
+ };
392
+ }
393
+
394
+ export function help() {
395
+ return {
396
+ notify: { message: "MCP: subcommands", level: "info" },
397
+ lines: HELP_LINES,
398
+ details: { actions: [...READ_ACTIONS, ...SERVER_ACTIONS] }
399
+ };
400
+ }