# Principal And Workspace Interoperability

Airlock governs commitments made through Snowflake. An external workspace may
host discussion, drafts, scheduled checks, and human or automated agents, but it
does not become an Airlock authorization layer.

## Identity Rules

1. Every mutating human or automated agent connects as an attributable
   Snowflake principal and receives only the Airlock assignments it needs.
2. When an agent acts for another principal, it uses an explicit Airlock
   delegation and passes `on_behalf_of_user`. Pass `delegation_id` only when
   Airlock reports more than one matching active grant.
3. Snowflake user type is metadata, not Airlock authority. Airlock does not
   require a special service-user class and does not grant additional authority
   because a session is human- or agent-active.
4. Workspace roles, channel membership, reactions, and approval conventions do
   not grant Airlock authority. Airlock roles, assignments, delegation,
   workflow policy, expectations, and application roles remain authoritative.
5. A shared principal must never hide multiple mutating actors. A read-only
   channel may have a dedicated scoped observer principal, but that principal
   must not be reused for commitments.

## Authority Matrix

| Principal pattern | Application role | Supported behavior |
| --- | --- | --- |
| Direct human or automated agent | `app_agent` | Discover assigned specs, read governed data when permitted, validate, load, attach, and move workflow within Airlock policy. |
| Delegated agent | `app_agent` | Perform only actions allowed by both the spec delegation policy and the active user-to-agent grant. |
| Scoped observer channel | `app_observer` | Observe metadata, activity, work, and state for explicitly assigned specs. It cannot read governed payload rows or mutate. |
| Global auditor | `app_observer` plus `global_observer` | Observe Airlock governance across specs without gaining governed payload or mutation access. |
| Administrator | `app_admin` | Change Airlock configuration and perform intentional administrative actions. |

Granting a Snowflake application role only exposes a procedure surface. Airlock
role assignments and policy still determine the principal's effective scope.

## Polling Contract

Use small state and work calls instead of repeatedly retrieving governed data.

| Question | Agent surface | Observer surface | Cursor behavior |
| --- | --- | --- | --- |
| Did this spec contract, visible dataset, or access scope change? | `agent.spec_state` or batched `agent.spec_states` | `observe.spec_state` or batched `observe.spec_states` | Compare each `STATE_TOKEN`. |
| Did this file data, workflow, attachment, or source-reference state change? | `agent.file_state` | `observe.file_state` | Compare `FILE_STATE_ID`. |
| What needs action or attention now? | `agent.list_my_work` | `observe.work` | Poll separately because deadlines can age without rotating a state token. |

State calls are authorization-scoped. A missing row is a terminal signal for
the current cursor: the object may not exist, may have been retired or deleted,
or may no longer be visible. The caller must return to normal discovery rather
than inferring which hidden condition occurred.

When a workspace already knows several spec names, prefer `spec_states` to poll
up to 100 under one role lens. It preserves first-request order after
case-insensitive deduplication and omits inaccessible or missing specs without
revealing which condition applied.

Polling cadence belongs to the workspace. Keep state-token checks inexpensive
and bounded, and poll time-sensitive work at the cadence justified by the
business process.

## Commitment Receipt

A workspace should store the result of each Airlock mutation in its own signed
or durable event history. The portable receipt shape is:

```json
{
  "schema": "airlock.commitment-receipt/v1",
  "procedure": "airlock.agent.load_data",
  "actor": {
    "snowflake_principal": "CSMITH",
    "on_behalf_of": "ASMITH",
    "delegation_id": "optional-when-known"
  },
  "target": {
    "spec_name": "reimbursements",
    "path": "asmith",
    "filename": "expense_2026_08_08"
  },
  "outcome": {
    "status": "ok",
    "code": "LOADED",
    "message": null,
    "issues": []
  },
  "committed_state": null,
  "operation_identity": {},
  "recorded_at": "2026-08-08T12:00:00Z"
}
```

The workspace supplies `actor.snowflake_principal` from the identity bound to
its Snowflake connection. It must not trust a chat display name as the actor.
The mutation result supplies the target and stable outcome fields. The receipt
adapter derives `on_behalf_of` and `delegation_id` from the actual procedure
arguments and rejects conflicting metadata.

After a successful direct file mutation, the adapter calls `agent.file_state`
with the returned `SPEC_NAME`, `PATH`/`FILE_PATH`, and
`FILENAME`/`FILE_FILENAME` to populate `committed_state`. It does not make that
read after delegated work because `agent.file_state` has no delegated call
shape; reading as the actor could fail or describe authority different from the
commitment. A workspace may perform a separate state poll only when that
connected principal independently has direct access. Attachment procedures
still populate `operation_identity.attachment_id` from their mutation result.

A denied mutation is still a useful receipt. Preserve its stable `CODE` and
`ISSUES`; do not manufacture a committed state or report workspace approval as
an Airlock commitment.

## Direct And Delegated Examples

Direct work uses the actor's own assignments:

```sql
CALL airlock.agent.load_data(
  spec_name => 'reimbursements',
  file_content => 'employee_id,amount\nCSMITH,42.50',
  filename => 'expense_2026_08_08'
);
```

Delegated work keeps the connected actor and principal distinct:

```sql
CALL airlock.agent.load_data(
  spec_name => 'reimbursements',
  file_content => 'employee_id,amount\nASMITH,42.50',
  filename => 'expense_2026_08_08',
  on_behalf_of_user => 'ASMITH'
);
```

The workspace records `CSMITH` as the connected actor and `ASMITH` as the
principal represented by the explicit delegation. It does not rewrite the
actor to make the event look direct.

## Reference Adapter

[`examples/workspace_interop`](https://github.com/reunionstudio/airlock/blob/main/examples/workspace_interop/README.md) contains a
standard-library Python adapter and fake-client proof. It demonstrates:

- polling agent or observer state without reading Airlock tables;
- treating an inaccessible state row as a terminal cursor result;
- invoking only an explicit allowlist of `agent.*` and intentional `admin.*`
  mutation procedures;
- hydrating a successful direct file mutation through `agent.file_state` while
  leaving delegated committed state unasserted; and
- producing bounded receipts without embedding governed payload data.

The adapter is a contract example, not an authentication library. A production
workspace remains responsible for secure Snowflake connection handling,
credential isolation, retry policy, and durable receipt storage.

## Runtime Decision

The current Airlock procedure surface is sufficient for this contract. No new
generic request-context parameter or shared integration identity is required.
An optional external deep link should be proposed separately only when a real
workspace proof shows that the stored Airlock receipt, declared business keys,
governed attachments, source references, and workflow comments are inadequate.
