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.
Files changed (156) hide show
  1. ags_cli-0.1.0.dist-info/METADATA +332 -0
  2. ags_cli-0.1.0.dist-info/RECORD +156 -0
  3. ags_cli-0.1.0.dist-info/WHEEL +5 -0
  4. ags_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. ags_cli-0.1.0.dist-info/top_level.txt +2 -0
  6. cli/__init__.py +0 -0
  7. cli/__main__.py +4 -0
  8. cli/client.py +145 -0
  9. cli/commands/__init__.py +0 -0
  10. cli/commands/adapter.py +33 -0
  11. cli/commands/artifact.py +21 -0
  12. cli/commands/ask.py +212 -0
  13. cli/commands/chat.py +649 -0
  14. cli/commands/diff.py +49 -0
  15. cli/commands/doctor.py +93 -0
  16. cli/commands/gui.py +45 -0
  17. cli/commands/init.py +20 -0
  18. cli/commands/mcp.py +50 -0
  19. cli/commands/memory.py +65 -0
  20. cli/commands/model.py +73 -0
  21. cli/commands/policy.py +52 -0
  22. cli/commands/project.py +122 -0
  23. cli/commands/quota.py +107 -0
  24. cli/commands/run.py +84 -0
  25. cli/commands/serve.py +171 -0
  26. cli/commands/service.py +236 -0
  27. cli/commands/session.py +219 -0
  28. cli/commands/stats.py +96 -0
  29. cli/commands/status.py +36 -0
  30. cli/commands/task.py +37 -0
  31. cli/commands/usage.py +86 -0
  32. cli/local.py +84 -0
  33. cli/main.py +149 -0
  34. harness/__init__.py +4 -0
  35. harness/adapters/__init__.py +26 -0
  36. harness/adapters/antigravity.py +301 -0
  37. harness/adapters/claude_code.py +522 -0
  38. harness/adapters/local_openai.py +534 -0
  39. harness/adapters/mock.py +112 -0
  40. harness/adapters/probe.py +65 -0
  41. harness/adapters/registry.py +56 -0
  42. harness/adapters/warm_pool.py +255 -0
  43. harness/api/__init__.py +0 -0
  44. harness/api/router.py +38 -0
  45. harness/api/v1/__init__.py +0 -0
  46. harness/api/v1/adapters.py +105 -0
  47. harness/api/v1/approvals.py +173 -0
  48. harness/api/v1/artifacts.py +44 -0
  49. harness/api/v1/attachments.py +48 -0
  50. harness/api/v1/doctor.py +22 -0
  51. harness/api/v1/health.py +37 -0
  52. harness/api/v1/mcp.py +131 -0
  53. harness/api/v1/memory.py +80 -0
  54. harness/api/v1/policies.py +49 -0
  55. harness/api/v1/project.py +302 -0
  56. harness/api/v1/runs.py +98 -0
  57. harness/api/v1/sessions.py +248 -0
  58. harness/api/v1/stream.py +110 -0
  59. harness/api/v1/tasks.py +30 -0
  60. harness/api/v1/usage.py +58 -0
  61. harness/bootstrap.py +216 -0
  62. harness/db.py +164 -0
  63. harness/deps.py +58 -0
  64. harness/eventbus/__init__.py +20 -0
  65. harness/eventbus/inprocess_bus.py +85 -0
  66. harness/main.py +144 -0
  67. harness/mcp/__init__.py +22 -0
  68. harness/mcp/client.py +139 -0
  69. harness/mcp/config_gen.py +77 -0
  70. harness/mcp/registry.py +156 -0
  71. harness/mcp/tools.py +81 -0
  72. harness/memory/__init__.py +13 -0
  73. harness/memory/context_budget.py +116 -0
  74. harness/memory/embed.py +217 -0
  75. harness/memory/extract.py +74 -0
  76. harness/memory/ingest.py +106 -0
  77. harness/memory/namespaces.py +57 -0
  78. harness/memory/ranking.py +31 -0
  79. harness/memory/redact.py +37 -0
  80. harness/memory/service.py +320 -0
  81. harness/memory/sqlite_vec_backend.py +266 -0
  82. harness/memory/summarize.py +68 -0
  83. harness/models/__init__.py +35 -0
  84. harness/models/adapter_health.py +27 -0
  85. harness/models/approval.py +37 -0
  86. harness/models/artifact.py +35 -0
  87. harness/models/audit_log.py +33 -0
  88. harness/models/comparison.py +33 -0
  89. harness/models/enums.py +146 -0
  90. harness/models/mcp_server_config.py +49 -0
  91. harness/models/memory.py +76 -0
  92. harness/models/policy.py +29 -0
  93. harness/models/provider_config.py +35 -0
  94. harness/models/run.py +46 -0
  95. harness/models/run_event.py +35 -0
  96. harness/models/session.py +50 -0
  97. harness/models/task.py +30 -0
  98. harness/models/types.py +63 -0
  99. harness/models/workspace.py +27 -0
  100. harness/observability/__init__.py +4 -0
  101. harness/observability/audit.py +88 -0
  102. harness/observability/logging.py +47 -0
  103. harness/observability/metrics.py +43 -0
  104. harness/observability/tracing.py +38 -0
  105. harness/orchestrator/__init__.py +19 -0
  106. harness/orchestrator/engine.py +821 -0
  107. harness/orchestrator/handoff.py +33 -0
  108. harness/orchestrator/lifecycle.py +69 -0
  109. harness/orchestrator/native_resume.py +93 -0
  110. harness/orchestrator/policies.py +101 -0
  111. harness/orchestrator/reconcile.py +39 -0
  112. harness/orchestrator/run_graph.py +62 -0
  113. harness/orchestrator/snapshot.py +49 -0
  114. harness/schemas/__init__.py +1 -0
  115. harness/schemas/adapter.py +26 -0
  116. harness/schemas/approval.py +25 -0
  117. harness/schemas/artifact.py +17 -0
  118. harness/schemas/common.py +20 -0
  119. harness/schemas/memory.py +50 -0
  120. harness/schemas/policy.py +39 -0
  121. harness/schemas/run.py +116 -0
  122. harness/schemas/session.py +93 -0
  123. harness/schemas/task.py +24 -0
  124. harness/security/__init__.py +5 -0
  125. harness/security/approval_policy.py +107 -0
  126. harness/security/auth.py +106 -0
  127. harness/security/permissions.py +59 -0
  128. harness/security/risk.py +59 -0
  129. harness/security/secrets.py +49 -0
  130. harness/services/__init__.py +1 -0
  131. harness/services/adapters_health.py +212 -0
  132. harness/services/approvals.py +84 -0
  133. harness/services/artifacts.py +78 -0
  134. harness/services/attachments.py +228 -0
  135. harness/services/comparison.py +345 -0
  136. harness/services/doctor.py +156 -0
  137. harness/services/files.py +287 -0
  138. harness/services/mcp_servers.py +179 -0
  139. harness/services/project_git.py +101 -0
  140. harness/services/retention.py +97 -0
  141. harness/services/runs.py +155 -0
  142. harness/services/runs_diff.py +201 -0
  143. harness/services/sessions.py +242 -0
  144. harness/services/ssh.py +300 -0
  145. harness/services/stats.py +132 -0
  146. harness/services/tasks.py +41 -0
  147. harness/services/workspaces.py +184 -0
  148. harness/services/worktrees.py +439 -0
  149. harness/settings.py +186 -0
  150. harness/skills/__init__.py +11 -0
  151. harness/skills/manager.py +141 -0
  152. harness/tools/__init__.py +1 -0
  153. harness/tools/fs_tools.py +295 -0
  154. harness/workers/__init__.py +0 -0
  155. harness/workers/dispatch.py +26 -0
  156. 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
+ )
@@ -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)
@@ -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)
@@ -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)