Files
ai-agent/backend/schemas/users.py
Gabriel Vidal deb01427d5 feat(backend): New projects — owner and guests start a project from a template
newproject/: the policy (guestDefaults.newProjects — quota, templates, the
ones the owner approves first, visibility, level, domain — and a per-guest
createProjects + address bucket), the New project form built from it, and the
flow: POST /api/new-project/form (the run's guesses as defaults) → submit →
shape/policy and host checks (a problem sends a corrected form back) → an
owner ask for a backend template → sidecar /scaffold → grant → /relocate +
resume the same session inside the project with an app-authored turn.
SpawnBody.newProject starts a guest's run in ~/projects/.new-project/<uid>
(staging_scope: /tmp only); /api/me reports newProjects; the thread follows
ConversationDetail.continuedIn.

Fix: a guest's turn into an existing conversation resumed with no cwd, so the
sidecar ran it in the lab checkout (its CLAUDE.md loaded) — it now resumes
where the session lives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:42:07 +02:00

192 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Guests: ``/api/me``, the owner's user admin, conversation sharing."""
from __future__ import annotations
from typing import Optional
from .base import Schema
__all__ = [
"ProjectGrant", "NotifyPrefs", "Me", "GuestUser", "UsersResponse",
"LoginLink", "ShareCandidate", "ConversationShare", "GuestShortcut",
"GuestDefaults", "GuestSkill", "GuestSkillsResponse",
"NewProjectAccess", "NewProjectPolicy", "NewProjectFormResult",
"NewProjectRequest", "NewProjectRequests",
]
class ProjectGrant(Schema):
slug: str
level: str # "view" | "chat" | "ship"
class NotifyPrefs(Schema):
channels: list[str] # "in_app" | "email" | "webpush"
class NewProjectAccess(Schema):
"""Whether the caller may start a project from a template now (the
composer's *+ New project* pick), and where it would be hosted."""
allowed: bool
reason: Optional[str] = None # why not, when not allowed
remaining: Optional[int] = None # projects left in the quota (None: no quota)
bucket: Optional[str] = None # a guest's address bucket: <name>.<bucket>.<domain>
domain: str = "gabvdl.xyz"
class NewProjectPolicy(Schema):
"""What guests may create (Settings → Users → Every guest → New projects)."""
quota: int = 10 # projects per guest, all time
templates: list[str] # templates offered to guests
approve: list[str] # of those, the ones the owner approves first
visibility: list[str] # "public" | "password" | "geo"
visibilityDefault: str = "public"
level: str = "ship" # the creator's grant on their new project
domain: str = "gabvdl.xyz" # <name>.<bucket>.<domain>
class Me(Schema):
"""Who the browser is: the owner, or a guest with their grants."""
id: str
role: str # "owner" | "guest"
name: str
email: Optional[str] = None
readonly: bool = False
projects: list[ProjectGrant]
models: list[str]
budgetUsd: Optional[float] = None
spentUsd: Optional[float] = None
notify: Optional[NotifyPrefs] = None
mail: bool = False # outbound mail configured (login links can be mailed)
surface: Optional[str] = None # "admin" | "guest"
# A guest's starting prompt shortcuts (what Reset goes back to); None for
# the owner, whose defaults are built into the frontend.
defaultShortcuts: Optional[list["GuestShortcut"]] = None
# The owner looking through this guest's eyes (Settings → Users →
# Impersonate): the PWA shows a "back to admin" bar.
impersonating: bool = False
newProjects: Optional[NewProjectAccess] = None
class GuestUser(Schema):
id: str
email: str
name: str
role: str
projects: dict[str, str]
models: list[str]
budgetUsd: float
notify: NotifyPrefs
disabled: bool
createdAt: str
lastLoginAt: Optional[str] = None
sessions: int = 0
# What their conversations cost: this month (what the budget is checked
# against) and all time, and how many conversations they started.
spentUsd: float = 0
totalSpentUsd: float = 0
conversations: int = 0
# New projects: may they create one, under which address bucket
# (blank ⇒ their id), and how many they created so far.
createProjects: bool = False
bucket: str = ""
createdProjects: int = 0
class UsersResponse(Schema):
users: list[GuestUser]
levels: list[str]
channels: list[str]
defaultModels: list[str]
mail: bool
guestUrl: Optional[str] = None
class LoginLink(Schema):
url: str
sent: bool
expiresInS: int
class ShareCandidate(Schema):
id: str
name: str
level: Optional[str] = None
class AudienceMember(Schema):
id: str # "owner" or a guest's users.json id
name: str
class ConversationShare(Schema):
sessionId: str
owner: Optional[str] = None
project: Optional[str] = None
participants: list[str]
candidates: list[ShareCandidate]
# Does the caller hear about this conversation (its pushes, asks, forms)?
notifying: bool = False
# Everyone who does — who started it, who it's shared with, the owner if
# they follow it, minus whoever muted it.
audience: list[AudienceMember] = []
class GuestShortcut(Schema):
"""A prompt shortcut in the frontend's ``PromptShortcut`` shape."""
id: str
label: str
icon: str = "Sparkles"
guideline: str = ""
defaultOn: bool = False
labelOff: Optional[str] = None
guidelineOff: Optional[str] = None
children: Optional[list[str]] = None
triggers: Optional[list[str]] = None
shared: Optional[bool] = None
class GuestDefaults(Schema):
"""What the owner sets for every guest at once (Settings → Users)."""
promptShortcuts: list[GuestShortcut]
sharedSkills: list[str] # `name` (repo) | `<project>/<name>`
newProjects: NewProjectPolicy
class NewProjectFormResult(Schema):
"""``POST /api/new-project/form`` — the form the requester fills in."""
formId: str
focusUrl: str
requester: str # "owner" | a guest's id
class NewProjectRequest(Schema):
"""One *New project* form and what became of it."""
id: str # the form id
sessionId: str
requester: str
status: str # pending | approval | creating | created | failed | declined | retried | cancelled
template: Optional[str] = None
slug: Optional[str] = None
url: Optional[str] = None
repo: Optional[str] = None
visibility: Optional[str] = None
error: Optional[str] = None
createdAt: str
updatedAt: str
class NewProjectRequests(Schema):
requests: list[NewProjectRequest]
class GuestSkill(Schema):
"""A skill a guest's run can use — the composer's `/` mentions."""
name: str # the invocation name (`shared:<name>` when shared)
description: str = ""
triggerWords: list[str] = []
source: str # "shared" | <project slug>
class GuestSkillsResponse(Schema):
skills: list[GuestSkill]