code.oscarkilo.com/okg

Hash:
a1b608958af930fdc13c1afc37a73dfbff3ab70e
Author:
Igor Naverniouk <[email protected]>
Date:
Fri Jun 5 16:16:49 2026 -0400
Message:
okg/who: add AppDataEntry + AppData field; document position Two prep moves: 1. AppDataEntry type with App/Key/Value/Timestamp fields, referenced by Profile.AppData (previously omitted) and User.AppData (newly added). 2. User struct gains an AppData field. Stays strictly public-safe — no internal-only fields. README.md picks up a "Position" section framing okg as the public entry point for external callers (agents, scripts, anything outside Oscar Kilo's infrastructure). Also scrubs implementation-internal references from package docs and inline comments — okg's source is itself world-public.
diff --git a/README.md b/README.md
index c8f3f6d..fc267cd 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,19 @@ services into one binary. Today: git ops against
Designed for both humans and AI agents (Claude, OpenClaw) to
use directly from the command line.

+## Position
+
+okg is world-public. It runs anywhere — on developer laptops,
+in cloud workers, in agent sandboxes — and authenticates
+against OscarKilo services with a Klex API key saved to
+`~/.config/okg/config.json`. The wire formats it sends and
+decodes are deliberately limited to fields that are safe to
+expose outside any data center.
+
+If you're writing an LLM agent, an automation script, or
+anything else that runs outside Oscar Kilo's infrastructure,
+okg is the entry point.
+
## Install

```bash
diff --git a/chat/chat.go b/chat/chat.go
index 29ca0fa..c31a40e 100644
--- a/chat/chat.go
+++ b/chat/chat.go
@@ -1,16 +1,10 @@
-// Package chat is the public HTTP client for OscarKilo's //chat
-// service.
+// Package chat is the public HTTP client for OscarKilo's chat
+// service. It is used by `okg` and any other tool that talks
+// to oscarkilo.com over the network.
//
-// Wire types mirror //clients/chat's domain types (Message,
-// Filter) and //chat/server's request/response wrappers
-// (SendRequest, SearchRequest, SearchResponse). The
-// duplication is deliberate — okg is world-public, //clients
-// is intra-cluster — and will get the same dot-import
-// treatment as //okg/who when both packages stabilize.
-//
-// The HTTP struct is named HTTPClient (not Client) so a future
-// `import . "oscarkilo.com/okg/chat"` inside //clients/chat
-// doesn't collide with that package's existing Client type.
+// The struct here is named HTTPClient (not just Client) so
+// applications can wrap it in their own Client abstractions
+// without name collision.
package chat

import "time"
@@ -27,9 +21,9 @@ func NewHTTPClient(host, apiKey string) *HTTPClient {
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
}

-// Message mirrors //clients/chat.Message. No JSON tags — the
-// server marshals with default Go field names (PascalCase).
-// FUTURE: snake_case once //clients/chat gets JSON tags.
+// Message is a single chat message. No JSON tags — the server
+// marshals with default Go field names (PascalCase). FUTURE:
+// switch to snake_case once the server adds JSON tags.
type Message struct {
From string
To string
@@ -58,7 +52,7 @@ type SearchRequest struct {
To string `json:"to,omitempty"`
}

-// SearchResponse mirrors //chat/server.SearchResponse.
+// SearchResponse is the body of POST /chat/search's response.
type SearchResponse struct {
Messages []*Message `json:"messages"`
}
diff --git a/who/who.go b/who/who.go
index a37d71c..d3999c7 100644
--- a/who/who.go
+++ b/who/who.go
@@ -1,17 +1,14 @@
// Package who is the public HTTP client for OscarKilo's //who
-// service. It is the world-public side of //who, used by
-// `okg` and any other tool that talks to oscarkilo.com over
-// the network.
+// service. It is used by `okg` and any other tool that talks
+// to oscarkilo.com over the network.
//
-// //clients/who (intra-cluster, has the in-process MockClient,
-// the /internal/* calls, the dev/prod factory) will eventually
-// import this package via `import . "oscarkilo.com/okg/who"`
-// so the two share types and request paths. To make that dot
-// import work, the struct here is named HTTPClient — leaving
-// the unqualified `Client` name free for //clients/who's
-// existing interface.
+// The struct here is named HTTPClient (not just Client) so
+// applications can wrap it in their own Client abstractions
+// without name collision.
package who

+import "time"
+
import "oscarkilo.com/okg/internal/rest"

// HTTPClient talks to a //who server over HTTPS. Embeds the
@@ -24,22 +21,32 @@ func NewHTTPClient(host, apiKey string) *HTTPClient {
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
}

+// ---- Common types ----
+
+// AppDataEntry is one user-scoped app-data record served by
+// //who (per-user key/value, namespaced by app).
+type AppDataEntry struct {
+ App string `json:"app"`
+ Key string `json:"key"`
+ Value string `json:"value"`
+ Timestamp time.Time `json:"timestamp"`
+}
+
// ---- Caller identity ----

// Profile is the caller's //who profile. Mirrors the
// JSON-serialized subset of //who.Profile that GET
-// /login/profile/get returns. Extra fields the server may
-// return (ApiKeys, Logins, AppData, timestamps) are decoded
-// permissively if added later; we just don't expose them here
-// until okg has a use for them.
+// /login/profile/get returns. ApiKeys, Logins, and timestamps
+// are not exposed here yet — add when okg has a use.
type Profile struct {
- Owid string `json:"owid"`
- Username string `json:"username"`
- Name string `json:"name"`
- Email string `json:"email"`
- PortraitUrl string `json:"portrait_url"`
- OwnerOwid string `json:"owner_owid"`
- Groups []string `json:"groups"`
+ Owid string `json:"owid"`
+ Username string `json:"username"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ PortraitUrl string `json:"portrait_url"`
+ OwnerOwid string `json:"owner_owid"`
+ Groups []string `json:"groups"`
+ AppData []AppDataEntry `json:"app_data,omitempty"`
}

