@odla-ai/cli 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,31 +3,48 @@
3
3
  Goal: Clerk sign-in on the site, the worker verifying session JWTs
4
4
  itself, at least one route gated. Dev only.
5
5
 
6
- Human obligation: create a Clerk application (dev instance) and paste
7
- its PUBLISHABLE key (`pk_test_…`) when asked. Tell them explicitly:
8
- publishable keys are public by design this is the only key-like value
9
- they will ever paste into chat. Never ask for `sk_…` or `whsec_…`.
6
+ Human obligation: **run `npx clerk auth login`** a one-time browser
7
+ OAuth into their Clerk account. This is the ONE interactive step and the
8
+ whole reason to surface it early: the agent cannot authenticate as the
9
+ user. After login, credentials are stored locally and the agent drives
10
+ the rest (`apps create`, `link`, `env pull`, `config patch`) from Bash.
11
+ There is no `pk_` to hand-paste — the CLI pulls it. (Never handle `sk_…`
12
+ or `whsec_…` in client mode; `env pull` writes an `sk_` into a gitignored
13
+ `.env.local` that stays local and unused.)
14
+
15
+ Use the Clerk CLI (`npx clerk`) to create the instance rather than the
16
+ dashboard. Do NOT assume an app exists: a stale publishable key can linger
17
+ on the registry's app_auth record from a deleted/recreated app —
18
+ `clerk apps list` is the source of truth for what's real.
10
19
 
11
20
  ## Steps
12
21
 
