@piagent/platform 1.23.1 → 1.25.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 (74) hide show
  1. package/CHANGELOG.md +28 -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 +2 -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/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/whats-new.html +13 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  56. package/package.json +1 -1
  57. package/packages/piagent-core/managed/command-tools.mjs +88 -20
  58. package/packages/piagent-core/managed/container-engine.mjs +40 -0
  59. package/packages/piagent-core/managed/host-command.mjs +67 -0
  60. package/packages/piagent-core/managed/language-environment.mjs +8 -2
  61. package/packages/piagent-core/managed/repository-operation.mjs +39 -17
  62. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +19 -8
  63. package/packages/piagent-core/managed/session.mjs +1 -1
  64. package/packages/piagent-core/managed/tool-boundary.mjs +58 -42
  65. package/packages/piagent-core/managed/workflow.mjs +10 -6
  66. package/packages/piagent-core/package.json +1 -1
  67. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-BdMpMzXP.js → MarkdownMessage-B8CsDh3B.js} +1 -1
  68. package/packages/piagent-webui/dist/client/assets/index-CiLLJu4R.js +231 -0
  69. package/packages/piagent-webui/dist/client/index.html +1 -1
  70. package/packages/piagent-webui/gateway/managed-gateway.mjs +7 -1
  71. package/packages/piagent-webui/gateway/session-command-controller.ts +5 -1
  72. package/packages/piagent-webui/gateway/session-inspection-registry.ts +2 -1
  73. package/schemas/piagent-webui/session-command-v1.schema.json +4 -0
  74. package/packages/piagent-webui/dist/client/assets/index-F_KXNLyp.js +0 -231
@@ -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.23.1 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.0 docs · EN</span>
34
34
  <nav class="language-switch" aria-label="Language">
35
35
  <a class="language-option" href="/security" hreflang="vi" lang="vi">VI</a>
36
36
  <a class="language-option current" href="/en/security" 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.23.1 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.0 docs · EN</span>
34
34
  <nav class="language-switch" aria-label="Language">
35
35
  <a class="language-option" href="/team" hreflang="vi" lang="vi">VI</a>
36
36
  <a class="language-option current" href="/en/team" 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.23.1 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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>
@@ -104,6 +104,18 @@
104
104
  <h1>1.9.0: ask in plain language, the team's Harness checks</h1>
105
105
  <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>
106
106
  </div>
107
+ <section class="section">
108
+ <p class="section-kicker">v1.25.0 · 2026-10-13</p>
109
+ <h2 id="v1-25-0">When the machine lacks something, the agent asks, and an approved command runs</h2>
110
+ <p class="section-lead">A company conversation no longer stops at “Java or Docker is missing here”. A missing runtime (a JDK, the Node version the project needs), your sign-in (git pull/push, a private registry) or a file outside the project: the agent requests <strong>run_on_host</strong>, one command that runs on your machine, outside the sandbox, once you approve it (Bypass asks too). Docker installed but not running: the agent requests <strong>start_docker</strong>, waits for the engine and carries on in the same conversation. What only you can do (a sudo password, an installer, a sign-in) comes as a question with the steps and “Done, continue / Skip”. <code>fetch_origin</code> fetches a repository inside a folder of several repositories, and a turn that only read through <code>git -C</code> is no longer counted as a code change.</p>
111
+ </section>
112
+
113
+ <section class="section">
114
+ <p class="section-kicker">v1.24.0 · 2026-10-13</p>
115
+ <h2 id="v1-24-0">Choose Fast when a company conversation starts</h2>
116
+ <p class="section-lead">Where the team's Harness allows fast mode, the new chat page shows <strong>Fast: off / Fast: on</strong> next to the company model. Turned on before sending, the conversation runs on Fast from its first message: the main agent answers faster on GPT (Codex) models, its tokens count 2.5 times toward the quota, and the model and thinking level are unchanged. The Fast switch in a conversation's header turns amber while on.</p>
117
+ </section>
118
+
107
119
  <section class="section">
108
120
  <p class="section-kicker">v1.23.1 · 2026-10-13</p>
109
121
  <h2 id="v1-23-1">An agent's question waits at the end of the conversation</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.23.1 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.0 docs · EN</span>
34
34
  <nav class="language-switch" aria-label="Language">
35
35
  <a class="language-option" href="/windows" hreflang="vi" lang="vi">VI</a>
36
36
  <a class="language-option current" href="/en/windows" 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.23.1 docs · EN</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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>
@@ -94,7 +94,7 @@
94
94
  <main>
95
95
  <div class="content-shell" data-article>
96
96
  <section class="hero">
97
- <p class="eyebrow">Piagent 1.23.1</p>
97
+ <p class="eyebrow">Piagent 1.25.0</p>
98
98
  <h1>Coding agent cho cả team, kiểm tra từng bước.</h1>
99
99
  <p>
100
100
  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.
@@ -108,7 +108,7 @@
108
108
  <div class="hero-actions">
109
109
  <a class="button primary" href="/quickstart">Bắt đầu</a>
110
110
  <a class="button" href="/ecosystem">Hệ sinh thái</a>
111
- <a class="button" href="/whats-new">Có gì mới trong 1.23.1</a>
111
+ <a class="button" href="/whats-new">Có gì mới trong 1.25.0</a>
112
112
  <a class="button" href="/commands">Xem command</a>
113
113
  </div>
114
114
  </section>
@@ -184,9 +184,9 @@ piagent-setup</code></pre>
184
184
  <h2 id="bat-dau-tu-dau">Bắt đầu từ đâu</h2>
185
185
  <div class="card-grid">
186
186
  <a class="doc-card" href="/whats-new">
187
- <span class="card-kicker">Bản 1.23.1</span>
187
+ <span class="card-kicker">Bản 1.25.0</span>
188
188
  <h3>Có gì mới</h3>
189
- <p>Câu hỏi của agent luôn nằm cuối cuộc trò chuyện, ngay trên ô nhập, và dễ đọc hơn; ở 1.23.0: nút Fast cho cuộc trò chuyện công ty khi Harness cho phép, tool lỗi được báo về Studio (Logs → Tool lỗi), cuộc trò chuyện mới gửi được tin nhắn đầu sau khi Harness đổi; ở 1.22.1: dashboard không còn quay mãi khi project nằm trên ổ Windows nhìn từ WSL (/mnt/d…) hoặc phiên công ty không trả lời; ở 1.22.0: đính kèm được mọi file code và văn bản (.html, .css, .ts, .py, Dockerfile…); ở 1.21.1: bật Bypass được cả khi lượt đang chạy và được giữ sau khi tải lại; ở 1.21.0: câu hỏi và yêu cầu duyệt hiện từng cái một (main agent trước), thông báo góc phải khi cuộc trò chuyện khác xong hay đang chờ bạn, Studio ghi lại mỗi lần compact; ở 1.20.0: cài package trong sandbox công ty không phải duyệt (npm, PyPI, Go, Maven, Gradle… qua proxy chỉ cho registry); ở 1.19.0: task công ty có checklist được làm đến hết, cuối mỗi lượt báo rõ xong, dừng giữa chừng hay lỗi, Docker trong sandbox công ty, folder chứa nhiều repo; ở 1.18.0: đổi nhánh Git trên dashboard, tự thử lại khi lỗi tạm thời, Claude Haiku 5.5; ở 1.17.0: dashboard hiện nhánh Git của project; ở 1.16.3: dashboard cập nhật xong vẫn tự kiểm tra bản mới; ở 1.16.2: Terminal nói giống dashboard: hỏi cập nhật, /bypass, lỗi và ghi chú Harness bằng tiếng Việt; ở 1.16.1: bản mới được báo bằng dialog và kiểm tra mỗi giờ, tab dashboard không còn mất đăng nhập; ở 1.16.0: Bypass cho cuộc trò chuyện công ty, đổi Harness không làm gián đoạn; ở 1.15.2: cuộc trò chuyện dài tự thu gọn trở lại; ở 1.15.1: Studio thấy mỗi call của model dùng để làm gì, nhiều cuộc trò chuyện trong một folder không nhận nhầm thay đổi của nhau; ở 1.15.0: subagent bị từ chối nói đúng lý do; ở 1.14: cập nhật giữ model bạn đã chọn; ở 1.13: cuộc trò chuyện giữ chỗ đang đọc, chữ của agent có định dạng, Piagent trên Windows qua WSL2: sandbox công ty bằng bubblewrap và Agent Watch cho Windows; ở 1.12: @ chọn file và folder trên máy, skill và command dùng bằng /, nhánh cuộc trò chuyện công ty gửi được tin nhắn.</p>
189
+ <p>Thiếu môi trường thì agent xin quyền và chạy được sau khi bạn duyệt (run_on_host ngoài sandbox, start_docker mở Docker ngay trong cuộc trò chuyện); ở 1.24.0: chọn Fast ngay trên trang tạo cuộc trò chuyện công ty, nút Fast đổi màu vàng khi đang bật; ở 1.23.1: câu hỏi của agent luôn nằm cuối cuộc trò chuyện, ngay trên ô nhập, và dễ đọc hơn; ở 1.23.0: nút Fast cho cuộc trò chuyện công ty khi Harness cho phép, tool lỗi được báo về Studio (Logs → Tool lỗi), cuộc trò chuyện mới gửi được tin nhắn đầu sau khi Harness đổi; ở 1.22.1: dashboard không còn quay mãi khi project nằm trên ổ Windows nhìn từ WSL (/mnt/d…) hoặc phiên công ty không trả lời; ở 1.22.0: đính kèm được mọi file code và văn bản (.html, .css, .ts, .py, Dockerfile…); ở 1.21.1: bật Bypass được cả khi lượt đang chạy và được giữ sau khi tải lại; ở 1.21.0: câu hỏi và yêu cầu duyệt hiện từng cái một (main agent trước), thông báo góc phải khi cuộc trò chuyện khác xong hay đang chờ bạn, Studio ghi lại mỗi lần compact; ở 1.20.0: cài package trong sandbox công ty không phải duyệt (npm, PyPI, Go, Maven, Gradle… qua proxy chỉ cho registry); ở 1.19.0: task công ty có checklist được làm đến hết, cuối mỗi lượt báo rõ xong, dừng giữa chừng hay lỗi, Docker trong sandbox công ty, folder chứa nhiều repo; ở 1.18.0: đổi nhánh Git trên dashboard, tự thử lại khi lỗi tạm thời, Claude Haiku 5.5; ở 1.17.0: dashboard hiện nhánh Git của project; ở 1.16.3: dashboard cập nhật xong vẫn tự kiểm tra bản mới; ở 1.16.2: Terminal nói giống dashboard: hỏi cập nhật, /bypass, lỗi và ghi chú Harness bằng tiếng Việt; ở 1.16.1: bản mới được báo bằng dialog và kiểm tra mỗi giờ, tab dashboard không còn mất đăng nhập; ở 1.16.0: Bypass cho cuộc trò chuyện công ty, đổi Harness không làm gián đoạn; ở 1.15.2: cuộc trò chuyện dài tự thu gọn trở lại; ở 1.15.1: Studio thấy mỗi call của model dùng để làm gì, nhiều cuộc trò chuyện trong một folder không nhận nhầm thay đổi của nhau; ở 1.15.0: subagent bị từ chối nói đúng lý do; ở 1.14: cập nhật giữ model bạn đã chọn; ở 1.13: cuộc trò chuyện giữ chỗ đang đọc, chữ của agent có định dạng, Piagent trên Windows qua WSL2: sandbox công ty bằng bubblewrap và Agent Watch cho Windows; ở 1.12: @ chọn file và folder trên máy, skill và command dùng bằng /, nhánh cuộc trò chuyện công ty gửi được tin nhắn.</p>
190
190
  </a>
