@opengeni/runtime 0.14.1 → 0.14.15

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 (79) hide show
  1. package/dist/{chunk-4YJ5Z52Z.js → chunk-7E2ZSCZK.js} +6 -6
  2. package/dist/{chunk-4YJ5Z52Z.js.map → chunk-7E2ZSCZK.js.map} +1 -1
  3. package/dist/{chunk-HHWIZHQM.js → chunk-GNEGANUF.js} +942 -300
  4. package/dist/chunk-GNEGANUF.js.map +1 -0
  5. package/dist/codex-tool-search.d.ts +100 -0
  6. package/dist/context-compaction.d.ts +236 -0
  7. package/dist/history-sanitizer.d.ts +320 -0
  8. package/dist/index.d.ts +186 -718
  9. package/dist/index.js +612 -126
  10. package/dist/index.js.map +1 -1
  11. package/dist/{localSources-T3HZ6KY4.js → localSources-BXHBKGF4.js} +2 -2
  12. package/dist/mcp-network.d.ts +32 -35
  13. package/dist/metrics.d.ts +33 -0
  14. package/dist/sandbox/capabilities.d.ts +11 -0
  15. package/dist/sandbox/channel-a.d.ts +226 -0
  16. package/dist/sandbox/display-stack.d.ts +99 -0
  17. package/dist/sandbox/errors.d.ts +16 -0
  18. package/dist/sandbox/exec-banner.d.ts +19 -0
  19. package/dist/sandbox/index.d.ts +190 -6
  20. package/dist/sandbox/index.js +20 -4
  21. package/dist/sandbox/op-correlation.d.ts +24 -0
  22. package/dist/sandbox/provider-errors.d.ts +32 -0
  23. package/dist/sandbox/provider-operation-gate.d.ts +12 -0
  24. package/dist/sandbox/providers/blaxel.d.ts +2 -0
  25. package/dist/sandbox/providers/cloudflare.d.ts +2 -0
  26. package/dist/sandbox/providers/daytona.d.ts +2 -0
  27. package/dist/sandbox/providers/docker.d.ts +2 -0
  28. package/dist/sandbox/providers/e2b.d.ts +2 -0
  29. package/dist/sandbox/providers/index.d.ts +11 -0
  30. package/dist/sandbox/providers/local.d.ts +2 -0
  31. package/dist/sandbox/providers/modal.d.ts +132 -0
  32. package/dist/sandbox/providers/none.d.ts +2 -0
  33. package/dist/sandbox/providers/runloop.d.ts +2 -0
  34. package/dist/sandbox/providers/selfhosted.d.ts +2 -0
  35. package/dist/sandbox/providers/types.d.ts +30 -0
  36. package/dist/sandbox/providers/vercel.d.ts +2 -0
  37. package/dist/sandbox/recording.d.ts +80 -0
  38. package/dist/sandbox/routing/backend-resolver.d.ts +173 -0
  39. package/dist/sandbox/routing/routing-session.d.ts +401 -0
  40. package/dist/sandbox/run-credentials.d.ts +65 -0
  41. package/dist/sandbox/select.d.ts +120 -0
  42. package/dist/sandbox/selfhosted/capabilities.d.ts +91 -0
  43. package/dist/sandbox/selfhosted/control-rpc.d.ts +175 -0
  44. package/dist/sandbox/selfhosted/fault-rendering.d.ts +40 -0
  45. package/dist/sandbox/selfhosted/op-observer.d.ts +40 -0
  46. package/dist/sandbox/selfhosted/op-stream.d.ts +104 -0
  47. package/dist/sandbox/selfhosted/op-testing.d.ts +108 -0
  48. package/dist/sandbox/selfhosted/op-transport.d.ts +60 -0
  49. package/dist/sandbox/selfhosted/retry-policy.d.ts +106 -0
  50. package/dist/sandbox/selfhosted/session.d.ts +552 -0
  51. package/dist/sandbox/selfhosted/testing.d.ts +48 -0
  52. package/dist/sandbox/stream-port.d.ts +80 -0
  53. package/dist/sandbox/stream-token.d.ts +44 -0
  54. package/dist/sandbox/terminal-server.d.ts +67 -0
  55. package/dist/sandbox/toolspace-token.d.ts +23 -0
  56. package/dist/sandbox/turn-tool-cancellation.d.ts +72 -0
  57. package/dist/sandbox/workspace-archive.d.ts +39 -0
  58. package/dist/sandbox-computer.d.ts +227 -0
  59. package/dist/screenshot-error-card.d.ts +1 -0
  60. package/dist/skill-library.d.ts +10 -12
  61. package/dist/usage-telemetry.d.ts +45 -0
  62. package/dist/workspace-skills.d.ts +31 -0
  63. package/package.json +7 -7
  64. package/src/context-compaction.ts +600 -6
  65. package/src/history-sanitizer.ts +10 -92
  66. package/src/index.ts +301 -62
  67. package/src/sandbox/channel-a.ts +267 -81
  68. package/src/sandbox/index.ts +56 -94
  69. package/src/sandbox/provider-operation-gate.ts +110 -0
  70. package/src/sandbox/providers/docker.ts +13 -1
  71. package/src/sandbox/providers/modal.ts +433 -3
  72. package/src/sandbox/routing/routing-session.ts +84 -8
  73. package/src/sandbox/selfhosted/session.ts +7 -6
  74. package/src/sandbox/turn-tool-cancellation.ts +47 -7
  75. package/src/sandbox/workspace-archive.ts +176 -73
  76. package/src/sandbox-computer.ts +25 -75
  77. package/dist/chunk-HHWIZHQM.js.map +0 -1
  78. package/dist/index-BraazrAO.d.ts +0 -2931
  79. /package/dist/{localSources-T3HZ6KY4.js.map → localSources-BXHBKGF4.js.map} +0 -0
@@ -219,6 +219,51 @@ const FS_LIST_MAX_OUTPUT_BYTES = 768 * 1024;
219
219
  // data expands to ~683 KiB, leaving room for confinement/provider framing.
220
220
  const GIT_COMMAND_CHUNK_BYTES = 512 * 1024;
221
221
  const GIT_METADATA_MAX_BYTES = 2 * 1024 * 1024;
222
+ const GIT_MEASURE_FRAME = "__OPENGENI_GIT_MEASURE_V1__";
223
+ const GIT_CHUNK_FRAME = "__OPENGENI_GIT_CHUNK_V1__";
224
+ const GIT_CHUNK_FRAME_END = "__OPENGENI_GIT_CHUNK_END_V1__";
225
+ // Keep Channel-A's remote hashing aligned with the existing git-credential
226
+ // wrapper portability contract: GNU coreutils on managed Linux boxes, stock
227
+ // shasum on macOS/BSD Connected Machines, then OpenSSL as the final common
228
+ // fallback. Missing or failing implementations remain a typed unavailable read.
229
+ const PORTABLE_SHA256_FILE_FUNCTION = [
230
+ "opengeni_sha256_file() {",
231
+ ' if command -v sha256sum >/dev/null 2>&1 && digest=$(sha256sum -- "$1" 2>/dev/null); then printf "%s\\n" "$digest" | awk \'{print $1}\'; return; fi',
232
+ ' if command -v shasum >/dev/null 2>&1 && digest=$(shasum -a 256 "$1" 2>/dev/null); then printf "%s\\n" "$digest" | awk \'{print $1}\'; return; fi',
233
+ ' if command -v openssl >/dev/null 2>&1 && digest=$(openssl dgst -sha256 "$1" 2>/dev/null); then printf "%s\\n" "$digest" | sed \'s/^.*= //\'; return; fi',
234
+ " return 127",
235
+ "}",
236
+ ].join("\n");
237
+ // Descriptor confinement has two provider command surfaces. Linux exposes the
238
+ // opened path through procfs and GNU stat; stock macOS exposes descriptor paths
239
+ // through lsof and BSD stat over /dev/fd. Both branches bind two already-open
240
+ // descriptors to the same current non-symlink inode beneath the physical repo
241
+ // root before any body reads bytes, preserving the symlink/parent-swap defense.
242
+ const PORTABLE_DESCRIPTOR_FUNCTIONS = [
243
+ "opengeni_set_fd_path() {",
244
+ ' fd="$1"',
245
+ " opengeni_fd_path=",
246
+ ' if opengeni_fd_path=$(readlink -f -- "/proc/$$/fd/$fd" 2>/dev/null); then [ -n "$opengeni_fd_path" ]; return; fi',
247
+ " lsof_bin=",
248
+ " if [ -x /usr/sbin/lsof ]; then lsof_bin=/usr/sbin/lsof; elif command -v lsof >/dev/null 2>&1; then lsof_bin=$(command -v lsof); else return 127; fi",
249
+ ' while IFS= read -r -d \'\' field; do case "$field" in n*) opengeni_fd_path=${field#n} ;; esac; done < <("$lsof_bin" -a -p "$$" -d "$fd" -Fn0 2>/dev/null)',
250
+ ' [ -n "$opengeni_fd_path" ]',
251
+ "}",
252
+ "opengeni_fd_identity() {",
253
+ ' fd="$1"',
254
+ ' if identity=$(stat -Lc "%d:%i" -- "/proc/$$/fd/$fd" 2>/dev/null); then printf "%s" "$identity"; return; fi',
255
+ ' stat -f "%d:%i" "/dev/fd/$fd" 2>/dev/null',
256
+ "}",
257
+ "opengeni_path_identity() {",
258
+ ' if identity=$(stat -c "%d:%i" -- "$1" 2>/dev/null); then printf "%s" "$identity"; return; fi',
259
+ ' stat -f "%d:%i" "$1" 2>/dev/null',
260
+ "}",
261
+ "opengeni_fd_size() {",
262
+ ' fd="$1"',
263
+ ' if size=$(stat -Lc "%s" -- "/proc/$$/fd/$fd" 2>/dev/null); then printf "%s" "$size"; return; fi',
264
+ ' stat -f "%z" "/dev/fd/$fd" 2>/dev/null',
265
+ "}",
266
+ ].join("\n");
222
267
  const US = String.fromCharCode(0x1f); // \x1f unit sep — git-log field separator
