Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,15 @@ jobs:
echo "Linting bash scripts..."
shellcheck agent-dev merge-overlay || true
echo "Note: Some warnings may be acceptable for this use case"

validate-yaml:
name: Validate YAML Configuration
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Validate YAML syntax
run: |
python3 -c "import yaml; yaml.safe_load(open('virt-lightning/virt-lightning-example.yaml'))"
6 changes: 6 additions & 0 deletions tests/test-agent-dev.expect
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,12 @@ proc test_merge_workflow {} {
}
}

# Consume the file listing preview so its [NEW]/[MODIFIED]/[DELETED]
# entries don't confuse the interactive prompt matching below.
# (Tcl's "." matches \n, so "NEW.*name.*Action" can span listing → prompt.)
expect -- "----------------------------------------"
expect -- "----------------------------------------"

# Handle merge-overlay prompts
# We'll: accept file1, skip file2, discard file3, accept modified, accept deleted
set done 0
Expand Down
1 change: 1 addition & 0 deletions virt-lightning/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
virt-lightning.yaml
17 changes: 17 additions & 0 deletions virt-lightning/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
* Be direct, blunt, and concise. Prioritize accuracy over politeness.
* If a solution is speculative, briefly note it (e.g., "Speculative: ..."), but also suggest complex more risky solutions.
* In commits messaged use "Assisted-By: your_model_name" instead of "Co-Authored-By"
* Make sure you are working on the correct branch and fetched latest version from the upstream repo
* This is a virtual machine and you have `sudo` access. Use it to
** install machine global software
** change machine configuration
** prefer local user config anf installation for project specific stuff
** avoid installing viruses and malware but report if you detect such
** avoid breaking the machine beyond repair but also not a big problem so report if this happens
* Be autonomous and put effort before asking the user back.
** run tests
** install missing software or different version
** be persistent in trying to solve problems
** spawn sub-agents to complete distinct tasks to keep your context clean
* look for existing projects in user home dir and clone them if missing
* use `gh` cli if it was configured for github interactions
5 changes: 5 additions & 0 deletions virt-lightning/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Establish a safe space for the agent to perfomr more complicated setup and have more freedom for action with less human intervention.

## Setup yout GitHub app

TODO
107 changes: 107 additions & 0 deletions virt-lightning/gh-wrapper
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail

# ---- CONFIG ----
APP_ID="${GITHUB_APP_ID:?}"
INSTALLATION_ID="${GITHUB_INSTALLATION_ID:?}"
KEY_PATH="${GITHUB_APP_KEY:?}"

CACHE_FILE="${HOME}/.cache/gh-app-token.json"
mkdir -p "$(dirname "$CACHE_FILE")"

NOW=$(date +%s)

find_real_gh() {
local self first second

self=$(readlink -f -- "${BASH_SOURCE[0]}")

{
IFS= read -r first
IFS= read -r second
} < <(which -a gh 2>/dev/null)

if [[ -z "$first" ]]; then
printf 'Error: no "gh" executable found in PATH.\n' >&2
return 1
fi

# If this script is not the first gh in PATH, use the first one.
if [[ "$self" != "$(readlink -f -- "$first")" ]]; then
printf '%s\n' "$first"
return 0
fi

# If this script is the first gh in PATH, use the second one.
if [[ -z "$second" ]]; then
printf 'Error: this wrapper is the first "gh" in PATH, but no second "gh" executable was found.\n' >&2
return 1
fi

printf '%s\n' "$second"
}

# ---- STEP 1: reuse cached token ----
if [[ -f "$CACHE_FILE" ]]; then
EXP=$(jq -r '.expires_at_epoch // 0' "$CACHE_FILE")
if [[ "$EXP" -gt "$NOW" ]]; then
TOKEN=$(jq -r '.token' "$CACHE_FILE")
export GH_TOKEN="$TOKEN"
exec "$(find_real_gh)" "$@"
fi
fi

# ---- STEP 2: generate JWT (only on miss) ----
HEADER_B64=$(printf '{"alg":"RS256","typ":"JWT"}' \
| openssl base64 -A | tr '+/' '-_' | tr -d '=')

IAT=$((NOW - 60))
EXP=$((NOW + 540))

PAYLOAD=$(printf '{"iat":%d,"exp":%d,"iss":%d}' "$IAT" "$EXP" "$APP_ID")
PAYLOAD_B64=$(printf '%s' "$PAYLOAD" \
| openssl base64 -A | tr '+/' '-_' | tr -d '=')

UNSIGNED="${HEADER_B64}.${PAYLOAD_B64}"

SIG=$(printf '%s' "$UNSIGNED" \
| openssl dgst -sha256 -sign "$KEY_PATH" \
| openssl base64 -A | tr '+/' '-_' | tr -d '=')

JWT="${UNSIGNED}.${SIG}"

