@piagent/platform 1.2.0 → 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 (54) hide show
  1. package/CHANGELOG.md +133 -1
  2. package/README.md +1 -1
  3. package/docs/command-reference-vietnamese.md +8 -3
  4. package/docs/distribution-standard.md +7 -7
  5. package/docs/mcp-and-tools.md +49 -3
  6. package/docs/operator-manual-vietnamese.md +20 -8
  7. package/docs/publishing-for-teams.md +1 -0
  8. package/docs/release-install-policy.md +6 -6
  9. package/docs/security-threat-model.md +4 -3
  10. package/docs/team-onboarding.md +3 -3
  11. package/docs/usage-observability.md +29 -0
  12. package/docs-site/architecture.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/mcp-approval.html +54 -2
  15. package/docs-site/content/quickstart.html +1 -1
  16. package/docs-site/index.html +1 -1
  17. package/docs-site/mcp-approval.html +55 -3
  18. package/docs-site/mcp-auth.html +1 -1
  19. package/docs-site/mcp-servers.html +1 -1
  20. package/docs-site/mcp.html +1 -1
  21. package/docs-site/permissions.html +1 -1
  22. package/docs-site/profiles.html +1 -1
  23. package/docs-site/quickstart.html +2 -2
  24. package/docs-site/runtime.html +1 -1
  25. package/docs-site/scope.html +1 -1
  26. package/docs-site/security.html +1 -1
  27. package/docs-site/team.html +1 -1
  28. package/docs-site/workflows.html +1 -1
  29. package/package.json +1 -1
  30. package/packages/piagent-core/capabilities/capability-core.js +41 -4
  31. package/packages/piagent-core/capabilities/capability-sources.js +73 -12
  32. package/packages/piagent-core/extensions/piagent-guard.ts +139 -25
  33. package/packages/piagent-core/extensions/policy-core.js +1586 -98
  34. package/packages/piagent-core/mcp/mcp-approval-store.js +56 -15
  35. package/packages/piagent-core/mcp/mcp-auth-readiness.js +31 -3
  36. package/packages/piagent-core/mcp/mcp-command-actions.js +139 -30
  37. package/packages/piagent-core/mcp/mcp-config-layers.js +339 -6
  38. package/packages/piagent-core/mcp/mcp-import-kinds.js +240 -0
  39. package/packages/piagent-core/mcp/mcp-server-entry.js +51 -0
  40. package/packages/piagent-core/mcp/mcp-session-view.js +55 -4
  41. package/packages/piagent-core/mcp/mcp-tool-naming.js +90 -0
  42. package/packages/piagent-core/package.json +1 -1
  43. package/scripts/configure-model-scope.sh +19 -3
  44. package/scripts/configure-subagents.sh +17 -4
  45. package/scripts/init-project.sh +15 -8
  46. package/scripts/mcp-manage.mjs +111 -22
  47. package/scripts/pi-model-catalog.sh +11 -1
  48. package/scripts/pi-session-stats.sh +27 -1
  49. package/scripts/pi-usage-history.mjs +567 -0
  50. package/scripts/quality-benchmark.sh +37 -9
  51. package/scripts/setup.sh +21 -13
  52. package/scripts/uninstall-global.sh +10 -4
  53. package/scripts/verify-local.sh +19 -0
  54. package/scripts/verify-release-identity.mjs +29 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,138 @@
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
+
5
137
  ## v1.2.0 - 2026-07-28
6
138
 
7
139
  ### Added
@@ -12,7 +144,7 @@ This file records release-facing changes for Pi Agent Platform. Copy the relevan
12
144
 
13
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.
14
146
 
15
- Enforcement is in the guard's `tool_call` hook, covering both the `mcp` proxy and the `mcp__<server>__<tool>` form that `directTools` produces. 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.
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.
16
148
 
17
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.
18
150
 
