Full excision: the web panel and chat page, the git ents agent CLI,
the agent-exec/agent-plan/agent-review effects and their workers, the
per-member credential seam, the AgentSession entity, its
refs/meta/agent-sessions namespace, and the gate + gate-rules
stateright wiring that enforced its lifecycle.
The review system (withdraw state and the review detail page included),
issues, comments, and all generic forge infrastructure are untouched.
Two generic seams added during the agent work — SandboxInputs::env and
the pub entity_transition — are kept as dormant infra; materialize’s
write_tree is removed since it had no remaining caller.
The feature stays in history and on the worktree-agent-sessions branch
to restore later.
No reviews of this commit yet — record a verdict below.
Start a review
crates/cli/ents-web/src/lib.rs
@@ -115,7 +115,6 @@
pub mod identity;
pub(crate) mod markdown;
pub mod pages;
-pub mod planner;
pub mod render;
pub mod router;
pub mod session;
crates/cli/ents-web/src/state.rs
@@ -32,7 +32,6 @@
use crate::auth::ChallengeStore;
use crate::identity::SigningIdentity;
-use crate::planner::{Planner, UnconfiguredPlanner};
use crate::session::SessionStore;
/// Who may mutate through this deployment's web UI — injected by the
@@ -102,11 +101,6 @@
/// unless the composition root said otherwise via
/// [`AppState::with_access`].
pub access: AccessPolicy,
- /// The planning-chat page's LLM seam
- /// (`docs/agent-sessions-plan.adoc`'s Phase 4): [`UnconfiguredPlanner`]
- /// unless the composition root said otherwise via
- /// [`AppState::with_planner`].
- pub planner: Box<dyn Planner>,
}
impl<O> AppState<O> {
@@ -130,7 +124,6 @@
path,
sessions: SessionStore::default(),
access: AccessPolicy::Trusted,
- planner: Box::new(UnconfiguredPlanner),
}
}
@@ -146,17 +139,6 @@
self
}
- /// Replace the default [`UnconfiguredPlanner`] — a real Planner is
- /// wired the same consuming-builder way once one exists (per-member
- /// credentials, `docs/agent-sessions-plan.adoc`'s Phase 6), so every
- /// existing `new` caller stays untouched until a composition root
- /// opts in.
- #[must_use]
- pub fn with_planner(mut self, planner: Box<dyn Planner>) -> Self {
- self.planner = planner;
- self
- }
-
/// Lock the object store for the duration of one request.
///
/// Poisoning recovers rather than propagating (mirrors
crates/cli/ents-web/tests/router.rs
@@ -13,7 +13,7 @@
use axum::http::{Request, StatusCode, header};
use ents_kiln::Toolchain;
use ents_model::{Account, Effect, MemberId, Provenance, Redaction, ResultRecord, Status};
-use ents_receive::{Identity, Mode, NullEventSink};
+use ents_receive::{Mode, NullEventSink};
use ents_testutil::{
CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, record_result, write_commit,
write_meta_entity,
@@ -1096,7 +1096,6 @@
"/commits",
"/reviews",
"/issues",
- "/agents",
"/comments",
"/meta",
"/account",
@@ -1107,7 +1106,7 @@
);
}
for label in [
- "Dashboard", "Code", "Commits", "Reviews", "Issues", "Agents", "Threads",
+ "Dashboard", "Code", "Commits", "Reviews", "Issues", "Threads",
] {
assert!(
overview.contains(&format!("title=\"{label}\"")),
@@ -3267,933 +3266,6 @@
);
}
-// ---------------------------------------------------------------------
-// `crate::pages::agents` (`docs/agent-sessions-plan.adoc`'s Phase 3).
-// ---------------------------------------------------------------------
-
-/// The actor signature every [`Identity`] the agent tests below build
-/// carries -- shaped exactly like [`FixtureIdentity::actor`]. Only this
-/// half factors into a helper: [`Identity::sign`] borrows its closure
-/// (`ents_web::identity`'s own doc explains why: the closure must live in
-/// the caller's own stack frame), so each test still builds its own
-/// `Identity` literal around this, the same shape
-/// `ents_web::receive_identity!` expands to.
-fn fixture_actor(name: &'static str) -> gix::actor::Signature {
- gix::actor::Signature {
- name: name.into(),
- email: format!("{name}@ents.test").into(),
- time: gix::date::Time {
- seconds: 1_000,
- offset: 0,
- },
- }
-}
-
-/// `POST /agents`, seeding `prompt` as a fresh session's initial thread
-/// turn -- what the agent tests below start a session through, exercising
-/// the actual signed-write path (`ents_forge::agent::new`) rather than
-/// poking the ref store directly. Asserts the write succeeded (a redirect
-/// to the new session's own page) and returns its id, read from that
-/// redirect's `Location` (`/agents/<id>`).
-async fn seed_agent_via_web(
- router: &axum::Router,
- state: &AppState<ObjectStore>,
- prompt: &str,
-) -> String {
- let (cookie, csrf) = session_cookie_and_csrf(router, state, "/agents").await;
- let form = format!(
- "prompt={}&base_ref=refs%2Fheads%2Fmain&model=claude-sonnet-5&review_policy=manual&csrf={csrf}",
- prompt.replace(' ', "+")
- );
- let response = router
- .clone()
- .oneshot(
- Request::post("/agents")
- .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .header(header::COOKIE, cookie)
- .body(Body::from(form))
- .expect("request"),
- )
- .await
- .expect("in-process call");
- assert!(
- response.status().is_redirection(),
- "agent session create did not succeed: {:?}",
- response.status()
- );
- response
- .headers()
- .get(header::LOCATION)
- .expect("a successful agent create redirects to the new session")
- .to_str()
- .expect("ascii")
- .strip_prefix("/agents/")
- .expect("redirect targets /agents/<id>")
- .to_owned()
-}
-
-/// A freshly started session (no plan yet) renders as `planning` on both
-/// the index and its own detail page, offers no confirm form, and never
-/// leaks its seed prompt -- a thread turn like any other, never rendered
-/// (`ents_forge::agent::AgentSession`'s own "never rendered" contract).
-#[tokio::test]
-// @relation(lens.parity, roots.web-signing, roots.web-session, scope=function, role=Verifies)
-async fn agents_index_and_detail_render_a_freshly_started_session_as_planning_with_no_thread_leak()
-{
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(21),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "top secret task instructions").await;
-
- let index = get_body(&router, "/agents").await;
- assert!(index.contains(ents_forge::abbreviate_id(&id)));
- assert!(index.contains("planning"));
- assert!(
- !index.contains("top secret task instructions"),
- "thread content (the seed prompt) must never render on the list page"
- );
-
- let detail = get_body(&router, &format!("/agents/{id}")).await;
- assert!(detail.contains("planning"));
- assert!(detail.contains("No plan has been drafted yet."));
- assert!(
- !detail.contains("Confirm plan"),
- "no confirm form before a plan exists"
- );
- assert!(
- !detail.contains("top secret task instructions"),
- "thread content (the seed prompt) must never render on the detail page either"
- );
-}
-
-/// `roots.web-session`: starting a session is a state-changing route, so a
-/// `POST /agents` with no CSRF field at all is rejected, and one with the
-/// wrong token is a bad request -- the same gate every mutation in this
-/// crate runs behind.
-#[tokio::test]
-// @relation(roots.web-session, scope=function, role=Verifies)
-async fn agent_create_is_rejected_without_a_valid_csrf_token() {
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(22),
- });
- let router = ents_web::router(Arc::clone(&state));
-
- let no_csrf = router
- .clone()
- .oneshot(
- Request::post("/agents")
- .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .body(Body::from(
- "prompt=sneaky&base_ref=HEAD&model=claude-sonnet-5&review_policy=manual",
- ))
- .expect("request"),
- )
- .await
- .expect("in-process call");
- assert!(
- !no_csrf.status().is_success() && !no_csrf.status().is_redirection(),
- "a POST with no csrf field must not start a session"
- );
-
- let (cookie, _csrf) = session_cookie_and_csrf(&router, &state, "/agents").await;
- let wrong = router
- .oneshot(
- Request::post("/agents")
- .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .header(header::COOKIE, cookie)
- .body(Body::from(
- "prompt=sneaky&base_ref=HEAD&model=claude-sonnet-5&review_policy=manual&csrf=not-the-token",
- ))
- .expect("request"),
- )
- .await
- .expect("in-process call");
- assert_eq!(wrong.status(), StatusCode::BAD_REQUEST);
-}
-
-/// A plan drafted (via `ents_forge::agent::revise_plan`, standing in for
-/// Phase 4's not-yet-built planning surface) puts a session in `awaiting
-/// confirmation` with a one-tap Confirm form; posting it
-/// (`POST /agents/{id}/confirm`) is a signed, CSRF-checked mutation that
-/// binds the plan's hash and reads back as `queued`, with the confirm form
-/// gone.
-#[tokio::test]
-// @relation(lens.parity, roots.web-signing, roots.web-session, scope=function, role=Verifies)
-async fn confirming_an_awaiting_session_transitions_it_to_queued_through_a_signed_post() {
- let seed = 23u8;
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(seed),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "draft a fix").await;
-
- let key = Keypair::from_seed(seed);
- let identity = Identity {
- actor: fixture_actor("filer"),
- author: None,
- sign: &|payload| key.sign(payload),
- };
- ents_forge::agent::revise_plan(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- "do the thing".to_owned(),
- &identity,
- state.mode,
- )
- .expect("revise_plan reaches an outcome");
-
- let detail = get_body(&router, &format!("/agents/{id}")).await;
- assert!(detail.contains("awaiting confirmation"));
- assert!(
- detail.contains("Confirm plan"),
- "a plan with no current confirm shows the one-tap confirm form"
- );
-
- let (cookie, csrf) = session_cookie_and_csrf(&router, &state, "/agents").await;
- let response = router
- .clone()
- .oneshot(
- Request::post(format!("/agents/{id}/confirm"))
- .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .header(header::COOKIE, cookie)
- .body(Body::from(format!("csrf={csrf}")))
- .expect("request"),
- )
- .await
- .expect("in-process call");
- assert!(
- response.status().is_redirection(),
- "{:?}",
- response.status()
- );
-
- let detail = get_body(&router, &format!("/agents/{id}")).await;
- assert!(detail.contains("queued"));
- assert!(
- !detail.contains("Confirm plan"),
- "a queued session no longer shows the confirm form"
- );
-}
-
-/// Every one of the six derived states
-/// (`docs/agent-sessions-plan.adoc`'s Phase 3: planning, awaiting
-/// confirmation, queued, running, done, failed) renders distinctly on the
-/// index, the running session's own sandbox name renders verbatim, the
-/// done/failed sessions show their result branch/failure detail -- and no
-/// session's thread content ever appears on either the index or any
-/// detail page, whatever its state.
-#[tokio::test]
-// @relation(lens.parity, scope=function, role=Verifies)
-async fn agents_index_renders_every_derived_state_distinctly_and_never_leaks_thread_content() {
- let refs = MemRefStore::default();
- let objects = ObjectStore::default();
-
- let base = |member: &str, seconds: i64| {
- ents_forge::agent::SessionMeta::new(
- MemberId::new(member),
- seconds,
- "claude-sonnet-5",
- vec![],
- "refs/heads/main",
- ents_forge::agent::ReviewPolicy::Manual,
- None,
- )
- };
-
- let planning = ents_forge::agent::AgentSession {
- meta: base("jdc", 100),
- plan: None,
- confirm: None,
- thread: vec![b"thread marker planning".to_vec()],
- };
- write_meta_entity(
- &refs,
- &objects,
- ents_model::namespace::agent_session_ref("s-planning").expect("valid"),
- &planning,
- None,
- 100,
- );
-
- let mut awaiting = ents_forge::agent::AgentSession {
- meta: base("jdc", 200),
- plan: Some("draft plan".to_owned()),
- confirm: None,
- thread: vec![b"thread marker awaiting".to_vec()],
- };
- awaiting.meta.status = ents_forge::agent::Status::Ready;
- write_meta_entity(
- &refs,
- &objects,
- ents_model::namespace::agent_session_ref("s-awaiting").expect("valid"),
- &awaiting,
- None,
- 200,
- );
-
- let mut queued = ents_forge::agent::AgentSession {
- meta: base("jdc", 300),
- plan: Some("confirmed plan".to_owned()),
- confirm: None,
- thread: vec![b"thread marker queued".to_vec()],
- };
- queued.meta.status = ents_forge::agent::Status::Ready;
- let hash = queued.plan_hash().expect("plan set");
- queued.confirm = Some(ents_forge::agent::Confirm::new(
- hash,
- ents_forge::agent::ReviewPolicy::Manual,
- ));
- write_meta_entity(
- &refs,
- &objects,
- ents_model::namespace::agent_session_ref("s-queued").expect("valid"),
- &queued,
- None,
- 300,
- );
-
- let mut running = ents_forge::agent::AgentSession {
- meta: base("jdc", 400),
- plan: Some("confirmed plan".to_owned()),
- confirm: None,
- thread: vec![b"thread marker running".to_vec()],
- };
- running.meta.status = ents_forge::agent::Status::Running;
- running.meta.sprite = Some("sprite-42".to_owned());
- running.meta.worker = Some(MemberId::new("worker-bot"));
- running.meta.started = Some(400);
- write_meta_entity(
- &refs,
- &objects,
- ents_model::namespace::agent_session_ref("s-running").expect("valid"),
- &running,
- None,
- 400,
- );
-
- let mut done = ents_forge::agent::AgentSession {
- meta: base("jdc", 500),
- plan: Some("confirmed plan".to_owned()),
- confirm: None,
- thread: vec![b"thread marker done".to_vec()],
- };
- done.meta.status = ents_forge::agent::Status::Done;
- done.meta.result_branch = Some("agent/jdc/deadbeef".to_owned());
- done.meta.finished = Some(500);
- write_meta_entity(
- &refs,
- &objects,
- ents_model::namespace::agent_session_ref("s-done").expect("valid"),
- &done,
- None,
- 500,
- );
-
- let mut failed = ents_forge::agent::AgentSession {
- meta: base("jdc", 600),
- plan: Some("confirmed plan".to_owned()),
- confirm: None,
- thread: vec![b"thread marker failed".to_vec()],
- };
- failed.meta.status = ents_forge::agent::Status::Failed(ents_forge::agent::FailureReason {
- detail: "sandbox died".to_owned(),
- });
- failed.meta.finished = Some(600);
- write_meta_entity(
- &refs,
- &objects,
- ents_model::namespace::agent_session_ref("s-failed").expect("valid"),
- &failed,
- None,
- 600,
- );
-
- let state = build_state_with(
- FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(24),
- },
- refs,
- objects,
- );
- let router = ents_web::router(state.clone());
-
- let index = get_body(&router, "/agents").await;
- for label in [
- "planning",
- "awaiting confirmation",
- "queued",
- "running",
- "done",
- "failed",
- ] {
- assert!(index.contains(label), "the index shows the {label} state");
- }
- for marker in [
- "thread marker planning",
- "thread marker awaiting",
- "thread marker queued",
- "thread marker running",
- "thread marker done",
- "thread marker failed",
- ] {
- assert!(
- !index.contains(marker),
- "thread content must never render on the list page: {marker}"
- );
- }
-
- let running_detail = get_body(&router, "/agents/s-running").await;
- assert!(
- running_detail.contains("sprite-42"),
- "the sandbox name renders verbatim while running"
- );
- assert!(!running_detail.contains("thread marker running"));
-
- let done_detail = get_body(&router, "/agents/s-done").await;
- assert!(done_detail.contains("agent/jdc/deadbeef"));
- assert!(!done_detail.contains("thread marker done"));
-
- let failed_detail = get_body(&router, "/agents/s-failed").await;
- assert!(failed_detail.contains("sandbox died"));
- assert!(!failed_detail.contains("thread marker failed"));
-}
-
-/// The "result record" ref [`crate::pages::agents::show`] displays for a
-/// terminal session is derived from the chain's own confirmed-and-queued
-/// commit -- exactly what `git-ents::agent_worker::run_agent_exec` reads
-/// as its own dispatched oid -- and reads back as "not yet recorded" until
-/// a result actually lands at that derived ref, then as recorded once one
-/// does.
-#[tokio::test]
-// @relation(lens.parity, effect.results-writeback, scope=function, role=Verifies)
-async fn result_record_ref_is_derived_from_the_chains_own_confirmed_commit() {
- let seed = 25u8;
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(seed),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "ship the fix").await;
-
- let key = Keypair::from_seed(seed);
- let identity = Identity {
- actor: fixture_actor("filer"),
- author: None,
- sign: &|payload| key.sign(payload),
- };
-
- ents_forge::agent::revise_plan(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- "do the thing".to_owned(),
- &identity,
- state.mode,
- )
- .expect("revise_plan reaches an outcome");
- ents_forge::agent::confirm(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- None,
- &identity,
- state.mode,
- )
- .expect("confirm reaches an outcome");
-
- let ref_name = ents_model::namespace::agent_session_ref(&id).expect("valid");
- let confirmed_tip = state
- .refs
- .get(ref_name.as_ref())
- .expect("read")
- .expect("confirmed tip exists");
-
- ents_forge::agent::claim(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- ents_forge::agent::ClaimAgentSession {
- worker: MemberId::new("worker-bot"),
- sprite: "sprite-9".to_owned(),
- },
- &identity,
- state.mode,
- )
- .expect("claim reaches an outcome");
- ents_forge::agent::finish(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- ents_forge::agent::FinishAgentSession {
- outcome: ents_forge::agent::FinishOutcome::Done,
- result_branch: Some("agent/filer/deadbeef".to_owned()),
- thread: vec![b"final log".to_vec()],
- },
- &identity,
- state.mode,
- )
- .expect("finish reaches an outcome");
-
- let expected_short = ents_effect::run::short_oid(confirmed_tip);
- let result_ref =
- ents_model::namespace::result_ref("agent-exec", &expected_short).expect("valid");
- let result_ref_display = result_ref.as_bstr().to_string();
-
- let before = get_body(&router, &format!("/agents/{id}")).await;
- assert!(before.contains(&result_ref_display));
- assert!(before.contains("not yet recorded"));
-
- let record = ResultRecord::new("agent-exec", confirmed_tip, Status::Pass);
- ents_receive::propose_entity(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- result_ref.clone(),
- &record,
- &identity,
- "Record agent-exec result",
- state.mode,
- )
- .expect("reaches an outcome");
-
- let after = get_body(&router, &format!("/agents/{id}")).await;
- assert!(after.contains(&result_ref_display));
- assert!(!after.contains("not yet recorded"));
-}
-
-// ---------------------------------------------------------------------
-// `crate::pages::agents`'s manual one-tap "Open review"
-// (`docs/agent-sessions-plan.adoc`'s Phase 5).
-// ---------------------------------------------------------------------
-
-/// Push a real `refs/heads/<branch>` tip directly through `state`'s own ref
-/// store -- a branch ref needs no signature at all
-/// (`gate.principled-split`), mirroring `git-ents`'s own
-/// `tests/agent.rs::advance_branch` but against this crate's `AppState`
-/// rather than a `LocalRoot`.
-fn advance_branch(state: &AppState<ObjectStore>, branch: &str, seconds: i64) -> gix_hash::ObjectId {
- let tree = state.objects().write(&Tree::empty()).expect("tree");
- let oid = write_commit(
- &*state.objects(),
- &CommitSpec {
- tree,
- parents: vec![],
- message: format!("agent-exec output for {branch}"),
- seconds,
- },
- None,
- );
- let name: gix::refs::FullName = format!("refs/heads/{branch}")
- .try_into()
- .expect("valid refname");
- state
- .refs
- .transaction(&[gix_ref_store::RefEdit {
- name,
- expected: gix_ref_store::Expected::Any,
- new: Some(oid),
- }])
- .expect("moves the ref");
- oid
-}
-
-/// A `Done`, `manual`-policy session with a result branch offers the
-/// one-tap "Open review" form; posting it (`POST /agents/{id}/review`)
-/// opens a review of the branch's own tip through the same signed web path
-/// `crate::pages::commits::review` uses for a commit-page review, after
-/// which the form no longer renders -- the review it would open already
-/// exists (`docs/agent-sessions-plan.adoc`'s Phase 5 acceptance: "manual
-/// yields none and the session page offers one-tap open").
-#[tokio::test]
-// @relation(model.review, model.review-pin, roots.web-signing, roots.web-session, lens.parity, scope=function, role=Verifies)
-async fn manual_session_offers_a_one_tap_open_review_form_that_opens_one() {
- let seed = 30u8;
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(seed),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "ship the fix").await;
-
- let key = Keypair::from_seed(seed);
- let identity = Identity {
- actor: fixture_actor("filer"),
- author: None,
- sign: &|payload| key.sign(payload),
- };
- ents_forge::agent::revise_plan(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- "do the thing".to_owned(),
- &identity,
- state.mode,
- )
- .expect("revise_plan reaches an outcome");
- ents_forge::agent::confirm(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- None,
- &identity,
- state.mode,
- )
- .expect("confirm reaches an outcome");
- ents_forge::agent::claim(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- ents_forge::agent::ClaimAgentSession {
- worker: MemberId::new("worker-bot"),
- sprite: "sprite-9".to_owned(),
- },
- &identity,
- state.mode,
- )
- .expect("claim reaches an outcome");
- let branch = "agent/filer/deadbeef";
- ents_forge::agent::finish(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- ents_forge::agent::FinishAgentSession {
- outcome: ents_forge::agent::FinishOutcome::Done,
- result_branch: Some(branch.to_owned()),
- thread: vec![b"final log".to_vec()],
- },
- &identity,
- state.mode,
- )
- .expect("finish reaches an outcome");
- advance_branch(&state, branch, 1_100);
-
- let detail = get_body(&router, &format!("/agents/{id}")).await;
- assert!(detail.contains("done"));
- assert!(
- detail.contains("Open review"),
- "a done, manual session with a result branch offers the one-tap open: {detail}"
- );
-
- let (cookie, csrf) = session_cookie_and_csrf(&router, &state, "/agents").await;
- let response = router
- .clone()
- .oneshot(
- Request::post(format!("/agents/{id}/review"))
- .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .header(header::COOKIE, cookie)
- .body(Body::from(format!("csrf={csrf}")))
- .expect("request"),
- )
- .await
- .expect("in-process call");
- assert!(
- response.status().is_redirection(),
- "{:?}",
- response.status()
- );
-
- let after = get_body(&router, &format!("/agents/{id}")).await;
- assert!(
- !after.contains("Open review"),
- "once opened, the one-tap form must not render again"
- );
-}
-
-/// `POST /agents/{id}/review` without a valid CSRF token is rejected --
-/// this is a state-changing route like every other signed mutation
-/// (`roots.web-session`).
-#[tokio::test]
-// @relation(roots.web-session, scope=function, role=Verifies)
-async fn open_review_is_rejected_without_a_valid_csrf_token() {
- let seed = 31u8;
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(seed),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "ship the fix").await;
-
- let (cookie, _csrf) = session_cookie_and_csrf(&router, &state, "/agents").await;
- let response = router
- .clone()
- .oneshot(
- Request::post(format!("/agents/{id}/review"))
- .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .header(header::COOKIE, cookie)
- .body(Body::from("csrf=not-the-token"))
- .expect("request"),
- )
- .await
- .expect("in-process call");
- assert_eq!(response.status(), StatusCode::BAD_REQUEST);
-}
-
-// ---------------------------------------------------------------------
-// `crate::pages::agent_chat` (`docs/agent-sessions-plan.adoc`'s Phase 4:
-// the laptop planning-chat page).
-// ---------------------------------------------------------------------
-
-/// `POST path` with a signed-in session's cookie and a form-encoded body,
-/// returning the response -- the chat-page tests' own thin wrapper around
-/// the same request shape [`agent_create_is_rejected_without_a_valid_csrf_token`]
-/// builds inline, factored out here since every test below needs at least
-/// one.
-async fn post_form(
- router: &axum::Router,
- path: &str,
- cookie: &str,
- body: String,
-) -> axum::http::Response<Body> {
- router
- .clone()
- .oneshot(
- Request::post(path)
- .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
- .header(header::COOKIE, cookie)
- .body(Body::from(body))
- .expect("request"),
- )
- .await
- .expect("in-process call")
-}
-
-/// A freshly started (`planning`) session's detail page links to its own
-/// planning-chat page, which itself renders the composer and the seeded
-/// prompt (the one place a thread blob is deliberately rendered -- see
-/// `crate::pages::agent_chat`'s own doc: it is the very page that wrote
-/// it, for the member who wrote it).
-#[tokio::test]
-// @relation(lens.parity, scope=function, role=Verifies)
-async fn agent_detail_links_to_chat_and_chat_renders_the_composer_and_prompt() {
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(30),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "reproduce the flaky test").await;
-
- let detail = get_body(&router, &format!("/agents/{id}")).await;
- assert!(detail.contains(&format!("/agents/{id}/chat")));
-
- let chat = get_body(&router, &format!("/agents/{id}/chat")).await;
- assert!(chat.contains("data-agent-chat"));
- assert!(chat.contains("reproduce the flaky test"));
- assert!(!chat.contains("Reopen for planning"));
-}
-
-/// `POST /agents/{id}/chat` appends the member's message and the injected
-/// `Planner`'s reply (`ents_web::planner::UnconfiguredPlanner`, the
-/// default every composition root installs) in one commit, both of which
-/// then render on a reload of the chat page.
-#[tokio::test]
-// @relation(lens.parity, roots.web-signing, roots.web-session, scope=function, role=Verifies)
-async fn agent_chat_send_appends_the_turn_pair_and_renders_the_stub_planners_reply() {
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(31),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "prompt").await;
-
- let (cookie, csrf) =
- session_cookie_and_csrf(&router, &state, &format!("/agents/{id}/chat")).await;
- let response = post_form(
- &router,
- &format!("/agents/{id}/chat"),
- &cookie,
- format!("message=what+should+the+plan+look+like%3F&csrf={csrf}"),
- )
- .await;
- assert!(response.status().is_redirection());
-
- let session =
- ents_forge::agent::show(state.refs.as_ref(), &*state.objects(), &id).expect("shows");
- assert_eq!(
- session.thread.len(),
- 3,
- "prompt + user turn + assistant turn"
- );
-
- let chat = get_body(&router, &format!("/agents/{id}/chat")).await;
- assert!(chat.contains("what should the plan look like?"));
- assert!(chat.to_lowercase().contains("not configured"));
-}
-
-/// `roots.web-session`: `POST /agents/{id}/chat` is a state-changing route,
-/// refused without a valid CSRF token exactly like every other mutation in
-/// this crate.
-#[tokio::test]
-// @relation(roots.web-session, scope=function, role=Verifies)
-async fn agent_chat_send_is_rejected_without_a_valid_csrf_token() {
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(32),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "prompt").await;
-
- let (cookie, _csrf) =
- session_cookie_and_csrf(&router, &state, &format!("/agents/{id}/chat")).await;
- let response = post_form(
- &router,
- &format!("/agents/{id}/chat"),
- &cookie,
- "message=sneaky&csrf=wrong".to_owned(),
- )
- .await;
- assert!(!response.status().is_success() && !response.status().is_redirection());
-
- let session =
- ents_forge::agent::show(state.refs.as_ref(), &*state.objects(), &id).expect("shows");
- assert_eq!(
- session.thread.len(),
- 1,
- "the bad-csrf message must not land"
- );
-}
-
-/// `docs/agent-sessions-plan.adoc`'s Phase 4 acceptance: once a session is
-/// confirmed and queued, `POST /agents/{id}/chat` refuses rather than
-/// silently un-queueing it; `POST /agents/{id}/reopen` is the explicit
-/// un-queue that returns it to `planning`, after which chatting works
-/// again.
-#[tokio::test]
-// @relation(scope=function, role=Verifies)
-async fn agent_chat_refuses_a_queued_session_until_explicitly_reopened() {
- let seed = 33u8;
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(seed),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "prompt").await;
-
- let key = Keypair::from_seed(seed);
- let identity = Identity {
- actor: fixture_actor("filer"),
- author: None,
- sign: &|payload| key.sign(payload),
- };
- ents_forge::agent::revise_plan(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- "do the thing".to_owned(),
- &identity,
- state.mode,
- )
- .expect("revises");
- ents_forge::agent::confirm(
- state.refs.as_ref(),
- &*state.objects(),
- state.events.as_ref(),
- &id,
- None,
- &identity,
- state.mode,
- )
- .expect("confirms");
- assert!(
- ents_forge::agent::show(state.refs.as_ref(), &*state.objects(), &id)
- .expect("shows")
- .queued()
- );
-
- let (cookie, csrf) =
- session_cookie_and_csrf(&router, &state, &format!("/agents/{id}/chat")).await;
- let refused = post_form(
- &router,
- &format!("/agents/{id}/chat"),
- &cookie,
- format!("message=let%27s+change+something&csrf={csrf}"),
- )
- .await;
- assert!(
- !refused.status().is_success() && !refused.status().is_redirection(),
- "a queued session must refuse a chat message rather than silently un-queue"
- );
-
- let queued_chat = get_body(&router, &format!("/agents/{id}/chat")).await;
- assert!(queued_chat.contains("Reopen for planning"));
-
- let reopened = post_form(
- &router,
- &format!("/agents/{id}/reopen"),
- &cookie,
- format!("csrf={csrf}"),
- )
- .await;
- assert!(reopened.status().is_redirection());
-
- let session =
- ents_forge::agent::show(state.refs.as_ref(), &*state.objects(), &id).expect("shows");
- assert_eq!(session.meta.status, ents_forge::agent::Status::Planning);
- assert!(session.confirm.is_none());
-
- let sent = post_form(
- &router,
- &format!("/agents/{id}/chat"),
- &cookie,
- format!("message=let%27s+change+something&csrf={csrf}"),
- )
- .await;
- assert!(
- sent.status().is_redirection(),
- "chatting works again once the session is reopened"
- );
-}
-
-/// `POST /agents/{id}/plan` commits the plan editor's text via
-/// `ents_forge::agent::revise_plan`, transitioning the session to
-/// `ready`/awaiting-confirmation -- the same commit path
-/// `docs/agent-sessions-plan.adoc`'s Phase 4 names for both this chat page
-/// and the headless `agent-plan` effect.
-#[tokio::test]
-// @relation(lens.parity, roots.web-signing, roots.web-session, scope=function, role=Verifies)
-async fn agent_plan_commit_transitions_to_ready_awaiting_confirmation() {
- let state = build_state(FixtureIdentity {
- name: "filer",
- key: Keypair::from_seed(34),
- });
- let router = ents_web::router(state.clone());
- let id = seed_agent_via_web(&router, &state, "prompt").await;
-
- let (cookie, csrf) =
- session_cookie_and_csrf(&router, &state, &format!("/agents/{id}/chat")).await;
- let response = post_form(
- &router,
- &format!("/agents/{id}/plan"),
- &cookie,
- format!("plan=1.+read+the+test%0A2.+fix+it&csrf={csrf}"),
- )
- .await;
- assert!(response.status().is_redirection());
-
- let session =
- ents_forge::agent::show(state.refs.as_ref(), &*state.objects(), &id).expect("shows");
- assert_eq!(session.meta.status, ents_forge::agent::Status::Ready);
- assert!(session.awaiting_confirmation());
-
- let detail = get_body(&router, &format!("/agents/{id}")).await;
- assert!(detail.contains("Confirm plan"));
-}
-
/// `GET /reviews` (`crate::pages::reviews`): a withdrawn review stays in
/// `refs/meta/reviews/*`'s own history (`model.review`, append-only) but
/// must not render in this aggregate listing, while an ordinary active
crates/cli/git-ents/src/cli.rs
@@ -9,7 +9,6 @@
use facet::Facet;
use figue::{self as args, FigueBuiltins};
-pub use ents_forge::agent::AgentAction;
pub use ents_forge::comment::CommentAction;
pub use ents_forge::issue::IssueAction;
pub use ents_forge::review::ReviewAction;
@@ -129,16 +128,6 @@
#[facet(args::subcommand)]
action: IssueAction,
},
- /// Manage agent sessions at `refs/meta/agent-sessions/<id>`: start one,
- /// draft or redraft its plan, and confirm it for execution
- /// (`docs/agent-sessions-plan.adoc`). Claiming and running a queued
- /// session is the effect worker's job (`git-ents hook post-receive` on
- /// the hosted root), not a subcommand here.
- Agent {
- /// The agent action to run.
- #[facet(args::subcommand)]
- action: AgentAction,
- },
/// Review a commit: a verdict plus a body at
/// `refs/meta/reviews/<target>/<member>`, with a retention pin at
/// `refs/meta/pins/reviews/<target>/<member>` keeping the reviewed
crates/cli/git-ents/src/hook.rs
@@ -327,10 +327,9 @@
let Some(effect) = read_effect(&root.refs, &root.objects, &effect_name)? else {
continue;
};
- // `run_one`/`run_agent_exec` no longer resolve toolchain names
- // themselves: resolve and materialize this effect's declared
- // toolchains here, before handing the run loop an
- // already-materialized slice.
+ // `run_one` no longer resolves toolchain names itself: resolve and
+ // materialize this effect's declared toolchains here, before
+ // handing the run loop an already-materialized slice.
let mut toolchains = Vec::with_capacity(effect.toolchains.len());
for toolchain_name in &effect.toolchains {
let (_, recipe) =
@@ -339,83 +338,20 @@
toolchains.push((toolchain_name.clone(), bin));
}
- // The `agent-exec` effect (`docs/agent-sessions-plan.adoc`'s Phase
- // 2) needs the bespoke dispatch/claim/finalize handling
- // `crate::agent_worker::run_agent_exec` provides — a plain
- // pass/fail result on a single ref, `run_one`'s own contract,
- // cannot express "claim, run a sandbox, and land the session's
- // terminal state, its result, and its result branch atomically."
- // Every other effect keeps the ordinary single-ref path.
- if effect_name == crate::agent_worker::AGENT_EXEC_NAME {
- crate::agent_worker::run_agent_exec(
- &root.refs,
- &root.objects,
- &root.events,
- executor,
- scratch,
- &toolchains,
- &effect.run,
- oid,
- ents_model::MemberId::new(crate::root::HOSTED_WORKER_NAME),
- crate::root::HOSTED_WORKER_NAME.to_owned(),
- &author,
- &|payload| signer.sign(payload),
- Mode::Mandatory,
- &root.credentials,
- )?;
- } else if effect_name == crate::plan_worker::AGENT_PLAN_NAME {
- // The `agent-plan` effect (`docs/agent-sessions-plan.adoc`'s
- // Phase 4) needs the same bespoke handling `agent-exec` does,
- // for the same reason: a plain pass/fail result on a single
- // ref cannot express "draft a plan and land it atomically with
- // this effect's own result."
- crate::plan_worker::run_agent_plan(
- &root.refs,
- &root.objects,
- &root.events,
- executor,
- scratch,
- &toolchains,
- &effect.run,
- oid,
- &author,
- &|payload| signer.sign(payload),
- Mode::Mandatory,
- &root.credentials,
- )?;
- } else if effect_name == crate::review_worker::AGENT_REVIEW_NAME {
- // The `agent-review` effect (`docs/agent-sessions-plan.adoc`'s
- // Phase 5) needs its own bespoke handling too, but for the
- // opposite reason `agent-exec`/`agent-plan` do: it runs no
- // sandboxed command at all (`crate::review_worker`'s own doc),
- // so `executor`/`toolchains`/`effect.run` — resolved above for
- // every effect uniformly — are simply unused for this one.
- crate::review_worker::run_agent_review(
- &root.refs,
- &root.objects,
- &root.events,
- oid,
- ents_model::MemberId::new(crate::root::HOSTED_WORKER_NAME),
- &author,
- &|payload| signer.sign(payload),
- Mode::Mandatory,
- )?;
- } else {
- run_one(
- &root.refs,
- &root.objects,
- &root.events,
- executor,
- scratch,
- &toolchains,
- oid,
- &effect,
- result_ref,
- &author,
- |payload| signer.sign(payload),
- Mode::Mandatory,
- )?;
- }
+ run_one(
+ &root.refs,
+ &root.objects,
+ &root.events,
+ executor,
+ scratch,
+ &toolchains,
+ oid,
+ &effect,
+ result_ref,
+ &author,
+ |payload| signer.sign(payload),
+ Mode::Mandatory,
+ )?;
ran = ran.saturating_add(1);
}
Ok(ran)
crates/cli/git-ents/src/lib.rs
@@ -73,18 +73,14 @@
//! assert!(root.refs.get(name.as_ref()).expect("reads").is_some());
//! ```
-pub mod agent_worker;
pub mod cli;
pub mod commands;
pub mod compose;
-pub mod credentials;
pub mod error;
pub mod exe;
pub mod hook;
pub mod mutate;
pub mod package;
-pub mod plan_worker;
-pub mod review_worker;
pub mod root;
pub mod sign;
crates/cli/git-ents/src/root.rs
@@ -53,7 +53,6 @@
use ents_receive::{Mode, NullEventSink};
use gix_ref_store::LooseRefStore;
-use crate::credentials::CredentialStore;
use crate::error::{Error, Result};
/// A real, on-disk object store: the repository's own odb, opened for
@@ -189,12 +188,6 @@
/// The fixed `Executor` this root wires (`roots.single-node-hosted`): a
/// `SpriteExecutor` targeting [`HOSTED_WORKER_NAME`].
pub executor: Box<dyn Executor>,
- /// Per-member BYOK credentials (`roots.config-isolation`), read once
- /// from [`crate::credentials::CREDENTIALS_FILE_VAR`] — the seam
- /// [`crate::agent_worker::run_agent_exec`]/
- /// [`crate::plan_worker::run_agent_plan`] resolve a session's own
- /// member's credential from before injecting it into a sandbox launch.
- pub credentials: CredentialStore,
}
/// The Sprite name (and commit author name) the single-node hosted root's
@@ -215,23 +208,19 @@
///
/// [`Error::Repo`] or [`Error::Refs`] if `path` is not a git
/// repository; [`Error::Receive`] if the reconciliation scan itself
- /// fails to read repository state; [`Error::Io`]/[`Error::InvalidArgument`]
- /// if [`crate::credentials::CREDENTIALS_FILE_VAR`] names a credentials
- /// file that cannot be read or is malformed.
+ /// fails to read repository state.
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref().to_owned();
let refs = LooseRefStore::open(&path)?;
let objects = QuarantineObjects::open(&path)?;
let events = ents_receive::MemoryEventSink::default();
ents_receive::reconcile(&refs, &objects, &events)?;
- let credentials = CredentialStore::from_env()?;
Ok(Self {
path,
refs,
objects,
events,
executor: Box::new(ents_effect::SpriteExecutor::new(HOSTED_WORKER_NAME)),
- credentials,
})
}
crates/cli/git-ents/tests/hosted_root.rs
@@ -245,7 +245,6 @@
config_ref,
&ents_gate::Config {
epoch: Some(1_000),
- ..ents_gate::Config::default()
},
&identity,
"Enable the tip invariant",
crates/forge/ents-forge/src/lib.rs
@@ -1,5 +1,5 @@
//! The forge domain: the [`Issue`], [`comment::Comment`],
-//! [`review::Review`], and [`agent::AgentSession`] entities, and the
+//! and [`review::Review`] entities, and the
//! command business logic driving each — kernel-independent, unlike
//! `ents-model`'s remaining entities,
//! because a comment or review command needs `ents-anchor` (to capture and
@@ -47,13 +47,6 @@
//! lens offers over these entities is one of this crate's library
//! functions; frontends only wire stores and render.
//!
-//! [`agent::AgentSession`] (`refs/meta/agent-sessions/<id>`,
-//! `namespace::agent_session_ref`) is Phase 1 of
-//! `docs/agent-sessions-plan.adoc`: no `model.agent-session` spec section
-//! exists yet (an owner item the plan itself names), so its own module docs
-//! cite only the `meta-ref.*` and `model.extensibility` ids above that
-//! already apply to any hash-identified, additively-evolving typed tree.
-//!
//! # Examples
//!
//! Build an [`Issue`], and a [`comment::Comment`] anchored to a stand-in
@@ -96,7 +89,6 @@
mod error;
-pub mod agent;
pub mod comment;
pub mod issue;
pub mod present;
@@ -203,13 +195,6 @@
#[case::comment(comment::Comment::SHAPE.type_identifier, "Comment")]
#[case::issue(Issue::SHAPE.type_identifier, "Issue")]
#[case::review(review::Review::SHAPE.type_identifier, "Review")]
- #[case::agent_session(agent::AgentSession::SHAPE.type_identifier, "AgentSession")]
- #[case::agent_session_meta(agent::SessionMeta::SHAPE.type_identifier, "SessionMeta")]
- #[case::agent_toolchain_pin(agent::ToolchainPin::SHAPE.type_identifier, "ToolchainPin")]
- #[case::agent_confirm(agent::Confirm::SHAPE.type_identifier, "Confirm")]
- #[case::agent_status(agent::Status::SHAPE.type_identifier, "Status")]
- #[case::agent_failure_reason(agent::FailureReason::SHAPE.type_identifier, "FailureReason")]
- #[case::agent_review_policy(agent::ReviewPolicy::SHAPE.type_identifier, "ReviewPolicy")]
// @relation(model.extensibility, scope=function, role=Verifies)
fn every_entity_shape_name_tracks_its_struct_declaration(
#[case] reflected: &str,
crates/forge/ents-forge/tests/round_trip.rs
@@ -37,53 +37,6 @@
}
}
-proptest! {
- #![proptest_config(ProptestConfig::with_cases(64))]
-
- // @relation(meta-ref.typed-tree, scope=function, role=Verifies)
- #[test]
- fn agent_session_round_trips_for_any_fields_and_collection_lengths(
- member in any::<String>(),
- created in any::<i64>(),
- model in any::<String>(),
- toolchain_names in prop::collection::vec(any::<String>(), 0..4),
- base_ref in any::<String>(),
- plan in proptest::option::of(any::<String>()),
- thread in prop::collection::vec(prop::collection::vec(any::<u8>(), 0..16), 0..4),
- ) {
- use ents_forge::agent::{AgentSession, Confirm, ReviewPolicy, SessionMeta, ToolchainPin};
-
- let toolchains: Vec<ToolchainPin> = toolchain_names
- .into_iter()
- .map(|name| ToolchainPin::new(name, gix_hash::ObjectId::null(gix_hash::Kind::Sha1)))
- .collect();
- let meta = SessionMeta::new(
- MemberId::new(member),
- created,
- model,
- toolchains,
- base_ref,
- ReviewPolicy::Manual,
- None,
- );
- // A confirm can only exist alongside a plan in practice (the command
- // layer refuses otherwise), but the typed tree itself places no such
- // constraint on what round-trips — exercised here as `plan`'s own
- // hash, present only when `plan` is.
- let confirm = plan
- .as_deref()
- .map(|text| Confirm::new(
- gix_object::compute_hash(gix_hash::Kind::Sha1, gix_object::Kind::Blob, text.as_bytes())
- .expect("hashing cannot fail"),
- ReviewPolicy::Auto,
- ));
- let session = AgentSession { meta, plan, confirm, thread };
- let (id, store) = serialize(&session).expect("serialize");
- let back: AgentSession = deserialize(&id, &store).expect("deserialize");
- prop_assert_eq!(back, session);
- }
-}
-
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
crates/kernel/ents-effect/src/definition.rs
@@ -56,143 +56,6 @@
Ok(())
}
-/// The canonical `agent-exec` effect's own name
-/// (`docs/agent-sessions-plan.adoc`'s Phase 2) — the final segment of
-/// `refs/meta/effects/agent-exec` (`model.effect-definition`).
-pub const AGENT_EXEC_NAME: &str = "agent-exec";
-
-/// `agent-exec`'s trigger: every author-written `refs/meta/agent-sessions/*`
-/// tip — every commit entering the agent-sessions namespace
-/// (`docs/agent-sessions-plan.adoc`'s Phase 2, "An `agent-exec` effect
-/// subscribed via `meta(...)` to the agent namespace"). `meta()`'s own
-/// grammar rule (`query.meta`) only forbids matching an effect-written
-/// namespace — `refs/meta/results/*` or `refs/meta/index/*` — and
-/// `refs/meta/agent-sessions/*` is neither, so this glob needs no
-/// grammar extension (`query.no-extensions`); this module's own tests pin
-/// that against the real parser.
-pub const AGENT_EXEC_TRIGGER: &str = "meta(refs/meta/agent-sessions/*)";
-
-/// The canonical `agent-exec` [`Effect`] definition
-/// (`docs/agent-sessions-plan.adoc`'s Phase 2): fires once per commit
-/// entering the agent-sessions namespace. `toolchains` and `run` are a
-/// deployment's own choice — this constructor only fixes the two fields
-/// that make the effect *this* effect, `name` and `trigger`
-/// (`model.effect-definition`); a real deployment still writes its own
-/// signed commit onto `refs/meta/effects/agent-exec` through the ordinary
-/// admin-only path (`effect.admin-only`), this fixture is not that write.
-///
-/// # Examples
-///
-/// ```
-/// use ents_effect::definition::{agent_exec, validate};
-///
-/// let effect = agent_exec(vec!["agent-runtime".to_owned()], "git-ents agent-exec run");
-/// assert_eq!(effect.name, "agent-exec");
-/// validate(&effect).expect("the canonical trigger validates");
-/// ```
-#[must_use]
-pub fn agent_exec(toolchains: Vec<String>, run: impl Into<String>) -> Effect {
- Effect {
- name: AGENT_EXEC_NAME.to_owned(),
- trigger: AGENT_EXEC_TRIGGER.to_owned(),
- toolchains,
- run: run.into(),
- }
-}
-
-/// The canonical `agent-plan` effect's own name
-/// (`docs/agent-sessions-plan.adoc`'s Phase 4, "headless plan drafting is a
-/// second effect (`agent-plan`)") — the final segment of
-/// `refs/meta/effects/agent-plan`.
-pub const AGENT_PLAN_NAME: &str = "agent-plan";
-
-/// `agent-plan`'s trigger: identical to [`AGENT_EXEC_TRIGGER`] — every
-/// author-written `refs/meta/agent-sessions/*` tip. Both effects share one
-/// query grammar; the plan's own words are "the runner decides by
-/// inspecting the tip" — what tells `agent-plan` and `agent-exec` apart is
-/// never the trigger, only each effect's own dispatch predicate
-/// (`ents_forge::agent::dispatch_plan` here, `ents_forge::agent::dispatch`
-/// for `agent-exec`) and its own results namespace
-/// (`refs/meta/results/agent-plan/*`, distinct from `agent-exec`'s).
-pub const AGENT_PLAN_TRIGGER: &str = AGENT_EXEC_TRIGGER;
-
-/// The canonical `agent-plan` [`Effect`] definition
-/// (`docs/agent-sessions-plan.adoc`'s Phase 4): headless plan drafting,
-/// firing on every commit entering the agent-sessions namespace exactly
-/// like `agent-exec` — the runner's own dispatch predicate is what makes it
-/// a cheap no-op except when a session is `planning`, carries a prompt,
-/// and has no plan leaf yet. `toolchains` and `run` are a deployment's own
-/// choice, exactly as [`agent_exec`]'s own doc explains for its two fixed
-/// fields.
-///
-/// # Examples
-///
-/// ```
-/// use ents_effect::definition::{agent_plan, validate};
-///
-/// let effect = agent_plan(vec!["agent-runtime".to_owned()], "git-ents agent-plan draft");
-/// assert_eq!(effect.name, "agent-plan");
-/// validate(&effect).expect("the canonical trigger validates");
-/// ```
-#[must_use]
-pub fn agent_plan(toolchains: Vec<String>, run: impl Into<String>) -> Effect {
- Effect {
- name: AGENT_PLAN_NAME.to_owned(),
- trigger: AGENT_PLAN_TRIGGER.to_owned(),
- toolchains,
- run: run.into(),
- }
-}
-
-/// The canonical `agent-review` effect's own name
-/// (`docs/agent-sessions-plan.adoc`'s Phase 5, "Auto-open is a follow-on
-/// effect") — the final segment of `refs/meta/effects/agent-review`.
-pub const AGENT_REVIEW_NAME: &str = "agent-review";
-
-/// `agent-review`'s trigger: every `agent-exec` result recorded `pass`
-/// (`query.results`) — the follow-on's own words, "subscribed via
-/// `results(agent-exec)`," resolved to `query.grammar`'s actual two-argument
-/// `results(effect, status)` form. Only a `pass` result is a completed run
-/// with a result branch to review; a `fail`/`error` result names a run that
-/// never reached `Done`, for which there is nothing to open a review of.
-/// This module's own tests pin the exact syntax against the real parser,
-/// mirroring [`AGENT_EXEC_TRIGGER`] and [`AGENT_PLAN_TRIGGER`]'s own tests.
-pub const AGENT_REVIEW_TRIGGER: &str = "results(agent-exec, pass)";
-
-/// The canonical `agent-review` [`Effect`] definition
-/// (`docs/agent-sessions-plan.adoc`'s Phase 5): opening a review is pure
-/// repository mutation (a signed commit onto the review's own entity ref
-/// plus its retention pin) with no sandboxed command to run at all, unlike
-/// [`agent_exec`] and [`agent_plan`] — so unlike those two constructors,
-/// this one takes no `toolchains`/`run` parameters to fix: an effect
-/// definition still carries the two fields (`model.effect-definition`
-/// requires them of every effect), but this handler
-/// (`git_ents::review_worker::run_agent_review`) never resolves a toolchain
-/// or invokes an [`ents_effect`]-crate `Executor` for it, so there is
-/// nothing meaningful a caller could fix them to.
-///
-/// # Examples
-///
-/// ```
-/// use ents_effect::definition::{agent_review, validate};
-///
-/// let effect = agent_review();
-/// assert_eq!(effect.name, "agent-review");
-/// assert!(effect.toolchains.is_empty());
-/// validate(&effect).expect("the canonical trigger validates");
-/// ```
-#[must_use]
-pub fn agent_review() -> Effect {
- Effect {
- name: AGENT_REVIEW_NAME.to_owned(),
- trigger: AGENT_REVIEW_TRIGGER.to_owned(),
- toolchains: Vec::new(),
- run: "no sandboxed command: agent-review is pure repository mutation, handled entirely \
- by its composition-root handler"
- .to_owned(),
- }
-}
-
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, reason = "unit test")]
@@ -239,97 +102,4 @@
fn validate_rejects_an_invalid_toolchain_name() {
assert!(validate(&effect("rev(refs/heads/main)", &["../escape"])).is_err());
}
-
- // ---- The canonical `agent-exec` definition
- // (`docs/agent-sessions-plan.adoc`'s Phase 2) ----
-
- #[rstest]
- // @relation(query.grammar, scope=function, role=Verifies)
- fn agent_exec_trigger_parses_against_the_real_query_grammar() {
- AGENT_EXEC_TRIGGER
- .parse::<ents_query::Query>()
- .expect("the canonical agent-exec trigger parses");
- }
-
- #[rstest]
- // @relation(effect.validation, query.meta, scope=function, role=Verifies)
- fn agent_exec_definition_validates() {
- let effect = agent_exec(vec!["agent-runtime".to_owned()], "git-ents agent-exec run");
- assert_eq!(effect.name, AGENT_EXEC_NAME);
- validate(&effect).expect("the canonical agent-exec definition validates");
- }
-
- /// `query.meta` forbids `meta(glob)` from matching only
- /// `refs/meta/results/*` and `refs/meta/index/*` — the agent-sessions
- /// namespace is neither, so it must never be rejected as
- /// effect-written the way `validate_rejects_a_meta_glob_naming_an_effect_written_namespace`
- /// proves the results namespace is.
- #[rstest]
- // @relation(query.meta, scope=function, role=Verifies)
- fn the_agent_sessions_namespace_is_not_rejected_as_effect_written() {
- assert!(
- validate(&effect(AGENT_EXEC_TRIGGER, &[])).is_ok(),
- "refs/meta/agent-sessions/* is an author-written namespace, not one of \
- query.meta's forbidden effect-written namespaces (refs/meta/results/*, \
- refs/meta/index/*)"
- );
- }
-
- // ---- The canonical `agent-plan` definition
- // (`docs/agent-sessions-plan.adoc`'s Phase 4) ----
-
- #[rstest]
- // @relation(query.grammar, scope=function, role=Verifies)
- fn agent_plan_trigger_parses_against_the_real_query_grammar() {
- AGENT_PLAN_TRIGGER
- .parse::<ents_query::Query>()
- .expect("the canonical agent-plan trigger parses");
- }
-
- #[rstest]
- // @relation(effect.validation, query.meta, scope=function, role=Verifies)
- fn agent_plan_definition_validates() {
- let effect = agent_plan(
- vec!["agent-runtime".to_owned()],
- "git-ents agent-plan draft",
- );
- assert_eq!(effect.name, AGENT_PLAN_NAME);
- validate(&effect).expect("the canonical agent-plan definition validates");
- }
-
- #[rstest]
- // @relation(scope=function, role=Verifies)
- fn agent_plan_and_agent_exec_share_a_trigger_but_not_a_name() {
- assert_eq!(AGENT_PLAN_TRIGGER, AGENT_EXEC_TRIGGER);
- assert_ne!(AGENT_PLAN_NAME, AGENT_EXEC_NAME);
- }
-
- // ---- The canonical `agent-review` definition
- // (`docs/agent-sessions-plan.adoc`'s Phase 5) ----
-
- #[rstest]
- // @relation(query.grammar, query.results, scope=function, role=Verifies)
- fn agent_review_trigger_parses_against_the_real_query_grammar() {
- let query: ents_query::Query = AGENT_REVIEW_TRIGGER
- .parse()
- .expect("the canonical agent-review trigger parses");
- assert_eq!(query.results_dependencies(), ["agent-exec"]);
- }
-
- #[rstest]
- // @relation(effect.validation, scope=function, role=Verifies)
- fn agent_review_definition_validates() {
- let effect = agent_review();
- assert_eq!(effect.name, AGENT_REVIEW_NAME);
- assert!(effect.toolchains.is_empty());
- validate(&effect).expect("the canonical agent-review definition validates");
- }
-
- #[rstest]
- // @relation(scope=function, role=Verifies)
- fn agent_review_is_downstream_of_agent_exec_pass_only() {
- assert!(AGENT_REVIEW_TRIGGER.contains("pass"));
- assert_ne!(AGENT_REVIEW_NAME, AGENT_EXEC_NAME);
- assert_ne!(AGENT_REVIEW_NAME, AGENT_PLAN_NAME);
- }
}
crates/kernel/ents-effect/src/materialize.rs
@@ -23,8 +23,8 @@
use gix_hash::ObjectId;
use gix_object::bstr::ByteSlice as _;
-use gix_object::tree::{Entry, EntryKind, EntryMode};
-use gix_object::{Find, Kind, Tree, TreeRef, Write};
+use gix_object::tree::EntryKind;
+use gix_object::{Find, Kind, TreeRef};
use crate::error::{Error, Result};
@@ -155,139 +155,6 @@
Ok(())
}
-/// The reverse of [`checkout`]: recursively read `src`'s current on-disk
-/// state and write it as a git tree, preserving the executable bit and
-/// symlink targets `checkout` itself would restore. A directory walk can
-/// never discover a gitlink (there is no way for a plain host directory to
-/// carry one), so unlike `checkout` this has no submodule case to refuse.
-///
-/// This is `docs/agent-sessions-plan.adoc`'s Phase 2 finalize's other
-/// half from `checkout`: after [`crate::Executor::run`] completes, a
-/// composition root reads the checked-out workdir's now-current state back
-/// through this function to build the sandbox's output tree — the tree the
-/// result branch's commit carries. Every effect backend built so far in
-/// this crate (`UnsandboxedExecutor` today; a future `SpriteExecutor` that
-/// syncs its sandbox's filesystem back onto `workdir` before returning)
-/// leaves its command's file-level effects on `workdir` itself, so this
-/// reads the same host directory [`SandboxInputs::workdir`] named, not a
-/// second, backend-specific location.
-///
-/// # Errors
-///
-/// [`Error::NotUtf8`] for a non-UTF-8 entry name or symlink target;
-/// [`Error::Io`] for a host filesystem failure reading `src` or one of its
-/// entries; [`Error::ObjectWrite`] if writing a blob or tree object fails.
-///
-/// # Examples
-///
-/// ```
-/// use ents_effect::materialize::write_tree;
-/// use ents_testutil::ObjectStore;
-///
-/// let dir = tempfile::tempdir().expect("tempdir");
-/// std::fs::write(dir.path().join("README"), b"hello\n").expect("write");
-/// std::fs::create_dir(dir.path().join("sub")).expect("mkdir");
-/// std::fs::write(dir.path().join("sub/leaf.txt"), b"leaf\n").expect("write");
-///
-/// let objects = ObjectStore::default();
-/// let tree = write_tree(&objects, dir.path()).expect("writes");
-///
-/// let checked_out = tempfile::tempdir().expect("tempdir");
-/// ents_effect::materialize::checkout(&objects, tree, checked_out.path()).expect("checkout");
-/// assert_eq!(
-/// std::fs::read_to_string(checked_out.path().join("README")).expect("read"),
-/// "hello\n"
-/// );
-/// assert_eq!(
-/// std::fs::read_to_string(checked_out.path().join("sub").join("leaf.txt")).expect("read"),
-/// "leaf\n"
-/// );
-/// ```
-// @relation(effect.execution, scope=function)
-pub fn write_tree(objects: &(impl Find + Write), src: &Path) -> Result<ObjectId> {
- let read_dir = std::fs::read_dir(src).map_err(|source| Error::Io {
- path: src.to_owned(),
- source,
- })?;
-
- let mut entries = Vec::new();
- for dir_entry in read_dir {
- let dir_entry = dir_entry.map_err(|source| Error::Io {
- path: src.to_owned(),
- source,
- })?;
- let path = dir_entry.path();
- let name = dir_entry
- .file_name()
- .into_string()
- .map_err(|_not_utf8| Error::NotUtf8(path.clone()))?;
- let file_type = dir_entry.file_type().map_err(|source| Error::Io {
- path: path.clone(),
- source,
- })?;
-
- let (mode, oid) = if file_type.is_dir() {
- (
- EntryMode::from(EntryKind::Tree),
- write_tree(objects, &path)?,
- )
- } else if file_type.is_symlink() {
- let target = std::fs::read_link(&path).map_err(|source| Error::Io {
- path: path.clone(),
- source,
- })?;
- let target = target
- .to_str()
- .ok_or_else(|| Error::NotUtf8(path.clone()))?;
- let oid = objects.write_buf(Kind::Blob, target.as_bytes())?;
- (EntryMode::from(EntryKind::Link), oid)
- } else {
- let bytes = std::fs::read(&path).map_err(|source| Error::Io {
- path: path.clone(),
- source,
- })?;
- let kind = if is_executable(&path)? {
- EntryKind::BlobExecutable
- } else {
- EntryKind::Blob
- };
- (
- EntryMode::from(kind),
- objects.write_buf(Kind::Blob, &bytes)?,
- )
- };
- entries.push(Entry {
- mode,
- filename: name.into(),
- oid,
- });
- }
- // `gix_object::Tree::write_to` debug-asserts its entries are sorted by
- // its own `Ord` (git's tree-entry order, a directory compared as if it
- // carried a trailing `/`) — a plain directory walk has no such
- // ordering, so this sorts before handing the tree to `objects.write`.
- entries.sort();
- Ok(objects.write(&Tree { entries })?)
-}
-
-#[cfg(unix)]
-fn is_executable(path: &Path) -> Result<bool> {
- use std::os::unix::fs::PermissionsExt as _;
- let mode = std::fs::metadata(path)
- .map_err(|source| Error::Io {
- path: path.to_owned(),
- source,
- })?
- .permissions()
- .mode();
- Ok(mode & 0o111 != 0)
-}
-
-#[cfg(not(unix))]
-fn is_executable(_path: &Path) -> Result<bool> {
- Ok(false)
-}
-
fn make_dir(path: &Path) -> Result<()> {
std::fs::create_dir_all(path).map_err(|source| Error::Io {
path: path.to_owned(),
@@ -572,83 +439,4 @@
);
}
- // -----------------------------------------------------------------
- // write_tree: checkout's reverse.
- // -----------------------------------------------------------------
-
- #[test]
- // @relation(effect.execution, scope=function, role=Verifies)
- fn write_tree_round_trips_plain_files_and_subdirectories_through_checkout() {
- let objects = ObjectStore::default();
- let src = tempfile::tempdir().expect("tempdir");
- std::fs::write(src.path().join("README"), b"hello\n").expect("write");
- std::fs::create_dir(src.path().join("sub")).expect("mkdir");
- std::fs::write(src.path().join("sub/leaf.txt"), b"leaf\n").expect("write");
-
- let tree = write_tree(&objects, src.path()).expect("writes");
-
- let dest = tempfile::tempdir().expect("tempdir");
- checkout(&objects, tree, dest.path()).expect("checkout");
- assert_eq!(
- std::fs::read_to_string(dest.path().join("README")).expect("read"),
- "hello\n"
- );
- assert_eq!(
- std::fs::read_to_string(dest.path().join("sub").join("leaf.txt")).expect("read"),
- "leaf\n"
- );
- }
-
- #[test]
- // @relation(effect.execution, scope=function, role=Verifies)
- fn write_tree_preserves_the_executable_bit() {
- let objects = ObjectStore::default();
- let src = tempfile::tempdir().expect("tempdir");
- let script = src.path().join("run.sh");
- std::fs::write(&script, b"#!/bin/sh\necho hi\n").expect("write");
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt as _;
- let mut perms = std::fs::metadata(&script).expect("stat").permissions();
- perms.set_mode(0o755);
- std::fs::set_permissions(&script, perms).expect("chmod");
- }
-
- let tree = write_tree(&objects, src.path()).expect("writes");
- let dest = tempfile::tempdir().expect("tempdir");
- checkout(&objects, tree, dest.path()).expect("checkout");
-
- #[cfg(unix)]
- {
- use std::os::unix::fs::PermissionsExt as _;
- let mode = std::fs::metadata(dest.path().join("run.sh"))
- .expect("stat")
- .permissions()
- .mode();
- assert_eq!(
- mode & 0o111,
- 0o111,
- "the executable bit must survive the round trip"
- );
- }
- }
-
- #[test]
- // @relation(effect.execution, scope=function, role=Verifies)
- fn write_tree_entries_are_sorted_for_serialization() {
- // A directory walk yields entries in whatever order the host
- // filesystem happens to return them, never guaranteed to already be
- // git's own tree-entry order; `write_tree` must sort them itself so
- // `Tree::write_to`'s debug assertion never trips.
- let objects = ObjectStore::default();
- let src = tempfile::tempdir().expect("tempdir");
- for name in ["z-file", "a-file", "m-dir"] {
- if name == "m-dir" {
- std::fs::create_dir(src.path().join(name)).expect("mkdir");
- } else {
- std::fs::write(src.path().join(name), b"x").expect("write");
- }
- }
- write_tree(&objects, src.path()).expect("writes without tripping the sort assertion");
- }
}
crates/kernel/ents-effect/src/run.rs
@@ -171,9 +171,9 @@
toolchains,
command: &effect.run,
// An ordinary effect run has no per-member credential to inject
- // (`roots.config-isolation`'s BYOK seam is `agent_worker`'s and
- // `plan_worker`'s own concern, in `git-ents`); every backend still
- // accepts the field uniformly, it is simply empty here.
+ // (`roots.config-isolation`'s BYOK injection seam is a
+ // composition-root concern); every backend still accepts the
+ // field uniformly, it is simply empty here.
env: &[],
};
let output = executor.run(&inputs)?;
crates/kernel/ents-gate-rules/src/lib.rs
@@ -59,20 +59,6 @@
//! canonical infrastructure, which needs more trust than an ordinary
//! append).
//!
-//! A second rule grows the set again, for Phase 1b of
-//! `docs/agent-sessions-plan.adoc` (the agent-sessions plan)'s lifecycle
-//! invariants, restated over three new session-specific facts
-//! ([`session_running`], [`session_plan_hash`], [`session_confirm_hash`]):
-//!
-//! - [`running_violation`](GateRules::running_violation) — a commit whose
-//! agent-session meta status is `Running` must descend from a parent
-//! whose own tree recorded a confirm leaf binding that parent's
-//! plan-leaf hash; absent that parent, a session could reach `Running`
-//! without ever having been confirmed against the plan a worker is about
-//! to execute (`docs/agent-sessions-plan.adoc`'s Phase 1b: "a commit
-//! whose meta status is running requires a parent whose tree contains a
-//! confirm leaf binding that parent's plan-leaf hash").
-//!
//! Two invariants are deliberately *not* encoded here, the gap marked
//! rather than papered over:
//!
@@ -161,20 +147,6 @@
relation context(Oid, Oid);
/// Objects the repository already has, or that arrive in this pack.
relation object_exists(Oid);
- /// A commit whose decoded agent-session meta status is `Running`
- /// (`ents_forge::agent::Status::Running`,
- /// `docs/agent-sessions-plan.adoc`'s Phase 1b) — the fact a real
- /// extractor would derive from the commit's typed tree.
- relation session_running(Oid);
- /// (commit, plan-leaf content hash) — the agent session's plan-leaf
- /// hash as recorded in this commit's own tree
- /// (`AgentSession::plan_hash`), absent when the tree carries no plan
- /// leaf.
- relation session_plan_hash(Oid, Oid);
- /// (commit, bound hash) — the plan-leaf hash this commit's own confirm
- /// leaf binds (`AgentSession::confirm`), absent when the tree carries
- /// no confirm leaf.
- relation session_confirm_hash(Oid, Oid);
// ---- IDB: derived relations ----
@@ -211,15 +183,6 @@
relation admin_signed(Oid);
admin_signed(c.clone()) <-- signed_by(c, k), member(k, Role::Admin);
- /// A `Running` commit whose immediate parent's own tree carries a
- /// confirm leaf binding that same parent's plan-leaf hash — the parent
- /// was `queued` the instant before the claim that produced this commit
- /// (`docs/agent-sessions-plan.adoc`'s Phase 1b).
- relation session_running_confirmed(Oid);
- session_running_confirmed(c.clone()) <--
- session_running(c), parent(c, p),
- session_confirm_hash(p, h), session_plan_hash(p, h);
-
// ---- Denial rules: any row here rejects the transaction ----
/// Fast-forward-only: the new tip must descend from the old tip.
@@ -266,14 +229,6 @@
effect_admin_violation(r.clone(), c.clone()) <--
introduced(r, c), if r.starts_with("refs/meta/effects/"),
!admin_signed(c);
-
- /// A commit whose agent-session meta status is `Running` must have a
- /// parent whose own tree carries a confirm leaf binding that parent's
- /// plan-leaf hash — otherwise the session reached `Running` without a
- /// confirmed plan (`docs/agent-sessions-plan.adoc`'s Phase 1b).
- relation running_violation(Ref, Oid);
- running_violation(r.clone(), c.clone()) <--
- introduced(r, c), session_running(c), !session_running_confirmed(c);
}
/// Facts for one proposed transaction. In the real system these would be
@@ -296,12 +251,6 @@
pub context: Vec<(Oid, Oid)>,
/// See [`GateRules::object_exists`].
pub object_exists: Vec<(Oid,)>,
- /// See [`GateRules::session_running`].
- pub session_running: Vec<(Oid,)>,
- /// See [`GateRules::session_plan_hash`].
- pub session_plan_hash: Vec<(Oid, Oid)>,
- /// See [`GateRules::session_confirm_hash`].
- pub session_confirm_hash: Vec<(Oid, Oid)>,
}
/// Run every denial rule to a fixpoint over `facts`. An empty result means
@@ -317,9 +266,6 @@
anchor: facts.anchor,
context: facts.context,
object_exists: facts.object_exists,
- session_running: facts.session_running,
- session_plan_hash: facts.session_plan_hash,
- session_confirm_hash: facts.session_confirm_hash,
..GateRules::default()
};
rules.run();
@@ -350,12 +296,6 @@
"effect-admin: {r}: {c} not signed by an admin-registered member"
));
}
- for (r, c) in &rules.running_violation {
- out.push(format!(
- "session-running: {r}: {c}'s session status is Running but no parent's confirm \
- leaf binds that parent's plan-leaf hash"
- ));
- }
out.sort();
out
}
@@ -367,7 +307,6 @@
const ISSUE: &str = "refs/meta/issues/g";
const COMMENT: &str = "refs/meta/comments/g2";
const EFFECT: &str = "refs/meta/effects/ci";
- const AGENT_SESSION: &str = "refs/meta/agent-sessions/g";
fn base() -> Facts {
Facts {
@@ -500,65 +439,4 @@
assert!(v.iter().any(|m| m.starts_with("effect-admin:")), "{v:?}");
assert!(!v.iter().any(|m| m.starts_with("signature:")), "{v:?}");
}
-
- // ---- Agent-session lifecycle: `running_violation` ----
- // (`docs/agent-sessions-plan.adoc`'s Phase 1b).
-
- /// RED: `c1` claims the session (its status decodes as `Running`), but
- /// its parent `g`'s tree carries no confirm leaf at all — `g` was never
- /// queued, so the claim should never have been admitted.
- #[test]
- fn a_claim_whose_parent_was_never_confirmed_is_rejected() {
- let mut f = base();
- f.ref_update = vec![(AGENT_SESSION.into(), Some("g".into()), "c1".into())];
- f.parent = vec![("c1".into(), "g".into())];
- f.signed_by = vec![("c1".into(), "key:joey".into())];
- f.session_running = vec![("c1".into(),)];
- f.session_plan_hash = vec![("g".into(), "hash:plan-a".into())];
- // No session_confirm_hash at all: `g` is awaiting confirmation, not
- // queued.
- let v = gate(f);
- assert!(
- v.iter()
- .any(|m| m.starts_with("session-running:") && m.contains("c1")),
- "{v:?}"
- );
- }
-
- /// RED (the stale-confirm variant): `g`'s confirm leaf binds an older
- /// plan hash than the one `g`'s own plan leaf now carries — a revision
- /// that should have dropped the confirm, per
- /// `ents_forge::agent::command::revise_plan`'s contract, evidently did
- /// not. The claim built on top of it is still rejected.
- #[test]
- fn a_claim_whose_parent_confirm_binds_a_stale_plan_hash_is_rejected() {
- let mut f = base();
- f.ref_update = vec![(AGENT_SESSION.into(), Some("g".into()), "c1".into())];
- f.parent = vec![("c1".into(), "g".into())];
- f.signed_by = vec![("c1".into(), "key:joey".into())];
- f.session_running = vec![("c1".into(),)];
- f.session_plan_hash = vec![("g".into(), "hash:plan-b".into())];
- f.session_confirm_hash = vec![("g".into(), "hash:plan-a".into())];
- let v = gate(f);
- assert!(
- v.iter()
- .any(|m| m.starts_with("session-running:") && m.contains("c1")),
- "{v:?}"
- );
- }
-
- /// GREEN: `c1` claims a session whose parent `g` was queued — `g`'s
- /// confirm leaf binds exactly `g`'s own plan-leaf hash. The rule's
- /// relation is empty; nothing else in the base fixture fires either.
- #[test]
- fn a_claim_whose_parent_was_confirmed_against_its_own_plan_passes() {
- let mut f = base();
- f.ref_update = vec![(AGENT_SESSION.into(), Some("g".into()), "c1".into())];
- f.parent = vec![("c1".into(), "g".into())];
- f.signed_by = vec![("c1".into(), "key:joey".into())];
- f.session_running = vec![("c1".into(),)];
- f.session_plan_hash = vec![("g".into(), "hash:plan-a".into())];
- f.session_confirm_hash = vec![("g".into(), "hash:plan-a".into())];
- assert!(gate(f).is_empty());
- }
}
crates/kernel/ents-gate/src/config.rs
@@ -1,25 +1,17 @@
-//! The verification epoch and designated-worker roster, read from
-//! `refs/meta/config` (`gate.epoch`, and this crate's own doc on
-//! "Finer-grained, config-stored refname rules").
+//! The verification epoch, read from `refs/meta/config` (`gate.epoch`).
use facet::Facet;
use gix_hash::ObjectId;
use gix_object::Find;
use gix_ref_store::RefStoreRead;
-use ents_model::MemberId;
-
use crate::error::{Error, Result};
use crate::object::expect_commit;
/// The slice of `refs/meta/config`'s typed tree the gate consults: the
-/// verification epoch (`gate.epoch`) and the designated-worker roster this
-/// crate's own module doc names as "a later, additive narrowing" —
-/// `docs/agent-sessions-plan.adoc`'s Phase 2a is the first consumer,
-/// narrowing agent-session advance authorization
-/// (`ents-gate`'s `owner_mutation`); a future narrowing of canonical
-/// `refs/meta/results/<effect>/*` (`effect.official`) would read the same
-/// field.
+/// verification epoch (`gate.epoch`). A later, additive narrowing (e.g. a
+/// designated-worker roster for `effect.official`) would read the same
+/// tree, alongside this field.
///
/// `model.sdoc` defines no Config entity yet, so this struct is the
/// first (and currently only) definition of the config tree's shape; it
@@ -41,7 +33,7 @@
/// ```
/// use ents_gate::Config;
///
-/// let config = Config { epoch: Some(1_700_000_000), ..Config::default() };
+/// let config = Config { epoch: Some(1_700_000_000) };
/// let (root, store) = facet_git_tree::serialize(&config).expect("serialize");
/// let back: Config = facet_git_tree::deserialize(&root, &store).expect("deserialize");
/// assert_eq!(back, config);
@@ -52,15 +44,6 @@
/// When the tip invariant came into force, seconds since the Unix
/// epoch; `None` while verification has never been enabled.
pub epoch: Option<u64>,
- /// Members trusted, alongside an entity's genesis signer and any
- /// admin-registered member, to advance an owner-mutation-gated
- /// namespace on another member's behalf — today, an agent session's
- /// claim/finish advance (`docs/agent-sessions-plan.adoc`'s Phase 2a);
- /// eventually the same roster narrowing `refs/meta/results/<effect>/*`
- /// per `effect.official`'s "designated worker keys", once that
- /// narrowing lands. Empty by default: no worker is designated until a
- /// signed config write adds one.
- pub workers: Vec<MemberId>,
}
/// The config recorded by the tree of the commit at `oid`, or an
@@ -106,16 +89,3 @@
pub(crate) fn current_epoch(refs: &dyn RefStoreRead, objects: &dyn Find) -> Result<Option<u64>> {
Ok(current_config(refs, objects)?.epoch)
}
-
-/// The designated-worker roster currently in force, read from
-/// `refs/meta/config`'s tip; empty when the config ref does not exist or
-/// designates no workers — [`crate::verify::verify`]'s AgentSession advance
-/// rule ORs this into the existing genesis-signer/admin check
-/// (`docs/agent-sessions-plan.adoc`'s Phase 2a).
-// @relation(gate.policy-as-state, scope=function)
-pub(crate) fn designated_workers(
- refs: &dyn RefStoreRead,
- objects: &dyn Find,
-) -> Result<Vec<MemberId>> {
- Ok(current_config(refs, objects)?.workers)
-}
crates/kernel/ents-gate/src/lib.rs
@@ -72,14 +72,11 @@
//! (`effect.admin-only`), and self-attested members are refused
//! canonical refs until promoted (`model.member-provenance`).
//! Finer-grained, config-stored refname rules are a later, additive
-//! narrowing read from the same [`Config`] the epoch already lives on
-//! (`Config::workers`): `docs/agent-sessions-plan.adoc`'s Phase 2a is the
-//! first one, admitting a designated worker to advance any member's agent
-//! session (∪ genesis signer, ∪ admins) — `verify::owner_mutation`'s
-//! `Namespace::AgentSession` arm. Designating worker keys for one effect's
-//! canonical results namespace (`effect.official`) is the same roster,
-//! unbuilt until a caller needs it; `Config` itself moves to `ents-model`
-//! only once configuration grows fields no gate rule reads.
+//! narrowing read from the same [`Config`] the epoch already lives on —
+//! e.g. designating worker keys for one effect's canonical results
+//! namespace (`effect.official`) — unbuilt until a caller needs it;
+//! `Config` itself moves to `ents-model` only once configuration grows
+//! fields no gate rule reads.
//!
//! Acceptance-time semantics: a signature is judged against the member
//! entity *currently in force* — the member ref's tip in the same
@@ -116,7 +113,7 @@
//! // 2. The epoch-setting commit is the first gated tip of refs/meta/config.
//! let config_ref: gix::refs::FullName = namespace::CONFIG_REF.try_into().expect("valid");
//! let epoch_tip = write_meta_entity(
-//! &refs, &objects, config_ref, &Config { epoch: Some(200), ..Config::default() }, Some(&key), 200,
+//! &refs, &objects, config_ref, &Config { epoch: Some(200) }, Some(&key), 200,
//! );
//!
//! // 3. From here on, every meta-ref update is judged by the tip invariant.
crates/kernel/ents-gate/src/verify.rs
@@ -244,22 +244,12 @@
}
// gate.owner-mutation: a hash-identified entity's ref advances only
- // under its genesis signer (∪ admins, ∪ designated workers for an
- // agent session — `docs/agent-sessions-plan.adoc`'s Phase 2a); a
- // review advances only under the member its refname names. Creation
- // stays provenance-keyed, already judged by `authorize` above.
+ // under its genesis signer (∪ admins); a review advances only under
+ // the member its refname names. Creation stays provenance-keyed,
+ // already judged by `authorize` above.
// @relation(gate.owner-mutation, scope=function)
- let workers = config::designated_workers(refs, objects)?;
- if let Some(refusal) = owner_mutation(
- objects,
- &update.name,
- old,
- new,
- &members,
- &id,
- &member,
- &workers,
- )? {
+ if let Some(refusal) = owner_mutation(objects, &update.name, old, new, &members, &id, &member)?
+ {
return Ok(Verdict::Fail(refusal));
}
@@ -621,14 +611,7 @@
Namespace::Effect | Namespace::Toolchain => {
bind_natural_key(objects, name, commit.tree, "name", &final_segment(name))
}
- // An agent session is hash-identified exactly like a comment or an
- // issue: the refname's final segment must be the genesis oid, and
- // the all-roots walk must find no doppelgänger
- // (`docs/agent-sessions-plan.adoc`'s Phase 1b, `meta-ref.
- // identity-binding`).
- Namespace::Comment | Namespace::Issue | Namespace::AgentSession => {
- bind_hash_identified(objects, name, new)
- }
+ Namespace::Comment | Namespace::Issue => bind_hash_identified(objects, name, new),
Namespace::Review => {
let Some((target, member)) = namespace::parse_review_ref(name.as_ref()) else {
return Ok(binding_refusal(
@@ -792,18 +775,11 @@
}
/// Ownership keys mutation (`gate.owner-mutation`): a hash-identified
-/// entity's ref advances only under its genesis signer, an admin-registered
-/// member, or — for an agent session only — a designated worker
-/// (`docs/agent-sessions-plan.adoc`'s Phase 2a, `workers`); a review
-/// advances only under the member its refname names. Creation stays
-/// provenance-keyed (judged by `authorize`), so this fires only on an
-/// advance.
+/// entity's ref advances only under its genesis signer or an
+/// admin-registered member; a review advances only under the member its
+/// refname names. Creation stays provenance-keyed (judged by `authorize`),
+/// so this fires only on an advance.
// @relation(gate.owner-mutation, scope=function)
-#[expect(
- clippy::too_many_arguments,
- reason = "a private continuation of verify(); grouping these into a struct would only rename \
- the arguments, same rationale as bootstrap()'s own expect"
-)]
fn owner_mutation(
objects: &dyn Find,
name: &FullName,
@@ -812,7 +788,6 @@
members: &[Enrolled],
signer_id: &MemberId,
signer: &Member,
- workers: &[MemberId],
) -> Result<Option<Refusal>> {
let Some(namespace) = namespace::classify(name.as_ref()) else {
return Ok(None);
@@ -852,46 +827,6 @@
)))
}
}
- // An agent session's mutation owner is its genesis signer (∪
- // admins, ∪ designated workers): Phase 1b kept this owner-only
- // because `ents-gate` had no notion of "a claim, once made,
- // authorizes a different signer"; Phase 2a is that machinery — a
- // signer listed in `refs/meta/config`'s `workers` roster
- // (`Config::workers`) may advance any member's session, exactly the
- // additive narrowing this crate's own module doc predicted for a
- // future `effect.official` roster over `refs/meta/results/*`. A
- // worker's advance still passes through every other check
- // unchanged: `gate.fast-forward` still refuses a non-descendant
- // tip, and `gate.identity-binding` still refuses a mismatched
- // refname — a designated worker gains a new signer authorized to
- // advance the ref, never a way around what "advance" means.
- Namespace::AgentSession => {
- // Creation is provenance-keyed; only an advance is owner-keyed.
- if old.is_none() {
- return Ok(None);
- }
- if is_admin {
- return Ok(None);
- }
- if workers.iter().any(|worker| worker == signer_id) {
- return Ok(None);
- }
- let genesis = all_roots(objects, new)?;
- let genesis_signer = match genesis.first() {
- Some(root) => commit_signer(objects, members, *root)?,
- None => None,
- };
- if genesis_signer.as_ref() == Some(signer_id) {
- Ok(None)
- } else {
- Ok(refuse(format!(
- "{signer_id} is neither the member whose signature this entity's genesis \
- carries, an admin-registered member, nor a designated worker, so may not \
- advance {}",
- name.as_bstr()
- )))
- }
- }
Namespace::Review => {
let Some((_, member)) = namespace::parse_review_ref(name.as_ref()) else {
return Ok(None);
crates/kernel/ents-gate/tests/gate.rs
@@ -65,10 +65,7 @@
&refs,
&objects,
config_ref,
- &Config {
- epoch: Some(200),
- ..Config::default()
- },
+ &Config { epoch: Some(200) },
Some(&admin),
200,
);
@@ -90,13 +87,6 @@
name(&format!("refs/meta/issues/{genesis}"))
}
-/// The oid-keyed refname of an agent session whose genesis is `genesis` —
-/// hash-identified exactly like [`issue_ref`] (`docs/agent-sessions-plan.
-/// adoc`'s Phase 1b).
-fn agent_session_ref(genesis: ObjectId) -> FullName {
- name(&format!("refs/meta/agent-sessions/{genesis}"))
-}
-
/// A signed empty-tree commit — a generic meta-mutation body. A
/// hash-identified entity binds by its genesis oid and the all-roots
/// walk, not by tree content, so an empty tree exercises the tip
@@ -535,39 +525,6 @@
);
}
-#[rstest]
-// @relation(gate.identity-binding, meta-ref.identity-binding, scope=function, role=Verifies)
-fn an_agent_session_binds_by_its_genesis_oid_like_other_hash_identified_entities() {
- // Phase 1b: refs/meta/agent-sessions/* binds exactly like
- // refs/meta/issues/* and refs/meta/comments/* — genesis oid plus the
- // all-roots walk, no natural-key tree field involved.
- let f = forge();
- let genesis = commit(&f, vec![], Some(&f.admin), 300);
- expect_pass(
- &run(&f, &agent_session_ref(genesis), Some(genesis)),
- AdmissionKind::TipInvariant,
- );
-
- let advance = commit(&f, vec![genesis], Some(&f.admin), 310);
- let refname = agent_session_ref(genesis);
- f.refs.set(refname.as_ref(), genesis);
- expect_pass(
- &run(&f, &refname, Some(advance)),
- AdmissionKind::TipInvariant,
- );
-
- // A refname whose segment is not the tip's own genesis is refused, the
- // same doppelgänger check `a_refname_not_naming_the_genesis_oid_is_refused`
- // exercises for issues.
- let wrong = agent_session_ref(
- ObjectId::from_hex(b"00000000000000000000000000000000deadbeef").expect("hex"),
- );
- expect_fail(
- &run(&f, &wrong, Some(genesis)),
- Requirement::IdentityBinding,
- );
-}
-
#[rstest]
// @relation(gate.identity-binding, model.review-pin, scope=function, role=Verifies)
fn a_pin_is_never_subjected_to_the_all_roots_walk() {
@@ -795,156 +752,6 @@
expect_fail(&verdict, Requirement::TipSigned);
}
-#[rstest]
-// @relation(gate.owner-mutation, scope=function, role=Verifies)
-fn an_admin_may_advance_another_members_agent_session() {
- // Same owner-mutation rule as a comment's: the genesis signer or an
- // admin-registered member may advance an agent session (Phase 1b keeps
- // this owner-only — see `owner_mutation`'s doc on why a worker's
- // status-advance is deliberately not modeled here yet).
- let f = forge();
- let second = Keypair::from_seed(OUTSIDER_SEED);
- enroll_member(
- &f.refs,
- &f.objects,
- "second",
- &second,
- Provenance::AdminRegistered,
- 210,
- );
-
- let genesis = commit(&f, vec![], Some(&f.admin), 300);
- let refname = agent_session_ref(genesis);
- f.refs.set(refname.as_ref(), genesis);
- let advance = commit(&f, vec![genesis], Some(&second), 310);
- expect_pass(
- &run(&f, &refname, Some(advance)),
- AdmissionKind::TipInvariant,
- );
-}
-
-#[rstest]
-// @relation(gate.owner-mutation, model.member-provenance, scope=function, role=Verifies)
-fn a_self_attested_non_owner_cannot_advance_an_agent_session() {
- // Mirrors `a_self_attested_non_owner_cannot_advance_a_comment`: a
- // self-attested member is not authorized for canonical refs at all
- // (creation stays provenance-keyed, routed to the inbox), so it cannot
- // advance someone else's agent session either.
- let f = forge();
- let genesis = commit(&f, vec![], Some(&f.admin), 300);
- let refname = agent_session_ref(genesis);
- f.refs.set(refname.as_ref(), genesis);
- let advance = commit(&f, vec![genesis], Some(&f.guest), 310);
- expect_fail(&run(&f, &refname, Some(advance)), Requirement::TipSigned);
-}
-
-// ---------------------------------------------------------------------
-// Designated workers (`docs/agent-sessions-plan.adoc`'s Phase 2a): an
-// additive ∪ onto the agent-session advance rule, read from
-// `refs/meta/config`'s `workers` roster.
-// ---------------------------------------------------------------------
-
-/// Advance `refs/meta/config` past `forge()`'s own epoch-setting write,
-/// designating `worker_id` in [`Config::workers`] — a signed fast-forward
-/// exactly like any other config mutation, not a special write path.
-fn designate_worker(f: &Forge, worker_id: &str, seconds: i64) {
- let config_ref: FullName = namespace::CONFIG_REF.try_into().expect("valid");
- write_meta_entity(
- &f.refs,
- &f.objects,
- config_ref,
- &Config {
- epoch: Some(200),
- workers: vec![MemberId::new(worker_id)],
- },
- Some(&f.admin),
- seconds,
- );
-}
-
-#[rstest]
-// @relation(gate.owner-mutation, model.member-worker, scope=function, role=Verifies)
-fn a_designated_worker_may_advance_another_members_agent_session() {
- // The new ∪ term: a member listed in `refs/meta/config`'s `workers`
- // roster may advance a session it neither created nor administers —
- // the machinery Phase 1b deferred ("a worker's status-advance commits
- // stay Phase 2's concern").
- let f = forge();
- let worker = Keypair::from_seed(OUTSIDER_SEED);
- enroll_member(
- &f.refs,
- &f.objects,
- "worker",
- &worker,
- Provenance::AdminRegistered,
- 210,
- );
- designate_worker(&f, "worker", 220);
-
- let genesis = commit(&f, vec![], Some(&f.admin), 300);
- let refname = agent_session_ref(genesis);
- f.refs.set(refname.as_ref(), genesis);
- let advance = commit(&f, vec![genesis], Some(&worker), 310);
- expect_pass(
- &run(&f, &refname, Some(advance)),
- AdmissionKind::TipInvariant,
- );
-}
-
-#[rstest]
-// @relation(gate.owner-mutation, model.member-provenance, model.member-worker, scope=function, role=Verifies)
-fn an_unpromoted_member_cannot_advance_an_agent_session_merely_by_being_listed_as_a_worker() {
- // Listing a member id in `Config::workers` is not itself a promotion:
- // a self-attested member stays refused for canonical refs
- // (`model.member-provenance`) even when a signed config write names
- // its id as a designated worker — the roster narrows an already
- // *authorized* signer's owner-mutation reach, it does not grant
- // canonical-ref authorization on its own. This is the "undesignated
- // member may not" boundary in this codebase's actual two-tier
- // provenance model: the only signer that ever reaches the
- // owner-mutation check unauthorized is a self-attested one, and even a
- // roster listing cannot carry it past `authorize`'s earlier refusal.
- let f = forge();
- designate_worker(&f, "guest", 220);
-
- let genesis = commit(&f, vec![], Some(&f.admin), 300);
- let refname = agent_session_ref(genesis);
- f.refs.set(refname.as_ref(), genesis);
- let advance = commit(&f, vec![genesis], Some(&f.guest), 310);
- expect_fail(&run(&f, &refname, Some(advance)), Requirement::TipSigned);
-}
-
-#[rstest]
-// @relation(gate.owner-mutation, gate.fast-forward, model.member-worker, scope=function, role=Verifies)
-fn a_designated_worker_still_cannot_rewrite_history() {
- // Owner-mutation admitting a new signer is not a way around what
- // "advance" means: a designated worker's tip that does not descend
- // from the session's current tip is still refused for fast-forward,
- // exactly as it would be for the genesis signer or an admin.
- let f = forge();
- let worker = Keypair::from_seed(OUTSIDER_SEED);
- enroll_member(
- &f.refs,
- &f.objects,
- "worker",
- &worker,
- Provenance::AdminRegistered,
- 210,
- );
- designate_worker(&f, "worker", 220);
-
- let genesis = commit(&f, vec![], Some(&f.admin), 300);
- let refname = agent_session_ref(genesis);
- let first_advance = commit(&f, vec![genesis], Some(&f.admin), 305);
- f.refs.set(refname.as_ref(), first_advance);
- // `sibling` is rooted at the same genesis (so identity-binding's
- // all-roots walk holds) but does not descend from `first_advance`, the
- // ref's current tip — a worker signing a divergent sibling is still
- // refused for fast-forward, not admitted as an "advance".
- let sibling = commit(&f, vec![genesis], Some(&worker), 310);
- expect_fail(&run(&f, &refname, Some(sibling)), Requirement::FastForward);
-}
-
#[rstest]
// @relation(gate.owner-mutation, model.review, scope=function, role=Verifies)
fn the_wrong_member_cannot_advance_a_review() {
@@ -1149,10 +956,7 @@
};
let tree = facet_git_tree::serialize_into(
- &Config {
- epoch: Some(200),
- ..Config::default()
- },
+ &Config { epoch: Some(200) },
&f.objects,
)
.expect("config serializes");
@@ -1195,10 +999,7 @@
&refs,
&objects,
config_ref,
- &Config {
- epoch: Some(50),
- ..Config::default()
- },
+ &Config { epoch: Some(50) },
None,
50,
);
@@ -1403,16 +1204,7 @@
#[rstest]
// @relation(gate.epoch, scope=function, role=Verifies)
fn config_round_trips_with_and_without_an_epoch() {
- for config in [
- Config {
- epoch: None,
- ..Config::default()
- },
- Config {
- epoch: Some(42),
- ..Config::default()
- },
- ] {
+ for config in [Config { epoch: None }, Config { epoch: Some(42) }] {
let (root, store) = facet_git_tree::serialize(&config).expect("serialize");
let back: Config = facet_git_tree::deserialize(&root, &store).expect("deserialize");
assert_eq!(back, config);
crates/kernel/ents-model/src/namespace.rs
@@ -61,26 +61,6 @@
build(format!("refs/meta/comments/{id}"))
}
-/// The ref holding the agent session named `id` — `refs/meta/agent-sessions/<id>`
-/// (`meta-ref.granularity`), where `<id>` is the oid of the session's own
-/// genesis commit — the same sign-then-name shape [`issue_ref`] and
-/// [`comment_ref`] bind by (`meta-ref.identity-binding`); `ents_forge::agent`
-/// (`docs/agent-sessions-plan.adoc`'s Phase 1) carries the entity and
-/// extracts `id` from the proposed ref the same way `ents-forge`'s own
-/// `genesis_id` does for a comment or issue.
-///
-/// [`classify`] routes this namespace to [`Namespace::AgentSession`]
-/// (`docs/agent-sessions-plan.adoc`'s Phase 1b), which `ents-gate`'s
-/// identity-binding and owner-mutation checks treat exactly like
-/// [`Namespace::Comment`] and [`Namespace::Issue`]: hash-identified by the
-/// genesis oid, mutable only by the genesis signer or an admin. The
-/// still-unwritten `meta-ref.adoc` namespace entry and `model.agent-session`
-/// section remain owner spec text (`docs/agent-sessions-plan.adoc`'s "Owner
-/// spec text needed").
-pub fn agent_session_ref(id: &str) -> Result<FullName> {
- build(format!("refs/meta/agent-sessions/{id}"))
-}
-
/// The ref holding one reviewer's review of one commit —
/// `refs/meta/reviews/<target>/<member>` (`meta-ref.granularity`,
/// `model.review`), where `<target>` is the oid of the first commit the
@@ -383,11 +363,6 @@
Issue,
/// `refs/meta/comments/*`.
Comment,
- /// `refs/meta/agent-sessions/*` — hash-identified by the session's own
- /// genesis commit oid, the same shape as [`Namespace::Comment`] and
- /// [`Namespace::Issue`] (`docs/agent-sessions-plan.adoc`'s Phase 1
- /// [`agent_session_ref`], Phase 1b for this classification).
- AgentSession,
/// `refs/meta/reviews/*`.
Review,
/// `refs/meta/pins/*` — retention pins (`model.review-pin`,
@@ -469,7 +444,6 @@
"member" => Some(Namespace::Member),
"issues" => Some(Namespace::Issue),
"comments" => Some(Namespace::Comment),
- "agent-sessions" => Some(Namespace::AgentSession),
"reviews" => Some(Namespace::Review),
"pins" => Some(Namespace::Pin),
"effects" => Some(Namespace::Effect),
@@ -543,7 +517,6 @@
#[case::outside_meta("refs/heads/main", None)]
#[case::unrecognized("refs/meta/index/abc", Some(Namespace::Unknown))]
#[case::novel_namespace("refs/meta/widgets/7", Some(Namespace::Unknown))]
- #[case::agent_session("refs/meta/agent-sessions/deadbeef", Some(Namespace::AgentSession))]
// @relation(meta-ref.namespace, meta-ref.granularity, scope=function, role=Verifies)
fn classify_matches_the_namespace_table(
#[case] refname: &str,
@@ -605,7 +578,6 @@
member_ref(&id).expect("valid"),
issue_ref("42").expect("valid"),
comment_ref("abc").expect("valid"),
- agent_session_ref("abc").expect("valid"),
review_ref("deadbeef", &id).expect("valid"),
review_pin_ref("deadbeef", &id).expect("valid"),
effect_ref("unit").expect("valid"),
crates/kernel/ents-receive/src/lib.rs
@@ -70,7 +70,7 @@
//! enroll_member(&refs, &objects, "admin", &admin, Provenance::AdminRegistered, 100);
//! let config_ref: gix::refs::FullName = namespace::CONFIG_REF.try_into().expect("valid");
//! let tip = write_meta_entity(
-//! &refs, &objects, config_ref.clone(), &Config { epoch: Some(200), ..Config::default() }, Some(&admin), 200,
+//! &refs, &objects, config_ref.clone(), &Config { epoch: Some(200) }, Some(&admin), 200,
//! );
//!
//! // The fixture already moved the ref; re-propose the same tip through
crates/kernel/ents-receive/src/propose.rs
@@ -141,10 +141,8 @@
///
/// Public so a caller assembling a larger, bespoke atomic multi-ref
/// proposal (`receive.multi-ref-atomicity`) than [`propose_entity_with_pin`]
-/// covers — for instance `ents-forge`'s agent-session `finish` alongside a
-/// result record and a result branch, `docs/agent-sessions-plan.adoc`'s
-/// Phase 2 finalize — can build one of its transitions with the identical
-/// signing plumbing every entity mutation uses, then bundle it into its own
+/// covers can build one of its transitions with the identical signing
+/// plumbing every entity mutation uses, then bundle it into its own
/// [`Proposal`] and call [`crate::receive`] exactly once. This does not
/// widen `receive`'s own contract: the transition still only becomes
/// durable through that one call.