Current section
Files
Jump to
Current section
Files
src/core/adk_failure.erl
%% @doc Structural, secret-free failure values for public runtime boundaries.
%%
%% Failure reasons and stacktraces are attacker/provider controlled and may
%% contain credentials, request bodies, or arbitrary user data. This module
%% deliberately classifies those terms instead of redacting selected keys.
%% Only bounded component/operation/class/status/correlation metadata survives.
-module(adk_failure).
-export([exception/4, external/3, sanitize/3,
callback_value/3, model_response/3,
log_metadata/3, is_failure/1]).
-define(MAX_CORRELATION_BYTES, 512).
-define(MAX_SCAN_DEPTH, 5).
-define(MAX_MAP_ENTRIES, 32).
-type failure() :: {adk_failure, map()}.
-export_type([failure/0]).
-spec exception(term(), term(), term(), term()) -> failure().
exception(Component, Operation, Class, Reason) ->
build(Component, Operation, safe_class(Class), Reason).
-spec external(term(), term(), term()) -> failure().
external(Component, Operation, Reason) ->
build(Component, Operation, external, Reason).
-spec sanitize(term(), term(), term()) -> failure().
sanitize(_Component, _Operation, Failure = {adk_failure, Metadata})
when is_map(Metadata) ->
Failure;
sanitize(Component, Operation, Reason) ->
external(Component, Operation, Reason).
%% Keep the conventional error wrapper used by model/tool plugin hooks while
%% replacing its payload. Successful values are intentionally not rewritten.
-spec callback_value(term(), term(), term()) -> term().
callback_value(Component, Operation, {error, Reason}) ->
{error, sanitize(Component, Operation, Reason)};
callback_value(Component, Operation, {failed, Reason}) ->
{failed, sanitize(Component, Operation, Reason)};
callback_value(Component, Operation, {'EXIT', Reason}) ->
{'EXIT', sanitize(Component, Operation, Reason)};
callback_value(_Component, _Operation,
Failure = {adk_failure, Metadata}) when is_map(Metadata) ->
Failure;
callback_value(Component, Operation, Reason) ->
sanitize(Component, Operation, Reason).
%% JSON-safe error body suitable for a tool response that will be shown to a
%% model. It never contains the original reason or an HTTP response body.
-spec model_response(term(), term(), term()) -> map().
model_response(Component, Operation, Reason) ->
{adk_failure, Metadata} = sanitize(Component, Operation, Reason),
#{<<"failure">> => json_metadata(Metadata)}.
-spec log_metadata(term(), term(), term()) -> map().
log_metadata(Component, Operation, Failure) ->
{adk_failure, Metadata} = sanitize(Component, Operation, Failure),
Metadata.
-spec is_failure(term()) -> boolean().
is_failure({adk_failure, Metadata}) when is_map(Metadata) -> true;
is_failure(_) -> false.
build(Component, Operation, Class, Reason) ->
Base = #{component => safe_tag(Component, unknown_component),
operation => safe_tag(Operation, unknown_operation),
class => Class,
reason => reason_tag(Reason)},
WithStatus = case find_status(Reason, 0) of
undefined -> Base;
Status -> Base#{status => Status}
end,
Correlations = find_correlations(Reason, 0, #{}),
Metadata = case map_size(Correlations) of
0 -> WithStatus;
_ -> WithStatus#{correlation => Correlations}
end,
{adk_failure, Metadata}.
safe_class(error) -> error;
safe_class(exit) -> exit;
safe_class(throw) -> throw;
safe_class(_) -> exception.
safe_tag(Tag, _Default) when is_atom(Tag) -> Tag;
safe_tag(_Tag, Default) -> Default.
reason_tag({adk_failure, Metadata}) when is_map(Metadata) ->
maps:get(reason, Metadata, failure);
reason_tag({Tag, _}) when is_atom(Tag) -> Tag;
reason_tag({Tag, _, _}) when is_atom(Tag) -> Tag;
reason_tag({Tag, _, _, _}) when is_atom(Tag) -> Tag;
reason_tag({Tag, _, _, _, _}) when is_atom(Tag) -> Tag;
reason_tag(Tag) when is_atom(Tag) -> Tag;
reason_tag(Binary) when is_binary(Binary) -> binary_failure;
reason_tag(List) when is_list(List) -> list_failure;
reason_tag(Map) when is_map(Map) -> map_failure;
reason_tag(Tuple) when is_tuple(Tuple) -> tuple_failure;
reason_tag(Integer) when is_integer(Integer) -> integer_failure;
reason_tag(Float) when is_float(Float) -> float_failure;
reason_tag(Pid) when is_pid(Pid) -> process_failure;
reason_tag(Ref) when is_reference(Ref) -> reference_failure;
reason_tag(Port) when is_port(Port) -> port_failure;
reason_tag(Fun) when is_function(Fun) -> function_failure;
reason_tag(_) -> unknown_failure.
find_status(_Term, Depth) when Depth >= ?MAX_SCAN_DEPTH -> undefined;
find_status(Map, Depth) when is_map(Map) ->
case status_from_map(Map) of
undefined ->
find_status_map(
maps:iterator(Map), Depth + 1, ?MAX_MAP_ENTRIES);
Status -> Status
end;
find_status(Tuple, Depth) when is_tuple(Tuple) ->
Values = tuple_to_list(Tuple),
case Values of
[Tag, Status | _]
when (Tag =:= http_error orelse Tag =:= http_status orelse
Tag =:= http_failure orelse Tag =:= status),
is_integer(Status), Status >= 100, Status =< 599 ->
Status;
_ -> find_status_list(Values, Depth + 1)
end;
find_status(List, Depth) when is_list(List) ->
find_status_list_bounded(List, Depth + 1, 32);
find_status(_, _) -> undefined.
status_from_map(Map) ->
status_from_keys([status, status_code, http_status,
<<"status">>, <<"status_code">>,
<<"http_status">>], Map).
status_from_keys([], _Map) -> undefined;
status_from_keys([Key | Rest], Map) ->
case maps:find(Key, Map) of
{ok, Status} when is_integer(Status), Status >= 100, Status =< 599 ->
Status;
_ -> status_from_keys(Rest, Map)
end.
find_status_list([], _Depth) -> undefined;
find_status_list([Value | Rest], Depth) ->
case find_status(Value, Depth) of
undefined -> find_status_list(Rest, Depth);
Status -> Status
end.
find_status_list_bounded(_List, _Depth, 0) -> undefined;
find_status_list_bounded([], _Depth, _Remaining) -> undefined;
find_status_list_bounded([Value | Rest], Depth, Remaining) ->
case find_status(Value, Depth) of
undefined -> find_status_list_bounded(Rest, Depth, Remaining - 1);
Status -> Status
end;
find_status_list_bounded(_Improper, _Depth, _Remaining) -> undefined.
find_status_map(_Iterator, _Depth, 0) -> undefined;
find_status_map(Iterator0, Depth, Remaining) ->
case maps:next(Iterator0) of
none -> undefined;
{_Key, Value, Iterator} ->
case find_status(Value, Depth) of
undefined -> find_status_map(
Iterator, Depth, Remaining - 1);
Status -> Status
end
end.
find_correlations(_Term, Depth, Acc) when Depth >= ?MAX_SCAN_DEPTH -> Acc;
find_correlations(Map, Depth, Acc0) when is_map(Map) ->
find_correlations_map(
maps:iterator(Map), Depth + 1, Acc0, ?MAX_MAP_ENTRIES);
find_correlations(Tuple, Depth, Acc) when is_tuple(Tuple) ->
find_correlations_list(tuple_to_list(Tuple), Depth + 1, Acc, 32);
find_correlations(List, Depth, Acc) when is_list(List) ->
find_correlations_list(List, Depth + 1, Acc, 32);
find_correlations(_, _, Acc) -> Acc.
find_correlations_list(_Values, _Depth, Acc, 0) -> Acc;
find_correlations_list([], _Depth, Acc, _Remaining) -> Acc;
find_correlations_list([Value | Rest], Depth, Acc0, Remaining) ->
Acc = find_correlations(Value, Depth, Acc0),
find_correlations_list(Rest, Depth, Acc, Remaining - 1);
find_correlations_list(_Improper, _Depth, Acc, _Remaining) -> Acc.
find_correlations_map(_Iterator, _Depth, Acc, 0) -> Acc;
find_correlations_map(Iterator0, Depth, Acc0, Remaining) ->
case maps:next(Iterator0) of
none -> Acc0;
{Key, Value, Iterator} ->
Acc = case correlation_key(Key) of
undefined -> find_correlations(Value, Depth, Acc0);
CorrelationKey ->
case correlation_fingerprint(Value) of
undefined -> Acc0;
Fingerprint -> Acc0#{CorrelationKey => Fingerprint}
end
end,
find_correlations_map(
Iterator, Depth, Acc, Remaining - 1)
end.
correlation_key(invocation_id) -> invocation_id;
correlation_key(run_id) -> run_id;
correlation_key(call_id) -> call_id;
correlation_key(task_ref) -> task_ref;
correlation_key(request_id) -> request_id;
correlation_key(correlation_id) -> correlation_id;
correlation_key(<<"invocation_id">>) -> invocation_id;
correlation_key(<<"run_id">>) -> run_id;
correlation_key(<<"call_id">>) -> call_id;
correlation_key(<<"task_ref">>) -> task_ref;
correlation_key(<<"request_id">>) -> request_id;
correlation_key(<<"correlation_id">>) -> correlation_id;
correlation_key(_) -> undefined.
correlation_fingerprint(Value) when is_binary(Value),
byte_size(Value) > 0,
byte_size(Value) =< ?MAX_CORRELATION_BYTES ->
fingerprint(Value);
correlation_fingerprint(Value) when is_atom(Value); is_integer(Value) ->
fingerprint(term_to_binary(Value));
correlation_fingerprint(_) -> undefined.
fingerprint(Binary) ->
Hash = crypto:hash(sha256, Binary),
Prefix = binary:part(Hash, 0, 12),
binary:encode_hex(Prefix, lowercase).
json_metadata(Metadata) ->
maps:from_list(
[{atom_to_binary(Key, utf8), json_value(Value)}
|| {Key, Value} <- maps:to_list(Metadata)]).
json_value(Value) when is_atom(Value) -> atom_to_binary(Value, utf8);
json_value(Value) when is_map(Value) -> json_metadata(Value);
json_value(Value) -> Value.