package/README.md CHANGED
@@ -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.2.0` 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
 
@@ -428,8 +428,8 @@ Các lệnh này chạy ngoài Pi.
428
428
  | Command | Dùng khi nào |
429
429
  |---|---|
430
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. |
431
- | `npm install -g --ignore-scripts @piagent/platform@1.2.0` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
432
- | `pi install git:github.com/Vt-mmm/piagent@v1.2.0` | 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. |
433
433
  | `pi install git:github.com/Vt-mmm/piagent` | Cài latest package platform cho máy cá nhân/sandbox. |
434
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ì. |
435
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. |
@@ -460,7 +460,9 @@ Các lệnh này chạy ngoài Pi.
460
460
  | `piagent-subagents --preset safe` | Re-apply subagent safe config. |
461
461
  | `pi install npm:pi-web-access@0.13.0` | Optional: cấp web/search/fetch tools cho builtin `researcher`. |
462
462
  | `piagent-setup <project> --with-web-access` | Setup project + optional web access cho research subagents. |
463
- | `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. |
464
466
  | `piagent-doctor /path/to/project --strict-share` | Kiểm tra project có share/open-source được không. |
465
467
  | `piagent-uninstall` | Báo cáo những gì sẽ được gỡ. Dry run, không đụng gì. |
466
468
  | `piagent-uninstall --apply` | Gỡ Pi package của platform khỏi Pi settings global. |
@@ -485,6 +487,7 @@ Khi đang develop chính repo `piagent`, có thể dùng npm scripts tương ứ
485
487
  | `npm run doctor -- <project> --strict-share` | `bash scripts/team-doctor.sh <project> --strict-share` |
486
488
  | `npm run benchmark -- ...` | `bash scripts/quality-benchmark.sh ...` |
487
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` |
488
491
  | `npm run models` | `bash scripts/pi-model-catalog.sh` |
489
492
  | `npm run model-scope -- --preset full` | `bash scripts/configure-model-scope.sh --preset full` |
490
493
  | `npm run mcp -- --preset core --scope global --replace` | `node scripts/mcp-manage.mjs --preset core --scope global --replace` |
@@ -515,6 +518,8 @@ export GITHUB_PERSONAL_ACCESS_TOKEN=<github-token>
515
518
  | Session này đang dùng model gì? | `/session` hoặc `/piagent-usage` |
516
519
  | Context window đang còn bao nhiêu? | `/piagent-usage` |
517
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` |
518
523
  | Subagents tốn bao nhiêu? | `/subagent-cost` |
519
524
  | Có nên compact chưa? | Xem `contextUsage.percent`; trên 75% mới cân nhắc `/compact`. |
520
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.2.0` |
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.2.0` |
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.2.0` | 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.2.0
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.2.0
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.2.0` 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.0`.
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.2.0 \
106
+ --package-source git:github.com/Vt-mmm/piagent@v1.2.1 \
107
107
  --mcp-preset core \
108
108
  --subagents-preset safe
109
109
  ```
@@ -121,11 +121,57 @@ Ba tính chất quan trọng:
121
121
 
122
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
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 chặn cả proxy (`mcp` với `input.server`) lẫn dạng direct tool (`mcp__<server>__<tool>`) — gate chỉ đọc proxy sẽ bị đi vòng bằng cách bật `directTools`.
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
125
 
126
- 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.
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
127
 
128
- 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.
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.
129
175
 
130
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`.
131
177
 
@@ -18,12 +18,12 @@ Support matrix release hiện tại: macOS Apple Silicon + Bash và Linux x64 +
18
18
  ```bash
19
19
  node --version # >= 22.19.0
20
20
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
21
- npm install -g --ignore-scripts @piagent/platform@1.2.0
21
+ npm install -g --ignore-scripts @piagent/platform@1.2.1
22
22
  piagent-install --stable --dry-run
23
23
  piagent-install --stable
24
24
  ```
25
25
 
26
- Khi seed `.pi/settings.json` cho team/repo cần audit lặp lại, giữ package source dạng pinned tag như `git:github.com/Vt-mmm/piagent@v1.2.0`. Máy cá nhân có thể dùng `git:github.com/Vt-mmm/piagent` để theo latest.
26
+ Khi seed `.pi/settings.json` cho team/repo cần audit lặp lại, giữ package source dạng pinned tag như `git:github.com/Vt-mmm/piagent@v1.2.1`. Máy cá nhân có thể dùng `git:github.com/Vt-mmm/piagent` để theo latest.
27
27
 
28
28
  Nếu đang ở source checkout của platform, dùng helper theo channel để preview trước khi đổi:
29
29
 
@@ -99,7 +99,7 @@ install package once
99
99
 
100
100
  ```bash
101
101
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
102
- npm install -g --ignore-scripts @piagent/platform@1.2.0
102
+ npm install -g --ignore-scripts @piagent/platform@1.2.1
103
103
  piagent-install --stable --dry-run
104
104
  piagent-install --stable
105
105
  ```
@@ -281,7 +281,7 @@ Không phải daily default. Dùng khi muốn tạo sẵn `.pi` files cho projec
281
281
  bash /path/to/piagent/scripts/setup.sh /path/to/project \
282
282
  --project-only \
283
283
  --profile auto \
284
- --package-source git:github.com/Vt-mmm/piagent@v1.2.0 \
284
+ --package-source git:github.com/Vt-mmm/piagent@v1.2.1 \
285
285
  --mcp-preset core \
286
286
  --subagents-preset safe
287
287
  ```
@@ -514,6 +514,16 @@ bash /path/to/piagent/scripts/pi-session-stats.sh \
514
514
  /Users/<user>/.pi/agent/sessions/<project-key>/<session>.jsonl
515
515
  ```
516
516
 
517
+ Để xem tổng usage lịch sử trên máy, kể cả session đã end:
518
+
519
+ ```bash
520
+ piagent-usage --history /path/to/project --days 7
521
+ piagent-usage --history --all-projects --days 7 --csv
522
+ piagent-usage --history --all-projects --since 2026-07-20 --until 2026-07-26 --json
523
+ ```
524
+
525
+ History mode đọc trực tiếp `~/.pi/agent/sessions/**/*.jsonl`. Mặc định có tính cả subagent session files vì đó là usage thật của máy; thêm `--no-subagents` nếu chỉ muốn parent/main sessions.
526
+
517
527
  ### Cách đọc số
518
528
 
519
529
  | Field | Ý nghĩa |
@@ -854,8 +864,8 @@ Watchdog là optional adversarial reviewer ở cuối turn, không bật mặc
854
864
 
855
865
  | Command | Dùng để |
856
866
  |---|---|
857
- | `npm install -g --ignore-scripts @piagent/platform@1.2.0` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
858
- | `pi install git:github.com/Vt-mmm/piagent@v1.2.0` | Install pinned release cho reproducible team setup. |
867
+ | `npm install -g --ignore-scripts @piagent/platform@1.2.1` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
868
+ | `pi install git:github.com/Vt-mmm/piagent@v1.2.1` | Install pinned release cho reproducible team setup. |
859
869
  | `pi install git:github.com/Vt-mmm/piagent` | Install latest platform package cho máy cá nhân/sandbox. |
860
870
  | Cài exact Pi host của release, rồi `npm install -g --ignore-scripts @piagent/platform@X.Y.Z` và `piagent-install --stable` | Full update: đồng bộ host, terminal helper và Pi package. Mỗi release pin một Pi host chính xác; lấy đúng version của release đang cài trong [release/install policy](release-install-policy.md). |
861
871
  | Cài exact Pi host ghi trong release cũ, rồi helper `vPREVIOUS` và `piagent-install --stable` | Full rollback; đánh giá lại dependency findings của host cũ trước khi hạ version. |
@@ -880,7 +890,9 @@ Watchdog là optional adversarial reviewer ở cuối turn, không bật mặc
880
890
  | `pi --fork <id-or-file>` | Fork session cũ sang session mới. |
881
891
  | `pi --name "<name>"` | Đặt tên session. |
882
892
  | `pi --tools read,grep,find,ls -p "Review src"` | Read-only one-shot. |
883
- | `piagent-usage /path/to/project` | Exact token/cost stats. |
893
+ | `piagent-usage /path/to/project` | Exact token/cost stats của session mới nhất. |
894
+ | `piagent-usage --history /path/to/project --days 7` | Tổng usage lịch sử của project. |
895
+ | `piagent-usage --history --all-projects --days 7 --csv` | CSV report cuối tuần toàn máy. |
884
896
  | `piagent-mcp --preset core --scope global --replace` | Setup or update the governed MCP baseline. |
885
897
  | `piagent-subagents --preset safe` | Setup subagents baseline. |
886
898
  | `bash scripts/verify-local.sh` | Verify platform repo. |
@@ -952,7 +964,7 @@ Mở lại Pi session sau khi install.
952
964
  Cài lại terminal helper đúng release rồi kiểm tra `PATH`:
953
965
 
954
966
  ```bash
955
- npm install -g --ignore-scripts @piagent/platform@1.2.0
967
+ npm install -g --ignore-scripts @piagent/platform@1.2.1
956
968
  command -v piagent-install
957
969
  ```
958
970
 
@@ -53,6 +53,7 @@ pi install /path/to/piagent
53
53
  - `1.1.8`: Pi báo ngay ở đầu session khi có release mới, kèm lệnh chạy update.
54
54
  - `1.1.9`: notice update hiện đúng trên bản cài bằng `pi install git:` — bản cài nhận diện theo vị trí thư mục, không theo `.git`.
55
55
  - `1.2.0`: `piagent-mcp` quản lý server đầy đủ, server do repo mang theo phải được duyệt mới dùng được, và `/piagent-mcp` là command trong session chứ không phải nhờ model chạy bash.
56
+ - `1.2.1`: approval gate thật sự phủ direct tool và server đến qua `imports` — hai đường vòng mà bản `1.2.0` để hở; kèm một loạt fix về containment khi vendor, độ sâu shell guard, và credential trong URL.
56
57
 
57
58
  ## Không publish
58
59
 
@@ -10,7 +10,7 @@ This file is the canonical install, update, rollback, and release checklist. Oth
10
10
 
11
11
  All supported environments require Node.js `>=22.19.0` and Pi Coding Agent `0.82.0`. The Pi host is installed as a Node CLI; Pi Agent Platform still defines its own release matrix because the terminal helpers and shell policy rely on Bash/POSIX behavior.
12
12
 
13
- | Surface | Status for v1.2.0 | Rollout guidance |
13
+ | Surface | Status for v1.2.1 | Rollout guidance |
14
14
  |---|---|---|
15
15
  | macOS Apple Silicon (`darwin/arm64`) + Bash | Verified for this release. | Safe default for team rollout after normal project smoke tests. |
16
16
  | Linux x64 + Bash | Verified in GitHub Actions. | Safe default for CI/server usage after normal project smoke tests. |
@@ -33,7 +33,7 @@ These three components are versioned independently. A full install, update, or r
33
33
 
34
34
  | Channel | Source shape | Mutability | Use when |
35
35
  |---|---|---:|---|
36
- | `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.2.0` | Fixed after resolution | Default for team rollout. |
36
+ | `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.2.1` | Fixed after resolution | Default for team rollout. |
37
37
  | `exact` | Tag, reviewed commit, or a tag resolved with `--resolve-tag` | Fixed when using a commit SHA | Pi-package-only roll forward, rollback, or reproduction. |
38
38
  | `dev` | `git:github.com/Vt-mmm/piagent` | Moving | Personal machine or sandbox only. |
39
39
  | `local` | `/path/to/piagent` | Local workspace | Platform development and dry-run validation. |
@@ -53,7 +53,7 @@ Use this flow when the team needs both terminal commands and the Pi package:
53
53
  ```bash
54
54
  node --version # >= 22.19.0
55
55
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
56
- npm install -g --ignore-scripts @piagent/platform@1.2.0
56
+ npm install -g --ignore-scripts @piagent/platform@1.2.1
57
57
  piagent-install --stable --dry-run
58
58
  piagent-install --stable
59
59
  ```
@@ -61,8 +61,8 @@ piagent-install --stable
61
61
  The stable preview and apply output includes:
62
62
 
63
63
  ```text
64
- currentRelease: v1.2.0 (helper package version)
65
- tag: v1.2.0
64
+ currentRelease: v1.2.1 (helper package version)
65
+ tag: v1.2.1
66
66
  resolvedCommit: <40-char-sha>
