Skip to content

[codex] Add Codex workspace cron runner#594

Merged
boxp merged 1 commit into
mainfrom
feature/BOXP-16-codex-workspace-cron
May 29, 2026
Merged

[codex] Add Codex workspace cron runner#594
boxp merged 1 commit into
mainfrom
feature/BOXP-16-codex-workspace-cron

Conversation

@boxp
Copy link
Copy Markdown
Owner

@boxp boxp commented May 29, 2026

Summary

  • Add a Codex workspace cron registry ConfigMap with jobs.yaml, multiple prompt files, and a reusable codex exec runner.
  • Register multiple suspended-by-default Kubernetes CronJobs, each selecting a different jobs.yaml entry via CODEX_CRON_JOB_ID.
  • Add a CronJob-specific Calico NetworkPolicy and local runbook for enabling, manual runs, and adding more scheduled prompts.
  • Add the BOXP-16 implementation plan under docs/project_docs/BOXP-16-codex-workspace-cron/plan.md.

Related PR

Notes

OpenClaw's official cron implementation runs inside the Gateway and persists multiple jobs in ~/.openclaw/cron/jobs.json, state in jobs-state.json, and run history under runs/*.jsonl. Codex workspace does not have an equivalent resident Gateway scheduler, so this PR maps the multi-job registration model onto ConfigMap-managed jobs.yaml plus Kubernetes CronJobs as the scheduler.

Each job has its own id, schedule, promptFile, workdir, and optional model/profile settings. The initial jobs are disabled with both enabled: false and suspend: true to avoid unattended model usage until each prompt and schedule is reviewed.

Runner helper code uses Babashka (bb), not Python.

Validation

  • kustomize build argoproj/codex-workspace
  • extracted run-codex-cron.sh from the ConfigMap and ran bash -n
  • extracted select-codex-cron-job.bb and verified it selects both registered jobs from jobs.yaml
  • git diff --check

@github-actions
Copy link
Copy Markdown
Contributor

ArgoCD Diff Result

Auth path: tailscale

アプリケーション: codex-workspace の差分

パス: argoproj/codex-workspace

===== batch/CronJob /codex-cron-workspace-maintenance ======
0a1,95
> apiVersion: batch/v1
> kind: CronJob
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:batch/CronJob:codex-workspace/codex-cron-workspace-maintenance
>   labels:
>     app: codex-workspace-cron
>     codex-workspace.boxp.io/cron: workspace-maintenance
>   name: codex-cron-workspace-maintenance
> spec:
>   concurrencyPolicy: Forbid
>   failedJobsHistoryLimit: 3
>   jobTemplate:
>     spec:
>       backoffLimit: 0
>       template:
>         metadata:
>           labels:
>             app: codex-workspace-cron
>             codex-workspace.boxp.io/cron: workspace-maintenance
>         spec:
>           affinity:
>             podAffinity:
>               requiredDuringSchedulingIgnoredDuringExecution:
>               - labelSelector:
>                   matchLabels:
>                     app: codex-workspace
>                 topologyKey: kubernetes.io/hostname
>           automountServiceAccountToken: false
>           containers:
>           - command:
>             - /opt/codex-cron/run-codex-cron.sh
>             env:
>             - name: HOME
>               value: /home/boxp
>             - name: CODEX_HOME
>               value: /home/boxp/.codex
>             - name: CODEX_CRON_NAME
>               value: workspace-maintenance
>             - name: CODEX_CRON_PROMPT_FILE
>               value: /opt/codex-cron/prompt-workspace-maintenance.md
>             - name: CODEX_CRON_WORKDIR
>               value: /home/boxp
>             - name: CODEX_CRON_OUTPUT_ROOT
>               value: /home/boxp/.codex-cron/runs
>             - name: CODEX_CRON_BYPASS_APPROVALS
>               value: "true"
>             - name: GRAFANA_URL
>               value: http://grafana.monitoring.svc.cluster.local:3000
>             - name: GRAFANA_SERVICE_ACCOUNT_TOKEN
>               valueFrom:
>                 secretKeyRef:
>                   key: service-account-token
>                   name: codex-workspace-grafana
>             image: ghcr.io/boxp/arch/codex-workspace:latest
>             imagePullPolicy: Always
>             name: codex
>             resources:
>               limits:
>                 cpu: "4"
>                 memory: 8Gi
>               requests:
>                 cpu: 250m
>                 memory: 512Mi
>             securityContext:
>               allowPrivilegeEscalation: false
>               capabilities:
>                 drop:
>                 - ALL
>               readOnlyRootFilesystem: false
>               runAsGroup: 1000
>               runAsNonRoot: true
>               runAsUser: 1000
>             volumeMounts:
>             - mountPath: /home/boxp
>               name: home
>             - mountPath: /opt/codex-cron
>               name: cron-config
>               readOnly: true
>           nodeSelector:
>             kubernetes.io/arch: amd64
>           restartPolicy: Never
>           volumes:
>           - name: home
>             persistentVolumeClaim:
>               claimName: codex-workspace-home
>           - configMap:
>               defaultMode: 365
>               name: codex-workspace-cron
>             name: cron-config
>   schedule: 0 22 * * *
>   startingDeadlineSeconds: 900
>   successfulJobsHistoryLimit: 3
>   suspend: true
>   timeZone: Etc/UTC

