Side A is a substantial, coherent refactor that removes '@' sigil ambiguity from wire identities, splits canonicalization/identity concerns into dedicated modules, updates callers across server/CLI/tests, and adds regression tests validating the new invariant. Side B is a one-line placeholder file addition with no functional code, tests, or explanatory value.
constitution · epochs · watch · epoch 3
c_e2ee16c7ada5 (tommy-mor) vs c_d2fd40385778 (tommy-mor)
download prompt · raw event · cmp_6b78f72b3b890a
council reasoning
Commit A is a substantive architectural refactor: it extracts canonical_path/identity modules, enforces strict stored-form wire identity (no @), makes delegate optional, and stops the reducer from rewriting identities—clear lasting design. Commit B only adds a one-line seed.tdsl (“open webui”) with no structural or corrective impact.
Side A introduces a substantive architectural refactor by separating identity parsing and path canonicalization into dedicated modules, enforcing strict stored-form identities without '@' in APIs, making delegate identity optional, and updating the reducer and API boundaries to stop rewriting identities. It also propagates these semantics consistently across authentication, ingest, feed, HTML display, types, and tests, whereas Side B only adds a one-line seed file containing "open webui" with no evident functional impact.
sides
A — c_e2ee16c7ada5 (tommy-mor)
message
[80ad7753] refactor: split canonical_path and identity; strict wire identity without @ - Add canonical_path.rs (tag + item URL normalization) and identity.rs (parse_username/parse_agent; reject @ in API input). - Slim events.rs to event types only; reducer applies no identity rewriting. - JSON APIs return stored-form usernames and agent ids; HTML keeps @/@@ for display. - Optional delegate on ingest; CLI and tests use naked uuid:rig:model. Made-with: Cursor
diff preview
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 5ac8e289f2b7a366b5959d9338d02f9b546f408a..630c5dea1f78c0ec9bc53e6b96234a0dc75bb705 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -61,8 +61,8 @@ enum Command {
/// Example: --before 2026-06-01
#[arg(long, value_name = "DATE_OR_MS")]
before: Option<String>,
- /// Filter to posts from this actor (UUID prefix match).
- /// Example: --actor 4d9d6173
+ /// Filter to posts from this principal username (prefix match, stored form).
+ /// Example: --actor alice
#[arg(long, value_name = "PREFIX")]
actor: Option<String>,
/// Fetch a single post by its ingest ID (from --json output).
@@ -75,9 +75,8 @@ enum Command {
///
/// SYNTAX:
///
- /// Actor (required, once per document):
- /// @<uuid>:<rig>:<model>
- /// Example: @7a3b9c2d-1234-5678-90ab-cdef12345678:claudecode:anthropic/claude-sonnet
+ /// Identity: human comes from the bearer token; optional AI delegate from `--delegate`
+ /// (`uuid:rig:provider/model`). The document body is DSL only (items, votes, prose) — no `@` lines.
///
/// Thread (required, once per document):
/// #thread-tag
@@ -109,7 +108,7 @@ enum Command {
/// Example: ~/python > ~/rust { Python's simpler syntax reduces learning curve. }
///
/// Prose (optional, anywhere):
- /// Any line that doesn't start with @, #, or ~ is prose.
+ /// Any line that doesn't start with # or ~ (or `http`) is prose.
/// Prose is displayed in thread context but does not affect rankings or items.
/// Use prose to write blog posts, reasoning, or notes within your ingest.
///
@@ -125,8 +124,7 @@ enum Command {
/// EXAMPLES:
///
/// # From heredoc (recommended for agents)
- /// npx slugsocial ingest << 'EOF'
- /// @7a3b9c2d-1234-5678-90ab-cdef12345678:claudecode:anthropic/claude-sonnet
+ /// npx slugsocial ingest --delegate '7a3b9c2d-1234-5678-90ab-cdef12345678:claudecode:anthropic/claude-sonnet' << 'EOF'
/// #languages: Python vs Rust for systems programming
///
/// ~/languages/python { A high-level language with simple syntax and rich ecosystem. }
@@ -153,14 +151,9 @@ enum Command {
/// Thread identifier (public tag like "languages", without #).
#[arg(long, env = "SLUG_THREAD", default_value = "public", value_name = "THREAD")]
thread: String,
- /// Agent delegate identity (request form), e.g. @@uuid:rig:provider/model
- #[arg(
- long,
- env = "SLUG_DELEGATE",
- default_value = "@@00000000-0000-0000-0000-000000000000:cli:local/dev",
- value_name = "DELEGATE"
- )]
- delegate: String,
+ /// Agent delegate `uuid:rig:provider/model`. Omit for human-only ingests.
+ #[arg(long, env = "SLUG_DELEGATE", value_name = "DELEGATE")]
+ delegate: Option<String>,
/// Output as JSON for agent parsing
#[arg(long)]
json: bool,
@@ -174,14 +167,9 @@ enum Command {
/// Thread identifier (public tag like "languages", without #).
#[arg(long, env = "SLUG_THREAD", default_value = "public", value_name = "THREAD")]
thread: String,
- /// Agent delegate identity (request form), e.g. @@uuid:rig:provider/model
- #[arg(
- long,
- env = "SLUG_DELEGATE",
- default_value = "@@00000000-0000-0000-0000-000000000000:cli:local/dev",
- value_name = "DELEGATE"
- )]
- delegate: String,
+ /// Agent delegate `uuid:rig:provider/model`. Omit for human-only ingests.
+ #[arg(long, env = "SLUG_DELEGATE", value_name = "DELEGATE")]
+ delegate: Option<String>,
/// Output as JSON for agent parsing
#[arg(long)]
json: bool,
@@ -193,10 +181,10 @@ enum Command {
/// Useful for agents to catch up on activity after a context reset.
///
/// Examples:
- /// npx slugsocial feed @<uuid>:<rig>:<model>
- /// npx slugsocial feed @<uuid>:<rig>:<model> --since 2026-01-01
+ /// npx slugsocial feed tommy
+ /// npx slugsocial feed tommy --since 2026-01-01
Feed {
- /// Actor identifier (@uuid:rig:model)
+ /// Principal username (stored form)
#[arg(value_name = "ACTOR")]
actor: String,
/// Override the lower bound. Accepts Unix ms or YYYY-MM-DD.
@@ -455,7 +443,7 @@ fn print_rank_history_response(resp: &slug_types::RankHistoryResponse) {
label,
);
for v in &e.caused_by {
- println!(" {} {} {} {}", v.a, v.ratio, v.b, v.actor.as_deref().map(|a| format!(" (@{})", a)).unwrap_or_default());
+ println!(" {} {} {} {}", v.a, v.ratio, v.b, v.actor.as_deref().map(|a| format!(" ({})", a)).unwrap_or_default());
if !v.body.is_empty() {
println!(" {}", v.body.lines().next().unwrap_or(&v.body).trim());
}
@@ -1116,7 +1104,7 @@ async fn main() -> Result<()> {
IdentityCmd::Start { rig, model, json } => {
let client = http_client()?;
let uuid = uuid::Uuid::new_v4().to_string();
- let delegate = format!("@@{}:{}:{}", uuid, rig, model);
+ let delegate = format!("{uuid}:{rig}:{model}");
let start: PendingSessionStartResponse = expect_json(
client
diff --git a/server/src/api/auth.rs b/server/src/api/auth.rs
index cb0faa29b834931e2c2b2f5c174c875e2e2e9346..995ce4a61d29b024c399c656134f541ecfd880cf 100644
--- a/server/src/api/auth.rs
+++ b/server/src/api/auth.rs
@@ -12,10 +12,8 @@ use tokio::sync::RwLock;
use crate::{
api::helpers::{api_error, now_ms, sha256_hex},
- events::{
- canonicalize_username, validate_agent_format, validate_username,
- Event, TokenIssued, UserRegistered,
- },
+ events::{Event, TokenIssued, UserRegistered},
+ identity::{parse_agent, parse_username},
html::{auth_complete_page, auth_signed_in_fragment, choose_username_error_fragment, choose_username_page},
state::{AppState, PendingSession},
};
@@ -77,9 +75,9 @@ fn verify_token(reduced: &crate::reducer::ReducerState, bearer: &str) -> Result<
Ok(username)
}
-fn issue_token_for_user(username: &str) -> (String, TokenIssued, String) {
- // Returns: (bearer, event, canonical_username)
- let canonical_user = canonicalize_username(username);
+/// `stored_username` must already be in persisted shape (lowercase slug, no `@`).
+fn issue_token_for_user(stored_username: &str) -> (String, TokenIssued) {
+ let username = stored_username.to_string();
let token_id = {
let mut id = String::new();
let alphabet = b"abcdefghijklmnopqrstuvwxyz0123456789";
@@ -103,13 +101,13 @@ fn issue_token_for_user(username: &str) -> (String, TokenIssued, String) {
let bearer = format!("slug_{token_id}_{secret}");
let event = TokenIssued {
ts: now_ms(),
- username: canonical_user.clone(),
+ username: username.clone(),
token_id,
token_hash,
salt,
issued_via: "oauth".to_string(),
};
- (bearer, event, canonical_user)
+ (bearer, event)
}
#[derive(Debug, Deserialize)]
@@ -207,7 +205,7 @@ pub async fn get_auth_callback(Query(q): Query<AuthCallbackQuery>, State(state):
s.provider = Some("google".to_string());
s.provider_id = Some(sub.clone());
if let Some(username) = existing {
- let (bearer, token_event, canon_user) = issue_token_for_user(&username);
+ let (bearer, token_event) = issue_token_for_user(&username);
// append token event
let ev = Event::TokenIssued(token_event);
if let Err(err) = state.event_log.append(&ev).await {
@@ -217,7 +215,7 @@ pub async fn get_auth_callback(Query(q): Query<AuthCallbackQuery>, State(state):
let mut reduced = reduced_arc.write().await;
reduced.apply_event(ev);
}
- s.complete = Some((canon_user, bearer));
+ s.complete = Some((username, bearer));
return Redirect::temporary(&format!("{public_url}/auth/complete")).into_response();
}
}
@@ -251,9 +249,10 @@ pub async fn post_choose_username(
State(state): State<AppState>,
Form(form): Form<ChooseUsernameForm>,
) -> impl IntoResponse {
- if let Err(msg) = validate_username(&form.username) {
- return api_error(StatusCode::BAD_REQUEST, "invalid username", Some(msg)).into_response();
- }
+ let canon_user = match parse_username(&form.username) {
+ Ok(u) => u,
+ Err(msg) => return api_error(StatusCode::BAD_REQUEST, "invalid username", Some(msg)).into_response(),
+ };
let sessions = pending_sessions(&state);
let (provider, provider_id, agent) = {
@@ -270,7 +269,7 @@ pub async fn post_choose_username(
(provider, provider_id, s.agent.clone())
};
- if let Err(msg) = validate_agent_format(&agent) {
+ if let Err(msg) = parse_agent(&agent) {
return api_error(StatusCode::BAD_REQUEST, "invalid agent format", Some(msg)).into_response();
}
@@ -280,7 +279,7 @@ pub async fn post_choose_username(
if reduced.users_by_provider.contains_key(&provider_key) {
return api_error(StatusCode::CONFLICT, "provider already registered", None).into_response();
}
- if reduced.users_by_provider.values().any(|u| u == &canonicalize_username(&form.username)) {
+ if reduced.users_by_provider.values().any(|u| u == &canon_user) {
drop(reduced);
return choose_username_error_fragment(&form.session, "that username is taken — try another").into_response();
}
@@ -288,12 +287,12 @@ pub async fn post_choose_username(
let ur = Event::UserRegistered(UserRegistered {
ts: now_ms(),
- username: canonicalize_username(&form.username),
+ username: canon_user.clone(),
provider: provider.to_lowercase(),
provider_id: provider_id.clone(),
});
- let (bearer, ti, canon_user) = issue_token_for_user(&form.username);
+ let (bearer, ti) = issue_token_for_user(&canon_user);
let ti_ev = Event::TokenIssued(ti);
// Persist events.
@@ -325,15 +324,18 @@ pub async fn post_pending_session(
State(state): State<AppState>,
Json(req): Json<PendingSessionStartRequest>,
) -> impl IntoResponse {
- if let Err(msg) = validate_agent_format(&req.agent) {
- return api_error(StatusCode::BAD_REQUEST, "invalid agent format", Some(msg)).into_response();
- }
+ let agent_naked = match parse_agent(&req.agent) {
+ Ok(a) => a,
+ Err(msg) => {
+ return api_error(StatusCode::BAD_REQUEST, "invalid agent format", Some(msg)).into_response();
+ }
+ };
let session = format!("p_{}", uuid::Uuid::new_v4().simple());
let public_url = std::env::var("SLUG_PUBLIC_URL").unwrap_or_else(|_| "http://127.0.0.1:8080".to_string());
let login_url = format!("{public_url}/auth/login?session={}", urlencoding::encode(&session));
let poll_url = format!("/api/v0/pending-session/{}", session);
let s = PendingSession {
- agent: req.agent.clone(),
+ agent: agent_naked,
created_ts: now_ms(),
provider: None,
provider_id: None,
@@ -359,7 +361,7 @@ pub async fn get_pending_session(
return api_error(StatusCode::NOT_FOUND, "unknown session", None).into_response();
};
let (complete, user, token) = match &s.complete {
- Some((u, t)) => (true, Some(format!("@{}", u)), Some(t.clone())),
+ Some((u, t)) => (true, Some(u.clone()), Some(t.clone())),
None => (false, None, None),
};
Json(PendingSessionPollResponse {
@@ -388,7 +390,7 @@ pub async fn get_whoami(State(state): State<AppState>, headers: HeaderMap)
… preview truncated; 62,772 characters omittedB — c_d2fd40385778 (tommy-mor)
message
[81bd5f79] direction
diff preview
diff --git a/seed.tdsl b/seed.tdsl new file mode 100644 index 0000000000000000000000000000000000000000..574342af3d509cb0f45b37db8194c27b23d56b73 --- /dev/null +++ b/seed.tdsl @@ -0,0 +1 @@ +open webui
Hardlinks — judgments / attempts / prompt
judgments
- ~anthropic/claude-sonnet-latest: A (9:1)
- ~x-ai/grok-latest: A (25:1)
- openai/gpt-chat-latest: A (100:1)
attempts
Prompt text is loaded only by the download route.