captchakraken 2.2.0 → 2.3.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.3.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,11 +42,18 @@ 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
 
52
59
  from .solver import CaptchaSolver, UnsupportedCaptchaError
@@ -372,6 +379,7 @@ def _handle_serve() -> bool:
372
379
  find-grid {image} -> grid_boxes | null
373
380
  grid-cell-states {a, b} -> states | {grid: null}
374
381
  grid-cell-states-fixed {a, b, grid_boxes} -> states
382
+ check-movement {a, b, threshold?} -> {has_movement: bool}
375
383
 
376
384
  Unknown cmd / malformed line -> an {ok:false} response (the process keeps
377
385
  running). EOF on stdin ends the loop cleanly. All heavy detection delegates
@@ -396,6 +404,15 @@ def _handle_serve() -> bool:
396
404
  if not grid_boxes:
397
405
  raise ValueError("empty grid_boxes")
398
406
  return _compute_grid_cell_states(req["a"], req["b"], grid_boxes)
407
+ if cmd == "check-movement":
408
+ # Freshness check for the JS solver: did the captcha frame change
409
+ # (tiles faded in / refreshed) between the screenshot we sent the
410
+ # model and now? Same primitive the one-shot `check-movement` uses.
411
+ from .image_processor import ImageProcessor
412
+
413
+ threshold = float(req.get("threshold", 0.005))
414
+ moved = ImageProcessor.detect_movement(req["a"], req["b"], threshold)
415
+ return {"has_movement": bool(moved)}
399
416
  raise ValueError(f"unknown cmd: {cmd!r}")
400
417
 
401
418
  # Signal readiness so the JS side knows imports are done before it polls.
@@ -560,9 +577,55 @@ def _handle_server_commands() -> bool:
560
577
  return True
561
578
 
562
579
 
580
+ def _handle_fetch() -> bool:
581
+ """`captchakraken fetch` — pull the latest published weights from the HF org
582
+ and refresh the vLLM serving stack, restarting a running local server so the
583
+ update takes effect. See updater.fetch for the full behavior."""
584
+ if len(sys.argv) <= 1 or sys.argv[1] not in {"fetch", "update"}:
585
+ return False
586
+
587
+ flags = set(sys.argv[2:])
588
+ unknown = flags - {"--weights-only", "--engine-only", "--no-restart", "--dry-run"}
589
+ if unknown:
590
+ print(
591
+ json.dumps({"error": f"unknown fetch flag(s): {sorted(unknown)} "
592
+ "(use --weights-only|--engine-only|--no-restart|--dry-run)"}),
593
+ file=sys.stderr,
594
+ )
595
+ sys.exit(2)
596
+ if "--weights-only" in flags and "--engine-only" in flags:
597
+ print(json.dumps({"error": "--weights-only and --engine-only are mutually exclusive"}),
598
+ file=sys.stderr)
599
+ sys.exit(2)
600
+
601
+ from . import updater
602
+
603
+ weights = "--engine-only" not in flags
604
+ engine = "--weights-only" not in flags
605
+ try:
606
+ result = updater.fetch(
607
+ weights=weights,
608
+ engine=engine,
609
+ restart="--no-restart" not in flags,
610
+ dry_run="--dry-run" in flags,
611
+ )
612
+ print(json.dumps(result))
613
+ return True
614
+ except subprocess.CalledProcessError as e:
615
+ print(json.dumps({"error": f"fetch step failed (exit {e.returncode}): "
616
+ f"{' '.join(e.cmd) if isinstance(e.cmd, list) else e.cmd}"}),
617
+ file=sys.stderr)
618
+ sys.exit(1)
619
+ except Exception as e:
620
+ print(json.dumps({"error": str(e)}), file=sys.stderr)
621
+ sys.exit(1)
622
+
623
+
563
624
  def main():
564
625
  if _handle_server_commands():
565
626
  return
627
+ if _handle_fetch():
628
+ return
566
629
  if _handle_movement_commands():
567
630
  return
568
631
  if _handle_move_commands():
@@ -13,12 +13,36 @@ 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 ──────────────────────────────────────────────────────
@@ -27,23 +51,81 @@ def base_url() -> str:
27
51
  return os.getenv("VLLM_BASE_URL", f"http://localhost:{port()}/v1")
28
52
 
29
53
 
54
+ def state_dir() -> Path:
55
+ """Shared state dir for the pidfile, lockfile, server log, and credentials."""
56
+ return Path(os.getenv("CAPTCHA_KRAKEN_STATE_DIR", str(Path.home() / ".captchakraken")))
57
+
58
+
59
+ def credentials_path() -> Path:
60
+ return state_dir() / "credentials"
61
+
62
+
63
+ def _key_from_credentials_file() -> str:
64
+ """Key written by the MCP server's signup flow (or a future `login` command).
65
+
66
+ Hosted-API onboarding writes the key to a 0600 file rather than returning it
67
+ through the agent transcript, so it never lands in an LLM context window.
68
+ Reading it here is what lets a solve authenticate with NO env vars set.
69
+
70
+ Deliberately tolerant of a bare token, an env-file line, or surrounding
71
+ quotes: the file is written by a *separate* tool, and a format mismatch here
72
+ would surface as a baffling 401 rather than an obvious parse error.
73
+ Returns "" whenever the file is missing, unreadable, or has no usable line.
74
+ """
75
+ try:
76
+ text = credentials_path().read_text(encoding="utf-8")
77
+ except OSError:
78
+ return ""
79
+
80
+ for line in text.splitlines():
81
+ line = line.strip()
82
+ if not line or line.startswith("#"):
83
+ continue
84
+ if "=" in line:
85
+ name, _, value = line.partition("=")
86
+ # An env-file with unrelated keys must not yield a bogus token.
87
+ if name.strip() not in {"CAPTCHA_KRAKEN_API_KEY", "VLLM_API_KEY"}:
88
+ continue
89
+ line = value.strip()
90
+ return line.strip("'\"")
91
+
92
+ return ""
93
+
94
+
30
95
  def api_key() -> str:
31
- return os.getenv("CAPTCHA_KRAKEN_API_KEY") or os.getenv("VLLM_API_KEY") or "EMPTY"
96
+ """Bearer token, in precedence order.
97
+
98
+ Env first so an explicit override always wins, then the credentials file so
99
+ the MCP/hosted path needs no env plumbing at all, then "EMPTY" (which leaves
100
+ a self-hosted local vLLM open, as before).
101
+ """
102
+ return (
103
+ os.getenv("CAPTCHA_KRAKEN_API_KEY")
104
+ or os.getenv("VLLM_API_KEY")
105
+ or _key_from_credentials_file()
106
+ or "EMPTY"
107
+ )
32
108
 
33
109
 
34
110
  # ── Model identity (all overridable — the CLI itself is model-agnostic) ──────
35
111
  def base_model() -> str:
36
- return os.getenv("CAPTCHA_BASE_MODEL", "Qwen/Qwen3.5-9B")
112
+ return os.getenv("CAPTCHA_BASE_MODEL", pinned()["base_model"])
37
113
 
38
114
 
39
115
  def lora_adapter() -> str:
40
116
  """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")
117
+ return os.getenv("CAPTCHA_LORA_ADAPTER", pinned()["lora_adapter"])
118
+
119
+
120
+ def lora_revision() -> str:
121
+ """Git revision of the adapter repo to serve — pinning this is what makes
122
+ 'the adapter this release was tested against' a reproducible statement."""
123
+ return os.getenv("CAPTCHA_LORA_REVISION", pinned()["lora_revision"])
42
124
 
43
125
 
44
126
  def lora_name() -> str:
45
127
  """The served LoRA name the client sends as the `model` field."""
46
- return os.getenv("CAPTCHA_LORA_NAME", "captcha")
128
+ return os.getenv("CAPTCHA_LORA_NAME", pinned()["lora_name"])
47
129
 
48
130
 
49
131
  # ── Local vLLM server knobs ─────────────────────────────────────────────────