Skip to content

Tenancy stamping: stamp org_id / workspace_id on every audit event from env + headers #157

@initializ-mk

Description

@initializ-mk

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions