Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

canPress / canEdit / canSee

Pressable(canPress: IsPlatformAdmin) · Input(canEdit: …) · Stack(canSee: …) — reflect a policy onto a control

`canPress`/`canEdit`/`canSee: <policy>` ties a control to a declared `policy`: `canPress` disables a button, `canEdit` makes a value control read-only, and `canSee` hides an element entirely — each unless the signed-in caller satisfies the policy. You declare the rule ONCE — on the entity or as a `policy` the server enforces — and the control simply REFLECTS it, live and reactively. It is a UX affordance, never the gate: the server still enforces the policy on every action, so a control that reflects the wrong answer can still never let a caller do something they may not.

stable3 examples compiled by CIuisecuritypolicycontrols

Summary#

canPress: <policy> ties a control's pressable state to a declared policy. The button is enabled when the signed-in caller satisfies the policy and disabled when they don't — computed on the client, reactively, so it reflects the current caller with no extra code.

// Creating an organization is admins-only — declared once on the entity:
//   entity Organization { ... security { allow create when IsPlatformAdmin; } }
// The button that starts that create REFLECTS the same rule, so a non-admin sees it disabled:
Pressable(onClick: NewOrg, canPress: IsPlatformAdmin) {
  BtnPrimary("New organization") { Icon(Icons.Plus, size: 18); }
}

This is a reflection, not a gate. The server enforces the policy on the action itself; canPress only spares the caller a click that would be refused. Because the rule lives in one place — the policy (see page authorization (policies)) — the button can never drift out of sync with what the server actually allows.

A value control uses canEdit the same way — the field stays visible but becomes read-only unless the caller satisfies the policy:

[Principal] entity User { [Required] string Email; }
[Role] enum AppRole { PlatformAdmin, Member }

entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role;
  security { allow read when IsAuthenticated; }
}

policy IsPlatformAdmin => RoleGrant.Any(r => r.User == user && r.Role == AppRole.PlatformAdmin);

entity Application {
  [Required] string Slug;
  [Required] string Name;
  security { allow create, read, update when IsAuthenticated; }
}

[Page("/settings/{id}")] [Render(CSR)]
component OrgSettings(Guid id) {
  var org = Application.Single(a => a.Id == id);
  action Seed() { }
  action NewOrg() { }
  render {
    // The field shows the value to everyone, editable to admins:
    Input(value: org.Slug, canEdit: IsPlatformAdmin);

    // An admin-only affordance — a member never sees it (nothing is rendered, not just hidden):
    Stack(canSee: IsPlatformAdmin) {
      Button("Seed sample data", onPress: Seed);
    }

    // Disabled rather than hidden, and it says WHY.
    Pressable(onClick: NewOrg,
              canPress: IsPlatformAdmin,
              whenDenied: "Only platform admins can create organizations.") {
      Text("New organization");
    }
  }
}

To hide an element (and its whole subtree) unless the policy holds, use canSee on any element — a container is the usual choice, so a whole section appears only for callers who satisfy the policy:

// An admin-only affordance — a member never sees it (nothing is rendered, not just hidden):
Stack(canSee: IsPlatformAdmin) {
  Button("Seed sample data", onPress: Seed);
}

Signature#

<PressableControl>(canPress: <PolicyName>)   // → disabled unless the policy holds
<ValueControl>(canEdit: <PolicyName>)        // → read-only unless the policy holds
<AnyElement>(canSee: <PolicyName>)           // → not rendered at all unless the policy holds
  • <PolicyName> — a declared policy, the same vocabulary page authorization (policies) uses. A name that isn't a declared policy is a compile error (with a did-you-mean).
  • canPress applies to pressable controls (a Pressable / button); canEdit applies to value controls (an Input) — used on a control that doesn't take the matching state, it is a compile error. canSee applies to any element; when the policy is false the element and its children render nothing at all.

Description#

