ghost-api-cli 0.2.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.
- ghost_api/__init__.py +3 -0
- ghost_api/__main__.py +5 -0
- ghost_api/adopt.py +173 -0
- ghost_api/agent/__init__.py +0 -0
- ghost_api/agent/orchestrator.py +413 -0
- ghost_api/agent/prompts.py +149 -0
- ghost_api/agent/tools.py +84 -0
- ghost_api/analysis/__init__.py +0 -0
- ghost_api/analysis/analyzer.py +130 -0
- ghost_api/analysis/detectors.py +188 -0
- ghost_api/analysis/exposure.py +80 -0
- ghost_api/analysis/graphql.py +67 -0
- ghost_api/analysis/models.py +64 -0
- ghost_api/analysis/schema.py +109 -0
- ghost_api/analysis/url_grouping.py +217 -0
- ghost_api/api.py +264 -0
- ghost_api/audit.py +78 -0
- ghost_api/browser/__init__.py +0 -0
- ghost_api/browser/auth_state.py +172 -0
- ghost_api/browser/driver.py +273 -0
- ghost_api/browser/egress.py +183 -0
- ghost_api/browser/gate.py +127 -0
- ghost_api/browser/install.py +28 -0
- ghost_api/browser/snapshot.py +221 -0
- ghost_api/capture/__init__.py +0 -0
- ghost_api/capture/filters.py +255 -0
- ghost_api/capture/har.py +148 -0
- ghost_api/capture/models.py +41 -0
- ghost_api/capture/personal.py +188 -0
- ghost_api/capture/recorder.py +181 -0
- ghost_api/cli/__init__.py +44 -0
- ghost_api/cli/_common.py +219 -0
- ghost_api/cli/client.py +487 -0
- ghost_api/cli/data.py +492 -0
- ghost_api/cli/explore.py +379 -0
- ghost_api/cli/manage.py +437 -0
- ghost_api/config.py +132 -0
- ghost_api/drift.py +344 -0
- ghost_api/drive.py +211 -0
- ghost_api/errors.py +106 -0
- ghost_api/events.py +213 -0
- ghost_api/exitcodes.py +21 -0
- ghost_api/export/__init__.py +71 -0
- ghost_api/export/confidence.py +45 -0
- ghost_api/export/html.py +41 -0
- ghost_api/export/openapi.py +201 -0
- ghost_api/export/report.py +227 -0
- ghost_api/export/site_data.py +196 -0
- ghost_api/export/tabular.py +88 -0
- ghost_api/export/viewer/report.html +286 -0
- ghost_api/generator/__init__.py +221 -0
- ghost_api/generator/cassettes.py +80 -0
- ghost_api/generator/models_gen.py +144 -0
- ghost_api/generator/naming.py +174 -0
- ghost_api/generator/package.py +197 -0
- ghost_api/generator/python_gen.py +172 -0
- ghost_api/generator/spec.py +486 -0
- ghost_api/generator/templates/client.py.j2 +518 -0
- ghost_api/generator/templates/client.test.ts.j2 +106 -0
- ghost_api/generator/templates/client.ts.j2 +339 -0
- ghost_api/generator/templates/models.py.j2 +45 -0
- ghost_api/generator/templates/package_readme.md.j2 +111 -0
- ghost_api/generator/templates/pyproject.toml.j2 +22 -0
- ghost_api/generator/templates/test_client.py.j2 +105 -0
- ghost_api/generator/ts_gen.py +232 -0
- ghost_api/integrations/__init__.py +0 -0
- ghost_api/integrations/claude_code/commands/ghost-data.md +10 -0
- ghost_api/integrations/claude_code/commands/ghost-diff.md +10 -0
- ghost_api/integrations/claude_code/commands/ghost-explore.md +10 -0
- ghost_api/integrations/claude_code/skills/ghost-api/SKILL.md +46 -0
- ghost_api/integrations/setup.py +84 -0
- ghost_api/llm/__init__.py +0 -0
- ghost_api/llm/doctor.py +60 -0
- ghost_api/llm/gateway.py +476 -0
- ghost_api/llm/providers.py +124 -0
- ghost_api/memory/__init__.py +0 -0
- ghost_api/memory/knowledge.py +157 -0
- ghost_api/memory/store.py +883 -0
- ghost_api/net.py +235 -0
- ghost_api/netguard.py +203 -0
- ghost_api/pipeline.py +745 -0
- ghost_api/probe/__init__.py +0 -0
- ghost_api/probe/prober.py +311 -0
- ghost_api/probe/replay.py +313 -0
- ghost_api/project.py +186 -0
- ghost_api/py.typed +0 -0
- ghost_api/runtime.py +397 -0
- ghost_api/serve/__init__.py +101 -0
- ghost_api/serve/explorer_mcp.py +305 -0
- ghost_api/serve/mcp.py +121 -0
- ghost_api/serve/mock.py +130 -0
- ghost_api/serve/rest.py +132 -0
- ghost_api/site.py +84 -0
- ghost_api/specdiff.py +122 -0
- ghost_api/ui/__init__.py +0 -0
- ghost_api/ui/keys.py +134 -0
- ghost_api/ui/repl.py +453 -0
- ghost_api/ui/terminal.py +573 -0
- ghost_api/verify/__init__.py +0 -0
- ghost_api/verify/classify.py +84 -0
- ghost_api/verify/repair.py +40 -0
- ghost_api/verify/runner.py +110 -0
- ghost_api/verify/verifier.py +291 -0
- ghost_api/watch.py +313 -0
- ghost_api/web/__init__.py +1 -0
- ghost_api/web/server.py +690 -0
- ghost_api/web/static/assets/Endpoints-zPnjlcM8.js +46 -0
- ghost_api/web/static/assets/Run-D85zWHey.js +91 -0
- ghost_api/web/static/assets/Settings-D0aWHFAt.js +11 -0
- ghost_api/web/static/assets/Site-a88shEmP.js +71 -0
- ghost_api/web/static/assets/Watches-CqZWIa9G.js +16 -0
- ghost_api/web/static/assets/circle-x-BcTE_1tr.js +11 -0
- ghost_api/web/static/assets/index-Ds2e5B9f.js +141 -0
- ghost_api/web/static/assets/index-xQpyIBbG.css +1 -0
- ghost_api/web/static/assets/minus-C2xqP1eQ.js +6 -0
- ghost_api/web/static/assets/refresh-cw-CxdR_8Oi.js +6 -0
- ghost_api/web/static/favicon.svg +1 -0
- ghost_api/web/static/index.html +26 -0
- ghost_api/workflows.py +203 -0
- ghost_api_cli-0.2.0.dist-info/METADATA +1019 -0
- ghost_api_cli-0.2.0.dist-info/RECORD +125 -0
- ghost_api_cli-0.2.0.dist-info/WHEEL +5 -0
- ghost_api_cli-0.2.0.dist-info/entry_points.txt +2 -0
- ghost_api_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- ghost_api_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""What Ghost API remembers about a site between runs (Stage 8).
|
|
2
|
+
|
|
3
|
+
- Endpoints (with probe results and statuses) and their redacted samples: already in
|
|
4
|
+
the store from earlier stages.
|
|
5
|
+
- Lessons: short notes for future runs. "auto" lessons are written by code (which action
|
|
6
|
+
revealed which endpoint), "llm" lessons by one optional LLM call per run.
|
|
7
|
+
- The naming plan, so regenerating a client needs no LLM call.
|
|
8
|
+
|
|
9
|
+
Lessons come from web content and LLM output, so they are treated as untrusted hints:
|
|
10
|
+
length-capped, single-line, redacted, and labelled as possibly outdated in prompts.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field
|
|
21
|
+
|
|
22
|
+
from ghost_api.analysis.models import Endpoint
|
|
23
|
+
from ghost_api.capture.filters import REDACTED, redact_text
|
|
24
|
+
from ghost_api.capture.models import CapturedRequest, RawRequest
|
|
25
|
+
from ghost_api.generator.naming import NamingPlan
|
|
26
|
+
from ghost_api.llm.gateway import LLMError, LLMGateway
|
|
27
|
+
from ghost_api.memory.store import Store
|
|
28
|
+
|
|
29
|
+
MAX_LESSON_CHARS = 200
|
|
30
|
+
MAX_LESSONS_IN_PROMPT = 6
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DomainMemory:
|
|
35
|
+
domain: str
|
|
36
|
+
goal: str
|
|
37
|
+
start_url: str
|
|
38
|
+
run_id: int # the run whose endpoints these are
|
|
39
|
+
endpoints: list[Endpoint]
|
|
40
|
+
lessons: list[str] = field(default_factory=list)
|
|
41
|
+
plan: NamingPlan | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_memory(store: Store, domain: str) -> DomainMemory | None:
|
|
45
|
+
run_id = store.latest_endpoint_run(domain)
|
|
46
|
+
runs = [r for r in store.list_runs(domain) if not r.goal.startswith("refresh:")]
|
|
47
|
+
if run_id is None or not runs:
|
|
48
|
+
return None
|
|
49
|
+
plan_json = store.load_naming_plan(domain)
|
|
50
|
+
plan = None
|
|
51
|
+
if plan_json:
|
|
52
|
+
try:
|
|
53
|
+
plan = NamingPlan.model_validate_json(plan_json)
|
|
54
|
+
except ValueError:
|
|
55
|
+
plan = None
|
|
56
|
+
return DomainMemory(
|
|
57
|
+
domain=domain,
|
|
58
|
+
goal=runs[0].goal,
|
|
59
|
+
start_url=runs[0].start_url,
|
|
60
|
+
run_id=run_id,
|
|
61
|
+
endpoints=store.current_endpoints(domain),
|
|
62
|
+
lessons=store.lessons_for(domain),
|
|
63
|
+
plan=plan,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def clean_lesson(text: str) -> str:
|
|
68
|
+
"""One line, no secrets, bounded length."""
|
|
69
|
+
text = re.sub(r"\s+", " ", redact_text(text)).strip()
|
|
70
|
+
return text[:MAX_LESSON_CHARS]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def memory_prompt(endpoints: Sequence[Endpoint], lessons: Sequence[str]) -> str:
|
|
74
|
+
"""Compact text for the agent's prompt, or '' when there is nothing to say."""
|
|
75
|
+
if not endpoints and not lessons:
|
|
76
|
+
return ""
|
|
77
|
+
lines = ["Earlier runs on this site (may be outdated; the current page is the truth):"]
|
|
78
|
+
for ep in list(endpoints)[:15]:
|
|
79
|
+
status = f" [{ep.status}]" if ep.status not in ("", "discovered") else ""
|
|
80
|
+
lines.append(f"- known endpoint: {ep.method} {ep.host}{ep.path_template}{status}")
|
|
81
|
+
for lesson in list(lessons)[:MAX_LESSONS_IN_PROMPT]:
|
|
82
|
+
lines.append(f"- note: {lesson}")
|
|
83
|
+
return "\n".join(lines)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def discovery_lessons(steps: Sequence[tuple[str, dict[str, object], list[str]]]) -> list[str]:
|
|
87
|
+
"""Deterministic lessons: which action revealed which endpoint.
|
|
88
|
+
|
|
89
|
+
`steps` are (action, args, new endpoint labels) tuples from the agent's log.
|
|
90
|
+
"""
|
|
91
|
+
lessons = []
|
|
92
|
+
for action, args, new in steps:
|
|
93
|
+
if not new or action in ("finish", "ask_user"):
|
|
94
|
+
continue
|
|
95
|
+
shown = ", ".join(f"{k}={v!r}" for k, v in args.items() if k != "element_id")
|
|
96
|
+
how = f"{action}({shown})" if shown else action
|
|
97
|
+
for label in new:
|
|
98
|
+
lessons.append(clean_lesson(f"{label} appeared after {how}"))
|
|
99
|
+
return lessons
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Lessons(BaseModel):
|
|
103
|
+
lessons: list[str] = Field(
|
|
104
|
+
description="1 to 3 short, specific tips for exploring this site next time.",
|
|
105
|
+
max_length=3,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
LESSONS_SYSTEM = """\
|
|
110
|
+
You review a finished exploration of a website's hidden API and write 1 to 3 short,
|
|
111
|
+
specific tips that would make the next exploration of the same site faster: which page
|
|
112
|
+
or action reveals which endpoint, what to avoid, what needs a login. No generic advice."""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def write_lessons(
|
|
116
|
+
llm: LLMGateway,
|
|
117
|
+
goal: str,
|
|
118
|
+
status: str,
|
|
119
|
+
steps: Sequence[tuple[str, dict[str, object], str]],
|
|
120
|
+
endpoints: Sequence[Endpoint],
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""One LLM call; returns [] if the LLM is unavailable."""
|
|
123
|
+
history = "\n".join(
|
|
124
|
+
f"{i}. {action}({json.dumps(args)}) -> {result[:160]}"
|
|
125
|
+
for i, (action, args, result) in enumerate(steps, 1)
|
|
126
|
+
)
|
|
127
|
+
found = "\n".join(f"- {e.method} {e.host}{e.path_template}" for e in endpoints) or "(none)"
|
|
128
|
+
prompt = f"GOAL: {goal}\nOUTCOME: {status}\n\nSTEPS:\n{history}\n\nENDPOINTS FOUND:\n{found}"
|
|
129
|
+
try:
|
|
130
|
+
reply = llm.ask(
|
|
131
|
+
[{"role": "system", "content": LESSONS_SYSTEM}, {"role": "user", "content": prompt}],
|
|
132
|
+
response_model=Lessons,
|
|
133
|
+
)
|
|
134
|
+
except LLMError:
|
|
135
|
+
return []
|
|
136
|
+
if reply.parsed is None:
|
|
137
|
+
return []
|
|
138
|
+
return [c for c in (clean_lesson(t) for t in reply.parsed.lessons) if c]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def rebuild_raw(captured: CapturedRequest, required_headers: Sequence[str]) -> RawRequest | None:
|
|
142
|
+
"""A replayable request from a stored (redacted) sample, when nothing needed is missing.
|
|
143
|
+
|
|
144
|
+
Returns None if a redacted value would be needed: a secret in the URL or body, or a
|
|
145
|
+
redacted header that probing found to be required. Fresh traffic is needed then.
|
|
146
|
+
"""
|
|
147
|
+
if REDACTED in captured.url or (captured.req_body and REDACTED in captured.req_body):
|
|
148
|
+
return None
|
|
149
|
+
required = {h.lower() for h in required_headers}
|
|
150
|
+
headers = {}
|
|
151
|
+
for name, value in captured.req_headers.items():
|
|
152
|
+
if value == REDACTED:
|
|
153
|
+
if name.lower() in required:
|
|
154
|
+
return None
|
|
155
|
+
continue # e.g. a cookie that turned out not to matter
|
|
156
|
+
headers[name] = value
|
|
157
|
+
return RawRequest(captured.method, captured.url, headers, captured.req_body)
|