Repository navigation
feat: krater v1 with proposal review, weave sign-in, skypilot budgets, slack and gallery - #1
Draft
Adambomb210 wants to merge 126 commits into
Draft
Adambomb210 wants to merge 126 commits into
Adambomb210 wants to merge 126 commits into
Conversation
Revises the Sep 24 Project Ganymede portal spec after checking it against the Weave codebase and the SkyPilot docs and source: - Budget enforcement moves into Krater (SkyPilot admin policy + spend reconciler); SkyPilot has no dollar-budget feature - Roles come from a proposed Weave groups claim + directory API - Submission is gated on full Slack membership (weave#118) - Reviews attach to immutable revisions; budget is an append-only ledger Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
SkyPilot's oauth2-proxy integration is two env vars on the API server, not a Helm-only feature, so members can sign in with Weave on the Docker Compose deployment. Replace the per-project token proposal with private per-project workspaces plus Weave sign-in limited to ganymede:member, and note that the Weave claims/directory work now exists on a branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
pyproject.toml/uv.lock pin Python 3.12 and the runtime/dev dependencies (FastAPI, SQLAlchemy 2, psycopg3, Alembic, pydantic-settings, procrastinate, ruff/pytest). krater/config.py adds a pydantic-settings Settings (env prefix KRATER_) with the Weave integration fields, and refuses stub Weave mode or a default secret key in production. krater/db.py adds the engine/session factory and the declarative Base with a naming convention for constraints. Empty krater/services and krater/weave packages scaffold the layout CLAUDE.md documents for later waves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
One module per aggregate under krater/models/, matching docs/SPEC.md's Data model section: User, Project, ProjectRevision, Review, BudgetEntry, SpendSnapshot, ApprovalPolicy, AuditEvent (GalleryEntry stays a query, not a table). UUID primary keys, integer-cents money columns, timezone-aware server-defaulted timestamps via shared mixins, and Postgres ARRAY/JSONB columns where SPEC calls for them. Every closed-set field from SPEC is a Python enum stored via a shared pg_enum() helper (sa.Enum(..., native_enum=False, create_constraint=True, validate_strings=True, values_callable=...)), so adding a value later is a plain migration, not a Postgres ALTER TYPE, and the stored strings match the enum members' .value (not their .name). AuditEvent.action stays a plain string since SPEC gives it as an open, "e.g." list. Project.current_revision_id/approved_revision_id and ProjectRevision.project_id form a genuine circular FK between the two tables; the two FKs on Project use use_alter=True and their relationships use post_update=True. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
env.py takes its database URL from krater.config.get_settings() (KRATER_DATABASE_URL),
never from alembic.ini, and wires target_metadata to krater.db.Base.metadata after
importing krater.models so every table is registered.
Two hand-reviewed migrations:
- 59b55982f8a6: all eight Krater tables. Started from `alembic revision
--autogenerate` and then fixed by hand: autogenerate had rendered a redundant
explicit CheckConstraint for every enum column on top of the one each
sa.Enum(create_constraint=True) already attaches, which made `upgrade()` fail
with DuplicateObject; and it had rendered the circular
projects <-> project_revisions foreign keys inline inside
create_table('projects', ...), which fails because project_revisions doesn't
exist yet at that point. The circular FKs are now added via
op.create_foreign_key(...) once both tables exist, and dropped again before
either table in downgrade().
- 67a1656dcb6d: procrastinate's own schema, applied via
procrastinate.schema.SchemaManager.get_schema() (the documented approach for
projects that manage migrations with a tool other than procrastinate's own
CLI) so `alembic upgrade head` is the only migration path anyone needs to run.
downgrade() drops procrastinate's tables/functions/types explicitly, since it
doesn't ship a "drop everything" helper.
Verified: `alembic upgrade head`, `downgrade base` and `upgrade head` again all
succeed against krater_dev, and a post-upgrade `alembic check` reports no
further autogenerate diff.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
krater/worker/app.py builds a procrastinate App on the same Postgres database (PsycopgConnector, converted from Krater's SQLAlchemy-style KRATER_DATABASE_URL to a plain libpq conninfo string) and registers one periodic task, heartbeat, that logs once a minute. Run with: `uv run procrastinate --app=krater.worker.app.app worker`. Verified: ran the worker for a few seconds against krater_dev -- it connects, registers itself, imports the task module, and the periodic scheduler picks up heartbeat -- then stopped it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
krater/web/app.py's create_app() factory wires SessionMiddleware (keyed on
settings.secret_key), mounts /static, and includes the pages router: GET /
renders a placeholder home page and GET /healthz runs SELECT 1 through the
get_session dependency before reporting {"status": "ok"}.
Jinja2 templates live in krater/web/templates/ with a base.html layout (header
with "Krater" and a placeholder nav, centered content column) styled by
hand-written CSS in krater/web/static/ using the Patchwork palette from
CLAUDE.md, no framework or CDN, and no horizontal scroll at phone width.
Verified live: ran uvicorn in the background against krater_dev, curled / and
/healthz, then stopped it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
…smoke test tests/conftest.py forces KRATER_DATABASE_URL to KRATER_TEST_DATABASE_URL (default postgresql+psycopg://root:root@localhost:5432/krater_test) before anything under krater is imported, runs `alembic upgrade head` once per session via the engine fixture, and gives each test a db_session on a SAVEPOINT (begin_nested, restarted on every "after_transaction_end") so code under test can commit freely while the outer transaction still rolls back the test's writes at teardown. The client fixture is a FastAPI TestClient with get_session overridden to that db_session. tests/web/test_pages.py covers GET /healthz and GET /. tests/models/test_smoke.py creates a user, a project, two revisions (one superseded, one approved) and a budget entry, then reads them all back -- including relationships -- through a fresh query. Verified: `uv run pytest` passes (3 passed), and running it twice in a row leaves krater_test's `users` table empty both times, confirming the rollback. Empty tests/services and tests/worker packages scaffold future waves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
…section Dockerfile: python:3.12-slim, uv copied from Astral's official image (no architecture pinned, so it builds for amd64 and arm64 under buildx), a non-root krater user, and a venv-first layer for build caching. docker-compose.yml: db (postgres:16, healthcheck, named volume), migrate (one-shot `alembic upgrade head`, depends on db's healthcheck), and portal/worker (depend on migrate completing successfully), all reading KRATER_/POSTGRES_ env vars from .env. .env.example documents every variable, including the docker-compose-only POSTGRES_*/PORT ones. .github/workflows/ci.yml: on push/PR, uv sync, ruff check, ruff format --check, and pytest against a postgres:16 service container. README.md gets a Development section (uv setup, db creation, running the app/ worker, lint/format/test, docker compose), keeping the existing doc links. Note: docs/weave-integration.md has a small pre-existing, uncommitted whitespace-only diff (comment spacing in a code sample) that predates this work and is unrelated to it; left as-is rather than folded into this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
alembic check reported that the next autogenerated migration would drop procrastinate's tables, since they come from its SQL rather than our models. Filter them out with include_name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
…ache krater/weave/types.py: WeaveIdentity (from a verified id_token) and WeaveUser (from the directory API), per docs/weave-integration.md. krater/weave/client.py: the WeaveClient Protocol every adapter implements (authorization_url, exchange_code, get_user, get_user_by_slack_id, list_users_in_group). krater/weave/errors.py: WeaveAuthError (bad state/code/id_token) vs. WeaveUnavailableError (Weave unreachable or erroring), so callers can branch on which one it was instead of on Weave's own exceptions or status codes. krater/weave/cache.py: a small in-process TTLCache, used by LiveWeaveClient's directory lookups (next commit) to absorb bursts of repeat calls. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
…factory LiveWeaveClient (krater/weave/live.py): OIDC against a real Weave. - Discovery (/.well-known/openid-configuration) and its JWKS are each fetched once per process and cached on the instance. - authorization_url builds a PKCE S256 challenge from the verifier and requests `openid profile email groups slack`. - exchange_code posts to the token endpoint with the client secret and PKCE verifier, then verifies the id_token with joserfc: RS256 signature against the JWKS, plus iss/aud/exp/nonce via JWTClaimsRegistry. Any failure there, or a 4xx from the token endpoint, raises WeaveAuthError; a 5xx or network error raises WeaveUnavailableError. - get_user/get_user_by_slack_id/list_users_in_group call the directory API with the X-Api-Key header; 404 becomes None. get_user and get_user_by_slack_id are cached for 60s via the new TTLCache. - The httpx.Client is injectable, so tests can pass one built on httpx.MockTransport instead of hitting the network. StubWeaveClient (krater/weave/stub.py): no network calls. Loads krater/weave/stub_users.json (or `weave_stub_users_file`) into memory; exchange_code treats the authorization code as the chosen user's `sub` directly, and authorization_url points at the local /auth/stub picker. The bundled fixture has 7 users: two plain members, two reviewers, an admin, a non-member, and one inactive member. krater/weave/__init__.py: get_weave_client(), an lru_cache'd FastAPI dependency that picks Live vs. Stub from settings.weave_mode, overridable in tests. Re-exports the public types/errors/client so `from krater.weave import ...` is the only import other code needs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Creates or updates a User by weave_sub, refreshing display_name, email, slack_user_id, groups_cached (sorted, for stable diffs) and last_login_at from a verified WeaveIdentity. Flushes but doesn't commit -- the router calling it (krater/web/routers/auth.py, next commit) owns the transaction. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
krater/web/csrf.py: a per-session CSRF token (get_or_create_csrf_token),
a csrf_token() Jinja global bound to the current request via
jinja2.pass_context, and verify_csrf_token, a dependency that 403s a POST
missing or mismatching the csrf_token form field. Wired into
krater/web/templates.py so every template can call {{ csrf_token() }}.
krater/web/deps.py: current_user (optional), require_user (redirects
signed-out HTML requests to /login?next=..., open-redirect-safe since next is
always this same app's own path), session_actor (cached groups, display only)
and fresh_actor (a live WeaveClient.get_user() call; 403s if the user is
inactive or no longer ganymede:member), plus require_reviewer/require_admin
built on it. Docstrings say which to use when; later waves should reach for
fresh_actor (or stricter) for anything state-changing.
krater/web/routers/auth.py:
- GET /login stores a fresh state/nonce/PKCE verifier and a validated `next`
in the session, then redirects to authorization_url.
- GET /auth/callback checks state, exchanges the code, 403s with a friendly
page for a non-member, otherwise upserts the user, commits, clears and
re-seeds the session (session fixation) with the user id/sub/display name,
and redirects to `next`.
- POST /logout (CSRF-protected) clears the session.
- GET /auth/stub (stub mode only, 404s under `live`) lists the fixture users
to click, linking to the callback with code=<sub>.
base.html now shows the signed-in user's name and a sign-out button, or a
"Sign in" link otherwise; new templates/auth/{not_a_member,stub_picker}.html
and a few additive CSS rules follow the existing hand-written styles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
tests/weave/test_live_client.py: LiveWeaveClient against a fake Weave built on httpx.MockTransport, with a real RSA key generated in the test. Covers a good token; wrong aud/iss/nonce, an expired token and a bad signature each failing with WeaveAuthError; a rejected code (WeaveAuthError) vs. a 5xx (WeaveUnavailableError); discovery/JWKS fetched only once; directory parsing including a missing/null slack_id and a 404; the X-Api-Key header; and the directory cache hit. tests/weave/test_stub_client.py: the bundled fixture covers every role, exchange_code/get_user/get_user_by_slack_id/list_users_in_group behave, and an unknown code raises WeaveAuthError. tests/weave/test_upsert.py: upsert_user_from_identity creates a User, then updates the same row (by weave_sub) on a second call. tests/web/test_auth.py: the full stub flow (login -> stub pick -> callback -> signed-in page showing the name -> logout) via TestClient, plus the reject cases: bad/missing state, a non-member (403, no User row created), a CSRF-less logout (403, still signed in), an absolute-URL and a protocol-relative `next` (both fall back to "/"), and /auth/stub 404ing once weave_mode is "live". tests/web/test_deps.py: fresh_actor built from a fake WeaveClient picks up a group removed after login and rejects an inactive or unknown-to-Weave user; require_admin needs the admin group specifically; and one true TestClient-driven check that require_user's FastAPI dependency chain actually redirects to /login?next=... (not just that the plain-Python-call version does). `uv run ruff check . && uv run ruff format --check .` clean; `uv run pytest` passes: 47 passed (3 pre-existing + 44 new). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
ApprovalPolicy.required_group must be checked against the groups a reviewer held at review time, not their current (possibly since-changed) groups. Snapshot Weave groups onto Review.reviewer_groups so approval_policy can check membership without re-deriving history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
- errors.py: DomainError base plus NotAllowed, InvalidState, ValidationFailed (field-keyed), NotFound. - audit.py: record() writes an AuditEvent for admin overrides. - budget.py: ceiling_cents/latest_spend_cents/remaining_cents plus the internal add_entry() every ledger write goes through. - approval_policy.py: stage_for(), applicable_policies(), is_satisfied() and an explain() helper for the UI, per the stacking/budget-tier/ required-group rules in docs/SPEC.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Browsers normalize /\evil.example to //evil.example and strip tabs and newlines, so those slipped past the // check and made next an open redirect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Covers the full happy path (proposal -> approve -> amend, including a budget decrease -> approve -> completion -> approve/reclaim), resubmit after reject with old reviews not counting, self-review (submitter and credited builder) and double-review blocks, non-reviewer rejection, multi-approval/required-group/budget-tier ApprovalPolicy rows, admin overrides with audit and budget-floor rules, withdraw/reclaim, and invalid-transition/validation errors across projects.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Two concurrent approvals from the same reviewer (a double-click, or a retried Slack action) could both pass record_review's "already reviewed" SELECT before either had inserted its row. Add a unique constraint on reviews(revision_id, reviewer_id), and have record_review turn the resulting IntegrityError into the same InvalidState the pre-check normally raises, via a savepoint around just the insert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Adds the full server-rendered UI on top of the existing services and
auth:
- Home (`/`): signed-out intro + gallery link; signed-in "My
projects" with status badges, "New proposal", and shortcuts to the
review queue (with a count) and admin for those roles.
- `/projects/new`, `/projects/{id}`, `/projects/{id}/edit`: draft
creation and editing (dollars entered by hand, converted to cents),
the project detail page (budget, policy explanation, escaped
write-up with line breaks, revision history with reviews), and every
contextual action form (submit, amend, complete, withdraw, review,
admin override), each hidden unless the viewer may actually use it.
The service remains the real enforcement.
- `/reviews` and `/admin`: the review queue and the admin overview
(projects by status, the approval-policy table).
- `/gallery`, `/gallery/{id}`: the public gallery of completed
projects, no auth.
- Domain errors map per spec: `NotAllowed` -> 403, `NotFound` -> 404
(so a project's existence never leaks to a viewer who isn't its
submitter, a reviewer, or an admin), `ValidationFailed` -> the form
re-rendered with field errors (422), `InvalidState` -> a flash
message and a redirect back to the project.
- New helpers: `web/money.py` (dollars <-> cents, tested),
`web/forms.py` (tags and credited-builder-email parsing),
`web/flash.py` (session-backed flash messages), `web/textfmt.py`
(escape-then-`<br>` write-up rendering).
- Tests: a `login_as` fixture and stub-user constants in
tests/conftest.py, per-route happy-path and authz tests (a
non-member can't create, a member can't view another's project, a
non-reviewer/self-submitter can't approve, a non-admin can't adjust
budget, POSTs without CSRF fail), and one HTTP-only end-to-end test
through the whole workflow into the gallery.
Verified manually against the running app in stub mode (create,
submit, review, amend, complete, gallery) in addition to the
automated tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
They're rendered as hrefs, including on the public gallery, so a javascript: or data: URL would have run script for every visitor who clicked it. Validate in the service layer, which every entry point uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Hands-on spike against a real local SkyPilot 0.13.0 API server to verify the assumptions in docs/skypilot-integration.md before building on them: admin-policy wire format (client- AND server-side calls, double-JSON encoding, workspace often absent from the payload), workspace CRUD payload shapes and allowed_users semantics, service-account/oauth2-proxy auth, cost_report row shape, and teardown API shapes. Several findings contradict the design doc's assumptions (default role is admin not user; the policy endpoint must be reachable from client machines, not just the SkyPilot container) and are flagged for follow-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
S3-compatible storage in docker-compose (plus a one-shot bucket-init service) until a long-term provider is chosen. SeaweedFS rather than MinIO, whose community edition stopped publishing images in 2025. Credentials come from the env; the S3 gateway config is written at container start so no secret lives in the repo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015etbcYs4SKqPxsnY7Pr2p6
Krater now looks like Weave and patchworklabs.org: the quilt palette and lattice background, stitched (dashed) cards and buttons, Geist and Shantell Sans (self-hosted, OFL), patch-colored status badges, a binding strip in the header and a hero card on the signed-out home page. Dark mode re-maps the same tokens, following the OS or a toggle like Weave's. theme.js is a plain external script because Krater's CSP allows no inline scripts. Template classes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A role removed in Weave used to keep working for up to 60 seconds, because the directory lookup that authorizes an action could come from the client's cache. `get_user` now takes `fresh=True`, which `fresh_actor` passes for anything but GET/HEAD and the Slack click path gets by default. Page views can still reuse a cached answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The project service refuses anything but an absolute http(s) link, but the create and edit routes never caught it: an `ssh://` repo link was an Internal Server Error that lost the whole form. They now check links up front (`link_errors`) alongside the budget, and the forms show the message under the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cents columns are 32-bit, so a budget from $21,474,836.48 up overflowed them and the form returned a 500. `parse_dollars` now refuses anything past $1,000,000.00 (either sign) with a field error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Admin overrides skipped the self-review rule, so an admin could submit a proposal, approve it and add budget with nobody else signing off. On a project they submitted or are credited on, an admin can no longer decide it or add budget; cutting or reclaiming budget and withdrawing still work. The project page says so instead of offering the form, and the staging runbook now asks for a second account to approve a test proposal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A zero adjustment wrote an empty ledger row and audit event and posted "+0.00" to the project's Slack channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SkyPilot applies `max_hourly_cost`, and Vast a bid, to each node, so a 20-node launch could cost 20 times the cap. The gate now caps each node at the hourly cap divided by `num_nodes`. Vast only rents single machines, so this guards any other cloud a workspace might allow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both are compute clouds in SkyPilot 0.13.0's registry and were missing from the per-project and default workspace deny-lists, which left them launchable. Tests now check both lists against the full registry. Existing project workspaces pick the change up on the next workspace sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A withdrawn or completed project kept its SkyPilot workspace until the reconciler tore it down, and its page kept telling the team how to launch into it. The hint now shows only for the statuses the launch gate accepts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate showed a project's title, status and spend to whoever the request's `user` block named, but anyone with the shared policy token can write any email there. Rejections for an inactive or out-of-budget project are now the same for everyone and name nothing about the project; the team reads the details on its Krater page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catch-all error handler runs outside RequestIdMiddleware, so crash logs said `request_id=None`; it now restores the id from `request.state`, sends it as X-Request-ID and shows it on the production error page. Rejected launches logged their workspace and user through `extra=`, which neither log format prints; they're in the message now, with the request name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Browsers got a raw `{"detail": "Weave is unavailable"}` when Weave couldn't be reached. They now get a page
saying nothing was changed and to try again shortly; API callers keep the JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`/docs`, `/redoc` and `/openapi.json` listed every route, the SkyPilot policy hook and Slack endpoints included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The icon is a quilt rosette in the brand palette. Four estimator fields in a row cut off the "Pricing basis" options. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`projects.max_hourly_cost_cents`: an admin-set hourly price cap for a project's SkyPilot launches. Null, as it is for every existing project, means the global `KRATER_SKYPILOT_MAX_HOURLY_COST_CENTS`, now described as the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate caps `max_hourly_cost` and Vast bids at the project's cap (`hourly_cap_cents`: its own, else the default), still split across `num_nodes`. A request whose workspace isn't a Krater project is no longer price-capped, only given autodown. Every request that can provision is enforced and rejected without a project workspace, so this only reaches the `validate`/ `optimize` hops that arrive without `active_workspace`. One `sky launch` passes through the gate several times, and capping that hop at the default could cut a project's higher cap before the launch hop saw it; a test chains the hops to check the cap survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new admin form on the project page sets the cap, or puts it back to the default when left blank. Like the other overrides it needs a reason, writes an AuditEvent with the old and new caps and posts to the project's Slack channel. It's refused once a project is finished, and an admin can only lower the cap on their own project. The launch hint now tells the team the cap that applies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A project can have its own cap now, so /pricing marks rows over the default per-machine cap, and the run-when-cheap guide points members at their project page for theirs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`latest` has already moved past the image staging was tested on. Screenshot storage stays on self-hosted SeaweedFS for now; bump the pin only after a staging run on the new version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both sides added a `projects` column with a migration on top of b7ba424bfaeb. The hourly-cap migration (e3f1a7c2b9d4) now follows the allowed-users one (5f3b46edeb95), so there's a single head. Both sides also gave the Weave e2e fixture users Krater's app roles; this keeps Jasper's version of the provisioning script, the live tests and their docs, which have run against Weave main. The docs keep both the uncached role checks for actions and the reconciler's workspace offboarding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both branches added a migration on top of 5f3b46edeb95: the per-project hourly cap (e3f1a7c2b9d4) here and quilt_outbox (83af68b415ca) there. quilt_outbox now revises e3f1a7c2b9d4, so there is one head and staging, already at e3f1a7c2b9d4, upgrades in one step. The krater/weave/live.py conflict was two neighbouring edits: the module docstring and the scope/TTL constants. Both are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First full version of Krater, the Project Ganymede portal. The spec is
docs/SPEC.md. Start withdocs/HANDOFF.md.This PR stays a draft until the maintainer approves it. The last step before that is the staging run in
docs/dev/staging.md.What is in it
rolesclaim (member,reviewer,admin) and checks it again through the Weave directory API before each state change. A stub mode gives fake users for development./pricingpage and a budget estimator.skypilotprofile adds the SkyPilot 0.13.0 API server and oauth2-proxy.Depends on Weave
All Weave support for roles is merged on Weave
mainand deployed to Weave staging (tracking issue patchworklabsorg/weave#151):groupsscope and claimrolesclaimSetup still to do on each Weave instance:
member,reviewerandadminon the Krater app page.directoryto the Krater app's scopes.Krater uses the
slackscope that Weave already has. It identifies every user by the PWL id (sub).Verification
alembic checkare clean. CI is green. pip-audit found no known vulnerabilities.live, skipped in CI) ran against a real Weave, a real SkyPilot 0.13.0 server and a real SeaweedFS. The live Weave tests now cover app roles and the directory API: 9 of 9 pass against Weavemain.skypilotprofile.Not verified yet
All of these are steps in
docs/dev/staging.md:sky api loginround trip;