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
53 changes: 19 additions & 34 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,53 +1,38 @@
# OpenCodex local/dev + container env. Copy to `.env` (gitignored).
# Never commit secrets, tokens, or Authentik client secrets.
#
# Live place lock (do not change from this checkout):
# systemd unit: opencodex-proxy
# version: 1.5.1
# source SHA: c88648fa87001d04beedc8df853bee7700253422
# health: http://<tailscale-ipv4>:10100/healthz
# The immutable GHCR digest lives only on the live host `.env` as OPENCODEX_IMAGE.
# OpenCodex local/dev + container environment. Copy to .env (gitignored).
# Never commit secrets, tokens, OAuth client secrets, or production host data.

# --- Local / compose.dev path (repo-root compose.yml) ---
# --- Local / compose path ---
# Generate a throwaway token file, then point at it:
# mkdir -p deploy/container/.secrets
# umask 077 && python3 -c 'import secrets; print(secrets.token_hex(16))' > deploy/container/.secrets/api-token
OPENCODEX_API_TOKEN_FILE=deploy/container/.secrets/api-token
OPENCODEX_STATE_DIR=./.tmp/opencodex-state
OPENCODEX_GIT_SHA=
OPENCODEX_VERSION=1.5.1
OPENCODEX_VERSION=

# --- Production compose.example.yml (digest-pinned; not used by local compose) ---
# OPENCODEX_IMAGE=ghcr.io/groeponline/opencodex:1.5.1@sha256:<immutable-digest>
# OPENCODEX_BIND_IP= # host Tailscale IPv4; required only for prod compose
# Optional production image reference. Pin an immutable digest in the private
# deployment system; do not encode a live host or digest in this example.
# OPENCODEX_IMAGE=ghcr.io/groeponline/opencodex:<version>@sha256:<digest>
# OPENCODEX_BIND_IP=

# --- Cloudflare Access (live public-host gate; optional locally) ---
# --- Optional edge authentication ---
CF_ACCESS_TEAM_DOMAIN=
CF_ACCESS_AUD=
CF_ACCESS_ALLOWED_HOSTS=

# --- Authentik OIDC (ChefGroep Auth product consumer) ---
# Public issuer APPLY DONE 2026-09-18 (discovery/JWKS 200, authorize 302).
# Not DNS HOLD. The proxy verifies Authentik ID tokens and can run
# GET /oauth/login → /oauth/callback when the secret file is set.
# Cloudflare Access remains the live public-host gate until
# deploy/oidc/CUTOVER-CHECKLIST.md is executed. client_secret stays
# file-only (never git).
# Redirect contract: deploy/oidc/authentik-ocx-client.placeholder.json
OIDC_ISSUER=https://auth.chefgroep.online/application/o/ocx/
OIDC_CLIENT_ID=chefgroep-ocx-oidc
# --- Authentik/OIDC product consumer ---
# Set deployment-specific values outside this repository. The client secret is
# always file-delivered.
OIDC_ISSUER=https://id.example.com/application/o/opencodex/
OIDC_CLIENT_ID=opencodex
OIDC_CLIENT_SECRET_FILE=
OIDC_ALLOWED_HOSTS=
# Local default. Production redirect is https://ocx.chefgroep.online/oauth/callback
OIDC_REDIRECT_URI=http://127.0.0.1:10100/oauth/callback

# --- Fleet Azure Foundry keys (host env file only; never commit values) ---
# See deploy/container/model-catalog.example.json and docs/models.md.
# AZURE_OPENAI_KEY_OPENAICHEF=
# AZURE_OPENAI_KEY_OPENAICHEF_SE=
# AZURE_OPENAI_KEY_AZURE_FOUNDRY_US=
# --- Example provider credential ---
# EXAMPLE_OPENAI_API_KEY=

# --- Healthz smoke (scripts/healthz-smoke.sh) ---
# --- Healthz smoke ---
# OPENCODEX_HEALTH_URL=http://127.0.0.1:10100/healthz
# OPENCODEX_SMOKE_EXPECT_SHA=c88648fa87001d04beedc8df853bee7700253422
# OPENCODEX_SMOKE_EXPECT_VERSION=1.5.1
# OPENCODEX_SMOKE_EXPECT_SHA=
# OPENCODEX_SMOKE_EXPECT_VERSION=
13 changes: 6 additions & 7 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: Legacy deploy route retired

