hyperkitty-chromium / src /hk_browser_capability.erl
SNAPKITTYWEST's picture
Add src/hk_browser_capability.erl
20c2253 verified
Raw
History Blame Contribute Delete
4.66 kB
%%%-------------------------------------------------------------------
%%% @doc HK-AGENT / HK-BROWSER boundary :: the typed browser
%%% capability interface and its authorization + argument checks.
%%%
%%% This module is the *only* place that (a) enumerates which
%%% browser operations exist, (b) decides whether a given agent's
%%% granted capability list permits a specific call, and
%%% (c) validates that call's arguments -- before anything reaches
%%% `hk_browser_session'. Per the security model
%%% ("Validate all tool arguments at the control-plane boundary" /
%%% "Do not treat an agent prompt as a security boundary"), this
%%% check happens in `hk_agent_fsm' (the control plane), not inside
%%% whatever produced the agent's plan. An agent with no granted
%%% capability for an operation cannot reach Chromium for it at all
%%% -- there is no fallback "just try it" path.
%%% @end
%%%-------------------------------------------------------------------
-module(hk_browser_capability).
-export([operations/0, is_granted/2, validate_args/2]).
-type operation() :: create_session | close_session | open_tab | close_tab
| navigate | back | forward | reload | read_page
| query_element | click | type | scroll | screenshot
| extract_links.
-export_type([operation/0]).
-spec operations() -> [operation()].
operations() ->
[create_session, close_session, open_tab, close_tab,
navigate, back, forward, reload, read_page,
query_element, click, type, scroll, screenshot, extract_links].
%% @doc `Capabilities' is the list granted to an agent (see
%% `#agent.capabilities'). A capability entry is either a bare atom
%% (unrestricted grant for that operation) or `{Op, Constraints}'
%% for operations that support finer scoping -- currently `navigate'
%% supports `#{allowed_domains => [binary()]}'.
-spec is_granted([term()], operation() | {operation(), map()}) -> boolean().
is_granted(Capabilities, {navigate, #{url := Url}}) ->
lists:any(
fun (navigate) -> true;
({navigate, #{allowed_domains := Domains}}) -> host_allowed(Url, Domains);
(_) -> false
end,
Capabilities);
is_granted(Capabilities, Op) when is_atom(Op) ->
lists:any(
fun (G) when G =:= Op -> true;
({G, _}) when G =:= Op -> true;
(_) -> false
end,
Capabilities).
%% @doc Structural + semantic validation of an operation's arguments.
%% Returns `{ok, NormalizedArgs}' or `{error, Reason}'. This is
%% deliberately independent of authorization (`is_granted/2') --
%% malformed arguments are rejected the same way whether or not the
%% operation would otherwise be permitted.
-spec validate_args(operation(), map()) -> {ok, map()} | {error, term()}.
validate_args(navigate, #{url := Url} = Args) when is_binary(Url) ->
case valid_url(Url) of
true -> {ok, Args};
false -> {error, {invalid_url, Url}}
end;
validate_args(navigate, _) ->
{error, missing_url};
validate_args(open_tab, #{url := Url} = Args) when is_binary(Url) ->
case valid_url(Url) of
true -> {ok, Args};
false -> {error, {invalid_url, Url}}
end;
validate_args(Op, #{selector := Selector} = Args)
when Op =:= click; Op =:= type; Op =:= query_element ->
case is_binary(Selector) andalso byte_size(Selector) > 0 andalso byte_size(Selector) < 2048 of
true when Op =:= type ->
case Args of
#{text := Text} when is_binary(Text), byte_size(Text) =< 10000 -> {ok, Args};
#{text := _} -> {error, text_too_long};
_ -> {error, missing_text}
end;
true -> {ok, Args};
false -> {error, invalid_selector}
end;
validate_args(scroll, #{delta_y := Dy} = Args) when is_integer(Dy), Dy >= -100000, Dy =< 100000 ->
{ok, Args};
validate_args(Op, Args) when Op =:= close_session; Op =:= close_tab; Op =:= back;
Op =:= forward; Op =:= reload; Op =:= read_page;
Op =:= screenshot; Op =:= extract_links; Op =:= create_session ->
{ok, Args};
validate_args(Op, _Args) ->
{error, {invalid_arguments, Op}}.
%%% internal
valid_url(Url) ->
case uri_string:parse(Url) of
#{scheme := Scheme} when Scheme =:= <<"http">>; Scheme =:= <<"https">>;
Scheme =:= <<"about">>; Scheme =:= <<"data">> ->
true;
_ ->
false
end.
host_allowed(Url, Domains) ->
case uri_string:parse(Url) of
#{host := Host} -> lists:member(Host, Domains);
_ -> false
end.