191
191
  <a class="doc-card" href="/ecosystem">
192
192
  <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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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>
@@ -104,6 +104,18 @@
104
104
  <h1>Bản 1.9.0: nói việc bằng lời, Harness của team kiểm tra</h1>
105
105
  <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>
106
106
  </div>
107
+ <section class="section">
108
+ <p class="section-kicker">v1.25.0 · 2026-10-13</p>
109
+ <h2 id="v1-25-0">Thiếu môi trường thì agent xin quyền, duyệt xong là chạy</h2>
110
+ <p class="section-lead">Cuộc trò chuyện công ty không còn dừng ở “máy thiếu Java/Docker, em không làm được”. Máy thiếu runtime (JDK, Node đúng version…), cần tài khoản của bạn (git pull/push, registry riêng) hay cần ghi ra ngoài project, agent xin <strong>run_on_host</strong>: đúng một lệnh chạy trên máy bạn, ngoài sandbox, sau khi bạn duyệt (Bypass cũng hỏi). Docker đã cài mà chưa bật thì agent xin <strong>start_docker</strong>, chờ Docker chạy rồi làm tiếp ngay trong cuộc trò chuyện đó. Việc chỉ bạn làm được (mật khẩu sudo, trình cài, đăng nhập) thì agent hỏi kèm các bước và lựa chọn “Đã xong, làm tiếp / Bỏ qua”. <code>fetch_origin</code> fetch được repo nằm trong folder chứa nhiều repo, và lượt chỉ đọc bằng <code>git -C</code> không còn bị Harness coi là có sửa code.</p>
111
+ </section>
112
+
113
+ <section class="section">
114
+ <p class="section-kicker">v1.24.0 · 2026-10-13</p>
115
+ <h2 id="v1-24-0">Chọn Fast ngay khi tạo cuộc trò chuyện công ty</h2>
116
+ <p class="section-lead">Khi Harness của team cho phép Fast, trang “Cuộc trò chuyện mới” có nút <strong>Fast: tắt / Fast: bật</strong> ngay cạnh model công ty. Bật trước khi gửi thì cuộc trò chuyện chạy Fast từ tin nhắn đầu tiên: main agent trả lời nhanh hơn trên model GPT (Codex), token tính vào quota gấp 2,5 lần, model và mức suy luận không đổi. Nút Fast ở đầu cuộc trò chuyện chuyển màu vàng khi đang bật để dễ nhận ra.</p>
117
+ </section>
118
+
107
119
  <section class="section">
108
120
  <p class="section-kicker">v1.23.1 · 2026-10-13</p>
109
121
  <h2 id="v1-23-1">Câu hỏi của agent luôn nằm cuối cuộc trò chuyện</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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.0 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/windows" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/windows" 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.23.1 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.25.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": 538,
260
- "sha256": "93173799932f8f9d88976e5ffad598645d01fb59f9545ed1dc783d2ca2b1a296"
259
+ "fileCount": 540,
260
+ "sha256": "bcf46ad85a0904f47e2fddc5f55642f7beb0622adfeeb3c62869b031c0c2c64e"
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.23.1",
3
+ "version": "1.25.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,11 +1,15 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import { networkConfirmation } from './permission.mjs';
3
+ import { engineAlive, engineLauncher } from './container-engine.mjs';
4
+ import { MEMBER_ONLY, memberOnlyReason, runHostCommand } from './host-command.mjs';
2
5
 
3
6
  // Commands that leave the plain sandbox, each after the member approves that
