Skip to content

Commit 592fa66

Browse files
committed
Validate Context ABI ref schemes
1 parent ce1349e commit 592fa66

3 files changed

Lines changed: 69 additions & 8 deletions

File tree

core/context_abi/lib/outer_brain/context_abi/context_packet.ex

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,31 @@ defmodule OuterBrain.ContextABI.ContextPacket do
4444

4545
def new(attrs) when is_map(attrs) do
4646
with :ok <- Validator.reject_raw_payload(attrs),
47-
{:ok, tenant_ref} <- Validator.required_string(attrs, :tenant_ref),
48-
{:ok, user_request_ref} <- Validator.required_string(attrs, :user_request_ref),
47+
{:ok, tenant_ref} <- Validator.required_ref(attrs, :tenant_ref, ["tenant://"]),
48+
{:ok, user_request_ref} <-
49+
Validator.required_ref(attrs, :user_request_ref, [
50+
"artifact://",
51+
"user-request://",
52+
"request://"
53+
]),
4954
{:ok, system_instruction_ref} <-
50-
Validator.required_string(attrs, :system_instruction_ref),
51-
{:ok, memory_refs} <- Validator.string_list(attrs, :memory_refs),
55+
Validator.required_ref(attrs, :system_instruction_ref, [
56+
"artifact://",
57+
"system-instruction://"
58+
]),
59+
{:ok, memory_refs} <-
60+
Validator.string_ref_list(attrs, :memory_refs, ["memory://", "memory-candidate://"]),
5261
:ok <- reject_unpromoted_memory_refs(memory_refs),
53-
{:ok, budget_ref} <- Validator.required_string(attrs, :budget_ref),
54-
{:ok, model_class_allowlist} <- Validator.string_list(attrs, :model_class_allowlist),
62+
{:ok, budget_ref} <- Validator.required_ref(attrs, :budget_ref, ["budget://"]),
63+
{:ok, model_class_allowlist} <-
64+
Validator.string_ref_list(attrs, :model_class_allowlist, [
65+
"model-class://",
66+
"class://"
67+
]),
5568
:ok <- require_nonempty(model_class_allowlist, :model_class_allowlist),
56-
{:ok, route_policy_ref} <- Validator.required_string(attrs, :route_policy_ref),
57-
{:ok, trace_ref} <- Validator.required_string(attrs, :trace_ref),
69+
{:ok, route_policy_ref} <-
70+
Validator.required_ref(attrs, :route_policy_ref, ["route-policy://"]),
71+
{:ok, trace_ref} <- Validator.required_ref(attrs, :trace_ref, ["trace://"]),
5872
{:ok, extension_refs} <- Validator.optional_map(attrs, :extension_refs) do
5973
hash_input = %{
6074
schema_ref: Validator.fetch(attrs, :schema_ref, @schema_ref),

core/context_abi/lib/outer_brain/context_abi/validator.ex

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ defmodule OuterBrain.ContextABI.Validator do
6363
end
6464
end
6565

66+
@spec required_ref(map(), atom(), [String.t()]) :: {:ok, String.t()} | {:error, Failure.t()}
67+
def required_ref(attrs, field, allowed_prefixes) do
68+
with {:ok, value} <- required_string(attrs, field),
69+
:ok <- validate_ref_scheme(value, field, allowed_prefixes) do
70+
{:ok, value}
71+
end
72+
end
73+
6674
@spec string_list(map(), atom()) :: {:ok, [String.t()]} | {:error, Failure.t()}
6775
def string_list(attrs, field) do
6876
case fetch(attrs, field, []) do
@@ -78,6 +86,24 @@ defmodule OuterBrain.ContextABI.Validator do
7886
end
7987
end
8088

89+
@spec string_ref_list(map(), atom(), [String.t()]) ::
90+
{:ok, [String.t()]} | {:error, Failure.t()}
91+
def string_ref_list(attrs, field, allowed_prefixes) do
92+
with {:ok, values} <- string_list(attrs, field) do
93+
values
94+
|> Enum.reduce_while(:ok, fn value, :ok ->
95+
case validate_ref_scheme(value, field, allowed_prefixes) do
96+
:ok -> {:cont, :ok}
97+
error -> {:halt, error}
98+
end
99+
end)
100+
|> case do
101+
:ok -> {:ok, values}
102+
error -> error
103+
end
104+
end
105+
end
106+
81107
@spec optional_map(map(), atom()) :: {:ok, map()} | {:error, Failure.t()}
82108
def optional_map(attrs, field) do
83109
case fetch(attrs, field, %{}) do
@@ -139,6 +165,17 @@ defmodule OuterBrain.ContextABI.Validator do
139165
failure(:outer_brain, reason_code, safe_message: "context vocabulary value is invalid")
140166
end
141167

168+
defp validate_ref_scheme(value, field, allowed_prefixes) do
169+
if Enum.any?(allowed_prefixes, &String.starts_with?(value, &1)) do
170+
:ok
171+
else
172+
failure(:outer_brain, "outer_brain.context.invalid_ref_scheme.v1",
173+
safe_message: "context ref uses an invalid scheme",
174+
evidence_refs: ["field://#{Atom.to_string(field)}", "ref://#{value}"]
175+
)
176+
end
177+
end
178+
142179
defp find_raw_key(%{__struct__: _} = value), do: value |> Map.from_struct() |> find_raw_key()
143180

144181
defp find_raw_key(%{} = map) do

core/context_abi/test/outer_brain/context_abi_test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ defmodule OuterBrain.ContextABITest do
6161
assert nested_failure.reason_code == "outer_brain.context.raw_payload_rejected.v1"
6262
end
6363

64+
test "compiler rejects invalid context ref schemes" do
65+
assert {:error, %Failure{} = failure} =
66+
compile_request()
67+
|> Map.put(:tenant_ref, "not-a-tenant-ref")
68+
|> ContextABI.compile()
69+
70+
assert failure.reason_code == "outer_brain.context.invalid_ref_scheme.v1"
71+
assert "field://tenant_ref" in failure.evidence_refs
72+
end
73+
6474
test "compiler rejects unpromoted memory candidate refs in production packets" do
6575
assert {:error, %Failure{} = failure} =
6676
compile_request()

0 commit comments

Comments
 (0)