File size: 4,663 Bytes
20c2253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
%%%-------------------------------------------------------------------
%%% @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.