Skip to content

Commit 4340c6b

Browse files
Merge pull request #2 from streamnative/fix/local-stack-compatibility
fix: support tutorial on a local Orca stack
2 parents 87a67e7 + 298cb4e commit 4340c6b

41 files changed

Lines changed: 881 additions & 152 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33

44
# ---------------------------------------------------------------- team card --
55

6-
# Service-account API key (API Key v2). One key is used everywhere: the Orca
7-
# Agent Engine API, Kafka, Schema Registry, and the StreamNative MCP server.
6+
# Service-account API key (API Key v2) for the hosted Agent Engine API,
7+
# Kafka and Schema Registry. OAuth MCP servers use a separate browser login.
88
SN_API_KEY=
99

1010
# Service-account principal, used as the Kafka SASL username.
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=
@@ -22,10 +27,24 @@ SCHEMA_REGISTRY_URL=
2227
# StreamNative MCP server for your SQL Workspace (the agent's data tools).
2328
SN_MCP_URL=
2429

30+
# MCP authentication: oauth (default) or static_bearer for API-key MCP servers.
31+
# All three tutorial paths call ork for the first OAuth login, then reuse the
32+
# credential stored in the vault. Install ork with the OAuth discovery support
33+
# from orca-cli PR #8 (or current main).
34+
SN_MCP_AUTH=oauth
35+
36+
# Optional authorization server selection. Leave empty for automatic discovery.
37+
# If multiple servers are advertised, copy one exact authorization_servers value
38+
# from the MCP protected-resource metadata; do not substitute the metadata issuer.
39+
SN_MCP_OAUTH_ISSUER=
40+
SN_MCP_OAUTH_SCOPE="openid profile email offline_access"
41+
2542
# ------------------------------------------------------------- your choices --
2643

27-
# The preloaded login topic.
28-
LOGIN_TOPIC=avro.security.login_events
44+
# The Kafka topic name. Injectors and doctor read this value.
45+
# L2 SQL files use the default below: edit their quoted "avro.<LOGIN_TOPIC>"
46+
# source name to match this value before running them in SQL Workspace.
47+
LOGIN_TOPIC=security.login_events
2948

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

