Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f4f817c
test(architecture): enforce reborn dependency boundaries as a workspa…
BenKurrek Jun 24, 2026
3a2c4ad
feat(memory): make the host the sole constructor of admitted memory c…
BenKurrek Jun 24, 2026
acc1b88
refactor(memory): route user-profile reads through the MemoryService …
BenKurrek Jun 25, 2026
f416967
docs(memory): document the two context-path deltas from the regressio…
BenKurrek Jun 25, 2026
e7b2eb0
feat(memory): host memory profile catalog, host ports, native v2 mani…
BenKurrek Jun 25, 2026
d9b90a3
feat(memory): add [memory] profile-binding config section (#3537)
BenKurrek Jun 25, 2026
6485f57
feat(memory): wire memory binding through composition + CLI startup (…
BenKurrek Jun 25, 2026
40179f5
docs(memory): add manifest-v2 + host-storage-port ADRs; update memory…
BenKurrek Jun 25, 2026
c8aceef
fix(memory): address review on the #3537 memory binding (scope guard,…
BenKurrek Jun 25, 2026
6e34b16
refactor(memory): single MemoryServiceResolver for every memory consu…
BenKurrek Jun 25, 2026
036540a
refactor(memory): collapse memory to a single always-on ironclaw.memo…
BenKurrek Jun 25, 2026
84086bf
refactor(memory): load native memory from the bundled v2 TOML manifes…
BenKurrek Jun 25, 2026
b4f1bfe
docs(memory): document the live always-on native v2 memory surface
BenKurrek Jun 25, 2026
566323d
fix(memory): keep display-preview summaries working for native memory…
BenKurrek Jun 25, 2026
68ccd94
fix(memory): complete native-package test + schema updates for host_r…
BenKurrek Jun 25, 2026
bd3f533
fix(memory): green the Reborn e2e harness + coverage for the native p…
BenKurrek Jun 25, 2026
958a8b7
feat(memory): config-driven mem0 document-store provider (#3537)
BenKurrek Jun 25, 2026
3d48e87
test(memory): host drops provider-supplied cross-scope context snippets
BenKurrek Jun 25, 2026
2038048
feat(memory): make the mem0 provider fully local (self-hosted OSS)
BenKurrek Jun 26, 2026
2d21441
fix(memory): keep mem0 off by default (no default base URL)
BenKurrek Jun 26, 2026
3dd8c83
fix(memory): apply consolidated review findings (docs, secrets, fail-…
BenKurrek Jun 26, 2026
ad84d34
docs(memory): document that prompt-write-safety is native-only (#5264)
BenKurrek Jun 26, 2026
b1da109
refactor(memory): feature-gate the mem0 provider behind `memory-mem0`…
BenKurrek Jun 26, 2026
3ea70cf
fix(memory): address CodeRabbit re-review findings (mem0 correctness,…
BenKurrek Jun 26, 2026
bb40e02
fix(memory): address CodeRabbit re-review round 2 (target containment…
BenKurrek Jun 26, 2026
4c96023
refactor(memory): forward the memory-mem0 feature from the reborn CLI
BenKurrek Jun 26, 2026
e58aa9e
Merge remote-tracking branch 'origin/main' into reborn/memory-lift-fo…
BenKurrek Jun 26, 2026
f8377b9
chore(deps): ignore RUSTSEC-2026-0187 (lopdf PDF stack-overflow, boun…
BenKurrek Jun 26, 2026
502f676
feat(memory): workspace-bound local-dev mem0 isolation (fold workspac…
BenKurrek Jun 26, 2026
6934436
fix(memory): make agent-facing memory surface backend-agnostic (ironc…
BenKurrek Jun 26, 2026
a0310d6
Merge origin/main into reborn/memory-lift-followups
BenKurrek Jul 6, 2026
13825be
fix(memory): address userland extension review feedback
BenKurrek Jul 6, 2026
0f0a350
Fix native memory CI harness wiring
BenKurrek Jul 6, 2026
0591e47
Merge origin/main into reborn/memory-lift-followups
BenKurrek Jul 20, 2026
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
18 changes: 18 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = [".", "crates/ironclaw_common", "crates/ironclaw_observability", "crates/ironclaw_host_api", "crates/ironclaw_filesystem", "crates/ironclaw_attachments", "crates/ironclaw_extractors", "crates/ironclaw_memory", "crates/ironclaw_memory_native", "crates/ironclaw_events", "crates/ironclaw_event_projections", "crates/ironclaw_event_streams", "crates/ironclaw_reborn_event_store", "crates/ironclaw_extensions", "crates/ironclaw_processes", "crates/ironclaw_dispatcher", "crates/ironclaw_scripts", "crates/ironclaw_process_sandbox", "crates/ironclaw_mcp", "crates/ironclaw_wasm", "crates/ironclaw_wasm_limiter", "crates/ironclaw_capabilities", "crates/ironclaw_secrets", "crates/ironclaw_network", "crates/ironclaw_host_runtime", "crates/ironclaw_runtime_policy", "crates/ironclaw_authorization", "crates/ironclaw_run_state", "crates/ironclaw_approvals", "crates/ironclaw_resources", "crates/ironclaw_auth", "crates/ironclaw_trust", "crates/ironclaw_turns", "crates/ironclaw_agent_loop", "crates/ironclaw_threads", "crates/ironclaw_prompt_envelope", "crates/ironclaw_hooks", "crates/ironclaw_loop_host", "crates/ironclaw_runner", "crates/ironclaw_reborn_config", "crates/ironclaw_reborn_composition", "crates/ironclaw_reborn_identity", "crates/ironclaw_first_party_extensions", "crates/ironclaw_reborn_cli", "crates/ironclaw_reborn_traces", "crates/ironclaw_webui", "crates/ironclaw_reborn_openai_compat", "crates/ironclaw_conversations", "crates/ironclaw_product_adapters", "crates/ironclaw_product_context", "crates/ironclaw_product_workflow", "crates/ironclaw_product_adapter_registry", "crates/ironclaw_wasm_product_adapters", "crates/ironclaw_channel_host", "crates/ironclaw_channel_delivery", "crates/ironclaw_telegram_extension", "crates/ironclaw_telegram_v2_adapter", "crates/ironclaw_slack_v2_adapter", "crates/ironclaw_outbound", "crates/ironclaw_triggers", "crates/ironclaw_projects", "crates/ironclaw_architecture", "crates/ironclaw_safety", "crates/ironclaw_skills", "crates/ironclaw_llm", "crates/ironclaw_embeddings", "crates/ironclaw_gateway", "crates/ironclaw_tui", "crates/ironclaw_reborn_migration", "tools/ironclaw_stress"]
members = [".", "crates/ironclaw_common", "crates/ironclaw_observability", "crates/ironclaw_host_api", "crates/ironclaw_filesystem", "crates/ironclaw_attachments", "crates/ironclaw_extractors", "crates/ironclaw_memory", "crates/ironclaw_memory_native", "crates/ironclaw_memory_mem0", "crates/ironclaw_events", "crates/ironclaw_event_projections", "crates/ironclaw_event_streams", "crates/ironclaw_reborn_event_store", "crates/ironclaw_extensions", "crates/ironclaw_processes", "crates/ironclaw_dispatcher", "crates/ironclaw_scripts", "crates/ironclaw_process_sandbox", "crates/ironclaw_mcp", "crates/ironclaw_wasm", "crates/ironclaw_wasm_limiter", "crates/ironclaw_capabilities", "crates/ironclaw_secrets", "crates/ironclaw_network", "crates/ironclaw_host_runtime", "crates/ironclaw_runtime_policy", "crates/ironclaw_authorization", "crates/ironclaw_run_state", "crates/ironclaw_approvals", "crates/ironclaw_resources", "crates/ironclaw_auth", "crates/ironclaw_trust", "crates/ironclaw_turns", "crates/ironclaw_agent_loop", "crates/ironclaw_threads", "crates/ironclaw_prompt_envelope", "crates/ironclaw_hooks", "crates/ironclaw_loop_host", "crates/ironclaw_runner", "crates/ironclaw_reborn_config", "crates/ironclaw_reborn_composition", "crates/ironclaw_reborn_identity", "crates/ironclaw_first_party_extensions", "crates/ironclaw_reborn_cli", "crates/ironclaw_reborn_traces", "crates/ironclaw_webui", "crates/ironclaw_reborn_openai_compat", "crates/ironclaw_conversations", "crates/ironclaw_product_adapters", "crates/ironclaw_product_context", "crates/ironclaw_product_workflow", "crates/ironclaw_product_adapter_registry", "crates/ironclaw_wasm_product_adapters", "crates/ironclaw_channel_host", "crates/ironclaw_channel_delivery", "crates/ironclaw_telegram_extension", "crates/ironclaw_telegram_v2_adapter", "crates/ironclaw_slack_v2_adapter", "crates/ironclaw_outbound", "crates/ironclaw_triggers", "crates/ironclaw_projects", "crates/ironclaw_architecture", "crates/ironclaw_safety", "crates/ironclaw_skills", "crates/ironclaw_llm", "crates/ironclaw_embeddings", "crates/ironclaw_gateway", "crates/ironclaw_tui", "crates/ironclaw_reborn_migration", "tools/ironclaw_stress"]
exclude = [
"channels-src/discord",
"channels-src/feishu",
Expand Down
43 changes: 43 additions & 0 deletions crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,49 @@ fn reborn_crate_dependency_boundaries_hold() {
}
}

#[test]
fn host_runtime_stays_memory_provider_neutral_and_only_composition_names_mem0() {
// The `ironclaw_memory_mem0` boundary rule's comment states that
// `ironclaw_host_runtime` must stay provider-agnostic and must NOT name the
// concrete mem0 provider crate; only the composition layer may depend on it
// (it is the one layer allowed to name concrete provider crates, building the
// `Arc<dyn MemoryService>` that the provider-neutral `MemoryServiceResolver`
// stores). That comment was previously unenforced. This test fails loudly if a
// future edit adds `ironclaw_memory_mem0` to `host_runtime/Cargo.toml` — or to
// any crate other than composition.
const MEM0: &str = "ironclaw_memory_mem0";

let metadata = cargo_metadata();
let packages = metadata["packages"]
.as_array()
.expect("cargo metadata must include packages");
let dependencies = packages
.iter()
.filter_map(package_dependencies)
.collect::<HashMap<_, _>>();

// host_runtime must not name the concrete mem0 provider.
assert_no_normal_workspace_deps(&dependencies, "ironclaw_host_runtime", [MEM0]);

// Only the composition layer (the mem0 crate itself aside) may take a normal
// dependency on the concrete mem0 provider.
let mut dependents = dependencies
.iter()
.filter(|(crate_name, deps)| {
crate_name.as_str() != MEM0 && deps.iter().any(|dependency| dependency == MEM0)
})
.map(|(crate_name, _)| crate_name.as_str())
.collect::<Vec<_>>();
dependents.sort_unstable();
assert_eq!(
dependents,
vec!["ironclaw_reborn_composition"],
"only ironclaw_reborn_composition may take a normal dependency on {MEM0}; \
ironclaw_host_runtime and every other crate must resolve memory through the \
provider-neutral MemoryServiceResolver"
);
}

#[test]
fn conversation_trusted_trigger_submitter_stays_conversation_or_composition_owned() {
let root = workspace_root();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ const ALLOWLIST: &[(&str, &str)] = &[
deployments this local-runtime helper does not assemble. Retires into \
`deployment.rs` when the build input carries a config.",
),
(
"memory_binding.rs",
"Maps the composition profile to a typed `MemoryDeploymentProfile` for \
the fail-closed memory profile-binding certification policy (#3537): \
production rejects unverified third-party bindings absent an admin \
override; local-dev permits them. The branch produces a typed \
memory-deployment axis, not a raw label. Retires into `DeploymentConfig` \
when it grows a memory-binding axis (#5264).",
),
(
"readiness.rs",
"Readiness diagnostics carry a profile as an operator-facing **label** \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use ironclaw_host_api::{
fn capability_profile_conformance_reports_missing_required_operations() {
let contract = context_retrieval_contract();
let claim = CapabilityProfileClaim::new(
CapabilityId::new("ironclaw.memory.native.context.retrieve").unwrap(),
CapabilityId::new("ironclaw.memory.context.retrieve").unwrap(),
CapabilityProfileId::new("memory.context_retrieval.v1").unwrap(),
Vec::new(),
)
Expand All @@ -34,7 +34,7 @@ fn capability_profile_conformance_reports_missing_required_operations() {
fn capability_profile_conformance_reports_schema_mismatches_and_extra_operations() {
let contract = context_retrieval_contract();
let claim = CapabilityProfileClaim::new(
CapabilityId::new("ironclaw.memory.native.context.retrieve").unwrap(),
CapabilityId::new("ironclaw.memory.context.retrieve").unwrap(),
CapabilityProfileId::new("memory.context_retrieval.v1").unwrap(),
vec![
CapabilityProfileClaimedOperation::new(
Expand Down Expand Up @@ -75,7 +75,7 @@ fn capability_profile_conformance_reports_schema_mismatches_and_extra_operations
fn capability_profile_conformance_accepts_matching_claims() {
let contract = context_retrieval_contract();
let claim = CapabilityProfileClaim::new(
CapabilityId::new("ironclaw.memory.native.context.retrieve").unwrap(),
CapabilityId::new("ironclaw.memory.context.retrieve").unwrap(),
CapabilityProfileId::new("memory.context_retrieval.v1").unwrap(),
vec![
CapabilityProfileClaimedOperation::new(
Expand Down Expand Up @@ -114,7 +114,7 @@ fn capability_profile_conformance_reports_profile_id_mismatch_with_profile_subje
let contract = context_retrieval_contract();
let mismatched_profile = CapabilityProfileId::new("memory.document_store.v1").unwrap();
let claim = CapabilityProfileClaim::new(
CapabilityId::new("ironclaw.memory.native.context.retrieve").unwrap(),
CapabilityId::new("ironclaw.memory.context.retrieve").unwrap(),
mismatched_profile.clone(),
vec![
CapabilityProfileClaimedOperation::new(
Expand Down
4 changes: 2 additions & 2 deletions crates/ironclaw_extensions/tests/manifest_v2_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ fn host_bundled_source_may_assert_first_party_and_reserved_id() {
let toml = format!(
r#"
schema_version = "{schema}"
id = "ironclaw.memory.native"
id = "ironclaw.memory"
name = "Reborn Native Memory"
version = "0.1.0"
description = "host-bundled"
Expand All @@ -542,7 +542,7 @@ kind = "first_party"
service = "native_memory_provider"

[[capabilities]]
id = "ironclaw.memory.native.context.retrieve"
id = "ironclaw.memory.context.retrieve"
implements = ["memory.context_retrieval.v1"]
description = "Retrieve bounded provider-neutral memory context."
default_permission = "allow"
Expand Down
14 changes: 14 additions & 0 deletions crates/ironclaw_host_api/src/host_port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ use crate::{
/// outbound HTTP through host policy, credential, and response-limit services.
pub const HOST_RUNTIME_HTTP_EGRESS_PORT_ID: &str = "host.runtime.http_egress";

/// First-party SQL/transaction storage port. HostBundled first-party only:
/// reserved for first-party durable storage that runs through a host-scoped
/// transaction surface rather than a raw pool handle. This is part of the future
/// storage-port vocabulary, not a live backing today — the native memory provider
/// is filesystem-backed and declares no host ports (see
/// `native_memory_declares_no_host_ports`). Concrete adapters live in host/runtime
/// service crates; this is the validation contract name only.
pub const HOST_STORAGE_SQL_TRANSACTION_FIRST_PARTY_PORT_ID: &str =
"host.storage.sql_transaction.first_party";

/// Host-mediated durable audit event port. Capabilities declare it when they
/// emit redacted significant events into the host's durable audit log.
pub const HOST_EVENTS_AUDIT_PORT_ID: &str = "host.events.audit";

fn validate_dotted_host_port_id(value: &str) -> Result<(), HostApiError> {
validate_dotted_id(
"host_port",
Expand Down
68 changes: 68 additions & 0 deletions crates/ironclaw_host_runtime/assets/memory_native/manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
schema_version = "reborn.extension_manifest.v2"
id = "ironclaw.memory"
name = "Reborn Memory"
version = "0.1.0"
description = "Host-bundled memory extension (issue #3537). The read and write tools below implement the host-defined memory.document_store.v1 profile; search and tree are provider-specific model-facing tools that implement no portable profile. Registered on the always-on first-party lane (like the builtin toolset), so the tools are unconditionally available. The backing provider is resolved by the document-store profile binding — the native filesystem provider by default, swappable without changing these tools — not by install/enable lifecycle."
# Requested trust only; the host trust policy computes effective trust. A
# `first_party` runtime + reserved `ironclaw.*` id are accepted only for the
# loader-supplied `HostBundled` source, and only when a matching host service
# (`native_memory_provider`) is registered — the bundled TOML alone is not
# authority.
trust = "first_party_requested"

[runtime]
kind = "first_party"
service = "native_memory_provider"

# memory.document_store.v1 — read operation, exposed as a model-facing tool. The
# schema refs match the document_store profile's read-operation refs so the
# conformance harness binds this capability to that operation.
[[capabilities]]
Comment thread
BenKurrek marked this conversation as resolved.
id = "ironclaw.memory.read"
implements = ["memory.document_store.v1"]
description = "Read a Reborn persistent memory document in the current tenant/user/agent/project scope."
effects = ["read_filesystem"]
default_permission = "allow"
visibility = "model"
input_schema_ref = "schemas/memory/document-read.input.v1.json"
output_schema_ref = "schemas/memory/document-read.output.v1.json"
prompt_doc_ref = "prompts/memory-native/read.md"

# memory.document_store.v1 — write operation, exposed as a model-facing tool.
# Host prompt-write safety runs before dispatching writes to prompt-injected
# memory files.
[[capabilities]]
id = "ironclaw.memory.write"
implements = ["memory.document_store.v1"]
description = "Write, append, or patch Reborn persistent memory documents in the current tenant/user/agent/project scope. For structured user facts (timezone, locale, location), use builtin.profile_set instead."
effects = ["read_filesystem", "write_filesystem"]
default_permission = "allow"
visibility = "model"
input_schema_ref = "schemas/memory/document-write.input.v1.json"
output_schema_ref = "schemas/memory/document-write.output.v1.json"
prompt_doc_ref = "prompts/memory-native/write.md"

# Native search convenience. Implements no portable profile operation — it is a
# provider-specific model tool layered on the same filesystem store.
[[capabilities]]
id = "ironclaw.memory.search"
implements = []
description = "Search only Reborn internal persistent memory documents in the current tenant/user/agent/project scope; this does not search connected app or extension data."
effects = ["read_filesystem"]
default_permission = "allow"
visibility = "model"
input_schema_ref = "schemas/memory/search.input.v1.json"
output_schema_ref = "schemas/memory/search.output.v1.json"
prompt_doc_ref = "prompts/memory-native/search.md"

# Native tree/list convenience. Implements no portable profile operation.
[[capabilities]]
id = "ironclaw.memory.tree"
implements = []
description = "List Reborn persistent memory documents as a compact tree."
effects = ["read_filesystem"]
default_permission = "allow"
visibility = "model"
input_schema_ref = "schemas/memory/tree.input.v1.json"
output_schema_ref = "schemas/memory/tree.output.v1.json"
prompt_doc_ref = "prompts/memory-native/tree.md"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Read a persistent memory document by relative path, scoped to the current
tenant/user/agent/project. Returns the document content and its word count.
Use it to recall previously written notes, logs, or profile facts before acting.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Search persistent memory documents with a natural-language `query`, scoped to
the current tenant/user/agent/project. Returns the best-matching snippets with
their source path and a relevance score. Use it to find relevant notes when you
do not know the exact document path.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
List persistent memory documents as a compact tree, scoped to the current
tenant/user/agent/project. Optionally pass a `path` to list a subdirectory and a
`depth` to bound traversal. Use it to discover what memory documents exist before
reading or writing.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Write, append, or patch a persistent memory document, scoped to the current
tenant/user/agent/project. Choose a `target` (e.g. `memory`, `daily_log`,
`heartbeat`, or a relative path); set `append` to add rather than replace; or
supply `old_string`/`new_string` to patch in place. For structured user facts
Comment thread
coderabbitai[bot] marked this conversation as resolved.
(timezone, locale, location) prefer builtin.profile_set instead.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "object",
"properties": {
"path": {
"type": "string",
"minLength": 1,
"pattern": "^[^/]",
"description": "Relative memory document path to read; must not be absolute (no leading '/'). The host filesystem additionally rejects '..' traversal and resolves the path within the scoped /memory mount."
}
},
"required": ["path"],
"additionalProperties": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"type": "object",
"properties": {
"path": { "type": "string", "description": "Relative memory document path that was read" },
"content": { "type": "string", "description": "Full document content" },
"word_count": { "type": "integer", "description": "Word count of the document content" }
},
"required": ["path", "content", "word_count"],
"additionalProperties": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Full content to write or append"
},
"target": {
"type": "string",
"description": "Where to write: 'memory' for MEMORY.md, 'daily_log' for today's log, 'heartbeat' for HEARTBEAT.md checklist, 'bootstrap' to clear BOOTSTRAP.md (content is ignored; the file is always cleared), or a relative memory document path. A reserved name or non-blank relative path is allowed; it must not be blank, absolute (no leading '/'), contain '..' traversal, or use backslash separators. The configured document-store provider owns final storage resolution.",
"default": "daily_log",
"not": { "pattern": "(^\\s*$)|(^/)|(\\.\\.)|(\\\\)" }
},
Comment thread
BenKurrek marked this conversation as resolved.
"append": {
"type": "boolean",
"description": "Append to existing content when true; replace when false",
"default": true
},
"metadata": {
"type": "object",
"description": "Optional document metadata such as skip_indexing or skip_versioning"
},
"old_string": {
"type": "string",
"description": "Exact text to replace; switches to patch mode"
},
"new_string": {
"type": "string",
"description": "Replacement text for patch mode"
},
"replace_all": {
"type": "boolean",
"description": "Replace every old_string occurrence in patch mode",
"default": false
},
"timezone": {
"type": "string",
"description": "IANA timezone used only for daily_log target date resolution"
}
},
"additionalProperties": false
Comment thread
BenKurrek marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["cleared", "patched", "written"],
"description": "Write outcome"
},
"path": { "type": "string", "description": "Relative memory document path that was written" },
"message": { "type": "string", "description": "Human-readable note (cleared status)" },
"replacements": { "type": "integer", "description": "Number of replacements made (patched status)" },
"content_length": { "type": "integer", "description": "Resulting content length in bytes" },
"append": { "type": "boolean", "description": "Whether the write appended (written status)" }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"required": ["status", "path"],
"additionalProperties": false
Comment thread
BenKurrek marked this conversation as resolved.
}
Loading
Loading