Skip to content
Open
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
192 changes: 186 additions & 6 deletions cli/golem-cli/src/app/build/gen_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ use crate::bridge_gen::scala::tool::ScalaToolBridgeGenerator;
use crate::bridge_gen::scala::{ScalaBridgeGenerator, ScalaBridgeMode};
use crate::bridge_gen::typescript::tool::TypeScriptToolBridgeGenerator;
use crate::bridge_gen::typescript::{TypeScriptBridgeGenerator, TypeScriptBridgeMode};
use crate::bridge_gen::{BridgeGenerator, BridgeMode, bridge_client_directory_name};
use crate::bridge_gen::{
BridgeGenerator, BridgeMode, bridge_client_directory_name,
validate_host_managed_agent_bridge_policy,
};
use crate::command::GolemCliCommand;
use crate::error::NonSuccessfulExit;
use crate::fs;
Expand Down Expand Up @@ -80,16 +83,18 @@ async fn gen_bridge_with_manifest_mode_filter_and_additional_collision_targets(
) -> anyhow::Result<()> {
let plan = plan_bridge_generation(ctx, manifest_bridge_mode_filter).await?;

let mut collision_targets = additional_collision_targets.to_vec();
collision_targets.extend(plan.targets.iter().cloned());
validate_supported_bridge_targets(&collision_targets)?;
validate_host_managed_bridge_targets(&collision_targets)?;

if plan.targets.is_empty() {
if !additional_collision_targets.is_empty() {
validate_no_output_dir_collisions(additional_collision_targets)?;
}
return Ok(());
}

let mut collision_targets = additional_collision_targets.to_vec();
collision_targets.extend(plan.targets.iter().cloned());
validate_supported_bridge_targets(&collision_targets)?;
validate_no_output_dir_collisions(&collision_targets)?;

write_repl_metadata(ctx, &plan).await?;
Expand Down Expand Up @@ -341,13 +346,28 @@ pub(crate) async fn gen_bridge_sdk_targets(
ctx: &BuildContext<'_>,
targets: Vec<BridgeSdkTarget>,
) -> anyhow::Result<()> {
validate_host_managed_bridge_targets(&targets)?;

for target in targets {
gen_bridge_sdk_target(ctx, target).await?;
}

Ok(())
}

pub(crate) fn validate_host_managed_bridge_targets(
targets: &[BridgeSdkTarget],
) -> anyhow::Result<()> {
for target in targets {
let BridgeSdkTargetSubject::Agent(agent) = &target.subject else {
continue;
};
validate_host_managed_agent_bridge_policy(agent, target.bridge_mode)?;
}

Ok(())
}

async fn collect_manifest_targets(
ctx: &BuildContext<'_>,
bridge_mode_filter: Option<BridgeMode>,
Expand Down Expand Up @@ -1215,10 +1235,14 @@ mod tests {
use crate::model::app::{Application, ApplicationPreload, ComponentPresetSelector};
use crate::model::app_raw;
use golem_common::model::Empty;
use golem_common::model::agent::{AgentMode, AgentTypeName, Snapshotting};
use golem_common::model::agent::{AgentConfigSource, AgentMode, AgentTypeName, Snapshotting};
use golem_common::model::component::ComponentName;
use golem_common::schema::agent::AgentConfigDeclarationSchema;
use golem_common::schema::tool::{CommandNode, CommandTree, Doc, Globals, Tool};
use golem_common::schema::{AgentConstructorSchema, AgentTypeSchema, InputSchema, SchemaGraph};
use golem_common::schema::{
AgentConstructorSchema, AgentMethodSchema, AgentTypeSchema, AutoInjectedKind, InputSchema,
NamedField, OutputSchema, SchemaGraph, SchemaType,
};
use indoc::indoc;
use strum::IntoEnumIterator;
use tempfile::{TempDir, tempdir};
Expand Down Expand Up @@ -1370,6 +1394,162 @@ mod tests {
);
}

#[test]
fn external_bridge_rejects_host_managed_method_types_before_touching_output() {
let temp_dir = tempdir().unwrap();
let output_dir = temp_dir.path().join("bridge/agent-client");
std::fs::create_dir_all(&output_dir).unwrap();
let sentinel = output_dir.join("sentinel");
std::fs::write(&sentinel, "keep").unwrap();

let mut target = bridge_sdk_target_with_mode(
"Agent",
GuestLanguage::Rust,
BridgeMode::External,
output_dir,
);
let agent = match &mut target.subject {
BridgeSdkTargetSubject::Agent(agent) => agent,
BridgeSdkTargetSubject::Tool(_) => unreachable!(),
};
agent.methods.push(AgentMethodSchema {
name: "forward".to_string(),
description: String::new(),
prompt_hint: None,
input_schema: InputSchema::parameters([NamedField::user_supplied(
"credentials",
SchemaType::list(SchemaType::secret(Default::default())),
)]),
output_schema: OutputSchema::Unit,
http_endpoint: vec![],
read_only: None,
});

let error = validate_host_managed_bridge_targets(&[target])
.unwrap_err()
.to_string();
assert!(error.contains("method `forward` input parameter `credentials`"));
assert!(error.contains("host-managed capability `secret`"));
assert!(sentinel.exists(), "preflight must not modify bridge output");
}

#[test]
fn guest_bridge_allows_host_managed_method_inputs_and_outputs() {
let mut target = bridge_sdk_target_with_mode(
"Agent",
GuestLanguage::Rust,
BridgeMode::Guest,
tempdir().unwrap().path().join("bridge/agent-client"),
);
let agent = match &mut target.subject {
BridgeSdkTargetSubject::Agent(agent) => agent,
BridgeSdkTargetSubject::Tool(_) => unreachable!(),
};
agent.methods.push(AgentMethodSchema {
name: "forward".to_string(),
description: String::new(),
prompt_hint: None,
input_schema: InputSchema::parameters([NamedField::user_supplied(
"credentials",
SchemaType::secret(Default::default()),
)]),
output_schema: OutputSchema::Single(Box::new(SchemaType::permission_card(
Default::default(),
))),
http_endpoint: vec![],
read_only: None,
});

validate_host_managed_bridge_targets(&[target]).unwrap();
}

#[test]
fn guest_bridge_rejects_host_managed_constructor_and_configuration_types() {
let mut constructor_target = bridge_sdk_target_with_mode(
"Agent",
GuestLanguage::Rust,
BridgeMode::Guest,
tempdir().unwrap().path().join("bridge/constructor-client"),
);
let constructor_agent = match &mut constructor_target.subject {
BridgeSdkTargetSubject::Agent(agent) => agent,
BridgeSdkTargetSubject::Tool(_) => unreachable!(),
};
constructor_agent.constructor.input_schema = InputSchema::parameters([
NamedField::user_supplied(
"authorization",
SchemaType::permission_card(Default::default()),
),
NamedField::auto_injected(
"host-secret",
AutoInjectedKind::Principal,
SchemaType::secret(Default::default()),
),
]);

let error = validate_host_managed_bridge_targets(&[constructor_target])
.unwrap_err()
.to_string();
assert!(error.contains("constructor parameter `authorization`"));
assert!(error.contains("host-managed capability `permission-card`"));

let mut config_target = bridge_sdk_target_with_mode(
"Agent",
GuestLanguage::Rust,
BridgeMode::Guest,
tempdir().unwrap().path().join("bridge/config-client"),
);
let config_agent = match &mut config_target.subject {
BridgeSdkTargetSubject::Agent(agent) => agent,
BridgeSdkTargetSubject::Tool(_) => unreachable!(),
};
config_agent.config.push(AgentConfigDeclarationSchema {
source: AgentConfigSource::Local,
path: vec!["limits".to_string()],
value_type: SchemaType::quota_token(Default::default()),
});

let error = validate_host_managed_bridge_targets(&[config_target])
.unwrap_err()
.to_string();
assert!(error.contains("configuration `limits`"));
assert!(error.contains("host-managed capability `quota-token`"));
}

#[test]
fn bridge_preflight_allows_host_supplied_capabilities() {
let mut target = bridge_sdk_target_with_mode(
"Agent",
GuestLanguage::Rust,
BridgeMode::External,
tempdir().unwrap().path().join("bridge/agent-client"),
);
let agent = match &mut target.subject {
BridgeSdkTargetSubject::Agent(agent) => agent,
BridgeSdkTargetSubject::Tool(_) => unreachable!(),
};
agent.config.push(AgentConfigDeclarationSchema {
source: AgentConfigSource::Secret,
path: vec!["credentials".to_string()],
value_type: SchemaType::secret(Default::default()),
});
agent.methods.push(AgentMethodSchema {
name: "inspect".to_string(),
description: String::new(),
prompt_hint: None,
input_schema: InputSchema::parameters([NamedField::auto_injected(
"authority",
AutoInjectedKind::Principal,
SchemaType::permission_card(Default::default()),
)]),
output_schema: OutputSchema::Unit,
http_endpoint: vec![],
read_only: None,
});

validate_host_managed_bridge_targets(&[target]).unwrap();
}

#[test]
fn dependency_guest_bridge_support_accepts_all_current_languages_for_agents_and_tools() {
let component_name = ComponentName("component".to_string());
Expand Down
5 changes: 3 additions & 2 deletions cli/golem-cli/src/app/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ use crate::app::build::gen_bridge::{
plan_dependency_guest_bridge_generation_for_components_lenient,
plan_explicit_manifest_guest_bridge_generation_for_components_lenient,
plan_manifest_external_bridge_generation_for_components_lenient,
plan_repl_bridge_generation_lenient, validate_no_output_dir_collisions,
validate_supported_bridge_targets, write_repl_metadata,
plan_repl_bridge_generation_lenient, validate_host_managed_bridge_targets,
validate_no_output_dir_collisions, validate_supported_bridge_targets, write_repl_metadata,
};
use crate::app::context::BuildContext;
use crate::bridge_gen::BridgeMode;
Expand Down Expand Up @@ -395,6 +395,7 @@ fn validate_and_filter_new_bridge_targets(
.map(|(_, target)| target.clone())
.collect::<Vec<_>>();
validate_supported_bridge_targets(&exact_targets)?;
validate_host_managed_bridge_targets(&exact_targets)?;
validate_no_output_dir_collisions(&exact_targets)?;
validate_exact_targets_against_claims(&tagged_targets, claims)?;

Expand Down
92 changes: 90 additions & 2 deletions cli/golem-cli/src/bridge_gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ pub mod type_naming;
pub mod typescript;

use camino::Utf8Path;
use golem_common::model::agent::AgentTypeName;
use golem_common::model::agent::{AgentConfigSource, AgentTypeName};
use golem_common::schema::graph::reachable_defs;
use golem_common::schema::schema_type::{NamedFieldType, SchemaType};
use golem_common::schema::{AgentTypeSchema, InputSchema, SchemaGraph};
use golem_common::schema::{
AgentTypeSchema, FieldSource, InputSchema, SchemaGraph, find_host_managed_type,
};
use heck::ToKebabCase;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
Expand Down Expand Up @@ -89,6 +91,92 @@ pub fn bridge_client_directory_name(agent_type_name: &AgentTypeName, mode: Bridg
}
}

pub(crate) fn validate_host_managed_agent_bridge_policy(
agent: &AgentTypeSchema,
mode: BridgeMode,
) -> anyhow::Result<()> {
for field in agent.constructor.input_schema.fields() {
if matches!(field.source, FieldSource::UserSupplied) {
validate_host_managed_bridge_root(
agent,
mode,
&field.schema,
&format!("constructor parameter `{}`", field.name),
)?;
}
}

for config in &agent.config {
if config.source == AgentConfigSource::Local {
validate_host_managed_bridge_root(
agent,
mode,
&config.value_type,
&format!("configuration `{}`", config.path.join(".")),
)?;
}
}

if mode == BridgeMode::External {
for method in &agent.methods {
for field in method.input_schema.fields() {
if matches!(field.source, FieldSource::UserSupplied) {
validate_host_managed_bridge_root(
agent,
mode,
&field.schema,
&format!("method `{}` input parameter `{}`", method.name, field.name),
)?;
}
}

if let Some(output) = method.output_schema.schema() {
validate_host_managed_bridge_root(
agent,
mode,
output,
&format!("method `{}` output", method.name),
)?;
}
}
}

Ok(())
}

fn validate_host_managed_bridge_root(
agent: &AgentTypeSchema,
mode: BridgeMode,
ty: &SchemaType,
root: &str,
) -> anyhow::Result<()> {
let occurrence = find_host_managed_type(&agent.schema, ty).map_err(|error| {
anyhow::anyhow!(
"cannot validate {root} for {mode} bridge SDK agent `{}`: {error}",
agent.type_name
)
})?;

if let Some(occurrence) = occurrence {
let supported_position = match mode {
BridgeMode::External => {
"host-managed capabilities are not supported by external bridge SDKs"
}
BridgeMode::Guest => {
"host-managed capabilities are supported only in guest RPC method inputs and outputs"
}
};
anyhow::bail!(
"cannot generate {mode} bridge SDK for agent `{}`: {root} contains host-managed capability `{}` at {}; {supported_position}",
agent.type_name,
occurrence.kind.kind_name(),
occurrence.path,
);
}

Ok(())
}

pub(crate) fn projected_schema_graph(graph: &SchemaGraph, root: &SchemaType) -> SchemaGraph {
SchemaGraph {
defs: reachable_defs(graph, root),
Expand Down
Loading
Loading