===== /ConfigMap codex-workspace/codex-workspace-cron ======
0a1,120
> apiVersion: v1
> data:
>   prompt-workspace-maintenance.md: |
>     あなたは Codex workspace の定期実行ジョブです。
> 
>     目的:
>     - Codex workspace の状態を軽く確認する。
>     - 変更は加えず、必要な次アクションだけを短く報告する。
> 
>     確認対象:
>     - `/home/boxp/Documents/obsidian-headless/BOXP/Tickets/BOXP-16.md`
>     - `/home/boxp/ghq/github.com/boxp/lolice`
>     - `/home/boxp/ghq/github.com/boxp/arch`
> 
>     制約:
>     - ファイル編集、commit、push、PR 作成はしない。
>     - 問題があれば、最後の応答に箇条書きで記録する。
>   run-codex-cron.sh: |
>     #!/usr/bin/env bash
>     set -Eeuo pipefail
> 
>     job_name="${CODEX_CRON_NAME:?CODEX_CRON_NAME is required}"
>     prompt_file="${CODEX_CRON_PROMPT_FILE:?CODEX_CRON_PROMPT_FILE is required}"
>     workdir="${CODEX_CRON_WORKDIR:-/home/boxp}"
>     output_root="${CODEX_CRON_OUTPUT_ROOT:-/home/boxp/.codex-cron/runs}"
>     lock_root="${CODEX_CRON_LOCK_ROOT:-/home/boxp/.codex-cron/locks}"
>     run_id="${CODEX_CRON_RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)}"
>     run_dir="${output_root}/${job_name}/${run_id}"
>     lock_dir="${lock_root}/${job_name}.lock"
> 
>     mkdir -p "${run_dir}" "${lock_root}"
> 
>     if ! mkdir "${lock_dir}" 2>/dev/null; then
>       lock_stale_seconds="${CODEX_CRON_LOCK_STALE_SECONDS:-43200}"
>       lock_mtime="$(stat -c %Y "${lock_dir}" 2>/dev/null || echo 0)"
>       now_epoch="$(date -u +%s)"
>       if (( now_epoch - lock_mtime > lock_stale_seconds )); then
>         echo "removing stale codex cron lock: ${lock_dir}" >&2
>         rm -rf "${lock_dir}"
>         mkdir "${lock_dir}"
>       else
>         echo "codex cron job '${job_name}' is already running: ${lock_dir}" >&2
>         exit 75
>       fi
>     fi
>     cleanup() {
>       rmdir "${lock_dir}" 2>/dev/null || true
>     }
>     trap cleanup EXIT
> 
>     started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
>     stdout_log="${run_dir}/events.jsonl"
>     stderr_log="${run_dir}/stderr.log"
>     last_message="${run_dir}/last-message.md"
>     summary_file="${run_dir}/summary.json"
> 
>     codex_args=(exec --json --cd "${workdir}" --output-last-message "${last_message}")
> 
>     if [[ "${CODEX_CRON_BYPASS_APPROVALS:-true}" == "true" ]]; then
>       codex_args+=(--dangerously-bypass-approvals-and-sandbox)
>     else
>       codex_args+=(--sandbox "${CODEX_CRON_SANDBOX:-workspace-write}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_MODEL:-}" ]]; then
>       codex_args+=(--model "${CODEX_CRON_MODEL}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_PROFILE:-}" ]]; then
>       codex_args+=(--profile "${CODEX_CRON_PROFILE}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_EXTRA_ARGS:-}" ]]; then
>       # Shell-style quoting is intentionally not supported. Keep values simple
>       # and use dedicated env vars above for options that need arguments.
>       read -r -a extra_args <<< "${CODEX_CRON_EXTRA_ARGS}"
>       codex_args+=("${extra_args[@]}")
>     fi
> 
>     {
>       echo "job=${job_name}"
>       echo "run_id=${run_id}"
>       echo "started_at=${started_at}"
>       echo "workdir=${workdir}"
>       echo "prompt_file=${prompt_file}"
>       echo "codex_args=${codex_args[*]}"
>     } > "${run_dir}/metadata.env"
> 
>     set +e
>     codex "${codex_args[@]}" - < "${prompt_file}" > "${stdout_log}" 2> "${stderr_log}"
>     exit_code=$?
>     set -e
> 
>     finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
>     status="ok"
>     if [[ "${exit_code}" -ne 0 ]]; then
>       status="error"
>     fi
> 
>     cat > "${summary_file}" <<EOF
>     {
>       "job": "${job_name}",
>       "runId": "${run_id}",
>       "status": "${status}",
>       "exitCode": ${exit_code},
>       "startedAt": "${started_at}",
>       "finishedAt": "${finished_at}",
>       "workdir": "${workdir}",
>       "promptFile": "${prompt_file}"
>     }
>     EOF
> 
>     echo "Codex cron ${job_name}/${run_id}: ${status} (exit ${exit_code})"
>     exit "${exit_code}"
> kind: ConfigMap
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:/ConfigMap:codex-workspace/codex-workspace-cron
>   name: codex-workspace-cron
>   namespace: codex-workspace

===== projectcalico.org/NetworkPolicy codex-workspace/codex-workspace-cron-network-policy ======
0a1,43
> apiVersion: projectcalico.org/v3
> kind: NetworkPolicy
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:projectcalico.org/NetworkPolicy:codex-workspace/codex-workspace-cron-network-policy
>   name: codex-workspace-cron-network-policy
>   namespace: codex-workspace
> spec:
>   egress:
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
>       ports:
>       - 53
>       selector: k8s-app == 'kube-dns'
>     protocol: UDP
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
>       ports:
>       - 53
>       selector: k8s-app == 'kube-dns'
>     protocol: TCP
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'monitoring'
>       ports:
>       - 3000
>       selector: app.kubernetes.io/component == 'grafana' && app.kubernetes.io/name
>         == 'grafana' && app.kubernetes.io/part-of == 'kube-prometheus'
>     protocol: TCP
>   - action: Allow
>     destination:
>       ports:
>       - 22
>       - 80
>       - 443
>     protocol: TCP
>   ingress: []
>   selector: app == 'codex-workspace-cron'
>   types:
>   - Ingress
>   - Egress
ℹ️ 上記の差分が見つかりました

@boxp boxp force-pushed the feature/BOXP-16-codex-workspace-cron branch from d78fe1a to ef0ffb6 Compare May 29, 2026 12:10
@github-actions
Copy link
Copy Markdown
Contributor

ArgoCD Diff Result

Auth path: tailscale

アプリケーション: codex-workspace の差分

パス: argoproj/codex-workspace