A policy names a condition about the caller — for example "is a platform admin", or "is an owner or admin of an organization". You already write policies to gate pages (page authorization (policies)) and entity access. canPress lets the same policy drive a control's enabled state, so the UI and the enforced rule are one declaration.

The control is disabled until the policy is known to be true. This is deliberate and fail-safe: while the data a policy depends on is still loading, the control stays locked, then unlocks the moment the policy resolves true — it never flashes enabled first. If the policy is false, the control stays disabled.

A policy usable on a control must be self-scoped — a check about the caller's own membership or roles, such as:

policy IsPlatformAdmin => PlatformRoleGrant.Any(g => g.User == user && g.Role == PlatformRole.Admin);
policy IsOrgAdmin      => OrganizationMember.Any(m => m.User == user && m.Role != OrgRole.Member);

A policy that compares to data the control doesn't hold (a specific page's row, or an unrelated entity) can't be reflected on a control today and is a compile error — keeping the clean syntax honest about what it can and can't show.

Only the caller's own rows are ever loaded to evaluate the reflection; nothing about other users is read to decide whether your button is enabled.

Saying WHY a control is locked — whenDenied#

Pair a policy prop with whenDenied to explain why a control is unavailable — the sentence shows as a tooltip while the control is locked (disabled or read-only), and is silent once the caller satisfies the policy:

Pressable(onClick: NewOrg,
          canPress: IsPlatformAdmin,
          whenDenied: "Only platform admins can create organizations.") {
  BtnPrimary("New organization") { Icon(Icons.Plus, size: 18); }
}

The message lives at the control, not the policy — the same policy can deny different actions for different reasons. It renders to the native title attribute only while the control is locked, so a normal reader hovers to see why an action is off, and assistive technology reads it either way.

Policy as a value#

Sometimes you don't want to reflect a policy onto one control — you want to branch the whole layout on it, or reuse the answer in several places. A policy can be used directly as a bool value: in an if, or held in a live var.

policy CanManageApp(Application a) => RoleGrant.Any(r => r.User == user && r.Role == AppRole.PlatformAdmin);

[Composable] component AppRosterEditor(Application app) { render { Text(app.Name); } }
[Composable] component AppRosterReadonly(Application app) { render { Text(app.Name); } }

// A page that lays out differently for someone who can manage the app vs. a plain viewer:
component AppDetail(string slug) {
  var app = Application.Single(a => a.Slug == slug);
  render {
    if (CanManageApp(app)) {
      // the manager's view — an editable roster, an invite button …
      AppRosterEditor(app: app);
    } else {
      // the read-only view
      AppRosterReadonly(app: app);
    }
  }
}

Hold the answer once and reuse it with a live var — it tracks reactively, exactly like a control prop:

[Composable] component NavLinks() { render { Text("nav"); } }

[Page("/toolbar")] [Render(CSR)]
component Toolbar() {
  live var canManage = IsPlatformAdmin;   // a bool, reactive to the caller's own rows
  action OpenSettings() { }
  render {
    if (canManage) { Button("Settings", onPress: OpenSettings); }
    NavLinks();
  }
}

Both forms lower to the same self-list reflection a canPress: prop uses — client-computed, reactive, and reading only the caller's own rows. And they carry the same guarantee: this is render-only. Branching the layout hides an affordance a caller can't use; it does not protect anything. The server still enforces the policy on every read and every action, so a page that showed the manager's view to the wrong caller would still refuse every mutation behind it.

The rules match a control prop: the value must be a declared, self-scoped policy (a typo is an unknown-identifier error, not a silently-false branch), and a parameterized policy is applied to a value the page already holds (CanManageApp(app)). A policy is not callable in a server position — you can't put one in an entity query's where; it is the server's own enforcement, never a query term.

See also#

Related

page authorization (policies)

A `policy` names a reusable authorization predicate over the current user — e.g. `policy Admins => UserRole.Any(r =>…

style props

Inside a `variants` block, each `Name = value` is a style prop from a fixed vocabulary the renderer maps to CSS — paint…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…