[PLAN] Define "Execution Principal" contract for sessionless, queued, and outside-in AI runs
>>> [!note] Migrated issue
<!-- Drupal.org comment -->
<!-- Migrated from issue #3573899. -->
Reported by: [scott falconer](https://www.drupal.org/user/52557)
>>>
<p><strong>Problem/Motivation</strong></p>
<p>Currently, the AI subsystem largely assumes that tool execution and access checks rely on the active web session ('\Drupal::currentUser()'). As we move toward autonomous background agents, cron runs, queue workers (Symfony Messenger), and outside-in orchestration (MCP), this assumption breaks down.</p>
<p>Attempts to solve sessionless execution by creating "synthetic users" or performing "temporary role masquerading" (e.g., <span class="drupalorg-gitlab-issue-link drupalorg-gitlab-link-wrapper"><a href="https://git.drupalcode.org/project/ai_agents/-/work_items/3518167" class="drupalorg-gitlab-link">https://git.drupalcode.org/project/ai_agents/-/work_items/3518167</a></span>) introduce severe systemic risks:</p>
<ol>
<li><strong>Security & Privilege Ambiguity</strong>: Faking roles without a fully loaded, real user account bypasses core invariants. If an ephemeral role fails to revert correctly, it risks severe privilege escalation.</li>
<li><strong>Cache Poisoning</strong>: Core access checks cache against the real evaluated account. Synthetic sessions and masqueraded roles can cause cache context leaking <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-13"><a href="https://www.drupal.org/project/drupal/issues/2628870" title="Status: Needs work">#2628870: Access result caching per user(.permissions) does not check for correct user</a></span>.</li>
<li><strong>Governance Breakdown</strong>: Revisions, audit logs, Content Moderation, and Workspaces require a stable, real user entity to track <em>who</em> authored a change. Ephemeral roles break this traceability.</li>
</ol>
<p>In short: Because LLM agents are fundamentally non-deterministic, the platform architecture they run on must be hyper-deterministic.</p>
<p><strong>Proposed Resolution<br>
</strong></p>
<p>Establish a formal "<em>Execution Principal</em>" contract across the AI ecosystem. Any actor (human, background agent, or external orchestrator) performing work on the platform must be evaluated safely, deterministically, and with a clear audit trail.</p>
<p>To achieve this, we must explicitly separate <em>who/what caused the run</em> from <em>who executes the run</em>, and formalize the <em>execution modality.</em></p>
<p><strong>Definitions<br>
</strong></p>
<ul>
<li><strong>Executor (Execution Principal)</strong>: The <em>real, loadable Drupal user entity</em> the system uses for access checks and tool execution (possibly a dedicated service account for background/outside-in work).</li>
<li><strong>Initiator</strong>: The upstream origin that caused the run to exist. The initiator <em>is not required to be a human</em>. It can be anonymous, a system trigger (cron/queue), or an external authenticated subject (MCP/orchestration credential).</li>
<li><strong>Modality</strong>: How the run is being executed ('interactive', 'background', 'outside_in', 'scheduled'). Modality maps to a <em>policy profile</em> dictating which guardrails, budgets, and rate limits apply.</li>
</ul>
<p><strong>Invariants / Non-goals<br>
</strong></p>
<ul>
<li><em>Executor MUST be a loadable Drupal user entity</em> (a service account is permitted and highly recommended for background/outside-in agents). </li>
<li>No synthetic users and no temporary role mutation at runtime.</li>
<li>Initiator MUST be recorded for provenance as an initiator descriptor:</li>
<li>The system MUST capture at least one of:</li>
<li> - 'initiator_uid' (may be 0), OR</li>
<li> 'initiator_subject' (a string describing the authenticated credential or system trigger, e.g. 'system:cron', 'system:messenger', 'mcp:apikey:<id>', 'oauth:sub:<sub>').</sub></id></li>
<li>The initiator does <em>not</em> have to be a human; it is the upstream origin of the run.</li>
<li>External callers MUST NOT choose <em>executor_uid</em> or the initiator descriptor.</li>
<li> - For outside-in HTTP entry points (MCP, orchestration APIs), executor and initiator MUST be resolved server-side from trusted configuration + authenticated credentials.</li>
<li> - Example: An MCP entry point maps an incoming API key or OAuth token to a designated Drupal Service Account and initiator subject via configuration, rather than trusting arbitrary fields passed in JSON payloads.</li>
<li> Attribution: When an operation creates or modifies an entity, the <em>revision author MUST be the Executor</em>. </li>
<li> The Initiator is preserved in the run metadata/logs for provenance.</li>
<li><em>Safe Context Switching</em>: All background/sessionless context switching MUST use core's 'account_switcher' (or a documented equivalent) and MUST switch back in a 'finally' block.</li>
<li><strong>Token Security / Token Context</strong></li>
<p> - no token replacement on untrusted LLM outputs by default<br>
- allow-list tokens if supported<br>
- token evaluation context must be explicit (default initiator), never implicitly executor
</p></ul>
<p><strong>Execution Envelope / Metadata<br>
</strong><br>
To support safe background queuing and observability (<span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-1"><a href="https://www.drupal.org/project/ai/issues/3533109" title="Status: Active">#3533109: [Meta] AI Logging/Observability</a></span>), internal dispatchers (Messenger, queues) and API boundaries must carry a standardized run metadata envelope.</p>
<p><strong>Strategic note</strong>: This envelope is the strict contract that every AI tool invocation flows through. It is foundational infrastructure and intentionally extensible for future AX metadata (benchmark tags, intent descriptors, behavioral observability), even if those are not in immediate scope.</p>
<p><strong>Required:</strong><br>
- 'run_id' (uuid)<br>
- 'modality' ('interactive' | 'background' | 'outside_in')<br>
- Modality maps to a <em>policy profile</em> (e.g., rate limits, backoff rules, tool frequency limits).<br>
- mode: execute|simulate<br>
- 'executor_uid' (required; must resolve to a loadable user entity)<br>
- 'initiator_uid' (optional; may be 0)<br>
- 'initiator_subject' (optional; REQUIRED if 'initiator_uid' is not present)</p>
<p><strong>Optional but recommended:</strong><br>
- 'thread_id'<br>
- environment_id (server-derived; mismatch must fail-safe before execution)<br>
- 'correlation_id' (if distinct from 'run_id')<br>
- 'caller_run_id' (if this run was spawned by another run; enables chain-of-custody)<br>
- 'source' ('ui', 'cron', 'messenger', 'mcp', 'orchestration')</p>
<p><strong>Definition of Done / Acceptance Criteria<br>
</strong></p>
<ul>
<li> [ ] Terminology is formally defined in AI Core architecture docs/interfaces (Initiator, Executor/Execution Principal, Modality, Run ID).</li>
<li> [ ] A canonical "run metadata envelope" is specified (fields + meaning) as a class/struct/value object.</li>
<li> [ ] A reference implementation path is agreed upon for:</li>
<li> - How an executor is configured for autonomous agents.</li>
<li> - How the envelope is propagated in Messenger payloads.</li>
<li> - How outside-in entry points securely resolve incoming credentials to a local executor + initiator descriptor.</li>
<li>[ ] Hard constraints are enforced in the architecture:</li>
<li> - No temporary role mutation.</li>
<li> - Executor must be loadable.</li>
<li> - 'account_switcher' is used and always reverts ('try/finally').</li>
<li>[ ] Explicit negative tests exist and are enforced:</li>
<li> Context switching always reverts via 'try/finally'.</li>
<li> - <strong>Negative</strong>: A run dispatched with a non-existent or invalid `executor_uid` MUST fail and halt *before* any tool execution occurs.</li>
<li> - <strong>Negative</strong>: A tool call MUST NOT silently fall back to `\Drupal::currentUser()` if an execution envelope is present (preventing privilege leakage).</li>
<li> - <strong>Negative</strong>: Outside-in endpoints MUST ignore/reject caller-supplied 'executor_uid' / initiator fields if maliciously provided in request bodies.</li>
<li> Tool access checks evaluate strictly against the executor.</li>
</ul>
> Related issue: [Issue #3518167](https://www.drupal.org/node/3518167)
> Related issue: [Issue #3556389](https://www.drupal.org/node/3556389)
> Related issue: [Issue #3560619](https://www.drupal.org/node/3560619)
> Related issue: [Issue #3493260](https://www.drupal.org/node/3493260)
> Related issue: [Issue #3533109](https://www.drupal.org/node/3533109)
> Related issue: [Issue #3554797](https://www.drupal.org/node/3554797)
> Related issue: [Issue #3575927](https://www.drupal.org/node/3575927)
issue