> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Give Devin Just-in-Time Database Access

> Let Devin query a Connector-protected database with OIDC, after a human approves each session in Slack

export const G = ({term, anchor, children}) => {
  const href = anchor ? `/docs/glossary/index#${anchor}` : `/docs/glossary/index`;
  return <a href={href} className="glossary-link" style={{
    textDecoration: "underline",
    textDecorationLine: "underline",
    textDecorationColor: "#6b7280",
    textDecorationThickness: "1px",
    textUnderlineOffset: "2px",
    color: "inherit",
    transition: "text-decoration-color 0.2s ease",
    borderBottom: "none"
  }} onMouseEnter={e => e.target.style.textDecorationColor = "#fff"} onMouseLeave={e => e.target.style.textDecorationColor = "#6b7280"}>
  {children || term}
</a>;
};

## Overview

You may want Devin to query a database while it debugs or answers a question.
A shared database password in the Devin environment would give every session
standing access, with no record of who asked for it.

With Formal, Devin authenticates with its own short-lived OIDC token. Formal
maps the token to a machine user and to the human who started the session. A
<G anchor="policy">policy</G> blocks the machine user by default. When Devin
needs data, it requests access, and the requester approves it in Slack. The
approval suspends the block for that requester and Devin session only, and
expires after the requested duration.

```mermaid theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
sequenceDiagram
  participant Devin
  participant Formal as Formal Control Plane
  participant Slack
  participant Connector as Formal Connector
  participant DB as Database

  Devin->>Formal: CreateWorkflowTrigger with Devin OIDC token
  Formal->>Slack: Ask the session requester
  Slack-->>Formal: Approve
  Formal->>Formal: Create policy suspension
  Devin->>Formal: CreateFederationToken with Devin OIDC token
  Formal-->>Devin: Short-lived username and token
  Devin->>Connector: psql with federation token
  Connector->>DB: Allowed, masked, and logged session
```

## Prerequisites

* A Postgres <G anchor="resource">resource</G> behind a Formal
  <G anchor="connector">Connector</G> listener that the Devin VM can reach
