Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
23802ec
Add a generic agent-skills host with an access-aware preflight
kumo-isaac Oct 20, 2026
14a51ec
Generic host: bare file table names; write path destinations exactly
kumo-isaac Oct 20, 2026
8caea0e
Generic host: stage a file population with TIMESTAMP, in a created di…
kumo-isaac Oct 21, 2026
5a15087
Generic host: a timestamped entity's population must carry TIMESTAMP
kumo-isaac Oct 21, 2026
7ed8896
Generic host: stage a Databricks population through the statement API
kumo-isaac Oct 21, 2026
76cd6f7
Generic host: cast a population's TIMESTAMP to a timestamp
kumo-isaac Oct 21, 2026
feefa6d
Generic host: no region or profile placeholder for S3 and GCS
kumo-isaac Oct 21, 2026
ad5d748
S3Connector: region is optional and normally omitted
kumo-isaac Oct 21, 2026
e6df3db
Router: show the later-destination example as text, not a python block
kumo-isaac Oct 21, 2026
923874f
Genie Code host: hosts/databricks-genie, genie package with --deploy
kumo-isaac Oct 21, 2026
7d9829c
package_skill: genie --deploy --prune deletes what the package does n…
kumo-isaac Oct 21, 2026
05c018d
Genie host: cover the Snowflake case groups — registry, retention, jo…
kumo-isaac Oct 21, 2026
13f002d
Genie adapter: a metric view's primary keys are its base tables' Unit…
kumo-isaac Oct 21, 2026
e35d691
SDK reference: threshold and top-k belong to predict, never to Predic…
kumo-isaac Oct 21, 2026
b7a391d
batch_inference: never ask for a saved model's source; scoring option…
kumo-isaac Oct 23, 2026
b5d5245
Address review on the generic host
kumo-isaac Oct 23, 2026
f0a5b31
Merge PR #48's review changes into the Genie host
kumo-isaac Oct 24, 2026
f163844
agent-skills: address remaining quick review comments
kumo-isaac Oct 24, 2026
79d5f45
agent-skills: CSV time column is Dtype.date in the connector notes
kumo-isaac Oct 26, 2026
bac10d9
agent-skills: stage a scoring population in the source's system
kumo-isaac Oct 26, 2026
e1ce213
agent-skills: stage a BigQuery scoring population through the BigQuer…
kumo-isaac Oct 26, 2026
d1cf34e
agent-skills: pick the population connector from the population path
kumo-isaac Oct 26, 2026
d4e2c81
Merge PR #48's later review changes into the Genie host
kumo-isaac Oct 26, 2026
86a1ba8
Genie host: address the adversarial review (staging volume, deploy, s…
kumo-isaac Oct 26, 2026
5ba5655
Merge branch 'main' into igutierrez/agent-skills-generic-host
kumo-isaac Oct 26, 2026
4f974c5
agent-skills: generic BigQuery connectors name kumo.BigQueryIdentity()
kumo-isaac Oct 26, 2026
f8cc57d
Merge branch 'igutierrez/agent-skills-generic-host' into igutierrez/a…
kumo-isaac Oct 27, 2026
25c5c4e
agent-skills: generic BigQuery bills to project_id unless the user na…
kumo-isaac Oct 27, 2026
1178a6d
agent-skills: generic BigQuery project_id is the confirmed scope's pr…
kumo-isaac Oct 27, 2026
23dae04
agent-skills: generic config cell has no billing constant for BigQuery
kumo-isaac Oct 27, 2026
6f3cdab
Merge branch 'main' into igutierrez/agent-skills-generic-host
kumo-isaac Oct 27, 2026
68a42b4
Merge remote-tracking branch 'origin/igutierrez/agent-skills-generic-…
kumo-isaac Oct 27, 2026
09baf89
Genie host: address Manush's review on #111
kumo-isaac Oct 27, 2026
56d92ae
Merge branch 'main' into igutierrez/agent-skills-generic-host
kumo-isaac Oct 28, 2026
d046770
Merge branch 'igutierrez/agent-skills-generic-host' into igutierrez/a…
kumo-isaac Oct 28, 2026
12a105f
Genie host: ASK BACK on a metric-view join that is not many_to_one
kumo-isaac Oct 28, 2026
521ccbb
Merge main (#48 squash-merged) into igutierrez/agent-skills-genie-host
kumo-isaac Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions agent-skills/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,8 @@ or nothing. Without the check it falls back on its priors, the failure seen when
empty skill tree. The agent's access is never the artifact's: the generated code authenticates
where it runs, so §0 changes no gate and no generated line. Credentials stay with the user —
the agent may ask them to authenticate a tool, never for a value.

Genie Code is the Databricks counterpart of CoCo: its own SQL, run as the signed-in user, is the
read tool, and the notebook it generates runs as whoever runs it, through `DatabricksIdentity()`'s
ambient credentials. So the Genie host names no service principal and no secret scope unless the
user asks for one; the legacy Databricks host required both, before the SDK had an ambient form.
10 changes: 8 additions & 2 deletions agent-skills/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Agent skills: `kumo-data-science`

Agent skill for building, training, and running inference with Kumo models. One shared source tree
serves every host; host-specific behavior lives under `skills/hosts/`. Two hosts:
serves every host; host-specific behavior lives under `skills/hosts/`. Three hosts:

| Host | Package | For |
|---|---|---|
| `hosts/snowflake-coco/` | `coco` | Snowflake Cortex Code (CoCo), inside Snowflake |
| `hosts/generic/` | `generic` | a coding agent beside the user — Claude Code, Codex — generating code for any of the six Kumo connectors |
| `hosts/databricks-genie/` | `genie` | Databricks Genie Code, inside a Databricks workspace, generating notebooks for a GPU cluster |

This directory is deliberately not under `.agents/skills/` or `.claude/skills/`: Codex, Claude Code
and the CoCo CLI load those for repo developers, and this skill triggers on "kumo" and "train a
Expand Down Expand Up @@ -39,13 +40,18 @@ path. Build a host package with:
```bash
python3 agent-skills/scripts/package_skill.py coco # -> agent-skills/dist/coco/
python3 agent-skills/scripts/package_skill.py generic # -> agent-skills/dist/generic/skills/
python3 agent-skills/scripts/package_skill.py genie # -> agent-skills/dist/genie/skills/
python3 agent-skills/scripts/package_skill.py genie --deploy # + upload to your Genie Code skills folder
python3 agent-skills/scripts/package_skill.py --check # build every host, verify references
```

A package carries exactly one host, and the core reaches it only as `hosts/<host>/`; tests fail if
a core file names a host. To use the `generic` package, copy
`dist/generic/skills/kumo-data-science/` into the agent's user-level skills directory (for Claude
Code, `~/.claude/skills/`) — not into this repository, for the reason above.
Code, `~/.claude/skills/`) — not into this repository, for the reason above. `genie --deploy`
uploads the `genie` package to `/Users/<you>/.assistant/skills/kumo-data-science/` in the
workspace named by `DATABRICKS_HOST` and `DATABRICKS_TOKEN` and reads every file back; a new Genie
Code chat loads it.

