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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,41 @@ concurrency:
cancel-in-progress: true

jobs:
compose_runtime:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Start and probe Compose infrastructure
shell: bash
run: |
set -Eeuo pipefail
cleanup() {
status=$?
trap - EXIT
if [ "$status" -ne 0 ]; then
docker compose ps --all || true
docker compose logs --no-color --timestamps --tail 200 postgres nats || true
fi
docker compose down --volumes --remove-orphans || true
exit "$status"
}
trap cleanup EXIT

docker compose up --detach --wait --wait-timeout 90
docker compose exec --no-TTY postgres psql -U lifeos -d lifeos -v ON_ERROR_STOP=1 -tAc 'SELECT 1' |
grep -Fx 1
curl --fail --silent --show-error --max-time 5 \
http://127.0.0.1:8222/jsz |
jq -e '(.streams | type) == "number" and (.consumers | type) == "number"' \
>/dev/null

validate:
needs: compose_runtime
runs-on: ubuntu-latest
timeout-minutes: 20
env:
Expand Down
100 changes: 86 additions & 14 deletions .github/workflows/opencode-commercial-development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -242,20 +242,44 @@ jobs:
COREPACK_HOME="$trusted_corepack_home" \
corepack install --global "$package_manager"
chmod -R u=rwX,go=rX "$trusted_corepack_home"
MODEL_HOME="$model_home" NIM_BRIDGE_PORT="$NIM_BRIDGE_PORT" python3 - <<'PYCONFIG'
MODEL_HOME="$model_home" MODEL_WORKSPACE="$model_workspace" NIM_BRIDGE_PORT="$NIM_BRIDGE_PORT" RECEIPT_DIR="$RECEIPT_DIR" python3 - <<'PYCONFIG'
import json
import os
from pathlib import Path

run = json.loads(
(Path(os.environ['RECEIPT_DIR']) / 'run.json').read_text(encoding='utf-8')
)
model_label = run['model_label']
provider_id, separator, model_id = model_label.partition('/')
if provider_id != 'nvidia' or separator != '/' or not model_id:
raise SystemExit('invalid_nvidia_model_label')
model_workspace_path = Path(os.environ['MODEL_WORKSPACE'])
instruction_files = [
model_workspace_path / 'AGENTS.md',
model_workspace_path / 'CLAUDE.md',
]
if any(not path.is_file() for path in instruction_files):
raise SystemExit('missing_reviewed_model_instruction')
instruction_paths = [str(path) for path in instruction_files]

config = {
'$schema': 'https://opencode.ai/config.json',
'autoupdate': False,
'share': 'disabled',
'enabled_providers': ['nvidia'],
'model': model_label,
'small_model': model_label,
'instructions': instruction_paths,
'provider': {
'nvidia': {
'whitelist': [model_id],
'models': {model_id: {'name': model_id}},
'options': {
'baseURL': f"http://127.0.0.1:{os.environ['NIM_BRIDGE_PORT']}/v1",
'apiKey': 'local-loopback-placeholder',
'timeout': 60_000,
'chunkTimeout': 30_000,
},
},
},
Expand Down Expand Up @@ -305,9 +329,28 @@ jobs:
echo 'MODEL_NETWORK_PHASE=model'
} >> "$GITHUB_ENV"

- name: Validate the explicit OpenCode model catalog
id: model_catalog
if: steps.branch.outcome == 'success'
run: |
set -Eeuo pipefail
OPENCODE_MODEL="$(jq -r '.model_label' "$RECEIPT_DIR/run.json")"
sudo -u opencode_model env -i \
HOME="$MODEL_HOME" \
PATH="$PATH" \
COREPACK_HOME="$TRUSTED_COREPACK_HOME" \
COREPACK_ENABLE_NETWORK=0 \
OPENCODE_CONFIG="$MODEL_HOME/opencode.json" \
OPENCODE_DISABLE_AUTOUPDATE=true \
OPENCODE_DISABLE_MODELS_FETCH=true \
OPENCODE_DISABLE_PROJECT_CONFIG=true \
NVIDIA_API_KEY=local-loopback-placeholder \
bash -c 'cd "$1" && catalog="$(pnpm --filter @life-os/commercial-development-agent exec opencode models nvidia)" && test "$catalog" = "$2"' \
_ "$MODEL_WORKSPACE" "$OPENCODE_MODEL"

