@piagent/platform 1.7.1 → 1.9.0

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 (227) hide show
  1. package/AGENTS.md +7 -0
  2. package/CHANGELOG.md +113 -1
  3. package/README.md +45 -70
  4. package/README.vi.md +34 -61
  5. package/architecture/layers.json +16 -3
  6. package/catalog/capabilities.json +14 -7
  7. package/catalog/claude-model-additions.json +42 -0
  8. package/docs/auto-delegation-policy.md +2 -0
  9. package/docs/command-reference-vietnamese.md +69 -91
  10. package/docs/context-window-policy.md +5 -5
  11. package/docs/distribution-standard.md +16 -19
  12. package/docs/en/architecture.md +9 -1
  13. package/docs/herdr-workflow.md +2 -3
  14. package/docs/languages.json +4 -4
  15. package/docs/managed-local-acceptance.md +403 -0
  16. package/docs/mcp-and-tools.md +5 -7
  17. package/docs/memory-policy.md +3 -3
  18. package/docs/model-options.md +22 -17
  19. package/docs/oauth-providers.md +1 -1
  20. package/docs/operator-manual-vietnamese.md +85 -130
  21. package/docs/package-architecture-notes.md +4 -4
  22. package/docs/piagent-core-improvement-plan.md +3 -5
  23. package/docs/piagent-core-plan-checkpoint-2026-09-06.json +2 -13
  24. package/docs/piagent-runtime-benchmark-execution-plan.md +10 -8
  25. package/docs/project-adapters.md +3 -3
  26. package/docs/project-onboarding.md +15 -16
  27. package/docs/publishing-for-teams.md +3 -1
  28. package/docs/quality-benchmark.md +2 -0
  29. package/docs/quickstart-vietnamese.md +24 -25
  30. package/docs/release-install-policy.md +16 -11
  31. package/docs/research/harness-next-evidence-review.md +7 -7
  32. package/docs/runtime-harness-standard.md +2 -0
  33. package/docs/runtime-policy-design.md +2 -0
  34. package/docs/runtime-quality-baseline.md +2 -0
  35. package/docs/subagent-orchestration-capabilities.md +1 -1
  36. package/docs/subagents-and-multiagent.md +10 -8
  37. package/docs/task-implementation-contract.md +2 -0
  38. package/docs/task-lifecycle-vietnamese.md +2 -0
  39. package/docs/team-onboarding.md +33 -64
  40. package/docs/usage-observability.md +2 -2
  41. package/docs/vi/architecture.md +9 -1
  42. package/docs/workflow-recipes.md +6 -10
  43. package/docs-site/architecture.html +4 -3
  44. package/docs-site/assets/docs.css +80 -0
  45. package/docs-site/benchmark.html +7 -1
  46. package/docs-site/commands.html +37 -23
  47. package/docs-site/content/architecture.html +2 -2
  48. package/docs-site/content/benchmark.html +5 -0
  49. package/docs-site/content/commands.html +35 -22
  50. package/docs-site/content/ecosystem.html +144 -0
  51. package/docs-site/content/en/architecture.html +1 -1
  52. package/docs-site/content/en/benchmark.html +5 -0
  53. package/docs-site/content/en/commands.html +30 -13
  54. package/docs-site/content/en/ecosystem.html +144 -0
  55. package/docs-site/content/en/index.html +24 -9
  56. package/docs-site/content/en/quickstart.html +10 -12
  57. package/docs-site/content/en/runtime.html +1 -1
  58. package/docs-site/content/en/security.html +1 -1
  59. package/docs-site/content/en/team.html +2 -2
  60. package/docs-site/content/en/whats-new.html +89 -1
  61. package/docs-site/content/en/workflows.html +29 -31
  62. package/docs-site/content/index.html +24 -9
  63. package/docs-site/content/quickstart.html +10 -12
  64. package/docs-site/content/runtime.html +2 -2
  65. package/docs-site/content/security.html +2 -2
  66. package/docs-site/content/team.html +2 -2
  67. package/docs-site/content/whats-new.html +89 -1
  68. package/docs-site/content/workflows.html +36 -38
  69. package/docs-site/context-engine.html +2 -1
  70. package/docs-site/ecosystem.html +283 -0
  71. package/docs-site/en/architecture.html +3 -2
  72. package/docs-site/en/benchmark.html +7 -1
  73. package/docs-site/en/commands.html +32 -14
  74. package/docs-site/en/context-engine.html +2 -1
  75. package/docs-site/en/ecosystem.html +283 -0
  76. package/docs-site/en/index.html +30 -14
  77. package/docs-site/en/mcp-approval.html +2 -1
  78. package/docs-site/en/mcp-auth.html +2 -1
  79. package/docs-site/en/mcp-servers.html +2 -1
  80. package/docs-site/en/mcp.html +2 -1
  81. package/docs-site/en/permissions.html +2 -1
  82. package/docs-site/en/profiles.html +2 -1
  83. package/docs-site/en/quickstart.html +17 -18
  84. package/docs-site/en/runtime.html +3 -2
  85. package/docs-site/en/scope.html +2 -1
  86. package/docs-site/en/security.html +6 -5
  87. package/docs-site/en/team.html +4 -3
  88. package/docs-site/en/whats-new.html +97 -8
  89. package/docs-site/en/workflows.html +34 -35
  90. package/docs-site/index.html +30 -14
  91. package/docs-site/mcp-approval.html +2 -1
  92. package/docs-site/mcp-auth.html +2 -1
  93. package/docs-site/mcp-servers.html +2 -1
  94. package/docs-site/mcp.html +2 -1
  95. package/docs-site/permissions.html +2 -1
  96. package/docs-site/profiles.html +2 -1
  97. package/docs-site/quickstart.html +17 -18
  98. package/docs-site/runtime.html +4 -3
  99. package/docs-site/scope.html +2 -1
  100. package/docs-site/security.html +7 -6
  101. package/docs-site/team.html +4 -3
  102. package/docs-site/whats-new.html +97 -8
  103. package/docs-site/workflows.html +41 -42
  104. package/evals/fs5-causal-arm.v1.json +2 -2
  105. package/evals/private-holdout-v1/CURRENT_BOUNDARY.md +17 -0
  106. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  107. package/package.json +10 -9
  108. package/packages/piagent-core/README.md +15 -23
  109. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +2 -0
  110. package/packages/piagent-core/extensions/experiment-loop-policy.ts +75 -0
  111. package/packages/piagent-core/extensions/local-state-path.js +15 -0
  112. package/packages/piagent-core/extensions/piagent-guard.ts +108 -960
  113. package/packages/piagent-core/extensions/policy-core.js +2 -2
  114. package/packages/piagent-core/extensions/source-checkout-paths.ts +54 -0
  115. package/packages/piagent-core/managed/broker-client.mjs +49 -0
  116. package/packages/piagent-core/managed/capabilities.mjs +28 -0
  117. package/packages/piagent-core/managed/compaction.mjs +63 -0
  118. package/packages/piagent-core/managed/doctor.mjs +107 -0
  119. package/packages/piagent-core/managed/native-catalog.mjs +42 -0
  120. package/packages/piagent-core/managed/patch-snapshot.mjs +48 -0
  121. package/packages/piagent-core/managed/repository-operation.mjs +84 -0
  122. package/packages/piagent-core/managed/request-stream.mjs +129 -0
  123. package/packages/piagent-core/managed/resource-loader.mjs +42 -0
  124. package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -0
  125. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +12 -0
  126. package/packages/piagent-core/managed/search-fallback.mjs +93 -0
  127. package/packages/piagent-core/managed/session.mjs +472 -0
  128. package/packages/piagent-core/managed/store.d.mts +3 -0
  129. package/packages/piagent-core/managed/store.mjs +64 -0
  130. package/packages/piagent-core/managed/tool-boundary.mjs +243 -0
  131. package/packages/piagent-core/managed/tool-worker.mjs +36 -0
  132. package/packages/piagent-core/managed/toolchain.mjs +120 -0
  133. package/packages/piagent-core/managed/web-fetch.mjs +105 -0
  134. package/packages/piagent-core/managed/web-search.mjs +96 -0
  135. package/packages/piagent-core/managed/workflow.mjs +299 -0
  136. package/packages/piagent-core/package.json +3 -6
  137. package/packages/piagent-core/policies/base-policy.json +80 -26
  138. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +3 -3
  139. package/packages/piagent-core/runtime/hooks/input-hook.ts +35 -196
  140. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -0
  141. package/packages/piagent-core/runtime/inspection/approval-broker.ts +9 -1
  142. package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +1 -1
  143. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +1 -1
  144. package/packages/piagent-core/runtime/managed-failure.d.mts +10 -0
  145. package/packages/piagent-core/runtime/managed-failure.mjs +164 -0
  146. package/packages/piagent-core/runtime/model/model-route-policy.ts +33 -13
  147. package/packages/piagent-core/runtime/model/model-route-types.ts +10 -6
  148. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +8 -29
  149. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +1 -1
  150. package/packages/piagent-core/runtime/policy/block-remedy.ts +5 -3
  151. package/packages/piagent-core/runtime/registration/context-commands.ts +1 -1
  152. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +7 -7
  153. package/packages/piagent-core/runtime/registration/policy-tools.ts +2 -2
  154. package/packages/piagent-core/runtime/registration/session-commands.ts +11 -26
  155. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
  156. package/packages/piagent-core/runtime/session/system-prompt.ts +6 -7
  157. package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +6 -6
  158. package/packages/piagent-core/runtime/workflows/webui-workflow.ts +6 -11
  159. package/packages/piagent-core/security/sensitive-text.js +9 -0
  160. package/packages/piagent-core/skills/piagent-ops/SKILL.md +8 -10
  161. package/packages/piagent-core/skills/test-audit/SKILL.md +101 -0
  162. package/packages/piagent-core/subagents/piagent-planner.md +1 -1
  163. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DGk34zJT.js → MarkdownMessage-Dd9wPk-v.js} +1 -1
  164. package/packages/piagent-webui/dist/client/assets/index-7I-50Q2U.js +228 -0
  165. package/packages/piagent-webui/dist/client/assets/{index-DVahUVWy.css → index-DgSmcnP8.css} +1 -1
  166. package/packages/piagent-webui/dist/client/index.html +2 -2
  167. package/packages/piagent-webui/extension/piagent-webui.ts +11 -11
  168. package/packages/piagent-webui/extension/session-stream-adapter.ts +3 -1
  169. package/packages/piagent-webui/extension/terminal-session-adapter.ts +6 -1
  170. package/packages/piagent-webui/gateway/company-relay.ts +308 -0
  171. package/packages/piagent-webui/gateway/control-socket.ts +39 -12
  172. package/packages/piagent-webui/gateway/gateway-service.ts +109 -28
  173. package/packages/piagent-webui/gateway/gateway-session-stream.ts +58 -2
  174. package/packages/piagent-webui/gateway/managed-gateway.mjs +41 -0
  175. package/packages/piagent-webui/gateway/managed-launch.ts +145 -0
  176. package/packages/piagent-webui/gateway/managed-projection.ts +20 -0
  177. package/packages/piagent-webui/gateway/pi-host.ts +22 -4
  178. package/packages/piagent-webui/gateway/runtime-session-controls.ts +5 -2
  179. package/packages/piagent-webui/gateway/session-approval-authority.ts +28 -0
  180. package/packages/piagent-webui/gateway/session-catalog.ts +11 -3
  181. package/packages/piagent-webui/gateway/session-command-controller.ts +8 -2
  182. package/packages/piagent-webui/gateway/session-file-facts.ts +25 -0
  183. package/packages/piagent-webui/gateway/session-inspection-registry.ts +42 -12
  184. package/packages/piagent-webui/gateway/session-list-cache.ts +68 -0
  185. package/packages/piagent-webui/gateway/session-runtime-ownership.ts +8 -1
  186. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +20 -27
  187. package/packages/piagent-webui/ownership/managed-terminal-lease.ts +27 -0
  188. package/packages/piagent-webui/ownership/profile-state.ts +2 -0
  189. package/packages/piagent-webui/ownership/session-lease-store.ts +22 -1
  190. package/packages/piagent-webui/server/approval-projection.ts +9 -0
  191. package/packages/piagent-webui/server/core-inspection-provider.ts +4 -9
  192. package/packages/piagent-webui/server/loopback-server.ts +37 -1
  193. package/packages/piagent-webui/server/message-stop-reason.ts +11 -0
  194. package/packages/piagent-webui/server/tool-call-summary.ts +185 -0
  195. package/packages/piagent-webui/server/transcript-projection.ts +86 -3
  196. package/packs/engineering-base/pack.json +4 -0
  197. package/schemas/piagent-webui/runtime-command-v1.schema.json +1 -2
  198. package/schemas/piagent-webui/session-catalog-v1.schema.json +8 -0
  199. package/schemas/piagent-webui/transcript-v1.schema.json +94 -1
  200. package/scripts/build-docs-site.mjs +23 -12
  201. package/scripts/check-public-wording.mjs +5 -6
  202. package/scripts/check-runtime-advisories.mjs +23 -6
  203. package/scripts/claude-model-additions.mjs +33 -0
  204. package/scripts/configure-model-scope.sh +36 -8
  205. package/scripts/configure-subagents.sh +1 -1
  206. package/scripts/explain-command.mjs +5 -6
  207. package/scripts/install-global.sh +2 -2
  208. package/scripts/model-route-evaluation-core.ts +1 -0
  209. package/scripts/model-scope-prune.mjs +75 -0
  210. package/scripts/phase-tools-evaluation.mjs +9 -4
  211. package/scripts/pi-model-route.mjs +1 -1
  212. package/scripts/piagent-cli.mjs +51 -2
  213. package/scripts/piagent-dashboard.mjs +27 -4
  214. package/scripts/piagent-installations.mjs +26 -0
  215. package/scripts/piagent-studio.mjs +155 -0
  216. package/scripts/runtime-advisory-exceptions.json +29 -0
  217. package/scripts/setup.sh +2 -2
  218. package/scripts/team-doctor.sh +20 -1
  219. package/scripts/typescript-loader.mjs +6 -0
  220. package/scripts/verify-local.sh +5 -5
  221. package/scripts/verify-release-identity.mjs +1 -1
  222. package/templates/global/settings.json +8 -2
  223. package/templates/project/.pi/project-context.md +3 -3
  224. package/templates/project/AGENTS.md +4 -5
  225. package/docs/piagent-core-reasoning-research-2026-09-06.md +0 -225
  226. package/docs/piagent-core-research-sources-2026-09-06.json +0 -416
  227. package/packages/piagent-webui/dist/client/assets/index-DiKRmCEP.js +0 -176
