@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.
- package/dist/{chunk-4YJ5Z52Z.js → chunk-7E2ZSCZK.js} +6 -6
- package/dist/{chunk-4YJ5Z52Z.js.map → chunk-7E2ZSCZK.js.map} +1 -1
- package/dist/{chunk-HHWIZHQM.js → chunk-GNEGANUF.js} +942 -300
- package/dist/chunk-GNEGANUF.js.map +1 -0
- package/dist/codex-tool-search.d.ts +100 -0
- package/dist/context-compaction.d.ts +236 -0
- package/dist/history-sanitizer.d.ts +320 -0
- package/dist/index.d.ts +186 -718
- package/dist/index.js +612 -126
- package/dist/index.js.map +1 -1
- package/dist/{localSources-T3HZ6KY4.js → localSources-BXHBKGF4.js} +2 -2
- package/dist/mcp-network.d.ts +32 -35
- package/dist/metrics.d.ts +33 -0
- package/dist/sandbox/capabilities.d.ts +11 -0
- package/dist/sandbox/channel-a.d.ts +226 -0
- package/dist/sandbox/display-stack.d.ts +99 -0
- package/dist/sandbox/errors.d.ts +16 -0
- package/dist/sandbox/exec-banner.d.ts +19 -0
- package/dist/sandbox/index.d.ts +190 -6
- package/dist/sandbox/index.js +20 -4
- package/dist/sandbox/op-correlation.d.ts +24 -0
- package/dist/sandbox/provider-errors.d.ts +32 -0
- package/dist/sandbox/provider-operation-gate.d.ts +12 -0
- package/dist/sandbox/providers/blaxel.d.ts +2 -0
- package/dist/sandbox/providers/cloudflare.d.ts +2 -0
- package/dist/sandbox/providers/daytona.d.ts +2 -0
- package/dist/sandbox/providers/docker.d.ts +2 -0
- package/dist/sandbox/providers/e2b.d.ts +2 -0
- package/dist/sandbox/providers/index.d.ts +11 -0
- package/dist/sandbox/providers/local.d.ts +2 -0
- package/dist/sandbox/providers/modal.d.ts +132 -0
- package/dist/sandbox/providers/none.d.ts +2 -0
- package/dist/sandbox/providers/runloop.d.ts +2 -0
- package/dist/sandbox/providers/selfhosted.d.ts +2 -0
- package/dist/sandbox/providers/types.d.ts +30 -0
- package/dist/sandbox/providers/vercel.d.ts +2 -0
- package/dist/sandbox/recording.d.ts +80 -0
- package/dist/sandbox/routing/backend-resolver.d.ts +173 -0
- package/dist/sandbox/routing/routing-session.d.ts +401 -0
- package/dist/sandbox/run-credentials.d.ts +65 -0
- package/dist/sandbox/select.d.ts +120 -0
- package/dist/sandbox/selfhosted/capabilities.d.ts +91 -0
- package/dist/sandbox/selfhosted/control-rpc.d.ts +175 -0
- package/dist/sandbox/selfhosted/fault-rendering.d.ts +40 -0
- package/dist/sandbox/selfhosted/op-observer.d.ts +40 -0
- package/dist/sandbox/selfhosted/op-stream.d.ts +104 -0
- package/dist/sandbox/selfhosted/op-testing.d.ts +108 -0
- package/dist/sandbox/selfhosted/op-transport.d.ts +60 -0
- package/dist/sandbox/selfhosted/retry-policy.d.ts +106 -0
- package/dist/sandbox/selfhosted/session.d.ts +552 -0
- package/dist/sandbox/selfhosted/testing.d.ts +48 -0
- package/dist/sandbox/stream-port.d.ts +80 -0
- package/dist/sandbox/stream-token.d.ts +44 -0
- package/dist/sandbox/terminal-server.d.ts +67 -0
- package/dist/sandbox/toolspace-token.d.ts +23 -0
- package/dist/sandbox/turn-tool-cancellation.d.ts +72 -0
- package/dist/sandbox/workspace-archive.d.ts +39 -0
- package/dist/sandbox-computer.d.ts +227 -0
- package/dist/screenshot-error-card.d.ts +1 -0
- package/dist/skill-library.d.ts +10 -12
- package/dist/usage-telemetry.d.ts +45 -0
- package/dist/workspace-skills.d.ts +31 -0
- package/package.json +7 -7
- package/src/context-compaction.ts +600 -6
- package/src/history-sanitizer.ts +10 -92
- package/src/index.ts +301 -62
- package/src/sandbox/channel-a.ts +267 -81
- package/src/sandbox/index.ts +56 -94
- package/src/sandbox/provider-operation-gate.ts +110 -0
- package/src/sandbox/providers/docker.ts +13 -1
- package/src/sandbox/providers/modal.ts +433 -3
- package/src/sandbox/routing/routing-session.ts +84 -8
- package/src/sandbox/selfhosted/session.ts +7 -6
- package/src/sandbox/turn-tool-cancellation.ts +47 -7
- package/src/sandbox/workspace-archive.ts +176 -73
- package/src/sandbox-computer.ts +25 -75
- package/dist/chunk-HHWIZHQM.js.map +0 -1
- package/dist/index-BraazrAO.d.ts +0 -2931
- /package/dist/{localSources-T3HZ6KY4.js.map → localSources-BXHBKGF4.js.map} +0 -0
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
"
|
|
816
|
-
|
|
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(
|
|
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[
|
|
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[
|
|
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
|
-
|
|
900
|
+
measurement: { sizeBytes: number; sha256: string },
|
|
901
|
+
prefixByteLength = measurement.sizeBytes,
|
|
847
902
|
): Promise<Buffer> {
|
|
848
|
-
if (
|
|
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 <
|
|
851
|
-
const expected = Math.min(GIT_COMMAND_CHUNK_BYTES,
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
"
|
|
858
|
-
|
|
859
|
-
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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 !==
|
|
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
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
|
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
|
|
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
|
|
1184
|
+
for (const target of decodeGitMetadataUtf8(untracked.bytes).split(NUL).filter(Boolean)) {
|
|
1065
1185
|
const fileArg = target.startsWith("./") ? target : `./${target}`;
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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 ===
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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)
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
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");
|