@piagent/platform 1.1.2 → 1.1.4

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 CHANGED
@@ -2,6 +2,29 @@
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.4 - 2026-07-27
6
+
7
+ ### Fixed
8
+
9
+ - A credential passed as a command-line option is recognised as one. `--token=abc123`, `--token abc123`, `--password`, `--api-key`, `--client-secret` and the rest went unrecognised because the assignment patterns require a key that starts with a letter, and `-` is not one. v1.1.3 made the redactor's verdict the gate that decides whether a command may serve as verify evidence, so anything it missed was stored in the clear *and* accepted as proof — the same invariant v1.1.3 was written to enforce, defeated by the syntax most command-line tools actually use. Env-style assignments were the only shape covered.
10
+ - An `Authorization` header is redacted wherever it sits among the arguments. The value ran to the end of the line, so the same credential survived or not depending on how much text happened to follow it: `curl URL -H "Authorization: Token abc123"` left the header value one character under the length bar, while moving the header ahead of the URL swept the URL into the value and pushed it over. The value is now the single whitespace-free token that follows the scheme, and under a known authentication scheme its length is not consulted at all. Prose such as `Authorization: not required for local runs` is still left alone.
11
+
12
+ ## v1.1.3 - 2026-07-27
13
+
14
+ Fixes for defects found reviewing v1.1.2 after it shipped, including two introduced by v1.1.2 itself. Every fix was reproduced first.
15
+
16
+ ### Fixed
17
+
18
+ - Verify evidence identifies the command exactly again. v1.1.2 moved the digest onto the redacted text to stop it confirming guesses at a secret, and in doing so gave every secret the same identity: an observation of `DATABASE_PASSWORD=CorrectHorse42 npm test` satisfied a claim about `DATABASE_PASSWORD=DifferentHorse99 npm test`, so a run against one database could be recorded as proof for another. That contradicts the exact-match rule the gate is built on. Neither text is safe to hash, so a command with anything to redact now gets no digest at all and cannot be evidence; the gate says so instead of matching loosely. Commands with no secret in them — every legitimate verify command — are identified by their own text as before.
19
+ - A document replaced by a named pipe is refused instead of hanging. The identity check that would have rejected it runs after the file is opened, and opening a pipe waits for a writer, so the substitution did not have to defeat the check — it only had to stop it from running. The read blocked indefinitely. The file is now opened without blocking and without following a symbolic link at the final component.
20
+ - Short credentials are redacted. Values under a key that names a secret outright were kept in the clear below eight or twelve characters, so `TOKEN=hunter2` was stored verbatim in the evidence ledger. Length is only ambiguous after a bare `:`, where English uses the same shape — `password: short` is prose, and `Authorization: Token abc` puts a scheme name where a value would go. Under `=`, inside quotes, in a query string, or as a field in a structure, any value that is not a placeholder is now redacted at any length.
21
+ - A redirect must land on the page being verified. `npm run site:check` compared the hostname, scheme, port and credentials of the redirect target but not its path, so `/missing` or `/?release=old` passed while the run only ever fetched `/`.
22
+ - `piagent-install` reports a missing Node runtime as a missing Node runtime. Validating the MCP preset early, added in v1.1.2, put a Node script ahead of the Node version check, so on a machine without Node the installer failed with `node: command not found` and blamed the preset.
23
+
24
+ ### Changed
25
+
26
+ - `docs/release-install-policy.md` named the wrong release for the code-scanning gap. A pull request last merged with a failing CodeQL check and an open high alert on `v1.1.0`; on `v1.1.2` the results gate blocked the pull request until the alerts were cleared.
27
+
5
28
  ## v1.1.2 - 2026-07-26
6
29
 
7
30
  Security and release-gating fixes found reviewing v1.1.1 after it shipped. Every fix in this release was reproduced as a working exploit or failure first.
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.2` or a reviewed commit.
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.4` or a reviewed commit.
287
287
 
288
288
  Ready for:
289
289
 
@@ -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.2` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
423
- | `pi install git:github.com/Vt-mmm/piagent@v1.1.2` | Cài pinned release khi cần reproducible team setup. |
422
+ | `npm install -g --ignore-scripts @piagent/platform@1.1.4` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
423
+ | `pi install git:github.com/Vt-mmm/piagent@v1.1.4` | 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
  | 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, npm-global 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). |
426
426
  | Cài exact Pi host ghi trong release cũ, rồi helper `vPREVIOUS` và `piagent-install --stable` | Full rollback; đánh giá dependency risk của host cũ trước khi hạ version. |
@@ -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.2` |
17
+ | Team pinned release | `git:github.com/Vt-mmm/piagent@v1.1.4` |
18
18
  | Personal/sandbox dev | `git:github.com/Vt-mmm/piagent` |
19
- | Registry distribution | `npm:@piagent/platform@1.1.2` |
19
+ | Registry distribution | `npm:@piagent/platform@1.1.4` |
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.2` | Install the Pi package matching the helper. |
29
+ | `stable` | `bash scripts/install-global.sh --stable` | Helper release tag resolved to commit SHA, currently `v1.1.4` | 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.2
56
+ pi install git:github.com/Vt-mmm/piagent@v1.1.4
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.2
68
+ npm install -g --ignore-scripts @piagent/platform@1.1.4
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.2` 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.2`.
73
+ `pi install git:github.com/Vt-mmm/piagent@v1.1.4` 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.4`.
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.2 \
106
+ --package-source git:github.com/Vt-mmm/piagent@v1.1.4 \
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.2
21
+ npm install -g --ignore-scripts @piagent/platform@1.1.4
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.2`. Máy cá nhân có thể dùng `git:github.com/Vt-mmm/piagent` để theo latest.
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.4`. 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.2
102
+ npm install -g --ignore-scripts @piagent/platform@1.1.4
103
103
  piagent-install --stable --dry-run
104
104
  piagent-install --stable
105
105
  ```
@@ -269,7 +269,7 @@ Không phải daily default. Dùng khi muốn tạo sẵn `.pi` files cho projec
269
269
  bash /path/to/piagent/scripts/setup.sh /path/to/project \
270
270
  --project-only \
271
271
  --profile auto \
272
- --package-source git:github.com/Vt-mmm/piagent@v1.1.2 \
272
+ --package-source git:github.com/Vt-mmm/piagent@v1.1.4 \
273
273
  --mcp-preset core \
274
274
  --subagents-preset safe
275
275
  ```
@@ -842,8 +842,8 @@ Watchdog là optional adversarial reviewer ở cuối turn, không bật mặc
842
842
 
843
843
  | Command | Dùng để |
844
844
  |---|---|
845
- | `npm install -g --ignore-scripts @piagent/platform@1.1.2` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
846
- | `pi install git:github.com/Vt-mmm/piagent@v1.1.2` | Install pinned release cho reproducible team setup. |
845
+ | `npm install -g --ignore-scripts @piagent/platform@1.1.4` | Cài terminal helper `piagent-*` từ release tag hiện tại. |
846
+ | `pi install git:github.com/Vt-mmm/piagent@v1.1.4` | Install pinned release cho reproducible team setup. |
847
847
  | `pi install git:github.com/Vt-mmm/piagent` | Install latest platform package cho máy cá nhân/sandbox. |
848
848
  | 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). |
849
849
  | 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. |
@@ -940,7 +940,7 @@ Mở lại Pi session sau khi install.
940
940
  Cài lại terminal helper đúng release rồi kiểm tra `PATH`:
941
941
 
942
942
  ```bash
943
- npm install -g --ignore-scripts @piagent/platform@1.1.2
943
+ npm install -g --ignore-scripts @piagent/platform@1.1.4
944
944
  command -v piagent-install
945
945
  ```
946
946
 
@@ -45,6 +45,8 @@ pi install /path/to/piagent
45
45
  - `1.0.2`: đủ security review, MCP registry, adapter schema versioned.
46
46
  - `1.1.0`: đọc document ngoài project qua `additionalReadRoots`, MCP baseline bật mặc định ở cả hai lệnh install, pin Pi host `0.82.0`.
47
47
  - `1.1.2`: đóng TOCTOU của document reader, hash evidence tính trên text đã redact, publish chờ đủ verify matrix.
48
+ - `1.1.3`: command mang secret không còn được dùng làm verify evidence, document reader từ chối FIFO thay vì treo, redact secret ngắn, redirect phải về đúng trang được kiểm.
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.
48
50
 
49
51
  ## Không publish
