Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 23 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@

# ---------------------------------------------------------------- team card --

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

# Service-account principal, used as the Kafka SASL username.
# Looks like: <service-account>@<org>.auth.streamnative.cloud
SN_SERVICE_ACCOUNT=

# Optional separate Registry workspace key for ork local (sent as x-api-key).
# Leave empty for a hosted team card; SN_API_KEY then authenticates Agent Engine.
# This key does not authenticate Kafka, Schema Registry, or StreamNative MCP.
ORCA_API_KEY=

# Agent Engine registry endpoint (the External one). Host root only, no /v1.
# Looks like: https://<workspace-host>
ORCA_BASE_URL=
Expand All @@ -22,10 +27,24 @@ SCHEMA_REGISTRY_URL=
# StreamNative MCP server for your SQL Workspace (the agent's data tools).
SN_MCP_URL=

# MCP authentication: oauth (default) or static_bearer for API-key MCP servers.
# All three tutorial paths call ork for the first OAuth login, then reuse the
# credential stored in the vault. Install ork with the OAuth discovery support
# from orca-cli PR #8 (or current main).
SN_MCP_AUTH=oauth

# Optional authorization server selection. Leave empty for automatic discovery.
# If multiple servers are advertised, copy one exact authorization_servers value
# from the MCP protected-resource metadata; do not substitute the metadata issuer.
SN_MCP_OAUTH_ISSUER=
SN_MCP_OAUTH_SCOPE="openid profile email offline_access"

# ------------------------------------------------------------- your choices --

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

# The model your agent runs on (served by the event's AI gateway).
ORCA_MODEL=claude-sonnet-4-6
Expand Down
108 changes: 90 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ them from live data, and flag the account once you say so.

```mermaid
flowchart LR
K["Kafka topic<br/>avro.security.login_events"] --> S["SQL Workspace<br/>materialized view<br/>login_failures"]
K["Kafka topic<br/>security.login_events"] --> S["SQL Workspace<br/>materialized view<br/>login_failures"]
J["inject<br/>(you, in L3)"] -- "new login burst" --> K
S -- "StreamNative MCP<br/>sql_workspace_query" --> A["Orca agent<br/>hello-agent-&lt;you&gt;"]
A -- "sql_workspace_insert_rows<br/>(only if you approve)" --> F["SQL table<br/>flagged_accounts"]
```

| Step | Time | Where | You | The idea |
|---|---|---|---|---|
| [0. Connect](#step-0-connect-3-min) | 3 min | terminal | Fill in `.env`, run the doctor | One key, checked end to end |
| [0. Connect](#step-0-connect-3-min) | 3 min | terminal | Fill in `.env`, run the doctor | Check service access; authorize MCP with OAuth |
| [L1. Hello, agent](#l1-hello-agent-5-min) | 5 min | CLI / Python / TS | Create an agent and chat | Agent, environment, session, events |
| [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 |
| [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 |
Expand Down Expand Up @@ -54,8 +54,52 @@ Go to your path's folder and run the doctor:
| TypeScript | `cd typescript && npm run doctor` |
| CLI | `cd cli`, and run the doctor from your helper language: `(cd ../python && .venv/bin/python doctor.py)` or `(cd ../typescript && npm run doctor)` |

Every line should say `PASS`. A failed check prints its fix. Still stuck after
two tries? Raise your hand.
The service checks should say `PASS`. Before the first OAuth login, the MCP
check asks you to run L3; that script opens your browser and stores the credential
in a vault. After completing L2 and running L3, rerun the doctor to validate the
stored OAuth credential. This check verifies MCP initialization; L3/L4 exercise
the actual SQL tools. A failed check prints its fix. Still stuck after two tries?
Raise your hand.

For StreamNative SQL Workspace MCP, keep `SN_MCP_AUTH=oauth`, leave
`SN_MCP_OAUTH_ISSUER` empty for automatic discovery, and use the scope from
`.env.example`. Use an `ork` build containing [PR #8](https://github.com/orca-ae/orca-cli/pull/8)
or current main. Its discovery accepts HTTPS issuer aliases within the same
registrable domain and port. Only set `SN_MCP_OAUTH_ISSUER` when selecting one
of multiple advertised `authorization_servers`; copy that advertised value
exactly rather than the final issuer in authorization-server metadata.
`SN_API_KEY` authenticates the hosted Agent Engine,
Kafka and Schema Registry; it is not the OAuth MCP access token. All three paths
use `ork` for the first MCP login, then reuse the live credential for the same URL
and auth type from `.orca-state/<participant>.json`. Tokens stay in the server-side
vault, where they can be refreshed; they are never written to `.env` or local state.
Set `SN_MCP_AUTH=static_bearer` only when your MCP server accepts `SN_API_KEY`.
Changing the auth mode archives the previous live credential for that same URL
before creating its replacement (the Registry permits one active credential per
URL in a vault). If authorization fails, rerun L3/L4 to finish setup; other URLs'
credentials are preserved. A local Agent Engine with OAuth MCP needs only
`ORCA_API_KEY` for Registry authentication; `SN_API_KEY` is still needed for Kafka
and Schema Registry.

### Use a local Agent Engine

Start the CLI's stack with a provider key in your shell:

```bash
export ANTHROPIC_API_KEY='<your-provider-key>'
ork local start --with-gateway
```

Set `ORCA_BASE_URL=http://127.0.0.1:8080` in the tutorial's `.env`, and copy the
workspace key from the file printed by `ork local start` into `ORCA_API_KEY`.
The tutorial sends this key as `x-api-key`. A hosted team card continues to use
`SN_API_KEY` as a Bearer token when `ORCA_API_KEY` is empty.

For L1, run `python doctor.py --agent-only` or `npm run doctor -- --agent-only`.
This checks the Agent Engine without requiring Kafka, Schema Registry, or MCP.
The local stack provides the Agent Engine and AI Gateway; L2–L4 still need the
streaming data services from your team card. For L3/L4, keep `SN_API_KEY` set to
the MCP service key, separately from the local Registry's `ORCA_API_KEY`.

## L1: Hello, agent (5 min)

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

In the StreamNative Cloud console, open **SQL Workspace**, select the hackathon
workspace, and pick your team's database. Use a new query tab for each step.
The default Kafka topic is `security.login_events`; SQL Workspace exposes its
Avro source as `"avro.security.login_events"`.

**Align the SQL with your `.env` before running it.** The injectors and doctor use
`LOGIN_TOPIC`, but the SQL files and examples below contain a fixed source name:
SQL Workspace does not read your local `.env`. Check `LOGIN_TOPIC`, then replace
`"avro.security.login_events"` with `"avro.<your LOGIN_TOPIC>"` in both
[`sql/01_explore.sql`](sql/01_explore.sql) and
[`sql/02_login_failures.sql`](sql/02_login_failures.sql), and in any query copied
from this page. For example, `LOGIN_TOPIC=security.team07_logins` requires
`FROM "avro.security.team07_logins"`. Keep the double quotes around the entire
source name and confirm that SQL Workspace imported that topic as an Avro source.
Keep the `login_failures` view name: L3/L4 query that view.

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

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

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

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

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

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

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

```
[approve?] The agent wants to run sql_workspace_insert_rows with:
{
"database": "<your database>",
"schema": "public",
"table": "flagged_accounts",
"rows": [{"account_id": "acct_9…", "reason": "6 failed logins then a success from one new IP"}]
"rows": [{
"account_id": "acct_9…",
"reason": "6 failed logins then a success from one new IP",
"flagged_at": "2026-09-30T12:00:00Z"
}]
}
Allow it? [y/N]
```
Expand All @@ -309,11 +378,12 @@ Type `y`, then check in SQL Workspace:
SELECT * FROM flagged_accounts;
```

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

**What changed** ([`agent/l4-act.json`](agent/l4-act.json)): one more tool,
`sql_workspace_insert_rows`, with `permission_policy: always_ask`. When the agent
**What changed** ([`agent/l4-act.json`](agent/l4-act.json)): the read-only
`sql_workspace_describe_table` checks the required columns, and
`sql_workspace_insert_rows` uses `permission_policy: always_ask`. When the agent
calls it, the session emits `agent.mcp_tool_use` and goes idle with
`stop_reason: requires_action`. Your script answers with a
`user.tool_confirmation`: `allow`, or `deny` with a reason. On the CLI that is:
Expand Down Expand Up @@ -341,9 +411,10 @@ action, and you have your hackathon project. Ideas and next steps:
| Doctor: `Agent Engine HTTP 401/403` | The key was rejected. A key created before its permissions must be re-created: ask a facilitator. |
| Doctor: `Kafka ... authentication` | `SN_SERVICE_ACCOUNT` must be the full principal, `<name>@<org>.auth.streamnative.cloud`; `SN_API_KEY` is the raw key. |
| The login topic isn't listed in SQL Workspace | Only topics with a registered Avro schema appear. Ask a facilitator. |
| `relation "avro.security.login_events" does not exist` | Select your team's database, and keep the double quotes around the name. |
| `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`. |
| The agent can't find `login_failures` | Create the view in your team's database (L2, step 2); the agent looks it up there. |
| `[error]` lines from MCP tools in L3 | Check `SN_MCP_URL` against your team card, then rerun the doctor. |
| `[error]` lines from MCP tools in L3 | Check `SN_MCP_URL` and `SN_MCP_AUTH`, finish the OAuth login, then rerun the doctor. |
| 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. |
| `Cannot reach the Agent Engine` | `ORCA_BASE_URL` must be the host root from your card, with no `/v1`. |
| The agent answers from memory instead of querying | Ask again, "check the view first". The system prompt tells it to always query. |

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

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

## What's in this repository

Expand Down
13 changes: 7 additions & 6 deletions agent/l4-act.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
{
"layer": "L4",
"summary": "+ insert into flagged_accounts, only with human approval",
"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.",
"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.",
"mcp_servers": [
{ "name": "streamnative", "type": "url", "url": "${SN_MCP_URL}" }
{"name": "streamnative", "type": "url", "url": "${SN_MCP_URL}"}
],
"tools": [
{
"type": "mcp_toolset",
"mcp_server_name": "streamnative",
"default_config": { "enabled": false },
"default_config": {"enabled": false},
"configs": [
{ "name": "sql_workspace_list_databases", "enabled": true, "permission_policy": { "type": "always_allow" } },
{ "name": "sql_workspace_query", "enabled": true, "permission_policy": { "type": "always_allow" } },
{ "name": "sql_workspace_insert_rows", "enabled": true, "permission_policy": { "type": "always_ask" } }
{"name": "sql_workspace_list_databases", "enabled": true, "permission_policy": {"type": "always_allow"}},
{"name": "sql_workspace_query", "enabled": true, "permission_policy": {"type": "always_allow"}},
{"name": "sql_workspace_describe_table", "enabled": true, "permission_policy": {"type": "always_allow"}},
{"name": "sql_workspace_insert_rows", "enabled": true, "permission_policy": {"type": "always_ask"}}
]
}
]
Expand Down
4 changes: 2 additions & 2 deletions cli/cleanup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
set -euo pipefail
# shellcheck source=lib.sh
. "$(dirname "$0")/lib.sh"
hello_setup ORCA_BASE_URL SN_API_KEY
hello_setup ORCA_BASE_URL

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