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
2 changes: 2 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"group": "Building Apps",
"pages": [
"sdk/building-apps",
"sdk/deployment",
"sdk/accounts",
"sdk/focus-app",
"sdk/ambient-app",
"sdk/install-steps"
Expand Down
185 changes: 185 additions & 0 deletions sdk/accounts.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
---
title: Accounts and authentication
description: "Build apps with separate accounts, connection-scoped credentials, OAuth, and remote MCP connectors"
---

An installed app has one bundle identity and one or more account connections.
Shared code is installed once. Credentials, account identity, and permission
choices belong to each connection.

## Choose the authentication flow

| Flow | Installer setup | CLI interaction |
| --- | --- | --- |
| Public service | Files/dependencies and optional welcome step | Continue when prompted |
| API key, token, or configuration form | `text` step with `scope: connection` | Enter fields; password fields are hidden |
| Browser OAuth | Connection-scoped `oauth` step | Open the authorization URL and complete the callback/code prompt |
| Remote MCP OAuth | Native connector plus connection-scoped OAuth | Same browser flow; firmware handles discovery, registration, and token storage |
| Device-code approval | Provider-specific native setup plan | Open the approval link; CLI waits for completion |
| Browser/VNC setup | Provider-specific native setup plan | Open the device setup URL and complete the displayed steps |

The CLI drives the setup plan supplied by firmware. Provider endpoints, client
registration, scopes, and callback URLs must match the provider's requirements.
Supporting a setup card does not establish that every provider works with it.

## Separate installation from account setup

For a process-based app, use `truffile_version: 4` and `connections: multi` when
multiple accounts are supported. Install-scoped steps prepare shared code;
connection-scoped steps collect or validate credentials for the first account,
additional accounts, and reconnections.

```yaml
truffile_version: 4
connections: multi
# Include your metadata and foreground/background process configuration.
steps:
- name: Copy code
type: files
scope: install
files:
- source: service.py
destination: ./service.py
- name: Connect account
type: text
scope: connection
fields:
- name: api_key
label: API key
type: password
env: SERVICE_API_KEY
```

This is a setup fragment, not a complete app. Add a provider validation step
before reporting the account as connected. The selected connection's runtime
receives its own `SERVICE_API_KEY`; do not embed secrets in the manifest or
shared app files. For custom Python apps, expose provider-confirmed identity
through `truffle://user-info` so account labels are meaningful.

## A remote MCP connector with OAuth

A native connector uses `truffile_version: 5` and
`metadata.foreground.connector`. Firmware connects to the upstream MCP server;
you do not need to write a Python proxy for this example.

Create a directory containing `icon.png` and this `truffile.yaml`. Use your own
unique bundle ID if you adapt the example for distribution.

```yaml
truffile_version: 5
connections: multi
metadata:
name: Todoist Tutorial
bundle_id: org.example.todoist.tutorial
description: Connect Todoist accounts through its remote MCP service.
icon_file: icon.png
foreground:
connector:
upstream_url: https://ai.todoist.net/mcp
transport:
request_timeout_seconds: 45
max_read_retries: 1
retry_delay_seconds: 1
auth:
mode: oauth
app_var_key: todoist_oauth_state
token_endpoint: https://todoist.com/oauth/access_token
resource: https://ai.todoist.net/mcp
overlay: {}
identity:
sources:
- tool: user-info
map:
provider_subject: userId
email: email
display_name: fullName
steps:
- name: Sign in to Todoist
type: oauth
scope: connection
provider: remote_mcp
description: Authorize Todoist, then return to the terminal.
redirect_uri: http://localhost:18791/callback
scopes:
- data:read_write
dynamic_client_registration: true
resource_metadata_endpoint: https://ai.todoist.net/.well-known/oauth-protected-resource/mcp
authorization_server_metadata_endpoint: https://todoist.com/.well-known/oauth-authorization-server
registration_endpoint: https://todoist.com/oauth/register
oauth_resource: https://ai.todoist.net/mcp
client_name: Todoist Tutorial
auth_endpoint: https://todoist.com/oauth/authorize
token_endpoint: https://todoist.com/oauth/access_token
token_endpoint_auth_method: none
client_id_env: TODOIST_CLIENT_ID
client_secret_env: null
app_var_key: todoist_oauth_state
prompt: null
access_type: null
include_granted_scopes: false
alias_from:
- identity.email
- identity.display_name
```

This follows the Todoist flow verified with two separate accounts. The
connector and OAuth step use the same `app_var_key`, which identifies
connection-scoped token state. The `identity` mapping reads Todoist's
`user-info` tool; other providers need their own verified identity mapping.

```bash
truffile validate ./todoist-tutorial
truffile deploy ./todoist-tutorial
truffile connections add "Todoist Tutorial"
truffile connections list "Todoist Tutorial" --json
truffile connections permissions "Todoist Tutorial" read-only
```

The permissions command prompts for the account. Repeat it for each account
you want to restrict. The provider's OAuth scopes and Truffle's account
permissions are separate: granting a provider scope does not override Truffle's
permission settings.

## OAuth callbacks and credentials

If the browser runs on the same computer as the CLI, the local callback
listener can receive a registered localhost redirect. If you open the browser
on another computer, copy its final localhost callback URL into the waiting
CLI prompt, even if that browser cannot load the callback page. The CLI
validates the state against the pending request.

Use the current sign-in link. Firmware may expire a setup after 15 minutes;
a longer CLI timeout does not extend that window. Start a fresh setup after
expiry. Never reuse a previous callback for another account.

Firmware exchanges and stores native OAuth credentials. Custom Python apps
should use the matching runtime credential helpers; connection-scoped tokens
must not be copied into app-global variables or source files. Native connectors
handle credential use and refresh through firmware.

## Account lifecycle and permissions

```bash
truffile connections reconnect "Todoist Tutorial"
truffile connections rename "Todoist Tutorial" Work
truffile connections permissions "Todoist Tutorial" Work auto-review --notes "Review external writes"
truffile connections delete "Todoist Tutorial" Work
truffile deploy ./todoist-tutorial --update
```

Friendly app names, bundle IDs, and app UUIDs work. An omitted account opens a
picker. In the rename example, choose an account and name it `Work`. To avoid
prompting, supply an exact account alias or connection UUID; deletion also
requires `--yes` in scripts.

New accounts use device policy unless an account override exists. Set an
override with `read-only`, `read-write`, or `auto-review`; `default` removes the
override and inherits device policy. The CLI does not change device-wide
defaults or offer an atomic install-and-set-permissions option. Tool annotations
such as `readOnlyHint` describe tool behavior; they are not permission grants.

An uploaded `--update` preserves installed accounts and their settings.
`--replace` intentionally resets accounts after confirmation. Test both accounts
after an update and verify that reconnecting one leaves the other intact.
See [deployment and account commands](/sdk/deployment) for cancellation,
noninteractive forms, packaging rules, and firmware requirements.
55 changes: 43 additions & 12 deletions sdk/building-apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ A Truffle app is a containerized Python program that runs on your Truffle and ex
| **Background** | On a schedule | Submits context to the proactive agent |
| **Hybrid** | On demand + on a schedule | Both: tools *and* scheduled context |

You pick the shape by including a `foreground`, `background`, or both sections in your `truffile.yaml`.
You pick the shape with `metadata.foreground`, `metadata.background`, or both
in `truffile.yaml`. Foreground apps can run Python tools or use a native remote
MCP connector. See [accounts and authentication](/sdk/accounts) for a complete
connector example and connection-scoped credentials.

The supported development loop is intentionally CLI-first: scaffold locally,
validate locally, dry-run the deploy plan, then deploy to a connected device.
check the bundle locally, preview the upload, then deploy through the native
installer and builders. Use `--update` for later code changes that preserve
installed accounts.

The bundled examples live under `truffile/app-store/` in the `truffile` repo. Treat them as reference implementations:

Expand Down Expand Up @@ -48,7 +53,8 @@ from truffile.app_runtime import (
)
```

The source lives in `truffile/app_runtime/` in the repo and ships with the installed package. Once you `pip install truffile`, the same runtime is importable from app code running on your device.
The runtime is copied from PyFW into the release wheel; it is not tracked in
the SDK source checkout. It ships as `truffile.app_runtime` in the installed package. Once you `pip install truffile`, the same runtime is importable from app code running on your device.

There are two other entry points that import the same names:

Expand Down Expand Up @@ -84,7 +90,8 @@ Flags:
- `truffile create <name> --path <dir>` — set name and base path
- `truffile create` — fully interactive

The generated `my_app_foreground.py` and `my_app_background.py` are stubs using the real SDK classes. Delete whichever you don't need — the app type is inferred from which `foreground` / `background` sections you keep in `truffile.yaml`.
The foreground starter includes a ping tool. The background starter is a
placeholder that you must implement before relying on scheduled work. Delete whichever you don't need — the app type is inferred from which `foreground` / `background` sections you keep in `truffile.yaml`.

---

Expand All @@ -104,9 +111,13 @@ The skill handles architecture decisions (foreground/background/hybrid, auth typ

## `truffile.yaml` structure

Every app needs a `truffile.yaml`. Minimum viable config:
Every app needs a `truffile.yaml`. This authenticated process example assumes
you implement the Python entrypoints, including a `--verify` mode that checks
the API key and exits. For a first ping test, use the generated scaffold as-is.

```yaml
truffile_version: 4
connections: multi
metadata:
name: My App
bundle_id: org.example.myapp
Expand Down Expand Up @@ -136,17 +147,22 @@ metadata:
steps:
- name: Install dependencies
type: bash
scope: install
run: |
pip install --no-cache-dir httpx

- name: Copy app files
type: files
scope: install
files:
- source: ./my_app_foreground.py
destination: ./my_app_foreground.py
- source: ./my_app_background.py
destination: ./my_app_background.py

- name: Configure access token
type: text
scope: connection
content: |
Enter an API key from https://example.com/settings/tokens
fields:
Expand Down Expand Up @@ -177,7 +193,9 @@ Steps run during installation on the device, in order. The manifest supports:
See the [Install steps reference](/sdk/install-steps) for every option on each type — field shapes, validators, exit code semantics, and multi-field examples.

<Note>
`vnc` / browser setup steps are not supported through `truffile`. `truffile validate` rejects them with an explanation and tells users to install those apps through Symphony Settings.
Native deployment can present browser/VNC setup cards from firmware. Open the
shown setup URL and follow the app's flow. The explicit legacy builder supports
only its older step subset; use normal deploy for modern manifests.
</Note>

---
Expand All @@ -194,9 +212,14 @@ Pick the smallest auth surface that matches the upstream service:
| Remote MCP OAuth | The upstream MCP server owns OAuth metadata or dynamic client registration | `oauth` with resource metadata / DCR fields | `notion` |
| Local bridge | The app on the device needs to talk back to a process on the user's computer | usually `text`, often prefilled by a helper command | `obsidian` |

For `text` auth, add a validator that calls `python ./foreground.py --verify` or `python ./background.py --verify`. For OAuth, write a tiny auth helper around `OAuth` or `RemoteMcpOAuth`, store the token file path in an env var, and verify it in a follow-up `bash` step.
For `text` auth, implement a provider check and call it from a connection-scoped
validator. Native OAuth stores tokens for the selected connection. Custom Python
apps use runtime credential helpers; native connectors declare an OAuth
`app_var_key` matching their connection step. See the [account guide](/sdk/accounts)
for the complete connector recipe. Older bundled token-file examples should not
be copied into shared state for a multi-account app.

If an update reaches an auth step that cannot be refreshed non-interactively, fail clearly and tell the user to reauthenticate from Settings.
If an update reaches an auth step that cannot be refreshed non-interactively, fail clearly and ask the user to run `truffile connections reconnect "My App"`.

---

Expand All @@ -208,11 +231,19 @@ truffile deploy --dry-run ./my-app
truffile deploy ./my-app
```

- **`validate`** checks the `truffile.yaml` structure, confirms referenced files exist, and parses all Python files for syntax errors.
- **`deploy --dry-run`** builds the deploy plan without changing anything on the device, so you can see what files will be uploaded, what bash steps will run, and how the process config will look.
- **`deploy`** opens a build session on the device, uploads files, runs your steps in order, and registers the app.
- **`validate`** checks bundle structure and basic metadata. Use `python -m compileall -q ./my-app` separately for Python syntax.
- **`deploy --dry-run`** packages and describes the bundle without changing the device.
- **`deploy`** sends the original manifest to firmware for semantic validation, builder execution and account setup.

After editing app code, run `truffile deploy ./my-app --update`. This preserves
accounts, credentials and saved settings. `--replace` deliberately resets
accounts after confirmation. Current firmware is required for uploaded updates
and replacements; the CLI checks support before either operation.

`--interactive` opens the explicit legacy builder's shell. Ordinary native
sign-in prompts do not require that flag. See [deployment](/sdk/deployment)
for packaging exclusions, timeouts, account selection and scripted setup.

Add `--interactive` to `deploy` to open a shell inside the app's container when the build steps finish — useful for installing extra packages or debugging before finalizing.

After deployment, use an agent-safe one-shot request as a routing smoke test:

Expand Down
Loading
Loading