@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
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -24,7 +24,7 @@
24
24
  </div>
25
25
  <div class="card">
26
26
  <h3 id="enforce-tool-call">Enforce ở tool call</h3>
27
- <p>Guard chặn cả proxy (<span class="inline-code">mcp</span> với <span class="inline-code">input.server</span>) lẫn dạng direct tool (<span class="inline-code">mcp__&lt;server&gt;__&lt;tool&gt;</span>). Gate chỉ đọc proxy sẽ bị đi vòng bằng cách bật <span class="inline-code">directTools</span>.</p>
27
+ <p>Guard đọc cả proxy (<span class="inline-code">mcp</span> với <span class="inline-code">input.server</span>) lẫn direct tool. Gate chỉ đọc proxy sẽ bị đi vòng bằng cách bật <span class="inline-code">directTools</span>.</p>
28
28
  </div>
29
29
  </div>
30
30
  </section>
@@ -40,6 +40,54 @@
40
40
  <p>Server ở <span class="inline-code">global</span> và <span class="inline-code">pi-global</span> 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 người dùng duyệt config của chính mình trên từng máy là vô nghĩa.</p>
41
41
  </section>
42
42
 
43
+ <section class="section">
44
+ <h2 id="imports">Server đến qua <span class="inline-code">imports</span></h2>
45
+
46
+ <p>Config MCP nhận thêm key <span class="inline-code">imports</span>, 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 nằm trong bốn scope nào, nên nó là đường vòng thứ hai quanh gate — và không cần bật <span class="inline-code">directTools</span>.</p>
47
+
48
+ <p>Trong sáu kind, chỉ <span class="inline-code">vscode</span> trỏ vào đường dẫn <strong>trong project</strong> (<span class="inline-code">.vscode/mcp.json</span>). Clone repo là mang theo file đó, nên server của nó phải duyệt như mọi server repo khai báo thẳng.</p>
49
+
50
+ <div class="table-wrap">
51
+ <table>
52
+ <thead>
53
+ <tr><th>Tình huống</th><th>Xử lý</th></tr>
54
+ </thead>
55
+ <tbody>
56
+ <tr>
57
+ <td>Layer bất kỳ import kind repo-relative</td>
58
+ <td>Server phải duyệt, kể cả khi <span class="inline-code">global</span> khai báo — nội dung đọc lại vẫn nằm trong repo.</td>
59
+ </tr>
60
+ <tr>
61
+ <td>Repo scope import kind bất kỳ</td>
62
+ <td>Server phải duyệt. Repo đang chọn hộ máy này chạy server nào.</td>
63
+ </tr>
64
+ <tr>
65
+ <td>Repo scope import kind không đọc được nội dung (<span class="inline-code">codex</span>, TOML)</td>
66
+ <td>Chặn mọi tool call, khi file đó có thật trên máy. Từ chối ở dòng khai báo: liệt kê được server của năm kind kia không nói gì về kind thứ sáu. Khai một kind máy này không cài thì là config cần dọn, không phải session cần dừng.</td>
67
+ </tr>
68
+ <tr>
69
+ <td><span class="inline-code">remove</span> / <span class="inline-code">enable</span> / <span class="inline-code">disable</span> trên server đến qua <span class="inline-code">imports</span></td>
70
+ <td>Từ chối, kèm tên file thật cần sửa. File được định vị là config của công cụ kia; ghi ngược bằng writer của platform sẽ viết lại file Cursor/VS Code của người dùng theo format chính công cụ đó không dùng.</td>
71
+ </tr>
72
+ <tr>
73
+ <td><span class="inline-code">directTools</span> + <span class="inline-code">toolPrefix: "none"</span> trong config repo mang theo</td>
74
+ <td>Chặn mọi tool call, không riêng lời gọi MCP. Không prefix thì tên tool không còn dấu vết server, không truy được để đối chiếu quyết định — mà chặn riêng proxy <span class="inline-code">mcp</span> thì đúng những cái tên trần cần chặn lại đi thẳng qua.</td>
75
+ </tr>
76
+ </tbody>
77
+ </table>
78
+ </div>
79
+
80
+ <p><span class="inline-code">piagent-mcp doctor</span> nêu tên file có <span class="inline-code">imports</span> và các kind của nó, nêu layer không parse được, và nêu cả import target <em>có thật nhưng không đọc được</em> — cả ba trạng thái trước đây đều đọc ra rỗng, không phân biệt được với &ldquo;không khai server nào&rdquo;.</p>
81
+
82
+ <p>Với hai trạng thái chặn-toàn-bộ ở trên, <span class="inline-code">doctor</span> và <span class="inline-code">list</span> in ra lý do và exit 1 — kể cả <span class="inline-code">list --json</span>, vốn trả <span class="inline-code">servers: []</span> với exit 0 đúng lúc không gì chạy được. Exit code trả lời &ldquo;MCP ở đây có chạy được không&rdquo;, không phải &ldquo;có in ra dòng nào không&rdquo;. Cả ba surface — guard, <span class="inline-code">/piagent-mcp</span>, CLI — đọc một state chung, vì 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.</p>
83
+
84
+ <p>Gate được cache theo project và 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 — <span class="inline-code">imports: ["codex"]</span> thành chặn đúng lúc file codex xuất hiện, và <span class="inline-code">directTools</span> thêm vào config global cá nhân là thứ guard đã load phải thấy ngay. Viết tay danh sách đó là cách nó lệch.</p>
85
+
86
+ <p>Adapter merge <span class="inline-code">settings</span> của cả bốn layer thành một block cho cả session. Nên <span class="inline-code">directTools</span> đặt ở file này và <span class="inline-code">toolPrefix: "none"</span> đặt ở file kia vẫn ra đúng tổ hợp làm gate mất chỗ bám, trong khi đọc riêng từng file thì không file nào có vấn đề. Settings được đọc sau merge, và báo cả hai file.</p>
87
+
88
+ <p>Cột <span class="inline-code">SCOPE</span> là layer khai báo <span class="inline-code">imports</span>, không phải nơi server được định nghĩa. Server import qua config <span class="inline-code">global</span> vẫn cần duyệt, vì file đọc lại nằm trong clone.</p>
89
+ </section>
90
+
43
91
  <section class="section">
44
92
  <h2 id="thiet-ke">Bốn quyết định thiết kế</h2>
45
93
 
@@ -61,9 +109,13 @@
61
109
  <td>Pin theo digest của định nghĩa</td>
62
110
  <td>Duyệt là duyệt <em>đúng</em> command và URL đó. Đổi command, đổi args, đổi URL thì server quay về <span class="inline-code">pending</span> chứ không kế thừa quyết định cũ.</td>
63
111
  </tr>
112
+ <tr>
113
+ <td>Preview in ra bản đã merge, digest tính trên mảnh của repo</td>
114
+ <td>Adapter merge server trùng tên theo từng key qua các layer, nên repo khai mỗi <span class="inline-code">{"args": [...]}</span> vẫn lấy <span class="inline-code">command</span> từ layer dưới. Preview phải là thứ sẽ chạy thật. Digest thì giữ trên phần repo kiểm soát — băm cả bản merge sẽ đẩy server về <span class="inline-code">pending</span> mỗi lần người dùng sửa config global của chính mình.</td>
115
+ </tr>
64
116
  <tr>
65
117
  <td>Chặn ở <span class="inline-code">tool_call</span></td>
66
- <td>Cả dạng proxy <span class="inline-code">mcp</span> lẫn dạng direct <span class="inline-code">mcp__&lt;server&gt;__&lt;tool&gt;</span>, để không có đường vòng.</td>
118
+ <td>Cả dạng proxy <span class="inline-code">mcp</span> lẫn direct tool, để không có đường vòng. Direct tool được truy theo tên server đang cấu hình, vì prefix của nó chính là tên server.</td>
67
119
  </tr>
68
120
  </tbody>
69
121
  </table>
@@ -4,7 +4,7 @@
4
4
  <article class="step">
5
5
  <div>
6
6
  <h3 id="cai-hai-lenh">Cài — hai lệnh</h3>
7
- <p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.2.0</span> đang pin. Chạy nó ngay trong thư mục project — nó cài Pi package, khởi tạo <span class="inline-code">.pi/</span>, cài MCP baseline cùng subagents, rồi chạy doctor. Thêm <span class="inline-code">--no-mcp</span>, <span class="inline-code">--no-subagents</span>, <span class="inline-code">--no-herdr</span> hoặc <span class="inline-code">--global-only</span> để bỏ bớt.</p>
7
+ <p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.2.1</span> đang pin. Chạy nó ngay trong thư mục project — nó cài Pi package, khởi tạo <span class="inline-code">.pi/</span>, cài MCP baseline cùng subagents, rồi chạy doctor. Thêm <span class="inline-code">--no-mcp</span>, <span class="inline-code">--no-subagents</span>, <span class="inline-code">--no-herdr</span> hoặc <span class="inline-code">--global-only</span> để bỏ bớt.</p>
8
8
  <div class="table-wrap compact-table">
