@mirasim/computer-use 0.1.0-rc.5 → 0.1.0-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +20 -1
  2. package/dist/platform/browser/bridge/client.d.ts +2 -0
  3. package/dist/platform/browser/bridge/client.js +1 -1
  4. package/dist/platform/browser/bridge/model-commands.d.ts +1 -1
  5. package/dist/platform/browser/bridge/model-commands.js +1 -1
  6. package/dist/platform/browser/bridge/model-core.d.ts +15 -1
  7. package/dist/platform/browser/bridge/model-core.js +1 -1
  8. package/dist/platform/browser/bridge/server.js +1 -1
  9. package/dist/platform/browser/bridge-tabs.d.ts +12 -1
  10. package/dist/platform/browser/bridge-tabs.js +1 -1
  11. package/dist/platform/browser/cdp-connection.js +1 -1
  12. package/dist/platform/browser/driver-verbs.d.ts +1 -0
  13. package/dist/platform/browser/driver-verbs.js +2 -2
  14. package/dist/platform/browser/driver.d.ts +13 -1
  15. package/dist/platform/browser/driver.js +3 -2
  16. package/dist/platform/browser/messages.d.ts +6 -0
  17. package/dist/platform/browser/messages.js +2 -2
  18. package/dist/platform/chromium/actions.js +2 -2
  19. package/dist/platform/chromium/run-js.d.ts +5 -1
  20. package/dist/platform/chromium/run-js.js +2 -2
  21. package/dist/platform/chromium/scroll.js +5 -5
  22. package/dist/platform/driver-base.d.ts +1 -0
  23. package/dist/platform/driver-base.js +1 -1
  24. package/dist/platform/driver.d.ts +1 -0
  25. package/dist/platform/driver.js +4 -4
  26. package/dist/platform/electron/driver.js +1 -1
  27. package/dist/platform/electron/verbs.js +1 -1
  28. package/dist/platform/macos/type-verb.js +1 -1
  29. package/dist/platform/tools/api-contract.d.ts +1 -0
  30. package/dist/platform/tools/api.js +1 -1
  31. package/dist/platform/tools/repl-docs.js +1 -1
  32. package/dist/platform/tools/repl.js +17 -17
  33. package/extension/_locales/en/messages.json +179 -0
  34. package/extension/_locales/zh_CN/messages.json +179 -0
  35. package/extension/access.js +245 -0
  36. package/extension/ask.html +26 -0
  37. package/extension/ask.js +20 -0
  38. package/extension/background.js +79 -3
  39. package/extension/group.js +347 -80
  40. package/extension/icons/icon-128.png +0 -0
  41. package/extension/icons/icon-16.png +0 -0
  42. package/extension/icons/icon-32.png +0 -0
  43. package/extension/icons/icon-48.png +0 -0
  44. package/extension/manifest.json +8 -2
  45. package/extension/panel.css +86 -0
  46. package/extension/panel.html +86 -0
  47. package/extension/panel.js +192 -0
  48. package/extension/relay.js +268 -4
  49. package/extension/toolbar.js +172 -0
  50. package/package.json +5 -5
  51. package/scripts/extension.mjs +4 -2