67
67
  source: git:github.com/Vt-mmm/piagent@<40-char-sha>
68
68
  ```
@@ -79,7 +79,7 @@ bash scripts/install-global.sh --stable
79
79
  Use this only when terminal commands are not needed:
80
80
 
81
81
  ```bash
82
- pi install git:github.com/Vt-mmm/piagent@v1.2.0
82
+ pi install git:github.com/Vt-mmm/piagent@v1.2.1
83
83
  ```
84
84
 
85
85
  Direct `pi install` does not create `piagent-*` commands on `PATH`.
@@ -21,7 +21,7 @@ This document covers the packaged guard, project profiles, capability locks, ins
21
21
  - The operator, operating-system account, Pi host, and explicitly trusted project repository are trusted to execute code.
22
22
  - Project-local Pi resources are loaded only after Pi project trust or an explicit operator override.
23
23
  - A human remains available to approve destructive and external-provider actions.
24
- - Supported release verification covers macOS Apple Silicon + Bash, Linux x64 + Bash, Node.js `>=22.19.0`, and the exact Pi host version declared by the release. macOS Intel + Bash and Linux ARM64 + Bash are supported targets that need local smoke verification before broad rollout. Native Windows is not a team-rollout target for v1.2.0, and WSL2 is experimental/unverified.
24
+ - Supported release verification covers macOS Apple Silicon + Bash, Linux x64 + Bash, Node.js `>=22.19.0`, and the exact Pi host version declared by the release. macOS Intel + Bash and Linux ARM64 + Bash are supported targets that need local smoke verification before broad rollout. Native Windows is not a team-rollout target for v1.2.1, and WSL2 is experimental/unverified.
25
25
  - Registry, GitHub, model-provider, MCP-provider, and Vercel controls are external dependencies. Their account security and platform guarantees are not replaced by this repository.
26
26
 
27
27
  ## Threat actors and failure modes
@@ -40,12 +40,13 @@ The final category is outside the containment capability of the Piagent guard. I
40
40
  | Vector | Current control | Residual risk |
41
41
  |---|---|---|
42
42
  | Direct or nested path-like tool input | Bounded recursive input inspection, protected-path precedence, repository boundary checks, filesystem capability scopes, and read-only profile paths | A new carrier or field name must be classified before it can be treated as controlled. Unknown/ambiguous tools fail closed where policy requires confirmation. |
43
+ | Shell glob reaching a protected file | Glob candidates are read from the same expanded, resolved word list as every other check and matched against examples of each protected pattern, so a pattern assembled by an expansion (`{cat,.env*}`, `cat $({echo,.env*})`, `{bash,} -c 'cat .env*'`, `echo .env* | xargs cat`) is seen. This is the only check that can answer for a pattern at all, since `.env*` matches no protected literal. Whether a word is matched against filenames is decided per character, so a quoted metacharacter names a file instead (`{".env*",x}`). A redirection is read by the rule the shell uses — it opens its target only where that target expands to exactly one word, and is refused as an ambiguous redirect otherwise (`> "{.env,}"{,}`, `> {.env,other}`), while a word that can still disappear, an unset variable or a pattern under `nullglob`, leaves the count unknowable and keeps every candidate | Matching is against generated examples of each protected pattern, not against the filesystem, so a protected pattern whose examples do not represent every real name it covers can leave a matching file unnamed. Bounded expansion refuses rather than enumerates when a glob is too broad to expand. |
43
44
  | Encoding, case variation, traversal, or symbolic-link alias | Single percent decode, case-insensitive protected matching, canonical resolution for existing paths, traversal/repository escape rejection | Filesystem races and code executing outside observed tool paths require an OS boundary. |
44
- | Shell glob, brace, redirection, wrapper, assignment, or command composition | Static Bash operand/action analysis with bounded expansion and regression cases; protected input is denied and external mutation is confirmation-gated | Runtime-generated interpreter payloads cannot be fully understood by static parsing. Sensitive output redaction is a backstop, not containment. |
45
+ | Shell glob, brace, redirection, wrapper, assignment, or command composition | Static Bash operand/action analysis with bounded expansion and regression cases; protected input is denied and external mutation is confirmation-gated. A path the analysis cannot resolve — literal text concatenated with a substitution, as in `.en$(echo v)` — is refused rather than matched on its literal half | Runtime-generated interpreter payloads cannot be fully understood by static parsing. Sensitive output redaction is a backstop, not containment. |
45
46
  | Secret returned in tool output | Shared contextual redaction across guarded text/results and synthetic recall benchmark | High-entropy, novel secret formats, or transformed output such as base64-encoded content can evade pattern-based redaction. Never intentionally print real secrets. |
46
47
  | Malicious project profile or relaxed capability lock | Trust-aware profile loading, deterministic lock digest, base-policy/source integrity, protected self-state, and fail-closed verification | An already trusted malicious repository can execute code through mechanisms outside the guard. |
47
48
  | External GitHub/HTTP/MCP/provider mutation | Direct and proxied action classification plus explicit operator confirmation, including guarded shell carriers | Provider semantics and new tool schemas can change; new integrations require policy and regression review. |
48
- | Destructive local action | Exec-policy classification and explicit confirmation; protected paths remain denied in every permission profile | Confirmation is not isolation. A trusted process outside the observed tool path retains OS authority. |
49
+ | Destructive local action | Exec-policy classification and explicit confirmation; protected paths remain denied in every permission profile. A target written as a command substitution is evaluated only where its body is one plain command with literal operands and a format this renderer reproduces exactly, and confirmed otherwise — a separator, a nested substitution, a redirection, a variable, quoting, an escape, a `printf` option, or a `printf` conversion whose result this does not reproduce (`*`, `%q`, `%b`, the numeric conversions, `%%`) all make the body unreadable here. A constant width or precision and format reuse are reproduced, so a target built from those is refused rather than confirmed, and the rendered output is read as a path candidate as well as the format's literal text and its arguments — a name assembled across a conversion (`.e%.1sv nv` is `.env`) appears in none of the pieces. Brace expansion, ranges included, is performed once per segment before anything reads it, so the rules, the legacy patterns, the nested-interpreter scan, the pipeline producers and the destructive checks all see the word list the shell would build rather than the text as typed; the expansion produces a token stream, so a command and its arguments assembled from one brace word (`{cat,.env}`) are read as both; quoting is tracked per character, so a word holding a quoted group beside an expanding one (`"{.env,}"{,}`) expands exactly as far as the shell expands it — and a target that is nothing but an unresolved parameter expansion is confirmed rather than permitted, since it can hold any value including root. The same holds for reads: a word that is only an unresolved parameter expansion is confirmed wherever the command opens files (`cat ${HOME:+.env}`), including across a pipe into an `xargs` that opens them (`echo $F \| xargs cat`), while a command substitution in that position is left to the nested scan that classifies the command inside it, and two expansions with nothing between them count as a name being assembled just as literal text beside one does. Brace expansion, parameter expansion and command substitution are performed once per segment, in the order a shell performs them, and every reader takes that one word list — so a name assembled by an expansion (`r$(printf %s m) -rf /`, `xa$(printf %s rgs)`, `$(printf %s ba)$(echo -n sh) -c '...'`) is the command it becomes rather than the text it was typed as. A name that still cannot be resolved is treated as an unknown command rather than a known-harmless one: it is enough to bring the destructive checks, the interpreter scan and the file-operand net into play, since `${X:+r}m -rf /` and `${X:+cat} ${X:+.env}` are those commands whenever the variable is set. Differential cases are generated rather than enumerated, over the cross product the defects live in — expansion form, position in the command, and reader — with each spelling placed in the command name, the operand, a flag's argument, a redirection target, a pipeline producer, an `xargs` consumer and an interpreter payload; the corpus fails if any generated spelling produced no word list, so a batch that dies early cannot pass as coverage, and it asserts in both directions so a check that answers for everything fails too | Confirmation is not isolation. A trusted process outside the observed tool path retains OS authority. A target produced by any other command is knowable only at run time, so it is gated by confirmation rather than refused. |
49
50
  | Forged verification result | Observed Bash result ledger, command hash, task-start ordering, exit-code and verify-command matching | Compromise of the trusted Pi/process environment is outside this evidence model. |
50
51
  | Mutable or substituted release source | Exact team tags, stable tag-to-commit resolution, release-commit binding in tag CI, annotated-tag requirement, add-on integrity checks, and documented tag ruleset requirement | Bootstrap helper installation still relies on GitHub tag immutability; repository administrators must prevent tag update/deletion. Signed provenance remains future work. |
51
52
  | Dependency or package-content compromise | `--ignore-scripts` bootstrap installs, root and exact runtime dependency audits, immutable Action SHAs, package allowlist, artifact tests, CodeQL, Dependabot, secret scanning, and push protection | Audits are point-in-time and lower-severity/upstream findings remain. Review alerts and refresh pins continuously. |
@@ -9,7 +9,7 @@ Một thành viên mới không cần biết local path của maintainer. Luồn
9
9
  ```bash
