Skip to content
Closed
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
180 changes: 180 additions & 0 deletions crates/webcodex-tool-contracts/src/tests/policy_contracts.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,185 @@
use super::*;

#[test]
fn tool_definitions_are_composition_contract_ssot() {
let parallel = ToolCompositionContract {
eligibility: ToolCompositionPolicy::Allowed,
concurrency: ToolConcurrencyPolicy::Parallel,
};
assert_eq!(
ToolCompositionContract::CONSERVATIVE,
ToolCompositionContract {
eligibility: ToolCompositionPolicy::Denied,
concurrency: ToolConcurrencyPolicy::Sequential,
}
);
assert_eq!(
runtime_tool_composition_contract("git_review_summary"),
parallel
);

for definition in tool_definitions() {
let expected = if definition.name == "git_review_summary" {
parallel
} else {
ToolCompositionContract::CONSERVATIVE
};
assert_eq!(definition.composition, expected, "{}", definition.name);
assert_eq!(
runtime_tool_composition_contract(definition.name),
definition.composition,
"{} composition lookup must use ToolDefinition",
definition.name
);
}
}

#[test]
fn composition_defaults_cover_unreviewed_tools_and_unknown_gateways() {
for name in [
"read_files",
"search_project_texts",
"project_overview",
"git_diff_hunks",
"show_changes",
"write_project_file",
"apply_text_edits",
"apply_patch",
"git_commit_paths",
"start_session",
"work_on_project",
"finish_coding_task",
"close_session",
"post_session_message",
"resolve_session_message",
"run_shell",
"run_script",
"run_process",
"run_detached_process",
"session_shell_exec",
"run_job",
"observe_jobs",
"stop_job",
"cargo_check",
"cargo_test",
"cargo_fmt",
"computer_observe",
"computer_control",
"plugin_tool",
"ssh_resource",
] {
assert!(lookup_tool_definition(name).is_some(), "{name}");
assert_eq!(
runtime_tool_composition_contract(name),
ToolCompositionContract::CONSERVATIVE,
"{name}"
);
}
for name in [
"__unknown_composition_tool__",
"call_runtime_tool",
"tool_search",
] {
assert!(lookup_tool_definition(name).is_none(), "{name}");
assert_eq!(
runtime_tool_composition_contract(name),
ToolCompositionContract::CONSERVATIVE,
"{name}"
);
}
}

#[test]
fn composition_opt_in_requires_an_explicit_declaration() {
let definition = lookup_tool_definition("git_status").unwrap();
assert_eq!(definition.metadata.effect, ToolEffect::Observe);
assert_eq!(definition.metadata.risk, ToolRisk::Read);
assert_eq!(definition.metadata.idempotency, ToolIdempotency::PureRead);
assert_eq!(
definition.context_continuity_policy(),
ToolContextContinuityPolicy::REOBSERVABLE
);
assert_eq!(
runtime_tool_composition_contract(definition.name),
ToolCompositionContract::CONSERVATIVE
);
}

#[test]
fn parallel_composition_requires_allowed_eligibility() {
for definition in tool_definitions()
.filter(|definition| definition.composition.concurrency == ToolConcurrencyPolicy::Parallel)
{
assert_eq!(
definition.composition.eligibility,
ToolCompositionPolicy::Allowed,
"{}",
definition.name
);
}
}

#[test]
fn git_review_summary_composition_opt_in_matches_reviewed_facts() {
let definition = lookup_tool_definition("git_review_summary").unwrap();
assert!(is_adaptive_runtime_direct_tool(definition.name));
assert_eq!(definition.metadata.effect, ToolEffect::Observe);
assert_eq!(definition.metadata.idempotency, ToolIdempotency::PureRead);
assert_eq!(
definition.context_continuity_policy(),
ToolContextContinuityPolicy::REOBSERVABLE
);
assert_eq!(definition.execution, None);
assert_eq!(
definition.session_evidence.lifecycle,
ToolSessionLifecycleEffect::None
);
assert!(!definition.metadata.destructive);
assert!(!definition.metadata.shell_like);
}

#[test]
fn composition_policy_stays_out_of_tool_specs_and_annotations() {
let spec_keys = BTreeSet::from([
"name",
"description",
"inputSchema",
"outputSchema",
"annotations",
]);
let annotation_keys = BTreeSet::from([
"readOnlyHint",
"destructiveHint",
"idempotentHint",
"openWorldHint",
]);
for spec in registered_tool_specs() {
let serialized = serde_json::to_value(&spec).unwrap();
assert_eq!(
serialized
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect::<BTreeSet<_>>(),
spec_keys,
"{} ToolSpec fields",
spec.name
);
assert_eq!(
serialized["annotations"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect::<BTreeSet<_>>(),
annotation_keys,
"{} annotation fields",
spec.name
);
}
}

#[test]
fn tool_definitions_are_context_continuity_ssot() {
use crate::metadata::ToolEffect;
Expand Down
47 changes: 43 additions & 4 deletions crates/webcodex-tool-contracts/src/tool_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ pub use super::tool_policy::{
runtime_tool_activity_interaction, runtime_tool_activity_semantics,
runtime_tool_advances_context_checkpoint, runtime_tool_approval_policy,
runtime_tool_captures_validation_output, runtime_tool_category,
runtime_tool_effect_annotations, runtime_tool_execution_contract,
runtime_tool_is_change_summary_like, runtime_tool_is_git_like, runtime_tool_is_read_like,
runtime_tool_is_shell_like, runtime_tool_is_write_like, runtime_tool_metadata,
runtime_tool_operator_extension_family, runtime_tool_permission_risk,
runtime_tool_composition_contract, runtime_tool_effect_annotations,
runtime_tool_execution_contract, runtime_tool_is_change_summary_like, runtime_tool_is_git_like,
runtime_tool_is_read_like, runtime_tool_is_shell_like, runtime_tool_is_write_like,
runtime_tool_metadata, runtime_tool_operator_extension_family, runtime_tool_permission_risk,
runtime_tool_requires_permission, runtime_tool_runner_capability,
runtime_tool_session_evidence_policy, runtime_tool_session_risk_class,
};
Expand Down Expand Up @@ -828,6 +828,37 @@ impl ToolExecutionContract {
}
}

/// Explicit eligibility for future canonical tool composition. This declaration
/// does not grant authority or change direct-tool admission.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolCompositionPolicy {
Denied,
Allowed,
}

/// Reviewed concurrency within composition, covering every supported input and
/// provider path. Read-only metadata alone does not imply parallel safety.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolConcurrencyPolicy {
Sequential,
Parallel,
}

/// Static composition facts owned only by ToolDefinition. Phase 0 records these
/// facts without adding a runtime consumer or model-facing projection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ToolCompositionContract {
pub eligibility: ToolCompositionPolicy,
pub concurrency: ToolConcurrencyPolicy,
}

impl ToolCompositionContract {
pub const CONSERVATIVE: Self = Self {
eligibility: ToolCompositionPolicy::Denied,
concurrency: ToolConcurrencyPolicy::Sequential,
};
}

/// Static Stateless Operator protocol-extension classification. This declares only
/// which protocol capability family admits a hidden runtime tool; authorization,
/// permission, Project authority, and Runner capability remain independent.
Expand Down Expand Up @@ -934,6 +965,8 @@ pub struct ToolDefinition {
pub operator_extension_family: Option<ToolOperatorExtensionFamily>,
/// Optional canonical selection semantics for ordinary execution tools.
pub execution: Option<ToolExecutionContract>,
/// Explicit composition policy; unreviewed tools remain denied/sequential.
pub composition: ToolCompositionContract,
pub visibility: ToolVisibility,
pub category: &'static str,
pub metadata: ToolMetadata,
Expand All @@ -960,6 +993,11 @@ impl ToolDefinition {
self
}

pub const fn with_composition(mut self, composition: ToolCompositionContract) -> Self {
self.composition = composition;
self
}

/// Override only GPT Actions presentation text. This never changes the
/// canonical ToolSpec schema, semantic contract, authority, or MCP copy.
pub const fn with_gpt_action_description(mut self, description: &'static str) -> Self {
Expand Down Expand Up @@ -1136,6 +1174,7 @@ const fn def(
model_surface: ToolModelSurfaceDeclaration::DEFAULT,
operator_extension_family: None,
execution: None,
composition: ToolCompositionContract::CONSERVATIVE,
visibility,
category,
metadata: make_tool_metadata(
Expand Down
6 changes: 5 additions & 1 deletion crates/webcodex-tool-contracts/src/tool_definition/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ pub(super) const SUMMARY_DEFINITIONS: &[ToolDefinition] = &[
git_review_summary_input_schema,
)))),
120,
),
)
.with_composition(super::ToolCompositionContract {
eligibility: super::ToolCompositionPolicy::Allowed,
concurrency: super::ToolConcurrencyPolicy::Parallel,
}),
adaptive_runtime_direct(
context_reobservable(change_summary_like(git_like(model_spec(
def(
Expand Down
21 changes: 15 additions & 6 deletions crates/webcodex-tool-contracts/src/tool_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ use super::metadata::{
};
use super::tool_definition::{
tool_definitions, RunnerCapabilityRequirement, ToolActivityInteraction, ToolActivityKind,
ToolActivityPresentation, ToolActivitySemantics, ToolAuditPolicy, ToolContextContinuityPolicy,
ToolDefinition, ToolDiffReviewEvidence, ToolEffectAnnotations, ToolExecutionContract,
ToolExecutionForm, ToolExplorationEvidence, ToolGptActionExposure, ToolOperatorExtensionFamily,
ToolReviewEvidence, ToolSessionEvidencePolicy, ToolValidationIdentityKind,
PERMISSION_RISK_ARTIFACT_WRITE, PERMISSION_RISK_DESTRUCTIVE, PERMISSION_RISK_PATCH,
PERMISSION_RISK_SHELL, PERMISSION_RISK_VALIDATION, PERMISSION_RISK_WRITE, TOOL_CATEGORY_JOB,
ToolActivityPresentation, ToolActivitySemantics, ToolAuditPolicy, ToolCompositionContract,
ToolContextContinuityPolicy, ToolDefinition, ToolDiffReviewEvidence, ToolEffectAnnotations,
ToolExecutionContract, ToolExecutionForm, ToolExplorationEvidence, ToolGptActionExposure,
ToolOperatorExtensionFamily, ToolReviewEvidence, ToolSessionEvidencePolicy,
ToolValidationIdentityKind, PERMISSION_RISK_ARTIFACT_WRITE, PERMISSION_RISK_DESTRUCTIVE,
PERMISSION_RISK_PATCH, PERMISSION_RISK_SHELL, PERMISSION_RISK_VALIDATION,
PERMISSION_RISK_WRITE, TOOL_CATEGORY_JOB,
};

impl ToolDefinition {
Expand Down Expand Up @@ -231,6 +232,14 @@ pub fn runtime_tool_execution_contract(name: &str) -> Option<ToolExecutionContra
lookup_tool_definition(name).and_then(|definition| definition.execution)
}

/// Returns only the explicit canonical composition declaration. Unknown tools
/// remain denied/sequential; names, effects, and capabilities never imply opt-in.
pub fn runtime_tool_composition_contract(name: &str) -> ToolCompositionContract {
lookup_tool_definition(name)
.map(|definition| definition.composition)
.unwrap_or(ToolCompositionContract::CONSERVATIVE)
}

pub fn runtime_tool_session_evidence_policy(name: &str) -> ToolSessionEvidencePolicy {
lookup_tool_definition(name)
.map(|definition| definition.session_evidence_policy())
Expand Down
Loading
Loading