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
7 changes: 7 additions & 0 deletions .env.cloud.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ SCHEMA_REGISTRY_URL=
# https://mcp.streamnative.cloud/mcp/x/<org>/sqlworkspace.compute.streamnative.io/<SQL workspace>
SN_MCP_URL=

# The SQL catalog/database used in Labs 2-4, not the SQL workspace name.
# Set this to the catalog NAME whose sourceRef names your Kafka cluster.
# Agent construction scopes all SQL instructions to this database. Leaving it
# empty keeps legacy automatic discovery; set it when a workspace has multiple databases.
# This is prompt guidance, not an MCP authorization boundary.
SN_SQL_DATABASE=

# MCP authentication: oauth (default) or static_bearer for API-key MCP servers.
# All three paths call ork for the first OAuth login, then reuse the
# credential stored in the vault. Needs ork v0.6.0 or newer.
Expand Down
6 changes: 6 additions & 0 deletions cli/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ agent_definition() { # agent_definition <layer>
elif type == "object" then with_entries(.value |= fill)
else . end;
{name: $name, model: $model, system: .system, mcp_servers: (.mcp_servers | fill), tools: (.tools | fill)}
| if ($ENV.TUTORIAL_STACK // "cloud") == "cloud" and (.mcp_servers | length) > 0 and (($ENV.SN_SQL_DATABASE // "") | length) > 0 then
.system = ("Target SQL database: " + ($ENV.SN_SQL_DATABASE | tojson) +
". Use this exact database for every SQL tool call, including reads, table descriptions, and writes. " +
"Do not discover or select another database; this overrides database discovery instructions below. " +
"If it is unavailable or required tables are missing, report the error and stop; never fall back to another database.\n\n" + .system)
else . end
' "$file"
}

Expand Down
6 changes: 6 additions & 0 deletions labs/cloud/00-set-up.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ NAME DISPLAY INSTANCE
c-abc1234 ana-kafka ana
```

From the SQL catalog list, choose the row whose `KAFKA_CLUSTER` and
`SQL_WORKSPACE` match your resources. Set `SN_SQL_DATABASE` in `.env` to that
row's `NAME`: this is the database you use in Lab 2 and the agent targets in
Labs 3 and 4. Do not use the SQL workspace name. Explicit selection matters
when your SQL workspace imports multiple catalogs.

Now ask for each address, and write it into `.env`:

| `.env` line | Command | Write it as |
Expand Down
2 changes: 2 additions & 0 deletions labs/cloud/02-streaming-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Lab 3, and a table it can write to in Lab 4.
- Open your SQL workspace. In the StreamNative Cloud console, open **SQL
Workspace**, select your SQL workspace, and pick the database named after your
SQL catalog (Lab 0, step 2). Use a new query tab for each step.
This must match `SN_SQL_DATABASE` in `.env`, so you and the agent use the same
database. SQL Workspace does not read `.env`; select the database yourself.
- If the console cannot open the database yet, use `psql` from the repository
root instead. Look up your SQL workspace's address, then connect as `root`
with your API key as the password:
Expand Down
4 changes: 4 additions & 0 deletions labs/cloud/03-live-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ materialized view, and its answer changes when the stream does.

- You finished [Lab 2](02-streaming-sql.md): `login_failures` exists in your
SQL workspace's database.
- Set `SN_SQL_DATABASE` in `.env` to that database's SQL catalog name (Lab 0,
step 2). The agent is instructed to use it for every SQL call, with no
fallback to another database. This is prompt guidance, not MCP permission
isolation; an empty value retains legacy automatic discovery.
- One terminal is in your path's folder, a second one is at the repository root.
- `ork` v0.6.0 or newer is installed. All three paths use it for the first MCP
login.
Expand Down
3 changes: 3 additions & 0 deletions labs/cloud/04-act-with-approval.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ yes, and has not flagged another because you said no.
- You finished [Lab 3](03-live-context.md): the agent reads `login_failures`,
and the browser login for the MCP server is done.
- `flagged_accounts` exists in your SQL workspace's database (Lab 2, step 3).
- `SN_SQL_DATABASE` in `.env` names that same database. Check the proposed
insert's `database` before approving: the configured target is prompt
guidance, not an MCP authorization boundary.
- Your SQL workspace's MCP access is read-write; the organizers set this up.
Read-only access offers the agent no tool that writes: see
[Troubleshooting](troubleshooting.md).
Expand Down
8 changes: 8 additions & 0 deletions python/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ def agent_params(layer: dict[str, Any], config: Config) -> dict[str, Any]:
"mcp_servers": _fill(layer["mcp_servers"], config),
"tools": _fill(layer["tools"], config),
}
if config.stack == "cloud" and layer["mcp_servers"] and config.values.get("SN_SQL_DATABASE"):
database = json.dumps(config["SN_SQL_DATABASE"], ensure_ascii=False)
params["system"] = (
f"Target SQL database: {database}. Use this exact database for every SQL tool call, including reads, table descriptions, and writes. "
"Do not discover or select another database; this overrides database discovery instructions below. "
"If it is unavailable or required tables are missing, report the error and stop; never fall back to another database.\n\n"
+ params["system"]
)
# Same recipe in every language (and `jq -cS` in the CLI): compact JSON, sorted keys.
canonical = json.dumps(params, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
fingerprint = hashlib.sha256(canonical.encode()).hexdigest()[:16]
Expand Down
38 changes: 36 additions & 2 deletions python/tests/test_agent_defs.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
"""agent_params: turn agent/<layer>.json into the arguments for agents.create/update."""

import inspect
import json
import os
import subprocess

import pytest
from orca.resources.agents.agents import Agents

from common import Config, ConfigError, agent_params, load_layer
from common import REPO_ROOT, Config, ConfigError, agent_params, load_layer
from policy import effective_policy

MCP_URL = "https://mcp.example.com/mcp/x/o-test/sqlworkspace/ws-1"


def config(**overrides: str) -> Config:
values = {"ORCA_MODEL": "claude-sonnet-4-6", "SN_MCP_URL": MCP_URL, **overrides}
values = {"ORCA_MODEL": "claude-sonnet-4-6", "SN_MCP_URL": MCP_URL, "RW_MCP_URL": "http://localhost:8080/mcp", **overrides}
return Config(values=values, participant="jane")


Expand Down Expand Up @@ -61,6 +64,37 @@ def test_different_layers_carry_different_definition_fingerprints():
assert fingerprint("l3-live-context") == fingerprint("l3-live-context")


@pytest.mark.parametrize("layer", ["l3-live-context", "l4-act"])
def test_configured_database_scopes_cloud_agent_and_changes_fingerprint(layer):
params = agent_params(load_layer(layer), config(SN_SQL_DATABASE='catalog-\"rfu'))
assert params["system"].startswith('Target SQL database: "catalog-\\\"rfu".')
assert "never fall back to another database" in params["system"]
assert params["metadata"]["definition_sha"] != agent_params(load_layer(layer), config())["metadata"]["definition_sha"]
assert params["metadata"]["definition_sha"] != agent_params(load_layer(layer), config(SN_SQL_DATABASE="other"))["metadata"]["definition_sha"]


@pytest.mark.parametrize("layer, stack", [("l1-hello", "cloud"), ("l3-live-context", "local"), ("l4-act", "local")])
def test_database_setting_does_not_change_hello_or_local_agents(layer, stack):
definition = load_layer(layer, stack)
assert agent_params(definition, config(TUTORIAL_STACK=stack, SN_SQL_DATABASE="catalog-rfu")) == agent_params(definition, config(TUTORIAL_STACK=stack))


@pytest.mark.parametrize("layer", ["l3-live-context", "l4-act"])
def test_cli_database_definition_matches_python_without_loading_dotenv(layer):
# Extract only the pure definition builder; never source lib.sh/env.sh or .env.
source = (REPO_ROOT / "cli/lib.sh").read_text()
builder = source[source.index("agent_definition() {"):source.index("\n# Same recipe as the other languages")]
script = """layer_file() { printf '%s/agent/cloud/%s.json' "$REPO" "$1"; }
""" + builder + '\nagent_definition "$LAYER"'
env = {"PATH": os.environ["PATH"], "REPO": str(REPO_ROOT), "LAYER": layer,
"HELLO_PARTICIPANT": "jane", "ORCA_MODEL": "claude-sonnet-4-6",
"SN_MCP_URL": MCP_URL, "SN_SQL_DATABASE": 'catalog-\"rfu'}
result = subprocess.run(["bash", "-c", script], env=env, capture_output=True, text=True, check=True)
expected = agent_params(load_layer(layer), config(SN_SQL_DATABASE=env["SN_SQL_DATABASE"]))
expected.pop("metadata")
assert json.loads(result.stdout) == expected


@pytest.mark.parametrize(
"layer, tool, expected",
[
Expand Down
8 changes: 8 additions & 0 deletions typescript/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ export function agentParams(layer: Layer, config: Config): AgentParams {
mcp_servers: fill(layer.mcp_servers, config),
tools: fill(layer.tools, config),
};
if (config.stack === 'cloud' && layer.mcp_servers.length > 0 && config.has('SN_SQL_DATABASE') && config.get('SN_SQL_DATABASE')) {
const database = JSON.stringify(config.get('SN_SQL_DATABASE'));
params.system =
`Target SQL database: ${database}. Use this exact database for every SQL tool call, including reads, table descriptions, and writes. ` +
'Do not discover or select another database; this overrides database discovery instructions below. ' +
'If it is unavailable or required tables are missing, report the error and stop; never fall back to another database.\n\n' +
params.system;
}
// Same recipe in every language (and `jq -cS` in the CLI): compact JSON, sorted keys.
const fingerprint = createHash('sha256').update(canonicalJson(params), 'utf8').digest('hex').slice(0, 16);
return { ...params, metadata: { tutorial: 'dss2026-hello-world', layer: layer.layer, definition_sha: fingerprint } };
Expand Down
19 changes: 18 additions & 1 deletion typescript/test/agent-defs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { effectivePolicy } from './policy.js';
const MCP_URL = 'https://mcp.example.com/mcp/x/o-test/sqlworkspace/ws-1';

function config(overrides: Record<string, string> = {}): Config {
return new Config({ ORCA_MODEL: 'claude-sonnet-4-6', SN_MCP_URL: MCP_URL, ...overrides }, 'jane');
return new Config({ ORCA_MODEL: 'claude-sonnet-4-6', SN_MCP_URL: MCP_URL, RW_MCP_URL: 'http://localhost:8080/mcp', ...overrides }, 'jane');
}

describe('agentParams', () => {
Expand Down Expand Up @@ -64,6 +64,23 @@ describe('agentParams', () => {
expect(agentParams(loadLayer(layer), config()).metadata.definition_sha).toBe(expected);
});

it.each(['l3-live-context', 'l4-act'])('%s uses the configured database and fingerprints it', (layer) => {
const params = agentParams(loadLayer(layer), config({ SN_SQL_DATABASE: 'catalog-"rfu' }));
expect(params.system).toContain(`Target SQL database: ${JSON.stringify('catalog-"rfu')}.`);
expect(params.system).toContain('never fall back to another database');
expect(params.metadata.definition_sha).not.toBe(agentParams(loadLayer(layer), config()).metadata.definition_sha);
expect(params.metadata.definition_sha).not.toBe(agentParams(loadLayer(layer), config({ SN_SQL_DATABASE: 'other' })).metadata.definition_sha);
});

it('does not change L1 or Local agents', () => {
expect(agentParams(loadLayer('l1-hello'), config({ SN_SQL_DATABASE: 'catalog-rfu' })))
.toEqual(agentParams(loadLayer('l1-hello'), config()));
for (const layer of ['l3-live-context', 'l4-act']) {
expect(agentParams(loadLayer(layer, 'local'), config({ TUTORIAL_STACK: 'local', SN_SQL_DATABASE: 'catalog-rfu' })))
.toEqual(agentParams(loadLayer(layer, 'local'), config({ TUTORIAL_STACK: 'local' })));
}
});

it.each([
['l3-live-context', 'sql_workspace_list_databases', 'always_allow'],
['l3-live-context', 'sql_workspace_query', 'always_allow'],
Expand Down
Loading