Skip to content

Commit 3af32fe

Browse files
committed
Validate Extravaganza context AI acceptance
1 parent 0b43474 commit 3af32fe

3 files changed

Lines changed: 264 additions & 5 deletions

File tree

lib/stack_lab/extravaganza_external_acceptance.ex

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
3535
run
3636
evidence
3737
route_evidence
38+
context_ai_summary
3839
events
3940
reviews
4041
review_decision
@@ -70,6 +71,20 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
7071
trace_ref
7172
)
7273

74+
@required_context_ai_summary_paths [
75+
["context_packet", "context_packet_ref"],
76+
["context_packet", "packet_hash"],
77+
["context_packet", "receipt_ref"],
78+
["route_decision", "route_decision_ref"],
79+
["model_invocation", "model_invocation_ref"],
80+
["model_invocation", "model_receipt_ref"],
81+
["model_invocation", "prompt_artifact_ref"],
82+
["model_invocation", "provider_payload_ref"],
83+
["model_invocation", "payload_hash"],
84+
["eval_verdict", "eval_verdict_ref"],
85+
["operator_review", "review_ref"]
86+
]
87+
7388
@spec command_args() :: [String.t()]
7489
def command_args, do: @product_args
7590

@@ -130,7 +145,7 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
130145
end
131146

132147
defp decode_product_receipt(output) do
133-
case Jason.decode(String.trim(output)) do
148+
case decode_json_receipt(output) do
134149
{:ok, %{} = receipt} ->
135150
{:ok, receipt}
136151

@@ -146,6 +161,35 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
146161
end
147162
end
148163

164+
defp decode_json_receipt(output) do
165+
trimmed = String.trim(output)
166+
167+
case Jason.decode(trimmed) do
168+
{:ok, decoded} -> {:ok, decoded}
169+
{:error, _reason} -> decode_trailing_json_receipt(trimmed)
170+
end
171+
end
172+
173+
defp decode_trailing_json_receipt(output) do
174+
output
175+
|> :binary.matches("{")
176+
|> Enum.map(fn {position, 1} ->
177+
binary_part(output, position, byte_size(output) - position)
178+
end)
179+
|> Enum.reverse()
180+
|> Enum.find_value(fn candidate ->
181+
case Jason.decode(String.trim(candidate)) do
182+
{:ok, %{"schema" => @product_schema} = receipt} -> {:ok, receipt}
183+
{:ok, %{} = receipt} -> {:ok, receipt}
184+
_other -> nil
185+
end
186+
end)
187+
|> case do
188+
nil -> Jason.decode(output)
189+
decoded -> decoded
190+
end
191+
end
192+
149193
defp validate_product_receipt(%{} = receipt) do
150194
with :ok <-
151195
require_equal(receipt["schema"], @product_schema, "extravaganza_receipt_bad_schema"),
@@ -155,7 +199,8 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
155199
:ok <- require_required_refs(receipt),
156200
:ok <- require_lower_terminal_ref(receipt),
157201
:ok <- require_readbacks(receipt),
158-
:ok <- require_route_evidence(receipt) do
202+
:ok <- require_route_evidence(receipt),
203+
:ok <- require_context_ai_summary(receipt) do
159204
require_projection_proof(receipt)
160205
end
161206
end
@@ -245,6 +290,74 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
245290
end
246291
end
247292

