Skip to content
Closed
23 changes: 23 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 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_host_api", "crates/ironclaw_filesystem", "crates/ironclaw_attachments", "crates/ironclaw_extractors", "crates/ironclaw_memory", "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_sandbox_core", "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_hooks_postgres", "crates/ironclaw_hooks_libsql", "crates/ironclaw_hooks_parity", "crates/ironclaw_loop_support", "crates/ironclaw_reborn", "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_reborn_webui_ingress", "crates/ironclaw_reborn_openai_compat", "crates/ironclaw_reborn_openai_compat_storage", "crates/ironclaw_conversations", "crates/ironclaw_product_adapters", "crates/ironclaw_product_context", "crates/ironclaw_product_workflow", "crates/ironclaw_product_workflow_storage", "crates/ironclaw_product_adapter_registry", "crates/ironclaw_wasm_product_adapters", "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_oauth", "crates/ironclaw_llm", "crates/ironclaw_embeddings", "crates/ironclaw_engine", "crates/ironclaw_gateway", "crates/ironclaw_tui", "crates/ironclaw_webui_v2", "crates/ironclaw_webui_v2_static", "crates/ironclaw_skill_learning"]
members = [".", "crates/ironclaw_common", "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_sandbox_core", "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_hooks_postgres", "crates/ironclaw_hooks_libsql", "crates/ironclaw_hooks_parity", "crates/ironclaw_loop_support", "crates/ironclaw_reborn", "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_reborn_webui_ingress", "crates/ironclaw_reborn_openai_compat", "crates/ironclaw_reborn_openai_compat_storage", "crates/ironclaw_conversations", "crates/ironclaw_product_adapters", "crates/ironclaw_product_context", "crates/ironclaw_product_workflow", "crates/ironclaw_product_workflow_storage", "crates/ironclaw_product_adapter_registry", "crates/ironclaw_wasm_product_adapters", "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_oauth", "crates/ironclaw_llm", "crates/ironclaw_embeddings", "crates/ironclaw_engine", "crates/ironclaw_gateway", "crates/ironclaw_tui", "crates/ironclaw_webui_v2", "crates/ironclaw_webui_v2_static", "crates/ironclaw_skill_learning"]
exclude = [
"channels-src/discord",
"channels-src/feishu",
Expand Down Expand Up @@ -137,6 +137,7 @@ ironclaw_extractors = { path = "crates/ironclaw_extractors", version = "0.1.0" }
ironclaw_host_api = { path = "crates/ironclaw_host_api", version = "0.1.0" }
ironclaw_loop_support = { path = "crates/ironclaw_loop_support", version = "0.1.0" }
ironclaw_memory = { path = "crates/ironclaw_memory", version = "0.1.0" }
ironclaw_memory_native = { path = "crates/ironclaw_memory_native", version = "0.1.0" }
ironclaw_resources = { path = "crates/ironclaw_resources", version = "0.1.0" }
ironclaw_runtime_policy = { path = "crates/ironclaw_runtime_policy", version = "0.1.0" }
ironclaw_turns = { path = "crates/ironclaw_turns", version = "0.1.0" }
Expand Down
24 changes: 24 additions & 0 deletions crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,30 @@ fn boundary_rules() -> Vec<BoundaryRule> {
"ironclaw_wasm",
],
},
BoundaryRule {
// Native memory provider: depends on the agnostic contract crate plus
// ironclaw_filesystem/ironclaw_safety/ironclaw_prompt_envelope, but is
// still a provider behind host-resolved scope and must not reach up into
// host composition, dispatch, or higher-authority subsystems.
crate_name: "ironclaw_memory_native",
forbidden: vec![
"ironclaw_authorization",
"ironclaw_approvals",
"ironclaw_capabilities",
"ironclaw_dispatcher",
"ironclaw_events",
"ironclaw_extensions",
"ironclaw_host_runtime",
"ironclaw_secrets",
"ironclaw_network",
"ironclaw_mcp",
"ironclaw_processes",
"ironclaw_resources",
"ironclaw_run_state",
"ironclaw_scripts",
"ironclaw_wasm",
],
},
BoundaryRule {
crate_name: "ironclaw_resources",
forbidden: vec![
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ default_permission = "ask"
visibility = "model"
input_schema_ref = "schemas/shell.input.v1.json"
output_schema_ref = "schemas/shell.output.v1.json"
prompt_doc_ref = "prompts/test/builtin_shell.md"
"#;
let manifest = ExtensionManifest::parse(
manifest_toml,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ default_permission = "ask"
visibility = "model"
input_schema_ref = "schemas/shell.input.v1.json"
output_schema_ref = "schemas/shell.output.v1.json"
prompt_doc_ref = "prompts/test/acme_shell.md"
"#;
let manifest = ExtensionManifest::parse(
manifest_toml,
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_event_projections/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ thiserror = "2"
ironclaw_extensions = { path = "../ironclaw_extensions" }
ironclaw_filesystem = { path = "../ironclaw_filesystem" }
ironclaw_memory = { path = "../ironclaw_memory" }
ironclaw_memory_native = { path = "../ironclaw_memory_native" }
ironclaw_reborn_event_store = { path = "../ironclaw_reborn_event_store" }
serde_json = "1"
tempfile = "3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,5 @@ default_permission = "allow"
visibility = "model"
input_schema_ref = "schemas/echo/extension_raw_schema_sentinel_3022.input.v1.json"
output_schema_ref = "schemas/echo/extension_raw_schema_sentinel_3022.output.v1.json"
prompt_doc_ref = "prompts/test/echo-say.md"
"#;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use ironclaw_host_api::{
AgentId, CorrelationId, InvocationId, MissionId, ProjectId, ResourceScope, TenantId, ThreadId,
UserId,
};
use ironclaw_memory::{
use ironclaw_memory_native::{
InMemoryMemoryDocumentRepository, MemoryBackend, MemoryContext, MemoryDocumentPath,
MemoryDocumentRepository, MemoryDocumentScope, RepositoryMemoryBackend, content_sha256,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use ironclaw_host_api::{
AgentId, CorrelationId, InvocationId, MissionId, ProjectId, ResourceScope, TenantId, ThreadId,
UserId, VirtualPath,
};
use ironclaw_memory::{
use ironclaw_memory_native::{
ChunkingMemoryDocumentIndexer, FilesystemMemoryDocumentRepository,
InMemoryMemoryDocumentRepository, MemoryBackend, MemoryBackendCapabilities,
MemoryBackendFilesystemAdapter, MemoryContext, MemoryDocumentPath, MemoryDocumentScope,
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_extensions/src/hosted_mcp_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ default_permission = "ask"
visibility = "model"
input_schema_ref = "schemas/notion/fetch.input.json"
output_schema_ref = "schemas/notion/fetch.output.json"
prompt_doc_ref = "prompts/test/notion-fetch.md"
runtime_credentials = [
{ handle = "notion_access_token", source = { type = "product_auth_account", provider = "notion" }, audience = { scheme = "https", host_pattern = "mcp.notion.com" }, target = { type = "header", name = "authorization", prefix = "Bearer " }, required = true }
]
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_extensions/src/installations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,7 @@ default_permission = "ask"
visibility = "model"
input_schema_ref = "schemas/read.input.json"
output_schema_ref = "schemas/read.output.json"
prompt_doc_ref = "prompts/test/read.md"
"#
)
}
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_extensions/src/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ default_permission = "ask"
visibility = "model"
input_schema_ref = "schemas/read.input.json"
output_schema_ref = "schemas/read.output.json"
prompt_doc_ref = "prompts/test/read.md"
"#
);
let manifest = ExtensionManifest::parse(
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_extensions/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ default_permission = "ask"
visibility = "model"
input_schema_ref = "schemas/{name}.input.json"
output_schema_ref = "schemas/{name}.output.json"
prompt_doc_ref = "prompts/test/{name}.md"
"#
)
})
Expand Down
21 changes: 19 additions & 2 deletions crates/ironclaw_extensions/src/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
//! - extension IDs starting with `ironclaw.` are reserved for HostBundled;
//! - installed manifests must use `wasm` / `mcp` / `script` runtimes only;
//! - every capability declares `visibility`, relative
//! [`CapabilityProfileSchemaRef`] input/output schema refs, optional lazy
//! `prompt_doc_ref`, and the set of host ports it needs;
//! [`CapabilityProfileSchemaRef`] input/output schema refs, model-visible
//! capabilities declare `prompt_doc_ref`, and every capability declares the
//! set of host ports it needs;
//! - host port names validate against a host-defined [`HostPortCatalog`].
//!
//! This module does **not** dispatch capabilities, load WASM modules, evaluate
Expand Down Expand Up @@ -953,6 +954,22 @@ impl CapabilityDeclV2 {
})
})
.transpose()?;
if prompt_doc_ref.is_none() {
match raw.visibility {
// `prompt_doc_ref` is the model-facing prompt documentation for a
// capability, so it is required only for model-visible capabilities
// (issue #3537). API- and host-internal-visible capabilities are not
// surfaced to the model and therefore do not need one.
CapabilityVisibility::Model => {
return Err(ManifestV2Error::Invalid {
reason: format!(
"model-visible capability {id} must declare prompt_doc_ref"
),
});
}
CapabilityVisibility::Api | CapabilityVisibility::HostInternal => {}
}
}

let mut required_host_ports_seen = BTreeSet::new();
let mut required_host_ports = Vec::with_capacity(raw.required_host_ports.len());
Expand Down
63 changes: 61 additions & 2 deletions crates/ironclaw_extensions/tests/extension_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,20 +591,79 @@ fn capability_provider_host_api_reuses_capability_validation() {
}

#[test]
fn capability_provider_host_api_allows_missing_prompt_doc_ref() {
fn capability_provider_host_api_rejects_model_visible_missing_prompt_doc_ref() {
let manifest = CAPABILITY_PROVIDER_MANIFEST.replace(
"prompt_doc_ref = \"prompts/telegram/send_message.md\"\n",
"",
);

let err = ExtensionManifest::parse_with_host_api_contracts(
&manifest,
ManifestSource::InstalledLocal,
&HostPortCatalog::empty(),
&capability_provider_contracts(),
)
.unwrap_err();

assert!(
matches!(
err,
ExtensionError::ManifestV2(ManifestV2Error::HostApiSectionRejected { ref reason, .. })
if reason.contains("telegram.send_message")
&& reason.contains("model-visible")
&& reason.contains("prompt_doc_ref")
),
"{err:?}"
);
}

#[test]
fn capability_provider_host_api_allows_api_visible_missing_prompt_doc_ref() {
// prompt_doc_ref is model-facing documentation, so api-visible capabilities
// may omit it (issue #3537).
let manifest = CAPABILITY_PROVIDER_MANIFEST
.replace("visibility = \"model\"", "visibility = \"api\"")
.replace(
"prompt_doc_ref = \"prompts/telegram/send_message.md\"\n",
"",
);

let manifest = ExtensionManifest::parse_with_host_api_contracts(
&manifest,
ManifestSource::InstalledLocal,
&HostPortCatalog::empty(),
&capability_provider_contracts(),
)
.expect("prompt_doc_ref is optional lazy help metadata");
.expect("api-visible capabilities may omit prompt_doc_ref");

assert_eq!(
manifest.capabilities[0].visibility,
CapabilityVisibility::Api
);
assert!(manifest.capabilities[0].prompt_doc_ref.is_none());
}

#[test]
fn capability_provider_host_api_allows_host_internal_missing_prompt_doc_ref() {
let manifest = CAPABILITY_PROVIDER_MANIFEST
.replace("visibility = \"model\"", "visibility = \"host_internal\"")
.replace(
"prompt_doc_ref = \"prompts/telegram/send_message.md\"\n",
"",
);

let manifest = ExtensionManifest::parse_with_host_api_contracts(
&manifest,
ManifestSource::InstalledLocal,
&HostPortCatalog::empty(),
&capability_provider_contracts(),
)
.expect("host-internal capabilities may omit prompt_doc_ref");

assert_eq!(
manifest.capabilities[0].visibility,
CapabilityVisibility::HostInternal
);
assert!(manifest.capabilities[0].prompt_doc_ref.is_none());
}

Expand Down
Loading
Loading