@optimuslabs/harness-map-staging 1.5.27 → 1.5.29
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 +45 -8
- package/dist/compliance_prompt_gate.js +8 -13
- package/dist/log_config_files/collection/claude_desktop_orgs_collector.js +7 -1
- package/dist/log_config_files/runtime/management_storage.js +22 -9
- package/dist/log_config_files/runtime/remediation_sync.js +4 -1
- package/package.json +1 -1
|
@@ -185,6 +185,11 @@ export function cursorTeamViolation(approved, signedIn) {
|
|
|
185
185
|
* the ChatGPT account is the person's own, so the login is keyed by email domain instead.
|
|
186
186
|
*/
|
|
187
187
|
const CODEX_PERSONAL_PLANS = new Set(['free', 'go', 'plus']);
|
|
188
|
+
/**
|
|
189
|
+
* Codex work plans, mirroring the backend's `CodexPlans.WORK`: only these are a shared workspace.
|
|
190
|
+
* Any other plan with a ChatGPT login (Pro, or one not recognised) is a personal account too.
|
|
191
|
+
*/
|
|
192
|
+
const CODEX_WORK_PLANS = new Set(['business', 'hc', 'enterprise', 'edu', 'education']);
|
|
188
193
|
/** Codex's login from ~/.codex/auth.json (claims only, never tokens), or null when there is none. */
|
|
189
194
|
export function readCodexSignedIn(home = homedir()) {
|
|
190
195
|
const identity = readCodexIdentity(home);
|
|
@@ -192,19 +197,24 @@ export function readCodexSignedIn(home = homedir()) {
|
|
|
192
197
|
}
|
|
193
198
|
/**
|
|
194
199
|
* 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),
|
|
196
|
-
* `@<email domain
|
|
200
|
+
* (`chatgpt_account_id`, the claim Codex's own `forced_chatgpt_workspace_id` checks) on a work plan,
|
|
201
|
+
* else `@<email domain>`, a personal account. Null when there is no ChatGPT login (an API key).
|
|
197
202
|
*/
|
|
198
203
|
export function codexTenants(signedIn) {
|
|
199
204
|
const plan = (signedIn.plan ?? '').trim().toLowerCase();
|
|
200
|
-
if (CODEX_PERSONAL_PLANS.has(plan))
|
|
201
|
-
|
|
202
|
-
const at = email.lastIndexOf('@');
|
|
203
|
-
return at >= 0 && at < email.length - 1 ? [`@${email.slice(at + 1).toLowerCase()}`] : [];
|
|
204
|
-
}
|
|
205
|
+
if (CODEX_PERSONAL_PLANS.has(plan))
|
|
206
|
+
return codexEmailDomainTenant(signedIn);
|
|
205
207
|
const account = (signedIn.account_id ?? '').trim();
|
|
206
208
|
// The collector falls back to the API organization ("org-...") with no ChatGPT account.
|
|
207
|
-
|
|
209
|
+
if (!account || account.startsWith('org-'))
|
|
210
|
+
return null;
|
|
211
|
+
return CODEX_WORK_PLANS.has(plan) ? [account] : codexEmailDomainTenant(signedIn);
|
|
212
|
+
}
|
|
213
|
+
/** `@<email domain>` for a Codex login, or [] when it has no email: how a personal account is keyed. */
|
|
214
|
+
function codexEmailDomainTenant(signedIn) {
|
|
215
|
+
const email = (signedIn.email ?? '').trim();
|
|
216
|
+
const at = email.lastIndexOf('@');
|
|
217
|
+
return at >= 0 && at < email.length - 1 ? [`@${email.slice(at + 1).toLowerCase()}`] : [];
|
|
208
218
|
}
|
|
209
219
|
/** The block message when Codex's login may not be used, else null. */
|
|
210
220
|
export function codexTenantViolation(approved, signedIn) {
|
|
@@ -218,6 +228,33 @@ export function codexTenantViolation(approved, signedIn) {
|
|
|
218
228
|
(v.allowed ? `Approved: ${v.allowed}. ` : '') +
|
|
219
229
|
'Run `codex logout`, then sign in to an approved workspace.');
|
|
220
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Whether a personal account's `@<domain>` tenant is on a team domain: an allowlisted domain that
|
|
233
|
+
* someone on an approved team or enterprise tenant signs in with (any vendor's). A subdomain counts.
|
|
234
|
+
*/
|
|
235
|
+
export function onTeamDomain(tenant, teamDomains) {
|
|
236
|
+
if (!tenant.startsWith('@') || !teamDomains?.length)
|
|
237
|
+
return false;
|
|
238
|
+
const domain = tenant.slice(1);
|
|
239
|
+
return teamDomains.some((d) => domain === d || domain.endsWith(`.${d}`));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The block message for Cursor's login, else null. A login on a team is checked against the
|
|
243
|
+
* approved teams. A personal account is only checked once its email is on a team domain; then it
|
|
244
|
+
* is blocked unless an admin approved that personal tenant (`@<domain>`).
|
|
245
|
+
*/
|
|
246
|
+
export function cursorLoginViolation(approved, signedIn, teamDomains) {
|
|
247
|
+
if (!signedIn)
|
|
248
|
+
return null;
|
|
249
|
+
const checked = signedIn.team_ids.length > 0 || cursorTenants(signedIn).some((t) => onTeamDomain(t, teamDomains));
|
|
250
|
+
return checked ? cursorTeamViolation(approved, signedIn) : null;
|
|
251
|
+
}
|
|
252
|
+
/** The block message for Codex's login, else null; a personal plan is checked as Cursor's is. */
|
|
253
|
+
export function codexLoginViolation(approved, signedIn, teamDomains) {
|
|
254
|
+
const tenants = signedIn ? codexTenants(signedIn) : null;
|
|
255
|
+
const checked = tenants?.some((t) => !t.startsWith('@') || onTeamDomain(t, teamDomains));
|
|
256
|
+
return checked ? codexTenantViolation(approved, signedIn) : null;
|
|
257
|
+
}
|
|
221
258
|
/**
|
|
222
259
|
* The org Claude Desktop is signed into now, for its Code tab's prompts, or null when unknown.
|
|
223
260
|
*
|
|
@@ -24,7 +24,7 @@ import { tryResolveHardwareUuid } from './log_config_files/runtime/hardware_uuid
|
|
|
24
24
|
import { cursorAutofixDefersInlineVerify, syncRemediations, } from './log_config_files/runtime/remediation_sync.js';
|
|
25
25
|
import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
|
|
26
26
|
import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
|
|
27
|
-
import { agentApprovalViolation, approvedTenantViolation,
|
|
27
|
+
import { agentApprovalViolation, approvedTenantViolation, codexLoginViolation, emailAllowlistViolation, cursorLoginViolation, readClaudeDesktopSignedInTenant, readClaudeSignedInTenant, readCodexSignedIn, readCursorSignedInTeams, } from './approved_tenant_gate.js';
|
|
28
28
|
const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
29
29
|
/** After verified (or failed verify), push enforcement + session log immediately — do not wait for background runner. */
|
|
30
30
|
async function flushRemediationOutcomeToServer(complianceLogPath) {
|
|
@@ -265,23 +265,18 @@ function loginEmail(agent) {
|
|
|
265
265
|
}
|
|
266
266
|
/** Why this agent's signed-in tenant is not approved, or null when it is (or cannot be read). */
|
|
267
267
|
function tenantViolation(agent) {
|
|
268
|
-
const approved = readRemediationInstructionsFile()
|
|
268
|
+
const { approved_tenants: approved, team_email_domains: teamDomains } = readRemediationInstructionsFile();
|
|
269
269
|
if (agent === 'claude')
|
|
270
270
|
return approvedTenantViolation('claude', approved, readClaudeSignedInTenant());
|
|
271
271
|
if (agent === 'claude_desktop') {
|
|
272
272
|
return approvedTenantViolation('claude_desktop', approved, readClaudeDesktopSignedInTenant());
|
|
273
273
|
}
|
|
274
|
-
// A personal account (Cursor on no team, Codex on a personal plan)
|
|
275
|
-
//
|
|
276
|
-
if (agent === 'codex')
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
if (agent === 'cursor') {
|
|
282
|
-
const signedIn = readCursorSignedInTeams();
|
|
283
|
-
return signedIn?.team_ids.length ? cursorTeamViolation(approved, signedIn) : null;
|
|
284
|
-
}
|
|
274
|
+
// A personal account (Cursor on no team, Codex on a personal plan) is left to the email allowlist
|
|
275
|
+
// until its domain is a team domain; then it needs its personal tenant approved.
|
|
276
|
+
if (agent === 'codex')
|
|
277
|
+
return codexLoginViolation(approved, readCodexSignedIn(), teamDomains);
|
|
278
|
+
if (agent === 'cursor')
|
|
279
|
+
return cursorLoginViolation(approved, readCursorSignedInTeams(), teamDomains);
|
|
285
280
|
return null;
|
|
286
281
|
}
|
|
287
282
|
/** The block for a login: which check refused it and why, or null when every check passes. */
|
|
@@ -126,11 +126,17 @@ function claudeJsonDetails(home, account, orgUuids) {
|
|
|
126
126
|
const name = text(oauth.organizationName);
|
|
127
127
|
if (name)
|
|
128
128
|
details.orgNames[org] = name;
|
|
129
|
-
|
|
129
|
+
// The org's own type first: the login token's plan describes whichever org the token is for now,
|
|
130
|
+
// which is not this one while ~/.claude.json still caches the org from before a move to a team.
|
|
131
|
+
const plan = claudeOrgPlan(oauth) || readClaudeLoginPlan(home) || claudeCodePlan(claudeJson, oauth);
|
|
130
132
|
if (plan)
|
|
131
133
|
details.orgPlans[org] = plan;
|
|
132
134
|
return details;
|
|
133
135
|
}
|
|
136
|
+
/** The plan of the org `oauthAccount` names, fetched with it: its type, else its seat tier. */
|
|
137
|
+
function claudeOrgPlan(oauth) {
|
|
138
|
+
return text(oauth.organizationType) || text(oauth.seatTier);
|
|
139
|
+
}
|
|
134
140
|
/** Raw, in the order the backend reads Claude Code's plan, so both normalise alike. */
|
|
135
141
|
function claudeCodePlan(claudeJson, oauth) {
|
|
136
142
|
return (text(claudeJson?.subscriptionType) ||
|
|
@@ -133,20 +133,29 @@ export function writeRemediationInstructionsFile(file) {
|
|
|
133
133
|
}
|
|
134
134
|
function writeInstructionsFile(file, remediationsWrittenAt) {
|
|
135
135
|
// Most callers write `{ remediations }` only; keep the approval lists they didn't pass.
|
|
136
|
-
const onDisk = readRemediationInstructionsFile();
|
|
137
|
-
const approved_tenants = 'approved_tenants' in file ? file.approved_tenants : onDisk.approved_tenants;
|
|
138
|
-
const unapproved_email_domains = 'unapproved_email_domains' in file ? file.unapproved_email_domains : onDisk.unapproved_email_domains;
|
|
139
|
-
const allowed_email_domains = 'allowed_email_domains' in file ? file.allowed_email_domains : onDisk.allowed_email_domains;
|
|
140
|
-
const approved_agents = 'approved_agents' in file ? file.approved_agents : onDisk.approved_agents;
|
|
141
136
|
atomicWriteJson(getRemediationInstructionsPath(), {
|
|
142
137
|
...file,
|
|
143
|
-
...(
|
|
144
|
-
...(unapproved_email_domains === undefined ? {} : { unapproved_email_domains }),
|
|
145
|
-
...(allowed_email_domains === undefined ? {} : { allowed_email_domains }),
|
|
146
|
-
...(approved_agents === undefined ? {} : { approved_agents }),
|
|
138
|
+
...keptApprovalLists(file, readRemediationInstructionsFile()),
|
|
147
139
|
remediations_written_at: remediationsWrittenAt,
|
|
148
140
|
});
|
|
149
141
|
}
|
|
142
|
+
const APPROVAL_LISTS = [
|
|
143
|
+
'approved_tenants',
|
|
144
|
+
'unapproved_email_domains',
|
|
145
|
+
'allowed_email_domains',
|
|
146
|
+
'approved_agents',
|
|
147
|
+
'team_email_domains',
|
|
148
|
+
];
|
|
149
|
+
/** Each approval list from `file` when it names one, else the one on disk; absent ones left out. */
|
|
150
|
+
function keptApprovalLists(file, onDisk) {
|
|
151
|
+
const kept = {};
|
|
152
|
+
for (const key of APPROVAL_LISTS) {
|
|
153
|
+
const value = key in file ? file[key] : onDisk[key];
|
|
154
|
+
if (value !== undefined)
|
|
155
|
+
kept[key] = value;
|
|
156
|
+
}
|
|
157
|
+
return kept;
|
|
158
|
+
}
|
|
150
159
|
/** When the remediations on disk were written: recorded, or for an older file its mtime. */
|
|
151
160
|
function remediationsWrittenAtOf(onDisk) {
|
|
152
161
|
if (onDisk.remediations_written_at)
|
|
@@ -167,6 +176,10 @@ export function writeUnapprovedEmailDomains(unapproved_email_domains) {
|
|
|
167
176
|
export function writeApprovedAgents(approved_agents) {
|
|
168
177
|
writeApprovalList({ approved_agents });
|
|
169
178
|
}
|
|
179
|
+
/** Replace only the team email domains, keeping everything else on disk. */
|
|
180
|
+
export function writeTeamEmailDomains(team_email_domains) {
|
|
181
|
+
writeApprovalList({ team_email_domains });
|
|
182
|
+
}
|
|
170
183
|
/** Replace only the email allowlist, keeping everything else on disk. */
|
|
171
184
|
export function writeAllowedEmailDomains(allowed_email_domains) {
|
|
172
185
|
writeApprovalList({ allowed_email_domains });
|
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { executeBody } from '../../endpoint_client/http_transport.js';
|
|
5
5
|
import { parseJsonWithJsoncFallback, parseTomlText, stringifyToml } from '../readers/file_readers.js';
|
|
6
6
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
7
|
-
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeApprovedTenants, writeUnapprovedEmailDomains, writeAllowedEmailDomains, writeApprovedAgents, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
7
|
+
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeApprovedTenants, writeUnapprovedEmailDomains, writeAllowedEmailDomains, writeTeamEmailDomains, writeApprovedAgents, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
8
8
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
9
9
|
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
10
10
|
import { createSignature } from '../sender/signing.js';
|
|
@@ -1653,6 +1653,9 @@ function storeApprovals(syncResult) {
|
|
|
1653
1653
|
const allowedDomains = syncResult.allowed_email_domains;
|
|
1654
1654
|
if (Array.isArray(allowedDomains))
|
|
1655
1655
|
writeAllowedEmailDomains(lowercaseStrings(allowedDomains));
|
|
1656
|
+
const teamDomains = syncResult.team_email_domains;
|
|
1657
|
+
if (Array.isArray(teamDomains))
|
|
1658
|
+
writeTeamEmailDomains(lowercaseStrings(teamDomains));
|
|
1656
1659
|
}
|
|
1657
1660
|
async function reconcileInstructions(endpointBase, machineUuid, want) {
|
|
1658
1661
|
try {
|