@foggy-projects/deepseek-harness-plugin 0.4.0-beta.13 → 0.4.0-beta.15

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.
@@ -18,6 +18,7 @@ import subprocess
18
18
  import sys
19
19
  import tempfile
20
20
  import time
21
+ import urllib.error
21
22
  import urllib.request
22
23
  import venv
23
24
  import zipfile
@@ -25,6 +26,8 @@ import zipfile
25
26
 
26
27
  STATE_SCHEMA = "foggy-deepseek-onboarding-install/v1"
27
28
  RUNTIME_STATE_SCHEMA = "foggy-deepseek-onboarding-runtime/v1"
29
+ RUNTIME_SETTINGS_SCHEMA = "foggy-deepseek-runtime-settings/v1"
30
+ RUNTIME_PORT_CONFLICT_SCHEMA = "foggy-deepseek-runtime-port-conflict/v1"
28
31
  ONBOARDING_STATE_SCHEMA = "foggy-deepseek-onboarding-state/v1"
29
32
  MANAGED_SKILL_SCHEMA = "foggy-managed-skill/v1"
30
33
  MANAGED_SKILL_MARKER = ".foggy-managed-skill.json"
@@ -43,6 +46,14 @@ class OnboardingError(RuntimeError):
43
46
  pass
44
47
 
45
48
 
49
+ class RuntimeSettingsError(OnboardingError):
50
+ code = "RUNTIME_SETTINGS_INVALID"
51
+
52
+
53
+ class RuntimePortUnavailableError(OnboardingError):
54
+ code = "RUNTIME_PORT_UNAVAILABLE"
55
+
56
+
46
57
  PROGRESS_SCHEMA = "foggy-deepseek-onboarding-progress/v1"
47
58
  ACTIVE_PROGRESS: "ProgressReporter | None" = None
48
59
 
@@ -206,6 +217,61 @@ def default_data_root() -> Path:
206
217
  return Path(base) / "foggy" / "deepseek-harness" if base else Path.home() / ".local" / "state" / "foggy" / "deepseek-harness"
207
218
 
208
219
 
220
+ def validate_runtime_port(value: object) -> int:
221
+ if isinstance(value, bool):
222
+ raise RuntimeSettingsError("Runtime port must be an integer between 1024 and 65535")
223
+ try:
224
+ port = int(value)
225
+ except (TypeError, ValueError) as exc:
226
+ raise RuntimeSettingsError("Runtime port must be an integer between 1024 and 65535") from exc
227
+ if str(value).strip() != str(port) or port < 1024 or port > 65535:
228
+ raise RuntimeSettingsError("Runtime port must be an integer between 1024 and 65535")
229
+ return port
230
+
231
+
232
+ def read_runtime_settings(data_root: Path, default_port: object) -> dict:
233
+ path = data_root / "runtime-settings.json"
234
+ if not path.is_file():
235
+ port = validate_runtime_port(default_port)
236
+ return {"schemaVersion": RUNTIME_SETTINGS_SCHEMA, "port": port, "source": "default", "path": str(path)}
237
+ try:
238
+ payload = json.loads(path.read_text(encoding="utf-8"))
239
+ except (OSError, json.JSONDecodeError) as exc:
240
+ raise RuntimeSettingsError(f"Runtime settings file is invalid: {path}") from exc
241
+ if payload.get("schemaVersion") != RUNTIME_SETTINGS_SCHEMA:
242
+ raise RuntimeSettingsError(f"Unexpected Runtime settings schema in {path}")
243
+ port = validate_runtime_port(payload.get("runtimePort"))
244
+ return {"schemaVersion": RUNTIME_SETTINGS_SCHEMA, "port": port, "source": "configured", "path": str(path)}
245
+
246
+
247
+ def assert_runtime_port_available(data_root: Path, port: int) -> None:
248
+ conflict_path = data_root / "last-runtime-port-conflict.json"
249
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
250
+ if os.name == "nt" and hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
251
+ probe.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
252
+ try:
253
+ # The packaged Java server uses a wildcard listener. Checking only
254
+ # 127.0.0.1 misses Windows portproxy entries bound to another local address.
255
+ probe.bind(("0.0.0.0", port))
256
+ except OSError as exc:
257
+ message = (
258
+ f"Runtime port {port} is unavailable. Another application or Windows port proxy "
259
+ "is already listening. Choose a different port in Foggy plugin settings or release "
260
+ "the port before retrying."
261
+ )
262
+ conflict = {
263
+ "schemaVersion": RUNTIME_PORT_CONFLICT_SCHEMA,
264
+ "detectedAt": now_utc(),
265
+ "port": port,
266
+ "bindAddress": "0.0.0.0",
267
+ "runtimeUrl": f"http://127.0.0.1:{port}",
268
+ "message": message,
269
+ }
270
+ atomic_json(conflict_path, conflict)
271
+ raise RuntimePortUnavailableError(message) from exc
272
+ conflict_path.unlink(missing_ok=True)
273
+
274
+
209
275
  def normalized(path: str | Path) -> Path:
