Skip to content
Draft
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
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ Compose reads `AGENTDNA_API_KEY` / `AGENTDNA_CHAIN_URL` / `JWT_SECRET` etc. from

## Endpoints

All endpoints are under `/agent-admin/v1`. Several share a common response envelope, `AgentResponse`:
Most endpoints are under `/agent-admin/v1`; the dashboard password endpoint is
under `/dashboard/v1`. Several share a common response envelope, `AgentResponse`:

- `status` (bool): whether the operation succeeded
- `message` (string): human-readable result
Expand Down Expand Up @@ -142,6 +143,35 @@ Sample failure:
}
```

### POST `/dashboard/v1/admin-change-password`

Changes an admin's password after verifying the current password. Unknown
usernames and incorrect current passwords return the same response so the
endpoint does not reveal whether an account exists.

Request type: `application/json`

```json
{
"username": "admin-user",
"currentPassword": "current-password",
"newPassword": "new-password"
}
```

The new password must contain at least 8 characters and must differ from the
current password. Success returns HTTP 200:

```json
{
"status": true,
"message": "Password updated successfully."
}
```

Credential failures return HTTP 401 with `Incorrect credentials`. Password
policy failures return HTTP 400 with the applicable message.

### POST `/create-agent`

Creates a new agent: stores its policy file (UUID-suffixed), deploys the agent NFT via AgentDNA, and records the agent in the `agents` table. The caller's DID (`creator_did`) must already be registered via `/register-admin`.
Expand Down
21 changes: 20 additions & 1 deletion db.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ def get_admin_by_username(username: str) -> dict[str, str] | None:
return None
return {"did": row[0], "org": row[1], "password": row[2]}


def update_admin_password(
username: str,
current_password_hash: str,
new_password_hash: str,
) -> bool:
"""Replace the expected password hash, returning whether a row was updated."""
with pool.connection() as conn:
cur = conn.execute(
"""
UPDATE admin
SET password = %s
WHERE username = %s AND password = %s
""",
(new_password_hash, username, current_password_hash),
)
return cur.rowcount > 0


def get_all_agents() -> list[dict[str, str]]:
"""Return all registered agents (without policy — see get_agent_by_did for that)."""
with pool.connection() as conn:
Expand Down Expand Up @@ -170,4 +189,4 @@ def agent_exists(did: str) -> bool:
"SELECT 1 FROM agents WHERE did = %s",
(did,),
).fetchone()
return row is not None
return row is not None
26 changes: 26 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
CreateAgentResponse,
RegisterAdminRequest,
LoginRequest,
AdminChangePasswordRequest,
AdminChangePasswordResponse,
AppRequest
)
from services import (
Expand All @@ -26,6 +28,7 @@
list_agents,
get_agent,
login,
change_admin_password,
)
from agentdna.types import (
IntentWorkflow
Expand Down Expand Up @@ -169,6 +172,29 @@ async def login_endpoint(payload: LoginRequest) -> AgentResponse:
return AgentResponse(status=status, message=message, data=token)


@app.post(
"/dashboard/v1/admin-change-password",
response_model=AdminChangePasswordResponse,
responses={
400: {"model": AdminChangePasswordResponse},
401: {"model": AdminChangePasswordResponse},
500: {"model": AdminChangePasswordResponse},
},
)
async def admin_change_password_endpoint(
payload: AdminChangePasswordRequest,
) -> JSONResponse:
status, message, status_code = await change_admin_password(
payload.username,
payload.current_password,
payload.new_password,
)
return JSONResponse(
status_code=status_code,
content={"status": status, "message": message},
)


@app.post("/agent-admin/v1/update-agent-policies", response_model=AgentResponse)
async def update_agent_policies_endpoint(
policy: UploadFile = File(...),
Expand Down
19 changes: 19 additions & 0 deletions schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ class LoginRequest(BaseModel):
password: str = Field(..., description="Admin password")


class AdminChangePasswordRequest(BaseModel):
username: str = Field(..., description="Admin username")
current_password: str = Field(
...,
alias="currentPassword",
description="Admin's current password",
)
new_password: str = Field(
...,
alias="newPassword",
description="Admin's new password",
)


class AdminChangePasswordResponse(BaseModel):
status: bool = Field(..., description="Whether the password was updated")
message: str = Field(..., description="Human-readable result message")


class CreateAgentResponse(BaseModel):
status: bool = Field(..., description="Whether the operation succeeded")
message: str = Field(..., description="Human-readable result message")
Expand Down
47 changes: 46 additions & 1 deletion services.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import shutil
from pathlib import Path
from uuid import uuid4
Expand All @@ -18,6 +19,7 @@
add_registered_agent,
get_username_by_did,
get_admin_by_username,
update_admin_password,
get_all_agents,
get_agent_by_did,
set_agent_policy,
Expand All @@ -27,7 +29,10 @@
from security import hash_password, verify_password, create_access_token
from config import settings

logger = logging.getLogger(__name__)

POLICY_STORE = Path(__file__).parent / "policy_store"
MIN_PASSWORD_LENGTH = 8


def _agent_dir(org_id: str, agent_name: str) -> Path:
Expand Down Expand Up @@ -165,6 +170,46 @@ async def login(username: str, password: str) -> tuple[bool, str, str | None]:
return True, "Login successful", token


async def change_admin_password(
username: str,
current_password: str,
new_password: str,
) -> tuple[bool, str, int]:
try:
admin = get_admin_by_username(username)
except Exception:
logger.exception("Failed to load admin credentials for password change")
return False, "Unable to update password.", 500

if admin is None or not verify_password(current_password, admin["password"]):
return False, "Incorrect credentials", 401

if verify_password(new_password, admin["password"]):
return False, "New password must differ from the current one.", 400

if len(new_password) < MIN_PASSWORD_LENGTH:
return (
False,
f"Password must be at least {MIN_PASSWORD_LENGTH} characters.",
400,
)

try:
updated = update_admin_password(
username,
admin["password"],
hash_password(new_password),
)
except Exception:
logger.exception("Failed to persist admin password change")
return False, "Unable to update password.", 500

if not updated:
return False, "Incorrect credentials", 401

return True, "Password updated successfully.", 200


async def authorize_action(agent_id: str, action_intent: str, intent_workflow: IntentWorkflow) -> tuple[bool, str]:
provenance_layer = Provenance(
name="admin-server",
Expand Down Expand Up @@ -303,4 +348,4 @@ async def get_agent(did: str) -> tuple[bool, str, dict[str, str] | None]:
return False, f"Failed to fetch agent: {exc}", None
if agent is None:
return False, f"No agent found with did '{did}'", None
return True, "Agent retrieved", agent
return True, "Agent retrieved", agent
Loading