@fairfox/polly 0.2.0 → 0.3.0

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.
@@ -0,0 +1,13 @@
1
+ FROM tlaplus/tla:latest
2
+
3
+ # Install additional tools for better DX
4
+ RUN apk add --no-cache \
5
+ bash \
6
+ vim \
7
+ less \
8
+ tree
9
+
10
+ WORKDIR /specs
11
+
12
+ # Default command: keep container alive for exec commands
13
+ CMD ["tail", "-f", "/dev/null"]
@@ -0,0 +1,37 @@
1
+ # Verification Specifications
2
+
3
+ This directory contains formal specifications and verification configurations for the `@fairfox/polly-verify` package.
4
+
5
+ ## Contents
6
+
7
+ ### `verification.config.ts`
8
+ Example verification configuration demonstrating how to use the verify package with web extensions.
9
+
10
+ ### `tla/`
11
+ TLA+ specifications for formal verification:
12
+ - `MessageRouter.tla` - TLA+ specification of the message routing system
13
+ - `MessageRouter.cfg` - TLC model checker configuration
14
+ - `README.md` - Documentation for running TLA+ verification
15
+
16
+ ### Docker Setup
17
+ - `Dockerfile` - Container setup for TLA+ toolchain
18
+ - `docker-compose.yml` - Docker Compose configuration for running verification
19
+
20
+ ## Running Verification
21
+
22
+ ### With Docker (Recommended)
23
+ ```bash
24
+ cd packages/verify/specs
25
+ docker-compose up
26
+ ```
27
+
28
+ ### With TLC Directly
29
+ ```bash
30
+ cd packages/verify/specs/tla
31
+ tlc MessageRouter.tla -config MessageRouter.cfg
32
+ ```
33
+
34
+ ## See Also
35
+ - [Verify Package Documentation](../README.md)
36
+ - [WebSocket Example](../examples/websocket-app/README.md)
37
+ - [TLA+ Specifications](./tla/README.md)
@@ -0,0 +1,9 @@
1
+ services:
2
+ tla:
3
+ build: .
4
+ container_name: web-ext-tla
5
+ volumes:
6
+ - ./tla:/specs
7
+ working_dir: /specs
8
+ stdin_open: true
9
+ tty: true
@@ -0,0 +1,24 @@
1
+ SPECIFICATION Spec
2
+
3
+ \* Constants
4
+ CONSTANTS
5
+ Contexts = {background, content, popup}
6
+ MaxMessages = 4
7
+ MaxTabId = 2
8
+ TimeoutLimit = 3
9
+
10
+ \* Invariants to check
11
+ INVARIANTS
12
+ TypeOK
13
+ NoRoutingLoops
14
+ DeliveredWerePending
15
+ NoOrphanedRequests
16
+
17
+ \* Properties to check
18
+ PROPERTIES
19
+ EventualResolution
20
+ ConnectedEventuallyDelivers
21
+
22
+ \* State constraint (keep state space manageable)
23
+ CONSTRAINT
24
+ Len(messages) <= MaxMessages
@@ -0,0 +1,221 @@
1
+ ------------------------- MODULE MessageRouter -------------------------
2
+ (*
3
+ Formal specification of the web extension MessageRouter.
4
+
5
+ This spec models the core message routing behavior across extension contexts:
6
+ - Background service worker (central hub)
7
+ - Content scripts (one per tab)
8
+ - DevTools, Popup, Options (UI contexts)
9
+
10
+ Key properties verified:
11
+ 1. No routing loops
12
+ 2. Messages eventually deliver (if port connected)
13
+ 3. All pending requests eventually resolve (success/timeout/disconnect)
14
+ 4. Port cleanup is complete
15
+ *)
16
+
17
+ EXTENDS Integers, Sequences, FiniteSets, TLC
18
+
19
+ CONSTANTS
20
+ Contexts, \* Set of all contexts: {"background", "content", "popup", ...}
21
+ MaxMessages, \* Bound on number of messages (for model checking)
22
+ MaxTabId, \* Maximum tab ID
23
+ TimeoutLimit \* Message timeout threshold
24
+
25
+ VARIABLES
26
+ ports, \* Port state: [context -> "connected" | "disconnected"]
27
+ messages, \* Sequence of messages in flight
28
+ pendingRequests, \* Map: messageId -> {sender, target, timestamp}
29
+ delivered, \* Set of delivered message IDs
30
+ routingDepth, \* Current routing depth (for loop detection)
31
+ time \* Logical clock
32
+
33
+ vars == <<ports, messages, pendingRequests, delivered, routingDepth, time>>
34
+
35
+ -----------------------------------------------------------------------------
36
+
37
+ (* Type definitions *)
38
+
39
+ ContextType == {"background", "content", "popup", "devtools", "options", "offscreen"}
40
+ PortState == {"connected", "disconnected"}
41
+ MessageStatus == {"pending", "routing", "delivered", "failed", "timeout"}
42
+
43
+ Message == [
44
+ id: Nat,
45
+ source: ContextType,
46
+ targets: SUBSET ContextType, \* Set of target contexts (can be multiple)
47
+ tabId: Nat,
48
+ msgType: STRING, \* Message type for handler dispatch
49
+ status: MessageStatus,
50
+ timestamp: Nat
51
+ ]
52
+
53
+ -----------------------------------------------------------------------------
54
+
55
+ (* Initial state *)
56
+
57
+ Init ==
58
+ /\ ports = [c \in Contexts |-> "disconnected"]
59
+ /\ messages = <<>>
60
+ /\ pendingRequests = [id \in {} |-> {}]
61
+ /\ delivered = {}
62
+ /\ routingDepth = 0
63
+ /\ time = 0
64
+
65
+ -----------------------------------------------------------------------------
66
+
67
+ (* Actions *)
68
+
69
+ (* A context connects a port *)
70
+ ConnectPort(context) ==
71
+ /\ ports[context] = "disconnected"
72
+ /\ ports' = [ports EXCEPT ![context] = "connected"]
73
+ /\ UNCHANGED <<messages, pendingRequests, delivered, routingDepth, time>>
74
+
75
+ (* A context disconnects *)
76
+ DisconnectPort(context) ==
77
+ /\ ports[context] = "connected"
78
+ /\ ports' = [ports EXCEPT ![context] = "disconnected"]
79
+ \* Clean up pending requests that target this context
80
+ /\ LET failedRequests == {id \in DOMAIN pendingRequests :
81
+ context \in pendingRequests[id].targets}
82
+ IN pendingRequests' = [id \in DOMAIN pendingRequests \ failedRequests |->
83
+ pendingRequests[id]]
84
+ /\ UNCHANGED <<messages, delivered, routingDepth, time>>
85
+
86
+ (* Send a message from source to one or more targets *)
87
+ SendMessage(source, targetSet, tabId, messageType) ==
88
+ /\ ports[source] = "connected"
89
+ /\ Len(messages) < MaxMessages
90
+ /\ routingDepth = 0 \* Only send at top level (no recursive sends)
91
+ /\ targetSet # {} \* Must have at least one target
92
+ /\ LET newId == Len(messages) + 1
93
+ newMsg == [
94
+ id |-> newId,
95
+ source |-> source,
96
+ targets |-> targetSet,
97
+ tabId |-> tabId,
98
+ msgType |-> messageType,
99
+ status |-> "pending",
100
+ timestamp |-> time
101
+ ]
102
+ IN /\ messages' = Append(messages, newMsg)
103
+ /\ pendingRequests' = pendingRequests @@
104
+ (newId :> [sender |-> source,
105
+ targets |-> targetSet,
106
+ timestamp |-> time])
107
+ /\ time' = time + 1
108
+ /\ UNCHANGED <<ports, delivered, routingDepth>>
109
+
110
+ (* Route a message to one of its targets *)
111
+ RouteMessage(msgIndex) ==
112
+ /\ msgIndex \in 1..Len(messages)
113
+ /\ LET msg == messages[msgIndex]
114
+ IN /\ msg.status = "pending"
115
+ /\ routingDepth' = routingDepth + 1
116
+ /\ routingDepth < 5 \* Safety bound: detect loops
117
+ /\ \* Non-deterministically choose a target from the targets set
118
+ \E target \in msg.targets :
119
+ /\ IF target \in Contexts /\ ports[target] = "connected"
120
+ THEN \* Successful delivery to this target
121
+ /\ messages' = [messages EXCEPT ![msgIndex].status = "delivered"]
122
+ /\ delivered' = delivered \union {msg.id}
123
+ /\ pendingRequests' = [id \in DOMAIN pendingRequests \ {msg.id} |->
124
+ pendingRequests[id]]
125
+ /\ time' = time + 1
126
+ ELSE \* Port not connected or invalid target, message fails
127
+ /\ messages' = [messages EXCEPT ![msgIndex].status = "failed"]
128
+ /\ pendingRequests' = [id \in DOMAIN pendingRequests \ {msg.id} |->
129
+ pendingRequests[id]]
130
+ /\ time' = time + 1
131
+ /\ UNCHANGED delivered
132
+ /\ UNCHANGED ports
133
+
134
+ (* Complete routing (reset depth) *)
135
+ CompleteRouting ==
136
+ /\ routingDepth > 0
137
+ /\ routingDepth' = 0
138
+ /\ UNCHANGED <<ports, messages, pendingRequests, delivered, time>>
139
+
140
+ (* Handle message timeout *)
141
+ TimeoutMessage(msgIndex) ==
142
+ /\ msgIndex \in 1..Len(messages)
143
+ /\ LET msg == messages[msgIndex]
144
+ IN /\ msg.status = "pending"
145
+ /\ time - msg.timestamp > TimeoutLimit
146
+ /\ messages' = [messages EXCEPT ![msgIndex].status = "timeout"]
147
+ /\ pendingRequests' = [id \in DOMAIN pendingRequests \ {msg.id} |->
148
+ pendingRequests[id]]
149
+ /\ time' = time + 1
150
+ /\ UNCHANGED <<ports, delivered, routingDepth>>
151
+
152
+ -----------------------------------------------------------------------------
153
+
154
+ (* Next state relation *)
155
+
156
+ Next ==
157
+ \/ \E c \in Contexts : ConnectPort(c)
158
+ \/ \E c \in Contexts : DisconnectPort(c)
159
+ \/ \E src \in Contexts : \E targetSet \in (SUBSET Contexts \ {{}}) : \E tab \in 0..MaxTabId : \E msgType \in {"msg1", "msg2"} : SendMessage(src, targetSet, tab, msgType)
160
+ \/ \E i \in 1..Len(messages) : RouteMessage(i)
161
+ \/ CompleteRouting
162
+ \/ \E i \in 1..Len(messages) : TimeoutMessage(i)
163
+
164
+ Spec == Init /\ [][Next]_vars /\ WF_vars(Next)
165
+
166
+ -----------------------------------------------------------------------------
167
+
168
+ (* Invariants *)
169
+
170
+ (* CRITICAL: No infinite routing loops *)
171
+ NoRoutingLoops ==
172
+ routingDepth < 3
173
+
174
+ (* All delivered messages were actually pending *)
175
+ DeliveredWerePending ==
176
+ \A msgId \in delivered :
177
+ \E i \in 1..Len(messages) : messages[i].id = msgId
178
+
179
+ (* No orphaned pending requests - every pending request has a corresponding message *)
180
+ NoOrphanedRequests ==
181
+ \A reqId \in DOMAIN pendingRequests :
182
+ \E i \in 1..Len(messages) :
183
+ /\ messages[i].id = reqId
184
+ /\ messages[i].status \in {"pending", "routing"}
185
+
186
+ (* Messages to disconnected ports eventually fail *)
187
+ DisconnectedPortsFail ==
188
+ \A i \in 1..Len(messages) :
189
+ LET msg == messages[i]
190
+ IN (msg.status = "pending" /\ \A target \in msg.targets : ports[target] = "disconnected")
191
+ => (time - msg.timestamp > TimeoutLimit => msg.status \in {"failed", "timeout"})
192
+
193
+ (* Type invariant *)
194
+ TypeOK ==
195
+ /\ ports \in [Contexts -> PortState]
196
+ /\ \A i \in 1..Len(messages) :
197
+ /\ messages[i].source \in Contexts
198
+ /\ messages[i].targets \subseteq Contexts
199
+ /\ messages[i].targets # {}
200
+ /\ messages[i].status \in MessageStatus
201
+ /\ routingDepth >= 0
202
+ /\ time >= 0
203
+
204
+ -----------------------------------------------------------------------------
205
+
206
+ (* Temporal properties *)
207
+
208
+ (* Eventually, all messages resolve (deliver, fail, or timeout) *)
209
+ EventualResolution ==
210
+ \A i \in 1..Len(messages) :
211
+ messages[i].status = "pending"
212
+ ~> messages[i].status \in {"delivered", "failed", "timeout"}
213
+
214
+ (* If ports are connected and message is sent to them, message eventually delivers *)
215
+ ConnectedEventuallyDelivers ==
216
+ \A i \in 1..Len(messages) :
217
+ LET msg == messages[i]
218
+ IN (msg.status = "pending" /\ \A target \in msg.targets : ports[target] = "connected")
219
+ ~> (msg.status = "delivered")
220
+
221
+ =============================================================================
@@ -0,0 +1,179 @@
1
+ # TLA+ Formal Specification for MessageRouter
2
+
3
+ This directory contains formal specifications for the web extension's message routing system using TLA+ (Temporal Logic of Actions).
4
+
5
+ ## What is This?
6
+
7
+ TLA+ is a formal specification language for concurrent and distributed systems. It allows us to:
8
+ - **Model** the message routing logic mathematically
9
+ - **Verify** properties like "no routing loops" or "messages eventually deliver"
10
+ - **Find edge cases** that are hard to catch with traditional testing
11
+ - **Document** the system behavior precisely
12
+
13
+ ## What We're Verifying
14
+
15
+ ### Core Properties
16
+
17
+ 1. **NoRoutingLoops** - Messages never create infinite routing cycles
18
+ 2. **EventualResolution** - Every message eventually resolves (delivers, fails, or times out)
19
+ 3. **ConnectedEventuallyDelivers** - Messages to connected ports eventually deliver
20
+ 4. **NoOrphanedRequests** - Pending requests always have corresponding messages
21
+ 5. **TypeOK** - All data structures maintain correct types
22
+
23
+ ### System Model
24
+
25
+ The spec models:
26
+ - **Contexts**: background, content, popup, devtools, options, offscreen
27
+ - **Port lifecycle**: disconnected ⇒ connected ⇒ disconnected
28
+ - **Message states**: pending → routing → delivered/failed/timeout
29
+ - **Routing depth tracking** (for loop detection)
30
+ - **Timeout handling**
31
+ - **Broadcast semantics**
32
+
33
+ ## Running the Model Checker
34
+
35
+ ### Prerequisites
36
+
37
+ Docker must be running. The TLA+ toolchain runs in a container.
38
+
39
+ ### Quick Start
40
+
41
+ ```bash
42
+ # From project root
43
+ bun run tla:up # Start container
44
+ bun run tla:check # Run model checker
45
+ bun run tla:down # Stop container
46
+ ```
47
+
48
+ ### Manual Commands
49
+
50
+ ```bash
51
+ # Start the TLA+ container
52
+ docker-compose -f specs/docker-compose.yml up -d
53
+
54
+ # Run the model checker
55
+ docker-compose -f specs/docker-compose.yml exec tla tlc MessageRouter.tla
56
+
57
+ # Interactive shell (for exploring)
58
+ docker-compose -f specs/docker-compose.yml exec tla sh
59
+
60
+ # Stop the container
61
+ docker-compose -f specs/docker-compose.yml down
62
+ ```
63
+
64
+ ## Understanding the Output
65
+
66
+ ### Success
67
+ ```
68
+ TLC2 Version X.X.X
69
+ ...
70
+ Model checking completed. No error has been found.
71
+ Estimates of the probability that TLC did not check all reachable states
72
+ because two distinct states had the same fingerprint:
73
+ calculated (optimistic): val = X.X%
74
+ ...
75
+ Finished in XXms at (YYYY-MM-DD HH:MM:SS)
76
+ ```
77
+
78
+ ### Violation Found
79
+ TLC will print:
80
+ - **Which invariant was violated**
81
+ - **The execution trace** (sequence of states) leading to the violation
82
+ - Each state shows the values of all variables
83
+
84
+ Example:
85
+ ```
86
+ Error: Invariant NoRoutingLoops is violated.
87
+
88
+ State 1: <Initial State>
89
+ /\ ports = ...
90
+ /\ routingDepth = 0
91
+ ...
92
+
93
+ State 2: <ConnectPort("background")>
94
+ /\ ports = [background |-> "connected", ...]
95
+ ...
96
+
97
+ State 3: <SendMessage("background", "content", 1)>
98
+ ...
99
+ ```
100
+
101
+ ## Editing the Spec
102
+
103
+ ### Files
104
+
105
+ - **MessageRouter.tla** - Main specification (edit this)
106
+ - **MessageRouter.cfg** - Model configuration (constants, invariants to check)
107
+
108
+ ### Workflow
109
+
110
+ 1. Edit `.tla` file locally (syntax highlighting via VSCode TLA+ extension)
111
+ 2. Save changes (files are volume-mounted into container)
112
+ 3. Run `bun run tla:check`
113
+ 4. Iterate on violations
114
+
115
+ ### Expanding the Model
116
+
117
+ Current model is intentionally simple (3 contexts, 4 messages max). To expand:
118
+
119
+ 1. **Add more contexts**: Edit `MessageRouter.cfg`:
120
+ ```
121
+ Contexts = {background, content, popup, devtools, options}
122
+ ```
123
+
124
+ 2. **Increase message limit**: (Warning: state space grows exponentially!)
125
+ ```
126
+ MaxMessages = 6
127
+ ```
128
+
129
+ 3. **Add new properties**: Define in `.tla`, add to `.cfg` under `INVARIANTS` or `PROPERTIES`
130
+
131
+ 4. **Model request/response pairing**: Extend `Message` type with response IDs
132
+
133
+ ## Performance Notes
134
+
135
+ Model checking explores **all possible states**. State space grows exponentially with:
136
+ - Number of contexts
137
+ - Number of messages
138
+ - Number of tabs
139
+
140
+ Current settings (3 contexts, 4 messages) check ~10,000 states in <1 second.
141
+
142
+ Increasing to 5 contexts + 6 messages = ~1 million states = ~10 seconds.
143
+
144
+ ## Relationship to Code
145
+
146
+ The spec is **not** the implementation - it's a mathematical model.
147
+
148
+ **Code → Spec mapping:**
149
+ - `MessageRouter.routeMessage()` → `RouteMessage` action
150
+ - `port.onDisconnect()` → `DisconnectPort` action
151
+ - `pendingRequests` Map → `pendingRequests` variable
152
+ - "no loops" tests → `NoRoutingLoops` invariant
153
+
154
+ **Use TLA+ to:**
155
+ 1. Find edge cases → Add as unit tests
156
+ 2. Verify design before implementing
157
+ 3. Document complex concurrent behavior
158
+
159
+ **Don't use TLA+ for:**
160
+ - Testing implementation details (use bun test)
161
+ - Performance testing
162
+ - Browser API quirks
163
+
164
+ ## Learning Resources
165
+
166
+ - [TLA+ Home](https://lamport.azurewebsites.net/tla/tla.html)
167
+ - [Learn TLA+](https://learntla.com/)
168
+ - [Practical TLA+](https://www.apress.com/gp/book/9781484238288) (book)
169
+ - [TLA+ Video Course](https://lamport.azurewebsites.net/video/videos.html)
170
+
171
+ ## Next Steps
172
+
173
+ Potential expansions:
174
+ - [ ] Model request/response pairing with IDs
175
+ - [ ] Model port reconnection scenarios
176
+ - [ ] Add tabId-specific routing
177
+ - [ ] Model MessageBus interaction
178
+ - [ ] Verify broadcast reaches all ports exactly once
179
+ - [ ] Model race conditions during port disconnect
@@ -0,0 +1,61 @@
1
+ // ═══════════════════════════════════════════════════════════════
2
+ // Verification Configuration
3
+ // ═══════════════════════════════════════════════════════════════
4
+ //
5
+ // This file configures TLA+ verification for your extension.
6
+ // Some values are auto-configured, others need your input.
7
+ //
8
+ // Look for:
9
+ // • /* CONFIGURE */ - Replace with your value
10
+ // • /* REVIEW */ - Check the auto-generated value
11
+ // • null - Must be replaced with a concrete value
12
+ //
13
+ // Run 'bun verify' to check for incomplete configuration.
14
+ // Run 'bun verify --setup' for interactive help.
15
+ //
16
+
17
+ import { defineVerification } from '../src/index'
18
+
19
+ export default defineVerification({
20
+ state: {
21
+ },
22
+
23
+ messages: {
24
+ // Maximum messages in flight simultaneously across all contexts.
25
+ // Higher = more realistic concurrency, but exponentially slower.
26
+ //
27
+ // Recommended values:
28
+ // • 2-3: Fast verification (< 10 seconds)
29
+ // • 4-6: Balanced (10-60 seconds)
30
+ // • 8+: Thorough but slow (minutes)
31
+ //
32
+ // WARNING: State space grows exponentially! Start small.
33
+ maxInFlight: 3,
34
+
35
+ // Maximum tab IDs to model (content scripts are per-tab).
36
+ //
37
+ // Recommended:
38
+ // • 0-1: Most extensions (single tab or tab-agnostic)
39
+ // • 2-3: Multi-tab coordination
40
+ //
41
+ // Start with 0 or 1 for faster verification.
42
+ maxTabs: 1,
43
+ },
44
+
45
+ // Verification behavior
46
+ // ─────────────────────
47
+ //
48
+ // onBuild: What to do during development builds
49
+ // • 'warn' - Show warnings but don't fail (recommended)
50
+ // • 'error' - Fail the build on violations
51
+ // • 'off' - Skip verification
52
+ //
53
+ onBuild: 'warn',
54
+
55
+ // onRelease: What to do during production builds
56
+ // • 'error' - Fail the build on violations (recommended)
57
+ // • 'warn' - Show warnings but don't fail
58
+ // • 'off' - Skip verification
59
+ //
60
+ onRelease: 'error',
61
+ })