@optimuslabs/harness-map-staging 1.5.28 → 1.5.30

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.
@@ -62,6 +62,7 @@ export function readClaudeSignedInTenant(env = process.env) {
62
62
  tenant_name: text('organizationName'),
63
63
  email: text('emailAddress'),
64
64
  account_uuid: text('accountUuid'),
65
+ plan: text('organizationType').toLowerCase().replace(/^claude_/, ''),
65
66
  };
66
67
  }
67
68
  catch {
@@ -69,14 +70,74 @@ export function readClaudeSignedInTenant(env = process.env) {
69
70
  return null;
70
71
  }
71
72
  }
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.`;
73
+ /**
74
+ * Block messages, one shape for every agent (Optimus Labs' prompt-block wording):
75
+ * "This device only allows <agent> <under/on/in> the <approved> <kind>. You're signed in to
76
+ * <what>. <how to switch>." With nothing approved yet: "This device doesn't allow <agent> signed in
77
+ * to <what>." and the same last sentence.
78
+ */
79
+ const TENANT_WORDS = {
80
+ claude: { name: 'Claude Code', kind: 'organization', prep: 'under' },
81
+ claude_desktop: { name: 'Claude Desktop', kind: 'organization', prep: 'under' },
82
+ cursor: { name: 'Cursor', kind: 'team', prep: 'on' },
83
+ codex: { name: 'Codex', kind: 'workspace', prep: 'in' },
84
+ };
85
+ /** A tenant's name as the admin approved it, or its id when it has none. */
86
+ function nameOf(t) {
87
+ return (t.tenant_name || '').trim() || t.tenant_id.trim();
88
+ }
89
+ function emailDomain(email) {
90
+ const address = (email ?? '').trim().toLowerCase();
91
+ const at = address.lastIndexOf('@');
92
+ return at >= 0 && at < address.length - 1 ? address.slice(at + 1) : '';
77
93
  }
78
- function label(name, id) {
79
- return name ? `${name} (${id})` : id;
94
+ function capitalised(word) {
95
+ return word ? word[0].toUpperCase() + word.slice(1) : word;
96
+ }
97
+ /** What to pick to get back in: the one approved tenant, one of several, or the company's own. */
98
+ function choice(approved, kind) {
99
+ if (approved.length === 1)
100
+ return approved[0];
101
+ return approved.length > 1 ? 'one of them' : `your company's ${kind}`;
102
+ }
103
+ function switchStep(agent, approved) {
104
+ const kind = TENANT_WORDS[agent].kind;
105
+ if (agent === 'claude_desktop')
106
+ return `Switch Claude Desktop to ${choice(approved, kind)}.`;
107
+ if (agent === 'codex')
108
+ return `Run codex logout, then codex login and choose ${choice(approved, kind)}.`;
109
+ if (agent === 'cursor') {
110
+ if (approved.length === 1)
111
+ return `Sign out of Cursor and sign in with your ${approved[0]} team account.`;
112
+ return approved.length > 1
113
+ ? 'Sign out of Cursor and sign in with an account on one of those teams.'
114
+ : "Sign out of Cursor and sign in with your company's team account.";
115
+ }
116
+ return `Run /login and choose ${choice(approved, kind)}.`;
117
+ }
118
+ /** The block message for `agent` signed in to `signedIn` (already worded) with these approved. */
119
+ export function tenantBlockMessage(agent, approved, signedIn) {
120
+ const w = TENANT_WORDS[agent];
121
+ const lead = approved.length
122
+ ? `This device only allows ${w.name} ${w.prep} the ${approved.join(' or ')} ${w.kind}. ` +
123
+ `You're signed in to ${signedIn}.`
124
+ : `This device doesn't allow ${w.name} signed in to ${signedIn}.`;
125
+ return `${lead} ${switchStep(agent, approved)}`;
126
+ }
127
+ const CLAUDE_PERSONAL_PLANS = new Set(['free', 'pro', 'max']);
128
+ /** "a personal Max account (gmail.com)" or "the Beta Corp organization". */
129
+ function claudeSignedInText(signedIn) {
130
+ const plan = (signedIn.plan ?? '').trim().toLowerCase();
131
+ const domain = emailDomain(signedIn.email);
132
+ const at = domain ? ` (${domain})` : '';
133
+ // A personal claude.ai org is named after its owner: "ada@gmail.com's Organization".
134
+ if (CLAUDE_PERSONAL_PLANS.has(plan) || /'s organization$/i.test(signedIn.tenant_name)) {
135
+ const planWord = CLAUDE_PERSONAL_PLANS.has(plan) ? `${capitalised(plan)} ` : '';
136
+ return `a personal ${planWord}account${at}`;
137
+ }
138
+ if (signedIn.tenant_name)
139
+ return `the ${signedIn.tenant_name} organization`;
140
+ return `an organization your company has not approved${at}`;
80
141
  }
