Skip to content

Commit 34dc4f2

Browse files
committed
fix: scope cloud agent SQL calls to configured database
1 parent 96c4495 commit 34dc4f2

10 files changed

Lines changed: 98 additions & 3 deletions

File tree

‎.env.cloud.example‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ SCHEMA_REGISTRY_URL=
3232
# https://mcp.streamnative.cloud/mcp/x/<org>/sqlworkspace.compute.streamnative.io/<SQL workspace>
3333
SN_MCP_URL=
3434

35+
# The SQL catalog/database used in Labs 2-4, not the SQL workspace name.
36+
# Set this to the catalog NAME whose sourceRef names your Kafka cluster.
37+
# Agent construction scopes all SQL instructions to this database. Leaving it
38+
# empty keeps legacy automatic discovery; set it when a workspace has multiple databases.
39+
# This is prompt guidance, not an MCP authorization boundary.
40+
SN_SQL_DATABASE=
41+
3542
# MCP authentication: oauth (default) or static_bearer for API-key MCP servers.
3643
# All three paths call ork for the first OAuth login, then reuse the
3744
# credential stored in the vault. Needs ork v0.6.0 or newer.

‎cli/lib.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ agent_definition() { # agent_definition <layer>
9292
elif type == "object" then with_entries(.value |= fill)
9393
else . end;
9494
{name: $name, model: $model, system: .system, mcp_servers: (.mcp_servers | fill), tools: (.tools | fill)}
95+
| if ($ENV.TUTORIAL_STACK // "cloud") == "cloud" and (.mcp_servers | length) > 0 and (($ENV.SN_SQL_DATABASE // "") | length) > 0 then
96+
.system = ("Target SQL database: " + ($ENV.SN_SQL_DATABASE | tojson) +
97+
". Use this exact database for every SQL tool call, including reads, table descriptions, and writes. " +
98+
"Do not discover or select another database; this overrides database discovery instructions below. " +
99+
"If it is unavailable or required tables are missing, report the error and stop; never fall back to another database.\n\n" + .system)
100+
else . end
95101
' "$file"
96102
}
97103

‎labs/cloud/00-set-up.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ NAME DISPLAY INSTANCE
108108
c-abc1234 ana-kafka ana
109109
```
110110

111+
From the SQL catalog list, choose the row whose `KAFKA_CLUSTER` and
112+
`SQL_WORKSPACE` match your resources. Set `SN_SQL_DATABASE` in `.env` to that
113+
row's `NAME`: this is the database you use in Lab 2 and the agent targets in
114+
Labs 3 and 4. Do not use the SQL workspace name. Explicit selection matters
115+
when your SQL workspace imports multiple catalogs.
116+
111117
Now ask for each address, and write it into `.env`:
112118

113119
| `.env` line | Command | Write it as |

‎labs/cloud/02-streaming-sql.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Lab 3, and a table it can write to in Lab 4.
1212
- Open your SQL workspace. In the StreamNative Cloud console, open **SQL
1313
Workspace**, select your SQL workspace, and pick the database named after your
1414
SQL catalog (Lab 0, step 2). Use a new query tab for each step.
15+
This must match `SN_SQL_DATABASE` in `.env`, so you and the agent use the same
16+
database. SQL Workspace does not read `.env`; select the database yourself.
1517
- If the console cannot open the database yet, use `psql` from the repository
1618
root instead. Look up your SQL workspace's address, then connect as `root`
1719
with your API key as the password:

‎labs/cloud/03-live-context.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ materialized view, and its answer changes when the stream does.
1010

1111
- You finished [Lab 2](02-streaming-sql.md): `login_failures` exists in your
1212
SQL workspace's database.
13+
- Set `SN_SQL_DATABASE` in `.env` to that database's SQL catalog name (Lab 0,
14+
step 2). The agent is instructed to use it for every SQL call, with no
15+
fallback to another database. This is prompt guidance, not MCP permission
16+
isolation; an empty value retains legacy automatic discovery.
1317
- One terminal is in your path's folder, a second one is at the repository root.
1418
- `ork` v0.6.0 or newer is installed. All three paths use it for the first MCP
1519
login.

‎labs/cloud/04-act-with-approval.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ yes, and has not flagged another because you said no.
1111
- You finished [Lab 3](03-live-context.md): the agent reads `login_failures`,
1212
and the browser login for the MCP server is done.
1313
- `flagged_accounts` exists in your SQL workspace's database (Lab 2, step 3).
14+
- `SN_SQL_DATABASE` in `.env` names that same database. Check the proposed
15+
insert's `database` before approving: the configured target is prompt
16+
guidance, not an MCP authorization boundary.
1417
- Your SQL workspace's MCP access is read-write; the organizers set this up.
1518
Read-only access offers the agent no tool that writes: see
1619
[Troubleshooting](troubleshooting.md).

‎python/common.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ def agent_params(layer: dict[str, Any], config: Config) -> dict[str, Any]:
133133
"mcp_servers": _fill(layer["mcp_servers"], config),
134134
"tools": _fill(layer["tools"], config),
135135
}
136+
if config.stack == "cloud" and layer["mcp_servers"] and config.values.get("SN_SQL_DATABASE"):
137+
database = json.dumps(config["SN_SQL_DATABASE"], ensure_ascii=False)
138+
params["system"] = (
139+
f"Target SQL database: {database}. Use this exact database for every SQL tool call, including reads, table descriptions, and writes. "
140+
"Do not discover or select another database; this overrides database discovery instructions below. "
141+
"If it is unavailable or required tables are missing, report the error and stop; never fall back to another database.\n\n"
142+
+ params["system"]
143+
)
136144
# Same recipe in every language (and `jq -cS` in the CLI): compact JSON, sorted keys.
137145
canonical = json.dumps(params, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
138146
fingerprint = hashlib.sha256(canonical.encode()).hexdigest()[:16]

‎python/tests/test_agent_defs.py‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
"""agent_params: turn agent/<layer>.json into the arguments for agents.create/update."""
22

33
import inspect
4+
import json
5+
import os
6+
import subprocess
47

58
import pytest
69
from orca.resources.agents.agents import Agents
710

8-
from common import Config, ConfigError, agent_params, load_layer
11+
from common import REPO_ROOT, Config, ConfigError, agent_params, load_layer
912
from policy import effective_policy
1013

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

1316

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

1821

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

6366

67+
@pytest.mark.parametrize("layer", ["l3-live-context", "l4-act"])
68+
def test_configured_database_scopes_cloud_agent_and_changes_fingerprint(layer):
69+
params = agent_params(load_layer(layer), config(SN_SQL_DATABASE='catalog-\"rfu'))
70+
assert params["system"].startswith('Target SQL database: "catalog-\\\"rfu".')
71+
assert "never fall back to another database" in params["system"]
72+
assert params["metadata"]["definition_sha"] != agent_params(load_layer(layer), config())["metadata"]["definition_sha"]
73+
assert params["metadata"]["definition_sha"] != agent_params(load_layer(layer), config(SN_SQL_DATABASE="other"))["metadata"]["definition_sha"]
74+
75+
76+
@pytest.mark.parametrize("layer, stack", [("l1-hello", "cloud"), ("l3-live-context", "local"), ("l4-act", "local")])
77+
def test_database_setting_does_not_change_hello_or_local_agents(layer, stack):
78+
definition = load_layer(layer, stack)
79+
assert agent_params(definition, config(TUTORIAL_STACK=stack, SN_SQL_DATABASE="catalog-rfu")) == agent_params(definition, config(TUTORIAL_STACK=stack))
80+
81+
82+
@pytest.mark.parametrize("layer", ["l3-live-context", "l4-act"])
83+
def test_cli_database_definition_matches_python_without_loading_dotenv(layer):
84+
# Extract only the pure definition builder; never source lib.sh/env.sh or .env.
85+
source = (REPO_ROOT / "cli/lib.sh").read_text()
86+
builder = source[source.index("agent_definition() {"):source.index("\n# Same recipe as the other languages")]
87+
script = """layer_file() { printf '%s/agent/cloud/%s.json' "$REPO" "$1"; }
88+
""" + builder + '\nagent_definition "$LAYER"'
89+
env = {"PATH": os.environ["PATH"], "REPO": str(REPO_ROOT), "LAYER": layer,
90+
"HELLO_PARTICIPANT": "jane", "ORCA_MODEL": "claude-sonnet-4-6",
91+
"SN_MCP_URL": MCP_URL, "SN_SQL_DATABASE": 'catalog-\"rfu'}
92+
result = subprocess.run(["bash", "-c", script], env=env, capture_output=True, text=True, check=True)
93+
expected = agent_params(load_layer(layer), config(SN_SQL_DATABASE=env["SN_SQL_DATABASE"]))
94+
expected.pop("metadata")
95+
assert json.loads(result.stdout) == expected
96+
97+
6498
@pytest.mark.parametrize(
6599
"layer, tool, expected",
66100
[

‎typescript/src/common.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@ export function agentParams(layer: Layer, config: Config): AgentParams {
180180
mcp_servers: fill(layer.mcp_servers, config),
181181
tools: fill(layer.tools, config),
182182
};
183+
if (config.stack === 'cloud' && layer.mcp_servers.length > 0 && config.has('SN_SQL_DATABASE') && config.get('SN_SQL_DATABASE')) {
184+
const database = JSON.stringify(config.get('SN_SQL_DATABASE'));
185+
params.system =
186+
`Target SQL database: ${database}. Use this exact database for every SQL tool call, including reads, table descriptions, and writes. ` +
187+
'Do not discover or select another database; this overrides database discovery instructions below. ' +
188+
'If it is unavailable or required tables are missing, report the error and stop; never fall back to another database.\n\n' +
189+
params.system;
190+
}
183191
// Same recipe in every language (and `jq -cS` in the CLI): compact JSON, sorted keys.
184192
const fingerprint = createHash('sha256').update(canonicalJson(params), 'utf8').digest('hex').slice(0, 16);
185193
return { ...params, metadata: { tutorial: 'dss2026-hello-world', layer: layer.layer, definition_sha: fingerprint } };

‎typescript/test/agent-defs.test.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { effectivePolicy } from './policy.js';
99
const MCP_URL = 'https://mcp.example.com/mcp/x/o-test/sqlworkspace/ws-1';
1010

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

1515
describe('agentParams', () => {
@@ -64,6 +64,23 @@ describe('agentParams', () => {
6464
expect(agentParams(loadLayer(layer), config()).metadata.definition_sha).toBe(expected);
6565
});
6666

67+
it.each(['l3-live-context', 'l4-act'])('%s uses the configured database and fingerprints it', (layer) => {
68+
const params = agentParams(loadLayer(layer), config({ SN_SQL_DATABASE: 'catalog-"rfu' }));
69+
expect(params.system).toContain(`Target SQL database: ${JSON.stringify('catalog-"rfu')}.`);
70+
expect(params.system).toContain('never fall back to another database');
71+
expect(params.metadata.definition_sha).not.toBe(agentParams(loadLayer(layer), config()).metadata.definition_sha);
72+
expect(params.metadata.definition_sha).not.toBe(agentParams(loadLayer(layer), config({ SN_SQL_DATABASE: 'other' })).metadata.definition_sha);
73+
});
74+
75+
it('does not change L1 or Local agents', () => {
76+
expect(agentParams(loadLayer('l1-hello'), config({ SN_SQL_DATABASE: 'catalog-rfu' })))
77+
.toEqual(agentParams(loadLayer('l1-hello'), config()));
78+
for (const layer of ['l3-live-context', 'l4-act']) {
79+
expect(agentParams(loadLayer(layer, 'local'), config({ TUTORIAL_STACK: 'local', SN_SQL_DATABASE: 'catalog-rfu' })))
80+
.toEqual(agentParams(loadLayer(layer, 'local'), config({ TUTORIAL_STACK: 'local' })));
81+
}
82+
});
83+
6784
it.each([
6885
['l3-live-context', 'sql_workspace_list_databases', 'always_allow'],
6986
['l3-live-context', 'sql_workspace_query', 'always_allow'],

0 commit comments

Comments
 (0)