4
- // exact command: run_with_network (internet, a local server or browser) and,
5
- // when this machine has a Docker engine, run_with_docker (the same plus the
6
- // engine's socket). Bypass runs both without asking (owner, 2026-10-09),
7
- // except what must be confirmed for any command (bypass-policy.mjs: pushes,
8
- // deploys, registry logins, deletes known only at run time).
7
+ // exact command: run_with_network (internet, a local server or browser),
8
+ // run_with_docker when this machine has Docker (the same plus the engine's
9
+ // socket) and run_on_host (on the member's machine, outside the sandbox).
10
+ // Bypass runs the first two without asking (owner, 2026-10-09), except what
11
+ // must be confirmed for any command (bypass-policy.mjs: pushes, deploys,
12
+ // registry logins, deletes known only at run time); run_on_host always asks.
9
13
  const PARAMETERS = { type: 'object', properties: { command: { type: 'string', minLength: 1, maxLength: 4000 }, reason: { type: 'string', minLength: 1, maxLength: 300 }, timeout: { type: 'number', minimum: 1, maximum: 1800 } }, required: ['command', 'reason'], additionalProperties: false };
10
14
 
11
15
  const KINDS = {
@@ -23,34 +27,98 @@ const KINDS = {
23
27
  title: 'Chạy lệnh dùng Docker', allowText: 'chạy đúng lệnh này một lần, có internet và Docker; container nó tạo có thể đọc thư mục Docker được chia sẻ trên máy và có thể tiếp tục chạy.',
24
28
  confirm: (self, command) => networkConfirmation(self, command),
25
29
  },
30
+ host: {
31
+ name: 'run_on_host', label: 'Run on this machine', scope: 'host-command-once', provider: 'host', riskClass: 'high',
32
+ description: 'Run one shell command on the member\'s machine, outside the sandbox, as the member, after they approve that exact command. Use it when the sandbox blocks what the task needs and no other tool covers it: install a runtime or tool the project needs (brew install openjdk@21, brew install node@22, sdk install java…), git pull or git push with the member\'s sign-in, a private registry, a file outside the project. It has the member\'s files, Keychain and sign-ins, so ask only for what the task needs and say why in reason. No input can be typed: commands that need the member\'s password (sudo, a .pkg installer) or an interactive sign-in are refused; ask the member with ask_user for those.',
33
+ allow: 'Run this exact command once on this machine, outside the sandbox, as you: it can read and change your files and use your sign-ins.',
34
+ title: 'Chạy lệnh trên máy (ngoài sandbox)', allowText: 'chạy đúng lệnh này một lần trên máy bạn, ngoài sandbox, bằng quyền của bạn: lệnh đọc/sửa được file và dùng được tài khoản đã đăng nhập trên máy.',
35
+ // Bypass does not cover it: the member approves each command (with what
36
+ // Bypass would still have asked about, when there is something).
37
+ confirm: async (self, command) => await networkConfirmation(self, command) ?? 'ask',
38
+ },
26
39
  };
27
40
 
