@piagent/platform 1.1.6 → 1.1.7
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/README.md +1 -1
- package/docs/capability-packs.md +21 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +7 -7
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/project-adapters.md +23 -0
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +6 -6
- package/docs/security-threat-model.md +1 -1
- package/docs/team-onboarding.md +3 -3
- package/docs-site/index.html +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/capabilities/capability-core.js +104 -7
- package/packages/piagent-core/capabilities/project-profile.js +83 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +96 -26
- package/packages/piagent-core/package.json +1 -1
- package/schemas/project-profile.schema.json +25 -7
- package/scripts/capability-catalog.mjs +28 -7
- package/scripts/init-project.sh +7 -0
- package/scripts/install-global.sh +3 -3
- package/scripts/profile-doctor.sh +13 -3
- package/scripts/runtime-policy-smoke.sh +12 -3
- package/scripts/team-doctor.sh +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
This file records release-facing changes for Pi Agent Platform. Copy the relevant version block into GitHub Releases when publishing a tag.
|
|
4
4
|
|
|
5
|
+
## v1.1.7 - 2026-07-28
|
|
6
|
+
|
|
7
|
+
A platform update is one global install. It was not behaving like one: every project that selected capability packs stopped working after a release until its profile was reapplied by hand, and the policy a release corrected never reached a project that had already onboarded.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- A capability lock that is behind the installed platform is re-pinned instead of refusing the session. The lock pinned two different things and gave them one answer. What the project consented to — which packs, from where, and how far they may reach — still blocks: a grant that widens, a protection that disappears, a pack that changes identity. Which build produced the resolution — package version, runtime file digests, artifact content — moves on every release, and blocking on it put a per-project chore behind a global update while not defending against the case it resembles, since the code that verifies the lock is itself one of the files the lock pins. A build move with an unchanged grant is recorded and reported.
|
|
12
|
+
|
|
13
|
+
- Artifact content is part of the build, not part of consent. Adapter policy files, base policy, prompts, skills, subagents and recipes all ship as pack artifacts, so pinning their bytes meant a corrected policy could never reach a project referencing it. Nothing is given up: which artifacts a pack provides is declared in its manifest and the manifest digest is still pinned, so an artifact that appears, disappears or moves still blocks — and a policy artifact that weakens still blocks, because the permissions it resolves to are compared directionally.
|
|
14
|
+
|
|
15
|
+
- A project profile can name the adapter it takes its policy from instead of copying it. `{"extends": "web-frontend", "projectId": ..., "displayName": ...}` keeps the project's identity and overrides in the project, and resolves the rest from the installed platform at runtime. Any key the project states replaces the adapter's key whole; a profile without `extends` keeps working exactly as before, self-contained. `init-project.sh` and `/onboard-project` write this form when the profile comes from a built-in adapter, so onboarding stays the one per-project step and updating stays global.
|
|
16
|
+
|
|
17
|
+
A profile naming an adapter that is not installed refuses rather than falling back to something weaker.
|
|
18
|
+
|
|
5
19
|
## v1.1.6 - 2026-07-27
|
|
6
20
|
|
|
7
21
|
Monorepo fixes found auditing how the platform scopes itself to a project. Every case was reproduced against a real directory tree before it was changed.
|
package/README.md
CHANGED
|
@@ -283,7 +283,7 @@ This repository intentionally excludes:
|
|
|
283
283
|
|
|
284
284
|
## Maturity
|
|
285
285
|
|
|
286
|
-
The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.1.
|
|
286
|
+
The current package version is read from package metadata and release tags. Personal machines may follow the unpinned package source when accepting ongoing updates; production/team quickstarts and committed project settings should pin an explicit tag such as `v1.1.7` or a reviewed commit.
|
|
287
287
|
|
|
288
288
|
Ready for:
|
|
289
289
|
|
package/docs/capability-packs.md
CHANGED
|
@@ -153,6 +153,7 @@ Pack không thể cấp thêm quyền cho chính nó:
|
|
|
153
153
|
- Doctor phát hiện catalog hoặc lock stale.
|
|
154
154
|
- Generated output dùng temporary file cùng thư mục, `fsync`, rồi atomic rename.
|
|
155
155
|
- Lock gắn với declared package-source identifier, package version, catalog và digest của các runtime enforcement file đang cài. Đây là integrity binding của runtime quan sát được, không phải chứng minh nguồn gốc cryptographic của package archive.
|
|
156
|
+
- Lock drift được phân loại hai nhánh, xem `## Lock drift` bên dưới.
|
|
156
157
|
- Optional runtime package được cài bằng exact version; installer đối chiếu registry integrity với digest đã phê duyệt trước khi cài.
|
|
157
158
|
- Khi cập nhật profile, lock được ghi trước; nếu profile không thể ghi thì lock được rollback. Process interruption tạo trạng thái mismatch fail-closed thay vì áp dụng profile thiếu lock.
|
|
158
159
|
|
|
@@ -166,6 +167,26 @@ Pack không thể cấp thêm quyền cho chính nó:
|
|
|
166
167
|
- `validate-action` chỉ kiểm tra contract, thời hạn và secret indicator; không authorize action, không đối chiếu requested permission với profile/lock và không đọc artifact để xác minh digest/size.
|
|
167
168
|
- External action vẫn cần human confirmation và executor riêng trước khi có side effect.
|
|
168
169
|
|
|
170
|
+
## Lock drift
|
|
171
|
+
|
|
172
|
+
Lock pin hai thứ khác nhau, và khi lệch thì câu trả lời cũng khác nhau.
|
|
173
|
+
|
|
174
|
+
**Consent — project phải đồng ý lại, session bị chặn (`blocked`):**
|
|
175
|
+
|
|
176
|
+
- `schemaVersion` của lock, `core.apiVersion`, `core.packageSource`;
|
|
177
|
+
- `profile` — `projectId`, `mode`, tên file, digest của document project tự lưu;
|
|
178
|
+
- `packs` — name, version, origin, source, owner, lifecycle và digest của manifest. Manifest khai luôn `provides`, nên artifact xuất hiện thêm, biến mất hoặc đổi path đều nằm ở nhánh này;
|
|
179
|
+
- permission theo hướng: granting key (`capabilities`, `filesystemRead`, `filesystemWrite`, `networkDomains`, `externalActions`) chặn khi **thêm**; restricting key (`protectedPaths`, `readOnlyPaths`, `shellProtectedPaths`) chặn khi **mất**.
|
|
180
|
+
|
|
181
|
+
**Build — platform đổi bản, lock được ghi lại và báo, session chạy tiếp (`repin`):**
|
|
182
|
+
|
|
183
|
+
- `core.packageVersion`, `core.runtimeFiles`, `core.packageDigest`;
|
|
184
|
+
- nội dung artifact (`artifacts[].digest`).
|
|
185
|
+
|
|
186
|
+
Vì sao nội dung artifact nằm ở nhánh build: adapter policy file, base policy, prompt, skill, subagent và recipe đều ship dạng artifact. Pin bytes của chúng nghĩa là bản sửa policy không bao giờ tới được project đang tham chiếu — đúng thứ mà việc ship policy tập trung tồn tại để làm. Không mất gì khi bỏ pin: danh sách artifact nằm trong manifest và manifest digest vẫn được pin ở `packs`, còn policy artifact bị nới lỏng vẫn chặn vì permission sau resolve được so trực tiếp theo hướng.
|
|
187
|
+
|
|
188
|
+
Việc pin build cũng chưa bao giờ chống được trường hợp nó trông giống: code kiểm tra lock chính là một trong các file mà lock pin, nên thứ gì sửa được platform đã cài thì sửa luôn được checker.
|
|
189
|
+
|
|
169
190
|
## Lifecycle
|
|
170
191
|
|
|
171
192
|
| Trạng thái | Ý nghĩa |
|
|
@@ -419,8 +419,8 @@ Các lệnh này chạy ngoài Pi.
|
|
|
419
419
|
| Command | Dùng khi nào |
|
|
420
420
|
|---|---|
|
|
421
421
|
| `npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0` | Cài Pi CLI tương thích với release hiện tại. |
|
|
422
|
-
| `npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
423
|
-
| `pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
422
|
+
| `npm install -g --ignore-scripts @piagent/platform@1.1.7` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
|
|
423
|
+
| `pi install git:github.com/Vt-mmm/piagent@v1.1.7` | Cài pinned release khi cần reproducible team setup. |
|
|
424
424
|
| `pi install git:github.com/Vt-mmm/piagent` | Cài latest package platform cho máy cá nhân/sandbox. |
|
|
425
425
|
| `piagent-update --check` | Báo version hiện tại vs version sẽ lên cho cả ba thành phần; không cài gì. |
|
|
426
426
|
| `piagent-update` | Full update một lệnh: Pi host → npm-global helper → Pi package, đúng thứ tự release yêu cầu. Thêm `--project <path>` để chạy doctor sau. |
|
|
@@ -14,9 +14,9 @@ Repo này phải chạy được cho nhiều project/domain khác nhau. Vì vậ
|
|
|
14
14
|
|
|
15
15
|
| Use case | Package source |
|
|
16
16
|
|---|---|
|
|
17
|
-
| Team pinned release | `git:github.com/Vt-mmm/piagent@v1.1.
|
|
17
|
+
| Team pinned release | `git:github.com/Vt-mmm/piagent@v1.1.7` |
|
|
18
18
|
| Personal/sandbox dev | `git:github.com/Vt-mmm/piagent` |
|
|
19
|
-
| Registry distribution | `npm:@piagent/platform@1.1.
|
|
19
|
+
| Registry distribution | `npm:@piagent/platform@1.1.7` |
|
|
20
20
|
| Enterprise npm (fork under another scope) | `npm:@your-scope/platform@x.y.z` |
|
|
21
21
|
| Local platform dev | `/path/to/piagent` |
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ Latest tiện cho máy cá nhân muốn nhận cập nhật nhanh. Pin tag/commi
|
|
|
26
26
|
|
|
27
27
|
| Channel | Command | Target | Policy |
|
|
28
28
|
|---|---|---|---|
|
|
29
|
-
| `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.1.
|
|
29
|
+
| `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.1.7` | Install the Pi package matching the helper. |
|
|
30
30
|
| `exact` | `bash scripts/install-global.sh --version vX.Y.Z --resolve-tag` | Requested tag resolved to commit SHA | Pi-package-only rollout, rollback, and incident recovery. |
|
|
31
31
|
| `dev` | `bash scripts/install-global.sh --dev` | Moving Git source | Personal/sandbox only; do not commit into project settings. |
|
|
32
32
|
| `local` | `bash scripts/install-global.sh --local` | Current checkout path | Platform development only. |
|
|
@@ -53,7 +53,7 @@ Root `package.json` có `pi` manifest trỏ tới:
|
|
|
53
53
|
Do đó team có thể:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
56
|
+
pi install git:github.com/Vt-mmm/piagent@v1.1.7
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Không cần biết internal folder `packages/piagent-core`. Source không pin chỉ dành cho personal/sandbox như bảng channel ở trên.
|
|
@@ -65,12 +65,12 @@ Team nên install global package một lần:
|
|
|
65
65
|
```bash
|
|
66
66
|
node --version # >= 22.19.0
|
|
67
67
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
68
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
68
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.7
|
|
69
69
|
piagent-install --stable --dry-run
|
|
70
70
|
piagent-install --stable
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
73
|
+
`pi install git:github.com/Vt-mmm/piagent@v1.1.7` vẫn hợp lệ nếu chỉ cần cài Pi package. Lệnh đó không tự tạo các binary terminal `piagent-*`; muốn có helper global thì dùng `npm install -g --ignore-scripts @piagent/platform@1.1.7`.
|
|
74
74
|
|
|
75
75
|
Support matrix hiện tại: macOS Apple Silicon + Bash và Linux x64 + Bash đã được verify; macOS Intel + Bash và Linux ARM64 + Bash là supported target cần chạy smoke trước rollout; native Windows chưa phải target rollout team; WSL2 experimental. Chi tiết và version runtime nằm trong [release/install policy](release-install-policy.md).
|
|
76
76
|
|
|
@@ -103,7 +103,7 @@ Nếu muốn commit sẵn `.pi/piagent-profile.json` vào repo hoặc bootstrap
|
|
|
103
103
|
```bash
|
|
104
104
|
bash scripts/setup.sh /path/to/project \
|
|
105
105
|
--profile be-readonly-fe \
|
|
106
|
-
--package-source git:github.com/Vt-mmm/piagent@v1.1.
|
|
106
|
+
--package-source git:github.com/Vt-mmm/piagent@v1.1.7 \
|
|
107
107
|
--mcp-preset core \
|
|
108
108
|
--subagents-preset safe
|
|
109
109
|
```
|
|
@@ -18,12 +18,12 @@ Support matrix release hiện tại: macOS Apple Silicon + Bash và Linux x64 +
|
|
|
18
18
|
```bash
|
|
19
19
|
node --version # >= 22.19.0
|
|
20
20
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
21
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
21
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.7
|
|
22
22
|
piagent-install --stable --dry-run
|
|
23
23
|
piagent-install --stable
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Khi seed `.pi/settings.json` cho team/repo cần audit lặp lại, giữ package source dạng pinned tag như `git:github.com/Vt-mmm/piagent@v1.1.
|
|
26
|
+
Khi seed `.pi/settings.json` cho team/repo cần audit lặp lại, giữ package source dạng pinned tag như `git:github.com/Vt-mmm/piagent@v1.1.7`. Máy cá nhân có thể dùng `git:github.com/Vt-mmm/piagent` để theo latest.
|
|
27
27
|
|
|
28
28
|
Nếu đang ở source checkout của platform, dùng helper theo channel để preview trước khi đổi:
|
|
29
29
|
|
|
@@ -99,7 +99,7 @@ install package once
|
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
101
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
102
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
102
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.7
|
|
103
103
|
piagent-install --stable --dry-run
|
|
104
104
|
piagent-install --stable
|
|
105
105
|
```
|
|
@@ -281,7 +281,7 @@ Không phải daily default. Dùng khi muốn tạo sẵn `.pi` files cho projec
|
|
|
281
281
|
bash /path/to/piagent/scripts/setup.sh /path/to/project \
|
|
282
282
|
--project-only \
|
|
283
283
|
--profile auto \
|
|
284
|
-
--package-source git:github.com/Vt-mmm/piagent@v1.1.
|
|
284
|
+
--package-source git:github.com/Vt-mmm/piagent@v1.1.7 \
|
|
285
285
|
--mcp-preset core \
|
|
286
286
|
--subagents-preset safe
|
|
287
287
|
```
|
|
@@ -854,8 +854,8 @@ Watchdog là optional adversarial reviewer ở cuối turn, không bật mặc
|
|
|
854
854
|
|
|
855
855
|
| Command | Dùng để |
|
|
856
856
|
|---|---|
|
|
857
|
-
| `npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
858
|
-
| `pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
857
|
+
| `npm install -g --ignore-scripts @piagent/platform@1.1.7` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
|
|
858
|
+
| `pi install git:github.com/Vt-mmm/piagent@v1.1.7` | Install pinned release cho reproducible team setup. |
|
|
859
859
|
| `pi install git:github.com/Vt-mmm/piagent` | Install latest platform package cho máy cá nhân/sandbox. |
|
|
860
860
|
| Cài exact Pi host của release, rồi `npm install -g --ignore-scripts @piagent/platform@X.Y.Z` và `piagent-install --stable` | Full update: đồng bộ host, terminal helper và Pi package. Mỗi release pin một Pi host chính xác; lấy đúng version của release đang cài trong [release/install policy](release-install-policy.md). |
|
|
861
861
|
| Cài exact Pi host ghi trong release cũ, rồi helper `vPREVIOUS` và `piagent-install --stable` | Full rollback; đánh giá lại dependency findings của host cũ trước khi hạ version. |
|
|
@@ -952,7 +952,7 @@ Mở lại Pi session sau khi install.
|
|
|
952
952
|
Cài lại terminal helper đúng release rồi kiểm tra `PATH`:
|
|
953
953
|
|
|
954
954
|
```bash
|
|
955
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
955
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.7
|
|
956
956
|
command -v piagent-install
|
|
957
957
|
```
|
|
958
958
|
|
package/docs/project-adapters.md
CHANGED
|
@@ -70,6 +70,29 @@ Path policy fields have distinct meanings:
|
|
|
70
70
|
|
|
71
71
|
Glob trong các field này neo từ project root, nên `backend/**` chỉ khớp backend nằm ở root. Với monorepo, phải liệt kê đúng vị trí thật (`packages/api/**`, `services/*/**`). Không dùng `**/api/**`: nó khớp luôn `packages/web/src/api/` — thư mục HTTP client của frontend — và biến chính phần được phép sửa thành read-only.
|
|
72
72
|
|
|
73
|
+
## Profile theo reference (`extends`)
|
|
74
|
+
|
|
75
|
+
Profile ở trên là dạng self-contained: project ghi toàn bộ policy. Dạng này đóng băng policy tại thời điểm onboard — platform sửa adapter thì project cũ vẫn giữ bản copy cũ mãi.
|
|
76
|
+
|
|
77
|
+
`extends` biến policy thành reference. Project chỉ giữ phần thuộc về project, phần còn lại lấy từ adapter trong platform đang cài:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"schemaVersion": 1,
|
|
82
|
+
"extends": "web-frontend",
|
|
83
|
+
"projectId": "my-project",
|
|
84
|
+
"displayName": "My Project"
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Quy tắc:
|
|
89
|
+
|
|
90
|
+
- `extends` trỏ tới một thư mục trong `adapters/` của platform đang cài. Tên không tồn tại → guard refuse, không fallback im lặng.
|
|
91
|
+
- Mọi key project tự khai **thay thế nguyên key** của adapter, không merge từng phần tử. Khai `protectedPaths` nghĩa là project sở hữu list đó và không nhận thay đổi từ adapter nữa.
|
|
92
|
+
- Không khai `extends` thì profile giữ nguyên hành vi cũ, self-contained.
|
|
93
|
+
|
|
94
|
+
`scripts/init-project.sh` và `/onboard-project` ghi dạng `extends` khi profile đến từ built-in adapter. Update platform sau đó không cần chạm vào project nào.
|
|
95
|
+
|
|
73
96
|
## Monorepo và workspace
|
|
74
97
|
|
|
75
98
|
Một project = một thư mục mở `pi`. Guard đọc profile tại `<cwd>/.pi/piagent-profile.json` và không đi ngược lên tìm, nên mở `pi` ở thư mục cha chứa nhiều project sẽ bỏ qua profile của từng project con và chạy unprofiled.
|
|
@@ -49,6 +49,7 @@ pi install /path/to/piagent
|
|
|
49
49
|
- `1.1.4`: nhận diện secret dạng CLI option (`--token`, `--password`, `--api-key`) và Authorization header ở mọi vị trí argument.
|
|
50
50
|
- `1.1.5`: thêm `piagent-update` — một lệnh update cả Pi host, terminal helper và Pi package đúng thứ tự.
|
|
51
51
|
- `1.1.6`: `be-readonly-fe` giữ được backend read-only trong monorepo, detect FE/BE đọc cả workspace, và rule detect gộp về một chỗ.
|
|
52
|
+
- `1.1.7`: update global không còn chặn project đang chạy — lock chỉ block khi grant thật sự đổi, và profile dạng `extends` nhận policy từ platform đang cài thay vì giữ bản copy lúc onboard.
|
|
52
53
|
|
|
53
54
|
## Không publish
|
|
54
55
|
|
|
@@ -10,7 +10,7 @@ This file is the canonical install, update, rollback, and release checklist. Oth
|
|
|
10
10
|
|
|
11
11
|
All supported environments require Node.js `>=22.19.0` and Pi Coding Agent `0.82.0`. The Pi host is installed as a Node CLI; Pi Agent Platform still defines its own release matrix because the terminal helpers and shell policy rely on Bash/POSIX behavior.
|
|
12
12
|
|
|
13
|
-
| Surface | Status for v1.1.
|
|
13
|
+
| Surface | Status for v1.1.7 | Rollout guidance |
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| macOS Apple Silicon (`darwin/arm64`) + Bash | Verified for this release. | Safe default for team rollout after normal project smoke tests. |
|
|
16
16
|
| Linux x64 + Bash | Verified in GitHub Actions. | Safe default for CI/server usage after normal project smoke tests. |
|
|
@@ -33,7 +33,7 @@ These three components are versioned independently. A full install, update, or r
|
|
|
33
33
|
|
|
34
34
|
| Channel | Source shape | Mutability | Use when |
|
|
35
35
|
|---|---|---:|---|
|
|
36
|
-
| `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.1.
|
|
36
|
+
| `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.1.7` | Fixed after resolution | Default for team rollout. |
|
|
37
37
|
| `exact` | Tag, reviewed commit, or a tag resolved with `--resolve-tag` | Fixed when using a commit SHA | Pi-package-only roll forward, rollback, or reproduction. |
|
|
38
38
|
| `dev` | `git:github.com/Vt-mmm/piagent` | Moving | Personal machine or sandbox only. |
|
|
39
39
|
| `local` | `/path/to/piagent` | Local workspace | Platform development and dry-run validation. |
|
|
@@ -53,7 +53,7 @@ Use this flow when the team needs both terminal commands and the Pi package:
|
|
|
53
53
|
```bash
|
|
54
54
|
node --version # >= 22.19.0
|
|
55
55
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
56
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
56
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.7
|
|
57
57
|
piagent-install --stable --dry-run
|
|
58
58
|
piagent-install --stable
|
|
59
59
|
```
|
|
@@ -61,8 +61,8 @@ piagent-install --stable
|
|
|
61
61
|
The stable preview and apply output includes:
|
|
62
62
|
|
|
63
63
|
```text
|
|
64
|
-
currentRelease: v1.1.
|
|
65
|
-
tag: v1.1.
|
|
64
|
+
currentRelease: v1.1.7 (helper package version)
|
|
65
|
+
tag: v1.1.7
|
|
66
66
|
resolvedCommit: <40-char-sha>
|
|
67
67
|
source: git:github.com/Vt-mmm/piagent@<40-char-sha>
|
|
68
68
|
```
|
|
@@ -79,7 +79,7 @@ bash scripts/install-global.sh --stable
|
|
|
79
79
|
Use this only when terminal commands are not needed:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
pi install git:github.com/Vt-mmm/piagent@v1.1.
|
|
82
|
+
pi install git:github.com/Vt-mmm/piagent@v1.1.7
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
Direct `pi install` does not create `piagent-*` commands on `PATH`.
|
|
@@ -21,7 +21,7 @@ This document covers the packaged guard, project profiles, capability locks, ins
|
|
|
21
21
|
- The operator, operating-system account, Pi host, and explicitly trusted project repository are trusted to execute code.
|
|
22
22
|
- Project-local Pi resources are loaded only after Pi project trust or an explicit operator override.
|
|
23
23
|
- A human remains available to approve destructive and external-provider actions.
|
|
24
|
-
- Supported release verification covers macOS Apple Silicon + Bash, Linux x64 + Bash, Node.js `>=22.19.0`, and the exact Pi host version declared by the release. macOS Intel + Bash and Linux ARM64 + Bash are supported targets that need local smoke verification before broad rollout. Native Windows is not a team-rollout target for v1.1.
|
|
24
|
+
- Supported release verification covers macOS Apple Silicon + Bash, Linux x64 + Bash, Node.js `>=22.19.0`, and the exact Pi host version declared by the release. macOS Intel + Bash and Linux ARM64 + Bash are supported targets that need local smoke verification before broad rollout. Native Windows is not a team-rollout target for v1.1.7, and WSL2 is experimental/unverified.
|
|
25
25
|
- Registry, GitHub, model-provider, MCP-provider, and Vercel controls are external dependencies. Their account security and platform guarantees are not replaced by this repository.
|
|
26
26
|
|
|
27
27
|
## Threat actors and failure modes
|
package/docs/team-onboarding.md
CHANGED
|
@@ -9,7 +9,7 @@ Một thành viên mới không cần biết local path của maintainer. Luồn
|
|
|
9
9
|
```bash
|
|
10
10
|
node --version # >= 22.19.0
|
|
11
11
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
12
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
12
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.7
|
|
13
13
|
piagent-install --stable
|
|
14
14
|
cd /path/to/project
|
|
15
15
|
pi
|
|
@@ -42,7 +42,7 @@ Mặc định team dùng stable helper:
|
|
|
42
42
|
```bash
|
|
43
43
|
node --version # >= 22.19.0
|
|
44
44
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@0.82.0
|
|
45
|
-
npm install -g --ignore-scripts @piagent/platform@1.1.
|
|
45
|
+
npm install -g --ignore-scripts @piagent/platform@1.1.7
|
|
46
46
|
piagent-install --stable --dry-run
|
|
47
47
|
piagent-install --stable
|
|
48
48
|
```
|
|
@@ -70,7 +70,7 @@ piagent-update --check
|
|
|
70
70
|
piagent-update --project /path/to/project
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`--check` chỉ báo version, không đụng gì. Lên đúng một bản cụ thể thì `piagent-update --version 1.1.
|
|
73
|
+
`--check` chỉ báo version, không đụng gì. Lên đúng một bản cụ thể thì `piagent-update --version 1.1.7`.
|
|
74
74
|
|
|
75
75
|
Sequence thủ công vẫn dùng được, và bắt buộc theo thứ tự này — `piagent-install` **fail** nếu Pi host lệch bản pin, nó không tự nâng host:
|
|
76
76
|
|
package/docs-site/index.html
CHANGED
|
@@ -798,7 +798,7 @@
|
|
|
798
798
|
<span>Pi Agent Platform</span>
|
|
799
799
|
</a>
|
|
800
800
|
<div class="top-actions">
|
|
801
|
-
<span class="pill"><span class="pill-dot"></span>v1.1.
|
|
801
|
+
<span class="pill"><span class="pill-dot"></span>v1.1.7 docs</span>
|
|
802
802
|
<a class="button social" href="https://github.com/Vt-mmm/piagent" target="_blank" rel="noopener noreferrer" aria-label="Mở GitHub repository">
|
|
803
803
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
804
804
|
<path d="M7 17 17 7M9 7h8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
@@ -934,7 +934,7 @@
|
|
|
934
934
|
<article class="step">
|
|
935
935
|
<div>
|
|
936
936
|
<h3>Cài — hai lệnh</h3>
|
|
937
|
-
<p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.1.
|
|
937
|
+
<p>Cần Node.js 22.19.0 trở lên. Không cần cài Pi Coding Agent trước: <span class="inline-code">piagent-setup</span> tự cài đúng bản <span class="inline-code">0.82.0</span> mà release <span class="inline-code">v1.1.7</span> đang pin. Chạy nó ngay trong thư mục project — nó cài Pi package, khởi tạo <span class="inline-code">.pi/</span>, cài MCP baseline cùng subagents, rồi chạy doctor. Thêm <span class="inline-code">--no-mcp</span>, <span class="inline-code">--no-subagents</span>, <span class="inline-code">--no-herdr</span> hoặc <span class="inline-code">--global-only</span> để bỏ bớt.</p>
|
|
938
938
|
<div class="table-wrap compact-table">
|
|
939
939
|
<table>
|
|
940
940
|
<thead>
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { containsSensitiveText } from "../security/sensitive-data.js";
|
|
5
|
+
import { resolveProjectProfileDocument } from "./project-profile.js";
|
|
5
6
|
|
|
6
7
|
const API_VERSION = "piagent/v1";
|
|
7
8
|
const CORE_API_VERSION = 1;
|
|
@@ -785,6 +786,9 @@ function buildCoreIntegrity(root) {
|
|
|
785
786
|
// capability-sources.js decides which external trees become readable packs,
|
|
786
787
|
// so tampering with it changes which code the profile can grant.
|
|
787
788
|
"packages/piagent-core/capabilities/capability-sources.js",
|
|
789
|
+
// project-profile.js decides which document is enforced when a project takes
|
|
790
|
+
// its policy from an adapter, so it selects the protected paths themselves.
|
|
791
|
+
"packages/piagent-core/capabilities/project-profile.js",
|
|
788
792
|
"packages/piagent-core/security/sensitive-data.js",
|
|
789
793
|
"packages/piagent-core/extensions/piagent-guard.ts",
|
|
790
794
|
// guard-io.js loads the project profile and guard-shell-analysis.ts decides
|
|
@@ -904,7 +908,7 @@ export function resolveCapabilityProfileDocument(root, profile, options = {}) {
|
|
|
904
908
|
projectId: profile.projectId,
|
|
905
909
|
mode: profile.mode,
|
|
906
910
|
file: profileFile,
|
|
907
|
-
digest: sha256(stableJson(profile))
|
|
911
|
+
digest: sha256(stableJson(options.storedProfile ?? profile))
|
|
908
912
|
},
|
|
909
913
|
packs: [...selected.values()].map((pack) => ({
|
|
910
914
|
name: pack.name,
|
|
@@ -927,25 +931,118 @@ export function resolveCapabilityProfileDocument(root, profile, options = {}) {
|
|
|
927
931
|
|
|
928
932
|
export function resolveCapabilityProfile(root, profilePath, options = {}) {
|
|
929
933
|
const profileAbsolute = fs.realpathSync(profilePath);
|
|
930
|
-
const
|
|
934
|
+
const stored = readJsonFile(profileAbsolute);
|
|
935
|
+
const { profile } = resolveProjectProfileDocument(fs.realpathSync(root), stored);
|
|
931
936
|
return resolveCapabilityProfileDocument(root, profile, {
|
|
932
937
|
profileFile: path.basename(profileAbsolute),
|
|
938
|
+
// The lock records the document the project owns. A profile that takes its
|
|
939
|
+
// policy from an adapter is meant to follow that adapter, so digesting the
|
|
940
|
+
// resolved document instead would turn every platform correction into a
|
|
941
|
+
// lock the project has to re-agree to by hand.
|
|
942
|
+
storedProfile: stored,
|
|
933
943
|
packageSource: options.packageSource,
|
|
934
944
|
extraRoots: options.extraRoots
|
|
935
945
|
});
|
|
936
946
|
}
|
|
937
947
|
|
|
948
|
+
// A lock pins two different things and they deserve different answers when they
|
|
949
|
+
// stop matching.
|
|
950
|
+
//
|
|
951
|
+
// What the project consented to: which packs, from where, and how far they may
|
|
952
|
+
// reach. Granting more than that, or covering less than that, is a change the
|
|
953
|
+
// project has to agree to, so it blocks.
|
|
954
|
+
//
|
|
955
|
+
// Which build of the platform produced the resolution: its version, the digests
|
|
956
|
+
// of its runtime files, and the content of the artifacts its packs ship. That
|
|
957
|
+
// moves on every release. Blocking on it put a per-project chore behind a global
|
|
958
|
+
// update — every project on a profile that selects packs stopped working until
|
|
959
|
+
// its profile was reapplied — while not actually defending against the case it
|
|
960
|
+
// resembles: the code that verifies the lock is itself one of the files the lock
|
|
961
|
+
// pins, so anything able to rewrite the installed platform can rewrite the check
|
|
962
|
+
// along with it. A build move with an unchanged grant is re-pinned and reported,
|
|
963
|
+
// not refused.
|
|
964
|
+
//
|
|
965
|
+
// Artifact content sits on the build side deliberately. Adapter policy files,
|
|
966
|
+
// base policy, prompts, skills, subagents, and recipes all ship as artifacts, so
|
|
967
|
+
// pinning their bytes means a policy correction cannot reach a project that
|
|
968
|
+
// references it — which is the whole point of shipping policy centrally. Nothing
|
|
969
|
+
// is given up by that: which artifacts a pack provides is declared in its
|
|
970
|
+
// manifest, and the manifest digest is pinned in `packs`, so an artifact that
|
|
971
|
+
// appears, disappears, or moves still blocks. A policy artifact that weakens
|
|
972
|
+
// blocks too, because the permissions it resolves to are compared directly
|
|
973
|
+
// below.
|
|
974
|
+
const GRANTING_PERMISSIONS = ["capabilities", "filesystemRead", "filesystemWrite", "networkDomains", "externalActions"];
|
|
975
|
+
const RESTRICTING_PERMISSIONS = ["protectedPaths", "readOnlyPaths", "shellProtectedPaths"];
|
|
976
|
+
|
|
977
|
+
function consentedShape(document) {
|
|
978
|
+
return {
|
|
979
|
+
schemaVersion: document?.schemaVersion,
|
|
980
|
+
apiVersion: document?.core?.apiVersion,
|
|
981
|
+
packageSource: document?.core?.packageSource,
|
|
982
|
+
profile: document?.profile,
|
|
983
|
+
packs: document?.packs
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function consentDrift(expected, actual) {
|
|
988
|
+
const before = consentedShape(actual);
|
|
989
|
+
const after = consentedShape(expected);
|
|
990
|
+
return Object.keys(after)
|
|
991
|
+
.filter((key) => stableJson(after[key]) !== stableJson(before[key]))
|
|
992
|
+
.map((key) => `${key} no longer matches the locked value`);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function permissionDrift(expected, actual) {
|
|
996
|
+
const locked = actual?.permissions ?? {};
|
|
997
|
+
const resolved = expected?.permissions ?? {};
|
|
998
|
+
const drift = [];
|
|
999
|
+
|
|
1000
|
+
for (const key of GRANTING_PERMISSIONS) {
|
|
1001
|
+
const agreed = new Set(locked[key] ?? []);
|
|
1002
|
+
const added = (resolved[key] ?? []).filter((item) => !agreed.has(item));
|
|
1003
|
+
if (added.length > 0) drift.push(`${key} would grant ${added.join(", ")}`);
|
|
1004
|
+
}
|
|
1005
|
+
// A protection that disappears is a weakening even though the list shrank.
|
|
1006
|
+
for (const key of RESTRICTING_PERMISSIONS) {
|
|
1007
|
+
const kept = new Set(resolved[key] ?? []);
|
|
1008
|
+
const dropped = (locked[key] ?? []).filter((item) => !kept.has(item));
|
|
1009
|
+
if (dropped.length > 0) drift.push(`${key} would stop covering ${dropped.join(", ")}`);
|
|
1010
|
+
}
|
|
1011
|
+
return drift;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export function classifyCapabilityLock(expected, actual) {
|
|
1015
|
+
const expectedDigest = sha256(stableJson(expected));
|
|
1016
|
+
const actualDigest = sha256(stableJson(actual));
|
|
1017
|
+
if (crypto.timingSafeEqual(Buffer.from(expectedDigest), Buffer.from(actualDigest))) {
|
|
1018
|
+
return { status: "current", reasons: [], expectedDigest, actualDigest };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const reasons = [...consentDrift(expected, actual), ...permissionDrift(expected, actual)];
|
|
1022
|
+
if (reasons.length > 0) return { status: "blocked", reasons, expectedDigest, actualDigest };
|
|
1023
|
+
|
|
1024
|
+
return {
|
|
1025
|
+
status: "repin",
|
|
1026
|
+
reasons: [`platform build moved to ${expected?.core?.packageVersion ?? "an unknown version"}`],
|
|
1027
|
+
expectedDigest,
|
|
1028
|
+
actualDigest
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
938
1032
|
export function verifyCapabilityLock(root, profilePath, lockDocument, options = {}) {
|
|
939
1033
|
const expected = resolveCapabilityProfile(root, profilePath, {
|
|
940
1034
|
packageSource: options.packageSource,
|
|
941
1035
|
extraRoots: options.extraRoots
|
|
942
1036
|
});
|
|
943
|
-
const
|
|
944
|
-
const actualText = stableJson(lockDocument);
|
|
1037
|
+
const classification = classifyCapabilityLock(expected, lockDocument);
|
|
945
1038
|
return {
|
|
946
|
-
ok:
|
|
947
|
-
|
|
948
|
-
|
|
1039
|
+
// `ok` stays the question callers were already asking: may this project run?
|
|
1040
|
+
// A re-pinnable lock may, and the caller is told so through `status`.
|
|
1041
|
+
ok: classification.status !== "blocked",
|
|
1042
|
+
status: classification.status,
|
|
1043
|
+
reasons: classification.reasons,
|
|
1044
|
+
expectedDigest: classification.expectedDigest,
|
|
1045
|
+
actualDigest: classification.actualDigest,
|
|
949
1046
|
expected
|
|
950
1047
|
};
|
|
951
1048
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// A project profile can either state its whole policy or name the adapter it
|
|
5
|
+
// takes that policy from.
|
|
6
|
+
//
|
|
7
|
+
// Stating the whole policy freezes it. The adapters ship with the platform and
|
|
8
|
+
// are corrected by it, but a project that copied one at onboarding time keeps
|
|
9
|
+
// the copy forever, so a global update leaves every existing project on the old
|
|
10
|
+
// rules. `extends` makes the policy a reference instead: the project keeps the
|
|
11
|
+
// parts that are about the project, and the installed platform supplies the
|
|
12
|
+
// rest.
|
|
13
|
+
//
|
|
14
|
+
// Profiles written before this existed carry no `extends` and keep working
|
|
15
|
+
// exactly as they did, self-contained.
|
|
16
|
+
|
|
17
|
+
const ADAPTER_NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
18
|
+
|
|
19
|
+
export class ProjectProfileError extends Error {}
|
|
20
|
+
|
|
21
|
+
export function adapterProfileFile(platformRoot, name) {
|
|
22
|
+
if (typeof name !== "string") return undefined;
|
|
23
|
+
const trimmed = name.trim();
|
|
24
|
+
// The name indexes a directory inside the installed platform, so it is
|
|
25
|
+
// restricted to a plain slug rather than sanitised after the fact.
|
|
26
|
+
if (!ADAPTER_NAME.test(trimmed)) return undefined;
|
|
27
|
+
const candidate = path.join(platformRoot, "adapters", trimmed, "profile.json");
|
|
28
|
+
return fs.existsSync(candidate) ? candidate : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJson(file) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a stored project profile document against the installed adapters.
|
|
41
|
+
* Returns the document to enforce, plus the adapter it came from when one was
|
|
42
|
+
* named. Any key the project states replaces the adapter's key whole — there is
|
|
43
|
+
* no per-entry merging of lists, because a half-merged protectedPaths is harder
|
|
44
|
+
* to reason about than a stated one.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveProjectProfileDocument(platformRoot, stored) {
|
|
47
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored)) {
|
|
48
|
+
return { profile: stored, base: undefined };
|
|
49
|
+
}
|
|
50
|
+
if (stored.extends === undefined) return { profile: stored, base: undefined };
|
|
51
|
+
|
|
52
|
+
const file = adapterProfileFile(platformRoot, stored.extends);
|
|
53
|
+
if (!file) {
|
|
54
|
+
throw new ProjectProfileError(
|
|
55
|
+
`project profile extends "${String(stored.extends)}", which is not an adapter in the installed platform`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const adapter = readJson(file);
|
|
59
|
+
if (!adapter) throw new ProjectProfileError(`adapter profile is unreadable: ${file}`);
|
|
60
|
+
|
|
61
|
+
const { extends: base, ...overrides } = stored;
|
|
62
|
+
return { profile: { ...adapter, ...overrides }, base };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readProjectProfileDocument(platformRoot, profilePath) {
|
|
66
|
+
const stored = readJson(profilePath);
|
|
67
|
+
if (stored === undefined) return { stored: undefined, profile: undefined, base: undefined };
|
|
68
|
+
const { profile, base } = resolveProjectProfileDocument(platformRoot, stored);
|
|
69
|
+
return { stored, profile, base };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The document a project stores when it takes its policy from an adapter: what
|
|
74
|
+
* identifies this project, and nothing the platform already knows.
|
|
75
|
+
*/
|
|
76
|
+
export function buildExtendingProfile(base, projectId, displayName) {
|
|
77
|
+
return {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
extends: base,
|
|
80
|
+
projectId,
|
|
81
|
+
displayName
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -40,9 +40,11 @@ import {
|
|
|
40
40
|
redactSensitiveText
|
|
41
41
|
} from "./redaction-core.js";
|
|
42
42
|
import { detectProfileName } from "./project-shape.js";
|
|
43
|
+
import { buildExtendingProfile, resolveProjectProfileDocument } from "../capabilities/project-profile.js";
|
|
43
44
|
import {
|
|
44
45
|
resolveCapabilityProfileDocument,
|
|
45
46
|
verifyCapabilityLock,
|
|
47
|
+
writeJsonAtomic,
|
|
46
48
|
writeProfileLockAtomic
|
|
47
49
|
} from "../capabilities/capability-core.js";
|
|
48
50
|
import { resolveCapabilitySourceRoots } from "../capabilities/capability-sources.js";
|
|
@@ -417,6 +419,10 @@ const DEFAULT_POLICY: BasePolicy = {
|
|
|
417
419
|
orchestrationPolicy: DEFAULT_ORCHESTRATION_POLICY
|
|
418
420
|
};
|
|
419
421
|
|
|
422
|
+
// Which platform supplies the adapters is fixed by where this file is installed,
|
|
423
|
+
// so it is resolved once here rather than threaded through every profile load.
|
|
424
|
+
const PLATFORM_ROOT = findPlatformRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
425
|
+
|
|
420
426
|
function loadPolicy(extensionDir: string): BasePolicy {
|
|
421
427
|
const root = findPackageRoot(extensionDir);
|
|
422
428
|
return readJsonFile<BasePolicy>(path.join(root, "policies", "base-policy.json")) ?? DEFAULT_POLICY;
|
|
@@ -433,17 +439,38 @@ function fallbackProfile(cwd: string, mode = "unprofiled"): ProjectProfile {
|
|
|
433
439
|
};
|
|
434
440
|
}
|
|
435
441
|
|
|
442
|
+
// A profile that names an adapter this platform does not have cannot be
|
|
443
|
+
// enforced, and the unprofiled shape carries no readOnlyPaths — falling back to
|
|
444
|
+
// it would quietly hand write access to whatever the profile was protecting. The
|
|
445
|
+
// session stays readable and says why instead.
|
|
446
|
+
function unresolvedProfile(cwd: string, reason: string): ProjectProfile {
|
|
447
|
+
return {
|
|
448
|
+
...fallbackProfile(cwd, "unresolved-profile-base"),
|
|
449
|
+
permissionProfile: "read-only",
|
|
450
|
+
unresolvedReason: reason
|
|
451
|
+
} as ProjectProfile;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveStoredProfile(stored: ProjectProfile | undefined, cwd: string, missingMode: string): ProjectProfile {
|
|
455
|
+
if (!stored) return fallbackProfile(cwd, missingMode);
|
|
456
|
+
try {
|
|
457
|
+
return resolveProjectProfileDocument(PLATFORM_ROOT, stored).profile as ProjectProfile;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
return unresolvedProfile(cwd, error instanceof Error ? error.message : String(error));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
436
463
|
function loadProfile(cwd: string, projectTrusted = false): ProjectProfile {
|
|
437
464
|
const explicit = process.env.PIAGENT_PROFILE;
|
|
438
465
|
if (explicit && explicit.trim().length > 0) {
|
|
439
|
-
return readJsonFile<ProjectProfile>(explicit)
|
|
466
|
+
return resolveStoredProfile(readJsonFile<ProjectProfile>(explicit), cwd, "explicit-profile-unreadable");
|
|
440
467
|
}
|
|
441
468
|
|
|
442
469
|
if (!projectTrusted) {
|
|
443
470
|
return fallbackProfile(cwd, "unprofiled-global-package");
|
|
444
471
|
}
|
|
445
472
|
|
|
446
|
-
return readJsonFile<ProjectProfile>(path.join(cwd, ".pi", "piagent-profile.json"))
|
|
473
|
+
return resolveStoredProfile(readJsonFile<ProjectProfile>(path.join(cwd, ".pi", "piagent-profile.json")), cwd, "unprofiled");
|
|
447
474
|
}
|
|
448
475
|
|
|
449
476
|
function loadProfileFromContext(ctx: ExtensionContext): ProjectProfile {
|
|
@@ -530,6 +557,7 @@ function capabilitySourceRoots(
|
|
|
530
557
|
type ProjectCapabilityState = {
|
|
531
558
|
ok: boolean;
|
|
532
559
|
reason?: string;
|
|
560
|
+
repinned?: string;
|
|
533
561
|
filesystemRead?: string[];
|
|
534
562
|
filesystemWrite?: string[];
|
|
535
563
|
};
|
|
@@ -539,8 +567,18 @@ function verifyProjectCapabilityState(extensionDir: string, cwd: string, project
|
|
|
539
567
|
if (!projectTrusted) return { ok: true };
|
|
540
568
|
const profilePath = projectProfilePath(cwd);
|
|
541
569
|
if (!fs.existsSync(profilePath)) return { ok: true };
|
|
542
|
-
|
|
543
|
-
|
|
570
|
+
// Which packs a project selects can come from the adapter it extends, so the
|
|
571
|
+
// question has to be asked of the resolved document. Asking the stored one
|
|
572
|
+
// would skip the lock entirely for every project that references an adapter.
|
|
573
|
+
const stored = readJsonFile<ProjectProfile>(profilePath);
|
|
574
|
+
if (!stored) return { ok: true };
|
|
575
|
+
let profile: ProjectProfile;
|
|
576
|
+
try {
|
|
577
|
+
profile = resolveProjectProfileDocument(PLATFORM_ROOT, stored).profile as ProjectProfile;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
580
|
+
}
|
|
581
|
+
if (!Array.isArray(profile.capabilityPacks)) return { ok: true };
|
|
544
582
|
const lockPath = path.join(cwd, ".pi", "piagent-profile.lock.json");
|
|
545
583
|
if (!fs.existsSync(lockPath)) return { ok: false, reason: "Capability lock is missing. Reapply the project profile." };
|
|
546
584
|
const lock = readJsonFile<Record<string, unknown>>(lockPath);
|
|
@@ -550,13 +588,30 @@ function verifyProjectCapabilityState(extensionDir: string, cwd: string, project
|
|
|
550
588
|
packageSource: projectPackageSource(cwd),
|
|
551
589
|
extraRoots: capabilitySourceRoots(cwd, profile)
|
|
552
590
|
});
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
591
|
+
if (verification.status === "blocked") {
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
reason: `Capability lock does not match what this project agreed to: ${verification.reasons.join("; ")}. Reapply the project profile.`
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const granted = {
|
|
598
|
+
filesystemRead: verification.expected.permissions.filesystemRead,
|
|
599
|
+
filesystemWrite: verification.expected.permissions.filesystemWrite
|
|
600
|
+
};
|
|
601
|
+
if (verification.status !== "repin") return { ok: true, ...granted };
|
|
602
|
+
|
|
603
|
+
// The grant is unchanged and only the platform build moved. Record the new
|
|
604
|
+
// build instead of stopping the session, which is what turned every release
|
|
605
|
+
// into a chore in every project.
|
|
606
|
+
try {
|
|
607
|
+
writeJsonAtomic(lockPath, verification.expected);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
return {
|
|
610
|
+
ok: false,
|
|
611
|
+
reason: `Capability lock is behind the installed platform and could not be rewritten: ${error instanceof Error ? error.message : String(error)}`
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
return { ok: true, repinned: verification.reasons.join("; "), ...granted };
|
|
560
615
|
} catch (error) {
|
|
561
616
|
return { ok: false, reason: `Capability validation failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
562
617
|
}
|
|
@@ -2854,12 +2909,18 @@ function buildTechStackManifest(profileName: string, selectedOptions: TechOption
|
|
|
2854
2909
|
};
|
|
2855
2910
|
}
|
|
2856
2911
|
|
|
2912
|
+
// `profile` is the document the project stores. When it names an adapter, the
|
|
2913
|
+
// lock is resolved against that adapter but still records the stored document,
|
|
2914
|
+
// so a later platform correction is not a lock the project has to re-agree to.
|
|
2857
2915
|
function writeProfileDocumentWithLock(extensionDir: string, cwd: string, profile: ProjectProfile): ProjectProfile {
|
|
2858
2916
|
const target = projectProfilePath(cwd);
|
|
2859
|
-
const
|
|
2917
|
+
const platformRoot = findPlatformRoot(extensionDir);
|
|
2918
|
+
const resolved = resolveProjectProfileDocument(platformRoot, profile).profile as ProjectProfile;
|
|
2919
|
+
const capabilityLock = resolveCapabilityProfileDocument(platformRoot, resolved, {
|
|
2860
2920
|
profileFile: "piagent-profile.json",
|
|
2921
|
+
storedProfile: profile,
|
|
2861
2922
|
packageSource: projectPackageSource(cwd),
|
|
2862
|
-
extraRoots: capabilitySourceRoots(cwd,
|
|
2923
|
+
extraRoots: capabilitySourceRoots(cwd, resolved)
|
|
2863
2924
|
});
|
|
2864
2925
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
2865
2926
|
writeProfileLockAtomic(target, profile, path.join(cwd, ".pi", "piagent-profile.lock.json"), capabilityLock);
|
|
@@ -2909,7 +2970,7 @@ function writeTechStackSelection(
|
|
|
2909
2970
|
};
|
|
2910
2971
|
writeProfileDocumentWithLock(extensionDir, cwd, profile);
|
|
2911
2972
|
ensureProjectContextPlaceholder(cwd);
|
|
2912
|
-
return { profile, manifest };
|
|
2973
|
+
return { profile: resolveProjectProfileDocument(findPlatformRoot(extensionDir), profile).profile as ProjectProfile, manifest };
|
|
2913
2974
|
}
|
|
2914
2975
|
|
|
2915
2976
|
function normalizeTechSelections(cwd: string, profileName: string, selections: Record<string, unknown> = {}, fillRecommended = true): {
|
|
@@ -3033,20 +3094,24 @@ function buildProfileFromAdapter(extensionDir: string, cwd: string, profileName:
|
|
|
3033
3094
|
const profile = readJsonFile<ProjectProfile>(source);
|
|
3034
3095
|
if (!profile) throw new Error(`Profile unreadable: ${profileName}`);
|
|
3035
3096
|
const projectName = path.basename(cwd);
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3097
|
+
// The project records which adapter it follows, not a copy of that adapter's
|
|
3098
|
+
// rules. A copy stops receiving corrections the moment it is written.
|
|
3099
|
+
return buildExtendingProfile(
|
|
3100
|
+
profileName,
|
|
3101
|
+
projectId ? slugify(projectId) : slugify(projectName),
|
|
3102
|
+
displayName?.trim() || titleize(projectName)
|
|
3103
|
+
) as ProjectProfile;
|
|
3041
3104
|
}
|
|
3042
3105
|
|
|
3043
3106
|
function writeProfileFromAdapter(extensionDir: string, cwd: string, profileName: string, overwrite = false, projectId?: string, displayName?: string): ProjectProfile {
|
|
3044
3107
|
const target = projectProfilePath(cwd);
|
|
3045
3108
|
if (fs.existsSync(target) && !overwrite) throw new Error(".pi/piagent-profile.json already exists. Pass overwrite=true to replace it.");
|
|
3046
|
-
const
|
|
3047
|
-
writeProfileDocumentWithLock(extensionDir, cwd,
|
|
3109
|
+
const stored = buildProfileFromAdapter(extensionDir, cwd, profileName, projectId, displayName);
|
|
3110
|
+
writeProfileDocumentWithLock(extensionDir, cwd, stored);
|
|
3048
3111
|
ensureProjectContextPlaceholder(cwd);
|
|
3049
|
-
|
|
3112
|
+
// Store the reference, hand back the document it resolves to: callers report
|
|
3113
|
+
// and trace what is now in force, not the four lines that point at it.
|
|
3114
|
+
return resolveProjectProfileDocument(findPlatformRoot(extensionDir), stored).profile as ProjectProfile;
|
|
3050
3115
|
}
|
|
3051
3116
|
|
|
3052
3117
|
function readTask(cwd: string, taskId: string): TaskContract | undefined {
|
|
@@ -3659,6 +3724,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
3659
3724
|
const permissionHint = ` permission=${permissionProfile.mode}`;
|
|
3660
3725
|
ctx.ui.notify(`Piagent Pi guard loaded: ${name}${profileHint}${permissionHint}${contextHint}`, preflight.recommendation === "fresh-session" ? "warning" : "info");
|
|
3661
3726
|
if (!capabilityState.ok) ctx.ui.notify(capabilityState.reason ?? "Capability validation failed.", "warning");
|
|
3727
|
+
if (capabilityState.repinned) {
|
|
3728
|
+
ctx.ui.notify(`Capability lock re-pinned: ${capabilityState.repinned}. The capabilities this project grants are unchanged.`, "info");
|
|
3729
|
+
}
|
|
3662
3730
|
const legacyWarning = legacyProjectStateWarning(ctx.cwd);
|
|
3663
3731
|
if (legacyWarning) ctx.ui.notify(legacyWarning, "warning");
|
|
3664
3732
|
if (permissionProfile.warning) ctx.ui.notify(permissionProfile.warning, "warning");
|
|
@@ -4675,10 +4743,12 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
4675
4743
|
entry.context7.digest = snapshot.digest;
|
|
4676
4744
|
manifest.updatedAt = nowIso();
|
|
4677
4745
|
fs.writeFileSync(techStackPath(ctx.cwd), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4746
|
+
// Write back the document the project stores, not the resolved one: saving
|
|
4747
|
+
// the resolved copy would inline the adapter and stop it following.
|
|
4748
|
+
const stored = readJsonFile<ProjectProfile>(projectProfilePath(ctx.cwd));
|
|
4749
|
+
if (stored?.techStack) {
|
|
4750
|
+
stored.techStack.updatedAt = manifest.updatedAt;
|
|
4751
|
+
writeProfileDocumentWithLock(extensionDir, ctx.cwd, stored);
|
|
4682
4752
|
}
|
|
4683
4753
|
appendTrace(ctx.cwd, { event: "profile_tech_context_record", techId, role: entry.role, libraryId: snapshot.resolvedLibraryId });
|
|
4684
4754
|
appendSessionTrace(pi, { event: "profile_tech_context_record", techId, role: entry.role, libraryId: snapshot.resolvedLibraryId });
|
|
@@ -6,19 +6,37 @@
|
|
|
6
6
|
"required": [
|
|
7
7
|
"schemaVersion",
|
|
8
8
|
"projectId",
|
|
9
|
-
"displayName"
|
|
10
|
-
"mode",
|
|
11
|
-
"rootMarkers",
|
|
12
|
-
"protectedPaths",
|
|
13
|
-
"requiredContext",
|
|
14
|
-
"verifyCommands",
|
|
15
|
-
"mcpCapabilities"
|
|
9
|
+
"displayName"
|
|
16
10
|
],
|
|
11
|
+
"if": {
|
|
12
|
+
"required": [
|
|
13
|
+
"extends"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"then": {
|
|
17
|
+
"description": "A profile that names an adapter stores identity plus overrides. Every policy field it omits comes from the installed adapter at runtime, so none of them are required here."
|
|
18
|
+
},
|
|
19
|
+
"else": {
|
|
20
|
+
"required": [
|
|
21
|
+
"mode",
|
|
22
|
+
"rootMarkers",
|
|
23
|
+
"protectedPaths",
|
|
24
|
+
"requiredContext",
|
|
25
|
+
"verifyCommands",
|
|
26
|
+
"mcpCapabilities"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
17
29
|
"properties": {
|
|
18
30
|
"schemaVersion": {
|
|
19
31
|
"type": "integer",
|
|
20
32
|
"const": 1
|
|
21
33
|
},
|
|
34
|
+
"extends": {
|
|
35
|
+
"description": "Name of a built-in adapter under adapters/ in the installed platform. Its profile is the base; every other key in this file replaces the adapter's key of the same name.",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"minLength": 1,
|
|
38
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
39
|
+
},
|
|
22
40
|
"projectId": {
|
|
23
41
|
"type": "string",
|
|
24
42
|
"minLength": 1,
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
validateCapabilitySources,
|
|
21
21
|
vendorRemoteSource
|
|
22
22
|
} from "../packages/piagent-core/capabilities/capability-sources.js";
|
|
23
|
+
import {
|
|
24
|
+
buildExtendingProfile,
|
|
25
|
+
resolveProjectProfileDocument
|
|
26
|
+
} from "../packages/piagent-core/capabilities/project-profile.js";
|
|
23
27
|
|
|
24
28
|
const platformRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
29
|
const catalogPath = path.join(platformRoot, "catalog", "capabilities.json");
|
|
@@ -28,7 +32,7 @@ function usage() {
|
|
|
28
32
|
process.stdout.write(`Usage:
|
|
29
33
|
piagent-capabilities catalog [--check | --write]
|
|
30
34
|
piagent-capabilities resolve --profile <profile.json> [--output <lock.json>] [--package-source <source>]
|
|
31
|
-
piagent-capabilities apply-profile --profile <source.json> --target <piagent-profile.json> [--package-source <source>] [--force]
|
|
35
|
+
piagent-capabilities apply-profile --profile <source.json> --target <piagent-profile.json> [--extends <adapter>] [--package-source <source>] [--force]
|
|
32
36
|
piagent-capabilities doctor [--profile <profile.json>] [--lock <lock.json>] [--package-source <source>]
|
|
33
37
|
piagent-capabilities vendor --profile <profile.json> [--source <name>]
|
|
34
38
|
piagent-capabilities validate-source --package-source <source>
|
|
@@ -58,7 +62,7 @@ function parseArguments(values) {
|
|
|
58
62
|
output.flags.set(value, true);
|
|
59
63
|
continue;
|
|
60
64
|
}
|
|
61
|
-
if (!["--profile", "--output", "--target", "--lock", "--file", "--package-source", "--source"].includes(value)) throw new CapabilityValidationError(`unknown option ${value}`);
|
|
65
|
+
if (!["--profile", "--extends", "--output", "--target", "--lock", "--file", "--package-source", "--source"].includes(value)) throw new CapabilityValidationError(`unknown option ${value}`);
|
|
62
66
|
if (output.flags.has(value)) throw new CapabilityValidationError(`duplicate option ${value}`);
|
|
63
67
|
const next = values[index + 1];
|
|
64
68
|
if (!next || next.startsWith("--")) throw new CapabilityValidationError(`${value} requires a value`);
|
|
@@ -157,7 +161,7 @@ function runResolve(flags) {
|
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
function runApplyProfile(flags) {
|
|
160
|
-
assertAllowedFlags(flags, new Set(["--profile", "--target", "--package-source", "--force"]));
|
|
164
|
+
assertAllowedFlags(flags, new Set(["--profile", "--extends", "--target", "--package-source", "--force"]));
|
|
161
165
|
const source = requireExistingFile(flags.get("--profile"), "--profile");
|
|
162
166
|
const targetValue = flags.get("--target");
|
|
163
167
|
if (!targetValue) throw new CapabilityValidationError("--target is required");
|
|
@@ -166,17 +170,27 @@ function runApplyProfile(flags) {
|
|
|
166
170
|
const parent = fs.realpathSync(path.dirname(target));
|
|
167
171
|
const normalizedTarget = path.join(parent, path.basename(target));
|
|
168
172
|
if (fs.existsSync(normalizedTarget) && !flags.has("--force")) throw new CapabilityValidationError("target profile already exists; pass --force to replace it");
|
|
169
|
-
const
|
|
173
|
+
const adapter = readJson(source);
|
|
174
|
+
const base = flags.get("--extends");
|
|
175
|
+
// With --extends the project records which adapter it follows and keeps only
|
|
176
|
+
// its own identity, so a later platform correction reaches it. Without it the
|
|
177
|
+
// source document is copied whole, which is what a caller supplying its own
|
|
178
|
+
// profile file wants.
|
|
179
|
+
const stored = base === undefined
|
|
180
|
+
? adapter
|
|
181
|
+
: buildExtendingProfile(base, adapter.projectId, adapter.displayName);
|
|
182
|
+
const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
|
|
170
183
|
const packageSource = flags.get("--package-source") ?? "workspace";
|
|
171
184
|
const lock = resolveCapabilityProfileDocument(platformRoot, profile, {
|
|
172
185
|
profileFile: path.basename(normalizedTarget),
|
|
186
|
+
storedProfile: stored,
|
|
173
187
|
packageSource,
|
|
174
188
|
extraRoots: profile.capabilitySources === undefined
|
|
175
189
|
? undefined
|
|
176
190
|
: resolveCapabilitySourceRoots(projectRootForProfile(normalizedTarget), profile.capabilitySources)
|
|
177
191
|
});
|
|
178
192
|
const lockTarget = path.join(parent, "piagent-profile.lock.json");
|
|
179
|
-
writeProfileLockAtomic(normalizedTarget,
|
|
193
|
+
writeProfileLockAtomic(normalizedTarget, stored, lockTarget, lock);
|
|
180
194
|
process.stdout.write(`${JSON.stringify({ ok: true, profile: normalizedTarget, lock: lockTarget, packs: lock.packs.length })}\n`);
|
|
181
195
|
}
|
|
182
196
|
|
|
@@ -210,11 +224,18 @@ function runDoctor(flags) {
|
|
|
210
224
|
const verification = verifyCapabilityLock(platformRoot, profile, readJson(lockFile), { packageSource, extraRoots });
|
|
211
225
|
report.lock = {
|
|
212
226
|
path: lockFile,
|
|
213
|
-
current: verification.
|
|
227
|
+
current: verification.status === "current",
|
|
228
|
+
status: verification.status,
|
|
229
|
+
reasons: verification.reasons,
|
|
214
230
|
expectedDigest: verification.expectedDigest,
|
|
215
231
|
actualDigest: verification.actualDigest
|
|
216
232
|
};
|
|
217
|
-
|
|
233
|
+
// A re-pinnable lock is behind the installed build but grants exactly what
|
|
234
|
+
// it agreed to, so it is reported rather than treated as a failure. The
|
|
235
|
+
// guard rewrites it on the next session.
|
|
236
|
+
if (verification.status === "blocked") {
|
|
237
|
+
throw new CapabilityValidationError(`capability lock no longer matches what the project agreed to: ${verification.reasons.join("; ")}`);
|
|
238
|
+
}
|
|
218
239
|
}
|
|
219
240
|
} else if (flags.has("--lock")) {
|
|
220
241
|
throw new CapabilityValidationError("--lock requires --profile");
|
package/scripts/init-project.sh
CHANGED
|
@@ -219,6 +219,13 @@ if [[ "$FORCE_PROFILE" == true || ! -f "$PROFILE_TARGET" ]]; then
|
|
|
219
219
|
--target "$PROFILE_TARGET"
|
|
220
220
|
--package-source "$PACKAGE_SOURCE"
|
|
221
221
|
)
|
|
222
|
+
# A built-in adapter is referenced, not copied, so a later platform correction
|
|
223
|
+
# reaches this project. A caller supplying its own profile file still gets that
|
|
224
|
+
# file written whole.
|
|
225
|
+
if [[ "$PROFILE_PATH" == "$PLATFORM_ROOT/adapters/"*"/profile.json" ]]; then
|
|
226
|
+
adapter_name="${PROFILE_PATH#"$PLATFORM_ROOT/adapters/"}"
|
|
227
|
+
apply_profile_args+=(--extends "${adapter_name%/profile.json}")
|
|
228
|
+
fi
|
|
222
229
|
if [[ "$FORCE_PROFILE" == true ]]; then
|
|
223
230
|
apply_profile_args+=(--force)
|
|
224
231
|
fi
|
|
@@ -15,7 +15,7 @@ Package source examples:
|
|
|
15
15
|
# Installs the stable tag after resolving it to a commit SHA.
|
|
16
16
|
|
|
17
17
|
# Pin a specific release:
|
|
18
|
-
scripts/install-global.sh --version v1.1.
|
|
18
|
+
scripts/install-global.sh --version v1.1.7 --resolve-tag
|
|
19
19
|
|
|
20
20
|
# Preview the planned install/update without changing user config:
|
|
21
21
|
scripts/install-global.sh --stable --dry-run
|
|
@@ -127,7 +127,7 @@ detect_runtime_surface() {
|
|
|
127
127
|
printf 'native-windows/%s (not release-gated; terminal helpers require Bash semantics)' "$arch_name"
|
|
128
128
|
;;
|
|
129
129
|
*)
|
|
130
|
-
printf '%s/%s (outside v1.1.
|
|
130
|
+
printf '%s/%s (outside v1.1.7 release matrix)' "$os_name" "$arch_name"
|
|
131
131
|
;;
|
|
132
132
|
esac
|
|
133
133
|
}
|
|
@@ -135,7 +135,7 @@ detect_runtime_surface() {
|
|
|
135
135
|
warn_runtime_surface_if_needed() {
|
|
136
136
|
local surface="$1"
|
|
137
137
|
case "$surface" in
|
|
138
|
-
*"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.
|
|
138
|
+
*"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.7 release matrix"*)
|
|
139
139
|
echo "WARN: runtime surface is $surface." >&2
|
|
140
140
|
echo "WARN: For team rollout, run piagent-doctor plus the project smoke/verify suite on this machine before relying on it." >&2
|
|
141
141
|
;;
|
|
@@ -19,12 +19,22 @@ if [[ ! -f "$PROFILE_PATH" ]]; then
|
|
|
19
19
|
exit 1
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
PLATFORM_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
23
|
+
|
|
24
|
+
node --input-type=module - "$PROJECT_PATH" "$PROFILE_PATH" "$PLATFORM_ROOT" <<'NODE'
|
|
23
25
|
import fs from "node:fs";
|
|
24
26
|
import path from "node:path";
|
|
27
|
+
import { pathToFileURL } from "node:url";
|
|
28
|
+
|
|
29
|
+
const [projectPath, profilePath, platformRoot] = process.argv.slice(2);
|
|
30
|
+
const stored = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
// A profile that names an adapter is checked as the document that will actually
|
|
33
|
+
// be enforced, not as the few lines the project happens to store.
|
|
34
|
+
const { resolveProjectProfileDocument } = await import(
|
|
35
|
+
pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
|
|
36
|
+
);
|
|
37
|
+
const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
|
|
28
38
|
|
|
29
39
|
const errors = [];
|
|
30
40
|
const warnings = [];
|
|
@@ -25,11 +25,20 @@ bash "$ROOT/scripts/profile-doctor.sh" "$PROJECT" >/dev/null
|
|
|
25
25
|
bash "$ROOT/scripts/quality-benchmark.sh" "$PROJECT" --init >/dev/null
|
|
26
26
|
bash "$ROOT/scripts/quality-benchmark.sh" "$PROJECT" --record --scenario smoke --surface pi --result pass --tokens 1 --verify "test -s README.md" >/dev/null
|
|
27
27
|
|
|
28
|
-
node --input-type=module - "$PROJECT/.pi/piagent-profile.json" "$PROJECT/.pi/benchmarks/quality-runs.jsonl" <<'NODE'
|
|
28
|
+
node --input-type=module - "$PROJECT/.pi/piagent-profile.json" "$PROJECT/.pi/benchmarks/quality-runs.jsonl" "$ROOT" <<'NODE'
|
|
29
29
|
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { pathToFileURL } from "node:url";
|
|
30
32
|
|
|
31
|
-
const [profilePath, benchmarkPath] = process.argv.slice(2);
|
|
32
|
-
const
|
|
33
|
+
const [profilePath, benchmarkPath, platformRoot] = process.argv.slice(2);
|
|
34
|
+
const stored = JSON.parse(fs.readFileSync(profilePath, "utf8"));
|
|
35
|
+
|
|
36
|
+
// A project that names an adapter stores identity and overrides only. The policy
|
|
37
|
+
// the runtime enforces is the resolved document, so that is what gets checked.
|
|
38
|
+
const { resolveProjectProfileDocument } = await import(
|
|
39
|
+
pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
|
|
40
|
+
);
|
|
41
|
+
const profile = resolveProjectProfileDocument(platformRoot, stored).profile;
|
|
33
42
|
for (const capability of ["filesystem-readonly", "filesystem-write", "shell", "memory"]) {
|
|
34
43
|
if (!profile.mcpCapabilities.includes(capability)) {
|
|
35
44
|
throw new Error(`missing capability ${capability}`);
|
package/scripts/team-doctor.sh
CHANGED
|
@@ -137,7 +137,7 @@ function runtimeMatrixStatus() {
|
|
|
137
137
|
surface: `${platform}/${arch}`,
|
|
138
138
|
status: "outside-release-matrix",
|
|
139
139
|
teamRolloutReady: false,
|
|
140
|
-
note: "This runtime is outside the documented v1.1.
|
|
140
|
+
note: "This runtime is outside the documented v1.1.7 support matrix."
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -313,7 +313,12 @@ if (foreignAgentConfig.length > 0) {
|
|
|
313
313
|
const projectProfilePath = path.join(projectPath, ".pi", "piagent-profile.json");
|
|
314
314
|
let projectProfile = null;
|
|
315
315
|
if (fs.existsSync(projectProfilePath)) {
|
|
316
|
-
|
|
316
|
+
// Check the document that will be enforced. A profile that names an adapter
|
|
317
|
+
// stores only its own identity and overrides.
|
|
318
|
+
const { resolveProjectProfileDocument } = await import(
|
|
319
|
+
pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "project-profile.js")).href
|
|
320
|
+
);
|
|
321
|
+
const profile = resolveProjectProfileDocument(platformRoot, readJson(projectProfilePath)).profile;
|
|
317
322
|
projectProfile = profile;
|
|
318
323
|
for (const field of ["schemaVersion", "projectId", "displayName", "mode"]) {
|
|
319
324
|
if (profile[field] === undefined || profile[field] === "") errors.push(`project profile missing ${field}`);
|
|
@@ -333,7 +338,11 @@ if (fs.existsSync(projectProfilePath)) {
|
|
|
333
338
|
try {
|
|
334
339
|
const lock = readJson(projectLockPath);
|
|
335
340
|
const verification = verifyCapabilityLock(platformRoot, projectProfilePath, lock, { packageSource: projectPackageSource });
|
|
336
|
-
if (
|
|
341
|
+
if (verification.status === "blocked") {
|
|
342
|
+
errors.push(`project capability lock no longer matches what the project agreed to: ${verification.reasons.join("; ")}`);
|
|
343
|
+
} else if (verification.status === "repin") {
|
|
344
|
+
warnings.push(`project capability lock is behind the installed platform and will be re-pinned on the next Pi session (${verification.reasons.join("; ")})`);
|
|
345
|
+
}
|
|
337
346
|
} catch (error) {
|
|
338
347
|
errors.push(`project capability lock validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
339
348
|
}
|