The build stamps the router's `SKILL-BUILD` marker with the source commit
(`<commit date>-<short sha>`, `-dirty` for uncommitted edits); never edit it by hand. Every build
Expand Down
150 changes: 149 additions & 1 deletion agent-skills/scripts/package_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

python3 agent-skills/scripts/package_skill.py coco # build into agent-skills/dist/
python3 agent-skills/scripts/package_skill.py generic # Claude Code, Codex, other agents
python3 agent-skills/scripts/package_skill.py genie # Databricks Genie Code
python3 agent-skills/scripts/package_skill.py genie --deploy # + upload to your Genie skills folder
python3 agent-skills/scripts/package_skill.py coco --out DIR # build somewhere else
python3 agent-skills/scripts/package_skill.py --check # build every host in a temp dir

Expand All @@ -18,18 +20,31 @@
`<commit date>-<short sha>`, suffixed `-dirty` when `skills/` has uncommitted changes. The source
tree carries `SKILL-BUILD: source`, so a session quoting that value read an unpackaged copy.

`genie --deploy` uploads the package to `/Users/<you>/.assistant/skills/kumo-data-science/`, the
folder Genie Code reads skills from, through the Workspace API, then reads every file back and
compares bytes. It authenticates with `DATABRICKS_HOST` (or `DATABRICKS_URL`) and
`DATABRICKS_TOKEN` from the environment or `--env-file`. Files already in that folder that the
package does not ship are reported, and deleted with `--prune` (only inside
`kumo-data-science/`). A new Genie Code chat picks up the change.