// GetProfile returns the caller's own profile, identified by
@@ -55,7 +62,7 @@ func (c *HTTPClient) GetProfile() (*Profile, error) {
// ---- Groups ----

// Group describes a //who group (a named entity with members).
-// Mirrors the listedGroup shape in //who/server/groups_list.go.
+// Matches the row shape in /groups/list's response.
type Group struct {
Owid string `json:"owid"`
Username string `json:"username"`
@@ -158,11 +165,12 @@ func (c *HTTPClient) DeleteGroup(req DeleteGroupRequest) error {

// ---- Authz ----

-// User is a thin //who-user identifier used inside AuthzEntry.
-// Mirrors the Owid/Username subset of //clients/who.User.
+// User is the public //who-user identifier. It carries only
+// the fields safe to expose to external callers.
type User struct {
- Owid string `json:"owid"`
- Username string `json:"username"`
+ Owid string `json:"owid"`
+ Username string `json:"username"`
+ AppData []AppDataEntry `json:"appdata,omitempty"`
}

// AuthzEntry is one row of GET /authz/list: the URI, its owner
a/README.md
b/README.md
1
# okg — Oscar Kilo Goodness
1
# okg — Oscar Kilo Goodness
2
2
3
A command-line tool that bundles Oscar Kilo's externally usable
3
A command-line tool that bundles Oscar Kilo's externally usable
4
services into one binary. Today: git ops against
4
services into one binary. Today: git ops against
5
[klee](https://code.oscarkilo.com), and LLM embeddings against
5
[klee](https://code.oscarkilo.com), and LLM embeddings against
6
[klex](https://oscarkilo.com/klex). More LLM utilities (`one`,
6
[klex](https://oscarkilo.com/klex). More LLM utilities (`one`,
7
`exemplary`) coming as the legacy `klex-git` binaries fold in.
7
`exemplary`) coming as the legacy `klex-git` binaries fold in.
8
8
9
Designed for both humans and AI agents (Claude, OpenClaw) to
9
Designed for both humans and AI agents (Claude, OpenClaw) to
10
use directly from the command line.
10
use directly from the command line.
11
11
12
## Position
13
14
okg is world-public. It runs anywhere — on developer laptops,
15
in cloud workers, in agent sandboxes — and authenticates
16
against OscarKilo services with a Klex API key saved to
17
`~/.config/okg/config.json`. The wire formats it sends and
18
decodes are deliberately limited to fields that are safe to
19
expose outside any data center.
20
21
If you're writing an LLM agent, an automation script, or
22
anything else that runs outside Oscar Kilo's infrastructure,
23
okg is the entry point.
24
12
## Install
25
## Install
13
26
14
```bash
27
```bash
15
go install oscarkilo.com/okg@latest
28
go install oscarkilo.com/okg@latest
16
```
29
```
17
30
18
Or build from source:
31
Or build from source:
19
32
20
```bash
33
```bash
21
git clone https://code.oscarkilo.com/okg
34
git clone https://code.oscarkilo.com/okg
22
cd okg && go build .
35
cd okg && go build .
23
```
36
```
24
37
25
## Setup
38
## Setup
26
39
27
By default okg talks to the production klee host
40
By default okg talks to the production klee host
28
(`https://code.oscarkilo.com`); the only setup you need is your
41
(`https://code.oscarkilo.com`); the only setup you need is your
29
API key. `okg auth login` saves it to
42
API key. `okg auth login` saves it to
30
`~/.config/okg/config.json`.
43
`~/.config/okg/config.json`.
31
44
32
```bash
45
```bash
33
# Non-interactive (for agents and scripts).
46
# Non-interactive (for agents and scripts).
34
okg auth login --key sk-...
47
okg auth login --key sk-...
35
cat ~/.klex.key | okg auth login # equivalent, ps-safe
48
cat ~/.klex.key | okg auth login # equivalent, ps-safe
36
49
37
# Interactive (prompts for host then key).
50
# Interactive (prompts for host then key).
38
okg auth login
51
okg auth login
39
```
52
```
40
53
41
Override the host for dev/local work with `--host` on `auth login`.
54
Override the host for dev/local work with `--host` on `auth login`.
42
55
43
## Commands
56
## Commands
44
57
45
```
58
```
46
okg repo list
59
okg repo list
47
60
48
okg pr list [--state open|closed]
61
okg pr list [--state open|closed]
49
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
62
okg pr create --head BRANCH [--base master] --title TITLE [--body BODY]
50
okg pr view NUMBER
63
okg pr view NUMBER
51
okg pr diff NUMBER
64
okg pr diff NUMBER
52
okg pr comment NUMBER --body BODY [--approve | --request-changes]
65
okg pr comment NUMBER --body BODY [--approve | --request-changes]
53
okg pr merge NUMBER
66
okg pr merge NUMBER
54
okg pr close NUMBER
67
okg pr close NUMBER
55
okg pr reopen NUMBER
68
okg pr reopen NUMBER
56
69
57
okg auth login [--key KEY] [--host HOST]
70
okg auth login [--key KEY] [--host HOST]
58
71
59
okg group list [--json]
72
okg group list [--json]
60
okg group create NAME [--full-name TEXT] [--owner USER]
73
okg group create NAME [--full-name TEXT] [--owner USER]
61
okg group add-member GROUP USER [USER ...]
74
okg group add-member GROUP USER [USER ...]
62
okg group remove-member GROUP USER [USER ...]
75
okg group remove-member GROUP USER [USER ...]
63
okg group members GROUP [--json]
76
okg group members GROUP [--json]
64
okg group delete NAME
77
okg group delete NAME
65
78
66
okg authz list [--json]
79
okg authz list [--json]
67
okg authz set URI OWNER READER
80
okg authz set URI OWNER READER
68
okg authz delete URI
81
okg authz delete URI
69
82
70
okg chat send TO TEXT
83
okg chat send TO TEXT
71
okg chat fetch [--to GROUP] [--json]
84
okg chat fetch [--to GROUP] [--json]
72
85
73
okg embed [--model NAME] [--dims N] [--full-path]
86
okg embed [--model NAME] [--dims N] [--full-path]
74
(reads stdin → vectors on stdout)
87
(reads stdin → vectors on stdout)
75
okg one [--model NAME] [--system-file FILE] \
88
okg one [--model NAME] [--system-file FILE] \
76
[--prompt-file FILE] [--attach FILE] \
89
[--prompt-file FILE] [--attach FILE] \
77
[--format text|json|jsonindent] [--fast-fail]
90
[--format text|json|jsonindent] [--fast-fail]
78
(reads stdin as a JSON MessagesRequest)
91
(reads stdin as a JSON MessagesRequest)
79
```
92
```
80
93
81
### Coming next
94
### Coming next
82
95
83
- `okg exemplary` — few-shot batch runner (from
96
- `okg exemplary` — few-shot batch runner (from
84
`klex-git/exemplary`).
97
`klex-git/exemplary`).
85
98
86
Once that lands, the standalone `klex-git` binaries are
99
Once that lands, the standalone `klex-git` binaries are
87
deprecated.
100
deprecated.
88
101
89
### Flags
102
### Flags
90
103
91
- `--repo REPO` overrides auto-detected repo name
104
- `--repo REPO` overrides auto-detected repo name
92
(normally parsed from `git remote get-url origin`)
105
(normally parsed from `git remote get-url origin`)
93
- `--json` outputs raw JSON for any command
106
- `--json` outputs raw JSON for any command
94
- `OKG_REPO` env var also overrides repo detection
107
- `OKG_REPO` env var also overrides repo detection
95
108
96
## Repo Detection
109
## Repo Detection
97
110
98
Like `gh`, okg detects the repo from the current directory's
111
Like `gh`, okg detects the repo from the current directory's
99
git remote:
112
git remote:
100
113
101
```
114
```
102
git remote get-url origin
115
git remote get-url origin
103
→ https://code.oscarkilo.com/widget.git
116
→ https://code.oscarkilo.com/widget.git
104
→ repo = "widget"
117
→ repo = "widget"
105
```
118
```
106
119
107
## Dependencies
120
## Dependencies
108
121
109
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
122
- `oscarkilo.com/klex-git` for the LLM API client (transitional;
110
to be folded in once all `klex-git` binaries have moved here).
123
to be folded in once all `klex-git` binaries have moved here).
111
- Otherwise Go standard library only.
124
- Otherwise Go standard library only.
a/chat/chat.go
b/chat/chat.go
1
// Package chat is the public HTTP client for OscarKilo's //chat
1
// Package chat is the public HTTP client for OscarKilo's chat
2
// service.
2
// service. It is used by `okg` and any other tool that talks
3
// to oscarkilo.com over the network.
3
//
4
//
4
// Wire types mirror //clients/chat's domain types (Message,
5
// The struct here is named HTTPClient (not just Client) so
5
// Filter) and //chat/server's request/response wrappers
6
// applications can wrap it in their own Client abstractions
6
// (SendRequest, SearchRequest, SearchResponse). The
7
// without name collision.
7
// duplication is deliberate — okg is world-public, //clients
8
// is intra-cluster — and will get the same dot-import
9
// treatment as //okg/who when both packages stabilize.
10
//
11
// The HTTP struct is named HTTPClient (not Client) so a future
12
// `import . "oscarkilo.com/okg/chat"` inside //clients/chat
13
// doesn't collide with that package's existing Client type.
14
package chat
8
package chat
15
9
16
import "time"
10
import "time"
17
11
18
import "oscarkilo.com/okg/internal/rest"
12
import "oscarkilo.com/okg/internal/rest"
19
13
20
// HTTPClient talks to a //chat server over HTTPS. Embeds the
14
// HTTPClient talks to a //chat server over HTTPS. Embeds the
21
// shared rest helpers; chat-specific methods live below.
15
// shared rest helpers; chat-specific methods live below.
22
type HTTPClient struct {
16
type HTTPClient struct {
23
*rest.Client
17
*rest.Client
24
}
18
}
25
19
26
func NewHTTPClient(host, apiKey string) *HTTPClient {
20
func NewHTTPClient(host, apiKey string) *HTTPClient {
27
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
21
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
28
}
22
}
29
23
30
// Message mirrors //clients/chat.Message. No JSON tags — the
24
// Message is a single chat message. No JSON tags — the server
31
// server marshals with default Go field names (PascalCase).
25
// marshals with default Go field names (PascalCase). FUTURE:
32
// FUTURE: snake_case once //clients/chat gets JSON tags.
26
// switch to snake_case once the server adds JSON tags.
33
type Message struct {
27
type Message struct {
34
From string
28
From string
35
To string
29
To string
36
Text string
30
Text string
37
CreatedAt time.Time
31
CreatedAt time.Time
38
}
32
}
39
33
40
// SendRequest is the body for POST /chat/send.
34
// SendRequest is the body for POST /chat/send.
41
type SendRequest struct {
35
type SendRequest struct {
42
To string `json:"to"`
36
To string `json:"to"`
43
Text string `json:"text"`
37
Text string `json:"text"`
44
}
38
}
45
39
46
// Send posts a message to a //chat group. The caller must have
40
// Send posts a message to a //chat group. The caller must have
47
// post rights on chat://<to>#post.
41
// post rights on chat://<to>#post.
48
func (c *HTTPClient) Send(req SendRequest) (*Message, error) {
42
func (c *HTTPClient) Send(req SendRequest) (*Message, error) {
49
var msg Message
43
var msg Message
50
if err := c.PostJSON("/chat/send", req, &msg); err != nil {
44
if err := c.PostJSON("/chat/send", req, &msg); err != nil {
51
return nil, err
45
return nil, err
52
}
46
}
53
return &msg, nil
47
return &msg, nil
54
}
48
}
55
49
56
// SearchRequest is the body for POST /chat/search.
50
// SearchRequest is the body for POST /chat/search.
57
type SearchRequest struct {
51
type SearchRequest struct {
58
To string `json:"to,omitempty"`
52
To string `json:"to,omitempty"`
59
}
53
}
60
54
61
// SearchResponse mirrors //chat/server.SearchResponse.
55
// SearchResponse is the body of POST /chat/search's response.
62
type SearchResponse struct {
56
type SearchResponse struct {
63
Messages []*Message `json:"messages"`
57
Messages []*Message `json:"messages"`
64
}
58
}
65
59
66
// Search returns messages matching the filter. The caller must
60
// Search returns messages matching the filter. The caller must
67
// have read rights on chat://<To>#read (otherwise the result
61
// have read rights on chat://<To>#read (otherwise the result
68
// is empty, not an error — per //chat's policy).
62
// is empty, not an error — per //chat's policy).
69
func (c *HTTPClient) Search(
63
func (c *HTTPClient) Search(
70
req SearchRequest,
64
req SearchRequest,
71
) ([]*Message, error) {
65
) ([]*Message, error) {
72
var res SearchResponse
66
var res SearchResponse
73
if err := c.PostJSON(
67
if err := c.PostJSON(
74
"/chat/search", req, &res); err != nil {
68
"/chat/search", req, &res); err != nil {
75
return nil, err
69
return nil, err
76
}
70
}
77
return res.Messages, nil
71
return res.Messages, nil
78
}
72
}
a/who/who.go
b/who/who.go
1
// Package who is the public HTTP client for OscarKilo's //who
1
// Package who is the public HTTP client for OscarKilo's //who
2
// service. It is the world-public side of //who, used by
2
// service. It is used by `okg` and any other tool that talks
3
// `okg` and any other tool that talks to oscarkilo.com over
3
// to oscarkilo.com over the network.
4
// the network.
5
//
4
//
6
// //clients/who (intra-cluster, has the in-process MockClient,
5
// The struct here is named HTTPClient (not just Client) so
7
// the /internal/* calls, the dev/prod factory) will eventually
6
// applications can wrap it in their own Client abstractions
8
// import this package via `import . "oscarkilo.com/okg/who"`
7
// without name collision.
9
// so the two share types and request paths. To make that dot
10
// import work, the struct here is named HTTPClient — leaving
11
// the unqualified `Client` name free for //clients/who's
12
// existing interface.
13
package who
8
package who
14
9
10
import "time"
11
15
import "oscarkilo.com/okg/internal/rest"
12
import "oscarkilo.com/okg/internal/rest"
16
13
17
// HTTPClient talks to a //who server over HTTPS. Embeds the
14
// HTTPClient talks to a //who server over HTTPS. Embeds the
18
// shared rest helpers; who-specific methods live below.
15
// shared rest helpers; who-specific methods live below.
19
type HTTPClient struct {
16
type HTTPClient struct {
20
*rest.Client
17
*rest.Client
21
}
18
}
22
19
23
func NewHTTPClient(host, apiKey string) *HTTPClient {
20
func NewHTTPClient(host, apiKey string) *HTTPClient {
24
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
21
return &HTTPClient{Client: rest.NewClient(host, apiKey)}
25
}
22
}
26
23
24
// ---- Common types ----
25
26
// AppDataEntry is one user-scoped app-data record served by
27
// //who (per-user key/value, namespaced by app).
28
type AppDataEntry struct {
29
App string `json:"app"`
30
Key string `json:"key"`
31
Value string `json:"value"`
32
Timestamp time.Time `json:"timestamp"`
33
}
34
27
// ---- Caller identity ----
35
// ---- Caller identity ----
28
36
29
// Profile is the caller's //who profile. Mirrors the
37
// Profile is the caller's //who profile. Mirrors the
30
// JSON-serialized subset of //who.Profile that GET
38
// JSON-serialized subset of //who.Profile that GET
31
// /login/profile/get returns. Extra fields the server may
39
// /login/profile/get returns. ApiKeys, Logins, and timestamps
32
// return (ApiKeys, Logins, AppData, timestamps) are decoded
40
// are not exposed here yet — add when okg has a use.
33
// permissively if added later; we just don't expose them here
34
// until okg has a use for them.
35
type Profile struct {
41
type Profile struct {
36
Owid string `json:"owid"`
42
Owid string `json:"owid"`
37
Username string `json:"username"`
43
Username string `json:"username"`
38
Name string `json:"name"`
44
Name string `json:"name"`
39
Email string `json:"email"`
45
Email string `json:"email"`
40
PortraitUrl string `json:"portrait_url"`
46
PortraitUrl string `json:"portrait_url"`
41
OwnerOwid string `json:"owner_owid"`
47
OwnerOwid string `json:"owner_owid"`
42
Groups []string `json:"groups"`
48
Groups []string `json:"groups"`
49
AppData []AppDataEntry `json:"app_data,omitempty"`
43
}
50
}
44
51
45
// GetProfile returns the caller's own profile, identified by
52
// GetProfile returns the caller's own profile, identified by
46
// the Bearer key.
53
// the Bearer key.
47
func (c *HTTPClient) GetProfile() (*Profile, error) {
54
func (c *HTTPClient) GetProfile() (*Profile, error) {
48
var p Profile
55
var p Profile
49
if err := c.GetJSON("/login/profile/get", &p); err != nil {
56
if err := c.GetJSON("/login/profile/get", &p); err != nil {
50
return nil, err
57
return nil, err
51
}
58
}
52
return &p, nil
59
return &p, nil
53
}
60
}
54
61
55
// ---- Groups ----
62
// ---- Groups ----
56
63
57
// Group describes a //who group (a named entity with members).
64
// Group describes a //who group (a named entity with members).
58
// Mirrors the listedGroup shape in //who/server/groups_list.go.
65
// Matches the row shape in /groups/list's response.
59
type Group struct {
66
type Group struct {
60
Owid string `json:"owid"`
67
Owid string `json:"owid"`
61
Username string `json:"username"`
68
Username string `json:"username"`
62
Name string `json:"name"`
69
Name string `json:"name"`
63
OwnerOwid string `json:"owner_owid"`
70
OwnerOwid string `json:"owner_owid"`
64
OwnerUsername string `json:"owner_username"`
71
OwnerUsername string `json:"owner_username"`
65
}
72
}
66
73
67
type listGroupsResponse struct {
74
type listGroupsResponse struct {
68
Groups []Group `json:"groups"`
75
Groups []Group `json:"groups"`
69
}
76
}
70
77
71
// ListGroups returns the groups visible to the caller.
78
// ListGroups returns the groups visible to the caller.
72
func (c *HTTPClient) ListGroups() ([]Group, error) {
79
func (c *HTTPClient) ListGroups() ([]Group, error) {
73
var res listGroupsResponse
80
var res listGroupsResponse
74
if err := c.GetJSON("/groups/list", &res); err != nil {
81
if err := c.GetJSON("/groups/list", &res); err != nil {
75
return nil, err
82
return nil, err
76
}
83
}
77
return res.Groups, nil
84
return res.Groups, nil
78
}
85
}
79
86
80
// CreateGroupRequest is the body for POST /groups/add. Username
87
// CreateGroupRequest is the body for POST /groups/add. Username
81
// is the group's //who username (e.g. "team"); Name is the
88
// is the group's //who username (e.g. "team"); Name is the
82
// human-facing display name; OwnerUsername is the user who'll
89
// human-facing display name; OwnerUsername is the user who'll
83
// own the new group.
90
// own the new group.
84
type CreateGroupRequest struct {
91
type CreateGroupRequest struct {
85
Username string `json:"username"`
92
Username string `json:"username"`
86
Name string `json:"name"`
93
Name string `json:"name"`
87
OwnerUsername string `json:"owner_username"`
94
OwnerUsername string `json:"owner_username"`
88
}
95
}
89
96
90
// CreateGroup creates a new //who group.
97
// CreateGroup creates a new //who group.
91
func (c *HTTPClient) CreateGroup(req CreateGroupRequest) error {
98
func (c *HTTPClient) CreateGroup(req CreateGroupRequest) error {
92
return c.PostJSON("/groups/add", req, nil)
99
return c.PostJSON("/groups/add", req, nil)
93
}
100
}
94
101
95
// JoinGroupsRequest is the body for POST /groups/join. The
102
// JoinGroupsRequest is the body for POST /groups/join. The
96
// server adds every (GroupUsernames[i], MemberUsernames[j])
103
// server adds every (GroupUsernames[i], MemberUsernames[j])
97
// pair, requiring the caller to be an owner of each group.
104
// pair, requiring the caller to be an owner of each group.
98
type JoinGroupsRequest struct {
105
type JoinGroupsRequest struct {
99
GroupUsernames []string `json:"group_usernames"`
106
GroupUsernames []string `json:"group_usernames"`
100
MemberUsernames []string `json:"member_usernames"`
107
MemberUsernames []string `json:"member_usernames"`
101
}
108
}
102
109
103
// JoinGroups adds members to groups (all pairwise combinations).
110
// JoinGroups adds members to groups (all pairwise combinations).
104
func (c *HTTPClient) JoinGroups(req JoinGroupsRequest) error {
111
func (c *HTTPClient) JoinGroups(req JoinGroupsRequest) error {
105
return c.PostJSON("/groups/join", req, nil)
112
return c.PostJSON("/groups/join", req, nil)
106
}
113
}
107
114
108
// GroupMembersResponse is the response from POST
115
// GroupMembersResponse is the response from POST
109
// /groups/members. Up lists the groups that contain the queried
116
// /groups/members. Up lists the groups that contain the queried
110
// entity (each level as a slice of owids). Down lists its
117
// entity (each level as a slice of owids). Down lists its
111
// members the same way. Usernames maps every owid mentioned to
118
// members the same way. Usernames maps every owid mentioned to
112
// its //who username.
119
// its //who username.
113
type GroupMembersResponse struct {
120
type GroupMembersResponse struct {
114
Up [][]string `json:"up"`
121
Up [][]string `json:"up"`
115
Down [][]string `json:"down"`
122
Down [][]string `json:"down"`
116
Usernames map[string]string `json:"usernames"`
123
Usernames map[string]string `json:"usernames"`
117
}
124
}
118
125
119
// GroupMembers returns membership DAG navigation for the given
126
// GroupMembers returns membership DAG navigation for the given
120
// entity owid. For a group, Down enumerates its members.
127
// entity owid. For a group, Down enumerates its members.
121
func (c *HTTPClient) GroupMembers(
128
func (c *HTTPClient) GroupMembers(
122
ownerOwid string,
129
ownerOwid string,
123
) (*GroupMembersResponse, error) {
130
) (*GroupMembersResponse, error) {
124
var res GroupMembersResponse
131
var res GroupMembersResponse
125
err := c.PostJSON("/groups/members",
132
err := c.PostJSON("/groups/members",
126
map[string]string{"owid": ownerOwid}, &res)
133
map[string]string{"owid": ownerOwid}, &res)
127
if err != nil {
134
if err != nil {
128
return nil, err
135
return nil, err
129
}
136
}
130
return &res, nil
137
return &res, nil
131
}
138
}
132
139
133
// LeaveGroupRequest is the body for POST /groups/leave. Both
140
// LeaveGroupRequest is the body for POST /groups/leave. Both
134
// IDs are owids (obfuscated wids); to translate from usernames,
141
// IDs are owids (obfuscated wids); to translate from usernames,
135
// call ListGroups + GroupMembers first.
142
// call ListGroups + GroupMembers first.
136
type LeaveGroupRequest struct {
143
type LeaveGroupRequest struct {
137
GroupOwid string `json:"group_owid"`
144
GroupOwid string `json:"group_owid"`
138
MemberOwid string `json:"member_owid"`
145
MemberOwid string `json:"member_owid"`
139
}
146
}
140
147
141
// LeaveGroup removes a member from a group. The caller must own
148
// LeaveGroup removes a member from a group. The caller must own
142
// the group.
149
// the group.
143
func (c *HTTPClient) LeaveGroup(req LeaveGroupRequest) error {
150
func (c *HTTPClient) LeaveGroup(req LeaveGroupRequest) error {
144
return c.PostJSON("/groups/leave", req, nil)
151
return c.PostJSON("/groups/leave", req, nil)
145
}
152
}
146
153
147
// DeleteGroupRequest is the body for POST /groups/delete. Owid
154
// DeleteGroupRequest is the body for POST /groups/delete. Owid
148
// is the group's obfuscated wid.
155
// is the group's obfuscated wid.
149
type DeleteGroupRequest struct {
156
type DeleteGroupRequest struct {
150
Owid string `json:"owid"`
157
Owid string `json:"owid"`
151
}
158
}
152
159
153
// DeleteGroup destroys a group. The caller must own it (or be a
160
// DeleteGroup destroys a group. The caller must own it (or be a
154
// //who admin).
161
// //who admin).
155
func (c *HTTPClient) DeleteGroup(req DeleteGroupRequest) error {
162
func (c *HTTPClient) DeleteGroup(req DeleteGroupRequest) error {
156
return c.PostJSON("/groups/delete", req, nil)
163
return c.PostJSON("/groups/delete", req, nil)
157
}
164
}
158
165
159
// ---- Authz ----
166
// ---- Authz ----
160
167
161
// User is a thin //who-user identifier used inside AuthzEntry.
168
// User is the public //who-user identifier. It carries only
162
// Mirrors the Owid/Username subset of //clients/who.User.
169
// the fields safe to expose to external callers.
163
type User struct {
170
type User struct {
164
Owid string `json:"owid"`
171
Owid string `json:"owid"`
165
Username string `json:"username"`
172
Username string `json:"username"`
173
AppData []AppDataEntry `json:"appdata,omitempty"`
166
}
174
}
167
175
168
// AuthzEntry is one row of GET /authz/list: the URI, its owner
176
// AuthzEntry is one row of GET /authz/list: the URI, its owner
169
// and reader, and the caller's effective rights on it.
177
// and reader, and the caller's effective rights on it.
170
type AuthzEntry struct {
178
type AuthzEntry struct {
171
Uri string `json:"uri"`
179
Uri string `json:"uri"`
172
Owner *User `json:"owner"`
180
Owner *User `json:"owner"`
173
Reader *User `json:"reader"`
181
Reader *User `json:"reader"`
174
IsOwner bool `json:"is_owner"`
182
IsOwner bool `json:"is_owner"`
175
IsReader bool `json:"is_reader"`
183
IsReader bool `json:"is_reader"`
176
}
184
}
177
185
178
type listAuthzResponse struct {
186
type listAuthzResponse struct {
179
Uris []AuthzEntry `json:"uris"`
187
Uris []AuthzEntry `json:"uris"`
180
}
188
}
181
189
182
// ListAuthz returns all authz URIs visible to the caller.
190
// ListAuthz returns all authz URIs visible to the caller.
183
func (c *HTTPClient) ListAuthz() ([]AuthzEntry, error) {
191
func (c *HTTPClient) ListAuthz() ([]AuthzEntry, error) {
184
var res listAuthzResponse
192
var res listAuthzResponse
185
if err := c.GetJSON("/authz/list", &res); err != nil {
193
if err := c.GetJSON("/authz/list", &res); err != nil {
186
return nil, err
194
return nil, err
187
}
195
}
188
return res.Uris, nil
196
return res.Uris, nil
189
}
197
}
190
198
191
// AuthzSetRequest is the body for POST /authz/set. Both
199
// AuthzSetRequest is the body for POST /authz/set. Both
192
// usernames must be non-empty and known to //who; the public
200
// usernames must be non-empty and known to //who; the public
193
// endpoint does not honor the "anyone" sentinel.
201
// endpoint does not honor the "anyone" sentinel.
194
type AuthzSetRequest struct {
202
type AuthzSetRequest struct {
195
Uri string `json:"uri"`
203
Uri string `json:"uri"`
196
OwnerUsername string `json:"owner_username"`
204
OwnerUsername string `json:"owner_username"`
197
ReaderUsername string `json:"reader_username"`
205
ReaderUsername string `json:"reader_username"`
198
}
206
}
199
207
200
// SetAuthz writes an authz entry. The caller must be an owner
208
// SetAuthz writes an authz entry. The caller must be an owner
201
// of the URI (or a //who admin).
209
// of the URI (or a //who admin).
202
func (c *HTTPClient) SetAuthz(req AuthzSetRequest) error {
210
func (c *HTTPClient) SetAuthz(req AuthzSetRequest) error {
203
return c.PostJSON("/authz/set", req, nil)
211
return c.PostJSON("/authz/set", req, nil)
204
}
212
}
205
213
206
// AuthzDeleteRequest is the body for POST /authz/delete.
214
// AuthzDeleteRequest is the body for POST /authz/delete.
207
type AuthzDeleteRequest struct {
215
type AuthzDeleteRequest struct {
208
Uri string `json:"uri"`
216
Uri string `json:"uri"`
209
}
217
}
210
218
211
// DeleteAuthz removes an authz entry. The caller must own the URI.
219
// DeleteAuthz removes an authz entry. The caller must own the URI.
212
func (c *HTTPClient) DeleteAuthz(req AuthzDeleteRequest) error {
220
func (c *HTTPClient) DeleteAuthz(req AuthzDeleteRequest) error {
213
return c.PostJSON("/authz/delete", req, nil)
221
return c.PostJSON("/authz/delete", req, nil)
214
}
222
}