Skip to content

Built-in and native tools - #3841

Merged
vigoo merged 9 commits into
mainfrom
gol-24
Sep 14, 2026
Merged

vigoo merged 9 commits into
mainfrom
gol-24

Conversation

@vigoo

@vigoo vigoo commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Resolves GOL-24

@netlify

netlify Bot commented Sep 8, 2026

Copy link
Copy Markdown

Deploy Preview for golemcloud canceled.

Name Link
🔨 Latest commit dbd9ad0
🔍 Latest deploy log https://app.netlify.com/projects/golemcloud/deploys/6aa81a35b6c78d0008e9b2b5

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

📖 Docs preview: https://docs-5ygaf8kbc-golem-cloud.vercel.app

Built from commit dbd9ad0de81c75b019d8e6bdce532243052d96cf by docs.yaml.

@vigoo
vigoo marked this pull request as ready for review September 9, 2026 15:15
@vigoo
vigoo requested a review from a team September 9, 2026 15:15
Comment on lines +2111 to +2134
let result = await_native_entity_body(
&runner_abort,
handler.invoke(retained.context_mut(), native_invocation),
)
.await;
retained
.context_mut()
.durable_ctx_mut()
.set_invocation_principal(None);
if let Err(error) = retained.prepare_parent_end().await {
return (Err(error), Some(Box::new(retained)));
}
let result = match result {
Ok(result) => {
if let Some(error) = stdout_limit_error(
stdout_observer
.as_ref()
.and_then(AttachmentObserver::terminal_snapshot)
.is_some_and(|terminal| terminal.host_resource_exhausted),
) {
encode_tool_operation_terminal(Err(error)).await
} else {
encode_tool_operation_terminal(result).await
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we mirror the component invocation path's post-body has_pending_live_admission_rejection guard for native tools? A native handler can catch an attachment admission error and return success; this path then encodes an ordinary terminal, while the later accounting check does not replace that response with the pending resource-exhaustion failure. Please preserve the rejection before terminal encoding and cover a handler that catches the host error.

Comment on lines +115 to +131
async fn invoke(
&self,
ctx: &mut Ctx,
invocation: NativeToolInvocation,
) -> Result<NativeToolResult, WorkerExecutorError> {
let result = self
.0
.invoke(
ctx,
golem_native_tool::NativeToolInvocation {
command_path: invocation.command_path,
input: invocation.input,
principal: invocation.principal,
stdin: invocation.stdin.map(|input| Box::new(input) as _),
stdout: invocation.stdout.map(|output| Box::new(output) as _),
},
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we expose and forward a safe cancellation handle through golem_native_tool::NativeToolInvocation? The executor invocation carries a CancellationToken, but NativeToolAdapter constructs the public invocation without it, so macro/SDK-authored native tools cannot observe cancellation even though direct executor handlers can. A cancellation test through the adapter would pin the supported authoring path.

Comment on lines +2116 to +2123
retained
.context_mut()
.durable_ctx_mut()
.set_invocation_principal(None);
if let Err(error) = retained.prepare_parent_end().await {
return (Err(error), Some(Box::new(retained)));
}
let result = match result {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the primary handler or infrastructure error when prepare_parent_end also fails, matching the component path? The native path currently returns the cleanup failure before examining the handler result, which can mask the failure that caused the invocation to abort. Substituting the cleanup error only when the body succeeded would retain the more useful diagnosis.

Comment on lines +1159 to +1160
if implementations.contains_key(&ambient.name)
|| app.tool_declarations().contains_key(&ambient.name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the intended rollout policy when a future Golem release introduces an ambient tool whose name is already used by an application or selected account-owned tool? AFAIU existing deployments remain intact, but the next deployment will fail. I guess this is fine (and similar to account shared ones), just wanted to double check.

@@ -1 +1 @@
9b8236ff15b23a2da0c9bbaaa28149cd9875f93d6a519dab7b5ed98c8dbdfd75
8432554acdeedaabbb278c764a8a66243102ea9197309ffa6248071564c9a3c3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should bump the diff model version, and create a v6 fingerprint

Comment on lines +1076 to +1085
// Ambient tools are registry-owned input. Resolve them before local validation and
// hashing; planning remains read-only.
let ambient_plan = self
.ctx
.golem_clients()
.await?
.environment
.get_environment_deployment_plan(&environment.environment_id.0)
.await
.map_service_error()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • prepare_deployment now fetches the same deployment plan twice; I think one read should be enough (also the first read calls it ambient_plan, which is not entirely correct)
  • ambientTools appears to be the first field in this DTO that is not information identifying or describing the staged diff side itself, but is auxiliary registry metadata used to construct the proposed side, so i think it would be clearer to separate it explicitly in the response DTO (or have some comment on it / naming convention for possible future similar ones?)

Comment thread cli/golem-cli/src/model/app_raw.rs Outdated
Comment on lines +368 to +373
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ManifestConfigKeyScope {
All(String),
Keys(Vec<String>),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of our checks are only visible in the JSON schema, but that is extra meta, not the main validation we use in the CLI:

The JSON Schema constraints are useful diagnostics, but they are not currently enforced by Rust when the manifest deserializes successfully: the untagged scope enums accept any string and any string list, while path validation happens only during deployment. Please enforce the wildcard/list shape through serde and validate every path while loading/building the application in app_raw.rs or app.rs. The same issue exists for the pre-existing secret scopes, and the generic audit also found that tool-binding keys rely on the schema regex without equivalent Rust load-time validation.

Comment on lines +946 to +954
"items": {
"type": "string",
"not": {
"const": "*"
}
}
}
]
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we keep modifying these dev one, unlike how they were originally intended to be used. I guess this can be the new norm, but we might want to adjust the skills so we do not modify non dev versions. And we might want to think about automating the publishing of these, to guarantee no update on the non dev one (not in this PR, just want to mention).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pub struct DeploymentPlanAmbientToolEntry {
pub release_id: crate::model::tool_release::ToolReleaseId,
pub name: ToolName,
pub version: String,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define either a new transparent newtype for this like DeploymentVersion or use that, if appropriate. Same applies for metadata_version below.

Comment thread golem-common/tests/agent_extraction.rs Outdated
#[test]
async fn can_extract_tool_metadata_from_component_bytes() -> anyhow::Result<()> {
let wasm = std::fs::read(
"../test-components/tool-streaming/golem-temp/agents/golem_it_tool_streaming_rust_provider_release.wasm",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not use a temp build dir - use the wasm from ../test-components where the test component build script places it

pub environment_state_service: Arc<EnvironmentStateService>,
pub http_api_deployment_service: Arc<HttpApiDeploymentService>,
pub mcp_deployment_service: Arc<McpDeploymentService>,
pub native_tool_catalog: Arc<crate::services::native_tool_catalog::NativeToolCatalog>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not import the trait - don't use fully qualified paths where not necessary (not just this line)

let mut registrations = vec![NativeToolRegistration {
definition: native_test_helper_definition(),
handler: Arc::new(NativeToolAdapter(
__GolemNativeToolInvokerNativeDurableHelperImplNativeDurableHelper::new(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this horrible type name and (if it is macro generated) why do we need to explicitly use here? Let's avoid that

/// conformance of these literals against their referenced type node is checked
/// separately by [`validate_tool`], which has the per-command field context
/// needed to resolve `value-is` references.
#[cfg(any())]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 12 #[cfg(any())] blocks in this file (none existed before the move). That is dead code that is merely compiled out — delete it instead. We do not keep old code paths around (see the backward-compatibility section in AGENTS.md).

&deployment_write_service,
&tool_release_service,
)
.await

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Built-in tool provisioning failure aborts registry startup here, while built-in plugin provisioning just above only warns. Pick one behaviour deliberately for both. Hard-failing is arguably right for protected system releases, but then the plugin path should match (or the difference should be documented).

@@ -120,11 +120,6 @@ impl ToolReleaseRecord {
"system tool release coordinate does not match its definition"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "protected system tool releases must use a host source" check was removed here so that component-implemented built-ins can also be protected system releases. Nothing now enforces the source/availability pairing though: AmbientHost source and GrantableComponent source. Please reinstate that invariant in its new form (here or in ToolReleaseService::provision_system_release).


static NATIVE_TEST_EFFECTS: AtomicUsize = AtomicUsize::new(0);
static NATIVE_TEST_HELPER_EFFECTS: AtomicUsize = AtomicUsize::new(0);
static NATIVE_TEST_TOOL_METADATA: OnceLock<golem_common::schema::tool::Tool> = OnceLock::new();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Process-global mutable state in test utils (OnceLock + install_native_test_tool_metadata) makes the native test tool registration depend on which test ran first in the process and leaks between tests. Register test tools explicitly on the test context/catalog instead — this goes away naturally once the catalog is a bootstrap-time service passed into the context rather than a static fn.

let ordinary_remote_tools = data
.remote_tools
.iter()
.filter(|deployment| !ambient_catalog.contains_key(&deployment.name))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI rejects a top-level tools entry whose name collides with an ambient tool, but the API silently accepts it: the user's release/provision/environment_binding is dropped here and replaced with the ambient one, and only agent_bindings survive. The server should reject the collision too (or at least error when the requested release differs from the ambient release) instead of rewriting the request.

crate::services::environment_tool_grant::ResolvedGrantedToolRelease {
owner: AccountSummary {
id: release.owner_account_id,
name: "Golem system".to_string(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded display name; take it from the actual built-in tool owner account instead of a literal.

@vigoo
vigoo merged commit 38299ee into main Sep 14, 2026
71 checks passed
@vigoo
vigoo deleted the gol-24 branch September 14, 2026 17:55
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants