Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8254211
fix: Make the laptop quickstart one command, and fix what blocked it
huang195 Sep 3, 2026
46ffe2e
fix: Keep every Cortex artifact under ~/.cortex
huang195 Sep 3, 2026
df79bab
refactor: Drop "demo" from the local install path
huang195 Sep 3, 2026
4b7c81c
fix: Make the quickstart hand off to the cost guide without breaking
huang195 Sep 3, 2026
1d1c79c
refactor: One local config, so the two guides stop being the same page
huang195 Sep 3, 2026
148fdc5
refactor: Drop two compatibility aliases that were compatible with no…
huang195 Sep 3, 2026
dc60ed4
fix: Don't enable pruning on a fresh install
huang195 Sep 3, 2026
fb49ffc
feat: Add `tools scan --all`, and fix a count that hid the window
huang195 Sep 3, 2026
655b71d
fix: Say which window the printed scan command uses
huang195 Sep 3, 2026
5f654ec
fix: Address PR review — Linux no-op, phantom paths, key perms
huang195 Sep 3, 2026
0987600
feat: Run Claude Code as plain `claude`, no env vars
huang195 Sep 3, 2026
d19126d
docs: Name Claude Code in the quickstart, and cut it in half
huang195 Sep 3, 2026
8225377
feat: Plain `abctl` finds the local Cortex; fix [l] pointing at the w…
huang195 Sep 3, 2026
1ab308f
fix: Address review round two — eight findings
huang195 Sep 3, 2026
389ebf7
docs: Catch plugin-catalog.md up with the scan's new behavior
huang195 Sep 3, 2026
79f2f57
fix: Checksum verification failed open, plus eight review findings
huang195 Sep 3, 2026
d10039d
refactor: Drop our lite-tags file in favour of #861
huang195 Sep 3, 2026
313b89c
fix: disable no longer deletes a setting the user had before enable
huang195 Sep 4, 2026
f7378c7
fix: A corrupt ownership record no longer loses a setting silently
huang195 Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ jobs:
binary:
- authbridge-proxy
- authbridge-envoy
# abctl is published by release-binaries.yaml, so it has to build
# under GOWORK=off here too. Omitting it let a missing go.sum entry
# for an authlib transitive dep reach main: the workspace build used
# everywhere else resolved it, and the per-module release build did
# not — surfacing only when a tag was cut.
- abctl
# authbridge-praxis was in no workflow at all, which is how it ended
# up broken under GOWORK=off by the same missing go.sum entry. Any
# cmd/* module absent from every workflow will drift this way — the
# workspace hides exactly this class of breakage. (authbridge-cpex is
# deliberately not here: it needs CGO and libcpex_ffi.a from a pinned
# release, so build.yaml covers it via its image build instead.)
- authbridge-praxis
defaults:
run:
working-directory: authbridge/cmd/${{ matrix.binary }}
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,12 @@ authbridge/authbridge-praxis
# Local experiment state, not project source.
mlflow.db
mlruns/

# Cortex writes its CA + keys under ~/.cortex. This covers the fallback location
# used when $HOME is unresolvable, and any explicit `--ca-dir ./cortex-ca`, so a
# private key can never be committed from a demo run inside a checkout.
cortex-ca/

# Local git worktrees (git worktree add .worktrees/<name>). Committing these
# gitlinks pins another branch's checkout into this tree, which is never wanted.
.worktrees/
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,41 @@ Cortex delivers easy-to-use platform services to agentic workloads. It runs in a

It ships as a single binary; the identity and access layer is **AuthBridge**, and the code lives under [`authbridge/`](./authbridge/).

## Quick start (local, no Kubernetes)
## Quick start — Claude Code on your laptop

Watch an AI agent's traffic — its model, tool, and agent-to-agent calls — decrypted and parsed live on your laptop.
See what Claude Code sends: model calls, tool calls, and agent-to-agent traffic,
decrypted and parsed live. No Kubernetes. macOS or Linux, amd64 or arm64.

1. **Install and start the demo** (macOS/Linux). Downloads two small binaries and starts the proxy in the background:
1. **Install, and point Claude Code at it** (asks first, changes nothing else):

```sh
curl -fsSL https://raw.githubusercontent.com/rossoctl/cortex/main/authbridge/install-demo.sh | sh
curl -fsSL https://raw.githubusercontent.com/rossoctl/cortex/main/authbridge/install.sh \
| sh -s -- --claude-code
```

2. **Open the live viewer** in another terminal:
2. **Open the viewer** in another terminal:

```sh
abctl --endpoint http://localhost:47601
abctl
```

3. **Send an agent's traffic through it** — e.g. Claude Code, from the directory where you started the demo:
3. **Run Claude Code:**

```sh
HTTPS_PROXY=http://localhost:47600 \
NODE_EXTRA_CA_CERTS="$PWD/cortex-ca/ca.crt" \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
claude
claude
```

Its calls stream into `abctl`, decrypted and parsed.
Its calls stream into `abctl`. Cortex only reads them — nothing is rewritten.

## Cut Claude Code token cost on your laptop
Stop it with `pkill -f authbridge-proxy`. Undo step 1 with
`abctl claude-code disable`.

