@@ -44,6 +44,15 @@ const {
4444const {
4545 agentSupportsWebSearch,
4646} : typeof import ( "./onboard/web-search-support" ) = require ( "./onboard/web-search-support" ) ;
47+ const dashboardAccess : typeof import ( "./onboard/dashboard-access" ) = require ( "./onboard/dashboard-access" ) ;
48+ const {
49+ buildGatewayBootstrapSecretsScript,
50+ createGatewayBootstrapRepairHelpers,
51+ getGatewayBootstrapRepairPlan,
52+ } : typeof import ( "./onboard/gateway-bootstrap" ) = require ( "./onboard/gateway-bootstrap" ) ;
53+ const {
54+ verifyWebSearchInsideSandbox : verifyWebSearchInsideSandboxWithDeps ,
55+ } : typeof import ( "./onboard/web-search-verify" ) = require ( "./onboard/web-search-verify" ) ;
4756const {
4857 verifyWebSearchInsideSandbox : verifyWebSearchInsideSandboxWithDeps ,
4958} : typeof import ( "./onboard/web-search-verify" ) = require ( "./onboard/web-search-verify" ) ;
@@ -346,12 +355,6 @@ const DIM = USE_COLOR ? "\x1b[2m" : "";
346355const RESET = USE_COLOR ? "\x1b[0m" : "" ;
347356let OPENSHELL_BIN : string | null = null ;
348357const GATEWAY_NAME = "nemoclaw" ;
349- const GATEWAY_BOOTSTRAP_SECRET_NAMES = [
350- "openshell-server-tls" ,
351- "openshell-server-client-ca" ,
352- "openshell-client-tls" ,
353- "openshell-ssh-handshake" ,
354- ] ;
355358const BACK_TO_SELECTION = "__NEMOCLAW_BACK_TO_SELECTION__" ;
356359type HermesAuthMethod = "oauth" | "api_key" ;
357360const HERMES_AUTH_METHOD_OAUTH : HermesAuthMethod = "oauth" ;
@@ -3183,6 +3186,15 @@ function getGatewayLocalEndpoint(): string {
31833186 return dockerDriverGatewayEnv . getGatewayHttpsEndpoint ( ) ;
31843187}
31853188
3189+ const {
3190+ gatewayClusterHealthcheckPassed,
3191+ repairGatewayBootstrapSecrets,
3192+ } = createGatewayBootstrapRepairHelpers ( {
3193+ buildGatewayClusterExecArgv,
3194+ run,
3195+ runCapture,
3196+ } ) ;
3197+
31863198function isLinuxDockerDriverGatewayEnabled (
31873199 platform : NodeJS . Platform = process . platform ,
31883200 arch : NodeJS . Architecture = process . arch ,
@@ -3550,117 +3562,7 @@ function registerDockerDriverGatewayEndpoint(): boolean {
35503562 return ok ;
35513563}
35523564
3553- function getGatewayBootstrapRepairPlan ( missingSecrets : string [ ] = [ ] ) {
3554- const allowed = new Set ( GATEWAY_BOOTSTRAP_SECRET_NAMES ) ;
3555- const normalized = [
3556- ...new Set ( ( missingSecrets || [ ] ) . map ( ( name ) => String ( name ) . trim ( ) ) . filter ( Boolean ) ) ,
3557- ] . filter ( ( name ) => allowed . has ( name ) ) ;
3558- const missing = new Set ( normalized ) ;
3559- const needsClientBundle =
3560- missing . has ( "openshell-server-client-ca" ) || missing . has ( "openshell-client-tls" ) ;
35613565
3562- return {
3563- missingSecrets : normalized ,
3564- needsRepair : normalized . length > 0 ,
3565- needsServerTls : missing . has ( "openshell-server-tls" ) ,
3566- needsClientBundle,
3567- needsHandshake : missing . has ( "openshell-ssh-handshake" ) ,
3568- } ;
3569- }
3570-
3571- function buildGatewayBootstrapSecretsScript ( missingSecrets : string [ ] = [ ] ) : string {
3572- const plan = getGatewayBootstrapRepairPlan ( missingSecrets ) ;
3573- if ( ! plan . needsRepair ) return "exit 0" ;
3574-
3575- return `
3576- set -eu
3577- export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
3578- kubectl get namespace openshell >/dev/null 2>&1
3579- kubectl -n openshell get statefulset/openshell >/dev/null 2>&1
3580- TMPDIR="$(mktemp -d)"
3581- cleanup() {
3582- rm -rf "$TMPDIR"
3583- }
3584- trap cleanup EXIT
3585- if ${ plan . needsServerTls ? "true" : "false" } ; then
3586- cat >"$TMPDIR/server-ext.cnf" <<'EOF'
3587- subjectAltName=DNS:openshell,DNS:openshell.openshell,DNS:openshell.openshell.svc,DNS:openshell.openshell.svc.cluster.local,DNS:localhost,IP:127.0.0.1
3588- extendedKeyUsage=serverAuth
3589- EOF
3590- openssl req -nodes -newkey rsa:2048 -keyout "$TMPDIR/server.key" -out "$TMPDIR/server.csr" -subj "/CN=openshell.openshell.svc.cluster.local" >/dev/null 2>&1
3591- openssl x509 -req -in "$TMPDIR/server.csr" -signkey "$TMPDIR/server.key" -out "$TMPDIR/server.crt" -days 3650 -sha256 -extfile "$TMPDIR/server-ext.cnf" >/dev/null 2>&1
3592- kubectl create secret tls -n openshell openshell-server-tls --cert="$TMPDIR/server.crt" --key="$TMPDIR/server.key" --dry-run=client -o yaml | kubectl apply -f -
3593- fi
3594- if ${ plan . needsClientBundle ? "true" : "false" } ; then
3595- cat >"$TMPDIR/client-ext.cnf" <<'EOF'
3596- extendedKeyUsage=clientAuth
3597- EOF
3598- openssl req -x509 -nodes -newkey rsa:2048 -keyout "$TMPDIR/client-ca.key" -out "$TMPDIR/client-ca.crt" -subj "/CN=openshell-client-ca" -days 3650 >/dev/null 2>&1
3599- openssl req -nodes -newkey rsa:2048 -keyout "$TMPDIR/client.key" -out "$TMPDIR/client.csr" -subj "/CN=openshell-client" >/dev/null 2>&1
3600- openssl x509 -req -in "$TMPDIR/client.csr" -CA "$TMPDIR/client-ca.crt" -CAkey "$TMPDIR/client-ca.key" -CAcreateserial -out "$TMPDIR/client.crt" -days 3650 -sha256 -extfile "$TMPDIR/client-ext.cnf" >/dev/null 2>&1
3601- kubectl create secret generic -n openshell openshell-server-client-ca --from-file=ca.crt="$TMPDIR/client-ca.crt" --dry-run=client -o yaml | kubectl apply -f -
3602- kubectl create secret generic -n openshell openshell-client-tls --from-file=tls.crt="$TMPDIR/client.crt" --from-file=tls.key="$TMPDIR/client.key" --from-file=ca.crt="$TMPDIR/client-ca.crt" --dry-run=client -o yaml | kubectl apply -f -
3603- fi
3604- if ${ plan . needsHandshake ? "true" : "false" } ; then
3605- kubectl create secret generic -n openshell openshell-ssh-handshake --from-literal=secret="$(openssl rand -hex 32)" --dry-run=client -o yaml | kubectl apply -f -
3606- fi
3607- ` ;
3608- }
3609-
3610- function runGatewayClusterCapture ( script : string , opts : RunnerOptions = { } ) {
3611- return runCapture ( buildGatewayClusterExecArgv ( script ) , opts ) ;
3612- }
3613-
3614- function runGatewayCluster ( script : string , opts : RunnerOptions = { } ) {
3615- return run ( buildGatewayClusterExecArgv ( script ) , opts ) ;
3616- }
3617-
3618- function listMissingGatewayBootstrapSecrets ( ) {
3619- const output = runGatewayClusterCapture (
3620- `
3621- set -eu
3622- export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
3623- kubectl get namespace openshell >/dev/null 2>&1 || exit 0
3624- kubectl -n openshell get statefulset/openshell >/dev/null 2>&1 || exit 0
3625- for name in ${ GATEWAY_BOOTSTRAP_SECRET_NAMES . map ( ( name ) => shellQuote ( name ) ) . join ( " " ) } ; do
3626- kubectl -n openshell get secret "$name" >/dev/null 2>&1 || printf '%s\\n' "$name"
3627- done
3628- ` ,
3629- { ignoreError : true } ,
3630- ) ;
3631- return output
3632- . split ( "\n" )
3633- . map ( ( line ) => line . trim ( ) )
3634- . filter ( Boolean ) ;
3635- }
3636-
3637- function gatewayClusterHealthcheckPassed ( ) : boolean {
3638- const result = runGatewayCluster ( "/usr/local/bin/cluster-healthcheck.sh" , {
3639- ignoreError : true ,
3640- suppressOutput : true ,
3641- } ) ;
3642- return result . status === 0 ;
3643- }
3644-
3645- function repairGatewayBootstrapSecrets ( ) : { repaired : boolean ; missingSecrets : string [ ] } {
3646- const missingSecrets = listMissingGatewayBootstrapSecrets ( ) ;
3647- const plan = getGatewayBootstrapRepairPlan ( missingSecrets ) ;
3648- if ( ! plan . needsRepair ) return { repaired : false , missingSecrets } ;
3649-
3650- console . log (
3651- ` OpenShell bootstrap secrets missing: ${ plan . missingSecrets . join ( ", " ) } . Repairing...` ,
3652- ) ;
3653- const repairResult = runGatewayCluster ( buildGatewayBootstrapSecretsScript ( plan . missingSecrets ) , {
3654- ignoreError : true ,
3655- suppressOutput : true ,
3656- } ) ;
3657- const remainingSecrets = listMissingGatewayBootstrapSecrets ( ) ;
3658- if ( repairResult . status === 0 && remainingSecrets . length === 0 ) {
3659- console . log ( " ✓ OpenShell bootstrap secrets created" ) ;
3660- return { repaired : true , missingSecrets : remainingSecrets } ;
3661- }
3662- return { repaired : false , missingSecrets : remainingSecrets } ;
3663- }
36643566
36653567function attachGatewayMetadataIfNeeded ( {
36663568 forceRefresh = false ,
@@ -9770,172 +9672,75 @@ function fetchGatewayAuthTokenFromSandbox(sandboxName: string): string | null {
97709672
97719673function buildDashboardChain (
97729674 chatUiUrl = process . env . CHAT_UI_URL || `http://127.0.0.1:${ CONTROL_UI_PORT } ` ,
9773- options : {
9774- wslHostAddress ?: string | null ;
9775- runCapture ?: typeof runCapture ;
9776- env ?: NodeJS . ProcessEnv ;
9777- platform ?: NodeJS . Platform ;
9778- release ?: string ;
9779- isWsl ?: boolean ;
9780- } = { } ,
9675+ options : Parameters < typeof dashboardAccess . buildDashboardChain > [ 1 ] = { } ,
97819676) {
9782- return buildChain ( {
9783- chatUiUrl,
9784- isWsl : isWsl ( options ) ,
9785- wslHostAddress : getWslHostAddress ( options ) ,
9786- } ) ;
9677+ return dashboardAccess . buildDashboardChain ( chatUiUrl , { ...options , runCapture : options . runCapture || runCapture } ) ;
97879678}
97889679
97899680function getDashboardForwardPort (
97909681 chatUiUrl = process . env . CHAT_UI_URL || `http://127.0.0.1:${ CONTROL_UI_PORT } ` ,
9791- options : {
9792- wslHostAddress ?: string | null ;
9793- runCapture ?: typeof runCapture ;
9794- env ?: NodeJS . ProcessEnv ;
9795- platform ?: NodeJS . Platform ;
9796- release ?: string ;
9797- isWsl ?: boolean ;
9798- } = { } ,
9682+ options : Parameters < typeof dashboardAccess . getDashboardForwardPort > [ 1 ] = { } ,
97999683) : string {
9800- return String ( buildDashboardChain ( chatUiUrl , options ) . port ) ;
9684+ return dashboardAccess . getDashboardForwardPort ( chatUiUrl , {
9685+ ...options ,
9686+ runCapture : options . runCapture || runCapture ,
9687+ } ) ;
98019688}
98029689
98039690function getDashboardForwardTarget (
98049691 chatUiUrl = process . env . CHAT_UI_URL || `http://127.0.0.1:${ CONTROL_UI_PORT } ` ,
9805- options : {
9806- wslHostAddress ?: string | null ;
9807- runCapture ?: typeof runCapture ;
9808- env ?: NodeJS . ProcessEnv ;
9809- platform ?: NodeJS . Platform ;
9810- release ?: string ;
9811- isWsl ?: boolean ;
9812- chatUiUrl ?: string ;
9813- token ?: string | null ;
9814- } = { } ,
9692+ options : Parameters < typeof dashboardAccess . getDashboardForwardTarget > [ 1 ] = { } ,
98159693) : string {
9816- return buildDashboardChain ( chatUiUrl , options ) . forwardTarget ;
9694+ return dashboardAccess . getDashboardForwardTarget ( chatUiUrl , {
9695+ ...options ,
9696+ runCapture : options . runCapture || runCapture ,
9697+ } ) ;
98179698}
98189699
98199700function getDashboardForwardStartCommand (
98209701 sandboxName : string ,
9821- options : {
9822- chatUiUrl ?: string ;
9823- openshellBinary ?: string ;
9824- wslHostAddress ?: string | null ;
9825- runCapture ?: typeof runCapture ;
9826- env ?: NodeJS . ProcessEnv ;
9827- platform ?: NodeJS . Platform ;
9828- release ?: string ;
9829- isWsl ?: boolean ;
9830- token ?: string | null ;
9831- } = { } ,
9702+ options : Parameters < typeof dashboardAccess . getDashboardForwardStartCommand > [ 1 ] = { } ,
98329703) : string {
9833- const chatUiUrl =
9834- options . chatUiUrl || process . env . CHAT_UI_URL || `http://127.0.0.1:${ CONTROL_UI_PORT } ` ;
9835- const forwardTarget = getDashboardForwardTarget ( chatUiUrl , options ) ;
9836- return `${ openshellShellCommand (
9837- [ "forward" , "start" , "--background" , forwardTarget , sandboxName ] ,
9838- options ,
9839- ) } `;
9704+ return dashboardAccess . getDashboardForwardStartCommand ( sandboxName , {
9705+ ...options ,
9706+ runCapture : options . runCapture || runCapture ,
9707+ openshellShellCommand,
9708+ } ) ;
98409709}
98419710
98429711function buildAuthenticatedDashboardUrl ( baseUrl : string , token : string | null = null ) : string {
9843- if ( ! token ) return baseUrl ;
9844- return `${ baseUrl } #token=${ encodeURIComponent ( token ) } ` ;
9712+ return dashboardAccess . buildAuthenticatedDashboardUrl ( baseUrl , token ) ;
98459713}
98469714
98479715function dashboardUrlForDisplay ( url : string ) : string {
9848- return redact ( url . replace ( / # t o k e n = [ ^ \s ' " ] * $ / i , "" ) ) ;
9716+ return dashboardAccess . dashboardUrlForDisplay ( url , redact ) ;
98499717}
98509718
98519719function getWslHostAddress (
9852- options : {
9853- wslHostAddress ?: string | null ;
9854- runCapture ?: typeof runCapture ;
9855- env ?: NodeJS . ProcessEnv ;
9856- platform ?: NodeJS . Platform ;
9857- release ?: string ;
9858- isWsl ?: boolean ;
9859- } = { } ,
9720+ options : Parameters < typeof dashboardAccess . getWslHostAddress > [ 0 ] = { } ,
98609721) : string | null {
9861- if ( options . wslHostAddress ) {
9862- return options . wslHostAddress ;
9863- }
9864- if ( ! isWsl ( options ) ) {
9865- return null ;
9866- }
9867- const runCaptureFn = options . runCapture || runCapture ;
9868- const output = runCaptureFn ( [ "hostname" , "-I" ] , { ignoreError : true } ) ;
9869- return (
9870- String ( output || "" )
9871- . trim ( )
9872- . split ( / \s + / )
9873- . filter ( Boolean ) [ 0 ] || null
9874- ) ;
9722+ return dashboardAccess . getWslHostAddress ( { ...options , runCapture : options . runCapture || runCapture } ) ;
98759723}
98769724
98779725function getDashboardAccessInfo (
98789726 sandboxName : string ,
9879- options : {
9880- token ?: string | null ;
9881- chatUiUrl ?: string ;
9882- wslHostAddress ?: string | null ;
9883- runCapture ?: typeof runCapture ;
9884- env ?: NodeJS . ProcessEnv ;
9885- platform ?: NodeJS . Platform ;
9886- release ?: string ;
9887- isWsl ?: boolean ;
9888- } = { } ,
9727+ options : Parameters < typeof dashboardAccess . getDashboardAccessInfo > [ 1 ] = { } ,
98899728) {
9890- const token = Object . prototype . hasOwnProperty . call ( options , "token" )
9891- ? options . token
9892- : fetchGatewayAuthTokenFromSandbox ( sandboxName ) ;
9893- const chatUiUrl =
9894- options . chatUiUrl || process . env . CHAT_UI_URL || `http://127.0.0.1:${ CONTROL_UI_PORT } ` ;
9895- const chain = buildDashboardChain ( chatUiUrl , options ) ;
9896- const dashboardAccess = buildControlUiUrls ( token , chain . port , chain . accessUrl ) . map (
9897- ( url , index ) => ( {
9898- label : index === 0 ? "Dashboard" : `Alt ${ index } ` ,
9899- url : buildAuthenticatedDashboardUrl ( url , null ) ,
9900- } ) ,
9901- ) ;
9902-
9903- const wslHostAddress = getWslHostAddress ( options ) ;
9904- if ( wslHostAddress ) {
9905- const wslUrl = buildAuthenticatedDashboardUrl ( `http://${ wslHostAddress } :${ chain . port } /` , token ) ;
9906- if ( ! dashboardAccess . some ( ( access ) => access . url === wslUrl ) ) {
9907- dashboardAccess . push ( { label : "VS Code/WSL" , url : wslUrl } ) ;
9908- }
9909- }
9910-
9911- return dashboardAccess ;
9729+ return dashboardAccess . getDashboardAccessInfo ( sandboxName , {
9730+ ...options ,
9731+ runCapture : options . runCapture || runCapture ,
9732+ fetchGatewayAuthToken : fetchGatewayAuthTokenFromSandbox ,
9733+ } ) ;
99129734}
99139735
99149736function getDashboardGuidanceLines (
9915- dashboardAccess : Array < { label : string ; url : string } > = [ ] ,
9916- options : {
9917- chatUiUrl ?: string ;
9918- wslHostAddress ?: string | null ;
9919- runCapture ?: typeof runCapture ;
9920- env ?: NodeJS . ProcessEnv ;
9921- platform ?: NodeJS . Platform ;
9922- release ?: string ;
9923- isWsl ?: boolean ;
9924- } = { } ,
9737+ access : Parameters < typeof dashboardAccess . getDashboardGuidanceLines > [ 0 ] = [ ] ,
9738+ options : Parameters < typeof dashboardAccess . getDashboardGuidanceLines > [ 1 ] = { } ,
99259739) : string [ ] {
9926- const chatUiUrl =
9927- options . chatUiUrl || process . env . CHAT_UI_URL || `http://127.0.0.1:${ CONTROL_UI_PORT } ` ;
9928- const chain = buildDashboardChain ( chatUiUrl , options ) ;
9929- const guidance = [ `Port ${ String ( chain . port ) } must be forwarded before opening these URLs.` ] ;
9930- if ( isWsl ( options ) ) {
9931- guidance . push (
9932- "WSL detected: if localhost fails in Windows, use the WSL host IP shown by `hostname -I`." ,
9933- ) ;
9934- }
9935- if ( dashboardAccess . length === 0 ) {
9936- guidance . push ( "No dashboard URLs were generated." ) ;
9937- }
9938- return guidance ;
9740+ return dashboardAccess . getDashboardGuidanceLines ( access , {
9741+ ...options ,
9742+ runCapture : options . runCapture || runCapture ,
9743+ } ) ;
99399744}
99409745/** Print the post-onboard dashboard with sandbox status and reconfiguration hints. */
99419746function printDashboard (
0 commit comments