@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.
- package/README.md +126 -31
- package/REQUIREMENTS.md +18 -3
- package/dist/bin.cjs +1340 -436
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/chunk-OERLHVLH.js +2107 -0
- package/dist/chunk-OERLHVLH.js.map +1 -0
- package/dist/index.cjs +1353 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +314 -21
- package/dist/index.d.ts +314 -21
- package/dist/index.js +15 -1
- package/llms.txt +442 -54
- package/package.json +15 -6
- package/skills/odla/SKILL.md +43 -12
- package/skills/odla/references/build.md +65 -17
- package/skills/odla/references/sdks.md +36 -3
- package/skills/odla-migrate/SKILL.md +38 -13
- package/skills/odla-migrate/references/phase-0-preflight.md +0 -2
- package/skills/odla-migrate/references/phase-1-static.md +2 -4
- package/skills/odla-migrate/references/phase-2-db.md +25 -17
- package/skills/odla-migrate/references/phase-3-auth.md +106 -20
- package/skills/odla-migrate/references/phase-3b-user-sync.md +57 -0
- package/skills/odla-migrate/references/phase-4-ai.md +6 -5
- package/skills/odla-migrate/references/phase-5-cutover.md +47 -16
- package/skills/odla-migrate/references/secrets-map.md +26 -11
- package/skills/odla-migrate/references/troubleshooting.md +27 -13
- package/skills/odla-o11y-debug/SKILL.md +93 -0
- package/dist/chunk-UWT7C6VT.js +0 -1187
- package/dist/chunk-UWT7C6VT.js.map +0 -1
|
@@ -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:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`
|
|
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
|
|
30
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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.
|
|
28
|
-
`
|
|
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);
|
|
9
|
-
the app
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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` |
|
|
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
|
-
| `
|
|
14
|
-
| `
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
45
|
-
|
|
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
|
|
59
|
-
reused; schema/rules re-push).
|
|
60
|
-
`--rotate-
|
|
61
|
-
|
|
62
|
-
|
|
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;
|
|
67
|
-
|
|
68
|
-
`node_modules/@odla-ai/*/llms.txt`. Do not improvise around a safety
|
|
69
|
-
|
|
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.
|