@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
@@ -0,0 +1,155 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://piagent.io.vn/schemas/piagent-webui/document-workspace-v1.schema.json",
4
+ "title": "Piagent WebUI bounded document workspace v1",
5
+ "oneOf": [
6
+ { "$ref": "#/$defs/listing" },
7
+ { "$ref": "#/$defs/document" }
8
+ ],
9
+ "$defs": {
10
+ "displayText": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "maxLength": 1024,
14
+ "pattern": "^[^\\u0000-\\u001F\\u007F]*$"
15
+ },
16
+ "nullableDisplayText": {
17
+ "anyOf": [
18
+ { "$ref": "#/$defs/displayText" },
19
+ { "type": "null" }
20
+ ]
21
+ },
22
+ "root": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "required": ["rootRef", "path", "source", "documentCount"],
26
+ "properties": {
27
+ "rootRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
28
+ "path": { "$ref": "#/$defs/displayText" },
29
+ "source": { "enum": ["project", "profile", "environment"] },
30
+ "documentCount": { "type": "integer", "minimum": 0, "maximum": 2000 }
31
+ }
32
+ },
33
+ "entry": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "required": ["documentRef", "rootRef", "name", "relativePath", "extension", "sizeBytes", "modifiedAt"],
37
+ "properties": {
38
+ "documentRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
39
+ "rootRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
40
+ "name": { "$ref": "#/$defs/displayText" },
41
+ "relativePath": { "$ref": "#/$defs/displayText" },
42
+ "extension": {
43
+ "enum": ["md", "markdown", "txt", "text", "csv", "tsv", "json", "yaml", "yml", "pdf", "docx"]
44
+ },
45
+ "sizeBytes": { "type": "integer", "minimum": 1, "maximum": 12582912 },
46
+ "modifiedAt": { "$ref": "common-v1.schema.json#/$defs/timestamp" }
47
+ }
48
+ },
49
+ "listing": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "required": ["schemaVersion", "version", "messageType", "generatedAt", "state", "roots", "documents", "truncated", "reasonCode"],
53
+ "properties": {
54
+ "schemaVersion": { "const": 1 },
55
+ "version": { "const": "piagent-webui-document-workspace-v1" },
56
+ "messageType": { "const": "listing" },
57
+ "generatedAt": { "$ref": "common-v1.schema.json#/$defs/timestamp" },
58
+ "state": { "enum": ["ready", "unavailable"] },
59
+ "roots": {
60
+ "type": "array",
61
+ "maxItems": 32,
62
+ "uniqueItems": true,
63
+ "items": { "$ref": "#/$defs/root" }
64
+ },
65
+ "documents": {
66
+ "type": "array",
67
+ "maxItems": 2000,
68
+ "items": { "$ref": "#/$defs/entry" }
69
+ },
70
+ "truncated": { "type": "boolean" },
71
+ "reasonCode": { "$ref": "common-v1.schema.json#/$defs/nullableReasonCode" }
72
+ },
73
+ "allOf": [
74
+ {
75
+ "if": { "properties": { "state": { "const": "unavailable" } }, "required": ["state"] },
76
+ "then": {
77
+ "properties": {
78
+ "roots": { "type": "array", "maxItems": 0 },
79
+ "documents": { "type": "array", "maxItems": 0 },
80
+ "reasonCode": { "$ref": "common-v1.schema.json#/$defs/reasonCode" }
81
+ }
82
+ }
83
+ },
84
+ {
85
+ "if": { "properties": { "state": { "const": "ready" } }, "required": ["state"] },
86
+ "then": {
87
+ "properties": {
88
+ "roots": { "type": "array", "minItems": 1 },
89
+ "reasonCode": { "type": "null" }
90
+ }
91
+ }
92
+ }
93
+ ]
94
+ },
95
+ "document": {
96
+ "type": "object",
97
+ "additionalProperties": false,
98
+ "required": ["schemaVersion", "version", "messageType", "generatedAt", "documentRef", "state", "name", "relativePath", "rootRef", "format", "text", "sizeBytes", "truncated", "redacted", "reasonCode"],
99
+ "properties": {
100
+ "schemaVersion": { "const": 1 },
101
+ "version": { "const": "piagent-webui-document-workspace-v1" },
102
+ "messageType": { "const": "document" },
103
+ "generatedAt": { "$ref": "common-v1.schema.json#/$defs/timestamp" },
104
+ "documentRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
105
+ "state": { "enum": ["ready", "unavailable"] },
106
+ "name": { "$ref": "#/$defs/nullableDisplayText" },
107
+ "relativePath": { "$ref": "#/$defs/nullableDisplayText" },
108
+ "rootRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
109
+ "format": { "enum": ["text", "docx", "pdf", null] },
110
+ "text": {
111
+ "anyOf": [
112
+ { "type": "string", "maxLength": 400000 },
113
+ { "type": "null" }
114
+ ]
115
+ },
116
+ "sizeBytes": {
117
+ "anyOf": [
118
+ { "type": "integer", "minimum": 1, "maximum": 12582912 },
119
+ { "type": "null" }
120
+ ]
121
+ },
122
+ "truncated": { "type": "boolean" },
123
+ "redacted": { "type": "boolean" },
124
+ "reasonCode": { "$ref": "common-v1.schema.json#/$defs/nullableReasonCode" }
125
+ },
126
+ "allOf": [
127
+ {
128
+ "if": { "properties": { "state": { "const": "ready" } }, "required": ["state"] },
129
+ "then": {
130
+ "properties": {
131
+ "name": { "$ref": "#/$defs/displayText" },
132
+ "relativePath": { "$ref": "#/$defs/displayText" },
133
+ "rootRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
134
+ "format": { "enum": ["text", "docx", "pdf"] },
135
+ "text": { "type": "string", "maxLength": 400000 },
136
+ "reasonCode": { "type": "null" }
137
+ }
138
+ }
139
+ },
140
+ {
141
+ "if": { "properties": { "state": { "const": "unavailable" } }, "required": ["state"] },
142
+ "then": {
143
+ "properties": {
144
+ "text": { "type": "null" },
145
+ "format": { "type": "null" },
146
+ "truncated": { "const": false },
147
+ "redacted": { "const": false },
148
+ "reasonCode": { "$ref": "common-v1.schema.json#/$defs/reasonCode" }
149
+ }
150
+ }
151
+ }
152
+ ]
153
+ }
154
+ }
155
+ }
@@ -56,7 +56,11 @@
56
56
  "modelRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
57
57
  "thinkingLevel": { "$ref": "#/$defs/thinkingLevel" },
58
58
  "message": { "type": "string", "minLength": 1, "maxLength": 32768, "pattern": "^[^\\u0000]*$" },
59
- "messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" }
59
+ "messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
60
+ "deferInitialMessage": {
61
+ "description": "Create the durable session without dispatching the first turn so the client can stage selected attachments against the new session before sending it.",
62
+ "type": "boolean"
63
+ }
60
64
  }
61
65
  }
62
66
  }
@@ -75,12 +79,19 @@
75
79
  "payload": {
76
80
  "type": "object",
77
81
  "additionalProperties": false,
78
- "required": ["delivery", "message", "messageRequestId", "expectedOperationRef"],
82
+ "required": ["delivery", "message", "messageRequestId", "expectedOperationRef", "attachmentRefs"],
79
83
  "properties": {
80
84
  "delivery": { "enum": ["new-operation", "follow-up", "steer"] },
81
85
  "message": { "type": "string", "minLength": 1, "maxLength": 32768, "pattern": "^[^\\u0000]*$" },
82
86
  "messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
83
- "expectedOperationRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" }
87
+ "expectedOperationRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
88
+ "attachmentRefs": {
89
+ "description": "Refs staged for this exact messageRequestId. Claimed once at dispatch and consumed.",
90
+ "type": "array",
91
+ "maxItems": 4,
92
+ "uniqueItems": true,
93
+ "items": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" }
94
+ }
84
95
  },
85
96
  "allOf": [
86
97
  {
@@ -188,6 +188,12 @@
188
188
  "agentOperationId": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
189
189
  "turnIndex": { "$ref": "#/$defs/nullableCount" },
190
190
  "content": { "$ref": "#/$defs/content" },
191
+ "attachments": {
192
+ "type": "array",
193
+ "maxItems": 4,
194
+ "uniqueItems": true,
195
+ "items": { "$ref": "#/$defs/attachmentSummary" }
196
+ },
191
197
  "toolCalls": {
192
198
  "type": "array",
193
199
  "maxItems": 64,
@@ -217,6 +223,27 @@
217
223
  }
218
224
  ]
219
225
  },
226
+ "attachmentSummary": {
227
+ "type": "object",
228
+ "additionalProperties": false,
229
+ "required": ["displayName", "kind", "mimeType", "truncated"],
230
+ "properties": {
231
+ "displayName": {
232
+ "type": "string",
233
+ "minLength": 1,
234
+ "maxLength": 160,
235
+ "pattern": "^[^\\u0000-\\u001F\\u007F/\\\\]*$"
236
+ },
237
+ "kind": { "enum": ["file", "image", "document"] },
238
+ "mimeType": {
239
+ "type": "string",
240
+ "minLength": 1,
241
+ "maxLength": 120,
242
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9.+-]*/[A-Za-z0-9][A-Za-z0-9.+-]*$"
243
+ },
244
+ "truncated": { "type": "boolean" }
245
+ }
246
+ },
220
247
  "page": {
221
248
  "type": "object",
222
249
  "additionalProperties": false,
@@ -24,6 +24,26 @@ const SITE = {
24
24
  facebook: "https://www.facebook.com/vinhtam0544/"
25
25
  };
26
26
 
27
+ // Vercel Web Analytics, added here rather than to a page so all locales and
28
+ // every generated page carry the same one. It is cookieless and stores no
29
+ // identifier in the browser, so the site needs no consent banner -- which is
30
+ // the reason to prefer it over an analytics product that would.
31
+ //
32
+ // The route only exists once Web Analytics is enabled for the Vercel project
33
+ // (dashboard -> Analytics -> Enable) and the site is redeployed. Until then the
34
+ // request 404s and the page is otherwise unaffected: the inline shim below
35
+ // queues calls instead of throwing on an undefined `window.va`.
36
+ //
37
+ // Enabling also provisions a second, project-unique route that ad blockers do
38
+ // not recognise. Swapping this src for that path collects more of the traffic;
39
+ // this stable one is used because it does not embed a per-project secret in a
40
+ // public repository.
41
+ const ANALYTICS_SNIPPET = ` <script>
42
+ window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
43
+ </script>
44
+ <script defer src="/_vercel/insights/script.js"></script>
45
+ `;
46
+
27
47
  // Order here is the order in the sidebar, and it is also the order of the
28
48
  // previous/next links at the foot of each page.
29
49
  const NAV = [
@@ -527,7 +547,7 @@ ${renderPager(index, pages, locale)}
527
547
  <nav data-toc></nav>
528
548
  </aside>
529
549
  </div>
530
- </body>
550
+ ${ANALYTICS_SNIPPET} </body>
531
551
  </html>
532
552
  `;
533
553
  }
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+ // Answers the one question the guard never answers: why.
3
+ //
4
+ // A blocked command is the only interface most operators ever have with this
5
+ // policy, and a denial that says "no" without saying "what now" leaves them
6
+ // guessing. This runs the guard's static shell preflight, in the same order,
7
+ // over the same resolved protected paths, and prints what each step saw.
8
+ //
9
+ // It reuses `shell-reach.js` rather than re-deriving anything. A second opinion
10
+ // about what a command reaches would be worse than no opinion: the operator
11
+ // would tune a command until this said allow and still be blocked at run time.
12
+ // The final decision also depends on live session/task facts this standalone
13
+ // process does not own, so passing static preflight is deliberately reported as
14
+ // indeterminate rather than as permission to execute.
15
+
16
+ import path from "node:path";
17
+
18
+ import { evaluateExecPolicyCore, findProtectedPathInCommand, unresolvedPathExpansions } from "../packages/piagent-core/extensions/policy-core.js";
19
+ import {
20
+ findResolvedProtectedPathInCommand,
21
+ shellGlobTargetsProtectedPath,
22
+ unresolvedExpansionReason
23
+ // `.ts` rather than the `.js` specifier the TypeScript sources use: this file is
24
+ // plain ESM, so the loader resolves what is actually on disk.
25
+ } from "../packages/piagent-core/extensions/shell-reach.ts";
26
+ import { effectiveProtectedPaths, loadProjectContextIndexPolicy } from "../packages/piagent-core/extensions/context-index-policy.js";
27
+
28
+ const platformRoot = path.resolve(import.meta.dirname, "..");
29
+
30
+ function usage() {
31
+ return [
32
+ "Usage: piagent explain <command> [--project <path>] [--json]",
33
+ " piagent explain --scope [--project <path>] [--json]",
34
+ "",
35
+ "Prints a fail-closed static preflight for a shell command and what to do",
36
+ "next. A static pass is indeterminate until the live Pi runtime evaluates",
37
+ "Task Contract, scope, permission, lifecycle, approval, and budget state.",
38
+ "Nothing is executed.",
39
+ "",
40
+ " --scope List the boundary in effect instead of judging a command",
41
+ " --project <path> Evaluate against this project's profile (default: cwd)",
42
+ " --json Machine-readable output",
43
+ " -h, --help Show this help"
44
+ ].join("\n");
45
+ }
46
+
47
+ function parseArgs(argv) {
48
+ let project = process.cwd(), json = false, scope = false;
49
+ const words = [];
50
+ for (let index = 0; index < argv.length; index += 1) {
51
+ const value = argv[index];
52
+ if (value === "-h" || value === "--help") return { help: true };
53
+ if (value === "--json") { json = true; continue; }
54
+ if (value === "--scope") { scope = true; continue; }
55
+ if (value === "--project") {
56
+ const next = argv[index += 1];
57
+ if (!next || next.startsWith("--")) throw new Error("--project needs a path");
58
+ project = path.resolve(next);
59
+ continue;
60
+ }
61
+ if (value.startsWith("--")) throw new Error(`Unknown option: ${value}`);
62
+ words.push(value);
63
+ }
64
+ return { help: false, project, json, scope, command: words.join(" ").trim() };
65
+ }
66
+
67
+ // What the operator should do next. The guard states 74 refusals and only three
68
+ // of them say this, which is why a denial so often ends the conversation.
69
+ const REMEDY = {
70
+ "protected-path": "Use an approved context projection if one is available. Direct access requires the operator to change the protected-path policy; task scope alone does not override it.",
71
+ "protected-glob": "Name the file you mean instead of a pattern; a pattern is refused when any name it can match is protected.",
72
+ "resolved-protected": "The path resolves to a protected file through a link or a parent directory. Use the real path you intend.",
73
+ "unresolved": "Write the path out, or put the expansion in its own argument, so the guard can see what is being opened.",
74
+ "exec-policy": "This is refused by an exec-policy rule, not by a path. Change the command, or ask the operator to run it.",
75
+ "runtime-required": "Run the command through the current Pi session. Only that runtime owns the Task Contract, scope, permission, lifecycle, approval, and budget facts needed for a final decision."
76
+ };
77
+
78
+ const RUNTIME_GATES = [
79
+ "task-contract",
80
+ "task-scope",
81
+ "permission-profile",
82
+ "lifecycle-control",
83
+ "approval-state",
84
+ "context-budget"
85
+ ];
86
+
87
+ function explain(command, protectedPaths, policy, cwd) {
88
+ const steps = [];
89
+ const record = (name, hit, detail) => { steps.push({ step: name, matched: Boolean(hit), detail }); return hit; };
90
+
91
+ const exec = evaluateExecPolicyCore(command, { policy, mode: "enforce" });
92
+ record("exec-policy", exec.decision === "forbid", exec.reasons.length ? exec.reasons.join("; ") : "no rule matched");
93
+ if (exec.decision === "forbid") {
94
+ return { decision: "deny", kind: "exec-policy", confidence: "exact", reason: exec.reasons.join("; "), steps, exec };
95
+ }
96
+
97
+ const literal = findProtectedPathInCommand(command, protectedPaths);
98
+ record("literal-path", literal, literal ? `${literal.candidate} matches ${literal.pattern}` : "no word names a protected path");
99
+ if (literal) {
100
+ return {
101
+ decision: "deny", kind: "protected-path", confidence: "exact",
102
+ reason: `Command touches protected path: ${literal.candidate} matches ${literal.pattern}`, steps, exec
103
+ };
104
+ }
105
+
106
+ const glob = shellGlobTargetsProtectedPath(command, protectedPaths);
107
+ record("glob", glob, glob ? `${glob.glob} can match ${glob.example} via ${glob.pattern}` : "no pattern can reach a protected name");
108
+ if (glob) {
109
+ return {
110
+ decision: "deny", kind: "protected-glob",
111
+ // A pattern is matched against generated examples of each protected
112
+ // pattern, not against the filesystem, so this answers for names that may
113
+ // not exist. That is deliberate, and worth saying out loud.
114
+ confidence: "over-approximate",
115
+ reason: `Command glob can target protected path: ${glob.glob} can match ${glob.example} via ${glob.pattern}`, steps, exec
116
+ };
117
+ }
118
+
119
+ const resolved = findResolvedProtectedPathInCommand(cwd, command, protectedPaths);
120
+ record("resolved-path", resolved, resolved ? `${resolved.candidate} resolves to ${resolved.resolved}` : "no candidate resolves onto a protected path");
121
+ if (resolved) {
122
+ return {
123
+ decision: "deny", kind: "resolved-protected", confidence: "exact",
124
+ reason: `Command resolves to protected path: ${resolved.candidate} resolves to ${resolved.resolved} matching ${resolved.pattern}`, steps, exec
125
+ };
126
+ }
127
+
128
+ const unresolved = protectedPaths.length > 0 ? unresolvedPathExpansions(command) : [];
129
+ record("unresolved-expansion", unresolved.length > 0, unresolved.length ? unresolved.join(", ") : "every path is resolvable");
130
+ if (unresolved.length > 0) {
131
+ return {
132
+ decision: "deny", kind: "unresolved",
133
+ // Nothing was proven to be protected here. The command is refused because
134
+ // its target is unknowable before it runs, which is a different statement
135
+ // and leads the operator to a different fix.
136
+ confidence: "unknown-target",
137
+ reason: unresolvedExpansionReason("Command", unresolved), steps, exec
138
+ };
139
+ }
140
+
141
+ const staticDecision = exec.decision === "prompt" ? "confirm" : "allow";
142
+ return {
143
+ decision: "indeterminate", kind: "runtime-required", confidence: "runtime-required",
144
+ staticDecision,
145
+ reason: staticDecision === "confirm"
146
+ ? `Static preflight requires confirmation (${exec.reasons.join("; ")}), but the live runtime may still block it.`
147
+ : "Static preflight found no refusal. A final decision requires live session and task state.",
148
+ remainingGates: RUNTIME_GATES,
149
+ steps, exec
150
+ };
151
+ }
152
+
153
+ function render(result, context) {
154
+ const lines = [`decision: ${result.decision}`, ""];
155
+ lines.push(` reason ${result.reason}`);
156
+ lines.push(` confidence ${result.confidence}`);
157
+ if (result.staticDecision) lines.push(` static ${result.staticDecision}`);
158
+ lines.push(` remedy ${REMEDY[result.kind] ?? REMEDY["runtime-required"]}`);
159
+ if (result.remainingGates?.length) lines.push(` still needs ${result.remainingGates.join(", ")}`);
160
+ lines.push("", " static chain (the order the guard asks these):");
161
+ for (const step of result.steps) {
162
+ lines.push(` ${step.matched ? "HIT " : " "} ${step.step.padEnd(20)} ${step.detail}`);
163
+ }
164
+ lines.push("", ` profile ${context.profileSource} (${context.profilePath})`);
165
+ lines.push(` protected ${context.protectedPaths.length} patterns in effect`);
166
+ return lines.join("\n");
167
+ }
168
+
169
+ function renderScope(loaded, effective) {
170
+ const lines = ["boundary in effect", ""];
171
+ lines.push(` profile ${loaded.profileSource} (${loaded.profilePath})`);
172
+ lines.push(` mode ${loaded.profile.mode ?? "unset"}`);
173
+ if (typeof loaded.profile.displayName === "string") lines.push(` project ${loaded.profile.displayName}`);
174
+ // Packs are `{name, version}` records, not names, so joining them raw printed
175
+ // `[object Object]` where the operator expected to read what is loaded.
176
+ const packs = (Array.isArray(loaded.profile.capabilityPacks) ? loaded.profile.capabilityPacks : [])
177
+ .map((pack) => (pack && typeof pack === "object" ? `${pack.name}@${pack.version}` : String(pack)));
178
+ lines.push(` packs ${packs.length > 0 ? packs.join(", ") : "none"}`);
179
+ for (const [label, key] of [
180
+ ["shell", "shellProtectedPaths"],
181
+ ["read", "readProtectedPaths"],
182
+ ["write", "writeProtectedPaths"],
183
+ ["read-only", "readOnlyPaths"]
184
+ ]) {
185
+ const values = effective[key];
186
+ lines.push("", ` ${label} (${values.length})`);
187
+ for (const value of values) lines.push(` ${value}`);
188
+ if (values.length === 0) lines.push(" (none)");
189
+ }
190
+ return lines.join("\n");
191
+ }
192
+
193
+ let parsed;
194
+ try {
195
+ parsed = parseArgs(process.argv.slice(2));
196
+ } catch (error) {
197
+ console.error(`FAIL: ${error.message}\n\n${usage()}`);
198
+ process.exit(2);
199
+ }
200
+ if (parsed.help) { process.stdout.write(`${usage()}\n`); process.exit(0); }
201
+ if (!parsed.command && !parsed.scope) { console.error(`FAIL: nothing to explain\n\n${usage()}`); process.exit(2); }
202
+
203
+ try {
204
+ const loaded = loadProjectContextIndexPolicy(platformRoot, parsed.project, { profilePath: process.env.PIAGENT_PROFILE });
205
+ const effective = effectiveProtectedPaths(loaded.policy, loaded.profile);
206
+ const protectedPaths = effective.shellProtectedPaths;
207
+ if (parsed.scope) {
208
+ // The boundary is otherwise only visible by running into it.
209
+ if (parsed.json) {
210
+ process.stdout.write(`${JSON.stringify({
211
+ profileSource: loaded.profileSource, profilePath: loaded.profilePath,
212
+ mode: loaded.profile.mode ?? null, capabilityPacks: loaded.profile.capabilityPacks ?? [], ...effective
213
+ }, null, 2)}\n`);
214
+ } else {
215
+ process.stdout.write(`${renderScope(loaded, effective)}\n`);
216
+ }
217
+ process.exit(0);
218
+ }
219
+ const result = explain(parsed.command, protectedPaths, loaded.policy, parsed.project);
220
+ const context = { profileSource: loaded.profileSource, profilePath: loaded.profilePath, protectedPaths };
221
+ if (parsed.json) {
222
+ process.stdout.write(`${JSON.stringify({
223
+ command: parsed.command, decision: result.decision, kind: result.kind, confidence: result.confidence,
224
+ reason: result.reason, remedy: REMEDY[result.kind] ?? REMEDY["runtime-required"],
225
+ staticDecision: result.staticDecision ?? null,
226
+ remainingGates: result.remainingGates ?? [], steps: result.steps,
227
+ protectedPatternCount: protectedPaths.length, profileSource: loaded.profileSource
228
+ }, null, 2)}\n`);
229
+ } else {
230
+ process.stdout.write(`${render(result, context)}\n`);
231
+ }
232
+ // Exit status is a permission signal for existing shell callers: 0 is reserved
233
+ // for commands that are conclusively allowed. This standalone process can no
234
+ // longer make that claim; 1 is a conclusive static denial and 2 means the live
235
+ // runtime still has to decide.
236
+ process.exit(result.decision === "deny" ? 1 : 2);
237
+ } catch (error) {
238
+ console.error(`FAIL: ${error instanceof Error ? error.message : String(error)}`);
239
+ process.exit(1);
240
+ }
@@ -4,7 +4,32 @@ set -euo pipefail
4
4
  usage() {
5
5
  cat <<'USAGE'
6
6
  Usage:
7
- scripts/install-global.sh [--stable|--dev|--local|--channel <stable|dev|local>] [--version <tag>] [--resolve-tag] [--package-source <source>] [--dry-run] [--with-mcp|--no-mcp] [--mcp-preset <preset>] [--with-subagents|--no-subagents] [--subagents-preset <preset>] [--with-web-access|--no-web-access] [--with-herdr] [--model-scope <preset>|--no-model-scope]
7
+ scripts/install-global.sh [--stable|--dev|--local|--channel <stable|dev|local>] [--version <tag>] [--resolve-tag] [--package-source <source>] [--dry-run] [--with-mcp|--no-mcp] [--mcp-preset <preset>] [--with-subagents|--no-subagents] [--subagents-preset <preset>] [--subagents-model-scope <scope>] [--with-web-access|--no-web-access] [--with-herdr] [--model-scope <preset>|--no-model-scope] [--default-model <model>] [-h|--help]
8
+
9
+ Options:
10
+ --channel <stable|dev|local> Package channel; --stable/--dev/--local are shorthands.
11
+ --version, --tag <tag> Pin a release tag (pair with --resolve-tag for a SHA).
12
+ --resolve-tag Resolve the tag to a commit SHA before installing.
13
+ --package-source <source> Explicit package source; overrides the channel.
14
+ --dry-run Print the planned steps without changing user config.
15
+ --with-mcp | --no-mcp Install the MCP adapter (default: install).
16
+ --mcp-preset <preset> MCP server set (default: core).
17
+ --with-subagents | --no-subagents
18
+ Install pi-subagents (default: only if already present).
19
+ --subagents-preset <minimal|safe|async|parallel>
20
+ Subagent runtime preset (default: safe).
21
+ --subagents-model-scope <none|piagent|codex|claude>
22
+ Model family the subagents run on (default: none).
23
+ --with-web-access | --no-web-access
24
+ Install pi-web-access for the researcher subagent (default: install).
25
+ --with-herdr Also configure Herdr integration.
26
+ --model-scope <full|codex|claude>
27
+ Model families to enable in Pi (default: full).
28
+ --no-model-scope Leave the user's enabledModels untouched.
29
+ --default-model <provider/model[:thinking]>
30
+ Default model written to Pi settings
31
+ (default: openai-codex/gpt-5.5:high).
32
+ -h, --help Print this help and exit.
8
33
 
9
34
  Purpose:
10
35
  Install the piagent Pi package into the current user's global Pi settings.
@@ -74,6 +99,7 @@ RESOLVE_TAG=false
74
99
  RESOLVED_PACKAGE_TAG=""
75
100
  RESOLVED_PACKAGE_COMMIT=""
76
101
  CLI_PACKAGE_SELECTOR=""
102
+ LOCAL_HELPER_PREFIX="${PIAGENT_LOCAL_HELPER_PREFIX:-}"
77
103
  PI_MCP_ADAPTER_SOURCE="npm:pi-mcp-adapter@2.15.0"
78
104
  PI_SUBAGENTS_SOURCE="npm:pi-subagents@0.38.0"
79
105
  PI_WEB_ACCESS_SOURCE="npm:pi-web-access@0.17.0"
@@ -181,6 +207,61 @@ run_cmd() {
181
207
  fi
182
208
  }
183
209
 
210
+ active_piagent_helper_prefix() {
211
+ if [[ -n "$LOCAL_HELPER_PREFIX" ]]; then
212
+ printf '%s\n' "$LOCAL_HELPER_PREFIX"
213
+ return 0
214
+ fi
215
+
216
+ local helper_command
217
+ helper_command="$(command -v piagent 2>/dev/null || true)"
218
+ [[ -n "$helper_command" ]] || return 1
219
+
220
+ node --input-type=module - "$helper_command" <<'NODE'
221
+ import fs from "node:fs";
222
+ import path from "node:path";
223
+
224
+ try {
225
+ const command = path.resolve(process.argv[2]);
226
+ if (path.basename(path.dirname(command)) === "bin") {
227
+ process.stdout.write(`${path.dirname(path.dirname(command))}\n`);
228
+ process.exit(0);
229
+ }
230
+ const target = fs.realpathSync(command);
231
+ const marker = `${path.sep}lib${path.sep}node_modules${path.sep}@piagent${path.sep}platform${path.sep}`;
232
+ const index = target.indexOf(marker);
233
+ if (index <= 0) process.exit(1);
234
+ process.stdout.write(`${target.slice(0, index)}\n`);
235
+ } catch {
236
+ process.exit(1);
237
+ }
238
+ NODE
239
+ }
240
+
241
+ sync_local_helper() {
242
+ [[ "$RESOLVED_CHANNEL_LABEL" == "local" ]] || return 0
243
+
244
+ if ! command -v npm >/dev/null 2>&1; then
245
+ echo "FAIL: npm is required to synchronize the local Piagent helper and Gateway runtime." >&2
246
+ exit 1
247
+ fi
248
+
249
+ local prefix="" used_fallback=false
250
+ prefix="$(active_piagent_helper_prefix 2>/dev/null || true)"
251
+ if [[ -z "$prefix" ]]; then
252
+ prefix="${HOME}/.pi/npm-global"
253
+ used_fallback=true
254
+ fi
255
+
256
+ echo "Synchronizing local terminal helper and Gateway runtime:"
257
+ run_cmd npm install -g --ignore-scripts --prefix "$prefix" "$PLATFORM_ROOT"
258
+
259
+ if [[ "$used_fallback" == true ]]; then
260
+ echo "NOTE: no existing Piagent helper was found; the local helper was installed under $prefix." >&2
261
+ echo "NOTE: add $prefix/bin to PATH if piagent is not found after this run." >&2
262
+ fi
263
+ }
264
+
184
265
  resolve_user_package_path() {
185
266
  local source="$1"
186
267
  local pi_agent_dir="${PI_CODING_AGENT_DIR:-"${HOME}/.pi/agent"}"
@@ -285,10 +366,29 @@ verify_npm_integrity() {
285
366
  echo "FAIL: npm is required to verify package integrity for $package_spec." >&2
286
367
  exit 1
287
368
  fi
288
- local actual
289
- actual="$(npm view "$package_spec" dist.integrity 2>/dev/null | tr -d '\r\n')"
369
+ # The status is captured rather than left to `set -e`. Declaring `local actual`
370
+ # and assigning on the next line makes the command substitution's own status the
371
+ # status of the assignment, so under `pipefail` an unreachable registry -- no
372
+ # network, a proxy, a private registry, an npm outage -- killed the script on
373
+ # this line. The `FAIL:` below was never reached and npm's stderr was already
374
+ # discarded, so the run ended with an empty screen and exit 1, after `pi install`
375
+ # had already succeeded. The operator was left half-installed with no reason
376
+ # given. (Writing `local actual="$(...)"` also avoids the exit, but by hiding the
377
+ # failure inside the empty-value branch, which then reports a digest mismatch --
378
+ # a security message for what is usually a network problem.)
379
+ local actual="" status=0
380
+ actual="$(npm view "$package_spec" dist.integrity 2>/dev/null | tr -d '\r\n')" || status=$?
381
+ if [[ "$status" -ne 0 ]]; then
382
+ echo "FAIL: could not reach the npm registry to verify $package_spec (npm exited $status)." >&2
383
+ echo " Nothing further was installed. Check network, proxy, or registry auth," >&2
384
+ echo " then re-run. To see npm's own message: npm view $package_spec dist.integrity" >&2
385
+ exit 1
386
+ fi
290
387
  if [[ -z "$actual" || "$actual" != "$expected" ]]; then
291
388
  echo "FAIL: registry integrity does not match the approved digest for $package_spec." >&2
389
+ echo " approved: $expected" >&2
390
+ echo " registry: ${actual:-<empty>}" >&2
391
+ echo " Do not proceed. The published package differs from the reviewed release." >&2
292
392
  exit 1
293
393
  fi
294
394
  }
@@ -609,6 +709,7 @@ if [[ -n "$RESOLVED_PACKAGE_COMMIT" ]]; then
609
709
  echo " resolvedCommit: $RESOLVED_PACKAGE_COMMIT"
610
710
  fi
611
711
  echo " source: $PACKAGE_SOURCE"
712
+ sync_local_helper
612
713
  echo "Removing previous Pi Agent Platform package registrations:"
613
714
  remove_existing_piagent_platform_sources
614
715
  run_cmd pi install "$PACKAGE_SOURCE"