-
Notifications
You must be signed in to change notification settings - Fork 0
feat: import Ferro operator console as standalone module #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
418c782
feat: add Ferro operator console
MitulShah1 22cdf29
docs: simplify v0.1.0 changelog for first release
MitulShah1 15b87b9
feat(tui): rework the wide home header
MitulShah1 55eacea
docs: re-record the console demo as docs/console.gif
MitulShah1 fa20b27
fix(tui): preallocate the header readout column, unexport the mark art
MitulShah1 3dd363b
fix(table): neutralize control characters in table cells
MitulShah1 d49c6cf
fix(command): honour "missing is not zero" for latency, stop escaping…
MitulShah1 a096c47
fix: attribute the stream idle timeout to ferro, not the gateway
MitulShah1 04d70cf
fix(api): fail loudly on oversized bodies, bound error text, sync tes…
MitulShah1 db2a76f
refactor(table): hold the model listing's shape in one place
MitulShah1 49854a2
fix(tui): keep a modal's actions when the pane is short
MitulShah1 855db28
ci: harden the module boundary check, the release job, and the fixtur…
MitulShah1 e53ed41
test: isolate config resolution, deflake the tail test, widen itest r…
MitulShah1 56b8e97
fix(table): neutralize control characters on every surface, not one
MitulShah1 61f880a
fix(api): charge every frame against the stream bound, free the heade…
MitulShah1 274d600
fix(api): keep paging when the gateway omits total_entries
MitulShah1 210e4b0
refactor(table): hold the provider listing's shape in one place
MitulShah1 806c330
fix(config): confine the MASTER_KEY fallback to a loopback gateway
MitulShah1 57beeaf
feat(command): gate keys rotate and revoke behind a typed confirmation
MitulShah1 fda8a7e
fix(version): fall back to the embedded build info
MitulShah1 ffc40b0
fix(tui): follow a selected log row by its identity, not its trace id
MitulShah1 18b5bc7
ci: unalias the concurrency group, make release builds reproducible
MitulShah1 0e2d3bc
test: isolate the chat tests from a developer's ferro config
MitulShah1 03b9fa3
docs: record the behaviour changes and declare the copyright
MitulShah1 b42f56e
docs: give the README the gateway's header and its own badges
MitulShah1 88ffdb3
docs: carry the gateway's logo.png verbatim
MitulShah1 0bf0e7b
fix(api): bound the envelope error message and the stream's header wait
MitulShah1 6dc82b6
refactor: write the 501 rule once, and start one fixture server per test
MitulShah1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: ["main"] | ||
| pull_request: | ||
| branches: ["main"] | ||
| workflow_call: | ||
| schedule: | ||
| - cron: "0 4 * * 1" | ||
|
|
||
| concurrency: | ||
| # Literal "ci-", never ${{ github.workflow }}: inside a reusable workflow that | ||
| # expression resolves to the *caller's* name, so the `uses: ./.github/workflows/ci.yml` | ||
| # leg of release.yml would compute "Release-refs/tags/vX" while release.yml's own | ||
| # group is "release-refs/tags/vX" -- the same group, because GitHub matches | ||
| # concurrency names case-insensitively, but with the two halves disagreeing about | ||
| # cancel-in-progress (true here, false there). A literal keeps this group's meaning | ||
| # fixed no matter who calls the workflow. Do not "restore" the expression. | ||
| group: ci-${{ github.ref }} | ||
| # Pull requests and pushes to main only care about the newest commit; superseded | ||
| # runs are wasted minutes. Tagged releases are serialised by release.yml's own | ||
| # group instead, which is where cancel-in-progress: false belongs. | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| boundary: | ||
| name: HTTP boundary intact | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | ||
| with: | ||
| go-version-file: go.mod | ||
| - run: ./scripts/check-module-boundary.sh | ||
|
|
||
| lint: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | ||
| with: | ||
| go-version-file: go.mod | ||
| - uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9 | ||
| with: | ||
| version: v2.11.4 | ||
|
|
||
| security: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | ||
| with: | ||
| go-version-file: go.mod | ||
| - run: go run golang.org/x/vuln/cmd/govulncheck@v1.3.0 ./... | ||
|
|
||
| test: | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, macos-latest, windows-latest] | ||
| runs-on: ${{ matrix.os }} | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| persist-credentials: false | ||
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | ||
| with: | ||
| go-version-file: go.mod | ||
| - run: go mod verify | ||
| - run: go vet ./... | ||
| - run: go test -race ./... | ||
| - run: go build ./cmd/ferro | ||
|
|
||
| integration: | ||
| name: Contract vs AI Gateway ${{ matrix.gateway_ref }} | ||
| runs-on: ubuntu-latest | ||
| # The pinned v1.4.2 leg is the required signal; "main" moves upstream of | ||
| # this repo and release.yml depends on ci via `needs: ci`, so a failure | ||
| # there must not block merges or tagged releases — only report it. | ||
| continue-on-error: ${{ matrix.gateway_ref == 'main' }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| gateway_ref: ["v1.4.2", "main"] | ||
| steps: | ||
| - name: Check out gateway-cli | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| path: gateway-cli | ||
| persist-credentials: false | ||
| - name: Check out AI Gateway | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| repository: ferro-labs/ai-gateway | ||
| ref: ${{ matrix.gateway_ref }} | ||
| path: ai-gateway | ||
| persist-credentials: false | ||
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | ||
| with: | ||
| go-version-file: gateway-cli/go.mod | ||
| - run: ./scripts/with-gateway.sh | ||
| working-directory: gateway-cli | ||
| env: | ||
| FERRO_GATEWAY_SOURCE: ${{ github.workspace }}/ai-gateway |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| push: | ||
| tags: ["v*"] | ||
|
|
||
| concurrency: | ||
| group: release-${{ github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| ci: | ||
| permissions: | ||
| contents: read | ||
| uses: ./.github/workflows/ci.yml | ||
|
|
||
| release: | ||
| name: GoReleaser | ||
| needs: ci | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| # GoReleaser publishes the GitHub release and uploads archives. | ||
| contents: write | ||
| steps: | ||
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | ||
| with: | ||
| go-version-file: go.mod | ||
| # A restored cache can carry entries from other refs; this job | ||
| # publishes release artifacts, so a poisoned cache is user-facing. | ||
| cache: false | ||
| # .goreleaser.yaml's sboms block shells out to syft, which is not present on | ||
| # GitHub-hosted runners; without this step the first real tag fails at the | ||
| # SBOM stage, after the binaries are already built. | ||
| - uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | ||
| - uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 | ||
| with: | ||
| distribution: goreleaser | ||
| version: "2.17.1" | ||
| args: release --clean | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,3 +30,8 @@ go.work.sum | |
| # Editor/IDE | ||
| # .idea/ | ||
| # .vscode/ | ||
|
|
||
| # Local builds | ||
| /ferro | ||
| /fakegw | ||
| /dist/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| version: "2" | ||
|
|
||
| run: | ||
| timeout: 5m | ||
|
|
||
| linters: | ||
| enable: | ||
| - bodyclose | ||
| - copyloopvar | ||
| - depguard | ||
| - dupl | ||
| - errcheck | ||
| - errorlint | ||
| - goconst | ||
| - gocritic | ||
| - gocyclo | ||
| - gosec | ||
| - govet | ||
| - ineffassign | ||
| - maintidx | ||
| - misspell | ||
| - noctx | ||
| - nolintlint | ||
| - prealloc | ||
| - revive | ||
| - staticcheck | ||
| - unconvert | ||
| - unparam | ||
| - unused | ||
|
|
||
| settings: | ||
| depguard: | ||
| rules: | ||
| main: | ||
| deny: | ||
| - pkg: gopkg.in/yaml.v3 | ||
| desc: use go.yaml.in/yaml/v3 | ||
| # depguard prefix-matches, so the module root also denies every | ||
| # current and future subpackage. | ||
| - pkg: github.com/ferro-labs/ai-gateway | ||
| desc: gateway-cli communicates with AI Gateway over HTTP only | ||
| dupl: | ||
| threshold: 150 | ||
| gocyclo: | ||
| min-complexity: 30 | ||
| nolintlint: | ||
| require-explanation: true | ||
| require-specific: true | ||
| allow-unused: false | ||
|
|
||
| exclusions: | ||
| rules: | ||
| - path: _test\.go | ||
| linters: [dupl, errorlint, goconst, unparam] | ||
| - path: _test\.go | ||
| linters: [staticcheck] | ||
| text: SA5011 | ||
| - path: internal/command/output\.go | ||
| linters: [gosec] | ||
| text: G115 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # GoReleaser for the standalone ferro operator console. | ||
| version: 2 | ||
| project_name: ferro | ||
| dist: dist | ||
|
|
||
| builds: | ||
| - id: ferro | ||
| main: ./cmd/ferro | ||
| binary: ferro | ||
| env: [CGO_ENABLED=0] | ||
| goos: [linux, darwin, windows] | ||
| goarch: [amd64, arm64] | ||
| # Reproducibility: rebuilding a tag must yield the same bytes, or a user has | ||
| # no way to check that a published binary was built from the source it names. | ||
| # mod_timestamp pins the file mtimes goreleaser stamps into archives, and | ||
| # .CommitDate replaces the build wall clock below -- both are needed, since | ||
| # either one alone still leaves the build time baked in. | ||
| mod_timestamp: "{{ .CommitTimestamp }}" | ||
| # -trimpath is the third leg: without it the binary embeds the absolute build | ||
| # directory, so a rebuild anywhere but the CI runner's own path differs even | ||
| # with the timestamps pinned -- and the published binary leaks that path. | ||
| flags: [-trimpath] | ||
| ldflags: | ||
| - -s -w | ||
| - -X github.com/ferro-labs/gateway-cli/internal/version.Version={{.Version}} | ||
| - -X github.com/ferro-labs/gateway-cli/internal/version.Commit={{.ShortCommit}} | ||
| - -X github.com/ferro-labs/gateway-cli/internal/version.Date={{.CommitDate}} | ||
|
|
||
| # cmd/fakegw is a development tool (a fake gateway for testing the CLI without | ||
| # a server). It is excluded by omission — builds are listed explicitly above. | ||
| # Do not add a build entry for it. | ||
|
|
||
| archives: | ||
| - formats: [tar.gz] | ||
| name_template: "ferro_{{ .Version }}_{{ .Os }}_{{ .Arch }}" | ||
| format_overrides: | ||
| - goos: windows | ||
| formats: [zip] | ||
| files: | ||
| - src: README.md | ||
| dst: README.md | ||
| - src: CHANGELOG.md | ||
| dst: CHANGELOG.md | ||
| - src: LICENSE | ||
| dst: LICENSE | ||
|
|
||
| checksum: | ||
| name_template: checksums.txt | ||
|
|
||
| # One SBOM per archive (syft's default, SPDX JSON), so a consumer can answer | ||
| # "is this build affected by CVE-x" without unpacking and re-scanning it. | ||
| # Requires syft on the runner -- release.yml installs it, and goreleaser fails | ||
| # the release rather than skipping the SBOM if it is missing. | ||
| sboms: | ||
| - artifacts: archive | ||
|
|
||
| release: | ||
| name_template: "Ferro CLI v{{ .Version }}" | ||
|
|
||
| changelog: | ||
| use: git | ||
| filters: | ||
| include: | ||
| - "^feat" | ||
| - "^fix" | ||
| - "^perf" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to **Ferro Operator Console**, the scriptable CLI and | ||
| interactive TUI for Ferro Labs AI Gateway. The binary it installs is `ferro`. | ||
|
|
||
| The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and | ||
| this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## v0.1.0 — 2026-08-14 | ||
|
|
||
| First release. One binary, `ferro`, with two surfaces over a single HTTP client: | ||
| scriptable commands for automation, and a full-screen console for operators. | ||
| Requires AI Gateway v1.4.2 or later. | ||
|
|
||
| ### Added | ||
|
|
||
| - **Commands** — `status`, `models`, `providers`, `mcp`, `plugins`, `services`, | ||
| `sessions`, `audit`, `keys list|get|create|rotate|revoke`, | ||
| `logs list|stats|tail`, `chat`, and `version`. Every one supports | ||
| `--format table|json|yaml`. | ||
| - **Console** — run bare `ferro` at a TTY for a full-screen operations view with | ||
| four screens: Home, Request logs, Keys, and Playground. Includes a command | ||
| composer with completion, history, and reverse search (`ctrl+r`). | ||
| - **Request-log tail** — follow live traffic in `logs tail` or the console, with | ||
| filters for time, model, provider, stage, and credential. | ||
| - **Key management** — list, create, rotate, and revoke API keys, with derived | ||
| `active` / `expired` / `revoked` state. New secrets are printed once, to | ||
| stdout, and never enter the console transcript or history. `rotate` and | ||
| `revoke` are irreversible, so at a terminal each asks for the key id typed | ||
| back before it runs; `--yes` skips the prompt, and is required when stdin is | ||
| not a terminal rather than the verb blocking or proceeding unasked. | ||
| - **Playground** — streaming chat against the gateway with `/model` and | ||
| `/clear`, showing token usage plus route and cost when available. | ||
| - **Connection profiles** at `os.UserConfigDir()/ferro/config.yaml`. URL | ||
| resolution: `--gateway-url` > `FERRO_URL` > profile > `http://localhost:8080`. | ||
| Key resolution: `FERRO_API_KEY` > profile `api_key_env` > `MASTER_KEY`, the | ||
| last of which applies only when the gateway URL is loopback. `MASTER_KEY` is | ||
| the gateway server's own variable and is not chosen for ferro by anyone, so | ||
| it is the one source that could otherwise be forwarded to a remote host the | ||
| operator named on the command line. Keys are never passed as flags, so they | ||
| stay out of shell history. | ||
| - **Global flags** `--gateway-url`, `--profile`, `--format`, `--ascii`, plus | ||
| `NO_COLOR`, TTY, and `TERM=dumb` detection. | ||
|
|
||
| ### Output contract | ||
|
|
||
| - stdout carries data; narration, warnings, and errors go to stderr — so | ||
| `--format json` is always safe to pipe. | ||
| - `status` exits 1 only when the gateway is unreachable. A reachable but | ||
| degraded gateway exits 0 and reports its degraded state. | ||
| - Missing measurements render `-`, never `0`. Endpoints the gateway does not | ||
| serve disable that panel with a hint instead of failing the command. | ||
| - Gateway-supplied text is data, never layout or terminal control. Control | ||
| characters in a provider name, an upstream error message, or a model's answer | ||
| are neutralized on every surface, so a table keeps its columns, a pane keeps | ||
| its line count, and nothing upstream can drive the terminal. | ||
|
|
||
| ### Distribution | ||
|
|
||
| - Release binaries are reproducible: `-trimpath`, and both the build stamp and | ||
| the archive timestamps come from the tagged commit rather than the build | ||
| clock, so two builds of one tag are byte-identical. | ||
| - An SPDX SBOM ships beside each archive. | ||
| - `go install` builds, which carry no linker stamp, now report their module | ||
| version and commit from the embedded build info instead of `dev`/`none`. | ||
|
|
||
| ### Known limitations | ||
|
|
||
| - The gateway does not report its version over HTTP, so the console's version | ||
| slot renders `—`. | ||
| - The traffic panel and route attribution come from the request log, and show as | ||
| unavailable when no request-log store is configured. | ||
| - Not yet shipped: config viewer / history / rollback, provider capability | ||
| matrix, doctor checklist, and init wizard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Contributing | ||
|
|
||
| Ferro Operator Console is a standalone Go module. Keep the runtime boundary | ||
| with AI Gateway HTTP-only; do not import packages from the gateway module. | ||
|
|
||
| Before opening a change, run: | ||
|
|
||
| ```bash | ||
| ./scripts/check-module-boundary.sh | ||
| go vet ./... | ||
| go test -race ./... | ||
| golangci-lint run ./... | ||
| ``` | ||
|
|
||
| For contract changes, run `FERRO_GATEWAY_SOURCE=/path/to/ai-gateway make itest`. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.