portacode 0.3.11.dev2__tar.gz → 0.3.11.dev3__tar.gz

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.
Files changed (39) hide show
  1. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/PKG-INFO +1 -1
  2. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/_version.py +2 -2
  3. portacode-0.3.11.dev3/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +423 -0
  4. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/base.py +57 -6
  5. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/PKG-INFO +1 -1
  6. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/SOURCES.txt +1 -0
  7. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/.gitignore +0 -0
  8. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/.gitmodules +0 -0
  9. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/LICENSE +0 -0
  10. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/MANIFEST.in +0 -0
  11. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/Makefile +0 -0
  12. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/README.md +0 -0
  13. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/docker-compose.yaml +0 -0
  14. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/README.md +0 -0
  15. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/__init__.py +0 -0
  16. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/__main__.py +0 -0
  17. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/cli.py +0 -0
  18. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/README.md +0 -0
  19. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/__init__.py +0 -0
  20. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/client.py +0 -0
  21. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/README.md +0 -0
  22. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/__init__.py +0 -0
  23. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/file_handlers.py +0 -0
  24. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/registry.py +0 -0
  25. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/session.py +0 -0
  26. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/system_handlers.py +0 -0
  27. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/terminal_handlers.py +0 -0
  28. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/multiplex.py +0 -0
  29. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/terminal.py +0 -0
  30. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/data.py +0 -0
  31. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/keypair.py +0 -0
  32. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/service.py +0 -0
  33. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/dependency_links.txt +0 -0
  34. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/entry_points.txt +0 -0
  35. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/requires.txt +0 -0
  36. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/top_level.txt +0 -0
  37. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/pyproject.toml +0 -0
  38. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/setup.cfg +0 -0
  39. {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.11.dev2
3
+ Version: 0.3.11.dev3
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.11.dev2'
21
- __version_tuple__ = version_tuple = (0, 3, 11, 'dev2')
20
+ __version__ = version = '0.3.11.dev3'
21
+ __version_tuple__ = version_tuple = (0, 3, 11, 'dev3')
@@ -0,0 +1,423 @@
1
+ # WebSocket Communication Protocol
2
+
3
+ This document outlines the WebSocket communication protocol between the Portacode server and the connected client devices.
4
+
5
+ ## Protocol Architecture
6
+
7
+ This protocol uses a **unified message structure** throughout the entire system. No message transformations occur between layers, ensuring consistency and eliminating complexity.
8
+
9
+ ## Raw Message Format
10
+
11
+ All communication over the WebSocket is managed by a [multiplexer](./multiplex.py) that wraps every message in a JSON object with a `device_channel` and a `payload`. This allows for multiple virtual communication channels over a single connection.
12
+
13
+ **Raw Message Structure:**
14
+
15
+ ```json
16
+ {
17
+ "device_channel": "<channel_id>",
18
+ "payload": {
19
+ // This is where the Action or Event object goes
20
+ }
21
+ }
22
+ ```
23
+
24
+ * `device_channel` (string|integer, mandatory): Identifies the virtual channel on the device side. When sending control commands to the device, they should be sent to channel 0. When the device responds to such control commands or sends system events, they will also be sent on channel 0. When a terminal session is created in the device, it is assigned a UUID, and that UUID becomes the device_channel for communicating to that specific terminal.
25
+ * `payload` (object, mandatory): The content of the message, which will be either an [Action](#actions) or an [Event](#events) object.
26
+
27
+ ---
28
+
29
+ ## Actions
30
+
31
+ Actions are messages sent from the server to the device, placed within the `payload` of a raw message. They instruct the device to perform a specific operation and are handled by the [`BaseHandler`](./base.py) and its subclasses.
32
+
33
+ **Action Structure (inside the `payload`):**
34
+
35
+ ```json
36
+ {
37
+ "command": "<command_name>",
38
+ "payload": {
39
+ "arg1": "value1",
40
+ "...": "..."
41
+ },
42
+ "client_channel": "<session_id>"
43
+ }
44
+ ```
45
+
46
+ * `command` (string, mandatory): The name of the action to be executed (e.g., `terminal_start`).
47
+ * `payload` (object, mandatory): An object containing the specific arguments for the action.
48
+ * `client_channel` (string, mandatory): A session identifier that the device will echo back in its response. This identifies which client session on the server side is communicating with the device. The device uses this to track active client sessions and ensure messages are only sent to connected clients. This works like a return address - the device_channel specifies which application on the device handles the message, while the client_channel specifies which client session should receive the response.
49
+
50
+ ### Session Management and Event Subscriptions
51
+
52
+ **Client Session Tracking**: Devices maintain awareness of active client sessions through explicit session management commands. The server notifies devices when client sessions connect, disconnect, or change their event subscriptions.
53
+
54
+ **Mandatory Client Channel**: All commands sent to devices MUST include a `client_channel` to enable proper session tracking and response routing.
55
+
56
+ **Event Subscription System**: Client sessions can specify which events they want to receive through subscription management. This allows devices to filter events and reduce bandwidth usage by only sending relevant updates to interested clients.
57
+
58
+ ---
59
+
60
+ ### `terminal_start`
61
+
62
+ Initiates a new terminal session on the device. Handled by [`terminal_start`](./terminal_handlers.py).
63
+
64
+ **Payload Fields:**
65
+
66
+ * `shell` (string, optional): The shell to use (e.g., `/bin/bash`). Defaults to the system's default shell.
67
+ * `cwd` (string, optional): The working directory to start the terminal in. Defaults to the user's home directory.
68
+ * `project_id` (string, optional): The ID of the project this terminal is associated with.
69
+
70
+ **Responses:**
71
+
72
+ * On success, the device will respond with a [`terminal_started`](#terminal_started) event.
73
+ * On error, a generic [`error`](#error) event is sent.
74
+
75
+ ### `terminal_send`
76
+
77
+ Sends input data to a running terminal session. Handled by [`terminal_send`](./terminal_handlers.py).
78
+
79
+ **Payload Fields:**
80
+
81
+ * `terminal_id` (string, mandatory): The ID of the terminal session to send data to.
82
+ * `data` (string, mandatory): The data to write to the terminal's standard input.
83
+
84
+ **Responses:**
85
+
86
+ * On success, the device will respond with a [`terminal_send_ack`](#terminal_send_ack) event.
87
+ * On error, a generic [`error`](#error) event is sent.
88
+
89
+ ### `terminal_stop`
90
+
91
+ Terminates a running terminal session. Handled by [`terminal_stop`](./terminal_handlers.py).
92
+
93
+ **Payload Fields:**
94
+
95
+ * `terminal_id` (string, mandatory): The ID of the terminal session to stop.
96
+
97
+ **Responses:**
98
+
99
+ * The device immediately responds with a [`terminal_stopped`](#terminal_stopped) event to acknowledge the request.
100
+ * Once the terminal is successfully stopped, a [`terminal_stop_completed`](#terminal_stop_completed) event is sent.
101
+ * If the terminal is not found, a [`terminal_stop_completed`](#terminal_stop_completed) with a "not_found" status is sent.
102
+
103
+ ### `terminal_list`
104
+
105
+ Requests a list of all active terminal sessions. Handled by [`terminal_list`](./terminal_handlers.py).
106
+
107
+ **Payload Fields:**
108
+
109
+ * `project_id` (string, optional): If provided, filters terminals by this project ID. If "all", lists all terminals.
110
+
111
+ **Responses:**
112
+
113
+ * On success, the device will respond with a [`terminal_list`](#terminal_list-event) event.
114
+
115
+ ### `system_info`
116
+
117
+ Requests system information from the device. Handled by [`system_info`](./system_handlers.py).
118
+
119
+ **Payload Fields:**
120
+
121
+ This action does not require any payload fields.
122
+
123
+ **Responses:**
124
+
125
+ * On success, the device will respond with a [`system_info`](#system_info-event) event.
126
+
127
+ ### `file_read`
128
+
129
+ Reads the content of a file. Handled by [`file_read`](./file_handlers.py).
130
+
131
+ **Payload Fields:**
132
+
133
+ * `path` (string, mandatory): The absolute path to the file to read.
134
+
135
+ **Responses:**
136
+
137
+ * On success, the device will respond with a [`file_read_response`](#file_read_response) event.
138
+ * On error, a generic [`error`](#error) event is sent.
139
+
140
+ ### `file_write`
141
+
142
+ Writes content to a file. Handled by [`file_write`](./file_handlers.py).
143
+
144
+ **Payload Fields:**
145
+
146
+ * `path` (string, mandatory): The absolute path to the file to write to.
147
+ * `content` (string, mandatory): The content to write to the file.
148
+
149
+ **Responses:**
150
+
151
+ * On success, the device will respond with a [`file_write_response`](#file_write_response) event.
152
+ * On error, a generic [`error`](#error) event is sent.
153
+
154
+ ### `directory_list`
155
+
156
+ Lists the contents of a directory. Handled by [`directory_list`](./file_handlers.py).
157
+
158
+ **Payload Fields:**
159
+
160
+ * `path` (string, optional): The path to the directory to list. Defaults to the current directory.
161
+ * `show_hidden` (boolean, optional): Whether to include hidden files in the listing. Defaults to `false`.
162
+
163
+ **Responses:**
164
+
165
+ * On success, the device will respond with a [`directory_list_response`](#directory_list_response) event.
166
+ * On error, a generic [`error`](#error) event is sent.
167
+
168
+ ### `file_info`
169
+
170
+ Gets information about a file or directory. Handled by [`file_info`](./file_handlers.py).
171
+
172
+ **Payload Fields:**
173
+
174
+ * `path` (string, mandatory): The path to the file or directory.
175
+
176
+ **Responses:**
177
+
178
+ * On success, the device will respond with a [`file_info_response`](#file_info_response) event.
179
+
180
+ ### `file_delete`
181
+
182
+ Deletes a file or directory. Handled by [`file_delete`](./file_handlers.py).
183
+
184
+ **Payload Fields:**
185
+
186
+ * `path` (string, mandatory): The path to the file or directory to delete.
187
+ * `recursive` (boolean, optional): If `true`, recursively deletes a directory and its contents. Defaults to `false`.
188
+
189
+ **Responses:**
190
+
191
+ * On success, the device will respond with a [`file_delete_response`](#file_delete_response) event.
192
+ * On error, a generic [`error`](#error) event is sent.
193
+
194
+ ### `session_subscribe`
195
+
196
+ Manages client session subscriptions and notifies the device about active sessions and their event interests. The device stores subscription filters for each active session and uses them to determine which sessions should receive each event.
197
+
198
+ **Payload Fields:**
199
+
200
+ * `client_channel` (string, mandatory): The session identifier being managed.
201
+ * `action` (string, mandatory): The subscription action - "connect", "disconnect", or "update".
202
+ * `filters` (array, optional): Array of filter objects the client is interested in. Required for "connect" and "update" actions.
203
+
204
+ **Filter Object Format:**
205
+ Each filter is a flexible object that can match against any field in event payloads:
206
+ * **Field Matching**: Any field name with string value or "*" for wildcard
207
+ * **Pattern Matching**: String values support wildcards (e.g., "terminal_*")
208
+ * **Multiple Conditions**: All specified fields must match for filter to match
209
+
210
+ **Example Filter Objects:**
211
+ ```json
212
+ [
213
+ {"event": "terminal_data", "terminal_id": "uuid-123"},
214
+ {"event": "terminal_*", "project_id": "project-456"},
215
+ {"event": "system_info"},
216
+ {"event": "*", "project_id": "project-789"},
217
+ {"event": "file_*", "path": "/home/user/*"}
218
+ ]
219
+ ```
220
+
221
+ **Processing Architecture:**
222
+ The device maintains a subscription registry mapping each `client_channel` to its filter objects. When broadcasting events, the device evaluates each active session's filters against the event payload to determine which sessions should receive the event.
223
+
224
+ **Responses:**
225
+
226
+ * On success, the device will respond with a [`session_subscribe_ack`](#session_subscribe_ack) event.
227
+ * On error, a generic [`error`](#error) event is sent.
228
+
229
+ ---
230
+
231
+ ## Events
232
+
233
+ Events are messages sent from the device to the server, placed within the `payload` of a raw message. They are sent in response to an action or to notify the server of a state change.
234
+
235
+ **Event Structure (inside the `payload`):**
236
+
237
+ ```json
238
+ {
239
+ "event": "<event_name>",
240
+ // Event-specific fields...
241
+ "client_channels": ["<session_id1>", "<session_id2>"]
242
+ }
243
+ ```
244
+
245
+ * `event` (string, mandatory): The name of the event being sent (e.g., `terminal_started`).
246
+ * <a name="client_channels"></a>`client_channels` (array, optional): List of client session identifiers that should receive this event. For direct responses to commands, this contains the single `client_channel` from the original request. For broadcast events, this contains all client sessions whose subscription filters matched the event. The server uses this to route the event to the correct client sessions.
247
+
248
+ ### Event Broadcasting and Session Awareness
249
+
250
+ **Targeted Events**: Events that are direct responses to commands include the original `client_channel` in the `client_channels` array, ensuring they are routed only to the requesting session.
251
+
252
+ **Broadcast Events**: Events that are not direct responses (such as spontaneous terminal exits or system notifications) are processed through subscription filtering. The device evaluates the event payload against each active session's filter objects to determine which sessions should receive the event.
253
+
254
+ **Subscription Filtering Process**:
255
+ 1. Device generates an event with its payload fields
256
+ 2. For each active client session, device evaluates session's filter objects against event payload
257
+ 3. If any filter matches, the session's `client_channel` is added to the `client_channels` array
258
+ 4. Event is sent once with all matching `client_channels`, minimizing network traffic
259
+ 5. Server routes the single event to all specified client sessions
260
+
261
+ **Filter Matching**: A filter matches an event if all specified fields in the filter have corresponding fields in the event payload with matching values (supporting wildcards like `terminal_*` or `*`).
262
+
263
+ **Efficiency Benefits**:
264
+ - **Single Event Transmission**: Each event is sent once from device to server, regardless of how many client sessions need it
265
+ - **Flexible Filtering**: Clients can filter on any event field without protocol changes
266
+ - **Minimal Bandwidth**: Only events with at least one interested client session are transmitted
267
+ - **Simple Routing**: Server receives pre-filtered events with target client sessions already identified
268
+
269
+ ---
270
+
271
+ ### <a name="error"></a>`error`
272
+
273
+ A generic event sent when an error occurs during the execution of an action.
274
+
275
+ **Event Fields:**
276
+
277
+ * `message` (string, mandatory): A description of the error that occurred.
278
+
279
+ ### <a name="session_subscribe_ack"></a>`session_subscribe_ack`
280
+
281
+ Acknowledges the receipt and processing of a `session_subscribe` action. This confirms that the device has updated its session registry and subscription filters.
282
+
283
+ **Event Fields:**
284
+
285
+ * `action` (string, mandatory): The subscription action that was processed ("connect", "disconnect", or "update").
286
+ * `active_sessions` (integer, mandatory): The total number of currently active client sessions.
287
+ * `filter_count` (integer, mandatory): The number of filter objects registered for the managed session (0 for disconnect action).
288
+
289
+ ### <a name="terminal_started"></a>`terminal_started`
290
+
291
+ Confirms that a new terminal session has been successfully started. Triggered by a `terminal_start` action. Handled by [`terminal_start`](./terminal_handlers.py).
292
+
293
+ **Event Fields:**
294
+
295
+ * `terminal_id` (string, mandatory): The unique ID of the newly created terminal session.
296
+ * `device_channel` (string, mandatory): The device channel name for terminal I/O (same as terminal_id).
297
+ * `project_id` (string, optional): The project ID associated with the terminal.
298
+
299
+ ### <a name="terminal_exit"></a>`terminal_exit`
300
+
301
+ Notifies the server that a terminal session has terminated. This can be due to the process ending or the session being stopped. Handled by [`terminal_start`](./terminal_handlers.py).
302
+
303
+ **Event Fields:**
304
+
305
+ * `terminal_id` (string, mandatory): The ID of the terminal session that has exited.
306
+ * `returncode` (integer, mandatory): The exit code of the terminal process.
307
+ * `project_id` (string, optional): The project ID associated with the terminal.
308
+
309
+ ### <a name="terminal_send_ack"></a>`terminal_send_ack`
310
+
311
+ Acknowledges the receipt of a `terminal_send` action. Handled by [`terminal_send`](./terminal_handlers.py). This event carries no extra fields.
312
+
313
+ ### <a name="terminal_stopped"></a>`terminal_stopped`
314
+
315
+ Acknowledges that a `terminal_stop` request has been received and is being processed. Handled by [`terminal_stop`](./terminal_handlers.py).
316
+
317
+ **Event Fields:**
318
+
319
+ * `terminal_id` (string, mandatory): The ID of the terminal being stopped.
320
+ * `status` (string, mandatory): The status of the stop operation (e.g., "stopping", "not_found").
321
+ * `message` (string, mandatory): A descriptive message about the status.
322
+ * `project_id` (string, optional): The project ID associated with the terminal.
323
+
324
+ ### <a name="terminal_stop_completed"></a>`terminal_stop_completed`
325
+
326
+ Confirms that a terminal session has been successfully stopped. Handled by [`terminal_stop`](./terminal_handlers.py).
327
+
328
+ **Event Fields:**
329
+
330
+ * `terminal_id` (string, mandatory): The ID of the stopped terminal.
331
+ * `status` (string, mandatory): The final status ("success", "timeout", "error", "not_found").
332
+ * `message` (string, mandatory): A descriptive message.
333
+ * `project_id` (string, optional): The project ID associated with the terminal.
334
+
335
+ ### <a name="terminal_list-event"></a>`terminal_list`
336
+
337
+ Provides the list of active terminal sessions in response to a `terminal_list` action. Handled by [`terminal_list`](./terminal_handlers.py).
338
+
339
+ **Event Fields:**
340
+
341
+ * `sessions` (array, mandatory): A list of active terminal session objects.
342
+ * `project_id` (string, optional): The project ID that was used to filter the list.
343
+
344
+ ### <a name="system_info-event"></a>`system_info`
345
+
346
+ Provides system information in response to a `system_info` action. Handled by [`system_info`](./system_handlers.py).
347
+
348
+ **Event Fields:**
349
+
350
+ * `info` (object, mandatory): An object containing system information, including:
351
+ * `cpu_percent` (float): CPU usage percentage.
352
+ * `memory` (object): Memory usage statistics.
353
+ * `disk` (object): Disk usage statistics.
354
+ * `os_info` (object): Operating system details, including `os_type`, `os_version`, `architecture`, `default_shell`, and `default_cwd`.
355
+
356
+ ### <a name="file_read_response"></a>`file_read_response`
357
+
358
+ Returns the content of a file in response to a `file_read` action. Handled by [`file_read`](./file_handlers.py).
359
+
360
+ **Event Fields:**
361
+
362
+ * `path` (string, mandatory): The path of the file that was read.
363
+ * `content` (string, mandatory): The content of the file.
364
+ * `size` (integer, mandatory): The size of the file in bytes.
365
+
366
+ ### <a name="file_write_response"></a>`file_write_response`
367
+
368
+ Confirms that a file has been written successfully in response to a `file_write` action. Handled by [`file_write`](./file_handlers.py).
369
+
370
+ **Event Fields:**
371
+
372
+ * `path` (string, mandatory): The path of the file that was written.
373
+ * `bytes_written` (integer, mandatory): The number of bytes written to the file.
374
+ * `success` (boolean, mandatory): Indicates whether the write operation was successful.
375
+
376
+ ### <a name="directory_list_response"></a>`directory_list_response`
377
+
378
+ Returns the contents of a directory in response to a `directory_list` action. Handled by [`directory_list`](./file_handlers.py).
379
+
380
+ **Event Fields:**
381
+
382
+ * `path` (string, mandatory): The path of the directory that was listed.
383
+ * `items` (array, mandatory): A list of objects, each representing a file or directory in the listed directory.
384
+ * `count` (integer, mandatory): The number of items in the `items` list.
385
+
386
+ ### <a name="file_info_response"></a>`file_info_response`
387
+
388
+ Returns information about a file or directory in response to a `file_info` action. Handled by [`file_info`](./file_handlers.py).
389
+
390
+ **Event Fields:**
391
+
392
+ * `path` (string, mandatory): The path of the file or directory.
393
+ * `exists` (boolean, mandatory): Indicates whether the file or directory exists.
394
+ * `is_file` (boolean, optional): Indicates if the path is a file.
395
+ * `is_dir` (boolean, optional): Indicates if the path is a directory.
396
+ * `is_symlink` (boolean, optional): Indicates if the path is a symbolic link.
397
+ * `size` (integer, optional): The size of the file in bytes.
398
+ * `modified` (float, optional): The last modification time (timestamp).
399
+ * `accessed` (float, optional): The last access time (timestamp).
400
+ * `created` (float, optional): The creation time (timestamp).
401
+ * `permissions` (string, optional): The file permissions in octal format.
402
+ * `owner_uid` (integer, optional): The user ID of the owner.
403
+ * `group_gid` (integer, optional): The group ID of the owner.
404
+
405
+ ### <a name="file_delete_response"></a>`file_delete_response`
406
+
407
+ Confirms that a file or directory has been deleted in response to a `file_delete` action. Handled by [`file_delete`](./file_handlers.py).
408
+
409
+ **Event Fields:**
410
+
411
+ * `path` (string, mandatory): The path of the deleted file or directory.
412
+ * `deleted_type` (string, mandatory): The type of the deleted item ("file" or "directory").
413
+ * `success` (boolean, mandatory): Indicates whether the deletion was successful.
414
+
415
+ ---
416
+
417
+ ## Future Enhancements
418
+
419
+ ### Bandwidth Management
420
+ The protocol will support configurable rate limiting and bandwidth monitoring to prevent resource exhaustion and ensure fair usage across multiple client sessions. This will include per-session bandwidth quotas, throttling mechanisms, and automatic rate limiting enforcement.
421
+
422
+ ### Channel Traffic Isolation
423
+ Different device channels (control, terminal sessions, file operations) will have separate traffic shaping to ensure high-priority control commands are not delayed by high-volume terminal output. This includes priority queuing and per-channel bandwidth allocation.
@@ -40,15 +40,58 @@ class BaseHandler(ABC):
40
40
  """
41
41
  pass
42
42
 
43
+ def _normalize_field_names(self, message: Dict[str, Any]) -> Dict[str, Any]:
44
+ """Normalize field names for backward compatibility.
45
+
46
+ Supports both old and new field names:
47
+ - 'cmd' -> 'command'
48
+ - 'reply_channel' -> 'client_channel'
49
+
50
+ Args:
51
+ message: Original message dict
52
+
53
+ Returns:
54
+ Message dict with normalized field names
55
+ """
56
+ normalized = message.copy()
57
+
58
+ # Support both 'cmd' and 'command' fields
59
+ if 'cmd' in normalized and 'command' not in normalized:
60
+ normalized['command'] = normalized['cmd']
61
+ logger.debug("Converted 'cmd' to 'command' for backward compatibility")
62
+
63
+ # Support both 'reply_channel' and 'client_channel' fields
64
+ if 'reply_channel' in normalized and 'client_channel' not in normalized:
65
+ normalized['client_channel'] = normalized['reply_channel']
66
+ logger.debug("Converted 'reply_channel' to 'client_channel' for backward compatibility")
67
+
68
+ return normalized
69
+
70
+ def _get_client_channels(self, client_channel: Optional[str] = None) -> Optional[list]:
71
+ """Convert single client_channel to client_channels array for responses.
72
+
73
+ Args:
74
+ client_channel: Single client channel identifier
75
+
76
+ Returns:
77
+ List with single client channel, or None if no channel provided
78
+ """
79
+ if client_channel:
80
+ return [client_channel]
81
+ return None
82
+
43
83
  async def send_response(self, payload: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
44
84
  """Send a response back to the gateway.
45
85
 
46
86
  Args:
47
87
  payload: Response payload
48
- reply_channel: Optional reply channel
88
+ reply_channel: Optional reply channel (supports both old and new names)
49
89
  """
90
+ # Support both reply_channel (old) and client_channel (new) parameter names
50
91
  if reply_channel:
51
- payload["reply_channel"] = reply_channel
92
+ # Add both formats for backward compatibility during transition
93
+ payload["reply_channel"] = reply_channel # Old format
94
+ payload["client_channels"] = [reply_channel] # New format
52
95
  await self.control_channel.send(payload)
53
96
 
54
97
  async def send_error(self, message: str, reply_channel: Optional[str] = None) -> None:
@@ -56,11 +99,13 @@ class BaseHandler(ABC):
56
99
 
57
100
  Args:
58
101
  message: Error message
59
- reply_channel: Optional reply channel
102
+ reply_channel: Optional reply channel (supports both old and new names)
60
103
  """
61
104
  payload = {"event": "error", "message": message}
62
105
  if reply_channel:
63
- payload["reply_channel"] = reply_channel
106
+ # Add both formats for backward compatibility during transition
107
+ payload["reply_channel"] = reply_channel # Old format
108
+ payload["client_channels"] = [reply_channel] # New format
64
109
  await self.control_channel.send(payload)
65
110
 
66
111
 
@@ -81,11 +126,14 @@ class AsyncHandler(BaseHandler):
81
126
 
82
127
  async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
83
128
  """Handle the command by executing it and sending the response."""
129
+ # Normalize field names for backward compatibility
130
+ normalized_message = self._normalize_field_names(message)
131
+
84
132
  logger.info("handler: Processing command %s with reply_channel=%s",
85
133
  self.command_name, reply_channel)
86
134
 
87
135
  try:
88
- response = await self.execute(message)
136
+ response = await self.execute(normalized_message)
89
137
  logger.info("handler: Command %s executed successfully", self.command_name)
90
138
  await self.send_response(response, reply_channel)
91
139
  except Exception as exc:
@@ -110,9 +158,12 @@ class SyncHandler(BaseHandler):
110
158
 
111
159
  async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
112
160
  """Handle the command by executing it in an executor and sending the response."""
161
+ # Normalize field names for backward compatibility
162
+ normalized_message = self._normalize_field_names(message)
163
+
113
164
  try:
114
165
  loop = asyncio.get_running_loop()
115
- response = await loop.run_in_executor(None, self.execute, message)
166
+ response = await loop.run_in_executor(None, self.execute, normalized_message)
116
167
  await self.send_response(response, reply_channel)
117
168
  except Exception as exc:
118
169
  logger.exception("Error in sync handler %s: %s", self.command_name, exc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.11.dev2
3
+ Version: 0.3.11.dev3
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -27,6 +27,7 @@ portacode/connection/client.py
27
27
  portacode/connection/multiplex.py
28
28
  portacode/connection/terminal.py
29
29
  portacode/connection/handlers/README.md
30
+ portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
30
31
  portacode/connection/handlers/__init__.py
31
32
  portacode/connection/handlers/base.py
32
33
  portacode/connection/handlers/file_handlers.py
File without changes