50
52
 
@@ -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.2 | Rollout guidance |
13
+ | Surface | Status for v1.1.4 | 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.2` | Fixed after resolution | Default for team rollout. |
36
+ | `stable` | `git:github.com/Vt-mmm/piagent@<resolved-commit-sha>` from `v1.1.4` | 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.2
56
+ npm install -g --ignore-scripts @piagent/platform@1.1.4
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.2 (helper package version)
65
- tag: v1.1.2
64
+ currentRelease: v1.1.4 (helper package version)
65
+ tag: v1.1.4
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.2
82
+ pi install git:github.com/Vt-mmm/piagent@v1.1.4
83
83
  ```
84
84
 
85
85
  Direct `pi install` does not create `piagent-*` commands on `PATH`.
@@ -192,7 +192,7 @@ Before broad team access, a repository administrator must enable these external
192
192
  3. Enable Dependency graph, Dependabot alerts, and Dependabot security updates. Keep `.github/dependabot.yml` enabled for scheduled npm and GitHub Actions update pull requests.
193
193
  4. Enable GitHub private vulnerability reporting so the form linked by `SECURITY.md` works before public rollout.
194
194
  5. Keep secret scanning and push protection enabled. Review CodeQL results after the workflow's first successful run.
195
- 6. Add the **Code scanning results** check to the `main` branch ruleset and set its alert threshold so that a new high-severity CodeQL alert blocks the merge. The `analyze (...)` jobs required above only prove the analysis ran and uploaded its SARIF; they succeed just as well when the analysis found new high-severity alerts. Without this separate check, a pull request merges green with open high alerts, which is what happened on `v1.1.2`. Requiring the `analyze` jobs is still necessary — it catches an analysis that failed to run — but it is not the results gate and must not be mistaken for one.
195
+ 6. Add the **Code scanning results** check to the `main` branch ruleset and set its alert threshold so that a new high-severity CodeQL alert blocks the merge. The `analyze (...)` jobs required above only prove the analysis ran and uploaded its SARIF; they succeed just as well when the analysis found new high-severity alerts. Without this separate check, a pull request merges green with open high alerts, which is what happened on `v1.1.0`: its pull request carried a failing `CodeQL` check run and a high alert open since the day before, and merged anyway because the rule did not yet require the result. Requiring the `analyze` jobs is still necessary — it catches an analysis that failed to run — but it is not the results gate and must not be mistaken for one.
196
196
 
197
197
  Verify these settings in GitHub **Settings → Rules** and **Settings → Code security and analysis**. Treat any missing control as an explicit rollout blocker, not as a passing source-code gate. Repository rules also close the remaining mutability risk of bootstrap-by-tag; tag-to-SHA resolution inside `piagent-install` alone cannot protect the helper package before it starts.
198
198
 
@@ -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.2, and WSL2 is experimental/unverified.
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.4, 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
@@ -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.2
12
+ npm install -g --ignore-scripts @piagent/platform@1.1.4
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.2
45
+ npm install -g --ignore-scripts @piagent/platform@1.1.4
46
46
  piagent-install --stable --dry-run
47
47
  piagent-install --stable
48
48
  ```
@@ -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.2 docs</span>
801
+ <span class="pill"><span class="pill-dot"></span>v1.1.4 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.2</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>
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.4</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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -510,14 +510,24 @@ export type ResolvedDocument = Extract<DocumentResolution, { status: "ok" }>;
510
510
 
511
511
  const DOCUMENT_CHANGED = "document changed between the safety checks and the read, so nothing was read";
512
512
 
513
+ // Opening blocks until a writer appears when the name has become a FIFO, and the
514
+ // identity check below never runs — the read hangs instead of being refused.
515
+ // O_NONBLOCK returns a descriptor immediately for any file type, and has no
516
+ // effect on reads from a regular file, which is the only type that gets past the
517
+ // check. O_NOFOLLOW refuses a symbolic link at the final component: the path was
518
+ // already canonicalised, so a link there means the name was swapped after it was
519
+ // checked, which is the substitution this open exists to defeat.
520
+ const OPEN_FLAGS = fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | fs.constants.O_NOFOLLOW;
521
+
513
522
  function openVerifiedDocument(
514
523
  resolution: ResolvedDocument
515
524
  ): { status: "ok"; fd: number; size: number } | { status: "error"; reason: string } {
516
525
  let fd: number;
517
526
  try {
518
- fd = fs.openSync(resolution.absolutePath, "r");
527
+ fd = fs.openSync(resolution.absolutePath, OPEN_FLAGS);
519
528
  } catch (error) {
520
529
  const code = error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : "";
530
+ if (code === "ELOOP") return { status: "error", reason: DOCUMENT_CHANGED };
521
531
  return {
522
532
  status: "error",
523
533
  reason: code === "ENOENT"
@@ -9,24 +9,27 @@ export function normalizeEvidenceCommand(command) {
9
9
  .trim();
10
10
  }
11
11
 
12
- // The digest is taken over the redacted command, never the raw one.
12
+ // A command that carries a secret gets no digest at all, and cannot be evidence.
13
13
  //
14
- // The ledger stores a redacted command beside its hash. Hashing the raw text
15
- // meant the stored pair was `export TOKEN= [REDACTED_SECRET] && npm test`
16
- // alongside a SHA-256 of the real line — the redaction published the template
17
- // and the hash confirmed guesses against it, so the only unknown left was the
18
- // secret itself and SHA-256 is fast enough to walk a candidate list offline.
19
- // Redacting first makes the digest cover exactly what the file already shows.
14
+ // Neither text is safe to hash. Hashing the raw command published a template:
15
+ // the ledger stored `export TOKEN= [REDACTED_SECRET] && npm test` beside a
16
+ // SHA-256 of the real line, so the only unknown left was the secret, and SHA-256
17
+ // is fast enough to walk a candidate list offline. Hashing the redacted text
18
+ // instead gives every secret the same digest, which is worse in a different
19
+ // direction: the gate promises the observed command matches `verifyCommands`
20
+ // exactly, and two runs differing only in a credential would satisfy each
21
+ // other's claim. Verifying against database A and recording it as proof for
22
+ // database B is exactly what the exact-match rule exists to prevent.
20
23
  //
21
- // Both sides of a lookup redact, so matching is unaffected: the verify command
22
- // from the task plan and the observed command normalise and redact the same way
23
- // before being compared.
24
+ // So the digest covers the raw command and is withheld when there is anything to
25
+ // redact. Withholding it costs nothing real: a verify command in a task plan has
26
+ // no business carrying an inline credential, and a command that does gets refused
27
+ // rather than matched loosely.
24
28
  export function hashEvidenceCommand(command) {
25
29
  const normalized = normalizeEvidenceCommand(command);
26
- return crypto
27
- .createHash("sha256")
28
- .update(redactSensitiveText(normalized).text)
29
- .digest("hex");
30
+ if (!normalized) return "";
31
+ if (redactSensitiveText(normalized).redacted) return "";
32
+ return crypto.createHash("sha256").update(normalized).digest("hex");
30
33
  }
31
34
 
32
35
  export function commandMatchesVerifyPlan(command, verifyCommands) {
@@ -88,8 +91,14 @@ export function observedBashResultFromToolResultEvent(event, cwd, nowMs = Date.n
88
91
 
89
92
  function canonicalObservedEntry(entry) {
90
93
  const rawNormalizedCommand = normalizeEvidenceCommand(entry?.normalizedCommand ?? entry?.command);
91
- const commandHash = entry?.commandHash || (rawNormalizedCommand ? hashEvidenceCommand(rawNormalizedCommand) : "");
92
- if (!commandHash) return undefined;
94
+ if (!rawNormalizedCommand) return undefined;
95
+ // `??`, not `||`: an entry read back from the ledger carries an empty digest
96
+ // when its command held a secret, and that emptiness has to survive. Recomputing
97
+ // it here would hash the stored text, which is redacted, and hand the entry the
98
+ // one digest this module refuses to produce.
99
+ const commandHash = typeof entry?.commandHash === "string"
100
+ ? entry.commandHash
101
+ : hashEvidenceCommand(rawNormalizedCommand);
93
102
  const command = typeof entry?.command === "string" ? redactSensitiveText(entry.command).text : "";
94
103
  const normalizedCommand = redactSensitiveText(rawNormalizedCommand).text;
95
104
  const recordedAtMs = parseTimeMs(entry?.recordedAtMs ?? entry?.recordedAt);
@@ -106,9 +115,11 @@ function canonicalObservedEntry(entry) {
106
115
  };
107
116
  }
108
117
 
109
- function observedCommandsMatch(entry, normalizedCommand, commandHash) {
110
- if (entry.commandHash) return entry.commandHash === commandHash;
111
- return normalizeEvidenceCommand(entry.normalizedCommand ?? entry.command) === normalizedCommand;
118
+ // Identity is the digest and nothing else. Falling back to comparing the stored
119
+ // text would compare redacted text, which is the collision this module refuses.
120
+ function observedCommandsMatch(entry, commandHash) {
121
+ if (!commandHash || !entry.commandHash) return false;
122
+ return entry.commandHash === commandHash;
112
123
  }
113
124
 
114
125
  export function findMatchingObservedBashResult(entries, { cwd, command, notBefore, exitCode }) {
@@ -119,12 +130,19 @@ export function findMatchingObservedBashResult(entries, { cwd, command, notBefor
119
130
  if (!normalizedCommand) {
120
131
  return { ok: false, reason: "Verify command is empty after normalization." };
121
132
  }
133
+ if (!commandHash) {
134
+ return {
135
+ ok: false,
136
+ reason: "Verify command carries a secret, so it cannot be matched against observed evidence. "
137
+ + "Move the credential into the environment and keep it out of the command."
138
+ };
139
+ }
122
140
 
123
141
  for (let index = entries.length - 1; index >= 0; index -= 1) {
124
142
  const entry = canonicalObservedEntry(entries[index]);
125
143
  if (!entry) continue;
126
144
  if (entry.cwd !== normalizedCwd) continue;
127
- if (!observedCommandsMatch(entry, normalizedCommand, commandHash)) continue;
145
+ if (!observedCommandsMatch(entry, commandHash)) continue;
128
146
  if (entry.recordedAtMs < notBeforeMs) continue;
129
147
  if (!claimedExitMatchesObserved(exitCode, entry)) {
130
148
  return {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -15,7 +15,27 @@ const TOKEN_PATTERNS = [
15
15
  const CONNECTION_URL_PATTERN = /\b((?:https?|ftp|postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/"']+:)([^@\s/"']+)(@[^\s"'<>]+)/gi;
16
16
  const BEARER_PATTERN = /\b(Bearer\s+)([A-Za-z0-9._~+/=-]{20,})\b/gi;
17
17
  const BASIC_AUTH_PATTERN = /\b(Basic\s+)([A-Za-z0-9+/]{12,}={0,2})(?=$|[\s,;}])/gi;
18
- const AUTHORIZATION_HEADER_PATTERN = /\b(Authorization\s*:\s*)([A-Za-z][A-Za-z0-9_-]{0,31})(\s+)([^\r\n]+)/gi;
18
+ // The value is one whitespace-free token, not the rest of the line. Taking the
19
+ // rest of the line made the result depend on where the header sat among the
20
+ // arguments: `curl URL -H "Authorization: Token abc123"` yielded `abc123"`, short
21
+ // enough to keep, while moving the header before the URL swept the URL into the
22
+ // value and pushed it past the length bar. Same header, same credential, opposite
23
+ // outcome. Credentials carry no spaces, so stopping at the first one also keeps
24
+ // prose like "Authorization: not required for local runs" intact.
25
+ const AUTHORIZATION_HEADER_PATTERN = /\b(Authorization\s*:\s*)([A-Za-z][A-Za-z0-9_-]{0,31})(\s+)([^\s\r\n"'`]+)/gi;
26
+
27
+ // A credential passed as a command-line option, which is how most CLIs take one.
28
+ // The assignment patterns below require a key starting with a letter, so every
29
+ // `--token=...` and `--token ...` walked straight past them.
30
+ const CLI_OPTION_SECRET_PATTERN = /(^|[\s({[])(--[A-Za-z][A-Za-z0-9_.-]{0,80})(=|\s+)("[^"\r\n]*"|'[^'\r\n]*'|[^\s"'`;,)}]+)/g;
31
+
32
+ // Under one of these schemes the header carries an opaque credential, so its
33
+ // length says nothing. Anything else after `Authorization:` is more likely prose
34
+ // — "Authorization: not required" — and keeps the ambiguous-syntax length bar.
35
+ const AUTHORIZATION_SCHEMES = new Set([
36
+ "bearer", "basic", "token", "digest", "negotiate", "ntlm",
37
+ "oauth", "hawk", "signature", "apikey", "jwt", "aws4-hmac-sha256"
38
+ ]);
19
39
  const QUERY_SECRET_PATTERN = /([?&])([A-Za-z][A-Za-z0-9_.-]{0,80})(=)([^&#\s]+)/gi;
20
40
  const DOUBLE_QUOTED_SECRET_ASSIGNMENT_PATTERN = /(^|[\s{[,(;])((?:"|')?[A-Za-z][A-Za-z0-9_.-]{0,80}(?:"|')?\s*)(:|=(?!=))\s*"((?:\\.|[^"\\\r\n])*)"/gim;
21
41
  const SINGLE_QUOTED_SECRET_ASSIGNMENT_PATTERN = /(^|[\s{[,(;])((?:"|')?[A-Za-z][A-Za-z0-9_.-]{0,80}(?:"|')?\s*)(:|=(?!=))\s*'((?:\\.|[^'\\\r\n])*)'/gim;
@@ -54,14 +74,6 @@ const SENSITIVE_PLURAL_KEYS = new Set([
54
74
  "signing_keys",
55
75
  "authorizations"
56
76
  ]);
57
- const LOWER_THRESHOLD_PLURAL_KEYS = new Set([
58
- "passwords",
59
- "secrets",
60
- "credentials",
61
- "passphrases",
62
- "private_keys",
63
- "signing_keys"
64
- ]);
65
77
 
66
78
  function normalizeSecretValue(value) {
67
79
  return String(value ?? "")
@@ -97,13 +109,6 @@ function keyLooksSensitive(key) {
97
109
  || normalized.endsWith(`_${term}`));
98
110
  }
99
111
 
100
- function keyRequiresLowerThreshold(key) {
101
- const normalized = normalizeSecretKey(key);
102
- return LOWER_THRESHOLD_PLURAL_KEYS.has(normalized)
103
- || ["password", "passwd", "pwd", "secret", "credential", "passphrase", "private_key", "signing_key"]
104
- .some((term) => normalized === term || normalized.endsWith(`_${term}`));
105
- }
106
-
107
112
  function valueLooksPlaceholder(value) {
108
113
  const clean = normalizeSecretValue(value);
109
114
  if (!clean) return true;
@@ -115,12 +120,40 @@ function valueLooksPlaceholder(value) {
115
120
  return false;
116
121
  }
117
122
 
118
- function valueLooksSensitive(key, value) {
123
+ // Length is only evidence where the syntax is ambiguous, and `=` is not.
124
+ //
125
+ // A short value used to be kept in the clear whatever the syntax, so `TOKEN=hunter2`
126
+ // and `PASSWORD=short7` went into the ledger verbatim. Every name in
127
+ // SENSITIVE_KEY_TERMS states outright what it holds — there is no `key` or `id`
128
+ // among them — so under an `=`, or inside quotes, a non-placeholder value is the
129
+ // secret the key says it is, at any length.
130
+ //
131
+ // A bare `:` is the ambiguous case, because English uses it too: `password: short`
132
+ // and `The secret: always run tests` are prose, and `Authorization: Token abc123`
133
+ // puts the scheme name where a value would go. Those keep a length bar.
134
+ const SHORT_VALUE_MIN_LENGTH = 8;
135
+ const AMBIGUOUS_VALUE_MIN_LENGTH = 12;
136
+
137
+ function valueLooksSensitive(key, value, syntax = "unquoted-colon") {
119
138
  const clean = normalizeSecretValue(value);
120
139
  if (!clean || valueLooksPlaceholder(clean)) return false;
121
140
  if (looksLikeKnownSecret(clean)) return true;
122
- if (keyRequiresLowerThreshold(key)) return clean.length >= 8;
123
- return keyLooksSensitive(key) && clean.length >= 12;
141
+ if (!keyLooksSensitive(key)) return false;
142
+ if (syntax !== "unquoted-colon") return true;
143
+ return clean.length >= (keyNamesASecretOutright(key) ? SHORT_VALUE_MIN_LENGTH : AMBIGUOUS_VALUE_MIN_LENGTH);
144
+ }
145
+
146
+ // Under a bare `:`, these names carry enough intent on their own to redact a short
147
+ // value; the broader terms wait for length before touching prose.
148
+ function keyNamesASecretOutright(key) {
149
+ const normalized = normalizeSecretKey(key);
150
+ return ["password", "passwd", "pwd", "secret", "credential", "passphrase", "private_key", "signing_key"]
151
+ .some((term) => normalized === term || normalized.endsWith(`_${term}`))
152
+ || ["passwords", "secrets", "credentials", "passphrases", "private_keys", "signing_keys"].includes(normalized);
153
+ }
154
+
155
+ function assignmentSyntax(separator) {
156
+ return separator === "=" ? "assignment" : "unquoted-colon";
124
157
  }
125
158
 
126
159
  export function redactSensitiveText(input) {
@@ -131,29 +164,41 @@ export function redactSensitiveText(input) {
131
164
  text = text.replace(BEARER_PATTERN, (_match, prefix) => `${prefix}${REDACTION}`);
132
165
  text = text.replace(BASIC_AUTH_PATTERN, (_match, prefix) => `${prefix}${REDACTION}`);
133
166
  text = text.replace(AUTHORIZATION_HEADER_PATTERN, (match, prefix, scheme, spacing, value) => {
134
- if (!valueLooksSensitive("authorization", value)) return match;
167
+ const syntax = AUTHORIZATION_SCHEMES.has(scheme.toLowerCase()) ? "assignment" : "unquoted-colon";
168
+ if (!valueLooksSensitive("authorization", value, syntax)) return match;
135
169
  return `${prefix}${scheme}${spacing}${REDACTION}`;
136
170
  });
137
171
 
172
+ // Over-redaction is the safe direction here: a flag whose name says "secret"
173
+ // and whose argument is a plain name — `docker service create --secret build` —
174
+ // loses that name from the record and cannot serve as verify evidence. Reading
175
+ // the name as the credential it is called is the cheaper mistake.
176
+ text = text.replace(CLI_OPTION_SECRET_PATTERN, (match, prefix, flag, separator, value) => {
177
+ // A following option is the next flag, not this flag's argument.
178
+ if (separator !== "=" && value.startsWith("-")) return match;
179
+ if (!keyLooksSensitive(flag) || !valueLooksSensitive(flag, value, "assignment")) return match;
180
+ return `${prefix}${flag}${separator === "=" ? "= " : " "}${REDACTION}`;
181
+ });
182
+
138
183
  for (const pattern of TOKEN_PATTERNS) text = text.replace(pattern, REDACTION);
139
184
 
140
185
  text = text.replace(QUERY_SECRET_PATTERN, (match, prefix, key, separator, value) => {
141
- if (!keyLooksSensitive(key) || !valueLooksSensitive(key, value)) return match;
186
+ if (!keyLooksSensitive(key) || !valueLooksSensitive(key, value, "assignment")) return match;
142
187
  return `${prefix}${key}${separator}${REDACTION}`;
143
188
  });
144
189
 
145
190
  text = text.replace(DOUBLE_QUOTED_SECRET_ASSIGNMENT_PATTERN, (match, prefix, key, separator, value) => {
146
- if (!keyLooksSensitive(key) || !valueLooksSensitive(key, value)) return match;
191
+ if (!keyLooksSensitive(key) || !valueLooksSensitive(key, value, "quoted")) return match;
147
192
  return `${prefix}${key}${separator} "${REDACTION}"`;
148
193
  });
149
194
 
150
195
  text = text.replace(SINGLE_QUOTED_SECRET_ASSIGNMENT_PATTERN, (match, prefix, key, separator, value) => {
151
- if (!keyLooksSensitive(key) || !valueLooksSensitive(key, value)) return match;
196
+ if (!keyLooksSensitive(key) || !valueLooksSensitive(key, value, "quoted")) return match;
152
197
  return `${prefix}${key}${separator} '${REDACTION}'`;
153
198
  });
154
199
 
155
200
  text = text.replace(SECRET_ASSIGNMENT_PATTERN, (match, prefix, key, separator, value) => {
156
- if (!keyLooksSensitive(key) || !valueLooksSensitive(key, value)) return match;
201
+ if (!keyLooksSensitive(key) || !valueLooksSensitive(key, value, assignmentSyntax(separator))) return match;
157
202
  return `${prefix}${key}${separator} ${REDACTION}`;
158
203
  });
159
204
 
@@ -173,7 +218,9 @@ function redactStorageValue(value, key) {
173
218
  if (typeof value === "string") {
174
219
  const redacted = redactSensitiveText(value);
175
220
  if (redacted.redacted) return redacted.text;
176
- return key && keyLooksSensitive(key) && valueLooksSensitive(key, value) ? REDACTION : value;
221
+ // A field name in a structure is machine syntax, never prose, so length says
222
+ // nothing here either.
223
+ return key && keyLooksSensitive(key) && valueLooksSensitive(key, value, "assignment") ? REDACTION : value;
177
224
  }
178
225
  if (Array.isArray(value)) return value.map((item) => redactStorageValue(item, key));
179
226
  if (!value || typeof value !== "object") return value;
@@ -185,6 +185,12 @@ export function judge(host, hosts, expect, result, family) {
185
185
  if (target.protocol !== "https:") return `HTTP ${result.status} redirecting away from HTTPS to ${target.href}`;
186
186
  if (target.port !== "" && target.port !== "443") return `HTTP ${result.status} redirecting to a non-standard port at ${target.href}`;
187
187
  if (target.username !== "" || target.password !== "") return `HTTP ${result.status} redirecting to a URL carrying credentials`;
188
+ // The check that follows a redirect is the check of the other host's `/`, which
189
+ // is the only page this run verifies. A redirect to `/missing` or to `/?release=old`
190
+ // was accepted on the strength of the hostname alone, so the run reported the
191
+ // version served at a page it never fetched.
192
+ if (target.pathname !== "/") return `HTTP ${result.status} redirecting to ${target.pathname}, which is not the page being verified`;
193
+ if (target.search !== "" || target.hash !== "") return `HTTP ${result.status} redirecting to ${target.href}, which is not the page being verified`;
188
194
  return null;
189
195
  }
190
196
  if (result.status !== 200) return `HTTP ${result.status}`;
@@ -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.2 --resolve-tag
18
+ scripts/install-global.sh --version v1.1.4 --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.2 release matrix)' "$os_name" "$arch_name"
130
+ printf '%s/%s (outside v1.1.4 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.2 release matrix"*)
138
+ *"supported target"*|*"experimental"*|*"not release-gated"*|*"outside v1.1.4 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
  ;;
@@ -344,11 +344,15 @@ if [[ "$WITH_MCP" == false && "$MCP_PRESET_EXPLICIT" == true ]]; then
344
344
  exit 2
345
345
  fi
346
346
 
347
+ require_node_version
348
+
347
349
  # The preset is only read at the very last step, after the platform package and
348
350
  # the MCP adapter are already installed. A name with a typo therefore failed
349
351
  # after two installs had landed, leaving a half-configured machine, and the
350
352
  # dry-run still exited 0 because it never reached the check. Validating here
351
- # costs one process and makes a bad preset cost nothing.
353
+ # costs one process and makes a bad preset cost nothing. It has to come after the
354
+ # Node check, not before: the validator is a Node script, so on a machine without
355
+ # Node it reported a missing interpreter as a bad preset.
352
356
  if [[ "$WITH_MCP" == true ]]; then
353
357
  if ! bash "$PLATFORM_ROOT/scripts/configure-mcp.sh" --scope global --preset "$MCP_PRESET" --validate-preset >/dev/null; then
354
358
  echo "Nothing was installed." >&2
@@ -356,7 +360,6 @@ if [[ "$WITH_MCP" == true ]]; then
356
360
  fi
357
361
  fi
358
362
 
359
- require_node_version
360
363
  CURRENT_RUNTIME_SURFACE="$(detect_runtime_surface)"
361
364
  warn_runtime_surface_if_needed "$CURRENT_RUNTIME_SURFACE"
362
365
  CURRENT_RELEASE_TAG="$(node -e 'const fs = require("node:fs"); const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(`v${p.version}`);' "$PLATFORM_ROOT/package.json")"
@@ -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.2 support matrix."
140
+ note: "This runtime is outside the documented v1.1.4 support matrix."
141
141
  };
142
142
  }
143
143