Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 97 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ name: Release
# Postgres so a broken build can't ship, then cut the GitHub Release with
# cross-compiled CLI binaries and move the `latest` image tag.
#
# The image set is runtime, dashboard, customer-app and the bundled
# supportdesk-agent — the last one is what `af-stack init` wires into a
# scaffolded app's own docker-compose.yml.
# The smoke job also scaffolds an app with the freshly built CLI and runs
# `npm start` in it, proving the published images boot a bundled backend the
# app can actually talk to before anything is tagged.
#
# Manual runs are supported via workflow_dispatch (e.g. to re-cut after a
# transient failure). Use dry_run to compute the version without publishing.
on:
Expand Down Expand Up @@ -112,10 +119,18 @@ jobs:
include:
- name: runtime
dockerfile: services/runtime/Dockerfile
context: .
- name: dashboard
dockerfile: apps/dashboard/Dockerfile
context: .
- name: customer-app
dockerfile: apps/customer-app/Dockerfile
context: .
# The agent image's Dockerfile does `COPY requirements.txt ./`, so it
# needs its own directory as the build context, not the repo root.
- name: supportdesk-agent
dockerfile: apps/backend/agents/supportdesk/Dockerfile
context: apps/backend/agents/supportdesk
steps:
- uses: actions/checkout@v7
with:
Expand All @@ -129,7 +144,7 @@ jobs:
- name: Build and push ${{ matrix.name }}
uses: docker/build-push-action@v6
with:
context: .
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: true
tags: ghcr.io/agent-field/af-stack-${{ matrix.name }}:${{ needs.prepare.outputs.version }}
Expand Down Expand Up @@ -175,6 +190,86 @@ jobs:
exit 1
fi
curl -fsS http://localhost:8080/health >/dev/null && echo "health OK"
# Free host port 8080 before the scaffold smoke. `af-stack dev` would
# auto-allocate around a busy port, but the scaffolded app is supposed to
# come up on the default one — so prove that path, don't fall back to it.
- name: Tear down the runtime smoke
run: docker compose -f docker-compose.release-smoke.yml down -v || true

# Second smoke: the scaffolded-app path, end to end, against the images
# this release just pushed. `af-stack init` writes a docker-compose.yml
# pinned to ${{ needs.prepare.outputs.version }}, `npm start` runs
# `af-stack dev` via its prestart hook and then calls supportdesk.echo.
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Put the CLI build dir on PATH
# `npm start`'s prestart hook shells out to `af-stack` by name.
run: echo /tmp >> "$GITHUB_PATH"
- name: Build the CLI pinned to the release version
env:
AF_STACK_VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
go build -ldflags "-X main.version=${AF_STACK_VERSION}" \
-o /tmp/af-stack ./services/cli/cmd/af-stack
/tmp/af-stack version || true
- name: Assert the release images are publicly pullable
# `af-stack init` apps pull these with NO registry login, the way any
# user does. GHCR creates packages private by default, so a release
# whose images are private ships a CLI whose scaffolds cannot boot.
# Fix: github.com/orgs/Agent-Field/packages → the package → Package
# settings → Change visibility → Public, then re-run this job.
env:
AF_STACK_VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
private=""
for svc in runtime dashboard customer-app supportdesk-agent; do
token="$(curl -fsS "https://ghcr.io/token?scope=repository:agent-field/af-stack-$svc:pull" | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')"
code="$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $token" -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json' "https://ghcr.io/v2/agent-field/af-stack-$svc/manifests/$AF_STACK_VERSION")"
echo "ghcr.io/agent-field/af-stack-$svc:$AF_STACK_VERSION anonymous pull: HTTP $code"
[ "$code" = "200" ] || private="$private af-stack-$svc"
done
if [ -n "$private" ]; then
echo "::error::these GHCR packages are not publicly pullable:$private — make each one Public under the org's package settings and re-run this job; until then apps from \`af-stack init\` cannot boot their bundled backend"
exit 1
fi
- name: Log out of GHCR so the scaffold pulls anonymously, like a user
run: docker logout ghcr.io || true
- name: Scaffold a standalone app
run: |
set -euo pipefail
rm -rf /tmp/smoke-app
/tmp/af-stack init smoke-app --dir /tmp
echo "--- generated docker-compose.yml ---"
cat /tmp/smoke-app/docker-compose.yml
- name: npm start — scaffolded app boots its bundled backend
# The first `docker compose up -d` pulls ~6 images.
timeout-minutes: 15
run: |
set -euo pipefail
export PATH=/tmp:$PATH
cd /tmp/smoke-app
# No dependencies — this just validates the generated package.json.
npm install
status=0
npm start 2>&1 | tee /tmp/smoke-start.log || status=$?
if [ "$status" -ne 0 ] || ! grep -q '^Echo agent replied:' /tmp/smoke-start.log; then
echo "::error::the scaffolded app could not talk to its bundled backend — refusing to publish the release"
for svc in runtime agentfield supportdesk-agent; do
echo "--- docker compose logs $svc ---"
docker compose logs --tail 80 "$svc" || true
done
exit 1
fi
echo "scaffolded app talked to its bundled backend OK"
- name: Tear down the scaffolded app
if: always()
run: docker compose -f /tmp/smoke-app/docker-compose.yml --project-directory /tmp/smoke-app down -v || true
- name: Tear down
if: always()
run: docker compose -f docker-compose.release-smoke.yml down -v || true
Expand Down Expand Up @@ -220,7 +315,7 @@ jobs:
if: needs.prepare.outputs.prerelease != 'true'
run: |
set -euo pipefail
for svc in runtime dashboard customer-app; do
for svc in runtime dashboard customer-app supportdesk-agent; do
docker buildx imagetools create \
--tag "ghcr.io/agent-field/af-stack-$svc:latest" \
"ghcr.io/agent-field/af-stack-$svc:${{ needs.prepare.outputs.version }}"
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ BackAI is currently in beta and under active development. Expect rapid improveme

