From 47c80f2bffea15c0e24200700e4caa59653ad47b Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Mon, 28 Sep 2026 16:52:55 +0800 Subject: [PATCH 01/72] =?UTF-8?q?fix:=20byg=20paa=20macos-15,=20aerlige=20?= =?UTF-8?q?forgrunds-loefter,=20fremmed-mac=20som=20=C3=A6gte=20port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci/release/udgiv-npm: macos-14 -> macos-15 (SCRecordingOutput mangler i 14-SDK, CI rød på main) - README + vejledning: fjern 'nothing comes to the front'; sig at forgrunden gives tilbage - menu-needs-front: 'may be' i stedet for påstået årsag; peg ikke paa en vej der afvises bagfra - brugsscenarier: skærm taget (selv med tilbagegivning) = 'delvist', ikke 'bevist' (MANDAT) - fremmed-mac: samlende port fejler jobbet hvis suite/mutant/scenarier ikke bestod Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 2 +- .github/workflows/fremmed-mac.yml | 22 ++++++++++++++++++++++ .github/workflows/release.yml | 2 +- .github/workflows/udgiv-npm.yml | 2 +- README.md | 5 +++-- mcp-server/index.js | 9 +++++---- test/brugsscenarier.mjs | 5 ++++- 7 files changed, 37 insertions(+), 10 deletions(-) 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..9177a13 100644 --- a/.github/workflows/fremmed-mac.yml +++ b/.github/workflows/fremmed-mac.yml @@ -59,6 +59,7 @@ jobs: mcp-server/vendor/cmcp-helper focused || true - name: Hele suiten + id: suite continue-on-error: true run: | export TMPDIR="$RUNNER_TEMP/" @@ -69,6 +70,7 @@ 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 @@ -83,6 +85,7 @@ 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 @@ -98,6 +101,7 @@ 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 @@ -115,3 +119,21 @@ jobs: ${{ runner.temp }}/baggrund.json ${{ runner.temp }}/forgrund.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 }}"; do + navn="${par%%:*}"; res="${par##*:}" + echo "$navn: $res" + [ "$res" = "success" ] || { echo "⛔ $navn bestod ikke ($res)"; fail=1; } + done + [ "$fail" = "0" ] && echo "Alle fire 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..b96ac77 100644 --- a/.github/workflows/udgiv-npm.yml +++ b/.github/workflows/udgiv-npm.yml @@ -47,7 +47,7 @@ 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 steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index e0eb672..ebe09e0 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ in the hours you are not watching. - **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. diff --git a/mcp-server/index.js b/mcp-server/index.js index d080980..475ff1e 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -176,10 +176,11 @@ When a step did nothing - the next route (measured 27-28 Sep): - \`computer_press\` said ok and nothing changed (a sidebar in App Store or System Settings): the app ignores presses there from behind. Find the same action in its menus with \`computer_menus\` - System Settings has every page under View. -- A menu item is greyed out, code \`menu-needs-front\`: the app only allows it - while it is in front. Ask the person to bring it forward, then call again. +- A menu item is greyed out, code \`menu-needs-front\`: this may be because the + app only enables it while it is in front - which cannot be done from behind. + Tell the person and let them bring it forward; do not keep pressing it. - The app runs but has no window: \`computer_launch\` with \`background: true\` - asks it to show one, without bringing it forward. + asks it to show one; if the app pulls itself forward the front is handed back. - Nothing is found in an app you just saw: check \`computer_windows\` - the window may have been closed. - Whichever it was, write it down with \`computer_learning\`: the route that @@ -1075,7 +1076,7 @@ async function haandterKald(request) { `so the person would see it happen.\n\n` + `Set \`${MANGLER_FOR_STILLE[name]}\` and call it again. ` + `The event then goes into that app's own queue: the pointer stays where ` + - `the person left it, nothing comes to the front, and it works on a window ` + + `the person left it, this call pulls nothing to the front, and it works on a window ` + `behind the one they are in.\n` + `Use computer_apps or computer_windows if you are unsure of the name.` ); diff --git a/test/brugsscenarier.mjs b/test/brugsscenarier.mjs index 689ce4d..4559f9d 100644 --- a/test/brugsscenarier.mjs +++ b/test/brugsscenarier.mjs @@ -484,7 +484,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'; From dcf361ab2dc89358bc5e54746ef5aa6a1acc98b0 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Mon, 28 Sep 2026 18:56:11 +0800 Subject: [PATCH 02/72] =?UTF-8?q?vejledning:=20sp=C3=B8rg=20n=C3=A5r=20du?= =?UTF-8?q?=20er=20blokeret,=20giv=20ikke=20op=20-=20tre=20ting=20er=20men?= =?UTF-8?q?neskets=20at=20give?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gustav 28/9: modellen skal vide den kan alt et menneske kan, og enten finde vejen eller spørge (computer_ask_user) frem for at stoppe. Login/kodeord, afsendelse til et rigtigt menneske, og forgrund er menneskets; aldrig taste kodeord, aldrig omgå en afvisning. Co-Authored-By: Claude Opus 4.8 --- mcp-server/index.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/mcp-server/index.js b/mcp-server/index.js index 475ff1e..05b562e 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -194,9 +194,16 @@ What you will be refused, and why: - an unredacted screenshot: that is the person's decision, never the model's - the Computer MCP status icon itself: it is their control surface -When something needs a human, the question waits in the menu bar icon and the -person answers it with Touch ID. You are told the action did not happen; call -it again after they approve. \`computer_pending\` lists what is waiting. +When something needs a human, do not give up - ask. This tool can drive anything +on the Mac a person can reach by hand; three things stay theirs to give: a login +or password, the go-ahead to send a message to a real person, and bringing an app +to the front for something that only works there. When you need one of these, call +\`computer_ask_user\`: the question waits in the menu bar icon and the person answers +with Touch ID and types any secret themselves - you never type a password and never +see it. You are told the action did not happen; call it again after they approve. +\`computer_pending\` lists what is waiting. Handing back "I can't" before you have +asked is the one wrong move; when in doubt whether you may do something, ask whether +to do it - and never work around a refusal. Reading: \`computer_inspect\` answers as text by default. If it says INCOMPLETE or [cut: ...], the answer is PART of the tree - narrow it with \`computer_find\` From 6a5868f846b856f2a88a8ebeb27a2082de01a0da Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Mon, 28 Sep 2026 19:25:29 +0800 Subject: [PATCH 03/72] docs: regenerer npm-README efter forgrunds-loefte-rettelsen (holder claim 45 groen) Co-Authored-By: Claude Opus 5.5 --- mcp-server/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mcp-server/README.md b/mcp-server/README.md index 71f8658..fa68dd9 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -7,8 +7,9 @@ in the hours you are not watching. - **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. From d1c5cfe8a7c38c0c9a77ce3ba970a4b9609710fa Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Mon, 28 Sep 2026 20:41:51 +0800 Subject: [PATCH 04/72] haerdning M1: programlaasen fejler LUKKET ved infra-fejl (ikke aaben) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fandt af batch-2-hærdningen (29 bekræftede huller): programlaas.js:45 kørte handlingen alligevel ved enhver ikke-EEXIST låsefejl (ENOTDIR/EACCES). To agenter kunne skrive i flæng i samme program - netop det låsen skal forhindre. Nu: kan låsen ikke tages, sker handlingen ikke, med en ærlig grund (infra, ikke contention). Prøve test/laas-fejler-lukket.mjs tvinger ENOTDIR frem; revert af M1 gør den rød. Co-Authored-By: Claude Opus 5.5 --- mcp-server/index.js | 6 ++++-- mcp-server/programlaas.js | 6 +++++- test/laas-fejler-lukket.mjs | 40 +++++++++++++++++++++++++++++++++++++ test/run-all.sh | 1 + 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 test/laas-fejler-lukket.mjs diff --git a/mcp-server/index.js b/mcp-server/index.js index 05b562e..97ff059 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -1288,9 +1288,11 @@ async function haandterKald(request) { return errorResult(`Refused: ${stopgrund}. Nothing was done. Call it again.`); } if (!laast.ok) { - const grund = `another agent is working in ${targetBundleId || 'the foreground app'} right now`; + // laast.grund er sat naar laasen fejlede LUKKET paa en infra-fejl (M1); + // ellers er det ventetiden der udloeb (en anden agent holdt laasen). + const grund = laast.grund || `another agent is working in ${targetBundleId || 'the foreground app'} right now, and it did not finish within a minute`; record({ tool: name, outcome: 'refused', reason: grund }); - return errorResult(`Refused: ${grund}, and it did not finish within a minute. Nothing was done. Try again shortly.`); + return errorResult(`Refused: ${grund}. Nothing was done. Try again shortly.`); } result = laast.vaerdi; } diff --git a/mcp-server/programlaas.js b/mcp-server/programlaas.js index b12aee1..6df83fb 100644 --- a/mcp-server/programlaas.js +++ b/mcp-server/programlaas.js @@ -42,7 +42,11 @@ export async function medProgramLaas(program, fn, maksVentMs = 60_000) { try { writeSync(fd, String(process.pid)); } finally { closeSync(fd); } break; } catch (err) { - if (err.code !== 'EEXIST') return { ok: true, vaerdi: await fn() }; // kan ikke laase: gaa videre som foer + // ⛔ 28/9 (haerdning M1): FOER kørte vi fn() alligevel her (fail-open) - en + // laase-infra-fejl (ENOTDIR hvis laase-mappen bliver en fil, EACCES m.fl.) + // lod to agenter skrive i flaeng, netop dét laasen skal forhindre. Nu + // fejler laasen LUKKET: kan handlingen ikke serialiseres, sker den ikke. + if (err.code !== 'EEXIST') return { ok: false, grund: `the per-app lock could not be taken (${err.code || 'unknown error'}); an action that cannot be serialised does not happen` }; if (!ejerLever(sti)) { try { unlinkSync(sti); } catch {} continue; } if (Date.now() - start > maksVentMs) return { ok: false }; await sov(40); diff --git a/test/laas-fejler-lukket.mjs b/test/laas-fejler-lukket.mjs new file mode 100644 index 0000000..1533256 --- /dev/null +++ b/test/laas-fejler-lukket.mjs @@ -0,0 +1,40 @@ +// 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 { 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/run-all.sh b/test/run-all.sh index df3e1ed..2b36940 100755 --- a/test/run-all.sh +++ b/test/run-all.sh @@ -92,6 +92,7 @@ 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 "fejlbeskeder" "node test/errors.mjs" run "flere agenter" "node test/concurrent.mjs" From b5d0953d3f1b39d7fbdd73666347298a0580df86 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Mon, 28 Sep 2026 23:44:19 +0800 Subject: [PATCH 05/72] haerdning M4: giv aldrig forgrunden fra et menneske der lige skiftede MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fandt af batch-2-review'et: givTilbage() gav forgrunden tilbage naar maalprogrammet kom frem UANSET nyligt menneske-input - klikkede mennesket selv over i dét program, rev vi det tilbage. Nu gaelder menneske-tjekket i alle grene, og et forgrunds-skift vi lader staa rapporteres aerligt (took_screen:true, gave_back:false) i stedet for et tavst false. Paa en maskine uden menneske (køreren) er adfaerden uaendret, saa giv-tilbage-mutanten verificerer fortsat. Co-Authored-By: Claude Opus 4.8 --- helper/Sources/cmcp-helper/Skaerm.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/helper/Sources/cmcp-helper/Skaerm.swift b/helper/Sources/cmcp-helper/Skaerm.swift index 63b7609..470eece 100644 --- a/helper/Sources/cmcp-helper/Skaerm.swift +++ b/helper/Sources/cmcp-helper/Skaerm.swift @@ -187,10 +187,19 @@ enum Skaerm { // trykker en tast eller klikker; et program der skubber sig frem, goer // ingen af delene. Uden menneskelig input i 1,5 s var det ikke mennesket. let maal = tilPid() - guard maal != foer.forrestPid, efter.forrestPid == maal || !menneskeRoerteNetop() else { - return ["took_screen": false, + // Maalet er der mennesket allerede var: intet skift at give tilbage. + if maal == foer.forrestPid { return ["took_screen": false] } + // ⛔ 28/9 (haerdning M4): FOER stod `efter.forrestPid == maal || !menneskeRoerteNetop()` + // - naar maalprogrammet kom frem, blev forgrunden givet tilbage UANSET om + // mennesket lige havde klikket/tastet. Klikkede mennesket selv over i netop + // dét program, rev vi det tilbage under haenderne paa dem. Nu gaelder + // menneske-tjekket i ALLE grene: har nogen roert tastatur eller mus lige + // foer, saa lader vi forgrunden staa - og siger aerligt at skaermen skiftede, + // i stedet for et tavst took_screen:false. + if menneskeRoerteNetop() { + return ["took_screen": true, "gave_back": false, "observed": ["frontmost_changed_to": efter.forrestNavn, - "note": "someone pressed a key or clicked just before, so this is taken to be the person switching - left alone"]] + "note": "a person clicked or typed just before, so this is taken to be them and the front was left with \(efter.forrestNavn)"]] } NSRunningApplication(processIdentifier: foer.forrestPid)?.activate(options: []) var tilbage = false From 4f9c98c3cdbb8fdc06b85963114750c6e3cff76d Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Tue, 29 Sep 2026 00:01:58 +0800 Subject: [PATCH 06/72] test(mutant): opdater M4-mutanten til den nye give-tilbage-kode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M4-ændringen fjernede den linje den gamle mutant patchede (efter.forrestPid==maal || !menneskeRoerteNetop()), saa mutanten kunne ikke anvendes og 'overlevede' falsk. Ny mutant: faa menneske-tjekket til altid at fyre -> forgrunden gives aldrig tilbage -> proeven bliver roed. Beviser M4's invariant paa en maskine uden menneske. Co-Authored-By: Claude Opus 5.5 --- .github/mutant-giv-tilbage.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/mutant-giv-tilbage.sh b/.github/mutant-giv-tilbage.sh index a181830..f89daf1 100644 --- a/.github/mutant-giv-tilbage.sh +++ b/.github/mutant-giv-tilbage.sh @@ -27,6 +27,9 @@ mut M1-giver-ikke-tilbage 'NSRunningApplication(processIdentifier: foer.forrestP 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' +# 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. +mut M4-menneske-tjek-blokerer 'if menneskeRoerteNetop() {' 'if true {' exit $fejl From c06305810731299382c49d9dba2f4ff289b96112 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Tue, 29 Sep 2026 00:29:33 +0800 Subject: [PATCH 07/72] doer (slice 1): computer_open - aabn en apps egen indgang bagfra, sikkert Baggrunds-doeren fra batch-2-spec'en, foerste skive. Modellen giver et intent (open_app/play_track/open_chat) + EN typet parameter, ALDRIG en URL - serveren bygger en fast app-URL og validerer parameteren strengt (kun cifre/bogstaver). Helperen aabner den med NSWorkspace .withoutActivation og dobbelt-tjekker mod en haardkodet scheme-allowlist (spotify/whatsapp/claude). Doeren navngiver sit maal via intent'et, saa porten ikke kalder den 'ukendt maal'. Baerer navigation, ALDRIG et send. test/doer.mjs (8 tjek) beviser: intet url-felt at injicere i, bad id/phone/bundle/intent afvist, praecis app-URL bygget. Swift-open-url + funktionen verificeres paa hhv. koereren og Gustavs Mac. Co-Authored-By: Claude Opus 4.8 --- helper/Sources/cmcp-helper/main.swift | 26 ++++++++++ mcp-server/index.js | 36 +++++++++++++ mcp-server/tools.js | 16 ++++++ test/doer.mjs | 75 +++++++++++++++++++++++++++ test/run-all.sh | 1 + 5 files changed, 154 insertions(+) create mode 100644 test/doer.mjs diff --git a/helper/Sources/cmcp-helper/main.swift b/helper/Sources/cmcp-helper/main.swift index 3ae9699..899ac87 100644 --- a/helper/Sources/cmcp-helper/main.swift +++ b/helper/Sources/cmcp-helper/main.swift @@ -277,6 +277,32 @@ case "launch": } Out.ok(ls) +case "open-url": + // Doeren: aabn en app's EGEN indgang bagfra (spotify:track:, whatsapp://chat) + // uden at aktivere. Serveren bygger URL'en af en fast skabelon og validerer + // hver parameter; helperen dobbelt-tjekker mod en HAARDKODET scheme-allowlist, + // saa selv en fejl i serveren aldrig kan aabne file:/shortcuts:/osascript. + guard let raw = args.str("url"), let url = URL(string: raw), let scheme = url.scheme?.lowercased() else { + Out.fail("--url is missing or is not a valid URL", code: "bad-args") + } + let tilladteSchemes: Set = ["spotify", "whatsapp", "claude"] + guard tilladteSchemes.contains(scheme) else { + Out.fail("the scheme '\(scheme)' is not one of this door's app schemes (\(tilladteSchemes.sorted().joined(separator: ", ")))", code: "scheme-not-allowed") + } + let foerU = Skaerm.stand() + let cfg = NSWorkspace.OpenConfiguration() + cfg.activates = false + let sem = DispatchSemaphore(value: 0) + var aabenFejl: Error? = nil + NSWorkspace.shared.open(url, configuration: cfg) { _, error in aabenFejl = error; sem.signal() } + _ = sem.wait(timeout: .now() + 10) + if let e = aabenFejl { Out.fail("opening the \(scheme) door failed: \(e.localizedDescription)", code: "open-failed") } + var uu: [String: Any] = ["opened_scheme": scheme] + // Kan hente sig selv frem trods activates:false - giv forgrunden straks tilbage. + let gu = Skaerm.givTilbage(foer: foerU, tilPid: { NSWorkspace.shared.frontmostApplication?.processIdentifier }, ventMs: 4000) + for (k, v) in gu { uu[k] = v } + Out.ok(uu) + case "quit": guard let hvad = args.str("app") else { Out.fail("--app is missing", code: "bad-args") } // ⛔ Den her maaling er ikke pynt. Et program med ugemt arbejde svarer paa diff --git a/mcp-server/index.js b/mcp-server/index.js index 97ff059..40d8379 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -502,6 +502,32 @@ async function runTool(name, args) { ...(args.background ? ['--background'] : [])]); return medSkaerm(textResult(r), r); } + case 'computer_open': { + // Doeren. Modellen giver et intent + EN parameter, aldrig en URL. Serveren + // bygger URL'en af en fast skabelon og validerer parameteren strengt, saa + // KUN cifre/bogstaver kan passere - ingen injektion, ingen fri scheme. + const intent = String(args.intent || ''); + if (intent === 'open_app') { + const bid = String(args.bundleId || ''); + if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$/.test(bid)) return errorResult('Refused: open_app needs a valid bundle id like com.spotify.client. Nothing was done.'); + const r = await callHelper(['launch', '--app', bid, '--background']); + return medSkaerm(textResult(r), r); + } + let url; + if (intent === 'play_track') { + const id = String(args.spotifyId || ''); + if (!/^[0-9A-Za-z]{22}$/.test(id)) return errorResult('Refused: play_track needs a 22-character Spotify track id (letters and digits only). Nothing was done.'); + url = `spotify:track:${id}`; + } else if (intent === 'open_chat') { + const phone = String(args.phone || '').replace(/[\s()-]/g, ''); + if (!/^\+?\d{4,15}$/.test(phone)) return errorResult('Refused: open_chat needs a phone number in international form, e.g. +4560174569. Nothing was done.'); + url = `whatsapp://send?phone=${encodeURIComponent(phone.replace(/^\+/, ''))}`; + } else { + return errorResult(`Refused: unknown intent '${intent}'. Use open_app, play_track or open_chat. Nothing was done.`); + } + const r = await callHelper(['open-url', '--url', url]); + return medSkaerm(textResult(r), r); + } case 'computer_quit': { // ⛔ 23/9: svaret baar intet `took_screen`, saa loggen kunne ikke goere // det op for den her vej. Et program der rejser et «vil du gemme?»-ark @@ -849,6 +875,16 @@ async function haandterKald(request) { targetBundleId = args.app ? await resolveBundleId(args.app) : await frontmostBundleId(); + // Doeren navngiver sit maal via intent'et (ikke via args.app), saa porten + // ser den rigtige app - ikke det der tilfaeldigvis er forrest - og ikke + // kalder den "ukendt maal" og spoerger. Et ugyldigt intent giver null, som + // saa afvises af porten, praecis som det skal. + if (name === 'computer_open') { + targetBundleId = args.intent === 'open_app' ? (args.bundleId ? String(args.bundleId) : null) + : args.intent === 'play_track' ? 'com.spotify.client' + : args.intent === 'open_chat' ? 'net.whatsapp.WhatsApp' + : null; + } // ⛔ FABLE 24/9: et LUKKET program findes ikke blandt de koerende, saa // `computer_launch` blev altid «ukendt maal» og afvist - vaerktoejet // kunne ikke det ene det er til. Hjaelperen svarer nu med det bundle-id diff --git a/mcp-server/tools.js b/mcp-server/tools.js index 7daabfb..0837498 100644 --- a/mcp-server/tools.js +++ b/mcp-server/tools.js @@ -46,6 +46,22 @@ export const TOOLS = [ required: ['app'] } }, + { + name: 'computer_open', + tier: TIER.WRITE, + description: "Open an app's OWN door in the background - a Spotify track, a WhatsApp chat, or the app itself - without bringing it to the front. This is the way to reach an app whose window is covered, or that will not act from behind: it uses the app's own URL, not its window. It carries navigation ONLY, never a send - to send a message you still act on the open chat and the person confirms it. You give an `intent` and its one parameter, never a URL: the server builds a fixed, validated URL for that intent, and file:, shortcuts:, osascript and anything else are refused.", + inputSchema: { + type: 'object', + required: ['intent'], + properties: { + intent: { type: 'string', enum: ['open_app', 'play_track', 'open_chat'], + description: 'open_app: bring an app up in the background (needs bundleId). play_track: start a Spotify track (needs spotifyId, 22 chars). open_chat: open a WhatsApp chat to a phone number with NO pre-filled text (needs phone).' }, + bundleId: { type: 'string', description: 'For open_app: the app, e.g. com.spotify.client.' }, + spotifyId: { type: 'string', description: 'For play_track: the 22-character Spotify track id.' }, + phone: { type: 'string', description: 'For open_chat: the phone number in international form, e.g. +4560174569.' } + } + } + }, { name: 'computer_quit', tier: TIER.WRITE, diff --git a/test/doer.mjs b/test/doer.mjs new file mode 100644 index 0000000..2ae161b --- /dev/null +++ b/test/doer.mjs @@ -0,0 +1,75 @@ +// 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 { 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'; + +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')], + { env: { ...process.env, CMCP_HELPER: fakeHelper, CMCP_MODE: 'allow', CMCP_STATUS_IKON: '0', 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/run-all.sh b/test/run-all.sh index 2b36940..05cf007 100755 --- a/test/run-all.sh +++ b/test/run-all.sh @@ -93,6 +93,7 @@ run "paastande" "node test/claims.mjs" # 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 "fejlbeskeder" "node test/errors.mjs" run "flere agenter" "node test/concurrent.mjs" From 35fbb6af4382f313b96a0de7428b811e13db48a0 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Tue, 29 Sep 2026 01:38:26 +0800 Subject: [PATCH 08/72] doer (slice 1) doc-sync: computer_open paa alle flader, tal 30->31 Tilfoej computer_open til vaerktoejslisterne (README, llms.txt, tools.html, capability-matrix), bump tool-tallet 30->31 (14 laesende, 17 skrivende) paa alle tekstflader + gemini-extension.json. Udvid ORD-ordlisten i baade sync-tal.py og claims.mjs forbi 'thirty' (crashede/parsede undefined ved 31). Ret to test-hygiejne- fund: doer.mjs saetter fake osascript (vagt 26), og open-url's fejltekst hejser join'et ud i en variabel saa lintens streng-udtraekker ikke eksponerer et dansk variabelnavn (vagt 27). claims.mjs groen. Ingen kode-logik roert. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +++--- docs/404.html | 2 +- docs/blog/driving-macos-from-an-ai-agent.html | 2 +- docs/docs/capability-matrix/index.html | 8 ++++---- docs/docs/install-claude-code/index.html | 6 +++--- docs/docs/install-claude-desktop/index.html | 6 +++--- docs/docs/install-codex/index.html | 6 +++--- docs/docs/install-copilot/index.html | 6 +++--- docs/docs/install-cursor/index.html | 6 +++--- docs/docs/install-vscode/index.html | 6 +++--- docs/index.html | 10 +++++----- docs/learn/computer-use-mcp/index.html | 2 +- docs/learn/owasp-mcp-top-10/index.html | 2 +- docs/llms-install.md | 4 ++-- docs/llms.txt | 6 +++--- docs/tools.html | 10 +++++++--- docs/use-cases/unattended-runs/index.html | 2 +- .../without-taking-over-your-screen/index.html | 12 ++++++------ gemini-extension.json | 2 +- helper/Sources/cmcp-helper/main.swift | 3 ++- mcp-server/README.md | 6 +++--- scripts/sync-tal.py | 4 +++- test/claims.mjs | 4 +++- test/doer.mjs | 4 +++- 24 files changed, 68 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index ebe09e0..c87de2e 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ it refuses. ## Tools -**30 tools: fourteen that look, sixteen that touch.** Twenty are offered by +**31 tools: fourteen that look, seventeen that touch.** Twenty 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: @@ -208,12 +208,12 @@ 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. +> **What you get today, honestly.** `npx @agent360/computer-mcp` currently serves **0.1.0**, which has 12 tools. The 31 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_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` diff --git a/docs/404.html b/docs/404.html index 9f015b8..b66f7e9 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 31 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..67bdb16 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-one 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..0ecc652 100644 --- a/docs/docs/capability-matrix/index.html +++ b/docs/docs/capability-matrix/index.html @@ -8,7 +8,7 @@ - + @@ -119,7 +119,7 @@

Walls we clear partly, and say so

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: 23 of the 31 are offered in background, nine 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. 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. @@ -147,10 +147,10 @@

Not yet, and honestly named

Windows or Linuxwill notmacOS only, and none planned. The accessibility model is the product. -

The thirty tools

+

The thirty-one 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 (17)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..a4b4d7b 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 31: 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 seventeen 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 31 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..be672a5 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 31: 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 +seventeen 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 31 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..a22e8f4 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 31: 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 seventeen 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 31 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..3deb78e 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 31: 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 +seventeen 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 31 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..7f6a7dd 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 31: 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 seventeen 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 31 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..ffd8991 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 31: 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 +seventeen 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 31 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..a16ac7b 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,8 @@

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 31 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-three of the thirty-one, 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.

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

No API key, no se
-

thirty tools

-

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

thirty-one tools

+

fourteen that look, seventeen 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"]}
diff --git a/docs/learn/computer-use-mcp/index.html b/docs/learn/computer-use-mcp/index.html index f3520aa..b9f8317 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 31 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..09aadd7 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: 31 tools in full mode, 11 in read-only.
So it can...What that means
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..4560da1 100644 --- a/docs/llms-install.md +++ b/docs/llms-install.md @@ -9,11 +9,11 @@ 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 **31 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. +> **What you get today, honestly.** `npx @agent360/computer-mcp` currently serves **0.1.0**, which has 12 tools. The 31 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..3093101 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -6,7 +6,7 @@ > 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 31 tools described below 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) @@ -52,7 +52,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 31 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 +61,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_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..db4556b 100644 --- a/docs/tools.html +++ b/docs/tools.html @@ -4,7 +4,7 @@ Tool reference - Computer MCP - + @@ -71,10 +71,10 @@

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-one tools. fourteen look, seventeen 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 31 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.

