wiki-viewer 1.5.2 → 1.7.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 (131) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/routes-manifest.json +6 -0
  5. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  24. package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js +1 -1
  25. package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js +1 -1
  27. package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js +1 -1
  29. package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js +2 -2
  33. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/wiki/content/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/wiki/download/route/app-paths-manifest.json +3 -0
  39. package/.next/standalone/.next/server/app/api/wiki/download/route/build-manifest.json +9 -0
  40. package/.next/standalone/.next/server/app/api/wiki/download/route/server-reference-manifest.json +4 -0
  41. package/.next/standalone/.next/server/app/api/wiki/download/route.js +15 -0
  42. package/.next/standalone/.next/server/app/api/wiki/download/route.js.map +5 -0
  43. package/.next/standalone/.next/server/app/api/wiki/download/route.js.nft.json +1 -0
  44. package/.next/standalone/.next/server/app/api/wiki/download/route_client-reference-manifest.js +3 -0
  45. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/wiki/watch/route.js +5 -5
  47. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/index.html +1 -1
  49. package/.next/standalone/.next/server/app/index.rsc +2 -2
  50. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  51. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  52. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  53. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  54. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  55. package/.next/standalone/.next/server/app/page.js +1 -1
  56. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/signin/page.js +1 -1
  59. package/.next/standalone/.next/server/app/signin/page.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  61. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +1 -1
  62. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0p12-6~._.js → [root-of-the-server]__094q5bj._.js} +2 -2
  63. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0w47wil._.js → [root-of-the-server]__0km8suj._.js} +2 -2
  64. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__12qerfy._.js → [root-of-the-server]__0mzo03s._.js} +2 -2
  65. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0e7si9p._.js → [root-of-the-server]__0rzo40z._.js} +2 -2
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t5mg.n._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0zi3nkn._.js → [root-of-the-server]__106uw_c._.js} +2 -2
  68. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0qhdf-p._.js → [root-of-the-server]__12800b1._.js} +2 -2
  69. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_download_route_actions_0rd6p-3.js +3 -0
  70. package/.next/standalone/.next/server/chunks/node_modules__pnpm_0aj5g-w._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/ssr/{0~w0_katex_dist_katex_mjs_00650~e._.js → 0~w0_katex_dist_katex_mjs_0b8ra51._.js} +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/{[externals]__0jyyo2t._.js → [externals]__05-uq57._.js} +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/{[externals]__0m6xq-c._.js → [externals]__0btd01s._.js} +2 -2
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__06mzg2t._.js → [root-of-the-server]__00lk0r~._.js} +2 -2
  75. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__12ahxbl._.js → [root-of-the-server]__0ytvlzs._.js} +2 -2
  76. package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +7 -7
  77. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0v_nwsz._.js → node_modules__pnpm_0-x6zex._.js} +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0tkiej3._.js → node_modules__pnpm_012nt.s._.js} +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0~2~c30._.js → node_modules__pnpm_07rxv2x._.js} +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0gbgliv._.js → node_modules__pnpm_0a4su30._.js} +2 -2
  81. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_10413dq._.js → node_modules__pnpm_0ajzar9._.js} +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0zzzsrj._.js → node_modules__pnpm_0bum3-3._.js} +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0wffzo0._.js → node_modules__pnpm_0c71i8a._.js} +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_099cv.b._.js → node_modules__pnpm_0cbsnwf._.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0f_uzgy._.js → node_modules__pnpm_0cxwhfd._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_03ij5.g._.js → node_modules__pnpm_0dboa_e._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0iojvye._.js → node_modules__pnpm_0de8xt-._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_02gx8km._.js → node_modules__pnpm_0ezm2us._.js} +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0m5-azh._.js → node_modules__pnpm_0hobcbs._.js} +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0~.6dgb._.js → node_modules__pnpm_0hrbz84._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0rebktf._.js → node_modules__pnpm_0i.pw_m._.js} +1 -1
  92. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_03v21p-._.js → node_modules__pnpm_0il8j.j._.js} +2 -2
  93. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0cwpt9p._.js → node_modules__pnpm_0j3mvgg._.js} +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0fixtms._.js → node_modules__pnpm_0j53hti._.js} +1 -1
  95. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0li2vzs._.js → node_modules__pnpm_0kwc-vs._.js} +1 -1
  96. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_05qsply._.js → node_modules__pnpm_0lm8e2c._.js} +2 -2
  97. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_08ibh0w._.js → node_modules__pnpm_0mdo4i3._.js} +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_06pgebo._.js → node_modules__pnpm_0o.c1z8._.js} +2 -2
  99. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0gofu~q._.js → node_modules__pnpm_0rw91-f._.js} +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0n1kfcp._.js → node_modules__pnpm_0xrmzh1._.js} +1 -1
  101. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_02mew~s._.js → node_modules__pnpm_0yq5z.h._.js} +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xxwmv2._.js → node_modules__pnpm_0z~h2ow._.js} +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0f9fpy5._.js → node_modules__pnpm_0~ln0by._.js} +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0wouz19._.js → node_modules__pnpm_11e~j49._.js} +1 -1
  105. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  106. package/.next/standalone/.next/server/middleware-manifest.json +1 -1
  107. package/.next/standalone/.next/server/pages/404.html +1 -1
  108. package/.next/standalone/.next/server/pages/500.html +1 -1
  109. package/.next/standalone/.next/static/chunks/{17~r3.8awh3qg.js → 0vwkqw_rofm-c.js} +7 -7
  110. package/.next/standalone/package.json +2 -2
  111. package/.next/standalone/packages/wiki-viewer-mcp/bench-e2e.mjs +166 -0
  112. package/.next/standalone/packages/wiki-viewer-mcp/bench-latency.mjs +87 -0
  113. package/.next/standalone/packages/wiki-viewer-mcp/bench-payload.mjs +45 -0
  114. package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +36 -7
  115. package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +50 -2
  116. package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +2 -0
  117. package/.next/standalone/packages/wiki-viewer-mcp/package.json +1 -1
  118. package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +121 -12
  119. package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +63 -5
  120. package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +10 -0
  121. package/.next/standalone/pnpm-lock.yaml +3 -3
  122. package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +173 -81
  123. package/.next/standalone/src/app/api/wiki/download/route.ts +99 -0
  124. package/.next/standalone/src/app/page.tsx +31 -1
  125. package/.next/standalone/src/tests/proof/agent-fs.test.ts +149 -0
  126. package/.next/standalone/src/tests/proof/bench-write.ts +179 -0
  127. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  128. package/package.json +2 -2
  129. /package/.next/standalone/.next/static/{_avhhNXTnEvzPnQc1sIDF → uQH5T9TFHOhO0NJbO4QGI}/_buildManifest.js +0 -0
  130. /package/.next/standalone/.next/static/{_avhhNXTnEvzPnQc1sIDF → uQH5T9TFHOhO0NJbO4QGI}/_clientMiddlewareManifest.js +0 -0
  131. /package/.next/standalone/.next/static/{_avhhNXTnEvzPnQc1sIDF → uQH5T9TFHOhO0NJbO4QGI}/_ssgManifest.js +0 -0
