From 56906f898a80af837f3d1c563d5b55ee6ee1e1b0 Mon Sep 17 00:00:00 2001 From: NECIB AMARA Date: Tue, 21 Jul 2026 14:40:39 +0100 Subject: [PATCH 01/11] chore: prepare scheduled demo follow-up From a948034eed76fe29dad8686dd2176c4abf855266 Mon Sep 17 00:00:00 2001 From: AmaraNecib Date: Tue, 21 Jul 2026 15:15:28 +0100 Subject: [PATCH 02/11] =?UTF-8?q?55:=20demo=20=E2=80=94=20make=20Admin=20d?= =?UTF-8?q?elete=20schedule-aware=20(Refs=20#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++ examples/interactive-decision-demo/README.md | 9 +-- examples/interactive-decision-demo/index.html | 2 +- examples/interactive-decision-demo/main.ts | 72 +++++++++++++++---- examples/interactive-decision-demo/style.css | 6 +- 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 395566e..033a905 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ dist/ .DS_Store .pi-subagents/ .scratch/ +videos/ +.playwright-cli/ +.playwright-mcp/ +.worktrees/ diff --git a/examples/interactive-decision-demo/README.md b/examples/interactive-decision-demo/README.md index 5cfc20f..223282f 100644 --- a/examples/interactive-decision-demo/README.md +++ b/examples/interactive-decision-demo/README.md @@ -45,16 +45,17 @@ Then open **http://localhost:3000** (or whatever port your server uses) in a bro | `cars.read` | ✅ | ✅ | ✅ | | `cars.create` | ✅ | ✅ | ✅ | | `cars.update` | ✅ | ✅ | ❌ `no-grant` | -| `cars.delete` | ✅ | ✅ | ❌ `matching-denial` | +| `cars.delete` | ✅ | ✅ (schedule) | ❌ `matching-denial` | | `manage-policy` | ✅ | ❌ `no-grant` | ❌ `no-grant` | | `reports.read` | ✅ (schedule) | ❌ `no-grant` | ❌ `no-grant` | ## How the schedule works Super Admin can enable/disable the schedule restriction on `reports.read` and -adjust the UTC business-hours window. A controllable evaluation clock advances -or rewinds time — within hours the permission allows, outside it returns -`outside-schedule`. Admin and Support cannot modify these settings. +Admin's `cars.delete`, adjusting the UTC business-hours window for both. +A controllable evaluation clock advances or rewinds time — within hours the +permission allows, outside it returns `outside-schedule`. +Admin and Support cannot modify these settings. ## Stack diff --git a/examples/interactive-decision-demo/index.html b/examples/interactive-decision-demo/index.html index dac196f..a99d360 100644 --- a/examples/interactive-decision-demo/index.html +++ b/examples/interactive-decision-demo/index.html @@ -55,7 +55,7 @@

Deny overrides

Temporal schedule

-

Hypothetical: reports.read evaluated against a configurable schedule.

+

Configure the weekday work-hours schedule. Affects reports.read (Super Admin) and cars.delete (Admin).

@@ -131,7 +96,34 @@

Temporal schedule

- + +
+ +
+ + : + + + + : + + UTC +
+
+ +
+ + + +
+
+
+
+ +
-

Schedule controls are in the toolbar above. Affects reports.read (Super Admin) and cars.delete (Admin).

+

Schedule controls are in the toolbar above, visible to all principals. Only Super Admin can modify them. Controls Admin's cars.delete permission.

@@ -100,7 +100,7 @@

Deny overrides

diff --git a/examples/interactive-decision-demo/main.ts b/examples/interactive-decision-demo/main.ts index c3da777..5d39fcc 100644 --- a/examples/interactive-decision-demo/main.ts +++ b/examples/interactive-decision-demo/main.ts @@ -166,9 +166,6 @@ const adapter = new MemoryAdapter({ permissions: [ { permission: "cars.*", effect: "grant" }, { permission: "manage-policy", effect: "grant" }, - // NOTE: reports.read is NOT in the role definition. - // It lives solely in the mutable policy source so the schedule - // editor is the single source of truth for that permission. ], }, { @@ -197,16 +194,11 @@ const adapter = new MemoryAdapter({ useMemoryAdapter(mizan, adapter); -// Policy source: Support overrides + sole reports.read. +// Policy source holds per-principal overrides managed through the editor. const policySource = new MutablePolicySource(); function applyDefaultPolicyFacts(): void { policySource.addFact("support", { permission: "cars.delete", effect: "deny" }); - policySource.addFact("super-admin", { - permission: "reports.read", - effect: "grant", - schedule: makeWeekSchedule(9, 0, 17, 0), - }); policySource.addFact("admin", { permission: "cars.delete", effect: "grant", @@ -215,10 +207,9 @@ function applyDefaultPolicyFacts(): void { } function restorePolicyFactsFromSaved(saved: SavedState): void { - // Wipe dynamic facts for the three principals we manage. + // Wipe dynamic facts for the two principals we manage. policySource.removeAllFacts("support", "cars.delete"); policySource.removeAllFacts("support", "cars.update"); - policySource.removeAllFacts("super-admin", "reports.read"); policySource.removeAllFacts("admin", "cars.delete"); if (saved.deleteDeny) { @@ -228,14 +219,6 @@ function restorePolicyFactsFromSaved(saved: SavedState): void { policySource.addFact("support", { permission: "cars.update", effect: "grant" }); } if (saved.scheduleEnabled) { - policySource.addFact("super-admin", { - permission: "reports.read", - effect: "grant", - schedule: makeWeekSchedule( - saved.scheduleStartH, saved.scheduleStartM, - saved.scheduleEndH, saved.scheduleEndM, - ), - }); policySource.addFact("admin", { permission: "cars.delete", effect: "grant", @@ -245,10 +228,6 @@ function restorePolicyFactsFromSaved(saved: SavedState): void { ), }); } else { - policySource.addFact("super-admin", { - permission: "reports.read", - effect: "grant", - }); policySource.addFact("admin", { permission: "cars.delete", effect: "grant", @@ -270,6 +249,25 @@ function getEvaluator(id: PrincipalId) { return evaluators[id]; } +/** + * Evaluate a permission against the demo evaluation clock, not the real + * system clock. All UI-facing authorization checks must use this helper + * so schedule-controlled permissions (e.g., Admin cars.delete) are + * consistently evaluated at the demo clock time everywhere — sidebar, + * table buttons, and actual action clicks. + */ +async function decideAt(permission: string): Promise { + return getEvaluator(currentPrincipal).decide(permission, { at: clockTime }); +} + +async function safeDecideAt(perm: string): Promise { + try { + return await decideAt(perm); + } catch { + return { decision: "deny", reason: "contract-violation" }; + } +} + // ─── Application state ───────────────────────────────────────────────────── let currentPrincipal: PrincipalId = "super-admin"; @@ -408,12 +406,11 @@ async function renderPermissions(): Promise { "cars.update", "cars.delete", "manage-policy", - "reports.read", ]; const results = await Promise.all( perms.map(async (p) => { try { - const r = await getEvaluator(currentPrincipal).decide(p); + const r = await decideAt(p); return { permission: p, result: r }; } catch { return { permission: p, result: { decision: "deny" as const, reason: "error" } }; @@ -480,9 +477,8 @@ function updateTrace( // ─── Protected action path (cars table) ──────────────────────────────────── async function attemptAction(action: CarAction, onAllowed: () => void): Promise { - const evaluator = getEvaluator(currentPrincipal); try { - const result = await evaluator.decide(action); + const result = await decideAt(action); showDecision(currentPrincipal, action, result.decision, result.reason); if (result.decision === "allow") { @@ -512,9 +508,8 @@ async function attemptManagement( onAllowed: () => void, label: string = "Policy change", ): Promise { - const evaluator = getEvaluator(currentPrincipal); try { - const result = await evaluator.decide("manage-policy"); + const result = await decideAt("manage-policy"); showDecision(currentPrincipal, "manage-policy", result.decision, result.reason); if (result.decision === "allow") { @@ -540,9 +535,9 @@ async function renderTable(): Promise { const tbody = byId("cars-tbody"); const [readResult, updateResult, deleteResult] = await Promise.all([ - safeDecide("cars.read"), - safeDecide("cars.update"), - safeDecide("cars.delete"), + safeDecideAt("cars.read"), + safeDecideAt("cars.update"), + safeDecideAt("cars.delete"), ]); tbody.innerHTML = ""; @@ -590,7 +585,7 @@ async function renderTable(): Promise { } } - const createResult = await safeDecide("cars.create"); + const createResult = await safeDecideAt("cars.create"); const createBtn = byId("create-car-btn"); if (createResult.decision === "allow") { createBtn.removeAttribute("disabled"); @@ -639,16 +634,20 @@ function addActionBtn( container.appendChild(btn); } -async function safeDecide(perm: string): Promise { - try { - return await getEvaluator(currentPrincipal).decide(perm); - } catch { - return { decision: "deny", reason: "contract-violation" }; - } -} - // ─── Principal switching ─────────────────────────────────────────────────── +function syncScheduleUI(): void { + const isSuper = currentPrincipal === "super-admin"; + byId("toggle-schedule").disabled = !isSuper; + byId("schedule-start-h").disabled = !isSuper; + byId("schedule-start-m").disabled = !isSuper; + byId("schedule-end-h").disabled = !isSuper; + byId("schedule-end-m").disabled = !isSuper; + byId("clock-dec").disabled = !isSuper; + byId("clock-inc").disabled = !isSuper; + byId("clock-reset").disabled = !isSuper; +} + async function setPrincipal(id: PrincipalId): Promise { currentPrincipal = id; @@ -662,8 +661,10 @@ async function setPrincipal(id: PrincipalId): Promise { byId("policy-super-admin").hidden = !isSuper; byId("policy-locked").hidden = isSuper; - // Schedule controls visible to Super Admin and Admin (not Support). - byId("schedule-bar").hidden = id === "support"; + // Schedule bar is visible to all principals (so everyone sees the status) + // but only Super Admin can interact with the controls. + byId("schedule-bar").hidden = false; + syncScheduleUI(); document.querySelectorAll(".actor-name").forEach((el) => { el.style.display = el.dataset.actor === id ? "inline" : "none"; @@ -749,64 +750,38 @@ function updateClockDisplay(): void { } /** - * Replace schedule-controlled facts (reports.read for super-admin, cars.delete for admin) - * with ones matching the current controls, then display both results. + * Replace the schedule-controlled facts with ones matching the current controls, + * then display the result for Admin's cars.delete. */ async function evaluateSchedule(): Promise { - policySource.removeAllFacts("super-admin", "reports.read"); policySource.removeAllFacts("admin", "cars.delete"); if (scheduleEnabled) { - policySource.addFact("super-admin", { - permission: "reports.read", - effect: "grant", - schedule: makeWeekSchedule(scheduleStartH, scheduleStartM, scheduleEndH, scheduleEndM), - }); policySource.addFact("admin", { permission: "cars.delete", effect: "grant", schedule: makeWeekSchedule(scheduleStartH, scheduleStartM, scheduleEndH, scheduleEndM), }); } else { - policySource.addFact("super-admin", { - permission: "reports.read", - effect: "grant", - }); policySource.addFact("admin", { permission: "cars.delete", effect: "grant", }); } - const evalSA = getEvaluator("super-admin"); - const evalAdmin = getEvaluator("admin"); try { - const [saResult, adminResult] = await Promise.all([ - evalSA.decide("reports.read", { at: clockTime }), - evalAdmin.decide("cars.delete", { at: clockTime }), - ]); - + const result = await getEvaluator("admin").decide("cars.delete", { at: clockTime }); const el = byId("schedule-result"); - const saOk = saResult.decision === "allow"; - const adminOk = adminResult.decision === "allow"; - - el.innerHTML = `
${ - saOk - ? scheduleEnabled - ? "✓ reports.read (Super Admin) — inside schedule" - : "✓ reports.read (Super Admin) — no restriction" - : `✗ reports.read (Super Admin) — ${saResult.reason ?? "unknown"}` - }
${ - adminOk + const isAllow = result.decision === "allow"; + el.innerHTML = `
${ + isAllow ? scheduleEnabled ? "✓ cars.delete (Admin) — inside schedule" : "✓ cars.delete (Admin) — no restriction" - : `✗ cars.delete (Admin) — ${adminResult.reason ?? "unknown"}` + : `✗ cars.delete (Admin) — ${result.reason ?? "unknown"}` }
`; el.className = "schedule-result"; - // Refresh the cars table and permissions sidebar so they reflect - // the updated schedule state (e.g., Admin's Delete buttons). await Promise.all([ renderTable(), renderPermissions(), @@ -819,7 +794,14 @@ async function evaluateSchedule(): Promise { function setupScheduleControls(): void { const toggle = byId("toggle-schedule"); toggle.addEventListener("change", async () => { - scheduleEnabled = toggle.checked; + const checked = toggle.checked; + const granted = await attemptManagement(() => { + scheduleEnabled = checked; + }, `Toggle schedule: ${checked ? "enable" : "disable"}`); + if (!granted) { + toggle.checked = !checked; + return; + } await evaluateSchedule(); saveState(); }); @@ -831,7 +813,19 @@ function setupScheduleControls(): void { if (isNaN(val)) return; const max = input.classList.contains("minute") ? 59 : 23; input.value = String(Math.max(0, Math.min(max, val))); - setter(Number(input.value)); + + const granted = await attemptManagement(() => { + setter(Number(input.value)); + }, "Adjust schedule hours"); + if (!granted) { + const isHour = !input.classList.contains("minute"); + input.value = String( + id.includes("start") + ? (isHour ? scheduleStartH : scheduleStartM) + : (isHour ? scheduleEndH : scheduleEndM), + ); + return; + } await evaluateSchedule(); saveState(); }); @@ -843,21 +837,30 @@ function setupScheduleControls(): void { setupHourInput("schedule-end-m", (v) => { scheduleEndM = v; }); byId("clock-inc").addEventListener("click", async () => { - clockTime = new Date(clockTime.getTime() + 3_600_000); + const granted = await attemptManagement(() => { + clockTime = new Date(clockTime.getTime() + 3_600_000); + }, "Advance clock"); + if (!granted) return; updateClockDisplay(); await evaluateSchedule(); saveState(); }); byId("clock-dec").addEventListener("click", async () => { - clockTime = new Date(clockTime.getTime() - 3_600_000); + const granted = await attemptManagement(() => { + clockTime = new Date(clockTime.getTime() - 3_600_000); + }, "Rewind clock"); + if (!granted) return; updateClockDisplay(); await evaluateSchedule(); saveState(); }); byId("clock-reset").addEventListener("click", async () => { - clockTime = new Date(INITIAL_CLOCK); + const granted = await attemptManagement(() => { + clockTime = new Date(INITIAL_CLOCK); + }, "Reset clock"); + if (!granted) return; updateClockDisplay(); await evaluateSchedule(); saveState(); @@ -891,7 +894,7 @@ function hideCreateForm(): void { function setupCreateForm(): void { byId("create-car-btn").addEventListener("click", async () => { - const result = await getEvaluator(currentPrincipal).decide("cars.create"); + const result = await decideAt("cars.create"); if (result.decision === "allow") { showCreateForm(); } else { @@ -957,7 +960,7 @@ function buildUpdateEditorRow(car: Car): HTMLTableRowElement { return; } // Re-check authorization before mutating. - const result = await getEvaluator(currentPrincipal).decide("cars.update"); + const result = await decideAt("cars.update"); if (result.decision !== "allow") { showDecision(currentPrincipal, "cars.update", result.decision, result.reason); showFeedback("cars.update no longer allowed", "error"); diff --git a/examples/interactive-decision-demo/style.css b/examples/interactive-decision-demo/style.css index 2cc9f12..8aa5416 100644 --- a/examples/interactive-decision-demo/style.css +++ b/examples/interactive-decision-demo/style.css @@ -300,6 +300,17 @@ body { outline-offset: 1px; } +/* Disabled controls — non-Super Admin sees them greyed out */ +.schedule-bar :disabled { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; +} +.schedule-bar .schedule-toggle-compact:has(:disabled) { + cursor: not-allowed; + opacity: 0.7; +} + /* ─── Controls bar ──────────────────────────────────────────────────── */ .controls-bar { display: flex; flex-wrap: wrap; gap: var(--space-md) var(--space-xl); From 675d16f1d6b8a8143801ea49daa5255d3f3a53fe Mon Sep 17 00:00:00 2001 From: AmaraNecib Date: Tue, 21 Jul 2026 23:00:46 +0100 Subject: [PATCH 08/11] 55: document hackathon story and AI workflow (Refs #36) --- README.md | 134 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 122 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 31a9a32..370876b 100644 --- a/README.md +++ b/README.md @@ -212,22 +212,132 @@ It does not currently: Those responsibilities stay with the host application or optional integrations. A production application should obtain a trusted principal from its authentication layer, resolve the required authorization facts through its adapters, call Mizan, and enforce the result on the server or API boundary. -## Built with Codex +## Inspiration -Mizan was developed for OpenAI Build Week using Codex and GPT-5.6. +Authorization is rebuilt inside almost every application, even though the +underlying questions are often the same: does this principal have this +permission, does a denial override a grant, is the access scoped or temporary, +and what should happen when the application's data model is different? -The human builder owned the product direction, architectural decisions, acceptance criteria, trade-offs, and final review. Codex and supporting AI agents were used to: +We wanted a small open-source decision layer that could be reused across +projects without forcing a database schema, ORM, authentication provider, or +cache. The central idea became simple: adapters provide facts; Mizan makes the +final authorization decision. -- turn the authorization concept into a small, testable v0.1 architecture; -- design the source/adapter boundary and decision model; -- implement the core evaluator and memory adapter; -- create tests for grants, denials, scopes, temporal windows, schedules, and source behavior; -- review changes, find edge cases, and improve documentation; -- build the interactive decision demo used for evaluation. +## What it does + +Mizan is a runtime-neutral, TypeScript-first authorization library. It +evaluates: + +- role-derived and direct grants; +- direct denial overrides, with denial taking precedence; +- exact permissions and patterns such as `files.*` and `*`; +- global and scoped facts; +- absolute validity windows with `startsAt` and `expiresAt`; +- recurring weekly and date-specific schedules; +- facts from named sources and source plans; +- explainable `allow` or `deny` decisions with stable reason codes. + +The interactive demo makes these decisions visible with Super Admin, Admin, +and Support principals. It demonstrates policy management, a scheduled Admin +delete permission, a Support denial override, a controllable evaluation clock, +and protected actions that call the real Mizan evaluator before changing demo +state. + +Mizan is authorization, not authentication. The host application remains +responsible for users, sessions, JWTs, cookies, persistence, revocation, +caching, and server-side enforcement. -The important design decision is that AI-assisted implementation does not move application data ownership into Mizan: adapters provide facts, while Mizan remains the decision layer. +## How we built it -The required Codex session information is provided in the Devpost submission rather than committed to this repository. +Mizan is a fixed-version TypeScript monorepo. The core package owns the +decision algorithm and source contracts, while the memory package provides a +small reference adapter for tests and examples. This keeps the core +independent from Prisma, Drizzle, PostgreSQL, SQLite, Redis, or any other +storage choice. + +### Codex and GPT-5.6 collaboration + +Mizan was developed for OpenAI Build Week using Codex and GPT-5.6 as the +planning, architecture, and review layer. The human builder owned the product +direction, architectural decisions, acceptance criteria, trade-offs, and final +review. + +ChatGPT/GPT-5.6 and Codex were used to: + +- turn the authorization concept into a small, testable v0.1 architecture; +- design the source/adapter boundary and decision model; +- break the work into milestones and focused implementation tasks; +- define acceptance criteria and test scenarios; +- inspect changes, find edge cases, and review the resulting behavior; +- guide the documentation and interactive demo story. + +Coding-capable worker agents/models carried out the implementation tasks under +those decisions. This was a deliberate separation: stronger reasoning models +focused on architecture and review, while specialized coding agents handled +the repository changes. Codex was the shared engineering workspace and +workflow used to coordinate that collaboration; it is not presented as the +sole author of the implementation. + +The important boundary is that AI-assisted implementation does not move +application data ownership into Mizan: adapters provide facts, while Mizan +remains the decision layer. + +The required Codex session information is provided in the Devpost submission +rather than committed to this repository. + +## Challenges we ran into + +The main challenge was balancing useful defaults with freedom for existing +applications. A library that owns the schema is easy to start with but quickly +becomes difficult to reuse, so we kept storage and authentication outside the +core and made the adapter boundary explicit. + +We also had to make precedence and time behavior visible rather than hiding it +inside a boolean helper. A direct denial must remain stronger than a role grant, +and a scheduled grant must be evaluated against one consistent clock across the +sidebar, table actions, and actual mutation path. Finally, the multi-agent +workflow required tests, review gates, and human decisions so that speed did not +replace correctness. + +## Accomplishments that we're proud of + +- A small reusable authorization core with no ORM or authentication coupling. +- A memory adapter that acts as a reference for custom adapters. +- A fixed-version monorepo structure ready for future integrations. +- Explainable decisions with stable denial reasons instead of opaque booleans. +- Coverage for role grants, direct grants, denial precedence, scopes, temporal + windows, schedules, and source behavior. +- An interactive browser demo that shows the decision layer working end to end. +- A documented AI-assisted development process using Codex and GPT-5.6. + +## What we learned + +We learned that the most reusable abstraction is not a database model but a +clear capability boundary: the adapter translates application data into facts, +and the authorization engine decides. We also reinforced that UI visibility is +only a user-experience concern; every protected operation must be checked again +at the server or API boundary. + +Working with AI agents also made the engineering process itself important. +Small milestones, explicit acceptance criteria, automated tests, adversarial +review, and a final human decision made the collaboration much more reliable +than asking one model to generate an entire system without checkpoints. + +## What's next for Mizan + +The next steps are driven by real applications rather than by trying to solve +every authorization problem at once: + +- keep the v0.1 core stable while improving adapter ergonomics and examples; +- add useful, tested integrations for common database and framework setups; +- support composed sources for database facts, cache lookups, and revocation + workflows without moving those responsibilities into the core; +- explore resource-aware rules and a small ABAC extension for ownership and + tenant-aware decisions; +- add optional tooling for policy import, synchronization, audit events, and + administration; +- publish stable fixed-version releases as the API matures toward v1. ## Hackathon testing path @@ -252,4 +362,4 @@ Mizan is an early open-source release focused on a dependable decision core and ## License -Mizan is released under the MIT License. See LICENSE. \ No newline at end of file +Mizan is released under the MIT License. See LICENSE. From 99b0f13eee7886522791cd5d327bafb887ef6c0d Mon Sep 17 00:00:00 2001 From: AmaraNecib Date: Tue, 21 Jul 2026 23:08:56 +0100 Subject: [PATCH 09/11] =?UTF-8?q?55:=20fix=20README=20wording=20=E2=80=94?= =?UTF-8?q?=20clarify=20schedule=20enforcement=20vs=20manage-policy=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/interactive-decision-demo/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/interactive-decision-demo/README.md b/examples/interactive-decision-demo/README.md index 101f1d4..d309ece 100644 --- a/examples/interactive-decision-demo/README.md +++ b/examples/interactive-decision-demo/README.md @@ -55,9 +55,10 @@ advances or rewinds time — within hours the permission allows, outside it returns `outside-schedule`. Admin and Support can see the schedule status but cannot modify the settings. -The schedule rule is enforced through the real Mizan `manage-policy` decision — -non-Super Admin users who attempt to change the schedule will be blocked by the -authorization engine, not just by disabled UI controls. +The schedule restriction is enforced by the real Mizan `cars.delete` decision +evaluated at the demo clock time. Changes to the schedule are separately gated +by the real Mizan `manage-policy` decision, so non-Super Admin users are blocked +by the authorization engine, not just by disabled UI controls. ## Stack From ba9f83fe9125d07aa4b9049aaa1111f3f2e4e75e Mon Sep 17 00:00:00 2001 From: AmaraNecib Date: Tue, 21 Jul 2026 23:11:28 +0100 Subject: [PATCH 10/11] 55: clarify schedule-controlled Admin access (Refs #36) --- README.md | 4 ++-- examples/interactive-decision-demo/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 370876b..e286d39 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ The demo is an authorization decision gallery, not an authentication or administ It lets you switch between: - **Super Admin** — can manage the demo policy and inspect the schedule controls; -- **Admin** — has full car actions but cannot manage policy; +- **Admin** — can read, create, and update cars; scheduled delete access is available only inside the configured work window, and policy management is still locked; - **Support** — can read and create, has a direct denial for delete, and can receive a live direct grant for update from the Super Admin policy editor. The demo shows: @@ -190,7 +190,7 @@ bun run build To test the browser demo, run bun run dev, then exercise the three principals and verify: -1. Admin can perform the car actions but cannot open policy controls. +1. Admin can read, create, and update cars; scheduled delete works inside the configured window and is denied outside it, while policy controls remain locked. 2. Support can read and create, but delete is denied with matching-denial. 3. Super Admin can grant Support cars.update and the decision changes without a page reload. 4. The schedule allows access inside its window and returns outside-schedule outside it. diff --git a/examples/interactive-decision-demo/README.md b/examples/interactive-decision-demo/README.md index d309ece..f270a98 100644 --- a/examples/interactive-decision-demo/README.md +++ b/examples/interactive-decision-demo/README.md @@ -28,7 +28,7 @@ Then open **http://localhost:3000** (or whatever port your server uses) in a bro | Concept | Implementation | |---------|---------------| -| **Three principals** | Super Admin (full access + policy management), Admin (full cars access), Support (restricted) | +| **Three principals** | Super Admin (full access + policy management), Admin (read/create/update plus schedule-controlled delete), Support (restricted) | | **Protected actions** | Every cars-table click calls `decide()` through Mizan before mutating state | | **Denial reasons** | `matching-denial` for the delete override, `no-grant` for missing update permission | | **Decision banner** | Current decision shown above the fold: actor, action, ALLOW/DENY, reason | From bb895b6d10f41435823cfc87127f8c82e47e78c3 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 00:51:52 +0000 Subject: [PATCH 11/11] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e286d39..3b9f6e6 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ The demo is an authorization decision gallery, not an authentication or administ It lets you switch between: - **Super Admin** — can manage the demo policy and inspect the schedule controls; -- **Admin** — can read, create, and update cars; scheduled delete access is available only inside the configured work window, and policy management is still locked; +- **Admin** — can read, create, and update cars; scheduled delete access is restricted to the configured work window only when scheduling is enabled (when disabled, car deletion is unrestricted), and policy management remains locked; - **Support** — can read and create, has a direct denial for delete, and can receive a live direct grant for update from the Super Admin policy editor. The demo shows: @@ -190,7 +190,7 @@ bun run build To test the browser demo, run bun run dev, then exercise the three principals and verify: -1. Admin can read, create, and update cars; scheduled delete works inside the configured window and is denied outside it, while policy controls remain locked. +1. Admin can read, create, and update cars; when scheduling is enabled, delete access is restricted to the configured work window and denied outside it; when scheduling is disabled, car deletion is unrestricted. Policy controls remain locked regardless. 2. Support can read and create, but delete is denied with matching-denial. 3. Super Admin can grant Support cars.update and the decision changes without a page reload. 4. The schedule allows access inside its window and returns outside-schedule outside it.