# `chef-control-az-01` is permanently retired. The previous Docker/GHCR
# workflow ran on a runner located there, so it must never be selected by a tag
# push or manual release dispatch. OCX now runs as a package release under the
# system service on bc-scan-2; its source-owned deployment contract is tracked
# separately and must be introduced with its own target and rollback evidence.
# The previous production Docker/GHCR deployment route is permanently retired.
# It must never be selected by a tag push or manual release dispatch. Production
# runtime cutover is owned by a private deployment contract with an explicit
# target, immutable artifact identity, health proof and rollback evidence.
#
# Keeping this small, explicit workflow provides a clear audit result for an
# accidental legacy dispatch without retaining credentials, host paths, or any
Expand All @@ -22,6 +21,6 @@ jobs:
steps:
- name: Refuse retired deployment route
run: |
echo "::error::The chef-control-az-01 deployment route is permanently retired."
echo "::error::Do not deploy this workflow; use the separately verified bc-scan-2 package deployment contract."
echo "::error::The previous production deployment route is permanently retired."
echo "::error::Do not deploy this workflow; use the separately verified private production deployment contract."
exit 1
4 changes: 2 additions & 2 deletions .github/workflows/publish-on-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Publish on tag

# Tag a commit on main with `vX.Y.Z` and this workflow publishes
# `@groeponline/opencodex` to npm. Publication does not deploy a runtime: the
# former chef-control-az-01 deploy route is permanently retired.
# former production deploy route is permanently retired.
#
# Publishing prefers npm Trusted Publishing (OIDC, no token). The Trusted Publisher
# on npmjs.com is registered for the `Release` workflow, so OIDC is only accepted
Expand Down Expand Up @@ -132,5 +132,5 @@ jobs:
gh release create "v${VERSION}" --title "v${VERSION}" --notes "$notes"

# Publication intentionally has no runtime side effect. The old Azure deploy
# route is retired and a bc-scan-2 package deployment contract must provide
# route is retired and a private production deployment contract must provide
# its own target, immutable artifact, health, and rollback verification.
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ on:
required: false
type: string
deploy:
description: "Legacy deploy.yml targets a permanently retired host and must remain false. Runtime cutover uses a separately verified bc-scan-2 package deployment contract."
description: "Legacy deploy.yml targets a permanently retired host and must remain false. Runtime cutover uses a separately verified private production deployment contract."
required: false
type: boolean
default: false
Expand Down Expand Up @@ -65,8 +65,8 @@ jobs:
DEPLOY: ${{ inputs.deploy }}
run: |
if [ "$DEPLOY" = "true" ]; then
echo "::error::deploy=true is disabled because deploy.yml targets retired chef-control-az-01."
echo "::error::Use the separately verified bc-scan-2 package deployment contract."
echo "::error::deploy=true is disabled because deploy.yml targets retired production host."
echo "::error::Use the separately verified private production deployment contract."
exit 1
fi

Expand Down Expand Up @@ -651,6 +651,6 @@ jobs:
echo "- runtime cutover: not dispatched; legacy Azure deployment is retired" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "::error::deploy=true is disabled because deploy.yml targets retired chef-control-az-01."
echo "::error::Use the separately verified bc-scan-2 package deployment contract."
echo "::error::deploy=true is disabled because deploy.yml targets retired production host."
echo "::error::Use the separately verified private production deployment contract."
exit 1
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ Bun runtime for end users, but contributor commands such as `bun install`, `bun

The production proxy path (Compose + systemd + `:10100/healthz`) has a complete local/dev
mirror: repo-root `compose.yml`, `.devcontainer/`, `.env.example`, and
`bash scripts/healthz-smoke.sh`. Authentik OIDC canary:
`bash scripts/oidc-authorize-canary.sh`. See
`bash scripts/healthz-smoke.sh`. Authentik OIDC canary requires explicit,
deployment-owned values, for example:
`OIDC_ISSUER=https://id.example.com/application/o/opencodex/ OIDC_CLIENT_ID=opencodex bash scripts/oidc-authorize-canary.sh`. See
[`deploy/container/README.md`](./deploy/container/README.md) and
[`deploy/oidc/CUTOVER-CHECKLIST.md`](./deploy/oidc/CUTOVER-CHECKLIST.md).

