diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 08bc3653d..f778fba6f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/framework-r-d/phlex-dev:latest +FROM ghcr.io/framework-r-d/phlex-dev:2026-07-23 # Validate Python site-packages symlink. # Invoke bash explicitly: the script uses bash features (pipefail, arrays, @@ -93,9 +93,8 @@ INSTALL_MAN_TOOLS RUN bash <<'INSTALL_DEV_UTILS' set -euo pipefail apt-get update -apt-get install -y --no-install-recommends jq podman ssh socat tree yq apt purge nodejs npm -y && apt autoremove -y || true -apt-get install -y curl --no-install-recommends --autoremove +apt-get install -y --no-install-recommends chrpath curl jq patchelf nano podman ssh socat tree vim yq curl -fsSL https://deb.nodesource.com/setup_26.x | bash - apt-get install -y nodejs apt-get clean @@ -115,3 +114,10 @@ RUN printf '. /entrypoint.sh\n' >> /root/.bashrc # Ensure /run/user/0 exists and is owned by root with 700 permissions to # allow GPG agent to function correctly when running as root. RUN mkdir -p /run/user/0 && chmod 700 /run/user/0 && chown root:root /run/user/0 + +COPY gitignore-global /root/.gitignore-global +RUN git config --global core.excludesfile '/root/.gitignore-global' + +# Install Kilo environment script that sets KILO_CONFIG_CONTENT globally +COPY kilo-env.sh /etc/profile.d/kilo-env.sh +RUN chmod +x /etc/profile.d/kilo-env.sh diff --git a/.devcontainer/codespace.code-workspace b/.devcontainer/codespace.code-workspace index b7b289bf1..81a554751 100644 --- a/.devcontainer/codespace.code-workspace +++ b/.devcontainer/codespace.code-workspace @@ -2,23 +2,23 @@ "folders": [ { "name": "Phlex", - "path": "/workspaces/phlex" + "path": ".." }, { "name": "Phlex Design", - "path": "/workspaces/phlex-design" + "path": "../../phlex-design" }, { "name": "Phlex Examples", - "path": "/workspaces/phlex-examples" + "path": "../../phlex-examples" }, { "name": "Phlex Coding Guidelines", - "path": "/workspaces/phlex-coding-guidelines" + "path": "../../phlex-coding-guidelines" }, { "name": "Phlex Spack Recipes", - "path": "/workspaces/phlex-spack-recipes" + "path": "../../phlex-spack-recipes" } ], "settings": { @@ -63,39 +63,16 @@ }, "python.languageServer": "Pylance", "python.terminal.activateEnvironment": false, - "python.defaultInterpreterPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", - "esbonio.server.pythonPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", + // Container-only interpreter paths (python.defaultInterpreterPath, + // esbonio.server.pythonPath) are intentionally not set here: this file + // is shared with host (non-devcontainer) checkouts, where + // /opt/spack-environments does not exist. The devcontainer supplies + // them via the Remote scope in devcontainer.json (Remote settings only + // apply when actually connected to that container). Host users should + // set their own interpreter in a local .vscode/settings.json (Folder + // scope overrides this Workspace scope) -- see + // .vscode/settings.json.example. "esbonio.logging.level": "debug", - "python.analysis.typeCheckingMode": "basic", - "python.analysis.diagnosticMode": "workspace", - "python.analysis.include": [ - "/workspaces/phlex" - ], - "python.analysis.extraPaths": [ - "/workspaces/phlex/plugins/python/python", - "/opt/spack-environments/phlex-ci/.spack-env/view/lib/root", - "/opt/spack-environments/phlex-ci/.spack-env/view/lib/python/site-packages" - ], - "python.analysis.exclude": [ - "/opt/spack-environments", - "/workspaces/phlex/local", - "/workspaces/phlex/build/_deps", - "/workspaces/phlex/build/CMakeFiles", - "/workspaces/phlex/**/.venv", - "/workspaces/phlex/**/py_virtual_env" - ], - "python.analysis.ignore": [ - "/opt/spack-environments", - "/workspaces/phlex-design", - "/workspaces/phlex-examples", - "/workspaces/phlex-coding-guidelines", - "/workspaces/phlex-spack-recipes", - "/workspaces/phlex/local", - "/workspaces/phlex/build/_deps", - "/workspaces/phlex/build/CMakeFiles", - "/workspaces/phlex/**/.venv", - "/workspaces/phlex/**/py_virtual_env" - ], "flake8.enabled": false, "pylint.enabled": false, "[python]": { diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 91b3276d7..ce8ebdc57 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,63 +2,64 @@ "name": "Phlex CI Dev Container", "dockerComposeFile": "docker-compose.yml", "service": "phlex-dev", - "workspaceFolder": "/workspaces/phlex", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "remoteUser": "root", "remoteEnv": { "KILO_API_KEY": "${localEnv:HEADROOM_UPSTREAM_KEY}" }, "initializeCommand": "bash .devcontainer/ensure-repos.sh", "onCreateCommand": "bash .devcontainer/setup-repos.sh /workspaces", - "postCreateCommand": "bash -lc 'bash .devcontainer/post-create.sh'", + "postCreateCommand": "git config --global --add safe.directory '${containerWorkspaceFolder}' && bash -lc 'bash .devcontainer/post-create.sh'", "customizations": { - "vscode": { "settings": { - "terminal.integrated.defaultProfile.linux": "bash", - "terminal.integrated.profiles.linux": { - "bash": { - "path": "/bin/bash", - "args": [ - "-i" - ], - "icon": "terminal-bash" - } + "chat.agent.sandbox.enabled": "on", + "chat.agent.sandbox.allowNetwork": true, + "chat.agent.sandbox.advanced.runtime": { + "enableWeakerNestedSandbox": false }, - "terminal.integrated.shellIntegration.suggestEnablement": false, - "python.terminal.activateEnvironment": false, - "python.defaultInterpreterPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", - "python.analysis.extraPaths": [ - "${workspaceFolder}/build", - "/opt/spack-environments/phlex-ci/.spack-env/view/lib/root", - "/opt/spack-environments/phlex-ci/.spack-env/view/lib/python/site-packages" - ], - "cmake.defaultConfigurePreset": "default", "cmake.buildDirectory": "${workspaceFolder}/build", + "cmake.defaultConfigurePreset": "default", "cmake.generator": "Ninja", + "cmake.useCMakePresets": "always", "C_Cpp.default.cStandard": "c17", + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", "C_Cpp.default.cppStandard": "c++23", "C_Cpp.default.intelliSenseMode": "linux-gcc-x64", - "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", - "python.languageServer": "Pylance", - "python.analysis.typeCheckingMode": "basic", - "python.analysis.diagnosticMode": "workspace", - "python.analysis.exclude": [ - "**/.venv/**", - "**/py_virtual_env/**" - ], - "python.analysis.ignore": [ - "**/.venv/**", - "**/py_virtual_env/**" - ], - "search.exclude": { + "files.watcherExclude": { "**/.venv/**": true, "**/py_virtual_env/**": true }, - "files.watcherExclude": { + "git.autoRepositoryDetection": "subFolders", + "git.repositoryScanMaxDepth": 1, + "git.scanRepositories": [ + "/workspaces/phlex", + "/workspaces/phlex-coding-guidelines", + "/workspaces/phlex-design", + "/workspaces/phlex-examples", + "/workspaces/phlex-spack-recipes" + ], + "kilocode.new.extraCaCerts": "", + "python.defaultInterpreterPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", + "python.languageServer": "Pylance", + "python.terminal.activateEnvironment": false, + "esbonio.server.pythonPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", + "esbonio.logging.level": "debug", + "search.exclude": { "**/.venv/**": true, "**/py_virtual_env/**": true }, - "kilocode.new.extraCaCerts": "" + "terminal.integrated.defaultProfile.linux": "bash", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/bin/bash", + "args": [ + "-i" + ], + "icon": "terminal-bash" + } + }, + "terminal.integrated.shellIntegration.suggestEnablement": false }, "extensions": [ "charliermarsh.ruff", @@ -82,6 +83,7 @@ "ms-python.vscode-pylance", "ms-python.vscode-python-envs", "ms-vscode.cmake-tools", + "ms-vscode.cpptools", "ms-vscode.cpptools-extension-pack", "ms-vscode.cpptools-themes", "ms-vscode.hexeditor", diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 49ac938b8..129ac8909 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -6,17 +6,18 @@ services: context: . dockerfile: Dockerfile volumes: - - ..:/workspaces/phlex - - ../../phlex-coding-guidelines:/workspaces/phlex-coding-guidelines - - ../../phlex-design:/workspaces/phlex-design - - ../../phlex-examples:/workspaces/phlex-examples - - ../../phlex-spack-recipes:/workspaces/phlex-spack-recipes - - ${HOME}/.config/gh:/root/.config/gh:ro - - ${HOME}/.config/kilo:/root/.config/kilo - - ${HOME}/.gnupg:/root/.gnupg - - ${HOME}/.podman-proxy/podman.sock:/tmp/podman.sock - - ${HOME}/.local/share/kilo:/root/.local/share/kilo:ro - - phlex-vscode-user-data:/root/.vscode-server-insiders/data/User + - ..:/workspaces/phlex:Z + - ../../phlex-coding-guidelines:/workspaces/phlex-coding-guidelines:Z + - ../../phlex-design:/workspaces/phlex-design:Z + - ../../phlex-examples:/workspaces/phlex-examples:Z + - ../../phlex-spack-recipes:/workspaces/phlex-spack-recipes:Z + - ${HOME}/.config/gh:/root/.config/gh:ro,Z + - ${HOME}/.config/kilo:/root/.config/kilo:Z + - ${HOME}/.gnupg:/root/.gnupg:ro,Z + - ${HOME}/.podman-proxy/podman.sock:/tmp/podman.sock:Z + - phlex-kilo-data:/root/.local/share/kilo:Z + - ${HOME}/.local/share/kilo/auth.json:/root/.local/share/kilo/auth.json:ro,Z + - phlex-vscode-user-data:/root/.vscode-server-insiders/data/User:Z environment: - CMAKE_GENERATOR=Ninja - GH_CONFIG_DIR=/root/.config/gh @@ -29,3 +30,4 @@ services: volumes: phlex-vscode-user-data: + phlex-kilo-data: diff --git a/.devcontainer/gitignore-global b/.devcontainer/gitignore-global new file mode 100644 index 000000000..a8dd7f5fc --- /dev/null +++ b/.devcontainer/gitignore-global @@ -0,0 +1,4 @@ +build/ +out/ +_deps/ +_build/ diff --git a/.devcontainer/kilo-env.sh b/.devcontainer/kilo-env.sh new file mode 100644 index 000000000..6184a4c16 --- /dev/null +++ b/.devcontainer/kilo-env.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Prepare KILO_CONFIG_CONTENT with rewritten headroom base URLs for container runtime. +# This script is sourced by /etc/profile.d/ so the variable is available to all processes. + +# Unset any previous KILO_CONFIG_CONTENT to ensure a clean slate. +unset KILO_CONFIG_CONTENT + +# If the headroom proxy ports (9797, 9798) are active via socat relays, +# Kilo needs a modified config with baseURL pointing to host.docker.internal. +# Parse the host config, rewrite baseURLs, and export as KILO_CONFIG_CONTENT. +if command -v kilo >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then + # Only rewrite if we have a working Kilo config to work with. + KILO_CONFIG_CONTENT="$(perl -pe 's{("(?:[^"\\]|\\.)*"|'"'"'(?:[^'"'"'\\]|\\.)*'"'"')|//.*}{$1 // ""}ge' "${HOME}/.config/kilo/kilo.jsonc" | jq -c '.provider |= with_entries( + .value.options.baseURL = + (if .value.options.baseURL == null then null + elif (.value.options.baseURL | test("^https?://127\\.0\\.0\\.1:")) + then (.value.options.baseURL | capture("https?://127\\.0\\.0\\.1:(?[0-9]+)(?.*)") | "http://host.docker.internal:" + ((.port | tonumber + 10000) | tostring) + .rest) + else .value.options.baseURL + end) + )')" + export KILO_CONFIG_CONTENT +fi diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 75fecde39..5da240287 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -11,12 +11,6 @@ cat > ~/.actrc <<'EOF' --container-options --userns=keep-id EOF -# Clear the VS Code extension-install marker so that VS Code always installs the -# extensions listed in devcontainer.json on a fresh container. Without this, the -# marker persists on the host via the Machine data bind-mount and VS Code skips -# installation on every rebuild. -rm -f /root/.vscode-server-insiders/data/Machine/.installExtensionsMarker - # Seed the Kilo Code auth token into the container-private data volume. # The volume is not shared with the host to avoid SQLite conflicts between # the Remote-SSH and devcontainer Kilo Code instances. The API key is @@ -98,32 +92,4 @@ elif command -v pre-commit >/dev/null 2>&1; then pre-commit install || true fi -# Configure Kilo to use host.docker.internal for headroom proxy ports. -# The headroom SSH tunnels bind to 127.0.0.1 only, but rootless Podman's -# pasta networking only routes host.docker.internal to ports listening on -# 0.0.0.0. We use socat relays (set up in ensure-repos.sh) to forward -# 127.0.0.1:9797 -> 0.0.0.0:19797 and 127.0.0.1:9798 -> 0.0.0.0:19798. -# This code rewrites Kilo's baseURL from 127.0.0.1: to -# host.docker.internal: for the relays to work. -cat >> /root/.bashrc <<'EOF' - -# Unset any previous KILO_CONFIG_CONTENT to ensure a clean slate. -unset KILO_CONFIG_CONTENT - -# If the headroom proxy ports (9797, 9798) are active via socat relays, -# Kilo needs a modified config with baseURL pointing to host.docker.internal. -# Parse 'kilo debug config', rewrite baseURLs, and export as KILO_CONFIG_CONTENT. -if command -v kilo >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then - # Only rewrite if we have a working Kilo config to work with. - if KILO_CONFIG_CONTENT="$(perl -pe 's{("(?:[^"\\]|\\.)*"|'"'"'(?:[^'"'"'\\]|\\.)*'"'"')|//.*}{$1 // ""}ge' "${HOME}/.config/kilo/kilo.jsonc" | jq -c '.provider |= with_entries( - .value.options.baseURL = - (if .value.options.baseURL == null then null - elif (.value.options.baseURL | test("^https?://127\\.0\\.0\\.1:")) - then (.value.options.baseURL | capture("https?://127\\.0\\.0\\.1:(?[0-9]+)(?.*)") | "http://host.docker.internal:" + ((.port | tonumber + 10000) | tostring) + .rest) - else .value.options.baseURL - end) - )')"; then - export KILO_CONFIG_CONTENT - fi -fi -EOF +# KILO configuration now provided by /etc/profile.d/kilo-env.sh diff --git a/.devcontainer/setup-repos.sh b/.devcontainer/setup-repos.sh index ec618b731..164327fcc 100755 --- a/.devcontainer/setup-repos.sh +++ b/.devcontainer/setup-repos.sh @@ -1,44 +1,54 @@ #!/bin/bash - # Clone companion repositories into the codespace so they are available # for reference by developers and AI agents. # # Intended to be called from devcontainer.json onCreateCommand. -set -euo pipefail - WORKSPACE_ROOT="${1:-/workspaces}" - if [ ! -d "$WORKSPACE_ROOT" ]; then echo "Error: workspace root does not exist: $WORKSPACE_ROOT" >&2 exit 1 fi +FAILED_REPOS=() + clone_if_absent() { local repo=$1 local dest="${WORKSPACE_ROOT}/${repo}" + local need_clone=1 if [ -e "$dest/.git" ]; then echo "Repository already present: $dest" - return + need_clone=0 elif [ -e "$dest" ]; then echo "WARNING: refusing to overwrite non-repository $dest:" ls -ld "$dest" - return + FAILED_REPOS+=("$repo") + return 1 fi - echo "Cloning Framework-R-D/${repo} into ${dest} ..." - local max_tries=5 current_try=0 - while ! git clone --depth 1 "https://github.com/Framework-R-D/${repo}.git" "$dest"; do - (( ++current_try )) - echo "Attempt $current_try/$max_tries to clone $repo from GitHub FAILED" - (( current_try < max_tries )) || break - sleep 5 - done - if (( current_try == max_tries )); then - echo "WARNING: unable to check out $repo to $dest from GitHub" 1>&2 + if (( need_clone )); then + echo "Cloning Framework-R-D/${repo} into ${dest} ..." + local max_tries=5 current_try=0 + while ! git clone --depth 1 "https://github.com/Framework-R-D/${repo}.git" "$dest"; do + (( ++current_try )) + echo "Attempt $current_try/$max_tries to clone $repo from GitHub FAILED" + (( current_try < max_tries )) || break + sleep 5 + done + if (( current_try == max_tries )); then + echo "WARNING: unable to check out $repo to $dest from GitHub" 1>&2 + FAILED_REPOS+=("$repo") + return 1 + fi fi + git config --global --add safe.directory "$dest" } clone_if_absent phlex-design clone_if_absent phlex-examples clone_if_absent phlex-coding-guidelines clone_if_absent phlex-spack-recipes + +if (( ${#FAILED_REPOS[@]} > 0 )); then + echo "Error: Failed to clone repositories: ${FAILED_REPOS[*]}" >&2 + exit 1 +fi diff --git a/phlex.code-workspace b/phlex.code-workspace index 043c61b3c..fce0252c7 100644 --- a/phlex.code-workspace +++ b/phlex.code-workspace @@ -77,36 +77,5 @@ "https://json.schemastore.org/github-workflow.json": "./.github/workflows/*", "https://json.schemastore.org/github-action.json": "./.github/actions/*/action.yaml" } - }, - "extensions": { - "recommendations": [ - "ms-vscode.cmake-tools", - "ms-vscode.cpptools", - "ms-vscode.cpptools-extension-pack", - "twxs.cmake", - "ms-vscode.vscode-json", - "redhat.vscode-yaml", - "charliermarsh.ruff", - "github.vscode-actions", - "github.copilot", - "github.copilot-chat", - "github.vscode-pull-request-github", - "ms-vscode.hexeditor" - ], - "unwantedRecommendations": [ - "reditorsupport.r", - "ms-vscode.r", - "ikuyadeu.r", - "ms-vscode.r-debugger", - "vscjava.vscode-java-pack", - "redhat.java", - "vscjava.vscode-java-debug", - "vscjava.vscode-java-test", - "vscjava.vscode-maven", - "vscjava.vscode-gradle", - "ms-python.python", - "ms-toolsai.jupyter", - "ms-python.flake8" - ] } } diff --git a/pyproject.toml b/pyproject.toml index 274287260..d860642a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,37 @@ warn_unreachable = true disable_error_code = "annotation-unchecked" [tool.pyright] -extraPaths = ["plugins/python/python", "scripts"] +# NOTE: this table is Pylance/Pyright's highest-precedence configuration +# source: once it exists, python.analysis.extraPaths (and include/exclude/ +# ignore/typeCheckingMode/etc.) set via VS Code settings.json or a +# .code-workspace file are ignored entirely. Update extraPaths here, not +# there. Paths are relative to this file's directory (the repo root) unless +# absolute; VS Code variables like ${workspaceFolder} are NOT substituted +# here. The Spack-specific absolute paths only exist inside the phlex-dev +# devcontainer and are harmless no-ops elsewhere. + +diagnosticMode = "workspace" +extraPaths = [ + "plugins/python/python", + "scripts", + "build/_deps", + "out/build/default/_deps", + "out/build/clang-tidy/_deps", + "/opt/spack-environments/phlex-ci/.spack-env/view/lib/root", + "/opt/spack-environments/phlex-ci/.spack-env/view/lib/python/site-packages", + "/spack/lib/spack" +] +exclude = [ + "**/.venv/**", + "**/py_virtual_env/**", + "**/build/_deps/**", + "**/out/build/*/_deps/**" +] +ignore = [ + "**/.venv/**", + "**/py_virtual_env/**" +] +typeCheckingMode = "basic" [tool.ruff] target-version = "py312"