Build Governed LangGraph Agent
From authorization to execution assurance and causal investigation. This runnable banking lab changes a loan from PENDING to APPROVED only after AIGP allows the exact operation.
The released lab resolves bhanuj-governance-plugin-api==1.1.1 and the adapter's public v0.1.0 GitHub tag. It does not depend on an editable adapter checkout.
What you will build
- Issue a trusted delegation and authorize the protected loan operation.
- Mutate SQLite only after an
ALLOW, then report a bounded receipt. - Simulate mutation-then-crash and reconcile it without repeating the mutation.
- Check replay eligibility, then run an isolated reference counterfactual when the required policy and worker are available.
- Inspect the authorization β receipt β manifest β replay β causal-audit lineage.
1. Install the LAB and Versioned Replay Adapter
git clone https://github.com/Bhanuj-AI/labs.git
cd labs/agent-frameworks/langgraph/langgraph-execution-assurance-lab
python -m venv .venv && source .venv/bin/activate
python -m pip install --upgrade -r requirements.txt
# The requirements file installs the tagged adapter release, not an editable checkout.
python -c "from importlib.metadata import version; assert version('aigp-langgraph-replay-adapter') == '0.1.0'"
export AIGP_LANGGRAPH_REPLAY_SNAPSHOT_DB=/tmp/aigp-langgraph-private-snapshots.dbStart a compatible OSS API before continuing. The lab makes real API calls; it does not create policies or fake authorization responses.
2. Obtain "Actor" identities & short-lived tokens
Do not invent values such as runtime-service or reuse placeholder tokens. The local stack creates Keycloak service-account subjects dynamically. The actor-ID script writes their real IDs into a generated file, and those exact IDs must be used in both Studio policies and the lab terminal.
Issuer identity
AI_GOVERNANCE_WALKTHROUGH_ACTOR_ID is the authenticated identity that creates the execution-scoped delegation. Its token is used only for delegation issuance and revocation.
Runtime identity
AI_GOVERNANCE_SYNTHETIC_RUNTIME_ACTOR_ID is the local stack's least-privilege external-runtime identity. It creates runtime evidence and reads replay eligibility. It is not the issuer.
# Run from the OSS repository after the local Keycloak stack is ready.
cd /path/to/ai-governance-oss
./scripts/keycloak/get-service-account-actorids.sh
# Load generated identities, the local OAuth clients, and the local runtime secret.
set -a
source .env.oauth.generated
source .env.service-accounts.generated
source keycloak-postgres/.env.keycloak
set +a
# The walkthrough account issues delegations. The dedicated external-runtime
# account runs the lab and has the least-privilege replay/causal-audit reads.
export AIGP_ISSUER_ACTOR_ID="$AI_GOVERNANCE_WALKTHROUGH_ACTOR_ID"
export AIGP_RUNTIME_ACTOR_ID="$AI_GOVERNANCE_SYNTHETIC_RUNTIME_ACTOR_ID"
export AIGP_BASE_URL=http://localhost:8000
export AIGP_ORGANIZATION_ID=org_default
export AIGP_PROJECT_ID=project_default
# Create short-lived tokens; never paste or commit their values.
export AIGP_ISSUER_TOKEN="$(
uv run python -c 'from ai_governance.oauth.client_credentials import access_token_from_environment; print(access_token_from_environment())'
)"
export AIGP_RUNTIME_TOKEN="$(
AI_GOVERNANCE_OAUTH_TOKEN_URL="$AI_GOVERNANCE_OAUTH_TOKEN_URL" \
AI_GOVERNANCE_OAUTH_CLIENT_ID=synthetic-agent-runtime \
AI_GOVERNANCE_OAUTH_CLIENT_SECRET="${SYNTHETIC_RUNTIME_CLIENT_SECRET:-local-synthetic-runtime-client-secret}" \
uv run python -c 'from ai_governance.oauth.client_credentials import access_token_from_environment; print(access_token_from_environment())'
)"
# Then move to the lab directory. The printed IDs are the values used in Studio.
printf 'Issuer: %s\nRuntime: %s\n' "$AIGP_ISSUER_ACTOR_ID" "$AIGP_RUNTIME_ACTOR_ID"
cd /path/to/labs/agent-frameworks/langgraph/langgraph-execution-assurance-labRun the token block again immediately before a live session if a token expires. Never save, print, paste, or commit either bearer token. The two printed actor IDs are safe identifiers for Studio policy fields; they are not credentials.
3. Create Policy 1: Delegation Issuance Policy
This policy answers one question: may this issuer create a short-lived delegation for this runtime, action, and loan resource? It does not authorize the SQLite mutation itself. Create it first and activate it before attempting any delegation.
Definition fields
Name: Lab: issue loan-approval delegation
Category: SECURITY
Target scope: Agent execution
Owner and created-by: your integration administrator.
Rule fields
Effect: APPROVE
Priority: 10
Severity: HIGH
Reason: Allow the lab issuer to delegate this loan approval to the runtime.
{
"runtime_action_delegation_issuance": {
"purpose": "runtime_action_delegation_issuance"
}
}| Field | Operator | Expected value | Why it is present |
|---|---|---|---|
| issuer.id | EQUALS | $AIGP_ISSUER_ACTOR_ID | The server-authenticated issuer that is allowed to create the delegation. |
| runtime_actor.id | EQUALS | $AIGP_RUNTIME_ACTOR_ID | The runtime that will receive, but cannot broaden, the delegation. |
| action.id | EQUALS | banking.loan.approve | The only external action delegated by this example. |
| resource.type | EQUALS | sqlite.row | The bounded kind of external resource. |
| resource.reference | EQUALS | loan:LOAN-001 | The one loan row that this delegation may cover. |
Do not use actor.id or delegation.* here. At issuance time no delegation exists, and the evidence paths are issuer.id and runtime_actor.id.
Add the delegation-issuance metadata before authoring the rule.
Enter the five bounded conditions above.
Review the exact scope before activation; this policy governs delegation creation, not a business mutation.
The issuer cannot create a delegation until this version is active.
4. Create Policy 2: Runtime-Action Authorization Policy
This policy is evaluated when the runtime proposes the protected SQLite write. The PDP resolves the saved delegation first, then applies this policy to the exact runtime, action, and resource. A matching delegation alone does not authorize the mutation.
Definition fields
Name: Lab: authorize loan approval
Category: SECURITY
Target scope: Agent execution
Owner and created-by: your integration administrator.
Rule fields
Effect: APPROVE
Priority: 10
Severity: HIGH
Reason: Allow the delegated runtime to approve the specified loan.
{
"runtime_action_authorization": {
"purpose": "runtime_action_authorization",
"registered_actions": ["banking.loan.approve"],
"registered_resource_types": ["sqlite.row"]
}
}| Field | Operator | Expected value | Why it is present |
|---|---|---|---|
| actor.id | EQUALS | $AIGP_RUNTIME_ACTOR_ID | The authenticated runtime proposing this operation. |
| delegation.issuer_id | EQUALS | $AIGP_ISSUER_ACTOR_ID | The issuer resolved from the stored delegation, not a caller claim. |
| action.id | EQUALS | banking.loan.approve | The registered protected operation. |
| resource.type | EQUALS | sqlite.row | The bounded kind of external resource. |
| resource.reference | EQUALS | loan:LOAN-001 | The one loan row this policy approves. |
Do not add delegation.id: the delegation is created anew for each execution. The PDP resolves its immutable ID, version, digest, expiry, revocation state, scope, and issuer from the tenant-scoped store.
Register banking.loan.approve and sqlite.row in the definition metadata.
Enter the runtime, issuer-backed delegation, action, and resource conditions above.
The runtime receives ALLOW only while a matching policy version is active.
Policy 1 permits delegation issuance; Policy 2 permits a runtime action after delegation resolution.
5. Run Scenarios
mkdir -p evidence
set -o pipefail
python run_lab.py authorized --database /tmp/aigp-banking.db | tee evidence/authorized.json
python run_lab.py unauthorized --database /tmp/aigp-banking-denied.db | tee evidence/unauthorized.json
python run_lab.py revoked --database /tmp/aigp-banking-revoked.db | tee evidence/revoked.json
# Run recovery only after crash has written the state file.
python run_lab.py crash --database /tmp/aigp-banking-recovery.db --state /tmp/aigp-recovery.json | tee evidence/crash.json
python run_lab.py recover --database /tmp/aigp-banking-recovery.db --state /tmp/aigp-recovery.json | tee evidence/recovery.json
python run_lab.py invalid-manifest --database /tmp/aigp-banking-invalid.db | tee evidence/invalid-manifest.json
PYTHONPATH=. python -m pytest -q | tee evidence/pytest.txtThe crash scenario can leave the banking database approved while the receipt is uncertain. Recovery reads runtime-owned idempotency evidence and submits a terminal receipt; it does not call the mutation again. Each tee file contains bounded IDs, statuses, and digestsβnot credentials or private tool payloads.
Ownership diagram: AIGP authorizes and preserves bounded evidence; the runtime and banking application own execution, transactionality, idempotency, and recovery. This is a conceptual guide to the observed scenarios, not independent proof of an external mutation.
6. Start Evidence Review
Open Studio's Agents Runtime β Executions page first. It is the inventory: use the execution ID or correlation ID from a saved JSON file to find the scenario before opening a single execution. Compare denied, revoked, crashed, and authorized runs here before drilling into their evidence.
Find the execution by its printed ID or correlation ID, then open the selected run to inspect its decision and runtime-reported evidence.
AIGP holds the governance evidence and lineage. It does not own or reopen the banking database, and a receipt remains an authenticated runtime attestation rather than independent proof of a SQLite mutation.
7. Scenario Results
These are compact, identifier-redacted terminal results from the local lab. Click a screenshot to inspect its full evidence view. They demonstrate authorization, denial, revocation, explicit uncertainty after a mutation, and fail-closed manifest validation.
Authorized operation: ALLOW, mutation, receipt, and manifest
The protected mutation changes the banking row only after an ALLOW. The receipt and manifest are recorded, while causal audit remains gated until an intervention policy is configured.
The SQLite loan changes from PENDING to APPROVED after ALLOW_POLICY_MATCHED, then the execution records tool and receipt evidence.
"after": "APPROVED",
"before": "PENDING",
"causal_audit_eligibility": {
"auditable_tool_call_count": 1,
"code": "INTERVENTION_POLICY_NOT_CONFIGURED",
"reason": "INTERVENTION_POLICY_NOT_CONFIGURED",
"replay_manifest_hash": "ffccd926926a2b788fb38b29dff4108bea0138a287be0ad854c13f6143d93da4"
},
"correlation_id": "banking-assurance-76b8e9c201344e7f9041d178e1094ecc",
"decision": {
"action_id": "banking.loan.approve",
"actor_id": "7775f8bf-01f8-43dc-9236-4779896f407e",
"correlation_id": "banking-assurance-76b8e9c201344e7f9041d178e1094ecc",
"decision_at": "2026-10-26T05:30:35.542185Z",
"decision_id": "1acb6f83-5758-49c4-94f6-6d2b64f601d9",
"delegation_digest": "sha256:7206a04c72e960ffc519871129670b8099bf36ca49555c0e7bb54888415ab0c2",
"delegation_id": "a3045db9-0aeb-4ef6-8689-a0c8183cb8f1",
"delegation_version": "1",
"evaluated_policies": [
{
"policy_id": "40a0996f-5dc8-4e17-be60-f6b327a89834",
"version": "2"
}
],
"execution_id": "f348c0d0-e199-4d3e-ace3-a5b5175f5557",
"expires_at": "2026-10-26T05:35:35.542185Z",
"idempotency_key": "banking-assurance-76b8e9c201344e7f9041d178e1094ecc:authorize",
"outcome": "ALLOW",
"policy_id": "40a0996f-5dc8-4e17-be60-f6b327a89834",
"policy_version": "2",
"reason_code": "ALLOW_POLICY_MATCHED",
"request_fingerprint": "251f8926f71404dbe546dbbee8deefef2604a4f66abefbf86fceefbbbf34a517",
"resource": {
"resource_reference": "loan:LOAN-001",
"resource_type": "sqlite.row"
},
"schema_version": "runtime-action-authorization/v2",
"tool_arguments_digest": "sha256:31f06ca010abf4c3fadf3a7fc7b7d9d7bcf29b3a463b58e09985174490ba14dc"
},
"execution_id": "f348c0d0-e199-4d3e-ace3-a5b5175f5557",
"private_snapshot_database": "authorized-3-private-snapshots.db",
"scenario": "authorized"
}Unauthorized scope: DENY, no mutation
The delegation does not cover loan:LOAN-UNAUTHORIZED, so the database remains PENDING.
{
"after": "PENDING",
"before": "PENDING",
"correlation_id": "banking-assurance-55fde1af1a3c43dea79386575ab7117c",
"decision": {
"action_id": "banking.loan.approve",
"actor_id": "7775f8bf-01f8-43dc-9236-4779896f407e",
"correlation_id": "banking-assurance-55fde1af1a3c43dea79386575ab7117c",
"decision_at": "2026-10-26T05:33:32.994048Z",
"decision_id": "eeb6711e-2985-4623-9faf-3cd416089b60",
"delegation_digest": null,
"delegation_id": null,
"delegation_version": null,
"evaluated_policies": [],
"execution_id": "51c1ca1a-e5a9-4333-a98e-433e5e49fde0",
"expires_at": "2026-10-26T05:38:32.994048Z",
"idempotency_key": "banking-assurance-55fde1af1a3c43dea79386575ab7117c:authorize",
"outcome": "DENY",
"policy_id": null,
"policy_version": null,
"reason_code": "DELEGATION_SCOPE_DENIED",
"request_fingerprint": "cc1a3936b049d374efbdbffbe3360a5d6ede2cc54c9db7c12f8b3d516a80ca06",
"resource": {
"resource_reference": "loan:LOAN-UNAUTHORIZED",
"resource_type": "sqlite.row"
},
"schema_version": "runtime-action-authorization/v2",
"tool_arguments_digest": "sha256:12f6523f27f038e465d5bb2e33efb66aa04a66df65883aff2004c89c6b494c01"
},
"execution_id": "51c1ca1a-e5a9-4333-a98e-433e5e49fde0",
"scenario": "unauthorized"
}
Revoked delegation: DENY, no mutation
Revocation blocks a new authorization decision; it does not rewrite delegation history.
{
"after": "PENDING",
"before": "PENDING",
"correlation_id": "banking-assurance-097d3964e4e6478da60cec4a552d8e84",
"decision": {
"action_id": "banking.loan.approve",
"actor_id": "7775f8bf-01f8-43dc-9236-4779896f407e",
"correlation_id": "banking-assurance-097d3964e4e6478da60cec4a552d8e84",
"decision_at": "2026-10-26T05:33:42.297308Z",
"decision_id": "596dd11a-8f29-4bd2-938f-46ac498db9f1",
"delegation_digest": null,
"delegation_id": null,
"delegation_version": null,
"evaluated_policies": [],
"execution_id": "816af0d1-4e75-4f81-b8a1-e5cb2376870f",
"expires_at": "2026-10-26T05:38:42.297308Z",
"idempotency_key": "banking-assurance-097d3964e4e6478da60cec4a552d8e84:authorize",
"outcome": "DENY",
"policy_id": null,
"policy_version": null,
"reason_code": "DELEGATION_REVOKED",
"request_fingerprint": "c36bbe80ae13fd20dbb7204290393169f5365a06849a3cf2f29720459aeb8e59",
"resource": {
"resource_reference": "loan:LOAN-001",
"resource_type": "sqlite.row"
},
"schema_version": "runtime-action-authorization/v2",
"tool_arguments_digest": "sha256:31f06ca010abf4c3fadf3a7fc7b7d9d7bcf29b3a463b58e09985174490ba14dc"
},
"execution_id": "816af0d1-4e75-4f81-b8a1-e5cb2376870f",
"scenario": "revoked"
}Mutation then crash: explicit uncertainty, not an automatic retry
The external SQLite mutation completed, but the runtime reports uncertainty. The separate recovery command must reconcile external state and supersede the UNKNOWN receipt without calling the mutation again.
The banking row is APPROVED while recovery state preserves the execution and uncertain-receipt linkage.
{
"after": "APPROVED",
"before": "PENDING",
"correlation_id": "banking-assurance-b63c514c79f74b33a8c5830e9fa2dd97",
"execution_id": "196e3611-d64a-4edb-9503-9f45c68ab53f",
"recovery_state": "/tmp/aigp-recovery.json",
"scenario": "mutation-then-crash"
}Invalid manifest: replay rejected before execution
The lab intentionally omits the required replay capability. AIGP classifies the execution as not replayable, and the original banking database remains untouched.
NOT_REPLAYABLE is an eligibility result; it never triggers a replay attempt.
{
"after": "PENDING",
"before": "PENDING",
"correlation_id": "banking-assurance-083b961c72a74404881c31a0791f9ecb",
"eligibility": {
"auditable_tool_call_count": 0,
"code": "NOT_REPLAYABLE",
"reason": "Observed execution replay capability is invalid.",
"replay_manifest_hash": "12492e6534d23073569e1494ffcb9b4a76c9a7eb2a4ced3943078eb3d713b3fb"
},
"execution_id": "5233bb90-d501-4c88-8415-040959abb224",
"scenario": "invalid-manifest"
}What v0.1.0 does and does not replay
The released adapter is a reference counterfactual executor. It resolves a private snapshot with precomputed scores and runs an isolated LangGraph graph to return a bounded outcome. It is not a generic checkpoint or workflow replay engine, does not invoke tools, and never changes the original banking database.
Controlled replay and causal-audit result screenshots are deliberately not included here. They require a separately activated intervention policy and a replay worker with the adapter installed; they were not part of the observed run set above. Adapter availability is separate from manifest eligibility.
