@piagent/platform 1.4.0 → 1.4.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 (100) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +8 -2
  3. package/README.vi.md +27 -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 +36 -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/package.json +14 -2
  59. package/packages/piagent-core/README.md +3 -2
  60. package/packages/piagent-core/capabilities/capability-verification-cache.js +17 -7
  61. package/packages/piagent-core/capabilities/runtime-integrity.js +51 -4
  62. package/packages/piagent-core/extensions/piagent-guard.ts +18 -235
  63. package/packages/piagent-core/extensions/runtime-evidence.js +12 -1
  64. package/packages/piagent-core/extensions/shell-reach.ts +250 -0
  65. package/packages/piagent-core/mcp/mcp-command-actions.js +1 -1
  66. package/packages/piagent-core/mcp/mcp-server-catalog.js +135 -5
  67. package/packages/piagent-core/package.json +1 -1
  68. package/packages/piagent-core/runtime/hooks/tool-call-hook.ts +9 -2
  69. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +25 -2
  70. package/packages/piagent-core/runtime/policy/block-remedy.ts +139 -0
  71. package/packages/piagent-core/runtime/registration/task-start-tool.ts +9 -13
  72. package/packages/piagent-core/runtime/session/token-economics.js +34 -0
  73. package/packages/piagent-core/runtime/tools/phase-tool-runtime.ts +2 -1
  74. package/packages/piagent-core/runtime/workflows/task-intake.ts +5 -0
  75. package/packages/piagent-core/security/sensitive-data.js +7 -0
  76. package/packages/piagent-webui/dist/client/assets/index-bu7XpD9Q.js +172 -0
  77. package/packages/piagent-webui/dist/client/index.html +1 -1
  78. package/packages/piagent-webui/extension/lifecycle-controller.ts +15 -0
  79. package/packages/piagent-webui/extension/piagent-webui.ts +4 -4
  80. package/packages/piagent-webui/extension/same-session-bridge.ts +3 -3
  81. package/packages/piagent-webui/gateway/extension-authority.ts +47 -0
  82. package/packages/piagent-webui/gateway/session-inspection-registry.ts +37 -2
  83. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +5 -1
  84. package/packages/piagent-webui/server/core-inspection-provider.ts +5 -5
  85. package/packages/piagent-webui/server/transcript-projection.ts +15 -0
  86. package/scripts/build-docs-site.mjs +21 -1
  87. package/scripts/explain-command.mjs +240 -0
  88. package/scripts/install-global.sh +104 -3
  89. package/scripts/mcp-manage.mjs +17 -4
  90. package/scripts/pi-session-stats.sh +10 -2
  91. package/scripts/piagent-cli.mjs +14 -1
  92. package/scripts/piagent-dashboard.mjs +55 -2
  93. package/scripts/setup.sh +69 -33
  94. package/scripts/traffic-snapshot.mjs +214 -0
  95. package/scripts/typescript-loader.mjs +20 -4
  96. package/scripts/uninstall-global.sh +53 -2
  97. package/scripts/update-global.mjs +33 -1
  98. package/scripts/verify-local.sh +4 -0
  99. package/scripts/verify-translation-pairing.mjs +121 -0
  100. package/packages/piagent-webui/dist/client/assets/index-DjMntaK5.js +0 -172
