Skip to content
Open
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
4 changes: 2 additions & 2 deletions documentation/docs/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ id: overview
slug: /
---

import { CardSection, Card } from "../src/components/Cards";
import Link from "@docusaurus/Link";
import TSLogo from "../static/img/ts-logo.svg";
import { Card, CardSection } from "../src/components/Cards";
import PythonLogo from "../static/img/python-logo.svg";
import TSLogo from "../static/img/ts-logo.svg";

# Build Apps with Reboot

Expand Down
44 changes: 20 additions & 24 deletions reboot/plugin/skills/chat-app/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ README for the manual install, team auto-enable, and the Codex
- Adding features, state, or UI to an existing Reboot AI Chat App
- Modifying state model, methods, or React UI in a Reboot AI Chat App
- Running an existing Reboot AI Chat App — e.g. at the start of a
new session. This needs no Plan or Build phase: load the
new session. This needs no design or build phase: load the
[`run` skill](../run/SKILL.md), which detects the app type,
starts the backend and frontend, and opens the setup wizard (from
which the user can launch MCPJam on demand).
Expand Down Expand Up @@ -155,19 +155,17 @@ they cover aren't restated inline below.
| [`references/auth-custom-oauth-provider.md`](references/auth-custom-oauth-provider.md) | **Writing your own OAuth provider** when none of the shipped ones fits (self-hosted Keycloak, internal SSO, an IdP Auth0 can't broker): subclass `RegisteredOAuthProvider`, implement `authorization_url` + `exchange_code` → `ExchangeResult` (the user id must be **stable** — it becomes `context.auth.user_id`), the `validate()` / `mount_routes()` hooks, and `token_service_id` + `OAuthTokens` for `store_tokens=True` support. |
| [`references/auth-store-tokens.md`](references/auth-store-tokens.md) | **Acting on the user's behalf** at an external service — the built-in `store_tokens=True` shortcut (works on any `oauth=` surface, web apps included): when the API belongs to your `Application(oauth=...)` identity provider, add extra OAuth `scopes=[...]` + `store_tokens=True` (needs `oauth_library()` + `ciphertext_library()` + `ordered_map_library()`) and the server captures its tokens. Reboot stores **the provider's own tokens only** (an `Auth0` sign-in stores an Auth0 token, not the upstream Google token). The full host-agnostic recipe — custom OAuth endpoints for any _other_ service, reading tokens back, calling the API **inside a `Workflow`**, refresh tokens, erasure — is [`python/references/auth-external-api-calls.md`](../python/references/auth-external-api-calls.md). |

## Workflow: Plan First, Then Build
## Workflow: Settle the Design, Then Build

**Always plan the design and get approval before writing code.** The
state model is the foundation — getting entities, field types, or
method types wrong means regenerating everything across 12+ files.
**Always settle the design before writing code.** The state model
is the foundation — getting entities, field types, or method types
wrong means regenerating everything across 12+ files.

### Plan Phase
### Design Phase

1. Analyze the user's description using the State Model Assessment
below.
2. Begin a plan for the user to approve (in Claude Code, enter plan
mode; in Codex, present the plan and wait for the go-ahead).
3. Present the proposed design:
2. State the design you are about to build:
- `User` type and its methods (the MCP front door for creating new
application-type instances and locating existing ones).
- Application types: state shape (fields, types, tags).
Expand All @@ -179,20 +177,19 @@ method types wrong means regenerating everything across 12+ files.
main user story — and most of them ending on a turn that
renders a `UI()` component, not just a tool call (see
"Example Prompts" under Key Framework Concepts).
4. Get user approval before writing any files.
5. Then execute the Step-by-Step Build Flow.
3. Then execute the Step-by-Step Build Flow.

For updates to existing apps, still plan: read current state, propose
changes, confirm, then modify.
For updates to existing apps, still work the design first: read
current state, state the changes, then modify.

### Writing the Plan for Human Review
### Writing the Design for a Human Reader

The plan is read by a **human who has not read the skill files**.
They are evaluating the design — entities, collections, methods,
auth — not verifying that you followed the skill. Write so the
plan stands on its own.
The design is read by a **human who has not read the skill files**.
They are judging the design — entities, collections, methods,
auth — not verifying that you followed the skill. Write so it
stands on its own.

**Don't quote skill-internal terms** when presenting the plan.
**Don't quote skill-internal terms** when presenting the design.
They mean nothing outside this skill:

- `Shape A` / `Shape B` / `Shape C` — name the actual data
Expand Down Expand Up @@ -240,7 +237,7 @@ Nested model reasoning — GOOD:
> own state actors because they have no lifecycle, methods, or
> auth independent of the Person they belong to.

**Escape hatch.** When the precise type name _is_ what the user
**Escape hatch.** When the precise type name _is_ what the reader
needs to see ("I'm proposing `OrderedMap` here, not `list[str]`"),
name the type — but pair it with the plain-English reason in the
same sentence. The rule is "no bare jargon", not "no technical
Expand Down Expand Up @@ -545,7 +542,7 @@ triggers a `UI()` method — phrase that turn as a natural "show me
example, the "…and show me the counter" / "show me the wins counter"
turns are exactly this: they resolve to the `Counter` UI and render the
live component, not just a text reply. When you write the set, look at
the method map from the plan: for each `UI()` method, make sure at
the method map from the design: for each `UI()` method, make sure at
least one example drives the user to it.

They live in `backend/src/example_prompts.py` and are passed to
Expand Down Expand Up @@ -595,8 +592,7 @@ a worked set are in

## Step-by-Step Build Flow

**Only execute after plan approval. All commands run from the
application directory.**
**All commands run from the application directory.**

1. Create `.python-version`, `pyproject.toml`, `.rbtrc`, and
`.mypy.ini` — see
Expand Down Expand Up @@ -629,7 +625,7 @@ application directory.**
11. `cd frontend && npm run build`.
12. **Write and run backend unit tests covering each user-facing
user story before handing the app off.** Enumerate the user
stories from the plan — every action the user should be able
stories from the design — every action the user should be able
to _do_ through the MCP tool surface (e.g. "create a new
todo list", "add an item and see it listed", "rename a
list"). Write one test method per user story in
Expand Down
28 changes: 14 additions & 14 deletions reboot/plugin/skills/chat-app/references/react-app-tsx.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,29 @@ tags: react, app-tsx, hooks, useType, css-module, snake-camel, app-tsx-example,

## React App.tsx — Generated Hooks and Component Patterns

`frontend/mcp/<ui-name>/App.tsx` is the React component for one UI. The
generated `use<Type>()` hook returns reader subscriptions and
mutation functions:
`frontend/mcp/<ui-name>/App.tsx` is the React component for one UI.

> **The generated client contract is shared with web apps** — the
> `use<Type>()` overloads, the `Use<Type>Api` surface, the three
> fields a reader returns, why mutations resolve to
> `{ response, aborted }` instead of throwing, and the typed error
> classes are all in
> [`python/references/react-generated-client.md`](../../python/references/react-generated-client.md).
> Read that for the shapes; this file covers what is specific to a
> UI rendered inside an MCP host.

```tsx
import { useCounter } from "@api/<pkg>/v1/<name>_rbt_react";

// useCounter() connects to the Counter state instance.
const counter = useCounter();

// Reader (WebSocket subscription, auto-updates):
const { response, isLoading } = counter.useGet();
// Reader (a live subscription — pushed on every change):
const { response } = counter.useGet();
const value = response?.value ?? 0;

// Writer (direct call to Reboot backend):
await counter.increment({ amount: 1 });
// Mutation (resolves to `{ response, aborted }` — it never throws):
const { aborted } = await counter.increment({ amount: 1 });
```

For list state, the same pattern applies — use the generated hook for
Expand Down Expand Up @@ -71,13 +78,6 @@ entity of the same Type), `useMcpToolData()` from
`@reboot-dev/reboot-react` returns the raw `{ids, ...}` object
the framework received.

## Naming Convention: snake_case → camelCase

The generated React bindings convert Python snake_case field names
to TypeScript camelCase. Python `from_index` becomes TypeScript
`fromIndex`. Same for every snake_case field name on every Request
or Response Model.

## Generated React Imports

```tsx
Expand Down
6 changes: 6 additions & 0 deletions reboot/plugin/skills/python/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,12 @@ above lists the right ones grouped by task type. The full catalog:
- `references/testing-harness.md`
- `references/testing-external-context.md`

**Frontend** (shared by `web-app` and `chat-app`):

- `references/react-generated-client.md` — what
`rbt generate --react=` emits: hook overloads, `Use<Type>Api`,
reader/mutation return shapes, typed errors, naming rules.

**Patterns** (`patterns-`):

- `references/patterns-error-handling.md`
Expand Down
7 changes: 3 additions & 4 deletions reboot/plugin/skills/python/references/auth-allow-deny.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ A rule's `execute` returns one of three outcomes:

## One Authorizer per Servicer

`def authorizer(self)` returns a single rule that applies to every
method on the Servicer. Per-method differentiation (e.g. allow reads
but gate writes) requires a custom authorizer subclass — see
`auth-custom-predicates.md`.
One rule covers every method on the Servicer; see
`servicer-authorizer.md` for what to do when methods need
different rules.
109 changes: 106 additions & 3 deletions reboot/plugin/skills/python/references/auth-custom-predicates.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,101 @@ def authorizer(self):
return allow_if(all=[has_verified_token, is_team_member])
```

## Annotate Predicates, or `mypy` Fails

The examples above are unannotated for brevity, but a project that
runs `mypy` needs the annotations — without them `allow_if` reports
`Argument "any" ... has incompatible type "list[function]"; expected "Sequence[AuthorizerCallable[...]]"`, and a helper that returns a
rule reports `Need type annotation`. The shapes that type-check:

Annotate `state` with the **pydantic state model from your API
definition** — the same `<X>State` class you declared in
`api/<pkg>/v1/<name>.py`. That is what the runtime passes: in a
pydantic app a predicate receives `<pkg>.v1.<name>.TaskListState`.

> **Not** `<Type>Authorizer.StateType` / `.RequestTypes`. The
> generated `<Type>Authorizer` class does expose those aliases, but
> they name the **protobuf** types (`<name>_pb2.TaskList`), which is
> not what a pydantic app's predicate is handed. They type-check —
> protobuf and pydantic carry the same field names — while
> describing the wrong class, which is worse than `Any`.

```python
from typing import Any

import rbt.v1alpha1.errors_pb2 as errors
from reboot.aio.auth.authorizers import (
Authorizer,
AuthorizerRule,
allow_if,
is_app_internal,
)
from reboot.aio.contexts import ReaderContext
from <pkg>.v1.<name> import TaskListState


def is_owner_or_member(
*,
context: ReaderContext,
state: TaskListState | None = None,
**kwargs: Any,
) -> Authorizer.Decision:
if context.auth is None or context.auth.user_id is None:
return errors.Unauthenticated()
if state is not None and state.owner_id == context.auth.user_id:
return errors.Ok()
return errors.PermissionDenied()


# A helper that returns a rule needs its return type spelled out.
def owner_or_member() -> AuthorizerRule[TaskListState, Any]:
return allow_if(any=[is_owner_or_member, is_app_internal])
```

**Declare only the arguments the body reads.** The runtime passes
`context`, `state`, and `request` by keyword; a predicate that never
looks at `request` simply omits it and lets `**kwargs: Any` absorb
it. That is also why `**kwargs` is mandatory — it is what keeps the
predicate working when the runtime starts passing something new.

When the body _does_ read `request`, declare it. One rule covers
every method on the Servicer, so what arrives is a union of all of
their request models — which is common, because restricting the
internal-only methods means telling the methods apart:

```python
def task_list_access(
*,
context: ReaderContext,
state: TaskListState | None = None,
request: Any = None,
**kwargs: Any,
) -> Authorizer.Decision:
# Methods only app-internal code may call.
if isinstance(request, (CreateRequest, AcceptTaskRequest)):
return errors.Ok() if context.app_internal else errors.PermissionDenied()
...
```

Annotate it `Any` and narrow with `isinstance`, or spell out the
union of the request models it actually distinguishes.

**Check that the annotation is actually doing something.** These
types only bite if mypy can resolve the API package: `mypy_path`
must include the project-root `api/`, and the "don't check generated
code" stanza must name `<pkg>.v1.<name>_rbt` rather than
`<pkg>.v1.*` — a blanket ignore silences your own API module, and
then `TaskListState` is `Any` and a misspelled field passes a green
mypy run. See `lifecycle-project-setup.md`.

## Predicate Signatures

The runtime invokes a predicate with at minimum these keyword args:
`context` (always a `ReaderContext`), `state` (the actor state, possibly
`None`), and `request` (the request message, possibly `None`). Always
declare them as keyword-only and include `**kwargs` so signature
extensions don't break the predicate.
`None`), and `request` (the request message, possibly `None`). Declare
the ones the body reads, keyword-only, and always include `**kwargs`
so the arguments you didn't declare — and any the runtime adds later
— land there harmlessly.