@@ -0,0 +1,179 @@
1
+ {
2
+ "tipUsing": {
3
+ "message": "The Mirasim agent is using this tab. Click for the panel."
4
+ },
5
+ "tipHandedOver": {
6
+ "message": "Handed to the Mirasim agent. Click for the panel."
7
+ },
8
+ "tipJoin": {
9
+ "message": "Click to hand this tab to the Mirasim agent."
10
+ },
11
+ "tipBlocked": {
12
+ "message": "Blocked by your Mirasim site rules. Click for the panel."
13
+ },
14
+ "tipRestricted": {
15
+ "message": "Mirasim cannot use this page. Click for the panel."
16
+ },
17
+ "tipPausedPrefix": {
18
+ "message": "Agent paused. "
19
+ },
20
+ "tipOfflinePrefix": {
21
+ "message": "Mirasim is not connected. "
22
+ },
23
+ "handOverFailed": {
24
+ "message": "Could not hand this tab over:"
25
+ },
26
+ "statusConnected": {
27
+ "message": "Connected"
28
+ },
29
+ "statusOffline": {
30
+ "message": "Not connected. Start Mirasim to use the agent."
31
+ },
32
+ "pause": {
33
+ "message": "Pause agent"
34
+ },
35
+ "resume": {
36
+ "message": "Resume agent"
37
+ },
38
+ "pausedNote": {
39
+ "message": "The agent is paused: every browser command is refused until you resume it."
40
+ },
41
+ "sectionThisTab": {
42
+ "message": "This tab"
43
+ },
44
+ "tabStateNone": {
45
+ "message": "Open the panel from the toolbar icon on a tab to manage that tab."
46
+ },
47
+ "tabStateRestricted": {
48
+ "message": "Browser pages cannot be handed to Mirasim."
49
+ },
50
+ "tabStateBlocked": {
51
+ "message": "Blocked by your site rules: the agent cannot use it."
52
+ },
53
+ "tabStateUsing": {
54
+ "message": "The agent is using this tab."
55
+ },
56
+ "tabStateHanded": {
57
+ "message": "Handed to the agent: it can use this tab."
58
+ },
59
+ "tabStateGranted": {
60
+ "message": "You allowed the agent to use this tab."
61
+ },
62
+ "tabStateOutsideAll": {
63
+ "message": "Not in the Mirasim group, but the agent can use it: all tabs are allowed."
64
+ },
65
+ "tabStateOutsideAsk": {
66
+ "message": "Not handed over: the agent asks you before using it."
67
+ },
68
+ "tabStateOutside": {
69
+ "message": "Not handed over: the agent cannot see this tab."
70
+ },
71
+ "handOver": {
72
+ "message": "Hand to agent"
73
+ },
74
+ "takeBack": {
75
+ "message": "Take back"
76
+ },
77
+ "sectionAgentTabs": {
78
+ "message": "Tabs the agent is using"
79
+ },
80
+ "agentTabsNone": {
81
+ "message": "None right now"
82
+ },
83
+ "sectionScope": {
84
+ "message": "What the agent can reach"
85
+ },
86
+ "scopeGroup": {
87
+ "message": "Only the Mirasim group"
88
+ },
89
+ "scopeGroupHint": {
90
+ "message": "Tabs you hand over, the agent's own tabs and their popups. It cannot see any other tab."
91
+ },
92
+ "scopeAsk": {
93
+ "message": "Ask me"
94
+ },
95
+ "scopeAskHint": {
96
+ "message": "The agent sees every tab's title and address, and asks you before using one outside the group."
97
+ },
98
+ "scopeAll": {
99
+ "message": "All tabs"
100
+ },
101
+ "scopeAllHint": {
102
+ "message": "The agent can see and use every tab."
103
+ },
104
+ "sectionSites": {
105
+ "message": "Site rules"
106
+ },
107
+ "blockedLabel": {
108
+ "message": "Never let the agent use these sites"
109
+ },
110
+ "sitesPlaceholder": {
111
+ "message": "One site per line, e.g. bank.com (covers its subdomains)"
112
+ },
113
+ "allowOnlyLabel": {
114
+ "message": "Only let the agent use these sites"
115
+ },
116
+ "save": {
117
+ "message": "Save"
118
+ },
119
+ "saved": {
120
+ "message": "Saved"
121
+ },
122
+ "sectionLog": {
123
+ "message": "Recently refused"
124
+ },
125
+ "logNone": {
126
+ "message": "Nothing refused yet"
127
+ },
128
+ "logTab": {
129
+ "message": "tab $1"
130
+ },
131
+ "reason_site_blocked": {
132
+ "message": "Blocked site"
133
+ },
134
+ "reason_site_not_allowed": {
135
+ "message": "Not on the allow list"
136
+ },
137
+ "reason_out_of_scope": {
138
+ "message": "Tab not handed over"
139
+ },
140
+ "reason_ask_pending": {
141
+ "message": "No answer in time"
142
+ },
143
+ "reason_declined": {
144
+ "message": "You declined"
145
+ },
146
+ "reason_paused": {
147
+ "message": "Agent paused"
148
+ },
149
+ "reason_clear_cookies": {
150
+ "message": "Clearing every cookie"
151
+ },
152
+ "reason_cookie_site": {
153
+ "message": "Cookies of a restricted site"
154
+ },
155
+ "footerExtension": {
156
+ "message": "Extension $1"
157
+ },
158
+ "footerBridge": {
159
+ "message": "bridge: $1"
160
+ },
161
+ "footerBridgeOld": {
162
+ "message": "bridge: an older version"
163
+ },
164
+ "askTitle": {
165
+ "message": "Let the Mirasim agent use this tab?"
166
+ },
167
+ "askBody": {
168
+ "message": "If you allow it, the agent can read and operate this page until the tab is closed."
169
+ },
170
+ "allow": {
171
+ "message": "Allow"
172
+ },
173
+ "deny": {
174
+ "message": "Don't allow"
175
+ },
176
+ "askGone": {
177
+ "message": "This tab has been closed."
178
+ }
179
+ }
@@ -0,0 +1,179 @@
1
+ {
2
+ "tipUsing": {
3
+ "message": "Mirasim Agent 正在使用这个标签页。点击打开面板。"
4
+ },
5
+ "tipHandedOver": {
6
+ "message": "已交给 Mirasim Agent。点击打开面板。"
7
+ },
8
+ "tipJoin": {
9
+ "message": "点击把这个标签页交给 Mirasim Agent。"
10
+ },
11
+ "tipBlocked": {
12
+ "message": "被 Mirasim 站点规则阻止。点击打开面板。"
13
+ },
14
+ "tipRestricted": {
15
+ "message": "Mirasim 无法使用这个页面。点击打开面板。"
16
+ },
17
+ "tipPausedPrefix": {
18
+ "message": "Agent 已暂停。"
19
+ },
20
+ "tipOfflinePrefix": {
21
+ "message": "Mirasim 未连接。"
22
+ },
23
+ "handOverFailed": {
24
+ "message": "没能交出这个标签页:"
25
+ },
26
+ "statusConnected": {
27
+ "message": "已连接"
28
+ },
29
+ "statusOffline": {
30
+ "message": "未连接,启动 Mirasim 后 Agent 才能使用浏览器"
31
+ },
32
+ "pause": {
33
+ "message": "暂停 Agent"
34
+ },
35
+ "resume": {
36
+ "message": "恢复 Agent"
37
+ },
38
+ "pausedNote": {
39
+ "message": "Agent 已暂停:恢复之前,它发来的浏览器命令都会被拒绝。"
40
+ },
41
+ "sectionThisTab": {
42
+ "message": "当前标签页"
43
+ },
44
+ "tabStateNone": {
45
+ "message": "在标签页上点工具栏图标打开面板,才能管理那个标签页。"
46
+ },
47
+ "tabStateRestricted": {
48
+ "message": "浏览器自带的页面无法交给 Mirasim。"
49
+ },
50
+ "tabStateBlocked": {
51
+ "message": "被站点规则阻止,Agent 无法使用。"
52
+ },
53
+ "tabStateUsing": {
54
+ "message": "Agent 正在使用这个标签页。"
55
+ },
56
+ "tabStateHanded": {
57
+ "message": "已交给 Agent,它可以使用这个标签页。"
58
+ },
59
+ "tabStateGranted": {
60
+ "message": "你已允许 Agent 使用这个标签页。"
61
+ },
62
+ "tabStateOutsideAll": {
63
+ "message": "不在 Mirasim 分组里,但当前允许所有标签页,Agent 也能使用。"
64
+ },
65
+ "tabStateOutsideAsk": {
66
+ "message": "未交给 Agent,它使用前会先问你。"
67
+ },
68
+ "tabStateOutside": {
69
+ "message": "未交给 Agent,它看不到这个标签页。"
70
+ },
71
+ "handOver": {
72
+ "message": "交给 Agent"
73
+ },
74
+ "takeBack": {
75
+ "message": "收回"
76
+ },
77
+ "sectionAgentTabs": {
78
+ "message": "Agent 正在使用的标签页"
79
+ },
80
+ "agentTabsNone": {
81
+ "message": "目前没有"
82
+ },
83
+ "sectionScope": {
84
+ "message": "Agent 能访问的范围"
85
+ },
86
+ "scopeGroup": {
87
+ "message": "只限 Mirasim 分组"
88
+ },
89
+ "scopeGroupHint": {
90
+ "message": "你交出的标签页、Agent 自己开的标签页和它们的弹窗。其他标签页它看不到。"
91
+ },
92
+ "scopeAsk": {
93
+ "message": "每次问我"
94
+ },
95
+ "scopeAskHint": {
96
+ "message": "Agent 能看到所有标签页的标题和网址,使用分组外的标签页前先问你。"
97
+ },
98
+ "scopeAll": {
99
+ "message": "所有标签页"
100
+ },
101
+ "scopeAllHint": {
102
+ "message": "Agent 能看到并使用所有标签页。"
103
+ },
104
+ "sectionSites": {
105
+ "message": "站点规则"
106
+ },
107
+ "blockedLabel": {
108
+ "message": "不允许 Agent 使用这些网站"
109
+ },
110
+ "sitesPlaceholder": {
111
+ "message": "每行一个网站,例如 bank.com(包括它的子域名)"
112
+ },
113
+ "allowOnlyLabel": {
114
+ "message": "只允许 Agent 使用以下网站"
115
+ },
116
+ "save": {
117
+ "message": "保存"
118
+ },
119
+ "saved": {
120
+ "message": "已保存"
121
+ },
122
+ "sectionLog": {
123
+ "message": "最近被拦下"
124
+ },
125
+ "logNone": {
126
+ "message": "还没有被拦下的请求"
127
+ },
128
+ "logTab": {
129
+ "message": "标签页 $1"
130
+ },
131
+ "reason_site_blocked": {
132
+ "message": "阻止名单里的网站"
133
+ },
134
+ "reason_site_not_allowed": {
135
+ "message": "不在允许名单里"
136
+ },
137
+ "reason_out_of_scope": {
138
+ "message": "未交出的标签页"
139
+ },
140
+ "reason_ask_pending": {
141
+ "message": "没等到你的确认"
142
+ },
143
+ "reason_declined": {
144
+ "message": "你拒绝了"
145
+ },
146
+ "reason_paused": {
147
+ "message": "Agent 已暂停"
148
+ },
149
+ "reason_clear_cookies": {
150
+ "message": "清空所有 cookie"
151
+ },
152
+ "reason_cookie_site": {
153
+ "message": "受限网站的 cookie"
154
+ },
155
+ "footerExtension": {
156
+ "message": "扩展 $1"
157
+ },
158
+ "footerBridge": {
159
+ "message": "桥接:$1"
160
+ },
161
+ "footerBridgeOld": {
162
+ "message": "桥接:旧版本"
163
+ },
164
+ "askTitle": {
165
+ "message": "允许 Mirasim Agent 使用这个标签页吗?"
166
+ },
167
+ "askBody": {
168
+ "message": "允许后,在这个标签页关闭之前,Agent 都能读取和操作它。"
169
+ },
170
+ "allow": {
171
+ "message": "允许"
172
+ },
173
+ "deny": {
174
+ "message": "不允许"
175
+ },
176
+ "askGone": {
177
+ "message": "这个标签页已经关闭。"
178
+ }
179
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * What the agent may reach in this browser: the user's own settings, enforced HERE.
3
+ *
4
+ * The extension is the one layer of the connection the agent does not run, so the limits live
5
+ * in it and hold whichever bridge is on the other end (the desktop app's, an npm package's, an
6
+ * older one). The relay asks this module before it lists a tab, attaches one, runs a command or
7
+ * forwards an event (relay.js), and the tab group says which tabs are in scope (group.js).
8
+ *
9
+ * - **scope** — `group` (the default): tabs in the mirasim group, tabs the agent opened and
10
+ * the popups of those; `all`: every tab, as before 0.1.3; `ask`: every tab is listed, and the
11
+ * first attach of one outside the group waits for the user's yes in a small window.
12
+ * - **site rules** — a block list, and an optional allow-only list, both of host names that
13
+ * also cover their subdomains. They hold in every scope: a blocked site is neither listed,
14
+ * opened, navigated to nor read, and its cookies and downloads are filtered out.
15
+ * - **pause** — every tab is let go of, and every command but the keepalive and the tab
16
+ * listing is refused until the user resumes (group.js `reevaluate`, relay.js).
17
+ *
18
+ * Settings live in `chrome.storage.sync` (they follow the user's Chrome profile); the pause,
19
+ * the ask-mode grants and the log of refusals live in `chrome.storage.session`, gone with the
20
+ * browser session they describe.
21
+ */
22
+
23
+ export const SETTINGS_KEY = "mirasim-access";
24
+ export const STATE_KEY = "mirasim-access-state";
25
+ /** Set once the user has opened the panel; until then the toolbar shows "new". */
26
+ export const PANEL_SEEN_KEY = "mirasim-panel-seen";
27
+ export const SCOPES = ["group", "all", "ask"];
28
+ export const DEFAULT_SETTINGS = Object.freeze({ scope: "group", blocked: [], allowOnly: false, allowed: [] });
29
+ /** How many refusals the panel shows. */
30
+ const LOG_LIMIT = 20;
31
+ /** The same refusal again within this window is one entry, not a flood (a paused agent retries). */
32
+ const LOG_DEDUPE_MS = 2_000;
33
+
34
+ /**
35
+ * A pattern as the user typed it, reduced to a host name: `https://www.Bank.com/login` and
36
+ * `*.bank.com` both become what they mean (`www.bank.com`, `bank.com`). Empty for nonsense.
37
+ */
38
+ export function normalizePattern(text) {
39
+ let s = String(text ?? "").trim().toLowerCase();
40
+ if (!s) return "";
41
+ s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
42
+ s = s.split(/[/?#]/)[0];
43
+ s = s.replace(/^.*@/, "").replace(/:\d+$/, "");
44
+ s = s.replace(/^(\*\.)+/, "").replace(/^\.+/, "").replace(/\.+$/, "");
45
+ return /^[a-z0-9.-]+$/.test(s) || /^\[[0-9a-f:]+\]$/.test(s) ? s : "";
46
+ }
47
+
48
+ /** `host` is `pattern` or one of its subdomains. */
49
+ export function hostMatches(host, pattern) {
50
+ return host === pattern || host.endsWith(`.${pattern}`);
51
+ }
52
+
53
+ /**
54
+ * The host a URL loads from, or null for one that loads nothing from the web (about:blank,
55
+ * data:, the browser's own pages). A blob: URL is its origin's.
56
+ */
57
+ export function hostOf(url) {
58
+ if (!url) return null;
59
+ try {
60
+ let u = new URL(url);
61
+ if (u.protocol === "blob:") u = new URL(u.pathname);
62
+ if (!/^(https?|wss?|ftp):$/.test(u.protocol)) return null;
63
+ return u.hostname.toLowerCase();
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function cleanList(list) {
70
+ return [...new Set((Array.isArray(list) ? list : []).map(normalizePattern).filter(Boolean))];
71
+ }
72
+
73
+ /** Settings as stored, made whole: unknown fields dropped, every list normalized. */
74
+ export function cleanSettings(raw) {
75
+ const s = raw && typeof raw === "object" ? raw : {};
76
+ return {
77
+ scope: SCOPES.includes(s.scope) ? s.scope : DEFAULT_SETTINGS.scope,
78
+ blocked: cleanList(s.blocked),
79
+ allowOnly: s.allowOnly === true,
80
+ allowed: cleanList(s.allowed),
81
+ };
82
+ }
83
+
84
+ export class AccessPolicy {
85
+ constructor() {
86
+ this.settings = cleanSettings(DEFAULT_SETTINGS);
87
+ this.paused = false;
88
+ /** Ask mode: tabs the user said yes to, until they close. */
89
+ this.grants = new Set();
90
+ /** @type {Array<{at: number, reason: string, tabId?: number, url?: string, detail?: string}>} */
91
+ this.log = [];
92
+ /** @type {Set<() => void>} */
93
+ this._listeners = new Set();
94
+ chrome.storage.onChanged.addListener((changes, area) => {
95
+ if (area === "sync" && changes[SETTINGS_KEY]) {
96
+ this.settings = cleanSettings(changes[SETTINGS_KEY].newValue);
97
+ this._emit();
98
+ } else if (area === "session" && changes[STATE_KEY]) {
99
+ const before = this.paused;
100
+ this._adoptState(changes[STATE_KEY].newValue);
101
+ if (before !== this.paused) this._emit();
102
+ }
103
+ });
104
+ this.ready = this._load();
105
+ }
106
+
107
+ async _load() {
108
+ const [sync, session] = await Promise.all([
109
+ chrome.storage.sync.get(SETTINGS_KEY).catch(() => ({})),
110
+ chrome.storage.session.get(STATE_KEY).catch(() => ({})),
111
+ ]);
112
+ this.settings = cleanSettings(sync?.[SETTINGS_KEY]);
113
+ this._adoptState(session?.[STATE_KEY]);
114
+ }
115
+
116
+ _adoptState(state) {
117
+ this.paused = state?.paused === true;
118
+ this.grants = new Set(Array.isArray(state?.grants) ? state.grants.filter(Number.isInteger) : []);
119
+ this.log = Array.isArray(state?.log) ? state.log.slice(-LOG_LIMIT) : [];
120
+ }
121
+
122
+ _saveState() {
123
+ return chrome.storage.session
124
+ .set({ [STATE_KEY]: { paused: this.paused, grants: [...this.grants], log: this.log } })
125
+ .catch(() => {});
126
+ }
127
+
128
+ /** Called whenever what is reachable may have changed: settings, or the pause. */
129
+ onChange(listener) {
130
+ this._listeners.add(listener);
131
+ return () => this._listeners.delete(listener);
132
+ }
133
+
134
+ _emit() {
135
+ for (const listener of this._listeners) {
136
+ try {
137
+ listener();
138
+ } catch {
139
+ /* one listener's failure is not the others' */
140
+ }
141
+ }
142
+ }
143
+
144
+ async saveSettings(next) {
145
+ this.settings = cleanSettings(next);
146
+ await chrome.storage.sync.set({ [SETTINGS_KEY]: this.settings });
147
+ this._emit();
148
+ return this.settings;
149
+ }
150
+
151
+ async setPaused(paused) {
152
+ this.paused = paused === true;
153
+ await this._saveState();
154
+ this._emit();
155
+ }
156
+
157
+ grant(tabId) {
158
+ this.grants.add(tabId);
159
+ void this._saveState();
160
+ }
161
+
162
+ forget(tabId) {
163
+ if (this.grants.delete(tabId)) void this._saveState();
164
+ }
165
+
166
+ /** Anything stricter than "every tab, every site"? */
167
+ restrictive() {
168
+ const s = this.settings;
169
+ return s.scope !== "all" || s.blocked.length > 0 || s.allowOnly;
170
+ }
171
+
172
+ /**
173
+ * Why a URL is off limits, or null when it is not: `site-blocked` for the block list,
174
+ * `site-not-allowed` for a host outside the allow-only list. A URL that loads nothing from
175
+ * the web (about:blank, data:) is never refused here.
176
+ */
177
+ siteVerdict(url) {
178
+ const host = hostOf(url);
179
+ if (host === null) return null;
180
+ const { blocked, allowOnly, allowed } = this.settings;
181
+ if (blocked.some((p) => hostMatches(host, p))) return "site-blocked";
182
+ if (allowOnly && !allowed.some((p) => hostMatches(host, p))) return "site-not-allowed";
183
+ return null;
184
+ }
185
+
186
+ /**
187
+ * May the agent see this cookie? A cookie set for a parent domain is sent to the blocked
188
+ * subdomain too, so `.bank.com` is withheld when `online.bank.com` is blocked; on an allow list,
189
+ * a cookie counts when it is sent to some allowed host.
190
+ */
191
+ cookieAllowed(domain) {
192
+ const d = String(domain ?? "").replace(/^\./, "").toLowerCase();
193
+ if (!d) return true;
194
+ const related = (p) => hostMatches(d, p) || hostMatches(p, d);
195
+ const { blocked, allowOnly, allowed } = this.settings;
196
+ if (blocked.some(related)) return false;
197
+ if (allowOnly && !allowed.some(related)) return false;
198
+ return true;
199
+ }
200
+
201
+ record(entry) {
202
+ const last = this.log[this.log.length - 1];
203
+ const now = Date.now();
204
+ if (last && last.reason === entry.reason && last.tabId === entry.tabId && last.url === entry.url && now - last.at < LOG_DEDUPE_MS) return;
205
+ this.log.push({ at: now, ...entry });
206
+ if (this.log.length > LOG_LIMIT) this.log.splice(0, this.log.length - LOG_LIMIT);
207
+ void this._saveState();
208
+ }
209
+ }
210
+
211
+ /**
212
+ * A refusal the bridge passes on to the agent as the command's error: what was refused and
213
+ * what the agent can do about it, in words a model relays to the person.
214
+ */
215
+ export function refusal(reason, { tabId, url } = {}) {
216
+ const host = hostOf(url) ?? url ?? "this site";
217
+ switch (reason) {
218
+ case "site-blocked":
219
+ return `${host} is blocked by the user's Mirasim site rules; the agent cannot open or read it.`;
220
+ case "site-not-allowed":
221
+ return `${host} is not on the user's Mirasim allow list; the agent can only use the sites on it.`;
222
+ case "out-of-scope":
223
+ return `Tab ${tabId} is not one the user handed to the agent. Ask them to click the Mirasim toolbar icon on that tab to hand it over, or open a tab of your own.`;
224
+ case "ask-pending":
225
+ return `Waiting for the user to allow tab ${tabId} in the Mirasim prompt; try again once they answer.`;
226
+ case "declined":
227
+ return `The user declined to let the agent use tab ${tabId}.`;
228
+ case "paused":
229
+ return "The user paused the agent from the Mirasim extension; wait until they resume it.";
230
+ case "clear-cookies":
231
+ return "Clearing every browser cookie is refused while the user limits what the agent can reach.";
232
+ case "cookie-site":
233
+ return `Cookies of ${host} are off limits under the user's Mirasim site rules.`;
234
+ default:
235
+ return "Refused by the user's Mirasim access settings.";
236
+ }
237
+ }
238
+
239
+ export class AccessError extends Error {
240
+ constructor(reason, details = {}) {
241
+ super(refusal(reason, details));
242
+ this.reason = reason;
243
+ this.details = details;
244
+ }
245
+ }
@@ -0,0 +1,26 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Mirasim</title>
6
+ <link rel="stylesheet" href="panel.css" />
7
+ </head>
8
+ <body class="ask">
9
+ <main>
10
+ <header>
11
+ <img src="icons/icon-32.png" alt="" width="24" height="24" />
12
+ <div class="brand"><strong data-i18n="askTitle"></strong></div>
13
+ </header>
14
+ <div class="card">
15
+ <div id="tab-title" class="tab-title"></div>
16
+ <div id="tab-host" class="tab-host"></div>
17
+ </div>
18
+ <p class="quiet" data-i18n="askBody"></p>
19
+ <div class="row">
20
+ <button id="allow" type="button" data-i18n="allow"></button>
21
+ <button id="deny" type="button" class="ghost" data-i18n="deny"></button>
22
+ </div>
23
+ </main>
24
+ <script src="ask.js" type="module"></script>
25
+ </body>
26
+ </html>
@@ -0,0 +1,20 @@
1
+ /** The ask window (`ask` scope, group.js `_ask`): may the agent use this tab? */
2
+
3
+ const t = (key) => chrome.i18n.getMessage(key) || key;
4
+ for (const el of document.querySelectorAll("[data-i18n]")) el.textContent = t(el.dataset.i18n);
5
+
6
+ const tabId = Number(new URLSearchParams(location.search).get("tab"));
7
+ const tab = await chrome.tabs.get(tabId).catch(() => null);
8
+ if (!tab) {
9
+ document.getElementById("tab-title").textContent = t("askGone");
10
+ document.getElementById("allow").disabled = true;
11
+ } else {
12
+ document.getElementById("tab-title").textContent = tab.title || tab.url;
13
+ document.getElementById("tab-host").textContent = tab.url;
14
+ }
15
+
16
+ function answer(allow) {
17
+ chrome.runtime.sendMessage({ type: "access.answer", tabId, allow }, () => window.close());
18
+ }
19
+ document.getElementById("allow").addEventListener("click", () => answer(true));
20
+ document.getElementById("deny").addEventListener("click", () => answer(false));