@@ -0,0 +1,139 @@
1
+ // What to do about a refusal.
2
+ //
3
+ // A blocked tool call is the only conversation most operators ever have with
4
+ // this policy, and for most of them it was one-sided: the guard states 74
5
+ // distinct refusals and three of them said what to do next. The rest ended at
6
+ // "no", which leaves the reader guessing at a rule they cannot see.
7
+ //
8
+ // Remedies are attached in one place rather than at the 29 sites that build a
9
+ // refusal, for the reason this codebase keeps arriving at: a rule applied at
10
+ // every call site is a rule that will be missed at the next one. Every decision
11
+ // leaves through `registerToolCallHook`, so it is enough to answer there, and a
12
+ // refusal added tomorrow is answered without anyone remembering to.
13
+
14
+ // Matched against the refusal text in order; the first match wins, so put the
15
+ // narrower phrase above the broader one it would otherwise be swallowed by.
16
+ const REMEDIES: Array<{ match: RegExp; remedy: string }> = [
17
+ {
18
+ // `grep glob targeting` and `find pattern targeting` are the same refusal
19
+ // reached through a different tool, and matching only the shell wording left
20
+ // two thirds of this family answering nothing.
21
+ match: /glob can target protected path|glob targeting protected path|pattern targeting protected path/i,
22
+ remedy: "Name the exact file instead of a pattern: a pattern is refused when any name it can match is protected."
23
+ + " `piagent explain '<command>'` shows which name it matched."
24
+ },
25
+ {
26
+ match: /builds a filename this guard cannot resolve/i,
27
+ // This refusal already carries its own guidance, so it only gains the
28
+ // pointer to the tool that shows the whole chain.
29
+ remedy: "`piagent explain '<command>'` shows every step that ran and what each one saw."
30
+ },
31
+ {
32
+ match: /(touches|resolves to) protected path|access to protected path|write to protected path/i,
33
+ remedy: "Use an approved `piagent-context` projection if one is available. Direct access requires the operator"
34
+ + " to change the protected-path policy; task scope alone does not override it."
35
+ + " `piagent explain '<command>'` shows which pattern matched."
36
+ },
37
+ {
38
+ match: /write to read-only path/i,
39
+ remedy: "The active profile marks this path read-only. Change `readOnlyPaths` in `.pi/piagent-profile.json`"
40
+ + " if the project really should allow writes here, then reapply the profile."
41
+ },
42
+ {
43
+ match: /outside resolved filesystem scope/i,
44
+ remedy: "Work inside the project, or widen the filesystem scope in the active profile and reapply it."
45
+ },
46
+ {
47
+ match: /cannot mutate paths outside its declared scope|semantic repair must stay inside its declared task scope/i,
48
+ remedy: "Start a new attempt with `piagent_task_start` listing this path in the scope; a running task cannot widen its own."
49
+ },
50
+ {
51
+ match: /Task Implementation Contract is required/i,
52
+ remedy: "Call `piagent_task_start` once with explicit project-relative scope, then retry."
53
+ },
54
+ {
55
+ match: /Capability lock is (missing|unreadable)|does not match what this project agreed to/i,
56
+ remedy: "Reapply the project profile to rebuild the lock: `piagent-init <project> --force-profile`."
57
+ },
58
+ {
59
+ match: /runtime changed during this session/i,
60
+ remedy: "Restart the Pi session. The lock is re-verified and re-pinned at session start, never mid-session."
61
+ },
62
+ {
63
+ match: /Permission profile read-only blocked/i,
64
+ remedy: "This session is read-only. Change the access level in Settings, or start it with a writable permission profile."
65
+ },
66
+ {
67
+ match: /Tool (is not registered|registry is disabled|registry blocked)/i,
68
+ remedy: "Enable the tool group for this profile, or use a tool the profile already allows."
69
+ },
70
+ {
71
+ match: /lifecycle control blocks tool start|resume is blocked|could not enter an audited repair phase/i,
72
+ remedy: "Resolve the task lifecycle first — resume, repair, or close the current attempt — then retry."
73
+ },
74
+ {
75
+ match: /path traverses symbolic link|path resolves outside the project|path component cannot be inspected/i,
76
+ remedy: "Use the real path inside the project. A link that leaves the project is refused rather than followed."
77
+ },
78
+ {
79
+ match: /requires confirmation|requires explicit operator approval/i,
80
+ remedy: "Approve it in the session when prompted; this is a decision the operator makes, not a defect."
81
+ },
82
+ {
83
+ // The whole session is refused here, so this is the refusal most in need of
84
+ // a next step -- and the one the narrower MCP wording used to miss.
85
+ match: /Blocked every tool call/i,
86
+ remedy: "Every tool call is refused while a repository config can reach an ungated server."
87
+ + " Run `piagent-mcp doctor` to see which config stopped the session, then approve or remove those servers."
88
+ },
89
+ {
90
+ match: /MCP (proxy|shell carrier|command|server)/i,
91
+ remedy: "Check the server with `piagent-mcp doctor`, and approve it explicitly if the project should reach it."
92
+ },
93
+ {
94
+ match: /is read-only; .*cannot mutate|read-only inspection allowlist/i,
95
+ remedy: "This task was started read-only. Start a source-change task with `piagent_task_start` when the work needs to write."
96
+ },
97
+ {
98
+ match: /opaque shell mutation whose write target is not statically bounded/i,
99
+ remedy: "Use the edit, write, or apply-patch tools so the target is named, instead of a shell command that computes it."
100
+ },
101
+ {
102
+ match: /Context budget blocked editing large file/i,
103
+ remedy: "Edit a narrower range of the file, or split the change, so the edit stays inside the context budget."
104
+ },
105
+ {
106
+ match: /Approval became stale/i,
107
+ remedy: "Ask for the action again; an approval is bound to the state it was granted for and does not survive a change to it."
108
+ },
109
+ {
110
+ match: /Missing capability|Capability validation failed/i,
111
+ remedy: "Add the pack that provides it to the project profile, then reapply the profile."
112
+ },
113
+ {
114
+ match: /nesting exceeds inspection depth|unsafe scope pattern/i,
115
+ remedy: "Flatten the tool input or narrow the scope pattern: anything the guard cannot finish inspecting is refused rather than guessed at."
116
+ },
117
+ {
118
+ match: /start a new attempt or a fresh task/i,
119
+ remedy: "Start the next attempt with `piagent_task_start`; a closed attempt cannot keep mutating the project."
120
+ }
121
+ ];
122
+
123
+ // Kept out of the appended text when the refusal already ends by saying what to
124
+ // do, so a reader is never given the same instruction twice in one line.
125
+ const ALREADY_GUIDED = /Write the path out|Reapply the project profile|Restart the session|then retry\.$|Start the task first/i;
126
+
127
+ export function remedyForReason(reason: string): string | undefined {
128
+ if (typeof reason !== "string" || reason.trim().length === 0) return undefined;
129
+ const found = REMEDIES.find((entry) => entry.match.test(reason));
130
+ if (!found) return undefined;
131
+ if (ALREADY_GUIDED.test(reason) && !/glob can target|cannot resolve/i.test(reason)) return undefined;
132
+ return found.remedy;
133
+ }
134
+
135
+ export function withRemedy<T extends { block: true; reason: string }>(decision: T): T {
136
+ const remedy = remedyForReason(decision.reason);
137
+ if (!remedy || decision.reason.includes(remedy)) return decision;
138
+ return { ...decision, reason: `${decision.reason.replace(/\s+$/, "")} → ${remedy}` };
139
+ }
@@ -44,10 +44,11 @@ export function registerTaskStartTool(pi: ExtensionAPI, deps: Record<string, any
44
44
  const taskStartTool = {
45
45
  name: "piagent_task_start",
46
46
  label: "Piagent Task Start",
47
- description: "Create a Task Implementation Contract for the current project before editing.",
48
- promptSnippet: "Start a governed implementation task and persist the task contract.",
47
+ description: "Create a Task Implementation Contract before governed project inspection, command execution, or source changes.",
48
+ promptSnippet: "Start a governed project task and persist the task contract.",
49
49
  promptGuidelines: [
50
50
  "Call this exactly once before source edits in a project managed by Pi Agent Platform.",
51
+ "Use source-change for project command execution, including tests, builds, lint, typecheck, verification, and package dry-runs, even when the operator requires source files to remain unchanged. Read-only intentionally blocks shell execution.",
51
52
  "Do not call context, status, policy, evidence-recording, trace, or gate tools first; runtime hooks provide those checks automatically.",
52
53
  "Use tiny for a bounded low-risk change, normal for ordinary multi-file work, and high-risk for security, data, release, migration, or external-impact work.",
53
54
  "Every scope entry must be a project-relative path or glob such as src/file.ts, src/**, or test/**; never put prose in scope.",
@@ -58,7 +59,9 @@ export function registerTaskStartTool(pi: ExtensionAPI, deps: Record<string, any
58
59
  taskId: Type.Optional(Type.String({ minLength: 1 })),
59
60
  summary: Type.String({ minLength: 10 }),
60
61
  riskLane: StringEnum(["tiny", "normal", "high-risk"] as const),
61
- changeMode: Type.Optional(StringEnum(["source-change", "read-only"] as const)),
62
+ changeMode: Type.Optional(StringEnum(["source-change", "read-only"] as const, {
63
+ description: "Use source-change for project command execution as well as edits; use read-only only for inspection that needs no shell execution."
64
+ })),
62
65
  maxAttempts: Type.Optional(Type.Number({ minimum: 1, maximum: 10 })),
63
66
  expectedOutput: Type.String({ minLength: 10 }),
64
67
  acceptanceCriteria: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, maxItems: 12 }),
@@ -103,15 +106,8 @@ export function registerTaskStartTool(pi: ExtensionAPI, deps: Record<string, any
103
106
  const migrationRetry = active?.taskId === taskId
104
107
  && active?.workingTreeDigestMigration?.status === "new-attempt-required";
105
108
  const authorityRetry = active?.taskId === taskId && activeAuthorityReplacement?.required === true;
106
- if (active && !migrationRetry && !authorityRetry) {
109
+ if (active?.trace.outcome === "pending" && !migrationRetry && !authorityRetry) {
107
110
  if (active.taskId === taskId) {
108
- if (active.trace.outcome !== "pending") {
109
- return {
110
- content: [{ type: "text", text: `Session already belongs to terminal task ${active.taskId} (${active.trace.outcome}). Start a fresh Pi session for a retry or new task.` }],
111
- details: active,
112
- isError: true
113
- };
114
- }
115
111
  const manifest = repositoryFileManifest(ctx.cwd);
116
112
  const currentScopeResolution = resolveTaskScopePatterns(active.scope, manifest);
117
113
  const proposedScopeResolution = Array.isArray(params.scope)
@@ -151,7 +147,7 @@ export function registerTaskStartTool(pi: ExtensionAPI, deps: Record<string, any
151
147
  };
152
148
  }
153
149
  return {
154
- content: [{ type: "text", text: `Session already belongs to task ${active.taskId} (${active.taskRunId}, ${active.trace.outcome}). Use one Pi session per task and start a fresh session before another task.` }],
150
+ content: [{ type: "text", text: `Session already has pending task ${active.taskId} (${active.taskRunId}). Complete or stop it before starting another task in this conversation.` }],
155
151
  details: active,
156
152
  isError: true
157
153
  };
@@ -428,7 +424,7 @@ export function registerTaskStartTool(pi: ExtensionAPI, deps: Record<string, any
428
424
  const started = await taskStartTool.execute(
429
425
  `runtime-intake-${ctx.sessionManager.getSessionId()}`,
430
426
  {
431
- taskId: hasOperatorSessionName(sessionName) ? sessionName : summary,
427
+ taskId: active && active.trace.outcome !== "pending" ? summary : hasOperatorSessionName(sessionName) ? sessionName : summary,
432
428
  summary,
433
429
  riskLane: automaticTaskRiskLane(prompt),
434
430
  intakeMode: "runtime",
@@ -0,0 +1,34 @@
1
+ // What a session's token counts mean, in one place.
2
+ //
3
+ // `fresh` is the benchmark's own definition -- input + output, the tokens billed
4
+ // at full rate -- restated here rather than recomputed differently by each
5
+ // reader. The published benchmark and the terminal must never mean two things by
6
+ // the same word.
7
+ //
8
+ // The cache hit rate is reported beside `fresh`, never instead of it. In that
9
+ // same benchmark the baseline surface held the *higher* hit rate, 84.9% against
10
+ // 53.0%, while spending more than twice the fresh tokens: a high rate on a large
11
+ // prompt is not a cheap session. The rate explains a cost, it does not report one.
12
+
13
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
14
+
15
+ export function sessionTokenEconomics(tokens = {}) {
16
+ const { input, output, cacheRead } = tokens ?? {};
17
+ const fresh = finite(input) && finite(output) ? input + output : null;
18
+ const prompt = finite(cacheRead) && finite(input) ? cacheRead + input : null;
19
+ return {
20
+ fresh,
21
+ // A session that has read nothing has no rate to report, which is not the
22
+ // same as a rate of zero.
23
+ cacheHitRate: prompt !== null && prompt > 0 ? cacheRead / prompt : null
24
+ };
25
+ }
26
+
27
+ export function formatTokenEconomics(economics, formatNumber, formatPercent) {
28
+ return {
29
+ fresh: economics.fresh === null ? "unknown" : `${formatNumber(economics.fresh)} billed at full rate`,
30
+ cache: economics.cacheHitRate === null
31
+ ? "unknown"
32
+ : `${formatPercent(100 * economics.cacheHitRate)} of prompt tokens served from cache`
33
+ };
34
+ }
@@ -80,7 +80,8 @@ export class PhaseToolRuntime {
80
80
 
81
81
  toolDecision(ctx: ExtensionContext, toolName: string): { block: true; reason: string } | undefined {
82
82
  if (this.effectiveMode(ctx) !== "on") return undefined;
83
- if (toolName === "piagent_task_start" && this.digestMigrationStatus(ctx) === "new-attempt-required") return undefined;
83
+ if (toolName === "piagent_task_start"
84
+ && (this.digestMigrationStatus(ctx) === "new-attempt-required" || this.states.get(this.key(ctx))?.currentPhase === "terminal")) return undefined;
84
85
  const state = this.states.get(this.key(ctx));
85
86
  if (!state) return undefined;
86
87
  const policy = phaseToolPolicy(state.currentPhase, state.changeMode);
@@ -12,6 +12,7 @@ const AUTO_INTAKE_READ_ONLY_LEAD = /^\s*\/?(?:analy[sz]e|audit|check|discuss|exp
12
12
  const AUTO_INTAKE_MANUAL_RISK = /\b(?:credential|database|deploy|destructive|encryption|external provider|payment|permission|production|publish|secret|token rotation)\b/i;
13
13
  const AUTO_READ_ONLY_INTENT = /\b(?:analy[sz]e|audit|check|diagnos(?:e|is)|explain|inspect|investigate|plan|research|review|scout|summari[sz]e|triage|kiem tra|nghien cuu|giai thich|danh gia)\b/i;
14
14
  const AUTO_READ_ONLY_BOUNDARY = /\b(?:read-only|no edits?|do not edit(?: files?| source| project| repo)?|do not change (?:files?|source|project|repo)|do not mutate (?:files?|source|project|repo|workspace)|khong sua(?: file| source| project)?|khong edit(?: file| source| project)?|khong doi(?: file| source| project)?)\b/i;
15
+ const AUTO_EXECUTION_INTENT = /(?:\b(?:run|execute|execution|rerun|re-run|chay)\b.{0,80}\b(?:tests?|build|checks?|gates?|lint|typecheck|package|pack|verify|verification)\b|\b(?:npm|pnpm|yarn|bun)\s+(?:test|pack|run\s+(?:build|check|lint|typecheck|verify))\b)/i;
15
16
 
16
17
  export const AUTO_INTAKE_SNAPSHOT_PATTERNS = [
17
18
  "src/**", "app/**", "lib/**", "packages/**", "test/**", "tests/**", "spec/**", "__tests__/**"
@@ -49,6 +50,10 @@ export function automaticTaskIntakeEligible(prompt: string, readProtectedPaths:
49
50
  const text = String(prompt ?? "").trim();
50
51
  if (!text || text.length > AUTO_INTAKE_MAX_PROMPT_CHARS) return false;
51
52
  const signal = classifyContextTask(text);
53
+ if (AUTO_EXECUTION_INTENT.test(text)) {
54
+ if (/\bpiagent_task_start\b/i.test(text)) return false;
55
+ return !signal.paths.some((candidate) => matchesProtectedPath(candidate, readProtectedPaths));
56
+ }
52
57
  if (signal.workflow !== "task" || !AUTO_INTAKE_CHANGE_INTENT.test(text)) return false;
53
58
  if (AUTO_INTAKE_READ_ONLY_LEAD.test(text) || AUTO_INTAKE_MANUAL_RISK.test(text)) return false;
54
59
  if (/\bpiagent_task_start\b/i.test(text)) return false;
@@ -8,6 +8,13 @@ const TOKEN_PATTERNS = [
8
8
  /\bsk-(?:(?:proj|svcacct|admin|ant)-)?[A-Za-z0-9_-]{20,}\b/g,
9
9
  /\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
10
10
  /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
11
+ // An npm token was recognised only where something named it -- `NPM_TOKEN=...`
12
+ // caught the assignment, a field called `token` caught the field -- so the one
13
+ // place it actually appears was missed: `//registry.npmjs.org/:_authToken=npm_…`
14
+ // is the whole of an `.npmrc`, and a `cat` of that file or a failing publish log
15
+ // carried the credential through unredacted. Every other provider here is
16
+ // recognised by the shape of its token; npm was the gap.
17
+ /\bnpm_[A-Za-z0-9]{30,}\b/g,
11
18
  /\bAIza[0-9A-Za-z_-]{20,}\b/g,
12
19
  /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
13
20
  ];