Skip to content

Commit f6d46cb

Browse files
xsh310Isaac
andcommitted
[skills] Record the workspace org id in downloaded-skill attribution
Capture the numeric workspace (org) id and store it as `workspace_id` on each downloaded-skill record, giving attribution a rename-proof workspace identifier alongside the workspace URL. Databricks stamps every authenticated response with an `X-Databricks-Org-Id` header, so the download's existing API calls already carry it; the HTTP GET helpers now capture it into a session cache (keyed by hostname, mirroring the model-service listing caches) with no extra request. A record omits the field when no response has revealed the id yet. Co-authored-by: Isaac <no-reply@databricks.com>
1 parent 9e715b7 commit f6d46cb

6 files changed

Lines changed: 78 additions & 0 deletions

File tree

src/ucode/databricks.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from dataclasses import dataclass
2828
from decimal import Decimal, InvalidOperation
29+
from email.message import Message
2930
from enum import Enum
3031
from pathlib import Path
3132
from typing import Literal, NamedTuple, NoReturn, cast, overload
@@ -260,6 +261,30 @@ def _http_get_retry_delay(retry_after: str | None, retry_index: int) -> float:
260261
return backoff + random.uniform(0, min(backoff * 0.25, 0.5))
261262

262263

264+
# Databricks stamps every authenticated API response with the caller's numeric workspace (org) id in
265+
# this header, so any call ucode already makes reveals it with no dedicated lookup. Captured by
266+
# hostname as responses go by; session-only, like the listing caches below.
267+
_ORG_ID_HEADER = "X-Databricks-Org-Id"
268+
_WORKSPACE_ORG_IDS: dict[str, str] = {}
269+
270+
271+
def _capture_org_id(url: str, headers: Message | None) -> None:
272+
org_id = headers.get(_ORG_ID_HEADER) if headers is not None else None
273+
hostname = urlparse(url).hostname
274+
if org_id and hostname:
275+
_WORKSPACE_ORG_IDS[hostname] = org_id
276+
277+
278+
def workspace_org_id(workspace: str) -> str | None:
279+
"""The numeric workspace (org) id for ``workspace``, or None if no response has revealed it yet."""
280+
return _WORKSPACE_ORG_IDS.get(workspace_hostname(workspace))
281+
282+
283+
def clear_workspace_org_id_cache() -> None:
284+
"""Forget captured workspace org ids (used by tests, and after a workspace switch)."""
285+
_WORKSPACE_ORG_IDS.clear()
286+
287+
263288
def _http_get_json(
264289
url: str,
265290
token: str,
@@ -286,6 +311,7 @@ def _http_get_json(
286311
try:
287312
with urllib_request.urlopen(request, timeout=timeout) as response:
288313
body = response.read().decode("utf-8")
314+
_capture_org_id(url, getattr(response, "headers", None))
289315
_debug(f"GET {url}", f"HTTP 200, {len(body)} bytes")
290316
if _debug_enabled():
291317
_debug("body", body[:4000])
@@ -434,6 +460,7 @@ def _http_get_bytes(url: str, token: str, *, timeout: int = 10) -> tuple[bytes |
434460
try:
435461
with urllib_request.urlopen(request, timeout=timeout) as response:
436462
body = response.read()
463+
_capture_org_id(url, getattr(response, "headers", None))
437464
_debug(f"GET {url}", f"HTTP 200, {len(body)} bytes")
438465
return body, None
439466
except urllib_error.HTTPError as exc:

src/ucode/skills_download.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
get_databricks_token,
1818
walk_catalog_schemas,
1919
workspace_hostname,
20+
workspace_org_id,
2021
)
2122
from ucode.mcp import register_schemaless_skills_connection, setup_mcp_clients
2223
from ucode.skills_state import (
@@ -333,6 +334,7 @@ def _skill_installs(
333334
"""Attribution records for ``refs`` written into ``roots`` (see ``skills_state``)."""
334335
base = path or str(Path.home())
335336
scope = "project" if path else "user"
337+
org_id = workspace_org_id(workspace)
336338
return [
337339
SkillInstall(
338340
fqn=ref.fqn,
@@ -342,6 +344,7 @@ def _skill_installs(
342344
base=base,
343345
dirs=tuple(str(root / ref.bundle_name) for root in roots),
344346
metastore_id=ref.metastore_id,
347+
workspace_id=org_id,
345348
skill_id=ref.skill_id,
346349
uc_update_time=ref.uc_update_time,
347350
)

src/ucode/skills_state.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class SkillInstall:
3838
base: str
3939
dirs: tuple[str, ...]
4040
metastore_id: str | None = None
41+
workspace_id: str | None = None
4142
skill_id: str | None = None
4243
uc_update_time: str | None = None
4344

@@ -121,6 +122,7 @@ def _to_record(install: SkillInstall) -> dict:
121122
"bundle_name": install.bundle_name,
122123
"metastore_id": install.metastore_id,
123124
"workspace": install.workspace,
125+
"workspace_id": install.workspace_id,
124126
"scope": install.scope,
125127
"base": install.base,
126128
"dirs": list(install.dirs),

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def reject_privileged_write(path, _desired_text):
6565
# The model-services listing is memoized for the life of the process, so without this a cached
6666
# result would leak into the next test and make a stubbed listing look like it was never called.
6767
databricks_mod.clear_model_services_cache()
68+
databricks_mod.clear_workspace_org_id_cache()
6869

6970

7071
def _workspace() -> str:

tests/test_databricks.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,38 @@ def test_invalid_url_raises(self):
183183
workspace_hostname("")
184184

185185

186+
class _FakeResponseWithHeaders(_FakeResponse):
187+
def __init__(self, payload: dict, headers: dict):
188+
super().__init__(payload)
189+
self.headers = headers
190+
191+
192+
class TestWorkspaceOrgId:
193+
def _stub_response(self, monkeypatch, headers: dict) -> None:
194+
monkeypatch.setattr(
195+
db_mod.urllib_request,
196+
"urlopen",
197+
lambda request, timeout=None: _FakeResponseWithHeaders({"ok": True}, headers),
198+
)
199+
200+
def test_captures_org_id_header_from_get(self, monkeypatch):
201+
self._stub_response(monkeypatch, {"X-Databricks-Org-Id": "1234567890"})
202+
203+
db_mod._http_get_json(f"{WS}/api/2.1/unity-catalog/skills", "token")
204+
205+
assert db_mod.workspace_org_id(WS) == "1234567890"
206+
207+
def test_absent_until_a_response_reveals_it(self):
208+
assert db_mod.workspace_org_id(WS) is None
209+
210+
def test_missing_header_leaves_it_absent(self, monkeypatch):
211+
self._stub_response(monkeypatch, {})
212+
213+
db_mod._http_get_json(f"{WS}/api/x", "token")
214+
215+
assert db_mod.workspace_org_id(WS) is None
216+
217+
186218
class TestBuildDatabricksCliEnv:
187219
def test_sets_databricks_host(self):
188220
env = build_databricks_cli_env(WS)

tests/test_skills_download.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,19 @@ def test_records_downloaded_skills(self, tmp_path, monkeypatch):
766766
assert record["fqn"] == "main.default.triage"
767767
assert record["scope"] == "project"
768768
assert record["base"] == str(tmp_path)
769+
assert "workspace_id" not in record
770+
771+
def test_records_workspace_id_when_known(self, tmp_path, monkeypatch):
772+
monkeypatch.setattr(sd, "get_skill", lambda ws, tok, fqn: ref(fqn.rsplit(".", 1)[-1]))
773+
monkeypatch.setattr(
774+
sd, "fetch_skill_bundle", lambda ws, tok, c, s, leaf: ({"SKILL.md": b"x"}, None)
775+
)
776+
monkeypatch.setattr(sd, "workspace_org_id", lambda ws: "org-42")
777+
778+
sd.download_selected_skills(WS, "token", ["main.default.triage"], str(tmp_path))
779+
780+
record = skills_state.attribution_for_dir(tmp_path / ".claude/skills/triage")
781+
assert record["workspace_id"] == "org-42"
769782

770783

771784
class TestSkillRefMetadata:

0 commit comments

Comments
 (0)