import {
ExternalActionAuthenticationAssurance,
ExternalActionEnvironment,
ExternalActionError,
ExternalActionGrant,
ExternalActionIntent,
ExternalActionMoney,
external_action_error,
external_action_grant_integrity_check,
external_action_intent_is_exact,
} from "std/external_action/contracts"
/** Extra restrictions supplied by an organization that manages the actor. */
pub type ExternalActionManagedPolicy = {
schema: "harn.external_action_managed_policy.v1",
external_actions_disabled: bool,
automatic_approval_forbidden: bool,
live_actions_forbidden: bool,
minimum_authentication_assurance: ExternalActionAuthenticationAssurance,
max_external_spend: list<ExternalActionMoney>,
allowed_providers?: list<string>,
allowed_capabilities?: list<string>,
allowed_environments?: list<ExternalActionEnvironment>,
}
const __EXTERNAL_ACTION_POLICY_KEYS = [
"schema",
"external_actions_disabled",
"automatic_approval_forbidden",
"live_actions_forbidden",
"minimum_authentication_assurance",
"max_external_spend",
"allowed_providers",
"allowed_capabilities",
"allowed_environments",
]
fn __external_action_policy_error(code: string, message: string) -> ExternalActionError {
return external_action_error("invalid_policy", code, message)
}
fn __external_action_policy_bool(raw: dict, field: string) -> Result<bool, ExternalActionError> {
const value = raw[field]
if value == nil {
return Ok(false)
}
if type_of(value) != "bool" {
return Err(__external_action_policy_error("invalid_" + field, field + " must be true or false"))
}
return Ok(value)
}
fn __external_action_policy_assurance(
raw,
) -> Result<ExternalActionAuthenticationAssurance, ExternalActionError> {
const value = lowercase(trim(to_string(raw ?? "none")))
if value == "none" || value == "session" || value == "biometric" || value == "managed" {
return Ok(value)
}
return Err(
__external_action_policy_error(
"invalid_minimum_authentication_assurance",
"minimum authentication assurance must be none, session, biometric, or managed",
),
)
}
fn __external_action_policy_text_list(
raw,
field: string,
) -> Result<list<string>?, ExternalActionError> {
if raw == nil {
return Ok(nil)
}
if type_of(raw) != "list" || len(raw) > 100 {
return Err(
__external_action_policy_error("invalid_" + field, field + " must be a bounded list"),
)
}
let values: list<string> = []
for item in raw {
const value = lowercase(trim(to_string(item)))
if value == "" || len(value) > 128 || value.contains("\n") {
return Err(
__external_action_policy_error(
"invalid_" + field,
field + " entries must be plain identifiers up to 128 characters",
),
)
}
if !values.contains(value) {
values = values + [value]
}
}
return Ok(values.sorted())
}
fn __external_action_policy_environments(
raw,
) -> Result<list<ExternalActionEnvironment>?, ExternalActionError> {
const checked = __external_action_policy_text_list(raw, "allowed_environments")
if !is_ok(checked) || unwrap(checked) == nil {
return checked
}
let environments: list<ExternalActionEnvironment> = []
for value in unwrap(checked) {
if value != "mock" && value != "test" && value != "live" {
return Err(
__external_action_policy_error(
"invalid_allowed_environments",
"allowed environments may contain only mock, test, or live",
),
)
}
environments = environments + [value]
}
return Ok(environments)
}
fn __external_action_policy_money(raw) -> Result<list<ExternalActionMoney>, ExternalActionError> {
if raw == nil {
return Ok([])
}
if type_of(raw) != "list" || len(raw) > 32 {
return Err(
__external_action_policy_error(
"invalid_max_external_spend",
"max external spend must be a bounded list of currency ceilings",
),
)
}
let ceilings: list<ExternalActionMoney> = []
let currencies: list<string> = []
for item in raw {
if type_of(item) != "dict" {
return Err(
__external_action_policy_error(
"invalid_max_external_spend",
"each spend ceiling must be an object",
),
)
}
const currency = uppercase(trim(to_string(item?.currency ?? "")))
if regex_match("^[A-Z]{3}$", currency) == nil || type_of(item?.amount_minor) != "int"
|| item
.amount_minor
< 0 {
return Err(
__external_action_policy_error(
"invalid_max_external_spend",
"each spend ceiling needs an ISO currency and non-negative minor-unit amount",
),
)
}
if currencies.contains(currency) {
return Err(
__external_action_policy_error(
"duplicate_spend_currency",
"max external spend may define each currency once",
),
)
}
currencies = currencies + [currency]
ceilings = ceilings + [{currency: currency, amount_minor: item.amount_minor}]
}
return Ok(ceilings)
}
/**
* Validate and normalize one versioned managed-policy projection.
* @effects: []
* @errors: []
*/
pub fn external_action_managed_policy_result(
raw: unknown,
) -> Result<ExternalActionManagedPolicy, ExternalActionError> {
if type_of(raw) != "dict" {
return Err(__external_action_policy_error("invalid_shape", "managed policy must be an object"))
}
for key in keys(raw) {
if !__EXTERNAL_ACTION_POLICY_KEYS.contains(key) {
return Err(
__external_action_policy_error(
"unknown_field",
"managed policy contains an unsupported field",
),
)
}
}
if raw?.schema != nil && raw.schema != "harn.external_action_managed_policy.v1" {
return Err(
__external_action_policy_error(
"unsupported_schema",
"managed policy schema must be harn.external_action_managed_policy.v1",
),
)
}
const disabled = __external_action_policy_bool(raw, "external_actions_disabled")
const no_auto = __external_action_policy_bool(raw, "automatic_approval_forbidden")
const no_live = __external_action_policy_bool(raw, "live_actions_forbidden")
const assurance = __external_action_policy_assurance(raw?.minimum_authentication_assurance)
const ceilings = __external_action_policy_money(raw?.max_external_spend)
const providers = __external_action_policy_text_list(raw?.allowed_providers, "allowed_providers")
const capabilities = __external_action_policy_text_list(
raw?.allowed_capabilities,
"allowed_capabilities",
)
const environments = __external_action_policy_environments(raw?.allowed_environments)
for checked in [
disabled,
no_auto,
no_live,
assurance,
ceilings,
providers,
capabilities,
environments,
] {
if !is_ok(checked) {
return Err(unwrap_err(checked))
}
}
let policy: ExternalActionManagedPolicy = {
schema: "harn.external_action_managed_policy.v1",
external_actions_disabled: unwrap(disabled),
automatic_approval_forbidden: unwrap(no_auto),
live_actions_forbidden: unwrap(no_live),
minimum_authentication_assurance: unwrap(assurance),
max_external_spend: unwrap(ceilings),
}
if unwrap(providers) != nil {
policy.allowed_providers = unwrap(providers)
}
if unwrap(capabilities) != nil {
policy.allowed_capabilities = unwrap(capabilities)
}
if unwrap(environments) != nil {
policy.allowed_environments = unwrap(environments)
}
return Ok(policy)
}
/**
* Normalize a managed policy or throw its typed validation error.
* @effects: []
* @errors: [invalid_policy]
*/
pub fn external_action_managed_policy(raw: unknown) -> ExternalActionManagedPolicy {
const checked = external_action_managed_policy_result(raw)
if !is_ok(checked) {
throw unwrap_err(checked)
}
return unwrap(checked)
}
fn __external_action_assurance_rank(value: ExternalActionAuthenticationAssurance) -> int {
if value == "none" {
return 0
}
if value == "session" {
return 1
}
if value == "biometric" {
return 2
}
return 3
}
fn __external_action_automatic_method(method: string) -> bool {
return method == "policy" || method == "adversarial_auto" || method == "managed_policy"
}
/**
* Apply organization restrictions after the user's exact grant is validated.
* This function can deny or narrow an action; it never creates authority.
* @effects: []
* @errors: []
*/
pub fn external_action_managed_policy_check(
intent: ExternalActionIntent,
grant: ExternalActionGrant,
policy: ExternalActionManagedPolicy,
) -> Result<ExternalActionGrant, ExternalActionError> {
if !external_action_intent_is_exact(intent)
|| !is_ok(
external_action_grant_integrity_check(intent, grant),
) {
return Err(
__external_action_policy_error(
"invalid_action_authority",
"action intent or grant is not exact",
),
)
}
const normalized = external_action_managed_policy_result(policy)
if !is_ok(normalized) || unwrap(normalized) != policy {
return Err(
__external_action_policy_error(
"policy_integrity_mismatch",
"managed policy is not canonical",
),
)
}
if policy.external_actions_disabled {
return Err(
__external_action_policy_error(
"external_actions_disabled",
"external actions are disabled by managed policy",
),
)
}
if policy.automatic_approval_forbidden
&& __external_action_automatic_method(
grant.authorization_method,
) {
return Err(
__external_action_policy_error(
"automatic_approval_forbidden",
"managed policy requires a person to approve this action",
),
)
}
if policy.live_actions_forbidden && intent.environment == "live" {
return Err(
__external_action_policy_error(
"live_actions_forbidden",
"live external actions are disabled by managed policy",
),
)
}
if policy.allowed_providers != nil && !policy.allowed_providers.contains(intent.provider) {
return Err(
__external_action_policy_error(
"provider_not_allowed",
"provider is not allowed by managed policy",
),
)
}
if policy.allowed_capabilities != nil
&& !policy.allowed_capabilities.contains(intent.capability) {
return Err(
__external_action_policy_error(
"capability_not_allowed",
"capability is not allowed by managed policy",
),
)
}
if policy.allowed_environments != nil
&& !policy.allowed_environments.contains(intent.environment) {
return Err(
__external_action_policy_error(
"environment_not_allowed",
"environment is not allowed by managed policy",
),
)
}
if __external_action_assurance_rank(grant.authentication_assurance)
< __external_action_assurance_rank(
policy.minimum_authentication_assurance,
) {
return Err(
__external_action_policy_error(
"authentication_too_weak",
"managed policy requires stronger authentication",
),
)
}
if intent.external_spend != nil && len(policy.max_external_spend) > 0 {
let allowed: ExternalActionMoney? = nil
for ceiling in policy.max_external_spend {
if ceiling.currency == intent.external_spend.currency {
allowed = ceiling
}
}
if allowed == nil || intent.external_spend.amount_minor > allowed.amount_minor {
return Err(
__external_action_policy_error(
"external_spend_exceeds_policy",
"external spend exceeds the managed policy limit",
),
)
}
}
return Ok(grant)
}