From d326b9125d75eb5a9c2d9f21664447f93ad74f46 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Fri, 24 Jul 2026 21:43:33 +0200 Subject: [PATCH 1/2] fix(railway): expose runner on the private network --- hosting/railway/oss/runner/Dockerfile | 1 + hosting/railway/oss/scripts/configure.sh | 1 + hosting/railway/oss/scripts/deploy-from-images.sh | 1 + 3 files changed, 3 insertions(+) diff --git a/hosting/railway/oss/runner/Dockerfile b/hosting/railway/oss/runner/Dockerfile index 65f5858699..7fe61d0965 100644 --- a/hosting/railway/oss/runner/Dockerfile +++ b/hosting/railway/oss/runner/Dockerfile @@ -3,6 +3,7 @@ FROM ghcr.io/agenta-ai/agenta-runner:latest +ENV AGENTA_RUNNER_HOST=0.0.0.0 ENV AGENTA_RUNNER_PORT=8765 CMD ["node_modules/.bin/tsx", "src/server.ts"] diff --git a/hosting/railway/oss/scripts/configure.sh b/hosting/railway/oss/scripts/configure.sh index b8a6ffeec8..b064673de4 100755 --- a/hosting/railway/oss/scripts/configure.sh +++ b/hosting/railway/oss/scripts/configure.sh @@ -365,6 +365,7 @@ main() { "DAYTONA_API_KEY=${DAYTONA_API_KEY:-}" set_vars runner \ + AGENTA_RUNNER_HOST=0.0.0.0 \ AGENTA_RUNNER_PORT=8765 \ AGENTA_RUNNER_TOKEN="$AGENTA_RUNNER_TOKEN" \ AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS="${AGENTA_RUNNER_ENABLED_SANDBOX_PROVIDERS:-local}" \ diff --git a/hosting/railway/oss/scripts/deploy-from-images.sh b/hosting/railway/oss/scripts/deploy-from-images.sh index 301908a3aa..7cfea41f37 100755 --- a/hosting/railway/oss/scripts/deploy-from-images.sh +++ b/hosting/railway/oss/scripts/deploy-from-images.sh @@ -153,6 +153,7 @@ render_runner_wrapper() { cat > "$dir/Dockerfile" < Date: Sat, 25 Jul 2026 00:40:16 +0200 Subject: [PATCH 2/2] fix(railway): persist mount storage credentials --- .github/workflows/44-railway-tests.yml | 1 + .../mounts/test_agent_mounts_basics.py | 9 ++-- .../acceptance/mounts/test_mounts_basics.py | 19 +++----- .../mounts/test_mounts_injection.py | 22 ++++----- api/oss/tests/pytest/utils/mounts.py | 13 +++++ hosting/railway/oss/README.md | 5 ++ hosting/railway/oss/scripts/configure.sh | 48 +++++++++++++++++-- 7 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 api/oss/tests/pytest/utils/mounts.py diff --git a/.github/workflows/44-railway-tests.yml b/.github/workflows/44-railway-tests.yml index eb8d978a56..e1d2b9388e 100644 --- a/.github/workflows/44-railway-tests.yml +++ b/.github/workflows/44-railway-tests.yml @@ -377,6 +377,7 @@ jobs: AGENTA_LICENSE: oss AGENTA_API_URL: ${{ needs.prepare.outputs.agenta_api_url }} AGENTA_AUTH_KEY: ${{ secrets.AGENTA_TEST_OSS_AUTH_KEY }} + AGENTA_TEST_REQUIRE_MOUNT_STORAGE: "true" steps: - uses: actions/checkout@v6 diff --git a/api/oss/tests/pytest/acceptance/mounts/test_agent_mounts_basics.py b/api/oss/tests/pytest/acceptance/mounts/test_agent_mounts_basics.py index 1d8ad24c04..25685e85e7 100644 --- a/api/oss/tests/pytest/acceptance/mounts/test_agent_mounts_basics.py +++ b/api/oss/tests/pytest/acceptance/mounts/test_agent_mounts_basics.py @@ -1,14 +1,14 @@ """Acceptance tests for artifact-scoped agent mounts. -Query-only tests run without an object store. Credential-signing tests skip when -the running API has no mount storage backend configured. Sign verifies the +Query-only tests run without an object store. Credential-signing tests fail when +the environment requires mount storage and skip otherwise. Sign verifies the artifact exists in the project (404 otherwise), so signing tests create a real workflow first. """ from uuid import uuid4 -import pytest +from oss.tests.pytest.utils.mounts import skip_if_mount_storage_unavailable def _create_workflow(authed_api): @@ -35,8 +35,7 @@ def _sign_agent_mount(authed_api, artifact_id, *, name="default"): "/mounts/agents/sign", params={"artifact_id": artifact_id, "name": name}, ) - if response.status_code == 503: - pytest.skip("Mount storage backend not configured in this environment") + skip_if_mount_storage_unavailable(response) return response diff --git a/api/oss/tests/pytest/acceptance/mounts/test_mounts_basics.py b/api/oss/tests/pytest/acceptance/mounts/test_mounts_basics.py index e446dbb032..1a31870562 100644 --- a/api/oss/tests/pytest/acceptance/mounts/test_mounts_basics.py +++ b/api/oss/tests/pytest/acceptance/mounts/test_mounts_basics.py @@ -6,6 +6,8 @@ from uuid import uuid4 +from oss.tests.pytest.utils.mounts import skip_if_mount_storage_unavailable + def _mount_payload(*, name=None, session_id=None): # Storage location is derived server-side (bucket from env, key = project_id/mount_id); @@ -231,8 +233,8 @@ def test_session_query_does_not_return_other_session_mounts(self, authed_api): # File ops (durable store contents) # # Require a configured object store (AGENTA_STORE_* → SeaweedFS in the dev -# stack). A 503 means the backend isn't configured in this env — skip rather -# than fail so the suite stays green where no store is wired. +# stack). Environments that require mount storage fail on a 503; other +# environments skip these checks. # --------------------------------------------------------------------------- @@ -243,13 +245,6 @@ def _create_mount(authed_api): return resp.json()["mount"]["id"] -def _skip_if_no_store(resp): - import pytest - - if resp.status_code == 503: - pytest.skip("Mount storage backend not configured in this environment") - - class TestMountFileOps: def test_write_read_list_delete_roundtrip(self, authed_api): mount_id = _create_mount(authed_api) @@ -260,7 +255,7 @@ def test_write_read_list_delete_roundtrip(self, authed_api): params={"path": "notes.txt"}, data=b"hello world", ) - _skip_if_no_store(write) + skip_if_mount_storage_unavailable(write) assert write.status_code == 200, write.text assert write.json()["path"] == "notes.txt" @@ -291,7 +286,7 @@ def test_create_folder_upload_read_cascade_delete(self, authed_api): folder = authed_api( "POST", f"/mounts/{mount_id}/files/folder", params={"path": "workspace"} ) - _skip_if_no_store(folder) + skip_if_mount_storage_unavailable(folder) assert folder.status_code == 200, folder.text assert folder.json()["path"] == "workspace" @@ -342,7 +337,7 @@ def test_upload_falls_back_to_filename(self, authed_api): f"/mounts/{mount_id}/files/upload", files={"file": ("report.txt", b"content", "text/plain")}, ) - _skip_if_no_store(upload) + skip_if_mount_storage_unavailable(upload) assert upload.status_code == 200, upload.text assert upload.json()["path"] == "report.txt" diff --git a/api/oss/tests/pytest/acceptance/mounts/test_mounts_injection.py b/api/oss/tests/pytest/acceptance/mounts/test_mounts_injection.py index 4159478dc9..ff4976a7d3 100644 --- a/api/oss/tests/pytest/acceptance/mounts/test_mounts_injection.py +++ b/api/oss/tests/pytest/acceptance/mounts/test_mounts_injection.py @@ -10,7 +10,8 @@ (the cross-turn persistence contract, exercised at the store level). Requires a configured object store with a working STS endpoint (AGENTA_STORE_* → -SeaweedFS in the dev stack). A 503 means no store / STS — skip rather than fail. +SeaweedFS in the dev stack). Environments that require mount storage fail on a 503; +other environments skip these checks. The full runner round-trip (geesefs mount inside a live run, file visible across two turns) is a manual/live check on the dev stack; see docs/designs/sessions/mounts/extend/tasks.md M10. @@ -18,12 +19,7 @@ from uuid import uuid4 -import pytest - - -def _skip_if_no_store(resp): - if resp.status_code == 503: - pytest.skip("Mount storage / STS backend not configured in this environment") +from oss.tests.pytest.utils.mounts import skip_if_mount_storage_unavailable def _sign_session(authed_api, session_id): @@ -36,7 +32,7 @@ class TestSessionMountSign: def test_bind_and_sign_returns_scoped_credentials(self, authed_api): session_id = f"session-{uuid4().hex[:8]}" resp = _sign_session(authed_api, session_id) - _skip_if_no_store(resp) + skip_if_mount_storage_unavailable(resp) assert resp.status_code == 200, resp.text body = resp.json() @@ -59,7 +55,7 @@ def test_bind_and_sign_returns_scoped_credentials(self, authed_api): def test_bind_is_idempotent_same_session_same_mount(self, authed_api): session_id = f"session-{uuid4().hex[:8]}" first = _sign_session(authed_api, session_id) - _skip_if_no_store(first) + skip_if_mount_storage_unavailable(first) assert first.status_code == 200, first.text second = _sign_session(authed_api, session_id) @@ -74,7 +70,7 @@ def test_bind_is_idempotent_same_session_same_mount(self, authed_api): def test_different_sessions_get_different_prefixes(self, authed_api): a = _sign_session(authed_api, f"session-{uuid4().hex[:8]}") - _skip_if_no_store(a) + skip_if_mount_storage_unavailable(a) assert a.status_code == 200, a.text b = _sign_session(authed_api, f"session-{uuid4().hex[:8]}") assert b.status_code == 200, b.text @@ -103,7 +99,7 @@ def test_sign_existing_mount(self, authed_api): mount = create.json()["mount"] resp = authed_api("POST", f"/mounts/{mount['id']}/sign") - _skip_if_no_store(resp) + skip_if_mount_storage_unavailable(resp) assert resp.status_code == 200, resp.text creds = resp.json()["credentials"] @@ -120,7 +116,7 @@ class TestDurablePrefixSurvivesRebind: def test_file_survives_rebind(self, authed_api): session_id = f"session-{uuid4().hex[:8]}" first = _sign_session(authed_api, session_id) - _skip_if_no_store(first) + skip_if_mount_storage_unavailable(first) assert first.status_code == 200, first.text mount_id = first.json()["mount"]["id"] @@ -131,7 +127,7 @@ def test_file_survives_rebind(self, authed_api): params={"path": "turn1.txt"}, data=b"written in turn 1", ) - _skip_if_no_store(write) + skip_if_mount_storage_unavailable(write) assert write.status_code == 200, write.text # Re-bind (a fresh "turn"): same mount, and the file is still readable. diff --git a/api/oss/tests/pytest/utils/mounts.py b/api/oss/tests/pytest/utils/mounts.py new file mode 100644 index 0000000000..9e19a61733 --- /dev/null +++ b/api/oss/tests/pytest/utils/mounts.py @@ -0,0 +1,13 @@ +import os + +import pytest + + +def skip_if_mount_storage_unavailable(response): + if response.status_code != 503: + return + + if os.getenv("AGENTA_TEST_REQUIRE_MOUNT_STORAGE", "").lower() == "true": + pytest.fail(f"Mount storage is required but unavailable: {response.text}") + + pytest.skip("Mount storage backend not configured in this environment") diff --git a/hosting/railway/oss/README.md b/hosting/railway/oss/README.md index 7f6f299c62..6f28427773 100644 --- a/hosting/railway/oss/README.md +++ b/hosting/railway/oss/README.md @@ -60,6 +60,11 @@ export AGENTA_RUNNER_TOKEN="$(openssl rand -hex 32)" export POSTGRES_PASSWORD="$(openssl rand -hex 24)" ``` +`configure.sh` creates the bundled mount store credentials on a fresh Railway +project and reuses them on later deployments. You can provide +`AGENTA_STORE_ACCESS_KEY`, `AGENTA_STORE_SECRET_KEY`, +`AGENTA_STORE_SIGNING_KEY`, and `AGENTA_STORE_JWT_PRIVATE_KEY` to override them. + ## Quick Start ```bash diff --git a/hosting/railway/oss/scripts/configure.sh b/hosting/railway/oss/scripts/configure.sh index b064673de4..adef504689 100755 --- a/hosting/railway/oss/scripts/configure.sh +++ b/hosting/railway/oss/scripts/configure.sh @@ -20,10 +20,10 @@ POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" AGENTA_STORE_ACCESS_KEY="${AGENTA_STORE_ACCESS_KEY:-}" AGENTA_STORE_SECRET_KEY="${AGENTA_STORE_SECRET_KEY:-}" AGENTA_STORE_BUCKET="${AGENTA_STORE_BUCKET:-agenta-store}" -AGENTA_STORE_SIGNING_KEY="${AGENTA_STORE_SIGNING_KEY:-$(openssl rand -base64 32)}" +AGENTA_STORE_SIGNING_KEY="${AGENTA_STORE_SIGNING_KEY:-}" # RSA key the API signs its store web-identity token with; the bundled SeaweedFS verifies it -# against the API's JWKS. Generated once per configure run if unset (single-replica Railway). -AGENTA_STORE_JWT_PRIVATE_KEY="${AGENTA_STORE_JWT_PRIVATE_KEY:-$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null)}" +# against the API's JWKS. +AGENTA_STORE_JWT_PRIVATE_KEY="${AGENTA_STORE_JWT_PRIVATE_KEY:-}" # Populated by resolve_railway_ids() after `railway link`. Used by the GraphQL # variableCollectionUpsert path; left empty -> upsert_service_vars falls back @@ -46,6 +46,44 @@ resolve_postgres_password() { fi } +_store_variable_from_json() { + local variables_json="$1" + local key="$2" + + jq -r --arg key "$key" \ + '.[$key] // empty | select(type == "string" and length > 0)' \ + <<<"$variables_json" +} + +resolve_store_configuration() { + local api_variables + api_variables="$(railway_call variable list --json --service api --environment "$ENV_NAME")" + if ! jq -e 'type == "object"' >/dev/null 2>&1 <<<"$api_variables"; then + printf "Unable to read existing mount storage configuration from the Railway API service.\n" >&2 + return 1 + fi + + if [ -z "$AGENTA_STORE_ACCESS_KEY" ]; then + AGENTA_STORE_ACCESS_KEY="$(_store_variable_from_json "$api_variables" AGENTA_STORE_ACCESS_KEY)" + fi + if [ -z "$AGENTA_STORE_SECRET_KEY" ]; then + AGENTA_STORE_SECRET_KEY="$(_store_variable_from_json "$api_variables" AGENTA_STORE_SECRET_KEY)" + fi + if [ -z "$AGENTA_STORE_SIGNING_KEY" ]; then + AGENTA_STORE_SIGNING_KEY="$(_store_variable_from_json "$api_variables" AGENTA_STORE_SIGNING_KEY)" + fi + if [ -z "$AGENTA_STORE_JWT_PRIVATE_KEY" ]; then + AGENTA_STORE_JWT_PRIVATE_KEY="$(_store_variable_from_json "$api_variables" AGENTA_STORE_JWT_PRIVATE_KEY)" + fi + + AGENTA_STORE_ACCESS_KEY="${AGENTA_STORE_ACCESS_KEY:-$(openssl rand -hex 10)}" + AGENTA_STORE_SECRET_KEY="${AGENTA_STORE_SECRET_KEY:-$(openssl rand -hex 32)}" + AGENTA_STORE_SIGNING_KEY="${AGENTA_STORE_SIGNING_KEY:-$(openssl rand -base64 32)}" + AGENTA_STORE_JWT_PRIVATE_KEY="${AGENTA_STORE_JWT_PRIVATE_KEY:-$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null)}" + + printf "Mount storage credentials are configured for this Railway environment.\n" +} + require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then printf "Missing required command: %s\n" "$1" >&2 @@ -263,6 +301,10 @@ main() { # the project/environment/services exist and are linked). resolve_railway_ids + # The API service is the canonical durable copy. Explicit operator values win; + # otherwise reuse the current values and generate only what a fresh project lacks. + resolve_store_configuration + railway_call domain --service gateway --json >/dev/null 2>&1 || true local public_domain_ref