package palo.agentic.governance

import rego.v1

# Developer-preview reference policy for the canonical PALO-AI contracts.
# This is not a production authorization policy. The reference MCP server supplies a
# locally registered profile, but production publisher identity and policy attestation
# remain the responsibility of the adopting organization.

policy_version := "palo-agentic-governance/1.4.0"

# Defense-in-depth fallback. The complete rules below already deny malformed
# input explicitly; this default preserves fail-closed behavior if a future
# edit accidentally leaves an input shape unmatched.
default action_decision := {
	"status": "denied",
	"reasons": ["policy input did not match a supported decision path"],
	"obligations": ["stop_action", "repair_policy_input"],
	"policyVersion": "palo-agentic-governance/1.4.0",
}

claim_contract_valid if {
	input.claim.schemaVersion == "1.1.0"
}

claim_contract_valid if {
	input.claim.schemaVersion == "1.2.0"
	is_object(input.claim.effectContract)
	input.claim.effectContract.format == "palo-agentic-effect-contract"
	input.claim.effectContract.schemaVersion in {"1.0.0", "1.1.0"}
	input.claim.effectContract.resourceSelector.resource == input.claim.action.resource
	input.claim.effectContract.resourceSelector.path == input.claim.action.path
}

claim_contract_valid if {
	input.claim.schemaVersion == "1.3.0"
	is_object(input.claim.effectContract)
	input.claim.effectContract.format == "palo-agentic-effect-contract"
	input.claim.effectContract.schemaVersion in {"1.0.0", "1.1.0"}
	input.claim.effectContract.resourceSelector.resource == input.claim.action.resource
	input.claim.effectContract.resourceSelector.path == input.claim.action.path
	is_object(input.claim.authorityContext)
	input.claim.authorityContext.agentIdentity.agentId == input.claim.agentId
	count(input.claim.authorityContext.delegationChain) == input.claim.delegation.depth
}

claim_contract_valid if {
	input.claim.schemaVersion == "1.4.0"
	is_object(input.claim.effectContract)
	input.claim.effectContract.format == "palo-agentic-effect-contract"
	input.claim.effectContract.schemaVersion in {"1.0.0", "1.1.0"}
	input.claim.effectContract.resourceSelector.resource == input.claim.action.resource
	input.claim.effectContract.resourceSelector.path == input.claim.action.path
	is_object(input.claim.authorityContext)
	input.claim.authorityContext.agentIdentity.agentId == input.claim.agentId
	count(input.claim.authorityContext.delegationChain) == input.claim.delegation.depth
	is_object(input.claim.dataGovernance)
	is_object(input.claim.dataGovernance.subject)
	is_string(input.claim.dataGovernance.purpose)
	is_string(input.claim.dataGovernance.fitnessDecisionId)
	is_string(input.claim.dataGovernance.fitnessDecisionDigest)
	is_string(input.claim.dataGovernance.disclosureContractId)
	is_string(input.claim.dataGovernance.disclosureContractDigest)
}

network_intent_consistent if {
	input.claim.action.networkIntent == "none"
	not input.claim.externalNetwork
}

network_intent_consistent if {
	input.claim.action.networkIntent != "none"
	input.claim.externalNetwork
}

input_valid if {
	is_object(input)
	is_object(input.claim)
	is_object(input.profile)
	is_object(input.policy)
	is_string(input.claim_digest)
	is_string(input.now)
	input.claim.format == "palo-agentic-action-claim"
	claim_contract_valid
	input.profile.format == "palo-agentic-interface"
	input.policy.status == "active"
	input.policy.entrypoint == "action_decision"
	input.claim.action.networkIntent in {"none", "read", "write", "bidirectional"}
	network_intent_consistent
	is_object(input.claim.action.arguments)
	is_number(input.claim.sequenceNumber)
	is_string(input.claim.idempotencyKey)
}

vibe_gate_valid if {
	gate := input.claim.metadata.vibeGate
	gate.status == "passed"
	is_string(gate.gateId)
	is_string(gate.evidenceDigest)
	startswith(gate.evidenceDigest, "sha256:")
}

violations contains "Vibe Coding gate is required before this coding-agent tool call" if {
	input.profile.authority.requireVibeGate
	not vibe_gate_valid
}