## Quickstart

Prerequisite: Docker with Compose. Node 18+ is optional: `af-stack dev`
uses it to auto-allocate conflict-free ports and falls back to the
defaults without it.
Prerequisite: Docker with Compose. Node 18+ is optional in this checkout:
`af-stack dev` uses it to auto-allocate conflict-free ports and falls back
to the defaults without it.

```bash
git clone https://github.com/Agent-Field/backai.git
Expand Down Expand Up @@ -139,8 +139,9 @@ approvals, sandbox limits, and audit records keep build and live operations
inside explicit boundaries.

```bash
# A standalone app that calls a running BackAI. Works in any directory.
af-stack init my-ai-product
# A complete app with its own bundled backend. Works in any directory; needs Docker.
af-stack init my-ai-product && cd my-ai-product
npm install && npm start # boots the backend, then talks to it

# Or brand a full fork and hand it to your coding agent. These run inside
# a clone of this repo; that clone is where the four surfaces below live.
Expand All @@ -150,6 +151,11 @@ af-stack init --name "Acme AI" --color "#2563EB"
af-stack agent new researcher
```

The first path needs no clone: the scaffold carries a `docker-compose.yml`
that boots Postgres, the LLM gateway, the runtime, the operator dashboard,
and a demo agent from the published release images, pinned to the CLI's
version.

Inside a fork, builders and agents usually edit only four surfaces:

| Surface | Path | Purpose |
Expand Down
4 changes: 4 additions & 0 deletions docs/cli-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ Postgres directly need:
Requests go to `${AF_STACK_URL}/api/v1<endpoint>` with
`Authorization: Bearer ${AF_STACK_API_KEY}`.

In an app scaffolded by `af-stack init <name>`, `af-stack dev` writes the
resolved `AF_STACK_URL` into that app's `.env` — read it from there when the
default port was busy.

### Minting an operator key

Operator keys are minted directly against the database, so **both** bootstrap
Expand Down
11 changes: 10 additions & 1 deletion docs/cli-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ docker compose up
Four commands. Works on any machine with git + docker. Browser opens
dashboard, the dev is "in."

With the CLI installed there is a no-clone route to the same backend:
`af-stack init <name>` scaffolds an app that carries its own
`docker-compose.yml`, and `npm start` boots it from the published release
images.

### Install CLI for power features

One line — the install script from
Expand Down Expand Up @@ -106,10 +111,14 @@ Every command below exists in the current binary (see
[`services/cli/cmd/af-stack/main.go`](../services/cli/cmd/af-stack/main.go)).

```bash
# Standalone app + its own bundled backend (any directory, no clone)
af-stack init my-ai-product # app + docker-compose.yml + backend/
cd my-ai-product && npm install && npm start # boots the backend, then the app

