ags-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ags_cli-0.1.0.dist-info/METADATA +332 -0
- ags_cli-0.1.0.dist-info/RECORD +156 -0
- ags_cli-0.1.0.dist-info/WHEEL +5 -0
- ags_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ags_cli-0.1.0.dist-info/top_level.txt +2 -0
- cli/__init__.py +0 -0
- cli/__main__.py +4 -0
- cli/client.py +145 -0
- cli/commands/__init__.py +0 -0
- cli/commands/adapter.py +33 -0
- cli/commands/artifact.py +21 -0
- cli/commands/ask.py +212 -0
- cli/commands/chat.py +649 -0
- cli/commands/diff.py +49 -0
- cli/commands/doctor.py +93 -0
- cli/commands/gui.py +45 -0
- cli/commands/init.py +20 -0
- cli/commands/mcp.py +50 -0
- cli/commands/memory.py +65 -0
- cli/commands/model.py +73 -0
- cli/commands/policy.py +52 -0
- cli/commands/project.py +122 -0
- cli/commands/quota.py +107 -0
- cli/commands/run.py +84 -0
- cli/commands/serve.py +171 -0
- cli/commands/service.py +236 -0
- cli/commands/session.py +219 -0
- cli/commands/stats.py +96 -0
- cli/commands/status.py +36 -0
- cli/commands/task.py +37 -0
- cli/commands/usage.py +86 -0
- cli/local.py +84 -0
- cli/main.py +149 -0
- harness/__init__.py +4 -0
- harness/adapters/__init__.py +26 -0
- harness/adapters/antigravity.py +301 -0
- harness/adapters/claude_code.py +522 -0
- harness/adapters/local_openai.py +534 -0
- harness/adapters/mock.py +112 -0
- harness/adapters/probe.py +65 -0
- harness/adapters/registry.py +56 -0
- harness/adapters/warm_pool.py +255 -0
- harness/api/__init__.py +0 -0
- harness/api/router.py +38 -0
- harness/api/v1/__init__.py +0 -0
- harness/api/v1/adapters.py +105 -0
- harness/api/v1/approvals.py +173 -0
- harness/api/v1/artifacts.py +44 -0
- harness/api/v1/attachments.py +48 -0
- harness/api/v1/doctor.py +22 -0
- harness/api/v1/health.py +37 -0
- harness/api/v1/mcp.py +131 -0
- harness/api/v1/memory.py +80 -0
- harness/api/v1/policies.py +49 -0
- harness/api/v1/project.py +302 -0
- harness/api/v1/runs.py +98 -0
- harness/api/v1/sessions.py +248 -0
- harness/api/v1/stream.py +110 -0
- harness/api/v1/tasks.py +30 -0
- harness/api/v1/usage.py +58 -0
- harness/bootstrap.py +216 -0
- harness/db.py +164 -0
- harness/deps.py +58 -0
- harness/eventbus/__init__.py +20 -0
- harness/eventbus/inprocess_bus.py +85 -0
- harness/main.py +144 -0
- harness/mcp/__init__.py +22 -0
- harness/mcp/client.py +139 -0
- harness/mcp/config_gen.py +77 -0
- harness/mcp/registry.py +156 -0
- harness/mcp/tools.py +81 -0
- harness/memory/__init__.py +13 -0
- harness/memory/context_budget.py +116 -0
- harness/memory/embed.py +217 -0
- harness/memory/extract.py +74 -0
- harness/memory/ingest.py +106 -0
- harness/memory/namespaces.py +57 -0
- harness/memory/ranking.py +31 -0
- harness/memory/redact.py +37 -0
- harness/memory/service.py +320 -0
- harness/memory/sqlite_vec_backend.py +266 -0
- harness/memory/summarize.py +68 -0
- harness/models/__init__.py +35 -0
- harness/models/adapter_health.py +27 -0
- harness/models/approval.py +37 -0
- harness/models/artifact.py +35 -0
- harness/models/audit_log.py +33 -0
- harness/models/comparison.py +33 -0
- harness/models/enums.py +146 -0
- harness/models/mcp_server_config.py +49 -0
- harness/models/memory.py +76 -0
- harness/models/policy.py +29 -0
- harness/models/provider_config.py +35 -0
- harness/models/run.py +46 -0
- harness/models/run_event.py +35 -0
- harness/models/session.py +50 -0
- harness/models/task.py +30 -0
- harness/models/types.py +63 -0
- harness/models/workspace.py +27 -0
- harness/observability/__init__.py +4 -0
- harness/observability/audit.py +88 -0
- harness/observability/logging.py +47 -0
- harness/observability/metrics.py +43 -0
- harness/observability/tracing.py +38 -0
- harness/orchestrator/__init__.py +19 -0
- harness/orchestrator/engine.py +821 -0
- harness/orchestrator/handoff.py +33 -0
- harness/orchestrator/lifecycle.py +69 -0
- harness/orchestrator/native_resume.py +93 -0
- harness/orchestrator/policies.py +101 -0
- harness/orchestrator/reconcile.py +39 -0
- harness/orchestrator/run_graph.py +62 -0
- harness/orchestrator/snapshot.py +49 -0
- harness/schemas/__init__.py +1 -0
- harness/schemas/adapter.py +26 -0
- harness/schemas/approval.py +25 -0
- harness/schemas/artifact.py +17 -0
- harness/schemas/common.py +20 -0
- harness/schemas/memory.py +50 -0
- harness/schemas/policy.py +39 -0
- harness/schemas/run.py +116 -0
- harness/schemas/session.py +93 -0
- harness/schemas/task.py +24 -0
- harness/security/__init__.py +5 -0
- harness/security/approval_policy.py +107 -0
- harness/security/auth.py +106 -0
- harness/security/permissions.py +59 -0
- harness/security/risk.py +59 -0
- harness/security/secrets.py +49 -0
- harness/services/__init__.py +1 -0
- harness/services/adapters_health.py +212 -0
- harness/services/approvals.py +84 -0
- harness/services/artifacts.py +78 -0
- harness/services/attachments.py +228 -0
- harness/services/comparison.py +345 -0
- harness/services/doctor.py +156 -0
- harness/services/files.py +287 -0
- harness/services/mcp_servers.py +179 -0
- harness/services/project_git.py +101 -0
- harness/services/retention.py +97 -0
- harness/services/runs.py +155 -0
- harness/services/runs_diff.py +201 -0
- harness/services/sessions.py +242 -0
- harness/services/ssh.py +300 -0
- harness/services/stats.py +132 -0
- harness/services/tasks.py +41 -0
- harness/services/workspaces.py +184 -0
- harness/services/worktrees.py +439 -0
- harness/settings.py +186 -0
- harness/skills/__init__.py +11 -0
- harness/skills/manager.py +141 -0
- harness/tools/__init__.py +1 -0
- harness/tools/fs_tools.py +295 -0
- harness/workers/__init__.py +0 -0
- harness/workers/dispatch.py +26 -0
- harness/workers/inprocess.py +135 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Summarization pipeline.
|
|
2
|
+
|
|
3
|
+
Produces provider-independent canonical summaries from the raw ``run_events``
|
|
4
|
+
ledger. This is an extractive, deterministic summarizer (no model call) so
|
|
5
|
+
the pipeline is testable offline; an LLM-backed summarizer can be swapped in behind
|
|
6
|
+
the same function signature. The session's rolling ``summary`` lets a session be
|
|
7
|
+
resumed from canonical state instead of a vendor transcript."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import select
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
from harness.models.enums import RunEventType
|
|
17
|
+
from harness.models.run import Run
|
|
18
|
+
from harness.models.run_event import RunEvent
|
|
19
|
+
from harness.models.session import Session
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _extractive_summary(objective: str, messages: list[str], max_chars: int = 1200) -> str:
|
|
23
|
+
head = f"Objective: {objective.strip()}"
|
|
24
|
+
body: list[str] = []
|
|
25
|
+
used = len(head)
|
|
26
|
+
for msg in messages:
|
|
27
|
+
snippet = msg.strip().replace("\n", " ")
|
|
28
|
+
if not snippet:
|
|
29
|
+
continue
|
|
30
|
+
line = f"- {snippet[:240]}"
|
|
31
|
+
if used + len(line) > max_chars:
|
|
32
|
+
break
|
|
33
|
+
body.append(line)
|
|
34
|
+
used += len(line)
|
|
35
|
+
return head + ("\nKey outputs:\n" + "\n".join(body) if body else "")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def summarize_session(session: AsyncSession, session_id: uuid.UUID) -> str:
|
|
39
|
+
"""Recompute and persist the canonical session summary + a snapshot."""
|
|
40
|
+
sess = (
|
|
41
|
+
await session.execute(select(Session).where(Session.id == session_id))
|
|
42
|
+
).scalar_one()
|
|
43
|
+
|
|
44
|
+
run_ids = (
|
|
45
|
+
await session.execute(select(Run.id).where(Run.session_id == session_id))
|
|
46
|
+
).scalars().all()
|
|
47
|
+
|
|
48
|
+
messages: list[str] = []
|
|
49
|
+
if run_ids:
|
|
50
|
+
rows = (
|
|
51
|
+
await session.execute(
|
|
52
|
+
select(RunEvent.payload)
|
|
53
|
+
.where(
|
|
54
|
+
RunEvent.run_id.in_(run_ids),
|
|
55
|
+
RunEvent.type == RunEventType.message,
|
|
56
|
+
)
|
|
57
|
+
.order_by(RunEvent.ts.asc())
|
|
58
|
+
)
|
|
59
|
+
).scalars().all()
|
|
60
|
+
messages = [p.get("text", "") for p in rows if isinstance(p, dict)]
|
|
61
|
+
|
|
62
|
+
summary = _extractive_summary(sess.canonical_objective, messages)
|
|
63
|
+
sess.summary = summary
|
|
64
|
+
snapshots = list(sess.summary_snapshots or [])
|
|
65
|
+
snapshots.append({"summary": summary, "n_messages": len(messages)})
|
|
66
|
+
sess.summary_snapshots = snapshots
|
|
67
|
+
await session.flush()
|
|
68
|
+
return summary
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""SQLAlchemy models. Importing this package registers every table on
|
|
2
|
+
``Base.metadata`` for ``metadata.create_all`` (used at startup and in tests)."""
|
|
3
|
+
|
|
4
|
+
from harness.models.adapter_health import AdapterHealth
|
|
5
|
+
from harness.models.approval import Approval
|
|
6
|
+
from harness.models.artifact import Artifact
|
|
7
|
+
from harness.models.audit_log import AuditLog
|
|
8
|
+
from harness.models.comparison import Comparison
|
|
9
|
+
from harness.models.mcp_server_config import McpServerConfig
|
|
10
|
+
from harness.models.memory import MemoryEmbedding, MemoryItem
|
|
11
|
+
from harness.models.policy import Policy
|
|
12
|
+
from harness.models.provider_config import ProviderConfig
|
|
13
|
+
from harness.models.run import Run
|
|
14
|
+
from harness.models.run_event import RunEvent
|
|
15
|
+
from harness.models.session import Session
|
|
16
|
+
from harness.models.task import Task
|
|
17
|
+
from harness.models.workspace import Workspace
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AdapterHealth",
|
|
21
|
+
"Approval",
|
|
22
|
+
"Artifact",
|
|
23
|
+
"AuditLog",
|
|
24
|
+
"Comparison",
|
|
25
|
+
"McpServerConfig",
|
|
26
|
+
"MemoryEmbedding",
|
|
27
|
+
"MemoryItem",
|
|
28
|
+
"Policy",
|
|
29
|
+
"ProviderConfig",
|
|
30
|
+
"Run",
|
|
31
|
+
"RunEvent",
|
|
32
|
+
"Session",
|
|
33
|
+
"Task",
|
|
34
|
+
"Workspace",
|
|
35
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, func
|
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
8
|
+
|
|
9
|
+
from harness.db import Base
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AdapterHealth(Base):
|
|
14
|
+
"""Time-series of adapter health checks for the observability/health page."""
|
|
15
|
+
|
|
16
|
+
__tablename__ = "adapter_health"
|
|
17
|
+
|
|
18
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
19
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
20
|
+
)
|
|
21
|
+
provider_config_id: Mapped[uuid.UUID] = mapped_column(
|
|
22
|
+
GUID(), ForeignKey("provider_configs.id", ondelete="CASCADE"), index=True
|
|
23
|
+
)
|
|
24
|
+
ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
25
|
+
ok: Mapped[bool] = mapped_column(Boolean)
|
|
26
|
+
latency_ms: Mapped[float] = mapped_column(Float, default=0.0)
|
|
27
|
+
detail: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import DateTime, Enum, ForeignKey, String
|
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
8
|
+
|
|
9
|
+
from harness.db import Base, TimestampMixin
|
|
10
|
+
from harness.models.enums import ApprovalActionClass, ApprovalStatus
|
|
11
|
+
from harness.models.types import GUID, json_col
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Approval(Base, TimestampMixin):
|
|
15
|
+
"""A human-in-the-loop checkpoint for a sensitive action. Runs that hit a gate
|
|
16
|
+
transition to ``needs_approval`` and block until a decision is recorded here."""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "approvals"
|
|
19
|
+
|
|
20
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
21
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
22
|
+
)
|
|
23
|
+
session_id: Mapped[uuid.UUID] = mapped_column(
|
|
24
|
+
GUID(), ForeignKey("sessions.id", ondelete="CASCADE"), index=True
|
|
25
|
+
)
|
|
26
|
+
run_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
27
|
+
GUID(), ForeignKey("runs.id", ondelete="SET NULL"), nullable=True
|
|
28
|
+
)
|
|
29
|
+
action_class: Mapped[ApprovalActionClass] = mapped_column(
|
|
30
|
+
Enum(ApprovalActionClass, name="approval_action_class")
|
|
31
|
+
)
|
|
32
|
+
payload: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
33
|
+
status: Mapped[ApprovalStatus] = mapped_column(
|
|
34
|
+
Enum(ApprovalStatus, name="approval_status"), default=ApprovalStatus.pending
|
|
35
|
+
)
|
|
36
|
+
decided_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
37
|
+
decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import BigInteger, Enum, ForeignKey, String
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import ArtifactKind
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Artifact(Base, TimestampMixin):
|
|
14
|
+
"""A generated output (file, patch, plan, doc). Content addressed by sha256
|
|
15
|
+
for dedup; stored by reference (path/uri) — large blobs live outside the DB."""
|
|
16
|
+
|
|
17
|
+
__tablename__ = "artifacts"
|
|
18
|
+
|
|
19
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
20
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
21
|
+
)
|
|
22
|
+
workspace_id: Mapped[uuid.UUID] = mapped_column(
|
|
23
|
+
GUID(), ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
24
|
+
)
|
|
25
|
+
session_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
26
|
+
GUID(), ForeignKey("sessions.id", ondelete="SET NULL"), nullable=True
|
|
27
|
+
)
|
|
28
|
+
run_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
29
|
+
GUID(), ForeignKey("runs.id", ondelete="SET NULL"), nullable=True
|
|
30
|
+
)
|
|
31
|
+
kind: Mapped[ArtifactKind] = mapped_column(Enum(ArtifactKind, name="artifact_kind"))
|
|
32
|
+
uri: Mapped[str] = mapped_column(String(1024))
|
|
33
|
+
sha256: Mapped[str] = mapped_column(String(64), index=True)
|
|
34
|
+
size: Mapped[int] = mapped_column(BigInteger, default=0)
|
|
35
|
+
meta: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import DateTime, String, func
|
|
7
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
8
|
+
|
|
9
|
+
from harness.db import Base
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuditLog(Base):
|
|
14
|
+
"""Immutable, hash-chained audit record. Each row's ``hash`` covers its own
|
|
15
|
+
content plus ``prev_hash``, making tampering detectable. Append-only — never
|
|
16
|
+
updated or deleted by application code."""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "audit_log"
|
|
19
|
+
|
|
20
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
21
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
22
|
+
)
|
|
23
|
+
ts: Mapped[datetime] = mapped_column(
|
|
24
|
+
DateTime(timezone=True), server_default=func.now(), index=True
|
|
25
|
+
)
|
|
26
|
+
actor: Mapped[str] = mapped_column(String(128))
|
|
27
|
+
action: Mapped[str] = mapped_column(String(128), index=True)
|
|
28
|
+
target_type: Mapped[str] = mapped_column(String(64))
|
|
29
|
+
target_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
30
|
+
before: Mapped[dict | None] = mapped_column(json_col(), nullable=True)
|
|
31
|
+
after: Mapped[dict | None] = mapped_column(json_col(), nullable=True)
|
|
32
|
+
prev_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
33
|
+
hash: Mapped[str] = mapped_column(String(64))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Enum, ForeignKey
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import ComparisonStrategy
|
|
10
|
+
from harness.models.types import GUID, json_col, uuid_array
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Comparison(Base, TimestampMixin):
|
|
14
|
+
"""The result of comparing/merging two or more runs into a canonical answer.
|
|
15
|
+
When a merge is persisted, ``canonical_memory_item_id`` links the resulting
|
|
16
|
+
workspace-memory record."""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "comparisons"
|
|
19
|
+
|
|
20
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
21
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
22
|
+
)
|
|
23
|
+
session_id: Mapped[uuid.UUID] = mapped_column(
|
|
24
|
+
GUID(), ForeignKey("sessions.id", ondelete="CASCADE"), index=True
|
|
25
|
+
)
|
|
26
|
+
run_ids: Mapped[list[uuid.UUID]] = mapped_column(uuid_array())
|
|
27
|
+
strategy: Mapped[ComparisonStrategy] = mapped_column(
|
|
28
|
+
Enum(ComparisonStrategy, name="comparison_strategy")
|
|
29
|
+
)
|
|
30
|
+
result: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
31
|
+
canonical_memory_item_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
32
|
+
GUID(), ForeignKey("memory_items.id", ondelete="SET NULL"), nullable=True
|
|
33
|
+
)
|
harness/models/enums.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Enumerations shared across models and schemas (single source of truth)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TrustPolicy(str, enum.Enum):
|
|
9
|
+
open = "open"
|
|
10
|
+
restricted = "restricted"
|
|
11
|
+
local_only = "local_only"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AdapterKind(str, enum.Enum):
|
|
15
|
+
mock = "mock"
|
|
16
|
+
openai_local = "openai_local" # any OpenAI-compatible endpoint, local or remote
|
|
17
|
+
claude_code = "claude_code"
|
|
18
|
+
antigravity = "antigravity" # Google's official `agy` CLI (Gemini successor)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Friendly config aliases for `kind:` in config.yaml. Remote OpenAI-compatible
|
|
22
|
+
# endpoints read more naturally as `kind: openai`; both normalize to `openai_local`,
|
|
23
|
+
# so no DB enum migration is needed and the canonical value stays valid.
|
|
24
|
+
_KIND_ALIASES = {"openai": "openai_local", "openai_compatible": "openai_local"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def adapter_kind_from_config(raw: str) -> AdapterKind:
|
|
28
|
+
"""Resolve a config.yaml ``kind:`` string to an :class:`AdapterKind`, honoring
|
|
29
|
+
friendly aliases (``openai`` / ``openai_compatible`` → ``openai_local``)."""
|
|
30
|
+
return AdapterKind(_KIND_ALIASES.get(raw, raw))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class McpTransport(str, enum.Enum):
|
|
34
|
+
"""How the harness reaches an MCP server.
|
|
35
|
+
|
|
36
|
+
``stdio`` launches a local subprocess and speaks MCP over its stdin/stdout;
|
|
37
|
+
``http`` connects to a remote streamable-HTTP / SSE endpoint."""
|
|
38
|
+
|
|
39
|
+
stdio = "stdio"
|
|
40
|
+
http = "http"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TaskStatus(str, enum.Enum):
|
|
44
|
+
created = "created"
|
|
45
|
+
running = "running"
|
|
46
|
+
done = "done"
|
|
47
|
+
failed = "failed"
|
|
48
|
+
cancelled = "cancelled"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SessionStatus(str, enum.Enum):
|
|
52
|
+
open = "open"
|
|
53
|
+
running = "running"
|
|
54
|
+
completed = "completed"
|
|
55
|
+
failed = "failed"
|
|
56
|
+
archived = "archived"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RunRole(str, enum.Enum):
|
|
60
|
+
primary = "primary"
|
|
61
|
+
draft = "draft"
|
|
62
|
+
verifier = "verifier"
|
|
63
|
+
judge = "judge"
|
|
64
|
+
merger = "merger"
|
|
65
|
+
secondary = "secondary"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RunStatus(str, enum.Enum):
|
|
69
|
+
queued = "queued"
|
|
70
|
+
running = "running"
|
|
71
|
+
streaming = "streaming"
|
|
72
|
+
succeeded = "succeeded"
|
|
73
|
+
failed = "failed"
|
|
74
|
+
cancelled = "cancelled"
|
|
75
|
+
needs_approval = "needs_approval"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class RunEventType(str, enum.Enum):
|
|
79
|
+
token = "token"
|
|
80
|
+
message = "message"
|
|
81
|
+
tool_call = "tool_call"
|
|
82
|
+
tool_result = "tool_result"
|
|
83
|
+
artifact = "artifact"
|
|
84
|
+
status = "status"
|
|
85
|
+
cost = "cost"
|
|
86
|
+
adapter_meta = "adapter_meta"
|
|
87
|
+
error = "error"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ArtifactKind(str, enum.Enum):
|
|
91
|
+
file = "file"
|
|
92
|
+
patch = "patch"
|
|
93
|
+
plan = "plan"
|
|
94
|
+
doc = "doc"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class MemoryScope(str, enum.Enum):
|
|
98
|
+
session = "session"
|
|
99
|
+
workspace = "workspace"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MemoryType(str, enum.Enum):
|
|
103
|
+
fact = "fact"
|
|
104
|
+
decision = "decision"
|
|
105
|
+
preference = "preference"
|
|
106
|
+
architecture = "architecture"
|
|
107
|
+
episodic = "episodic"
|
|
108
|
+
policy = "policy"
|
|
109
|
+
artifact_ref = "artifact_ref"
|
|
110
|
+
summary = "summary"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class PolicyKind(str, enum.Enum):
|
|
114
|
+
routing = "routing"
|
|
115
|
+
approval = "approval"
|
|
116
|
+
egress = "egress"
|
|
117
|
+
fs_allowlist = "fs_allowlist"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ApprovalActionClass(str, enum.Enum):
|
|
121
|
+
high_risk = "high_risk"
|
|
122
|
+
command = "command"
|
|
123
|
+
egress = "egress"
|
|
124
|
+
secret = "secret"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ApprovalStatus(str, enum.Enum):
|
|
128
|
+
pending = "pending"
|
|
129
|
+
approved = "approved"
|
|
130
|
+
denied = "denied"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ComparisonStrategy(str, enum.Enum):
|
|
134
|
+
judge = "judge"
|
|
135
|
+
merge = "merge"
|
|
136
|
+
manual = "manual"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class RoutingMode(str, enum.Enum):
|
|
140
|
+
single = "single"
|
|
141
|
+
fanout = "fanout"
|
|
142
|
+
fallback = "fallback"
|
|
143
|
+
judge_select = "judge_select"
|
|
144
|
+
propose_merge = "propose_merge"
|
|
145
|
+
cheap_first_escalate = "cheap_first_escalate"
|
|
146
|
+
local_first_escalate = "local_first_escalate"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Boolean, Enum, String
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import McpTransport
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class McpServerConfig(Base, TimestampMixin):
|
|
14
|
+
"""A configured MCP (Model Context Protocol) server the harness may expose to
|
|
15
|
+
its agents.
|
|
16
|
+
|
|
17
|
+
Mirrors :class:`~harness.models.provider_config.ProviderConfig`: declared in
|
|
18
|
+
``config.yaml`` (``mcp.servers``), seeded into the DB, and resolvable by name.
|
|
19
|
+
Secrets are referenced via ``secret_ref`` (e.g. ``env:GITHUB_TOKEN``) and never
|
|
20
|
+
stored. ``bind`` scopes the server to workspaces/providers; ``tool_allowlist``
|
|
21
|
+
(empty = all) restricts which of the server's tools are offered.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__tablename__ = "mcp_server_configs"
|
|
25
|
+
|
|
26
|
+
id: Mapped[uuid.UUID] = mapped_column(GUID(), primary_key=True, default=uuid.uuid4)
|
|
27
|
+
# Name is the global lookup key and the tool-namespace segment (mcp__<name>__<tool>),
|
|
28
|
+
# so it must be unique.
|
|
29
|
+
name: Mapped[str] = mapped_column(String(128), index=True, unique=True)
|
|
30
|
+
transport: Mapped[McpTransport] = mapped_column(
|
|
31
|
+
Enum(McpTransport, name="mcp_transport")
|
|
32
|
+
)
|
|
33
|
+
# stdio transport: command + args + env.
|
|
34
|
+
command: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
35
|
+
args: Mapped[list] = mapped_column(json_col(), default=list)
|
|
36
|
+
env: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
37
|
+
# http transport: url + headers.
|
|
38
|
+
url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
39
|
+
headers: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
40
|
+
# Optional bearer/token secret, resolved at use time (never persisted in plaintext).
|
|
41
|
+
secret_ref: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
42
|
+
# Empty list ⇒ all tools the server advertises are allowed.
|
|
43
|
+
tool_allowlist: Mapped[list] = mapped_column(json_col(), default=list)
|
|
44
|
+
# {"workspaces": [...], "providers": [...]} — empty lists ⇒ unrestricted.
|
|
45
|
+
bind: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
46
|
+
# When true the server's tools are considered side-effect-free and remain offered
|
|
47
|
+
# in read-only / plan runs and local_only workspaces.
|
|
48
|
+
read_only: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
49
|
+
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
harness/models/memory.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from sqlalchemy import (
|
|
7
|
+
Boolean,
|
|
8
|
+
DateTime,
|
|
9
|
+
Enum,
|
|
10
|
+
ForeignKey,
|
|
11
|
+
Index,
|
|
12
|
+
Integer,
|
|
13
|
+
String,
|
|
14
|
+
Text,
|
|
15
|
+
text,
|
|
16
|
+
)
|
|
17
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
18
|
+
|
|
19
|
+
from harness.db import Base, TimestampMixin
|
|
20
|
+
from harness.models.enums import MemoryScope, MemoryType
|
|
21
|
+
from harness.models.types import GUID, embedding_col, json_col, str_array
|
|
22
|
+
from harness.settings import get_settings
|
|
23
|
+
|
|
24
|
+
EMBEDDING_DIM = get_settings().embedding_dim
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MemoryItem(Base, TimestampMixin):
|
|
28
|
+
"""Canonical, vendor-neutral memory. ``namespace`` is a composite partition
|
|
29
|
+
(``ws/<slug>[/project][/provider][/session]``) enforcing isolation. Volatile
|
|
30
|
+
items carry a ``ttl_expires_at``; durable workspace items leave it null."""
|
|
31
|
+
|
|
32
|
+
__tablename__ = "memory_items"
|
|
33
|
+
__table_args__ = (
|
|
34
|
+
Index("ix_memory_namespace_scope_type", "namespace", "scope", "type"),
|
|
35
|
+
Index(
|
|
36
|
+
"ix_memory_ttl",
|
|
37
|
+
"ttl_expires_at",
|
|
38
|
+
postgresql_where=text("ttl_expires_at IS NOT NULL"),
|
|
39
|
+
),
|
|
40
|
+
Index("uq_memory_dedup", "namespace", "dedup_hash", unique=True),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
44
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
45
|
+
)
|
|
46
|
+
namespace: Mapped[str] = mapped_column(String(512), index=True)
|
|
47
|
+
scope: Mapped[MemoryScope] = mapped_column(Enum(MemoryScope, name="memory_scope"))
|
|
48
|
+
type: Mapped[MemoryType] = mapped_column(Enum(MemoryType, name="memory_type"))
|
|
49
|
+
content: Mapped[str] = mapped_column(Text)
|
|
50
|
+
tags: Mapped[list[str]] = mapped_column(str_array(), default=list)
|
|
51
|
+
source_run_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
52
|
+
GUID(), ForeignKey("runs.id", ondelete="SET NULL"), nullable=True
|
|
53
|
+
)
|
|
54
|
+
dedup_hash: Mapped[str] = mapped_column(String(64), index=True)
|
|
55
|
+
redacted: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
56
|
+
ttl_expires_at: Mapped[datetime | None] = mapped_column(
|
|
57
|
+
DateTime(timezone=True), nullable=True
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MemoryEmbedding(Base):
|
|
62
|
+
"""Chunked embeddings for a memory item. Kept in a separate table so transcript
|
|
63
|
+
storage, canonical memory, and vectors remain cleanly decoupled."""
|
|
64
|
+
|
|
65
|
+
__tablename__ = "memory_embeddings"
|
|
66
|
+
|
|
67
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
68
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
69
|
+
)
|
|
70
|
+
memory_item_id: Mapped[uuid.UUID] = mapped_column(
|
|
71
|
+
GUID(), ForeignKey("memory_items.id", ondelete="CASCADE"), index=True
|
|
72
|
+
)
|
|
73
|
+
chunk_idx: Mapped[int] = mapped_column(Integer, default=0)
|
|
74
|
+
model: Mapped[str] = mapped_column(String(128))
|
|
75
|
+
embedding: Mapped[list[float]] = mapped_column(embedding_col(EMBEDDING_DIM))
|
|
76
|
+
meta: Mapped[dict] = mapped_column(json_col(), default=dict)
|
harness/models/policy.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Enum, ForeignKey, Integer, String
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import PolicyKind
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Policy(Base, TimestampMixin):
|
|
14
|
+
"""A workspace-scoped policy: routing rules, approval rules, egress rules, or
|
|
15
|
+
filesystem allowlists. ``spec`` is a typed-by-kind JSON document; higher
|
|
16
|
+
``priority`` wins on conflict."""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "policies"
|
|
19
|
+
|
|
20
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
21
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
22
|
+
)
|
|
23
|
+
workspace_id: Mapped[uuid.UUID] = mapped_column(
|
|
24
|
+
GUID(), ForeignKey("workspaces.id", ondelete="CASCADE"), index=True
|
|
25
|
+
)
|
|
26
|
+
name: Mapped[str] = mapped_column(String(128))
|
|
27
|
+
kind: Mapped[PolicyKind] = mapped_column(Enum(PolicyKind, name="policy_kind"))
|
|
28
|
+
spec: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
29
|
+
priority: Mapped[int] = mapped_column(Integer, default=0)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import Boolean, Enum, ForeignKey, String
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import AdapterKind
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProviderConfig(Base, TimestampMixin):
|
|
14
|
+
"""A configured adapter instance. ``secret_ref`` points into env/Docker
|
|
15
|
+
secrets (e.g. ``env:LOCAL_LLM_API_KEY``) — the secret value is never stored."""
|
|
16
|
+
|
|
17
|
+
__tablename__ = "provider_configs"
|
|
18
|
+
|
|
19
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
20
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
21
|
+
)
|
|
22
|
+
workspace_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
23
|
+
GUID(), ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=True
|
|
24
|
+
)
|
|
25
|
+
# Provider name is the global lookup key (adapter test/models/run routing all
|
|
26
|
+
# resolve by name), so it must be unique — otherwise duplicate rows make
|
|
27
|
+
# single-row lookups raise MultipleResultsFound. See migration 0003.
|
|
28
|
+
name: Mapped[str] = mapped_column(String(128), index=True, unique=True)
|
|
29
|
+
kind: Mapped[AdapterKind] = mapped_column(Enum(AdapterKind, name="adapter_kind"))
|
|
30
|
+
base_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
31
|
+
model: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
32
|
+
secret_ref: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
33
|
+
params: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
34
|
+
capabilities: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
35
|
+
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
harness/models/run.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import DateTime, Enum, ForeignKey, Index, String, Text
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
from harness.db import Base, TimestampMixin
|
|
9
|
+
from harness.models.enums import AdapterKind, RunRole, RunStatus
|
|
10
|
+
from harness.models.types import GUID, json_col
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Run(Base, TimestampMixin):
|
|
14
|
+
"""A single provider execution within a session. The unit of idempotency for
|
|
15
|
+
workers (``idempotency_key``) and the node identity in the orchestration graph."""
|
|
16
|
+
|
|
17
|
+
__tablename__ = "runs"
|
|
18
|
+
__table_args__ = (
|
|
19
|
+
Index("ix_runs_session_status", "session_id", "status"),
|
|
20
|
+
Index("ix_runs_status_started", "status", "started_at"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
24
|
+
GUID(), primary_key=True, default=uuid.uuid4
|
|
25
|
+
)
|
|
26
|
+
session_id: Mapped[uuid.UUID] = mapped_column(
|
|
27
|
+
GUID(), ForeignKey("sessions.id", ondelete="CASCADE"), index=True
|
|
28
|
+
)
|
|
29
|
+
provider_config_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
30
|
+
GUID(), ForeignKey("provider_configs.id", ondelete="SET NULL"), nullable=True
|
|
31
|
+
)
|
|
32
|
+
adapter_kind: Mapped[AdapterKind] = mapped_column(Enum(AdapterKind, name="adapter_kind"))
|
|
33
|
+
role: Mapped[RunRole] = mapped_column(
|
|
34
|
+
Enum(RunRole, name="run_role"), default=RunRole.primary
|
|
35
|
+
)
|
|
36
|
+
status: Mapped[RunStatus] = mapped_column(
|
|
37
|
+
Enum(RunStatus, name="run_status"), default=RunStatus.queued
|
|
38
|
+
)
|
|
39
|
+
graph_node_id: Mapped[str] = mapped_column(String(128), default="root")
|
|
40
|
+
idempotency_key: Mapped[str] = mapped_column(String(256), unique=True)
|
|
41
|
+
started_at: Mapped[object | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
42
|
+
finished_at: Mapped[object | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
43
|
+
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
44
|
+
cost: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
45
|
+
context_meta: Mapped[dict] = mapped_column(json_col(), default=dict)
|
|
46
|
+
final_output: Mapped[str | None] = mapped_column(Text, nullable=True)
|