anvilsign in

collin/anvil

1//! Persisted domain types, defined as Toasty models. Tables are created from
2//! these definitions via [`crate::db`]'s `push_schema`.
3//!
4//! Foreign keys are kept as plain scalar fields (`owner_id`, `user_id`) and
5//! queried explicitly, rather than declaring Toasty relations — simpler and a
6//! good fit for our small schema.
7
8/// A registered user account.
9#[derive(Debug, toasty::Model)]
10pub struct User {
11 #[key]
12 #[auto]
13 pub id: i64,
14 #[unique]
15 pub username: String,
16 pub email: String,
17 /// Argon2 PHC-format password hash. Empty for an account that has only
18 /// ever signed in through the identity provider — no password can hash to
19 /// it, so [`crate::users::verify_password`] refuses every guess.
20 pub password_hash: String,
21 pub is_admin: bool,
22 /// Unix timestamp (seconds) of account creation.
23 pub created_at: i64,
24 /// The OIDC `sub` claim this account is linked to, or empty if it isn't.
25 /// Accounts are keyed on `sub` rather than email because `sub` is the one
26 /// claim the provider promises never changes. New columns go last so
27 /// `ALTER TABLE ADD COLUMN` on existing databases agrees with the
28 /// fresh-schema column order.
29 pub sso_sub: String,
30}
31
32/// A hosted repository, owned by a [`User`].
33#[derive(Debug, toasty::Model)]
34pub struct Repository {
35 #[key]
36 #[auto]
37 pub id: i64,
38 #[index]
39 pub owner_id: i64,
40 pub name: String,
41 pub description: String,
42 pub is_private: bool,
43 /// Short name of the default branch, e.g. `main`.
44 pub default_branch: String,
45 pub created_at: i64,
46 /// Push-mirror remote: after every successful push, refs are mirrored to
47 /// this git URL (`git push --mirror`). Empty disables mirroring. New
48 /// columns go last so `ALTER TABLE ADD COLUMN` on existing databases
49 /// agrees with the fresh-schema column order.
50 pub mirror_url: String,
51 /// SHA-256 hash of the preview image extracted from README (empty if none).
52 pub preview_image_hash: String,
53 /// Primary language detected in the repository (e.g. "Rust", empty if no files).
54 pub primary_language: String,
55 /// JSON array of language percentages: [{"lang": "Rust", "percent": 75.5}, ...].
56 pub languages_json: String,
57}
58
59/// A CI run for a pushed commit.
60///
61/// `status` is one of `queued`, `running`, `success`, `failure` (a step exited
62/// non-zero), or `error` (the runner itself failed). `started_at`/`finished_at`
63/// are 0 until they occur.
64#[derive(Clone, Debug, toasty::Model)]
65pub struct CiRun {
66 #[key]
67 #[auto]
68 pub id: i64,
69 #[index]
70 pub repo_id: i64,
71 /// Full commit SHA the run is for.
72 pub commit: String,
73 /// Short branch name that was pushed (e.g. `main`).
74 pub ref_name: String,
75 pub status: String,
76 /// Accumulated run log.
77 pub log: String,
78 pub created_at: i64,
79 pub started_at: i64,
80 pub finished_at: i64,
81}
82
83/// One artifact produced by a CI run, stored on disk under
84/// `data_dir/artifacts/{repo_id}/{commit}/` (see `docs/ci-artifacts.md`).
85///
86/// `commit` is denormalized from the run so per-commit lookups (the
87/// latest-on-branch alias) don't join through runs.
88#[derive(Clone, Debug, toasty::Model)]
89pub struct CiArtifact {
90 #[key]
91 #[auto]
92 pub id: i64,
93 #[index]
94 pub run_id: i64,
95 #[index]
96 pub repo_id: i64,
97 /// Full commit SHA the producing run was for.
98 pub commit: String,
99 /// Declared artifact name (unique within a pipeline, not globally).
100 pub name: String,
101 /// Total size in bytes (summed over files for directory artifacts).
102 pub size: i64,
103 /// Directory artifact (stored as a tarball, or extracted when `browse`).
104 pub is_dir: bool,
105 /// Served as a browsable static site rather than a download.
106 pub browse: bool,
107 /// JSON object of metadata-extractor key → output.
108 pub meta: String,
109 pub created_at: i64,
110}
111
112/// One agent session: a long-lived container running tmux plus an agent CLI
113/// against a repository, attachable from the browser.
114///
115/// `status` is one of `starting`, `running`, `exited` (the agent finished on
116/// its own), `failed` (the supervisor could not start or keep it), or `reaped`
117/// (a timeout, an operator stop, or a server restart took it). The transcript
118/// is *not* a column — it lives at
119/// [`storage::session_transcript_path`](crate::storage::session_transcript_path),
120/// because a terminal stream grows continuously and [`CiRun::log`] is rewritten
121/// whole on every append.
122#[derive(Clone, Debug, toasty::Model)]
123pub struct AgentSession {
124 #[key]
125 #[auto]
126 pub id: i64,
127 #[index]
128 pub repo_id: i64,
129 /// Account that started the session; its access decides who may attach.
130 pub user_id: i64,
131 #[index]
132 pub status: String,
133 /// `interactive` (a human drives it) or `autonomous` (started with a
134 /// prompt and left to run).
135 pub kind: String,
136 /// Branch name the session was started from, e.g. `main`.
137 pub base_ref: String,
138 /// Full commit SHA the workspace was seeded at.
139 pub base_commit: String,
140 /// Branch the agent's work lands on (`agent/{id}`). Empty until the
141 /// session has something to push.
142 pub branch: String,
143 /// Opening prompt for an autonomous session; empty for an interactive one.
144 pub prompt: String,
145 /// Docker container id, empty before it is created.
146 pub container_id: String,
147 /// Image the container was created from, recorded so a session's
148 /// provenance survives a config change.
149 pub image: String,
150 pub created_at: i64,
151 pub started_at: i64,
152 pub finished_at: i64,
153 /// Last time a viewer was attached, driving the idle sweep.
154 pub last_attach_at: i64,
155 /// Container exit code once it stops; 0 until then.
156 pub exit_code: i64,
157 /// Supervisor-facing failure detail, empty when there is none.
158 pub error: String,
159 /// Comma-separated names of user secrets (`UserSecret`) this session
160 /// opted into at start time. May name one that isn't currently
161 /// unlocked — launch fails with a clear error rather than silently
162 /// starting without it. Recorded (not just consumed) so a session's page
163 /// can show what it could reach. New column: goes last, see `User.sso_sub`.
164 pub secret_names: String,
165}
166
167/// An issue on a repository. `number` is the user-facing per-repo sequence
168/// (`#1`, `#2`, …); `id` stays the global key. `state` is `open` or `closed`.
169///
170/// Numbering is assigned as max+1 at creation; with a single server process
171/// (our deployment shape) that cannot race.
172#[derive(Clone, Debug, toasty::Model)]
173pub struct Issue {
174 #[key]
175 #[auto]
176 pub id: i64,
177 #[index]
178 pub repo_id: i64,
179 pub number: i64,
180 pub title: String,
181 /// Markdown body (may be empty).
182 pub body: String,
183 pub author_id: i64,
184 pub state: String,
185 pub created_at: i64,
186 /// Bumped on comments and state changes, for "recently active" ordering.
187 pub updated_at: i64,
188}
189
190/// A comment on an [`Issue`].
191#[derive(Clone, Debug, toasty::Model)]
192pub struct IssueComment {
193 #[key]
194 #[auto]
195 pub id: i64,
196 #[index]
197 pub issue_id: i64,
198 pub author_id: i64,
199 /// Markdown body.
200 pub body: String,
201 pub created_at: i64,
202}
203
204/// A web login session, keyed by an opaque random token stored in a cookie.
205#[derive(Debug, toasty::Model)]
206pub struct Session {
207 #[key]
208 pub token: String,
209 #[index]
210 pub user_id: i64,
211 pub created_at: i64,
212 /// Unix timestamp (seconds) after which the session is invalid.
213 pub expires_at: i64,
214}
215
216/// An uploaded file (e.g. an image pasted into the file editor), stored
217/// outside git at `data_dir/attachments/{repo_id}/{hash}` so large binaries
218/// never enter the repository's history. Markdown carries only the serve URL.
219///
220/// Content-addressed: `hash` is the lowercase hex SHA-256 of the bytes, so the
221/// same content uploaded twice to a repo dedupes to one file. Lookups and GC
222/// scope by `repo_id`, which also gates serving by the repo's read access.
223#[derive(Clone, Debug, toasty::Model)]
224pub struct Attachment {
225 #[key]
226 #[auto]
227 pub id: i64,
228 #[index]
229 pub repo_id: i64,
230 /// Lowercase hex SHA-256 of the content — both the dedup key and the path
231 /// component under the repo's attachment directory.
232 pub hash: String,
233 /// MIME type to serve the bytes with (e.g. `image/png`).
234 pub content_type: String,
235 pub size: i64,
236 /// The user who first uploaded this content to the repo.
237 pub uploader_id: i64,
238 pub created_at: i64,
239}
240
241/// A personal access token: a long-lived, scoped bearer credential for
242/// non-browser API clients (e.g. tooling that fetches attachments). Only the
243/// SHA-256 hash of the token is stored; the plaintext is shown once at
244/// creation. A PAT is least-privilege by design — its `scopes` bound what it
245/// can do, and the only scope today (`read`) authenticates safe (GET/HEAD)
246/// requests only, so a leaked token can never mutate.
247#[derive(Clone, Debug, toasty::Model)]
248pub struct ApiToken {
249 #[key]
250 #[auto]
251 pub id: i64,
252 #[index]
253 pub user_id: i64,
254 /// A human label for the token (e.g. "claude"), for listing/revoking.
255 pub name: String,
256 /// Lowercase hex SHA-256 of the token; the lookup key.
257 #[unique]
258 pub token_hash: String,
259 /// Comma-separated scopes granted to this token (e.g. `read`).
260 pub scopes: String,
261 pub created_at: i64,
262}
263
264/// A registered SSH public key, used to authenticate git-over-SSH connections.
265#[derive(Debug, toasty::Model)]
266pub struct SshKey {
267 #[key]
268 #[auto]
269 pub id: i64,
270 #[index]
271 pub user_id: i64,
272 pub title: String,
273 /// Canonical SHA256 fingerprint, e.g. `SHA256:…`.
274 #[unique]
275 pub fingerprint: String,
276 /// Normalized OpenSSH public-key line.
277 pub content: String,
278 pub created_at: i64,
279}
280
281/// A per-repository secret, stored only as a sealed envelope.
282///
283/// The server cannot read `envelope`: it is encrypted to the owner's
284/// ssh-ed25519 keys by the client that set it (see [`crate::secrets`]).
285/// `recipients` denormalizes the envelope's fingerprints so the UI can tell,
286/// without opening anything, which secrets a newly registered key still cannot
287/// decrypt — those need `anvild secret rekey`.
288#[derive(Clone, Debug, toasty::Model)]
289pub struct RepoSecret {
290 #[key]
291 #[auto]
292 pub id: i64,
293 #[index]
294 pub repo_id: i64,
295 /// Environment variable name, e.g. `DEPLOY_TOKEN`. Unique per repository.
296 pub name: String,
297 /// The sealed envelope, as JSON (`anvil-secret-v1`).
298 pub envelope: String,
299 /// Comma-separated SSH fingerprints the envelope is sealed to.
300 pub recipients: String,
301 pub created_at: i64,
302 pub updated_at: i64,
303}
304
305/// A per-account secret, stored only as a sealed envelope, sealed to *its
306/// own owner's* ssh-ed25519 keys rather than a repository's — see
307/// [`RepoSecret`] for the shared envelope shape and the crypto notes.
308///
309/// Consumed by that same account's agent sessions, which opt in to specific
310/// names by name at start time (`AgentSession::secret_names`) — never
311/// injected blanket into every session, since a session can be steered by
312/// repo content it reads (prompt injection).
313#[derive(Clone, Debug, toasty::Model)]
314pub struct UserSecret {
315 #[key]
316 #[auto]
317 pub id: i64,
318 #[index]
319 pub user_id: i64,
320 /// Unique per account. Also the environment variable name for `kind ==
321 /// "env"`; just an identifier otherwise.
322 pub name: String,
323 /// `"env"`, `"file"`, or `"json"` — see [`crate::secrets::kind`].
324 pub kind: String,
325 /// Destination under the session's `$HOME` for `"file"`/`"json"`
326 /// (e.g. `.claude/.credentials.json`); empty for `"env"`. Named
327 /// `dest_path` rather than `path`: toasty's derive macro reserves `path`
328 /// as a generated identifier on every model, and collides with a field
329 /// of that name.
330 pub dest_path: String,
331 /// jq-style assignment path within `dest_path`'s JSON (e.g.
332 /// `.oauthAccount.token`); only set, and only meaningful, for `"json"`.
333 pub field: String,
334 /// The sealed envelope, as JSON (`anvil-secret-v1`).
335 pub envelope: String,
336 /// Comma-separated SSH fingerprints the envelope is sealed to.
337 pub recipients: String,
338 pub created_at: i64,
339 pub updated_at: i64,
340}
341
342/// Cached admin metrics computed periodically (e.g., disk usage snapshot).
343#[derive(Clone, Debug, toasty::Model)]
344pub struct AdminCache {
345 #[key]
346 #[auto]
347 pub id: i64,
348 /// Cache key (e.g., "disk_usage").
349 pub key: String,
350 /// JSON-encoded cached data.
351 pub value: String,
352 /// Unix timestamp (seconds) of when this snapshot was taken.
353 pub computed_at: i64,
354}