===== batch/CronJob /codex-cron-ticket-review ======
0a1,89
> apiVersion: batch/v1
> kind: CronJob
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:batch/CronJob:codex-workspace/codex-cron-ticket-review
>   labels:
>     app: codex-workspace-cron
>     codex-workspace.boxp.io/cron: ticket-review
>   name: codex-cron-ticket-review
> spec:
>   concurrencyPolicy: Forbid
>   failedJobsHistoryLimit: 3
>   jobTemplate:
>     spec:
>       backoffLimit: 0
>       template:
>         metadata:
>           labels:
>             app: codex-workspace-cron
>             codex-workspace.boxp.io/cron: ticket-review
>         spec:
>           affinity:
>             podAffinity:
>               requiredDuringSchedulingIgnoredDuringExecution:
>               - labelSelector:
>                   matchLabels:
>                     app: codex-workspace
>                 topologyKey: kubernetes.io/hostname
>           automountServiceAccountToken: false
>           containers:
>           - command:
>             - /opt/codex-cron/run-codex-cron.sh
>             env:
>             - name: HOME
>               value: /home/boxp
>             - name: CODEX_HOME
>               value: /home/boxp/.codex
>             - name: CODEX_CRON_JOB_ID
>               value: ticket-review
>             - name: CODEX_CRON_JOBS_FILE
>               value: /opt/codex-cron/jobs.yaml
>             - name: GRAFANA_URL
>               value: http://grafana.monitoring.svc.cluster.local:3000
>             - name: GRAFANA_SERVICE_ACCOUNT_TOKEN
>               valueFrom:
>                 secretKeyRef:
>                   key: service-account-token
>                   name: codex-workspace-grafana
>             image: ghcr.io/boxp/arch/codex-workspace:latest
>             imagePullPolicy: Always
>             name: codex
>             resources:
>               limits:
>                 cpu: "4"
>                 memory: 8Gi
>               requests:
>                 cpu: 250m
>                 memory: 512Mi
>             securityContext:
>               allowPrivilegeEscalation: false
>               capabilities:
>                 drop:
>                 - ALL
>               readOnlyRootFilesystem: false
>               runAsGroup: 1000
>               runAsNonRoot: true
>               runAsUser: 1000
>             volumeMounts:
>             - mountPath: /home/boxp
>               name: home
>             - mountPath: /opt/codex-cron
>               name: cron-config
>               readOnly: true
>           nodeSelector:
>             kubernetes.io/arch: amd64
>           restartPolicy: Never
>           volumes:
>           - name: home
>             persistentVolumeClaim:
>               claimName: codex-workspace-home
>           - configMap:
>               defaultMode: 365
>               name: codex-workspace-cron
>             name: cron-config
>   schedule: 30 22 * * *
>   startingDeadlineSeconds: 900
>   successfulJobsHistoryLimit: 3
>   suspend: true
>   timeZone: Etc/UTC

===== projectcalico.org/NetworkPolicy codex-workspace/codex-workspace-cron-network-policy ======
0a1,43
> apiVersion: projectcalico.org/v3
> kind: NetworkPolicy
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:projectcalico.org/NetworkPolicy:codex-workspace/codex-workspace-cron-network-policy
>   name: codex-workspace-cron-network-policy
>   namespace: codex-workspace
> spec:
>   egress:
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
>       ports:
>       - 53
>       selector: k8s-app == 'kube-dns'
>     protocol: UDP
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
>       ports:
>       - 53
>       selector: k8s-app == 'kube-dns'
>     protocol: TCP
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'monitoring'
>       ports:
>       - 3000
>       selector: app.kubernetes.io/component == 'grafana' && app.kubernetes.io/name
>         == 'grafana' && app.kubernetes.io/part-of == 'kube-prometheus'
>     protocol: TCP
>   - action: Allow
>     destination:
>       ports:
>       - 22
>       - 80
>       - 443
>     protocol: TCP
>   ingress: []
>   selector: app == 'codex-workspace-cron'
>   types:
>   - Ingress
>   - Egress

