Caller context
The caller assertion tells an application who is calling. Caller context tells it who the call is for.
A service user, such as an
external workload
that serves its own users, can send its own context with a call to Main
Sequence: which of its users the call is for, and its own information about the
call. It sends that context in the W3C baggage
header. The W3C traceparent travels
with it and ties the work to one trace; the context itself is the baggage.
Main Sequence carries the context to the applications that run on it (FastAPI releases, Job runs and Agents) and records it as the caller's claim. It never uses it to authenticate, authorize, admit or bill a call, defines no keys, and builds no feature on it. Applications use it to make their own rules and filters, for example to return only the data of the person a call is for.
Keys
Each member of the header is one key and its value:
baggage: mainsequence.<application>.<name>=<value>,mainsequence.<application>.<name>=<value>
- Only keys of the form
mainsequence.<application>.<name>are recorded and carried into the work a call creates, such as Job runs and Agent Tasks. Other keys stop there, and W3C properties (anything after a member's;) are dropped. Values are kept as received. Give every key that an application on Main Sequence must receive this form. - The application chooses its segment and its names, with no registry and no approval. Main Sequence gives no key a meaning and treats every key the same way. Two applications that choose the same segment collide only on calls that cross both, and a collision grants nothing in Main Sequence.
- What the keys and values mean, including whether one refers to a person, is up to whoever designs the application.
- Main Sequence never parses a value. Encode a
,,;,=or space inside a value with percent-encoding, as W3C Baggage requires; OpenTelemetry's propagators do it for you. - Values are public by contract. Baggage is passed on downstream, to third parties too, and Main Sequence records it. Never put a secret, a credential or a proof in it.
Trust
Anyone who can call an application can send it any context. Context is a claim, and it is worth exactly as much as the application's trust in the caller that delivered it. Main Sequence never signs it and never checks a value.
- Believe context only from callers you trust to vouch for it, checked
against the verified caller at the hop where you read it. At a FastAPI
release, that caller is
User.get_logged_user(). - From any other caller, ignore it, or treat it as that caller's own unverified claim.
- Trust is per hop. An application that receives a call from another application trusts that application to have checked the original caller. It does not see, and does not need, the original caller's identity.
- Whoever may run a Job may set its runs' context. A run carries the context of the request that created it, so a Job whose code makes rules from that context gives run permission only to service users it trusts; see Who can use a Job.
What Main Sequence guarantees:
- at a release or an Agent, the context arrives next to the authenticated caller, as received;
- the context of a Job run or an Agent Task is exactly the context of the request that created it, never merged with another request's and never changed afterwards;
- no Main Sequence decision reads it.
It does not guarantee that any value is true.
Caller context is not a requester. It names no Main Sequence User and grants nothing; an application that acts for a Main Sequence person does so only through requester-bound access.
Where context arrives
| Where the application runs | What it receives |
|---|---|
| A FastAPI release | The request's baggage and traceparent headers, on every request, next to the caller assertion. Never as environment variables: each request brings its own. |
| A Job run | BAGGAGE and TRACEPARENT environment variables, in W3C header format, when the run was created by a request that carried context. |
| An Agent Task | baggage and traceparent headers on the Task's dispatch signal. |
Work keeps the context of the request that created it: a Job run or an Agent Task started by a call with context carries that context. A Job, its schedule, a release or an Agent never stores one, so a scheduled run, or anything the platform starts on its own, has no context.
A call a Job run makes to Main Sequence with its run credentials and no
baggage header is recorded with the run's own context, so the work it creates
inherits that context even when the run's code does not pass it on. A header,
when the run sends one, wins.
Missing context
Missing context means no external user. Fail closed: refuse what needs a person, and never fall back to broader access. Context can be missing because the caller sent none, because it was malformed or too large, or because an older SDK on the way did not pass it on. Context from a caller you do not trust counts as missing too.
Limits
The limits are those of OpenTelemetry's W3C baggage parser:
- a header over 8192 bytes is ignored whole;
- a member over 4096 bytes is skipped;
- at most 180 members are read.
A malformed member is skipped. Bad baggage never fails a request: what cannot be read counts as missing. Keep context to a few short keys.
With the SDK
The SDK gives no key a meaning and adds no accessor of its own: application
code reads the context with OpenTelemetry's standard API,
opentelemetry.baggage.
- FastAPI releases.
install_request_identity(app)makes each request'sbaggageandtraceparentthe OpenTelemetry context of that request, next to the caller it verifies. See Caller context on the FastAPI page. - Job runs.
import mainsequencerestoresTRACEPARENTandBAGGAGEas the process's OpenTelemetry context. See Caller context in a run. - Calls the SDK makes, to Main Sequence and to Agents, carry the current
traceparentandbaggage. A Job run or an Agent Task that a release starts while it handles a request therefore receives that request's context. A header you set yourself wins, and without a current context nothing is added. - Log lines written through the SDK's logger carry the current context's
mainsequence.keys as abaggagefield. - Agents built with the Agents SDK receive a Task's context on its dispatch signal; the Agents SDK documentation describes how their code reads it.
Example
Advisor is an invented application: a service outside Main Sequence, connected
as the external workload advisor-backend, that calls the FastAPI release
portfolio-api for its own users. It chooses two keys:
mainsequence.advisor.person: Advisor's opaque id for the person;mainsequence.advisor.request: Advisor's id for the request.
1. advisor-backend sends its context with each call it makes as its
external workload, signed in with
workload identity:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
baggage: mainsequence.advisor.person=p-7f3a9c,mainsequence.advisor.request=r-0042
With OpenTelemetry, the service sets the keys on its context and its propagator writes the headers:
from opentelemetry import baggage, propagate
context = baggage.set_baggage("mainsequence.advisor.person", "p-7f3a9c")
context = baggage.set_baggage("mainsequence.advisor.request", "r-0042", context=context)
headers = {}
propagate.inject(headers, context=context) # W3C baggage, and traceparent when tracing
2. portfolio-api checks its caller, then reads the person. It believes
an Advisor person only when the verified caller is advisor-backend's workload
User, whose UID it read from a call that workload made:
from fastapi import FastAPI, HTTPException
from opentelemetry import baggage
from mainsequence.client import User
from mainsequence.server.fastapi import install_request_identity
ADVISOR_BACKEND = "<workload User UID of advisor-backend>"
app = FastAPI()
install_request_identity(app)
def advisor_person() -> str | None:
"""The Advisor person this call is for, or None."""
if User.get_logged_user().uid != ADVISOR_BACKEND:
return None # only advisor-backend vouches for Advisor people
return baggage.get_baggage("mainsequence.advisor.person")
@app.get("/portfolio")
def portfolio():
person = advisor_person()
if person is None:
raise HTTPException(status_code=403, detail="This operation needs an Advisor person.")
return load_portfolio(person) # that person's data, and nothing else
A call from any other caller gets no Advisor person, whatever its baggage says.
A call from advisor-backend without context gets none either.
3. The work it starts keeps the context. While it handles the request,
portfolio-api runs the Job portfolio-rebalance with job.run_job(). The
SDK passes the request's context on, so the run starts with BAGGAGE and
TRACEPARENT:
import mainsequence # restores the run's TRACEPARENT and BAGGAGE
from opentelemetry import baggage
person = baggage.get_baggage("mainsequence.advisor.person")
if person is None:
raise SystemExit("This run needs an Advisor person.") # a scheduled run has none
Run permission on portfolio-rebalance is given to portfolio-api's workload
and to no other service user, so the run can trust its context.
4. Main Sequence records the context as a claim on the work it carried, and no Main Sequence decision reads it.