@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,180 @@
2
2
 
3
3
  This file records release-facing changes for Pi Agent Platform. Copy the relevant version block into GitHub Releases when publishing a tag.
4
4
 
5
+ ## v1.2.1 - 2026-07-28
6
+
7
+ ### Fixed
8
+
9
+ - The approval gate never covered direct MCP tools. It looked for a tool named `mcp__<server>__<tool>`; the adapter builds a name by joining the server's own name to the tool's, so `github` exposes `github_create_issue` and nothing ever matched. Every server was reachable with one `directTools` setting, which `piagent-mcp add --direct-tools` offers and the docs recommend for small servers. Attribution now runs against the configured server names across every prefixing mode.
10
+
11
+ A repository-carried config that sets `directTools` with `toolPrefix: "none"` leaves no evidence of a server in any tool name, so there is nothing to check a decision against. Every tool call in that session is refused while the setting stands. Refusing only the `mcp` proxy, as the first fix did, closed nothing: the proxy is the one form that still names its server, and the bare names it was protecting went straight through.
12
+
13
+ The v1.2.0 notes, `docs/mcp-and-tools.md` and the documentation site all stated the direct form was covered. It was not, and the test that asserted it asserted the same invented name shape.
14
+
15
+ - A repository could reach servers the gate never enumerated through the adapter's `imports` key. `.mcp.json` naming `vscode` pulls servers out of `.vscode/mcp.json`, which travels with the clone, while the gate reported no repository servers at all. Servers reachable through a repository-relative import are now collected and gated whichever layer declares the import, and a repository scope that imports any kind has those servers gated too. `piagent-mcp doctor` names the file and the kinds, and reports a config layer it cannot parse instead of counting it as empty.
16
+
17
+ One kind cannot be enumerated at all: `codex` keeps its servers in TOML, and nothing here parses TOML. Listing the servers of the five readable kinds says nothing about that one, so a repository declaring it has every tool call refused rather than the servers that happened to be readable gated and the rest let through.
18
+
19
+ - `piagent-mcp doctor` and `list` reported "No MCP servers configured" and exited 0 in exactly the states where the guard was refusing every tool call, and `list` went on to suggest seeding a baseline. Both now report the config that stopped the session and exit 1. The condition is decided in one place that the guard and the CLI both read, because the two disagreeing is what turned a blocked session into an unexplained one.
20
+
21
+ - A server imported from a repository-relative file was reported `ready` when a global config declared the import, while the gate treated it as a repository server awaiting approval. Scope answers which layer named the import, not where the definitions came from; a `global` config importing `vscode` reads them out of the clone. Whether a server needs approval is now decided where servers are collected, so both surfaces give the same answer, and the reason names the file the definition actually came from.
22
+
23
+ - `piagent-capability vendor` emptied its destination before checking containment and only tested the immediate parent for a symlink. A repository shipping `.pi` as a symlink had the recursive delete and the fetch land outside the project. The whole path is now verified against the resolved project root before anything is created or removed.
24
+
25
+ Emptying the destination first was also not recoverable: a fetch that failed for any reason — no network, a hostile archive rejected by the symlink check — left the project with neither the new source nor the reviewed one it had been running on. The fetch now lands beside the destination and moves into place only once it has passed every check, so a failed re-vendor leaves the working tree exactly as it was.
26
+
27
+ - The shell guard followed nested interpreters four levels deep and then stopped looking, so a payload wrapped in five layers of `bash -c` was permitted on the strength of a level nobody read. The walk now goes to sixteen levels and refuses a command that nests past it, rather than falling silent.
28
+
29
+ - An artifact shipped by a vendored capability source could change content while the lock still classified as `repin`. Artifact bytes stay on the build side for packs the platform ships — pinning them would stop a policy correction reaching the projects that reference it — but that reasoning does not transfer to a source the project vendored in, whose manifest digest pins the artifact list rather than its contents.
30
+
31
+ - A credential in an MCP server URL was stored and displayed in clear text. `add` now refuses userinfo and credential-shaped query parameters the same way it refuses them in `--env` and `--header`, and `list` and `get` mask a URL already on disk.
32
+
33
+ - `piagent-mcp approve`, `reject` and `reset` reported success when the approval store could not be written, leaving the operator waiting on a gate that was still blocking. All three now fail with the path that could not be written.
34
+
35
+ - A server using `bearerTokenEnvVar` reported `ready` with the variable unset. The field names its variable directly rather than through a `${VAR}` reference, so the scan that finds referenced variables never saw the one setting that guarantees a credential is needed.
36
+
37
+ - A capability pack could declare `activation.mode: "profile"` with no profiles list and crash resolution with a TypeError. The validator requires the list, the same way it already required triggers for trigger activation.
38
+
39
+ - Every script that takes `--flag <value>` read the next argument without checking it was a value. `--preset full --settings --dry-run` wrote a settings file named `--dry-run`, exited 0 and reported success, having skipped the dry run it was asked for and never touched the real settings file. `init-project.sh` and `setup.sh` checked only that an argument was present, which catches a flag at the end of the line and nothing else. All eight scripts now refuse a flag-shaped value, and the suite asserts it for every value-taking option in `scripts/`, including ones added later.
40
+
41
+ - The shell guard did not see a path reached through an inline interpreter script, an unbalanced quote, or a shell variable with a default. `node -e "require('fs').readFileSync('.env')"`, `cat .env'` and `cat .${X:-env}` all read a protected path without matching one. Literals inside an inline script are now extracted, tokenizing survives a quote with no partner, and a variable's default value is expanded when nothing else defines it.
42
+
43
+ - ANSI-C quoting hid a path from the guard entirely. `$'...'` decodes its escapes before the word is passed on, so `cat $'\x2eenv'` opens `.env` while the guard saw the literal `x2eenv`; a redirection target was not handled at all, so `printf x >$'.env'` truncated a protected file. The same trick reached the exec policy: `rm -rf $'\x2f'` was permitted, having been read as a removal of something called `x2f`. Escapes are decoded — hex, octal, short and long unicode, and the named ones — in tokenizing and in redirection targets alike.
44
+
45
+ - The destructive-command checks read raw shell words, so a catastrophic target written as a substitution was compared as literal text and matched nothing. `rm -rf /` was refused while `rm -rf $(printf /)`, `rm -rf $(echo /)`, `` rm -rf `printf /` `` and `find $(printf /) -delete` were all permitted; nothing downstream caught them either, since `/` is not a protected path. `printf` and `echo` with literal arguments are pure text and are now evaluated, so those resolve and the same refusal applies, and a variable holding the target (`D=/; rm -rf "$D"`) is expanded the way the path checks already expand it.
46
+
47
+ A target only the shell can produce — `rm -rf $(mktemp -d)`, `find $(mktemp -d) -delete` — is a confirmation rather than a refusal: refusing outright takes a common idiom away, and permitting silently is how the check above was walked around.
48
+
49
+ Evaluation is also limited to the `printf` formats the renderer reproduces: `%s` with no flags, width, precision or positional argument, and a format with no conversions at all. Everything else transforms its argument rather than printing it — `%.0s` truncates it away and prints the rest of the format, `%*s` consumes an argument as the width, `%q` requotes, `%b` decodes — so substituting the raw argument produced a value bash never printed. `rm -rf $(printf %.0s/ x)`, `$(printf %*s 0 /)`, `$(printf %.*s 1 /)` and `$(printf %q /)` are all `/` in a shell and were all permitted. `%%` and a format with more arguments than conversions (which bash reuses and this does not) are excluded for the same reason.
50
+
51
+ The same renderer fed the protected-path scan, so `cat $(printf %.0s.env x)` and `printf data > $(printf %.0s.env x)` reached `.env` without matching it. A `printf` format's literal text arrives in its output whatever the conversions do with the arguments, so it is now read as a path candidate in its own right — with the conversions taken out and what is left closed up, because a conversion that prints nothing lets the text on either side meet: `.e%.0snv` is `.env`, and reading the pieces separately gave `.e` and `nv`, neither of which is a file anybody protects. `printf %.0s.env x` on its own stays permitted, because printing a name is not opening it.
52
+
53
+ Single quotes suspend substitution, and quoting is gone by the time a word is tokenized, so `rm -rf '$(printf /)'` — a file with an awkward name — was refused as a root removal. Whether the segment contains a substitution the shell would actually run is now read from the raw text, and the scan that reads substitution bodies skips single-quoted text as well: `rm -rf $(printf /) '$(a;b)'` really does remove `/`, and reading the literal beside it as a body made the whole segment unevaluable, dropping a refusal to a confirmation.
54
+
55
+ Evaluation is limited to a substitution body that is one plain command and its literal operands. A separator puts a second command after the one being read, so `$(printf /; echo)` is `/` and `$(printf /; printf /)` is `//` however little `printf /` says so; a nested body cannot be located by pattern at all, since a pattern for `$(...)` matches the inner half of `$(printf $(printf /))` and describes a substitution nobody asked about; and quoting is applied before a word is tokenized, so `$(printf '\x2f')` arrives here as `$(printf x2f)`, which evaluates to a value the shell never had. Permitting on a wrong value is worse than not resolving, so any body carrying a separator, a nested substitution, a redirection, a variable, quoting or an escape gives up on evaluation and asks. A substitution still present after the resolution is not treated as resolved.
56
+
57
+ Every round of this so far reported a `printf` format, and reading one operand shape per round is how the same class kept coming back. Four more forms are closed together. Brace expansion runs before every other expansion and was not modelled at all, so `rm -rf {/,}` removed root and `cat {.env,}` opened a protected file while both read as ordinary words — the empty alternative is the whole trick, making a one-word expansion out of a form that does not look like one; it is now performed, and only where the shell would perform it, since quoting suspends it and `"{/,}"` really is one awkwardly named file. `--` ends `printf`'s options, so `$(printf -- /)` prints `/` while the format was read as `--`; any other `printf` option is now treated as one this does not model, since `-v NAME` assigns the result and prints nothing. A target that is nothing but a parameter expansion can be any value, root included: `${HOME:0:1}` is `/`, substring expansion is not modelled, and the word stayed exactly as written — so the shape decides rather than the operator, and a recursive removal of an unresolved whole-word expansion is asked about instead of permitted, while `$TMPDIR/build` is untouched because it cannot become root. And escapes are decoded inside a substitution body the same way they already were elsewhere: `$(echo -e '.en\x76')` and `$(printf %b '.en\x76')` both print `.env`, and the backslash is gone by the time the body is tokenized, so it is read back off the raw text.
58
+
59
+ The `printf` renderer substituted the raw argument for every conversion and ran the format once, so its output was not what `printf` writes and the protected-path scan never consulted it at all — it was offered the format's literal text and the raw arguments separately. A name assembled out of both went unseen: `.e%.1sv` with `nv` is `.env` while the pieces on offer were `.ev` and `nv`, `au%.2s.json` with `thX` is `auth.json`, and because bash reuses a format while arguments remain, `%s` with `.e` and `nv` is also `.env`. `cat $(printf .e%.1sv nv)`, `printf data > $(printf %s .e nv)` and `printf %s au th.json | xargs cat` all reached a protected file with nothing matching. The renderer now applies precision, width and format reuse, and its output is a path candidate in its own right; printing a name on its own stays permitted, as before.
60
+
61
+ Because the rendering is now accurate for those formats, the destructive check no longer merely asks about them: a constant precision or width and format reuse are reproduced byte for byte, so `rm -rf $(printf %.0s/ x)` and `rm -rf $(printf %s / /)` are refused rather than confirmed — they are `/` and `//` in any shell. `*` stays a confirmation even though the renderer now follows one: the width it consumes has its own rules, and a modelling slip there becomes a refusal of something nobody asked for. `%q`, `%b`, the numeric conversions and `%%` are unchanged and still confirmed.
62
+
63
+ Brace expansion was applied to the operands and nowhere else, but the shell performs it before it decides what the command even is. The command name, the flags and the `xargs` payload could all be assembled by it, and each read here as the text the author typed: `r{m,} -rf /` is `rm r -rf /` to the shell and a command called `r{m,}` to this, `rm {-rf,} /` lost the `-rf` the same way, and `fi{nd,} / -delete`, `find / {-delete,}` and `echo / | xargs rm {-rf,}` all went through. The whole word list is expanded now, before anything reads a command name or a flag. `find` also read only its first path operand, so `find fi / -delete` was judged on `fi` — every leading operand is a starting point and all of them are checked.
64
+
65
+ The `xargs` producer reads its own tokens rather than going through the shared candidate path, so the brace expansion done there never reached it: `printf {.env,} | xargs cat`, `printf .{en,}v | xargs cat` and `echo auth{.json,} | xargs cat` each opened a protected file with nothing matching. Quoting still suspends the expansion in both places, so `printf "{.env,}" | xargs cat` names the file it literally names.
66
+
67
+ Expanding braces inside the destructive checks fixed those checks and left every other reader on the raw text, which is not what the shell does — it expands once, before it decides what the command is. So `git reset --har{d,}` passed a legacy pattern, `docker volume pr{une,}` an exec-policy rule, `git p{ush,}` a confirmation pattern, and `{bash,} -c '...'` the nested-interpreter scan, each of them reading the words as typed. The word list is now built in one place and every reader takes it from there. Ranges are expanded as well: they look like they only enumerate counters, but `{n..n}` is a single letter with the braces removed, so `r{m..m}` is `rm`, `fi{n..n}d` is `find` and `.e{n..n}v` is `.env`.
68
+
69
+ Two things the expansion then exposed. `find` takes its own options before the paths start, and the target scan stopped at the first `-`, so `find -H / -delete` was judged with no target at all; `-H`, `-L`, `-P`, `-O<level>`, `-D` and `--` are skipped now. And a lone `-` between `-c` and the script ends the option list without being the script, so `bash -c - 'rm -rf /'` runs the removal while the scan read `-` and found nothing — which is exactly the shape `bash -{c,} '...'` takes once expanded.
70
+
71
+ Two readers were still on the raw words after that. The `xargs` producer identified its command as written, so `{printf,} .e%.1sv nv | xargs cat` found no producer at all — the output was never rendered and the `.env` it prints was never offered; the same held for a producer inside a substitution, and for the escape decoding, which is gated on the producer's name too. The producer's *name* now comes from the expanded list while its operands stay on the raw tokens, which are the only thing that still knows where the quotes were.
72
+
73
+ And the path candidates expanded braces unconditionally, which was the conservative half of the same problem: `cat "{.env,}"` opens a file with that literal name, and offering `.env` for it refused a command that touches nothing protected. The tokenizer already decides whether the shell would expand a word, and that verdict is now carried to the candidate list instead of guessed at — including for redirection targets, where the scanner reports it alongside the path.
74
+
75
+ Expanding into a *word list* while still reading operands off the raw tokens left one word doing two jobs. A brace word can produce the command and its arguments together: `{cat,.env}` is `cat .env`, and taking the name from the expanded list and the arguments from the unexpanded one saw a `cat` with no operands at all. The same shape hid a producer's format — `{printf,.e%.1sv} nv` prints `.env` — from the renderer. The expansion now produces a token *stream*, and command, flags and operands all come off it.
76
+
77
+ Quoting is tracked per character rather than per word, because one word can hold both: `"{.env,}"{,}` is the literal `{.env,}` twice, and a single flag for the token could only say "expand it all" or "expand none of it" — the first refused a command that opens nothing protected. Each piece of the syntax is decided on its own, so a quoted opening brace, a quoted comma, and a backslash in front of either each stop the expansion exactly where the shell stops it.
78
+
79
+ The regression test for this class no longer enumerates formats. It builds the word list `bash` actually builds for each expansion form and requires the policy to agree about what that list contains, in both directions — a catastrophic target must not be permitted, and a protected path must be seen — so a form nobody thought to write down is still compared against the shell rather than against an expectation.
80
+
81
+ That test now generates the forms instead of listing them: it assembles spellings out of the pieces a shell composes — quotes, escapes, ANSI-C, braces, ranges, `printf`, parameter expansion — and puts each one in front of several readers. It found three misses on its first run, and one of them was the cause of the other two. A brace group holding a parameter expansion closed on the wrong brace, because the scan for the closing `}` counted the one that ends `${...}`: `{cat,${X:-.env}}` became `cat}` and `${X:-.env}`, so the command was not `cat`, the operand was not a path, and `{rm,-rf,${X:-/}}` was permitted outright. The brace boundaries are now read with the same per-character record of what is syntax and what is text that the alternatives were already split by.
82
+
83
+ A word that is nothing but an unresolved parameter expansion is now refused wherever the command says the word is a file. `cat ${HOME:+.env}` opens `.env`, and every check here read it as a word naming nothing. A command substitution in the same position is left alone deliberately: it carries a command that the nested scan classifies, so `cat $(gh issue create ...)` is still answered as an external write rather than with a vaguer reason. Two expansions with nothing between them are treated as a name being assembled, the same as literal text beside one, because `$(printf %.1sen .Z)$(printf %.1s vZ)` spells `.env` and neither half contains it.
84
+
85
+ And a pipeline is judged as a pipeline. `echo $F | xargs cat` opens whatever `$F` names while neither segment shows it — the producer only prints, and the consumer's operand list is empty because the filename arrives on stdin. A producer feeding an `xargs` that opens files is now judged against the command at the far end of the pipe. A producer holding no expansion is unaffected, so `git diff --name-only | xargs cat` is untouched.
86
+
87
+ What kept this class alive for so many rounds was never the spelling. A segment had a dozen readers and each derived its own view of it, so closing a form for one reader left it open for the other eleven — and each round closed the reader that had just been reported. The generated corpus was then only generating *operands*, which is why it came back clean while the command name was still being read as written: `rm -rf $(printf /)` was refused because the destructive check resolved its own target, and `r$(printf %s m) -rf /` — the same removal with the name assembled the same way — was permitted by every check in the file.
88
+
89
+ Substitution resolution has moved to where brace expansion already was, so the shell's order — braces, then parameters, then substitutions — is performed once and every reader takes the result. That closed the command name, the `find` and `xargs` operands, the interpreter name in `$(printf %s ba)$(echo -n sh) -c '...'`, the `xargs` consumer in `echo .env | xa$(printf %s rgs) cat`, and the redirection target of a data-only command, which no reader had covered because `printf`'s own arguments are not files even though what `>` names always is.
90
+
91
+ Where a name still cannot be resolved, it is now treated as the unknown command it is. `${X:+r}m -rf /`, `${X:+b}ash -c 'cat .env'` and `${X:+cat} ${X:+.env}` are each that command whenever the variable is set, and each read as something harmless with an odd name. A name carrying an expansion no longer has to match a known command for the destructive checks, the interpreter scan or the file-operand net to look at it. A named command with a literal operand is untouched, so `grep -r TODO src` and `npm run $SCRIPT` keep their answers.
92
+
93
+ The corpus that found all of this generates the cross product the bugs actually live in — expansion form × position × reader — putting each spelling in the command name, the operand, a flag's argument, a redirection target, a pipeline producer, an `xargs` consumer and an interpreter payload. It ran clean at 36,000 commands with no missed protected path, no permitted removal of root, and nothing reported for a command that touches neither.
94
+
95
+ One reader was named in that change and not moved: the glob candidates were still read off the words as typed. That mattered more than it looked, because a pattern is the one thing the literal check cannot answer for — `.env*` matches no protected literal, so the glob reader is all that stands between it and the file. `{cat,.env*}`, `{grep,-f,.env*,README.md}`, `cat $({echo,.env*})`, `{bash,} -c 'cat .env*'`, `bash -{c,} 'cat .env*'` and `echo .env* | xargs cat` each read a protected file with no candidate produced at all. It now takes the same word list as everything else, descends the brace-expanded text, and follows a pattern printed into an `xargs` that opens files.
96
+
97
+ Brace expansion carries its per-character record out with each word instead of dropping it, because one token can produce words that are not the same kind of thing: `{cat,.env*}` is a command and a pattern, and inheriting the token's verdict got one of them wrong. Whether a word is matched against filenames is now decided per word, so `{".env*",x}` names a file and globs nothing, exactly as the shell has it.
98
+
99
+ The same record fixes the redirection scanner, which kept one flag for the whole target. `printf x > "{.env,}"{,}` is an ambiguous redirect that writes nothing, and expanding the quoted group anyway offered `.env` for a file the command never touches.
100
+
101
+ That fix reached the literal reader and stopped at the glob reader, which was still holding the target as typed and answering on the pattern inside it, so the same three commands stayed blocked on `.env` while writing no file at all. Both readers now apply the rule bash applies: a redirection opens its target only where it expands to exactly one word, and answers `ambiguous redirect` otherwise. `> {.env,other}` and `> .en{v,w}*` were the same false positive unreported, while `> {.env,}` really does truncate `.env`, because an empty alternative is dropped rather than counted. A word that can still disappear — an unset variable, or a pattern under `nullglob` — leaves the count unknowable, so those keep every candidate. The flag the scanner used to report beside the mask is gone; a flag for a whole word is what went wrong here to begin with, and two records that can disagree is one too many.
102
+
103
+ The generated corpus asserted off the word list bash builds, which for a redirection over-states what it opens — that is how it demanded an answer for `printf x > ".e"{,}nv`, a command that writes nothing. The oracle now asks whether bash opens the target, not what it expands to.
104
+
105
+ The corpus had no `*` in it, which is why it reported clean while all of this was open — a spelling axis missing from the grammar is a class of defect the generator cannot reach. Patterns are now generated too, expanded in a scratch directory holding names a protected pattern matches, since that is the only way to tell an active glob from a quoted one.
106
+
107
+ - A finding's strength depended on how its sentence started. The exec-policy decision was derived from the prose of each reason — "Refusing", "Forbidden", "Prompt" — rather than from the action the check had already recorded, so a finding whose wording did not begin with one of those words was recorded, displayed, and then counted as `allow`. The decision now reads the recorded action, with the prose test kept for the legacy confirmation patterns, which record a reason and no action.
108
+
109
+ - The MCP gate is cached per project and recomputed when a signature over the files behind it changes. That signature was written out by hand from the repository layers, while the check it guards had grown to read merged settings from all four scopes and to stat import targets outside the repository. Adding `directTools: true` to a personal global config while a repository set `toolPrefix: "none"` therefore left an already-loaded guard permitting tool calls that `piagent-mcp doctor` was reporting as blocked, until the module happened to be reloaded; an `imports: ["codex"]` declaration behaved the same way once the codex config appeared. The file list now comes from the same traversal the readers use, absences included, so a reader that starts consulting a new file is watched for it without a second list needing to be updated to match.
110
+
111
+ - The shell guard read `>|` as a pipe. Splitting there made the file being truncated the first word of a second command, where it read as an executable name, so `printf x >| .env` overwrote a protected path without matching one. `>&word` opens that word for writing unless a digit or `-` follows, and the whole form was skipped; a redirection can also precede its command, where it was read as the command name. Paths carried as an operand value (`dd if=`, `--file=`), inside an array literal, or rewritten by a parameter operator (`#`, `%`, `/`) were not resolved either, and a word was split at the space inside `$( )`, so the filename being assembled was never a single token.
112
+
113
+ Where a filename still cannot be resolved — literal text glued onto a substitution, as in `cat .en$(echo v)` — the command is refused and the word that caused it is named. Matching the literal half against the protected patterns answers a different question than the one being asked.
114
+
115
+ - `piagent-mcp list --json` reported `servers: []` and exited 0 while the guard was refusing every tool call, so a caller scripting against it read the blocked state as an empty one. Its exit code now answers whether MCP can work here rather than whether any rows printed, and the JSON carries the reason. The in-session `/piagent-mcp status`, `doctor` and menu report it too: all three surfaces now read one shared state instead of assembling their own from a different subset of the same calls.
116
+
117
+ - The adapter merges `settings` across all four config layers into one session-wide block. `directTools` set in one file and `toolPrefix: "none"` set in another therefore produced a session with neither property visible to a check that read either file on its own — the exact combination that leaves the approval gate nothing to check. Settings are now read merged, and the report names both files, since editing either one fixes it.
118
+
119
+ - `remove`, `enable` and `disable` on a server reached through `imports` wrote to a file the adapter does not read for that server, so the command reported success and the next tool call still reached it. Worse, the file it located was the other tool's config: the write would have rewritten somebody's Cursor or VS Code file in this platform's format. All three now refuse and name the file that actually defines it.
120
+
121
+ - The menu and the detail view decided whether a server needed approval from its scope. A repository-relative import declared by a `global` config carries the scope `global` and a definition read out of the clone, so the servers whose origin is hardest to see were the ones never offered a decision. Both now use the same rule the gate uses.
122
+
123
+ - Approving showed the fragment the repository layer contributed rather than the definition that runs. The adapter merges a same-named server key by key across layers, so a repository declaring only `{"args": [...]}` inherits its command from a lower layer, and the operator was consenting to a command line they were never shown. The preview is now the merged entry; the digest stays over the repository's own fragment, so editing a personal global config does not return a server to pending.
124
+
125
+ - A repository importing `codex` had every tool call refused whether or not that config existed on the machine. An import of a kind nothing here can parse is a hole only when the file it names is actually there; otherwise it is a config to clean up, not a session to stop.
126
+
127
+ - An MCP config that parses but is not a JSON object was read as empty, so the next write replaced it with this tool's shape and destroyed whatever the document had been. It is now refused. A write also no longer follows a symlink at the config path, drops `imports` kinds this version does not recognise, or hides an import target that exists but cannot be parsed — that last one reported as no servers, which is indistinguishable from a tool that defines none.
128
+
129
+ - The approval store could not tell absent from unreadable. Both read as pending, which is right for the gate and wrong for a write: recording one decision on top of a store this process could not parse discarded every decision in it, including the ones for other projects, whose only symptom would be returning to pending one at a time. Writes now refuse when the store could not be read.
130
+
131
+ ### Changed
132
+
133
+ - `docs/mcp-and-tools.md` records the config path and server key each supported import kind actually uses, against what the adapter reads. Three of the six disagree, so the platform reads the union: listing a server that never loads costs one approval, missing one costs the gate.
134
+
135
+ - `verify` refuses a source file carrying a control byte. Three files held a raw NUL inside a string literal — used as a key separator, and as ZIP magic in a test fixture. They ran correctly, but Git classified them as binary, which took them out of every grep-based gate in `verify`, including the wording checks, and out of the diff a reviewer sees on a pull request. The bytes are written as escapes now.
136
+
137
+ ## v1.2.0 - 2026-07-28
138
+
139
+ ### Added
140
+
141
+ - `piagent-mcp` manages servers instead of only seeding presets: `add`, `remove`, `get`, `list`, `enable`, `disable` and `doctor`, across the four config layers via `--scope`. `add` reports which scope it wrote and the exact file it changed, so nobody has to guess which layer is in effect. A server added by hand keeps surviving `piagent-update`, because a preset only rewrites the server IDs it owns.
142
+
143
+ - A server a repository defines is not usable until somebody on this machine approves it. `.mcp.json` and `.pi/mcp.json` travel with a clone, and opening a session in one should not hand its author a process here with this operator's credentials. `piagent-mcp approve` prints the definition before recording the decision, `reject` and `reset` are the other two answers, and the decision is stored in `~/.pi/piagent-mcp-approvals.json` rather than in the repository — a repository that can approve its own servers has not been gated.
144
+
145
+ The record pins the digest of what was approved. Approval is consent to what the server runs, so changing its command, URL or arguments returns it to pending, which is the same split the capability lock draws between consent and build.
146
+
147
+ Enforcement is in the guard's `tool_call` hook. As shipped in this version it covered the `mcp` proxy only — the direct-tool branch matched a name shape the adapter does not emit, so it never fired. See v1.2.1. The adapter owns the connection and the extension cannot prevent one being opened, so what is stopped is every tool call, not the connection itself. Servers in the global layers are not gated: nothing a repository contains can put a server there.
148
+
149
+ - MCP is a command inside a session, not a request to the model. `/piagent-mcp` on its own opens a menu, because a surface only helps if it can be found without already knowing the subcommand. The menu is built from what the project has: approving is not offered when nothing is waiting on a decision, and each entry carries its own count. Picking an action that needs a server asks which one, unless there is only one it could be.
150
+
151
+ Every entry is also typeable — `status`, `get`, `doctor`, `approve`, `reject`, `reset`, `enable`, `disable` — and both paths run the same code, so the menu teaches the direct form instead of replacing it. Subcommands and server names autocomplete. Where no prompt can be answered, in print and JSON mode, the report and the subcommand list are printed rather than blocking on a question nobody can answer.
152
+
153
+ Typing the shell command mid-session asked the model to run bash, read the output and report back — three model turns for a question this process answers from files it has already read, and a different summary each time. Pi dispatches a registered command straight to its handler, so the model is not involved at all.
154
+
155
+ Adding and removing a server stay in the terminal: `add` carries shell quoting, `${VAR}` references and a `--` command line. Those already have one correct parser, and it is the shell; a slash command that re-implemented it would be a second, worse one. `/piagent-mcp add` prints the terminal command rather than guessing at one.
156
+
157
+ Both surfaces read the same module, so the two cannot disagree about whether a server is usable. Remedies name the form that works where they are read: the session prints `/piagent-mcp approve <name>`, the terminal prints `piagent-mcp approve <name>`.
158
+
159
+ - `piagent-mcp doctor` and `piagent-doctor` say what each configured server still needs — a referenced variable that is not exported, an executable that is not on PATH, a decision that has not been made, the Docker daemon not answering. Counting configured servers, which is what the doctor did before, says nothing about whether any of them can answer. A session start names the ones that are definitely stuck; `PIAGENT_NO_MCP_NOTICE=1` switches that off.
160
+
161
+ ### Changed
162
+
163
+ - `piagent-mcp add` refuses to write a credential into MCP config and prints the reference form to use instead. The check reads both the field name and the value, so `--env CONFIG=ghp_...` is caught as well as `--env GITHUB_TOKEN=...`, and it covers `--header` too. `--url` accepts https, or http only on loopback. `list` and `get` mask values while leaving `${VAR}` readable, because that is a variable name rather than its contents.
164
+
165
+ OAuth is not automated and no token is stored by this platform. Pi already owns that flow — `/mcp-auth <server>` — and a third credential store holding other people's credentials would be worse than either of the two that already exist.
166
+
167
+ - `scripts/configure-mcp.sh` is replaced by `scripts/mcp-manage.mjs`; the pinned server catalog moved into `packages/piagent-core/mcp/`, where the capability lock pins it. The `piagent-mcp` command is unchanged. The three MCP files that decide which servers a session may call are now part of the locked runtime set.
168
+
169
+ - The docs site is one page per topic instead of a single file, grouped in the sidebar as Get started, Build, MCP and Operate. MCP has four pages of its own, since it now has a command surface, an approval decision and an auth story to explain. Each page carries a table of contents and previous/next links.
170
+
171
+ Pages are generated by `scripts/build-docs-site.mjs` from a content fragment each, plus shared `docs-site/assets/docs.css` and `docs-site/assets/docs.js`. The generated HTML is committed, because the site is served as static files with no build step, and `npm run verify` fails when the committed output no longer matches its source. `npm run site:preview` serves the result the way the deployment does — the site relies on clean URLs, so an ordinary static server answers 404 for every internal link.
172
+
173
+ - Public text describes this platform on its own terms rather than against named competing products. The page that compared feature tables now states what the platform takes on and what it leaves to the host, and is called Phạm vi platform rather than carrying two other products' names in the sidebar of every page. The same wording pass covers the MCP pages, `docs/mcp-and-tools.md` and a design-rationale comment in `document-intake.ts`.
174
+
175
+ Where "Codex" and "Claude" mean model families they are now labelled as such, with their provider ids, so a reader cannot mistake a model choice for a different tool. `--model-scope` and `--preset` say "model families" in their help text for the same reason.
176
+
177
+ The wording gate in `npm run verify` only read `README.md`, `docs/`, the prompts and the project template, which is why the site and the runtime strings were never checked. It now also reads `docs-site/content`, the site builder — navigation labels live there, not in a fragment — and the `mcp` and `extensions` directories that print to an operator.
178
+
5
179
  ## v1.1.9 - 2026-07-28
