From 160e7efad39efc569deb450da6e9790d51b839e0 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Mon, 9 Nov 2026 18:08:37 -0500 Subject: [PATCH 1/2] Chore: Move the demos to rossoctl/cortex-demos The demos are now in https://github.com/rossoctl/cortex-demos, with their history. This change removes demos/ and the demo skill from this repo. - CI: remove the go-ci-demos job (it is now in cortex-demos). Remove the demos exclusions from dependabot-tidy and the Trivy config scan. - The cpex hr-demo tests read two hr-cpex manifests. Copies of those files are now in core/plugins/cpex/testdata/hr-cpex/. - Links and comments that point at demos/ now point at cortex-demos. - CLAUDE.md, .github/CLAUDE.md and CONTRIBUTING.md: 7 Go modules, not 10. Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- .claude/skills/demo/SKILL.md | 234 --- .github/CLAUDE.md | 2 +- .github/workflows/ci.yaml | 28 - .github/workflows/dependabot-tidy.yml | 2 +- .github/workflows/security-scans.yaml | 1 - .gitignore | 3 +- CLAUDE.md | 9 +- CONTRIBUTING.md | 4 +- cmd/README.md | 2 +- cmd/agentop/cmd_pipeline.go | 2 +- cmd/agentop/cmd_pipeline_test.go | 2 +- core/config/config_test.go | 2 +- core/config/tlsbridge_bind_test.go | 2 +- core/listener/parity/protocol_parity_test.go | 2 +- core/plugins/CLAUDE.md | 4 +- core/plugins/cpex/README.md | 2 +- core/plugins/cpex/hrdemo_cpex_test.go | 4 +- .../cpex/testdata/hr-cpex}/authbridge.yaml | 0 .../cpex/testdata/hr-cpex}/cpex-policy.yaml | 0 demos/CLAUDE.md | 213 --- demos/README.md | 167 -- demos/coding-agent-cpex/.gitignore | 5 - demos/coding-agent-cpex/README.md | 217 --- demos/coding-agent-cpex/agent/agent.py | 266 --- demos/coding-agent-cpex/claude-code/README.md | 113 -- .../claude-code/settings.json.example | 36 - demos/coding-agent-cpex/claude-code/setup.sh | 95 - demos/coding-agent-cpex/config/cortex.yaml | 56 - .../coding-agent-cpex/config/cpex-policy.yaml | 107 -- demos/coding-agent-cpex/demo/caption_bar.py | 90 - .../demo/coding-agent-cpex.svg | 126 -- demos/coding-agent-cpex/demo/policy_slide.py | 376 ---- demos/coding-agent-cpex/demo/record_demo.py | 173 -- demos/coding-agent-cpex/demo/record_tmux.sh | 229 --- demos/coding-agent-cpex/demo/steps.md | 38 - .../demo/test_record_demo.py | 110 -- demos/coding-agent-cpex/docker-compose.yml | 13 - demos/coding-agent-cpex/env.sh | 25 - demos/coding-agent-cpex/fixtures/env.fixture | 4 - .../fixtures/report-clean.json | 1 - .../fixtures/report-with-key.json | 1 - .../coding-agent-cpex/mocks/llm/scripts.json | 157 -- demos/coding-agent-cpex/mocks/llm/server.py | 147 -- .../coding-agent-cpex/mocks/support/server.py | 100 -- demos/coding-agent-cpex/run.sh | 407 ----- .../scenarios/00-sandbox-boundary.sh | 16 - .../scenarios/01-unapproved-host-deny.sh | 19 - .../scenarios/02-clean-report-allow.sh | 16 - .../scenarios/03-secret-in-report-deny.sh | 20 - .../scenarios/04-tainted-session-deny.sh | 29 - .../scenarios/05-wrong-project-deny.sh | 15 - .../scenarios/06-fresh-session-allow.sh | 15 - .../scenarios/07-custody-allow.sh | 22 - .../08-custody-no-credential-on-deny.sh | 28 - .../scenarios/09-intent-deny.sh | 34 - demos/coding-agent-cpex/scenarios/_lib.sh | 360 ---- demos/coding-agent-cpex/walkthrough.sh | 163 -- demos/context-guru/README.md | 162 -- .../context-guru/context-guru-tls-bridge.yaml | 77 - demos/context-guru/k8s/agent.yaml | 70 - demos/context-guru/k8s/authbridge-config.yaml | 51 - demos/context-guru/run.sh | 124 -- demos/echo/.gitignore | 1 - demos/echo/Makefile | 300 ---- demos/echo/README.md | 152 -- demos/echo/agent/Dockerfile | 17 - demos/echo/agent/main.go | 398 ----- demos/echo/go.mod | 3 - demos/echo/k8s/agent.yaml | 149 -- demos/echo/k8s/echo-patch.yaml | 16 - demos/echo/k8s/upstream.yaml | 76 - demos/echo/scripts/echo-merge.py | 114 -- demos/echo/scripts/patch-echo-config.sh | 187 -- demos/echo/scripts/setup_keycloak.py | 460 ----- demos/echo/upstream/Dockerfile | 10 - demos/echo/upstream/main.go | 43 - demos/finance-sparc/.gitignore | 1 - demos/finance-sparc/Makefile | 200 --- demos/finance-sparc/README.md | 189 -- demos/finance-sparc/finance-agent/Dockerfile | 12 - demos/finance-sparc/finance-agent/main.go | 471 ----- demos/finance-sparc/finance-mcp/Dockerfile | 11 - demos/finance-sparc/finance-mcp/main.go | 289 ---- demos/finance-sparc/go.mod | 3 - demos/finance-sparc/k8s/agent.yaml | 118 -- demos/finance-sparc/k8s/finance-mcp.yaml | 63 - demos/finance-sparc/k8s/ollama.yaml | 95 - demos/finance-sparc/k8s/sparc-patch.yaml | 53 - demos/finance-sparc/scripts/drive-demo.sh | 69 - demos/finance-sparc/scripts/host-setup.sh | 53 - demos/finance-sparc/scripts/launch-agentop.sh | 20 - .../scripts/patch-sparc-config.sh | 111 -- demos/finance-sparc/scripts/pipeline-merge.py | 49 - .../scripts/setup_keycloak_finance.py | 130 -- demos/finance-sparc/scripts/show-verdicts.py | 52 - .../scripts/stage-ollama-model.sh | 34 - demos/github-issue/aiac/.gitignore | 14 - demos/github-issue/aiac/Makefile | 110 -- demos/github-issue/aiac/aiac.env.TEMPLATE | 6 - .../github-issue/aiac/aiac_agent/__init__.py | 12 - .../aiac/aiac_agent/agent/__init__.py | 21 - .../aiac/aiac_agent/agent/graph.py | 574 ------- .../aiac/aiac_agent/agent/state.py | 43 - .../aiac/aiac_agent/config/__init__.py | 19 - .../aiac/aiac_agent/config/config_utils.py | 104 -- .../aiac/aiac_agent/config/constants.py | 10 - .../aiac_agent/config/llm_conf.yaml.TEMPLATE | 62 - .../aiac/aiac_agent/config/llm_config.py | 287 ---- .../aiac/aiac_agent/prompts/__init__.py | 15 - .../aiac/aiac_agent/prompts/prompt_builder.py | 294 ---- .../aiac/aiac_agent/utils/__init__.py | 17 - .../aiac/aiac_agent/utils/parsers.py | 118 -- .../aiac/aiac_agent/utils/validators.py | 288 ---- demos/github-issue/aiac/aiac_cli.py | 282 --- .../aiac/keycloak_ops/__init__.py | 22 - .../aiac/keycloak_ops/apply_policy.py | 109 -- .../aiac/keycloak_ops/delete_policy.py | 88 - .../aiac/keycloak_ops/export_config.py | 362 ---- .../aiac/policies/permissive_policy.txt | 2 - .../aiac/policies/regular_policy.txt | 2 - .../github-issue/aiac/scripts/show-result.py | 113 -- demos/github-issue/demo-aiac.md | 850 --------- demos/github-issue/demo-manual.md | 1204 ------------- demos/github-issue/demo-ui.md | 1217 ------------- demos/github-issue/demo.md | 94 - demos/github-issue/k8s/configmaps.yaml | 64 - .../k8s/git-issue-agent-deployment.yaml | 161 -- .../k8s/github-tool-deployment.yaml | 100 -- demos/github-issue/rbac/Makefile | 259 --- demos/github-issue/rbac/config.yaml | 54 - demos/github-issue/rbac/policy.yaml | 35 - demos/github-issue/setup_keycloak.py | 1521 ----------------- demos/hr-cpex/Makefile | 283 --- demos/hr-cpex/README.md | 577 ------- demos/hr-cpex/agent/CHAT-WALKTHROUGH.md | 297 ---- demos/hr-cpex/agent/Dockerfile | 28 - demos/hr-cpex/agent/agent.py | 572 ------- demos/hr-cpex/agent/chat.py | 469 ----- demos/hr-cpex/agent/requirements-client.txt | 17 - demos/hr-cpex/agent/requirements.txt | 31 - demos/hr-cpex/auth-channel/Dockerfile | 12 - demos/hr-cpex/auth-channel/requirements.txt | 7 - demos/hr-cpex/auth-channel/server.py | 174 -- demos/hr-cpex/demo/.gitignore | 2 - demos/hr-cpex/demo/record_demo.py | 112 -- demos/hr-cpex/demo/steps.md | 33 - demos/hr-cpex/demo/test_record_demo.py | 69 - demos/hr-cpex/hr-mcp-server/Dockerfile | 12 - demos/hr-cpex/hr-mcp-server/requirements.txt | 7 - demos/hr-cpex/hr-mcp-server/server.py | 506 ------ demos/hr-cpex/hr-mcp-server/test_server.py | 105 -- demos/hr-cpex/k8s/00-namespace.yaml | 8 - demos/hr-cpex/k8s/10-keycloak.yaml | 145 -- demos/hr-cpex/k8s/20-hr-mcp.yaml | 72 - demos/hr-cpex/k8s/30-agent.yaml | 221 --- demos/hr-cpex/k8s/40-auth-channel.yaml | 69 - demos/hr-cpex/k8s/realm-export.json | 443 ----- demos/hr-cpex/k8s/spire/agent-patch.yaml | 18 - demos/hr-cpex/k8s/spire/agent-unpatch.yaml | 12 - .../hr-cpex/k8s/spire/authbridge-spiffe.yaml | 7 - demos/hr-cpex/k8s/spire/spire.yaml | 330 ---- demos/hr-cpex/mint-token.sh | 65 - demos/hr-cpex/scenarios/01-bob-allow.sh | 33 - demos/hr-cpex/scenarios/02-alice-deny.sh | 21 - demos/hr-cpex/scenarios/03-eve-redact.sh | 33 - .../scenarios/04-alice-internal-allow.sh | 29 - .../scenarios/05-alice-external-cedar-deny.sh | 22 - demos/hr-cpex/scenarios/06-bob-apl-deny.sh | 22 - demos/hr-cpex/scenarios/07-bob-pii-deny.sh | 25 - demos/hr-cpex/scenarios/08-bob-taint-deny.sh | 45 - .../09-cross-principal-taint-isolation.sh | 47 - .../scenarios/10-adjust-under-threshold.sh | 76 - .../scenarios/11-adjust-needs-approval.sh | 63 - demos/hr-cpex/scenarios/12-assertions.sh | 43 - .../scenarios/13-workload-delegation.sh | 58 - demos/hr-cpex/scenarios/_lib.sh | 392 ----- demos/hr-cpex/verify-token-exchange.sh | 116 -- demos/hr-cpex/walkthrough.sh | 172 -- demos/ibac/.gitignore | 1 - demos/ibac/Makefile | 266 --- demos/ibac/README.md | 137 -- demos/ibac/agent/Dockerfile | 17 - demos/ibac/agent/main.go | 825 --------- demos/ibac/email-server/Dockerfile | 10 - demos/ibac/email-server/main.go | 167 -- demos/ibac/evil-server/Dockerfile | 10 - demos/ibac/evil-server/main.go | 40 - demos/ibac/go.mod | 3 - demos/ibac/k8s/agent-unprotected.yaml | 144 -- demos/ibac/k8s/agent.yaml | 156 -- demos/ibac/k8s/email-server.yaml | 66 - demos/ibac/k8s/evil-server.yaml | 62 - demos/ibac/k8s/ibac-patch.yaml | 94 - demos/ibac/k8s/parsers-patch.yaml | 16 - demos/ibac/scripts/chat.sh | 71 - demos/ibac/scripts/ibac-merge.py | 57 - demos/ibac/scripts/launch-agentop.sh | 36 - demos/ibac/scripts/patch-ibac-config.sh | 175 -- demos/lineage/README.md | 220 --- demos/lineage/ask.sh | 40 - demos/lineage/k8s/weather.yaml | 202 --- demos/lineage/show-trace.py | 185 -- demos/session-budget/README.md | 120 -- demos/session-budget/hitl-local.md | 301 ---- demos/session-budget/hitl-with-claude-code.md | 192 --- .../k8s/pause-webhook-stub.yaml | 91 - demos/session-budget/local/approver.go | 170 -- .../local/config-https-observe.yaml | 38 - demos/session-budget/local/config-https.yaml | 40 - demos/session-budget/local/config.yaml | 34 - demos/token-exchange-routes/README.md | 211 --- demos/token-exchange-routes/routes.yaml | 23 - demos/weather-agent/demo-ui-advanced.md | 383 ----- demos/weather-agent/demo-ui.md | 845 --------- demos/weather-agent/demo-with-agentop.md | 278 --- .../deploy_and_verify_advanced.sh | 288 ---- ...agentruntime-weather-service-advanced.yaml | 12 - .../agentruntime-weather-tool-advanced.yaml | 19 - .../k8s/configmaps-advanced.yaml | 57 - .../k8s/weather-service-advanced.yaml | 118 -- .../k8s/weather-tool-advanced.yaml | 129 -- .../setup_keycloak_weather_advanced.py | 374 ---- deploy/lineage-attach/README.md | 2 +- deploy/proxy-init/README.md | 4 +- docs/README.md | 2 +- docs/architecture.md | 4 +- docs/cpex-plugin.md | 4 +- docs/framework-architecture.md | 2 +- docs/ibac-plugin.md | 2 +- docs/kubernetes.md | 8 +- docs/session-budget-plugin.md | 4 +- docs/sparc-plugin.md | 2 +- scripts/dev/verify-spire-keycloak.sh | 2 +- 233 files changed, 37 insertions(+), 29902 deletions(-) delete mode 100644 .claude/skills/demo/SKILL.md rename {demos/hr-cpex/k8s => core/plugins/cpex/testdata/hr-cpex}/authbridge.yaml (100%) rename {demos/hr-cpex/k8s => core/plugins/cpex/testdata/hr-cpex}/cpex-policy.yaml (100%) delete mode 100644 demos/CLAUDE.md delete mode 100644 demos/README.md delete mode 100644 demos/coding-agent-cpex/.gitignore delete mode 100644 demos/coding-agent-cpex/README.md delete mode 100644 demos/coding-agent-cpex/agent/agent.py delete mode 100644 demos/coding-agent-cpex/claude-code/README.md delete mode 100644 demos/coding-agent-cpex/claude-code/settings.json.example delete mode 100755 demos/coding-agent-cpex/claude-code/setup.sh delete mode 100644 demos/coding-agent-cpex/config/cortex.yaml delete mode 100644 demos/coding-agent-cpex/config/cpex-policy.yaml delete mode 100644 demos/coding-agent-cpex/demo/caption_bar.py delete mode 100644 demos/coding-agent-cpex/demo/coding-agent-cpex.svg delete mode 100755 demos/coding-agent-cpex/demo/policy_slide.py delete mode 100644 demos/coding-agent-cpex/demo/record_demo.py delete mode 100755 demos/coding-agent-cpex/demo/record_tmux.sh delete mode 100644 demos/coding-agent-cpex/demo/steps.md delete mode 100644 demos/coding-agent-cpex/demo/test_record_demo.py delete mode 100644 demos/coding-agent-cpex/docker-compose.yml delete mode 100644 demos/coding-agent-cpex/env.sh delete mode 100644 demos/coding-agent-cpex/fixtures/env.fixture delete mode 100644 demos/coding-agent-cpex/fixtures/report-clean.json delete mode 100644 demos/coding-agent-cpex/fixtures/report-with-key.json delete mode 100644 demos/coding-agent-cpex/mocks/llm/scripts.json delete mode 100644 demos/coding-agent-cpex/mocks/llm/server.py delete mode 100644 demos/coding-agent-cpex/mocks/support/server.py delete mode 100755 demos/coding-agent-cpex/run.sh delete mode 100755 demos/coding-agent-cpex/scenarios/00-sandbox-boundary.sh delete mode 100755 demos/coding-agent-cpex/scenarios/01-unapproved-host-deny.sh delete mode 100755 demos/coding-agent-cpex/scenarios/02-clean-report-allow.sh delete mode 100755 demos/coding-agent-cpex/scenarios/03-secret-in-report-deny.sh delete mode 100755 demos/coding-agent-cpex/scenarios/04-tainted-session-deny.sh delete mode 100755 demos/coding-agent-cpex/scenarios/05-wrong-project-deny.sh delete mode 100755 demos/coding-agent-cpex/scenarios/06-fresh-session-allow.sh delete mode 100755 demos/coding-agent-cpex/scenarios/07-custody-allow.sh delete mode 100755 demos/coding-agent-cpex/scenarios/08-custody-no-credential-on-deny.sh delete mode 100755 demos/coding-agent-cpex/scenarios/09-intent-deny.sh delete mode 100755 demos/coding-agent-cpex/scenarios/_lib.sh delete mode 100755 demos/coding-agent-cpex/walkthrough.sh delete mode 100644 demos/context-guru/README.md delete mode 100644 demos/context-guru/context-guru-tls-bridge.yaml delete mode 100644 demos/context-guru/k8s/agent.yaml delete mode 100644 demos/context-guru/k8s/authbridge-config.yaml delete mode 100755 demos/context-guru/run.sh delete mode 100644 demos/echo/.gitignore delete mode 100644 demos/echo/Makefile delete mode 100644 demos/echo/README.md delete mode 100644 demos/echo/agent/Dockerfile delete mode 100644 demos/echo/agent/main.go delete mode 100644 demos/echo/go.mod delete mode 100644 demos/echo/k8s/agent.yaml delete mode 100644 demos/echo/k8s/echo-patch.yaml delete mode 100644 demos/echo/k8s/upstream.yaml delete mode 100644 demos/echo/scripts/echo-merge.py delete mode 100755 demos/echo/scripts/patch-echo-config.sh delete mode 100644 demos/echo/scripts/setup_keycloak.py delete mode 100644 demos/echo/upstream/Dockerfile delete mode 100644 demos/echo/upstream/main.go delete mode 100644 demos/finance-sparc/.gitignore delete mode 100644 demos/finance-sparc/Makefile delete mode 100644 demos/finance-sparc/README.md delete mode 100644 demos/finance-sparc/finance-agent/Dockerfile delete mode 100644 demos/finance-sparc/finance-agent/main.go delete mode 100644 demos/finance-sparc/finance-mcp/Dockerfile delete mode 100644 demos/finance-sparc/finance-mcp/main.go delete mode 100644 demos/finance-sparc/go.mod delete mode 100644 demos/finance-sparc/k8s/agent.yaml delete mode 100644 demos/finance-sparc/k8s/finance-mcp.yaml delete mode 100644 demos/finance-sparc/k8s/ollama.yaml delete mode 100644 demos/finance-sparc/k8s/sparc-patch.yaml delete mode 100644 demos/finance-sparc/scripts/drive-demo.sh delete mode 100644 demos/finance-sparc/scripts/host-setup.sh delete mode 100644 demos/finance-sparc/scripts/launch-agentop.sh delete mode 100644 demos/finance-sparc/scripts/patch-sparc-config.sh delete mode 100644 demos/finance-sparc/scripts/pipeline-merge.py delete mode 100644 demos/finance-sparc/scripts/setup_keycloak_finance.py delete mode 100644 demos/finance-sparc/scripts/show-verdicts.py delete mode 100644 demos/finance-sparc/scripts/stage-ollama-model.sh delete mode 100644 demos/github-issue/aiac/.gitignore delete mode 100644 demos/github-issue/aiac/Makefile delete mode 100644 demos/github-issue/aiac/aiac.env.TEMPLATE delete mode 100644 demos/github-issue/aiac/aiac_agent/__init__.py delete mode 100644 demos/github-issue/aiac/aiac_agent/agent/__init__.py delete mode 100644 demos/github-issue/aiac/aiac_agent/agent/graph.py delete mode 100644 demos/github-issue/aiac/aiac_agent/agent/state.py delete mode 100644 demos/github-issue/aiac/aiac_agent/config/__init__.py delete mode 100644 demos/github-issue/aiac/aiac_agent/config/config_utils.py delete mode 100644 demos/github-issue/aiac/aiac_agent/config/constants.py delete mode 100644 demos/github-issue/aiac/aiac_agent/config/llm_conf.yaml.TEMPLATE delete mode 100644 demos/github-issue/aiac/aiac_agent/config/llm_config.py delete mode 100644 demos/github-issue/aiac/aiac_agent/prompts/__init__.py delete mode 100644 demos/github-issue/aiac/aiac_agent/prompts/prompt_builder.py delete mode 100644 demos/github-issue/aiac/aiac_agent/utils/__init__.py delete mode 100644 demos/github-issue/aiac/aiac_agent/utils/parsers.py delete mode 100644 demos/github-issue/aiac/aiac_agent/utils/validators.py delete mode 100644 demos/github-issue/aiac/aiac_cli.py delete mode 100644 demos/github-issue/aiac/keycloak_ops/__init__.py delete mode 100644 demos/github-issue/aiac/keycloak_ops/apply_policy.py delete mode 100644 demos/github-issue/aiac/keycloak_ops/delete_policy.py delete mode 100644 demos/github-issue/aiac/keycloak_ops/export_config.py delete mode 100644 demos/github-issue/aiac/policies/permissive_policy.txt delete mode 100644 demos/github-issue/aiac/policies/regular_policy.txt delete mode 100644 demos/github-issue/aiac/scripts/show-result.py delete mode 100644 demos/github-issue/demo-aiac.md delete mode 100644 demos/github-issue/demo-manual.md delete mode 100644 demos/github-issue/demo-ui.md delete mode 100644 demos/github-issue/demo.md delete mode 100644 demos/github-issue/k8s/configmaps.yaml delete mode 100644 demos/github-issue/k8s/git-issue-agent-deployment.yaml delete mode 100644 demos/github-issue/k8s/github-tool-deployment.yaml delete mode 100644 demos/github-issue/rbac/Makefile delete mode 100644 demos/github-issue/rbac/config.yaml delete mode 100644 demos/github-issue/rbac/policy.yaml delete mode 100644 demos/github-issue/setup_keycloak.py delete mode 100644 demos/hr-cpex/Makefile delete mode 100644 demos/hr-cpex/README.md delete mode 100644 demos/hr-cpex/agent/CHAT-WALKTHROUGH.md delete mode 100644 demos/hr-cpex/agent/Dockerfile delete mode 100644 demos/hr-cpex/agent/agent.py delete mode 100755 demos/hr-cpex/agent/chat.py delete mode 100644 demos/hr-cpex/agent/requirements-client.txt delete mode 100644 demos/hr-cpex/agent/requirements.txt delete mode 100644 demos/hr-cpex/auth-channel/Dockerfile delete mode 100644 demos/hr-cpex/auth-channel/requirements.txt delete mode 100644 demos/hr-cpex/auth-channel/server.py delete mode 100644 demos/hr-cpex/demo/.gitignore delete mode 100755 demos/hr-cpex/demo/record_demo.py delete mode 100644 demos/hr-cpex/demo/steps.md delete mode 100644 demos/hr-cpex/demo/test_record_demo.py delete mode 100644 demos/hr-cpex/hr-mcp-server/Dockerfile delete mode 100644 demos/hr-cpex/hr-mcp-server/requirements.txt delete mode 100644 demos/hr-cpex/hr-mcp-server/server.py delete mode 100644 demos/hr-cpex/hr-mcp-server/test_server.py delete mode 100644 demos/hr-cpex/k8s/00-namespace.yaml delete mode 100644 demos/hr-cpex/k8s/10-keycloak.yaml delete mode 100644 demos/hr-cpex/k8s/20-hr-mcp.yaml delete mode 100644 demos/hr-cpex/k8s/30-agent.yaml delete mode 100644 demos/hr-cpex/k8s/40-auth-channel.yaml delete mode 100644 demos/hr-cpex/k8s/realm-export.json delete mode 100644 demos/hr-cpex/k8s/spire/agent-patch.yaml delete mode 100644 demos/hr-cpex/k8s/spire/agent-unpatch.yaml delete mode 100644 demos/hr-cpex/k8s/spire/authbridge-spiffe.yaml delete mode 100644 demos/hr-cpex/k8s/spire/spire.yaml delete mode 100755 demos/hr-cpex/mint-token.sh delete mode 100755 demos/hr-cpex/scenarios/01-bob-allow.sh delete mode 100755 demos/hr-cpex/scenarios/02-alice-deny.sh delete mode 100755 demos/hr-cpex/scenarios/03-eve-redact.sh delete mode 100755 demos/hr-cpex/scenarios/04-alice-internal-allow.sh delete mode 100755 demos/hr-cpex/scenarios/05-alice-external-cedar-deny.sh delete mode 100755 demos/hr-cpex/scenarios/06-bob-apl-deny.sh delete mode 100755 demos/hr-cpex/scenarios/07-bob-pii-deny.sh delete mode 100755 demos/hr-cpex/scenarios/08-bob-taint-deny.sh delete mode 100755 demos/hr-cpex/scenarios/09-cross-principal-taint-isolation.sh delete mode 100755 demos/hr-cpex/scenarios/10-adjust-under-threshold.sh delete mode 100755 demos/hr-cpex/scenarios/11-adjust-needs-approval.sh delete mode 100755 demos/hr-cpex/scenarios/12-assertions.sh delete mode 100755 demos/hr-cpex/scenarios/13-workload-delegation.sh delete mode 100755 demos/hr-cpex/scenarios/_lib.sh delete mode 100755 demos/hr-cpex/verify-token-exchange.sh delete mode 100755 demos/hr-cpex/walkthrough.sh delete mode 100644 demos/ibac/.gitignore delete mode 100644 demos/ibac/Makefile delete mode 100644 demos/ibac/README.md delete mode 100644 demos/ibac/agent/Dockerfile delete mode 100644 demos/ibac/agent/main.go delete mode 100644 demos/ibac/email-server/Dockerfile delete mode 100644 demos/ibac/email-server/main.go delete mode 100644 demos/ibac/evil-server/Dockerfile delete mode 100644 demos/ibac/evil-server/main.go delete mode 100644 demos/ibac/go.mod delete mode 100644 demos/ibac/k8s/agent-unprotected.yaml delete mode 100644 demos/ibac/k8s/agent.yaml delete mode 100644 demos/ibac/k8s/email-server.yaml delete mode 100644 demos/ibac/k8s/evil-server.yaml delete mode 100644 demos/ibac/k8s/ibac-patch.yaml delete mode 100644 demos/ibac/k8s/parsers-patch.yaml delete mode 100755 demos/ibac/scripts/chat.sh delete mode 100755 demos/ibac/scripts/ibac-merge.py delete mode 100755 demos/ibac/scripts/launch-agentop.sh delete mode 100755 demos/ibac/scripts/patch-ibac-config.sh delete mode 100644 demos/lineage/README.md delete mode 100755 demos/lineage/ask.sh delete mode 100644 demos/lineage/k8s/weather.yaml delete mode 100755 demos/lineage/show-trace.py delete mode 100644 demos/session-budget/README.md delete mode 100644 demos/session-budget/hitl-local.md delete mode 100644 demos/session-budget/hitl-with-claude-code.md delete mode 100644 demos/session-budget/k8s/pause-webhook-stub.yaml delete mode 100644 demos/session-budget/local/approver.go delete mode 100644 demos/session-budget/local/config-https-observe.yaml delete mode 100644 demos/session-budget/local/config-https.yaml delete mode 100644 demos/session-budget/local/config.yaml delete mode 100644 demos/token-exchange-routes/README.md delete mode 100644 demos/token-exchange-routes/routes.yaml delete mode 100644 demos/weather-agent/demo-ui-advanced.md delete mode 100644 demos/weather-agent/demo-ui.md delete mode 100644 demos/weather-agent/demo-with-agentop.md delete mode 100755 demos/weather-agent/deploy_and_verify_advanced.sh delete mode 100644 demos/weather-agent/k8s/agentruntime-weather-service-advanced.yaml delete mode 100644 demos/weather-agent/k8s/agentruntime-weather-tool-advanced.yaml delete mode 100644 demos/weather-agent/k8s/configmaps-advanced.yaml delete mode 100644 demos/weather-agent/k8s/weather-service-advanced.yaml delete mode 100644 demos/weather-agent/k8s/weather-tool-advanced.yaml delete mode 100644 demos/weather-agent/setup_keycloak_weather_advanced.py diff --git a/.claude/skills/demo/SKILL.md b/.claude/skills/demo/SKILL.md deleted file mode 100644 index 8d45c8205..000000000 --- a/.claude/skills/demo/SKILL.md +++ /dev/null @@ -1,234 +0,0 @@ ---- -name: demo -description: Use when building, debugging, or running a Cortex demo end-to-end on a Kind cluster with SPIFFE/SPIRE, Keycloak, and Istio ambient mesh — covers the demo directory layout, the Keycloak setup scripts, and the recurring failure modes (ext_proc header ordering, ambient-mesh inbound path, Keycloak scope assignment). ---- - -# Skill: Cortex Demo Development - -This skill captures knowledge from building, debugging, and running Cortex demos end-to-end on Kind clusters with SPIFFE/SPIRE, Keycloak, and Istio ambient mesh. - -> **Some entries below are historical.** They describe failures from the -> pre-cortex#411 multi-sidecar shape, when `spiffe-helper` and -> `client-registration` were separate containers. Neither exists now — SVIDs are -> fetched in-process by `core/spiffe` and registration runs in the operator — -> but the diagnoses are kept because the same *symptoms* still appear in older -> clusters. Such entries are marked HISTORICAL. - -## Repository Context - -- **Repo:** `rossoctl/cortex` (monorepo) -- **Container registry:** `ghcr.io/rossoctl/cortex/` -- **Agent examples repo:** `rossoctl/examples` (separate repo, images NOT published to GHCR) -- **Demo guides:** `demos//demo-manual.md` (manual kubectl) and `demo-ui.md` (UI-driven) - -## Demo Directory Convention - -Each demo lives under `demos//`: - -``` -demos// -├── k8s/ -│ ├── configmaps.yaml # Per-demo ConfigMap *overrides* — typically -│ │ # authbridge-config and authproxy-routes; -│ │ # the installer supplies the defaults -│ ├── -deployment.yaml # Agent Deployment + Service -│ └── -deployment.yaml # Tool Deployment + Service (if applicable) -├── setup_keycloak.py # Keycloak realm/client/scope/user setup -├── demo.md # Index page linking manual + UI guides -├── demo-manual.md # Full manual deployment guide (kubectl only) -└── demo-ui.md # UI-driven deployment guide -``` - -## Building and Loading Images for Kind - -Agent/tool images from `rossoctl/examples` must be built locally and loaded into Kind: - -```bash -# Build from agent-examples repo -docker build -t ghcr.io/rossoctl/examples/:latest ./a2a// -docker build -t ghcr.io/rossoctl/examples/:latest ./mcp// - -# Build AuthBridge sidecar images. Every plugin is opt-in, so GO_BUILD_TAGS must -# name a profile: a build without it registers no plugins and rejects every -# config it is handed. -cd cortex -docker build -f cmd/cortex/Dockerfile \ - --build-arg GO_BUILD_TAGS="$(go -C scripts/profile-tags run . full)" \ - -t ghcr.io/rossoctl/cortex/authbridge:latest . -docker build -f cmd/cortex-envoy/Dockerfile \ - --build-arg GO_BUILD_TAGS="$(go -C scripts/profile-tags run . envoy)" \ - -t ghcr.io/rossoctl/cortex/authbridge-envoy:latest . - -cd deploy/proxy-init -docker build -f Dockerfile.init -t ghcr.io/rossoctl/cortex/proxy-init:latest . - -# Load into Kind -kind load docker-image --name rossoctl -``` - -`scripts/dev/local-build-and-test.sh` builds and Kind-loads `authbridge`, -`authbridge-envoy`, `authbridge-lite` and `proxy-init` (plus `spiffe-idp-setup` -from the rossoctl repo) — prefer it over building by hand. Note it does not build -`authbridge-cpex`. - -Use fully qualified image names in Dockerfiles (e.g., `docker.io/library/golang:1.26-alpine`) to avoid Podman/Buildah "short-name resolution enforced" errors in Shipwright builds. - -## Envoy Config: No Longer Hand-Maintained Per Demo (HISTORICAL) - -This section used to list five demo files that each carried a copy of the same -inbound listener, to be edited in lockstep. **None of them does now** — four of -the five files no longer exist (`demos/single-target/` and `authproxy/` are both -gone), and `demos/github-issue/k8s/configmaps.yaml` has shrunk to -`authbridge-config` + `authproxy-routes` with no listener in it at all. - -Inbound interception is configured through `authbridge-config` and rendered by -the operator, not hand-written into demo ConfigMaps. No hand-maintained Envoy filter chain is left in the repo — the last one, -`demos/mtls/k8s/envoy-config-mtls.yaml`, was retired with that demo — so -nothing needs lockstep edits any more. - -## Critical Bugs and Fixes - -### 1. iptables Backend Mismatch (proxy-init) - -**Symptom:** Inbound traffic bypasses Envoy — requests reach the agent without JWT validation. - -**Root cause:** Alpine 3.18's default `iptables` uses the **nf_tables** backend. Kind/kubeadm use **iptables-legacy**. Rules set via nft have no effect when the cluster uses legacy. - -**Fix:** `init-iptables.sh` auto-detects the backend via `detect_iptables_cmd()` (prefers `iptables-legacy`). All iptables calls use `${IPT}` variable. Override with `IPTABLES_CMD` env var if needed. - -**Verification:** proxy-init logs must show `Using iptables command: iptables-legacy`. - -**Diagnosis:** -```bash -docker exec iptables --version # Check node backend -docker exec bash -c ' # Inspect pod's rules - PID=$(crictl inspect $(crictl ps --name envoy-proxy -q | head -1) | jq -r .info.pid) - nsenter -t $PID -n iptables -t nat -L PREROUTING -n -v - nsenter -t $PID -n iptables -t nat -L PROXY_INBOUND -n -v' -``` - -### 2. Envoy Filter Ordering (ext_proc never sees direction header) - -**Symptom:** ext_proc logs `=== Outbound Request Headers ===` for INBOUND traffic. All requests treated as outbound. - -**Root cause:** `x-authbridge-direction: inbound` was at the **route level** (`request_headers_to_add` in `virtual_hosts`). Route headers are applied by the **router filter** — the LAST in the HTTP filter chain. ext_proc runs BEFORE router and never sees the header. - -**Fix:** Add a **Lua filter** BEFORE ext_proc in the inbound listener: -```yaml -http_filters: -- name: envoy.filters.http.lua - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua - inline_code: | - function envoy_on_request(request_handle) - request_handle:headers():add("x-authbridge-direction", "inbound") - end -- name: envoy.filters.http.ext_proc - ... -- name: envoy.filters.http.router - ... -``` - -**Key lesson:** Envoy HTTP filter execution order is: Lua → ext_proc → router. Route-level `request_headers_to_add` only takes effect during routing. Always use a filter to inject headers ext_proc needs. - -### 3. SPIFFE File Permission Denied (HISTORICAL) - -**Symptom:** `cat: /opt/jwt_svid.token: Permission denied` from a second container. - -**Root cause:** spiffe-helper ran as root and wrote the file `0600`, while the -reader (client-registration) ran as UID 1000. - -**Fix at the time:** align `RunAsUser` / `RunAsGroup` across both containers. - -**Today this symptom cannot recur:** the in-process mirror writes -`/opt/jwt_svid.token` mode `0644` (`core/spiffe/mirror.go`), not `0600`, and the -`client-registration` reader is gone. `svid_key.pem` is still `0600`, so a -different-UID reader of *the key* would be denied — that is a different symptom -from the one above. - -### 4. Istio Ambient Mesh Inbound Path - -With Istio ambient mesh, inbound traffic enters via **OUTPUT** (ztunnel delivery), NOT PREROUTING. `init-iptables.sh` PROXY_OUTPUT rule 1 (mark 0x539 + !uid 1337 + dst LOCAL) handles this correctly. - -**Diagnosis:** If PROXY_INBOUND REDIRECT has 0 packets but PROXY_OUTPUT rule 1 has non-zero, ambient mesh is active. - -## Debugging Techniques - -### Inbound Validation - -```bash -# Direct to inbound port (bypasses iptables, tests ext_proc only) -AGENT_POD_IP=$(kubectl get pod -n -l app= -o jsonpath='{.items[0].status.podIP}') -kubectl exec test-client -n -- curl -s -o /dev/null -w "%{http_code}" http://$AGENT_POD_IP:15124/.well-known/agent.json -# Expected: 401 - -# Through service (full path: iptables + ext_proc) -kubectl exec test-client -n -- curl -s http://:8000/.well-known/agent.json -# Expected: {"error":"unauthorized","message":"missing Authorization header"} -``` - -### ext_proc Direction Classification - -```bash -kubectl logs deployment/ -n -c envoy-proxy --since=30s 2>&1 | head -40 -# "=== Inbound Request Headers ===" → correctly classified -# "=== Outbound Request Headers ===" → Lua filter missing or misconfigured -# "Traffic direction INBOUND" → Envoy knows it's inbound (from listener) -``` - -### AuthBridge Logs - -```bash -# Inbound JWT validation -kubectl logs deployment/ -n -c envoy-proxy 2>&1 | grep "\[Inbound\]" -# Token exchange (outbound) -kubectl logs deployment/ -n -c envoy-proxy 2>&1 | grep "\[Token Exchange\]" -``` - -### Client Registration - -There is no `rossoctl-client-registration` container to read logs from — after -cortex#411 registration runs in the operator's `ClientRegistrationReconciler`. -For the registration side, read the operator's controller logs in its own -namespace; only the Keycloak query below still applies here. - -```bash -# Query Keycloak (use --data-urlencode for SPIFFE IDs) -curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \ - --data-urlencode "clientId=spiffe://localtest.me/ns//sa/" \ - --get "http://keycloak.localtest.me:8080/admin/realms/rossoctl/clients" | jq '.[0].clientId' -``` - -## Demo Deployment Checklist - -1. **ConfigMaps FIRST** — Apply before deploying agents. Stale ConfigMaps cause silent registration failures. -2. **Rebuild after code changes** — `init-iptables.sh` changes → rebuild proxy-init. `configmaps.yaml` changes → `kubectl apply` + restart pods. -3. **Verify proxy-init backend** — Logs must show `Using iptables command: iptables-legacy`. -4. **Agent env vars** — git-issue-agent uses `MCP_URL` (NOT `MCP_SERVER_URL`) and needs `JWKS_URI`. -5. **Envoy timeout** — Set to `300s` for LLM agents. Default 15s causes `upstream request timeout`. -6. **Ollama** — Must be running (`ollama serve`) before end-to-end queries with local LLM. -7. **A2A protocol** — git-issue-agent uses v0.3.0 with method `message/send` (NOT `tasks/send`). Requires `messageId` field. -8. **Keycloak client ID with SPIRE** — Full SPIFFE ID (e.g., `spiffe://localtest.me/ns/team1/sa/git-issue-agent`), not a short name. -9. **Sidecar injection** — there is no `webhook-rollout.sh` in this repo any more; injection is the operator's webhook, so roll pods rather than re-running a script. -10. **Keycloak scopes** — `github-full-access` is OPTIONAL; must be explicitly requested in token requests. -11. **ISSUER vs TOKEN_URL** — `ISSUER` = Keycloak frontend URL (in token `iss` claim). `TOKEN_URL` = internal service URL. They differ in K8s. -12. **Keycloak port 8080** — Must be in `OUTBOUND_PORTS_EXCLUDE` to prevent ext_proc token exchange redirect loop. - -## PR and Issue Conventions - -- PR titles MUST follow conventional commits: `feat:`, `fix:`, `docs:`, `refactor:`, etc. -- Demo documentation PRs use `docs:` prefix. -- Bug fix PRs use `fix:` prefix. -- Split large changes into reviewable PRs (e.g., manual demo separate from UI demo, AuthProxy fixes separate from demo docs). - -## What Triggers a Rebuild - -| Change | Action | -|--------|--------| -| `init-iptables.sh` or `Dockerfile.init` | Rebuild proxy-init image, `kind load`, delete pod | -| `core/` or `cmd/cortex/` | Rebuild the `authbridge` image, `kind load`, delete pod | -| `core/` or `cmd/cortex-envoy/` | Rebuild the `authbridge-envoy` image, `kind load`, delete pod | -| `configmaps.yaml` (any section) | `kubectl apply -f configmaps.yaml`, delete pod | -| `*-deployment.yaml` | `kubectl apply -f ` (rolling update) | -| `setup_keycloak.py` | Re-run `python setup_keycloak.py` | -| Sidecar injection behaviour | Lives in `rossoctl/operator`, not here — rebuild and redeploy the operator there, then delete the agent pod for re-injection | diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md index 16f3696c4..8c0a76384 100644 --- a/.github/CLAUDE.md +++ b/.github/CLAUDE.md @@ -8,7 +8,7 @@ including the PR title format and the Go checks that CI gates on, are in the roo | Workflow | Trigger | Purpose | |----------|---------|---------| -| `ci.yaml` | PR to main/release-*, push to main | Pre-commit; `go vet` and build for all 10 modules, and tests for all but the three demos; a macOS job for `peerproc`, `tlsbridge`, `cmd/cortex` and `cmd/agentop`; the cgo `cpex` builds (`go-ci-cpex`); `go mod tidy -diff` for all 10 modules; proxy-init iptables, Python, install-script and release-notes-script tests. `go fmt` rewrites rather than fails, so it does not gate | +| `ci.yaml` | PR to main/release-*, push to main | Pre-commit; `go vet`, build and tests for all 7 modules; a macOS job for `peerproc`, `tlsbridge`, `cmd/cortex` and `cmd/agentop`; the cgo `cpex` builds (`go-ci-cpex`); `go mod tidy -diff` for all 7 modules; proxy-init iptables, Python, install-script and release-notes-script tests. `go fmt` rewrites rather than fails, so it does not gate | | `build.yaml` | Tag push (`v*`), push to main, manual; PRs to main build amd64 only and skip `**.md` | Multi-arch Docker builds for the six images below. Every Go image passes `GO_BUILD_TAGS` naming a profile — plugins are all opt-in, so an image built without tags registers none | | `praxis.yaml` | PR to main/release-*, push to main, on `praxis/**` or the Go parser paths it ports | `cargo fmt --check`, clippy and tests for `praxis/`. A failure blocks only when `praxis/` or the workflow changed | | `security-scans.yaml` | PR to main | Dependency review, shellcheck, YAML lint, Hadolint, Bandit, Trivy, CodeQL | diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1a8290c2f..e453a97cb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -265,34 +265,6 @@ jobs: - name: Test run: go test -v -race -cover ./... - # The demo modules build against core through a replace directive, so a core - # API change can break them; nothing else compiles them. - go-ci-demos: - name: Go CI (demos/${{ matrix.demo }}) - runs-on: ubuntu-latest - timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - demo: [echo, finance-sparc, ibac] - defaults: - run: - working-directory: demos/${{ matrix.demo }} - env: - GOWORK: "off" - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: demos/${{ matrix.demo }}/go.mod - cache-dependency-path: demos/${{ matrix.demo }}/go.sum - - - name: Vet and build - run: | - go vet ./... - go build ./... - go-ci-readme-demo: name: Go CI (README demo) runs-on: ubuntu-latest diff --git a/.github/workflows/dependabot-tidy.yml b/.github/workflows/dependabot-tidy.yml index 71a3b31f9..1ea6a8ff0 100644 --- a/.github/workflows/dependabot-tidy.yml +++ b/.github/workflows/dependabot-tidy.yml @@ -51,7 +51,7 @@ jobs: echo "::group::go mod tidy in $mod" (cd "$mod" && go mod tidy) echo "::endgroup::" - done < <(find . -name go.mod -not -path '*/demos/*' -print0 | sort -z) + done < <(find . -name go.mod -print0 | sort -z) - name: Commit and push if changed id: commit diff --git a/.github/workflows/security-scans.yaml b/.github/workflows/security-scans.yaml index 4b4dc0bd0..ecf1baa1e 100644 --- a/.github/workflows/security-scans.yaml +++ b/.github/workflows/security-scans.yaml @@ -248,7 +248,6 @@ jobs: scan-type: 'config' scan-ref: '.' severity: 'CRITICAL,HIGH,MEDIUM' - skip-dirs: 'demos' exit-code: '0' format: 'table' diff --git a/.gitignore b/.gitignore index e5c4dfb8d..c9bad6217 100644 --- a/.gitignore +++ b/.gitignore @@ -77,8 +77,7 @@ __pycache__/ # can tell "the compiled output" from a source file or a package directory — # `cmd/*/*` would ignore the source. One line per binary is the only # pattern that is safe, so the list below is exhaustive for `cmd/` and for the -# two `scripts/` helpers further down. The `demos/*` mains are deliberately not -# listed. Keep adding to it. +# two `scripts/` helpers further down. Keep adding to it. cmd/agentop/agentop cmd/cortex/cortex cmd/cortex-envoy/cortex-envoy diff --git a/CLAUDE.md b/CLAUDE.md index 44c621aab..a00bd92e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,7 +93,6 @@ directories, read its `CLAUDE.md`** if it has not loaded: | [`core/plugins/CLAUDE.md`](core/plugins/CLAUDE.md) | Inbound JWT validation and outbound token exchange: route resolver, config loading, credential files | | [`core/listener/vmcontrol/CLAUDE.md`](core/listener/vmcontrol/CLAUDE.md) | The MOCA SBX VM control channel | | [`deploy/proxy-init/CLAUDE.md`](deploy/proxy-init/CLAUDE.md) | `init-iptables.sh` and its gotchas | -| [`demos/CLAUDE.md`](demos/CLAUDE.md) | Kubernetes deployment: pod shape, shared volumes, ConfigMaps, Envoy, Keycloak, Kind builds, cluster gotchas | | [`.github/CLAUDE.md`](.github/CLAUDE.md) | CI workflows, container images, adding an image to CI, release binaries | ## Repository Overview @@ -132,7 +131,7 @@ artifact identifiers that other things address by name: | Frozen — do not rename | Where it is defined | |---|---| | `authbridge`, `authbridge-envoy`, `authbridge-lite`, `authbridge-cpex` | published image names; the operator selects images **by name** | -| `authbridge-proxy` as a **container** name | **another repo's API** — the operator's `AuthBridgeProxyContainerName`. `kubectl … -c authbridge-proxy`, agentop's pod picker and the demos' sidecar detection address it. The binary inside is `cortex`; the operator sets only `Args`, so the image's entrypoint picks it | +| `authbridge-proxy` as a **container** name | **another repo's API** — the operator's `AuthBridgeProxyContainerName`. `kubectl … -c authbridge-proxy`, agentop's pod picker and the cortex-demos sidecar detection address it. The binary inside is `cortex`; the operator sets only `Args`, so the image's entrypoint picks it | | `x-authbridge-{direction,secret}`, `x-authbridge-unmapped-` | wire protocol | | `AUTHBRIDGE_*` | env vars users may already have set | | `authbridge-config{,-}`, `authbridge-runtime{,-config,-mtls}`, `authproxy-routes` | ConfigMaps the operator creates and mounts | @@ -176,7 +175,6 @@ remains there: it runs `scripts/install.sh`, for the URL v0.7.0 and its docs giv | `docs/` | Plugin and framework reference ([`docs/README.md`](docs/README.md) is the index). `docs/superpowers/{plans,specs}/` holds dated design records and is **still written to** | | `scripts/` | `install.sh` (laptop installer), `keycloak_sync.py` (scope mappings from `routes.yaml`), `profile-tags/` (build-tag profiles), `hooks/commit-msg`, `dev/` | | `deploy/` | `proxy-init/` (iptables init container), `sparc-service/`, `lineage-attach/` | -| `demos/` | End-to-end demos; [`demos/README.md`](demos/README.md) gives the order | | `tests/` | Python tests | ## AuthBridge Binaries @@ -200,11 +198,10 @@ each binary links it in its own `storage_redis.go`. A build with no `-tags` regi no plugins and rejects every config it is handed. Tag sets come from [`scripts/profile-tags`](scripts/profile-tags/), one profile per shipped artifact. -**Go modules:** 10 in total, all on Go 1.26.10 except the three `demos/*` modules (1.24). +**Go modules:** 7 in total, all on Go 1.26.10. - `core/`, `core/storage/redis/` (nested, but its own module), `cmd/cortex`, `cmd/cortex-envoy`, `cmd/agentop`, `scripts/profile-tags`, `scripts/readme-demo`. `go.work` links these 7. -- `demos/{echo,finance-sparc,ibac}/` are self-contained and outside the workspace. - `core/` is **consumed outside this repo**, so removing exported API there is a cross-repo change. @@ -299,7 +296,7 @@ re-verify them by hand after a change. - Run `go vet ./...` and `gofmt -l .` from each module you touched, before you push. **Always give `gofmt` a path:** a bare `gofmt -l` reads stdin, scans nothing and exits 0. -- **Only `go vet` is a CI gate**, on all 10 modules (cpex code is vetted with +- **Only `go vet` is a CI gate**, on all 7 modules (cpex code is vetted with `-tags cpex` in `go-ci-cpex`). pre-commit has no Go hooks, and CI's `go fmt ./...` rewrites the checkout and exits 0, so formatting drift never fails a build. - Run per-module commands with `GOWORK=off`, as every CI Go job but `core`'s does, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4cd70a722..384c26164 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -110,7 +110,7 @@ out to be one of these six. **4. Deploy a workload.** Use a demo rather than hand-written manifests; they are kept current, and the manual path is not. Start from -[`demos/README.md`](demos/README.md). +[`cortex-demos/README.md`](https://github.com/rossoctl/cortex-demos/blob/main/README.md). ## Installing an unreleased build @@ -364,7 +364,7 @@ Smaller pull requests are typically easier to review and merge. If your pull req Always give `gofmt` a path — a bare `gofmt -l` reads stdin, so it scans nothing and exits 0. `gofmt -l .` also lists any pre-existing offenders, so check the names it prints are yours. -- `go vet` is gated: every `Go CI (…)` job runs it, on all 10 modules, and a finding fails the job. +- `go vet` is gated: every `Go CI (…)` job runs it, on all 7 modules, and a finding fails the job. `gofmt` is not — CI's lint step runs `go fmt`, which rewrites files and exits 0, so unformatted code still goes green. There are no Go hooks in pre-commit either. - Run per-module with `GOWORK=off` — how the root Makefile builds, and how every diff --git a/cmd/README.md b/cmd/README.md index 7cc7d1c91..6d1ee78a5 100644 --- a/cmd/README.md +++ b/cmd/README.md @@ -41,7 +41,7 @@ The session-events API at `:9094` is documented in [`../docs/session-api.md`](../docs/session-api.md), hot-reload in [`../CLAUDE.md`](../CLAUDE.md#config-hot-reload-9093reloadstatus), and the supporting ConfigMap contracts in -[`../demos/CLAUDE.md`](../demos/CLAUDE.md#configmaps-and-secrets-expected-at-runtime). +[`cortex-demos/CLAUDE.md`](https://github.com/rossoctl/cortex-demos/blob/main/CLAUDE.md#configmaps-and-secrets-expected-at-runtime). ## Ports diff --git a/cmd/agentop/cmd_pipeline.go b/cmd/agentop/cmd_pipeline.go index b71662c22..1affa1794 100644 --- a/cmd/agentop/cmd_pipeline.go +++ b/cmd/agentop/cmd_pipeline.go @@ -247,7 +247,7 @@ func writePipelineTable(view *apiclient.PipelineView, stdout io.Writer) { // same divider. // // Only when there is something on both sides of it. A pipeline with one chain empty is - // ordinary — demos/context-guru/k8s/authbridge-config.yaml ships `inbound.plugins: []` + // ordinary — cortex-demos/context-guru/k8s/authbridge-config.yaml ships `inbound.plugins: []` // — and printing the divider there put it first or last, asserting that plugins run // before or after the application when none do. The whole-table guard above catches // only the case where BOTH chains are empty. diff --git a/cmd/agentop/cmd_pipeline_test.go b/cmd/agentop/cmd_pipeline_test.go index 4da71dcbf..93363db4d 100644 --- a/cmd/agentop/cmd_pipeline_test.go +++ b/cmd/agentop/cmd_pipeline_test.go @@ -357,7 +357,7 @@ func TestRunPipelineGet_ColumnsAlignAcrossRows(t *testing.T) { // TestRunPipelineGet_DividerOnlyBetweenTwoChains — the divider asserts that plugins run // before and after the application, so it must not appear when one side is empty. // -// Not hypothetical: demos/context-guru/k8s/authbridge-config.yaml in this repo ships +// Not hypothetical: cortex-demos/context-guru/k8s/authbridge-config.yaml ships // `inbound.plugins: []` with an outbound chain, and against that the divider printed as the // FIRST row of the table. func TestRunPipelineGet_DividerOnlyBetweenTwoChains(t *testing.T) { diff --git a/core/config/config_test.go b/core/config/config_test.go index ffcdfc957..ab87f79e8 100644 --- a/core/config/config_test.go +++ b/core/config/config_test.go @@ -568,7 +568,7 @@ func TestSessionConfig_SessionIDHeadersFor(t *testing.T) { // TestSessionConfig_InClusterDefaultDoesNotReadGenericHeaders pins the in-cluster default to // what it was before OpenCode support. X-Session-Id is a generic name: the IBAC demo agent -// (demos/ibac/agent) sends it on its tool calls with an id it minted. Read by default, that +// (cortex-demos/ibac/agent) sends it on its tool calls with an id it minted. Read by default, that // filed its http_post into an empty session, where IBAC found no user intent and, under // no_intent_policy: allow, let the exfiltration through. // diff --git a/core/config/tlsbridge_bind_test.go b/core/config/tlsbridge_bind_test.go index 7bbbd49cc..c456f814b 100644 --- a/core/config/tlsbridge_bind_test.go +++ b/core/config/tlsbridge_bind_test.go @@ -8,7 +8,7 @@ import "testing" // fills the wildcard ":9094" — leaving the session API, which carries // DECRYPTED bodies under the bridge, bound on every interface. This is the // exact ordering trap the BindLoopbackOnly block's comment in presets.go -// describes. demos/context-guru/context-guru-tls-bridge.yaml has this shape. +// describes. cortex-demos/context-guru/context-guru-tls-bridge.yaml has this shape. func TestTLSBridge_SessionAPILoopbackWhenAddrUnset(t *testing.T) { cfg := loadFrom(t, `mode: proxy-sidecar listener: diff --git a/core/listener/parity/protocol_parity_test.go b/core/listener/parity/protocol_parity_test.go index 1f1917af7..ffd97c549 100644 --- a/core/listener/parity/protocol_parity_test.go +++ b/core/listener/parity/protocol_parity_test.go @@ -14,7 +14,7 @@ import ( // A protocol parse reaches the session row in both directions, on every listener (#1575). // // The parsers do not check the direction, and our own configs put a2a-parser on the -// outbound chain (demos/session-budget). But each listener built its rows by hand, and +// outbound chain (cortex-demos/session-budget). But each listener built its rows by hand, and // the copies kept only the extension expected in their direction: outbound rows had no // A2A and inbound rows had no MCP. The parser's invocation was recorded, so the row // showed "a2a-parser observe/matched_message/send" with no method, contextId or parts diff --git a/core/plugins/CLAUDE.md b/core/plugins/CLAUDE.md index ee00e977d..536b14192 100644 --- a/core/plugins/CLAUDE.md +++ b/core/plugins/CLAUDE.md @@ -35,7 +35,7 @@ including the plugin capability and classification rules, are in the root - The operator-supplied env vars (`KEYCLOAK_URL`, `KEYCLOAK_REALM`, `TOKEN_URL`, `ISSUER`, `DEFAULT_OUTBOUND_POLICY`, `CLIENT_ID`) are consumed by the default `authbridge-runtime-config` via `${VAR}` expansion — they land inside the appropriate plugin's `config:` block rather than a top-level section. - `jwt-validation` derives `jwks_url` when omitted: from `keycloak_url` + `keycloak_realm` if both are set, else from `issuer` (appends `/protocol/openid-connect/certs`). - `token-exchange` derives `token_url` when omitted from `provider` + `provider_url` + `provider_realm`. `keycloak_url` / `keycloak_realm` are deprecated aliases and log a warning. -- Credential files: the **operator** registers each workload with Keycloak and creates a Secret containing `client-id.txt` + `client-secret.txt`; the operator's webhook mounts that Secret at `/shared/client-id.txt` and `/shared/client-secret.txt` in containers that share the `shared-data` volume. SPIRE-issued credentials are sourced in-process via the `spiffe.Provider` (built from the top-level `spiffe:` block in `authbridge-runtime-config`) — authbridge's hot path reads X.509 SVIDs from an in-memory `spiffe.X509Source` (no per-handshake file I/O), and `token-exchange` consumes a JWT-SVID from the injected Provider via `plugins.BuildWithSPIFFE`. The Provider also mirrors the SVIDs under `/opt/` (see the Shared Volume Contract in [`demos/CLAUDE.md`](../../demos/CLAUDE.md#shared-volume-contract)). `jwt-validation` reads the audience from `/shared/client-id.txt` via `audience_file`; `token-exchange` reads client credentials via `client_id_file` / `client_secret_file` (polling: see the credential-file gotcha below). The `rossoctl.io/client-registration-inject: "true"` label is **no longer functional** — the operator's `ClientRegistrationReconciler` still treats it as a "skip operator-managed registration" signal (`SkipReason` in `operator/internal/clientreg/names.go:58`), but the legacy sidecar that the label deferred to is gone. Setting it today silently breaks registration; do not add it to new manifests. +- Credential files: the **operator** registers each workload with Keycloak and creates a Secret containing `client-id.txt` + `client-secret.txt`; the operator's webhook mounts that Secret at `/shared/client-id.txt` and `/shared/client-secret.txt` in containers that share the `shared-data` volume. SPIRE-issued credentials are sourced in-process via the `spiffe.Provider` (built from the top-level `spiffe:` block in `authbridge-runtime-config`) — authbridge's hot path reads X.509 SVIDs from an in-memory `spiffe.X509Source` (no per-handshake file I/O), and `token-exchange` consumes a JWT-SVID from the injected Provider via `plugins.BuildWithSPIFFE`. The Provider also mirrors the SVIDs under `/opt/` (see the Shared Volume Contract in [`cortex-demos/CLAUDE.md`](https://github.com/rossoctl/cortex-demos/blob/main/CLAUDE.md#shared-volume-contract)). `jwt-validation` reads the audience from `/shared/client-id.txt` via `audience_file`; `token-exchange` reads client credentials via `client_id_file` / `client_secret_file` (polling: see the credential-file gotcha below). The `rossoctl.io/client-registration-inject: "true"` label is **no longer functional** — the operator's `ClientRegistrationReconciler` still treats it as a "skip operator-managed registration" signal (`SkipReason` in `operator/internal/clientreg/names.go:58`), but the legacy sidecar that the label deferred to is gone. Setting it today silently breaks registration; do not add it to new manifests. - Outbound `default_policy`: `passthrough` (default) or `exchange`, configured per-plugin (there is no top-level field; `authbridge-runtime-config` expands the `DEFAULT_OUTBOUND_POLICY` env var into the plugin config). ## Modifying Token Exchange Logic @@ -47,7 +47,7 @@ including the plugin capability and classification rules, are in the root `podman build -f cmd/cortex-envoy/Dockerfile --build-arg GO_BUILD_TAGS="$(go -C scripts/profile-tags run . envoy)" -t authbridge-envoy:latest .`, then load it into Kind as - [`demos/CLAUDE.md`](../../demos/CLAUDE.md#building-everything-locally) shows. + [`cortex-demos/CLAUDE.md`](https://github.com/rossoctl/cortex-demos/blob/main/CLAUDE.md#building-everything-locally) shows. ## Modifying Inbound JWT Validation - Edit `core/plugins/jwtvalidation/validation/` -- the JWKS-backed JWT verifier diff --git a/core/plugins/cpex/README.md b/core/plugins/cpex/README.md index 3d2f27769..90671c232 100644 --- a/core/plugins/cpex/README.md +++ b/core/plugins/cpex/README.md @@ -649,6 +649,6 @@ the linker warns that objects target a newer macOS; `make cortex-cpex` does. ## See also -- `demos/hr-cpex` — end-to-end demo with Bob, Eve, and Alice personas +- `cortex-demos/hr-cpex` — end-to-end demo with Bob, Eve, and Alice personas covering the Workday flow, GitHub flow with Cedar, PII scanning, and session taint propagation. diff --git a/core/plugins/cpex/hrdemo_cpex_test.go b/core/plugins/cpex/hrdemo_cpex_test.go index dc0b4c317..037fb2af9 100644 --- a/core/plugins/cpex/hrdemo_cpex_test.go +++ b/core/plugins/cpex/hrdemo_cpex_test.go @@ -280,7 +280,7 @@ func (m *hrIdP) clientToken(t *testing.T) string { // secret in a temp file. func hrPolicy(t *testing.T, idp *hrIdP) string { t.Helper() - raw, err := os.ReadFile(filepath.Join("..", "..", "..", "demos", "hr-cpex", "k8s", "cpex-policy.yaml")) + raw, err := os.ReadFile(filepath.Join("testdata", "hr-cpex", "cpex-policy.yaml")) if err != nil { t.Fatal(err) } @@ -327,7 +327,7 @@ type hrCPEXConfig struct { func hrManifestConfig(t *testing.T) hrCPEXConfig { t.Helper() - raw, err := os.ReadFile(filepath.Join("..", "..", "..", "demos", "hr-cpex", "k8s", "authbridge.yaml")) + raw, err := os.ReadFile(filepath.Join("testdata", "hr-cpex", "authbridge.yaml")) if err != nil { t.Fatal(err) } diff --git a/demos/hr-cpex/k8s/authbridge.yaml b/core/plugins/cpex/testdata/hr-cpex/authbridge.yaml similarity index 100% rename from demos/hr-cpex/k8s/authbridge.yaml rename to core/plugins/cpex/testdata/hr-cpex/authbridge.yaml diff --git a/demos/hr-cpex/k8s/cpex-policy.yaml b/core/plugins/cpex/testdata/hr-cpex/cpex-policy.yaml similarity index 100% rename from demos/hr-cpex/k8s/cpex-policy.yaml rename to core/plugins/cpex/testdata/hr-cpex/cpex-policy.yaml diff --git a/demos/CLAUDE.md b/demos/CLAUDE.md deleted file mode 100644 index 29aa8648d..000000000 --- a/demos/CLAUDE.md +++ /dev/null @@ -1,213 +0,0 @@ -# demos — agent notes: Kubernetes deployment - -Loaded by Claude Code when you read a file under `demos/`. It covers how the AuthBridge sidecar runs in a Kubernetes pod: the pod shape, the files and ConfigMaps the operator provides, Envoy, Keycloak, and the cluster gotchas. The repo-wide rules are in the root [`CLAUDE.md`](../CLAUDE.md). The iptables details are in [`deploy/proxy-init/CLAUDE.md`](../deploy/proxy-init/CLAUDE.md), and the auth plugin paths in [`core/plugins/CLAUDE.md`](../core/plugins/CLAUDE.md). Paths in backticks are written from the repo root. - -## How the Components Work Together - -The operator (in a separate repo) injects AuthBridge sidecars -into workload pods. Default deployment shape (proxy-sidecar mode): - -``` - ┌────────────────────────────────────┐ - │ WORKLOAD POD │ - │ │ - │ cortex ──► SPIRE Agent │ (in-process - │ - spiffe.Provider reads SVIDs │ Workload API - │ over the Workload API and │ client; shaped - │ mirrors them under /opt/ │ by the `spiffe:` - │ - Reverse proxy: inbound JWT │ config block) - │ - Forward proxy: outbound │ - │ token exchange │ - │ │ │ - │ Your Application │ - │ (HTTP_PROXY → forward proxy) │ - └────────────────────────────────────┘ - - The operator also creates a Secret with client-id + - client-secret and mounts it at /shared/. - - For envoy-sidecar mode, replace cortex with - the authbridge-envoy image (Envoy + ext_proc) and add a - proxy-init container for iptables. -``` - -There is no `spiffe-helper` sidecar and no `SPIRE_ENABLED` gate. SPIRE -credentials are fetched in-process by `core/spiffe`'s Provider. - -## Shared Volume Contract - -The sidecar and the operator communicate through files on shared volumes: - -| Path | Writer | Reader | Content | -|------|--------|--------|---------| -| `/opt/jwt_svid.token` | spiffe.Provider mirror | authbridge (token-exchange) + external readers | JWT SVID, audience from the plugin's `identity.jwt_audience`. Written **only** when a plugin requests a JWT-SVID for an audience (today: `token-exchange` with `identity.type: spiffe`) — an X.509-only workload never produces it | -| `/opt/svid.pem` | spiffe.Provider mirror | external readers (debugging, future Envoy SDS) | X.509 SVID leaf cert (PEM) | -| `/opt/svid_key.pem` | spiffe.Provider mirror | external readers | X.509 SVID private key (PEM) | -| `/opt/svid_bundle.pem` | spiffe.Provider mirror | external readers | SPIRE trust bundle (PEM, may concatenate multiple CAs) | -| `/shared/client-id.txt` | operator (Secret mount) | authbridge (`jwt-validation`'s `audience_file`) | SPIFFE ID or workload name | -| `/shared/client-secret.txt` | operator (Secret mount) | authbridge (`token-exchange`) | Keycloak client secret | - -The X.509 SVID files are mirrored to disk by the in-process -`spiffe.Provider` when `spiffe.mirror_files` is on (the default); the files -exist for external readers (e2e probes, -debugging, future Envoy filesystem SDS) and are kept fresh on every -rotation. The listener itself reads SVIDs in-memory via -`spiffe.X509Source` and never re-reads the files. authbridge enables -mTLS only when `mtls:` is configured at the top level of the -runtime config; absent that block, today's plaintext behavior is -preserved. - -## ConfigMaps and Secrets Expected at Runtime - -When the operator injects sidecars, the target namespace needs these resources. -The rossoctl Helm chart's `agent-namespaces.yaml` and -`authbridge-template-configmaps.yaml` templates render them; the operator copies -them into agent namespaces that don't already have them: - -| Resource | Kind | Used by | Keys | -|----------|------|---------|------| -| `authbridge-config` | ConfigMap | authbridge | `KEYCLOAK_URL`, `KEYCLOAK_REALM`, `PLATFORM_CLIENT_IDS` (optional), `TOKEN_URL` (optional, derived from KEYCLOAK_URL+KEYCLOAK_REALM), `ISSUER` (optional, derived or explicit for split-horizon DNS), `DEFAULT_OUTBOUND_POLICY` (optional, defaults to `passthrough`). Inbound audience validation uses `CLIENT_ID` from `/shared/client-id.txt` by default (the other audience sources are in [`core/plugins/CLAUDE.md`](../core/plugins/CLAUDE.md)). Target audience and scopes are configured per-route in `authproxy-routes`. | -| `authbridge-runtime-config` | ConfigMap | authbridge | The runtime `config.yaml` — `mode`, `listener`, `pipeline`, and the top-level `session` / `stats` / `mtls` / `spiffe` / `tls_bridge` / `memory` / `pricing` / `cost_ledger` blocks. Note the name differs from the `authbridge-runtime` volume the operator mounts it through. | -| `keycloak-admin-secret` | Secret | operator (ClientRegistrationReconciler) | `KEYCLOAK_ADMIN_USERNAME`, `KEYCLOAK_ADMIN_PASSWORD` | -| `authproxy-routes` | ConfigMap (optional) | authbridge | `routes.yaml` -- per-host token exchange rules (format below) | -| `envoy-config` | ConfigMap | Envoy (inside the `authbridge-envoy` combined image, envoy-sidecar mode only) | `envoy.yaml` (full Envoy configuration) | -| `spiffe-helper-config` | ConfigMap (legacy, unused) | nothing | Held `helper.conf` for the `spiffe-helper` binary, which no image bundles now. The demos still list it as expected, but authbridge does not read it: the top-level `spiffe:` block in `authbridge-runtime-config` configures SPIRE. | - -**`authproxy-routes` format** (`routes.yaml`): -```yaml -routes: - - host: "github-tool-mcp" - target_audience: "github-tool" - token_scopes: "openid github-tool-aud github-full-access" - - host: "auth-target-*" - target_audience: "auth-target" - token_scopes: "openid auth-target-aud" -``` - -**Note:** `authproxy-routes` is optional. Without it, all outbound traffic passes through unchanged (the default policy is `passthrough`). Only create it when the agent needs to call services that require token exchange. Set `DEFAULT_OUTBOUND_POLICY: "exchange"` in `authbridge-config` to restore the legacy behavior. - -## Envoy Configuration - -Envoy config lives in the `envoy-config` ConfigMap rendered by the [rossoctl Helm chart](https://github.com/rossoctl/rossoctl) at install time (template: `charts/rossoctl/templates/agent-namespaces.yaml` / `authbridge-template-configmaps.yaml`). Key listeners: `outbound_listener` (15123), `inbound_listener` (15124). Inbound listener injects `x-authbridge-direction: inbound` header. Both use ext_proc cluster pointing to the authbridge binary on localhost:9090. - -## Changing Envoy Configuration -- Edit the `envoy.yaml` template in the [rossoctl Helm chart](https://github.com/rossoctl/rossoctl) - (`charts/rossoctl/templates/agent-namespaces.yaml` or - `authbridge-template-configmaps.yaml`) and `helm upgrade` -- Key listener/cluster names: `outbound_listener`, `inbound_listener`, `original_destination`, `ext_proc_cluster` -- After changes, restart the affected pods so they pick up the new ConfigMap content - -## Keycloak Setup Scripts - -The two main demo setup scripts (`demos/echo/` and `demos/finance-sparc/` have their own): - -| Script | Location | Use Case | -|--------|----------|----------| -| `setup_keycloak_weather_advanced.py` | `demos/weather-agent/` | Weather agent (advanced) demo: realm setup, scopes for token exchange to the weather tool's audience, alice user. Used by `deploy_and_verify_advanced.sh` (run by hand; no workflow runs it). | -| `setup_keycloak.py` | `demos/github-issue/` | GitHub issue integration demo (creates github-tool client, github-tool-aud + github-full-access scopes, alice + bob users) | - -**Common Keycloak defaults across all scripts:** -- URL: `http://keycloak.localtest.me:8080` -- Realm: `rossoctl` -- Admin: `admin` / `admin` - -**Note:** the two scripts above share the same helper function patterns (`get_or_create_realm`, `get_or_create_client`, `get_or_create_client_scope`, etc.) and are idempotent. - -## Adding New Keycloak Resources to Setup -- Edit the appropriate `setup_keycloak*.py` script -- Use the `get_or_create_*` helper pattern for idempotency -- All scripts use `python-keycloak` library (KeycloakAdmin class) - -## Building Everything Locally - -`scripts/dev/local-build-and-test.sh` orchestrates every image -the platform needs (`spiffe-idp-setup` from rossoctl, plus -`authbridge`, `authbridge-envoy`, `authbridge-lite`, `proxy-init` -from this repo) and loads them into a Kind cluster. Run it from -the repo root: - -```bash -ROSSOCTL_DIR=../rossoctl ./scripts/dev/local-build-and-test.sh -``` - -To build a single image directly. The Go images build from the repo root, and -proxy-init builds from `deploy/proxy-init/`: - -```bash -# proxy-init (iptables init container, envoy-sidecar + proxy-sidecar enforce-redirect) -cd deploy/proxy-init -make docker-build-init -make load-image # Uses KIND_CLUSTER_NAME env var (default: rossoctl) -cd ../.. - -# Sidecar images. Pick whichever you need; the operator selects the image per -# workload from the resolved AuthBridge mode. Plugins are all opt-in, so these two -# Dockerfiles refuse to build without GO_BUILD_TAGS naming a profile. -podman build -f cmd/cortex/Dockerfile \ - --build-arg GO_BUILD_TAGS="$(go -C scripts/profile-tags run . full)" \ - -t authbridge:latest . # proxy-sidecar (default) -podman build -f cmd/cortex-envoy/Dockerfile \ - --build-arg GO_BUILD_TAGS="$(go -C scripts/profile-tags run . envoy)" \ - -t authbridge-envoy:latest . # envoy-sidecar -# authbridge-lite: same proxy Dockerfile, built with the `lite` profile. -podman build -f cmd/cortex/Dockerfile \ - --build-arg GO_BUILD_TAGS="$(go -C scripts/profile-tags run . lite)" \ - -t authbridge-lite:latest . - -# `kind load docker-image` reads Docker's image store. For images built with podman, -# load an archive instead: see load_image_to_kind in scripts/dev/local-build-and-test.sh. -kind load docker-image authbridge:latest --name rossoctl -kind load docker-image authbridge-envoy:latest --name rossoctl -kind load docker-image authbridge-lite:latest --name rossoctl -``` - -## Running the Full Demo - -1. Set up a Kind cluster with SPIRE + Keycloak (use [Rossoctl installer](https://www.rossoctl.dev/docs/get-started/kubernetes)) -2. Deploy the webhook via [operator](https://github.com/rossoctl/operator) -3. See the [Cortex demos index](README.md) for a recommended learning path: - - **Getting started**: `demos/weather-agent/demo-ui.md` (inbound validation, UI deployment) - - **Full flow**: `demos/github-issue/demo-ui.md` (token exchange + scope-based access) - - **Routes config reference**: `demos/token-exchange-routes/README.md` (single + multi-target route patterns) - -The recommended end-to-end flow is the weather-agent advanced demo, which -exercises the combined sidecar shape with token exchange to a tool's audience: - -```bash -# Apply manifests, run Keycloak setup, verify end-to-end -demos/weather-agent/deploy_and_verify_advanced.sh -``` - -For an interactive walkthrough see -[`demos/weather-agent/demo-ui-advanced.md`](weather-agent/demo-ui-advanced.md). - -## Kubernetes Manifests -- Example deployment YAMLs in `demos/*/k8s/` - -## Important Cross-Component Relationships - -1. **Envoy Proxy UID:** Envoy runs as UID 1337. The `proxy-init` iptables rules exclude this UID from redirection to prevent loops. The operator runs the injected proxy container as the same UID (`Proxy.UID`, default 1337), whichever image it is. The `authbridge` image's own default is `USER 1001`, so a pod built without the operator runs it as 1001, which proxy-init does not exempt. - -2. **Port Coordination:** the `proxy-init` iptables rules redirect to Envoy's 15123 (outbound) and 15124 (inbound); see Important Port Mapping in the root [`CLAUDE.md`](../CLAUDE.md#important-port-mapping). - -## Cluster gotchas - -- **ISSUER vs TOKEN_URL**: `ISSUER` must be the Keycloak **frontend URL** (what appears in the `iss` claim of tokens), while `TOKEN_URL` is the **internal service URL**. These are often different in Kubernetes (e.g., `http://keycloak.localtest.me:8080` vs `http://keycloak-service.keycloak.svc:8080`). - -- **TLS passthrough is one-way**: Outbound HTTPS traffic passes through Envoy without token exchange via the TLS passthrough filter chain. Only plaintext HTTP outbound traffic reaches authbridge. With the default outbound policy of `"passthrough"`, even plaintext HTTP traffic is forwarded unchanged unless it matches an explicit route in `authproxy-routes`. - -- **Outbound passthrough is the safe default**: `DEFAULT_OUTBOUND_POLICY` defaults to `passthrough`, so token exchange only happens for hosts explicitly listed in `authproxy-routes`, where target audience and scopes are configured per-route. That is what lets outbound traffic to LLM inference endpoints (e.g., Ollama via `host.docker.internal`) work untouched. If this were set to `exchange`, every outbound HTTP call would attempt token exchange and fail for non-Keycloak destinations. - -- **Route host patterns must match the HTTP Host header**: The `host` field in `authproxy-routes` is matched against the HTTP `Host` header, which the client sets from the URL hostname. For in-cluster calls that is the **short Kubernetes service name** from `MCP_URL` (e.g., `github-tool-mcp`), not the FQDN. Glob patterns (`*`, `?`, `[...]`) work, but a plain service name is the common case; the wrong pattern (e.g., `*.github-issue-tool*.svc.cluster.local`) silently falls through to the default passthrough policy. - -- **Envoy config not embedded:** The envoy-proxy sidecar mounts `envoy-config` ConfigMap at `/etc/envoy`. This ConfigMap must exist in the target namespace before workloads are created. - -- **Envoy Lua filter required for inbound**: The `x-authbridge-direction: inbound` header MUST be injected via a Lua filter before the ext_proc filter in the inbound listener. Route-level `request_headers_to_add` does NOT work because the router filter runs after ext_proc. - -- **Admin credentials in ConfigMap**: the rossoctl Helm chart's - `agent-namespaces.yaml` template stores Keycloak admin credentials - in `authbridge-config` (a ConfigMap, not a Secret). This is for - demo / dev clusters only — production should use a Kubernetes - Secret and mount via SecretKeyRef. - -- **Keycloak scope assignment for dynamically registered clients**: When the operator's `ClientRegistrationReconciler` auto-registers an agent as a Keycloak client, the client may not inherit all necessary scopes. The agent's own audience scope (e.g., `agent-team1-git-issue-agent-aud`) must be a **default** client scope for inbound JWT audience validation to work. Token exchange scopes (e.g., `github-tool-aud`, `github-full-access`) must be **optional** client scopes for `client_credentials` grants with explicit `scope=` to succeed. Re-run the demo's `setup_keycloak.py` after the agent is deployed to assign these scopes to the registered client. diff --git a/demos/README.md b/demos/README.md deleted file mode 100644 index e47044786..000000000 --- a/demos/README.md +++ /dev/null @@ -1,167 +0,0 @@ -# Cortex Demos - -This directory contains demo scenarios showing Cortex providing zero-trust -authentication for Kubernetes agent workloads. Each demo progressively introduces -more Cortex capabilities. - -> **Note:** These demos use the operator-injected combined sidecar (after -> cortex#411 — `authbridge` for proxy-sidecar, `authbridge-envoy` -> for envoy-sidecar, and `authbridge-lite`, the proxy image built with the -> sidecar-minimum `lite` profile from -> `scripts/profile-tags`). The previous `authbridge-unified` image and the per-component -> sidecars (`client-registration`, standalone `spiffe-helper`) have been -> removed. - -## Available Demos - -| Demo | Difficulty | What It Shows | Deployment | -|------|:----------:|---------------|:----------:| -| **[Weather Agent](weather-agent/demo-ui.md)** | Beginner | Inbound JWT validation, automatic identity registration, outbound passthrough | UI | -| **[Weather Agent (advanced)](weather-agent/demo-ui-advanced.md)** | Intermediate | Inbound on agent **and** tool, outbound token exchange, ingress JWT verification on the tool | [kubectl + script](weather-agent/demo-ui-advanced.md#step-5-optional-verify-via-cli) | -| **[GitHub Issue Agent](github-issue/demo.md)** | Intermediate | Inbound validation + outbound token exchange + scope-based access control | [UI](github-issue/demo-ui.md) or [Manual](github-issue/demo-manual.md) | -| **[Token-Exchange Routes](token-exchange-routes/README.md)** | Reference | How to write `authproxy-routes` for single- and multi-target token exchange | Configuration only | -| **[Session Budget](session-budget/README.md)** | Reference | Test assets for the `session-budget` plugin, including a pause-mode webhook stub. Also see [`hitl-local.md`](session-budget/hitl-local.md) for a laptop-only walkthrough of `on_exceed: pause` (no Kubernetes required). | kubectl or local | -| **[agentop Walkthrough](weather-agent/demo-with-agentop.md)** | Reference | Watch the Cortex plugin pipeline live with the `agentop` TUI | Tooling only | -| **[IBAC](ibac/README.md)** | Intermediate | Intent-Based Access Control: LLM judge denies outbound HTTP that doesn't align with the user's recorded intent. Reproduces the email-poison / prompt-injection attack from `huang195/ibac`; runs the agent with and without IBAC side by side, so agentop shows the one row where they differ: the exfiltration, denied or delivered | kubectl + agentop | -| **[SPARC (finance)](finance-sparc/README.md)** | Intermediate | SPARC pre-tool reflection: the `sparc` plugin blocks a hallucinated/ungrounded tool argument (an invented transaction id) before it executes and transparently asks the user to clarify, then approves the corrected call. Complements IBAC — SPARC verifies argument grounding, IBAC verifies intent alignment | UI + kubectl | -| **[Lineage](lineage/README.md)** | Intermediate | Per-request lineage on the Weather Agent pair: attach the sidecar with the lineage attach kit and see one turn first as 19 separate traces (the app forwards no `traceparent`), then as one trace of 70 spans with one root once the app's own propagation is switched on. Deploys the two stock images plain; edits nothing else | kubectl + scripts | -| **[Lineage attach kit](../deploy/lineage-attach/README.md)** | Reference | Attach per-request lineage to any existing Deployment: enable the `lineage-telemetry` plugin and every HTTP exchange becomes two facts-only spans (`request` + `response`, paired by `lineage.exchange.id`) sent to **any** OTLP consumer. A strategic-merge patch + ConfigMap, generated and validated; a propagate-only OTel shim for uninstrumented Python apps, activated by one env var | kubectl + scripts | -| **[CPEX Bridge (HR)](hr-cpex/README.md)** | Advanced | CPEX/APL declarative policy: one route chains a coarse APL predicate, an embedded Cedar PDP, RFC 8693 token exchange with a post-check, PII redaction and audit plugins. Same request, different data per caller (Bob sees an SSN, Eve gets it redacted). Self-contained: its own kind cluster + namespace, deployed via `make` rather than operator injection | [kubectl (make)](hr-cpex/README.md#quick-start) | -| **[Coding agent (CPEX)](coding-agent-cpex/README.md)** | Advanced | A sandboxed coding agent behind Cortex built with cpex: an allowed host is not an authorized disclosure. A report carrying a secret, sent from a session that read `.env`, or filed with the wrong project is denied, while clean reports pass; a command that ignores the proxy cannot connect. With `--custody`, Vault holds the support credential and Cortex attaches it only to allowed reports. Scripted agent, mock LLM and support service, runs on a laptop | local (`run.sh`) | -| **[Echo](echo/README.md)** | Beginner | Credential placeholder swap: the user's real token never reaches the agent — inbound `jwt-validation` forwards only an opaque placeholder — yet the agent's outbound call still carries a correctly exchanged real token to the upstream, which echoes what it received as ground truth | UI + kubectl (make) | -| **[Context Guru](context-guru/README.md)** | Intermediate | Context engineering: the `context-guru` outbound plugin compacts an agent's growing tool-output context before it reaches the LLM, so a task whose raw context exceeds the model's window still fits. Same agent/model/window in three modes (`off` / `observe` / `enforce`) shows the agent's answer flip from wrong to right | UI + kubectl (script) | - -## Recommended Path - -**New to Cortex?** Start with the demos in this order: - -1. **[Weather Agent](weather-agent/demo-ui.md)** — Fastest way to see Cortex - in action. Deploys via the Rossoctl UI with inbound JWT validation protecting - the agent. No token exchange configuration needed; outbound traffic uses the - default passthrough policy. - -2. **[GitHub Issue Agent](github-issue/demo.md)** — Full Cortex demo with - inbound validation *and* outbound token exchange. Shows how Cortex - transparently exchanges tokens when the agent calls the GitHub tool, with - scope-based access control (Alice vs Bob). - -3. **[Token-Exchange Routes](token-exchange-routes/README.md)** — Reference - for the `authproxy-routes` ConfigMap. Covers both single-target (one - route) and multi-target (one agent → multiple tools, each with its own - audience) patterns. Configuration-only — pair with one of the deployment - demos above for a working stack. - -## What Each Demo Covers - -### Weather Agent (Getting Started) -- Deploy agent + tool via **Rossoctl UI** -- AuthBridge inbound JWT validation (signature, issuer, audience) -- Automatic SPIFFE identity registration with Keycloak -- Default outbound passthrough — agents work out-of-the-box with any tool or LLM -- CLI testing: public endpoints, token rejection, valid token - -### Weather Agent (Advanced) -- Same images as the beginner demo, separate **`*-advanced`** Deployments so the - getting-started flow stays untouched -- Outbound **token exchange** to the tool SPIFFE audience (`authproxy-routes`) -- AuthBridge **injected on the MCP tool** — JWT checks at Envoy before the tool process -- `deploy_and_verify_advanced.sh` for reproducible CI-style verification (Keycloak - exchange + MCP `initialize` without requiring a working LLM) - -### GitHub Issue Agent (Full Cortex Flow) -- Deploy agent + tool via **Rossoctl UI** or **kubectl** -- Keycloak configuration for token exchange (realm, clients, scopes) -- Inbound JWT validation protecting the agent -- Outbound OAuth 2.0 token exchange (RFC 8693) — agent-scoped token exchanged - for tool-scoped token -- Subject preservation through exchange (`sub` claim maintained) -- Scope-based access control: Alice (public repos) vs Bob (all repos) -- Comprehensive CLI testing and AuthBridge sidecar log verification - -### Token-Exchange Routes (Configuration Reference) -- How AuthBridge resolves the request `Host` header to a route entry -- ConfigMap shape for `authproxy-routes` — fields, glob patterns, ordering -- Single-target example (one route) -- Multi-target example (multiple routes, one agent → multiple tools) -- Mixing exchange and passthrough; tightening to `default_policy: exchange` -- Troubleshooting: `Host` mismatches, missing scopes, audience errors - -### Session Budget -- Cap how much an agent can spend per session — inference calls, - tokens, or wall-clock time with Redis-backed counters shared - across pods -- Three responses when a session hits its cap: `deny` (return 403), - `observe` (log only, don't block), or `pause` (call a webhook and - let a human approve or reject before the request continues) -- Cluster path ([`README.md`](session-budget/README.md)): production - shape — plugin runs as a sidecar, session IDs come from inbound - A2A requests, pause approvals go to a webhook stub deployed via - `kubectl` -- Local path ([`hitl-local.md`](session-budget/hitl-local.md)): - laptop-only walkthrough with Docker, `go run`, and `curl` — the - fastest way to see pause-mode approval end-to-end, no Kubernetes - required - -### agentop Walkthrough (Tooling Reference) -- Run the `agentop` TUI against the weather-agent's session API -- See inbound JWT validation → protocol parsers → outbound exchange - → LLM inference → response, paired live - -### CPEX Bridge (HR) — APL Policy Engine -- **Self-contained**: own namespace `cpex-demo`, deployed with plain - `kubectl apply` via `make deploy` (not operator-injected); the - `hr-mcp` backend, Keycloak realm, and chat agent are all vendored -- Runs the `cpex` plugin (the `authbridge-cpex` image) with CPEX's - policy language **APL** as a sidecar in front of an MCP backend -- **Embedded PDP**: APL consults Cedar through a `cedar:` step (the - slot is engine-agnostic by design — OPA, AuthZen, …) -- **Explicit effects**: field redaction on request *and* response, a - PII content guardrail, and structured audit logging — all as policy - steps, not backend code -- **Authorization as action**: RFC 8693 token exchange (`delegate(...)`) - is a first-class policy step, with a post-check that refuses to - forward a token the IdP narrowed below what the call needs -- Seven deterministic curl scenarios plus an interactive LLM chat with - mid-conversation persona switching (Bob / Eve / Alice) - -## Prerequisites - -Cluster-backed demos (everything above except the session-budget local -walkthrough) require: -- A Kubernetes cluster with the Rossoctl platform installed - ([Installation Guide](https://github.com/rossoctl/rossoctl/blob/main/docs/operate/install-kubernetes.md)) -- Keycloak deployed in the `keycloak` namespace (not used by the Lineage demo) -- SPIRE deployed (for demos using SPIFFE identity; not used by the Lineage demo) - -UI-based demos additionally require: -- The Rossoctl UI running at `http://rossoctl-ui.localtest.me:8080` - -The session-budget [`hitl-local.md`](session-budget/hitl-local.md) -walkthrough is Kubernetes-free — see its own Prerequisites section -(Docker, Ollama, Go). - -## Common Setup: Keycloak Port-Forward - -Most demos need Keycloak accessible at `http://keycloak.localtest.me:8080`. -If not already available via an ingress: - -```bash -kubectl port-forward service/keycloak-service -n keycloak 8080:8080 -``` - -## Common Setup: Python Environment - -Demos that configure Keycloak need a Python virtual environment: - -```bash -# from the repo root -python -m venv venv -source venv/bin/activate -pip install --upgrade pip -pip install -r requirements.txt -``` - -## Related Documentation - -- [Cortex Overview](../docs/architecture.md) — Architecture and design -- [Rossoctl Operator](https://github.com/rossoctl/operator) — Admission webhook for sidecar injection (migrated from this repo) diff --git a/demos/coding-agent-cpex/.gitignore b/demos/coding-agent-cpex/.gitignore deleted file mode 100644 index da209dc1b..000000000 --- a/demos/coding-agent-cpex/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Generated by run.sh: keys, tokens, CAs, logs. -.state/ -config/identity/ -demo/*.cast -demo/*.gif diff --git a/demos/coding-agent-cpex/README.md b/demos/coding-agent-cpex/README.md deleted file mode 100644 index f026aa7b4..000000000 --- a/demos/coding-agent-cpex/README.md +++ /dev/null @@ -1,217 +0,0 @@ -# Coding agent security with Cortex and CPEX - -This demo runs a scripted coding agent through Cortex with the -[cpex bridge](../../core/plugins/cpex/README.md). -The agent submits bug reports to a mock support service. Policy checks block -reports containing secrets, reports for unauthorized projects, and later -reports from sessions that have read a secret. - -Cortex also checks the model's proposed tool calls for secret leaks before -returning them to the agent. The scenarios use scripted model responses, so -you can repeat each test and check its outcome. - -[![asciicast](./demo/coding-agent-cpex.svg)](https://asciinema.org/a/ixFycJuuJlZqrEGR) - -To use the same Cortex and policy with real Claude Code, follow the -[Claude Code guide](claude-code/README.md). - -## Run the demo - -You need: - -- macOS on arm64. -- `python3`, `openssl`, `curl`, `jq`, `lsof`, and `gzip`. -- [`srt`](https://www.npmjs.com/package/@anthropic-ai/sandbox-runtime), - installed with `npm install -g @anthropic-ai/sandbox-runtime`. -- DNS resolution for `localtest.me`, which resolves to loopback. -- A cpex build of Cortex: run `make cortex-cpex` from the repository root, - or install it with `install.sh --cpex`. - -From the repository root: - -```sh -cd demos/coding-agent-cpex -./run.sh up # Start Cortex and the mock services -./run.sh scenarios # Run all scenarios and print a summary -./run.sh walkthrough # Step through the same scenarios with narration -./run.sh down # Stop the demo -``` - -The walkthrough pauses for Enter between scenarios. Use `--fast` to skip -pauses or `--pace N` to advance automatically every N seconds. - -The demo starts a separate Cortex instance on loopback ports `4770x`, with -its own CA and state under `.state/` and keys under `config/identity/`. -It leaves `~/.cortex` and any installed Cortex service on `:47600` unchanged. -Startup fails if another process is using a demo port. - -### Optional: keep the support credential in Vault - -Start with `./run.sh up --custody` to have Cortex retrieve and attach the -support credential only after a report passes policy checks. The agent -never receives the credential, and the support service requires it. - -This mode needs Docker and a Cortex build linked against CPEX 0.3.1 or -later, as provided by `make cortex-cpex`. Without `--custody`, scenarios 07 -and 08 are skipped. - -## Scenarios - -| Scenario | Action | Expected result | -|---|---|---| -| 00 | Bypass the proxy or read the developer token from a sandboxed command | Both attempts blocked by the sandbox | -| 01 | Send a report to an unapproved host | Connection blocked (403) | -| 02 | Send a report without secrets to project A | Accepted (201) | -| 03 | Send a report containing an AWS key | Report blocked (403) | -| 04 | Read `.env`, then send a report without secrets | Report blocked (403) | -| 05 | Send a report to project B as a developer authorized only for A | Report blocked (403) | -| 06 | Send a report without secrets from a new session | Accepted (201) | -| 07 | Send valid reports with `--custody` enabled | Accepted (201), with the credential attached by Cortex | -| 08 | With `--custody`, send disallowed reports, then a valid report while Vault is unavailable | All blocked (403); no reports delivered | -| 09 | After reading `.env`, receive a model response proposing a command that leaks the key | Model response blocked (403); the proposed command never runs | - -The scenarios also test several bypass attempts and failure cases: - -- **01:** Setting `Host: support.localtest.me` does not disguise a connection - to `paste.evil.example`: both attempts get `cpex.egress_host_not_allowed`. - Cortex also rejects requests whose `Host` differs from the tunnel's - destination (421). -- **03:** The key triggers `cpex.secret_in_body`. Padding it beyond the - inspection limit or gzip-encoding the body triggers - `cpex.egress_body_uninspectable`. -- **04:** Changing `X-Session-Id` does not bypass `cpex.session_tainted_secret`. -- **07–08:** Cortex replaces a made-up `Authorization` header with the Vault - credential. Disallowed reports never trigger a Vault lookup. If Vault - does not respond, an otherwise valid report gets - `cpex.delegation_vault_timeout`. -- **09:** The model response check runs for both JSON and streamed replies - (SSE), returning `cpex.secret_in_tool_call`. A report without secrets from - a separate session still succeeds. - -Checks inspect tool output, the Cortex session timeline, and the mock -services' request logs. They verify that blocked reports never reach the -support service, denial responses and CPEX decision records omit the test -secret, and neither mock receives the developer token. Model traffic that -contains a secret can still appear on the timeline; see [Limits](#limits). - -## What goes through Cortex - -| Traffic | Route to Cortex | Coverage | -|---|---|---| -| Sandboxed commands and their children | The sandbox's proxy port | Enforced: commands have no other network route | -| The agent's model calls (and WebFetch in Claude Code) | The agent's proxy configuration; `HTTPS_PROXY` for Claude Code | Routed through Cortex while the process respects that configuration | -| File tools, local MCP servers, and hooks | No direct interception | Execution is outside the sandbox and Cortex's traffic inspection | - -The agent and both mock services run locally. The mock Anthropic API tells -the agent which tools to run: `Read` runs in the agent process; `Bash` runs in -[`srt`](https://www.npmjs.com/package/@anthropic-ai/sandbox-runtime), the -sandbox runtime used by Claude Code. Sandboxed commands can reach the -network only through Cortex. The mock support service accepts bug reports -for projects A and B. - -```text - agent ──(its own model calls, X-Claude-Code-Session-Id)──┐ - └─ srt sandbox: curl ──(only route out)───────────────┤ - ▼ - demo Cortex :47700 inference-parser → mcp-parser → cpex - │ │ - ▼ ▼ - mock LLM llm.localtest.me:47710 mock support support.localtest.me:47711 -``` - -## How it works - -The policy is in [`config/cpex-policy.yaml`](config/cpex-policy.yaml), with -bridge settings in [`config/cortex.yaml`](config/cortex.yaml). - -- **Identify the developer.** Cortex loads a developer JWT from - `principal.token_file` and passes it to CPEX in memory. The token is never - forwarded to either mock service, and the sandbox cannot read it. -- **Associate requests with a session.** The agent names its session with - `X-Claude-Code-Session-Id`. Cortex uses process ancestry to associate - child commands such as `curl` with that session. With - `session_source: resolved`, a child's `X-Session-Id` header cannot change - that association. -- **Check HTTP requests.** With `synthetic_egress`, requests not handled by - another parser become `http_egress` tool calls. Policy checks the host, - path, and request body. This demo inspects up to 8 KiB and blocks bodies - that exceed that limit or use an unsupported encoding. -- **Track secret reads.** When a model request includes a tool result - matching a secret pattern, CPEX marks the session as having read a secret - ("tainted"). PPE 0.5.0 requires a denial to set this mark. The bridge's - `observe_codes` setting records `secret_in_tool_result` as `observe`, - allowing the model request to continue. Later support reports from the - session are blocked. -- **Check proposed tool calls.** On `cmf.llm_output`, policy checks - `custom.tool_calls`, including calls assembled from an SSE stream. In a - tainted session, a call containing a recognized secret or matching a - command pattern for sending a secrets file causes Cortex to block the - model response (403). Requests from commands that do run are still - checked by the HTTP policy. -- **Fetch credentials after approval.** With `--custody`, the final - `http_egress` policy step calls `delegate(support-vault, ...)` to retrieve - the support credential as the developer. Cortex attaches it through - `delegated_credentials`, restricted to `support.localtest.me` by - `delegated_credential_hosts`. Vault runs in dev mode on loopback, with JWT - authentication for the developer key, the credential at - `agents/dev-alice/support`, and its root token under `.state/`. See - [`docker-compose.yml`](docker-compose.yml). - -## Limits - -- The mock model returns scripted responses. The demo tests specific policy - rules and bypass attempts; it does not measure a live model's behavior. -- Secret detection uses the patterns in the policy. Transformed secrets - that do not match those patterns may go undetected. HTTP request bodies - larger than 8 KiB or using an encoding other than `identity` are blocked. -- Session tracking starts when Cortex sees a secret in a tool result sent - to the model. A command that reads and sends a secret can run before - Cortex observes that read. HTTP request checks still apply to its traffic. -- With cpex enabled, Cortex buffers responses, including SSE streams, up - to the listener's 10 MB cap. The agent receives a streamed reply only - after it is complete. Downloads exceeding the cap fail when Cortex - decrypts and inspects the connection. Hosts configured for passthrough - avoid this buffering and are not inspected. -- Model traffic is recorded on the session timeline. A tool result sent to - the model can therefore expose the secret there. The developer JWT and, - with `--custody`, the Vault support credential are kept out of that traffic. -- Session taint is stored in CPEX's process memory and resets when Cortex - restarts or reloads its configuration. -- Without `--custody`, the mock support service accepts reports without a - support credential. - -## Recording and development - -Run `./run.sh check` to verify that [`demo/steps.md`](demo/steps.md), the -walkthrough, and `scenarios/` list the same scenarios, and to run the -recording script's tests. - -With the demo running, `python3 demo/record_demo.py --record` records the -walkthrough using asciinema. For a video showing the walkthrough, policy -rules, and agentop together, use `demo/record_tmux.sh`. It requires tmux, -asciinema 3, agg, ffmpeg, and an agentop build (`make agentop` from the -repository root). - -The video recorder clears the demo Cortex's session list and writes `.cast`, -`.gif`, and `.mp4` files under the repository's `.sketchpad/demo-video/`. -Use `--only NN` to record one scenario. See -[`demo/record_tmux.sh`](demo/record_tmux.sh) or run it with `--help` for layout -and timing details. - -For shorter terminal output, run `./walkthrough.sh --presenter` (or set -`PRESENTER=1`). It prints one line per agent action and one outcome per step -while keeping all checks enabled. Failed checks still print in full. See -`./walkthrough.sh --help` for other options. - -## References - -- [PPE (Praxis Policy Engine)](https://github.com/praxis-proxy/policy) provides - the policy engine behind the demo's reference monitor, including identity - resolution, authorization, session tracking, and credential delegation. - In this demo, it validates the developer's identity, checks requests and - proposed tool calls, tracks sessions that have read secrets, and optionally - retrieves support credentials from Vault for approved requests. -- [CPEX](https://github.com/contextforge-org/cpex) wraps PPE and provides Go - bindings through a C FFI. Cortex's cpex bridge uses those bindings to - submit requests and model responses for evaluation and enforce the - resulting decisions. diff --git a/demos/coding-agent-cpex/agent/agent.py b/demos/coding-agent-cpex/agent/agent.py deleted file mode 100644 index a3bf74c32..000000000 --- a/demos/coding-agent-cpex/agent/agent.py +++ /dev/null @@ -1,266 +0,0 @@ -"""Scripted coding agent. - -Sends Anthropic Messages requests through the demo Cortex, using -X-Claude-Code-Session-Id to identify the session. Runs Bash tools under srt -and Read tools in the agent process. Supports JSON and SSE model responses. -Prints the transcript and writes tool results to --out as JSON. - -Environment: DEMO_PROXY, DEMO_CA, LLM_URL, SRT_SETTINGS, WORKSPACE, -SUPPORT_URL. AGENT_TURN_DELAY adds a delay in seconds before model calls -after the first (default 0). AGENT_OPEN_DELAY can extend the first delay -to let the recording open the session. - -PRESENTER=1 prints tool summaries and outcomes. WALKTHROUGH_HOOK receives -`verdict allow|deny|blocked