lighthouse-cli 0.16.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.
- lighthouse/__init__.py +6 -0
- lighthouse/__main__.py +9 -0
- lighthouse/commands/__init__.py +0 -0
- lighthouse/commands/agents.py +310 -0
- lighthouse/commands/builds.py +85 -0
- lighthouse/commands/collectors.py +147 -0
- lighthouse/commands/connect.py +717 -0
- lighthouse/commands/connections.py +201 -0
- lighthouse/commands/evals.py +198 -0
- lighthouse/commands/kb.py +224 -0
- lighthouse/commands/language.py +56 -0
- lighthouse/commands/limits.py +61 -0
- lighthouse/commands/login.py +207 -0
- lighthouse/commands/models.py +61 -0
- lighthouse/commands/runs.py +157 -0
- lighthouse/commands/spend.py +84 -0
- lighthouse/commands/stack.py +103 -0
- lighthouse/commands/terms.py +50 -0
- lighthouse/commands/whoami.py +80 -0
- lighthouse/config.py +141 -0
- lighthouse/credentials.py +233 -0
- lighthouse/device.py +124 -0
- lighthouse/exits.py +78 -0
- lighthouse/flags.py +61 -0
- lighthouse/http.py +355 -0
- lighthouse/i18n.py +250 -0
- lighthouse/locales/ar.json +343 -0
- lighthouse/locales/de.json +343 -0
- lighthouse/locales/en.json +343 -0
- lighthouse/locales/es.json +343 -0
- lighthouse/locales/fr.json +343 -0
- lighthouse/locales/ga.json +343 -0
- lighthouse/locales/it.json +343 -0
- lighthouse/locales/ja.json +343 -0
- lighthouse/locales/nl.json +343 -0
- lighthouse/locales/pt.json +343 -0
- lighthouse/locales/zh-Hans.json +343 -0
- lighthouse/locales/zh-Hant.json +343 -0
- lighthouse/main.py +229 -0
- lighthouse/out.py +169 -0
- lighthouse/py.typed +0 -0
- lighthouse/sealing.py +448 -0
- lighthouse_cli-0.16.0.dist-info/METADATA +23 -0
- lighthouse_cli-0.16.0.dist-info/RECORD +49 -0
- lighthouse_cli-0.16.0.dist-info/WHEEL +5 -0
- lighthouse_cli-0.16.0.dist-info/entry_points.txt +2 -0
- lighthouse_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
- lighthouse_cli-0.16.0.dist-info/licenses/NOTICE +2 -0
- lighthouse_cli-0.16.0.dist-info/top_level.txt +1 -0
lighthouse/__init__.py
ADDED
lighthouse/__main__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copyright 2026 Atlantic Agentic Labs. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""`python -m lighthouse`, the same as the `lighthouse` script."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from lighthouse.main import run
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
run()
|
|
File without changes
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# Copyright 2026 Atlantic Agentic Labs. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""`lighthouse agents`: list, inspect, scaffold, deploy, read logs, roll back.
|
|
4
|
+
|
|
5
|
+
lighthouse agents # every agent, with its state
|
|
6
|
+
lighthouse agents show research:summary
|
|
7
|
+
lighthouse agents init my-agent # scaffold agent.toml here
|
|
8
|
+
lighthouse agents logs research:summary --limit 20
|
|
9
|
+
lighthouse agents deploy research:summary --version 1.4.0 --wait
|
|
10
|
+
lighthouse agents rollback research:summary --to 1.3.0
|
|
11
|
+
|
|
12
|
+
**`init` is local and complete.** It writes an `agent.toml` next to you, with
|
|
13
|
+
every field the pipeline will ask for and a comment on each: instructions, the
|
|
14
|
+
knowledge bases, the tools, the model policy, the guardrails, the approvals and
|
|
15
|
+
the evaluation set. Nothing is sent anywhere, so it works before you have
|
|
16
|
+
signed in, and the file is what `deploy` will read.
|
|
17
|
+
|
|
18
|
+
**`logs` is complete**: it reads the agent's runs (`GET /api/v1/runs`) and,
|
|
19
|
+
with `--trace`, one run's spans, which is what anybody actually wants when an
|
|
20
|
+
agent misbehaves.
|
|
21
|
+
|
|
22
|
+
**`deploy` and `rollback` make a released version current** (the `deploy`
|
|
23
|
+
scope: sign in with `lighthouse login --scope "telemetry:read agents:read
|
|
24
|
+
deploy"`). Each sends `release.add` for the agent and version; rolling back is
|
|
25
|
+
the same with an earlier version (`--to`, or the last version that passed its
|
|
26
|
+
gate before the current one). The service decides what happens:
|
|
27
|
+
|
|
28
|
+
- the version has passed its evaluation gate and your policy allows it: it
|
|
29
|
+
goes live, as you, and `--wait` follows the command until your deployment
|
|
30
|
+
says `applied` (or refused, or failed: exit 1);
|
|
31
|
+
- otherwise nothing is sent: a **pending request** waits for a person to
|
|
32
|
+
approve it in LightHouse with their second factor (sign-off happens in
|
|
33
|
+
LightHouse and nowhere else, `docs/adr/0002-approvals-only-in-the-platform.md`).
|
|
34
|
+
The command prints where. `--wait` waits for that decision and then for
|
|
35
|
+
the command; rejected or expired is exit 1, still waiting at
|
|
36
|
+
`--wait-timeout` is exit 5.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import time
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
from lighthouse.exits import FAILED, RETRY, USAGE, CliError
|
|
46
|
+
from lighthouse.flags import inherited
|
|
47
|
+
from lighthouse.main import Command, Context
|
|
48
|
+
from lighthouse.out import Out
|
|
49
|
+
|
|
50
|
+
SCAFFOLD = '''# An agent for LightHouse. `lighthouse agents deploy` reads this file.
|
|
51
|
+
#
|
|
52
|
+
# Everything here is checked before an agent runs: the model policy against the
|
|
53
|
+
# model library's evidence scores, the knowledge bases against what this tenant
|
|
54
|
+
# has indexed, the tools against the Cedar gate, and the evaluation set against
|
|
55
|
+
# its own thresholds. A field left out takes the tenant's default, which the
|
|
56
|
+
# portal's Deployments page shows.
|
|
57
|
+
name = "{name}"
|
|
58
|
+
description = "One sentence: what this agent is for."
|
|
59
|
+
|
|
60
|
+
# Plain words. Retrieved text is evidence, never instruction: an agent is told
|
|
61
|
+
# here what to do, and the gateway enforces what it may touch regardless.
|
|
62
|
+
instructions = """
|
|
63
|
+
You are …
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# Knowledge bases this agent may read, by index name (`lighthouse kb`).
|
|
67
|
+
knowledge_bases = []
|
|
68
|
+
|
|
69
|
+
# Tools, by name. Write access is a separate, named tool and sits behind an
|
|
70
|
+
# approval; an agent never gains one by asking.
|
|
71
|
+
tools = []
|
|
72
|
+
|
|
73
|
+
[model]
|
|
74
|
+
# A task class, so the router picks the cheapest model that meets the bar
|
|
75
|
+
# (see https://docs.atlanticagenticlabs.com/). Pin one with `id` instead if you
|
|
76
|
+
# must, and the run map will say you did.
|
|
77
|
+
task = "summarise"
|
|
78
|
+
# id = "…"
|
|
79
|
+
max_output_tokens = 2048
|
|
80
|
+
|
|
81
|
+
[guardrails]
|
|
82
|
+
# A daily cap in the tenant's billing currency. The gateway enforces it in
|
|
83
|
+
# code the model cannot reach.
|
|
84
|
+
daily_spend_cap = 5.0
|
|
85
|
+
refuse_outside_knowledge_bases = true
|
|
86
|
+
|
|
87
|
+
[approvals]
|
|
88
|
+
# Anything that acts outside LightHouse waits for a person. Sign-off is in
|
|
89
|
+
# LightHouse and nowhere else.
|
|
90
|
+
required_for = ["write", "send", "pay"]
|
|
91
|
+
|
|
92
|
+
[evals]
|
|
93
|
+
# The set this agent must pass before a version can be deployed. Paths are
|
|
94
|
+
# relative to this file.
|
|
95
|
+
set = "evals/"
|
|
96
|
+
threshold = 0.8
|
|
97
|
+
'''
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _configure(ap: argparse.ArgumentParser) -> None:
|
|
101
|
+
sub = ap.add_subparsers(dest="agents_command", metavar="<subcommand>")
|
|
102
|
+
sub.add_parser("list", parents=[inherited()], help="every agent (the default)")
|
|
103
|
+
show = sub.add_parser("show", parents=[inherited()], help="one agent's configuration and last runs")
|
|
104
|
+
show.add_argument("agent", help="pack:card, e.g. research:summary")
|
|
105
|
+
init = sub.add_parser("init", parents=[inherited()], help="scaffold agent.toml in this directory")
|
|
106
|
+
init.add_argument("name", help="the agent's name")
|
|
107
|
+
init.add_argument("--file", default="agent.toml", help="where to write it")
|
|
108
|
+
init.add_argument("--force", action="store_true", help="overwrite an existing file")
|
|
109
|
+
logs = sub.add_parser("logs", parents=[inherited()], help="an agent's runs, and one run's steps")
|
|
110
|
+
logs.add_argument("agent")
|
|
111
|
+
logs.add_argument("--limit", type=int, default=20)
|
|
112
|
+
logs.add_argument("--status", help="only runs with this status")
|
|
113
|
+
logs.add_argument("--trace", metavar="RUN_ID_HASH", help="one run's spans instead")
|
|
114
|
+
deploy = sub.add_parser("deploy", parents=[inherited()],
|
|
115
|
+
help="make a released version of an agent current (deploy scope)")
|
|
116
|
+
deploy.add_argument("agent", help="pack:card, e.g. research:summary")
|
|
117
|
+
deploy.add_argument("--version", required=True, help="the released version to make current")
|
|
118
|
+
_release_args(deploy)
|
|
119
|
+
rollback = sub.add_parser("rollback", parents=[inherited()],
|
|
120
|
+
help="go back to an earlier released version (deploy scope)")
|
|
121
|
+
rollback.add_argument("agent", help="pack:card")
|
|
122
|
+
rollback.add_argument("--to", default=None,
|
|
123
|
+
help="the version to go back to (default: the last one that passed "
|
|
124
|
+
"its gate before the current one)")
|
|
125
|
+
_release_args(rollback)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _release_args(ap: argparse.ArgumentParser) -> None:
|
|
129
|
+
ap.add_argument("--deployment", help="the deployment, where the tenant has more than one")
|
|
130
|
+
ap.add_argument("--reason", default=None, help="why, for the audit log")
|
|
131
|
+
ap.add_argument("--wait", action="store_true",
|
|
132
|
+
help="wait until the deployment answers (and, if it waits for approval, "
|
|
133
|
+
"for the decision)")
|
|
134
|
+
ap.add_argument("--wait-timeout", type=float, default=900.0, dest="wait_timeout",
|
|
135
|
+
help="seconds to wait at most (default 900); then exit 5")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _init(ctx: Context) -> dict[str, Any]:
|
|
139
|
+
target = Path(ctx.args.file)
|
|
140
|
+
if target.exists() and not ctx.args.force:
|
|
141
|
+
raise CliError(f"{target} already exists", error="exists",
|
|
142
|
+
hint="pass --force to overwrite it")
|
|
143
|
+
target.write_text(SCAFFOLD.format(name=ctx.args.name))
|
|
144
|
+
return {"ok": True, "wrote": str(target), "agent": ctx.args.name,
|
|
145
|
+
"next": ["edit it", "lighthouse evals run <pack:card> --cap 5 --wait",
|
|
146
|
+
"lighthouse agents deploy <pack:card> --version <version> --wait"]}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _logs(ctx: Context) -> dict[str, Any]:
|
|
150
|
+
client = ctx.client()
|
|
151
|
+
if ctx.args.trace:
|
|
152
|
+
return {"ok": True, "agent": ctx.args.agent,
|
|
153
|
+
**client.get(f"/api/v1/runs/{ctx.args.trace}/trace")}
|
|
154
|
+
pack, _, card = str(ctx.args.agent).partition(":")
|
|
155
|
+
rows = list(client.each("/api/v1/runs", "runs", limit=ctx.args.limit,
|
|
156
|
+
pack=pack, card=card or None, status=ctx.args.status))
|
|
157
|
+
return {"ok": True, "agent": ctx.args.agent, "runs": rows}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def agents(ctx: Context) -> dict[str, Any]:
|
|
161
|
+
which = getattr(ctx.args, "agents_command", None) or "list"
|
|
162
|
+
if which == "init":
|
|
163
|
+
return _init(ctx)
|
|
164
|
+
if which == "logs":
|
|
165
|
+
return _logs(ctx)
|
|
166
|
+
if which == "deploy":
|
|
167
|
+
return _release(ctx, str(ctx.args.version), "deploy",
|
|
168
|
+
ctx.args.reason or "deployed from the lighthouse CLI")
|
|
169
|
+
if which == "rollback":
|
|
170
|
+
return _release(ctx, ctx.args.to or _previous(ctx), "rollback",
|
|
171
|
+
ctx.args.reason or "rolled back from the lighthouse CLI")
|
|
172
|
+
if which == "show":
|
|
173
|
+
agent = ctx.args.agent
|
|
174
|
+
if not agent:
|
|
175
|
+
raise CliError("which agent?", code=USAGE, error="usage")
|
|
176
|
+
return {"ok": True, **ctx.client().get(f"/api/v1/agents/{agent}")}
|
|
177
|
+
return {"ok": True, **ctx.client().get_all("/api/v1/agents", "items")}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
FINAL = ("applied", "refused", "failed", "expired")
|
|
181
|
+
POLL_S = 3.0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _previous(ctx: Context) -> str:
|
|
185
|
+
"""The version to roll back to: the newest release that passed its gate
|
|
186
|
+
and is not the one running now."""
|
|
187
|
+
a = ctx.client().get(f"/api/v1/agents/{ctx.args.agent}")
|
|
188
|
+
current = a.get("version")
|
|
189
|
+
for r in a.get("releases") or []:
|
|
190
|
+
if r.get("gate_passed") and r.get("version") and r.get("version") != current:
|
|
191
|
+
return str(r["version"])
|
|
192
|
+
raise CliError(f"{ctx.args.agent} has no earlier version that passed its gate to go back to",
|
|
193
|
+
code=USAGE, error="no_earlier_version", hint="name one with --to")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def wait_for(ctx: Context, read: Any, done: Any, what: str) -> dict[str, Any]:
|
|
197
|
+
"""Poll `read()` until `done(doc)`, every POLL_S, for at most
|
|
198
|
+
--wait-timeout seconds (exit 5 after that)."""
|
|
199
|
+
deadline = time.monotonic() + float(getattr(ctx.args, "wait_timeout", 900.0) or 900.0)
|
|
200
|
+
tries = max(1, int(float(getattr(ctx.args, "wait_timeout", 900.0) or 900.0) / POLL_S) + 1)
|
|
201
|
+
sleep = ctx.sleep or time.sleep
|
|
202
|
+
doc: dict[str, Any] = read()
|
|
203
|
+
for _ in range(tries):
|
|
204
|
+
if done(doc):
|
|
205
|
+
return doc
|
|
206
|
+
if time.monotonic() > deadline:
|
|
207
|
+
break
|
|
208
|
+
sleep(POLL_S)
|
|
209
|
+
doc = read()
|
|
210
|
+
if done(doc):
|
|
211
|
+
return doc
|
|
212
|
+
raise CliError(f"still waiting for {what}", code=RETRY, error="still_waiting",
|
|
213
|
+
hint="run it again with --wait later, or look in the portal")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _follow(ctx: Context, agent: str, command: dict[str, Any]) -> dict[str, Any]:
|
|
217
|
+
cid = command["command_id"]
|
|
218
|
+
client = ctx.client()
|
|
219
|
+
got = wait_for(ctx, lambda: client.get(f"/api/v1/agents/{agent}/commands/{cid}"),
|
|
220
|
+
lambda d: d.get("status") in FINAL, "your deployment to answer")
|
|
221
|
+
if got.get("status") != "applied":
|
|
222
|
+
raise CliError(f"your deployment did not make it current: {got.get('status')}"
|
|
223
|
+
+ (f" ({got.get('reason')})" if got.get("reason") else ""),
|
|
224
|
+
code=FAILED, error=f"command_{got.get('status')}",
|
|
225
|
+
command=cid, reason=got.get("reason"))
|
|
226
|
+
return got
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _release(ctx: Context, version: str, action: str, reason: str) -> dict[str, Any]:
|
|
230
|
+
agent = str(ctx.args.agent)
|
|
231
|
+
body: dict[str, Any] = {"type": "release.add",
|
|
232
|
+
"params": {"version": version, "reason": reason}}
|
|
233
|
+
if ctx.args.deployment:
|
|
234
|
+
body["deployment"] = ctx.args.deployment
|
|
235
|
+
got = ctx.client().post(f"/api/v1/agents/{agent}/commands", body)
|
|
236
|
+
mode = (got.get("deploy") or {}).get("mode", "live")
|
|
237
|
+
out: dict[str, Any] = {"ok": True, "action": action, "agent": agent, "version": version,
|
|
238
|
+
"mode": mode}
|
|
239
|
+
if mode == "pending":
|
|
240
|
+
out.update({"status": "pending_approval", "request_id": got.get("request_id"),
|
|
241
|
+
"why": got.get("why"), "why_words": got.get("why_words"),
|
|
242
|
+
"approve_url": got.get("approve_url")})
|
|
243
|
+
if not ctx.args.wait:
|
|
244
|
+
return out
|
|
245
|
+
rid = got["request_id"]
|
|
246
|
+
client = ctx.client()
|
|
247
|
+
decided = wait_for(ctx, lambda: client.get(f"/api/v1/deploy-requests/{rid}"),
|
|
248
|
+
lambda d: d.get("status") != "pending", "a person to approve it")
|
|
249
|
+
if decided.get("status") != "approved":
|
|
250
|
+
raise CliError(f"the deploy request was {decided.get('status')}"
|
|
251
|
+
+ (f": {decided.get('decision_reason')}"
|
|
252
|
+
if decided.get("decision_reason") else ""),
|
|
253
|
+
code=FAILED, error=f"request_{decided.get('status')}",
|
|
254
|
+
request_id=rid)
|
|
255
|
+
command = decided.get("command") or {"command_id": decided.get("command_id")}
|
|
256
|
+
out["approved_by"] = decided.get("decided_by")
|
|
257
|
+
else:
|
|
258
|
+
command = got
|
|
259
|
+
out.update({"command": command.get("command_id"), "status": command.get("status")})
|
|
260
|
+
if ctx.args.wait and command.get("command_id"):
|
|
261
|
+
final = _follow(ctx, agent, command)
|
|
262
|
+
out.update({"status": final["status"], "detail": final.get("detail")})
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _show(out: Out, body: dict[str, Any]) -> None:
|
|
267
|
+
if body.get("action") in ("deploy", "rollback"):
|
|
268
|
+
out.pairs([("agent", body["agent"]), ("version", body["version"]),
|
|
269
|
+
("status", body.get("status") or ""),
|
|
270
|
+
("command", body.get("command") or "")])
|
|
271
|
+
if body["mode"] == "pending":
|
|
272
|
+
out.note(f"waiting for approval in LightHouse: "
|
|
273
|
+
f"{body.get('why_words') or body.get('why')}")
|
|
274
|
+
if body.get("approve_url"):
|
|
275
|
+
out.note(f"a person approves it at {body['approve_url']}")
|
|
276
|
+
elif body.get("status") == "applied":
|
|
277
|
+
out.done(f"version {body['version']} is current")
|
|
278
|
+
else:
|
|
279
|
+
out.note("sent to your deployment; `--wait` follows it until it answers")
|
|
280
|
+
return
|
|
281
|
+
if "wrote" in body:
|
|
282
|
+
out.done(f"wrote {body['wrote']}")
|
|
283
|
+
for step in body["next"]:
|
|
284
|
+
out.note(f" then: {step}")
|
|
285
|
+
return
|
|
286
|
+
if "spans" in body:
|
|
287
|
+
out.table(["span", "kind", "starts at (ms)", "took (ms)", "status"],
|
|
288
|
+
[[s.get("name"), s.get("kind"), s.get("start_ms"),
|
|
289
|
+
s.get("duration_ms"), s.get("status")] for s in body["spans"]])
|
|
290
|
+
return
|
|
291
|
+
if "runs" in body:
|
|
292
|
+
out.table(["run", "started", "status", "took (ms)", "attempts", "trigger"],
|
|
293
|
+
[[r.get("run_id_hash"), r.get("started_at"), r.get("status"),
|
|
294
|
+
r.get("duration_ms"), r.get("attempts"), r.get("trigger")]
|
|
295
|
+
for r in body["runs"]])
|
|
296
|
+
return
|
|
297
|
+
rows = body.get("items")
|
|
298
|
+
if rows is None:
|
|
299
|
+
out.pairs([(k, v) for k, v in body.items()
|
|
300
|
+
if k != "ok" and not isinstance(v, (list, dict))])
|
|
301
|
+
return
|
|
302
|
+
out.table(["agent", "pack", "state", "models", "runs (month)", "estimated cost"],
|
|
303
|
+
[[a.get("id"), a.get("pack"), a.get("state"), ", ".join(a.get("models") or []),
|
|
304
|
+
(a.get("month") or {}).get("runs"), (a.get("month") or {}).get("cost_usd")]
|
|
305
|
+
for a in rows])
|
|
306
|
+
out.note("every cost figure is estimated")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
COMMANDS = (Command("agents", "list, inspect, scaffold, deploy and roll back agents",
|
|
310
|
+
_configure, agents, _show),)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Copyright 2026 Atlantic Agentic Labs. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""`lighthouse builds`: agents built with the agent builder, and where each is.
|
|
4
|
+
|
|
5
|
+
lighthouse builds
|
|
6
|
+
lighthouse builds --status publish_requested
|
|
7
|
+
lighthouse builds show 5f0c...e1 # one, with its scores and estimated cost
|
|
8
|
+
lighthouse builds --json | jq '.proposals[] | select(.status == "evaluated")'
|
|
9
|
+
|
|
10
|
+
Reads `GET /api/v1/builder/proposals` (contract 1.24). Read only, on purpose:
|
|
11
|
+
an agent is drafted, sent to your deployment, evaluated and approved in the
|
|
12
|
+
portal (Agents, then Build an agent), because approval needs a person with a
|
|
13
|
+
second factor and a delegated token is never that.
|
|
14
|
+
|
|
15
|
+
Every cost shown is **estimated**, from your deployment's own price card.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from lighthouse.exits import USAGE, CliError
|
|
23
|
+
from lighthouse.flags import inherited
|
|
24
|
+
from lighthouse.main import Command, Context
|
|
25
|
+
from lighthouse.out import Out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _configure(ap: argparse.ArgumentParser) -> None:
|
|
29
|
+
ap.add_argument("--status", help="only proposals at this step, e.g. publish_requested")
|
|
30
|
+
sub = ap.add_subparsers(dest="builds_command", metavar="<subcommand>")
|
|
31
|
+
show = sub.add_parser("show", parents=[inherited()],
|
|
32
|
+
help="one proposal, with its scores and estimated cost")
|
|
33
|
+
show.add_argument("proposal", help="the proposal's id (32 hex characters)")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def builds(ctx: Context) -> dict[str, Any]:
|
|
37
|
+
client = ctx.client()
|
|
38
|
+
if getattr(ctx.args, "builds_command", None) == "show":
|
|
39
|
+
pid = str(ctx.args.proposal)
|
|
40
|
+
if len(pid) != 32 or any(c not in "0123456789abcdef" for c in pid):
|
|
41
|
+
raise CliError("a proposal id is 32 hex characters", code=USAGE, error="usage")
|
|
42
|
+
return {"ok": True, "proposal": client.get(f"/api/v1/builder/proposals/{pid}")}
|
|
43
|
+
params = {"status": ctx.args.status} if getattr(ctx.args, "status", None) else {}
|
|
44
|
+
return {"ok": True, **client.get("/api/v1/builder/proposals", **params)}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _money(v: Any) -> str:
|
|
48
|
+
return "-" if v is None else f"${float(v):,.2f}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _scores(p: dict[str, Any]) -> str:
|
|
52
|
+
ev = p.get("evaluation") or {}
|
|
53
|
+
if ev.get("passed") is None:
|
|
54
|
+
return "not yet"
|
|
55
|
+
held = sum(1 for c in (ev.get("redteam") or {}).values() if c.get("passed"))
|
|
56
|
+
return ("passed" if ev["passed"] else "failed") + \
|
|
57
|
+
f" (red team {held}/{len(ev.get('redteam') or {})})"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _show(out: Out, body: dict[str, Any]) -> None:
|
|
61
|
+
one = body.get("proposal")
|
|
62
|
+
if one is not None:
|
|
63
|
+
est = one.get("estimate") or {}
|
|
64
|
+
ev = one.get("evaluation") or {}
|
|
65
|
+
out.pairs([("agent", one.get("agent")), ("status", one.get("status")),
|
|
66
|
+
("deployment", one.get("deployment")), ("version", one.get("version")),
|
|
67
|
+
("template", one.get("template")),
|
|
68
|
+
("problems", ", ".join(one.get("problems") or []) or "-"),
|
|
69
|
+
("retrieval", (ev.get("retrieval") or {}).get("metrics") or "not yet"),
|
|
70
|
+
("evaluation", _scores(one)),
|
|
71
|
+
("estimated per run", _money(est.get("per_run_usd"))),
|
|
72
|
+
("estimated per month", _money(est.get("per_month_usd")))])
|
|
73
|
+
out.note("costs are estimated, from your deployment's own price card")
|
|
74
|
+
return
|
|
75
|
+
rows = body.get("proposals") or []
|
|
76
|
+
out.table(["proposal", "agent", "status", "evaluation", "estimated / month"],
|
|
77
|
+
[[p.get("proposal_id", "")[:12], p.get("agent"), p.get("status"), _scores(p),
|
|
78
|
+
_money((p.get("estimate") or {}).get("per_month_usd"))] for p in rows])
|
|
79
|
+
if body.get("waiting"):
|
|
80
|
+
out.note(f"{body['waiting']} waiting for approval in the portal (Agents, then Build "
|
|
81
|
+
"an agent)")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
COMMANDS = (Command("builds", "agents built with the agent builder, and where each is",
|
|
85
|
+
_configure, builds, _show),)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Copyright 2026 Atlantic Agentic Labs. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""`lighthouse collectors`: ask Atlantic for a collector, and follow the request.
|
|
4
|
+
|
|
5
|
+
lighthouse collectors requests # your tenant's requests and their stage
|
|
6
|
+
lighthouse collectors request --vendor "Invented CRM" --product "Sales Suite" \\
|
|
7
|
+
--reads "Accounts and opportunities" --cloud aws [--kind crm] [--access read] \\
|
|
8
|
+
[--urgency normal] [--docs-url https://...]
|
|
9
|
+
|
|
10
|
+
**A request becomes a public issue** in Atlantic's collectors backlog. The
|
|
11
|
+
issue carries the fields below and "Requested by N LightHouse clients", never
|
|
12
|
+
your organisation's name, a person or a domain; the service refuses text that
|
|
13
|
+
names your organisation (`names_your_organisation`, exit 2). If another
|
|
14
|
+
client already asked for the same vendor and product, your request joins
|
|
15
|
+
that issue instead of opening another.
|
|
16
|
+
|
|
17
|
+
**Asking needs the handler or client_admin role**; reading needs viewer. Both
|
|
18
|
+
use the CLI's own sign-in (`lighthouse login`). At a terminal, a field left
|
|
19
|
+
off the command line is asked for; under `--no-input`, or with standard input
|
|
20
|
+
not a terminal, a missing required field is exit 7.
|
|
21
|
+
|
|
22
|
+
**The stage** is the backlog's: requested, triage, needs-onboarding,
|
|
23
|
+
approved, building, certifying, certified or declined. Approved, certified
|
|
24
|
+
and declined also arrive in Notices.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import sys
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from lighthouse.exits import USAGE, CliError, NeedsAPerson
|
|
33
|
+
from lighthouse.flags import inherited
|
|
34
|
+
from lighthouse.http import Refused
|
|
35
|
+
from lighthouse.main import Command, Context
|
|
36
|
+
from lighthouse.out import Out
|
|
37
|
+
|
|
38
|
+
PATH = "/api/v1/collector-requests"
|
|
39
|
+
ACCESS = ("read", "read_write")
|
|
40
|
+
CLOUDS = ("aws", "azure", "gcp", "on-premises", "atlantic-hosted")
|
|
41
|
+
URGENCY = ("low", "normal", "high")
|
|
42
|
+
# (flag, body field, question, required)
|
|
43
|
+
FIELDS = (("vendor", "vendor", "Vendor (who makes the system)", True),
|
|
44
|
+
("product", "product", "Product", True),
|
|
45
|
+
("reads", "reads", "What should it read?", True),
|
|
46
|
+
("cloud", "deployment_cloud", f"Deployment cloud ({', '.join(CLOUDS)})", True),
|
|
47
|
+
("kind", "kind", "Kind, e.g. crm or tickets (Enter to skip)", False),
|
|
48
|
+
("docs_url", "docs_url", "Public API documentation link (Enter to skip)", False))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _configure(ap: argparse.ArgumentParser) -> None:
|
|
52
|
+
sub = ap.add_subparsers(dest="collectors_command", metavar="<subcommand>")
|
|
53
|
+
sub.add_parser("requests", parents=[inherited()],
|
|
54
|
+
help="your tenant's collector requests and their stage (the default)")
|
|
55
|
+
req = sub.add_parser("request", parents=[inherited()],
|
|
56
|
+
help="ask Atlantic for a collector (handler or client_admin)")
|
|
57
|
+
req.add_argument("--vendor", help="who makes the system, e.g. 'Invented CRM'")
|
|
58
|
+
req.add_argument("--product", help="the product, e.g. 'Sales Suite'")
|
|
59
|
+
req.add_argument("--reads", help="what it should read")
|
|
60
|
+
req.add_argument("--cloud", choices=CLOUDS, help="where your deployment runs")
|
|
61
|
+
req.add_argument("--kind", help="what it reads, as a slug: crm, tickets, wiki, ...")
|
|
62
|
+
req.add_argument("--access", choices=ACCESS, default="read",
|
|
63
|
+
help="read (the default) or read_write")
|
|
64
|
+
req.add_argument("--urgency", choices=URGENCY, default="normal",
|
|
65
|
+
help="low, normal (the default) or high")
|
|
66
|
+
req.add_argument("--docs-url", dest="docs_url",
|
|
67
|
+
help="the vendor's public API documentation, https://")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _row(r: dict[str, Any]) -> dict[str, Any]:
|
|
71
|
+
return {"id": r.get("id"), "vendor": r.get("vendor"), "product": r.get("product"),
|
|
72
|
+
"kind": r.get("kind"), "stage": r.get("stage"), "issue_iid": r.get("issue_iid"),
|
|
73
|
+
"issue_url": r.get("issue_url"), "request_count": r.get("request_count"),
|
|
74
|
+
"created_at": r.get("created_at"), "updated_at": r.get("updated_at")}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _list(ctx: Context) -> dict[str, Any]:
|
|
78
|
+
body = ctx.client().get(PATH) or {}
|
|
79
|
+
return {"ok": True, "requests": [_row(r) for r in body.get("requests") or []]}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _interactive(ctx: Context) -> bool:
|
|
83
|
+
if getattr(ctx.args, "no_input", False):
|
|
84
|
+
return False
|
|
85
|
+
return bool((ctx.stdin or sys.stdin).isatty())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _ask(ctx: Context) -> dict[str, Any]:
|
|
89
|
+
a = ctx.args
|
|
90
|
+
body: dict[str, Any] = {"access": a.access, "urgency": a.urgency}
|
|
91
|
+
for flag, field, question, required in FIELDS:
|
|
92
|
+
value = getattr(a, flag, None)
|
|
93
|
+
if value is None:
|
|
94
|
+
if not _interactive(ctx):
|
|
95
|
+
if required:
|
|
96
|
+
raise NeedsAPerson(f"--{flag.replace('_', '-')}")
|
|
97
|
+
continue
|
|
98
|
+
if flag == "vendor":
|
|
99
|
+
ctx.out.note("The request becomes a public issue: do not name your "
|
|
100
|
+
"organisation, its clients or any person.")
|
|
101
|
+
value = (ctx.reader or input)(f"{question}: ").strip() or None
|
|
102
|
+
if value is None and required:
|
|
103
|
+
raise CliError(f"{question} is required", code=USAGE, error="invalid")
|
|
104
|
+
if value is not None:
|
|
105
|
+
body[field] = value
|
|
106
|
+
try:
|
|
107
|
+
got = ctx.client().request("POST", PATH, json_body=body) or {}
|
|
108
|
+
except Refused as e:
|
|
109
|
+
if e.error in ("invalid", "bad_query", "names_your_organisation"):
|
|
110
|
+
raise CliError(e.message, code=USAGE, error=e.error,
|
|
111
|
+
hint="see `lighthouse collectors request --help`") from None
|
|
112
|
+
if e.error == "conflict":
|
|
113
|
+
raise CliError(e.message, code=e.code, error=e.error,
|
|
114
|
+
hint="`lighthouse collectors requests` shows it") from None
|
|
115
|
+
raise
|
|
116
|
+
return {"ok": True, "action": "request", **_row(got), "access": got.get("access"),
|
|
117
|
+
"deployment_cloud": got.get("deployment_cloud"), "urgency": got.get("urgency")}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def collectors(ctx: Context) -> dict[str, Any]:
|
|
121
|
+
if getattr(ctx.args, "collectors_command", None) == "request":
|
|
122
|
+
return _ask(ctx)
|
|
123
|
+
return _list(ctx)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _issue(r: dict[str, Any]) -> str:
|
|
127
|
+
return f"#{r['issue_iid']}" if r.get("issue_iid") else "not sent yet"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _show(out: Out, body: dict[str, Any]) -> None:
|
|
131
|
+
if body.get("action") == "request":
|
|
132
|
+
out.pairs([("request", body["id"]), ("collector", f"{body['vendor']} {body['product']}"),
|
|
133
|
+
("stage", body["stage"]), ("issue", _issue(body)),
|
|
134
|
+
("clients asking", body["request_count"] if body["request_count"] else "-")])
|
|
135
|
+
out.note("Atlantic triages it in the collectors backlog; approved, certified and "
|
|
136
|
+
"declined arrive in Notices" if body.get("issue_iid") else
|
|
137
|
+
"recorded; it is sent to the collectors backlog shortly")
|
|
138
|
+
return
|
|
139
|
+
out.table(["request", "collector", "stage", "issue", "clients asking", "asked"],
|
|
140
|
+
[[r["id"], f"{r['vendor']} {r['product']}", r["stage"], _issue(r),
|
|
141
|
+
r["request_count"] if r["request_count"] else "-", r["created_at"] or "-"]
|
|
142
|
+
for r in body["requests"]])
|
|
143
|
+
out.note("`lighthouse collectors request --vendor ... --product ...` asks for another")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
COMMANDS = (Command("collectors", "ask Atlantic for a collector, and follow your requests",
|
|
147
|
+
_configure, collectors, _show),)
|