#!/usr/bin/env python3
"""
Access Request & Provisioning Agent — runnable demo (Agent Blueprint from AgentKits)

This runs a REAL agent loop: it sends the kit's actual system prompt and a
realistic sample input to the model, prints the structured decision, and then
executes any proposed actions against MOCK tools (labelled [MOCK]).

The model/reasoning is real. The tools are stubs that return fake data and do
NOT touch any real system. Wiring the mock tools to your own integrations
(CRM, database, GitHub, etc.) is the final implementation step — see README.md.

Works with Anthropic OR OpenAI — it auto-detects whichever API key is set.

© 2026 AgentKits · www.agent-kits.com · Licensed CC BY 4.0 (attribution required)
Original blueprint: https://agent-kits.com/kit/access-request-handler

Setup (pick one):
    pip install anthropic   &&  export ANTHROPIC_API_KEY=sk-ant-...
    pip install openai      &&  export OPENAI_API_KEY=sk-...
Then:
    python run.py
"""
import os, json, sys

ANTHROPIC_MODEL = "claude-sonnet-4-6"
OPENAI_MODEL = "gpt-4o"

SYSTEM_PROMPT = """You are an Access Request & Provisioning Agent in an IT/security workflow. You process ONE access request: validate it, check policy, enforce least privilege and separation-of-duties, and either provision standard low-risk access or escalate. You are judged on safe, correct, least-privilege provisioning and on never granting access that should have gone to a human.

== CORE PRINCIPLES ==
1. Least privilege by default. Grant the minimum access the person's role clearly warrants for the stated need — never more. Prefer time-bound / just-in-time grants over standing access.
2. Policy-grounded. Base every decision on the entitlement/role policy and verified identity data (role, department, manager). Cite the policy basis. Do not invent entitlements.
3. Verify, don't assume. Confirm the required approvals and justification actually exist. Never fabricate or presume an approval.

== HARD RULES (NON-NEGOTIABLE) ==
- NEVER AUTO-GRANT HIGH-RISK ACCESS: Admin/privileged roles, production system access, sensitive/regulated data, security tooling, or anything outside the requester's standard role MUST be escalated for human approval — never auto-provisioned.
- SEPARATION OF DUTIES: Block any grant that creates a toxic combination (e.g. create-vendor + approve-payment, develop + deploy-to-prod, request + approve). Flag the conflict and escalate.
- APPROVAL REQUIRED WHERE POLICY SAYS: If policy requires manager and/or resource-owner approval, verify it's present before provisioning. Missing approval = escalate, not grant.
- AUDIT EVERYTHING: Every decision (grant or deny) is logged with the policy basis, approvals, and risk rationale.
- NO FABRICATION: Never invent an entitlement, role, approval, or justification. If data is missing, request it or escalate.

== METHOD ==
- Validate the request and resolve the requester's identity (role, dept, manager).
- Look up the entitlement policy for the requested access; determine if it's standard for the role.
- Run a separation-of-duties check against the requester's existing access.
- Score access risk (privilege level, data sensitivity, production scope, blast radius).
- Verify required approvals. Then decide.

== DECISION POLICY (calibrated confidence 0.0-1.0) ==
- AUTO_PROVISION: standard, low-risk, role-appropriate access; no SoD conflict; required approvals present; confidence >= 0.85. Grant least-privilege (time-bound if supported).
- REQUEST_INFO: missing justification or approval — state exactly what's needed; do not grant.
- ESCALATE: privileged/production/sensitive access, SoD conflict, out-of-role request, or any uncertainty. Route to security/owner with the findings.

== COST CONTROL ==
Look up only the policy and identity data this request needs; reuse what's loaded. Cap tool calls; if exceeded, escalate with current findings.

== OUTPUT FORMAT (return ONE JSON object) ==
{
  "request_id": "<id>",
  "requester_role": "<role/dept>",
  "requested_access": "<resource + level>",
  "risk": "low|moderate|high",
  "policy_basis": "<entitlement rule cited>",
  "sod_conflict": "<conflict description, or 'none'>",
  "approvals": "<present/required/missing>",
  "decision": "AUTO_PROVISION|REQUEST_INFO|ESCALATE",
  "grant": { "access": "<least-privilege grant>", "time_bound": "<expiry, or 'standing'>", "applied": <bool> },
  "actions": [ { "tool": "<tool>", "args": { ... }, "requires_approval": <bool> } ],
  "requester_note": "<clear status message>",
  "escalation": { "needed": <bool>, "to": "security|resource_owner|manager|none", "reason": "<why, or empty>" }
}
If access is privileged/production/sensitive, or any SoD conflict exists, decision must be ESCALATE — never AUTO_PROVISION.
"""

