@piagent/platform 1.11.1 → 1.12.0

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 (77) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +1 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +12 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +12 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +13 -1
  38. package/docs-site/en/workflows.html +1 -1
  39. package/docs-site/index.html +5 -5
  40. package/docs-site/mcp-approval.html +1 -1
  41. package/docs-site/mcp-auth.html +1 -1
  42. package/docs-site/mcp-servers.html +1 -1
  43. package/docs-site/mcp.html +1 -1
  44. package/docs-site/permissions.html +1 -1
  45. package/docs-site/profiles.html +1 -1
  46. package/docs-site/quickstart.html +1 -1
  47. package/docs-site/runtime.html +1 -1
  48. package/docs-site/scope.html +1 -1
  49. package/docs-site/security.html +1 -1
  50. package/docs-site/team.html +1 -1
  51. package/docs-site/whats-new.html +13 -1
  52. package/docs-site/workflows.html +1 -1
  53. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  54. package/package.json +1 -1
  55. package/packages/piagent-core/extensions/piagent-guard.ts +11 -5
  56. package/packages/piagent-core/extensions/source-checkout-paths.ts +31 -0
  57. package/packages/piagent-core/managed/agent-skills.mjs +44 -0
  58. package/packages/piagent-core/managed/doctor.mjs +23 -2
  59. package/packages/piagent-core/managed/language-environment.mjs +118 -0
  60. package/packages/piagent-core/managed/resource-loader.mjs +5 -4
  61. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +4 -1
  62. package/packages/piagent-core/managed/session.mjs +15 -3
  63. package/packages/piagent-core/managed/tool-boundary.mjs +110 -28
  64. package/packages/piagent-core/managed/tool-worker.mjs +3 -3
  65. package/packages/piagent-core/managed/toolchain.mjs +10 -8
  66. package/packages/piagent-core/package.json +1 -1
  67. package/packages/piagent-core/runtime/registration/agent-resources.ts +12 -0
  68. package/packages/piagent-core/runtime/resources/agent-resources.d.mts +6 -0
  69. package/packages/piagent-core/runtime/resources/agent-resources.mjs +140 -0
  70. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-D70xkf0d.js → MarkdownMessage-FWfOWZjV.js} +1 -1
  71. package/packages/piagent-webui/dist/client/assets/{index-BuIJR5rd.js → index-DFKHLVYk.js} +14 -14
  72. package/packages/piagent-webui/dist/client/index.html +1 -1
  73. package/packages/piagent-webui/gateway/gateway-service.ts +16 -0
  74. package/packages/piagent-webui/gateway/path-suggestions.ts +118 -0
  75. package/packages/piagent-webui/server/loopback-server.ts +2 -0
  76. package/packages/piagent-webui/server/member-routes.ts +22 -3
  77. package/packages/piagent-webui/server/transcript-projection.ts +7 -1
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · EN</span>
34
34
  <nav class="language-switch" aria-label="Language">
35
35
  <a class="language-option" href="/whats-new" hreflang="vi" lang="vi">VI</a>
36
36
  <a class="language-option current" href="/en/whats-new" hreflang="en" lang="en" aria-current="true">EN</a>
@@ -103,6 +103,18 @@
103
103
  <h1>1.9.0: ask in plain language, the team's Harness checks</h1>
104
104
  <p>The task contract and workflow commands are gone. Company mode asks for checks and a review on its own; Stop, network commands on approval and clearer approval cards.</p>
105
105
  </div>
106
+ <section class="section">
107
+ <p class="section-kicker">v1.12.0 · 2026-10-06</p>
108
+ <h2 id="v1-12-0">@ any folder, skills from /, and tests in the company sandbox</h2>
109
+ <p class="section-lead">Point the agent at exactly the file or folder to read, reuse the skills and commands you already keep, and let the agent run the project's unit and integration tests.</p>
110
+ <div class="grid cols-2">
111
+ <article class="card"><h3 id="at-mentions">@ files and folders</h3><p>Type <code>@</code> in the composer to pick the project's files and folders; type <code>~/</code> or <code>/</code> for any other folder on the Mac. The agent reads folders outside the project for reference, read-only; hidden home files, <code>~/Library</code> and credential files stay closed.</p></article>
112
+ <article class="card"><h3 id="kept-drafts">Unsent messages are kept</h3><p>A message typed in a conversation or in the new chat is still there after opening another conversation or reloading the page, with its attached files.</p></article>
113
+ <article class="card"><h3 id="skills-commands">Skills and commands</h3><p>Skills and commands in the project's and your <code>.claude</code>, <code>.codex</code>, <code>.agents</code> and <code>.pi</code> folders work as they are: the model sees the skills, and a <code>/</code> at the start of a message offers <code>/command</code> and <code>/skill:name</code>.</p></article>
114
+ <article class="card"><h3 id="tests-in-sandbox">Tests in the company sandbox</h3><p>Package caches kept per project (npm, pnpm, pip, uv, Go, Cargo, Gradle, Maven, NuGet…), servers and databases on localhost, the selected Xcode, JDKs, .NET. Run <code>piagent studio --doctor</code> to see which languages are ready. Docker and the iOS Simulator are not available in the sandbox yet.</p></article>
115
+ </div>
116
+ </section>
117
+
106
118
  <section class="section">
107
119
  <p class="section-kicker">v1.11.1 · 2026-10-05</p>
108
120
  <h2 id="v1-11-1">An update ends on the new release's dashboard</h2>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · EN</span>
34
34
  <nav class="language-switch" aria-label="Language">
35
35
  <a class="language-option" href="/workflows" hreflang="vi" lang="vi">VI</a>
36
36
  <a class="language-option current" href="/en/workflows" hreflang="en" lang="en" aria-current="true">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/" hreflang="en" lang="en">EN</a>
@@ -93,7 +93,7 @@
93
93
  <main>
94
94
  <div class="content-shell" data-article>
95
95
  <section class="hero">
96
- <p class="eyebrow">Piagent 1.11.1</p>
96
+ <p class="eyebrow">Piagent 1.12.0</p>
97
97
  <h1>Coding agent cho cả team, kiểm tra từng bước.</h1>
98
98
  <p>
99
99
  Một dashboard cho Terminal và trình duyệt. Dùng model bằng tài khoản của bạn, hoặc model của công ty qua Agent Watch và Agent Studio. Guard kiểm tra từng tool call; ở chế độ công ty, Harness của team tự yêu cầu chạy check, cho subagent review và báo kết quả ngay dưới câu trả lời.
@@ -107,7 +107,7 @@
107
107
  <div class="hero-actions">
108
108
  <a class="button primary" href="/quickstart">Bắt đầu</a>
109
109
  <a class="button" href="/ecosystem">Hệ sinh thái</a>
110
- <a class="button" href="/whats-new">Có gì mới trong 1.11.1</a>
110
+ <a class="button" href="/whats-new">Có gì mới trong 1.12.0</a>
111
111
  <a class="button" href="/commands">Xem command</a>
112
112
  </div>
113
113
  </section>
@@ -183,9 +183,9 @@ piagent-setup</code></pre>
183
183
  <h2 id="bat-dau-tu-dau">Bắt đầu từ đâu</h2>
184
184
  <div class="card-grid">
185
185
  <a class="doc-card" href="/whats-new">
186
- <span class="card-kicker">Bản 1.11.1</span>
186
+ <span class="card-kicker">Bản 1.12.0</span>
187
187
  <h3>Có gì mới</h3>
188
- <p>Cập nhật xong là dashboard chạy bản mới; ở 1.11.0: cập nhật Pi và Piagent bằng một nút, bảng lệnh ⌘K và Cài đặt tìm được.</p>
188
+ <p>@ chọn file và folder trên máy, giữ tin nhắn đang soạn, skill và command dùng bằng /, sandbox công ty chạy được test của nhiều ngôn ngữ.</p>
189
189
  </a>
190
190
  <a class="doc-card" href="/ecosystem">
191
191
  <span class="card-kicker">Get started</span>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/mcp-approval" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/mcp-approval" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/mcp-auth" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/mcp-auth" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/mcp-servers" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/mcp-servers" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/mcp" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/mcp" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/permissions" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/permissions" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/profiles" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/profiles" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/quickstart" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/quickstart" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/runtime" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/runtime" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/scope" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/scope" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/security" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/security" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/team" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/team" hreflang="en" lang="en">EN</a>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/whats-new" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/whats-new" hreflang="en" lang="en">EN</a>
@@ -103,6 +103,18 @@
103
103
  <h1>Bản 1.9.0: nói việc bằng lời, Harness của team kiểm tra</h1>
104
104
  <p>Bỏ task contract và lệnh workflow. Chế độ công ty tự yêu cầu check và review; nút Dừng, lệnh có mạng theo lần duyệt và thẻ duyệt dễ đọc hơn.</p>
105
105
  </div>
