@mnemom/mnemom 0.17.1-next.0 → 0.17.1

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.
@@ -164,6 +164,9 @@ export async function probeCodeAccess() {
164
164
  return {
165
165
  mark: "ok",
166
166
  text: `Mnemom Agent access: enabled for ${orgLabel} (${sourceLabel(ent.source)})`,
167
+ hint: ent.remote_control
168
+ ? undefined
169
+ : "Remote Control (steer from claude.ai / the Claude app) is an internal feature — your sessions run terminal-only",
167
170
  };
168
171
  }
169
172
  catch (err) {
@@ -41,7 +41,7 @@ export declare function isCodeNotEnabled(err: unknown): err is MnemomApiError;
41
41
  * guidance + throws CodeNotEnabledError) ONLY on a determined `code_not_enabled`;
42
42
  * every other failure is inconclusive and fails open with a dim note.
43
43
  */
44
- export declare function assertCodeEnabled(orgId: string, orgLabel: string): Promise<void>;
44
+ export declare function assertCodeEnabled(orgId: string, orgLabel: string): Promise<CodeEntitlement | null>;
45
45
  /** Ensures a Mnemom session exists (injected so tests never touch the real login flow). */
46
46
  export type EnsureSession = (opts: {
47
47
  interactive?: boolean;
@@ -85,7 +85,7 @@ function printNotEnabled(orgLabel, hint) {
85
85
  */
86
86
  export async function assertCodeEnabled(orgId, orgLabel) {
87
87
  try {
88
- await getCodeEntitlement(orgId);
88
+ return await getCodeEntitlement(orgId);
89
89
  }
90
90
  catch (err) {
91
91
  if (isCodeNotEnabled(err)) {
@@ -93,6 +93,9 @@ export async function assertCodeEnabled(orgId, orgLabel) {
93
93
  throw new CodeNotEnabledError(orgLabel);
94
94
  }
95
95
  say(fmt.dim(` (Could not verify Mnemom Agent access — ${err instanceof Error ? err.message : String(err)}. Continuing.)`));
96
+ // Inconclusive (network / 5xx / membership 403) → fail open on access, but
97
+ // the null entitlement fails CLOSED on Remote Control at the call site.
98
+ return null;
96
99
  }
97
100
  }
98
101
  async function defaultEnsureSession(opts) {
@@ -23,6 +23,7 @@
23
23
  * birthThroughGateway (wrap.ts), deriveHashProof (agents.ts), claimAgent +
24
24
  * putAlignmentCard/putProtectionCard (lib/api.ts) — rather than reinventing it.
25
25
  */
26
+ import { type CodeEntitlement } from "../lib/api.js";
26
27
  /**
27
28
  * The default governed-agent slug for the current customer: `agent-<handle>`,
28
29
  * where <handle> is the local-part of their mnemom login email (sanitised), or
@@ -112,4 +113,5 @@ export declare function ensureSessionAndBalance(opts: {
112
113
  }): Promise<{
113
114
  orgId: string;
114
115
  orgLabel: string;
116
+ entitlement: CodeEntitlement | null;
115
117
  }>;
@@ -379,9 +379,9 @@ export async function ensureSessionAndBalance(opts) {
379
379
  const orgLabel = active ? `${active.name} (${active.slug})` : "your personal org";
380
380
  // Invited-cohort gate FIRST: an org that is not enabled for Mnemom Agent gets
381
381
  // the invite guidance, not a top-up message (the µ check would be moot).
382
- await assertCodeEnabled(orgId, orgLabel);
382
+ const entitlement = await assertCodeEnabled(orgId, orgLabel);
383
383
  await assertMuBalance(orgId, orgLabel);
384
- return { orgId, orgLabel };
384
+ return { orgId, orgLabel, entitlement };
385
385
  }
386
386
  // ── The default coding posture (cards observe-only; goal contracts nudge) ────
387
387
  function buildCodeAlignmentCard(agentId) {
@@ -38,7 +38,7 @@ import { fmt } from "../lib/format.js";
38
38
  import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
39
39
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
40
40
  import { defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, } from "./agent-setup.js";
41
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, isProdGateway, launchPreferenceExplicit, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, sanitizeAgentName, } from "../lib/agent.js";
41
+ import { assembleContract, buildGoalModeHeader, buildCustomHeaders, isProdGateway, launchPreferenceExplicit, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, sanitizeAgentName, } from "../lib/agent.js";
42
42
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
43
43
  const CODE_STORE = join(MNEMOM_DIR, "launcher.json");
44
44
  const say = (line = "") => console.error(line);
@@ -106,33 +106,26 @@ export async function agentCommand(scenarioArg, options = {}) {
106
106
  catch (err) {
107
107
  cliError = err instanceof Error ? err : new Error(String(err));
108
108
  }
109
- // Default launch shape: prefer Remote-Control-in-terminal (the good default —
110
- // one session steerable from this terminal AND claude.ai/the app) when the
111
- // machine can do it; otherwise terminal-only with a clear notice. An explicit
112
- // choice (flag / env / config launch=) is always respected. Needs a resolved
113
- // CLI to test RC capability, so it's applied once the CLI is known.
114
- const applyAutoShape = (resolved) => {
115
- if (shapeExplicit || shape !== "terminal")
116
- return;
117
- const rc = checkRcCapability(resolved);
118
- if (rc.ok) {
119
- shape = "remote-control";
120
- }
121
- else {
122
- say(`mnemom agent: launching terminal-only (Remote Control unavailable: ${rc.missing.join("; ")}).`);
123
- say("mnemom agent: once those are in place, `mnemom agent <scenario> --remote-control` (or `config set launch remote-control`) gives you a session steerable from claude.ai and the Claude app too.");
124
- }
125
- };
126
- if (cli)
127
- applyAutoShape(cli);
109
+ // NB: the launch SHAPE (terminal vs Remote Control) is resolved in two steps.
110
+ // Remote Control is an INTERNAL Mnemom capability, so the auto-upgrade and any
111
+ // explicit `--remote-control`/`--server` are gated on the org's entitlement —
112
+ // which we only know AFTER sign-in. The entitlement-aware decision
113
+ // (`resolveRemoteControlShape`) happens below, right after
114
+ // `ensureSessionAndBalance`. --dry-run prints the plan BEFORE sign-in, so it
115
+ // can't know the entitlement; it previews the shape the MACHINE is capable of
116
+ // (as the pre-gate behaviour did), then notes that the real launch applies the
117
+ // entitlement gate. `shape` itself stays the requested (ungated) shape.
128
118
  if (options.dryRun) {
129
119
  const planCli = cli ?? {
130
120
  bin: "(not found — pass --cli <path> or `mnemom agent config set cli <path>`)",
131
121
  name: basename((options.cli || env.MNEMOM_AGENT_CLI || "claude").trim()),
132
122
  };
123
+ const previewShape = !shapeExplicit && shape === "terminal" && cli && checkRcCapability(cli).ok
124
+ ? "remote-control"
125
+ : shape;
133
126
  printPlan({
134
127
  scenario,
135
- shape,
128
+ shape: previewShape,
136
129
  agent,
137
130
  conversationId,
138
131
  door,
@@ -142,6 +135,9 @@ export async function agentCommand(scenarioArg, options = {}) {
142
135
  yolo,
143
136
  cli: planCli,
144
137
  });
138
+ if (previewShape === "remote-control") {
139
+ say("mnemom agent: the plan shows Remote Control because this machine can run it; the real launch uses it only if your account is entitled (internal Mnemom accounts), else terminal-only.");
140
+ }
145
141
  return;
146
142
  }
147
143
  // Real launch: make sure we actually have a CLI to run. If we couldn't find it,
@@ -149,7 +145,6 @@ export async function agentCommand(scenarioArg, options = {}) {
149
145
  // non-interactive session still gets the clear, actionable error.
150
146
  if (!cli) {
151
147
  cli = await promptForCliPath(options.cli, env, cliError);
152
- applyAutoShape(cli);
153
148
  }
154
149
  // ── resolve the Anthropic key (zero 1Password; never logged) ──
155
150
  const anthropicKey = await resolveAnthropicKey(env, {
@@ -171,6 +166,30 @@ export async function agentCommand(scenarioArg, options = {}) {
171
166
  process.exit(1); // guidance already printed
172
167
  throw err;
173
168
  }
169
+ // ── Remote-Control entitlement gate ─────────────────────────────────────────
170
+ // Remote Control (a session steerable from claude.ai / the Claude app) is
171
+ // internal-only. Now that we know the org's entitlement, resolve the final
172
+ // shape: auto-upgrade only for an allowed account on a capable machine, and
173
+ // degrade an explicit RC/server request from a non-allowed account to
174
+ // terminal-only with a notice. FAIL-CLOSED — a null entitlement (gate failed
175
+ // open) or an api without the field yields no Remote Control.
176
+ const rcAllowed = remoteControlAllowed(account.entitlement);
177
+ const rc = checkRcCapability(cli);
178
+ // Preserve the "how to turn Remote Control on" guidance for an allowed account
179
+ // whose machine can't do it yet and who didn't pin a shape (the old
180
+ // applyAutoShape behaviour). Non-allowed accounts never see it — RC isn't
181
+ // theirs — resolveRemoteControlShape gives them the internal-only notice.
182
+ if (rcAllowed && !shapeExplicit && shape === "terminal" && !rc.ok) {
183
+ say(`mnemom agent: launching terminal-only (Remote Control unavailable: ${rc.missing.join("; ")}).`);
184
+ say("mnemom agent: once those are in place, `mnemom agent <scenario> --remote-control` (or `config set launch remote-control`) gives you a session steerable from claude.ai and the Claude app too.");
185
+ }
186
+ shape = resolveRemoteControlShape({
187
+ shape,
188
+ shapeExplicit,
189
+ rcAllowed,
190
+ rcCapable: rc.ok,
191
+ say,
192
+ });
174
193
  // Now that a session exists, a defaulted slug uses the real login handle from
175
194
  // /v1/auth/me (the stored token has no email, so the pre-sign-in value was the
176
195
  // "agent-user" fallback). Only when the caller didn't name an agent.
@@ -156,6 +156,39 @@ export declare function launchPreferenceExplicit(opts: {
156
156
  remoteControl?: boolean;
157
157
  server?: boolean;
158
158
  }, env?: NodeJS.ProcessEnv): boolean;
159
+ /**
160
+ * Is this account allowed to use Remote Control (a session steerable from
161
+ * claude.ai and the Claude app)? Remote Control is an INTERNAL Mnemom capability
162
+ * — only the internal cohort (@mnemom.ai) and platform staff get it; invited
163
+ * customers run terminal-only. The server decides and returns `remote_control`
164
+ * on `GET /v1/code/entitlement`; this reads that flag, FAIL-CLOSED: a `null`
165
+ * entitlement (the gate failed open / no session) or an absent field (an older
166
+ * api that predates this flag) both mean "not allowed".
167
+ */
168
+ export declare function remoteControlAllowed(entitlement: {
169
+ remote_control?: boolean;
170
+ } | null | undefined): boolean;
171
+ /**
172
+ * Final launch shape after applying the Remote-Control entitlement. Pure: the
173
+ * machine-capability probe is passed in as `rcCapable`, and narration goes
174
+ * through the injected `say`.
175
+ *
176
+ * - Not allowed + a requested RC/server shape (explicit flag, env, or
177
+ * config) → degrade to `terminal` with a one-line notice. The CLI can't
178
+ * block a binary the user owns, but it won't route them through Remote
179
+ * Control by default.
180
+ * - Not allowed + terminal → terminal (nothing to say).
181
+ * - Allowed + terminal + not pinned → AUTO-upgrade to `remote-control` when
182
+ * the machine can do it (the zero-friction default), else terminal.
183
+ * - Allowed + an explicit RC/server shape → kept as-is.
184
+ */
185
+ export declare function resolveRemoteControlShape(args: {
186
+ shape: LaunchShape;
187
+ shapeExplicit: boolean;
188
+ rcAllowed: boolean;
189
+ rcCapable: boolean;
190
+ say?: (line?: string) => void;
191
+ }): LaunchShape;
159
192
  /**
160
193
  * Build the `ANTHROPIC_CUSTOM_HEADERS` value for the terminal-only launch: the
161
194
  * real Anthropic key as `x-api-key`, plus the governed-identity headers and the
package/dist/lib/agent.js CHANGED
@@ -291,6 +291,49 @@ export function launchPreferenceExplicit(opts, env = process.env) {
291
291
  return true;
292
292
  return (env.MNEMOM_AGENT_REMOTE_CONTROL ?? "").trim() !== "";
293
293
  }
294
+ /**
295
+ * Is this account allowed to use Remote Control (a session steerable from
296
+ * claude.ai and the Claude app)? Remote Control is an INTERNAL Mnemom capability
297
+ * — only the internal cohort (@mnemom.ai) and platform staff get it; invited
298
+ * customers run terminal-only. The server decides and returns `remote_control`
299
+ * on `GET /v1/code/entitlement`; this reads that flag, FAIL-CLOSED: a `null`
300
+ * entitlement (the gate failed open / no session) or an absent field (an older
301
+ * api that predates this flag) both mean "not allowed".
302
+ */
303
+ export function remoteControlAllowed(entitlement) {
304
+ return entitlement?.remote_control === true;
305
+ }
306
+ /**
307
+ * Final launch shape after applying the Remote-Control entitlement. Pure: the
308
+ * machine-capability probe is passed in as `rcCapable`, and narration goes
309
+ * through the injected `say`.
310
+ *
311
+ * - Not allowed + a requested RC/server shape (explicit flag, env, or
312
+ * config) → degrade to `terminal` with a one-line notice. The CLI can't
313
+ * block a binary the user owns, but it won't route them through Remote
314
+ * Control by default.
315
+ * - Not allowed + terminal → terminal (nothing to say).
316
+ * - Allowed + terminal + not pinned → AUTO-upgrade to `remote-control` when
317
+ * the machine can do it (the zero-friction default), else terminal.
318
+ * - Allowed + an explicit RC/server shape → kept as-is.
319
+ */
320
+ export function resolveRemoteControlShape(args) {
321
+ const emit = args.say ?? (() => { });
322
+ if (!args.rcAllowed) {
323
+ if (args.shape !== "terminal") {
324
+ emit("mnemom agent: Remote Control is an internal Mnemom feature — launching terminal-only.");
325
+ }
326
+ return "terminal";
327
+ }
328
+ // Allowed. Only auto-upgrade a non-pinned terminal shape, and only when the
329
+ // machine can do it. When it can't, stay terminal silently — the caller owns
330
+ // the detailed "why unavailable / how to enable" narration (it has the
331
+ // RcCapability with the specific missing pieces).
332
+ if (args.shape === "terminal" && !args.shapeExplicit) {
333
+ return args.rcCapable ? "remote-control" : "terminal";
334
+ }
335
+ return args.shape;
336
+ }
294
337
  /**
295
338
  * Build the `ANTHROPIC_CUSTOM_HEADERS` value for the terminal-only launch: the
296
339
  * real Anthropic key as `x-api-key`, plus the governed-identity headers and the
package/dist/lib/api.d.ts CHANGED
@@ -1209,6 +1209,13 @@ export interface CodeEntitlement {
1209
1209
  org_id: string;
1210
1210
  enabled: true;
1211
1211
  source: "internal_cohort" | "staff" | "invitation";
1212
+ /**
1213
+ * May this account use Remote Control (a session steerable from claude.ai and
1214
+ * the Claude app)? Internal cohort + staff only; invited customers run
1215
+ * terminal-only. OPTIONAL for fail-closed back-compat: an older api that
1216
+ * predates the flag omits it, and absent is treated as `false`.
1217
+ */
1218
+ remote_control?: boolean;
1212
1219
  }
1213
1220
  /** The machine-readable code on the gate's 403 when the org is not enabled. */
1214
1221
  export declare const CODE_NOT_ENABLED = "code_not_enabled";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.1-next.0",
3
+ "version": "0.17.1",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {