← Commit history

Upload 1 file(s)

John Lauer ·0d5f0c8f2d ·2mo ago ·parent d2c9969
1 file changed +143−11
aps.py+143−11
@@ -104,8 +104,10 @@ def _aps_auth001_recovery_hint(client_id: str) -> str:     return (         "⚠ APS AUTH-001 - the APS app (Client ID " + (client_id[:8] + "...") + ") LOST its Data Management "         "API product, so authorize fails BEFORE any login. This is fixable; here is the exact recovery "-        "(drive with ABE/nbrowser - if not installed, install adom-browser-extension first, it is the "-        "ONLY reliable way to drive the portal): "+        "(this one genuinely needs ABE/nbrowser: recovery CLICKS and EVALs inside the portal, which the "+        "extension-free nbrowser_open_os_window cannot do - it only opens a URL. If the extension is not "+        "installed, install adom-browser-extension first; ABE can do it automatically, roughly a 5-minute "+        "AI-driven process. Alternatively walk the USER through steps 1-3 by hand): "         "(1) nbrowser_open_window {sessionId, thread, purpose, profile:<the warm Edge/Chrome profile>, "         "url:'https://aps.autodesk.com/myapps/'} and open the app whose Client ID is this one. "         "(2) On the app's App-settings page, API Access section: confirm 'Data Management API' is checked, "@@ -227,6 +229,106 @@ _signin_lock = threading.Lock() _signin_state = None  # {"verifier","state","server","thread","done","error","startedAt"}  +# ── Extension-free browser opening (issue #12) ───────────────────────────────+# ABE gained OS-level, EXTENSION-FREE verbs (nbrowser_open_os_window / nbrowser_os_windows):+# they open a URL in a REAL browser PROFILE without the Adom extension being installed in that+# Chrome/Edge. That is exactly the OAuth-consent case, so APS sign-in no longer dead-ends on a+# machine with no extension (Caleb, issue #12). The extension is still WORTH having - with it we+# can drive/see/screenshot the window instead of asking the user to click - so we upsell it.+_ABE_UPSELL = ("OPTIONAL, offer it: installing the Adom browser extension lets me DRIVE this "+               "sign-in instead of asking the user to click (fill, read the result, screenshot, "+               "clean up). ABE can install it for them automatically - it is just slow, roughly a "+               "5-minute AI-driven process. See the 'installing-the-extension' flow. Everything "+               "here works WITHOUT it; the extension only buys automation.")+++def _ad(verb: str, args: dict, timeout: int = 60) -> dict:+    """Call an AD verb from aps.py. Lazy-imports server (which imports us) so there is no+    circular import at module load. Never raises."""+    try:+        import server as _s+        return _s._ad_call(verb, args, timeout=timeout) or {}+    except Exception as e:+        return {"_adCallError": str(e)}+++def _abe_state() -> dict:+    """Probe ABE readiness so the response can say WHY a path was chosen, and whether the+    (optional) extension is present. Degrades to {'known': False} on any failure."""+    r = _ad("nbrowser_readiness", {}, timeout=30)+    if not isinstance(r, dict) or r.get("_adCallError") or "ready" not in r:+        return {"known": False,+                "note": "could not probe ABE (verb missing or CLI too old); the extension-free "+                        "path does not need it"}+    return {"known": True,+            "ready": r.get("ready"),+            "extensionConnected": r.get("extensionConnected"),+            "profiles": r.get("profiles") or [],+            "chromeInstalled": r.get("chromeInstalled"),+            "extensionStale": r.get("extensionStale"),+            "staleProfiles": r.get("staleProfiles") or []}+++def _open_os_window_extension_free(url: str, profile: str = None) -> dict:+    """Open `url` in the user's REAL browser profile with NO extension required.++    Returns {launched: bool, where: str, profile: str, error: str}. `launched` is intentionally+    generous: nbrowser_open_os_window returns ok:false when it cannot resolve the new window's+    hwnd within ~8s, but the browser usually DID open (the tab often groups into an existing+    window). For a consent flow, "the page is in front of the user" is what matters, so a fired+    launch counts.+    """+    if not profile:+        # reuse the Autodesk profile the user already picked for Fusion's own sign-in+        try:+            import server as _s+            profile = _s._remembered_signin_profile()+        except Exception:+            profile = None+    a = {"sessionId": "aps-signin-%d" % int(time.time()),+         "thread": "fusion-aps-signin",+         "purpose": "Autodesk APS consent (extension-free)",+         "url": url, "background": True}+    if profile:+        a["profile"] = profile+    r = _ad("nbrowser_open_os_window", a, timeout=60)+    if not isinstance(r, dict) or r.get("_adCallError"):+        return {"launched": False, "profile": profile,+                "error": (r or {}).get("_adCallError") or "nbrowser_open_os_window unavailable"}+    launched = bool(r.get("ok") or r.get("hwnd") or "Launch fired" in str(r.get("_hint", "")))+    return {"launched": launched,+            "where": r.get("displayName") or r.get("profile") or profile,+            "profile": r.get("profile") or profile,+            "hwnd": r.get("hwnd"),+            "extensionInstalled": r.get("extensionInstalled"),+            "error": None if launched else (str(r.get("error") or r.get("_hint") or "")[:200])}+++def _signin_setup_chain(opened: bool, abe: dict) -> list:+    """A chain the agent can follow MECHANICALLY (Caleb's ask), instead of a bare command that+    may not exist on the box."""+    if opened:+        return [{"step": "user_approves",+                 "do": "Tell the user the exact profile it opened in and ask them to click Approve."},+                {"step": "poll", "verb": "fusion_aps_status",+                 "until": "signedIn:true"}]+    chain = [{"step": "open_consent", "verb": "nbrowser_open_os_window",+              "args": {"sessionId": "<task id>", "thread": "<your thread>",+                       "purpose": "Autodesk APS consent", "profile": "chrome:<their-autodesk-email>",+                       "url": "<authUrl from this response>"},+              "note": "EXTENSION-FREE - works with no Adom extension installed. If this verb is "+                      "unknown, the adom-desktop CLI on this box is too old: update it (or have "+                      "the user paste authUrl into the right profile by hand)."},+             {"step": "user_approves",+              "do": "Ask the user to approve in that profile; without the extension we cannot click for them."},+             {"step": "poll", "verb": "fusion_aps_status", "until": "signedIn:true"}]+    if abe.get("known") and not abe.get("extensionConnected"):+        chain.append({"step": "optional_extension", "flow": "installing-the-extension",+                      "why": "lets the assistant DRIVE the sign-in instead of asking for clicks",+                      "cost": "~5 minutes, ABE automates it"})+    return chain++ # ── Config + token storage ─────────────────────────────────────────────────── def _read_json(path: Path) -> dict:     try:@@ -533,6 +635,8 @@ def start_signin(args: dict | None = None) -> dict:     pref = passed or load_signin_browser()     opened_via = None     opened_pref = None+    ext_free = None      # result of the EXTENSION-FREE open, if we take that path+    abe = _abe_state()   # probed once, reported either way (issue #12)     if pref and _launch_url_in_browser(pref, auth_url):         opened_via = "remembered_browser" if not passed else "specified_browser"         opened_pref = {k: pref.get(k) for k in ("name", "exe", "profileDir")}@@ -551,7 +655,17 @@ def start_signin(args: dict | None = None) -> dict:             except Exception:                 opened_via = None     else:-        opened_via = "none_use_extension"+        # EXTENSION-FREE open (issue #12, Caleb): we used to stop here and hand back a hint+        # telling the AI to run nbrowser_open_window - which REQUIRES the ABE extension. On a+        # box without it (or on a CLI too old to know the verb) that hint was un-followable and+        # the whole sign-in dead-ended. ABE now exposes nbrowser_open_os_window, which opens a+        # URL in a REAL browser PROFILE with NO extension installed - exactly the OAuth-consent+        # case. Try that before giving up.+        ext_free = _open_os_window_extension_free(auth_url, args.get("profile"))+        if ext_free.get("launched"):+            opened_via = "os_window_extension_free"+        else:+            opened_via = "none_use_extension"      if opened_via in ("remembered_browser", "specified_browser"):         _hint = (f"Opened the Autodesk consent in the REMEMBERED browser ({opened_pref.get('name') or 'cached'}"@@ -562,25 +676,43 @@ def start_signin(args: dict | None = None) -> dict:         _hint = ("Opened in the OS DEFAULT browser (fallback). On a multi-browser machine this may be the "                  "wrong profile; prefer driving authUrl in the user's real browser via nbrowser_open_window "                  "(new window), then call fusion_aps_set_browser to remember it. Poll fusion_aps_status.")+    elif opened_via == "os_window_extension_free":+        _hint = ("Opened the Autodesk consent in %s using the EXTENSION-FREE path "+                 "(nbrowser_open_os_window) - no Adom browser extension required. TELL THE USER which "+                 "profile it opened in (never just 'your browser') and that THEY need to click Approve, "+                 "because without the extension I cannot see or drive that window. A listener on port "+                 "%s captures the token once they approve. Poll fusion_aps_status until signedIn:true. "+                 "%s" % (ext_free.get("where") or "the user's browser profile", bound_port,+                         _ABE_UPSELL))     else:-        _hint = ("NO browser opened on purpose. This machine has no REMEMBERED Autodesk browser yet, and a "-                 "multi-browser box's OS default is usually the wrong one. OPEN authUrl in the user's REAL "-                 "browser via a NEW window (adom-desktop nbrowser_open_window {url:authUrl}) so its warm "-                 "Autodesk/SSO session is used, then call fusion_aps_set_browser {name,exe,profileDir} to "-                 "REMEMBER it for next time. A listener on port "-                 f"{bound_port} captures the token once you approve. Poll fusion_aps_status until signedIn:true.")-+        _hint = ("Could not open a browser for you. No REMEMBERED Autodesk browser, and the EXTENSION-FREE "+                 "open (nbrowser_open_os_window) did not fire%s. DO THIS: run the setupChain in this "+                 "response in order. Simplest manual fallback - have the USER paste authUrl into the "+                 "browser profile their Autodesk account lives in. A listener on port "+                 "%s captures the token once they approve; poll fusion_aps_status until signedIn:true. %s"+                 % ((": " + str(ext_free.get("error"))[:120]) if ext_free and ext_free.get("error") else "",+                    bound_port, _ABE_UPSELL))++    ok_open = bool(opened_via and opened_via != "none_use_extension")     return {         "success": True,         "output": ("Autodesk sign-in started. " + (             "Approve access in the opened browser, then call fusion_aps_status."-            if opened_via and opened_via != "none_use_extension"+            if ok_open             else "Open authUrl in the user's real browser (new window) to approve.")),         "authUrl": auth_url,         "redirectUri": _active_redirect_uri,         "openedVia": opened_via,         "openedBrowser": opened_pref,+        # Extension-free open no longer NEEDS the extension - keep the old flag truthful for+        # existing callers, but only assert it when we genuinely could not open anything.         "needsExtensionOpen": opened_via == "none_use_extension",+        "extensionFree": ext_free,+        # Caleb's asks (issue #12): a structured code + a chain the agent can follow mechanically,+        # instead of a raw command that may not exist on this box.+        "errorCode": None if ok_open else "browser_open_failed",+        "abe": abe,+        "setupChain": _signin_setup_chain(ok_open, abe),         "_hint": _hint,     }