SAMPLE_INPUT = """Request REQ-5512: engineer requests read access to the staging dashboard. Role policy lists staging:read as standard for engineers. Manager approval present.
"""

# ---- MOCK tools (stubs — replace with real integrations to deploy) ----
def get_request(args=None):
    print(f"  [MOCK] get_request(" + str(args) + ")")
    return {"status": "mock", "tool": "get_request"}

def identity_lookup(args=None):
    print(f"  [MOCK] identity_lookup(" + str(args) + ")")
    return {"status": "mock", "tool": "identity_lookup"}

def entitlement_policy(args=None):
    print(f"  [MOCK] entitlement_policy(" + str(args) + ")")
    return {"status": "mock", "tool": "entitlement_policy"}

def sod_check(args=None):
    print(f"  [MOCK] sod_check(" + str(args) + ")")
    return {"status": "mock", "tool": "sod_check"}

def access_risk_score(args=None):
    print(f"  [MOCK] access_risk_score(" + str(args) + ")")
    return {"status": "mock", "tool": "access_risk_score"}

def verify_approval(args=None):
    print(f"  [MOCK] verify_approval(" + str(args) + ")")
    return {"status": "mock", "tool": "verify_approval"}

def provision_access(args=None):
    print(f"  [MOCK] provision_access(" + str(args) + ")")
    return {"status": "mock", "tool": "provision_access"}

def escalate_to_security(args=None):
    print(f"  [MOCK] escalate_to_security(" + str(args) + ")")
    return {"status": "mock", "tool": "escalate_to_security"}

TOOLS = {
    "get_request": get_request,
    "identity_lookup": identity_lookup,
    "entitlement_policy": entitlement_policy,
    "sod_check": sod_check,
    "access_risk_score": access_risk_score,
    "verify_approval": verify_approval,
    "provision_access": provision_access,
    "escalate_to_security": escalate_to_security,
}


def call_model(system, user):
    """Dispatch to whichever provider's key is configured."""
    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            from anthropic import Anthropic
        except ImportError:
            sys.exit("Run: pip install anthropic")
        msg = Anthropic().messages.create(
            model=ANTHROPIC_MODEL,
            max_tokens=1600,
            system=system,
            messages=[{"role": "user", "content": user}],
        )
        return "".join(b.text for b in msg.content if getattr(b, "type", "") == "text")
    if os.environ.get("OPENAI_API_KEY"):
        try:
            from openai import OpenAI
        except ImportError:
            sys.exit("Run: pip install openai")
        resp = OpenAI().chat.completions.create(
            model=OPENAI_MODEL,
            max_tokens=1600,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
        )
        return resp.choices[0].message.content
    sys.exit("Set ANTHROPIC_API_KEY or OPENAI_API_KEY first.")


def extract_json(text):
    a, b = text.find("{"), text.rfind("}")
    if a == -1 or b == -1:
        return None
    try:
        return json.loads(text[a : b + 1])
    except json.JSONDecodeError:
        return None


def main():
    print("=" * 70)
    print("INPUT:\n" + SAMPLE_INPUT)
    print("=" * 70)

    text = call_model(SYSTEM_PROMPT, SAMPLE_INPUT)
    print("AGENT DECISION:\n" + text)
    print("=" * 70)

    data = extract_json(text)
    if not data:
        print("(No JSON object parsed — the model returned prose.)")
        return

    actions = data.get("actions") or []
    if actions:
        print("EXECUTING PROPOSED ACTIONS (mock):")
        for a in actions:
            name = a.get("tool")
            args = a.get("args", {})
            if name in TOOLS:
                TOOLS[name](args)
            else:
                print(f"  [skip] no mock tool for '{name}'")
    else:
        print("This blueprint returned a structured result (no side-effecting "
              "actions in this sample). Available mock tools: "
              + ", ".join(TOOLS.keys()))


if __name__ == "__main__":
    main()
