Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions apps/moltnet-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ moltnet vouch list # List your active (unredeemed) vouchers

```bash
moltnet config repair # Validate and fix moltnet.json
moltnet config schema # Print JSON Schema for moltnet.json
moltnet ssh-key # Export identity as SSH key files
moltnet git setup # Configure git for SSH commit signing
moltnet github setup # Configure git for GitHub App identity
Expand All @@ -90,6 +91,9 @@ moltnet help
## Configuration

Credentials are stored at `~/.config/moltnet/moltnet.json` after `moltnet register`.
Generated config files include a `$schema` pointer to
`https://api.themolt.net/schemas/moltnet-config/v1.json` for IDE validation and
autocomplete.

All API commands accept `--api-url` to override the default (`https://api.themolt.net`).

Expand Down
12 changes: 12 additions & 0 deletions apps/moltnet-cli/cobra_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,18 @@ func TestEntryCommitAcceptsWriter(t *testing.T) {

// --- Issue 3+4: --credentials flag is plumbed through ---

func TestConfigSchemaCommand(t *testing.T) {
t.Parallel()
root := NewRootCmd("test", "")
stdout, _, err := executeCommand(root, "config", "schema")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stdout != string(embeddedMoltnetConfigSchema) {
t.Fatal("config schema output mismatch")
}
}

func TestCredentialsFlagPlumbedToAgentsWhoami(t *testing.T) {
// Create a valid credentials file with OAuth2 creds pointing at unreachable API
kp, err := GenerateKeyPair()
Expand Down
15 changes: 14 additions & 1 deletion apps/moltnet-cli/cobra_config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import "github.com/spf13/cobra"
import (
"github.com/spf13/cobra"
)

func newConfigCmd() *cobra.Command {
configCmd := &cobra.Command{
Expand Down Expand Up @@ -65,6 +67,16 @@ Optional env vars:
initFromEnvCmd.Flags().String("env-file", "", "Load variables from a dotenv file")
initFromEnvCmd.Flags().Bool("override", false, "Let env-file values override process environment")

schemaCmd := &cobra.Command{
Use: "schema",
Short: "Print the JSON Schema for moltnet.json",
Example: ` moltnet config schema
moltnet config schema > moltnet-config.schema.json`,
RunE: func(cmd *cobra.Command, args []string) error {
return runConfigSchemaCmd(cmd.OutOrStdout())
},
}

exportEnvCmd := &cobra.Command{
Use: "export-env",
Short: "Export agent config as MOLTNET_* environment variables",
Expand All @@ -90,6 +102,7 @@ usable with init-from-env --env-file.`,
exportEnvCmd.Flags().Bool("include-github-pem", false, "Include GitHub App private key content")

configCmd.AddCommand(repairCmd)
configCmd.AddCommand(schemaCmd)
configCmd.AddCommand(initFromEnvCmd)
configCmd.AddCommand(exportEnvCmd)
return configCmd
Expand Down
8 changes: 7 additions & 1 deletion apps/moltnet-cli/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

// CredentialsFile matches the JS SDK MoltNetConfig format.
type CredentialsFile struct {
Schema string `json:"$schema,omitempty" jsonschema:"format=uri"`
IdentityID string `json:"identity_id"`
OAuth2 CredentialsOAuth2 `json:"oauth2"`
Keys CredentialsKeys `json:"keys"`
Expand Down Expand Up @@ -137,7 +138,12 @@ func WriteConfigTo(config *CredentialsFile, path string) (string, error) {
return "", fmt.Errorf("create config dir: %w", err)
}

data, err := json.MarshalIndent(config, "", " ")
configWithSchema := *config
if configWithSchema.Schema == "" {
configWithSchema.Schema = moltnetConfigSchemaURL
}

data, err := json.MarshalIndent(&configWithSchema, "", " ")
if err != nil {
return "", fmt.Errorf("marshal config: %w", err)
}
Expand Down
6 changes: 6 additions & 0 deletions apps/moltnet-cli/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ func TestWriteConfig(t *testing.T) {
if read.IdentityID != "uuid-write-test" {
t.Errorf("identity_id: got %s", read.IdentityID)
}
if read.Schema != moltnetConfigSchemaURL {
t.Errorf("$schema: got %s, want %s", read.Schema, moltnetConfigSchemaURL)
}
}

func TestOptionalSections(t *testing.T) {
Expand Down Expand Up @@ -243,6 +246,9 @@ func TestOptionalSections_OmitEmpty(t *testing.T) {
var raw map[string]interface{}
json.Unmarshal(data, &raw)

if raw["$schema"] != moltnetConfigSchemaURL {
t.Errorf("$schema: got %v, want %s", raw["$schema"], moltnetConfigSchemaURL)
}
if _, exists := raw["ssh"]; exists {
t.Error("ssh should be omitted when nil")
}
Expand Down
5 changes: 5 additions & 0 deletions apps/moltnet-cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/getlarge/themoltnet/libs/moltnet-api-client v1.28.0
github.com/go-faster/jx v1.2.0
github.com/google/uuid v1.6.0
github.com/invopop/jsonschema v0.13.0
github.com/ipfs/go-cid v0.6.0
github.com/joho/godotenv v1.5.1
github.com/multiformats/go-multihash v0.2.3
Expand All @@ -24,6 +25,8 @@ require (
github.com/XiaoConstantine/mcp-go v0.3.1 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/anthropics/anthropic-sdk-go v1.27.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
Expand All @@ -40,6 +43,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
Expand All @@ -57,6 +61,7 @@ require (
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
Expand Down
11 changes: 11 additions & 0 deletions apps/moltnet-cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ github.com/apache/arrow/go/v13 v13.0.0 h1:kELrvDQuKZo8csdWYqBQfyi431x6Zs/YJTEgUu
github.com/apache/arrow/go/v13 v13.0.0/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc=
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
Expand Down Expand Up @@ -68,10 +72,13 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30=
github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
Expand All @@ -86,6 +93,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand Down Expand Up @@ -147,6 +156,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/vbauerster/mpb/v8 v8.12.0 h1:+gneY3ifzc88tKDzOtfG8k8gfngCx615S2ZmFM4liWg=
github.com/vbauerster/mpb/v8 v8.12.0/go.mod h1:V02YIuMVo301Y1VE9VtZlD8s84OMsk+EKN6mwvf/588=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand Down
8 changes: 8 additions & 0 deletions apps/moltnet-cli/repair.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ func loadAndValidate(credPath string) (string, *CredentialsFile, []ConfigIssue,
}
}

if creds.Schema == "" {
creds.Schema = moltnetConfigSchemaURL
issues = append(issues, ConfigIssue{Field: "$schema", Problem: "missing — set to canonical MoltNet config schema", Action: "fixed"})
} else if creds.Schema != moltnetConfigSchemaURL {
creds.Schema = moltnetConfigSchemaURL
issues = append(issues, ConfigIssue{Field: "$schema", Problem: "incorrect — updated to " + moltnetConfigSchemaURL, Action: "fixed"})
}

// Required fields
if creds.IdentityID == "" {
issues = append(issues, ConfigIssue{Field: "identity_id", Problem: "missing", Action: "warning"})
Expand Down
14 changes: 14 additions & 0 deletions apps/moltnet-cli/repair_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func TestLoadAndValidate_ValidConfig(t *testing.T) {
tmpDir := t.TempDir()

creds := CredentialsFile{
Schema: moltnetConfigSchemaURL,
IdentityID: "test-agent-12345678",
Keys: CredentialsKeys{
PublicKey: "ed25519:O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik=",
Expand Down Expand Up @@ -79,17 +80,27 @@ func TestLoadAndValidate_FixesMissingMCP(t *testing.T) {
}

var fixedMCP bool
var fixedSchema bool
for _, iss := range issues {
if iss.Field == "endpoints.mcp" && iss.Action == "fixed" {
fixedMCP = true
}
if iss.Field == "$schema" && iss.Action == "fixed" {
fixedSchema = true
}
}
if !fixedMCP {
t.Error("expected 'fixed' issue for endpoints.mcp")
}
if !fixedSchema {
t.Error("expected 'fixed' issue for $schema")
}
if updated.Endpoints.MCP != "https://mcp.themolt.net/mcp" {
t.Errorf("MCP endpoint = %q, want %q", updated.Endpoints.MCP, "https://mcp.themolt.net/mcp")
}
if updated.Schema != moltnetConfigSchemaURL {
t.Errorf("$schema = %q, want %q", updated.Schema, moltnetConfigSchemaURL)
}
}

func TestLoadAndValidate_StaleSSHPaths(t *testing.T) {
Expand Down Expand Up @@ -227,6 +238,9 @@ func TestRunConfigRepair_AppliesFixes(t *testing.T) {
if updated.Endpoints.MCP != "https://mcp.themolt.net/mcp" {
t.Errorf("MCP endpoint = %q, want %q", updated.Endpoints.MCP, "https://mcp.themolt.net/mcp")
}
if updated.Schema != moltnetConfigSchemaURL {
t.Errorf("$schema = %q, want %q", updated.Schema, moltnetConfigSchemaURL)
}
}

func TestLoadAndValidate_EnvAuthorshipInvalid(t *testing.T) {
Expand Down
37 changes: 37 additions & 0 deletions apps/moltnet-cli/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
_ "embed"
"encoding/json"
"fmt"
"io"

"github.com/invopop/jsonschema"
)

const moltnetConfigSchemaURL = "https://api.themolt.net/schemas/moltnet-config/v1.json"
const jsonSchemaDraft2020_12 = "https://json-schema.org/draft/2020-12/schema"

//go:embed schema/moltnet-config.v1.json
var embeddedMoltnetConfigSchema []byte

func runConfigSchemaCmd(w io.Writer) error {
_, err := w.Write(embeddedMoltnetConfigSchema)
return err
}

func marshalMoltnetConfigSchema() ([]byte, error) {
reflector := &jsonschema.Reflector{DoNotReference: true}
schema := reflector.Reflect(&CredentialsFile{})
schema.Version = jsonSchemaDraft2020_12
schema.ID = jsonschema.ID(moltnetConfigSchemaURL)
schema.Title = "MoltNet credentials file"
schema.Description = "JSON Schema for moltnet.json agent credentials."

data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return nil, fmt.Errorf("marshal schema: %w", err)
}

return append(data, '\n'), nil
}
Loading
Loading