9
9
  <table>
10
10
  <thead>
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -118,7 +118,7 @@
118
118
  </div>
119
119
  <div class="card">
120
120
  <h3 id="enforce-tool-call">Enforce ở tool call</h3>
121
- <p>Guard chặn cả proxy (<span class="inline-code">mcp</span> với <span class="inline-code">input.server</span>) lẫn dạng direct tool (<span class="inline-code">mcp__&lt;server&gt;__&lt;tool&gt;</span>). Gate chỉ đọc proxy sẽ bị đi vòng bằng cách bật <span class="inline-code">directTools</span>.</p>
121
+ <p>Guard đọc cả proxy (<span class="inline-code">mcp</span> với <span class="inline-code">input.server</span>) lẫn direct tool. Gate chỉ đọc proxy sẽ bị đi vòng bằng cách bật <span class="inline-code">directTools</span>.</p>
122
122
  </div>
123
123
  </div>
124
124
  </section>
@@ -134,6 +134,54 @@
134
134
  <p>Server ở <span class="inline-code">global</span> và <span class="inline-code">pi-global</span> 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 người dùng duyệt config của chính mình trên từng máy là vô nghĩa.</p>
135
135
  </section>
136
136
 
137
+ <section class="section">
138
+ <h2 id="imports">Server đến qua <span class="inline-code">imports</span></h2>
139
+
140
+ <p>Config MCP nhận thêm key <span class="inline-code">imports</span>, 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 nằm trong bốn scope nào, nên nó là đường vòng thứ hai quanh gate — và không cần bật <span class="inline-code">directTools</span>.</p>
141
+
142
+ <p>Trong sáu kind, chỉ <span class="inline-code">vscode</span> trỏ vào đường dẫn <strong>trong project</strong> (<span class="inline-code">.vscode/mcp.json</span>). Clone repo là mang theo file đó, nên server của nó phải duyệt như mọi server repo khai báo thẳng.</p>
143
+
144
+ <div class="table-wrap">
145
+ <table>
146
+ <thead>
147
+ <tr><th>Tình huống</th><th>Xử lý</th></tr>
148
+ </thead>
149
+ <tbody>
150
+ <tr>
151
+ <td>Layer bất kỳ import kind repo-relative</td>
152
+ <td>Server phải duyệt, kể cả khi <span class="inline-code">global</span> khai báo — nội dung đọc lại vẫn nằm trong repo.</td>
153
+ </tr>
154
+ <tr>
155
+ <td>Repo scope import kind bất kỳ</td>
156
+ <td>Server phải duyệt. Repo đang chọn hộ máy này chạy server nào.</td>
157
+ </tr>
158
+ <tr>
159
+ <td>Repo scope import kind không đọc được nội dung (<span class="inline-code">codex</span>, TOML)</td>
160
+ <td>Chặn mọi tool call, khi file đó có thật trên máy. Từ chối ở dòng khai báo: liệt kê được server của năm kind kia không nói gì về kind thứ sáu. Khai một kind máy này không cài thì là config cần dọn, không phải session cần dừng.</td>
161
+ </tr>
162
+ <tr>
163
+ <td><span class="inline-code">remove</span> / <span class="inline-code">enable</span> / <span class="inline-code">disable</span> trên server đến qua <span class="inline-code">imports</span></td>
164
+ <td>Từ chối, kèm tên file thật cần sửa. File được định vị là config của công cụ kia; ghi ngược bằng writer của platform sẽ viết lại file Cursor/VS Code của người dùng theo format chính công cụ đó không dùng.</td>
165
+ </tr>
166
+ <tr>
167
+ <td><span class="inline-code">directTools</span> + <span class="inline-code">toolPrefix: "none"</span> trong config repo mang theo</td>
168
+ <td>Chặn mọi tool call, không riêng lời gọi MCP. Không prefix thì tên tool không còn dấu vết server, không truy được để đối chiếu quyết định — mà chặn riêng proxy <span class="inline-code">mcp</span> thì đúng những cái tên trần cần chặn lại đi thẳng qua.</td>
169
+ </tr>
170
+ </tbody>
171
+ </table>
172
+ </div>
173
+
174
+ <p><span class="inline-code">piagent-mcp doctor</span> nêu tên file có <span class="inline-code">imports</span> và các kind của nó, nêu layer không parse được, và nêu cả import target <em>có thật nhưng không đọc được</em> — cả ba trạng thái trước đây đều đọc ra rỗng, không phân biệt được với &ldquo;không khai server nào&rdquo;.</p>
175
+
176
+ <p>Với hai trạng thái chặn-toàn-bộ ở trên, <span class="inline-code">doctor</span> và <span class="inline-code">list</span> in ra lý do và exit 1 — kể cả <span class="inline-code">list --json</span>, vốn trả <span class="inline-code">servers: []</span> với exit 0 đúng lúc không gì chạy được. Exit code trả lời &ldquo;MCP ở đây có chạy được không&rdquo;, không phải &ldquo;có in ra dòng nào không&rdquo;. Cả ba surface — guard, <span class="inline-code">/piagent-mcp</span>, CLI — đọc một state chung, vì 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.</p>
177
+
178
+ <p>Gate được cache theo project và 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 — <span class="inline-code">imports: ["codex"]</span> thành chặn đúng lúc file codex xuất hiện, và <span class="inline-code">directTools</span> thêm vào config global cá nhân là thứ guard đã load phải thấy ngay. Viết tay danh sách đó là cách nó lệch.</p>
179
+
180
+ <p>Adapter merge <span class="inline-code">settings</span> của cả bốn layer thành một block cho cả session. Nên <span class="inline-code">directTools</span> đặt ở file này và <span class="inline-code">toolPrefix: "none"</span> đặt ở file kia vẫn ra đúng tổ hợp làm gate mất chỗ bám, trong khi đọc riêng từng file thì không file nào có vấn đề. Settings được đọc sau merge, và báo cả hai file.</p>
181
+
182
+ <p>Cột <span class="inline-code">SCOPE</span> là layer khai báo <span class="inline-code">imports</span>, không phải nơi server được định nghĩa. Server import qua config <span class="inline-code">global</span> vẫn cần duyệt, vì file đọc lại nằm trong clone.</p>
183
+ </section>
184
+
137
185
  <section class="section">
