ward-kdl: glitchtip spec verb (provisioning+read) for bulk project/DSN setup #170

Closed
opened 2026-06-18 07:30:53 +00:00 by coilysiren · 5 comments
Owner

Goal

Add a glitchtip spec-driven verb surface to ward-kdl so we can drive the self-hosted GlitchTip API (Sentry-compatible error tracking) as audited ward verbs - and, crucially, bulk-provision ~20 projects + their DSNs instead of hand-clicking each one in the UI.

GlitchTip is live on kai-server (tailnet phase-1). Plan + topology: coilyco-flight-deck/infrastructure:docs/glitchtip-deploy-plan.md.

What exists already (no action needed)

  • Live API, OpenAPI 3.1, django-ninja. Spec at https://glitchtip.<tailnet>.ts.net/api/openapi.json (unauthenticated). 99 paths / 154 ops. TokenAuth = HTTP bearer.
  • SSM params (all minted):
    • /glitchtip/api-token - bearer token, scopes org:read team:admin project:read project:write project:admin event:read. Verified: authed GET /api/0/organizations/ -> 200, unauth -> 401.
    • /glitchtip/base-url - https://glitchtip.<tailnet>.ts.net (opaque host, SecureString - that's why base-url is SSM-resolved, not literal).

Deliverable

Author cmd/ward-kdl/ward-kdl.glitchtip.guardfile.kdl plus its committed spec lock + reference doc, following the existing ward-kdl.tailscale.guardfile.kdl (OpenAPI 3.1, bearer, SSM) and ward-kdl.forgejo.guardfile.kdl (the action block) as the closest models.

Guardfile skeleton:

wrap ward-kdl ops glitchtip {
    spec glitchtip.openapi.json
    base-url { value ssm "/glitchtip/base-url" }   // block form - host is opaque
    auth bearer { value ssm "/glitchtip/api-token" }

    // orgs (org:read)
    can get org
    can list org
    never create org { message "org creation is human-only; do it in the UI on first login" }
    never delete org { message "org deletion is irreversible and human-only" }

    // teams (team:admin) - needed because projects are created under a team
    can get team
    can list team
    can create team

    // projects (project:read/write/admin) - the bulk-provisioning surface.
    // create resolves under /teams/{org}/{team}/projects/ - pin the op if convention misses.
    can get project
    can list project
    can create project { op "apps_projects_api_create_project" }
    can edit project
    can delete project { describe "irreversible: deletes the project and its events" }

    // project keys / DSNs (project:admin)
    can list project-key
    can get project-key
    can create project-key
    can delete project-key

    // read-only triage (event:read)
    can list issue
    can get issue
    can list event
    can get event

    // ingest is SDK-only, never a ward verb
    never store event { message "event ingest is the SDK's job, not ward" }

    // payoff action: stand up one repo's project end to end and emit its DSN
    action provision-project {
        describe "Create a project under a team and mint its DSN in one shot."
        // inputs: org slug, team slug, project name/slug, platform
        // calls: create project -> create project-key -> surface the DSN
    }
}

Acceptance criteria

  1. ward-kdl --guardfile cmd/ward-kdl/ward-kdl.glitchtip.guardfile.kdl lock succeeds: writes glitchtip.openapi.lock.json (pruned to granted ops) and refreshes specverb.lock.
  2. ward-kdl ops glitchtip org list returns the orgs (empty list is fine pre-onboarding) - proves auth + base-url + bearer wiring end to end.
  3. provision-project action creates a project + key and prints the DSN.
  4. gen/skew clean. Reference doc docs/ward-kdl.glitchtip.guardfile.md written. Conventional commit, push to canonical Forgejo, closes this issue.

Environment caveat (read before dispatching)