10
10
  node --version # >= 22.19.0
11
11
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
12
- npm install -g --ignore-scripts @piagent/platform@1.2.0
12
+ npm install -g --ignore-scripts @piagent/platform@1.2.1
13
13
  piagent-install --stable
14
14
  cd /path/to/project
15
15
  pi
@@ -42,7 +42,7 @@ Mặc định team dùng stable helper:
42
42
  ```bash
43
43
  node --version # >= 22.19.0
44
44
  npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
45
- npm install -g --ignore-scripts @piagent/platform@1.2.0
45
+ npm install -g --ignore-scripts @piagent/platform@1.2.1
46
46
  piagent-install --stable --dry-run
47
47
  piagent-install --stable
48
48
  ```
@@ -70,7 +70,7 @@ piagent-update --check
70
70
  piagent-update --project /path/to/project
71
71
  ```
72
72
 
73
- `--check` chỉ báo version, không đụng gì. Lên đúng một bản cụ thể thì `piagent-update --version 1.2.0`.
73
+ `--check` chỉ báo version, không đụng gì. Lên đúng một bản cụ thể thì `piagent-update --version 1.2.1`.
74
74
 
75
75
  Không cần ai nhớ đi kiểm tra: khi có release mới, Pi tự báo ngay ở dòng notice lúc mở session, kèm luôn lệnh chạy.
76
76
 
@@ -88,6 +88,35 @@ Ví dụ output:
88
88
  }
89
89
  ```
90
90
 
91
+ ## Usage lịch sử / report tuần
92
+
93
+ `piagent-usage /path/to/project` chỉ trả exact stats của session mới nhất. Để xem tổng usage đã lưu trên máy, kể cả session đã end hoặc subagent runs:
94
+
95
+ ```bash
96
+ piagent-usage --history /path/to/project
97
+ piagent-usage --history /path/to/project --days 7
98
+ piagent-usage --history --all-projects --days 7 --csv
99
+ piagent-usage --history --all-projects --since 2026-07-20 --until 2026-07-26 --json
100
+ ```
101
+
102
+ History mode đọc trực tiếp `~/.pi/agent/sessions/**/*.jsonl`, cộng usage từ từng assistant message:
103
+
104
+ - `input`, `output`, `cacheRead`, `cacheWrite`, `reasoning`, `totalTokens`;
105
+ - `cost.total`;
106
+ - số user messages, assistant messages, tool calls, tool results;
107
+ - breakdown theo project và top sessions.
108
+
109
+ Mặc định history mode **bao gồm subagent session files** vì đó là usage thật của máy. Dùng `--no-subagents` khi chỉ muốn parent/main sessions.
110
+
111
+ Format hỗ trợ:
112
+
113
+ | Format | Lệnh |
114
+ |---|---|
115
+ | Human table | `piagent-usage --history <project>` |
116
+ | JSON | `piagent-usage --history <project> --json` |
117
+ | CSV | `piagent-usage --history <project> --csv` |
118
+ | Markdown | `piagent-usage --history <project> --markdown` |
119
+
91
120
  ## Cách đọc số
92
121
 
93
122
  | Field | Ý nghĩa |