Skip to content

feat(api): give every run a principal and decide access in one place - #7754

Merged
mmabrouk merged 37 commits into
release/v0.124.0from
feat/m2-run-principal
Oct 24, 2026
Merged

mmabrouk merged 37 commits into
release/v0.124.0from
feat/m2-run-principal

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Context

A run had no identity of its own. Every check and every minted credential used a bare user id: the request's user, or the created_by_id of a stored row. So a schedule kept running as its creator after the creator lost access, an API key's run acted with the user's full reach in every project, an approval resumed under the approver's id, and a deleted key or a removed member could keep using a cached answer. check_action_access and the credential exchange also cached their decisions.

This PR is milestone M2 of the sharing program (design in #7747; stacked on M1, #7748): every run carries an explicit principal, and one service decides access on every call.

What changes for users

  • Revocation is immediate on the API. A deleted API key, a removed member or a lost role is refused at the next checked API request and at the next credential refresh, even with warm caches. The API caches no decision. The services layer keeps an allowed credential exchange for its usual cache TTL (60 seconds, as in M1), so a run started inside that window is refused at its first checked call with 403 access_denied, never a 500.
  • An API key acts only in its own project. PATCH or DELETE /projects/{id} with a key of another project is now 403. Before, the key used its creator's role in the other project. Browser sessions do not change.
  • Workspace and organization management routes with an API key need a key of the workspace's default project. Invite, resend invite, role changes, member removal and workspace update check the workspace's default project, so a key of another project in that workspace is now refused (403). Browser sessions do not change.
  • Schedules and subscriptions run as their principal. A trigger whose principal can no longer run it is paused at its next tick, with a failed delivery that says why (for example "the API key, user or installation this runs as no longer exists or is inactive"). Starting it again rechecks the principal (403 access_denied or 409 missing_principal) and clears the pause reason.
  • Archiving an agent pauses its schedules and subscriptions. Unarchiving does not resume them.
  • Evaluations run as the person or key that started them. A run whose principal lost access fails; a live evaluation closes with the reason in meta.closed_reason.
  • Approvals keep the asking run's identity. The resumed run keeps its principal and sender; the approver is stored on the turn (approved_by).
  • A removed member's agent invoke is refused with one clear 403 access_denied. With a cold services cache the exchange refuses it before the run starts; with a cached exchange the run's first checked call (connection resolution) refuses it. Before, it failed with a 500.

Changes

  • core/access: RunEnvelope (principal, authority, run context, sender, approver), PrincipalRef kinds human, api_key, installation, and AccessService.decide (eligibility, restrictions, capability). The principal record and the membership are cached and invalidated by their writers; decisions are not.
  • The auth middleware sets the request's principal. check_action_access keeps its signature and decides for that principal only: without request access, or for another user, it raises. Code without a request calls check_human or decide.
  • Every run credential carries the envelope, and sign_secret_token requires it. The token's user is the run's acting user, derived from its authority, never an approver or a row's creator. A Secret without a principal is 401 missing_principal.
  • Durable work stores its envelope or principal and rechecks before it runs: queued inputs and continuations, approvals, channel turns, child runs, triggers (target resolved and checked before the delivery is claimed) and evaluation runs. A refusal is a terminal settlement, and the session queue moves on to the next input.
  • Turns record principal_kind, principal_id, sender_*, origin, origin_id, parent_run_id and approved_by. Read DTOs expose the new fields; the TypeScript client is regenerated from an EE spec.
  • The services middleware keeps its credential-exchange cache as in M1 (no extra API round trip per request). A refused run credential during connection resolution now reaches the caller as 403 access_denied with the API's reason, never a 500.
  • An API key request reuses the key row that authentication read as its principal record, and an explicit evaluation start or rerun writes its principal in the UPDATE it already makes, so M2 adds no Postgres statement there.
  • A decision reads the principal record and the membership in one Redis MGET, and later decisions of the same request read nothing (request memo). The EE RBAC entitlement is a 30 second process memo. A warm checked request makes no more Redis round trips than M1.

Before (a trigger tick):

invoke(user_id=schedule.created_by_id)   # the creator's role, forever

After:

envelope = stored_work_envelope(principal=schedule.principal, ...)
access.require(envelope, RUN_WORKFLOWS, resource=agent)   # refused -> failed delivery + pause
claim delivery -> dispatch(envelope)

Docs

Updated in this PR (v0.124.0 ships M1 and M2 together, so the M1 doc changes are here too):

  • Self-host upgrade notes: the section is now "Upgrading to 0.124.0", with API keys confined to their project, automations and evaluations running as their creator, deploying the API, workers and services together, and work cancelled once during the upgrade.
  • Security overview, the API guide, Organizations and RBAC: an API key acts only in its own project; workspace and organization management needs a key of the default project; project create, rename and delete per role; ownership transfer swaps roles.
  • Organizations and RBAC: roles change only through the API for now; the app has no control for it yet.
  • Automations: an automation runs as the person who set it up and is paused when that access ends.
  • Errors: the access_denied agent run error.

Migrations

  • oss000000046: nullable columns only (no default, no foreign key), ADD COLUMN IF NOT EXISTS, and two concurrent indexes on (project_id, artifact_id) of the trigger tables. lock_timeout 5 s. Safe to rerun after an index failure.
  • oss000000047 (data): keyset batches of 1,000 on (project_id, id), 5 s lock and 60 s statement timeouts. Schedules, subscriptions and evaluation runs with a creator get principal = human(created_by_id); live triggers with no creator are paused (missing_principal, ids in the hook log); artifact_id is set for triggers bound to an agent directly (environment-bound triggers keep NULL). Idempotent.
  • Rollout on restored pre-M2 copies of both local editions with queued work in place: both revisions applied (046 about 25 ms, 047 about 50 ms on the seed data); a continuation queued before the deploy is settled missing_envelope on its next resume; a parked approval answered after the deploy returns 409 missing_envelope and is cancelled; a schedule of a deleted creator is paused at its next tick.

Latency

Access-decision statements per request on warm caches (counted by phase with real DAOs; test_run_principal_query_counts_postgres.py, OSS and EE):

Path Access-decision statements Endpoint statements vs an always-allow stub
check_action_access allow and denial 0 0
GET /access/permissions/check (exchange) 0 equal
POST /gateways/credentials 0 0
Trigger tick (real DAOs, real target resolution) 0 equal

A cold principal is one statement, a cold membership one statement.

Request-level latency (p50/p99 against the M1 base, both editions) is measured on the same local stacks; the table follows in a comment.

Tests / notes

  • API unit OSS 8641 passed, EE 9583 passed. Integration OSS 455 passed; EE passes except the 7 wallet Redis tests that cannot reach the stack Redis from the host (7/7 inside the API container). Acceptance OSS 859 passed; EE 899 passed, with 1 to 7 read timeouts on account creation while QA agents loaded the same stack (the failing files pass alone). SDK unit 3559 passed, services unit 226 passed.
  • Count tests pin the warm paths at zero access-decision statements in both editions.
  • Live QA on local OSS and EE stacks in /m and /w: web chat attribution per sender, API keys in and out of their project, run credentials, deleted keys, schedules (fire, deleted creator, deleted key, archive), evaluations (start, rerun, principal rules), approvals from the UI, channel installation turns, a settings sweep, and log checks (no credential strings, ids only in denial lines). QA found seven defects: the session queue stopped after a refused run, the approver was missing on the resumed turn, a removed member's invoke failed with a 500 inside the run, and four small trigger and evaluation messages. All are fixed in this PR with tests.
  • Codex reviewed the diff in four rounds and checked it against the spec; every accepted finding is fixed in this PR.

Release notes

  • An API key acts only in its own project.
  • Workspace and organization management with an API key needs a key of the workspace's default project.
  • Messages queued and approvals parked during the deploy are cancelled once and must be sent or answered again; runs started before the deploy fail once.
  • Schedules and subscriptions without a creator are paused by the migration; schedules whose creator no longer exists are paused at their next run.
  • Archiving an agent pauses its schedules and subscriptions.
  • A schedule, subscription or live evaluation stops when the person or key it runs as loses access.

Human steps

  1. Before the release, run the counting queries in the oss000000047 docstring on a copy of each region's database, then both revisions, and time the backfill. Keep the counts in the private release notes.
  2. Deploy the API, workers and services together; during the rollout a removed member's run on old services fails with the old 500 instead of the new 403.

Merge note: revision ids

The central allocation is M1 044/045, M2 046/047, M3 048/049, M4a 050. Whichever of M2 and a later milestone merges second re-points its first revision to the other's head.

Follow-ups (not in this PR)

  • A removed member's browser session gets 401 for that project (the M1 rule that a non-member session is unauthenticated there). A clear 403 access_denied needs frontend handling.
  • A live subscription event path, a linked channel user and the approver chain were tested in unit and integration tests only.
  • Bind a session heartbeat to the run credential's turn once the runner mints one credential per turn everywhere (CodeRabbit round 2).
  • A role-change control in the app (today roles change only through the API).
  • Spec text corrections from the implementation are listed for the docs PR.

@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says: @coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Summary

Summary by CodeRabbit

  • New Features
    • Authorization now reflects the active user, API key, or installation across workflows, evaluations, sessions, triggers, and gateway credentials.
    • Run credentials carry access context through child workflows, queued work, and resumed sessions; session turns can retain sender and approval details.
    • API keys are limited to their associated project, with organization and workspace management requiring an eligible project key.
  • Bug Fixes
    • Access is rechecked for ongoing and durable work; denied or unverifiable work is refused instead of proceeding.
    • Trigger refusals are recorded, and affected triggers can be paused when access is lost or an agent is archived.
    • Python SDKs preserve structured access-denial details and distinguish these refusals from retryable failures.

This pull request introduces principal-based authorization and run-envelope propagation. It persists principal and attribution data, rechecks access for durable work, updates trigger and channel execution, adds migrations, and extends API and SDK error handling.

Changes: Run principal access and propagation

Layer / File(s) Summary
Principal contracts and access decisions
api/oss/src/core/access/*, api/oss/src/middlewares/auth.py, api/oss/src/apis/fastapi/access/router.py
Adds principal and envelope DTOs, request access context, principal-based decisions, eligibility checks, authority restrictions, cache invalidation, and structured denial errors.
Durable work and execution attribution
api/oss/src/core/evaluations/*, api/oss/src/core/sessions/*, api/oss/src/core/workflows/*, api/oss/src/core/tools/*
Propagates envelopes through evaluations, sessions, tools, and workflow invocation. Refused work is closed, cancelled, or settled without retry.
Triggers, channels, and organization writes
api/oss/src/core/triggers/*, api/oss/src/tasks/asyncio/*, api/oss/src/routers/*, api/ee/src/apis/fastapi/*
Stores trigger principals and artifact targets, rechecks access before dispatch, builds installation envelopes, and adds organization-level write checks.
Persistence, migrations, tests, docs, and SDK
api/oss/src/dbs/postgres/*, api/oss/databases/postgres/migrations/*, api/oss/tests/*, docs/*, sdks/python/*
Adds PostgreSQL columns, mappings, indexes, resumable backfills, access and refusal tests, documentation, and structured SDK access-denial handling.

Priority: High

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AuthMiddleware
  participant AccessService
  participant SessionCommandsService
  participant WorkflowsService
  AuthMiddleware->>AccessService: Build request access from principal and envelope
  SessionCommandsService->>AccessService: Recheck RUN_SESSIONS
  AccessService-->>SessionCommandsService: Allow or AccessDenied
  SessionCommandsService->>WorkflowsService: Invoke with run envelope
  WorkflowsService-->>SessionCommandsService: Execute or report refusal
Loading

Possibly related PRs

  • Agenta-AI/agenta#7747 — Defines the run-principal contracts and centralized authorization behavior implemented across this pull request.
  • Agenta-AI/agenta#7776 — Changes authorization for the EE organization domain and SSO-provider endpoints updated here.
  • Agenta-AI/agenta#7693 — Changes terminal settlement for session continuations, which this pull request also updates for refused continuations.
Merge Risk: Low · up to 40ed2

The access-control rework looks sound. The remaining items are small: a short window after a plan change when role checks stay off, a paused automation that can be turned back on and then refused at its next run, a missing acting user on batch-created evaluation runs, and an incomplete upgrade note about which API-key writes need a default-project key. None blocks merge, but the author should fix or accept them.

Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Docstring Coverage Inconclusive Docstring coverage is 44.30% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 237 functions across 50 files. (166 skipp… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description check Passed The description clearly explains the principal-based access model, centralized access decisions, durable-work rechecks, migrations, user-facing behavior, and validation for the changeset.
Title check Passed The title is concise and accurately states the primary change: assigning principals to runs and centralizing access decisions.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 44.30% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 237 functions across 50 files. (166 skipped: 8 unsupported, 158 over the file limit.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@mmabrouk I couldn't produce a complete response to this request. Please try again.

You are interacting with an AI system.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Website preview

Preview URL: https://pr-7754-agenta-website-preview.mahmoud-637.workers.dev

Built from 40ed26d528455f445ddf9ac740a06382e0544cf0. This comment updates in place on every push.

@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says:

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments: 1
api/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.py: 1

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Restore the docstring text.

A bulk edit changed the module docstring. It now reads "heartbeat(envelope=make_envelope())'s acquire-then-refresh fallback". The original text referred to the plain heartbeat() method. The new wording reads as if the test helper owns the fallback.

Proposed fix
-Before this, `heartbeat(envelope=make_envelope())`'s acquire-then-refresh fallback silently re-acquired a lost alive
+Before this, `heartbeat()`'s acquire-then-refresh fallback silently re-acquired a lost alive

ℹ️ Review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between cac4d3a and af7bfb2.

⛔ Files ignored due to path filters: 24
  • web/packages/agenta-api-client/src/generated/api/resources/access/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/access/client/requests/CheckPermissionsRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/gateway/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/gatewayMcp/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/gatewayMcp/client/requests/McpEndpointCheckRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/AgentError.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/AgentRef.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/EvaluationRun.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/McpCredentialBinding.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/PrincipalKind.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/PrincipalRef.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunApproval.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunAuthority.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunContext.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunEnvelope.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunOrigin.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunRestrictions.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunSender.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionTurn.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/TriggerDeliveryData.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/TriggerSchedule.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/TriggerSubscription.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/index.ts is excluded by !**/generated/**
📒 Files selected for processing: 197
  • api/AGENTS.md
  • api/ee/tests/pytest/acceptance/accounts/test_project_creation_seeding_ee.py
  • api/entrypoints/access.py
  • api/entrypoints/channel_adapters.py
  • api/entrypoints/routers.py
  • api/entrypoints/worker_queues.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000046_add_run_principal_columns.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000047_backfill_run_principal.py
  • api/oss/src/apis/fastapi/access/router.py
  • api/oss/src/apis/fastapi/agent_templates/router.py
  • api/oss/src/apis/fastapi/evaluations/router.py
  • api/oss/src/apis/fastapi/gateways/credentials_router.py
  • api/oss/src/apis/fastapi/gateways/mcps/router.py
  • api/oss/src/apis/fastapi/sessions/router.py
  • api/oss/src/apis/fastapi/tools/router.py
  • api/oss/src/apis/fastapi/triggers/router.py
  • api/oss/src/core/access/context.py
  • api/oss/src/core/access/dtos.py
  • api/oss/src/core/access/interfaces.py
  • api/oss/src/core/access/permissions/service.py
  • api/oss/src/core/access/principals.py
  • api/oss/src/core/access/service.py
  • api/oss/src/core/access/types.py
  • api/oss/src/core/accounts/service.py
  • api/oss/src/core/agent_templates/loader.py
  • api/oss/src/core/channels/queue.py
  • api/oss/src/core/channels/service.py
  • api/oss/src/core/evaluations/interfaces.py
  • api/oss/src/core/evaluations/run_access.py
  • api/oss/src/core/evaluations/runtime/adapters.py
  • api/oss/src/core/evaluations/service.py
  • api/oss/src/core/evaluations/tasks/processor.py
  • api/oss/src/core/evaluations/tasks/run.py
  • api/oss/src/core/evaluations/types.py
  • api/oss/src/core/memberships/dtos.py
  • api/oss/src/core/memberships/service.py
  • api/oss/src/core/sessions/commands/delivery.py
  • api/oss/src/core/sessions/commands/dtos.py
  • api/oss/src/core/sessions/commands/interfaces.py
  • api/oss/src/core/sessions/commands/service.py
  • api/oss/src/core/sessions/inputs/dtos.py
  • api/oss/src/core/sessions/inputs/service.py
  • api/oss/src/core/sessions/interactions/dtos.py
  • api/oss/src/core/sessions/interactions/service.py
  • api/oss/src/core/sessions/starts/service.py
  • api/oss/src/core/sessions/streams/service.py
  • api/oss/src/core/sessions/streams/types.py
  • api/oss/src/core/sessions/turns/dtos.py
  • api/oss/src/core/tools/platform_handlers.py
  • api/oss/src/core/triggers/dtos.py
  • api/oss/src/core/triggers/interfaces.py
  • api/oss/src/core/triggers/service.py
  • api/oss/src/core/workflows/dtos.py
  • api/oss/src/core/workflows/service.py
  • api/oss/src/dbs/http/sessions/control_delivery_direct.py
  • api/oss/src/dbs/postgres/access/__init__.py
  • api/oss/src/dbs/postgres/access/dao.py
  • api/oss/src/dbs/postgres/access/mappings.py
  • api/oss/src/dbs/postgres/evaluations/dao.py
  • api/oss/src/dbs/postgres/evaluations/dbas.py
  • api/oss/src/dbs/postgres/evaluations/mappings.py
  • api/oss/src/dbs/postgres/sessions/commands/dbas.py
  • api/oss/src/dbs/postgres/sessions/commands/mappings.py
  • api/oss/src/dbs/postgres/sessions/inputs/dao.py
  • api/oss/src/dbs/postgres/sessions/inputs/dbes.py
  • api/oss/src/dbs/postgres/sessions/inputs/mappings.py
  • api/oss/src/dbs/postgres/sessions/interactions/dbas.py
  • api/oss/src/dbs/postgres/sessions/interactions/mappings.py
  • api/oss/src/dbs/postgres/sessions/turns/dao.py
  • api/oss/src/dbs/postgres/sessions/turns/dbas.py
  • api/oss/src/dbs/postgres/sessions/turns/mappings.py
  • api/oss/src/dbs/postgres/triggers/dao.py
  • api/oss/src/dbs/postgres/triggers/dbas.py
  • api/oss/src/dbs/postgres/triggers/mappings.py
  • api/oss/src/middlewares/auth.py
  • api/oss/src/routers/projects_router.py
  • api/oss/src/services/api_key_service.py
  • api/oss/src/services/user_service.py
  • api/oss/src/tasks/asyncio/channels/inbox.py
  • api/oss/src/tasks/asyncio/sessions/interactions_dispatcher.py
  • api/oss/src/tasks/asyncio/triggers/dispatcher.py
  • api/oss/src/tasks/taskiq/sessions/interactions_worker.py
  • api/oss/tests/pytest/acceptance/accounts/test_project_route_permissions.py
  • api/oss/tests/pytest/acceptance/sessions/test_run_principal_acceptance.py
  • api/oss/tests/pytest/integration/access/__init__.py
  • api/oss/tests/pytest/integration/access/conftest.py
  • api/oss/tests/pytest/integration/access/test_principals_dao_postgres.py
  • api/oss/tests/pytest/integration/access/test_run_principal_query_counts_postgres.py
  • api/oss/tests/pytest/integration/channels/conftest.py
  • api/oss/tests/pytest/integration/gateways/test_mcp_oauth_connect.py
  • api/oss/tests/pytest/integration/memberships/test_admin_membership_invalidation_postgres.py
  • api/oss/tests/pytest/integration/memberships/test_hot_path_query_counts_postgres.py
  • api/oss/tests/pytest/integration/memberships/test_membership_parity_postgres.py
  • api/oss/tests/pytest/integration/migrations/__init__.py
  • api/oss/tests/pytest/integration/migrations/conftest.py
  • api/oss/tests/pytest/integration/migrations/test_run_principal_backfill_postgres.py
  • api/oss/tests/pytest/integration/migrations/test_run_principal_columns_rerun_postgres.py
  • api/oss/tests/pytest/integration/sessions/test_refused_continuations_postgres.py
  • api/oss/tests/pytest/integration/sessions/test_run_envelope_columns_postgres.py
  • api/oss/tests/pytest/integration/triggers/__init__.py
  • api/oss/tests/pytest/integration/triggers/conftest.py
  • api/oss/tests/pytest/integration/triggers/test_triggers_refuse_postgres.py
  • api/oss/tests/pytest/unit/access/test_access_boundaries.py
  • api/oss/tests/pytest/unit/access/test_access_service.py
  • api/oss/tests/pytest/unit/access/test_acting_user.py
  • api/oss/tests/pytest/unit/access/test_check_action_access_wrapper.py
  • api/oss/tests/pytest/unit/access/test_decide.py
  • api/oss/tests/pytest/unit/access/test_grant_exchange.py
  • api/oss/tests/pytest/unit/access/test_principal_invalidation.py
  • api/oss/tests/pytest/unit/access/test_principals_cache.py
  • api/oss/tests/pytest/unit/access/test_run_principal_dtos.py
  • api/oss/tests/pytest/unit/access/test_run_principal_mappings.py
  • api/oss/tests/pytest/unit/agent_templates/conftest.py
  • api/oss/tests/pytest/unit/agent_templates/test_github_loading.py
  • api/oss/tests/pytest/unit/agent_templates/test_loader.py
  • api/oss/tests/pytest/unit/agent_templates/test_router.py
  • api/oss/tests/pytest/unit/channels/conftest.py
  • api/oss/tests/pytest/unit/channels/test_channels_approval_admission.py
  • api/oss/tests/pytest/unit/channels/test_channels_inbox_dispatcher.py
  • api/oss/tests/pytest/unit/channels/test_channels_inbox_principal.py
  • api/oss/tests/pytest/unit/channels/test_channels_live_qa_regressions.py
  • api/oss/tests/pytest/unit/channels/test_channels_rechecks.py
  • api/oss/tests/pytest/unit/channels/test_channels_turn_context.py
  • api/oss/tests/pytest/unit/evaluations/conftest.py
  • api/oss/tests/pytest/unit/evaluations/test_query_eval_loops.py
  • api/oss/tests/pytest/unit/evaluations/test_run_batch_invocation.py
  • api/oss/tests/pytest/unit/evaluations/test_run_principal.py
  • api/oss/tests/pytest/unit/evaluations/test_run_slice_ops.py
  • api/oss/tests/pytest/unit/evaluations/test_runtime_topology_planner.py
  • api/oss/tests/pytest/unit/gateways/conftest.py
  • api/oss/tests/pytest/unit/gateways/test_gateway_credentials_envelope.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_mcp_oauth_callback_authorization.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_mcp_router.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_plane_flags.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_rollout_switches.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_sandbox_credential.py
  • api/oss/tests/pytest/unit/memberships/test_membership_cache.py
  • api/oss/tests/pytest/unit/memberships/test_memberships_invalidation.py
  • api/oss/tests/pytest/unit/middlewares/test_auth_grants.py
  • api/oss/tests/pytest/unit/middlewares/test_auth_run_principal.py
  • api/oss/tests/pytest/unit/middlewares/test_auth_secret_token.py
  • api/oss/tests/pytest/unit/migrations/test_migration_gate.py
  • api/oss/tests/pytest/unit/sessions/conftest.py
  • api/oss/tests/pytest/unit/sessions/test_cancel_stop_guard.py
  • api/oss/tests/pytest/unit/sessions/test_command_claim_unknown_kind.py
  • api/oss/tests/pytest/unit/sessions/test_command_settle_unknown_kind.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_first_touch_race.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_lock_races.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_mirrors_existing_row.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_parked_zombie.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_refused_turn_final_beat.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_replica_id_control_chars.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_run_credential.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_runner_token.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_stale_turn_end.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_turn_binding.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_turn_handover.py
  • api/oss/tests/pytest/unit/sessions/test_inputs_run_envelope.py
  • api/oss/tests/pytest/unit/sessions/test_interaction_continuation_admission.py
  • api/oss/tests/pytest/unit/sessions/test_interactions_dispatcher.py
  • api/oss/tests/pytest/unit/sessions/test_live_relay.py
  • api/oss/tests/pytest/unit/sessions/test_pending_inputs_service.py
  • api/oss/tests/pytest/unit/sessions/test_respond_interaction_durable.py
  • api/oss/tests/pytest/unit/sessions/test_session_commands_dao.py
  • api/oss/tests/pytest/unit/sessions/test_session_inputs_dao.py
  • api/oss/tests/pytest/unit/sessions/test_session_start_service.py
  • api/oss/tests/pytest/unit/sessions/test_stream_fill_once.py
  • api/oss/tests/pytest/unit/sessions/test_stream_header_merge.py
  • api/oss/tests/pytest/unit/sessions/test_turn_attribution.py
  • api/oss/tests/pytest/unit/sessions/test_watch_interactions_publish.py
  • api/oss/tests/pytest/unit/sessions/test_watch_lifecycle_publish.py
  • api/oss/tests/pytest/unit/sessions/test_watch_session_list_publish.py
  • api/oss/tests/pytest/unit/test_channel_adapters_entrypoint.py
  • api/oss/tests/pytest/unit/tools/conftest.py
  • api/oss/tests/pytest/unit/tools/test_platform_handlers.py
  • api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py
  • api/oss/tests/pytest/unit/triggers/conftest.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_dispatcher.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_lifecycle.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_principal.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_schedules_refresh.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_soft_delete_postgres.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_subscription_test_mode.py
  • api/oss/tests/pytest/unit/vault/test_write_only_routes.py
  • api/oss/tests/pytest/unit/workflows/conftest.py
  • api/oss/tests/pytest/unit/workflows/test_archive_pauses_triggers.py
  • api/oss/tests/pytest/unit/workflows/test_invoke_detached.py
  • api/oss/tests/pytest/unit/workflows/test_prepare_invoke_envelope.py
  • api/oss/tests/pytest/unit/workflows/test_service_url_policy.py
  • api/oss/tests/pytest/utils/cache.py
  • api/oss/tests/pytest/utils/memberships.py
  • api/oss/tests/pytest/utils/principals.py
  • api/oss/tests/pytest/utils/statements.py
  • sdks/python/agenta/sdk/middlewares/routing/auth.py
  • sdks/python/oss/tests/pytest/unit/test_auth_middleware_credentials.py
  • services/oss/tests/pytest/unit/agent/test_credential_exchange.py
💤 Files with no reviewable changes: 1
  • services/oss/tests/pytest/unit/agent/test_credential_exchange.py

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread api/oss/src/core/access/service.py
Comment thread api/oss/src/core/evaluations/interfaces.py
Comment thread api/oss/src/core/sessions/streams/service.py Outdated
Comment thread api/oss/src/core/workflows/service.py
Comment thread api/oss/src/core/workflows/service.py
Comment thread api/oss/src/tasks/asyncio/channels/inbox.py Outdated
Comment thread api/oss/src/tasks/asyncio/triggers/dispatcher.py Outdated
@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says: Round 1: all 7 findings and the nitpick (docstring restored) are fixed in 39ab8ee; threads answered and resolved.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between af7bfb2 and 39ab8ee.

📒 Files selected for processing: 10
  • api/oss/src/core/access/service.py
  • api/oss/src/core/evaluations/interfaces.py
  • api/oss/src/core/sessions/streams/service.py
  • api/oss/src/core/workflows/service.py
  • api/oss/src/tasks/asyncio/channels/inbox.py
  • api/oss/src/tasks/asyncio/triggers/dispatcher.py
  • api/oss/tests/pytest/unit/channels/test_channels_rechecks.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_run_credential.py
  • api/oss/tests/pytest/unit/workflows/test_archive_pauses_triggers.py
🚧 Files skipped from review as they are similar to previous changes: 2
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.py
  • api/oss/src/tasks/asyncio/triggers/dispatcher.py

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread api/oss/src/core/sessions/streams/service.py
@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says: Latency, network round trips and database load after the gate fixes (head b3fe813b41).

What changed since the latency gate failed

The gate measured session send +12 to +20 ms p50 on af7bfb2ed0. The cause was the removed services credential cache: one extra GET /api/access/permissions/check per services request. Fixes in this PR:

  • The services exchange cache is back, with M1's TTL (60 s). A removed member's run that starts with a cached credential is refused at its first checked call with one 403 access_denied (reason passed through), never a 500.
  • A decision reads the principal record and the membership in one Redis MGET. Later decisions in the same request read nothing (request memo). The EE RBAC entitlement is a 30 s process memo.
  • An API key request reuses the key row that authentication already read as its principal record. A cold decision no longer reads api_keys twice.
  • An explicit evaluation start or rerun writes its principal in the activation UPDATE it already makes.
  • oss000000046 stays in one transaction (owner decision), with lock_timeout 5 s and ADD COLUMN IF NOT EXISTS, so a rerun completes.

API calls per session send (deterministic)

These are API access-log lines in the window of 10 sends. Each side is a separate services process of the same stack (same API, same database). "before" runs the services SDK of af7bfb2ed0 (no exchange cache). "after" runs this PR.

Edition Credential Exchange calls, before Exchange calls, after
OSS ApiKey 10 2
OSS Bearer 10 3
EE ApiKey 10 2
EE Bearer 10 8

"before" makes one exchange per send. "after" makes 0 per send on a warm cache, as M1 does. The few calls left on "after" are the cold first call and other traffic on the shared stack (the EE Bearer window had extra background traffic: 324 API lines against about 230 in the other windows). The rest of a send's API calls (about 22) are the same on both sides.

Session send latency (time to response headers)

The runs are interleaved before/after, 3 rounds, N=20 after 5 warm-up sends, ApiKey and Bearer, OSS and EE. Values are the median over the rounds, in ms.

Edition Credential p50 before p50 after p99 before p99 after
OSS ApiKey 148 103 171 168
OSS Bearer 92 115 159 165
EE ApiKey 86 90 144 133
EE Bearer 101 123 217 184

The box load stayed high during the whole run: the 1-minute load average was 2.3 to 58.6, with a median of 15, from other agents' test suites. The rounds vary by up to 60 ms between each other, so these latency numbers cannot resolve a 10 ms effect. The call counts above are the measurement that answers the gate: the extra round trip is gone.

Network round trips added by M2 per request (compared with M1)

Path When Cached M1 M2
services → API GET /access/permissions/check (exchange) per services request yes, 60 s (M1 TTL) 0 warm, 1 cold 0 warm, 1 cold
services → API GET /secrets/ (connection resolution) per turn as before 1 1
runner/sandbox → API POST /gateways/credentials per turn no (as before) 1 1
runner → API heartbeat, session writes, outcomes per beat / per turn as before unchanged unchanged
API → services invoke (triggers, channels, approvals, child runs) per run n/a 1 1
Live stream recheck every 60 s per open stream in-process 0 HTTP 0 HTTP
Trigger tick, channel turn, evaluation worker decisions per run in-process 0 HTTP 0 HTTP
API → Redis, warm checked request per request n/a 1 GET (cached decision) 1 MGET (principal + membership); 0 for later decisions in the request
API → Redis, EE RBAC entitlement per decision n/a 0 (inside the cached decision) 0 (30 s process memo)
API → Redis, POST /gateways/credentials eligibility per turn n/a 0 1 MGET
API → Redis, run-credential mint acting user (_prepare_invoke) per invoke n/a 0 0 in a request (memo); 1 GET in a worker-started run

M2 adds no HTTP call (checked over the full diff). Two Redis reads remain per turn, not per request: the gateway-credential eligibility check and, in worker-started runs, the acting-user read. Both are cached reads with no Postgres statement.

Database load (statements per request and per turn)

Measured by the DB-load check on pinned base f8e8fca4da, M1 cac4d3a05c and M2 855bc3df9b. R/W = read/write statements.

Path base R/W M1 R/W M2 R/W
Agent turn, warm, ApiKey (every API call of a turn) 16/8 16/8 16/8
Agent turn, cold, ApiKey 64/8 24/8 23/8
Heartbeat, per beat, warm / cold 3/1 / 13/1 3/1 / 5/1 3/1 / 6/1
Run-token exchange, warm / cold (ApiKey) 0/0 / 12/0 0/0 / 4/0 0/0 / 5/0
Workflow query, warm / cold (ApiKey) 1/0 / 13/0 1/0 / 5/0 1/0 / 6/0
Gateway credentials, warm / cold (ApiKey) 0/0 / 3/0 0/0 / 3/0 0/0 / 5/0
Trigger tick in the worker, warm 10/2 10/2 10/2

All warm paths are equal in base, M1 and M2. The extra cold reads in the M2 column (+1 on cold ApiKey decisions, +2 on cold gateway credentials) come from the repeated api_keys read. This PR removes it after the measured commit, so cold ApiKey decisions are expected to cost the same as M1. The explicit evaluation start no longer adds an UPDATE.

Two items are recorded and not changed:

  • Target lookup before the claim (trigger tick). Moving it after the claim needs a new settlement path for a claimed delivery. Only a tick that loses the claim race pays the extra reads.
  • Wider session_commands rows. The existing settle churn on that table predates M2.

Evidence (local paths): ~/agenta-m2-qa/latency/ (runner, raw results, call counts), qa-m2/DB-LOAD.md, qa-m2/LATENCY.md.

@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says: New commits since round 2 (services cache, 403 mapping, one MGET per decision, request memo, folded evaluation UPDATE).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 39ab8ee and b3fe813.

📒 Files selected for processing: 21
  • api/ee/src/core/access/rbac.py
  • api/oss/src/core/access/context.py
  • api/oss/src/core/access/interfaces.py
  • api/oss/src/core/access/service.py
  • api/oss/src/core/evaluations/service.py
  • api/oss/src/core/memberships/service.py
  • api/oss/src/dbs/postgres/access/dao.py
  • api/oss/src/dbs/postgres/evaluations/dao.py
  • api/oss/src/middlewares/auth.py
  • api/oss/src/utils/caching.py
  • api/oss/tests/pytest/integration/access/test_run_principal_query_counts_postgres.py
  • api/oss/tests/pytest/unit/evaluations/test_run_principal.py
  • api/oss/tests/pytest/unit/middlewares/test_auth_run_principal.py
  • api/oss/tests/pytest/utils/cache.py
  • sdks/python/agenta/sdk/agents/connections/__init__.py
  • sdks/python/agenta/sdk/agents/connections/errors.py
  • sdks/python/agenta/sdk/agents/platform/connections.py
  • sdks/python/agenta/sdk/middlewares/routing/auth.py
  • sdks/python/agenta/sdk/middlewares/running/normalizer.py
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py
  • sdks/python/oss/tests/pytest/unit/test_auth_middleware_credentials.py

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 4 remain after this review.

Comment thread sdks/python/agenta/sdk/agents/connections/errors.py Outdated
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

📘 Docs preview

Status ✅ Ready
Preview https://pr-7754-agenta-docs-preview.mahmoud-637.workers.dev/docs/
Inspect Actions run
Commit 40ed26d528455f445ddf9ac740a06382e0544cf0

Pages changed in this pull request

This comment updates in place on every push.

@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says:

Rebased onto release/v0.124.0 (a81a3ef), on top of the rebased #7748. The base stays feat/m1-membership-resolver. The rebase had no conflicts. The branch now includes the v0.123.1 changes unchanged. API unit tests (OSS and EE) and SDK unit tests pass on the combined tip of the stack.

In SessionTurnsRouter (api/oss/src/apis/fastapi/sessions/router.py), the turn routes keep the runner-token handling of agent_session_id and sandbox_id from v0.123.1, and this PR adds envelope=request_run_envelope(request) next to it. Both are kept.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Item Value
Status stopped
Commit 40ed26d528455f445ddf9ac740a06382e0544cf0
Manual expiry (UTC) Not granted
CI deadline (UTC) Not granted
Workflow https://github.com/Agenta-AI/agenta/actions/runs/40137125292

PR closed or converted to draft. Environment absence verified; retained storage requires separate provider accounting.

Post /preview as a new comment to request a one-hour preview. Repeating it while active does not extend the hour.
Preview data is disposable. Expiry is enforced by the next successful cleanup sweep, not an exact timer.
Environment deletion stops its compute. Provider-retained storage and permanent services may still be billed.

@mmabrouk
mmabrouk force-pushed the feat/m1-membership-resolver branch from 2f42ffa to ddfbaf6 Compare October 24, 2026 18:29
@mmabrouk
mmabrouk force-pushed the feat/m2-run-principal branch 2 times, most recently from adc6980 to 0319338 Compare October 24, 2026 18:46
@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says:

This PR is rebased again onto the new tip, on top of #7748. There were no conflicts. Two commits are added:

  • aae0017: each 60 s access recheck on GET /sessions/{id}/events now reads the current principal and membership (fresh_request_memo). It no longer reads the values that the check at connect saved.
  • 0319338: test_run resolves its delta with scope_policy=PARAMETERS_ONLY, so both delta forms have the same parameters-only boundary.

Each commit includes a unit test. The API unit layers pass for OSS and EE.

@mmabrouk
mmabrouk force-pushed the feat/m2-run-principal branch from 0319338 to 3f0e23c Compare October 24, 2026 19:32
Agenta Product Agent and others added 15 commits October 24, 2026 23:11
…rover on the turn, refuse removed members before a run starts
…es cache TTL, no repeated key read, principal in the start UPDATE)
The events stream rechecks VIEW_SESSIONS every live_auth_recheck_seconds. Its generator runs in a copy of the request context and shared the access memo that the check at connect filled, so a recheck answered from that memo. Each check now runs with a fresh memo (fresh_request_memo) and reads the current principal and membership.
The release already resolves the test_run delta with PARAMETERS_ONLY and answers an
out-of-scope change with PlatformToolHandlerRefused, so this commit keeps that code and
adds tests only: an ordered set or merge on url, uri or headers is refused before any
invoke, and an ordered change inside parameters still runs. The fake resolver applies
the real change-set engine with the scope it receives. The release's two test_run tests
pass the run envelope that test_run needs since M2.
On EE the owner's organization is now pinned to a plan with the RBAC entitlement and no project limit, so the role-change test checks the role rule and the two tests that create a second project can create it.
@mmabrouk
mmabrouk force-pushed the feat/m1-membership-resolver branch from 83995d7 to 6f5366b Compare October 24, 2026 21:14
@mmabrouk
mmabrouk force-pushed the feat/m2-run-principal branch from 3f0e23c to 2e143ac Compare October 24, 2026 21:14
@mmabrouk

Copy link
Copy Markdown
Member Author

🤖 The AI agent says:

This PR is rebased onto a76ec79, on top of #7748. Head: 2e143ac. Since #7782, the release resolves the test_run delta with PARAMETERS_ONLY. I kept the release code and both sets of tests. The release tests now pass the run envelope. New commits:

  • Organization-level writes by an API key or a run credential must come from the organization's default project (check_organization_write). This covers organization rename, transfer and delete, workspace create, the EE domain and SSO writes, EE billing writes, and workspace role change and removal. Browser sessions are not affected.
  • On EE, the run-principal acceptance runs with an owner on a plan that has the RBAC entitlement.

@mmabrouk
mmabrouk changed the base branch from feat/m1-membership-resolver to release/v0.124.0 October 24, 2026 21:33
Invite, resend, role change, member removal and workspace update already check the workspace's default project, so an API key of another project is refused. Organization rename, ownership transfer, deletion and workspace creation, the EE domain and SSO provider writes, and the EE billing writes now apply the same rule through check_organization_write, which runs check_action_access on the organization's default project for an API key or a run credential. Browser sessions are not confined. get_default_project_by_organization_id now returns the oldest default project, so the answer is stable for an organization with several workspaces.
…ization's default project

These routes check the workspace's default project and then change the member across the whole organization. An API key or a run credential must now also be of the organization's default project (check_organization_write). Browser sessions are not affected.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments: 1
api/oss/src/core/triggers/interfaces.py: 1

235-267: Trivial · Maintainability & Code Quality · Quick win

Mark refuse_delivery, pause_trigger, and pause_triggers_for_artifact as @abstractmethod.

These three methods raise NotImplementedError but are not abstract. Every other method on TriggersDAOInterface is abstract. The previous review made EvaluationsDAOInterface.set_run_principal abstract for the same reason.

A DAO that omits one of these methods instantiates without error and fails later:

  • WorkflowsService.archive_workflow wraps pause_triggers_for_artifact in except Exception. A missing implementation is logged as a warning, and triggers stay active. The dispatcher still refuses them later, but only per tick.
  • The dispatcher fails at runtime when it calls refuse_delivery.

An abstract declaration makes an incomplete DAO fail at instantiation.

Proposed change
+    @abstractmethod
     async def refuse_delivery(
@@
+    @abstractmethod
     async def pause_trigger(
@@
+    @abstractmethod
     async def pause_triggers_for_artifact(

Test fakes that subclass the interface must implement the three methods.


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between b3fe813 and 40ed26d.

Files ignored due to path filters: 24
  • web/packages/agenta-api-client/src/generated/api/resources/access/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/access/client/requests/CheckPermissionsRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/gateway/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/gatewayMcp/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/gatewayMcp/client/requests/McpEndpointCheckRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/AgentError.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/AgentRef.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/EvaluationRun.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/McpCredentialBinding.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/PrincipalKind.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/PrincipalRef.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunApproval.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunAuthority.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunContext.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunEnvelope.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunOrigin.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunRestrictions.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/RunSender.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionTurn.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/TriggerDeliveryData.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/TriggerSchedule.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/TriggerSubscription.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/index.ts is excluded by !**/generated/**
Files selected for processing: 219
  • api/AGENTS.md
  • api/ee/src/apis/fastapi/billing/router.py
  • api/ee/src/apis/fastapi/organizations/router.py
  • api/ee/src/core/access/rbac.py
  • api/ee/tests/pytest/acceptance/accounts/test_project_creation_seeding_ee.py
  • api/entrypoints/access.py
  • api/entrypoints/channel_adapters.py
  • api/entrypoints/routers.py
  • api/entrypoints/worker_queues.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000046_add_run_principal_columns.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000047_backfill_run_principal.py
  • api/oss/src/apis/fastapi/access/router.py
  • api/oss/src/apis/fastapi/agent_templates/router.py
  • api/oss/src/apis/fastapi/evaluations/router.py
  • api/oss/src/apis/fastapi/gateways/credentials_router.py
  • api/oss/src/apis/fastapi/gateways/mcps/router.py
  • api/oss/src/apis/fastapi/sessions/router.py
  • api/oss/src/apis/fastapi/tools/router.py
  • api/oss/src/apis/fastapi/triggers/router.py
  • api/oss/src/core/access/context.py
  • api/oss/src/core/access/dtos.py
  • api/oss/src/core/access/interfaces.py
  • api/oss/src/core/access/permissions/service.py
  • api/oss/src/core/access/principals.py
  • api/oss/src/core/access/service.py
  • api/oss/src/core/access/types.py
  • api/oss/src/core/accounts/service.py
  • api/oss/src/core/agent_templates/loader.py
  • api/oss/src/core/channels/queue.py
  • api/oss/src/core/channels/service.py
  • api/oss/src/core/evaluations/interfaces.py
  • api/oss/src/core/evaluations/run_access.py
  • api/oss/src/core/evaluations/runtime/adapters.py
  • api/oss/src/core/evaluations/service.py
  • api/oss/src/core/evaluations/tasks/processor.py
  • api/oss/src/core/evaluations/tasks/run.py
  • api/oss/src/core/evaluations/types.py
  • api/oss/src/core/memberships/dtos.py
  • api/oss/src/core/memberships/service.py
  • api/oss/src/core/sessions/commands/delivery.py
  • api/oss/src/core/sessions/commands/dtos.py
  • api/oss/src/core/sessions/commands/interfaces.py
  • api/oss/src/core/sessions/commands/service.py
  • api/oss/src/core/sessions/inputs/dtos.py
  • api/oss/src/core/sessions/inputs/service.py
  • api/oss/src/core/sessions/interactions/dtos.py
  • api/oss/src/core/sessions/interactions/service.py
  • api/oss/src/core/sessions/starts/service.py
  • api/oss/src/core/sessions/streams/service.py
  • api/oss/src/core/sessions/streams/types.py
  • api/oss/src/core/sessions/turns/dtos.py
  • api/oss/src/core/tools/platform_handlers.py
  • api/oss/src/core/triggers/dtos.py
  • api/oss/src/core/triggers/interfaces.py
  • api/oss/src/core/triggers/service.py
  • api/oss/src/core/workflows/dtos.py
  • api/oss/src/core/workflows/service.py
  • api/oss/src/dbs/http/sessions/control_delivery_direct.py
  • api/oss/src/dbs/postgres/access/__init__.py
  • api/oss/src/dbs/postgres/access/dao.py
  • api/oss/src/dbs/postgres/access/mappings.py
  • api/oss/src/dbs/postgres/evaluations/dao.py
  • api/oss/src/dbs/postgres/evaluations/dbas.py
  • api/oss/src/dbs/postgres/evaluations/mappings.py
  • api/oss/src/dbs/postgres/sessions/commands/dbas.py
  • api/oss/src/dbs/postgres/sessions/commands/mappings.py
  • api/oss/src/dbs/postgres/sessions/inputs/dao.py
  • api/oss/src/dbs/postgres/sessions/inputs/dbes.py
  • api/oss/src/dbs/postgres/sessions/inputs/mappings.py
  • api/oss/src/dbs/postgres/sessions/interactions/dbas.py
  • api/oss/src/dbs/postgres/sessions/interactions/mappings.py
  • api/oss/src/dbs/postgres/sessions/turns/dao.py
  • api/oss/src/dbs/postgres/sessions/turns/dbas.py
  • api/oss/src/dbs/postgres/sessions/turns/mappings.py
  • api/oss/src/dbs/postgres/triggers/dao.py
  • api/oss/src/dbs/postgres/triggers/dbas.py
  • api/oss/src/dbs/postgres/triggers/mappings.py
  • api/oss/src/middlewares/auth.py
  • api/oss/src/routers/organization_router.py
  • api/oss/src/routers/projects_router.py
  • api/oss/src/routers/workspace_router.py
  • api/oss/src/services/api_key_service.py
  • api/oss/src/services/db_manager.py
  • api/oss/src/services/user_service.py
  • api/oss/src/tasks/asyncio/channels/inbox.py
  • api/oss/src/tasks/asyncio/sessions/interactions_dispatcher.py
  • api/oss/src/tasks/asyncio/triggers/dispatcher.py
  • api/oss/src/tasks/taskiq/sessions/interactions_worker.py
  • api/oss/src/utils/caching.py
  • api/oss/tests/pytest/acceptance/accounts/test_organizations.py
  • api/oss/tests/pytest/acceptance/accounts/test_project_route_permissions.py
  • api/oss/tests/pytest/acceptance/sessions/test_run_principal_acceptance.py
  • api/oss/tests/pytest/integration/access/__init__.py
  • api/oss/tests/pytest/integration/access/conftest.py
  • api/oss/tests/pytest/integration/access/test_principals_dao_postgres.py
  • api/oss/tests/pytest/integration/access/test_run_principal_query_counts_postgres.py
  • api/oss/tests/pytest/integration/channels/conftest.py
  • api/oss/tests/pytest/integration/gateways/test_mcp_oauth_connect.py
  • api/oss/tests/pytest/integration/memberships/test_admin_membership_invalidation_postgres.py
  • api/oss/tests/pytest/integration/memberships/test_hot_path_query_counts_postgres.py
  • api/oss/tests/pytest/integration/memberships/test_membership_parity_postgres.py
  • api/oss/tests/pytest/integration/migrations/__init__.py
  • api/oss/tests/pytest/integration/migrations/conftest.py
  • api/oss/tests/pytest/integration/migrations/test_run_principal_backfill_postgres.py
  • api/oss/tests/pytest/integration/migrations/test_run_principal_columns_rerun_postgres.py
  • api/oss/tests/pytest/integration/sessions/test_refused_continuations_postgres.py
  • api/oss/tests/pytest/integration/sessions/test_run_envelope_columns_postgres.py
  • api/oss/tests/pytest/integration/triggers/__init__.py
  • api/oss/tests/pytest/integration/triggers/conftest.py
  • api/oss/tests/pytest/integration/triggers/test_triggers_refuse_postgres.py
  • api/oss/tests/pytest/unit/access/test_access_boundaries.py
  • api/oss/tests/pytest/unit/access/test_access_service.py
  • api/oss/tests/pytest/unit/access/test_acting_user.py
  • api/oss/tests/pytest/unit/access/test_check_action_access_wrapper.py
  • api/oss/tests/pytest/unit/access/test_decide.py
  • api/oss/tests/pytest/unit/access/test_grant_exchange.py
  • api/oss/tests/pytest/unit/access/test_organization_write.py
  • api/oss/tests/pytest/unit/access/test_principal_invalidation.py
  • api/oss/tests/pytest/unit/access/test_principals_cache.py
  • api/oss/tests/pytest/unit/access/test_run_principal_dtos.py
  • api/oss/tests/pytest/unit/access/test_run_principal_mappings.py
  • api/oss/tests/pytest/unit/agent_templates/conftest.py
  • api/oss/tests/pytest/unit/agent_templates/test_github_loading.py
  • api/oss/tests/pytest/unit/agent_templates/test_loader.py
  • api/oss/tests/pytest/unit/agent_templates/test_router.py
  • api/oss/tests/pytest/unit/channels/conftest.py
  • api/oss/tests/pytest/unit/channels/test_channels_approval_admission.py
  • api/oss/tests/pytest/unit/channels/test_channels_inbox_dispatcher.py
  • api/oss/tests/pytest/unit/channels/test_channels_inbox_principal.py
  • api/oss/tests/pytest/unit/channels/test_channels_live_qa_regressions.py
  • api/oss/tests/pytest/unit/channels/test_channels_rechecks.py
  • api/oss/tests/pytest/unit/channels/test_channels_turn_context.py
  • api/oss/tests/pytest/unit/evaluations/conftest.py
  • api/oss/tests/pytest/unit/evaluations/test_query_eval_loops.py
  • api/oss/tests/pytest/unit/evaluations/test_run_batch_invocation.py
  • api/oss/tests/pytest/unit/evaluations/test_run_principal.py
  • api/oss/tests/pytest/unit/evaluations/test_run_slice_ops.py
  • api/oss/tests/pytest/unit/evaluations/test_runtime_topology_planner.py
  • api/oss/tests/pytest/unit/gateways/conftest.py
  • api/oss/tests/pytest/unit/gateways/test_gateway_credentials_envelope.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_mcp_oauth_callback_authorization.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_mcp_router.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_plane_flags.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_rollout_switches.py
  • api/oss/tests/pytest/unit/gateways/test_gateways_sandbox_credential.py
  • api/oss/tests/pytest/unit/memberships/test_membership_cache.py
  • api/oss/tests/pytest/unit/memberships/test_memberships_invalidation.py
  • api/oss/tests/pytest/unit/middlewares/test_auth_grants.py
  • api/oss/tests/pytest/unit/middlewares/test_auth_run_principal.py
  • api/oss/tests/pytest/unit/middlewares/test_auth_secret_token.py
  • api/oss/tests/pytest/unit/migrations/test_migration_gate.py
  • api/oss/tests/pytest/unit/routers/test_membership_endpoints.py
  • api/oss/tests/pytest/unit/sessions/conftest.py
  • api/oss/tests/pytest/unit/sessions/test_cancel_stop_guard.py
  • api/oss/tests/pytest/unit/sessions/test_command_claim_unknown_kind.py
  • api/oss/tests/pytest/unit/sessions/test_command_settle_unknown_kind.py
  • api/oss/tests/pytest/unit/sessions/test_events_recheck_fresh_access.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_first_touch_race.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_lock_races.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_mirrors_existing_row.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_parked_zombie.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_refused_turn_final_beat.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_replica_id_control_chars.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_run_credential.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_runner_token.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_stale_turn_end.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_turn_binding.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_turn_handover.py
  • api/oss/tests/pytest/unit/sessions/test_inputs_run_envelope.py
  • api/oss/tests/pytest/unit/sessions/test_interaction_continuation_admission.py
  • api/oss/tests/pytest/unit/sessions/test_interactions_dispatcher.py
  • api/oss/tests/pytest/unit/sessions/test_live_relay.py
  • api/oss/tests/pytest/unit/sessions/test_pending_inputs_service.py
  • api/oss/tests/pytest/unit/sessions/test_respond_interaction_durable.py
  • api/oss/tests/pytest/unit/sessions/test_session_commands_dao.py
  • api/oss/tests/pytest/unit/sessions/test_session_inputs_dao.py
  • api/oss/tests/pytest/unit/sessions/test_session_start_service.py
  • api/oss/tests/pytest/unit/sessions/test_stream_fill_once.py
  • api/oss/tests/pytest/unit/sessions/test_stream_header_merge.py
  • api/oss/tests/pytest/unit/sessions/test_turn_attribution.py
  • api/oss/tests/pytest/unit/sessions/test_watch_interactions_publish.py
  • api/oss/tests/pytest/unit/sessions/test_watch_lifecycle_publish.py
  • api/oss/tests/pytest/unit/sessions/test_watch_session_list_publish.py
  • api/oss/tests/pytest/unit/test_channel_adapters_entrypoint.py
  • api/oss/tests/pytest/unit/tools/conftest.py
  • api/oss/tests/pytest/unit/tools/test_platform_handlers.py
  • api/oss/tests/pytest/unit/tools/test_workflow_tool_call.py
  • api/oss/tests/pytest/unit/triggers/conftest.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_dispatcher.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_lifecycle.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_principal.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_schedules_refresh.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_soft_delete_postgres.py
  • api/oss/tests/pytest/unit/triggers/test_triggers_subscription_test_mode.py
  • api/oss/tests/pytest/unit/vault/test_write_only_routes.py
  • api/oss/tests/pytest/unit/workflows/conftest.py
  • api/oss/tests/pytest/unit/workflows/test_archive_pauses_triggers.py
  • api/oss/tests/pytest/unit/workflows/test_invoke_detached.py
  • api/oss/tests/pytest/unit/workflows/test_prepare_invoke_envelope.py
  • api/oss/tests/pytest/unit/workflows/test_service_url_policy.py
  • api/oss/tests/pytest/utils/cache.py
  • api/oss/tests/pytest/utils/memberships.py
  • api/oss/tests/pytest/utils/principals.py
  • api/oss/tests/pytest/utils/statements.py
  • docs/docs/administration/access-control/01-organizations.mdx
  • docs/docs/administration/access-control/03-rbac.mdx
  • docs/docs/administration/security/01-overview.mdx
  • docs/docs/concepts/07-automations.mdx
  • docs/docs/misc/07-errors.mdx
  • docs/docs/reference/api-guide/01-overview.mdx
  • docs/docs/self-host/upgrade-and-migrate/01-upgrading.mdx
  • sdks/python/agenta/sdk/agents/connections/__init__.py
  • sdks/python/agenta/sdk/agents/connections/errors.py
  • sdks/python/agenta/sdk/agents/platform/connections.py
  • sdks/python/agenta/sdk/middlewares/routing/auth.py
  • sdks/python/agenta/sdk/middlewares/running/normalizer.py
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py
  • sdks/python/oss/tests/pytest/unit/test_auth_middleware_credentials.py
Files with no reviewable changes: 3
  • api/oss/tests/pytest/integration/migrations/init.py
  • api/oss/tests/pytest/integration/triggers/init.py
  • api/oss/tests/pytest/integration/access/init.py
Files skipped from review as they are similar to previous changes: 1
  • api/oss/src/dbs/postgres/access/init.py

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment on lines +13 to +39
# The RBAC entitlement of an organization changes with its plan, which is rare. A
# decision reads it on every call; this process-local memo keeps that read off the hot
# path (no Redis round trip per decision). It is at most 30 s old, which is fresher
# than the 300 s cached decision M1 kept for the same answer.
_ENFORCED_TTL_SECONDS = 30.0
_MEMO_CAPACITY = 4096


class EntitlementsRbacEnforcement(RbacEnforcementInterface):
"""EE: role checks apply only when the organization's plan has the RBAC
entitlement. Without it every member passes, as before."""

def __init__(self) -> None:
self._memo: Dict[UUID, Tuple[float, bool]] = {}

async def is_enforced(self, *, organization_id: UUID) -> bool:
now = monotonic()
cached = self._memo.get(organization_id)
if cached is not None and now - cached[0] < _ENFORCED_TTL_SECONDS:
return cached[1]
check, _, _ = await check_entitlements(
key=Flag.RBAC,
scope=scope_from(organization_id=organization_id),
)
if len(self._memo) >= _MEMO_CAPACITY:
self._memo.clear()
self._memo[organization_id] = (now, bool(check))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor · Security & Privacy · Quick win

Document or invalidate the process-local RBAC entitlement memo.

api/AGENTS.md limits caching to three items: the principal record, the membership, and the project's organization. Their writers invalidate them. This memo adds a fourth cached input to decide, and nothing invalidates it.

Suppose a plan change grants the RBAC entitlement. Until the memo expires, each pod can still return False from is_enforced for up to 30 s. During that time _capability allows every member (RBAC_NOT_ENTITLED).

Use one of these fixes:

  • Clear the memo entry from the subscription and plan writer.
  • Record this bounded exception in api/AGENTS.md, so the invariant stays accurate.

As per coding guidelines: "Only the principal record (fetch_principal), the membership (resolve_membership) and the project's organization are cached, and their writers invalidate them."

Source: Coding guidelines

Comment on lines +1317 to +1321
if is_active:
await recheck_trigger_principal(
project_id=project_id, trigger=existing, origin=RunOrigin.SUBSCRIPTION
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor · Functional Correctness · Quick win

Resuming through a full-PUT edit skips the principal recheck.

set_subscription_active and set_schedule_active call recheck_trigger_principal before they set is_active=True. edit_subscription and edit_schedule do not. Both edit methods accept flags.is_active from the client. A client can therefore send is_active: true through the edit route on a trigger that was paused for access_denied or missing_principal. The trigger is then active again without a recheck, and the old meta.paused_reason can remain.

The dispatcher still refuses at the next fire, so access is not bypassed. The result is the delayed refusal that the recheck_trigger_principal docstring says it prevents, and the trigger pauses itself again. Run the same recheck in both edit methods when existing.flags.is_active is false and the edit sets it to true. When the recheck passes, also apply _resumed_meta.

Proposed fix (edit_schedule; apply the same change in edit_subscription with RunOrigin.SUBSCRIPTION)
        if schedule.flags.is_active and not existing.flags.is_active:
            await recheck_trigger_principal(
                project_id=project_id, trigger=existing, origin=RunOrigin.SCHEDULE
            )
            schedule.meta = _resumed_meta(schedule.meta)

Comment on lines +196 to +198
run_dbe.principal_kind = principal.kind.value
run_dbe.principal_id = principal.id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor · Data Integrity & Integration · Quick win

create_runs does not write principal_user_id, but create_run does.

create_run sets principal_kind, principal_id, and principal_user_id = user_id. edit_run also writes all three columns. create_runs writes only principal_kind and principal_id, so every run created in a batch has principal_user_id set to NULL.

EvaluationRunDBA documents principal_user_id as the acting user. For an installation principal, the run uses that user's role. A batch-created run with an installation principal then has no acting user when its principal is rechecked. Set the column the same way create_run does.

Proposed fix
             run_dbe.principal_kind = principal.kind.value
             run_dbe.principal_id = principal.id
+            run_dbe.principal_user_id = user_id


An API key now acts only in the project it belongs to. A request with the key to another project, such as `PATCH /projects/{id}` or `DELETE /projects/{id}`, returns `403`. Before, the key used its creator's role in the other project. Browser sessions do not change.

Workspace and organization management with an API key (invite, resend invite, role change, member removal, workspace update) needs a key of the workspace's default project. A key of another project in that workspace returns `403`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor · Functional Correctness · Quick win

List all organization-level writes that require a key from the default project.

This paragraph names only the workspace operations: invite, resend invite, role change, member removal and workspace update. The PR also applies the same rule to these writes:

  • organization rename, ownership transfer and deletion
  • workspace creation
  • EE domain, SSO and billing writes

The acceptance test test_organization_writes_need_a_key_of_the_default_project asserts 403 for these writes. An operator who reads only this list may expect an automation that renames or transfers an organization with a non-default project key to keep working. That automation fails after the upgrade.

Proposed wording
--- "a/docs/docs/self-host/upgrade-and-migrate/01-upgrading.mdx"
+++ "b/docs/docs/self-host/upgrade-and-migrate/01-upgrading.mdx"
@@ -60,7 +60,7 @@
 
 An API key now acts only in the project it belongs to. A request with the key to another project, such as `PATCH /projects/{id}` or `DELETE /projects/{id}`, returns `403`. Before, the key used its creator's role in the other project. Browser sessions do not change.
 
-Workspace and organization management with an API key (invite, resend invite, role change, member removal, workspace update) needs a key of the workspace's default project. A key of another project in that workspace returns `403`.
+Workspace and organization management with an API key needs a key of the default project. This covers invite, resend invite, role change, member removal and workspace update, and also organization rename, ownership transfer and deletion, and workspace creation. In Enterprise it also covers domain, SSO and billing writes. A key of another project returns `403`.
 
 ### Automations and evaluations run as the user or key that started them
 
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Workspace and organization management with an API key (invite, resend invite, role change, member removal, workspace update) needs a key of the workspace's default project. A key of another project in that workspace returns `403`.
Workspace and organization management with an API key needs a key of the default project. This covers invite, resend invite, role change, member removal and workspace update, and also organization rename, ownership transfer and deletion, and workspace creation. In Enterprise it also covers domain, SSO and billing writes. A key of another project returns `403`.

@mmabrouk mmabrouk closed this Oct 24, 2026
@mmabrouk mmabrouk reopened this Oct 24, 2026
@mmabrouk mmabrouk closed this Oct 24, 2026
@mmabrouk mmabrouk reopened this Oct 24, 2026
@mmabrouk
mmabrouk merged commit 548a584 into release/v0.124.0 Oct 24, 2026
252 of 270 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant