@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 @@
|
|
|
1
|
+
# xactor widget libraries
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
GitHub -- Personal Access Tokens widget (classic and fine-grained).
|
|
3
|
+
|
|
4
|
+
Covers GitHub token management:
|
|
5
|
+
GitHubLogin -> TokensPage -> CreateClassicTokenDialog -> TokenCreatedDialog
|
|
6
|
+
\-> CreateFineGrainedTokenDialog -> TokenCreatedDialog
|
|
7
|
+
|
|
8
|
+
Token flow (login -> generate -> extract):
|
|
9
|
+
0. Navigate to github.com/settings/tokens
|
|
10
|
+
1. If not authenticated, login page appears (do not attempt login)
|
|
11
|
+
2. TokensPage shows existing tokens and "Generate new token" dropdown
|
|
12
|
+
3. Select token type (Classic or Fine-grained)
|
|
13
|
+
4. Fill in token details (name, expiration, scopes/permissions)
|
|
14
|
+
5. Generate -> TokenCreatedDialog
|
|
15
|
+
6. Extract token value using extract_secret
|
|
16
|
+
7. Done -> TokensPage
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
# In an xactor shell:
|
|
20
|
+
await api.use_widget("github_tokens")
|
|
21
|
+
state = await registry.here(api)
|
|
22
|
+
print(state.describe())
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# These names are injected into scope by the xactor shell runtime:
|
|
26
|
+
# PageState, Observation, Transition, Step, registry
|
|
27
|
+
|
|
28
|
+
# ===============================================================
|
|
29
|
+
# State 0: GitHub Login (authentication gate)
|
|
30
|
+
# ===============================================================
|
|
31
|
+
github_login = PageState(
|
|
32
|
+
"GitHubLogin",
|
|
33
|
+
url_pattern=r"github\.com/login(?:\?|$)",
|
|
34
|
+
keywords=["Sign in to GitHub", "Username or email address", "Password"],
|
|
35
|
+
expected_timeout=120.0,
|
|
36
|
+
observations=[
|
|
37
|
+
Observation(
|
|
38
|
+
"login_form",
|
|
39
|
+
description="Login form presence and any error messages",
|
|
40
|
+
js="""(() => {
|
|
41
|
+
const form = document.querySelector('form');
|
|
42
|
+
const error = document.querySelector('[role=alert], .flash-error');
|
|
43
|
+
return {
|
|
44
|
+
form_present: !!form,
|
|
45
|
+
error_message: error?.textContent?.trim() || null,
|
|
46
|
+
fields: form ? Array.from(form.querySelectorAll('input[type=text], input[type=password], textbox')).map(
|
|
47
|
+
f => f.getAttribute('aria-label') || f.name || ''
|
|
48
|
+
).filter(Boolean) : [],
|
|
49
|
+
};
|
|
50
|
+
})()""",
|
|
51
|
+
),
|
|
52
|
+
],
|
|
53
|
+
transitions=[
|
|
54
|
+
Transition(
|
|
55
|
+
"wait_for_login",
|
|
56
|
+
description="Wait for the user to complete GitHub sign-in. "
|
|
57
|
+
"Blocks until the browser leaves the login page. "
|
|
58
|
+
"Do NOT attempt to fill credentials -- let the user sign in manually.",
|
|
59
|
+
steps=[
|
|
60
|
+
Step("wait_for", 'main/heading"Personal access tokens"', timeout_ms=120000),
|
|
61
|
+
],
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# ===============================================================
|
|
67
|
+
# State 1: Tokens Page (/settings/tokens)
|
|
68
|
+
# ===============================================================
|
|
69
|
+
tokens_page = PageState(
|
|
70
|
+
"TokensPage",
|
|
71
|
+
url_pattern=r"github\.com/settings/tokens",
|
|
72
|
+
keywords=["Personal access tokens", "Generate new token", "Classic", "Fine-grained"],
|
|
73
|
+
expected_timeout=5.0,
|
|
74
|
+
observations=[
|
|
75
|
+
Observation(
|
|
76
|
+
"existing_tokens",
|
|
77
|
+
description="List of existing personal access tokens with name, last used, and expiration",
|
|
78
|
+
js="""(() => {
|
|
79
|
+
const rows = Array.from(document.querySelectorAll('table tbody tr, [role=row]'));
|
|
80
|
+
return rows.map(row => {
|
|
81
|
+
const cells = Array.from(row.querySelectorAll('td, [role=gridcell]'));
|
|
82
|
+
return {
|
|
83
|
+
name: cells[0]?.textContent?.trim(),
|
|
84
|
+
last_used: cells[1]?.textContent?.trim(),
|
|
85
|
+
expiration: cells[2]?.textContent?.trim(),
|
|
86
|
+
scopes_or_permissions: cells[3]?.textContent?.trim(),
|
|
87
|
+
};
|
|
88
|
+
}).filter(r => r.name && r.name.length > 0);
|
|
89
|
+
})()""",
|
|
90
|
+
),
|
|
91
|
+
Observation(
|
|
92
|
+
"page_structure",
|
|
93
|
+
description="Headings and interactive elements on the tokens page",
|
|
94
|
+
js="""(() => {
|
|
95
|
+
const headings = Array.from(document.querySelectorAll('h1,h2,h3,[role=heading]')).map(
|
|
96
|
+
h => h.textContent?.trim()
|
|
97
|
+
).filter(Boolean);
|
|
98
|
+
const buttons = Array.from(document.querySelectorAll('button, [role=button]')).map(
|
|
99
|
+
b => b.textContent.trim()
|
|
100
|
+
).filter(t => t.length > 0 && t.length < 50);
|
|
101
|
+
return { headings, buttons };
|
|
102
|
+
})()""",
|
|
103
|
+
),
|
|
104
|
+
],
|
|
105
|
+
transitions=[
|
|
106
|
+
Transition(
|
|
107
|
+
"generate_classic_token",
|
|
108
|
+
description="Open the dialog to generate a classic personal access token.",
|
|
109
|
+
steps=[
|
|
110
|
+
Step("click", 'button"Generate new token"'),
|
|
111
|
+
Step("click", "Classic"),
|
|
112
|
+
],
|
|
113
|
+
),
|
|
114
|
+
Transition(
|
|
115
|
+
"generate_fine_grained_token",
|
|
116
|
+
description="Open the dialog to generate a fine-grained personal access token.",
|
|
117
|
+
steps=[
|
|
118
|
+
Step("click", 'button"Generate new token"'),
|
|
119
|
+
Step("click", "Fine-grained"),
|
|
120
|
+
],
|
|
121
|
+
),
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# ===============================================================
|
|
126
|
+
# State 2: Create Classic Token Dialog
|
|
127
|
+
# ===============================================================
|
|
128
|
+
create_classic_token_dialog = PageState(
|
|
129
|
+
"CreateClassicTokenDialog",
|
|
130
|
+
url_pattern=r"github\.com/settings/tokens/new",
|
|
131
|
+
keywords=["New personal access token", "Note", "Expiration", "Select scopes", "Generate token"],
|
|
132
|
+
expected_timeout=5.0,
|
|
133
|
+
observations=[
|
|
134
|
+
Observation(
|
|
135
|
+
"form_state",
|
|
136
|
+
description="Current state of the classic token creation form",
|
|
137
|
+
js="""(() => {
|
|
138
|
+
const noteInput = document.querySelector('input[placeholder*="Token description i"], input[aria-label*="Description i"]');
|
|
139
|
+
const expirationSelect = document.querySelector('select[aria-label*="Expiration"], [role=combobox]');
|
|
140
|
+
const scopeCheckboxes = Array.from(document.querySelectorAll('input[type=checkbox]')).map(
|
|
141
|
+
cb => ({
|
|
142
|
+
label: cb.labels?.[0]?.textContent?.trim() || cb.getAttribute('aria-label') || '',
|
|
143
|
+
checked: cb.checked
|
|
144
|
+
})
|
|
145
|
+
).filter(s => s.label);
|
|
146
|
+
return {
|
|
147
|
+
note_value: noteInput?.value || '',
|
|
148
|
+
note_placeholder: noteInput?.placeholder || '',
|
|
149
|
+
expiration_value: expirationSelect?.value || '',
|
|
150
|
+
scopes_selected: scopeCheckboxes.filter(s => s.checked).map(s => s.label),
|
|
151
|
+
scopes_available: scopeCheckboxes.map(s => s.label),
|
|
152
|
+
};
|
|
153
|
+
})()""",
|
|
154
|
+
),
|
|
155
|
+
],
|
|
156
|
+
transitions=[
|
|
157
|
+
Transition(
|
|
158
|
+
"set_token_name",
|
|
159
|
+
description="Type a description/name for the token.",
|
|
160
|
+
params={"name": "str"},
|
|
161
|
+
steps=[Step("clear_and_type", 'textbox"Token description"', text="$name")],
|
|
162
|
+
),
|
|
163
|
+
Transition(
|
|
164
|
+
"set_expiration",
|
|
165
|
+
description="Set token expiration. Options: 7 days, 30 days, 60 days, 90 days, 1 year, or No expiration.",
|
|
166
|
+
params={"days": "str"},
|
|
167
|
+
steps=[Step("select", 'select[aria-label*="Expiration"]', label="$days")],
|
|
168
|
+
),
|
|
169
|
+
Transition(
|
|
170
|
+
"toggle_scope",
|
|
171
|
+
description="Toggle a scope checkbox by its label (e.g., 'repo', 'gist', 'read:user').",
|
|
172
|
+
params={"scope_name": "str"},
|
|
173
|
+
steps=[Step("click", 'input[aria-label*="$scope_name"], label:has-text("$scope_name")')],
|
|
174
|
+
),
|
|
175
|
+
Transition(
|
|
176
|
+
"generate_token",
|
|
177
|
+
description="Click 'Generate token' to create the token. "
|
|
178
|
+
"Waits for the token value display to appear.",
|
|
179
|
+
steps=[
|
|
180
|
+
Step("scroll_to", 'button"Generate token"'),
|
|
181
|
+
Step("click", 'button"Generate token"'),
|
|
182
|
+
Step("wait_for", 'pre, code, input[readonly]', timeout_ms=10000),
|
|
183
|
+
],
|
|
184
|
+
),
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# ===============================================================
|
|
189
|
+
# State 3: Create Fine-Grained Token Dialog
|
|
190
|
+
# ===============================================================
|
|
191
|
+
create_fine_grained_token_dialog = PageState(
|
|
192
|
+
"CreateFineGrainedTokenDialog",
|
|
193
|
+
url_pattern=r"github\.com/settings/tokens/new\?type=beta",
|
|
194
|
+
keywords=["New fine-grained personal access token", "Token name", "Expiration",
|
|
195
|
+
"Repository access", "Permissions", "Generate token"],
|
|
196
|
+
expected_timeout=5.0,
|
|
197
|
+
observations=[
|
|
198
|
+
Observation(
|
|
199
|
+
"form_state",
|
|
200
|
+
description="Current state of the fine-grained token creation form",
|
|
201
|
+
js="""(() => {
|
|
202
|
+
const nameInput = document.querySelector('input[aria-label*="Token name"]');
|
|
203
|
+
const expirationSelect = document.querySelector('select[aria-label*="Expiration"]');
|
|
204
|
+
const repoAccess = document.querySelector('fieldset:has(legend:contains("Repository access"))');
|
|
205
|
+
return {
|
|
206
|
+
token_name: nameInput?.value || '',
|
|
207
|
+
expiration: expirationSelect?.value || '',
|
|
208
|
+
has_repo_access_section: !!repoAccess,
|
|
209
|
+
};
|
|
210
|
+
})()""",
|
|
211
|
+
),
|
|
212
|
+
],
|
|
213
|
+
transitions=[
|
|
214
|
+
Transition(
|
|
215
|
+
"set_token_name",
|
|
216
|
+
description="Type a name for the fine-grained token.",
|
|
217
|
+
params={"name": "str"},
|
|
218
|
+
steps=[Step("clear_and_type", 'textbox"Token name"', text="$name")],
|
|
219
|
+
),
|
|
220
|
+
Transition(
|
|
221
|
+
"set_expiration",
|
|
222
|
+
description="Set token expiration. Options: 7 days, 30 days, 60 days, 90 days, 1 year, or No expiration.",
|
|
223
|
+
params={"days": "str"},
|
|
224
|
+
steps=[Step("select", 'select[aria-label*="Expiration"]', label="$days")],
|
|
225
|
+
),
|
|
226
|
+
Transition(
|
|
227
|
+
"generate_token",
|
|
228
|
+
description="Click 'Generate token' to create the fine-grained token. "
|
|
229
|
+
"Waits for the token value display to appear.",
|
|
230
|
+
steps=[
|
|
231
|
+
Step("scroll_to", 'button"Generate token"'),
|
|
232
|
+
Step("click", 'button"Generate token"'),
|
|
233
|
+
Step("wait_for", 'pre, code, input[readonly]', timeout_ms=10000),
|
|
234
|
+
],
|
|
235
|
+
),
|
|
236
|
+
],
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# ===============================================================
|
|
240
|
+
# State 4: Token Created (success view with token value)
|
|
241
|
+
# ===============================================================
|
|
242
|
+
token_created_dialog = PageState(
|
|
243
|
+
"TokenCreatedDialog",
|
|
244
|
+
url_pattern=r"github\.com/settings/tokens/\w+",
|
|
245
|
+
keywords=["Personal access token", "Make sure to copy", "won't be shown again",
|
|
246
|
+
"Copy", "Close", "Remember to store securely"],
|
|
247
|
+
expected_timeout=5.0,
|
|
248
|
+
observations=[
|
|
249
|
+
Observation(
|
|
250
|
+
"token_info",
|
|
251
|
+
description="Token creation success state. Shows token value display area "
|
|
252
|
+
"(use extract_token to securely save it).",
|
|
253
|
+
js="""(() => {
|
|
254
|
+
const heading = Array.from(document.querySelectorAll('h1,h2,h3,[role=heading]')).find(
|
|
255
|
+
h => h.textContent?.includes('Personal access token') || h.textContent?.includes('Token')
|
|
256
|
+
);
|
|
257
|
+
if (!heading) return {dialog_open: false};
|
|
258
|
+
let container = heading;
|
|
259
|
+
for (let i = 0; i < 8; i++) { if (container.parentElement) container = container.parentElement; }
|
|
260
|
+
const pre = container.querySelector('pre');
|
|
261
|
+
const codeEl = container.querySelector('code');
|
|
262
|
+
const readonlyInput = container.querySelector('input[readonly]');
|
|
263
|
+
const copyButton = container.querySelector('button[aria-label*="Copy"], button:has-text("Copy")');
|
|
264
|
+
return {
|
|
265
|
+
token_visible: !!(pre?.textContent || codeEl?.textContent || readonlyInput?.value),
|
|
266
|
+
has_copy_button: !!copyButton,
|
|
267
|
+
selector_hint: pre ? 'pre'
|
|
268
|
+
: codeEl ? 'code'
|
|
269
|
+
: readonlyInput ? 'input[readonly]'
|
|
270
|
+
: 'unknown',
|
|
271
|
+
};
|
|
272
|
+
})()""",
|
|
273
|
+
),
|
|
274
|
+
],
|
|
275
|
+
transitions=[
|
|
276
|
+
Transition(
|
|
277
|
+
"extract_token",
|
|
278
|
+
description="Securely extract the token value and save it to .env using extract_secret. "
|
|
279
|
+
"The token value never passes through the agent. "
|
|
280
|
+
"Pass the desired env variable name (e.g., 'GITHUB_TOKEN').",
|
|
281
|
+
params={"env_var": "str"},
|
|
282
|
+
steps=[Step("extract_secret", 'pre, code, input[readonly]', env_var="$env_var")],
|
|
283
|
+
),
|
|
284
|
+
Transition(
|
|
285
|
+
"close",
|
|
286
|
+
description="Close the token view and return to the tokens list.",
|
|
287
|
+
steps=[Step("navigate", "https://github.com/settings/tokens")],
|
|
288
|
+
),
|
|
289
|
+
],
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# ===============================================================
|
|
293
|
+
# Register all states with adjacency graph
|
|
294
|
+
# ===============================================================
|
|
295
|
+
registry.add(github_login, transitions_to=[
|
|
296
|
+
"TokensPage",
|
|
297
|
+
])
|
|
298
|
+
|
|
299
|
+
registry.add(tokens_page, transitions_to=[
|
|
300
|
+
"CreateClassicTokenDialog", "CreateFineGrainedTokenDialog",
|
|
301
|
+
])
|
|
302
|
+
|
|
303
|
+
registry.add(create_classic_token_dialog, transitions_to=[
|
|
304
|
+
"TokenCreatedDialog", "TokensPage",
|
|
305
|
+
])
|
|
306
|
+
|
|
307
|
+
registry.add(create_fine_grained_token_dialog, transitions_to=[
|
|
308
|
+
"TokenCreatedDialog", "TokensPage",
|
|
309
|
+
])
|
|
310
|
+
|
|
311
|
+
registry.add(token_created_dialog, transitions_to=[
|
|
312
|
+
"TokensPage",
|
|
313
|
+
])
|
|
314
|
+
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Google AI Studio — key creation for the providers panel.
|
|
3
|
+
|
|
4
|
+
AIStudioServiceBlocked ─▶ AIStudioAccountChooser ─┐
|
|
5
|
+
AIStudioSignIn ───────────────────────────────────┴─▶ AIStudioKeys
|
|
6
|
+
│
|
|
7
|
+
AIStudioKeyDetails ◀── AIStudioCreateKey
|
|
8
|
+
|
|
9
|
+
`AIStudio` and not `Google`, because `gcp_oauth.py` already owns a `GoogleSignIn`
|
|
10
|
+
covering the same `accounts.google.com` — and `StateRegistry` is keyed by name,
|
|
11
|
+
so on the agent path, where every widget loads into one shell, the second
|
|
12
|
+
registration would silently replace the first. The panel never sees this (it
|
|
13
|
+
loads one widget), which is exactly what would have made it hard to find.
|
|
14
|
+
|
|
15
|
+
Angular Material. `mat-dialog-container`, `mat-select`, `mat-form-field` are
|
|
16
|
+
documented tag selectors and are used throughout. Ids are **not**: the name
|
|
17
|
+
input's id is a per-render GUID (`input-260574C1-…`), the exact inverse of xAI.
|
|
18
|
+
`aria-label` is stable here and this site labels almost everything, including
|
|
19
|
+
each individual copy button — which matters because icon buttons render their
|
|
20
|
+
ligature name as text (`content_copy`, `close`, `delete`), so matching buttons
|
|
21
|
+
on `innerText` is noisy in a way it is not on the other three consoles.
|
|
22
|
+
|
|
23
|
+
Written from `.widgex/plans/recon/google.md`.
|
|
24
|
+
|
|
25
|
+
## Three ways in, and only one of them is a sign-in
|
|
26
|
+
|
|
27
|
+
**A Workspace account can be refused outright.** With a managed account active,
|
|
28
|
+
`/apikey` redirects to `access.workspace.google.com/ServiceNotAllowed` — *"Your
|
|
29
|
+
account is managed by an organization that has this service turned off."* That
|
|
30
|
+
is terminal, not a login prompt: no credential fixes it, only a different
|
|
31
|
+
account. `AIStudioServiceBlocked` exists to tell those two apart, because the
|
|
32
|
+
remedies have nothing in common.
|
|
33
|
+
|
|
34
|
+
**The account chooser needs no credentials.** Picking an already-authenticated
|
|
35
|
+
session from `accounts.google.com/AccountChooser` is a session switch that
|
|
36
|
+
completes straight through to the destination, so it is automatable. Entering a
|
|
37
|
+
password is not, and is not modelled — `AIStudioSignIn` waits.
|
|
38
|
+
|
|
39
|
+
**The URL you land on shares no path with the one you asked for.** `/apikey`
|
|
40
|
+
becomes `/u/<n>/api-keys?pli=1&project=…`. Any pattern has to tolerate the
|
|
41
|
+
account-index segment, so these match on host plus a trailing key path.
|
|
42
|
+
|
|
43
|
+
## Two consequences of a pre-filled form
|
|
44
|
+
|
|
45
|
+
Both fields arrive filled — the name defaults to `Gemini API Key` and the
|
|
46
|
+
project to the first imported one — and `Create key` is enabled immediately.
|
|
47
|
+
That is why the recon account holds `Gemini API Key`, `Gemini API Key 2`, `3`
|
|
48
|
+
and `4`.
|
|
49
|
+
|
|
50
|
+
So `set_name` must *overwrite* rather than type into an assumed-empty field
|
|
51
|
+
(`dom_fill` replaces, which is why it is the verb), and because a bare commit
|
|
52
|
+
silently succeeds under the generic name there is no natural error to catch if
|
|
53
|
+
naming fails — `form_state` exists to be checked, not merely to be available.
|
|
54
|
+
|
|
55
|
+
The project select is deliberately not driven. It arrives pointed at a real
|
|
56
|
+
imported project and the panel has nothing better to say about which one; a
|
|
57
|
+
control the panel never touches does not get a transition.
|
|
58
|
+
|
|
59
|
+
## The reveal is text, and Google is the only one that forgives it
|
|
60
|
+
|
|
61
|
+
`API key details` contains no input element at all — the key is text following
|
|
62
|
+
the `API Key` label, so it lands in any `innerText` read. `key_location` marks
|
|
63
|
+
the one node so `capture_secret` can take that node alone, the same as xAI.
|
|
64
|
+
|
|
65
|
+
Unlike the other three, though, the key stays retrievable: clicking a row's
|
|
66
|
+
masked value reopens this dialog with the full secret. A failed capture here is
|
|
67
|
+
recoverable, so the "do not dismiss until the host confirms capture" rule the
|
|
68
|
+
others need does not bind. It is still followed, because one code path is
|
|
69
|
+
better than two.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
# Injected into scope by the xactor shell runtime:
|
|
73
|
+
# PageState, Observation, Transition, Step, registry
|
|
74
|
+
|
|
75
|
+
# `/apikey` as requested, `/api-keys` as landed on, `/u/<n>/` in between.
|
|
76
|
+
GOOGLE_KEYS_URL = r"aistudio\.google\.com/(?:u/\d+/)?api-?keys?"
|
|
77
|
+
|
|
78
|
+
# ===============================================================
|
|
79
|
+
# Wall 1 — the account is not allowed to use AI Studio at all
|
|
80
|
+
# ===============================================================
|
|
81
|
+
ai_studio_service_blocked = PageState(
|
|
82
|
+
"AIStudioServiceBlocked",
|
|
83
|
+
url_pattern=r"access\.workspace\.google\.com",
|
|
84
|
+
keywords=["Service Not Allowed", "do not have access", "managed by an organization"],
|
|
85
|
+
expected_timeout=10.0,
|
|
86
|
+
observations=[
|
|
87
|
+
Observation(
|
|
88
|
+
"block_reason",
|
|
89
|
+
description="That AI Studio is disabled for the signed-in Workspace account, "
|
|
90
|
+
"and whether an account switch is offered. No credential will fix "
|
|
91
|
+
"this; a different account is the only route.",
|
|
92
|
+
js="""(() => {
|
|
93
|
+
const chooser = document.querySelector('a[href*="AccountChooser"]');
|
|
94
|
+
return {
|
|
95
|
+
blocked: true,
|
|
96
|
+
url: location.href,
|
|
97
|
+
message: document.body.innerText.trim().slice(0, 300),
|
|
98
|
+
can_switch_account: !!chooser,
|
|
99
|
+
};
|
|
100
|
+
})()""",
|
|
101
|
+
),
|
|
102
|
+
],
|
|
103
|
+
transitions=[
|
|
104
|
+
Transition(
|
|
105
|
+
"switch_account",
|
|
106
|
+
description="Follow the page's own 'Sign in with a different account' link to "
|
|
107
|
+
"the account chooser.",
|
|
108
|
+
steps=[
|
|
109
|
+
Step("dom_click", 'a[href*="AccountChooser"]'),
|
|
110
|
+
Step("wait_for_element", "[data-identifier]", timeout_ms=20000),
|
|
111
|
+
],
|
|
112
|
+
),
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# ===============================================================
|
|
117
|
+
# Wall 2 — pick among sessions that are already signed in
|
|
118
|
+
# ===============================================================
|
|
119
|
+
#
|
|
120
|
+
# Automatable, and the only Google wall that is: choosing a listed identity is a
|
|
121
|
+
# session switch, not an authentication. `Use another account` leads to a
|
|
122
|
+
# credential form and is not modelled.
|
|
123
|
+
ai_studio_account_chooser = PageState(
|
|
124
|
+
"AIStudioAccountChooser",
|
|
125
|
+
url_pattern=r"accounts\.google\.com/AccountChooser",
|
|
126
|
+
keywords=["Choose an account", "Use another account", "Signed out"],
|
|
127
|
+
expected_timeout=15.0,
|
|
128
|
+
observations=[
|
|
129
|
+
Observation(
|
|
130
|
+
"accounts",
|
|
131
|
+
description="Email addresses of sessions already authenticated in this browser. "
|
|
132
|
+
"Picking one needs no credentials.",
|
|
133
|
+
js="""Array.from(document.querySelectorAll('[data-identifier]')).map(
|
|
134
|
+
el => el.getAttribute('data-identifier')
|
|
135
|
+
).filter(Boolean)""",
|
|
136
|
+
),
|
|
137
|
+
],
|
|
138
|
+
transitions=[
|
|
139
|
+
Transition(
|
|
140
|
+
"choose_account",
|
|
141
|
+
description="Continue as one of the already-authenticated accounts. Pass an "
|
|
142
|
+
"address returned by the 'accounts' observation.",
|
|
143
|
+
params={"email": "str"},
|
|
144
|
+
steps=[
|
|
145
|
+
# The address goes into a selector, not into the page — this is
|
|
146
|
+
# a click, and the value came off this same page a moment ago.
|
|
147
|
+
# A miss resolves to nothing and `dom_click` says so.
|
|
148
|
+
Step("dom_click", '[data-identifier="$email"]'),
|
|
149
|
+
],
|
|
150
|
+
),
|
|
151
|
+
],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# ===============================================================
|
|
155
|
+
# Wall 3 — an ordinary sign-in
|
|
156
|
+
# ===============================================================
|
|
157
|
+
ai_studio_sign_in = PageState(
|
|
158
|
+
"AIStudioSignIn",
|
|
159
|
+
url_pattern=r"accounts\.google\.com/(?:v\d+/)?(?:signin|ServiceLogin)",
|
|
160
|
+
keywords=["Sign in", "to continue to", "Forgot email", "Next"],
|
|
161
|
+
expected_timeout=120.0,
|
|
162
|
+
observations=[
|
|
163
|
+
Observation(
|
|
164
|
+
"sign_in_state",
|
|
165
|
+
description="Whether a sign-in page is showing. Never reads or reports "
|
|
166
|
+
"credentials.",
|
|
167
|
+
js="""(() => ({ signed_out: true, url: location.href }))()""",
|
|
168
|
+
),
|
|
169
|
+
],
|
|
170
|
+
transitions=[
|
|
171
|
+
Transition(
|
|
172
|
+
"wait_for_sign_in",
|
|
173
|
+
description="Block until the user has signed in and the key list has rendered. "
|
|
174
|
+
"Do not fill credentials — reveal the browser and let the user do it.",
|
|
175
|
+
steps=[
|
|
176
|
+
Step("wait_for_element", '[role=row]', timeout_ms=120000, interval_ms=2000),
|
|
177
|
+
],
|
|
178
|
+
),
|
|
179
|
+
],
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# ===============================================================
|
|
183
|
+
# /u/<n>/api-keys
|
|
184
|
+
# ===============================================================
|
|
185
|
+
ai_studio_keys = PageState(
|
|
186
|
+
"AIStudioKeys",
|
|
187
|
+
url_pattern=GOOGLE_KEYS_URL,
|
|
188
|
+
entry="https://aistudio.google.com/apikey",
|
|
189
|
+
keywords=["Billing Tier", "Import projects"],
|
|
190
|
+
expected_timeout=10.0,
|
|
191
|
+
observations=[
|
|
192
|
+
Observation(
|
|
193
|
+
"existing_keys",
|
|
194
|
+
description="Existing keys: masked value, project, created date, billing tier. "
|
|
195
|
+
"The table shows a suffix mask only.",
|
|
196
|
+
js="""(() => {
|
|
197
|
+
const rows = Array.from(document.querySelectorAll('[role=row]'));
|
|
198
|
+
return rows.map(row => {
|
|
199
|
+
const c = Array.from(row.querySelectorAll('[role=cell], [role=gridcell]')).map(
|
|
200
|
+
cell => cell.textContent?.trim() || ''
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
masked: c[0] || '',
|
|
204
|
+
project: c[1] || '',
|
|
205
|
+
created: c[2] || '',
|
|
206
|
+
billing_tier: c[3] || '',
|
|
207
|
+
};
|
|
208
|
+
}).filter(r => r.masked && r.project);
|
|
209
|
+
})()""",
|
|
210
|
+
),
|
|
211
|
+
Observation(
|
|
212
|
+
"page_ready",
|
|
213
|
+
description="Whether the key list has rendered and the create button is "
|
|
214
|
+
"available. The opener flips to disabled while a dialog is open, "
|
|
215
|
+
"which is a cheap check that one is.",
|
|
216
|
+
js="""(() => {
|
|
217
|
+
const opener = Array.from(document.querySelectorAll('button')).find(
|
|
218
|
+
b => /create api key/i.test(b.innerText || '')
|
|
219
|
+
);
|
|
220
|
+
return {
|
|
221
|
+
has_rows: !!document.querySelector('[role=row]'),
|
|
222
|
+
opener: !!opener,
|
|
223
|
+
dialog_open: !!opener && opener.disabled,
|
|
224
|
+
};
|
|
225
|
+
})()""",
|
|
226
|
+
),
|
|
227
|
+
],
|
|
228
|
+
transitions=[
|
|
229
|
+
Transition(
|
|
230
|
+
"open_create_dialog",
|
|
231
|
+
description="Open the 'Create a new key' dialog.",
|
|
232
|
+
steps=[
|
|
233
|
+
# The button's own text is `key Create API key` — the leading
|
|
234
|
+
# word is the icon ligature. Text resolution is substring-based,
|
|
235
|
+
# so the label alone finds it.
|
|
236
|
+
Step("dom_click", "Create API key"),
|
|
237
|
+
Step("wait_for_element", "mat-dialog-container", timeout_ms=15000),
|
|
238
|
+
],
|
|
239
|
+
),
|
|
240
|
+
],
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
ai_studio_create_key = PageState(
|
|
244
|
+
"AIStudioCreateKey",
|
|
245
|
+
url_pattern=GOOGLE_KEYS_URL,
|
|
246
|
+
# `Create a new key` is the dialog heading; the opener behind it says
|
|
247
|
+
# `Create API key`, and neither is a substring of the other.
|
|
248
|
+
keywords=["Create a new key", "Choose an imported project", "Cancel"],
|
|
249
|
+
expected_timeout=8.0,
|
|
250
|
+
observations=[
|
|
251
|
+
Observation(
|
|
252
|
+
"form_state",
|
|
253
|
+
description="The create dialog's current name and project. Both arrive "
|
|
254
|
+
"pre-filled, so this is what a caller checks after set_name to "
|
|
255
|
+
"confirm the name actually landed — committing under the default "
|
|
256
|
+
"name succeeds silently and raises nothing.",
|
|
257
|
+
js="""(() => {
|
|
258
|
+
const dlg = document.querySelector('mat-dialog-container');
|
|
259
|
+
if (!dlg) return { dialog_open: false };
|
|
260
|
+
const name = dlg.querySelector('input[aria-label="Name your key"]');
|
|
261
|
+
const project = dlg.querySelector('mat-select[aria-label="Project"]');
|
|
262
|
+
const commit = dlg.querySelector('button.ms-button-primary');
|
|
263
|
+
return {
|
|
264
|
+
dialog_open: true,
|
|
265
|
+
name: name?.value || '',
|
|
266
|
+
project: project?.textContent?.trim() || '',
|
|
267
|
+
can_commit: !!commit && !commit.disabled,
|
|
268
|
+
};
|
|
269
|
+
})()""",
|
|
270
|
+
),
|
|
271
|
+
],
|
|
272
|
+
transitions=[
|
|
273
|
+
Transition(
|
|
274
|
+
"set_name",
|
|
275
|
+
description="Replace the pre-filled key name. Check form_state afterwards — "
|
|
276
|
+
"a failure here produces another 'Gemini API Key', not an error.",
|
|
277
|
+
params={"name": "str"},
|
|
278
|
+
steps=[
|
|
279
|
+
Step("dom_fill", 'mat-dialog-container input[aria-label="Name your key"]',
|
|
280
|
+
text="$name"),
|
|
281
|
+
],
|
|
282
|
+
),
|
|
283
|
+
Transition(
|
|
284
|
+
"commit",
|
|
285
|
+
description="Create the key, landing on the API key details dialog.",
|
|
286
|
+
steps=[
|
|
287
|
+
Step("dom_click", "mat-dialog-container button.ms-button-primary"),
|
|
288
|
+
# A labelled control that exists only on the details dialog, and
|
|
289
|
+
# so is a real arrival marker rather than a guess. Not scoped to
|
|
290
|
+
# `mat-dialog-container`, because at this moment the details
|
|
291
|
+
# dialog is the only one open.
|
|
292
|
+
Step("wait_for_element", 'button[aria-label="Copy key to clipboard"]',
|
|
293
|
+
timeout_ms=20000),
|
|
294
|
+
],
|
|
295
|
+
),
|
|
296
|
+
Transition(
|
|
297
|
+
"cancel",
|
|
298
|
+
description="Abandon key creation and return to the list.",
|
|
299
|
+
steps=[Step("dom_click", "Cancel", container="mat-dialog-container")],
|
|
300
|
+
),
|
|
301
|
+
],
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
ai_studio_key_details = PageState(
|
|
305
|
+
"AIStudioKeyDetails",
|
|
306
|
+
url_pattern=GOOGLE_KEYS_URL,
|
|
307
|
+
keywords=["API key details", "Project number", "Copy cURL quickstart", "Delete key"],
|
|
308
|
+
expected_timeout=8.0,
|
|
309
|
+
observations=[
|
|
310
|
+
Observation(
|
|
311
|
+
"key_location",
|
|
312
|
+
description="Where the secret is, as a selector — never the secret itself. "
|
|
313
|
+
"The host reads it with capture_secret, which checks it against "
|
|
314
|
+
"Google's key shape and returns a handle.",
|
|
315
|
+
# Marked before it is named, as on xAI and for the same reason: the
|
|
316
|
+
# key is bare text in a labelled field list, so finding it means
|
|
317
|
+
# matching on content and handing it over means leaving behind
|
|
318
|
+
# something a selector can address.
|
|
319
|
+
#
|
|
320
|
+
# `dialogs[last]` rather than `querySelector`, because Google stacks
|
|
321
|
+
# dialogs — the delete confirmation opens *over* this one, and the
|
|
322
|
+
# first match is then the wrong one.
|
|
323
|
+
js="""(() => {
|
|
324
|
+
const dialogs = document.querySelectorAll('mat-dialog-container');
|
|
325
|
+
const root = dialogs[dialogs.length - 1];
|
|
326
|
+
const miss = { ready: false, selector: null, container: null, index: 0 };
|
|
327
|
+
if (!root) return miss;
|
|
328
|
+
const el = Array.from(root.querySelectorAll('*')).find(n =>
|
|
329
|
+
n.children.length === 0 &&
|
|
330
|
+
/^(?:AIza[A-Za-z0-9_-]{35}|AQ\\.[A-Za-z0-9_-]{30,})$/.test(
|
|
331
|
+
(n.textContent || '').trim()
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
if (!el) return miss;
|
|
335
|
+
el.setAttribute('data-widgex-key', '1');
|
|
336
|
+
return {
|
|
337
|
+
ready: true,
|
|
338
|
+
selector: '[data-widgex-key="1"]',
|
|
339
|
+
container: null,
|
|
340
|
+
index: 0,
|
|
341
|
+
};
|
|
342
|
+
})()""",
|
|
343
|
+
),
|
|
344
|
+
],
|
|
345
|
+
transitions=[
|
|
346
|
+
Transition(
|
|
347
|
+
"dismiss",
|
|
348
|
+
description="Close the details dialog and return to the key list.",
|
|
349
|
+
steps=[Step("dom_click", 'button[aria-label="Close dialog"]')],
|
|
350
|
+
),
|
|
351
|
+
],
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# ===============================================================
|
|
355
|
+
# Registration
|
|
356
|
+
# ===============================================================
|
|
357
|
+
registry.add(ai_studio_service_blocked, transitions_to=["AIStudioAccountChooser"])
|
|
358
|
+
registry.add(ai_studio_account_chooser, transitions_to=["AIStudioKeys", "AIStudioSignIn"])
|
|
359
|
+
registry.add(ai_studio_sign_in, transitions_to=["AIStudioKeys"])
|
|
360
|
+
|
|
361
|
+
registry.add(ai_studio_keys, transitions_to=[
|
|
362
|
+
"AIStudioCreateKey", "AIStudioSignIn", "AIStudioServiceBlocked",
|
|
363
|
+
])
|
|
364
|
+
registry.add(ai_studio_create_key, transitions_to=["AIStudioKeyDetails", "AIStudioKeys"])
|
|
365
|
+
registry.add(ai_studio_key_details, transitions_to=["AIStudioKeys"])
|