feat(FN-009): implement config and secrets management with runtime injection

- Add RuntimeInjectionService for scope-based config/secret resolution
- Mount configs as JSON files at /app/config/ with 0400 permissions
- Inject secrets as environment variables with uppercase keys
- Implement scope hierarchy: instance > project > user > global
- Create ConfigListPage and SecretListPage frontend components
- Mask secret values in API responses (never expose decrypted)
- Validate secrets exist before spawning containers
- Add comprehensive tests for runtime injection service
- Update documentation with config/secrets workflow
This commit is contained in:
2026-05-15 16:44:26 +02:00
parent 78aaddb2b5
commit 51d93d9dc6
12 changed files with 838 additions and 86 deletions
+5 -29
View File
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.dependencies import get_current_active_user
from app.db import get_db_session
from app.encryption import decrypt_value, encrypt_value
from app.encryption import encrypt_value
from app.models.project import Project
from app.models.secret import Secret
from app.models.tool_instance import ToolInstance
@@ -55,13 +55,7 @@ async def create_secret(
session.add(secret)
await session.commit()
await session.refresh(secret)
return SecretRead(
id=secret.id,
scope_type=secret.scope_type,
scope_id=secret.scope_id,
key=secret.key,
value=decrypt_value(secret.encrypted_value),
)
return SecretRead.from_secret(secret)
@router.get("/secrets", response_model=list[SecretRead])
@@ -82,13 +76,7 @@ async def list_secrets(
for s in secrets:
try:
await _verify_secret_ownership(s, current_user, session)
allowed.append(SecretRead(
id=s.id,
scope_type=s.scope_type,
scope_id=s.scope_id,
key=s.key,
value=decrypt_value(s.encrypted_value),
))
allowed.append(SecretRead.from_secret(s))
except HTTPException:
pass
return allowed
@@ -104,13 +92,7 @@ async def get_secret(
if not s:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Secret not found")
await _verify_secret_ownership(s, current_user, session)
return SecretRead(
id=s.id,
scope_type=s.scope_type,
scope_id=s.scope_id,
key=s.key,
value=decrypt_value(s.encrypted_value),
)
return SecretRead.from_secret(s)
@router.put("/secrets/{secret_id}", response_model=SecretRead)
@@ -130,13 +112,7 @@ async def update_secret(
s.encrypted_value = encrypt_value(secret_in.value)
await session.commit()
await session.refresh(s)
return SecretRead(
id=s.id,
scope_type=s.scope_type,
scope_id=s.scope_id,
key=s.key,
value=decrypt_value(s.encrypted_value),
)
return SecretRead.from_secret(s)
@router.delete("/secrets/{secret_id}", status_code=status.HTTP_204_NO_CONTENT)