138
186
  <h2 id="thiet-ke">Bốn quyết định thiết kế</h2>
139
187
 
@@ -155,9 +203,13 @@
155
203
  <td>Pin theo digest của định nghĩa</td>
156
204
  <td>Duyệt là duyệt <em>đúng</em> command và URL đó. Đổi command, đổi args, đổi URL thì server quay về <span class="inline-code">pending</span> chứ không kế thừa quyết định cũ.</td>
157
205
  </tr>
206
+ <tr>
207
+ <td>Preview in ra bản đã merge, digest tính trên mảnh của repo</td>
208
+ <td>Adapter merge server trùng tên theo từng key qua các layer, nên repo khai mỗi <span class="inline-code">{"args": [...]}</span> vẫn lấy <span class="inline-code">command</span> từ layer dưới. Preview phải là thứ sẽ chạy thật. Digest thì giữ trên phần repo kiểm soát — băm cả bản merge sẽ đẩy server về <span class="inline-code">pending</span> mỗi lần người dùng sửa config global của chính mình.</td>
209
+ </tr>
158
210
  <tr>
159
211
  <td>Chặn ở <span class="inline-code">tool_call</span></td>
160
- <td>Cả dạng proxy <span class="inline-code">mcp</span> lẫn dạng direct <span class="inline-code">mcp__&lt;server&gt;__&lt;tool&gt;</span>, để không có đường vòng.</td>
212
+ <td>Cả dạng proxy <span class="inline-code">mcp</span> lẫn direct tool, để không có đường vòng. Direct tool được truy theo tên server đang cấu hình, vì prefix của nó chính là tên server.</td>
161
213
  </tr>
162
214
  </tbody>
163
215
  </table>
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -98,7 +98,7 @@
98
98
  <article class="step">
99
99
  <div>
100
100
  <h3 id="cai-hai-lenh">Cài — hai lệnh</h3>
