Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5ff50c8
fix(azure-foundry): make getAgentStatus stateless for multi-replica d…
shaileshpadave Jul 31, 2026
82d9fb8
style: apply spotless formatting
shaileshpadave Jul 31, 2026
91812da
fix: update test-harness to match getAgentStatus(executionId, request…
shaileshpadave Jul 31, 2026
ca16fef
refactor: plain text comments, remove HTML tags and Javadoc markup
shaileshpadave Jul 31, 2026
7e45df1
refactor(azure-foundry): replace compound executionId with AzureAgent…
shaileshpadave Jul 31, 2026
bb112c9
agent upates
v1r3n Aug 28, 2026
84c95e6
Merge origin/main into hosted-agent tool execution
v1r3n Aug 28, 2026
c1f703d
feat(agents): fold in #1447 — Entra ID auth modes, agent discovery, A…
v1r3n Aug 28, 2026
76a7588
feat(azure-foundry): route to all three Foundry surfaces
v1r3n Aug 28, 2026
c1cd4a3
test(azure-foundry): drop the dead token-endpoint interceptor
v1r3n Aug 28, 2026
1ba7e36
refactor(agents): hand clients resolved credentials instead of a secr…
v1r3n Aug 29, 2026
8a1d4b9
feat(agents): consistent API-key auth, and Bedrock says it cannot tak…
v1r3n Aug 29, 2026
1139557
feat(bedrock): support Bedrock API keys via bearer auth
v1r3n Aug 29, 2026
3b41186
feat(ui): guided credential picker for hosted agent tasks
v1r3n Aug 29, 2026
91fd21f
fix(ui): stop "secret" meaning two things on the agent credentials sc…
v1r3n Aug 29, 2026
9c6d3a1
fix(ui): explain the Azure token scope, and hide it where it does not…
v1r3n Aug 29, 2026
8867ff5
fix(agents): refuse to run as the server when a credential fails to r…
v1r3n Aug 29, 2026
d7321d7
fix(secrets): read a JSON secret that arrived wrapped in quotes
v1r3n Aug 29, 2026
caa446f
fix(agents): close the gaps the quote-wrapped secret exposed
v1r3n Aug 29, 2026
cea2b19
fix(azure-foundry): key the token cache on what a token depends on
v1r3n Aug 29, 2026
b04f55b
refactor: rename Azure AI Foundry to Microsoft Foundry
v1r3n Aug 29, 2026
535dcb2
docs(hosted-agents): correct what the credential changes actually did
v1r3n Aug 29, 2026
870f7aa
feat(agent): reject an AGENT task with no prompt when the definition …
v1r3n Aug 30, 2026
1fd82e6
feat(agents): record the tools a hosted agent ran by itself
v1r3n Aug 30, 2026
9b9f9ec
fix(microsoft-foundry): invoke the agent instead of imitating it
v1r3n Aug 30, 2026
aedf8eb
fix(microsoft-foundry): the property is agent_reference, not agent
v1r3n Aug 30, 2026
1b7387c
chore(microsoft-foundry): log which item types a Responses reply carried
v1r3n Aug 30, 2026
5fb3d70
fix(microsoft-foundry): stop logging the start request, credentials i…
v1r3n Aug 30, 2026
600bca3
fix(agents): stop the AGENT worker dropping half its own output
v1r3n Aug 30, 2026
f1c93ad
fix(microsoft-foundry): a function_call is a tool request, not work a…
v1r3n Aug 30, 2026
31f025f
feat(engine): scheduleDynamicTasks, for a task that decides its own n…
v1r3n Aug 30, 2026
e49ab86
fix(agents): defects found reviewing the Responses and scheduling work
v1r3n Aug 30, 2026
2869608
test(agents): prove a function tool actually round-trips
v1r3n Aug 30, 2026
1c65aaa
feat(microsoft-foundry): let a task declare the tools it will serve i…
v1r3n Aug 30, 2026
566ebad
docs(hosted-agents): how to give a Foundry agent a function tool
v1r3n Aug 30, 2026
21f1ce0
docs(hosted-agents): model is a deployment name, not a model name
v1r3n Aug 30, 2026
26f10b2
fix(agents): treat model-written tool arguments as text, not expressions
v1r3n Aug 30, 2026
6d7c9f9
fix(microsoft-foundry): say where the model actually comes from on De…
v1r3n Aug 30, 2026
09cd073
feat(agents): run an agent's tools as tasks in its own workflow, by d…
v1r3n Aug 30, 2026
b30abe0
test(agents): assert the container produces exactly one tool dispatcher
v1r3n Aug 30, 2026
6172ce7
feat(microsoft-foundry): carry endpoint in AgentSummary, auto-fill fr…
shaileshpadave Aug 30, 2026
4f76829
test(microsoft-foundry): verify endpoint is carried in AgentSummary f…
shaileshpadave Aug 30, 2026
a6ea569
fix(agents): make the tool loop survive more than one turn
v1r3n Aug 31, 2026
0eb2f80
feat(ui): show an agent's tools as its children, and say when one has…
v1r3n Aug 31, 2026
8db0995
test(agents): update tool-loop assertion to match clearToolBatch beha…
shaileshpadave Aug 31, 2026
ec7355e
test(agents): raise agentInternalFailure timeout from 60s to 90s
shaileshpadave Sep 1, 2026
58cc09f
Merge branch 'main' into feat/hosted-agent-tool-execution
shaileshpadave Sep 1, 2026
27464cc
Merge remote-tracking branch 'origin/main' into feat/hosted-agent-too…
shaileshpadave Sep 2, 2026
3cd442c
Merge branch 'main' into feat/hosted-agent-tool-execution
v1r3n Sep 3, 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
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,17 @@ This matters because plausible-looking docs can be silently wrong. Concretely: a
2. Verify each one using the steps above — not just the block you plan to change.
3. Fix anything you find while you're there.

### Building the docs

```bash
PYTHONPATH=. ./.mkdocs-venv/bin/mkdocs build
```

`PYTHONPATH=.` is required. `mkdocs.yml` references `main.mermaid_fence`, and a console script puts
its own directory on `sys.path` rather than the working directory, so without it the build dies at
config parse with `cannot find module 'main'` — which reads like a broken checkout rather than a
missing path entry.

### When you can't verify

If a running server or CLI binary is unavailable:
Expand Down
12 changes: 11 additions & 1 deletion agentspan/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ dependencies {
implementation project(':conductor-core')
implementation project(':conductor-ai')

// AWS Bedrock Agent Runtime — for BedrockAgentClient
// AWS Bedrock Agent Runtime — for BedrockAgentClient (invoke)
implementation "software.amazon.awssdk:bedrockagentruntime:${revAwsSdk}"
// AWS Bedrock Agent management client — for listing agents
implementation "software.amazon.awssdk:bedrockagent:${revAwsSdk}"
// AWS STS — for AssumeRole
implementation "software.amazon.awssdk:sts:${revAwsSdk}"

// Azure Identity SDK — service principal, managed identity, and default credential chain
implementation "com.azure:azure-identity:${revAzureIdentitySdk}"

// Exercise the adapters against a real persisted backend rather than mocked DAOs.
testImplementation project(':conductor-sqlite-persistence')
Expand All @@ -21,6 +28,9 @@ dependencies {
// Conductor-Agents' real event listener depends on its SSE registry at test runtime.
testImplementation 'org.springframework:spring-webmvc'

// Drives AzureFoundryAgentClient against a real HTTP server rather than a mocked OkHttpClient.
testImplementation "com.squareup.okhttp3:mockwebserver:4.12.0"

testImplementation 'org.graalvm.polyglot:polyglot:25.0.2'
testImplementation 'org.graalvm.js:js:25.0.2'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ private KnownProviderEnvVars() {}
"BEDROCK_API_KEY",
"BEDROCK_AGENT_ID",
"BEDROCK_AGENT_ALIAS_ID",
// Azure Foundry / Entra ID
// Microsoft Foundry / Entra ID
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"AZURE_TENANT_ID",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright 2026 Conductor Authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.conductoross.conductor.ai.agentspan.runtime.service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;

/**
* Reads the credential values Conductor substituted into a task's input.
*
* <p>Shared by every hosted-agent client so one credential reads the same way whichever platform it
* is for, and so the guard against an unsubstituted reference exists in exactly one place.
*/
public final class AgentCredentials {

private AgentCredentials() {}

/**
* One credential value, rejecting anything the engine did not substitute.
*
* <p>Conductor resolves {@code ${workflow.secrets.X}} in task input before the task runs, but
* not when the input was offloaded to external payload storage. Passing such a value on would
* mean sending the literal reference as a credential — and since every lookup would then miss,
* auth would fall through to the host's own identity and the agent would silently run as
* someone else. Fail instead.
*/
public static String value(Map<String, String> credentials, String key) {
if (credentials == null) {
return null;
}
String value = credentials.get(key);
// Checked before the quoting guard: a reference that was never substituted is the more
// specific diagnosis, and a quoted one would otherwise be reported as merely quoted.
if (value != null && value.contains("${workflow.secrets.")) {
throw new IllegalArgumentException(
"Credential '"
+ key
+ "' still holds an unresolved secret reference. Conductor does not"
+ " substitute secrets for task input held in external payload storage;"
+ " pass the value another way rather than running as the host identity.");
}
rejectQuoteWrapped(key, value);
return value;
}

/**
* Rejects a credential that arrived wrapped in quote characters.
*
* <p>A reference with a sub-key, ${workflow.secrets.NAME.key}, is extracted from JSON and so
* cannot pick up stray quotes. A flat one, ${workflow.secrets.NAME}, is handed over exactly as
* stored, and a .env file read verbatim keeps the quotes a shell would have stripped. The
* credential then reaches the provider with two extra characters and comes back as a plain
* authentication failure, which says nothing about where to look.
*
* <p>Rejected rather than trimmed: no API key, client secret, or access key is quoted on
* purpose, and silently sending a guess produces the same opaque failure this is meant to
* prevent.
*/
private static void rejectQuoteWrapped(String key, String value) {
if (value == null || value.length() < 2) {
return;
}
char first = value.charAt(0);
char last = value.charAt(value.length() - 1);
if ((first == '\'' || first == '"') && first == last) {
throw new IllegalArgumentException(
"Credential '"
+ key
+ "' starts and ends with a "
+ first
+ " character. The stored secret includes the quotes; a .env file read"
+ " verbatim keeps the ones a shell would have removed. Store the value"
+ " without them.");
}
}

/**
* The API key, under either spelling.
*
* <p>Providers name this differently in their own docs — Azure writes {@code apiKey}, OpenAI
* {@code api_key} — and a workflow author configuring a second provider should not have to
* discover that. Both are accepted everywhere an API key is.
*/
public static String apiKey(Map<String, String> credentials) {
return StringUtils.defaultIfBlank(
value(credentials, "apiKey"), value(credentials, "api_key"));
}

/**
* Guards the point where a client would otherwise fall back to the identity the server itself
* runs as. That fallback is correct for a task that supplied no credentials — it is how managed
* identity and instance roles are meant to work — and wrong for a task that supplied some,
* because it silently authenticates as somebody else.
*
* <p>A task reaches this state without any reference surviving in the input: {@code
* ${workflow.secrets.NAME.key}} resolves to null when the secret is missing, or holds something
* that is not JSON with that key, so every credential arrives blank and no auth mode matches.
* Nothing in the task input then shows what went wrong.
*
* @param authKeys the credential keys this provider can authenticate with, so unrelated keys
* carried in the same map (a scope override, say) do not read as a broken credential
*/
public static void rejectPartiallyResolved(
Map<String, String> credentials, Set<String> authKeys, String provider) {
if (credentials == null) {
return;
}
List<String> empty = new ArrayList<>();
List<String> resolved = new ArrayList<>();
credentials.forEach(
(key, value) -> {
if (authKeys.contains(key)) {
(StringUtils.isBlank(value) ? empty : resolved).add(key);
}
});
if (empty.isEmpty() && resolved.isEmpty()) {
return;
}
Collections.sort(empty);
Collections.sort(resolved);

// Which keys arrived and which did not is the whole diagnosis: an empty set points at the
// secret, a mixed one points at the task. Reporting "none resolved" for a partly resolved
// credential sends the reader to the wrong place, which is how this class of bug stays
// expensive.
String detail =
resolved.isEmpty()
? "the task set the credentials "
+ empty
+ " and every one of them is empty. A ${workflow.secrets.NAME.key}"
+ " reference resolves to nothing when the secret does not exist,"
+ " or does not hold that key."
: "the task set "
+ resolved
+ ", which resolved, alongside "
+ empty
+ ", which did not. That is not a complete way to authenticate.";

throw new IllegalArgumentException(
"No "
+ provider
+ " authentication mode could be built: "
+ detail
+ " Refusing to fall back to the identity this server runs as, which would"
+ " authenticate as somebody else.");
}
}
Loading
Loading