Skip to content

Commit d3dfcea

Browse files
fix: support tutorial on a local Orca stack
Use Registry workspace keys for local Agent Engine requests while retaining hosted Bearer authentication. Add an Agent-only doctor mode, archive used environments during cleanup, and align Kafka preflight with the actual topic. Assisted-by: Codex
1 parent 87a67e7 commit d3dfcea

24 files changed

Lines changed: 261 additions & 63 deletions

‎.env.example‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ SN_API_KEY=
1111
# Looks like: <service-account>@<org>.auth.streamnative.cloud
1212
SN_SERVICE_ACCOUNT=
1313

14+
# Optional separate Registry workspace key for ork local (sent as x-api-key).
15+
# Leave empty for a hosted team card; SN_API_KEY then authenticates Agent Engine.
16+
# This key does not authenticate Kafka, Schema Registry, or StreamNative MCP.
17+
ORCA_API_KEY=
18+
1419
# Agent Engine registry endpoint (the External one). Host root only, no /v1.
1520
# Looks like: https://<workspace-host>
1621
ORCA_BASE_URL=
@@ -24,8 +29,8 @@ SN_MCP_URL=
2429

2530
# ------------------------------------------------------------- your choices --
2631

27-
# The preloaded login topic.
28-
LOGIN_TOPIC=avro.security.login_events
32+
# The Kafka topic name (SQL Workspace exposes it as avro.security.login_events).
33+
LOGIN_TOPIC=security.login_events
2934

3035
# The model your agent runs on (served by the event's AI gateway).
3136
ORCA_MODEL=claude-sonnet-4-6

‎README.md‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ them from live data, and flag the account once you say so.
1818

1919
```mermaid
2020
flowchart LR
21-
K["Kafka topic<br/>avro.security.login_events"] --> S["SQL Workspace<br/>materialized view<br/>login_failures"]
21+
K["Kafka topic<br/>security.login_events"] --> S["SQL Workspace<br/>materialized view<br/>login_failures"]
2222
J["inject<br/>(you, in L3)"] -- "new login burst" --> K
2323
S -- "StreamNative MCP<br/>sql_workspace_query" --> A["Orca agent<br/>hello-agent-&lt;you&gt;"]
2424
A -- "sql_workspace_insert_rows<br/>(only if you approve)" --> F["SQL table<br/>flagged_accounts"]
@@ -57,6 +57,26 @@ Go to your path's folder and run the doctor:
5757
Every line should say `PASS`. A failed check prints its fix. Still stuck after
5858
two tries? Raise your hand.
5959

60+
### Use a local Agent Engine
61+
62+
Start the CLI's stack with a provider key in your shell:
63+
64+
```bash
65+
export ANTHROPIC_API_KEY='<your-provider-key>'
66+
ork local start --with-gateway
67+
```
68+
69+
Set `ORCA_BASE_URL=http://127.0.0.1:8080` in the tutorial's `.env`, and copy the
70+
workspace key from the file printed by `ork local start` into `ORCA_API_KEY`.
71+
The tutorial sends this key as `x-api-key`. A hosted team card continues to use
72+
`SN_API_KEY` as a Bearer token when `ORCA_API_KEY` is empty.
73+
74+
For L1, run `python doctor.py --agent-only` or `npm run doctor -- --agent-only`.
75+
This checks the Agent Engine without requiring Kafka, Schema Registry, or MCP.
76+
The local stack provides the Agent Engine and AI Gateway; L2–L4 still need the
77+
streaming data services from your team card. For L3/L4, keep `SN_API_KEY` set to
78+
the MCP service key, separately from the local Registry's `ORCA_API_KEY`.
79+
6080
## L1: Hello, agent (5 min)
6181

6282
| CLI | Python | TypeScript |
@@ -150,6 +170,8 @@ create a new version when its definition changes.
150170

