From 6c76e88302f9126d76f68d36af57030726bef392 Mon Sep 17 00:00:00 2001 From: voidReq <100312344+voidReq@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:33:59 -0400 Subject: [PATCH] NUworks rows' assistant browser starts signed in (NUauto's NUworks cookies); a session you answered leaves the Assistants list at once Co-Authored-By: Claude Opus 5.5 --- docs/ASSIST.md | 8 +++-- docs/REMOTE.md | 4 ++- src/nuauto/assist.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ src/nuauto/gui.py | 5 +++- tests/test_assist.py | 16 ++++++++++ tests/test_remote.py | 9 ++++++ 6 files changed, 108 insertions(+), 4 deletions(-) diff --git a/docs/ASSIST.md b/docs/ASSIST.md index f4b1e7d..17809d5 100644 --- a/docs/ASSIST.md +++ b/docs/ASSIST.md @@ -11,8 +11,12 @@ COMPANY-SITE AGENT (assist.py + prompts/ASSIST_PROMPT.md, `nuauto assist `; assist.by_hand_target, the row's own NUworks job page (never another link, no --url), never one whose Submit click is unresolved or past its deadline. Such a "by-hand" session is told what stopped NUauto and the exact resume label (assist.hand_note; the rules' NUWORKS ROWS): it clicks the job's Apply, fills the popup, uploads the transcript or the - cover letter, and its Submit asks me like any other. NUworks' sign-in: the one click on "Current Students And - Alumni", else it asks me to sign in in its browser (never typed by it: accounts.never). One application at a time on + cover letter, and its Submit asks me like any other. NUworks' sign-in: its browser starts signed in (2026-10-23, my + call: "i don't wanna sign in each time": before the agent starts, assist.seed_nuworks copies NUauto's own NUworks + sign-in from local/session_cookies.json into that slot's profile, only the cookies a browser sends to NUworks and its + two sign-in hosts (assist.nuworks_cookies; tests/test_assist.py), through the browser tool's own package so the same + browser build opens the profile); if that is stale, the one click on "Current Students And Alumni", else it asks me to + sign in in its browser (never typed by it: accounts.never). One application at a time on NUworks: it holds NUworks' site lock (one session per site: one by-hand session per machine), takes the NUworks turn (remote.nuworks_turn, so not beside the other machine's Apply), won't start while an Apply run holds the NUworks browser, and `nuauto apply` / the NUworks side refuse while it runs (assist.nuworks_session). The weekly cap is diff --git a/docs/REMOTE.md b/docs/REMOTE.md index df430ce..b3eb228 100644 --- a/docs/REMOTE.md +++ b/docs/REMOTE.md @@ -158,7 +158,9 @@ AN ASSISTANT'S SCREEN (#/assistant/ in the homelab's app; review.view / ac the session started in, and never when that pane shows a shell (the assistant ended). End session (/exit) and End: do it on the laptop. Ended Did it go through? Yes / No / No: do it on the laptop (typed into the launcher's question). One tap: - back to Apply > Assistants, and the screen says your answer until the run closes. + back to Apply > Assistants, and the screen says your answer until the run closes. A session you + answered is no longer listed or counted there (gui.assistants), though it holds its slot a few + seconds more while it writes the sheet and cleans up (2026-10-23, my request). - Each answer goes in the run's actions.log with the paired device's name. - The agent on the homelab never opens your own network: localhost, the LAN, Tailscale addresses (so not this page, not the browser view) are refused in its browser and WebFetch (assist.own_network). diff --git a/src/nuauto/assist.py b/src/nuauto/assist.py index 4ab2da2..0ea3e9b 100644 --- a/src/nuauto/assist.py +++ b/src/nuauto/assist.py @@ -1614,6 +1614,74 @@ def hand_note(row): f"Resume to pick in the popup's Resume list (exactly this text): {label or 'not set: ask the user'}\n") +# ---------- a NUworks row's browser starts signed in (my call, 2026-10-23: "i don't wanna sign in each time") ---------- + +SEED_HOURS = 12 # a session cookie copied in lasts this long in the slot's profile (a browser drops session cookies on close) +# Adds the cookies (JSON on stdin) to the slot's profile with the browser tool's own package (the same browser build, so +# the profile is never opened by another version), then closes it. Run through npx, like the browser tool. +SEED_JS = """import { createRequire } from "module"; +import fs from "fs"; +import path from "path"; +const [profile, name] = process.argv.slice(2); +const bin = process.env.PATH.split(path.delimiter).find((p) => fs.existsSync(path.join(p, "playwright-mcp"))); +if (!bin) { console.error("the browser tool's package was not found"); process.exit(2); } +const pw = createRequire(fs.realpathSync(path.join(bin, "playwright-mcp")))("playwright-core"); +const cookies = JSON.parse(fs.readFileSync(0, "utf8")); +const firefox = name === "firefox"; +const ctx = await (firefox ? pw.firefox : pw.chromium).launchPersistentContext(profile, + { headless: true, ...(firefox || name === "chromium" ? {} : { channel: name || "chrome" }) }); +try { await ctx.addCookies(cookies); } finally { await ctx.close(); } +""" +COOKIE_KEYS = ("name", "value", "domain", "path", "expires", "httpOnly", "secure", "sameSite") + + +def nuworks_cookies(saved, now): + """Of NUauto's own browser's saved cookies (local/session_cookies.json), the ones a browser sends to NUworks and its + two sign-in hosts (config.ALLOWED_HOSTS, SSO_HOSTS), nothing else; expired ones left out, session ones given + SEED_HOURS (so they outlive the profile's next close).""" + hosts = config.ALLOWED_HOSTS | config.SSO_HOSTS + out = [] + for c in saved if isinstance(saved, list) else []: + d = str(c.get("domain") or "").lstrip(".").lower() if isinstance(c, dict) else "" + if not d or not any(h == d or h.endswith("." + d) for h in hosts): + continue + c = {k: c[k] for k in COOKIE_KEYS if k in c} + exp = c.get("expires") + if not isinstance(exp, (int, float)) or exp <= 0: + c["expires"] = int(now + SEED_HOURS * 3600) + elif exp < now: + continue + if c.get("sameSite") == "None" and not c.get("secure"): # (Chrome refuses that pair) + c["sameSite"] = "Lax" + out.append(c) + return out + + +def seed_nuworks(profile, d, log_): + """(a NUworks row's session) This slot's browser gets NUauto's NUworks sign-in before it starts. Never fails the + session: without it, the one click on "Current Students And Alumni", else it asks you (as before).""" + import time + try: + with open(config.COOKIES_PATH) as f: + cookies = nuworks_cookies(json.load(f), time.time()) + except (OSError, ValueError): + cookies = [] + if not cookies: + return log_.write("No saved NUworks sign-in to give this browser: it signs in with the one click, or asks you.") + js = os.path.join(d, "seed_cookies.mjs") + with open(js, "w") as f: + f.write(SEED_JS) + name = config.ASSIST_BROWSER or "chrome" # (the browser tool's default) + cmd = ["npx", "-y", "-p", MCP_PACKAGE, "-c", " ".join(shlex.quote(a) for a in ("node", js, profile, name))] + try: + r = subprocess.run(cmd, input=json.dumps(cookies), capture_output=True, text=True, timeout=120) + ok = r.returncode == 0 + except (OSError, subprocess.TimeoutExpired): + ok = False + log_.write("This browser starts signed in to NUworks (NUauto's own sign-in)." if ok else + "Could not give this browser NUauto's NUworks sign-in: it signs in with the one click, or asks you.") + + OTHER_NOTE = ("This job is not on NUworks (the sheet's Other jobs tab): nothing is submitted on NUworks afterwards. " "Its answer bank leaves out the NUworks co-op answers (start / end dates, co-op term): when the form " "asks one, guess it from the posting (record the guess) like any other.\n") @@ -1884,6 +1952,8 @@ def run_locked(number, other, row, url, where, tab, slot, slot_lock, hand=False) json.dump(state, f) profile = slot_profile(slot) os.makedirs(profile, mode=0o700, exist_ok=True) + if hand and not config.DEMO: + seed_nuworks(profile, d, log_) cmd = session(d, answer_cmd, read_roots, profile, rules_text(answer_cmd) + context_text( where, row, url, other, resume, read_roots, answer_cmd, writeups, view.url if view else None, transcript, hand), where, view.display if view else None) diff --git a/src/nuauto/gui.py b/src/nuauto/gui.py index 2e40795..80d151c 100644 --- a/src/nuauto/gui.py +++ b/src/nuauto/gui.py @@ -1397,7 +1397,10 @@ def assistants(): homelab: the homelab's own sessions (remote_assist), so a row it works on offers no Start here.""" from nuauto import assist from nuauto import link - return {"sessions": assist.sessions(), "slots": assist.slots(), "homelab": homelab_assistants(), + # one whose "Did it go through?" you answered is closing (its sheet write, its cleanup): not listed, though it still + # holds its slot a few seconds + live = [s for s in assist.sessions() if (s.get("attention") or {}).get("kind") != "answered"] + return {"sessions": live, "slots": assist.slots(), "homelab": homelab_assistants(), "homelab_review": homelab_review(), "linked": not REMOTE and link.linked()} # linked: Start on the homelab diff --git a/tests/test_assist.py b/tests/test_assist.py index 75d85fd..cca029b 100644 --- a/tests/test_assist.py +++ b/tests/test_assist.py @@ -1720,4 +1720,20 @@ def status_of(n): finally: accounts.PATH = pw_path +# a NUworks row's browser starts signed in: only the cookies a browser sends to NUworks and its two sign-in hosts +now_ = 1_800_000_000 +saved_ = [{"name": "nu", "value": "a", "domain": "northeastern-csm.symplicity.com", "path": "/", "expires": -1, "httpOnly": True, + "secure": True, "sameSite": "None", "partitionKey": "x"}, + {"name": "sso", "value": "b", "domain": ".neu.edu", "path": "/", "expires": now_ + 99, "secure": False, "sameSite": "None"}, + {"name": "shib", "value": "c", "domain": ".symplicity.com", "path": "/", "expires": now_ + 5, "sameSite": "Lax"}, + {"name": "old", "value": "d", "domain": "neuidmsso.neu.edu", "path": "/", "expires": now_ - 1}, + {"name": "ms", "value": "e", "domain": "login.microsoftonline.com", "path": "/", "expires": -1}, + {"name": "other", "value": "f", "domain": "csm.symplicity.com.evil.example", "path": "/", "expires": -1}, + {"name": "mail", "value": "g", "domain": "outlook.neu.edu.example", "path": "/", "expires": -1}, "junk"] +got_ = {c["name"]: c for c in assist.nuworks_cookies(saved_, now_)} +assert sorted(got_) == ["nu", "shib", "sso"], got_ # nothing else, nothing expired +assert got_["nu"]["expires"] == now_ + assist.SEED_HOURS * 3600 and "partitionKey" not in got_["nu"] # a session one lasts +assert got_["nu"]["sameSite"] == "None" and got_["sso"]["sameSite"] == "Lax" # (None without secure: Lax) +assert assist.nuworks_cookies({"not": "a list"}, now_) == [] + print("All assist (agent guard) checks passed.") diff --git a/tests/test_remote.py b/tests/test_remote.py index 4040ad5..322b250 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1126,6 +1126,15 @@ def boom(): gui.VIEW, config.DEMO, config.REMOTE_ASSIST, config.IS_SERVER, config.REMOTE_APP, config.HAS_SERVER = real_flags threading.excepthook = threading.__excepthook__ +# ---- a session whose "Did it go through?" you answered is closing: the lists leave it out (it still holds its slot) +real_assist_sessions = assist.sessions +try: + assist.sessions = lambda: [{"slot": 1, "tab": "jobs", "row": 4, "attention": {"kind": "answered"}}, + {"slot": 2, "tab": "jobs", "row": 5, "attention": {"kind": "approve"}}, {"slot": 3, "tab": "other", "row": 6}] + assert [s["slot"] for s in gui.assistants()["sessions"]] == [2, 3] +finally: + assist.sessions = real_assist_sessions + # ---- the GUI's Homelab assistants card: the review page's address, Pair this computer, the browser tool's screenshots real_flags = (gui.VIEW, config.DEMO, config.REMOTE_ASSIST, config.IS_SERVER) real_local = config.LOCAL