81
142
  function tenantIds(tenants) {
82
143
  return (tenants ?? []).filter((t) => t && typeof t.tenant_id === 'string' && t.tenant_id.trim());
@@ -96,21 +157,11 @@ export function approvedTenantViolation(agent, approved, signedIn) {
96
157
  const unapprovedTenants = tenantIds(policy?.unapproved_tenants);
97
158
  const signedInId = signedIn.tenant_id.trim();
98
159
  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 {
160
+ const blocked = isIn(unapprovedTenants) ||
161
+ ((policy?.enforced || approvedTenants.length > 0) && !isIn(approvedTenants));
162
+ if (!blocked)
107
163
  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')));
164
+ return tenantBlockMessage(agent, approvedTenants.map(nameOf), claudeSignedInText(signedIn));
114
165
  }
115
166
  /**
116
167
  * The tenants a Cursor login is in: its teams, or, on no team, `@` and its email domain. Mirrors the
@@ -142,49 +193,72 @@ export function readCursorSignedInTeams(home = homedir()) {
142
193
  * the login; failing that, one marked unapproved is always blocked, and any other -- an unknown
143
194
  * tenant, or a personal account -- only once at least one tenant is approved for this agent.
144
195
  */
145
- function listedTenantViolation(policy, tenants, noun) {
196
+ function listedTenantViolation(policy, tenants) {
146
197
  const approvedList = tenantIds(policy?.tenants);
147
198
  if (tenants.some((id) => isListed(approvedList, id)))
148
199
  return null;
149
- const what = refusedTenantsText(tenants, noun, approvedList.length > 0 || Boolean(policy?.enforced), tenantIds(policy?.unapproved_tenants));
150
- if (what === null)
200
+ const enforced = approvedList.length > 0 || Boolean(policy?.enforced);
201
+ if (!isRefused(tenants, enforced, tenantIds(policy?.unapproved_tenants)))
151
202
  return null;
152
- return { what, allowed: approvedList.map((t) => label(t.tenant_name, t.tenant_id)).join(', ') };
203
+ return { approved: approvedList.map(nameOf) };
153
204
  }
154
205
  function isListed(list, id) {
155
206
  return list.some((t) => t.tenant_id.trim() === id);
156
207
  }
157
- function describeTenant(id, noun) {
158
- return id.startsWith('@') ? `a personal account with an ${id} email` : `${noun} ${id}`;
208
+ /** Whether a login in these tenants is refused: one marked unapproved always; any other only once
209
+ * the agent is enforced (an older backend that sends no flag, with nothing approved, lets it pass). */
210
+ function isRefused(tenants, enforced, unapprovedList) {
211
+ if (tenants.some((id) => isListed(unapprovedList, id)))
212
+ return true;
213
+ return enforced;
159
214
  }
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`;
215
+ /** A tenant's name from the organization's lists, or '' when it has none there. */
216
+ function knownName(policy, id) {
217
+ const all = [...tenantIds(policy?.tenants), ...tenantIds(policy?.unapproved_tenants)];
218
+ const hit = all.find((t) => t.tenant_id.trim() === id);
219
+ return (hit?.tenant_name || '').trim();
220
+ }
221
+ /** "a personal account on no team (gmail.com)", "the Beta team" or "team 9876". */
222
+ function cursorSignedInText(signedIn, policy) {
223
+ const tenants = cursorTenants(signedIn);
224
+ if (!tenants.length || tenants[0].startsWith('@')) {
225
+ const domain = emailDomain(signedIn.email) || (tenants[0] ?? '').slice(1);
226
+ return `a personal account on no team${domain ? ` (${domain})` : ''}`;
227
+ }
228
+ return tenants
229
+ .map((id) => (knownName(policy, id) ? `the ${knownName(policy, id)} team` : `team ${id}`))
230
+ .join(' and ');
231
+ }
232
+ /** "a personal ChatGPT Plus account (gmail.com)", "the Beta workspace" or "workspace ws-1". */
233
+ function codexSignedInText(signedIn, tenants, policy) {
234
+ const tenant = tenants[0] ?? '';
235
+ if (!tenant || tenant.startsWith('@')) {
236
+ const plan = capitalised((signedIn.plan ?? '').trim().toLowerCase());
237
+ const domain = emailDomain(signedIn.email) || tenant.slice(1);
238
+ return `a personal ChatGPT ${plan ? `${plan} ` : ''}account${domain ? ` (${domain})` : ''}`;
239
+ }
240
+ const name = knownName(policy, tenant);
241
+ return name ? `the ${name} workspace` : `workspace ${tenant}`;
171
242
  }
172
243
  /** The block message when Cursor's login may not be used, else null. */
173
244
  export function cursorTeamViolation(approved, signedIn) {
174
245
  if (!signedIn)
175
246
  return null;
176
- const v = listedTenantViolation(approved?.cursor, cursorTenants(signedIn), 'team');
247
+ const v = listedTenantViolation(approved?.cursor, cursorTenants(signedIn));
177
248
  if (!v)
178
249
  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.');
250
+ return tenantBlockMessage('cursor', v.approved, cursorSignedInText(signedIn, approved?.cursor));
182
251
  }
183
252
  /**
184
253
  * Codex plans that are personal, mirroring the backend's `CodexPlans.PERSONAL`. On one of these
185
254
  * the ChatGPT account is the person's own, so the login is keyed by email domain instead.
186
255
  */
187
256
  const CODEX_PERSONAL_PLANS = new Set(['free', 'go', 'plus']);
257
+ /**
258
+ * Codex work plans, mirroring the backend's `CodexPlans.WORK`: only these are a shared workspace.
259
+ * Any other plan with a ChatGPT login (Pro, or one not recognised) is a personal account too.
260
+ */
261
+ const CODEX_WORK_PLANS = new Set(['business', 'hc', 'enterprise', 'edu', 'education']);
188
262
  /** Codex's login from ~/.codex/auth.json (claims only, never tokens), or null when there is none. */
189
263
  export function readCodexSignedIn(home = homedir()) {
190
264
  const identity = readCodexIdentity(home);
@@ -192,31 +266,34 @@ export function readCodexSignedIn(home = homedir()) {
192
266
  }
193
267
  /**
194
268
  * 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).
269
+ * (`chatgpt_account_id`, the claim Codex's own `forced_chatgpt_workspace_id` checks) on a work plan,
270
+ * else `@<email domain>`, a personal account. Null when there is no ChatGPT login (an API key).
197
271
  */
198
272
  export function codexTenants(signedIn) {
199
273
  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
- }
274
+ if (CODEX_PERSONAL_PLANS.has(plan))
275
+ return codexEmailDomainTenant(signedIn);
205
276
  const account = (signedIn.account_id ?? '').trim();
206
277
  // The collector falls back to the API organization ("org-...") with no ChatGPT account.
207
- return account && !account.startsWith('org-') ? [account] : null;
278
+ if (!account || account.startsWith('org-'))
279
+ return null;
280
+ return CODEX_WORK_PLANS.has(plan) ? [account] : codexEmailDomainTenant(signedIn);
281
+ }
282
+ /** `@<email domain>` for a Codex login, or [] when it has no email: how a personal account is keyed. */
283
+ function codexEmailDomainTenant(signedIn) {
284
+ const email = (signedIn.email ?? '').trim();
285
+ const at = email.lastIndexOf('@');
286
+ return at >= 0 && at < email.length - 1 ? [`@${email.slice(at + 1).toLowerCase()}`] : [];
208
287
  }
209
288
  /** The block message when Codex's login may not be used, else null. */
210
289
  export function codexTenantViolation(approved, signedIn) {
211
290
  const tenants = signedIn ? codexTenants(signedIn) : null;
212
291
  if (!tenants)
213
292
  return null;
214
- const v = listedTenantViolation(approved?.codex, tenants, 'workspace');
293
+ const v = listedTenantViolation(approved?.codex, tenants);
215
294
  if (!v)
216
295
  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.');
296
+ return tenantBlockMessage('codex', v.approved, codexSignedInText(signedIn, tenants, approved?.codex));
220
297
  }
221
298
  /**
222
299
  * Whether a personal account's `@<domain>` tenant is on a team domain: an allowlisted domain that
@@ -266,6 +343,7 @@ export function readClaudeDesktopSignedInTenant(home = homedir(), env = process.
266
343
  tenant_name: same ? fromClaudeCode.tenant_name : '',
267
344
  email: same ? fromClaudeCode.email : '',
268
345
  account_uuid: account || (same ? fromClaudeCode.account_uuid : ''),
346
+ plan: same ? fromClaudeCode.plan : '',
269
347
  };
270
348
  }
271
349
  /**
@@ -283,8 +361,8 @@ export function unapprovedEmailDomainViolation(email, unapproved) {
283
361
  const hit = unapproved.find((d) => domain === d || domain.endsWith(`.${d}`));
284
362
  if (!hit)
285
363
  return null;
286
- return (`this session is signed in as ${address}, on @${hit}, which your organization has unapproved. ` +
287
- 'Sign in with an account on an approved email domain.');
364
+ return (`This device doesn't allow accounts on ${hit}. You're signed in as ${address}. ` +
365
+ 'Sign in with your work account.');
288
366
  }
289
367
  /**
290
368
  * The block message when a login's email is not on the organization's email allowlist, else null.
@@ -303,8 +381,8 @@ export function emailAllowlistViolation(email, allowed) {
303
381
  const domain = address.slice(at + 1);
304
382
  if (allowed.some((d) => domain === d || domain.endsWith(`.${d}`)))
305
383
  return null;
306
- return (`this session is signed in as ${address}, and @${domain} is not on your organization's email allowlist. ` +
307
- 'Sign in with an account on an allowed email domain.');
384
+ return (`This device only allows accounts on your organization's email domains. You're signed in as ${address}. ` +
385
+ 'Sign in with your work account.');
308
386
  }
309
387
  /** The id an agent is approved under on the Agents page: Desktop's Code tab runs Claude Code. */
310
388
  const APPROVAL_ID = { claude_desktop: 'claude' };
@@ -87,9 +87,10 @@ function printAllowWithAdvisory(ide, advisoryMessage) {
87
87
  }));