151171
In the StreamNative Cloud console, open **SQL Workspace**, select the hackathon
152172
workspace, and pick your team's database. Use a new query tab for each step.
173+
The Kafka topic is `security.login_events` (`LOGIN_TOPIC` in `.env`); SQL Workspace
174+
exposes the Avro source as `"avro.security.login_events"`.
153175

154176
**1. Peek at the stream** ([`sql/01_explore.sql`](sql/01_explore.sql)). Each row
155177
is one login attempt. The topic name contains dots, so it's double-quoted.
@@ -353,8 +375,9 @@ action, and you have your hackathon project. Ideas and next steps:
353375
|---|---|---|
354376
| `./cleanup.sh` | `python cleanup.py` | `npm run cleanup` |
355377

356-
This archives your agent and deletes your vault and environment. To start L2
357-
over, run [`sql/99_reset.sql`](sql/99_reset.sql).
378+
This archives your agent and environment, and deletes your vault. An environment
379+
with session history cannot be deleted; archiving keeps that history available.
380+
To start L2 over, run [`sql/99_reset.sql`](sql/99_reset.sql).
358381

359382
## What's in this repository
360383

‎cli/cleanup.sh‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
set -euo pipefail
99
# shellcheck source=lib.sh
1010
. "$(dirname "$0")/lib.sh"
11-
hello_setup ORCA_BASE_URL SN_API_KEY
11+
hello_setup ORCA_BASE_URL
1212

