@piagent/platform 1.2.0 → 1.2.2
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 +149 -1
- package/README.md +1 -1
- package/catalog/capabilities.json +4 -4
- package/docs/command-reference-vietnamese.md +15 -4
- package/docs/context-window-policy.md +1 -0
- package/docs/distribution-standard.md +7 -7
- package/docs/mcp-and-tools.md +49 -3
- package/docs/operator-manual-vietnamese.md +52 -11
- package/docs/publishing-for-teams.md +2 -0
- package/docs/quickstart-vietnamese.md +4 -2
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +4 -3
- package/docs/team-onboarding.md +3 -3
- package/docs/usage-observability.md +49 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/mcp-approval.html +54 -2
- package/docs-site/content/quickstart.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +55 -3
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +2 -2
- 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/workflows.html +1 -1
- package/package.json +1 -1
- package/packages/piagent-core/README.md +2 -0
- package/packages/piagent-core/capabilities/capability-core.js +41 -4
- package/packages/piagent-core/capabilities/capability-sources.js +73 -12
- package/packages/piagent-core/extensions/piagent-guard.ts +539 -70
- package/packages/piagent-core/extensions/policy-core.js +1586 -98
- package/packages/piagent-core/mcp/mcp-approval-store.js +56 -15
- package/packages/piagent-core/mcp/mcp-auth-readiness.js +31 -3
- package/packages/piagent-core/mcp/mcp-command-actions.js +139 -30
- package/packages/piagent-core/mcp/mcp-config-layers.js +339 -6
- package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
- package/packages/piagent-core/mcp/mcp-server-entry.js +51 -0
- package/packages/piagent-core/mcp/mcp-session-view.js +55 -4
- package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/prompts/be-to-fe.md +1 -0
- package/packages/piagent-core/prompts/piagent-commands.md +1 -0
- package/packages/piagent-core/prompts/platform-improve.md +1 -0
- package/packages/piagent-core/prompts/review.md +1 -0
- package/packages/piagent-core/prompts/scout.md +1 -0
- package/packages/piagent-core/prompts/task.md +1 -0
- package/scripts/configure-model-scope.sh +19 -3
- package/scripts/configure-subagents.sh +17 -4
- package/scripts/init-project.sh +15 -8
- package/scripts/mcp-manage.mjs +111 -22
- package/scripts/pi-model-catalog.sh +11 -1
- package/scripts/pi-session-stats.sh +27 -1
- package/scripts/pi-usage-history.mjs +567 -0
- package/scripts/quality-benchmark.sh +37 -9
- package/scripts/setup.sh +21 -13
- package/scripts/uninstall-global.sh +10 -4
- package/scripts/verify-local.sh +19 -0
- package/scripts/verify-release-identity.mjs +29 -0
package/scripts/mcp-manage.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
collectServers,
|
|
11
11
|
configPathForScope,
|
|
12
12
|
isRecord,
|
|
13
|
+
mergedServerEntry,
|
|
13
14
|
readMcpConfig,
|
|
14
15
|
writeMcpConfig
|
|
15
16
|
} from "../packages/piagent-core/mcp/mcp-config-layers.js";
|
|
@@ -28,18 +29,18 @@ import {
|
|
|
28
29
|
} from "../packages/piagent-core/mcp/mcp-server-entry.js";
|
|
29
30
|
import {
|
|
30
31
|
approvalState,
|
|
32
|
+
approvalStorePath,
|
|
31
33
|
clearApproval,
|
|
32
34
|
recordApproval
|
|
33
35
|
} from "../packages/piagent-core/mcp/mcp-approval-store.js";
|
|
34
36
|
import { evaluateServerReadiness } from "../packages/piagent-core/mcp/mcp-auth-readiness.js";
|
|
35
37
|
import {
|
|
36
38
|
McpViewError,
|
|
37
|
-
blockedRows,
|
|
38
|
-
collectServerRows,
|
|
39
39
|
describeServer,
|
|
40
40
|
formatDoctorReport,
|
|
41
41
|
formatServerDetail,
|
|
42
42
|
formatServerTable,
|
|
43
|
+
sessionMcpState,
|
|
43
44
|
usesDocker
|
|
44
45
|
} from "../packages/piagent-core/mcp/mcp-session-view.js";
|
|
45
46
|
|
|
@@ -214,6 +215,25 @@ function locateServer(name, flags) {
|
|
|
214
215
|
return servers[0];
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
/**
|
|
219
|
+
* A server this command has no file of its own to edit: it was read out of
|
|
220
|
+
* another tool's config through `imports`. `located.file` is that other tool's
|
|
221
|
+
* config, and writing it back through `writeMcpConfig` would rewrite somebody's
|
|
222
|
+
* Cursor or VS Code file in this platform's format -- adding a `settings` block
|
|
223
|
+
* it does not use and dropping every key this shape does not carry.
|
|
224
|
+
*
|
|
225
|
+
* @param {{name: string, origin?: string, scope: string, file: string}} located
|
|
226
|
+
* @param {string} action
|
|
227
|
+
*/
|
|
228
|
+
function assertOwnedByThisTool(located, action) {
|
|
229
|
+
if (!located.origin?.startsWith("import:")) return;
|
|
230
|
+
const kind = located.origin.slice("import:".length);
|
|
231
|
+
throw new UsageError(
|
|
232
|
+
`${located.name} is not defined by any config this command owns. The ${located.scope} config imports ${kind}, ` +
|
|
233
|
+
`and the definition lives in ${located.file}. Edit that file to ${action} it, or drop "${kind}" from its imports list.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
217
237
|
// --- commands ---------------------------------------------------------------
|
|
218
238
|
|
|
219
239
|
/** @param {ReturnType<typeof parseArgs>} args */
|
|
@@ -277,12 +297,21 @@ function commandRemove(args) {
|
|
|
277
297
|
const name = args.positionals[0];
|
|
278
298
|
if (!name) throw new UsageError("remove needs a server name");
|
|
279
299
|
const located = locateServer(name, args.flags);
|
|
300
|
+
assertOwnedByThisTool(located, "remove");
|
|
280
301
|
const config = readMcpConfig(located.file);
|
|
281
302
|
delete config.mcpServers[name];
|
|
282
303
|
writeMcpConfig(located.file, config);
|
|
283
|
-
clearApproval({ projectPath: projectPath(args.flags), name });
|
|
284
304
|
process.stdout.write(`Removed MCP server ${name} from ${located.scope} scope\n`);
|
|
285
305
|
process.stdout.write(`File modified: ${located.file}\n`);
|
|
306
|
+
// The definition is gone either way; the decision about it is a separate
|
|
307
|
+
// record, and leaving one behind for a name that no longer exists means a
|
|
308
|
+
// later server reusing the name would meet an approval nobody granted it.
|
|
309
|
+
if (!clearApproval({ projectPath: projectPath(args.flags), name })) {
|
|
310
|
+
process.stdout.write(
|
|
311
|
+
`Warning: could not update ${approvalStorePath() ?? "the approval store"}, ` +
|
|
312
|
+
`so the recorded decision for ${name} is still there. Run \`piagent-mcp reset ${name}\` once it is writable.\n`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
286
315
|
return 0;
|
|
287
316
|
}
|
|
288
317
|
|
|
@@ -318,33 +347,51 @@ function commandGet(args) {
|
|
|
318
347
|
function commandList(args) {
|
|
319
348
|
const scopeFilter = value(args.flags, "--scope");
|
|
320
349
|
const project = projectPath(args.flags);
|
|
321
|
-
|
|
350
|
+
// Listing nothing is the correct answer only when nothing is reaching the
|
|
351
|
+
// session. A config the gate cannot verify reaches it with an unknown set of
|
|
352
|
+
// servers, and telling somebody to seed a baseline in that state sends them
|
|
353
|
+
// to the wrong file entirely.
|
|
354
|
+
const state = sessionMcpState({ projectPath: project, scopes: scopeFilter ? [scopeFilter] : undefined });
|
|
355
|
+
const { rows } = state;
|
|
322
356
|
|
|
323
357
|
if (args.flags.get("--json") === true) {
|
|
358
|
+
// The same fields the human output carries. A caller scripting against this
|
|
359
|
+
// read `servers: []` as "nothing configured" while every tool call was being
|
|
360
|
+
// refused, and an exit code of 0 agreed with it.
|
|
324
361
|
process.stdout.write(`${JSON.stringify({
|
|
325
362
|
project,
|
|
363
|
+
usable: state.usable,
|
|
364
|
+
blockingConfig: state.blockingConfig,
|
|
326
365
|
servers: rows.map((row) => ({
|
|
327
366
|
name: row.name,
|
|
328
367
|
scope: row.scope,
|
|
368
|
+
origin: row.origin,
|
|
369
|
+
requiresApproval: row.requiresApproval,
|
|
329
370
|
file: row.file,
|
|
330
371
|
state: row.readiness.state,
|
|
331
372
|
detail: row.readiness.detail,
|
|
332
373
|
entry: maskServerEntry(row.entry)
|
|
333
374
|
}))
|
|
334
375
|
}, null, 2)}\n`);
|
|
335
|
-
return 0;
|
|
376
|
+
return state.usable ? 0 : 1;
|
|
336
377
|
}
|
|
337
378
|
|
|
379
|
+
for (const problem of state.blockingConfig) process.stdout.write(`BLOCKED every tool call: ${problem.detail}\n`);
|
|
380
|
+
|
|
338
381
|
if (rows.length === 0) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
382
|
+
if (state.usable) {
|
|
383
|
+
process.stdout.write("No MCP servers configured.\n");
|
|
384
|
+
process.stdout.write("Seed the pinned baseline with: piagent-mcp --preset core --scope global\n");
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
process.stdout.write(`${formatServerTable(rows)}\n`);
|
|
388
|
+
if (state.blocked.length > 0) {
|
|
389
|
+
process.stdout.write(`\n${state.blocked.length} server(s) not usable yet. Run \`piagent-mcp doctor\`.\n`);
|
|
390
|
+
}
|
|
342
391
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (blocked.length > 0) process.stdout.write(`\n${blocked.length} server(s) not usable yet. Run \`piagent-mcp doctor\`.\n`);
|
|
347
|
-
return 0;
|
|
392
|
+
// Out of the no-rows branch: the exit code answers "can MCP work here", and
|
|
393
|
+
// that question does not depend on how many rows happened to print.
|
|
394
|
+
return state.usable ? 0 : 1;
|
|
348
395
|
}
|
|
349
396
|
|
|
350
397
|
/** @param {ReturnType<typeof parseArgs>} args @param {boolean} enabled */
|
|
@@ -352,6 +399,7 @@ function commandToggle(args, enabled) {
|
|
|
352
399
|
const name = args.positionals[0];
|
|
353
400
|
if (!name) throw new UsageError(`${enabled ? "enable" : "disable"} needs a server name`);
|
|
354
401
|
const located = locateServer(name, args.flags);
|
|
402
|
+
assertOwnedByThisTool(located, enabled ? "enable" : "disable");
|
|
355
403
|
const config = readMcpConfig(located.file);
|
|
356
404
|
const entry = { ...(isRecord(config.mcpServers[name]) ? config.mcpServers[name] : {}) };
|
|
357
405
|
if (enabled) delete entry.enabled;
|
|
@@ -384,9 +432,19 @@ function commandDecide(args, decision) {
|
|
|
384
432
|
if (decision === "approved" && before.state === "pending" && args.flags.get("--force") !== true) {
|
|
385
433
|
// Approving without reading it is the failure this gate exists to prevent, so
|
|
386
434
|
// the definition is put in front of the operator before the decision lands.
|
|
387
|
-
|
|
435
|
+
// The merged entry, not this layer's fragment: the adapter merges a
|
|
436
|
+
// same-named server key by key across layers, so a repository declaring only
|
|
437
|
+
// `{"args": [...]}` runs with a command it never showed the operator.
|
|
438
|
+
process.stdout.write(`${JSON.stringify(
|
|
439
|
+
maskServerEntry(mergedServerEntry({ projectPath: project, name, entry: server.entry })), null, 2
|
|
440
|
+
)}\n`);
|
|
441
|
+
}
|
|
442
|
+
// A decision that was not written is not a decision. Printing "Approved" while
|
|
443
|
+
// the store rejected the write would leave the operator waiting for a gate that
|
|
444
|
+
// is still blocking, with nothing on screen to explain why.
|
|
445
|
+
if (!recordApproval({ projectPath: project, name, entry: server.entry, decision })) {
|
|
446
|
+
throw new UsageError(`could not write ${approvalStorePath() ?? "the approval store"}; ${name} is still ${before.state}`);
|
|
388
447
|
}
|
|
389
|
-
recordApproval({ projectPath: project, name, entry: server.entry, decision });
|
|
390
448
|
const after = approvalState({ projectPath: project, name, entry: server.entry });
|
|
391
449
|
process.stdout.write(`${decision === "approved" ? "Approved" : "Rejected"} MCP server ${name} (${server.scope}) for ${project}\n`);
|
|
392
450
|
process.stdout.write(`Pinned to this definition: ${after.digest}\n`);
|
|
@@ -398,7 +456,9 @@ function commandDecide(args, decision) {
|
|
|
398
456
|
function commandReset(args) {
|
|
399
457
|
const project = projectPath(args.flags);
|
|
400
458
|
const name = args.positionals[0];
|
|
401
|
-
clearApproval({ projectPath: project, name })
|
|
459
|
+
if (!clearApproval({ projectPath: project, name })) {
|
|
460
|
+
throw new UsageError(`could not write ${approvalStorePath() ?? "the approval store"}; nothing was forgotten`);
|
|
461
|
+
}
|
|
402
462
|
process.stdout.write(name
|
|
403
463
|
? `Forgot the decision for ${name} in ${project}\n`
|
|
404
464
|
: `Forgot every MCP approval decision for ${project}\n`);
|
|
@@ -408,7 +468,12 @@ function commandReset(args) {
|
|
|
408
468
|
/** @param {ReturnType<typeof parseArgs>} args */
|
|
409
469
|
function commandDoctor(args) {
|
|
410
470
|
const project = projectPath(args.flags);
|
|
411
|
-
|
|
471
|
+
// Resolved before the no-servers shortcut below. A layer nobody can parse
|
|
472
|
+
// produces no rows, so reporting "no servers configured" and stopping would
|
|
473
|
+
// answer the question exactly backwards. The guard refuses every tool call
|
|
474
|
+
// under `blockingConfig`, and this is where somebody comes to find out why.
|
|
475
|
+
const state = sessionMcpState({ projectPath: project });
|
|
476
|
+
const { rows, unreadable: broken, imports, blockingConfig: unverifiable } = state;
|
|
412
477
|
|
|
413
478
|
// The one check too slow for a session start: a daemon that is installed but
|
|
414
479
|
// not running looks exactly like one that is running until something asks it.
|
|
@@ -422,22 +487,46 @@ function commandDoctor(args) {
|
|
|
422
487
|
process.stdout.write(`${JSON.stringify({
|
|
423
488
|
project,
|
|
424
489
|
dockerRunning,
|
|
425
|
-
servers: rows.map((row) => ({ name: row.name, scope: row.scope, state: row.readiness.state, detail: row.readiness.detail, remedy: row.readiness.remedy }))
|
|
490
|
+
servers: rows.map((row) => ({ name: row.name, scope: row.scope, state: row.readiness.state, detail: row.readiness.detail, remedy: row.readiness.remedy })),
|
|
491
|
+
unreadableLayers: broken,
|
|
492
|
+
repositoryImports: imports.map((layer) => ({ scope: layer.scope, kinds: layer.kinds })),
|
|
493
|
+
unverifiableConfig: unverifiable
|
|
426
494
|
}, null, 2)}\n`);
|
|
427
|
-
return
|
|
495
|
+
return state.blocked.length > 0 || broken.length > 0 || unverifiable.length > 0 ? 1 : 0;
|
|
428
496
|
}
|
|
429
497
|
|
|
498
|
+
for (const layer of broken) process.stdout.write(`Unreadable ${layer.scope} config: ${layer.detail}\n`);
|
|
499
|
+
for (const problem of unverifiable) process.stdout.write(`BLOCKED every tool call: ${problem.detail}\n`);
|
|
500
|
+
|
|
430
501
|
if (rows.length === 0) {
|
|
502
|
+
if (unverifiable.length > 0 || broken.length > 0) {
|
|
503
|
+
process.stdout.write(`\n${unverifiable.length + broken.length} MCP config problem(s) need attention.\n`);
|
|
504
|
+
return 1;
|
|
505
|
+
}
|
|
431
506
|
process.stdout.write("No MCP servers configured.\n");
|
|
432
507
|
return 0;
|
|
433
508
|
}
|
|
434
509
|
|
|
435
510
|
const report = formatDoctorReport(rows, { dockerRunning, rerun: "piagent-mcp doctor" });
|
|
436
511
|
for (const line of report.lines) process.stdout.write(`${line}\n`);
|
|
437
|
-
|
|
512
|
+
|
|
513
|
+
// A layer nothing can parse hides whatever it defines and reads as empty, so
|
|
514
|
+
// it is counted (and already printed above). An import is named but not
|
|
515
|
+
// counted: the servers it brings in are in the report above with their own
|
|
516
|
+
// state, and this line says where they came from, which reading the
|
|
517
|
+
// repository's own config would not reveal.
|
|
518
|
+
for (const layer of imports) {
|
|
519
|
+
process.stdout.write(
|
|
520
|
+
`${layer.file} imports servers from ${layer.kinds.join(", ")} config; ` +
|
|
521
|
+
"they need approval here like any server the repository defines.\n"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const problems = report.problems + broken.length + unverifiable.length;
|
|
526
|
+
process.stdout.write(problems === 0
|
|
438
527
|
? "\nPASS: every configured MCP server can be reached.\n"
|
|
439
|
-
: `\n${
|
|
440
|
-
return
|
|
528
|
+
: `\n${problems} MCP problem(s) need attention.\n`);
|
|
529
|
+
return problems === 0 ? 0 : 1;
|
|
441
530
|
}
|
|
442
531
|
|
|
443
532
|
/** @param {ReturnType<typeof parseArgs>} args */
|
|
@@ -22,6 +22,15 @@ USAGE
|
|
|
22
22
|
FORMAT="text"
|
|
23
23
|
PROVIDER=""
|
|
24
24
|
|
|
25
|
+
require_value() {
|
|
26
|
+
local option="$1"
|
|
27
|
+
local value="${2:-}"
|
|
28
|
+
if [[ -z "$value" || "$value" == --* ]]; then
|
|
29
|
+
echo "Missing value for $option" >&2
|
|
30
|
+
exit 2
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
while [[ $# -gt 0 ]]; do
|
|
26
35
|
case "$1" in
|
|
27
36
|
--json)
|
|
@@ -29,7 +38,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
29
38
|
shift
|
|
30
39
|
;;
|
|
31
40
|
--provider)
|
|
32
|
-
|
|
41
|
+
require_value "$1" "${2:-}"
|
|
42
|
+
PROVIDER="$2"
|
|
33
43
|
shift 2
|
|
34
44
|
;;
|
|
35
45
|
-h|--help)
|
|
@@ -5,23 +5,49 @@ usage() {
|
|
|
5
5
|
cat <<'USAGE'
|
|
6
6
|
Usage:
|
|
7
7
|
scripts/pi-session-stats.sh [project-path] [session-file]
|
|
8
|
+
scripts/pi-session-stats.sh --history [project-path] [history-options]
|
|
8
9
|
|
|
9
10
|
Examples:
|
|
10
11
|
scripts/pi-session-stats.sh .
|
|
11
12
|
scripts/pi-session-stats.sh /path/to/project
|
|
12
13
|
scripts/pi-session-stats.sh /path/to/project ~/.pi/agent/sessions/.../session.jsonl
|
|
14
|
+
scripts/pi-session-stats.sh --history . --days 7
|
|
15
|
+
scripts/pi-session-stats.sh --history --all-projects --csv
|
|
13
16
|
|
|
14
17
|
Notes:
|
|
15
18
|
- Reads exact Pi token/cache/cost totals via RPC get_session_stats.
|
|
16
19
|
- If session-file is omitted, the newest persisted session whose header cwd matches project-path is used.
|
|
20
|
+
- Use --history for finished/old session totals read directly from Pi JSONL.
|
|
17
21
|
USAGE
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
HISTORY_SCRIPT="$SCRIPT_DIR/pi-usage-history.mjs"
|
|
26
|
+
|
|
20
27
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
21
28
|
usage
|
|
22
29
|
exit 0
|
|
23
30
|
fi
|
|
24
31
|
|
|
32
|
+
for argument in "$@"; do
|
|
33
|
+
case "$argument" in
|
|
34
|
+
--history|--all-projects|--days|--since|--until|--format|--json|--csv|--markdown|--include-subagents|--no-subagents|--limit|--sessions-dir)
|
|
35
|
+
exec node "$HISTORY_SCRIPT" "$@"
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
# Both arguments are positional and the project path comes first, so a flag in
|
|
41
|
+
# either slot is a typo rather than a path. Saying so beats failing later with a
|
|
42
|
+
# message about a directory that was never meant to be one.
|
|
43
|
+
for argument in "${1:-}" "${2:-}"; do
|
|
44
|
+
if [[ "$argument" == -* ]]; then
|
|
45
|
+
echo "FAIL: this script takes <project-path> [session-file], not flags; received: $argument" >&2
|
|
46
|
+
usage >&2
|
|
47
|
+
exit 2
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
25
51
|
PROJECT_PATH="${1:-.}"
|
|
26
52
|
SESSION_FILE="${2:-}"
|
|
27
53
|
|
|
@@ -79,7 +105,7 @@ fi
|
|
|
79
105
|
|
|
80
106
|
RPC_OUTPUT="$(
|
|
81
107
|
printf '%s\n' '{"type":"get_session_stats","id":"piagent-usage"}' \
|
|
82
|
-
| pi --mode rpc --session "$SESSION_FILE" --offline --approve
|
|
108
|
+
| pi -ne --mode rpc --session "$SESSION_FILE" --offline --approve
|
|
83
109
|
)"
|
|
84
110
|
|
|
85
111
|
node --input-type=module - "$RPC_OUTPUT" <<'NODE'
|