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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Install: pip install pre-commit && pre-commit install
# Run manually: pre-commit run --all-files

exclude: ^backend/app/one_person_security_dept/(openclaw|claude_agent_sdk_python/claude-agent-sdk-python)/

repos:
# General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
34 changes: 34 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,37 @@ engine-arm:

engine-amd:
DOCKER_BUILDKIT=1 docker buildx build --platform linux/amd64 --progress=plain --file engine.amd64.Dockerfile -t seclens-engine-amd:0.2 .

security-sdk-subtree-setup:
../scripts/update-security-sdk-subtree.sh --setup-only

security-sdk-subtree-update:
@if [ -z "$(REF)" ]; then \
echo "Usage: make security-sdk-subtree-update REF=<tag-or-branch>"; \
exit 1; \
fi
../scripts/update-security-sdk-subtree.sh --ref $(REF)

security-sdk-subtree-dry-run:
@if [ -z "$(REF)" ]; then \
echo "Usage: make security-sdk-subtree-dry-run REF=<tag-or-branch>"; \
exit 1; \
fi
../scripts/update-security-sdk-subtree.sh --ref $(REF) --dry-run

openclaw-subtree-setup:
../scripts/update-openclaw-subtree.sh --setup-only

openclaw-subtree-update:
@if [ -z "$(REF)" ]; then \
echo "Usage: make openclaw-subtree-update REF=<tag-or-branch>"; \
exit 1; \
fi
../scripts/update-openclaw-subtree.sh --ref $(REF)

openclaw-subtree-dry-run:
@if [ -z "$(REF)" ]; then \
echo "Usage: make openclaw-subtree-dry-run REF=<tag-or-branch>"; \
exit 1; \
fi
../scripts/update-openclaw-subtree.sh --ref $(REF) --dry-run
54 changes: 54 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,60 @@ pytest
pytest --cov=app
```

## 🔄 One Person Security Dept SDK 更新(Git Subtree)

`claude-agent-sdk-python` 已 vendored 到以下目录:

`backend/app/one_person_security_dept/claude_agent_sdk_python/claude-agent-sdk-python`

推荐通过 `git subtree` 更新,不影响现有开发者工作流。

```bash
# 1) 一次性设置 upstream remote
make security-sdk-subtree-setup

# 2) 先预览更新(建议)
make security-sdk-subtree-dry-run REF=v0.1.43

# 3) 实际更新到指定 tag/branch
make security-sdk-subtree-update REF=v0.1.43
# 或
make security-sdk-subtree-update REF=main
```

也可直接运行脚本:

```bash
./scripts/update-security-sdk-subtree.sh --ref v0.1.43
```

## 🔄 One Person Security Dept OpenClaw 更新(Git Subtree)

`openclaw` 已 vendored 到以下目录:

`backend/app/one_person_security_dept/openclaw`

同样推荐通过 `git subtree` 更新:

```bash
# 1) 一次性设置 upstream remote
make openclaw-subtree-setup

# 2) 先预览更新(建议)
make openclaw-subtree-dry-run REF=v2026.2.24

