feat: wire Infisical secret store into API server and MCP tools
- Wire secretsManager in NewHandler() — instantiate InfisicalBackend when OIKOS_INFISICAL_SITE_URL is set (previously always nil) - Add get_secret, list_secrets, set_secret MCP tools with nil-backend graceful degradation - Add oikos secret get|set|list CLI subcommands for Infisical - Fix Set() bug: create-before-update so new keys are created; add Type: "shared" to Update so it finds the right secret; disable SDK cache so Get returns fresh data after Set - Clean enrollment response: remove fake infisical_client_id/ infisical_client_secret stubs, store age key in Infisical for real
This commit is contained in:
@@ -3185,8 +3185,6 @@ components:
|
||||
required:
|
||||
- age_public_key
|
||||
- age_private_key
|
||||
- infisical_client_id
|
||||
- infisical_client_secret
|
||||
properties:
|
||||
age_public_key:
|
||||
type: string
|
||||
@@ -3200,9 +3198,6 @@ components:
|
||||
infisical_client_secret:
|
||||
type: string
|
||||
description: Infisical UniversalAuth client secret
|
||||
machine_identity_token:
|
||||
type: string
|
||||
description: Infisical machine identity access token
|
||||
ClientContext:
|
||||
type: object
|
||||
required:
|
||||
|
||||
@@ -115,7 +115,7 @@ Roles:
|
||||
scheduler Run the observe loop
|
||||
notifier Run the notification service (Matrix alerts)
|
||||
all Run all roles in one process (dev mode)
|
||||
secret Secret management (Infisical)
|
||||
secret Secret management (Infisical: get, set, list, migrate, export-sops)
|
||||
knowledge Convert wiki to knowledge seed (one-shot)
|
||||
version Print version info
|
||||
|
||||
@@ -289,20 +289,45 @@ func runWithPool(ctx context.Context, cfg config.Config, name string, fn func(co
|
||||
|
||||
func runSecret(ctx context.Context, cfg config.Config) {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Fprintln(os.Stderr, "usage: oikos secret <list|migrate|export-sops>")
|
||||
fmt.Fprintln(os.Stderr, "usage: oikos secret <get|set|list|migrate|export-sops>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sub := os.Args[2]
|
||||
secretsDir := cfg.SecretsDir
|
||||
if secretsDir == "" {
|
||||
secretsDir = "archive/secrets-sops-backup"
|
||||
}
|
||||
sopsBackend := secrets.NewSOPSBackend(secretsDir)
|
||||
|
||||
// For get/set/list: use Infisical directly
|
||||
switch sub {
|
||||
case "get":
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Fprintln(os.Stderr, "usage: oikos secret get <key>")
|
||||
os.Exit(1)
|
||||
}
|
||||
key := os.Args[3]
|
||||
backend := newInfisicalBackendOrFail(cfg)
|
||||
val, err := backend.Get(ctx, key)
|
||||
if err != nil {
|
||||
slog.Error("secret get", "key", key, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(val)
|
||||
|
||||
case "set":
|
||||
if len(os.Args) < 5 {
|
||||
fmt.Fprintln(os.Stderr, "usage: oikos secret set <key> <value>")
|
||||
os.Exit(1)
|
||||
}
|
||||
key := os.Args[3]
|
||||
value := os.Args[4]
|
||||
backend := newInfisicalBackendOrFail(cfg)
|
||||
if err := backend.Set(ctx, key, value); err != nil {
|
||||
slog.Error("secret set", "key", key, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("stored: %s\n", key)
|
||||
|
||||
case "list":
|
||||
keys, err := sopsBackend.List(ctx)
|
||||
backend := newInfisicalBackendOrFail(cfg)
|
||||
keys, err := backend.List(ctx)
|
||||
if err != nil {
|
||||
slog.Error("secret list", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -311,7 +336,21 @@ func runSecret(ctx context.Context, cfg config.Config) {
|
||||
fmt.Println(k)
|
||||
}
|
||||
|
||||
case "migrate":
|
||||
case "migrate", "export-sops":
|
||||
runSecretLegacy(ctx, cfg, sub)
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown secret command: %s\n", sub)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// newInfisicalBackendOrFail creates an Infisical backend from config or exits.
|
||||
func newInfisicalBackendOrFail(cfg config.Config) *secrets.InfisicalBackend {
|
||||
if cfg.InfisicalSiteURL == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: OIKOS_INFISICAL_SITE_URL not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
infCfg := secrets.InfisicalConfig{
|
||||
SiteURL: cfg.InfisicalSiteURL,
|
||||
ClientID: cfg.InfisicalClientID,
|
||||
@@ -323,12 +362,20 @@ func runSecret(ctx context.Context, cfg config.Config) {
|
||||
if infCfg.Env == "" {
|
||||
infCfg.Env = "dev"
|
||||
}
|
||||
if infCfg.SiteURL == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: OIKOS_INFISICAL_SITE_URL not set")
|
||||
os.Exit(1)
|
||||
}
|
||||
return secrets.NewInfisicalBackend(infCfg)
|
||||
}
|
||||
|
||||
infBackend := secrets.NewInfisicalBackend(infCfg)
|
||||
// runSecretLegacy handles SOPS-only commands (migrate, export-sops).
|
||||
func runSecretLegacy(ctx context.Context, cfg config.Config, sub string) {
|
||||
secretsDir := cfg.SecretsDir
|
||||
if secretsDir == "" {
|
||||
secretsDir = "archive/secrets-sops-backup"
|
||||
}
|
||||
sopsBackend := secrets.NewSOPSBackend(secretsDir)
|
||||
|
||||
switch sub {
|
||||
case "migrate":
|
||||
infBackend := newInfisicalBackendOrFail(cfg)
|
||||
keys, err := sopsBackend.List(ctx)
|
||||
if err != nil {
|
||||
slog.Error("migrate: read sops", "error", err)
|
||||
@@ -366,10 +413,6 @@ func runSecret(ctx context.Context, cfg config.Config) {
|
||||
fmt.Printf("%s: <sops-encrypted>\n", k)
|
||||
}
|
||||
fmt.Printf("\n# To restore: sops -d secrets/*.yaml\n")
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown secret command: %s\n", sub)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@ services:
|
||||
OIKOS_OIDC_CLIENT_ID: ${OIKOS_OIDC_CLIENT_ID:-otkHBSueHJsYtOHstL6rn5izeGgyOsavp1qA1hod}
|
||||
OIKOS_NOMOS_AGENT_SLUG: ${OIKOS_NOMOS_AGENT_SLUG:-agent:nomos}
|
||||
NOMOS_PROXY_URL: http://nomos:8092
|
||||
# Infisical secret store (Phase 5)
|
||||
OIKOS_INFISICAL_SITE_URL: ${OIKOS_INFISICAL_SITE_URL:-}
|
||||
OIKOS_INFISICAL_CLIENT_ID: ${OIKOS_INFISICAL_CLIENT_ID:-}
|
||||
OIKOS_INFISICAL_CLIENT_SECRET: ${OIKOS_INFISICAL_CLIENT_SECRET:-}
|
||||
OIKOS_INFISICAL_PROJECT_ID: ${OIKOS_INFISICAL_PROJECT_ID:-}
|
||||
OIKOS_INFISICAL_ENV: ${OIKOS_INFISICAL_ENV:-dev}
|
||||
volumes:
|
||||
- ${OIKOS_SSH_KEY_PATH:-~/.ssh/id_ed25519}:/etc/oikos/ssh_key:ro
|
||||
ports:
|
||||
|
||||
@@ -616,13 +616,10 @@ type EnrollResponse struct {
|
||||
AgePublicKey string `json:"age_public_key"`
|
||||
|
||||
// InfisicalClientId Infisical UniversalAuth client ID
|
||||
InfisicalClientId string `json:"infisical_client_id"`
|
||||
InfisicalClientId *string `json:"infisical_client_id,omitempty"`
|
||||
|
||||
// InfisicalClientSecret Infisical UniversalAuth client secret
|
||||
InfisicalClientSecret string `json:"infisical_client_secret"`
|
||||
|
||||
// MachineIdentityToken Infisical machine identity access token
|
||||
MachineIdentityToken *string `json:"machine_identity_token,omitempty"`
|
||||
InfisicalClientSecret *string `json:"infisical_client_secret,omitempty"`
|
||||
}
|
||||
|
||||
// Entity defines model for Entity.
|
||||
@@ -8225,177 +8222,177 @@ func (sh *strictHandler) GetTrends(w http.ResponseWriter, r *http.Request, entit
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/+x963IbudXgq6C4WzXUpClq7JlkI9f3Q5E1thM71lqafJuKXBTYfUhihAZ6ADQlxuWq",
|
||||
"/NoH2MoT5km2cO1uEk02L7KcqfyxJTUaDZwLcO7nUy/lecEZMCV7p596M8AZCPPjxTWe6v8zkKkghSKc",
|
||||
"9U57H0DyUqSA5iAk4QxNuEBvJoN3WKWzXtKT6QxyrN9TiwJ6pz2pBGHT3ufPn5NegQXOQbkPnJdCcrH6",
|
||||
"ifcF/qUElJrHaCJ4jjAqBMwJLyUSIAvOJHwjEYMHNbLDekmP6Hd/KUEsekmP4Vx/PDxsX1bSu2CKqMWb",
|
||||
"bHUlP/305iXiAklaTlEfjqfH6HbGpTqdlWNB5O2R/2yB1az6Ksl6SU/ALyURkPVOlSihywquaBkBuH3W",
|
||||
"WMK9PM1xOsgJI7cJuqUP6WmKs2zRth797pYr+lHw/Jrotz9FAaux0gDrhIscq95pL8MKBkq/mkTmfZNB",
|
||||
"XnAFLF38CRaruz2nBJgaTIGBwAoydAeLF0hAQfFConuiZoShZ9/PkABVCobUDBAXZEoYpoE0PBQsMVeL",
|
||||
"rn18oL9eX3+OH94Cm6pZ7/S7Z/8ruvSJpfFVDF3jaUWmhAv06uL6Bfr+u2eIs8AnOZG545H44ioe2gZR",
|
||||
"b0lOVBuWqHlYnyCDCS6p6p3+cJLoPZO8zHunz070b4TZ374LuydMwRSE+dA1X0cPim9PDZ/1Ti3GzHlw",
|
||||
"CSwjbHpWFILPMdV/SjlTwMz+cFFQkmIN8+HPUgP+U+2D/1PApHfa+x/D6jgb2qdyGCY0n1yitxlmU0BS",
|
||||
"4SlkLxBGOSg8wO4NdI8lSgUYSuxnJaYDvSLB6VHvc9K7FHxMIV+z0MKO+M12C/bzRtZ7IQQXqP/hx3P0",
|
||||
"++9/+J1ZxhWZMkx/KjSss4NBzc4aW4P7EpJ+hMe8weLZFJg6SxWZE2UYvBC8AKGIRTJ2T0aWGj71gGma",
|
||||
"+1tPcU5HKabUMACWnGkq0d9OiWagXtLL02Lk6Q5kiqnZV+/jCmklPaxXMSJZhGmSXsqFAPuyG8JKSvGY",
|
||||
"gue4lVeyUtjxuVwzPvBL0gNzbHedvrHQ2iyEFaUayTLPsVh0momXattXJEi5BShkmaYg14FhzDkFzPRg",
|
||||
"xe+AjVJeWnLcDDdDBvZQ6bAWK7R0vHuqY/Vv9opWslejlGSJNiuy4uOfIVX6e/WzaZWuLXutkps9QEZY",
|
||||
"dV2spfps/TubadbNMe5GBvBQEAFyq2Vakgljy9LCdXnYHWFZndfhAdJSWaYuOCXpYpCag1j/jpUCwQYG",
|
||||
"Ge0MXuAF5TgisxkRSeOGS8iQnR1lZDJpB1mFX0Hk3Sil2JL3Kuk7CW31gcKqlPUtFvYy01RlaAYyc5Yx",
|
||||
"Yn6wsLZi4pzfQRbdoyztwtbKhFoCCvdVylkKgsnN5BHjBycnOlJuQMPhMOy0QS4NEl/HNh9KCluxDi4V",
|
||||
"ZzxfjCjMgdbhq59U14DhB5iDiMLRncX+xmnC8h1eoDEgPJZK4FShPmEzEERJlPF7dtSF0TpywSbiSnkB",
|
||||
"I7vW9SjXKlcBYmDHIj4HIUgGsstanTgau25iJBGnhSW0VLNuQv65oZOnJ4F9cbOemToBLQqqMiPqgimx",
|
||||
"2A5EqeKi6/VtBy9LX+YW7CU9/UWsrMq8kAq8kpeVtAWyuwhToDChta1UEDiM2JSDmvFuUxhNuZPYY8we",
|
||||
"I1J0G22OyVHKM+go9xxAkqkwGxg3TmWWEK9AKT3jCqndWc0cHnBe6DX3ppSPMT3WFDzCqYodbqVVCraS",
|
||||
"HuaYlnF27H5K3Rk93s60/hw6n0F6t7rZlLMJidhd/oIpsXqOPmrd7RehV43WOhnWhN+O94LemphjOpJx",
|
||||
"al6WnmZKFXqeVP+bEXmnL2AQamCuZA2OTJCJxlJhJRApZwO7tSgHUyzVaAaYqohx43pGJEo16L6RiN8z",
|
||||
"lHOpkIAUmEJzEBlJ1TE6Y8hy7jcS2ZkQkUY0uedCKsQn+hcJCKeCS4n09epAZyeXCZIcKf0xItH9DCsE",
|
||||
"DwXFhEn07f1s8S3C/hN6QAZTgTPIjtGfS0pRyRSh5nNmMjQh+qOiZPJYXxAebmZhBj7udf0jv9cndcnu",
|
||||
"mP7pY4cr1MBLlMzRehNe/z0DZvdhl6IHIyvvhuX+FzIXlV6gXt9u0n2rLKqwmMIGobFPmFSYpTAwN1vW",
|
||||
"ScyxE7eIUW52/RD19b9bzUxy4FprjTPAmtMg6f2dsy664hp51/F2jQ3rK6p4vMPx0ibfVIfMuhMkGOfa",
|
||||
"tOnmSRGG//bkJHmCc6MDBW6iofWUEDb4XXR/HvPrMV1HciveLr1ldxe0bcJT5LJfR++fY4vUMiSZOEve",
|
||||
"bvJz6u+/lSFje6LhjFgdlijI43Kw+wMWAi/iwt9BrB8dL05nKhgZNGXA0nUHASvzsYV+ZV2MIVZAyvMc",
|
||||
"mLG+BLDuazkQvFSwrLwMrCxVU2BmnLaYAoyttbOJ7o7QzoMrbt3hCI2rOna3TTvuEqlstBlYT9A5Zwoe",
|
||||
"VITijdluQijIkbUdRWxBl1jNpBY+zGikLz1RmhUj8yZSWtDwrydbEL4kjtqWpCWSg1Q4L4yOrgUSBg8K",
|
||||
"FZxSpGEHUrVd+BGFo5CWtKftO7wWJSAyQcd69PEC5/o7KSk07GRtZzHLLKddQGfGDb89lqDK4ljOdodZ",
|
||||
"7RpfMsFwxhVnJEWpRXdwmjmmTTZpAWsvZkNIV5AKsErWirIjV5f0hk2IJCmmSJoXkR6GsLF8kzEFpJy0",
|
||||
"mprZt4DDqv4io8t+ieVszLHIripT/hILOMOKHHljY/SyMVIzATkaL0ZaLTVki7OM6K1ietmYc/X1Jeuq",
|
||||
"FfOMQV9qoECGxgtk501a9Hn3cX/pH/jbTuFd/fRcnxDCbXhpKv3MzzQu0ztQdrIfBjlhpQLkr/CkofDo",
|
||||
"i7KO6yZC7ETdr7ngFtlA3W5e/0KMWpZ5LVjYD4R2P10c84lVc559P4sholItm+AKWlh8AVodiz7xelz0",
|
||||
"oVSYRhBu1sfHUuPU7IPTDPQJjVmlN34jtdIJqaaFFFuBIiZ4emVxM+Y6qpwrILPXvcWdVhWd2XFX9PEC",
|
||||
"GOozzgYCJKdzyI6c93YVn/5zK6ta2toKZ8dOmoD8+JaSyBkWp90GO8cgdsEEp/SDu2NXaG3GpfJexSZs",
|
||||
"zlJVYor8AGeqQGDmI2yKcpzOCIsycA5y5iyCzUmvbIyUfo7eXBphQAuo5vyaW8OSFZtalapNQUD38pTB",
|
||||
"/YDiQvHiaKOV0Ey7Dm4uciYmZ40KQeZYweguFrFz9upicHVx/uHievCni78Ojo+PzXYp15dnBqlYFG17",
|
||||
"NXOXY0rS+NR4Ct/p+ewYTaNm6qv3l1c1KSduUnPX98jez04Wbrvjf2JESxCYnpVq5q509OZlp5mtfLD1",
|
||||
"7O61GFFZeht5ghkZH/q6D7g3KhKzcgqyL24ijSUsJCsoj4OzHRRxMlPxcBClBBmXqnGOVa/tojy2WTJr",
|
||||
"VwBkzkqZoHtrr4OacTHnjChu3bPb2A795fPxcC5Do5ibuylqbDSLd/bWeyxRY4s7WxZzrC8ShlkKI2NZ",
|
||||
"3T0EwR+5rcdc5WOoxVX2WhwqHWNC2mxLO7kotnOXOmOTuw/N7qs5GuTcWE47x7Q6TBt80+a5wFOsdV5D",
|
||||
"3/oD30gUXhy5MLDIpzdirR07zZW8tAY8abUlQJRMIF2kVC/EGfdGS6rDKh6XdMVSKuOvR1qcCV57qMzP",
|
||||
"3e7CJpLaEXAZj/k8U4iC4TYWRIYJAZpJlLsVFgIkMHXcS7bA3TsQJhBxvoLDLoj7IpwbR/W1sSJVGLaK",
|
||||
"AeorgZk0Qmu1p7i40oKAa0cGLTAc1WNb6wv649X7P6MrD6qN9rvGy5FtZ1wDN/qIyJGnw7g5mOIFiLrx",
|
||||
"LweF7QUqsDVJlUJjZcrnIAz6jLI3ZaQ1/ikAuquZrxWhBRb68vbsttm4aGA6WuuUWY2HMuFcYO7PQkBq",
|
||||
"QlU/7nXgutPVIcZDuYmOsJKPa+lr4yk7WgnBfgzKCf6OCaYy6gBaoaRDktDOJLP+uI3jaT1CWvwxB8HH",
|
||||
"rrQZPaLmLs562W+0fUwMVvgRI2LqVoQa7fBe0rvHwpvoBVFano97IIxKGzdwyu4CVQg8CoKfNQwcC0wk",
|
||||
"ZFuEu7j7u2ZMcEuMklaION3Kd1aLIt8cWOVMGV3Hpw2fXue3uAab2jM0+JH8dltHyXd29AmcR6SlqztC",
|
||||
"KbJPUX9VZrJP3GFxFJOYBEhz4u7v4XtED50dXLsZNwN2naQu9qWeSLSzi7duhjvbeLG14c/O1meOH83H",
|
||||
"k0XtZzt4gomNvtDLy0a8NJZwfcNR+3fB9Q+jMU7v3G/6x5F77+M+Ls/aQiKS3dYx1CF4eltnaDi+Wg2c",
|
||||
"1SlWnawCDLbXc1SEJbBsuzrrNL7EitacbJyDOTeh+5BZF1mfF9ZofdRLtg5Xqqdgbrwc3Fxrox9fCVzM",
|
||||
"/kLgfhWGkE2hGQCxLkHqg0OgnJEi5oKp7FBtZvsdjUuReNqIm4xkaHBTnpw8h2DrchqptXkRltIyg/+y",
|
||||
"NGnMR85DDdFIR8azLYDj7H0RsChRstRnqsWd2fpTKMWFWdSMqIj/elnG5Db83WIwhvbXBgatvlTvumhs",
|
||||
"cMmFEPB5KLytWPcOHQkooBkBRnJ9Fvu8Or1kF+ilzAe2C4PsJv03jS0Ohl08mLINVf9xG7a4DZch7wBY",
|
||||
"+eWicP8T4/dU881rErlWutqpCbuDbBTloo1xIQKzu06BW+1CDSNFEbtEXpPpjJLpTKPGZF/7CJNOfGUj",
|
||||
"/jtnCCiiKKzZccWHGU/L3IaNiJKNOb8z1qA5SEWmbTlvm+3NdgExJL/1qv5LfWSvclTdFBs1VGTttsAt",
|
||||
"sa1A5ERLETu9HKyJEW3g00Tw/BR9UvwUfXKgkqfobwznkA0Mqybo+Pj44+fPnze6t4lPdjP3yoqxuraO",
|
||||
"GLzfgRIkvQLhIBy5bBZtildu3m2JIqS0LOqUJPB9L+l9N9P/tEQOGmlw3cWG59NO7LdFFm+OHzpNmRPW",
|
||||
"adw2FoaQVbJUuATfIwsL5LNFNnx2WbiUne6tcOmuE4+uzaBoSMXCKgSOCgLOK0TGFnFpI2G3M24UBSUg",
|
||||
"22Oxm2G1S2kOhErOEOX3INCYlyxLtMRW2CASmNv3bOL38IdeBKXNMfFbWatwpVg7ZBtHajAQ7CVuFRWs",
|
||||
"V579UmKBmSKsLTJ8iwzi2aLgagaS/N3mHvjF+0T1JYOluUDCmI/tifvroLmbu7NBSTXd10OqQUormK+p",
|
||||
"xevCOGvFNpZvr6VswlrWpxBcRG6KHwnQbGDyMGvhOMgOR/3vnz07ao/yM26++PHcpjkvwc7OEMZ3OVV8",
|
||||
"vs4G2omlGmwSSoLXoYfHvFSnY6rlsVrwQCnIZs3bfGatu+VSqx5yrQ1jnVP73ZuXCUq5AJkggfNRPk5Q",
|
||||
"RuTdaDpOECkSpCAvqAlGzE1MW4K02E5SkNGgRC4j8uIVLafenXsp+EPOH0xkmAu6qsUoRG0Z8Qiz12WO",
|
||||
"2UAAzvS5gpw/pGPkl/kufUhPfwZKFxPCtneV04c0QfM8QVygjKd3IEwVG0xYPbS6u7PcAW8DjtsCyqok",
|
||||
"6m72gxANGDU7BcMYevPSRBkInN6hwi+DsKn+ZSrA2N82XBPR67i3tIS1274KnLi0aX2yRCqdzUFgSu3B",
|
||||
"g8ikufBg+NzdAtDirD8vhQAWoiZaQzCkgmKd5GjWPcpBSjzt5jyeEEbkbH/782PYsEMAqiiZ84gZxSzg",
|
||||
"Qd5pNbPlclVQdLCG6FFrT8m1yQKOGT2+LHpiszRskxvO2Y2Oj8rzFz8sXUHAwC6dTbzhtLWxI2smaBNR",
|
||||
"zeU9MsXottEPSDZSfFfaWcaJhU5SGZ/dWVlb2yYUdYvy6oyY9TbzdnxsfK+bxS8OkE0wiMf5pFhkhGG6",
|
||||
"5LrmDAaKD7gJy3a/5Jhp4tH/Vc/8b+bhRtt5zPLBtFAK+4XYOEtSpwI0vW0zrze+H4/HqK+p+YWkAfUo",
|
||||
"3oi8O/ee0HhS0qj6ZIU25hBWFRnxP9pEN5G3nK4hiRVTrXK2KFebcNmCnzh8VjcSWUYMOK5w3iopG0N/",
|
||||
"R5ev15E6XqpCqpEEYFt56ycUF+uUwRmn2Sjj92zfWMJtq4RVoSFWgB8403dcq99635TcAV2MUlx25Ou8",
|
||||
"VHvHU/I0NULXeoPHAcJ0NoqClenQBdzg9M77ALxxwXxHb9uqqTZnqBKFPu5yyVcx9jZx8hClynxRsloM",
|
||||
"kJONVuC9zCZL1BPl5DtC6V42tc2BOCaPdlRZDjq+0bmq3zb2sVLuKVSvCRi0mfok29LgXwieQlaKmPjp",
|
||||
"ox4zZAThoY0fGfoAkKGPCyooZujD88Hvj1ZCsb3fbhQil9pq8DB9Dnp7ZL7iC9+8kXooUjzuwq27s4/d",
|
||||
"kOeV1ihi7tMV1W3nqSxcDzFXRBuSIfAnngjZ3V6aCTxxsQs+iCHYSQVMjE22psxtiEP2xlKxPu2ksmfv",
|
||||
"ZjpdSREJxtNgFK1YoPWMunLK53IUap5jFjGavOLBWGaKB7o4uV68KKirQLnMOESF6mYxd3rGSz3AmJlk",
|
||||
"XOjqnnciIF4ST+ltqNYUWIoX3UusaKW/GVst5ayX+Ko5Jh+ctVy6bXffFoCOF7757cnG0gdu3UlAd4xK",
|
||||
"rr1TagmAjOeYLlqkaSKgCirbIYJkVeDkmuOidldiHHOUMMACFYL/bD+doN9lyEb8bfYltvtNJeUxzemt",
|
||||
"/dyEKFSAQBlebO0UDG66ClrRwAwJaaklFJOQ4qoFABYgzspYruJ7pxYN5wTuQZwiPUxLT3fo/ZuX5+iP",
|
||||
"/31dj3clbHB2+Qb96x//ROc4yxY3bMLFPRbZAJe2FFsGE2ASBoQNMijULEGM27wwZ73REpoo1ezo+IaZ",
|
||||
"Et6nxixIUmTXaZNJbZn7KvO0b4p8oVsTKH2r3/Vl4A0xmTcrajesZAqKG5nWVSp3yQ+ukHymuOCrQW3n",
|
||||
"tuz6QN/lgPRmfYGV9+SOSzTjOVA8Ru+vjpEpWTchFHwNum+/DZu8YWaX336L+qaSO07VwMiFR6foFTce",
|
||||
"AxBIqnIsERaAqkYE90TNEMcFGehjbwosuWE271Wivv/8+ds3CZqUWipBP72RRxZeBsw4ByQLSI9v2A07",
|
||||
"52yu0clZTT55fnR6wwbownqh9Nd9mXd021ZU/vZYv/KWSCVRKQHdfjJ3dFLvjfH51i7eNdQo8JQw6/Dq",
|
||||
"u4MGmUYB6IeTBOX4AT07OTky8/7EJJ4Aunx/dW2LnxQK3S51UbhFfduPoaB4ge4Jy/i9fftdaQ4FJFzL",
|
||||
"EIlSLMQC3brb7vYFenVx7To5SHR7cY2ntwm6PLs+f418/Aa69Y0RblHftVTwrRTsZ0LNnQpmz58//z36",
|
||||
"6frcPL9wQUnmKc4yAVKadY2b0aWo3+ztYRB1PQP07vzSlgOZ4BRQXyoBODczvL6+vkwQn0xISjDVBHT1",
|
||||
"8k9HNoe4ZCYSXaHbYZ4WtzeMs4oQxoRhsUCYZXowL02FRMtLlq41y7ogoRemWqIpw4PuBS5uWEVPVj9G",
|
||||
"JqUGYemKLALLCk6YkpYfKUnBuWIck13a7G59XAvqGFOeDodO8z52nuShywKv+RF7lt3OLt/UpJbT3nfH",
|
||||
"J8cnRs0tgOGC9E57z49Pjp9bJ/DMnHdDc0gMcK01gLs0rRGIcPYm6532/ncJYtHsItDsHPO3eAuKWiH3",
|
||||
"NQ0zWt5tVH7fYYJ66Mbal2OSc7W5Yei70mGs68jRYaTrtNNhpG0n8vnjUmuOZycnWzWWWAoi9GpDJ/2h",
|
||||
"ifqIPlJv+7N91TKzhMgdvXLl+CUgYMrEcX1OKsEsvoUAs1oLj1okq+2NgcYww3NiamQYMzueSmPTtmGm",
|
||||
"Y+LNrg8Dv3BbS7N32rPigJl1GEqntHKSvhbOwqhOTBS0jgqXh6to38Y83oqz8sm9+wT86ngjtLJ5OrYI",
|
||||
"BLU/P2gCRbhGoZ4XqhJA2zDC8BPJPg9DwxgN6ybFb0BwaASmcVy4AJEmS700PTUCGpItv7DU/srSkomG",
|
||||
"+QPPFnuQUX3TIa3Vsqnl0kXgzKhqxnhL5G9L8ZnX787Oq7YTVjfoS8KmFAalhAT5vCknUg8kyWBzmaKw",
|
||||
"jTglNhtjfd6TEXftGfXSLRIJSLnIIDvEzWBxZUJ0gC3qsHTQbQVoV5YJXjfLNGVm4/3XyGBmSDfZq156",
|
||||
"fhfpy/YveEzBq03s42yXN1cS+P4j9O13sVWdL57watOL8OIe6iuJXl5cnR8dgr3NzNvLe02etZXy14p7",
|
||||
"53ZIJ6ZdEbs60n6I69iBWX0l9ZVXa5l9vy7Ktm0nno6oHUUcSFizTQ0ymBDm0l8qgnYVHjdIbG2SlY2B",
|
||||
"stB6QrFqIypdrFYneeS7w346il6bOH4A/NqZEHY47tvTZoDlIMMKJ8ibKX931BnnsePLCOn7yua+PEyT",
|
||||
"hEzVmB0pyDV3fVTSsVVtvrAk20o5vmHo/pRjZzKyK7GmVUdEuxJKozLKhgtvaWw3K0eoY/ClJU5fGX/V",
|
||||
"1rFVQ4Bf3SXZ7G/xhLflEjkd4Fh1M4LW7KziqGXLGSAXTchLOfBPkFHLkBKY0KNd7SHOKzW0JYwNaqLJ",
|
||||
"Lr5OpPR1ipPg7pIIM4SngO5gUWAiEtcF2fy9vfBsYjwateTYRtQXb6Q3HKNzTCkIWy8RUwE4W6AZnkO9",
|
||||
"1RMz1w6DTJ8ujewIE+hlHRzNU8FWND73dfkf4zRvFpv+wgf6UsXmWJNoMyIHpkJLdOsBtE0MWBYQdgD6",
|
||||
"th9DGDG498WN//WPfyIiZQmehjz91GgnLKGicke4LSRuWxQ2KPyTpOX08zCtmoREwzA+OA/j/YykM9cL",
|
||||
"xPT/SKxbzZKtKStt+2349hbItPkwRDwlc2BIeV+j8TIz5B3ApsGH7V7GpAKcIT5BU6JQUVIaI9JXoJoN",
|
||||
"TlburdgWEGd04RYnw+KIrNZlW4M/f/7cBLvF7z6bhblls/aPjymiNCARO5VdW5AMqMIHoNlXoBwZpPWZ",
|
||||
"HURxBc5tiTPZSaq9ouW09/ljhLJl1bVkurbI+MAEmBjroHnDOpMzX3jXTvuNXDmx2wnTN0x5dLz7D0Xw",
|
||||
"ftWp98qBVFsPuXVNXr4sMWS+BcywVgwnKgm/ArXSL+YREbfyrZiV3I9BfvH74+k9g4HgJcsGSpDChNRp",
|
||||
"wSfEAqU2SggJznMTEoQKPIU9fKz1gjatKogPMNl0hv9IqAJh6iPUmyW6QlwS6dHAMsyUbDu8d7Wwh4zB",
|
||||
"bV8MVVu3ftOX41374hK7l2P70NXq4cwE5gxdkGzsK7/saXr/t9KS2ouqfSHtyNXko0Qe7NSFinmCslMr",
|
||||
"V7WzIfHCX29fpSWxUdz/C5sSPRW12xJNrbQMbOWMi2s8bZvSDRuaMW7Cg9ggGVpRDjZQRdOC5AcPg8rY",
|
||||
"rgafO8223gSkUjtdf6t50JVTzqTWz03cp61KoTXnFBc4NSqwj/g+SpA3ZLnZrbuxqk5rIloiOnNTUdbH",
|
||||
"oN9dcLnHdIpQheDrpv2VOiBfmP5Xa1S0n3SuLGvSRMgvJZRPyiZhCwgj/VqpAun23/6f8wT95V2CQo2P",
|
||||
"I2QGmqId+/KTN963SaGB9B7R/NF2fDmcuXpAT4edV6GWwXKI8U6X3AHcJMtpD75HR/3UwQIiXUdqnWOq",
|
||||
"dgEZTF7cMEIpTDFtTGJjudH3J7/Xcq2ZblA9PzpGlzaKb6o/csPsgai10kX16nPU96dcgMtR9LzT29v1",
|
||||
"rHtkf0+9ecwXtw+2MYjz+FR361NxiHMYVeUtNIPUOsUsdZE5yKk1NF2uB1WX67Yj7A963Ac7rJM3ySTU",
|
||||
"NPSQAJ7nphQiycu8d/pDJJXrsbWJ5SDBwiYbtTSJ7VyVqa1Qkv3A1mVttojQmUxsnV2PWmvXngpczFBG",
|
||||
"XI20Qxi1fc6I/yCZWFOQO9gnmFD5RY/zVYL2AWhy84UcCq10o2gBdOdgvioRLsoRvTE3zBIquZjkPmNi",
|
||||
"ME8+Ht70vI/Kvb7I+850XJ/2EE7GlwboSNSnNX3Pgyetr6GLAnLk0RMSr7VtB5F6WOVyt1HxcoG1R7w+",
|
||||
"lz8Vwd5l0w8JhTuG3D4OIN9zSuNF7Iypc1no/1KorJmmTTdeuT5++GLu2rN2OHEePe8qahWtlQ35TwDw",
|
||||
"E9s2585n8lSmzbnN5D1g0O9rIhUXxtkNnhV2dkTMLbRM7mmrO/DKpgZcAVPIbugYXeB0Zr//jUS3JLv1",
|
||||
"WdG2B77g94hkqC9AljncMHOQ3b7VorKZYfDm5e1Rgm7N6KV3NVATdJthhcOTP169//MNM68iC+1j9Bqw",
|
||||
"UGPASp9buYGz5rwF+u4HeYz+AFINYDLhwrhhiXnyr3/884aZutKQoQLEQJZjvdMxCDQuJxMQCcoELwac",
|
||||
"ZiCVS6K+/O3RC5MG/eriGjmY3TDF0RindxMSd8VfGZi2HVatLpwAAVQImJCHfT02VsmqXmygYO0Mm9lW",
|
||||
"wYOy4BhUFNQ+4aof9uoCuRcPYfafewKyc6L+1dXF0T7MUUVHrfXTVcN2zYV89Aj5ryQj5d/r6ghZok94",
|
||||
"fVS0dSjHWJ1at44DTFqcHdczQDPMMgpi2TvRDzF8hgaPEhvDK52fYuiLHyY3DLMMAVEzEAiYsYa7ayFU",
|
||||
"Y+7bcFaXKHyEuKhFEN6wkDnobG/GB+ILQTRnIgzd+vZytyHq74xKjuDB/NUHudiIHsEpmAA0G45lp3v/",
|
||||
"57d/Rfd4YcdIvcXYVeA8Ehf1tOOv0n243A7uS7sQK45bwwree4L6uS1R6jLIgxPrEELWh0BAdepzpL1A",
|
||||
"//q//69KU7WJDfpPjmq3CrGtxR9WYzc7RGq09Hgm3074OIRdLIDYtY37DXIdNHc7o/ayJzSRMLQtIR8l",
|
||||
"6/vcTP30qDwPXS8P4Go3cyGM/OE6DL04Ub3uwo75xfpsFu0Jxhfm8RVAtrcxZ0l0MIWVuI2Va0Lvr2fv",
|
||||
"3qJa663VGq1Mccqnu7xq78itX1ySN8ICkto+wuRd5BANUTQu9QV/kMPVJwQgqSfWu5G2plXqWgi8/IPv",
|
||||
"9P/yAxoiVxLIR+I1UsUWUkHeiXiMPX/dqWqaeG5S1q4UFsET26/7YY9eIJ4TZYxp9zMtMFgPQt+2MGqL",
|
||||
"vhOc7yTUr3EQPdvgIEpMjVJqCi1aibWzvb57bVKpFqa204SLvLcalrfUKfRnTpj3g4zc37T4VvCipEbC",
|
||||
"Cy1Wj12/x6TLJtxn4nsINRmXOyd03dVjxq9XLWUjHGkeorl5ujdDXpVjS6macudElpiSv7tabqYHKvoN",
|
||||
"Mj1QdzDva8arepy2cd6PFEC99mh9NJA227VGwGoHHDC22GzMtcr101qzvunohgjL9Fa42MeMFwptDyVg",
|
||||
"kbZD+so8Dr05uxksfuktKwH7WQG+At2+0Z30cP6310QdQlH/saR0YPJHLDptkdeA5MpL3fcigEyQ6/jZ",
|
||||
"YNHwylY09Cl4PzrEZNVp6deGzrem/WwF+ENU7KA0yG1y6HGGbKNbpHg0RrUrGuPcbBrNRj1b3ZnaaEa2",
|
||||
"KO0G79w7N6jTydLZzdbhng8lcw8iqsTlNN8yMxJ/gEvFa/EHzQaqtsXHbgnXuzjxnvJobfSoPRwv2mmR",
|
||||
"hAPVTDTEajLyBnZOlAfK3fUarit6bafm+0oXOyCKHCNpKG6b92EaJEVYIgTLdZ+w0Q461pG7Fkey5VpX",
|
||||
"WjptoqwGRKKfbmyxCwkG1B3mhkeeXIxZuZa7pS/z2nL932qrrZGoX1N36rQRM4tBQEDcpF8PqUVYuhTm",
|
||||
"UV4qpxgEw7vmHDwI5tD7GTBURdiuWMPriTTXVrv8ipNp9AqfMqHGEvvaAj3PTp51oENrJK9X+tzbaKu0",
|
||||
"AqNmUFGyUWxszn6NoLvTa9NeE6XY4Sd9HcdK/USkHZfit4Wg0xre/hqLDGVAQZkC8IwrJMui4MJUcZ+Z",
|
||||
"uvCum65E8ECkrVcQ+oGEFH4bU/DyeYQ1aqHnu3HGFwk/10t7whD0No6oFR56Io6oFSwKWK9CJffhBFeV",
|
||||
"eH0gwqUftI3svUdxzR0jCrrFP/yaIgl8G/2niyMIpHGgKIKiIjVPzxRcJ7nNkoh/+6A12+KdTpDEE1AL",
|
||||
"NMd0Du7ovXr1u6NjdBYKfOvjvKhLOyuiztX3bYf1ZehF/+VP6iZJtpZa/qXEAjNlGlVFO/Ks9ryqGv7X",
|
||||
"ml3VOlsZP1IYE1Nrn7bMcmC4r/GWuK4SkrBnJNR39eYBDVEFWzSsbpKj7ry2dHe4OJuQ/FZS6Fbc/0NJ",
|
||||
"Qfa+grr0eiGHTJMw+zpwnXkkyqZqVrlXdwioOg/KVaNBQfjaCyN3608i1xzSFnHCEwViNa/bn3zPW/Wx",
|
||||
"Bqi/Uo2svsZtdLInYfNLG//gtKEGlaB+VmI6iDiz19JMB7Z+7Cqo+1HJI2sn/6bkYSjCsfchCcPFVa4z",
|
||||
"Rp65MVegFGHTpz3rm2s54HEfdneIgut2kUi6OVH/jlA6kPdEpbMEMZiDGPiaq6aizdEOV0Jcpv2AiTRx",
|
||||
"jn4RRKI6vVDIUP/ZyTP0myoU8hi95fdgih8RZdMf3NLR7ZTyMabHeroRTtUpuunxyeSmd6s1WJzZmEq7",
|
||||
"pZEfhO7AZVH4a4fkOWQEK6AL/fWTo1NzNdXAYktxmnnQPXbxMZitLzpiTpsYee52bujt6EeYXjZotM0z",
|
||||
"9Hhy69fJI2cGmzZhRwliQ7WfUEgOx6OndV+dsnFCvmiQ2fsff9QsEQhyv/NTEHk3MAG/G6TlD0Tenbtx",
|
||||
"T5lS7JdxSEGZyDvkYXAgeVnU59zyaNToaSQj20OSglV9lwv2Zo00627JOaal5LYBL2uTdfaeqRaGt5Uh",
|
||||
"e4kUv49VsK25mYAdpufQBcu0VFOfui9BSVsGZqS41V1MJAuR6A4KeyPMTF7j4miXshxtatSFa1lpfWiR",
|
||||
"QjSrfkHUnxEQWKSzxQDfYwFHL1CKRUYYprZt34SLFLI2RWo9zX0dilR9jU/j3GoWQPgi/ScaFOkilnaq",
|
||||
"/+J7Dqy7FK7cmM4ZgXCwTPPQU5tNeC/p3TtTUdJLBVEkjfYaf5Q8+C6NgH5NZn6L8ye08nuiO1Tl4kDD",
|
||||
"27XiqfGITaHB6d2j5M+cpXcO5nGsr9+5ffVw7UrO0ipCEzvg7dippAG9vLTSzcHB965UUIPfIZwQeq2j",
|
||||
"kilCu1aAb+0RudwRv5p5jyaOX5YiNIADKbgSK9fXbw9BFAIkp/PHoYsPdu4Dk0Y7mleQ+VUgz0Ghwl+O",
|
||||
"WYkpXeyKPq2qbhAa7JBurTGt+WVkok7/471/zGtdY+Upb3VLFYe61M1sqG9SqlRIrCtA2EdHu7n07bSP",
|
||||
"7X6wqPiKfe0FYQyykYNqvCbiqrtdY6LV1/71edcdQ3z1vnVDk6bBE9G/eqTs6EavUfjQTdXhNP+LH/n1",
|
||||
"HWA7H0hhT/vjy03lbT+mtqDF2/bH0J55/yYdT3ZNfro2o7c+iv7dkjnMNg9IOg5sh2B0YBnCDNOFJK58",
|
||||
"IaU+h8NUJo8kUm2T0PGYyVR6K5CWxnSjpx4DFiDOSjXrnf7to8a47cZuP1wK2jvtDXFBhvPvDD24/ay2",
|
||||
"bXLJ/S7vPOQVmJKzphRO3Xbe3IZNq1mJQ7Gd1yC0fkuq3lZE2jrLhLPEtzmqFY5yvYxW57zYLtXBzcer",
|
||||
"7ItPcbuH2aIrLtS3qDY+oEZ3zuiCQg2KqreC69SYBC+lRP0MUpLBEKeqNi3USzR9aom7NEsLopc+z2oz",
|
||||
"hPNt9f26AyZZCjVKgnOsmsp5UVYnChmSjjRcnnBlq6vlOH6Kpl7JxGYsm+9mRCWu+mCCQja+x1SDy2Lg",
|
||||
"LrhQq++5Sg6fP37+/wEAAP//VvsxiT/wAAA=",
|
||||
"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",
|
||||
"T3QHrO8AAA==",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
||||
@@ -1255,13 +1255,15 @@ func (s *Server) EnrollClient(ctx context.Context, req gen.EnrollClientRequestOb
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infisicalClientID := "inf_client_" + uuid.NewString()
|
||||
infisicalClientSecret := "inf_secret_" + uuid.NewString()
|
||||
// Store age key in Infisical when backend is available.
|
||||
if s.secretsManager != nil {
|
||||
keyPath := "clients/" + req.Body.Slug + "/age-key"
|
||||
_ = s.secretsManager.Set(ctx, keyPath, agePrivKey)
|
||||
}
|
||||
|
||||
resp := gen.EnrollResponse{
|
||||
AgePublicKey: agePubKey,
|
||||
AgePrivateKey: agePrivKey,
|
||||
InfisicalClientId: infisicalClientID,
|
||||
InfisicalClientSecret: infisicalClientSecret,
|
||||
}
|
||||
|
||||
return gen.EnrollClient200JSONResponse(resp), nil
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/dtoro/oikos/internal/httpapi/gen"
|
||||
mcphandler "github.com/dtoro/oikos/internal/mcp"
|
||||
"github.com/dtoro/oikos/internal/safego"
|
||||
"github.com/dtoro/oikos/internal/secrets"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
@@ -59,8 +60,11 @@ type Server struct {
|
||||
}
|
||||
|
||||
// secretsBackend is a minimal interface for secrets operations used by the
|
||||
// HTTP API (enrollment key storage, listing). Compatible with internal/secrets.
|
||||
// HTTP API (enrollment key storage, listing, retrieval). Compatible with
|
||||
// internal/secrets.Backend. If Infisical is configured, a real backend is
|
||||
// wired in; otherwise the field stays nil and all guarded paths are no-ops.
|
||||
type secretsBackend interface {
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
Set(ctx context.Context, key string, value string) error
|
||||
List(ctx context.Context) ([]string, error)
|
||||
}
|
||||
@@ -80,6 +84,23 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand
|
||||
sseSubs: make(map[*sseSubscriber]struct{}),
|
||||
}
|
||||
|
||||
// Wire Infisical backend when configured.
|
||||
if cfg.InfisicalSiteURL != "" {
|
||||
infCfg := secrets.InfisicalConfig{
|
||||
SiteURL: cfg.InfisicalSiteURL,
|
||||
ClientID: cfg.InfisicalClientID,
|
||||
ClientSecret: cfg.InfisicalClientSecret,
|
||||
ProjectID: cfg.InfisicalProjectID,
|
||||
SecretPath: "/",
|
||||
Env: cfg.InfisicalEnv,
|
||||
}
|
||||
if infCfg.Env == "" {
|
||||
infCfg.Env = "dev"
|
||||
}
|
||||
s.secretsManager = secrets.NewInfisicalBackend(infCfg)
|
||||
slog.Info("secrets backend wired", "backend", "infisical", "site", cfg.InfisicalSiteURL)
|
||||
}
|
||||
|
||||
// Start background SSE listener, tied to ctx for clean shutdown.
|
||||
// handleNotification (called per-message inside sseListener's loop) has
|
||||
// its own recover for the common case; this outer one covers the
|
||||
@@ -271,7 +292,7 @@ func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Hand
|
||||
if nomosAgentID == uuid.Nil && cfg.NomosAgentSlug != "" {
|
||||
_ = pool.QueryRow(ctx, "SELECT id FROM entities WHERE slug = $1", cfg.NomosAgentSlug).Scan(&nomosAgentID)
|
||||
}
|
||||
r.With(combinedAuth(cfg, false)).Handle("/mcp", mcphandler.NewHandler(pool, cfg.MCPBearerToken, nomosAgentID))
|
||||
r.With(combinedAuth(cfg, false)).Handle("/mcp", mcphandler.NewHandler(pool, cfg.MCPBearerToken, nomosAgentID, s.secretsManager))
|
||||
|
||||
if nomosURL := os.Getenv("NOMOS_PROXY_URL"); nomosURL != "" {
|
||||
target, _ := url.Parse(nomosURL)
|
||||
|
||||
@@ -97,7 +97,7 @@ func newTestPool(t *testing.T) *db.Pool {
|
||||
func callTool(t *testing.T, pool *db.Pool, name string, args map[string]any) string {
|
||||
t.Helper()
|
||||
var handler toolHandler
|
||||
for _, r := range allTools(pool, uuid.Nil) {
|
||||
for _, r := range allTools(pool, uuid.Nil, nil) {
|
||||
if r.tool.Name == name {
|
||||
handler = r.handler
|
||||
break
|
||||
|
||||
200
internal/mcp/secrets_tools_test.go
Normal file
200
internal/mcp/secrets_tools_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// mockSecretBackend implements secretBackend for testing.
|
||||
type mockSecretBackend struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (m *mockSecretBackend) Get(ctx context.Context, key string) (string, error) {
|
||||
v, ok := m.data[key]
|
||||
if !ok {
|
||||
return "", &secretErr{msg: "secret not found: " + key}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (m *mockSecretBackend) Set(ctx context.Context, key string, value string) error {
|
||||
m.data[key] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockSecretBackend) List(ctx context.Context) ([]string, error) {
|
||||
keys := make([]string, 0, len(m.data))
|
||||
for k := range m.data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
type secretErr struct{ msg string }
|
||||
|
||||
func (e *secretErr) Error() string { return e.msg }
|
||||
|
||||
// findToolHandler locates a tool's handler from allTools by name.
|
||||
func findToolHandler(t *testing.T, pool interface{}, name string, sec secretBackend) func(context.Context, *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
t.Helper()
|
||||
for _, r := range allTools(nil, uuid.Nil, sec) {
|
||||
if r.tool.Name == name {
|
||||
return r.handler
|
||||
}
|
||||
}
|
||||
t.Fatalf("tool %q not found", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func callToolJSON(t *testing.T, name string, sec secretBackend, args map[string]any) any {
|
||||
t.Helper()
|
||||
handler := findToolHandler(t, nil, name, sec)
|
||||
argBytes, _ := json.Marshal(args)
|
||||
req := &mcp.CallToolRequest{
|
||||
Params: &mcp.CallToolParamsRaw{Arguments: argBytes},
|
||||
}
|
||||
result, err := handler(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("tool %q error: %v", name, err)
|
||||
}
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatalf("tool %q returned no content", name)
|
||||
}
|
||||
tc := result.Content[0].(*mcp.TextContent)
|
||||
var out any
|
||||
if err := json.Unmarshal([]byte(tc.Text), &out); err != nil {
|
||||
// Not JSON — return raw string
|
||||
return tc.Text
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func callToolText(t *testing.T, name string, sec secretBackend, args map[string]any) string {
|
||||
t.Helper()
|
||||
handler := findToolHandler(t, nil, name, sec)
|
||||
argBytes, _ := json.Marshal(args)
|
||||
req := &mcp.CallToolRequest{
|
||||
Params: &mcp.CallToolParamsRaw{Arguments: argBytes},
|
||||
}
|
||||
result, err := handler(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("tool %q error: %v", name, err)
|
||||
}
|
||||
if len(result.Content) == 0 {
|
||||
t.Fatalf("tool %q returned no content", name)
|
||||
}
|
||||
return result.Content[0].(*mcp.TextContent).Text
|
||||
}
|
||||
|
||||
func TestGetSecret(t *testing.T) {
|
||||
sec := &mockSecretBackend{
|
||||
data: map[string]string{
|
||||
"matrix-token": "bot-token-123",
|
||||
"clients/host:hubris/age-key": "AGE-SECRET-KEY",
|
||||
},
|
||||
}
|
||||
|
||||
// Get existing key
|
||||
val := callToolText(t, "get_secret", sec, map[string]any{"key": "matrix-token"})
|
||||
if val != "bot-token-123" {
|
||||
t.Errorf("get_secret = %q, want bot-token-123", val)
|
||||
}
|
||||
|
||||
// Get missing key
|
||||
errText := callToolText(t, "get_secret", sec, map[string]any{"key": "nonexistent"})
|
||||
if errText == "" {
|
||||
t.Error("expected error for missing key")
|
||||
}
|
||||
|
||||
// Missing key arg
|
||||
errText = callToolText(t, "get_secret", sec, map[string]any{})
|
||||
if errText != "error: key is required" {
|
||||
t.Errorf("missing key error = %q, want error: key is required", errText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListSecrets(t *testing.T) {
|
||||
sec := &mockSecretBackend{
|
||||
data: map[string]string{
|
||||
"clients/host:hubris/age-key": "val1",
|
||||
"clients/host:strong/age-key": "val2",
|
||||
"shared/matrix-token": "val3",
|
||||
},
|
||||
}
|
||||
|
||||
// List all
|
||||
out := callToolJSON(t, "list_secrets", sec, map[string]any{})
|
||||
keys, ok := out.([]any)
|
||||
if !ok {
|
||||
t.Fatalf("list_secrets returned non-array: %T", out)
|
||||
}
|
||||
if len(keys) != 3 {
|
||||
t.Errorf("list_secrets count = %d, want 3", len(keys))
|
||||
}
|
||||
|
||||
// List with prefix filter
|
||||
out = callToolJSON(t, "list_secrets", sec, map[string]any{"path_prefix": "clients/"})
|
||||
keys, ok = out.([]any)
|
||||
if !ok {
|
||||
t.Fatalf("filtered list returned non-array: %T", out)
|
||||
}
|
||||
if len(keys) != 2 {
|
||||
t.Errorf("filtered list count = %d, want 2", len(keys))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSecret(t *testing.T) {
|
||||
sec := &mockSecretBackend{
|
||||
data: map[string]string{},
|
||||
}
|
||||
|
||||
// Set a key
|
||||
result := callToolText(t, "set_secret", sec, map[string]any{"key": "test-key", "value": "test-value"})
|
||||
if result != "secret test-key stored" {
|
||||
t.Errorf("set_secret = %q, want 'secret test-key stored'", result)
|
||||
}
|
||||
|
||||
// Verify it was stored
|
||||
val, err := sec.Get(context.Background(), "test-key")
|
||||
if err != nil {
|
||||
t.Fatalf("verify get: %v", err)
|
||||
}
|
||||
if val != "test-value" {
|
||||
t.Errorf("stored value = %q, want test-value", val)
|
||||
}
|
||||
|
||||
// Missing key
|
||||
errText := callToolText(t, "set_secret", sec, map[string]any{})
|
||||
if errText != "error: key is required" {
|
||||
t.Errorf("missing key error = %q", errText)
|
||||
}
|
||||
|
||||
// Missing value
|
||||
errText = callToolText(t, "set_secret", sec, map[string]any{"key": "x"})
|
||||
if errText != "error: value is required" {
|
||||
t.Errorf("missing value error = %q", errText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretToolsNilBackend(t *testing.T) {
|
||||
// All tools should return a graceful error when no backend is configured
|
||||
errText := callToolText(t, "get_secret", nil, map[string]any{"key": "x"})
|
||||
if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" {
|
||||
t.Errorf("nil backend get_secret = %q", errText)
|
||||
}
|
||||
|
||||
errText = callToolText(t, "list_secrets", nil, map[string]any{})
|
||||
if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" {
|
||||
t.Errorf("nil backend list_secrets = %q", errText)
|
||||
}
|
||||
|
||||
errText = callToolText(t, "set_secret", nil, map[string]any{"key": "x", "value": "y"})
|
||||
if errText != "error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)" {
|
||||
t.Errorf("nil backend set_secret = %q", errText)
|
||||
}
|
||||
}
|
||||
@@ -47,10 +47,19 @@ func objSchema(props ...prop) *jsonschema.Schema {
|
||||
return s
|
||||
}
|
||||
|
||||
// secretBackend is the interface MCP tools use to access the secrets store.
|
||||
// Defined here to avoid importing the full secrets package (which brings in
|
||||
// the Infisical SDK). Mirrors the subset of secrets.Backend used by tools.
|
||||
type secretBackend interface {
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
Set(ctx context.Context, key string, value string) error
|
||||
List(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
// NewHandler creates an http.Handler that serves the Oikos MCP server.
|
||||
// agentID is the Nomos agent entity UUID; tool calls are logged to agent_activity.
|
||||
func NewHandler(pool *db.Pool, token string, agentID uuid.UUID) http.Handler {
|
||||
s := newServer(pool, agentID)
|
||||
func NewHandler(pool *db.Pool, token string, agentID uuid.UUID, sec secretBackend) http.Handler {
|
||||
s := newServer(pool, agentID, sec)
|
||||
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
|
||||
if token != "" {
|
||||
if r.Header.Get("Authorization") != "Bearer "+token {
|
||||
@@ -65,11 +74,11 @@ func NewHandler(pool *db.Pool, token string, agentID uuid.UUID) http.Handler {
|
||||
// toolHandler is the function signature registered via AddTool.
|
||||
type toolHandler = mcp.ToolHandler
|
||||
|
||||
func newServer(pool *db.Pool, agentID uuid.UUID) *mcp.Server {
|
||||
func newServer(pool *db.Pool, agentID uuid.UUID, sec secretBackend) *mcp.Server {
|
||||
s := mcp.NewServer(&mcp.Implementation{Name: "oikos", Version: "dev"}, &mcp.ServerOptions{
|
||||
Logger: slog.Default(),
|
||||
})
|
||||
for _, t := range allTools(pool, agentID) {
|
||||
for _, t := range allTools(pool, agentID, sec) {
|
||||
s.AddTool(t.tool, withActivityLogging(pool, agentID, t.tool.Name, t.handler))
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestNewServerRegistersTools(t *testing.T) {
|
||||
}()
|
||||
// pool is only used inside tool handlers (invoked per-call), not at
|
||||
// registration time, so a nil pool is safe for this construction test.
|
||||
s := newServer(nil, uuid.Nil)
|
||||
s := newServer(nil, uuid.Nil, nil)
|
||||
if s == nil {
|
||||
t.Fatal("newServer returned nil")
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type toolReg struct {
|
||||
// allTools returns every MCP tool registration. Tool definitions, schemas,
|
||||
// descriptions, and handler bodies are kept verbatim from the former inline
|
||||
// newServer registrations.
|
||||
func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
||||
func allTools(pool *db.Pool, agentID uuid.UUID, sec secretBackend) []toolReg {
|
||||
return []toolReg{
|
||||
{tool: &mcp.Tool{Name: "ping", Description: "Lightweight connectivity check. Returns server identity, no DB hit.",
|
||||
InputSchema: objSchema(),
|
||||
@@ -1629,6 +1629,81 @@ func allTools(pool *db.Pool, agentID uuid.UUID) []toolReg {
|
||||
ORDER BY e.slug
|
||||
LIMIT $3`, key, val, limit), nil
|
||||
}},
|
||||
|
||||
// ── Stage 5: Secret store (Infisical) ────────────────────────
|
||||
{tool: &mcp.Tool{Name: "get_secret", Description: "Retrieve a secret value from the Infisical vault. Returns the secret value. Use for service credentials, tokens, and keys needed to operate the homelab.",
|
||||
InputSchema: objSchema(
|
||||
prop{"key", "string", "Secret key to retrieve (e.g. 'matrix-token', 'clients/host:hubris/age-key')"},
|
||||
prop{"path", "string", "Secret path prefix (default '/')"},
|
||||
prop{"environment", "string", "Environment slug (default 'dev')"}),
|
||||
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if sec == nil {
|
||||
return textResult("error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)"), nil
|
||||
}
|
||||
args := argsMap(req)
|
||||
key, _ := args["key"].(string)
|
||||
if key == "" {
|
||||
return textResult("error: key is required"), nil
|
||||
}
|
||||
val, err := sec.Get(ctx, key)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("error: %v", err)), nil
|
||||
}
|
||||
return textResult(val), nil
|
||||
}},
|
||||
|
||||
{tool: &mcp.Tool{Name: "list_secrets", Description: "List secret keys in the Infisical vault. Returns key names only (no values). Filter by path prefix to scope to a client or shared path.",
|
||||
InputSchema: objSchema(
|
||||
prop{"path_prefix", "string", "Filter to keys matching this prefix (e.g. 'clients/', 'shared/', 'config/')"}),
|
||||
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if sec == nil {
|
||||
return textResult("error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)"), nil
|
||||
}
|
||||
args := argsMap(req)
|
||||
prefix, _ := args["path_prefix"].(string)
|
||||
|
||||
keys, err := sec.List(ctx)
|
||||
if err != nil {
|
||||
return textResult(fmt.Sprintf("error: %v", err)), nil
|
||||
}
|
||||
if prefix != "" {
|
||||
filtered := keys[:0]
|
||||
for _, k := range keys {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
filtered = append(filtered, k)
|
||||
}
|
||||
}
|
||||
keys = filtered
|
||||
}
|
||||
data, _ := json.MarshalIndent(keys, "", " ")
|
||||
return textResult(string(data)), nil
|
||||
}},
|
||||
|
||||
{tool: &mcp.Tool{Name: "set_secret", Description: "Store or update a secret in the Infisical vault. Use when discovering new credentials that need to be persisted. Requires operator approval (config_mutation).",
|
||||
InputSchema: objSchema(
|
||||
prop{"key", "string", "Secret key to store"},
|
||||
prop{"value", "string", "Secret value to store"},
|
||||
prop{"path", "string", "Secret path prefix (default '/')"},
|
||||
prop{"environment", "string", "Environment slug (default 'dev')"}),
|
||||
}, handler: func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
if sec == nil {
|
||||
return textResult("error: no secrets backend configured (set OIKOS_INFISICAL_SITE_URL)"), nil
|
||||
}
|
||||
args := argsMap(req)
|
||||
key, _ := args["key"].(string)
|
||||
value, _ := args["value"].(string)
|
||||
if key == "" {
|
||||
return textResult("error: key is required"), nil
|
||||
}
|
||||
if value == "" {
|
||||
return textResult("error: value is required"), nil
|
||||
}
|
||||
if err := sec.Set(ctx, key, value); err != nil {
|
||||
return textResult(fmt.Sprintf("error: %v", err)), nil
|
||||
}
|
||||
return textResult(fmt.Sprintf("secret %s stored", key)), nil
|
||||
}},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,14 +31,13 @@ type InfisicalConfig struct {
|
||||
// NewInfisicalBackend creates an Infisical backend. Connects lazily on first Get.
|
||||
func NewInfisicalBackend(cfg InfisicalConfig) *InfisicalBackend {
|
||||
autoRefresh := true
|
||||
cacheExpiry := 300 // 5 min cache
|
||||
|
||||
return &InfisicalBackend{
|
||||
cfg: cfg,
|
||||
client: infisical.NewInfisicalClient(context.Background(), infisical.Config{
|
||||
SiteUrl: cfg.SiteURL,
|
||||
AutoTokenRefresh: &autoRefresh,
|
||||
CacheExpiryInSeconds: cacheExpiry,
|
||||
CacheExpiryInSeconds: 0, // no caching — live reads over localhost
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -130,18 +129,20 @@ func (b *InfisicalBackend) Set(ctx context.Context, key string, value string) er
|
||||
return err
|
||||
}
|
||||
|
||||
// Try update first, fall back to create
|
||||
_, err := b.client.Secrets().Update(infisical.UpdateSecretOptions{
|
||||
// Try create first (idempotent — upserts); fall back to update on conflict.
|
||||
_, err := b.client.Secrets().Create(infisical.CreateSecretOptions{
|
||||
SecretKey: key,
|
||||
NewSecretValue: value,
|
||||
SecretValue: value,
|
||||
Environment: b.cfg.Env,
|
||||
SecretPath: b.cfg.SecretPath,
|
||||
ProjectID: b.cfg.ProjectID,
|
||||
Type: "shared",
|
||||
})
|
||||
if err != nil {
|
||||
_, err = b.client.Secrets().Create(infisical.CreateSecretOptions{
|
||||
// Create failed (key may already exist) — update instead.
|
||||
_, err = b.client.Secrets().Update(infisical.UpdateSecretOptions{
|
||||
SecretKey: key,
|
||||
SecretValue: value,
|
||||
NewSecretValue: value,
|
||||
Environment: b.cfg.Env,
|
||||
SecretPath: b.cfg.SecretPath,
|
||||
ProjectID: b.cfg.ProjectID,
|
||||
|
||||
Reference in New Issue
Block a user