Governance
Apply managed permissions, runtime policy, and durable approval decisions
Governance controls who can act and which effects can proceed. Capability RBAC protects managed APIs. ActionGovernor applies policy before effects, and durable approval records support decisions made later. Access is denied by default, and a matching deny takes precedence.
Capability RBAC
RBAC means role-based access control. Managed grants use effect feature:action [on resource-pattern]. Roles bind them to the Iggy user identity stamped by the server.
const role: Role = {
name: "support-reader",
grants: [{
effect: "allow",
feature: "kv",
action: "read",
resource: { kind: "prefix", value: "support/" }
}]
}
await laser.defineRole(role)
await laser.bindRoles(targetUser, [role.name])use laser_sdk::rbac::{Action, Effect, Feature, Grant, Role, ResourcePattern};
let role = Role {
name: "support-reader".to_owned(),
grants: vec![Grant {
effect: Effect::Allow,
feature: Feature::Kv,
action: Action::Read,
resource: ResourcePattern::prefix("support/"),
}],
};
laser.define_role(role).await?;
laser.bind_roles(target_user, vec!["support-reader".to_owned()]).await?;grants = [
ls.Grant(
"kv",
"read",
resource_kind="prefix",
resource_value="support/",
)
]
await laser.define_role("support-reader", grants)
await laser.bind_roles(target_user, ["support-reader"])The API provides these operations and rules:
laser.whoami()returns the caller's roles and effective grants.list_roles,get_role,get_bindings,define_role,delete_role, andbind_rolesmanage roles and bindings.- A deny wins over an allow for the same feature, action, and resource match. This rule cannot be disabled.
- Role names must satisfy a 64-byte character allowlist. Invalid names fail locally before a request.
- Managed grants supplement Iggy's own permissions and are enforced at the streaming edge.
When an agent acts for a user, both grant sets must allow the action:
const allowed = delegatedAllow(
agentGrants,
userGrants,
"kv",
"write",
"support/tickets/acme"
)let allowed = delegated_allow(
&agent_grants,
&user_grants,
Feature::Kv,
Action::Write,
Some("support/tickets/acme"),
);allowed = ls.delegated_allow(
agent_grants,
user_grants,
"kv",
"write",
"support/tickets/acme",
)A user session cannot extend the agent's grants. The user's restrictions also remain effective through the agent.
A2A and MCP claims are evaluated before they reach AGDX. A wrong audience is rejected. A valid audience without a required scope receives a typed step-up challenge that names the missing scope.
Role bindings support updates guarded by revision. Rust uses bind_roles_expect_revision. TypeScript supplies expectRevision as the third argument to bindRoles, and Python supplies expect_revision to bind_roles. After a conflict, read the bindings again. New Iggy users receive no managed role automatically.
ActionGovernor
The agent feature provides ActionGovernor. It applies runtime policy before an effect, including budgets, rates, and deployment rules. RBAC determines the principal's permission separately.
Run budgets limit event counts, model calls, tool calls, elapsed time, and cost. They do not decide who can submit a run.
const run = await laser.runs().submitBudgeted(
"governance-auditor",
{
maxEvents: 8n,
maxModelCalls: 1n,
maxToolCalls: 2n,
maxWallClockMicros: 30_000_000n
},
body
)let budget = RunBudget {
max_events: Some(8),
max_model_calls: Some(1),
max_tool_calls: Some(2),
max_wall_clock_micros: Some(30_000_000),
..Default::default()
};
let run = laser
.runs()
.submit_budgeted("governance-auditor", Some(body), budget)
.await?;budget = ls.RunBudget(
max_events=8,
max_model_calls=1,
max_tool_calls=2,
max_wall_clock_micros=30_000_000,
)
run = await laser.runs().submit_budgeted(
"governance-auditor",
input=body,
budget=budget,
)Durable approval records
Approvals use typed Intent, Vote, and Decision records. The application publishes them and combines their recorded results into a decision.
const now = BigInt(Date.now()) * 1_000n
const safety = AgentId.new("safety")
const intent = new Intent({
conversation: ConversationId.new(),
proposer: AgentId.new("planner"),
body: new TextEncoder().encode("reserve inventory"),
eligibleVoters: [safety],
policy: { kind: "all" },
policyVersion: 7n,
atMicros: now,
deadlineMicros: now + 30_000_000n
})
const vote = Vote.cast(intent, safety, VoteChoice.Allow)
const decision = decide(intent, [vote], BigInt(Date.now()) * 1_000n)
if (decision?.authorizes(intent)) {
// apply the fenced effect, then persist the decision
}use laser_sdk::intent::{decide, Intent, IntentPolicy, Vote, VoteChoice};
let intent = Intent::builder()
.conversation(ConversationId::new())
.proposer("planner".parse()?)
.body(b"reserve inventory".to_vec())
.eligible_voters(vec!["safety".parse()?])
.policy(IntentPolicy::All)
.policy_version(7)
.deadline_micros(deadline)
.build()?;
let vote = Vote::cast(&intent, "safety".parse()?, VoteChoice::Allow)?;
if let Some(decision) = decide(&intent, &[vote], now)? {
if decision.authorizes(&intent)? {
// apply the fenced effect, then persist the decision
}
}now = time.time_ns() // 1_000
intent = ls.Intent(
conversation=ls.new_conversation_id(),
proposer="planner",
body=b"reserve inventory",
eligible_voters=["safety"],
policy=ls.IntentPolicy.all(),
policy_version=7,
deadline_micros=now + 30_000_000,
)
vote = ls.Vote.cast(intent, "safety", "allow")
decision = ls.decide(intent, [vote], time.time_ns() // 1_000)
if decision and decision.authorizes(intent):
# apply the fenced effect, then persist the decision
passThe SDK evaluates an intent before publication and decision. Invalid policy fails immediately. A voter name is a claim within a record. Use signed principals or topic access rules when the application must trust voter identity.
Running it
Role and binding operations require the managed authz capability from Laser Stack or LaserData Cloud. Decision helpers and durable intent records work with Iggy alone.