Skip to content
Draft
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
30 changes: 30 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,36 @@ FIRST_TREE_DATABASE_URL=postgresql://firsttree:firsttree@localhost:5432/firsttre
# Bind address — MUST be 0.0.0.0 in Docker; 127.0.0.1 for local
FIRST_TREE_HOST=0.0.0.0

# S3-compatible object storage for attachment/avatar payloads (any of AWS
# S3, Cloudflare R2, MinIO). Local dev: docker compose up -d provides MinIO
# at localhost:9000 with these exact values. Without this group the server
# boots but rejects attachment uploads with 503 until it is configured;
# after configuring, move pre-existing payloads out of PostgreSQL with:
# pnpm --filter @first-tree/server migrate:attachments
FIRST_TREE_S3_BUCKET=firsttree-attachments
FIRST_TREE_S3_ENDPOINT=http://localhost:9000
FIRST_TREE_S3_ACCESS_KEY_ID=firsttree
FIRST_TREE_S3_SECRET_ACCESS_KEY=firsttree-minio
# Path-style addressing is required for MinIO; leave false for AWS S3/R2.
FIRST_TREE_S3_FORCE_PATH_STYLE=true
# FIRST_TREE_S3_REGION=us-east-1
# Browser-reachable endpoint for redirect-mode presigned URLs when the
# server reaches storage over an internal address.
# FIRST_TREE_S3_PUBLIC_ENDPOINT=

# Attachment governance (defaults shown). Downloads: proxy streams bytes
# through the server and works everywhere; redirect answers 302 with a
# <=5 min presigned URL and requires a browser-reachable bucket with CORS
# for the web origin. Quotas are hard rejects (413/422); the governed "2 GB"
# byte quota is implemented as 2 GiB (2^31).
# FIRST_TREE_ATTACHMENT_DOWNLOAD_MODE=proxy
# FIRST_TREE_ATTACHMENT_ORG_QUOTA_BYTES=2147483648
# FIRST_TREE_ATTACHMENT_ORG_QUOTA_COUNT=1000
# FIRST_TREE_ATTACHMENT_SWEEP_INTERVAL_SECONDS=900
# FIRST_TREE_ATTACHMENT_ORPHAN_GRACE_SECONDS=86400
# FIRST_TREE_ATTACHMENT_PENDING_TTL_SECONDS=3600
# FIRST_TREE_ATTACHMENT_MAX_CONCURRENT_UPLOADS_PER_UPLOADER=4

# ┌───────────────────────────────────────────────────────────────────────────┐
# │ Server (SaaS internal) — Optional │
# └───────────────────────────────────────────────────────────────────────────┘
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Operator-only flows such as `login`, `daemon install`, and `agent create` belong
## Architecture Rules

- **Package boundaries:** Server, Client, Command, and Web are independently packaged/deployed and share code through `@first-tree/shared`. The CLI is the user-facing command surface and depends only on Client + Shared. Server ships separately as the SaaS Docker image.
- **Server state:** Server is stateless. PostgreSQL is the only persistence/queue/notification backend; do not add Redis or MQ.
- **Server state:** Server is stateless. PostgreSQL is the only relational/queue/notification backend; do not add Redis or MQ. Binary attachment and avatar payloads live in S3-compatible object storage (`FIRST_TREE_S3_*`); PostgreSQL keeps their metadata only.
- **Unified user-JWT auth:** A single user JWT authorizes Web/Admin API calls and every agent the user manages on the client WebSocket. Route classification, JWT shape, and scope helpers live in [docs/development/http-path-conventions.md](docs/development/http-path-conventions.md). Channel homes live in [docs/development/local-dev-isolation.md](docs/development/local-dev-isolation.md). Agents bind via `agents.client_id` + `agent:pinned`; R-RUN is re-evaluated at every `agent:bind`. Switching users goes through `first-tree login <code>` and the local-client switch path; `logout --purge` retires the current server client and cuts its runtime routes before destructive local cleanup, after which cleared agents can be moved to a new connected runtime from Web.
- **Inbox boundary:** Server writes to Inbox; Client pulls / receives WebSocket notifications. Delivery is at-least-once; Client deduplicates.
- **Agent identity:** Agents are managed by the server Admin API. Agent profile markdown lives in `agents.profile`. Context Tree integration is optional and injected by Client at workspace startup.
Expand Down
17 changes: 17 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ Run the server and web commands in separate terminals. The server command
loads the root `.env` file and enables the local dev GitHub callback stub via
its package script.