6
180
 
7
181
  ### Fixed
@@ -241,7 +415,7 @@ Breaking release. The `company` namespace is replaced by `piagent` with no alias
241
415
 
242
416
  ### Changed
243
417
 
244
- - Clarified public docs comparison with Codex CLI and Claude Code: Pi Company Platform brings similar governance concepts into Pi and packages them for team workflows.
418
+ - Clarified in the public docs where this platform sits relative to a general agent coding CLI: Pi Company Platform brings similar governance concepts into Pi and packages them for team workflows.
245
419
  - Reworded security docs to describe the guard as an application-level policy enforcement layer, not a complete security boundary or OS sandbox.
246
420
  - Split install guidance so production/team setup uses pinned `v0.4.8` or resolved commit sources, while latest is reserved for personal/sandbox use.
247
421
  - Clarified that redaction benchmarks and internal review are not equivalent to an external security audit.
package/README.md CHANGED
@@ -248,7 +248,7 @@ This repository intentionally excludes:
248
248
  ## Documentation
249
249
 
250
250
  - [Public docs site](https://piagent.io.vn)
251
- - [Static team docs site](docs-site/index.html)
251
+ - [Static team docs site](docs-site/index.html) — generated from `docs-site/content/`, preview with `npm run site:preview`
252
252
  - [Changelog](CHANGELOG.md)
253
253
  - [Vercel docs site deploy](docs/vercel-docs-site.md)
254
254
  - [Operator manual tiếng Việt](docs/operator-manual-vietnamese.md)
@@ -283,7 +283,7 @@ This repository intentionally excludes:
283
283
 
284
284
  ## Maturity
285
285
 
286
- The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.1.9` or a reviewed commit.
286
+ The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.2.1` or a reviewed commit.
287
287
 
288
288
  Ready for:
289
289
 
@@ -222,7 +222,7 @@ Các command này thuộc Pi core hoặc package Pi chính. Tên/availability c
222
222
 
223
223
  | Command/hotkey | Dịch nghĩa | Dùng khi nào | Ghi chú |
224
224
  |---|---|---|---|
225
- | `/login` | Đăng nhập provider | Lần đầu dùng OpenAI/Codex hoặc Anthropic/Claude. | OAuth/session lưu local trong Pi, không commit repo. |
225
+ | `/login` | Đăng nhập provider | Lần đầu dùng model OpenAI Codex hoặc model Anthropic Claude. | OAuth/session lưu local trong Pi, không commit repo. |
226
226
  | `/model` | Chọn model | Muốn chọn OpenAI provider bằng native selector. | Đây là flow chính, không phải hỏi agent tự chọn thay. |
227
227
  | `Ctrl+L` | Mở model selector | Đổi model nhanh. | Tương đương UI selector của Pi. |
228
228
  | `/scoped-models` | Chỉnh danh sách model cycle | Muốn `Ctrl+P` chỉ xoay quanh vài model hay dùng. | Global setup seed sẵn provider model families. |
@@ -237,6 +237,15 @@ Các command này thuộc Pi core hoặc package Pi chính. Tên/availability c
237
237
  | `/mcp tools` | List MCP tools | Muốn biết server expose tool nào. | Dùng trước khi bảo agent gọi tool ngoài. |
238
238
  | `/mcp reconnect` | Kết nối lại MCP | Khi server lỗi, token mới, hoặc config đổi. | Không thay thế việc export secret env. |
239
239
  | `/mcp-auth figma` | OAuth Figma MCP | Khi dùng Figma remote MCP. | Có thể khác theo Figma/Pi MCP package version. |
240
+ | `/piagent-mcp` | Menu MCP | Không nhớ subcommand, hoặc muốn xem có việc gì cần làm. | Menu dựng theo project; không có select UI thì in thẳng bảng trạng thái. |
241
+ | `/piagent-mcp status` | Danh sách MCP server + state | Muốn biết server nào đang có, ở scope nào, sẵn sàng chưa. | Đọc config, không kiểm tra kết nối live — cái đó là `/mcp`. |
242
+ | `/piagent-mcp get <name>` | Chi tiết một server | Trước khi duyệt, hoặc khi không rõ scope nào đang có hiệu lực. | Giá trị credential bị mask; `${VAR}` in nguyên vì là tên biến. |
243
+ | `/piagent-mcp doctor` | Server nào chưa chạy được và thiếu gì | Sau khi cài, hoặc khi tool call MCP bị chặn. | Không probe Docker daemon; chạy `piagent-mcp doctor` ở terminal cho việc đó. |
244
+ | `/piagent-mcp approve <name>` | Duyệt server repo khai | Khi mở một repo có `.mcp.json`. | In định nghĩa ra trước khi ghi quyết định. Pin theo digest. |
245
+ | `/piagent-mcp reject <name>` | Từ chối | Không tin server repo khai. | `reset` để quên quyết định. |
246
+ | `/piagent-mcp enable\|disable <name>` | Bật/tắt server | Tắt tạm mà không mất định nghĩa. | Cần `/reload` để adapter nhận thay đổi. |
247
+
248
+ `/piagent-mcp` chạy thẳng trong session, không qua model. Thêm/xoá server và preset ở terminal: `piagent-mcp add|remove|--preset`.
240
249
 
241
250
  ## Command subagent
242
251
 
@@ -419,8 +428,8 @@ Các lệnh này chạy ngoài Pi.
419
428
  | Command | Dùng khi nào |
420
429
  |---|---|
421
430
  | `npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0` | Cài Pi CLI tương thích với release hiện tại. |
422
- | `npm install -g --ignore-scripts @piagent/platform@1.1.9` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
423
- | `pi install git:github.com/Vt-mmm/piagent@v1.1.9` | Cài pinned release khi cần reproducible team setup. |
431
+ | `npm install -g --ignore-scripts @piagent/platform@1.2.1` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
432
+ | `pi install git:github.com/Vt-mmm/piagent@v1.2.1` | Cài pinned release khi cần reproducible team setup. |
424
433
  | `pi install git:github.com/Vt-mmm/piagent` | Cài latest package platform cho máy cá nhân/sandbox. |
425
434
  | `piagent-update --check` | Báo version hiện tại vs version sẽ lên cho cả ba thành phần; không cài gì. |
426
435
  | `piagent-update` | Full update một lệnh: Pi host → npm-global helper → Pi package, đúng thứ tự release yêu cầu. Thêm `--project <path>` để chạy doctor sau. |
@@ -451,7 +460,9 @@ Các lệnh này chạy ngoài Pi.
451
460
  | `piagent-subagents --preset safe` | Re-apply subagent safe config. |
452
461
  | `pi install npm:pi-web-access@0.13.0` | Optional: cấp web/search/fetch tools cho builtin `researcher`. |
453
462
  | `piagent-setup <project> --with-web-access` | Setup project + optional web access cho research subagents. |
454
- | `piagent-usage /path/to/project` | Lấy exact session usage từ terminal khác. |
463
+ | `piagent-usage /path/to/project` | Lấy exact usage của session mới nhất từ terminal khác. |
464
+ | `piagent-usage --history /path/to/project --days 7` | Tổng usage lịch sử của project, gồm session đã end. |
465
+ | `piagent-usage --history --all-projects --days 7 --csv` | Xuất usage tuần toàn máy để đưa vào report. |
455
466
  | `piagent-doctor /path/to/project --strict-share` | Kiểm tra project có share/open-source được không. |
456
467
  | `piagent-uninstall` | Báo cáo những gì sẽ được gỡ. Dry run, không đụng gì. |
457
468
  | `piagent-uninstall --apply` | Gỡ Pi package của platform khỏi Pi settings global. |
@@ -476,9 +487,10 @@ Khi đang develop chính repo `piagent`, có thể dùng npm scripts tương ứ
476
487
  | `npm run doctor -- <project> --strict-share` | `bash scripts/team-doctor.sh <project> --strict-share` |
477
488
  | `npm run benchmark -- ...` | `bash scripts/quality-benchmark.sh ...` |
478
489
  | `npm run usage -- <project>` | `bash scripts/pi-session-stats.sh <project>` |
490
+ | `npm run usage -- --history <project> --days 7` | `bash scripts/pi-session-stats.sh --history <project> --days 7` |
479
491
  | `npm run models` | `bash scripts/pi-model-catalog.sh` |
480
492
  | `npm run model-scope -- --preset full` | `bash scripts/configure-model-scope.sh --preset full` |
481
- | `npm run mcp -- --preset core --scope global --replace` | `bash scripts/configure-mcp.sh --preset core --scope global --replace` |
493
+ | `npm run mcp -- --preset core --scope global --replace` | `node scripts/mcp-manage.mjs --preset core --scope global --replace` |
482
494
  | `npm run subagents -- --preset safe` | `bash scripts/configure-subagents.sh --preset safe` |
483
495
 
484
496
  ## MCP command quick map
@@ -506,6 +518,8 @@ export GITHUB_PERSONAL_ACCESS_TOKEN=<github-token>
506
518
  | Session này đang dùng model gì? | `/session` hoặc `/piagent-usage` |
507
519
  | Context window đang còn bao nhiêu? | `/piagent-usage` |
508
520
  | Exact token/cost từ terminal khác? | `piagent-usage /path/to/project` |
521
+ | Tổng token/cost các session cũ? | `piagent-usage --history /path/to/project --days 7` |
522
+ | CSV report cuối tuần toàn máy? | `piagent-usage --history --all-projects --days 7 --csv` |
509
523
  | Subagents tốn bao nhiêu? | `/subagent-cost` |
510
524
  | Có nên compact chưa? | Xem `contextUsage.percent`; trên 75% mới cân nhắc `/compact`. |
511
525
 
@@ -14,9 +14,9 @@ Repo này phải chạy được cho nhiều project/domain khác nhau. Vì vậ
14
14
 
15
15
  | Use case | Package source |
16
16
  |---|---|
17
- | Team pinned release | `git:github.com/Vt-mmm/piagent@v1.1.9` |
17
+ | Team pinned release | `git:github.com/Vt-mmm/piagent@v1.2.1` |
18
18
  | Personal/sandbox dev | `git:github.com/Vt-mmm/piagent` |
19
- | Registry distribution | `npm:@piagent/platform@1.1.9` |
19
+ | Registry distribution | `npm:@piagent/platform@1.2.1` |
20
20
  | Enterprise npm (fork under another scope) | `npm:@your-scope/platform@x.y.z` |
21
21
  | Local platform dev | `/path/to/piagent` |
22
22
 
@@ -26,7 +26,7 @@ Latest tiện cho máy cá nhân muốn nhận cập nhật nhanh. Pin tag/commi
26
26
 
27
27
  | Channel | Command | Target | Policy |
28
28
  |---|---|---|---|
29
- | `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.1.9` | Install the Pi package matching the helper. |
29
+ | `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.2.1` | Install the Pi package matching the helper. |
30
30
  | `exact` | `bash scripts/install-global.sh --version vX.Y.Z --resolve-tag` | Requested tag resolved to commit SHA | Pi-package-only rollout, rollback, and incident recovery. |
31
31
  | `dev` | `bash scripts/install-global.sh --dev` | Moving Git source | Personal/sandbox only; do not commit into project settings. |
32
32
  | `local` | `bash scripts/install-global.sh --local` | Current checkout path | Platform development only. |
@@ -53,7 +53,7 @@ Root `package.json` có `pi` manifest trỏ tới:
53
53
  Do đó team có thể:
54
54
 
55
55
  ```bash
56
- pi install git:github.com/Vt-mmm/piagent@v1.1.9
56
+ pi install git:github.com/Vt-mmm/piagent@v1.2.1
57
57
  ```
58
58
 
59
59
  Không cần biết internal folder `packages/piagent-core`. Source không pin chỉ dành cho personal/sandbox như bảng channel ở trên.
@@ -65,12 +65,12 @@ Team nên install global package một lần:
65
65
  ```bash
66
66
  node --version # >= 22.19.0
67
67
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
68
- npm install -g --ignore-scripts @piagent/platform@1.1.9
68
+ npm install -g --ignore-scripts @piagent/platform@1.2.1
69
69
  piagent-install --stable --dry-run
70
70
  piagent-install --stable
71
71
  ```
72
72
 
73
- `pi install git:github.com/Vt-mmm/piagent@v1.1.9` vẫn hợp lệ nếu chỉ cần cài Pi package. Lệnh đó không tự tạo các binary terminal `piagent-*`; muốn có helper global thì dùng `npm install -g --ignore-scripts @piagent/platform@1.1.9`.
73
+ `pi install git:github.com/Vt-mmm/piagent@v1.2.1` vẫn hợp lệ nếu chỉ cần cài Pi package. Lệnh đó không tự tạo các binary terminal `piagent-*`; muốn có helper global thì dùng `npm install -g --ignore-scripts @piagent/platform@1.2.1`.
74
74
 
75
75
  Support matrix hiện tại: macOS Apple Silicon + Bash và Linux x64 + Bash đã được verify; macOS Intel + Bash và Linux ARM64 + Bash là supported target cần chạy smoke trước rollout; native Windows chưa phải target rollout team; WSL2 experimental. Chi tiết và version runtime nằm trong [release/install policy](release-install-policy.md).
76
76
 
@@ -103,7 +103,7 @@ Nếu muốn commit sẵn `.pi/piagent-profile.json` vào repo hoặc bootstrap
103
103
  ```bash
104
104
  bash scripts/setup.sh /path/to/project \
105
105
  --profile be-readonly-fe \
106
- --package-source git:github.com/Vt-mmm/piagent@v1.1.9 \
106
+ --package-source git:github.com/Vt-mmm/piagent@v1.2.1 \
107
107
  --mcp-preset core \
108
108
  --subagents-preset safe
109
109
  ```
@@ -18,11 +18,163 @@ Không có dòng code nào map capability name sang MCP server id. Khai `"github
18
18
 
19
19
  Từ `v0.3.7`, repo có thêm:
20
20
 
21
- - `piagent-mcp` / `scripts/configure-mcp.sh`;
21
+ - `piagent-mcp` / `scripts/mcp-manage.mjs`;
22
22
  - MCP preset `core`, `popular`, `all`, `design`, `design-local`, `browser`, `docs`, `github`;
23
23
  - template `.mcp.json` project-shared;
24
24
  - doctor warning khi adapter có nhưng không có MCP server nào.
25
25
 
26
+ ## Hai bề mặt
27
+
28
+ Trong session Pi thì dùng slash command, ngoài terminal thì dùng CLI. Cùng module đọc config, cùng cách phân loại trạng thái.
29
+
30
+ | Việc | Trong session | Trong terminal |
31
+ |---|---|---|
32
+ | Mở menu | `/piagent-mcp` | — |
33
+ | Danh sách + state | `/piagent-mcp status` | `piagent-mcp list` |
34
+ | Chi tiết một server | `/piagent-mcp get <name>` | `piagent-mcp get <name>` |
35
+ | Thiếu gì để chạy | `/piagent-mcp doctor` | `piagent-mcp doctor` |
36
+ | Duyệt server của repo | `/piagent-mcp approve <name>` | `piagent-mcp approve <name>` |
37
+ | Bật/tắt | `/piagent-mcp enable\|disable <name>` | `piagent-mcp enable\|disable <name>` |
38
+ | Thêm/xoá/preset | — | `piagent-mcp add\|remove\|--preset` |
39
+ | Kết nối live, OAuth | `/mcp`, `/mcp-auth <name>` | — |
40
+
41
+ Slash command do Pi dispatch thẳng tới handler và **không đi qua model**: gõ `piagent-mcp list` giữa session là nhờ model chạy bash, đọc output rồi thuật lại — ba lượt model cho một câu hỏi process này trả lời được từ file nó đã đọc.
42
+
43
+ Thêm và xoá server ở lại terminal: `add` mang shell quoting, tham chiếu `${VAR}` và một command line sau `--` — ba thứ đã có sẵn một bộ parse đúng là chính shell, viết lại bên trong slash command chỉ tạo ra bộ parse thứ hai tệ hơn. Gọi `/piagent-mcp add` sẽ in ra lệnh terminal tương ứng chứ không đoán.
44
+
45
+ Gõ `/piagent-mcp` không tham số sẽ mở menu dựng theo đúng thứ project đang có — chưa server nào bị tắt thì không hiện "bật lại", không server nào chờ duyệt thì không hiện "approve". Chọn một việc cần server thì nó hỏi tiếp server nào, trừ khi chỉ có đúng một lựa chọn. Mọi mục trong menu đều gõ thẳng được, nên menu dạy dạng trực tiếp chứ không thay thế.
46
+
47
+ Không có select UI (print mode `-p`, JSON mode) thì in thẳng bảng trạng thái + danh sách subcommand, không treo chờ trả lời.
48
+
49
+ `/piagent-mcp` có auto-complete cho subcommand và tên server.
50
+
51
+ ## Quản lý server
52
+
53
+ `piagent-mcp` có subcommand đầy đủ, không chỉ preset:
54
+
55
+ ```bash
56
+ piagent-mcp add sentry --url https://mcp.sentry.dev/mcp --scope global
57
+ piagent-mcp add internal --scope project -- npx -y @acme/internal-mcp
58
+ piagent-mcp list
59
+ piagent-mcp get sentry
60
+ piagent-mcp disable sentry # giữ định nghĩa, chỉ tắt
61
+ piagent-mcp remove sentry --scope global
62
+ piagent-mcp doctor
63
+ ```
64
+
65
+ `--scope` nhận `global`, `pi-global`, `project`, `pi-project` — ứng với 4 file ở bảng bên dưới. Mặc định `global`. `add` in ra scope và **đường dẫn file vừa sửa**, để không phải đoán config nào đang có hiệu lực.
66
+
67
+ Server tự thêm sống sót qua mọi lần `piagent-update`: preset chỉ ghi đè đúng những server ID nó sở hữu. Ngược lại, sửa tay một server thuộc preset (`github`, `context7`…) sẽ bị `--replace` đưa về baseline ở lần update kế tiếp — muốn giữ bản riêng thì đặt tên khác.
68
+
69
+ ## Secret không bao giờ nằm trong config
70
+
71
+ `add` **từ chối** giá trị trông giống credential và in ra dạng cần dùng:
72
+
73
+ ```bash
74
+ $ piagent-mcp add gh --env GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxx -- npx -y @acme/gh-mcp
75
+ FAIL: GITHUB_PERSONAL_ACCESS_TOKEN names a credential, so its value cannot be
76
+ written into MCP config. Export it in your shell and reference it instead:
77
+ --env 'GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN}'
78
+ ```
79
+
80
+ Chặn theo hai lớp: tên field nghe như credential, và giá trị tự nó trông như secret (bắt được cả `--env CONFIG=ghp_...`). Áp dụng cho cả `--env` lẫn `--header`.
81
+
82
+ `--url` chỉ nhận `https`, hoặc `http` khi host là loopback — Figma desktop MCP là đúng trường hợp đó.
83
+
84
+ `list` và `get` mask giá trị; riêng `${VAR}` được in nguyên vì đó là **tên biến**, không phải nội dung.
85
+
86
+ ## Login vào account MCP
87
+
88
+ Platform **không tự chạy OAuth và không giữ token**. `pi-mcp-adapter` đã sở hữu luồng đó — nó đăng ký `/mcp` và `/mcp-auth`, giữ callback server và OAuth provider — và dựng kho credential thứ ba cho credential của bên khác là lựa chọn tệ hơn. Việc của platform là phát hiện và chỉ đường:
89
+
90
+ ```text
91
+ /mcp-auth figma
92
+ ```
93
+
94
+ `/piagent-mcp doctor` (hoặc `piagent-mcp doctor` ngoài terminal) phân loại từng server:
95
+
96
+ | State | Nghĩa |
97
+ |---|---|
98
+ | `ready` | đủ điều kiện, gọi được |
99
+ | `needs-env` | config tham chiếu `${VAR}` mà biến chưa export (chỉ in **tên biến**) |
100
+ | `needs-command` | executable không có trên PATH |
101
+ | `pending-approval` | repo khai server này, chưa ai duyệt trên máy này |
102
+ | `approval-changed` | đã duyệt nhưng định nghĩa đã đổi |
103
+ | `rejected` | đã từ chối cho project này |
104
+ | `disabled` | tắt bằng `/piagent-mcp disable` |
105
+ | `oauth` | đăng nhập do Pi giữ; platform không biết còn hạn hay không |
106
+
107
+ `oauth` **không** bị coi là lỗi, và không xuất hiện trong notice đầu session — platform không phân biệt được đã login hay chưa, nên báo mỗi session sẽ thành nhiễu.
108
+
109
+ `doctor` chạy thêm `docker info` (chậm, nên chỉ ở doctor chứ không ở session start). `piagent-doctor` cũng đưa các state chặn vào `warnings`.
110
+
111
+ ## Approval gate cho server đến từ repo
112
+
113
+ Repo mang theo được `.mcp.json` và `.pi/mcp.json`. Clone một repo lạ rồi mở session **không** được phép cho tác giả repo đó một process trên máy mình với credential của mình. Nên server ở hai scope này không dùng được cho tới khi có người trên máy này duyệt.
114
+
115
+ ```bash
116
+ piagent-mcp get internal # đọc trước
117
+ piagent-mcp approve internal # hoặc reject / remove
118
+ ```
119
+
120
+ Ba tính chất quan trọng:
121
+
122
+ - **Quyết định nằm ngoài repo** — `~/.pi/piagent-mcp-approvals.json`. Repo tự duyệt được cho mình thì gate coi như không có. Fork, đổi tên, checkout thứ hai đều bắt đầu lại từ `pending`.
123
+ - **Pin theo digest của định nghĩa.** Duyệt là đồng ý với *thứ server đó chạy*; đổi command, URL hay args là câu hỏi mới, state quay về `approval-changed`. Cùng cách tách consent/build mà capability lock đang dùng.
124
+ - **Enforce ở `tool_call`**, không phải ở config. Guard đọc cả hai dạng lời gọi: proxy (`mcp` với `input.server`), và direct tool khi bật `directTools`. Gate chỉ đọc proxy sẽ bị đi vòng bằng cách bật `directTools`.
125
+
126
+ Dạng tên của direct tool là thứ trước đây ghi sai trong tài liệu này. Adapter **không** sinh ra `mcp__<server>__<tool>`. Nó ghép prefix lấy từ chính tên server:
127
+
128
+ | `toolPrefix` | prefix | server `repo-tools`, tool `search` |
129
+ |---|---|---|
130
+ | `server` (mặc định) | tên server, `-` đổi thành `_` | `repo_tools_search` |
131
+ | `short` | như trên, bỏ đuôi `-mcp`/`mcp` | `repo_tools_search` |
132
+ | `none` | không có prefix | `search` |
133
+
134
+ Nên guard truy server theo **tên server đang cấu hình**, không theo một pattern cố định. Với `toolPrefix: "none"` thì tên tool không còn dấu vết server nào cả — không truy được.
135
+
136
+ Vì vậy một config đặt `directTools: true` cùng `toolPrefix: "none"` làm **mọi tool call trong session bị chặn**, không riêng lời gọi MCP, cho tới khi bỏ setting đó. Chặn riêng proxy `mcp` là vá nhầm chỗ: proxy là dạng duy nhất còn nêu tên server, còn đúng những cái tên trần cần chặn thì đi thẳng qua.
137
+
138
+ Adapter **merge `settings` của cả bốn layer thành một block cho cả session**, layer sau ghi đè theo từng key — không phải per-server. Nên `directTools` đặt ở file này và `toolPrefix: "none"` đặt ở file kia vẫn ra đúng tổ hợp đó, trong khi đọc riêng từng file thì không file nào có vấn đề. Platform đọc settings đã merge, và báo cả hai file, vì sửa file nào cũng đủ.
139
+
140
+ ### `imports` — server không nằm trong bốn scope
141
+
142
+ Adapter nhận thêm key `imports` trong config MCP, để kéo định nghĩa server từ config của công cụ khác. Server vào session theo đường này **không** xuất hiện ở scope nào trong bốn scope trên, nên trước đây gate không thấy chúng.
143
+
144
+ Trong sáu kind, chỉ `vscode` trỏ vào đường dẫn **trong project** (`.vscode/mcp.json`) — nghĩa là clone repo là mang theo file đó. Vì vậy:
145
+
146
+ - Server đến từ một kind repo-relative luôn phải duyệt, **bất kể layer nào khai báo import** — kể cả `global`.
147
+ - Repo scope khai báo `imports` với kind nào thì server của kind đó cũng phải duyệt: repo đang chọn hộ máy này chạy server nào.
148
+ - Kind `codex` giữ server trong TOML, không bên nào parse được. Liệt kê server của năm kind đọc được không nói gì về kind thứ sáu, nên repo khai báo `codex` làm **mọi tool call bị chặn** — từ chối ngay ở dòng khai báo, không phải từ chối theo nội dung. Chỉ chặn khi file đó **có thật trên máy**: khai báo một kind mà máy này không cài thì đó là config cần dọn, không phải session cần dừng.
149
+ - Cột `SCOPE` trong `list` là **layer khai báo `imports`**, không phải nơi định nghĩa server. Global khai báo `imports: ["vscode"]` thì server hiện `scope: global` nhưng định nghĩa nằm trong clone — vẫn phải duyệt, và `detail` nêu đúng kind nó đến từ đâu. Menu `/piagent-mcp` và `get` cũng quyết theo đúng luật đó chứ không theo scope, nên server khó thấy nhất không còn là server không bao giờ được hỏi.
150
+ - `piagent-mcp doctor` nêu tên file có `imports` và các kind của nó, và nêu cả import target **có thật nhưng không parse được** — trước đây trạng thái đó đọc ra rỗng, không phân biệt được với một công cụ không khai server nào.
151
+ - `remove` / `enable` / `disable` trên server đến qua `imports` bị **từ chối**, kèm tên file thật cần sửa. File `list` chỉ ra là config của công cụ kia; ghi ngược lại bằng writer của platform sẽ viết lại file Cursor/VS Code của người dùng theo format mà chính công cụ đó không dùng.
152
+
153
+ Hai trạng thái làm gate không kiểm được gì (`toolPrefix: "none"`, và import kind không enumerate được) thì `doctor` và `list` **in ra và exit 1**, không im lặng báo "No MCP servers configured" — kể cả `list --json`, vốn trả `servers: []` với exit 0 đúng lúc không gì chạy được. Exit code trả lời "MCP ở đây có chạy được không", không phải "có in ra dòng nào không". Cả ba surface (guard, `/piagent-mcp`, CLI) đọc **một** state chung; hai bên lệch nhau chính là cách một session bị chặn biến thành một session hỏng không rõ lý do.
154
+
155
+ Gate được cache theo project, tính lại khi signature trên các file phía sau nó đổi. Danh sách file đó **lấy từ chính đường đi mà các reader dùng**, kể cả file đang không tồn tại — vì `imports: ["codex"]` trở thành chặn đúng lúc `~/.codex/config.toml` xuất hiện, và `directTools` thêm vào config global cá nhân là thứ guard đã load phải thấy ngay chứ không đợi reload module. Viết tay danh sách đó là cách nó lệch: check thì mở rộng ra đọc settings merge của cả bốn scope và stat import target ngoài repo, còn signature thì không.
156
+
157
+ ### Duyệt là duyệt cái gì
158
+
159
+ Adapter merge server **trùng tên** theo từng key qua các layer. Repo khai `{"args": ["--evil"]}` thì `command` vẫn lấy từ layer dưới — nên preview lúc duyệt in ra **định nghĩa đã merge**, tức thứ sẽ chạy thật, thay vì mảnh mà layer repo đóng góp. Digest vẫn tính trên mảnh của repo: đó là phần repo kiểm soát, và băm cả bản merge sẽ đẩy server về `pending` mỗi lần người dùng sửa config global của chính mình.
160
+
161
+ Bảng dưới là đường dẫn và key **thật** của từng công cụ, đối chiếu với thứ adapter đang đọc. Ba dòng lệch nhau, nên platform đọc hợp của cả hai phía: liệt kê thừa một server chỉ tốn một lần duyệt, liệt kê thiếu thì mất gate.
162
+
163
+ | kind | file thật | key thật | adapter đang đọc |
164
+ |---|---|---|---|
165
+ | `cursor` | `~/.cursor/mcp.json` | `mcpServers` | khớp |
166
+ | `claude-desktop` | `~/Library/Application Support/Claude/claude_desktop_config.json` | `mcpServers` | khớp |
167
+ | `claude-code` | `~/.claude.json` | `mcpServers` | khớp ở user scope; local scope lồng theo project nên cả hai bên đều không đọc |
168
+ | `vscode` | `.vscode/mcp.json` | **`servers`** | đọc `mcpServers` |
169
+ | `codex` | `~/.codex/config.toml` | **`mcp_servers`** (TOML) | đọc `~/.codex/config.json` |
170
+ | `windsurf` | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | đọc `~/.windsurf/mcp.json` |
171
+
172
+ Giới hạn cần nói thẳng: extension **không chặn được adapter mở connection**, chỉ chặn tool call. Phần còn lại là bản thân kết nối, và việc server phía kia ghi log rằng có người kết nối. Riêng file TOML của `codex` thì không bên nào parse, nên platform không liệt kê được nội dung của nó.
173
+
174
+ Server ở `global` / `pi-global` không qua gate — hai layer đó nằm ngoài mọi repo, không repo nào đặt được server vào đó, và bắt duyệt config của chính mình trên từng máy là vô nghĩa. Ngoại lệ duy nhất là `imports` trỏ vào kind repo-relative, vì lúc đó nội dung được đọc lại nằm trong repo.
175
+
176
+ Notice đầu session nêu tên server đang chờ quyết định hoặc thiếu setup. Tắt bằng `PIAGENT_NO_MCP_NOTICE=1`.
177
+
26
178
  ## Cấu hình một lần cho máy cá nhân/team
27
179
 
28
180
  Cả `piagent-setup` và `piagent-install` đều cài adapter và seed preset `core` **mặc định**. Muốn bỏ qua thì thêm `--no-mcp`.
@@ -56,7 +208,7 @@ piagent-mcp --list
56
208
  Nếu clone repo GitHub và chưa link npm bin:
57
209
 
58
210
  ```bash
59
- bash /path/to/piagent/scripts/configure-mcp.sh --preset popular --scope global --replace
211
+ node /path/to/piagent/scripts/mcp-manage.mjs --preset popular --scope global --replace
60
212
  ```
61
213
 
62
214
  `--replace` cập nhật các server ID thuộc preset về baseline đã pin và vẫn giữ nguyên server ID khác. Luồng install/upgrade toàn cục luôn dùng chế độ này để không duy trì dependency động từ cấu hình cũ.