general-augment-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 (42) hide show
  1. general_augment_cli-0.1.0.dist-info/METADATA +180 -0
  2. general_augment_cli-0.1.0.dist-info/RECORD +42 -0
  3. general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
  4. general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. platform_cli/__init__.py +5 -0
  6. platform_cli/branding.py +27 -0
  7. platform_cli/client.py +179 -0
  8. platform_cli/commands/__init__.py +1 -0
  9. platform_cli/commands/approvals.py +150 -0
  10. platform_cli/commands/auth.py +96 -0
  11. platform_cli/commands/billing.py +143 -0
  12. platform_cli/commands/channels.py +212 -0
  13. platform_cli/commands/deploy.py +72 -0
  14. platform_cli/commands/dev.py +38 -0
  15. platform_cli/commands/doctor.py +170 -0
  16. platform_cli/commands/identity.py +433 -0
  17. platform_cli/commands/init.py +55 -0
  18. platform_cli/commands/integrate.py +94 -0
  19. platform_cli/commands/keys.py +116 -0
  20. platform_cli/commands/logs.py +43 -0
  21. platform_cli/commands/mcp.py +258 -0
  22. platform_cli/commands/memory.py +316 -0
  23. platform_cli/commands/mock.py +30 -0
  24. platform_cli/commands/model_providers.py +226 -0
  25. platform_cli/commands/observability.py +174 -0
  26. platform_cli/commands/onboarding.py +72 -0
  27. platform_cli/commands/projects.py +302 -0
  28. platform_cli/commands/skills.py +116 -0
  29. platform_cli/commands/smoke.py +280 -0
  30. platform_cli/commands/status.py +49 -0
  31. platform_cli/commands/tools.py +179 -0
  32. platform_cli/commands/users.py +150 -0
  33. platform_cli/commands/validate.py +96 -0
  34. platform_cli/commands/verify.py +648 -0
  35. platform_cli/config.py +114 -0
  36. platform_cli/errors.py +103 -0
  37. platform_cli/local_mock.py +1392 -0
  38. platform_cli/main.py +130 -0
  39. platform_cli/openapi.py +1048 -0
  40. platform_cli/output.py +47 -0
  41. platform_cli/readiness.py +176 -0
  42. platform_cli/runtime.py +22 -0