101
- <p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.2.0</span> đang pin. Chạy nó ngay trong thư mục project — nó cài Pi package, khởi tạo <span class="inline-code">.pi/</span>, cài MCP baseline cùng subagents, rồi chạy doctor. Thêm <span class="inline-code">--no-mcp</span>, <span class="inline-code">--no-subagents</span>, <span class="inline-code">--no-herdr</span> hoặc <span class="inline-code">--global-only</span> để bỏ bớt.</p>
101
+ <p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.2.1</span> đang pin. Chạy nó ngay trong thư mục project — nó cài Pi package, khởi tạo <span class="inline-code">.pi/</span>, cài MCP baseline cùng subagents, rồi chạy doctor. Thêm <span class="inline-code">--no-mcp</span>, <span class="inline-code">--no-subagents</span>, <span class="inline-code">--no-herdr</span> hoặc <span class="inline-code">--global-only</span> để bỏ bớt.</p>
102
102
  <div class="table-wrap compact-table">
103
103
  <table>
104
104
  <thead>
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
@@ -27,7 +27,7 @@
27
27
  <span>Pi Agent Platform</span>
28
28
  </a>
29
29
  <div class="top-actions">
30
- <span class="pill"><span class="pill-dot"></span>v1.2.0 docs</span>
30
+ <span class="pill"><span class="pill-dot"></span>v1.2.1 docs</span>
31
31
  <a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