@@ -112,6 +112,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..871cfe2 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..3cfc6ae 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() From f22496a52e66c884da4910b5bdbc200a4a1e1243 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 14:30:18 +0800 Subject: [PATCH 59/72] =?UTF-8?q?sikkerhed:=20et=20blindt=20koordinat-klik?= =?UTF-8?q?=20fejlede=20=C3=85BENT=20i=20stedet=20for=20lukket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fundet af Opus i et konsulentpanel om canvas-blinde apps (2/10): knapErFarlig() behandlede "intet fundet på punktet", "fundet uden navn/beskrivelse" og "opslaget fejlede" som IKKE farligt for computer_click - selvom et koordinat-klik rammer skærmen uanset om opslaget her lykkes. Et "Delete" i en canvas-tegnet dialog, et fjernskrivebord, eller ethvert punkt serveren ikke kan identificere, kunne rammes uden at nogen blev spurgt - stik imod README's løfte om at alt der sletter eller rydder spørger hver gang. Den oprindelige begrundelse ("et tryk der ikke kan slås op, finder heller ikke noget at trykke på") holder for computer_press, som ikke kan udføres uden et fundet element - men ikke for et klik. De tre tilfælde fejler nu LUKKET (spørg) for klik, mens press er uændret (fejler stadig åbent, med rette). test/knap-ord.mjs udvidet med tre nye tjek (intet fundet, intet navn, opslaget fejler) + ny kalibrering. Mutant Q26 i .github/mutanter-porte.json. Begge bekræftet manuelt: reverteres fixet, går præcis de tre nye tjek røde. --- .github/mutanter-porte.json | 12 ++++++++++++ mcp-server/index.js | 27 ++++++++++++++++++++------- test/knap-ord.mjs | 19 ++++++++++++++++++- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/.github/mutanter-porte.json b/.github/mutanter-porte.json index 6ee5aa9..9f61c51 100644 --- a/.github/mutanter-porte.json +++ b/.github/mutanter-porte.json @@ -850,5 +850,17 @@ " 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;" + ] + ] } ] diff --git a/mcp-server/index.js b/mcp-server/index.js index 0241ee5..46a44b1 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -470,19 +470,32 @@ async function menuGenvejErFarlig(app, path) { /// «Erase», trykket med computer_press eller ramt af et klik, spurgte aldrig. /// Samme ord som menuen. Kendes elementets navn ikke, kan det ikke doemmes herfra: /// et tryk der ikke kan slaas op, finder heller ikke noget at trykke paa. +/// ⛔ 2/10 (Opus-panelet, canvas-spørgsmålet): den sætning gælder KUN press - et +/// tryk der ikke kan slås op, rammer intet, så at fejle åbent er harmløst der. +/// Et KOORDINAT-KLIK rammer skærmen uanset om opslaget her lykkes. Før stod +/// "intet fundet"/"intet navn"/en fejl alle som `false` (ikke farligt) - netop +/// de tre tilstande hvor serveren IKKE ved hvad der klikkes på (en canvas-tegnet +/// dialog, et fjernskrivebord, et navnløst element). README's løfte holdt ikke +/// på den vej: et "Delete" kunne rammes blindt uden at nogen blev spurgt. Et +/// punkt vi ikke kan identificere, er nu farligt - spørg, fail CLOSED. async function knapErFarlig(name, args) { - try { - if (name === 'computer_press') { + if (name === 'computer_press') { + try { const { a, soeg } = trykArgv(args); const d = await callHelper([...a, '--dry'], { stdin: JSON.stringify(soeg), timeout: 15000 }); const el = d?.would_press; return !!el && [el.name, ...(el.names || []), el.title].filter(Boolean).some(n => menuSerFarlig(n)); - } - if (name === 'computer_click' && Number.isFinite(args.x) && Number.isFinite(args.y)) { + } catch { return false; } + } + if (name === 'computer_click' && Number.isFinite(args.x) && Number.isFinite(args.y)) { + try { const d = await callHelper(['at', '--x', String(args.x), '--y', String(args.y)], { timeout: 8000 }); - return !!d?.found && [d.title, d.description].filter(Boolean).some(n => menuSerFarlig(n)); - } - } catch {} + if (!d?.found) return true; + const navne = [d.title, d.description].filter(Boolean); + if (!navne.length) return true; + return navne.some(n => menuSerFarlig(n)); + } catch { return true; } + } return false; } diff --git a/test/knap-ord.mjs b/test/knap-ord.mjs index bcb858b..7253532 100644 --- a/test/knap-ord.mjs +++ b/test/knap-ord.mjs @@ -64,7 +64,24 @@ 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); -check('5 kalibrering: porten spurgte et menneske om 1, 2 og 4', spoerger.gangeSpurgt() === 3, `spurgt ${spoerger.gangeSpurgt()} gange`); +// ⛔ 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); + +check('9 kalibrering: porten spurgte et menneske om 1, 2, 4, 6, 7 og 8', spoerger.gangeSpurgt() === 6, `spurgt ${spoerger.gangeSpurgt()} gange`); srv.kill(); console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); process.exit(fails.length ? 1 : 0); From a3483c54e97deedd1fffcb8b62ab8d3f8d6df471 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 14:39:42 +0800 Subject: [PATCH 60/72] =?UTF-8?q?D3:=20f=C3=B8lg-panelets=20"Show=20me=20w?= =?UTF-8?q?here"=20-=20peger=20p=C3=A5=20agentens=20m=C3=A5l=20uden=20selv?= =?UTF-8?q?=20at=20tage=20fokus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Følg-panelet (klik på en session i menuen) viste kun løbende tekst om hvad agenten gør. D3 bad om mere: "panelet peger på det program/vindue agenten arbejder i, uden at tage forgrunden af sig selv - menneskets klik er det eneste der flytter fokus". status.js: statusHandling() tager nu en valgfri tredje parameter - den raa app-streng (bundle-id ELLER synligt navn) serveren allerede har i hånden fra `args.app`. Slås IKKE op her (resolveApp ville lægge et ekstra helper-opslag på HVER handling bare for denne knap) - ikonet forsøger selv begge former, når mennesket klikker. main.swift: Post fik et `target`-felt. LivePanel fik en "Show me where"-knap, samme princip som den eksisterende "Take me there" på et samtykke (hentFrem): MENNESKETS klik aktiverer appen, agenten rører aldrig forgrunden selv. Knappen læser `senesteMaal()` - det igangværende mål, eller ellers det sidst FÆRDIGE (uden faldbagget ville den næsten aldrig være aktiv, fordi `now` kun er sat de få millisekunder en handling rent faktisk kører). --dump fik et nyt `nowTarget`-felt, så test/status-ikon.mjs kan bevise at målet når helt frem uden en levende GUI (D3's prøve-halvdel). Knappens EGEN aktivering (klik → appen kommer frem) kræver stadig film på GitHubs Mac, som D1-D4 alle gør. Verificeret lokalt: swift build ren, scripts/build-release.sh kørt (vendor- binær opdateret, gitignored), test/status-ikon.mjs + test/ikon-menu.mjs begge grønne mod den friske binær. --- helper/Sources/cmcp-status/main.swift | 54 +++++++++++++++++++++++++-- mcp-server/index.js | 4 +- mcp-server/status.js | 10 ++++- test/status-ikon.mjs | 9 +++++ 4 files changed, 71 insertions(+), 6 deletions(-) diff --git a/helper/Sources/cmcp-status/main.swift b/helper/Sources/cmcp-status/main.swift index d690787..7b3be87 100644 --- a/helper/Sources/cmcp-status/main.swift +++ b/helper/Sources/cmcp-status/main.swift @@ -21,7 +21,10 @@ import UserNotifications // MARK: - Model -struct Post: Codable { let ts: String; let text: String; let outcome: String } +// D3 (2/10): `target` er den raa app-streng (bundle-id ELLER synligt navn) serveren +// allerede havde i hånden - se status.js' statusHandling for hvorfor den ikke +// slås op dér. Mangler nøglen i ældre/andre poster, afkodes den som nil (Codable). +struct Post: Codable { let ts: String; let text: String; let outcome: String; let target: String? } struct Session: Codable { let session: String let pid: Int32 @@ -90,6 +93,15 @@ func overskrift(_ n: Int) -> String { } /// Live-vinduets tekst for én agent: nyeste oeverst. +/// D3 (2/10): det seneste KENDTE maal - den igangvaerende handling hvis der er +/// én, ellers den sidst FAERDIGE. `now` er kun sat de faa millisekunder en +/// handling rent faktisk koerer; uden faldbagget til `recent` ville "Show me +/// where" naesten aldrig vaere aktiv, fordi de fleste handlinger er for hurtige +/// til at nogen naar at se dem som "now". +func senesteMaal(_ s: Session) -> String? { + s.now?.target ?? s.recent.last?.target +} + func liveTekst(_ s: Session) -> String { var linjer = ["\(navn(s)) (pid \(s.pid), started \(siden(s.started)))", ""] if let n = s.now { linjer.append("NOW ▶︎ \(n.text)"); linjer.append("") } @@ -481,7 +493,13 @@ if CommandLine.arguments.contains("--dump") { "title": overskrift(s.count), "items": s.map(menuLinje), "live": s.map(liveTekst), - "sessions": s.map { $0.session } + "sessions": s.map { $0.session }, + // D3: maalet for den igangvaerende handling, saa en proeve kan maale at + // det naar helt frem til --dump uden en levende GUI. nil -> NSNull (JSON null). + "nowTarget": s.map { sess -> Any in + if let t = senesteMaal(sess) { return t } + return NSNull() + } ] let data = try! JSONSerialization.data(withJSONObject: ud, options: [.prettyPrinted, .sortedKeys]) FileHandle.standardOutput.write(data) @@ -506,8 +524,10 @@ startSocket() final class LivePanel: NSObject, NSWindowDelegate { let panel: NSPanel let tekst: NSTextView + let visKnap = NSButton(title: "Show me where", target: nil, action: nil) var session: String var lukket: () -> Void = {} + var maal: String? = nil // raa app-streng fra den igangvaerende handling (D3) init(session: String) { self.session = session @@ -521,7 +541,11 @@ final class LivePanel: NSObject, NSWindowDelegate { panel.hidesOnDeactivate = false panel.becomesKeyOnlyIfNeeded = true panel.isReleasedWhenClosed = false - let scroll = NSScrollView(frame: panel.contentView!.bounds) + // D3 (2/10): en knaprad nederst, resten scroller. MENNESKETS klik henter + // programmet frem - samme princip som "Take me there" paa et samtykke + // (hentFrem): agenten selv roerer aldrig forgrunden. + let hoejde = panel.contentView!.bounds.height + let scroll = NSScrollView(frame: NSRect(x: 0, y: 32, width: panel.contentView!.bounds.width, height: hoejde - 32)) scroll.autoresizingMask = [.width, .height] scroll.hasVerticalScroller = true tekst = NSTextView(frame: scroll.bounds) @@ -531,7 +555,15 @@ final class LivePanel: NSObject, NSWindowDelegate { tekst.autoresizingMask = [.width] scroll.documentView = tekst panel.contentView?.addSubview(scroll) + visKnap.frame = NSRect(x: 12, y: 6, width: 160, height: 22) + visKnap.bezelStyle = .rounded + visKnap.controlSize = .small + visKnap.autoresizingMask = [.maxXMargin] + visKnap.isEnabled = false + panel.contentView?.addSubview(visKnap) super.init() + visKnap.target = self + visKnap.action = #selector(visMigHvor) panel.delegate = self } @@ -548,11 +580,27 @@ final class LivePanel: NSObject, NSWindowDelegate { func opdater() { guard let s = laesSessioner().first(where: { $0.session == session }) else { panel.title = "Computer MCP — agent \(session) has stopped" + maal = nil; visKnap.isEnabled = false return } panel.title = "Computer MCP — \(navn(s))" let ny = liveTekst(s) if tekst.string != ny { tekst.string = ny } + maal = senesteMaal(s) + visKnap.isEnabled = maal != nil + } + + /// MENNESKETS klik - aldrig agentens. Proever bundle-id foerst (det + /// aegte format), falder tilbage til et synligt navn (det modellen kan + /// have skrevet i stedet). Finder den intet, sker der ingenting - ingen + /// fejlboks, ingen gaetten paa et andet program. + @objc func visMigHvor() { + guard let m = maal, !m.isEmpty else { return } + if let app = NSRunningApplication.runningApplications(withBundleIdentifier: m).first { + app.activate(options: []) + return + } + NSWorkspace.shared.runningApplications.first { $0.localizedName == m }?.activate(options: []) } func windowWillClose(_ n: Notification) { lukket() } diff --git a/mcp-server/index.js b/mcp-server/index.js index 46a44b1..8e317d5 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -1836,7 +1836,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { const a = request.params.arguments || {}; const klient = server.getClientVersion?.()?.name; if (klient) statusKlient(klient); - statusHandling(liveTekst(navn, a), 'running'); + // D3 (2/10): raa app-streng videre til foelg-panelets "Show me where"-knap - + // se status.js' statusHandling for hvorfor den IKKE sloeges op her. + statusHandling(liveTekst(navn, a), 'running', typeof a.app === 'string' ? a.app : null); let svar; try { svar = await iKald(() => haandterKald(request)); diff --git a/mcp-server/status.js b/mcp-server/status.js index 53d7b52..3fdef9d 100644 --- a/mcp-server/status.js +++ b/mcp-server/status.js @@ -70,9 +70,15 @@ export function statusStart({ session, client, version }) { } /// En handling er begyndt, eller afvist foer den begyndte. -export function statusHandling(tekst, udfald) { +/// `maal` (D3, 2/10): den raa app-streng mennesket/modellen navngav (bundle-id +/// ELLER synligt navn - begge forekommer i `args.app`). Sendes videre uprøvet: +/// at SLÅ den op (resolveApp) her ville lægge et ekstra helper-opslag på HVER +/// handling bare for at fodre følg-panelets knap. Ikonet forsøger selv begge +/// former, naar mennesket klikker "Show me where" - aldrig agenten selv. +export function statusHandling(tekst, udfald, maal = null) { if (!tilstand) return; - const post = { ts: new Date().toISOString(), text: String(tekst).slice(0, 160), outcome: udfald }; + const post = { ts: new Date().toISOString(), text: String(tekst).slice(0, 160), outcome: udfald, + target: maal ? String(maal).slice(0, 200) : null }; tilstand.recent.push(post); if (tilstand.recent.length > HISTORIK) tilstand.recent.shift(); tilstand.now = udfald === 'running' ? post : null; diff --git a/test/status-ikon.mjs b/test/status-ikon.mjs index 7f852f4..82dc8df 100644 --- a/test/status-ikon.mjs +++ b/test/status-ikon.mjs @@ -78,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`); From 391d9584e91f6e31ab0bf9ae80e5beab2f110935 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 14:43:43 +0800 Subject: [PATCH 61/72] =?UTF-8?q?vejledning:=20canvas-blinde=20apps=20rapp?= =?UTF-8?q?orterer=20tilbage,=20g=C3=A6tter=20ikke=20selv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opus-panelets punkt (b) (2/10, canvas-fallback-panelet): den eneste agent-guidede vej for en app der ikke udgiver noget til tilgængeligheds- træet (spil, plotte-værktøjer, fjernskrivebord) er nu at tage ÉT afgrænset skærmbillede, beskrive hvad man ser, og give trinnet til mennesket via computer_ask_user - ikke gætte koordinater selv. Ingen ny mekanik: samme værktøjer, bare en instruktion om hvornår de bruges. Panelets øvrige anbefalinger: (a) knapErFarlig fail-closed for blinde klik - rettet i f22496a. (c) "ret READMEs upræcise citat" - falsk alarm, den faktiske README.md:37-45 er allerede ordret præcis; fejlen var i min egen PAKKE.md-opsummering, ikke i produktet. --- mcp-server/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mcp-server/index.js b/mcp-server/index.js index 8e317d5..0516682 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -185,6 +185,13 @@ When a step did nothing - the next route (measured 27-28 Sep): asks it to show one; if the app pulls itself forward the front is handed back. - Nothing is found in an app you just saw: check \`computer_windows\` - the window may have been closed. +- \`computer_find\` finds nothing anywhere in an app that draws its own controls + on a canvas (some games, some plotting tools, some remote-desktop windows): + take one \`computer_screenshot\` of that app only, describe what you see, and + hand the step to the person with \`computer_ask_user\` - do not guess + coordinates from the image yourself. A canvas is also where the accessibility + layer cannot black out a password field, so the person - never you - decides + if that screenshot is safe to take. - Whichever it was, write it down with \`computer_learning\`: the route that failed and the one that worked. That is how this tool gets better. From a46c6a30e7df5657dad4ce7729d9a0529ca2026a Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 14:59:45 +0800 Subject: [PATCH 62/72] =?UTF-8?q?sync-tal.py:=20FORBEHOLD-linjeskiftet=20f?= =?UTF-8?q?orsvandt=20efter=20lukkemark=C3=B8ren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fundet af forside-chatten (2/10): `hale.rstrip('\n')` gjorde `hale` ALTID tom ('\n'.rstrip('\n') == ''), uanset om linjen ovenfor havde regnet sig frem til enkelt eller dobbelt linjeskift. og teksten der fulgte landede på samme linje - GitHub og npm viste markøren som rå tekst ("**Look:** `computer_pending` ·" stod bogstaveligt i README.md). Rettelsen fjerner blot .rstrip('\n') - `hale` var allerede den rigtige streng (smart enkelt/dobbelt alt efter om der i forvejen fulgte en tom linje), den skal bare bruges som den er. Kørt to gange i træk: anden kørsel rapporterer "flader rettet: 0 af 29" (idempotent, som scriptets egen kommentar kræver). Fem flader rettet: README.md, mcp-server/README.md (afledt), docs/index.html, docs/tools.html, docs/llms.txt, docs/llms-install.md. --- README.md | 4 +++- docs/index.html | 4 +++- docs/llms-install.md | 4 +++- docs/llms.txt | 4 +++- docs/tools.html | 4 +++- mcp-server/README.md | 4 +++- scripts/sync-tal.py | 9 ++++++++- 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4de1f22..3b7b4ec 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,9 @@ 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 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` · + + +**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` diff --git a/docs/index.html b/docs/index.html index 5c966f5..530e8fb 100644 --- a/docs/index.html +++ b/docs/index.html @@ -184,7 +184,9 @@

