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:
@@ -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
|
||||
}},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user