- name: Start loopback NVIDIA credential bridge
id: bridge
if: steps.branch.outcome == 'success'
if: steps.model_catalog.outcome == 'success'
env:
NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}
run: |
Expand Down Expand Up @@ -451,6 +494,8 @@ jobs:
COREPACK_ENABLE_NETWORK=0 \
OPENCODE_CONFIG="$MODEL_HOME/opencode.json" \
OPENCODE_DISABLE_AUTOUPDATE=true \
OPENCODE_DISABLE_MODELS_FETCH=true \
OPENCODE_DISABLE_PROJECT_CONFIG=true \
NVIDIA_API_KEY=local-loopback-placeholder \
bash -c 'cd "$1" && timeout --signal=TERM --kill-after=30s 90m pnpm --filter @life-os/commercial-development-agent exec opencode run --pure --auto --model "$2" --format json --file .opencode-task.md "Execute the attached policy-isolated LifeOS task."' \
_ "$MODEL_WORKSPACE" "$OPENCODE_MODEL" \
Expand Down Expand Up @@ -692,7 +737,8 @@ jobs:
id: verification
if: steps.diff.outputs.accepted == 'true'
run: |
set +e
set -Eeuo pipefail
status=0
sudo -u opencode_model env -i \
HOME="$MODEL_HOME" \
PATH="$PATH" \
Expand All @@ -705,21 +751,37 @@ jobs:
HABIT_DATABASE_URL="$HABIT_DATABASE_URL" \
NOTIFICATION_DATABASE_URL="$NOTIFICATION_DATABASE_URL" \
PRIVACY_DATABASE_URL="$PRIVACY_DATABASE_URL" \
bash -c 'cd "$1" && pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm build && docker compose config >/dev/null' \
bash -c 'cd "$1" && pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm build' \
_ "$MODEL_WORKSPACE" \
> "$RECEIPT_DIR/verification.log" 2>&1
status=$?
> "$RECEIPT_DIR/verification.log" 2>&1 || status=$?
sudo pkill --signal TERM --euid opencode_model 2>/dev/null || true
set -e
chmod 0600 "$RECEIPT_DIR/verification.log"
if [ "$status" -eq 0 ]; then
echo 'passed=true' >> "$GITHUB_OUTPUT"
else
echo 'passed=false' >> "$GITHUB_OUTPUT"
fi

- name: Materialize verified candidate through trusted boundary
- name: Validate Compose configuration through trusted boundary
id: compose_config
if: steps.verification.outputs.passed == 'true'
run: |
set -Eeuo pipefail
status=0
docker compose \
--file "$MODEL_WORKSPACE/compose.yaml" \
--project-directory "$MODEL_WORKSPACE" \
config --quiet \
> "$RECEIPT_DIR/compose-validation.log" 2>&1 || status=$?
chmod 0600 "$RECEIPT_DIR/compose-validation.log"
if [ "$status" -eq 0 ]; then
echo 'passed=true' >> "$GITHUB_OUTPUT"
else
echo 'passed=false' >> "$GITHUB_OUTPUT"
fi

- name: Materialize verified candidate through trusted boundary
if: steps.compose_config.outputs.passed == 'true'
run: |
set -Eeuo pipefail
python3 - <<'PYMATERIALIZE'
Expand Down Expand Up @@ -801,7 +863,7 @@ jobs:

- name: Recheck the exact main base before remote mutation
id: base
if: steps.verification.outputs.passed == 'true'
if: steps.compose_config.outputs.passed == 'true'
run: |
set -Eeuo pipefail
current="$(timeout 30s git ls-remote origin refs/heads/main | awk 'NR == 1 {print $1}')"
Expand Down Expand Up @@ -876,10 +938,12 @@ jobs:
env:
OPEN_PULL_REQUESTS: ${{ steps.github_evidence.outputs.open_pull_requests }}
SELECTED: ${{ steps.selection.outputs.selected }}
MODEL_CATALOG_OUTCOME: ${{ steps.model_catalog.outcome }}
BRIDGE_REASON: ${{ steps.bridge.outputs.reason }}
MODEL_REASON: ${{ steps.model.outputs.reason }}
DIFF_ACCEPTED: ${{ steps.diff.outputs.accepted }}
VERIFICATION_PASSED: ${{ steps.verification.outputs.passed }}
COMPOSE_CONFIG_PASSED: ${{ steps.compose_config.outputs.passed }}
BASE_MATCHED: ${{ steps.base.outputs.matched }}
PR_CREATED: ${{ steps.mutation.outputs.created }}
BRANCH_NAME: ${{ steps.branch.outputs.branch_name }}
Expand Down Expand Up @@ -916,15 +980,20 @@ jobs:

open_prs = os.environ.get('OPEN_PULL_REQUESTS', '0')
selected = os.environ.get('SELECTED', 'false') == 'true'
model_catalog_outcome = os.environ.get('MODEL_CATALOG_OUTCOME', '')
bridge_reason = os.environ.get('BRIDGE_REASON', '')
model_reason = os.environ.get('MODEL_REASON', '')
diff_accepted = os.environ.get('DIFF_ACCEPTED', '') == 'true'
verification = os.environ.get('VERIFICATION_PASSED', '') == 'true'
source_verification = os.environ.get('VERIFICATION_PASSED', '') == 'true'
compose_configuration = os.environ.get('COMPOSE_CONFIG_PASSED', '') == 'true'
verification = source_verification and compose_configuration
base_matched = os.environ.get('BASE_MATCHED', '') == 'true'
pr_created = os.environ.get('PR_CREATED', '') == 'true'