210
276
  return Path(path).expanduser().resolve(strict=False)
211
277
 
@@ -853,7 +919,7 @@ def validate_connection(payload: dict) -> dict:
853
919
  if payload.get("schemaVersion") != CONNECTION_SCHEMA:
854
920
  raise OnboardingError(f"connection schemaVersion must be {CONNECTION_SCHEMA}")
855
921
  allowed = {
856
- "schemaVersion", "name", "type", "jdbcUrl", "username", "passwordEnv",
922
+ "schemaVersion", "name", "type", "jdbcUrl", "username", "password", "passwordEnv",
857
923
  "opaqueProfileId", "opaqueRevision",
858
924
  "profile", "namespace", "schemas", "modelsDir", "evidenceDir", "readOnlyRecommended",
859
925
  }
@@ -865,6 +931,7 @@ def validate_connection(payload: dict) -> dict:
865
931
  for name in required:
866
932
  if not isinstance(payload.get(name), str) or not payload[name].strip():
867
933
  raise OnboardingError(f"connection.{name} must be a non-empty string")
934
+ password = payload.get("password")
868
935
  password_env = payload.get("passwordEnv")
869
936
  jdbc_url = None
870
937
  if opaque:
@@ -872,15 +939,19 @@ def validate_connection(payload: dict) -> dict:
872
939
  raise OnboardingError("connection.opaqueProfileId must be an opaque Foggy profile ID")
873
940
  if not isinstance(payload.get("opaqueRevision"), str) or not OPAQUE_REVISION_PATTERN.fullmatch(payload["opaqueRevision"]):
874
941
  raise OnboardingError("connection.opaqueRevision must be a sha256 revision")
875
- exposed = sorted(name for name in ("jdbcUrl", "username", "passwordEnv") if name in payload)
942
+ exposed = sorted(name for name in ("jdbcUrl", "username", "password", "passwordEnv") if name in payload)
876
943
  if exposed:
877
944
  raise OnboardingError(f"Opaque connection plans must not contain: {', '.join(exposed)}")
878
945
  else:
946
+ if password is not None and not isinstance(password, str):
947
+ raise OnboardingError("connection.password must be a string")
879
948
  if password_env is not None and (not isinstance(password_env, str) or not ENV_NAME_PATTERN.fullmatch(password_env)):
880
949
  raise OnboardingError("connection.passwordEnv must be an environment variable name")
950
+ if password is not None and password_env is not None:
951
+ raise OnboardingError("Provide either connection.password or connection.passwordEnv, not both")
881
952
  jdbc_url = payload["jdbcUrl"].strip()
882
953
  if re.search(r"(?i)(?:password|passwd|pwd)\s*=", jdbc_url) or re.search(r"//[^/@:]+:[^/@]+@", jdbc_url):
883
- raise OnboardingError("Do not embed passwords in jdbcUrl; use passwordEnv")
954
+ raise OnboardingError("Do not embed passwords in jdbcUrl; use password or passwordEnv")
884
955
  schemas = payload.get("schemas", [])
885
956
  if not isinstance(schemas, list) or any(not isinstance(item, str) or not item.strip() for item in schemas):
886
957
  raise OnboardingError("connection.schemas must be an array of non-empty strings")
@@ -908,6 +979,7 @@ def validate_connection(payload: dict) -> dict:
908
979
  else:
909
980
  result["jdbcUrl"] = jdbc_url
910
981
  result["username"] = payload.get("username")
982
+ result["password"] = password
911
983
  result["passwordEnv"] = password_env
912
984
  if payload.get("profile") is not None:
913
985
  result["profile"] = safe_profile(payload["profile"])
@@ -915,11 +987,21 @@ def validate_connection(payload: dict) -> dict:
915
987
  result["evidenceDir"] = payload["evidenceDir"].strip()
916
988
  if result["type"] not in {"sqlite", "mysql", "postgres", "postgresql"}:
917
989
  raise OnboardingError("Initial onboarding supports sqlite, mysql, postgres, and postgresql")
918
- if not opaque and result["type"] != "sqlite" and not result["passwordEnv"]:
919
- raise OnboardingError("Non-SQLite connections require passwordEnv")
920
990
  return result
921
991
 
922
992
 
993
+ def persisted_connection(connection: dict) -> dict:
994
+ """Return the resumable connection contract without an inline development password."""
995
+ persisted = {key: value for key, value in connection.items() if key != "password"}
996
+ persisted["credentialMode"] = (
997
+ "inline-development" if connection.get("password") is not None
998
+ else "agent-environment" if connection.get("passwordEnv")
999
+ else "opaque-profile" if connection.get("connectionMode") == "opaque-profile"
1000
+ else "none"
1001
+ )
1002
+ return persisted
1003
+
1004
+
923
1005
  def validate_semantic_plan(payload: dict) -> dict:
924
1006
  if payload.get("schemaVersion") != SEMANTIC_PLAN_SCHEMA:
925
1007
  raise OnboardingError(f"semantic plan schemaVersion must be {SEMANTIC_PLAN_SCHEMA}")
@@ -1556,12 +1638,9 @@ def runtime_start_command(args: argparse.Namespace) -> dict:
1556
1638
  }
1557
1639
  existing.unlink()
1558
1640
  progress.update("runtime-preflight", 1, "Checking port and Runtime workspace", percent=10)
1559
- port = args.port or int(versions["defaults"]["port"])
1560
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
1561
- try:
1562
- probe.bind(("127.0.0.1", port))
1563
- except OSError as exc:
1564
- raise OnboardingError(f"Port {port} is not available") from exc
1641
+ configured_runtime = read_runtime_settings(data_root, versions["defaults"]["port"])
1642
+ port = validate_runtime_port(args.port) if args.port is not None else configured_runtime["port"]
1643
+ assert_runtime_port_available(data_root, port)
1565
1644
  work_dir = data_root / "runtime"
1566
1645
  work_dir.mkdir(parents=True, exist_ok=True)
1567
1646
  launcher_dir = normalized(state["launcher"]["path"])
@@ -1718,6 +1797,64 @@ def cli_json(install_state: dict, runtime_state: dict, namespace: str, command:
1718
1797
  return payload
1719
1798
 
1720
1799
 
1800
+ def runtime_api_json(
1801
+ runtime_state: dict,
1802
+ namespace: str,
1803
+ method: str,
1804
+ path: str,
1805
+ body: dict | None,
1806
+ label: str,
1807
+ timeout: int = 60,
1808
+ ) -> dict:
1809
+ """Call the public Runtime API for development inputs not yet exposed by the pinned CLI."""
1810
+ base_url = str(runtime_state["runtimeUrl"]).rstrip("/")
1811
+ request_body = json.dumps(body).encode("utf-8") if body is not None else None
1812
+ headers = {
1813
+ "Accept": "application/json",
1814
+ "Content-Type": "application/json",
1815
+ "X-NS": namespace,
1816
+ }
1817
+ auth_code = os.environ.get("FOGGY_RUNTIME_API_AUTH_CODE")
1818
+ if auth_code:
1819
+ headers["X-Foggy-Runtime-Code"] = auth_code
1820
+ request = urllib.request.Request(
1821
+ f"{base_url}{path}",
1822
+ data=request_body,
1823
+ headers=headers,
1824
+ method=method,
1825
+ )
1826
+ try:
1827
+ with urllib.request.urlopen(request, timeout=timeout) as response:
1828
+ raw = response.read().decode("utf-8", errors="replace")
1829
+ except urllib.error.HTTPError as exc:
1830
+ raw = exc.read().decode("utf-8", errors="replace")
1831
+ try:
1832
+ failed = json.loads(raw)
1833
+ except json.JSONDecodeError:
1834
+ failed = None
1835
+ error = failed.get("error") if isinstance(failed, dict) else None
1836
+ detail = (
1837
+ " | ".join(str(error.get(name)) for name in ("code", "phase", "message") if error.get(name))
1838
+ if isinstance(error, dict)
1839
+ else str(exc.reason)
1840
+ )
1841
+ raise OnboardingError(f"{label} failed with HTTP {exc.code}: {detail or exc.reason}") from exc
1842
+ except urllib.error.URLError as exc:
1843
+ raise OnboardingError(f"{label} could not reach Runtime: {exc.reason}") from exc
1844
+ try:
1845
+ payload = json.loads(raw)
1846
+ except json.JSONDecodeError as exc:
1847
+ raise OnboardingError(f"{label} did not return JSON") from exc
1848
+ if payload.get("success") is not True:
1849
+ error = payload.get("error")
1850
+ if isinstance(error, dict):
1851
+ detail = " | ".join(str(error.get(name)) for name in ("code", "phase", "message") if error.get(name))
1852
+ else:
1853
+ detail = str(error or "unknown Runtime error")
1854
+ raise OnboardingError(f"{label} returned success=false: {detail}")
1855
+ return payload
1856
+
1857
+
1721
1858
  CONNECTION_SECRET_KEYS = {
1722
1859
  "jdbcurl", "url", "username", "password", "passwordenv", "passwordref",
1723
1860
  }
@@ -1775,7 +1912,8 @@ def onboarding_plan_command(args: argparse.Namespace) -> dict:
1775
1912
  if not project_root.is_dir():
1776
1913
  raise OnboardingError(f"Project root not found: {project_root}")
1777
1914
  connection_file = normalized(args.connection_file)
1778
- connection = validate_connection(read_json_object(connection_file, "Connection plan"))
1915
+ requested_connection = validate_connection(read_json_object(connection_file, "Connection plan"))
1916
+ connection = persisted_connection(requested_connection)
1779
1917
  if connection.get("connectionMode") == "opaque-profile":
1780
1918
  require_opaque_profile_cli(install_state)
1781
1919
  existing = read_onboarding_state(data_root, profile, required=False)
@@ -1820,7 +1958,10 @@ def onboarding_plan_command(args: argparse.Namespace) -> dict:
1820
1958
  "schemaVersion": "foggy-deepseek-onboarding-plan-result/v1",
1821
1959
  "profile": profile,
1822
1960
  "statePath": str(path),
1823
- "connection": {**connection, "passwordEnvPresent": bool(password_env and os.environ.get(password_env))},
1961
+ "connection": {
1962
+ **connection,
1963
+ "passwordEnvPresent": bool(password_env and os.environ.get(password_env)),
1964
+ },
1824
1965
  "runtimeAvailable": runtime_state is not None,
1825
1966
  "next": "run datasource-configure --apply after reviewing the plan",
1826
1967
  "productionReady": False,
@@ -1851,10 +1992,11 @@ def datasource_configure_command(args: argparse.Namespace) -> dict:
1851
1992
  if opaque:
1852
1993
  plan.update({"profileId": connection["opaqueProfileId"], "revision": connection["opaqueRevision"]})
1853
1994
  else:
1995
+ inline_password = getattr(args, "runtime_password", None)
1854
1996
  plan.update({
1855
1997
  "jdbcUrl": connection["jdbcUrl"],
1856
1998
  "username": connection.get("username"),
1857
- "passwordEnv": connection.get("passwordEnv"),
1999
+ "credentialMode": connection.get("credentialMode", "none"),
1858
2000
  })
1859
2001
  if not args.apply:
1860
2002
  return {"success": True, "dryRun": True, "profile": state["profile"], "plan": plan, "next": "rerun with --apply after approval"}
@@ -1866,22 +2008,50 @@ def datasource_configure_command(args: argparse.Namespace) -> dict:
1866
2008
  ]
1867
2009
  label = "opaque profile configure"
1868
2010
  else:
2011
+ inline_password = getattr(args, "runtime_password", None)
1869
2012
  password_env = connection.get("passwordEnv")
1870
- if password_env and not os.environ.get(password_env):
2013
+ if connection.get("credentialMode") == "inline-development" and inline_password is None:
2014
+ raise OnboardingError(
2015
+ "Direct development password is not persisted; rerun onboard-datasource-run with the original connection file"
2016
+ )
2017
+ if inline_password is None and password_env and os.environ.get(password_env) is None:
1871
2018
  raise OnboardingError(f"Required password environment variable is not present: {password_env}")
1872
- command = ["datasources", "add", "--name", connection["name"], "--type", connection["type"], "--jdbc-url", connection["jdbcUrl"]]
1873
- if connection.get("username"):
1874
- command.extend(["--username", connection["username"]])
1875
- if password_env:
1876
- command.extend(["--password-env", password_env])
1877
2019
  label = "datasources add"
1878
- if args.replace:
1879
- command.append("--replace")
2020
+ resolved_password = inline_password if inline_password is not None else (
2021
+ os.environ.get(password_env) if password_env else None
2022
+ )
2023
+ command = None
1880
2024
  already_present = False
1881
2025
  try:
1882
- result = redact_connection_material(
1883
- cli_json(install_state, runtime_state, connection["namespace"], command, label)
1884
- )
2026
+ if not opaque and resolved_password is not None:
2027
+ body = {
2028
+ "name": connection["name"],
2029
+ "type": connection["type"],
2030
+ "jdbcUrl": connection["jdbcUrl"],
2031
+ "replace": bool(args.replace),
2032
+ "enabled": True,
2033
+ "password": resolved_password,
2034
+ }
2035
+ if connection.get("username"):
2036
+ body["username"] = connection["username"]
2037
+ result = redact_connection_material(runtime_api_json(
2038
+ runtime_state,
2039
+ connection["namespace"],
2040
+ "POST",
2041
+ "/api/v1/datasources",
2042
+ body,
2043
+ label,
2044
+ ))
2045
+ else:
2046
+ if not opaque:
2047
+ command = ["datasources", "add", "--name", connection["name"], "--type", connection["type"], "--jdbc-url", connection["jdbcUrl"]]
2048
+ if connection.get("username"):
2049
+ command.extend(["--username", connection["username"]])
2050
+ if args.replace:
2051
+ command.append("--replace")
2052
+ result = redact_connection_material(
2053
+ cli_json(install_state, runtime_state, connection["namespace"], command, label)
2054
+ )
1885
2055
  except OnboardingError as exc:
1886
2056
  if "DATASOURCE_ALREADY_EXISTS" not in str(exc):
1887
2057
  raise
@@ -2579,6 +2749,7 @@ def datasource_run_command(args: argparse.Namespace) -> dict:
2579
2749
  _install_root, install_state, data_root, _runtime_state = onboarding_context(args, require_runtime=True)
2580
2750
  project_root = normalized(args.project_root or Path.cwd())
2581
2751
  requested_connection = validate_connection(read_json_object(normalized(args.connection_file), "Connection plan"))
2752
+ resumable_connection = persisted_connection(requested_connection)
2582
2753
  if not requested_connection.get("profile"):
2583
2754
  raise OnboardingError("Composite datasource onboarding requires connection.profile in the approved contract")
2584
2755
  profile = requested_connection["profile"]
@@ -2592,7 +2763,7 @@ def datasource_run_command(args: argparse.Namespace) -> dict:
2592
2763
  files: list[str] = []
2593
2764
  existing = read_onboarding_state(data_root, profile, required=False)
2594
2765
  if existing:
2595
- if existing.get("connection") != requested_connection:
2766
+ if persisted_connection(existing.get("connection", {})) != resumable_connection:
2596
2767
  raise OnboardingError("Existing onboarding profile does not match the requested connection plan")
2597
2768
  adopted = bind_completed_workspace(existing, data_root, project_root)
2598
2769
  plan_result = {
@@ -2625,6 +2796,7 @@ def datasource_run_command(args: argparse.Namespace) -> dict:
2625
2796
  else:
2626
2797
  configure_dry = datasource_configure_command(argparse.Namespace(
2627
2798
  install_root=args.install_root, data_root=args.data_root, profile=profile, apply=False, replace=False,
2799
+ runtime_password=requested_connection.get("password"),
2628
2800
  ))
2629
2801
  save_composite_result(evidence_dir, "02-datasource-dry.json", configure_dry, files)
2630
2802
  if not args.approve_configure:
@@ -2640,6 +2812,7 @@ def datasource_run_command(args: argparse.Namespace) -> dict:
2640
2812
  }
2641
2813
  configured = datasource_configure_command(argparse.Namespace(
2642
2814
  install_root=args.install_root, data_root=args.data_root, profile=profile, apply=True, replace=False,
2815
+ runtime_password=requested_connection.get("password"),
2643
2816
  ))
2644
2817
  save_composite_result(evidence_dir, "03-datasource-apply.json", configured, files)
2645
2818
  state = read_onboarding_state(data_root, profile)
@@ -3262,7 +3435,7 @@ def main() -> None:
3262
3435
  except OnboardingError as exc:
3263
3436
  if ACTIVE_PROGRESS is not None:
3264
3437
  ACTIVE_PROGRESS.fail(exc)
3265
- emit({"success": False, "error": {"code": "ONBOARDING_ERROR", "message": str(exc)}, "productionReady": False}, 1)
3438
+ emit({"success": False, "error": {"code": getattr(exc, "code", "ONBOARDING_ERROR"), "message": str(exc)}, "productionReady": False}, 1)
3266
3439
  except Exception as exc:
3267
3440
  if ACTIVE_PROGRESS is not None:
3268
3441
  ACTIVE_PROGRESS.fail(exc)