===== /ConfigMap codex-workspace/codex-workspace-cron ======
0a1,234
> apiVersion: v1
> data:
>   jobs.yaml: |
>     jobs:
>       - id: workspace-maintenance
>         name: Workspace maintenance
>         enabled: false
>         schedule: "0 22 * * *"
>         timeZone: Etc/UTC
>         session: isolated
>         promptFile: /opt/codex-cron/prompt-workspace-maintenance.md
>         workdir: /home/boxp
>         outputRoot: /home/boxp/.codex-cron/runs
>         bypassApprovals: true
>       - id: ticket-review
>         name: Ticket review
>         enabled: false
>         schedule: "30 22 * * *"
>         timeZone: Etc/UTC
>         session: isolated
>         promptFile: /opt/codex-cron/prompt-ticket-review.md
>         workdir: /home/boxp
>         outputRoot: /home/boxp/.codex-cron/runs
>         bypassApprovals: true
>   prompt-ticket-review.md: |
>     あなたは Codex workspace の定期実行ジョブです。
> 
>     目的:
>     - Obsidian vault のタスクボードを確認する。
>     - stale なチケットや次アクションが曖昧なチケットを短く報告する。
> 
>     確認対象:
>     - `/home/boxp/Documents/obsidian-headless/BOXP/Boards/Task Board.md`
>     - `/home/boxp/Documents/obsidian-headless/BOXP/Tickets`
> 
>     制約:
>     - ファイル編集、commit、push、PR 作成はしない。
>     - 日本語で簡潔に報告する。
>   prompt-workspace-maintenance.md: |
>     あなたは Codex workspace の定期実行ジョブです。
> 
>     目的:
>     - Codex workspace の状態を軽く確認する。
>     - 変更は加えず、必要な次アクションだけを短く報告する。
> 
>     確認対象:
>     - `/home/boxp/Documents/obsidian-headless/BOXP/Tickets/BOXP-16.md`
>     - `/home/boxp/ghq/github.com/boxp/lolice`
>     - `/home/boxp/ghq/github.com/boxp/arch`
> 
>     制約:
>     - ファイル編集、commit、push、PR 作成はしない。
>     - 問題があれば、最後の応答に箇条書きで記録する。
>   run-codex-cron.sh: |
>     #!/usr/bin/env bash
>     set -Eeuo pipefail
> 
>     job_id="${CODEX_CRON_JOB_ID:?CODEX_CRON_JOB_ID is required}"
>     jobs_file="${CODEX_CRON_JOBS_FILE:-/opt/codex-cron/jobs.yaml}"
> 
>     eval "$(
>       python3 /opt/codex-cron/select-codex-cron-job.py "${jobs_file}" "${job_id}"
>     )"
> 
>     job_name="${CODEX_CRON_NAME:-${CODEX_CRON_JOB_ID}}"
>     prompt_file="${CODEX_CRON_PROMPT_FILE:?CODEX_CRON_PROMPT_FILE is required}"
>     workdir="${CODEX_CRON_WORKDIR:-/home/boxp}"
>     output_root="${CODEX_CRON_OUTPUT_ROOT:-/home/boxp/.codex-cron/runs}"
>     lock_root="${CODEX_CRON_LOCK_ROOT:-/home/boxp/.codex-cron/locks}"
>     run_id="${CODEX_CRON_RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)}"
>     run_dir="${output_root}/${job_id}/${run_id}"
>     lock_dir="${lock_root}/${job_id}.lock"
> 
>     mkdir -p "${run_dir}" "${lock_root}"
> 
>     if ! mkdir "${lock_dir}" 2>/dev/null; then
>       lock_stale_seconds="${CODEX_CRON_LOCK_STALE_SECONDS:-43200}"
>       lock_mtime="$(stat -c %Y "${lock_dir}" 2>/dev/null || echo 0)"
>       now_epoch="$(date -u +%s)"
>       if (( now_epoch - lock_mtime > lock_stale_seconds )); then
>         echo "removing stale codex cron lock: ${lock_dir}" >&2
>         rm -rf "${lock_dir}"
>         mkdir "${lock_dir}"
>       else
>         echo "codex cron job '${job_name}' is already running: ${lock_dir}" >&2
>         exit 75
>       fi
>     fi
>     cleanup() {
>       rmdir "${lock_dir}" 2>/dev/null || true
>     }
>     trap cleanup EXIT
> 
>     started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
>     stdout_log="${run_dir}/events.jsonl"
>     stderr_log="${run_dir}/stderr.log"
>     last_message="${run_dir}/last-message.md"
>     summary_file="${run_dir}/summary.json"
> 
>     codex_args=(exec --json --cd "${workdir}" --output-last-message "${last_message}")
> 
>     if [[ "${CODEX_CRON_BYPASS_APPROVALS:-true}" == "true" ]]; then
>       codex_args+=(--dangerously-bypass-approvals-and-sandbox)
>     else
>       codex_args+=(--sandbox "${CODEX_CRON_SANDBOX:-workspace-write}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_MODEL:-}" ]]; then
>       codex_args+=(--model "${CODEX_CRON_MODEL}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_PROFILE:-}" ]]; then
>       codex_args+=(--profile "${CODEX_CRON_PROFILE}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_EXTRA_ARGS:-}" ]]; then
>       # Shell-style quoting is intentionally not supported. Keep values simple
>       # and use dedicated env vars above for options that need arguments.
>       read -r -a extra_args <<< "${CODEX_CRON_EXTRA_ARGS}"
>       codex_args+=("${extra_args[@]}")
>     fi
> 
>     {
>       echo "job=${job_name}"
>       echo "job_id=${job_id}"
>       echo "run_id=${run_id}"
>       echo "started_at=${started_at}"
>       echo "workdir=${workdir}"
>       echo "prompt_file=${prompt_file}"
>       echo "codex_args=${codex_args[*]}"
>     } > "${run_dir}/metadata.env"
> 
>     set +e
>     codex "${codex_args[@]}" - < "${prompt_file}" > "${stdout_log}" 2> "${stderr_log}"
>     exit_code=$?
>     set -e
> 
>     finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
>     status="ok"
>     if [[ "${exit_code}" -ne 0 ]]; then
>       status="error"
>     fi
> 
>     cat > "${summary_file}" <<EOF
>     {
>       "job": "${job_id}",
>       "name": "${job_name}",
>       "runId": "${run_id}",
>       "status": "${status}",
>       "exitCode": ${exit_code},
>       "startedAt": "${started_at}",
>       "finishedAt": "${finished_at}",
>       "workdir": "${workdir}",
>       "promptFile": "${prompt_file}"
>     }
>     EOF
> 
>     echo "Codex cron ${job_id}/${run_id}: ${status} (exit ${exit_code})"
>     exit "${exit_code}"
>   select-codex-cron-job.py: |
>     #!/usr/bin/env python3
>     import re
>     import shlex
>     import sys
> 
>     SCALAR_RE = re.compile(r"^([A-Za-z][A-Za-z0-9]*):(?: +(.*))?$")
> 
>     def parse_scalar(raw):
>       value = (raw or "").strip()
>       if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
>         value = value[1:-1]
>       if value.lower() == "true":
>         return True
>       if value.lower() == "false":
>         return False
>       return value
> 
>     def load_jobs(path):
>       jobs = []
>       current = None
>       with open(path, encoding="utf-8") as f:
>         for line in f:
>           stripped = line.strip()
>           if not stripped or stripped.startswith("#") or stripped == "jobs:":
>             continue
>           if stripped.startswith("- "):
>             if current:
>               jobs.append(current)
>             current = {}
>             stripped = stripped[2:].strip()
>             if not stripped:
>               continue
>           if current is None:
>             raise SystemExit(f"invalid jobs file: property outside a job: {line.rstrip()}")
>           match = SCALAR_RE.match(stripped)
>           if not match:
>             raise SystemExit(f"unsupported jobs.yaml line: {line.rstrip()}")
>           current[match.group(1)] = parse_scalar(match.group(2))
>       if current:
>         jobs.append(current)
>       return jobs
> 
>     def emit(name, value):
>       if value is None or value == "":
>         return
>       print(f"{name}={shlex.quote(str(value))}")
> 
>     def main():
>       if len(sys.argv) != 3:
>         raise SystemExit("usage: select-codex-cron-job.py <jobs.yaml> <job-id>")
>       jobs = load_jobs(sys.argv[1])
>       job = next((j for j in jobs if j.get("id") == sys.argv[2]), None)
>       if not job:
>         raise SystemExit(f"codex cron job not found: {sys.argv[2]}")
>       if job.get("enabled") is not True:
>         raise SystemExit(f"codex cron job is disabled in jobs.yaml: {sys.argv[2]}")
> 
>       emit("CODEX_CRON_NAME", job.get("name") or job.get("id"))
>       emit("CODEX_CRON_PROMPT_FILE", job.get("promptFile"))
>       emit("CODEX_CRON_WORKDIR", job.get("workdir"))
>       emit("CODEX_CRON_OUTPUT_ROOT", job.get("outputRoot"))
>       emit("CODEX_CRON_MODEL", job.get("model"))
>       emit("CODEX_CRON_PROFILE", job.get("profile"))
>       emit("CODEX_CRON_BYPASS_APPROVALS", str(job.get("bypassApprovals", True)).lower())
>       emit("CODEX_CRON_EXTRA_ARGS", job.get("extraArgs"))
> 
>     if __name__ == "__main__":
>       main()
> kind: ConfigMap
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:/ConfigMap:codex-workspace/codex-workspace-cron
>   name: codex-workspace-cron
>   namespace: codex-workspace

