@optimuslabs/harness-map-staging 1.5.35 → 1.5.38
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/dist/agent_access/decision.js +20 -0
- package/dist/agent_access/window_runner.js +129 -0
- package/dist/agent_block_window/agent_block_window.jxa +223 -0
- package/dist/approved_tenant_gate.js +196 -105
- package/dist/compliance_prompt_gate.js +89 -26
- package/dist/log_config_files/collection/skill_symlink.js +9 -0
- package/dist/log_config_files/runtime/management_storage.js +34 -0
- package/dist/log_config_files/runtime/remediation_sync.js +64 -1
- package/dist/log_config_files/runtime/skill_block.js +253 -72
- package/dist/log_config_files/runtime/skill_block_sessions.js +76 -20
- package/package.json +2 -2
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an agent may be used, and whether to open the blocked-agent window beside the block
|
|
3
|
+
* (docs/agent-exception-requests/DESIGN.md). Pure: it reads only what it is given, so the prompt
|
|
4
|
+
* gate and the tests can call it alike.
|
|
5
|
+
*
|
|
6
|
+
* An agent the organization has not approved (Unapproved, or Shadow: never reviewed) is blocked, and
|
|
7
|
+
* the window opens beside every such block.
|
|
8
|
+
*/
|
|
9
|
+
/** The id an agent is approved under: Claude Desktop's Code tab runs Claude Code. */
|
|
10
|
+
const APPROVAL_ID = { claude_desktop: 'claude' };
|
|
11
|
+
export function approvalId(agent) {
|
|
12
|
+
return APPROVAL_ID[agent] ?? agent;
|
|
13
|
+
}
|
|
14
|
+
export function agentAccessDecision(agent, approvedAgents) {
|
|
15
|
+
// Before the first sync nothing is enforced, as for the other approval lists.
|
|
16
|
+
if (!Array.isArray(approvedAgents))
|
|
17
|
+
return { allowed: true, window: false };
|
|
18
|
+
const allowed = approvedAgents.includes(approvalId(agent));
|
|
19
|
+
return { allowed, window: !allowed };
|
|
20
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shows the blocked-agent window (docs/agent-exception-requests/DESIGN.md).
|
|
3
|
+
*
|
|
4
|
+
* Started detached by the prompt gate (`launchAgentAccessWindow`), so the hook returns its block at
|
|
5
|
+
* once. Run as `node window_runner.js <agent> [<words.json>]`: an agent's own block reads the window's
|
|
6
|
+
* words from the synced approvals; a tenant or email-domain block passes them, built by the gate, in
|
|
7
|
+
* a file of its own temporary folder. It draws the window with `osascript` (macOS only),
|
|
8
|
+
* then acts on what the user chose: Open launches the approved app, Cancel only closes it. The prompt
|
|
9
|
+
* was already blocked. Nothing here talks to the network; a failure is logged and never thrown.
|
|
10
|
+
*/
|
|
11
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
12
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { basename, dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { hookRunLog } from '../log_config_files/runtime/hook_logger.js';
|
|
17
|
+
import { readRemediationInstructionsFile } from '../log_config_files/runtime/management_storage.js';
|
|
18
|
+
import { approvalId } from './decision.js';
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const WINDOW_SCRIPT = join(HERE, '..', 'agent_block_window', 'agent_block_window.jxa');
|
|
21
|
+
/** The agents the gate checks, by approval id: nothing else is ever passed to a process. */
|
|
22
|
+
const AGENTS = ['claude', 'cursor', 'codex', 'copilot', 'opencode'];
|
|
23
|
+
/** A macOS bundle identifier, and nothing that could be read as an option or a path. */
|
|
24
|
+
const BUNDLE_ID = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
|
|
25
|
+
/** Where the gate leaves a login block's words for the runner: a folder of its own under tmpdir. */
|
|
26
|
+
const WORDS_PREFIX = 'optimus-window-words-';
|
|
27
|
+
/** Write a login block's words where only the runner reads them; returns the file's path. */
|
|
28
|
+
function writeWords(words) {
|
|
29
|
+
const dir = mkdtempSync(join(tmpdir(), WORDS_PREFIX));
|
|
30
|
+
const path = join(dir, 'words.json');
|
|
31
|
+
writeFileSync(path, JSON.stringify(words), { mode: 0o600 });
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The words in a file the gate wrote with `writeWords`, and nothing else: the path must be a
|
|
36
|
+
* `words.json` directly inside one of its folders under tmpdir. The folder is removed once read.
|
|
37
|
+
*/
|
|
38
|
+
export function readWords(path) {
|
|
39
|
+
try {
|
|
40
|
+
const real = realpathSync(path);
|
|
41
|
+
const folder = dirname(real);
|
|
42
|
+
if (basename(real) !== 'words.json' || dirname(folder) !== realpathSync(tmpdir()))
|
|
43
|
+
return null;
|
|
44
|
+
if (!basename(folder).startsWith(WORDS_PREFIX))
|
|
45
|
+
return null;
|
|
46
|
+
const words = JSON.parse(readFileSync(real, 'utf8'));
|
|
47
|
+
rmSync(folder, { recursive: true, force: true });
|
|
48
|
+
return words && typeof words === 'object' ? words : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The agent's own block window, as remediation sync stored it; undefined when none was synced. */
|
|
55
|
+
function syncedWindow(agent) {
|
|
56
|
+
return readRemediationInstructionsFile().agent_access_windows?.[approvalId(agent)];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Start the runner in the background, detached from the hook, on macOS only; true when it started.
|
|
60
|
+
* `words` are a tenant or email-domain block's, built by the gate; without them the runner reads the
|
|
61
|
+
* agent's own block window from the synced approvals. False when there is nothing to show (an
|
|
62
|
+
* approvals file synced before the window existed), so Cursor's hook keeps its own alert.
|
|
63
|
+
*/
|
|
64
|
+
export function launchAgentAccessWindow(agent, words) {
|
|
65
|
+
if (process.platform !== 'darwin' || !AGENTS.includes(agent))
|
|
66
|
+
return false;
|
|
67
|
+
if (!words && !syncedWindow(agent))
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
const args = [fileURLToPath(import.meta.url), agent, ...(words ? [writeWords(words)] : [])];
|
|
71
|
+
const options = { detached: true, stdio: 'ignore' };
|
|
72
|
+
// An argument list, no shell: an agent from the fixed list above, and the path of a file it wrote.
|
|
73
|
+
const child = spawn(process.execPath, args, options); // nosafe: SAFE801
|
|
74
|
+
child.unref();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
hookRunLog(`agent_access_window: could not start: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Draw the window and return what the user did; null when it could not open. */
|
|
83
|
+
export function showWindow(words) {
|
|
84
|
+
if (!words)
|
|
85
|
+
return null;
|
|
86
|
+
const dir = mkdtempSync(join(tmpdir(), 'optimus-window-'));
|
|
87
|
+
const payloadPath = join(dir, 'payload.json');
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(payloadPath, JSON.stringify({ words }));
|
|
90
|
+
const out = spawnSync('osascript', ['-l', 'JavaScript', WINDOW_SCRIPT, payloadPath], { encoding: 'utf8' });
|
|
91
|
+
if (out.status !== 0) {
|
|
92
|
+
hookRunLog(`agent_access_window: osascript failed: ${(out.stderr || '').trim()}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return JSON.parse(out.stdout.trim());
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
hookRunLog(`agent_access_window: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Open an app by bundle identifier; anything else is refused. */
|
|
106
|
+
export function openApp(bundleId) {
|
|
107
|
+
if (!BUNDLE_ID.test(bundleId))
|
|
108
|
+
return false;
|
|
109
|
+
// `open -b` with a checked bundle identifier as its only argument, no shell.
|
|
110
|
+
spawnSync('open', ['-b', bundleId]); // nosafe: SAFE801
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
/** Act on what the user chose in the window. */
|
|
114
|
+
export function actOn(result) {
|
|
115
|
+
if (result.action === 'open')
|
|
116
|
+
openApp(result.bundle_id ?? '');
|
|
117
|
+
}
|
|
118
|
+
export function runAgentAccessWindow(agent, wordsPath = '') {
|
|
119
|
+
const words = wordsPath ? readWords(wordsPath) : syncedWindow(agent);
|
|
120
|
+
const result = showWindow(words ?? undefined);
|
|
121
|
+
if (result)
|
|
122
|
+
actOn(result);
|
|
123
|
+
}
|
|
124
|
+
const isEntryPoint = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
125
|
+
if (isEntryPoint) {
|
|
126
|
+
const [agent, wordsPath] = process.argv.slice(2);
|
|
127
|
+
if (agent && AGENTS.includes(agent))
|
|
128
|
+
runAgentAccessWindow(agent, wordsPath);
|
|
129
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The blocked-agent window (docs/agent-exception-requests/DESIGN.md), run as
|
|
3
|
+
* `osascript -l JavaScript agent_block_window.jxa <payload.json> [--self-test]`.
|
|
4
|
+
* Kept as `.jxa` so TypeScript leaves it alone; the build copies it into dist.
|
|
5
|
+
*
|
|
6
|
+
* A native macOS window with a web view. Its words come from the backend (`payload.words`); this
|
|
7
|
+
* script only draws them and prints what the user did as one JSON line:
|
|
8
|
+
* {"action": "open", "agent", "bundle_id"} or {"action": "cancel"}
|
|
9
|
+
* Closing the window, or Escape, is "cancel". Nothing here talks to the network.
|
|
10
|
+
* `--self-test` makes the page press Cancel by itself once loaded, to check the page-to-script path
|
|
11
|
+
* without a person.
|
|
12
|
+
*/
|
|
13
|
+
ObjC.import('Cocoa');
|
|
14
|
+
ObjC.import('WebKit');
|
|
15
|
+
|
|
16
|
+
const WIDTH = 540;
|
|
17
|
+
|
|
18
|
+
function readPayload(path) {
|
|
19
|
+
const text = $.NSString.stringWithContentsOfFileEncodingError(path, $.NSUTF8StringEncoding, null);
|
|
20
|
+
if (text.isNil()) throw new Error(`Cannot read ${path}`);
|
|
21
|
+
return JSON.parse(text.js);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Every payload string goes through this before it reaches the page. */
|
|
25
|
+
function esc(value) {
|
|
26
|
+
return String(value ?? '')
|
|
27
|
+
.replace(/&/g, '&')
|
|
28
|
+
.replace(/</g, '<')
|
|
29
|
+
.replace(/>/g, '>')
|
|
30
|
+
.replace(/"/g, '"')
|
|
31
|
+
.replace(/'/g, ''');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const STYLE = `
|
|
35
|
+
:root { color-scheme: dark; }
|
|
36
|
+
* { box-sizing: border-box; }
|
|
37
|
+
html, body { margin: 0; background: transparent; }
|
|
38
|
+
body { font: 14px/1.45 -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif; color: #e8e8ea;
|
|
39
|
+
-webkit-user-select: none; cursor: default; }
|
|
40
|
+
.card { background: #2b2d31; }
|
|
41
|
+
header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px;
|
|
42
|
+
border-bottom: 1px solid #3a3c41; }
|
|
43
|
+
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 15px; color: #cfd0d3; }
|
|
44
|
+
.logo { flex: none; width: 24px; height: 24px; border-radius: 50%; background: #c9f2cf; color: #15361c;
|
|
45
|
+
display: grid; place-items: center; font: 700 12px/1 ui-rounded, -apple-system, sans-serif; }
|
|
46
|
+
.logo.big { width: 44px; height: 44px; font-size: 18px; }
|
|
47
|
+
.scope { font-size: 13px; color: #b7b8bc; }
|
|
48
|
+
main { padding: 22px 20px 18px; }
|
|
49
|
+
h1 { font-size: 22px; line-height: 1.25; font-weight: 700; margin: 0 0 16px; color: #fff; }
|
|
50
|
+
.box { border-radius: 10px; padding: 14px 16px; margin-bottom: 14px; }
|
|
51
|
+
.reason { background: #3a2224; border: 1px solid #7a3d3d; display: flex; gap: 12px; }
|
|
52
|
+
.reason .icon { flex: none; width: 18px; height: 18px; margin-top: 2px; border: 1.6px solid #f0a8a8;
|
|
53
|
+
border-radius: 50%; color: #f0a8a8; display: grid; place-items: center; font: 700 11px/1 sans-serif; }
|
|
54
|
+
.reason strong { display: block; color: #fff; font-size: 15px; margin-bottom: 4px; }
|
|
55
|
+
.reason p { margin: 0; color: #dedee0; }
|
|
56
|
+
.reason hr { border: 0; border-top: 1px solid #7a3d3d; margin: 12px 0 10px; }
|
|
57
|
+
.findings { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
58
|
+
.findings dt { font-size: 12px; font-weight: 700; letter-spacing: .06em; color: #f0d58a; padding-top: 1px; }
|
|
59
|
+
.findings dt.critical, .findings dt.high { color: #f0a8a8; }
|
|
60
|
+
.findings dt.low, .findings dt.unknown { color: #b7b8bc; }
|
|
61
|
+
.findings dd { margin: 0; color: #dedee0; }
|
|
62
|
+
.path { background: #2c382f; border: 1px solid #3f6a47; }
|
|
63
|
+
.row { display: flex; align-items: center; gap: 16px; }
|
|
64
|
+
.grow { flex: 1; }
|
|
65
|
+
.label { font-size: 11.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #8fd39a; }
|
|
66
|
+
.path h2 { margin: 4px 0 2px; font-size: 16px; color: #fff; }
|
|
67
|
+
.path p { margin: 0; color: #b9bdb9; font-size: 13px; }
|
|
68
|
+
.path p.sub { color: #d4e7d7; font-size: 13.5px; margin-bottom: 4px; }
|
|
69
|
+
.path hr { border: 0; border-top: 1px solid #44574a; margin: 14px 0 12px; }
|
|
70
|
+
.also { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; color: #b9bdb9; font-size: 13px; }
|
|
71
|
+
.chip { background: #2f3134; border: 1px solid #4b4d52; border-radius: 7px; padding: 5px 12px; color: #e8e8ea; font-size: 13px; }
|
|
72
|
+
button { font: inherit; cursor: pointer; border-radius: 8px; }
|
|
73
|
+
.primary { background: #2f6fdb; color: #fff; border: 0; padding: 10px 22px; font-weight: 600; font-size: 14px; white-space: nowrap; }
|
|
74
|
+
.primary:hover { background: #3a7bea; }
|
|
75
|
+
.secondary { background: #3a3c41; color: #e8e8ea; border: 1px solid #4b4d52; padding: 8px 20px; font-size: 14px; }
|
|
76
|
+
.secondary:hover { background: #44464b; }
|
|
77
|
+
.link { background: none; border: 0; padding: 0; color: #7fb0ff; text-decoration: underline; font-size: 14px; }
|
|
78
|
+
footer { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px;
|
|
79
|
+
border-top: 1px solid #3a3c41; gap: 12px; }
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
function header(w, withScope) {
|
|
83
|
+
return `<header><div class="brand"><span class="logo">o/</span>${esc(w.brand)}</div>${
|
|
84
|
+
withScope ? `<div class="scope">${esc(w.scope)}</div>` : ''
|
|
85
|
+
}</header>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function openButton(app) {
|
|
89
|
+
return app ? `<button class="primary" data-open="${esc(app.agent)}" data-bundle="${esc(app.bundle_id)}">${esc(app.open)}</button>` : '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The other approved agents, as chips under their label; '' when there are none. */
|
|
93
|
+
function alsoApproved(w) {
|
|
94
|
+
const chips = (w.also_approved?.agents ?? []).map((a) => `<span class="chip">${esc(a.name)}</span>`).join('');
|
|
95
|
+
return chips ? `<div class="also">${esc(w.also_approved.label)} ${chips}</div>` : '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pathCard(w) {
|
|
99
|
+
const best = w.best_match;
|
|
100
|
+
// With nothing in the blocked agent's category to open, the other approved agents still show.
|
|
101
|
+
if (!best) {
|
|
102
|
+
const others = alsoApproved(w);
|
|
103
|
+
return others ? `<section class="box path">${others}</section>` : '';
|
|
104
|
+
}
|
|
105
|
+
const others = alsoApproved(w);
|
|
106
|
+
const also = others ? `<hr>${others}` : '';
|
|
107
|
+
// A login block names the account to use but has nothing to open, so no button.
|
|
108
|
+
const button = best.bundle_id ? openButton(best) : '';
|
|
109
|
+
const subtitle = best.subtitle ? `<p class="sub">${esc(best.subtitle)}</p>` : '';
|
|
110
|
+
return `<section class="box path"><div class="row"><div class="grow">
|
|
111
|
+
<div class="label">${esc(best.label)}</div><h2>${esc(best.title)}</h2>${subtitle}<p>${esc(best.body)}</p>
|
|
112
|
+
</div>${button}</div>${also}</section>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A skill's scan findings under the reason, by severity ({severity, label, text}); '' when none. */
|
|
116
|
+
function findings(reason) {
|
|
117
|
+
const rows = (reason?.findings ?? [])
|
|
118
|
+
.map((f) => {
|
|
119
|
+
const level = ['critical', 'high', 'medium', 'low'].includes(f.severity) ? f.severity : 'unknown';
|
|
120
|
+
return `<dt class="${level}">${esc(f.label)}</dt><dd>${esc(f.text)}</dd>`;
|
|
121
|
+
})
|
|
122
|
+
.join('');
|
|
123
|
+
return rows ? `<hr><dl class="findings">${rows}</dl>` : '';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formView(w) {
|
|
127
|
+
const body = w.reason?.body ? `<p>${esc(w.reason.body)}</p>` : '';
|
|
128
|
+
return `${header(w, true)}<main>
|
|
129
|
+
<h1>${esc(w.title)}</h1>
|
|
130
|
+
<section class="box reason"><span class="icon">!</span>
|
|
131
|
+
<div class="grow"><strong>${esc(w.reason?.title)}</strong>${body}${findings(w.reason)}</div></section>
|
|
132
|
+
${pathCard(w)}
|
|
133
|
+
</main>
|
|
134
|
+
<footer><span></span><button class="secondary" data-close="cancel">${esc(w.cancel)}</button></footer>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function pageHtml(p, selfTest) {
|
|
138
|
+
return `<!doctype html><html><head><meta charset="utf-8"><style>${STYLE}</style></head><body>
|
|
139
|
+
<div class="card" id="card">${formView(p.words)}</div>
|
|
140
|
+
<script>
|
|
141
|
+
const post = (m) => window.webkit.messageHandlers.optimus.postMessage(JSON.stringify(m));
|
|
142
|
+
const size = () => window.webkit.messageHandlers.optimus.postMessage(JSON.stringify({ type: 'size', height: document.getElementById('card').offsetHeight }));
|
|
143
|
+
document.querySelectorAll('[data-close]').forEach((b) => b.onclick = () => post({ action: b.dataset.close }));
|
|
144
|
+
document.querySelectorAll('[data-open]').forEach((b) => b.onclick = () => post({ action: 'open', agent: b.dataset.open, bundle_id: b.dataset.bundle }));
|
|
145
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') post({ action: 'cancel' }); });
|
|
146
|
+
window.addEventListener('load', size);
|
|
147
|
+
${selfTest ? "window.addEventListener('load', () => setTimeout(() => post({ action: 'cancel' }), 300));" : ''}
|
|
148
|
+
</script></body></html>`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function place(win, height) {
|
|
152
|
+
win.setContentSize($.NSMakeSize(WIDTH, Math.min(Math.max(height, 120), 900)));
|
|
153
|
+
win.center;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function run(argv) {
|
|
157
|
+
const payload = readPayload(argv[0]);
|
|
158
|
+
const app = $.NSApplication.sharedApplication;
|
|
159
|
+
app.setActivationPolicy($.NSApplicationActivationPolicyAccessory);
|
|
160
|
+
app.finishLaunching;
|
|
161
|
+
|
|
162
|
+
let result = null;
|
|
163
|
+
let win = null;
|
|
164
|
+
ObjC.registerSubclass({
|
|
165
|
+
name: 'OptimusBlockWindowHandler',
|
|
166
|
+
superclass: 'NSObject',
|
|
167
|
+
// Not declared as conforming to WKScriptMessageHandler: osascript cannot see that protocol, and
|
|
168
|
+
// WebKit only needs the method below.
|
|
169
|
+
methods: {
|
|
170
|
+
'userContentController:didReceiveScriptMessage:': {
|
|
171
|
+
types: ['void', ['id', 'id']],
|
|
172
|
+
implementation: function (_controller, message) {
|
|
173
|
+
const m = JSON.parse(ObjC.unwrap(message.body));
|
|
174
|
+
if (m.type === 'size') {
|
|
175
|
+
place(win, m.height);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
result = m;
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const config = $.WKWebViewConfiguration.alloc.init;
|
|
185
|
+
config.userContentController.addScriptMessageHandlerName($.OptimusBlockWindowHandler.alloc.init, 'optimus');
|
|
186
|
+
|
|
187
|
+
const width = WIDTH;
|
|
188
|
+
const style = $.NSWindowStyleMaskTitled | $.NSWindowStyleMaskClosable | $.NSWindowStyleMaskFullSizeContentView;
|
|
189
|
+
win = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer($.NSMakeRect(0, 0, width, 480), style, $.NSBackingStoreBuffered, false);
|
|
190
|
+
win.titlebarAppearsTransparent = true;
|
|
191
|
+
win.titleVisibility = $.NSWindowTitleHidden;
|
|
192
|
+
win.movableByWindowBackground = true;
|
|
193
|
+
win.appearance = $.NSAppearance.appearanceNamed($.NSAppearanceNameDarkAqua);
|
|
194
|
+
win.backgroundColor = $.NSColor.colorWithSRGBRedGreenBlueAlpha(43 / 255, 45 / 255, 49 / 255, 1);
|
|
195
|
+
win.level = $.NSFloatingWindowLevel;
|
|
196
|
+
for (const button of [$.NSWindowMiniaturizeButton, $.NSWindowZoomButton]) {
|
|
197
|
+
win.standardWindowButton(button).hidden = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const web = $.WKWebView.alloc.initWithFrameConfiguration($.NSMakeRect(0, 0, width, 480), config);
|
|
201
|
+
web.setValueForKey($.NSNumber.numberWithBool(false), 'drawsBackground');
|
|
202
|
+
web.loadHTMLStringBaseURL($(pageHtml(payload, argv[1] === '--self-test')), $.NSURL.URLWithString('about:blank'));
|
|
203
|
+
win.contentView = web;
|
|
204
|
+
win.center;
|
|
205
|
+
win.makeKeyAndOrderFront(null);
|
|
206
|
+
app.activateIgnoringOtherApps(true);
|
|
207
|
+
|
|
208
|
+
// Pump events ourselves: osascript has no running NSApplication loop to hand them to the window.
|
|
209
|
+
// NSAnyEventMask, not NSEventMaskAny: the newer constant is all 64 bits, which a JavaScript number
|
|
210
|
+
// rounds to a mask matching no event at all; the old one arrives as -1, every bit set.
|
|
211
|
+
while (result === null) {
|
|
212
|
+
const event = app.nextEventMatchingMaskUntilDateInModeDequeue(
|
|
213
|
+
$.NSAnyEventMask,
|
|
214
|
+
$.NSDate.dateWithTimeIntervalSinceNow(0.1),
|
|
215
|
+
$.NSDefaultRunLoopMode,
|
|
216
|
+
true,
|
|
217
|
+
);
|
|
218
|
+
if (!event.isNil()) app.sendEvent(event);
|
|
219
|
+
if (!win.isVisible) result = { action: 'cancel' };
|
|
220
|
+
}
|
|
221
|
+
win.orderOut(null);
|
|
222
|
+
return JSON.stringify(result);
|
|
223
|
+
}
|