Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 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' && 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
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
23 changes: 22 additions & 1 deletion python-interpreter/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,28 @@ 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

# Validate VARIANT early so typos fail loudly instead of silently producing a slim build.
RUN if [ "$VARIANT" != "full" ] && [ "$VARIANT" != "slim" ]; then \
echo "ERROR: VARIANT must be 'full' or 'slim', got: '$VARIANT'" >&2; \
exit 1; \
fi

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)
# Use bind mounts so the uv binaries are never written into any image layer for slim builds.
RUN --mount=type=bind,from=ghcr.io/astral-sh/uv:0.10.0,source=/uv,target=/tmp/uv \
--mount=type=bind,from=ghcr.io/astral-sh/uv:0.10.0,source=/uvx,target=/tmp/uvx \
if [ "$VARIANT" = "full" ]; then \
cp /tmp/uv /tmp/uvx /bin/; \
fi

# Set working directory
WORKDIR /app
Expand All @@ -45,7 +66,7 @@ ENV PYTHONUNBUFFERED=1
ENV AFM_RUNTIME=docker

# Expose default port for web interfaces
EXPOSE 8000
EXPOSE 8085

# Entry point to run the afm CLI
ENTRYPOINT ["afm"]
Expand Down
4 changes: 2 additions & 2 deletions python-interpreter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ uv run afm path/to/agent.afm.md
Configuration via environment variables or CLI options:

- `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc. (Required based on provider)
- HTTP port can be set via `-p` or `--port` (default: 8000)
- HTTP port can be set via `-p` or `--port` (default: 8085)

## Running with Docker

Expand All @@ -51,7 +51,7 @@ The Docker image bundles the LangChain execution backend.
# Using the pre-built image
docker run -v $(pwd)/path/to/agent.afm.md:/app/agent.afm.md \
-e OPENAI_API_KEY=$OPENAI_API_KEY \
-p 8000:8000 \
-p 8085:8085 \
ghcr.io/wso2/afm-langchain-interpreter:latest run /app/agent.afm.md

# Or build locally
Expand Down
4 changes: 2 additions & 2 deletions python-interpreter/packages/afm-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ afm framework list

### CLI Options

- `-p, --port PORT` - Port for web interfaces (default: 8000)
- `-p, --port PORT` - Port for web interfaces (default: 8085)
- `--help` - Show help message

## Features
Expand Down Expand Up @@ -126,7 +126,7 @@ docker build -t afm-langchain-interpreter .
# Run with an AFM file mounted
docker run -v $(pwd)/agent.afm.md:/app/agent.afm.md \
-e OPENAI_API_KEY=$OPENAI_API_KEY \
-p 8000:8000 \
-p 8085:8085 \
afm-langchain-interpreter afm /app/agent.afm.md
```

Expand Down
Loading
Loading