Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
22 changes: 21 additions & 1 deletion .github/workflows/python-interpreter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ jobs:
OWNER_LOWER=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')
echo "image_name=ghcr.io/$OWNER_LOWER/afm-langchain-interpreter" >> $GITHUB_OUTPUT
- name: Build and push Docker image
- name: Build and push full image
uses: docker/build-push-action@v5
with:
context: python-interpreter
push: true
platforms: linux/amd64,linux/arm64
build-args: VARIANT=full
tags: |
${{ steps.meta.outputs.image_name }}:latest
${{ steps.meta.outputs.image_name }}:${{ github.sha }}
Expand All @@ -90,3 +91,22 @@ jobs:
annotations: |
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
index:org.opencontainers.image.licenses=Apache-2.0
- name: Build and push slim image
uses: docker/build-push-action@v5
with:
context: python-interpreter
push: true
platforms: linux/amd64,linux/arm64
build-args: VARIANT=slim
tags: |
${{ steps.meta.outputs.image_name }}:slim
${{ steps.meta.outputs.image_name }}:${{ github.sha }}-slim
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.title=AFM LangChain Interpreter (Slim)
org.opencontainers.image.licenses=Apache-2.0
annotations: |
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
index:org.opencontainers.image.licenses=Apache-2.0
1 change: 1 addition & 0 deletions .github/workflows/release-ballerina.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ jobs:

bump-version:
needs: [validate, finalize]
if: false # Disabled: direct push blocked by branch protection
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
74 changes: 60 additions & 14 deletions .github/workflows/release-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ on:
description: "Human-readable image title for OCI labels (e.g., AFM Ballerina Interpreter)"
required: true
type: string
build_slim:
description: "Whether to build and push a slim image variant"
required: false
default: false
type: boolean

jobs:
docker:
Expand Down Expand Up @@ -56,21 +61,24 @@ jobs:
run: |
# GHCR requires lowercase repository names
OWNER_LOWER=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')
FULL_IMAGE="ghcr.io/$OWNER_LOWER/$IMAGE_NAME"
TAGS="$FULL_IMAGE:v$VERSION"
if [ "$UPDATE_LATEST" = "true" ]; then
TAGS="$TAGS,$FULL_IMAGE:latest"
fi
echo "TAGS=$TAGS" >> $GITHUB_OUTPUT
echo "FULL_IMAGE=$FULL_IMAGE" >> $GITHUB_OUTPUT
BASE_IMAGE="ghcr.io/$OWNER_LOWER/$IMAGE_NAME"
TAGS_FULL="$BASE_IMAGE:v$VERSION"
[ "$UPDATE_LATEST" = "true" ] && TAGS_FULL="$TAGS_FULL,$BASE_IMAGE:latest"
echo "TAGS_FULL=$TAGS_FULL" >> $GITHUB_OUTPUT

- name: Build and push Docker image
TAGS_SLIM="$BASE_IMAGE:v$VERSION-slim"
[ "$UPDATE_LATEST" = "true" ] && TAGS_SLIM="$TAGS_SLIM,$BASE_IMAGE:slim"
echo "TAGS_SLIM=$TAGS_SLIM" >> $GITHUB_OUTPUT
echo "BASE_IMAGE=$BASE_IMAGE" >> $GITHUB_OUTPUT

- name: Build and push full image
uses: docker/build-push-action@v5
with:
context: ${{ inputs.context }}
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.docker-tags.outputs.TAGS }}
build-args: VARIANT=full
tags: ${{ steps.docker-tags.outputs.TAGS_FULL }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.version=${{ inputs.version }}
Expand All @@ -81,18 +89,56 @@ jobs:
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
index:org.opencontainers.image.licenses=Apache-2.0

- name: Scan Docker image for vulnerabilities
- name: Build and push slim image
if: ${{ inputs.build_slim }}
uses: docker/build-push-action@v5
with:
context: ${{ inputs.context }}
push: true
platforms: linux/amd64,linux/arm64
build-args: VARIANT=slim
tags: ${{ steps.docker-tags.outputs.TAGS_SLIM }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.version=${{ inputs.version }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.title=${{ inputs.image_title }} (Slim)
org.opencontainers.image.licenses=Apache-2.0
annotations: |
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
index:org.opencontainers.image.licenses=Apache-2.0

- name: Scan full Docker image for vulnerabilities
uses: aquasecurity/trivy-action@0.34.0
with:
image-ref: ${{ steps.docker-tags.outputs.FULL_IMAGE }}:v${{ inputs.version }}
image-ref: ${{ steps.docker-tags.outputs.BASE_IMAGE }}:v${{ inputs.version }}
format: "sarif"
output: "trivy-results.sarif"
output: "trivy-results-full.sarif"
severity: "CRITICAL,HIGH"
limit-severities-for-sarif: true
exit-code: "1"

- name: Upload Trivy scan results to GitHub Security tab
- name: Upload full image Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: "trivy-results.sarif"
sarif_file: "trivy-results-full.sarif"
category: "trivy-full-${{ inputs.image_name }}"

- name: Scan slim Docker image for vulnerabilities
if: ${{ always() && inputs.build_slim }}
uses: aquasecurity/trivy-action@0.34.0
with:
image-ref: ${{ steps.docker-tags.outputs.BASE_IMAGE }}:v${{ inputs.version }}-slim
format: "sarif"
output: "trivy-results-slim.sarif"
severity: "CRITICAL,HIGH"
limit-severities-for-sarif: true
exit-code: "1"

- name: Upload slim image Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: ${{ always() && inputs.build_slim }}
with:
sarif_file: "trivy-results-slim.sarif"
category: "trivy-slim-${{ inputs.image_name }}"
39 changes: 22 additions & 17 deletions .github/workflows/release-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ on:
workflow_dispatch:
inputs:
package:
description: 'Python package to release'
description: "Python package to release"
required: true
type: choice
options:
- afm-core
- afm-langchain
branch:
description: 'Branch to release from'
description: "Branch to release from"
required: false
default: 'main'
default: "main"
type: string
skip_pypi:
description: "Skip PyPI publishing"
required: false
default: false
type: boolean

concurrency:
group: release-python-interpreter
Expand Down Expand Up @@ -121,11 +126,12 @@ jobs:
pypi-publish:
needs: [validate, test, docker]
if: >-
!cancelled()
&& needs.validate.result == 'success'
&& needs.test.result == 'success'
&& (needs.docker.result == 'success'
|| needs.docker.result == 'skipped')
!cancelled()
&& !inputs.skip_pypi
&& needs.validate.result == 'success'
&& needs.test.result == 'success'
&& (needs.docker.result == 'success'
|| needs.docker.result == 'skipped')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
Expand Down Expand Up @@ -181,18 +187,20 @@ jobs:
version: ${{ needs.validate.outputs.release_version }}
branch: ${{ inputs.branch }}
image_title: AFM LangChain Interpreter
build_slim: true
permissions:
packages: write
security-events: write

finalize:
needs: [validate, pypi-publish, docker]
if: >-
!cancelled()
&& needs.validate.result == 'success'
&& needs.pypi-publish.result == 'success'
&& (needs.docker.result == 'success'
|| needs.docker.result == 'skipped')
!cancelled()
&& needs.validate.result == 'success'
&& (needs.pypi-publish.result == 'success'
|| (needs.pypi-publish.result == 'skipped' && inputs.skip_pypi))
&& (needs.docker.result == 'success'
|| needs.docker.result == 'skipped')
uses: ./.github/workflows/release-finalize.yml
with:
tag: ${{ needs.validate.outputs.tag }}
Expand All @@ -206,10 +214,7 @@ jobs:

bump-version:
needs: [validate, finalize]
if: >-
!cancelled()
&& needs.validate.result == 'success'
&& needs.finalize.result == 'success'
if: false # Disabled: direct push blocked by branch protection
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
5 changes: 2 additions & 3 deletions ballerina-interpreter/agent.bal
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@ function createAgent(AFMRecord afmRecord) returns ai:Agent|error {
if mcpServers is MCPServer[] {
foreach MCPServer mcpConn in mcpServers {
Transport transport = mcpConn.transport;
if transport.'type != "http" {
log:printWarn(string `Unsupported transport type: ${transport.'type}, only 'http' is supported`);
continue;
if transport is StdioTransport {
return error("Stdio transport is not yet supported by the Ballerina interpreter");
}

string[]? filteredTools = getFilteredTools(mcpConn.tool_filter);
Expand Down
36 changes: 30 additions & 6 deletions ballerina-interpreter/parser.bal
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,36 @@ function validateHttpVariables(AFMRecord afmRecord) returns error? {
}

Transport transport = server.transport;
if containsHttpVariable(transport.url) {
erroredKeys.push("tools.mcp.transport.url");
}

if authenticationContainsHttpVariable(transport.authentication) {
erroredKeys.push("tools.mcp.transport.authentication");
if transport is HttpTransport {
if containsHttpVariable(transport.url) {
erroredKeys.push("tools.mcp.transport.url");
}

if authenticationContainsHttpVariable(transport.authentication) {
erroredKeys.push("tools.mcp.transport.authentication");
}
} else {
if containsHttpVariable(transport.command) {
erroredKeys.push("tools.mcp.transport.command");
}

string[]? args = transport.args;
if args is string[] {
foreach int idx in 0 ..< args.length() {
if containsHttpVariable(args[idx]) {
erroredKeys.push(string `tools.mcp.transport.args[${idx}]`);
}
}
}

map<string>? env = transport.env;
if env is map<string> {
foreach [string, string] [k, val] in env.entries() {
if containsHttpVariable(val) {
erroredKeys.push("tools.mcp.transport.env." + k);
}
}
}
}

if toolFilterContainsHttpVariable(server.tool_filter) {
Expand Down
69 changes: 69 additions & 0 deletions ballerina-interpreter/tests/main_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,75 @@ function testContainsHttpVariable() {
test:assertFalse(containsHttpVariable("no variables here"));
}

@test:Config
function testValidateHttpVariablesInStdioTransportArgs() {
AFMRecord afmRecord = {
metadata: {
spec_version: "0.3.0",
tools: {
mcp: [
{
name: "test-server",
transport: <StdioTransport>{
'type: stdio,
command: "some-command",
args: ["${http:payload.field}", "--safe-arg", "${http:header.auth}"]
}
}
]
}
},
role: "",
instructions: ""
};

error? result = validateHttpVariables(afmRecord);
if result is () {
test:assertFail("Expected error for http: variables in stdio transport args");
}
test:assertTrue(result.message().includes("tools.mcp.transport.args[0]"),
"Expected error to include 'tools.mcp.transport.args[0]'");
test:assertTrue(result.message().includes("tools.mcp.transport.args[2]"),
"Expected error to include 'tools.mcp.transport.args[2]'");
test:assertFalse(result.message().includes("tools.mcp.transport.args[1]"),
"Expected error NOT to include 'tools.mcp.transport.args[1]' (clean arg)");
}

@test:Config
function testValidateHttpVariablesInStdioTransportEnv() {
AFMRecord afmRecord = {
metadata: {
spec_version: "0.3.0",
tools: {
mcp: [
{
name: "test-server",
transport: <StdioTransport>{
'type: stdio,
command: "some-command",
env: {
"CLEAN_VAR": "safe-value",
"SECRET_KEY": "${http:header.Authorization}"
}
}
}
]
}
},
role: "",
instructions: ""
};

error? result = validateHttpVariables(afmRecord);
if result is () {
test:assertFail("Expected error for http: variables in stdio transport env");
}
test:assertTrue(result.message().includes("tools.mcp.transport.env.SECRET_KEY"),
"Expected error to include 'tools.mcp.transport.env.SECRET_KEY'");
test:assertFalse(result.message().includes("tools.mcp.transport.env.CLEAN_VAR"),
"Expected error NOT to include 'tools.mcp.transport.env.CLEAN_VAR' (clean env var)");
}

@test:Config
function testParseAfmWithoutFrontmatter() {
string content = string `# Role
Expand Down
14 changes: 12 additions & 2 deletions ballerina-interpreter/types.bal
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,25 @@ type Model record {|
|};

enum TransportType {
http
http,
stdio
}

type Transport record {|
type HttpTransport record {|
http 'type = http;
string url;
ClientAuthentication authentication?;
|};

type StdioTransport record {|
stdio 'type = stdio;
string command;
string[] args?;
map<string> env?;
|};

type Transport HttpTransport|StdioTransport;

type ClientAuthentication record {
string 'type;
};
Expand Down
Loading
Loading