Symptom
A Forge agent deployed into a multi-tenant platform (initializ or any
SIEM-fronted environment) has no clean way to assert "this agent run
belongs to org X and workspace Y" on its audit stream. The
information is half-present and never reaches downstream consumers:
- `Identity.OrgID` is resolved by the auth provider and stamped only
on the `auth_verify` event's `fields.org_id`. Every other event
(`session_start`, `llm_call`, `guardrail_check`,
`invocation_complete`, `tool_exec`, etc.) omits it entirely.
- `Identity.WorkspaceID` is a field on the auth struct
(`forge-core/auth/provider.go:50`) that is never emitted on any
audit event.
- The env vars `FORGE_AGENT_ID` / `FORGE_ORG_ID` are read only by
the guardrails-library bootstrap in `BuildGuardrailChecker`. The
audit pipeline doesn't see them.
A SIEM consumer asking "show me every event for workspace W in org O"
currently has to join against the `auth_verify` row's `fields.org_id`
via `correlation_id`, and for workspace there's no source at all.
Proposal
Two-layer tenancy stamp, mirroring how the existing FWS-2 workflow
correlation headers work.
Layer 1 — Deployment-time stamp (the static case)
Static tenancy: the agent is deployed into a workspace and an org;
the values do not change per request. Read once at runner startup,
stashed on the `AuditLogger` itself, stamped on every emit.
| Env var |
Audit field |
| `FORGE_ORG_ID` |
top-level `org_id` on every event |
| `FORGE_WORKSPACE_ID` |
top-level `workspace_id` on every event |
The initializ platform deployment manifest sets these per-tenant:
```yaml
env:
- name: FORGE_ORG_ID
value: "org_abc123"
- name: FORGE_WORKSPACE_ID
value: "ws_xyz789"
```
Every emitted event — including startup banners (`agent_card_published`,
`policy_loaded`, `audit_export_status`) — carries `org_id` and
`workspace_id` at the top level. SIEM filter:
`org_id = "org_abc123" AND workspace_id = "ws_xyz789"`.
Layer 2 — Per-request override (the multi-tenant routing case)
For deployments where one Forge agent serves multiple workspaces and
the orchestrator routes per request, two headers override the env stamp:
| Header |
Top-level field |
Behavior |
| `X-Forge-Org-ID` |
`org_id` |
overrides env when present |
| `X-Forge-Workspace-ID` |
`workspace_id` |
overrides env when present |
Picked up at the A2A request boundary alongside the workflow headers,
stashed in ctx via a `TenancyContext` mirroring `WorkflowContext`.
`EmitFromContext` uses ctx-first, logger-fallback (env stamp).
Schema impact
Two new top-level `omitempty` keys on `AuditEvent`:
```go
type AuditEvent struct {
// ... existing fields
OrgID string `json:"org_id,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
}
```
Additive only — no schema version bump. Existing consumers that ignore
unknown keys continue to work; the events that previously had
`org_id` inside `auth_verify.fields` keep it there for back-compat
and ALSO get the top-level stamp.
Files
| File |
Change |
| `forge-core/runtime/audit.go` |
Add `OrgID`/`WorkspaceID` to `AuditEvent`; add `WithTenancy(orgID, workspaceID)` setter on `AuditLogger`; teach `EmitFromContext` to merge ctx-first / logger-fallback |
| `forge-core/runtime/tenancy.go` (new) |
`TenancyContext{OrgID, WorkspaceID}`, `WithTenancyContext`, `TenancyContextFromHTTPHeaders` — direct mirror of `workflow.go` |
| `forge-cli/runtime/runner.go` |
Read env at startup, call `auditLogger.WithTenancy(…)`; pick up tenancy headers at every A2A request boundary alongside `WorkflowContextFromHTTPHeaders` |
| `docs/security/audit-logging.md` |
New "Tenancy stamping" section explaining env + header layers |
| Tests |
env stamps applied, header overrides env, both absent → keys omitted (back-compat) |
Out of scope
- Auto-deriving `org_id` from `Identity.OrgID` for the top-level stamp. The
auth-derived value continues to live in `auth_verify.fields.org_id`
for back-compat. Deployment env + per-request header are the
sources of truth for the new top-level field — they're the
operator's explicit declarations, not whatever an inbound token
claimed.
- Cross-stamp on outbound A2A calls (agent-to-agent). The workflow
headers already propagate; tenancy headers can be added in a
follow-up if needed for hierarchical multi-tenancy.
Symptom
A Forge agent deployed into a multi-tenant platform (initializ or any
SIEM-fronted environment) has no clean way to assert "this agent run
belongs to org X and workspace Y" on its audit stream. The
information is half-present and never reaches downstream consumers:
on the `auth_verify` event's `fields.org_id`. Every other event
(`session_start`, `llm_call`, `guardrail_check`,
`invocation_complete`, `tool_exec`, etc.) omits it entirely.
(`forge-core/auth/provider.go:50`) that is never emitted on any
audit event.
the guardrails-library bootstrap in `BuildGuardrailChecker`. The
audit pipeline doesn't see them.
A SIEM consumer asking "show me every event for workspace W in org O"
currently has to join against the `auth_verify` row's `fields.org_id`
via `correlation_id`, and for workspace there's no source at all.
Proposal
Two-layer tenancy stamp, mirroring how the existing FWS-2 workflow
correlation headers work.
Layer 1 — Deployment-time stamp (the static case)
Static tenancy: the agent is deployed into a workspace and an org;
the values do not change per request. Read once at runner startup,
stashed on the `AuditLogger` itself, stamped on every emit.
The initializ platform deployment manifest sets these per-tenant:
```yaml
env:
value: "org_abc123"
value: "ws_xyz789"
```
Every emitted event — including startup banners (`agent_card_published`,
`policy_loaded`, `audit_export_status`) — carries `org_id` and
`workspace_id` at the top level. SIEM filter:
`org_id = "org_abc123" AND workspace_id = "ws_xyz789"`.
Layer 2 — Per-request override (the multi-tenant routing case)
For deployments where one Forge agent serves multiple workspaces and
the orchestrator routes per request, two headers override the env stamp:
Picked up at the A2A request boundary alongside the workflow headers,
stashed in ctx via a `TenancyContext` mirroring `WorkflowContext`.
`EmitFromContext` uses ctx-first, logger-fallback (env stamp).
Schema impact
Two new top-level `omitempty` keys on `AuditEvent`:
```go
type AuditEvent struct {
// ... existing fields
OrgID string `json:"org_id,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
}
```
Additive only — no schema version bump. Existing consumers that ignore
unknown keys continue to work; the events that previously had
`org_id` inside `auth_verify.fields` keep it there for back-compat
and ALSO get the top-level stamp.
Files
Out of scope
auth-derived value continues to live in `auth_verify.fields.org_id`
for back-compat. Deployment env + per-request header are the
sources of truth for the new top-level field — they're the
operator's explicit declarations, not whatever an inbound token
claimed.
headers already propagate; tenancy headers can be added in a
follow-up if needed for hierarchical multi-tenancy.