===== batch/CronJob /codex-cron-workspace-maintenance ======
0a1,89
> apiVersion: batch/v1
> kind: CronJob
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:batch/CronJob:codex-workspace/codex-cron-workspace-maintenance
>   labels:
>     app: codex-workspace-cron
>     codex-workspace.boxp.io/cron: workspace-maintenance
>   name: codex-cron-workspace-maintenance
> spec:
>   concurrencyPolicy: Forbid
>   failedJobsHistoryLimit: 3
>   jobTemplate:
>     spec:
>       backoffLimit: 0
>       template:
>         metadata:
>           labels:
>             app: codex-workspace-cron
>             codex-workspace.boxp.io/cron: workspace-maintenance
>         spec:
>           affinity:
>             podAffinity:
>               requiredDuringSchedulingIgnoredDuringExecution:
>               - labelSelector:
>                   matchLabels:
>                     app: codex-workspace
>                 topologyKey: kubernetes.io/hostname
>           automountServiceAccountToken: false
>           containers:
>           - command:
>             - /opt/codex-cron/run-codex-cron.sh
>             env:
>             - name: HOME
>               value: /home/boxp
>             - name: CODEX_HOME
>               value: /home/boxp/.codex
>             - name: CODEX_CRON_JOB_ID
>               value: workspace-maintenance
>             - name: CODEX_CRON_JOBS_FILE
>               value: /opt/codex-cron/jobs.yaml
>             - name: GRAFANA_URL
>               value: http://grafana.monitoring.svc.cluster.local:3000
>             - name: GRAFANA_SERVICE_ACCOUNT_TOKEN
>               valueFrom:
>                 secretKeyRef:
>                   key: service-account-token
>                   name: codex-workspace-grafana
>             image: ghcr.io/boxp/arch/codex-workspace:latest
>             imagePullPolicy: Always
>             name: codex
>             resources:
>               limits:
>                 cpu: "4"
>                 memory: 8Gi
>               requests:
>                 cpu: 250m
>                 memory: 512Mi
>             securityContext:
>               allowPrivilegeEscalation: false
>               capabilities:
>                 drop:
>                 - ALL
>               readOnlyRootFilesystem: false
>               runAsGroup: 1000
>               runAsNonRoot: true
>               runAsUser: 1000
>             volumeMounts:
>             - mountPath: /home/boxp
>               name: home
>             - mountPath: /opt/codex-cron
>               name: cron-config
>               readOnly: true
>           nodeSelector:
>             kubernetes.io/arch: amd64
>           restartPolicy: Never
>           volumes:
>           - name: home
>             persistentVolumeClaim:
>               claimName: codex-workspace-home
>           - configMap:
>               defaultMode: 365
>               name: codex-workspace-cron
>             name: cron-config
>   schedule: 0 22 * * *
>   startingDeadlineSeconds: 900
>   successfulJobsHistoryLimit: 3
>   suspend: true
>   timeZone: Etc/UTC
ℹ️ 上記の差分が見つかりました

@boxp boxp force-pushed the feature/BOXP-16-codex-workspace-cron branch from ef0ffb6 to eb1ca30 Compare May 29, 2026 12:23
@github-actions
Copy link
Copy Markdown
Contributor

ArgoCD Diff Result

Auth path: tailscale

アプリケーション: codex-workspace の差分

パス: argoproj/codex-workspace

===== projectcalico.org/NetworkPolicy codex-workspace/codex-workspace-cron-network-policy ======
0a1,43
> apiVersion: projectcalico.org/v3
> kind: NetworkPolicy
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:projectcalico.org/NetworkPolicy:codex-workspace/codex-workspace-cron-network-policy
>   name: codex-workspace-cron-network-policy
>   namespace: codex-workspace
> spec:
>   egress:
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
>       ports:
>       - 53
>       selector: k8s-app == 'kube-dns'
>     protocol: UDP
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
>       ports:
>       - 53
>       selector: k8s-app == 'kube-dns'
>     protocol: TCP
>   - action: Allow
>     destination:
>       namespaceSelector: kubernetes.io/metadata.name == 'monitoring'
>       ports:
>       - 3000
>       selector: app.kubernetes.io/component == 'grafana' && app.kubernetes.io/name
>         == 'grafana' && app.kubernetes.io/part-of == 'kube-prometheus'
>     protocol: TCP
>   - action: Allow
>     destination:
>       ports:
>       - 22
>       - 80
>       - 443
>     protocol: TCP
>   ingress: []
>   selector: app == 'codex-workspace-cron'
>   types:
>   - Ingress
>   - Egress