223
268
  const RS = String.fromCharCode(0x1e); // \x1e record sep — git-log record separator
224
269
  const SELFHOSTED_VIRTUAL_ROOT = "/workspace";
@@ -807,33 +852,42 @@ export class SandboxChannelAService {
807
852
 
808
853
  /** Measure one Git command without returning its potentially large or
809
854
  * NUL-bearing output through the provider's text transport. */
810
- private async measureConfinedCommandBytes(repo: string, command: string): Promise<number> {
855
+ private async measureConfinedCommandBytes(
856
+ repo: string,
857
+ command: string,
858
+ ): Promise<{ sizeBytes: number; sha256: string }> {
811
859
  const measured = await this.runInConfinedDirectory(repo, {
812
860
  cmd: [
813
- "set -o pipefail",
814
- `{ ${command}; } 2>/dev/null | { bytes=$(wc -c | tr -d ' \\n'); printf '%s' "$bytes"; }`,
815
- "producer_status=${PIPESTATUS[0]}",
816
- "printf '\\t%s\\n' \"$producer_status\"",
861
+ PORTABLE_SHA256_FILE_FUNCTION,
862
+ "tmp=$(mktemp) || exit 70",
863
+ "trap 'rm -f \"$tmp\"' EXIT",
864
+ `/bin/bash --noprofile --norc -c ${shellQuote(command)} >"$tmp" 2>/dev/null`,
865
+ "producer_status=$?",
866
+ `bytes=$(wc -c <"$tmp" | tr -d ' \\n')`,
867
+ `sha256=$(opengeni_sha256_file "$tmp") || exit 71`,
868
+ `printf '${GIT_MEASURE_FRAME}\\t%s\\t%s\\t%s' "$producer_status" "$bytes" "$sha256"`,
817
869
  ].join("; "),
818
870
  maxOutputTokens: 1_024,
819
871
  });
820
- const match = measured.stdout.match(/^(\d+)\t(\d+)\n?$/);
872
+ const match = measured.stdout.match(
873
+ new RegExp(`^${GIT_MEASURE_FRAME}\\t(\\d+)\\t(\\d+)\\t([0-9a-f]{64})$`),
874
+ );
821
875
  if (
822
876
  !match ||
823
877
  (measured.exitCode !== null && measured.exitCode !== 0) ||
824
- Number(match[2]) !== 0
878
+ Number(match[1]) !== 0
825
879
  ) {
826
880
  throw new ChannelAUnavailableError(
827
881
  "Workspace Git data is temporarily unavailable. Retry the operation.",
828
882
  );
829
883
  }
830
- const size = Number(match[1]);
884
+ const size = Number(match[2]);
831
885
  if (!Number.isSafeInteger(size) || size < 0) {
832
886
  throw new ChannelAUnavailableError(
833
887
  "Workspace Git data is temporarily unavailable. Retry the operation.",
834
888
  );
835
889
  }
836
- return size;
890
+ return { sizeBytes: size, sha256: match[3]! };
837
891
  }
838
892
 
839
893
  /** Read an exact prefix of a command's output in chunks that remain below
@@ -843,20 +897,35 @@ export class SandboxChannelAService {
843
897
  private async readConfinedCommandPrefix(
844
898
  repo: string,
845
899
  command: string,
846
- byteLength: number,
900
+ measurement: { sizeBytes: number; sha256: string },
901
+ prefixByteLength = measurement.sizeBytes,
847
902
  ): Promise<Buffer> {
848
- if (byteLength === 0) return Buffer.alloc(0);
903
+ if (
904
+ !Number.isSafeInteger(prefixByteLength) ||
905
+ prefixByteLength < 0 ||
906
+ prefixByteLength > measurement.sizeBytes
907
+ ) {
908
+ throw new ChannelAUnavailableError(
909
+ "Workspace Git data is temporarily unavailable. Retry the operation.",
910
+ );
911
+ }
912
+ if (prefixByteLength === 0) return Buffer.alloc(0);
849
913
  const chunks: Buffer[] = [];
850
- for (let offset = 0; offset < byteLength; offset += GIT_COMMAND_CHUNK_BYTES) {
851
- const expected = Math.min(GIT_COMMAND_CHUNK_BYTES, byteLength - offset);
914
+ for (let offset = 0; offset < prefixByteLength; offset += GIT_COMMAND_CHUNK_BYTES) {
915
+ const expected = Math.min(GIT_COMMAND_CHUNK_BYTES, prefixByteLength - offset);
852
916
  const chunk = await this.runInConfinedDirectory(repo, {
853
917
  cmd: [
854
- `{ ${command}; } 2>/dev/null`,
855
- `tail -c +${offset + 1}`,
856
- `head -c ${expected}`,
857
- "base64",
858
- `tr -d '\\n'`,
859
- ].join(" | "),
918
+ PORTABLE_SHA256_FILE_FUNCTION,
919
+ "tmp=$(mktemp) || exit 70",
920
+ "trap 'rm -f \"$tmp\"' EXIT",
921
+ `/bin/bash --noprofile --norc -c ${shellQuote(command)} >"$tmp" 2>/dev/null`,
922
+ "producer_status=$?",
923
+ `bytes=$(wc -c <"$tmp" | tr -d ' \\n')`,
924
+ `sha256=$(opengeni_sha256_file "$tmp") || exit 71`,
925
+ `printf '${GIT_CHUNK_FRAME}\\t%s\\t%s\\t%s\\t%s\\t%s\\n' "$producer_status" "$bytes" "$sha256" ${offset} ${expected}`,
926
+ `if [ "$producer_status" -eq 0 ]; then tail -c +${offset + 1} "$tmp" | head -c ${expected} | base64 | tr -d '\\n'; fi`,
927
+ `printf '\\n${GIT_CHUNK_FRAME_END}'`,
928
+ ].join("; "),
860
929
  maxOutputTokens: Math.ceil((GIT_COMMAND_CHUNK_BYTES * 4) / 3) + 1_024,
861
930
  });
862
931
  if (chunk.exitCode !== null && chunk.exitCode !== 0) {
@@ -864,7 +933,28 @@ export class SandboxChannelAService {
864
933
  "Workspace Git data is temporarily unavailable. Retry the operation.",
865
934
  );
866
935
  }
867
- const encoded = chunk.stdout.replace(/\s/g, "");
936
+ const headerEnd = chunk.stdout.indexOf("\n");
937
+ const trailer = `\n${GIT_CHUNK_FRAME_END}`;
938
+ if (headerEnd < 0 || !chunk.stdout.endsWith(trailer)) {
939
+ throw new ChannelAUnavailableError(
940
+ "Workspace Git data is temporarily unavailable. Retry the operation.",
941
+ );
942
+ }
943
+ const header = chunk.stdout.slice(0, headerEnd).split("\t");
944
+ if (
945
+ header.length !== 6 ||
946
+ header[0] !== GIT_CHUNK_FRAME ||
947
+ header[1] !== "0" ||
948
+ header[2] !== String(measurement.sizeBytes) ||
949
+ header[3] !== measurement.sha256 ||
950
+ header[4] !== String(offset) ||
951
+ header[5] !== String(expected)
952
+ ) {
953
+ throw new ChannelAUnavailableError(
954
+ "Workspace Git data changed during capture. Retry the operation.",
955
+ );
956
+ }
957
+ const encoded = chunk.stdout.slice(headerEnd + 1, -trailer.length);
868
958
  const bytes = Buffer.from(encoded, "base64");
869
959
  if (bytes.byteLength !== expected || bytes.toString("base64") !== encoded) {
870
960
  throw new ChannelAUnavailableError(
@@ -873,7 +963,7 @@ export class SandboxChannelAService {
873
963
  }
874
964
  chunks.push(bytes);
875
965
  }
876
- return Buffer.concat(chunks, byteLength);
966
+ return Buffer.concat(chunks, prefixByteLength);
877
967
  }
878
968
 
879
969
  private async readConfinedCommandBytes(
@@ -881,37 +971,65 @@ export class SandboxChannelAService {
881
971
  command: string,
882
972
  maxBytes: number,
883
973
  ): Promise<{ bytes: Buffer; sizeBytes: number; truncated: boolean }> {
884
- const sizeBytes = await this.measureConfinedCommandBytes(repo, command);
885
- if (sizeBytes > maxBytes) {
886
- return { bytes: Buffer.alloc(0), sizeBytes, truncated: true };
974
+ const measurement = await this.measureConfinedCommandBytes(repo, command);
975
+ if (measurement.sizeBytes > maxBytes) {
976
+ return { bytes: Buffer.alloc(0), sizeBytes: measurement.sizeBytes, truncated: true };
887
977
  }
888
978
  return {
889
- bytes: await this.readConfinedCommandPrefix(repo, command, sizeBytes),
890
- sizeBytes,
979
+ bytes: await this.readConfinedCommandPrefix(repo, command, measurement),
980
+ sizeBytes: measurement.sizeBytes,
891
981
  truncated: false,
892
982
  };
893
983
  }
894
984
 
985
+ /** Bind an untracked regular-file descriptor to its repository root and its
986
+ * lstat identity before producing bytes. The final producer reads fd 3 rather
987
+ * than reopening the path, so a check→read symlink swap cannot disclose a
988
+ * target outside the repository (or substitute another in-repository inode). */
989
+ private confinedUntrackedRegularFileCommand(target: string, body: string): string {
990
+ const fileArg = target.startsWith("./") ? target : `./${target}`;
991
+ return [
992
+ PORTABLE_DESCRIPTOR_FUNCTIONS,
993
+ `file=${shellQuote(fileArg)}`,
994
+ 'exec 3<"$file" || exit 66',
995
+ 'exec 4<"$file" || exit 66',
996
+ "root=$(pwd -P) || exit 66",
997
+ "opengeni_set_fd_path 3 || exit 66",
998
+ 'opened3="$opengeni_fd_path"',
999
+ "opengeni_set_fd_path 4 || exit 66",
1000
+ 'opened4="$opengeni_fd_path"',
1001
+ 'case "$opened3" in "$root"|"$root"/*) ;; *) exit 67 ;; esac',
1002
+ 'case "$opened4" in "$root"|"$root"/*) ;; *) exit 67 ;; esac',
1003
+ 'test ! -L "$file" && test -f "$file" || exit 66',
1004
+ "opened3_identity=$(opengeni_fd_identity 3) || exit 66",
1005
+ "opened4_identity=$(opengeni_fd_identity 4) || exit 66",
1006
+ 'path_identity=$(opengeni_path_identity "$file") || exit 66',
1007
+ 'test "$opened3_identity" = "$opened4_identity" || exit 66',
1008
+ 'test "$opened3_identity" = "$path_identity" || exit 66',
1009
+ body,
1010
+ ].join("; ");
1011
+ }
1012
+
895
1013
  /** Read a regular untracked file without routing its body through one large
896
1014
  * execCommand response. Native provider reads are binary-safe; providers
897
1015
  * without that surface use the same bounded chunk protocol as Git patches. */
898
1016
  private async readUntrackedRegularFile(
899
1017
  repo: string,
900
1018
  target: string,
901
- byteLength: number,
1019
+ prefixByteLength: number,
1020
+ sizeBytes: number,
902
1021
  ): Promise<Buffer> {
903
- if (byteLength === 0) return Buffer.alloc(0);
904
1022
  const path = repo ? `${repo}/${target}` : target;
905
1023
  assertSafeRelPath(path);
906
1024
  if (this.session.readFile) {
907
1025
  try {
908
1026
  const raw = await this.session.readFile({
909
1027
  path: this.joinRoot(path),
910
- maxBytes: byteLength,
1028
+ maxBytes: prefixByteLength,
911
1029
  ...(this.runAs ? { runAs: this.runAs } : {}),
912
1030
  });
913
1031
  const bytes = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw);
914
- if (bytes.byteLength !== byteLength) {
1032
+ if (bytes.byteLength !== prefixByteLength) {
915
1033
  throw new ChannelAUnavailableError(
916
1034
  "Workspace Git data changed during capture. Retry the operation.",
917
1035
  );
@@ -932,12 +1050,14 @@ export class SandboxChannelAService {
932
1050
  }
933
1051
  }
934
1052
  }
935
- const fileArg = target.startsWith("./") ? target : `./${target}`;
936
- return await this.readConfinedCommandPrefix(
937
- repo,
938
- `file=${shellQuote(fileArg)}; test ! -L "$file" && test -f "$file" || exit 66; cat -- "$file"`,
939
- byteLength,
940
- );
1053
+ const command = this.confinedUntrackedRegularFileCommand(target, "cat <&3");
1054
+ const measurement = await this.measureConfinedCommandBytes(repo, command);
1055
+ if (measurement.sizeBytes !== sizeBytes) {
1056
+ throw new ChannelAUnavailableError(
1057
+ "Workspace Git data changed during capture. Retry the operation.",
1058
+ );
1059
+ }
1060
+ return await this.readConfinedCommandPrefix(repo, command, measurement, prefixByteLength);
941
1061
  }
942
1062
 
943
1063
  async gitStatus(req: GitStatusRequest): Promise<GitStatusResponse> {
@@ -967,7 +1087,7 @@ export class SandboxChannelAService {
967
1087
  "Workspace Git metadata exceeded the safe capture limit. Retry with a narrower path.",
968
1088
  );
969
1089
  }
970
- return { ...parsePorcelainV2(status.bytes.toString("utf8")), revision: this.revision };
1090
+ return { ...parsePorcelainV2(decodeGitMetadataUtf8(status.bytes)), revision: this.revision };
971
1091
  }
972
1092
 
973
1093
  async gitDiff(req: GitDiffRequest): Promise<GitDiffResponse> {
@@ -1003,7 +1123,7 @@ export class SandboxChannelAService {
1003
1123
  "Workspace Git metadata exceeded the safe capture limit. Retry with a narrower path.",
1004
1124
  );
1005
1125
  }
1006
- const stats = parseNumstatZ(numstat.bytes.toString("utf8"));
1126
+ const stats = parseNumstatZ(decodeGitMetadataUtf8(numstat.bytes));
1007
1127
 
1008
1128
  const files: GitFileDiff[] = [];
1009
1129
  for (const stat of stats) {
@@ -1061,57 +1181,79 @@ export class SandboxChannelAService {
1061
1181
  "Workspace Git metadata exceeded the safe capture limit. Retry with a narrower path.",
1062
1182
  );
1063
1183
  }
1064
- for (const target of untracked.bytes.toString("utf8").split(NUL).filter(Boolean)) {
1184
+ for (const target of decodeGitMetadataUtf8(untracked.bytes).split(NUL).filter(Boolean)) {
1065
1185
  const fileArg = target.startsWith("./") ? target : `./${target}`;
1066
- const read = await this.runInConfinedDirectory(repo, {
1067
- cmd: [
1068
- `file=${shellQuote(fileArg)}`,
1069
- 'if [ -L "$file" ]; then printf "L\\t"; readlink -z -- "$file" | base64 | tr -d "\\n"; exit; fi',
1070
- 'size=$(wc -c < "$file") || exit 66',
1071
- 'line_count=$(wc -l < "$file") || exit 66',
1072
- 'if [ "$size" -gt 0 ]; then last_byte=$(tail -c 1 "$file" | od -An -tu1 | tr -d " \\n"); if [ "$last_byte" != "10" ]; then line_count=$((line_count + 1)); fi; fi',
1073
- 'printf "F\\t%s\\t%s" "$size" "$line_count"',
1074
- ].join("; "),
1075
- });
1076
- if (read.exitCode !== null && read.exitCode !== 0) {
1186
+ const kind = await this.readConfinedCommandBytes(
1187
+ repo,
1188
+ `file=${shellQuote(fileArg)}; if [ -L "$file" ]; then printf L; elif [ -f "$file" ]; then printf F; else exit 66; fi`,
1189
+ 1,
1190
+ );
1191
+ if (kind.truncated || (kind.bytes[0] !== 0x4c && kind.bytes[0] !== 0x46)) {
1077
1192
  throw new ChannelAUnavailableError(
1078
1193
  "Workspace Git data is temporarily unavailable. Retry the operation.",
1079
1194
  );
1080
1195
  }
1081
1196
 
1082
- const [kind, first, second] = read.stdout.split("\t");
1083
1197
  let sampled: Buffer = Buffer.alloc(0);
1084
1198
  let sizeBytes: number;
1085
1199
  let lineCount: number;
1086
- if (kind === "L") {
1087
- const encoded = (first ?? "").replace(/\s/g, "");
1088
- sampled = Buffer.from(encoded, "base64");
1089
- // GNU readlink -z preserves every byte (including trailing newlines)
1090
- // and adds one NUL framing byte. The diff shows the link destination
1200
+ if (kind.bytes[0] === 0x4c) {
1201
+ const link = await this.readConfinedCommandBytes(
1202
+ repo,
1203
+ `file=${shellQuote(fileArg)}; test -L "$file" || exit 66; readlink -n "$file" || exit 66; printf '\\0'`,
1204
+ GIT_METADATA_MAX_BYTES,
1205
+ );
1206
+ if (link.truncated) {
1207
+ throw new ChannelAUnavailableError(
1208
+ "Workspace Git metadata exceeded the safe capture limit. Retry with a narrower path.",
1209
+ );
1210
+ }
1211
+ sampled = link.bytes;
1212
+ // GNU and BSD readlink both support -n. Append our own NUL framing byte
1213
+ // so trailing newlines remain data and the diff shows the link target
1091
1214
  // itself, matching Git's symlink-blob semantics, never target content.
1092
- if (sampled.at(-1) !== 0) continue;
1215
+ if (sampled.at(-1) !== 0) {
1216
+ throw new ChannelAUnavailableError(
1217
+ "Workspace Git data is temporarily unavailable. Retry the operation.",
1218
+ );
1219
+ }
1093
1220
  sampled = sampled.subarray(0, -1);
1094
1221
  sizeBytes = sampled.length;
1095
1222
  lineCount = addedLines(sampled).length;
1096
1223
  } else {
1097
- if (kind !== "F") continue;
1098
- const parsedSize = safeInt(first);
1099
- const parsedLineCount = safeInt(second);
1100
- if (
1101
- parsedSize === null ||
1102
- parsedSize < 0 ||
1103
- parsedLineCount === null ||
1104
- parsedLineCount < 0
1105
- ) {
1106
- continue;
1224
+ const metadata = await this.readConfinedCommandBytes(
1225
+ repo,
1226
+ this.confinedUntrackedRegularFileCommand(
1227
+ target,
1228
+ [
1229
+ "size=$(opengeni_fd_size 3) || exit 66",
1230
+ "line_count=$(wc -l <&3) || exit 66",
1231
+ 'if [ "$size" -gt 0 ]; then last_byte=$(tail -c 1 <&4 | od -An -tu1 | tr -d " \\n") || exit 66; if [ "$last_byte" != "10" ]; then line_count=$((line_count + 1)); fi; fi',
1232
+ 'printf "%s\\t%s" "$size" "$line_count"',
1233
+ ].join("; "),
1234
+ ),
1235
+ 128,
1236
+ );
1237
+ if (metadata.truncated) {
1238
+ throw new ChannelAUnavailableError(
1239
+ "Workspace Git data is temporarily unavailable. Retry the operation.",
1240
+ );
1241
+ }
1242
+ const frame = metadata.bytes.toString("ascii").match(/^(0|[1-9]\d*)\t(0|[1-9]\d*)$/);
1243
+ const parsedSize = safeInt(frame?.[1]);
1244
+ const parsedLineCount = safeInt(frame?.[2]);
1245
+ if (parsedSize === null || parsedLineCount === null) {
1246
+ throw new ChannelAUnavailableError(
1247
+ "Workspace Git data is temporarily unavailable. Retry the operation.",
1248
+ );
1107
1249
  }
1108
1250
  sizeBytes = parsedSize;
1109
1251
  lineCount = parsedLineCount;
1110
1252
  const truncated = sizeBytes > req.maxBytesPerFile;
1111
1253
  const sampleSize = Math.min(sizeBytes, 8_192);
1112
- sampled = await this.readUntrackedRegularFile(repo, target, sampleSize);
1254
+ sampled = await this.readUntrackedRegularFile(repo, target, sampleSize, sizeBytes);
1113
1255
  if (!sniffBinary(sampled) && !truncated && sampleSize < sizeBytes) {
1114
- sampled = await this.readUntrackedRegularFile(repo, target, sizeBytes);
1256
+ sampled = await this.readUntrackedRegularFile(repo, target, sizeBytes, sizeBytes);
1115
1257
  }
1116
1258
  }
1117
1259
  const truncated = sizeBytes > req.maxBytesPerFile || sampled.length > req.maxBytesPerFile;
@@ -1527,18 +1669,28 @@ export class SandboxChannelAService {
1527
1669
  const rejectLink = safe
1528
1670
  ? `test ! -L ${shellQuote(abs)} || { printf '__OPENGENI_FS_SYMLINK__'; exit 68; }`
1529
1671
  : ":";
1530
- const script = [
1531
- `root=$(realpath -e -- ${shellQuote(root)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
1532
- rejectLink,
1533
- `target=$(realpath -e -- ${shellQuote(abs)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
1534
- `case "$target" in "$root"|"$root"/*) ;; *) printf '__OPENGENI_FS_ESCAPE__'; exit 67 ;; esac`,
1535
- `test -d "$target" || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
1536
- `cd -P -- "$target"`,
1537
- `printf '__OPENGENI_FS_CONFINED_OK__\\n'`,
1538
- args.cmd,
1539
- ].join("; ");
1540
- const successPrefix = "__OPENGENI_FS_CONFINED_OK__\n";
1541
1672
  for (let attempt = 0; attempt < 2; attempt += 1) {
1673
+ // A fresh nonce on each transport attempt prevents a delayed/replayed
1674
+ // response from an earlier attempt from satisfying this attempt's frame.
1675
+ const frameId = crypto.randomUUID().replaceAll("-", "");
1676
+ const successPrefix = `__OPENGENI_FS_CONFINED_${frameId}_OK__\n`;
1677
+ const successSuffixPrefix = `__OPENGENI_FS_CONFINED_${frameId}_END__:`;
1678
+ const successSuffixTerminator = "__";
1679
+ const script = [
1680
+ `root=$(realpath -e -- ${shellQuote(root)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
1681
+ rejectLink,
1682
+ `target=$(realpath -e -- ${shellQuote(abs)}) || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
1683
+ `case "$target" in "$root"|"$root"/*) ;; *) printf '__OPENGENI_FS_ESCAPE__'; exit 67 ;; esac`,
1684
+ `test -d "$target" || { printf '__OPENGENI_FS_NOT_FOUND__'; exit 66; }`,
1685
+ `cd -P -- "$target"`,
1686
+ `printf ${shellQuote(successPrefix)}`,
1687
+ // Keep an operation-level `exit` inside the subshell so the trusted end
1688
+ // frame is emitted for every completed command.
1689
+ `( ${args.cmd} )`,
1690
+ `operation_status=$?`,
1691
+ `printf ${shellQuote(`${successSuffixPrefix}%s${successSuffixTerminator}`)} "$operation_status"`,
1692
+ `exit "$operation_status"`,
1693
+ ].join("; ");
1542
1694
  let result: Awaited<ReturnType<SandboxChannelAService["run"]>>;
1543
1695
  try {
1544
1696
  result = await this.run({
@@ -1560,7 +1712,27 @@ export class SandboxChannelAService {
1560
1712
  }
1561
1713
  const successIndex = result.stdout.indexOf(successPrefix);
1562
1714
  if (successIndex >= 0) {
1563
- return { ...result, stdout: result.stdout.slice(successIndex + successPrefix.length) };
1715
+ const payloadStart = successIndex + successPrefix.length;
1716
+ const suffixIndex = result.stdout.lastIndexOf(successSuffixPrefix);
1717
+ if (suffixIndex >= payloadStart) {
1718
+ const suffixEnd = result.stdout.indexOf(
1719
+ successSuffixTerminator,
1720
+ suffixIndex + successSuffixPrefix.length,
1721
+ );
1722
+ const statusText =
1723
+ suffixEnd < 0
1724
+ ? null
1725
+ : result.stdout.slice(suffixIndex + successSuffixPrefix.length, suffixEnd);
1726
+ if (statusText && /^(?:0|[1-9][0-9]{0,2})$/.test(statusText)) {
1727
+ const operationStatus = Number(statusText);
1728
+ if (operationStatus > 255) continue;
1729
+ return {
1730
+ ...result,
1731
+ stdout: result.stdout.slice(payloadStart, suffixIndex),
1732
+ exitCode: operationStatus,
1733
+ };
1734
+ }
1735
+ }
1564
1736
  }
1565
1737
  if (result.stdout.includes("__OPENGENI_FS_ESCAPE__") || result.exitCode === 67) {
1566
1738
  throw new ChannelAValidationError(`path resolves outside workspace: ${safe || "."}`);
@@ -1688,6 +1860,20 @@ function sniffBinary(bytes: Buffer): boolean {
1688
1860
  return false;
1689
1861
  }
1690
1862
 
1863
+ /** Git's -z metadata is byte-oriented. JavaScript's ordinary UTF-8 decode
1864
+ * replaces malformed bytes with U+FFFD; reissuing that replacement as a Git
1865
+ * pathspec would read a different path while claiming authority. Preserve only
1866
+ * byte-round-trippable UTF-8 and make every other path set unavailable. */
1867
+ function decodeGitMetadataUtf8(bytes: Buffer): string {
1868
+ const decoded = bytes.toString("utf8");
1869
+ if (!Buffer.from(decoded, "utf8").equals(bytes)) {
1870
+ throw new ChannelAUnavailableError(
1871
+ "Workspace Git paths are not valid UTF-8 and cannot be captured authoritatively.",
1872
+ );
1873
+ }
1874
+ return decoded;
1875
+ }
1876
+
1691
1877
  function addedLines(bytes: Buffer): GitDiffHunk["lines"] {
1692
1878
  if (bytes.length === 0) return [];
1693
1879
  const lines = bytes.toString("utf8").split("\n");