106
+ <section class="section">
107
+ <p class="section-kicker">v1.12.0 · 2026-10-06</p>
108
+ <h2 id="v1-12-0">@ tới mọi folder, skill dùng bằng /, và test chạy được trong sandbox công ty</h2>
109
+ <p class="section-lead">Chỉ cho agent đúng file hay folder cần đọc, dùng lại skill và command đã có, và để agent tự chạy unit test lẫn integration test của project.</p>
110
+ <div class="grid cols-2">
111
+ <article class="card"><h3 id="at-mentions">@ file và folder</h3><p>Gõ <code>@</code> trong khung chat để chọn file, folder của project; gõ <code>~/</code> hoặc <code>/</code> để chọn folder khác trên máy. Agent đọc được folder ngoài project, chỉ đọc; file ẩn trong home, <code>~/Library</code> và file chứa credential vẫn bị chặn.</p></article>
112
+ <article class="card"><h3 id="kept-drafts">Giữ tin nhắn đang soạn</h3><p>Tin nhắn gõ dở ở mỗi cuộc trò chuyện và ở khung chat mới vẫn còn khi chuyển sang cuộc trò chuyện khác hay tải lại trang, cùng file đã đính kèm.</p></article>
113
+ <article class="card"><h3 id="skills-commands">Skill và command</h3><p>Skill và command trong <code>.claude</code>, <code>.codex</code>, <code>.agents</code> và <code>.pi</code> của project và trên máy dùng được ngay: model thấy danh sách skill, gõ <code>/</code> ở đầu tin nhắn để chọn <code>/command</code> hoặc <code>/skill:tên</code>.</p></article>
114
+ <article class="card"><h3 id="tests-in-sandbox">Test trong sandbox công ty</h3><p>Cache package giữ theo project (npm, pnpm, pip, uv, Go, Cargo, Gradle, Maven, NuGet…), server và database trên localhost, Xcode đã chọn, JDK, .NET. Chạy <code>piagent studio --doctor</code> để xem ngôn ngữ nào sẵn sàng. Docker và iOS Simulator chưa dùng được trong sandbox.</p></article>
115
+ </div>
116
+ </section>
117
+
106
118
  <section class="section">
107
119
  <p class="section-kicker">v1.11.1 · 2026-10-05</p>
108
120
  <h2 id="v1-11-1">Cập nhật xong là dashboard chạy bản mới</h2>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.11.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.12.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/workflows" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/workflows" hreflang="en" lang="en">EN</a>
@@ -256,8 +256,8 @@
256
256
  },
257
257
  {
258
258
  "path": "tests",
259
- "fileCount": 518,
260
- "sha256": "92327b355aae9153c31a8dcf39be2b15b9ef2d0e9036e83b048c98ec5872f17a"
259
+ "fileCount": 521,
260
+ "sha256": "a070df51c583ffb57a918ece6e2afa9347e9f5a2fe895882232e9da6623be8d1"
261
261
  }
262
262
  ],
263
263
  "custodianComparisonRules": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -18,7 +18,7 @@ import {
18
18
  resolveRepositoryPathCandidate, shellGlobTargetsProtectedPath,
19
19
  unresolvedExpansionReason
20
20
  } from "./shell-reach.ts";
21
- import { grantedSourceCheckoutRootForPath, shellTouchesGrantedSourceCheckout } from "./source-checkout-paths.ts";
21
+ import { grantedSourceCheckoutRootForPath, piToolPathInput, referenceReadRootForPath, shellTouchesGrantedSourceCheckout } from "./source-checkout-paths.ts";
22
22
  import { experimentLoopDecision } from "./experiment-loop-policy.ts";
23
23
  import { extractShellWritePathCandidates } from "./shell-write-targets.js";
24
24
  import { isProjectMutatingShellCommand } from "./readonly-inline-inspection.ts";
@@ -163,6 +163,8 @@ import { finalGateConfig } from "./acceptance-diagnostic-policy.ts";
163
163
  import { fallbackBasePolicy } from "./fallback-base-policy.ts";
164
164
  import { prefixCompletions, registerPiagentTool as registerToolDefinition, registerRuntimeCommand, registerRuntimeTool } from "../runtime/registration/extension-registration.ts";
165
165
  import { registerTaskEvidenceTools } from "../runtime/registration/task-evidence-tools.ts";
166
+ import { registerAgentResources } from "../runtime/registration/agent-resources.ts";
167
+ import { userSkillFolders } from "../runtime/resources/agent-resources.mjs";
166
168
  import { FRESH_COMMAND_ACTIONS, FRESH_COMMAND_HELP, ONBOARDING_COMMAND_ACTIONS, WORKFLOW_COMMAND_EXCLUSIONS } from "../runtime/registration/operator-catalogs.ts";
167
169
  import { registerPiagentStatusCommand } from "../runtime/registration/runtime-model-status.ts";
168
170
  import { registerTaskPreflightCommand } from "../runtime/registration/task-preflight.ts";
