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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,6 @@ skills.local.yaml
# devague working state (not committed by default)
.devague/reviews/
.claude/settings.json

# wrangler local state (created by `wrangler pages deploy` at repo root)
.wrangler/
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.2] - 2026-07-11

### Added

- Phase-1 (signed-out) production deploy of agentculture.org/learn: Cloudflare Pages project agentculture-learn + proxy Worker owning the agentculture.org/learn/* zone route. Signed-out tour, all subject/story pages, and the 401 resource-gate are live; verified 38/38 by the LIVE_ORIGIN launch gate.
- workers/learn-api/wrangler.signedout.toml — the committed Phase-1 config (proxy-only: route + PAGES_ORIGIN, no KV/D1/secrets).

### Changed

- workers/learn-api/README.md: document the two-phase go-live (signed-out live vs signed-in pending) and the exact token permissions each phase needs (Pages+Workers Scripts for Phase 1; +KV+D1 for Phase 2).

### Fixed

- wrangler.toml: moved the routes array above the [vars] table. Under [vars], TOML scoped it as vars.routes and wrangler silently bound no route (No targets deployed).

## [0.5.1] - 2026-07-11

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "learn-cli"
version = "0.5.1"
version = "0.5.2"
description = "A CLI, MCP server, and web front for learning — a hosted site where humans and agents learn a subject step by step. Fronts the french-cli and spanish-cli language tutors, and generalizes to other learnable domains such as leading teams of agents. The learn command is the local operator surface and how agents drive it."
readme = "README.md"
license = "Apache-2.0"
Expand Down
56 changes: 46 additions & 10 deletions workers/learn-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,43 @@ wrangler dev # serves http://localhost:8787
For `wrangler dev`, put non-secret vars in `wrangler.toml` and provide dev
secrets in a git-ignored `.dev.vars` file (see below).

## Provisioning (operator steps still owed)
## Deploy status: two phases

These steps are done once, by the operator, before the first deploy. Nothing
here is committed.
The go-live is staged. **Phase 1 (signed-out) is LIVE** at
<https://agentculture.org/learn/> (deployed 2026-07-11); **Phase 2 (signed-in)
is pending** two operator-held credentials.

| | Phase 1 — signed-out (LIVE) | Phase 2 — signed-in (pending) |
| --- | --- | --- |
| Config | [`wrangler.signedout.toml`](wrangler.signedout.toml) | [`wrangler.toml`](wrangler.toml) |
| Serves | Static `/learn/*` proxied to Pages; `/api/*` returns 401 | + sessions, ledger, tutoring |
| KV / D1 | none | KV `SESSIONS` + D1 `learn-ledger` |
| Secrets | none | `SESSION_SECRET`, `GITHUB_CLIENT_SECRET`, `INFERENCE_TOKEN` |
| Token perms | Cloudflare Pages: Edit **+** Workers Scripts: Edit | **+** Workers KV Storage: Edit **+** D1: Edit |
| Deploy | `wrangler deploy -c wrangler.signedout.toml` | `wrangler deploy` |

The signed-out tier touches no storage and spends no inference — `/api/me` and
`POST /api/tutor` return `401` *before* any KV/D1 access (proven live by the
launch gate's "signed-out fires ONLY GET /api/me" walk). That is why Phase 1
deploys with a Pages+Workers token and nothing else.

**Phase 1 was deployed with the repo's `.env` token, which carries Pages: Edit
and Workers Scripts: Edit but NOT KV/D1.** To do Phase 2 self-serve, that token
needs **Workers KV Storage: Edit** and **D1: Edit** added (or swap in a token
that has them).

## Provisioning Phase 2 (signed-in)

These steps are done once, by the operator, before the signed-in deploy.
Nothing here is committed.

### 1. Create the GitHub OAuth app

Create an OAuth app at
`https://github.com/settings/developers` (or in the AgentCulture org):

- **Authorization callback URL:** `https://<worker-host>/api/auth/callback`
(e.g. the route mounted under `agentculture.org/learn`).
- **Authorization callback URL:**
`https://agentculture.org/learn/api/auth/callback` (the live zone route).
- **Enable Device Flow** — required for the CLI/MCP device sign-in (t12).
- Requested scope is `read:user` only. Do **not** request `user:email`; the API
never reads or stores email.
Expand All @@ -93,7 +118,16 @@ wrangler d1 execute learn-ledger --file schema.sql

Copy the returned ids into `wrangler.toml` (replace the `REPLACE_ME_*`
placeholders for `id`, `preview_id`, and `database_id`), and set
`GITHUB_CLIENT_ID` there too (it is public).
`GITHUB_CLIENT_ID` there too (it is public). The route + `PUBLIC_URL` /
`APP_URL` / `CORS_ORIGIN` / `PAGES_ORIGIN` vars are already filled in
`wrangler.toml` from the live Phase-1 deploy.

Apply the schema to the **remote** D1 (the `--local` form only touches the
`wrangler dev` SQLite):

```bash
wrangler d1 execute learn-ledger --remote --file schema.sql
```

### 3. Set secrets

Expand Down Expand Up @@ -128,12 +162,14 @@ INFERENCE_TOKEN = "..."
### 4. Deploy

```bash
wrangler deploy
wrangler deploy # uses wrangler.toml — full stack, same /learn/* route
```

Then mount the Worker under the `agentculture.org/learn` zone via the
operator-owned Cloudflare routing (coordinated with the `org` / cultureflare
path — the same route that fronts the Pages site).
The `agentculture.org/learn/*` zone route is already live (Phase 1) and is
declared in `wrangler.toml`, so this deploy upgrades the *same* worker in place
— the route does not move. Confirm the signed-in path end-to-end afterward
(`GET /learn/api/me` with a real session cookie should `200`), then merge org's
Learn-nav link to open public discovery.

## Endpoint shapes for downstream waves

Expand Down
42 changes: 42 additions & 0 deletions workers/learn-api/wrangler.signedout.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# wrangler config — PHASE 1 (signed-out) deploy of the learn API Worker.
#
# This is the config that is LIVE in production today (deployed 2026-07-11).
# It stands up the whole SIGNED-OUT experience at agentculture.org/learn:
# the worker owns the /learn/* zone route and reverse-proxies every non-API
# path to the Cloudflare Pages origin (PAGES_ORIGIN). The signed-out tour is
# fully static; the resource-gated API routes (/api/me, /api/tutor, …) return
# 401 WITHOUT ever touching storage, so this deploy needs NO KV, NO D1, and
# NO secrets. It requires only two token permissions: Cloudflare Pages: Edit
# (for the Pages project) and Workers Scripts: Edit (for this worker + route).
#
# Deploy with:
# wrangler deploy -c wrangler.signedout.toml
#
# PHASE 2 (signed-in) uses the full ./wrangler.toml instead — it adds the KV
# namespace, the D1 ledger, the GitHub OAuth client id, and the secrets. That
# deploy additionally needs Workers KV Storage: Edit + D1: Edit on the token,
# and the GitHub OAuth app must exist first. See README.md "Provisioning".

name = "learn-api"
main = "src/index.js"
compatibility_date = "2024-11-01"
workers_dev = false

# Top-level key — MUST precede [vars] (see the gotcha note in wrangler.toml).
routes = [
{ pattern = "agentculture.org/learn/*", zone_name = "agentculture.org" },
{ pattern = "agentculture.org/learn", zone_name = "agentculture.org" },
]

[vars]
# Placeholder client id: harmless in phase 1. The sign-in button is only
# reachable by direct navigation (org's Learn nav link is held until phase 2),
# so a non-functional OAuth start has no public surface yet.
GITHUB_CLIENT_ID = "REPLACE_ME_github_oauth_client_id"
PUBLIC_URL = "https://agentculture.org"
Comment on lines +31 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Signed-out config hits github 🐞 Bug ☼ Reliability

wrangler.signedout.toml sets a non-empty placeholder GITHUB_CLIENT_ID, so phase-1 deployments will
still allow direct requests to /learn/api/auth/login and /learn/api/auth/device which attempt GitHub
OAuth/device-flow network calls instead of being cleanly disabled.
Agent Prompt
## Issue description
Phase-1 (signed-out) config includes a placeholder `GITHUB_CLIENT_ID`, which keeps the public auth endpoints callable and triggers outbound GitHub requests if hit directly.

## Issue Context
Even if the UI link is withheld, these endpoints remain reachable by URL and can create noisy failures and unexpected external traffic in the signed-out tier.

## Fix Focus Areas
- workers/learn-api/wrangler.signedout.toml[31-40]
- workers/learn-api/src/index.js[125-188]
- workers/learn-api/src/github.js[25-35]
- workers/learn-api/src/github.js[79-90]

## Notes
Preferred low-effort fix: remove `GITHUB_CLIENT_ID` from `wrangler.signedout.toml` (or set it empty) so `requireConfig()` returns a controlled 503 and no outbound GitHub calls occur.
Alternative: add a phase flag (e.g. `SIGNED_OUT=1`) and have the router return 401/404 for `/api/auth/*` and `/api/health` as desired in phase 1.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

APP_URL = "https://agentculture.org/learn/"
CORS_ORIGIN = "https://agentculture.org"
PAGES_ORIGIN = "https://agentculture-learn.pages.dev"

[observability]
enabled = true
37 changes: 22 additions & 15 deletions workers/learn-api/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,34 @@ compatibility_date = "2024-11-01"
# Node-compat is not required — the Worker uses only Web-standard APIs
# (fetch, Request/Response, crypto.subtle, btoa/atob). Keep the bundle lean.

# --- Zone route (the same-origin /learn mount) -------------------------------
# Cloudflare Pages cannot be mounted at a path, so this ONE worker owns
# agentculture.org/learn/*: /learn/api/* is handled here, everything else
# proxies to PAGES_ORIGIN (which serves the site under /learn/ too).
#
# GOTCHA: `routes` is a TOP-LEVEL key and MUST appear ABOVE the [vars] table.
# Placed after [vars], TOML scopes it as `vars.routes` and wrangler silently
# ignores it ("No targets deployed") — the worker uploads but binds no route.
# This block is live in production (deployed 2026-07-11); the signed-out mount
# is served by exactly these two patterns.
routes = [
{ pattern = "agentculture.org/learn/*", zone_name = "agentculture.org" },
{ pattern = "agentculture.org/learn", zone_name = "agentculture.org" },
]
Comment on lines +28 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Oauth callback missing /learn 🐞 Bug ≡ Correctness

With the worker now bound only on the /learn route, handleLogin() still generates the GitHub
redirect_uri as https://agentculture.org/api/auth/callback (no /learn), so the OAuth callback will
land outside this worker and the web login flow cannot complete.
Agent Prompt
## Issue description
The worker is now mounted at `agentculture.org/learn/*`, but `handleLogin()` constructs the GitHub OAuth `redirect_uri` at `/api/auth/callback` (no `/learn` prefix). This will cause GitHub to redirect to a path that does not hit this Worker route.

## Issue Context
- Requests arrive as `/learn/api/auth/login`, but the router strips `/learn` only for internal dispatch.
- GitHub must be configured to call back into the mounted worker path: `/learn/api/auth/callback`.

## Fix Focus Areas
- workers/learn-api/src/index.js[63-79]
- workers/learn-api/src/index.js[125-133]
- workers/learn-api/src/index.js[310-312]
- workers/learn-api/README.md[97-104]

## Notes
Implement a mount-aware callback builder, e.g. derive a `mountPrefix` from the incoming request pathname (`/learn` when mounted) and build:
- `redirectUri = `${publicOrigin(env,url)}${mountPrefix}/api/auth/callback``
Then ensure README and GitHub OAuth app config remain aligned.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


# --- Non-secret vars -------------------------------------------------------
# INFERENCE_URL points at the cloudai-cli / ec2bedrock-cli SERVED endpoint
# (an OpenAI-shaped HTTP API), NEVER a bespoke provider SDK. The broker only
# ever POSTs JSON to this URL. Leave it unset to disable tutoring (broker 503s).
[vars]
# GITHUB_CLIENT_ID is public; still REPLACE_ME until the GitHub OAuth app exists
# (needed only for the signed-in tier — see README "Phase 2").
GITHUB_CLIENT_ID = "REPLACE_ME_github_oauth_client_id"
# PUBLIC_URL = "https://agentculture.org" # origin used to build the OAuth callback
# APP_URL = "https://agentculture.org/learn/" # where the web callback redirects post-login
# CORS_ORIGIN = "https://agentculture.org" # allow the /learn site to call the API with creds
# INFERENCE_URL = "https://<cloudai-or-ec2bedrock-served-endpoint>/v1/messages"
# PAGES_ORIGIN = "https://agentculture-learn.pages.dev" # static-site origin the /learn zone mount proxies to

# --- Zone route (the same-origin /learn mount) -------------------------------
# Cloudflare Pages cannot be mounted at a path, so this ONE worker owns
# agentculture.org/learn/*: /learn/api/* is handled here, everything else
# proxies to PAGES_ORIGIN (which serves the site under /learn/ too).
# Uncomment once the zone is reachable from this account's token:
# routes = [
# { pattern = "agentculture.org/learn/*", zone_name = "agentculture.org" },
# { pattern = "agentculture.org/learn", zone_name = "agentculture.org" },
# ]
PUBLIC_URL = "https://agentculture.org" # origin used to build the OAuth callback
APP_URL = "https://agentculture.org/learn/" # where the web callback redirects post-login
CORS_ORIGIN = "https://agentculture.org" # allow the /learn site to call the API with creds
PAGES_ORIGIN = "https://agentculture-learn.pages.dev" # static-site origin the /learn zone mount proxies to
Comment on lines +41 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

3. Prod vars hinder wrangler dev 🐞 Bug ⚙ Maintainability

wrangler.toml now hard-codes PUBLIC_URL/APP_URL/CORS_ORIGIN to production, so wrangler dev will emit
production OAuth redirect URIs and CORS headers (breaking localhost auth/browser testing) unless
developers override these vars.
Agent Prompt
## Issue description
Production defaults for `PUBLIC_URL`, `APP_URL`, and `CORS_ORIGIN` in `wrangler.toml` make local `wrangler dev` behave like production (wrong OAuth redirect URIs and CORS allow-origin), which is error-prone for contributors.

## Issue Context
README directs developers to use `wrangler.toml` for `wrangler dev`, and the runtime code prefers these env vars over the request origin.

## Fix Focus Areas
- workers/learn-api/wrangler.toml[33-46]
- workers/learn-api/README.md[64-66]
- workers/learn-api/src/index.js[125-133]
- workers/learn-api/src/index.js[322-335]

## Notes
Options:
1) Add a `wrangler.dev.toml` (or `wrangler.local.toml`) with localhost values and document `wrangler dev -c wrangler.dev.toml`.
2) Document explicitly that `.dev.vars` should also override these non-secret vars for local runs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

# INFERENCE_URL = "https://<cloudai-or-ec2bedrock-served-endpoint>/v1/messages" # unset ⇒ tutoring 503s

# --- Secrets (NEVER committed) ---------------------------------------------
# Set with: wrangler secret put <NAME>
Expand Down
Loading