@@ -0,0 +1,1392 @@
1
+ """Run a local test-only General Augment HTTP mock server.
2
+
3
+ The mock is for app contract tests and fixtures. It does not run Hermes, call models,
4
+ enforce billing, validate credentials, or persist data outside this process.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import copy
11
+ import hashlib
12
+ import json
13
+ import re
14
+ import sys
15
+ from collections.abc import Mapping
16
+ from dataclasses import dataclass, field
17
+ from http import HTTPStatus
18
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
19
+ from typing import Any, cast
20
+ from urllib.parse import parse_qs, unquote, urlparse
21
+ from uuid import NAMESPACE_URL, uuid5
22
+
23
+ import yaml
24
+
25
+ DEFAULT_HOST = "127.0.0.1"
26
+ DEFAULT_PORT = 8787
27
+ MOCK_CREATED_AT = "2026-01-01T00:00:00Z"
28
+ EXACT_REPLY_RE = re.compile(r"reply exactly with:\s*(.+)", re.IGNORECASE)
29
+ HEALTH_PATHS = {"/v1/health", "/health/ready", "/health/live"}
30
+
31
+
32
+ @dataclass(slots=True)
33
+ class MemoryFact:
34
+ """One in-memory fact stored by the local mock."""
35
+
36
+ memory_id: str
37
+ user_id: str
38
+ fact: str
39
+ fact_type: str = "fact"
40
+ importance_score: float | None = None
41
+ source: str | None = None
42
+ metadata: dict[str, Any] = field(default_factory=dict)
43
+ created_at: str = MOCK_CREATED_AT
44
+ expires_at: str | None = None
45
+
46
+ def as_result(self, score: float | None) -> dict[str, Any]:
47
+ """Return a public memory-hit shape."""
48
+ return {
49
+ "id": self.memory_id,
50
+ "memory_id": self.memory_id,
51
+ "fact": self.fact,
52
+ "fact_type": self.fact_type,
53
+ "content": self.fact,
54
+ "importance_score": self.importance_score,
55
+ "similarity": score,
56
+ "score": score,
57
+ "created_at": self.created_at,
58
+ "expires_at": self.expires_at,
59
+ "source": self.source,
60
+ "metadata": self.metadata,
61
+ }
62
+
63
+
64
+ class LocalGAMockStore:
65
+ """Deterministic in-memory state for local app contract tests."""
66
+
67
+ def __init__(self) -> None:
68
+ """Initialize empty response replay and memory state."""
69
+ self.response_replays: dict[str, tuple[str, dict[str, Any]]] = {}
70
+ self.memory_replays: dict[tuple[str, str], tuple[str, dict[str, Any]]] = {}
71
+ self.memories: dict[str, list[MemoryFact]] = {}
72
+ self.profiles: dict[str, dict[str, Any]] = {}
73
+ self.projects: dict[str, dict[str, Any]] = {}
74
+ self.logs_by_project: dict[str, list[dict[str, Any]]] = {}
75
+ self.project_traces: dict[str, list[dict[str, Any]]] = {}
76
+ self.project_usage: dict[str, dict[str, Any]] = {}
77
+ self.api_keys: dict[str, dict[str, Any]] = {}
78
+
79
+ def list_projects(self, headers: Mapping[str, str] | None = None) -> tuple[int, dict[str, Any]]:
80
+ """Return mock projects visible to any local management key."""
81
+
82
+ scoped_project_id = self._scoped_project_id(headers)
83
+ if scoped_project_id:
84
+ project = self.projects.get(scoped_project_id)
85
+ return HTTPStatus.OK, {"items": [project] if project else []}
86
+ return HTTPStatus.OK, {"items": list(self.projects.values())}
87
+
88
+ def me(self, headers: Mapping[str, str] | None = None) -> tuple[int, dict[str, Any]]:
89
+ """Return a stable local admin identity for CLI auth preflight."""
90
+
91
+ scoped_project_id = self._scoped_project_id(headers)
92
+ if scoped_project_id:
93
+ return HTTPStatus.OK, {
94
+ "auth_method": "api_key",
95
+ "project_id": scoped_project_id,
96
+ "project_ids": [scoped_project_id],
97
+ }
98
+ return HTTPStatus.OK, {
99
+ "auth_method": "api_key",
100
+ "project_ids": sorted(self.projects),
101
+ }
102
+
103
+ def deploy_project(
104
+ self, payload: dict[str, Any], project_id: str | None = None
105
+ ) -> tuple[int, dict[str, Any]]:
106
+ """Create or update a mock project from a deploy payload."""
107
+
108
+ yaml_content = str(payload.get("yaml_content") or "")
109
+ manifest = yaml.safe_load(yaml_content) if yaml_content else {}
110
+ manifest = manifest if isinstance(manifest, dict) else {}
111
+ raw_metadata = manifest.get("metadata")
112
+ metadata = cast(dict[str, Any], raw_metadata) if isinstance(raw_metadata, dict) else {}
113
+ slug = str(metadata.get("name") or project_id or "local-mock-project")
114
+ name = str(metadata.get("display_name") or _display_name(slug))
115
+ resolved_id = project_id or f"proj_mock_{_digest_json({'slug': slug})[:12]}"
116
+ project = {
117
+ "id": resolved_id,
118
+ "name": name,
119
+ "slug": slug,
120
+ "status": "active",
121
+ "channels": _manifest_channels(manifest),
122
+ "enabled_tool_ids": _manifest_tool_ids(manifest),
123
+ "soul_content": str(payload.get("soul_content") or ""),
124
+ "skill_contents": [
125
+ str(content) for content in payload.get("skills", []) if isinstance(content, str)
126
+ ],
127
+ }
128
+ self.projects[resolved_id] = project
129
+ self.project_usage.setdefault(resolved_id, {"agent_turns_count": 0, "total_cost_usd": 0.0})
130
+ return HTTPStatus.OK, project
131
+
132
+ def register_openapi_tools(
133
+ self, project_id: str, payload: dict[str, Any]
134
+ ) -> tuple[int, dict[str, Any]]:
135
+ """Pretend to register curated OpenAPI tools for a project."""
136
+
137
+ project = self.projects.get(project_id)
138
+ if project is None:
139
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
140
+ enabled = list(project.get("enabled_tool_ids") or [])
141
+ generated_count = max(len(enabled), 1)
142
+ return HTTPStatus.OK, {
143
+ "generated_count": generated_count,
144
+ "curated_count": generated_count,
145
+ "enabled_tool_ids": enabled,
146
+ "auto_deployed": bool(payload.get("auto_deploy", True)),
147
+ "mcp_server": {"name": f"{project['slug']}-api"},
148
+ "tools": [{"id": tool_id, "risk_level": "low"} for tool_id in enabled],
149
+ }
150
+
151
+ def list_tools(self) -> tuple[int, dict[str, Any]]:
152
+ """Return a stable built-in plus generated tool registry shape."""
153
+
154
+ tool_ids = {"web_search", "calendar_read"}
155
+ for project in self.projects.values():
156
+ tool_ids.update(str(tool_id) for tool_id in project.get("enabled_tool_ids") or [])
157
+ return HTTPStatus.OK, {
158
+ "items": [
159
+ {"id": tool_id, "risk_level": "low", "requires_approval": False}
160
+ for tool_id in sorted(tool_ids)
161
+ ]
162
+ }
163
+
164
+ def test_project(self, project_id: str, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
165
+ """Run a deterministic mock project test and retain logs/trace rows."""
166
+
167
+ project = self.projects.get(project_id)
168
+ if project is None:
169
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
170
+ message = str(payload.get("message") or "")
171
+ output = _mock_output_text(message, "mock-admin-user")
172
+ trace_id = f"trace_mock_{_digest_json({'project_id': project_id, 'message': message})[:16]}"
173
+ self._append_project_log(project_id, "user", message, trace_id)
174
+ self._append_project_log(project_id, "assistant", output, trace_id)
175
+ response_id = f"resp_mock_{trace_id.removeprefix('trace_mock_')}"
176
+ self.project_traces.setdefault(project_id, []).append(
177
+ {"trace_id": trace_id, "response_id": response_id}
178
+ )
179
+ self.project_usage.setdefault(project_id, {"agent_turns_count": 0, "total_cost_usd": 0.0})
180
+ self.project_usage[project_id]["agent_turns_count"] += 1
181
+ return HTTPStatus.OK, {
182
+ "response": output,
183
+ "response_text": output,
184
+ "warnings": [],
185
+ "metadata": {"trace_id": trace_id},
186
+ "error": None,
187
+ "model_used": "mock/balanced",
188
+ "cost_usd": 0.0,
189
+ }
190
+
191
+ def project_logs(self, project_id: str, limit: int) -> tuple[int, dict[str, Any]]:
192
+ """Return retained mock project logs."""
193
+
194
+ return HTTPStatus.OK, {"items": self.logs_by_project.get(project_id, [])[-limit:]}
195
+
196
+ def project_usage_detail(self, project_id: str) -> tuple[int, dict[str, Any]]:
197
+ """Return retained mock usage counters."""
198
+
199
+ totals = self.project_usage.get(project_id, {"agent_turns_count": 0, "total_cost_usd": 0.0})
200
+ return HTTPStatus.OK, {
201
+ "project_id": project_id,
202
+ "start_date": "2026-01-01",
203
+ "end_date": "2026-01-01",
204
+ "totals": totals,
205
+ "days": [{"date": "2026-01-01", **totals}],
206
+ "limits": {},
207
+ }
208
+
209
+ def project_observability(self, project_id: str) -> tuple[int, dict[str, Any]]:
210
+ """Return retained mock observability rows."""
211
+
212
+ traces = self.project_traces.get(project_id, [])
213
+ return HTTPStatus.OK, {
214
+ "langfuse_enabled": False,
215
+ "langfuse_project_id": None,
216
+ "langfuse_url": None,
217
+ "traces": traces,
218
+ "metrics": {"trace_count": len(traces)},
219
+ "messages_over_time": [],
220
+ "model_distribution": [{"model": "mock/balanced", "count": len(traces)}],
221
+ "tool_usage": [],
222
+ }
223
+
224
+ def project_channel_status(self, project_id: str) -> tuple[int, dict[str, Any]]:
225
+ """Return hosted-compatible channel readiness for one mock project."""
226
+
227
+ project = self.projects.get(project_id)
228
+ if project is None:
229
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
230
+ raw_channels = project.get("channels")
231
+ configured: dict[str, Any] = raw_channels if isinstance(raw_channels, dict) else {}
232
+ channels = [
233
+ _channel_health("in_app", "In-app", configured=True),
234
+ _channel_health("whatsapp", "WhatsApp", configured=bool(configured.get("whatsapp"))),
235
+ _channel_health("telegram", "Telegram", configured=bool(configured.get("telegram"))),
236
+ _channel_health("sms", "SMS", configured=bool(configured.get("sms"))),
237
+ ]
238
+ return HTTPStatus.OK, {"project_id": project_id, "channels": channels}
239
+
240
+ def project_runtime_policy(self, project_id: str) -> tuple[int, dict[str, Any]]:
241
+ """Return a hosted-compatible, secret-free runtime policy summary."""
242
+
243
+ project = self.projects.get(project_id)
244
+ if project is None:
245
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
246
+ enabled_tool_ids = [
247
+ str(tool_id) for tool_id in project.get("enabled_tool_ids") or []
248
+ ]
249
+ return HTTPStatus.OK, {
250
+ "project_id": project_id,
251
+ "model_routing": {
252
+ "mode": "tiered_complexity",
253
+ "tiers": {
254
+ "simple": "mock/simple",
255
+ "balanced": "mock/balanced",
256
+ "complex": "mock/complex",
257
+ },
258
+ "default_tier": "balanced",
259
+ "auto_routes_by": [
260
+ "prompt complexity",
261
+ "enabled tools",
262
+ "conversation history",
263
+ "reasoning_effort override when supplied",
264
+ ],
265
+ "channel_parity": True,
266
+ },
267
+ "tool_discovery": {
268
+ "mode": "auto",
269
+ "direct_schema_tool_limit": 8,
270
+ "max_search_results": 5,
271
+ },
272
+ "hermes_exposure": {
273
+ "uses_dynamic_discovery_by_default": False,
274
+ "turn_path": "shared_hermes",
275
+ },
276
+ "platform_tools": {
277
+ "enabled_tool_ids": enabled_tool_ids,
278
+ "unknown_tool_ids": [],
279
+ },
280
+ "mcp": {"enabled_tool_ids": []},
281
+ "skills": {
282
+ "names": [
283
+ _skill_name_from_content(content)
284
+ for content in project.get("skill_contents", []) or []
285
+ ]
286
+ },
287
+ }
288
+
289
+ def project_soul(self, project_id: str) -> tuple[int, dict[str, Any]]:
290
+ """Return SOUL.md content for one mock project."""
291
+
292
+ project = self.projects.get(project_id)
293
+ if project is None:
294
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
295
+ return HTTPStatus.OK, {
296
+ "project_id": project_id,
297
+ "content": str(project.get("soul_content") or ""),
298
+ }
299
+
300
+ def project_skills(self, project_id: str) -> tuple[int, dict[str, Any]]:
301
+ """Return SKILL.md summaries for one mock project."""
302
+
303
+ project = self.projects.get(project_id)
304
+ if project is None:
305
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
306
+ return HTTPStatus.OK, {
307
+ "items": [
308
+ _skill_summary(content)
309
+ for content in project.get("skill_contents", []) or []
310
+ if isinstance(content, str)
311
+ ]
312
+ }
313
+
314
+ def get_project_skill(self, project_id: str, skill_name: str) -> tuple[int, dict[str, Any]]:
315
+ """Return one mock SKILL.md file."""
316
+
317
+ project = self.projects.get(project_id)
318
+ if project is None:
319
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
320
+ for content in project.get("skill_contents", []) or []:
321
+ if isinstance(content, str) and _skill_name_from_content(content) == skill_name:
322
+ return HTTPStatus.OK, {
323
+ "name": skill_name,
324
+ "content": content,
325
+ "metadata": _skill_metadata(content),
326
+ }
327
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Skill not found.")
328
+
329
+ def add_project_skill(
330
+ self, project_id: str, payload: dict[str, Any]
331
+ ) -> tuple[int, dict[str, Any]]:
332
+ """Create or replace one mock SKILL.md file."""
333
+
334
+ project = self.projects.get(project_id)
335
+ if project is None:
336
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
337
+ content = str(payload.get("content") or "")
338
+ if not content.strip():
339
+ return HTTPStatus.BAD_REQUEST, _error("invalid_request", "Skill content is required.")
340
+ skill_name = _skill_name_from_content(content)
341
+ existing = [
342
+ item
343
+ for item in project.get("skill_contents", []) or []
344
+ if isinstance(item, str) and _skill_name_from_content(item) != skill_name
345
+ ]
346
+ project["skill_contents"] = [*existing, content]
347
+ return HTTPStatus.OK, {
348
+ "name": skill_name,
349
+ "content": content,
350
+ "metadata": _skill_metadata(content),
351
+ }
352
+
353
+ def delete_project_skill(self, project_id: str, skill_name: str) -> tuple[int, dict[str, Any]]:
354
+ """Delete one mock SKILL.md file."""
355
+
356
+ project = self.projects.get(project_id)
357
+ if project is None:
358
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
359
+ existing = [
360
+ item
361
+ for item in project.get("skill_contents", []) or []
362
+ if isinstance(item, str) and _skill_name_from_content(item) != skill_name
363
+ ]
364
+ if len(existing) == len(project.get("skill_contents", []) or []):
365
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Skill not found.")
366
+ project["skill_contents"] = existing
367
+ return HTTPStatus.OK, {"status": "deleted", "name": skill_name}
368
+
369
+ def project_tool_call_audit(self, project_id: str) -> tuple[int, dict[str, Any]]:
370
+ """Return a stable empty audit list for local verification."""
371
+
372
+ if project_id not in self.projects:
373
+ return HTTPStatus.NOT_FOUND, _error("not_found", "Project not found.")
374
+ return HTTPStatus.OK, {"items": [], "next_cursor": None}
375
+
376
+ def create_api_key(self, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
377
+ """Create a deterministic local API key row."""
378
+
379
+ key_id = f"key_mock_{_digest_json(payload)[:12]}"
380
+ raw_key = f"gaadmlocal_{_digest_json({'key_id': key_id})[:24]}"
381
+ row = {
382
+ "id": key_id,
383
+ "name": str(payload.get("name") or "Local mock key"),
384
+ "api_key": raw_key,
385
+ "masked_key": f"{raw_key[:12]}...{raw_key[-4:]}",
386
+ "scopes": payload.get("scopes") or ["admin"],
387
+ "project_id": payload.get("project_id"),
388
+ "expires_at": payload.get("expires_at"),
389
+ "created_by": "local_mock",
390
+ "created_at": MOCK_CREATED_AT,
391
+ "last_used_at": None,
392
+ }
393
+ self.api_keys[key_id] = row
394
+ return HTTPStatus.OK, row
395
+
396
+ def list_api_keys(self, headers: Mapping[str, str] | None = None) -> tuple[int, dict[str, Any]]:
397
+ """List mock API keys without raw secrets."""
398
+
399
+ scoped_project_id = self._scoped_project_id(headers)
400
+ items = []
401
+ for row in self.api_keys.values():
402
+ if scoped_project_id and str(row.get("project_id") or "") != scoped_project_id:
403
+ continue
404
+ item = dict(row)
405
+ item.pop("api_key", None)
406
+ items.append(item)
407
+ return HTTPStatus.OK, {"items": items}
408
+
409
+ def _scoped_project_id(self, headers: Mapping[str, str] | None) -> str | None:
410
+ """Return the project id for a raw mock project key."""
411
+
412
+ credential = _auth_credential(headers or {})
413
+ if not credential:
414
+ return None
415
+ for row in self.api_keys.values():
416
+ if row.get("api_key") == credential and row.get("project_id"):
417
+ return str(row["project_id"])
418
+ return None
419
+
420
+ def update_api_key(self, key_id: str, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
421
+ """Update mock API key metadata."""
422
+
423
+ row = self.api_keys.get(key_id)
424
+ if row is None:
425
+ return HTTPStatus.NOT_FOUND, _error("not_found", "API key not found.")
426
+ for key in ("name", "scopes", "expires_at"):
427
+ if key in payload:
428
+ row[key] = payload[key]
429
+ item = dict(row)
430
+ item.pop("api_key", None)
431
+ return HTTPStatus.OK, item
432
+
433
+ def revoke_api_key(self, key_id: str) -> tuple[int, dict[str, Any]]:
434
+ """Revoke a mock API key."""
435
+
436
+ self.api_keys.pop(key_id, None)
437
+ return HTTPStatus.OK, {"status": "revoked", "id": key_id}
438
+
439
+ def response(
440
+ self,
441
+ payload: dict[str, Any],
442
+ headers: Mapping[str, str],
443
+ ) -> tuple[int, dict[str, Any]]:
444
+ """Return a deterministic Responses-shaped object."""
445
+ idempotency_key = _header(headers, "X-Idempotency-Key")
446
+ request_digest = _digest_json(payload)
447
+ if idempotency_key and idempotency_key in self.response_replays:
448
+ stored_digest, stored_response = self.response_replays[idempotency_key]
449
+ if stored_digest != request_digest:
450
+ return HTTPStatus.CONFLICT, _error(
451
+ "idempotency_key_conflict",
452
+ "X-Idempotency-Key was reused with a different mock request body.",
453
+ )
454
+ return HTTPStatus.OK, copy.deepcopy(stored_response)
455
+
456
+ response = self._build_response(payload, headers, idempotency_key)
457
+ project_id = _header(headers, "X-Project-ID")
458
+ if project_id:
459
+ self._record_response_turn(project_id, payload, response)
460
+ if idempotency_key:
461
+ self.response_replays[idempotency_key] = (request_digest, copy.deepcopy(response))
462
+ return HTTPStatus.OK, response
463
+
464
+ def _record_response_turn(
465
+ self, project_id: str, payload: dict[str, Any], response: dict[str, Any]
466
+ ) -> None:
467
+ """Record an app-facing response in project logs and observability."""
468
+
469
+ input_text = _extract_input_text(payload.get("input"))
470
+ output_text = response["output"][0]["content"][0]["text"]
471
+ trace_id = str(response["metadata"]["general_augment_trace_id"])
472
+ self._append_project_log(project_id, "user", input_text, trace_id)
473
+ self._append_project_log(project_id, "assistant", output_text, trace_id)
474
+ self.project_traces.setdefault(project_id, []).append(
475
+ {"trace_id": trace_id, "response_id": response["id"]}
476
+ )
477
+ self.project_usage.setdefault(project_id, {"agent_turns_count": 0, "total_cost_usd": 0.0})
478
+ self.project_usage[project_id]["agent_turns_count"] += 1
479
+
480
+ def _append_project_log(
481
+ self, project_id: str, role: str, content: str, trace_id: str
482
+ ) -> None:
483
+ """Append one mock project log row."""
484
+
485
+ digest = _digest_json({"project_id": project_id, "role": role, "content": content})
486
+ log_id = f"msg_mock_{digest[:12]}"
487
+ self.logs_by_project.setdefault(project_id, []).append(
488
+ {
489
+ "id": log_id,
490
+ "created_at": MOCK_CREATED_AT,
491
+ "session_id": _mock_user_uuid(f"{project_id}:session"),
492
+ "user_id": _mock_user_uuid(f"{project_id}:user"),
493
+ "role": role,
494
+ "content": content,
495
+ "observability_trace_id": trace_id,
496
+ "model_used": "mock/balanced" if role == "assistant" else None,
497
+ "cost_usd": 0.0 if role == "assistant" else None,
498
+ }
499
+ )
500
+
501
+ def store_memory(self, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
502
+ """Store one explicit memory fact in process memory."""
503
+ user_id = _external_user_id(payload)
504
+ fact = str(payload.get("fact") or payload.get("content") or "").strip()
505
+ if not fact:
506
+ return HTTPStatus.BAD_REQUEST, _error("invalid_memory", "fact is required.")
507
+
508
+ idempotency_key = _optional_string(payload.get("idempotency_key"))
509
+ request_digest = _digest_json(payload)
510
+ replay_key = (user_id, idempotency_key or "")
511
+ if idempotency_key and replay_key in self.memory_replays:
512
+ stored_digest, stored_response = self.memory_replays[replay_key]
513
+ if stored_digest != request_digest:
514
+ return HTTPStatus.CONFLICT, _error(
515
+ "memory_idempotency_key_conflict",
516
+ "idempotency_key was reused with a different memory write request.",
517
+ )
518
+ return HTTPStatus.OK, copy.deepcopy(stored_response)
519
+
520
+ memory_id = f"mem_mock_{_digest_json({'user_id': user_id, 'fact': fact})[:16]}"
521
+ metadata = _object_payload(payload.get("metadata"))
522
+ fact_row = MemoryFact(
523
+ memory_id=memory_id,
524
+ user_id=user_id,
525
+ fact=fact,
526
+ fact_type=str(payload.get("fact_type") or "fact"),
527
+ importance_score=_optional_float(payload.get("importance_score")),
528
+ source=_optional_string(payload.get("source")),
529
+ metadata=dict(metadata),
530
+ )
531
+ self.memories.setdefault(user_id, [])
532
+ if not any(existing.memory_id == memory_id for existing in self.memories[user_id]):
533
+ self.memories[user_id].append(fact_row)
534
+
535
+ user_profile = _object_payload(payload.get("user_profile"))
536
+ if user_profile:
537
+ self.profiles.setdefault(user_id, {}).update(user_profile)
538
+
539
+ response = {
540
+ "user_id": user_id,
541
+ "general_augment_user_id": _mock_user_uuid(user_id),
542
+ "memory_id": memory_id,
543
+ "content": fact,
544
+ "fact": fact,
545
+ "fact_type": fact_row.fact_type,
546
+ "importance_score": fact_row.importance_score,
547
+ "source": fact_row.source,
548
+ "metadata": fact_row.metadata,
549
+ "status": "stored",
550
+ }
551
+ if idempotency_key:
552
+ self.memory_replays[replay_key] = (request_digest, copy.deepcopy(response))
553
+ return HTTPStatus.OK, response
554
+
555
+ def search_memory(self, payload: dict[str, Any]) -> tuple[int, dict[str, Any]]:
556
+ """Search stored mock memory with deterministic lexical scores."""
557
+ user_id = _external_user_id(payload)
558
+ query = str(payload.get("query") or "")
559
+ limit = _positive_int(payload.get("limit"), default=10)
560
+ min_similarity = _optional_float(payload.get("min_similarity"))
561
+ min_importance = _optional_float(payload.get("min_importance"))
562
+ fact_type = _optional_string(payload.get("fact_type"))
563
+ source = _optional_string(payload.get("source"))
564
+
565
+ ranked: list[tuple[float, MemoryFact]] = []
566
+ for fact in self.memories.get(user_id, []):
567
+ if fact_type and fact.fact_type != fact_type:
568
+ continue
569
+ if source and fact.source != source:
570
+ continue
571
+ if min_importance is not None and (fact.importance_score or 0.0) < min_importance:
572
+ continue
573
+ score = _lexical_score(query, fact.fact)
574
+ if min_similarity is not None and score < min_similarity:
575
+ continue
576
+ ranked.append((score, fact))
577
+
578
+ ranked.sort(key=lambda item: (-item[0], item[1].memory_id))
579
+ facts = [fact.as_result(round(score, 4)) for score, fact in ranked[:limit]]
580
+ return HTTPStatus.OK, {
581
+ "user_id": user_id,
582
+ "general_augment_user_id": _mock_user_uuid(user_id),
583
+ "facts": facts,
584
+ }
585
+
586
+ def memory_profile(self, user_id: str) -> tuple[int, dict[str, Any]]:
587
+ """Return the mock profile and recent facts for one app user."""
588
+ facts = self.memories.get(user_id, [])
589
+ profile = {
590
+ "external_user_id": user_id,
591
+ "external_provider": "local_mock",
592
+ **self.profiles.get(user_id, {}),
593
+ }
594
+ return HTTPStatus.OK, {
595
+ "user_id": user_id,
596
+ "general_augment_user_id": _mock_user_uuid(user_id),
597
+ "profile": profile,
598
+ "recent_facts": [fact.as_result(None) for fact in facts],
599
+ "total_facts": len(facts),
600
+ }
601
+
602
+ def delete_memory(self, memory_id: str, user_id: str) -> tuple[int, dict[str, Any]]:
603
+ """Delete one memory for the scoped app user."""
604
+ facts = self.memories.get(user_id, [])
605
+ kept = [fact for fact in facts if fact.memory_id != memory_id]
606
+ deleted_count = len(facts) - len(kept)
607
+ self.memories[user_id] = kept
608
+ return HTTPStatus.OK, {
609
+ "user_id": user_id,
610
+ "general_augment_user_id": _mock_user_uuid(user_id),
611
+ "memory_id": memory_id,
612
+ "deleted_ids": [memory_id] if deleted_count else [],
613
+ "deleted_count": deleted_count,
614
+ "status": "deleted" if deleted_count else "not_found",
615
+ }
616
+
617
+ def purge_user_memory(self, user_id: str) -> tuple[int, dict[str, Any]]:
618
+ """Delete all memory rows for one app user."""
619
+ deleted_ids = [fact.memory_id for fact in self.memories.get(user_id, [])]
620
+ self.memories[user_id] = []
621
+ return HTTPStatus.OK, {
622
+ "user_id": user_id,
623
+ "general_augment_user_id": _mock_user_uuid(user_id),
624
+ "memory_id": None,
625
+ "deleted_ids": deleted_ids,
626
+ "deleted_count": len(deleted_ids),
627
+ "status": "purged",
628
+ }
629
+
630
+ def _build_response(
631
+ self,
632
+ payload: dict[str, Any],
633
+ headers: Mapping[str, str],
634
+ idempotency_key: str | None,
635
+ ) -> dict[str, Any]:
636
+ """Build one mock Responses object."""
637
+ input_text = _extract_input_text(payload.get("input"))
638
+ output_text = _mock_response_text(
639
+ payload,
640
+ input_text,
641
+ str(payload.get("user") or "mock-user"),
642
+ )
643
+ digest = _digest_json(
644
+ {
645
+ "input": input_text,
646
+ "user": payload.get("user"),
647
+ "model": payload.get("model"),
648
+ "idempotency_key": idempotency_key,
649
+ }
650
+ )
651
+ response_id = f"resp_mock_{digest[:20]}"
652
+ request_id = _header(headers, "X-Request-ID") or f"req_mock_{digest[:16]}"
653
+ trace_id = f"trace_mock_{digest[:16]}"
654
+ model = str(payload.get("model") or "balanced")
655
+ input_tokens = max(1, len(input_text.split()))
656
+ output_tokens = max(1, len(output_text.split()))
657
+ metadata = _object_payload(payload.get("metadata"))
658
+ response_metadata = dict(metadata)
659
+ response_metadata.update(
660
+ {
661
+ "general_augment_response_id": response_id,
662
+ "general_augment_request_id": request_id,
663
+ "general_augment_trace_id": trace_id,
664
+ "general_augment_model": f"mock/{model}",
665
+ "general_augment_input_tokens": input_tokens,
666
+ "general_augment_output_tokens": output_tokens,
667
+ "general_augment_cost_usd": 0.0,
668
+ "general_augment_latency_ms": 0,
669
+ }
670
+ )
671
+ response_metadata.setdefault("trace_id", trace_id)
672
+ response_metadata.setdefault("request_id", request_id)
673
+ _copy_trace_header(headers, response_metadata, "traceparent")
674
+ _copy_trace_header(headers, response_metadata, "tracestate")
675
+
676
+ return {
677
+ "id": response_id,
678
+ "object": "response",
679
+ "created_at": MOCK_CREATED_AT,
680
+ "status": "completed",
681
+ "model": f"mock/{model}",
682
+ "output": [
683
+ {
684
+ "id": f"msg_mock_{digest[:20]}",
685
+ "type": "message",
686
+ "role": "assistant",
687
+ "status": "completed",
688
+ "content": [
689
+ {
690
+ "type": "output_text",
691
+ "text": output_text,
692
+ "annotations": [],
693
+ }
694
+ ],
695
+ }
696
+ ],
697
+ "usage": {
698
+ "input_tokens": input_tokens,
699
+ "input_tokens_details": {"cached_tokens": 0},
700
+ "output_tokens": output_tokens,
701
+ "output_tokens_details": {"reasoning_tokens": 0},
702
+ "total_tokens": input_tokens + output_tokens,
703
+ },
704
+ "previous_response_id": payload.get("previous_response_id"),
705
+ "metadata": response_metadata,
706
+ "error": None,
707
+ }
708
+
709
+
710
+ def sse_events_for_response(response: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
711
+ """Return semantic SSE events for a completed mock response."""
712
+ output_item = response["output"][0]
713
+ output_text = output_item["content"][0]["text"]
714
+ response_stub = {
715
+ "id": response["id"],
716
+ "object": "response",
717
+ "status": "in_progress",
718
+ "model": response["model"],
719
+ "output": [],
720
+ "metadata": response["metadata"],
721
+ }
722
+ return [
723
+ (
724
+ "response.created",
725
+ {"type": "response.created", "sequence_number": 0, "response": response_stub},
726
+ ),
727
+ (
728
+ "response.in_progress",
729
+ {"type": "response.in_progress", "sequence_number": 1, "response": response_stub},
730
+ ),
731
+ (
732
+ "response.output_item.added",
733
+ {
734
+ "type": "response.output_item.added",
735
+ "sequence_number": 2,
736
+ "output_index": 0,
737
+ "item": output_item,
738
+ },
739
+ ),
740
+ (
741
+ "response.output_text.delta",
742
+ {
743
+ "type": "response.output_text.delta",
744
+ "sequence_number": 3,
745
+ "item_id": output_item["id"],
746
+ "output_index": 0,
747
+ "content_index": 0,
748
+ "delta": output_text,
749
+ },
750
+ ),
751
+ (
752
+ "response.output_text.done",
753
+ {
754
+ "type": "response.output_text.done",
755
+ "sequence_number": 4,
756
+ "item_id": output_item["id"],
757
+ "output_index": 0,
758
+ "content_index": 0,
759
+ "text": output_text,
760
+ },
761
+ ),
762
+ (
763
+ "response.completed",
764
+ {
765
+ "type": "response.completed",
766
+ "sequence_number": 5,
767
+ "response": response,
768
+ },
769
+ ),
770
+ ]
771
+
772
+
773
+ class LocalGAMockHandler(BaseHTTPRequestHandler):
774
+ """HTTP handler for the local test mock."""
775
+
776
+ store = LocalGAMockStore()
777
+ quiet = False
778
+ server_version = "GeneralAugmentLocalMock/1.0"
779
+
780
+ def do_GET(self) -> None:
781
+ """Handle health and memory profile requests."""
782
+ parsed = urlparse(self.path)
783
+ if parsed.path in HEALTH_PATHS:
784
+ self._send_json(
785
+ HTTPStatus.OK,
786
+ {
787
+ "status": "ok",
788
+ "db": "connected (local mock)",
789
+ "redis": "connected (local mock)",
790
+ },
791
+ )
792
+ return
793
+ if parsed.path == "/api/v1/admin/projects":
794
+ status, payload = self.store.list_projects(_headers_dict(self.headers))
795
+ self._send_json(status, payload)
796
+ return
797
+ if parsed.path == "/api/v1/admin/me":
798
+ status, payload = self.store.me(_headers_dict(self.headers))
799
+ self._send_json(status, payload)
800
+ return
801
+ if parsed.path == "/api/v1/admin/tools":
802
+ status, payload = self.store.list_tools()
803
+ self._send_json(status, payload)
804
+ return
805
+ if parsed.path == "/api/v1/admin/keys":
806
+ status, payload = self.store.list_api_keys(_headers_dict(self.headers))
807
+ self._send_json(status, payload)
808
+ return
809
+ admin_match = _admin_project_route(parsed.path)
810
+ if admin_match:
811
+ project_id, suffix = admin_match
812
+ query = parse_qs(parsed.query)
813
+ if suffix == "logs":
814
+ limit = _positive_int(query.get("limit", ["50"])[0], default=50)
815
+ status, payload = self.store.project_logs(project_id, limit)
816
+ self._send_json(status, payload)
817
+ return
818
+ if suffix == "usage":
819
+ status, payload = self.store.project_usage_detail(project_id)
820
+ self._send_json(status, payload)
821
+ return
822
+ if suffix == "observability":
823
+ status, payload = self.store.project_observability(project_id)
824
+ self._send_json(status, payload)
825
+ return
826
+ if suffix == "channels/status":
827
+ status, payload = self.store.project_channel_status(project_id)
828
+ self._send_json(status, payload)
829
+ return
830
+ if suffix == "runtime-policy":
831
+ status, payload = self.store.project_runtime_policy(project_id)
832
+ self._send_json(status, payload)
833
+ return
834
+ if suffix == "soul":
835
+ status, payload = self.store.project_soul(project_id)
836
+ self._send_json(status, payload)
837
+ return
838
+ if suffix == "skills":
839
+ status, payload = self.store.project_skills(project_id)
840
+ self._send_json(status, payload)
841
+ return
842
+ if suffix.startswith("skills/"):
843
+ skill_name = unquote(suffix.removeprefix("skills/"))
844
+ status, payload = self.store.get_project_skill(project_id, skill_name)
845
+ self._send_json(status, payload)
846
+ return
847
+ if suffix == "audit/tool-calls":
848
+ status, payload = self.store.project_tool_call_audit(project_id)
849
+ self._send_json(status, payload)
850
+ return
851
+ if parsed.path.startswith("/api/v1/agent/memory/profile/"):
852
+ user_id = _path_suffix(parsed.path, "/api/v1/agent/memory/profile/")
853
+ status, payload = self.store.memory_profile(user_id)
854
+ self._send_json(status, payload)
855
+ return
856
+ self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
857
+
858
+ def do_POST(self) -> None:
859
+ """Handle Responses and memory write/search requests."""
860
+ parsed = urlparse(self.path)
861
+ payload = self._read_json()
862
+ if payload is None:
863
+ return
864
+ if parsed.path == "/v1/responses":
865
+ status, response = self.store.response(payload, _headers_dict(self.headers))
866
+ if status == HTTPStatus.OK and payload.get("stream") is True:
867
+ self._send_sse(response)
868
+ return
869
+ self._send_json(status, response)
870
+ return
871
+ if parsed.path == "/api/v1/admin/projects/from-config":
872
+ status, response = self.store.deploy_project(payload)
873
+ self._send_json(status, response)
874
+ return
875
+ if parsed.path == "/api/v1/admin/keys":
876
+ status, response = self.store.create_api_key(payload)
877
+ self._send_json(status, response)
878
+ return
879
+ admin_match = _admin_project_route(parsed.path)
880
+ if admin_match:
881
+ project_id, suffix = admin_match
882
+ if suffix == "tools/from-openapi":
883
+ status, response = self.store.register_openapi_tools(project_id, payload)
884
+ self._send_json(status, response)
885
+ return
886
+ if suffix == "test":
887
+ status, response = self.store.test_project(project_id, payload)
888
+ self._send_json(status, response)
889
+ return
890
+ if suffix == "skills":
891
+ status, response = self.store.add_project_skill(project_id, payload)
892
+ self._send_json(status, response)
893
+ return
894
+ if parsed.path == "/api/v1/agent/memory/store":
895
+ status, response = self.store.store_memory(payload)
896
+ self._send_json(status, response)
897
+ return
898
+ if parsed.path == "/api/v1/agent/memory/search":
899
+ status, response = self.store.search_memory(payload)
900
+ self._send_json(status, response)
901
+ return
902
+ self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
903
+
904
+ def do_DELETE(self) -> None:
905
+ """Handle memory delete and purge requests."""
906
+ parsed = urlparse(self.path)
907
+ if parsed.path.startswith("/api/v1/agent/memory/user/"):
908
+ user_id = _path_suffix(parsed.path, "/api/v1/agent/memory/user/")
909
+ status, payload = self.store.purge_user_memory(user_id)
910
+ self._send_json(status, payload)
911
+ return
912
+ if parsed.path.startswith("/api/v1/agent/memory/"):
913
+ memory_id = _path_suffix(parsed.path, "/api/v1/agent/memory/")
914
+ query = parse_qs(parsed.query)
915
+ user_id = query.get("user_id", [""])[0]
916
+ status, payload = self.store.delete_memory(memory_id, user_id)
917
+ self._send_json(status, payload)
918
+ return
919
+ if parsed.path.startswith("/api/v1/admin/keys/"):
920
+ key_id = _path_suffix(parsed.path, "/api/v1/admin/keys/")
921
+ status, payload = self.store.revoke_api_key(key_id)
922
+ self._send_json(status, payload)
923
+ return
924
+ admin_match = _admin_project_route(parsed.path)
925
+ if admin_match:
926
+ project_id, suffix = admin_match
927
+ if suffix.startswith("skills/"):
928
+ skill_name = unquote(suffix.removeprefix("skills/"))
929
+ status, payload = self.store.delete_project_skill(project_id, skill_name)
930
+ self._send_json(status, payload)
931
+ return
932
+ self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
933
+
934
+ def do_PUT(self) -> None:
935
+ """Handle project config updates."""
936
+
937
+ parsed = urlparse(self.path)
938
+ payload = self._read_json()
939
+ if payload is None:
940
+ return
941
+ admin_match = _admin_project_route(parsed.path)
942
+ if admin_match:
943
+ project_id, suffix = admin_match
944
+ if suffix == "config":
945
+ status, response = self.store.deploy_project(payload, project_id=project_id)
946
+ self._send_json(status, response)
947
+ return
948
+ if suffix.startswith("skills/"):
949
+ status, response = self.store.add_project_skill(project_id, payload)
950
+ self._send_json(status, response)
951
+ return
952
+ self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
953
+
954
+ def do_PATCH(self) -> None:
955
+ """Handle API key metadata updates."""
956
+
957
+ parsed = urlparse(self.path)
958
+ payload = self._read_json()
959
+ if payload is None:
960
+ return
961
+ if parsed.path.startswith("/api/v1/admin/keys/"):
962
+ key_id = _path_suffix(parsed.path, "/api/v1/admin/keys/")
963
+ status, response = self.store.update_api_key(key_id, payload)
964
+ self._send_json(status, response)
965
+ return
966
+ self._send_json(HTTPStatus.NOT_FOUND, _error("not_found", "No mock route matched."))
967
+
968
+ def log_message(self, format: str, *args: object) -> None:
969
+ """Keep test output quiet unless the server is run verbosely."""
970
+ if not self.quiet:
971
+ super().log_message(format, *args)
972
+
973
+ def _read_json(self) -> dict[str, Any] | None:
974
+ """Read a JSON object request body."""
975
+ length = int(self.headers.get("Content-Length") or "0")
976
+ raw = self.rfile.read(length) if length else b"{}"
977
+ try:
978
+ payload = json.loads(raw.decode("utf-8"))
979
+ except (UnicodeDecodeError, json.JSONDecodeError):
980
+ self._send_json(HTTPStatus.BAD_REQUEST, _error("invalid_json", "Body must be JSON."))
981
+ return None
982
+ if not isinstance(payload, dict):
983
+ self._send_json(
984
+ HTTPStatus.BAD_REQUEST,
985
+ _error("invalid_json", "Body must be an object."),
986
+ )
987
+ return None
988
+ return payload
989
+
990
+ def _send_json(self, status: int, payload: dict[str, Any]) -> None:
991
+ """Send a JSON response."""
992
+ body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
993
+ self.send_response(int(status))
994
+ self.send_header("Content-Type", "application/json")
995
+ self.send_header("Content-Length", str(len(body)))
996
+ self.end_headers()
997
+ self.wfile.write(body)
998
+
999
+ def _send_sse(self, response: dict[str, Any]) -> None:
1000
+ """Send a semantic SSE response."""
1001
+ self.send_response(HTTPStatus.OK)
1002
+ self.send_header("Content-Type", "text/event-stream")
1003
+ self.send_header("Cache-Control", "no-cache")
1004
+ self.end_headers()
1005
+ for event, payload in sse_events_for_response(response):
1006
+ data = json.dumps(payload, sort_keys=True, separators=(",", ":"))
1007
+ self.wfile.write(f"event: {event}\ndata: {data}\n\n".encode())
1008
+
1009
+
1010
+ def make_handler(store: LocalGAMockStore, *, quiet: bool) -> type[LocalGAMockHandler]:
1011
+ """Return a handler class bound to one mock store."""
1012
+
1013
+ class BoundLocalGAMockHandler(LocalGAMockHandler):
1014
+ """Handler with injected process-local state."""
1015
+
1016
+ BoundLocalGAMockHandler.store = store
1017
+ BoundLocalGAMockHandler.quiet = quiet
1018
+ return BoundLocalGAMockHandler
1019
+
1020
+
1021
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
1022
+ """Parse local mock CLI arguments."""
1023
+ parser = argparse.ArgumentParser(description="Run a local General Augment mock server.")
1024
+ parser.add_argument("--host", default=DEFAULT_HOST)
1025
+ parser.add_argument("--port", type=int, default=DEFAULT_PORT)
1026
+ parser.add_argument("--quiet", action="store_true")
1027
+ return parser.parse_args(argv)
1028
+
1029
+
1030
+ def run_server(host: str, port: int, *, quiet: bool = False) -> None:
1031
+ """Run the mock HTTP server until interrupted."""
1032
+ store = LocalGAMockStore()
1033
+ handler = make_handler(store, quiet=quiet)
1034
+ server = ThreadingHTTPServer((host, port), handler)
1035
+ print(f"General Augment local mock listening at http://{host}:{port}", flush=True)
1036
+ try:
1037
+ server.serve_forever()
1038
+ finally:
1039
+ server.server_close()
1040
+
1041
+
1042
+ def main(argv: list[str] | None = None) -> int:
1043
+ """CLI entrypoint."""
1044
+ args = parse_args(argv)
1045
+ try:
1046
+ run_server(args.host, args.port, quiet=args.quiet)
1047
+ except KeyboardInterrupt:
1048
+ print("\nStopped General Augment local mock.", file=sys.stderr)
1049
+ return 0
1050
+
1051
+
1052
+ def _header(headers: Mapping[str, str], name: str) -> str | None:
1053
+ """Return a case-insensitive header value from mapping-like headers."""
1054
+ for key, value in headers.items():
1055
+ if key.lower() == name.lower():
1056
+ return str(value)
1057
+ return None
1058
+
1059
+
1060
+ def _headers_dict(headers: Any) -> dict[str, str]:
1061
+ """Return a plain string mapping for request headers."""
1062
+ return {str(key): str(value) for key, value in headers.items()}
1063
+
1064
+
1065
+ def _auth_credential(headers: Mapping[str, str]) -> str | None:
1066
+ """Return either admin or bearer credential from request headers."""
1067
+
1068
+ admin_key = _header(headers, "X-Admin-Key")
1069
+ if admin_key:
1070
+ return admin_key
1071
+ authorization = _header(headers, "Authorization")
1072
+ if authorization and authorization.lower().startswith("bearer "):
1073
+ return authorization.split(None, 1)[1]
1074
+ return None
1075
+
1076
+
1077
+ def _path_suffix(path: str, prefix: str) -> str:
1078
+ """Return a URL-decoded path suffix after a known route prefix."""
1079
+ return unquote(path.removeprefix(prefix))
1080
+
1081
+
1082
+ def _admin_project_route(path: str) -> tuple[str, str] | None:
1083
+ """Parse /api/v1/admin/projects/{project_id}/{suffix} routes."""
1084
+
1085
+ prefix = "/api/v1/admin/projects/"
1086
+ if not path.startswith(prefix):
1087
+ return None
1088
+ tail = path.removeprefix(prefix)
1089
+ if "/" not in tail:
1090
+ return unquote(tail), ""
1091
+ raw_project_id, suffix = tail.split("/", 1)
1092
+ return unquote(raw_project_id), suffix
1093
+
1094
+
1095
+ def _manifest_tool_ids(manifest: dict[str, Any]) -> list[str]:
1096
+ """Extract enabled generated tool ids from a manifest."""
1097
+
1098
+ tools = manifest.get("tools") if isinstance(manifest.get("tools"), dict) else {}
1099
+ mcp_servers = tools.get("mcp") if isinstance(tools, dict) else []
1100
+ tool_ids: list[str] = []
1101
+ if not isinstance(mcp_servers, list):
1102
+ return tool_ids
1103
+ for server in mcp_servers:
1104
+ if not isinstance(server, dict):
1105
+ continue
1106
+ include = _manifest_server_include(server)
1107
+ if isinstance(include, list):
1108
+ tool_ids.extend(str(tool_id) for tool_id in include)
1109
+ return sorted(set(tool_ids))
1110
+
1111
+
1112
+ def _manifest_server_include(server: dict[str, Any]) -> list[Any] | None:
1113
+ """Return tool includes from legacy or current generated MCP server shape."""
1114
+
1115
+ include = server.get("include")
1116
+ nested_tools = server.get("tools")
1117
+ if include is None and isinstance(nested_tools, dict):
1118
+ include = nested_tools.get("include")
1119
+ return include if isinstance(include, list) else None
1120
+
1121
+
1122
+ def _manifest_channels(manifest: dict[str, Any]) -> dict[str, Any]:
1123
+ """Extract configured channel blocks from a generated agent manifest."""
1124
+
1125
+ channels = manifest.get("channels")
1126
+ if not isinstance(channels, dict):
1127
+ return {}
1128
+ return {str(name): value for name, value in channels.items() if isinstance(value, dict)}
1129
+
1130
+
1131
+ def _skill_metadata(content: str) -> dict[str, Any]:
1132
+ """Return frontmatter metadata from a local mock SKILL.md file."""
1133
+
1134
+ if not content.startswith("---"):
1135
+ return {}
1136
+ parts = content.split("---", 2)
1137
+ if len(parts) < 3:
1138
+ return {}
1139
+ loaded = yaml.safe_load(parts[1]) or {}
1140
+ return loaded if isinstance(loaded, dict) else {}
1141
+
1142
+
1143
+ def _skill_name_from_content(content: str) -> str:
1144
+ """Return the display name for a local mock SKILL.md file."""
1145
+
1146
+ metadata = _skill_metadata(content)
1147
+ name = metadata.get("name")
1148
+ if isinstance(name, str) and name.strip():
1149
+ return name.strip()
1150
+ for line in content.splitlines():
1151
+ stripped = line.strip()
1152
+ if stripped.startswith("#"):
1153
+ return stripped.lstrip("#").strip() or "Untitled Skill"
1154
+ return "Untitled Skill"
1155
+
1156
+
1157
+ def _skill_summary(content: str) -> dict[str, Any]:
1158
+ """Return a hosted-like skill summary."""
1159
+
1160
+ metadata = _skill_metadata(content)
1161
+ return {
1162
+ "name": _skill_name_from_content(content),
1163
+ "description": str(metadata.get("description") or ""),
1164
+ "version": str(metadata.get("version") or "1.0"),
1165
+ "tags": [str(tag) for tag in metadata.get("tags", [])]
1166
+ if isinstance(metadata.get("tags"), list)
1167
+ else [],
1168
+ "tools": [str(tool) for tool in metadata.get("tools", [])]
1169
+ if isinstance(metadata.get("tools"), list)
1170
+ else [],
1171
+ "path": f"skills/{_slug(_skill_name_from_content(content))}/SKILL.md",
1172
+ }
1173
+
1174
+
1175
+ def _channel_health(channel: str, label: str, *, configured: bool) -> dict[str, Any]:
1176
+ """Return one hosted-like local channel health row."""
1177
+
1178
+ return {
1179
+ "channel": channel,
1180
+ "label": label,
1181
+ "status": "configured" if configured else "open",
1182
+ "sender": f"local-mock:{channel}" if configured else None,
1183
+ "provider_status": "local_mock",
1184
+ "delivery": "available" if configured else "not_configured",
1185
+ "last_message_at": None,
1186
+ "message_count_24h": 0,
1187
+ "details": {},
1188
+ }
1189
+
1190
+
1191
+ def _object_payload(value: Any) -> dict[str, Any]:
1192
+ """Return a string-keyed object payload or an empty mapping."""
1193
+ if not isinstance(value, dict):
1194
+ return {}
1195
+ return {str(key): item for key, item in value.items()}
1196
+
1197
+
1198
+ def _copy_trace_header(
1199
+ headers: Mapping[str, str],
1200
+ metadata: dict[str, Any],
1201
+ header_name: str,
1202
+ ) -> None:
1203
+ """Copy bounded trace context into mock metadata."""
1204
+ value = _header(headers, header_name)
1205
+ if not value or len(value) > 512:
1206
+ return
1207
+ metadata[f"general_augment_{header_name}"] = value
1208
+ metadata.setdefault(header_name, value)
1209
+
1210
+
1211
+ def _digest_json(value: Any) -> str:
1212
+ """Return a stable digest for JSON-compatible values."""
1213
+ encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
1214
+ return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
1215
+
1216
+
1217
+ def _error(code: str, message: str) -> dict[str, Any]:
1218
+ """Return a small General Augment-like error body."""
1219
+ return {
1220
+ "code": code,
1221
+ "reason": code,
1222
+ "message": message,
1223
+ "detail": {"code": code, "reason": code, "message": message},
1224
+ }
1225
+
1226
+
1227
+ def _extract_input_text(value: Any) -> str:
1228
+ """Extract readable text from common Responses input shapes."""
1229
+ if value is None:
1230
+ return ""
1231
+ if isinstance(value, str):
1232
+ return value
1233
+ if isinstance(value, list):
1234
+ return " ".join(part for item in value if (part := _extract_input_text(item)))
1235
+ if isinstance(value, dict):
1236
+ if "content" in value:
1237
+ return _extract_input_text(value["content"])
1238
+ if "text" in value:
1239
+ return str(value["text"])
1240
+ if "input_text" in value:
1241
+ return str(value["input_text"])
1242
+ return json.dumps(value, sort_keys=True, default=str)
1243
+
1244
+
1245
+ def _mock_output_text(input_text: str, user_id: str) -> str:
1246
+ """Return deterministic mock assistant text."""
1247
+ exact = EXACT_REPLY_RE.search(input_text)
1248
+ if exact:
1249
+ return exact.group(1).strip().strip('"`')
1250
+ clipped = " ".join(input_text.split())[:160] or "empty input"
1251
+ return f"Mock General Augment response for {user_id}: {clipped}"
1252
+
1253
+
1254
+ def _mock_response_text(payload: dict[str, Any], input_text: str, user_id: str) -> str:
1255
+ """Return deterministic text or schema-shaped JSON for a mock response."""
1256
+ text_format = _object_payload(_object_payload(payload.get("text")).get("format"))
1257
+ if text_format.get("type") == "json_schema":
1258
+ schema = _object_payload(text_format.get("schema"))
1259
+ value = _mock_value_for_schema(schema, input_text)
1260
+ return json.dumps(value, sort_keys=True)
1261
+ if text_format.get("type") == "json_object":
1262
+ return json.dumps({"ok": True, "label": _structured_label(input_text)}, sort_keys=True)
1263
+ return _mock_output_text(input_text, user_id)
1264
+
1265
+
1266
+ def _mock_value_for_schema(schema: dict[str, Any], input_text: str) -> Any:
1267
+ """Create a small valid-ish JSON value for common smoke-test schemas."""
1268
+ if "const" in schema:
1269
+ return schema["const"]
1270
+ enum = schema.get("enum")
1271
+ if isinstance(enum, list) and enum:
1272
+ return enum[0]
1273
+
1274
+ schema_type = schema.get("type")
1275
+ if isinstance(schema_type, list):
1276
+ schema_type = next((item for item in schema_type if item != "null"), schema_type[0])
1277
+
1278
+ if schema_type == "object" or (
1279
+ schema_type is None and isinstance(schema.get("properties"), dict)
1280
+ ):
1281
+ properties = _object_payload(schema.get("properties"))
1282
+ required = schema.get("required")
1283
+ if isinstance(required, list) and required:
1284
+ names = [str(name) for name in required if str(name) in properties]
1285
+ else:
1286
+ names = list(properties)
1287
+ return {
1288
+ name: _mock_named_value(name, _object_payload(properties.get(name)), input_text)
1289
+ for name in names
1290
+ }
1291
+ if schema_type == "array":
1292
+ return [_mock_value_for_schema(_object_payload(schema.get("items")), input_text)]
1293
+ if schema_type == "boolean":
1294
+ return True
1295
+ if schema_type == "integer":
1296
+ return 1
1297
+ if schema_type == "number":
1298
+ return 1.0
1299
+ if schema_type == "null":
1300
+ return None
1301
+ return _structured_label(input_text)
1302
+
1303
+
1304
+ def _mock_named_value(name: str, schema: dict[str, Any], input_text: str) -> Any:
1305
+ """Return a deterministic value with nicer defaults for common smoke fields."""
1306
+ normalized = name.lower().replace("-", "_")
1307
+ if normalized in {"ok", "success", "passed", "valid"} and "type" not in schema:
1308
+ return True
1309
+ if normalized in {"label", "status", "message", "summary"} and "type" not in schema:
1310
+ return _structured_label(input_text)
1311
+ return _mock_value_for_schema(schema, input_text)
1312
+
1313
+
1314
+ def _structured_label(input_text: str) -> str:
1315
+ """Return a stable label for structured mock responses."""
1316
+ exact = EXACT_REPLY_RE.search(input_text)
1317
+ if exact:
1318
+ return exact.group(1).strip().strip('"`')
1319
+ if "genaug-structured-ok" in input_text:
1320
+ return "genaug-structured-ok"
1321
+ if "genaug-smoke-ok" in input_text:
1322
+ return "genaug-smoke-ok"
1323
+ return "genaug-smoke-ok"
1324
+
1325
+
1326
+ def _display_name(value: str) -> str:
1327
+ """Create a readable display name from a slug."""
1328
+
1329
+ return value.replace("-", " ").replace("_", " ").title()
1330
+
1331
+
1332
+ def _slug(value: str) -> str:
1333
+ """Create a stable local mock slug."""
1334
+
1335
+ slug = re.sub(r"[^a-zA-Z0-9_-]+", "-", value.strip().lower()).strip("-")
1336
+ return slug or "skill"
1337
+
1338
+
1339
+ def _external_user_id(payload: dict[str, Any]) -> str:
1340
+ """Resolve app user fields accepted by the mock memory endpoints."""
1341
+ return str(payload.get("user_id") or payload.get("user") or "mock-user")
1342
+
1343
+
1344
+ def _mock_user_uuid(user_id: str) -> str:
1345
+ """Return a deterministic UUID-shaped General Augment user id."""
1346
+ return str(uuid5(NAMESPACE_URL, f"general-augment-local-mock:{user_id}"))
1347
+
1348
+
1349
+ def _optional_string(value: Any) -> str | None:
1350
+ """Return a non-empty string or None."""
1351
+ if value is None:
1352
+ return None
1353
+ text = str(value)
1354
+ return text or None
1355
+
1356
+
1357
+ def _optional_float(value: Any) -> float | None:
1358
+ """Return a float if the value is present and numeric."""
1359
+ if value is None:
1360
+ return None
1361
+ try:
1362
+ return float(value)
1363
+ except (TypeError, ValueError):
1364
+ return None
1365
+
1366
+
1367
+ def _positive_int(value: Any, *, default: int) -> int:
1368
+ """Parse a positive integer with a default."""
1369
+ try:
1370
+ parsed = int(value)
1371
+ except (TypeError, ValueError):
1372
+ return default
1373
+ return max(1, parsed)
1374
+
1375
+
1376
+ def _lexical_score(query: str, fact: str) -> float:
1377
+ """Return a deterministic lexical similarity score."""
1378
+ normalized_query = query.strip().lower()
1379
+ normalized_fact = fact.lower()
1380
+ if not normalized_query:
1381
+ return 1.0
1382
+ if normalized_query in normalized_fact:
1383
+ return 0.99
1384
+ query_terms = set(normalized_query.split())
1385
+ fact_terms = set(normalized_fact.split())
1386
+ if not query_terms:
1387
+ return 1.0
1388
+ return len(query_terms & fact_terms) / len(query_terms)
1389
+
1390
+
1391
+ if __name__ == "__main__":
1392
+ raise SystemExit(main())