captchakraken 2.2.0 → 2.4.0

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.
package/python/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ CaptchaKraken Source-Available License v1.0
2
+ Copyright (c) 2026 JobHarvest / JWriter20. All rights reserved.
3
+
4
+ This license governs use of the CaptchaKraken software in this repository and of
5
+ the associated machine-learning model weights and LoRA adapters published by the
6
+ copyright holder (collectively, the "Software"). By using the Software you agree
7
+ to these terms. If you do not agree, do not use the Software.
8
+
9
+ ================================================================================
10
+ 1. GRANT
11
+ ================================================================================
12
+ Subject to the restrictions below, you are granted a worldwide, royalty-free,
13
+ non-exclusive license to use, copy, modify, and distribute the Software, and to
14
+ incorporate it into your own products and services.
15
+
16
+ ================================================================================
17
+ 2. PERMITTED USES
18
+ ================================================================================
19
+ You MAY use the Software for:
20
+
21
+ (a) Personal, non-commercial use, research, and education.
22
+
23
+ (b) Commercial use INSIDE a larger product or service that delivers
24
+ substantial value BEYOND captcha solving itself — that is, where captcha
25
+ solving is an internal, enabling component rather than the product.
26
+
27
+ Illustrative (non-exhaustive) examples of PERMITTED commercial use:
28
+ - Web scrapers and data-collection pipelines.
29
+ - Stealth / anti-detection browsers and browser automation frameworks
30
+ that use the Software as one internal capability.
31
+ - QA, testing, and accessibility tooling.
32
+ - Any application where the captcha solve is a means to an end the user
33
+ is actually paying for.
34
+
35
+ ================================================================================
36
+ 3. PROHIBITED USES
37
+ ================================================================================
38
+ You MAY NOT, without a separate written commercial agreement from the copyright
39
+ holder:
40
+
41
+ (a) Sell, resell, rent, or otherwise offer for a fee any captcha-SOLVING
42
+ service, product, or API whose primary value is solving captchas,
43
+ where that service is powered by the Software or its model outputs.
44
+
45
+ (b) Distribute "thin wrappers" around the Software whose primary purpose is
46
+ captcha solving — including but not limited to browser extensions,
47
+ hosted endpoints, SaaS products, or CLIs that simply expose the
48
+ Software's solving capability to end users.
49
+
50
+ (c) Expose, proxy, or relay the responses or outputs of the Software's LoRA
51
+ models (e.g. bounding boxes, tile selections, click plans) through a paid
52
+ or public API as a captcha-solving service.
53
+
54
+ In short: you may BUILD WITH this Software, but you may not SELL THE SOLVE.
55
+
56
+ ================================================================================
57
+ 4. MODEL OUTPUTS
58
+ ================================================================================
59
+ Outputs produced by the model weights/adapters are subject to Sections 2 and 3
60
+ to the same extent as the Software itself. Reselling or API-relaying those
61
+ outputs as a captcha-solving service is prohibited under Section 3(c).
62
+
63
+ ================================================================================
64
+ 5. REDISTRIBUTION
65
+ ================================================================================
66
+ If you redistribute the Software (modified or not), you must retain this license
67
+ and this notice, and you must not remove or alter the restrictions in Section 3.
68
+
69
+ ================================================================================
70
+ 6. TRADEMARKS
71
+ ================================================================================
72
+ This license does not grant permission to use the trade names, trademarks, or
73
+ product names of the copyright holder, except as required to describe the origin
74
+ of the Software.
75
+
76
+ ================================================================================
77
+ 7. NO WARRANTY
78
+ ================================================================================
79
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
80
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
81
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
82
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER
83
+ IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
84
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
85
+
86
+ ================================================================================
87
+ 8. RESPONSIBLE USE
88
+ ================================================================================
89
+ You are responsible for ensuring your use of the Software complies with all
90
+ applicable laws, the terms of service of any site you interact with, and
91
+ applicable anti-fraud and computer-access statutes. The copyright holder does
92
+ not endorse using the Software to violate any third party's terms or rights.
93
+
94
+ ================================================================================
95
+ 9. COMMERCIAL LICENSING
96
+ ================================================================================
97
+ For uses prohibited under Section 3 — including selling captcha-solving as a
98
+ service — contact the copyright holder to discuss a separate commercial license.
99
+
100
+ --------------------------------------------------------------------------------
101
+ This text is a license, not legal advice. If the boundary between a permitted
102
+ "value-add" product and a prohibited "thin wrapper" is unclear for your use
103
+ case, ask by opening an issue or messaging the maintainer on GitHub:
104
+ https://github.com/JWriter20/CaptchaKraken
105
+ --------------------------------------------------------------------------------
package/python/README.md CHANGED
@@ -68,4 +68,6 @@ Everything model-specific lives in `captchakraken.config` and is env-overridable
68
68
 
69
69
  ## License
70
70
 
71
- GPL-3.0-or-later.
71
+ **CaptchaKraken Source-Available License v1.0** — see [LICENSE](./LICENSE).
72
+ Build *with* it (scrapers, stealth browsers, QA tooling); you may **not sell the
73
+ solve itself** or ship a thin wrapper (browser extension, hosted solving API).
@@ -4,17 +4,21 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "captchakraken"
7
- version = "2.2.0"
7
+ version = "2.4.0"
8
8
  description = "Self-hosted captcha solver: OpenCV grid detection + a fine-tuned Qwen3.5-9B vision LoRA served on vLLM."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
- license = { text = "GPL-3.0-or-later" }
11
+ # Source-available, NOT GPL. The CaptchaKraken Source-Available License permits
12
+ # commercial use as an internal component but prohibits selling the solve itself
13
+ # or thin-wrapper resale — see LICENSE. (GPL-3.0 would wrongly permit reselling.)
14
+ license = { file = "LICENSE" }
12
15
  authors = [{ name = "Jake Writer" }]
13
16
  keywords = ["captcha", "recaptcha", "hcaptcha", "vllm", "qwen", "computer-vision", "automation"]
14
17
  classifiers = [
15
18
  "Programming Language :: Python :: 3",
16
19
  "Operating System :: OS Independent",
17
20
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ "License :: Other/Proprietary License",
18
22
  ]
19
23
 
20
24
  # Core = the lightweight CLIENT: OpenCV grid detection + the HTTP planner that
@@ -40,14 +40,26 @@ from .overlay import add_overlays_to_image
40
40
  try: # pragma: no cover - exercised only when the serving stack is installed
41
41
  from .planner import ActionPlanner
42
42
  from .solver import CaptchaSolver, solve_captcha
43
+
44
+ # The page driver sits behind the same guard: it imports `solver`, so it has
45
+ # the same dependency floor. It needs NO browser package of its own — the
46
+ # caller supplies the Playwright-compatible page (see page_solver's module
47
+ # docstring on why we duck-type rather than import one).
48
+ from .page_solver import PageSolver, SolveResult, solve_captcha_on_page
43
49
  except ModuleNotFoundError:
44
50
  ActionPlanner = None # type: ignore[assignment,misc]
45
51
  CaptchaSolver = None # type: ignore[assignment,misc]
46
52
  solve_captcha = None # type: ignore[assignment]
53
+ PageSolver = None # type: ignore[assignment,misc]
54
+ SolveResult = None # type: ignore[assignment,misc]
55
+ solve_captcha_on_page = None # type: ignore[assignment]
47
56
 
48
57
  __all__ = [
49
58
  "CaptchaSolver",
50
59
  "solve_captcha",
60
+ "PageSolver",
61
+ "SolveResult",
62
+ "solve_captcha_on_page",
51
63
  "ActionPlanner",
52
64
  "ImageProcessor",
53
65
  "CaptchaAction",
@@ -58,4 +70,4 @@ __all__ = [
58
70
  "add_overlays_to_image",
59
71
  ]
60
72
 
61
- __version__ = "2.0.0"
73
+ __version__ = "2.3.0"
@@ -42,13 +42,21 @@ Modes:
42
42
  these — a local server auto-starts on the first solve (disable with
43
43
  CAPTCHA_KRAKEN_AUTOSTART=0). Point VLLM_BASE_URL at your own server to
44
44
  skip local management entirely.
45
+
46
+ captchakraken fetch [--weights-only|--engine-only] [--no-restart] [--dry-run]
47
+ Update in one step: pull the latest CaptchaKraken model from the HF org
48
+ (https://huggingface.co/CaptchaKraken) AND upgrade the vLLM serving
49
+ stack, then restart a running local server so it takes effect. Use this
50
+ to get new model revisions + engine fixes without re-running setup.sh.
45
51
  """
46
52
 
47
53
  import argparse
48
54
  import json
49
55
  import os
56
+ import subprocess
50
57
  import sys
51
58
 
59
+ from .errors import CaptchaKrakenAPIError
52
60
  from .solver import CaptchaSolver, UnsupportedCaptchaError
53
61
  from .timing import timed
54
62
 
@@ -372,6 +380,7 @@ def _handle_serve() -> bool:
372
380
  find-grid {image} -> grid_boxes | null
373
381
  grid-cell-states {a, b} -> states | {grid: null}
374
382
  grid-cell-states-fixed {a, b, grid_boxes} -> states
383
+ check-movement {a, b, threshold?} -> {has_movement: bool}
375
384
 
376
385
  Unknown cmd / malformed line -> an {ok:false} response (the process keeps
377
386
  running). EOF on stdin ends the loop cleanly. All heavy detection delegates
@@ -396,6 +405,15 @@ def _handle_serve() -> bool:
396
405
  if not grid_boxes:
397
406
  raise ValueError("empty grid_boxes")
398
407
  return _compute_grid_cell_states(req["a"], req["b"], grid_boxes)
408
+ if cmd == "check-movement":
409
+ # Freshness check for the JS solver: did the captcha frame change
410
+ # (tiles faded in / refreshed) between the screenshot we sent the
411
+ # model and now? Same primitive the one-shot `check-movement` uses.
412
+ from .image_processor import ImageProcessor
413
+
414
+ threshold = float(req.get("threshold", 0.005))
415
+ moved = ImageProcessor.detect_movement(req["a"], req["b"], threshold)
416
+ return {"has_movement": bool(moved)}
399
417
  raise ValueError(f"unknown cmd: {cmd!r}")
400
418
 
401
419
  # Signal readiness so the JS side knows imports are done before it polls.
@@ -560,9 +578,55 @@ def _handle_server_commands() -> bool:
560
578
  return True
561
579
 
562
580
 
581
+ def _handle_fetch() -> bool:
582
+ """`captchakraken fetch` — pull the latest published weights from the HF org
583
+ and refresh the vLLM serving stack, restarting a running local server so the
584
+ update takes effect. See updater.fetch for the full behavior."""
585
+ if len(sys.argv) <= 1 or sys.argv[1] not in {"fetch", "update"}:
586
+ return False
587
+
588
+ flags = set(sys.argv[2:])
589
+ unknown = flags - {"--weights-only", "--engine-only", "--no-restart", "--dry-run"}
590
+ if unknown:
591
+ print(
592
+ json.dumps({"error": f"unknown fetch flag(s): {sorted(unknown)} "
593
+ "(use --weights-only|--engine-only|--no-restart|--dry-run)"}),
594
+ file=sys.stderr,
595
+ )
596
+ sys.exit(2)
597
+ if "--weights-only" in flags and "--engine-only" in flags:
598
+ print(json.dumps({"error": "--weights-only and --engine-only are mutually exclusive"}),
599
+ file=sys.stderr)
600
+ sys.exit(2)
601
+
602
+ from . import updater
603
+
604
+ weights = "--engine-only" not in flags
605
+ engine = "--weights-only" not in flags
606
+ try:
607
+ result = updater.fetch(
608
+ weights=weights,
609
+ engine=engine,
610
+ restart="--no-restart" not in flags,
611
+ dry_run="--dry-run" in flags,
612
+ )
613
+ print(json.dumps(result))
614
+ return True
615
+ except subprocess.CalledProcessError as e:
616
+ print(json.dumps({"error": f"fetch step failed (exit {e.returncode}): "
617
+ f"{' '.join(e.cmd) if isinstance(e.cmd, list) else e.cmd}"}),
618
+ file=sys.stderr)
619
+ sys.exit(1)
620
+ except Exception as e:
621
+ print(json.dumps({"error": str(e)}), file=sys.stderr)
622
+ sys.exit(1)
623
+
624
+
563
625
  def main():
564
626
  if _handle_server_commands():
565
627
  return
628
+ if _handle_fetch():
629
+ return
566
630
  if _handle_movement_commands():
567
631
  return
568
632
  if _handle_move_commands():
@@ -644,6 +708,17 @@ def main():
644
708
  # checkboxes. Emit a clean error with no traceback.
645
709
  print(json.dumps({"error": str(e), "unsupported": True}), file=sys.stderr)
646
710
  sys.exit(2)
711
+ except CaptchaKrakenAPIError as e:
712
+ # The hosted API refused us and told us why. That is an answer, not a
713
+ # crash, so no traceback: a stack trace here buries the one line the
714
+ # user needs ("out of credits, top up at …") under frames from a module
715
+ # they have never opened.
716
+ #
717
+ # Exit 3, distinct from 1 (genuine failure) and 2 (unsupported puzzle),
718
+ # and the payload carries the machine-readable fields so the JS driver
719
+ # can rebuild the error rather than regex it out of prose.
720
+ print(json.dumps(e.to_payload()), file=sys.stderr)
721
+ sys.exit(3)
647
722
  except Exception as e:
648
723
  import traceback
649
724
 
@@ -13,37 +13,187 @@ The two variables MOST users ever set:
13
13
  Advanced / self-hosting overrides (only needed to change WHICH model is served):
14
14
  CAPTCHA_BASE_MODEL base weights vLLM loads (HF id or local path)
15
15
  CAPTCHA_LORA_ADAPTER LoRA applied on top (HF id or local path)
16
+ CAPTCHA_LORA_REVISION adapter git revision to serve
16
17
  CAPTCHA_LORA_NAME served adapter name the client asks for as `model`
18
+
19
+ Defaults for the model-identity values come from `pinned_model.json` (see
20
+ `pinned()`), which records the model + prompt pair this release was validated
21
+ against.
17
22
  VLLM_PORT, VLLM_GPU_MEMORY_UTILIZATION, VLLM_MAX_MODEL_LEN, VLLM_MAX_LORA_RANK
18
23
  CAPTCHA_KRAKEN_AUTOSTART=0 disable auto-starting a local server on first use
19
24
  """
20
25
 
26
+ import json
21
27
  import os
28
+ from functools import lru_cache
29
+ from pathlib import Path
30
+ from typing import Any, Dict
31
+
32
+ _PINNED_MODEL_PATH = Path(__file__).with_name("pinned_model.json")
33
+
34
+
35
+ @lru_cache(maxsize=1)
36
+ def pinned() -> Dict[str, Any]:
37
+ """The pinned model manifest shipped with this release.
38
+
39
+ Defines the (base model, adapter, serving prompts) triple this version was
40
+ validated against. Env vars still override every value below — the manifest
41
+ supplies the DEFAULT and gives CI something to assert against, so an adapter
42
+ or prompt change is a reviewable diff instead of a silent drift.
43
+ """
44
+ with _PINNED_MODEL_PATH.open(encoding="utf-8") as f:
45
+ return json.load(f)
22
46
 
23
47
 
24
48
  # ── Inference endpoint ──────────────────────────────────────────────────────
25
49
  def base_url() -> str:
26
- """OpenAI-compatible endpoint, e.g. http://localhost:8000/v1."""
27
- return os.getenv("VLLM_BASE_URL", f"http://localhost:{port()}/v1")
50
+ """OpenAI-compatible endpoint, e.g. http://localhost:8000/v1.
51
+
52
+ Precedence mirrors `api_key()` exactly — env, then the credentials file,
53
+ then the local default — because the two values are set together and a
54
+ reader who has learned one ordering should not have to learn a second.
55
+
56
+ 1. VLLM_BASE_URL. An explicit override always wins; self-hosters and the
57
+ dev environment depend on this and must not be silently redirected.
58
+ 2. The credentials file. Written by the MCP signup flow alongside the key,
59
+ so onboarding configures the endpoint in the same step and a hosted
60
+ user needs no env plumbing at all.
61
+ 3. localhost. Unchanged, and still correct: someone with no credentials
62
+ file is by definition not a hosted user, so the self-hosted default is
63
+ the right guess for them.
64
+
65
+ The bug this closes: a camoufox user who signed up through the MCP had a
66
+ valid key and no endpoint, so every solve dialled a local port with nothing
67
+ behind it and failed with connection-refused — a message that says nothing
68
+ about the fact that their requests were meant to go to api.captchakraken.com.
69
+ """
70
+ return (
71
+ os.getenv("VLLM_BASE_URL")
72
+ or _base_url_from_credentials_file()
73
+ or f"http://localhost:{port()}/v1"
74
+ )
75
+
76
+
77
+ def state_dir() -> Path:
78
+ """Shared state dir for the pidfile, lockfile, server log, and credentials."""
79
+ return Path(os.getenv("CAPTCHA_KRAKEN_STATE_DIR", str(Path.home() / ".captchakraken")))
80
+
81
+
82
+ def credentials_path() -> Path:
83
+ return state_dir() / "credentials"
84
+
85
+
86
+ # Names the credentials file may use for each value. Two spellings each, because
87
+ # the file doubles as something a user can `source` — the VLLM_* names are the
88
+ # env vars this module already honours, and the CAPTCHA_KRAKEN_* names are what
89
+ # the product calls them. Accepting both costs nothing and spares anyone the
90
+ # discovery that they picked the wrong one.
91
+ _KEY_NAMES = ("CAPTCHA_KRAKEN_API_KEY", "VLLM_API_KEY")
92
+ _BASE_URL_NAMES = ("CAPTCHA_KRAKEN_BASE_URL", "VLLM_BASE_URL")
93
+
94
+
95
+ def _read_credentials_file() -> Dict[str, str]:
96
+ """Parse the file the MCP server's signup flow writes.
97
+
98
+ Hosted-API onboarding writes the key to a 0600 file rather than returning it
99
+ through the agent transcript, so it never lands in an LLM context window.
100
+ Reading it here is what lets a solve authenticate with NO env vars set.
101
+
102
+ Returns a mapping of recognised name -> value. The special key ``""`` holds a
103
+ BARE TOKEN — a file containing nothing but `ck_live_…`, which is what the
104
+ original single-value format was and what a user hand-writing this file is
105
+ most likely to produce. Dropping that would silently break every existing
106
+ credentials file, so it is still accepted and still means "the API key".
107
+
108
+ Deliberately tolerant of quoting and comments: the file is written by a
109
+ *separate* tool, and a format mismatch here surfaces as a baffling 401
110
+ rather than an obvious parse error. Returns {} whenever the file is missing
111
+ or unreadable — the self-hosted case, which must stay silent.
112
+ """
113
+ try:
114
+ text = credentials_path().read_text(encoding="utf-8")
115
+ except OSError:
116
+ return {}
117
+
118
+ found: Dict[str, str] = {}
119
+ for line in text.splitlines():
120
+ line = line.strip()
121
+ if not line or line.startswith("#"):
122
+ continue
123
+ if "=" in line:
124
+ name, _, value = line.partition("=")
125
+ name = name.strip()
126
+ # An env-file with unrelated keys must not yield a bogus token.
127
+ if name not in _KEY_NAMES and name not in _BASE_URL_NAMES:
128
+ continue
129
+ found.setdefault(name, value.strip().strip("'\""))
130
+ elif "" not in found:
131
+ # A bare token. Only the first such line counts; later prose in a
132
+ # hand-edited file must not overwrite the credential.
133
+ found[""] = line.strip("'\"")
134
+
135
+ return found
136
+
137
+
138
+ def _first_present(values: Dict[str, str], names) -> str:
139
+ """First non-empty value among `names`, in the order given."""
140
+ for name in names:
141
+ value = values.get(name, "").strip()
142
+ if value:
143
+ return value
144
+ return ""
145
+
146
+
147
+ def _key_from_credentials_file() -> str:
148
+ """The API key from the credentials file, or "" if there isn't one."""
149
+ values = _read_credentials_file()
150
+ return _first_present(values, _KEY_NAMES) or values.get("", "").strip()
151
+
152
+
153
+ def _base_url_from_credentials_file() -> str:
154
+ """The endpoint from the credentials file, or "" if it names none.
155
+
156
+ A bare-token file deliberately yields nothing here: it carries no endpoint,
157
+ and inventing the hosted one for it would silently redirect a self-hoster
158
+ who wrote a token into that file by hand.
159
+ """
160
+ return _first_present(_read_credentials_file(), _BASE_URL_NAMES)
28
161
 
29
162
 
30
163
  def api_key() -> str:
31
- return os.getenv("CAPTCHA_KRAKEN_API_KEY") or os.getenv("VLLM_API_KEY") or "EMPTY"
164
+ """Bearer token, in precedence order.
165
+
166
+ Env first so an explicit override always wins, then the credentials file so
167
+ the MCP/hosted path needs no env plumbing at all, then "EMPTY" (which leaves
168
+ a self-hosted local vLLM open, as before).
169
+ """
170
+ return (
171
+ os.getenv("CAPTCHA_KRAKEN_API_KEY")
172
+ or os.getenv("VLLM_API_KEY")
173
+ or _key_from_credentials_file()
174
+ or "EMPTY"
175
+ )
32
176
 
33
177
 
34
178
  # ── Model identity (all overridable — the CLI itself is model-agnostic) ──────
35
179
  def base_model() -> str:
36
- return os.getenv("CAPTCHA_BASE_MODEL", "Qwen/Qwen3.5-9B")
180
+ return os.getenv("CAPTCHA_BASE_MODEL", pinned()["base_model"])
37
181
 
38
182
 
39
183
  def lora_adapter() -> str:
40
184
  """HF repo id or local path of the captcha adapter served on top of the base."""
41
- return os.getenv("CAPTCHA_LORA_ADAPTER", "CaptchaKraken/CaptchaKraken_v1")
185
+ return os.getenv("CAPTCHA_LORA_ADAPTER", pinned()["lora_adapter"])
186
+
187
+
188
+ def lora_revision() -> str:
189
+ """Git revision of the adapter repo to serve — pinning this is what makes
190
+ 'the adapter this release was tested against' a reproducible statement."""
191
+ return os.getenv("CAPTCHA_LORA_REVISION", pinned()["lora_revision"])
42
192
 
43
193
 
44
194
  def lora_name() -> str:
45
195
  """The served LoRA name the client sends as the `model` field."""
46
- return os.getenv("CAPTCHA_LORA_NAME", "captcha")
196
+ return os.getenv("CAPTCHA_LORA_NAME", pinned()["lora_name"])
47
197
 
48
198
 
49
199
  # ── Local vLLM server knobs ─────────────────────────────────────────────────