@optimuslabs/harness-map-staging 1.5.36 → 1.5.39
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 +149 -24
- package/dist/compliance_prompt_gate.js +76 -19
- 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 +67 -1
- package/dist/log_config_files/runtime/skill_block.js +253 -72
- package/dist/log_config_files/runtime/skill_block_sessions.js +126 -22
- 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
|
+
}
|
|
@@ -159,6 +159,81 @@ export function companyName(approved) {
|
|
|
159
159
|
.map(nameOf));
|
|
160
160
|
return names.size === 1 ? [...names][0] : '';
|
|
161
161
|
}
|
|
162
|
+
/** The one account to name: the approved one, else the company's; '' for several or none known. */
|
|
163
|
+
function accountOf(block) {
|
|
164
|
+
if (block.approved.length === 1)
|
|
165
|
+
return block.approved[0];
|
|
166
|
+
return block.approved.length ? '' : block.company;
|
|
167
|
+
}
|
|
168
|
+
/** The one-line block message, as the agent shows it. */
|
|
169
|
+
export function tenantBlockText(block) {
|
|
170
|
+
const account = accountOf(block);
|
|
171
|
+
if (block.agent !== 'cursor' || !account) {
|
|
172
|
+
return tenantBlockMessage(block.agent, block.approved, block.signedIn, block.company);
|
|
173
|
+
}
|
|
174
|
+
const domains = block.domains.length ? ` (${block.domains.join(', ')})` : '';
|
|
175
|
+
return (`${ORGANIZATION_LEAD} does not allow Cursor to be signed in to ${block.signedIn}. ` +
|
|
176
|
+
`The approved organization's team account for Cursor is ${account}${domains}. ` +
|
|
177
|
+
'Please sign out and sign in with the approved account to use Cursor.');
|
|
178
|
+
}
|
|
179
|
+
/** What an approved account is called, per agent: "team account" for Cursor's teams. */
|
|
180
|
+
const ACCOUNT_NOUN = {
|
|
181
|
+
claude: 'organization',
|
|
182
|
+
claude_desktop: 'organization',
|
|
183
|
+
cursor: 'team account',
|
|
184
|
+
codex: 'workspace',
|
|
185
|
+
};
|
|
186
|
+
/** A login block's window has nothing to open, so its one button only acknowledges: OK. */
|
|
187
|
+
function loginWindow(title, reason, path) {
|
|
188
|
+
return { brand: 'Optimus Labs', scope: 'Org-wide policy', title, reason, best_match: path, cancel: 'OK' };
|
|
189
|
+
}
|
|
190
|
+
/** When personal accounts on an email domain are what is approved: "your optimuslabs.io account". */
|
|
191
|
+
function personalPathCard(name, domains) {
|
|
192
|
+
return {
|
|
193
|
+
label: 'Approved path',
|
|
194
|
+
title: domains.length === 1 ? `Sign in with your ${domains[0]} account` : 'Sign in with an account on one of these domains',
|
|
195
|
+
subtitle: domains.length === 1 ? '' : domains.join(', '),
|
|
196
|
+
body: `Sign out of ${name}, then sign in with that account.`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** When nothing is approved to switch to: say so, and point at an admin rather than an account. */
|
|
200
|
+
function noPathCard(name, kind) {
|
|
201
|
+
return {
|
|
202
|
+
label: 'What to do',
|
|
203
|
+
title: 'Ask an admin which account to use',
|
|
204
|
+
subtitle: '',
|
|
205
|
+
body: `Your organisation hasn't approved a ${name} ${kind} yet. Sign out of ${name}, then sign in with your work account once an admin has approved it.`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** The window for a login whose tenant is refused: what it is signed in to, and what to use instead. */
|
|
209
|
+
export function tenantBlockWindow(block) {
|
|
210
|
+
const words = TENANT_WORDS[block.agent];
|
|
211
|
+
const account = accountOf(block);
|
|
212
|
+
const title = `${words.name} is signed in to an account your organisation doesn't allow`;
|
|
213
|
+
const reason = {
|
|
214
|
+
title: block.personal ? "It's a personal account." : `It isn't an approved ${words.kind}.`,
|
|
215
|
+
body: 'Agents signed in to accounts outside your organisation can send its code and data there.',
|
|
216
|
+
};
|
|
217
|
+
// An approved path is something approved for this agent itself: a team, or personal accounts on
|
|
218
|
+
// an email domain. The company's name alone (from another agent's tenant, such as a Claude
|
|
219
|
+
// organization) is not one, so it gets What to do.
|
|
220
|
+
if (!block.approved.length && block.personalDomains.length) {
|
|
221
|
+
return loginWindow(title, reason, personalPathCard(words.name, block.personalDomains));
|
|
222
|
+
}
|
|
223
|
+
if (!block.approved.length)
|
|
224
|
+
return loginWindow(title, reason, noPathCard(words.name, words.kind));
|
|
225
|
+
const how = block.agent === 'cursor'
|
|
226
|
+
? 'Sign out of Cursor, then sign in with that account.'
|
|
227
|
+
: switchStep(block.agent, block.approved, block.company);
|
|
228
|
+
return loginWindow(title, reason, {
|
|
229
|
+
label: 'Approved path',
|
|
230
|
+
title: account
|
|
231
|
+
? `Sign in with your ${account} ${ACCOUNT_NOUN[block.agent]}`
|
|
232
|
+
: `Sign in with one of: ${block.approved.join(', ')}`,
|
|
233
|
+
subtitle: block.domains.join(', '),
|
|
234
|
+
body: how,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
162
237
|
const CLAUDE_PERSONAL_PLANS = new Set(['free', 'pro', 'max']);
|
|
163
238
|
/** "a personal Max account (gmail.com)" or "the Beta Corp organization". */
|
|
164
239
|
function claudeSignedInText(signedIn) {
|
|
@@ -184,14 +259,27 @@ function tenantIds(tenants) {
|
|
|
184
259
|
* awaiting review and gets through, as an approved one does. Ids compare exactly, as the backend
|
|
185
260
|
* does -- vendor ids are not ours to case-fold.
|
|
186
261
|
*/
|
|
187
|
-
export function
|
|
262
|
+
export function approvedTenantBlock(agent, approved, signedIn) {
|
|
188
263
|
if (!signedIn)
|
|
189
264
|
return null;
|
|
190
265
|
const policy = approved?.[agent];
|
|
191
266
|
const signedInId = signedIn.tenant_id.trim();
|
|
192
267
|
if (!tenantIds(policy?.unapproved_tenants).some((t) => t.tenant_id.trim() === signedInId))
|
|
193
268
|
return null;
|
|
194
|
-
|
|
269
|
+
const signedInText = claudeSignedInText(signedIn);
|
|
270
|
+
return {
|
|
271
|
+
agent,
|
|
272
|
+
signedIn: signedInText,
|
|
273
|
+
personal: signedInText.startsWith('a personal'),
|
|
274
|
+
approved: tenantIds(policy?.tenants).map(nameOf),
|
|
275
|
+
company: companyName(approved),
|
|
276
|
+
domains: [],
|
|
277
|
+
personalDomains: [],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export function approvedTenantViolation(agent, approved, signedIn) {
|
|
281
|
+
const block = approvedTenantBlock(agent, approved, signedIn);
|
|
282
|
+
return block ? tenantBlockText(block) : null;
|
|
195
283
|
}
|
|
196
284
|
/**
|
|
197
285
|
* The tenants a Cursor login is in: its teams, or, on no team, `@` and its email domain. Mirrors the
|
|
@@ -227,7 +315,14 @@ function listedTenantViolation(policy, tenants) {
|
|
|
227
315
|
const unapprovedList = tenantIds(policy?.unapproved_tenants);
|
|
228
316
|
if (!tenants.length || !tenants.every((id) => isListed(unapprovedList, id)))
|
|
229
317
|
return null;
|
|
230
|
-
|
|
318
|
+
// Approved personal-account keys (`@acme.io`) are not teams to sign in to: never named as one,
|
|
319
|
+
// but kept as the email domains whose personal accounts are approved.
|
|
320
|
+
const approved = tenantIds(policy?.tenants);
|
|
321
|
+
const isPersonal = (t) => t.tenant_id.trim().startsWith('@');
|
|
322
|
+
return {
|
|
323
|
+
approved: approved.filter((t) => !isPersonal(t)).map(nameOf),
|
|
324
|
+
personalDomains: approved.filter(isPersonal).map((t) => t.tenant_id.trim().slice(1)),
|
|
325
|
+
};
|
|
231
326
|
}
|
|
232
327
|
function isListed(list, id) {
|
|
233
328
|
return list.some((t) => t.tenant_id.trim() === id);
|
|
@@ -259,22 +354,27 @@ function codexSignedInText(signedIn, tenants, policy) {
|
|
|
259
354
|
return name ? `the ${name} workspace` : `workspace ${tenant}`;
|
|
260
355
|
}
|
|
261
356
|
/** The block message when Cursor's login may not be used, else null. */
|
|
262
|
-
export function
|
|
357
|
+
export function cursorTeamBlock(approved, signedIn, teamDomains = []) {
|
|
263
358
|
if (!signedIn)
|
|
264
359
|
return null;
|
|
265
360
|
const v = listedTenantViolation(approved?.cursor, cursorTenants(signedIn));
|
|
266
361
|
if (!v)
|
|
267
362
|
return null;
|
|
268
363
|
const signedInText = cursorSignedInText(signedIn, approved?.cursor);
|
|
269
|
-
// The one account to point at
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
364
|
+
// The one account to point at is the approved team, else the company's name (accountOf).
|
|
365
|
+
return {
|
|
366
|
+
agent: 'cursor',
|
|
367
|
+
signedIn: signedInText,
|
|
368
|
+
personal: signedInText === 'a personal account',
|
|
369
|
+
approved: v.approved,
|
|
370
|
+
company: companyName(approved),
|
|
371
|
+
domains: teamDomains,
|
|
372
|
+
personalDomains: v.personalDomains,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
export function cursorTeamViolation(approved, signedIn, teamDomains = []) {
|
|
376
|
+
const block = cursorTeamBlock(approved, signedIn, teamDomains);
|
|
377
|
+
return block ? tenantBlockText(block) : null;
|
|
278
378
|
}
|
|
279
379
|
/**
|
|
280
380
|
* Codex plans that are personal, mirroring the backend's `CodexPlans.PERSONAL`. On one of these
|
|
@@ -313,14 +413,27 @@ function codexEmailDomainTenant(signedIn) {
|
|
|
313
413
|
return at >= 0 && at < email.length - 1 ? [`@${email.slice(at + 1).toLowerCase()}`] : [];
|
|
314
414
|
}
|
|
315
415
|
/** The block message when Codex's login may not be used, else null. */
|
|
316
|
-
export function
|
|
416
|
+
export function codexTenantBlock(approved, signedIn) {
|
|
317
417
|
const tenants = signedIn ? codexTenants(signedIn) : null;
|
|
318
418
|
if (!tenants)
|
|
319
419
|
return null;
|
|
320
420
|
const v = listedTenantViolation(approved?.codex, tenants);
|
|
321
421
|
if (!v)
|
|
322
422
|
return null;
|
|
323
|
-
|
|
423
|
+
const signedInText = codexSignedInText(signedIn, tenants, approved?.codex);
|
|
424
|
+
return {
|
|
425
|
+
agent: 'codex',
|
|
426
|
+
signedIn: signedInText,
|
|
427
|
+
personal: signedInText.startsWith('a personal'),
|
|
428
|
+
approved: v.approved,
|
|
429
|
+
company: companyName(approved),
|
|
430
|
+
domains: [],
|
|
431
|
+
personalDomains: v.personalDomains,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
export function codexTenantViolation(approved, signedIn) {
|
|
435
|
+
const block = codexTenantBlock(approved, signedIn);
|
|
436
|
+
return block ? tenantBlockText(block) : null;
|
|
324
437
|
}
|
|
325
438
|
/**
|
|
326
439
|
* The org Claude Desktop is signed into now, for its Code tab's prompts, or null when unknown.
|
|
@@ -346,24 +459,36 @@ export function readClaudeDesktopSignedInTenant(home = homedir(), env = process.
|
|
|
346
459
|
plan: same ? fromClaudeCode.plan : '',
|
|
347
460
|
};
|
|
348
461
|
}
|
|
349
|
-
|
|
350
|
-
* The block message when a login's email is on a domain the organization unapproved, else null.
|
|
351
|
-
*
|
|
352
|
-
* Unapproving a domain covers every agent and every address on it, and its subdomains
|
|
353
|
-
* (`mail.acme.com` falls under `acme.com`), the way an approved domain covers them.
|
|
354
|
-
*/
|
|
355
|
-
export function unapprovedEmailDomainViolation(email, unapproved) {
|
|
462
|
+
export function unapprovedEmailDomainBlock(email, unapproved) {
|
|
356
463
|
const address = (email ?? '').trim().toLowerCase();
|
|
357
464
|
const at = address.lastIndexOf('@');
|
|
358
465
|
if (at < 0 || !unapproved?.length)
|
|
359
466
|
return null;
|
|
360
467
|
const domain = address.slice(at + 1);
|
|
361
468
|
const hit = unapproved.find((d) => domain === d || domain.endsWith(`.${d}`));
|
|
362
|
-
|
|
469
|
+
return hit ? { domain: hit, address } : null;
|
|
470
|
+
}
|
|
471
|
+
export function unapprovedEmailDomainViolation(email, unapproved) {
|
|
472
|
+
const block = unapprovedEmailDomainBlock(email, unapproved);
|
|
473
|
+
if (!block)
|
|
363
474
|
return null;
|
|
364
|
-
return (`This device doesn't allow accounts on ${
|
|
475
|
+
return (`This device doesn't allow accounts on ${block.domain}. You're signed in as ${block.address}. ` +
|
|
365
476
|
'Sign in with your work account.');
|
|
366
477
|
}
|
|
478
|
+
/** The window for a login on an unapproved email domain. `agent` is one the gate reads logins for. */
|
|
479
|
+
export function emailDomainBlockWindow(agent, block, teamDomains = []) {
|
|
480
|
+
const name = TENANT_WORDS[agent]?.name ?? agent;
|
|
481
|
+
return loginWindow(`${name} is signed in with an email your organisation doesn't allow`, {
|
|
482
|
+
title: `Accounts on ${block.domain} aren't allowed.`,
|
|
483
|
+
body: `You're signed in as ${block.address}. Your organisation doesn't allow this email domain in any agent.`,
|
|
484
|
+
}, {
|
|
485
|
+
// With a team domain to name there is an approved path; without one, only advice.
|
|
486
|
+
label: teamDomains.length ? 'Approved path' : 'What to do',
|
|
487
|
+
title: 'Sign in with your work account',
|
|
488
|
+
subtitle: teamDomains.join(', '),
|
|
489
|
+
body: `Sign out of ${name}, then sign in with your work email.`,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
367
492
|
/** The id an agent is approved under on the Agents page: Desktop's Code tab runs Claude Code. */
|
|
368
493
|
const APPROVAL_ID = { claude_desktop: 'claude' };
|
|
369
494
|
const AGENT_NAME = {
|