Introduction
IT Access Request & Provisioning Agent. It reads a user’s request (e.g., “Give me read access to Billing DB”), checks policy and approvals, proposes least-privilege access, and—only if rules are satisfied—creates the grant with a receipt. It never claims success without evidence from the backend.
The Use Case
Employees request access to systems (apps, databases, dashboards). The agent must validate identity and role, check policy and training requirements, detect conflicts (toxic combinations), confirm manager approval, and provision time-boxed, least-privilege access. All decisions cite policy claims; all writes return receipts.
Prompt Contract (the agent’s interface)
# file: contracts/it_access_v1.yaml
role: "ITAccessAgent"
scope: >
Triage and fulfill least-privilege access requests. Use claims for policy, approvals, and identity.
Ask once if critical fields are missing (user_id, system, role/requested_scope, justification).
Propose tools; never assert success without a receipt.
output:
type: object
required: [summary, decision, next_steps, citations, tool_proposals]
properties:
summary: {type: string, maxWords: 60}
decision: {type: string, enum: ["approve","reject","need_approval","need_more_info"]}
next_steps: {type: array, items: {type: string, maxWords: 18}, maxItems: 6}
citations: {type: array, items: {type: string}}
tool_proposals:
type: array
items:
type: object
required: [name, args, preconditions, idempotency_key]
properties:
name: {type: string, enum: [GetIdentity, CheckPolicy, CheckToxicCombo, EnsureApproval, GrantAccess]}
args: {type: object}
preconditions: {type: string}
idempotency_key: {type: string}
policy_id: "iam_policy.v4.us"
citation_rule: "1-2 minimal-span claim_ids per factual sentence"
decoding:
narrative: {top_p: 0.92, temperature: 0.72, stop: ["\n\n## "]}
bullets: {top_p: 0.82, temperature: 0.45}
Example claims (context to the model)
[
{"claim_id":"policy:lp:least_priv","text":"Grants must be least privilege and time-boxed.",
"effective_date":"2025-04-01","source_id":"doc:iam_policy_v4","span":"least privilege and time-boxed"},
{"claim_id":"policy:db:reader_requires_training","text":"DB Reader requires annual data-handling training.",
"effective_date":"2025-02-15","source_id":"doc:iam_policy_v4","span":"requires annual data-handling training"},
{"claim_id":"policy:approval:manager_needed","text":"Manager approval is required for any DB access.",
"effective_date":"2025-03-20","source_id":"doc:iam_policy_v4","span":"Manager approval is required"},
{"claim_id":"id:U042:dept","text":"User U042 is in Finance and reports to M015.",
"effective_date":"2025-09-01","source_id":"hr:is","span":"reports to M015"}
]
Tool Interfaces (typed, with receipts)
# tools.py
from pydantic import BaseModel, Field, HttpUrl
from datetime import datetime, timedelta
from typing import Optional, Dict
class GetIdentityArgs(BaseModel):
user_id: str
class CheckPolicyArgs(BaseModel):
user_id: str
system: str
requested_role: str
justification: str
training_completed_at: Optional[datetime] = None
class CheckToxicComboArgs(BaseModel):
user_id: str
system: str
requested_role: str
class EnsureApprovalArgs(BaseModel):
user_id: str
approver_id: str
system: str
requested_role: str
class GrantAccessArgs(BaseModel):
user_id: str
system: str
role: str
expires_at: datetime # time-boxing
class ToolReceipt(BaseModel):
tool: str
ok: bool
ref: str
message: str = ""
data: Optional[Dict] = None
# adapters.py (demo logic)
from tools import *
from datetime import datetime, timedelta
HR = {"U042": {"manager":"M015","dept":"Finance","training_completed_at": datetime(2025,9,10)}}
APPROVALS = {("U042","db.billing","reader"): "appr-777"} # prior approved ticket
TOXIC = {("db.billing","writer"), ("db.ledger","approver")} # example sensitive roles
def get_identity(a: GetIdentityArgs) -> ToolReceipt:
rec = HR.get(a.user_id)
if not rec:
return ToolReceipt(tool="GetIdentity", ok=False, ref="id-missing", message="User not found")
return ToolReceipt(tool="GetIdentity", ok=True, ref=f"id-{a.user_id}", data=rec)
def check_policy(a: CheckPolicyArgs) -> ToolReceipt:
if a.system.startswith("db.") and a.requested_role == "reader":
if not a.training_completed_at or (datetime.utcnow() - a.training_completed_at).days > 365:
return ToolReceipt(tool="CheckPolicy", ok=False, ref="pol-training", message="Training required")
if a.system.startswith("db.") and a.requested_role not in {"reader","analyst"}:
return ToolReceipt(tool="CheckPolicy", ok=False, ref="pol-lp", message="Not least privilege")
return ToolReceipt(tool="CheckPolicy", ok=True, ref="pol-ok", message="Meets policy")
def check_toxic_combo(a: CheckToxicComboArgs) -> ToolReceipt:
if (a.system, a.requested_role) in TOXIC:
return ToolReceipt(tool="CheckToxicCombo", ok=False, ref="tox-hit", message="Toxic combination")
return ToolReceipt(tool="CheckToxicCombo", ok=True, ref="tox-clear", message="No conflict")
def ensure_approval(a: EnsureApprovalArgs) -> ToolReceipt:
key = (a.user_id, a.system, a.requested_role)
if key in APPROVALS:
return ToolReceipt(tool="EnsureApproval", ok=True, ref=APPROVALS[key], message="Manager approved")
return ToolReceipt(tool="EnsureApproval", ok=False, ref="appr-missing", message="Approval not found")
def grant_access(a: GrantAccessArgs) -> ToolReceipt:
# pretend we call IAM and create a grant
return ToolReceipt(tool="GrantAccess", ok=True, ref=f"grant-{a.user_id}-{a.system}-{a.role}",
data={"expires_at": a.expires_at.isoformat()})
Agent Loop (proposal → verification → execution → receipts)
# agent_access.py
import uuid, json
from datetime import datetime, timedelta
from typing import Any, Dict, List
from tools import *
from adapters import *
ALLOWED_TOOLS = {"GetIdentity","CheckPolicy","CheckToxicCombo","EnsureApproval","GrantAccess"}
def new_idem() -> str:
return f"idem-{uuid.uuid4()}"
def verify_proposal(p: Dict[str, Any]) -> str:
need = {"name","args","preconditions","idempotency_key"}
if not need.issubset(p): return "Missing proposal fields"
if p["name"] not in ALLOWED_TOOLS: return "Tool not allowed"
if p["name"] == "GrantAccess":
exp = p["args"].get("expires_at")
if not exp: return "Grant must be time-boxed"
return ""
def execute(p: Dict[str, Any]) -> ToolReceipt:
n, a = p["name"], p["args"]
if n == "GetIdentity": return get_identity(GetIdentityArgs(**a))
if n == "CheckPolicy":
if "training_completed_at" in a and isinstance(a["training_completed_at"], str):
a["training_completed_at"] = datetime.fromisoformat(a["training_completed_at"])
return check_policy(CheckPolicyArgs(**a))
if n == "CheckToxicCombo": return check_toxic_combo(CheckToxicComboArgs(**a))
if n == "EnsureApproval": return ensure_approval(EnsureApprovalArgs(**a))
if n == "GrantAccess":
if isinstance(a["expires_at"], str):
a["expires_at"] = datetime.fromisoformat(a["expires_at"])
return grant_access(GrantAccessArgs(**a))
return ToolReceipt(tool=n, ok=False, ref="none", message="Unknown tool")
# --- Model shim returning a plan per contract (replace with LLM call) ---
def call_model(contract_yaml: str, claims: List[Dict[str,Any]], req: Dict[str,Any]) -> Dict[str,Any]:
ttl = (datetime.utcnow() + timedelta(days=30)).isoformat()
return {
"summary": f"Request for {req['system']} {req['requested_role']} appears eligible with time-boxed access.",
"decision": "approve",
"next_steps": ["Verify identity","Check policy and toxic combos","Confirm manager approval","Grant least-privilege"],
"citations": ["policy:lp:least_priv","policy:db:reader_requires_training","policy:approval:manager_needed","id:U042:dept"],
"tool_proposals": [
{"name":"GetIdentity",
"args":{"user_id":req["user_id"]},
"preconditions":"User exists with manager and training timestamps.",
"idempotency_key": new_idem()},
{"name":"CheckPolicy",
"args":{"user_id":req["user_id"],"system":req["system"],"requested_role":req["requested_role"],
"justification":req["justification"],"training_completed_at":"2025-09-10T00:00:00"},
"preconditions":"Meets training and least-privilege rules.",
"idempotency_key": new_idem()},
{"name":"CheckToxicCombo",
"args":{"user_id":req["user_id"],"system":req["system"],"requested_role":req["requested_role"]},
"preconditions":"No toxic combo.",
"idempotency_key": new_idem()},
{"name":"EnsureApproval",
"args":{"user_id":req["user_id"],"approver_id":"M015","system":req["system"],
"requested_role":req["requested_role"]},
"preconditions":"Manager approval exists.",
"idempotency_key": new_idem()},
{"name":"GrantAccess",
"args":{"user_id":req["user_id"],"system":req["system"],"role":"reader","expires_at": ttl},
"preconditions":"Identity ok, policy ok, approval ok, no toxic combo.",
"idempotency_key": new_idem()}
]
}
def render_response(model_json: Dict[str,Any], receipts: List[ToolReceipt]) -> str:
idx = {r.tool:r for r in receipts}
lines = [model_json["summary"], ""]
lines.append(f"Decision: {model_json['decision']}")
lines.append("Next:")
for s in model_json["next_steps"]: lines.append(f"• {s}")
if idx.get("GrantAccess") and idx["GrantAccess"].ok:
exp = idx["GrantAccess"].data["expires_at"]
lines.append(f"\nAccess granted (ref {idx['GrantAccess'].ref}), expires {exp}")
lines.append("\nCitations: " + ", ".join(model_json["citations"]))
return "\n".join(lines)
def handle(req: Dict[str,Any]) -> str:
contract = open("contracts/it_access_v1.yaml").read()
claims: List[Dict[str,Any]] = [] # load real claims from IAM policy & HR
plan = call_model(contract, claims, req)
receipts: List[ToolReceipt] = []
for prop in plan["tool_proposals"]:
reason = verify_proposal(prop)
if reason:
receipts.append(ToolReceipt(tool=prop["name"], ok=False, ref="blocked", message=reason)); continue
rec = execute(prop)
receipts.append(rec)
# Halt on any blocker before write
if not rec.ok and prop["name"] in {"GrantAccess"}: break
return render_response(plan, receipts)
if __name__ == "__main__":
request = {"user_id":"U042","system":"db.billing","requested_role":"reader",
"justification":"Month-end variance analysis"}
print(handle(request))
The Prompt You’d Send to the Model (concise and testable)
System:
You are ITAccessAgent. Follow the contract:
- Ask once if user_id, system, requested_role, or justification is missing.
- Cite 1–2 claim_ids per factual sentence using provided claims.
- Propose tools; never assert success without a receipt.
- Output JSON with keys: summary, decision, next_steps[], citations[], tool_proposals[].
Claims (eligible only):
[ ... JSON array of policy + identity claims like above ... ]
User:
Request access:
{"user_id":"U042","system":"db.billing","requested_role":"reader","justification":"Month-end variance analysis"}
How to adapt quickly
Swap the demo adapters with your HR, Policy/IAM, and Approval systems; keep idempotency and time-boxed grants. Load claims from policy docs and HR with freshness windows. Add a validator step before execution for schema, lexicon, locale, citation coverage, and no implied writes. Ship the contract, policy bundle, and decoding settings as a bundle behind a feature flag + canary + rollback.