@piagent/platform 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +12 -2
  3. package/README.vi.md +47 -0
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +7 -7
  6. package/docs/en/architecture.md +13 -0
  7. package/docs/languages.json +20 -4
  8. package/docs/mcp-and-tools.md +112 -1
  9. package/docs/operator-manual-vietnamese.md +7 -7
  10. package/docs/publishing-for-teams.md +42 -0
  11. package/docs/release-install-policy.md +21 -8
  12. package/docs/runtime-harness-standard.md +1 -1
  13. package/docs/security-threat-model.md +1 -1
  14. package/docs/task-implementation-contract.md +11 -4
  15. package/docs/team-onboarding.md +2 -2
  16. package/docs/usage-observability.md +70 -0
  17. package/docs/vi/architecture.md +13 -0
  18. package/docs-site/architecture.html +5 -1
  19. package/docs-site/benchmark.html +5 -1
  20. package/docs-site/commands.html +5 -1
  21. package/docs-site/content/en/index.html +49 -0
  22. package/docs-site/content/en/runtime.html +13 -0
  23. package/docs-site/content/index.html +49 -0
  24. package/docs-site/content/runtime.html +28 -0
  25. package/docs-site/context-engine.html +5 -1
  26. package/docs-site/en/architecture.html +5 -1
  27. package/docs-site/en/benchmark.html +5 -1
  28. package/docs-site/en/commands.html +5 -1
  29. package/docs-site/en/context-engine.html +5 -1
  30. package/docs-site/en/index.html +54 -1
  31. package/docs-site/en/mcp-approval.html +5 -1
  32. package/docs-site/en/mcp-auth.html +5 -1
  33. package/docs-site/en/mcp-servers.html +5 -1
  34. package/docs-site/en/mcp.html +5 -1
  35. package/docs-site/en/permissions.html +5 -1
  36. package/docs-site/en/profiles.html +5 -1
  37. package/docs-site/en/quickstart.html +5 -1
  38. package/docs-site/en/runtime.html +18 -1
  39. package/docs-site/en/scope.html +5 -1
  40. package/docs-site/en/security.html +5 -1
  41. package/docs-site/en/team.html +5 -1
  42. package/docs-site/en/whats-new.html +5 -1
  43. package/docs-site/en/workflows.html +5 -1
  44. package/docs-site/index.html +54 -1
  45. package/docs-site/mcp-approval.html +5 -1
  46. package/docs-site/mcp-auth.html +5 -1
  47. package/docs-site/mcp-servers.html +5 -1
  48. package/docs-site/mcp.html +5 -1
  49. package/docs-site/permissions.html +5 -1
  50. package/docs-site/profiles.html +5 -1
  51. package/docs-site/quickstart.html +5 -1
  52. package/docs-site/runtime.html +33 -1
  53. package/docs-site/scope.html +5 -1
  54. package/docs-site/security.html +5 -1
  55. package/docs-site/team.html +5 -1
  56. package/docs-site/whats-new.html +5 -1
  57. package/docs-site/workflows.html +5 -1
  58. package/evals/fixtures/piagent-webui/document-workspace-v1.invalid.json +17 -0
  59. package/evals/fixtures/piagent-webui/document-workspace-v1.valid.json +42 -0
  60. package/evals/fixtures/piagent-webui/session-command-v1.valid.json +2 -1
  61. package/package.json +14 -2
  62. package/packages/piagent-core/README.md +3 -2
  63. package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
  64. package/packages/piagent-core/capabilities/runtime-integrity.js +51 -4
  65. package/packages/piagent-core/extensions/document-intake.ts +6 -3
  66. package/packages/piagent-core/extensions/piagent-guard.ts +20 -237
  67. package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
  68. package/packages/piagent-core/extensions/shell-reach.ts +250 -0
  69. package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
  70. package/packages/piagent-core/mcp/mcp-server-catalog.js +135 -5
  71. package/packages/piagent-core/package.json +1 -1
  72. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +9 -2
  73. package/packages/piagent-core/runtime/input/attachment-document-command.ts +39 -0
  74. package/packages/piagent-core/runtime/input/attachment-reservation.ts +36 -0
  75. package/packages/{piagent-webui/extension → piagent-core/runtime/input}/attachment-store.ts +221 -44
  76. package/packages/piagent-core/runtime/inspection/document-workspace-projection.ts +276 -0
  77. package/packages/piagent-core/runtime/inspection/project-runtime-inspection.ts +20 -5
  78. package/packages/piagent-core/runtime/inspection/session-identity.ts +20 -0
  79. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +25 -2
  80. package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
  81. package/packages/piagent-core/runtime/registration/task-start-tool.ts +9 -13
  82. package/packages/piagent-core/runtime/registration/workflow-commands.ts +5 -4
  83. package/packages/piagent-core/runtime/session/token-economics.js +34 -0
  84. package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
  85. package/packages/piagent-core/runtime/workflows/input-routing.ts +13 -2
  86. package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
  87. package/packages/piagent-core/security/sensitive-data.js +7 -0
  88. package/packages/piagent-webui/dist/client/assets/MarkdownMessage-Uouhx1QO.js +29 -0
  89. package/packages/piagent-webui/dist/client/assets/index-BV-_03yl.css +1 -0
  90. package/packages/piagent-webui/dist/client/assets/index-MBZq8XLV.js +174 -0
  91. package/packages/piagent-webui/dist/client/index.html +2 -2
  92. package/packages/piagent-webui/extension/lifecycle-controller.ts +15 -0
  93. package/packages/piagent-webui/extension/piagent-webui.ts +19 -6
  94. package/packages/piagent-webui/extension/same-session-bridge.ts +12 -12
  95. package/packages/piagent-webui/gateway/ai-capability-inspection.ts +53 -0
  96. package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
  97. package/packages/piagent-webui/gateway/gateway-service.ts +23 -3
  98. package/packages/piagent-webui/gateway/native-project-picker.ts +63 -7
  99. package/packages/piagent-webui/gateway/session-attachment-registry.ts +165 -0
  100. package/packages/piagent-webui/gateway/session-catalog.ts +62 -9
  101. package/packages/piagent-webui/gateway/session-command-controller.ts +47 -14
  102. package/packages/piagent-webui/gateway/session-inspection-registry.ts +67 -12
  103. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +24 -11
  104. package/packages/piagent-webui/package.json +3 -1
  105. package/packages/piagent-webui/server/core-inspection-provider.ts +52 -15
  106. package/packages/piagent-webui/server/ipc-read-model-client.ts +2 -0
  107. package/packages/piagent-webui/server/loopback-server.ts +63 -25
  108. package/packages/piagent-webui/server/read-model-provider.ts +2 -0
  109. package/packages/piagent-webui/server/read-only-router.ts +6 -0
  110. package/packages/piagent-webui/server/transcript-projection.ts +58 -2
  111. package/schemas/piagent-webui/attachment-v1.schema.json +24 -4
  112. package/schemas/piagent-webui/capabilities-v1.schema.json +2 -2
  113. package/schemas/piagent-webui/catalog-v1.json +9 -0
  114. package/schemas/piagent-webui/document-workspace-v1.schema.json +155 -0
  115. package/schemas/piagent-webui/session-command-v1.schema.json +14 -3
  116. package/schemas/piagent-webui/transcript-v1.schema.json +27 -0
  117. package/scripts/build-docs-site.mjs +21 -1
  118. package/scripts/explain-command.mjs +240 -0
  119. package/scripts/install-global.sh +104 -3
  120. package/scripts/mcp-manage.mjs +17 -4
  121. package/scripts/pi-session-stats.sh +10 -2
  122. package/scripts/piagent-cli.mjs +14 -1
  123. package/scripts/piagent-dashboard.mjs +55 -2
  124. package/scripts/setup.sh +69 -33
  125. package/scripts/traffic-snapshot.mjs +214 -0
  126. package/scripts/typescript-loader.mjs +20 -4
  127. package/scripts/uninstall-global.sh +53 -2
  128. package/scripts/update-global.mjs +33 -1
  129. package/scripts/verify-local.sh +20 -1
  130. package/scripts/verify-translation-pairing.mjs +121 -0
  131. package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +0 -172
  132. package/packages/piagent-webui/dist/client/assets/index-DwKCWbwd.css +0 -1
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.4.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.5.1 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/runtime" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/runtime" hreflang="en" lang="en">EN</a>
@@ -154,6 +154,34 @@ piagent-auto --full-access -p "Run the trusted local benchmark suite."</code></p
154
154
  <header><span>Capabilities</span><button class="clip-btn" data-clip="piagent-capabilities catalog --check&#10;piagent-capabilities doctor --profile .pi/piagent-profile.json">Lấy lệnh</button></header>
