Projects: shareable, registry-backed, container-mounted; drop ticket board
Nightly Build / build (push) Successful in 6m27s
Nightly Build / build (push) Successful in 6m27s
Rework projects from single-user leftovers into shareable endeavours.
- DB: move `projects` from the owner bucket to the registry (system.db,
not encrypted); add `owner_user_id` + `slug` (drop free `path`); new
`project_members(project_id, user_id, can_write)` mirroring
`shared_folder_members`. Drop `project_tickets` entirely. Only user↔agent
conversations stay encrypted (per-user DB) — each member keeps a private
project chat. Registry home dissolves the cross-DB-FK problem.
- Filesystem/container: on disk `{WD}/projects/{owner_userid}/{slug}`,
agent/container path `projects/{owner_username}/{slug}`. Two-segment routing
in UserFs (ProjectMount + host_base_and_tail arm) and a second loop in
build_user_fs; read-only members get a :ro mount. Reuse the shared-folder
remount machinery (refresh_user_shared_folders -> refresh_user_mounts).
- Remove the ticket system: ProjectTicketManager, UserContext.tickets, its
wiring, and the project_tickets references in scheduled_jobs/cron.
- API: repoint handlers to the registry pool + membership scoping. Sharing is
self-service — owner or any write-member may add/remove members and set
read/write; only the owner deletes; the owner cannot be removed. New
POST/DELETE /api/projects/{id}/members[/{user_id}]. Seed `@fs_any allow
projects/*`; build_runtime_run_context sets working_directory to the agent
path and drops the host-path allow_fs_writes.
- Frontend: create form without the free path field, owner/read-write badges;
the detail page becomes header + description + sharing panel + Open chat + a
file-explorer placeholder (the future primary surface). i18n en/it/fr.
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
//! | `user-memory/…` | SQLite (the user's pool) — routed *before* this |
|
||||
//! | `shared-memory/…` | SQLite (`system.db`) — routed *before* this |
|
||||
//! | `shared/{X}/…` | host `{WD}/shared/{X}`, mount `{home}/shared/{X}` |
|
||||
//! | `projects/{O}/{S}`| host `{WD}/projects/{owner_userid}/{S}`, mount `{home}/projects/{O}/{S}` (O = owner username) |
|
||||
//! | `~/…`, relative | host `{WD}/homes/{userid}`, mount `{container_home}`|
|
||||
//!
|
||||
//! `UserFs` is a **pure value type** with no filesystem access: it carries the
|
||||
@@ -33,6 +34,25 @@ pub struct SharedMount {
|
||||
pub can_write: bool,
|
||||
}
|
||||
|
||||
/// One project folder mounted into a user's container. Unlike a shared folder its
|
||||
/// agent path has **two** segments — `projects/{owner_username}/{slug}` — because a
|
||||
/// project is namespaced by its owner (two members can each own a `budget`). The host
|
||||
/// path keys on the owner's stable **userid**, the agent/container path on the
|
||||
/// (mutable) **username**.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjectMount {
|
||||
/// The owner's username — the first agent-visible segment under `projects/`.
|
||||
pub owner_username: String,
|
||||
/// The project slug — the second agent-visible segment.
|
||||
pub slug: String,
|
||||
/// Absolute host directory that backs it (`{WD}/projects/{owner_userid}/{slug}`).
|
||||
pub host: PathBuf,
|
||||
/// Where it is mounted inside the container (`{home}/projects/{owner_username}/{slug}`).
|
||||
pub container: PathBuf,
|
||||
/// Whether this member may write to it.
|
||||
pub can_write: bool,
|
||||
}
|
||||
|
||||
/// The filesystem view of one user: their private home plus the shared folders
|
||||
/// they belong to, and the container those are mounted into.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -46,6 +66,8 @@ pub struct UserFs {
|
||||
pub container_home: PathBuf,
|
||||
/// Shared folders this user can reach, in name order.
|
||||
pub shared: Vec<SharedMount>,
|
||||
/// Projects this user can reach (owned + shared-with-them), by owner then slug.
|
||||
pub projects: Vec<ProjectMount>,
|
||||
}
|
||||
|
||||
impl UserFs {
|
||||
@@ -55,6 +77,7 @@ impl UserFs {
|
||||
container_name: impl Into<String>,
|
||||
container_home: PathBuf,
|
||||
shared: Vec<SharedMount>,
|
||||
projects: Vec<ProjectMount>,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_id: user_id.into(),
|
||||
@@ -62,6 +85,7 @@ impl UserFs {
|
||||
container_name: container_name.into(),
|
||||
container_home,
|
||||
shared,
|
||||
projects,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,12 +94,22 @@ impl UserFs {
|
||||
self.shared.iter().find(|m| m.name == name)
|
||||
}
|
||||
|
||||
/// Look up a project mount by its owner username + slug (the two agent segments).
|
||||
pub fn project_mount(&self, owner_username: &str, slug: &str) -> Option<&ProjectMount> {
|
||||
self.projects
|
||||
.iter()
|
||||
.find(|m| m.owner_username == owner_username && m.slug == slug)
|
||||
}
|
||||
|
||||
/// The bind mounts for `docker create`: `(host, container, writable)`, home first.
|
||||
pub fn mounts(&self) -> Vec<(PathBuf, PathBuf, bool)> {
|
||||
let mut out = vec![(self.home_host.clone(), self.container_home.clone(), true)];
|
||||
for m in &self.shared {
|
||||
out.push((m.host.clone(), m.container.clone(), m.can_write));
|
||||
}
|
||||
for m in &self.projects {
|
||||
out.push((m.host.clone(), m.container.clone(), m.can_write));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -100,14 +134,25 @@ impl UserFs {
|
||||
let mount = self.shared_mount(name)?;
|
||||
Some((mount.host.clone(), tail.to_string()))
|
||||
}
|
||||
Some("projects") => {
|
||||
// Two segments: `projects/{owner_username}/{slug}/{tail…}`.
|
||||
let rest = parts.next().unwrap_or("");
|
||||
let mut seg = rest.splitn(3, ['/', '\\']);
|
||||
let owner = seg.next().unwrap_or("");
|
||||
let slug = seg.next().unwrap_or("");
|
||||
let tail = seg.next().unwrap_or("");
|
||||
let mount = self.project_mount(owner, slug)?;
|
||||
Some((mount.host.clone(), tail.to_string()))
|
||||
}
|
||||
_ => Some((self.home_host.clone(), stripped.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an agent path to its **container** path (pure, lexical): `~`/relative →
|
||||
/// under `container_home`; `shared/{X}` → under `container_home/shared/{X}`; an
|
||||
/// already-absolute path is taken as a container path as-is. Used to set the
|
||||
/// working directory of an `execute_cmd` inside the container.
|
||||
/// under `container_home`; `shared/{X}` and `projects/{O}/{S}` → under
|
||||
/// `container_home/…` (they mirror the container layout); an already-absolute path
|
||||
/// is taken as a container path as-is. Used to set the working directory of an
|
||||
/// `execute_cmd` inside the container.
|
||||
pub fn to_container(&self, agent_path: &str) -> PathBuf {
|
||||
let p = Path::new(agent_path);
|
||||
if p.is_absolute() {
|
||||
|
||||
Reference in New Issue
Block a user