What still asks, and

Install

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.

+ + +

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.

diff --git a/docs/llms-install.md b/docs/llms-install.md index dd5f74d..69e878f 100644 --- a/docs/llms-install.md +++ b/docs/llms-install.md @@ -14,7 +14,9 @@ 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 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 + + +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 93e643b..a272e2e 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -9,7 +9,9 @@ VERSION: npx serves 0.1.0, which has 12 tools. The 32 tools described below 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 diff --git a/docs/tools.html b/docs/tools.html index 01c0212..930ba32 100644 --- a/docs/tools.html +++ b/docs/tools.html @@ -75,7 +75,9 @@

Tool reference

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.

diff --git a/mcp-server/README.md b/mcp-server/README.md index 2c8f67f..449ed50 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -216,7 +216,9 @@ 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 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` · + + +**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` diff --git a/scripts/sync-tal.py b/scripts/sync-tal.py index 4418b12..ede1059 100755 --- a/scripts/sync-tal.py +++ b/scripts/sync-tal.py @@ -236,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) From ac7da21b52de2f3eba8b4b522883e08349e8fdc5 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 20:09:43 +0800 Subject: [PATCH 63/72] =?UTF-8?q?sikkerhed:=20knapErFarlig=20var=20for=20b?= =?UTF-8?q?red=20-=20almindelige=20klik=20begyndte=20at=20sp=C3=B8rge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To regressioner fundet af CI samme dag som fail-closed-rettelsen (f22496a): 1. test/sende-port.mjs q9: et klik på en navnløs AXGroup (helt almindeligt layout-lag i Electron-apps) begyndte at spørge. Rettet ved at genbruge send-portens egen `navnSender`-sondring: kun en rolle der plausibelt ER en knap (ukendt, Button, Image, Unknown) kan overhovedet hedde "Delete" - en gruppe kan ikke. 2. test/e2e-forloeb.mjs "click --app tager ikke skaermen": et klik på en helt almindelig, navngivet NSButton ("klik-maal", fundet af computer_find sekunder før) blev afvist. Målt årsag: `at`-punktopslaget fejlede med AX- fejl -25200 ("did not say who owns that point") - en kendt, almindelig AX-kvirk, ikke kun et canvas/fjernskrivebord-symptom som Opus-panelet antog. At spørge ved hver forekomst ville gøre computer_click næsten ubrugeligt. Rettet: `at` svarer stadig med hvilket program der ejer punktet selv når elementet ikke kan slås op - matcher det programmet kaldet selv navngav (targetBundleId, allerede opslået, intet nyt kald), er det ikke Opus' "jeg aner ikke hvad jeg rammer". Verificeret lokalt (sikkert, ingen af dem rører en rigtig skærm): knap-ord (ny case 9), sende-port, e2e-forloeb, klik-ejer, argumenter, baggrund-stille, failclosed, server-e2e, claims - alle grønne. Derudover: lille grammatikrettelse i sync-tal.py's llms.txt-tekst, fundet af forside-chatten ("described below the source" -> "described below are the source"). --- docs/llms.txt | 2 +- mcp-server/index.js | 39 +++++++++++++++++++++++++++++++++------ scripts/sync-tal.py | 2 +- test/knap-ord.mjs | 11 ++++++++++- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/docs/llms.txt b/docs/llms.txt index a272e2e..8b94cfc 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -6,7 +6,7 @@ > 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 32 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. # /FORBEHOLD diff --git a/mcp-server/index.js b/mcp-server/index.js index 0516682..d64c6af 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -485,7 +485,30 @@ async function menuGenvejErFarlig(app, path) { /// dialog, et fjernskrivebord, et navnløst element). README's løfte holdt ikke /// på den vej: et "Delete" kunne rammes blindt uden at nogen blev spurgt. Et /// punkt vi ikke kan identificere, er nu farligt - spørg, fail CLOSED. -async function knapErFarlig(name, args) { +/// ⛔ RETTET samme dag (CI, test/sende-port.mjs q9): den første udgave spurgte +/// ved ETHVERT navnløst element, inklusiv en navnløs AXGroup - et helt +/// almindeligt, harmløst layout-lag i Electron-apps. Sende-portens EGEN +/// `navnSender` har allerede løst præcis dette (runde 2, Fable 2 - se +/// kommentaren der): kun en rolle der plausibelt ER en knap (ukendt, eller +/// Button/Image/Unknown) kan overhovedet VÆRE et "Slet", så kun DEN +/// rolle-klasse spørger når navnet mangler. En gruppe kan ikke hedde "Delete". +/// ⛔ RETTET samme dag igen (CI, test/e2e-forloeb.mjs "click --app tager ikke +/// skaermen"): `at`-opslaget svarede `found:false` PAA EN HELT ALMINDELIG, +/// navngivet NSButton ("klik-maal") - maalt fejl -25200, "the accessibility +/// layer did not say who owns that point". `computer_find` havde fundet +/// samme knap sekunder foer. Et mislykket PUNKT-opslag er altsaa en kendt, +/// almindelig AX-kvirk - ikke kun canvas/fjernskrivebord, som Opus-panelet +/// antog. At spoerge VED HVER forekomst ville goere computer_click næsten +/// ubrugeligt. `at` svarer stadig med `under` (hvilket program der ejer +/// punktet) selv naar det specifikke element ikke kan slaas op. Det er det +/// rigtige skel: kender vi i det mindste APPEN klikket selv sigter efter +/// (targetBundleId, allerede opslaaet af kaldet ovenfor - intet nyt opslag), +/// er det IKKE Opus' "jeg aner ikke hvad jeg rammer" - spørg kun når punktet +/// tilhører et ANDET program end det kaldet navngav, eller slet intet kendt. +function kanVaereKnap(rolle) { + return !rolle || /^AX(Button|Image|Unknown)$/.test(String(rolle)); +} +async function knapErFarlig(name, args, targetBundleId) { if (name === 'computer_press') { try { const { a, soeg } = trykArgv(args); @@ -497,10 +520,14 @@ async function knapErFarlig(name, args) { if (name === 'computer_click' && Number.isFinite(args.x) && Number.isFinite(args.y)) { try { const d = await callHelper(['at', '--x', String(args.x), '--y', String(args.y)], { timeout: 8000 }); - if (!d?.found) return true; - const navne = [d.title, d.description].filter(Boolean); - if (!navne.length) return true; - return navne.some(n => menuSerFarlig(n)); + if (d?.found) { + const navne = [d.title, d.description].filter(Boolean); + if (!navne.length) return kanVaereKnap(d.role); + return navne.some(n => menuSerFarlig(n)); + } + const ejer = (d?.under || [])[0] || null; + if (!ejer) return true; + return targetBundleId ? ejer !== targetBundleId : true; } catch { return true; } } return false; @@ -1564,7 +1591,7 @@ async function haandterKald(request) { const menuFarlig = name === 'computer_menu' && effektivTier !== TIER.READ && (menuSerFarlig(args.path) || await menuGenvejErFarlig(args.app, args.path)); const knapFarlig = (name === 'computer_press' || name === 'computer_click') && effektivTier !== TIER.READ - && await knapErFarlig(name, args); + && await knapErFarlig(name, args, targetBundleId); const verdict = name === 'computer_ask_user' ? (currentMode() === 'readonly' diff --git a/scripts/sync-tal.py b/scripts/sync-tal.py index ede1059..fdb1565 100755 --- a/scripts/sync-tal.py +++ b/scripts/sync-tal.py @@ -197,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 diff --git a/test/knap-ord.mjs b/test/knap-ord.mjs index 7253532..514c5a5 100644 --- a/test/knap-ord.mjs +++ b/test/knap-ord.mjs @@ -81,7 +81,16 @@ 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); -check('9 kalibrering: porten spurgte et menneske om 1, 2, 4, 6, 7 og 8', spoerger.gangeSpurgt() === 6, `spurgt ${spoerger.gangeSpurgt()} gange`); +// ⛔ 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); + +check('10 kalibrering: porten spurgte et menneske om 1, 2, 4, 6, 7 og 8', spoerger.gangeSpurgt() === 6, `spurgt ${spoerger.gangeSpurgt()} gange`); srv.kill(); console.log(fails.length ? `DUMPET: ${fails.length} tjek` : 'Alle tjek bestået.'); process.exit(fails.length ? 1 : 0); From 0ff9db6f6c9fd957a99dce36ab68a7308ce4c408 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 20:14:46 +0800 Subject: [PATCH 64/72] =?UTF-8?q?CMCP=5FFILM=5FPYNT:=20ren=20optagelse=20t?= =?UTF-8?q?il=20forsiden,=20uden=20at=20r=C3=B8re=20et=20eneste=20tjek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forside-chatten bad om en optagelse af parallel-prøvens 10-runde der kan stå på computermcp.dev uden "menneske.txt"/"m0 m1 m2", Netflix' ophavsretlige plakater, eller et sort skrivebord med Game Center-popup. CMCP_FILM_PYNT=1 (kun via workflow_dispatch, aldrig på push) skifter: - test/parallel.mjs: filnavnet til notes.txt, og teksten der "skrives" fra "m$i " til en kort, cyklende huskeliste (Buy milk, Call Alex, ...) - samme --app-vej, samme forsøgt/verificeret-tælling, samme loop-struktur. - test/brugsscenarier.mjs: scenarie 5's URL/titel-match fra netflix.com til en neutral Wikipedia-artikel (macOS) - scenariets navn, nummer og app-liste er UÆNDREDE, kun destinationen skifter. - fremmed-mac.yml: sætter en rigtig desktop-baggrund (det første .heic koereren rent faktisk har) og bedste forsøg på at tie Game Center/ notifikationer, FØR optagelsen - alt med `|| true`, fejler aldrig bygningen. Verificeret lokalt (sikkert - ingen GUI/skærm rørt): node --check begge filer, SCENARIER.length stadig 19 med og uden flaget, scenarie 5's navn/apps uændret. --- .github/workflows/fremmed-mac.yml | 20 ++++++++++++++++++++ test/brugsscenarier.mjs | 9 ++++++++- test/parallel.mjs | 19 ++++++++++++++++--- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/.github/workflows/fremmed-mac.yml b/.github/workflows/fremmed-mac.yml index bd84a39..2e104c8 100644 --- a/.github/workflows/fremmed-mac.yml +++ b/.github/workflows/fremmed-mac.yml @@ -18,6 +18,10 @@ on: 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 - @@ -50,6 +54,7 @@ jobs: env: CMCP_FREMMED_MASKINE: '1' CMCP_STATUS_IKON: '0' + CMCP_FILM_PYNT: ${{ inputs.film_pynt && '1' || '' }} steps: - uses: actions/checkout@v4 @@ -59,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 diff --git a/test/brugsscenarier.mjs b/test/brugsscenarier.mjs index 0bde029..f919145 100644 --- a/test/brugsscenarier.mjs +++ b/test/brugsscenarier.mjs @@ -38,6 +38,11 @@ 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); @@ -322,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 diff --git a/test/parallel.mjs b/test/parallel.mjs index 7bff65d..8967e69 100644 --- a/test/parallel.mjs +++ b/test/parallel.mjs @@ -36,6 +36,10 @@ const vent = (ms) => new Promise(r => setTimeout(r, ms)); // (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)'); @@ -66,7 +70,7 @@ 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, 'menneske.txt'); + 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 @@ -113,10 +117,19 @@ function startMenneske() { // 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 - R=$("${HJ}" type --app com.apple.TextEdit --text "m$i " 2>&1) + ${skrivLinje} printf '%s' "$R" > "${sidsteSvar}" - if printf '%s' "$R" | grep -q '"verified":true'; then v=$((v+1)); printf "m$i " >> "${log}"; fi + 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' }); From 2aece3dea449d6819a36fa8fc9aa2c446c55bfb5 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 20:45:19 +0800 Subject: [PATCH 65/72] =?UTF-8?q?bevis.sh:=20"ikke=20skubbet"=20m=C3=A5lte?= =?UTF-8?q?=20mod=20origin/main,=20ikke=20husets=20egen=20PR-gren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fundet under fuld-review af denne chats arbejde (2/10): kortet meldte "64 commits ligger lokalt", selvom alt var skubbet til origin/proeve/rettelser-2026-09-28. Lokal main tracker origin/main (git config), men husets egen deploy-doktrin pusher til en proeve/-gren via PR - main er LÅST (computermcp_main_laast). Tjekket målte sin egen hardcodede antagelse, ikke den faktiske push-tilstand. Rettet: er HEAD med på NOGEN fjern-gren (git branch -r --contains HEAD), er den skubbet - og kortet navngiver hvilken. Verificeret: viser nu korrekt "HEAD er på origin/proeve/rettelser-2026-09-28". --- tools/bevis.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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) From 46e3cdc4ae424fc74505153a7474bd03bccb8650 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 21:01:37 +0800 Subject: [PATCH 66/72] =?UTF-8?q?sikkerhed:=20fjern=20targetBundleId-undta?= =?UTF-8?q?gelsen=20-=20den=20gen=C3=A5bnede=20Opus'=20fund?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uafhængigt review-security-review (2/10, efter fuld-review-skillets Trin 5) fandt at ac7da21's "punktet tilhører den app kaldet selv navngav"-undtagelse (F1, Critical, confidence 90) genåbner PRÆCIS det Opus-panelet fandt: et fjernskrivebord eller en VM ejer sit eget vindue legitimt, men AX-laget kan ALDRIG se ind i det - "under" matcher altid "app" derinde. At kende APPEN er ikke det samme som at kende ELEMENTET. Prøvede først et genforsøg (en ægte AX-kvirk burde være forbigående) - MÅLT at svigte empirisk: samme AX-fejl -25200 to gange i træk på den samme, helt almindelige NSButton fra test/e2e-forloeb.mjs. Kvirken er ikke timing, og der er ingen billig måde at skelne "en almindelig knap AX tilfældigvis ikke kan slå op" fra "et fjernskrivebord der aldrig kan slås op" med kun ét punkt-opslag. Efter tre runder der hver rettede ét symptom og genåbnede et andet, er den disciplinerede løsning den simpleste sikre: `found:false` spørger ALTID, uden undtagelse - computer_click bliver dyrere på kendte AX-kvirke, og den pris er betalt med vilje. test/e2e-forloeb.mjs's "klik-maal"-knap rammes nu korrekt af spørgsmålet; testens forventning er opdateret til at afspejle den sikrere, sande opførsel (og "sent"-effektens dækning flyttet til test/klik-ejer.mjs, hvor den allerede proves med et ja). Dette er FJERDE version af knapErFarlig i dag (07a4a8f-serien, f22496a, ac7da21, nu denne). Resten af review-security's fund (F2 nameless AXLink/ AXStaticText, F3 click-through på dækket vindue, F5 press fail-open på timeout, F6-F11) er IKKE rettet i denne omgang - rapporteret, ikke skjult. Verificeret lokalt: knap-ord (9/9), sende-port, klik-ejer, e2e-forloeb (32/32), argumenter, baggrund-stille, failclosed, server-e2e, claims - alle grønne. Q26-mutantens anker matcher igen koden uændret. --- mcp-server/index.js | 44 +++++++++++++++++++++++--------------------- test/e2e-forloeb.mjs | 34 +++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/mcp-server/index.js b/mcp-server/index.js index d64c6af..dcb4570 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -494,21 +494,27 @@ async function menuGenvejErFarlig(app, path) { /// rolle-klasse spørger når navnet mangler. En gruppe kan ikke hedde "Delete". /// ⛔ RETTET samme dag igen (CI, test/e2e-forloeb.mjs "click --app tager ikke /// skaermen"): `at`-opslaget svarede `found:false` PAA EN HELT ALMINDELIG, -/// navngivet NSButton ("klik-maal") - maalt fejl -25200, "the accessibility -/// layer did not say who owns that point". `computer_find` havde fundet -/// samme knap sekunder foer. Et mislykket PUNKT-opslag er altsaa en kendt, -/// almindelig AX-kvirk - ikke kun canvas/fjernskrivebord, som Opus-panelet -/// antog. At spoerge VED HVER forekomst ville goere computer_click næsten -/// ubrugeligt. `at` svarer stadig med `under` (hvilket program der ejer -/// punktet) selv naar det specifikke element ikke kan slaas op. Det er det -/// rigtige skel: kender vi i det mindste APPEN klikket selv sigter efter -/// (targetBundleId, allerede opslaaet af kaldet ovenfor - intet nyt opslag), -/// er det IKKE Opus' "jeg aner ikke hvad jeg rammer" - spørg kun når punktet -/// tilhører et ANDET program end det kaldet navngav, eller slet intet kendt. +/// navngivet NSButton ("klik-maal") - maalt fejl -25200. Forsøgte da at lade +/// "punktet tilhører den app kaldet selv navngav" (targetBundleId === under) +/// tælle som harmløst. +/// ⛔ RETTET EN TREDJE GANG (review-security, 2/10, Critical, confidence 90): +/// den rettelse genåbnede PRÆCIS det Opus-panelet fandt. Et fjernskrivebord +/// eller en VM ejer sit eget vindue legitimt - "under" matcher ALTID "app" +/// derinde, for hele pointen med en RDP-klient er at AX-laget aldrig kan se +/// ind i den. At kende APPEN er ikke det samme som at kende ELEMENTET. +/// Prøvede derefter et genforsøg 150ms senere (en ægte AX-kvirk burde være +/// forbigående, et opaque fjernskrivebord ville fejle igen) - MÅLT at +/// svigte: samme -25200 to gange i træk på den samme knap. Kvirken er ikke +/// timing. Der er ingen billig måde at skelne "en helt almindelig knap AX +/// tilfældigvis ikke kan slå op" fra "et fjernskrivebord der aldrig kan +/// slås op" med kun ÉT punkt-opslag - så der gættes ikke mere. `found:false` +/// spørger nu ALTID, uden undtagelse. computer_click bliver dyrere på en +/// kendt AX-kvirk (test/e2e-forloeb.mjs's egen "klik-maal"-knap rammes af +/// den) - det er prisen for at lukke hullet, og den er betalt med vilje. function kanVaereKnap(rolle) { return !rolle || /^AX(Button|Image|Unknown)$/.test(String(rolle)); } -async function knapErFarlig(name, args, targetBundleId) { +async function knapErFarlig(name, args) { if (name === 'computer_press') { try { const { a, soeg } = trykArgv(args); @@ -520,14 +526,10 @@ async function knapErFarlig(name, args, targetBundleId) { if (name === 'computer_click' && Number.isFinite(args.x) && Number.isFinite(args.y)) { try { const d = await callHelper(['at', '--x', String(args.x), '--y', String(args.y)], { timeout: 8000 }); - if (d?.found) { - const navne = [d.title, d.description].filter(Boolean); - if (!navne.length) return kanVaereKnap(d.role); - return navne.some(n => menuSerFarlig(n)); - } - const ejer = (d?.under || [])[0] || null; - if (!ejer) return true; - return targetBundleId ? ejer !== targetBundleId : true; + if (!d?.found) return true; + const navne = [d.title, d.description].filter(Boolean); + if (!navne.length) return kanVaereKnap(d.role); + return navne.some(n => menuSerFarlig(n)); } catch { return true; } } return false; @@ -1591,7 +1593,7 @@ async function haandterKald(request) { const menuFarlig = name === 'computer_menu' && effektivTier !== TIER.READ && (menuSerFarlig(args.path) || await menuGenvejErFarlig(args.app, args.path)); const knapFarlig = (name === 'computer_press' || name === 'computer_click') && effektivTier !== TIER.READ - && await knapErFarlig(name, args, targetBundleId); + && await knapErFarlig(name, args); const verdict = name === 'computer_ask_user' ? (currentMode() === 'readonly' diff --git a/test/e2e-forloeb.mjs b/test/e2e-forloeb.mjs index b6fdc01..53b9366 100644 --- a/test/e2e-forloeb.mjs +++ b/test/e2e-forloeb.mjs @@ -176,16 +176,23 @@ 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)); } @@ -245,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(); From 2ba02e62753818007eba1e2eab2587a8b9bbfd51 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 21:53:44 +0800 Subject: [PATCH 67/72] rettet mutationsbevis: navnSender's klik-gren var en kopi af kanVaereKnap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI (46e3cdc) fejlede på mutant R3-ukendt-rolle-ufarlig: "1 overlevede, 0 fejl" - en ægte overlevelse, ikke en instrument-fejl. Målt årsag: navnSender's klik-rolle-tjek var en ord-for-ord kopi af knapErFarlig's kanVaereKnap() (bevidst genbrugt mønster, se f22496a). To uafhængige kopier af samme logik betyder at en mutation i navnSender's egen kopi blev maskeret - knapErFarlig spurgte stadig, af sin EGEN grund, så den observerede opførsel ("spurgte den?") var uændret uanset mutationen. Rettet ved at fjerne duplikeringen: navnSender's klik-gren kalder nu kanVaereKnap(rolle) direkte i stedet for at gentage udtrykket. Én kilde, én mutation rammer begge porte - og rammer nu BEGGE porte's prøver (verificeret: mutanten gør både sende-port.mjs OG knap-ord.mjs røde). Mutant R3's anker opdateret til den delte funktions linje. Verificeret lokalt: mutationen anvendt manuelt → begge testfiler røde på den forventede linje → gendannet → begge grønne igen i deres normale tilstand. --- .github/mutanter-porte.json | 4 ++-- mcp-server/index.js | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/mutanter-porte.json b/.github/mutanter-porte.json index 9f61c51..dd4f0f1 100644 --- a/.github/mutanter-porte.json +++ b/.github/mutanter-porte.json @@ -564,8 +564,8 @@ "fil": "mcp-server/index.js", "par": [ [ - " return klik ? (!rolle || /^AX(Button|Image|Unknown)$/.test(String(rolle)))", - " return klik ? /^AX(Button|Image)$/.test(String(rolle))" + " return !rolle || /^AX(Button|Image|Unknown)$/.test(String(rolle));", + " return /^AX(Button|Image)$/.test(String(rolle));" ] ], "proeve": "test/sende-port.mjs", diff --git a/mcp-server/index.js b/mcp-server/index.js index dcb4570..a52c768 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -587,7 +587,13 @@ async function sendeDom(name, args, bid) { if (n) return SENDE_ORD.test(n); // Runde 3 (Astra 3): en UKENDT rolle er ogsaa ved et klik en mulig send - kun // en KENDT ufarlig rolle (fx AXGroup) er undtaget. - return klik ? (!rolle || /^AX(Button|Image|Unknown)$/.test(String(rolle))) : (!rolle || /^AX(Button|Image|Group|Unknown|Link)$/.test(String(rolle))); + // ⛔ 2/10: klik-grenen her var en ORD-FOR-ORD kopi af knapErFarlig's + // kanVaereKnap() (samme mønster genbrugt bevidst). To kopier af samme + // logik betyder at en mutation i den ene maskeres af den anden - MÅLT: + // mutant R3-ukendt-rolle-ufarlig overlevede, fordi knapErFarlig stadig + // spurgte af sin EGEN grund. Kalder nu den delte funktion direkte - + // én kilde, én mutation rammer begge gates' prøver. + return klik ? kanVaereKnap(rolle) : (!rolle || /^AX(Button|Image|Group|Unknown|Link)$/.test(String(rolle))); }; let knapVindue = null, knapRamme = null, erKontrol = false; let sender = false; From 420b99800642fa4fbfbbeb7081c004e7d72b57b2 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 22:15:23 +0800 Subject: [PATCH 68/72] udgivelse: ret to konkrete fejl fundet af Astra+Opus panelet 2/10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - udgiv-npm.yml: opsætnings-kommentaren sagde "Environment: (tom)" til Gustav, i modstrid med jobbets egen "environment: udgivelse" og husets låste beslutning (computermcp_main_laast_2026_09_29). Ville have fået npm OIDC til at afvise tokenet pga. environment-claim-mismatch ved første rigtige publish-forsøg. - release.sh: fjernet gen-indsættelse af værktøjstal i repo-beskrivelsen - tallet blev bevidst fjernet 2/10 fordi det var forkert mod både koden og npm; scriptet ville have sat den forkerte værdi tilbage ved næste udgivelse. Begge fund verificeret selv mod koden før rettelse, ikke taget for pålydende fra panelrapporterne. Ingen af de to kræver en beslutning - de retter kode til at stemme med fakta der allerede er afgjort. --- .github/workflows/udgiv-npm.yml | 6 +++++- scripts/release.sh | 13 ++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/udgiv-npm.yml b/.github/workflows/udgiv-npm.yml index 4f77195..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. # --------------------------------------------------------------------------- diff --git a/scripts/release.sh b/scripts/release.sh index a19e073..fa35cc7 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -294,14 +294,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" From b18485444450abc5104c08257af3c81ea777a8a6 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 22:45:05 +0800 Subject: [PATCH 69/72] mutant Q9-navnloes-gruppe-er-send: anker peger ikke paa levende kode mere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI på 2ba02e6 fejlede ægte: "Q9-navnloes-gruppe-er-send: ankrene findes [0] gange" - dedup-refaktoreringen i 2ba02e6 (navnSender kalder nu kanVaereKnap direkte i stedet for sin egen inline-kopi) fjernede den tekst mutanten søgte efter. 0 mutanter overlevede; dette var en knækket anker, ikke en reel fejl. Retarget til kanVaereKnap's funktionskrop (samme sted R3-ukendt-rolle-ufarlig peger), med sin egen mutation (tilføjer Group i stedet for at fjerne Unknown) så den stadig beviser noget andet end R3. Mutationsbevist manuelt begge veje før push: muteret → test/sende-port.mjs gik rødt netop på "q9 klik paa en navnloes gruppe" (den påstand mutanten selv hedder) → gendannet → git diff tom → baseline grøn igen. knap-ord.mjs upåvirket. --- .github/mutanter-porte.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/mutanter-porte.json b/.github/mutanter-porte.json index dd4f0f1..d01c837 100644 --- a/.github/mutanter-porte.json +++ b/.github/mutanter-porte.json @@ -516,8 +516,8 @@ "fil": "mcp-server/index.js", "par": [ [ - " return klik ? (!rolle || /^AX(Button|Image|Unknown)$/", - " return klik ? (!rolle || /^AX(Button|Image|Unknown|Group)$/" + " return !rolle || /^AX(Button|Image|Unknown)$/.test(String(rolle));", + " return !rolle || /^AX(Button|Image|Unknown|Group)$/.test(String(rolle));" ] ], "proeve": "test/sende-port.mjs", From 0e6274540b05c7c96e297f097905f1273d6ccbab Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 23:16:13 +0800 Subject: [PATCH 70/72] udgivelse: ret tre fejl i faerdiggoer-udgivelsen.sh (Astra+Opus 2/10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tilføjet eksplicit "mcp-publisher login" før publish, som to-vejs-skridt (samme rettelse release.sh allerede har - set -e dækker ikke en &&-kæde). - Registerkontrollen kunne matche en FREMMED server ved navn-overlap i søgningen; kræver nu præcis navn "io.github.Agent360dk/computer-mcp" OG sammenligner med den faktisk udgivne version. Selvtestet mod det ægte, læsende registerkald (0.1.0 matcher, 0.2.1 rapporteres som uoverensstemmelse). - Scriptets sidste trin anbefalede "git push origin main" - afvises altid, main kræver PR (enforce_admins=true, bekræftet). Erstattet med en gren + gh pr create-vej. Ingen CI-vagt kører dette script (grep-bekræftet); ingen risiko for porten. Intet kørt - kun scriptets egen tekst og logik rettet. --- tools/faerdiggoer-udgivelsen.sh | 43 +++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) 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; } From f460c15930bf44db009e5648d77ccaaeea491612 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Fri, 2 Oct 2026 23:18:32 +0800 Subject: [PATCH 71/72] =?UTF-8?q?udgivelse:=20release.sh=20skubbede=20alti?= =?UTF-8?q?d=20forg=C3=A6ves=20eller=20forkert=20til=20protected=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tre fund, alle Opus 2/10, verificeret selv mod branch-beskyttelsen (gh api .../branches/main/protection, bekræftet enforce_admins=true): 1. Intet tjek for at lokalt HEAD faktisk er origin/main FØR der tagges. Køres scriptet i et træ hvis lokale main er bagud (målt i en anden chats træ: stod på 46e3cdc), ville det offentlige mærke v$V pege på en commit GitHub ikke har - permanent forkert, mærker omskrives ikke uden historik- brud. Tilføjet et hårdt stop før trin 0. 2. "git push origin main --tags" (linje ~254) forsøgte at skubbe "main" for et skridt der kun handler om mærket - meningsløst når HEAD nu er verificeret synkron, og ville være blevet afvist af beskyttelsen hvis det alligevel ikke var. Skubber nu kun mærket. 3. Efter en VIRKELIG npm-udgivelse (irreversibel, fælden allerede afvæbnet) prøvede scriptet samme afviste "git push origin main" for at synkronisere PUBLICERET/docs - et set -e-stop her ville have efterladt Gustav i en uklar tilstand midt i en udgivelse. Erstattet med gren + automatisk PR; scriptet fortæller præcis hvilken PR der skal merges, uden selv at merge den. Intet af dette rammer CI (grep-bekræftet: ingen workflow eller test udfører scripts/release.sh - kun build-release.sh, en anden fil). bash -n ren. --- scripts/release.sh | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index fa35cc7..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/^/ /' @@ -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 ==" From 77ba8b65571736d2bfd530a4c9f947dc96328415 Mon Sep 17 00:00:00 2001 From: Gustav Louv Date: Sat, 3 Oct 2026 00:16:53 +0800 Subject: [PATCH 72/72] sikkerhed F5: computer_press delte klikkets gamle fail-open-fejl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review-security fandt det (F5, Important), en søsterchats Astra+Opus-panel genfandt og verificerede det selv i koden (420b998, index.js:517-524) og Gustav gav ja til at rette den nu, resten af review-security-fundene som kendt risiko (forbrugeragent-3e, 2/10). Verificeret selv mod den faktiske kode før rettelse: knapErFarlig's klik-gren blev gjort fail-closed i dag (46e3cdc) - `catch { return true; }`, `if (!d?.found) return true;`. Press-grenen gjorde præcis det modsatte - `catch { return false; }` og et manglende would_press talte som "ufarligt". Samme fejlklasse: et mislykket opslag læst som "ved besked" i stedet for "ved ikke", nøjagtig den antagelse F1 lukkede for klik i dag. Rettet til samme fail-closed-regel som klik, ingen undtagelse. To nye prøver (knap-ord.mjs 11-12: manglende would_press, og selve dry-run-kaldet fejler) plus to mutanter (Q27, Q28) - mutationsbevist manuelt begge veje FØR commit, og hele den lokale mutationssuite kørt bagefter: 0 overlevede, 0 fejl (74 mutanter inkl. de to nye). test/sende-port.mjs og test/e2e-forloeb.mjs upåvirkede. Co-Authored-By: Claude Sonnet 5 --- .github/mutanter-porte.json | 24 ++++++++++++++++++++++++ mcp-server/index.js | 9 +++++++-- test/knap-ord.mjs | 14 +++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/.github/mutanter-porte.json b/.github/mutanter-porte.json index d01c837..bdfd46e 100644 --- a/.github/mutanter-porte.json +++ b/.github/mutanter-porte.json @@ -862,5 +862,29 @@ " 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/mcp-server/index.js b/mcp-server/index.js index a52c768..f2584d4 100644 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -516,12 +516,17 @@ function kanVaereKnap(rolle) { } async function knapErFarlig(name, args) { if (name === 'computer_press') { + // ⛔ review-security F5 (2/10), Gustav ja: samme fejlklasse som klikkets F1 i dag - + // et mislykket opslag blev laest som "ufarligt", ikke som "ved ikke". Et + // tidsudloeb paa toerkoerslen fortaeller intet om hvad et AEGTE tryk ville + // ramme - fail-closed, som klikket allerede goer, ingen undtagelse. try { const { a, soeg } = trykArgv(args); const d = await callHelper([...a, '--dry'], { stdin: JSON.stringify(soeg), timeout: 15000 }); const el = d?.would_press; - return !!el && [el.name, ...(el.names || []), el.title].filter(Boolean).some(n => menuSerFarlig(n)); - } catch { return false; } + if (!el) return true; + return [el.name, ...(el.names || []), el.title].filter(Boolean).some(n => menuSerFarlig(n)); + } catch { return true; } } if (name === 'computer_click' && Number.isFinite(args.x) && Number.isFinite(args.y)) { try { diff --git a/test/knap-ord.mjs b/test/knap-ord.mjs index 514c5a5..0e3d3b3 100644 --- a/test/knap-ord.mjs +++ b/test/knap-ord.mjs @@ -90,7 +90,19 @@ writeFileSync(AT, JSON.stringify({ ok: true, found: true, bundleId: 'com.apple.T 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); -check('10 kalibrering: porten spurgte et menneske om 1, 2, 4, 6, 7 og 8', spoerger.gangeSpurgt() === 6, `spurgt ${spoerger.gangeSpurgt()} gange`); +// ⛔ 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);
Claude Codeclaude mcp add computer -- npx -y @agent360/computer-mcp
Cursor / VS Code / CodexIn mcp.json: {"command":"npx","args":["-y","@agent360/computer-mcp"]}