# Fork bootstrap + dev loop (run inside a clone of this repo)
af-stack init --name "DocuChat" --color "#0A66C2"
# optional: --logo ./your-logo.svg sets the light+dark mark in brand.yaml
af-stack dev --detach
af-stack dev --detach # in a scaffolded app, af-stack dev boots its bundled backend
af-stack mode personal|saas # auth+billing off ⇄ multi-tenant SaaS
af-stack upgrade [--check] # pull latest upstream into this fork

Expand Down
6 changes: 4 additions & 2 deletions docs/dx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ af-stack deploy helm # ship it (helm | fly | railway | render)

Four commands, one loop: **init → dev → edit → deploy**, all inside the
clone. `af-stack init <name>` with a positional name is a different thing:
it scaffolds a small standalone app that *calls* a running BackAI, in any
directory, and has no surfaces to brand or extend. See
it scaffolds an app that *carries its own backend* — a `docker-compose.yml`
and a `backend/` directory pinned to the CLI's version — in any directory,
with no clone and no surfaces to brand or extend. `af-stack dev` inside that
app boots the backend from the published release images. See
[run.md](run.md) for what `af-stack dev` actually brings up and
[build-app.md](build-app.md) for the surfaces you edit.

Expand Down
49 changes: 45 additions & 4 deletions docs/dx/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ af-stack dev
```

From inside the clone, that's the whole thing — see the
[golden path](README.md) for the `git clone` line. Run it anywhere else and
it exits 1 with `must run from inside a BackAI checkout — a clone of
https://github.com/Agent-Field/backai …`, followed by the clone command and
the standalone-app alternative. `af-stack dev`:
[golden path](README.md) for the `git clone` line. `af-stack dev`:

1. Runs a **port preflight** (`scripts/preflight.mjs --fix`) — finds a
free host port for each service, writes the overrides into `.env`, and
Expand All @@ -25,6 +22,11 @@ the standalone-app alternative. `af-stack dev`:
Prefer raw compose? `docker compose up` works too — but then you own port
conflicts yourself.

