feat(agentic): add platform auth compatibility surfaces

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-31 14:23:12 +00:00
parent cccc02ed64
commit 27928fc9b4
8 changed files with 432 additions and 0 deletions

183
pkg/agentic/auth.go Normal file
View file

@ -0,0 +1,183 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
core "dappco.re/go/core"
)
// key := agentic.AgentAPIKey{ID: 7, Name: "codex local", Prefix: "ak_abcd", RateLimit: 60}
type AgentAPIKey struct {
ID int `json:"id"`
WorkspaceID int `json:"workspace_id,omitempty"`
Name string `json:"name,omitempty"`
Key string `json:"key,omitempty"`
Prefix string `json:"prefix,omitempty"`
Permissions []string `json:"permissions,omitempty"`
RateLimit int `json:"rate_limit,omitempty"`
CallCount int `json:"call_count,omitempty"`
LastUsedAt string `json:"last_used_at,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
RevokedAt string `json:"revoked_at,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}
// input := agentic.AuthProvisionInput{OAuthUserID: "user-42", Permissions: []string{"plans:read"}}
type AuthProvisionInput struct {
OAuthUserID string `json:"oauth_user_id"`
Name string `json:"name,omitempty"`
Permissions []string `json:"permissions,omitempty"`
RateLimit int `json:"rate_limit,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
}
// out := agentic.AuthProvisionOutput{Success: true, Key: agentic.AgentAPIKey{Prefix: "ak_abcd"}}
type AuthProvisionOutput struct {
Success bool `json:"success"`
Key AgentAPIKey `json:"key"`
}
// input := agentic.AuthRevokeInput{KeyID: "7"}
type AuthRevokeInput struct {
KeyID string `json:"key_id"`
}
// out := agentic.AuthRevokeOutput{Success: true, KeyID: "7", Revoked: true}
type AuthRevokeOutput struct {
Success bool `json:"success"`
KeyID string `json:"key_id"`
Revoked bool `json:"revoked"`
}
// result := c.Action("agentic.auth.provision").Run(ctx, core.NewOptions(
//
// core.Option{Key: "oauth_user_id", Value: "user-42"},
// core.Option{Key: "permissions", Value: "plans:read,plans:write"},
//
// ))
func (s *PrepSubsystem) handleAuthProvision(ctx context.Context, options core.Options) core.Result {
input := AuthProvisionInput{
OAuthUserID: optionStringValue(options, "oauth_user_id", "oauth-user-id", "user_id", "user-id", "_arg"),
Name: optionStringValue(options, "name"),
Permissions: optionStringSliceValue(options, "permissions"),
RateLimit: optionIntValue(options, "rate_limit", "rate-limit"),
ExpiresAt: optionStringValue(options, "expires_at", "expires-at"),
}
if input.OAuthUserID == "" {
return core.Result{Value: core.E("agentic.auth.provision", "oauth_user_id is required", nil), OK: false}
}
body := map[string]any{
"oauth_user_id": input.OAuthUserID,
}
if input.Name != "" {
body["name"] = input.Name
}
if len(input.Permissions) > 0 {
body["permissions"] = input.Permissions
}
if input.RateLimit > 0 {
body["rate_limit"] = input.RateLimit
}
if input.ExpiresAt != "" {
body["expires_at"] = input.ExpiresAt
}
result := s.platformPayload(ctx, "agentic.auth.provision", "POST", "/v1/agent/auth/provision", body)
if !result.OK {
return result
}
return core.Result{Value: AuthProvisionOutput{
Success: true,
Key: parseAgentAPIKey(payloadResourceMap(result.Value.(map[string]any), "key", "api_key", "agent_api_key")),
}, OK: true}
}
// result := c.Action("agentic.auth.revoke").Run(ctx, core.NewOptions(core.Option{Key: "key_id", Value: "7"}))
func (s *PrepSubsystem) handleAuthRevoke(ctx context.Context, options core.Options) core.Result {
keyID := optionStringValue(options, "key_id", "key-id", "_arg")
if keyID == "" {
return core.Result{Value: core.E("agentic.auth.revoke", "key_id is required", nil), OK: false}
}
path := core.Concat("/v1/agent/auth/revoke/", keyID)
result := s.platformPayload(ctx, "agentic.auth.revoke", "DELETE", path, nil)
if !result.OK {
return result
}
output := AuthRevokeOutput{
Success: true,
KeyID: keyID,
Revoked: true,
}
payload, ok := result.Value.(map[string]any)
if !ok {
return core.Result{Value: output, OK: true}
}
if data := payloadResourceMap(payload, "result", "revocation"); len(data) > 0 {
if value := stringValue(data["key_id"]); value != "" {
output.KeyID = value
}
if value, ok := boolValueOK(data["revoked"]); ok {
output.Revoked = value
}
if value, ok := boolValueOK(data["success"]); ok {
output.Success = value
}
return core.Result{Value: output, OK: output.Success && output.Revoked}
}
if data, exists := payload["data"]; exists {
if value, ok := boolValueOK(data); ok {
output.Revoked = value
return core.Result{Value: output, OK: output.Success && output.Revoked}
}
}
return core.Result{Value: output, OK: true}
}
func parseAgentAPIKey(values map[string]any) AgentAPIKey {
return AgentAPIKey{
ID: intValue(values["id"]),
WorkspaceID: intValue(values["workspace_id"]),
Name: stringValue(values["name"]),
Key: stringValue(values["key"]),
Prefix: stringValue(values["prefix"]),
Permissions: listValue(values["permissions"]),
RateLimit: intValue(values["rate_limit"]),
CallCount: intValue(values["call_count"]),
LastUsedAt: stringValue(values["last_used_at"]),
ExpiresAt: stringValue(values["expires_at"]),
RevokedAt: stringValue(values["revoked_at"]),
CreatedAt: stringValue(values["created_at"]),
}
}
func boolValueOK(value any) (bool, bool) {
switch typed := value.(type) {
case bool:
return typed, true
case string:
trimmed := core.Lower(core.Trim(typed))
switch trimmed {
case "true", "1", "yes":
return true, true
case "false", "0", "no":
return false, true
}
case int:
return typed != 0, true
case int64:
return typed != 0, true
case float64:
return typed != 0, true
}
return false, false
}

View file

@ -0,0 +1,17 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import "fmt"
func Example_parseAgentAPIKey() {
key := parseAgentAPIKey(map[string]any{
"id": 7,
"name": "codex local",
"prefix": "ak_live",
"permissions": []any{"plans:read", "plans:write"},
})
fmt.Println(key.ID, key.Name, key.Prefix, len(key.Permissions))
// Output: 7 codex local ak_live 2
}

128
pkg/agentic/auth_test.go Normal file
View file

@ -0,0 +1,128 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"net/http"
"net/http/httptest"
"testing"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuth_HandleAuthProvision_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/v1/agent/auth/provision", r.URL.Path)
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "Bearer secret-token", r.Header.Get("Authorization"))
bodyResult := core.ReadAll(r.Body)
require.True(t, bodyResult.OK)
var payload map[string]any
parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload)
require.True(t, parseResult.OK)
require.Equal(t, "user-42", payload["oauth_user_id"])
require.Equal(t, "codex local", payload["name"])
require.Equal(t, float64(60), payload["rate_limit"])
require.Equal(t, "2026-04-01T00:00:00Z", payload["expires_at"])
permissions, ok := payload["permissions"].([]any)
require.True(t, ok)
require.Equal(t, []any{"plans:read", "plans:write"}, permissions)
_, _ = w.Write([]byte(`{"data":{"id":7,"workspace_id":3,"name":"codex local","key":"ak_live_secret","prefix":"ak_live","permissions":["plans:read","plans:write"],"rate_limit":60,"call_count":2,"expires_at":"2026-04-01T00:00:00Z"}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.handleAuthProvision(context.Background(), core.NewOptions(
core.Option{Key: "oauth_user_id", Value: "user-42"},
core.Option{Key: "name", Value: "codex local"},
core.Option{Key: "permissions", Value: "plans:read,plans:write"},
core.Option{Key: "rate_limit", Value: 60},
core.Option{Key: "expires_at", Value: "2026-04-01T00:00:00Z"},
))
require.True(t, result.OK)
output, ok := result.Value.(AuthProvisionOutput)
require.True(t, ok)
assert.True(t, output.Success)
assert.Equal(t, 7, output.Key.ID)
assert.Equal(t, 3, output.Key.WorkspaceID)
assert.Equal(t, "codex local", output.Key.Name)
assert.Equal(t, "ak_live_secret", output.Key.Key)
assert.Equal(t, "ak_live", output.Key.Prefix)
assert.Equal(t, []string{"plans:read", "plans:write"}, output.Key.Permissions)
assert.Equal(t, 60, output.Key.RateLimit)
assert.Equal(t, 2, output.Key.CallCount)
}
func TestAuth_HandleAuthProvision_Bad(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
result := subsystem.handleAuthProvision(context.Background(), core.NewOptions())
assert.False(t, result.OK)
}
func TestAuth_HandleAuthProvision_Ugly(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{broken json`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.handleAuthProvision(context.Background(), core.NewOptions(
core.Option{Key: "oauth_user_id", Value: "user-42"},
))
assert.False(t, result.OK)
}
func TestAuth_HandleAuthRevoke_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/v1/agent/auth/revoke/7", r.URL.Path)
require.Equal(t, http.MethodDelete, r.Method)
require.Equal(t, "Bearer secret-token", r.Header.Get("Authorization"))
_, _ = w.Write([]byte(`{"data":{"key_id":"7","revoked":true}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.handleAuthRevoke(context.Background(), core.NewOptions(
core.Option{Key: "key_id", Value: "7"},
))
require.True(t, result.OK)
output, ok := result.Value.(AuthRevokeOutput)
require.True(t, ok)
assert.True(t, output.Success)
assert.Equal(t, "7", output.KeyID)
assert.True(t, output.Revoked)
}
func TestAuth_HandleAuthRevoke_Bad(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "secret-token")
result := subsystem.handleAuthRevoke(context.Background(), core.NewOptions())
assert.False(t, result.OK)
}
func TestAuth_HandleAuthRevoke_Ugly(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"data":true}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.handleAuthRevoke(context.Background(), core.NewOptions(
core.Option{Key: "key_id", Value: "7"},
))
require.True(t, result.OK)
output, ok := result.Value.(AuthRevokeOutput)
require.True(t, ok)
assert.True(t, output.Success)
assert.Equal(t, "7", output.KeyID)
assert.True(t, output.Revoked)
}

View file

@ -11,6 +11,8 @@ func (s *PrepSubsystem) registerPlatformCommands() {
c.Command("sync/push", core.Command{Description: "Push completed dispatch state to the platform API", Action: s.cmdSyncPush})
c.Command("sync/pull", core.Command{Description: "Pull shared fleet context from the platform API", Action: s.cmdSyncPull})
c.Command("sync/status", core.Command{Description: "Show platform sync status for the current or named agent", Action: s.cmdSyncStatus})
c.Command("auth/provision", core.Command{Description: "Provision a platform API key for an authenticated agent user", Action: s.cmdAuthProvision})
c.Command("auth/revoke", core.Command{Description: "Revoke a platform API key", Action: s.cmdAuthRevoke})
c.Command("fleet/register", core.Command{Description: "Register a fleet node with the platform API", Action: s.cmdFleetRegister})
c.Command("fleet/heartbeat", core.Command{Description: "Send a heartbeat for a registered fleet node", Action: s.cmdFleetHeartbeat})
@ -31,6 +33,65 @@ func (s *PrepSubsystem) registerPlatformCommands() {
c.Command("subscription/update-budget", core.Command{Description: "Update compute budget for a fleet node", Action: s.cmdSubscriptionUpdateBudget})
}
func (s *PrepSubsystem) cmdAuthProvision(options core.Options) core.Result {
if optionStringValue(options, "oauth_user_id", "oauth-user-id", "user_id", "user-id", "_arg") == "" {
core.Print(nil, "usage: core-agent auth provision <oauth-user-id> [--name=codex] [--permissions=plans:read,plans:write] [--rate-limit=60] [--expires-at=2026-04-01T00:00:00Z]")
return core.Result{Value: core.E("agentic.cmdAuthProvision", "oauth_user_id is required", nil), OK: false}
}
result := s.handleAuthProvision(s.commandContext(), options)
if !result.OK {
err := commandResultError("agentic.cmdAuthProvision", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(AuthProvisionOutput)
if !ok {
err := core.E("agentic.cmdAuthProvision", "invalid auth provision output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "key id: %d", output.Key.ID)
core.Print(nil, "name: %s", output.Key.Name)
core.Print(nil, "prefix: %s", output.Key.Prefix)
if output.Key.Key != "" {
core.Print(nil, "key: %s", output.Key.Key)
}
if len(output.Key.Permissions) > 0 {
core.Print(nil, "permissions: %s", core.Join(",", output.Key.Permissions...))
}
if output.Key.ExpiresAt != "" {
core.Print(nil, "expires: %s", output.Key.ExpiresAt)
}
return core.Result{OK: true}
}
func (s *PrepSubsystem) cmdAuthRevoke(options core.Options) core.Result {
if optionStringValue(options, "key_id", "key-id", "_arg") == "" {
core.Print(nil, "usage: core-agent auth revoke <key-id>")
return core.Result{Value: core.E("agentic.cmdAuthRevoke", "key_id is required", nil), OK: false}
}
result := s.handleAuthRevoke(s.commandContext(), options)
if !result.OK {
err := commandResultError("agentic.cmdAuthRevoke", result)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
output, ok := result.Value.(AuthRevokeOutput)
if !ok {
err := core.E("agentic.cmdAuthRevoke", "invalid auth revoke output", nil)
core.Print(nil, "error: %v", err)
return core.Result{Value: err, OK: false}
}
core.Print(nil, "revoked: %s", output.KeyID)
return core.Result{OK: true}
}
func (s *PrepSubsystem) cmdSyncPush(options core.Options) core.Result {
result := s.handleSyncPush(s.commandContext(), options)
if !result.OK {

View file

@ -15,3 +15,15 @@ func ExamplePrepSubsystem_cmdFleetRegister() {
// usage: core-agent fleet register <agent-id> --platform=linux [--models=codex,gpt-5.4] [--capabilities=go,review]
// false
}
func ExamplePrepSubsystem_cmdAuthProvision() {
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(core.New(), AgentOptions{}),
}
result := s.cmdAuthProvision(core.NewOptions())
core.Println(result.OK)
// Output:
// usage: core-agent auth provision <oauth-user-id> [--name=codex] [--permissions=plans:read,plans:write] [--rate-limit=60] [--expires-at=2026-04-01T00:00:00Z]
// false
}

View file

@ -17,6 +17,27 @@ func TestCommandsplatform_CmdFleetRegister_Bad(t *testing.T) {
assert.False(t, result.OK)
}
func TestCommandsplatform_CmdAuthProvision_Bad(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "")
result := subsystem.cmdAuthProvision(core.NewOptions())
assert.False(t, result.OK)
}
func TestCommandsplatform_CmdAuthRevoke_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"data":{"key_id":"7","revoked":true}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
output := captureStdout(t, func() {
result := subsystem.cmdAuthRevoke(core.NewOptions(core.Option{Key: "_arg", Value: "7"}))
assert.True(t, result.OK)
})
assert.Contains(t, output, "revoked: 7")
}
func TestCommandsplatform_CmdFleetNodes_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"data":[{"id":1,"agent_id":"charon","platform":"linux","models":["codex"],"status":"online"}],"total":1}`))

View file

@ -120,6 +120,10 @@ func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result {
c.Action("agent.sync.pull", s.handleSyncPull).Description = "Pull fleet context from the platform API"
c.Action("agentic.sync.status", s.handleSyncStatus).Description = "Get fleet sync status from the platform API"
c.Action("agent.sync.status", s.handleSyncStatus).Description = "Get fleet sync status from the platform API"
c.Action("agentic.auth.provision", s.handleAuthProvision).Description = "Provision a platform API key for an authenticated agent user"
c.Action("agent.auth.provision", s.handleAuthProvision).Description = "Provision a platform API key for an authenticated agent user"
c.Action("agentic.auth.revoke", s.handleAuthRevoke).Description = "Revoke a platform API key"
c.Action("agent.auth.revoke", s.handleAuthRevoke).Description = "Revoke a platform API key"
c.Action("agentic.fleet.register", s.handleFleetRegister).Description = "Register a fleet node with the platform API"
c.Action("agent.fleet.register", s.handleFleetRegister).Description = "Register a fleet node with the platform API"
c.Action("agentic.fleet.heartbeat", s.handleFleetHeartbeat).Description = "Send a heartbeat for a fleet node"

View file

@ -489,6 +489,10 @@ func TestPrep_OnStartup_Good_RegistersPlatformActionAliases(t *testing.T) {
require.True(t, s.OnStartup(context.Background()).OK)
assert.True(t, c.Action("agentic.sync.push").Exists())
assert.True(t, c.Action("agent.sync.push").Exists())
assert.True(t, c.Action("agentic.auth.provision").Exists())
assert.True(t, c.Action("agent.auth.provision").Exists())
assert.True(t, c.Action("agentic.auth.revoke").Exists())
assert.True(t, c.Action("agent.auth.revoke").Exists())
assert.True(t, c.Action("agentic.fleet.register").Exists())
assert.True(t, c.Action("agent.fleet.register").Exists())
assert.True(t, c.Action("agentic.credits.balance").Exists())
@ -506,6 +510,8 @@ func TestPrep_OnStartup_Good_RegistersPlatformCommandAlias(t *testing.T) {
s.ServiceRuntime = core.NewServiceRuntime(c, AgentOptions{})
require.True(t, s.OnStartup(context.Background()).OK)
assert.Contains(t, c.Commands(), "auth/provision")
assert.Contains(t, c.Commands(), "auth/revoke")
assert.Contains(t, c.Commands(), "subscription/budget/update")
assert.Contains(t, c.Commands(), "subscription/update-budget")
}