1313
remove() { # remove <label> <state key> <ork command...>
1414
local id
@@ -26,5 +26,5 @@ remove() { # remove <label> <state key> <ork command...>
2626
# Agents cannot be deleted, only archived.
2727
remove agent agent_id agent archive
2828
remove vault vault_id agent vaults delete
29-
remove environment environment_id agent environments delete
29+
remove environment environment_id agent environments archive
3030
rm -f "$HELLO_STATE_FILE"

‎cli/env.sh‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,17 @@ hello_setup() {
7575
hello_die "jq is not installed. Install it (brew install jq, apt install jq, or winget install jqlang.jq) and try again."
7676

7777
hello_load_dotenv
78-
hello_require "$@"
79-
80-
# ork reads ORCA_REGISTRY_URL and ORCA_ACCESS_TOKEN (sent as a Bearer token),
81-
# and refuses to start if ORCA_API_KEY is also set.
78+
# Registry workspace keys use x-api-key; team-card keys use Bearer.
79+
# Keep the two CLI credentials mutually exclusive.
80+
if [ -n "$(hello_trim "${ORCA_API_KEY:-}")" ]; then
81+
hello_require "$@" ORCA_API_KEY
82+
unset ORCA_ACCESS_TOKEN
83+
else
84+
hello_require "$@" SN_API_KEY
85+
export ORCA_ACCESS_TOKEN="$SN_API_KEY"
86+
unset ORCA_API_KEY
87+
fi
8288
export ORCA_REGISTRY_URL="$ORCA_BASE_URL"
83-
export ORCA_ACCESS_TOKEN="$SN_API_KEY"
84-
unset ORCA_API_KEY
8589

8690
local who
8791
who=$(printenv PARTICIPANT || true)

‎cli/l1_hello.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
set -euo pipefail
1212
# shellcheck source=lib.sh
1313
. "$(dirname "$0")/lib.sh"
14-
hello_setup ORCA_BASE_URL SN_API_KEY ORCA_MODEL
14+
hello_setup ORCA_BASE_URL ORCA_MODEL
1515

1616
QUESTION="Hi! What is the Data + Agent Hackathon, and what can you see right now?"
1717
[ $# -eq 0 ] || QUESTION="$*"

‎cli/tests/fake-ork‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ set -euo pipefail
1919
dir=${FAKE_ORK_DIR:?FAKE_ORK_DIR is not set}
2020
mkdir -p "$dir/agents" "$dir/envs" "$dir/vaults" "$dir/creds" "$dir/sessions" "$dir/reactions"
2121
jq -cn '$ARGS.positional' --args -- "$@" >>"$dir/calls.log"
22+
api_key_set=false
23+
access_token_set=false
24+
[ -z "${ORCA_API_KEY:-}" ] || api_key_set=true
25+
[ -z "${ORCA_ACCESS_TOKEN:-}" ] || access_token_set=true
26+
jq -cn --argjson api "$api_key_set" --argjson bearer "$access_token_set" \
27+
'{api_key_set: $api, access_token_set: $bearer}' >"$dir/auth.json"
2228

2329
fail() { # fail <status> <method> <path>
2430
printf 'Error: failed: %s http://fake.invalid%s returned status %s: {"type":"error","error":{"type":"error","message":"fake %s"}}\n' \
@@ -164,6 +170,12 @@ case "$command" in
164170
[ -f "$dir/envs/${words[3]}.json" ] || fail 404 GET "/v1/environments/${words[3]}"
165171
cat "$dir/envs/${words[3]}.json"
166172
;;
173+
"agent environments archive "*)
174+
[ -f "$dir/envs/${words[3]}.json" ] || fail 404 POST "/v1/environments/${words[3]}/archive"
175+
jq --arg now "$(now)" '.archived_at = $now' "$dir/envs/${words[3]}.json" >"$dir/env.tmp"
176+
mv "$dir/env.tmp" "$dir/envs/${words[3]}.json"
177+
echo "archived ${words[3]}"
178+
;;
167179
"agent environments delete "*)
168180
[ -f "$dir/envs/${words[3]}.json" ] || fail 404 DELETE "/v1/environments/${words[3]}"
169181
rm "$dir/envs/${words[3]}.json"

‎cli/tests/run.sh‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ test_missing_team_card() {
106106
run "" l1_hello.sh
107107
check "exits 1 without a team card" [ "$STATUS" -eq 1 ]
108108
check "names every missing variable" \
109-
err_has "Missing ORCA_BASE_URL, SN_API_KEY, ORCA_MODEL. Copy .env.example to .env in the repo root and fill it in from your team card."
109+
err_has "Missing ORCA_BASE_URL, ORCA_MODEL, SN_API_KEY. Copy .env.example to .env in the repo root and fill it in from your team card."
110110
check "runs no ork command" [ ! -s "$FAKE_ORK_DIR/calls.log" ]
111111
}
112112

@@ -116,6 +116,7 @@ test_l1_creates_everything() {
116116
reaction 1 "$(message evt_a 'Hello!')" "$(end_turn 1)"
117117
run "" l1_hello.sh
118118
check "L1 succeeds" [ "$STATUS" -eq 0 ]
119+
check "hosted team cards use only Bearer" jq -e '.access_token_set and (.api_key_set | not)' "$FAKE_ORK_DIR/auth.json"
119120
check "creates the environment" called '["agent","environments","create","--name","hello-env-jane","-o","json"]'
120121
check "creates the agent with the L1 fingerprint" called_with "agent create" "definition_sha=$SHA_L1"
121122
check "names the agent after the participant" called_with "agent create" "hello-agent-jane"
@@ -176,7 +177,7 @@ test_l1_creates_everything() {
176177
check "cleanup succeeds" [ "$STATUS" -eq 0 ]
177178
check "archives the agent" out_has "removed agent agent_2"
178179
check "deletes the vault" out_has "removed vault vlt_1"
179-
check "deletes the environment" out_has "removed environment env_1"
180+
check "archives the environment" out_has "removed environment env_1"
180181
check "forgets the ids" [ ! -f "$R/.orca-state/jane.json" ]
181182
}
182183

@@ -323,6 +324,23 @@ test_team_card_parsing() {
323324
check "state goes to the participant's file" [ -f "$R/.orca-state/jane-doe-42.json" ]
324325
}
325326

327+
test_local_registry_key() {
328+
fresh_repo local-key
329+
cat >"$R/.env" <<EOF
330+
ORCA_BASE_URL=http://127.0.0.1:8080
331+
ORCA_API_KEY=local-test-key
332+
ORCA_MODEL=claude-sonnet-4-6
333+
PARTICIPANT=jane
334+
EOF
335+
export ORCA_ACCESS_TOKEN=stale-bearer
336+
reaction 1 "$(message evt_a 'Hello locally!')" "$(end_turn local)"
337+
run "" l1_hello.sh
338+
unset ORCA_ACCESS_TOKEN
339+
check "local L1 needs no team-card key" [ "$STATUS" -eq 0 ]
340+
check "local key uses only x-api-key" jq -e '.api_key_set and (.access_token_set | not)' "$FAKE_ORK_DIR/auth.json"
341+
}
342+
343+
test_local_registry_key
326344
test_missing_team_card
327345
test_l1_creates_everything
328346
test_turns_ignore_history_and_duplicates

‎python/cleanup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
def main() -> None:
12-
config = load_config(["ORCA_BASE_URL", "SN_API_KEY"])
12+
config = load_config(["ORCA_BASE_URL"])
1313
cleanup(orca_client(config), state_for(config))
1414

1515

‎python/common.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,12 @@ def _lookup(name: str, config: Config) -> str:
130130

131131

132132
def orca_client(config: Config) -> Orca:
133-
"""One service-account API key, sent as a Bearer token, authenticates everything."""
134-
return Orca(base_url=config["ORCA_BASE_URL"], api_key=config["SN_API_KEY"], timeout=600)
133+
"""Use a Registry workspace key locally, or the team card's hosted Bearer key."""
134+
if key := config.values.get("ORCA_API_KEY"):
135+
return Orca(base_url=config["ORCA_BASE_URL"], api_key=None, default_headers={"x-api-key": key}, timeout=600)
136+
if key := config.values.get("SN_API_KEY"):
137+
return Orca(base_url=config["ORCA_BASE_URL"], api_key=key, timeout=600)
138+
raise ConfigError("Set ORCA_API_KEY for ork local, or SN_API_KEY from your team card.")
135139

136140

137141
def ensure_environment(client: Any, state: State, name: str) -> str:
@@ -216,11 +220,11 @@ def ask_human(tool_use: dict[str, Any], *, ask: Callable[[str], str] = input, ou
216220

217221

218222
def cleanup(client: Any, state: State, *, out: Callable[[str], None] = print) -> None:
219-
"""Remove what the scripts created. Agents cannot be deleted, only archived."""
223+
"""Archive the agent and environment, and delete the vault. Sessions reserve the environment."""
220224
steps = [
221225
("agent", "agent_id", client.agents.archive),
222226
("vault", "vault_id", client.vaults.delete),
223-
("environment", "environment_id", client.environments.delete),
227+
("environment", "environment_id", client.environments.archive),
224228
]
225229
for label, key, remove in steps:
226230
resource_id = state.get(key)

‎python/doctor.py‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
python doctor.py # laptop + .env + Agent Engine + Kafka + Schema Registry + MCP
44
python doctor.py --offline # laptop only (run this before the event)
5+
python doctor.py --agent-only # laptop + Agent Engine (for ork local / L1)
56
67
Every failed check prints the fix.
78
"""
@@ -40,9 +41,10 @@ def check_python(version: tuple) -> Check:
4041

4142
def check_orca_base_url(url: str) -> Check:
4243
parts = urlsplit(url.strip())
43-
if parts.scheme != "https" or not parts.netloc:
44-
return Check("ORCA_BASE_URL", False, url, "Use the full https:// registry endpoint from your team card.")
45-
root = f"https://{parts.netloc}"
44+
local_http = parts.scheme == "http" and parts.hostname in ("localhost", "127.0.0.1", "::1")
45+
if not parts.netloc or (parts.scheme != "https" and not local_http):
46+
return Check("ORCA_BASE_URL", False, url, "Use the https:// registry endpoint from your team card, or http://127.0.0.1:8080 for ork local.")
47+
root = f"{parts.scheme}://{parts.netloc}"
4648
if parts.path.rstrip("/"):
4749
return Check("ORCA_BASE_URL", False, url, f"Use the host root only: ORCA_BASE_URL={root}")
4850
return Check("ORCA_BASE_URL", True, root)
@@ -98,9 +100,11 @@ def kafka_hint(error: str) -> str:
98100
# ---------------------------------------------------------------- probes --
99101

100102

101-
def check_packages() -> list[Check]:
103+
def check_packages(agent_only: bool = False) -> list[Check]:
102104
checks = []
103105
for module, package in PACKAGES.items():
106+
if agent_only and module == "confluent_kafka":
107+
continue
104108
found = importlib.util.find_spec(module) is not None
105109
checks.append(Check(f"package {package}", found, "", "" if found else "Run: pip install -r requirements.txt"))
106110
return checks
@@ -123,7 +127,7 @@ def probe_orca(config: Any) -> Check:
123127
orca_client(config).agents.list(limit=1)
124128
except APIStatusError as err:
125129
if err.status_code in (401, 403):
126-
fix = "The Agent Engine rejected SN_API_KEY. A key created before its rolebinding must be re-created: ask a facilitator."
130+
fix = "The Agent Engine rejected the key. For ork local use its generated workspace key as ORCA_API_KEY; for a team card check SN_API_KEY and its rolebinding."
127131
elif err.status_code == 404:
128132
fix = "ORCA_BASE_URL is not an Agent Engine registry: copy the registry endpoint from your team card."
129133
else:
@@ -151,7 +155,8 @@ def probe_kafka(config: Any) -> Check:
151155
)
152156
topic_name = config["LOGIN_TOPIC"]
153157
try:
154-
topic = admin.list_topics(topic=topic_name, timeout=15).topics.get(topic_name)
158+
# Listing all existing topics avoids metadata requests that can auto-create a missing topic.
159+
topic = admin.list_topics(timeout=15).topics.get(topic_name)
155160
except KafkaException as err:
156161
reason = errors[0] if errors else str(err)
157162
return Check("Kafka", False, reason, kafka_hint(reason))
@@ -219,15 +224,18 @@ def probe_mcp(config: Any) -> Check:
219224
# ------------------------------------------------------------------ main --
220225

221226

222-
def run_checks(offline: bool) -> list[Check]:
223-
checks = [check_python(sys.version_info), *check_packages(), *check_cli_tools()]
227+
def run_checks(offline: bool, agent_only: bool = False) -> list[Check]:
228+
checks = [check_python(sys.version_info), *check_packages(agent_only), *check_cli_tools()]
224229
if offline or not all(check.ok for check in checks):
225230
return checks
226231

227232
from common import load_config
228233

229234
config = load_config([])
230-
missing = [name for name in CARD if name not in config.values]
235+
required = ("ORCA_BASE_URL", "ORCA_MODEL") if agent_only else CARD
236+
missing = [name for name in required if name not in config.values]
237+
if agent_only and not any(name in config.values for name in ("ORCA_API_KEY", "SN_API_KEY")):
238+
missing.append("ORCA_API_KEY or SN_API_KEY")
231239
if missing:
232240
fix = "Copy .env.example to .env in the repo root and paste your team card."
233241
return [*checks, Check("team card (.env)", False, "missing " + ", ".join(missing), fix)]
@@ -237,14 +245,16 @@ def run_checks(offline: bool) -> list[Check]:
237245
checks.append(base_url)
238246
if base_url.ok:
239247
checks.append(probe_orca(config))
248+
if agent_only:
249+
return checks
240250
checks.append(probe_kafka(config))
241251
checks += probe_schema_registry(config)
242252
checks.append(probe_mcp(config))
243253
return checks
244254

245255

246256
def main() -> int:
247-
checks = run_checks(offline="--offline" in sys.argv[1:])
257+
checks = run_checks(offline="--offline" in sys.argv[1:], agent_only="--agent-only" in sys.argv[1:])
248258
for check in checks:
249259
print(f"{'PASS' if check.ok else 'FAIL'} {check.name:<32} {check.detail}")
250260
if not check.ok and check.fix:

0 commit comments

Comments
 (0)