@@ -0,0 +1,129 @@
1
+ // One model request of a company role: through Studio with the role's grant,
2
+ // on the route the harness chose. Studio answers a failure with a machine
3
+ // code; this says who failed and why, in words. Only refusals that come before
4
+ // the model does any work are asked again: a refusal for capacity waits in
5
+ // line, a brief one (a sign-in being renewed, a lapsed grant) is asked again a
6
+ // few times. Nothing that reached the model is sent twice.
7
+ import { randomUUID } from 'node:crypto';
8
+ import { setTimeout as sleep } from 'node:timers/promises';
9
+ import { describeFailure, failureCode, failureIsAdmissionRefusal, failureIsBriefRefusal, failureReason, unnamedFailureText } from '../runtime/managed-failure.mjs';
10
+
11
+ // How long one request waits in line for a free company model account.
12
+ const CAPACITY_WAIT_MS = 20 * 60_000;
13
+ // A brief refusal (see failureIsBriefRefusal) is asked again up to three times,
14
+ // 3, 6 and 9 s apart.
15
+ const BRIEF_ASKS = 3, BRIEF_PAUSE_MS = 3_000;
16
+
17
+ export function wrapRoleStreams(owner, runtime, role, { provider, verifyGrant }) {
18
+ for (const method of ['stream', 'streamSimple']) {
19
+ const original = runtime[method].bind(runtime);
20
+ runtime[method] = (model, context, options) => {
21
+ // Tools run between two answers: the count names the answer they came from.
22
+ if (role === 'main') owner.mainAnswers = (owner.mainAnswers ?? 0) + 1;
23
+ const out = owner.ai.createAssistantMessageEventStream(), signal = options?.signal;
24
+ void (async () => {
25
+ let wait = null, noted = false, brief = 0, refused = null;
26
+ // The error event and the final message carry the same failure: kept once.
27
+ const unnamed = raw => { noted ||= keepUnnamed(owner, role, raw); return raw; };
28
+ try {
29
+ if (role === 'main' && owner.preflight) throw Error(owner.preflight);
30
+ if (!owner.grant || model.provider !== provider) throw Error('managed-run-required');
31
+ // Studio's answer to a failed request is a machine code: say who
32
+ // failed and why, in words. Only an admission refusal is asked again.
33
+ // A request the member stopped ended, it did not fail; the provider may
34
+ // still report it as an error ("This operation was aborted").
35
+ const told = message => message?.stopReason !== 'error' ? message
36
+ : signal?.aborted ? { ...message, stopReason: 'aborted', errorMessage: 'Request was aborted' }
37
+ : { ...message, errorMessage: describeFailure(role, unnamed(message.errorMessage)) };
38
+ for (;;) {
39
+ // Asked again after a refusal and the grant cannot be renewed (the
40
+ // run was closed): the refusal says why, not the renewal.
41
+ const grant = await owner.broker.request('renew', { role }).catch((error) => { throw refused ? Error(refused) : error; });
42
+ verifyGrant(grant, owner.manifest, role);
43
+ if (role === 'main') owner.grant = grant;
44
+ const route = owner.routes.get(runtime);
45
+ if (!route || grant.provider_model_id !== route.native || grant.provider !== route.provider || grant.model_id !== route.model_id || model.id !== 'agent-watch-auto') throw Error('managed-route-changed');
46
+ const asked = Date.now();
47
+ const stream = original({ ...model, id: route.native }, context, { ...options, apiKey: grant.token, sessionId: grant.role_id, maxRetries: 0,
48
+ headers: { ...options?.headers, 'X-Session-Id': grant.role_id } });
49
+ // Whether Studio admitted the request is known at its first event
50
+ // after "start": until then nothing is passed on, so a refused
51
+ // request leaves no trace in the conversation.
52
+ let opening = [], refusal = null;
53
+ for await (const event of stream) {
54
+ if (opening && event.type === 'start') { opening.push(event); continue; }
55
+ const code = event.type === 'error' ? failureCode(event.error?.errorMessage) : null;
56
+ if (opening && code && !signal?.aborted && (failureIsAdmissionRefusal(code) || failureIsBriefRefusal(code) && brief < BRIEF_ASKS)) { refusal = event.error; break; }
57
+ if (opening) { for (const held of opening) out.push(held); opening = null; wait = capacityWaitEnded(owner, wait, 'admitted'); }
58
+ out.push(event.type === 'error' ? { ...event, ...(signal?.aborted ? { reason: 'aborted' } : {}), error: told(event.error) } : event);
59
+ }
60
+ if (!refusal) { for (const held of opening ?? []) out.push(held); out.end(told(await stream.result())); return; }
61
+ refused = String(refusal.errorMessage ?? '');
62
+ // A brief refusal is asked again after a short pause, without a wait step.
63
+ if (failureIsBriefRefusal(failureCode(refusal.errorMessage))) { brief += 1; await sleep((owner.briefPauseMs ?? BRIEF_PAUSE_MS) * brief, undefined, { signal }); continue; }
64
+ wait = await waitForCapacity(owner, wait, role, failureCode(refusal.errorMessage), signal, Date.now() - asked);
65
+ if (!wait) { out.push({ type: 'error', reason: 'error', error: told(refusal) }); return; }
66
+ }
67
+ } catch (error) {
68
+ // A request the member stopped ended, it did not fail: Pi's providers
69
+ // report it as "aborted", and so does this one.
70
+ const aborted = !!signal?.aborted;
71
+ out.push({ type: 'error', reason: aborted ? 'aborted' : 'error', error: { role: 'assistant', content: [], api: model.api, provider: provider, model: model.id,
72
+ stopReason: aborted ? 'aborted' : 'error', errorMessage: aborted ? 'Request was aborted' : describeFailure(role, unnamed(error?.message)), timestamp: Date.now(),
73
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } } });
74
+ } finally { capacityWaitEnded(owner, wait, signal?.aborted ? 'stopped' : 'ended'); }
75
+ })();
76
+ return out;
77
+ };
78
+ }
79
+ }
80
+
81
+ // A failure no code names is shown as "could not be sent"; its own words are
82
+ // kept, redacted, in the conversation file (never shown) so it can be told
83
+ // apart later. True when it was kept.
84
+ export const UNNAMED_FAILURE_ENTRY = 'agent-watch-failure-detail';
85
+ function keepUnnamed(owner, role, raw) {
86
+ const text = unnamedFailureText(raw);
87
+ if (text === null) return false;
88
+ try { owner.session?.sessionManager?.appendCustomEntry(UNNAMED_FAILURE_ENTRY, { role, text }); return true; }
89
+ catch { return false; /* the failure is still shown */ }
90
+ }
91
+
92
+ // Studio itself holds a refused request up to 30 s while it waits for a slot:
93
+ // one it held is asked again almost at once, so a long wait keeps its place
94
+ // against requests that just arrived. One refused at once (a rate limit) backs
95
+ // off, 2 s doubling to 15 s.
96
+ export function capacityDelayMs(asks, heldMs, random = Math.random()) {
97
+ return heldMs >= 10_000 ? 500 + 1000 * random : Math.min(15_000, 1_000 * 2 ** Math.min(asks, 4)) * (0.75 + random / 2);
98
+ }
99
+
100
+ // Studio refused a request for capacity (every company model account that
101
+ // can serve this conversation is busy): the request waits in line and asks
102
+ // again, shown as a step of the running turn; Stop ends the wait. Each ask
103
+ // is an event, so the WebUI's idle watchdog sees the turn alive. After
104
+ // CAPACITY_WAIT_MS it fails with Studio's reason. Null: stop waiting.
105
+ async function waitForCapacity(owner, wait, role, code, signal, heldMs = 0) {
106
+ if (wait && Date.now() - wait.since >= (owner.capacityWaitMs ?? CAPACITY_WAIT_MS)) { capacityWaitEnded(owner, wait, 'gave-up'); return null; }
107
+ wait ??= { id: randomUUID(), role, since: Date.now(), asks: 0, open: true };
108
+ wait.asks += 1; wait.code = code;
109
+ owner.capacityWaits.add(wait);
110
+ owner.notify({ type: 'managed_capacity_wait', id: wait.id, role, code, state: wait.asks === 1 ? 'start' : 'waiting', waitedMs: Date.now() - wait.since });
111
+ capacityStatus(owner);
112
+ try { await sleep(capacityDelayMs(wait.asks, heldMs), undefined, { signal }); }
113
+ catch (error) { capacityWaitEnded(owner, wait, 'stopped'); throw error; }
114
+ return wait;
115
+ }
116
+ function capacityWaitEnded(owner, wait, outcome) {
117
+ if (!wait?.open) return null;
118
+ wait.open = false; owner.capacityWaits.delete(wait);
119
+ owner.notify({ type: 'managed_capacity_wait', id: wait.id, role: wait.role, code: wait.code, state: 'end', outcome, waitedMs: Date.now() - wait.since });
120
+ capacityStatus(owner);
121
+ return null;
122
+ }
123
+ // The Terminal's working line says who waits and why, while anyone does.
124
+ function capacityStatus(owner) {
125
+ const wait = [...owner.capacityWaits].at(-1);
126
+ const who = wait?.role === 'main' ? 'main agent' : `${wait?.role} subagent`;
127
+ try { owner.session?.extensionRunner?.getUIContext?.()?.setWorkingMessage?.(wait ? `Agent Watch ${who}: waiting for a free company model account (${failureReason(wait.code)}). Esc stops.` : undefined); }
128
+ catch { /* no Terminal */ }
129
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const INSTRUCTION_FILES = ['AGENTS.override.md', 'AGENTS.md', 'AGENTS.MD', 'CLAUDE.md', 'CLAUDE.MD'];
5
+ // Project instructions from the project folder up to its repository root, as
6
+ // context only (never as permission). Symlinks and large files are skipped;
7
+ // personal/global instruction files are not read.
8
+ export function projectInstructions(cwd, top = cwd) {
9
+ const files = [];
10
+ for (let dir = cwd, depth = 0; depth < 12; depth += 1) {
11
+ for (const name of INSTRUCTION_FILES) {
12
+ const file = path.join(dir, name);
13
+ try {
14
+ const stat = fs.lstatSync(file);
15
+ if (stat.isFile() && stat.size <= 64 * 1024) { files.unshift({ path: file, content: fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '') }); break; }
16
+ } catch { /* absent */ }
17
+ }
18
+ if (dir === top || path.dirname(dir) === dir || !(dir + path.sep).startsWith(top + path.sep)) break;
19
+ dir = path.dirname(dir);
20
+ }
21
+ return files.slice(-4);
22
+ }
23
+
24
+ // Managed sessions never consult the personal trust store or execute resources
25
+ // discovered in a project.
26
+ export function managedResourceLoader(api, { systemPrompt, extensions = [], agentsFiles = [] } = {}) {
27
+ const runtime = api.createExtensionRuntime();
28
+ const empty = Object.freeze([]);
29
+ return Object.freeze({
30
+ getExtensions: () => ({ extensions, errors: empty, runtime }),
31
+ getSkills: () => ({ skills: empty, diagnostics: empty }),
32
+ getPrompts: () => ({ prompts: empty, diagnostics: empty }),
33
+ getThemes: () => ({ themes: empty, diagnostics: empty }),
34
+ getAgentsFiles: () => ({ agentsFiles: agentsFiles.map((file) => ({ ...file })) }),
35
+ getSystemPrompt: () => typeof systemPrompt === 'function' ? systemPrompt() : systemPrompt,
36
+ getSystemPromptSource: () => undefined,
37
+ getAppendSystemPrompt: () => empty,
38
+ getAppendSystemPromptSources: () => empty,
39
+ extendResources: () => { throw new Error('managed-project-resources-disabled'); },
40
+ reload: async () => {},
41
+ });
42
+ }
@@ -0,0 +1,27 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { managedGit } from './toolchain.mjs';
5
+ // Executed only through the tool Seatbelt boundary; repo-local Git config cannot
6
+ // grant a hook, textconv or credential helper access to the trusted runtime.
7
+ const git = args => execFileSync(managedGit(), ['--no-pager', ...args], { encoding: 'utf8', maxBuffer: 160000, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1' } });
8
+ const patch = git(['diff', '--binary', '--no-ext-diff', '--no-textconv', 'HEAD', '--', '.']);
9
+ const paths = git(['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean).sort();
10
+ if (paths.length > 100) throw Error('review-untracked-limit');
11
+ const untracked = paths.map(name => {
12
+ const stat = fs.lstatSync(name);
13
+ if (stat.isSymbolicLink()) return { path: name, mode: 'symlink', target: fs.readlinkSync(name) };
14
+ if (!stat.isFile() || stat.size > 120000) throw Error('review-file-limit');
15
+ // Text goes as text, so the reviewer can read new files; binary as base64.
16
+ const data = fs.readFileSync(name);
17
+ let text = null;
18
+ if (!data.includes(0)) { try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(data); } catch { text = null; } }
19
+ return text === null ? { path: name, mode: stat.mode & 0o777, contents: data.toString('base64'), encoding: 'base64' } : { path: name, mode: stat.mode & 0o777, contents: text, encoding: 'utf8' };
20
+ });
21
+ const snapshot = JSON.stringify({ patch, untracked });
22
+ if (Buffer.byteLength(snapshot) > 120000) throw Error('review-patch-limit');
23
+ // With a part number the snapshot is sent in base64 parts small enough for the
24
+ // shell tool's output limit, each headed by the whole snapshot's digest.
25
+ const part = process.argv[2], bytes = Buffer.from(snapshot), SIZE = 30000;
26
+ if (part === undefined) process.stdout.write(snapshot);
27
+ else process.stdout.write(`${createHash('sha256').update(bytes).digest('hex')} ${Math.max(1, Math.ceil(bytes.length / SIZE))}\n${bytes.subarray(Number(part) * SIZE, (Number(part) + 1) * SIZE).toString('base64')}`);
@@ -0,0 +1,12 @@
1
+ const DENIAL = /\b(?:EPERM|EACCES)\b|operation not permitted|permission denied/i;
2
+ const OFFLINE = /ENOTFOUND|EAI_AGAIN|Could not resolve host|getaddrinfo|npm error network|Failed to establish a new connection/i;
3
+ // `network`: the member approved this command and it ran with network, so a
4
+ // name that does not resolve is the machine's network, not the sandbox.
5
+ export function sandboxDiagnostic(message, { network = false } = {}) {
6
+ const text=String(message);
7
+ if(network&&!DENIAL.test(text)&&OFFLINE.test(text))return 'managed-network-unreachable: Lệnh đã được duyệt và chạy có mạng nhưng không tới được host (máy đang offline, cần VPN/proxy công ty, hoặc sai tên host). Không xin duyệt lại cùng lệnh; báo người dùng lỗi mạng này.\n'+text;
8
+ if(!DENIAL.test(text)&&OFFLINE.test(text))return 'managed-network-blocked: Lệnh bash thường không có mạng trong chế độ công ty. Nếu lệnh thật sự cần mạng (cài package, tải dữ liệu), dùng run_with_network để người dùng duyệt đúng lệnh đó.\n'+text;
9
+ if(!DENIAL.test(text))return text;
10
+ return 'managed-sandbox-denied: Công cụ bị giới hạn quyền trong chế độ công ty (hoặc quyền file của hệ điều hành). Keychain và file chứa credential như .env/.npmrc bị chặn. Lệnh cần mạng (cài package, tải dữ liệu) dùng run_with_network để xin duyệt; git fetch origin dùng fetch_origin. Không tự chạy lại bên ngoài sandbox.\n'+text;
11
+ }
12
+
@@ -0,0 +1,93 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ // Built-in grep/find for machines without ripgrep/fd (and where they could not
6
+ // be downloaded). Runs inside the sandbox worker. Files come from git (so
7
+ // .gitignore applies) or a bounded walk; unreadable files, such as credential
8
+ // files the sandbox denies, are skipped. Output mirrors Pi's grep/find.
9
+ const SKIP = new Set(['.git', 'node_modules', '.hg', '.svn']);
10
+ const MAX_FILES = 20_000, MAX_FILE_BYTES = 2 * 1024 * 1024, MAX_LINE = 500, MAX_OUTPUT = 50 * 1024;
11
+
12
+ export function globToRegExp(glob) {
13
+ let out = '', index = 0;
14
+ while (index < glob.length) {
15
+ const char = glob[index];
16
+ if (char === '*' && glob[index + 1] === '*') { out += glob[index + 2] === '/' ? '(?:.*/)?' : '.*'; index += glob[index + 2] === '/' ? 3 : 2; continue; }
17
+ if (char === '*') out += '[^/]*';
18
+ else if (char === '?') out += '[^/]';
19
+ else if (char === '{') { const end = glob.indexOf('}', index); if (end > index) { out += '(?:' + glob.slice(index + 1, end).split(',').map(part => part.replace(/[.+^$()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')).join('|') + ')'; index = end + 1; continue; } out += '\\{'; }
20
+ else out += char.replace(/[.+^$()|[\]\\]/g, '\\$&');
21
+ index += 1;
22
+ }
23
+ return new RegExp('^' + out + '$');
24
+ }
25
+ const matcher = glob => {
26
+ if (!glob) return () => true;
27
+ const regex = globToRegExp(glob.replace(/^\.\//, ''));
28
+ return glob.includes('/') ? file => regex.test(file) : file => regex.test(file) || regex.test(path.basename(file));
29
+ };
30
+
31
+ function listFiles(root) {
32
+ try {
33
+ const out = execFileSync('git', ['-C', root, 'ls-files', '-co', '--exclude-standard', '-z'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
34
+ return out.split('\0').filter(Boolean).slice(0, MAX_FILES);
35
+ } catch { /* not a repository: walk */ }
36
+ const files = [];
37
+ const walk = dir => {
38
+ let entries = [];
39
+ try { entries = fs.readdirSync(path.join(root, dir), { withFileTypes: true }); } catch { return; }
40
+ for (const entry of entries) {
41
+ if (files.length >= MAX_FILES) return;
42
+ const rel = dir ? `${dir}/${entry.name}` : entry.name;
43
+ if (entry.isDirectory()) { if (!SKIP.has(entry.name)) walk(rel); }
44
+ else if (entry.isFile()) files.push(rel);
45
+ }
46
+ };
47
+ walk('');
48
+ return files;
49
+ }
50
+
51
+ function scope(cwd, target = '.') {
52
+ const absolute = path.resolve(cwd, target);
53
+ if (fs.statSync(absolute).isFile()) return { root: path.dirname(absolute), only: path.basename(absolute), base: absolute };
54
+ return { root: absolute, only: null, base: absolute };
55
+ }
56
+
57
+ export function fallbackGrep(cwd, { pattern, path: target, glob, ignoreCase, literal, context = 0, limit = 100 }) {
58
+ const { root, only } = scope(cwd, target);
59
+ const source = literal ? String(pattern).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : String(pattern);
60
+ const regex = new RegExp(source, ignoreCase ? 'i' : '');
61
+ const accept = matcher(glob), cap = Math.max(1, Math.min(1000, Number(limit) || 100)), around = Math.max(0, Math.min(10, Number(context) || 0));
62
+ const lines = []; let matches = 0, bytes = 0;
63
+ for (const rel of only ? [only] : listFiles(root)) {
64
+ if (matches >= cap || bytes > MAX_OUTPUT) break;
65
+ if (!accept(rel)) continue;
66
+ const file = path.join(root, rel), shown = path.relative(cwd, file) || rel;
67
+ let text;
68
+ try { if (fs.statSync(file).size > MAX_FILE_BYTES) continue; text = fs.readFileSync(file, 'utf8'); } catch { continue; }
69
+ if (text.includes('\0')) continue;
70
+ const rows = text.split(/\r?\n/);
71
+ for (let index = 0; index < rows.length && matches < cap; index += 1) {
72
+ if (!regex.test(rows[index])) continue;
73
+ matches += 1;
74
+ for (let at = Math.max(0, index - around); at <= Math.min(rows.length - 1, index + around); at += 1) {
75
+ const line = `${shown}${at === index ? ':' : '-'}${at + 1}${at === index ? ':' : '-'} ${rows[at].slice(0, MAX_LINE)}`;
76
+ lines.push(line); bytes += line.length;
77
+ }
78
+ }
79
+ }
80
+ const notice = matches >= cap ? `\n\n[${cap} matches limit reached. Use limit=${cap * 2} for more, or refine pattern]` : '';
81
+ return { content: [{ type: 'text', text: matches ? lines.join('\n') + notice : 'No matches found' }], details: matches >= cap ? { matchLimitReached: cap } : undefined };
82
+ }
83
+
84
+ export function fallbackFind(cwd, { pattern, path: target, limit = 1000 }) {
85
+ const { root } = scope(cwd, target);
86
+ const accept = matcher(String(pattern)), cap = Math.max(1, Math.min(5000, Number(limit) || 1000));
87
+ const found = [];
88
+ for (const rel of listFiles(root)) {
89
+ if (found.length >= cap) break;
90
+ if (accept(rel)) found.push(path.relative(cwd, path.join(root, rel)) || rel);
91
+ }
92
+ return { content: [{ type: 'text', text: found.length ? found.join('\n') : 'No files found matching pattern' }], details: undefined };
93
+ }