Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ crates/
├── allmystuff-bridge # Inventory ──► graph Capabilities (+ presence summary)
├── allmystuff-session # live presence + the route offer/accept handshake + media frame types (audio/video/input/terminal/files/clipboard)
├── allmystuff-updater # self-update: release feed, SHA-256 verify, stage-then-apply
└── allmystuff-cli # `allmystuff` (opens the GUI) + scan / capabilities / update
├── allmystuff-cli # `allmystuff` (opens the GUI) + scan / capabilities / update
├── allmystuff-cec # the CEC service client: the typed HTTP contract, a transport-generic client, and an in-memory mock backend
├── allmystuff-cec-mock # `allmystuff-cec-mock` — a runnable reference CEC backend over the mock, for end-to-end testing
└── allmystuff-agent # `allmystuff-agent` — the headless CEC technician tool (sign in, go online, accept help sessions)
```

### allmystuff-inventory
Expand Down Expand Up @@ -574,6 +577,76 @@ carries traffic when something happens, never on a timer. The fleet roster
(it holds the grouping key) is only ever *handed* to fleet members; presence
goes to everyone.

## The CEC service

**CEC** — Critical Error Computing — is the company behind AllMyStuff, and the
hosted backend behind the two services advertised on allmystuff.works. The free
app needs none of it; an **optional account** unlocks:

- **Ask-for-Help** (Concierge) — a real CEC technician, one tap away.
- **Private Line** — "a venue of your own": CEC-hosted signaling/STUN/TURN
serving only your devices.

The app is a *client* of this backend exactly the way it's a client of the
daemon: `allmystuff-cec` mirrors the HTTP contract (the typed Rust DTOs in
`model.rs` **are** the contract — see `crates/allmystuff-cec/CONTRACT.md`)
without owning the server, and the Tauri backend (`gui/src-tauri/src/cec.rs`)
holds the `reqwest`-backed client plus the persisted account state
(`~/.myownmesh/allmystuff-cec.json`). A runnable reference backend
(`allmystuff-cec-mock`) and a headless agent tool (`allmystuff-agent`) round
out the picture, both built on the same client crate.

### Accounts

Optional, and passwordless: enter an email, get a one-time code. Identity proof
is the code; the same `verify` call **binds this device's mesh pubkey** to the
account, so the backend knows which nodes to provision a mesh for and pre-trust.
The token lives app-side (never the webview); the entitlements blob
(`{ hardware, private_line, concierge }`) drives the two product decisions —
whether to stand up the CEC mesh (`hardware || private_line || concierge`) and
whether Ask-for-Help is live (`concierge.is_some()`).

### The CEC mesh — one service node, agents behind it

When a customer has CEC hardware *or* a service, the app provisions a dedicated,
isolated network — `cec-customer-<hash>`, where `<hash>` is a stable, opaque
digest of the account id (no PII reaches the mesh). It rides the **existing
networks-as-venues machinery**: the backend's `MeshProvision` carries a venue
(its own signaling/STUN/TURN, plus a live venue-file URL the app adds as an
ordinary **remote venue**) and the bare pubkey of a single **CEC Service**
node. The app joins the network with `auto_approve`, assigns the CEC venue, and
pre-approves that one node in the roster.

The isolation is the point: because the network id is unique, its signaling
room (`SHA-256(app-id : network-id)`) is too, so the only peers a customer ever
sees there are their own devices and the one CEC Service node. **Individual
agents are never mesh peers** — the backend operates that single node identity
and bridges whichever technician is handling the customer to it. "Agents log in
through the app, but their connections are managed by the backend provider, not
the mesh engine" falls straight out of this: no MyOwnMesh changes were needed.

### Ask-for-Help — a help room exposed to agents

Pressing Ask-for-Help (the front-end's `cecAskHelp`) mints a host-side help
room on the CEC network — an ordinary virtual room (`CHANNEL_ROOMS`) with a
`room:{host}:cec-{nonce}` id, holding just this device and the CEC Service node
— then `POST`s it to the backend, which exposes the queued session to online
agents. An agent (via `allmystuff-agent` → the backend) accepts, the backend
joins the room as the CEC Service node, and the customer's existing room call
UI carries the session — screen share, control, voice, chat — over normal
routes. The customer polls `GET /v1/help/{id}` and watches it go
`queued → assigned → connected`. Nothing about the mesh's room plane changed;
the CEC help room is just a recognisably-named one.

### Testing

`allmystuff-cec` is fully testable without a network: the `MockBackend` is a
faithful in-memory implementation of the contract, driven through a socket-free
`MockTransport` in unit/integration tests (the customer and agent flows both),
and through a tiny HTTP shim in `allmystuff-cec-mock` for running the real app
and agent against it (`allmystuff-cec-mock --demo`). The mesh side is the app's
usual control-socket client; only the HTTP layer is new.

## Next milestones

- **Storage transport** over the same route pipe that audio, screen,
Expand Down
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ members = [
"crates/allmystuff-session",
"crates/allmystuff-updater",
"crates/allmystuff-cli",
"crates/allmystuff-cec",
"crates/allmystuff-cec-mock",
"crates/allmystuff-agent",
]
# The Tauri desktop app lives in its own Cargo workspace (`gui/src-tauri`)
# so a `cargo build --workspace` at the root stays fast and webview-free —
Expand All @@ -32,6 +35,7 @@ allmystuff-protocol = { path = "crates/allmystuff-protocol", version = "0.1.12"
allmystuff-bridge = { path = "crates/allmystuff-bridge", version = "0.1.12" }
allmystuff-session = { path = "crates/allmystuff-session", version = "0.1.12" }
allmystuff-updater = { path = "crates/allmystuff-updater", version = "0.1.12" }
allmystuff-cec = { path = "crates/allmystuff-cec", version = "0.1.12", default-features = false }

serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
29 changes: 29 additions & 0 deletions crates/allmystuff-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "allmystuff-agent"
description = "The CEC agent — a headless tool a Critical Error Computing technician uses to sign in, go online, and pick up Ask-for-Help sessions. Shares the allmystuff-cec contract; its live connection to a customer is brokered by the backend through the single CEC Service node."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true

[lib]
name = "allmystuff_agent"
path = "src/lib.rs"

[[bin]]
name = "allmystuff-agent"
path = "src/main.rs"

[lints.rust]
unsafe_code = "forbid"

[dependencies]
allmystuff-cec = { workspace = true, features = ["reqwest"] }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
123 changes: 123 additions & 0 deletions crates/allmystuff-agent/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! The reusable core of the CEC agent tool: persisted config plus the
//! transport-generic "watch" operation, kept out of `main.rs` so it can be
//! tested against the in-memory backend without a socket.

use std::path::{Path, PathBuf};

use allmystuff_cec::model::{AgentAssignment, HelpSession};
use allmystuff_cec::{CecClient, Error, Transport};
use serde::{Deserialize, Serialize};

/// Where the app family advertises its reference backend. Override with
/// `--backend` (e.g. point at a local `allmystuff-cec-mock`).
pub const DEFAULT_BACKEND: &str = "https://api.allmystuff.works";

/// The agent's persisted state: which backend, the session token, and a note
/// of who's signed in. Lives next to the mesh identity under `~/.myownmesh`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default = "default_backend")]
pub backend_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
}

fn default_backend() -> String {
DEFAULT_BACKEND.to_string()
}

impl Default for Config {
fn default() -> Self {
Config {
backend_url: default_backend(),
token: None,
email: None,
}
}
}

impl Config {
/// `~/.myownmesh/allmystuff-agent.json`, honouring `MYOWNMESH_HOME` exactly
/// like the daemon control socket does.
pub fn default_path() -> Option<PathBuf> {
let home = std::env::var_os("MYOWNMESH_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(PathBuf::from))?;
Some(home.join(".myownmesh").join("allmystuff-agent.json"))
}

/// Load config from `path`, or a default if it doesn't exist.
pub fn load(path: &Path) -> Self {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}

/// Persist to `path`, creating the parent directory.
pub fn save(&self, path: &Path) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let body = serde_json::to_string_pretty(self).unwrap_or_default();
std::fs::write(path, body)
}

pub fn is_signed_in(&self) -> bool {
self.token.is_some()
}
}

/// What one pass of `watch` found (and possibly took).
#[derive(Debug, Clone)]
pub struct WatchReport {
pub queued: Vec<HelpSession>,
pub accepted: Option<AgentAssignment>,
}

/// One pass of the agent loop: read the queue, and — when `accept_all` — take
/// the oldest waiting session, returning the assignment to act on.
///
/// The actual live link to the customer is the backend's job: it operates the
/// single CEC Service node on the customer's mesh and bridges this agent to
/// it. The agent tool's responsibility ends at "I've got it" — exactly the
/// "connections are managed by the backend provider, not the mesh engine"
/// split the product describes.
pub async fn watch_once<T: Transport>(
client: &CecClient<T>,
accept_all: bool,
) -> Result<WatchReport, Error> {
let queued = client.agent_queue().await?;
let mut accepted = None;
if accept_all {
if let Some(first) = queued.iter().min_by_key(|s| s.created_at) {
accepted = Some(client.accept_help(&first.id).await?);
}
}
Ok(WatchReport { queued, accepted })
}

/// A one-line summary of a help session for the console.
pub fn fmt_session(s: &HelpSession) -> String {
let topic = s.topic.as_deref().unwrap_or("(no topic)");
format!(
" {} · {} · {} · {}",
s.id,
s.customer_label,
status_word(s),
topic
)
}

fn status_word(s: &HelpSession) -> &'static str {
use allmystuff_cec::model::HelpStatus::*;
match s.status {
Queued => "waiting",
Assigned => "assigned",
Connected => "connected",
Ended => "ended",
Cancelled => "cancelled",
}
}
Loading
Loading