Governance
Roles and pre-action policy checks, deny by default.
Laser SDK has three governance mechanisms. Capability RBAC protects managed APIs. ActionGovernor checks policy before an effect. Durable approval records support asynchronous decisions. Deny wins, and the default is deny.
Capability RBAC
Managed surfaces are governed by grants of the shape effect feature:action [on resource-pattern], assembled through roles bound to the server-stamped Iggy user:
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"])laser.whoami()returns the caller's roles and effective grants.list_roles/get_role/get_bindings/define_role/delete_role/bind_rolesmanage the role catalog and its bindings.- A deny grant always wins over an allow on the same feature/action/resource match - deny-wins is not configurable.
- Role names pass a 64-byte charset safelist before any round-trip, so a malformed name fails locally, not on the server.
- This layer is orthogonal to Iggy's own permissions and enforced at the streaming edge, not inside a single feature.
On-behalf-of intersection. When an agent acts for a user, the two grant sets are intersected - an agent is allowed to do something only where both the agent's own grants and the user's grants allow it:
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",
)An agent can never use a user's session to exceed its own configured grants, and a user's own restrictions still apply through the agent.
External edge audience and step-up. A2A and MCP claims are checked before the request reaches AGDX. A wrong audience is rejected. A valid audience with a missing scope receives a typed step-up challenge. The challenge names the required scope.
ActionGovernor
The agent feature includes ActionGovernor. It checks policy before a side effect. RBAC checks the principal's permission. A governor checks runtime limits such as budgets, rates, and deployment policy.
Run budgets are a governor, not a grant - they cap what a submitted run may do (event count, model calls, tool calls, wall-clock time, cost) rather than deciding who may submit it:
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
The SDK models asynchronous approval with Intent, Vote, and Decision records on typed topics. The application publishes and folds these validated records.
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 validates an intent before publishing and before making a decision. Invalid policy fails immediately. A voter name is still a record claim. Use signed principals or topic ACLs when voter identity must be trusted.
Running it
Role and binding calls require the managed authz capability. Laser Stack and LaserData Cloud provide it. Decision helpers and durable intent records run on every VSR target.