@@ -17,6 +17,7 @@ import {
1717 privilegedSandboxExecArgv ,
1818 withPrivilegedSandboxExecutionLease ,
1919} from "../../../sandbox/privileged-exec" ;
20+ import { sanitizeReadinessText } from "../../../readiness/sanitize" ;
2021import { readManagedSnapshotProfileAuthority } from "./managed-profile" ;
2122import { captureSandboxRuntimeSnapshot } from "./provider-lifecycle" ;
2223
@@ -42,27 +43,36 @@ interface SnapshotBackupAuthorityDependencies {
4243const MAX_OPENCLAW_CONFIG_BYTES = 16 * 1024 * 1024 ;
4344const OPENCLAW_CONFIG_CAPTURE_MAX_BUFFER = MAX_OPENCLAW_CONFIG_BYTES + 1024 * 1024 ;
4445const OPENCLAW_CONFIG_CAPTURE_TIMEOUT_MS = 30_000 ;
46+ const OPENCLAW_CONFIG_CAPTURE_PROTOCOL_PREFIX = "nemoclaw-openclaw-config-capture:" ;
47+ const OPENCLAW_CONFIG_CAPTURE_PROTOCOL_MAX_BYTES = 128 ;
48+ const OPENCLAW_CONFIG_CAPTURE_DIAGNOSTIC_MAX_BYTES = 1024 ;
4549const OPENCLAW_CONFIG_CAPTURE_SCRIPT = `import os, stat, sys
4650maximum = ${ MAX_OPENCLAW_CONFIG_BYTES }
4751directory = "/sandbox/.openclaw"
4852name = "openclaw.json"
53+ protocol = "${ OPENCLAW_CONFIG_CAPTURE_PROTOCOL_PREFIX } "
54+ def fail(status, reason):
55+ print(protocol + reason, file=sys.stderr)
56+ raise SystemExit(status)
4957directory_flags = os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
5058file_flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
5159try:
5260 directory_fd = os.open(directory, directory_flags)
5361except OSError:
54- raise SystemExit (10)
62+ fail (10, "directory-unavailable" )
5563try:
5664 try:
5765 file_fd = os.open(name, file_flags, dir_fd=directory_fd)
5866 except FileNotFoundError:
59- raise SystemExit(2 )
67+ fail(2, "missing" )
6068 except OSError:
61- raise SystemExit (10)
69+ fail (10, "file-unavailable" )
6270 try:
6371 before = os.fstat(file_fd)
64- if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1 or before.st_size > maximum:
65- raise SystemExit(11)
72+ if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1:
73+ fail(11, "unsafe-file-metadata")
74+ if before.st_size > maximum:
75+ fail(12, "size-limit-exceeded")
6676 chunks = []
6777 total = 0
6878 while True:
7282 chunks.append(chunk)
7383 total += len(chunk)
7484 if total > maximum:
75- raise SystemExit (12)
85+ fail (12, "size-limit-exceeded" )
7686 after = os.fstat(file_fd)
7787 current = os.stat(name, dir_fd=directory_fd, follow_symlinks=False)
7888 identity = lambda value: (value.st_dev, value.st_ino, value.st_size, value.st_mtime_ns, value.st_ctime_ns, value.st_nlink)
7989 if identity(before) != identity(after) or identity(before) != identity(current) or not stat.S_ISREG(current.st_mode):
80- raise SystemExit (13)
90+ fail (13, "file-changed-during-read" )
8191 sys.stdout.buffer.write(b"".join(chunks))
8292 finally:
8393 os.close(file_fd)
8494finally:
8595 os.close(directory_fd)
8696` ;
8797
98+ type OpenClawConfigCaptureFailure =
99+ | "missing"
100+ | "directory-unavailable"
101+ | "file-unavailable"
102+ | "unsafe-file-metadata"
103+ | "size-limit-exceeded"
104+ | "file-changed-during-read" ;
105+
106+ function captureFailureProtocol ( stderr : unknown ) : OpenClawConfigCaptureFailure | null {
107+ if (
108+ ( Buffer . isBuffer ( stderr ) && stderr . length > OPENCLAW_CONFIG_CAPTURE_PROTOCOL_MAX_BYTES ) ||
109+ ( typeof stderr === "string" &&
110+ Buffer . byteLength ( stderr ) > OPENCLAW_CONFIG_CAPTURE_PROTOCOL_MAX_BYTES )
111+ ) {
112+ return null ;
113+ }
114+ const value = Buffer . isBuffer ( stderr )
115+ ? stderr . toString ( "utf8" )
116+ : typeof stderr === "string"
117+ ? stderr
118+ : "" ;
119+ const line = value . endsWith ( "\n" ) ? value . slice ( 0 , - 1 ) : value ;
120+ if ( ! line . startsWith ( OPENCLAW_CONFIG_CAPTURE_PROTOCOL_PREFIX ) || / [ \r \n ] / . test ( line ) ) {
121+ return null ;
122+ }
123+ const reason = line . slice ( OPENCLAW_CONFIG_CAPTURE_PROTOCOL_PREFIX . length ) ;
124+ switch ( reason ) {
125+ case "missing" :
126+ case "directory-unavailable" :
127+ case "file-unavailable" :
128+ case "unsafe-file-metadata" :
129+ case "size-limit-exceeded" :
130+ case "file-changed-during-read" :
131+ return reason ;
132+ default :
133+ return null ;
134+ }
135+ }
136+
137+ function captureFailureDiagnostic ( stderr : unknown ) : string | null {
138+ const value = Buffer . isBuffer ( stderr )
139+ ? stderr . subarray ( 0 , OPENCLAW_CONFIG_CAPTURE_DIAGNOSTIC_MAX_BYTES ) . toString ( "utf8" )
140+ : typeof stderr === "string"
141+ ? Buffer . from ( stderr )
142+ . subarray ( 0 , OPENCLAW_CONFIG_CAPTURE_DIAGNOSTIC_MAX_BYTES )
143+ . toString ( "utf8" )
144+ : "" ;
145+ const sanitized = sanitizeReadinessText ( value , 240 ) . replace ( / \s + / g, " " ) . trim ( ) ;
146+ return sanitized || null ;
147+ }
148+
88149export function captureOpenClawStateFile (
89150 sandboxName : string ,
90151 request : sandboxState . StateFileCaptureRequest ,
@@ -113,7 +174,13 @@ export function captureOpenClawStateFile(
113174 timeout : OPENCLAW_CONFIG_CAPTURE_TIMEOUT_MS ,
114175 maxBuffer : OPENCLAW_CONFIG_CAPTURE_MAX_BUFFER ,
115176 } ) ;
116- if ( result . status === 2 && result . signal === null && ! result . error ) {
177+ const protocolFailure = captureFailureProtocol ( result . stderr ) ;
178+ if (
179+ result . status === 2 &&
180+ result . signal === null &&
181+ ! result . error &&
182+ protocolFailure === "missing"
183+ ) {
117184 return { outcome : "missing" } ;
118185 }
119186 if (
@@ -122,9 +189,13 @@ export function captureOpenClawStateFile(
122189 result . error ||
123190 ! Buffer . isBuffer ( result . stdout )
124191 ) {
125- const detail =
192+ const primaryDetail =
126193 result . error ?. message ??
127194 ( result . signal ? `signal ${ result . signal } ` : `exit ${ String ( result . status ) } ` ) ;
195+ const stderrDetail = protocolFailure
196+ ? `reason ${ protocolFailure } `
197+ : captureFailureDiagnostic ( result . stderr ) ;
198+ const detail = stderrDetail ? `${ primaryDetail } ; ${ stderrDetail } ` : primaryDetail ;
128199 return { outcome : "failed" , error : `privileged config capture failed: ${ detail } ` } ;
129200 }
130201 return { outcome : "backed_up" , data : result . stdout } ;
0 commit comments