pycodex-ws 0.5.1__tar.gz → 0.6.0__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.
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/PKG-INFO +69 -5
- pycodex_ws-0.6.0/README.md +174 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/server.py +15 -15
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_ws/__init__.py +2 -5
- pycodex_ws-0.6.0/pycodex_ws/feishu_auth.py +30 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_ws/feishu_card.py +18 -243
- pycodex_ws-0.5.1/pycodex_ws/feishu_link.py → pycodex_ws-0.6.0/pycodex_ws/feishu_connection.py +15 -166
- pycodex_ws-0.6.0/pycodex_ws/feishu_link.py +157 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_ws/feishu_oauth.py +3 -4
- pycodex_ws-0.6.0/pycodex_ws/feishu_render.py +215 -0
- pycodex_ws-0.6.0/pycodex_ws/server_support.py +299 -0
- pycodex_ws-0.6.0/pycodex_ws/workspace_config.py +186 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pyproject.toml +2 -2
- pycodex_ws-0.6.0/workspace_server/WORKSPACE_API.md +315 -0
- pycodex_ws-0.6.0/workspace_server/app.py +438 -0
- pycodex_ws-0.6.0/workspace_server/content_views.css +1 -0
- pycodex_ws-0.6.0/workspace_server/content_views.mjs +37 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/customization.py +1 -1
- pycodex_ws-0.6.0/workspace_server/frontend/content_views.css +176 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/content_views.mjs +103 -6
- pycodex_ws-0.6.0/workspace_server/frontend/transcript.mjs +385 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/CONTRIBUTING.md +157 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/README.md +30 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/README.md +107 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview-mobile.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview.html +43 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/preview.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/custom.css +67 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/custom.js +102 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/fixture.mjs +85 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/clock-ticks/source/verify.mjs +82 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/README.md +118 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/preview-mobile.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/preview.html +48 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/preview.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/source/fixture.mjs +11 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/source/tool-groups.css +11 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/source/tool-groups.js +111 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/consecutive-tool-groups/source/verify.mjs +61 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/README.md +85 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/preview-mobile.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/preview.html +33 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/preview.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/source/custom.css +5 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/source/custom.js +75 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/source/fixture.mjs +18 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/exec-status/source/verify.mjs +80 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/README.md +88 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/preview-mobile.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/preview.html +34 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/preview.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/source/custom.css +6 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/source/custom.js +95 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/source/fixture.mjs +26 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/reasoning-display/source/verify.mjs +80 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/README.md +126 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/preview-mobile.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/preview.html +57 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/preview.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/custom.css +46 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/custom.js +49 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/fixture.mjs +25 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/session-tips.js +165 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/session-tips/source/verify.mjs +71 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/README.md +92 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/preview-mobile.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/preview.html +34 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/preview.png +0 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/source/custom.css +6 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/source/custom.js +112 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/source/fixture.mjs +25 -0
- pycodex_ws-0.6.0/workspace_server/optional_features/tool-calls/source/verify.mjs +80 -0
- pycodex_ws-0.6.0/workspace_server/routes.py +694 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/runtime_info.py +3 -1
- pycodex_ws-0.6.0/workspace_server/session_api.py +143 -0
- pycodex_ws-0.6.0/workspace_server/session_view.py +669 -0
- pycodex_ws-0.6.0/workspace_server/sessions.py +435 -0
- pycodex_ws-0.6.0/workspace_server/sharing.py +93 -0
- pycodex_ws-0.6.0/workspace_server/view_sources.py +251 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/workspace.html +821 -113
- pycodex_ws-0.6.0/workspace_server/workspace_views.py +304 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/workspaces.py +37 -199
- pycodex_ws-0.5.1/README.md +0 -110
- pycodex_ws-0.5.1/workspace_server/app.py +0 -2151
- pycodex_ws-0.5.1/workspace_server/content_views.css +0 -1
- pycodex_ws-0.5.1/workspace_server/content_views.mjs +0 -32
- pycodex_ws-0.5.1/workspace_server/frontend/content_views.css +0 -107
- pycodex_ws-0.5.1/workspace_server/workspace_views.py +0 -534
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/.gitignore +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/LICENSE +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/__init__.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/__main__.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/browser.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/client.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/index.html +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/pycodex_router/protocol.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/__init__.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/__main__.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/agent_access.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/content_views.LICENSE.txt +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/editor.css +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/editor.mjs +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/.gitignore +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/build.mjs +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/composer.mjs +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/editor.css +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/editor.mjs +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/editor_context.mjs +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/package-lock.json +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/package.json +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/prompt_history.mjs +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/touch_selection.mjs +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/frontend/view_labels.mjs +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/owned_process.py +0 -0
- {pycodex_ws-0.5.1 → pycodex_ws-0.6.0}/workspace_server/workspaces.html +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pycodex-ws
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Browser workspace and optional Feishu integration for python-codex
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.7
|
|
7
7
|
Requires-Dist: pillow>=9.0.0
|
|
8
|
-
Requires-Dist: python-codex==0.
|
|
8
|
+
Requires-Dist: python-codex==0.6.0
|
|
9
9
|
Requires-Dist: websockets<16,>=11
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
|
|
@@ -16,6 +16,9 @@ optional Feishu integration for
|
|
|
16
16
|
[`python-codex`](https://pypi.org/project/python-codex/).
|
|
17
17
|
Requires Python 3.7+ and installs the matching core version.
|
|
18
18
|
|
|
19
|
+
See the [0.6.0 release notes](https://github.com/Randomizez/pycodex/blob/v0.6.0/docs/RELEASE_0.6.0.md) for changes and
|
|
20
|
+
integration migration from 0.5.0.
|
|
21
|
+
|
|
19
22
|
## Quick start
|
|
20
23
|
|
|
21
24
|
```bash
|
|
@@ -52,6 +55,12 @@ running sessions and unread replies. `/cd <directory>` changes the current
|
|
|
52
55
|
session's working directory while idle; `/model`, `/resume`, `/compact` and
|
|
53
56
|
other core session commands are also available.
|
|
54
57
|
|
|
58
|
+
Consecutive model reconnection diagnostics share one collapsed `Model retries`
|
|
59
|
+
row per sequence. The summary shows the number of recorded retries, the latest
|
|
60
|
+
attempt and its HTTP status when available. Expand it to read the original errors
|
|
61
|
+
in a scrollable log. New retries update the same row and preserve its open state;
|
|
62
|
+
refreshing starts collapsed. Other model diagnostics and final errors stay visible.
|
|
63
|
+
|
|
55
64
|
Click Markdown links to open files and comparisons, for example
|
|
56
65
|
`[source](app.py:42)` or `[changes](diff:HEAD:app.py)`. In a code or diff view,
|
|
57
66
|
select text and choose **Insert into chat** to add editable context to the draft.
|
|
@@ -62,12 +71,22 @@ When using a router, its directory groups workspaces by device and lists unread
|
|
|
62
71
|
workspaces at the top. Device handles reorder the list; pencil buttons edit
|
|
63
72
|
device names.
|
|
64
73
|
|
|
74
|
+
The runtime information shared between Agents recommends plain messages, which
|
|
75
|
+
steer active work at the next safe boundary. Use `/queue` explicitly for an
|
|
76
|
+
independent follow-up task that should wait for current work to finish.
|
|
77
|
+
|
|
78
|
+
The message API also accepts a string `name`, defaulting to `"user"`, for
|
|
79
|
+
identifying the source of a user message. The history API returns it, and
|
|
80
|
+
recorded sessions preserve it across resume. It is metadata only: the model request and frontend display do
|
|
81
|
+
not include it. See the [message API contract](workspace_server/WORKSPACE_API.md#commands-and-results).
|
|
82
|
+
|
|
65
83
|
The [VS Code extension](extensions/vscode/README.md) uses this backend and is
|
|
66
84
|
distributed separately as a VSIX.
|
|
67
85
|
|
|
68
86
|
## Customize the interface
|
|
69
87
|
|
|
70
|
-
Click the gear button
|
|
88
|
+
Click the gear button and choose **Frontend customization** to start a
|
|
89
|
+
customization conversation with the Agent.
|
|
71
90
|
Custom files live in the server user's `~/.pycodex/frontend/`:
|
|
72
91
|
|
|
73
92
|
- `custom.css` and `custom.js`: optional styles and an ES module, loaded automatically.
|
|
@@ -78,6 +97,43 @@ Refresh to apply changes and keep the selected conversation. Open the workspace
|
|
|
78
97
|
with `?builtin=1` to bypass all customizations; click “Re-enable customizations”
|
|
79
98
|
to return. Changes apply to all workspaces for that OS user.
|
|
80
99
|
See the [customization contract](../../docs/RUNTIME.md#frontend-customization).
|
|
100
|
+
For custom interfaces, the [resource API](../../docs/WORKSPACE_API.md) exposes
|
|
101
|
+
state, model catalogs, asynchronous command results, paginated history, event
|
|
102
|
+
replay and sub-agent/background-work inventories. A `mount(host)` export in
|
|
103
|
+
`custom.js` can access the built-in page's session, draft and view controls.
|
|
104
|
+
|
|
105
|
+
Backend module ownership and the preserved session interfaces are documented in
|
|
106
|
+
the [runtime guide](../../docs/RUNTIME.md#workspace-session-api).
|
|
107
|
+
File/Git source resolution and view storage retain their existing public classes;
|
|
108
|
+
see [content views](../../docs/RUNTIME.md#workspace-content-views).
|
|
109
|
+
|
|
110
|
+
Ask the Agent to show the [feature catalog](workspace_server/optional_features/README.md)
|
|
111
|
+
in the view area, then choose features by name or ID in chat. The Agent adapts
|
|
112
|
+
their source into your frontend. To share a local change, ask it to publish a
|
|
113
|
+
feature PR on GitHub; the [contribution guide](workspace_server/optional_features/CONTRIBUTING.md)
|
|
114
|
+
specifies the source, documentation, validation and PR format.
|
|
115
|
+
|
|
116
|
+
## Share a workspace
|
|
117
|
+
|
|
118
|
+
Start the workspace server with an administrator password (`--password`), then
|
|
119
|
+
choose **Settings → Share** in a workspace. Set a different share password and
|
|
120
|
+
send the displayed workspace link and password to the recipient.
|
|
121
|
+
|
|
122
|
+
The existing login page accepts either the administrator password or the share
|
|
123
|
+
password for the workspace being opened. Visitors can use all its conversations,
|
|
124
|
+
Canvas, files, APIs and WebSocket connections, but cannot access workspace
|
|
125
|
+
management, another workspace or sharing settings. The settings button and
|
|
126
|
+
management navigation are hidden for visitors.
|
|
127
|
+
|
|
128
|
+
Passwords, verification values and visitor tokens are kept only in process
|
|
129
|
+
memory. Changing the password or stopping sharing invalidates existing visitor
|
|
130
|
+
logins and disconnects their WebSockets. Restarting the process clears all
|
|
131
|
+
shares. Already accepted Agent work continues.
|
|
132
|
+
|
|
133
|
+
This restricts web access; Agent tools retain their existing file and execution
|
|
134
|
+
permissions. An unprotected server must configure its administrator password
|
|
135
|
+
before enabling sharing. Router authentication is unchanged: a link through a
|
|
136
|
+
router still requires its normal login.
|
|
81
137
|
|
|
82
138
|
## Storage
|
|
83
139
|
|
|
@@ -105,6 +161,11 @@ token in `~/.codex/.feishu_refresh_token`. In a workspace session, use
|
|
|
105
161
|
`/link <feishu-email|open_id|chat_id>` to connect a card and `/unlink` to disconnect.
|
|
106
162
|
Other workspace features work without the SDK.
|
|
107
163
|
|
|
164
|
+
The adapter separates card rendering, refresh-token storage and SDK connections.
|
|
165
|
+
`PycodexCard` and `PycodexRuntimeLink` keep their existing imports and interfaces;
|
|
166
|
+
see [connection lifecycle](../../docs/RUNTIME.md#feishu-connection-lifecycle)
|
|
167
|
+
and [event presentation](../../docs/RUNTIME.md#observation).
|
|
168
|
+
|
|
108
169
|
## Development
|
|
109
170
|
|
|
110
171
|
From the repository root:
|
|
@@ -116,6 +177,9 @@ uv run --dev pytest packages/pycodex-ws/tests
|
|
|
116
177
|
uv build packages/pycodex-ws
|
|
117
178
|
```
|
|
118
179
|
|
|
119
|
-
Python integrations
|
|
120
|
-
|
|
180
|
+
Python integrations construct a runtime through `AgentsManager.create_root_runtime()`
|
|
181
|
+
and call `pycodex_ws.register_feishu_commands(runtime)` to include `/link` and
|
|
182
|
+
`/unlink`. The context indicator shows tokens against the auto-compaction limit;
|
|
183
|
+
its expandable Agent tree shows each task's status and latest assistant text.
|
|
184
|
+
See [runtime contracts](../../docs/RUNTIME.md) and
|
|
121
185
|
[test documentation](../../tests/TESTS.md).
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# pycodex-ws
|
|
2
|
+
|
|
3
|
+
Browser conversations, a live HTML Canvas, read-only file/diff viewers and
|
|
4
|
+
optional Feishu integration for
|
|
5
|
+
[`python-codex`](https://pypi.org/project/python-codex/).
|
|
6
|
+
Requires Python 3.7+ and installs the matching core version.
|
|
7
|
+
|
|
8
|
+
See the [0.6.0 release notes](https://github.com/Randomizez/pycodex/blob/v0.6.0/docs/RELEASE_0.6.0.md) for changes and
|
|
9
|
+
integration migration from 0.5.0.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python3 -m pip install pycodex-ws
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Create `workspaces.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"workspaces": [
|
|
22
|
+
{"id": "demo", "work_dir": ".", "state_path": "demo", "enable_board": true}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pycodex-ws --listen 127.0.0.1:6007 --workspace-config workspaces.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Open `http://127.0.0.1:6007`. Use `python3 -m workspace_server` if the command
|
|
32
|
+
is not on `PATH`. Keep stdin open while the server runs; EOF initiates shutdown.
|
|
33
|
+
Model configuration comes from `~/.codex/config.toml`.
|
|
34
|
+
|
|
35
|
+
## Conversations and views
|
|
36
|
+
|
|
37
|
+
The session sidebar starts collapsed. Hover to preview it, click to keep it
|
|
38
|
+
open, and click the toggle or empty space again to close it. Expansion narrows
|
|
39
|
+
the conversation while keeping the board fixed. On phones, use the header
|
|
40
|
+
buttons to switch between chat and board.
|
|
41
|
+
|
|
42
|
+
Use the expanded sidebar to create, rename or close sessions. Status dots show
|
|
43
|
+
running sessions and unread replies. `/cd <directory>` changes the current
|
|
44
|
+
session's working directory while idle; `/model`, `/resume`, `/compact` and
|
|
45
|
+
other core session commands are also available.
|
|
46
|
+
|
|
47
|
+
Consecutive model reconnection diagnostics share one collapsed `Model retries`
|
|
48
|
+
row per sequence. The summary shows the number of recorded retries, the latest
|
|
49
|
+
attempt and its HTTP status when available. Expand it to read the original errors
|
|
50
|
+
in a scrollable log. New retries update the same row and preserve its open state;
|
|
51
|
+
refreshing starts collapsed. Other model diagnostics and final errors stay visible.
|
|
52
|
+
|
|
53
|
+
Click Markdown links to open files and comparisons, for example
|
|
54
|
+
`[source](app.py:42)` or `[changes](diff:HEAD:app.py)`. In a code or diff view,
|
|
55
|
+
select text and choose **Insert into chat** to add editable context to the draft.
|
|
56
|
+
Click a context block to edit or remove it before sending.
|
|
57
|
+
See [view links and supported formats](../../docs/RUNTIME.md#workspace-content-views).
|
|
58
|
+
|
|
59
|
+
When using a router, its directory groups workspaces by device and lists unread
|
|
60
|
+
workspaces at the top. Device handles reorder the list; pencil buttons edit
|
|
61
|
+
device names.
|
|
62
|
+
|
|
63
|
+
The runtime information shared between Agents recommends plain messages, which
|
|
64
|
+
steer active work at the next safe boundary. Use `/queue` explicitly for an
|
|
65
|
+
independent follow-up task that should wait for current work to finish.
|
|
66
|
+
|
|
67
|
+
The message API also accepts a string `name`, defaulting to `"user"`, for
|
|
68
|
+
identifying the source of a user message. The history API returns it, and
|
|
69
|
+
recorded sessions preserve it across resume. It is metadata only: the model request and frontend display do
|
|
70
|
+
not include it. See the [message API contract](workspace_server/WORKSPACE_API.md#commands-and-results).
|
|
71
|
+
|
|
72
|
+
The [VS Code extension](extensions/vscode/README.md) uses this backend and is
|
|
73
|
+
distributed separately as a VSIX.
|
|
74
|
+
|
|
75
|
+
## Customize the interface
|
|
76
|
+
|
|
77
|
+
Click the gear button and choose **Frontend customization** to start a
|
|
78
|
+
customization conversation with the Agent.
|
|
79
|
+
Custom files live in the server user's `~/.pycodex/frontend/`:
|
|
80
|
+
|
|
81
|
+
- `custom.css` and `custom.js`: optional styles and an ES module, loaded automatically.
|
|
82
|
+
- `workspace.html`: optional full-page override; otherwise the packaged page is used.
|
|
83
|
+
- Other files: available at workspace-relative `frontend/<path>` URLs.
|
|
84
|
+
|
|
85
|
+
Refresh to apply changes and keep the selected conversation. Open the workspace
|
|
86
|
+
with `?builtin=1` to bypass all customizations; click “Re-enable customizations”
|
|
87
|
+
to return. Changes apply to all workspaces for that OS user.
|
|
88
|
+
See the [customization contract](../../docs/RUNTIME.md#frontend-customization).
|
|
89
|
+
For custom interfaces, the [resource API](../../docs/WORKSPACE_API.md) exposes
|
|
90
|
+
state, model catalogs, asynchronous command results, paginated history, event
|
|
91
|
+
replay and sub-agent/background-work inventories. A `mount(host)` export in
|
|
92
|
+
`custom.js` can access the built-in page's session, draft and view controls.
|
|
93
|
+
|
|
94
|
+
Backend module ownership and the preserved session interfaces are documented in
|
|
95
|
+
the [runtime guide](../../docs/RUNTIME.md#workspace-session-api).
|
|
96
|
+
File/Git source resolution and view storage retain their existing public classes;
|
|
97
|
+
see [content views](../../docs/RUNTIME.md#workspace-content-views).
|
|
98
|
+
|
|
99
|
+
Ask the Agent to show the [feature catalog](workspace_server/optional_features/README.md)
|
|
100
|
+
in the view area, then choose features by name or ID in chat. The Agent adapts
|
|
101
|
+
their source into your frontend. To share a local change, ask it to publish a
|
|
102
|
+
feature PR on GitHub; the [contribution guide](workspace_server/optional_features/CONTRIBUTING.md)
|
|
103
|
+
specifies the source, documentation, validation and PR format.
|
|
104
|
+
|
|
105
|
+
## Share a workspace
|
|
106
|
+
|
|
107
|
+
Start the workspace server with an administrator password (`--password`), then
|
|
108
|
+
choose **Settings → Share** in a workspace. Set a different share password and
|
|
109
|
+
send the displayed workspace link and password to the recipient.
|
|
110
|
+
|
|
111
|
+
The existing login page accepts either the administrator password or the share
|
|
112
|
+
password for the workspace being opened. Visitors can use all its conversations,
|
|
113
|
+
Canvas, files, APIs and WebSocket connections, but cannot access workspace
|
|
114
|
+
management, another workspace or sharing settings. The settings button and
|
|
115
|
+
management navigation are hidden for visitors.
|
|
116
|
+
|
|
117
|
+
Passwords, verification values and visitor tokens are kept only in process
|
|
118
|
+
memory. Changing the password or stopping sharing invalidates existing visitor
|
|
119
|
+
logins and disconnects their WebSockets. Restarting the process clears all
|
|
120
|
+
shares. Already accepted Agent work continues.
|
|
121
|
+
|
|
122
|
+
This restricts web access; Agent tools retain their existing file and execution
|
|
123
|
+
permissions. An unprotected server must configure its administrator password
|
|
124
|
+
before enabling sharing. Router authentication is unchanged: a link through a
|
|
125
|
+
router still requires its normal login.
|
|
126
|
+
|
|
127
|
+
## Storage
|
|
128
|
+
|
|
129
|
+
Paths in `workspaces.json` resolve from the configuration directory.
|
|
130
|
+
`state_path` is a unique filename prefix: the example saves sessions in
|
|
131
|
+
`demo.pycodex-ws.json`, file views in `demo.pycodex-views.json`, and Canvas in
|
|
132
|
+
`demo.html`. Suffixes are appended to the complete prefix, including any dots.
|
|
133
|
+
|
|
134
|
+
Set `enable_board` to `false` to disable Canvas while retaining conversations
|
|
135
|
+
and file views. Session directories and history survive server restarts;
|
|
136
|
+
changing a session's directory does not move Canvas or workspace state files.
|
|
137
|
+
Configuration and restoration details are in the [runtime guide](../../docs/RUNTIME.md).
|
|
138
|
+
|
|
139
|
+
## Feishu
|
|
140
|
+
|
|
141
|
+
Install the optional SDK in the workspace's Python environment, then authorize:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
python3 -m pip install lark-oapi
|
|
145
|
+
python3 -m pycodex_ws.feishu_oauth
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Authorization reads app credentials from `~/.codex/.env` and stores the refresh
|
|
149
|
+
token in `~/.codex/.feishu_refresh_token`. In a workspace session, use
|
|
150
|
+
`/link <feishu-email|open_id|chat_id>` to connect a card and `/unlink` to disconnect.
|
|
151
|
+
Other workspace features work without the SDK.
|
|
152
|
+
|
|
153
|
+
The adapter separates card rendering, refresh-token storage and SDK connections.
|
|
154
|
+
`PycodexCard` and `PycodexRuntimeLink` keep their existing imports and interfaces;
|
|
155
|
+
see [connection lifecycle](../../docs/RUNTIME.md#feishu-connection-lifecycle)
|
|
156
|
+
and [event presentation](../../docs/RUNTIME.md#observation).
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
From the repository root:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
uv sync --dev
|
|
164
|
+
uv run pycodex-ws --help
|
|
165
|
+
uv run --dev pytest packages/pycodex-ws/tests
|
|
166
|
+
uv build packages/pycodex-ws
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Python integrations construct a runtime through `AgentsManager.create_root_runtime()`
|
|
170
|
+
and call `pycodex_ws.register_feishu_commands(runtime)` to include `/link` and
|
|
171
|
+
`/unlink`. The context indicator shows tokens against the auto-compaction limit;
|
|
172
|
+
its expandable Agent tree shows each task's status and latest assistant text.
|
|
173
|
+
See [runtime contracts](../../docs/RUNTIME.md) and
|
|
174
|
+
[test documentation](../../tests/TESTS.md).
|
|
@@ -15,15 +15,15 @@ from starlette.requests import HTTPConnection
|
|
|
15
15
|
from starlette.responses import HTMLResponse, JSONResponse
|
|
16
16
|
from starlette.routing import Mount, Route, WebSocketRoute
|
|
17
17
|
from starlette.websockets import WebSocketDisconnect, WebSocketState
|
|
18
|
-
from pycodex.portable.
|
|
18
|
+
from pycodex.portable.protocol import base58_encode
|
|
19
19
|
from pycodex.utils.session_persist import resolve_codex_home
|
|
20
|
-
from
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
from pycodex_ws.server_support import (
|
|
21
|
+
auth_cookie_matches,
|
|
22
|
+
install_auth,
|
|
23
|
+
json_object_body,
|
|
24
24
|
parse_listen,
|
|
25
25
|
)
|
|
26
|
-
from
|
|
26
|
+
from pycodex_ws.workspace_config import normalize_workspace_id
|
|
27
27
|
|
|
28
28
|
from .browser import BrowserAccess, require_login
|
|
29
29
|
from .protocol import (
|
|
@@ -52,7 +52,7 @@ def load_registration_key(path):
|
|
|
52
52
|
except FileExistsError:
|
|
53
53
|
value = path.read_bytes().strip()
|
|
54
54
|
else:
|
|
55
|
-
value =
|
|
55
|
+
value = base58_encode(os.urandom(16)).encode("ascii")
|
|
56
56
|
with os.fdopen(descriptor, "wb") as handle:
|
|
57
57
|
handle.write(value + b"\n")
|
|
58
58
|
if not re.fullmatch(b"[1-9A-HJ-NP-Za-km-z]{1,22}", value):
|
|
@@ -162,7 +162,7 @@ class Router:
|
|
|
162
162
|
self.api_url = normalize_api_url(api_url) if api_url is not None else None
|
|
163
163
|
self.api_port = api_port
|
|
164
164
|
self.registration_key = (
|
|
165
|
-
|
|
165
|
+
base58_encode(os.urandom(16))
|
|
166
166
|
if registration_key is None
|
|
167
167
|
else registration_key
|
|
168
168
|
)
|
|
@@ -208,11 +208,11 @@ class Router:
|
|
|
208
208
|
self.aliases = aliases
|
|
209
209
|
|
|
210
210
|
async def reorder_devices(self, request):
|
|
211
|
-
if not self.browser_token or not
|
|
211
|
+
if not self.browser_token or not auth_cookie_matches(
|
|
212
212
|
self.browser_token, request.cookies.get("pycodex_router_auth")
|
|
213
213
|
):
|
|
214
214
|
raise HTTPException(401, "Authentication required.")
|
|
215
|
-
payload = await
|
|
215
|
+
payload = await json_object_body(request, max_bytes=64 * 1024)
|
|
216
216
|
order = payload.get("order")
|
|
217
217
|
if (
|
|
218
218
|
not isinstance(order, list)
|
|
@@ -235,14 +235,14 @@ class Router:
|
|
|
235
235
|
return JSONResponse({"order": order})
|
|
236
236
|
|
|
237
237
|
async def rename_device(self, request):
|
|
238
|
-
if not self.browser_token or not
|
|
238
|
+
if not self.browser_token or not auth_cookie_matches(
|
|
239
239
|
self.browser_token, request.cookies.get("pycodex_router_auth")
|
|
240
240
|
):
|
|
241
241
|
raise HTTPException(401, "Authentication required.")
|
|
242
242
|
node_id = request.path_params["node_id"]
|
|
243
243
|
if node_id not in self.nodes:
|
|
244
244
|
raise HTTPException(404, "Device is not connected.")
|
|
245
|
-
payload = await
|
|
245
|
+
payload = await json_object_body(request)
|
|
246
246
|
try:
|
|
247
247
|
alias = normalize_alias(payload.get("alias"))
|
|
248
248
|
except ValueError as exc:
|
|
@@ -407,7 +407,7 @@ class Router:
|
|
|
407
407
|
agent_access = trusted_agent_request(scope)
|
|
408
408
|
if blocked or not (
|
|
409
409
|
agent_access
|
|
410
|
-
or
|
|
410
|
+
or auth_cookie_matches(
|
|
411
411
|
self.browser_token, connection.cookies.get("pycodex_router_auth")
|
|
412
412
|
)
|
|
413
413
|
):
|
|
@@ -535,7 +535,7 @@ def create_app(
|
|
|
535
535
|
)
|
|
536
536
|
|
|
537
537
|
async def registration_key_info(request):
|
|
538
|
-
if not router.browser_token or not
|
|
538
|
+
if not router.browser_token or not auth_cookie_matches(
|
|
539
539
|
router.browser_token, request.cookies.get("pycodex_router_auth")
|
|
540
540
|
):
|
|
541
541
|
return JSONResponse({"error": "authentication required"}, status_code=401)
|
|
@@ -553,7 +553,7 @@ def create_app(
|
|
|
553
553
|
Route("/api/nodes/{node_id}/alias", router.rename_device, methods=["POST"]),
|
|
554
554
|
]
|
|
555
555
|
)
|
|
556
|
-
router.browser_token =
|
|
556
|
+
router.browser_token = install_auth(
|
|
557
557
|
web,
|
|
558
558
|
password,
|
|
559
559
|
cookie_name="pycodex_router_auth",
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from importlib import import_module
|
|
4
4
|
|
|
5
|
-
from pycodex.bootstrap import build_runtime as build_core_runtime
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
runtime = build_core_runtime(agent)
|
|
6
|
+
def register_feishu_commands(runtime):
|
|
7
|
+
"""Attach Feishu commands and connection cleanup to an existing runtime."""
|
|
10
8
|
link = None
|
|
11
9
|
|
|
12
10
|
async def unlink(argument):
|
|
@@ -52,4 +50,3 @@ def build_runtime(agent):
|
|
|
52
50
|
runtime.register_command("link", connect)
|
|
53
51
|
runtime.register_command("unlink", unlink)
|
|
54
52
|
runtime.add_close_handler(close_link)
|
|
55
|
-
return runtime
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Shared Feishu endpoints and refresh-token storage."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import typing
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
|
8
|
+
FEISHU_DOMAIN = "https://open.feishu.cn"
|
|
9
|
+
FEISHU_REFRESH_TOKEN_FILE = "~/.codex/.feishu_refresh_token"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def refresh_token_path() -> Path:
|
|
13
|
+
return Path(FEISHU_REFRESH_TOKEN_FILE).expanduser()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_refresh_token() -> "typing.Union[str, None]":
|
|
17
|
+
path = refresh_token_path()
|
|
18
|
+
if not path.exists():
|
|
19
|
+
return None
|
|
20
|
+
value = path.read_text(encoding="utf-8", errors="replace").strip()
|
|
21
|
+
return value or None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def write_refresh_token(value: str) -> None:
|
|
25
|
+
path = refresh_token_path()
|
|
26
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
descriptor = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
28
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
29
|
+
path.chmod(0o600)
|
|
30
|
+
stream.write("{0}\n".format(value))
|