@piagent/platform 1.38.2 → 1.40.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 (80) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +12 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +12 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +13 -1
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/whats-new.html +13 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  56. package/package.json +1 -1
  57. package/packages/piagent-core/managed/request-stream.mjs +3 -0
  58. package/packages/piagent-core/managed/workspace-projects.mjs +61 -0
  59. package/packages/piagent-core/managed/workspace-report.mjs +72 -0
  60. package/packages/piagent-core/package.json +1 -1
  61. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DtEwqme0.js → MarkdownMessage-BpoF2k2-.js} +1 -1
  62. package/packages/piagent-webui/dist/client/assets/{TerminalPanel-C3vkzjQ6.js → TerminalPanel-Dg3eghy4.js} +2 -2
  63. package/packages/piagent-webui/dist/client/assets/{TerminalView-TNjhBGfa.js → TerminalView-Dw9xqakv.js} +1 -1
  64. package/packages/piagent-webui/dist/client/assets/{WorkspaceCodeEditor-BAhYwQNM.js → WorkspaceCodeEditor-DtwXZAgK.js} +1 -1
  65. package/packages/piagent-webui/dist/client/assets/{WorkspaceExplorer-Coh_GWCO.js → WorkspaceExplorer-WS-yLlnb.js} +3 -3
  66. package/packages/piagent-webui/dist/client/assets/{WorkspaceQuickOpen-Bn4miZq4.js → WorkspaceQuickOpen-CYCY-s44.js} +1 -1
  67. package/packages/piagent-webui/dist/client/assets/{index-a-perpOX.js → index-BgJgso6j.js} +18 -18
  68. package/packages/piagent-webui/dist/client/assets/{tab-close-nAxrK7Lq.js → tab-close-CqPDs-w5.js} +1 -1
  69. package/packages/piagent-webui/dist/client/index.html +1 -1
  70. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  71. package/packages/piagent-webui/gateway/managed-projection.ts +7 -1
  72. package/packages/piagent-webui/gateway/session-catalog.ts +3 -2
  73. package/packages/piagent-webui/gateway/ui-preferences-store.ts +45 -0
  74. package/packages/piagent-webui/server/loopback-server.ts +2 -1
  75. package/packages/piagent-webui/server/member-routes.ts +20 -0
  76. package/packages/piagent-webui/shared/project-warning-copy.ts +21 -0
  77. package/schemas/piagent-webui/session-catalog-v1.schema.json +1 -1
  78. package/scripts/check-runtime-advisories.mjs +28 -3
  79. package/scripts/company-terminal.mjs +2 -0
  80. package/scripts/runtime-advisory-exceptions.json +26 -1
@@ -1 +1 @@
1
- import{D as e}from"./ui-preferences-BO3AV7QE.js";import{m as t}from"./Alert-DGWhP2MM.js";import{E as n}from"./index-a-perpOX.js";var r=e(),i=t((0,r.jsx)(`path`,{d:`M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z`}),`Close`);function a(e,t,a){let o=e=>{e.stopPropagation(),e.preventDefault(),a()};return(0,r.jsxs)(n,{component:`span`,sx:{display:`inline-flex`,alignItems:`center`,gap:.75},children:[(0,r.jsx)(`span`,{children:e}),(0,r.jsx)(n,{component:`span`,role:`button`,tabIndex:0,"aria-label":t,title:t,onClick:o,onMouseDown:e=>e.stopPropagation(),onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&o(e)},sx:{display:`inline-grid`,placeItems:`center`,width:18,height:18,borderRadius:`2px`,opacity:.7,"&:hover":{opacity:1,bgcolor:`action.hover`}},children:(0,r.jsx)(i,{sx:{fontSize:14}})})]})}export{i as n,a as t};
1
+ import{D as e}from"./ui-preferences-BO3AV7QE.js";import{m as t}from"./Alert-DGWhP2MM.js";import{E as n}from"./index-BgJgso6j.js";var r=e(),i=t((0,r.jsx)(`path`,{d:`M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z`}),`Close`);function a(e,t,a){let o=e=>{e.stopPropagation(),e.preventDefault(),a()};return(0,r.jsxs)(n,{component:`span`,sx:{display:`inline-flex`,alignItems:`center`,gap:.75},children:[(0,r.jsx)(`span`,{children:e}),(0,r.jsx)(n,{component:`span`,role:`button`,tabIndex:0,"aria-label":t,title:t,onClick:o,onMouseDown:e=>e.stopPropagation(),onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&o(e)},sx:{display:`inline-grid`,placeItems:`center`,width:18,height:18,borderRadius:`2px`,opacity:.7,"&:hover":{opacity:1,bgcolor:`action.hover`}},children:(0,r.jsx)(i,{sx:{fontSize:14}})})]})}export{i as n,a as t};
@@ -7,7 +7,7 @@
7
7
  <meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