88
88
  }
89
89
  }
90
+ /** Every block reads as an Optimus Labs notice: a title line, then the message. */
91
+ const BLOCK_TITLE = 'Optimus Labs\n\n';
90
92
  function blockPayload(ide, violationMessage) {
91
- const prefix = 'Prompt blocked by Optimus: ';
92
- const text = prefix + violationMessage;
93
+ const text = BLOCK_TITLE + violationMessage;
93
94
  if (ide === 'claude' || ide === 'copilot' || ide === 'opencode') {
94
95
  return JSON.stringify({ decision: 'block', reason: text, systemMessage: text });
95
96
  }
@@ -100,7 +101,7 @@ function blockPayload(ide, violationMessage) {
100
101
  * warning, so the Claude-shaped payload (which carries both) prints the same sentence twice.
101
102
  */
102
103
  function codexBlockPayload(violationMessage) {
103
- return JSON.stringify({ decision: 'block', reason: 'Prompt blocked by Optimus: ' + violationMessage });
104
+ return JSON.stringify({ decision: 'block', reason: BLOCK_TITLE + violationMessage });
104
105
  }
105
106
  function getManifestStalenessMs() {
106
107
  try {
@@ -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
- const plan = readClaudeLoginPlan(home) || claudeCodePlan(claudeJson, oauth);
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) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimuslabs/harness-map-staging",
3
- "version": "1.5.28",
3
+ "version": "1.5.30",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {