Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
68 changes: 57 additions & 11 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 @@ -57,20 +62,23 @@ jobs:
# GHCR requires lowercase repository names
OWNER_LOWER=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')
FULL_IMAGE="ghcr.io/$OWNER_LOWER/$IMAGE_NAME"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we change this variable name now that we have two variants of the Docker image, include a "full" one :)

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.

Addressed in 49c15ed

TAGS="$FULL_IMAGE:v$VERSION"
if [ "$UPDATE_LATEST" = "true" ]; then
TAGS="$TAGS,$FULL_IMAGE:latest"
fi
echo "TAGS=$TAGS" >> $GITHUB_OUTPUT
TAGS_FULL="$FULL_IMAGE:v$VERSION"
[ "$UPDATE_LATEST" = "true" ] && TAGS_FULL="$TAGS_FULL,$FULL_IMAGE:latest"
echo "TAGS_FULL=$TAGS_FULL" >> $GITHUB_OUTPUT

TAGS_SLIM="$FULL_IMAGE:v$VERSION-slim"
[ "$UPDATE_LATEST" = "true" ] && TAGS_SLIM="$TAGS_SLIM,$FULL_IMAGE:slim"
echo "TAGS_SLIM=$TAGS_SLIM" >> $GITHUB_OUTPUT
echo "FULL_IMAGE=$FULL_IMAGE" >> $GITHUB_OUTPUT

- name: Build and push Docker image
- 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 }}
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.FULL_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')
&& (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
38 changes: 32 additions & 6 deletions ballerina-interpreter/parser.bal
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,38 @@ 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 string arg in args {
if containsHttpVariable(arg) {
erroredKeys.push("tools.mcp.transport.args");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess we could go one level down and include the arg?

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.

Addressed in 7f3763b

break;
}
}
}

map<string>? env = transport.env;
if env is map<string> {
foreach string val in env {
if containsHttpVariable(val) {
erroredKeys.push("tools.mcp.transport.env");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ditto.

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.

Addressed in 7f3763b

break;
}
}
}
}

if toolFilterContainsHttpVariable(server.tool_filter) {
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
14 changes: 14 additions & 0 deletions python-interpreter/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,21 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-editable --package afm-core --package afm-langchain

# Stage 2: Final image
# VARIANT=full installs Node.js, npm (npx), git, and uv/uvx for MCP server support.
# VARIANT=slim ships only Python + .venv.
FROM python:3.13-alpine
ARG VARIANT=full

RUN if [ "$VARIANT" = "full" ]; then \
apk add --no-cache nodejs npm git; \
fi

# Install uv and uvx for Python-based MCP server support (full variant only)
COPY --from=ghcr.io/astral-sh/uv:0.10.0 /uv /uvx /uv-bins/
RUN if [ "$VARIANT" = "full" ]; then \
mv /uv-bins/uv /uv-bins/uvx /bin/; \
fi && \
rm -rf /uv-bins

# Set working directory
WORKDIR /app
Expand Down
6 changes: 3 additions & 3 deletions python-interpreter/packages/afm-cli/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "afm-cli"
version = "0.2.10"
version = "0.3.0"
description = "AFM CLI metapackage: installs afm-core and afm-langchain"
readme = "README.md"
classifiers = [
Expand All @@ -11,8 +11,8 @@ license = "Apache-2.0"
requires-python = ">=3.11"
urls = { Repository = "https://github.com/wso2/reference-implementations-afm" }
dependencies = [
"afm-core==0.1.7",
"afm-langchain>=0.1.0",
"afm-core==0.2.0",
"afm-langchain>=0.2.0",
]

[project.scripts]
Expand Down
2 changes: 1 addition & 1 deletion python-interpreter/packages/afm-core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "afm-core"
version = "0.1.7"
version = "0.2.0"
description = "AFM (Agent-Flavored Markdown) core: parser, CLI, protocols, and interfaces"
readme = "README.md"
classifiers = [
Expand Down
8 changes: 7 additions & 1 deletion python-interpreter/packages/afm-core/src/afm/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from .models import (
ConsoleChatInterface,
HttpTransport,
WebChatInterface,
WebhookInterface,
)
Expand Down Expand Up @@ -232,7 +233,12 @@ def format_validation_output(afm: AFMRecord) -> str:
lines.append("")
lines.append(" MCP Servers:")
for server in afm.metadata.tools.mcp:
lines.append(f" - {server.name}: {server.transport.url}")
transport = server.transport
if isinstance(transport, HttpTransport):
transport_info = transport.url
else:
transport_info = transport.command
lines.append(f" - {server.name}: {transport_info}")
if server.tool_filter:
if server.tool_filter.allow:
lines.append(f" Allow: {', '.join(server.tool_filter.allow)}")
Expand Down
Loading
Loading