* A Slack integration in [Integrations](https://app.formal.ai/integrations), with every Devin
  user in your Slack workspace
* A Formal user for every Devin user, with the same email address
* Permission to create machine users, OIDC integrations, Permissions,
  policies, and workflows in Formal
* Permission to edit your Devin environment

## Create the machine users

Go to [Users](https://app.formal.ai/users) and create two machine users:

| Machine user | Used by |
| - | - |
| `devin` | Devin sessions, through the OIDC integration |
| `workflow` | The approval workflow, to create policy suspensions |

Copy both `user_...` IDs. Keep the workflow actor separate from Devin, so
Devin can never create its own suspension.

## Configure the OIDC integration

Go to [OIDC Integrations](https://app.formal.ai/oidc-integrations) and click
**Create Integration**. Select the **Devin** preset and the `devin` machine
user.

<Steps>
  <Step title="Restrict the claim condition">
    Replace `your-org-id` in the preset claim condition with your Devin
    organization ID:

    ```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    claims.org_id == "<DEVIN_ORG_ID>"
    ```

    See [Devin's OIDC documentation](https://docs.devin.ai/product-guides/oidc)
    for all available claims.
  </Step>

  <Step title="Map the session requester">
    Keep the preset **End-User Email Expression**:

    ```cel theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    claims.requesting_user_email
    ```

    Formal resolves this email to a Formal user when Devin mints a federation
    token. The Connector then logs and evaluates each session with that person
    as the <G anchor="end-user-identity">end user</G>.
  </Step>

  <Step title="Copy the audience">
    Copy the generated audience, `oidc.formal.ai/integrationoidc_...`. Devin
    requests OIDC tokens for this audience.
  </Step>
</Steps>

## Restrict Control Plane access

Devin only needs to request access and mint federation tokens. The workflow
actor only needs to create policy suspensions. Go to
[Permissions](https://app.formal.ai/permissions), click **Create Permission**,
name it `devin-database-access`, and paste:

```rego theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
package formal.app

import future.keywords.if
import future.keywords.in

default allow = true

allow = false if {
  input.user.id == "<DEVIN_MACHINE_USER_ID>"
  not input.app.command.name in {"CreateFederationToken", "CreateWorkflowTrigger"}
}

allow = false if {
  input.user.id == "<WORKFLOW_MACHINE_USER_ID>"
  input.app.command.name != "CreatePolicySuspension"
}
```

Set the Permission to **Active**.

## Block Devin by default

Go to [Policies](https://app.formal.ai/policies), click **Create Policy**, name
it `devin-default-block`, and paste:

```rego theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
package formal.v2

import future.keywords.if

block := {
  "action": "block",
  "type": "block_with_formal_message",
  "reason": "Devin is blocked by default; request JIT access",
}

session := block if {
  input.user.id == "<DEVIN_MACHINE_USER_ID>"
}

request := block if {
  input.user.id == "<DEVIN_MACHINE_USER_ID>"
}
```

Set the policy to **Active** and copy its `policy_...` ID. Approved access
still goes through your other policies, so masking and read-only rules keep
applying to Devin.

## Create the approval workflow

Go to [Workflows](https://app.formal.ai/workflows), click **Create Workflow**,
name it `devin-database-access`, and paste:

```yaml theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
trigger:
  type: api-request
  name: devin_database_access_request
  args:
    allow: "user.id == '<DEVIN_MACHINE_USER_ID>' && has(user.oidc) && has(user.oidc.claims.requesting_user_email) && has(user.oidc.claims.devin_id)"

actions:
  - name: ask_end_user
    type: ask-in-chat
    args:
      integration: slack
      recipient_email: ${{ trigger.user.oidc.claims.requesting_user_email }}
      message: |-
        Approve temporary database access for Devin acting as `${{ trigger.user.oidc.claims.requesting_user_email }}`?
        Devin session: `${{ trigger.user.oidc.claims.devin_id }}`
        Reason: ${{ trigger.payload.reason }}
        Requested duration: ${{ trigger.payload.expiration_minutes }} minutes

  - name: suspend_block_policy
    type: formal-app-command
    depends_on: [ask_end_user]
    if: "actions.ask_end_user.response == 'yes'"
    args:
      app: Policies
      command:
        name: PolicySuspension
        type: Create
      machine_user_id: "<WORKFLOW_MACHINE_USER_ID>"
      input:
        policy_id: "<BLOCK_POLICY_ID>"
        identity_type: user
        identity_id: ${{ trigger.user.id }}
        input_condition: 'input.end_user.email == "${{ trigger.user.oidc.claims.requesting_user_email }}" && input.user.oidc.claims.devin_id == "${{ trigger.user.oidc.claims.devin_id }}"'
        oneoff: false
        expiration_minutes: ${{ trigger.payload.expiration_minutes }}
        reason: "${{ trigger.payload.reason }}"

  - name: notify_approved
    type: send-slack-message
    depends_on: [suspend_block_policy]
    if: "has(actions.suspend_block_policy.status_code) && actions.suspend_block_policy.status_code == 200"
    args:
      recipient_email: ${{ trigger.user.oidc.claims.requesting_user_email }}
      text: |-
        Devin database access approved.
        Access expires in ${{ trigger.payload.expiration_minutes }} minutes.

  - name: notify_suspend_failed
    type: send-slack-message
    depends_on: [suspend_block_policy]
    if: "has(actions.suspend_block_policy.status_code) && actions.suspend_block_policy.status_code != 200"
    args:
      recipient_email: ${{ trigger.user.oidc.claims.requesting_user_email }}
      text: |-
        Failed to grant Devin database access. Status: `${{ actions.suspend_block_policy.status_code }}`
```

Replace the three placeholders, then set the workflow to **Active** and copy
its `workflow_...` ID.

The suspension condition uses only verified token claims: the requester's
email and the Devin session (`devin_id`). Never interpolate payload fields into
`input_condition`, since Devin controls the payload and could inject CEL. Devin
chooses the reason and duration, and the approver sees both before approving.
A new Devin session needs a new approval.

<Tip>
  To manage this setup with Terraform, use `formal_user`,
  `formal_integration_oidc`, `formal_permission`, `formal_policy`, and
  `formal_workflow`. Inside a Terraform heredoc, escape workflow expressions as
  `$${{ ... }}`.
</Tip>

## Configure the Devin environment

<Steps>
  <Step title="Install the OIDC helper and clients">
    Add these steps to the `initialize` section of your
    [environment blueprint](https://docs.devin.ai/onboard-devin/environment-yaml):

    ```yaml theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    initialize:
      - uses: github.com/CognitionAI/actions/setup-devin-oidc@main
      - run: sudo apt-get update && sudo apt-get install --yes postgresql-client jq
    ```

    The action installs the `devin-oidc` CLI, which exchanges the session
    token for an audience-scoped OIDC token.
  </Step>

  <Step title="Reach the Connector">
    The Devin VM must reach the Connector listener. If the Connector is on a
    private network, connect the VM to it, for example with a Tailscale subnet
    router.
  </Step>

  <Step title="Add the access script">
    Commit `scripts/formal-db.sh` to the repository Devin works in:

    ```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    #!/usr/bin/env bash
    set -euo pipefail

    FORMAL_OIDC_AUDIENCE="oidc.formal.ai/<INTEGRATION_ID>"
    FORMAL_WORKFLOW_ID="<WORKFLOW_ID>"
    FORMAL_CONNECTOR_HOST="<CONNECTOR_HOST>"
    FORMAL_CONNECTOR_PORT="5432"
    FORMAL_DATABASE="<DATABASE>"

    # Reads the token from stdin so it never appears in a process list.
    formal_api() {
      local token
      token="$(devin-oidc token --audience "${FORMAL_OIDC_AUDIENCE}")"
      printf 'header = "Authorization: Bearer %s"\n' "${token}" |
        curl --silent --show-error --fail-with-body --config - \
          --header "Content-Type: application/json" \
          --data "$2" \
          "https://api.formal.ai/core.v1.$1"
    }

    case "${1:-}" in
    request)
      formal_api WorkflowService/CreateWorkflowTrigger "$(jq -n \
        --arg workflow_id "${FORMAL_WORKFLOW_ID}" \
        --arg reason "${2:?usage: formal-db.sh request <reason> [minutes]}" \
        --argjson minutes "${3:-15}" \
        '{workflow_id: $workflow_id, payload: {reason: $reason, expiration_minutes: $minutes}}')"
      ;;
    psql)
      shift
      federation="$(formal_api ConnectorService/CreateFederationToken '{}')"
      PGPASSWORD="$(jq -r .token <<<"${federation}")" \
        exec psql "host=${FORMAL_CONNECTOR_HOST} port=${FORMAL_CONNECTOR_PORT} dbname=${FORMAL_DATABASE} sslmode=require user=$(jq -r .username <<<"${federation}")" "$@"
      ;;
    *)
      echo "usage: formal-db.sh request <reason> [minutes] | psql [psql args]" >&2
      exit 2
      ;;
    esac
    ```

    Replace the placeholders. If the listener serves more than one resource,
    set `<DATABASE>` to `<DATABASE>@<RESOURCE_NAME>`.

    The script mints a new federation token for each connection. Devin OIDC
    tokens are short-lived, and a federation token never outlives the OIDC
    token it came from.
  </Step>

  <Step title="Tell Devin how to use it">
    Add instructions like these to Devin, for example as
    [Knowledge](https://docs.devin.ai/product-guides/knowledge):

    ```text theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
    To query the <DATABASE> database, run scripts/formal-db.sh psql -c '<SQL>'.
    If Formal blocks the connection, run
    scripts/formal-db.sh request '<short, task-specific reason>' 15
    and message me to approve the request in my Slack DM from the Formal app.
    Retry the query after I approve. Never print tokens or passwords.
    ```
  </Step>
</Steps>

## Verify the setup

Start a new Devin session and ask it to run:

```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
scripts/formal-db.sh psql -c 'SELECT 1'
```

The Connector should reject the connection with
`Devin is blocked by default; request JIT access`. Then run:

```bash theme={"languages":{"custom":["/languages/cel.json","/languages/rego.json"]}}
scripts/formal-db.sh request 'verify Formal JIT access' 5
# Expected: {"triggered":true,"runId":"..."}
```

You should receive a Slack DM from the Formal app. Click **Yes**, wait for the
approval message, then run the `SELECT 1` again. It should return one row.

Open [Logs](https://app.formal.ai/logs). The session should show the `devin`
machine user, your email as the end user, and the Devin claims under
`user.oidc.claims`. The suspension appears in
[Policy Suspensions](https://app.formal.ai/policies?tab=suspensions) until it
expires.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Formal API calls return 401">
    Confirm the integration is **Active** and its issuer is
    `https://app.devin.ai`. Confirm the script's audience matches the
    integration audience exactly. Confirm the token's `org_id` matches the
    claim condition.
  </Accordion>

  <Accordion title="The workflow returns triggered: false">
    The `allow` expression rejected the caller. Confirm the machine user ID in
    the expression is the integration's machine user.
  </Accordion>

  <Accordion title="CreateFederationToken returns failed precondition">
    The requester's email has no matching Formal user. Create the Formal user
    or fix the email, then retry.
  </Accordion>

  <Accordion title="The connection is still blocked after approval">
    Check that the workflow run created a suspension. The suspension covers
    only the requesting Devin session, so a new session needs a new request.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="OIDC Integrations" icon="id-card" href="/docs/guides/integrations/oidc">
    Configure claims and federated access
  </Card>

  <Card title="Workflows" icon="diagram-project" href="/docs/guides/configuration/workflows">
    Build other approval flows
  </Card>

  <Card title="Policy Suspensions" icon="clock-rotate-left" href="/docs/guides/policies/suspensions">
    Review and revoke approved access
  </Card>

  <Card title="Block Agent Auto-Merge" icon="cloud" href="/docs/guides/example-use-cases/formal-endpoint-cloud-agents">
    Govern Devin's GitHub access with the Formal Endpoint
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.