Targets
IntentSharp names no database. You connect a database, a collection or a service as a target, an ITarget in the
IntentSharp.Targets namespace: it describes what it holds, reads a planned command without running it, and runs a
command that passed the check. TargetRuntime asks the model for one command in the language the target describes,
has the target inspect it, checks the inspection against your rule sets at the Data checkpoint, and only then lets
the target run it. Target limits covers the data.* settings, writes, and the time and result
limits a command runs under.
The examples on this page run against a target over a SQLite database with two tables, Customers and
PaymentCards, under the rule files below.
The rule files decide what may run
A manifest, intentsharp.guardrails.json, lists the rule files of a document store:
{
"intentsharp": "guardrails/1",
"documents": ["rules/customers.rules.yaml", "rules/customers-write.rules.yaml", "rules/customers-review.rules.yaml"]
}
This rule file is the set customers-read, checked at the Data checkpoint before any command runs:
intentsharp: rules/1
set: customers-read
checkpoint: data
target: sqlite
rules:
- id: customers-only
subject: data.entities
allow: [Customers]
message: Use only the Customers table.
- id: read-only
subject: data.operations
allow: [read]
message: Only read; this set never changes data.
settings:
data.maxResults: 200
| In the file | What it does |
|---|---|
target: sqlite |
Binds the set to the target of that name; a runtime refuses every check that names a set written for another target. |
data.entities, data.operations |
Each must be covered by an enforced rule. With no allow list, nothing is allowed. allow: ["@described"] allows every entity the target describes. |
message |
What the model is told when its command breaks the rule. |
The client reads the rule sets from the store. Reading rule files takes the IntentSharp.Guardrails package;
Guardrails covers the format and the other checkpoints:
// "policy" holds the provider file, the rule file manifest intentsharp.guardrails.json and the rule files.
using var store = new FolderDocumentStore("policy");
using var client = new IntentClientBuilder()
.UseProviderOptions(store)
.WithGuardrailFiles(store, "customers-read")
.Build();
A saved edit to a rule file applies to the next check; a version that does not load is reported and the version before it keeps running.
Run a command
// The rule file customers.rules.yaml: read only, and only the Customers table.
var runtime = new TargetRuntime(client, target, new TargetRuntimeOptions { GuardrailSets = ["customers-read"] });
CommandOutcome outcome = await runtime.RunAsync("Customers in California with a total above 1000, largest first");
if (!outcome.Executed)
return []; // see "When a command is refused"
foreach (var record in outcome.Result!.Records)
Console.WriteLine(record.ToJsonString());
if (outcome.Result.Truncated)
Console.WriteLine("Only the first records were read (data.maxResults).");
IReadOnlyList<CustomerRow> customers = outcome.As<CustomerRow>();
RunAsyncdescribes the target, plans, checks and runs, in that order.Executedsays whether the command ran.- Each record is a JSON object;
As<T>()maps the records to your type.Truncatedis true when there were more records thandata.maxResults(Target limits).
The model receives your request, the input you pass, the target's instructions and its description: every entity the target describes, with its fields, here both tables. The rules, not the prompt, keep a command to the entities they allow; a target of your own can leave an entity out of its description. The records a command returns stay with you.
Plan, check and execute
The stages of RunAsync can also run one at a time:
var runtime = new TargetRuntime(client, target, new TargetRuntimeOptions { GuardrailSets = ["customers-read"] });
// Planned and checked, not run.
AIResult<CommandPlan> plan = await runtime.PlanAsync("Customers in California with a total above 1000");
if (!plan.Success || plan.Value is not { } planned)
return null;
CommandCheck check = await runtime.CheckAsync(planned);
Console.WriteLine($"{planned.Command} (allowed: {check.Allowed})");
foreach (var (name, value) in planned.Parameters)
Console.WriteLine($"@{name} = {value?.ToJsonString()}");
// After a person approves it, run the same plan. The target inspects it and the rules check it again first.
CommandOutcome? executed = approve(planned) ? await runtime.ExecuteAsync(planned) : null;
| Method | What it does |
|---|---|
PlanAsync |
Returns the plan, a CommandPlan (Command, Parameters, Explanation, Entities, Language), without running it. |
CheckAsync |
Has the target inspect the plan and checks the inspection. The CommandCheck says Allowed, and lists the Violations and the Refusals, those of the target and those of the check itself. |
ExecuteAsync |
Inspects and checks the plan again, then runs it, with no model call. A plan you build or change yourself is checked the same way. |
Each stage is checked with its own value of data.stage: sense after the target is described, plan on each planning
attempt and in CheckAsync, and execute just before the target runs the command. A rule on data.stage can allow
one stage and refuse another. The set customers-review lets the model plan and lets you check, and runs nothing:
intentsharp: rules/1
set: customers-review
checkpoint: data
target: sqlite
rules:
- id: customers-only
subject: data.entities
allow: [Customers]
message: Use only the Customers table.
- id: read-only
subject: data.operations
allow: [read]
message: Only read; this set never changes data.
- id: parameters-only
subject: sql.inlineValues
count: { max: 0 }
message: Pass every value as a named parameter, such as @state, never as a literal.
- id: plan-only
subject: data.stage
allow: [sense, plan]
message: Commands under this set are planned and checked for review; none runs.
sql.inlineValues, a subject the SQLite target declares, counts the values a statement writes into its text instead of
passing them as parameters, such as the 'CA' of State = 'CA'; count: { max: 0 } refuses any.
When a command is refused
A refused command is not an exception: Executed is false, and nothing ran.
CommandOutcome outcome = await runtime.RunAsync(request);
if (!outcome.Executed)
{
// Nothing ran. The rules refused the command, the target refused it, or no plan passed the check.
foreach (var violation in outcome.Check.Violations)
Console.WriteLine($"- {violation.Set}/{violation.RuleId}: {violation.Message}");
foreach (var refusal in outcome.Check.Refusals)
Console.WriteLine($"- {refusal}");
if (!outcome.Planning.Success)
foreach (var issue in outcome.Planning.Issues)
Console.WriteLine($"- {issue.Message}"); // what the model was told on each attempt
}
These plans, checked with CheckAsync and, for the last SQLite one, run with ExecuteAsync, print:
SELECT CustomerId, LastFour FROM PaymentCards, under customers-read
Allowed: false
- customers-read/customers-only: Use only the Customers table.
DELETE FROM Customers WHERE State = @state, under customers-read
Allowed: false
- customers-read/read-only: Only read; this set never changes data.
- writes are not allowed: data.allowWrites resolved to false
WITH numbers AS (SELECT 1 AS n) SELECT n FROM numbers, under customers-read
Allowed: false
- nothing was checked: the target's inspection named no entity and did not say the command touches none
SELECT 1, under customers-read
Allowed: true
TouchesNoEntity: true
SELECT FullName FROM Customers WHERE State = 'CA', under customers-review
Allowed: false
- customers-review/parameters-only: Pass every value as a named parameter, such as @state, never as a literal.
ExecuteAsync of SELECT FullName FROM Customers WHERE State = @state, under customers-review
Allowed: false
- customers-review/plan-only: Commands under this set are planned and checked for review; none runs.
The JSON-collection sample's filter of the collection audit, under store-data
Allowed: false
- store-data/store-collections: Read only the orders and products collections.
| When | What happens |
|---|---|
| The check refuses a plan | The plan goes back to the model with the rules' messages, within the attempt limit. When no attempt passes, Planning holds what the model was told. |
| The inspection names an operation and no entity | The command is refused as "nothing was checked", unless the target sets TouchesNoEntity: an allow rule checks a list item by item, so an empty list of entities would pass it. The SQLite target sets it for a statement such as SELECT 1, which reads no table; a read of a common table expression alone is refused. |
| The target refuses | Its own refusals always apply, whatever a rule file allows. |
| The target itself fails | It throws TargetOperationException, whose Kind is SchemaMismatch, Refused, Transient, Timeout or Other. After a schema mismatch the runtime describes the target again and plans once more. |
Write a target of your own
ITarget has four members to implement and three with default bodies:
| Member | What it does |
|---|---|
Name |
The name rule files bind to with target:. It does not change for the life of the target. |
DescribeAsync |
Returns a TargetDescription: the command language, the entities with their fields and operations, instructions for the model and, for a command written as JSON, its schema. |
InspectAsync |
Reads a planned command without running it and returns a TargetInspection. It should need no I/O, and should refuse rather than throw. |
ExecuteAsync |
Runs a command that passed the check. It receives a TargetExecution, which has no public constructor: only the runtime creates one, after a passing check, with the command the target inspected and the Limits (Target limits). |
Subjects |
The subjects the target hands to the Data checkpoint: the core's, TargetSubjects.Core, then any of its own. |
ProbeAsync |
A token for the schema: the same token for the same schema in any process, or null. A counter or the time of the last write does not qualify. |
GetChangeToken |
A change token that fires when the technology reports a schema change, or null. |
A TargetInspection says what the command would do: Entities, Operations (read for a read; any other operation
needs data.allowWrites), ParameterNames, Extra (the values of the target's own subjects), Refusals and
TouchesNoEntity.
The two worked examples here, a target over JSON documents grouped in collections and the SQLite target of this page,
are examples, not part of the packages. The full samples, samples/IntentSharp.Samples.JsonCollection and
samples/IntentSharp.Samples.Sqlite, are in the library's source that licensees receive.
The JSON-collection target's command is a JSON filter. It describes its collections:
/// <summary>Describes the collections, ordered by name, with the fields their documents hold.</summary>
public ValueTask<TargetDescription> DescribeAsync(TargetContext context, CancellationToken ct = default)
{
List<TargetEntity> entities;
lock (_gate) entities = Entities();
return new ValueTask<TargetDescription>(new TargetDescription
{
Language = LanguageName,
CommandSchema = CommandSchema,
Instructions = Instructions,
Entities = entities,
});
}
It reads a filter without running it. Every filter reads the collection it names, so it never sets TouchesNoEntity:
/// <summary>
/// Reads a filter: the collection it names (its one entity) and <c>read</c>. It refuses a command that is not a filter,
/// a <c>where</c> deeper than <see cref="MaxWhereDepth"/>, a field the described collection does not have, and
/// parameters, which this target does not take. Every filter reads the collection it names, so
/// <see cref="TargetInspection.TouchesNoEntity"/> always stays false.
/// </summary>
public ValueTask<TargetInspection> InspectAsync(TargetCommand command, TargetContext context, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(command);
var (filter, refusals) = ReadFilter(command.Command);
if (command.Parameters is { Count: > 0 })
refusals.Add("This target takes no parameters; write the values directly in where.");
if (filter is null) return new ValueTask<TargetInspection>(new TargetInspection { Refusals = refusals });
var entity = context?.Description?.Entities.FirstOrDefault(e => e.Name == filter.Collection);
if (entity is not null) refusals.AddRange(FieldProblems(filter, entity));
return new ValueTask<TargetInspection>(new TargetInspection
{
Entities = [filter.Collection],
TouchesNoEntity = false, // a filter always reads the collection it names
Operations = ["read"],
Refusals = refusals,
});
}
Its rule file allows two of its three collections; the last entry of the refusals above is its refusal of the third:
intentsharp: rules/1
set: store-data
label: "1"
checkpoint: data
target: documents
rules:
- id: store-collections
subject: data.entities
allow: [orders, products]
message: Read only the orders and products collections.
- id: read-only
subject: data.operations
allow: [read]
message: Only read.
settings:
data.maxResults: 50
The SQLite target declares subjects of its own after the core's, among them sql.inlineValues, which
customers-review uses:
/// <summary>The core's data subjects, then the inspector's: <c>sql.statementKinds</c>, <c>sql.words</c> and <c>sql.inlineValues</c>.</summary>
public IReadOnlyList<SubjectDescriptor> Subjects => TargetSubjects.Core.Concat(SqliteCommandInspector.Subjects).ToList();
TargetContractKit, in IntentSharp.Testing, runs the checks every target must pass: two descriptions of the
unchanged target agree, one read is planned, checked and run, and a plan that was not checked never reaches
ExecuteAsync. Run it in the target's own tests:
// In the target's own tests: two descriptions agree, one read is planned, checked and run, and a plan that was
// not checked never reaches ExecuteAsync. A step that fails throws TargetContractException, naming the step.
await TargetContractKit.RunAsync(target, new TargetContractScript
{
Request = "List the open orders.",
Command = JsonNode.Parse("""{ "collection": "orders", "where": { "status": "open" }, "project": ["id"] }""")!,
ExpectedEntities = ["orders"],
});
The check is a guard, not a permission boundary
The inspection lets the rules refuse a command before it runs. It does not prove a command harmless, and it can miss what it cannot read. So give the target a database account with only the permissions the application needs: read-only for reads, and write access only to what a write should change.