# 3) 实际更新到指定 tag/branch
make openclaw-subtree-update REF=v2026.2.24
# 或
make openclaw-subtree-update REF=main
```

也可直接运行脚本:

```bash
./scripts/update-openclaw-subtree.sh --ref v2026.2.24
```


### Docker 部署 (推荐)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""add_security_dept_tasks_table

Revision ID: 000000000008
Revises: 000000000007
Create Date: 2026-02-26 16:00:00.000000

"""

from typing import Sequence, Union

import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "000000000008"
down_revision: Union[str, None] = "000000000007"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"security_dept_tasks",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("user_id", sa.String(length=255), nullable=False),
sa.Column("workspace_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("scenario", sa.String(length=50), nullable=False),
sa.Column("profile", sa.String(length=100), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False),
sa.Column("target", sa.Text(), nullable=True),
sa.Column("instruction_digest", sa.String(length=64), nullable=False),
sa.Column("instruction_preview", sa.String(length=500), nullable=False),
sa.Column(
"selected_skills",
postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
server_default=sa.text("'[]'::jsonb"),
),
sa.Column("summary_md", sa.Text(), nullable=True),
sa.Column("result_structured", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column("error_code", sa.String(length=100), nullable=True),
sa.Column("error_message", sa.Text(), nullable=True),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("duration_ms", sa.BigInteger(), nullable=True),
sa.Column("token_usage", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column("cost_usd", sa.Float(), nullable=True),
sa.Column("execution_stats", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)

op.create_index("ix_security_dept_tasks_status", "security_dept_tasks", ["status"], unique=False)
op.create_index("ix_security_dept_tasks_user_id", "security_dept_tasks", ["user_id"], unique=False)
op.create_index("ix_security_dept_tasks_workspace_id", "security_dept_tasks", ["workspace_id"], unique=False)
op.create_index(
"ix_security_dept_tasks_user_created",
"security_dept_tasks",
["user_id", "created_at"],
unique=False,
)
op.create_index(
"ix_security_dept_tasks_user_status",
"security_dept_tasks",
["user_id", "status"],
unique=False,
)
op.create_index(
"ix_security_dept_tasks_workspace_created",
"security_dept_tasks",
["workspace_id", "created_at"],
unique=False,
)


def downgrade() -> None:
op.drop_index("ix_security_dept_tasks_workspace_created", table_name="security_dept_tasks")
op.drop_index("ix_security_dept_tasks_user_status", table_name="security_dept_tasks")
op.drop_index("ix_security_dept_tasks_user_created", table_name="security_dept_tasks")
op.drop_index("ix_security_dept_tasks_workspace_id", table_name="security_dept_tasks")
op.drop_index("ix_security_dept_tasks_user_id", table_name="security_dept_tasks")
op.drop_index("ix_security_dept_tasks_status", table_name="security_dept_tasks")
op.drop_table("security_dept_tasks")
3 changes: 3 additions & 0 deletions backend/app/api/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from fastapi import APIRouter

from app.one_person_security_dept.api.router import router as security_dept_router

from .admin_sandboxes import router as admin_sandboxes_router
from .api_keys import router as api_keys_router
from .auth import router as auth_router
Expand Down Expand Up @@ -56,6 +58,7 @@
traces_router,
users_router,
environment_router,
security_dept_router,
]


Expand Down
34 changes: 34 additions & 0 deletions backend/app/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,40 @@ def parse_cors_origins(cls, v: Union[str, List[str]]) -> List[str]:
description="Workspace root directory for storing session files and workspace data",
)

# One Person Security Dept
security_dept_enabled: bool = Field(
default=True,
validation_alias=AliasChoices("SECURITY_DEPT_ENABLED", "ONE_PERSON_SECURITY_DEPT_ENABLED"),
description="Enable One Person Security Dept module",
)
security_dept_max_concurrent_tasks: int = Field(
default=2,
validation_alias=AliasChoices("SECURITY_DEPT_MAX_CONCURRENT_TASKS", "ONE_PERSON_SECURITY_DEPT_MAX_CONCURRENT"),
description="Maximum concurrent Security Dept tasks per backend process",
)
security_dept_task_timeout_seconds: int = Field(
default=1800,
validation_alias=AliasChoices(
"SECURITY_DEPT_TASK_TIMEOUT_SECONDS", "ONE_PERSON_SECURITY_DEPT_TASK_TIMEOUT_SECONDS"
),
description="Timeout in seconds for one Security Dept task",
)
security_dept_workdir_root: str = Field(
default=str(BASE_DIR / "workspace" / "security_dept_runs"),
validation_alias=AliasChoices("SECURITY_DEPT_WORKDIR_ROOT", "ONE_PERSON_SECURITY_DEPT_WORKDIR_ROOT"),
description="Working root directory for Security Dept task execution sandboxes",
)
security_dept_event_ttl_seconds: int = Field(
default=7200,
validation_alias=AliasChoices("SECURITY_DEPT_EVENT_TTL_SECONDS", "ONE_PERSON_SECURITY_DEPT_EVENT_TTL_SECONDS"),
description="TTL for Security Dept task temporary Redis keys and status",
)
security_dept_claude_cli_path: Optional[str] = Field(
default=None,
validation_alias=AliasChoices("SECURITY_DEPT_CLAUDE_CLI_PATH", "ONE_PERSON_SECURITY_DEPT_CLAUDE_CLI_PATH"),
description="Optional custom path to Claude Code CLI binary for claude-agent-sdk",
)

@property
def WORKSPACE_ROOT(self) -> str:
"""Alias for workspace_root for backward compatibility"""
Expand Down
2 changes: 2 additions & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .oauth_account import OAuthAccount
from .organization import Member, Organization
from .security_audit_log import SecurityAuditLog
from .security_dept_task import SecurityDeptTask
from .settings import Environment, Settings, WorkspaceEnvironment
from .skill import Skill, SkillFile
from .user_sandbox import UserSandbox
Expand Down Expand Up @@ -83,6 +84,7 @@
"Skill",
"SkillFile",
"SecurityAuditLog",
"SecurityDeptTask",
"Memory",
"ExecutionTrace",
"ExecutionObservation",
Expand Down
113 changes: 113 additions & 0 deletions backend/app/models/security_dept_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Security Dept task model.

Stores execution metadata and summarized outputs for One Person Security Dept.
"""

from __future__ import annotations

import uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Optional

from sqlalchemy import BigInteger, DateTime, Float, ForeignKey, Index, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.core.database import Base
from app.models.base import TimestampMixin

if TYPE_CHECKING:
from app.models.auth import AuthUser


def utc_now() -> datetime:
return datetime.now(timezone.utc)


class SecurityDeptTask(Base, TimestampMixin):
"""Task record for Security Dept asynchronous runs."""

__tablename__ = "security_dept_tasks"

id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)

user_id: Mapped[str] = mapped_column(
String(255),
ForeignKey("user.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
workspace_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True, index=True)

scenario: Mapped[str] = mapped_column(String(50), nullable=False, default="pentest")
profile: Mapped[str] = mapped_column(String(100), nullable=False, default="pentest_full_access_v1")
status: Mapped[str] = mapped_column(String(32), nullable=False, default="queued", index=True)

target: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
instruction_digest: Mapped[str] = mapped_column(String(64), nullable=False)
instruction_preview: Mapped[str] = mapped_column(String(500), nullable=False)
selected_skills: Mapped[list[str]] = mapped_column(JSONB, nullable=False, default=list)

summary_md: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
result_structured: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)

error_code: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)

started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
duration_ms: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)

token_usage: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
cost_usd: Mapped[Optional[float]] = mapped_column(Float, nullable=True)

execution_stats: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)

user: Mapped["AuthUser"] = relationship("AuthUser", lazy="selectin")

__table_args__ = (
Index("ix_security_dept_tasks_user_created", "user_id", "created_at"),
Index("ix_security_dept_tasks_user_status", "user_id", "status"),
Index("ix_security_dept_tasks_workspace_created", "workspace_id", "created_at"),
)

def mark_running(self) -> None:
self.status = "running"
self.started_at = utc_now()
self.error_code = None
self.error_message = None

def mark_completed(
self,
*,
summary_md: Optional[str],
result_structured: Optional[dict[str, Any]],
token_usage: Optional[dict[str, Any]],
cost_usd: Optional[float],
) -> None:
now = utc_now()
self.status = "completed"
self.summary_md = summary_md
self.result_structured = result_structured
self.token_usage = token_usage
self.cost_usd = cost_usd
self.finished_at = now
if self.started_at is not None:
self.duration_ms = int((now - self.started_at).total_seconds() * 1000)

def mark_failed(self, *, error_code: str, error_message: str) -> None:
now = utc_now()
self.status = "failed"
self.error_code = error_code
self.error_message = error_message
self.finished_at = now
if self.started_at is not None:
self.duration_ms = int((now - self.started_at).total_seconds() * 1000)

def mark_cancelled(self) -> None:
now = utc_now()
self.status = "cancelled"
self.finished_at = now
if self.started_at is not None:
self.duration_ms = int((now - self.started_at).total_seconds() * 1000)
1 change: 1 addition & 0 deletions backend/app/one_person_security_dept/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""One Person Security Dept module."""
5 changes: 5 additions & 0 deletions backend/app/one_person_security_dept/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""API module for One Person Security Dept."""

from .router import router

__all__ = ["router"]
Loading
Loading