lock, skew, and the live org list / provision-project checks require tailnet egress to glitchtip.<tailnet>.ts.net from wherever the headless runner executes. If the ephemeral container has no tailnet route, either run the dispatch on a tailnet-connected host or pre-stage the spec: a current snapshot is on kais-macbook-pro at /tmp/glitchtip-openapi.json (commit it as glitchtip.openapi.json and gen works offline; only live calls need the tailnet).

Out of scope

Wiring DSNs into each repo's secrets (separate follow-up once the 20 projects exist), and phase-2 public ingress (re-point base-url to glitchtip.coilysiren.me then).

## Goal Add a `glitchtip` spec-driven verb surface to `ward-kdl` so we can drive the self-hosted GlitchTip API (Sentry-compatible error tracking) as audited ward verbs - and, crucially, **bulk-provision ~20 projects + their DSNs** instead of hand-clicking each one in the UI. GlitchTip is live on kai-server (tailnet phase-1). Plan + topology: `coilyco-flight-deck/infrastructure:docs/glitchtip-deploy-plan.md`. ## What exists already (no action needed) - **Live API**, OpenAPI 3.1, django-ninja. Spec at `https://glitchtip.<tailnet>.ts.net/api/openapi.json` (unauthenticated). 99 paths / 154 ops. `TokenAuth` = HTTP **bearer**. - **SSM params** (all minted): - `/glitchtip/api-token` - bearer token, scopes `org:read team:admin project:read project:write project:admin event:read`. Verified: authed `GET /api/0/organizations/` -> 200, unauth -> 401. - `/glitchtip/base-url` - `https://glitchtip.<tailnet>.ts.net` (opaque host, SecureString - that's why base-url is SSM-resolved, not literal). ## Deliverable Author `cmd/ward-kdl/ward-kdl.glitchtip.guardfile.kdl` plus its committed spec lock + reference doc, following the existing `ward-kdl.tailscale.guardfile.kdl` (OpenAPI 3.1, bearer, SSM) and `ward-kdl.forgejo.guardfile.kdl` (the `action` block) as the closest models. Guardfile skeleton: ```kdl wrap ward-kdl ops glitchtip { spec glitchtip.openapi.json base-url { value ssm "/glitchtip/base-url" } // block form - host is opaque auth bearer { value ssm "/glitchtip/api-token" } // orgs (org:read) can get org can list org never create org { message "org creation is human-only; do it in the UI on first login" } never delete org { message "org deletion is irreversible and human-only" } // teams (team:admin) - needed because projects are created under a team can get team can list team can create team // projects (project:read/write/admin) - the bulk-provisioning surface. // create resolves under /teams/{org}/{team}/projects/ - pin the op if convention misses. can get project can list project can create project { op "apps_projects_api_create_project" } can edit project can delete project { describe "irreversible: deletes the project and its events" } // project keys / DSNs (project:admin) can list project-key can get project-key can create project-key can delete project-key // read-only triage (event:read) can list issue can get issue can list event can get event // ingest is SDK-only, never a ward verb never store event { message "event ingest is the SDK's job, not ward" } // payoff action: stand up one repo's project end to end and emit its DSN action provision-project { describe "Create a project under a team and mint its DSN in one shot." // inputs: org slug, team slug, project name/slug, platform // calls: create project -> create project-key -> surface the DSN } } ``` ## Acceptance criteria 1. `ward-kdl --guardfile cmd/ward-kdl/ward-kdl.glitchtip.guardfile.kdl lock` succeeds: writes `glitchtip.openapi.lock.json` (pruned to granted ops) and refreshes `specverb.lock`. 2. `ward-kdl ops glitchtip org list` returns the orgs (empty list is fine pre-onboarding) - proves auth + base-url + bearer wiring end to end. 3. `provision-project` action creates a project + key and prints the DSN. 4. `gen`/`skew` clean. Reference doc `docs/ward-kdl.glitchtip.guardfile.md` written. Conventional commit, push to canonical Forgejo, `closes` this issue. ## Environment caveat (read before dispatching) `lock`, `skew`, and the live `org list` / `provision-project` checks require **tailnet egress to `glitchtip.<tailnet>.ts.net`** from wherever the headless runner executes. If the ephemeral container has no tailnet route, either run the dispatch on a tailnet-connected host or pre-stage the spec: a current snapshot is on kais-macbook-pro at `/tmp/glitchtip-openapi.json` (commit it as `glitchtip.openapi.json` and `gen` works offline; only live calls need the tailnet). ## Out of scope Wiring DSNs into each repo's secrets (separate follow-up once the 20 projects exist), and phase-2 public ingress (re-point base-url to `glitchtip.coilysiren.me` then).
Member

🛫 ward pre-flight: NO-GO

ward agent claude headless ran a pre-flight feasibility read on this issue before detaching a fire-and-forget run, and the agent judged it NO-GO - it should not be carried unattended until a human weighs in.

headless container lacks tailnet egress to glitchtip and the spec snapshot isn't in the clone, so lock/live-call/provision acceptance criteria can't be met or verified — needs a tailnet-connected runner or the spec committed first.

No container was launched. Review the issue (clarify the scope, resolve the unknown, or split it), then re-dispatch - ward agent claude headless <ref> --no-preflight skips this gate once you've decided it's good to go.

full pre-flight read

This is a guardfile-authoring task on ward-kdl with clear models to follow (tailscale + forgejo guardfiles), so the authoring itself is tractable. The blocker is the environment caveat: acceptance criteria 1-3 (lock, live org list, provision-project) all require tailnet egress to glitchtip.<tailnet>.ts.net, which a fire-and-forget ephemeral container almost certainly lacks. The documented fallback — pre-staging /tmp/glitchtip-openapi.json — lives on kais-macbook-pro, not in the fresh clone I'd get, so I can't even gen offline. I'd be able to write the guardfile and reference doc but couldn't satisfy or verify the locking and live-call criteria unattended, which means I couldn't honestly carry it to a verified merge.

NO-GO: headless container lacks tailnet egress to glitchtip and the spec snapshot isn't in the clone, so lock/live-call/provision acceptance criteria can't be met or verified — needs a tailnet-connected runner or the spec committed first.


Posted automatically by ward agent claude headless pre-flight (ward#147, ward#149).

— Claude (she/her), via ward agent

### 🛫 ward pre-flight: NO-GO `ward agent claude headless` ran a pre-flight feasibility read on this issue before detaching a fire-and-forget run, and the agent judged it **NO-GO** - it should not be carried unattended until a human weighs in. > headless container lacks tailnet egress to glitchtip and the spec snapshot isn't in the clone, so lock/live-call/provision acceptance criteria can't be met or verified — needs a tailnet-connected runner or the spec committed first. No container was launched. Review the issue (clarify the scope, resolve the unknown, or split it), then re-dispatch - `ward agent claude headless <ref> --no-preflight` skips this gate once you've decided it's good to go. <details><summary>full pre-flight read</summary> This is a guardfile-authoring task on `ward-kdl` with clear models to follow (tailscale + forgejo guardfiles), so the authoring itself is tractable. The blocker is the environment caveat: acceptance criteria 1-3 (`lock`, live `org list`, `provision-project`) all require tailnet egress to `glitchtip.<tailnet>.ts.net`, which a fire-and-forget ephemeral container almost certainly lacks. The documented fallback — pre-staging `/tmp/glitchtip-openapi.json` — lives on kais-macbook-pro, not in the fresh clone I'd get, so I can't even `gen` offline. I'd be able to write the guardfile and reference doc but couldn't satisfy or verify the locking and live-call criteria unattended, which means I couldn't honestly carry it to a verified merge. NO-GO: headless container lacks tailnet egress to glitchtip and the spec snapshot isn't in the clone, so lock/live-call/provision acceptance criteria can't be met or verified — needs a tailnet-connected runner or the spec committed first. </details> --- Posted automatically by `ward agent claude headless` pre-flight (ward#147, ward#149). <!-- ward-preflight-nogo --> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Member

Clarifying context (added for dispatch)

Downstream consumer is now tracked: infrastructure#395 - the Sentry retirement / DSN cutover. The "wiring DSNs into each repo's secrets" out-of-scope follow-up here == #395. This surface is what unblocks it.

The "~20 projects" is the ceiling; the currently-live producers are fewer and already mapped to existing /sentry-dsn/* SSM SecureStrings. Name the GlitchTip project slugs to mirror the param slug so the cutover is a pure value-swap (put-parameter --overwrite the GlitchTip DSN into the matching key):

  • backend/sentry-dsn/backend
  • eco-mcp-app (eco-app, fused) ↔ /sentry-dsn/eco-mcp-app
  • galaxy-gen/sentry-dsn/galaxy-gen
  • website/sentry-dsn/website
  • kai-server (thermal-heartbeat host cron) ↔ /sentry-dsn/kai-server
  • retiring, no project needed: eco-spec-tracker, eco-telemetry (folded into eco-app); coily (repo archived, not a Sentry producer)

So provision-project's doc/examples should use these slugs, and the bulk run only needs ~5 live projects today (more as new services instrument).

Spec snapshot refreshed today on kais-macbook-pro at /tmp/glitchtip-openapi.json (OpenAPI 3.1.0, 99 paths, title "GlitchTip API") for the offline-gen fallback. Live unauth spec is <base-url>/api/openapi.json (<base-url> = SSM /glitchtip/base-url); live calls need tailnet egress per the caveat above.

## Clarifying context (added for dispatch) **Downstream consumer is now tracked: [infrastructure#395](https://forgejo.coilysiren.me/coilyco-flight-deck/infrastructure/issues/395)** - the Sentry retirement / DSN cutover. The "wiring DSNs into each repo's secrets" out-of-scope follow-up here == #395. This surface is what unblocks it. **The "~20 projects" is the ceiling; the currently-live producers are fewer and already mapped to existing `/sentry-dsn/*` SSM SecureStrings.** Name the GlitchTip project slugs to **mirror the param slug** so the cutover is a pure value-swap (`put-parameter --overwrite` the GlitchTip DSN into the matching key): - `backend` ↔ `/sentry-dsn/backend` - `eco-mcp-app` (eco-app, fused) ↔ `/sentry-dsn/eco-mcp-app` - `galaxy-gen` ↔ `/sentry-dsn/galaxy-gen` - `website` ↔ `/sentry-dsn/website` - `kai-server` (thermal-heartbeat host cron) ↔ `/sentry-dsn/kai-server` - retiring, **no project needed**: `eco-spec-tracker`, `eco-telemetry` (folded into eco-app); `coily` (repo archived, not a Sentry producer) So `provision-project`'s doc/examples should use these slugs, and the bulk run only needs ~5 live projects today (more as new services instrument). **Spec snapshot refreshed today** on kais-macbook-pro at `/tmp/glitchtip-openapi.json` (OpenAPI 3.1.0, 99 paths, title "GlitchTip API") for the offline-`gen` fallback. Live unauth spec is `<base-url>/api/openapi.json` (`<base-url>` = SSM `/glitchtip/base-url`); live calls need tailnet egress per the caveat above.
Member

🔒 Reserved by ward agent claude — container ward-ward-issue-170-claude-74906a0f on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-24T03:51:45Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-ward-issue-170-claude-74906a0f` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-24T03:51:45Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Member

🔒 Reserved by ward agent claude — container ward-ward-issue-170-claude-b6332d19 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-24T03:52:36Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-ward-issue-170-claude-b6332d19` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-24T03:52:36Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Member

🔒 Reserved by ward agent claude — container ward-ward-issue-170-claude-72119c38 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-24T04:35:36Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-ward-issue-170-claude-72119c38` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-24T04:35:36Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#170
No description provided.