@@ -348,9 +350,10 @@ const DEFAULT_POLICY = fallbackBasePolicy(CONTEXT_INDEX_FILE, DEFAULT_ORCHESTRAT
348
350
  // so it is resolved once here rather than threaded through every profile load.
349
351
  const PLATFORM_ROOT = findPlatformRoot(path.dirname(fileURLToPath(import.meta.url)));
350
352
  // The skills Pi lists for the model are read where they are installed: this
351
- // package's and the user's. Reading them is allowed; writes stay in the project.
353
+ // package's and the user's (also those kept for other coding agents, such as
354
+ // ~/.claude/skills). Reading them is allowed; writes stay in the project.
352
355
  const SKILL_READ_ROOTS = [path.join(PLATFORM_ROOT, "packages", "piagent-core", "skills"),
353
- path.join(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"), "skills")]
356
+ path.join(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"), "skills"), ...userSkillFolders()]
354
357
  .flatMap((root) => { try { return [fs.realpathSync.native(root)]; } catch { return []; } });
355
358
  const UPDATE_CHECK_MODULE = fileURLToPath(new URL("./update-check.js", import.meta.url));
356
359
  // The installed version, read from the package this file ships in. A maintainer
@@ -924,11 +927,12 @@ function evaluatePathLikeToolAccess(
924
927
  sourceCheckoutReadRoots?: string[];
925
928
  } = {}
926
929
  ): { block: boolean; reason?: string } {
927
- const scopeAwareTool = options.forceScopeAware === true || ["read", "write", "edit", "grep", "find", "ls"].includes(toolName)
930
+ const piFileTool = ["read", "write", "edit", "grep", "find", "ls"].includes(toolName);
931
+ const scopeAwareTool = options.forceScopeAware === true || piFileTool
928
932
  || /(?:^|[_-])(?:fs|filesystem)(?:[_-]|$)/i.test(toolName);
929
933
  const inspection = inspectPathInputsFromInput(
930
934
  cwd,
931
- input,
935
+ piFileTool ? piToolPathInput(input) : input,
932
936
  usesFilesystemContentFields(toolName, options.allowAmbiguousFilesystemContentFields !== false)
933
937
  );
934
938
  if (inspection.maxDepthExceeded) {
@@ -966,6 +970,7 @@ function evaluatePathLikeToolAccess(
966
970
  const fieldAccessMode = filesystemFieldAccessMode(toolName, item.field, writesFilesystem);
967
971
  const sourceCheckoutReadRoot = fieldAccessMode === "read"
968
972
  ? grantedSourceCheckoutRootForPath(cwd, item.path, options.sourceCheckoutReadRoots ?? [])
973
+ ?? (piFileTool && !writesFilesystem ? referenceReadRootForPath(cwd, item.path) : undefined)
969
974
  : undefined;
970
975
  if (scopeAwareTool && isFilesystemScopeField(item.field)) {
971
976
  const boundary = inspectRepositoryPathBoundary(cwd, item.path);
@@ -4144,5 +4149,6 @@ export default function piagentGuard(pi: ExtensionAPI) {
4144
4149
  // registers. The completion module holds only task-bound tools (memory
4145
4150
  // citations need a task id), so it is no longer registered at all.
4146
4151
  registerTaskEvidenceTools(pi, registrationDeps);
4152
+ registerAgentResources(pi);
4147
4153
  registerSessionCommands(pi, { ...registrationDeps, ...contextCommandApi });
4148
4154
  }
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  import { extractShellPathCandidates, normalizePathCandidate } from "./policy-core.js";
@@ -24,6 +25,36 @@ export function grantedSourceCheckoutRootForPath(cwd: string, candidate: string,
24
25
  return roots.find((root) => inside(root, canonical));
25
26
  }
26
27
 
28
+ // Folders a member points the agent to for reference (an @ mention, "read
29
+ // ~/Documents/old-shop"): their own files, external drives and the shared
30
+ // folder, read with read, grep, find or ls. Hidden entries of the home folder
31
+ // (shell files, histories, tool configs) and ~/Library (app data, mail,
32
+ // browser profiles) stay closed; a link is judged by where it leads.
33
+ export function referenceReadRootForPath(cwd: string, candidate: string): string | undefined {
34
+ let target: string, home: string;
35
+ try {
36
+ target = fs.realpathSync.native(path.resolve(cwd, normalizePathCandidate(candidate)));
37
+ home = fs.realpathSync.native(os.homedir());
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ if (inside(home, target)) {
42
+ const first = path.relative(home, target).split(path.sep)[0];
43
+ return first.startsWith(".") || first === "Library" ? undefined : home;
44
+ }
45
+ return ["/Volumes", "/Users/Shared", "/private/tmp"].find((root) => inside(root, target));
46
+ }
47
+
48
+ // Pi's own file tools drop a leading @ (an @path mention) and expand ~ to the
49
+ // home folder before they open a path: check the path they will open.
50
+ export function piToolPathInput(input: Record<string, unknown>): Record<string, unknown> {
51
+ const value = input.path;
52
+ if (typeof value !== "string") return input;
53
+ const bare = value.startsWith("@") ? value.slice(1) : value;
54
+ const opened = bare === "~" ? os.homedir() : bare.startsWith("~/") ? path.join(os.homedir(), bare.slice(2)) : bare;
55
+ return opened === value ? input : { ...input, path: opened };
56
+ }
57
+
27
58
  // A shell command can name a file it is about to create. realpath fails for that
28
59
  // file, so resolve its nearest existing parent instead; otherwise a new file in
29
60
  // the cache, spelled through an alias such as /var for /private/var, escapes
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { agentResourcePaths } from '../runtime/resources/agent-resources.mjs';
5
+
6
+ // The skills and commands of the project and of the member (see
7
+ // agent-resources.mjs), loaded with Pi's own loaders: skills are listed to
8
+ // the model by name and description, commands become /name. Also the folders
9
+ // the sandbox opens for reading: a skill's SKILL.md and the files it bundles,
10
+ // where the skill is installed.
11
+ export async function loadAgentResources(api, sdk, cwd, home) {
12
+ // A folder that cannot be read never keeps the conversation from opening.
13
+ try { return await load(api, sdk, cwd, home); }
14
+ catch { return { skills: [], prompts: [], readable: [], expand: (text) => text }; }
15
+ }
16
+
17
+ async function load(api, sdk, cwd, home) {
18
+ const { skillPaths, promptPaths } = agentResourcePaths({ cwd, ...(home ? { home } : {}) });
19
+ // Defaults off: no personal Pi folder is consulted, only these paths.
20
+ const skills = skillPaths.length ? api.loadSkills({ cwd, agentDir: cwd, skillPaths, includeDefaults: false }).skills : [];
21
+ const templates = await import(pathToFileURL(path.join(sdk, 'dist/core/prompt-templates.js')));
22
+ const prompts = promptPaths.length ? templates.loadPromptTemplates({ cwd, agentDir: cwd, promptPaths, includeDefaults: false }).templates : [];
23
+ const real = (dir) => { try { return fs.realpathSync(dir); } catch { return null; } };
24
+ const readable = [...new Set([...skillPaths, ...skills.map((skill) => real(skill.baseDir))].filter(Boolean))];
25
+ return { skills, prompts, readable, expand: (text) => expandCommand(text, skills, prompts, api.stripFrontmatter, templates.expandPromptTemplate) };
26
+ }
27
+
28
+ // A member's /skill:name, /command or /skill-name (as other agents call a
29
+ // skill), expanded as Pi does: a command's prompt with its arguments, or the
30
+ // skill's instructions followed by what the member asked. Anything else, a
31
+ // path such as /tmp/x included, is left as typed.
32
+ export function expandCommand(text, skills, prompts, stripFrontmatter, expandPromptTemplate) {
33
+ if (typeof text !== 'string' || !text.startsWith('/')) return text;
34
+ const space = text.search(/\s/), head = space < 0 ? text.slice(1) : text.slice(1, space);
35
+ const args = space < 0 ? '' : text.slice(space + 1).trim();
36
+ const explicit = head.startsWith('skill:'), name = explicit ? head.slice(6) : head;
37
+ if (!explicit && prompts.some((prompt) => prompt.name === name)) return expandPromptTemplate(text, prompts);
38
+ const skill = skills.find((item) => item.name === name);
39
+ if (!skill) return text;
40
+ let body;
41
+ try { body = stripFrontmatter(fs.readFileSync(skill.filePath, 'utf8')).trim(); } catch { return text; }
42
+ const block = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`;
43
+ return args ? `${block}\n\n${args}` : block;
44
+ }
@@ -15,6 +15,12 @@ import { fetchPublicPage } from './web-fetch.mjs';
15
15
  const text = result => (result?.content ?? []).filter(part => part.type === 'text').map(part => part.text).join('\n').trim();
16
16
  const first = value => String(value ?? '').split('\n')[0].slice(0, 160);
17
17
 
18
+ const LANGUAGES = [['pnpm', 'pnpm --version'], ['yarn', 'yarn --version'], ['bun', 'bun --version'], ['deno', 'deno --version'], ['uv', 'uv --version'],
19
+ ['poetry', 'poetry --version'], ['go', 'go version'], ['rust (cargo)', 'cargo --version'], ['java', 'javac -version', 'test -n "$JAVA_HOME"'], ['maven', 'mvn -v'],
20
+ ['gradle', 'gradle --version --offline'], ['kotlin', 'kotlinc -version'], ['.NET', 'dotnet --version'], ['php', 'php --version'], ['composer', 'composer --version'],
21
+ ['ruby', 'ruby --version'], ['bundler', 'bundle --version'], ['swift', 'swift --version'], ['dart', 'dart --version'], ['flutter', 'flutter --version --suppress-analytics'],
22
+ ['elixir', 'elixir --version']];
23
+
18
24
  export async function runDoctor({ sdkRoot, origin, broker, network = true }) {
19
25
  const results = [];
20
26
  const check = async (label, required, run) => {
@@ -47,10 +53,12 @@ export async function runDoctor({ sdkRoot, origin, broker, network = true }) {
47
53
  await boundary.invoke('write', { path: 'Dự án mới/ghi chú.md', content: 'ổn\n' });
48
54
  if (await bash("cat 'Dự án mới/ghi chú.md'") !== 'ổn') throw Error('content changed'); return 'ok';
49
55
  });
50
- await check('files outside the project stay out of reach', true, async () => {
56
+ await check('folders outside the project are read-only', true, async () => {
51
57
  let refused = false; try { await boundary.invoke('write', { path: '../piagent-doctor-outside.txt', content: 'x' }); } catch { refused = true; }
52
58
  if (!refused) { fs.rmSync(path.join(path.dirname(project), 'piagent-doctor-outside.txt'), { force: true }); throw Error('a write outside the project was allowed'); }
53
- return 'refused';
59
+ // A folder the member points to for reference (@~/…) is readable.
60
+ await boundary.invoke('ls', { path: '~' });
61
+ return 'read, write refused';
54
62
  });
55
63
  await check('commands have no network unless approved', true, async () => {
56
64
  let refused = false; try { await bash('curl -sS -m 5 https://example.com/ >/dev/null'); } catch { refused = true; }
@@ -70,6 +78,19 @@ export async function runDoctor({ sdkRoot, origin, broker, network = true }) {
70
78
  await bash('cc main.c -o main && ./main'); return first(await bash('cc --version'));
71
79
  });
72
80
  await check('make', false, () => bash('make --version'));
81
+ // Tests that start a server or use a local database (tool-boundary.mjs).
82
+ await check('tests reach servers on this Mac (localhost)', false, async () => {
83
+ await boundary.invoke('write', { path: 'loopback.mjs', content: "import http from 'node:http'; const s = http.createServer((q, r) => r.end('ok')); s.listen(0, '127.0.0.1', async () => { console.log(await (await fetch(`http://127.0.0.1:${s.address().port}/`)).text()); s.close(); });\n" });
84
+ try { return await bash('node loopback.mjs'); }
85
+ catch (error) { throw Error(/loopback-blocked/.test(String(error?.message)) ? 'a local proxy listens on this Mac: tests that use localhost need run_with_network' : first(String(error?.message))); }
86
+ });
87
+ await check('package caches kept for the project', false, async () => { await bash('mkdir -p "$npm_config_cache" && touch "$npm_config_cache/.piagent-doctor"'); return boundary.packageCache; });
88
+ // The languages installed on this Mac, each started once in the sandbox.
89
+ // (macOS ships stubs for some, such as javac without a JDK: a probe decides.)
90
+ for (const [label, command, probe = `command -v ${command.split(' ')[0]} >/dev/null`] of LANGUAGES) {
91
+ if (await bash(`${probe} && echo yes || true`) !== 'yes') continue;
92
+ await check(label, false, async () => first(await bash(`${command} 2>&1`)));
93
+ }
73
94
  if (network) await check('web_fetch reads a public page', false, async () => `status ${(await fetchPublicPage('https://example.com/', { maxChars: 1000 })).status}`);
74
95
  } finally { await boundary?.dispose(); fs.rmSync(project, { recursive: true, force: true }); }
75
96
  if (broker) {
@@ -0,0 +1,118 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { execFileSync } from 'node:child_process';
5
+
6
+ // What test runners and package managers of the common languages need in the
7
+ // company sandbox beyond the toolchains themselves (toolchain.mjs):
8
+ // - package caches that persist for the project between conversations,
9
+ // outside the project and private to it: the first install asks for
10
+ // network (run_with_network), later runs work offline;
11
+ // - the member's own dependency caches as read-only seeds, where the tool can
12
+ // layer one (Go, Gradle, Maven), so what is already downloaded is reused;
13
+ // - runtimes installed in places the sandbox otherwise keeps closed (JDKs
14
+ // under ~/Library/Java, the Android SDK, the system gem folder, MacPorts);
15
+ // - SwiftPM and xcodebuild, which cannot start their own sandbox inside this
16
+ // one: their manifest sandbox is off; this sandbox confines the same
17
+ // processes.
18
+ const KEEP_DAYS = 30;
19
+ const existing = dir => { try { const real = fs.realpathSync(dir); return fs.statSync(real).isDirectory() ? real : null; } catch { return null; } };
20
+
21
+ // Unused project caches are removed after a month.
22
+ function prune(root, keep) {
23
+ let entries = [];
24
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
25
+ const limit = Date.now() - KEEP_DAYS * 24 * 60 * 60 * 1000;
26
+ for (const entry of entries) {
27
+ const dir = path.join(root, entry.name);
28
+ try { if (entry.isDirectory() && dir !== keep && fs.statSync(dir).mtimeMs < limit) fs.rmSync(dir, { recursive: true, force: true }); } catch { /* in use or gone */ }
29
+ }
30
+ }
31
+
32
+ function javaHome(userHome) {
33
+ try {
34
+ const home = execFileSync('/usr/libexec/java_home', [], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], env: { HOME: userHome, PATH: '/usr/bin:/bin' } }).trim();
35
+ return existing(home);
36
+ } catch { return null; }
37
+ }
38
+
39
+ // SwiftPM and xcodebuild start sandbox-exec for package manifests and
40
+ // plugins; inside this sandbox that fails ("sandbox_apply: Operation not
41
+ // permitted"). These wrappers, first on PATH, pass the switch that turns it
42
+ // off, then run the member's own swift/xcodebuild found further on PATH.
43
+ const SWIFT_WRAPPER = `#!/bin/sh
44
+ self=$(cd "$(dirname "$0")" && pwd -P)
45
+ real=; IFS=:; for dir in $PATH; do [ "$dir" = "$self" ] && continue; [ -x "$dir/$(basename "$0")" ] && { real="$dir/$(basename "$0")"; break; }; done; unset IFS
46
+ [ -n "$real" ] || { echo "$(basename "$0"): not installed" >&2; exit 127; }
47
+ case "$(basename "$0"):$1" in
48
+ swift:build|swift:test|swift:run) sub=$1; shift; exec "$real" "$sub" --disable-sandbox "$@" ;;
49
+ swift:package) shift; exec "$real" package --disable-sandbox "$@" ;;
50
+ xcodebuild:*)
51
+ # Build products go to the project's cache (~/Library/Developer is closed).
52
+ derived=; case " $* " in *" -derivedDataPath "*) ;; *" -scheme "*|*" -xctestrun "*|*" -testProductsPath "*) derived="-derivedDataPath $PIAGENT_DERIVED_DATA" ;; esac
53
+ exec "$real" -IDEPackageSupportDisableManifestSandbox=YES -IDEPackageSupportDisablePluginExecutionSandbox=YES $derived "$@" ;;
54
+ *) exec "$real" "$@" ;;
55
+ esac
56
+ `;
57
+
58
+ export function languageEnvironment({ userHome, repositoryTop, home }) {
59
+ const root = path.join(userHome, 'Library/Caches/Piagent/sandbox');
60
+ const cache = path.join(root, createHash('sha256').update(repositoryTop).digest('hex').slice(0, 24));
61
+ fs.mkdirSync(cache, { recursive: true, mode: 0o700 });
62
+ fs.chmodSync(cache, 0o700);
63
+ const now = new Date(); try { fs.utimesSync(cache, now, now); } catch { /* best effort */ }
64
+ prune(root, cache);
65
+ const at = name => path.join(cache, name);
66
+
67
+ // Read-only: runtimes and the member's caches used as seeds.
68
+ const seeds = {
69
+ gradle: existing(path.join(userHome, '.gradle/caches')),
70
+ maven: existing(path.join(userHome, '.m2/repository')),
71
+ go: existing(path.join(process.env.GOMODCACHE || path.join(userHome, 'go/pkg/mod'), 'cache/download')),
72
+ // .NET restores over TLS through the macOS Security framework, closed
73
+ // here: packages the member restored are read from their own folder.
74
+ nuget: existing(path.join(userHome, '.nuget/packages')),
75
+ };
76
+ const java = javaHome(userHome), android = existing(path.join(userHome, 'Library/Android/sdk'));
77
+ const readRoots = [...new Set([...Object.values(seeds), java, android, existing(path.join(userHome, 'Library/Java/JavaVirtualMachines')),
78
+ existing('/Library/Ruby'), existing('/opt/local')].filter(Boolean))];
79
+
80
+ const bin = path.join(home, '.piagent/bin');
81
+ fs.mkdirSync(bin, { recursive: true, mode: 0o700 });
82
+ for (const name of ['swift', 'xcodebuild']) fs.writeFileSync(path.join(bin, name), SWIFT_WRAPPER, { mode: 0o755 });
83
+ // pnpm's store: its own settings file, which npm does not read (an npm_config_
84
+ // variable would make every npm command warn about an unknown setting).
85
+ const pnpm = path.join(home, 'Library/Preferences/pnpm');
86
+ fs.mkdirSync(pnpm, { recursive: true, mode: 0o700 });
87
+ fs.writeFileSync(path.join(pnpm, 'rc'), `store-dir=${at('pnpm-store')}\n`, { mode: 0o600 });
88
+
89
+ const common = {
90
+ npm_config_cache: at('npm'), YARN_CACHE_FOLDER: at('yarn'), BUN_INSTALL_CACHE_DIR: at('bun'), DENO_DIR: at('deno'),
91
+ PIP_CACHE_DIR: at('pip'), UV_CACHE_DIR: at('uv'), POETRY_CACHE_DIR: at('poetry'), XDG_CACHE_HOME: at('xdg'),
92
+ GOPATH: at('go'), GOMODCACHE: at('go/pkg/mod'), GOCACHE: at('go/build'), GOFLAGS: '-modcacherw',
93
+ CARGO_HOME: at('cargo'), GRADLE_USER_HOME: at('gradle'), NUGET_PACKAGES: at('nuget'), COMPOSER_CACHE_DIR: at('composer'),
94
+ PUB_CACHE: at('pub'), GEM_HOME: at('gem'), BUNDLE_PATH: at('bundle'), CP_HOME_DIR: at('cocoapods'), PIAGENT_DERIVED_DATA: at('DerivedData'),
95
+ DOTNET_CLI_TELEMETRY_OPTOUT: '1', DOTNET_NOLOGO: '1', DOTNET_SKIP_FIRST_TIME_EXPERIENCE: '1',
96
+ MAVEN_OPTS: [`-Dmaven.repo.local=${at('m2/repository')}`, ...(seeds.maven ? [`-Dmaven.repo.local.tail=${seeds.maven}`] : [])].join(' '),
97
+ ...(seeds.gradle ? { GRADLE_RO_DEP_CACHE: seeds.gradle } : {}),
98
+ ...(seeds.nuget ? { NUGET_FALLBACK_PACKAGES: seeds.nuget } : {}),
99
+ ...(java ? { JAVA_HOME: java } : {}),
100
+ ...(android ? { ANDROID_HOME: android, ANDROID_SDK_ROOT: android } : {}),
101
+ };
102
+ // Go reads modules the member already has from their own cache first; an
103
+ // approved command then goes on to the usual proxies.
104
+ const seed = seeds.go ? `file://${seeds.go},` : '';
105
+ // .NET keeps its named mutexes (NuGet's among them) as files in
106
+ // /tmp/.dotnet, whatever TMPDIR says.
107
+ const dotnet = [path.join(userHome, '.dotnet/dotnet'), '/usr/local/share/dotnet/dotnet', '/opt/homebrew/bin/dotnet'].some(file => fs.existsSync(file));
108
+ if (dotnet) try { fs.mkdirSync('/private/tmp/.dotnet', { recursive: true, mode: 0o777 }); } catch { /* another user's */ }
109
+ return {
110
+ cache, readRoots, writeRoots: [cache, ...(dotnet && existing('/private/tmp/.dotnet') ? ['/private/tmp/.dotnet'] : [])], bin,
111
+ env: {
112
+ // .NET's dual-stack sockets reach 127.0.0.1 as ::ffff:127.0.0.1, which
113
+ // the loopback rule does not name (the test host would not connect).
114
+ offline: { ...common, GOPROXY: `${seed}off`, npm_config_prefer_offline: 'true', DOTNET_SYSTEM_NET_DISABLEIPV6: '1' },
115
+ network: { ...common, GOPROXY: `${seed}${process.env.GOPROXY || 'https://proxy.golang.org,direct'}` },
116
+ },
117
+ };
118
+ }
@@ -22,14 +22,15 @@ export function projectInstructions(cwd, top = cwd) {
22
22
  }
23
23
 
24
24
  // Managed sessions never consult the personal trust store or execute resources
25
- // discovered in a project.
26
- export function managedResourceLoader(api, { systemPrompt, extensions = [], agentsFiles = [] } = {}) {
25
+ // discovered in a project. Skills and commands are text (agent-skills.mjs):
26
+ // what they ask for runs with the session's own tools.
27
+ export function managedResourceLoader(api, { systemPrompt, extensions = [], agentsFiles = [], skills = [], prompts = [] } = {}) {
27
28
  const runtime = api.createExtensionRuntime();
28
29
  const empty = Object.freeze([]);
29
30
  return Object.freeze({
30
31
  getExtensions: () => ({ extensions, errors: empty, runtime }),
31
- getSkills: () => ({ skills: empty, diagnostics: empty }),
32
- getPrompts: () => ({ prompts: empty, diagnostics: empty }),
32
+ getSkills: () => ({ skills: [...skills], diagnostics: empty }),
33
+ getPrompts: () => ({ prompts: [...prompts], diagnostics: empty }),
33
34
  getThemes: () => ({ themes: empty, diagnostics: empty }),
34
35
  getAgentsFiles: () => ({ agentsFiles: agentsFiles.map((file) => ({ ...file })) }),
35
36
  getSystemPrompt: () => typeof systemPrompt === 'function' ? systemPrompt() : systemPrompt,