8
8
  <meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
9
9
  <title>Piagent WebUI</title>
10
- <script type="module" crossorigin src="/assets/index-a-perpOX.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-BgJgso6j.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/ui-preferences-BO3AV7QE.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/useId-DEefKUAT.js">
@@ -54,6 +54,7 @@ import { ProviderAuthBroker } from "./provider-auth-broker.ts";
54
54
  import { McpAuthBroker } from "./mcp-auth-broker.ts";
55
55
  import { AgentStatusFiles } from "./agent-status-files.ts";
56
56
  import type { ScopedBrokerRouter, RuntimeFactory } from "./session-runtime-factory.ts";
57
+ import { UiPreferencesStore } from "./ui-preferences-store.ts";
57
58
 
58
59
  function unavailable(reasonCode: string) {
59
60
  return { status: "unavailable" as const, version: null, reasonCode };
@@ -292,7 +293,9 @@ export async function startPiagentGateway(options: {
292
293
  const sourceReverts = options.managed ? new ManagedSourceRevertSessions({ root: state.root, key, gatewayInstanceRef, runtimes,
293
294
  managedSource: options.managed.managedSource, changed: sessionRef => events.publish("session.changed", { sessionRef }) }) : new SourceRevertSessions({ root: state.root, key, runtimes, managed: !!options.managed,
294
295
  provider: sessionRef => inspections.provider(sessionRef), changed: sessionRef => events.publish("session.changed", { sessionRef }) });
296
+ const uiPreferences = new UiPreferencesStore(state.root);
295
297
  loopback = await startLoopbackServer({
298
+ uiPreferences: { read: () => uiPreferences.read(), write: value => uiPreferences.write(value) },
296
299
  checkpoints: {
297
300
  state: sessionRef => company(sessionRef) ? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/checkpoints`) : checkpoints.state(sessionRef),
298
301
  preview: (sessionRef, target) => company(sessionRef) ? relay!.json("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/checkpoints/${encodeURIComponent(target)}`) : checkpoints.preview(sessionRef, target),
@@ -36,7 +36,13 @@ export function managedProjection(context: any, entries: any[]) {
36
36
  const permission = chosen === 'bypass' ? (bypassAllowed ? 'bypass' : 'trusted-full-access') : chosen === 'trusted-full-access' ? 'trusted-full-access' : 'workspace-write';
37
37
  // Fast mode, only where the conversation's Harness allows it (fast-mode.mjs).
38
38
  const fast = last('agent-watch-fast');
39
+ // Projects Studio says do not belong to the key's team (workspace-report.mjs);
40
+ // a local project carries its folder name.
41
+ const project = last('agent-watch-project'), kinds = ['other_team', 'unlisted', 'unregistered', 'no_repository'];
42
+ const projectWarnings = Array.isArray(project?.warnings) ? project.warnings.filter((w: any) => kinds.includes(w?.kind) && typeof w.repo === 'string' && w.repo.trim())
43
+ .slice(0, 6).map((w: any) => ({ repo: w.repo.trim().slice(0, 200), kind: w.kind, ...(typeof w.name === 'string' && w.name.trim() ? { name: w.name.trim().slice(0, 80) } : {}) })) : [];
44
+ const projectWarning = projectWarnings.length ? { warnings: projectWarnings, ...(typeof project.team === 'string' && project.team.trim() ? { team: project.team.trim().slice(0, 80) } : {}) } : null;
39
45
  return {modelLabel:'agent-watch-auto', managedPermission: permission, ...(bypassAllowed ? {managedBypass: true as const} : {}), ...(fast?.allowed === true ? {managedFast: fast.enabled === true ? 'on' as const : 'off' as const} : {}), managedHelpers:{active:Number.isInteger(counter?.active) && counter.active>=0 && counter.active<=maximum ? counter.active : 0, maximum},
40
46
  ...(Array.isArray(capabilities)?{managedThinkingLevels:levels.filter(level=>capabilities.includes(level))}:{}),
41
- ...(steps ? {managedPlan:{steps}} : {}), ...(process ? {managedProcess:process} : {}), ...(turnEnd ? {managedTurnEnd:turnEnd} : {}), ...(review ? {managedReview:review} : {})};
47
+ ...(steps ? {managedPlan:{steps}} : {}), ...(process ? {managedProcess:process} : {}), ...(turnEnd ? {managedTurnEnd:turnEnd} : {}), ...(review ? {managedReview:review} : {}), ...(projectWarning ? {managedProjectWarning:projectWarning} : {})};
42
48
  }
@@ -49,7 +49,7 @@ function catalogRef(key: Buffer, namespace: string, value: string): string {
49
49
  }
50
50
 
51
51
  export type SessionOwnerProjection = Pick<SessionRow, "state" | "liveState" | "composerAvailable" | "needsAttention" | "owner" | "reasonCode">;
52
- export type SessionOptionProjection = Pick<SessionRow, "modelLabel" | "thinkingLevel" | "managedHelpers" | "managedThinkingLevels" | "managedPermission" | "managedBypass" | "managedFast" | "managedPlan" | "managedProcess" | "managedTurnEnd" | "managedReview">;
52
+ export type SessionOptionProjection = Pick<SessionRow, "modelLabel" | "thinkingLevel" | "managedHelpers" | "managedThinkingLevels" | "managedPermission" | "managedBypass" | "managedFast" | "managedPlan" | "managedProcess" | "managedTurnEnd" | "managedReview" | "managedProjectWarning">;
53
53
 
54
54
  function display(value: unknown, maximum: number, fallback: string): string {
55
55
  const clean = redactSensitiveText(String(value ?? "")).text
@@ -141,6 +141,7 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
141
141
  ...(sessionOptions?.managedProcess ? {managedProcess:sessionOptions.managedProcess} : {}),
142
142
  ...(sessionOptions?.managedTurnEnd ? {managedTurnEnd:sessionOptions.managedTurnEnd} : {}),
143
143
  ...(sessionOptions?.managedReview ? {managedReview:sessionOptions.managedReview} : {}),
144
+ ...(sessionOptions?.managedProjectWarning ? {managedProjectWarning:sessionOptions.managedProjectWarning} : {}),
144
145
  modelLabel: sessionOptions?.modelLabel ?? null,
145
146
  thinkingLevel: sessionOptions?.thinkingLevel ?? "unknown",
146
147
  contextUsage: { usedTokens: null, contextWindow: null, ratio: null, state: "unknown" },
@@ -149,7 +150,7 @@ function row(key: Buffer, info: PiSessionInfo, metadata: SessionMetadata | undef
149
150
  ? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "released" }
150
151
  : ownership?.owner ?? { kind: "none", ownerEpoch: null, gatewayInstanceRef: null, runtimeInstanceRef: null, continuity: "unknown" },
151
152
  sessionRevision: revision(key, [info.path, info.modified.toISOString(), info.messageCount, info.name ?? null, metadata?.revision ?? null,
152
- sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedBypass ?? null, sessionOptions?.managedFast ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess, sessionOptions?.managedTurnEnd, sessionOptions?.managedReview,
153
+ sessionOptions?.modelLabel ?? null, sessionOptions?.thinkingLevel ?? "unknown", sessionOptions?.managedHelpers, sessionOptions?.managedThinkingLevels, sessionOptions?.managedPermission ?? null, sessionOptions?.managedBypass ?? null, sessionOptions?.managedFast ?? null, sessionOptions?.managedPlan, sessionOptions?.managedProcess, sessionOptions?.managedTurnEnd, sessionOptions?.managedReview, sessionOptions?.managedProjectWarning,
153
154
  gitBranch, gitRepositories,
154
155
  metadata?.archived ? "archived" : ownership ? [ownership.state, ownership.liveState, ownership.owner.kind,
155
156
  ownership.owner.ownerEpoch, ownership.owner.runtimeInstanceRef, ownership.reasonCode] : "offline"]),
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+
5
+ // The dashboard's language and colour theme, kept by the Gateway so they
6
+ // survive a restart: the browser's own storage belongs to one origin, and the
7
+ // dashboard's port changes each time it starts.
8
+ export type UiPreferences = { locale?: "vi" | "en"; themeId?: string };
9
+ const MAX_BYTES = 4096, THEME = /^[a-z0-9][a-z0-9-]{0,39}$/;
10
+
11
+ function valid(value: unknown): UiPreferences | null {
12
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
13
+ const { locale, themeId, ...rest } = value as Record<string, unknown>;
14
+ if (Object.keys(rest).length) return null;
15
+ if (locale !== undefined && locale !== "vi" && locale !== "en") return null;
16
+ if (themeId !== undefined && (typeof themeId !== "string" || !THEME.test(themeId))) return null;
17
+ return { ...(locale ? { locale: locale as "vi" | "en" } : {}), ...(themeId ? { themeId: themeId as string } : {}) };
18
+ }
19
+
20
+ export class UiPreferencesStore {
21
+ readonly file: string;
22
+ #root: string;
23
+ constructor(root: string) { this.#root = root; this.file = path.join(root, "ui-preferences.json"); }
24
+
25
+ read(): UiPreferences {
26
+ try {
27
+ const stat = fs.lstatSync(this.file);
28
+ if (!stat.isFile() || stat.size > MAX_BYTES) return {};
29
+ return valid(JSON.parse(fs.readFileSync(this.file, "utf8"))) ?? {};
30
+ } catch { return {}; }
31
+ }
32
+
33
+ // Merges what the browser sent; anything else is refused unchanged.
34
+ write(value: unknown): UiPreferences {
35
+ const change = valid(value);
36
+ if (!change) throw new Error("preferences-invalid");
37
+ const next = { ...this.read(), ...change };
38
+ const temporary = path.join(this.#root, `.ui-preferences-${process.pid}-${randomBytes(8).toString("hex")}.tmp`);
39
+ const descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
40
+ try { fs.writeFileSync(descriptor, `${JSON.stringify(next)}\n`); fs.fsyncSync(descriptor); }
41
+ finally { fs.closeSync(descriptor); }
42
+ fs.renameSync(temporary, this.file);
43
+ return next;
44
+ }
45
+ }
@@ -3,7 +3,7 @@ import http, { type ServerResponse } from "node:http";
3
3
  import { randomBytes } from "node:crypto";
4
4
 
5
5
  import { errorResponse, applySecurityHeaders, jsonResponse, requestBody } from "./http-security.ts";
6
- import { routeMemberRequest, type UpdateRoutes } from "./member-routes.ts";
6
+ import { routeMemberRequest, type UiPreferenceRoutes, type UpdateRoutes } from "./member-routes.ts";
7
7
  import { routeWorkspaceWrite, type WorkspaceWriteRoutes } from "./workspace-write-routes.ts";
8
8
  import { routeCheckpoint, type CheckpointRoutes } from "./checkpoint-routes.ts";
9
9
  import { ReadModelNotFound, type WebUiReadModelProvider } from "./read-model-provider.ts";
@@ -81,6 +81,7 @@ export async function startLoopbackServer(options: {
81
81
  listBranches?: (projectRef: string, repository: string | null) => unknown | Promise<unknown>;
82
82
  switchBranch?: (projectRef: string, request: unknown) => unknown | Promise<unknown>;
83
83
  updates?: UpdateRoutes;
84
+ uiPreferences?: UiPreferenceRoutes;
84
85
  bootstrapTtlMs?: number;
85
86
  sessionTtlMs?: number;
86
87
  // Requests a minute from one address before it has a browser session (the
@@ -23,6 +23,9 @@ export type UpdateRoutes = {
23
23
  apply(request: unknown): unknown | Promise<unknown>;
24
24
  };
25
25
 
26
+ // The dashboard's language and theme, kept across restarts (the Gateway only).
27
+ export type UiPreferenceRoutes = { read(): unknown | Promise<unknown>; write(value: unknown): unknown | Promise<unknown> };
28
+
26
29
  type Context = {
27
30
  request: IncomingMessage; response: ServerResponse; url: URL; origin: string; requestOrigin: string | undefined;
28
31
  auth: SessionAuthority; now: number; consumeControl(sessionId: string, now: number): boolean;
@@ -40,6 +43,7 @@ type Options = {
40
43
  listBranches?: (projectRef: string, repository: string | null) => unknown | Promise<unknown>;
41
44
  switchBranch?: (projectRef: string, request: unknown) => unknown | Promise<unknown>;
42
45
  updates?: UpdateRoutes;
46
+ uiPreferences?: UiPreferenceRoutes;
43
47
  };
44
48
 
45
49
  // The checks every change shares; a refusal is answered here and false returned.
@@ -137,6 +141,22 @@ export async function routeMemberRequest(context: Context, options: Options): Pr
137
141
  return true;
138
142
  }
139
143
 
144
+ if (options.uiPreferences && url.pathname === "/api/v1/preferences") {
145
+ const store = options.uiPreferences;
146
+ if (request.method === "GET") {
147
+ if (!auth.authenticate(request)) { errorResponse(response, 401, "authentication-required"); return true; }
148
+ try { jsonResponse(response, 200, await store.read()); } catch { errorResponse(response, 503, "preferences-unavailable"); }
149
+ return true;
150
+ }
151
+ if (request.method !== "POST") { errorResponse(response, 405, "method-not-allowed"); return true; }
152
+ if (!authorizeChange(context)) return true;
153
+ const body = await jsonBody(context, "preferences-invalid");
154
+ if (!body.ok) return true;
155
+ try { jsonResponse(response, 200, await store.write(body.value)); }
156
+ catch (error) { const invalid = error instanceof Error && error.message === "preferences-invalid"; errorResponse(response, invalid ? 400 : 503, invalid ? "preferences-invalid" : "preferences-unavailable"); }
157
+ return true;
158
+ }
159
+
140
160
  if (!options.updates || !url.pathname.startsWith("/api/v1/updates")) return false;
141
161
  const updates = options.updates;
142
162
  if (request.method === "GET" && url.pathname === "/api/v1/updates") {
@@ -0,0 +1,21 @@
1
+ // The line a member reads when Studio says their company conversation works
2
+ // in a project that does not belong to the key's team ("agent-watch-project"),
3
+ // in the dashboard and in the company Terminal. A local project (no readable
4
+ // remote) is named by its folder.
5
+ import type { CopyLocale } from "./company-copy.ts";
6
+
7
+ export type ProjectWarningItem = { repo: string; kind: "other_team" | "unlisted" | "unregistered" | "no_repository"; name?: string };
8
+
9
+ const label = (item: ProjectWarningItem) => item.name ?? item.repo;
10
+
11
+ export function projectWarningText(warnings: readonly ProjectWarningItem[], team: string | null | undefined, locale: CopyLocale): string {
12
+ const vi = locale === "vi", name = team ? ` ${team}` : "", told = vi ? " Admin Studio đã được báo." : " Studio administrators were told.";
13
+ const of = (kind: ProjectWarningItem["kind"]) => warnings.filter(item => item.kind === kind).map(label).join(", ");
14
+ const parts: string[] = [];
15
+ const other = of("other_team"), unlisted = of("unlisted"), unregistered = of("unregistered"), none = of("no_repository");
16
+ if (other) parts.push(vi ? `${other} thuộc team khác, không phải team${name} của key này.` : `${other} belongs to another team, not this key's team${name}.`);
17
+ if (unlisted) parts.push(vi ? `${unlisted} chưa có trong danh sách project của team${name}.` : `${unlisted} is not in the project list of team${name}.`);
18
+ if (unregistered) parts.push(vi ? `${unregistered} chưa được đăng ký cho team${name}.` : `${unregistered} is not registered to team${name}.`);
19
+ if (none) parts.push(vi ? `Không xác định được project của thư mục ${none}.` : `The project of folder ${none} could not be identified.`);
20
+ return parts.join(" ") + told;
21
+ }
@@ -92,7 +92,7 @@
92
92
  "composerAvailable": { "type": "boolean" },
93
93
  "needsAttention": { "type": "boolean" },
94
94
  "managedHelpers": { "type": "object", "additionalProperties": false, "required": ["active", "maximum"], "properties": {"active": {"type": ["integer", "null"], "minimum": 0, "maximum": 4}, "maximum": {"type": "integer", "minimum": 0, "maximum": 4}} },
95
- "managedThinkingLevels": {"type": "array", "uniqueItems": true, "maxItems": 7, "items": {"enum": ["off","minimal","low","medium","high","xhigh","max"]}}, "managedPermission": {"enum": ["workspace-write","trusted-full-access","bypass"]}, "managedBypass": {"const": true}, "managedFast": {"enum": ["off","on"]},
95
+ "managedThinkingLevels": {"type": "array", "uniqueItems": true, "maxItems": 7, "items": {"enum": ["off","minimal","low","medium","high","xhigh","max"]}}, "managedPermission": {"enum": ["workspace-write","trusted-full-access","bypass"]}, "managedBypass": {"const": true}, "managedProjectWarning": {"type": "object", "additionalProperties": false, "required": ["warnings"], "properties": {"team": {"type": "string", "minLength": 1, "maxLength": 80}, "warnings": {"type": "array", "minItems": 1, "maxItems": 6, "items": {"type": "object", "additionalProperties": false, "required": ["repo", "kind"], "properties": {"repo": {"type": "string", "minLength": 1, "maxLength": 200}, "name": {"type": "string", "minLength": 1, "maxLength": 80}, "kind": {"enum": ["other_team", "unlisted", "unregistered", "no_repository"]}}}}}}, "managedFast": {"enum": ["off","on"]},
96
96
  "managedPlan": { "type": "object", "additionalProperties": false, "required": ["steps"], "properties": {
97
97
  "steps": { "type": "array", "minItems": 1, "maxItems": 30, "items": { "type": "object", "additionalProperties": false, "required": ["step", "status"], "properties": {
98
98
  "step": { "type": "string", "minLength": 1, "maxLength": 200 }, "status": { "enum": ["pending", "in_progress", "completed", "deferred"] } } } } } },
@@ -7,7 +7,11 @@
7
7
  // The only exceptions are listed in runtime-advisory-exceptions.json, each an
8
8
  // owner decision for one advisory at one package path in the tree, with a day
9
9
  // it stops applying. The same advisory anywhere else, any other advisory, and
10
- // an exception past its day all block. Its tests check every entry.
10
+ // an exception past its day all block. Its tests check every entry. A package
11
+ // flagged only because it depends on flagged packages (no advisory of its
12
+ // own, `via` names packages) is carried: it passes when every package it
13
+ // names is in the report with all of its advisories accepted, and blocks
14
+ // otherwise.
11
15
  import fs from "node:fs";
12
16
 
13
17
  function readStdin() {
@@ -50,12 +54,14 @@ const { exceptions } = JSON.parse(fs.readFileSync(process.env.PIAGENT_ADVISORY_E
50
54
  // A test seam for the day exceptions are judged on; the gate uses today (UTC).
51
55
  const today = process.env.PIAGENT_ADVISORY_TODAY ?? new Date().toISOString().slice(0, 10);
52
56
 
53
- const blocking = [], accepted = [];
57
+ const blocking = [], accepted = [], pending = [], carried = [];
54
58
  for (const [name, vulnerability] of Object.entries(report.vulnerabilities ?? {})) {
55
59
  if (!["moderate", "high", "critical"].includes(vulnerability.severity)) continue;
56
60
  const ids = advisoryIdsFor(vulnerability);
57
61
  if (ids.length === 0) {
58
- blocking.push({ name, severity: vulnerability.severity, id: "(no advisory id)" });
62
+ const via = vulnerability.via ?? [];
63
+ if (via.length > 0 && via.every((item) => typeof item === "string")) pending.push({ name, severity: vulnerability.severity, via });
64
+ else blocking.push({ name, severity: vulnerability.severity, id: "(no advisory id)" });
59
65
  continue;
60
66
  }
61
67
  const nodes = Array.isArray(vulnerability.nodes) ? vulnerability.nodes : [];
@@ -67,10 +73,29 @@ for (const [name, vulnerability] of Object.entries(report.vulnerabilities ?? {})
67
73
  }
68
74
  }
69
75
 
76
+ // Carried packages may chain (a via b via c): settle them until nothing changes;
77
+ // whatever is left names a package that is not accepted.
78
+ const cleared = new Set(accepted.map((entry) => entry.name).filter((name) => !blocking.some((entry) => entry.name === name)));
79
+ for (let changed = true; changed;) {
80
+ changed = false;
81
+ for (const entry of [...pending]) {
82
+ if (!entry.via.every((name) => cleared.has(name))) continue;
83
+ pending.splice(pending.indexOf(entry), 1);
84
+ carried.push(entry);
85
+ cleared.add(entry.name);
86
+ changed = true;
87
+ }
88
+ }
89
+ for (const entry of pending) blocking.push({ name: entry.name, severity: entry.severity, id: `(no advisory id, through ${entry.via.join(", ")})` });
90
+
70
91
  for (const entry of accepted) {
71
92
  console.log(`ACCEPTED: ${entry.severity} advisory ${entry.id} in ${entry.name} at ${entry.exception.node} until ${entry.exception.until} (owner decision ${entry.exception.decided})`);
72
93
  }
73
94
 
95
+ for (const entry of carried) {
96
+ console.log(`CARRIED: ${entry.severity} in ${entry.name} only through accepted advisories of ${entry.via.join(", ")}`);
97
+ }
98
+
74
99
  const failures = blocking.map(
75
100
  (entry) => `${entry.severity} advisory ${entry.id} in ${entry.name} blocks the supported runtime`
76
101
  + (entry.expired ? ` (its exception ended ${entry.expired})` : "")
@@ -16,6 +16,7 @@ import { memoryOn } from '../packages/piagent-core/managed/memory.mjs';
16
16
  import { companyFailureText } from '../packages/piagent-webui/shared/company-copy.ts';
17
17
  import { headlineText, outcomeText, processItems } from '../packages/piagent-webui/shared/process-copy.ts';
18
18
  import { turnEndCopy, turnEndOf } from '../packages/piagent-webui/shared/turn-end.ts';
19
+ import { projectWarningText } from '../packages/piagent-webui/shared/project-warning-copy.ts';
19
20
 
20
21
  const LOCALE = 'vi';
21
22
 
@@ -399,6 +400,7 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
399
400
  void showBranch();
400
401
  session.subscribe((event) => {
401
402
  if (event?.type === 'managed_helpers') ctx.ui?.setStatus?.('agent-watch-helpers', event.active ? `Subagent ${event.active}/${event.maximum}` : undefined);
403
+ if (event?.type === 'managed_project_warning') ctx.ui?.notify?.(projectWarningText(event.warnings, event.team, LOCALE), 'warning');
402
404
  const end = event?.type === 'managed_turn_end' ? turnEndOf(event.end) : null;
403
405
  if (!end) return;
404
406
  const copy = turnEndCopy(end, LOCALE), stats = turnStats(session.messages);
@@ -1,4 +1,29 @@
1
1
  {
2
2
  "version": 1,
3
- "exceptions": []
3
+ "exceptions": [
4
+ {
5
+ "id": "GHSA-xjj6-f33q-929c",
6
+ "package": "undici",
7
+ "node": "node_modules/undici",
8
+ "decided": "2026-10-24",
9
+ "until": "2026-11-07",
10
+ "reason": "Denial of service (an unhandled error crashes the process) when a response lists too many content-encodings; no data exposure. undici 8.10.2 is an exact dependency of every published Pi host (1.3.0 and 1.4.0), installed globally, so no Piagent change can update it; 8.12.2 fixes it. Remove when a Pi host carries undici >= 8.12.2."
11
+ },
12
+ {
13
+ "id": "GHSA-4qv8-2p9j-2685",
14
+ "package": "undici",
15
+ "node": "node_modules/undici",
16
+ "decided": "2026-10-24",
17
+ "until": "2026-11-07",
18
+ "reason": "Denial of service (unbounded memory) in undici's EventSource parser fed by a hostile server; no data exposure. Pinned by the Pi host as above; fixed in undici 8.12.2."
19
+ },
20
+ {
21
+ "id": "GHSA-f72v-6w45-q472",
22
+ "package": "undici",
23
+ "node": "node_modules/undici",
24
+ "decided": "2026-10-24",
25
+ "until": "2026-11-07",
26
+ "reason": "Subresource Integrity check bypassed with a non-lowercase algorithm token; Pi does not fetch with integrity metadata. Pinned by the Pi host as above; fixed in undici 8.12.2."
27
+ }
28
+ ]
4
29
  }