41
+ // The engine answers, looked up again when it did not (started since).
42
+ async function engineReady(boundary) {
43
+ if (boundary.engine && await engineAlive(boundary.engine.socket)) return true;
44
+ return boundary.refreshEnvironment() && engineAlive(boundary.engine.socket);
45
+ }
46
+ const DOCKER_STOPPED = 'managed-docker-unreachable: Docker engine trên máy không chạy. Gọi start_docker để xin member duyệt mở Docker, rồi chạy lại đúng lệnh này bằng run_with_docker.';
47
+
48
+ // One approval of the member (the dashboard card, else the Terminal).
49
+ async function approve(self, id, signal, ctx, action, { title, body }) {
50
+ const { piApprovalBroker } = await import('../runtime/inspection/approval-broker.ts');
51
+ const decision = await piApprovalBroker.request({ cwd: self.cwd, rawSessionId: self.session.sessionManager.getSessionId(), toolCallId: id,
52
+ action: { kind: 'external-provider-action', preconditionClass: 'runtime-only', targetPaths: [self.cwd], urlOrigin: null, ...action },
53
+ terminalConfirm: () => ctx?.ui?.confirm?.(title, body) ?? Promise.resolve(false),
54
+ unavailableFallback: 'terminal-confirm', recheck: () => !signal?.aborted && Boolean(self.grant) });
55
+ if (!decision.allowed || !decision.consume() || signal?.aborted) throw Error('managed-operation-denied');
56
+ }
57
+
28
58
  function commandTool(self, kind) {
29
59
  const spec = KINDS[kind];
30
60
  return { name: spec.name, label: spec.label, description: spec.description, parameters: PARAMETERS,
31
61
  execute: async (id, args, signal, onUpdate, ctx) => {
62
+ if (kind === 'docker' && !await engineReady(self.boundary)) throw Error(DOCKER_STOPPED);
63
+ const memberOnly = kind === 'host' ? memberOnlyReason(args.command) : null;
64
+ if (memberOnly) throw Error(MEMBER_ONLY(memberOnly));
32
65
  const confirm = await spec.confirm(self, args.command);
33
- const decision = confirm === null ? { allowed: true, consume: () => true } : await (await import('../runtime/inspection/approval-broker.ts')).piApprovalBroker.request({ cwd: self.cwd, rawSessionId: self.session.sessionManager.getSessionId(), toolCallId: id,
34
- action: { kind: 'external-provider-action', preconditionClass: 'runtime-only', toolName: spec.name, rawAction: { command: args.command }, commandPreview: String(args.command),
35
- targetPaths: [self.cwd], provider: spec.provider, urlOrigin: null, requestedScope: spec.scope, reason: (confirm === 'ask' ? String(args.reason) : `Bypass still asks: ${confirm}. ${args.reason}`).slice(0, 300), riskClass: spec.riskClass,
36
- allowConsequence: spec.allow, denyConsequence: 'The command does not run; the agent is told you declined.' },
37
- terminalConfirm: () => ctx?.ui?.confirm?.(spec.title, `${args.command}\n\nLý do: ${args.reason}${confirm === 'ask' ? '' : `\nBypass vẫn hỏi: ${confirm}`}\n\nĐồng ý: ${spec.allowText}\nTừ chối: lệnh không chạy, agent được báo bạn đã từ chối.`) ?? Promise.resolve(false),
38
- unavailableFallback: 'terminal-confirm', recheck: () => !signal?.aborted && Boolean(self.grant) });
39
- if (!decision.allowed || !decision.consume() || signal?.aborted) throw Error('managed-operation-denied');
66
+ if (confirm !== null) await approve(self, id, signal, ctx, { toolName: spec.name, rawAction: { command: args.command }, commandPreview: String(args.command),
67
+ provider: spec.provider, requestedScope: spec.scope, reason: (confirm === 'ask' ? String(args.reason) : `Bypass still asks: ${confirm}. ${args.reason}`).slice(0, 300), riskClass: spec.riskClass,
68
+ allowConsequence: spec.allow, denyConsequence: 'The command does not run; the agent is told you declined.' },
69
+ { title: spec.title, body: `${args.command}\n\nLý do: ${args.reason}${confirm === 'ask' ? '' : `\nBypass vẫn hỏi: ${confirm}`}\n\nĐồng ý: ${spec.allowText}\nTừ chối: lệnh không chạy, agent được báo bạn đã từ chối.` });
40
70
  let ok = false;
41
71
  const before = await self.changedFiles();
42
- try { const result = await self.boundary.invoke('bash', { command: args.command, ...(args.timeout ? { timeout: args.timeout } : {}) }, signal, onUpdate, ctx?.model, kind === 'docker' ? { docker: true } : { network: true }); ok = !result?.isError; return result; }
43
- finally { await self.claimChanges(before); await self.run?.afterTool('bash', args, ok, () => self.digest()); await self.refreshReview(); }
72
+ try {
73
+ const result = kind === 'host' ? await runHostCommand({ command: args.command, cwd: self.cwd, userHome: self.boundary.userHome, timeout: args.timeout, signal, onUpdate })
74
+ : await self.boundary.invoke('bash', { command: args.command, ...(args.timeout ? { timeout: args.timeout } : {}) }, signal, onUpdate, ctx?.model, kind === 'docker' ? { docker: true } : { network: true });
75
+ ok = !result?.isError; return result;
76
+ } finally {
77
+ // What the command installed (a JDK, Node) is there for the next one.
78
+ if (kind === 'host') self.boundary.refreshEnvironment();
79
+ await self.claimChanges(before); await self.run?.afterTool('bash', args, ok, () => self.digest()); await self.refreshReview();
80
+ }
81
+ } };
82
+ }
83
+
84
+ // start_docker: the engine the member installed, started after they approve
85
+ // (Bypass starts it without asking: local and reversible), then waited for.
86
+ const textResult = text => ({ content: [{ type: 'text', text }], details: {} });
87
+ function startDockerTool(self, { wait = 180_000, every = 2000, launcher: found = engineLauncher, spawnImpl = spawn } = {}) {
88
+ return { name: 'start_docker', label: 'Start Docker',
89
+ description: 'Start the Docker engine installed on this machine (Docker Desktop, OrbStack, Colima, rootless Docker) after the member approves, and wait until it answers; run_with_docker works right after, in this conversation. Use it when a command reports that Docker is not running.',
90
+ parameters: { type: 'object', properties: { reason: { type: 'string', minLength: 1, maxLength: 300 } }, required: ['reason'], additionalProperties: false },
91
+ execute: async (id, args, signal, _onUpdate, ctx) => {
92
+ if (await engineReady(self.boundary)) return textResult('The Docker engine is running: use run_with_docker.');
93
+ const launcher = found({ userHome: self.boundary.userHome });
94
+ if (!launcher) throw Error(self.boundary.dockerCli ? MEMBER_ONLY("starting this machine's Docker service needs the member's password")
95
+ : 'managed-docker-not-installed: Máy chưa cài Docker. Hỏi member bằng ask_user: cài Docker Desktop hoặc OrbStack (trình cài cần mật khẩu nên member tự cài; máy có Homebrew thì có thể xin run_on_host "brew install colima docker"), hay chạy check theo cách khác.');
96
+ const preview = `${launcher.command} ${launcher.args.join(' ')}`;
97
+ if (self.permission !== 'trusted-full-access') await approve(self, id, signal, ctx, { toolName: 'start_docker', rawAction: { launcher: launcher.label }, commandPreview: preview,
98
+ provider: 'docker', requestedScope: 'start-docker-once', reason: String(args.reason).slice(0, 300), riskClass: 'medium',
99
+ allowConsequence: `Start ${launcher.label} on this machine; it keeps running after the conversation.`, denyConsequence: 'Docker is not started; the agent is told you declined.' },
100
+ { title: `Mở ${launcher.label}`, body: `${preview}\n\nLý do: ${args.reason}\n\nĐồng ý: mở ${launcher.label} trên máy; nó vẫn chạy sau cuộc trò chuyện.\nTừ chối: không mở Docker, agent được báo bạn đã từ chối.` });
101
+ try { spawnImpl(launcher.command, launcher.args, { detached: true, stdio: 'ignore', env: { HOME: self.boundary.userHome, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin', USER: process.env.USER ?? '', LANG: 'en_US.UTF-8' } }).on('error', () => {}).unref(); } catch { /* waited for below */ }
102
+ for (const until = Date.now() + wait; Date.now() < until;) {
103
+ if (signal?.aborted) throw Error('managed-tool-cancelled');
104
+ await new Promise(resolve => setTimeout(resolve, every));
105
+ if (await engineReady(self.boundary)) return textResult(`${launcher.label} is running. Run the Docker command again with run_with_docker.`);
106
+ }
107
+ throw Error(`managed-docker-start-timeout: ${launcher.label} đã được mở nhưng engine chưa trả lời sau ${Math.round(wait / 60000)} phút (lần đầu mở app có thể chờ member bấm đồng ý điều khoản hoặc đăng nhập trong cửa sổ Docker). Hỏi member bằng ask_user, kèm lựa chọn "Docker đã chạy, thử lại" và "Bỏ qua bước này".`);
44
108
  } };
45
109
  }
46
110
 
47
- export function commandTools(self) {
48
- return [commandTool(self, 'network'), ...(self.boundary.engine ? [commandTool(self, 'docker')] : [])];
111
+ export function commandTools(self, options) {
112
+ const docker = self.boundary.engine || self.boundary.dockerCli;
113
+ return [commandTool(self, 'network'), ...(docker ? [commandTool(self, 'docker')] : []), startDockerTool(self, options), commandTool(self, 'host')];
49
114
  }
50
115
 
51
- // What the system prompt says about services: Docker when the machine has an
52
- // engine (run_with_docker), else the member starts them.
116
+ // What the system prompt says about services and what the sandbox blocks: it
117
+ // is read for every request, so a Docker started since shows up.
118
+ const HOST_PROMPT = ' When the sandbox blocks something the task needs (a missing runtime such as a JDK or a Node version, the member\'s sign-in for git pull or git push or a private registry, a file outside the project), do not stop and do not tell the member to run it: request run_on_host with the exact command; it runs on the member\'s machine once they approve. Only what needs the member themselves (their password for sudo or an installer, an interactive sign-in, a window to click) goes to ask_user, with the exact steps and the options to continue or skip; after their answer, check and carry on.';
53
119
  export function servicesPrompt(boundary) {
54
- if (boundary.engine) return ' Docker is available through run_with_docker (the member approves each command): a database or service for tests (docker compose up -d, then the tests through bash or run_with_network), docker build, or Testcontainers tests. Stop what you started when done unless the member wants it kept. The iOS Simulator is not available in the sandbox: ask the member to run those tests.';
55
- return ' Docker is not running on this machine and the iOS Simulator is not available in the sandbox: ask the member to start those services (or Docker, after which a new conversation has run_with_docker) or run those tests.';
120
+ const simulator = ' The iOS Simulator is not available in the sandbox: ask the member with ask_user to run those tests.';
121
+ if (boundary.engine) return ' Docker is available through run_with_docker (the member approves each command): a database or service for tests (docker compose up -d, then the tests through bash or run_with_network), docker build, or Testcontainers tests. If the engine stops answering, start_docker asks the member to start it. Stop what you started when done unless the member wants it kept.' + simulator + HOST_PROMPT;
122
+ if (boundary.dockerCli) return ' Docker is installed but not running: when the task needs it, call start_docker (the member approves), then run_with_docker.' + simulator + HOST_PROMPT;
123
+ return ' Docker is not installed on this machine: when the task needs it, ask the member with ask_user (install it, or check another way).' + simulator + HOST_PROMPT;
56
124
  }
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import net from 'node:net';
2
3
  import path from 'node:path';
3
4
 
4
5
  // The Docker engine a company command may use once the member approves it
@@ -66,3 +67,42 @@ export function engineEnvironment(engine, home) {
66
67
  ...(process.platform === 'darwin' ? { TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE: '/var/run/docker.sock' } : {}),
67
68
  };
68
69
  }
70
+
71
+ // The Docker CLI on this machine, engine running or not.
72
+ export const dockerCli = (paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/Applications/Docker.app/Contents/Resources/bin']) =>
73
+ paths.map(dir => real(path.join(dir, 'docker'))).find(Boolean) ?? null;
74
+
75
+ // Whether the engine behind a socket answers (Docker Desktop leaves its
76
+ // socket file in place when it quits).
77
+ export function engineAlive(socket, timeoutMs = 2000) {
78
+ return new Promise(resolve => {
79
+ let reply = '';
80
+ const done = value => { clearTimeout(timer); connection.destroy(); resolve(value); };
81
+ const connection = net.createConnection(socket, () => connection.write('GET /_ping HTTP/1.0\r\nHost: docker\r\n\r\n'));
82
+ const timer = setTimeout(() => done(false), timeoutMs);
83
+ connection.on('data', chunk => { reply += chunk; if (/\r\n\r\nOK/.test(reply)) done(true); });
84
+ connection.on('error', () => done(false)); connection.on('end', () => done(/\r\n\r\nOK/.test(reply)));
85
+ });
86
+ }
87
+
88
+ // How start_docker starts an engine the member installed: the app (Docker
89
+ // Desktop, OrbStack, Rancher Desktop) or Colima on macOS, the user service on
90
+ // Linux. Null when none is installed, or only the system service is (that
91
+ // needs the member's password).
92
+ export function engineLauncher({ userHome, platform = process.platform, exists = fs.existsSync } = {}) {
93
+ if (platform === 'darwin') {
94
+ for (const app of ['Docker', 'OrbStack', 'Rancher Desktop']) {
95
+ const bundle = [`/Applications/${app}.app`, path.join(userHome, `Applications/${app}.app`)].find(exists);
96
+ if (bundle) return { label: app === 'Docker' ? 'Docker Desktop' : app, command: '/usr/bin/open', args: ['-g', '-a', bundle] };
97
+ }
98
+ const colima = ['/opt/homebrew/bin/colima', '/usr/local/bin/colima'].find(exists);
99
+ return colima ? { label: 'Colima', command: colima, args: ['start'] } : null;
100
+ }
101
+ // WSL2: Docker Desktop is a Windows app that serves this distribution.
102
+ const desktop = '/mnt/c/Program Files/Docker/Docker/Docker Desktop.exe';
103
+ if (exists('/proc/sys/fs/binfmt_misc/WSLInterop') && exists(desktop))
104
+ return { label: 'Docker Desktop', command: '/mnt/c/Windows/System32/cmd.exe', args: ['/c', 'start', '""', 'C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe'] };
105
+ const units = [path.join(userHome, '.config/systemd/user/docker.service'), '/usr/lib/systemd/user/docker.service', '/usr/lib/systemd/user/podman.socket'];
106
+ const unit = units.find(exists);
107
+ return unit ? { label: unit.endsWith('podman.socket') ? 'Podman' : 'Docker (rootless)', command: '/usr/bin/systemctl', args: ['--user', 'start', path.basename(unit)] } : null;
108
+ }
@@ -0,0 +1,67 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import { spawn } from 'node:child_process';
4
+ import { redactedText } from '../runtime/managed-failure.mjs';
5
+
6
+ // Commands the member approved to run on their machine, outside the sandbox
7
+ // (run_on_host, start_docker): installing a runtime the project needs, git
8
+ // with the member's own sign-in, a file outside the project (owner,
9
+ // 2026-10-13: once the member approves, it runs; only what needs the member
10
+ // themselves goes back to them).
11
+
12
+ // What only the member can do: type their password, sign in interactively.
13
+ const PASSWORD = new Set(['sudo', 'doas', 'su', 'pkexec', 'passwd']);
14
+ const SIGN_IN = [/^gh auth (login|refresh)\b/, /^(npm|pnpm) (login|adduser)\b/, /^yarn npm login\b/, /^docker login\b(?!.*--password-stdin)/, /^az login\b/,
15
+ /^gcloud auth (login|application-default login)\b/, /^aws (configure\s*$|sso login\b)/, /^(firebase|vercel|wrangler|heroku|netlify) login\b/];
16
+ const WRAPPERS = new Set(['env', 'command', 'exec', 'time', 'nohup']);
17
+ export function memberOnlyReason(command) {
18
+ for (const part of String(command).split(/&&|\|\||;|\||\n/)) {
19
+ const words = part.trim().split(/\s+/).filter(Boolean);
20
+ while (words.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]) || WRAPPERS.has(words[0]))) words.shift();
21
+ const name = words[0]?.split('/').pop(), rest = words.join(' ');
22
+ if (PASSWORD.has(name)) return `${name} needs the member's password`;
23
+ if (name === 'installer' || name === 'softwareupdate') return `${name} needs the member's password`;
24
+ if (SIGN_IN.some(rule => rule.test(rest))) return 'an interactive sign-in needs the member';
25
+ }
26
+ return null;
27
+ }
28
+ // What a command printed when it stopped to ask for a password or a terminal.
29
+ export const NEEDS_MEMBER = /sudo: a (terminal|password) is required|no tty present|askpass|Password:\s*$|must be run from a terminal|Inappropriate ioctl for device/im;
30
+
31
+ export const MEMBER_ONLY = reason => `managed-host-member-only: ${reason}. Lệnh này member phải tự làm (nhập mật khẩu máy hoặc đăng nhập tương tác). Dùng ask_user: đưa đúng lệnh hoặc các bước, nói vì sao cần, kèm lựa chọn "Tôi đã làm xong" và "Bỏ qua bước này"; member trả lời xong thì kiểm tra lại và làm tiếp.`;
32
+
33
+ const TAIL = 50 * 1024;
34
+ const tail = text => text.length > TAIL ? `[… ${text.length - TAIL} characters before …]\n${text.slice(-TAIL)}` : text;
35
+ const shell = () => process.platform === 'darwin' ? '/bin/zsh' : fs.existsSync('/bin/bash') ? '/bin/bash' : '/bin/sh';
36
+
37
+ // Runs one command as the member, in a login shell (Homebrew, SDKMAN… on
38
+ // PATH as in their Terminal). Nothing of Piagent's own environment (Studio
39
+ // tokens) is passed; no input, so a prompt ends the command instead of hanging.
40
+ export function runHostCommand({ command, cwd, userHome = os.homedir(), timeout = 600, signal, onUpdate, spawnImpl = spawn }) {
41
+ return new Promise((resolve, reject) => {
42
+ if (signal?.aborted) return reject(Error('Command aborted'));
43
+ const login = shell(), user = os.userInfo().username;
44
+ const child = spawnImpl(login, ['-lc', command], { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'],
45
+ env: { HOME: userHome, USER: user, LOGNAME: user, SHELL: login, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin', LANG: 'en_US.UTF-8', TERM: 'dumb',
46
+ GIT_TERMINAL_PROMPT: '0', NONINTERACTIVE: '1', HOMEBREW_NO_ENV_HINTS: '1', DEBIAN_FRONTEND: 'noninteractive' } });
47
+ let output = '', ended = null, last = 0;
48
+ const kill = why => { ended ??= why; try { process.kill(-child.pid, 'SIGTERM'); } catch {} setTimeout(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch {} }, 2000).unref(); };
49
+ const seconds = Math.min(Math.max(Number(timeout) || 600, 1), 1800);
50
+ const timer = setTimeout(() => kill(`Command timed out after ${seconds} seconds`), seconds * 1000); timer.unref();
51
+ const abort = () => kill('Command aborted'); signal?.addEventListener('abort', abort, { once: true });
52
+ const take = chunk => {
53
+ output += chunk; if (output.length > 4 * TAIL) output = output.slice(-2 * TAIL);
54
+ if (onUpdate && Date.now() - last > 250) { last = Date.now(); onUpdate({ content: [{ type: 'text', text: redactedText(tail(output)) }], details: { host: true } }); }
55
+ };
56
+ child.stdout.on('data', take); child.stderr.on('data', take);
57
+ child.on('error', () => { ended ??= 'managed-host-command-failed: the shell did not start'; });
58
+ child.on('close', code => {
59
+ clearTimeout(timer); signal?.removeEventListener('abort', abort);
60
+ const text = redactedText(tail(output.trimEnd()));
61
+ if (ended) return reject(Error(`${text}${text ? '\n\n' : ''}${ended}`));
62
+ if (code === 0) return resolve({ content: [{ type: 'text', text: text || '(no output)' }], details: { host: true, exitCode: 0 } });
63
+ const member = NEEDS_MEMBER.test(output) ? `\n\n${MEMBER_ONLY('the command stopped to ask for a password or a terminal')}` : '';
64
+ reject(Error(`${text}${member}\n\nCommand exited with code ${code}`));
65
+ });
66
+ });
67
+ }
@@ -43,8 +43,14 @@ function javaHome(userHome) {
43
43
  }
44
44
  try {
45
45
  const home = execFileSync('/usr/libexec/java_home', [], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], env: { HOME: userHome, PATH: '/usr/bin:/bin' } }).trim();
46
- return existing(home);
47
- } catch { return null; }
46
+ if (existing(home)) return existing(home);
47
+ } catch { /* none registered with macOS */ }
48
+ // A JDK macOS does not list: Homebrew's openjdk formulas (keg-only, what
49
+ // run_on_host installs without a password), SDKMAN's current Java.
50
+ const kegs = ['/opt/homebrew/opt', '/usr/local/opt'].flatMap(dir => {
51
+ try { return fs.readdirSync(dir).filter(name => /^openjdk(@\d+)?$/.test(name)).map(name => path.join(dir, name, 'libexec/openjdk.jdk/Contents/Home')); } catch { return []; }
52
+ }).sort((a, b) => (/openjdk\//.test(b) - /openjdk\//.test(a)) || Number(/@(\d+)/.exec(b)?.[1] ?? 0) - Number(/@(\d+)/.exec(a)?.[1] ?? 0));
53
+ return [...kegs, path.join(userHome, '.sdkman/candidates/java/current')].map(existing).find(Boolean) ?? null;
48
54
  }
49
55
 
50
56
  // SwiftPM and xcodebuild start sandbox-exec for package manifests and