@piagent/platform 1.41.0 → 1.42.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 (125) hide show
  1. package/CHANGELOG.md +1020 -2038
  2. package/CONTRIBUTING.md +2 -0
  3. package/README.md +128 -325
  4. package/README.vi.md +149 -143
  5. package/docs/command-reference-vietnamese.md +2 -2
  6. package/docs/distribution-standard.md +6 -6
  7. package/docs/languages.json +2 -2
  8. package/docs/operator-manual-vietnamese.md +7 -7
  9. package/docs/publishing-for-teams.md +2 -0
  10. package/docs/release-install-policy.md +3 -3
  11. package/docs/team-onboarding.md +2 -2
  12. package/docs-site/architecture.html +71 -63
  13. package/docs-site/assets/docs.css +2736 -1030
  14. package/docs-site/assets/docs.js +451 -43
  15. package/docs-site/assets/og.png +0 -0
  16. package/docs-site/assets/piagent-logo.svg +9 -31
  17. package/docs-site/assets/search-en.json +1 -0
  18. package/docs-site/assets/search-vi.json +1 -0
  19. package/docs-site/benchmark.html +71 -63
  20. package/docs-site/commands.html +72 -64
  21. package/docs-site/content/en/index.html +220 -131
  22. package/docs-site/content/en/quickstart.html +74 -90
  23. package/docs-site/content/en/update.html +80 -0
  24. package/docs-site/content/en/whats-new.html +1 -684
  25. package/docs-site/content/index.html +220 -131
  26. package/docs-site/content/quickstart.html +73 -108
  27. package/docs-site/content/update.html +80 -0
  28. package/docs-site/content/whats-new.html +1 -684
  29. package/docs-site/context-engine.html +73 -65
  30. package/docs-site/ecosystem.html +73 -65
  31. package/docs-site/en/architecture.html +67 -59
  32. package/docs-site/en/benchmark.html +67 -59
  33. package/docs-site/en/commands.html +68 -60
  34. package/docs-site/en/context-engine.html +68 -60
  35. package/docs-site/en/ecosystem.html +68 -60
  36. package/docs-site/en/index.html +281 -220
  37. package/docs-site/en/mcp-approval.html +67 -59
  38. package/docs-site/en/mcp-auth.html +67 -59
  39. package/docs-site/en/mcp-servers.html +68 -60
  40. package/docs-site/en/mcp.html +64 -56
  41. package/docs-site/en/permissions.html +68 -60
  42. package/docs-site/en/profiles.html +68 -60
  43. package/docs-site/en/quickstart.html +142 -150
  44. package/docs-site/en/runtime.html +67 -59
  45. package/docs-site/en/scope.html +68 -60
  46. package/docs-site/en/security.html +64 -56
  47. package/docs-site/en/team.html +67 -59
  48. package/docs-site/en/update.html +228 -0
  49. package/docs-site/en/whats-new.html +1612 -725
  50. package/docs-site/en/windows.html +71 -63
  51. package/docs-site/en/workflows.html +68 -60
  52. package/docs-site/favicon.svg +9 -14
  53. package/docs-site/index.html +281 -220
  54. package/docs-site/mcp-approval.html +70 -62
  55. package/docs-site/mcp-auth.html +70 -62
  56. package/docs-site/mcp-servers.html +71 -63
  57. package/docs-site/mcp.html +70 -62
  58. package/docs-site/permissions.html +72 -64
  59. package/docs-site/profiles.html +72 -64
  60. package/docs-site/quickstart.html +145 -172
  61. package/docs-site/releases.json +4485 -0
  62. package/docs-site/runtime.html +73 -65
  63. package/docs-site/scope.html +73 -65
  64. package/docs-site/security.html +73 -65
  65. package/docs-site/team.html +71 -63
  66. package/docs-site/update.html +228 -0
  67. package/docs-site/whats-new.html +1616 -729
  68. package/docs-site/windows.html +75 -67
  69. package/docs-site/workflows.html +73 -65
  70. package/evals/private-holdout-v1/public-exposure.v2.json +1 -1
  71. package/package.json +1 -1
  72. package/packages/piagent-core/managed/dist/workspace/manifest.json +1 -1
  73. package/packages/piagent-core/managed/dist/workspace/worker.mjs +1 -1
  74. package/packages/piagent-core/package.json +1 -1
  75. package/packages/piagent-core/security/workspace-file-reader.ts +3 -1
  76. package/packages/piagent-webui/dist/client/assets/ActivityPanel-DRAiHliI.js +1 -0
  77. package/packages/piagent-webui/dist/client/assets/App-JMuP_9-r.js +5 -0
  78. package/packages/piagent-webui/dist/client/assets/GatewayApp-awcOLVTi.js +93 -0
  79. package/packages/piagent-webui/dist/client/assets/InspectionApp-DtDc9NxN.js +1 -0
  80. package/packages/piagent-webui/dist/client/assets/LineReview-DZ9ez8kO.js +3 -0
  81. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DeLo-xiz.js → MarkdownMessage-D7bbIWJe.js} +2 -2
  82. package/packages/piagent-webui/dist/client/assets/SessionAgentWorkspace-DLwK12hX.js +1 -0
  83. package/packages/piagent-webui/dist/client/assets/SessionInspectorDrawer-CsPNI6aU.js +2 -0
  84. package/packages/piagent-webui/dist/client/assets/SettingsNavigation-CM0s3QUC.js +3 -0
  85. package/packages/piagent-webui/dist/client/assets/SettingsPage-Cr6M2FVg.js +1 -0
  86. package/packages/piagent-webui/dist/client/assets/TerminalPanel-A78JVp4e.js +2 -0
  87. package/packages/piagent-webui/dist/client/assets/{TerminalView-r9rG24IO.js → TerminalView-CR4TO5f1.js} +3 -3
  88. package/packages/piagent-webui/dist/client/assets/WorkspaceCodeEditor-DPOp-ZoD.js +2 -0
  89. package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-H8Ogln3d.js +5 -0
  90. package/packages/piagent-webui/dist/client/assets/WorkspaceQuickOpen-vQ5DqfJl.js +1 -0
  91. package/packages/piagent-webui/dist/client/assets/api-BUjrtT1B.js +1 -0
  92. package/packages/piagent-webui/dist/client/assets/composer-drafts-BtMaVw8W.js +1 -0
  93. package/packages/piagent-webui/dist/client/assets/index-Dqb2hNan.css +1 -0
  94. package/packages/piagent-webui/dist/client/assets/index-qi6mjvTy.js +37 -0
  95. package/packages/piagent-webui/dist/client/assets/latest-request-DXcYQW86.js +1 -0
  96. package/packages/piagent-webui/dist/client/assets/tab-close-DHDqCxTL.js +1 -0
  97. package/packages/piagent-webui/dist/client/assets/terminal-transport-Dk5pYNX5.js +1 -0
  98. package/packages/piagent-webui/dist/client/assets/transcript-view-model-BNGQCkzY.js +3 -0
  99. package/packages/piagent-webui/dist/client/assets/ui-preferences-Dy4qeODT.js +1 -0
  100. package/packages/piagent-webui/dist/client/assets/vendor-CA5pzJiI.js +136 -0
  101. package/packages/piagent-webui/dist/client/assets/view-model-CWNiS4JN.js +1 -0
  102. package/packages/piagent-webui/dist/client/assets/workspace-navigation-C8GtShbg.js +1 -0
  103. package/packages/piagent-webui/dist/client/index.html +4 -9
  104. package/packages/piagent-webui/gateway/signed-session-journal.ts +16 -6
  105. package/packages/piagent-webui/server/loopback-server.ts +19 -5
  106. package/packages/piagent-webui/server/static-bundle.ts +10 -2
  107. package/scripts/build-docs-site.mjs +205 -348
  108. package/scripts/check-public-wording.mjs +2 -1
  109. package/scripts/docs-site-icons.mjs +84 -0
  110. package/scripts/docs-site-nav.mjs +302 -0
  111. package/scripts/docs-site-releases.mjs +227 -0
  112. package/scripts/verify-local.sh +17 -1
  113. package/packages/piagent-webui/dist/client/assets/Alert-DGWhP2MM.js +0 -1
  114. package/packages/piagent-webui/dist/client/assets/Button-QItfZUNj.js +0 -1
  115. package/packages/piagent-webui/dist/client/assets/DialogTitle-CyaFPaiW.js +0 -3
  116. package/packages/piagent-webui/dist/client/assets/TerminalPanel-BG-DP_qm.js +0 -2
  117. package/packages/piagent-webui/dist/client/assets/Typography-B9om7Evq.js +0 -1
  118. package/packages/piagent-webui/dist/client/assets/WorkspaceCodeEditor-BnqaR3pK.js +0 -2
  119. package/packages/piagent-webui/dist/client/assets/WorkspaceExplorer-BIxwiGtl.js +0 -5
  120. package/packages/piagent-webui/dist/client/assets/WorkspaceQuickOpen-CK3dILhQ.js +0 -1
  121. package/packages/piagent-webui/dist/client/assets/index-DBUTrEXi.css +0 -1
  122. package/packages/piagent-webui/dist/client/assets/index-rlyRccut.js +0 -102
  123. package/packages/piagent-webui/dist/client/assets/tab-close-BoJRyBO_.js +0 -1
  124. package/packages/piagent-webui/dist/client/assets/ui-preferences-BO3AV7QE.js +0 -10
  125. package/packages/piagent-webui/dist/client/assets/useId-DEefKUAT.js +0 -124
