@optimuslabs/harness-map-staging 1.5.25 → 1.5.27
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/approved_tenant_gate.js +302 -0
- package/dist/compliance_prompt_gate.js +315 -156
- package/dist/log_config_files/collection/claude_desktop_orgs_collector.js +175 -0
- package/dist/log_config_files/collection/claude_login_plan.js +72 -0
- package/dist/log_config_files/collection/codex_token_usage_collector.js +1 -1
- package/dist/log_config_files/collection/config_collector.js +1 -0
- package/dist/log_config_files/collection/cursor_token_usage_collector.js +127 -91
- package/dist/log_config_files/runtime/main_runner.js +69 -147
- package/dist/log_config_files/runtime/management_storage.js +46 -2
- package/dist/log_config_files/runtime/remediation_sync.js +156 -109
- package/package.json +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approved-tenant check for the pre-prompt gate.
|
|
3
|
+
*
|
|
4
|
+
* The organization's approved and unapproved vendor tenants arrive with remediation sync and are
|
|
5
|
+
* stored in remediation_instructions.json, so this reads local files only. Claude reports one tenant,
|
|
6
|
+
* `oauthAccount.organizationUuid` in `.claude.json`; Cursor reports the teams its login belongs to,
|
|
7
|
+
* `aiSettings.teamIds` in its state.vscdb.
|
|
8
|
+
*
|
|
9
|
+
* Only a claude.ai login is checked. A session without `oauthAccount` (an API key, Bedrock, a
|
|
10
|
+
* gateway) is allowed here; blocking those is a separate level. So is one whose API key, auth
|
|
11
|
+
* token, Bedrock or Vertex setting overrides an `oauthAccount` left behind by an earlier login:
|
|
12
|
+
* Claude Code is not using that login, the same precedence as the backend's derive_auth_method.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { readCursorEmail, readCursorTeamIds, vscdbPath, } from './log_config_files/collection/cursor_token_usage_collector.js';
|
|
18
|
+
import { resolveSqlite3Binary } from './log_config_files/runtime/sqlite_binary.js';
|
|
19
|
+
import { readCodexIdentity } from './log_config_files/collection/codex_token_usage_collector.js';
|
|
20
|
+
import { readDesktopActiveLogin } from './log_config_files/collection/claude_desktop_orgs_collector.js';
|
|
21
|
+
const CLAUDE_CREDENTIAL_ENV = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'];
|
|
22
|
+
const CLAUDE_PROVIDER_ENV = ['CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX'];
|
|
23
|
+
/** The `env` block of Claude Code's user settings.json, or {} when there is none to read. */
|
|
24
|
+
function claudeSettingsEnv(configDir) {
|
|
25
|
+
const path = join(configDir || join(homedir(), '.claude'), 'settings.json');
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
28
|
+
const env = parsed?.env;
|
|
29
|
+
return env && typeof env === 'object' && !Array.isArray(env) ? env : {};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether Claude Code authenticates with something other than its claude.ai login: an API key or
|
|
37
|
+
* auth token, or Bedrock / Vertex, from the session's environment or settings.json's `env`.
|
|
38
|
+
*/
|
|
39
|
+
function claudeUsesOtherCredentials(env, configDir) {
|
|
40
|
+
const settingsEnv = claudeSettingsEnv(configDir);
|
|
41
|
+
const value = (name) => String(env[name] ?? settingsEnv[name] ?? '').trim();
|
|
42
|
+
return (CLAUDE_CREDENTIAL_ENV.some((name) => value(name) !== '') ||
|
|
43
|
+
CLAUDE_PROVIDER_ENV.some((name) => ['1', 'true', 'yes', 'on'].includes(value(name).toLowerCase())));
|
|
44
|
+
}
|
|
45
|
+
/** The claude.ai org this Claude Code session is signed into, or null when there is none to check. */
|
|
46
|
+
export function readClaudeSignedInTenant(env = process.env) {
|
|
47
|
+
// Claude Code keeps .claude.json under CLAUDE_CONFIG_DIR when set, so a second account's
|
|
48
|
+
// directory is checked rather than the default one.
|
|
49
|
+
const configDir = (env.CLAUDE_CONFIG_DIR || '').trim();
|
|
50
|
+
const path = configDir ? join(configDir, '.claude.json') : join(homedir(), '.claude.json');
|
|
51
|
+
if (!existsSync(path) || claudeUsesOtherCredentials(env, configDir))
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
55
|
+
const account = parsed?.oauthAccount;
|
|
56
|
+
const tenantId = typeof account?.organizationUuid === 'string' ? account.organizationUuid.trim() : '';
|
|
57
|
+
if (!tenantId)
|
|
58
|
+
return null;
|
|
59
|
+
const text = (key) => (typeof account?.[key] === 'string' ? account[key].trim() : '');
|
|
60
|
+
return {
|
|
61
|
+
tenant_id: tenantId,
|
|
62
|
+
tenant_name: text('organizationName'),
|
|
63
|
+
email: text('emailAddress'),
|
|
64
|
+
account_uuid: text('accountUuid'),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Unreadable or mid-write: never lock the user out on a parse failure.
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** How to get out of a blocked tenant: Claude Desktop switches organization, Claude Code logs in. */
|
|
73
|
+
function fix(agent, which) {
|
|
74
|
+
return agent === 'claude_desktop'
|
|
75
|
+
? `Switch Claude Desktop to ${which} organization, or sign in to one.`
|
|
76
|
+
: `Run /login to sign in to ${which} organization.`;
|
|
77
|
+
}
|
|
78
|
+
function label(name, id) {
|
|
79
|
+
return name ? `${name} (${id})` : id;
|
|
80
|
+
}
|
|
81
|
+
function tenantIds(tenants) {
|
|
82
|
+
return (tenants ?? []).filter((t) => t && typeof t.tenant_id === 'string' && t.tenant_id.trim());
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The block message when `signedIn` may not be used for `agent`, else null.
|
|
86
|
+
*
|
|
87
|
+
* Mirrors the backend's `tenant_approval`: a tenant the organization marked unapproved is always
|
|
88
|
+
* blocked; a shadow tenant (never decided) only once the agent has at least one approved tenant.
|
|
89
|
+
* Ids compare exactly, as the backend does -- vendor ids are not ours to case-fold.
|
|
90
|
+
*/
|
|
91
|
+
export function approvedTenantViolation(agent, approved, signedIn) {
|
|
92
|
+
if (!signedIn)
|
|
93
|
+
return null;
|
|
94
|
+
const policy = approved?.[agent];
|
|
95
|
+
const approvedTenants = tenantIds(policy?.tenants);
|
|
96
|
+
const unapprovedTenants = tenantIds(policy?.unapproved_tenants);
|
|
97
|
+
const signedInId = signedIn.tenant_id.trim();
|
|
98
|
+
const isIn = (list) => list.some((t) => t.tenant_id.trim() === signedInId);
|
|
99
|
+
let reason;
|
|
100
|
+
if (isIn(unapprovedTenants)) {
|
|
101
|
+
reason = 'which your organization has marked unapproved';
|
|
102
|
+
}
|
|
103
|
+
else if ((policy?.enforced || approvedTenants.length > 0) && !isIn(approvedTenants)) {
|
|
104
|
+
reason = 'which your organization has not approved';
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const allowed = approvedTenants.map((t) => label(t.tenant_name, t.tenant_id)).join(', ');
|
|
110
|
+
return (`this session is signed into ${label(signedIn.tenant_name, signedIn.tenant_id)}, ${reason}. ` +
|
|
111
|
+
(allowed
|
|
112
|
+
? `Approved: ${allowed}. ${fix(agent, 'an approved')}`
|
|
113
|
+
: fix(agent, 'a different')));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The tenants a Cursor login is in: its teams, or, on no team, `@` and its email domain. Mirrors the
|
|
117
|
+
* backend's `cursor_tenants`, so an organization can approve its own people's personal accounts.
|
|
118
|
+
*/
|
|
119
|
+
export function cursorTenants(signedIn) {
|
|
120
|
+
const teams = signedIn.team_ids.map((t) => t.trim()).filter((t) => t.length > 0);
|
|
121
|
+
if (teams.length)
|
|
122
|
+
return teams;
|
|
123
|
+
const email = (signedIn.email ?? '').trim();
|
|
124
|
+
const at = email.lastIndexOf('@');
|
|
125
|
+
return at >= 0 && at < email.length - 1 ? [`@${email.slice(at + 1).toLowerCase()}`] : [];
|
|
126
|
+
}
|
|
127
|
+
/** The teams Cursor's signed-in login belongs to, or null when they cannot be read. */
|
|
128
|
+
export function readCursorSignedInTeams(home = homedir()) {
|
|
129
|
+
const dbPath = vscdbPath(home);
|
|
130
|
+
if (!existsSync(dbPath))
|
|
131
|
+
return null;
|
|
132
|
+
const sqlite3 = resolveSqlite3Binary();
|
|
133
|
+
if (!sqlite3)
|
|
134
|
+
return null;
|
|
135
|
+
const teamIds = readCursorTeamIds(sqlite3, dbPath);
|
|
136
|
+
return teamIds === null ? null : { team_ids: teamIds, email: readCursorEmail(sqlite3, dbPath) };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Why a login in these tenants may not be used, or null when it may. Shared by every agent whose
|
|
140
|
+
* login is a list of tenants (Cursor's teams) or one tenant that may be a personal account's
|
|
141
|
+
* `@<domain>` (Codex), and mirrors the backend's `team_tenant_approval`: any approved tenant admits
|
|
142
|
+
* the login; failing that, one marked unapproved is always blocked, and any other -- an unknown
|
|
143
|
+
* tenant, or a personal account -- only once at least one tenant is approved for this agent.
|
|
144
|
+
*/
|
|
145
|
+
function listedTenantViolation(policy, tenants, noun) {
|
|
146
|
+
const approvedList = tenantIds(policy?.tenants);
|
|
147
|
+
if (tenants.some((id) => isListed(approvedList, id)))
|
|
148
|
+
return null;
|
|
149
|
+
const what = refusedTenantsText(tenants, noun, approvedList.length > 0 || Boolean(policy?.enforced), tenantIds(policy?.unapproved_tenants));
|
|
150
|
+
if (what === null)
|
|
151
|
+
return null;
|
|
152
|
+
return { what, allowed: approvedList.map((t) => label(t.tenant_name, t.tenant_id)).join(', ') };
|
|
153
|
+
}
|
|
154
|
+
function isListed(list, id) {
|
|
155
|
+
return list.some((t) => t.tenant_id.trim() === id);
|
|
156
|
+
}
|
|
157
|
+
function describeTenant(id, noun) {
|
|
158
|
+
return id.startsWith('@') ? `a personal account with an ${id} email` : `${noun} ${id}`;
|
|
159
|
+
}
|
|
160
|
+
/** What the login is signed into and why that is refused, or null when it is not refused. */
|
|
161
|
+
function refusedTenantsText(tenants, noun, enforced, unapprovedList) {
|
|
162
|
+
const marked = tenants.find((id) => isListed(unapprovedList, id));
|
|
163
|
+
if (marked)
|
|
164
|
+
return `${describeTenant(marked, noun)}, which your organization has marked unapproved`;
|
|
165
|
+
// Not enforced (an older backend that sends no flag) and nothing approved: shadow passes.
|
|
166
|
+
if (!enforced)
|
|
167
|
+
return null;
|
|
168
|
+
if (tenants.length === 0)
|
|
169
|
+
return `a personal account that is on no ${noun}, which your organization has not approved`;
|
|
170
|
+
return `${tenants.map((id) => describeTenant(id, noun)).join(', ')}, which your organization has not approved`;
|
|
171
|
+
}
|
|
172
|
+
/** The block message when Cursor's login may not be used, else null. */
|
|
173
|
+
export function cursorTeamViolation(approved, signedIn) {
|
|
174
|
+
if (!signedIn)
|
|
175
|
+
return null;
|
|
176
|
+
const v = listedTenantViolation(approved?.cursor, cursorTenants(signedIn), 'team');
|
|
177
|
+
if (!v)
|
|
178
|
+
return null;
|
|
179
|
+
return (`this Cursor session is signed into ${v.what}. ` +
|
|
180
|
+
(v.allowed ? `Approved: ${v.allowed}. ` : '') +
|
|
181
|
+
'Sign out of Cursor and sign in with an account on an approved team.');
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Codex plans that are personal, mirroring the backend's `CodexPlans.PERSONAL`. On one of these
|
|
185
|
+
* the ChatGPT account is the person's own, so the login is keyed by email domain instead.
|
|
186
|
+
*/
|
|
187
|
+
const CODEX_PERSONAL_PLANS = new Set(['free', 'go', 'plus']);
|
|
188
|
+
/** Codex's login from ~/.codex/auth.json (claims only, never tokens), or null when there is none. */
|
|
189
|
+
export function readCodexSignedIn(home = homedir()) {
|
|
190
|
+
const identity = readCodexIdentity(home);
|
|
191
|
+
return identity.auth_mode || identity.email ? identity : null;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The tenant a Codex login is in, mirroring the backend's `codex_tenants`: its ChatGPT workspace
|
|
195
|
+
* (`chatgpt_account_id`, the claim Codex's own `forced_chatgpt_workspace_id` checks), or
|
|
196
|
+
* `@<email domain>` on a personal plan. Null when there is no ChatGPT login (an API key).
|
|
197
|
+
*/
|
|
198
|
+
export function codexTenants(signedIn) {
|
|
199
|
+
const plan = (signedIn.plan ?? '').trim().toLowerCase();
|
|
200
|
+
if (CODEX_PERSONAL_PLANS.has(plan)) {
|
|
201
|
+
const email = (signedIn.email ?? '').trim();
|
|
202
|
+
const at = email.lastIndexOf('@');
|
|
203
|
+
return at >= 0 && at < email.length - 1 ? [`@${email.slice(at + 1).toLowerCase()}`] : [];
|
|
204
|
+
}
|
|
205
|
+
const account = (signedIn.account_id ?? '').trim();
|
|
206
|
+
// The collector falls back to the API organization ("org-...") with no ChatGPT account.
|
|
207
|
+
return account && !account.startsWith('org-') ? [account] : null;
|
|
208
|
+
}
|
|
209
|
+
/** The block message when Codex's login may not be used, else null. */
|
|
210
|
+
export function codexTenantViolation(approved, signedIn) {
|
|
211
|
+
const tenants = signedIn ? codexTenants(signedIn) : null;
|
|
212
|
+
if (!tenants)
|
|
213
|
+
return null;
|
|
214
|
+
const v = listedTenantViolation(approved?.codex, tenants, 'workspace');
|
|
215
|
+
if (!v)
|
|
216
|
+
return null;
|
|
217
|
+
return (`this Codex session is signed into ${v.what}. ` +
|
|
218
|
+
(v.allowed ? `Approved: ${v.allowed}. ` : '') +
|
|
219
|
+
'Run `codex logout`, then sign in to an approved workspace.');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The org Claude Desktop is signed into now, for its Code tab's prompts, or null when unknown.
|
|
223
|
+
*
|
|
224
|
+
* Desktop keeps its own login, apart from Claude Code's ~/.claude.json, and logs every token lookup
|
|
225
|
+
* as `[oauth-v2] … orgId=<org>` (with `cacheKey=acct:<account>|…` on lookups): the newest such
|
|
226
|
+
* line is the org Desktop is using. The name and email come from ~/.claude.json only when it is the
|
|
227
|
+
* same account and org. With no such line (another OS, or a fresh log), ~/.claude.json is used, so
|
|
228
|
+
* this is never a weaker check than Claude Code's.
|
|
229
|
+
*/
|
|
230
|
+
export function readClaudeDesktopSignedInTenant(home = homedir(), env = process.env) {
|
|
231
|
+
const fromClaudeCode = readClaudeSignedInTenant(env);
|
|
232
|
+
const active = readDesktopActiveLogin(home);
|
|
233
|
+
if (!active)
|
|
234
|
+
return fromClaudeCode;
|
|
235
|
+
const { org, account } = active;
|
|
236
|
+
const same = fromClaudeCode && fromClaudeCode.tenant_id.toLowerCase() === org;
|
|
237
|
+
return {
|
|
238
|
+
tenant_id: org,
|
|
239
|
+
tenant_name: same ? fromClaudeCode.tenant_name : '',
|
|
240
|
+
email: same ? fromClaudeCode.email : '',
|
|
241
|
+
account_uuid: account || (same ? fromClaudeCode.account_uuid : ''),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The block message when a login's email is on a domain the organization unapproved, else null.
|
|
246
|
+
*
|
|
247
|
+
* Unapproving a domain covers every agent and every address on it, and its subdomains
|
|
248
|
+
* (`mail.acme.com` falls under `acme.com`), the way an approved domain covers them.
|
|
249
|
+
*/
|
|
250
|
+
export function unapprovedEmailDomainViolation(email, unapproved) {
|
|
251
|
+
const address = (email ?? '').trim().toLowerCase();
|
|
252
|
+
const at = address.lastIndexOf('@');
|
|
253
|
+
if (at < 0 || !unapproved?.length)
|
|
254
|
+
return null;
|
|
255
|
+
const domain = address.slice(at + 1);
|
|
256
|
+
const hit = unapproved.find((d) => domain === d || domain.endsWith(`.${d}`));
|
|
257
|
+
if (!hit)
|
|
258
|
+
return null;
|
|
259
|
+
return (`this session is signed in as ${address}, on @${hit}, which your organization has unapproved. ` +
|
|
260
|
+
'Sign in with an account on an approved email domain.');
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The block message when a login's email is not on the organization's email allowlist, else null.
|
|
264
|
+
*
|
|
265
|
+
* The allowlist is the organization's approved email domains; a domain covers its subdomains
|
|
266
|
+
* (`mail.acme.com` under `acme.com`). Null, so the login is allowed, when no allowlist has been
|
|
267
|
+
* synced to this machine yet or the login has no email we can read.
|
|
268
|
+
*/
|
|
269
|
+
export function emailAllowlistViolation(email, allowed) {
|
|
270
|
+
if (!Array.isArray(allowed))
|
|
271
|
+
return null;
|
|
272
|
+
const address = (email ?? '').trim().toLowerCase();
|
|
273
|
+
const at = address.lastIndexOf('@');
|
|
274
|
+
if (at < 0 || at === address.length - 1)
|
|
275
|
+
return null;
|
|
276
|
+
const domain = address.slice(at + 1);
|
|
277
|
+
if (allowed.some((d) => domain === d || domain.endsWith(`.${d}`)))
|
|
278
|
+
return null;
|
|
279
|
+
return (`this session is signed in as ${address}, and @${domain} is not on your organization's email allowlist. ` +
|
|
280
|
+
'Sign in with an account on an allowed email domain.');
|
|
281
|
+
}
|
|
282
|
+
/** The id an agent is approved under on the Agents page: Desktop's Code tab runs Claude Code. */
|
|
283
|
+
const APPROVAL_ID = { claude_desktop: 'claude' };
|
|
284
|
+
const AGENT_NAME = {
|
|
285
|
+
claude: 'Claude Code',
|
|
286
|
+
cursor: 'Cursor',
|
|
287
|
+
codex: 'Codex',
|
|
288
|
+
copilot: 'GitHub Copilot',
|
|
289
|
+
opencode: 'OpenCode',
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* The block message when this agent is not one the organization approved (unapproved or not yet
|
|
293
|
+
* reviewed), else null. Null when no approved-agents list has been synced to this machine yet.
|
|
294
|
+
*/
|
|
295
|
+
export function agentApprovalViolation(agent, approvedAgents) {
|
|
296
|
+
if (!Array.isArray(approvedAgents))
|
|
297
|
+
return null;
|
|
298
|
+
const id = APPROVAL_ID[agent] ?? agent;
|
|
299
|
+
if (approvedAgents.includes(id))
|
|
300
|
+
return null;
|
|
301
|
+
return `${AGENT_NAME[id] ?? id} is not an approved agent in your organization. Ask an admin to approve it.`;
|
|
302
|
+
}
|