@piagent/platform 1.46.4 → 1.47.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.
- package/CHANGELOG.md +14 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/company-dev-commands.md +2 -1
- package/docs/distribution-standard.md +6 -6
- package/docs/mcp-and-tools.md +21 -1
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/mcp.html +8 -0
- package/docs-site/content/mcp.html +8 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +9 -9
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +9 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/update.html +3 -3
- package/docs-site/en/whats-new.html +29 -17
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +9 -9
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +9 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/releases.json +36 -0
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/update.html +3 -3
- package/docs-site/whats-new.html +29 -17
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/extensions/piagent-guard.ts +2 -146
- package/packages/piagent-core/managed/command-tools.mjs +17 -3
- package/packages/piagent-core/managed/member-mcp.mjs +71 -0
- package/packages/piagent-core/managed/memory-lessons.mjs +1 -1
- package/packages/piagent-core/managed/resource-loader.mjs +19 -7
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +4 -0
- package/packages/piagent-core/managed/session.mjs +3 -3
- package/packages/piagent-core/managed/simulator-commands.mjs +117 -0
- package/packages/piagent-core/mcp/mcp-server-catalog.js +18 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/mcp-repository-gate.d.mts +10 -0
- package/packages/piagent-core/runtime/mcp-repository-gate.mjs +150 -0
- package/scripts/setup.sh +1 -1
package/docs-site/update.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.47.0" title="v1.47.0 docs · VI"><span class="dot"></span>v1.47.0</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new">Changelog</a>
|
|
@@ -145,9 +145,9 @@
|
|
|
145
145
|
<h2 id="pin-cho-team">Pin một bản cho cả team</h2>
|
|
146
146
|
<p>Khi rollout cho team, cài đúng bản Pi host và đúng bản platform, rồi chạy installer ở chế độ stable. Bước <code>--dry-run</code> in ra những gì sẽ đổi.</p>
|
|
147
147
|
<div class="command-card">
|
|
148
|
-
<header><span>Cài pin theo bản</span><button class="clip-btn" data-clip="npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0 npm install -g --ignore-scripts @piagent/platform@1.
|
|
148
|
+
<header><span>Cài pin theo bản</span><button class="clip-btn" data-clip="npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0 npm install -g --ignore-scripts @piagent/platform@1.47.0 piagent-install --stable --dry-run piagent-install --stable">Lấy lệnh</button></header>
|
|
149
149
|
<pre><code>npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0
|
|
150
|
-
npm install -g --ignore-scripts @piagent/platform@1.
|
|
150
|
+
npm install -g --ignore-scripts @piagent/platform@1.47.0
|
|
151
151
|
piagent-install --stable --dry-run
|
|
152
152
|
piagent-install --stable</code></pre>
|
|
153
153
|
</div>
|
package/docs-site/whats-new.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.47.0" title="v1.47.0 docs · VI"><span class="dot"></span>v1.47.0</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new" aria-current="page">Changelog</a>
|
|
@@ -108,7 +108,20 @@
|
|
|
108
108
|
<div class="release-filter" role="toolbar" aria-label="Lọc theo loại thay đổi"><button type="button" data-filter-kind="all" aria-pressed="true">Tất cả</button><button type="button" data-filter-kind="new" aria-pressed="false">Mới</button><button type="button" data-filter-kind="improved" aria-pressed="false">Cải thiện</button><button type="button" data-filter-kind="fixed" aria-pressed="false">Sửa lỗi</button><button type="button" data-filter-kind="security" aria-pressed="false">Bảo mật</button></div>
|
|
109
109
|
<section class="section releases" data-releases>
|
|
110
110
|
<h2 id="latest" class="sr-only">Mới nhất</h2>
|
|
111
|
-
<article class="release latest" id="v1-
|
|
111
|
+
<article class="release latest" id="v1-47-0" data-release>
|
|
112
|
+
<div class="release-meta"><a class="release-version" href="#v1-47-0">v1.47.0</a><time class="release-date" datetime="2026-10-29">29/10/2026</time></div>
|
|
113
|
+
<div class="release-body">
|
|
114
|
+
<h3 data-toc-skip id="v1-47-0-title">Phiên công ty dùng MCP server của bạn và điều khiển simulator iOS, Android</h3>
|
|
115
|
+
<p>Jira, GitHub và mọi MCP server bạn cài cho Piagent giờ dùng được trong phiên công ty, và agent tự build, chạy, chụp màn hình và test app mobile trên simulator.</p>
|
|
116
|
+
<ul class="release-items">
|
|
117
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Phiên công ty nạp các MCP server đã cài cho Piagent cá nhân, dùng chung login. Server do repo khai báo vẫn cần duyệt; kết quả tới agent dưới dạng dữ liệu.</span></li>
|
|
118
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Preset atlassian thêm Jira và Confluence Cloud: piagent-mcp --preset atlassian, rồi /mcp-auth atlassian bằng tài khoản Atlassian của bạn.</span></li>
|
|
119
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>run_on_simulator boot simulator, build, cài và mở app, chạy XCUITest, chụp màn hình, đọc log: xcrun simctl, xcodebuild, adb, emulator, gradlew, flutter, expo.</span></li>
|
|
120
|
+
<li data-kind="security"><span class="kind kind-security">Bảo mật</span><span>run_on_simulator từ chối chương trình khác, ;, &, $( ) và file ngoài project. Full access và Bypass chạy luôn, nhưng vẫn hỏi khi lệnh đụng tới credentials.</span></li>
|
|
121
|
+
</ul>
|
|
122
|
+
</div>
|
|
123
|
+
</article>
|
|
124
|
+
<article class="release" id="v1-46-4" data-release>
|
|
112
125
|
<div class="release-meta"><a class="release-version" href="#v1-46-4">v1.46.4</a><time class="release-date" datetime="2026-10-29">29/10/2026</time></div>
|
|
113
126
|
<div class="release-body">
|
|
114
127
|
<h3 data-toc-skip id="v1-46-4-title">Một subagent thay vì bị chặn; bản cập nhật đọc được models.json có comment</h3>
|
|
@@ -405,26 +418,25 @@
|
|
|
405
418
|
</ul>
|
|
406
419
|
</div>
|
|
407
420
|
</article>
|
|
408
|
-
<article class="release" id="v1-41-0" data-release>
|
|
409
|
-
<div class="release-meta"><a class="release-version" href="#v1-41-0">v1.41.0</a><time class="release-date" datetime="2026-10-24">24/10/2026</time></div>
|
|
410
|
-
<div class="release-body">
|
|
411
|
-
<h3 data-toc-skip id="v1-41-0-title">Báo cáo thay đổi giao diện với ảnh trước/sau</h3>
|
|
412
|
-
<p>Skill mới biến task front-end thành báo cáo có ảnh trước/sau được khoanh khung, thay đổi business và logic, kết quả check, hiện ngay trên dashboard.</p>
|
|
413
|
-
<ul class="release-items">
|
|
414
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Skill <code>fe-change-report</code> chụp trang cũ và mới ở chế độ headless trên cùng dữ liệu, khoanh từng thay đổi bằng khung đỏ đánh số trên cả hai ảnh.</span></li>
|
|
415
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Agent chạy check của project và toàn bộ E2E headless, ghi rõ lỗi có từ trước, rồi viết lại thay đổi về business, logic và giao diện.</span></li>
|
|
416
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Tài liệu trên dashboard vẽ <code>reports/fe-change/<slug>/report.json</code> thành báo cáo, có phóng to từng khung, kích thước màn hình khác và toàn màn hình.</span></li>
|
|
417
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Script dùng Playwright có sẵn của project, không thêm dependency, và xuất một file <code>report.html</code> độc lập, không có script, để gửi cho bất kỳ ai.</span></li>
|
|
418
|
-
<li data-kind="security"><span class="kind kind-security">Bảo mật</span><span>Báo cáo được vẽ từ dữ liệu, không từ file HTML, và chỉ ảnh PNG của nó được trả về, nên dashboard vẫn không hiển thị markup do model tạo.</span></li>
|
|
419
|
-
</ul>
|
|
420
|
-
</div>
|
|
421
|
-
</article>
|
|
422
421
|
</section>
|
|
423
422
|
<section class="section archive">
|
|
424
423
|
<h2 id="archive">Các bản trước</h2>
|
|
425
424
|
<p class="section-lead">Bản cũ hơn, gom theo tháng. Mở một tháng để xem từng thay đổi.</p>
|
|
426
425
|
<details class="release-archive">
|
|
427
|
-
<summary><span>Tháng 10/2026</span><span class="archive-count">
|
|
426
|
+
<summary><span>Tháng 10/2026</span><span class="archive-count">62</span></summary>
|
|
427
|
+
<div class="archive-row" id="v1-41-0">
|
|
428
|
+
<div class="archive-head"><a class="release-version" href="#v1-41-0">v1.41.0</a> <span class="release-date">24/10/2026</span></div>
|
|
429
|
+
<div class="archive-body">
|
|
430
|
+
<p><strong>Báo cáo thay đổi giao diện với ảnh trước/sau</strong> — Skill mới biến task front-end thành báo cáo có ảnh trước/sau được khoanh khung, thay đổi business và logic, kết quả check, hiện ngay trên dashboard.</p>
|
|
431
|
+
<ul class="release-items">
|
|
432
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Skill <code>fe-change-report</code> chụp trang cũ và mới ở chế độ headless trên cùng dữ liệu, khoanh từng thay đổi bằng khung đỏ đánh số trên cả hai ảnh.</span></li>
|
|
433
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Agent chạy check của project và toàn bộ E2E headless, ghi rõ lỗi có từ trước, rồi viết lại thay đổi về business, logic và giao diện.</span></li>
|
|
434
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Tài liệu trên dashboard vẽ <code>reports/fe-change/<slug>/report.json</code> thành báo cáo, có phóng to từng khung, kích thước màn hình khác và toàn màn hình.</span></li>
|
|
435
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Script dùng Playwright có sẵn của project, không thêm dependency, và xuất một file <code>report.html</code> độc lập, không có script, để gửi cho bất kỳ ai.</span></li>
|
|
436
|
+
<li data-kind="security"><span class="kind kind-security">Bảo mật</span><span>Báo cáo được vẽ từ dữ liệu, không từ file HTML, và chỉ ảnh PNG của nó được trả về, nên dashboard vẫn không hiển thị markup do model tạo.</span></li>
|
|
437
|
+
</ul>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
428
440
|
<div class="archive-row" id="v1-40-0">
|
|
429
441
|
<div class="archive-head"><a class="release-version" href="#v1-40-0">v1.40.0</a> <span class="release-date">24/10/2026</span></div>
|
|
430
442
|
<div class="archive-body">
|
package/docs-site/windows.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.47.0" title="v1.47.0 docs · VI"><span class="dot"></span>v1.47.0</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new">Changelog</a>
|
package/docs-site/workflows.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.47.0" title="v1.47.0 docs · VI"><span class="dot"></span>v1.47.0</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new">Changelog</a>
|
|
@@ -256,8 +256,8 @@
|
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
"path": "tests",
|
|
259
|
-
"fileCount":
|
|
260
|
-
"sha256": "
|
|
259
|
+
"fileCount": 638,
|
|
260
|
+
"sha256": "74109d76ee46eeb519070b53699a7e582a3705c74030e9ca067242566afbb5df"
|
|
261
261
|
}
|
|
262
262
|
],
|
|
263
263
|
"custodianComparisonRules": {
|
package/package.json
CHANGED
|
@@ -38,15 +38,8 @@ import {
|
|
|
38
38
|
import { detectProfileName } from "./project-shape.js";
|
|
39
39
|
import { evaluateUpdateCheck, isInstalledPlatform, readUpdateCache, startUpdateProbe } from "./update-check.js";
|
|
40
40
|
import { dormantInPersonalPi, personalPiAgentDir } from "./agent-home.js";
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
collectServers,
|
|
44
|
-
configPathForScope,
|
|
45
|
-
mcpDecisionInputs,
|
|
46
|
-
unverifiableMcpConfig
|
|
47
|
-
} from "../mcp/mcp-config-layers.js";
|
|
48
|
-
import { attributeDirectTool } from "../mcp/mcp-tool-naming.js";
|
|
49
|
-
import { approvalState } from "../mcp/mcp-approval-store.js";
|
|
41
|
+
import { collectServers } from "../mcp/mcp-config-layers.js";
|
|
42
|
+
import { evaluateMcpApproval, mcpApprovalCache } from "../runtime/mcp-repository-gate.mjs";
|
|
50
43
|
import { evaluateServerReadiness, readinessNotice } from "../mcp/mcp-auth-readiness.js";
|
|
51
44
|
import * as mcpActions from "../mcp/mcp-command-actions.js";
|
|
52
45
|
import { buildExtendingProfile, resolveProjectProfileDocument } from "../capabilities/project-profile.js";
|
|
@@ -1789,143 +1782,6 @@ function prepareToolInputForPolicy(
|
|
|
1789
1782
|
};
|
|
1790
1783
|
}
|
|
1791
1784
|
|
|
1792
|
-
// Which MCP server a tool call is addressed to. The adapter exposes one proxy
|
|
1793
|
-
// tool named `mcp` that carries the server in `input.server`, and, under
|
|
1794
|
-
// directTools, one tool per server tool named `<prefix>_<tool>` where the prefix
|
|
1795
|
-
// is derived from the server name. Direct names are matched against the servers
|
|
1796
|
-
// this gate is holding rather than against a fixed pattern, because the prefix
|
|
1797
|
-
// is the server's own name and there is nothing constant to anchor on.
|
|
1798
|
-
function mcpServerFromToolCall(
|
|
1799
|
-
toolName: string,
|
|
1800
|
-
input: Record<string, unknown>,
|
|
1801
|
-
candidates: Iterable<string>
|
|
1802
|
-
): string | undefined {
|
|
1803
|
-
if (normalizeActionToken(toolName) === "mcp") {
|
|
1804
|
-
return typeof input.server === "string" && input.server.trim() ? input.server.trim() : undefined;
|
|
1805
|
-
}
|
|
1806
|
-
return attributeDirectTool(toolName, candidates);
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
// Servers a repository defines are not usable until somebody on this machine has
|
|
1810
|
-
// approved them. This is the enforcement point: the adapter owns the connection
|
|
1811
|
-
// and this extension cannot prevent one being opened, but no tool call reaches an
|
|
1812
|
-
// unapproved server. The residual exposure is the connection itself, which is
|
|
1813
|
-
// worth stating plainly rather than describing this as a full block.
|
|
1814
|
-
//
|
|
1815
|
-
// Recomputed only when one of the files behind the decision changes, because
|
|
1816
|
-
// this runs on every tool call.
|
|
1817
|
-
interface RepositoryMcpGate {
|
|
1818
|
-
blocked: Map<string, { state: string; origin: string }>; serverNames: Set<string>;
|
|
1819
|
-
// Conditions under which no tool call can be checked at all, each with the
|
|
1820
|
-
// sentence to show. These are not "a server is unapproved" — they are "this
|
|
1821
|
-
// repository's config has put the gate in a state where it cannot tell which
|
|
1822
|
-
// server a call belongs to", and the only sound answer to that is to stop.
|
|
1823
|
-
unverifiable: string[];
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
const mcpApprovalCache = new Map<string, { signature: string; gate: RepositoryMcpGate }>();
|
|
1827
|
-
|
|
1828
|
-
function repositoryMcpGate(cwd: string): RepositoryMcpGate {
|
|
1829
|
-
// Repository scopes plus anything an `imports` key drags in from a file the
|
|
1830
|
-
// clone carries. Collecting those here is the whole point: a server the gate
|
|
1831
|
-
// never enumerates is a server it silently permits.
|
|
1832
|
-
const servers = collectServers({ projectPath: cwd, scopes: [...REPOSITORY_SCOPES] });
|
|
1833
|
-
const repositoryFiles = [...REPOSITORY_SCOPES].map((scope) => ({
|
|
1834
|
-
scope,
|
|
1835
|
-
file: configPathForScope(scope, { projectPath: cwd })
|
|
1836
|
-
}));
|
|
1837
|
-
// Every file the decision reads, listed by the module that reads them rather
|
|
1838
|
-
// than restated here. Building this by hand is what went wrong: the scan grew
|
|
1839
|
-
// to read merged settings from all four scopes and to stat import targets
|
|
1840
|
-
// outside the repository, the hand-written list did not, and an already-loaded
|
|
1841
|
-
// guard went on permitting calls after a personal global config had put the
|
|
1842
|
-
// session in the state this gate exists to refuse.
|
|
1843
|
-
const signature = [
|
|
1844
|
-
...servers.map((server) => `${server.origin}:${server.name}:${fileSignature(server.file)}`),
|
|
1845
|
-
...repositoryFiles.map((layer) => `layer:${layer.scope}:${fileSignature(layer.file)}`),
|
|
1846
|
-
...mcpDecisionInputs({ projectPath: cwd }).map((file) => `input:${file}:${fileSignature(file)}`),
|
|
1847
|
-
`store:${fileSignature(path.join(os.homedir(), ".pi", "piagent-mcp-approvals.json"))}`
|
|
1848
|
-
].join("|");
|
|
1849
|
-
const cached = mcpApprovalCache.get(cwd);
|
|
1850
|
-
if (cached?.signature === signature) return cached.gate;
|
|
1851
|
-
|
|
1852
|
-
const blocked = new Map<string, { state: string; origin: string }>();
|
|
1853
|
-
for (const server of servers) {
|
|
1854
|
-
const state = approvalState({ projectPath: cwd, name: server.name, entry: server.entry });
|
|
1855
|
-
if (state.state === "approved") continue;
|
|
1856
|
-
// A name defined more than once keeps its blocking entry: the approved copy
|
|
1857
|
-
// does not vouch for the one nobody has looked at.
|
|
1858
|
-
blocked.set(server.name, { state: state.state, origin: server.origin });
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
// Two things a repository-carried config can do that leave nothing to check.
|
|
1862
|
-
//
|
|
1863
|
-
// It can ask for tools with no prefix, so a direct tool arrives under a bare
|
|
1864
|
-
// name carrying no evidence of its server. Refusing only the proxy there was
|
|
1865
|
-
// not a fix: the proxy is the one form that names its server, so blocking it
|
|
1866
|
-
// and allowing the bare names left the hole exactly where it was.
|
|
1867
|
-
//
|
|
1868
|
-
// It can also import a kind whose config this platform cannot enumerate, and
|
|
1869
|
-
// then the set of servers reaching the session is simply unknown. Blocking the
|
|
1870
|
-
// servers that could be listed says nothing about the ones that could not.
|
|
1871
|
-
//
|
|
1872
|
-
// Both stop every tool call. A repository can make a session refuse to run;
|
|
1873
|
-
// it must not be able to make one run a server nobody approved.
|
|
1874
|
-
// Read from the shared module rather than decided here, so `piagent-mcp
|
|
1875
|
-
// doctor` reports exactly what this refuses. The two disagreeing is how an
|
|
1876
|
-
// operator ends up reading "PASS" while every tool call is being stopped.
|
|
1877
|
-
const unverifiable = unverifiableMcpConfig({ projectPath: cwd }).map((problem) => problem.detail);
|
|
1878
|
-
|
|
1879
|
-
const gate = { blocked, serverNames: new Set(servers.map((server) => server.name)), unverifiable };
|
|
1880
|
-
mcpApprovalCache.set(cwd, { signature, gate });
|
|
1881
|
-
return gate;
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
function fileSignature(file: string): string {
|
|
1885
|
-
try {
|
|
1886
|
-
const stat = fs.statSync(file);
|
|
1887
|
-
return `${stat.mtimeMs}:${stat.size}`;
|
|
1888
|
-
} catch {
|
|
1889
|
-
return "absent";
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
function evaluateMcpApproval(
|
|
1894
|
-
cwd: string,
|
|
1895
|
-
toolName: string,
|
|
1896
|
-
input: Record<string, unknown>
|
|
1897
|
-
): { block: boolean; reason?: string } {
|
|
1898
|
-
const gate = repositoryMcpGate(cwd);
|
|
1899
|
-
|
|
1900
|
-
// Every tool call, not only the ones that look like MCP. Under either of these
|
|
1901
|
-
// conditions a call cannot be traced to a server, so there is no such thing as
|
|
1902
|
-
// a call this gate can clear.
|
|
1903
|
-
if (gate.unverifiable.length > 0) {
|
|
1904
|
-
return { block: true, reason: `Blocked every tool call: ${gate.unverifiable.join(" ")}` };
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
const server = mcpServerFromToolCall(toolName, input, gate.blocked.keys());
|
|
1908
|
-
if (!server) return { block: false };
|
|
1909
|
-
const record = gate.blocked.get(server);
|
|
1910
|
-
if (!record) return { block: false };
|
|
1911
|
-
const explanation = record.state === "rejected"
|
|
1912
|
-
? "it was rejected for this project"
|
|
1913
|
-
: record.state === "changed"
|
|
1914
|
-
? "its definition changed since it was approved"
|
|
1915
|
-
: record.origin.startsWith("import:")
|
|
1916
|
-
? `this repository imports it from ${record.origin.slice("import:".length)} config and nobody on this machine has approved it here`
|
|
1917
|
-
: "this repository defines it and nobody on this machine has approved it";
|
|
1918
|
-
return {
|
|
1919
|
-
block: true,
|
|
1920
|
-
// Named as commands, because this is read inside a session: the reader can
|
|
1921
|
-
// run these where they already are, without shelling out.
|
|
1922
|
-
reason:
|
|
1923
|
-
`Blocked MCP server ${server}: ${explanation}. ` +
|
|
1924
|
-
`Review it with \`/piagent-mcp get ${server}\`, then \`/piagent-mcp approve ${server}\` to allow it ` +
|
|
1925
|
-
`or \`/piagent-mcp reject ${server}\` to refuse it.`
|
|
1926
|
-
};
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
1785
|
// A word whose final path segment glues literal text onto an expansion --
|
|
1930
1786
|
// `.en$(echo v)`, `${D}.env` -- names a file this process cannot know without
|
|
1931
1787
|
// running the substitution. Matching the literal half against the protected
|
|
@@ -4,6 +4,7 @@ import { engineAlive, engineLauncher } from './container-engine.mjs';
|
|
|
4
4
|
import { MEMBER_ONLY, memberOnlyReason, credentialReason, runHostCommand } from './host-command.mjs';
|
|
5
5
|
import { processTools } from './background-process.mjs';
|
|
6
6
|
import { checkpointsOf } from './checkpoints.mjs';
|
|
7
|
+
import { detachedEmulator, simulatorCommandReason } from './simulator-commands.mjs';
|
|
7
8
|
|
|
8
9
|
// Commands that leave the plain sandbox, each after the member approves that
|
|
9
10
|
// exact command: run_with_network (internet, a local server or browser),
|
|
@@ -40,6 +41,16 @@ const KINDS = {
|
|
|
40
41
|
// credentials: a prompt injection's "cat ~/.ssh/id_rsa" is never silent.
|
|
41
42
|
confirm: async (self, command) => asksNothing(self) ? credentialReason(command) : await networkConfirmation(self, command) ?? 'ask',
|
|
42
43
|
},
|
|
44
|
+
// The sandbox reaches neither CoreSimulator nor adb. Simulator and app build
|
|
45
|
+
// commands only (simulator-commands.mjs), asked about as network commands:
|
|
46
|
+
// Full access and Bypass run them, except what reaches credentials.
|
|
47
|
+
simulator: {
|
|
48
|
+
name: 'run_on_simulator', label: 'Run on simulator', scope: 'simulator-command-once', provider: 'host', riskClass: 'medium',
|
|
49
|
+
description: 'Run one iOS Simulator or Android emulator command on the member\'s machine, outside the sandbox. iOS: xcrun simctl (list, boot, bootstatus -b, install, launch, terminate, openurl, io <device> screenshot <file>, ui appearance, status_bar, spawn <device> log show --info --last 5m --predicate \'process == "<App>"\'), xcodebuild with a simulator destination (build, test: XCUITest is the reliable way to tap, type and answer system dialogs), xcodegen generate, axe (describe-ui reads the screen\'s elements; tap, type, swipe where this Xcode accepts them), open -a Simulator (some Xcode versions run simulators without that window; the device works without it). Android: adb (devices, install, shell am start, shell input tap/text/swipe, exec-out screencap -p > <file>, logcat -d), emulator -avd <name> (started in the background), avdmanager, ./gradlew installDebug. Also flutter, pod install, npx expo/react-native/cap, mkdir -p and cd inside the project. Parts may be joined by && or || and piped into grep, head, tail, xcbeautify or jq; files may be written only inside the project or to /dev/null. Read a screenshot you saved with read to see the screen. Other commands: run_on_host.',
|
|
50
|
+
allow: 'Run this exact simulator or app build command once on this machine, outside the sandbox: a build can run the project\'s build scripts as you.',
|
|
51
|
+
title: 'Chạy lệnh simulator', allowText: 'chạy đúng lệnh simulator/build app này một lần trên máy bạn, ngoài sandbox: build có thể chạy script build của project bằng quyền của bạn.',
|
|
52
|
+
confirm: async (self, command) => credentialReason(command) ?? await networkConfirmation(self, command),
|
|
53
|
+
},
|
|
43
54
|
};
|
|
44
55
|
|
|
45
56
|
// The engine answers, looked up again when it did not (started since).
|
|
@@ -66,6 +77,8 @@ function commandTool(self, kind) {
|
|
|
66
77
|
if (kind === 'docker' && !await engineReady(self.boundary)) throw Error(DOCKER_STOPPED);
|
|
67
78
|
const memberOnly = kind === 'host' ? memberOnlyReason(args.command) : null;
|
|
68
79
|
if (memberOnly) throw Error(MEMBER_ONLY(memberOnly));
|
|
80
|
+
const notSimulator = kind === 'simulator' ? simulatorCommandReason(args.command, self.cwd) : null;
|
|
81
|
+
if (notSimulator) throw Error(`managed-simulator-command: ${notSimulator}.`);
|
|
69
82
|
const confirm = await spec.confirm(self, args.command);
|
|
70
83
|
if (confirm !== null) await approve(self, id, signal, ctx, { toolName: spec.name, rawAction: { command: args.command }, commandPreview: String(args.command),
|
|
71
84
|
provider: spec.provider, requestedScope: spec.scope, reason: (confirm === 'ask' ? String(args.reason) : `Full access still asks: ${confirm}. ${args.reason}`).slice(0, 300), riskClass: spec.riskClass,
|
|
@@ -75,7 +88,8 @@ function commandTool(self, kind) {
|
|
|
75
88
|
if (self.run) await checkpointsOf(self).take();
|
|
76
89
|
const before = await self.changedFiles();
|
|
77
90
|
try {
|
|
78
|
-
const result = kind === '
|
|
91
|
+
const result = kind === 'simulator' && detachedEmulator(args.command) ? await runHostCommand({ command: `nohup ${args.command} >/dev/null 2>&1 & sleep 3; echo "Emulator started in the background; wait for it with: adb wait-for-device"`, cwd: self.cwd, userHome: self.boundary.userHome, timeout: 60, signal, onUpdate })
|
|
92
|
+
: kind === 'host' || kind === 'simulator' ? await runHostCommand({ command: args.command, cwd: self.cwd, userHome: self.boundary.userHome, timeout: args.timeout, signal, onUpdate })
|
|
79
93
|
: await self.boundary.invoke('bash', { command: args.command, ...(args.timeout ? { timeout: args.timeout } : {}) }, signal, onUpdate, ctx?.model, kind === 'docker' ? { docker: true } : { network: true });
|
|
80
94
|
ok = !result?.isError; return result;
|
|
81
95
|
} finally {
|
|
@@ -115,14 +129,14 @@ function startDockerTool(self, { wait = 180_000, every = 2000, launcher: found =
|
|
|
115
129
|
|
|
116
130
|
export function commandTools(self, options) {
|
|
117
131
|
const docker = self.boundary.engine || self.boundary.dockerCli;
|
|
118
|
-
return [commandTool(self, 'network'), ...(docker ? [commandTool(self, 'docker')] : []), startDockerTool(self, options), commandTool(self, 'host'), ...processTools(self)];
|
|
132
|
+
return [commandTool(self, 'network'), ...(docker ? [commandTool(self, 'docker')] : []), startDockerTool(self, options), commandTool(self, 'host'), commandTool(self, 'simulator'), ...processTools(self)];
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
// What the system prompt says about services and what the sandbox blocks: it
|
|
122
136
|
// is read for every request, so a Docker started since shows up.
|
|
123
137
|
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.';
|
|
124
138
|
export function servicesPrompt(boundary) {
|
|
125
|
-
const simulator = ' The iOS Simulator
|
|
139
|
+
const simulator = ' The iOS Simulator and Android emulator are not reachable from bash: boot them, build and install the app, launch it, tap and type, and take screenshots with run_on_simulator (save the screenshot in the project, then read it to see the screen). Do not ask the member to run simulator steps you can run.';
|
|
126
140
|
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;
|
|
127
141
|
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;
|
|
128
142
|
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;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { evaluateMcpApproval } from '../runtime/mcp-repository-gate.mjs';
|
|
5
|
+
import { userToolchains } from './toolchain.mjs';
|
|
6
|
+
import { fenced } from './untrusted.mjs';
|
|
7
|
+
|
|
8
|
+
// The member's MCP servers in company sessions. Company sessions load no Pi
|
|
9
|
+
// extension of the member's, with one exception: the MCP adapter Piagent
|
|
10
|
+
// installs (pi-mcp-adapter in Piagent's own Pi home). It reads the same
|
|
11
|
+
// configuration as personal Piagent (piagent-mcp add, ~/.config/mcp/mcp.json,
|
|
12
|
+
// a project's .mcp.json, /mcp) and the same logins (/mcp-auth), so a server the
|
|
13
|
+
// member added once works in both. The member decides which servers exist;
|
|
14
|
+
// company sessions keep the two protections personal Piagent has: a server a
|
|
15
|
+
// repository defines runs only once someone on this machine approved it, and
|
|
16
|
+
// what a server returns reaches the agent as data, not instructions.
|
|
17
|
+
|
|
18
|
+
const ADAPTER = 'pi-mcp-adapter';
|
|
19
|
+
const readJson = file => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
20
|
+
const inside = (file, dir) => { const relative = path.relative(dir, file); return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); };
|
|
21
|
+
|
|
22
|
+
// The adapter the member installed in Piagent's Pi home and still lists among
|
|
23
|
+
// its packages, or null. `piagent studio` starts from a cleared environment, so
|
|
24
|
+
// the home is the default one (~/.piagent/agent) unless a caller names another.
|
|
25
|
+
export function memberMcpAdapter(home = os.homedir(), agentDir = path.join(home, '.piagent', 'agent')) {
|
|
26
|
+
const packages = readJson(path.join(agentDir, 'settings.json'))?.packages;
|
|
27
|
+
const listed = Array.isArray(packages) && packages.some(entry => new RegExp(`(^|[:/])${ADAPTER}(@|$)`).test(String(typeof entry === 'string' ? entry : entry?.source ?? '')));
|
|
28
|
+
const dir = path.join(agentDir, 'npm', 'node_modules', ADAPTER);
|
|
29
|
+
if (!listed || !fs.existsSync(path.join(dir, 'package.json'))) return null;
|
|
30
|
+
return { agentDir, dir: fs.realpathSync(dir) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// What the adapter and the servers it starts need from a cleared environment:
|
|
34
|
+
// Piagent's Pi home (its mcp.json, logins and caches, shared with personal
|
|
35
|
+
// Piagent) and a PATH that finds npx, uvx, docker… as in the member's
|
|
36
|
+
// Terminal. Nothing else of the member's shell is passed, as for host commands.
|
|
37
|
+
export function memberMcpEnvironment(adapter, home = os.homedir(), env = process.env) {
|
|
38
|
+
env.PI_CODING_AGENT_DIR ||= adapter.agentDir;
|
|
39
|
+
const toolchains = userToolchains(home);
|
|
40
|
+
const dirs = [...toolchains.bins, path.dirname(process.execPath), '/opt/homebrew/bin', '/usr/local/bin', ...(env.PATH ?? '/usr/bin:/bin').split(':')];
|
|
41
|
+
env.PATH = [...new Set(dirs.filter(Boolean))].join(':');
|
|
42
|
+
for (const [name, value] of Object.entries(toolchains.env)) env[name] ||= value;
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The repository approval gate on every tool call (as personal Piagent's
|
|
47
|
+
// guard), and the adapter's results fenced as data.
|
|
48
|
+
export function memberMcpGuard(cwd, adapterDir) {
|
|
49
|
+
return pi => {
|
|
50
|
+
const fromAdapter = name => { const source = pi.getAllTools().find(tool => tool.name === name)?.sourceInfo?.path; return !!source && (source === adapterDir || inside(source, adapterDir)); };
|
|
51
|
+
pi.on('tool_call', event => {
|
|
52
|
+
const decision = evaluateMcpApproval(cwd, event.toolName, event.input ?? {});
|
|
53
|
+
return decision.block ? { block: true, reason: decision.reason } : undefined;
|
|
54
|
+
});
|
|
55
|
+
pi.on('tool_result', event => {
|
|
56
|
+
if (!fromAdapter(event.toolName)) return undefined;
|
|
57
|
+
const text = (event.content ?? []).filter(part => part?.type === 'text').map(part => part.text).join('\n');
|
|
58
|
+
const images = (event.content ?? []).filter(part => part?.type === 'image');
|
|
59
|
+
return { content: [{ type: 'text', text: fenced(`Result of MCP tool ${event.toolName}`, text) }, ...images] };
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The company session's own extension factories, with the member's MCP added
|
|
65
|
+
// when the adapter is installed: [factories, extension paths] for inlineExtensions.
|
|
66
|
+
export function withMemberMcp(cwd, factories, home = os.homedir()) {
|
|
67
|
+
const adapter = memberMcpAdapter(home);
|
|
68
|
+
if (!adapter) return [factories, []];
|
|
69
|
+
memberMcpEnvironment(adapter, home);
|
|
70
|
+
return [[...factories, memberMcpGuard(cwd, adapter.dir)], [adapter.dir]];
|
|
71
|
+
}
|
|
@@ -8,7 +8,7 @@ import { createHash } from 'node:crypto';
|
|
|
8
8
|
import { redactedText } from '../runtime/managed-failure.mjs';
|
|
9
9
|
import { isCheckCommand } from './workflow.mjs';
|
|
10
10
|
|
|
11
|
-
const COMMAND_TOOLS = new Set(['bash','run_with_network','run_with_docker','run_on_host']);
|
|
11
|
+
const COMMAND_TOOLS = new Set(['bash','run_with_network','run_with_docker','run_on_host','run_on_simulator']);
|
|
12
12
|
const textOf = result => (result?.content ?? []).filter(p => p?.type === 'text').map(p => p.text ?? '').join('\n');
|
|
13
13
|
const clean = (value, max) => redactedText(String(value ?? '')).replace(/\bas_(?:live|run)_[A-Za-z0-9_-]{8,}/g,'[REDACTED_SECRET]').split(os.homedir()).join('~').replace(/\s+/g,' ').trim().slice(0,max);
|
|
14
14
|
const hash = s => createHash('sha256').update(s).digest('hex').slice(0,20);
|
|
@@ -26,10 +26,10 @@ export function projectInstructions(cwd, top = cwd) {
|
|
|
26
26
|
// discovered in a project. Skills and commands are text (agent-skills.mjs):
|
|
27
27
|
// what they ask for runs with the session's own tools.
|
|
28
28
|
// themes: Piagent's own (the company Terminal's /style), loaded by the host.
|
|
29
|
-
export function managedResourceLoader(api, { systemPrompt, extensions = [], runtime = api.createExtensionRuntime(), agentsFiles = [], skills = [], prompts = [], themes = [] } = {}) {
|
|
29
|
+
export function managedResourceLoader(api, { systemPrompt, extensions = [], runtime = api.createExtensionRuntime(), errors = [], agentsFiles = [], skills = [], prompts = [], themes = [] } = {}) {
|
|
30
30
|
const empty = Object.freeze([]);
|
|
31
31
|
return Object.freeze({
|
|
32
|
-
getExtensions: () => ({ extensions, errors:
|
|
32
|
+
getExtensions: () => ({ extensions, errors: [...errors], runtime }),
|
|
33
33
|
getSkills: () => ({ skills: [...skills], diagnostics: empty }),
|
|
34
34
|
getPrompts: () => ({ prompts: [...prompts], diagnostics: empty }),
|
|
35
35
|
getThemes: () => ({ themes: [...themes], diagnostics: empty }),
|
|
@@ -45,12 +45,24 @@ export function managedResourceLoader(api, { systemPrompt, extensions = [], runt
|
|
|
45
45
|
|
|
46
46
|
// Extensions the company Terminal adds itself (its commands and how Harness
|
|
47
47
|
// notes read), built the way Pi builds an inline extension. Never project code.
|
|
48
|
-
|
|
48
|
+
// `paths`: installed extensions loaded the way Pi loads a package's (only the
|
|
49
|
+
// member's MCP adapter, member-mcp.mjs); one that fails to load is reported in
|
|
50
|
+
// `errors` and the session opens without it.
|
|
51
|
+
export async function inlineExtensions(api, sdkRoot, cwd, factories = [], paths = []) {
|
|
49
52
|
const runtime = api.createExtensionRuntime();
|
|
50
|
-
if (!factories.length) return { extensions: [], runtime };
|
|
53
|
+
if (!factories.length && !paths.length) return { extensions: [], runtime };
|
|
51
54
|
const load = (file) => import(pathToFileURL(path.join(sdkRoot, 'dist', ...file.split('/'))).href);
|
|
52
|
-
const [{ loadExtensionFromFactory }, { createEventBus }] = await Promise.all([load('core/extensions/loader.js'), load('core/event-bus.js')]);
|
|
53
|
-
const bus = createEventBus(), extensions = [];
|
|
55
|
+
const [{ loadExtensionFromFactory, loadExtensions }, { createEventBus }] = await Promise.all([load('core/extensions/loader.js'), load('core/event-bus.js')]);
|
|
56
|
+
const bus = createEventBus(), extensions = [], errors = [];
|
|
54
57
|
for (const [index, factory] of factories.entries()) extensions.push(await loadExtensionFromFactory(factory, cwd, bus, runtime, `<inline:company-terminal-${index + 1}>`));
|
|
55
|
-
|
|
58
|
+
if (paths.length) {
|
|
59
|
+
// Pi's theme exists only once a terminal draws; the MCP adapter colours its
|
|
60
|
+
// status line with it in every mode, and without one (RPC, the WebUI) its
|
|
61
|
+
// start failed. The Terminal sets its own theme later.
|
|
62
|
+
const { theme, initTheme } = await load('modes/interactive/theme/theme.js');
|
|
63
|
+
try { void theme.fg; } catch { initTheme(); }
|
|
64
|
+
try { const loaded = await loadExtensions(paths, cwd, bus, runtime); extensions.push(...loaded.extensions); errors.push(...loaded.errors); }
|
|
65
|
+
catch (error) { errors.push({ path: paths.join(', '), error: error instanceof Error ? error.message : String(error) }); }
|
|
66
|
+
}
|
|
67
|
+
return { extensions, runtime, errors };
|
|
56
68
|
}
|
|
@@ -21,11 +21,15 @@ const DOCKER_DAEMON = /Cannot connect to the Docker daemon|connect to the docker
|
|
|
21
21
|
// A runtime or tool the command needs is not on this machine (or not the
|
|
22
22
|
// version the project asks for).
|
|
23
23
|
const TOOLCHAIN = /Unable to locate a Java Runtime|No Java runtime present|JAVA_HOME is not (?:set|defined correctly)|(?:^|\n)\S*?(?:ba|z)?sh: (?:line \d+: )?[\w.+-]+: command not found|zsh: command not found: [\w.+-]+|xcrun: error: (?:unable to find utility|invalid active developer path)|env: '?[\w.+-]+'?: No such file or directory|Unsupported class file major version|invalid target release: \d+|release version \d+ not supported|requires? (?:at least )?Java \d+|The engine "node" is incompatible|Unsupported engine/i;
|
|
24
|
+
// The iOS Simulator (CoreSimulator) and Android's adb/emulator, which the
|
|
25
|
+
// sandbox does not reach.
|
|
26
|
+
const SIMULATOR = /CoreSimulator|simctl|Unable to boot device|Unable to lookup in current state|Failed to start launchd_sim|adb: (?:cannot connect|failed to check server version|no devices|device offline)|cannot connect to daemon|\bemulator: (?:ERROR|PANIC)|Unable to find a destination matching|xcodebuild: error: Unable to find a destination/i;
|
|
24
27
|
// The member's sign-in, which the sandbox does not have.
|
|
25
28
|
const SIGN_IN = /could not read (?:Username|Password) for|terminal prompts disabled|Authentication failed for|Permission denied \(publickey|Host key verification failed|not logged into any GitHub hosts|please run:\s+gh auth login|Could not open a connection to your authentication agent|\bENEEDAUTH\b|\bE401\b|\b401 Unauthorized|requires authentication|denied: requested access to the resource is denied|pull access denied/i;
|
|
26
29
|
|
|
27
30
|
export function sandboxDiagnostic(message, { network = false, isolated = false, docker = false, engine = false } = {}) {
|
|
28
31
|
const text=String(message);
|
|
32
|
+
if(!network&&SIMULATOR.test(text))return 'managed-simulator-unreachable: Lệnh bash trong sandbox không tới được iOS Simulator hay Android emulator. Chạy lệnh simulator (xcrun simctl, xcodebuild cho simulator, adb, emulator, ./gradlew installDebug, flutter, npx expo) bằng run_on_simulator.\n'+text;
|
|
29
33
|
if(docker&&DOCKER_DAEMON.test(text))return 'managed-docker-unreachable: Lệnh chạy với Docker nhưng Docker engine không trả lời (Docker Desktop/Colima/OrbStack chưa chạy hoặc đang khởi động). 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.\n'+text;
|
|
30
34
|
if(!docker&&DOCKER_DAEMON.test(text))return (engine?'managed-docker-blocked: Lệnh bash thường không dùng được Docker trong chế độ công ty. Lệnh docker, docker compose, hoặc test cần Docker (Testcontainers) chạy qua run_with_docker để người dùng duyệt đúng lệnh đó.\n':'managed-docker-unavailable: Docker engine trên máy chưa chạy. Gọi start_docker để xin member duyệt mở Docker, rồi chạy lệnh bằng run_with_docker.\n')+text;
|
|
31
35
|
if(TOOLCHAIN.test(text))return 'managed-toolchain-missing: Máy thiếu runtime/công cụ mà lệnh cần, hoặc sai version project yêu cầu. Đừng dừng ở đây: xem project cần gì (pom.xml, build.gradle, .nvmrc, .tool-versions, package.json engines…) rồi dùng run_on_host để xin member duyệt cài trên máy (ví dụ brew install openjdk@21, brew install node@22, sdk install java 21-tem), sau đó chạy lại lệnh. Nếu project build được trong Docker thì có thể đề xuất run_with_docker. Trình cài cần mật khẩu (sudo, file .pkg) thì hỏi member bằng ask_user.\n'+text;
|
|
@@ -24,7 +24,7 @@ import { readPatchDigest, readPatchSnapshot, readChangedFiles, readHead } from '
|
|
|
24
24
|
import { memberTurn } from './continuation.mjs';
|
|
25
25
|
import { workflowPolicy, repositoryChecks, sessionPlanTool, currentPlan, workflowPrompt, RunProcess, completionGate, pendingBaseline, pendingPaths, pendingHead, processEditTools } from './workflow.mjs';
|
|
26
26
|
import { workflowContextTool, workflowContextExtension } from './workflow-context.mjs';
|
|
27
|
-
import { beforeDelegate, resolveTool } from './objections.mjs'; import { fanOut, withDroppedNote } from './copies.mjs';
|
|
27
|
+
import { beforeDelegate, resolveTool } from './objections.mjs'; import { fanOut, withDroppedNote } from './copies.mjs'; import { withMemberMcp } from './member-mcp.mjs';
|
|
28
28
|
import { HELPER_CALLS, HELPER_ROLES, helperRoles, helperPrompt, webPrompt, delegateDescription, mainWebLookups, limitedWebTools } from './helper-roles.mjs'; import { delegateParameters, delegateArgs, SPEC_PROMPT } from './helper-settlement.mjs';
|
|
29
29
|
import { loadAgentResources } from './agent-skills.mjs';
|
|
30
30
|
import { loadPiagentThemes } from '../runtime/terminal/terminal-style.mjs';
|
|
@@ -137,8 +137,8 @@ export class ManagedSession {
|
|
|
137
137
|
self.checks = repositoryChecks(agentsFiles, self.cwd, self.boundary.repositoryTop);
|
|
138
138
|
// The Harness workflow (possibly changed on a later enrollment) adds its process to the prompt.
|
|
139
139
|
const loader = managedResourceLoader(api, { systemPrompt: () => BASE_PROMPT + referencePrompt(self.boundary.userHome) + TEST_PROMPT + servicesPrompt(self.boundary) + webPrompt(helperRoles(manifest), mainWebLookups(manifest)) + helperPrompt(helperRoles(self.manifest)) + workflowPrompt(workflowPolicy(self.manifest), self.checks), agentsFiles,
|
|
140
|
-
skills: self.resources.skills, prompts: self.resources.prompts, themes: terminalExtensions.length ? await loadPiagentThemes(sdk) : [], ...await inlineExtensions(api, sdk, self.cwd,
|
|
141
|
-
[...terminalExtensions, workflowContextExtension(self.cwd, () => self.session.sessionManager)]) });
|
|
140
|
+
skills: self.resources.skills, prompts: self.resources.prompts, themes: terminalExtensions.length ? await loadPiagentThemes(sdk) : [], ...await inlineExtensions(api, sdk, self.cwd, // and the member's MCP servers (member-mcp.mjs)
|
|
141
|
+
...withMemberMcp(self.cwd, [...terminalExtensions, workflowContextExtension(self.cwd, () => self.session.sessionManager)])) });
|
|
142
142
|
const settings = api.SettingsManager.inMemory({ retry: { enabled: false, provider: { maxRetries: 0 } }, cacheWarming: 'off', enableInstallTelemetry: false, enableAnalytics: false, enableSkillCommands: true });
|
|
143
143
|
const manager = sessionManager ?? api.SessionManager.inMemory(self.cwd); attachMemory(self, manager, agentDir, g => verifyGrant(g, self.manifest, 'main')); self.studioLink = new StudioLink({ origin: self.origin, agentDir, notify: event => self.notify(event), ui: () => self.session?.extensionRunner?.getUIContext?.() });
|
|
144
144
|
const scope = scopeOf(self.origin, manifest);
|