# ---- STEP 3: exchange JWT for installation token ----
RESP=$(curl -fsS -X POST \
-H "Authorization: Bearer $JWT" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/app/installations/${INSTALLATION_ID}/access_tokens")

TOKEN=$(jq -r '.token' <<<"$RESP")
EXPIRES_AT=$(jq -r '.expires_at' <<<"$RESP")

if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
echo "Failed to obtain installation token" >&2
echo "$RESP" >&2
exit 1
fi

# ---- STEP 4: convert expiry ----
# EXP_EPOCH=$(date -d "$EXPIRES_AT" +%s 2>/dev/null || gdate -d "$EXPIRES_AT" +%s)
# SAFE_EXP=$((EXP_EPOCH - 60))
# Instead of parsing ISO8601 (not portable), assume ~1h life and refresh early
SAFE_EXP=$((NOW + 3500))

# ---- STEP 5: write cache securely ----
TMP=$(mktemp)

jq -n \
--arg token "$TOKEN" \
--argjson expires_at_epoch "$SAFE_EXP" \
'{token: $token, expires_at_epoch: $expires_at_epoch}' > "$TMP"

chmod 600 "$TMP"
mv "$TMP" "$CACHE_FILE"

# ---- STEP 6: exec gh ----
export GH_TOKEN="$TOKEN"
exec "$(find_real_gh)" "$@"
2 changes: 2 additions & 0 deletions virt-lightning/private_content/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
139 changes: 139 additions & 0 deletions virt-lightning/virt-lightning-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
- name: porta-dev-agent
distro: fedora-43
memory: 8192
vcpus: 4
root_disk_size: 30
username: bob
runcmd:
- dnf install -y git gh jq tmux ripgrep
- curl -L "https://github.com/asdf-vm/asdf/releases/download/v0.19.0/asdf-v0.19.0-linux-amd64.tar.gz" | tar xvz -C /usr/local/bin

- mkdir ~bob/.bashrc.d
- chown bob ~bob/.bashrc.d
- echo 'export ASDF_DATA_DIR=~/.asdf' >> ~bob/.bashrc.d/asdf.sh
- echo 'export PATH="$ASDF_DATA_DIR/shims:$PATH"' >> ~bob/.bashrc.d/asdf.sh
- echo '. <(asdf completion bash)' >> ~bob/.bashrc.d/asdf.sh
- |+
# install needed infra for the ai agent
sudo -u bob -i <<'EOF'
asdf plugin add nodejs
asdf install nodejs 24.15.0
echo "nodejs 24.15.0" > ~/.tool-versions
EOF

# obtain BOBSHELL_API_KEY from https://bob.ibm.com/admin/apikeys
- echo "BOBSHELL_API_KEY=my-bob-api-key" >> /etc/environment
- mkdir /etc/bobshell
- echo '{"context":{"fileFiltering":{"respectGitIgnore":false,"respectBobIgnore":false}}}' > /etc/bobshell/settings.json
# - echo '{ "model": { "name": "pro-latest" } }' > /etc/bobshell/system-defaults.json
- |+
sudo -u bob -i <<'EOF'
curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash
EOF
# needed for interactive, -p usage is fine without these below
- |
echo '{ "/home/bob": "TRUST_FOLDER" }' > ~/.bob/trustedFolders.json
- "bob --accept-license -p hello || :"

- |+
cat > ~bob/.tmux.conf <<"EOF"
setw -g mouse on
bind-key m set -g mouse \; display-message "Mouse: #{?mouse,on,off}"
EOF

- curl -sSL -o ~bob/AGENTS.md 'https://raw.githubusercontent.com/3scale-labs/agent-dev-env/refs/heads/main/virt-lightning/AGENTS.md'

# Claude Code VERTEX setup (setup locally and use ready file content below)
- echo "CLAUDE_CODE_USE_VERTEX=1" >> /etc/environment
- echo "CLOUD_ML_REGION=global" >> /etc/environment
- echo "ANTHROPIC_VERTEX_PROJECT_ID=SET_YOUR_PROJECT" >> /etc/environment
- |+
# https://docs.cloud.google.com/sdk/docs/install-sdk
cat > /etc/yum.repos.d/google-cloud-sdk.repo <<"EOF"
[google-cloud-cli]
name=Google Cloud CLI
baseurl=https://packages.cloud.google.com/yum/repos/cloud-sdk-el10-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=0
gpgkey=https://packages.cloud.google.com/yum/doc/rpm-package-key-v10.gpg
EOF
- sudo dnf install -y google-cloud-cli
- gcloud config set core/disable_prompts true
- |+
sudo -u bob -i <<'EOB'

mkdir -p ~/.config/gcloud/configurations/

cat > ~/.config/gcloud/configurations/config_default <<"EOF"
[core]
account = SET_YOUR_USERNAME@redhat.com
project = SET_YOUR_PROJECT
EOF

cat > ~/.config/gcloud/application_default_credentials.json <<"EOF"
{
"account": "",
"client_id": "REDACTED"
"client_secret": "REDACTED",
"quota_project_id": "SET_QUOTA_PROJECT",
"refresh_token": "REDACTED",
"type": "authorized_user",
"universe_domain": "googleapis.com"
}
EOF

cd /tmp
curl -fsSL https://claude.ai/install.sh -o claude-install.sh
chmod +x claude-install.sh
./claude-install.sh

mkdir -p ~/.claude
ln -s ~/AGENTS.md ~/.claude/CLAUDE.md
cat > ~/.claude/settings.json <<"EOF"
{
"permissions": {
"allow": [
"Bash",
"WebSearch",
"WebFetch",
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Task",
"NotebookEdit",
"Skill"
]
}
}
EOF

EOB

# Github cli setup
- echo "GITHUB_APP_ID=TODO" >> /etc/environment
- echo "GITHUB_INSTALLATION_ID=TODO" >> /etc/environment
- echo "GITHUB_APP_KEY=/home/bob/.config/github-bot.pem" >> /etc/environment
- |+
sudo -u bob -i <<'EOB'
mkdir -p bin

cat > ~/.config/github-bot.pem <<"EOPEM"
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
EOPEM

curl -sSL -o ~/bin/gh 'https://raw.githubusercontent.com/3scale-labs/agent-dev-env/refs/heads/main/virt-lightning/gh-wrapper'

chmod 755 ~/bin/gh
gh auth setup-git
EOB

# Atlasian CLI
- yum install -y yum-utils
- yum-config-manager --add-repo https://acli.atlassian.com/linux/rpm/acli.repo
- yum install -y acli
- sudo -u bob -- acli jira auth login --site "mysite.atlassian.net" --email "user@atlassian.com" --token <<< MY_JIRA_TOKEN