Execution Contexts

An ExecutionContext is the per-invocation envelope that travels with every capability call. It binds together who is calling (subject), where the data lives (Space), what trust profile applies (Environment), what they are asking for (capability ref + input), and the caller's correlation IDs. Every authorization decision, every audit-log entry, and every runner dispatch reads from this one shape.

Where Spaces and Environments are durable, ExecutionContext is ephemeral — built fresh for each invocation, then discarded after the response is audited.


The envelope shape

The contract lives in @xemahq/contracts/execution-context:

ts
1interface ExecutionContext {
2  // Who is calling
3  subject:     SubjectRef;       // { kind: SubjectKind; id: string }
4  caller:      CallerKind;       // 'user' | 'agent' | 'workflow' | 'app' | 'service' | 'runner'
5  callerOrigin?: string;         // optional UI origin or service name
6
7  // Where data lives + what trust profile applies
8  space:       SpaceRef;         // xema://orgs/acme/projects/billing
9  environment: ExecutionEnvironmentId;  // 'org' | 'project' | 'sandbox' | …
10
11  // What is being asked
12  capability:  CapabilityRef;    // 'connector:scm.create-pull-request@1'
13  input:       unknown;          // schema-validated against describe(ref)
14
15  // Correlation
16  invocationId: string;          // ULID; one per call
17  parentInvocationId?: string;   // for sub-agent / nested-step lineage
18  traceId?:     string;          // OpenTelemetry-compatible
19
20  // Policy-mediated annotations (filled in by the router)
21  allowedCapabilities?: CapabilityRef[];   // set after policy resolution
22  obligations?:         PolicyObligation[];
23  routeHints?:          RouteHint[];
24
25  // Audit metadata
26  auditId?:     string;          // populated after the audit-log row exists
27}

The closed enums SubjectKind, CallerKind, SpaceKind, and ExecutionEnvironmentId make the envelope statically typed end-to-end. Unknown values fail-fast at the router boundary.


What flows where

text
1caller (UI / agent / workflow / external app)
2   │  Constructs minimal context: { subject, capability, input }
3   ▼
4xema-capability-router
5   │  Resolves Space + Environment from session/active context.
6   │  Builds the full ExecutionContext.
7   ▼
8authorization-api.policyCheck(context)
9   │  Returns PolicyDecision: allow | deny | needs_approval
10   │  Plus obligations + routeHints
11   ▼
12router updates context.{allowedCapabilities, obligations, routeHints}
13   │
14   ▼
15runner dispatch
16   │  Receives the full context (signed); enforces routeHints.
17   ▼
18audit-log-api
19      Persists the complete envelope with the decision.

The router is the only process that builds the full context. Subjects (agents, UIs, workflows) provide the minimal triplet { subject, capability, input }; the router fills in Space, Environment, correlation IDs, and audit metadata. Subjects cannot forge environment or space.


Building a context from a session

Most invocations originate inside a session. The router resolves Space + Environment by walking the session's lineage:

Session kindDefault SpaceDefault Environment
Project workspace sessionxema://orgs/<org>/projects/<project>project
Org admin console sessionxema://orgs/<org>org
Sandbox build sessionxema://biomes/<biome>sandbox
Delegated app sessionxema://orgs/<org>/apps/<app>app or public
Personal sessionxema://users/<userId>org (fallback to user)

A subject may request an alternate Space or Environment via the Shell's --space / --environment flags or the capability invoker's requestedEnvironment field. The router applies the request only if the subject holds a grant that allows it; otherwise the request is rejected before policy check.


Caller kinds — who originated the call

CallerKind records the role of the originating actor, not its identity. Identity lives in subject:

CallerKindWhat it represents
userA logged-in human at a UI
agentAn LLM agent running inside a session
workflowA workflow step running on the workflow runtime
appAn external-subject session through a published app
serviceService-to-service call between platform services
runnerA runner reporting status or fetching work

Audit rows group by caller for human-readable reports ("Who has been invoking connector:bank.transfer@1?" returns one row per agent, workflow, user).


Sub-invocations and lineage

When an agent spawns a sub-agent, or a workflow step invokes a capability that itself invokes another, the child invocation inherits the parent context with two changes:

  • A fresh invocationId.
  • parentInvocationId set to the parent's invocationId.

Lineage queries are first-class: the Shell's why-denied <auditId> walks the parent chain and reports the originating subject plus the chain of capabilities that led to the denial.


Why one envelope, everywhere

Before ExecutionContext, every service had its own ad-hoc bag of fields: subjectEnvironment, actorRef, tenantId, auditCorrelationId, with no shared contract. ExecutionContext collapses that into one typed shape that:

  • The router constructs once per call.
  • Policy reads to make a decision.
  • The runner enforces during dispatch.
  • The audit log persists verbatim.

A new field (e.g. dataResidencyHint) is added in exactly one place — the contract package — and every consumer picks it up via the next generated client.


  • Spaces — the space field on every context.
  • Environments — the environment field on every context.
  • Policy — what reads the context to make a decision.
  • Runners — what enforces routeHints during dispatch.
  • Capabilities — the invocation surface every context flows through.

Previous: ← Spaces Next: Policy →