Documentation
Table of Contents

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>();
  • RunAsync describes the target, plans, checks and runs, in that order. Executed says whether the command ran.
  • Each record is a JSON object; As<T>() maps the records to your type. Truncated is true when there were more records than data.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.