diff --git a/automation/cloud-pipelines/ai-workflows.mdx b/automation/cloud-pipelines/ai-workflows.mdx index 698248c..f87a7ca 100644 --- a/automation/cloud-pipelines/ai-workflows.mdx +++ b/automation/cloud-pipelines/ai-workflows.mdx @@ -92,20 +92,42 @@ Per the [CI/CD policy](/infrastructure/cicd/policy#dependency-versioning), Jacob ## Authentication -Reusable `.yml` workflows call [`anthropics/claude-code-action@v1`](https://github.com/anthropics/claude-code-action) through a shared wrapper action. The runtime contract is AI-agnostic: +Reusable `.yml` workflows use a **provider-agnostic `GH_ACTION_AI_*` namespace** so you can swap providers, endpoints, or models at the org level without editing any caller. -- `secrets.AI_TOKEN` — provider credential -- `vars.AI_PROVIDER` — defaults to `claude_oauth` -- `vars.AI_BASE_URL` or `secrets.AI_BASE_URL` — required only for OpenRouter or another Anthropic-compatible router -- `vars.AI_MODEL*` — defaults to Claude `sonnet` +| Name | Kind | Purpose | +| --- | --- | --- | +| `GH_ACTION_AI_API_KEY` | Secret | Your provider's API key. Required by all workflows. | +| `GH_ACTION_AI_BASE_URL` | Variable | Provider endpoint. Leave **empty** for direct Anthropic. | +| `GH_ACTION_AI_MODEL` | Variable | Global default model name. | +| `GH_ACTION_AI_MODEL_CODE` | Variable | Code-generation tier (falls back to `GH_ACTION_AI_MODEL`). | +| `GH_ACTION_AI_MODEL_ISSUES` | Variable | Issue-management tier (falls back to `GH_ACTION_AI_MODEL`). | +| `GH_ACTION_AI_MODEL_PLAN` | Variable | Deep-planning tier (falls back to `GH_ACTION_AI_MODEL`). | + +Set them at org level with the GitHub CLI: + +```bash +gh secret set GH_ACTION_AI_API_KEY --org # paste your key +gh variable set GH_ACTION_AI_BASE_URL --org -b "" # empty = direct Anthropic +gh variable set GH_ACTION_AI_MODEL --org -b "claude-sonnet-4-6" +``` + +The same workflows run unchanged against any provider — only the org-level values change: -OpenRouter is still supported, but it is not hardcoded: set `AI_PROVIDER=openrouter`, put the key in `AI_TOKEN`, set `AI_BASE_URL`, and choose the model with `AI_MODEL`. +| Provider | `GH_ACTION_AI_API_KEY` | `GH_ACTION_AI_BASE_URL` | Example model | +| --- | --- | --- | --- | +| Direct Anthropic | standard API key | *(empty)* | `claude-sonnet-4-6` | +| OpenRouter | OpenRouter key | `https://openrouter.ai/api/v1` | `anthropic/claude-sonnet-4` | +| Chutes.ai | Chutes key | Chutes endpoint | provider-specific name | -GH-AW imports are different. The current `public-docs-updater` wrapper uses the Copilot engine because GH-AW Claude does not support Claude OAuth tokens on the pinned compiler path. The provider matrix and GH-AW caveats live in [AUTHENTICATION.md](https://github.com/JacobPEvans/ai-workflows/blob/main/docs/AUTHENTICATION.md). +> **OAuth tokens are prohibited in unattended CI.** A Claude Code subscription token (`CLAUDE_CODE_OAUTH_TOKEN`) used inside a GitHub Actions workflow violates the [Claude Code Terms of Service](https://www.anthropic.com/legal/terms) and risks an account ban — the subscription is intended for interactive sessions only. Use a standard API key (`GH_ACTION_AI_API_KEY`) instead; it is purpose-built for programmatic access with no ToS concerns. + +GH-AW imports use the Copilot engine (the current `public-docs-updater` wrapper). Provider details and model configuration live in [AUTHENTICATION.md](https://github.com/JacobPEvans/ai-workflows/blob/main/docs/AUTHENTICATION.md). ## Commit signing -Every PR-writing reusable workflow mints a `JacobPEvans-claude` GitHub App installation token immediately before calling the action, then hands it in as `github_token` with `use_commit_signing: true`. Commits land web-flow-signed and attributed to the bot. The App credentials (`GH_APP_CLAUDE_BOT_PRIVATE_KEY`, `GH_APP_CLAUDE_BOT_ID`) are distributed by `secrets-sync` to every repo in the `_github_app_repos` anchor. +`cc-ci-fix` (the CI auto-fixer) uses GitHub's `createCommitOnBranch` GraphQL mutation to push fix commits — no local clone, no custom signing script. The mutation runs as the Actions bot (`${{ github.token }}`), so every fix commit lands web-flow-signed and attributed to `github-actions[bot]`. This works without any App credential on the consumer repo, and is compatible with repos that enforce signed commits. + +Other PR-writing workflows (`issue-resolver`, `code-simplifier`, `post-merge-*`) mint a `JacobPEvans-claude` GitHub App installation token and rely on `use_commit_signing: true` in the action. The App credentials (`GH_APP_CLAUDE_BOT_PRIVATE_KEY`, `GH_APP_CLAUDE_BOT_ID`) are distributed by `secrets-sync` to every repo in the `_github_app_repos` anchor. ## Where to go next @@ -117,7 +139,7 @@ Every PR-writing reusable workflow mints a `JacobPEvans-claude` GitHub App insta The post-merge dispatch pattern, bot guards, and other recurring shapes. - `AI_TOKEN`, provider routing, model variables, and GH-AW engine caveats. + Full `GH_ACTION_AI_*` reference, provider routing, model variables, and GH-AW engine caveats. The e2e runbook for checking a freshly-wired repo end to end. @@ -126,6 +148,6 @@ Every PR-writing reusable workflow mints a `JacobPEvans-claude` GitHub App insta Exactly which six callers are wired on `JacobPEvans/docs` and why. - How `AI_TOKEN` and the App credentials land on each consumer repo. + How `GH_ACTION_AI_API_KEY` and the App credentials land on each consumer repo. diff --git a/conventions/org-gitignore.mdx b/conventions/org-gitignore.mdx new file mode 100644 index 0000000..d5fa76d --- /dev/null +++ b/conventions/org-gitignore.mdx @@ -0,0 +1,52 @@ +--- +title: "Org-default .gitignore" +description: "A canonical .gitignore baseline shipped from dryvist/.github that every repo adopts. Prevents secrets, credentials, and AI-assistant local state from ever reaching git history." +tier: 1 +--- + +> One gitignore to rule them all. Never commit secrets or AI local state — not by accident, not by habit. + +The `dryvist/.github` repo ships a canonical `.gitignore` baseline at `configs/gitignore`. Every new repo adopts it by appending (not overwriting) to the repo's own `.gitignore`, preserving repo-specific entries while inheriting the org-wide safety floor. + +## What it covers + +The baseline targets two categories of files that must never reach git history: + +**Secrets and credentials** +- `.env`, `.env.*` (except `.env.example`) — never commit real env files +- `*.pem`, `*.key`, `*.p12`, `*.pfx` — private keys +- `terraform.tfvars` (unencrypted), `credentials.json`, `secrets.yaml` (unencrypted) + +**AI-assistant local / machine state** +- `.claude/mcp_settings.json` — MCP server list (can contain tokens) +- `CLAUDE.local.md`, `AGENTS.local.md`, `.envrc.local` — local overrides (gitignored by convention) +- `*.local.md` — any local-only markdown + +**Intentional carve-outs** (do NOT add these back to `.gitignore`): + +| Path | Why it's committed | +| --- | --- | +| `.envrc` | `use flake` directive; the `SOPS_AGE_KEY_FILE` path is not a secret | +| `*.sops.yaml` / `*.sops.yml` | SOPS-encrypted ciphertext — safe to commit | +| `.terraform.lock.hcl` | Provider lock file — committed by convention | +| `.claude/settings.json` | Project AI config — committed on purpose | +| `.claude/rules/`, committed skills/agents | Project Claude Code config | +| `CLAUDE.md`, `AGENTS.md` | Project AI instructions | + +## Adopting in a new repo + +```bash +gh api repos/dryvist/.github/contents/configs/gitignore \ + -H "Accept: application/vnd.github.raw" >> .gitignore +``` + +Use `>>` (append) so repo-specific entries are preserved. De-duplicate afterward if needed. + +## Scope + +The baseline covers secrets and AI state only — it is not a comprehensive language gitignore. Pair it with a language-specific template (GitHub's `.gitignore` templates, `gitignore.io`) for full coverage. + +## Future work + +- Automate adoption in the repo scaffold / copier template so new repos inherit the baseline at creation time rather than manually. +- Add a pre-commit hook that checks for `.env` files without the example suffix before every commit. diff --git a/docs.json b/docs.json index 7814ddf..3779895 100644 --- a/docs.json +++ b/docs.json @@ -222,6 +222,7 @@ "conventions/branch-conventions", "conventions/pr-conventions", "conventions/readme-conventions", + "conventions/org-gitignore", "conventions/git-transport", "conventions/no-scripts", "conventions/diagramming", diff --git a/infrastructure/repos/tofu-proxmox.mdx b/infrastructure/repos/tofu-proxmox.mdx index 14a8875..d4e9821 100644 --- a/infrastructure/repos/tofu-proxmox.mdx +++ b/infrastructure/repos/tofu-proxmox.mdx @@ -20,6 +20,7 @@ import { RepoMeta, RepoFit } from "/snippets/repo-summary.mdx"; - Declares per-node ZFS storage (`node_storage`) that Ansible provisions — OpenTofu references the datastore by id and never creates the pool itself (`zpool create` is an OS-level operation) - Uses Terragrunt to share variables across `prod`, `staging`, and one-off environments - Outputs a list of provisioned hosts that Ansible inventories consume directly +- Provisions the **object_storage** module: RustFS LXC (S3-compatible, active) alongside MinIO (being migrated from); both run concurrently during the transition ## How it fits @@ -32,6 +33,15 @@ import { RepoMeta, RepoFit } from "/snippets/repo-summary.mdx"; Provisioning only. Anything that runs *inside* a host belongs in `ansible-proxmox` or `ansible-proxmox-apps`. +## Object storage migration + +The homelab is migrating from **MinIO** to **RustFS** for S3-compatible object storage. Both LXCs are declared in the `object_storage` module and run in parallel during the migration: + +- **RustFS** — the target; Splunkbase sync and new data flows write here +- **MinIO** — the source; decommissioned once all consumers have cut over + +`ansible-proxmox` backs up the RustFS data volume via `sanoid`/`syncoid`. `ansible-splunk` repoints Splunkbase app sync to the RustFS endpoint. After all consumers migrate, the MinIO LXC will be removed from the module. + ## Getting started diff --git a/nix/nix-ai.mdx b/nix/nix-ai.mdx index 8f20152..5b592bf 100644 --- a/nix/nix-ai.mdx +++ b/nix/nix-ai.mdx @@ -30,6 +30,27 @@ import { RepoMeta, RepoFit } from "/snippets/repo-summary.mdx"; Anything that's "an AI tool I want everywhere" belongs here. Single-project AI experiments go in a project-local `nix-devenv`-style flake. +## Configuration surface + +`nix-ai` ships as a reusable flake module with a single knob surface — `modules/maintainer-profile.nix`. This is the one file a consumer touches to adapt the stack: + +- **Identity** fields (`user.fullName`, `user.trustedOrgs`) have maintainer defaults so derived values (e.g. Claude auto-mode trusted-org list) stay internally consistent; override them for your own identity. +- **Infrastructure** fields (`homelab.*`, `telemetry.*`, `trustedProjectDirs`) default to clean/off/empty, so the module evaluates to a neutral config with zero consumer input — no personal context ships in the flake. + +Modules read `userConfig` via `_module.args` so nothing outside `maintainer-profile.nix` needs to import it directly. A consumer overrides values either in their own `nix-darwin` config (via `extraSpecialArgs`) or by importing the module and setting options. + +### Secrets and injection + +Runtime secrets (API keys, tokens, Doppler credentials) are never stored in Nix store paths or committed files. Three injection patterns are in use: + +| Pattern | Used by | Mechanism | +| --- | --- | --- | +| Doppler subprocess wrapper | Google Workspace MCP, Splunk MCP | `doppler run -p -c prd -- ` | +| macOS Keychain via shell init | HuggingFace token, GitHub PAT | `security find-generic-password` exported in `~/.zshrc` | +| Kubernetes Doppler Operator | Bifrost, Cribl pods | In-cluster operator syncs Doppler → K8s Secrets | + +The variable catalog (required vs optional, purpose, source) lives in `.env.example` at the repo root; a real `.env` is an opt-in convenience — direct injection is preferred. The local injection runbook is `AGENTS.local.md` (gitignored). + ## Getting started @@ -42,6 +63,9 @@ Anything that's "an AI tool I want everywhere" belongs here. Single-project AI e Add `nix-ai` as a flake input, then include `nix-ai.overlays.default` in your nixpkgs config. The README has the boilerplate. + + Set `userConfig.user.fullName` and any `homelab.*` / `telemetry.*` options in your own config to adapt the stack to your environment. + ## Related repos