155
155
  <pre><code>piagent-capabilities catalog --check
156
156
  piagent-capabilities doctor --profile .pi/piagent-profile.json</code></pre>
157
+ </article>
158
+ <article class="command-card">
159
+ <header><span>Session Hub</span><button class="clip-btn" data-clip="piagent dashboard&#10;piagent-webui --no-open">Lấy lệnh</button></header>
160
+ <pre><code>piagent dashboard
161
+ piagent-webui --no-open</code></pre>
162
+ <p class="command-note">Bí danh: <code>piagent-dashboard</code>, <code>piagent-webui</code>, <code>piagent-explain</code> cũng chạy được như binary riêng.</p>
163
+ </article>
164
+ <article class="command-card">
165
+ <header><span>Giải thích quyết định</span><button class="clip-btn" data-clip="piagent explain &#39;cat .env*&#39;&#10;piagent explain --scope">Lấy lệnh</button></header>
166
+ <pre><code>piagent explain 'cat .env*'
167
+ piagent explain --scope</code></pre>
168
+ </article>
169
+ <article class="command-card">
170
+ <header><span>Usage và token</span><button class="clip-btn" data-clip="piagent-usage .&#10;piagent-usage --history . --days 7">Lấy lệnh</button></header>
171
+ <pre><code>piagent-usage .
172
+ piagent-usage --history . --days 7</code></pre>
173
+ </article>
174
+ <article class="command-card">
175
+ <header><span>Model và routing</span><button class="clip-btn" data-clip="piagent-models&#10;piagent-model-scope&#10;piagent-route">Lấy lệnh</button></header>
176
+ <pre><code>piagent-models
177
+ piagent-model-scope
178
+ piagent-route</code></pre>
179
+ </article>
180
+ <article class="command-card">
181
+ <header><span>Subagents và migration</span><button class="clip-btn" data-clip="piagent-subagents --preset safe&#10;piagent-migrate . --apply&#10;piagent-import-instructions">Lấy lệnh</button></header>
182
+ <pre><code>piagent-subagents --preset safe
183
+ piagent-migrate . --apply
184
+ piagent-import-instructions</code></pre>
157
185
  </article>
158
186
  <article class="command-card">
159
187
  <header><span>Verification</span><button class="clip-btn" data-clip="npm run verify&#10;npm test&#10;npm run typecheck">Lấy lệnh</button></header>
@@ -191,5 +219,9 @@ npm run typecheck</code></pre>
191
219
  <nav data-toc></nav>
192
220
  </aside>
193
221
  </div>
222
+ <script>
223
+ window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
224
+ </script>
225
+ <script defer src="/_vercel/insights/script.js"></script>
194
226
  </body>
195
227
  </html>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.4.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.5.1 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/scope" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/scope" hreflang="en" lang="en">EN</a>
@@ -201,5 +201,9 @@
201
201
  <nav data-toc></nav>
202
202
  </aside>
203
203
  </div>
204
+ <script>
205
+ window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
206
+ </script>
207
+ <script defer src="/_vercel/insights/script.js"></script>
204
208
  </body>
205
209
  </html>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.4.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.5.1 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/security" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/security" hreflang="en" lang="en">EN</a>
@@ -182,5 +182,9 @@
182
182
  <nav data-toc></nav>
183
183
  </aside>
184
184
  </div>
185
+ <script>
186
+ window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
187
+ </script>
188
+ <script defer src="/_vercel/insights/script.js"></script>
185
189
  </body>
186
190
  </html>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.4.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.5.1 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/team" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/team" hreflang="en" lang="en">EN</a>
@@ -149,5 +149,9 @@
149
149
  <nav data-toc></nav>
150
150
  </aside>
151
151
  </div>
152
+ <script>
153
+ window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
154
+ </script>
155
+ <script defer src="/_vercel/insights/script.js"></script>
152
156
  </body>
153
157
  </html>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.4.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.5.1 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/whats-new" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/whats-new" hreflang="en" lang="en">EN</a>
@@ -237,5 +237,9 @@
237
237
  <nav data-toc></nav>
238
238
  </aside>
239
239
  </div>
240
+ <script>
241
+ window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
242
+ </script>
243
+ <script defer src="/_vercel/insights/script.js"></script>
240
244
  </body>
241
245
  </html>
@@ -30,7 +30,7 @@
30
30
  <span>Pi Agent Platform</span>
31
31
  </a>
32
32
  <div class="top-actions">
33
- <span class="pill"><span class="pill-dot"></span>v1.4.0 docs · VI</span>
33
+ <span class="pill"><span class="pill-dot"></span>v1.5.1 docs · VI</span>
34
34
  <nav class="language-switch" aria-label="Ngôn ngữ">
35
35
  <a class="language-option current" href="/workflows" hreflang="vi" lang="vi" aria-current="true">VI</a>
36
36
  <a class="language-option" href="/en/workflows" hreflang="en" lang="en">EN</a>
@@ -237,5 +237,9 @@
237
237
  <nav data-toc></nav>
238
238
  </aside>
239
239
  </div>
240
+ <script>
241
+ window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
242
+ </script>
243
+ <script defer src="/_vercel/insights/script.js"></script>
240
244
  </body>
241
245
  </html>
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "piagent-webui-document-workspace-v1",
4
+ "messageType": "document",
5
+ "generatedAt": "2026-08-17T09:00:00.000Z",
6
+ "documentRef": "document_01",
7
+ "state": "unavailable",
8
+ "name": null,
9
+ "relativePath": null,
10
+ "rootRef": null,
11
+ "format": null,
12
+ "text": "# Ke hoach quy ba\n\nContent that must never accompany a refusal.\n",
13
+ "sizeBytes": null,
14
+ "truncated": false,
15
+ "redacted": false,
16
+ "reasonCode": "document-protected"
17
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "piagent-webui-document-workspace-v1",
4
+ "messageType": "listing",
5
+ "generatedAt": "2026-08-17T09:00:00.000Z",
6
+ "state": "ready",
7
+ "roots": [
8
+ { "rootRef": "document-root_01", "path": "/workspace/project", "source": "project", "documentCount": 2 },
9
+ { "rootRef": "document-root_02", "path": "/home/operator/Downloads", "source": "profile", "documentCount": 1 }
10
+ ],
11
+ "documents": [
12
+ {
13
+ "documentRef": "document_01",
14
+ "rootRef": "document-root_01",
15
+ "name": "ke-hoach.md",
16
+ "relativePath": "tai-lieu/ke-hoach.md",
17
+ "extension": "md",
18
+ "sizeBytes": 4096,
19
+ "modifiedAt": "2026-08-17T08:30:00.000Z"
20
+ },
21
+ {
22
+ "documentRef": "document_02",
23
+ "rootRef": "document-root_01",
24
+ "name": "bao-cao.docx",
25
+ "relativePath": "tai-lieu/bao-cao.docx",
26
+ "extension": "docx",
27
+ "sizeBytes": 245760,
28
+ "modifiedAt": "2026-08-17T08:45:00.000Z"
29
+ },
30
+ {
31
+ "documentRef": "document_03",
32
+ "rootRef": "document-root_02",
33
+ "name": "vendor-spec.pdf",
34
+ "relativePath": "vendor-spec.pdf",
35
+ "extension": "pdf",
36
+ "sizeBytes": 1048576,
37
+ "modifiedAt": "2026-08-16T17:10:00.000Z"
38
+ }
39
+ ],
40
+ "truncated": false,
41
+ "reasonCode": null
42
+ }
@@ -14,6 +14,7 @@
14
14
  "delivery": "new-operation",
15
15
  "message": "Continue the gateway implementation.",
16
16
  "messageRequestId": "message_request_01",
17
- "expectedOperationRef": null
17
+ "expectedOperationRef": null,
18
+ "attachmentRefs": ["attachment_01"]
18
19
  }
19
20
  }
package/package.json CHANGED
@@ -1,19 +1,30 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": true
8
8
  },
9
9
  "type": "module",
10
- "description": "Reusable Pi agent platform, project adapters, MCP/tool policy, and coding workflow harness.",
10
+ "description": "Guardrails for AI coding agents: blocks secret reads, destructive commands, and unapproved MCP servers before the tool call runs. Ships project profiles, capability locks, a context engine, and task verification.",
11
11
  "homepage": "https://piagent.io.vn",
12
12
  "repository": {
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/Vt-mmm/piagent.git"
15
15
  },
16
16
  "keywords": [
17
+ "ai-agent",
18
+ "coding-agent",
19
+ "agent-guardrails",
20
+ "agent-security",
21
+ "llm-tool-policy",
22
+ "prompt-injection",
23
+ "mcp",
24
+ "mcp-security",
25
+ "secret-redaction",
26
+ "context-engineering",
27
+ "developer-tools",
17
28
  "pi-package",
18
29
  "pi-extension",
19
30
  "pi-skill",
@@ -75,6 +86,7 @@
75
86
  "piagent-import-instructions": "scripts/piagent-cli.mjs",
76
87
  "piagent-auto": "scripts/piagent-cli.mjs",
77
88
  "piagent-context": "scripts/piagent-cli.mjs",
89
+ "piagent-explain": "scripts/piagent-cli.mjs",
78
90
  "piagent-webui": "scripts/piagent-cli.mjs"
79
91
  },
80
92
  "pi": {
@@ -179,8 +179,9 @@ Project-local state belongs in `.pi/.gitignore`.
179
179
  Task files use schema v2 and bind one unique `taskRunId` to one Pi `sessionId`.
180
180
  They record attempt/max-attempt history, dependency-safe work-plan progress,
181
181
  Git baseline/final digests, observed changes, all exact verify results and final
182
- trace. A terminal task is immutable; retry starts in a fresh session and carries
183
- the prior failure/ruled-out evidence. Legacy v1 state is migrated and archived
182
+ trace. A terminal task is immutable; the same conversation may bind a new task
183
+ or retry attempt after the prior task closes, while keeping only one task pending
184
+ at a time and carrying prior failure/ruled-out evidence. Legacy v1 state is migrated and archived
184
185
  without requiring project onboarding again.
185
186
 
186
187
  Local state directories are owner-only. Task state, context telemetry, trace,
@@ -148,12 +148,21 @@ function lexicalAncestors(target) {
148
148
  return ancestors;
149
149
  }
150
150
 
151
- function metadataSignature(targets) {
151
+ // The directories above every target, sorted, derived once per snapshot. The
152
+ // set is a pure function of the target list, and a cached entry's target list
153
+ // cannot change while the entry lives, so deriving it per call was repeating
154
+ // work that could not produce a different answer -- and at 296 pinned files it
155
+ // was the single most expensive part of a cache hit, more than every `lstat`
156
+ // this function performs put together.
157
+ function ancestorPaths(targets) {
158
+ return [...new Set(targets.flatMap((target) => lexicalAncestors(target.path)))].sort(compare);
159
+ }
160
+
161
+ function metadataSignature(targets, ancestors) {
152
162
  if (!targets) return undefined;
153
163
  const records = [];
154
- const ancestors = new Set(targets.flatMap((target) => lexicalAncestors(target.path)));
155
164
  try {
156
- for (const ancestor of [...ancestors].sort(compare)) {
165
+ for (const ancestor of ancestors ?? ancestorPaths(targets)) {
157
166
  const stat = fs.lstatSync(ancestor, { bigint: true });
158
167
  const symbolic = stat.isSymbolicLink();
159
168
  const identity = symbolic ? statIdentity(stat, true) : ancestorIdentity(stat);
@@ -194,13 +203,14 @@ function cacheIdentity(lockDocument, options = {}) {
194
203
  }
195
204
 
196
205
  function snapshotTargets(targets) {
197
- const signature = metadataSignature(targets);
198
- return signature ? { signature, targets } : undefined;
206
+ const ancestors = ancestorPaths(targets);
207
+ const signature = metadataSignature(targets, ancestors);
208
+ return signature ? { signature, targets, ancestors } : undefined;
199
209
  }
200
210
 
201
211
  function stableSnapshot(snapshot) {
202
212
  if (!snapshot) return false;
203
- const current = metadataSignature(snapshot.targets);
213
+ const current = metadataSignature(snapshot.targets, snapshot.ancestors);
204
214
  return Boolean(current && current === snapshot.signature);
205
215
  }
206
216
 
@@ -209,7 +219,7 @@ export function verifyCapabilityLockCached(cache, root, profilePath, lockPath, l
209
219
  const identity = cacheIdentity(lockDocument, options);
210
220
  const cached = options.forceFull === true ? undefined : cache?.get(key);
211
221
  if (cached?.identity === identity) {
212
- const signature = metadataSignature(cached.targets);
222
+ const signature = metadataSignature(cached.targets, cached.ancestors);
213
223
  if (signature && signature === cached.signature) return { ...cached.verification, cacheStatus: "hit" };
214
224
  }
215
225
  cache?.delete(key);
@@ -164,6 +164,11 @@ const EXECUTABLE_SOURCE = /\.(?:[cm]?js|ts|json|sh)$/;
164
164
  const IMPORT_SPECIFIER = /(?:\bimport\s*(?:type\s+)?(?:[^"'`]*?\s+from\s*)?|\bexport\s+(?:type\s+)?[^"'`]*?\s+from\s*)["']([^"']+)["']/g;
165
165
  const DYNAMIC_IMPORT = /\bimport\(\s*["']([^"']+)["']\s*\)/g;
166
166
  const RUNTIME_LITERAL = /["']((?:packages|scripts)\/[A-Za-z0-9_./-]+\.(?:[cm]?js|ts|json|sh))["']/g;
167
+ // A shell entrypoint reaches a sibling program through the directory it computed
168
+ // for itself -- `"$SCRIPT_DIR/pi-usage-history.mjs"` -- so the repository-relative
169
+ // literal the scanner above looks for never appears in the source. `piagent-usage`
170
+ // is a pinned command that ran a 567-line unpinned program this way.
171
+ const SHELL_SIBLING = /\$(?:\{)?(?:SCRIPT_DIR|ROOT|REPO_ROOT|PLATFORM_ROOT)(?:\})?\/([A-Za-z0-9_./-]+\.(?:[cm]?js|ts|json|sh))/g;
167
172
  const MAX_INTEGRITY_FILES = 5_000;
168
173
 
169
174
  function normalizedRelative(root, absolute) {
@@ -172,7 +177,16 @@ function normalizedRelative(root, absolute) {
172
177
  return relative.split(path.sep).join("/");
173
178
  }
174
179
 
175
- function executableFilesBelow(root, relativeDirectory) {
180
+ // A served bundle is more than its scripts: the document names which script runs,
181
+ // so pinning the JavaScript while leaving the HTML free would leave the choice of
182
+ // program unpinned.
183
+ const SERVED_BUNDLE_ASSET = /\.(?:[cm]?js|css|html|json|svg|woff2)$/;
184
+
185
+ function bundleFilesBelow(root, relativeDirectory) {
186
+ return executableFilesBelow(root, relativeDirectory, SERVED_BUNDLE_ASSET);
187
+ }
188
+
189
+ function executableFilesBelow(root, relativeDirectory, accept = EXECUTABLE_SOURCE) {
176
190
  const directory = path.join(root, relativeDirectory);
177
191
  if (!fs.existsSync(directory)) return [];
178
192
  const files = [];
@@ -182,7 +196,7 @@ function executableFilesBelow(root, relativeDirectory) {
182
196
  const target = path.join(absolute, entry.name), stat = fs.lstatSync(target);
183
197
  if (stat.isSymbolicLink()) throw new Error(`runtime-integrity-symlink:${normalizedRelative(root, target) ?? "outside-root"}`);
184
198
  if (stat.isDirectory()) visit(target);
185
- else if (stat.isFile() && EXECUTABLE_SOURCE.test(entry.name)) files.push(normalizedRelative(root, target));
199
+ else if (stat.isFile() && accept.test(entry.name)) files.push(normalizedRelative(root, target));
186
200
  }
187
201
  };
188
202
  visit(directory);
@@ -192,7 +206,13 @@ function executableFilesBelow(root, relativeDirectory) {
192
206
  function localImportTarget(root, importer, specifier) {
193
207
  if (!specifier.startsWith(".")) return undefined;
194
208
  const base = path.resolve(path.dirname(path.join(root, importer)), specifier);
195
- const candidates = [base, `${base}.ts`, `${base}.js`, `${base}.mjs`, path.join(base, "index.ts"), path.join(base, "index.js")];
209
+ // TypeScript sources import a sibling module by its emitted name, so `./x.js`
210
+ // is how a `.ts` file refers to `x.ts`. Without that rewrite the specifier
211
+ // resolves to nothing and the reference is dropped in silence -- a module
212
+ // that decides what the guard enforces would then sit outside the lock while
213
+ // this list still claimed to cover the whole import graph.
214
+ const rewritten = base.endsWith(".js") ? [`${base.slice(0, -3)}.ts`] : [];
215
+ const candidates = [base, ...rewritten, `${base}.ts`, `${base}.js`, `${base}.mjs`, path.join(base, "index.ts"), path.join(base, "index.js")];
196
216
  for (const candidate of candidates) {
197
217
  const relative = normalizedRelative(root, candidate);
198
218
  if (!relative || !fs.existsSync(candidate)) continue;
@@ -204,8 +224,24 @@ function localImportTarget(root, importer, specifier) {
204
224
  }
205
225
 
206
226
  function sourceReferences(root, relativeFile) {
207
- if (!/\.(?:[cm]?js|ts)$/.test(relativeFile)) return [];
227
+ // Shell entrypoints are scanned too. They were skipped here, so a pinned
228
+ // command could execute an unpinned program and the lock reported a complete
229
+ // graph while missing it.
230
+ const shell = relativeFile.endsWith(".sh");
231
+ if (!shell && !/\.(?:[cm]?js|ts)$/.test(relativeFile)) return [];
208
232
  const source = fs.readFileSync(path.join(root, relativeFile), "utf8"), references = new Set();
233
+ if (shell) {
234
+ const directory = path.posix.dirname(relativeFile);
235
+ for (const pattern of [RUNTIME_LITERAL, SHELL_SIBLING]) {
236
+ pattern.lastIndex = 0;
237
+ for (let match = pattern.exec(source); match; match = pattern.exec(source)) {
238
+ const candidate = pattern === SHELL_SIBLING ? path.posix.join(directory, match[1]) : match[1];
239
+ const absolute = path.join(root, candidate), relative = normalizedRelative(root, absolute);
240
+ if (relative && fs.existsSync(absolute) && fs.lstatSync(absolute).isFile()) references.add(relative);
241
+ }
242
+ }
243
+ return [...references];
244
+ }
209
245
  for (const pattern of [IMPORT_SPECIFIER, DYNAMIC_IMPORT]) {
210
246
  pattern.lastIndex = 0;
211
247
  for (let match = pattern.exec(source); match; match = pattern.exec(source)) {
@@ -231,6 +267,17 @@ export function discoverRuntimeIntegrityFiles(root) {
231
267
  ...CORE_RUNTIME_INTEGRITY_FILES,
232
268
  "packages/piagent-core/extensions/piagent-guard.ts",
233
269
  ...executableFilesBelow(root, "packages/piagent-core/runtime"),
270
+ // The browser bundle the WebUI serves. The published package ships the built
271
+ // assets and not the sources they came from, so on an installed platform
272
+ // this code cannot be regenerated or cross-checked against anything -- and it
273
+ // runs inside the origin that holds the session cookie and the CSRF token,
274
+ // with the authority to drive session commands, source mutation and provider
275
+ // auth. The server that serves it was pinned; what it serves was not.
276
+ //
277
+ // The tree is absent in a source checkout until the workspace is built, and
278
+ // absence is not a failure here: a lock recorded with these entries still
279
+ // fails verification if they later disappear.
280
+ ...bundleFilesBelow(root, "packages/piagent-webui/dist/client"),
234
281
  ...declaredExtensions,
235
282
  ...declaredBins,
236
283
  ...declaredEntrypoints
@@ -51,7 +51,7 @@ export type DocumentExtraction =
51
51
  | { status: "ok"; text: string; truncated: boolean; kind: "text" | "docx" | "pdf" }
52
52
  | { status: "error"; reason: string };
53
53
 
54
- type DocumentCommandResult = { status: number | null; stdout: string; stderr: string; error?: Error };
54
+ export type DocumentCommandResult = { status: number | null; stdout: string; stderr: string; error?: Error };
55
55
 
56
56
  export function expandHome(candidate: string, home: string | undefined): string {
57
57
  if (candidate === "~") return home ?? candidate;
@@ -450,7 +450,10 @@ export function extractPdfFromDescriptor(
450
450
  // stdout out. The converter is handed the already-open descriptor rather than
451
451
  // a path, so it reads the file whose identity was verified and cannot be
452
452
  // pointed somewhere else by a name that changed in the meantime.
453
- const result = run("pdftotext", ["-layout", "-enc", "UTF-8", "-", "-"], fd);
453
+ return extractPdfCommandResult(run("pdftotext", ["-layout", "-enc", "UTF-8", "-", "-"], fd));
454
+ }
455
+
456
+ export function extractPdfCommandResult(result: DocumentCommandResult): DocumentExtraction {
454
457
  if (result.error) {
455
458
  return {
456
459
  status: "error",
@@ -471,7 +474,7 @@ export function extractPdfFromDescriptor(
471
474
 
472
475
  // spawnSync blocks the whole runtime, so an external converter that never
473
476
  // returns would hang the session rather than fail the read.
474
- const PDF_TIMEOUT_MS = 20_000;
477
+ export const PDF_TIMEOUT_MS = 20_000;
475
478
 
476
479
  export function probeExecutableOnPath(executable: string): DocumentCommandResult {
477
480
  const result = spawnSync("/bin/sh", ["-c", 'command -v "$1"', "piagent-command-probe", executable], {