p1 bootstrap v2: licenses key required; existence != privacy; stage in /tmp
Browse files- probes/p1_secret_bootstrap.py +83 -65
probes/p1_secret_bootstrap.py
CHANGED
|
@@ -1,23 +1,23 @@
|
|
| 1 |
# Phase 1 bootstrap worker: create a PRIVATE Kaggle dataset to hold the HF token, safely.
|
| 2 |
#
|
| 3 |
-
# Why
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
#
|
| 7 |
-
# their source.
|
| 8 |
#
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
#
|
|
|
|
|
|
|
|
|
|
| 13 |
#
|
| 14 |
-
#
|
| 15 |
-
#
|
| 16 |
-
#
|
| 17 |
-
# 4. re-prove, and delete nothing that was not created here.
|
| 18 |
#
|
| 19 |
-
#
|
| 20 |
-
# No token value is ever printed; kernel stdout is retained. Zero GPU quota (CPU session).
|
| 21 |
|
| 22 |
import hashlib
|
| 23 |
import json
|
|
@@ -31,7 +31,7 @@ R = {}
|
|
| 31 |
DS_SLUG = "ounce100m-secret-store"
|
| 32 |
DS_ID = f"dodosoomro/{DS_SLUG}"
|
| 33 |
TOKEN_ENV = "OUNCE100M_HF_TOKEN"
|
| 34 |
-
DS_DIR = "/kaggle/working
|
| 35 |
PLACEHOLDER = "placeholder.txt"
|
| 36 |
SECRET_FILE = "credentials.json"
|
| 37 |
|
|
@@ -49,44 +49,54 @@ def run(cmd, timeout=300):
|
|
| 49 |
return {"rc": p.returncode, "out": scrub(p.stdout), "err": scrub(p.stderr)}
|
| 50 |
|
| 51 |
|
| 52 |
-
def
|
| 53 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
url = f"https://www.kaggle.com/api/v1/datasets/download/{DS_ID}"
|
| 55 |
try:
|
| 56 |
with urllib.request.urlopen(url, timeout=60) as r:
|
| 57 |
-
return {"
|
| 58 |
except urllib.error.HTTPError as e:
|
| 59 |
-
return {"
|
|
|
|
| 60 |
except Exception as e:
|
| 61 |
-
return {"
|
| 62 |
|
| 63 |
|
| 64 |
-
def write_metadata(
|
| 65 |
-
"""Both spellings of the visibility key, because the CLI version in the image is 2.0.2 and the
|
| 66 |
-
field name has moved across releases; an unrecognised key is ignored without error."""
|
| 67 |
-
md = {
|
| 68 |
-
"title": "ounce100m runtime assets",
|
| 69 |
-
"id": DS_ID,
|
| 70 |
-
"resources": [{"path": PLACEHOLDER, "description": "bootstrap placeholder",
|
| 71 |
-
"resourceName": "placeholder", "resourceType": "dataset"}],
|
| 72 |
-
}
|
| 73 |
-
if private:
|
| 74 |
-
md["isPrivate"] = True
|
| 75 |
-
md["defineIsPrivate"] = True
|
| 76 |
-
with open(os.path.join(DS_DIR, "dataset-metadata.json"), "w") as f:
|
| 77 |
-
json.dump(md, f)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def write_files(with_secret):
|
| 81 |
os.makedirs(DS_DIR, exist_ok=True)
|
| 82 |
with open(os.path.join(DS_DIR, PLACEHOLDER), "w") as f:
|
| 83 |
f.write("ounce100m bootstrap placeholder\n")
|
| 84 |
if with_secret:
|
| 85 |
-
tok = os.environ[TOKEN_ENV]
|
| 86 |
with open(os.path.join(DS_DIR, SECRET_FILE), "w") as f:
|
| 87 |
-
json.dump({"HF_TOKEN":
|
| 88 |
os.chmod(os.path.join(DS_DIR, SECRET_FILE), 0o600)
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
tok = os.environ.get(TOKEN_ENV, "")
|
| 92 |
R["pre"] = {"token_present": bool(tok), "token_length": len(tok),
|
|
@@ -95,44 +105,52 @@ R["pre"] = {"token_present": bool(tok), "token_length": len(tok),
|
|
| 95 |
if not tok:
|
| 96 |
R["fatal"] = f"{TOKEN_ENV} unset; nothing created"
|
| 97 |
else:
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
existing = run(f"kaggle datasets status {DS_ID} 2>&1", 120)
|
| 102 |
-
R["pre_existing_status"] = existing
|
| 103 |
-
already = existing["rc"] == 0
|
| 104 |
-
|
| 105 |
-
if not already:
|
| 106 |
t = time.monotonic()
|
| 107 |
R["create"] = run(f"kaggle datasets create -p {DS_DIR} -q 2>&1", timeout=600)
|
| 108 |
R["create"]["seconds"] = round(time.monotonic() - t, 1)
|
| 109 |
-
time.sleep(
|
| 110 |
|
| 111 |
-
#
|
| 112 |
-
|
| 113 |
-
R["
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
|
| 116 |
-
if R["
|
| 117 |
-
|
| 118 |
t = time.monotonic()
|
| 119 |
R["version_with_secret"] = run(
|
| 120 |
f'kaggle datasets version -p {DS_DIR} -m "provision runtime credential" -q 2>&1',
|
| 121 |
timeout=600)
|
| 122 |
R["version_with_secret"]["seconds"] = round(time.monotonic() - t, 1)
|
| 123 |
-
time.sleep(
|
| 124 |
-
after =
|
| 125 |
-
R["
|
| 126 |
-
R["secret_still_private"] = after
|
| 127 |
if not R["secret_still_private"]:
|
| 128 |
-
R["EMERGENCY"] = ("Anonymous read
|
| 129 |
-
"
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
else:
|
| 132 |
-
R["refused_to_upload"] =
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
if R.get("secret_still_private"):
|
| 137 |
R["mount_hint_for_future_jobs"] = {
|
| 138 |
"datasetDataSources": [DS_ID],
|
|
|
|
| 1 |
# Phase 1 bootstrap worker: create a PRIVATE Kaggle dataset to hold the HF token, safely.
|
| 2 |
#
|
| 3 |
+
# Why (closes D-002): §2 permits "the environment or a secret store" and forbids the token reaching a
|
| 4 |
+
# public artifact. p1_credential_path showed the Kaggle CLI is preinstalled and pre-authenticated
|
| 5 |
+
# inside an internet-enabled session, so a job can create its own private dataset and later jobs can
|
| 6 |
+
# mount it from /kaggle/input instead of carrying the token in their source.
|
|
|
|
| 7 |
#
|
| 8 |
+
# v1 FAILED, and the failure taught two things that are encoded here:
|
| 9 |
+
# * `kaggle datasets create` on CLI 2.0.2 aborts with "Key licenses not found in data" unless
|
| 10 |
+
# dataset-metadata.json carries a `licenses` array. Nothing was created, so nothing leaked.
|
| 11 |
+
# * worse, my privacy gate was NOT discriminating: it treated an anonymous 403 as "private", but a
|
| 12 |
+
# 403 is also what a NON-EXISTENT dataset returns. So v1 reported private_confirmed=true for a
|
| 13 |
+
# dataset that did not exist. Existence and privacy are now proved by two different calls, and
|
| 14 |
+
# privacy is only asserted when the dataset is known to exist.
|
| 15 |
#
|
| 16 |
+
# Staging lives under /tmp, never /kaggle/working: files in the working directory are published as
|
| 17 |
+
# kernel output artifacts, and v1 briefly wrote the credential there. It stayed behind private-kernel
|
| 18 |
+
# auth, but there is no reason for a secret to be an output artifact at all.
|
|
|
|
| 19 |
#
|
| 20 |
+
# No token value is ever printed. Zero GPU quota (CPU session).
|
|
|
|
| 21 |
|
| 22 |
import hashlib
|
| 23 |
import json
|
|
|
|
| 31 |
DS_SLUG = "ounce100m-secret-store"
|
| 32 |
DS_ID = f"dodosoomro/{DS_SLUG}"
|
| 33 |
TOKEN_ENV = "OUNCE100M_HF_TOKEN"
|
| 34 |
+
DS_DIR = "/tmp/ounce100m_ds" # outside /kaggle/working on purpose -- see header
|
| 35 |
PLACEHOLDER = "placeholder.txt"
|
| 36 |
SECRET_FILE = "credentials.json"
|
| 37 |
|
|
|
|
| 49 |
return {"rc": p.returncode, "out": scrub(p.stdout), "err": scrub(p.stderr)}
|
| 50 |
|
| 51 |
|
| 52 |
+
def exists_authenticated():
|
| 53 |
+
"""A successful *authenticated* listing proves the dataset exists."""
|
| 54 |
+
r = run(f"kaggle datasets files {DS_ID} 2>&1", 120)
|
| 55 |
+
return {"rc": r["rc"], "out": r["out"][:400], "exists": r["rc"] == 0}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def anon_access():
|
| 59 |
+
"""Classify anonymous reachability. Only HTTP 200 means PUBLIC-and-leaking. 404 means the dataset
|
| 60 |
+
is not there at all, which is NOT evidence of privacy."""
|
| 61 |
url = f"https://www.kaggle.com/api/v1/datasets/download/{DS_ID}"
|
| 62 |
try:
|
| 63 |
with urllib.request.urlopen(url, timeout=60) as r:
|
| 64 |
+
return {"status": r.status, "verdict": "PUBLIC"}
|
| 65 |
except urllib.error.HTTPError as e:
|
| 66 |
+
return {"status": e.code,
|
| 67 |
+
"verdict": "PRIVATE" if e.code in (401, 403) else "OTHER_HTTP"}
|
| 68 |
except Exception as e:
|
| 69 |
+
return {"error": f"{type(e).__name__}: {e}"[:160], "verdict": "UNKNOWN"}
|
| 70 |
|
| 71 |
|
| 72 |
+
def write_metadata(with_secret):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
os.makedirs(DS_DIR, exist_ok=True)
|
| 74 |
with open(os.path.join(DS_DIR, PLACEHOLDER), "w") as f:
|
| 75 |
f.write("ounce100m bootstrap placeholder\n")
|
| 76 |
if with_secret:
|
|
|
|
| 77 |
with open(os.path.join(DS_DIR, SECRET_FILE), "w") as f:
|
| 78 |
+
json.dump({"HF_TOKEN": os.environ[TOKEN_ENV]}, f)
|
| 79 |
os.chmod(os.path.join(DS_DIR, SECRET_FILE), 0o600)
|
| 80 |
|
| 81 |
+
resources = [{"path": PLACEHOLDER, "description": "bootstrap placeholder",
|
| 82 |
+
"resourceName": "placeholder", "resourceType": "dataset"}]
|
| 83 |
+
if with_secret:
|
| 84 |
+
resources.append({"path": SECRET_FILE, "description": "runtime credentials",
|
| 85 |
+
"resourceName": "credentials", "resourceType": "dataset"})
|
| 86 |
+
md = {
|
| 87 |
+
"title": "ounce100m runtime assets",
|
| 88 |
+
"id": DS_ID,
|
| 89 |
+
# CLI 2.0.2 refuses to create without this key -- the exact fault that stopped v1.
|
| 90 |
+
"licenses": [{"name": "other"}],
|
| 91 |
+
"resources": resources,
|
| 92 |
+
# Both historical spellings of the visibility flag: an unrecognised key is ignored silently,
|
| 93 |
+
# and a silently-ignored key here would mean a public dataset holding a credential.
|
| 94 |
+
"isPrivate": True,
|
| 95 |
+
"defineIsPrivate": True,
|
| 96 |
+
}
|
| 97 |
+
with open(os.path.join(DS_DIR, "dataset-metadata.json"), "w") as f:
|
| 98 |
+
json.dump(md, f)
|
| 99 |
+
|
| 100 |
|
| 101 |
tok = os.environ.get(TOKEN_ENV, "")
|
| 102 |
R["pre"] = {"token_present": bool(tok), "token_length": len(tok),
|
|
|
|
| 105 |
if not tok:
|
| 106 |
R["fatal"] = f"{TOKEN_ENV} unset; nothing created"
|
| 107 |
else:
|
| 108 |
+
write_metadata(with_secret=False)
|
| 109 |
+
if not exists_authenticated()["exists"]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
t = time.monotonic()
|
| 111 |
R["create"] = run(f"kaggle datasets create -p {DS_DIR} -q 2>&1", timeout=600)
|
| 112 |
R["create"]["seconds"] = round(time.monotonic() - t, 1)
|
| 113 |
+
time.sleep(15)
|
| 114 |
|
| 115 |
+
# Gate 1: does it exist? (authenticated) Gate 2: is it unreachable anonymously?
|
| 116 |
+
ex = exists_authenticated()
|
| 117 |
+
R["existence"] = ex
|
| 118 |
+
priv = anon_access() if ex["exists"] else {"verdict": "SKIPPED_NOT_EXISTING"}
|
| 119 |
+
R["privacy_placeholder_only"] = priv
|
| 120 |
+
R["safe_to_upload_secret"] = bool(ex["exists"]) and priv["verdict"] == "PRIVATE"
|
| 121 |
|
| 122 |
+
if R["safe_to_upload_secret"]:
|
| 123 |
+
write_metadata(with_secret=True)
|
| 124 |
t = time.monotonic()
|
| 125 |
R["version_with_secret"] = run(
|
| 126 |
f'kaggle datasets version -p {DS_DIR} -m "provision runtime credential" -q 2>&1',
|
| 127 |
timeout=600)
|
| 128 |
R["version_with_secret"]["seconds"] = round(time.monotonic() - t, 1)
|
| 129 |
+
time.sleep(15)
|
| 130 |
+
after = anon_access()
|
| 131 |
+
R["privacy_after_secret"] = after
|
| 132 |
+
R["secret_still_private"] = after["verdict"] == "PRIVATE"
|
| 133 |
if not R["secret_still_private"]:
|
| 134 |
+
R["EMERGENCY"] = ("Anonymous read works AFTER the credential upload: delete "
|
| 135 |
+
f"{DS_ID} from the Kaggle web UI now and rotate the HF token.")
|
| 136 |
+
# A public dataset would also show up in the public search index -- independent second check.
|
| 137 |
+
R["public_search_check"] = scrub(
|
| 138 |
+
urllib.request.urlopen(
|
| 139 |
+
f"https://www.kaggle.com/api/v1/datasets/list?public=true&user=dodosoomro"
|
| 140 |
+
f"&search=ounce100m", timeout=60).read().decode()[:400])
|
| 141 |
else:
|
| 142 |
+
R["refused_to_upload"] = {
|
| 143 |
+
"reason": "existence or privacy not positively established",
|
| 144 |
+
"existence_rc": ex["rc"], "privacy": priv,
|
| 145 |
+
"create_output": R.get("create", {}).get("out"),
|
| 146 |
+
}
|
| 147 |
|
| 148 |
+
# Remove the staged credential from disk regardless of outcome, then report what is left.
|
| 149 |
+
if os.path.exists(os.path.join(DS_DIR, SECRET_FILE)):
|
| 150 |
+
os.remove(os.path.join(DS_DIR, SECRET_FILE))
|
| 151 |
+
R["staging_after"] = {"secret_removed": not os.path.exists(os.path.join(DS_DIR, SECRET_FILE)),
|
| 152 |
+
"dir_listing": sorted(os.listdir(DS_DIR)),
|
| 153 |
+
"working_dir_listing": sorted(os.listdir("/kaggle/working"))}
|
| 154 |
if R.get("secret_still_private"):
|
| 155 |
R["mount_hint_for_future_jobs"] = {
|
| 156 |
"datasetDataSources": [DS_ID],
|