Expand Down
2 changes: 1 addition & 1 deletion RELEASE_PROCESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ created by that token, so `container.yml` is dispatched explicitly on `refs/tags

Runtime cutover is intentionally not part of release publication. The former Azure deploy
route is permanently retired; leave the `deploy` input at its default `false`. Deploying the
bc-scan-2 package service is a separate operation with its own immutable artifact, health and
private production service is a separate operation with its own immutable artifact, health and
rollback evidence.

## Post-release
Expand Down
126 changes: 36 additions & 90 deletions deploy/container/README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,20 @@
# OpenCodex container + systemd path

This directory is the production compose/unit contract. Local/dev uses the same
proxy/app path (`:10100/healthz`) without touching the live place lock.

| Path | Role |
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| [`compose.example.yml`](./compose.example.yml) | Digest-pinned Compose reference for `opencodex-proxy.service`; not the live bc-scan-2 deployment. |
| [`opencodex-proxy.service`](./opencodex-proxy.service) | Non-serving systemd oneshot that runs `docker compose up/down` in that directory. |
| [`../../compose.yml`](../../compose.yml) | Local/dev Compose. Builds from this repo's `Dockerfile`, loopback `:10100` only. |
| [`../../.env.example`](../../.env.example) | Env template. No secrets. |
| [`../../.devcontainer/`](../../.devcontainer/) | Coding container (Bun 1.4.0). |
| [`../oidc/authentik-ocx-client.placeholder.json`](../oidc/authentik-ocx-client.placeholder.json) | Authentik OIDC client contract (`chefgroep-ocx-oidc`; issuer APPLY DONE 2026-09-18). |
| [`../oidc/CUTOVER-CHECKLIST.md`](../oidc/CUTOVER-CHECKLIST.md) | Authorize canary and CoS / Cloudflare Access cutover checklist. |
| [`../../scripts/oidc-authorize-canary.sh`](../../scripts/oidc-authorize-canary.sh) | Secret-free discovery/JWKS (and optional `/oauth/login`) canary. |
| [`../../scripts/healthz-smoke.sh`](../../scripts/healthz-smoke.sh) | Local/dev `/healthz` identity smoke matching `:10100/healthz`. |

## Live place lock

Do **not** retarget, redeploy, or rewrite the live pin from this repository
change. Reported live identity:

| Field | Value |
| ---------- | --------------------------------------------------------- |
| Unit | `opencodex-proxy.service` |
| Unit state | Non-serving; a separate npm process owns port `10100` |
| Version | `1.5.1` |
| Source SHA | `c88648fa87001d04beedc8df853bee7700253422` (tag `v1.5.1`) |
| Health | `GET /healthz` on the host Tailscale IPv4, port `10100` |

The live bc-scan-2 service runs the published npm package from
`/home/joep/.opencodex/releases/current`; this unit file does not embed a
version and is not the live process owner. Do not restart it until the npm
package procedure is documented or the unit is reconciled. `.github/workflows/deploy.yml`
is retired fail-closed and no longer retargets or rolls back any host. A
source-owned bc-scan-2 package deployment contract remains a separate operation.

## Local/dev (complete proxy/app path)
This directory contains generic deployment examples for the OpenCodex proxy.
It deliberately does not describe any ChefGroep production host, private
network address, release-tree path, credential location or live cutover state.

| Path | Role |
| --- | --- |
| [`compose.example.yml`](./compose.example.yml) | Digest-pinned Compose reference. |
| [`opencodex-proxy.service`](./opencodex-proxy.service) | Example systemd wrapper for the Compose deployment. |
| [`../../compose.yml`](../../compose.yml) | Local/dev Compose using this repository's Dockerfile. |
| [`../../.env.example`](../../.env.example) | Secret-free environment template. |
| [`model-catalog.example.json`](./model-catalog.example.json) | Generic, key-free provider catalog example. |
| [`../oidc/CUTOVER-CHECKLIST.md`](../oidc/CUTOVER-CHECKLIST.md) | Generic OIDC cutover checklist. |
| [`../../scripts/healthz-smoke.sh`](../../scripts/healthz-smoke.sh) | `/healthz` identity smoke. |

## Local/dev

```bash
cp .env.example .env
Expand All @@ -46,70 +25,37 @@ docker compose up -d --build
bash scripts/healthz-smoke.sh
```

Source-only (no Docker), same health contract:
Source-only:

```bash
bun install --frozen-lockfile
bun run start
bash scripts/healthz-smoke.sh
```

`scripts/healthz-smoke.sh` defaults to `http://127.0.0.1:10100/healthz` and
requires `status=ok`, `service=opencodex`, numeric `pid`/`port`, and a non-empty
`gitSha`. Set `OPENCODEX_SMOKE_EXPECT_SHA` / `OPENCODEX_SMOKE_EXPECT_VERSION` to
bind those fields the way the production health gate binds the release tag.

## Authentik OIDC consumer

Public ChefGroep Auth issuer is **APPLY DONE 2026-09-18** at
`https://auth.chefgroep.online/application/o/ocx/` (discovery/JWKS 200,
authorize 302). The issuer is **not** DNS HOLD. Live `client_id` is
`chefgroep-ocx-oidc` (Infra smoke + Cloudflare Access IdP). `client_secret`
stays `null` / file-only (`OIDC_CLIENT_SECRET_FILE`).

The consumer contract lists redirect URIs for local loopback and
`https://ocx.chefgroep.online`. Compose forwards `OIDC_*` when set. The proxy
verifies Authentik ID tokens (JWKS) and runs `GET /oauth/login` →
`/oauth/callback` (authorization-code + PKCE) when the secret file is present.
**Cloudflare Access remains the live public-host dashboard gate** until
operators execute [`../oidc/CUTOVER-CHECKLIST.md`](../oidc/CUTOVER-CHECKLIST.md).

- Do not put a client secret in git. Use `OIDC_CLIENT_SECRET_FILE`.
- Do not register this client in ChefFactory catalogs (Factory owns catalog).
- Do not apply Cloudflare DNS from this repository.

```bash
bash scripts/oidc-authorize-canary.sh
# optional, against a running local proxy with OIDC_* set:
OPENCODEX_OIDC_CANARY_URL=http://127.0.0.1:10100 bash scripts/oidc-authorize-canary.sh
```
`scripts/healthz-smoke.sh` defaults to
`http://127.0.0.1:10100/healthz` and requires `status=ok`,
`service=opencodex`, numeric `pid`/`port`, and a non-empty `gitSha`.
Use `OPENCODEX_SMOKE_EXPECT_SHA` and
`OPENCODEX_SMOKE_EXPECT_VERSION` when a deployment gate must bind an exact
artifact.

## Fleet model catalog
## Authentication

Runtime providers are not part of the image pin. The key-free default is
[`model-catalog.example.json`](./model-catalog.example.json), described in
[`../../docs/models.md`](../../docs/models.md).
Remote data-plane binds must use a generated client admission key or the
service-token mechanism described by the product. Do not distribute a host
service credential to clients.

| Check | Rule |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Providers | `azure-us` (`openaichef`, eastus), `azure-se` (`openaichef-se`, swedencentral), `azure-foundry-us` (`azure-foundry-us`, eastus) |
| Wire | `openai-chat` against `https://<resource>.cognitiveservices.azure.com/openai/v1` |
| Keys | Host env only: `AZURE_OPENAI_KEY_OPENAICHEF`, `AZURE_OPENAI_KEY_OPENAICHEF_SE`, `AZURE_OPENAI_KEY_AZURE_FOUNDRY_US`. Never commit values. |
| Removed | `jort-7512-resource`, AWS/Bedrock hosts, `chef-control-az-01` as a model upstream, stopped llama.cpp/weg54 inference |
| Apply | Edit the host `~/.opencodex/config.json`, keep mode `0600`, then defer restart until the npm procedure is documented or the unit is reconciled. Do not retarget the binary pin from this file. |
| Check | `ocx config validate` before restart. `GET /v1/models` on the bind address must list `azure-us/*`, `azure-se/*`, and `azure-foundry-us/fw-deepseek-v4-pro`. |
OIDC deployments provide issuer, client-id, redirect and client-secret-file
settings through the deployment environment. Client secrets never belong in
source control. See [the generic cutover checklist](../oidc/CUTOVER-CHECKLIST.md).

Copying the example over a live file drops every other provider. Merge it into
the existing `providers` map instead.
## Production boundary

## Honest blockers
Publishing a package or image does not deploy a production runtime. Production
host placement, private addresses, release paths, credentials, current version
and rollback evidence belong to the operator's private deployment repository or
configuration system.

1. Authentik issuer public apply is done (2026-09-18). The consumer is wired,
but Cloudflare Access remains the live public-host gate until the cutover
checklist is executed. Client secret is not in git.
2. This change documents the `1.5.1` / `c88648fa8` live release and does not
move it. Do not deploy this PR to bc-scan-2.
3. The former Azure `deploy.yml` route is retired fail-closed; do not treat a
merge here as a live cutover.
4. Public `ocx.chefgroep.online` Cloudflare is already applied; this repo does
not mutate DNS or ChefFactory catalogs.
The public repository intentionally fails closed instead of carrying a live
ChefGroep deployment target.
Loading
Loading