Tenancy Stamping
For multi-tenant deployments, every Forge audit event can carry top-level org_id and workspace_id keys so SIEM / audit-warehouse consumers filter by tenancy without joining against auth_verify rows. See issue #157.
Two layers
The same agent process supports both the static-deployment case (one agent serves one workspace) and the multi-tenant routing case (one agent serves many workspaces, the orchestrator picks per request).
Tenancy fields (org_id / workspace_id)
| Layer | Source | Wins when |
|---|---|---|
| 1 — Explicit on event | AuditEvent.OrgID / AuditEvent.WorkspaceID set before emit | Always — caller-owned event takes precedence over every fallback |
| 2 — Per-request override | X-Forge-Org-ID / X-Forge-Workspace-ID request headers | Inside the request scope when present; ctx falls through to layer 3 otherwise |
| 3 — Deployment-time stamp | FORGE_ORG_ID / FORGE_WORKSPACE_ID env vars | Whenever the higher layers carry no value |
Each field is resolved independently. A request that overrides only X-Forge-Org-ID still lets the env stamp fill in workspace_id.
Entity fields (entity_id / entity_type) — #164
| Layer | Source | Wins when |
|---|---|---|
| 1 — Explicit on event | AuditEvent.EntityID / AuditEvent.EntityType set before emit | Always — caller-owned event takes precedence |
| 2 — Deployment-time stamp | FORGE_AGENT_ID env / forge.yaml agent_id → entity_id; entity_type hardcoded to "agent" | Whenever the higher layer is empty |
Entity identity has no per-request header layer — entity is fixed at process startup. If a deployment needs per-request entity routing, the tenancy layer above already covers that (an agent serving multiple workspaces). Agent identity is the process, by definition.
entity_type and entity_id match the field names + values the guardrails library uses (see EntityType constants: agent / workflow / assistant). Forge only runs agents today, so the value is always "agent"; future entity types are an additive value change, not a schema change.
Static tenancy (one agent per workspace)
The simplest case: deploy one Forge agent into one workspace, declare the tenancy via env, never set headers. Every emitted event — including startup banners — carries the stamp.
# Kubernetes deployment fragment
env:
- name: FORGE_ORG_ID
value: "org_abc123"
- name: FORGE_WORKSPACE_ID
value: "ws_xyz789"
The audit stream then looks like:
{"ts":"2026-06-14T10:00:00Z","event":"agent_card_published","schema_version":"1.0","org_id":"org_abc123","workspace_id":"ws_xyz789","fields":{...}}
{"ts":"2026-06-14T10:00:05Z","event":"session_start","schema_version":"1.0","seq":1,"correlation_id":"...","task_id":"...","org_id":"org_abc123","workspace_id":"ws_xyz789"}
{"ts":"2026-06-14T10:00:08Z","event":"llm_call","schema_version":"1.0","seq":2,"correlation_id":"...","task_id":"...","model":"...","provider":"...","org_id":"org_abc123","workspace_id":"ws_xyz789"}
SIEM filter: org_id = "org_abc123" AND workspace_id = "ws_xyz789".
Per-request routing (one agent serves many workspaces)
For deployments where one Forge agent fronts many workspaces and the orchestrator routes per request, set the env vars to a default tenancy (or leave them empty) and have the orchestrator send the override headers on every request:
curl -X POST https://agent.example.com/ \
-H 'X-Forge-Org-ID: org_def456' \
-H 'X-Forge-Workspace-ID: ws_pqr012' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"tasks/send","params":{...}}'
Every audit event emitted during that request carries "org_id":"org_def456","workspace_id":"ws_pqr012". The next request from a different workspace gets its own stamp.
Startup banners (agent_card_published, policy_loaded, audit_export_status) still reflect the env stamp because they have no request context.
Outbound propagation (agent-to-agent flows)
When one Forge agent calls another (via the egress proxy with explicit propagation), the helper coreruntime.TenancyContextFromContext(ctx).ApplyToHTTPHeaders(req.Header) writes both headers onto the outbound request. The downstream agent picks them up at its A2A boundary the same way.
Auto-propagation is NOT built into the egress proxy. The agent only propagates tenancy when it knows the target is a tenancy-aware Forge peer. This mirrors the workflow-header behavior: explicit only, to avoid leaking tenancy to unrelated third-party APIs.
Outbound propagation (platform callouts)
Every Forge→platform HTTP callout — admission (FORGE_ADMISSION_URL), the remote session store, the PDP (POST /pdp/decide), and the MCP platform token / authorize endpoints (type: platform / type: user) — sends Org-Id + Workspace-Id headers (from FORGE_ORG_ID / FORGE_WORKSPACE_ID) alongside Authorization: Bearer ${FORGE_PLATFORM_TOKEN}. This is a hard contract, not best-effort: the platform verifies a per-org HS256 token and needs Org-Id to select the signing secret before it can validate the bearer — omitting it returns 401 "missing org-id header". Note the header spelling here is Org-Id / Workspace-Id (the platform-callout convention), distinct from the inbound X-Forge-Org-ID / X-Forge-Workspace-ID request-override headers above.
Workload-identity presentation — X-Workload-Token (agent-identity L1)
When an agent is deployed with WORKLOAD_IDENTITY_MODE=k8s_sa, the platform (agent-builder) provisions a per-agent Kubernetes ServiceAccount and projects an audience-bound, kubelet-rotated SA token into a file (default /var/run/secrets/initializ.ai/workload/token, override via INITIALIZ_WORKLOAD_TOKEN_PATH; audience initializ:platform-token-endpoint). Every platform callout above additionally sends that token as the X-Workload-Token header, so the platform’s per-agent entitlement check can bind the call to the agent’s workload identity — e.g. an agent bound to svc-runbooks can no longer fetch svc-security’s token.
The token is read fresh from the file on every request and never cached — the kubelet rotates the file in place, so a cached value goes stale and is rejected (the no_token failure class). Like the tenancy headers, X-Workload-Token is omitted entirely (never sent empty) when workload identity is inactive: no WORKLOAD_IDENTITY_MODE=k8s_sa, or no readable token file (the normal case for self-hosted / non-Kubernetes deploys). Presentation is additive and never blocks a callout.
Attestation level + PDP floors (rollout note). In k8s_sa mode forge stamps attestation_level: attested:placement on the PDP request + audit events (agent-identity L2); without it the level is empty, which ranks below attested:placement. So a PDP policy with a per-tool bind-strength floor of attested:placement (or higher) will DENY a forge deployed without WORKLOAD_IDENTITY_MODE=k8s_sa once enforcement is on. This is correct-by-design (an unattested workload shouldn’t clear a placement floor), but self-hosted / non-Kubernetes deployments must either run in k8s_sa mode or keep those floors at asserted (empty ranks equal to asserted, so an asserted floor does not deny).
Backwards compatibility
Both org_id and workspace_id use omitempty. Deployments that set neither env nor header keep emitting the pre-tenancy JSON shape verbatim. Consumers that ignore unknown keys continue to work unchanged. The audit schema version is not bumped — additive optional fields are schema-compatible per the documented policy.
Distinct from auth_verify.fields.org_id
The auth provider chain resolves an Identity.OrgID from the inbound bearer token (whatever the issuer claims) and stamps it on auth_verify.fields.org_id for back-compat. That value reflects the user’s org from their identity token.
The top-level org_id documented here is the deployment’s declared tenancy — the operator’s explicit assertion of where this agent runs. The two can differ legitimately (federated identity, cross-tenant invocation) and downstream consumers should treat them as independent signals. Both can be present on the same auth_verify event.
See also
- Audit Logging — full event catalog
- Workflow correlation IDs — the sibling FWS-2 header system (
X-Workflow-*) - Authentication — where
Identity.OrgIDcomes from