32
32
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
33
33
  <path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -238,8 +238,13 @@ export function validateCapabilityPack(document, options = {}) {
238
238
 
239
239
  if (rejectUnknownKeys(spec.activation, new Set(["mode", "profiles", "triggers"]), `${source}.spec.activation`, errors)) {
240
240
  if (!ACTIVATION_MODES.has(spec.activation.mode)) pushError(errors, `${source}.spec.activation.mode`, "must be explicit, profile, or trigger");
241
- validateStringArray(spec.activation.profiles ?? [], `${source}.spec.activation.profiles`, errors, { maxItems: 64, pattern: NAME_PATTERN });
241
+ const profiles = validateStringArray(spec.activation.profiles ?? [], `${source}.spec.activation.profiles`, errors, { maxItems: 64, pattern: NAME_PATTERN });
242
242
  const triggers = validateStringArray(spec.activation.triggers ?? [], `${source}.spec.activation.triggers`, errors, { maxItems: 64 });
243
+ // Both modes name the thing that switches them on, so both are checked for
244
+ // it. Only `trigger` was, which let a pack declare profile activation and
245
+ // list no profiles: resolution then read `.includes` off an absent array
246
+ // and died with a TypeError where it should have refused with a reason.
247
+ if (spec.activation.mode === "profile" && profiles.length === 0) pushError(errors, `${source}.spec.activation.profiles`, "must not be empty for profile activation");
243
248
  if (spec.activation.mode === "trigger" && triggers.length === 0) pushError(errors, `${source}.spec.activation.triggers`, "must not be empty for trigger activation");
244
249
  }
245
250
 
@@ -859,7 +864,10 @@ export function resolveCapabilityProfileDocument(root, profile, options = {}) {
859
864
  const key = `${selector.name}@${selector.version}`;
860
865
  const directPack = byKey.get(key);
861
866
  if (!directPack) throw new CapabilityValidationError(`profile selects unknown capability pack ${key}`);
862
- if (directPack.activation.mode === "profile" && !directPack.activation.profiles.includes(profile.mode)) throw new CapabilityValidationError(`${key} is not enabled for profile mode ${profile.mode}`);
867
+ // Defaulted rather than assumed present. The validator now requires the list
868
+ // for this mode, but this line runs against whatever is on disk, and reading
869
+ // through to a TypeError would turn a refusal into a crash.
870
+ if (directPack.activation.mode === "profile" && !(directPack.activation.profiles ?? []).includes(profile.mode)) throw new CapabilityValidationError(`${key} is not enabled for profile mode ${profile.mode}`);
863
871
  select(key);
864
872
  }
865
873
 
@@ -995,22 +1003,51 @@ export function resolveCapabilityProfile(root, profilePath, options = {}) {
995
1003
  const GRANTING_PERMISSIONS = ["capabilities", "filesystemRead", "filesystemWrite", "networkDomains", "externalActions"];
996
1004
  const RESTRICTING_PERMISSIONS = ["protectedPaths", "readOnlyPaths", "shellProtectedPaths"];
997
1005
 
1006
+ // Everything above is about artifacts the platform ships. A pack vendored from
1007
+ // somewhere else is a different question, and gets a different answer.
1008
+ //
1009
+ // None of the reasons for leaving artifact content on the build side apply to
1010
+ // it. Its bytes change when that source decides they change, not when this
1011
+ // project updates. The manifest digest pins which artifacts a pack declares, not
1012
+ // what is in them, so a rewritten prompt or policy file keeps the same manifest.
1013
+ // And the argument that the platform can rewrite its own verifier is an argument
1014
+ // about the platform: an external source cannot, which is exactly why its
1015
+ // content is worth pinning. Prompts and policy files are what a hostile source
1016
+ // would edit, and it would rather not be asked again.
1017
+ //
1018
+ // `workspace` is the platform's own origin and is a reserved source name, so a
1019
+ // vendored source cannot claim it.
1020
+ function externalArtifacts(document) {
1021
+ const external = new Set(
1022
+ (document?.packs ?? [])
1023
+ .filter((pack) => pack?.origin && pack.origin !== "workspace")
1024
+ .map((pack) => `${pack.name}@${pack.version}`)
1025
+ );
1026
+ if (external.size === 0) return undefined;
1027
+ return (document?.artifacts ?? []).filter((artifact) => external.has(artifact?.pack));
1028
+ }
1029
+
998
1030
  function consentedShape(document) {
999
1031
  return {
1000
1032
  schemaVersion: document?.schemaVersion,
1001
1033
  apiVersion: document?.core?.apiVersion,
1002
1034
  packageSource: document?.core?.packageSource,
1003
1035
  profile: document?.profile,
1004
- packs: document?.packs
1036
+ packs: document?.packs,
1037
+ externalArtifacts: externalArtifacts(document)
1005
1038
  };
1006
1039
  }
1007
1040
 
1041
+ const CONSENT_DRIFT_REASONS = {
1042
+ externalArtifacts: "an artifact shipped by a vendored capability source changed content"
1043
+ };
1044
+
1008
1045
  function consentDrift(expected, actual) {
1009
1046
  const before = consentedShape(actual);
1010
1047
  const after = consentedShape(expected);
1011
1048
  return Object.keys(after)
1012
1049
  .filter((key) => stableJson(after[key]) !== stableJson(before[key]))
1013
- .map((key) => `${key} no longer matches the locked value`);
1050
+ .map((key) => CONSENT_DRIFT_REASONS[key] ?? `${key} no longer matches the locked value`);
1014
1051
  }
1015
1052
 
1016
1053
  function permissionDrift(expected, actual) {
@@ -110,6 +110,39 @@ function assertContainedDirectory(projectRoot, target, label) {
110
110
  return targetReal;
111
111
  }
112
112
 
113
+ /**
114
+ * Verify a path this code is about to create, empty or write sits inside the
115
+ * project, without requiring it to exist yet.
116
+ *
117
+ * The read path can lstat its target and be done with it. A write path cannot:
118
+ * the destination is normally absent, and the failure that matters is an
119
+ * *ancestor* leading somewhere else. A repository shipping `.pi` as a symlink is
120
+ * the whole attack — `mkdir -p` follows it, the directory that appears below is
121
+ * a real directory, so a check on the immediate parent sees nothing wrong, and
122
+ * the recursive delete that comes next lands outside the project.
123
+ *
124
+ * @param {string} rootReal a project root already through realpath
125
+ * @param {string} target
126
+ * @param {string} label
127
+ */
128
+ function assertContainedWritePath(rootReal, target, label) {
129
+ const relative = path.relative(rootReal, path.resolve(rootReal, target));
130
+ if (relative.startsWith("..") || path.isAbsolute(relative)) fail(`${label} resolves outside the project`);
131
+ let current = rootReal;
132
+ for (const segment of relative.split(path.sep)) {
133
+ if (!segment) continue;
134
+ current = path.join(current, segment);
135
+ let stat;
136
+ try {
137
+ stat = fs.lstatSync(current);
138
+ } catch {
139
+ // Absent from here down, so there is nothing further to follow.
140
+ return;
141
+ }
142
+ if (stat.isSymbolicLink()) fail(`${label} must not resolve through a symbolic link (${segment})`);
143
+ }
144
+ }
145
+
113
146
  function assertNoSymbolicLinks(root, label, budget = { remaining: MAX_VENDOR_ENTRIES }) {
114
147
  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
115
148
  if (budget.remaining-- <= 0) fail(`${label} contains more than ${MAX_VENDOR_ENTRIES} entries`);
@@ -212,27 +245,55 @@ export function vendorRemoteSource(projectRoot, entry) {
212
245
  const [validated] = validateCapabilitySources([entry]);
213
246
  if (!validated.source) fail(`capability source ${validated.name} is a local path and does not need vendoring`);
214
247
 
215
- const destination = vendorDirectoryFor(projectRoot, validated.name);
248
+ // Resolved the same way the read path resolves it, so the containment check
249
+ // below compares two paths of the same form.
250
+ const rootReal = fs.realpathSync(projectRoot);
251
+ const destination = vendorDirectoryFor(rootReal, validated.name);
252
+ const label = `capability source ${validated.name}`;
253
+
254
+ // Before anything is created and well before anything is removed: every call
255
+ // after this line writes or deletes, and the point of the check is to have
256
+ // happened first.
257
+ assertContainedWritePath(rootReal, destination, label);
216
258
  const parent = path.dirname(destination);
217
259
  fs.mkdirSync(parent, { recursive: true, mode: 0o755 });
218
- if (fs.lstatSync(parent).isSymbolicLink()) fail(`${parent} must not be a symbolic link`);
219
- fs.rmSync(destination, { recursive: true, force: true });
220
- fs.mkdirSync(destination, { mode: 0o755 });
260
+ // Re-checked after the directories exist. The first pass ran against a path
261
+ // that was mostly absent, so it proved nothing about the components this call
262
+ // just created or about one swapped underneath in between.
263
+ assertContainedWritePath(rootReal, destination, label);
264
+
265
+ // Fetched beside the destination and moved into place at the end, so a fetch
266
+ // that fails leaves the reviewed tree that was already there. Emptying the
267
+ // destination first meant a network error or a hostile archive destroyed a
268
+ // working vendor directory and left the project with neither version.
269
+ const staging = `${destination}.incoming.${process.pid}`;
270
+ assertContainedWritePath(rootReal, staging, label);
271
+ for (const sibling of fs.readdirSync(parent)) {
272
+ // Debris from a run that died between creating this and moving it.
273
+ if (sibling.startsWith(`${validated.name}.incoming.`)) fs.rmSync(path.join(parent, sibling), { recursive: true, force: true });
274
+ }
275
+ fs.mkdirSync(staging, { mode: 0o755 });
221
276
 
222
277
  try {
223
- if (validated.source.startsWith("npm:")) fetchNpmSource(validated.source.slice("npm:".length), destination);
224
- else fetchGitSource(validated.source, destination);
278
+ if (validated.source.startsWith("npm:")) fetchNpmSource(validated.source.slice("npm:".length), staging);
279
+ else fetchGitSource(validated.source, staging);
225
280
 
226
281
  // The fetched tree is untrusted until this passes. Checking here means a
227
- // hostile archive is rejected before anything reads a manifest out of it.
228
- assertNoSymbolicLinks(destination, `capability source ${validated.name}`);
229
- if (!fs.existsSync(path.join(destination, "packs"))) {
282
+ // hostile archive is rejected before anything reads a manifest out of it,
283
+ // and before it can replace the tree already in place.
284
+ assertNoSymbolicLinks(staging, `capability source ${validated.name}`);
285
+ if (!fs.existsSync(path.join(staging, "packs"))) {
230
286
  fail(`capability source ${validated.name} provides no packs directory`);
231
287
  }
232
- } catch (error) {
233
- // A half-written vendor directory would resolve on the next run and hide
234
- // the failure, so remove it.
288
+
289
+ // Everything above succeeded, so the old tree can go.
235
290
  fs.rmSync(destination, { recursive: true, force: true });
291
+ fs.renameSync(staging, destination);
292
+ } catch (error) {
293
+ // Only the half-written fetch is removed. The destination is either
294
+ // untouched, because the failure happened before the swap, or already
295
+ // replaced by a tree that passed every check.
296
+ fs.rmSync(staging, { recursive: true, force: true });
236
297
  throw error;
237
298
  }
238
299