`af-stack dev` also runs inside an app scaffolded by
`af-stack init <name>` — that app brings its own backend, so no clone is
involved. See [In a scaffolded app](#in-a-scaffolded-app) below. Anywhere
else (no checkout, no scaffold) it exits 1 and prints both ways in.

## Local URLs

After `af-stack dev`, the default endpoint map:
Expand All @@ -45,6 +47,45 @@ next free one and records the override in `.env` (e.g.
`AGENTFIELD_PORT`, `MINIO_CONSOLE_PORT`) — so read `.env` / the printed map
if a URL above doesn't respond.

## In a scaffolded app

`af-stack init <name>` writes an app that carries its own backend: a
`docker-compose.yml` plus a `backend/` directory
(`backend/postgres-init.sh`, `backend/litellm-config.yaml`). The compose
file pulls the published BackAI release images, pinned to the version of
the CLI that scaffolded it — Postgres (pgvector), MinIO, LiteLLM, the
AgentField control plane, the runtime, the operator dashboard, and the
`supportdesk` demo agent with its no-key `echo` reasoner. Docker with
Compose is the only prerequisite (plus Node 18+ for the app itself).

Run `af-stack dev` from inside that app and it:

1. Allocates conflict-free host ports, writing `AF_STACK_PORT`,
`AGENTFIELD_PORT`, `POSTGRES_PORT` and friends into the app's `.env`
when the defaults (8080 / 8081 / 5432 / …) are busy.
2. Runs `docker compose up -d` and waits for the runtime's `/ready`.
3. Writes `AF_STACK_URL=http://localhost:<port>` into `.env` and prints the
URLs — API runtime, operator dashboard
(`http://localhost:33000`, `operator@af-stack.local` / `changeme123`),
AgentField UI.

It is **detached**: it returns once the backend is ready. The first run
pulls the images, so give it a minute.

The scaffold's `package.json` wires that up for you:

| Command | Does |
| --- | --- |
| `npm start` | `prestart` runs `af-stack dev` (a no-op when the backend is already up), then the app lists the registered agents and calls `supportdesk.echo` |
| `npm run backend` | `af-stack dev` on its own |
| `npm run backend:stop` | `docker compose down` (add `-v` to drop the data volumes too) |

`af-stack init <name> --template saas` (a Vite/React starter) gets the same
bundled backend; there `npm run dev` boots it via a `predev` hook.

There is no customer app in the scaffolded backend — the app you scaffolded
is the customer app.

## `.env`

`af-stack dev` reads and writes `.env`. Start from `.env.example`. Preflight
Expand Down
2 changes: 2 additions & 0 deletions services/cli/cmd/af-stack/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import (

"github.com/Agent-Field/backai/services/cli/internal/admincmd"
"github.com/Agent-Field/backai/services/cli/internal/billingcmd"
"github.com/Agent-Field/backai/services/cli/internal/buildinfo"
"github.com/Agent-Field/backai/services/cli/internal/client"
"github.com/Agent-Field/backai/services/cli/internal/conncmd"
"github.com/Agent-Field/backai/services/cli/internal/dbcmd"
Expand All @@ -86,6 +87,7 @@ func main() {
// The global --no-telemetry flag may appear anywhere; strip it before
// dispatch so subcommand flag parsers never see it.
optOut, args := extractNoTelemetry(os.Args[1:])
buildinfo.Version = version

tel := telemetry.New(version, optOut, os.Stderr)
cmdName := "help"
Expand Down
10 changes: 10 additions & 0 deletions services/cli/internal/buildinfo/buildinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0

// Package buildinfo exposes the CLI's own version to packages that need to
// pin published artifacts (release images) to the binary that wrote them.
// main sets Version from its -ldflags "-X main.version=..." value at startup.
package buildinfo

// Version is the CLI version, or a development placeholder when the binary
// was built without the release linker flag.
var Version = "0.0.1"
13 changes: 8 additions & 5 deletions services/cli/internal/checkout/checkout.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Package checkout locates the BackAI checkout — a clone of the repository —
// that in-tree commands (`init --name`, `dev`, `agent|module|plugin new`,
// `deploy`) operate on, and explains clearly when there is none.
// that in-tree commands (`init --name`, `agent|module|plugin new`, `deploy`,
// and `dev` when there is one) operate on, and explains clearly when there
// is none.
//
// The most common way to end up outside a checkout is to scaffold a
// standalone app with `af-stack init <name>`, cd into it, and then run a
// fork command there. That directory calls a running BackAI; it has no
// apps/ tree to brand or add agents to. The error says so.
// fork command there. That directory has no apps/ tree to brand or add
// agents to; the error says so. (`dev` is the exception: the app carries a
// bundled backend, and project.RunDev runs it when NotFoundError names a
// ScaffoldedApp.)
package checkout

import (
Expand All @@ -31,7 +34,7 @@ func (e *NotFoundError) Error() string {
var b strings.Builder
fmt.Fprintf(&b, "must run from inside a BackAI checkout — a clone of %s (a directory containing apps/dashboard and apps/customer-app); %s is not one.", RepoURL, e.Dir)
if e.ScaffoldedApp != "" {
fmt.Fprintf(&b, "\n %s is a standalone app created by `af-stack init <name>`: it calls a running BackAI and has no fork surfaces to brand or extend.", e.ScaffoldedApp)
fmt.Fprintf(&b, "\n %s is a standalone app created by `af-stack init <name>`: it has a bundled backend (`af-stack dev` works there) but no fork surfaces to brand or extend.", e.ScaffoldedApp)
}
fmt.Fprintf(&b, "\n To brand a fork or add agents, modules, or plugins: git clone %s my-fork && cd my-fork", RepoURL)
fmt.Fprintf(&b, "\n To scaffold a standalone app instead: af-stack init <name> (works in any directory)")
Expand Down
Loading
Loading