mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
TypeScript / Build (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
## What Adds a CRUD interface for **server-managed Environments** at `/settings/environments`, driven by the `/api/v1/environments` REST API (list / create / retrieve / replace / delete), and reshapes how built-in environments are provisioned and protected. The page lives in the **Workflows** settings nav section (also introduced in this branch), positioned before Variables. ## Why The Environments REST API shipped (#453) but had no UI — environments could only be managed via the API/CLI. This gives operators a web UI alongside Variables and Secrets, and along the way tightens the model: environments are seeded at install time (not silently re-created on every boot), and the `default` fallback is an ordinary, deletable environment. ## Web UI **Pages & component** - `settings-environments.tsx` — list view: provider badge, image/resource summary, row actions (Edit/Delete). **"New environment" is a dropdown** of the enabled sandbox providers; the chosen provider is fixed for the environment's lifetime. - `settings-environments-new.tsx` / `settings-environments-edit.tsx` — create/edit flows; create reads the provider from a query param. - `environment-form.tsx` — shared form, reorganized: - **General** panel (merged identity + image): id, and an **image-source selector** (Image reference *vs* inline Dockerfile) that shows, requires, and sends only the selected, mutually-exclusive source. - **Resources**: CPU / memory / disk as **range sliders** (CPU 1–8, memory 1–16 GB, disk 1–20 GB), each always writing a concrete value. - **Environment variables** key/value editor. - **Advanced** progressive-disclosure section holding **Network** (a single "Block all network access" toggle — allow-all vs block) and **Lifecycle** (preserve / stop-on-terminal / auto-stop). Opens by default when any advanced value is non-default. - The in-form **provider control and the Labels editor were removed** — labels remain API-managed and are round-tripped untouched so UI edits never clear them. **Data layer**: `environmentsApi` client, `queryKeys.environments`, `useEnvironments` / `useEnvironment` SWR hooks. **Nav & routing**: "Environments" item in the Workflows section before Variables; routes registered in `router.tsx`. ## Backend: seed at install, deletable `default` - **Seeding moved to install time.** The server no longer seeds built-ins on startup; `EnvironmentStore::load_or_seed` → `load` (load-only). A new public `seed_environments(dir)` (idempotent, preserves operator edits) is called by both the web installer and the CLI installer. An uninstalled instance therefore has no managed environments, and a run selecting an absent environment fails explicitly (`unknown environment: default`) rather than resurrecting a built-in. - **`default` is no longer protected.** The delete guard and the `Protected` error variant are gone; deleting `default` succeeds (204) and removes the run fallback on purpose — forcing an explicit choice. `local` is unchanged (reserved, in-memory). - **`volumes` removed** from environment settings across the OpenAPI spec, generated Rust + TS clients, config layers, sandbox/server/workflow plumbing, docs, and tests. ## API contract details honored - Edit sends the environment `revision` as `If-Match`; 409 conflicts surface a "changed since you opened it" message. - The REST API accepts inline Dockerfiles only — the form never sends a Dockerfile path. ## Verification - Rust: `cargo build` (touched crates) ✅, `cargo nextest -p fabro-environment` 21/21 ✅, server env unit + `tests/it` integration 2/2 + 15/15 ✅, `clippy` (nightly, touched crates, all targets) clean ✅, `fmt --check` clean ✅. Full `--workspace` suite not run here — worth a CI pass. - Web: `bun run typecheck` ✅, `bun run build` ✅, `environment-form.test.ts` 5/5 ✅. Web suite: 512 pass / 1 unrelated pre-existing `RunDetail` failure. - **Not visually verified in-browser** — the local app is login-gated and automated loads redirect to `/login`; rendering of the form, the New-environment dropdown, and `default` delete should be confirmed in a logged-in session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Co-authored-by: Fabro <noreply@fabro.sh> Co-authored-by: Release Repro <release-repro@example.com>
189 lines
7.4 KiB
Markdown
189 lines
7.4 KiB
Markdown
# Fabro Events Strategy
|
|
|
|
Fabro emits structured **workflow run events** during execution for observability. Events are the durable audit trail for a run: they drive the run store, SSE streaming, CLI progress rendering, and optional JSONL sinks.
|
|
|
|
Events are distinct from tracing logs. Tracing is developer diagnostics; events are product-facing state transitions and activity records that other systems consume.
|
|
|
|
Detached runs rely on this distinction. If something needs to be visible after reattach, emit a `Event` rather than only logging to stderr or `detach.log`.
|
|
|
|
## Architecture
|
|
|
|
```text
|
|
Engine/Handler -> Event -> Emitter::emit()
|
|
|- trace(raw event)
|
|
|- canonicalize -> RunEvent
|
|
`- on_event(&RunEvent)
|
|
|- run store
|
|
|- SSE
|
|
|- optional JSONL/debug sinks
|
|
`- CLI / tests / metrics listeners
|
|
```
|
|
|
|
The canonical `RunEvent` is built exactly once in the `fabro-workflow::event` module.
|
|
|
|
- `Event` (in `fabro-workflow`) is the internal typed event emitted by engine and handlers.
|
|
- `Emitter` owns an immutable `run_id` and converts `Event` into `RunEvent` via `to_run_event_at()`.
|
|
- `RunEvent` (in `fabro-types`) holds envelope metadata plus a typed `body: EventBody`. It has no cached JSON fields; the wire format is produced only during serialization.
|
|
- Every listener receives `&RunEvent`, not `&Event`.
|
|
- Bypass paths that cannot go through the emitter must call `to_run_event()` once and reuse the same `RunEvent` for every sink.
|
|
|
|
## Canonical Envelope
|
|
|
|
Each serialized `RunEvent` uses this canonical envelope:
|
|
|
|
```json
|
|
{
|
|
"id": "01960d0c-5d16-7d6e-8f61-9fd6f4a532b5",
|
|
"ts": "2026-03-30T12:00:01.000Z",
|
|
"run_id": "01JQ...",
|
|
"event": "agent.tool.started",
|
|
"session_id": "ses_child",
|
|
"parent_session_id": "ses_parent",
|
|
"node_id": "code",
|
|
"node_label": "Code",
|
|
"actor": {
|
|
"kind": "agent",
|
|
"session_id": "ses_child",
|
|
"parent_session_id": "ses_parent",
|
|
"model": "gpt-5.2"
|
|
},
|
|
"properties": {
|
|
"tool_name": "read_file",
|
|
"tool_call_id": "call_1",
|
|
"arguments": {"path": "src/main.rs"}
|
|
}
|
|
}
|
|
```
|
|
|
|
Always-present fields:
|
|
|
|
| Field | Type | Notes |
|
|
|---|---|---|
|
|
| `id` | string | UUIDv7 event id |
|
|
| `ts` | string | UTC timestamp with millisecond precision |
|
|
| `run_id` | string | Workflow run id |
|
|
| `event` | string | Lowercase dot-notation event name |
|
|
|
|
Optional top-level fields:
|
|
|
|
| Field | When present |
|
|
|---|---|
|
|
| `session_id` | Agent/session events |
|
|
| `parent_session_id` | Forwarded child-session events |
|
|
| `node_id` | Events tied to a graph node or branch |
|
|
| `node_label` | Display label for `node_id`; omitted when not applicable |
|
|
| `actor` | The principal responsible for the event |
|
|
|
|
Everything else lives inside `properties`.
|
|
|
|
Important rules:
|
|
|
|
- Optional envelope fields are omitted, not serialized as `null`.
|
|
- Event-specific fields do not get flattened into the top level.
|
|
- Actor identity normally lives only in top-level `actor: Principal`; do not duplicate it in
|
|
event-specific properties. The exception is `run.created`, whose
|
|
`properties.provenance.subject` is the durable run creator stored in `RunSpec`; its envelope
|
|
`actor` is derived from the same principal.
|
|
- User actors must carry canonical IdP identity through `Principal::User { identity, login, auth_method }`, not a login-only string.
|
|
- `EventPayload` validation requires `id`, `ts`, `run_id`, and `event`.
|
|
|
|
## Naming
|
|
|
|
The external event name is lowercase dot notation, for example:
|
|
|
|
- `run.started`
|
|
- `stage.completed`
|
|
- `agent.tool.started`
|
|
- `sandbox.ready`
|
|
- `parallel.branch.completed`
|
|
|
|
`event_name()` in the `fabro-workflow::event` module is exhaustive. Do not use wildcard fallthroughs when adding new variants.
|
|
|
|
## Node And Session Metadata
|
|
|
|
`node_id` is the stable graph identifier. `node_label` is the human-facing display name. Stage events should surface both through the envelope when applicable.
|
|
|
|
Agent events now use explicit session links:
|
|
|
|
- `session_id` identifies the session that originally emitted the event.
|
|
- `parent_session_id` identifies the immediate parent session for forwarded child events.
|
|
- Nested sub-agents preserve immediate parentage across boundaries.
|
|
|
|
`AgentEvent::SubAgentEvent` no longer exists. Child activity is forwarded as normal agent events with session linkage in the envelope.
|
|
|
|
## Direct-Write Paths
|
|
|
|
Most events flow through `Emitter::emit()`. The remaining direct-write paths must use:
|
|
|
|
1. `to_run_event(run_id, event)`
|
|
2. Serialize and redact once
|
|
3. Reuse that exact `RunEvent` for every sink
|
|
|
|
Never build the same `RunEvent` twice if multiple sinks receive it.
|
|
|
|
## Adding A New Event
|
|
|
|
### 1. Add the typed event
|
|
|
|
Add a variant to `Event`, `AgentEvent`, or `SandboxEvent` as appropriate.
|
|
|
|
### 2. Add tracing
|
|
|
|
Extend `Event::trace()` so the raw event is observable in tracing output.
|
|
|
|
### 3. Add an external name
|
|
|
|
Extend `event_name()` with the new lowercase dot-notation string.
|
|
|
|
### 4. Add the `EventBody` variant
|
|
|
|
Add a variant to `EventBody` in `fabro-types/src/run_event/mod.rs` with a corresponding props struct. Use `#[serde(rename = "dotted.name")]` matching the external name from step 3.
|
|
|
|
### 5. Map envelope fields and construct `EventBody`
|
|
|
|
Update `stored_event_fields()` and `event_body_from_event()` in the `fabro-workflow::event` module:
|
|
|
|
- Move `node_id`, `node_label`, `session_id`, and `parent_session_id` into the envelope when appropriate.
|
|
- Construct the `EventBody` variant directly from the `Event` fields.
|
|
- For `Event::Agent` sub-variants, merge `visit` into the inner props and lift `stage` to `node_id`.
|
|
- For `Event::Sandbox` sub-variants, unwrap and flatten into the corresponding `EventBody` variant.
|
|
|
|
### 6. Emit it
|
|
|
|
Prefer `Emitter::emit(&Event::...)`.
|
|
|
|
Use `to_run_event()` only for true bypass paths.
|
|
|
|
For cache-backed lifecycle work, emit slow-path start events only when the operation actually misses cache or waits on remote state. Completion events should represent a real ensure step (inspect, build, pull, or poll), not a configured no-op.
|
|
|
|
### 7. Update consumers
|
|
|
|
Check:
|
|
|
|
- CLI progress parsing
|
|
- `fabro events`
|
|
- store validation
|
|
- tests or fixtures that inspect event names or fields
|
|
|
|
## Consumer Guidance
|
|
|
|
When writing Rust consumers (listeners, store projections, CLI progress):
|
|
|
|
- Match on `event.body` using `EventBody::*` variants. This gives you typed access to event-specific fields.
|
|
- Use `event.node_id`, `event.node_label`, `event.session_id`, and `event.parent_session_id` for envelope metadata.
|
|
- Only use `event.event_name()` or `event.properties()` for generic/display purposes (logging, forwarding). These involve serialization and should not be used on hot paths.
|
|
|
|
When writing external JSON consumers (SSE clients, JSONL parsers):
|
|
|
|
- Match on the `"event"` field for the dot-notation event name.
|
|
- Read event-specific data from `"properties"`.
|
|
- Read stage/branch identity from `"node_id"` and `"node_label"`.
|
|
- Read agent hierarchy from `"session_id"` and `"parent_session_id"`.
|
|
|
|
Do not rebuild or mutate the `RunEvent` in downstream listeners.
|
|
|
|
## Bypass And Persistence Guarantees
|
|
|
|
Any JSONL sink, the run store, and SSE should reflect the same canonical envelope bytes after redaction.
|
|
|
|
`status.json` remains the authoritative completion signal for detached runs. Terminal run status should only be written after all post-run work is finished.
|