Skip to content

Commit 5c74a7c

Browse files
johnteeecursoragent
andcommitted
Wire multi-sig relay URLs from workspace config and document release gate.
Load peer_relay_urls and local_relay_base_url from .teaagent/config.json, add WAN deployment guide and config template, and record Dependabot #10 resolution steps with full pre-commit verification notes. Constraint: Dependabot dismiss requires GitHub UI or authenticated gh API. Tested: TEAAGENT_PRECOMMIT_FULL=1 pre-commit (all hooks passed); uv run pytest -q (2641 passed). Confidence: high Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bf86d96 commit 5c74a7c

9 files changed

Lines changed: 197 additions & 3 deletions

File tree

SECURITY.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,23 @@ uv export --format requirements-txt -o /tmp/req.txt
218218
uv run pip-audit -r /tmp/req.txt
219219
```
220220

221+
**Dependabot alert #10 (CVE-2026-23949):** Resolved in-tree as of 2026-05-29 — `uv.lock`
222+
pins `jaraco-context` **6.1.2** (>= 6.1.0). Dismiss in GitHub **Security → Dependabot →
223+
alert #10 → Close as fixed** with reason: *fix already on default branch*.
224+
225+
```bash
226+
# Verify locally
227+
python3 -c "import importlib.metadata as m; print(m.version('jaraco.context'))"
228+
teaagent selftest --root .
229+
```
230+
231+
If `gh` is installed and authenticated:
232+
233+
```bash
234+
gh api -X PATCH repos/TeaEntityLab/teaAgent/dependabot/alerts/10 \
235+
-f state=fixed -f dismissed_reason=fix_started
236+
```
237+
221238
If GitHub Dependabot still reports an open alert while `pip-audit` is clean, reconcile
222239
in the repository **Security → Dependabot** UI (dismiss as fixed/upstream, or refresh
223240
the lockfile after the advisory maps to a patched release).

docs/dependabot-alert-10.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Dependabot alert #10CVE-2026-23949 (`jaraco.context`)
2+
3+
## Status: fixed on `main` (dismiss in GitHub UI)
4+
5+
| Check | Result |
6+
|-------|--------|
7+
| `uv.lock` | `jaraco-context` **6.1.2** |
8+
| Constraint | `pyproject.toml``[tool.uv] constraint-dependencies``>=6.1.0` |
9+
| Selftest | `teaagent selftest``jaraco_context.ok: true` when installed |
10+
11+
## Dismiss (maintainer)
12+
13+
GitHub → **Security****Dependabot** → alert **#10****Close as fixed**
14+
15+
Reason: *Vulnerability is patched in default branch (jaraco-context 6.1.2).*
16+
17+
Or with authenticated `gh`:
18+
19+
```bash
20+
gh api -X PATCH repos/TeaEntityLab/teaAgent/dependabot/alerts/10 \
21+
-f state=fixed \
22+
-f dismissed_reason=fix_started \
23+
-f dismissed_comment='uv.lock pins jaraco-context 6.1.2 (>=6.1.0)'
24+
```

docs/http-surface-auth.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,12 @@ teaagent sync signature-relay submit \
111111
--request-id <id> --peer-id peer-1 --signature "<ssh-blob>"
112112
```
113113

114-
Configure `MultiSigQuorumConfig.local_relay_base_url` and `peer_relay_urls` in policy,
115-
or set `TEAAGENT_SIGNATURE_RELAY_TOKEN` / `TEAAGENT_RELAY_TOKEN` for HTTP client auth.
114+
Set relay URLs in `.teaagent/config.json` under `multi_sig` (loaded automatically by
115+
`ApprovalPolicy` / `chat_agent`). See
116+
[multi-sig-wan-deployment.md](multi-sig-wan-deployment.md) and
117+
[templates/multi-sig/config.json.example](../templates/multi-sig/config.json.example).
118+
119+
Set `TEAAGENT_SIGNATURE_RELAY_TOKEN` / `TEAAGENT_RELAY_TOKEN` for HTTP client auth.
116120

117121
API:
118122

