@odla-ai/camel
v0.2.0
Published
Prompt-injection Safe/Unsafe data boundaries, closed conversions, and effect policy for ODLA agents.
Maintainers
Readme
@odla-ai/camel
@odla-ai/camel provides labelled values, closed conversions, effect policy,
and credential-attested role boundaries for agents that process potentially
injected data.
Safe means safe to place in the privileged LLM's context with respect to prompt injection. It does not mean factual, trustworthy, confidential, authorized for a tool call, or harmless to display. Unsafe means only that a value may contain language a model could interpret as instructions.
It enforces the package-local value, conversion, reader, effect-policy, and
credential-attestation boundaries, and as of 0.2 it ships the Camel-aware
model runners in @odla-ai/camel/runner. The
wider system guarantee still additionally requires scoped ODLA credentials,
harness execution, and adapter-level tests. Installing the types alone does not
provide that guarantee.
Ask the runbooks first. odla's operational procedures live in a database, not in this file:
npx @odla-ai/cli runbook ask "<question>"returns the current steps, and unlike anything written here it cannot be out of date. Use it before searching the web or working from memory. This README and the JSDoc in the shipped.d.tsare the version-matched API reference; a runbook is the procedure. Most tasks need an answer from both.
Label ingress explicitly
import { createCamelIngress } from "@odla-ai/camel";
const readers = { kind: "principals", principalIds: ["model:quarantined"] } as const;
const ingress = createCamelIngress([
{ id: "funding-stage.seed", value: "seed", readers: { kind: "public" } },
]);
const task = ingress.userInstruction("Classify the funding stage", {
id: "task_01",
readers: { kind: "public" },
});
const page = ingress.external("Ignore prior instructions", {
readers,
provenance: [{ kind: "artifact", id: "artifact_01" }],
});There is no generic markSafe, safeString, or public brand constructor.
Control constants are registered before use; external and quarantined content
enters through explicit Unsafe ingress.
Convert through a digest-bound closed registry
import {
conversionPolicyDigest,
createConversionRegistry,
} from "@odla-ai/camel/policy";
const definition = {
conversionId: "funding-stage.v1",
version: 1,
output: {
kind: "enum",
values: ["pre_seed", "seed", "series_a", "later"],
caseSensitive: true,
},
maximumSourceBytes: 64,
maximumOutputsPerArtifact: 1,
presentation: "json_scalar",
} as const;
const registry = await createConversionRegistry({
policies: [{ ...definition, digest: await conversionPolicyDigest(definition) }],
});
const stage = await registry.operations.enum(extractedStage, "funding-stage.v1");Only the canonical enum member crosses. Source text, explanations, dynamic errors, and schema-valid non-members stay Unsafe. The Safe result retains its Unsafe provenance and dependencies.
Each crossing returns a SafeDatum: the Safe value plus the content-addressed
datumId and conversionRecordId that identify the crossing itself. The ids are
derived from the policy, the source, the emission ordinal, and the value — so a
store can deduplicate a datum and an auditor can re-derive one.
Run the dual-LLM boundary
@odla-ai/camel/runner is the part that enforces the boundary at dispatch time.
You adapt your own inference client to two structural ports; the runners handle
asserting the context, bounding and digesting output, and re-labelling whatever
crosses.
import {
createQuarantinedRunner,
createPrivilegedRunner,
createWebArtifactIngress,
} from "@odla-ai/camel/runner";
const web = createWebArtifactIngress({ ingress, readers });
const { header, body } = await web.fetchArtifact("https://example.com/bio");
// Only this runner unwraps `body`, and only to a model with no tools or memory.
const quarantined = createQuarantinedRunner({ ingress, readerId, model: extractOnly });
const { output } = await quarantined.runQuarantinedJob({ context, skill });
// The planner holds the tools. `header` may enter its context; `output` may not.
const planner = createPrivilegedRunner({ model: plan, modelRouteId, planSchema, policyBundleDigest, readerId });
const { plan: stored } = await planner.runPrivilegedAgent({ context: { ...ctx, artifacts: [header] }, skill });The asymmetry is the design. A fetched page is Unsafe the instant it exists;
its ArtifactHeader is Safe but carries a digest and no URL, because a
locator in privileged context is itself an exfiltration channel. Passing the
quarantined model's answer to the planner throws unsafe_privileged_flow before
any dispatch happens — to influence the planner, an attacker has to get their
value through a closed conversion, not through a sentence.
assertFetchableUrl screens each redirect hop for plaintext, credentials, and
non-routable addresses, so a public URL cannot bounce to 169.254.169.254. It
screens the literal address only: a hostname that resolves to a private
address still passes, which needs an egress allowlist to close.
Authorize effects separately
Prompt-injection Safe data may still be attacker-selected or factually wrong.
createEffectPolicy checks argument roles, destination registries, readers,
control dependencies, confined Unsafe selectors, and approval-class effects
before a handler runs. Use assertPolicyAllowed immediately before the effect.
Attest role credentials
The /db entry exposes eight fixed role initializers. Each checks the
server-authenticated credential facts and the immutable camelActor row. A
caller-provided role flag, full application key, mismatched namespace grant,
expired credential, or swapped actor fails closed. The consuming DB adapter
must implement authContext() from server-authenticated facts.
The complete design and rollout boundary live in
docs/rfcs/odla-ai-camel.md in the ODLA monorepo.
