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.
- general_augment_cli-0.1.0.dist-info/METADATA +180 -0
- general_augment_cli-0.1.0.dist-info/RECORD +42 -0
- general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
- general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
- platform_cli/__init__.py +5 -0
- platform_cli/branding.py +27 -0
- platform_cli/client.py +179 -0
- platform_cli/commands/__init__.py +1 -0
- platform_cli/commands/approvals.py +150 -0
- platform_cli/commands/auth.py +96 -0
- platform_cli/commands/billing.py +143 -0
- platform_cli/commands/channels.py +212 -0
- platform_cli/commands/deploy.py +72 -0
- platform_cli/commands/dev.py +38 -0
- platform_cli/commands/doctor.py +170 -0
- platform_cli/commands/identity.py +433 -0
- platform_cli/commands/init.py +55 -0
- platform_cli/commands/integrate.py +94 -0
- platform_cli/commands/keys.py +116 -0
- platform_cli/commands/logs.py +43 -0
- platform_cli/commands/mcp.py +258 -0
- platform_cli/commands/memory.py +316 -0
- platform_cli/commands/mock.py +30 -0
- platform_cli/commands/model_providers.py +226 -0
- platform_cli/commands/observability.py +174 -0
- platform_cli/commands/onboarding.py +72 -0
- platform_cli/commands/projects.py +302 -0
- platform_cli/commands/skills.py +116 -0
- platform_cli/commands/smoke.py +280 -0
- platform_cli/commands/status.py +49 -0
- platform_cli/commands/tools.py +179 -0
- platform_cli/commands/users.py +150 -0
- platform_cli/commands/validate.py +96 -0
- platform_cli/commands/verify.py +648 -0
- platform_cli/config.py +114 -0
- platform_cli/errors.py +103 -0
- platform_cli/local_mock.py +1392 -0
- platform_cli/main.py +130 -0
- platform_cli/openapi.py +1048 -0
- platform_cli/output.py +47 -0
- platform_cli/readiness.py +176 -0
- platform_cli/runtime.py +22 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
"""Project verification command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from platform_cli import __version__
|
|
12
|
+
from platform_cli.client import encode_path_segment, resolve_project
|
|
13
|
+
from platform_cli.errors import CLIError
|
|
14
|
+
from platform_cli.output import panel, print_json, table
|
|
15
|
+
from platform_cli.readiness import build_readiness_checklist
|
|
16
|
+
from platform_cli.runtime import Runtime
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def verify(
|
|
20
|
+
ctx: typer.Context,
|
|
21
|
+
project: str = typer.Option(..., help="Project id, slug, or name."),
|
|
22
|
+
message: str = typer.Option(
|
|
23
|
+
"Reply with one short sentence confirming this General Augment project works.",
|
|
24
|
+
help="Message for the hosted agent test.",
|
|
25
|
+
),
|
|
26
|
+
user: str = typer.Option(
|
|
27
|
+
"genaug-verify-user",
|
|
28
|
+
help="Synthetic app user id for memory and agent checks.",
|
|
29
|
+
),
|
|
30
|
+
phone_e164: str = typer.Option("+15550000000", help="Synthetic E.164 user identity."),
|
|
31
|
+
channel: str = typer.Option("sms", help="Synthetic channel: sms, whatsapp, ios, or telegram."),
|
|
32
|
+
dashboard_url: str = typer.Option(
|
|
33
|
+
os.getenv("GENAUG_DASHBOARD_URL", "https://app.generalaugment.com"),
|
|
34
|
+
help="Dashboard base URL for follow-up UI checks.",
|
|
35
|
+
),
|
|
36
|
+
json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Verify a project through the CLI before checking the dashboard UI."""
|
|
39
|
+
runtime: Runtime = ctx.obj
|
|
40
|
+
with runtime.client() as client:
|
|
41
|
+
payload = build_project_verification_payload(
|
|
42
|
+
client,
|
|
43
|
+
project=project,
|
|
44
|
+
message=message,
|
|
45
|
+
user=user,
|
|
46
|
+
phone_e164=phone_e164,
|
|
47
|
+
channel=channel,
|
|
48
|
+
dashboard_url=dashboard_url,
|
|
49
|
+
)
|
|
50
|
+
if json_output:
|
|
51
|
+
print_json(payload)
|
|
52
|
+
else:
|
|
53
|
+
table(
|
|
54
|
+
f"Project Verify: {payload['project']['slug']}",
|
|
55
|
+
["Check", "Status", "Detail"],
|
|
56
|
+
[[item["name"], item["status"], item["detail"]] for item in payload["checks"]],
|
|
57
|
+
)
|
|
58
|
+
panel(
|
|
59
|
+
"Dashboard Follow-up",
|
|
60
|
+
"\n".join(f"{key}: {value}" for key, value in payload["dashboard"].items()),
|
|
61
|
+
)
|
|
62
|
+
if payload["verdict"] != "PASS":
|
|
63
|
+
failed = ", ".join(item["name"] for item in payload["checks"] if item["status"] == "FAIL")
|
|
64
|
+
raise CLIError(f"Project verification failed: {failed}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_project_verification_payload(
|
|
68
|
+
client: Any,
|
|
69
|
+
*,
|
|
70
|
+
project: str,
|
|
71
|
+
message: str,
|
|
72
|
+
user: str,
|
|
73
|
+
phone_e164: str,
|
|
74
|
+
channel: str,
|
|
75
|
+
dashboard_url: str,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""Run project acceptance checks and return a machine-readable payload."""
|
|
78
|
+
|
|
79
|
+
checks: list[dict[str, Any]] = []
|
|
80
|
+
ready = client.public("GET", "/health/ready")
|
|
81
|
+
checks.append(_check("api_ready", _health_ok(ready), _health_detail(ready)))
|
|
82
|
+
|
|
83
|
+
project_payload = resolve_project(client, project)
|
|
84
|
+
project_id = str(project_payload["id"])
|
|
85
|
+
project_slug = str(project_payload.get("slug") or project)
|
|
86
|
+
checks.append(_check("project_resolved", True, project_id))
|
|
87
|
+
identity = client.admin("GET", "/me")
|
|
88
|
+
|
|
89
|
+
keys = client.admin("GET", "/keys")
|
|
90
|
+
key_items = keys.get("items", []) if isinstance(keys, dict) else []
|
|
91
|
+
project_keys = [
|
|
92
|
+
item
|
|
93
|
+
for item in key_items
|
|
94
|
+
if isinstance(item, dict) and str(item.get("project_id") or "") == project_id
|
|
95
|
+
]
|
|
96
|
+
checks.append(
|
|
97
|
+
_check(
|
|
98
|
+
"project_api_key",
|
|
99
|
+
bool(project_keys),
|
|
100
|
+
_key_detail(project_keys),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
checks.append(
|
|
104
|
+
_project_key_execution_check(
|
|
105
|
+
client,
|
|
106
|
+
project_id=project_id,
|
|
107
|
+
user=user,
|
|
108
|
+
message=message,
|
|
109
|
+
identity=identity,
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
tools_payload = client.admin("GET", "/tools")
|
|
114
|
+
tools = tools_payload if isinstance(tools_payload, list) else tools_payload.get("items", [])
|
|
115
|
+
checks.append(_check("tool_registry", isinstance(tools, list), f"{len(tools)} tools"))
|
|
116
|
+
|
|
117
|
+
runtime_policy = client.admin(
|
|
118
|
+
"GET",
|
|
119
|
+
f"/projects/{encode_path_segment(project_id)}/runtime-policy",
|
|
120
|
+
)
|
|
121
|
+
checks.append(
|
|
122
|
+
_check(
|
|
123
|
+
"runtime_policy_model_routing",
|
|
124
|
+
_model_routing_policy_ok(runtime_policy),
|
|
125
|
+
_model_routing_policy_detail(runtime_policy),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
soul = client.admin("GET", f"/projects/{encode_path_segment(project_id)}/soul")
|
|
129
|
+
checks.append(_check("soul_visible", _soul_visible_ok(soul), _soul_detail(soul)))
|
|
130
|
+
|
|
131
|
+
skills = client.admin(
|
|
132
|
+
"GET",
|
|
133
|
+
f"/projects/{encode_path_segment(project_id)}/skills",
|
|
134
|
+
params={"limit": 100},
|
|
135
|
+
)
|
|
136
|
+
checks.append(
|
|
137
|
+
_check(
|
|
138
|
+
"skills_visible",
|
|
139
|
+
_skills_visible_ok(skills, runtime_policy),
|
|
140
|
+
_skills_detail(skills, runtime_policy),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
agent_test = client.admin(
|
|
145
|
+
"POST",
|
|
146
|
+
f"/projects/{encode_path_segment(project_id)}/test",
|
|
147
|
+
json={"message": message, "phone_e164": phone_e164, "channel": channel},
|
|
148
|
+
)
|
|
149
|
+
agent_ok = not agent_test.get("error") and bool(
|
|
150
|
+
agent_test.get("response_text") or agent_test.get("response")
|
|
151
|
+
)
|
|
152
|
+
checks.append(
|
|
153
|
+
_check(
|
|
154
|
+
"agent_test",
|
|
155
|
+
agent_ok,
|
|
156
|
+
agent_test.get("error")
|
|
157
|
+
or agent_test.get("details")
|
|
158
|
+
or str(agent_test.get("response_text") or agent_test.get("response") or ""),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
logs = client.admin(
|
|
163
|
+
"GET",
|
|
164
|
+
f"/projects/{encode_path_segment(project_id)}/logs",
|
|
165
|
+
params={"limit": 5},
|
|
166
|
+
)
|
|
167
|
+
log_items = logs.get("items", []) if isinstance(logs, dict) else []
|
|
168
|
+
checks.append(_check("logs", isinstance(log_items, list), f"{len(log_items)} recent rows"))
|
|
169
|
+
|
|
170
|
+
usage = client.admin("GET", f"/projects/{encode_path_segment(project_id)}/usage")
|
|
171
|
+
totals = usage.get("totals", {}) if isinstance(usage, dict) else {}
|
|
172
|
+
limits = usage.get("limits", {}) if isinstance(usage, dict) else {}
|
|
173
|
+
checks.append(_check("usage", isinstance(totals, dict), _usage_detail(totals)))
|
|
174
|
+
checks.append(_check("usage_limits", isinstance(limits, dict), _limits_detail(limits)))
|
|
175
|
+
|
|
176
|
+
observability = client.admin(
|
|
177
|
+
"GET",
|
|
178
|
+
f"/projects/{encode_path_segment(project_id)}/observability",
|
|
179
|
+
params={"limit": 5},
|
|
180
|
+
)
|
|
181
|
+
traces = observability.get("traces", []) if isinstance(observability, dict) else []
|
|
182
|
+
checks.append(_check("observability", isinstance(traces, list), f"{len(traces)} trace rows"))
|
|
183
|
+
|
|
184
|
+
channel_status = client.admin(
|
|
185
|
+
"GET",
|
|
186
|
+
f"/projects/{encode_path_segment(project_id)}/channels/status",
|
|
187
|
+
)
|
|
188
|
+
channel_items = channel_status.get("channels", []) if isinstance(channel_status, dict) else []
|
|
189
|
+
checks.append(
|
|
190
|
+
_check("channel_status", isinstance(channel_items, list), f"{len(channel_items)} channels")
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
verification_id = uuid.uuid4().hex[:12]
|
|
194
|
+
memory_checks = _run_memory_lifecycle(
|
|
195
|
+
client,
|
|
196
|
+
project_id=project_id,
|
|
197
|
+
user=user,
|
|
198
|
+
verification_id=verification_id,
|
|
199
|
+
)
|
|
200
|
+
checks.extend(memory_checks)
|
|
201
|
+
|
|
202
|
+
audit = client.admin(
|
|
203
|
+
"GET",
|
|
204
|
+
f"/projects/{encode_path_segment(project_id)}/audit/tool-calls",
|
|
205
|
+
params={"limit": 5},
|
|
206
|
+
)
|
|
207
|
+
audit_items = audit.get("items", []) if isinstance(audit, dict) else []
|
|
208
|
+
checks.append(
|
|
209
|
+
_check("tool_call_audit", isinstance(audit_items, list), f"{len(audit_items)} rows")
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
dashboard = _dashboard_links(dashboard_url, project_id)
|
|
213
|
+
return {
|
|
214
|
+
"cli": {"version": __version__},
|
|
215
|
+
"api": _api_version_detail(ready),
|
|
216
|
+
"auth": _auth_detail(identity),
|
|
217
|
+
"project": {
|
|
218
|
+
"id": project_id,
|
|
219
|
+
"slug": project_slug,
|
|
220
|
+
"name": project_payload.get("name"),
|
|
221
|
+
},
|
|
222
|
+
"verdict": _verdict(checks),
|
|
223
|
+
"checks": checks,
|
|
224
|
+
"readiness_checklist": build_readiness_checklist(checks, project=project_payload),
|
|
225
|
+
"runtime_policy": _runtime_policy_artifact(runtime_policy),
|
|
226
|
+
"dashboard": dashboard,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _check(name: str, passed: bool, detail: str) -> dict[str, str]:
|
|
231
|
+
"""Build a machine-readable check row."""
|
|
232
|
+
|
|
233
|
+
return {"name": name, "status": "PASS" if passed else "FAIL", "detail": detail}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _skip(name: str, detail: str) -> dict[str, str]:
|
|
237
|
+
"""Build a machine-readable skipped check row."""
|
|
238
|
+
|
|
239
|
+
return {"name": name, "status": "SKIP", "detail": detail}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _verdict(checks: list[dict[str, str]]) -> str:
|
|
243
|
+
"""Return FAIL only when a required check failed."""
|
|
244
|
+
|
|
245
|
+
return "FAIL" if any(item["status"] == "FAIL" for item in checks) else "PASS"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _health_ok(payload: object) -> bool:
|
|
249
|
+
"""Return whether a health payload is ready."""
|
|
250
|
+
|
|
251
|
+
return isinstance(payload, dict) and payload.get("status") == "ok"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _health_detail(payload: object) -> str:
|
|
255
|
+
"""Return compact health detail."""
|
|
256
|
+
|
|
257
|
+
if not isinstance(payload, dict):
|
|
258
|
+
return str(payload)
|
|
259
|
+
dependencies = [
|
|
260
|
+
f"{key}={payload[key]}"
|
|
261
|
+
for key in ("db", "redis")
|
|
262
|
+
if key in payload and payload[key] is not None
|
|
263
|
+
]
|
|
264
|
+
return ", ".join(dependencies) or str(payload.get("status"))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _api_version_detail(payload: object) -> dict[str, str]:
|
|
268
|
+
"""Return API build/version metadata for automation artifacts."""
|
|
269
|
+
|
|
270
|
+
if not isinstance(payload, dict):
|
|
271
|
+
return {}
|
|
272
|
+
return {
|
|
273
|
+
key: str(payload[key])
|
|
274
|
+
for key in ("version", "build_sha", "status")
|
|
275
|
+
if key in payload and payload[key] is not None
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _auth_detail(payload: object) -> dict[str, str]:
|
|
280
|
+
"""Return safe auth metadata for automation artifacts."""
|
|
281
|
+
|
|
282
|
+
if not isinstance(payload, dict):
|
|
283
|
+
return {}
|
|
284
|
+
detail = {
|
|
285
|
+
key: str(payload[key])
|
|
286
|
+
for key in ("auth_method", "project_id")
|
|
287
|
+
if key in payload and payload[key] is not None
|
|
288
|
+
}
|
|
289
|
+
project_ids = _identity_project_ids(payload)
|
|
290
|
+
if project_ids:
|
|
291
|
+
detail["project_ids"] = ",".join(project_ids)
|
|
292
|
+
return detail
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _project_key_execution_check(
|
|
296
|
+
client: Any,
|
|
297
|
+
*,
|
|
298
|
+
project_id: str,
|
|
299
|
+
user: str,
|
|
300
|
+
message: str,
|
|
301
|
+
identity: object,
|
|
302
|
+
) -> dict[str, str]:
|
|
303
|
+
"""Exercise `/v1/responses` when the configured CLI key is project-scoped."""
|
|
304
|
+
|
|
305
|
+
if not _identity_is_project_scoped_to(identity, project_id):
|
|
306
|
+
return _skip(
|
|
307
|
+
"project_key_execution",
|
|
308
|
+
(
|
|
309
|
+
"configured CLI key is not project-scoped to this project; "
|
|
310
|
+
"project key existence was checked, but project-key execution was not"
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
response = client.app(
|
|
314
|
+
"POST",
|
|
315
|
+
"/v1/responses",
|
|
316
|
+
json={
|
|
317
|
+
"model": "balanced",
|
|
318
|
+
"user": user,
|
|
319
|
+
"input": message,
|
|
320
|
+
"metadata": {
|
|
321
|
+
"source": "genaug-cli-verify",
|
|
322
|
+
"feature": "project_key_execution",
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
if not isinstance(response, dict):
|
|
327
|
+
return _check("project_key_execution", False, str(response))
|
|
328
|
+
response_id = str(response.get("id") or "")
|
|
329
|
+
status = str(response.get("status") or "")
|
|
330
|
+
text = _response_text(response)
|
|
331
|
+
passed = bool(response_id) and status in {"completed", "complete", ""}
|
|
332
|
+
return _check(
|
|
333
|
+
"project_key_execution",
|
|
334
|
+
passed,
|
|
335
|
+
response_id or text or status or "missing response id",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _identity_is_project_scoped_to(identity: object, project_id: str) -> bool:
|
|
340
|
+
"""Return whether the configured credential itself is scoped to this project."""
|
|
341
|
+
|
|
342
|
+
if not isinstance(identity, dict):
|
|
343
|
+
return False
|
|
344
|
+
return str(identity.get("project_id") or "") == project_id
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _identity_project_ids(identity: dict[str, Any]) -> list[str]:
|
|
348
|
+
"""Return safe project id strings from an identity payload."""
|
|
349
|
+
|
|
350
|
+
raw_project_ids = identity.get("project_ids")
|
|
351
|
+
if isinstance(raw_project_ids, list) and raw_project_ids:
|
|
352
|
+
return [str(project_id) for project_id in raw_project_ids]
|
|
353
|
+
if identity.get("project_id"):
|
|
354
|
+
return [str(identity["project_id"])]
|
|
355
|
+
return []
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _response_text(response: dict[str, Any]) -> str:
|
|
359
|
+
"""Return compact text from a Responses-compatible payload."""
|
|
360
|
+
|
|
361
|
+
if response.get("output_text"):
|
|
362
|
+
return str(response["output_text"])
|
|
363
|
+
output = response.get("output")
|
|
364
|
+
if not isinstance(output, list):
|
|
365
|
+
return ""
|
|
366
|
+
texts: list[str] = []
|
|
367
|
+
for item in output:
|
|
368
|
+
if not isinstance(item, dict):
|
|
369
|
+
continue
|
|
370
|
+
content = item.get("content")
|
|
371
|
+
if not isinstance(content, list):
|
|
372
|
+
continue
|
|
373
|
+
for block in content:
|
|
374
|
+
if isinstance(block, dict) and block.get("type") == "output_text":
|
|
375
|
+
texts.append(str(block.get("text") or ""))
|
|
376
|
+
return "\n".join(texts)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _usage_detail(totals: dict[str, Any]) -> str:
|
|
380
|
+
"""Return compact usage detail."""
|
|
381
|
+
|
|
382
|
+
turns = totals.get("agent_turns_count", 0)
|
|
383
|
+
cost = totals.get("total_cost_usd", 0)
|
|
384
|
+
return f"agent_turns={turns}, cost_usd={cost}"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _key_detail(items: list[dict[str, Any]]) -> str:
|
|
388
|
+
"""Return a compact project-key readiness summary."""
|
|
389
|
+
|
|
390
|
+
if not items:
|
|
391
|
+
return "no project-scoped API keys found"
|
|
392
|
+
names = ", ".join(str(item.get("name") or item.get("id") or "key") for item in items[:3])
|
|
393
|
+
return f"{len(items)} project key(s): {names}"
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _limits_detail(limits: dict[str, Any]) -> str:
|
|
397
|
+
"""Return compact usage-limit detail."""
|
|
398
|
+
|
|
399
|
+
turns = limits.get("agent_turns_per_day", "unknown")
|
|
400
|
+
tokens = limits.get("tokens_per_day", "unknown")
|
|
401
|
+
over_limit = limits.get("over_limit", False)
|
|
402
|
+
return f"agent_turns_per_day={turns}, tokens_per_day={tokens}, over_limit={over_limit}"
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _model_routing_policy_ok(payload: object) -> bool:
|
|
406
|
+
"""Return whether runtime policy exposes usable tenant model routing."""
|
|
407
|
+
|
|
408
|
+
if not isinstance(payload, dict):
|
|
409
|
+
return False
|
|
410
|
+
routing = payload.get("model_routing")
|
|
411
|
+
if not isinstance(routing, dict):
|
|
412
|
+
return False
|
|
413
|
+
tiers = routing.get("tiers")
|
|
414
|
+
required_tiers = ("simple", "balanced", "complex")
|
|
415
|
+
return (
|
|
416
|
+
routing.get("mode") == "tiered_complexity"
|
|
417
|
+
and routing.get("default_tier") == "balanced"
|
|
418
|
+
and routing.get("channel_parity") is True
|
|
419
|
+
and isinstance(tiers, dict)
|
|
420
|
+
and all(str(tiers.get(tier) or "") for tier in required_tiers)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _model_routing_policy_detail(payload: object) -> str:
|
|
425
|
+
"""Return a compact model-routing summary for verify output."""
|
|
426
|
+
|
|
427
|
+
if not isinstance(payload, dict):
|
|
428
|
+
return "runtime policy response was not an object"
|
|
429
|
+
routing = payload.get("model_routing")
|
|
430
|
+
if not isinstance(routing, dict):
|
|
431
|
+
return "runtime policy did not include model_routing"
|
|
432
|
+
tiers = routing.get("tiers")
|
|
433
|
+
if not isinstance(tiers, dict):
|
|
434
|
+
return "model_routing.tiers missing"
|
|
435
|
+
simple = str(tiers.get("simple") or "missing")
|
|
436
|
+
balanced = str(tiers.get("balanced") or "missing")
|
|
437
|
+
complex_model = str(tiers.get("complex") or "missing")
|
|
438
|
+
mode = str(routing.get("mode") or "missing")
|
|
439
|
+
default_tier = str(routing.get("default_tier") or "missing")
|
|
440
|
+
parity = routing.get("channel_parity")
|
|
441
|
+
return (
|
|
442
|
+
f"mode={mode}, simple={simple}, balanced={balanced}, "
|
|
443
|
+
f"complex={complex_model}, default_tier={default_tier}, channel_parity={parity}"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _soul_visible_ok(payload: object) -> bool:
|
|
448
|
+
"""Return whether SOUL content is visible through the admin API."""
|
|
449
|
+
|
|
450
|
+
return isinstance(payload, dict) and bool(str(payload.get("content") or "").strip())
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _soul_detail(payload: object) -> str:
|
|
454
|
+
"""Return compact SOUL visibility detail."""
|
|
455
|
+
|
|
456
|
+
if not isinstance(payload, dict):
|
|
457
|
+
return "SOUL response was not an object"
|
|
458
|
+
content = str(payload.get("content") or "")
|
|
459
|
+
if not content.strip():
|
|
460
|
+
return "SOUL content missing"
|
|
461
|
+
return f"{len(content)} chars"
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _skills_visible_ok(payload: object, runtime_policy: object) -> bool:
|
|
465
|
+
"""Return whether tenant skills are visible and match runtime-policy names."""
|
|
466
|
+
|
|
467
|
+
items = _skill_items(payload)
|
|
468
|
+
if items is None:
|
|
469
|
+
return False
|
|
470
|
+
listed_names = _skill_names_from_items(items)
|
|
471
|
+
runtime_names = set(_runtime_policy_skill_names(runtime_policy))
|
|
472
|
+
return runtime_names.issubset(listed_names)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _skills_detail(payload: object, runtime_policy: object) -> str:
|
|
476
|
+
"""Return compact skill visibility detail."""
|
|
477
|
+
|
|
478
|
+
items = _skill_items(payload)
|
|
479
|
+
if items is None:
|
|
480
|
+
return "skills response did not include an items list"
|
|
481
|
+
listed_names = _skill_names_from_items(items)
|
|
482
|
+
runtime_names = set(_runtime_policy_skill_names(runtime_policy))
|
|
483
|
+
missing = sorted(runtime_names - listed_names)
|
|
484
|
+
if missing:
|
|
485
|
+
return f"{len(listed_names)} listed, missing runtime policy skills: {', '.join(missing)}"
|
|
486
|
+
return f"{len(listed_names)} listed"
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _skill_items(payload: object) -> list[object] | None:
|
|
490
|
+
"""Return the skill list from an admin API response."""
|
|
491
|
+
|
|
492
|
+
if not isinstance(payload, dict):
|
|
493
|
+
return None
|
|
494
|
+
items = payload.get("items")
|
|
495
|
+
return items if isinstance(items, list) else None
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _skill_names_from_items(items: list[object]) -> set[str]:
|
|
499
|
+
"""Return skill names from list response rows."""
|
|
500
|
+
|
|
501
|
+
names: set[str] = set()
|
|
502
|
+
for item in items:
|
|
503
|
+
if isinstance(item, dict) and item.get("name"):
|
|
504
|
+
names.add(str(item["name"]))
|
|
505
|
+
return names
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _runtime_policy_skill_names(payload: object) -> list[str]:
|
|
509
|
+
"""Return runtime-policy skill names."""
|
|
510
|
+
|
|
511
|
+
if not isinstance(payload, dict):
|
|
512
|
+
return []
|
|
513
|
+
skills = payload.get("skills")
|
|
514
|
+
if not isinstance(skills, dict):
|
|
515
|
+
return []
|
|
516
|
+
names = skills.get("names")
|
|
517
|
+
if not isinstance(names, list):
|
|
518
|
+
return []
|
|
519
|
+
return [str(name) for name in names if str(name)]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _runtime_policy_artifact(payload: object) -> dict[str, Any]:
|
|
523
|
+
"""Return the secret-free runtime policy fields useful in verify artifacts."""
|
|
524
|
+
|
|
525
|
+
if not isinstance(payload, dict):
|
|
526
|
+
return {}
|
|
527
|
+
artifact: dict[str, Any] = {}
|
|
528
|
+
for key in (
|
|
529
|
+
"project_id",
|
|
530
|
+
"model_routing",
|
|
531
|
+
"tool_discovery",
|
|
532
|
+
"hermes_exposure",
|
|
533
|
+
"platform_tools",
|
|
534
|
+
"mcp",
|
|
535
|
+
"skills",
|
|
536
|
+
):
|
|
537
|
+
value = payload.get(key)
|
|
538
|
+
if value is not None:
|
|
539
|
+
artifact[key] = value
|
|
540
|
+
return artifact
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _dashboard_links(dashboard_url: str, project_id: str) -> dict[str, str]:
|
|
544
|
+
"""Build dashboard URLs for UI follow-up checks."""
|
|
545
|
+
|
|
546
|
+
base = dashboard_url.rstrip("/")
|
|
547
|
+
encoded = encode_path_segment(project_id)
|
|
548
|
+
project_root = f"{base}/dashboard/projects/{encoded}"
|
|
549
|
+
return {
|
|
550
|
+
"project": project_root,
|
|
551
|
+
"integrate": f"{project_root}/integrate",
|
|
552
|
+
"tools": f"{project_root}/tools",
|
|
553
|
+
"observability": f"{project_root}/observability",
|
|
554
|
+
"analytics": f"{project_root}/analytics",
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _run_memory_lifecycle(
|
|
559
|
+
client: Any,
|
|
560
|
+
*,
|
|
561
|
+
project_id: str,
|
|
562
|
+
user: str,
|
|
563
|
+
verification_id: str,
|
|
564
|
+
) -> list[dict[str, str]]:
|
|
565
|
+
"""Store, search, profile, and delete one synthetic memory fact."""
|
|
566
|
+
|
|
567
|
+
headers = {"X-Project-ID": project_id}
|
|
568
|
+
fact = "CLI verification user prefers concise onboarding notes."
|
|
569
|
+
checks: list[dict[str, str]] = []
|
|
570
|
+
|
|
571
|
+
stored = client.app(
|
|
572
|
+
"POST",
|
|
573
|
+
"/api/v1/agent/memory/store",
|
|
574
|
+
json={
|
|
575
|
+
"user_id": user,
|
|
576
|
+
"fact": fact,
|
|
577
|
+
"fact_type": "preference",
|
|
578
|
+
"importance_score": 0.8,
|
|
579
|
+
"source": "genaug-cli-verify",
|
|
580
|
+
"metadata": {"scenario": "project-verify", "verification_id": verification_id},
|
|
581
|
+
"idempotency_key": f"genaug-verify-{project_id}-{user}-{verification_id}",
|
|
582
|
+
},
|
|
583
|
+
headers=headers,
|
|
584
|
+
)
|
|
585
|
+
memory_id = str(stored.get("memory_id") or stored.get("id") or "")
|
|
586
|
+
checks.append(_check("memory_store", bool(memory_id), memory_id or "missing memory_id"))
|
|
587
|
+
|
|
588
|
+
search = client.app(
|
|
589
|
+
"POST",
|
|
590
|
+
"/api/v1/agent/memory/search",
|
|
591
|
+
json={
|
|
592
|
+
"user_id": user,
|
|
593
|
+
"query": "concise onboarding notes",
|
|
594
|
+
"limit": 5,
|
|
595
|
+
"min_similarity": 0,
|
|
596
|
+
"fact_type": "preference",
|
|
597
|
+
"min_importance": 0.5,
|
|
598
|
+
"source": "genaug-cli-verify",
|
|
599
|
+
},
|
|
600
|
+
headers=headers,
|
|
601
|
+
)
|
|
602
|
+
facts = search.get("facts", []) if isinstance(search, dict) else []
|
|
603
|
+
found_memory = _memory_hit_found(facts, memory_id)
|
|
604
|
+
checks.append(_check("memory_search", found_memory, f"{len(facts)} facts"))
|
|
605
|
+
|
|
606
|
+
profile = client.app(
|
|
607
|
+
"GET",
|
|
608
|
+
f"/api/v1/agent/memory/profile/{encode_path_segment(user)}",
|
|
609
|
+
headers=headers,
|
|
610
|
+
)
|
|
611
|
+
total_facts = profile.get("total_facts") if isinstance(profile, dict) else None
|
|
612
|
+
checks.append(
|
|
613
|
+
_check("memory_profile", isinstance(total_facts, int), f"total_facts={total_facts}")
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
if memory_id:
|
|
617
|
+
deleted = client.app(
|
|
618
|
+
"DELETE",
|
|
619
|
+
f"/api/v1/agent/memory/{encode_path_segment(memory_id)}",
|
|
620
|
+
params={"user_id": user},
|
|
621
|
+
headers=headers,
|
|
622
|
+
)
|
|
623
|
+
deleted_count = deleted.get("deleted_count") if isinstance(deleted, dict) else None
|
|
624
|
+
checks.append(
|
|
625
|
+
_check(
|
|
626
|
+
"memory_delete",
|
|
627
|
+
isinstance(deleted_count, int) and deleted_count >= 1,
|
|
628
|
+
f"deleted_count={deleted_count}",
|
|
629
|
+
)
|
|
630
|
+
)
|
|
631
|
+
else:
|
|
632
|
+
checks.append(_check("memory_delete", False, "skipped because memory_store failed"))
|
|
633
|
+
return checks
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _memory_hit_found(facts: object, memory_id: str) -> bool:
|
|
637
|
+
"""Return whether a memory search result includes the stored memory."""
|
|
638
|
+
|
|
639
|
+
if not isinstance(facts, list) or not facts:
|
|
640
|
+
return False
|
|
641
|
+
if not memory_id:
|
|
642
|
+
return True
|
|
643
|
+
for fact in facts:
|
|
644
|
+
if not isinstance(fact, dict):
|
|
645
|
+
continue
|
|
646
|
+
if str(fact.get("memory_id") or fact.get("id") or "") == memory_id:
|
|
647
|
+
return True
|
|
648
|
+
return False
|