293+
defp require_context_ai_summary(receipt) do
294+
summary = context_ai_summary(receipt)
295+
296+
with :ok <- require_context_summary_present(summary),
297+
:ok <- require_context_summary_surface(summary),
298+
:ok <- require_context_summary_product_safe(summary),
299+
:ok <- require_context_summary_hashes(summary) do
300+
require_context_summary_refs(summary)
301+
end
302+
end
303+
304+
defp require_context_summary_present(%{} = summary) when map_size(summary) > 0, do: :ok
305+
306+
defp require_context_summary_present(_summary) do
307+
{:error, error("extravaganza_receipt_missing_context_ai_summary")}
308+
end
309+
310+
defp require_context_summary_surface(%{"surface" => "AppKit.ContextSurface"}), do: :ok
311+
312+
defp require_context_summary_surface(summary) do
313+
{:error, error("extravaganza_receipt_bad_context_surface", surface: summary["surface"])}
314+
end
315+
316+
defp require_context_summary_product_safe(summary) do
317+
if summary["live_provider_required?"] == false and summary["lower_stack_imports?"] == false and
318+
summary["forbidden_raw_fields_present?"] == false do
319+
:ok
320+
else
321+
context_summary_safety_error(summary)
322+
end
323+
end
324+
325+
defp context_summary_safety_error(%{"forbidden_raw_fields_present?" => value})
326+
when value != false do
327+
{:error, error("extravaganza_receipt_context_summary_has_raw_fields")}
328+
end
329+
330+
defp context_summary_safety_error(_summary) do
331+
{:error, error("extravaganza_receipt_context_summary_not_product_safe")}
332+
end
333+
334+
defp require_context_summary_hashes(summary) do
335+
cond do
336+
not safe_sha?(get_in(summary, ["context_packet", "packet_hash"])) ->
337+
{:error, error("extravaganza_receipt_context_packet_hash_invalid")}
338+
339+
not safe_sha?(get_in(summary, ["model_invocation", "payload_hash"])) ->
340+
{:error, error("extravaganza_receipt_model_payload_hash_invalid")}
341+
342+
true ->
343+
:ok
344+
end
345+
end
346+
347+
defp require_context_summary_refs(summary) do
348+
missing =
349+
@required_context_ai_summary_paths
350+
|> Enum.reject(fn path -> safe_ref?(get_in(summary, path)) end)
351+
|> Enum.map(&Enum.join(&1, "."))
352+
353+
if missing == [] do
354+
:ok
355+
else
356+
{:error,
357+
error("extravaganza_receipt_context_summary_missing_refs", missing_fields: missing)}
358+
end
359+
end
360+
248361
defp external_receipt(product_receipt, opts) do
249362
root = Keyword.get(opts, :extravaganza_root, default_extravaganza_root())
250363
refs = refs(product_receipt)
@@ -270,6 +383,7 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
270383
},
271384
"validated_refs" => validated_refs(refs),
272385
"validated_route_evidence" => route_evidence(product_receipt),
386+
"validated_context_ai_summary" => compact_context_summary(product_receipt),
273387
"product_receipt" => product_summary(product_receipt),
274388
"provider_smoke" => %{
275389
"classification" => "separate_provider_only_not_product_acceptance",
@@ -304,6 +418,7 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
304418
"readbacks" => Enum.map(readbacks(receipt), & &1["name"]),
305419
"steps" => proof_steps(receipt),
306420
"route_evidence" => route_evidence(receipt),
421+
"context_ai_summary" => compact_context_summary(receipt),
307422
"refs" =>
308423
Map.take(
309424
refs(receipt),
@@ -348,6 +463,49 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
348463

349464
defp route_evidence_readback(_readback), do: nil
350465

466+
defp context_ai_summary(receipt) do
467+
direct = map_or_empty(get_in(receipt, ["data", "context_ai_summary"]))
468+
proof_summary = map_or_empty(get_in(receipt, ["data", "proof", "context_ai_summary"]))
469+
470+
cond do
471+
direct != %{} ->
472+
direct
473+
474+
proof_summary != %{} ->
475+
proof_summary
476+
477+
true ->
478+
receipt
479+
|> readbacks()
480+
|> Enum.find_value(%{}, &context_ai_summary_readback/1)
481+
end
482+
end
483+
484+
defp context_ai_summary_readback(%{"name" => "context_ai_summary"} = readback) do
485+
map_or_empty(readback["data"])
486+
end
487+
488+
defp context_ai_summary_readback(_readback), do: nil
489+
490+
defp compact_context_summary(receipt) do
491+
summary = context_ai_summary(receipt)
492+
493+
%{
494+
"surface" => summary["surface"],
495+
"proof_class" => summary["proof_class"],
496+
"context_packet_ref" => get_in(summary, ["context_packet", "context_packet_ref"]),
497+
"packet_hash" => get_in(summary, ["context_packet", "packet_hash"]),
498+
"route_decision_ref" => get_in(summary, ["route_decision", "route_decision_ref"]),
499+
"model_invocation_ref" => get_in(summary, ["model_invocation", "model_invocation_ref"]),
500+
"model_receipt_ref" => get_in(summary, ["model_invocation", "model_receipt_ref"]),
501+
"eval_verdict_ref" => get_in(summary, ["eval_verdict", "eval_verdict_ref"]),
502+
"review_ref" => get_in(summary, ["operator_review", "review_ref"]),
503+
"prompt_artifact_ref" => get_in(summary, ["model_invocation", "prompt_artifact_ref"]),
504+
"provider_payload_ref" => get_in(summary, ["model_invocation", "provider_payload_ref"]),
505+
"payload_hash" => get_in(summary, ["model_invocation", "payload_hash"])
506+
}
507+
end
508+
351509
defp readbacks(receipt) do
352510
case proof(receipt)["readbacks"] do
353511
readbacks when is_list(readbacks) -> readbacks
@@ -359,6 +517,8 @@ defmodule StackLab.ExtravaganzaExternalAcceptance do
359517
defp map_or_empty(_value), do: %{}
360518

361519
defp safe_ref?(value), do: is_binary(value) and String.trim(value) != ""
520+
defp safe_sha?("sha256:" <> hash), do: byte_size(hash) == 64
521+
defp safe_sha?(_value), do: false
362522

363523
defp mix_executable, do: System.find_executable("mix") || "mix"
364524

support/no_bypass_scanner/test/no_bypass_scanner_test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule StackLab.NoBypassScannerTest do
1111
assert receipt.status == :pass
1212
assert receipt.fixture_ref == "AOC-044"
1313
assert receipt.owner_repo == "extravaganza"
14+
assert "app-kit-context-surface://tenant-1/context" in receipt.approved_facade_refs
1415
assert receipt.findings == []
1516
end
1617

@@ -56,6 +57,7 @@ defmodule StackLab.NoBypassScannerTest do
5657
target_code_paths: ["apps/extravaganza_core/lib/extravaganza/headless_surface.ex"],
5758
approved_facade_refs: [
5859
"app-kit-adaptive-control-surface://tenant-1/adaptive",
60+
"app-kit-context-surface://tenant-1/context",
5961
"app-kit-headless-surface://tenant-1/headless",
6062
"app-kit-authority-projection://tenant-1/headless"
6163
],

0 commit comments

Comments
 (0)