From e404578a63c5fd7952a4c25bf1f699fb519d0065 Mon Sep 17 00:00:00 2001 From: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:10:17 +0100 Subject: [PATCH 01/83] Record the owner's decisions on meetings and transcripts D1: local transcription is wanted, in its own spec. D2: no extra gate, open_mess is for the owner's own use, so transcripts reach agents under the account's read permission like chat messages. D3: a transcript fetch must never count as a view in the recording owner's analytics, so step 0 gets a gate that checks the counters on the user's own recording with a control, and the download path ships only if it passes. Reviewed by Codex and GLM in round 11 (both approve). --- .specs/2026-10-28-meetings-transcripts.md | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.specs/2026-10-28-meetings-transcripts.md b/.specs/2026-10-28-meetings-transcripts.md index 3f13f00..df6507f 100644 --- a/.specs/2026-10-28-meetings-transcripts.md +++ b/.specs/2026-10-28-meetings-transcripts.md @@ -1,7 +1,7 @@ # open_mess - spotkania i transkrypcje Teams (etap 2) Data: 2026-10-28 -Status: zatwierdzony przez Codex i GLM w rundzie R10 (2026-10-28); poprawki z recenzji commitów po R10 w changelogu +Status: zatwierdzony przez Codex i GLM w rundzie R10 (2026-10-28); poprawki z recenzji commitów po R10 w changelogu; decyzje właściciela D1-D3 (2026-10-28) od rundy R11 Powiązane: `.specs/2026-10-26-open-mess-design.md` (8.3, 8.5, 8.6, 12, 15), `docs/protocols/teams.md` (sekcja 9, "Live results" 9, "Needs live capture" 13-16), `wit/module.wit`. ## 1. Cel @@ -16,8 +16,7 @@ Podsumowanie robi agent; open_mess niczego nie streszcza, nie nagrywa i nie tran - Etap 2 = spotkania, na których użytkownik jest, i transkrypcje Teams; podsumowuje agent (spec 3, 12). - Live check tylko do odczytu, przy właścicielu, za zgodą w czacie - zrobiony 2026-10-27 (`docs/protocols/teams.md`, "Live results" 9). - -Otwarte decyzje właściciela - sekcja 10. Ten spec ich nie rozstrzyga. +- Decyzje D1-D3 z 2026-10-28 - sekcja 10: lokalna transkrypcja w osobnym specu, transkrypcje dla agentów bez dodatkowej bramki, pobranie transkrypcji nigdy nie liczy się jako wyświetlenie nagrania. ## 3. Kontekst w kodzie i protokole @@ -71,7 +70,7 @@ Moduł: 3. Gdy `metadata.ownerTenantId` różni się od `tid` konta: błąd "not supported yet: the transcript is stored by another organization" (live: SharePoint innej organizacji odrzuca token, docs 9 "Live results" 9). 4. Origin z `location` musi być jedną z dwóch witryn organizacji użytkownika: `https://-my.sharepoint.com` (OneDrive) albo `https://.sharepoint.com` (witryny zespołów), gdzie `` pochodzi z własnej witryny OneDrive użytkownika - `personalFileSite` z `GET /v1/users/ME/properties` (tylko odczyt, klucz potwierdzony live, "Live results" 4), pobierane raz na sesję. Inny origin = błąd "transcript stored outside your organization", bez prośby o token: odbiorca tokenu nigdy nie pochodzi wyłącznie z odpowiedzi MCPS, więc wroga albo błędna odpowiedź nie skieruje tokenu SharePoint do obcej organizacji (recenzja R7). Brak `personalFileSite` = "not supported yet: cannot tell your organization's SharePoint". Znane ograniczenie: w organizacji wielogeograficznej (authsvc zwraca `isMultiGeo`, "Live results" 2) witryny innych geografii mają inne etykiety i dostają ten sam błąd - obsługa to osobna zmiana, gdy będzie potrzebna. Dalej `driveId` i `driveItemId` tylko znaki `[A-Za-z0-9!_-]`, do 256 znaków; inaczej "niezgodny protokół". 5. `media/transcripts`; pusta lista albo brak pozycji z `isVisible` = wynik z `reason: "transcript not ready"` (Teams jeszcze przetwarza nagranie), nie błąd (recenzja R3). Wybór wyłącznie spośród pozycji z `isVisible: true` (niewidocznych Teams nie pokazuje, open_mess też nie): ta z `id == metadata.transcriptId`, inaczej ta z `isDefault`, inaczej pierwsza (recenzja R4). Związek `transcriptId` z MCPS i `id` z ODSP to "Needs live capture" 13. `id` wybranej pozycji przechodzi tę samą kontrolę co `driveId` (krok 4: dozwolone znaki, długość) i trafia do ścieżki zakodowany procentowo; inaczej "niezgodny protokół" (recenzja R2). -6. `language` = `languageTag` pozycji, tylko gdy pasuje do `^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8}){0,4}$`, inaczej `null` (recenzja R7). `streamContent?is=1` (WebVTT, do `MAX_BODY`). Parser: nagłówek `WEBVTT` wymagany; fragmenty = linia czasu `[HH:]MM:SS.mmm --> [HH:]MM:SS.mmm` (obie postaci znacznika z WebVTT; ustawienia po znacznikach ignorowane) + tekst; mówca z ``; pozostałe tagi usuwane; najwyżej 100 000 fragmentów, 4000 znaków tekstu i 200 znaków mówcy na fragment (nadmiar obcięty ze znacznikiem, recenzja R4); zły format = "niezgodny protokół", nigdy panika. +6. `language` = `languageTag` pozycji, tylko gdy pasuje do `^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8}){0,4}$`, inaczej `null` (recenzja R7). `streamContent?is=1` (WebVTT, do `MAX_BODY`) - tylko jeśli krok 0 potwierdzi, że to pobranie nie liczy się jako wyświetlenie nagrania (D3); inaczej krok 0 wskazuje inną drogę i spec wraca do review, a bez takiej drogi `teams_get_transcript` nie powstaje. Parser: nagłówek `WEBVTT` wymagany; fragmenty = linia czasu `[HH:]MM:SS.mmm --> [HH:]MM:SS.mmm` (obie postaci znacznika z WebVTT; ustawienia po znacznikach ignorowane) + tekst; mówca z ``; pozostałe tagi usuwane; najwyżej 100 000 fragmentów, 4000 znaków tekstu i 200 znaków mówcy na fragment (nadmiar obcięty ze znacznikiem, recenzja R4); zły format = "niezgodny protokół", nigdy panika. 7. Strona: fragmenty od `cursor`, dopóki suma długości fragmentów zserializowanych do JSON (w bajtach, z ucieczkami znaków) nie przekroczy `max_bytes`, najwyżej 2000 fragmentów (co najmniej jeden fragment; pojedynczy fragment ma najwyżej 4000 znaków, więc kilkadziesiąt KB); `next_cursor` albo `null`. Górna granica `max_bytes` zostawia zapas do limitu wyniku 1 MiB (5.1) na resztę wyniku (recenzje R1, R2). Każda strona pobiera VTT od nowa (ponytail: bez pamięci podręcznej; transkrypcja godzinnego spotkania to ok. 100 KB; pamięć podręczna, gdy pomiar pokaże potrzebę). Wynik: @@ -119,17 +118,19 @@ Token SharePoint ma odbiorcę zależnego od organizacji (`https://-my.sh - Oba narzędzia czytają: podlegają prawu czytania konta/rozmowy (5.1), nigdy nie piszą na platformę i nie zmieniają stanu "przeczytane" (tylko GET do kalendarza, MCPS i SharePoint). - Nic nie jest zapisywane lokalnie: brak pamięci podręcznej transkrypcji, brak plików, w logach tylko kody statusu i liczby (spec 8.4). -- Treść transkrypcji, tematy i nazwiska trafiają tylko do agentów podłączonych przez użytkownika (i dalej do dostawców ich modeli), jak wiadomości (spec 5, AGENTS.md) - z zastrzeżeniem otwartej decyzji D2 (sekcja 10). +- Treść transkrypcji, tematy i nazwiska trafiają tylko do agentów podłączonych przez użytkownika (i dalej do dostawców ich modeli), jak wiadomości (spec 5, AGENTS.md), bez dodatkowej bramki poza prawem czytania konta/rozmowy (D2, sekcja 10). +- Pobranie transkrypcji nie zostawia śladu wyświetlenia w analityce nagrania u jego właściciela (D3, sekcja 10): moduł używa wyłącznie drogi, którą krok 0 potwierdził jako niewidoczną w analityce. - Brak wyzwalaczy w tym etapie ("transkrypcja gotowa" - poza zakresem). ## 7. Kroki 0. **Live capture przy właścicielu** (docs "Needs live capture" 13-16), tylko odczyt, tylko kody, klucze i liczby: kształt `paginationInfo` MCPS i odpowiedź kalendarza dla gęstego okna 31 dni (czy pojawia się stronicowanie), pobranie transkrypcji we własnej organizacji (lista ODSP, `streamContent?is=1`, związek `transcriptId` z `id`), MCPS dla spotkania cyklicznego i bez transkrypcji, kształt `Media_CallTranscript`, czy pobranie transkrypcji liczy się jako wyświetlenie nagrania, alfabet i długość identyfikatorów `19:meeting_...` (tylko klasy znaków i długości, nie wartości), zbiór wartości `myResponseType` i `eventType`, obecność pola prywatności i znacznika odwołania spotkania, postać `startTime`/`endTime` i rola `eventTimeZone`/`utcOffset`, postać znaczników czasu w VTT, postać `personalFileSite` (tylko schemat i wzór hosta, bez nazwy organizacji) i czy `location` transkrypcji własnej organizacji leży na jednym z dwóch originów z 4.2 krok 4. Wyniki do `docs/protocols/teams.md`. Krok 0 zapisuje też klasy znaków i długości `driveId`, `driveItemId` i `id` transkrypcji ODSP (bez wartości). Jeśli pobranie wymaga innej drogi niż 4.2 kroki 4-6, kalendarz okaże się stronicowany albo identyfikatory wyjdą poza klasy z 4.2 krok 4, spec wraca do review. + **Bramka D3 (wyświetlenia)**, część kroku 0: (a) REA klienta desktopowego: jak klient i odtwarzacz nagrań rejestrują wyświetlenie (osobne wywołanie, np. zapis aktywności albo zdarzenie analityki, czy sam odczyt treści po stronie serwera) i jakie zapytania klient wysyła, pokazując transkrypcję w podsumowaniu spotkania; (b) na żywo, wyłącznie na nagraniu, którego właścicielem jest sam użytkownik (więc ewentualne wyświetlenie trafia tylko do jego własnej analityki): liczniki pozycji `GET /_api/v2.1/drives//items//analytics/allTime` i `.../lastSevenDays` (`access.actionCount`, `access.actorCount` - tylko liczby) oraz liczba wyświetleń, którą pokazuje odtwarzacz nagrania, przed jednym pobraniem transkrypcji i ponownie po opóźnieniu analityki (następnego dnia), bez innych otwarć nagrania w tym czasie; kontrola: te same liczniki po otwarciu nagrania w oficjalnym kliencie. Droga przechodzi bramkę tylko wtedy, gdy pobranie nie zmienia żadnego licznika, a kontrola zmienia. Pobranie zmienia licznik = droga odrzucona; wtedy krok 0 szuka w REA innej drogi do tej samej treści (np. `temporaryDownloadUrl`, `streamContent?format=json`, usługa z 9.6 w docs) i sprawdza ją tak samo. Kontrola nie zmienia licznika (wynik nierozstrzygnięty) albo żadna droga nie przechodzi = stop i raport dla właściciela; `teams_get_transcript` nie jest wtedy wydawany, `teams_list_meetings` może być. 1. Host: wzorce zakresów WAM (5.2) z testami. 2. Rdzeń: dodatkowe narzędzia w rejestrze (5.1); moduł testowy `modules/test-echo` deklaruje jedno narzędzie czytające do testów rejestru; poprawiony komentarz w `wit/module.wit`. 3. Teams: `teams_list_meetings` z testami dekodera na oczyszczonych, syntetycznych odpowiedziach o kształcie z live check. 4. Teams: `teams_get_transcript` z testami (MCPS, wybór, inna organizacja, 401/404, parser VTT). -5. Test akceptacyjny na żywo przy właścicielu (sekcja 9), wydanie dopiero po decyzji D2. +5. Test akceptacyjny na żywo przy właścicielu (sekcja 9). ## 8. Dotknięte pliki @@ -155,12 +156,13 @@ Token SharePoint ma odbiorcę zależnego od organizacji (`https://-my.sh - Parser VTT: zły nagłówek, ucięty plik, brak `-->`, ogromna liczba fragmentów, bardzo długa linia, nie-UTF-8 - błąd albo obcięcie, nigdy panika (testy regresji). - Żadne z narzędzi nie wysyła zapytania innego niż GET do kalendarza, MCPS i SharePoint (test na nagranych wywołaniach `http.fetch`). - Oficjalny klient Teams działa jak dotąd; stan "przeczytane" czatów spotkań bez zmian. +- Pobranie transkrypcji nie zmienia liczników wyświetleń nagrania (bramka D3 w kroku 0, wynik w `docs/protocols/teams.md`). -## 10. Otwarte decyzje właściciela +## 10. Decyzje właściciela (2026-10-28) -- **D1. Lokalna transkrypcja dźwięku (np. Whisper)** dla spotkań bez transkrypcji Teams i innych platform (spec 12). Nie wchodzi w ten spec. Do rozstrzygnięcia: czy w ogóle, a jeśli tak - nagrywanie tylko na wyraźne polecenie użytkownika dla danego spotkania czy automatycznie, informacja dla uczestników, model wyłącznie lokalny, gdzie i jak długo leży dźwięk. Wymaga osobnego specu (przechwyt dźwięku to nowy import hosta, spec 12). -- **D2. Zgoda / informacja dla uczestników (RODO)** przy przekazywaniu transkrypcji - mowy innych osób - agentom i dalej dostawcom ich modeli. Możliwości: (a) bez dodatkowej bramki, jak wiadomości czatu (prawo czytania konta/rozmowy); (b) osobny przełącznik per konto "transkrypcje dla agentów", domyślnie wyłączony; (c) jak (b) i tylko spotkania organizowane przez użytkownika; (d) inna. Do decyzji kroki 0-4 można zbudować, ale wydanie z tymi narzędziami (`cargo xtask release`) czeka na decyzję; (b) i (c) to mała zmiana w 5.1 (dodatkowy warunek przed wywołaniem) i w oknie. -- **D3. Wyświetlenie w analityce nagrania** - tylko jeśli krok 0 pokaże, że pobranie transkrypcji widać u właściciela nagrania jako wyświetlenie: czy to akceptowalne (oficjalny klient robi to samo, otwierając podsumowanie spotkania). +- **D1. Lokalna transkrypcja dźwięku (np. Whisper)** dla spotkań bez transkrypcji Teams i innych platform (spec 12): **tak, ale w osobnym specu**, nie w tym. Tamten spec rozstrzyga nagrywanie na polecenie czy automatycznie, informację dla uczestników, model wyłącznie lokalny, gdzie i jak długo leży dźwięk (przechwyt dźwięku to nowy import hosta, spec 12). +- **D2. Zgoda / informacja dla uczestników (RODO)** przy przekazywaniu transkrypcji agentom: **(a) bez dodatkowej bramki**, bo open_mess służy właścicielowi do własnego użytku. Transkrypcje trafiają do agentów na prawie czytania konta/rozmowy, jak wiadomości czatu (5.1, 6); wydanie nie czeka na nic więcej. Odrzucone: (b) przełącznik per konto, (c) tylko spotkania organizowane przez użytkownika. +- **D3. Wyświetlenie w analityce nagrania**: **niedopuszczalne**. Pobranie transkrypcji nie może pojawić się u właściciela nagrania jako wyświetlenie. Jeśli krok 0 pokaże, że droga z 4.2 liczy się jako wyświetlenie, trzeba znaleźć (REA klienta desktopowego) drogę, która się nie liczy, albo zatrzymać się i zgłosić to właścicielowi; nigdy nie wydawać pobrania, które rejestruje wyświetlenie (bramka D3 w kroku 0). ## 11. Threat model @@ -169,6 +171,7 @@ Token SharePoint ma odbiorcę zależnego od organizacji (`https://-my.sh - **Niezaufane**: odpowiedzi middle tier, MCPS i SharePoint oraz pliki VTT - limity rozmiaru (16 MiB odpowiedź, 1 MiB wynik narzędzia, 100 000 fragmentów, 4000 znaków na fragment), ścisłe dekodowanie, sprawdzanie originu SharePoint i identyfikatorów przed użyciem w adresie (moduł nie zbuduje zapytania do hosta wskazanego przez odpowiedź serwera poza `*.sharepoint.com`, a rdzeń i tak egzekwuje listę hostów), brak paniki na złych danych. - **Moduł**: nowe tokeny (SharePoint z `Sites.FullControl.All`, MCPS) poszerzają to, co moduł może zrobić na koncie - w granicy zaufania z spec 15 (rebutal R4 dokumentu projektowego); okno pokazuje nowe zakresy przy ponownej zgodzie. Najmniejsze uprawnienia: kalendarz przez middle tier, którego token moduł już ma, zamiast Graph, którego token klienta Teams niesie m.in. `Mail.ReadWrite` i `Mail.Send` (live check). - **Wzorzec zakresu WAM**: obejmuje tylko jedną etykietę pod `sharepoint.com`; inne domeny i głębsze nazwy odrzucone, więc moduł nie wyłudzi tokenu dla obcych usług. +- **Właściciel nagrania** (często inna osoba niż użytkownik): open_mess nie zostawia u niego śladu wyświetlenia (D3); droga pobrania przechodzi bramkę D3 z kroku 0, a jej zmiana wraca do review. - **Inne organizacje**: transkrypcje spotkań cudzych organizacji nie są pobierane (błąd). Token SharePoint jest proszony wyłącznie dla dwóch originów organizacji użytkownika wyprowadzonych z jego własnej witryny OneDrive (4.2 krok 4), więc ani `ownerTenantId`, ani `location` z odpowiedzi MCPS nie wskażą odbiorcy tokenu (recenzja R7). - **Granica twardnienia**: jak spec 15 (bez obrony przed złośliwym kodem na koncie Windows). @@ -188,3 +191,4 @@ Token SharePoint ma odbiorcę zależnego od organizacji (`https://-my.sh - R8 (Codex REJECT, GLM APPROVE): zasada dla wartości serwera poza `untrusted` - ustalony zbiór, przepisany format albo żeton base64url (Codex P2); czasy kalendarza sprowadzane do UTC z kontrolą w kroku 0, odwołane spotkania pomijane (GLM P3). - R9 (Codex REJECT, GLM APPROVE): spotkania prywatne bez znacznika w odpowiedzi - rebutal w sekcji 12 (Codex P2); pola elementu dla starszej transkrypcji `Transcript`, próg 900 KiB przed filtrem uprawnień opisany wprost (GLM P3). - R10 (Codex APPROVE, GLM APPROVE): koniec pętli recenzji. Po R10: drugi odczyt `cTag` po pobraniu VTT (Codex P2 w recenzji commita 4bf20c2); limit 200 transkrypcji w wyniku, duplikat nazwy narzędzia w jednym manifeście nie jest wystawiany (GLM P3). Uwaga GLM P3 o treści serwera poza `untrusted` w wynikach narzędzi dodatkowych bez ścisłego parsowania w rdzeniu - przyjęta bez zmian: moduł jest zaufany dla swojego konta (spec 15), a zasada z sekcji 4 wiąże moduł Teams. +- R11 (Codex APPROVE, GLM APPROVE): decyzje właściciela z 2026-10-28 w sekcji 10 - D1 lokalna transkrypcja w osobnym specu, D2 (a) bez dodatkowej bramki (sekcje 2, 6, krok 5 bez czekania na decyzję), D3 pobranie nie może liczyć się jako wyświetlenie: bramka D3 w kroku 0 (REA, liczniki analityki tylko na nagraniu użytkownika, kontrola, droga zastępcza albo stop), warunek w 4.2 krok 6, kryterium akceptacji i wpis w threat model. From fc631e363b458e8cdcf4808126c90c48a457b7f7 Mon Sep 17 00:00:00 2001 From: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:21:02 +0100 Subject: [PATCH 02/83] List modules' read tools and allow WAM scope patterns Stage 2 needs the Teams module's own tools to reach agents, and a SharePoint token whose audience is the user's own tenant, which no manifest built once for everyone can spell out. The registry now lists the read tools of active modules as _, with an optional account the core adds and resolves. The core checks the arguments against the declared schema, the read permission before the call and again after it for the same account, drops items of conversations the agent may not read, and takes only a JSON object of at most 1 MiB back. Write tools, bad names, names taken twice or by a core tool, schemas the core cannot check and schemas declaring account are not listed; status says why. Every MCP session hears tools/list_changed when the list changes, from a scan or from the host stopping a module after crashes. A WAM scope may now be https://*./.default, checked at load like a host pattern; it covers one lowercase DNS label on the module's host list and nothing else. Literal scopes compare as before. The scope check now runs before the tests' fake WAM too, and the approval window says what the SharePoint pattern hands over. --- crates/open-mess-host/src/hosts.rs | 21 +- crates/open-mess-host/src/runtime.rs | 7 + crates/open-mess-host/src/sign_in.rs | 108 +++++++++- crates/open-mess-host/tests/load.rs | 12 ++ crates/open-mess-host/tests/sign_in.rs | 42 ++++ crates/open-mess/src/api.rs | 5 +- crates/open-mess/src/daemon.rs | 1 + crates/open-mess/src/modules.rs | 31 ++- crates/open-mess/src/tools/extra.rs | 285 +++++++++++++++++++++++++ crates/open-mess/src/tools/mod.rs | 138 +++++++++++- crates/open-mess/src/ui/commands.rs | 14 +- crates/open-mess/tests/channel.rs | 75 ++++++- crates/open-mess/tests/modules.rs | 14 +- crates/open-mess/tests/shared_tools.rs | 104 +++++++++ crates/open-mess/tests/ui.rs | 8 +- modules/test-echo/src/lib.rs | 70 +++++- wit/module.wit | 13 +- 17 files changed, 898 insertions(+), 50 deletions(-) create mode 100644 crates/open-mess/src/tools/extra.rs diff --git a/crates/open-mess-host/src/hosts.rs b/crates/open-mess-host/src/hosts.rs index dc3b8ad..d25dad0 100644 --- a/crates/open-mess-host/src/hosts.rs +++ b/crates/open-mess-host/src/hosts.rs @@ -20,20 +20,23 @@ fn normalize(name: &str) -> Result { Ok(host.trim_end_matches('.').to_string()) } +/// The base of a `*.` pattern, normalized; it must sit below a registrable domain of the +/// public suffix list, so `*.com`, `*.co.uk` or `*.github.io` are refused (spec 8.3, R7). +pub(crate) fn pattern_base(pattern: &str, base: &str) -> Result { + let base = normalize(base)?; + if psl::domain_str(&base).is_none() { + return Err(HostError::Load(format!("pattern {pattern:?} is not below a registrable domain"))); + } + Ok(base) +} + impl HostList { - /// Parses a manifest's host list. A `*.` pattern must sit below a registrable domain of the - /// public suffix list, so `*.com`, `*.co.uk` or `*.github.io` are refused (spec 8.3, R7). + /// Parses a manifest's host list: exact names and `*.` patterns ([`pattern_base`]). pub fn parse(patterns: &[String]) -> Result { let mut list = HostList::default(); for pattern in patterns { match pattern.strip_prefix("*.") { - Some(base) => { - let base = normalize(base)?; - if psl::domain_str(&base).is_none() { - return Err(HostError::Load(format!("host pattern {pattern:?} is not below a registrable domain"))); - } - list.suffixes.push(format!(".{base}")); - } + Some(base) => list.suffixes.push(format!(".{}", pattern_base(pattern, base)?)), None => list.exact.push(normalize(pattern)?), } } diff --git a/crates/open-mess-host/src/runtime.rs b/crates/open-mess-host/src/runtime.rs index dcd064e..618576e 100644 --- a/crates/open-mess-host/src/runtime.rs +++ b/crates/open-mess-host/src/runtime.rs @@ -341,6 +341,7 @@ impl Engine { let hosts = HostList::parse(&manifest.hosts)?; // Not used until the sign-in window (milestone E), but a bad manifest is refused now. HostList::parse(&manifest.sign_in_hosts)?; + crate::sign_in::check_scopes(manifest.wam.as_ref())?; actor.reset(Arc::new(ModuleEnv { name: manifest.name.clone(), data_dir, @@ -398,6 +399,12 @@ impl ModuleHandle { self.state.borrow().clone() } + /// The module's state as the host changes it (a crash, a stop after crashes in a row); closed + /// once the module is gone. + pub fn state_changes(&self) -> watch::Receiver { + self.state.clone() + } + /// How many times the module crashed since it was loaded. A good call does not reset it (unlike /// the crashes in a row that disable a module); only loading the module again does. pub fn crashes_since_load(&self) -> u32 { diff --git a/crates/open-mess-host/src/sign_in.rs b/crates/open-mess-host/src/sign_in.rs index 2e6fe80..aa5b49d 100644 --- a/crates/open-mess-host/src/sign_in.rs +++ b/crates/open-mess-host/src/sign_in.rs @@ -20,6 +20,8 @@ use tokio::sync::{mpsc, oneshot}; use crate::actor::Deadline; use crate::bindings::open_mess::module::sign_in::{Host, SignInError}; use crate::bindings::open_mess::module::types; +use crate::error::HostError; +use crate::hosts::{self, HostList}; use crate::runtime::{HostState, WamIdentity}; use crate::storage::Storage; use crate::wam::{self, TokenRequest, WamAccount}; @@ -188,21 +190,53 @@ async fn blocking(call: impl FnOnce() -> Result(identity: Option<&'a WamIdentity>, scopes: &[String]) -> Result<&'a WamIdentity, SignInError> { +fn allowed<'a>(identity: Option<&'a WamIdentity>, hosts: &HostList, scopes: &[String]) -> Result<&'a WamIdentity, SignInError> { let identity = identity.ok_or_else(|| SignInError::Refused("the module's manifest declares no Windows account identity".to_string()))?; if scopes.is_empty() { return Err(SignInError::Refused("no scope asked for".to_string())); } - match scopes.iter().find(|scope| !identity.scopes.contains(scope)) { + match scopes.iter().find(|scope| !identity.scopes.iter().any(|approved| covers(approved, scope, hosts))) { Some(scope) => Err(SignInError::Refused(format!("scope {scope:?} is not in the module's manifest"))), None => Ok(identity), } } +/// The base of a scope pattern `https://*./.default`, else None. +fn pattern_base(scope: &str) -> Option<&str> { + scope.strip_prefix("https://*.")?.strip_suffix("/.default") +} + +/// Whether an approved scope covers one asked for. A scope without `*` is compared as it is (its +/// audience may be outside the host list, like `https://api.spaces.skype.com`); a pattern +/// `https://*./.default` covers `https://