@@ -0,0 +1 @@
1
+ var e={active:`Đang hoạt động`,"pause-requested":`Đang yêu cầu tạm dừng`,paused:`Đã tạm dừng`,terminal:`Đã kết thúc`,idle:`Đang chờ`,running:`Đang chạy`,stopping:`Đang dừng`,settled:`Đã ổn định`,unknown:`Chưa xác định`,pending:`Chưa xong`,completed:`Hoàn thành`,blocked:`Bị chặn`,partial:`Một phần`,failed:`Thất bại`,satisfied:`Đã đạt`,unavailable:`Không có dữ liệu`,connected:`Đã kết nối`,disconnected:`Mất kết nối`,reconnecting:`Đang kết nối lại`,"resync-required":`Cần đồng bộ lại`,none:`Không có`,waiting:`Đang chờ duyệt`,current:`Mới nhất`,stale:`Đã cũ`,"not-run":`Chưa chạy`,"in-progress":`Đang làm`,done:`Đã xong`,skipped:`Bỏ qua`,resume:`Tiếp tục`,retry:`Thử lại`,observed:`Đã ghi nhận`,requested:`Đã yêu cầu`,"possible-interruption":`Có thể bị gián đoạn`,complete:`Đầy đủ`,"aggregate-only":`Chỉ có dữ liệu tổng hợp`,off:`Tắt`,minimal:`Tối thiểu`,low:`Thấp`,medium:`Trung bình`,high:`Cao`,xhigh:`Rất cao`,max:`Tối đa`,orphaned:`Mất owner`,cancelled:`Đã hủy`,succeeded:`Thành công`,"stale-result":`Kết quả đến muộn`,accepted:`Đã nhận`,rejected:`Không nhận`,"not-recorded":`Chưa có kết quả`,parent:`Task cha`,helper:`Helper`,"read-only":`Chỉ đọc`,"workspace-write":`Ghi trong project`,"trusted-full-access":`Toàn quyền`,bypass:`Bypass`,"single-writer":`Một writer`,retriever:`Retriever`,scout:`Scout`,planner:`Planner`,worker:`Worker`,reviewer:`Reviewer`,oracle:`Oracle`,researcher:`Researcher`,ready:`Sẵn sàng`,missing:`Chưa có bằng chứng`,interrupted:`Bị gián đoạn`,stopped:`Đã dừng`,aborted:`Đã hủy do lỗi`,incomplete:`Chưa có kết luận`,passed:`Đạt`,"not-applicable":`Không áp dụng`,smoke:`Smoke`,"public-regression":`Public regression`,capability:`Capability`,"private-holdout":`Private holdout`,"production-shadow":`Production shadow`,"pdf-converter-unavailable":`Host chưa cài pdftotext (poppler) nên chưa đọc được .pdf`,"pdf-text-unavailable":`PDF này không có text bóc được; nhiều khả năng là bản scan, cần OCR trước`,"pdf-extraction-failed":`Không chuyển được PDF này sang text`,"docx-unreadable":`File .docx này hỏng, bị mã hóa, hoặc không phải file Word thật`,"document-text-empty":`Tài liệu không có chữ nào để đọc`,"document-size-limit":`Tài liệu vượt giới hạn dung lượng`,"text-size-limit":`File text vượt giới hạn dung lượng`,"text-content-invalid":`Nội dung file không phải văn bản (file nhị phân), chưa gửi vào chat được`,"image-mime-mismatch":`Nội dung ảnh không khớp với đuôi file`,"model-image-input-unavailable":`Model hiện tại không nhận ảnh`,"attachment-capacity-limit":`Đã chạm giới hạn số file hoặc dung lượng đính kèm`,"no-readable-root":`Chưa có thư mục nào đọc được`,"document-roots-unavailable":`Không xác định được thư mục tài liệu`,"document-not-listed":`Tài liệu không còn trong danh sách; hãy tải lại`,"document-unavailable":`Không mở được tài liệu này`,"document-protected":`Tài liệu nằm trong protectedPaths nên không được đọc`,"document-changed":`File đã thay đổi sau khi liệt kê; chưa đọc gì cả`,"document-unreadable":`Không đọc được nội dung tài liệu`,"document-needs-ocr":`Tài liệu là bản scan, cần OCR trước khi đọc được chữ`,"document-converter-unavailable":`Host chưa cài pdftotext (poppler) nên chưa đọc được .pdf`,"session-command-effect-unknown":`Pi đã nhận yêu cầu nhưng đang đồng bộ trạng thái; không gửi lại để tránh chạy trùng`},t={active:`Active`,"pause-requested":`Pause requested`,paused:`Paused`,terminal:`Finished`,idle:`Idle`,running:`Running`,stopping:`Stopping`,settled:`Settled`,unknown:`Unknown`,pending:`Pending`,completed:`Completed`,blocked:`Blocked`,partial:`Partial`,failed:`Failed`,satisfied:`Satisfied`,unavailable:`Unavailable`,connected:`Connected`,disconnected:`Disconnected`,reconnecting:`Reconnecting`,"resync-required":`Resync required`,none:`None`,waiting:`Awaiting approval`,current:`Current`,stale:`Stale`,"not-run":`Not run`,"in-progress":`In progress`,done:`Done`,skipped:`Skipped`,resume:`Resume`,retry:`Retry`,observed:`Observed`,requested:`Requested`,"possible-interruption":`Possible interruption`,complete:`Complete`,"aggregate-only":`Aggregate only`,off:`Off`,minimal:`Minimal`,low:`Low`,medium:`Medium`,high:`High`,xhigh:`Extra high`,max:`Maximum`,orphaned:`Owner missing`,cancelled:`Cancelled`,succeeded:`Succeeded`,"stale-result":`Stale result`,accepted:`Accepted`,rejected:`Rejected`,"not-recorded":`Not recorded`,parent:`Parent task`,helper:`Helper`,"read-only":`Read only`,"workspace-write":`Workspace write`,"trusted-full-access":`Full access`,bypass:`Bypass`,"single-writer":`Single writer`,retriever:`Retriever`,scout:`Scout`,planner:`Planner`,worker:`Worker`,reviewer:`Reviewer`,oracle:`Oracle`,researcher:`Researcher`,ready:`Ready`,missing:`Evidence missing`,interrupted:`Interrupted`,stopped:`Stopped`,aborted:`Aborted`,incomplete:`Incomplete`,passed:`Passed`,"not-applicable":`Not applicable`,smoke:`Smoke`,"public-regression":`Public regression`,capability:`Capability`,"private-holdout":`Private holdout`,"production-shadow":`Production shadow`,"pdf-converter-unavailable":`This host has no pdftotext (poppler), so .pdf cannot be read`,"pdf-text-unavailable":`This PDF holds no extractable text; it is probably scanned and needs OCR first`,"pdf-extraction-failed":`The PDF could not be converted to text`,"docx-unreadable":`This .docx is corrupt, encrypted, or not really a Word document`,"document-text-empty":`The document contains no readable text`,"document-size-limit":`The document is over the size limit`,"text-size-limit":`The text file is over the size limit`,"text-content-invalid":`The file is not text (binary), so it cannot go into the chat`,"image-mime-mismatch":`The image content does not match its file extension`,"model-image-input-unavailable":`The current model does not accept images`,"attachment-capacity-limit":`The attachment count or size limit was reached`,"no-readable-root":`No readable directory is available`,"document-roots-unavailable":`The document directories could not be resolved`,"document-not-listed":`The document is no longer listed; reload the list`,"document-unavailable":`This document could not be opened`,"document-protected":`The document is under protectedPaths and is not readable`,"document-changed":`The file changed after it was listed; nothing was read`,"document-unreadable":`The document content could not be read`,"document-needs-ocr":`The document is scanned and needs OCR before it holds text`,"document-converter-unavailable":`This host has no pdftotext (poppler), so .pdf cannot be read`,"session-command-effect-unknown":`Pi received the request and is resyncing; do not resend it, to avoid a duplicate run`};function n(n,r=`vi`){return n?(r===`vi`?e:t)[n]??n.replaceAll(`-`,` `):r===`vi`?`Chưa có`:`Not available`}function r(e){return[`completed`,`satisfied`,`current`,`connected`,`done`,`passed`].includes(e??``)?`positive`:[`blocked`,`failed`,`error`,`expired`].includes(e??``)?`danger`:[`pause-requested`,`paused`,`waiting`,`stale`,`stale-result`,`orphaned`,`partial`,`reconnecting`].includes(e??``)?`warning`:[`running`,`active`,`in-progress`].includes(e??``)?`info`:`neutral`}function i(e){return e?e.length>22?`${e.slice(0,10)}…${e.slice(-7)}`:e:`—`}function a(e,t=`vi`){return e.session.model.state===`known`&&e.session.model.value?e.session.model.value.displayName:t===`vi`?`Model chưa xác định`:`Unknown model`}function o(e,t=`vi`){return e.session.thinking.state===`known`&&e.session.thinking.value?n(e.session.thinking.value,t):t===`vi`?`Chưa xác định`:`Unknown`}function s(e,t=`vi`){let r=e.session.permissionProfile;return r.state===`known`&&r.value?n(r.value,t):t===`vi`?`Quyền chưa xác định`:`Unknown permission`}function c(e,t=`vi`){let n=e.relatedFileRefs.length,r=e.verifierAttemptRefs.length;return!n&&!r?t===`vi`?`Chưa có bằng chứng liên kết`:`No linked evidence`:t===`vi`?`${n} file · ${r} lần kiểm tra`:`${n} files · ${r} verifier runs`}function l(e,t=`vi`){let n=e.task;return n?{completed:n.progress.completed,total:n.progress.total,percent:Math.max(0,Math.min(100,n.progress.percent)),text:t===`vi`?`${n.progress.completed}/${n.progress.total} tiêu chí`:`${n.progress.completed}/${n.progress.total} criteria`}:{completed:0,total:0,percent:0,text:t===`vi`?`Chưa có task đang chạy`:`No active task`}}export{s as a,r as c,a as i,c as n,l as o,n as r,o as s,i as t};
@@ -0,0 +1 @@
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{Dn as t,In as n,zt as r}from"./vendor-CA5pzJiI.js";import{i,n as a}from"./ui-preferences-Dy4qeODT.js";var o=e(n(),1),s=t(),c=`piagent-workspace-open`;function l(e,t=1){let n=/^(.+?)(?::(\d+)(?::\d+)?)?$/.exec(e.replace(/^@/,``)),r=n?.[1];return!r||r.length>4096||/^[~/]/.test(r)||/[\\\x00-\x1f\x7f:]/.test(r)||r.split(`/`).some(e=>!e||e===`.`||e===`..`)?null:{path:r,line:Math.max(1,Math.min(1e6,Number(n?.[2])||t||1))}}var u=(0,o.createContext)(null);function d({sessionRef:e,onOpen:t,children:n}){return(0,s.jsx)(u.Provider,{value:e?n=>{t(),window.dispatchEvent(new CustomEvent(c,{detail:{sessionRef:e,...n}}))}:null,children:n})}function f({path:e,line:t,children:n}){let c=(0,o.useContext)(u),{locale:d}=i(),f=l(e,t);return!c||!f?n?(0,s.jsx)(s.Fragment,{children:n}):null:(0,s.jsx)(r,{size:`small`,onClick:()=>c(f),sx:{textTransform:`none`,verticalAlign:`baseline`},children:n??a(d,`Mở trong editor`,`Open in editor`)})}function p({children:e}){return(0,s.jsx)(s.Fragment,{children:o.Children.map(e,e=>typeof e==`string`?e.split(/((?<![\w@])@[\w./-]+\.[\w]+(?::\d+)?)/g).map((e,t)=>e.startsWith(`@`)&&l(e)?(0,s.jsx)(f,{path:e,children:e},t):e):e)})}export{l as a,d as i,f as n,p as r,c as t};
@@ -7,16 +7,11 @@
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-rlyRccut.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-qi6mjvTy.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
12
- <link rel="modulepreload" crossorigin href="/assets/ui-preferences-BO3AV7QE.js">
13
- <link rel="modulepreload" crossorigin href="/assets/useId-DEefKUAT.js">
14
- <link rel="modulepreload" crossorigin href="/assets/Typography-B9om7Evq.js">
15
- <link rel="modulepreload" crossorigin href="/assets/Alert-DGWhP2MM.js">
16
- <link rel="modulepreload" crossorigin href="/assets/DialogTitle-CyaFPaiW.js">
17
- <link rel="modulepreload" crossorigin href="/assets/Button-QItfZUNj.js">
18
- <link rel="modulepreload" crossorigin href="/assets/bootstrap-CedkNtRG.js">
19
- <link rel="stylesheet" crossorigin href="/assets/index-DBUTrEXi.css">
12
+ <link rel="modulepreload" crossorigin href="/assets/vendor-CA5pzJiI.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/ui-preferences-Dy4qeODT.js">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-Dqb2hNan.css">
20
15
  </head>
21
16
  <body>
22
17
  <div id="root"></div>
@@ -2,6 +2,8 @@ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
+ // Windows uses inherited ACLs in the Gateway profile; Node mode bits do not represent them.
6
+ const POSIX_PERMISSIONS = process.platform !== "win32";
5
7
  const MAX_BYTES = 8 * 1024 * 1024, MAX_RECORDS = 4000;
6
8
  type SessionRecord = { identity: { sessionRef: string }; idempotencyKeyDigest: string };
7
9
  type Journal<T> = { version: 1; sequence: number; records: Array<{ sequence: number; receipt: T }> };
@@ -24,7 +26,7 @@ export class SignedSessionJournal<T extends SessionRecord> {
24
26
  }
25
27
  #checkDirectory(): void {
26
28
  const stat = fs.lstatSync(this.#root);
27
- if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077)
29
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (POSIX_PERMISSIONS && (stat.mode & 0o077))
28
30
  || typeof process.getuid === "function" && stat.uid !== process.getuid()
29
31
  || stat.dev !== this.#directory.dev || stat.ino !== this.#directory.ino) throw new Error("workspace-journal-unsafe");
30
32
  }
@@ -37,12 +39,17 @@ export class SignedSessionJournal<T extends SessionRecord> {
37
39
  }
38
40
  #read(sessionRef: string): Journal<T> {
39
41
  this.#checkDirectory();
40
- let descriptor: number;
41
- try { descriptor = fs.openSync(this.#file(sessionRef), fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK); }
42
+ let descriptor: number, before: fs.Stats;
43
+ const file = this.#file(sessionRef);
44
+ try {
45
+ before = fs.lstatSync(file);
46
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error("workspace-journal-unsafe");
47
+ descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
48
+ }
42
49
  catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return { version: 1, sequence: 0, records: [] }; throw error; }
43
50
  try {
44
51
  const stat = fs.fstatSync(descriptor);
45
- if (!stat.isFile() || stat.nlink !== 1 || stat.size > MAX_BYTES || (stat.mode & 0o077)
52
+ if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || stat.nlink !== 1 || stat.size > MAX_BYTES || (POSIX_PERMISSIONS && (stat.mode & 0o077))
46
53
  || typeof process.getuid === "function" && stat.uid !== process.getuid()) throw new Error("workspace-journal-unsafe");
47
54
  const { value, mac } = JSON.parse(fs.readFileSync(descriptor, "utf8")) as { value: Journal<T>; mac: string };
48
55
  if (!value || typeof mac !== "string" || !/^[a-f0-9]{64}$/.test(mac) || !timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(this.#sign(sessionRef, value), "hex"))
@@ -70,8 +77,11 @@ export class SignedSessionJournal<T extends SessionRecord> {
70
77
  descriptor = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
71
78
  fs.writeFileSync(descriptor, bytes); fs.fsyncSync(descriptor); fs.closeSync(descriptor); descriptor = undefined;
72
79
  this.#checkDirectory(); fs.renameSync(temporary, this.#file(sessionRef));
73
- const directory = fs.openSync(this.#root, fs.constants.O_RDONLY);
74
- try { fs.fsyncSync(directory); } finally { fs.closeSync(directory); }
80
+ // Windows cannot open a directory with the Node read-only file API.
81
+ if (process.platform !== "win32") {
82
+ const directory = fs.openSync(this.#root, fs.constants.O_RDONLY);
83
+ try { fs.fsyncSync(directory); } finally { fs.closeSync(directory); }
84
+ }
75
85
  } finally { if (descriptor !== undefined) fs.closeSync(descriptor); fs.rmSync(temporary, { force: true }); }
76
86
  }
77
87
  }
@@ -36,12 +36,27 @@ export type LoopbackServer = {
36
36
  close(): Promise<void>;
37
37
  };
38
38
 
39
- function serveAsset(response: ServerResponse, asset: StaticAsset): void {
39
+ function acceptsGzip(header: string | undefined): boolean {
40
+ // Opt in only to an explicit gzip token, respecting a refusal/invalid quality.
41
+ const token = header?.split(",").map(value => value.trim().toLowerCase())
42
+ .find(value => value.split(";", 1)[0].trim() === "gzip");
43
+ if (!token) return false;
44
+ const parameters = token.split(";").slice(1).map(value => value.trim());
45
+ if (!parameters.length) return true;
46
+ return parameters.length === 1 && /^q\s*=\s*(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/.test(parameters[0])
47
+ && Number(parameters[0].split("=")[1].trim()) > 0;
48
+ }
49
+
50
+ function serveAsset(response: ServerResponse, asset: StaticAsset, encoding: string | undefined, head: boolean): void {
51
+ const gzip = asset.gzipBody && acceptsGzip(encoding);
52
+ const body = gzip ? asset.gzipBody! : asset.body;
40
53
  response.statusCode = 200;
41
54
  response.setHeader("Cache-Control", "no-store");
42
55
  response.setHeader("Content-Type", asset.contentType);
43
- response.setHeader("Content-Length", asset.body.length);
44
- response.end(asset.body);
56
+ if (asset.gzipBody) response.setHeader("Vary", "Accept-Encoding");
57
+ if (gzip) response.setHeader("Content-Encoding", "gzip");
58
+ response.setHeader("Content-Length", body.length);
59
+ response.end(head ? undefined : body);
45
60
  }
46
61
 
47
62
  export async function startLoopbackServer(options: {
@@ -453,8 +468,7 @@ export async function startLoopbackServer(options: {
453
468
  if (request.method !== "GET" && request.method !== "HEAD") return errorResponse(response, 405, "method-not-allowed");
454
469
  const asset = assets.get(url.pathname);
455
470
  if (!asset || url.search) return errorResponse(response, 404, "not-found");
456
- if (request.method === "HEAD") { response.statusCode = 200; response.setHeader("Content-Type", asset.contentType); return response.end(); }
457
- serveAsset(response, asset);
471
+ serveAsset(response, asset, request.headers["accept-encoding"], request.method === "HEAD");
458
472
  });
459
473
  const gatewaySocket = options.gatewayProtocol ? attachGatewayWebSocket({
460
474
  server, origin: () => origin, authority: auth, protocol: options.gatewayProtocol
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { gzipSync } from "node:zlib";
3
4
 
4
5
  const MAX_FILE_BYTES = 2 * 1024 * 1024;
5
6
  const MAX_TOTAL_BYTES = 16 * 1024 * 1024;
@@ -9,7 +10,7 @@ const MIME = new Map([
9
10
  [".svg", "image/svg+xml"], [".woff2", "font/woff2"]
10
11
  ]);
11
12
 
12
- export type StaticAsset = { body: Buffer; contentType: string };
13
+ export type StaticAsset = { body: Buffer; contentType: string; gzipBody?: Buffer };
13
14
 
14
15
  export function loadStaticBundle(root: string): ReadonlyMap<string, StaticAsset> {
15
16
  const absoluteRoot = path.resolve(root), rootStat = fs.lstatSync(absoluteRoot);
@@ -30,7 +31,14 @@ export function loadStaticBundle(root: string): ReadonlyMap<string, StaticAsset>
30
31
  try {
31
32
  const opened = fs.fstatSync(descriptor);
32
33
  if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino || opened.size !== stat.size) throw new Error("static-asset-race");
33
- assets.set(relative === "index.html" ? "/" : `/${relative}`, { body: fs.readFileSync(descriptor), contentType });
34
+ const body = fs.readFileSync(descriptor);
35
+ // Only immutable code/styles. HTML receives a per-server CSP nonce later.
36
+ // Compression is paid once at startup, never on an API or asset request.
37
+ const compressed = body.length >= 1024 && [".js", ".css"].includes(path.extname(relative))
38
+ ? gzipSync(body) : undefined;
39
+ assets.set(relative === "index.html" ? "/" : `/${relative}`, {
40
+ body, contentType, gzipBody: compressed && compressed.length < body.length ? compressed : undefined
41
+ });
34
42
  } finally { fs.closeSync(descriptor); }
35
43
  }
36
44
  };