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>
192 lines
6.0 KiB
Python
192 lines
6.0 KiB
Python
"""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]
|