===== /ConfigMap codex-workspace/codex-workspace-cron ======
0a1,244
> apiVersion: v1
> data:
>   jobs.yaml: |
>     jobs:
>       - id: workspace-maintenance
>         name: Workspace maintenance
>         enabled: false
>         schedule: "0 22 * * *"
>         timeZone: Etc/UTC
>         session: isolated
>         promptFile: /opt/codex-cron/prompt-workspace-maintenance.md
>         workdir: /home/boxp
>         outputRoot: /home/boxp/.codex-cron/runs
>         bypassApprovals: true
>       - id: ticket-review
>         name: Ticket review
>         enabled: false
>         schedule: "30 22 * * *"
>         timeZone: Etc/UTC
>         session: isolated
>         promptFile: /opt/codex-cron/prompt-ticket-review.md
>         workdir: /home/boxp
>         outputRoot: /home/boxp/.codex-cron/runs
>         bypassApprovals: true
>   prompt-ticket-review.md: |
>     あなたは Codex workspace の定期実行ジョブです。
> 
>     目的:
>     - Obsidian vault のタスクボードを確認する。
>     - stale なチケットや次アクションが曖昧なチケットを短く報告する。
> 
>     確認対象:
>     - `/home/boxp/Documents/obsidian-headless/BOXP/Boards/Task Board.md`
>     - `/home/boxp/Documents/obsidian-headless/BOXP/Tickets`
> 
>     制約:
>     - ファイル編集、commit、push、PR 作成はしない。
>     - 日本語で簡潔に報告する。
>   prompt-workspace-maintenance.md: |
>     あなたは Codex workspace の定期実行ジョブです。
> 
>     目的:
>     - Codex workspace の状態を軽く確認する。
>     - 変更は加えず、必要な次アクションだけを短く報告する。
> 
>     確認対象:
>     - `/home/boxp/Documents/obsidian-headless/BOXP/Tickets/BOXP-16.md`
>     - `/home/boxp/ghq/github.com/boxp/lolice`
>     - `/home/boxp/ghq/github.com/boxp/arch`
> 
>     制約:
>     - ファイル編集、commit、push、PR 作成はしない。
>     - 問題があれば、最後の応答に箇条書きで記録する。
>   run-codex-cron.sh: |
>     #!/usr/bin/env bash
>     set -Eeuo pipefail
> 
>     job_id="${CODEX_CRON_JOB_ID:?CODEX_CRON_JOB_ID is required}"
>     jobs_file="${CODEX_CRON_JOBS_FILE:-/opt/codex-cron/jobs.yaml}"
> 
>     eval "$(
>       bb /opt/codex-cron/select-codex-cron-job.bb "${jobs_file}" "${job_id}"
>     )"
> 
>     job_name="${CODEX_CRON_NAME:-${CODEX_CRON_JOB_ID}}"
>     prompt_file="${CODEX_CRON_PROMPT_FILE:?CODEX_CRON_PROMPT_FILE is required}"
>     workdir="${CODEX_CRON_WORKDIR:-/home/boxp}"
>     output_root="${CODEX_CRON_OUTPUT_ROOT:-/home/boxp/.codex-cron/runs}"
>     lock_root="${CODEX_CRON_LOCK_ROOT:-/home/boxp/.codex-cron/locks}"
>     run_id="${CODEX_CRON_RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)}"
>     run_dir="${output_root}/${job_id}/${run_id}"
>     lock_dir="${lock_root}/${job_id}.lock"
> 
>     mkdir -p "${run_dir}" "${lock_root}"
> 
>     if ! mkdir "${lock_dir}" 2>/dev/null; then
>       lock_stale_seconds="${CODEX_CRON_LOCK_STALE_SECONDS:-43200}"
>       lock_mtime="$(stat -c %Y "${lock_dir}" 2>/dev/null || echo 0)"
>       now_epoch="$(date -u +%s)"
>       if (( now_epoch - lock_mtime > lock_stale_seconds )); then
>         echo "removing stale codex cron lock: ${lock_dir}" >&2
>         rm -rf "${lock_dir}"
>         mkdir "${lock_dir}"
>       else
>         echo "codex cron job '${job_name}' is already running: ${lock_dir}" >&2
>         exit 75
>       fi
>     fi
>     cleanup() {
>       rmdir "${lock_dir}" 2>/dev/null || true
>     }
>     trap cleanup EXIT
> 
>     started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
>     stdout_log="${run_dir}/events.jsonl"
>     stderr_log="${run_dir}/stderr.log"
>     last_message="${run_dir}/last-message.md"
>     summary_file="${run_dir}/summary.json"
> 
>     codex_args=(exec --json --cd "${workdir}" --output-last-message "${last_message}")
> 
>     if [[ "${CODEX_CRON_BYPASS_APPROVALS:-true}" == "true" ]]; then
>       codex_args+=(--dangerously-bypass-approvals-and-sandbox)
>     else
>       codex_args+=(--sandbox "${CODEX_CRON_SANDBOX:-workspace-write}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_MODEL:-}" ]]; then
>       codex_args+=(--model "${CODEX_CRON_MODEL}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_PROFILE:-}" ]]; then
>       codex_args+=(--profile "${CODEX_CRON_PROFILE}")
>     fi
> 
>     if [[ -n "${CODEX_CRON_EXTRA_ARGS:-}" ]]; then
>       # Shell-style quoting is intentionally not supported. Keep values simple
>       # and use dedicated env vars above for options that need arguments.
>       read -r -a extra_args <<< "${CODEX_CRON_EXTRA_ARGS}"
>       codex_args+=("${extra_args[@]}")
>     fi
> 
>     {
>       echo "job=${job_name}"
>       echo "job_id=${job_id}"
>       echo "run_id=${run_id}"
>       echo "started_at=${started_at}"
>       echo "workdir=${workdir}"
>       echo "prompt_file=${prompt_file}"
>       echo "codex_args=${codex_args[*]}"
>     } > "${run_dir}/metadata.env"
> 
>     set +e
>     codex "${codex_args[@]}" - < "${prompt_file}" > "${stdout_log}" 2> "${stderr_log}"
>     exit_code=$?
>     set -e
> 
>     finished_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
>     status="ok"
>     if [[ "${exit_code}" -ne 0 ]]; then
>       status="error"
>     fi
> 
>     cat > "${summary_file}" <<EOF
>     {
>       "job": "${job_id}",
>       "name": "${job_name}",
>       "runId": "${run_id}",
>       "status": "${status}",
>       "exitCode": ${exit_code},
>       "startedAt": "${started_at}",
>       "finishedAt": "${finished_at}",
>       "workdir": "${workdir}",
>       "promptFile": "${prompt_file}"
>     }
>     EOF
> 
>     echo "Codex cron ${job_id}/${run_id}: ${status} (exit ${exit_code})"
>     exit "${exit_code}"
>   select-codex-cron-job.bb: |
>     #!/usr/bin/env bb
> 
>     (require '[clojure.string :as str])
> 
>     (defn fail [message]
>       (binding [*out* *err*]
>         (println message))
>       (System/exit 1))
> 
>     (defn shell-quote [value]
>       (let [s (str value)]
>         (str "'" (str/replace s #"'" "'\"'\"'") "'")))
> 
>     (defn parse-scalar [raw]
>       (let [value (str/trim (or raw ""))]
>         (cond
>           (and (>= (count value) 2)
>                (= (first value) (last value))
>                (#{\" \'} (first value)))
>           (subs value 1 (dec (count value)))
> 
>           (= "true" (str/lower-case value)) true
>           (= "false" (str/lower-case value)) false
>           :else value)))
> 
>     (defn parse-jobs [text]
>       (loop [lines (str/split-lines text)
>              jobs []
>              current nil]
>         (if-not (seq lines)
>           (cond-> jobs current (conj current))
>           (let [raw (first lines)
>                 stripped (str/trim raw)]
>             (cond
>               (or (empty? stripped) (= stripped "jobs:") (str/starts-with? stripped "#"))
>               (recur (rest lines) jobs current)
> 
>               (str/starts-with? stripped "- ")
>               (let [jobs (cond-> jobs current (conj current))
>                     stripped (str/trim (subs stripped 2))
>                     current {}]
>                 (if (empty? stripped)
>                   (recur (rest lines) jobs current)
>                   (recur (cons stripped (rest lines)) jobs current)))
> 
>               (nil? current)
>               (fail (str "invalid jobs file: property outside a job: " raw))
> 
>               (not (str/includes? stripped ":"))
>               (fail (str "unsupported jobs.yaml line: " raw))
> 
>               :else
>               (let [[k v] (str/split stripped #":" 2)
>                     k (str/trim k)]
>                 (when-not (re-matches #"[A-Za-z][A-Za-z0-9]*" k)
>                   (fail (str "unsupported key: " k)))
>                 (recur (rest lines) jobs (assoc current k (parse-scalar v)))))))))
> 
>     (defn emit [name value]
>       (when-not (or (nil? value) (= "" value))
>         (println (str name "=" (shell-quote value)))))
> 
>     (let [[jobs-file job-id] *command-line-args*]
>       (when-not (and jobs-file job-id)
>         (fail "usage: select-codex-cron-job.bb <jobs.yaml> <job-id>"))
>       (let [job (first (filter #(= job-id (get % "id")) (parse-jobs (slurp jobs-file))))]
>         (when-not job
>           (fail (str "codex cron job not found: " job-id)))
>         (when-not (true? (get job "enabled"))
>           (fail (str "codex cron job is disabled in jobs.yaml: " job-id)))
>         (emit "CODEX_CRON_NAME" (or (get job "name") (get job "id")))
>         (emit "CODEX_CRON_PROMPT_FILE" (get job "promptFile"))
>         (emit "CODEX_CRON_WORKDIR" (get job "workdir"))
>         (emit "CODEX_CRON_OUTPUT_ROOT" (get job "outputRoot"))
>         (emit "CODEX_CRON_MODEL" (get job "model"))
>         (emit "CODEX_CRON_PROFILE" (get job "profile"))
>         (emit "CODEX_CRON_BYPASS_APPROVALS" (str (get job "bypassApprovals" true)))
>         (emit "CODEX_CRON_EXTRA_ARGS" (get job "extraArgs"))))
> kind: ConfigMap
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:/ConfigMap:codex-workspace/codex-workspace-cron
>   name: codex-workspace-cron
>   namespace: codex-workspace

===== batch/CronJob /codex-cron-ticket-review ======
0a1,89
> apiVersion: batch/v1
> kind: CronJob
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:batch/CronJob:codex-workspace/codex-cron-ticket-review
>   labels:
>     app: codex-workspace-cron
>     codex-workspace.boxp.io/cron: ticket-review
>   name: codex-cron-ticket-review
> spec:
>   concurrencyPolicy: Forbid
>   failedJobsHistoryLimit: 3
>   jobTemplate:
>     spec:
>       backoffLimit: 0
>       template:
>         metadata:
>           labels:
>             app: codex-workspace-cron
>             codex-workspace.boxp.io/cron: ticket-review
>         spec:
>           affinity:
>             podAffinity:
>               requiredDuringSchedulingIgnoredDuringExecution:
>               - labelSelector:
>                   matchLabels:
>                     app: codex-workspace
>                 topologyKey: kubernetes.io/hostname
>           automountServiceAccountToken: false
>           containers:
>           - command:
>             - /opt/codex-cron/run-codex-cron.sh
>             env:
>             - name: HOME
>               value: /home/boxp
>             - name: CODEX_HOME
>               value: /home/boxp/.codex
>             - name: CODEX_CRON_JOB_ID
>               value: ticket-review
>             - name: CODEX_CRON_JOBS_FILE
>               value: /opt/codex-cron/jobs.yaml
>             - name: GRAFANA_URL
>               value: http://grafana.monitoring.svc.cluster.local:3000
>             - name: GRAFANA_SERVICE_ACCOUNT_TOKEN
>               valueFrom:
>                 secretKeyRef:
>                   key: service-account-token
>                   name: codex-workspace-grafana
>             image: ghcr.io/boxp/arch/codex-workspace:latest
>             imagePullPolicy: Always
>             name: codex
>             resources:
>               limits:
>                 cpu: "4"
>                 memory: 8Gi
>               requests:
>                 cpu: 250m
>                 memory: 512Mi
>             securityContext:
>               allowPrivilegeEscalation: false
>               capabilities:
>                 drop:
>                 - ALL
>               readOnlyRootFilesystem: false
>               runAsGroup: 1000
>               runAsNonRoot: true
>               runAsUser: 1000
>             volumeMounts:
>             - mountPath: /home/boxp
>               name: home
>             - mountPath: /opt/codex-cron
>               name: cron-config
>               readOnly: true
>           nodeSelector:
>             kubernetes.io/arch: amd64
>           restartPolicy: Never
>           volumes:
>           - name: home
>             persistentVolumeClaim:
>               claimName: codex-workspace-home
>           - configMap:
>               defaultMode: 365
>               name: codex-workspace-cron
>             name: cron-config
>   schedule: 30 22 * * *
>   startingDeadlineSeconds: 900
>   successfulJobsHistoryLimit: 3
>   suspend: true
>   timeZone: Etc/UTC

===== batch/CronJob /codex-cron-workspace-maintenance ======
0a1,89
> apiVersion: batch/v1
> kind: CronJob
> metadata:
>   annotations:
>     argocd.argoproj.io/tracking-id: codex-workspace:batch/CronJob:codex-workspace/codex-cron-workspace-maintenance
>   labels:
>     app: codex-workspace-cron
>     codex-workspace.boxp.io/cron: workspace-maintenance
>   name: codex-cron-workspace-maintenance
> spec:
>   concurrencyPolicy: Forbid
>   failedJobsHistoryLimit: 3
>   jobTemplate:
>     spec:
>       backoffLimit: 0
>       template:
>         metadata:
>           labels:
>             app: codex-workspace-cron
>             codex-workspace.boxp.io/cron: workspace-maintenance
>         spec:
>           affinity:
>             podAffinity:
>               requiredDuringSchedulingIgnoredDuringExecution:
>               - labelSelector:
>                   matchLabels:
>                     app: codex-workspace
>                 topologyKey: kubernetes.io/hostname
>           automountServiceAccountToken: false
>           containers:
>           - command:
>             - /opt/codex-cron/run-codex-cron.sh
>             env:
>             - name: HOME
>               value: /home/boxp
>             - name: CODEX_HOME
>               value: /home/boxp/.codex
>             - name: CODEX_CRON_JOB_ID
>               value: workspace-maintenance
>             - name: CODEX_CRON_JOBS_FILE
>               value: /opt/codex-cron/jobs.yaml
>             - name: GRAFANA_URL
>               value: http://grafana.monitoring.svc.cluster.local:3000
>             - name: GRAFANA_SERVICE_ACCOUNT_TOKEN
>               valueFrom:
>                 secretKeyRef:
>                   key: service-account-token
>                   name: codex-workspace-grafana
>             image: ghcr.io/boxp/arch/codex-workspace:latest
>             imagePullPolicy: Always
>             name: codex
>             resources:
>               limits:
>                 cpu: "4"
>                 memory: 8Gi
>               requests:
>                 cpu: 250m
>                 memory: 512Mi
>             securityContext:
>               allowPrivilegeEscalation: false
>               capabilities:
>                 drop:
>                 - ALL
>               readOnlyRootFilesystem: false
>               runAsGroup: 1000
>               runAsNonRoot: true
>               runAsUser: 1000
>             volumeMounts:
>             - mountPath: /home/boxp
>               name: home
>             - mountPath: /opt/codex-cron
>               name: cron-config
>               readOnly: true
>           nodeSelector:
>             kubernetes.io/arch: amd64
>           restartPolicy: Never
>           volumes:
>           - name: home
>             persistentVolumeClaim:
>               claimName: codex-workspace-home
>           - configMap:
>               defaultMode: 365
>               name: codex-workspace-cron
>             name: cron-config
>   schedule: 0 22 * * *
>   startingDeadlineSeconds: 900
>   successfulJobsHistoryLimit: 3
>   suspend: true
>   timeZone: Etc/UTC
ℹ️ 上記の差分が見つかりました

@boxp boxp marked this pull request as ready for review May 29, 2026 12:27
@boxp boxp merged commit ca2f70a into main May 29, 2026
2 checks passed
@boxp boxp deleted the feature/BOXP-16-codex-workspace-cron branch May 29, 2026 12:27
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb1ca30baa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +23 to +24
labels:
app: codex-workspace-cron
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Grafana への ingress 許可に合うラベルにしてください

cron Pod には app: codex-workspace-cron だけが付くため、既存の argoproj/prometheus-operator/overlays/network-policy-grafana.yaml で許可されている codex-workspace namespace かつ podSelector app: codex-workspace に一致しません。この状態では、cron 側の egress と GRAFANA_URL/token を追加しても、Grafana 側の ingress NetworkPolicy により TCP 3000 がブロックされるため、CronJob から Grafana/MCP を使う実行だけ失敗します。Grafana 側の policy に app: codex-workspace-cron を追加するか、Pod ラベルを許可済みセレクタにも一致させる必要があります。

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant