collab-runtime 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {collab_runtime-0.3.0/collab_runtime.egg-info → collab_runtime-0.4.0}/PKG-INFO +33 -2
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/README.md +29 -6
- collab_runtime-0.4.0/collab/agent_identity.py +293 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/dashboard/index.html +152 -41
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/dashboard_server.py +87 -8
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/live_locks_watcher.py +163 -91
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/lock_client.py +242 -118
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/main.py +75 -7
- {collab_runtime-0.3.0 → collab_runtime-0.4.0/collab_runtime.egg-info}/PKG-INFO +33 -2
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab_runtime.egg-info/SOURCES.txt +1 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/docs/pypi/README.md +32 -1
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/pyproject.toml +1 -1
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/LICENSE +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/__init__.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/__main__.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/errors.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/logging_config.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/platform_probe.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/safe_subprocess.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab/subprocess_bridge.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab_runtime.egg-info/dependency_links.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab_runtime.egg-info/entry_points.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab_runtime.egg-info/requires.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/collab_runtime.egg-info/top_level.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: collab-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Collaborative file locking runtime
|
|
5
5
|
Author-email: KirilMT <kiril.mt95@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -68,7 +68,7 @@ pip install collab-runtime
|
|
|
68
68
|
For a minimum-version install (ensures you get the latest compatible release):
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
pip install "collab-runtime>=0.
|
|
71
|
+
pip install "collab-runtime>=0.3.2"
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
---
|
|
@@ -91,10 +91,37 @@ SUPABASE_ANON_KEY=your_anon_key_here
|
|
|
91
91
|
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key # Required for force-release
|
|
92
92
|
DEVELOPER_ID=your_name # Optional, defaults to git user.name
|
|
93
93
|
LOCK_STRICT=0 # 1 = block on lock errors, 0 = warn only
|
|
94
|
+
COLLAB_AGENT_ID=agent-my-task # Optional: unique id per AI agent session
|
|
95
|
+
COLLAB_AGENT_LABEL=refactor-auth # Optional display label
|
|
96
|
+
COLLAB_AGENT_MODE=1 # Auto-generate/persist agent id when unset
|
|
94
97
|
```
|
|
95
98
|
|
|
96
99
|
> **Keep `SUPABASE_SERVICE_ROLE_KEY` private — never commit it to version control.**
|
|
97
100
|
|
|
101
|
+
### Multi-agent usage (same GitHub user, multiple AI agents)
|
|
102
|
+
|
|
103
|
+
When one developer runs several AI agents in the same repo, give each agent its own identity so
|
|
104
|
+
locks do not collide (effective owner is `developer_id` + `agent_id`):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Agent A
|
|
108
|
+
export COLLAB_AGENT_ID=agent-refactor-auth
|
|
109
|
+
collab whoami
|
|
110
|
+
collab acquire src/auth.py --reason "Refactor auth"
|
|
111
|
+
|
|
112
|
+
# Agent B (different id) — conflicts with A on the same file
|
|
113
|
+
export COLLAB_AGENT_ID=agent-fix-tests
|
|
114
|
+
collab acquire src/auth.py
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
CLI flags `--agent-id` and `--agent-label` override env vars. Use `collab active --mine` to list
|
|
118
|
+
only locks held by the current human + agent pair.
|
|
119
|
+
|
|
120
|
+
**Existing Supabase projects:** apply the `agent_id` / `agent_label` columns and updated
|
|
121
|
+
`acquire_lock` function from
|
|
122
|
+
[`supabase/schema.sql`](https://github.com/KirilMT/collab/blob/main/supabase/schema.sql) in the SQL
|
|
123
|
+
Editor (fresh installs already include them).
|
|
124
|
+
|
|
98
125
|
### 3 — Verify Connection
|
|
99
126
|
|
|
100
127
|
```bash
|
|
@@ -108,8 +135,12 @@ If connected, this lists all currently active locks (empty on a fresh setup).
|
|
|
108
135
|
## CLI Reference
|
|
109
136
|
|
|
110
137
|
```bash
|
|
138
|
+
# Show resolved developer and agent identity
|
|
139
|
+
collab whoami
|
|
140
|
+
|
|
111
141
|
# Show all active locks across the team
|
|
112
142
|
collab active
|
|
143
|
+
collab active --mine
|
|
113
144
|
|
|
114
145
|
# Lock a file before editing
|
|
115
146
|
collab acquire path/to/file.py --reason "Implementing feature X"
|
|
@@ -74,15 +74,38 @@ The setup script automatically:
|
|
|
74
74
|
|
|
75
75
|
After setup, verify your `.env` at the project root has these values:
|
|
76
76
|
|
|
77
|
-
| Variable | Description
|
|
78
|
-
| --------------------------- |
|
|
79
|
-
| `SUPABASE_URL` | Your Supabase project URL (from Project Settings → API)
|
|
80
|
-
| `SUPABASE_ANON_KEY` | Anonymous/public key (from Project Settings → API)
|
|
81
|
-
| `SUPABASE_SERVICE_ROLE_KEY` | Service role key (**required** for dashboard force-release)
|
|
82
|
-
| `LOCK_STRICT` | If `1`, git hooks block on lock errors. Default `0` (warn only)
|
|
77
|
+
| Variable | Description |
|
|
78
|
+
| --------------------------- | ---------------------------------------------------------------- |
|
|
79
|
+
| `SUPABASE_URL` | Your Supabase project URL (from Project Settings → API) |
|
|
80
|
+
| `SUPABASE_ANON_KEY` | Anonymous/public key (from Project Settings → API) |
|
|
81
|
+
| `SUPABASE_SERVICE_ROLE_KEY` | Service role key (**required** for dashboard force-release) |
|
|
82
|
+
| `LOCK_STRICT` | If `1`, git hooks block on lock errors. Default `0` (warn only) |
|
|
83
|
+
| `COLLAB_AGENT_ID` | Optional stable id for an AI agent session (multi-agent locking) |
|
|
84
|
+
| `COLLAB_AGENT_LABEL` | Optional display label (e.g. `refactor-auth`) |
|
|
85
|
+
| `COLLAB_AGENT_MODE` | Set to `1` to auto-generate/persist an agent id when unset |
|
|
83
86
|
|
|
84
87
|
> **Important:** `SUPABASE_SERVICE_ROLE_KEY` is needed for the dashboard's Force Release button. Without it, only your own locks can be released.
|
|
85
88
|
|
|
89
|
+
### Multi-agent usage (same GitHub user, multiple AI agents)
|
|
90
|
+
|
|
91
|
+
When one developer runs several AI agents in the same repo, give each agent its own identity so
|
|
92
|
+
locks do not collide:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Terminal / agent A
|
|
96
|
+
set COLLAB_AGENT_ID=agent-refactor-auth
|
|
97
|
+
collab whoami
|
|
98
|
+
collab acquire src/auth.py --reason "Refactor auth"
|
|
99
|
+
|
|
100
|
+
# Terminal / agent B (different id)
|
|
101
|
+
set COLLAB_AGENT_ID=agent-fix-tests
|
|
102
|
+
collab acquire src/auth.py # conflict — locked by agent-refactor-auth
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For existing Supabase projects, re-run the `acquire_lock` function and add the `agent_id` /
|
|
106
|
+
`agent_label` columns from the updated [supabase/schema.sql](supabase/schema.sql) (fresh installs
|
|
107
|
+
already include them).
|
|
108
|
+
|
|
86
109
|
### 4. Verify Setup
|
|
87
110
|
|
|
88
111
|
After setup, verify the connection:
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Agent identity resolution for multi-agent collaborative locking.
|
|
2
|
+
|
|
3
|
+
When one GitHub user runs multiple AI agents in the same repository, each agent needs a
|
|
4
|
+
stable, unique identity layered on top of the human ``developer_id``. This module
|
|
5
|
+
resolves ``agent_id`` / ``agent_label`` and provides helpers for lock ownership
|
|
6
|
+
comparisons.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
_AGENT_ID_FILE = ".agent_id"
|
|
18
|
+
_AGENT_LABEL_FILE = ".agent_label"
|
|
19
|
+
_AGENT_ID_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$")
|
|
20
|
+
|
|
21
|
+
# Environment variables that imply an AI agent runtime (not exhaustive).
|
|
22
|
+
_AGENT_RUNTIME_MARKERS: tuple[tuple[str, str], ...] = (
|
|
23
|
+
("CURSOR_TRACE_ID", "cursor"),
|
|
24
|
+
("CURSOR_SESSION_ID", "cursor"),
|
|
25
|
+
("CURSOR_AGENT", "cursor"),
|
|
26
|
+
("COMPOSER_SESSION_ID", "composer"),
|
|
27
|
+
("CLAUDE_CODE", "claude-code"),
|
|
28
|
+
("CLAUDE_CODE_SESSION", "claude-code"),
|
|
29
|
+
("GITHUB_COPILOT_AGENT_ID", "copilot"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_clean_env(name: str) -> Optional[str]:
|
|
34
|
+
raw = os.getenv(name)
|
|
35
|
+
if raw is None:
|
|
36
|
+
return None
|
|
37
|
+
val = raw.strip()
|
|
38
|
+
if not val or val.startswith("#"):
|
|
39
|
+
return None
|
|
40
|
+
if "#" in val:
|
|
41
|
+
val = val.split("#", 1)[0].strip()
|
|
42
|
+
return val or None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_truthy_env(name: str) -> bool:
|
|
46
|
+
raw = _read_clean_env(name)
|
|
47
|
+
if raw is None:
|
|
48
|
+
return False
|
|
49
|
+
return raw.lower() in {"1", "true", "yes", "on"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def detect_agent_runtime_label() -> Optional[str]:
|
|
53
|
+
"""Return a friendly runtime label when known agent env markers are present."""
|
|
54
|
+
for env_name, label in _AGENT_RUNTIME_MARKERS:
|
|
55
|
+
if _read_clean_env(env_name):
|
|
56
|
+
return label
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_agent_mode_requested() -> bool:
|
|
61
|
+
"""Return True when agent identity should be active for this process."""
|
|
62
|
+
if _read_clean_env("COLLAB_AGENT_ID"):
|
|
63
|
+
return True
|
|
64
|
+
if _is_truthy_env("COLLAB_AGENT_MODE"):
|
|
65
|
+
return True
|
|
66
|
+
if detect_agent_runtime_label():
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _sanitize_agent_id(value: str) -> Optional[str]:
|
|
72
|
+
candidate = value.strip()
|
|
73
|
+
if not candidate or not _AGENT_ID_PATTERN.match(candidate):
|
|
74
|
+
return None
|
|
75
|
+
return candidate
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _agent_id_file(state_dir: str) -> str:
|
|
79
|
+
return os.path.join(state_dir, _AGENT_ID_FILE)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_persisted_agent_id(state_dir: str) -> Optional[str]:
|
|
83
|
+
"""Load a previously persisted agent id from the collab state directory."""
|
|
84
|
+
path = _agent_id_file(state_dir)
|
|
85
|
+
try:
|
|
86
|
+
if not os.path.isfile(path):
|
|
87
|
+
return None
|
|
88
|
+
with open(path, encoding="utf-8") as fh:
|
|
89
|
+
raw = fh.read().strip()
|
|
90
|
+
return _sanitize_agent_id(raw) if raw else None
|
|
91
|
+
except OSError:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def persist_agent_id(state_dir: str, agent_id: str) -> None:
|
|
96
|
+
"""Persist agent id so subsequent invocations in this state dir reuse it."""
|
|
97
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
98
|
+
path = _agent_id_file(state_dir)
|
|
99
|
+
tmp = f"{path}.tmp"
|
|
100
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
101
|
+
fh.write(agent_id)
|
|
102
|
+
fh.flush()
|
|
103
|
+
try:
|
|
104
|
+
os.fsync(fh.fileno())
|
|
105
|
+
except OSError:
|
|
106
|
+
pass
|
|
107
|
+
os.replace(tmp, path)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def generate_agent_id() -> str:
|
|
111
|
+
"""Generate a stable-format unique agent id."""
|
|
112
|
+
return f"agent-{uuid.uuid4().hex[:8]}"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def resolve_agent_id(
|
|
116
|
+
state_dir: str,
|
|
117
|
+
*,
|
|
118
|
+
explicit_agent_id: Optional[str] = None,
|
|
119
|
+
agent_mode: Optional[bool] = None,
|
|
120
|
+
) -> Optional[str]:
|
|
121
|
+
"""Resolve agent id using precedence: explicit env/arg → persisted → generated.
|
|
122
|
+
|
|
123
|
+
Returns ``None`` when agent mode is off (human-only locking).
|
|
124
|
+
"""
|
|
125
|
+
mode = is_agent_mode_requested() if agent_mode is None else agent_mode
|
|
126
|
+
if explicit_agent_id or _read_clean_env("COLLAB_AGENT_ID"):
|
|
127
|
+
mode = True
|
|
128
|
+
if not mode:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
for candidate in (
|
|
132
|
+
explicit_agent_id,
|
|
133
|
+
_read_clean_env("COLLAB_AGENT_ID"),
|
|
134
|
+
):
|
|
135
|
+
if candidate:
|
|
136
|
+
sanitized = _sanitize_agent_id(candidate)
|
|
137
|
+
if sanitized:
|
|
138
|
+
persist_agent_id(state_dir, sanitized)
|
|
139
|
+
return sanitized
|
|
140
|
+
|
|
141
|
+
persisted = load_persisted_agent_id(state_dir)
|
|
142
|
+
if persisted:
|
|
143
|
+
return persisted
|
|
144
|
+
|
|
145
|
+
generated = generate_agent_id()
|
|
146
|
+
persist_agent_id(state_dir, generated)
|
|
147
|
+
return generated
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def resolve_agent_label(
|
|
151
|
+
*,
|
|
152
|
+
explicit_label: Optional[str] = None,
|
|
153
|
+
runtime_label: Optional[str] = None,
|
|
154
|
+
) -> Optional[str]:
|
|
155
|
+
"""Resolve optional human-readable agent label."""
|
|
156
|
+
for candidate in (
|
|
157
|
+
explicit_label,
|
|
158
|
+
_read_clean_env("COLLAB_AGENT_LABEL"),
|
|
159
|
+
runtime_label,
|
|
160
|
+
detect_agent_runtime_label(),
|
|
161
|
+
):
|
|
162
|
+
if candidate:
|
|
163
|
+
val = candidate.strip()
|
|
164
|
+
if val:
|
|
165
|
+
return val[:256]
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def agent_ids_match(
|
|
170
|
+
lock_agent_id: Optional[str],
|
|
171
|
+
client_agent_id: Optional[str],
|
|
172
|
+
) -> bool:
|
|
173
|
+
"""Return True when two agent_id values represent the same lock owner."""
|
|
174
|
+
return (lock_agent_id or None) == (client_agent_id or None)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def lock_owned_by_client(
|
|
178
|
+
lock: dict[str, Any],
|
|
179
|
+
developer_id: str,
|
|
180
|
+
agent_id: Optional[str],
|
|
181
|
+
) -> bool:
|
|
182
|
+
"""Return True when *lock* belongs to the given human + agent pair."""
|
|
183
|
+
if lock.get("developer_id") != developer_id:
|
|
184
|
+
return False
|
|
185
|
+
return agent_ids_match(lock.get("agent_id"), agent_id)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def format_lock_owner(
|
|
189
|
+
developer_id: str,
|
|
190
|
+
agent_id: Optional[str] = None,
|
|
191
|
+
agent_label: Optional[str] = None,
|
|
192
|
+
) -> str:
|
|
193
|
+
"""Format lock owner for CLI/log messages."""
|
|
194
|
+
base = f"@{developer_id}"
|
|
195
|
+
if agent_id:
|
|
196
|
+
label = agent_label or agent_id
|
|
197
|
+
return f"{base} (agent: {label})"
|
|
198
|
+
return base
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def format_conflict_message(
|
|
202
|
+
file_path: str,
|
|
203
|
+
developer_id: str,
|
|
204
|
+
agent_id: Optional[str] = None,
|
|
205
|
+
agent_label: Optional[str] = None,
|
|
206
|
+
) -> str:
|
|
207
|
+
"""Build a user-facing conflict message."""
|
|
208
|
+
owner = format_lock_owner(developer_id, agent_id, agent_label)
|
|
209
|
+
return f"⚠ {file_path} is locked by {owner}. Editing is not recommended."
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def session_token_seed(
|
|
213
|
+
developer_id: str,
|
|
214
|
+
agent_id: Optional[str],
|
|
215
|
+
hostname: str,
|
|
216
|
+
project_root: str,
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Build the seed string for deterministic session tokens.
|
|
219
|
+
|
|
220
|
+
Each component derivation is defensive: if a value cannot be stringified, a safe
|
|
221
|
+
fallback is used rather than raising (a raised seed breaks re-adoption).
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
dev = str(developer_id).strip().lower() if developer_id else "unknown"
|
|
225
|
+
except Exception:
|
|
226
|
+
dev = "unknown"
|
|
227
|
+
try:
|
|
228
|
+
host = hostname.lower() if hostname else "localhost"
|
|
229
|
+
except Exception:
|
|
230
|
+
host = "localhost"
|
|
231
|
+
try:
|
|
232
|
+
root = project_root.lower().rstrip("\\/") if project_root else "project"
|
|
233
|
+
except Exception:
|
|
234
|
+
root = "project"
|
|
235
|
+
if agent_id:
|
|
236
|
+
try:
|
|
237
|
+
agent = str(agent_id).strip().lower()
|
|
238
|
+
except Exception:
|
|
239
|
+
agent = "agent"
|
|
240
|
+
return f"{dev}:{agent}:{host}:{root}"
|
|
241
|
+
return f"{dev}:{host}:{root}"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def session_token_from_seed(seed: str) -> str:
|
|
245
|
+
"""Derive the 16-char hex session token from a seed."""
|
|
246
|
+
return hashlib.sha256(seed.encode()).hexdigest()[:16]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def daemon_pid_basename(agent_id: Optional[str]) -> str:
|
|
250
|
+
"""Return the daemon PID filename for the given agent (or default)."""
|
|
251
|
+
if not agent_id:
|
|
252
|
+
return ".daemon.pid"
|
|
253
|
+
safe = re.sub(r"[^a-zA-Z0-9._-]", "_", agent_id)
|
|
254
|
+
return f".daemon.{safe}.pid"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def resolve_daemon_pid_path(
|
|
258
|
+
state_dir: str,
|
|
259
|
+
agent_id: Optional[str],
|
|
260
|
+
*,
|
|
261
|
+
env_override: Optional[str] = None,
|
|
262
|
+
) -> str:
|
|
263
|
+
"""Resolve the PID file path for a watcher instance."""
|
|
264
|
+
override = env_override or _read_clean_env("COLLAB_PID_FILE")
|
|
265
|
+
if override:
|
|
266
|
+
return override
|
|
267
|
+
return os.path.join(state_dir, daemon_pid_basename(agent_id))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def apply_agent_filter(query: Any, agent_id: Optional[str]) -> Any:
|
|
271
|
+
"""Scope a PostgREST delete/update query to the current agent_id (or NULL)."""
|
|
272
|
+
if agent_id is None:
|
|
273
|
+
is_null = getattr(query, "is_", None)
|
|
274
|
+
if callable(is_null):
|
|
275
|
+
return is_null("agent_id", "null")
|
|
276
|
+
# Test doubles may only implement ``eq`` — treat NULL as None there.
|
|
277
|
+
return query.eq("agent_id", None)
|
|
278
|
+
return query.eq("agent_id", agent_id)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def identity_summary(
|
|
282
|
+
developer_id: str,
|
|
283
|
+
agent_id: Optional[str],
|
|
284
|
+
agent_label: Optional[str],
|
|
285
|
+
) -> dict[str, Optional[str]]:
|
|
286
|
+
"""Return a dict suitable for ``collab whoami`` JSON output."""
|
|
287
|
+
mode = "agent" if agent_id else "human"
|
|
288
|
+
return {
|
|
289
|
+
"developer_id": developer_id,
|
|
290
|
+
"agent_id": agent_id,
|
|
291
|
+
"agent_label": agent_label,
|
|
292
|
+
"mode": mode,
|
|
293
|
+
}
|