docs/multi-sig-wan-deployment.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# WAN multi-sig deployment
2+
3+
Configure HTTP signature relay URLs in workspace policy so `ApprovalPolicy` loads
4+
them automatically from `.teaagent/config.json`.
5+
6+
## 1. Collector (requester workspace)
7+
8+
Copy [templates/multi-sig/config.json.example](../templates/multi-sig/config.json.example)
9+
to `.teaagent/config.json` and set:
10+
11+
- `local_relay_base_url` — public URL of **this** workspace's signature relay
12+
- `peer_relay_urls` — map each `peer_agent_ids` entry to that peer's relay base URL
13+
14+
Start the collector relay (behind TLS termination in production):
15+
16+
```bash
17+
teaagent sync signature-relay serve \
18+
--host 127.0.0.1 --port 8791 \
19+
--api-token-file .teaagent/relay-tokens.json
20+
```
21+
22+
Export for peers signing remotely:
23+
24+
```bash
25+
export TEAAGENT_SIGNATURE_RELAY_TOKEN="<token from relay-tokens.json>"
26+
```
27+
28+
## 2. Peer workspaces
29+
30+
Each peer runs its own relay and receives approval requests via HTTP POST from the
31+
requester. After review, peers submit signatures:
32+
33+
```bash
34+
teaagent sync signature-relay submit \
35+
--relay-url https://collector.example:8791 \
36+
--request-id "<request_id>" \
37+
--peer-id peer-a \
38+
--signature "<ssh-signature-blob>"
39+
```
40+
41+
## 3. Verification
42+
43+
- `uv.lock` pins `jaraco-context` 6.1.2+ (CVE-2026-23949 / Dependabot #10)
44+
- `teaagent selftest` reports `jaraco_context.ok: true` when the package is installed
45+
- Run `TEAAGENT_PRECOMMIT_FULL=1 pre-commit run --all-files` before release tags
46+
47+
## Related
48+
49+
- [http-surface-auth.md](http-surface-auth.md) — bearer tokens and relay headers
50+
- [ADR 0008](adr/0008-p4-strategic-posture.md) — strategic posture

docs/plans/remediation-roadmap.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ Principle: **smallest verifiable step** per phase; no big-bang refactors.
7171

7272
Full SQLite audit/run migration remains **P4+ / backlog** — not started.
7373

74+
**Release gate (2026-05-29):** `TEAAGENT_PRECOMMIT_FULL=1 pre-commit run --all-files` green;
75+
`uv run pytest -q` → 2641 passed. Dependabot #10: dismiss as *fixed* (`jaraco-context` 6.1.2 in `uv.lock`).
76+
7477
---
7578

7679
## Cost controls (operational)

teaagent/chat_agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
LLMRequest,
3030
)
3131
from teaagent.memory import MemoryCatalog, memory_entries_to_prompt
32-
from teaagent.policy import ApprovalPolicy, PermissionMode
32+
from teaagent.policy import ApprovalPolicy, MultiSigQuorumConfig, PermissionMode
3333
from teaagent.prompt import (
3434
DECISION_JSON_SCHEMA,
3535
assemble_agent_prompt,
@@ -478,6 +478,7 @@ def run_chat_agent(
478478
permission_mode=config.permission_mode,
479479
approval_store=approval_store,
480480
approval_origin_run_id=context_extra.get('resumed_from') or run_id,
481+
multi_sig_config=MultiSigQuorumConfig.from_workspace_config(config.root),
481482
),
482483
approval_handler=config.approval_handler,
483484
compactor=ContextCompactor(memory_keys=('task_spec', 'memories')),

teaagent/policy.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import warnings
1313
from dataclasses import dataclass, field
1414
from enum import Enum
15+
from pathlib import Path
1516
from typing import TYPE_CHECKING, Any, Optional
1617

1718
from teaagent.errors import ToolPermissionError
@@ -110,6 +111,48 @@ class MultiSigQuorumConfig:
110111
) # Patterns triggering multi-sig
111112
timeout_seconds: int = 300 # Timeout for collecting signatures
112113

114+
@classmethod
115+
def from_workspace_config(cls, root: str | Path) -> MultiSigQuorumConfig:
116+
"""Load ``multi_sig`` section from ``<root>/.teaagent/config.json``."""
117+
from teaagent.config_loader import load_workspace_config
118+
119+
section = load_workspace_config(root).get('multi_sig')
120+
if not isinstance(section, dict):
121+
return cls()
122+
123+
peer_ids = section.get('peer_agent_ids') or []
124+
if not isinstance(peer_ids, list):
125+
peer_ids = []
126+
127+
patterns = section.get('high_risk_patterns') or []
128+
if not isinstance(patterns, list):
129+
patterns = []
130+
131+
relay_urls = section.get('peer_relay_urls') or {}
132+
if not isinstance(relay_urls, dict):
133+
relay_urls = {}
134+
135+
public_keys = section.get('peer_public_keys') or {}
136+
if not isinstance(public_keys, dict):
137+
public_keys = {}
138+
139+
local_relay = section.get('local_relay_base_url')
140+
local_relay_url = str(local_relay).strip() if local_relay else None
141+
if local_relay_url == '':
142+
local_relay_url = None
143+
144+
return cls(
145+
enabled=bool(section.get('enabled', False)),
146+
required_approvals=int(section.get('required_approvals', 2)),
147+
peer_agent_ids=[str(item) for item in peer_ids],
148+
peer_public_keys={str(k): str(v) for k, v in public_keys.items()},
149+
peer_relay_urls={str(k): str(v).rstrip('/') for k, v in relay_urls.items()},
150+
local_relay_base_url=local_relay_url,
151+
allow_dev_signatures=bool(section.get('allow_dev_signatures', False)),
152+
high_risk_patterns=[str(item) for item in patterns],
153+
timeout_seconds=int(section.get('timeout_seconds', 300)),
154+
)
155+
113156

114157
@dataclass(frozen=True)
115158
class ApprovalRequest:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"multi_sig": {
3+
"enabled": true,
4+
"required_approvals": 2,
5+
"peer_agent_ids": ["peer-a", "peer-b"],
6+
"peer_public_keys": {
7+
"peer-a": "ssh-ed25519 AAAA... peer-a@host",
8+
"peer-b": "ssh-ed25519 AAAA... peer-b@host"
9+
},
10+
"peer_relay_urls": {
11+
"peer-a": "https://peer-a.example:8791",
12+
"peer-b": "https://peer-b.example:8791"
13+
},
14+
"local_relay_base_url": "https://collector.example:8791",
15+
"timeout_seconds": 300,
16+
"high_risk_patterns": ["/prod", "/production"],
17+
"allow_dev_signatures": false
18+
}
19+
}

tests/test_policy.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,39 @@ def test_multi_sig_config_defaults(self) -> None:
767767
self.assertEqual(config.high_risk_patterns, [])
768768
self.assertEqual(config.timeout_seconds, 300)
769769

770+
def test_multi_sig_config_from_workspace_json(self) -> None:
771+
import json
772+
import tempfile
773+
from pathlib import Path
774+
775+
with tempfile.TemporaryDirectory() as tmpdir:
776+
root = Path(tmpdir)
777+
(root / '.teaagent').mkdir()
778+
(root / '.teaagent' / 'config.json').write_text(
779+
json.dumps(
780+
{
781+
'multi_sig': {
782+
'enabled': True,
783+
'required_approvals': 2,
784+
'peer_agent_ids': ['peer-a'],
785+
'peer_relay_urls': {
786+
'peer-a': 'https://peer-a.example:8791'
787+
},
788+
'local_relay_base_url': 'https://collector.example:8791',
789+
}
790+
}
791+
),
792+
encoding='utf-8',
793+
)
794+
config = MultiSigQuorumConfig.from_workspace_config(root)
795+
self.assertTrue(config.enabled)
796+
self.assertEqual(
797+
config.peer_relay_urls['peer-a'], 'https://peer-a.example:8791'
798+
)
799+
self.assertEqual(
800+
config.local_relay_base_url, 'https://collector.example:8791'
801+
)
802+
770803
def test_multi_sig_config_custom(self) -> None:
771804
"""Verify custom multi-sig configuration."""
772805
config = MultiSigQuorumConfig(

0 commit comments

Comments
 (0)