`docker compose up -d` starts PostgreSQL (relational data) and MinIO
(S3-compatible object storage for attachment/avatar payloads; console at
`http://localhost:9001`). The `.env.example` values point the server at the
compose MinIO out of the box, and the server creates its bucket on boot.
Without the `FIRST_TREE_S3_*` group the server still runs, but attachment
uploads answer 503 until storage is configured. Deployments upgrading from
a pre-object-storage version move existing payloads out of PostgreSQL with
`pnpm --filter @first-tree/server migrate:attachments` (idempotent,
live-safe; see `.env.example`). Server tests need neither service
pre-started — vitest provisions its own containers (or uses the
`CI_DATABASE_URL` / `CI_S3_ENDPOINT` escape hatches).

## Local URLs

- API server: `http://127.0.0.1:8000`
Expand All @@ -53,6 +65,11 @@ FIRST_TREE_DATABASE_URL=postgresql://firsttree:firsttree@localhost:5432/firsttre
FIRST_TREE_HOST=127.0.0.1
FIRST_TREE_PORT=8000
FIRST_TREE_CHANNEL=dev
FIRST_TREE_S3_BUCKET=firsttree-attachments
FIRST_TREE_S3_ENDPOINT=http://localhost:9000
FIRST_TREE_S3_ACCESS_KEY_ID=firsttree
FIRST_TREE_S3_SECRET_ACCESS_KEY=firsttree-minio
FIRST_TREE_S3_FORCE_PATH_STYLE=true
```

The local `dev` channel can auto-generate `FIRST_TREE_JWT_SECRET` and
Expand Down
20 changes: 20 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,25 @@ services:
timeout: 3s
retries: 5

# S3-compatible object storage for attachment/avatar payloads. The server
# auto-creates the bucket on boot; console at http://localhost:9001.
minio:
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
command: server /data --console-address ":9001"
ports:
- "${MINIO_PORT:-9000}:9000"
- "${MINIO_CONSOLE_PORT:-9001}:9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-firsttree}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-firsttree-minio}
volumes:
- miniodata:/data
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9000/minio/health/ready || exit 1"]
interval: 5s
timeout: 3s
retries: 5

volumes:
pgdata:
miniodata:
18 changes: 18 additions & 0 deletions packages/server/drizzle/0083_overconfident_maelstrom.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE TABLE "attachment_references" (
"attachment_id" text NOT NULL,
"message_id" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "attachment_references_attachment_id_message_id_pk" PRIMARY KEY("attachment_id","message_id")
);
--> statement-breakpoint
ALTER TABLE "attachments" ALTER COLUMN "data" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "agents" ADD COLUMN "avatar_object_key" text;--> statement-breakpoint
ALTER TABLE "attachments" ADD COLUMN "organization_id" text;--> statement-breakpoint
ALTER TABLE "attachments" ADD COLUMN "object_key" text;--> statement-breakpoint
ALTER TABLE "attachments" ADD COLUMN "state" text DEFAULT 'stored' NOT NULL;--> statement-breakpoint
ALTER TABLE "attachment_references" ADD CONSTRAINT "attachment_references_attachment_id_attachments_id_fk" FOREIGN KEY ("attachment_id") REFERENCES "public"."attachments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "attachment_references" ADD CONSTRAINT "attachment_references_message_id_messages_id_fk" FOREIGN KEY ("message_id") REFERENCES "public"."messages"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "attachment_references_message_id_idx" ON "attachment_references" USING btree ("message_id");--> statement-breakpoint
ALTER TABLE "attachments" ADD CONSTRAINT "attachments_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "attachments_org_state_idx" ON "attachments" USING btree ("organization_id","state");--> statement-breakpoint
CREATE INDEX "attachments_state_created_at_idx" ON "attachments" USING btree ("state","created_at");
2 changes: 1 addition & 1 deletion packages/server/drizzle/LATEST
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0082_heavy_moonstone
0083_overconfident_maelstrom
Loading
Loading