Skip to content

Commit 8c8d8ce

Browse files
GenerQAQclaude
andauthored
fix(cli): remove unnecessary Supabase auth from public-only commands (#455)
* fix(cli): remove unnecessary Supabase auth from public-only commands Public CLI commands (dash ping, skill upload, etc.) only need an API key, not a Supabase JWT. Move login validation into requireAdmin() and call it only from admin commands (projects list/select/create/delete/stats). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): populate dashUserEmail in PersistentPreRunE for non-admin commands List/create commands for sessions, disks, spaces, and skills use dashUserEmail to scope results to the current user. After removing mandatory Supabase auth, dashUserEmail was only set inside requireAdmin(), leaving it empty for non-project commands. Add a best-effort auth.Load() fallback so the user email is always available when auth.json exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5b03a14 commit 8c8d8ce

6 files changed

Lines changed: 109 additions & 75 deletions

File tree

landingpage/public/SKILL.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,18 @@ All dashboard commands are under `acontext dash`:
123123
Example — upload a skill directory:
124124
```bash
125125
acontext skill upload ./my-skill-dir
126+
acontext skill upload ./my-skill-dir --user alice@example.com
127+
acontext skill upload ./my-skill-dir --api-key sk-ac-xxx
126128
```
127129
The directory must contain a `SKILL.md` with name and description in YAML front-matter.
128130

131+
| Flag | Default | Description |
132+
| ----------- | ------------------------ | ------------------------------ |
133+
| `--user` | logged-in email (if any) | User identifier for the skill |
134+
| `--api-key` | credentials.json default | Project API key |
135+
| `--meta` || Metadata as JSON string |
136+
| `--base-url`|| API base URL override |
137+
129138
### Other CLI Commands
130139

131140
| Command | Description |

src/client/acontext-cli/cmd/dash.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"fmt"
5+
"sync"
56

67
"github.com/memodb-io/Acontext/acontext-cli/internal/api"
78
"github.com/memodb-io/Acontext/acontext-cli/internal/auth"
@@ -23,11 +24,14 @@ var (
2324
dashAccessToken string
2425
)
2526

