@widgex/wgx 0.0.0-stage → 0.4.2
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.txt +49 -0
- package/README.md +33 -2
- package/ThirdPartyNotices.txt +4685 -0
- package/package.json +30 -4
- package/provider-clis.json +1211 -0
- package/python/XACTOR.md +146 -0
- package/python/browser_actor_host.py +456 -0
- package/python/cdp_page.py +363 -0
- package/python/harness_check.py +77 -0
- package/python/requirements.txt +1 -0
- package/python/test_line_endings.py +185 -0
- package/python/xactor/__init__.py +18 -0
- package/python/xactor/__main__.py +226 -0
- package/python/xactor/api.py +882 -0
- package/python/xactor/bridge.py +784 -0
- package/python/xactor/dom.py +117 -0
- package/python/xactor/man/add_card.md +89 -0
- package/python/xactor/man/agent-vs-fork.md +108 -0
- package/python/xactor/man/agent.md +333 -0
- package/python/xactor/man/ask_model.md +110 -0
- package/python/xactor/man/ask_reference.md +56 -0
- package/python/xactor/man/ask_user.md +103 -0
- package/python/xactor/man/attach_app.md +595 -0
- package/python/xactor/man/attach_ide.md +65 -0
- package/python/xactor/man/attach_ide_surface.md +31 -0
- package/python/xactor/man/await_user.md +214 -0
- package/python/xactor/man/bash.md +142 -0
- package/python/xactor/man/browser-driver.md +137 -0
- package/python/xactor/man/browser_activity.md +53 -0
- package/python/xactor/man/browser_attach.md +84 -0
- package/python/xactor/man/browser_close_tab.md +50 -0
- package/python/xactor/man/browser_detach.md +50 -0
- package/python/xactor/man/browser_exec.md +66 -0
- package/python/xactor/man/browser_focus_tab.md +47 -0
- package/python/xactor/man/browser_hybrid.md +47 -0
- package/python/xactor/man/browser_open.md +50 -0
- package/python/xactor/man/browser_open_tab.md +58 -0
- package/python/xactor/man/browser_plumbing.md +107 -0
- package/python/xactor/man/browser_screenshot.md +56 -0
- package/python/xactor/man/browser_snapshot.md +52 -0
- package/python/xactor/man/browser_tabs.md +67 -0
- package/python/xactor/man/bulletin.md +88 -0
- package/python/xactor/man/capture_page.md +54 -0
- package/python/xactor/man/cd.md +64 -0
- package/python/xactor/man/chess_move.md +53 -0
- package/python/xactor/man/children.md +89 -0
- package/python/xactor/man/close_task.md +14 -0
- package/python/xactor/man/compact.md +75 -0
- package/python/xactor/man/drive.md +140 -0
- package/python/xactor/man/drive_space.md +72 -0
- package/python/xactor/man/driving.md +168 -0
- package/python/xactor/man/edit_file.md +112 -0
- package/python/xactor/man/finding.md +194 -0
- package/python/xactor/man/fork_agent.md +45 -0
- package/python/xactor/man/glob.md +83 -0
- package/python/xactor/man/grep.md +115 -0
- package/python/xactor/man/harness.md +165 -0
- package/python/xactor/man/harness_check.md +70 -0
- package/python/xactor/man/harness_format.md +257 -0
- package/python/xactor/man/highlight.md +66 -0
- package/python/xactor/man/instruct.md +111 -0
- package/python/xactor/man/ls.md +63 -0
- package/python/xactor/man/man.md +86 -0
- package/python/xactor/man/merge_wiz.md +40 -0
- package/python/xactor/man/notebook.md +98 -0
- package/python/xactor/man/overview.md +89 -0
- package/python/xactor/man/permissions.md +137 -0
- package/python/xactor/man/phase.md +150 -0
- package/python/xactor/man/powershell.md +74 -0
- package/python/xactor/man/profiles.md +60 -0
- package/python/xactor/man/project_drive.md +49 -0
- package/python/xactor/man/read_bulletin.md +89 -0
- package/python/xactor/man/read_file.md +90 -0
- package/python/xactor/man/relay.md +216 -0
- package/python/xactor/man/reply.md +72 -0
- package/python/xactor/man/reply_message.md +78 -0
- package/python/xactor/man/report_bug.md +118 -0
- package/python/xactor/man/request_answer.md +142 -0
- package/python/xactor/man/request_merge_decisions.md +121 -0
- package/python/xactor/man/request_operator_action.md +152 -0
- package/python/xactor/man/request_review.md +122 -0
- package/python/xactor/man/request_user_action.md +126 -0
- package/python/xactor/man/return_card.md +71 -0
- package/python/xactor/man/run.md +99 -0
- package/python/xactor/man/sandbox.md +144 -0
- package/python/xactor/man/session_fork.md +74 -0
- package/python/xactor/man/session_harness.md +109 -0
- package/python/xactor/man/session_join.md +102 -0
- package/python/xactor/man/session_land.md +73 -0
- package/python/xactor/man/session_nudge.md +73 -0
- package/python/xactor/man/session_rest.md +73 -0
- package/python/xactor/man/session_scope.md +84 -0
- package/python/xactor/man/session_terminate.md +58 -0
- package/python/xactor/man/session_token.md +78 -0
- package/python/xactor/man/sessions.md +93 -0
- package/python/xactor/man/set_harness.md +15 -0
- package/python/xactor/man/spawn_swarm.md +80 -0
- package/python/xactor/man/submit_evidence.md +54 -0
- package/python/xactor/man/swarm_brief.md +30 -0
- package/python/xactor/man/task_status.md +82 -0
- package/python/xactor/man/tasks.md +47 -0
- package/python/xactor/man/todo_write.md +104 -0
- package/python/xactor/man/update_card.md +70 -0
- package/python/xactor/man/use_profile.md +90 -0
- package/python/xactor/man/use_widget.md +82 -0
- package/python/xactor/man/wait_for_task.md +105 -0
- package/python/xactor/man/widget-model.md +284 -0
- package/python/xactor/man/widgets.md +59 -0
- package/python/xactor/man/write_file.md +76 -0
- package/python/xactor/man/writing-a-widget.md +142 -0
- package/python/xactor/man/xylon.md +120 -0
- package/python/xactor/man/xylon_blame.md +80 -0
- package/python/xactor/man/xylon_read.md +131 -0
- package/python/xactor/man/xylon_search.md +125 -0
- package/python/xactor/manual.py +195 -0
- package/python/xactor/parent_watch.py +115 -0
- package/python/xactor/plan_mode.py +350 -0
- package/python/xactor/sandbox.py +856 -0
- package/python/xactor/seals.py +438 -0
- package/python/xactor/shell_hints.py +210 -0
- package/python/xactor/states/__init__.py +13 -0
- package/python/xactor/states/base.py +523 -0
- package/python/xactor/states/registry.py +274 -0
- package/python/xactor/stdlib_api.py +6345 -0
- package/python/xactor/widgets/__init__.py +1 -0
- package/python/xactor/widgets/github_tokens.py +314 -0
- package/python/xactor/widgets/google_aistudio.py +365 -0
- package/settings-schema.json +403 -0
- package/viewer/index.css +160 -0
- package/viewer/index.js +15 -0
- package/wgx.mjs +290659 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Daemon entry point: python -m xactor --dispatcher-id <id> [--backend browser|stdlib]"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
# The host puts this package on PYTHONPATH rather than starting us inside it
|
|
8
|
+
# (a working folder inside a worktree keeps Windows from ever deleting the
|
|
9
|
+
# tree). `sys.path` was settled when the interpreter started, so the variable
|
|
10
|
+
# has done its job; put back what the user had, so agent commands spawned from
|
|
11
|
+
# here see their own environment rather than ours.
|
|
12
|
+
_PYTHONPATH_BEFORE = os.environ.pop("WIDGEX_PYTHONPATH_BEFORE", None)
|
|
13
|
+
if _PYTHONPATH_BEFORE is not None:
|
|
14
|
+
if _PYTHONPATH_BEFORE:
|
|
15
|
+
os.environ["PYTHONPATH"] = _PYTHONPATH_BEFORE
|
|
16
|
+
else:
|
|
17
|
+
os.environ.pop("PYTHONPATH", None)
|
|
18
|
+
|
|
19
|
+
from . import bridge, plan_mode # noqa: E402
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _start_browser_backend(dispatcher_id: str) -> None:
|
|
23
|
+
"""Original browser automation backend — requires a CDP dispatcher."""
|
|
24
|
+
from .api import ElementaryAPI
|
|
25
|
+
from .dom import DOMSnapshot, HybridNode, HybridDiff
|
|
26
|
+
from .states import PageState, LoadingState, Observation, Transition, Step, StateRegistry
|
|
27
|
+
|
|
28
|
+
api = ElementaryAPI()
|
|
29
|
+
|
|
30
|
+
bridge.set_exec_scope({
|
|
31
|
+
"api": api,
|
|
32
|
+
"asyncio": asyncio,
|
|
33
|
+
# The host composes exec code for `run_transition`, and needs one safe
|
|
34
|
+
# way to hand structured parameters across that boundary. JSON is that
|
|
35
|
+
# way: `json.loads` of a string literal cannot be talked into
|
|
36
|
+
# evaluating anything, whereas a Python dict literal assembled by
|
|
37
|
+
# string concatenation is one `true` away from a NameError and one
|
|
38
|
+
# quote away from being an injection.
|
|
39
|
+
"json": __import__("json"),
|
|
40
|
+
"DOMSnapshot": DOMSnapshot,
|
|
41
|
+
"HybridNode": HybridNode,
|
|
42
|
+
"HybridDiff": HybridDiff,
|
|
43
|
+
# Layer 2
|
|
44
|
+
"PageState": PageState,
|
|
45
|
+
"LoadingState": LoadingState,
|
|
46
|
+
"Observation": Observation,
|
|
47
|
+
"Transition": Transition,
|
|
48
|
+
"Step": Step,
|
|
49
|
+
"registry": StateRegistry,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _graft_browser_l1(api: object) -> None:
|
|
54
|
+
"""Put the Layer 1 browser verbs on the stdlib `api`, as bound methods.
|
|
55
|
+
|
|
56
|
+
The browser backend used to be a *second* Python process whose only real
|
|
57
|
+
job was holding this namespace: the terminal already reached every browser
|
|
58
|
+
command through `api.browser_*`, so the extra interpreter existed so that
|
|
59
|
+
`api.click(...)` and `registry` would resolve somewhere. That made
|
|
60
|
+
`browser_exec` a courier — the terminal shipped a string of Python to
|
|
61
|
+
another process to run — and cost an interpreter spawn on every browser
|
|
62
|
+
open.
|
|
63
|
+
|
|
64
|
+
Grafting rather than nesting under `api.browser` keeps every man page and
|
|
65
|
+
every habit intact: the verbs read the same here as they did there.
|
|
66
|
+
|
|
67
|
+
Grafting bound methods, rather than delegating via `__getattr__`, is what
|
|
68
|
+
keeps `dir(api)` honest and tracebacks pointing at `ElementaryAPI`. The
|
|
69
|
+
queue state lives on the instance the methods are bound to, so `click()`
|
|
70
|
+
and the `drain()` that collects it stay on the same object.
|
|
71
|
+
"""
|
|
72
|
+
from .api import ElementaryAPI
|
|
73
|
+
|
|
74
|
+
browser = ElementaryAPI()
|
|
75
|
+
# So a verb that hands `self` onward — `here()` building a PageState — gives
|
|
76
|
+
# it the whole api rather than this half of it. See `ElementaryAPI._surface`.
|
|
77
|
+
browser._merged = api
|
|
78
|
+
for name in dir(browser):
|
|
79
|
+
if name.startswith("_"):
|
|
80
|
+
continue
|
|
81
|
+
# The two surfaces are disjoint today (28 verbs against 58, no overlap).
|
|
82
|
+
# Nothing enforces that, and a later `StdlibAPI.select` would silently
|
|
83
|
+
# shadow the browser's — or be shadowed, depending on which way the
|
|
84
|
+
# loop ran. Refusing to start is the only failure mode here that cannot
|
|
85
|
+
# be mistaken for the verb simply misbehaving.
|
|
86
|
+
if hasattr(api, name):
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
f"Cannot merge browser Layer 1 into the shell: `api.{name}` "
|
|
89
|
+
f"already exists on StdlibAPI. Rename one of them."
|
|
90
|
+
)
|
|
91
|
+
# Wrapped, because this `setattr` is the reason these verbs have never
|
|
92
|
+
# been gated: `_gate_api_methods` decorates what is in the class body,
|
|
93
|
+
# and these arrive afterwards. `guard` returns the function untouched
|
|
94
|
+
# when Plan mode permits it, so the read verbs cost nothing.
|
|
95
|
+
setattr(api, name, plan_mode.guard(name, getattr(browser, name)))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _start_stdlib_backend(cwd: str, sandbox_mode: str, fallback_cwd: str | None = None,
|
|
99
|
+
fence: str | None = None, allow: list[str] | None = None) -> None:
|
|
100
|
+
"""Stdlib coding backend — file I/O, subprocess, search, sessions, xylon, browser."""
|
|
101
|
+
from .stdlib_api import StdlibAPI
|
|
102
|
+
from . import sandbox
|
|
103
|
+
|
|
104
|
+
# Install the sandbox BEFORE any user code can run
|
|
105
|
+
sandbox.configure(
|
|
106
|
+
cwd=cwd,
|
|
107
|
+
mode=sandbox_mode,
|
|
108
|
+
allow_subprocess=(sandbox_mode in ("permissive", "open")),
|
|
109
|
+
allow_network=(sandbox_mode == "open"),
|
|
110
|
+
# The task's other working copies (its slots on other boards): one
|
|
111
|
+
# task's folders, read and written without asking.
|
|
112
|
+
read_allow=list(allow or []),
|
|
113
|
+
write_allow=list(allow or []),
|
|
114
|
+
)
|
|
115
|
+
if fence is not None:
|
|
116
|
+
# A drive-channel task: strict paths rooted at its folder, so a read
|
|
117
|
+
# outside it is refused (and asked about) rather than allowed the way
|
|
118
|
+
# permissive allows it. Subprocesses stay on — `api.bash` is how the
|
|
119
|
+
# agent works, and the host asks about each command it runs — which
|
|
120
|
+
# strict alone would turn off. An empty fence is one the host could
|
|
121
|
+
# not name; it admits no folder, so every path is asked about.
|
|
122
|
+
sandbox.configure(cwd=fence or cwd, mode="strict", fence=fence, allow_subprocess=True)
|
|
123
|
+
sandbox.install()
|
|
124
|
+
|
|
125
|
+
from .dom import DOMSnapshot, HybridNode, HybridDiff
|
|
126
|
+
from .states import PageState, LoadingState, Observation, Transition, Step, StateRegistry
|
|
127
|
+
|
|
128
|
+
api = StdlibAPI(cwd=cwd, bridge=bridge, fallback_cwd=fallback_cwd)
|
|
129
|
+
_graft_browser_l1(api)
|
|
130
|
+
|
|
131
|
+
# os/Path/json stay bound for path and data manipulation; the operations
|
|
132
|
+
# they expose (open, os.remove, ...) are denied by the audit hook unless
|
|
133
|
+
# they happen inside an api.* call.
|
|
134
|
+
bridge.set_exec_scope({
|
|
135
|
+
"api": api,
|
|
136
|
+
"asyncio": asyncio,
|
|
137
|
+
# Re-export stdlib modules for convenience in exec scope.
|
|
138
|
+
#
|
|
139
|
+
# `os` is the sandboxed one. Every other way into the OS raises an
|
|
140
|
+
# audit event the hook above can deny; reading `os.environ` raises
|
|
141
|
+
# none, so it was the one part of the outside world agent code could
|
|
142
|
+
# reach directly, in every permission mode. See sandbox._GuardedOS —
|
|
143
|
+
# path manipulation, which is why `os` is bound at all, is untouched.
|
|
144
|
+
"os": sandbox.guarded_os(),
|
|
145
|
+
"Path": __import__("pathlib").Path,
|
|
146
|
+
"json": __import__("json"),
|
|
147
|
+
# Layer 2, formerly the browser process's whole reason to exist. Widget
|
|
148
|
+
# files are read by the host and arrive here as exec'd source, so they
|
|
149
|
+
# need these bound — they import nothing, by design, and could not
|
|
150
|
+
# import under `restrict_builtins` anyway.
|
|
151
|
+
"DOMSnapshot": DOMSnapshot,
|
|
152
|
+
"HybridNode": HybridNode,
|
|
153
|
+
"HybridDiff": HybridDiff,
|
|
154
|
+
"PageState": PageState,
|
|
155
|
+
"LoadingState": LoadingState,
|
|
156
|
+
"Observation": Observation,
|
|
157
|
+
"Transition": Transition,
|
|
158
|
+
"Step": Step,
|
|
159
|
+
"registry": StateRegistry,
|
|
160
|
+
}, restrict_builtins=True)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main() -> None:
|
|
164
|
+
parser = argparse.ArgumentParser()
|
|
165
|
+
parser.add_argument("--dispatcher-id", default="stdlib")
|
|
166
|
+
parser.add_argument("--backend", choices=["browser", "stdlib"], default="browser")
|
|
167
|
+
parser.add_argument("--cwd", default=None, help="Working directory for stdlib backend")
|
|
168
|
+
parser.add_argument("--requested-cwd", default=None,
|
|
169
|
+
help="What the host originally asked for, for disclosure")
|
|
170
|
+
parser.add_argument("--fallback-cwd", default=None,
|
|
171
|
+
help="Directory to use when --cwd no longer exists")
|
|
172
|
+
parser.add_argument("--sandbox", default="permissive",
|
|
173
|
+
choices=["strict", "permissive", "open"],
|
|
174
|
+
help="Sandbox permission mode")
|
|
175
|
+
parser.add_argument("--fence", default=None,
|
|
176
|
+
help="The only folder file calls may use without asking")
|
|
177
|
+
parser.add_argument("--allow", action="append", default=[],
|
|
178
|
+
help="A folder of the same task, read and written without asking")
|
|
179
|
+
args = parser.parse_args()
|
|
180
|
+
|
|
181
|
+
# Before the sandbox goes on, which refuses some of what watching needs.
|
|
182
|
+
# See `parent_watch` for why EOF alone did not end these daemons.
|
|
183
|
+
from . import parent_watch
|
|
184
|
+
parent_watch.watch(parent_watch.leave)
|
|
185
|
+
|
|
186
|
+
if args.backend == "stdlib":
|
|
187
|
+
import os
|
|
188
|
+
cwd = args.cwd or os.getcwd()
|
|
189
|
+
# The host checked this a moment ago, but the directory can be gone by
|
|
190
|
+
# the time we start — and a daemon that dies here reports ENOENT against
|
|
191
|
+
# the interpreter, naming neither the session nor the missing path.
|
|
192
|
+
if not os.path.isdir(cwd):
|
|
193
|
+
for candidate in (args.fallback_cwd, os.path.expanduser("~")):
|
|
194
|
+
if candidate and os.path.isdir(candidate):
|
|
195
|
+
bridge._emit({
|
|
196
|
+
"event": "cwd_fallback",
|
|
197
|
+
"requested": args.requested_cwd or cwd,
|
|
198
|
+
"effective": candidate,
|
|
199
|
+
})
|
|
200
|
+
cwd = candidate
|
|
201
|
+
break
|
|
202
|
+
_start_stdlib_backend(cwd, args.sandbox, fallback_cwd=args.fallback_cwd, fence=args.fence, allow=args.allow)
|
|
203
|
+
else:
|
|
204
|
+
_start_browser_backend(args.dispatcher_id)
|
|
205
|
+
|
|
206
|
+
loop = asyncio.new_event_loop()
|
|
207
|
+
asyncio.set_event_loop(loop)
|
|
208
|
+
|
|
209
|
+
# The read loop is the only thing servicing stdin. If it ever stops, every
|
|
210
|
+
# bridge call still waiting is unresolvable and every later one is too — the
|
|
211
|
+
# session looks alive and answers nothing. It used to die silently; now its
|
|
212
|
+
# death is announced and the pending calls are failed rather than orphaned.
|
|
213
|
+
_read_task = loop.create_task(bridge._read_loop())
|
|
214
|
+
_read_task.add_done_callback(bridge._read_loop_stopped)
|
|
215
|
+
|
|
216
|
+
bridge._emit({
|
|
217
|
+
"event": "ready",
|
|
218
|
+
"dispatcher_id": args.dispatcher_id,
|
|
219
|
+
"backend": args.backend,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
loop.run_forever()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|