## Order Predicates by Cost in `all`

Expand All @@ -85,6 +173,21 @@ return allow_if(all=[has_verified_token, expensive_team_check])
return allow_if(all=[expensive_team_check, has_verified_token])
```

## The Error Types a Predicate Returns

They come from `rbt.v1alpha1.errors_pb2`; a predicate only ever
returns `Ok`, `Unauthenticated`, or `PermissionDenied`. The module
also carries the framework errors a call can abort with, which is
what a typed error union widens to:

```
Ok Unauthenticated PermissionDenied NotFound AlreadyExists
InvalidArgument FailedPrecondition OutOfRange ResourceExhausted
DeadlineExceeded Cancelled Unavailable Unimplemented Internal
Unknown DataLoss Aborted StateNotConstructed
StateAlreadyConstructed UnknownService UnknownTask InvalidMethod
```

## Distinguish `PermissionDenied` from `Unauthenticated`

Return `Unauthenticated` when the call has no identity at all (or no
Expand Down
18 changes: 16 additions & 2 deletions reboot/plugin/skills/python/references/lifecycle-project-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ warn_unused_configs = True
# Find modules in our source tree (and tests). Since `protoc` doesn't
# generate `__init__.py` files, treat these as explicit package bases:
# https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules
mypy_path = backend/tests:backend/src:backend/api
mypy_path = backend/tests:backend/src:backend/api:api
explicit_package_bases = True

# Stricter than the default, but cheap to adhere to and high value.
Expand All @@ -157,11 +157,25 @@ ignore_missing_imports = True

# The generated `*_rbt.py` for your API package is not hand-written;
# don't type-check it (you never edit it anyway). Repeat per package.
[mypy-<pkg>.v1.*]
# Name the generated module specifically — a blanket `<pkg>.v1.*`
# would also silence your own `api/<pkg>/v1/<name>.py`, and with it
# every state and request model your code is annotated with.
[mypy-<pkg>.v1.<name>_rbt]
ignore_errors = True
ignore_missing_imports = True
```

**Both details in that file are load-bearing.** The project-root
`api/` entry in `mypy_path` is what lets mypy resolve the
hand-written pydantic API module, and the narrow ignore stanza is
what stops it being silenced again. Get either wrong and
`from <pkg>.v1.<name> import <X>State` quietly resolves to `Any`:
mypy still reports "Success", but every annotation mentioning a
state or request model checks nothing, and a misspelled field on
`state` sails through to a test failure. A quick way to confirm the
config is live: add a bogus attribute access on a state model and
check that mypy reports `has no attribute`.

## Always Type-Check What You Write

After writing or changing any Python in `backend/`, run mypy from the
Expand Down
Loading
Loading