diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 504e20a..5fdcae3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -327,8 +327,14 @@ jobs: # single-attachment volume: rolling tries to stand up a parallel # machine and errors on the attachment slot. The agent group has # no volume, but strategy is app-wide. + # --ha=false because Fly provisions TWO machines by default for + # a newly-declared process group. For `agent` that is actively + # wrong: two workers drain the same run queue, and POST /start + # is idempotent rather than exclusive, so both would claim the + # same run and we would pay for the LLM call twice. The first + # deploy of the agent group did exactly this. echo "Releasing $IMAGE to sentinel-command" - flyctl deploy --image "$IMAGE" --strategy immediate --yes -a sentinel-command + flyctl deploy --image "$IMAGE" --strategy immediate --ha=false --yes -a sentinel-command echo "--- process groups after release ---" flyctl machines list -a sentinel-command --json \ diff --git a/backend/app/sentinel_agent/config.py b/backend/app/sentinel_agent/config.py index e93f943..1f47e40 100644 --- a/backend/app/sentinel_agent/config.py +++ b/backend/app/sentinel_agent/config.py @@ -12,7 +12,7 @@ the MCP server which org each tool call is on behalf of. """ -from pydantic import field_validator, model_validator +from pydantic import AliasChoices, Field, field_validator, model_validator from pydantic_settings import BaseSettings @@ -80,7 +80,25 @@ class Settings(BaseSettings): # The override header is still sent and is accepted as long as it # names the key's own org. opensentry_mcp_url: str = "" # derived from opensentry_api_base if blank - opensentry_mcp_agent_key: str = "" + # Reads OPENSENTRY_MCP_AGENT_KEY first, then Command Center's own + # SENTINEL_AGENT_MCP_KEY. The second alias exists because the agent + # now runs as a process group of the sentinel-command app and shares + # its environment — without it the hosted agent would need a + # duplicate copy of a secret that is already right there, under a + # different name. + # + # This matters more than tidiness: the first-party deployment uses + # two genuinely DIFFERENT shared secrets (run-queue vs MCP), so the + # self-hosted fallback below would quietly hand the MCP surface the + # wrong key — an agent that fetches work and then fails every tool + # call, which is exactly the failure that fallback was written to + # prevent for self-hosters. + opensentry_mcp_agent_key: str = Field( + "", + validation_alias=AliasChoices( + "OPENSENTRY_MCP_AGENT_KEY", "SENTINEL_AGENT_MCP_KEY" + ), + ) # ── Webhook signature behaviour ────────────────────────────────── # When False, /wakeup skips HMAC verification — useful for local