@widgex/wgx 0.0.0-stage → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +49 -0
- package/README.md +33 -2
- package/ThirdPartyNotices.txt +4802 -0
- package/package.json +29 -4
- 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 +273753 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""What Plan mode permits, and the refusal for everything else.
|
|
2
|
+
|
|
3
|
+
Plan mode is read-only, and the enforcement is here rather than at the
|
|
4
|
+
shell-exec boundary for the reason the whole permission model is per-call:
|
|
5
|
+
gating the enclosing exec cannot tell `api.read_file` from `api.write_file`,
|
|
6
|
+
so it asked about both. That made planning an approval loop, which is the
|
|
7
|
+
complaint this module exists to answer.
|
|
8
|
+
|
|
9
|
+
**An allowlist, not a denylist**, for a reason the codebase already
|
|
10
|
+
demonstrates. `_gate_api_methods` iterates `vars(cls)` at class-definition
|
|
11
|
+
time and therefore silently omits the 34 verbs `_graft_browser_l1` attaches
|
|
12
|
+
later — a denylist would inherit exactly that shape of bug, where a new verb
|
|
13
|
+
is dangerous by default and safe only if someone remembers. Inverted, a new
|
|
14
|
+
verb is *broken in Plan mode* until someone opts it in: loud, immediate, and
|
|
15
|
+
caught by the coverage sweep in `test/planModeApiSurface.test.mjs` rather
|
|
16
|
+
than by a user discovering their plan edited something.
|
|
17
|
+
|
|
18
|
+
The cost is real and worth naming: a genuinely read-only api method added
|
|
19
|
+
later is refused in Plan mode until it is listed here. That sweep test fails
|
|
20
|
+
with the unclassified name printed, so the fix is one line and obvious.
|
|
21
|
+
|
|
22
|
+
The membership criterion, stated so future entries are decided rather than
|
|
23
|
+
argued: **a call is Plan-safe when it changes nothing that survives the plan
|
|
24
|
+
being rejected.** Session-scoped UI state dies with the session and is fine.
|
|
25
|
+
Anything durable, anything outside this process, and anything that acts on a
|
|
26
|
+
page, a process, another session or the human's screen is not.
|
|
27
|
+
|
|
28
|
+
Deliberately *not* "does it read secrets". Plan mode permits `read_file`, so
|
|
29
|
+
confinement is the sandbox's job — see `sandbox.guarded_os`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import functools
|
|
33
|
+
import inspect
|
|
34
|
+
from typing import Any, Callable
|
|
35
|
+
|
|
36
|
+
from . import bridge
|
|
37
|
+
|
|
38
|
+
#: Calls Plan mode runs without asking. Everything else is refused.
|
|
39
|
+
#:
|
|
40
|
+
#: Grouped by why, because every group was a judgment call:
|
|
41
|
+
PLAN_SAFE = frozenset({
|
|
42
|
+
# ── Reads. The reason this module exists. ──────────────────────────
|
|
43
|
+
"read_file", "glob", "grep", "ls", "man",
|
|
44
|
+
# Checking a harness file reads it and says what is wrong. A plan that
|
|
45
|
+
# proposes a harness is better for having been checked first.
|
|
46
|
+
"harness_check",
|
|
47
|
+
# Reading what an *already approved* background command is doing. The
|
|
48
|
+
# command was gated when it started; watching it is not a second act.
|
|
49
|
+
"bash_output", "bash_list",
|
|
50
|
+
|
|
51
|
+
# ── Session and tree reads ─────────────────────────────────────────
|
|
52
|
+
"children", "tasks", "session_join", "wait_for_task",
|
|
53
|
+
# Asking a conversation the person attached as a reference. It starts an
|
|
54
|
+
# agent, which nothing else here does — but that agent is handed the
|
|
55
|
+
# asker's own mode, so under a plan it can read and answer and no more,
|
|
56
|
+
# and what an earlier task decided is exactly what a plan should rest on.
|
|
57
|
+
"ask_reference",
|
|
58
|
+
# Saying where your *own* task has got to. Not a read, and admitted
|
|
59
|
+
# anyway: it writes one line of workspace state and nothing in the
|
|
60
|
+
# project, and a planning agent is exactly the one whose progress is
|
|
61
|
+
# worth seeing — it is reading code for minutes with nothing else to
|
|
62
|
+
# show for it. Refusing it would make Plan mode the one mode whose
|
|
63
|
+
# cards never say anything.
|
|
64
|
+
"task_status",
|
|
65
|
+
# A swarm member's progress report, on the same terms: one line into the
|
|
66
|
+
# swarm's stream and nothing in the project. `spawn_swarm` is not here —
|
|
67
|
+
# it starts agents that write, which is what planning is not.
|
|
68
|
+
"swarm_brief",
|
|
69
|
+
# The user's `/chess` game: a move on a board in wgx's memory, and a read
|
|
70
|
+
# of it. Nothing in the project, and the person started the game.
|
|
71
|
+
"chess_move", "chess_state",
|
|
72
|
+
# Clearing your own context. It writes nothing in the project, and a
|
|
73
|
+
# long planning session is as likely as any to need it.
|
|
74
|
+
"compact",
|
|
75
|
+
"xylon_list_trees", "xylon_get_tree", "xylon_get_node", "xylon_read_messages",
|
|
76
|
+
"xylon_search", "xylon_read_turns", "xylon_blame",
|
|
77
|
+
# A drive space's listing, and where a review stands. Both read; neither
|
|
78
|
+
# fetches a file or opens anything.
|
|
79
|
+
"drive_tree", "drive_review_status",
|
|
80
|
+
# The project Drive's documents and what differs in this machine's copy.
|
|
81
|
+
# It brings the copy up to date with what teammates landed, which writes
|
|
82
|
+
# under ~/.widgex and nothing in the project; suggesting is not here.
|
|
83
|
+
"project_drive",
|
|
84
|
+
# `widgets` and `profiles` are absent from PERMISSION_REQUIRED for the
|
|
85
|
+
# reason given there — listing what is installed opens nothing and
|
|
86
|
+
# reveals nothing. The same reasoning admits them here.
|
|
87
|
+
"widgets", "profiles",
|
|
88
|
+
# Reads of a browser someone already attached. Reaching a *new* site
|
|
89
|
+
# still needs `drive` / `use_widget` / `browser_open_tab`, all gated.
|
|
90
|
+
"browser_tabs", "browser_hybrid",
|
|
91
|
+
|
|
92
|
+
# ── Talking ────────────────────────────────────────────────────────
|
|
93
|
+
# Exactly the set `COMMUNICATION_CALLS` (src/providers/tools.ts) admits
|
|
94
|
+
# in Read-only, which is the stricter mode. Plan must not be stricter
|
|
95
|
+
# than Read-only about answering a question.
|
|
96
|
+
#
|
|
97
|
+
# Known escape hatch, accepted knowingly: `relay_send` can ask another
|
|
98
|
+
# agent to do the work. That is pre-existing and deliberate — it is the
|
|
99
|
+
# same hatch Read-only has — not something Plan mode opens.
|
|
100
|
+
"read_bulletin",
|
|
101
|
+
"relay_connect", "relay_accept", "relay_decline",
|
|
102
|
+
"relay_send", "relay_inbox", "relay_close",
|
|
103
|
+
# Asking the human, and asking another model for text. `ask_user` is the
|
|
104
|
+
# single most valuable call a planner has when the requirements are
|
|
105
|
+
# ambiguous; refusing it would make Plan mode worse at planning than any
|
|
106
|
+
# other mode is. `ask_model` is the sanctioned alternative to `agent`
|
|
107
|
+
# for a second opinion — no tools, no subagent, no session.
|
|
108
|
+
"ask_user", "ask_model",
|
|
109
|
+
|
|
110
|
+
# ── Session-scoped UI. Dies with the session. ──────────────────────
|
|
111
|
+
# `todo_write` is how the user watches the plan being built.
|
|
112
|
+
"todo_write", "session_scope",
|
|
113
|
+
# `phase` only reads: where the plan being written will land, and what came before.
|
|
114
|
+
"phase",
|
|
115
|
+
|
|
116
|
+
# ── Durable, and permitted anyway, by explicit decision ────────────
|
|
117
|
+
# Both write something that outlives the plan, so the criterion above
|
|
118
|
+
# argues against them. Allowed because the alternative is worse: an
|
|
119
|
+
# agent that hits a WidgeX defect while planning and cannot report it,
|
|
120
|
+
# or notices follow-up work and has to hold it in prose until the plan
|
|
121
|
+
# is approved.
|
|
122
|
+
"report_bug", "add_card",
|
|
123
|
+
|
|
124
|
+
# ── Argument-dependent. See StdlibAPI._plan_safe_agent. ────────────
|
|
125
|
+
"agent",
|
|
126
|
+
|
|
127
|
+
# ── Grafted browser verbs: the ones that only read the page ────────
|
|
128
|
+
"find", "search", "one", "outline", "snapshot", "digest",
|
|
129
|
+
"get_state", "here", "wait_for", "wait_stable",
|
|
130
|
+
|
|
131
|
+
# ── Grafted: withdrawn stubs that only raise a redirect ────────────
|
|
132
|
+
# `api.query()` and friends do nothing but raise "use api.search(...)".
|
|
133
|
+
# Refusing them would replace a genuinely helpful hint with a mode
|
|
134
|
+
# refusal, for a call that could not have done anything either way.
|
|
135
|
+
"query", "query_one", "hybrid_snapshot",
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
#: What the Support Sprite may do *beyond* reading: drive the IDE it is
|
|
140
|
+
#: attached to.
|
|
141
|
+
#:
|
|
142
|
+
#: Support mode is Plan mode's reads plus these. The sprite answers by pointing
|
|
143
|
+
#: at things in the user's own window — ringing a control, captioning a step,
|
|
144
|
+
#: clicking through a menu with them watching — so the page verbs are the job,
|
|
145
|
+
#: not an escape from it. What stays refused is everything that acts *off*
|
|
146
|
+
#: screen: files, shell, other sessions, other sites.
|
|
147
|
+
#:
|
|
148
|
+
#: Deliberately absent: `evaluate` (arbitrary script in the workbench, which can
|
|
149
|
+
#: reach far more than the screen shows), `navigate` (moving a webview away from
|
|
150
|
+
#: the IDE's own page breaks it), and every `request_*` handoff.
|
|
151
|
+
SUPPORT_DRIVE = frozenset({
|
|
152
|
+
"click", "double_click", "type_text", "clear_and_type", "hover",
|
|
153
|
+
"scroll_to", "select", "focus", "press", "dom_click", "dom_fill", "drain",
|
|
154
|
+
"screenshot",
|
|
155
|
+
# The guide verbs: the reason the sprite exists.
|
|
156
|
+
"highlight", "instruct", "reply", "await_user",
|
|
157
|
+
# Re-pointing at another part of its own window.
|
|
158
|
+
"attach_ide",
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
#: Everything a support session may call without being refused.
|
|
162
|
+
SUPPORT_SAFE = PLAN_SAFE | SUPPORT_DRIVE
|
|
163
|
+
|
|
164
|
+
#: The most a grant can ever add to support mode, whatever the exec meta says.
|
|
165
|
+
#: Mirrors SUPPORT_GRANT_CALLS in src/ide-widgets/supportSprite/supportGrant.ts.
|
|
166
|
+
#: Three of the four ask permission, and the host checks their arguments.
|
|
167
|
+
SUPPORT_GRANTABLE = frozenset({
|
|
168
|
+
"drive", "request_operator_action", "write_file", "edit_file",
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
#: The permission mode the Support Sprite runs in. See ClaudeSessionService.
|
|
172
|
+
SUPPORT_MODE = "support"
|
|
173
|
+
|
|
174
|
+
#: A read-only copy's mode: a conversation copied to answer one question about
|
|
175
|
+
#: it, which is how an @-mentioned agent answers in a channel. See
|
|
176
|
+
#: ClaudeSessionService.READONLY_MODE.
|
|
177
|
+
COPY_MODE = "readonly"
|
|
178
|
+
|
|
179
|
+
#: Everything a read-only copy may call. Narrower than Plan on purpose: Plan
|
|
180
|
+
#: is somebody planning their own work, and admits asking the user, starting
|
|
181
|
+
#: an agent, filing a card and writing todos. A copy answers somebody else's
|
|
182
|
+
#: question about somebody else's work; it reads, and that is all. The
|
|
183
|
+
#: criterion is stricter than Plan's too: **a call is copy-safe when it
|
|
184
|
+
#: changes nothing at all**, durable or not, here or anywhere.
|
|
185
|
+
#:
|
|
186
|
+
#: The page-reading verbs are here only because `guard` hands Plan-safe
|
|
187
|
+
#: grafted verbs back unwrapped, so a copy could not be refused them anyway;
|
|
188
|
+
#: listing them says so rather than leaving it implicit. They read a page
|
|
189
|
+
#: someone already attached, and a copy attaches nothing.
|
|
190
|
+
COPY_SAFE = frozenset({
|
|
191
|
+
"read_file", "glob", "grep", "ls", "man",
|
|
192
|
+
"tasks", "read_bulletin",
|
|
193
|
+
"xylon_list_trees", "xylon_get_tree", "xylon_get_node", "xylon_read_messages",
|
|
194
|
+
"xylon_search", "xylon_read_turns", "xylon_blame",
|
|
195
|
+
"drive_tree", "drive_review_status",
|
|
196
|
+
"harness_check",
|
|
197
|
+
"find", "search", "one", "outline", "snapshot", "digest",
|
|
198
|
+
"get_state", "here", "wait_for", "wait_stable",
|
|
199
|
+
"query", "query_one", "hybrid_snapshot",
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _message(name: str) -> str:
|
|
204
|
+
"""The refusal an agent reads.
|
|
205
|
+
|
|
206
|
+
Written against the two things a model does when a call is refused, both
|
|
207
|
+
of which cost turns and neither of which can work here:
|
|
208
|
+
|
|
209
|
+
1. It retries, on the theory that the user might answer differently.
|
|
210
|
+
2. It looks for another spelling — `click` refused, so `dom_click`, then
|
|
211
|
+
`evaluate("...click()")`, then `bash("osascript ...")`. That instinct
|
|
212
|
+
is real enough that `_browser_intent_hint` exists to catch it.
|
|
213
|
+
|
|
214
|
+
So the message says outright that it is the mode and not the user, that
|
|
215
|
+
no other call does this, and what *does* still work — a refusal that only
|
|
216
|
+
says no is a refusal the model argues with.
|
|
217
|
+
"""
|
|
218
|
+
return (
|
|
219
|
+
f"Plan mode does not permit api.{name}().\n"
|
|
220
|
+
"\n"
|
|
221
|
+
"It is the mode refusing, not the user — there is nothing to retry, "
|
|
222
|
+
"and no other call does this without the same refusal. What still "
|
|
223
|
+
"works: reads (read_file, glob, grep, ls), the page-reading verbs "
|
|
224
|
+
"(search, outline, one), the xylon tree, relay and the bulletin, "
|
|
225
|
+
"ask_user, ask_model, todo_write, man.\n"
|
|
226
|
+
"\n"
|
|
227
|
+
"Write the plan, then call ExitPlanMode. The user approves it, the "
|
|
228
|
+
"mode changes, and this call runs. If ExitPlanMode answers \"You are "
|
|
229
|
+
"not in plan mode\", WidgeX and the harness disagree about the mode: "
|
|
230
|
+
"call EnterPlanMode and then ExitPlanMode, so both see a fresh approval."
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _support_message(name: str) -> str:
|
|
235
|
+
"""The refusal the Support Sprite reads.
|
|
236
|
+
|
|
237
|
+
**Duplicated in ClaudeSessionService.SUPPORT_DENIAL**, which refuses the
|
|
238
|
+
same calls from the other side of the bridge.
|
|
239
|
+
"""
|
|
240
|
+
return (
|
|
241
|
+
f"Support mode does not permit api.{name}().\n"
|
|
242
|
+
"\n"
|
|
243
|
+
"You are read-only: you can read, look things up, and drive the IDE on "
|
|
244
|
+
"screen, but not edit files or run commands yourself. It is the mode "
|
|
245
|
+
"refusing, not the user — nothing will make this call succeed. Show the "
|
|
246
|
+
"user how to do it instead: point at the control with api.highlight or "
|
|
247
|
+
"api.instruct, and let them press it."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _copy_message(name: str) -> str:
|
|
252
|
+
"""The refusal a read-only copy reads.
|
|
253
|
+
|
|
254
|
+
**Duplicated in ClaudeSessionService.READONLY_DENIAL**, which refuses the
|
|
255
|
+
same calls from the other side of the bridge.
|
|
256
|
+
"""
|
|
257
|
+
return (
|
|
258
|
+
f"A read-only copy does not permit api.{name}().\n"
|
|
259
|
+
"\n"
|
|
260
|
+
"You are a copy of a conversation, made to answer a question about it. "
|
|
261
|
+
"You can read files, search them and read the project, and nothing "
|
|
262
|
+
"else. It is the mode refusing, not the user, and nothing will make "
|
|
263
|
+
"this call succeed. Answer from what you can read."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def refuse_if_planning(name: str, allowed: bool | None = None) -> None:
|
|
268
|
+
"""Raise when `name` is not something Plan mode permits. No-op otherwise.
|
|
269
|
+
|
|
270
|
+
`allowed` is the verdict of a `_plan_safe_<name>` sibling, for the calls
|
|
271
|
+
whose answer depends on their arguments. It **overrides** membership in
|
|
272
|
+
both directions, and it has to: `agent` is listed in `PLAN_SAFE` — the
|
|
273
|
+
coverage sweep reads that list to know the call has been classified at all
|
|
274
|
+
— while only one of its shapes is actually safe. Reading membership first
|
|
275
|
+
and returning would have made every `agent(wait=False)` plan-safe, which
|
|
276
|
+
is precisely the escape this module exists to close.
|
|
277
|
+
|
|
278
|
+
The hot path — a loop of `read_file`, which is what planning is — costs one
|
|
279
|
+
contextvar read and one frozenset lookup. The mode is read first because
|
|
280
|
+
support mode has its own list: it refuses `agent`, which Plan admits.
|
|
281
|
+
|
|
282
|
+
**A missing mode means unrestricted, deliberately.** Tests that spawn a
|
|
283
|
+
bare daemon and headless use send no `permissionMode`, and this mirrors
|
|
284
|
+
`_require_permission`'s own "no bridge means nobody to ask, so proceed".
|
|
285
|
+
The *allowlist* fails closed within Plan mode; the *mode* fails open when
|
|
286
|
+
unstated, and what makes that acceptable is that exactly one place stamps
|
|
287
|
+
it — `StdlibShell.exec` — rather than each of the four call sites that
|
|
288
|
+
assemble exec meta.
|
|
289
|
+
"""
|
|
290
|
+
meta = bridge.exec_meta.get()
|
|
291
|
+
mode = meta.get("permissionMode")
|
|
292
|
+
# First, because a copy is narrower than Plan in both directions: the
|
|
293
|
+
# Plan-safe verdict (`agent` with wait=True, say) does not admit anything
|
|
294
|
+
# here, and nothing outside COPY_SAFE runs whatever its arguments.
|
|
295
|
+
if mode == COPY_MODE:
|
|
296
|
+
if name in COPY_SAFE:
|
|
297
|
+
return
|
|
298
|
+
raise PermissionError(_copy_message(name))
|
|
299
|
+
if mode != SUPPORT_MODE and (allowed if allowed is not None else (name in PLAN_SAFE)):
|
|
300
|
+
return
|
|
301
|
+
if mode == SUPPORT_MODE:
|
|
302
|
+
# The sprite's own allowlist. `allowed` is a planning verdict and is
|
|
303
|
+
# not consulted: support mode is narrower than Plan about subagents.
|
|
304
|
+
if name in SUPPORT_SAFE and name != "agent":
|
|
305
|
+
return
|
|
306
|
+
# A Walter opened for one job (drive setup) may also make the calls
|
|
307
|
+
# that job needs. Names only, stamped by StdlibShell.exec; which url
|
|
308
|
+
# and which file are checked by the host when the call asks.
|
|
309
|
+
if name in SUPPORT_GRANTABLE and name in (meta.get("supportGrants") or ()):
|
|
310
|
+
return
|
|
311
|
+
raise PermissionError(_support_message(name))
|
|
312
|
+
if mode != "plan":
|
|
313
|
+
return
|
|
314
|
+
raise PermissionError(_message(name))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def guard(name: str, fn: Callable) -> Callable:
|
|
318
|
+
"""Wrap a grafted browser verb so Plan mode can refuse it.
|
|
319
|
+
|
|
320
|
+
`_graft_browser_l1` attaches these with `setattr` *after* the class body
|
|
321
|
+
has run, so `_gate_api_methods` never sees them and `_api_method` never
|
|
322
|
+
wraps them. This is the second of the two call sites enforcing PLAN_SAFE.
|
|
323
|
+
|
|
324
|
+
**The sync branch is load-bearing, not tidiness.** Ten of these verbs —
|
|
325
|
+
`click`, `type_text`, `press`, `hover`, `select`, … — are plain `def`.
|
|
326
|
+
They call `bridge.call(...)` eagerly and hand the coroutine to `_enqueue`,
|
|
327
|
+
which does `asyncio.ensure_future`, so *the request goes out on the next
|
|
328
|
+
tick whether or not the agent awaits it*. An `async def` wrapper on one of
|
|
329
|
+
those would return a coroutine nobody awaits: the guard would never run,
|
|
330
|
+
the agent would see no error, and the only trace would be a RuntimeWarning
|
|
331
|
+
on a stderr nobody reads. The refusal has to happen at the call site.
|
|
332
|
+
|
|
333
|
+
Allowlisted verbs are returned unwrapped rather than wrapped-and-allowed,
|
|
334
|
+
so `outline()` and `search()` are byte-identical to before this existed.
|
|
335
|
+
"""
|
|
336
|
+
if name in PLAN_SAFE:
|
|
337
|
+
return fn
|
|
338
|
+
|
|
339
|
+
if inspect.iscoroutinefunction(fn):
|
|
340
|
+
@functools.wraps(fn)
|
|
341
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
342
|
+
refuse_if_planning(name)
|
|
343
|
+
return await fn(*args, **kwargs)
|
|
344
|
+
else:
|
|
345
|
+
@functools.wraps(fn)
|
|
346
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
|
|
347
|
+
refuse_if_planning(name)
|
|
348
|
+
return fn(*args, **kwargs)
|
|
349
|
+
|
|
350
|
+
return wrapper
|