Skip to content

What a program cannot do

The model writes the code. You did not read it before it ran, and on the next run it will be different code.

Build an Agent answered this question for one record: bind the tool, then prove the binding with a forged value. Code mode needs the same treatment, and the proofs are different because the thing you do not trust is different.

This whole lesson calls no model. All five checks cost nothing, and you can run them as often as you like.

CC_FILE_CLAIM is registered as direct:

l_tool_id := uc_ai_tools_api.merge_tool_from_schema(
p_tool_code => 'CC_FILE_CLAIM'
, p_description => 'File one claim against one shipment that broke the cold chain. ...'
, p_function_call => 'return cc_analyst_pkg.file_claim(:ARGUMENTS);'
, p_json_schema => json_object_t('{ ... }')
, p_tags => apex_t_varchar2('coldchain')
, p_code_mode_access => 'direct'
);

Code mode splits the work three ways:

  • the program computes over large data
  • the model decides what to do with the small result
  • the database decides whether the decision is allowed

Lesson 6 runs that split end to end. This lesson proves the first half of it: that a program cannot reach the write tool at all.

The program below names the tool exactly, with the right arguments. Shipment 7 is SHP-2047, and 150 minutes at 720 EUR is the claim the database agrees with:

const result = await callTool("CC_FILE_CLAIM",
{ shipment_id: 7, minutes_over: 150, amount_eur: 720 });
{"error":"code execution failed: ORA-20504: Tool not found: CC_FILE_CLAIM",
"hint":"Fix the program and call the code tool again. ..."}

“Tool not found” is the answer from inside the program: for a program, this tool does not exist. The allow-list of the run answers before any of your code runs.

Check 2: the program cannot reach the database

Section titled “Check 2: the program cannot reach the database”

The program runs in an Oracle Multilingual Engine (MLE) context of type PURE. A PURE context has no access to SQL at all, and it removes every JavaScript API that reaches the database. The script writes an uncommitted row first, then runs this probe:

const probe = {};
try { probe.require = typeof require("mle-js-oracledb"); } catch (e) { probe.require = "blocked"; }
try { await import("mle-js-oracledb"); probe.import = "reached"; } catch (e) { probe.import = "blocked"; }
probe.globals = [typeof oracledb, typeof session, typeof soda, typeof plsffi].join(",");
const result = probe;
{"require":"blocked","import":"blocked","globals":"undefined,undefined,undefined,undefined"}
the uncommitted row of the caller is still pending: yes

COMMIT and ROLLBACK need no privilege. A sandbox built only on privileges still lets generated code throw away the pending work of the caller. Removal of all SQL is what stops that, and it is why PURE is the context to use.

Check 3: the hook fires on every inner call

Section titled “Check 3: the hook fires on every inner call”

A hook is a package you write. UC AI finds before_tool_call in it by name, and you attach the package to the session with one call:

uc_ai_agents_api.set_execution_hook('CC_HOOK_PKG');

cc_hook_pkg counts calls to before_tool_call:

procedure before_tool_call( ... )
as
begin
g_calls := g_calls + 1;
if p_tool_code = g_block_tool then
raise_application_error(-20999, 'Policy: ' || p_tool_code || ' is not allowed now.');
end if;
end before_tool_call;

Then one program reads every shipment:

3. A program reads every shipment in one call:
{"shipments":24,"readings":1176}
before_tool_call fired 25 times.

Twenty-five. One for CC_LIST_SHIPMENTS and one for each of the 24 CC_GET_READINGS calls. UC AI does not call the hook one time for the program. It calls the hook for every callTool inside it.

So authorization and auditing through this hook keep working in code mode. It also means that a hook which writes an audit row must be cheap. One tool call from the model can produce a hundred rows.

Now the hook refuses one tool, and the program catches the error and continues:

let caught = "no";
try {
await callTool("CC_GET_READINGS", { shipment_id: 7 });
} catch (e) {
caught = "yes";
}
const result = { the_program_carried_on: caught };

The program finished. The request did not:

ORA-20503: Invalid code mode tool call: vetoed by the before_tool_call hook:
ORA-20999: Policy: CC_GET_READINGS is not allowed now.

This is the one code-mode failure that is not handed to the model as data. UC AI records the veto and raises it after the sandbox returns. Catching the rejected promise in JavaScript does not prevent UC AI from raising the veto.

One program can make 100 tool calls. The number is a constant, and you cannot change it:

let ok = 0, stoppedAt = null;
for (let i = 0; i < 150; i++) {
try { await callTool("CC_GET_LIMITS", {}); ok++; }
catch (e) { stoppedAt = i; break; }
}
const result = { calls_that_worked: ok, stopped_at_call: stoppedAt };
{"calls_that_worked":100,"stopped_at_call":100}

A program can catch that error and keep calling. So UC AI stops a program for good after 1000 calls, whatever it catches.

Every check above ran in the sandbox. No model was called, and nothing was written.

select count(*) as claims_filed from cc_claims;
CLAIMS_FILED
0
select code, code_mode_access
from uc_ai_tools
where code like 'CC\_%' escape '\'
order by code;
CODE CODE_MODE_ACCESS
CC_FILE_CLAIM direct
CC_GET_LIMITS both
CC_GET_READINGS code
CC_LIST_SHIPMENTS code

All five checks must hold:

  • Check 1 returns Tool not found: CC_FILE_CLAIM.
  • Check 2 returns blocked, blocked, and four times undefined, and the uncommitted row is still pending.
  • Check 3 prints 25.
  • Check 4 raises ORA-20503 and names the hook in the message.
  • Check 5 stops at 100.

Lesson 6 gives CC_FILE_CLAIM to an agent, and the claims arrive there.

  • code_mode_access of direct keeps a tool out of every program, whatever the program writes.
  • A PURE context removes all SQL, so generated code cannot read data and cannot commit or roll back your transaction.
  • Your tools keep every privilege and run in the transaction of the caller. The sandbox constrains the JavaScript, never the tools.
  • before_tool_call fires for each callTool inside a program. Keep it cheap.
  • A hook veto stops the request and cannot be caught by the program.
  • The budget is 100 tool calls for one program, and it is not configurable.

Full reference: The security model.