27+
var adminOnce sync.Once
28+
var adminErr error
29+
2630
// DashCmd is the parent command for all dashboard operations.
2731
var DashCmd = &cobra.Command{
2832
Use: "dash",
2933
Short: "Dashboard operations — manage projects, sessions, skills, and more",
30-
Long: "Interact with the Acontext Dashboard API. Requires login (run 'acontext login' first).",
34+
Long: "Interact with the Acontext Dashboard API. Most commands require an API key; admin commands (projects) also require login.",
3135
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
3236
// Inherit parent persistent pre-run hooks (telemetry, etc.)
3337
if parentE := cmd.Root().PersistentPreRunE; parentE != nil {
@@ -38,23 +42,11 @@ var DashCmd = &cobra.Command{
3842
parent(cmd, args)
3943
}
4044

41-
// 1. Require login
42-
af, err := auth.Load()
43-
if err != nil || af == nil {
44-
return fmt.Errorf("not logged in — run 'acontext login' first")
45-
}
46-
af, err = auth.ValidateAndRefresh(af)
47-
if err != nil {
48-
return fmt.Errorf("session invalid — run 'acontext login' again: %w", err)
49-
}
50-
dashUserEmail = af.User.Email
51-
dashUserID = af.User.ID
52-
dashAccessToken = af.AccessToken
45+
// Reset adminOnce for each command invocation
46+
adminOnce = sync.Once{}
47+
adminErr = nil
5348

54-
// 2. Admin client always available (JWT only)
55-
dashAdminClient = api.NewAdminClient(dashBaseURL, af.AccessToken)
56-
57-
// 3. Resolve API key for /api/v1 routes: --api-key flag > --project flag > credentials.json default
49+
// Resolve API key for /api/v1 routes: --api-key flag > --project flag > credentials.json default
5850
apiKey := dashAPIKey
5951
if apiKey == "" && dashProject != "" {
6052
apiKey = auth.GetProjectKey(dashProject)
@@ -71,7 +63,15 @@ var DashCmd = &cobra.Command{
7163
}
7264

7365
if apiKey != "" {
74-
dashClient = api.NewClient(dashBaseURL, apiKey, af.AccessToken)
66+
dashClient = api.NewClient(dashBaseURL, apiKey)
67+
}
68+
69+
// Best-effort: populate user email from auth.json without token validation.
70+
// This ensures list/create commands scope to the correct user even when
71+
// full admin login is not required.
72+
if af, _ := auth.Load(); af != nil {
73+
dashUserEmail = af.User.Email
74+
dashUserID = af.User.ID
7575
}
7676

7777
return nil
@@ -94,3 +94,25 @@ func requireClient() (*api.Client, error) {
9494
}
9595
return dashClient, nil
9696
}
97+
98+
// requireAdmin validates the Supabase login and creates the admin client.
99+
// It is cached: multiple calls within the same command invocation are no-ops.
100+
func requireAdmin() error {
101+
adminOnce.Do(func() {
102+
af, err := auth.Load()
103+
if err != nil || af == nil {
104+
adminErr = fmt.Errorf("not logged in — run 'acontext login' first")
105+
return
106+
}
107+
af, err = auth.ValidateAndRefresh(af)
108+
if err != nil {
109+
adminErr = fmt.Errorf("session invalid — run 'acontext login' again: %w", err)
110+
return
111+
}
112+
dashAccessToken = af.AccessToken
113+
dashUserEmail = af.User.Email
114+
dashUserID = af.User.ID
115+
dashAdminClient = api.NewAdminClient(dashBaseURL, af.AccessToken)
116+
})
117+
return adminErr
118+
}

src/client/acontext-cli/cmd/dash_projects.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ func init() {
2424
listCmd := &cobra.Command{
2525
Use: "list", Short: "List your organizations and projects",
2626
RunE: func(cmd *cobra.Command, args []string) error {
27+
if err := requireAdmin(); err != nil {
28+
return err
29+
}
2730
orgs, err := auth.ListOrganizations(dashAccessToken, dashUserID)
2831
if err != nil {
2932
return fmt.Errorf("fetch organizations: %w", err)
@@ -74,6 +77,9 @@ func init() {
7477
Short: "Select and configure a default project",
7578
Long: "Interactively select a project, or use --project <id> for non-interactive mode. Generates and saves an API key locally.",
7679
RunE: func(cmd *cobra.Command, args []string) error {
80+
if err := requireAdmin(); err != nil {
81+
return err
82+
}
7783
projectFlag, _ := cmd.Flags().GetString("project")
7884
apiKeyFlag, _ := cmd.Flags().GetString("api-key")
7985
rotateFlag, _ := cmd.Flags().GetBool("rotate")
@@ -147,6 +153,9 @@ func init() {
147153
createCmd := &cobra.Command{
148154
Use: "create", Short: "Create a new project",
149155
RunE: func(cmd *cobra.Command, args []string) error {
156+
if err := requireAdmin(); err != nil {
157+
return err
158+
}
150159
name, _ := cmd.Flags().GetString("name")
151160
orgFlag, _ := cmd.Flags().GetString("org")
152161

@@ -231,6 +240,9 @@ func init() {
231240
deleteCmd := &cobra.Command{
232241
Use: "delete <project-id>", Short: "Delete a project", Args: cobra.ExactArgs(1),
233242
RunE: func(cmd *cobra.Command, args []string) error {
243+
if err := requireAdmin(); err != nil {
244+
return err
245+
}
234246
yes, _ := cmd.Flags().GetBool("yes")
235247
if !yes {
236248
if !auth.IsTTY() {
@@ -261,6 +273,9 @@ func init() {
261273
statsCmd := &cobra.Command{
262274
Use: "stats <project-id>", Short: "Show project statistics", Args: cobra.ExactArgs(1),
263275
RunE: func(cmd *cobra.Command, args []string) error {
276+
if err := requireAdmin(); err != nil {
277+
return err
278+
}
264279
stats, err := dashAdminClient.AdminGetProjectStats(context.Background(), args[0])
265280
if err != nil {
266281
return err

src/client/acontext-cli/cmd/skill_upload.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var (
1919
var SkillCmd = &cobra.Command{
2020
Use: "skill",
2121
Short: "Manage agent skills",
22-
Long: "Upload and manage agent skills. Requires login (run 'acontext login' first).",
22+
Long: "Upload and manage agent skills. Requires an API key (run 'acontext dash projects select' first).",
2323
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
2424
// Inherit parent persistent pre-run hooks (telemetry, etc.)
2525
if parentE := cmd.Root().PersistentPreRunE; parentE != nil {
@@ -30,18 +30,6 @@ var SkillCmd = &cobra.Command{
3030
parent(cmd, args)
3131
}
3232

33-
// Require login
34-
af, err := auth.Load()
35-
if err != nil || af == nil {
36-
return fmt.Errorf("not logged in — run 'acontext login' first")
37-
}
38-
af, err = auth.ValidateAndRefresh(af)
39-
if err != nil {
40-
return fmt.Errorf("session invalid — run 'acontext login' again: %w", err)
41-
}
42-
dashUserEmail = af.User.Email
43-
dashAccessToken = af.AccessToken
44-
4533
// Resolve API key: flag > default project keystore
4634
apiKey := skillAPIKey
4735
if apiKey == "" {
@@ -51,7 +39,7 @@ var SkillCmd = &cobra.Command{
5139
}
5240
}
5341
if apiKey != "" {
54-
dashClient = api.NewClient(skillBaseURL, apiKey, af.AccessToken)
42+
dashClient = api.NewClient(skillBaseURL, apiKey)
5543
}
5644

5745
return nil
@@ -86,7 +74,10 @@ func init() {
8674

8775
user, _ := cmd.Flags().GetString("user")
8876
if user == "" {
89-
user = dashUserEmail
77+
// Best-effort: read email from auth.json without token validation
78+
if af, _ := auth.Load(); af != nil {
79+
user = af.User.Email
80+
}
9081
}
9182
meta, _ := cmd.Flags().GetString("meta")
9283

src/client/acontext-cli/internal/api/client.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ type Client struct {
2323
headers map[string]string
2424
}
2525

26-
// NewClient creates a client that authenticates with both JWT and API key.
27-
// The /api/v1 routes require ProjectAuth (API key) + SupabaseAuth (JWT).
28-
func NewClient(baseURL, apiKey, accessToken string) *Client {
26+
// NewClient creates a client that authenticates with an API key.
27+
// The /api/v1 routes require ProjectAuth (API key) via Authorization header.
28+
func NewClient(baseURL, apiKey string) *Client {
2929
if baseURL == "" {
3030
baseURL = DefaultBaseURL
3131
}
@@ -35,9 +35,6 @@ func NewClient(baseURL, apiKey, accessToken string) *Client {
3535
if apiKey != "" {
3636
headers["Authorization"] = "Bearer " + apiKey
3737
}
38-
if accessToken != "" {
39-
headers["X-Access-Token"] = accessToken
40-
}
4138
return &Client{
4239
baseURL: baseURL,
4340
httpClient: &http.Client{Timeout: requestTimeout},

0 commit comments

Comments
 (0)