#!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO="${REPO:-Sybil-2}" WORKLOAD="${WORKLOAD:-sybil-dev}" CLUSTER="${CLUSTER:-viper}" NAMESPACE="${NAMESPACE:-buzzert}" QUOTA="${QUOTA:-research-api}" SECRET="${SECRET:-sybil-dev-env}" REMOTE_ROOT="${REMOTE_ROOT:-/root/code/$REPO}" INGRESS_PORT="${INGRESS_PORT:-5173}" CODEX_IMAGE="${CODEX_IMAGE:-openai.azurecr.io/rcall:crow-1b540b5e15f9-2026-01-22-2404-312}" CODEX_SERVICE="${CODEX_SERVICE:-sybil-codex-devbox}" CODEX_CONTAINER="${CODEX_CONTAINER:-codex-devbox}" CODEX_SSH_SECRET="${CODEX_SSH_SECRET:-sybil-codex-ssh}" CODEX_SSH_PORT="${CODEX_SSH_PORT:-2222}" CODEX_WORKDIR="${CODEX_WORKDIR:-/workspace/sybil-codex}" CODEX_SSH_KEY_PATH="${CODEX_SSH_KEY_PATH:-/run/secrets/sybil-codex-ssh/id_ed25519}" echo "Syncing $REPO to $WORKLOAD on $CLUSTER/$NAMESPACE..." brix create \ --project-root "$(dirname "$ROOT")" \ --path "$REPO=$ROOT" \ --repositories "$REPO" \ --include-untracked \ --reconcile-policy continuous \ -- \ "cluster=$CLUSTER" \ "namespace=$NAMESPACE" \ "name=$WORKLOAD" \ "pool.spec.quota=$QUOTA" \ "pool.spec.size.resource=cpu" \ "pool.spec.size.quantity=1" \ "pool.spec.priority=low" if [[ "$REPO" != "openai" ]]; then if kubectl --context "$CLUSTER" -n "$NAMESPACE" get git "$WORKLOAD" -o json 2>/dev/null | jq -e '.spec.repositories.openai?' >/dev/null; then kubectl --context "$CLUSTER" -n "$NAMESPACE" patch git "$WORKLOAD" --type=json -p='[{"op":"remove","path":"/spec/repositories/openai"}]' fi fi if [[ -n "${OPENAI_API_KEY:-}" && -n "${EXA_API_KEY:-}" ]]; then kubectl --context "$CLUSTER" -n "$NAMESPACE" create secret generic "$SECRET" \ --from-literal=OPENAI_API_KEY="$OPENAI_API_KEY" \ --from-literal=EXA_API_KEY="$EXA_API_KEY" \ --dry-run=client -o yaml | kubectl --context "$CLUSTER" -n "$NAMESPACE" apply -f - elif [[ -n "${OPENAI_API_KEY:-}" || -n "${EXA_API_KEY:-}" ]]; then echo "Only one API key env var is set; keeping existing Kubernetes secret $SECRET." >&2 fi if [[ "$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get secret "$SECRET" -o name 2>/dev/null || true)" == "" ]]; then echo "Secret $SECRET does not exist. Re-run with OPENAI_API_KEY and EXA_API_KEY set." >&2 exit 1 fi if [[ "$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get secret "$CODEX_SSH_SECRET" -o name 2>/dev/null || true)" == "" ]]; then tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT ssh-keygen -t ed25519 -N "" -C "sybil-codex@$WORKLOAD" -f "$tmpdir/id_ed25519" >/dev/null kubectl --context "$CLUSTER" -n "$NAMESPACE" create secret generic "$CODEX_SSH_SECRET" \ --from-file=id_ed25519="$tmpdir/id_ed25519" \ --from-file=id_ed25519.pub="$tmpdir/id_ed25519.pub" \ --dry-run=client -o yaml | kubectl --context "$CLUSTER" -n "$NAMESPACE" apply -f - fi kubectl --context "$CLUSTER" -n "$NAMESPACE" patch secret "$SECRET" --type=merge -p "$(jq -n \ --arg host "$CODEX_SERVICE" \ --arg port "$CODEX_SSH_PORT" \ --arg workdir "$CODEX_WORKDIR" \ --arg keyPath "$CODEX_SSH_KEY_PATH" \ '{ stringData: { CHAT_CODEX_TOOL_ENABLED: "true", CHAT_CODEX_REMOTE_HOST: $host, CHAT_CODEX_REMOTE_USER: "root", CHAT_CODEX_REMOTE_PORT: $port, CHAT_CODEX_REMOTE_WORKDIR: $workdir, CHAT_CODEX_SSH_KEY_PATH: $keyPath, CHAT_CODEX_EXEC_TIMEOUT_MS: "600000", CHAT_SHELL_TOOL_ENABLED: "true" } }')" ensure_codex_deployment() { kubectl --context "$CLUSTER" -n "$NAMESPACE" apply -f - </dev/null 2>&1; then apt-get update apt-get install -y --no-install-recommends openssh-server ca-certificates git curl rm -rf /var/lib/apt/lists/* fi if ! command -v codex >/dev/null 2>&1; then npm install -g @openai/codex fi mkdir -p /run/sshd /root/.ssh "$CODEX_WORKDIR" cp /run/secrets/sybil-codex-ssh/id_ed25519.pub /root/.ssh/authorized_keys chmod 700 /root/.ssh chmod 600 /root/.ssh/authorized_keys { if [[ -n "\${OPENAI_API_KEY:-}" ]]; then printf 'export OPENAI_API_KEY=%q\n' "\$OPENAI_API_KEY" fi if [[ -n "\${EXA_API_KEY:-}" ]]; then printf 'export EXA_API_KEY=%q\n' "\$EXA_API_KEY" fi } >/root/.sybil-codex-env chmod 600 /root/.sybil-codex-env cat >/usr/local/bin/sybil-codex-ssh-command <<'SSH_COMMAND' #!/usr/bin/env bash set -euo pipefail if [[ -f /root/.sybil-codex-env ]]; then source /root/.sybil-codex-env fi if [[ -n "\${SSH_ORIGINAL_COMMAND:-}" ]]; then exec /bin/bash -lc "\$SSH_ORIGINAL_COMMAND" fi exec /bin/bash -l SSH_COMMAND chmod 700 /usr/local/bin/sybil-codex-ssh-command ssh-keygen -A cat >/tmp/sybil-codex-sshd_config </dev/null || true)" if [[ "$existing_env_from" == *"$SECRET"* ]]; then return fi local patch if [[ -z "$existing_env_from" ]]; then patch="[{\"op\":\"add\",\"path\":\"$patch_path\",\"value\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]}]" else patch="[{\"op\":\"add\",\"path\":\"$patch_path/-\",\"value\":{\"secretRef\":{\"name\":\"$SECRET\"}}}]" fi kubectl --context "$CLUSTER" -n "$NAMESPACE" patch "$resource" "$name" --type=json -p "$patch" } ensure_env_from \ workload \ "$WORKLOAD" \ "{.spec.pools[0].template.template.spec.workers.template.spec.containers[0].envFrom}" \ "/spec/pools/0/template/template/spec/workers/template/spec/containers/0/envFrom" ensure_env_from \ pool \ "$WORKLOAD" \ "{.spec.workers.template.spec.containers[0].envFrom}" \ "/spec/workers/template/spec/containers/0/envFrom" codex_ssh_volume_json="$(jq -n --arg secret "$CODEX_SSH_SECRET" '{ name: "sybil-codex-ssh", secret: { secretName: $secret, defaultMode: 384 } }')" codex_main_mount_json="$(jq -n '{name: "sybil-codex-ssh", mountPath: "/run/secrets/sybil-codex-ssh", readOnly: true}')" patch_pool_codex_mount() { local pool_json containers volumes patch pool_json="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pool "$WORKLOAD" -o json)" containers="$(jq \ --arg sidecarName "$CODEX_CONTAINER" \ --argjson mainMount "$codex_main_mount_json" \ ' def ensure_mount($mount): .volumeMounts = (((.volumeMounts // []) | map(select(.name != $mount.name))) + [$mount]); (.spec.workers.template.spec.containers // []) | map(if .name == "main" then ensure_mount($mainMount) else . end) | map(select(.name != $sidecarName)) ' <<<"$pool_json")" volumes="$(jq \ --argjson sshVolume "$codex_ssh_volume_json" \ ' (.spec.workers.template.spec.volumes // []) | map(select(.name != $sshVolume.name and .name != "sybil-codex-workspace")) + [$sshVolume] ' <<<"$pool_json")" patch="$(jq -n --argjson containers "$containers" --argjson volumes "$volumes" '{ spec: { workers: { template: { spec: { containers: $containers, volumes: $volumes } } } } }')" kubectl --context "$CLUSTER" -n "$NAMESPACE" patch pool "$WORKLOAD" --type=merge -p "$patch" } patch_workload_codex_mount() { local workload_json pools patch workload_json="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get workload "$WORKLOAD" -o json)" pools="$(jq \ --arg groupName "$WORKLOAD" \ --arg sidecarName "$CODEX_CONTAINER" \ --argjson mainMount "$codex_main_mount_json" \ --argjson sshVolume "$codex_ssh_volume_json" \ ' def ensure_mount($mount): .volumeMounts = (((.volumeMounts // []) | map(select(.name != $mount.name))) + [$mount]); .spec.pools | map( if .groupName == $groupName then .template.template.spec.workers.template.spec.containers = ((.template.template.spec.workers.template.spec.containers // []) | map(if .name == "main" then ensure_mount($mainMount) else . end) | map(select(.name != $sidecarName))) | .template.template.spec.workers.template.spec.volumes = ((.template.template.spec.workers.template.spec.volumes // []) | map(select(.name != $sshVolume.name and .name != "sybil-codex-workspace")) + [$sshVolume]) else . end ) ' <<<"$workload_json")" patch="$(jq -n --argjson pools "$pools" '{spec: {pools: $pools}}')" kubectl --context "$CLUSTER" -n "$NAMESPACE" patch workload "$WORKLOAD" --type=merge -p "$patch" } patch_workload_codex_mount patch_pool_codex_mount echo "Waiting for $WORKLOAD-0 to run the current pool revision..." deadline=$((SECONDS + 300)) while true; do pool_revision="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pool "$WORKLOAD" -o jsonpath='{.status.revision}' 2>/dev/null || true)" pod_revision="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pod "$WORKLOAD-0" -o jsonpath='{.metadata.labels.brix\.openai\.com/revision}' 2>/dev/null || true)" pod_ready="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pod "$WORKLOAD-0" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)" pod_deletion_timestamp="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pod "$WORKLOAD-0" -o jsonpath='{.metadata.deletionTimestamp}' 2>/dev/null || true)" if [[ -n "$pool_revision" && "$pod_revision" == "$pool_revision" && "$pod_ready" == "True" && -z "$pod_deletion_timestamp" ]]; then break fi if (( SECONDS >= deadline )); then echo "Timed out waiting for current ready pod. pool_revision=$pool_revision pod_revision=$pod_revision pod_ready=$pod_ready deleting=$pod_deletion_timestamp" >&2 exit 1 fi sleep 5 done brix run --clusters "$CLUSTER" --namespace "$NAMESPACE" --pools "$WORKLOAD" --dir "$REMOTE_ROOT" -- ./scripts/devbox-start.sh ssh_check="ssh -n -i '$CODEX_SSH_KEY_PATH' -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/sybil-codex-known-hosts -p '$CODEX_SSH_PORT' root@'$CODEX_SERVICE' true" brix run --clusters "$CLUSTER" --namespace "$NAMESPACE" --pools "$WORKLOAD" --dir "$REMOTE_ROOT" -- /bin/bash -lc "$ssh_check" url="$(brix ingress create --cluster "$CLUSTER" --namespace "$NAMESPACE" --pod "$WORKLOAD-0" --ports "web:$INGRESS_PORT" --root web --timeout 300 "$WORKLOAD" | tail -1)" echo "Devbox update complete: $url"