if open_prs != '0' or not selected:
status, reason = 'unavailable', 'no_eligible_issue'
elif model_catalog_outcome != 'success':
status, reason = 'failed', 'invalid_configuration'
elif bridge_reason == 'provider_credential_missing':
status, reason = 'unavailable', 'provider_credential_missing'
elif bridge_reason != 'completed' or model_reason != 'completed':
Expand All @@ -943,10 +1012,12 @@ jobs:
validations = [
{'name': 'open_pull_request_drain', 'status': 'passed' if open_prs == '0' else 'failed'},
{'name': 'issue_policy', 'status': 'passed' if selected else 'skipped'},
{'name': 'credential_bridge', 'status': 'skipped' if not selected else 'passed' if bridge_reason == 'completed' else 'failed'},
{'name': 'provider_run', 'status': 'skipped' if not selected or bridge_reason != 'completed' else 'passed' if model_reason == 'completed' else 'failed'},
{'name': 'diff_policy', 'status': 'passed' if diff_accepted else 'skipped' if not selected else 'failed'},
{'name': 'repository_verification', 'status': 'passed' if verification else 'skipped' if not diff_accepted else 'failed'},
{'name': 'model_catalog', 'status': 'skipped' if not selected or open_prs != '0' else 'passed' if model_catalog_outcome == 'success' else 'failed'},
{'name': 'credential_bridge', 'status': 'skipped' if not selected or open_prs != '0' or model_catalog_outcome != 'success' else 'passed' if bridge_reason == 'completed' else 'failed'},
{'name': 'provider_run', 'status': 'skipped' if not selected or open_prs != '0' or model_catalog_outcome != 'success' or bridge_reason != 'completed' else 'passed' if model_reason == 'completed' else 'failed'},
{'name': 'diff_policy', 'status': 'skipped' if not selected or open_prs != '0' or model_catalog_outcome != 'success' or bridge_reason != 'completed' or model_reason != 'completed' else 'passed' if diff_accepted else 'failed'},
{'name': 'repository_verification', 'status': 'passed' if source_verification else 'skipped' if not diff_accepted else 'failed'},
{'name': 'compose_configuration', 'status': 'passed' if compose_configuration else 'skipped' if not source_verification else 'failed'},
{'name': 'base_sha', 'status': 'passed' if base_matched else 'skipped' if not verification else 'failed'},
]
input_value = {
Expand Down Expand Up @@ -1012,6 +1083,7 @@ jobs:
"$RECEIPT_DIR/prompt.txt" \
"$RECEIPT_DIR/opencode.log" \
"$RECEIPT_DIR/verification.log" \
"$RECEIPT_DIR/compose-validation.log" \
"$RECEIPT_DIR/nim-bridge.py" \
"$RECEIPT_DIR/issues.json" \
"$RECEIPT_DIR/pulls.json" \
Expand Down
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ Pull requests follow one loop: inspect every review and check, fix root causes,

Scheduled model-assisted automation uses `NVIDIA_NIM_API_KEY`; `COPILOT_GITHUB_TOKEN` is prohibited. Existing dedicated review-agent credentials are not repurposed. Deterministic audit and merge eligibility remain independently enforceable even when a model provider is unavailable.

The pinned OpenCode configuration disables project-local overrides, explicitly reloads reviewed repository instructions, enables only NVIDIA, registers and whitelists one model label independently of the bundled catalog, pins primary and small-model work to it, and checks that effective catalog offline before its credential bridge starts; the bridge exposes no provider-wide discovery route. Model-generated source verification runs without Docker authority. A later trusted operation parses the accepted candidate's explicitly selected Compose file, while credential-free pull-request CI starts digest-pinned images, proves PostgreSQL query execution and NATS JetStream availability, binds published ports to loopback, and tears down unconditionally.

## 6. Documentation hierarchy

1. `AGENTS.md` — repository-wide agent and merge rules.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All notable changes to LifeOS are documented in this file.

### Fixed

- The OpenCode development loop now prevents project settings from overriding its pinned offline NVIDIA model, records catalog failures accurately, parses the accepted candidate's exact Compose file outside the model account, and requires digest-pinned PostgreSQL queries plus NATS JetStream probes in pull-request CI.
- Live contextual-orchestrator responses now classify successful empty bodies as evaluation failures, emit exactly one terminal observation, canonicalize retained timestamps safely, and preserve null metric denominators instead of fabricating deltas.
- Stale AI proposal revision conflicts now belong to the technology-independent audit domain while the PostgreSQL adapter preserves its compatibility export.
- Planning search now normalizes browser query text and prevents stale or unmounted requests from replacing the latest visible result state.
Expand All @@ -32,6 +33,7 @@ All notable changes to LifeOS are documented in this file.

### Security

- The commercial-development model account no longer performs Docker commands, never receives Docker-socket authority, and cannot trigger provider-wide model discovery through the credential bridge.
- The scheduled live-model harness uses only `NVIDIA_NIM_API_KEY`, seeds it through the encrypted contextual-orchestrator credential registry, installs hash-locked dependencies from an exact commit, confines LifeOS traffic to loopback, allowlists NVIDIA NIM egress, and excludes provider credentials, prompts, responses, traces, and hidden reasoning from retained artifacts.
- Proposal quality reports now discard nested model failures and response bodies, normalize labeled sentinel checks, expose no provider credential or mutation dependency, and measure prompt-injection resistance together with benign utility instead of rewarding blanket refusal.
- External proposal generation now accepts only one credential-free HTTPS orchestrator origin, stops responses at 65536 bytes, enforces a bounded abort timeout, supplies no tools, treats planning context as untrusted data, and exposes only sanitized failures.
Expand Down
10 changes: 5 additions & 5 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
services:
postgres:
image: postgres:17-alpine
image: postgres:17.10-alpine@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193
environment:
POSTGRES_USER: lifeos
POSTGRES_PASSWORD: lifeos
POSTGRES_DB: lifeos
ports:
- '5432:5432'
- '127.0.0.1:5432:5432'
volumes:
- lifeos-postgres:/var/lib/postgresql/data
healthcheck:
Expand All @@ -16,11 +16,11 @@ services:
retries: 10

nats:
image: nats:2.11-alpine
image: nats:2.11.6-alpine@sha256:e4bf19f15fd3218814a4e3c9e0064e1334bd8aa20d5984b9f1a0afd084f8cc00
command: ['-js', '-m', '8222']
ports:
- '4222:4222'
- '8222:8222'
- '127.0.0.1:4222:4222'
- '127.0.0.1:8222:8222'
volumes:
- lifeos-nats:/data
healthcheck:
Expand Down
Loading
Loading