‎README.md‎

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ 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"]
2525
```
2626

2727
| Step | Time | Where | You | The idea |
2828
|---|---|---|---|---|
29-
| [0. Connect](#step-0-connect-3-min) | 3 min | terminal | Fill in `.env`, run the doctor | One key, checked end to end |
29+
| [0. Connect](#step-0-connect-3-min) | 3 min | terminal | Fill in `.env`, run the doctor | Check service access; authorize MCP with OAuth |
3030
| [L1. Hello, agent](#l1-hello-agent-5-min) | 5 min | CLI / Python / TS | Create an agent and chat | Agent, environment, session, events |
3131
| [L2. Hello, streaming SQL](#l2-hello-streaming-sql-8-min) | 8 min | SQL Workspace | Build a materialized view over the topic | Context that keeps itself fresh |
3232
| [L3. Agent + live context](#l3-agent--live-context-9-min) | 9 min | CLI / Python / TS | Give the agent SQL tools, inject new data | The answer changes with the data |
@@ -54,8 +54,52 @@ Go to your path's folder and run the doctor:
5454
| TypeScript | `cd typescript && npm run doctor` |
5555
| CLI | `cd cli`, and run the doctor from your helper language: `(cd ../python && .venv/bin/python doctor.py)` or `(cd ../typescript && npm run doctor)` |
5656

57-
Every line should say `PASS`. A failed check prints its fix. Still stuck after
58-
two tries? Raise your hand.
57+
The service checks should say `PASS`. Before the first OAuth login, the MCP
58+
check asks you to run L3; that script opens your browser and stores the credential
59+
in a vault. After completing L2 and running L3, rerun the doctor to validate the
60+
stored OAuth credential. This check verifies MCP initialization; L3/L4 exercise
61+
the actual SQL tools. A failed check prints its fix. Still stuck after two tries?
62+
Raise your hand.
63+
64+
For StreamNative SQL Workspace MCP, keep `SN_MCP_AUTH=oauth`, leave
65+
`SN_MCP_OAUTH_ISSUER` empty for automatic discovery, and use the scope from
66+
`.env.example`. Use an `ork` build containing [PR #8](https://github.com/orca-ae/orca-cli/pull/8)
67+
or current main. Its discovery accepts HTTPS issuer aliases within the same
68+
registrable domain and port. Only set `SN_MCP_OAUTH_ISSUER` when selecting one
69+
of multiple advertised `authorization_servers`; copy that advertised value
70+
exactly rather than the final issuer in authorization-server metadata.
71+
`SN_API_KEY` authenticates the hosted Agent Engine,
72+
Kafka and Schema Registry; it is not the OAuth MCP access token. All three paths
73+
use `ork` for the first MCP login, then reuse the live credential for the same URL
74+
and auth type from `.orca-state/<participant>.json`. Tokens stay in the server-side
75+
vault, where they can be refreshed; they are never written to `.env` or local state.
76+
Set `SN_MCP_AUTH=static_bearer` only when your MCP server accepts `SN_API_KEY`.
77+
Changing the auth mode archives the previous live credential for that same URL
78+
before creating its replacement (the Registry permits one active credential per
79+
URL in a vault). If authorization fails, rerun L3/L4 to finish setup; other URLs'
80+
credentials are preserved. A local Agent Engine with OAuth MCP needs only
81+
`ORCA_API_KEY` for Registry authentication; `SN_API_KEY` is still needed for Kafka
82+
and Schema Registry.
83+
84+
### Use a local Agent Engine
85+
86+
Start the CLI's stack with a provider key in your shell:
87+
88+
```bash
89+
export ANTHROPIC_API_KEY='<your-provider-key>'
90+
ork local start --with-gateway
91+
```
92+
93+
Set `ORCA_BASE_URL=http://127.0.0.1:8080` in the tutorial's `.env`, and copy the
94+
workspace key from the file printed by `ork local start` into `ORCA_API_KEY`.
95+
The tutorial sends this key as `x-api-key`. A hosted team card continues to use
96+
`SN_API_KEY` as a Bearer token when `ORCA_API_KEY` is empty.
97+
98+
For L1, run `python doctor.py --agent-only` or `npm run doctor -- --agent-only`.
99+
This checks the Agent Engine without requiring Kafka, Schema Registry, or MCP.
100+
The local stack provides the Agent Engine and AI Gateway; L2–L4 still need the
101+
streaming data services from your team card. For L3/L4, keep `SN_API_KEY` set to
102+
the MCP service key, separately from the local Registry's `ORCA_API_KEY`.
59103

60104
## L1: Hello, agent (5 min)
61105

@@ -150,6 +194,19 @@ create a new version when its definition changes.
150194

151195
In the StreamNative Cloud console, open **SQL Workspace**, select the hackathon
152196
workspace, and pick your team's database. Use a new query tab for each step.
197+
The default Kafka topic is `security.login_events`; SQL Workspace exposes its
198+
Avro source as `"avro.security.login_events"`.
199+
200+
**Align the SQL with your `.env` before running it.** The injectors and doctor use
201+
`LOGIN_TOPIC`, but the SQL files and examples below contain a fixed source name:
202+
SQL Workspace does not read your local `.env`. Check `LOGIN_TOPIC`, then replace
203+
`"avro.security.login_events"` with `"avro.<your LOGIN_TOPIC>"` in both
204+
[`sql/01_explore.sql`](sql/01_explore.sql) and
205+
[`sql/02_login_failures.sql`](sql/02_login_failures.sql), and in any query copied
206+
from this page. For example, `LOGIN_TOPIC=security.team07_logins` requires
207+
`FROM "avro.security.team07_logins"`. Keep the double quotes around the entire
208+
source name and confirm that SQL Workspace imported that topic as an Avro source.
209+
Keep the `login_failures` view name: L3/L4 query that view.
153210

