From ff5c2af4c0a55b6d8fac1273c6c4a629992e2ac9 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 28 Aug 2026 08:39:40 -0700 Subject: [PATCH 1/3] Address promotion PR #1053 review: argv-aware write gate, subcommand-aware flags Fixes real bugs the promotion PR's fresh full-diff review surfaced (8 across qodo and CodeRabbit) that the feature PR's own incremental rounds missed: - `_is_gh_write`'s gate rewritten to be argv-aware for `gh api` calls, reusing the parsers rules 3/5 already build (`_all_gh_arg_lists`, `_gh_api_path`, `_gh_effective_method`, `_gh_graphql_query`) instead of raw-substring regexes. Fixes a false write classification when an opaque flag value (a --jq expression, for example) happens to contain a write-method spelling like "-XPOST" as data, which previously misclassified a harmless cross-owner read as a write and denied it. Also picks up `gh.exe api` invocations for free, since `_all_gh_arg_lists` already recognizes that executable form; `_GH_WRITE_SUB`/`_GRAPHQL` (the two remaining raw-text gates) are extended for `gh.exe` too, for consistency. - `_gh_write_targets`/`_gh_api_path` are now subcommand-aware: `-f`/`-F` are value-taking only inside `gh api`. On `gh pr create` they are the boolean `--fill`, so treating them as value-consuming there silently swallowed a real following `--repo /` flag whole, letting a cross-owner target through unnoticed. - `_REPOS_PATH_TOKEN` accepts an optional leading slash, matching `gh api`'s own accepted `/repos/owner/repo/...` path spelling. - `_gh_effective_method` treats `--input` as implying POST, same as a field flag, so a reply-endpoint request with a JSON body no longer reads as GET and escapes rule 5. - Rule 5's GraphQL branch denies a `--input`-supplied body outright when no `-f query=...` field is present to read instead, since a resolveReviewThread mutation there is equally invisible to this parser; permitted under the same cross-owner grant as the inline case. Two qodo findings declined with evidence in the thread rather than fixed: the module-header/docstring restating governance policy (already-established file convention, per items 1-4 predating this PR) and a claimed duplicate-query bypass, disproven empirically against the real gh binary (`gh api graphql -f query=X -f query=Y` errors "unexpected override existing field", it never reaches the server). Regression tests added for every fix. --- host-setup/agent-safety/gh-write-guard.py | 155 ++++++++++++++++++---- 1 file changed, 126 insertions(+), 29 deletions(-) diff --git a/host-setup/agent-safety/gh-write-guard.py b/host-setup/agent-safety/gh-write-guard.py index 2eee93ec..9d0eef0d 100755 --- a/host-setup/agent-safety/gh-write-guard.py +++ b/host-setup/agent-safety/gh-write-guard.py @@ -44,9 +44,10 @@ # --- What counts as a GitHub write ------------------------------------------------------------------- # The gh subcommands that mutate. -# The `gh api` command is handled separately, since it needs field and method inspection. +# A path-qualified or `.exe`-suffixed `gh` still starts a shell word this matches, the same recognition `_is_gh_exe` gives it for argv-position parsing. +# The `gh api` command is handled separately, in `_is_gh_write`, since it needs argv-aware method and GraphQL-query inspection rather than a fixed subcommand list. _GH_WRITE_SUB = re.compile( - r"""\bgh\s+(?: + r"""\bgh(?:\.exe)?\s+(?: pr\s+(?:create|comment|close|merge|edit|review|reopen|ready|lock|unlock) | issue\s+(?:create|comment|close|edit|reopen|delete|lock|unlock|pin|unpin|transfer) | release\s+(?:create|edit|delete|upload) @@ -56,17 +57,7 @@ )\b""", re.VERBOSE, ) -_GH_API = re.compile(r"\bgh\s+api\b") -# `-X`/`--method` accept a separate value (`-X POST`), an attached one (`-XPOST`), and an equals-attached one (`-X=POST`, `--method=POST`). -# This must match every spelling, matching how `_gh_effective_method` reads it. -_EXPLICIT_WRITE_METHOD = re.compile( - r"(?:--method[= ]|-X[= ]?)\s*(?:POST|PUT|PATCH|DELETE)\b", re.IGNORECASE -) -# A gh api call with a field flag defaults to POST even without -X, so it is a write. -# `-f`/`-F` also accept an attached value (`-fbody=x`), so no trailing `\b` is required after them. -# It is required after the long-form spellings, where one legitimately separates the flag from the next word. -_API_FIELD_FLAG = re.compile(r"(?:^|\s)(?:-f|-F)|(?:^|\s)(?:--field|--raw-field|--input)\b") -_GRAPHQL = re.compile(r"\bgh\s+api\b.*\bgraphql\b", re.DOTALL) +_GRAPHQL = re.compile(r"\bgh(?:\.exe)?\s+api\b.*\bgraphql\b", re.DOTALL) _MUTATION = re.compile(r"\bmutation\b") # Loose pre-filter only: matches `git` before `push` even with global options between them # (git -C push). _push_arg_lists is the accurate arbiter that confirms an executable push. @@ -104,11 +95,12 @@ # Every spelling gh accepts for the target flag, being `--repo x`, `--repo=x`, `-R x`, `-R=x`, and the attached short form `-Rx`. # A form left out is not a near-miss, it is a silent bypass of the whole repository scope, so each is read by argv position below (`_gh_write_targets`) rather than assumed to be a space-separated pair. _REPO_FLAG_BARE = {"--repo", "-R"} -_REPOS_PATH_TOKEN = re.compile(r"^repos/(?P[A-Za-z0-9_.\-]+)/(?P[A-Za-z0-9_.\-]+)") -# Flags whose own value is opaque text (a PR/issue title or body, a GraphQL field, a jq/template expression, a header), and so is skipped whole rather than pattern-matched for a repo target. +# `gh api` accepts a leading slash on the path (`gh api /repos/o/r/...`), so it is optional here too. +_REPOS_PATH_TOKEN = re.compile(r"^/?repos/(?P[A-Za-z0-9_.\-]+)/(?P[A-Za-z0-9_.\-]+)") +# Flags whose own value is opaque text (a PR/issue title, body, or notes), and so is skipped whole rather than pattern-matched for a repo target. # Without this, a --body describing a `--repo /` doc line, or a commit message quoting the same convention, reads as a real flag. -# The incident this closes denied an ordinary `git commit` whose message body merely quoted the fleet's own `--repo owner/repo` example text. -_GH_TEXT_VALUE_FLAGS = { +# Shared across every create/comment/edit-style subcommand (pr, issue, release, gist). +_GH_CREATE_TEXT_VALUE_FLAGS = { "--title", "-t", "--body", @@ -119,6 +111,10 @@ "--message", "-m", "--desc", +} +# `gh api`'s own value-taking flags, meaningful only inside an `api` invocation. +# `-f`/`-F` are the boolean `--fill` on `gh pr create`, so they must not be treated as value-consuming outside of `api`, or the flag right after them (a real `--repo /`) is silently skipped. +_GH_API_VALUE_FLAGS = _GH_CREATE_TEXT_VALUE_FLAGS | { "-f", "-F", "--field", @@ -139,15 +135,36 @@ def _is_gh_write(cmd): + """True when `cmd` is a GitHub write: a known-mutating `gh` subcommand, a `git push`, or a `gh api` + call whose effective method is not GET. + + The `gh api` half is argv-aware (via `_all_gh_arg_lists`/`_gh_api_path`/`_gh_effective_method`/ + `_gh_graphql_query`), reading a flag or a GraphQL query only from where it actually sits in one + invocation's own argv rather than a substring search over the whole command. A raw substring search + reads a write-method spelling out of an opaque flag value too, such as a `--jq` expression that + merely contains the text `-XPOST` as data, misclassifying a harmless read as a write. + + A GraphQL call whose body comes from `--input` is treated as a write whenever its query text cannot + be read at all (`_gh_graphql_query` returns None), since a `resolveReviewThread` mutation supplied + that way is equally invisible; `_check_reply_resolve_helper` denies that case explicitly. + """ if _GH_WRITE_SUB.search(cmd) or _push_arg_lists(cmd): return True - if _GH_API.search(cmd): - if _EXPLICIT_WRITE_METHOD.search(cmd): - return True - if _GRAPHQL.search(cmd) and _MUTATION.search(cmd): + for args in _all_gh_arg_lists(cmd): + if not args or args[0] != "api": + continue + path = _gh_api_path(args) + if path == "graphql": + q = _gh_graphql_query(args) + if q: + if _MUTATION.search(q): + return True + continue # a genuine read-only query, not a mutation + if _gh_has_input(args): + return True # uninspectable body; treat cautiously so rules 1-5 can look closer + continue + if _gh_effective_method(args) != "GET": return True - if _API_FIELD_FLAG.search(cmd) and not _GRAPHQL.search(cmd): - return True # gh api -f k=v => POST return False @@ -490,11 +507,13 @@ def _gh_write_targets(cmd): """ targets = [] for args in _all_gh_arg_lists(cmd): + # `-f`/`-F` are value-taking only inside `api`, on `pr create` etc. they are the boolean `--fill`, so treating them as value-consuming there would swallow a real following `--repo` flag whole. + flags = _GH_API_VALUE_FLAGS if args and args[0] == "api" else _GH_CREATE_TEXT_VALUE_FLAGS n = len(args) i = 0 while i < n: t = args[i] - if t in _GH_TEXT_VALUE_FLAGS and "=" not in t: + if t in flags and "=" not in t: i += 2 # this flag's own value is opaque text, never a repo target continue if t in _REPO_FLAG_BARE: @@ -530,7 +549,7 @@ def _gh_api_path(args): i = 1 while i < n: t = args[i] - if t in _GH_TEXT_VALUE_FLAGS and "=" not in t: + if t in _GH_API_VALUE_FLAGS and "=" not in t: i += 2 continue if t.startswith("-"): @@ -581,15 +600,22 @@ def _gh_graphql_query(args): return None +def _gh_has_input(args): + """True when this `gh api` invocation's own argv carries `--input` (bare or equals-attached), gh's + flag for supplying the request body from a file or stdin. + """ + return any(t == "--input" or t.startswith("--input=") for t in args) + + def _gh_effective_method(args): """The effective HTTP method of a `gh api` invocation's own argv: an explicit `-X`/`--method` value - when present, in every spelling `gh` accepts, else POST when a field flag is present (`gh`'s own - default for a write-shaped call), else GET. + when present, in every spelling `gh` accepts, else POST when a field flag or `--input` is present + (`gh`'s own default for a write-shaped call), else GET. """ n = len(args) i = 0 method = None - has_field = False + has_field = _gh_has_input(args) while i < n: t = args[i] if t in ("-X", "--method"): @@ -791,6 +817,10 @@ def _check_reply_resolve_helper(cmd, environ): target in its own text (the thread id is opaque), so the same fallback is permitted there whenever any grant is active this session, a coarser signal than a REST reply gets, and the residual gap the module docstring's "precision over recall" already accepts for this class of rule. + + A GraphQL body supplied via `--input` is denied outright when it has no `-f`/`-F query=...` field to + read instead (`_gh_graphql_query` returns None), since a `resolveReviewThread` mutation there is + equally invisible to this parser and there is nothing to distinguish it from the inline case above. """ granted = _granted_targets(environ) helper = ( @@ -811,6 +841,14 @@ def _check_reply_resolve_helper(cmd, environ): "helper that captures the reply and the resolve in one call, so a reply can be left " "unresolved across a push and a re-request. " + helper ) + if not q and _gh_has_input(args): + if granted: + continue + return "deny", ( + "This gh api graphql call supplies its body via --input, which cannot be inspected " + "for a resolveReviewThread mutation, so it is denied by the same rule as an inline " + "one. " + helper + ) if path and _REPLY_ENDPOINT_PATH.search(path) and _gh_effective_method(args) == "POST": m = _REPOS_PATH_TOKEN.match(path) if m: @@ -1231,6 +1269,65 @@ def classify(cmd, cwd=None, origin=None, current_branch=None, rules_lookup=None, "deny", "the equals-attached -X=POST form still enters the write gate (CodeRabbit)", ), + ( + "gh api repos/ptr727/PlexCleaner/pulls/5/comments/9/replies --input body.json", + {}, + "deny", + "--input on the replies endpoint is still read as a write (promotion review)", + ), + ( + "gh api graphql --method POST --input resolve.json", + {}, + "deny", + "a GraphQL body from --input is denied as uninspectable (promotion review)", + ), + ( + "gh api graphql --method POST --input resolve.json", + {_ALLOW_ENV: "esphome/esphome"}, + "allow", + "an uninspectable --input GraphQL body is permitted under a cross-owner grant, like the inline case", + ), +] + +_SCOPE_CASES_MORE: list[tuple[str, dict[str, str], str, str]] = [ + # More Rule-3 scope cases, covering the promotion-review round's findings, kept as their own literal rather than growing the one above further. + # (command, environ, expected_decision, label) + ( + "gh api repos/esphome/esphome/issues --jq '.[] | \"-XPOST\"'", + {}, + "allow", + "a --jq expression only containing the text -XPOST is a read, not misread as a write (promotion review)", + ), + ( + "gh.exe api repos/esphome/esphome/issues -f body=x", + {}, + "deny", + "gh.exe is still recognized for an api write (CodeRabbit)", + ), + ( + "gh.exe pr create --repo esphome/esphome --title x", + {}, + "deny", + "gh.exe is still recognized for a pr-create write (companion to CodeRabbit's gh.exe finding)", + ), + ( + "gh api /repos/esphome/esphome/issues -f title=x", + {}, + "deny", + "a leading slash on the REST path does not hide the cross-owner target (CodeRabbit)", + ), + ( + "gh pr create -f --repo esphome/esphome --title x", + {}, + "deny", + "-f as pr create's boolean --fill does not swallow the following --repo (CodeRabbit)", + ), + ( + "gh pr create -f --title x --body y", + {}, + "allow", + "-f as pr create's boolean --fill does not swallow --title either, with no foreign target present", + ), ] # Rule-4 cases, covering branch-rule bypass. @@ -1631,7 +1728,7 @@ def _selftest(): if got != want: ok = False print(f" {mark} [{got:5}] want={want:5} {label}") - for cmd, env, want, label in _SCOPE_CASES: + for cmd, env, want, label in _SCOPE_CASES + _SCOPE_CASES_MORE: got, _ = classify( cmd, origin=origin, From a54a30a7c955306a9707d51fb70bb2d637377aa1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 28 Aug 2026 08:49:06 -0700 Subject: [PATCH 2/3] Address CodeRabbit review: gh.exe --admin, --input ordering Fixes two real bugs CodeRabbit's review of PR #1054 found: - _GH_ADMIN_MERGE required the literal "gh" (no .exe suffix), so gh.exe pr merge --admin passed _check_bypass_flags unnoticed even though _GH_WRITE_SUB already recognizes gh.exe for the ordinary write-subcommand list. - The GraphQL branches in _is_gh_write and _check_reply_resolve_helper checked -f/-F query=... before --input, but gh sends -f/-F fields as URL query-string parameters rather than body fields whenever --input is also present. A harmless decoy query alongside a real --input mutation file therefore had no effect on the actual request, while my classifier trusted the decoy and let the call through unclassified. --input is now checked first in both places. Regression tests added for both. --- host-setup/agent-safety/gh-write-guard.py | 40 ++++++++++++++++------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/host-setup/agent-safety/gh-write-guard.py b/host-setup/agent-safety/gh-write-guard.py index 9d0eef0d..e843fed2 100755 --- a/host-setup/agent-safety/gh-write-guard.py +++ b/host-setup/agent-safety/gh-write-guard.py @@ -73,7 +73,7 @@ _PROTECTED_DEFAULT_ORDER = ("main", "master", "develop") _PROTECTED_DEFAULT = set(_PROTECTED_DEFAULT_ORDER) # `gh pr merge --admin` overrides required reviews/status checks with admin power. -_GH_ADMIN_MERGE = re.compile(r"\bgh\s+pr\s+merge\b[^\n|&;]*(?:^|\s)--admin\b") +_GH_ADMIN_MERGE = re.compile(r"\bgh(?:\.exe)?\s+pr\s+merge\b[^\n|&;]*(?:^|\s)--admin\b") # --- Risk-pattern detectors -------------------------------------------------------------------------- # Output-discard and force-success tails. @@ -155,13 +155,16 @@ def _is_gh_write(cmd): continue path = _gh_api_path(args) if path == "graphql": + # --input checked before trusting any -f/-F query=... value. + # A -f/-F field becomes a URL query-string parameter rather than a body field whenever --input is also present. + # A harmless-looking inline query alongside --input therefore has no effect on the actual request, and the real body is the uninspectable input file. + if _gh_has_input(args): + return True # uninspectable body; treat cautiously so rules 1-5 can look closer q = _gh_graphql_query(args) if q: if _MUTATION.search(q): return True continue # a genuine read-only query, not a mutation - if _gh_has_input(args): - return True # uninspectable body; treat cautiously so rules 1-5 can look closer continue if _gh_effective_method(args) != "GET": return True @@ -832,6 +835,16 @@ def _check_reply_resolve_helper(cmd, environ): for args in _all_gh_arg_lists(cmd): path = _gh_api_path(args) if path == "graphql": + # --input checked before trusting any -f/-F query=... value, matching `_is_gh_write`. + # A harmless decoy query alongside --input has no effect on gh's actual request. + if _gh_has_input(args): + if granted: + continue + return "deny", ( + "This gh api graphql call supplies its body via --input, which cannot be inspected " + "for a resolveReviewThread mutation, so it is denied by the same rule as an inline " + "one. " + helper + ) q = _gh_graphql_query(args) if q and _MUTATION.search(q) and _RESOLVE_THREAD_MUTATION.search(q): if granted: @@ -841,14 +854,6 @@ def _check_reply_resolve_helper(cmd, environ): "helper that captures the reply and the resolve in one call, so a reply can be left " "unresolved across a push and a re-request. " + helper ) - if not q and _gh_has_input(args): - if granted: - continue - return "deny", ( - "This gh api graphql call supplies its body via --input, which cannot be inspected " - "for a resolveReviewThread mutation, so it is denied by the same rule as an inline " - "one. " + helper - ) if path and _REPLY_ENDPOINT_PATH.search(path) and _gh_effective_method(args) == "POST": m = _REPOS_PATH_TOKEN.match(path) if m: @@ -1287,6 +1292,12 @@ def classify(cmd, cwd=None, origin=None, current_branch=None, rules_lookup=None, "allow", "an uninspectable --input GraphQL body is permitted under a cross-owner grant, like the inline case", ), + ( + "gh api graphql --input mutation.json -f query='{viewer{login}}'", + {}, + "deny", + "a decoy -f query=... alongside --input does not hide an uninspectable body (CodeRabbit)", + ), ] _SCOPE_CASES_MORE: list[tuple[str, dict[str, str], str, str]] = [ @@ -1500,6 +1511,13 @@ def classify(cmd, cwd=None, origin=None, current_branch=None, rules_lookup=None, "deny", "line-continued gh pr merge --admin still caught", ), + ( + "gh.exe pr merge 5 --admin --squash", + None, + {}, + "deny", + "gh.exe pr merge --admin still caught (CodeRabbit)", + ), ( "git commit -m 'mention --no-verify in the message'", None, From 8f5c2f684d94fa1f492dc27069d6840528953b97 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 28 Aug 2026 08:52:34 -0700 Subject: [PATCH 3/3] Address qodo review: -F on pr create, semicolons, docstring scope Fixes a real bug qodo's review of PR #1054 found: the subcommand-aware flag split dropped -F entirely from the create-style flag set, but -F is --body-file on gh pr create/issue create (value-taking), distinct from -f which is the boolean --fill only on pr create. A body-file path shaped like repos// was therefore misread as an API target and could falsely deny an otherwise in-scope PR creation. -F is restored to the shared set, with -f staying create-set-excluded (still boolean there). Regression test added. Also fixes two prose findings in this PR's own new content: two semicolons (fleet's no-semicolon rule), and trims the _is_gh_write docstring to state only its behavior contract, moving the argv-aware rationale to a concise inline comment (matching the same class of finding already fixed once in #1052's own review round). Two other findings on this PR (the --input-before-query ordering, and the gh.exe --admin case) were already fixed by the previous commit; both bots' reviews here ran against the commit before that fix landed. --- host-setup/agent-safety/gh-write-guard.py | 30 ++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/host-setup/agent-safety/gh-write-guard.py b/host-setup/agent-safety/gh-write-guard.py index e843fed2..05e649f8 100755 --- a/host-setup/agent-safety/gh-write-guard.py +++ b/host-setup/agent-safety/gh-write-guard.py @@ -106,6 +106,7 @@ "--body", "-b", "--body-file", + "-F", "--notes", "--notes-file", "--message", @@ -113,7 +114,8 @@ "--desc", } # `gh api`'s own value-taking flags, meaningful only inside an `api` invocation. -# `-f`/`-F` are the boolean `--fill` on `gh pr create`, so they must not be treated as value-consuming outside of `api`, or the flag right after them (a real `--repo /`) is silently skipped. +# `-f` alone is the boolean `--fill` on `gh pr create`, so it must not be treated as value-consuming outside of `api`, or the flag right after it (a real `--repo /`) is silently skipped. +# `-F` is value-taking either way (`--body-file` on create, `--field` on api), so it stays shared. _GH_API_VALUE_FLAGS = _GH_CREATE_TEXT_VALUE_FLAGS | { "-f", "-F", @@ -136,18 +138,12 @@ def _is_gh_write(cmd): """True when `cmd` is a GitHub write: a known-mutating `gh` subcommand, a `git push`, or a `gh api` - call whose effective method is not GET. - - The `gh api` half is argv-aware (via `_all_gh_arg_lists`/`_gh_api_path`/`_gh_effective_method`/ - `_gh_graphql_query`), reading a flag or a GraphQL query only from where it actually sits in one - invocation's own argv rather than a substring search over the whole command. A raw substring search - reads a write-method spelling out of an opaque flag value too, such as a `--jq` expression that - merely contains the text `-XPOST` as data, misclassifying a harmless read as a write. - - A GraphQL call whose body comes from `--input` is treated as a write whenever its query text cannot - be read at all (`_gh_graphql_query` returns None), since a `resolveReviewThread` mutation supplied - that way is equally invisible; `_check_reply_resolve_helper` denies that case explicitly. + call whose effective method is not GET. A GraphQL call is a write only when its query is a mutation, + or when its body is supplied by `--input` and so cannot be read at all. """ + # Argv-aware for the `gh api` half, reading a flag or a GraphQL query only from where it actually sits in one invocation's own argv, not a raw substring search over the whole command. + # A substring search reads a write-method spelling out of an opaque flag value too, such as a + # `--jq` expression that merely contains the text `-XPOST` as data, misclassifying a harmless read. if _GH_WRITE_SUB.search(cmd) or _push_arg_lists(cmd): return True for args in _all_gh_arg_lists(cmd): @@ -159,7 +155,7 @@ def _is_gh_write(cmd): # A -f/-F field becomes a URL query-string parameter rather than a body field whenever --input is also present. # A harmless-looking inline query alongside --input therefore has no effect on the actual request, and the real body is the uninspectable input file. if _gh_has_input(args): - return True # uninspectable body; treat cautiously so rules 1-5 can look closer + return True # uninspectable body, treated cautiously so rules 1-5 can look closer q = _gh_graphql_query(args) if q: if _MUTATION.search(q): @@ -510,7 +506,7 @@ def _gh_write_targets(cmd): """ targets = [] for args in _all_gh_arg_lists(cmd): - # `-f`/`-F` are value-taking only inside `api`, on `pr create` etc. they are the boolean `--fill`, so treating them as value-consuming there would swallow a real following `--repo` flag whole. + # `-f` alone is value-taking only inside `api`, on `pr create` it is the boolean `--fill`, so treating it as value-consuming there would swallow a real following `--repo` flag whole. flags = _GH_API_VALUE_FLAGS if args and args[0] == "api" else _GH_CREATE_TEXT_VALUE_FLAGS n = len(args) i = 0 @@ -1339,6 +1335,12 @@ def classify(cmd, cwd=None, origin=None, current_branch=None, rules_lookup=None, "allow", "-f as pr create's boolean --fill does not swallow --title either, with no foreign target present", ), + ( + "gh pr create -F repos/esphome/esphome --title x", + {}, + "allow", + "-F stays value-taking (--body-file) on pr create, so a body-file path is not misread as an API target (qodo)", + ), ] # Rule-4 cases, covering branch-rule bypass.