Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3aefe53
feat: add authz types and initialize beads tracking
Dec 29, 2025
1459c12
feat: add shared authorization evaluation
Dec 29, 2025
605e091
feat: add access control and repo permission actors
Dec 29, 2025
29db3ff
feat: add test auth and permission evaluator
Dec 29, 2025
eed7e54
feat: add access API parameters and routes
Dec 29, 2025
8eafb0e
feat: add access SDK
Dec 29, 2025
e246923
feat: add access CLI commands
Dec 29, 2025
2e89ccd
feat: enforce authz on repo and storage endpoints
Dec 29, 2025
23d90da
test: add authz unit and integration coverage
Dec 29, 2025
77b7ab2
fix: authz serialization and test setup
Dec 29, 2025
db92213
feat: add auth config and claim mapping
Dec 30, 2025
8d8e47a
chore: close auth config task
Dec 30, 2025
9ab982b
feat: add auth login endpoints
Dec 30, 2025
a3b91bf
chore: close auth login task
Dec 30, 2025
6217728
feat: wire microsoft oidc and jwt auth
Dec 30, 2025
890933f
chore: close auth scheme task
Dec 30, 2025
a802d50
feat: add cli device code auth and sdk token support
Dec 30, 2025
b835379
test: cover auth endpoints and cli auth configuration
Dec 30, 2025
db1aa7a
docs: add microsoft auth setup guide
Dec 30, 2025
fed9ac8
fix: force OIDC code flow for Microsoft auth
Dec 31, 2025
15034a7
fix: ensure scope sent during OIDC code redemption
Dec 31, 2025
01429b2
Add REPO_INDEX.md
Dec 31, 2025
57d142a
Add PAT auth flow and improve Aspire auth wiring
Jan 2, 2026
84f6847
Defer CLI defaults and help output
Jan 3, 2026
04c68b0
Close bead issue Grace-lp2
Jan 3, 2026
d5bfc28
bd sync: 2026-01-02 22:22:40
Jan 3, 2026
79752df
Fix multi-line help defaults
Jan 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
{"id":"Grace-f1u","title":"PAT auth: SDK support","description":"Add SDK wrappers and server URI handling for PAT endpoints.","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-01-01T17:33:44.9629598-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:49:56.514499-08:00","closed_at":"2026-01-01T17:49:56.514499-08:00","close_reason":"Completed"}
{"id":"Grace-f1u.1","title":"Add PAT SDK wrappers","description":"Implement Grace.SDK PersonalAccessToken wrappers for create/list/revoke and update project compile list.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:35:17.2437547-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:49:41.0114696-08:00","closed_at":"2026-01-01T17:49:41.0114696-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-f1u.1","depends_on_id":"Grace-f1u","type":"parent-child","created_at":"2026-01-01T17:35:17.2831975-08:00","created_by":"daemon"}]}
{"id":"Grace-f1u.2","title":"Ensure PAT SDK uses GRACE_SERVER_URI","description":"Make PAT SDK endpoints work without graceconfig.json by using GRACE_SERVER_URI or a helper that bypasses Configuration.Current().","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:35:24.0343531-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:49:49.2757481-08:00","closed_at":"2026-01-01T17:49:49.2757481-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-f1u.2","depends_on_id":"Grace-f1u","type":"parent-child","created_at":"2026-01-01T17:35:24.0786964-08:00","created_by":"daemon"}]}
{"id":"Grace-lp2","title":"Defer CLI defaults for help","description":"Implement deferred defaults + help customization to avoid config access during help. See spec in chat.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-02T21:13:38.8410648-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-02T21:52:40.9246985-08:00","closed_at":"2026-01-02T21:52:40.9246985-08:00","close_reason":"Closed"}
{"id":"Grace-o3z","title":"PAT auth: Orleans actor storage","description":"Add PersonalAccessToken grain interface, state, proxy, and implementation.","status":"closed","priority":1,"issue_type":"epic","created_at":"2026-01-01T17:33:33.121442-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:42:57.8202313-08:00","closed_at":"2026-01-01T17:42:57.8202313-08:00","close_reason":"Completed"}
{"id":"Grace-o3z.1","title":"Add PAT actor constants and interface","description":"Add PersonalAccessToken actor/state names and IPersonalAccessTokenActor interface in Grace.Actors.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:34:29.5312209-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:40:21.9276448-08:00","closed_at":"2026-01-01T17:40:21.9276448-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-o3z.1","depends_on_id":"Grace-o3z","type":"parent-child","created_at":"2026-01-01T17:34:29.5383697-08:00","created_by":"daemon"}]}
{"id":"Grace-o3z.2","title":"Add PAT actor proxy helper","description":"Add ActorProxy.PersonalAccessToken.CreateActorProxy helper in Grace.Actors/ActorProxy.Extensions.Actor.fs.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T17:34:36.1715625-08:00","created_by":"Scott Arbeit","updated_at":"2026-01-01T17:40:54.6647026-08:00","closed_at":"2026-01-01T17:40:54.6647026-08:00","close_reason":"Completed","dependencies":[{"issue_id":"Grace-o3z.2","depends_on_id":"Grace-o3z","type":"parent-child","created_at":"2026-01-01T17:34:36.1768734-08:00","created_by":"daemon"}]}
Expand Down
3 changes: 2 additions & 1 deletion .beads/metadata.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"database": "beads.db",
"jsonl_export": "issues.jsonl"
"jsonl_export": "issues.jsonl",
"last_bd_version": "0.29.0"
}
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This project uses **bd** (beads) for issue tracking. The repo is initialized at the root `.beads`, so run `bd` from the repo root.