154211
**1. Peek at the stream** ([`sql/01_explore.sql`](sql/01_explore.sql)). Each row
155212
is one login attempt. The topic name contains dots, so it's double-quoted.
@@ -228,8 +285,10 @@ event landed in Kafka, the view updated itself, and the agent read the view.
228285
- `mcp_servers`: the StreamNative MCP server for your SQL Workspace.
229286
- `tools`: an allow-list. Two read-only tools run without asking
230287
(`always_allow`); every other tool on that server is disabled.
231-
- A **vault**: the MCP server's credential (your team key) is stored server-side.
232-
The session references the vault by id, so the key never enters the prompt.
288+
- A **vault**: the MCP server's OAuth credential is created through `ork` and
289+
stored server-side. Approve the browser login on the first run. The session
290+
references the vault by id, so tokens never enter the prompt. Later runs reuse
291+
the credential without another browser login.
233292

234293
<details>
235294
<summary>The code (Python)</summary>
@@ -238,7 +297,7 @@ event landed in Kafka, the view updated itself, and the agent read the view.
238297
layer = load_layer("l3-live-context")
239298
agent = ensure_agent(client, state, agent_params(layer, config))
240299

241-
vault_id = ensure_vault(client, state, f"hello-vault-{config.participant}", config["SN_MCP_URL"], config["SN_API_KEY"])
300+
vault_id = ensure_vault(client, state, f"hello-vault-{config.participant}", config)
242301
session = client.sessions.create(
243302
environment_id=environment_id,
244303
agent={"type": "agent", "id": agent.id, "version": agent.version},
@@ -256,7 +315,7 @@ chat(client, session.id, QUESTION)
256315
const layer = loadLayer('l3-live-context');
257316
const agent = await ensureAgent(client, state, agentParams(layer, config));
258317

259-
const vaultId = await ensureVault(client, state, `hello-vault-${config.participant}`, config.get('SN_MCP_URL'), config.get('SN_API_KEY'));
318+
const vaultId = await ensureVault(client, state, `hello-vault-${config.participant}`, config);
260319
const session = await client.sessions.create({
261320
environment_id: environmentId,
262321
agent: { type: 'agent', id: agent.id, version: agent.version },
@@ -278,7 +337,8 @@ ork agent update "$AGENT_ID" --version 1 --model "$ORCA_MODEL" \
278337

279338
ork agent vaults create --display-name hello-vault-ana -o json
280339
ork agent vaults credentials create --vault "$VAULT_ID" --display-name streamnative-mcp \
281-
--auth-json '{"type":"static_bearer","mcp_server_url":"<SN_MCP_URL>","token":"<SN_API_KEY>"}'
340+
--mcp-server-url "$SN_MCP_URL" \
341+
--oauth-scope "$SN_MCP_OAUTH_SCOPE" -o json
282342

283343
ork agent sessions create --agent "$AGENT_ID" --agent-version 2 \
284344
--environment-id "$ENVIRONMENT_ID" --vault-id "$VAULT_ID" --title "L3: live context" -o json
@@ -292,13 +352,22 @@ ork agent sessions create --agent "$AGENT_ID" --agent-version 2 \
292352
| `./l4_act.sh` | `python l4_act.py` | `npm run l4` |
293353

294354
The agent (version 3) gets one write tool, and it can only use it with your
295-
approval. It queries the view, then proposes an insert, and the session pauses:
355+
approval. It queries the view, describes the flag table, and reads the database
356+
time before proposing an insert. The MCP insert tool requires every writable
357+
column, including nullable columns; it does not apply table defaults. The
358+
session pauses before the proposed row is written:
296359

297360
```
298361
[approve?] The agent wants to run sql_workspace_insert_rows with:
299362
{
363+
"database": "<your database>",
364+
"schema": "public",
300365
"table": "flagged_accounts",
301-
"rows": [{"account_id": "acct_9…", "reason": "6 failed logins then a success from one new IP"}]
366+
"rows": [{
367+
"account_id": "acct_9…",
368+
"reason": "6 failed logins then a success from one new IP",
369+
"flagged_at": "2026-09-30T12:00:00Z"
370+
}]
302371
}
303372
Allow it? [y/N]
304373
```
@@ -309,11 +378,12 @@ Type `y`, then check in SQL Workspace:
309378
SELECT * FROM flagged_accounts;
310379
```
311380

312-
Ask again, and answer `n` this time. The agent is told a human denied the insert,
381+
Ask the agent to flag a different account, and answer `n` this time. The agent is told a human denied the insert,
313382
and it does not retry.
314383

315-
**What changed** ([`agent/l4-act.json`](agent/l4-act.json)): one more tool,
316-
`sql_workspace_insert_rows`, with `permission_policy: always_ask`. When the agent
384+
**What changed** ([`agent/l4-act.json`](agent/l4-act.json)): the read-only
385+
`sql_workspace_describe_table` checks the required columns, and
386+
`sql_workspace_insert_rows` uses `permission_policy: always_ask`. When the agent
317387
calls it, the session emits `agent.mcp_tool_use` and goes idle with
318388
`stop_reason: requires_action`. Your script answers with a
319389
`user.tool_confirmation`: `allow`, or `deny` with a reason. On the CLI that is:
@@ -341,9 +411,10 @@ action, and you have your hackathon project. Ideas and next steps:
341411
| Doctor: `Agent Engine HTTP 401/403` | The key was rejected. A key created before its permissions must be re-created: ask a facilitator. |
342412
| Doctor: `Kafka ... authentication` | `SN_SERVICE_ACCOUNT` must be the full principal, `<name>@<org>.auth.streamnative.cloud`; `SN_API_KEY` is the raw key. |
343413
| The login topic isn't listed in SQL Workspace | Only topics with a registered Avro schema appear. Ask a facilitator. |
344-
| `relation "avro.security.login_events" does not exist` | Select your team's database, and keep the double quotes around the name. |
414+
| `relation "avro.security.login_events" does not exist` | Select your team's database and update the quoted Avro source in both L2 SQL files to match `LOGIN_TOPIC` in `.env`. |
345415
| The agent can't find `login_failures` | Create the view in your team's database (L2, step 2); the agent looks it up there. |
346-
| `[error]` lines from MCP tools in L3 | Check `SN_MCP_URL` against your team card, then rerun the doctor. |
416+
| `[error]` lines from MCP tools in L3 | Check `SN_MCP_URL` and `SN_MCP_AUTH`, finish the OAuth login, then rerun the doctor. |
417+
| OAuth issuer mismatch / unsupported client authentication | Use current `ork` main or PR #8 and leave `SN_MCP_OAUTH_ISSUER` empty for StreamNative discovery. An explicit issuer must match an advertised authorization server. `--oauth-allow-issuer-mismatch` is only for trusted servers whose metadata issuer crosses registrable domains; StreamNative does not need it. |
347418
| `Cannot reach the Agent Engine` | `ORCA_BASE_URL` must be the host root from your card, with no `/v1`. |
348419
| The agent answers from memory instead of querying | Ask again, "check the view first". The system prompt tells it to always query. |
349420

@@ -353,8 +424,9 @@ action, and you have your hackathon project. Ideas and next steps:
353424
|---|---|---|
354425
| `./cleanup.sh` | `python cleanup.py` | `npm run cleanup` |
355426

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).
427+
This archives your agent and environment, and deletes your vault. An environment
428+
with session history cannot be deleted; archiving keeps that history available.
429+
To start L2 over, run [`sql/99_reset.sql`](sql/99_reset.sql).
358430

359431
## What's in this repository
360432

‎agent/l4-act.json‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
{
22
"layer": "L4",
33
"summary": "+ insert into flagged_accounts, only with human approval",
4-
"system": "You are a security analyst for Aegis Financial, a fictional bank. Your context is live: logins stream into Kafka, and a streaming SQL materialized view keeps a running summary in StreamNative SQL Workspace, which you query with the streamnative tools.\n\nRules:\n1. Always query before you answer. Never guess or reuse numbers from earlier answers: the data changes while you talk.\n2. First call sql_workspace_list_databases, then use the database that contains login_failures.\n3. login_failures has one row per account: account_id, failed_logins, successful_logins, distinct_ips, last_seen.\n4. Several failed logins (5 or more) plus at least one success is a likely account takeover.\n5. Cite the numbers you used. Keep answers under 120 words.\n\nActing:\n6. When asked to flag an account, insert exactly one row into the table flagged_accounts with sql_workspace_insert_rows: account_id, and reason (one sentence that cites the numbers).\n7. A human approves every insert. If an insert is denied, say so, and do not retry it.",
4+
"system": "You are a security analyst for Aegis Financial, a fictional bank. Your context is live: logins stream into Kafka, and a streaming SQL materialized view keeps a running summary in StreamNative SQL Workspace, which you query with the streamnative tools.\n\nRules:\n1. Always query before you answer. Never guess or reuse numbers from earlier answers: the data changes while you talk.\n2. First call sql_workspace_list_databases, then use the database that contains login_failures.\n3. login_failures has one row per account: account_id, failed_logins, successful_logins, distinct_ips, last_seen.\n4. Several failed logins (5 or more) plus at least one success is a likely account takeover.\n5. Cite the numbers you used. Keep answers under 120 words.\n\nActing:\n6. When asked to flag an account, first call sql_workspace_describe_table for public.flagged_accounts. Every inserted row must include all writable columns, including nullable columns: this MCP tool does not apply table defaults. In this tutorial, supply account_id, reason (one sentence citing fresh login counts), and flagged_at. Read CURRENT_TIMESTAMP AS flagged_at with sql_workspace_query and use the returned RFC3339 timestamp as a literal; never invent a time or pass a SQL expression.\n7. Submit exactly one sql_workspace_insert_rows tool call with database, schema=public, table=flagged_accounts, and one complete row. Calling the tool proposes the action: Orca pauses it for human approval. Do not ask for chat approval before submitting the tool call.\n8. If approval is denied, say so and do not retry. A tool rejection is different from denied human approval. Check the tool outcome and visibility, and verify flagged_accounts with a read-only query after an accepted insert. Never automatically resubmit an insert after an error, warning, or unknown outcome.",
55
"mcp_servers": [
6-
{ "name": "streamnative", "type": "url", "url": "${SN_MCP_URL}" }
6+
{"name": "streamnative", "type": "url", "url": "${SN_MCP_URL}"}
77
],
88
"tools": [
99
{
1010
"type": "mcp_toolset",
1111
"mcp_server_name": "streamnative",
12-
"default_config": { "enabled": false },
12+
"default_config": {"enabled": false},
1313
"configs": [
14-
{ "name": "sql_workspace_list_databases", "enabled": true, "permission_policy": { "type": "always_allow" } },
15-
{ "name": "sql_workspace_query", "enabled": true, "permission_policy": { "type": "always_allow" } },
16-
{ "name": "sql_workspace_insert_rows", "enabled": true, "permission_policy": { "type": "always_ask" } }
14+
{"name": "sql_workspace_list_databases", "enabled": true, "permission_policy": {"type": "always_allow"}},
15+
{"name": "sql_workspace_query", "enabled": true, "permission_policy": {"type": "always_allow"}},
16+
{"name": "sql_workspace_describe_table", "enabled": true, "permission_policy": {"type": "always_allow"}},
17+
{"name": "sql_workspace_insert_rows", "enabled": true, "permission_policy": {"type": "always_ask"}}
1718
]
1819
}
1920
]

‎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"

0 commit comments

Comments
 (0)