@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.
- package/README.md +24 -14
- package/docs/PUBLIC-BETA-READINESS.md +32 -6
- package/docs/WINDOWS-BETA-ACCEPTANCE.md +28 -14
- package/experience/linux/prepare.sh +1 -1
- package/lib/client.js +191 -24
- package/lib/index.js +66 -0
- package/lib/remote-descriptor.js +15 -2
- package/lib/runtime-settings.js +89 -0
- package/package.json +2 -2
- package/skills/foggy-deepseek-onboarding/SKILL.md +106 -127
- package/skills/foggy-deepseek-onboarding/assets/connection.schema.json +6 -8
- package/skills/foggy-deepseek-onboarding/assets/datasource.example.json +3 -2
- package/skills/foggy-deepseek-onboarding/assets/env.example +4 -4
- package/skills/foggy-deepseek-onboarding/assets/onboarding-state.schema.json +23 -1
- package/skills/foggy-deepseek-onboarding/assets/versions.json +2 -2
- package/skills/foggy-deepseek-onboarding/references/onboarding-workflow.md +113 -123
- package/skills/foggy-deepseek-onboarding/scripts/onboarding.py +200 -27
|
@@ -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
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
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": {
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
1879
|
-
|
|
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
|
-
|
|
1883
|
-
|
|
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") !=
|
|
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)
|