Already using Claude Code? Cortex can strip the tool definitions your agent never
calls out of every request. Measured over 99 requests in one session: **4–20% of
the prompt billed per turn, median 6%**. The share is highest early — the removed
bytes are a fixed size, so as the conversation grows they shrink as a fraction of
it — and depends on how many of the tools you actually use. Four steps, about two minutes:
**[Cut Claude Code token cost](./authbridge/docs/laptop-token-savings.md)**.
**Cut token cost too:** Cortex can strip the tool definitions your agent never
calls, worth **4–20% of the prompt per turn, median 6%** —
**[one more command](./authbridge/docs/laptop-token-savings.md)**.

Any agent works, not just Claude Code — point it at the proxy on
`localhost:47600` and trust `~/.cortex/ca/ca.crt`.

## Running on Kubernetes

Expand Down
8 changes: 8 additions & 0 deletions authbridge/authlib/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,14 @@ type ListenerConfig struct {
// mode preset is ":9094". Set to empty string to disable the endpoint.
SessionAPIAddr string `yaml:"session_api_addr" json:"session_api_addr"`

// HealthAddr is the bind address for the liveness/readiness server
// (/healthz, /readyz). Every mode preset defaults it to ":9091", which is
// what Kubernetes probes expect. It is configurable because the literal was
// previously hardcoded, and two proxies on one host could therefore never
// coexist: the second died on a bind conflict. Local setups can pin it to
// loopback on another port; leaving it empty keeps the preset default.
HealthAddr string `yaml:"health_addr" json:"health_addr"`

// SkipHosts lists outbound destination host patterns whose traffic
// bypasses the plugin pipeline AND session recording entirely. The
// listener forwards matched requests as a transparent proxy without
Expand Down
4 changes: 4 additions & 0 deletions authbridge/authlib/config/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func ApplyPreset(cfg *Config) {
// session.enabled: false — main.go skips the API server when the
// store itself is nil.
setDefault(&cfg.Listener.SessionAPIAddr, ":9094")

// Health server is default-on for every mode; ":9091" is what the operator's
// probe config and the container images expect.
setDefault(&cfg.Listener.HealthAddr, ":9091")
}

func setDefault(field *string, value string) {
Expand Down
8 changes: 8 additions & 0 deletions authbridge/authlib/runtimeutil/runtimeutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ func InitLogging(binaryName string) {
}
h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})
slog.SetDefault(slog.New(h).With("binary", binaryName))
// slog.SetDefault also routes the standard log package through this handler,
// and it does so at Info unless told otherwise. Every fatal startup error in
// the binaries goes through log.Fatalf, so without this a port clash — the
// most common local failure — prints as INFO and the process then exits,
// leaving an operator scanning an apparently clean log for a cause. Fatals
// are the only std-log users in these binaries, so raising the bridge to
// Error labels them correctly rather than mislabelling anything else.
slog.SetLogLoggerLevel(slog.LevelError)
}

// StartSignalToggle installs a SIGUSR1 handler that toggles the process log
Expand Down
8 changes: 7 additions & 1 deletion authbridge/authlib/tlsbridge/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,13 @@ func NewGeneratedFileSource(certPath, keyPath, trustPath string) (CASource, erro
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(certPath), 0o755); err != nil {
// 0700, not 0755: this directory is about to hold a CA signing key. The key
// file itself is 0600 below, so 0755 exposed the listing rather than the key
// — but under the default layout ca_dir sits inside a 0700 ~/.cortex, and with
// an explicit --ca-dir elsewhere it had no private parent at all. An existing
// directory keeps its mode (MkdirAll does not tighten), so a mounted ca_dir is
// unaffected.
if err := os.MkdirAll(filepath.Dir(certPath), 0o700); err != nil {
return nil, fmt.Errorf("tlsbridge: create ca_dir: %w", err)
}
// Each file is written atomically (temp + rename) so a reader or a
Expand Down
11 changes: 10 additions & 1 deletion authbridge/cmd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,22 @@ ConfigMap contracts are documented in
| 8081 | Forward proxy (outbound; HTTP_PROXY target) |
| 8082 | Transparent egress listener (enforce-redirect capture target) |
| 8083 | Transparent inbound listener (`inbound_interception: transparent`) |
| 9091 | Health |
| 9091 | Health (`listener.health_addr`) |
| 9093 | Stats / config inspection |
| 9094 | Session Events API (consumed by `abctl`) |

`8080` and `8083` are mutually exclusive: `inbound_interception` picks one
inbound mechanism, and the preset fills only that one's address.

All of these are overridable, which matters for running two proxies on one host:
a second instance on the default ports dies on a bind conflict. They are not all
under the same config key — everything above is a `listener.*` address except
`9093`, which is `stats.stats_address`. The defaults bind every interface, which
is what Kubernetes probes and sidecar traffic need but not what a laptop wants;
local single-host setups typically pin them all to `127.0.0.1`. `authbridge-proxy
--local` ships exactly such a config — see
[`docs/laptop-token-savings.md`](../docs/laptop-token-savings.md).

`8082` and `8083` are the iptables REDIRECT targets installed by
[`proxy-init`](../proxy-init/) and must match its `TRANSPARENT_PORT` /
`INBOUND_TRANSPARENT_PORT`. A mismatch redirects traffic to a dead port.
Expand Down
Loading
Loading