diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..494fce3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,35 @@ +name: "Something did not work" +description: "An agent tried something on your Mac and it failed, or it took your screen when it should not have." +title: "Bug: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Nothing is sent from your Mac on its own. Paste only what you are happy to share: + the audit log hashes typed text and never stores what was on screen, but app names + and window titles are in it. + - type: textarea + id: what + attributes: + label: What the agent tried, and what actually happened + validations: + required: true + - type: textarea + id: audit + attributes: + label: The last few lines of the audit log + description: "Ask the agent for `computer_audit` with limit 10, or copy from ~/.local/state/computer-mcp/audit.jsonl." + render: json + - type: textarea + id: learning + attributes: + label: Anything it wrote with computer_learning + description: "~/.local/state/computer-mcp/learnings.jsonl - numbers and email addresses are already masked." + - type: input + id: versions + attributes: + label: macOS version and computer-mcp version + placeholder: "macOS 26.4.1 · computer-mcp 0.2.1" + validations: + required: true diff --git a/.github/mutant-giv-tilbage.sh b/.github/mutant-giv-tilbage.sh index a181830..aca1dec 100644 --- a/.github/mutant-giv-tilbage.sh +++ b/.github/mutant-giv-tilbage.sh @@ -5,28 +5,69 @@ set -u fejl=0 mut() { - local navn="$1" fra="$2" til="$3" d="$RUNNER_TEMP/mut-$1" + local navn="$1" fra="$2" til="$3" d="$RUNNER_TEMP/mut-$1" maal="${MUT_MAAL:-cmcp-helper}" # Uden byggemappen: den baerer faste stier til originalen (koersel 3 kunne ikke bygge). mkdir -p "$d" && cp -R helper/Package.swift helper/Sources "$d"/ [ -f helper/Package.resolved ] && cp helper/Package.resolved "$d"/ - python3 - "$d/Sources/cmcp-helper/${MUT_FIL:-Skaerm.swift}" "$fra" "$til" <<'PY' + # ⛔ 30/9 (Astra R2 7): et anker der ikke fandtes, stoppede ikke noget - scriptet + # byggede den UMUTEREDE kode og kaldte resultatet et bevis. Nu er det en fejl. + if ! python3 - "$d/Sources/$maal/${MUT_FIL:-Skaerm.swift}" "$fra" "$til" <<'PY2' import sys p, a, b = sys.argv[1:4] s = open(p).read() assert s.count(a) == 1, a open(p, 'w').write(s.replace(a, b)) -PY - (cd "$d" && swift build -c release 2>&1 | tail -5) ; [ -x "$d/.build/release/cmcp-helper" ] || { echo "::error::$navn kunne ikke bygges"; fejl=1; return; } - if CMCP_HELPER="$d/.build/release/cmcp-helper" node test/giv-tilbage.mjs; then - echo "::error::mutanten $navn overlevede - proeven maaler ikke"; fejl=1 - else - echo "mutanten $navn er roed" - fi +PY2 + then echo "::error::$navn: ankeret findes ikke praecis én gang"; fejl=1; return; fi + (cd "$d" && swift build -c release 2>&1 | tail -5) ; [ -x "$d/.build/release/$maal" ] || { echo "::error::$navn kunne ikke bygges"; fejl=1; return; } + local ud rc + ud=$(CMCP_HELPER="$d/.build/release/cmcp-helper" CMCP_STATUS_BIN="$d/.build/release/cmcp-status" node "${PROEVE:-test/giv-tilbage.mjs}" 2>&1); rc=$? + # Roed = exit 1 OG en DUMP-linje. Exit 0 = overlevede. Alt andet = instrumentet svarede ikke. + if [ $rc -eq 0 ]; then echo "::error::mutanten $navn overlevede - proeven maaler ikke"; fejl=1 + # Runde 3 (Astra 6): KUN den prove mutanten er skrevet til taeller (FORVENTET), ikke en vilkaarlig anden. + # Runde 5 (Astra): hele det forventede praefiks skal staa FORREST paa en DUMP-linje. + elif [ $rc -eq 1 ] && printf '%s\n' "$ud" | awk -v p="DUMP ${FORVENTET:-}" 'index($0, p) == 1 { f = 1 } END { exit !f }'; then echo "mutanten $navn er roed ($(printf '%s\n' "$ud" | awk -v p="DUMP ${FORVENTET:-}" 'index($0, p) == 1 { print; exit }' | cut -c1-90))" + else echo "::error::$navn: instrumentet svarede ikke (rc=$rc) - det er ikke et bevis"; fejl=1; fi } -mut M1-giver-ikke-tilbage 'NSRunningApplication(processIdentifier: foer.forrestPid)?.activate(options: [])' '' -mut M2-maaler-ikke 'guard foer.forrestPid > 0, efter.forrestPid != foer.forrestPid else { return ["took_screen": false] }' 'return ["took_screen": false]' +FORVENTET=2b mut M1-giver-ikke-tilbage 'NSRunningApplication(processIdentifier: foer.forrestPid)?.activate(options: [])' '' +FORVENTET='2 et tryk' mut M2-maaler-ikke 'guard foer.forrestPid > 0, efter.forrestPid != foer.forrestPid else { return ["took_screen": false] }' 'return ["took_screen": false]' # M3: launch giver ikke forgrunden tilbage (main.swift) -MUT_FIL=main.swift mut M3-launch-giver-ikke-tilbage 'if (g["took_screen"] as? Bool) == true { for (k, v) in g { ls[k] = v } }' 'if false { for (k, v) in g { ls[k] = v } }' -# M4: kun et skift til maalprogrammet gives tilbage (reglen foer koersel 10) -mut M4-kun-maalprogrammet 'efter.forrestPid == maal || !menneskeRoerteNetop()' 'efter.forrestPid == maal' +FORVENTET='3 et program' MUT_FIL=main.swift mut M3-launch-giver-ikke-tilbage 'if (g["took_screen"] as? Bool) == true || g["observed"] != nil { for (k, v) in g { ls[k] = v } }' 'if false { for (k, v) in g { ls[k] = v } }' +# M4 (28/9): menneske-tjekket maa ikke spurioest blokere give-tilbage. Paa en +# maskine UDEN menneske skal forgrunden stadig gives tilbage - saa hvis nogen +# faar menneske-grenen til altid at fyre (og dermed altid lade forgrunden staa), +# SKAL proeven blive roed. +FORVENTET=2b mut M4-menneske-tjek-blokerer 'if menneskeRoerteNetop() {' 'if true {' +# M5-M6 (29/9, panelet): kodeordsfelter faar heller ikke tastetryk fra `type`. +# M5: fokus-opslaget siger altid «ikke sikkert» -> proeve 3/3k/3b skal blive roed. +# M6: tjekket kun foer foerste tegn -> fokus der flytter undervejs (3c) skal blive roed. +FORVENTET='3 et kodeordsfelt' PROEVE=test/skriv-ankommer.mjs MUT_FIL=Accessibility.swift mut M5-type-i-kodeordsfelt \ + 'static func sikkerStatus(_ el: AXUIElement, kendtRolle: String? = nil) -> Bool? {' \ + 'static func sikkerStatus(_ el: AXUIElement, kendtRolle: String? = nil) -> Bool? { + if true { return false }' +FORVENTET=3c PROEVE=test/skriv-ankommer.mjs MUT_FIL=Input.swift mut M6-kun-tjek-ved-start \ + 'if let stop, stop() { return sendt }' 'if let stop, sendt == 0, stop() { return sendt }' +# M7-M8 (30/9, runde 5): paste stopper foer Cmd+V naar laanet slutter. +# M7: det foerste stop (foer udklipsholderen roeres) er vaek -> «before anything changed» maa ikke kunne naas. +FORVENTET='1 SIGUSR1' PROEVE=test/paste-stop.mjs MUT_FIL=Accessibility.swift mut M7-paste-roerer-udklip-foer-stop \ + ' if pasteStop { return (false, "stopped before anything changed: the screen loan ended", false) }' '' +# M8: stoppet laeses aldrig -> paste fortsaetter til Cmd+V (kun paa en fremmed maskine). +FORVENTET='1 SIGUSR1' PROEVE=test/paste-stop.mjs MUT_FIL=Accessibility.swift mut M8-paste-hoerer-ikke-stop \ + ' return sigismember(&s, SIGUSR1) == 1' ' return false' +# M9 (30/9, live-proeven): boksen naevner ikke det ventende spoergsmaal -> 3c skal blive roed. +FORVENTET=3c PROEVE=test/ikon-menu.mjs MUT_MAAL=cmcp-status MUT_FIL=Tekst.swift mut M9-boksen-tier-om-spoergsmaalet \ + '[antal == 1 ? "Needs you: click the orange menu bar icon"' '[antal == 1 ? "Computer MCP"' +# M10 (1/10, konsulent-panelet): giv-tilbage river igen ethvert program tilbage - +# ogsaa et der koerte i forvejen (= mennesket der skiftede skrivebord) -> 4 skal blive roed. +FORVENTET='4 et program der koerte' mut M10-river-menneskets-skift-tilbage \ + 'if !egen {' 'if false {' +# M11 (1/10, GitHubs Mac 36863990504 - retarget 36901238860): almindelig skrivning +# taeller igen som et menneskeligt skift -> et rigtigt tastetryk ville fejlagtigt +# blokere en legitim tilbagegivelse. Oprindeligt bevist via parallel.mjs's +# menneske-simulation, som siden skriver via tilgaengeligheds-laget (ingen rigtig +# keyDown postes der laengere - den mutant blev umaalelig af parallel.mjs selv). +# giv-tilbage.mjs's sag 5 sender et AEGTE globalt tastetryk med vilje og maaler +# praecis dette. +FORVENTET='5 et rigtigt tastetryk' mut M11-skrivning-er-et-skift \ + '[.flagsChanged, .leftMouseDown' '[.keyDown, .flagsChanged, .leftMouseDown' exit $fejl diff --git a/.github/mutant-porte.py b/.github/mutant-porte.py new file mode 100644 index 0000000..d1f6927 --- /dev/null +++ b/.github/mutant-porte.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Mutationsbevis for portene (29/9, B7 - MANDAT: «hver vagt skal kunne blive roed»). +# Hver mutant i mutanter-porte.json bygger én fejl ind i en vagt; dens proeve SKAL +# blive roed. Filen gendannes ALTID fra en kopi taget lige foer - aldrig fra git, +# for filen kan baere ucommittet arbejde (30/9: `git checkout` tog en hel funktion). +import hashlib, json, os, subprocess, sys +ROD = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +os.chdir(ROD) +overlevede, fejl = [], [] +for m in json.load(open('.github/mutanter-porte.json', encoding='utf-8')): + orig = open(m['fil'], encoding='utf-8').read() + h = hashlib.sha256(orig.encode()).hexdigest() + par = m.get('par') or [[m['fra'], m['til']]] + tael = [orig.count(a) for a, _ in par] + if any(t != 1 for t in tael): + fejl.append(f"{m['navn']}: ankrene findes {tael} gange"); continue + muteret = orig + for a, b in par: muteret = muteret.replace(a, b) + try: + open(m['fil'], 'w', encoding='utf-8').write(muteret) + try: + k = subprocess.run(['node', m['proeve']], capture_output=True, text=True, timeout=600) + rc, ud = k.returncode, k.stdout + except subprocess.TimeoutExpired: + rc, ud = 'timeout', '' + finally: + open(m['fil'], 'w', encoding='utf-8').write(orig) + if hashlib.sha256(open(m['fil'], encoding='utf-8').read().encode()).hexdigest() != h: + fejl.append(f"{m['navn']}: {m['fil']} blev IKKE gendannet"); break + # ⛔ Runde 1 30/9 (Astra 9): enhver ikke-nul exit talte som «roed» - ogsaa en + # prove der crashede for foerste assertion. Roed = rc 1 OG en DUMP-linje. + # ⛔ Runde 2 (Astra 7): en tilfaeldig ANDEN DUMP-linje var ogsaa «bevis». Nu skal + # den prove mutanten er skrevet til (`forventet`), vaere en af de roede. + doemt = [l for l in ud.splitlines() if l.startswith('DUMP ' + m.get('forventet', ''))] + if rc == 0: + overlevede.append(m['navn']); print(f"::error::mutanten {m['navn']} overlevede - {m['proeve']} maaler ikke") + elif rc == 1 and doemt: + print(f"mutanten {m['navn']} er roed ({m['proeve']}: {doemt[0][:90]})") + else: + fejl.append(f"{m['navn']}: instrumentet svarede ikke (rc={rc}, {len(doemt)} DUMP-linjer) - det er ikke et bevis") +for f in fejl: print(f"::error::{f}") +print(f"{len(overlevede)} overlevede, {len(fejl)} fejl") +sys.exit(1 if overlevede or fejl else 0) diff --git a/.github/mutanter-porte.json b/.github/mutanter-porte.json new file mode 100644 index 0000000..bdfd46e --- /dev/null +++ b/.github/mutanter-porte.json @@ -0,0 +1,890 @@ +[ + { + "navn": "S2-tekst-klippet", + "fil": "mcp-server/godkend.js", + "par": [ + [ + "text: helTekst, scope: kort(scope)", + "text: helTekst.slice(0, 200), scope: kort(scope)" + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "A14" + }, + { + "navn": "S2-intet-loft", + "fil": "mcp-server/godkend.js", + "par": [ + [ + " if (n > TEKST_LOFT) return `the action is", + " if (false) return `the action is" + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "A15" + }, + { + "navn": "S3-venter-haenger", + "fil": "mcp-server/godkend.js", + "par": [ + [ + " venter = false;\n clearTimeout(ur);", + " clearTimeout(ur);" + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "AG" + }, + { + "navn": "S3-buffer-overskrevet", + "fil": "mcp-server/godkend.js", + "par": [ + [ + " buf += d;\n const i = buf.indexOf('\\n');\n if (i < 0) return;\n let m = null;", + " buf = String(d);\n const i = buf.indexOf('\\n');\n if (i < 0) return;\n let m = null;" + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "F3" + }, + { + "navn": "S3-lukket-er-ja", + "fil": "mcp-server/godkend.js", + "par": [ + [ + "sock.on('close', () => slut({ ok: false,", + "sock.on('close', () => slut({ ok: true," + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "F1" + }, + { + "navn": "B3-done-overalt", + "fil": "mcp-server/godkend.js", + "par": [ + [ + "m.verified === 'done' && kind === 'goer-selv'", + "m.verified === 'done'" + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "E3" + }, + { + "navn": "B3-modellen-skriver-hvor", + "fil": "mcp-server/index.js", + "par": [ + [ + " target: hvorIkon, targetBundle: bid", + " target: String(args.message).slice(0, 80), targetBundle: bid" + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "E1" + }, + { + "navn": "R1-boks-efter-nej", + "fil": "mcp-server/policy.js", + "par": [ + [ + " if (!svar.ikkeKoerer) {\n if (svar.ok && nytSessionsProgram", + " if (!svar.ikkeSpurgt) {\n if (svar.ok && nytSessionsProgram" + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "G1" + }, + { + "navn": "B4-spoerger-ikke", + "fil": "mcp-server/index.js", + "par": [ + [ + " || !!sende,", + " || false," + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "a send-knappen" + }, + { + "navn": "B4-return-er-ikke-send", + "fil": "mcp-server/policy.js", + "par": [ + [ + " if (tast === 'return' || tast === 'enter') return slags === 'chat' || mods.has('cmd') || mods.has('ctrl');", + "" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "b «return»" + }, + { + "navn": "B4-linjeskift-slipper", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (name === 'computer_type' && slags === 'chat' && /[\\p{Cc}\\p{Zl}\\p{Zp}]/u.test(String(args.text ?? '').replace(/\\t/g, ''))) {", + " if (false) {" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "c linjeskift" + }, + { + "navn": "B4-modellen-skriver-modtager", + "fil": "mcp-server/index.js", + "par": [ + [ + " const hvem = [...(sam.recipient ? [sam.recipient] : (sam.headings || []))]", + " const hvem = [String(args.contains || ''), ...(sam.recipient ? [sam.recipient] : (sam.headings || []))]" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "d en falsk modtager" + }, + { + "navn": "R4-genkontrol-kun-ved-send", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (beskedSlags(targetBundleId)) {\n const igen", + " if (sende?.describe) {\n const igen" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r4" + }, + { + "navn": "R5-svagt-fingeraftryk", + "fil": "mcp-server/index.js", + "par": [ + [ + " const fp = JSON.stringify({ w: sam.window || '', h: sam.headings || [], r: sam.recipient || '', v: felt.value, c: !!sam.column });", + " const fp = JSON.stringify({ v: felt.value });" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r5b" + }, + { + "navn": "R5-ulaeseligt-spoerges", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (!hvem.length || !tekst) {", + " if (false) {" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "u ulaeselig" + }, + { + "navn": "R6-navnloes-er-ikke-send", + "fil": "mcp-server/index.js", + "par": [ + [ + " : (!rolle || /^AX(Button|Image|Group|Unknown|Link)$/.test(String(rolle)));", + " : false;" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r6 en navnloes" + }, + { + "navn": "R6-danske-ord", + "fil": "mcp-server/policy.js", + "par": [ + [ + "(send|resend|sende|afsend|indsend|reply|svar|besvar|", + "(send|resend|sende|indsend|reply|svar|" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r6 «Afsend»" + }, + { + "navn": "R6-webchat-overset", + "fil": "mcp-server/index.js", + "par": [ + [ + " slags = titler.some(x => !x || WEBCHAT.test(x)) ? 'chat' : titler.some(x => WEBMAIL.test(x)) ? 'mail' : null;", + " slags = titler.some(x => !x) ? 'chat' : null;" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r6 Return i WhatsApp Web" + }, + { + "navn": "R7-mail-return-er-send", + "fil": "mcp-server/policy.js", + "par": [ + [ + " if (tast === 'return' || tast === 'enter') return slags === 'chat' || mods.has('cmd') || mods.has('ctrl');", + " if (tast === 'return' || tast === 'enter') return true;" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r7 Return i mail" + }, + { + "navn": "B2-laan-virker-ikke", + "fil": "mcp-server/policy.js", + "par": [ + [ + " if (v === '') return !laanAktivt();", + " if (v === '') return true;" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "3 ...og move tilbydes nu" + }, + { + "navn": "B2-tilbagetagning-ignoreret", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (nr !== laanNr) return;\n saetLaan(0); laanHaandtag = null;", + " if (nr !== laanNr) return;\n laanHaandtag = null;" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "6 skaermen taget tilbage" + }, + { + "navn": "B2-loft-ignoreret", + "fil": "mcp-server/policy.js", + "par": [ + [ + " return v !== '' && !['0', 'false', 'no', 'off', 'nej', 'fra'].includes(v);\n}", + " return false;\n}" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "11" + }, + { + "navn": "B2-ikonets-ur-ignoreret", + "fil": "mcp-server/godkend.js", + "par": [ + [ + " const til = Math.min(vores, Number(m.until) > 0 ? Number(m.until) : vores);", + " const til = vores;" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "7 laanet udloeb" + }, + { + "navn": "R8-kun-skaermkald", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (laanAktivt()) {\n let m = null;", + " if (laanAktivt() && tagerSkaermen(name, args)) {\n let m = null;" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "5c" + }, + { + "navn": "R8-egen-input", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (idle < Math.min(graense, sidenEgen)) {", + " if (idle < graense) {" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "5d" + }, + { + "navn": "B5-menneske-ignoreret", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (idle < Math.min(graense, sidenEgen)) {", + " if (false) {" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "5 mennesket bruger" + }, + { + "navn": "R9-laan-loft", + "fil": "mcp-server/godkend.js", + "par": [ + [ + " if ([...helTekst].length > TEKST_LOFT) return { ok: false, grund: `the reason is", + " if (false) return { ok: false, grund: `the reason is" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "9b" + }, + { + "navn": "R13-oprindelig-fejl", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (nr !== laanNr) return;", + " if (!laanHaandtag && !laanAktivt()) return;" + ], + [ + " const t = setTimeout(() => { if (nr === laanNr && !laanAktivt()) laanHaandtag?.(); }", + " const t = setTimeout(() => { if (!laanAktivt()) { const h = laanHaandtag; laanHaandtag = null; h?.(); } }" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "7b" + }, + { + "navn": "B6-osascript-overset", + "fil": "mcp-server/index.js", + "par": [ + [ + " [/^Bash\\([^)]*osascript/i,", + " [/^INGEN-osascript/i," + ] + ], + "proeve": "test/omveje.mjs", + "forventet": "1 osascript" + }, + { + "navn": "B6-alt-naevnes", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (hvorfor) fundet.push(", + " if (true) fundet.push(" + ] + ], + "proeve": "test/omveje.mjs", + "forventet": "4 harmloese" + }, + { + "navn": "R15-skaller-overset", + "fil": "mcp-server/index.js", + "par": [ + [ + " [/^Bash\\((\\/[\\w/.-]*\\/)?(ba|z|k|c|da|fi|tc)?sh(\\s|:|\\)|$)/,", + " [/^INGEN-skal/," + ] + ], + "proeve": "test/omveje.mjs", + "forventet": "4b" + }, + { + "navn": "S2-ombryd-taber-tegn", + "fil": "helper/Sources/cmcp-status/Tekst.swift", + "par": [ + [ + " linjer.append(String(ord.prefix(bredde)))\n", + "" + ] + ], + "proeve": "test/ombryd.mjs", + "forventet": "1 ord laengere" + }, + { + "navn": "Q1-ingen-binding", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (laanOejeblik() !== laanVedDom) {", + " if (false) {" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "6b" + }, + { + "navn": "Q1-stille-draebes-ikke", + "fil": "mcp-server/helper.js", + "par": [ + [ + " if (OPSLAG.has(argv[0]) || argv.includes('--dry')) continue;", + " if (OPSLAG.has(argv[0]) || argv.includes('--dry') || argv.includes('--app')) continue;" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "6d" + }, + { + "navn": "Q1-oprindelig-nummerfejl", + "fil": "mcp-server/index.js", + "par": [ + [ + " let nr = null, lukketFoerStart = false;", + " let nr = ++laanNr, lukketFoerStart = false;" + ], + [ + " nr = ++laanNr; laanAfgivet = false;", + " laanAfgivet = false;" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "6e ...og naar" + }, + { + "navn": "Q6-pause-efter-ikon", + "fil": "mcp-server/godkend.js", + "par": [ + [ + " const nej = forhaandsNej(sp.text);\n if (nej) return", + " const nej = forhaandsNej(sp.text);\n if (false) return" + ] + ], + "proeve": "test/ikon-godkend.mjs", + "forventet": "G3a" + }, + { + "navn": "Q3-kolonne-ikke-krævet", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (hvem.length && sam.column !== true) {", + " if (false) {" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q3 uden kolonne" + }, + { + "navn": "Q3-soegefelt-godtaget", + "fil": "mcp-server/index.js", + "par": [ + [ + "&& felt.subrole !== 'AXSearchField';", + "&& true;" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q3 et soegefelt" + }, + { + "navn": "Q3-andet-vindue-godtaget", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (knapVindue && sam.window && knapVindue !== sam.window) {", + " if (false) {" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q3 Send-knappen ligger" + }, + { + "navn": "Q3-alle-overskrifter-som-modtager", + "fil": "mcp-server/index.js", + "par": [ + [ + " `To (read from the screen, above the text field): ${hvem[0]}`,", + " `To (read from the screen, above the text field): ${hvem.join(' - ')}`," + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q3 kun den foerste" + }, + { + "navn": "Q4-ulaeselig-titel-er-ikke-chat", + "fil": "mcp-server/index.js", + "par": [ + [ + " slags = titler.some(x => !x || WEBCHAT.test(x)) ? 'chat' : titler.some(x => WEBMAIL.test(x)) ? 'mail' : null;", + " slags = titler.some(x => x && WEBCHAT.test(x)) ? 'chat' : titler.some(x => WEBMAIL.test(x)) ? 'mail' : null;" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q4 browser med ulaeselig" + }, + { + "navn": "Q4-mail-foer-chat", + "fil": "mcp-server/index.js", + "par": [ + [ + " slags = titler.some(x => !x || WEBCHAT.test(x)) ? 'chat' : titler.some(x => WEBMAIL.test(x)) ? 'mail' : null;", + " slags = titler.some(x => WEBMAIL.test(x)) ? 'mail' : titler.some(x => !x || WEBCHAT.test(x)) ? 'chat' : null;" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q4 «Gmail - Slack»" + }, + { + "navn": "Q9-navnloes-gruppe-er-send", + "fil": "mcp-server/index.js", + "par": [ + [ + " return !rolle || /^AX(Button|Image|Unknown)$/.test(String(rolle));", + " return !rolle || /^AX(Button|Image|Unknown|Group)$/.test(String(rolle));" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q9 klik paa en navnloes gruppe" + }, + { + "navn": "Q8-sudo-overset", + "fil": "mcp-server/index.js", + "par": [ + [ + " [/^Bash\\((\\/[\\w/.-]*\\/)?(sudo|doas)(\\s*\\*|:\\*|\\s+(\\/\\S*\\/)?((ba|z|k|c|da|fi|tc)?sh|python\\d*|node|ruby|perl|osascript|open)\\b)/,", + " [/^INGEN-sudo/," + ] + ], + "proeve": "test/omveje.mjs", + "forventet": "4d" + }, + { + "navn": "R3-kontrol-uden-vindue", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (!knapVindue || !sam.window || !sammeRaekke) {", + " if (!sammeRaekke) {" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q3 Send-knappens vindue kan ikke" + }, + { + "navn": "R3-kontrol-ikke-ved-feltet", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (!knapVindue || !sam.window || !sammeRaekke) {", + " if (!knapVindue || !sam.window) {" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q3 Send-knappen ligger ikke ved" + }, + { + "navn": "R3-ukendt-rolle-ufarlig", + "fil": "mcp-server/index.js", + "par": [ + [ + " return !rolle || /^AX(Button|Image|Unknown)$/.test(String(rolle));", + " return /^AX(Button|Image)$/.test(String(rolle));" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "q9 klik paa et element med ukendt" + }, + { + "navn": "R4-ax-flytter-stempel", + "fil": "mcp-server/index.js", + "par": [ + [ + "POSTER_INPUT.has(name) && res.__postedeInput !== false) sidsteEgenHandling", + "POSTER_INPUT.has(name)) sidsteEgenHandling" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "5e" + }, + { + "navn": "R4-kun-lodret-raekke", + "fil": "mcp-server/index.js", + "par": [ + [ + "\n && k.x >= f.x - 16 && k.x <= f.x + f.w + 160;", + ";" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r4a" + }, + { + "navn": "R4-opslagsfejl-springer-binding", + "fil": "mcp-server/index.js", + "par": [ + [ + " erKontrol = true;\n const k = await kontrol();", + " const k = await kontrol();\n erKontrol = !!(k && !k.fejl);" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r4b" + }, + { + "navn": "R4-browser-kun-fokus-titel", + "fil": "mcp-server/index.js", + "par": [ + [ + " const titler = [String(t.window || '').trim(), ...(k ? [k.fejl ? '' : String(k.vindue || '').trim()] : [])];", + " const titler = [String(t.window || '').trim()];" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r4c" + }, + { + "navn": "R4-paste-undtaget", + "fil": "mcp-server/helper.js", + "par": [ + [ + " if (OPSLAG.has(argv[0]) || argv.includes('--dry')) continue;", + " if (OPSLAG.has(argv[0]) || argv.includes('--dry') || argv[0] === 'paste') continue;" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "6f" + }, + { + "navn": "R5-nul-binding-skjuler-laan", + "fil": "mcp-server/index.js", + "par": [ + [ + " const laanVedDom = laanOejeblik();", + " const laanVedDom = laanAktivt() ? laanNr : 0;" + ], + [ + " if (laanOejeblik() !== laanVedDom) {", + " if ((laanAktivt() ? laanNr : 0) !== laanVedDom) {" + ] + ], + "proeve": "test/skaerm-laan.mjs", + "forventet": "6g" + }, + { + "navn": "R5-ulaeseligt-kontrolvindue-ignoreres", + "fil": "mcp-server/index.js", + "par": [ + [ + " const titler = [String(t.window || '').trim(), ...(k ? [k.fejl ? '' : String(k.vindue || '').trim()] : [])];", + " const titler = [String(t.window || '').trim(), ...(k?.fejl ? [''] : k?.vindue ? [String(k.vindue).trim()] : [])];" + ] + ], + "proeve": "test/sende-port.mjs", + "forventet": "r4d" + }, + { + "navn": "Q10-praecise-kommandoer-taeller", + "fil": "mcp-server/index.js", + "par": [ + [ + " if (arg !== null && r !== 'Bash' && !/\\*/.test(arg) && !/(^|\\s)-$/.test(arg.trim())) continue;", + " if (false) continue;" + ] + ], + "proeve": "test/omveje.mjs", + "forventet": "4f" + }, + { + "navn": "Q11-menu-genvej-overset", + "fil": "mcp-server/index.js", + "proeve": "test/menu-genvej.mjs", + "forventet": "2 et punkt med Cmd+Q", + "par": [ + [ + " return !!p.shortcut && tastSerFarlig(p.shortcut);", + " return false;" + ] + ] + }, + { + "navn": "Q12-menu-ukendt-aabner", + "fil": "mcp-server/index.js", + "proeve": "test/menu-genvej.mjs", + "forventet": "3 et punkt der ikke kan", + "par": [ + [ + " if (!p) return true;", + " if (!p) return false;" + ] + ] + }, + { + "navn": "Q13-lukke-ord-vaek", + "fil": "mcp-server/policy.js", + "proeve": "test/claims.mjs", + "forventet": "11. farlige menustier", + "par": [ + [ + " return FARLIGE_MENUORD.some(o => p.includes(o)) || LUKKE_ORD.test(p);", + " return FARLIGE_MENUORD.some(o => p.includes(o));" + ] + ] + }, + { + "navn": "Q14-tilbagerykning-overset", + "fil": "test/forrest-log.mjs", + "proeve": "test/forrest-log-kalibrering.mjs", + "forventet": "10 rød", + "par": [ + [ + " if (s.aarsag === 'SetFrontProcess' && forrige", + " if (false && s.aarsag === 'SetFrontProcess' && forrige" + ] + ] + }, + { + "navn": "Q15-agent-forrest-overset", + "fil": "test/forrest-log.mjs", + "proeve": "test/forrest-log-kalibrering.mjs", + "forventet": "5 rød", + "par": [ + [ + " if (agentPid(s.pid) && s.aarsag !== 'SetManagedDisplayCurrentSpace')", + " if (false)" + ] + ] + }, + { + "navn": "Q16-mennesket-er-agent", + "fil": "test/forrest-log.mjs", + "proeve": "test/forrest-log-kalibrering.mjs", + "forventet": "15 grøn", + "par": [ + [ + " if (agentPid(s.pid) && s.aarsag !== 'SetManagedDisplayCurrentSpace')", + " if (s.aarsag === 'SetManagedDisplayCurrentSpace')" + ] + ] + }, + { + "navn": "Q17-launch-slaar-ikke-op", + "fil": "mcp-server/index.js", + "proeve": "test/launch-lukket.mjs", + "forventet": "1 et lukket", + "par": [ + [ + " targetBundleId = r.bundleId || null;", + " targetBundleId = null;" + ] + ] + }, + { + "navn": "Q18-doer-sti-smugles", + "fil": "mcp-server/index.js", + "proeve": "test/doer.mjs", + "forventet": "4 et forsoeg", + "par": [ + [ + " if (!/^[0-9A-Za-z]{22}$/.test(id)) return errorResult(", + " if (false) return errorResult(" + ] + ] + }, + { + "navn": "Q19-laering-nummer-slipper", + "fil": "mcp-server/index.js", + "proeve": "test/laering.mjs", + "forventet": "2b et telefonnummer", + "par": [ + [ + " .replace(/\\+?\\d[\\d ,.\\-]{4,}\\d/g, '[number]')\n", + "\n" + ] + ] + }, + { + "navn": "Q20-quit-spoerger-ikke", + "fil": "mcp-server/index.js", + "proeve": "test/claims-offline.mjs", + "forventet": "quit spoerger", + "par": [ + [ + " || name === 'computer_quit'\n", + "\n" + ] + ] + }, + { + "navn": "Q21-luk-vindue-spoerger-ikke", + "fil": "mcp-server/index.js", + "proeve": "test/claims-offline.mjs", + "forventet": "at lukke et vindue", + "par": [ + [ + " || (name === 'computer_window' && args.button === 'close')\n", + "\n" + ] + ] + }, + { + "navn": "Q22-optagelse-spoerger-ikke", + "fil": "mcp-server/index.js", + "proeve": "test/optagelse.mjs", + "forventet": "2 start spoerger", + "par": [ + [ + " || optagStart\n", + "\n" + ] + ] + }, + { + "navn": "Q23-kaede-brud-overset", + "fil": "mcp-server/audit.js", + "proeve": "test/audit-chain.mjs", + "forventet": "en AENDRET linje", + "par": [ + [ + " ok: aegte.length === 0 && !haleFjernet, checked: tjekket,", + " ok: true, checked: tjekket," + ] + ] + }, + { + "navn": "Q24-modtager-overses", + "fil": "mcp-server/index.js", + "proeve": "test/sende-port.mjs", + "forventet": "w1 modtageren", + "par": [ + [ + " const hvem = [...(sam.recipient ? [sam.recipient] : (sam.headings || []))]", + " const hvem = [...(sam.headings || [])]" + ] + ] + }, + { + "navn": "Q25-slet-knap-spoerger-ikke", + "fil": "mcp-server/index.js", + "proeve": "test/knap-ord.mjs", + "forventet": "1 en knap der hedder", + "par": [ + [ + " alwaysAsk: menuFarlig || knapFarlig", + " alwaysAsk: menuFarlig" + ] + ] + }, + { + "navn": "Q26-klik-fejler-aabent", + "fil": "mcp-server/index.js", + "proeve": "test/knap-ord.mjs", + "forventet": "6 intet fundet", + "par": [ + [ + " if (!d?.found) return true;", + " if (!d?.found) return false;" + ] + ] + }, + { + "navn": "Q27-tryk-fejler-aabent", + "fil": "mcp-server/index.js", + "proeve": "test/knap-ord.mjs", + "forventet": "11 dry-run finder intet", + "par": [ + [ + " if (!el) return true;", + " if (!el) return false;" + ] + ] + }, + { + "navn": "Q28-toerkoersel-fejler-aabent", + "fil": "mcp-server/index.js", + "proeve": "test/knap-ord.mjs", + "forventet": "12 selve toerkoerslen fejler", + "par": [ + [ + " return [el.name, ...(el.names || []), el.title].filter(Boolean).some(n => menuSerFarlig(n));\n } catch { return true; }", + " return [el.name, ...(el.names || []), el.title].filter(Boolean).some(n => menuSerFarlig(n));\n } catch { return false; }" + ] + ] + } +] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9197a27..762a373 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: build-and-test: # macOS-koerer. Der findes ingen anden slags: hele produktet er bygget paa # macOS' egne rammevaerker. - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/fremmed-mac.yml b/.github/workflows/fremmed-mac.yml index 7796c39..2e104c8 100644 --- a/.github/workflows/fremmed-mac.yml +++ b/.github/workflows/fremmed-mac.yml @@ -13,14 +13,48 @@ on: push: branches: ['proeve/**'] workflow_dispatch: + inputs: + full_matrix: + description: 'Kør på hele macOS-matrixen (Intel + Apple Silicon, begge GA-versioner) i stedet for kun macos-15' + type: boolean + default: false + film_pynt: + description: 'Ren optagelse til forsiden (2/10, forside-chatten): notes.txt+almindelige ord, neutral side i stedet for Netflix, normalt skrivebord, Game Center tavs. Rører ALDRIG tjekkene.' + type: boolean + default: false jobs: + # ⛔ A6 (1/10): "macos-14 + macos-15, Intel + Apple Silicon" er forældet ordlyd - + # macos-14 er nu DEPRECATED hos GitHub (højst to GA-versioner ad gangen). + # Nutidens svar er de to AKTIVE GA-versioner, begge arkitekturer. En push + # til en proeve/-gren rører stadig kun macos-15 (uændret hastighed); hele + # matrixen køres kun ved manuelt workflow_dispatch med full_matrix=true - + # ellers 4x koererminutter på hver kørsel under udvikling. + matrix-plan: + runs-on: ubuntu-latest + outputs: + os: ${{ steps.plan.outputs.os }} + steps: + - id: plan + run: | + if [ "${{ inputs.full_matrix }}" = "true" ]; then + echo 'os=["macos-15","macos-15-intel","macos-26","macos-26-intel"]' >> "$GITHUB_OUTPUT" + else + echo 'os=["macos-15"]' >> "$GITHUB_OUTPUT" + fi + fremmed-mac: - runs-on: macos-15 + needs: matrix-plan + strategy: + fail-fast: false + matrix: + os: ${{ fromJSON(needs.matrix-plan.outputs.os) }} + runs-on: ${{ matrix.os }} timeout-minutes: 90 env: CMCP_FREMMED_MASKINE: '1' CMCP_STATUS_IKON: '0' + CMCP_FILM_PYNT: ${{ inputs.film_pynt && '1' || '' }} steps: - uses: actions/checkout@v4 @@ -30,6 +64,21 @@ jobs: defaults read -g AppleLanguages || true ls /Applications + # Pynt til forsiden (2/10, forside-chatten): kun KAMERAET's billede ændrer + # sig her - rører intet tjek. Alt er best-effort og fejler aldrig bygningen + # (`|| true` overalt): en koerer uden netop den defaults-noegle skal stadig + # kunne koere resten af suiten. + - name: Pynt til forsiden + if: ${{ inputs.film_pynt }} + run: | + PIC=$(ls "/System/Library/Desktop Pictures/"*.heic 2>/dev/null | head -1) + if [ -n "$PIC" ]; then + osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"$PIC\"" || true + fi + defaults write com.apple.gamed GKAutoSignInEnabled -bool false 2>/dev/null || true + defaults -currentHost write com.apple.notificationcenterui doNotDisturb -bool true 2>/dev/null || true + killall NotificationCenter 2>/dev/null || true + # Samme bygning som udgivelsen: hjaelperen til begge arkitekturer OG # menulinje-ikonet (koersel 3: statusikon-proeven fandt intet ikon). - name: Byg som udgivelsen @@ -50,6 +99,13 @@ jobs: - name: Pillow run: python -m pip install --quiet --disable-pip-version-check pillow + # 1/10 (Gustav: «film hvordan den fungerer»): hvert scenarie og hver + # parallel-koersel optages - kun her, paa GitHubs Mac. + - name: Kamera til filmene + run: | + which ffmpeg || brew install --quiet ffmpeg + ffmpeg -hide_banner -f avfoundation -list_devices true -i "" 2>&1 | grep -i "capture screen" || true + # Uden et menneske er Finder forrest, og produktet roerer (med rette) aldrig # det program mennesket bruger. Et menneske ville arbejde i et andet program # - ogsaa mens suiten koerer (koersel 3: to suiter ramte Finder). @@ -59,6 +115,7 @@ jobs: mcp-server/vendor/cmcp-helper focused || true - name: Hele suiten + id: suite continue-on-error: true run: | export TMPDIR="$RUNNER_TEMP/" @@ -69,8 +126,15 @@ jobs: exit $rc - name: Mutationsbevis for giv-tilbage (proeven tager forgrunden, saa kun her) + id: mutant continue-on-error: true - run: bash .github/mutant-giv-tilbage.sh + # 29/9 (B7): og hver port-vagt (samtykke, sende-port, skaerm-laan, omveje) + # skal kunne blive roed. bash -e: uden «|| rc» stoppede trinnet ved foerste. + run: | + rc=0 + bash .github/mutant-giv-tilbage.sh || rc=1 + python3 .github/mutant-porte.py || rc=1 + exit $rc # Uden et menneske er Finder forrest, og produktet roerer (med rette) aldrig # det program mennesket bruger. Et menneske ville arbejde i et andet program. @@ -83,8 +147,9 @@ jobs: mcp-server/vendor/cmcp-helper focused || true - name: Brugsscenarierne i baggrunden + id: bg continue-on-error: true - run: CMCP_BRUG=laes,lokal,kamera CMCP_BRUG_RAPPORT="$RUNNER_TEMP/baggrund.json" node test/brugsscenarier.mjs + run: CMCP_FILM="$RUNNER_TEMP/film" CMCP_BRUG=laes,lokal,kamera CMCP_BRUG_RAPPORT="$RUNNER_TEMP/baggrund.json" node test/brugsscenarier.mjs - name: Hvad maskinen viste efter baggrunden if: always() @@ -98,20 +163,80 @@ jobs: sleep 2 - name: Brugsscenarierne i forgrunden + id: fg continue-on-error: true - run: CMCP_BRUG=laes,lokal,kamera CMCP_BRUG_TILSTAND=forgrund CMCP_BRUG_TAG_SKAERMEN=1 CMCP_BRUG_RAPPORT="$RUNNER_TEMP/forgrund.json" node test/brugsscenarier.mjs + run: CMCP_FILM="$RUNNER_TEMP/film" CMCP_BRUG=laes,lokal,kamera CMCP_BRUG_TILSTAND=forgrund CMCP_BRUG_TAG_SKAERMEN=1 CMCP_BRUG_RAPPORT="$RUNNER_TEMP/forgrund.json" node test/brugsscenarier.mjs - name: Hvad maskinen viste efter forgrunden if: always() run: bash .github/diagnose.sh forgrund + # 1/10 (Gustav): 5, 10 og 15 agenter samtidig i baggrunden, mens et menneske + # skriver i TextEdit. Forrest maales hvert 100. ms; teksten tegn for tegn. + - name: Mange agenter samtidig + id: par + continue-on-error: true + run: | + killall Chess Calculator Contacts "System Settings" "Activity Monitor" Notes Safari "Script Editor" 2>/dev/null || true + CMCP_FILM="$RUNNER_TEMP/film" CMCP_PARALLEL_RAPPORT="$RUNNER_TEMP/parallel.json" node test/parallel.mjs + + - name: Filmene + if: always() + uses: actions/upload-artifact@v4 + with: + # Matrix-sikkert navn (A6, 2/10): flere OS-koerere i samme kørsel ville + # ellers kollidere på samme artefakt-navn (upload-artifact@v4 kræver unikt). + name: fremmed-mac-film-${{ matrix.os }} + path: ${{ runner.temp }}/film/ + if-no-files-found: warn + + # B3 (2/10): ét billede pr. use case (før/efter) i en HTML-rapport, linket + # fra PR'en - uden at se alle 19+19 film i fuld længde. Trækker kun billeder + # ud af film "Kamera til filmene" allerede har optaget her. + - name: Kontaktark + if: always() + run: | + python3 scripts/kontaktark.py "${{ runner.temp }}/film" "${{ runner.temp }}/kontaktark" || true + if [ -f "${{ runner.temp }}/kontaktark/index.html" ]; then + echo "### Kontaktark (B3)" >> "$GITHUB_STEP_SUMMARY" + echo "Før/efter-billede pr. use case: se artefaktet **fremmed-mac-kontaktark-${{ matrix.os }}** på denne kørsel." >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Kontaktark-artefakt + if: always() + uses: actions/upload-artifact@v4 + with: + name: fremmed-mac-kontaktark-${{ matrix.os }} + path: ${{ runner.temp }}/kontaktark/ + if-no-files-found: warn + - name: Beviserne if: always() uses: actions/upload-artifact@v4 with: - name: fremmed-mac-beviser + name: fremmed-mac-beviser-${{ matrix.os }} path: | ${{ runner.temp }}/suite.log ${{ runner.temp }}/baggrund.json ${{ runner.temp }}/forgrund.json + ${{ runner.temp }}/parallel.json ${{ runner.temp }}/diag/ + + # ⛔ 28/9: foer var suite, mutant og de to scenarie-koersler alle + # continue-on-error uden en samlende port - en groen koersel beviste + # intet om proeverne. Nu fejler jobbet, hvis en af dem ikke bestod. + # (Beviserne uploades ovenfor med if: always, saa de findes stadig.) + - name: Porten - en fejlet proeve goer hele jobbet roedt + if: always() + run: | + fail=0 + for par in "Suiten:${{ steps.suite.outcome }}" \ + "Mutationsbeviset:${{ steps.mutant.outcome }}" \ + "Baggrunds-scenarier:${{ steps.bg.outcome }}" \ + "Forgrunds-scenarier:${{ steps.fg.outcome }}" \ + "Mange agenter samtidig:${{ steps.par.outcome }}"; do + navn="${par%%:*}"; res="${par##*:}" + echo "$navn: $res" + [ "$res" = "success" ] || { echo "⛔ $navn bestod ikke ($res)"; fail=1; } + done + [ "$fail" = "0" ] && echo "Alle fem porte grønne." || exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32b6f89..e2bfcaa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ permissions: jobs: universal: - runs-on: macos-14 + runs-on: macos-15 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/udgiv-npm.yml b/.github/workflows/udgiv-npm.yml index 605da7c..9328d97 100644 --- a/.github/workflows/udgiv-npm.yml +++ b/.github/workflows/udgiv-npm.yml @@ -27,7 +27,11 @@ name: Udgiv til npm # Provider: GitHub Actions # Repository: Agent360dk/computerMCP # Workflow: udgiv-npm.yml -# Environment: (tom) +# Environment: udgivelse +# (skal matche jobbets "environment: udgivelse" nedenfor - ellers afviser +# npm OIDC-tokenet, fordi dets environment-claim ikke matcher det +# registrerede. Rettet 2/10 - stod tidligere som "(tom)", i modstrid med +# jobbets egen linje og den låste beslutning om at binde til "udgivelse".) # # Derefter: ingen token, ingen engangskode, ingen udloeb. Nogensinde. # --------------------------------------------------------------------------- @@ -47,7 +51,10 @@ jobs: udgiv: # macOS fordi build-release.sh bygger den universelle Swift-binaer # (arm64 + x86_64) der ligger i pakken. En Linux-koerer kan ikke. - runs-on: macos-14 + runs-on: macos-15 + # ⛔ 30/9: bundet til miljoeet «udgivelse» (Gustav eneste godkender, kun main). + # Uden den kunne enhver med skriveadgang udgive paa npm uden hans klik. + environment: udgivelse steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 567460e..8b56b01 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ videos/*/.hyperframes/ # lokalt saa alting rigtigt ud fordi filen laa paa disken. !docs/demo.mp4 .dialog-kvittering + +# MCPB-bundle build-output (bygges on demand med scripts/build-mcpb.sh) +dist-mcpb/ +*.mcpb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6328064..c9021ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,64 @@ new relative to 0.1.0. **Typing reaches an app in the background.** `computer_type` with an `app` now writes into the field that app has focus in and reads it back, instead of sending keystrokes an app without a key window can drop (text sent to Finder's search -field arrived nowhere). A field that does not accept that, and every password -field, still gets keystrokes, and the answer says the text was not confirmed. +field arrived nowhere). A field that does not accept that still gets +keystrokes, and the answer says the text was not confirmed. A password field gets +nothing: `computer_type` now refuses it as `computer_set_value` always did, and +checks before every character, so a Tab in "userpassword" - or a click, or a +field that moves on by itself - stops the typing at the password field. +**It says where it can be walked around.** The consent gate guards this +server's own calls; an agent with `osascript` or browser automation in its +client's allow list never reaches it. `computer_permissions` now names those +rules (Claude Code settings; other clients are not read) - only the ones that +allow anything: a wildcard or a script on stdin, not an exact command that runs +one fixed thing (on a real Mac: 22 of 127 matching rules) - and SECURITY.md says +plainly what the gate is and is not. +**A waiting question says so on screen.** Measured on a real Mac: four screen +loans expired unanswered while the icon correctly turned orange - the box on +screen only said how many agents were working, on another display. It now says +`Needs you: click the orange menu bar icon`, and moves to the screen you are +on when a question arrives (text only; the box never gets a button). +**The screen can be lent, and taken back.** Background mode kept seven tools +out of reach unless you restarted with `CMCP_BACKGROUND=0`, per server. +`computer_request_screen` now asks in the menu bar: you lend this one agent the +screen with Touch ID for at most 15 minutes, only one agent at a time. Those +tools then appear for it; each step waits while you are using the keyboard or +mouse (a step already running finishes), and `Take the screen back now` ends +the loan at once and stops a step that is still running. Setting `CMCP_BACKGROUND` +locks it out. Consent in the foreground now goes through the menu bar icon too +(Touch ID); the dialog is only the fallback when the icon is not running. A no in +the menu bar, a question already waiting, or an action too long to show in full +is a no - never a dialog instead. +**A message to a real person asks every time.** Until now, pressing Send in +WhatsApp went through in `allow` without anyone being asked. In messaging apps, +Send (pressed or clicked) and Return/Enter with any modifier now ask for that one +message, showing the recipient and the text read from the screen by the server. +A line break in `computer_type` is refused in chat apps, since it sends; in mail it +is a new line, and Cmd+Return / Cmd+Shift+D is the send. Web chats and webmail in +a browser count too, recognised by the page title (a chat on a site that is not on +the list is not recognised). The recipient is the name +above the text field; if it or the text cannot be read, nothing is sent. If what +would be sent changes after the yes, nothing is sent. +**`computer_ask_user` works in background mode.** It used to be a dialog, so +background mode refused it - and an agent that hit a password or a 2FA code +could only give up. The question now waits in the menu bar icon: the whole +request, where it lands (written by the server), and `Take me there`, so it is +the person who brings the app forward. `Done` is a signal, never a consent, and +is only accepted on this kind of question. +**You see the whole action before you allow it.** The menu bar question used to +cut the action at 200 characters and the Touch ID sheet at 80, without saying so. +The full text now sits wrapped above `Allow…` (up to 4,000 characters), the sheet +says when it is shortened, and a longer action is not asked at all. **When an app brings itself forward, the front is handed back.** Pressing a button or a menu item can make the app bring itself to the front: "New Finder Window" pulled Finder over the app the person was working in, and the answer said nothing. `computer_press` and `computer_menu` now report `took_screen`, and when the target app took the front, it is handed straight back to the app the -person was in (`gave_back`). Only a switch to the target app is counted. +person was in (`gave_back`). The front is only handed back when the change was +the app's own doing: if someone pressed a key or clicked just before, it is left +where they put it and the reply says the screen changed rather than reactivating +under their hands. And taking the screen and handing it back is no longer counted +as a clean background run - the scenario reports it as `delvist`, not proven. `computer_launch` with `background: true` does the same: measured on a clean Mac, six of eight apps brought themselves forward as they started, before the hide took effect. And every `took_screen` that rested on a change of front app @@ -46,8 +96,20 @@ nothing to reach. An app that already has a window is left exactly as it is. `index` (its number in the list `computer_find` returned) pick one element. `computer_set_value` no longer guesses with `first` when several fields match: in Finder the first text field can be a file name. - -**The safety fixes, first.** 0.1.0's redaction could fail open: if painting +**An app's own door, opened from behind.** `computer_open` reaches an app whose +window is covered or that will not act from behind, through the app's own URL +rather than its window: `play_track` (a Spotify track), `open_chat` (a WhatsApp +chat, with no pre-filled text), `open_app`. You give an intent and one parameter, +never a URL; the server builds a fixed, validated URL for it, and `file:`, +`shortcuts:`, `osascript` and anything else are refused. It carries navigation +only, never a send. Its input safety is proven; its effect on the real apps is +not yet measured on a Mac that has them. + +**The safety fixes, first.** +The per-app lock now fails closed: if it cannot be taken for any reason other +than another agent holding it, the action does not happen (before, a lock- +infrastructure error let two agents write over each other - the very thing the +lock exists to stop). 0.1.0's redaction could fail open: if painting over a password field failed, the unpainted image was used. It now fails closed, and the whole image is blacked out when the scan runs out of time. Password managers are left out of the capture itself, not just painted over. diff --git a/README.md b/README.md index e0eb672..3b7b4ec 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,14 @@ A macOS computer-use MCP server built for the part everyone skips: what happens in the hours you are not watching. +- **It runs in every MCP client, not one app.** Claude Code, Cursor, VS Code, + Codex, Windsurf, Zed, or your own agent - one `npx` line, the same server + everywhere. It is not tied to a single vendor's desktop app or plan. - **It does not take over your Mac.** It presses buttons and fills fields in windows that stay behind the one you are in, and it leaves your pointer where - you put it. Nothing gets minimised, nothing comes to the front, nothing steals - what you are typing. + you put it. When an app pulls itself to the front as it opens, the front is + handed straight back and the reply says so - nothing is left stealing what you + are typing. - **It does not pretend.** When something is refused or fails, it says so and says why. The most common complaint about agents driving a computer is that they carry on as if it worked. @@ -159,7 +163,7 @@ it refuses. ## Tools -**30 tools: fourteen that look, sixteen that touch.** Twenty are offered by +**32 tools: fourteen that look, eighteen that touch.** Twenty-five are offered by default, and the agent uses them without asking - the same way a browser tool drives a browser. Two gates survive that, and they are the two that matter: @@ -197,22 +201,30 @@ and the front is handed straight back to the app you were in (`gave_back`). `computer_launch` joins them with `background: true`: the app starts behind what you are doing. Many apps bring themselves forward as they start - six of eight did on a clean Mac - and when one does, the front is handed straight back -and the answer says `took_screen` and `gave_back`. It is a moment, not nothing. Eight are still held back: -`move`, `activate`, `quit`, `space`, `window`, `drag`, `paste` and `ask_user`. `CMCP_BACKGROUND=0` +and the answer says `took_screen` and `gave_back`. It is a moment, not nothing. Seven are still held back: +`move`, `activate`, `quit`, `space`, `window`, `drag` and `paste` - until you lend +the screen: `computer_request_screen` asks you in the menu bar, one agent at a +time, approved with Touch ID, for at most 15 minutes. While it has the screen, +each step waits while you are using the keyboard or mouse (a step already +running finishes), and `Take the screen back now` ends the loan at once and +stops a step that is still running. Setting `CMCP_BACKGROUND` locks it +out entirely. `CMCP_BACKGROUND=0` gives you those too - and a typo will not turn it off, only `0`, `false`, `no` or `off`. `CMCP_MODE=ask` puts one consent dialog per session in front of the -first write, and `CMCP_MODE=readonly` leaves you the twelve that only look. +first write, and `CMCP_MODE=readonly` leaves you the fourteen that only look. Both surviving gates are mutation-proved: break them in the source and the refusal turns into a free pass, which is how we know the test can fail. -> **What you get today, honestly.** `npx @agent360/computer-mcp` currently serves **0.1.0**, which has 12 tools. The 30 tools described here are the source: they are built and tested, but not published yet. Building from source takes about thirty-five seconds if you want them now. -**Look:** `computer_pending` · `computer_screenshot` · `computer_record` · `computer_inspect` · `computer_find` · +> **What you get today, honestly.** `npx @agent360/computer-mcp` currently serves **0.1.0**, which has 12 tools. The 32 tools described here are the source: they are built and tested, but not published yet. Building from source takes about thirty-five seconds if you want them now. + + +**Look:** `computer_pending` · `computer_screenshot` · `computer_record` · `computer_inspect` · `computer_find` · `computer_wait_for` · `computer_focused` · `computer_apps` · `computer_windows` · `computer_permissions` · `computer_displays` · `computer_menus` · `computer_audit` · `computer_learning` -**Touch:** `computer_launch` · `computer_quit` · `computer_paste` · `computer_window` · `computer_space` · `computer_menu` · `computer_press` · `computer_set_value` · `computer_ask_user` · +**Touch:** `computer_request_screen` · `computer_launch` · `computer_open` · `computer_quit` · `computer_paste` · `computer_window` · `computer_space` · `computer_menu` · `computer_press` · `computer_set_value` · `computer_ask_user` · `computer_click` · `computer_drag` · `computer_type` · `computer_key` · `computer_scroll` · `computer_move` · `computer_activate` @@ -220,12 +232,19 @@ refusal turns into a free pass, which is how we know the test can fail. or false, never text. The agent puts the cursor in the field, the dialog names the app and the window it is about to land in - written by the server, not by the model - and you type on your own keyboard. There is deliberately no route -through this server for a password to reach a model. +through this server for a password to reach a model. In background mode there is +no dialog: the question waits in the menu bar icon, with the whole request, where +it lands, and `Take me there` - your click brings the app forward, not the agent's. +You do it and choose `Done`. A Done is a signal, not a consent: it can never +approve anything else. **`computer_set_value`** writes into a field behind another window without moving your pointer, and refuses on a secure field every time. We removed that check on purpose once: the modified build wrote into the password box. It is the only thing standing there. +`computer_type` refuses the same way, and checks before every character: when a +Tab, a click or the page itself moves the focus into a password field halfway +through, the typing stops there. **`computer_wait_for`** waits for an element to appear instead of taking screenshots in a loop. Twenty polls cost one call here and twenty images the @@ -268,6 +287,13 @@ still ask every time. `computer_press` names its target app, and that name is what the gate judges - so pressing something in 1Password asks even when 1Password is nowhere near the front. +**Claude Code's auto mode and the screen tools.** Measured on 1 Oct: after the +person lent the screen with Touch ID, Claude Code's auto mode still refused +`computer_move` as a security weakening. That is the client's own guard, not this +server's, and it is right to be careful. To use the screen tools, run the session +without auto mode and approve each call when Claude Code asks; the lease and the +gates in this server still apply on top. + **Several agents at once.** Each MCP client starts its own server, so a second chat is just a second process. They share one audit log, and every line carries a per-server `session` mark - set `CMCP_CLIENT=` and the line carries that @@ -302,6 +328,30 @@ thing this product puts on your screen, and it never takes focus. answer travels back down the same socket connection the question came in on, bound to a one-time number with a deadline. A late answer, a different number, or a yes without your fingerprint is a no. +- **You see the whole action before you allow it.** The full text sits in the + submenu, wrapped, right above `Allow…` - up to 4,000 characters. Anything longer + is not asked at all: a yes has to cover everything you saw. +- **A message to a real person asks every time.** In the chat and mail apps on + its list (WhatsApp, Messages, Slack, Teams, Telegram, Signal, Discord, Mail, + Outlook and others) and in web chats and webmail recognised by the page title + (WhatsApp, Messenger, Slack, Teams, Telegram, Discord, LinkedIn, Instagram, + Facebook, Google Chat, X, Gmail, Outlook and a few more - a chat on any other + site is not recognised), pressing or + clicking Send - in several languages, or a button with no name at all - and + pressing Return in a chat (Cmd+Return in mail) asks you first, for that one + message. The question shows who it goes to and what it says, read from the + screen by the server: the name above the text field, and the field itself. + If either cannot be read, nothing is sent - you send it yourself. A line break + typed into a chat is refused, because there it sends; in mail it is just a new + line. If the conversation or the text changes after you say yes, nothing is + sent. What it cannot see: apps not on the list, and messages sent by other + tools your client allows, such as browser automation or `osascript`. +- **It tells you where it can be walked around.** The gate guards this server's + own calls, not your whole Mac. `computer_permissions` lists the rules it + recognises in your client that let an agent click, type or send without it - + a shell, `sudo`, `osascript`, a script interpreter, browser or computer + automation - so you can decide whether to keep them. It reads Claude Code's + settings; other clients are not read. - **Some things can never be approved there**: apps on the always-ask list (password managers), an action whose target app could not be resolved, and an unredacted screenshot. Those stay refused and show up in `computer_pending`. diff --git a/SECURITY.md b/SECURITY.md index d858490..77bdc21 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,6 +20,8 @@ In scope: - Making an expired or dismissed dialog resolve as approval - Clear-text secrets appearing in the audit log - Command injection through any argument that reaches the helper binary +- Sending a message in a messaging app through this server without the person + seeing who it goes to and what it says, and approving that one message Out of scope, and documented as such: @@ -29,6 +31,27 @@ Out of scope, and documented as such: containment, and the README says so before you install. - Prompt injection that leads to an action the user then approves. - macOS permissions being held by the host app rather than by this package. +- Sends that never pass through this server: browser automation, `osascript`, + or any other tool your client allows. The send port guards this server's own + calls; your client's permissions decide the rest. +- A web chat or webmail whose page title is not on the recognised list, and a + messaging app not on the list. Recognition is by title and bundle ID; the + lists are in `policy.js` and in the README. + +## What the consent gate is, and is not + +The gate guards this server's own tool calls. It is not a jail. An agent that +also has a shell under your user can quit the menu bar icon and answer on its +socket, edit this package's files, or click and type with `osascript` without +ever calling us. Nothing inside this package can prevent that - everything it +has is writable by the same user. + +The control that does sit outside the agent is your MCP client's permissions. +`computer_permissions` lists the rules it recognises there that let an agent act +without this server (for Claude Code: a shell, `sudo`, `osascript`, script +interpreters, browser or computer automation). It is a list of known patterns, +not a proof that nothing else gets around the gate; other clients are not read. +Only you can remove those rules. ## Supported versions diff --git a/bundle/manifest.json b/bundle/manifest.json new file mode 100644 index 0000000..ffd122c --- /dev/null +++ b/bundle/manifest.json @@ -0,0 +1,44 @@ +{ + "manifest_version": "0.3", + "name": "computer-mcp", + "display_name": "Computer MCP", + "version": "0.2.1", + "description": "macOS computer use you can leave running: it stays behind your window and never touches passwords.", + "long_description": "A macOS computer-use MCP server built for the hours you are not watching. It acts in windows behind the one you are in and leaves your pointer alone, it says so when something fails, several can run at once, and it will not go near your password manager. Every action is written to an append-only audit log. Runs in any MCP client, macOS, MIT, no account.", + "author": { + "name": "Agent360", + "url": "https://computermcp.dev" + }, + "homepage": "https://computermcp.dev", + "documentation": "https://computermcp.dev", + "repository": { + "type": "git", + "url": "https://github.com/Agent360dk/computerMCP" + }, + "license": "MIT", + "keywords": ["macos", "computer-use", "mcp", "accessibility", "security", "privacy", "background", "audit", "automation"], + "server": { + "type": "node", + "entry_point": "server/index.js", + "mcp_config": { + "command": "node", + "args": ["${__dirname}/server/index.js"], + "env": { + "CMCP_MODE": "${user_config.mode}", + "CMCP_CLIENT": "claude-desktop" + } + } + }, + "compatibility": { + "platforms": ["darwin"] + }, + "user_config": { + "mode": { + "type": "string", + "title": "Mode", + "description": "readonly = only read tools · ask = confirm each write · allow = drive without asking (default)", + "default": "allow", + "required": false + } + } +} diff --git a/docs/404.html b/docs/404.html index 9f015b8..4d4930f 100644 --- a/docs/404.html +++ b/docs/404.html @@ -87,7 +87,7 @@

The pages that do exist

Install for Claude Code Install for Codex Install for Cursor -All 30 tools +All 32 tools Capability matrix Threat model MCP security diff --git a/docs/blog/driving-macos-from-an-ai-agent.html b/docs/blog/driving-macos-from-an-ai-agent.html index e4f5c76..36008b6 100644 --- a/docs/blog/driving-macos-from-an-ai-agent.html +++ b/docs/blog/driving-macos-from-an-ai-agent.html @@ -156,7 +156,7 @@

What we would tell someone starting today

-

Computer MCP is the macOS computer-use MCP server these findings came out of. Thirty tools, no API key, MIT, and a security model that is specific about what it does not cover. The recipes folder collects app-specific notes like the Electron numbers above, and it takes contributions.

+

Computer MCP is the macOS computer-use MCP server these findings came out of. Thirty-two tools, no API key, MIT, and a security model that is specific about what it does not cover. The recipes folder collects app-specific notes like the Electron numbers above, and it takes contributions.

diff --git a/docs/docs/capability-matrix/index.html b/docs/docs/capability-matrix/index.html index f63f79f..b17318b 100644 --- a/docs/docs/capability-matrix/index.html +++ b/docs/docs/capability-matrix/index.html @@ -8,7 +8,7 @@ - + @@ -93,7 +93,7 @@

Walls that exist because the tool is not you

WallWhat actually happens macOS permissionsby designAccessibility and Screen Recording must be granted by a human, once, in System Settings. No tool can grant them to itself - that is the whole point of them. unmeasured: which process the grant attaches to when the server is started by an MCP client over npx, and whether upgrading re-prompts. Issue #4 tracks it. Touch ID, biometricswill notCannot be simulated, and should not be. A fingerprint prompt is a human saying yes with their body. -Passwords, 2FA codes, CAPTCHAsby designThe agent never receives one. computer_ask_user puts the cursor in the field, shows a dialog naming the app and window, and you type on your own keyboard. It returns true or false - never text. There is deliberately no route through this server for a secret to reach a model. +Passwords, 2FA codes, CAPTCHAsby designThe agent never receives one. computer_ask_user puts the cursor in the field and names the app and window - in the menu bar icon in background mode, in a dialog otherwise - and you type on your own keyboard. computer_type refuses password fields, checked before every character. It returns true or false - never text. There is deliberately no route through this server for a secret to reach a model.

Walls in macOS itself

@@ -112,14 +112,14 @@

Walls we clear partly, and say so

- + - + @@ -147,10 +147,10 @@

Not yet, and honestly named

WallWhat actually happens
Redacting secretsmeasuredOnly what macOS marks as secure is painted out. Measured on a real web page: a password input carries role AXTextField with subrole AXSecureTextField and is redacted; a plain text field is not. A password in a text editor, or a token printed in a terminal, carries no marker and will not be hidden. Use readonly when the screen holds something the system cannot know about.
Working without taking over the screenmeasuredThree kinds, and the difference is how the action is delivered, not what the tool is called. Quiet by delivery: computer_press, computer_set_value and computer_menu fire the element's own accessibility action, so they work on a window behind the one you are in and never touch your pointer. The app can still bring itself forward in response - measured: File > New Finder Window did - and then press and menu answer took_screen and hand the front straight back. Quiet when you name the app: computer_click, computer_type, computer_key and computer_scroll are delivered into that app's own event queue - measured: the text arrived, the pointer stayed where it was left, and the front window did not change. computer_launch joins them with background: true - measured on a clean Mac, six of eight apps brought themselves forward as they started, and the front is handed straight back. Without an app they go through the system input tap and land wherever your keyboard focus is, so in background mode they are refused. One measured limit: keystrokes do not land in a Chromium window that is not focused, so for anything Chromium-based use computer_press and computer_set_value. Held back in background mode: computer_move, computer_drag, computer_paste, computer_activate, computer_quit, computer_space, computer_window and computer_ask_user. Each either moves your pointer, brings something to the front, or changes what you see - a window moved on a screen you are looking at is a quiet channel with a very visible result.
Working without taking over the screenmeasuredThree kinds, and the difference is how the action is delivered, not what the tool is called. Quiet by delivery: computer_press, computer_set_value and computer_menu fire the element's own accessibility action, so they work on a window behind the one you are in and never touch your pointer. The app can still bring itself forward in response - measured: File > New Finder Window did - and then press and menu answer took_screen and hand the front straight back. Quiet when you name the app: computer_click, computer_type, computer_key and computer_scroll are delivered into that app's own event queue - measured: the text arrived, the pointer stayed where it was left, and the front window did not change. computer_launch joins them with background: true - measured on a clean Mac, six of eight apps brought themselves forward as they started, and the front is handed straight back. Without an app they go through the system input tap and land wherever your keyboard focus is, so in background mode they are refused. One measured limit: keystrokes do not land in a Chromium window that is not focused, so for anything Chromium-based use computer_press and computer_set_value. Held back in background mode: computer_move, computer_drag, computer_paste, computer_activate, computer_quit, computer_space and computer_window. Each either moves your pointer, brings something to the front, or changes what you see - a window moved on a screen you are looking at is a quiet channel with a very visible result. computer_ask_user left this list on 29 September: in background mode its question waits in the menu bar icon instead of a dialog. And the held-back seven are no longer out of reach: computer_request_screen asks you to lend this one agent the screen, approved with Touch ID in the menu bar, for at most 15 minutes. While it has it, each step waits while you are using the keyboard or mouse (a step already running finishes), and one click in the menu takes the screen back and stops a step still running. Set CMCP_BACKGROUND and nothing can borrow it.
Several agents at oncemeasuredEach MCP client runs its own server. Measured: two servers, 50 interleaved writes, zero torn lines, and every line carries a session mark. But they share one pointer and one focused window, and nothing locks between them. Give background work to computer_press.
Knowing where the cursor ismeasuredcomputer_focused reports the focused element and whether it is secure. The system-wide lookup alone returns nothing - measured - so it goes through the focused application, then the frontmost app.
A Save / Don't Save sheetmeasuredThe sheet you meet every time you close something with unsaved work. Measured: the sheet is found as AXSheet, all three buttons come back with a center and a pressable flag, and computer_find with contains: "Don't Save" returns exactly that one button - an agent looks for the words, not for a position. It is also the clearest case for the gate: a sheet can throw away work on your behalf, so in read-only mode pressing it is refused and nothing reaches the machine. Measured against a sheet the test puts up itself, in a fully transparent window, so proving it costs nobody their screen.
The Dock and the status iconsmeasuredWi-Fi, the clock, the battery, and every app in the Dock were invisible until today, and the reason was not that they are special: they simply live outside any window. Measured: the Dock has zero windows and 32 dock items hanging off the application itself; Control Center has zero windows and nine menu bar items. The search walked down through windows only, so everything outside one was unreachable. It now falls back to the application element when an app has no windows - computer_find returns them like anything else, with a center you can click and a pressable flag. Nothing changes for apps that do have windows.
Whether an update breaks your permissionsmeasuredIt does not. The grant belongs to the host - the app that launches the server, which is your terminal, your editor or Claude Desktop - and not to our binary. Measured directly: the same helper run from a path macOS had never seen still reports Accessibility, and so does a copy with a byte appended, which gives it a different signature entirely. So upgrading the package does not send you back to System Settings. The flip side is worth knowing too: the permission you granted covers anything that host launches, not this tool alone.
Knowing that something is waiting for youmeasuredIf the server may never take the screen, it cannot knock either. A write that would need a dialog is refused, and the explanation goes back to the model - which only reaches you if you read that particular conversation. computer_pending makes it visible instead: what was asked for, when, and why it stopped. It is a list, not a button - nothing there can be approved from there, because a consent granted without a human is the one thing the gate exists to prevent. To allow the work you change the mode, and the queue simply says what is waiting.
Never taking over the screen at allmeasuredThis is the default. The server runs in background mode unless you set CMCP_BACKGROUND=0, and a typo does not turn it off - only 0, false, no or off count as a no.

What that means for the tools. Until 21 September background mode hid all thirteen of those that could take the screen, which made it the product minus half its hands. That was because "takes the screen" was a property of the tool's name. It is not - it is a property of the delivery. An event sent to the global input stream lands in whatever window you are using; the same event delivered into one app's own queue does not. So four of them - computer_type, computer_key, computer_scroll and computer_click - now take an app, and with it they are offered in background mode. Without one the call is refused with an explanation of what to pass instead. Measured: 22 of the 30 are offered in background, eight of them writing - up from 15 and three.

Measured on a machine in use, through the shipped product: text typed into an app that was not in front arrived in that app, the pointer stayed where the person had left it, and the front window did not change. Typing goes into the field that app has focus in and is read back; a field that does not accept that (and every password field) gets keystrokes instead, which an app in the background can drop, and the answer then says the text was not confirmed. That measurement covers typing and key presses. Clicks and scrolls take the same route and have not been measured - a mouse event carries no window number, and whether an app accepts one it never saw the pointer reach is a question we have not answered. The answer still reports on the screen honestly; it does not claim the click landed. Every key press, click, scroll and typed string now carries took_screen in its own answer, derived from what the server did rather than from what happened around it - so it cannot mistake the person moving their own mouse for the server moving it.

Still hidden in background mode, and some always will be: computer_move is the pointer, computer_activate and computer_space exist to move you, and computer_ask_user is a dialog. computer_launch joins the quiet side with background: true - the app starts behind what you are doing. quit, window, drag and paste have not been rebuilt yet.

One thing it does to other apps, and you should know it. To find a password field in Slack, Notion or Discord at all, Chromium's accessibility tree has to be on - so taking a screenshot switches it on in every app on screen, not only the one you asked about. Without that, the blackout is blind in exactly the apps where people type passwords. The cost is carried by those apps: they build and keep a tree they would not otherwise have. CMCP_INGEN_ELECTRON=1 turns it off, and the blackout is then blind there.

The honest limit that does not change: this is a promise about what we do. Press a button and the app may open a window of its own; that is its choice, not ours.
Never taking over the screen at allmeasuredThis is the default. The server runs in background mode unless you set CMCP_BACKGROUND=0, and a typo does not turn it off - only 0, false, no or off count as a no.

What that means for the tools. Until 21 September background mode hid all thirteen of those that could take the screen, which made it the product minus half its hands. That was because "takes the screen" was a property of the tool's name. It is not - it is a property of the delivery. An event sent to the global input stream lands in whatever window you are using; the same event delivered into one app's own queue does not. So four of them - computer_type, computer_key, computer_scroll and computer_click - now take an app, and with it they are offered in background mode. Without one the call is refused with an explanation of what to pass instead. Measured: 25 of the 32 are offered in background, eleven of them writing - up from 15 and three.

Measured on a machine in use, through the shipped product: text typed into an app that was not in front arrived in that app, the pointer stayed where the person had left it, and the front window did not change. Typing goes into the field that app has focus in and is read back; a field that does not accept that gets keystrokes instead, which an app in the background can drop, and the answer then says the text was not confirmed. A password field gets nothing - checked before every character, so a Tab into one halfway through stops the typing there. That measurement covers typing and key presses. Clicks and scrolls take the same route and have not been measured - a mouse event carries no window number, and whether an app accepts one it never saw the pointer reach is a question we have not answered. The answer still reports on the screen honestly; it does not claim the click landed. Every key press, click, scroll and typed string now carries took_screen in its own answer, derived from what the server did rather than from what happened around it - so it cannot mistake the person moving their own mouse for the server moving it.

Still hidden in background mode, and some always will be: computer_move is the pointer, computer_activate and computer_space exist to move you. computer_ask_user left this list on 29 September: its question now waits in the menu bar icon. computer_launch joins the quiet side with background: true - the app starts behind what you are doing. quit, window, drag and paste have not been rebuilt yet.

One thing it does to other apps, and you should know it. To find a password field in Slack, Notion or Discord at all, Chromium's accessibility tree has to be on - so taking a screenshot switches it on in every app on screen, not only the one you asked about. Without that, the blackout is blind in exactly the apps where people type passwords. The cost is carried by those apps: they build and keep a tree they would not otherwise have. CMCP_INGEN_ELECTRON=1 turns it off, and the blackout is then blind there.

The honest limit that does not change: this is a promise about what we do. Press a button and the app may open a window of its own; that is its choice, not ours.
Password managers, in the tree as well as the imagemeasuredScreenshots black out Keychain Access, Apple Passwords, 1Password, Bitwarden, LastPass and Dashlane entirely. Until today the accessibility tree did not: computer_inspect and computer_find would hand over the contents of the very window the image hides - as a read, so with no consent, in read-only mode, and in background mode. Anything not marked as a secure field came with it: a revealed password sitting in a static field, a one-time code, a secure note, every username. Both now stop at the app: find returns nothing, inspect returns one node saying denied and why - silence with a reason beats silence. Measured on a running app added to the list: 3 matches and 33 nodes became 0 and 1, with zero values.
Acting quietly inside an appmeasuredThe quiet route - computer_press and computer_set_value, which work on a window behind another one without moving your pointer - only exists where the app publishes an accessibility tree. Measured on five running apps: Chrome exposes 321 elements across 4 windows, 148 of them things you can actually press or type into. An Electron app measured before its tree was switched on exposed its 2 windows and 16 elements inside them, none usable; with the tree on, the same two windows gave 728 buttons. Finder's desktop window is 103 icons and nothing pressable. Where the tree is empty the only route left is a screenshot and a click by coordinate - which does move the real cursor. So "works on a window behind another one" is true of the app, not of every app.
Reaching a full-screen appmeasuredcomputer_space switches desktop the way Control+Arrow does, because macOS gives every full-screen window its own desktop - a window the agent cannot find is often simply on another one. It is the one tool here that deliberately does move what you are looking at, so it asks every time, in every mode, like quitting an app. Two honest limits, both measured: if the system shortcut for switching desktops is off on this machine it says so and sends nothing rather than pressing a dead key; and it reports whether the desktop actually changed by comparing which windows are on screen before and after, so it never claims a switch it cannot prove. Two earlier ways of checking that were wrong - counting accessibility windows drifted 10 to 8 with nothing changing, and the system notification never reaches a short-lived command-line process.
Windows or Linuxwill notmacOS only, and none planned. The accessibility model is the product.
-

The thirty tools

+

The thirty-two tools

- +
Look (14)computer_permissions · computer_record · computer_apps · computer_windows · computer_displays · computer_menus · computer_screenshot · computer_inspect · computer_pending · computer_audit · computer_find · computer_wait_for · computer_focused · computer_learning
Touch (16)computer_launch · computer_quit · computer_drag · computer_space · computer_paste · computer_window · computer_menu · computer_set_value · computer_press · computer_ask_user · computer_click · computer_move · computer_scroll · computer_type · computer_key · computer_activate
Touch (18)computer_request_screen · computer_launch · computer_open · computer_quit · computer_drag · computer_space · computer_paste · computer_window · computer_menu · computer_set_value · computer_press · computer_ask_user · computer_click · computer_move · computer_scroll · computer_type · computer_key · computer_activate

In CMCP_MODE=readonly only the fourteen reading tools are listed at all. A model that can see a tool will eventually try it, and a refusal per call is noise.

diff --git a/docs/docs/install-claude-code/index.html b/docs/docs/install-claude-code/index.html index cc0407b..52e0827 100644 --- a/docs/docs/install-claude-code/index.html +++ b/docs/docs/install-claude-code/index.html @@ -91,7 +91,7 @@

Install Computer MCP for Claude Code

What you get today, honestly. npx currently serves -0.1.0, which has 12 tools. The code in the repository has 30: menu bar +0.1.0, which has 12 tools. The code in the repository has 32: menu bar access, window control, moving windows between screens, pasting, opening and quitting apps, waiting for something to appear, writing into a field behind another window, and asking you for a password without the model ever seeing it. @@ -108,7 +108,7 @@

Install Computer MCP for Claude Code

Start in read-only. Set CMCP_MODE=readonly for the first run and -the sixteen write tools are not even listed to the model. It can look at your screen and it +the eighteen write tools are not even listed to the model. It can look at your screen and it cannot touch anything. You see the whole tool list before you grant anything, and you can move to ask when you have. Step 1b below shows where the setting goes.

@@ -202,7 +202,7 @@

FAQ

Is there a Windows or Linux build?

No, and there is no plan for one. The three guarantees are built on macOS APIs: the Accessibility API for what an element is, and ScreenCaptureKit for the screenshot. A port would be a different product with different guarantees, not the same one recompiled.

Related

-
All 30 tools +
All 32 tools What it can and cannot reach Threat model MCP security checklist diff --git a/docs/docs/install-claude-desktop/index.html b/docs/docs/install-claude-desktop/index.html index e83eeb9..1833a4c 100644 --- a/docs/docs/install-claude-desktop/index.html +++ b/docs/docs/install-claude-desktop/index.html @@ -91,7 +91,7 @@

Install Computer MCP for Claude Desktop

What you get today, honestly. npx currently serves -0.1.0, which has 12 tools. The code in the repository has 30: menu bar +0.1.0, which has 12 tools. The code in the repository has 32: menu bar access, window control, moving windows between screens, pasting, opening and quitting apps, waiting for something to appear, writing into a field behind another window, and asking you for a password without the model ever seeing it. @@ -108,7 +108,7 @@

Install Computer MCP for Claude Desktop

Start in read-only. Set CMCP_MODE=readonly for the first run and the -sixteen write tools are not even listed to the model. It can look at your screen and it cannot +eighteen write tools are not even listed to the model. It can look at your screen and it cannot touch anything. You see the whole tool list before you grant anything.

@@ -229,7 +229,7 @@

FAQ

Is there a Windows or Linux build?

No, and there is no plan for one. The three guarantees are built on macOS APIs: the Accessibility API for what an element is, and ScreenCaptureKit for the screenshot. A port would be a different product with different guarantees, not the same one recompiled.

Related

-
All 30 tools +
All 32 tools What it can and cannot reach When not to use it Uninstall diff --git a/docs/docs/install-codex/index.html b/docs/docs/install-codex/index.html index 11974e6..6d54060 100644 --- a/docs/docs/install-codex/index.html +++ b/docs/docs/install-codex/index.html @@ -91,7 +91,7 @@

Install Computer MCP for OpenAI Codex

What you get today, honestly. npx currently serves -0.1.0, which has 12 tools. The code in the repository has 30: menu bar +0.1.0, which has 12 tools. The code in the repository has 32: menu bar access, window control, moving windows between screens, pasting, opening and quitting apps, waiting for something to appear, writing into a field behind another window, and asking you for a password without the model ever seeing it. @@ -108,7 +108,7 @@

Install Computer MCP for OpenAI Codex

Start in read-only. Set CMCP_MODE=readonly for the first run and -the sixteen write tools are not even listed to the model. It can look at your screen and it +the eighteen write tools are not even listed to the model. It can look at your screen and it cannot touch anything. You see the whole tool list before you grant anything, and you can move to ask when you have. Step 1b below shows where the setting goes.

@@ -202,7 +202,7 @@

FAQ

Is there a Windows or Linux build?

No, and there is no plan for one. The three guarantees are built on macOS APIs: the Accessibility API for what an element is, and ScreenCaptureKit for the screenshot. A port would be a different product with different guarantees, not the same one recompiled.

Related

-
All 30 tools +
All 32 tools What it can and cannot reach Threat model MCP security checklist diff --git a/docs/docs/install-copilot/index.html b/docs/docs/install-copilot/index.html index 84de403..fd4551e 100644 --- a/docs/docs/install-copilot/index.html +++ b/docs/docs/install-copilot/index.html @@ -91,7 +91,7 @@

Install Computer MCP for GitHub Copilot

What you get today, honestly. npx currently serves -0.1.0, which has 12 tools. The code in the repository has 30: menu bar +0.1.0, which has 12 tools. The code in the repository has 32: menu bar access, window control, moving windows between screens, pasting, opening and quitting apps, waiting for something to appear, writing into a field behind another window, and asking you for a password without the model ever seeing it. @@ -108,7 +108,7 @@

Install Computer MCP for GitHub Copilot

Start in read-only. Set CMCP_MODE=readonly for the first run and the -sixteen write tools are not even listed to the model. It can look at your screen and it cannot +eighteen write tools are not even listed to the model. It can look at your screen and it cannot touch anything. You see the whole tool list before you grant anything.

@@ -228,7 +228,7 @@

FAQ

Is there a Windows or Linux build?

No, and there is no plan for one. The three guarantees are built on macOS APIs: the Accessibility API for what an element is, and ScreenCaptureKit for the screenshot. A port would be a different product with different guarantees, not the same one recompiled.

Related

-
All 30 tools +
All 32 tools What it can and cannot reach When not to use it Uninstall diff --git a/docs/docs/install-cursor/index.html b/docs/docs/install-cursor/index.html index 96986f8..f006376 100644 --- a/docs/docs/install-cursor/index.html +++ b/docs/docs/install-cursor/index.html @@ -91,7 +91,7 @@

Install Computer MCP for Cursor

What you get today, honestly. npx currently serves -0.1.0, which has 12 tools. The code in the repository has 30: menu bar +0.1.0, which has 12 tools. The code in the repository has 32: menu bar access, window control, moving windows between screens, pasting, opening and quitting apps, waiting for something to appear, writing into a field behind another window, and asking you for a password without the model ever seeing it. @@ -108,7 +108,7 @@

Install Computer MCP for Cursor

Start in read-only. Set CMCP_MODE=readonly for the first run and -the sixteen write tools are not even listed to the model. It can look at your screen and it +the eighteen write tools are not even listed to the model. It can look at your screen and it cannot touch anything. You see the whole tool list before you grant anything, and you can move to ask when you have. Step 1b below shows where the setting goes.

@@ -215,7 +215,7 @@

FAQ

Is there a Windows or Linux build?

No, and there is no plan for one. The three guarantees are built on macOS APIs: the Accessibility API for what an element is, and ScreenCaptureKit for the screenshot. A port would be a different product with different guarantees, not the same one recompiled.

Related

-
All 30 tools +
All 32 tools What it can and cannot reach Threat model MCP security checklist diff --git a/docs/docs/install-vscode/index.html b/docs/docs/install-vscode/index.html index 2a6ad8b..5c73fd2 100644 --- a/docs/docs/install-vscode/index.html +++ b/docs/docs/install-vscode/index.html @@ -91,7 +91,7 @@

Install Computer MCP for VS Code

What you get today, honestly. npx currently serves -0.1.0, which has 12 tools. The code in the repository has 30: menu bar +0.1.0, which has 12 tools. The code in the repository has 32: menu bar access, window control, moving windows between screens, pasting, opening and quitting apps, waiting for something to appear, writing into a field behind another window, and asking you for a password without the model ever seeing it. @@ -108,7 +108,7 @@

Install Computer MCP for VS Code

Start in read-only. Set CMCP_MODE=readonly for the first run and the -sixteen write tools are not even listed to the model. It can look at your screen and it cannot +eighteen write tools are not even listed to the model. It can look at your screen and it cannot touch anything. You see the whole tool list before you grant anything.

@@ -218,7 +218,7 @@

FAQ

Is there a Windows or Linux build?

No, and there is no plan for one. The three guarantees are built on macOS APIs: the Accessibility API for what an element is, and ScreenCaptureKit for the screenshot. A port would be a different product with different guarantees, not the same one recompiled.

Related

-
All 30 tools +
All 32 tools What it can and cannot reach When not to use it Uninstall diff --git a/docs/index.html b/docs/index.html index d4b3c8f..530e8fb 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,7 +6,7 @@ Computer MCP - computer use you can actually leave running - + @@ -183,8 +183,10 @@

What still asks, and

Install

-

What you get today, honestly. npx @agent360/computer-mcp currently serves 0.1.0, which has 12 tools. The 30 tools described here are the source: they are built and tested, but not published yet. Building from source takes about thirty-five seconds if you want them now.

-

Add it to your MCP client, grant two macOS permissions, done. It starts ready to work: twenty-two of the thirty, no asking, except your password manager, anything that deletes or clears, and starting a screen recording. It runs in the background by default, so nothing takes your screen; CMCP_BACKGROUND=0 adds the eight that do.

+

What you get today, honestly. npx @agent360/computer-mcp currently serves 0.1.0, which has 12 tools. The 32 tools described here are the source: they are built and tested, but not published yet. Building from source takes about thirty-five seconds if you want them now.

+ + +

Add it to your MCP client, grant two macOS permissions, done. It starts ready to work: twenty-five of the thirty-two, no asking, except your password manager, anything that deletes or clears, and starting a screen recording. It runs in the background by default, so nothing takes your screen; CMCP_BACKGROUND=0 adds the seven that do.

@@ -270,8 +272,8 @@

No API key, no se
-

thirty tools

-

fourteen that look, sixteen that touch. Grouped by what you would +

thirty-two tools

+

fourteen that look, eighteen that touch. Grouped by what you would actually want, with the names underneath in case you are wiring this up yourself.

Claude Codeclaude mcp add computer -- npx -y @agent360/computer-mcp
Cursor / VS Code / CodexIn mcp.json: {"command":"npx","args":["-y","@agent360/computer-mcp"]}
@@ -291,7 +293,8 @@

thirty tools

hundred screenshots of a progress bar, and you pay for every one of them. + box and asks you to type - in the menu bar icon, so nothing jumps in front of you. + It gets back "done" or "cancelled" - never the text. diff --git a/docs/learn/computer-use-mcp/index.html b/docs/learn/computer-use-mcp/index.html index f3520aa..00eed09 100644 --- a/docs/learn/computer-use-mcp/index.html +++ b/docs/learn/computer-use-mcp/index.html @@ -262,7 +262,7 @@

Related

MCP security: what a server can reachLeaving an agent running overnightWhen not to use it -All 30 tools +All 32 tools

Computer MCP is built by Agent360, who also make diff --git a/docs/learn/owasp-mcp-top-10/index.html b/docs/learn/owasp-mcp-top-10/index.html index 0b10be3..c9081c9 100644 --- a/docs/learn/owasp-mcp-top-10/index.html +++ b/docs/learn/owasp-mcp-top-10/index.html @@ -127,7 +127,7 @@

The ten, and what we do about each

+talked into trying. Measured: 32 tools in full mode, 11 in read-only.
So it can...What that means
Ask you
ask_user
When it hits a password or a code from your phone, it puts the cursor in the - box and asks you to type. It gets back "done" or "cancelled" - never the text.
Show its work
audit · permissions
Read back everything it did, in order. What you typed is stored as a fingerprint, never as readable text.
addressed Tools carry a tier, and CMCP_MODE=readonly does not merely refuse the write tools - it does not list them. A tool the model cannot see is a tool it cannot be -talked into trying. Measured: 30 tools in full mode, 11 in read-only.
MCP03Tool Poisoning not our layer This is about a malicious server diff --git a/docs/llms-install.md b/docs/llms-install.md index d66fc48..69e878f 100644 --- a/docs/llms-install.md +++ b/docs/llms-install.md @@ -9,12 +9,14 @@ finished until `computer_permissions` confirms it. Computer MCP drives macOS itself: it reads the accessibility tree, takes screenshots, clicks, types, presses buttons, chooses menu items and moves -windows. The source has **30 tools, 14 of them read-only**, no API key, and +windows. The source has **32 tools, 14 of them read-only**, no API key, and no network calls of its own. -> **What you get today, honestly.** `npx @agent360/computer-mcp` currently serves **0.1.0**, which has 12 tools. The 30 tools described here are the source: they are built and tested, but not published yet. Building from source takes about thirty-five seconds if you want them now. -The design premise is that it is safe to leave running: password fields are blacked out in memory +> **What you get today, honestly.** `npx @agent360/computer-mcp` currently serves **0.1.0**, which has 12 tools. The 32 tools described here are the source: they are built and tested, but not published yet. Building from source takes about thirty-five seconds if you want them now. + + +The design premise is that it is safe to leave running: password fields are blacked out in memory before a screenshot is written to disk, writes go through a consent gate, and every action lands in a log the server only appends to - every line carries a fingerprint of itself and the one before it, so a removed or edited line breaks the chain and says where that never stores typed text in clear. diff --git a/docs/llms.txt b/docs/llms.txt index bfbdaf4..8b94cfc 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -6,10 +6,12 @@ > log that only grows. No account, no API key, no model inside it. MIT. # FORBEHOLD -VERSION: npx serves 0.1.0, which has 12 tools. The 30 tools described below +VERSION: npx serves 0.1.0, which has 12 tools. The 32 tools described below are the source: they are built and tested but not published yet. Do not tell a user that a tool is available after an npx install unless it is one of the twelve. -# /FORBEHOLDInstall: `npx @agent360/computer-mcp` (ships a signed universal binary; no Swift needed) +# /FORBEHOLD + +Install: `npx @agent360/computer-mcp` (ships a signed universal binary; no Swift needed) Repository: https://github.com/Agent360dk/computerMCP npm: @agent360/computer-mcp Registry name: io.github.Agent360dk/computer-mcp @@ -52,7 +54,7 @@ for, because that machine also holds passwords, keys and production terminals. password managers still ask every time, terminals and editors once per session, and that is not configurable -## The 30 tools, 14 of them read-only +## The 32 tools, 14 of them read-only Look: computer_screenshot, computer_record, computer_inspect, computer_find, computer_wait_for, computer_focused, computer_apps, computer_windows, computer_displays, @@ -61,7 +63,7 @@ computer_permissions, computer_pending computer_audit computer_learning -Touch: computer_launch, computer_quit, computer_paste, computer_window, computer_space, computer_menu, computer_press, computer_set_value, computer_ask_user, computer_click, +Touch: computer_request_screen, computer_launch, computer_open, computer_quit, computer_paste, computer_window, computer_space, computer_menu, computer_press, computer_set_value, computer_ask_user, computer_click, computer_type, computer_key, computer_scroll, computer_move, computer_drag, computer_activate computer_ask_user returns only true or false - never text. It is how a human diff --git a/docs/tools.html b/docs/tools.html index c4c2778..930ba32 100644 --- a/docs/tools.html +++ b/docs/tools.html @@ -4,7 +4,7 @@ Tool reference - Computer MCP - + @@ -71,11 +71,13 @@

Tool reference

-

thirty tools. fourteen look, sixteen touch. The useful part of this page is not the parameter lists - it is knowing which tool to reach for when the obvious one is the wrong one.

+

thirty-two tools. fourteen look, eighteen touch. The useful part of this page is not the parameter lists - it is knowing which tool to reach for when the obvious one is the wrong one.

-

What you get today, honestly. npx @agent360/computer-mcp currently serves 0.1.0, which has 12 tools. The 30 tools described here are the source: they are built and tested, but not published yet. Building from source takes about thirty-five seconds if you want them now.

-
+

What you get today, honestly. npx @agent360/computer-mcp currently serves 0.1.0, which has 12 tools. The 32 tools described here are the source: they are built and tested, but not published yet. Building from source takes about thirty-five seconds if you want them now.

+ + +

The one habit that matters. Find things by name, not by pixel. computer_inspect and computer_find return the frame of every element, so the agent can act on "the Log in button" rather than on a coordinate that stops being true the moment a window moves. Reach for computer_click only when there is no accessibility element to aim at: a canvas, a map, an image.

@@ -98,6 +100,10 @@

computer_set_valuewrite

Write a value straight into a field through the accessibility API: no focus change, no keystrokes, no cursor movement. It reaches a field in a window sitting behind another one, which typing cannot.

It refuses on a secure field, every time. We removed that check once on purpose to see what happened: the modified build wrote into the password box. It is the only thing standing there.

+

computer_request_screenwrite

+

Ask the person to lend this one agent the screen, for the few steps that cannot be done from behind: moving the pointer, dragging, bringing a window forward. They approve it in the menu bar icon with Touch ID, for at most 15 minutes.

+

The screen stays theirs. While the agent has it, each step waits while they are using the keyboard or mouse (a step already running finishes), and Take the screen back now in the menu ends the loan at once and stops a step still running. The loan is the open connection to the icon: when it closes, the loan is over. With CMCP_BACKGROUND set, nothing can borrow it.

+

computer_ask_userwrite

Ask the person to do something themselves, and wait. Use it for anything the model must not see: a password, a 2FA code, a CAPTCHA, an OAuth consent.

The agent puts the cursor in the right field first, then calls this. A native dialog names the app and the window it is about to land in - written by the server, not by the model - and the person types on their own keyboard.

@@ -112,6 +118,10 @@

computer_launchwrite

Open an application, by bundle ID or by its plain name. computer_activate can only bring forward something already running; this starts what is closed. Already running, it just comes forward, and the answer says which of the two happened.

Launching cannot lose anything, so it is not on the always-ask list the way quitting is - but it is still a write, and it goes through the same gate as every other write: refused in read-only mode, and in ask mode the first one asks for the session. Quitting is separate because it can lose work, and it asks every single time.

+

computer_openwrite

+

Open an app's own door in the background - a Spotify track, a WhatsApp chat, or the app itself - via the app's own URL, never a send. You give an intent and its one parameter, not a URL: the server builds a fixed, validated URL for that intent, and anything else is refused.

+

It carries navigation only. To send a message you still act on the chat once it is open, and the person confirms it - opening a chat is not sending into it.

+

computer_quitwrite

Ask an application to quit, the same way Cmd+Q does. It never kills a process: the app still gets to ask you about unsaved work, and an agent should trigger that question rather than route around it.

Quitting can lose work, so this asks every time, in every mode - like closing a window, and like a menu item that looks destructive.

diff --git a/docs/use-cases/unattended-runs/index.html b/docs/use-cases/unattended-runs/index.html index 1b01e66..acac5a9 100644 --- a/docs/use-cases/unattended-runs/index.html +++ b/docs/use-cases/unattended-runs/index.html @@ -166,7 +166,7 @@

FAQ

What should I do before starting a long run?

Look at your own screen. Close terminals with credentials in the scrollback and quit the password manager. The redaction covers what macOS marks as secure; it cannot cover a secret sitting in a plain text window.

Related

diff --git a/docs/use-cases/without-taking-over-your-screen/index.html b/docs/use-cases/without-taking-over-your-screen/index.html index 69ee5a5..ca037fb 100644 --- a/docs/use-cases/without-taking-over-your-screen/index.html +++ b/docs/use-cases/without-taking-over-your-screen/index.html @@ -8,15 +8,15 @@ - + - + - + +

Kontaktark · hver use case, før og efter (B3)

+

Trukket automatisk fra filmene i dette CI-job. Intet er optaget på Gustavs Mac.

+{''.join(rows)}
ScenarieBaggrundForgrund
+

Parallelle agenter

+
{par_rows}
+""" + (ud_dir / "index.html").write_text(html, encoding="utf-8") + print(f"Kontaktark skrevet: {ud_dir / 'index.html'} ({len(use_cases)} scenarier, {len(parallelle)} parallel-film)") + + +if __name__ == "__main__": + main() diff --git a/scripts/release.sh b/scripts/release.sh index d495680..0b15475 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -50,6 +50,22 @@ if [ -n "$(git status --porcelain)" ]; then exit 1 fi +# ⛔ OPUS (2/10): uden dette tjek kan lokal main staa paa en commit origin/main +# IKKE har (fx en anden chats traee) - scriptet ville taenke HEAD'en v$V, +# skubbe maerket offentligt paa en forkert commit, og saa faa SELVE +# main-pushet nedenfor afvist af branch-beskyttelsen. Maerket ville alligevel +# staa derude, forkert. Stop foer noget som helst skubbes. +git fetch origin main --quiet +HEAD_NU="$(git rev-parse HEAD)" +ORIGIN_MAIN="$(git rev-parse origin/main)" +if [ "$HEAD_NU" != "$ORIGIN_MAIN" ]; then + echo "⛔ HEAD ($HEAD_NU) er ikke det samme som origin/main ($ORIGIN_MAIN)." + echo " Udgivelsen maerker og pakker HEAD - er den ikke identisk med origin/main," + echo " maerkes/pakkes en commit GitHub ikke har endnu (eller en forkert én)." + echo " git checkout main && git pull - og koer saa scriptet igen." + exit 1 +fi + echo "== 0/7 teksten skal beskrive DEN version vi udgiver ==" echo "$V" > "$ROOT/PUBLICERET" python3 "$ROOT/scripts/sync-tal.py" | sed 's/^/ /' @@ -175,7 +191,7 @@ N=$(node -e "import('./mcp-server/tools.js').then(m=>console.log(m.TOOLS.length) # streng, og vagten ville have afvist hver eneste fil - eller vaerre, # matchet paa ingenting. En vagt med en graense skal naa laengere end # det den vogter. -WORDS="zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty-one twenty-two twenty-three twenty-four twenty-five twenty-six twenty-seven twenty-eight twenty-nine thirty" +WORDS="zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty twenty-one twenty-two twenty-three twenty-four twenty-five twenty-six twenty-seven twenty-eight twenty-nine thirty thirty-one thirty-two thirty-three thirty-four thirty-five thirty-six thirty-seven thirty-eight thirty-nine forty" WORD=$(echo "$WORDS" | cut -d' ' -f$((N+1))) bad=0 for f in README.md docs/index.html docs/tools.html docs/llms.txt; do @@ -251,7 +267,11 @@ echo "== 5/7 maerk og skub FOER der udgives ==" # fortrydes, foerst. npm whoami >/dev/null 2>&1 || { echo "⛔ npm-tokenen er ikke gyldig. Gustav skal lave en ny (2FA)."; exit 1; } git tag -a "v$V" -m "v$V" -git push origin main --tags +# ⛔ OPUS (2/10): "main" her var meningsløst OG farligt - HEAD er lige verificeret +# identisk med origin/main ovenfor, saa der er intet nyt at skubbe til main; +# og en protected branch afviser alligevel et direkte "push origin main". +# Det eneste dette skridt reelt skal skubbe, er maerket. +git push origin "refs/tags/v$V" # ⛔ 25/9 (Astra): her stod `... 2>/dev/null || echo "(fandtes i forvejen)"` - enhver # fejl (login, netvaerk, rettigheder) blev meldt som at udgivelsen fandtes. if gh release view "v$V" >/dev/null 2>&1; then @@ -282,10 +302,28 @@ echo " PUBLICERET staar paa $V, og forbeholdet er fjernet fra alle flader." # Kun de filer sync-tal aendrede - traeet var rent foer vi startede. AENDREDE=$(git status --porcelain | awk '{print $2}') if [ -n "$AENDREDE" ]; then + # ⛔ OPUS (2/10): "git push origin main" her blev altid afvist - main er + # protected (PR kraeves, enforce_admins=true). Faelden er allerede afvaebnet + # (npm ER udgivet), saa et set -e-stop her ville efterlade en uklar + # halvfaerdig tilstand. En gren + PR er det eneste der rent faktisk virker. + GREN="release-$V-dok" + git checkout -q -b "$GREN" git add $AENDREDE git commit -q -m "release: $V er udgivet - PUBLICERET og siderne foelger med" - git push origin main - echo " ✓ PUBLICERET og siderne committet og skubbet" + git push -q origin "$GREN" + if PR_URL=$(gh pr create --base main --head "$GREN" \ + --title "release: $V er udgivet - forbeholdene vaek" \ + --body "npm og GitHub-udgivelsen for $V er allerede ude (irreversibelt). Denne PR synkroniserer kun PUBLICERET og de afledte sider - ingen ny funktionalitet." \ + 2>&1); then + echo " ✓ PUBLICERET og siderne committet, skubbet til $GREN:" + echo " $PR_URL" + echo " Merge den PR for at faa teksten med paa main - npm-udgivelsen venter ikke paa den." + else + echo "⛔ PR-opret fejlede, men commit'en og grenen er skubbet og trygge:" + echo "$PR_URL" | sed 's/^/ /' + echo " Aabn PR'en i haanden: gh pr create --base main --head $GREN" + fi + git checkout -q main fi echo "== 7/7 MCP-registret ==" @@ -294,14 +332,9 @@ echo "== 7/7 MCP-registret ==" # fire steder». Hvert skridt for sig, og en fejl er en fejl. mcp-publisher login github || { echo "⛔ login til MCP-registret fejlede - npm ER udgivet, registret er IKKE"; exit 1; } mcp-publisher publish || { echo "⛔ MCP-registret afviste udgivelsen - npm ER udgivet, registret er IKKE"; exit 1; } -# ⛔ Y4c. Repo-beskrivelsen er den streng hvert katalog hoester. Staar der et -# vaerktoejstal, skal det aendres i SAMME oejeblik som pakken - ikke foer -# (saa lyver den for npx-brugere) og ikke efter (saa lyver den for alle). -DESC=$(gh api repos/Agent360dk/computerMCP --jq .description 2>/dev/null) -case "$DESC" in - *" $N tools"*) echo " ✓ repo-beskrivelsen siger allerede $N" ;; - *) echo " ⚠️ repo-beskrivelsen siger ikke '$N tools' - ret den nu:" - echo " gh repo edit Agent360dk/computerMCP --description \"...$N tools...\"" ;; -esac +# ⛔ Fjernet 2/10: et vaerktoejstal i repo-beskrivelsen blev bevidst fjernet +# 2/10, fordi tallet var forkert baade mod koden og mod npm. Dette script +# genindsatte det ved hver udgivelse - en regression mod den beslutning. +# Repo-beskrivelsen roeres ikke her laengere. echo "✅ $V er ude fire steder. Tjek: npm view @agent360/computer-mcp version" diff --git a/scripts/sync-tal.py b/scripts/sync-tal.py index 7bf2e20..fdb1565 100755 --- a/scripts/sync-tal.py +++ b/scripts/sync-tal.py @@ -18,7 +18,9 @@ 'eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen', 'eighteen','nineteen','twenty','twenty-one','twenty-two','twenty-three', 'twenty-four','twenty-five','twenty-six','twenty-seven','twenty-eight', - 'twenty-nine','thirty'] + 'twenty-nine','thirty','thirty-one','thirty-two','thirty-three', + 'thirty-four','thirty-five','thirty-six','thirty-seven','thirty-eight', + 'thirty-nine','forty'] ud = subprocess.run(['node','-e', "import('./mcp-server/tools.js').then(m=>console.log(JSON.stringify(" @@ -195,7 +197,7 @@ def _forbehold(fil, udgivet, n): .replace('npx @agent360/computer-mcp', 'npx @agent360/computer-mcp') + '

') if fil.endswith('.txt'): - return (f'VERSION: npx serves {udgivet}, which has 12 tools. The {n} tools described below\n' + return (f'VERSION: npx serves {udgivet}, which has 12 tools. The {n} tools described below are\n' f'the source: they are built and tested but not published yet. Do not tell a user\n' f'that a tool is available after an npx install unless it is one of the twelve.') return '> **What you get today, honestly.** ' + lang @@ -234,9 +236,16 @@ def _forbehold(fil, udgivet, n): # aendrede noget. En mekanik der ikke kan koeres to gange uden # forskel, er ikke idempotent - og saa kan man ikke se hvad der er # ens arbejde og hvad der er scriptets stoej. + # ⛔ FUNDET AF FORSIDE-CHATTEN (2/10): `.rstrip('\n')` her gjorde `hale` + # ALTID tom ('\n'.rstrip('\n') == ''), uanset hvad linjen ovenfor lige + # havde regnet ud. Linjeskiftet efter lukkemarkøren forsvandt, så + # og det der fulgte stod på samme linje - GitHub + # og npm viste markøren som rå tekst i stedet for en skjult HTML- + # kommentar. `hale` er allerede den rigtige streng; den skal bruges + # som den er, ikke strippes. hale = '\n' if t3[m3.end():].startswith('\n') else '\n\n' ny3 = (t3[:m3.start()] + aaben + '\n' + _forbehold(f, UDGIVET, N) + '\n' - + luk + hale.rstrip('\n') + t3[m3.end():]) + + luk + hale + t3[m3.end():]) if ny3 != t3: io.open(p3,'w',encoding='utf-8').write(ny3) print((' forbeholdet sat ind igen: ' if not indhold else ' forbeholdet skrevet om: ') + f) diff --git a/test/baggrund-stille.mjs b/test/baggrund-stille.mjs index c1f4cfa..a4147e2 100644 --- a/test/baggrund-stille.mjs +++ b/test/baggrund-stille.mjs @@ -25,6 +25,7 @@ let sprungetHer = 0; const fails = []; const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; +const SPOERGER = lavFalskSpoerger('udloeb', 'cmcp-baggrund-spoerger'); const env = { ...process.env, CMCP_HELPER: VAGT.sti, @@ -44,7 +45,9 @@ const env = { // menneskets skaerm. Betalt to gange i dag paa den anden side af samme // regel - en proeve der skrev i hans vindue. Attrappen er ikke betinget // af noget flag: en boks man kan komme til at vise, bliver vist. - CMCP_OSASCRIPT: lavFalskSpoerger('udloeb', 'cmcp-baggrund-spoerger').sti, + CMCP_OSASCRIPT: SPOERGER.sti, + // Intet rigtigt ikon i menneskets menulinje - heller ikke koert uden run-all.sh. + CMCP_STATUS_IKON: '0', }; const srv = spawn('node', [join(ROOT, 'mcp-server', 'index.js')], { env, stdio: ['pipe', 'pipe', 'pipe'] }); let buf = ''; @@ -79,10 +82,23 @@ try { mangler.length ? 'mangler: ' + mangler.join(', ') : fire.join(', ')); // 2. ...og de der ALDRIG kan goeres stille, gemmes stadig. - const skal_vaere_vaek = ['computer_move', 'computer_activate', 'computer_space', 'computer_ask_user']; + // ⛔ 29/9: `computer_ask_user` er IKKE laengere blandt dem. I baggrund + // spoerger den via menulinje-ikonet i stedet for med en dialog (panelet 29/9). + const skal_vaere_vaek = ['computer_move', 'computer_activate', 'computer_space']; const slap_igennem = skal_vaere_vaek.filter(x => navne.includes(x)); check('de der altid tager skaermen er stadig skjult', slap_igennem.length === 0, - slap_igennem.join(', ') || 'move, activate, space, ask_user er vaek'); + slap_igennem.join(', ') || 'move, activate, space er vaek'); + + // 2b. ask_user tilbydes - og tager ALDRIG skaermen: uden `app` afvist, uden ikon + // afvist, og aldrig en osascript-dialog. + check('ask_user tilbydes i baggrund (den spoerger via ikonet)', navne.includes('computer_ask_user')); + const spUden = await rpc('tools/call', { name: 'computer_ask_user', arguments: { message: 'Type the 2FA code' } }); + const tA = JSON.stringify(spUden.result ?? spUden.error ?? {}); + check('ask_user uden app: afvist, og siger at app skal navngives', /Refused/.test(tA) && /name the `app`/.test(tA), tA.slice(0, 120)); + const spIkon = await rpc('tools/call', { name: 'computer_ask_user', arguments: { message: 'Type the 2FA code', app: 'Finder' } }); + const tB = JSON.stringify(spIkon.result ?? spIkon.error ?? {}); + check('ask_user uden ikon: afvist, intet bragt frem', /Refused/.test(tB) && /menu bar icon is not running/.test(tB), tB.slice(0, 120)); + check('...og ingen dialog blev rejst i baggrund', SPOERGER.gangeSpurgt() === 0, `${SPOERGER.gangeSpurgt()} dialoger`); // 3. Uden `app` afvises kaldet - og afvisningen skal SIGE hvad man goer. const uden = await rpc('tools/call', { name: 'computer_type', arguments: { text: 'x' } }); diff --git a/test/brugsscenarier.mjs b/test/brugsscenarier.mjs index 689ce4d..f919145 100644 --- a/test/brugsscenarier.mjs +++ b/test/brugsscenarier.mjs @@ -33,10 +33,16 @@ import { tmpdir, homedir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { lavFalskSpoerger, lavVagtHjaelper } from './falsk-hjaelper.mjs'; +import { startFilm } from './film.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const AEGTE = join(ROOT, 'mcp-server', 'vendor', 'cmcp-helper'); process.env.CMCP_STATUS_IKON = '0'; // aldrig det rigtige ikon i menneskets menulinje +// PYNT (2/10, forside-chatten): kun scenarie 5's URL/titel skifter - samme tjek, +// samme scenarie-nummer, samme browserSide()-mekanik. Netflix' plakater er +// ophavsretligt beskyttede; en ren optagelse til computermcp.dev bruger en +// neutral side i stedet. +const PYNT = process.env.CMCP_FILM_PYNT === '1'; const vent = (ms) => new Promise(r => setTimeout(r, ms)); const tilfaeldig = () => Math.random().toString(36).slice(2, 8); @@ -72,13 +78,15 @@ export function maaKoere(s, env, aabentAllerede = false) { // EN FRISK SERVER pr. scenarie: egen tilstandsmappe, egen revisionslog, og et // sikkerhedsnet der kun lader handlinger nå scenariets egne programmer. // --------------------------------------------------------------------------- -async function nyServer({ tilladte, forgrund }) { +async function nyServer({ tilladte, forgrund, delt }) { const vagt = lavVagtHjaelper(process.env.CMCP_HELPER || AEGTE); vagt.tillad(...tilladte); // Samtykke: prøven svarer ja - men kun scenariets egne programmer kan nå // hjælperen, og hvert spørgsmål står i rapporten bagefter. const spoerger = lavFalskSpoerger('ja', 'cmcp-brug'); - const state = mkdtempSync(join(tmpdir(), 'cmcp-brug-')); + // 1/10 (konsulent-panelet): parallel-proeven deler EN state-mappe, som rigtige + // chats paa én Mac goer - ellers proeves programlaasen og revisionskaeden ikke. + const state = delt || mkdtempSync(join(tmpdir(), 'cmcp-brug-')); const srv = spawn('node', [join(ROOT, 'mcp-server/index.js')], { env: { ...process.env, CMCP_HELPER: vagt.sti, CMCP_STATE_DIR: state, CMCP_OSASCRIPT: spoerger.sti, CMCP_BACKGROUND: forgrund ? '0' : '1' }, @@ -94,7 +102,7 @@ async function nyServer({ tilladte, forgrund }) { let data = null; try { data = JSON.parse(tekst); } catch {} return { fejl: !!r.result?.isError || !!r.error, tekst, data }; }; - const luk = () => { try { srv.kill(); } catch {} rmSync(state, { recursive: true, force: true }); }; + const luk = () => { try { srv.kill(); } catch {} if (!delt) rmSync(state, { recursive: true, force: true }); }; return { kald, luk, vagt, spoerger }; } @@ -319,7 +327,9 @@ export const SCENARIER = [ return `bonden er flyttet: «${m[0].name}»`; }, }, - browserSide(5, 'åbne Netflix', 'https://www.netflix.com/', /Netflix/), + PYNT + ? browserSide(5, 'åbne Netflix', 'https://en.wikipedia.org/wiki/MacOS', /macOS/) + : browserSide(5, 'åbne Netflix', 'https://www.netflix.com/', /Netflix/), { // ⛔ 27/9 på en fremmed Mac: tryk på «Wallpaper» i sidebjælken er tryk på en // tekst og skifter ingen side, og vinduets titel er tom. Indstillinger har @@ -420,14 +430,14 @@ export const SCENARIER = [ async trin(c) { const A = 'com.apple.calculator'; await c.start(A); await c.ventVindue(A); - await c.trykEn(A, [L({ en: 'All Clear', da: 'Ryd alt' }), L({ en: 'Clear', da: 'Ryd' })]); + // 1/10: «All Clear» er en RYD-knap og spoerger nu (som README lover). Et nystartet + // Lommeregner-vindue staar allerede paa 0, saa trykket var aldrig noedvendigt. await c.trykEn(A, ['7']); await c.trykEn(A, [L({ en: 'Add', da: 'Plus' }), 'Plus', '+']); await c.trykEn(A, ['5']); await c.trykEn(A, [L({ en: 'Equals', da: 'Lig med' }), '=']); }, async tjek(c) { const m = await c.ventPaa('com.apple.calculator', { contains: '12' }); return `displayet viser «${m[0].name}»`; }, - async ryd(c) { await c.trykEn('com.apple.calculator', [L({ en: 'All Clear', da: 'Ryd alt' }), L({ en: 'Clear', da: 'Ryd' })]).catch(() => {}); }, }, { // Søgefeltet i Finder og et filnavn er begge AXTextField; kun undertypen @@ -461,16 +471,24 @@ export const SCENARIER = [ // --------------------------------------------------------------------------- // KØRSLEN af ét scenarie: trin -> tjek -> ryd (altid) -> luk det, vi startede. // --------------------------------------------------------------------------- -export async function koerScenarie(s, { forgrund = false } = {}) { +export async function koerScenarie(s, { forgrund = false, film: filmNavn, menneskeArbejder = false, stateDir } = {}) { const spor = [], startede = new Set(), udenfor = [], tog = []; - const srv = await nyServer({ tilladte: s.apps, forgrund }); + // 1/10: hvert rigtigt scenarie filmes paa en fremmed maskine (CMCP_FILM) - selvproevens attrapper ikke. + const film = s.nr > 0 && filmNavn !== false + ? startFilm(filmNavn || `brug-${forgrund ? 'forgrund' : 'baggrund'}-${String(s.nr).padStart(2, '0')}-${s.navn}`) : null; + const srv = await nyServer({ tilladte: s.apps, forgrund, delt: stateDir }); const c = vaerktoej(srv, { forgrund, startede, spor, apps: s.apps, udenfor, tog }); const res = { nr: s.nr, navn: s.navn, status: '', bevis: '', ryd: '', spor }; let fase = 'trin'; // Løftet, målt pr. scenarie: i baggrunden er det program mennesket var i, // stadig forrest bagefter (27/9: Finder kom frem midt i en kørsel, og intet // enkelt svar sagde det). - const forrest = async () => ((await srv.kald('computer_apps')).data?.apps || []).find(a => a.active)?.bundleId; + // ⛔ 1/10 (GitHubs Mac, koersel 36859187191): programlistens «active» var falsk for + // ALLE programmer paa koereren - forrest var undefined, og tjekket blev sprunget + // tavst over i hver eneste koersel. Tilgaengeligheds-lagets fokus (samme kilde som + // hjaelperens egen Skaerm.forrestLige) spoerges foerst; listen er kun en reserve. + const forrest = async () => (await srv.kald('computer_focused')).data?.element?.bundleId + || ((await srv.kald('computer_apps')).data?.apps || []).find(a => a.active)?.bundleId; const forrestFoer = await forrest(); try { await s.trin(c); @@ -484,7 +502,10 @@ export async function koerScenarie(s, { forgrund = false } = {}) { res.status = 'fejlede'; res.bevis = `tog skærmen og gav den ikke tilbage: ${beholdt.map(t => `${t.navn} (${t.hvorfor.slice(0, 80)})`).join('; ')} · ${res.bevis}`; } else if (tog.length) { - res.bevis += ` · ${tog.length}x hentede et program sig selv frem, forgrunden blev givet straks tilbage`; + // MANDAT: at tage skærmen og give den tilbage er stadig at tage skærmen. + // Genopretning er bedre end intet, men den gør ikke et forløb til baggrund. + res.status = 'delvist'; + res.bevis += ` · ${tog.length}x hentede et program sig selv frem (skærmen taget, forgrunden givet straks tilbage) - ikke rent baggrundsforløb`; } } catch (e) { res.status = e instanceof KunForgrund ? 'delvist' : 'fejlede'; @@ -498,31 +519,60 @@ export async function koerScenarie(s, { forgrund = false } = {}) { await vent(2500); const forrestEfter = await forrest(); res.forrest = `${forrestFoer} -> ${forrestEfter}`; - if (forrestFoer && forrestEfter !== forrestFoer) { + // Paa en Mac hvor et menneske arbejder imens (parallel.mjs, egen Mac), skifter + // HAN program. Kun scenariets egne programmer foran er saa scenariets skyld. + if (forrestFoer && forrestEfter !== forrestFoer && (!menneskeArbejder || s.apps.includes(forrestEfter))) { res.status = 'fejlede'; res.bevis = `tog skærmen: ${forrestFoer} var forrest, bagefter ${forrestEfter} · ${res.bevis}`; } + // ⛔ 1/10 (Astra): en manglende førmåling sprang tjekket over og gav «bevist» + // (læser 102: «undefined -> com.apple.DiskUtility»). Umålt er ikke grønt. + if (!forrestFoer || !forrestEfter) { + res.status = 'fejlede'; + res.bevis = `skærmen er umålt: hvad der stod forrest kunne ikke læses (${forrestFoer} -> ${forrestEfter}) · ${res.bevis}`; + } + } + res.tog = tog.length; + // Knapperne læses FØR programmet lukkes - bagefter er der intet at læse (Opus, 1/10). + if (res.status === 'fejlede' || res.status === 'delvist') { + res.knapper = {}; + for (const app of s.apps) { + const m = (await srv.kald('computer_find', { app, role: 'AXButton', limit: 40 })).data?.matches || []; + res.knapper[app] = m.map(x => x.name).filter(Boolean).slice(0, 25); + } } try { res.ryd = (s.ryd ? await s.ryd(c) : '') || ''; } catch (e) { res.ryd = `oprydningen fejlede: ${String(e.message).slice(0, 160)}`; } // Programmer scenariet selv startede, lukkes igen - gennem programmets egen // menu (genvejen er ens på alle sprog), for i baggrunden er computer_quit afvist. + // ⛔ 1/10 (MÅLT på Gustavs Mac): «Slut Skak» blev trykket, Skak spurgte «gem + // partiet?» og blev stående foran ham - og oprydningen meldte intet, fordi den + // kun tjekkede at menupunktet blev trykket. Nu: programmet SKAL være væk. + // Spørger det om at gemme, er det prøvens eget dokument: «Gem ikke». + const vaek = async (app, sek) => { for (let i = 0; i < sek * 2; i++) { if (!(await c.koerer(app))) return true; await vent(500); } return false; }; for (const app of startede) { try { if (forgrund) await c.k('computer_quit', { app }, { maaFejle: true }); else await c.menuGenvej(app, 'cmd+q'); - } catch { res.ryd += ` · ${app} blev ikke lukket igen`; } + } catch {} + if (!(await vaek(app, 6))) { + await c.trykEn(app, ['Gem ikke', "Don't Save"]).catch(() => {}); + // ⛔ 1/10 (GitHubs Mac): i baggrunden lukkes et program ALDRIG uden et menneskes ja - + // med vilje, for at lukke kan tabe arbejde. Proeven lod derfor programmerne staa + // efter hver use case i ugevis, og ingen saa det. Programmet er proevens eget + // (startet af scenariet): proeven lukker det selv, uden om produktet. + if (!(await vaek(app, 2))) { + const pid = ((await srv.kald('computer_apps')).data?.apps || []).find(a => a.bundleId === app)?.pid; + if (pid) { try { process.kill(pid, 'SIGTERM'); } catch {} } + } + if (!(await vaek(app, 4))) { + res.ryd += ` · ${app} blev ikke lukket igen (kører stadig)`; + res.status = 'fejlede'; + res.bevis += ` · oprydningen efterlod ${app} kørende`; + } + } } const rev = (await srv.kald('computer_audit', { limit: 1 })).data; res.revision = rev ? `${rev.total} linjer, kæden ${String(rev.chain).split(' ')[0]}` : 'ingen revisionslog'; - // Fejlede det, eller mangler noget: hvilke knapper programmet viste. Så kan - // næste runde ramme det rigtige element i stedet for at gætte. - if (res.status === 'fejlede' || res.status === 'delvist') { - res.knapper = {}; - for (const app of s.apps) { - const m = (await srv.kald('computer_find', { app, role: 'AXButton', limit: 40 })).data?.matches || []; - res.knapper[app] = m.map(x => x.name).filter(Boolean).slice(0, 25); - } - } res.stoppet = srv.vagt.stoppet(); res.samtykker = srv.spoerger.tekster().map(t => t.replace(/\s+/g, ' ').slice(0, 120)); res.udenfor = udenfor; @@ -531,6 +581,7 @@ export async function koerScenarie(s, { forgrund = false } = {}) { res.bevis += ` · ${res.stoppet.length + udenfor.length} handling(er) prøvede at nå et program uden for scenariet`; } srv.luk(); + if (film) res.film = await film.stop(); } return res; } @@ -640,6 +691,8 @@ async function selvproeve() { } // --------------------------------------------------------------------------- +// 1/10: filen kan importeres (test/parallel.mjs) uden at koere sig selv. +if (process.argv[1] && fileURLToPath(import.meta.url) === (await import('node:path')).resolve(process.argv[1])) { const fails = await selvproeve(); const forgrund = process.env.CMCP_BRUG_TILSTAND === 'forgrund'; const res = []; @@ -675,3 +728,4 @@ if (process.env.CMCP_BRUG_RAPPORT) writeFileSync(process.env.CMCP_BRUG_RAPPORT, const roede = res.filter(r => r.status === 'fejlede'); console.log(fails.length ? `DUMPET: ${fails.length} tjek i selvprøven` : 'Selvprøven bestået.'); process.exit(fails.length || roede.length ? 1 : 0); +} diff --git a/test/claims-offline.mjs b/test/claims-offline.mjs index 0b8f188..b73bcb3 100644 --- a/test/claims-offline.mjs +++ b/test/claims-offline.mjs @@ -89,8 +89,9 @@ check('at lukke et vindue spoerger hver gang', /computer_window' && args\.button === 'close'/.test(altidSpoerg), 'window+close staar i alwaysAsk'); check('et farligt menupunkt spoerger hver gang', - /computer_menu' && menuSerFarlig/.test(altidSpoerg), - 'menu gaar gennem menuSerFarlig'); + /alwaysAsk: menuFarlig/.test(altidSpoerg) + && /menuFarlig = name === 'computer_menu'[^;]*menuSerFarlig\(args\.path\)[^;]*menuGenvejErFarlig/.test(idx), + 'menu gaar gennem menuSerFarlig OG genvejen (1/10)'); check('et Space-skift spoerger hver gang', /name === 'computer_space'/.test(altidSpoerg), 'computer_space staar i alwaysAsk'); @@ -195,6 +196,26 @@ if (raa === null) { fanget.length ? 'fanget: ' + fanget.join(', ') : `${harmloese.length} harmloese gik igennem`); } +// ⛔ 1/10 (skaerm-koe-panelet): et skaerm-laan brugte det almindelige 60 s-vindue, +// og MAALT i Gustavs chat-historik kom 11 af 12 udloebne anmodninger inden for +// 3 minutter efter han sagde «klar». skaermVentetid() giver skaerm-anmodninger +// et eget, laengere vindue - adskilt fra askTimeout(), som styrer alle andre +// samtykke-dialoger og IKKE maa aendre sig ved et uheld. +{ + const gemt = process.env.CMCP_SCREEN_WAIT; + delete process.env.CMCP_SCREEN_WAIT; + check('skaermVentetid(): standard er 5 minutter (300 s), laengere end askTimeout()', + policy.skaermVentetid() === 300 && policy.skaermVentetid() > policy.askTimeout(), + `skaermVentetid=${policy.skaermVentetid()}, askTimeout=${policy.askTimeout()}`); + process.env.CMCP_SCREEN_WAIT = '9999'; + check('skaermVentetid(): loftet er 20 minutter (1200 s), ogsaa ved et stoerre tal', + policy.skaermVentetid() === 1200, String(policy.skaermVentetid())); + process.env.CMCP_SCREEN_WAIT = '45'; + check('skaermVentetid(): en gyldig vaerdi under loftet respekteres', + policy.skaermVentetid() === 45, String(policy.skaermVentetid())); + if (gemt === undefined) delete process.env.CMCP_SCREEN_WAIT; else process.env.CMCP_SCREEN_WAIT = gemt; +} + console.log(); console.log(fails.length ? `DUMPET: ${fails.length}` : 'BESTAAET'); process.exit(fails.length ? 1 : 0); diff --git a/test/claims.mjs b/test/claims.mjs index 383121e..3e27541 100644 --- a/test/claims.mjs +++ b/test/claims.mjs @@ -3,6 +3,7 @@ // De var alle sande i koden da de blev skrevet. Forskellen paa "sand i dag" // og "bliver ved med at vaere sand" er en proeve. Et sikkerhedsloefte uden // proeve er en kommentar. +import './ryd-op.mjs'; import { spawn } from 'child_process'; import { readFileSync, existsSync, mkdtempSync} from 'fs'; import { createHash } from 'crypto'; @@ -530,8 +531,11 @@ esac check('9. computer_ask_user findes', false, 'vaerktoejet mangler'); } else { const props = Object.keys(t.inputSchema?.properties || {}); + // 29/9: `app` kom til - det peger paa HVOR mennesket skal taste (baggrund, + // via ikonet). Et programnavn ind kan ikke blive til en hemmelighed ud. check('9. ask_user kan ikke bede om en hemmelighed', - props.length === 1 && props[0] === 'message', + props.length === 2 && props.includes('message') && props.includes('app') + && t.inputSchema.properties.app.type === 'string', `felter: ${props.join(', ') || 'ingen'}`); const txt = JSON.stringify(t.inputSchema); check('9b. skemaet har intet password-felt', @@ -635,10 +639,12 @@ esac // Heuristikken paa aegte stier fra Gustavs egen Chrome, paa dansk - som // menulinjen faktisk er. En regel bygget paa engelske ord alene ville have // sluppet hver eneste af dem igennem. + // 1/10 (konsulent-panelet): «Slut Skak» og «Luk» slap igennem paa Gustavs Mac. const farlige = ['Chrome > Slet browserdata…', 'Chrome > Afslut Google Chrome', - 'Finder > Tøm papirkurv', 'History > Clear browsing data']; + 'Finder > Tøm papirkurv', 'History > Clear browsing data', + 'Skak > Slut Skak', 'Arkiv > Luk', 'Vis > Slut proces']; const harmloese = ['Arkiv > Udskriv…', 'Rediger > Kopiér', 'Vis > Zoom ind', - 'Bogmærker > Vis alle bogmærker']; + 'Bogmærker > Vis alle bogmærker', 'Indsæt > Slutnote', 'Historik > Genåbn lukkede faner']; const f = farlige.filter(p => pol.menuSerFarlig(p)).length; const h = harmloese.filter(p => !pol.menuSerFarlig(p)).length; check('11. farlige menustier genkendes', f === farlige.length, `${f}/${farlige.length}`); @@ -1659,7 +1665,9 @@ esac const ORD = ['zero','one','two','three','four','five','six','seven','eight','nine','ten', 'eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen', 'nineteen','twenty','twenty-one','twenty-two','twenty-three','twenty-four','twenty-five', - 'twenty-six','twenty-seven','twenty-eight','twenty-nine','thirty']; + 'twenty-six','twenty-seven','twenty-eight','twenty-nine','thirty','thirty-one','thirty-two', + 'thirty-three','thirty-four','thirty-five','thirty-six','thirty-seven','thirty-eight', + 'thirty-nine','forty']; // Alt der ligner en paastand om antal vaerktoejer, paa enhver flade. const MOENSTRE = [ @@ -1824,6 +1832,16 @@ esac const side = fs42.existsSync(join(ROOT, 'docs/index.html')) ? fs42.readFileSync(join(ROOT, 'docs/index.html'), 'utf8') : ''; check('42c. forsidens tal for skjulte vaerktoejer stemmer', !side || side.includes(`adds the ${ORD[skjult]} that do`), `koden skjuler ${skjult} i baggrund`); + // 42d (runde 1 30/9, Astra 10 + Fable P1): README sagde «Twenty-four are offered» + // mens koden tilboed 25 - tallet var kun vogtet paa sitet, ikke i README. + const { TOOLS: V42 } = await import(join(ROOT, 'mcp-server', 'tools.js') + '?42d'); + const tilbudt = V42.filter(t => !T42.has(t.name) || K42.has(t.name)).length; + const ORD2 = ['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve','thirteen','fourteen', + 'fifteen','sixteen','seventeen','eighteen','nineteen','twenty','twenty-one','twenty-two','twenty-three','twenty-four','twenty-five', + 'twenty-six','twenty-seven','twenty-eight','twenty-nine','thirty','thirty-one','thirty-two','thirty-three','thirty-four','thirty-five']; + const ord = ORD2[tilbudt] || String(tilbudt); + check('42d. README siger hvor mange vaerktoejer der tilbydes som standard', + readme.includes(`${ord[0].toUpperCase() + ord.slice(1)} are offered by`), `koden tilbyder ${tilbudt} i baggrund`); } // 43. ⛔ KONSULENTEN 22/9: revisionsloggen fingeraftrykker `title`/`contains` diff --git a/test/concurrent.mjs b/test/concurrent.mjs index 3b4b138..8feb1ab 100644 --- a/test/concurrent.mjs +++ b/test/concurrent.mjs @@ -25,6 +25,7 @@ // aabner et mikrosekunds vindue, kan slippe forbi en enkelt koersel. // Vil man vaere sikker, er svaret O_APPEND i kernen - som er praecis det // `appendFileSync` giver os, og derfor det vagten findes for at beskytte. +import './ryd-op.mjs'; import { spawn } from 'child_process'; import { readFileSync, existsSync, mkdtempSync } from 'fs'; import { fileURLToPath } from 'url'; diff --git a/test/doer.mjs b/test/doer.mjs new file mode 100644 index 0000000..fb683aa --- /dev/null +++ b/test/doer.mjs @@ -0,0 +1,78 @@ +// Døren (computer_open): åbn en app's egen indgang bagfra via et INTENT + én +// typet parameter - aldrig en fri URL fra modellen. Serveren bygger URL'en af +// en fast skabelon og validerer parameteren strengt. +// +// ⛔ HVORFOR DEN FINDES (28/9) +// En dør der tog en fri URL/scheme ville være en generisk bagdør: +// file:, shortcuts://run-shortcut → shell, osascript = Turing-komplet. Prøven +// beviser: (1) der er INGEN url-parameter at injicere i, (2) hver parameter +// valideres så kun cifre/bogstaver passerer, (3) serveren bygger præcis den +// forventede app-URL. Helperen er mocket, så intet åbnes. +import './ryd-op.mjs'; +import { spawn } from 'node:child_process'; +import { mkdtempSync, writeFileSync, chmodSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { lavFalskSpoerger } from './falsk-hjaelper.mjs'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; + +// Mock-helper: ekko den modtagne --url / --app som JSON, aabn intet. +const DIR = mkdtempSync(join(tmpdir(), 'cmcp-doer-')); +const fakeHelper = join(DIR, 'fake-helper'); +writeFileSync(fakeHelper, `#!/bin/bash +cmd="$1"; shift; url=""; app="" +while [ $# -gt 0 ]; do case "$1" in --url) url="$2"; shift 2;; --app) app="$2"; shift 2;; *) shift;; esac; done +if [ "$cmd" = "open-url" ]; then echo "{\\"ok\\":true,\\"echoed_url\\":\\"$url\\",\\"took_screen\\":false}" +elif [ "$cmd" = "launch" ]; then echo "{\\"ok\\":true,\\"result\\":\\"launched in the background\\",\\"echoed_app\\":\\"$app\\",\\"took_screen\\":false}" +else echo "{\\"ok\\":true}"; fi +`); +chmodSync(fakeHelper, 0o755); + +const srv = spawn('node', [join(ROOT, 'mcp-server/index.js')], + // Husets vagt 26: ingen prøve må kunne rejse en ægte macOS-dialog - fake osascript. + { env: { ...process.env, CMCP_HELPER: fakeHelper, CMCP_MODE: 'allow', CMCP_STATUS_IKON: '0', CMCP_OSASCRIPT: lavFalskSpoerger('udloeb', 'cmcp-doer').sti, CMCP_STATE_DIR: join(DIR, 'state') }, stdio: ['pipe', 'pipe', 'pipe'] }); +let buf = '', n = 0; const w = new Map(); +srv.stdout.on('data', d => { buf += d; let i; while ((i = buf.indexOf('\n')) >= 0) { const l = buf.slice(0, i); buf = buf.slice(i + 1); try { const m = JSON.parse(l); w.get(m.id)?.(m); } catch {} } }); +const rpc = (m, p) => new Promise(r => { const id = ++n; w.set(id, r); srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method: m, params: p }) + '\n'); }); +const kald = async (args) => { const r = await rpc('tools/call', { name: 'computer_open', arguments: args }); const t = r.result?.content?.[0]?.text ?? ''; let d = null; try { d = JSON.parse(t); } catch {} return { fejl: !!r.result?.isError, tekst: t, data: d }; }; + +try { + await rpc('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'doer', version: '1' } }); + srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'); + + const tools = (await rpc('tools/list')).result?.tools || []; + const doer = tools.find(t => t.name === 'computer_open'); + check('1 doeren findes', !!doer); + const props = Object.keys(doer?.inputSchema?.properties || {}); + check('1b der er INGEN url-parameter at injicere i', !props.includes('url'), props.join(',')); + check('1c parametrene er intent + typede felter', props.includes('intent') && props.includes('spotifyId') && props.includes('phone')); + + // Gyldige intents -> praecis den forventede app-URL (mock ekko). + const t1 = await kald({ intent: 'play_track', spotifyId: '4cOdK2wGLETKBW3PvgPWqT' }); + check('2 play_track bygger spotify:track: med id\'et', !t1.fejl && t1.data?.echoed_url === 'spotify:track:4cOdK2wGLETKBW3PvgPWqT', t1.data?.echoed_url || t1.tekst.slice(0, 80)); + const t2 = await kald({ intent: 'open_chat', phone: '+45 60 17 45 69' }); + check('3 open_chat bygger whatsapp://send?phone= (tegnene renset, ingen text)', !t2.fejl && t2.data?.echoed_url === 'whatsapp://send?phone=4560174569', t2.data?.echoed_url || t2.tekst.slice(0, 80)); + + // Ugyldige parametre -> AFVIST, intet aabnet. + const b1 = await kald({ intent: 'play_track', spotifyId: 'file:///etc/passwd' }); + check('4 et forsoeg paa at smugle en sti ind i spotifyId afvises', b1.fejl && !b1.data?.echoed_url, b1.tekst.slice(0, 70)); + const b2 = await kald({ intent: 'open_chat', phone: '4560; do shell script' }); + check('5 et telefonnummer med skal-tekst afvises', b2.fejl, b2.tekst.slice(0, 70)); + const b3 = await kald({ intent: 'run_shortcut', spotifyId: 'x' }); + check('6 et ukendt intent afvises (ingen fri handling)', b3.fejl, b3.tekst.slice(0, 70)); + const b4 = await kald({ intent: 'open_app', bundleId: 'com.x; rm -rf /' }); + check('7 en bundle id med skal-tegn afvises', b4.fejl, b4.tekst.slice(0, 70)); + + // En medsendt fri url AFVISES helt: den er ikke et felt paa vaerktoejet, saa + // der er ingen vej at injicere en scheme ad. (Staerkere end at ignorere den.) + const t3 = await kald({ intent: 'play_track', spotifyId: '4cOdK2wGLETKBW3PvgPWqT', url: 'file:///etc/passwd' }); + check('8 en medsendt fri url AFVISES (intet felt at injicere i)', t3.fejl && !t3.data?.echoed_url, t3.tekst.slice(0, 70)); +} finally { + srv.kill(); +} +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/e2e-forloeb.mjs b/test/e2e-forloeb.mjs index 5c38207..53b9366 100644 --- a/test/e2e-forloeb.mjs +++ b/test/e2e-forloeb.mjs @@ -176,28 +176,37 @@ try { midte = (JSON.parse(kf.tekst).matches || []).find(m => /orden-3/.test(JSON let klikMidte = null; try { klikMidte = (JSON.parse(kf.tekst).matches || []).find(m => /klik-maal/.test(JSON.stringify(m)))?.center; } catch {} if (klikMidte) { + // ⛔ RETTET (review-security, 2/10): denne knap er navngivet og helt + // almindelig ("klik-maal"), men maaler alligevel en KENDT AX-kvirk: punkt- + // opslaget (`at`) svarer found:false her (fejl -25200), gentagne gange, + // malt empirisk. Foer dette fund blev et saadant klik lukket igennem + // stille - noejagtig den samme fejlklasse som README's "Delete" paa et + // fjernskrivebord. Nu spoerger et klik der ikke kan identificeres, ALTID - + // ogsaa paa en helt harmloes knap. Attrap-spoergeren her svarer aldrig + // (`udloeb`), saa klikket afvises - det ER den sikre, korrekte opfoersel. + // Det fulde "menneske siger ja, klikket lander"-forloeb er allerede + // bevist andetsteds med en rigtig ja-spoerger (test/knap-ord.mjs, + // test/sende-port.mjs). const k = await kald('computer_click', { app: BID, x: klikMidte.x, y: klikMidte.y }); - trin('haender+', 'click --app tager ikke skaermen', !k.fejl && /pointer stayed/.test(k.tekst), k.tekst.slice(0, 60)); - // «It does not pretend»: svaret maa IKKE paastaa at klikket lykkedes. - trin('haender+', '...og paastaar ikke at klikket landede', /NOT verified/.test(k.tekst) && !/^Clicked at/.test(k.tekst), - k.tekst.slice(0, 60)); - await new Promise(r => setTimeout(r, 700)); - const efterK = await kald('computer_inspect', { app: BID }); - // ⛔ UMAALT indtil nu. Rapporteres som maaling, ikke som paastand. - const landede = /KLIKKET/.test(efterK.tekst); - console.log(`\n MAALING · landede klikket? ${landede ? 'JA' : 'NEJ'} - ${landede ? 'knappen skiftede titel' : 'knappen hedder stadig klik-maal'}`); + // Samme afvisnings-moenster som "adgangskode-program afvises" ovenfor: + // baggrundstilstand naegter en handling der kraever spoergsmaal, foer den + // overhovedet naar frem til en spoerger. + trin('haender+', 'et klik der ikke kan identificeres, spoerger - ogsaa paa en harmloes knap (AX-kvirk)', + k.fejl && /this would need a dialog/.test(k.tekst), k.tekst.slice(0, 90)); } else if (midte) { trin('haender+', 'click --app (kunne ikke finde en knap at sigte paa)', false, kf.tekst.slice(0, 60)); } -// --- 3d. DE OTTE DER ALDRIG KAN GOERES STILLE - skal vaere SKJULT i baggrund, +// --- 3d. DE SYV DER ALDRIG KAN GOERES STILLE - skal vaere SKJULT i baggrund, +// (29/9: `computer_ask_user` er ude af listen - i baggrund spoerger den via +// menulinje-ikonet; det maales i ikon-godkend.mjs del E.) // og afvist hvis de kaldes ved navn alligevel. Skjult er ikke det samme // som afvist, saa begge dele proeves. const SKJULT = ['computer_quit', 'computer_drag', 'computer_space', 'computer_paste', - 'computer_window', 'computer_ask_user', 'computer_move', 'computer_activate']; + 'computer_window', 'computer_move', 'computer_activate']; const tilbudt = SKJULT.filter(x => liste.includes(x)); -trin('skjult', 'de otte larmende tilbydes IKKE i baggrund', tilbudt.length === 0, - tilbudt.length ? 'TILBUDT: ' + tilbudt.join(', ') : 'ingen af de otte i listen'); +trin('skjult', 'de syv larmende tilbydes IKKE i baggrund', tilbudt.length === 0, + tilbudt.length ? 'TILBUDT: ' + tilbudt.join(', ') : 'ingen af de syv i listen'); const tvang = await kald('computer_move', { x: 5, y: 5 }); trin('skjult', '...og afvises hvis de kaldes ved navn alligevel', /Refused|not offered|unknown|background/i.test(tvang.tekst), tvang.tekst.slice(0, 70)); @@ -243,9 +252,14 @@ trin('log', 'took_screen staar i loggen', /took_screen/.test(log.tekst), (log.te // ⛔ Konsulenten 22/9: loggen sagde «ok» om baade «vi sendte det» og «det // virkede». Nu staar der hvad vi FAKTISK ved: verified (laest efter), // performed (programmet udfoerte den) eller sent (afleveret, udfald ukendt). +// ⛔ RETTET (review-security, 2/10): «sent» kom tidligere KUN fra klikket paa +// «klik-maal» - det klik spoerger nu altid (se ovenfor), saa denne proeve +// naar aldrig frem til at taelle «sent» i DENNE frisk server. Selve +// effect-vaerdien er stadig i brug og proevet andetsteds, med en rigtig +// ja-spoerger (test/klik-ejer.mjs "9a kalibrering ... Clicked at 700, 50"). const eff = [...log.tekst.matchAll(/"effect": "(\w+)"/g)].map(m => m[1]); -trin('log', 'loggen skelner «sendt» fra «virkede»', - eff.includes('verified') && eff.includes('performed') && eff.includes('sent'), +trin('log', 'loggen skelner mindst «verified» fra «virkede» («sent» proeves med et ja i test/klik-ejer.mjs)', + eff.includes('verified') && eff.includes('performed'), eff.length ? [...new Set(eff)].join(', ') : 'intet effect-felt'); srv.kill(); attrap.kill(); diff --git a/test/failclosed.mjs b/test/failclosed.mjs index 5953747..456e53f 100644 --- a/test/failclosed.mjs +++ b/test/failclosed.mjs @@ -16,6 +16,7 @@ // blev aldrig muteret, og proeven var groen mod ren kode. Det lignede et // bevis. Fanget fordi jeg sammenlignede md5 foer og efter i stedet for at // antage at gendannelsen var noedvendig. +import './ryd-op.mjs'; import { spawn } from 'child_process'; import { tmpdir } from 'os'; import { mkdtempSync } from 'fs'; diff --git a/test/falsk-hjaelper.mjs b/test/falsk-hjaelper.mjs index 0025a87..8a27ee9 100644 --- a/test/falsk-hjaelper.mjs +++ b/test/falsk-hjaelper.mjs @@ -13,6 +13,7 @@ // Seam'en fandtes allerede (`CMCP_HELPER`, brugt i paastand 8). Den bruges // nu i ALLE port-proever: bryder porten sammen, lander handlingen i en // tekstfil i stedet for paa skaermen, og proeven kan stadig se at den kom. +import './ryd-op.mjs'; import { writeFileSync, chmodSync, mkdtempSync, existsSync, readFileSync, appendFileSync } from 'fs'; import { join } from 'path'; import { spawn, spawnSync } from 'child_process'; @@ -26,7 +27,7 @@ import { tmpdir } from 'os'; /// `screenshot` er et opslag i hjaelperens forstand, men det optager skaermen: /// det sendes kun videre med --plan eller paa en fremmed maskine (se nedenfor). export const HJAELPER_OPSLAG = ['apps', 'displays', 'find', 'focused', 'inspect', 'menus', 'permissions', - 'redact', 'screenshot', 'secure-rects', 'version', 'wait-for', 'windows', 'at', 'resolve-app']; + 'redact', 'screenshot', 'secure-rects', 'version', 'wait-for', 'windows', 'at', 'resolve-app', 'samtale', 'idle']; export function lavFalskHjaelper(navn = 'cmcp-falsk') { const dir = mkdtempSync(join(tmpdir(), navn + '-')); @@ -46,10 +47,37 @@ export function lavFalskHjaelper(navn = 'cmcp-falsk') { // Derfor: OPSLAG sendes videre til den rigtige hjaelper og svarer sandt. // Kun HANDLINGER - dem der kan roere skaermen - sluges og noteres. writeFileSync(js, ` -import { appendFileSync, writeSync } from 'fs'; +import { appendFileSync, writeSync, readFileSync } from 'fs'; import { spawnSync } from 'child_process'; const argv = process.argv.slice(2); const kommando = argv[0] || ''; +// Scriptede svar (29/9, sende-porten): en proeve kan bestemme hvad et opslag +// svarer - «samtale», «at» eller «press --dry» - og aendre det undervejs. +try { + const SVAR = JSON.parse(readFileSync(${JSON.stringify(join(dir, 'svar.json'))}, 'utf8')); + const noegle = kommando + (argv.includes('--dry') ? ' --dry' : ''); + if (SVAR[noegle]) { + // «_vent» (ms): hjaelperen «arbejder» saa laenge - saa en proeve kan afbryde den midt i. + if (SVAR[noegle]._vent) { + // Ikke-blokerende, saa et signal kan naa frem undervejs. SIGUSR1 = «stop foer + // du goer noget» (paste ved laanets slut, runde 4): registreres og svares som + // den rigtige hjaelper goer. + process.on('SIGUSR1', () => { + appendFileSync(${JSON.stringify(spor)}, JSON.stringify({ argv, ts: Date.now(), signal: 'SIGUSR1' }) + '\\n'); + writeSync(1, JSON.stringify({ ok: false, code: 'screen-taken-back', error: 'stopped before pasting: the screen loan ended' }) + '\\n'); + process.exit(1); + }); + await new Promise(r => setTimeout(r, SVAR[noegle]._vent)); + } + appendFileSync(${JSON.stringify(spor)}, JSON.stringify({ argv, ts: Date.now(), scriptet: true }) + '\\n'); + writeSync(1, JSON.stringify({ ok: true, ...SVAR[noegle] }) + '\\n'); + process.exit(0); + } +} catch {} +// ⛔ 30/9: «samtale» laeser en beskedapps modtager og tekst. Sendt videre til den +// rigtige hjaelper kunne en proevekoersel paa en udviklers Mac laese hans +// RIGTIGE chats (claim 35 ramte Gustavs WhatsApp). Uscriptet svarer den tomt. +if (kommando === 'samtale') { writeSync(1, JSON.stringify({ ok: true }) + '\\n'); process.exit(0); } // ⛔ FUNDET 22/9: her stod en liste over HANDLINGER der skulle sluges - // 'menu', 'window' - mens hjaelperens rigtige kommandoer hedder // 'menu-click', 'window-button', 'window-set', 'set-value' og 'drag'. @@ -58,7 +86,8 @@ const kommando = argv[0] || ''; // En liste over hvad der er farligt, er altid ufuldstaendig. Nu er det // omvendt: kun kendte OPSLAG sendes videre, alt andet sluges. const OPSLAG = new Set(${JSON.stringify(HJAELPER_OPSLAG)}); -appendFileSync(${JSON.stringify(spor)}, JSON.stringify({ argv, ts: Date.now() }) + '\\n'); +// Et --dry er et opslag (intet trykkes), ikke en handling. +if (!argv.includes('--dry')) appendFileSync(${JSON.stringify(spor)}, JSON.stringify({ argv, ts: Date.now() }) + '\\n'); // ⛔ 25/9 (proeve-reviewet): «screenshot» blev sendt videre til den AEGTE hjaelper - // kun flaget i proeverne beskyttede menneskets skaerm, ikke attrappen selv. if (kommando === 'screenshot' && !argv.includes('--plan') && process.env.CMCP_FREMMED_MASKINE !== '1') { @@ -100,6 +129,8 @@ process.exit(r.status === null ? 1 : r.status); return { sti: wrapper, /// Hvad naaede frem til hjaelperen? Tom liste = porten holdt. + /// Scriptede svar: { samtale: {...}, at: {...}, 'press --dry': {...} }. + saetSvar(svar) { writeFileSync(join(dir, 'svar.json'), JSON.stringify(svar)); }, kald() { if (!existsSync(spor)) return []; return readFileSync(spor, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse); @@ -127,7 +158,7 @@ process.exit(r.status === null ? 1 : r.status); /// harmloese. Et nyt vaerktoej er daekket den dag det skrives. handlingerNaaedeFrem() { const OPSLAG = new Set(HJAELPER_OPSLAG); - return this.kald().filter(k => !OPSLAG.has(k.argv[0])); + return this.kald().filter(k => !OPSLAG.has(k.argv[0]) && !k.argv.includes('--dry')); } }; } diff --git a/test/film.mjs b/test/film.mjs new file mode 100644 index 0000000..252078e --- /dev/null +++ b/test/film.mjs @@ -0,0 +1,54 @@ +// FILM: en skaermoptagelse af det en proeve goer, saa et menneske kan SE beviset. +// +// ⛔ HVORFOR DEN FINDES (1/10-2026) +// Gustav: «film hvordan den fungerer, saa du sikrer at alle use cases leverer». +// Et JSON-svar siger at det virkede; en film viser det - ogsaa det ingen proeve +// spurgte om (et vindue der blinkede frem, en dialog der stod og ventede). +// +// ⛔ KUN PAA EN FREMMED MASKINE (CMCP_FREMMED_MASKINE=1) og kun naar en mappe er +// givet (CMCP_FILM=). Paa menneskets Mac optages ALDRIG - det er hans +// skaerm, ikke vores bevismateriale. +import { spawn, execFileSync } from 'node:child_process'; +import { mkdirSync, existsSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const vent = (ms) => new Promise(r => setTimeout(r, ms)); + +export function filmMappe() { + if (process.env.CMCP_FREMMED_MASKINE !== '1' || !process.env.CMCP_FILM) return null; + mkdirSync(process.env.CMCP_FILM, { recursive: true }); + return process.env.CMCP_FILM; +} + +let ffmpeg = null; +function findFfmpeg() { + if (ffmpeg !== null) return ffmpeg; + for (const p of ['/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg']) if (existsSync(p)) return (ffmpeg = p); + try { return (ffmpeg = execFileSync('which', ['ffmpeg'], { encoding: 'utf8' }).trim()); } catch { return (ffmpeg = ''); } +} + +/// Starter en optagelse af hovedskaermen. `stop()` afslutter den og giver stien, +/// eller null hvis der ikke kom en brugbar film ud (saa siger rapporten det). +export function startFilm(navn) { + const mappe = filmMappe(); + const ff = mappe && findFfmpeg(); + if (!mappe || !ff) return { sti: null, stop: async () => null, grund: mappe ? 'ffmpeg mangler' : 'ikke en fremmed maskine' }; + const sti = join(mappe, `${navn.replace(/[^\w.-]+/g, '-')}.mp4`); + const p = spawn(ff, ['-hide_banner', '-loglevel', 'error', '-f', 'avfoundation', '-capture_cursor', '1', + '-framerate', '10', '-i', 'Capture screen 0:none', '-vf', 'scale=1280:-2', '-pix_fmt', 'yuv420p', + '-c:v', 'libx264', '-preset', 'ultrafast', '-y', sti], { stdio: ['pipe', 'ignore', 'pipe'] }); + let fejl = ''; + p.stderr.on('data', d => { fejl += d; }); + const slut = new Promise(r => p.on('close', r)); + return { + sti, + async stop() { + try { p.stdin.write('q'); p.stdin.end(); } catch {} + const t = setTimeout(() => { try { p.kill('SIGINT'); } catch {} }, 8000); + await slut; clearTimeout(t); + await vent(200); + return existsSync(sti) && statSync(sti).size > 10_000 ? sti : null; + }, + fejl: () => fejl.slice(0, 300), + }; +} diff --git a/test/fixture/proevemaal.swift b/test/fixture/proevemaal.swift index 814a84d..98be19a 100644 --- a/test/fixture/proevemaal.swift +++ b/test/fixture/proevemaal.swift @@ -24,6 +24,7 @@ final class App: NSObject, NSApplicationDelegate { var vindue: NSWindow! var felt: NSTextField! var knap: NSButton! + var skift: DispatchSourceSignal? @objc func trykket() { knap.title = "TRYKKET" } @objc func hentFrem() { NSApp.activate(ignoringOtherApps: true) } @@ -125,8 +126,34 @@ final class App: NSObject, NSApplicationDelegate { vindue.orderFront(nil) // frem, men IKKE makeKey - vi stjaeler ingen fokus // Feltet har fokus INDE I appen. Det er den tilstand et rigtigt // program er i: noget er valgt, selv naar vinduet ikke er forrest. + // En «samtale» som i en beskedapp (30/9, runde 1 Fable P4): en sidebar med et + // ANDET navn oeverst til venstre, samtalens navn over feltet, og feltet med + // fokus. Sende-porten skal laese samtalens navn - aldrig sidebarens. + if ProcessInfo.processInfo.environment["CMCP_PROEVE_SAMTALE"] == "1" { + vindue.setContentSize(NSSize(width: 400, height: 400)) + let sidebar = NSTextField(labelWithString: "Alice Sidebar") + sidebar.frame = NSRect(x: 10, y: 350, width: 120, height: 20) + let overskrift = NSTextField(labelWithString: "Bob Samtale") + overskrift.frame = NSRect(x: 160, y: 300, width: 200, height: 20) + let beskedfelt = NSTextField(frame: NSRect(x: 160, y: 40, width: 200, height: 24)) + beskedfelt.stringValue = "hej Bob" + for v in [sidebar, overskrift, beskedfelt] { vindue.contentView?.addSubview(v) } + foerste = beskedfelt + } vindue.initialFirstResponder = foerste vindue.makeFirstResponder(foerste) + // Fokus flytter ind i et kodeordsfelt MIDT i en skrivning - som et Tab i + // «bruger\tkode», et klik eller et felt der selv hopper videre (29/9). + // Proeven sender SIGUSR1, naar skrivningen er i gang. + if ProcessInfo.processInfo.environment["CMCP_PROEVE_SIKKER_SKIFT"] == "1" { + let senere = NSSecureTextField(frame: NSRect(x: 10, y: 230, width: 280, height: 24)) + vindue.contentView?.addSubview(senere) + signal(SIGUSR1, SIG_IGN) + let s = DispatchSource.makeSignalSource(signal: SIGUSR1, queue: .main) + s.setEventHandler { [weak self] in self?.vindue.makeFirstResponder(senere) } + s.resume() + skift = s + } if ProcessInfo.processInfo.environment["CMCP_PROEVE_GEMPANEL"] == "1" { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.aabnGemPanel() } } diff --git a/test/fixtures/forrest-log-1-10.ndjson b/test/fixtures/forrest-log-1-10.ndjson new file mode 100644 index 0000000..bed4b64 --- /dev/null +++ b/test/fixtures/forrest-log-1-10.ndjson @@ -0,0 +1,53 @@ +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:03.406106+0800","machTimestamp":6925351374220,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c48ad the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33295393,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:03.406220+0800","machTimestamp":6925351376961,"eventMessage":"SETFRONT: pid=25436 asn=0x-0x178c48ad foreground=1 oldFrontPid=25441 oldFrontASN=0x-0x395077807","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:03.824785+0800","machTimestamp":6925361422531,"eventMessage":"61403[SetFrontProcess]: [DataSource] Making 0x0-0x178c68af the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33296743,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:03.824941+0800","machTimestamp":6925361426274,"eventMessage":"SETFRONT: pid=25441 asn=0x-0x178c68af foreground=1 oldFrontPid=25436 oldFrontASN=0x-0x395069613","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:07.914162+0800","machTimestamp":6925459567578,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33296744,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:07.914329+0800","machTimestamp":6925459571578,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=25441 oldFrontASN=0x-0x395077807","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:08.553152+0800","machTimestamp":6925474903328,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c48ad the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33296707,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:08.553345+0800","machTimestamp":6925474907965,"eventMessage":"SETFRONT: pid=25436 asn=0x-0x178c48ad foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":55325486,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:31.208910+0800","machTimestamp":6926018641530,"eventMessage":"9afe3[DoDeferredOrdering]: [DataSource] Making 0x0-0x17bb8ba1 the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170586116,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":55325485} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":55325486,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33297566,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:31.209035+0800","machTimestamp":6926018644531,"eventMessage":"SETFRONT: pid=62888 asn=0x-0x17bb8ba1 foreground=1 oldFrontPid=25436 oldFrontASN=0x-0x395069613","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349766148,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":55325485} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:41.514352+0800","machTimestamp":6926265972127,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33296730,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:41.514449+0800","machTimestamp":6926265974469,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=62888 oldFrontASN=0x-0x398166945","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:42.232057+0800","machTimestamp":6926283197055,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c98b2 the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33299126,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:42.232248+0800","machTimestamp":6926283201644,"eventMessage":"SETFRONT: pid=25460 asn=0x-0x178c98b2 foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:44.873205+0800","machTimestamp":6926346584609,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33298580,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:44.873436+0800","machTimestamp":6926346590153,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=25460 oldFrontASN=0x-0x395090098","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:46.478612+0800","machTimestamp":6926385114372,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c68af the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33298190,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:46.478738+0800","machTimestamp":6926385117400,"eventMessage":"SETFRONT: pid=25441 asn=0x-0x178c68af foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:53.029982+0800","machTimestamp":6926542347254,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33299383,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:53.030109+0800","machTimestamp":6926542350298,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=25441 oldFrontASN=0x-0x395077807","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:53.674100+0800","machTimestamp":6926557806071,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x17bb8ba1 the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33298190,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:53.674307+0800","machTimestamp":6926557811041,"eventMessage":"SETFRONT: pid=62888 asn=0x-0x17bb8ba1 foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:49:54.372187+0800","machTimestamp":6926574560167,"eventMessage":"7dd83[DoDeferredOrdering]: [DataSource] Making 0x0-0x178c48ad the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33298580,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:49:54.372332+0800","machTimestamp":6926574563654,"eventMessage":"SETFRONT: pid=25436 asn=0x-0x178c48ad foreground=1 oldFrontPid=62888 oldFrontASN=0x-0x398166945","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:50:14.567154+0800","machTimestamp":6927059239367,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33300507,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:50:14.567268+0800","machTimestamp":6927059242115,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=25436 oldFrontASN=0x-0x395069613","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:50:14.749162+0800","machTimestamp":6927063607566,"eventMessage":"7dd83[SetFrontProcess]: [DataSource] Making 0x0-0x178c48ad the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33300510,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:50:14.749280+0800","machTimestamp":6927063610400,"eventMessage":"SETFRONT: pid=25436 asn=0x-0x178c48ad foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:50:26.531912+0800","machTimestamp":6927346393576,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33297189,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:50:26.532057+0800","machTimestamp":6927346397045,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=25436 oldFrontASN=0x-0x395069613","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:50:27.159561+0800","machTimestamp":6927361457149,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c98b2 the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33300508,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:50:27.159771+0800","machTimestamp":6927361462190,"eventMessage":"SETFRONT: pid=25460 asn=0x-0x178c98b2 foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:50:27.761014+0800","machTimestamp":6927375892007,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c68af the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33297189,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:50:27.761143+0800","machTimestamp":6927375895112,"eventMessage":"SETFRONT: pid=25441 asn=0x-0x178c68af foreground=1 oldFrontPid=25460 oldFrontASN=0x-0x395090098","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:51:49.605509+0800","machTimestamp":6929340159886,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33307042,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:51:49.605834+0800","machTimestamp":6929340167707,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=25441 oldFrontASN=0x-0x395077807","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:51:50.290155+0800","machTimestamp":6929356591398,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c48ad the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33303414,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:51:50.290338+0800","machTimestamp":6929356595801,"eventMessage":"SETFRONT: pid=25436 asn=0x-0x178c48ad foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:52:28.650487+0800","machTimestamp":6930277239381,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33309682,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:52:28.650915+0800","machTimestamp":6930277249641,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=25436 oldFrontASN=0x-0x395069613","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:52:29.333239+0800","machTimestamp":6930293625418,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c48ad the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33309681,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:52:29.333379+0800","machTimestamp":6930293628783,"eventMessage":"SETFRONT: pid=25436 asn=0x-0x178c48ad foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:52:40.330715+0800","machTimestamp":6930557564839,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c68af the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33303415,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:52:40.330963+0800","machTimestamp":6930557570789,"eventMessage":"SETFRONT: pid=25441 asn=0x-0x178c68af foreground=1 oldFrontPid=25436 oldFrontASN=0x-0x395069613","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:52:47.885647+0800","machTimestamp":6930738883209,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178d28bb the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33310137,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:52:47.885856+0800","machTimestamp":6930738888237,"eventMessage":"SETFRONT: pid=25476 asn=0x-0x178d28bb foreground=1 oldFrontPid=25441 oldFrontASN=0x-0x395077807","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:52:48.521959+0800","machTimestamp":6930754154689,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c48ad the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33309680,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:52:48.522132+0800","machTimestamp":6930754158851,"eventMessage":"SETFRONT: pid=25436 asn=0x-0x178c48ad foreground=1 oldFrontPid=25476 oldFrontASN=0x-0x395126971","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:52:54.829979+0800","machTimestamp":6930905547186,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c98b2 the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33311316,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:52:54.830139+0800","machTimestamp":6930905551028,"eventMessage":"SETFRONT: pid=25460 asn=0x-0x178c98b2 foreground=1 oldFrontPid=25436 oldFrontASN=0x-0x395069613","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"%x[%s]: [DataSource] Making %s 0x%x-0x%x the front process in session 0x%x","userID":88,"activityIdentifier":0,"subsystem":"com.apple.SkyLight.processes","category":"Focus","threadID":33159032,"senderImageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367","backtrace":{"frames":[{"imageOffset":647588,"imageUUID":"EF82BE18-450B-33FE-9C68-A2C88144F367"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/Resources\/WindowServer","senderImagePath":"\/System\/Library\/PrivateFrameworks\/SkyLight.framework\/Versions\/A\/SkyLight","timestamp":"2026-10-01 16:52:57.552492+0800","machTimestamp":6930970887497,"eventMessage":"7d633[SetManagedDisplayCurrentSpace]: [DataSource] Making 0x0-0x178c68af the front process in session 0x1a83b","processImageUUID":"7B15FBD3-BAAE-3FF4-8830-3910FED42941","traceID":514116712170520580,"processID":22090,"senderProgramCounter":647588,"parentActivityIdentifier":0} +{"timezoneName":"","messageType":"Default","eventType":"logEvent","source":null,"formatString":"SETFRONT: pid=%d asn=0x-0x%x foreground=%d oldFrontPid=%d oldFrontASN=0x-0x%u","userID":0,"activityIdentifier":0,"subsystem":"com.apple.processmanager","category":"front-35286506","threadID":33312122,"senderImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","backtrace":{"frames":[{"imageOffset":199600,"imageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926"}]},"bootUUID":"2FBB5CA2-8BFB-4D43-AA84-FDDA5AA609B6","processImagePath":"\/System\/Library\/CoreServices\/launchservicesd","senderImagePath":"\/System\/Library\/CoreServices\/launchservicesd","timestamp":"2026-10-01 16:52:57.552670+0800","machTimestamp":6930970891755,"eventMessage":"SETFRONT: pid=25441 asn=0x-0x178c68af foreground=1 oldFrontPid=25460 oldFrontASN=0x-0x395090098","processImageUUID":"6949285D-03FB-3B34-B4CF-0A1300750926","traceID":1758355349700612,"processID":377,"senderProgramCounter":199600,"parentActivityIdentifier":0} +{"count":52,"finished":1} diff --git a/test/forrest-log-kalibrering.mjs b/test/forrest-log-kalibrering.mjs new file mode 100644 index 0000000..9afcc5f --- /dev/null +++ b/test/forrest-log-kalibrering.mjs @@ -0,0 +1,35 @@ +// Forrest-loggens kalibrering: afspiller macOS' egen log fra koerslen paa Gustavs Mac +// 1/10-2026 (16:48-16:55 maskintid WITA) og kraever de tre domme konsulent-panelet fandt. +// +// ⛔ HVORFOR DEN FINDES: den gamle vagt (NSWorkspace i en osascript-loekke) saa 1 skift +// pr. runde, mens loggen havde 26 - og kaldte en runde groen, hvor Skak stod forrest. +// Et instrument der ikke kan blive roedt paa den kendte fejl, maaler intet. +// Intet startes, intet laeses fra skaermen: kun en gemt logfil. +import './ryd-op.mjs'; +import { readFileSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { tolk, parSkift, doem } from './forrest-log.mjs'; + +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; +const fil = join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'forrest-log-1-10.ndjson'); +const skift = parSkift(readFileSync(fil, 'utf8').split('\n').map(tolk).filter(Boolean)); +// Agenternes programmer i koerslen (pids fra macOS' processmanager-log). +const AGENT = new Set([62766, 62888, 62984, 63975, 64022, 65698, 65808]); +const t = (hms) => Date.parse(`2026-10-01T${hms}+08:00`); +const runde = (fra, til) => doem(skift.filter(s => s.t >= t(fra) && s.t <= t(til)), p => AGENT.has(p)); + +check('0 loggen har de 26 skift', skift.length === 26, String(skift.length)); +const r5 = runde('16:48:43', '16:49:39'), r10 = runde('16:49:40', '16:51:02'), r15 = runde('16:51:04', '16:53:58'); +check('5 rød: skærmen revet tilbage + Skak forrest', r5.length === 2 + && r5.some(f => f.slags === 'trak forgrunden tilbage fra mennesket') && r5.some(f => f.slags === 'agent-program forrest' && f.pid === 62888), + JSON.stringify(r5.map(f => f.slags))); +check('10 rød: skærmen revet tilbage', r10.length === 1 && r10[0].slags === 'trak forgrunden tilbage fra mennesket', JSON.stringify(r10.map(f => f.slags))); +check('15 grøn: kun menneskets egne skrivebordsskift', r15.length === 0, JSON.stringify(r15)); +// Menneskets egne skift er aldrig en fejl - ellers kan proeven ikke koere paa en Mac i brug. +check('mennesket: 9 skrivebordsskift i 15-runden giver ingen fund', + skift.filter(s => s.t >= t('16:51:04') && s.t <= t('16:53:58') && s.aarsag === 'SetManagedDisplayCurrentSpace').length === 9); + +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/forrest-log.mjs b/test/forrest-log.mjs new file mode 100644 index 0000000..fa0c14f --- /dev/null +++ b/test/forrest-log.mjs @@ -0,0 +1,107 @@ +// FORREST-LOGGEN: hvad der stod forrest, læst i macOS' egen log - ikke gættet. +// +// ⛔ HVORFOR DEN FINDES (1/10-2026, konsulent-panelet) +// Den gamle vagt i parallel.mjs spurgte NSWorkspace.frontmostApplication i en +// langlivet osascript-løkke. Den værdi opdateres kun når processens run-loop +// kører (Apple: «if you repeatedly poll … without allowing the run loop to run, +// it will continue to return false»). Målt på Gustavs Mac 1/10: vagten skrev +// 1 linje pr. runde, mens macOS' log havde 26 skift - og den kaldte en runde +// grøn, hvor Skak stod forrest i 10 sekunder. +// +// Her læses to kilder i systemets log, med årsag: +// launchservicesd SETFRONT: pid=… oldFrontPid=… (HVEM blev forrest) +// WindowServer [Årsag]: … the front process (HVORFOR) +// Årsagerne målt 1/10: SetManagedDisplayCurrentSpace (mennesket skiftede +// skrivebord), SetFrontProcess (et program bad om det), DoDeferredOrdering +// (programmet ordnede selv et vindue frem, fx et gem-panel). +// +// ⛔ Instrumentet beviser selv at det ser: en puls via `logger` hvert 2. sekund +// skal nå strømmen, og antallet af skift i strømmen skal være det samme som +// `log show` finder for samme tidsrum bagefter. Ellers er målingen UMÅLT. +import { spawn, execFileSync } from 'node:child_process'; + +const PRED_SKIFT = '(subsystem == "com.apple.processmanager" AND eventMessage CONTAINS "SETFRONT")'; +const PRED_AARSAG = '(process == "WindowServer" AND category == "Focus" AND eventMessage CONTAINS "the front process")'; +const PULS = 'cmcp-vagt-puls'; +const vent = (ms) => new Promise(r => setTimeout(r, ms)); + +/// Én hændelse fra loggen (ndjson-linje fra `log stream`/`log show --style ndjson`) +/// → {t, slags:'skift'|'aarsag'|'puls', pid, fra, aarsag}. Ukendt → null. +export function tolk(linje) { + let e; try { e = JSON.parse(linje); } catch { return null; } + const m = e.eventMessage || ''; + const t = Date.parse(String(e.timestamp || '').replace(' ', 'T').replace(/([+-]\d\d)(\d\d)$/, '$1:$2')); + let r; + if ((r = /SETFRONT: pid=(\d+).*oldFrontPid=(-?\d+)/.exec(m))) return { t, slags: 'skift', pid: +r[1], fra: +r[2] }; + if ((r = /\[([A-Za-z]+)\]: \[DataSource\] Making .* the front process/.exec(m))) return { t, slags: 'aarsag', aarsag: r[1] }; + if (m.includes(PULS)) return { t, slags: 'puls' }; + return null; +} + +/// Parrer hvert skift med WindowServers årsag (nærmeste inden for 150 ms). +export function parSkift(haendelser) { + const aarsager = haendelser.filter(h => h.slags === 'aarsag'); + return haendelser.filter(h => h.slags === 'skift').map(s => { + let bedst = null; + for (const a of aarsager) if (Math.abs(a.t - s.t) <= 150 && (!bedst || Math.abs(a.t - s.t) < Math.abs(bedst.t - s.t))) bedst = a; + return { ...s, aarsag: bedst?.aarsag || 'ukendt' }; + }); +} + +/// Dommen over en række skift. `agentPid(pid)` siger om pid'en er et af agenternes programmer. +/// 1. Et agent-program kom forrest af andet end menneskets skrivebordsskift. +/// 2. Forgrunden blev trukket TILBAGE (SetFrontProcess til det program der stod forrest +/// lige før) inden for 1,5 s efter at mennesket selv skiftede skrivebord. +export function doem(skift, agentPid) { + const fejl = []; + skift.forEach((s, i) => { + if (agentPid(s.pid) && s.aarsag !== 'SetManagedDisplayCurrentSpace') + fejl.push({ t: s.t, slags: 'agent-program forrest', pid: s.pid, aarsag: s.aarsag }); + const forrige = skift[i - 1]; + if (s.aarsag === 'SetFrontProcess' && forrige && forrige.aarsag === 'SetManagedDisplayCurrentSpace' + && s.t - forrige.t <= 1500 && s.pid === forrige.fra) + fejl.push({ t: s.t, slags: 'trak forgrunden tilbage fra mennesket', pid: s.pid, aarsag: s.aarsag, efterMs: s.t - forrige.t }); + }); + return fejl; +} + +const hhmmss = (t) => new Date(t).toTimeString().slice(0, 8); + +/// Start strømmen. stop() giver {skift, puls, umaalt?:grund}. +export function startForrestLog() { + const t0 = Date.now(); + const p = spawn('/usr/bin/log', ['stream', '--style', 'ndjson', '--predicate', + `${PRED_SKIFT} OR ${PRED_AARSAG} OR (process == "logger" AND eventMessage CONTAINS "${PULS}")`], + { stdio: ['ignore', 'pipe', 'ignore'] }); + const h = []; let buf = ''; + p.stdout.on('data', d => { buf += d; let i; while ((i = buf.indexOf('\n')) >= 0) { const x = tolk(buf.slice(0, i)); buf = buf.slice(i + 1); if (x) h.push(x); } }); + let n = 0; + const ur = setInterval(() => { try { spawn('logger', [`${PULS} ${++n}`], { stdio: 'ignore' }); } catch {} }, 2000); + return { + async stop() { + clearInterval(ur); + try { spawn('logger', [`${PULS} slut`], { stdio: 'ignore' }); } catch {} + await vent(2500); + try { p.kill(); } catch {} + const t1 = Date.now(); + const skift = parSkift(h); + const puls = h.filter(x => x.slags === 'puls').length; + let umaalt = null; + // Under last (15 agenter) naar ikke hver puls frem til tiden; én pr. 10 s beviser at strømmen lever. + if (puls < Math.max(1, Math.floor((t1 - t0) / 10000))) umaalt = `strømmen svarede ikke: ${puls} puls(er) på ${((t1 - t0) / 1000).toFixed(0)} s`; + // Kalibrering: arkivet skal have præcis de samme skift som strømmen så. + if (!umaalt) { + const fmt = (t) => { const d = new Date(t); const z = (x, n = 2) => String(x).padStart(n, '0'); + return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())} ${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}`; }; + try { + const ud = execFileSync('/usr/bin/log', ['show', '--style', 'ndjson', '--start', fmt(t0 - 1000), '--end', fmt(t1), + '--predicate', PRED_SKIFT], { encoding: 'utf8', maxBuffer: 64 << 20, timeout: 120000 }); + const arkiv = ud.split('\n').map(tolk).filter(x => x?.slags === 'skift' && x.t >= t0 && x.t <= t1 - 2500).length; + const strom = skift.filter(s => s.t >= t0 && s.t <= t1 - 2500).length; + if (arkiv !== strom) umaalt = `strømmen så ${strom} skift, arkivet har ${arkiv} for samme tidsrum`; + } catch (e) { umaalt = `arkivet kunne ikke læses: ${String(e.message).slice(0, 120)}`; } + } + return { skift, puls, umaalt, t0, hhmmss }; + }, + }; +} diff --git a/test/giv-tilbage.mjs b/test/giv-tilbage.mjs index 341dbe3..810eb6d 100644 --- a/test/giv-tilbage.mjs +++ b/test/giv-tilbage.mjs @@ -94,9 +94,12 @@ try { try { execFileSync('pkill', ['-x', navn3]); } catch {} try { execFileSync(LSREGISTER, ['-u', pakke3]); } catch {} - // 4. Et TREDJE program skubber sig frem, mens et andet startes (koersel 7-9: Kontakter - // kom frem, da Aktivitetsovervågning startede). Ingen har rørt tastatur eller mus, - // så det var ikke mennesket - forgrunden skal gives tilbage, uanset hvem der kom frem. + // 4. Et TREDJE program, der koerte i forvejen, kommer frem, mens et andet startes. + // ⛔ VENDT 1/10 (konsulent-panelet, maalt paa Gustavs Mac i macOS' egen log): to + // gange var «det tredje program» mennesket selv, der skiftede skrivebord, og + // hjaelperen rev skaermen tilbage under ham. Et skrivebordsskift er hverken tast + // eller klik, saa det kan ikke skelnes fra et program der skubber sig frem. + // Derfor roeres et program der koerte i forvejen IKKE - forgrunden bliver staaende. await vent(800); const menneske4 = forrest(); const tredje = spawn(join(pakke, 'Contents', 'MacOS', navn), { stdio: ['ignore', 'pipe', 'ignore'], env: { ...process.env, CMCP_PROEVE_FREM_EFTER: '2500' } }); @@ -115,15 +118,60 @@ try { `); execFileSync('cp', [join(pakke, 'Contents', 'MacOS', navn), join(pakke4, 'Contents', 'MacOS', navn4)]); execFileSync(LSREGISTER, ['-f', pakke4]); + // Mindst et sekund mellem det tredje programs start og handlingen: et program + // startet inden for et halvt sekund foer, regnes for handlingens eget. + await vent(1200); const r4 = koer('launch', '--app', bid4, '--background'); const svar4 = JSON.stringify({ ok: r4.ok, took_screen: r4.took_screen, gave_back: r4.gave_back, why: r4.why, observed: r4.observed }); - check('4 et tredje program der skubber sig frem under en start, siges: took_screen: true', r4.ok && r4.took_screen === true, svar4); - check('4b ...og forgrunden gives tilbage, selv om det ikke var det startede program', r4.gave_back === true, svar4); + check('4 et program der koerte i forvejen og kom frem, rives IKKE tilbage', r4.ok && r4.took_screen === false && r4.gave_back === undefined, svar4); + check('4b ...og svaret siger hvad der kom frem', /came to the front/.test(JSON.stringify(r4.observed || {})), svar4); await vent(300); - check('4c det program mennesket var i, er forrest igen', forrest() === menneske4, `${forrest()} (var ${menneske4})`); + check('4c forgrunden blev staaende hos det program der kom frem', forrest() !== menneske4, `${forrest()} (var ${menneske4})`); try { tredje.kill(); } catch {} try { execFileSync('pkill', ['-x', navn4]); } catch {} try { execFileSync(LSREGISTER, ['-u', pakke4]); } catch {} + + // 5. ⛔ 1/10 (M11, konsulent-panelet): et RIGTIGT tastetryk (globalt, uden --app - + // tvinger CGEvent-vejen uden om tilgaengeligheds-indsaettelsen) maa ALDRIG laese + // som «mennesket roerte maskinen» og dermed blokere en legitim tilbagegivelse. + // Det er den PRAECISE fejl M11 muterer: menneskeRoerteNetop() med .keyDown + // inkluderet ville have set agentens EGEN skrivning som et menneske og ladet + // Finder/et selv-aktiverende program blive staaende. Samme attrap-moenster som + // sag 3 (CMCPFremVedStart), en FRISK proces, saa selv-aktiveringen er aekte. + const navn5 = 'cmcptast' + Math.random().toString(36).slice(2, 7); + const bid5 = 'dk.agent360.cmcp.' + navn5; + const pakke5 = join(START_DIR, navn5 + '.app'); + mkdirSync(join(pakke5, 'Contents', 'MacOS'), { recursive: true }); + writeFileSync(join(pakke5, 'Contents', 'Info.plist'), ` + + +CFBundleIdentifier${bid5} +CFBundleExecutable${navn5} +CFBundlePackageTypeAPPL +LSUIElement +CMCPFremVedStart +`); + execFileSync('cp', [join(pakke, 'Contents', 'MacOS', navn), join(pakke5, 'Contents', 'MacOS', navn5)]); + execFileSync(LSREGISTER, ['-f', pakke5]); + const menneske5 = forrest(); + // Intet --app: tvinger den globale CGEvent-vej (samme vej det almindelige «type» + // ville have brugt FOER rettelsen af menneske-simulationen i parallel.mjs). + koer('type', '--text', 'x'); + const r5 = koer('launch', '--app', bid5, '--background'); + const svar5 = JSON.stringify({ ok: r5.ok, took_screen: r5.took_screen, gave_back: r5.gave_back, why: r5.why, observed: r5.observed }); + // ⛔ MAALT 2/10 (koersel 36907620350): under belastning (Hele suiten/15 agenter) kan + // selve REAKTIVERINGEN tabe kaploebet mod 500 ms og give `gave_back:false` - en + // KENDT, UBESLAEGTET timing-flage i NSRunningApplication.activate(), ikke den fejl + // M11 tester. De to grene har hver sin PRAECISE signatur: menneskeRoerteNetop() + // returnerer ALTID `observed.note` med «taken to be them» FOER noget forsoeges + // reaktiveret; den langsomme-reaktivering-grenen har ALDRIG et `observed`-felt. + // Proeven maaler derfor PRAECIS det, og kun det: blev handlingen fejlagtigt tilskrevet + // mennesket - ikke om selve genaktiveringen naaede at fuldfoeres inden for 500 ms. + check('5 et rigtigt tastetryk lige foer bliver IKKE laest som mennesket', + r5.ok && r5.took_screen === true && r5.observed === undefined, svar5); + try { execFileSync('pkill', ['-x', navn5]); } catch {} + try { execFileSync(LSREGISTER, ['-u', pakke5]); } catch {} + } finally { try { b.kill(); } catch {} rmSync(ARB, { recursive: true, force: true }); diff --git a/test/ikon-godkend.mjs b/test/ikon-godkend.mjs index 699e2c8..3b1390a 100644 --- a/test/ikon-godkend.mjs +++ b/test/ikon-godkend.mjs @@ -14,6 +14,7 @@ // P7 ukendt maal, usloeret billede ....... A4, A5 // kapløb mellem servere .................. B1 // tjek igen efter ja ..................... C1 +import './ryd-op.mjs'; import { createServer } from 'node:net'; import { spawn } from 'node:child_process'; import { mkdtempSync, writeFileSync, existsSync, readFileSync, chmodSync } from 'node:fs'; @@ -72,11 +73,21 @@ check('A10b efterladt socket-fil: genkendt som «ikonet koerer ikke»', (await import('node:fs')).unlinkSync(join(STATE_A, 'ikon.sock')); let mode = 'ja'; -const ikon = await lavIkon(STATE_A, q => { +// ⛔ 29/9: closuren tog ikke `sock`, saa «sent-ja» kastede en ReferenceError som +// try{} slugte - det sene ja blev ALDRIG sendt, og A13 maalte kun «intet svar». +const ikon = await lavIkon(STATE_A, (q, sock) => { if (mode === 'ja') return { nonce: q.nonce, ok: true, verified: 'owner' }; if (mode === 'uden-bekraeftelse') return { nonce: q.nonce, ok: true, verified: 'none' }; if (mode === 'forkert-nonce') return { nonce: 'f'.repeat(32), ok: true, verified: 'owner' }; if (mode === 'nej') return { nonce: q.nonce, ok: false, verified: 'none' }; + if (mode === 'krasj') { sock.destroy(); return null; } // ikonet gaar ned midt i spoergsmaalet + if (mode === 'skrald') { sock.write('ikke json\n'); return null; } + if (mode === 'delt') { // svaret kommer i to stykker + const hel = JSON.stringify({ nonce: q.nonce, ok: true, verified: 'owner' }) + '\n'; + sock.write(hel.slice(0, 10)); + setTimeout(() => { try { sock.write(hel.slice(10)); } catch {} }, 80); + return null; + } if (mode === 'sent-ja') { // svarer FOR SENT, med et gyldigt ja setTimeout(() => { try { sock.write(JSON.stringify({ nonce: q.nonce, ok: true, verified: 'owner' }) + '\n'); } catch {} }, 2600); return null; @@ -141,6 +152,17 @@ await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describ const q11 = ikon.modtaget[ikon.modtaget.length - 1]; check('A11 linjeskift renses foer teksten naar ikonet', !/[\r\n]/.test(q11.text), JSON.stringify(q11.text)); +// A14/A15 (29/9, panelet): et ja skal daekke ALT det mennesket saa. Foer klippede +// protokollen teksten tavst ved 200 tegn - resten naaede aldrig ikonet. +const lang = 'Send til Benjamin: ' + 'computer-MCP virker. '.repeat(140); // ~2.960 tegn +await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: lang, alwaysAsk: true, ikon: IKON }); +const q14 = ikon.modtaget[ikon.modtaget.length - 1]; +check('A14 en lang tekst naar ikonet HEL, ikke klippet', q14?.text === lang.trim(), `${q14?.text?.length} af ${lang.trim().length} tegn`); +const foer15 = antal(); +const a15 = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'x'.repeat(4001), alwaysAsk: true, ikon: IKON }); +check('A15 over loftet: ikke spurgt og afvist - intet ja til noget halvt laest', + !a15.allow && antal() === foer15 && /more than the 4000/.test(a15.reason), a15.reason); + // A12: hoejst ét aabent spoergsmaal pr. agent. mode = 'tavs'; const [b1, b2] = await Promise.all([ @@ -153,6 +175,33 @@ check('A12 to samtidige spoergsmaal fra samme agent: det andet naar aldrig ikone check('A8 intet svar i tide: afvist og i koeen', [b1, b2].some(v => !v.allow && v.koe && /nobody answered/.test(v.reason))); +// F (29/9, panelet: «lad porten fejle og komme tilbage» foer den udvides). +// Porten er brugt 3 gange paa ni dage; hver fejlvej skal ende i et nej - og +// den NAESTE spoergsmaal skal virke, ellers har én fejl lukket porten for altid. +mode = 'krasj'; +const f1 = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); +check('F1 ikonet gaar ned midt i spoergsmaalet: et nej, ikke et ja', !f1.allow && /closed without an answer|could not be reached/.test(f1.reason), f1.reason); +mode = 'ja'; +const f1b = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); +check('F1b ...og det naeste spoergsmaal virker straks (ingen haengende «venter», ingen straf-pause)', f1b.allow, f1b.reason); +mode = 'skrald'; +const f2 = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); +check('F2 et svar der ikke kan laeses: et nej', !f2.allow && /did not match/.test(f2.reason), f2.reason); +mode = 'delt'; +const f3 = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); +check('F3 et ja der kommer i to stykker, laeses som ét', f3.allow, f3.reason); +// F4: ikonet doer helt og startes igen - porten skal finde det nye. +ikon.srv.close(); +try { (await import('node:fs')).unlinkSync(join(STATE_A, 'ikon.sock')); } catch {} +const f4 = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); +check('F4 ikonet er vaek: afvist uden at haenge', !f4.allow && /not running/.test(f4.reason), f4.reason); +mode = 'ja'; +const ikon2 = await lavIkon(STATE_A, q => ({ nonce: q.nonce, ok: mode !== 'nej', verified: mode === 'nej' ? 'none' : 'owner' })); +const f4b = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); +check('F4b ...et nyt ikon paa samme sted: porten virker igen', f4b.allow && ikon2.modtaget.length === 1, f4b.reason); +// Resten af del A taler med det nye ikon. +ikon.srv = ikon2.srv; ikon.modtaget = ikon2.modtaget; + // A9: et nej giver en pause, saa agenten ikke kan traette mennesket til et ja. mode = 'nej'; const a9 = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); @@ -162,6 +211,36 @@ const a9b = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.find check('A9 et nej er et nej', !a9.allow && /said no/.test(a9.reason), a9.reason); check('A9 ...og samme agent maa ikke spoerge igen lige efter', !a9b.allow && antal() === foer9, a9b.reason); check('A ingen osascript-dialog blev forsoegt i hele del A', dialogAttrap.gangeSpurgt() === 0, `${dialogAttrap.gangeSpurgt()} forsoeg`); + +// G (runde 1 30/9, Astra 7 + Fable F1): i FORGRUND - og dermed under et skaerm-laan - +// maa et «ikke spurgt» fra ikonet aldrig blive til en osascript-boks. Kun et ikon +// der slet ikke koerer, giver boksen. Pausen fra A9s nej er stadig aktiv her. +process.env.CMCP_BACKGROUND = '0'; +const dG = dialogAttrap.gangeSpurgt(); +const g1 = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); +check('G1 forgrund lige efter et nej i ikonet: afvist, ingen boks', !g1.allow && /said no/.test(g1.reason) && dialogAttrap.gangeSpurgt() === dG, g1.reason); +const g2 = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'x'.repeat(4001), alwaysAsk: true, ikon: IKON }); +check('G2 forgrund, tekst over loftet: afvist, ingen boks', !g2.allow && /more than the 4000/.test(g2.reason) && dialogAttrap.gangeSpurgt() === dG, g2.reason); +ikon.srv.close(); ikon.srv = { close() {} }; +try { (await import('node:fs')).unlinkSync(join(STATE_A, 'ikon.sock')); } catch {} +const g3a = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: IKON }); +check('G3a ikonet koerer ikke OG personen sagde lige nej: afvist, ingen boks (Astra R2 6)', !g3a.allow && /said no/.test(g3a.reason) && dialogAttrap.gangeSpurgt() === dG, g3a.reason); +const g3b = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'x'.repeat(4001), alwaysAsk: true, ikon: IKON }); +check('G3b ikonet koerer ikke, tekst over loftet: afvist, ingen boks', !g3b.allow && /more than the 4000/.test(g3b.reason) && dialogAttrap.gangeSpurgt() === dG, g3b.reason); +// G3c: uden pause og uden ikon ER boksen faldbag - maalt i en frisk proces (pausen lever i modulet). +{ + const { execFileSync: ef } = await import('node:child_process'); + const sp3 = lavFalskSpoerger('ja', 'cmcp-godkend-g3'); + const kode = `const P = await import(${JSON.stringify(join(ROOT, 'mcp-server', 'policy.js'))}); + const v = await P.decide({ tier: P.TIER.WRITE, targetBundleId: 'com.apple.finder', describe: 'Quit Finder', alwaysAsk: true, ikon: { session: 's', client: 'c' } }); + console.log(JSON.stringify(v));`; + const ud = ef(process.execPath, ['--input-type=module', '-e', kode], { encoding: 'utf8', timeout: 60000, + env: { ...process.env, CMCP_BACKGROUND: '0', CMCP_STATUS_IKON: '0', CMCP_ASK_TIMEOUT: '2', CMCP_OSASCRIPT: sp3.sti, + CMCP_STATE_DIR: mkdtempSync(join(tmpdir(), 'cmcp-godkend-g3-')) } }); + const g3c = JSON.parse(ud.trim().split('\n').pop()); + check('G3c forgrund, ikonet koerer ikke, ingen pause: boksen er faldbag', g3c.allow && sp3.gangeSpurgt() === 1, JSON.stringify(g3c).slice(0, 100)); +} +delete process.env.CMCP_BACKGROUND; ikon.srv.close(); // ─── B: to rigtige servere spoerger samtidig ───────────────────────────── @@ -331,6 +410,54 @@ check('D2 ...og anden skrivning spurgte ikke igen', !d2b.result.isError && ikonD D2.srv.kill(); ikonD.srv.close(); process.env.CMCP_MODE = 'allow'; +// ─── E: computer_ask_user via ikonet (29/9, panelet) ───────────────────── +// I baggrund gaar spoergsmaalet til ikonet som «goer det selv». «Done» er et +// SIGNAL: det maa aldrig kunne blive et ja til en anden handling. +const STATE_E = mkdtempSync(join(tmpdir(), 'cmcp-godkend-e-')); +const hjE = lavFalskHjaelper('cmcp-godkend-e'); +let eSvar = 'done'; +const ikonE = await lavIkon(STATE_E, q => eSvar === 'done' ? { nonce: q.nonce, ok: true, verified: 'done' } + : eSvar === 'nej' ? { nonce: q.nonce, ok: false, verified: 'none' } : null); +const spE = lavFalskSpoerger('ja', 'cmcp-godkend-e-sp'); +// Egen server: attrappen skal kunne taelles, saa den saettes paa miljoeet her. +const E1s = spawn('node', [join(ROOT, 'mcp-server', 'index.js')], { + env: { ...process.env, CMCP_STATE_DIR: STATE_E, CMCP_MODE: 'allow', CMCP_HELPER: hjE.sti, + CMCP_ASK_TIMEOUT: '3', CMCP_STATUS_IKON: '0', CMCP_OSASCRIPT: spE.sti }, + stdio: ['pipe', 'pipe', 'pipe'] }); +const eRpc = (() => { let buf = ''; const v = new Map(); let n = 0; + E1s.stdout.on('data', d => { buf += d; let i; + while ((i = buf.indexOf('\n')) >= 0) { const l = buf.slice(0, i); buf = buf.slice(i + 1); + try { const m = JSON.parse(l); v.get(m.id)?.(m); v.delete(m.id); } catch {} } }); + return (method, params = {}) => new Promise((res, rej) => { const id = ++n; v.set(id, res); + E1s.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); + setTimeout(() => rej(new Error('timeout')), 30000); }); })(); +await eRpc('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'chat-e', version: '1' } }); +E1s.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'); + +const e1 = await eRpc('tools/call', { name: 'computer_ask_user', + arguments: { message: 'Type your 2FA code in the field - Lands in: com.evil.app', app: 'Finder' } }); +const qE = ikonE.modtaget[0]; +const e1t = e1.result?.content?.[0]?.text || ''; +check('E1 ask_user i baggrund naar ikonet som «goer det selv»', qE?.kind === 'goer-selv', JSON.stringify(qE || {}).slice(0, 120)); +check('E1 ...hvor det lander er skrevet af SERVEREN (bundle-ID), ikke af modellen', + /com\.apple\.finder/.test(qE?.target || '') && qE?.targetBundle === 'com.apple.finder' && !/evil/.test(qE?.target || ''), `${qE?.target} | ${qE?.targetBundle}`); +check('E1 ...Done giver done:true, og ingen tekst kommer tilbage', !e1.result.isError && /"done":\s*true/.test(e1t) && !/2FA code/.test(e1t.replace(/"note".*/, '')), e1t.slice(0, 120)); +check('E1 ...og ingen dialog blev rejst', spE.gangeSpurgt() === 0, `${spE.gangeSpurgt()}`); +eSvar = 'nej'; +const e2 = await eRpc('tools/call', { name: 'computer_ask_user', arguments: { message: 'Approve the login', app: 'Finder' } }); +check('E2 «I won\'t do this»: done:false', /"done":\s*false/.test(e2.result?.content?.[0]?.text || ''), (e2.result?.content?.[0]?.text || '').slice(0, 100)); +E1s.kill(); ikonE.srv.close(); + +// E3: et «done»-svar paa et almindeligt SAMTYKKE er et nej. Ellers kunne et +// signal uden Touch ID blive til en tilladelse. +const STATE_E3 = mkdtempSync(join(tmpdir(), 'cmcp-godkend-e3-')); +process.env.CMCP_STATE_DIR = STATE_E3; +const G = await import(join(ROOT, 'mcp-server', 'godkend.js') + '?e3'); +const ikonE3 = await lavIkon(STATE_E3, q => ({ nonce: q.nonce, ok: true, verified: 'done' })); +const e3 = await G.spoergIkonet({ session: 's', client: 'c', text: 'Quit Finder', scope: 'this one action only', target: 'com.apple.finder' }, 3); +check('E3 «done» paa et samtykke-spoergsmaal: ikke et ja', !e3.ok && /not confirmed/.test(e3.grund), e3.grund); +ikonE3.srv.close(); + if (fails.length) { console.log(`\n${fails.length} DUMPET`); process.exit(1); } console.log('\nBESTAAET'); process.exit(0); diff --git a/test/ikon-menu.mjs b/test/ikon-menu.mjs new file mode 100644 index 0000000..ba38e6f --- /dev/null +++ b/test/ikon-menu.mjs @@ -0,0 +1,82 @@ +// IKON-MENU: viser menulinje-ikonet det rigtige for hvert slags spoergsmaal? +// +// ⛔ HVORFOR DEN FINDES (29/9-2026) +// Tre nye ting i ikonet - hele teksten over knapperne, «goer det selv» med Done, +// og skaerm-laanet - var kun kompileret, aldrig koert. Menuen bygges af +// `spoergsmaalMenu`, og `cmcp-status --dump-question` skriver praecis den +// tekst som JSON UDEN at vise noget. Ikonet startes aldrig her. +import './ryd-op.mjs'; +import { execFileSync } from 'node:child_process'; +import { existsSync, statSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; + +// ⛔ En AELDRE binaer kender ikke --dump-question og ville starte det rigtige ikon. +// Derfor den nyeste af kandidaterne, og en hard frist. +// En udpeget binaer (mutationsbeviset) vinder altid over den nyeste. +const IKON = process.env.CMCP_STATUS_BIN || [ + join(ROOT, 'mcp-server', 'vendor', 'ComputerMCPStatus.app', 'Contents', 'MacOS', 'cmcp-status'), + join(ROOT, 'helper', '.build', 'release', 'cmcp-status')] + .filter(p => p && existsSync(p)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0]; +const STATE = mkdtempSync(join(tmpdir(), 'cmcp-ikonmenu-')); +const vis = (q) => JSON.parse(execFileSync(IKON, ['--dump-question'], { + input: JSON.stringify({ nonce: 'n', session: 's1', client: 'chat', scope: 'If you allow it, this one action only.', + target: 'com.apple.finder', expires: Date.now() + 60000, ...q }), + env: { ...process.env, CMCP_STATE_DIR: STATE }, timeout: 10000, killSignal: 'SIGKILL' }).toString()); + +if (!IKON) { console.log('SPR. ingen ikon-binaer at maale'); process.exit(1); } + +// 1. Et almindeligt samtykke: hele teksten, fakta, Allow/Deny. +// Serveren trimmer teksten foer ikonet faar den (godkend.js), saa proeven goer det samme. +const lang = ('Send til Benjamin: ' + 'computer-MCP virker. '.repeat(40)).trim(); +const a = vis({ text: lang }); +check('1 samtykke: Allow og Deny, i den raekkefoelge', JSON.stringify(a.buttons) === JSON.stringify(['Allow… (confirm with Touch ID)', 'Deny']), JSON.stringify(a.buttons)); +check('1 ...hele teksten staar i undermenuen', a.text.join(' ').replace(/\s+/g, '') === lang.trim().replace(/\s+/g, ''), `${a.text.length} linjer`); +check('1 ...overskriften siger hvor mange tegn', a.header === `The whole action (${lang.trim().length} characters):`, a.header); +check('1 ...hovedmenuen viser en kort udgave', a.title.endsWith('…') && a.title.length < 100, a.title); +check('1 ...hvor det lander staar som fakta', a.facts.includes('Lands in: com.apple.finder'), JSON.stringify(a.facts)); +check('1 ...Touch ID-arket siger at det er forkortet, og hvor resten staar', /characters, all shown in the menu/.test(a.touchId), a.touchId.slice(0, 120)); + +// 2. Goer det selv: ingen Allow, ingen Touch ID-knap. +const g = vis({ text: 'Type your 2FA code', kind: 'goer-selv', targetBundle: 'com.apple.finder' }); +check('2 goer-selv: Take me there, Done, I won\'t - og INGEN Allow', + JSON.stringify(g.buttons) === JSON.stringify(['Take me there', 'Done — I did it', "I won't do this"]), JSON.stringify(g.buttons)); +const g2 = vis({ text: 'Approve the login', kind: 'goer-selv' }); +check('2b uden program at hente frem: ingen Take me there', !g2.buttons.includes('Take me there'), JSON.stringify(g2.buttons)); + +// 3. Skaerm-laanet: Allow kraever Touch ID, og mens laanet gaelder staar det oeverst. +const l = vis({ text: 'Use your screen for 5 minutes: drag the file', kind: 'screen', minutes: 5, target: 'your screen' }); +check('3 laan: Allow med Touch ID', l.buttons[0] === 'Allow… (confirm with Touch ID)', JSON.stringify(l.buttons)); +check('3 ...Touch ID-arket siger laan, minutter og at det kan tages tilbage', /lend chat your screen for 5 minutes/.test(l.touchId) && /take it back/.test(l.touchId), l.touchId); +check('3 ...og mens det gaelder: hvem, hvor laenge, og «Take the screen back now»', + /chat is using your screen — 5 min left/.test(l.whileLent?.[0] || '') && l.whileLent?.[1] === 'Take the screen back now', JSON.stringify(l.whileLent)); +// 3c (live-proeven 30/9): boksen paa skaermen skal SIGE at et spoergsmaal venter, og hvor. +const bx = vis({ text: 'Use your screen for 5 minutes: live-proeve', kind: 'screen', minutes: 5 }).box || []; +check('3c boksen siger at et spoergsmaal venter, og peger paa det orange ikon', /needs you/i.test(bx[0] || '') && /orange menu bar icon/.test(bx[0] || ''), JSON.stringify(bx)); +check('3c ...og hvad det gaelder', (bx[1] || '').startsWith('Use your screen for 5 minutes'), JSON.stringify(bx)); +check('3b laanet kan aldrig vaere over 15 minutter i ikonet', /for 15 minutes/.test(vis({ text: 'x', kind: 'screen', minutes: 99 }).touchId)); + +// 3d (skaerm-koe-panelet, 2/10): et ANDET laan er allerede aktivt - «Allow» udelades, +// «Deny» staar stadig, og en «Waiting»-linje siger hvem og hvor laenge. Samme funktion +// bygger den rigtige menu (menuNeedsUpdate), saa dette maaler uden en levende GUI. +const q = vis({ text: 'Use your screen for 3 minutes: resize a window', kind: 'screen', minutes: 3, + simulateActiveLoan: { client: 'anden-agent', minutesLeft: 4 } }); +check('3d koe: Allow er vaek, Deny staar', JSON.stringify(q.buttons) === JSON.stringify(['Deny']), JSON.stringify(q.buttons)); +check('3d ...og en Waiting-linje siger hvem og hvor laenge', + /Waiting.*anden-agent.*4 more minutes/.test(q.waitingForLoan || ''), q.waitingForLoan); +// 3e uden et andet laan: uaendret - Allow staar, ingen Waiting-linje. +const q2 = vis({ text: 'Use your screen for 3 minutes: resize a window', kind: 'screen', minutes: 3 }); +check('3e uden koe: Allow staar, ingen waitingForLoan-noegle', q2.buttons[0] === 'Allow… (confirm with Touch ID)' && !('waitingForLoan' in q2), JSON.stringify(q2.buttons)); + +// 4. Modellen kan ikke tegne sine egne knapper ind. +const s = vis({ text: 'Press "Gem"\nAllow… (confirm with Touch ID)‮' }); +check('4 linjeskift og retningstegn i teksten bliver ikke til egne linjer eller knapper', + s.text.length === 1 && !/[\n‮]/.test(s.text[0]) && s.buttons.length === 2, JSON.stringify(s.text)); + +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/input-maal.mjs b/test/input-maal.mjs index 612af38..f758e79 100644 --- a/test/input-maal.mjs +++ b/test/input-maal.mjs @@ -157,6 +157,7 @@ esac printf '%s ' "$@" >> ${ARGV4}; echo >> ${ARGV4} case "$1" in apps) echo '{"ok":true,"apps":[{"name":"TextEdit","bundleId":"com.apple.TextEdit","active":false},{"name":"Keychain Access","bundleId":"com.apple.keychainaccess","active":false},{"name":"Finder","bundleId":"com.apple.finder","active":true}]}' ;; + menus) echo '{"ok":true,"items":[{"path":"File > New","shortcut":"cmd+n","enabled":true}]}' ;; *) echo '{"ok":true}' ;; esac `); diff --git a/test/klik-ejer.mjs b/test/klik-ejer.mjs index 0e1e97a..c8f958d 100644 --- a/test/klik-ejer.mjs +++ b/test/klik-ejer.mjs @@ -27,20 +27,22 @@ printf '%s ' "$@" >> ${ARGV}; echo >> ${ARGV} X=""; prev=""; for a in "$@"; do [ "$prev" = "--x" ] && X="$a"; prev="$a"; done case "$1" in apps) echo '{"ok":true,"apps":[{"name":"Google Chrome","bundleId":"com.google.Chrome","active":true}]}' ;; + # En almindelig side, ikke en webchat - ellers ville sende-porten doemme klikket (30/9). + samtale) echo '{"ok":true,"window":"Example Domain - Google Chrome"}' ;; at) # Markoeren staar over et Passwords-vindue. - case " $* " in *" --pointer "*) echo '{"ok":true,"found":true,"bundleId":"com.apple.Passwords"}'; exit 0 ;; esac + case " $* " in *" --pointer "*) echo '{"ok":true,"found":true,"bundleId":"com.apple.Passwords","role":"AXGroup","window":"Passwords"}'; exit 0 ;; esac case "$X" in - 100) echo '{"ok":true,"found":true,"bundleId":"com.google.Chrome"}' ;; - 200) echo '{"ok":true,"found":true,"bundleId":"com.apple.Passwords"}' ;; + 100) echo '{"ok":true,"found":true,"bundleId":"com.google.Chrome","role":"AXStaticText","window":"Example Domain - Google Chrome"}' ;; + 200) echo '{"ok":true,"found":true,"bundleId":"com.apple.Passwords","role":"AXGroup","window":"Passwords"}' ;; 400) # Foerste opslag: Chrome. Derefter: Passwords - et vindue kom frem imens. - if [ -f ${join(D, 'skiftet')} ]; then echo '{"ok":true,"found":true,"bundleId":"com.apple.Passwords"}' - else : > ${join(D, 'skiftet')}; echo '{"ok":true,"found":true,"bundleId":"com.google.Chrome"}'; fi ;; + if [ -f ${join(D, 'skiftet')} ]; then echo '{"ok":true,"found":true,"bundleId":"com.apple.Passwords","role":"AXGroup","window":"Passwords"}' + else : > ${join(D, 'skiftet')}; echo '{"ok":true,"found":true,"bundleId":"com.google.Chrome","role":"AXStaticText","window":"Example Domain - Google Chrome"}'; fi ;; 500) # Tilgaengeligheds-laget siger Chrome; vindues-stakken har 1Password under et gennemsigtigt lag. - echo '{"ok":true,"found":true,"bundleId":"com.google.Chrome","under":["com.apple.dock","com.agilebits.onepassword7"]}' ;; + echo '{"ok":true,"found":true,"bundleId":"com.google.Chrome","role":"AXStaticText","window":"Example Domain - Google Chrome","under":["com.apple.dock","com.agilebits.onepassword7"]}' ;; 600|700) # Foerste opslag: ingen ejer. Derefter: 600 -> Passwords, 700 -> Chrome. if [ -f ${join(D, 'set-')}$X ]; then - [ "$X" = 600 ] && echo '{"ok":true,"found":true,"bundleId":"com.apple.Passwords"}' || echo '{"ok":true,"found":true,"bundleId":"com.google.Chrome"}' + [ "$X" = 600 ] && echo '{"ok":true,"found":true,"bundleId":"com.apple.Passwords","role":"AXGroup","window":"Passwords"}' || echo '{"ok":true,"found":true,"bundleId":"com.google.Chrome","role":"AXStaticText","window":"Example Domain - Google Chrome"}' else : > ${join(D, 'set-')}$X; echo '{"ok":true,"found":false}'; fi ;; *) echo '{"ok":true,"found":false}' ;; esac ;; diff --git a/test/knap-ord.mjs b/test/knap-ord.mjs new file mode 100644 index 0000000..0e3d3b3 --- /dev/null +++ b/test/knap-ord.mjs @@ -0,0 +1,108 @@ +// En KNAP der sletter, rydder eller lukker, spoerger - som et menupunkt med samme ord. +// +// ⛔ HVORFOR DEN FINDES (1/10-2026, konsulent-panelet om skaerm-koeen) +// README lover «Anything that deletes or clears asks every time, recognised from the +// words in the action itself». Ordene blev kun laest paa menuer og genveje: et tryk +// paa en knap der hed «Slet», eller et klik paa «Erase», spurgte aldrig. +// Intet startes, intet tager skaermen: attrap-hjaelper og attrap-samtykke. +import './ryd-op.mjs'; +import { spawn } from 'node:child_process'; +import { writeFileSync, chmodSync, readFileSync, existsSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { lavFalskSpoerger } from './falsk-hjaelper.mjs'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; +const D = mkdtempSync(join(tmpdir(), 'cmcp-knapord-')); +const ARGV = join(D, 'argv.txt'), KNAP = join(D, 'knap.json'), AT = join(D, 'at.json'), STUB = join(D, 'stub.sh'); +writeFileSync(STUB, `#!/bin/sh +printf '%s ' "$@" >> ${ARGV}; echo >> ${ARGV} +case "$1" in + apps) echo '{"ok":true,"apps":[{"bundleId":"com.apple.TextEdit","name":"TextEdit","pid":4242,"active":false}]}' ;; + resolve-app) echo '{"ok":true,"bundleId":"com.apple.TextEdit","running":true}' ;; + press) case " $* " in *" --dry "*) cat ${KNAP} ;; *) echo '{"ok":true,"pressed":{}}' ;; esac ;; + at) cat ${AT} ;; + *) echo '{"ok":true}' ;; +esac +`); +chmodSync(STUB, 0o755); +const knap = (navn) => writeFileSync(KNAP, JSON.stringify({ ok: true, would_press: { name: navn, role: 'AXButton' } })); +const ved = (titel) => writeFileSync(AT, JSON.stringify({ ok: true, found: true, bundleId: 'com.apple.TextEdit', role: 'AXButton', title: titel })); + +const spoerger = lavFalskSpoerger('udloeb', 'cmcp-knapord'); +const srv = spawn('node', [join(ROOT, 'mcp-server/index.js')], { + env: { ...process.env, CMCP_HELPER: STUB, CMCP_STATUS_IKON: '0', CMCP_NO_PARENT_WATCH: '1', + CMCP_STATE_DIR: join(D, 'state'), CMCP_BACKGROUND: '0', CMCP_MODE: 'allow', + CMCP_ASK_TIMEOUT: '2', CMCP_OSASCRIPT: spoerger.sti }, + stdio: ['pipe', 'pipe', 'pipe'] }); +let buf = '', n = 0; const w = new Map(); +srv.stdout.on('data', d => { buf += d; let i; while ((i = buf.indexOf('\n')) >= 0) { const l = buf.slice(0, i); buf = buf.slice(i + 1); try { const m = JSON.parse(l); w.get(m.id)?.(m); } catch {} } }); +const rpc = (m, p) => new Promise(r => { const id = ++n; w.set(id, r); srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method: m, params: p }) + '\n'); }); +const kald = async (navn, args) => (await rpc('tools/call', { name: navn, arguments: args })).result?.content?.[0]?.text || ''; +await rpc('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'knapord', version: '1' } }); +srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'); +const linjer = () => (existsSync(ARGV) ? readFileSync(ARGV, 'utf8') : '').split('\n'); +const trykket = () => linjer().filter(l => l.startsWith('press ') && !l.includes('--dry')).length; +const klikket = () => linjer().filter(l => l.startsWith('click ')).length; + +knap('Slet'); let f = trykket(); +await kald('computer_press', { app: 'com.apple.TextEdit', title: 'Slet' }); +check('1 en knap der hedder «Slet», trykkes IKKE uden et ja', trykket() === f); + +knap('Erase'); f = trykket(); +await kald('computer_press', { app: 'com.apple.TextEdit', contains: 'ras' }); +check('2 knappens EGET navn afgør det, ikke det agenten søgte på («ras» -> «Erase»)', trykket() === f); + +knap('Gem'); f = trykket(); +await kald('computer_press', { app: 'com.apple.TextEdit', title: 'Gem' }); +check('3 en harmløs knap («Gem») trykkes uden at spørge', trykket() === f + 1); + +ved('Delete'); f = klikket(); +await kald('computer_click', { app: 'com.apple.TextEdit', x: 100, y: 200 }); +check('4 et klik der rammer «Delete», sker IKKE uden et ja', klikket() === f); + +// ⛔ 2/10 (Opus-panelet): et klik rammer UANSET om opslaget her kan sige hvad +// der er på punktet - i modsætning til press, som slet ikke kan udføres uden +// et fundet element. Før fejlede disse tre tilfælde ÅBENT (ikke farligt). +writeFileSync(AT, JSON.stringify({ ok: true, found: false })); f = klikket(); +await kald('computer_click', { app: 'com.apple.TextEdit', x: 10, y: 10 }); +check('6 intet fundet på punktet: klikket sker IKKE uden et ja (fail closed)', klikket() === f); + +writeFileSync(AT, JSON.stringify({ ok: true, found: true, bundleId: 'com.apple.TextEdit', role: 'AXUnknown' })); f = klikket(); +await kald('computer_click', { app: 'com.apple.TextEdit', x: 10, y: 10 }); +check('7 fundet, men uden navn eller beskrivelse: klikket sker IKKE uden et ja', klikket() === f); + +const stubKilde = readFileSync(STUB, 'utf8'); +writeFileSync(STUB, stubKilde.replace(/^ at\) cat .*$/m, ' at) exit 1 ;;')); f = klikket(); +await kald('computer_click', { app: 'com.apple.TextEdit', x: 10, y: 10 }); +check('8 opslaget selv fejler: klikket sker IKKE uden et ja', klikket() === f); +writeFileSync(STUB, stubKilde); + +// ⛔ RETTET samme dag (CI fandt det, test/sende-port.mjs q9): en navnløs +// AXGroup er et helt almindeligt layout-lag, ikke en mulig "Slet"-knap - +// kun roller der plausibelt ER en knap (ukendt, Button, Image, Unknown) +// spørger når navnet mangler. Uden denne sondring spurgte porten på HVERT +// navnløst klik, også harmløse - for bredt. +writeFileSync(AT, JSON.stringify({ ok: true, found: true, bundleId: 'com.apple.TextEdit', role: 'AXGroup' })); f = klikket(); +await kald('computer_click', { app: 'com.apple.TextEdit', x: 10, y: 10 }); +check('9 fundet uden navn, men rollen er en harmløs gruppe: klikket sker UDEN at spørge', klikket() === f + 1); + +// ⛔ review-security F5 (2/10), Gustav ja: samme klasse fejl som klikkets F1 i dag, +// bare paa tryk-grenen - et mislykket toerkoersels-opslag blev laest som "ufarligt". +writeFileSync(KNAP, JSON.stringify({ ok: true })); f = trykket(); +await kald('computer_press', { app: 'com.apple.TextEdit', title: 'Hvad Som Helst' }); +check('11 dry-run finder intet at trykke (intet would_press): trykket sker IKKE uden et ja (fail closed)', trykket() === f); + +const stubKilde2 = readFileSync(STUB, 'utf8'); +writeFileSync(STUB, stubKilde2.replace(/^ press\) case.*$/m, ' press) exit 1 ;;')); f = trykket(); +await kald('computer_press', { app: 'com.apple.TextEdit', title: 'Noget' }); +check('12 selve toerkoerslen fejler: trykket sker IKKE uden et ja (fail closed)', trykket() === f); +writeFileSync(STUB, stubKilde2); + +check('10 kalibrering: porten spurgte et menneske om 1, 2, 4, 6, 7, 8, 11 og 12', spoerger.gangeSpurgt() === 8, `spurgt ${spoerger.gangeSpurgt()} gange`); +srv.kill(); +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/laas-fejler-lukket.mjs b/test/laas-fejler-lukket.mjs new file mode 100644 index 0000000..3bc8350 --- /dev/null +++ b/test/laas-fejler-lukket.mjs @@ -0,0 +1,41 @@ +// M1 (28/9): programlåsen fejler LUKKET ved en infra-fejl. +// +// ⛔ HVORFOR DEN FINDES +// programlaas.js:45 kørte FØR handlingen alligevel (fail-open), hvis låsen +// ikke kunne tages af en anden grund end "optaget" (EEXIST). En låse-mappe +// der bliver en fil giver ENOTDIR - og så skrev to agenter i flæng i samme +// program, netop det låsen skal forhindre. Prøven tvinger ENOTDIR frem og +// kræver, at handlingen AFVISES (ok:false), ikke udføres. +import './ryd-op.mjs'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; + +// Gør 'laase'-stien til en FIL, så openSync inde i den giver ENOTDIR (ikke EEXIST). +const base = mkdtempSync(join(tmpdir(), 'cmcp-laas-')); +writeFileSync(join(base, 'laase'), 'jeg er en fil, ikke en mappe'); +process.env.CMCP_STATE_DIR = base; + +// DIR læses ved modul-load fra env, så env SKAL sættes før import. +const { medProgramLaas } = await import('../mcp-server/programlaas.js'); + +let kørt = false; +const r = await medProgramLaas('com.apple.TextEdit', async () => { kørt = true; return 'skulle-ikke-ske'; }); + +check('1 laasen fejler LUKKET (ok:false), ikke aaben', r.ok === false, JSON.stringify(r)); +check('2 handlingen blev IKKE udfoert', kørt === false); +check('3 afslaget siger hvorfor (infra, ikke contention)', /could not be taken|serialised/.test(r.grund || ''), r.grund); + +// Kontrol: en ren mappe låser og kører handlingen som normalt. +const base2 = mkdtempSync(join(tmpdir(), 'cmcp-laas-ok-')); +process.env.CMCP_STATE_DIR = base2; +const { medProgramLaas: laas2 } = await import('../mcp-server/programlaas.js?ren'); +let kørt2 = false; +const r2 = await laas2('com.apple.TextEdit', async () => { kørt2 = true; return 42; }); +check('4 en ren mappe laaser og koerer handlingen', r2.ok === true && r2.vaerdi === 42 && kørt2 === true, JSON.stringify(r2)); + +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/larmende-veje.mjs b/test/larmende-veje.mjs index 2fa924a..aaaa75e 100644 --- a/test/larmende-veje.mjs +++ b/test/larmende-veje.mjs @@ -324,7 +324,7 @@ for (const [t, hvorfor] of [ ['computer_activate', 'haever et program til forgrunden og tager dermed fokus'], ['computer_space', 'skifter menneskets Space'], ['computer_paste', 'udklipsholderen er global - en proeve ville overskrive hans'], - ['computer_ask_user', 'skal vise en aegte boks; maales af failclosed.mjs med CMCP_DIALOGS=1'], + ['computer_ask_user', 'forgrundens boks maales af failclosed.mjs med CMCP_DIALOGS=1; baggrundens ikon-vej af ikon-godkend.mjs del E (29/9)'], ['window resize', 'attrappen er borderless (har ingen AXSize); et vindue MED titellinje klemmer macOS ind paa hans skaerm'], ['window close/minim', 'samme: en lukkeknap findes kun paa en titellinje. Minimering laver desuden en genie-animation i HANS Dock'], ]) console.log(` ${t.padEnd(20)} ${hvorfor}`); diff --git a/test/menu-genvej.mjs b/test/menu-genvej.mjs new file mode 100644 index 0000000..e81d86e --- /dev/null +++ b/test/menu-genvej.mjs @@ -0,0 +1,78 @@ +// Et menupunkt der LUKKER noget, spoerger - uanset hvad det hedder paa skaermens sprog. +// +// ⛔ HVORFOR DEN FINDES (1/10-2026, konsulent-panelet, maalt paa Gustavs Mac) +// Prøvens oprydning valgte «Skak > Slut Skak». Porten kendte «afslut» og «quit», +// ikke «slut», saa ingen blev spurgt - og et ugemt parti blev lukket. Ord baerer +// ikke paa tvaers af sprog. Genvejen gor: Cmd+Q staar i programmets egen menu. +// +// Intet startes, intet tager skaermen: serveren koerer mod en attrap-hjaelper der +// kun skriver sit argv ned, og samtykket gaar til en attrap der ikke svarer. +import './ryd-op.mjs'; +import { spawn } from 'node:child_process'; +import { writeFileSync, chmodSync, readFileSync, existsSync, mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { lavFalskSpoerger } from './falsk-hjaelper.mjs'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; +const D = mkdtempSync(join(tmpdir(), 'cmcp-menugenvej-')); +const ARGV = join(D, 'argv.txt'), MENUS = join(D, 'menus.json'), STUB = join(D, 'stub.sh'); +writeFileSync(STUB, `#!/bin/sh +printf '%s ' "$@" >> ${ARGV}; echo >> ${ARGV} +case "$1" in + apps) echo '{"ok":true,"apps":[{"bundleId":"com.apple.Chess","name":"Skak","pid":4242,"active":false}]}' ;; + resolve-app) echo '{"ok":true,"bundleId":"com.apple.Chess","running":true}' ;; + menus) cat ${MENUS} ;; + menu-click) echo '{"ok":true,"result":"pressed"}' ;; + *) echo '{"ok":true}' ;; +esac +`); +chmodSync(STUB, 0o755); +const menu = (items) => writeFileSync(MENUS, JSON.stringify({ ok: true, items })); + +const spoerger = lavFalskSpoerger('udloeb', 'cmcp-menugenvej'); +const srv = spawn('node', [join(ROOT, 'mcp-server/index.js')], { + env: { ...process.env, CMCP_HELPER: STUB, CMCP_STATUS_IKON: '0', CMCP_NO_PARENT_WATCH: '1', + CMCP_STATE_DIR: join(D, 'state'), CMCP_BACKGROUND: '0', CMCP_MODE: 'allow', + CMCP_ASK_TIMEOUT: '2', CMCP_OSASCRIPT: spoerger.sti }, + stdio: ['pipe', 'pipe', 'pipe'] }); +let buf = '', n = 0; const w = new Map(); +srv.stdout.on('data', d => { buf += d; let i; while ((i = buf.indexOf('\n')) >= 0) { const l = buf.slice(0, i); buf = buf.slice(i + 1); try { const m = JSON.parse(l); w.get(m.id)?.(m); } catch {} } }); +const rpc = (m, p) => new Promise(r => { const id = ++n; w.set(id, r); srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method: m, params: p }) + '\n'); }); +const kald = async (navn, args) => (await rpc('tools/call', { name: navn, arguments: args })).result?.content?.[0]?.text || ''; +await rpc('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'menugenvej', version: '1' } }); +srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'); + +const klikket = (sti) => (existsSync(ARGV) ? readFileSync(ARGV, 'utf8') : '').split('\n') + .some(l => l.startsWith('menu-click ') && l.includes(`--path ${sti} `)); + +// 1. Det ord der slap igennem paa Gustavs Mac. +menu([{ path: 'Skak > Slut Skak', shortcut: 'cmd+q', enabled: true }]); +await kald('computer_menu', { app: 'com.apple.Chess', path: 'Skak > Slut Skak' }); +check('1 «Skak > Slut Skak» trykkes IKKE uden et ja', !klikket('Skak > Slut Skak')); + +// 2. Et harmloest navn med en lukke-genvej: genvejen afgoer det, ikke ordet. +menu([{ path: 'Skak > Faerdig', shortcut: 'cmd+q', enabled: true }]); +await kald('computer_menu', { app: 'com.apple.Chess', path: 'Skak > Faerdig' }); +check('2 et punkt med Cmd+Q spoerger, uanset navnet', !klikket('Skak > Faerdig')); + +// 3. Kan punktet ikke slaas op, spoerges der (lukket, ikke aabent). +menu([]); +await kald('computer_menu', { app: 'com.apple.Chess', path: 'Skak > Ukendt' }); +check('3 et punkt der ikke kan slaas op, trykkes IKKE uden et ja', !klikket('Skak > Ukendt')); + +// 4. Et harmloest punkt med en harmloes genvej gaar fri - ellers spoerger alt. +menu([{ path: 'Arkiv > Nyt spil', shortcut: 'cmd+n', enabled: true }]); +await kald('computer_menu', { app: 'com.apple.Chess', path: 'Arkiv > Nyt spil' }); +check('4 «Arkiv > Nyt spil» (Cmd+N) trykkes uden at spoerge', klikket('Arkiv > Nyt spil')); + +// 5. Kalibrering: 1-3 blev stoppet af et menneske-spoergsmaal, ikke af noget andet. +check('5 kalibrering: porten spurgte et menneske om 1, 2 og 3', spoerger.gangeSpurgt() === 3, + `spurgt ${spoerger.gangeSpurgt()} gange`); + +srv.kill(); +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/ombryd.mjs b/test/ombryd.mjs new file mode 100644 index 0000000..b1592ed --- /dev/null +++ b/test/ombryd.mjs @@ -0,0 +1,52 @@ +// OMBRYD: viser menuen HELE teksten, mennesket siger ja til? +// +// ⛔ HVORFOR DEN FINDES (29/9-2026, panelet Astra + Fable) +// Touch ID-arket klippede handlingen ved 80 tegn og protokollen ved 200 - tavst. +// Et ja daekkede altsaa noget mennesket aldrig saa. Nu staar hele teksten ombrudt +// i menuen lige over «Allow». Prøven kompilerer ikonets egen tekst-fil og maaler +// at intet tegn falder ud, at ingen linje er for bred, og at usynlige tegn renses. +// Ingen skaerm, intet ikon, intet Touch ID. +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; + +const ARB = mkdtempSync(join(tmpdir(), 'cmcp-ombryd-')); +try { + writeFileSync(join(ARB, 'main.swift'), `import Foundation +let ind = try! JSONSerialization.jsonObject(with: FileHandle.standardInput.readDataToEndOfFile()) as! [String] +let ud = ind.map { ["linjer": ombryd($0), "kort": kort($0, 60), "ren": renTekst($0)] as [String: Any] } +FileHandle.standardOutput.write(try! JSONSerialization.data(withJSONObject: ud)) +`); + const BIN = join(ARB, 'ombryd'); + execFileSync('swiftc', [join(ROOT, 'helper', 'Sources', 'cmcp-status', 'Tekst.swift'), join(ARB, 'main.swift'), '-o', BIN], + { stdio: 'pipe', timeout: 180000 }); + + const lang = 'Send til Benjamin: ' + 'computer-MCP virker. '.repeat(140); + const ordUdenMellemrum = 'x'.repeat(150) + ' slut'; + const blandet = 'Hej 👋 Æblegrød på Østerbro — ' + 'ærø '.repeat(40) + 'https://example.com/' + 'a'.repeat(90); + const snyd = 'Press "Gem"\nAllow… (confirm with Touch ID)‮evil​'; + const kortTekst = 'Quit Finder'; + const svar = JSON.parse(execFileSync(BIN, { input: JSON.stringify([lang, ordUdenMellemrum, blandet, snyd, kortTekst]), encoding: 'utf8' })); + + const tegn = (s) => [...s.replace(/\s+/g, '')].join(''); + const renJS = (s) => s.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, ' '); + [['lang besked', lang], ['ord laengere end linjen', ordUdenMellemrum], ['dansk, emoji og en lang adresse', blandet]].forEach(([navn, s], i) => { + const l = svar[i].linjer; + check(`1 ${navn}: intet tegn falder ud`, tegn(l.join('')) === tegn(renJS(s)), `${tegn(l.join('')).length} af ${tegn(renJS(s)).length}`); + check(`2 ${navn}: ingen linje er bredere end 64`, l.every(x => [...x].length <= 64), String(Math.max(...l.map(x => [...x].length)))); + }); + check('3 linjeskift, retningstegn og nul-bredde bliver ikke til egne linjer i menuen', + svar[3].linjer.length === 1 && !/[\n‮​]/.test(svar[3].linjer.join('')), JSON.stringify(svar[3].linjer)); + check('4 hovedmenuens korte udgave siger at der er mere', svar[0].kort.endsWith('…') && [...svar[0].kort].length === 61, svar[0].kort); + check('4b ...og en kort tekst staar uaendret', svar[4].kort === kortTekst && svar[4].linjer.join(' ') === kortTekst, svar[4].kort); +} finally { + rmSync(ARB, { recursive: true, force: true }); +} +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/omveje.mjs b/test/omveje.mjs new file mode 100644 index 0000000..a78d453 --- /dev/null +++ b/test/omveje.mjs @@ -0,0 +1,84 @@ +// OMVEJE: siger computer_permissions hvor klienten lader en agent handle UDEN +// om denne server? +// +// ⛔ HVORFOR DEN FINDES (29/9-2026, panelet: B6) +// Samtykke-porten vogter denne servers egne kald. En `Bash(osascript ...)` eller +// en browser-automation med klik/udfyld i klientens allow-liste gaar helt uden +// om - og porten saa ud som et faengsel, den ikke er. Nu naevnes de regler. +// Proeven bruger en MIDLERTIDIG indstillingsfil, aldrig menneskets rigtige. +import './ryd-op.mjs'; +import { spawn } from 'node:child_process'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { lavFalskHjaelper, lavFalskSpoerger } from './falsk-hjaelper.mjs'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; + +const ARB = mkdtempSync(join(tmpdir(), 'cmcp-omveje-')); +const MED = join(ARB, 'med.json'), UDEN = join(ARB, 'uden.json'); +writeFileSync(MED, JSON.stringify({ permissions: { allow: [ + 'Bash(osascript -e \' *)', 'mcp__browser-mcp__browser_click', 'mcp__browser-mcp__browser_fill', 'Bash(*)', + 'Bash(git status)', 'mcp__computer-mcp__computer_find', 'mcp__browser-mcp__browser_screenshot', 'Read(HEMMELIG-STI)', + // Runde 1 30/9 (Fable P5): skaller, fortolkere og en hel browser-server - stod i husets egne indstillinger. + 'Bash(bash *)', 'Bash(zsh *)', 'Bash(python3 *)', 'Bash(node -e \' *)', 'mcp__browser-mcp', 'Bash(npm test)', + // Runde 2 30/9 (Fable 1): root, fuld sti, programmer der koerer programmer, og de + // automations-servere klienterne selv skibes med. Og det der IKKE er en omvej. + 'Bash(sudo *)', 'Bash(/bin/zsh *)', 'Bash(npx playwright *)', 'mcp__claude-in-chrome', 'mcp__computer-use__computer', + 'Bash(npx tsc:*)', 'mcp__computer-mcp', + // Live-proeven 30/9: PRAECISE kommandoer tillader kun den ene ting - ikke en omvej. + 'Bash(node --check index.js)', 'Bash(python3 -c "import ast; ast.parse(open(\'x.py\').read())")', + 'Bash(osascript -e \'tell application "Simulator" to activate\')', 'Bash(open -a Simulator)', 'Bash(sudo pmset *)', + // ...men et script fra stdin er en omvej. + 'Bash(python3 -)'] } })); +writeFileSync(UDEN, JSON.stringify({ permissions: { allow: ['Bash(git status)', 'mcp__computer-mcp__computer_press'] } })); + +async function permissions(filer) { + const HJ = lavFalskHjaelper('cmcp-omveje'); + const srv = spawn('node', [join(ROOT, 'mcp-server', 'index.js')], { + env: { ...process.env, CMCP_HELPER: HJ.sti, CMCP_STATE_DIR: ARB, CMCP_STATUS_IKON: '0', CMCP_KLIENT_INDSTILLINGER: filer, + CMCP_OSASCRIPT: lavFalskSpoerger('udloeb', 'cmcp-omveje-sp').sti }, + stdio: ['pipe', 'pipe', 'pipe'] }); + let buf = ''; const v = new Map(); let n = 0; + srv.stdout.on('data', d => { buf += d; let i; + while ((i = buf.indexOf('\n')) >= 0) { const l = buf.slice(0, i); buf = buf.slice(i + 1); + try { const m = JSON.parse(l); v.get(m.id)?.(m); v.delete(m.id); } catch {} } }); + const rpc = (method, params = {}) => new Promise((res, rej) => { const id = ++n; v.set(id, res); + srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); + setTimeout(() => rej(new Error('timeout')), 30000); }); + try { + await rpc('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'omveje', version: '1' } }); + const r = await rpc('tools/call', { name: 'computer_permissions', arguments: {} }); + return r.result?.content?.[0]?.text || ''; + } finally { srv.kill(); } +} + +const med = await permissions(MED); +let j = {}; try { j = JSON.parse(med); } catch {} +const regler = (j.bypasses || []).map(b => b.rule); +check('1 osascript i allow naevnes som omvej', regler.some(r => /osascript/.test(r)), regler.join(' | ')); +check('2 en fri shell naevnes', regler.includes('Bash(*)'), regler.join(' | ')); +check('3 browser-klik og -udfyld naevnes', regler.includes('mcp__browser-mcp__browser_click') && regler.includes('mcp__browser-mcp__browser_fill'), regler.join(' | ')); +check('4 harmloese regler naevnes IKKE (git status, computer-mcp selv, et skaermbillede)', + !regler.some(r => /git status|computer-mcp|screenshot/.test(r)), regler.join(' | ')); +check('4b skaller, fortolkere og en hel browser-server naevnes (Fable P5)', + ['Bash(bash *)', 'Bash(zsh *)', 'Bash(python3 *)', "Bash(node -e ' *)", 'mcp__browser-mcp'].every(r => regler.includes(r)), regler.join(' | ')); +check('4c ...men ikke «npm test»', !regler.includes('Bash(npm test)'), regler.join(' | ')); +check('4d root, fuld sti, npx playwright, Claude in Chrome og computer-use naevnes (Fable R2 1)', + ['Bash(sudo *)', 'Bash(/bin/zsh *)', 'Bash(npx playwright *)', 'mcp__claude-in-chrome', 'mcp__computer-use__computer'].every(r => regler.includes(r)), regler.join(' | ')); +check('4f praecise kommandoer (uden wildcard) naevnes IKKE - kun de der tillader hvad som helst', + !regler.some(r => /node --check|ast\.parse|to activate|open -a Simulator|sudo pmset/.test(r)), regler.join(' | ')); +check('4g et script fra stdin (python3 -) er en omvej', regler.includes('Bash(python3 -)'), regler.join(' | ')); +check('4e ...men ikke «npx tsc» eller computer-mcp selv', !regler.includes('Bash(npx tsc:*)') && !regler.includes('mcp__computer-mcp'), regler.join(' | ')); +check('5 resten af filen laekkes ikke', !/HEMMELIG-STI/.test(med), 'svaret naevner kun de fundne regler'); +check('6 og mennesket faar at vide at kun det kan fjerne dem', /Only the person can remove them/.test(j.bypassNote || ''), j.bypassNote); +const uden = JSON.parse(await permissions(UDEN) || '{}'); +check('7 uden omveje: ingen advarsel', Array.isArray(uden.bypasses) && uden.bypasses.length === 0 && !uden.bypassNote, JSON.stringify(uden.bypasses)); +const ingen = JSON.parse(await permissions(join(ARB, 'findes-ikke.json')) || '{}'); +check('8 ingen indstillinger fundet: det siges, ikke tavshed', /other MCP clients are not checked/.test(String(ingen.bypassesChecked)), String(ingen.bypassesChecked)); + +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/parallel.mjs b/test/parallel.mjs new file mode 100644 index 0000000..8967e69 --- /dev/null +++ b/test/parallel.mjs @@ -0,0 +1,272 @@ +// PARALLEL: kan 5, 10 og 15 agenter arbejde SAMTIDIG i baggrunden, mens et +// menneske skriver i et andet program - uden at nogen af dem tager skaermen? +// +// ⛔ HVORFOR DEN FINDES (1/10-2026) +// Gustav: «ellers er der ikke nogen grund til at vi kan koere 15 samtidig». +// concurrent.mjs beviser kun at revisionsloggen ikke blandes. Her koerer hver +// agent en rigtig use case (sin egen server, sin egen hjaelper-vagt), alle paa +// én gang, og det maales udefra: +// 1. hvad der stod FORREST, hvert 100. ms, hele vejen - maa aldrig skifte +// 2. menneskets tekst i TextEdit, tegn for tegn - intet maa mangle eller komme til +// 3. hver agents eget tjek (det programmet SELV viser bagefter) +// 4. ingen agent naaede et program uden for sin liste (vagt-hjaelperen) +// +// ⛔ KUN PAA EN FREMMED MASKINE: proeven skriver som et menneske i TextEdit og +// starter og lukker programmer. Paa menneskets Mac er det ikke en proeve. +import './ryd-op.mjs'; +import { spawn, execFileSync } from 'node:child_process'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { writeFileSync, mkdtempSync, existsSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { SCENARIER, koerScenarie } from './brugsscenarier.mjs'; +import { startFilm } from './film.mjs'; +import { startForrestLog, doem } from './forrest-log.mjs'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; +const vent = (ms) => new Promise(r => setTimeout(r, ms)); + +// To tilstande: +// FREMMED (CMCP_FREMMED_MASKINE=1, GitHubs Mac): proeven spiller mennesket i TextEdit og filmer. +// EGEN MAC (CMCP_PARALLEL_EGEN_MAC=1, 1/10 Gustav: «testet og tjekket paa computeren»): +// mennesket ER der og arbejder som han plejer. Proeven skriver ALDRIG, filmer ikke, +// lukker ikke hans programmer, og koerer kun opgaver der ikke aendrer hans data +// (Lommeregner, Skak, Aktivitetsovervaagning, Finder laest). Maalet: kom NOGEN af +// agenternes programmer frem foran ham - hvert 100. ms, hele vejen. +const FREMMED = process.env.CMCP_FREMMED_MASKINE === '1'; +// PYNT (2/10, forside-chatten): rører KUN hvad kameraet viser - filnavn og tekstens +// ord. Rører ALDRIG tjekkene: samme forsoegt/verificeret-taelling, samme scenarie- +// valg, samme antal agenter. Kun til en ren optagelse til computermcp.dev. +const PYNT = process.env.CMCP_FILM_PYNT === '1'; +const EGEN = !FREMMED && process.env.CMCP_PARALLEL_EGEN_MAC === '1'; +if (!FREMMED && !EGEN) { + console.log('SPR. saet CMCP_FREMMED_MASKINE=1 (GitHubs Mac) eller CMCP_PARALLEL_EGEN_MAC=1 (din egen Mac, mens du arbejder)'); + process.exit(0); +} + +// Arbejderne: hver sit program, saa to agenter aldrig deler fokus i samme program. +// ⛔ 1/10: Skak er et DOKUMENT - et ugemt parti gav et gem-panel foran Gustav. Kun paa GitHubs Mac. +const ARBEJDERE = (EGEN ? [14, 13] : [14, 15, 9, 4, 17, 5, 13, 6]).map(nr => SCENARIER.find(s => s.nr === nr)).filter(Boolean); +// Laeserne: flere agenter der laeser Finder samtidig - laesning deler intet fokus. +const laeser = (i) => ({ + nr: 100 + i, navn: `laese Finder ${i}`, apps: ['com.apple.finder'], klasse: 'laes', + async trin() {}, + async tjek(c) { + const v = await c.vinduer('com.apple.finder'); + const m = await c.find('com.apple.finder', { role: 'AXButton', limit: 5 }); + return `${v.length} vindue(r), ${m.length} knap(per) laest`; + }, +}); +const NIVEAUER = (process.env.CMCP_PARALLEL_NIVEAUER || '5,10,15').split(',').map(Number); + +// Mennesket: et TextEdit-dokument forrest, og ord der skrives som et menneske gør. +// ⛔ Koersel 36832790982 OG 36837334933 (1/10): TextEdit svarede ikke paa AppleEvents +// fra proeven paa GitHubs Mac (-1712, 40 gange), og jobbet broendte 90 min. Nu +// INGEN AppleEvents: `open` (LaunchServices) aabner, og hjaelperen - som har +// tilgaengeligheds-tilladelsen paa koereren - skriver og laeser. Hård frist 20 s. +const HJ = join(ROOT, 'mcp-server', 'vendor', 'cmcp-helper'); +const hj = (...a) => JSON.parse(execFileSync(HJ, a, { encoding: 'utf8', timeout: 10000 })); +function startMenneske() { + const d = mkdtempSync(join(tmpdir(), 'cmcp-parallel-')); + const fil = join(d, PYNT ? 'notes.txt' : 'menneske.txt'); + writeFileSync(fil, ''); + // ⛔ 1/10 (koersel 36890103580): SIGKILL alene hjalp ikke - traef-tallet voksede + // staedigt 2 -> 3 -> 4 paa tvaers af runderne, ALDRIG nulstillet, praecis det + // moenster macOS' "Resume" giver: TextEdit gemmer sin vinduestilstand + // periodisk og GENSKABER den naeste gang «open -a TextEdit» koeres - ogsaa + // efter en -9. Samme opskrift som allerede bruges til Skak her i filen + // (nulstil()): slaa Resume fra for netop TextEdit, og slet den gemte + // tilstand foer hver aabning. + try { execFileSync('defaults', ['write', 'com.apple.TextEdit', 'NSQuitAlwaysKeepsWindows', '-bool', 'false']); } catch {} + try { execFileSync('defaults', ['write', 'com.apple.TextEdit', 'ApplePersistenceIgnoreState', '-bool', 'true']); } catch {} + try { execFileSync('rm', ['-rf', `${process.env.HOME}/Library/Saved Application State/com.apple.TextEdit.savedState`]); } catch {} + execFileSync('open', ['-a', 'TextEdit', fil]); + const frist = Date.now() + 20000; + for (;;) { + try { if (hj('windows', '--app', 'com.apple.TextEdit').count > 0 && hj('focused').element?.bundleId === 'com.apple.TextEdit') break; } catch {} + if (Date.now() > frist) throw new Error('UMÅLT: TextEdit kom ikke frem med et vindue inden 20 s'); + execFileSync('sleep', ['0.5']); + } + // ⛔ 1/10 (koersel 36876560087): «tePid» blev FOER gaettet ud fra skift-loggen + // FOER rundens start - men staar mennesket helt stille i de foerste 1,5 s + // (det almindelige), er der INTET skift at gaette ud fra, og [...new + // Set([])].pop() giver undefined. Det inverterede hele doemmelsen: hver + // eneste TextEdit-visning blev saa selv kaldt "et udsving der aldrig blev + // givet tilbage". Nu laeses TextEdits PID direkte, her, mens vi VED den har + // fokus - ikke gaettet bagefter. + const pid = hj('apps').apps?.find(a => a.bundleId === 'com.apple.TextEdit')?.pid ?? null; + if (!pid) throw new Error('UMÅLT: TextEdit stod med et vindue og fokus, men kunne ikke findes i apps-listen'); + // Sin egen proces: tastaturet maa ikke bremse proevens egen haendelsesloekke. + // ⛔ 1/10 (koersel 36869119105): uden --app gik tasterne til det FORRESTE program - + // og naar Finders menu-klik (scenarie 15) kort gjorde Finder forrest, forsvandt + // tasterne derind, mens helperen stadig svarede ok (sendt-tallet talte dem med). + // TextEdits dokument endte TOMT, selvom "skrevet" viste 250+ tegn. Nu skriver + // mennesket PRAECIS som produktet selv skriver i baggrunden: med --app, via + // tilgaengeligheds-laget, saa teksten rammer TextEdit uanset hvad der er forrest. + // ⛔ Men ogsaa EFTER den rettelse var staar.laengde=0 i to koersler i traek - + // derfor logges hvert kalds RAA svar (sidste vinder), saa en fortsat 0 kan + // laeses i stedet for gaettes paa en tredje gang. + // ⛔ 1/10 (koersler 36869119105, 36876560087, 36884149245, 36890103580, 36896072791): + // fem koersler i traek paa at faa et paalideligt AFSLUTTENDE laes af TextEdits + // dokument. Resume-rettelsen gjorde traef-tallet entydigt (1), men vaerdien laeses + // stadig tom, selv om HVERT ENKELT skrive-kald selv bekraefter (sit eget readback- + // tjek i AX.indsaetIFokus) at akkurat DEN tekst landede. I stedet for at gaette en + // sjette gang paa TextEdits AX-kvirk: beviset flyttes til data vi ALLEREDE har og + // stoler paa - hvert kalds EGEN «verified»-bekraeftelse - i stedet for et skroebeligt + // `find`-opslag efter det hele er overstaaet. + const log = join(d, 'skrevet.txt'), tael = join(d, 'taelling.txt'), stop = join(d, 'stop'), sidsteSvar = join(d, 'sidste-svar.json'); + // PYNT (2/10): kun ORDET der tastes skifter - samme loekke, samme --app-vej, + // samme verified-taelling. "ord[$((i % N))]" cykler en kort huskeliste i + // stedet for "m$i " - checken laeser aldrig selve teksten, kun tael/sidsteSvar. + const skrivLinje = PYNT + ? `ord=(Buy milk Call Alex Book flights Walk the dog Read a book Water the plants Send the invoice Pack lunch Charge the laptop Reply to Sam) + w="\${ord[$((i % \${#ord[@]}))]} " + R=$("${HJ}" type --app com.apple.TextEdit --text "$w" 2>&1)` + : `w="m$i " + R=$("${HJ}" type --app com.apple.TextEdit --text "$w" 2>&1)`; + const p = spawn('bash', ['-c', `i=0; v=0; while [ ! -f "${stop}" ]; do + ${skrivLinje} + printf '%s' "$R" > "${sidsteSvar}" + if printf '%s' "$R" | grep -q '"verified":true'; then v=$((v+1)); printf '%s' "$w" >> "${log}"; fi + i=$((i+1)); sleep 0.12 + done + printf '%s %s' "$i" "$v" > "${tael}"`], { stdio: 'ignore' }); + const slut = new Promise(r => p.on('close', r)); + return { + pid, + async slut() { writeFileSync(stop, ''); await slut; await vent(800); return existsSync(log) ? readFileSync(log, 'utf8') : ''; }, + sidsteSvar: () => { try { return readFileSync(sidsteSvar, 'utf8').trim().slice(0, 300); } catch { return '(intet svar logget)'; } }, + // Hvor mange af de FORSOEGTE skrivninger blev VERIFICERET af hjaelperen selv - + // den stoerre proeve er om de alle naaede frem, ikke om et skroebeligt slut-opslag kan laese dem igen. + taelling() { + try { const [forsoegt, verificeret] = readFileSync(tael, 'utf8').trim().split(' ').map(Number); return { forsoegt, verificeret }; } + catch { return { forsoegt: 0, verificeret: 0 }; } + }, + // ⛔ 1/10 (koersel 36884149245): et almindeligt «killall» venter paa TextEdits + // egen afslutningslogik - og et UGEMT dokument beder om «Gem aendringer?», + // som intet svarer paa. Vinduet blev ikke lukket, og NAESTE rundes nye + // TextEdit-vindue laa saa OVENI det gamle (2, saa 3, saa 4 AXTextArea-traef + // paa tvaers af runderne) - find-opslaget tog det FOERSTE traef, som kunne + // vaere det forkerte (gamle, tomme) vindue. -9 spoerger ikke; her er intet + // at gemme, det er proevens eget kasserede dokument. + luk() { + try { execFileSync('killall', ['-9', 'TextEdit'], { stdio: 'ignore' }); } catch {} + for (let i = 0; i < 20; i++) { + try { if (!(hj('apps').apps || []).some(a => a.bundleId === 'com.apple.TextEdit')) return; } catch {} + try { execFileSync('sleep', ['0.1']); } catch {} + } + }, + }; +} + +// Hvilke pids er agenternes programmer? Laeses loebende, for et program kan vaere lukket igen +// naar dommen faeldes. +function startPidKort(bundles) { + const kort = new Map(); + const tag = () => { try { for (const a of hj('apps').apps || []) if (bundles.has(a.bundleId)) kort.set(a.pid, a.bundleId); } catch {} }; + tag(); + const ur = setInterval(tag, 1000); + return { kort, stop() { clearInterval(ur); tag(); } }; +} + +function nulstil() { + try { execFileSync('killall', ['Chess', 'Calculator', 'Contacts', 'System Settings', 'Activity Monitor', 'Notes', 'Safari'], { stdio: 'ignore' }); } catch {} + try { execFileSync('rm', ['-rf', `${process.env.HOME}/Library/Containers/com.apple.Chess/Data/Library/Saved Application State`]); } catch {} +} + +const rapport = []; +for (const n of NIVEAUER) { + if (FREMMED) { nulstil(); await vent(2500); } // ⛔ aldrig paa menneskets Mac: killall lukker HANS programmer + const hold = [...ARBEJDERE.slice(0, n), ...Array.from({ length: Math.max(0, n - ARBEJDERE.length) }, (_, i) => laeser(i + 1))]; + console.log(`\n== ${n} agenter samtidig: ${hold.map(s => s.nr).join(', ')}`); + let menneske = null; + if (FREMMED) { + try { menneske = startMenneske(); } catch (e) { check(`${n}.0 mennesket kom i gang`, false, e.message); continue; } + await vent(1500); + } + const film = FREMMED ? startFilm(`parallel-${n}-agenter`) : { stop: async () => null }; + const agentBundles = new Set(hold.flatMap(s => s.apps).filter(a => a !== 'com.apple.finder')); + const pids = startPidKort(agentBundles); + const vagt = startForrestLog(); + await vent(1500); + const t0 = Date.now(); + // Én delt state-mappe pr. runde: programlaas og revisionskaede deles, som paa en rigtig Mac. + const delt = mkdtempSync(join(tmpdir(), 'cmcp-parallel-state-')); + const res = await Promise.all(hold.map(s => koerScenarie(s, { film: false, menneskeArbejder: EGEN, stateDir: delt }))); + const sek = ((Date.now() - t0) / 1000).toFixed(1); + await vent(2500); // et program kan hente sig selv frem sekunder efter (Kontakter, 27/9) + const maaling = await vagt.stop(); + pids.stop(); + const skrevet = menneske ? await menneske.slut() : ''; + const taelling = menneske ? menneske.taelling() : { forsoegt: 0, verificeret: 0 }; + const filmSti = await film.stop(); + menneske?.luk(); + const iRunden = maaling.skift.filter(x => x.t >= t0 - 1500); + + // ⛔ 1/10 (koersel 36869119105): «tog > 0» fejlede ogsaa en scenarie der selv + // tog skaermen OG gav den aerligt tilbage (menu-klik, README linje 204: "it is + // a moment, not nothing") - det er allerede koerScenariens egen 'delvist', ikke + // en fejl. Et IKKE-tilbagegivet tag staar som 'fejlede' i koerScenarie selv + // (branchen "tog skærmen og gav den ikke tilbage"), saa status alene er nok; + // 5.1b dømmer den AERLIGE tilbagegivelsestid for mennesket uafhaengigt. + const roede = res.filter(r => r.status === 'fejlede'); + const tid = (t) => `${((t - t0) / 1000).toFixed(1)} s`; + // ⛔ 1/10: maalt i macOS' egen log, med aarsag - den gamle vagt var blind (1 linje mod 26 skift). + check(`${n}.0 maaleren saa skaermen (${maaling.puls} puls, ${maaling.skift.length} skift)`, !maaling.umaalt, maaling.umaalt || ''); + const fund = doem(iRunden, (pid) => pids.kort.has(pid)); + check(`${n}.1 intet agent-program kom frem, og intet skift blev revet tilbage fra mennesket`, fund.length === 0, + fund.map(f => `${f.slags} (${pids.kort.get(f.pid) || 'pid ' + f.pid}, ${f.aarsag}) efter ${tid(f.t)}`).join(', ')); + let fremmede = fund.length; + if (FREMMED) { + // ⛔ 1/10 (koersel 36876560087): FOER blev tePid gaettet ud fra skift-loggen - + // staar mennesket stille foer runden (det almindelige), er der intet at + // gaette ud fra, og hele doemmelsen inverteredes. Nu den PID vi faktisk + // laeste direkte fra TextEdit, da menneske-simulationen startede. + const tePid = menneske.pid; + const efter = iRunden.filter(x => x.t >= t0); + // ⛔ 1/10 (koersel 36869119105): «TextEdit forrest hele vejen» var for strengt - + // scenarie 15 klikker Finders menu (cmd+n, shift+cmd+g), og et menu-klik + // SKAL kort goere programmet forrest for at kunne trykke dets menulinje. + // README (linje 204) lover selv ærligt «took_screen»+«gave_back»: "it is a + // moment, not nothing" - IKKE nul beroering nogensinde. Det der skal maales + // er om skaermen blev givet AERLIGT tilbage, ikke om den aldrig blev taget. + // Give-tilbage-vinduerne er 240 ms (tryk), 2000 ms (menu), 4000 ms (start); + // 3,5 s tolerance daekker alle tre plus AX-rundtursforsinkelse under last. + const TOLERANCE_MS = 3500; + const udsving = []; + let start = null; + for (const x of efter) { + if (x.pid !== tePid) { if (!start) start = x; } + else if (start) { udsving.push({ start, slut: x, varighedMs: x.t - start.t }); start = null; } + } + if (start) udsving.push({ start, slut: null, varighedMs: null }); + const forLangsomme = udsving.filter(u => u.varighedMs === null || u.varighedMs > TOLERANCE_MS); + fremmede += forLangsomme.length; + check(`${n}.1b paa GitHubs Mac: TextEdit faar skaermen aerligt tilbage (${udsving.length} udsving, ${TOLERANCE_MS}ms tolerance)`, + forLangsomme.length === 0, + forLangsomme.map(u => `pid ${u.start.pid} (${u.start.aarsag}) efter ${tid(u.start.t)}` + + (u.varighedMs == null ? ' - aldrig givet tilbage' : `, varede ${u.varighedMs} ms`)).join(', ')); + // ⛔ 1/10: SEKS koersler forsoegte et paalideligt AFSLUTTENDE laes af TextEdits + // dokument via find-opslag - traef-tallet blev entydigt (Resume-rettelsen), men + // vaerdien laeste stadig tom, en AX-kvirk uden for computer-mcp's egen kode. I + // stedet bruges hjaelperens EGEN readback-bekraeftelse pr. kald: hvert «verified: + // true» beviser at AKKURAT den tekst landede, maalt i selve oejeblikket - en + // staerkere, mere granulaer proeve end et skroebeligt slut-opslag. + const tabtRate = taelling.forsoegt > 0 ? (taelling.forsoegt - taelling.verificeret) / taelling.forsoegt : 1; + check(`${n}.2 mennesket kunne skrive uafbrudt (${taelling.verificeret}/${taelling.forsoegt} skrivninger verificeret af hjaelperen)`, + taelling.forsoegt > 0 && tabtRate < 0.05, + `${taelling.verificeret} af ${taelling.forsoegt} verificeret (${(tabtRate * 100).toFixed(0)}% tabt) - sidste svar: ${menneske.sidsteSvar()}`); + } + check(`${n}.3 ingen agent fejlede eller tog skaermen (${res.filter(r => r.status === 'bevist').length} bevist, ${res.filter(r => r.status === 'delvist').length} delvist)`, + roede.length === 0, roede.map(r => `${r.nr} ${r.navn}: ${String(r.bevis).slice(0, 120)}`).join(' | ')); + check(`${n}.4 ingen agent naaede et program uden for sin liste`, res.every(r => !(r.stoppet || []).length && !(r.udenfor || []).length)); + for (const r of res) console.log(` ${r.status.padEnd(8)} ${String(r.nr).padStart(3)} ${r.navn}: ${String(r.bevis).slice(0, 110)}`); + // Sporet beholdes: uden det kan ingen se HVORFOR en agent fejlede (Astra/Opus, 1/10). + rapport.push({ n, sek, film: filmSti, fund: fremmede, maaling: { puls: maaling.puls, umaalt: maaling.umaalt, skift: iRunden }, tegn: skrevet.length, taelling, res }); + console.log(` ${n} agenter paa ${sek} s · film: ${filmSti || 'ingen'}`); +} +if (process.env.CMCP_PARALLEL_RAPPORT) writeFileSync(process.env.CMCP_PARALLEL_RAPPORT, JSON.stringify(rapport, null, 2)); +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/paste-stop.mjs b/test/paste-stop.mjs new file mode 100644 index 0000000..3a85bdb --- /dev/null +++ b/test/paste-stop.mjs @@ -0,0 +1,58 @@ +// PASTE-STOP: stopper den RIGTIGE hjaelper et paste, naar skaerm-laanet slutter? +// +// ⛔ HVORFOR DEN FINDES (runde 5, 30/9-2026, Astra 3) +// Serveren sender SIGUSR1 til et paste der koerer, naar laanet slutter. Den falske +// hjaelper (skaerm-laan 6f) beviser at signalet SENDES - ikke at Swift-siden +// stopper. Her faar den rigtige hjaelper signalet, mens den venter paa teksten paa +// stdin, og SKAL svare «stopped before anything changed» uden at have roert +// udklipsholderen eller trykket Cmd+V. +// +// ⛔ KUN PAA EN FREMMED MASKINE: svigter stoppet, indsaettes teksten i det program +// der er forrest. Paa menneskets Mac er det ikke en proeve, det er en skade. +import { spawn } from 'node:child_process'; +import { existsSync, statSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; +if (process.env.CMCP_FREMMED_MASKINE !== '1') { + console.log('SPR. kun paa en fremmed maskine (CMCP_FREMMED_MASKINE=1) - et svigtende stop ville indsaette tekst'); + process.exit(0); +} +const HJ = [process.env.CMCP_HELPER, join(ROOT, 'mcp-server', 'vendor', 'cmcp-helper'), join(ROOT, 'helper', '.build', 'release', 'cmcp-helper')] + .filter(p => p && existsSync(p)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0]; + +const koer = (forsinkelseMs, tekstEfterMs = 300) => new Promise((res) => { + const p = spawn(HJ, ['paste'], { stdio: ['pipe', 'pipe', 'pipe'] }); + let ud = ''; + p.stdout.on('data', (d) => { ud += d; }); + p.on('close', (rc, sig) => res({ rc, sig, ud: ud.trim() })); + setTimeout(() => { + try { p.kill('SIGUSR1'); } catch {} + const lever = () => { try { p.stdin.end('proeve-tekst der aldrig maa indsaettes'); } catch {} }; + if (tekstEfterMs === 0) lever(); else setTimeout(lever, tekstEfterMs); + }, forsinkelseMs); +}); + +// 1. Signalet kommer mens hjaelperen venter paa teksten: den stopper foer noget er roert. +const r1 = await koer(800); +let j1 = {}; try { j1 = JSON.parse(r1.ud.split('\n').pop()); } catch {} +check('1 SIGUSR1 foer teksten: stoppet, intet aendret', j1.ok === false && j1.code === 'screen-taken-back' && /before anything changed/.test(j1.error || ''), r1.ud.slice(0, 160)); + +// 1b (efterkontrol, Astra): signalet og teksten i SAMME oejeblik - intet vindue for en +// forsinket callback at overhale. Stoppet SKAL ses foer Cmd+V. +const r1b = await koer(800, 0); +let j1b = {}; try { j1b = JSON.parse(r1b.ud.split('\n').pop()); } catch {} +check('1b SIGUSR1 og teksten samtidig: stoppet foer Cmd+V', j1b.ok === false && j1b.code === 'screen-taken-back', r1b.ud.slice(0, 160)); + +// 2. Signalet kommer MED DET SAMME (maaske foer signalkilden findes): enten stoppet, +// eller processen doer af signalet - aldrig et paste. +const r2 = await koer(0); +let j2 = {}; try { j2 = JSON.parse(r2.ud.split('\n').pop()); } catch {} +check('2 SIGUSR1 med det samme: stoppet eller doed af signalet - aldrig indsat', r2.sig === 'SIGUSR1' || (j2.ok === false && j2.code === 'screen-taken-back'), + `sig=${r2.sig} ${r2.ud.slice(0, 120)}`); + +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/run-all.sh b/test/run-all.sh index df3e1ed..28f1a20 100755 --- a/test/run-all.sh +++ b/test/run-all.sh @@ -92,6 +92,11 @@ run "paastande" "node test/claims.mjs" # og den var dermed ubevist i naesten hver koersel. Nu gaar spoergsmaalet # gennem en attrap, saa den koerer HVER gang og viser ingenting. run "fejl-lukket" "node test/failclosed.mjs" +run "laas fejler lukket" "node test/laas-fejler-lukket.mjs" +run "doeren" "node test/doer.mjs" +run "menu-genvej" "node test/menu-genvej.mjs" +run "knap-ord" "node test/knap-ord.mjs" +run "forrest-log" "node test/forrest-log-kalibrering.mjs" run "fejlbeskeder" "node test/errors.mjs" run "flere agenter" "node test/concurrent.mjs" @@ -133,6 +138,18 @@ run "argumenter" "node test/argumenter.mjs" run "pakken starter" "node test/pakken-starter.mjs" # ⛔ B5: samtykket viste ikke HVAD der godkendes - «read the path above» uden en sti. run "samtykke-tekst" "node test/samtykke-tekst.mjs" +# ⛔ 29/9: Touch ID-arket klippede handlingen ved 80 tegn, tavst - et ja til noget ulaest. +run "hele teksten i menuen" "node test/ombryd.mjs" +# ⛔ 29/9: goer-selv, skaerm-laanet og den fulde tekst var kun kompileret i ikonet, aldrig koert. +run "ikonets menu" "node test/ikon-menu.mjs" +# ⛔ 29/9: Send i WhatsApp gik igennem i allow uden at nogen blev spurgt. +run "sende-porten" "node test/sende-port.mjs" +# ⛔ 29/9: forgrund kun via env-var + genstart, pr. server - nu laaner mennesket skaermen ud i ikonet. +run "skaerm-laanet" "node test/skaerm-laan.mjs" +# ⛔ 29/9: porten lignede et faengsel; osascript/browser i allow gik udenom, usagt. +run "omvejene" "node test/omveje.mjs" +# ⛔ 30/9: stopper den RIGTIGE hjaelper et paste ved laanets slut? Kun paa en fremmed maskine. +run "paste stopper" "node test/paste-stop.mjs" run "launch lukket" "node test/launch-lukket.mjs" run "log der ikke kan skrives" "node test/log-fejl.mjs" run "klik rammer ejeren" "node test/klik-ejer.mjs" diff --git a/test/ryd-op.mjs b/test/ryd-op.mjs new file mode 100644 index 0000000..aee78e1 --- /dev/null +++ b/test/ryd-op.mjs @@ -0,0 +1,23 @@ +// Proevernes midlertidige mapper slettes, naar proeven er BESTAAET. +// +// ⛔ MAALT 30/9-2026: 5.442 cmcp-*-mapper (3,4 GB) i $TMPDIR paa Gustavs Mac - +// hver koersel af suiten eller mutationsbeviset efterlod ~400, og disken loeb +// fuld midt i en mutant. En dumpet proeve beholder sine mapper: de er beviset. +import fs from 'node:fs'; +import { syncBuiltinESMExports } from 'node:module'; +import { tmpdir } from 'node:os'; +const { realpathSync } = fs; + +const mapper = []; +const original = fs.mkdtempSync; +fs.mkdtempSync = (prefix, ...rest) => { + const d = original(prefix, ...rest); + // Kun mapper i systemets midlertidige mappe - aldrig noget en proeve fik udefra. + if (realpathSync(d).startsWith(realpathSync(tmpdir()) + '/')) mapper.push(d); + return d; +}; +syncBuiltinESMExports(); +process.on('exit', (kode) => { + if (kode !== 0 || process.exitCode) return; + for (const d of mapper) { try { fs.rmSync(d, { recursive: true, force: true }); } catch {} } +}); diff --git a/test/sende-port.mjs b/test/sende-port.mjs new file mode 100644 index 0000000..032cc81 --- /dev/null +++ b/test/sende-port.mjs @@ -0,0 +1,343 @@ +// SENDE-PORTEN: kan en besked til et rigtigt menneske gaa ud, uden at mennesket +// saa HVEM og HVAD - og sagde ja til netop den? +// +// ⛔ HVORFOR DEN FINDES (29/9-2026, dommen 28/9 D4 + trin 7) +// En besked kan ikke kaldes tilbage. Foer i dag fandtes intet beskedapp-begreb +// i porten: `computer_press` paa Send i WhatsApp gik igennem i allow uden at +// nogen blev spurgt. Dommens seks modforsoeg skal alle give roedt: +// (a) send-knappen · (b) Return/Enter · (c) linjeskift i computer_type · +// (d) en falsk modtager fra modellen · (e) genbrugt nonce · (f) ja efter fristen. +// (e) og (f) er portens egne regler og maales i ikon-godkend.mjs (A7, A13, E3). +// Her: (a)-(d), genkontrollen under laasen, og en modtager der ikke kan laeses. +// +// Rigtig server, FALSK ikon og FALSK hjaelper med scriptede skaerm-svar: +// ingen skaerm, ingen rigtig beskedapp, intet sendt. +import './ryd-op.mjs'; +import { createServer } from 'node:net'; +import { spawn } from 'node:child_process'; +import { mkdtempSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { lavFalskHjaelper, lavFalskSpoerger } from './falsk-hjaelper.mjs'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; + +const STATE = mkdtempSync(join(tmpdir(), 'cmcp-sende-')); +const HJ = lavFalskHjaelper('cmcp-sende'); +const SP = lavFalskSpoerger('ja', 'cmcp-sende-sp'); +const WA = 'net.whatsapp.WhatsApp'; +// Feltet og Send-knappen paa samme raekke i samme vindue - saadan ligger de i en chat-app. +const FELT_RAMME = { x: 160, y: 600, w: 400, h: 40 }; +const KNAP = { window: 'WhatsApp', frame: { x: 570, y: 605, w: 30, h: 30 } }; +const SAMTALE = { window: 'WhatsApp', headings: ['Benjamin Riber', 'online'], column: true, field: { role: 'AXTextArea', value: 'computer-MCP virker', frame: FELT_RAMME } }; +const APPS = { apps: [{ name: 'WhatsApp', bundleId: WA, active: false }, { name: 'Finder', bundleId: 'com.apple.finder', active: true }, + { name: 'Google Chrome', bundleId: 'com.google.Chrome', active: false }, { name: 'Mail', bundleId: 'com.apple.mail', active: false }] }; +const svar = (ekstra = {}) => HJ.saetSvar({ apps: APPS, samtale: SAMTALE, 'press --dry': { would_press: { name: 'Send', role: 'AXButton', ...KNAP } }, ...ekstra }); +svar(); + +// Det falske ikon: svarer efter `ikonSvar`, og kan aendre skaermen FOER det svarer. +let ikonSvar = 'ja', foerSvar = null; +const spurgt = []; +await new Promise(res => createServer(sock => { + let buf = ''; + sock.on('data', d => { + buf += d; const i = buf.indexOf('\n'); if (i < 0) return; + const q = JSON.parse(buf.slice(0, i)); spurgt.push(q); + if (foerSvar) { foerSvar(); foerSvar = null; } + const m = ikonSvar === 'ja' ? { ok: true, verified: 'owner' } : { ok: true, verified: 'none' }; + sock.write(JSON.stringify({ nonce: q.nonce, ...m }) + '\n'); + }); + sock.on('error', () => {}); +}).listen(join(STATE, 'ikon.sock'), res)); + +const srv = spawn('node', [join(ROOT, 'mcp-server', 'index.js')], { + env: { ...process.env, CMCP_STATE_DIR: STATE, CMCP_MODE: 'allow', CMCP_HELPER: HJ.sti, CMCP_ASK_TIMEOUT: '3', + CMCP_STATUS_IKON: '0', CMCP_OSASCRIPT: SP.sti, CMCP_BACKGROUND: '1' }, + stdio: ['pipe', 'pipe', 'pipe'] }); +let buf = ''; const v = new Map(); let n = 0; +srv.stdout.on('data', d => { buf += d; let i; + while ((i = buf.indexOf('\n')) >= 0) { const l = buf.slice(0, i); buf = buf.slice(i + 1); + try { const m = JSON.parse(l); v.get(m.id)?.(m); v.delete(m.id); } catch {} } }); +const rpc = (method, params = {}) => new Promise((res, rej) => { const id = ++n; v.set(id, res); + srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); + setTimeout(() => rej(new Error('timeout ' + method)), 30000); }); +const kald = async (name, args) => { const r = await rpc('tools/call', { name, arguments: args }); + return { fejl: !!r.result?.isError, tekst: r.result?.content?.[0]?.text || JSON.stringify(r.error || {}) }; }; +const handlinger = () => HJ.handlingerNaaedeFrem().map(k => k.argv[0]); + +try { + await rpc('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'chat-sende', version: '1' } }); + srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'); + + // 0. At skrive et udkast er IKKE at sende: ingen spoergsmaal. + let foer = spurgt.length; + const s0 = await kald('computer_type', { app: 'WhatsApp', text: 'computer-MCP virker' }); + check('0 et udkast uden linjeskift skrives uden at spoerge', !s0.fejl && spurgt.length === foer && handlinger().includes('type'), s0.tekst.slice(0, 90)); + + // (a) Send-knappen: spoerger, med modtager og tekst fra SKAERMEN. + foer = spurgt.length; ikonSvar = 'ja'; + const sa = await kald('computer_press', { app: 'WhatsApp', role: 'AXButton', title: 'Send' }); + const qa = spurgt[foer]; + check('a send-knappen spoerger mennesket', !!qa, `${spurgt.length - foer} spoergsmaal`); + check('a ...og viser hvem det gaar til, laest fra skaermen over feltet', /To \(read from the screen, above the text field\): Benjamin Riber/.test(qa?.text || ''), qa?.text); + check('a ...og hele beskeden, laest tilbage fra feltet', /computer-MCP virker/.test(qa?.text || ''), qa?.text); + check('a ...kun for denne ene besked', /this one action only/.test(qa?.scope || '') && /cannot be taken back/.test(qa?.scope || ''), qa?.scope); + check('a ...og efter ja blev der trykket', !sa.fejl && handlinger().filter(x => x === 'press').length === 1, sa.tekst.slice(0, 90)); + + // En knap der IKKE sender, spoerger ikke. + foer = spurgt.length; svar({ 'press --dry': { would_press: { name: 'Attach', role: 'AXButton' } } }); + await kald('computer_press', { app: 'WhatsApp', role: 'AXButton', title: 'Attach' }); + check('a2 en knap der ikke sender, spoerger ikke', spurgt.length === foer, `${spurgt.length - foer}`); + svar(); + + // (b) Return og Enter - med ethvert ekstra modifikator - spoerger. Et nej = intet sendt. + ikonSvar = 'nej'; + for (const combo of ['return', 'enter', 'cmd+return', 'cmd+cmd+return', 'shift+enter', 'cmd+shift+d']) { + foer = spurgt.length; const k0 = handlinger().filter(x => x === 'key').length; + const r = await kald('computer_key', { app: 'WhatsApp', combo }); + check(`b «${combo}» spoerger, og uden ja sendes intet`, spurgt.length === foer + 1 && r.fejl + && handlinger().filter(x => x === 'key').length === k0, r.tekst.slice(0, 80)); + } + + // (c) Et linjeskift i teksten er en usynlig afsendelse: afvist, intet skrevet. + for (const [navn, t] of [['\\n', 'hej\nmed dig'], ['\\r', 'hej\r'], ['U+2028', 'hej
']]) { + foer = spurgt.length; const t0 = handlinger().filter(x => x === 'type').length; + const r = await kald('computer_type', { app: 'WhatsApp', text: t }); + check(`c linjeskift (${navn}) i en beskedapp: afvist, intet skrevet`, r.fejl && /line break sends/.test(r.tekst) + && handlinger().filter(x => x === 'type').length === t0 && spurgt.length === foer, r.tekst.slice(0, 80)); + } + + // (d) Modellen kan ikke skrive modtageren: dens egne ord naar ikke spoergsmaalet. + foer = spurgt.length; ikonSvar = 'nej'; + await kald('computer_press', { app: 'WhatsApp', role: 'AXButton', title: 'Send', contains: 'to Mom' }); + const qd = spurgt[foer]; + check('d en falsk modtager fra modellen naar ikke spoergsmaalet', !!qd && !/Mom/.test(qd.text) && /Benjamin Riber/.test(qd.text), qd?.text); + + // Et klik paa en send-knap spoerger ogsaa - og et klik der ikke kan bestemmes, spoerger. + foer = spurgt.length; + svar({ at: { found: true, bundleId: WA, role: 'AXButton', title: '', description: 'Send', ...KNAP } }); + await kald('computer_click', { app: 'WhatsApp', x: 500, y: 500 }); + check('d2 et klik paa send-knappen spoerger', spurgt.length === foer + 1, `${spurgt.length - foer}`); + foer = spurgt.length; + svar({ at: { found: true, bundleId: 'com.apple.finder', role: 'AXButton', title: 'Other' } }); + const kd3 = handlinger().filter(x => x === 'click').length; + const d3 = await kald('computer_click', { app: 'WhatsApp', x: 500, y: 500 }); + check('d3 et klik hvor noget andet ligger foran: fejler lukket (afvist eller spurgt), intet klikket uden ja', + (d3.fejl || spurgt.length === foer + 1) && handlinger().filter(x => x === 'click').length === kd3, d3.tekst.slice(0, 90)); + svar(); + + // Genkontrol under laasen: mennesket siger ja - men feltet skifter foer tasten trykkes. + foer = spurgt.length; ikonSvar = 'ja'; + const k0 = handlinger().filter(x => x === 'key').length; + foerSvar = () => svar({ samtale: { ...SAMTALE, field: { role: 'AXTextArea', value: 'en HELT anden besked' } } }); + const g = await kald('computer_key', { app: 'WhatsApp', combo: 'return' }); + check('g det der sendes, aendrede sig efter ja: intet sendt', g.fejl && /changed after the person approved/.test(g.tekst) + && handlinger().filter(x => x === 'key').length === k0, g.tekst.slice(0, 100)); + svar(); + + // u (runde 1, Astra 3): kan modtager eller tekst IKKE laeses, kan et ja ikke bindes til + // det der sendes - saa sendes intet, og mennesket spoerges ikke om noget ulaeseligt. + for (const [navn, sam] of [['modtager', { window: '', headings: [], field: { role: 'AXTextArea', value: 'hej' } }], + ['tekst', { ...SAMTALE, field: { role: 'AXTextArea' } }]]) { + foer = spurgt.length; svar({ samtale: sam }); + const k0 = handlinger().filter(x => x === 'key').length; + const r = await kald('computer_key', { app: 'WhatsApp', combo: 'return' }); + check(`u ulaeselig ${navn}: afvist, ikke spurgt, intet sendt`, r.fejl && /could not be read/.test(r.tekst) && spurgt.length === foer + && handlinger().filter(x => x === 'key').length === k0, r.tekst.slice(0, 100)); + } + svar(); + + // R6 (runde 1, Astra 1 + Fable F3): «Afsend», en navnloes ikon-knap og mellemrum paa en knap sender ogsaa. + ikonSvar = 'nej'; + for (const [navn, el] of [['«Afsend»', { name: 'Afsend', role: 'AXButton' }], ['«Besvar»', { name: 'Besvar', role: 'AXButton' }], + ['en navnloes knap', { role: 'AXButton' }]]) { + foer = spurgt.length; svar({ 'press --dry': { would_press: { ...el, ...KNAP } } }); + await kald('computer_press', { app: 'WhatsApp', role: 'AXButton', index: 3 }); + check(`r6 ${navn} spoerger`, spurgt.length === foer + 1, `${spurgt.length - foer}`); + } + // Med fokus paa knappen kan beskedteksten ikke laeses -> afvist (fejl lukket). Det der + // taeller: tasten naar aldrig frem uden et ja. + foer = spurgt.length; svar({ samtale: { ...SAMTALE, field: { role: 'AXButton' } } }); + const ks = handlinger().filter(x => x === 'key').length; + const sp = await kald('computer_key', { app: 'WhatsApp', combo: 'space' }); + check('r6 mellemrum med fokus paa en knap: behandlet som send (her afvist), tasten trykkes ikke', sp.fejl + && /could not be read/.test(sp.tekst) && handlinger().filter(x => x === 'key').length === ks, sp.tekst.slice(0, 90)); + foer = spurgt.length; svar(); + const ksp = handlinger().filter(x => x === 'key').length; + const spt = await kald('computer_key', { app: 'WhatsApp', combo: 'space' }); + check('r6 ...men mellemrum i tekstfeltet spoerger ikke - og tasten trykkes', spurgt.length === foer && !spt.fejl + && handlinger().filter(x => x === 'key').length === ksp + 1, `${spurgt.length - foer} · ${spt.tekst.slice(0, 60)}`); + + // R6 (Fable P3): en webchat i en browser er ogsaa en afsendelse - en almindelig side er ikke. + foer = spurgt.length; svar({ samtale: { ...SAMTALE, window: 'WhatsApp - Google Chrome' } }); + await kald('computer_key', { app: 'Google Chrome', combo: 'return' }); + check('r6 Return i WhatsApp Web i en browser spoerger', spurgt.length === foer + 1, `${spurgt.length - foer}`); + foer = spurgt.length; svar({ samtale: { ...SAMTALE, window: 'Wikipedia - Google Chrome' } }); + const k1 = handlinger().filter(x => x === 'key').length; + await kald('computer_key', { app: 'Google Chrome', combo: 'return' }); + check('r6 ...Return paa en almindelig side spoerger ikke', spurgt.length === foer && handlinger().filter(x => x === 'key').length === k1 + 1, `${spurgt.length - foer}`); + + // R7 (Fable F8): i mail er Return en ny linje - og en mail med afsnit kan skrives. + foer = spurgt.length; svar({ samtale: { ...SAMTALE, window: 'Ny besked' } }); + const t1 = handlinger().filter(x => x === 'type').length; + const mt = await kald('computer_type', { app: 'Mail', text: 'Hej Benjamin,\n\ncomputer-MCP virker.' }); + check('r7 en mail med afsnit skrives uden at spoerge', !mt.fejl && spurgt.length === foer && handlinger().filter(x => x === 'type').length === t1 + 1, mt.tekst.slice(0, 80)); + foer = spurgt.length; + const kmr = handlinger().filter(x => x === 'key').length; + const mr = await kald('computer_key', { app: 'Mail', combo: 'return' }); + check('r7 Return i mail spoerger ikke - og tasten trykkes', spurgt.length === foer && !mr.fejl + && handlinger().filter(x => x === 'key').length === kmr + 1, `${spurgt.length - foer} · ${mr.tekst.slice(0, 60)}`); + foer = spurgt.length; + await kald('computer_key', { app: 'Mail', combo: 'cmd+return' }); + check('r7 Cmd+Return i mail spoerger (Fable R2 8)', spurgt.length === foer + 1, `${spurgt.length - foer}`); + foer = spurgt.length; + await kald('computer_key', { app: 'Mail', combo: 'cmd+shift+d' }); + check('r7 ...men cmd+shift+D i mail spoerger', spurgt.length === foer + 1, `${spurgt.length - foer}`); + foer = spurgt.length; svar(); + const ctl = await kald('computer_type', { app: 'WhatsApp', text: 'hej\u0003' }); + check('r7 andre styretegn i en chat afvises ogsaa (Fable F4)', ctl.fejl && /line break sends/.test(ctl.tekst), ctl.tekst.slice(0, 80)); + + // R4 (runde 1, Astra 2): et klik der pegede paa Attach ved dommen og paa Send under + // laasen, maa ikke gaa igennem. Proeven holder laasen og skifter knappen imens. + svar({ at: { found: true, bundleId: WA, role: 'AXButton', title: 'Attach', description: '' } }); + const LAAS = join(STATE, 'laase'); mkdirSync(LAAS, { recursive: true }); + writeFileSync(join(LAAS, WA + '.lock'), String(process.pid)); + const c0 = handlinger().filter(x => x === 'click').length; + foer = spurgt.length; + const atFoer = HJ.kald().filter(k => k.argv[0] === 'at').length; + const venter = kald('computer_click', { app: 'WhatsApp', x: 500, y: 500 }); + // ⛔ 1/10: her stod en fast ventetid paa 1,2 s - under last naaede dommen ikke at se + // «Attach» foer knappen blev byttet (1 af 3 roede paa Gustavs Mac). Nu ventes der + // til serveren HAR slaaet knappen op, og saa en ekstra tand for at den naar laasen. + for (let i = 0; i < 100 && HJ.kald().filter(k => k.argv[0] === 'at').length === atFoer; i++) await new Promise(r => setTimeout(r, 100)); + await new Promise(r => setTimeout(r, 400)); + svar({ at: { found: true, bundleId: WA, role: 'AXButton', title: 'Send', description: '' } }); + unlinkSync(join(LAAS, WA + '.lock')); + const r4 = await venter; + check('r4 Attach ved dommen, Send under laasen: intet klikket', r4.fejl && /changed after the person approved/.test(r4.tekst) + && handlinger().filter(x => x === 'click').length === c0, r4.tekst.slice(0, 100)); + svar(); + + // R5 (runde 1, Astra 3): en ny overskrift eller vinduestitel efter ja er en anden samtale. + foer = spurgt.length; ikonSvar = 'ja'; + const k2 = handlinger().filter(x => x === 'key').length; + foerSvar = () => svar({ samtale: { ...SAMTALE, headings: ['Benjamin Riber', 'online', 'Alice'], window: 'Alice' } }); + const g2 = await kald('computer_key', { app: 'WhatsApp', combo: 'return' }); + check('r5 samtalen skiftede efter ja (tredje overskrift, titel): intet sendt', g2.fejl && /changed after the person approved/.test(g2.tekst) + && handlinger().filter(x => x === 'key').length === k2, g2.tekst.slice(0, 100)); + svar(); + // r5b: KUN vinduets titel skifter (samme overskrifter, samme tekst) - et andet vindue + // er en anden samtale. Det er fingeraftrykkets del; beskrivelsen ser det ikke. + const k3 = handlinger().filter(x => x === 'key').length; + foerSvar = () => svar({ samtale: { ...SAMTALE, window: 'WhatsApp - Alice' } }); + const g3 = await kald('computer_key', { app: 'WhatsApp', combo: 'return' }); + check('r5b kun vinduet skiftede efter ja: intet sendt', g3.fejl && /changed after the person approved/.test(g3.tekst) + && handlinger().filter(x => x === 'key').length === k3, g3.tekst.slice(0, 100)); + svar(); + // Q3 (runde 2, Astra 3): binding til modtager, felt og vindue. + ikonSvar = 'nej'; + const afvist = async (navn, sam, ekstra, kaldet = ['computer_key', { app: 'WhatsApp', combo: 'return' }], grund = /could not|not in the conversation/) => { + foer = spurgt.length; svar({ samtale: sam, ...ekstra }); + const r = await kald(...kaldet); + check(navn, r.fejl && grund.test(r.tekst) && spurgt.length === foer, r.tekst.slice(0, 110)); + }; + await afvist('q3 uden kolonne (feltets placering ulaeselig): afvist, ikke spurgt', { ...SAMTALE, column: false }, {}); + await afvist('q3 et soegefelt med fokus er ikke en besked: afvist', { ...SAMTALE, field: { role: 'AXTextField', subrole: 'AXSearchField', value: 'Benjamin' } }, {}); + await afvist('q3 Send-knappens vindue kan ikke laeses: afvist (Astra R3 2)', SAMTALE, + { 'press --dry': { would_press: { name: 'Send', role: 'AXButton', frame: KNAP.frame } } }, + ['computer_press', { app: 'WhatsApp', role: 'AXButton', title: 'Send' }]); + await afvist('q3 Send-knappen ligger ikke ved beskedfeltet: afvist', SAMTALE, + { 'press --dry': { would_press: { name: 'Send', role: 'AXButton', window: 'WhatsApp', frame: { x: 570, y: 80, w: 30, h: 30 } } } }, + ['computer_press', { app: 'WhatsApp', role: 'AXButton', title: 'Send' }]); + // Runde 4 (Astra 2): tre veje uden om bindingen. + await afvist('r4a samme titel og hoejde, men langt til hoejre for feltet: afvist', SAMTALE, + { 'press --dry': { would_press: { name: 'Send', role: 'AXButton', window: 'WhatsApp', frame: { x: 1500, y: 605, w: 30, h: 30 } } } }, + ['computer_press', { app: 'WhatsApp', role: 'AXButton', title: 'Send' }]); + { + foer = spurgt.length; svar({ samtale: SAMTALE, 'press --dry': { ok: false, error: 'opslaget fejlede', code: 'ax-timeout' } }); + const p0 = handlinger().filter(x => x === 'press').length; + const r = await kald('computer_press', { app: 'WhatsApp', role: 'AXButton', title: 'Send' }); + check('r4b opslaget af Send-kontrollen fejler: afvist, ikke spurgt, intet trykket', r.fejl && spurgt.length === foer + && handlinger().filter(x => x === 'press').length === p0, r.tekst.slice(0, 110)); + } + { + foer = spurgt.length; + svar({ samtale: { ...SAMTALE, window: 'Wikipedia - Google Chrome' }, + 'press --dry': { would_press: { name: 'Send', role: 'AXButton', window: 'WhatsApp - Google Chrome', frame: KNAP.frame } } }); + const p0 = handlinger().filter(x => x === 'press').length; + const r = await kald('computer_press', { app: 'Google Chrome', role: 'AXButton', title: 'Send' }); + check('r4c browser: fokus paa en almindelig side, Send i et WhatsApp-vindue: aldrig trykket uden ja', (r.fejl || spurgt.length === foer + 1) + && handlinger().filter(x => x === 'press').length === p0, r.tekst.slice(0, 110)); + } + { + // r4d (runde 5, Astra 2): browser, fokus paa en almindelig side, Send-kontrol hvis vindue + // IKKE kan laeses -> ukendt vindue, doemmes som en chat: aldrig trykket uden ja. + foer = spurgt.length; + svar({ samtale: { ...SAMTALE, window: 'Wikipedia - Google Chrome' }, + 'press --dry': { would_press: { name: 'Send', role: 'AXButton', frame: KNAP.frame } } }); + const p0 = handlinger().filter(x => x === 'press').length; + const r = await kald('computer_press', { app: 'Google Chrome', role: 'AXButton', title: 'Send' }); + check('r4d browser: Send-kontrol med ulaeseligt vindue: aldrig trykket uden ja', (r.fejl || spurgt.length === foer + 1) + && handlinger().filter(x => x === 'press').length === p0, r.tekst.slice(0, 110)); + } + await afvist('q3 Send-knappen ligger i et andet vindue end samtalen: afvist', SAMTALE, + { 'press --dry': { would_press: { name: 'Send', role: 'AXButton', window: 'Arkiv', frame: KNAP.frame } } }, + ['computer_press', { app: 'WhatsApp', role: 'AXButton', title: 'Send' }]); + foer = spurgt.length; svar({ samtale: { ...SAMTALE, headings: ['Benjamin Riber', 'online', 'hej ses i morgen'] } }); + await kald('computer_key', { app: 'WhatsApp', combo: 'return' }); + const qTo = spurgt[foer]?.text || ''; + check('q3 kun den foerste overskrift vises som modtager (Fable R2 4)', /To \(read from the screen, above the text field\): Benjamin Riber Message/.test(qTo) && !/ses i morgen/.test(qTo), qTo.slice(0, 160)); + + // W (1/10, MAALT i Gustavs rigtige WhatsApp): navnet staar i en KNAP oeverst i samtalen, + // og overskrifterne over feltet er datoer. Hjaelperens «recipient» vinder over dem. + foer = spurgt.length; svar({ samtale: { ...SAMTALE, headings: ['I dag'], recipient: 'Benjamin Riber' } }); + await kald('computer_key', { app: 'WhatsApp', combo: 'return' }); + const qW = spurgt[foer]?.text || ''; + check('w1 modtageren fra samtalens top vises - ikke datoen over feltet', /: Benjamin Riber Message/.test(qW) && !/: I dag Message/.test(qW), qW.slice(0, 160)); + // ...og uden modtager og uden overskrifter: afvist som foer (intet gaettes). + await afvist('w2 hverken modtager eller overskrift: intet sendt', { ...SAMTALE, headings: [] }, {}, + ['computer_key', { app: 'WhatsApp', combo: 'return' }]); + + // Q4 (runde 2, Astra 4 + Fable 7): browseren. + await afvist('q4 browser med ulaeselig titel behandles som chat (fejl lukket): intet sendt', { window: '', headings: [], field: {} }, {}, + ['computer_key', { app: 'Google Chrome', combo: 'return' }]); + foer = spurgt.length; svar({ samtale: { ...SAMTALE, window: 'Gmail - Slack' } }); + await kald('computer_key', { app: 'Google Chrome', combo: 'return' }); + check('q4 «Gmail - Slack» er en chat: Return spoerger', spurgt.length === foer + 1, `${spurgt.length - foer}`); + foer = spurgt.length; svar({ samtale: { ...SAMTALE, window: 'Signal processing - Wikipedia' } }); + const kw = handlinger().filter(x => x === 'key').length; + await kald('computer_key', { app: 'Google Chrome', combo: 'return' }); + check('q4 «Signal processing - Wikipedia» er ikke en chat', spurgt.length === foer && handlinger().filter(x => x === 'key').length === kw + 1, `${spurgt.length - foer}`); + foer = spurgt.length; svar({ samtale: { ...SAMTALE, window: 'Inbox (3) - x@gmail.com - Gmail' } }); + await kald('computer_key', { app: 'Google Chrome', combo: 'return' }); + check('q4 webmail: Return spoerger ikke', spurgt.length === foer, `${spurgt.length - foer}`); + foer = spurgt.length; + await kald('computer_key', { app: 'Google Chrome', combo: 'cmd+return' }); + check('q4 webmail: Cmd+Return spoerger', spurgt.length === foer + 1, `${spurgt.length - foer}`); + + // Q9 (runde 2, Fable 2): et klik paa en navnloes GRUPPE er ikke en send; paa en navnloes KNAP er det. + foer = spurgt.length; svar({ at: { found: true, bundleId: WA, role: 'AXGroup', title: '', description: '' } }); + const kc = handlinger().filter(x => x === 'click').length; + const gk = await kald('computer_click', { app: 'WhatsApp', x: 300, y: 300 }); + check('q9 klik paa en navnloes gruppe: ikke en afsendelse, klikket sker', spurgt.length === foer && !gk.fejl + && handlinger().filter(x => x === 'click').length === kc + 1, gk.tekst.slice(0, 80)); + foer = spurgt.length; svar({ at: { found: true, bundleId: WA, role: 'AXButton', title: '', description: '', ...KNAP } }); + await kald('computer_click', { app: 'WhatsApp', x: 300, y: 300 }); + check('q9 klik paa en navnloes knap spoerger', spurgt.length === foer + 1, `${spurgt.length - foer}`); + // Runde 3 (Astra 3): en UKENDT rolle (opslaget fejlede) er ikke ufarlig ved et klik. + foer = spurgt.length; svar({ at: { found: true, bundleId: WA, role: '', title: '', description: '', ...KNAP } }); + const kq = handlinger().filter(x => x === 'click').length; + await kald('computer_click', { app: 'WhatsApp', x: 300, y: 300 }); + check('q9 klik paa et element med ukendt rolle spoerger, klikkes ikke uden ja', spurgt.length === foer + 1 + && handlinger().filter(x => x === 'click').length === kq, `${spurgt.length - foer}`); + svar(); + + check('ingen dialog blev rejst i hele proeven', SP.gangeSpurgt() === 0, `${SP.gangeSpurgt()}`); +} finally { + srv.kill(); +} +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/server-e2e.mjs b/test/server-e2e.mjs index 522ab1a..3fdf1fe 100644 --- a/test/server-e2e.mjs +++ b/test/server-e2e.mjs @@ -1,5 +1,6 @@ // Taler MCP-protokollen mod serveren som en rigtig klient ville. // Koeres i readonly, saa der ikke popper samtykke-dialoger op i en proeve. +import './ryd-op.mjs'; import { spawn } from 'child_process'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; diff --git a/test/skaerm-laan.mjs b/test/skaerm-laan.mjs new file mode 100644 index 0000000..fb6e4cb --- /dev/null +++ b/test/skaerm-laan.mjs @@ -0,0 +1,362 @@ +// SKAERM-LAANET: kan en agent faa skaermen - KUN naar mennesket giver den, kun +// saa laenge, og aldrig mens mennesket selv bruger den? +// +// ⛔ HVORFOR DEN FINDES (29/9-2026, panelet: B2 + B5) +// Foer: forgrund kun via CMCP_BACKGROUND=0 og genstart, pr. proces - 21 servere +// kunne vaere uenige om én skaerm. Nu ejer menulinje-ikonet kontakten: mennesket +// laaner EN agent skaermen med Touch ID, i hoejst 15 minutter, og kan tage den +// tilbage. Laanet ER den aabne forbindelse; lukkes den, er skaermen menneskets. +// +// Rigtig server, FALSK ikon (ingen menu, intet Touch ID), FALSK hjaelper med +// scriptet «idle»-svar. Intet flyttes, intet tager skaermen. +import './ryd-op.mjs'; +import { createServer } from 'node:net'; +import { spawn } from 'node:child_process'; +import { mkdtempSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { lavFalskHjaelper, lavFalskSpoerger } from './falsk-hjaelper.mjs'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); +const fails = []; +const check = (l, c, d = '') => { console.log(`${c ? 'OK ' : 'DUMP'} ${l}${d ? ' - ' + d : ''}`); if (!c) fails.push(l); }; +const vent = ms => new Promise(r => setTimeout(r, ms)); + +function lavIkon(state) { + const ikon = { svar: 'ja', laanMs: 60_000, spurgt: [], laan: null, lukketAfServer: 0, koe: [] }; + // Skaerm-koe (2/10): naar det aktive laan slutter, svares den AELDSTE ventende + // skaerm-anmodning «ja» - ingen ny forespoergsel noedvendig (ikke en afvisning). + // ⛔ MAALT: en lokal .destroy() udloeser «close» paa EGEN socket, ikke «end» + // («end» er kun for en FJERN-afsendt FIN) - saa koeen rykker videre paa + // «close», som faar naar laanet slutter uanset hvem der lukkede det. + const naesteIKoe = () => { + const n = ikon.koe.shift(); + if (!n) return; + ikon.laan = n.sock; + n.sock.on('close', () => { if (ikon.laan === n.sock) ikon.laan = null; naesteIKoe(); }); + try { n.sock.write(JSON.stringify({ nonce: n.q.nonce, ok: true, verified: 'owner', until: Date.now() + ikon.laanMs }) + '\n'); } catch {} + }; + return new Promise(res => createServer(sock => { + let buf = '', svaret = false; + sock.on('data', d => { + if (svaret) return; + buf += d; const i = buf.indexOf('\n'); if (i < 0) return; + const q = JSON.parse(buf.slice(0, i)); ikon.spurgt.push(q); svaret = true; + if (ikon.svar === 'koe' && q.kind === 'screen') { + if (ikon.laan) { + // Et andet laan har allerede skaermen: staar i koen, IKKE afvist - + // serveren venter selv (skaermVentetid()), fake-ikonet svarer foerst + // naar koen aabner (naesteIKoe). + ikon.koe.push({ sock, q }); + sock.on('close', () => { ikon.koe = ikon.koe.filter(x => x.sock !== sock); }); + } else { + ikon.laan = sock; + sock.on('close', () => { if (ikon.laan === sock) ikon.laan = null; naesteIKoe(); }); + sock.write(JSON.stringify({ nonce: q.nonce, ok: true, verified: 'owner', until: Date.now() + ikon.laanMs }) + '\n'); + } + } else if (ikon.svar === 'langsom') { // svarer ja efter 800 ms + setTimeout(() => { try { sock.write(JSON.stringify({ nonce: q.nonce, ok: true, verified: 'owner', until: Date.now() + ikon.laanMs }) + '\n'); } catch {} }, 800); + ikon.laan = sock; sock.on('end', () => { ikon.lukketAfServer++; }); + } else if (ikon.svar === 'ja') { + sock.write(JSON.stringify({ nonce: q.nonce, ok: true, verified: 'owner', until: Date.now() + ikon.laanMs }) + '\n'); + ikon.laan = sock; // forbindelsen holdes AABEN: den er laanet + sock.on('end', () => { ikon.lukketAfServer++; }); + } else if (ikon.svar === 'optaget') { + sock.end(JSON.stringify({ nonce: q.nonce, ok: false, verified: 'optaget' }) + '\n'); + } else { + sock.end(JSON.stringify({ nonce: q.nonce, ok: false, verified: 'none' }) + '\n'); + } + }); + sock.on('error', () => {}); + }).listen(join(state, 'ikon.sock'), () => res(ikon))); +} + +function server(state, hj, ekstra = {}) { + const sp = lavFalskSpoerger('ja', 'cmcp-laan-sp'); + // Menneske-graensen er 5 s her (standard 1,5): prooverne af attribution maa ikke + // afhaenge af hvor hurtigt en belastet maskine svarer (5d var ustabil, 30/9). + const env = { ...process.env, CMCP_STATE_DIR: state, CMCP_MODE: 'allow', CMCP_HELPER: hj.sti, CMCP_MENNESKE_SEK: '5', + CMCP_ASK_TIMEOUT: '3', CMCP_STATUS_IKON: '0', CMCP_OSASCRIPT: sp.sti, ...ekstra }; + if (!('CMCP_BACKGROUND' in ekstra)) delete env.CMCP_BACKGROUND; + const srv = spawn('node', [join(ROOT, 'mcp-server', 'index.js')], { env, stdio: ['pipe', 'pipe', 'pipe'] }); + let buf = ''; const v = new Map(); let n = 0; const meldt = []; + srv.stdout.on('data', d => { buf += d; let i; + while ((i = buf.indexOf('\n')) >= 0) { const l = buf.slice(0, i); buf = buf.slice(i + 1); + try { const m = JSON.parse(l); if (m.method) meldt.push(m.method); else { v.get(m.id)?.(m); v.delete(m.id); } } catch {} } }); + const rpc = (method, params = {}) => new Promise((res, rej) => { const id = ++n; v.set(id, res); + srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); + setTimeout(() => rej(new Error('timeout ' + method)), 30000); }); + return { + srv, sp, meldt, + async klar() { await rpc('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'chat-laan', version: '1' } }); + srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'); }, + async navne() { return ((await rpc('tools/list')).result?.tools || []).map(t => t.name); }, + async kald(name, args) { const r = await rpc('tools/call', { name, arguments: args }); + return { fejl: !!r.result?.isError, tekst: r.result?.content?.[0]?.text || JSON.stringify(r.error || {}) }; } + }; +} +const listeMeldt = (s) => s.meldt.filter(m => m === 'notifications/tools/list_changed').length; + +const STATE = mkdtempSync(join(tmpdir(), 'cmcp-laan-')); +const HJ = lavFalskHjaelper('cmcp-laan'); +// «Hvad er forrest» SKAL vaere scriptet: ellers svarer den rigtige Mac, og proevens +// programlaas rammer et andet program end kaldet (6b var groen af den grund, 30/9). +const APPS = { apps: [{ name: 'Finder', bundleId: 'com.apple.finder', active: true }, { name: 'TextEdit', bundleId: 'com.apple.TextEdit', active: false }] }; +// Og «hvem ejer punktet» (at) ogsaa: ellers laeses menneskets RIGTIGE skaerm, og +// prooven afhaenger af hvad der ligger under (5,5) (kørsel 2, 30/9: WhatsApp). +const AT = { found: true, bundleId: 'com.apple.finder', role: 'AXWindow', title: '', description: '', under: [] }; +const saet = (o) => HJ.saetSvar({ apps: APPS, at: AT, ...o }); +saet({ idle: { idle: 30 } }); +const ikon = await lavIkon(STATE); +const S = server(STATE, HJ); +const flyt = () => HJ.handlingerNaaedeFrem().filter(k => k.argv[0] === 'move').length; +try { + await S.klar(); + let navne = await S.navne(); + check('1 i baggrund: move er skjult, request_screen tilbydes', !navne.includes('computer_move') && navne.includes('computer_request_screen')); + const r0 = await S.kald('computer_move', { x: 5, y: 5 }); + check('1b ...og move afvises, med vejen til at bede om skaermen', r0.fejl && /computer_request_screen/.test(r0.tekst), r0.tekst.slice(0, 90)); + + const uden = await S.kald('computer_request_screen', { action: 'request' }); + check('2 uden grund: afvist, ikonet ikke spurgt', uden.fejl && /reason/.test(uden.tekst) && ikon.spurgt.length === 0, uden.tekst.slice(0, 80)); + + // 3. Mennesket giver skaermen. + const foerMeld = listeMeldt(S); + const ja = await S.kald('computer_request_screen', { action: 'request', reason: 'drag the file to the Trash', minutes: 5 }); + const q = ikon.spurgt[0]; + check('3 laanet: givet', !ja.fejl && /"granted":\s*true/.test(ja.tekst), ja.tekst.slice(0, 90)); + check('3 ...ikonet blev spurgt om SKAERMEN, med grund og minutter', q?.kind === 'screen' && q?.minutes === 5 && /drag the file/.test(q?.text || ''), JSON.stringify(q || {}).slice(0, 140)); + check('3 ...og det siger at agenten pauser og at skaermen kan tages tilbage', /waits if you are using the keyboard or mouse/.test(q?.scope || '') && /Take the screen back/.test(q?.scope || ''), q?.scope); + await vent(200); + navne = await S.navne(); + check('3 ...klienten faar besked om at vaerktoejerne aendrede sig', listeMeldt(S) > foerMeld, `${listeMeldt(S) - foerMeld} meldinger`); + check('3 ...og move tilbydes nu', navne.includes('computer_move')); + + // 4. Med laanet: move virker - naar mennesket IKKE bruger maskinen. + const m1 = await S.kald('computer_move', { x: 5, y: 5 }); + check('4 med laanet og ingen menneske-input: move udfoeres', !m1.fejl && flyt() === 1, m1.tekst.slice(0, 80)); + // 5. Mennesket bruger musen: agenten venter, intet flyttes. + await vent(700); + saet({ idle: { idle: 0.1 } }); + const m2 = await S.kald('computer_move', { x: 6, y: 6 }); + check('5 mennesket bruger maskinen: agenten venter, intet flyttes', m2.fejl && /using the keyboard or mouse/.test(m2.tekst) && flyt() === 1, m2.tekst.slice(0, 100)); + saet({ idle: { idle: 'ulaeseligt' } }); // scriptet: aldrig den rigtige hjaelper + const m2b = await S.kald('computer_move', { x: 6, y: 6 }); + check('5b kan det ikke laeses om mennesket er der: intet flyttes', m2b.fejl && flyt() === 1, m2b.tekst.slice(0, 100)); + saet({ idle: { idle: 30 } }); + + // 5c (runde 1, Astra 5 + Fable F2): pausen gaelder OGSAA stille kald med `app` under laanet. + const t0 = HJ.handlingerNaaedeFrem().filter(k => k.argv[0] === 'type').length; + await vent(600); + saet({ idle: { idle: 0.1 } }); + const ty = await S.kald('computer_type', { app: 'Finder', text: 'x' }); + check('5c under laanet pauser ogsaa et stille kald med app', ty.fejl && /using the keyboard or mouse/.test(ty.tekst) + && HJ.handlingerNaaedeFrem().filter(k => k.argv[0] === 'type').length === t0, ty.tekst.slice(0, 90)); + // 5d (Fable): input der er AELDRE end vores egen sidste handling, er vores eget - ingen pause. + saet({ idle: { idle: 30 } }); + const forr5d = await S.kald('computer_move', { x: 5, y: 5 }); + check('forudsaetning 5d: flytningen foer blev udfoert', !forr5d.fejl, forr5d.tekst.slice(0, 120)); + await vent(200); + saet({ idle: { idle: 4.5 } }); // aeldre end vores egen handling, men under graensen + const egen = await S.kald('computer_move', { x: 6, y: 6 }); + check('5d input fra foer vores egen sidste handling taeller ikke som et menneske', !egen.fejl, egen.tekst.slice(0, 90)); + // 5e (runde 4, Astra 3 + Fable 1): en skrivning via tilgaengeligheds-laget POSTER intet + // input - den maa ikke goere menneskets input fra foer den til «vores». Sidste + // rigtige input-handling ligger >3 s tilbage; AX-skrivningen lige nu. + await vent(3200); + saet({ idle: { idle: 30 }, type: { method: 'accessibility', verified: true } }); + const ax = await S.kald('computer_type', { app: 'Finder', text: 'hej' }); + check('forudsaetning 5e: AX-skrivningen blev udfoert', !ax.fejl, ax.tekst.slice(0, 100)); + saet({ idle: { idle: 1.8 } }); // input for 1,8 s siden: FOER AX-skrivningen, EFTER sidste rigtige input + const efterAx = await S.kald('computer_move', { x: 7, y: 7 }); + check('5e input foer en AX-skrivning er stadig menneskets - agenten venter', efterAx.fejl && /using the keyboard or mouse/.test(efterAx.tekst), efterAx.tekst.slice(0, 100)); + saet({ idle: { idle: 30 } }); + const flytNu = flyt(); + + // 6. Mennesket tager skaermen tilbage: lukket forbindelse = slut. + const foer6 = listeMeldt(S); + ikon.laan.destroy(); + await vent(300); + navne = await S.navne(); + const m3 = await S.kald('computer_move', { x: 7, y: 7 }); + check('6 skaermen taget tilbage: listen meldes, move skjult og afvist', listeMeldt(S) > foer6 && !navne.includes('computer_move') && m3.fejl && flyt() === flytNu, + `${listeMeldt(S) - foer6} meldinger · ${m3.tekst.slice(0, 60)}`); + + // 6b (runde 1, Astra 4): et kald der VENTER paa programlaasen, mens laanet tilbagekaldes, + // maa ikke udfoeres bagefter. Proeven holder selv laasen. + ikon.svar = 'ja'; ikon.laanMs = 60_000; + { const f = await S.kald('computer_request_screen', { action: 'request', reason: 'move the pointer', minutes: 2 }); + check('forudsaetning: laanet til «move the pointer» blev givet', /"granted":\s*true/.test(f.tekst), f.tekst.slice(0, 120)); } + const LAAS = join(STATE, 'laase'); mkdirSync(LAAS, { recursive: true }); + writeFileSync(join(LAAS, 'com.apple.finder.lock'), String(process.pid)); + const f6 = flyt(); + const venter6 = S.kald('computer_move', { x: 9, y: 9 }); + await vent(2000); ikon.laan.destroy(); await vent(300); // 2 s: kaldet skal have naaet laasen (500 ms var for lidt paa en belastet maskine, 30/9) + unlinkSync(join(LAAS, 'com.apple.finder.lock')); + const r6 = await venter6; + check('6b laanet sluttede mens kaldet ventede paa laasen: intet flyttes', r6.fejl && /screen loan changed while this waited/.test(r6.tekst) && flyt() === f6, r6.tekst.slice(0, 90)); + + // 6c (runde 2, Astra 1): en STILLE skrivning (type med app) doemt under laanet, der + // venter paa laasen mens laanet slutter, sker heller ikke. + { const f = await S.kald('computer_request_screen', { action: 'request', reason: 'type in Finder', minutes: 2 }); + check('forudsaetning: laanet til «type in Finder» blev givet', /"granted":\s*true/.test(f.tekst), f.tekst.slice(0, 120)); } + writeFileSync(join(LAAS, 'com.apple.finder.lock'), String(process.pid)); + const ty0 = HJ.handlingerNaaedeFrem().filter(k => k.argv[0] === 'type').length; + const venter6c = S.kald('computer_type', { app: 'Finder', text: 'x' }); + await vent(2000); ikon.laan.destroy(); await vent(300); // 2 s: kaldet skal have naaet laasen (500 ms var for lidt paa en belastet maskine, 30/9) + unlinkSync(join(LAAS, 'com.apple.finder.lock')); + const r6c = await venter6c; + check('6c en stille skrivning doemt under laanet sker ikke efter laanets slut', r6c.fejl && /screen loan changed while this waited/.test(r6c.tekst) + && HJ.handlingerNaaedeFrem().filter(k => k.argv[0] === 'type').length === ty0, r6c.tekst.slice(0, 90)); + + // 6d (runde 2, Fable 3): en skrivning der KOERER naar mennesket tager skaermen tilbage, stoppes. + { const f = await S.kald('computer_request_screen', { action: 'request', reason: 'type a long text', minutes: 2 }); + check('forudsaetning: laanet til «type a long text» blev givet', /"granted":\s*true/.test(f.tekst), f.tekst.slice(0, 120)); } + saet({ idle: { idle: 30 }, type: { _vent: 4000, typed: 99 } }); + const loeber = S.kald('computer_type', { app: 'Finder', text: 'en lang tekst' }); + await vent(1200); ikon.laan.destroy(); + const r6d = await loeber; + check('6d en kørende skrivning stoppes naar skaermen tages tilbage', r6d.fejl && /screen loan ended while this ran/.test(r6d.tekst), r6d.tekst.slice(0, 90)); + saet({ idle: { idle: 30 } }); + + // 6f (runde 4, Astra 4): et paste der KOERER naar laanet slutter, faar besked om at + // stoppe foer Cmd+V (SIGUSR1) - ikke et drab der ville tabe udklipsholderen. + { const f = await S.kald('computer_request_screen', { action: 'request', reason: 'paste a paragraph', minutes: 2 }); + check('forudsaetning: laanet til «paste a paragraph» blev givet', /"granted":\s*true/.test(f.tekst), f.tekst.slice(0, 120)); } + saet({ idle: { idle: 30 }, paste: { _vent: 4000, pasted: true } }); + const pl = S.kald('computer_paste', { text: 'et langt afsnit' }); + await vent(1500); ikon.laan.destroy(); + const r6f = await pl; + const sig = HJ.kald().filter(k => k.signal === 'SIGUSR1' && k.argv[0] === 'paste').length; + check('6f et koerende paste faar SIGUSR1 og stopper foer Cmd+V', sig === 1 && r6f.fejl && /stopped before pasting/.test(r6f.tekst), `${sig} signal · ${r6f.tekst.slice(0, 80)}`); + saet({ idle: { idle: 30 } }); + + // 6g (runde 5, Astra 1): et kald startet UDEN laan, hvor et laan begynder OG slutter + // mens det venter paa laasen. «Intet laan» foer og efter er ikke «uaendret». + saet({ idle: { idle: 30 } }); + writeFileSync(join(LAAS, 'com.apple.TextEdit.lock'), String(process.pid)); + const p6g0 = HJ.handlingerNaaedeFrem().filter(k => k.argv[0] === 'press').length; + const venter6g = S.kald('computer_press', { app: 'TextEdit', role: 'AXButton', title: 'OK' }); + await vent(1500); + { const f = await S.kald('computer_request_screen', { action: 'request', reason: 'a moment', minutes: 2 }); + check('forudsaetning 6g: laanet mellem start og udfoerelse blev givet', /"granted":\s*true/.test(f.tekst), f.tekst.slice(0, 100)); } + await S.kald('computer_request_screen', { action: 'release' }); + await vent(300); + unlinkSync(join(LAAS, 'com.apple.TextEdit.lock')); + const r6g = await venter6g; + check('6g et laan der begyndte og sluttede mens kaldet ventede: intet udfoeres', r6g.fejl && /screen loan changed while this waited/.test(r6g.tekst) + && HJ.handlingerNaaedeFrem().filter(k => k.argv[0] === 'press').length === p6g0, r6g.tekst.slice(0, 110)); + + // 6e (runde 2, Astra 1): to anmodninger paa én gang fra samme agent. Den anden afvises + // («already waiting»); den foerste godkendes - og naar den tages tilbage, SLUTTER den. + ikon.svar = 'langsom'; + const [la, lb] = await Promise.all([ + S.kald('computer_request_screen', { action: 'request', reason: 'first', minutes: 2 }), + (async () => { await vent(100); return S.kald('computer_request_screen', { action: 'request', reason: 'second', minutes: 2 }); })()]); + check('6e den anden samtidige anmodning afvises', lb.fejl && /already has a question waiting/.test(lb.tekst) && !la.fejl, `${la.tekst.slice(0, 40)} | ${lb.tekst.slice(0, 60)}`); + const meld6e = listeMeldt(S); + ikon.laan.destroy(); await vent(300); + const st6e = await S.kald('computer_request_screen', { action: 'status' }); + check('6e ...og naar den godkendte tages tilbage, er laanet SLUT (listen meldt)', /background/.test(st6e.tekst) && listeMeldt(S) > meld6e, st6e.tekst.slice(0, 80)); + ikon.svar = 'ja'; + + // 7. Tiden udloeber: vores eget ur, ogsaa hvis ikonet ikke lukker. + ikon.laanMs = 1200; + await S.kald('computer_request_screen', { action: 'request', reason: 'move a window', minutes: 1 }); + const lukFoer = ikon.lukketAfServer; + const flyt7 = flyt(); + const meldFoer7 = listeMeldt(S); + await vent(1700); + const m4 = await S.kald('computer_move', { x: 8, y: 8 }); + check('7 laanet udloeb: move afvist uden at ikonet lukkede', m4.fejl && flyt() === flyt7, m4.tekst.slice(0, 80)); + await vent(200); + check('7 ...og serveren lukkede selv forbindelsen', ikon.lukketAfServer > lukFoer, `${ikon.lukketAfServer - lukFoer}`); + check('7b ...og klienten fik besked om at vaerktoejerne forsvandt (Fable F6)', listeMeldt(S) > meldFoer7, `${listeMeldt(S) - meldFoer7}`); + + // 8. Agenten giver den tilbage selv. + ikon.laanMs = 60_000; + await S.kald('computer_request_screen', { action: 'request', reason: 'resize a window', minutes: 3 }); + const lukFoer8 = ikon.lukketAfServer; + const rel = await S.kald('computer_request_screen', { action: 'release' }); + await vent(200); + const st = await S.kald('computer_request_screen', { action: 'status' }); + check('8 release: givet tilbage, forbindelsen lukket, status siger baggrund', + /"released":\s*true/.test(rel.tekst) && ikon.lukketAfServer > lukFoer8 && /background/.test(st.tekst), st.tekst.slice(0, 80)); + + // 9. En anden agent har skaermen: afvist, og det er ikke et menneskes nej. + ikon.svar = 'optaget'; + const opt = await S.kald('computer_request_screen', { action: 'request', reason: 'x', minutes: 1 }); + check('9 skaermen er optaget: afvist med den grund', opt.fejl && /another agent has the screen/.test(opt.tekst), opt.tekst.slice(0, 80)); + + // 9b (runde 1, Astra 8): en grund over loftet spoerges ikke - den klippes ikke tavst. + ikon.svar = 'ja'; + const foer9b = ikon.spurgt.length; + const lang = await S.kald('computer_request_screen', { action: 'request', reason: 'x'.repeat(4100), minutes: 2 }); + check('9b en grund over loftet: afvist, ikonet ikke spurgt', lang.fejl && /more than the 4000/.test(lang.tekst) && ikon.spurgt.length === foer9b, lang.tekst.slice(0, 90)); + + // 10. Et nej er et nej. + ikon.svar = 'nej'; + const nej = await S.kald('computer_request_screen', { action: 'request', reason: 'drag it', minutes: 2 }); + check('10 mennesket siger nej: afvist, move stadig skjult', nej.fejl && !(await S.navne()).includes('computer_move'), nej.tekst.slice(0, 80)); + check('ingen osascript-dialog i hele forloebet', S.sp.gangeSpurgt() === 0, `${S.sp.gangeSpurgt()} · ${JSON.stringify(S.sp.tekster?.() || []).slice(0, 300)}`); +} finally { S.srv.kill(); } + +// 11. CMCP_BACKGROUND sat er et LOFT: intet laan, ikonet ikke spurgt. +const STATE2 = mkdtempSync(join(tmpdir(), 'cmcp-laan-loft-')); +const ikon2 = await lavIkon(STATE2); +const L = server(STATE2, HJ, { CMCP_BACKGROUND: '1' }); +try { + await L.klar(); + const r = await L.kald('computer_request_screen', { action: 'request', reason: 'drag', minutes: 2 }); + check('11 laast til baggrund: afvist, ikonet ikke spurgt', r.fejl && /locked this server to background/.test(r.tekst) && ikon2.spurgt.length === 0, r.tekst.slice(0, 90)); +} finally { L.srv.kill(); } + +// 12. ⛔ 1/10 (skaerm-koe-panelet): skaerm-anmodninger ventede foer paa det +// almindelige 60 s-spoergsmaals-vindue (CMCP_ASK_TIMEOUT). Nu har de deres eget, +// laengere - bevist her ved at goere det MODSATTE: saette skaerm-vinduet +// KORTERE end askTimeout, og vise at skaerm-vinduet er det der afgoer det. Hvis +// koden ved et uheld blev lagt tilbage til askTimeout() (3 s i denne proeve), +// ville et svar efter 800 ms stadig naa frem - her SKAL det IKKE naa frem. +const STATE3 = mkdtempSync(join(tmpdir(), 'cmcp-laan-vent-')); +const ikon3 = await lavIkon(STATE3); +ikon3.svar = 'langsom'; ikon3.laanMs = 60_000; // svarer ja efter 800 ms +const V = server(STATE3, HJ, { CMCP_SCREEN_WAIT: '0.5', CMCP_ASK_TIMEOUT: '3' }); +try { + await V.klar(); + const t0 = Date.now(); + const r = await V.kald('computer_request_screen', { action: 'request', reason: 'kort skaerm-vindue', minutes: 2 }); + const ms = Date.now() - t0; + check('12 skaermVentetid() - ikke askTimeout() - styrer ventetiden paa et skaerm-laan', + r.fejl && /nobody answered in the menu bar in time/.test(r.tekst) && ms < 2500, + `${ms} ms · ${r.tekst.slice(0, 90)}`); +} finally { V.srv.kill(); } + +// 13. ⛔ 2/10 (skaerm-koe-panelet, trin 2): en skaerm-anmodning fra en ANDEN agent, +// mens et laan allerede er aktivt, bliver IKKE laengere afvist med «optaget» - +// den staar i koe, og serveren venter taalmodigt (den goer det allerede, trin 1: +// skaermVentetid()). TO servere (to agent-processer, samme ikon-socket) - en +// enkelt proces ville ramme 6e's «already waiting»-vagt foer den naaede koen. +const STATE4 = mkdtempSync(join(tmpdir(), 'cmcp-laan-koe-')); +const ikon4 = await lavIkon(STATE4); +ikon4.svar = 'koe'; ikon4.laanMs = 60_000; +const K1 = server(STATE4, HJ, { CMCP_SCREEN_WAIT: '10' }); +const K2 = server(STATE4, HJ, { CMCP_SCREEN_WAIT: '10' }); +try { + await Promise.all([K1.klar(), K2.klar()]); + const foerste = await K1.kald('computer_request_screen', { action: 'request', reason: 'first in line', minutes: 2 }); + check('13 foerste agent: laanet givet straks', /"granted":\s*true/.test(foerste.tekst), foerste.tekst.slice(0, 90)); + const foerAnden = Date.now(); + let andenSvaret = null; + const anden = K2.kald('computer_request_screen', { action: 'request', reason: 'second in line', minutes: 2 }).then(r => { andenSvaret = r; return r; }); + await vent(800); + check('13b anden agent staar STADIG og venter i koe - IKKE afvist som «optaget»', + andenSvaret === null && ikon4.koe.length === 1, `svaret: ${JSON.stringify(andenSvaret)} · koe: ${ikon4.koe.length}`); + ikon4.laan.destroy(); // foerste agent giver slip -> koeen aabner for den anden + const r = await anden; + check('13c anden agent godkendes naar foerste slutter - uden selv at spoerge forfra', + /"granted":\s*true/.test(r.tekst) && (Date.now() - foerAnden) > 700, `${Date.now() - foerAnden} ms · ${r.tekst.slice(0, 80)}`); +} finally { K1.srv.kill(); K2.srv.kill(); } + +console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); +process.exit(fails.length ? 1 : 0); diff --git a/test/skriv-ankommer.mjs b/test/skriv-ankommer.mjs index 07509eb..f584741 100644 --- a/test/skriv-ankommer.mjs +++ b/test/skriv-ankommer.mjs @@ -51,15 +51,46 @@ try { const r2 = koer('type', '--app', navn, '--keystrokes', '--text', 'x'); check('2 --keystrokes sender tastetryk og siger «ikke efterprøvet»', r2.ok && r2.method === 'keystrokes' && r2.verified === false, JSON.stringify(r2).slice(0, 160)); - // 3. Et kodeordsfelt med fokus: aldrig tilgængeligheds-vejen. + // 3. Et kodeordsfelt med fokus får INTET - hverken gennem tilgængeligheds-laget + // eller som tastetryk. Før 29/9 faldt `type` tilbage til tastetryk og tastede + // i feltet; kun `set_value` sagde nej (panelet 29/9). + const nej = (r) => r.ok === false && r.code === 'secure-field' && !r.typed; const sikker = await start({ CMCP_PROEVE_SIKKER: '1' }); const r3 = koer('type', '--app', sikker, '--text', 'hemmelig123'); - check('3 et kodeordsfelt får aldrig tekst sat ind gennem tilgængeligheds-laget', r3.method !== 'accessibility', JSON.stringify(r3).slice(0, 160)); + check('3 et kodeordsfelt får ingen tekst, heller ikke som tastetryk', nej(r3), JSON.stringify(r3).slice(0, 160)); + const r3k = koer('type', '--app', sikker, '--keystrokes', '--text', 'hemmelig123'); + check('3k ...heller ikke når tastetryk vælges direkte', nej(r3k), JSON.stringify(r3k).slice(0, 160)); // 3b. Et kodeordsfelt som på en webside (AXTextField med undertypen AXSecureTextField). // macOS beskytter ikke dette; kun vores egen vagt gør. const web = await start({ CMCP_PROEVE_SIKKER: 'web' }); const r3b = koer('type', '--app', web, '--text', 'hemmelig456'); - check('3b et kodeordsfelt som på en webside får heller ikke den vej', r3b.method !== 'accessibility', JSON.stringify(r3b).slice(0, 160)); + check('3b et kodeordsfelt som på en webside får heller ingen tekst', nej(r3b), JSON.stringify(r3b).slice(0, 160)); + // 3s (runde 3 30/9, Fable R3): set_value er den tredje skrivevej - samme regel. + // Et native kodeordsfelt har ROLLEN AXTextField og UNDERROLLEN AXSecureTextField (CI 30/9: --role fandt intet). + const r3s = koer('set-value', '--app', sikker, '--subrole', 'AXSecureTextField', '--text', 'hemmelig789'); + check('3s set_value i et kodeordsfelt: afvist', r3s.ok === false && r3s.code === 'secure-field', JSON.stringify(r3s).slice(0, 160)); + const r3sw = koer('set-value', '--app', web, '--subrole', 'AXSecureTextField', '--text', 'hemmelig789'); + check('3sw ...ogsaa som paa en webside', r3sw.ok === false && r3sw.code === 'secure-field', JSON.stringify(r3sw).slice(0, 160)); + // 3c. Fokus flytter ind i et kodeordsfelt MIDT i teksten (Tab i «bruger\tkode», + // et klik, et felt der selv hopper videre): skrivningen stopper dér. + const skift = await start({ CMCP_PROEVE_SIKKER_SKIFT: '1' }); + const fiks = boern[boern.length - 1]; + const r3c = await new Promise((res) => { + const p = spawn(HJAELPER, ['type', '--app', skift, '--keystrokes', '--cps', '5', '--text', 'a'.repeat(40)]); + let ud = ''; p.stdout.on('data', (d) => { ud += d; }); + p.on('close', () => { try { res(JSON.parse(ud)); } catch { res({ ok: false, error: ud.slice(0, 200) }); } }); + setTimeout(() => fiks.kill('SIGUSR1'), 2500); + }); + check('3c fokus der flytter ind i et kodeordsfelt undervejs stopper skrivningen dér', + r3c.ok === false && r3c.code === 'secure-field' && r3c.typed > 0 && r3c.typed < 40, JSON.stringify(r3c).slice(0, 180)); + + // 4. Sende-portens aflaesning (runde 1 30/9, Fable P4): modtageren er navnet OVER + // feltet i samme kolonne - aldrig sidebarens oeverste navn. Og teksten er feltets. + const sam = await start({ CMCP_PROEVE_SAMTALE: '1' }); + const s4 = koer('samtale', '--app', sam); + check('4 samtalen: modtageren er navnet over feltet, ikke sidebarens', + (s4.headings || [])[0] === 'Bob Samtale' && !(s4.headings || []).includes('Alice Sidebar') && s4.column === true, JSON.stringify(s4).slice(0, 200)); + check('4b ...og teksten er feltets', s4.field?.value === 'hej Bob', JSON.stringify(s4.field || {})); } finally { for (const b of boern) { try { b.kill(); } catch {} } rmSync(ARB, { recursive: true, force: true }); diff --git a/test/status-ikon.mjs b/test/status-ikon.mjs index d3e24bf..82dc8df 100644 --- a/test/status-ikon.mjs +++ b/test/status-ikon.mjs @@ -11,6 +11,7 @@ // 3. Det der blev tastet, staar ALDRIG i statusfilen eller i menuen. // 4. En agent der stopper, forsvinder - ogsaa en der blev draebt uden // at naa at rydde op efter sig. +import './ryd-op.mjs'; import { spawn, execFileSync } from 'child_process'; import { readFileSync, readdirSync, existsSync, mkdtempSync } from 'fs'; import { fileURLToPath } from 'url'; @@ -77,6 +78,15 @@ check('...og at det blev afvist', /⊘ .*Type \d+ characters/.test(liveA), liveA const liveB = d1.live.find(t => t.startsWith('chat-beta')) || ''; check('en laesning staar ogsaa, som ✓', /✓ .*apps/.test(liveB), liveB.split('\n')[2]); +// D3 (2/10): foelg-panelets "Show me where"-knap laeser det raa maal fra --dump, +// uden en levende GUI. chat-alfa's eneste handling navngav 'Notes'. +const alfaIdx = d1.items.findIndex(i => i.startsWith('chat-alfa')); +check('det seneste maal naar frem til --dump (D3, "Show me where")', + d1.nowTarget[alfaIdx] === 'Notes', JSON.stringify(d1.nowTarget)); +const betaIdx = d1.items.findIndex(i => i.startsWith('chat-beta')); +check('...og en handling UDEN app giver null, ikke en tom streng', + d1.nowTarget[betaIdx] === null, JSON.stringify(d1.nowTarget)); + // 3. Det tastede maa ikke staa nogen steder ikonet kan se. const filer = readdirSync(join(STATE, 'sessions')).map(f => readFileSync(join(STATE, 'sessions', f), 'utf8')).join('\n'); check('det tastede staar ikke i statusfilerne', !filer.includes(HEMMELIG), `${filer.length} tegn gennemsoegt`); diff --git a/tools/bevis.sh b/tools/bevis.sh index f73ca24..0ae8142 100755 --- a/tools/bevis.sh +++ b/tools/bevis.sh @@ -48,9 +48,15 @@ if [ "$UDGIVET" = "UMAALT" ]; then umaalt "npm" "kunne ikke naas" elif [ "$UDGIVET" = "$KILDE" ]; then ja "npm = kilden" "$UDGIVET" else nej "npm er bagud" "npx giver $UDGIVET, kilden er $KILDE"; fi -FORAN=$(git rev-list origin/main..HEAD --count 2>/dev/null || echo 0) -if [ "$FORAN" = "0" ]; then ja "alt er skubbet" "0 commits foran origin" -else nej "ikke skubbet" "$FORAN commits ligger lokalt"; fi +## ⛔ FUNDET under fuld-review 2/10: hardcoded mod origin/main gav falsk "ikke +## skubbet" på 64 commits - lokal main tracker origin/main (git config), men +## husets EGNE workflow pusher til en proeve/-gren via PR (main er LÅST, +## skub via PR). Tjekket maalte sin egen antagelse, ikke virkeligheden. +## Nu: er HEAD med på NOGEN fjern-gren, er den skubbet - navngiv hvilken. +git fetch origin --quiet 2>/dev/null || true +PAA_GREN=$(git branch -r --contains HEAD 2>/dev/null | grep -v ' -> ' | sed 's/^ *//' | head -1) +if [ -n "$PAA_GREN" ]; then ja "alt er skubbet" "HEAD er på $PAA_GREN" +else nej "ikke skubbet" "HEAD findes på ingen fjern-gren"; fi CI=$(gh run list -R Agent360dk/computerMCP -w CI --limit 20 --json headSha,conclusion \ -q "[.[] | select(.headSha==\"$(git rev-parse HEAD)\")] | .[0].conclusion" 2>/dev/null) diff --git a/tools/faerdiggoer-udgivelsen.sh b/tools/faerdiggoer-udgivelsen.sh index fbe45b9..9d659a3 100755 --- a/tools/faerdiggoer-udgivelsen.sh +++ b/tools/faerdiggoer-udgivelsen.sh @@ -31,15 +31,35 @@ if [ "$PAA_NPM" != "$VENTET" ]; then fi sig "1 · MCP-registret" +# ⛔ ASTRA (2/10): login og publish skal vaere to skridt, ikke et &&-kaedet - +# samme fejlklasse som release.sh allerede retter (set -e gaelder ikke i en &&-kaede). if command -v mcp-publisher >/dev/null 2>&1; then + mcp-publisher login github || { echo " login til registret fejlede"; faldt=1; } mcp-publisher publish || { echo " registret afviste - se ovenfor"; faldt=1; } else echo " mcp-publisher findes ikke lokalt; henter den engangs" + npx -y @modelcontextprotocol/publisher login github || { echo " login til registret fejlede"; faldt=1; } npx -y @modelcontextprotocol/publisher publish || { echo " registret afviste"; faldt=1; } fi -echo -n " registret siger nu: " +echo -n " registret siger for VORES server ($VENTET ventet): " +# ⛔ ASTRA (2/10): søgningen kan matche en FREMMED server ved navn-overlap - +# maalt med et syntetisk svar der gav en anden servers 9.9.9. Kraev praecis +# navn, og sammenlign med den version vi faktisk lige udgav. curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=computer-mcp" \ - | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((e['server']['version'] for e in d.get('servers',[]) if e.get('_meta',{}).get('io.modelcontextprotocol.registry/official',{}).get('isLatest')),'(ikke fundet)'))" + | python3 -c " +import json, sys +NAVN = 'io.github.Agent360dk/computer-mcp' +VENTET = '$VENTET' +d = json.load(sys.stdin) +fundet = [e for e in d.get('servers', []) + if e.get('server', {}).get('name') == NAVN + and e.get('_meta', {}).get('io.modelcontextprotocol.registry/official', {}).get('isLatest')] +if not fundet: + print('(ikke fundet under det praecise navn)'); sys.exit(1) +v = fundet[0]['server']['version'] +print(v) +sys.exit(0 if v == VENTET else 1) +" || faldt=1 sig "2 · forbeholdene sletter sig selv" echo "$VENTET" > PUBLICERET @@ -60,10 +80,19 @@ sig "5 · i hus" git add PUBLICERET README.md docs/ server.json 2>/dev/null || true git status --short | sed 's/^/ /' echo -echo " Naeste skridt er i haanden, med vilje:" -echo " git commit -F - <<'M'" -echo " udgivet: $VENTET staar paa npm, forbeholdene er vaek" -echo " M" -echo " git push origin main" +# ⛔ OPUS (2/10): main er laast (PR kraeves, enforce_admins=true) - en direkte +# "git push origin main" afvises altid, ogsaa for en administrator. +if [ -n "$(git status --porcelain -- PUBLICERET README.md docs/ server.json 2>/dev/null)" ]; then + GREN="udgivelse-$VENTET-dok" + echo " Naeste skridt er i haanden, med vilje (main er laast - en direkte push bliver afvist):" + echo " git checkout -b $GREN" + echo " git commit -F - <<'M'" + echo " udgivet: $VENTET staar paa npm, forbeholdene er vaek" + echo " M" + echo " git push origin $GREN" + echo " gh pr create --base main --head $GREN --title \"udgivet: $VENTET - forbeholdene vaek\" --body \"npm og registret bekraeftet paa $VENTET; PUBLICERET og de afledte sider er synkroniseret af sync-tal.py.\"" +else + echo " intet at committe - PUBLICERET og siderne stod allerede rigtigt." +fi [ "$faldt" -eq 0 ] && sig "ALT GROENT" || { sig "NOGET FALDT - se ovenfor"; exit 1; }