Every build is checked: each markdown link must resolve relative to the file that holds it, and
each backticked path naming a package directory must resolve relative to the package root. A
package that references a file it does not ship fails the build.
"""
from __future__ import annotations

import argparse
import base64
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
Expand All @@ -55,7 +70,13 @@
'generic': dict(prefix='skills',
hosts=['generic/adapter.md', 'generic/runtime.md'],
capability=True),
# Databricks Genie Code. Its skills folder holds the one skill directory, like `generic`.
'genie': dict(prefix='skills',
hosts=['databricks-genie/adapter.md', 'databricks-genie/runtime.md',
'databricks-genie/job.md'],
capability=True),
}
GENIE_SKILLS = '/Users/{user}/.assistant/skills'

MARKER = re.compile(r'SKILL-BUILD: [^`*\s]+')
MD_LINK = re.compile(r'\]\(([^)\s#]+)(?:#[^)]*)?\)')
Expand Down Expand Up @@ -149,14 +170,132 @@ def unresolved(pkg: Path) -> list[str]:
return errors


class Workspace:
"""The few Databricks REST calls a deploy needs, with a personal access token."""

def __init__(self, host: str, token: str) -> None:
self.host = host if host.startswith('http') else f'https://{host}'
self.host = self.host.rstrip('/')
Comment on lines +177 to +178

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major · Security & Privacy · Quick win

Reject HTTP workspace URLs before sending the token.

If DATABRICKS_HOST or DATABRICKS_URL starts with http://, Workspace.api sends DATABRICKS_TOKEN in an unencrypted Authorization header. Require an https:// URL before the first request. Databricks documents bearer-token API calls over HTTPS. (docs.databricks.com)

self.token = token

def api(self, method: str, path: str, body: dict | None = None,
query: dict | None = None) -> dict:
url = self.host + path + ('?' + urllib.parse.urlencode(query) if query else '')
req = urllib.request.Request(
url, method=method, data=json.dumps(body).encode() if body is not None else None,
headers={'Authorization': f'Bearer {self.token}', 'Content-Type': 'application/json'})
try:
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read() or b'{}')
except urllib.error.HTTPError as e:
raise SystemExit(f'{method} {path}: HTTP {e.code} {e.read().decode()[:300]}') from None

def me(self) -> str:
return self.api('GET', '/api/2.0/preview/scim/v2/Me')['userName']

def listing(self, path: str, dirs: bool = False) -> list[str]:
"""Every file under `path`, recursively — or, with `dirs`, every directory, deepest
first; [] when it does not exist."""
try:
objects = self.api('GET', '/api/2.0/workspace/list', query=dict(path=path))
except SystemExit as e:
if 'RESOURCE_DOES_NOT_EXIST' in str(e):
return []
raise
out = []
for o in objects.get('objects', []):
if o['object_type'] == 'DIRECTORY':
out += self.listing(o['path'], dirs) + ([o['path']] if dirs else [])
elif not dirs:
out.append(o['path'])
return out

def delete(self, path: str) -> None:
"""Delete one file or one empty directory; never recursive."""
self.api('POST', '/api/2.0/workspace/delete', dict(path=path, recursive=False))


def read_env_file(path: Path) -> None:
"""Set KEY=VALUE lines from `path` into the environment, without overriding it."""
for line in path.read_text().splitlines():
key, sep, value = line.strip().partition('=')
if sep and not key.startswith('#'):
os.environ.setdefault(key.strip(), value.strip().strip('\'"'))


def deploy_genie(pkg: Path, ws: Workspace, prune: bool = False) -> str:
"""Upload `pkg` to the user's Genie Code skills folder and read every file back.

Files in the target that the package does not ship — an older build's host directory, say —
are reported, and with `prune` deleted, along with directories they leave empty. Pruning runs
before the upload, so a failed delete leaves the old build rather than two host directories.
Only the `kumo-data-science/` folder is touched; other skills beside it never are, and a listed
path outside it stops the deploy before anything is deleted. A stale file under `hosts/` is
refused without `prune` before anything is written: the router expects exactly one host
directory, so a second one left in place would let Genie read the wrong adapter.
"""
target = f'{GENIE_SKILLS.format(user=ws.me())}/{ROUTER}'
files = sorted(p for p in pkg.rglob('*') if p.is_file())
remote = {f'{target}/{p.relative_to(pkg).as_posix()}': p for p in files}

def inside(paths: list[str]) -> list[str]:
outside = [path for path in paths if not path.startswith(target + '/')]
if outside:
raise SystemExit(f'workspace listing of {target} returned paths outside it '
f'(e.g. {outside[0]}). Nothing was deleted.')
return paths

stale = sorted(set(inside(ws.listing(target))) - set(remote))
stale_hosts = [path for path in stale if path.startswith(f'{target}/hosts/')]
if stale_hosts and not prune:
raise SystemExit(f'{len(stale_hosts)} file(s) under {target}/hosts/ are not in this package '
f'(e.g. {stale_hosts[0]}); a second host directory would be read. '
f'Rerun with --prune to delete them. Nothing was written.')
if prune:
for path in stale:
ws.delete(path)
print(f' not in this package, deleted: {path}')
for d in inside(ws.listing(target, dirs=True)):
if not any(p.startswith(d + '/') for p in remote) and not ws.listing(d):
ws.delete(d)
print(f' empty directory deleted: {d}')
for path, p in remote.items():
ws.api('POST', '/api/2.0/workspace/mkdirs', dict(path=path.rsplit('/', 1)[0]))
ws.api('POST', '/api/2.0/workspace/import', dict(
path=path, format='AUTO', overwrite=True,
content=base64.b64encode(p.read_bytes()).decode()))
bad = [path for path, p in remote.items()
if base64.b64decode(ws.api('GET', '/api/2.0/workspace/export',
query=dict(path=path, format='AUTO')).get('content', ''))
!= p.read_bytes()]
if bad:
raise SystemExit(f'deploy check failed for {len(bad)} file(s): {bad[:5]}')
print(f'genie: {len(files)} files deployed and read back byte for byte -> {target}')
if not prune and stale:
for path in stale:
print(f' not in this package, left in place: {path}')
print(' --prune deletes these.')
print('Start a new Genie Code chat (agent mode) to load it; ask it to quote SKILL-BUILD.')
return target


def main() -> None:
ap = argparse.ArgumentParser(description='Build and check a kumo-data-science skill package.')
ap.add_argument('host', nargs='?', choices=sorted(HOSTS))
ap.add_argument('--out', type=Path, default=DIST, help='output root (default: %(default)s)')
ap.add_argument('--check', action='store_true', help='build and check every host in a temp dir')
ap.add_argument('--deploy', action='store_true',
help='genie: upload to your Genie Code skills folder and read it back')
ap.add_argument('--prune', action='store_true',
help='genie --deploy: delete files in the target the package does not ship')
ap.add_argument('--env-file', type=Path, help='KEY=VALUE file with DATABRICKS_HOST/_TOKEN')
args = ap.parse_args()
if not args.check and not args.host:
ap.error('give a host, or --check')
if args.deploy and (args.check or args.host != 'genie'):
ap.error('--deploy is for the genie host')
if args.prune and not args.deploy:
ap.error('--prune goes with --deploy')

failed = False
with tempfile.TemporaryDirectory() as tmp:
Expand All @@ -168,7 +307,16 @@ def main() -> None:
for e in errors:
print(f' {e}')
failed |= bool(errors)
sys.exit(1 if failed else 0)
if failed:
sys.exit(1)
if args.deploy:
if args.env_file:
read_env_file(args.env_file)
host = os.environ.get('DATABRICKS_HOST') or os.environ.get('DATABRICKS_URL')
token = os.environ.get('DATABRICKS_TOKEN')
if not host or not token:
sys.exit('--deploy needs DATABRICKS_HOST (or DATABRICKS_URL) and DATABRICKS_TOKEN')
deploy_genie(pkg, Workspace(host, token), prune=args.prune)


if __name__ == '__main__':
Expand Down
Loading
Loading