@@ -23,6 +23,7 @@ import { _resetAuditDb } from "../../lib/proof/audit.js";
23
23
  // Route handlers (loaded after env is set)
24
24
  let fileGET: (req: Request, ctx: { params: Promise<{ path: string[] }> }) => Promise<Response>;
25
25
  let filePUT: (req: Request, ctx: { params: Promise<{ path: string[] }> }) => Promise<Response>;
26
+ let filePATCH: (req: Request, ctx: { params: Promise<{ path: string[] }> }) => Promise<Response>;
26
27
  let fileDELETE: (req: Request, ctx: { params: Promise<{ path: string[] }> }) => Promise<Response>;
27
28
  let lsGET: (req: Request, ctx: { params: Promise<{ path?: string[] }> }) => Promise<Response>;
28
29
  let movePOST: (req: Request) => Promise<Response>;
@@ -103,6 +104,7 @@ before(async () => {
103
104
  const fileRoute = await import("../../app/api/agent/fs/file/[...path]/route.js");
104
105
  fileGET = fileRoute.GET;
105
106
  filePUT = fileRoute.PUT;
107
+ filePATCH = fileRoute.PATCH;
106
108
  fileDELETE = fileRoute.DELETE;
107
109
 
108
110
  const lsRoute = await import("../../app/api/agent/fs/ls/[[...path]]/route.js");
@@ -822,3 +824,150 @@ test("traversal: .proof/ denied on PUT", async () => {
822
824
  const res = await filePUT(req, makeCtx([".proof", "inject.json"]));
823
825
  assert.equal(res.status, 400);
824
826
  });
827
+
828
+ // ── PATCH (server-side str-replace) ──────────────────────────────────────────
829
+
830
+ test("PATCH: str-replace on .md succeeds, sends only find/replace", async () => {
831
+ const orig = Buffer.from("# Title\n\nHello world. Keep this.\n");
832
+ await writeFile(path.join(tmpRoot, "patch1.md"), orig);
833
+ const req = new Request(fileUrl("patch1.md"), {
834
+ method: "PATCH",
835
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": sha256(orig) },
836
+ body: JSON.stringify({ find: "Hello world", replace: "Goodbye moon" }),
837
+ });
838
+ const res = await filePATCH(req, makeCtx(["patch1.md"]));
839
+ assert.equal(res.status, 200, await res.text());
840
+ const out = await readFile(path.join(tmpRoot, "patch1.md"), "utf-8");
841
+ assert.equal(out, "# Title\n\nGoodbye moon. Keep this.\n");
842
+ });
843
+
844
+ test("PATCH: missing If-Match → 412", async () => {
845
+ const orig = Buffer.from("alpha beta gamma");
846
+ await writeFile(path.join(tmpRoot, "patch2.txt"), orig);
847
+ const req = new Request(fileUrl("patch2.txt"), {
848
+ method: "PATCH",
849
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json" },
850
+ body: JSON.stringify({ find: "beta", replace: "BETA" }),
851
+ });
852
+ const res = await filePATCH(req, makeCtx(["patch2.txt"]));
853
+ assert.equal(res.status, 412);
854
+ });
855
+
856
+ test("PATCH: If-Match mismatch → 412", async () => {
857
+ const orig = Buffer.from("alpha beta gamma");
858
+ await writeFile(path.join(tmpRoot, "patch3.txt"), orig);
859
+ const req = new Request(fileUrl("patch3.txt"), {
860
+ method: "PATCH",
861
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": "sha256:deadbeef" },
862
+ body: JSON.stringify({ find: "beta", replace: "BETA" }),
863
+ });
864
+ const res = await filePATCH(req, makeCtx(["patch3.txt"]));
865
+ assert.equal(res.status, 412);
866
+ });
867
+
868
+ test("PATCH: zero matches → 422 MATCH_COUNT_MISMATCH", async () => {
869
+ const orig = Buffer.from("alpha beta gamma");
870
+ await writeFile(path.join(tmpRoot, "patch4.txt"), orig);
871
+ const req = new Request(fileUrl("patch4.txt"), {
872
+ method: "PATCH",
873
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": sha256(orig) },
874
+ body: JSON.stringify({ find: "nonexistent", replace: "x" }),
875
+ });
876
+ const res = await filePATCH(req, makeCtx(["patch4.txt"]));
877
+ assert.equal(res.status, 422);
878
+ const j = await res.json();
879
+ assert.equal(j.error, "MATCH_COUNT_MISMATCH");
880
+ assert.equal(j.found, 0);
881
+ });
882
+
883
+ test("PATCH: multiple matches but expected 1 → 422", async () => {
884
+ const orig = Buffer.from("foo bar foo baz foo");
885
+ await writeFile(path.join(tmpRoot, "patch5.txt"), orig);
886
+ const req = new Request(fileUrl("patch5.txt"), {
887
+ method: "PATCH",
888
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": sha256(orig) },
889
+ body: JSON.stringify({ find: "foo", replace: "X" }),
890
+ });
891
+ const res = await filePATCH(req, makeCtx(["patch5.txt"]));
892
+ assert.equal(res.status, 422);
893
+ const j = await res.json();
894
+ assert.equal(j.found, 3);
895
+ });
896
+
897
+ test("PATCH: expectedOccurrences matching multi replaces all", async () => {
898
+ const orig = Buffer.from("foo bar foo baz foo");
899
+ await writeFile(path.join(tmpRoot, "patch6.txt"), orig);
900
+ const req = new Request(fileUrl("patch6.txt"), {
901
+ method: "PATCH",
902
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": sha256(orig) },
903
+ body: JSON.stringify({ find: "foo", replace: "X", expectedOccurrences: 3 }),
904
+ });
905
+ const res = await filePATCH(req, makeCtx(["patch6.txt"]));
906
+ assert.equal(res.status, 200, await res.text());
907
+ const out = await readFile(path.join(tmpRoot, "patch6.txt"), "utf-8");
908
+ assert.equal(out, "X bar X baz X");
909
+ });
910
+
911
+ test("PATCH: empty find → 400", async () => {
912
+ const orig = Buffer.from("abc");
913
+ await writeFile(path.join(tmpRoot, "patch7.txt"), orig);
914
+ const req = new Request(fileUrl("patch7.txt"), {
915
+ method: "PATCH",
916
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": sha256(orig) },
917
+ body: JSON.stringify({ find: "", replace: "x" }),
918
+ });
919
+ const res = await filePATCH(req, makeCtx(["patch7.txt"]));
920
+ assert.equal(res.status, 400);
921
+ });
922
+
923
+ test("PATCH: binary file → 415", async () => {
924
+ const orig = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0x00, 0x10]);
925
+ await writeFile(path.join(tmpRoot, "patch8.bin"), orig);
926
+ const req = new Request(fileUrl("patch8.bin"), {
927
+ method: "PATCH",
928
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": sha256(orig) },
929
+ body: JSON.stringify({ find: "x", replace: "y" }),
930
+ });
931
+ const res = await filePATCH(req, makeCtx(["patch8.bin"]));
932
+ assert.equal(res.status, 415);
933
+ });
934
+
935
+ test("PATCH: nonexistent file → 404 (no create)", async () => {
936
+ const req = new Request(fileUrl("patch-missing.txt"), {
937
+ method: "PATCH",
938
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": "sha256:abc" },
939
+ body: JSON.stringify({ find: "a", replace: "b" }),
940
+ });
941
+ const res = await filePATCH(req, makeCtx(["patch-missing.txt"]));
942
+ assert.equal(res.status, 404);
943
+ });
944
+
945
+ test("PATCH: .md emits file.rawWritten + reconciles (same path as PUT)", async () => {
946
+ const orig = Buffer.from("# H\n\noriginal paragraph here\n");
947
+ await writeFile(path.join(tmpRoot, "patch-md.md"), orig);
948
+ const req = new Request(fileUrl("patch-md.md"), {
949
+ method: "PATCH",
950
+ headers: { ...agentHeaders(MUTATE_TOKEN, "ai:mutate-agent"), "Content-Type": "application/json", "If-Match": sha256(orig) },
951
+ body: JSON.stringify({ find: "original", replace: "updated" }),
952
+ });
953
+ const res = await filePATCH(req, makeCtx(["patch-md.md"]));
954
+ assert.equal(res.status, 200, await res.text());
955
+ const sidecar = await readSidecar(tmpRoot, "patch-md.md");
956
+ assert.ok(sidecar, "sidecar created");
957
+ const events = sidecar!.events.filter((e) => e.type === "file.rawWritten");
958
+ assert.ok(events.length > 0, "file.rawWritten event present");
959
+ const newContent = await readFile(path.join(tmpRoot, "patch-md.md"), "utf-8");
960
+ assert.equal(sidecar!.fingerprint, sha256(Buffer.from(newContent)), "fingerprint matches new content (eager reconcile)");
961
+ });
962
+
963
+ test("PATCH: requires mutate scope", async () => {
964
+ const orig = Buffer.from("hello");
965
+ await writeFile(path.join(tmpRoot, "patch-scope.txt"), orig);
966
+ const req = new Request(fileUrl("patch-scope.txt"), {
967
+ method: "PATCH",
968
+ headers: { ...agentHeaders(READ_TOKEN, "ai:read-agent"), "Content-Type": "application/json", "If-Match": sha256(orig) },
969
+ body: JSON.stringify({ find: "hello", replace: "hi" }),
970
+ });
971
+ const res = await filePATCH(req, makeCtx(["patch-scope.txt"]));
972
+ assert.equal(res.status, 403);
973
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Isolated write-path benchmark — NOT a test, NOT touching production state.
3
+ *
4
+ * Drives the real Tier-1 route handlers (filePUT / fileGET) against a throwaway
5
+ * temp rootDir + temp HOME (lock dir), exactly like agent-fs.test.ts, and
6
+ * measures end-to-end server-side latency for the write path:
7
+ * - non-.md overwrite (raw, no lock/sidecar)
8
+ * - .md overwrite (cross-proc lock + reconcile + sidecar write + datasync)
9
+ * - edit_file equivalent (GET + PUT round-trip on .md)
10
+ *
11
+ * Run: npx tsx src/tests/proof/bench-write.ts
12
+ * Optional env: BENCH_ITERS (default 300), BENCH_WARMUP (default 30)
13
+ */
14
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import path from "node:path";
17
+ import { createHash, randomBytes } from "node:crypto";
18
+
19
+ import { setRootDir } from "../../lib/root-dir.js";
20
+ import { ensureRegistry, addAgent, hashToken } from "../../lib/proof/registry.js";
21
+
22
+ const ITERS = Number(process.env.BENCH_ITERS) || 300;
23
+ const WARMUP = Number(process.env.BENCH_WARMUP) || 30;
24
+
25
+ function sha256(buf: Buffer): string {
26
+ return "sha256:" + createHash("sha256").update(buf).digest("hex");
27
+ }
28
+ function hdrs(token: string, id: string): Record<string, string> {
29
+ return { Authorization: `Bearer ${token}`, "X-Agent-Id": id };
30
+ }
31
+ function fileUrl(rel: string, qs = ""): string {
32
+ return `http://localhost/api/agent/fs/file/${rel}${qs}`;
33
+ }
34
+
35
+ function pct(sorted: number[], p: number): number {
36
+ const i = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
37
+ return sorted[i];
38
+ }
39
+ function stats(label: string, samples: number[]) {
40
+ const s = [...samples].sort((a, b) => a - b);
41
+ const sum = s.reduce((a, b) => a + b, 0);
42
+ const mean = sum / s.length;
43
+ return {
44
+ label,
45
+ n: s.length,
46
+ mean: +mean.toFixed(3),
47
+ p50: +pct(s, 50).toFixed(3),
48
+ p95: +pct(s, 95).toFixed(3),
49
+ p99: +pct(s, 99).toFixed(3),
50
+ min: +s[0].toFixed(3),
51
+ max: +s[s.length - 1].toFixed(3),
52
+ };
53
+ }
54
+
55
+ async function timeIt(fn: () => Promise<void>): Promise<number> {
56
+ const t0 = performance.now();
57
+ await fn();
58
+ return performance.now() - t0;
59
+ }
60
+
61
+ async function main() {
62
+ const tmpHome = await mkdtemp(path.join(tmpdir(), "bench-home-"));
63
+ const tmpRoot = await mkdtemp(path.join(tmpdir(), "bench-root-"));
64
+ process.env.HOME = tmpHome;
65
+ setRootDir(tmpRoot);
66
+ await ensureRegistry();
67
+
68
+ const TOKEN = randomBytes(32).toString("hex");
69
+ await addAgent({
70
+ id: "ai:bench",
71
+ displayName: "Bench",
72
+ tokenHash: hashToken(TOKEN),
73
+ scope: { paths: ["**/*"], ops: ["read", "mutate", "delete"] },
74
+ createdAt: new Date().toISOString(),
75
+ lastSeen: new Date().toISOString(),
76
+ });
77
+
78
+ const fileRoute = await import("../../app/api/agent/fs/file/[...path]/route.js");
79
+ const filePUT = fileRoute.PUT;
80
+ const fileGET = fileRoute.GET;
81
+ const H = hdrs(TOKEN, "ai:bench");
82
+
83
+ const ctx = (rel: string) => ({ params: Promise.resolve({ path: rel.split("/") }) });
84
+
85
+ // Seed bodies (~1KB, realistic small doc)
86
+ const body = Buffer.from("# Doc\n\n" + "lorem ipsum dolor sit amet. ".repeat(40));
87
+ const txtBody = Buffer.from("config_value = " + "x".repeat(900));
88
+
89
+ async function putOnce(rel: string, data: Buffer, ifMatch?: string, ifCollab?: string): Promise<Response> {
90
+ const headers: Record<string, string> = { ...H, "Content-Type": "application/octet-stream" };
91
+ if (ifMatch) headers["If-Match"] = ifMatch;
92
+ if (ifCollab) headers["If-Collab-Match"] = ifCollab;
93
+ return filePUT(new Request(fileUrl(rel), { method: "PUT", headers, body: new Uint8Array(data) }), ctx(rel));
94
+ }
95
+ async function getSha(rel: string): Promise<{ sha: string; collabRev: string | null }> {
96
+ const res = await fileGET(new Request(fileUrl(rel), { headers: H }), ctx(rel));
97
+ const etag = (res.headers.get("ETag") ?? "").replace(/"/g, "");
98
+ return { sha: etag, collabRev: res.headers.get("X-Collab-Revision") };
99
+ }
100
+
101
+ // ── Scenario A: non-.md overwrite (lean path) ──────────────────────────────
102
+ await putOnce("bench.txt", txtBody, undefined); // create
103
+ const txtSamples: number[] = [];
104
+ {
105
+ let sha = (await getSha("bench.txt")).sha;
106
+ for (let i = 0; i < WARMUP + ITERS; i++) {
107
+ const data = Buffer.from(txtBody.toString() + i);
108
+ const t = await timeIt(async () => {
109
+ const res = await putOnce("bench.txt", data, sha);
110
+ if (res.status !== 200) throw new Error("txt PUT " + res.status);
111
+ sha = (await res.json() as { sha256: string }).sha256;
112
+ });
113
+ if (i >= WARMUP) txtSamples.push(t);
114
+ }
115
+ }
116
+
117
+ // ── Scenario B: .md overwrite (lock + reconcile + sidecar + datasync) ──────
118
+ await putOnce("bench.md", body, undefined); // create
119
+ const mdSamples: number[] = [];
120
+ {
121
+ let sha = (await getSha("bench.md")).sha;
122
+ for (let i = 0; i < WARMUP + ITERS; i++) {
123
+ const data = Buffer.from(body.toString() + "\n\nedit " + i);
124
+ const t = await timeIt(async () => {
125
+ const res = await putOnce("bench.md", data, sha);
126
+ if (res.status !== 200) throw new Error("md PUT " + res.status + " " + await res.text());
127
+ sha = (await res.json() as { sha256: string }).sha256;
128
+ });
129
+ if (i >= WARMUP) mdSamples.push(t);
130
+ }
131
+ }
132
+
133
+ // ── Scenario C: edit_file equivalent — GET + PUT round-trip on .md ─────────
134
+ const editSamples: number[] = [];
135
+ {
136
+ for (let i = 0; i < WARMUP + ITERS; i++) {
137
+ const data = Buffer.from(body.toString() + "\n\nrt " + i);
138
+ const t = await timeIt(async () => {
139
+ const { sha } = await getSha("bench.md");
140
+ const res = await putOnce("bench.md", data, sha);
141
+ if (res.status !== 200) throw new Error("edit PUT " + res.status);
142
+ await res.json();
143
+ });
144
+ if (i >= WARMUP) editSamples.push(t);
145
+ }
146
+ }
147
+
148
+ const results = [
149
+ stats("non-.md overwrite (lean)", txtSamples),
150
+ stats(".md overwrite (lock+reconcile+sidecar+datasync)", mdSamples),
151
+ stats("edit_file (GET+PUT .md round-trip)", editSamples),
152
+ ];
153
+
154
+ console.log(`\nwrite-path benchmark (iters=${ITERS}, warmup=${WARMUP}, ~1KB bodies)\n`);
155
+ console.log(
156
+ ["scenario".padEnd(48), "p50", "p95", "p99", "mean", "max"].join(" "),
157
+ );
158
+ for (const r of results) {
159
+ console.log(
160
+ [
161
+ r.label.padEnd(48),
162
+ `${r.p50}ms`.padStart(7),
163
+ `${r.p95}ms`.padStart(7),
164
+ `${r.p99}ms`.padStart(7),
165
+ `${r.mean}ms`.padStart(7),
166
+ `${r.max}ms`.padStart(7),
167
+ ].join(" "),
168
+ );
169
+ }
170
+ console.log("");
171
+
172
+ await rm(tmpHome, { recursive: true, force: true });
173
+ await rm(tmpRoot, { recursive: true, force: true });
174
+ }
175
+
176
+ main().catch((e) => {
177
+ console.error(e);
178
+ process.exit(1);
179
+ });