violations contains "agent identity does not match the registered profile" if {
	input.claim.agentId != input.profile.agentId
}

violations contains "agent profile is not active" if {
	object.get(input.profile, "status", "active") != "active"
}

violations contains "tool is outside the registered allowlist" if {
	not value_in(input.claim.action.tool, input.profile.authority.allowedTools)
}

violations contains "operation is outside the registered allowlist" if {
	not value_in(input.claim.action.operation, input.profile.authority.allowedOperations)
}

violations contains "external network access is not authorized" if {
	input.claim.externalNetwork
	not input.profile.authority.externalNetwork
}

violations contains "network host is outside the registered allowlist" if {
	input.claim.externalNetwork
	host := object.get(input.claim.action, "networkHost", "")
	not host_allowed(host, object.get(input.profile.authority, "allowedNetworkHosts", []))
}

violations contains "one or more read scopes are outside registered authority" if {
	some requested in input.claim.requestedScopes.read
	not scope_allowed(requested, input.profile.authority.readScopes)
}

violations contains "one or more write scopes are outside registered authority" if {
	some requested in input.claim.requestedScopes.write
	not scope_allowed(requested, input.profile.authority.writeScopes)
}

violations contains "delegation depth exceeds the registered limit" if {
	input.claim.delegation.depth > input.profile.delegation.maxDepth
}

violations contains "subagent count exceeds the registered limit" if {
	input.claim.delegation.subagentCount > input.profile.delegation.maxSubagents
}

violations contains "subagent role is outside the registered allowlist" if {
	role := object.get(input.claim.delegation, "requestedSubagentRole", "")
	role != ""
	not value_in(role, input.profile.delegation.allowedSubagentRoles)
}

violations contains "approval was denied, cancelled, or expired" if {
	input.profile.delegation.requireHumanValidation
	status := object.get(object.get(input, "approval", {}), "status", "")
	status in {"denied", "cancelled", "expired"}
}

approval_valid if {
	approval := input.approval
	approval.status == "approved"
	approval.claimId == input.claim.claimId
	approval.claimDigest == input.claim_digest
	time.parse_rfc3339_ns(approval.expiresAt) > time.parse_rfc3339_ns(input.now)
}

requires_approval if {
	input.profile.delegation.requireHumanValidation
	not approval_valid
}

allowed_obligations := ["record_execution_outcome", "verify_declared_effects", "verify_data_disclosure"] if {
	input.claim.schemaVersion == "1.4.0"
}

allowed_obligations := ["record_execution_outcome", "verify_declared_effects"] if {
	input.claim.schemaVersion != "1.4.0"
}

action_decision := {
	"status": "denied",
	"reasons": ["policy input is missing or malformed"],
	"obligations": ["stop_action", "repair_policy_input"],
	"policyVersion": policy_version,
} if {
	not input_valid
}

action_decision := {
	"status": "denied",
	"reasons": sort([reason | some reason in violations]),
	"obligations": ["stop_action", "review_agent_authority"],
	"policyVersion": policy_version,
} if {
	input_valid
	count(violations) > 0
}

action_decision := {
	"status": "pending_approval",
	"reasons": ["human validation is required for this exact action claim"],
	"obligations": ["obtain_bound_human_approval"],
	"policyVersion": policy_version,
} if {
	input_valid
	count(violations) == 0
	requires_approval
}

action_decision := {
	"status": "allowed",
	"reasons": ["action is within the registered authority profile"],
	"obligations": allowed_obligations,
	"policyVersion": policy_version,
} if {
	input_valid
	count(violations) == 0
	not requires_approval
}

value_in(value, values) if {
	some candidate in values
	candidate == value
}

host_allowed(host, allowed) if {
	host != ""
	value_in("*", allowed)
}

host_allowed(host, allowed) if {
	host != ""
	value_in(host, allowed)
}

scope_allowed(requested, allowed) if {
	value_in(requested, allowed)
}

scope_allowed(requested, allowed) if {
	some candidate in allowed
	endswith(candidate, "/*")
	prefix := trim_suffix(candidate, "*")
	startswith(requested, prefix)
}
