> For the complete documentation index, see [llms.txt](https://docs.veedna.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.veedna.com/unifai/setting-up/deploy-unifai-on-azure.md).

# Deploy UnifAI on Azure

Deploy the Lineaje UnifAI MCP Server on an Azure Linux VM.

## Deploy UnifAI on Azure

### Hybrid MCP Server Deployment Guide

Deploy the Lineaje UnifAI MCP Server on an Azure Linux VM with Docker Compose.

The standard deployment exposes MCP over HTTPS. Caddy protects the private application container.

{% hint style="info" %}
Ema and Pay-i are optional integrations. They are not required for base deployment.
{% endhint %}

### 1. Architecture overview

| Component  | Image / runtime          | Purpose                                    | Network exposure           |
| ---------- | ------------------------ | ------------------------------------------ | -------------------------- |
| `combined` | `lineaje-combined:<tag>` | UnifAI MCP server, APIs, and enabled tools | Port `8080`, internal only |
| `caddy`    | `caddy:2-alpine`         | TLS termination and reverse proxy          | Port `443`                 |

External clients connect through Caddy. Do not expose `8080` through the Azure NSG or host firewall.

```
MCP client → HTTPS :443 → Caddy TLS proxy → HTTP :8080 → UnifAI MCP server
```

| Endpoint  | Protocol        | Purpose                                 |
| --------- | --------------- | --------------------------------------- |
| `/health` | `GET`           | Service readiness and health validation |
| `/mcp`    | Streamable HTTP | MCP-compatible client endpoint          |

### 2. Azure requirements

| Resource         | Minimum               |
| ---------------- | --------------------- |
| CPU              | 4 vCPU                |
| Memory           | 16 GB RAM             |
| Operating system | Ubuntu 22.04 or later |
| Architecture     | `x86_64` / `amd64`    |
| Storage          | 300 GB                |

Install Docker Engine 24+, Docker Compose v2, OpenSSH, and the current Azure CLI.

```bash
docker --version
docker compose version
az version
```

### 3. Identity and network access

Enable a system-assigned managed identity on the VM. Grant it the appropriate Microsoft Foundry inference role.

`Cognitive Services User` also supports model inference where available.

```bash
az vm identity show -g <RESOURCE_GROUP> -n <VM_NAME> --query principalId -o tsv
az role assignment list --assignee <PRINCIPAL_ID> --scope <FOUNDRY_RESOURCE_ID> -o table
```

Use the managed identity for Foundry authentication. Do not use a long-lived model API key.

Allow inbound HTTPS on `443` only from approved corporate, VPN, or VNet networks. Open `80` only when an HTTP ACME challenge requires it.

Allow outbound HTTPS to:

* Microsoft Foundry: `*.services.ai.azure.com`, `*.openai.azure.com`, or the configured model endpoint.
* Lineaje SCIM: `https://scim-service.v2.prod.veedna.com`.
* Lineaje Data Service: `https://data-service-v2-apigw.v2.prod.veedna.com`.
* Lineaje Identity Service: `https://lineaje-identity-service.v2.prod.veedna.com/lineajeidentity/realms/idp`.
* Lineaje S3: `https://us-east-1-commercialprod-veedna-opa.s3.us-east-1.amazonaws.com/` for artifact-transfer workflows.

Use destination allowlisting where required.

Azure Instance Metadata Service (IMDS) provides managed-identity tokens to the VM. IMDS is VM-local and needs no firewall allowlist entry.

### 4. TLS configuration

Caddy terminates HTTPS.

For controlled testing, set:

```bash
SITE_ADDRESS=:443
```

Caddy uses its local certificate authority. Trust its generated CA or allow the test certificate.

For production, set a resolvable FQDN:

```bash
SITE_ADDRESS=unifai.example.com
```

Use an approved certificate configuration. Open `80` if the certificate flow needs HTTP validation.

### 5. Before you begin

Confirm the VM meets sizing requirements. Confirm `443` is restricted. Verify managed-identity access, DNS, TLS, required configuration values, and rollback availability.

{% hint style="warning" %}
Do not start production deployment when TLS, inbound ranges, identity permissions, or authentication configuration are unknown.
{% endhint %}

### 6. Deploy UnifAI

Connect and run preflight checks:

```bash
ssh -i <keyfile.pem> azureuser@<VM_PUBLIC_IP>
uname -m
lsb_release -ds
docker --version
docker compose version
free -h
df -h
```

Transfer and extract the provided release bundle:

[Download Hybrid UnifAI Server Bundle (v8)](https://downloads.prod.veedna.com/Wipro/INTERCEPT/v8_folder/lineaje-deploy-intercept-v8.tar.zip)

```bash
unzip -o lineaje-deploy-intercept-v8.tar.zip
tar -xf lineaje-deploy-intercept-v8.tar
cd lineaje-deploy-intercept-v8
```

The bundle can include `run_on_vm.sh`. The script loads the bundled image, starts configured services, and waits up to 150 seconds for `GET /health` to return HTTP 200. It prints container logs and exits with an error when health validation fails.

Configure and protect the environment file:

[Download Hybrid Environment Configuration (v8)](https://downloads.prod.veedna.com/Wipro/INTERCEPT/v8_folder/lineaje.env.zip)

```bash
unzip -o lineaje.env.zip
chmod 600 .env.azure
nano .env.azure
```

| Setting                    | Value / guidance                                                                     |
| -------------------------- | ------------------------------------------------------------------------------------ |
| `UNIFAI_MODE`              | `hybrid`                                                                             |
| `LLM_API_URL`              | Customer's Azure AI Foundry project endpoint                                         |
| `LLM_API_VERSION`          | `2025-05-01`                                                                         |
| `AZURE_OPENAI_API_VERSION` | `2025-05-01`                                                                         |
| `UNIFAI_EVAL_MODEL`        | Customer's Foundry deployment name                                                   |
| `UNIFAI_REMED_MODEL`       | Customer's Foundry deployment name used for LLM remediation (for example, `gpt-5.4`) |
| `UNIFAI_DETECTION_MODEL`   | Customer's Foundry deployment name used for detection (for example, `gpt-5.4`)       |
| `SITE_ADDRESS`             | `localhost, 127.0.0.1, <VM_PUBLIC_IP>`                                               |
| `DEFAULT_SNI`              | `<VM_PUBLIC_IP>` (TLS SNI for Caddy when clients connect by IP)                      |

The three model settings require the Foundry deployment name. Do not use a model version or date.

To move the deployment to another VM, replace the VM IP in `SITE_ADDRESS` and `DEFAULT_SNI`, and in the MCP server URL used by the SCM scanner.

{% hint style="warning" %}
`LLM_API_VERSION` and `AZURE_OPENAI_API_VERSION` are required. Without them, Foundry requests can fail and scans may report 0 violations.
{% endhint %}

Treat the completed `.env.azure` file as a secret. Never commit, email, or share it in screenshots.

#### Scan, harness, and agent settings

The v8 `.env.azure` adds these settings. The defaults below are the shipped values.

| Setting                                   | Default                                 | Purpose                                                                                                    |
| ----------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `UNIFAI_FORCE_S3_ARCHIVE_UPLOAD`          | `false`                                 | Hybrid mode: scan archives stay on the VM and are not pushed to S3                                         |
| `LINEAJE_ENABLE_GUARDRAIL_STUB_INSERTION` | `1`                                     | Inserts guardrail stubs that call GR `/enforce` in the `combined` container. `0` turns it off              |
| `BLOB_MAX_ATTEMPTS`                       | `3`                                     | Maximum retries for blob polling                                                                           |
| `UNIFAI_HARNESS`                          | `opencode`                              | Runs scans with the OpenCode agentic harness                                                               |
| `UNIFAI_PIPELINE_MODE`                    | `true`                                  | Required by the harness                                                                                    |
| `UNIFAI_HARNESS_API_KEY`                  | empty                                   | Not used. The harness runs on Azure Foundry with the managed identity. Never commit a key                  |
| `UNIFAI_HARNESS_TIMEOUT`                  | `1800`                                  | Seconds allowed for one harness scan                                                                       |
| `UNIFAI_SCAN_DIR`                         | `/scan`                                 | Per-scan working directories: `<UNIFAI_SCAN_DIR>/<sbom_id>-<unique_id>/`                                   |
| `UNIFAI_SCAN_KEEP`                        | `0`                                     | `0` deletes the scan directory after a successful scan (failed scans are kept). `1` keeps it for debugging |
| `UNIFAI_AGENT_TIER1_MAX`                  | `64`                                    | Maximum discover/relations subagents. `0` turns agents off                                                 |
| `UNIFAI_INSTRUMENT_AGENT`                 | `1`                                     | Instrument subagent on. `0` turns it off                                                                   |
| `UNIFAI_FINDINGS_INSTRUMENTATION_ROWS`    | `0`                                     | `ai_instrumentation` findings rows off. `1` opts in                                                        |
| `UNIFAI_AGENT_APPLY_REJECTS`              | Commented out (on by default)           | Discover reject verdicts remove or retype the static component. `0` records them only                      |
| `UNIFAI_AGENT_EVIDENCE_BUCKET`            | Commented out                           | Dedicated S3 bucket for `agent_evidence.json.gz`. When unset, the file is kept locally                     |
| `LINEAJE_KEEP_LOCAL_UPLOAD_ARTIFACTS`     | `1`                                     | Keeps a local copy of each scan's result tarballs so the SCM workflow can collect them                     |
| `LINEAJE_LOCAL_UPLOAD_ARTIFACTS_DIR`      | `/home/azureuser/unifai-scans/tarballs` | Where the local result tarballs are written                                                                |

{% hint style="warning" %}
After you change `.env.azure` on a running deployment, recreate the container with `docker compose --env-file .env.azure up -d combined`. `docker compose restart` does not re-read the env file.
{% endhint %}

<details>

<summary>Environment configuration reference</summary>

```bash
# --- Mode ---
UNIFAI_MODE=hybrid
UNIFAI_FORCE_S3_ARCHIVE_UPLOAD=false

# --- Azure AI Foundry ---
UNIFAI_INFERENCE_BACKEND=azure_foundry_gpt
LLM_API_URL=https://<FOUNDRY_RESOURCE>.services.ai.azure.com/api/projects/<FOUNDRY_PROJECT>
LLM_API_VERSION=2025-05-01
AZURE_OPENAI_API_VERSION=2025-05-01

UNIFAI_EVAL_MODEL=<FOUNDRY_DEPLOYMENT_NAME>
UNIFAI_REMED_MODEL=<FOUNDRY_DEPLOYMENT_NAME>
UNIFAI_DETECTION_MODEL=<FOUNDRY_DEPLOYMENT_NAME>

AZURE_IMDS_RESOURCE=https://ai.azure.com

# --- Lineaje SaaS endpoints ---
SCIM_SERVICE_URL=https://scim-service.v2.prod.veedna.com
SCM_SERVICE_URL=https://scm-service.v2.prod.veedna.com
S3_BUCKET=us-east-1-commercialprod-veedna-opa
DATA_SERVICE_BASE_URL=https://data-service-v2-apigw.v2.prod.veedna.com
KEYCLOAK_BASE=https://lineaje-identity-service.v2.prod.veedna.com/lineajeidentity/realms/idp

# --- Server / TLS ---
COMBINED_IMAGE=lineaje-combined:local
COMBINED_HTTP_PORT=8080
SITE_ADDRESS=localhost, 127.0.0.1, <VM_PUBLIC_IP>
DEFAULT_SNI=<VM_PUBLIC_IP>
OAUTH_ENABLED=false

# --- Runtime guardrail library ---
GR_DB_PATH=/data/gr_policies.db
GR_FAIL_MODE=closed
GR_SEED_FILE=/config/seed_demo.yaml
LINEAJE_ENABLE_GUARDRAIL_STUB_INSERTION=1

# --- Agentic harness ---
UNIFAI_HARNESS=opencode
UNIFAI_PIPELINE_MODE=true
UNIFAI_HARNESS_API_KEY=
UNIFAI_HARNESS_TIMEOUT=1800

# --- Scan working directories ---
UNIFAI_SCAN_DIR=/scan
UNIFAI_SCAN_KEEP=0

# --- Subagents ---
UNIFAI_AGENT_TIER1_MAX=64
UNIFAI_INSTRUMENT_AGENT=1
UNIFAI_FINDINGS_INSTRUMENTATION_ROWS=0
# UNIFAI_AGENT_APPLY_REJECTS=0
# UNIFAI_AGENT_EVIDENCE_BUCKET=<S3_BUCKET_NAME>

# --- Local copies of scan result tarballs (used by the SCM workflow) ---
LINEAJE_KEEP_LOCAL_UPLOAD_ARTIFACTS=1
LINEAJE_LOCAL_UPLOAD_ARTIFACTS_DIR=/home/azureuser/unifai-scans/tarballs

# --- Optional: Ema Azure Blob poller ---
# BLOB_ACCOUNT_URL=<BLOB_ACCOUNT_URL>
# BLOB_CONTAINER=<BLOB_CONTAINER>
# BLOB_SAS_TOKEN=<BLOB_SAS_TOKEN>
# BLOB_INCOMING_PREFIX=<INPUT_PATH>/
# BLOB_OUTPUT_PREFIX=<OUTPUT_PATH>/
# BLOB_TENANT_ID=<BLOB_TENANT_ID>
# BLOB_ENVIRONMENT=production
# BLOB_POLL_INTERVAL=300
BLOB_MAX_ATTEMPTS=3
```

Do not set `LINEAJE_POLICY_DIR`; it points the server at demo policies.

The v8 `.env.azure` no longer includes `GR_ADMIN_URL`, `LINEAJE_BEARER_TOKEN`, `LINEAJE_PAT`, `LINEAJE_POLICY_DIR`, `LLM_ANTHROPIC_VERSION`, `SCM_ACCESS_TOKEN`, or the Pay-i keys (`PAYI_API_KEY`, `PAYI_CATEGORY`, `PAYI_TIMEOUT_S`, `PAYI_USECASE_NAME`). Remove them when you carry an older env file forward.

</details>

Start the base runtime:

```bash
bash run_on_vm.sh
```

{% hint style="warning" %}
Always start a newly downloaded release with `run_on_vm.sh`. Running `docker compose up` alone does not load the image included with the release and may start an older image already present on the VM.
{% endhint %}

Only `combined` and `caddy` are required for the standard deployment.

### 7. Validate the deployment

Run the local health check:

```bash
curl -k -i https://localhost/health
```

Expected result: `HTTP 200`.

The response typically includes `status: "ok"` and `routines_registered`. Confirm `routines_registered` is at least `1`.

```bash
curl -sk -o /dev/null -w "%{http_code}\n" https://<VM_PUBLIC_IP>/enforce
```

`404` means the current server version is running. `405` means an older server version is still running.

Opening `https://<VM_PUBLIC_IP>/mcp` in a browser may return `405 Method Not Allowed`. A normal `curl` request can do the same. This is expected because the MCP endpoint accepts scanner requests, not browser `GET` requests.

From an approved network:

```bash
curl -i https://<FQDN_OR_VM_IP>/health
```

Use the FQDN and normal certificate validation in production.

Review runtime logs:

```bash
docker compose --env-file .env.azure logs --tail=100 combined caddy
```

Resolve crash loops, port bind failures, authentication failures, and upstream connectivity errors before continuing.

Deployment is successfully verified when `/health` reports healthy and `/enforce` returns `404`.

### Next Steps

After you deploy the Hybrid UnifAI MCP Server and verify its health, choose the SCM platform to connect:

1. Deploy Hybrid UnifAI MCP Server.
2. Verify server health.
3. Choose [GitHub Actions](/unifai/setting-up/scm-integrations/github-actions.md) or [Azure Pipelines in Azure DevOps](/unifai/setting-up/scm-integrations/azure-pipelines-in-azure-devops.md).
4. Configure the selected pipeline to use the Hybrid MCP Server.
5. Run the scan.

The SCM pages contain the platform-specific configuration. Do not duplicate it here.

#### Smoke-test credentials

Use a valid Lineaje credential for authenticated checks:

* A Lineaje access token from **Settings** → **API**.
* A Lineaje personal access token, when supported by the deployment.

Replace `https://localhost` with the deployed FQDN or public IP for remote tests.

### 8. Connect an MCP client

Use the Streamable HTTP endpoint:

```
https://<FQDN>/mcp
```

#### Claude Code

Add this to `~/.claude/settings.json` or `.claude/settings.json`:

```json
{
  "mcpServers": {
    "unifai": {
      "type": "http",
      "url": "https://<FQDN>/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

Verify with:

```bash
claude mcp list
```

#### Cursor

Create `.cursor/mcp.json` or `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "unifai": {
      "url": "https://<FQDN>/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

#### VS Code

Create `.vscode/mcp.json`:

```json
{
  "servers": {
    "unifai": {
      "type": "http",
      "url": "https://<FQDN>/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

Use supported secret storage or environment-variable substitution. Never commit bearer tokens.

### Optional Ema integration

Ema is not required for MCP deployment. Enable it only for runtime-event ingestion.

Configure Azure Blob Storage and provide:

```bash
BLOB_ACCOUNT_URL=<AZURE_BLOB_ACCOUNT_URL>
BLOB_CONTAINER=<CONTAINER_NAME>
BLOB_INCOMING_PREFIX=<INPUT_PATH>/
BLOB_OUTPUT_PREFIX=<OUTPUT_PATH>/
BLOB_SAS_TOKEN=<SAS_TOKEN>
```

Do not include a leading `?` when the deployment expects a raw SAS token.

`blob-poller` reads incoming runtime logs, submits them to UnifAI, and writes `violations.json` to the configured output path.

```bash
docker compose --env-file .env.azure up -d blob-poller
docker compose --env-file .env.azure logs -f blob-poller
```

Confirm incoming objects are detected, processed, and written to the expected output path.

### Optional Pay-i integration

Pay-i is not required for UnifAI deployment or MCP functionality.

{% hint style="info" %}
The v8 `.env.azure` no longer ships with Pay-i keys. Add them to `.env.azure` yourself only if you use Pay-i.
{% endhint %}

Configure it only for FinOps reporting and model-cost attribution:

```bash
PAYI_API_KEY=<PAYI_API_KEY>
PAYI_CATEGORY=<CATEGORY>
PAYI_RESOURCE=<RESOURCE>
PAYI_USECASE_NAME=<USE_CASE_NAME>
PAYI_TIMEOUT_S=<TIMEOUT>
```

`PAYI_CATEGORY` identifies the configured model or provider category. `PAYI_RESOURCE` adds an optional attribution identifier. `PAYI_USECASE_NAME` groups UnifAI activity under a logical use case. `PAYI_TIMEOUT_S` sets the reporting-request timeout.

Leave `PAYI_API_KEY` unset or empty to disable the integration.

### 10. Operations

#### Check status and logs

```bash
docker compose --env-file .env.azure ps
docker compose --env-file .env.azure logs -f combined caddy
```

If Ema is enabled:

```bash
docker compose --env-file .env.azure logs -f blob-poller
```

#### Restart, stop, and start

```bash
docker compose --env-file .env.azure restart combined caddy
docker compose --env-file .env.azure stop combined caddy
docker compose --env-file .env.azure start combined caddy
```

Restart `blob-poller` separately when Ema is enabled.

`restart` does not re-read `.env.azure`. After an env change, recreate the container instead:

```bash
docker compose --env-file .env.azure up -d combined
```

After restarts, validate `/health` and repeat MCP tool discovery.

#### Upgrade and rollback

For upgrades, retain the prior bundle, back up `.env.azure`, compare configuration keys, then validate health and MCP access.

For rollback, use the prior release directory:

```bash
docker compose --env-file .env.azure up -d combined caddy
curl -k -i https://localhost/health
```

Rollback succeeds when `/health` returns `HTTP 200` and MCP authentication and tool discovery work again.

Docker Compose prefixes volume names with the directory where it runs. Check the actual volume name before investigating rollback state:

```bash
docker volume ls | grep gr_data
```

### Troubleshooting

| Symptom                                                          | Likely cause                                            | Corrective action                                                                                                                                                  |
| ---------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Cannot reach `443`                                               | Compose status, NSG, firewall, Caddy listener           | Start Caddy and correct routing rules                                                                                                                              |
| `/health` fails locally                                          | Logs, container health, image architecture, environment | Resolve the application or configuration issue                                                                                                                     |
| TLS error                                                        | `SITE_ADDRESS`, DNS, Caddy logs                         | Correct DNS or certificate configuration                                                                                                                           |
| MCP `401` or `403`                                               | Token, identity reachability, system clock              | Use a valid credential                                                                                                                                             |
| Foundry authorization fails                                      | VM identity, role, scope                                | Assign the correct inference role                                                                                                                                  |
| Container restarts                                               | Docker logs, memory, disk, environment                  | Resolve the first fatal error                                                                                                                                      |
| Scan completes in roughly 15–20 seconds and reports 0 violations | Required Foundry API-version settings are missing       | Add `LLM_API_VERSION=2025-05-01` and `AZURE_OPENAI_API_VERSION=2025-05-01`, then recreate the container with `docker compose --env-file .env.azure up -d combined` |
| `.env.azure` changes have no effect                              | The container was restarted, not recreated              | Run `docker compose --env-file .env.azure up -d combined`                                                                                                          |
| `stubs_only` is reported and no remediation PR is created        | An older server image or version is running             | Run `bash run_on_vm.sh` again from the new v8 release folder, then verify that `/enforce` returns `404`                                                            |

### Useful diagnostics

```bash
docker compose --env-file .env.azure ps
docker compose --env-file .env.azure logs --tail=200 combined caddy
docker inspect $(docker compose --env-file .env.azure ps -q combined)
ss -lntp | grep -E '(:443|:8080)'
df -h
free -h
```

### Appendix: Optional Azure Container Registry delivery

Use ACR only when the deployment pulls the application image.

Build and push the release image from a machine with Docker, Azure CLI access, and permission to push to the registry:

```bash
ACR_NAME=<ACR_NAME> IMAGE_TAG=<RELEASE_TAG> deploy/build_and_push.sh
```

The resulting image reference follows this format:

```
<acr-name>.azurecr.io/lineaje-combined:<release-tag>
```

From the VM, authenticate, pull the selected release, and start the core runtime:

```bash
az acr login --name <ACR_NAME>
docker pull <acr-name>.azurecr.io/lineaje-combined:<release-tag>
docker compose --env-file .env.azure up -d combined caddy
```

For air-gapped delivery, build and export the image before transferring it to the VM:

```bash
docker build -t lineaje-combined:<release-tag> .
docker save lineaje-combined:<release-tag> | gzip > lineaje-combined.tar.gz
```

Use immutable release tags or image digests in production.

### Deployment acceptance checklist

* [ ] VM sizing and architecture meet requirements.
* [ ] Inbound `443` is restricted to approved networks.
* [ ] Port `8080` is not externally exposed.
* [ ] Managed identity has the required Foundry role.
* [ ] Required platform and Foundry endpoints are reachable.
* [ ] `.env.azure` is protected.
* [ ] `combined` and `caddy` are running.
* [ ] Local and remote `/health` checks succeed.
* [ ] An MCP client authenticates and discovers tools.
* [ ] The current and rollback releases are recorded.
* [ ] Optional integrations are validated when enabled.

{% hint style="success" %}
Deployment is complete when UnifAI is healthy, HTTPS works, and an approved MCP client discovers tools through `/mcp`.
{% endhint %}

### GitHub workflow

Save this workflow as `.github/workflows/lineaje-unifai-scan.yaml` and place `scm_unifai_scan_hybrid.py` in the repository's `.github/` folder. The workflow runs on a self-hosted runner on the UnifAI VM.

[Download the GitHub workflow (v8)](https://downloads.prod.veedna.com/Wipro/INTERCEPT/v8_folder/lineaje-unifai-scan.yaml)

[Download the scanner script scm\_unifai\_scan\_hybrid.py (v8)](https://downloads.prod.veedna.com/Wipro/INTERCEPT/v8_folder/scm_unifai_scan_hybrid.py)

What changed in v8:

* The scan step passes `--pr-mode direct` explicitly and sets `UNIFAI_SCAN_ARCHIVE_DIR`, so the scanned source is archived in a per-run folder on the VM.
* A new **Prepare scan output folder** step creates that folder at `/home/azureuser/unifai-scans/<repo>/<branch>-<short-sha>-<run-id>/`.
* A new **Collect result tarballs** step runs even when the scan fails. It copies the run's `entities.aientity` and `findings.aifinding` tarballs into the run folder. This needs `LINEAJE_KEEP_LOCAL_UPLOAD_ARTIFACTS=1` and `LINEAJE_LOCAL_UPLOAD_ARTIFACTS_DIR=/home/azureuser/unifai-scans/tarballs` on the server.
* `--mcp-server-url https://localhost/mcp` works because the runner is on the VM. If the runner is on a different host, use `https://<VM_PUBLIC_IP>/mcp`.

```yaml
name: Lineaje UnifAI Scan

on:
  workflow_dispatch:
    inputs:
      create_fix_pr:
        description: "Open a remediation PR with the code suggestions and intercepts"
        type: boolean
        default: true

permissions:
  contents: write
  pull-requests: write

jobs:
  lineaje-scan:
    runs-on: self-hosted

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install mcp==1.27.1 httpx

      - name: Prepare scanner
        run: |
          cp .github/scm_unifai_scan_hybrid.py /tmp/scm_unifai_scan_hybrid.py
          rm -rf .github .git

      - name: Prepare scan output folder
        run: |
          # Built from the runner's GITHUB_* variables: no context expressions inside run:.
          RUN_DIR="/home/azureuser/unifai-scans/${GITHUB_REPOSITORY}/${GITHUB_REF_NAME}-${GITHUB_SHA::7}-${GITHUB_RUN_ID}"
          mkdir -p "$RUN_DIR/source" "$RUN_DIR/tarballs"
          echo "UNIFAI_RUN_DIR=$RUN_DIR" >> "$GITHUB_ENV"
          touch "$RUN_DIR/.started"

      - name: Run Lineaje AI policy scan
        env:
          LINEAJE_PAT_TOKEN: ${{ secrets.LINEAJE_PAT_TOKEN }}
          GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
          MCP_TLS_INSECURE_SKIP_VERIFY: "1"
          UNIFAI_SCAN_ARCHIVE_DIR: ${{ env.UNIFAI_RUN_DIR }}/source
        run: |
          python /tmp/scm_unifai_scan_hybrid.py \
            --source-path . \
            --repo ${{ github.repository }} \
            --branch ${{ github.ref_name }} \
            --head-sha ${{ github.sha }} \
            --mcp-server-url https://localhost/mcp \
            --pr-mode direct \
            ${{ inputs.create_fix_pr && '--create-fix-pr' || '' }} >> "$GITHUB_STEP_SUMMARY"

      - name: Collect result tarballs
        if: always()
        run: |
          SRC=/home/azureuser/unifai-scans/tarballs
          if [ -d "$SRC" ]; then
            (cd "$SRC" && find . -type f -name '*.tar.gz' -newer "$UNIFAI_RUN_DIR/.started" \
              -exec cp --parents {} "$UNIFAI_RUN_DIR/tarballs/" \;) || true
          fi
          echo "Scan output on the VM: $UNIFAI_RUN_DIR"
          find "$UNIFAI_RUN_DIR" -type f ! -name .started -printf '  %P  (%s bytes)\n'
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.veedna.com/unifai/setting-up/deploy-unifai-on-azure.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
