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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,20 @@ policy settings such as `hostedReview.allowManagedRules`,
`hostedReview.allowPlugins` can opt specific surfaces back in for controlled
deployments. Prefer tenant-approved managed rules over `allowUserMemory`.

Session memory extraction is also approval-gated in hosted review mode.
Untrusted fork or contributor sessions cannot automatically append learned
facts to durable `.coven-code/AGENTS.md` memory. Instead, extracted memories
are written as JSON candidates under `.coven-code/memory-candidates/` with
content, semantic category, confidence, provenance, source trust, proposed
scope, proposed visibility, status, and rejection reason metadata. Approved
candidates can be promoted into durable memory as maintainer-approved entries;
rejected candidates remain artifacts and are not loaded into future prompts.

Direct hosted auto-persistence requires an explicit trusted policy:
`hostedReview.allowAutoMemoryPersistence` must be true and
`hostedReview.memorySourceTrust` must meet or exceed
`hostedReview.memoryTrustThreshold`.

---

## Security and permissions
Expand Down
29 changes: 29 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,35 @@ shared surfaces back in:
review jobs should prefer tenant-approved managed rules over operator-global
user memory.

Auto-extracted memories are approval-gated in hosted review mode. By default,
hosted sessions write reviewable JSON candidates under
`.coven-code/memory-candidates/` instead of appending directly to durable
`.coven-code/AGENTS.md` memory. Each candidate records content, category,
confidence, provenance, source trust, proposed scope, proposed visibility,
status, and any rejection reason.

Trusted deployments can opt into direct durable writes only when the source
trust meets the configured threshold:

```json
{
"config": {
"hostedReview": {
"enabled": true,
"allowAutoMemoryPersistence": true,
"memorySourceTrust": "maintainer-approved",
"memoryTrustThreshold": "maintainer-approved"
}
}
}
```

Supported `memorySourceTrust` and `memoryTrustThreshold` values are
`system-policy`, `maintainer-approved`, `default-branch-code`,
`contributor-input`, `fork-input`, `model-inferred`, and `unknown`.
Untrusted fork or contributor contexts should leave durable persistence
disabled and promote only reviewed candidates.

### Tool access

| Key | Type | Default | Description |
Expand Down
30 changes: 30 additions & 0 deletions docs/shared/hosted-review-phase-3-pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Hosted Review Phase 3 PR Notes

## Linked issues

Fixes #101.
Fixes #107.
Fixes #108.

## Summary

- Adds hosted memory source trust classification with configurable `memorySourceTrust`, `memoryTrustThreshold`, and `allowAutoMemoryPersistence`.
- Routes hosted session memory extraction through a policy gate instead of always appending to durable `.coven-code/AGENTS.md`.
- Writes untrusted or unapproved hosted extractions as reviewable JSON candidates under `.coven-code/memory-candidates/`.
- Adds candidate approval and rejection APIs; approval promotes candidates to durable memory as maintainer-approved entries, while rejection records a reason without durable writes.
- Preserves local mode direct memory persistence.

## Test evidence

- `cargo fmt --all -- --check`
- `cargo check --workspace`
- `cargo clippy --workspace --all-targets -- -D warnings`
- `cargo test -p claurst-core --lib hosted --quiet`
- `cargo test -p claurst-query session_memory --quiet`
- `cargo test --workspace --quiet`

## Risk notes

- Candidate approval/rejection is exposed as Rust API surface in this phase; hosted dashboard or CLI wiring can call it in a later integration PR.
- Hosted direct durable writes remain disabled by default and require both explicit policy and sufficient source trust.
- Candidate artifacts are not loaded into prompts by the existing memory loader, so rejected or pending candidates do not affect future sessions.
114 changes: 113 additions & 1 deletion src-rust/crates/core/src/hosted_review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl RuntimeMode {
}

/// Settings-backed hosted review configuration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostedReviewConfig {
#[serde(default, skip_serializing_if = "is_false")]
Expand All @@ -34,6 +34,31 @@ pub struct HostedReviewConfig {
pub allow_mcp_servers: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub allow_plugins: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub allow_auto_memory_persistence: bool,
#[serde(default, skip_serializing_if = "MemorySourceTrust::is_unknown")]
pub memory_source_trust: MemorySourceTrust,
#[serde(
default = "default_memory_trust_threshold",
skip_serializing_if = "is_default_memory_trust_threshold"
)]
pub memory_trust_threshold: MemorySourceTrust,
}

impl Default for HostedReviewConfig {
fn default() -> Self {
Self {
enabled: false,
allow_user_memory: false,
allow_managed_rules: false,
allow_write_tools: false,
allow_mcp_servers: false,
allow_plugins: false,
allow_auto_memory_persistence: false,
memory_source_trust: MemorySourceTrust::Unknown,
memory_trust_threshold: default_memory_trust_threshold(),
}
}
}

impl HostedReviewConfig {
Expand All @@ -44,9 +69,71 @@ impl HostedReviewConfig {
&& !self.allow_write_tools
&& !self.allow_mcp_servers
&& !self.allow_plugins
&& !self.allow_auto_memory_persistence
&& self.memory_source_trust == MemorySourceTrust::Unknown
&& self.memory_trust_threshold == default_memory_trust_threshold()
}

pub fn memory_source_trust(&self) -> MemorySourceTrust {
self.memory_source_trust
}

pub fn memory_trust_threshold(&self) -> MemorySourceTrust {
self.memory_trust_threshold
}

pub fn allows_auto_memory_persistence(&self) -> bool {
self.allow_auto_memory_persistence
&& self
.memory_source_trust
.meets_threshold(self.memory_trust_threshold)
}
}

/// Trust classification for the source that produced or approved memory.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum MemorySourceTrust {
SystemPolicy,
MaintainerApproved,
DefaultBranchCode,
ContributorInput,
ForkInput,
ModelInferred,
#[default]
Unknown,
}

impl MemorySourceTrust {
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}

pub fn meets_threshold(self, threshold: Self) -> bool {
self.rank() >= threshold.rank()
}

fn rank(self) -> u8 {
match self {
Self::Unknown => 0,
Self::ForkInput => 10,
Self::ContributorInput => 20,
Self::ModelInferred => 30,
Self::DefaultBranchCode => 60,
Self::MaintainerApproved => 80,
Self::SystemPolicy => 100,
}
}
}

fn default_memory_trust_threshold() -> MemorySourceTrust {
MemorySourceTrust::MaintainerApproved
}

fn is_default_memory_trust_threshold(value: &MemorySourceTrust) -> bool {
*value == default_memory_trust_threshold()
}

/// Canonical repository identity supplied by the hosted control plane or
/// derived from a git remote for local diagnostics.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -327,6 +414,31 @@ mod tests {
);
}

#[test]
fn memory_source_trust_enforces_threshold_order() {
assert!(MemorySourceTrust::MaintainerApproved
.meets_threshold(MemorySourceTrust::DefaultBranchCode));
assert!(!MemorySourceTrust::ContributorInput
.meets_threshold(MemorySourceTrust::MaintainerApproved));
assert!(!MemorySourceTrust::ForkInput.meets_threshold(MemorySourceTrust::ContributorInput));
}

#[test]
fn hosted_memory_persistence_requires_explicit_trusted_policy() {
let mut config = HostedReviewConfig {
enabled: true,
..Default::default()
};
assert!(!config.allows_auto_memory_persistence());

config.allow_auto_memory_persistence = true;
config.memory_source_trust = MemorySourceTrust::ContributorInput;
assert!(!config.allows_auto_memory_persistence());

config.memory_source_trust = MemorySourceTrust::MaintainerApproved;
assert!(config.allows_auto_memory_persistence());
}

#[test]
fn security_private_domain_requires_explicit_public_review_allowance() {
assert!(!MemoryDomain::SecurityPrivate.can_load_in_public_review(false));
Expand Down
24 changes: 17 additions & 7 deletions src-rust/crates/query/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ pub use compact::{
pub use cron_scheduler::start_cron_scheduler;
pub use goal_loop::{check_and_continue_goal, mark_goal_complete, GoalContinuation, StopReason};
pub use session_memory::{
ExtractedMemory, MemoryCategory, SessionMemoryExtractor, SessionMemoryState,
ExtractedMemory, MemoryCandidate, MemoryCandidateStatus, MemoryCandidateStore, MemoryCategory,
MemoryPersistenceOutcome, SessionMemoryExtractor, SessionMemoryState,
};
pub use skill_prefetch::{
format_skill_listing, prefetch_skills, SharedSkillIndex, SkillDefinition, SkillIndex,
Expand Down Expand Up @@ -1956,6 +1957,8 @@ pub async fn run_query_loop(
let model_clone = config.model.clone();
let messages_clone = messages.clone();
let working_dir_clone = tool_ctx.working_dir.clone();
let runtime_mode = tool_ctx.config.runtime_mode();
let hosted_review_config = tool_ctx.config.hosted_review.clone();

// Build a fresh client using the same API key. This avoids
// requiring an Arc in the existing run_query_loop signature.
Expand All @@ -1979,15 +1982,22 @@ pub async fn run_query_loop(
let target = working_dir_clone
.join(".coven-code")
.join("AGENTS.md");
if let Err(e) =
session_memory::SessionMemoryExtractor::persist(
&memories, &target,
)
.await
let candidate_store =
session_memory::MemoryCandidateStore::for_working_dir(
&working_dir_clone,
);
if let Err(e) = session_memory::SessionMemoryExtractor::persist_with_policy(
&memories,
&target,
&candidate_store,
runtime_mode,
&hosted_review_config,
)
.await
{
tracing::warn!(
error = %e,
"Failed to persist session memories"
"Failed to store session memories"
);
}
}
Expand Down
Loading