## Before You Start (MANDATORY)
- Always run `bd ready --json` from repo root before doing anything else.
- You MUST claim or create an issue before editing code:
- If a suitable issue exists: `bd update <id> --status in_progress`
- If none exist: create a new issue, then set it in progress.
- Do not make code changes until an issue is in progress.

## Quick Reference

```bash
Expand Down
11 changes: 6 additions & 5 deletions src/Grace.CLI.Tests/Grace.CLI.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
<OtherFlags>--test:ParallelIlxGen</OtherFlags>
</PropertyGroup>

<ItemGroup>
<Compile Include="Auth.Tests.fs" />
<Compile Include="HistoryStorage.Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Auth.Tests.fs" />
<Compile Include="HelpDoesNotReadConfig.Tests.fs" />
<Compile Include="HistoryStorage.Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FsCheck" Version="3.2.0" />
Expand Down
115 changes: 115 additions & 0 deletions src/Grace.CLI.Tests/HelpDoesNotReadConfig.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
namespace Grace.CLI.Tests

open FsUnit
open Grace.CLI
open Grace.Shared.Client.Configuration
open Grace.Shared.Utilities
open NUnit.Framework
open System
open System.IO

[<NonParallelizable>]
module HelpDoesNotReadConfigTests =

let private withTempDir (action: string -> unit) =
let tempDir = Path.Combine(Path.GetTempPath(), $"grace-cli-tests-{Guid.NewGuid():N}")
Directory.CreateDirectory(tempDir) |> ignore
let originalDir = Environment.CurrentDirectory

try
Environment.CurrentDirectory <- tempDir
action tempDir
finally
Environment.CurrentDirectory <- originalDir
if Directory.Exists(tempDir) then
try
Directory.Delete(tempDir, true)
with _ ->
()

let private writeInvalidConfig (root: string) =
let graceDir = Path.Combine(root, ".grace")
Directory.CreateDirectory(graceDir) |> ignore
File.WriteAllText(Path.Combine(graceDir, "graceconfig.json"), "not json")

let private writeValidConfig (root: string) (ownerId: Guid) (orgId: Guid) (repoId: Guid) (branchId: Guid) =
let graceDir = Path.Combine(root, ".grace")
Directory.CreateDirectory(graceDir) |> ignore
let config = GraceConfiguration()
config.OwnerId <- ownerId
config.OrganizationId <- orgId
config.RepositoryId <- repoId
config.BranchId <- branchId
let json = serialize config
File.WriteAllText(Path.Combine(graceDir, "graceconfig.json"), json)

[<Test>]
let ``help works with invalid config`` () =
withTempDir (fun root ->
writeInvalidConfig root
let exitCode = GraceCommand.main [| "access"; "grant-role"; "-h" |]
exitCode |> should equal 0)

[<Test>]
let ``help works without config`` () =
withTempDir (fun _ ->
let exitCode = GraceCommand.main [| "access"; "grant-role"; "-h" |]
exitCode |> should equal 0)

[<Test>]
let ``help shows symbolic defaults`` () =
withTempDir (fun _ ->
use writer = new StringWriter()
let originalOut = Console.Out

try
Console.SetOut(writer)
let exitCode = GraceCommand.main [| "access"; "grant-role"; "-h" |]
exitCode |> should equal 0
finally
Console.SetOut(originalOut)

let output = writer.ToString()
output |> should contain "[default: current OwnerId]"
output |> should contain "[default: current OrganizationId]"
output |> should contain "[default: current RepositoryId]"
output |> should contain "[default: current BranchId]"
output |> should contain "[default: new NanoId]"
)

[<Test>]
let ``create help rewrites empty guid defaults`` () =
withTempDir (fun _ ->
use writer = new StringWriter()
let originalOut = Console.Out

try
Console.SetOut(writer)
let exitCode = GraceCommand.main [| "repository"; "create"; "-h" |]
exitCode |> should equal 0
finally
Console.SetOut(originalOut)

let output = writer.ToString()
output |> should contain "[default: current OwnerId]"
output |> should contain "[default: current OrganizationId]"
output |> should contain "[default: new Guid]"
output |> should not' (contain "00000000-0000-0000-0000-000000000000")
)

[<Test>]
let ``getNormalizedIdsAndNames falls back to config ids`` () =
withTempDir (fun root ->
let ownerId = Guid.NewGuid()
let orgId = Guid.NewGuid()
let repoId = Guid.NewGuid()
let branchId = Guid.NewGuid()
writeValidConfig root ownerId orgId repoId branchId

let parseResult = GraceCommand.rootCommand.Parse([| "access"; "grant-role" |])
let graceIds = Services.getNormalizedIdsAndNames parseResult

graceIds.OwnerId |> should equal ownerId
graceIds.OrganizationId |> should equal orgId
graceIds.RepositoryId |> should equal repoId
graceIds.BranchId |> should equal branchId)
18 changes: 4 additions & 14 deletions src/Grace.CLI/Command/Access.CLI.fs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module Access =
Required = false,
Description = "The owner ID <Guid>.",
Arity = ArgumentArity.ZeroOrOne,
DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.Empty else Current().OwnerId)
DefaultValueFactory = (fun _ -> OwnerId.Empty)
)

let organizationId =
Expand All @@ -42,12 +42,7 @@ module Access =
Required = false,
Description = "The organization ID <Guid>.",
Arity = ArgumentArity.ZeroOrOne,
DefaultValueFactory =
(fun _ ->
if Current().OrganizationId = Guid.Empty then
Guid.Empty
else
Current().OrganizationId)
DefaultValueFactory = (fun _ -> OrganizationId.Empty)
)

let repositoryId =
Expand All @@ -57,12 +52,7 @@ module Access =
Required = false,
Description = "The repository ID <Guid>.",
Arity = ArgumentArity.ZeroOrOne,
DefaultValueFactory =
(fun _ ->
if Current().RepositoryId = Guid.Empty then
Guid.Empty
else
Current().RepositoryId)
DefaultValueFactory = (fun _ -> RepositoryId.Empty)
)

let branchId =
Expand All @@ -71,7 +61,7 @@ module Access =
Required = false,
Description = "The branch ID <Guid>.",
Arity = ArgumentArity.ZeroOrOne,
DefaultValueFactory = (fun _ -> if Current().BranchId = Guid.Empty then Guid.Empty else Current().BranchId)
DefaultValueFactory = (fun _ -> BranchId.Empty)
)

let principalTypeRequired =
Expand Down
16 changes: 3 additions & 13 deletions src/Grace.CLI/Command/Admin.CLI.fs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module Admin =
Required = false,
Description = "The repository's owner ID <Guid>.",
Arity = ArgumentArity.ZeroOrOne,
DefaultValueFactory = (fun _ -> if Current().OwnerId = Guid.Empty then Guid.Empty else Current().OwnerId)
DefaultValueFactory = (fun _ -> OwnerId.Empty)
)

let ownerName =
Expand All @@ -54,12 +54,7 @@ module Admin =
Required = false,
Description = "The repository's organization ID <Guid>.",
Arity = ArgumentArity.ZeroOrOne,
DefaultValueFactory =
(fun _ ->
if Current().OrganizationId = Guid.Empty then
Guid.Empty
else
Current().OrganizationId)
DefaultValueFactory = (fun _ -> OrganizationId.Empty)
)

let organizationName =
Expand All @@ -77,12 +72,7 @@ module Admin =
Required = false,
Description = "The repository's ID <Guid>.",
Arity = ArgumentArity.ExactlyOne,
DefaultValueFactory =
(fun _ ->
if Current().RepositoryId = Guid.Empty then
Guid.Empty
else
Current().RepositoryId)
DefaultValueFactory = (fun _ -> RepositoryId.Empty)
)

let repositoryName =
Expand Down
Loading
Loading