@framers/agentos-ext-meeting-browser 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +173 -2
- package/dist/BrowserMeetingProvider.d.ts +74 -0
- package/dist/BrowserMeetingProvider.d.ts.map +1 -0
- package/dist/BrowserMeetingProvider.js +136 -0
- package/dist/BrowserMeetingProvider.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +294 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +81 -0
- package/dist/protocol.js.map +1 -0
- package/dist/scheduler/channel.d.ts +45 -0
- package/dist/scheduler/channel.d.ts.map +1 -0
- package/dist/scheduler/channel.js +122 -0
- package/dist/scheduler/channel.js.map +1 -0
- package/dist/scheduler/docker.d.ts +86 -0
- package/dist/scheduler/docker.d.ts.map +1 -0
- package/dist/scheduler/docker.js +35 -0
- package/dist/scheduler/docker.js.map +1 -0
- package/dist/scheduler/main.d.ts +3 -0
- package/dist/scheduler/main.d.ts.map +1 -0
- package/dist/scheduler/main.js +79 -0
- package/dist/scheduler/main.js.map +1 -0
- package/dist/scheduler/runs.d.ts +75 -0
- package/dist/scheduler/runs.d.ts.map +1 -0
- package/dist/scheduler/runs.js +123 -0
- package/dist/scheduler/runs.js.map +1 -0
- package/dist/scheduler/server.d.ts +177 -0
- package/dist/scheduler/server.d.ts.map +1 -0
- package/dist/scheduler/server.js +653 -0
- package/dist/scheduler/server.js.map +1 -0
- package/dist/scheduler/urls.d.ts +28 -0
- package/dist/scheduler/urls.d.ts.map +1 -0
- package/dist/scheduler/urls.js +49 -0
- package/dist/scheduler/urls.js.map +1 -0
- package/manifest.json +25 -0
- package/package.json +73 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
10
|
+
|
|
11
|
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
12
|
+
|
|
13
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
|
14
|
+
|
|
15
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
16
|
+
|
|
17
|
+
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
18
|
+
|
|
19
|
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
|
20
|
+
|
|
21
|
+
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
|
22
|
+
|
|
23
|
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
|
24
|
+
|
|
25
|
+
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
|
26
|
+
|
|
27
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
|
28
|
+
|
|
29
|
+
2. Grant of Copyright License.
|
|
30
|
+
|
|
31
|
+
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
32
|
+
|
|
33
|
+
3. Grant of Patent License.
|
|
34
|
+
|
|
35
|
+
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
|
36
|
+
|
|
37
|
+
4. Redistribution.
|
|
38
|
+
|
|
39
|
+
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
|
40
|
+
|
|
41
|
+
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
|
42
|
+
|
|
43
|
+
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
|
44
|
+
|
|
45
|
+
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
|
46
|
+
|
|
47
|
+
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
|
48
|
+
|
|
49
|
+
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
|
50
|
+
|
|
51
|
+
5. Submission of Contributions.
|
|
52
|
+
|
|
53
|
+
Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
|
54
|
+
|
|
55
|
+
6. Trademarks.
|
|
56
|
+
|
|
57
|
+
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
|
58
|
+
|
|
59
|
+
7. Disclaimer of Warranty.
|
|
60
|
+
|
|
61
|
+
Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
|
62
|
+
|
|
63
|
+
8. Limitation of Liability.
|
|
64
|
+
|
|
65
|
+
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
|
66
|
+
|
|
67
|
+
9. Accepting Warranty or Additional Liability.
|
|
68
|
+
|
|
69
|
+
While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
|
70
|
+
|
|
71
|
+
END OF TERMS AND CONDITIONS
|
|
72
|
+
|
|
73
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
74
|
+
|
|
75
|
+
To apply the Apache License to your work, attach the following
|
|
76
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
77
|
+
replaced with your own identifying information. (Don't include
|
|
78
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
79
|
+
comment syntax for the file format. We also recommend that a
|
|
80
|
+
file or class name and description of purpose be included on the
|
|
81
|
+
same "printed page" as the copyright notice for easier
|
|
82
|
+
identification within third-party archives.
|
|
83
|
+
|
|
84
|
+
Copyright (c) 2025 Framers
|
|
85
|
+
|
|
86
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
87
|
+
you may not use this file except in compliance with the License.
|
|
88
|
+
You may obtain a copy of the License at
|
|
89
|
+
|
|
90
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
91
|
+
|
|
92
|
+
Unless required by applicable law or agreed to in writing, software
|
|
93
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
94
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
95
|
+
See the License for the specific language governing permissions and
|
|
96
|
+
limitations under the License.
|
|
97
|
+
|
|
98
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,174 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Browser meeting bot provider
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This pack gives the AgentOS meeting-participant channel (`@framers/agentos/io/channels/meetings`) a provider for Google Meet and Microsoft Teams. The provider hands each run to the scheduler on a worker machine over HTTP and authenticates the reports that scheduler forwards by the run's worker token. It never drives a browser itself.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @framers/agentos-ext-meeting-browser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependency: `@framers/agentos` (`>=0.13.82`, the first release that exports `@framers/agentos/io/channels/meetings`).
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { EXTENSION_KIND_MEETING_PROVIDER } from '@framers/agentos';
|
|
17
|
+
import type { IMeetingBotProvider } from '@framers/agentos/io/channels/meetings';
|
|
18
|
+
import { createExtensionPack } from '@framers/agentos-ext-meeting-browser';
|
|
19
|
+
|
|
20
|
+
// The client credential comes from MEETING_SCHEDULER_TOKEN when no option or secret gives it.
|
|
21
|
+
const pack = createExtensionPack({
|
|
22
|
+
options: { schedulerUrl: 'https://scheduler.example.com', workerId: 'worker-1' },
|
|
23
|
+
});
|
|
24
|
+
const descriptor = pack.descriptors.find((d) => d.kind === EXTENSION_KIND_MEETING_PROVIDER);
|
|
25
|
+
manager.registerProvider(descriptor!.payload as IMeetingBotProvider); // the host's MeetingManager
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
`createExtensionPack` throws when `schedulerUrl`, `workerId` or the client credential is missing. The credential resolves from `schedulerToken`, then the secret `meetings.schedulerToken`, then the environment variable that `schedulerTokenEnv` names.
|
|
31
|
+
|
|
32
|
+
| Option | Required | Default | What it sets |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `schedulerUrl` | Yes | None, throws if absent | The scheduler's base url on the worker |
|
|
35
|
+
| `workerId` | Yes | None, throws if absent | The worker's id, kept on each run's dispatch record |
|
|
36
|
+
| `schedulerToken` | Yes, here or as the secret or the environment variable | The secret `meetings.schedulerToken`, then the environment | The client credential the scheduler accepts |
|
|
37
|
+
| `schedulerTokenEnv` | No | `MEETING_SCHEDULER_TOKEN` | The environment variable read for the credential |
|
|
38
|
+
| `botIdentity` | No | `bot` | The identity the bot publishes under in the run's LiveKit room |
|
|
39
|
+
| `platforms` | No | `['google_meet', 'microsoft_teams']` | The platforms this worker serves |
|
|
40
|
+
| `fetchImpl` | No | The global `fetch` | The `fetch` the provider calls the scheduler with |
|
|
41
|
+
| `priority` | No | `50` | The descriptor's priority |
|
|
42
|
+
|
|
43
|
+
The pack returns one descriptor of kind `meeting-provider`, `browserMeetingProvider`, whose payload is a `BrowserMeetingProvider` named `browser`.
|
|
44
|
+
|
|
45
|
+
## What the provider does
|
|
46
|
+
|
|
47
|
+
- **Join links.** `checkJoinUrl` accepts `https://meet.google.com/xxx-xxxx-xxx` links and Microsoft Teams join links on `teams.microsoft.com` (`/l/meetup-join/...`) and `teams.live.com` (`/meet/...`). It refuses every other host, an address literal included, any scheme but `https`, a link that carries credentials, and a platform the worker does not serve.
|
|
48
|
+
- **Dispatch.** `dispatch` posts the run to the scheduler's `POST /runs` with the run's link, bot name, notice, stop phrase and limits, the room's url and publish token, the per-run worker token and the report url. A `201` dispatches the run, a `409` throws `NoCapacityError`, and any other status throws.
|
|
49
|
+
- **Commands.** `command` posts `startCapture`, `suspendCapture`, `postChat`, `leave` or `kill` to `POST /runs/:id/command`. A `404` means the scheduler no longer holds the run, and is not an error.
|
|
50
|
+
- **Usage.** `usage` reads `GET /runs` and answers each run asked for (every run the scheduler lists when none is named) with `alive` (the scheduler lists it in a state other than `exited`) and the seconds the scheduler counted.
|
|
51
|
+
- **Deletion.** `deleteRun` calls `DELETE /runs/:id` and answers `{ deleted: false }`: the provider keeps no copy of a run. A `404` means the scheduler no longer holds the run. Any other refusal throws, so the purge that called it tries again.
|
|
52
|
+
- **Reports.** The scheduler posts each report to the run's report url as a `ReportPost` under `Authorization: Bearer <worker token>`. `verifyEvent` accepts a report whose bearer token hashes to the run's `dispatch.workerTokenHash`. `parseEvent` returns one event whose id is `browser:<run>:<seq>:<event>`, so a report delivered twice keeps one id.
|
|
53
|
+
|
|
54
|
+
Every request to the scheduler carries `Authorization: Bearer <schedulerToken>` and follows no redirect.
|
|
55
|
+
|
|
56
|
+
The provider declares itself a participant that posts and reads the chat, publishes the call's audio into the run's LiveKit room, records and transcribes nothing, keeps nothing, and leaves on the stop phrase in the chat: `{ joinsAsParticipant: true, postsChat: true, readsChat: true, audioPath: 'livekit_room', recordsAudio: false, transcribes: false, keeps: 'nothing', stopPath: 'chat' }`.
|
|
57
|
+
|
|
58
|
+
## The protocol
|
|
59
|
+
|
|
60
|
+
`src/protocol.ts` holds the zod schemas of the messages between the scheduler and a bot container (`JobMessage`, `CommandMessage`, `AckMessage`, `ReportMessage` and their union `ChannelMessage`) and of the scheduler's HTTP API (`StartRunRequest`, `ReportPost`, `RunStatus`, `Capacity`), with `reportEventId`. The package root exports them all.
|
|
61
|
+
|
|
62
|
+
## The scheduler
|
|
63
|
+
|
|
64
|
+
The scheduler runs on each worker machine beside Docker. It leases a slot per run, starts one bot container per run under a fixed policy, holds each container's command channel, relays the provider's commands down it, and forwards the container's reports to the run's report url. The meeting manager's job store stays the source of truth: the scheduler holds only the runs it was given.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
MEETING_WORKER_ID=worker-1 MEETING_SLOTS=2 MEETING_CLIENT_TOKEN_FILE=/srv/meeting/secrets/client \
|
|
68
|
+
MEETING_BOT_IMAGE=ghcr.io/framerslab/agentos-meeting-bot:0.1.0 MEETING_LISTEN=127.0.0.1:8788,10.90.0.1:8788 \
|
|
69
|
+
MEETING_CHANNEL_URL=ws://10.90.0.1:8788/channel MEETING_STATE_ROOT=/srv/meeting/secure/state \
|
|
70
|
+
MEETING_PROFILES_ROOT=/srv/meeting/secure/profiles MEETING_SECCOMP_PROFILE=/srv/meeting/seccomp.json \
|
|
71
|
+
agentos-meeting-scheduler
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Variable | Required | What it sets |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `MEETING_WORKER_ID` | Yes | The worker's id: on every forwarded report, in `GET /capacity`, and on its containers' labels |
|
|
77
|
+
| `MEETING_SLOTS` | Yes | How many runs the worker holds at once |
|
|
78
|
+
| `MEETING_CLIENT_TOKEN_FILE` | Yes | A file holding the client credential (16 characters or more) the provider presents; the value itself never sits in the environment |
|
|
79
|
+
| `MEETING_BOT_IMAGE` | Yes | The bot image, by tag or digest |
|
|
80
|
+
| `MEETING_LISTEN` | Yes | The addresses it listens on, `host:port` separated by commas (`[::1]:8788` for IPv6) |
|
|
81
|
+
| `MEETING_CHANNEL_URL` | No | The channel url the containers reach; default `ws://<first address>/channel` |
|
|
82
|
+
| `MEETING_STATE_ROOT` | No | The folder of the runs' state files, on an encrypted volume; without it a restart takes no run back |
|
|
83
|
+
| `MEETING_PROFILES_ROOT` | No | The identity pool's folder; without it a Google Meet run joins with a fresh browser profile |
|
|
84
|
+
| `MEETING_SECCOMP_PROFILE` | No | A seccomp profile file; the scheduler sends its JSON with each container's security options |
|
|
85
|
+
| `MEETING_RESOLVE_ALLOW` | No | Test hosts, separated by commas, whose links it takes over `http` at any address; empty in production |
|
|
86
|
+
|
|
87
|
+
It logs JSON lines through AgentOS's `createLogger`. The lines carry run ids, states, reasons and counts, never a meeting link, a name, a chat line or a token. `SIGTERM` stops it and leaves the containers running for the next start to take back.
|
|
88
|
+
|
|
89
|
+
TLS belongs to the reverse proxy in front of the scheduler. Every route but `GET /health` takes the client credential as a bearer token:
|
|
90
|
+
|
|
91
|
+
| Route | Answer |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `GET /health` | `200` with the worker id |
|
|
94
|
+
| `GET /capacity` | The `Capacity`: the slots, the busy count and the pause flag |
|
|
95
|
+
| `GET /runs` | A `RunStatus` per run it holds; an exited run stays listed for an hour after its exit, its seconds stopped at the exit |
|
|
96
|
+
| `POST /runs` | `201` with the run's `RunStatus`, also for the same run sent again with its token; `400` for a refused link or body; `409` when paused, when the slots are full, or when a Google Meet run finds no free identity; `413` for a body over 64 KiB; `503` when another start of the same run got there first |
|
|
97
|
+
| `POST /runs/:id/command` | The container's acknowledgement (`200`, or `500` when it refused or did not answer within 10 seconds); `kill` stops the container itself and answers `200`; a `leave` before the bot has opened its channel stops its container, and one while a reconnecting bot's channel is down answers `202` and goes down the next channel; `409` for any other command before the bot has opened its channel; `404` for a run it does not hold, and for any command but `kill` once the run has ended |
|
|
98
|
+
| `DELETE /runs/:id` | Stops and removes the container and forgets the run; nothing is reported for it afterwards |
|
|
99
|
+
|
|
100
|
+
**The join link.** A link must be a Google Meet or Microsoft Teams join link (the provider's `classifyJoinUrl`), and every address its host resolves to must pass AgentOS's `isPublicNetworkAddress` (from `@framers/agentos/io/media/images`, the rule AgentOS's own guarded fetches apply): loopback, private, link-local, carrier-grade NAT, documentation, benchmarking, multicast, reserved and unallocated ranges are refused, and so is an IPv6 address that carries a refused IPv4 one (IPv4-mapped, NAT64, 6to4), whatever its spelling. A host named in `MEETING_RESOLVE_ALLOW` is taken over `http` or `https` at any address, names its platform in its `platform` query parameter, and its job carries `adapter: 'fixture'`, so the bot drives the fixture page's adapter.
|
|
101
|
+
|
|
102
|
+
**The container.** Nothing customer-supplied reaches Docker. A container gets the image and limits of the scheduler's configuration, the bots' network (`meeting-bots`), no capabilities, a read-only root with `tmpfs` scratch folders, `no-new-privileges` and the seccomp profile, and only two environment variables: the channel url and the run id. The run's token goes to the container's stdin as one line, and stdin is then closed, so `docker inspect` never shows it. The job, the meeting link among it, travels over the command channel.
|
|
103
|
+
|
|
104
|
+
**The command channel.** The bot opens `<channel url>` with `Authorization: Bearer <its run's token>`. A token no live run holds gets `401`, and a second channel for one run is closed with `4409`. The job is the channel's first message on every channel the bot opens, so a bot that reconnects after a restart picks up where it was; a leave taken while it was away goes down its next channel. Each run's reports go to its report url one at a time, in the order they came, under `Authorization: Bearer <run token>`. A post is tried three times with a 10-second timeout each, and a `4xx` answer other than `429` is not tried again.
|
|
105
|
+
|
|
106
|
+
**The identity pool.** Google Meet joins as a signed-in identity: each identity is a folder under `MEETING_PROFILES_ROOT` holding its browser profile, mounted read-write at `/home/bot/profile`. A run takes the first folder, in name order, that holds a `provisioned` file, holds no `needs_reauth` file, and no live run holds: one run per identity. A bot that finds its identity signed out reports the error `needs_reauth`, and the scheduler writes `needs_reauth` in that folder, so the identity leaves the pool until it is signed in again. Microsoft Teams joins as a guest with no profile.
|
|
107
|
+
|
|
108
|
+
**Restarts.** With `MEETING_STATE_ROOT`, each run's material (its job, its token, its report url, its start time, its report sequence) is written to `<run id>.json` (mode `0600`) as it changes, and deleted with the run. At start the scheduler takes back every run whose state file and container it finds and watches the container's exit again. A container of this worker with no state file is stopped and removed. A state file whose container is gone is reported `crashed`.
|
|
109
|
+
|
|
110
|
+
**Ends and kills.** A container's exit is reported `done` (exit code 0) or `crashed`, unless the bot reported its own end first. A run told to leave that has not exited 30 seconds later, a bot that has not opened its channel two minutes after its start, and a run a minute past its whole length are killed, and the exit reports each as `crashed`. A dispatch sent again with the run's token answers the run it started; one with a new token replaces the earlier container, and nothing is reported for that container.
|
|
111
|
+
|
|
112
|
+
## The bot image
|
|
113
|
+
|
|
114
|
+
`image/` builds the container the scheduler starts for each run, from this folder (`docker build -f image/Dockerfile .`), since the bot shares `src/protocol.ts` with the scheduler: Debian 12 with Node 24, Chromium through Playwright 1.63.0, PulseAudio, Xvfb, `@livekit/rtc-node`, and the bot program in `image/src/`. One container is one run.
|
|
115
|
+
|
|
116
|
+
- **Start.** `entrypoint.sh` starts PulseAudio on `image/pulse/default.pa` alone: a null sink `meeting`, the browser's output, whose monitor the bot publishes, and a null sink `silence`, whose monitor is the browser's microphone. The root file system is read-only, so PulseAudio's runtime and state folders and its cookie are made under the `/run/pulse` tmpfs as the bot user, and Chromium's config folder (`XDG_CONFIG_HOME`), where its crash handler keeps its database and without which the browser does not start, is `/tmp/.config` in the `/tmp` tmpfs. Then Xvfb on display `:99`, then `bot.js`, which reads the run's token from stdin and opens the command channel at `MEETING_CHANNEL_URL` with it.
|
|
117
|
+
- **The run.** The job is the channel's first message. The bot joins the run's LiveKit room without publishing, launches Chromium on the identity's profile at `/home/bot/profile` when the run has one (a Google Meet identity from the pool) or on a fresh profile, and drives the platform's adapter: it reports `joining`, `waiting_room` and `in_call`, posts the notice, reads it back from the chat panel and reports `notice_posted`, then reads the panel every second. A `monitor_heartbeat` goes every ten seconds, and only while the panel was read through in the last ten seconds.
|
|
118
|
+
- **The audio.** The call's audio is published into the room only on `startCapture`: a 48 kHz mono microphone track read from the `meeting` monitor with `parec`, with nothing written to disk. Thirty seconds without a good read of the chat suspend the track inside the container, whatever the manager's clock says, and `startCapture` is refused while the read is that stale, before the notice's chat has been read, or while the bot is leaving. Starts and suspends run one at a time, in the order they come: a `startCapture` that arrives while another is in flight publishes nothing more, so two commands close together publish one track and run one `parec`, and a suspend that arrives while a start is in flight waits for it, then stops the track and the capture that start began.
|
|
119
|
+
- **The chat and the stop.** Each message the bot did not post itself is reported as `chat_message`. A message that holds the stop phrase is also reported as `stop_command`, and the bot leaves on it at once with "Leaving now; everything I took is being deleted.". The bot's own posts, the notice that names the stop phrase among them, are never reported: the bot knows them by the texts it posted, never by a sender's name, so a participant whose name is You is heard like any other. A message is remembered by the client's id for it, or, when its element carries none, by the element and its text, so the same text sent twice is reported twice.
|
|
120
|
+
- **The limits.** The bot leaves when the waiting room's limit passes (`not_admitted`), when the client's participant count shows nobody else for the alone limit (`alone`), and at the run's whole length (`cap`); the manager's clock is the backstop. A count the client does not show is unknown, never zero. The bot reports `participant_joined` and `participant_left` when the count changes, and `usage` once a minute.
|
|
121
|
+
- **Leaving.** On `leave` the bot suspends the track, posts its farewell, presses Leave, reports `left` with the reason and `done` with the seconds since its start, and exits with code 0. On `kill` it exits at once. A browser or a page that dies under the bot, a step that fails or a signed-out identity ends the run at once: the bot reports `left` (`crashed`) and `crashed` with its exit code, and exits with that code.
|
|
122
|
+
- **The network.** Chromium runs with service workers blocked, since a service worker's requests pass Playwright's routing by, and a route on every request: `https` only, no address literal as a host, the page's top-level document on the platform's join hosts alone, and every other request, a frame's document included, on the platform's hosts. Blocked requests are counted by host and reported once at the end, as the error `network_blocked: <host>x<count>,...`. WebSocket and media connections are the worker's firewall's.
|
|
123
|
+
- **Errors.** An error report carries a phase and a content-free code (`join: join_control_missing`, `browser: TimeoutError`), never a library's message, which can quote the meeting's link or a page's text. A signed-in Google Meet identity that Google signed out or challenges reports `needs_reauth` before it asks to join, and the scheduler takes it out of the pool.
|
|
124
|
+
- **The sandbox.** Chromium runs without its own sandbox, which needs user namespaces that the container's dropped capabilities and seccomp profile refuse; the container is the boundary. `image/seccomp.json` is Docker's default seccomp profile, unchanged (`seccomp/default.json` of moby/profiles at `6fe7deb1`); the scheduler sends it with each container when `MEETING_SECCOMP_PROFILE` names it.
|
|
125
|
+
- **Logs.** The bot writes JSON lines through pino, each with the run's id: events, phases, reasons, codes and counts, never a link, a name or a chat line.
|
|
126
|
+
|
|
127
|
+
**The adapters.** `image/src/adapters/google-meet.ts` and `microsoft-teams.ts` each hold a selector table and a `selectorsVersion`, the date its selectors were last read against the live client, and the hosts the client may load. Every selector is one Playwright engine: CSS, a comma list included, or one `text=` selector. Their checks read what is visible, a read waits two seconds at most for its element, and the chat is read only while its panel is open. The Teams chat is read as open-source Teams clients read it: the composer is CKEditor's editable, which some builds draw inside `[data-tid="ckeditor"]` and others as that element itself, and the adapter types into either; a message is its card's body (`[data-tid="messageBodyContent"]`), apart from the author's name and the time the card also shows. `fixture.ts` runs the adapter of the platform the link's `platform` parameter names against the test fixture page, whose markup carries that platform's selectors; the bot takes it only for a job marked `adapter: 'fixture'`, and its network policy admits the fixture's own host alone, over plain `http` too.
|
|
128
|
+
|
|
129
|
+
## Tests
|
|
130
|
+
|
|
131
|
+
`test/provider.spec.ts` runs the provider against a stubbed `fetch`: the declaration, the join-link checks, dispatch and the `409`, report verification and parsing, commands, deletion and its refusals, and usage. `test/protocol.spec.ts` parses each channel message, refuses an unknown report event and checks the event ids. `test/scheduler.spec.ts` runs the scheduler on an ephemeral port with a fake Docker (`test/fakes/docker.ts`), a fake product that records every report (`test/fakes/product.ts`) and real `ws` sockets: the credential, the slots and the container policy, the join-link checks, the channel's job, commands and reports, a second channel, exits, the bot's own end, restarts and stray containers, the kills, capacity and pausing, the fixture host, a dispatch sent again, deletion, a leave taken while the bot was away, report order and retries, the identity pool, refused bodies, start failures and the listen addresses. `image/src/__tests__/adapters.test.ts` checks the bot's adapters with no browser: every selector in the Google Meet and Microsoft Teams tables is one Playwright engine, the network policy's rule on each platform's hosts, schemes and address literals, and the fixture adapter's platform, code and hosts. `image/src/__tests__/audio.test.ts` runs the audio publisher against fakes of `@livekit/rtc-node` and `parec` whose publish waits until the test answers it: two starts at once publish one track and start one capture, and a suspend that comes while a start waits stops the track and the capture that start began. CI runs all five with the pack's tests.
|
|
132
|
+
|
|
133
|
+
**The lifecycle test.** `image/fixture/` is a meeting page that carries the Google Meet adapter's selectors: a pre-join screen (the name field, the camera and microphone toggles, Ask to join), a waiting screen, the call's tiles, the participant count, the chat panel and the Leave key, and an `<audio>` element that plays a known recording into the page's output, which is the call's audio. With `platform=microsoft_teams` in its link it carries the Microsoft Teams adapter's selectors instead, and draws the Teams chat as open-source Teams clients read it: a message card (`[data-tid="chat-pane-message"]`) holds its author, its time and its body (`[data-tid="messageBodyContent"]`), and the composer is CKEditor's editable inside `[data-tid="ckeditor"]`, or that element itself with `composer=self`, the two shapes Teams builds show. Its server, `serve.mjs` on port 8787, queues scripted events from `POST /control` (`admit`, `late_joiner`, `chat` with a sender and a text, and `noId` for a Meet message drawn without `data-message-id`, `hide_chat`, `show_chat`, `leave_all`) for the page to poll, and keeps what the page did on `GET /state`: its knocks, the texts the bot posted, its presses of Leave. The recording is spoken from `recording.txt` by espeak-ng when the fixture's image is built: about twenty seconds of 16 kHz mono speech. `image/test/compose.yml` runs LiveKit 1.13.7 in development mode (its documented key pair, `devkey` and `secret`), the fixture, a fake product that records every report with a hash of the token it came under, and the scheduler on this pack's build, which starts real bot containers from `agentos-meeting-bot:ci` on the `meeting-bots` network under its fixed container policy, the seccomp profile included. `image/test/lifecycle.mjs` sends five runs through the scheduler's API and listens to each run's room with `@livekit/rtc-node`:
|
|
134
|
+
|
|
135
|
+
- The first run joins, waits in the waiting room, is admitted, posts the notice, reads it back and reports a heartbeat within 12 seconds of it. No audio frame reaches the room before `startCapture`.
|
|
136
|
+
- After `startCapture` the recording is heard in the room: above 0.01 RMS in at least three of five seconds, the first frame after `notice_posted`.
|
|
137
|
+
- With the chat panel hidden the heartbeats stop, the bot suspends its own capture within 45 seconds and the audio stops. With the panel shown the heartbeats come back and the capture starts again.
|
|
138
|
+
- A late joiner is reported as `participant_joined`, and the short notice is posted.
|
|
139
|
+
- A participant's stop phrase is reported as `chat_message` and `stop_command`. The bot posts "Leaving now; everything I took is being deleted.", presses Leave, and reports `left` (`stopped_by_participant`) and `done`. No audio reaches the room after `left`, and the container is removed.
|
|
140
|
+
- A second run, admitted and confirmed but never sent `startCapture`, publishes no track for 15 seconds, then leaves on the `leave` command with its reason.
|
|
141
|
+
- A third run, sent two `startCapture` commands at once, publishes one track. The same short message sent twice by messages without a `data-message-id` gives two `chat_message` reports, and the stop phrase from a participant whose name is You is obeyed.
|
|
142
|
+
- A Microsoft Teams run with the chat's editable inside `[data-tid="ckeditor"]`, and another with that element itself as the editable, each post the notice and read it back, report the same short message sent twice as two `chat_message` reports, report each message as its body with its author, and leave on the stop phrase.
|
|
143
|
+
- `docker inspect` of each live container shows neither its meeting link nor a token, and shows no bind mount, the browser profile in a tmpfs, a read-only root, no capabilities and the seccomp profile.
|
|
144
|
+
- Every report reaches the product under its run's token.
|
|
145
|
+
|
|
146
|
+
Each step prints one line. The first failed check of the first two runs and of the last two checks prints the reports received so far, by sequence number, event and time, and exits 1. The third run and the two Teams runs go on past a failed check where their run allows, and the test then fails at its end naming every check of theirs that failed. From this folder, once the pack is built:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
docker build -f image/Dockerfile -t agentos-meeting-bot:ci .
|
|
150
|
+
openssl rand -hex 24 > image/test/client-token.txt
|
|
151
|
+
docker compose -f image/test/compose.yml up -d --build
|
|
152
|
+
npm install --prefix image/test --no-audit --no-fund
|
|
153
|
+
node image/test/lifecycle.mjs
|
|
154
|
+
docker compose -f image/test/compose.yml down -v
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## The image, the nightly check and the measurement in CI
|
|
158
|
+
|
|
159
|
+
**The bot image** ([`meeting-bot-image.yml`](https://github.com/framerslab/agentos-extensions/blob/master/.github/workflows/meeting-bot-image.yml), "Meeting bot image") runs on every pull request that touches this folder. It builds the pack, checks that `image/seccomp.json` is byte for byte `seccomp/default.json` of moby/profiles at `6fe7deb1` and prints its sha256, builds the bot image, starts its Chromium under the scheduler's container policy with the image's own entrypoint and `image/test/browser-smoke.mjs` in the bot program's place (it prints Playwright's own error when the browser does not start, which the bot's content-free log does not), and runs the lifecycle test above against the compose stack. A push to `master` that touches this folder also pushes the image to GHCR as `ghcr.io/framerslab/agentos-meeting-bot:<version>`, the version this folder's `package.json` holds at that commit, and as `:sha-<commit>`, and prints the pushed digest. The push runs in a job of its own, the only one in the workflow that can write packages: it loads the image the test ran against, while the build and the test, which install dependencies and run their scripts, hold a read-only token. The version pull request's merge changes that `package.json`, so the released version's image is pushed by that merge. The image's dependencies are installed from `image/package-lock.json`; a branch without one gets one resolved by the workflow and offered as the artifact `image-lock`, to commit.
|
|
160
|
+
|
|
161
|
+
**The nightly real-join check** ([`nightly-real-join.yml`](https://github.com/framerslab/agentos-extensions/blob/master/.github/workflows/nightly-real-join.yml), daily at 03:17 UTC and by hand) joins meetings it makes in the team's own tenants, so no one else's meeting is touched. `image/test/nightly/create-meetings.mjs` makes a Google Calendar event with a Meet on a host user's calendar with the bot's identity invited (a service account with domain-wide delegation for the Calendar events scope) and a Microsoft Teams meeting for an organiser user that lets everyone past its lobby, since no person attends it (an app registration with the application permission `OnlineMeetings.ReadWrite.All` and an application access policy granted for that user). `image/test/nightly/real-join.mjs` refuses a Meet whose organiser is not the host user and a Teams link outside the team's tenant, sends the bot to each through the worker's scheduler, sends `startCapture` after the notice and a heartbeat and `leave` after a usage report, and writes per platform to `image/test/nightly/outcome.json` whether the bot was admitted, posted its notice, was heard (its capture started and a usage report followed) and left, with the seconds from its joining report to each. The run fails when a platform did not reach `in_call` or `notice_posted`; three failed nights in a row for one platform are the signal to pause it with the meeting manager's `pausePlatform` until a check passes. Both meetings are removed at the end. The scheduler posts the check's reports to a report sink beside it on the worker, `http://127.0.0.1:8789/report`, which serves a run's reports as a JSON array on `GET <scheduler url>/sink/<run id>` behind the scheduler's client credential. The check reads these repository secrets by name and skips, naming the missing ones, until they are set:
|
|
162
|
+
|
|
163
|
+
| Secret | What it is |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `MEETING_SCHEDULER_URL`, `MEETING_SCHEDULER_TOKEN` | the worker's scheduler and its client credential |
|
|
166
|
+
| `MEET_CALENDAR_SA_JSON`, `MEET_HOST_USER`, `MEET_BOT_IDENTITY` | the service account's key, the user whose calendar hosts the Meet, the bot identity's address |
|
|
167
|
+
| `TEAMS_GRAPH_TENANT_ID`, `TEAMS_GRAPH_CLIENT_ID`, `TEAMS_GRAPH_CLIENT_SECRET`, `TEAMS_ORGANIZER_USER_ID` | the app registration and the user the Teams meeting is made for |
|
|
168
|
+
| `NIGHTLY_LIVEKIT_URL`, `NIGHTLY_LIVEKIT_API_KEY`, `NIGHTLY_LIVEKIT_API_SECRET` | the LiveKit server the bots publish into, as the worker reaches it, and its key pair |
|
|
169
|
+
|
|
170
|
+
**The measurement** ([`measure-meeting-bots.yml`](https://github.com/framerslab/agentos-extensions/blob/master/.github/workflows/measure-meeting-bots.yml), "Measure meeting bots", by hand with `concurrency`, `minutes` and the transcriber's `price_per_min`) measures a worker's capacity on the worker itself. Over SSH (the secrets `WORKER_HOST` and `WORKER_SSH_KEY`) it copies the fixture, the seccomp profile and `image/test/measure/` to `/srv/meeting/measure` and runs `measure.sh` as the worker's `bots` user. That starts a stack of its own on the bots' network: LiveKit 1.13.7 with a key pair made for the run, one fixture per bot, and a scheduler of the newest published version of this pack with those fixtures as its test hosts, which starts the bots from the published image of that version. `measure-listen.mjs` sends each bot to its fixture, admits it, sends `startCapture` after the notice and a heartbeat, and listens to each run's room with AgentOS's `startListenSession` on `LiveKitStreamTransport` at 16 kHz with `DeepgramStreamingSTT` (its key read from `/srv/meeting/secrets/deepgram` on the worker). After the given minutes it writes `measurement.json`, the workflow's artifact `measurement`: each bot's memory and CPU (median and peak over the window, from `docker stats` every 15 seconds), the LiveKit server's, the harness's and the scheduler's load (also per concurrent run), the seconds sent to the transcriber and their cost at the given price, the word error rate of each run's transcript against `recording.txt` (a token-level edit distance against the looping recording), and the slots the worker carries with the bots at their peak under 70 percent of its CPU and memory. While it runs, the bots reach the stack's containers on the bridge: the fixtures on 8787, LiveKit on 7880, 7881 and 7882/udp, and the scheduler's channel on 8798. Neither runs on a pull request or a push: the nightly check runs on its schedule or by hand, the measurement by hand only.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
Apache-2.0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type DispatchInput, type DispatchResult, type IMeetingBotProvider, type MeetingBotCapabilities, type MeetingPlatform, type MeetingRunRecord, type ProviderCommand, type ProviderEventContext, type ProviderParseResult, type ProviderUsage, type ProviderVerificationResult } from '@framers/agentos/io/channels/meetings';
|
|
2
|
+
/** How the provider reaches a worker's scheduler. */
|
|
3
|
+
export interface BrowserMeetingProviderOptions {
|
|
4
|
+
/** The scheduler's base url on the worker (`https://…`). */
|
|
5
|
+
schedulerUrl: string;
|
|
6
|
+
/** The client credential the scheduler accepts from the product. */
|
|
7
|
+
schedulerToken: string;
|
|
8
|
+
/** The worker's id, as the scheduler reports it. */
|
|
9
|
+
workerId: string;
|
|
10
|
+
/** The identity the bot publishes under in the run's room. Default `bot`. */
|
|
11
|
+
botIdentity?: string;
|
|
12
|
+
/** The `fetch` the provider calls the scheduler with. Default the global `fetch`. */
|
|
13
|
+
fetchImpl?: typeof fetch;
|
|
14
|
+
/** Platforms this worker serves. Default both. */
|
|
15
|
+
platforms?: readonly MeetingPlatform[];
|
|
16
|
+
}
|
|
17
|
+
/** A join url's platform, by its host and path: no redirect is followed, no other host is accepted. */
|
|
18
|
+
export declare function classifyJoinUrl(url: string): {
|
|
19
|
+
ok: true;
|
|
20
|
+
platform: MeetingPlatform;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The browser-join meeting provider: it hands each run to a worker's scheduler
|
|
27
|
+
* over HTTP and authenticates the scheduler's reports by the run's worker
|
|
28
|
+
* token. It never drives a browser itself; the scheduler starts one container
|
|
29
|
+
* per run.
|
|
30
|
+
*/
|
|
31
|
+
export declare class BrowserMeetingProvider implements IMeetingBotProvider {
|
|
32
|
+
private readonly options;
|
|
33
|
+
/** The name the manager registers the provider under. */
|
|
34
|
+
readonly name = "browser";
|
|
35
|
+
/** The platforms this worker serves. */
|
|
36
|
+
readonly platforms: readonly MeetingPlatform[];
|
|
37
|
+
/** A participant that posts and reads the chat, publishes audio into a LiveKit room, and records nothing. */
|
|
38
|
+
readonly capabilities: MeetingBotCapabilities;
|
|
39
|
+
private readonly fetch;
|
|
40
|
+
/** @param options - The scheduler's url, its client credential and the worker's id. */
|
|
41
|
+
constructor(options: BrowserMeetingProviderOptions);
|
|
42
|
+
/** Classifies a join url ({@link classifyJoinUrl}) and refuses a platform this worker does not serve. */
|
|
43
|
+
checkJoinUrl(url: string): {
|
|
44
|
+
ok: true;
|
|
45
|
+
platform: MeetingPlatform;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
/** Accepts a report whose bearer token hashes to the run's dispatch record. */
|
|
51
|
+
verifyEvent(ctx: ProviderEventContext, run: MeetingRunRecord | undefined): ProviderVerificationResult;
|
|
52
|
+
/** Reads one forwarded report; its event id is `browser:<run>:<seq>:<event>`. Throws on a body that is not a report. */
|
|
53
|
+
parseEvent(ctx: ProviderEventContext): ProviderParseResult;
|
|
54
|
+
private call;
|
|
55
|
+
/** Starts the run on the scheduler (`POST /runs`); a 409 is {@link NoCapacityError}. */
|
|
56
|
+
dispatch(input: DispatchInput): Promise<DispatchResult>;
|
|
57
|
+
/** Relays a command to the run's container (`POST /runs/:id/command`); a run the scheduler no longer holds is not an error. */
|
|
58
|
+
command(runId: string, command: ProviderCommand): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Removes the run's container and the scheduler's record of the run (`DELETE /runs/:id`). A run the scheduler no
|
|
61
|
+
* longer holds has nothing left to remove; any other refusal throws, so the purge that called it tries again. The
|
|
62
|
+
* provider keeps no copy of a run, so nothing of its own is deleted.
|
|
63
|
+
*/
|
|
64
|
+
deleteRun(input: {
|
|
65
|
+
runId: string;
|
|
66
|
+
}): Promise<{
|
|
67
|
+
deleted: boolean;
|
|
68
|
+
}>;
|
|
69
|
+
/** Each run the scheduler lists (`GET /runs`), alive or not, with the seconds it has counted. */
|
|
70
|
+
usage(input: {
|
|
71
|
+
runIds?: string[];
|
|
72
|
+
}): Promise<ProviderUsage[]>;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=BrowserMeetingProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrowserMeetingProvider.d.ts","sourceRoot":"","sources":["../src/BrowserMeetingProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,KAAK,mBAAmB,EAAE,KAAK,sBAAsB,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,oBAAoB,EAAE,KAAK,mBAAmB,EAAE,KAAK,aAAa,EAAE,KAAK,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAGzW,qDAAqD;AACrD,MAAM,WAAW,6BAA6B;IAC5C,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,kDAAkD;IAClD,SAAS,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;CACxC;AAKD,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,eAAe,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAgBrH;AAED;;;;;GAKG;AACH,qBAAa,sBAAuB,YAAW,mBAAmB;IAUpD,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,yDAAyD;IACzD,QAAQ,CAAC,IAAI,aAAa;IAC1B,wCAAwC;IACxC,QAAQ,CAAC,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,6GAA6G;IAC7G,QAAQ,CAAC,YAAY,EAAE,sBAAsB,CAA0K;IACvN,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IAErC,uFAAuF;gBAC1D,OAAO,EAAE,6BAA6B;IAKnE,yGAAyG;IACzG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,QAAQ,EAAE,eAAe,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAMnG,+EAA+E;IAC/E,WAAW,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,gBAAgB,GAAG,SAAS,GAAG,0BAA0B;IAQrG,wHAAwH;IACxH,UAAU,CAAC,GAAG,EAAE,oBAAoB,GAAG,mBAAmB;YAM5C,IAAI;IAUlB,wFAAwF;IAClF,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAoB7D,+HAA+H;IACzH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrE;;;;OAIG;IACG,SAAS,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAMxE,iGAAiG;IAC3F,KAAK,CAAC,KAAK,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;CAWpE"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { NoCapacityError, workerTokenMatches } from '@framers/agentos/io/channels/meetings';
|
|
2
|
+
import { ReportPost, RunStatus, reportEventId } from './protocol.js';
|
|
3
|
+
const MEET_URL = /^https:\/\/meet\.google\.com\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:[?#].*)?$/;
|
|
4
|
+
const TEAMS_HOSTS = new Set(['teams.microsoft.com', 'teams.live.com']);
|
|
5
|
+
/** A join url's platform, by its host and path: no redirect is followed, no other host is accepted. */
|
|
6
|
+
export function classifyJoinUrl(url) {
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = new URL(url);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return { ok: false, message: 'not a url' };
|
|
13
|
+
}
|
|
14
|
+
if (parsed.protocol !== 'https:')
|
|
15
|
+
return { ok: false, message: 'the link must be https' };
|
|
16
|
+
if (parsed.username || parsed.password)
|
|
17
|
+
return { ok: false, message: 'the link carries credentials' };
|
|
18
|
+
if (parsed.hostname === 'meet.google.com')
|
|
19
|
+
return MEET_URL.test(url) ? { ok: true, platform: 'google_meet' } : { ok: false, message: 'not a Google Meet join link' };
|
|
20
|
+
if (TEAMS_HOSTS.has(parsed.hostname)) {
|
|
21
|
+
const path = parsed.pathname;
|
|
22
|
+
if (path.startsWith('/l/meetup-join/') || path.startsWith('/meet/'))
|
|
23
|
+
return { ok: true, platform: 'microsoft_teams' };
|
|
24
|
+
return { ok: false, message: 'not a Microsoft Teams join link' };
|
|
25
|
+
}
|
|
26
|
+
return { ok: false, message: 'not a Google Meet or Microsoft Teams link' };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The browser-join meeting provider: it hands each run to a worker's scheduler
|
|
30
|
+
* over HTTP and authenticates the scheduler's reports by the run's worker
|
|
31
|
+
* token. It never drives a browser itself; the scheduler starts one container
|
|
32
|
+
* per run.
|
|
33
|
+
*/
|
|
34
|
+
export class BrowserMeetingProvider {
|
|
35
|
+
options;
|
|
36
|
+
/** The name the manager registers the provider under. */
|
|
37
|
+
name = 'browser';
|
|
38
|
+
/** The platforms this worker serves. */
|
|
39
|
+
platforms;
|
|
40
|
+
/** A participant that posts and reads the chat, publishes audio into a LiveKit room, and records nothing. */
|
|
41
|
+
capabilities = { joinsAsParticipant: true, postsChat: true, readsChat: true, audioPath: 'livekit_room', recordsAudio: false, transcribes: false, keeps: 'nothing', stopPath: 'chat' };
|
|
42
|
+
fetch;
|
|
43
|
+
/** @param options - The scheduler's url, its client credential and the worker's id. */
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
this.platforms = options.platforms ?? ['google_meet', 'microsoft_teams'];
|
|
47
|
+
this.fetch = options.fetchImpl ?? globalThis.fetch;
|
|
48
|
+
}
|
|
49
|
+
/** Classifies a join url ({@link classifyJoinUrl}) and refuses a platform this worker does not serve. */
|
|
50
|
+
checkJoinUrl(url) {
|
|
51
|
+
const r = classifyJoinUrl(url);
|
|
52
|
+
if (r.ok && !this.platforms.includes(r.platform))
|
|
53
|
+
return { ok: false, message: `${r.platform} is not served by this worker` };
|
|
54
|
+
return r;
|
|
55
|
+
}
|
|
56
|
+
/** Accepts a report whose bearer token hashes to the run's dispatch record. */
|
|
57
|
+
verifyEvent(ctx, run) {
|
|
58
|
+
const raw = ctx.headers.authorization ?? ctx.headers.Authorization;
|
|
59
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
60
|
+
if (!value?.startsWith('Bearer '))
|
|
61
|
+
return { valid: false, error: 'no bearer token' };
|
|
62
|
+
if (!run?.dispatch?.workerTokenHash)
|
|
63
|
+
return { valid: false, error: 'the run has no dispatch record' };
|
|
64
|
+
return workerTokenMatches(value.slice(7), run.dispatch.workerTokenHash) ? { valid: true } : { valid: false, error: 'token mismatch' };
|
|
65
|
+
}
|
|
66
|
+
/** Reads one forwarded report; its event id is `browser:<run>:<seq>:<event>`. Throws on a body that is not a report. */
|
|
67
|
+
parseEvent(ctx) {
|
|
68
|
+
const text = typeof ctx.body === 'string' ? ctx.body : ctx.body.toString('utf8');
|
|
69
|
+
const post = ReportPost.parse(JSON.parse(text));
|
|
70
|
+
return { events: [{ eventId: reportEventId(post.runId, post.seq, post.event), runId: post.runId, type: post.event, at: post.at, data: post.data }], rawData: post };
|
|
71
|
+
}
|
|
72
|
+
async call(path, init) {
|
|
73
|
+
const res = await this.fetch(`${this.options.schedulerUrl}${path}`, {
|
|
74
|
+
method: init.method,
|
|
75
|
+
headers: { authorization: `Bearer ${this.options.schedulerToken}`, 'content-type': 'application/json' },
|
|
76
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
77
|
+
redirect: 'error',
|
|
78
|
+
});
|
|
79
|
+
return res;
|
|
80
|
+
}
|
|
81
|
+
/** Starts the run on the scheduler (`POST /runs`); a 409 is {@link NoCapacityError}. */
|
|
82
|
+
async dispatch(input) {
|
|
83
|
+
const body = {
|
|
84
|
+
runId: input.run.runId,
|
|
85
|
+
platform: input.run.platform,
|
|
86
|
+
meetingUrl: input.run.meetingUrl,
|
|
87
|
+
botName: input.run.botName,
|
|
88
|
+
notice: input.run.notice,
|
|
89
|
+
stopPhrase: input.run.stopPhrase,
|
|
90
|
+
limits: { waitingRoomMs: input.run.limits.waitingRoomMs, aloneMs: input.run.limits.aloneMs, totalMs: input.run.limits.totalMs },
|
|
91
|
+
livekit: { url: input.room.url, token: input.room.publishToken, identity: this.options.botIdentity ?? 'bot' },
|
|
92
|
+
workerToken: input.workerToken,
|
|
93
|
+
reportUrl: input.reportUrl,
|
|
94
|
+
};
|
|
95
|
+
const res = await this.call('/runs', { method: 'POST', body });
|
|
96
|
+
if (res.status === 409)
|
|
97
|
+
throw new NoCapacityError();
|
|
98
|
+
if (res.status !== 201)
|
|
99
|
+
throw new Error(`scheduler refused the run: ${res.status}`);
|
|
100
|
+
const status = RunStatus.parse(await res.json());
|
|
101
|
+
return { workerId: this.options.workerId, providerRunRef: status.runId };
|
|
102
|
+
}
|
|
103
|
+
/** Relays a command to the run's container (`POST /runs/:id/command`); a run the scheduler no longer holds is not an error. */
|
|
104
|
+
async command(runId, command) {
|
|
105
|
+
const res = await this.call(`/runs/${encodeURIComponent(runId)}/command`, { method: 'POST', body: command });
|
|
106
|
+
if (res.status === 404)
|
|
107
|
+
return; // the container is gone; the report that says so is on its way
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
throw new Error(`scheduler refused the command: ${res.status}`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Removes the run's container and the scheduler's record of the run (`DELETE /runs/:id`). A run the scheduler no
|
|
113
|
+
* longer holds has nothing left to remove; any other refusal throws, so the purge that called it tries again. The
|
|
114
|
+
* provider keeps no copy of a run, so nothing of its own is deleted.
|
|
115
|
+
*/
|
|
116
|
+
async deleteRun(input) {
|
|
117
|
+
const res = await this.call(`/runs/${encodeURIComponent(input.runId)}`, { method: 'DELETE' });
|
|
118
|
+
if (!res.ok && res.status !== 404)
|
|
119
|
+
throw new Error(`scheduler refused the delete: ${res.status}`);
|
|
120
|
+
return { deleted: false }; // the worker's container and its record of the run go with the delete; the provider held neither
|
|
121
|
+
}
|
|
122
|
+
/** Each run the scheduler lists (`GET /runs`), alive or not, with the seconds it has counted. */
|
|
123
|
+
async usage(input) {
|
|
124
|
+
const res = await this.call('/runs', { method: 'GET' });
|
|
125
|
+
if (!res.ok)
|
|
126
|
+
throw new Error(`scheduler runs unavailable: ${res.status}`);
|
|
127
|
+
const statuses = RunStatus.array().parse(await res.json());
|
|
128
|
+
const byId = new Map(statuses.map((s) => [s.runId, s]));
|
|
129
|
+
const ids = input.runIds ?? [...byId.keys()];
|
|
130
|
+
return ids.map((runId) => {
|
|
131
|
+
const s = byId.get(runId);
|
|
132
|
+
return { runId, alive: !!s && s.state !== 'exited', seconds: s?.seconds ?? 0 };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=BrowserMeetingProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrowserMeetingProvider.js","sourceRoot":"","sources":["../src/BrowserMeetingProvider.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtensionPack, ExtensionPackContext } from '@framers/agentos';
|
|
2
|
+
import { type BrowserMeetingProviderOptions } from './BrowserMeetingProvider.js';
|
|
3
|
+
/** The pack's options: the provider's, with the client credential optional here since a secret or the environment can supply it. */
|
|
4
|
+
export interface BrowserMeetingPackOptions extends Omit<BrowserMeetingProviderOptions, 'schedulerToken'> {
|
|
5
|
+
/** The client credential the scheduler accepts. Default the secret `meetings.schedulerToken`, then the environment. */
|
|
6
|
+
schedulerToken?: string;
|
|
7
|
+
/** The environment name that holds the client credential when `schedulerToken` is absent. Default `MEETING_SCHEDULER_TOKEN`. */
|
|
8
|
+
schedulerTokenEnv?: string;
|
|
9
|
+
/** The descriptor's priority. Default `50`. */
|
|
10
|
+
priority?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds the pack: one `meeting-provider` descriptor, `browserMeetingProvider`, whose payload is a
|
|
14
|
+
* {@link BrowserMeetingProvider} the host registers on its meeting manager. Throws without a scheduler url,
|
|
15
|
+
* a worker id and a client credential.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createExtensionPack(context: ExtensionPackContext): ExtensionPack;
|
|
18
|
+
export { BrowserMeetingProvider, classifyJoinUrl } from './BrowserMeetingProvider.js';
|
|
19
|
+
export type { BrowserMeetingProviderOptions } from './BrowserMeetingProvider.js';
|
|
20
|
+
export * from './protocol.js';
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE5E,OAAO,EAA0B,KAAK,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AAEzG,oIAAoI;AACpI,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,6BAA6B,EAAE,gBAAgB,CAAC;IACtG,uHAAuH;IACvH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gIAAgI;IAChI,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAYhF;AAED,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACtF,YAAY,EAAE,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACjF,cAAc,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { EXTENSION_KIND_MEETING_PROVIDER } from '@framers/agentos';
|
|
2
|
+
import { BrowserMeetingProvider } from './BrowserMeetingProvider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the pack: one `meeting-provider` descriptor, `browserMeetingProvider`, whose payload is a
|
|
5
|
+
* {@link BrowserMeetingProvider} the host registers on its meeting manager. Throws without a scheduler url,
|
|
6
|
+
* a worker id and a client credential.
|
|
7
|
+
*/
|
|
8
|
+
export function createExtensionPack(context) {
|
|
9
|
+
// The host's options are untyped; every field may be absent until the check below.
|
|
10
|
+
const options = (context.options ?? {});
|
|
11
|
+
const token = options.schedulerToken ?? context.getSecret?.('meetings.schedulerToken') ?? process.env[options.schedulerTokenEnv ?? 'MEETING_SCHEDULER_TOKEN'];
|
|
12
|
+
if (!options.schedulerUrl || !options.workerId || !token) {
|
|
13
|
+
throw new Error('The browser meeting provider needs schedulerUrl, workerId and a scheduler token (options.schedulerToken, the secret meetings.schedulerToken, or MEETING_SCHEDULER_TOKEN).');
|
|
14
|
+
}
|
|
15
|
+
const provider = new BrowserMeetingProvider({ ...options, schedulerUrl: options.schedulerUrl, workerId: options.workerId, schedulerToken: token });
|
|
16
|
+
return {
|
|
17
|
+
name: '@framers/agentos-ext-meeting-browser',
|
|
18
|
+
descriptors: [{ id: 'browserMeetingProvider', kind: EXTENSION_KIND_MEETING_PROVIDER, priority: options.priority ?? 50, payload: provider }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export { BrowserMeetingProvider, classifyJoinUrl } from './BrowserMeetingProvider.js';
|
|
22
|
+
export * from './protocol.js';
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,+BAA+B,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAsC,MAAM,6BAA6B,CAAC;AAYzG;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA6B;IAC/D,mFAAmF;IACnF,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAuC,CAAC;IAC9E,MAAM,KAAK,GAAG,OAAO,CAAC,cAAc,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,yBAAyB,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,iBAAiB,IAAI,yBAAyB,CAAC,CAAC;IAC9J,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,KAAK,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CAAC,2KAA2K,CAAC,CAAC;IAC/L,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,sBAAsB,CAAC,EAAE,GAAG,OAAO,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;IACnJ,OAAO;QACL,IAAI,EAAE,sCAAsC;QAC5C,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,wBAAwB,EAAE,IAAI,EAAE,+BAA+B,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;KAC5I,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEtF,cAAc,eAAe,CAAC"}
|