Skip to content

Commit a113913

Browse files
feat(core): scope hosted review identity
Fixes OpenCoven#98. Fixes OpenCoven#99. Fixes OpenCoven#104. Fixes OpenCoven#110. - add hosted tenant/installation/repo/domain scope types - key hosted memory, transcripts, settings sync, and team memory by scoped identity - add canonical repo parsing and Windows test isolation for home-derived paths Signed-off-by: Timothy Wayne Gregg <Timothy.Gregg@complete.tech>
1 parent ba1cbc3 commit a113913

16 files changed

Lines changed: 682 additions & 24 deletions

docs/advanced.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,8 +531,10 @@ Hosted review mode is enabled with `--hosted-review`,
531531
Coven Code does not load user-scope memory by default. The prompt records that
532532
hosted review mode is active and lists the AGENTS.md scopes that were loaded.
533533
Durable hosted memory and transcript namespaces are separated under a hosted
534-
review path and require tenant scope plus a canonical repository identity before
535-
they can be resolved for persistence.
534+
review path and require tenant scope, GitHub App installation id, stable repo
535+
id, and canonical repository identity before they can be resolved for
536+
persistence. Hosted namespaces include a domain component so default branch,
537+
branch, pull-request, release, and security-private memory stay separated.
536538

537539
Hosted review mode also disables write/execute-capable tools, configured MCP
538540
servers, plugins, user memory, and managed rules by default. Trusted hosted

docs/configuration.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,11 @@ Or in `settings.json`:
129129
When hosted review mode is active, Coven Code skips user-scope memory
130130
(`~/.coven-code/AGENTS.md` and `~/.coven-code/CLAUDE.md`) by default, marks
131131
new session artifacts as hosted review artifacts, and requires a tenant plus
132-
canonical repository identity before resolving hosted durable memory paths.
133-
Local-personal mode remains the default and continues to load user memory.
132+
GitHub App installation id, stable repository id, and canonical repository
133+
identity before resolving hosted durable memory paths. Hosted memory and
134+
transcripts are also split by memory domain, such as default branch, named
135+
branch, pull request, release, or security-private review. Local-personal mode
136+
remains the default and continues to load user memory.
134137

135138
Hosted review also disables write/execute-capable tools, configured MCP
136139
servers, and plugins by default. A trusted hosted policy can opt individual
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Hosted Review Phase 2 PR Notes
2+
3+
## Linked issues
4+
5+
Fixes #98.
6+
Fixes #99.
7+
Fixes #104.
8+
Fixes #110.
9+
10+
## Summary
11+
12+
- Expands hosted review scope to include tenant id, GitHub App installation id, stable repo id, repo full name, canonical repo identity, and memory domain.
13+
- Moves hosted memory and transcript paths from local-path identity to tenant/installation/repo/domain namespaces.
14+
- Adds canonical GitHub repo identity parsing for HTTPS and SSH remotes plus deterministic local project ids.
15+
- Adds hosted-derived settings sync project keys and hosted team-memory repo keys so hosted callers do not pass arbitrary project ids.
16+
- Splits hosted memory domains for default branch, branch, release, pull request, and security-private review contexts.
17+
- Hardens Windows test isolation for home-derived paths and gates Unix-socket daemon tests to Unix.
18+
19+
## Test evidence
20+
21+
- `cargo fmt --all`
22+
- `cargo check --workspace`
23+
- `cargo clippy --workspace --all-targets -- -D warnings`
24+
- `cargo test -p claurst-core --lib hosted -- --nocapture`
25+
- `cargo test -p claurst-core --lib local_project_id -- --nocapture`
26+
- `cargo test -p claurst-core --lib sync_keys -- --nocapture`
27+
- `cargo test -p claurst-core --lib team_memory_key -- --nocapture`
28+
- `cargo test -p claurst-commands --lib named_commands::tests::test_agents_reset_removes_saved_roster_state -- --nocapture`
29+
- `cargo test -p claurst-core --lib roster_reset -- --nocapture`
30+
- `cargo test -p claurst-core --lib build_import_preview_maps_settings_and_doc -- --nocapture`
31+
- `cargo test -p claurst-core --lib test_imported_anthropic_cli_token_resolves_without_coven_oauth_client -- --nocapture`
32+
33+
## Full-suite status
34+
35+
- `cargo test --workspace` progressed through CLI, ACP, API, bridge, buddy, commands, and core tests, then Smart App Control blocked an unsigned freshly built test binary.
36+
- Latest block: `C:\dev-cargo-target\coven-code\debug\deps\claurst_tools-f5b8d284b5de1d1b.exe`, OS error 4551.
37+
- Code Integrity event: Smart App Control reported the binary did not meet Enterprise signing level requirements under policy `{0283ac0f-fff1-49ae-ada1-8a933130cad6}`.
38+
39+
## Risk notes
40+
41+
- Local mode pathing remains available through existing local APIs.
42+
- Hosted durable state now requires explicit scope to avoid path-derived cross-tenant collisions.
43+
- Security-private domains are represented and are excluded from public review loading unless explicitly allowed by policy.

src-rust/crates/commands/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10363,6 +10363,8 @@ pub(crate) mod test_env {
1036310363

1036410364
pub(crate) struct CommandEnvGuard {
1036510365
old_home: Option<String>,
10366+
old_test_home: Option<String>,
10367+
old_userprofile: Option<String>,
1036610368
old_coven_home: Option<String>,
1036710369
old_user: Option<String>,
1036810370
old_username: Option<String>,
@@ -10376,6 +10378,8 @@ pub(crate) mod test_env {
1037610378
let lock = ENV_LOCK.lock().unwrap_or_else(|err| err.into_inner());
1037710379
let guard = Self {
1037810380
old_home: std::env::var("HOME").ok(),
10381+
old_test_home: std::env::var("COVEN_CODE_TEST_HOME").ok(),
10382+
old_userprofile: std::env::var("USERPROFILE").ok(),
1037910383
old_coven_home: std::env::var("COVEN_HOME").ok(),
1038010384
old_user: std::env::var("USER").ok(),
1038110385
old_username: std::env::var("USERNAME").ok(),
@@ -10384,6 +10388,8 @@ pub(crate) mod test_env {
1038410388
_lock: lock,
1038510389
};
1038610390
std::env::set_var("HOME", home);
10391+
std::env::set_var("COVEN_CODE_TEST_HOME", home);
10392+
std::env::set_var("USERPROFILE", home);
1038710393
std::env::set_var("COVEN_HOME", coven_home);
1038810394
match user {
1038910395
Some(value) => std::env::set_var("USER", value),
@@ -10415,6 +10421,14 @@ pub(crate) mod test_env {
1041510421
Some(value) => std::env::set_var("HOME", value),
1041610422
None => std::env::remove_var("HOME"),
1041710423
}
10424+
match &self.old_test_home {
10425+
Some(value) => std::env::set_var("COVEN_CODE_TEST_HOME", value),
10426+
None => std::env::remove_var("COVEN_CODE_TEST_HOME"),
10427+
}
10428+
match &self.old_userprofile {
10429+
Some(value) => std::env::set_var("USERPROFILE", value),
10430+
None => std::env::remove_var("USERPROFILE"),
10431+
}
1041810432
match &self.old_coven_home {
1041910433
Some(value) => std::env::set_var("COVEN_HOME", value),
1042010434
None => std::env::remove_var("COVEN_HOME"),

src-rust/crates/commands/src/named_commands.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,12 +1012,12 @@ mod tests {
10121012
let home = temp.path().join("home");
10131013
let coven_home = temp.path().join("coven");
10141014
let project = temp.path().join("project");
1015-
let global_agents = home.join(".coven-code").join("agents");
10161015
let project_agents = project.join(".coven-code").join("agents");
1017-
std::fs::create_dir_all(&global_agents).expect("global agents dir");
10181016
std::fs::create_dir_all(&project_agents).expect("project agents dir");
10191017
std::fs::create_dir_all(&coven_home).expect("coven home");
10201018
let _guard = CommandEnvGuard::set(&home, &coven_home, None);
1019+
let global_agents = claurst_core::Settings::config_dir().join("agents");
1020+
std::fs::create_dir_all(&global_agents).expect("global agents dir");
10211021

10221022
let global_agent = global_agents.join("global.md");
10231023
let project_agent = project_agents.join("project.md");

src-rust/crates/core/src/anthropic_cli_import.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,18 @@ pub struct DiscoveredCredential {
4747
// ---------------------------------------------------------------------------
4848

4949
fn claude_code_credentials_path() -> Option<PathBuf> {
50-
Some(dirs::home_dir()?.join(".claude").join(".credentials.json"))
50+
Some(cli_home_dir()?.join(".claude").join(".credentials.json"))
51+
}
52+
53+
fn cli_home_dir() -> Option<PathBuf> {
54+
#[cfg(test)]
55+
if let Ok(home) = std::env::var("COVEN_CODE_TEST_HOME") {
56+
if !home.is_empty() {
57+
return Some(PathBuf::from(home));
58+
}
59+
}
60+
61+
dirs::home_dir()
5162
}
5263

5364
fn ant_credentials_dir() -> Option<PathBuf> {

src-rust/crates/core/src/coven_daemon.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,7 @@ fn url_quote(input: &str) -> String {
771771
mod tests {
772772
use super::*;
773773
use crate::coven_shared::COVEN_HOME_ENV_LOCK;
774+
#[cfg(unix)]
774775
use std::fs;
775776

776777
/// Guard that temporarily sets `COVEN_HOME` and restores it on drop.
@@ -805,6 +806,7 @@ mod tests {
805806
assert!(DaemonClient::new().is_none());
806807
}
807808

809+
#[cfg(unix)]
808810
#[test]
809811
fn new_returns_some_when_sock_present() {
810812
let _lock = COVEN_HOME_ENV_LOCK
@@ -873,6 +875,7 @@ mod tests {
873875
assert_eq!(s1.active_sessions, 0);
874876
}
875877

878+
#[cfg(unix)]
876879
#[test]
877880
fn familiar_statuses_returns_offline_when_connect_fails() {
878881
let _lock = COVEN_HOME_ENV_LOCK

src-rust/crates/core/src/git_utils.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Git utilities for Coven Code.
22
//! Mirrors src/utils/git.ts (926 lines) and src/utils/git/ subdirectory.
33
4+
use crate::hosted_review::CanonicalRepoIdentity;
5+
use sha2::{Digest, Sha256};
46
use std::path::{Path, PathBuf};
57
use std::process::Command;
68

@@ -49,6 +51,28 @@ pub fn get_current_branch(repo_root: &Path) -> String {
4951
}
5052
}
5153

54+
pub fn get_origin_remote_url(repo_root: &Path) -> Option<String> {
55+
let remote = git_output(repo_root, &["remote", "get-url", "origin"]);
56+
(!remote.is_empty()).then_some(remote)
57+
}
58+
59+
pub fn canonical_repo_identity_from_origin(repo_root: &Path) -> Option<CanonicalRepoIdentity> {
60+
let remote = get_origin_remote_url(repo_root)?;
61+
CanonicalRepoIdentity::from_git_remote_url(&remote)
62+
}
63+
64+
pub fn local_project_id_from_identity(identity: &CanonicalRepoIdentity) -> String {
65+
let mut hasher = Sha256::new();
66+
hasher.update(identity.canonical_string().as_bytes());
67+
let digest = hex::encode(hasher.finalize());
68+
format!("local-git-{}", &digest[..16])
69+
}
70+
71+
pub fn local_project_id_from_origin(repo_root: &Path) -> Option<String> {
72+
canonical_repo_identity_from_origin(repo_root)
73+
.map(|identity| local_project_id_from_identity(&identity))
74+
}
75+
5276
/// Return list of files modified (staged or unstaged).
5377
pub fn list_modified_files(repo_root: &Path) -> Vec<PathBuf> {
5478
let output = git_output(repo_root, &["diff", "--name-only", "HEAD"]);
@@ -216,4 +240,20 @@ mod tests {
216240
let commits = get_commit_history(Path::new("."), 0);
217241
assert!(commits.is_empty());
218242
}
243+
244+
#[test]
245+
fn local_project_id_normalizes_equivalent_https_and_ssh_remotes() {
246+
let https = CanonicalRepoIdentity::from_git_remote_url(
247+
"https://github.com/OpenCoven/coven-code.git",
248+
)
249+
.unwrap();
250+
let ssh =
251+
CanonicalRepoIdentity::from_git_remote_url("git@github.com:OpenCoven/coven-code.git")
252+
.unwrap();
253+
254+
assert_eq!(
255+
local_project_id_from_identity(&https),
256+
local_project_id_from_identity(&ssh)
257+
);
258+
}
219259
}

0 commit comments

Comments
 (0)