13
- 1. Add to `odla.config.mjs`:
14
- `auth: { clerk: { dev: "<pk_test_…>" } }`
15
- (Inline is fine the value is public. The `"$ENV_VAR"` indirection
16
- the config template shows is optional.)
17
- 2. `npx odla-ai provision` (idempotent) calls setAuth for the dev env;
18
- the issuer is derived from the key. The worker fetches auth config
19
- from the registry's public-config at runtime, so a key rotation
20
- propagates without a redeploy.
21
- 3. `npm i jose`. In the worker (lift the exact pattern from
22
- `examples/demo-app/src/worker.ts`):
22
+ 1. Provision the Clerk app with the CLI (after the human's `clerk auth
23
+ login`):
24
+ - `npx clerk apps list` confirm what exists (catch stale/leftover
25
+ instances); create if absent.
26
+ - `npx clerk apps create "<App Name>"` creates the app + dev
27
+ instance (`pk_test_…`).
28
+ - `npx clerk link` (or `--app <id>`), then `npx clerk env pull` — pull
29
+ the dev `pk_test_…` into `.env.local`. Take only the publishable key.
30
+ 2. Add the key to `odla.config.mjs`: `auth: { clerk: { dev: "<pk_test_…>" } }`
31
+ (inline is fine — it's public), then `npx @odla-ai/cli provision`
32
+ (idempotent) — calls setAuth for the dev env; the issuer is derived
33
+ from the key. **provision OVERWRITES any stale key** on public-config.
34
+ The worker fetches auth config from public-config at runtime, so a key
35
+ change propagates without a redeploy.
36
+ 3. `npm i jose`. In the worker, use this documented verification pattern:
23
37
  - fetch `<ODLA_PLATFORM>/registry/apps/<ODLA_APP_ID>/public-config?env=<ODLA_ENV>`,
24
38
  cache ~5 min per isolate
25
39
  - `createRemoteJWKSet(new URL(issuer + "/.well-known/jwks.json"))`,
26
40
  cached per issuer
27
41
  - `jwtVerify(token, jwks, { issuer })` on the `Authorization: Bearer`
28
42
  header; pass `{ sub, email }` to routes
29
- - ensure the Clerk session token includes an email claim (Clerk
30
- dashboard session token customization) if routes need email
43
+ - ensure the Clerk session token includes an email claim if routes
44
+ need email: try `npx clerk config schema --keys session` then
45
+ `npx clerk config patch --json '{…}'`; else set it in the dashboard
46
+ (Sessions → customize session token) to
47
+ `{"email": "{{user.primary_email_address}}"}`
31
48
  4. Gate at least one `/api/*` route on a verified user; return 401
32
49
  otherwise.
33
50
  5. Add Clerk sign-in to the site pages. For a React/Preact site, use
@@ -36,17 +53,86 @@ they will ever paste into chat. Never ask for `sk_…` or `whsec_…`.
36
53
  clerk-js (no clerk-react), loads clerk-js as a lazy chunk, and themes
37
54
  to odla-ui via `clerkAppearanceFromTokens()`. Otherwise drive ClerkJS
38
55
  directly with the publishable key from public-config, or use Clerk's
39
- hosted pages.
56
+ hosted pages. For a **vanilla/static site**, load Clerk's browser bundle
57
+ with the key on the tag — it hot-loads `window.Clerk` as a ready
58
+ *instance* (not a constructor): `<script src="https://<frontend-api>/
59
+ npm/@clerk/clerk-js@5/dist/clerk.browser.js" data-clerk-publishable-key=
60
+ "<pk>">` then `await window.Clerk.load()`. The `<frontend-api>` is the
61
+ host encoded in the pk. Then mount `Clerk.mountSignIn(el, { appearance })`
62
+ and attach `Clerk.session.getToken()` as the `Bearer` on `/api/*` calls.
40
63
  6. Deploy dev (`npm run deploy:app:dev`).
41
64
 
42
- Auth mode "full" (webhook-synced $users via svix, whsec in the tenant
43
- vault) is a later upgrade not needed to ship login.
65
+ Clerk is the **source of truth** for identity; odla-db keeps a mirror in
66
+ `$users`. Shipping login (this phase) is enough to gate routes. Mirroring
67
+ users into `$users` (svix webhook, "full" mode) is a **separate, optional
68
+ next step — see phase-3b-user-sync.md** — not needed to ship login.
69
+
70
+ ## Shape the instance with the Clerk CLI (optional, for invite-only sites)
71
+
72
+ The same `clerk` CLI configures the instance as code (per app, from any
73
+ directory with `--app <id>`) — no dashboard clicking. For a members-only
74
+ site you typically want email-only + no social SSO + an allowlist:
75
+
76
+ - Inspect: `npx clerk config schema --keys session auth_email auth_password
77
+ auth_access_control` ; pull current with `npx clerk config pull`.
78
+ - Apply a partial patch (dry-run first with `--dry-run`):
79
+ `npx clerk config patch --json '{"auth_password":{"enabled":false},
80
+ "connection_oauth_google":{"enabled":false},
81
+ "auth_access_control":{"allowlist_enabled":true,
82
+ "allowlist_blocklist_enforced_on_sign_in":true}}'`
83
+ (email-code sign-in stays on via `auth_email`). A committed
84
+ `clerk-config.json` + `clerk config patch --file` keeps it reproducible and
85
+ reusable across the dev and prod instances.
86
+ - Invite-gate sign-ups by adding allowed emails to the allowlist:
87
+ `npx clerk api /allowlist_identifiers -X POST -d
88
+ '{"identifier":"<email>","notify":false}'` (per app with `--app <id>`).
89
+ - The email session-token claim (Phase 3 step 3) is just another patch:
90
+ `clerk config patch --json '{"session":{"claims":{"email":
91
+ "{{user.primary_email_address}}"}}}'`.
92
+
93
+ ## Match the site's existing look and feel (do NOT invent a style)
94
+
95
+ Every visible element you add — the sign-in gate, any nav/top bar, an
96
+ admin console, cross-links — MUST look like it was always part of the
97
+ site. A bolted-on or default-styled element is a defect even if it
98
+ works. Before writing any markup:
99
+
100
+ 1. **Read the site's design system first.** Find its stylesheet(s) and
101
+ shared components (header/nav/footer). Note the exact tokens: fonts
102
+ (heading vs body), color palette, logo/wordmark markup, spacing,
103
+ button styles, radii. Reuse them verbatim — same classes, or copy the
104
+ exact values. Never approximate; "close enough" reads as wrong (a
105
+ 32px logo next to the real 36px one, the accent colour off by a shade,
106
+ a heading in the body font — all instantly visible).
107
+ 2. **Reuse existing components.** If the site has a `<site-header>`/nav
108
+ component, extend or replicate it exactly rather than inventing a new
109
+ bar. New surfaces (admin, member area) should share ONE bar with the
110
+ rest of the site — identical logo size, wordmark size, padding, max
111
+ width.
112
+ 3. **Theme third-party widgets to the brand.** A vendor's default chrome
113
+ (Clerk's card + purple user-button avatar, Stripe's default inputs)
114
+ is off-brand by definition. Use its theming API — Clerk `appearance`:
115
+ strip the card to blend into a host card, set `colorPrimary`/fonts/
116
+ radii to the site tokens, hide redundant headers, `overflow:visible`
117
+ so it can't clip its own labels. Don't ship a raw vendor widget, and
118
+ don't show an off-brand account avatar when a plain "Sign out" fits.
119
+ 4. **Bump the cache-bust version** (`?v=`) on any shared asset you change
120
+ (stylesheet, header script), on every page that embeds it — otherwise
121
+ cached visitors keep the old copy and "your fix didn't deploy".
122
+ 5. **You cannot see the render — so never claim a visual match.** State
123
+ what you changed and ask the human to confirm; expect a round or two
124
+ of "the logo's too small / it clips / that colour is wrong" and treat
125
+ that as the normal loop, not something to argue with.
44
126
 
45
127
  ## Verification checklist
46
128
 
47
129
  - [ ] Unauthenticated curl to the gated route → 401
48
130
  - [ ] Signed-in browser session reaches the gated route
49
- - [ ] `npx odla-ai smoke --env dev` still passes
131
+ - [ ] `npx @odla-ai/cli smoke --env dev` still passes
132
+ - [ ] Every added UI element matches the site's existing look (fonts,
133
+ palette, logo, spacing) — human-confirmed, not self-asserted
134
+ - [ ] Post-login redirect lands on a real URL (mind trailing-slash /
135
+ directory-index routing), not a 404
50
136
  - [ ] MIGRATION.md updated (gated routes, Clerk instance name)
51
137
 
52
138
  Rollback: remove the auth layer / ungate the route; nothing outside dev
@@ -0,0 +1,57 @@
1
+ # Phase 3b — Mirror Clerk users into odla-db (`$users`)
2
+
3
+ Optional, and staged: ship login first (Phase 3), then turn this on. **Clerk is
4
+ the source of truth for identity.** odla-db keeps a *mirror* of your users in the
5
+ reserved `$users` namespace so rules and app data can reference them. You do not
6
+ build the mirror yourself — the platform does it. This phase enables it.
7
+
8
+ Goal: signed-in users appear as rows in the app's `$users`, kept in sync with
9
+ Clerk (create/update/delete). Dev first, then prod.
10
+
11
+ Human obligation: create a Clerk webhook endpoint and paste its signing secret
12
+ (`whsec_…`) into Studio (write-only). That plus the Phase-3 `clerk auth login`
13
+ are the only human Clerk steps.
14
+
15
+ ## How `$users` fills — two paths
16
+
17
+ 1. **Browser talks to odla-db directly with its Clerk session token.** The
18
+ platform verifies the JWT and auto-creates a basic `$users` row (`id` = the
19
+ Clerk `sub`). No setup. This only happens for *user-token* access.
20
+ 2. **The Clerk webhook (full profile).** Clerk pushes `user.created/updated/
21
+ deleted` to odla-db, which upserts the full profile (`email`, `name`,
22
+ `imageUrl`) and tombstones on delete. Works for **any** app.
23
+
24
+ **If your app is worker-mediated** (the worker holds the app's admin/scoped key
25
+ and calls odla-db itself — the Phase-2/3 pattern), that scoped credential is
26
+ **not projected into `$users`**. So the webhook (path 2) is how you get `$users`.
27
+ Enable it here.
28
+
29
+ ## Enable the webhook
30
+
31
+ 1. **Create the endpoint in Clerk** (per instance — dev instance → dev tenant;
32
+ prod instance → prod tenant). URL:
33
+ `https://db.odla.ai/webhooks/clerk/<ODLA_TENANT>`
34
+ (the odla tenant, e.g. `<appId>--dev` in dev, `<appId>` in prod). Subscribe to
35
+ `user.created`, `user.updated`, `user.deleted`. Clerk returns a signing secret
36
+ `whsec_…`. Prefer the Clerk CLI if it exposes webhook creation; otherwise the
37
+ Clerk dashboard → Webhooks → Add Endpoint. (`whsec_` is a real secret — never
38
+ print/commit it; it is the only thing you paste, and it goes into the vault,
39
+ not chat.)
40
+ 2. **Store the secret in the tenant vault** as `clerk_webhook_secret`, write-only,
41
+ via Studio's secret UI (`odla.ai/studio` → the app → the env → secrets). The server
42
+ decrypts it only to verify inbound events (svix HMAC over `id.timestamp.body`).
43
+ 3. **(Optional) `sk_…`** — the webhook payload already carries email/name/image,
44
+ so the basic mirror needs only `whsec_`. Add the Clerk backend key `sk_…` to
45
+ the vault only if you later need to resolve data the webhook doesn't send.
46
+
47
+ ## Verify
48
+
49
+ - Sign in (or edit the user in Clerk) → a `$users` row appears (check Studio, or
50
+ a worker route that runs `db.query({ $users: {} })`).
51
+ - Delete the Clerk user → the row is tombstoned, not hard-deleted.
52
+ - Rules can now gate on it: `$users` is platform-managed (a browser may view its
53
+ own row but cannot write `$users`). Your app's own tables (members, roles,
54
+ status) layer on top, keyed by the Clerk `sub` or `email`.
55
+
56
+ Done when: a real sign-in produces a `$users` row in the dev tenant. Repeat the
57
+ endpoint + secret for the prod instance/tenant at Phase 5.
@@ -15,22 +15,23 @@ own terminal so you never see the value.
15
15
  via this session):
16
16
 
17
17
  export <PROVIDER>_API_KEY=... # their key
18
- npx odla-ai provision
18
+ npx @odla-ai/cli provision
19
19
 
20
20
  provision stores the key in the tenant vault and sets provider/model
21
21
  in the registry. Their shell forgets it when closed; wrangler and git
22
- never see it. If they use `! npx odla-ai provision` in-session, the
22
+ never see it. If they use `! npx @odla-ai/cli provision` in-session, the
23
23
  export must still happen in a terminal you don't read.
24
24
  3. `npm i @odla-ai/ai`. In the worker, use `initFromPlatform` — it reads
25
25
  provider/model from public-config (cached ~60s) and resolves the key
26
26
  from the vault at call time using only the app's db key. Provider and
27
- model are switchable in Studio with no redeploy. Reference:
28
- `examples/kg-demo` in the odla-ai monorepo.
27
+ model are switchable in Studio with no redeploy. Read
28
+ `node_modules/@odla-ai/ai/llms.txt` and the public platform manual for the
29
+ complete vault + public-config contract.
29
30
  4. Add one `/api/*` route that round-trips the model; deploy dev.
30
31
 
31
32
  ## Verification checklist
32
33
 
33
- - [ ] `npx odla-ai smoke --env dev` passes
34
+ - [ ] `npx @odla-ai/cli smoke --env dev` passes
34
35
  - [ ] The AI route returns a model response on the deployed dev worker
35
36
  - [ ] `wrangler.jsonc` `vars` contain NO provider key; `git grep` for the
36
37
  key's prefix finds nothing
@@ -5,25 +5,39 @@ worker, DNS cut over — with GitHub Pages kept live as the rollback for
5
5
  at least 72 hours.
6
6
 
7
7
  Human obligations: add the domain to Cloudflare; click through the DNS
8
- changes (you supply exact values); create the prod Clerk instance if
9
- the app uses login; final go/no-go at each step below.
8
+ changes (you supply exact values); if using login, **activate production
9
+ on the existing Clerk app** (`npx clerk deploy`) the same app's prod
10
+ instance, never a second Clerk app per env; final go/no-go at each step.
10
11
 
11
12
  ## Steps
12
13
 
13
14
  1. Update `odla.config.mjs`: add `"prod"` to `envs`; add
14
- `auth.clerk.prod` (`pk_live_…` from the prod Clerk instance) if using
15
- login; add `links: { prod: "https://<domain>" }` (the CNAME domain
16
- captured in Phase 0). If using AI, the human re-runs the Phase 4
17
- export + provision so the PROD tenant's vault gets the key.
18
- 2. `npx odla-ai provision --dry-run`, show the human, then
19
- `npx odla-ai provision` provisions the prod tenant (`<appId>`),
20
- idempotent for everything already done in dev.
21
- 3. Push the prod db key: `npx odla-ai secrets push --env prod --yes`
22
- (targets the top-level wrangler env; `--yes` is the explicit prod
23
- consent see references/secrets-map.md).
24
- 4. Build, then `npx wrangler deploy` (first prod deploy). Verify
15
+ `auth.clerk.prod` (`pk_live_…` from the app's **activated production
16
+ instance** one Clerk app, two instances, see below) if using login;
17
+ add `links: { prod: "https://<domain>" }` (the CNAME domain captured in
18
+ Phase 0). If using AI, the human re-runs the Phase 4 export + provision
19
+ so the PROD tenant's vault gets the key.
20
+ 2. `npx @odla-ai/cli provision --dry-run`, show the human, then
21
+ `npx @odla-ai/cli provision --yes --push-secrets` provisions the prod
22
+ tenant (`<appId>`) and transfers its configured db/o11y secrets through
23
+ Wrangler stdin. `--yes` is the explicit production consent; use standalone
24
+ `secrets push --env prod --yes` only to retry the transfer (see
25
+ references/secrets-map.md).
26
+ 3. Build, then run the passive pre-cutover security gate:
27
+ first require `npm view @odla-ai/security@0.2.0 version` to succeed. An
28
+ exact-version `E404` means the release is unavailable and blocks cutover;
29
+ it is not a clean scan and does not prove the package name is absent. Run
30
+ `npm i -D --save-exact @odla-ai/security@0.2.0` followed by
31
+ `npx odla-security scan . --profile odla --out .odla/security/pre-cutover --fail-on high --fail-on-candidates critical`.
32
+ Review `REPORT.md`; a candidate is a lead, not confirmation, and a baseline
33
+ requires a concrete reason, owner, and expiry.
34
+ If the human explicitly approves redacted tracked-source disclosure, follow
35
+ with `npx @odla-ai/cli security run . --env prod --ack-redacted-source`.
36
+ The platform owns provider credentials and attributes the bounded,
37
+ independent-model run to this app; never request provider keys locally.
38
+ 4. `npx wrangler deploy` (first prod deploy). Verify
25
39
  `/api/health` and the parity paths on the prod workers.dev URL.
26
- 5. `npx odla-ai smoke --env prod`.
40
+ 5. `npx @odla-ai/cli smoke --env prod`.
27
41
  6. Human: add the domain to Cloudflare, then attach it to the prod
28
42
  worker (Workers & Pages → the worker → Domains & Routes). Supply
29
43
  them the exact hostname values to enter.
@@ -37,9 +51,26 @@ the app uses login; final go/no-go at each step below.
37
51
  confirmation: disable GitHub Pages in the repo settings. KEEP the
38
52
  repo — it is still the source of the site.
39
53
 
54
+ ## Clerk in prod — one app, two instances
55
+
56
+ A Clerk **application** has a Development instance (used in dev) and a
57
+ Production instance. Production (`pk_live_…`) is **domain-coupled**:
58
+ `npx clerk deploy` walks the human through the production domain + DNS —
59
+ the same domain you're cutting over here — so activating it fits naturally
60
+ at Phase 5. **Do NOT create a second Clerk app per environment** — activate
61
+ production on the *existing* app. Then:
62
+
63
+ - Re-apply the instance config to the prod instance:
64
+ `npx clerk config patch --instance prod --file clerk-config.json`, and
65
+ re-add allowlist identifiers with `--app <id>` (see phase-3-auth.md).
66
+ - If mirroring users, add the `$users` webhook for the prod instance/tenant
67
+ (`https://db.odla.ai/webhooks/clerk/<appId>` + `whsec_` in the prod
68
+ tenant vault) — see phase-3b-user-sync.md.
69
+
40
70
  ## Verification checklist
41
71
 
42
- - [ ] `npx odla-ai smoke --env prod` passes
72
+ - [ ] `npx @odla-ai/cli smoke --env prod` passes
73
+ - [ ] Passive `odla-security` pre-cutover report reviewed; critical lead gate passes
43
74
  - [ ] Public domain serves from the worker (check the `x-odla-worker`
44
75
  header) on every parity path
45
76
  - [ ] Auth and AI routes verified from the public domain
@@ -49,5 +80,5 @@ Rollback: point DNS back at GitHub Pages (minutes). Nothing on the
49
80
  odla/Cloudflare side needs to be torn down to roll back.
50
81
 
51
82
  Done when: the 72-hour confirmation is done and MIGRATION.md is closed
52
- out. Congratulate the human — and mention Studio (https://odla.ai) as
83
+ out. Congratulate the human — and mention Studio (https://odla.ai/studio) as
53
84
  where they watch their app from now on.
@@ -8,20 +8,31 @@
8
8
  | Clerk webhook secret (`whsec_…`) | tenant vault (`clerk_webhook_secret`) | only for auth mode "full"; entered in Studio, never wrangler, never chat |
9
9
  | Clerk secret key (`sk_test_`/`sk_live_`) | not used in this journey | never ask for it |
10
10
  | LLM provider key | tenant vault | env var in the HUMAN's shell for one provision run; never wrangler vars, never git, never chat |
11
- | `odla_sk_…` (tenant db key) | wrangler secret `ODLA_API_KEY` + `.odla/credentials.local.json` (0600) + `.dev.vars` | the only wrangler secret in this journey; move it only with the pipeline below |
11
+ | `odla_sk_…` (tenant db key) | wrangler secret `ODLA_API_KEY` + `.odla/credentials.local.json` (0600) + `.dev.vars` | present when db is enabled; move it only with the pipeline below |
12
12
  | `odla_dev_…` (developer token) | `.odla/dev-token.json` (0600) | ~24h lifetime, provision-time only; never deployed |
13
- | `o11y_…` (o11y ingest token) | wrangler secret `ODLA_O11Y_TOKEN` + `.odla/credentials.local.json` (0600) + `.dev.vars` | only if the app enables o11y; provision auto-mints it (dashboard can too); moved with the pipeline below alongside the db key; never a var, never chat |
14
- | `ODLA_ENDPOINT` / `ODLA_TENANT` / `ODLA_PLATFORM` / `ODLA_APP_ID` / `ODLA_ENV` / `ODLA_O11Y_ENDPOINT` / `ODLA_O11Y_SERVICE` | wrangler `vars` | not secrets; keep them set in every env block |
13
+ | scoped `odla_dev_…` (admin-approved platform action) | `.odla/admin-token.local.json` (0600) | exact visible scope, ~15m lifetime, never ambient admin or deployed |
14
+ | `o11y_…` (o11y ingest token) | wrangler secret `ODLA_O11Y_TOKEN` + `.odla/credentials.local.json` (0600) + `.dev.vars` | only if the app enables o11y; provision issues/reuses it and moves it alongside the db key; never a var, never chat |
15
+ | `ODLA_ENDPOINT` / `ODLA_TENANT` / `ODLA_PLATFORM` / `ODLA_APP_ID` / `ODLA_ENV` | wrangler `vars` | not secrets; keep them set in every env block |
16
+ | `ODLA_O11Y_ENDPOINT` / `ODLA_O11Y_SERVICE` | `.dev.vars` from provision or optional wrangler `vars` overrides | not secrets; public ingest defaults to `https://o11y.odla.ai` and `ODLA_APP_ID` when the token is present |
17
+
18
+ System AI provider credentials are platform-owned and never enter a customer
19
+ migration. `odla-ai security run` receives only app/run-bound role grants; do
20
+ not ask the human for Anthropic/OpenAI/Google keys for hosted security.
15
21
 
16
22
  ## Moving the db key (+ o11y token) into the Worker (never through the transcript)
17
23
 
18
- Use the CLI — it reads the 0600 credentials file and pipes each value to
19
- wrangler over stdin, with preflights (`wrangler whoami`, config present)
20
- and redacted output. It pushes `ODLA_API_KEY` and, when the app enabled o11y,
21
- `ODLA_O11Y_TOKEN` too:
24
+ Normal provisioning owns the complete sequence issuance, mode-0600 local
25
+ storage, `.dev.vars`, and redacted Wrangler stdin transfer. It pushes
26
+ `ODLA_API_KEY` and, when the app enabled o11y, `ODLA_O11Y_TOKEN` too:
27
+
28
+ npx @odla-ai/cli provision --write-dev-vars --push-secrets
29
+ npx @odla-ai/cli provision --yes --push-secrets # Phase 5; includes prod consent
30
+
31
+ Use the narrower command only to retry one environment's already-saved
32
+ credential transfer:
22
33
 
23
- npx odla-ai secrets push --env dev
24
- npx odla-ai secrets push --env prod --yes # Phase 5; --yes is the prod consent
34
+ npx @odla-ai/cli secrets push --env dev
35
+ npx @odla-ai/cli secrets push --env prod --yes
25
36
 
26
37
  Manual fallback (identical mechanics, if the CLI is unavailable):
27
38
 
@@ -41,5 +52,9 @@ wrangler env is prod.)
41
52
  but verify.
42
53
  - If a secret value ever does land in the conversation or a committed
43
54
  file: tell the human immediately, treat it as burned, and rotate it
44
- (`provision --rotate-keys` for db keys with their explicit
45
- approval; provider dashboard for LLM/Clerk values).
55
+ (`provision --rotate-keys` for the broad credential set, or
56
+ `provision --rotate-o11y-token --push-secrets` for o11y only — with explicit
57
+ human approval; provider dashboard for LLM/Clerk values).
58
+ - Studio's o11y token control is manual recovery only. It invalidates the live
59
+ token immediately and does not update `.odla/credentials.local.json`; prefer
60
+ the CLI rotation above so the saved and deployed values move together.
@@ -22,14 +22,14 @@ Cause: tenant confusion — the code ran against `<appId>` (prod) instead
22
22
  of `<appId>--dev`, or vice versa. Typical trigger: `wrangler deploy`
23
23
  without `--env dev`, or `wrangler dev` picking up top-level vars.
24
24
  Fix: check `ODLA_TENANT`/`ODLA_ENV` in the relevant `wrangler.jsonc`
25
- env block and which deploy command ran. `npx odla-ai smoke --env dev`
25
+ env block and which deploy command ran. `npx @odla-ai/cli smoke --env dev`
26
26
  prints what it verified against.
27
27
 
28
28
  ## Provision fails with an auth/token error
29
29
 
30
30
  Cause: the `odla_dev_…` token expired (~24h) or the handshake was never
31
31
  approved.
32
- Fix: re-run `npx odla-ai provision` — it starts a fresh handshake; the
32
+ Fix: re-run `npx @odla-ai/cli provision` — it starts a fresh handshake; the
33
33
  human approves the new code at the printed URL. Use `--no-open` in
34
34
  non-interactive shells if the browser launch misbehaves.
35
35
 
@@ -37,16 +37,27 @@ non-interactive shells if the browser launch misbehaves.
37
37
 
38
38
  Cause: `.odla/credentials.local.json` absent, for a different app id,
39
39
  or lacking a db key for the requested env.
40
- Fix: run `npx odla-ai provision` for the configured envs first; confirm
40
+ Fix: run `npx @odla-ai/cli provision` for the configured envs first; confirm
41
41
  `envs` in odla.config.mjs includes the env you're smoking.
42
42
 
43
43
  ## `smoke` fails: schema mismatch
44
44
 
45
45
  Cause: local `src/odla/schema.mjs` changed since the last push.
46
- Fix: re-run `npx odla-ai provision` (schema re-push is the migration
46
+ Fix: re-run `npx @odla-ai/cli provision` (schema re-push is the migration
47
47
  mechanism; additive changes are safe on live tenants), then re-run
48
48
  smoke.
49
49
 
50
+ ## Provision reports an existing o11y token but no local credential
51
+
52
+ Cause: the shown-once token was issued elsewhere, or the local credentials file
53
+ was lost. Plain provision refuses to invalidate a deployed Worker just to
54
+ recover plaintext.
55
+ Fix: after explicit human approval, run
56
+ `npx @odla-ai/cli provision --rotate-o11y-token --push-secrets` (add `--yes`
57
+ when production is in the plan). This persists the replacement before moving it
58
+ to the Worker. Do not use the Studio button for routine recovery; it cannot sync
59
+ the repository's credential file.
60
+
50
61
  ## Clerk-verified requests return no email
51
62
 
52
63
  Cause: the Clerk session token doesn't include an email claim.
@@ -55,15 +66,18 @@ claim, then re-test. The worker only forwards what the JWT carries.
55
66
 
56
67
  ## Re-running provision — is it safe?
57
68
 
58
- Yes: re-runs are idempotent (existing app registration and db keys are
59
- reused; schema/rules re-push). The only destructive flag is
60
- `--rotate-keys`, which mints new db keys — deployed workers keep
61
- working only after you push the new key to them. Use it only on
62
- explicit human request (e.g. a burned key).
69
+ Yes: plain re-runs are idempotent (existing app registration and configured db
70
+ and o11y credentials are reused; schema/rules re-push). Destructive rotation is
71
+ always explicit: `--rotate-o11y-token` replaces only o11y, while
72
+ `--rotate-keys` is the broader credential rotation. Use either only on explicit
73
+ human request, and pair rotation with `--push-secrets` so the Worker receives
74
+ the replacement in the same run. This is not atomic: if the final Wrangler
75
+ transfer fails, the new value is already in the private credentials file. Run
76
+ the printed `secrets push --env ...` retry and do not rotate again.
63
77
 
64
78
  ## Something not covered here
65
79
 
66
- Check, in order: `npx odla-ai doctor` output; the field notes in
67
- https://odla.ai/llms.txt; the relevant
68
- `node_modules/@odla-ai/*/llms.txt`. Do not improvise around a safety
69
- rule to unblock yourself — surface the blocker to the human instead.
80
+ Check, in order: `npx @odla-ai/cli doctor` output; this skill's current phase
81
+ and troubleshooting references; then the relevant installed
82
+ `node_modules/@odla-ai/*/llms.txt`. Do not improvise around a safety rule to
83
+ unblock yourself — surface the blocker to the human instead.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: odla-o11y-debug
3
+ description: >
4
+ Triage a production issue in an app instrumented with @odla-ai/o11y — find why
5
+ it's erroring, why latency or cost is spiking, and what's unusual about the bad
6
+ requests — by reading the odla-o11y API. Use when a deployed odla app is
7
+ misbehaving and you need to dig into its telemetry.
8
+ runbookOrder: []
9
+ ---
10
+
11
+ # odla-o11y-debug
12
+
13
+ You are debugging a **live odla app** through its observability data. The app is
14
+ instrumented with `@odla-ai/o11y`; its traces, errors, metrics, and LLM cost land
15
+ in the `odla-o11y` collector. This skill is the read side: how to pull the data
16
+ and reason from a symptom to a cause.
17
+
18
+ The core move is **cutting high-dimensional data to isolate the bad slice** — not
19
+ "is p95 high" but "*which* route / status / model / user is dragging it, and what
20
+ do those requests share."
21
+
22
+ ## Access
23
+
24
+ All reads go through the platform shell, operator-authed, so the collector's token
25
+ never leaves the server:
26
+
27
+ ```
28
+ GET https://odla.ai/o11y/<appId>/<endpoint>?env=prod
29
+ Authorization: Bearer <developer-token>
30
+ ```
31
+
32
+ Get `<developer-token>` from the device handshake (`POST https://odla.ai/handshake`
33
+ → approve in the Studio → `POST /handshake/poll`). `env` is `prod` (default) or
34
+ `dev`. If a read returns `{"error":"ae_not_configured"}` (501), the app's metrics
35
+ reads aren't turned on yet — errors/traces still work; say so and fall back to them.
36
+
37
+ ## Endpoints
38
+
39
+ | Endpoint | Answers |
40
+ | --- | --- |
41
+ | `GET /o11y/<app>/events` | recent activity feed (traces + errors), newest first |
42
+ | `GET /o11y/<app>/errors` | deduped error groups (type, code, route, count, last seen) |
43
+ | `GET /o11y/<app>/errors/<fingerprint>` | **one group + its message + STACK + span waterfall** |
44
+ | `GET /o11y/<app>/traces/<traceId>` | the merged span tree for a trace |
45
+ | `GET /o11y/<app>/metrics/red?minutes=60` | request/error/latency by route |
46
+ | `GET /o11y/<app>/metrics/timeseries?minutes=60` | RED over time (spot the spike) |
47
+ | `GET /o11y/<app>/explore?dimension=&metric=` | **RED grouped by ANY dimension** |
48
+ | `GET /o11y/<app>/llm/cost?minutes=1440` | spend by provider/model |
49
+ | `GET /o11y/<app>/llm/cost/timeseries` | spend over time |
50
+ | `GET /o11y/<app>/alerts` | configured triggers + firing history |
51
+
52
+ `explore` dimensions: `route`, `status`, `name`, `kind`, `http_status`,
53
+ `http_method`, `env`. `metric` (what to rank by): `p95_ms`, `avg_ms`, `requests`,
54
+ `errors`.
55
+
56
+ ## Triage loops
57
+
58
+ **An error is reported.** `GET /errors` → find the group by type/route/count →
59
+ `GET /errors/<fingerprint>`. The response `bundle` carries the **message and full
60
+ stack** (recovered from the logs signal — this is the whole point; older builds
61
+ dropped it). Read the stack, then read the `spans` waterfall to see which
62
+ downstream call (db, LLM, fetch) failed or was slow. Name the failing line.
63
+
64
+ **Latency or error rate is spiking ("why is p95 up?").** This is the exploration
65
+ loop:
66
+ 1. `GET /metrics/timeseries?minutes=<window>` — confirm the spike and its window.
67
+ 2. `GET /explore?dimension=route&metric=p95_ms` — which route owns the p95? Then
68
+ pivot the dimension: `status`, `http_status`, `http_method`, `env` — keep
69
+ slicing until one value dominates the metric.
70
+ 3. Pull a couple of the offending traces (`/events` filtered to that route, then
71
+ `/traces/<id>`) and read their spans + attributes. State **what the bad
72
+ requests share** (a route, a status code, a slow db/LLM span, a deploy env).
73
+
74
+ **Cost is climbing.** `GET /llm/cost/timeseries` (when) + `GET /llm/cost` (which
75
+ model). A single expensive model or a runaway call count is usually the story.
76
+
77
+ **"Is anything already watching this?"** `GET /alerts` — configured triggers and
78
+ their firing history (with per-channel delivery). If a spike had no trigger,
79
+ suggest one (metric + threshold + window).
80
+
81
+ ## Reasoning discipline
82
+
83
+ - **Lead with the stack.** For any error, quote `bundle.message` and the top of
84
+ `bundle.stack` before theorizing — it usually names the cause outright.
85
+ - **Isolate before explaining.** Don't explain an aggregate ("p95 is 800ms");
86
+ slice with `explore` until you have the specific cohort, then explain that.
87
+ - **Attribute, don't guess.** "These slow requests are all `http_status=503` on
88
+ `POST /pay`" beats "the payment service might be slow."
89
+ - **Weighted counts.** Metric aggregates are sampling-weighted (Analytics Engine
90
+ adaptively samples under load) — treat counts as estimates, ratios as sound.
91
+ - **Say what you couldn't see.** Arbitrary custom attributes live in the trace
92
+ payload, not the group-by dimensions; if the cohort is defined by a custom
93
+ field, note that you inferred it from individual traces, not an aggregate.