@mnemom/mnemom 0.17.3-next.6 → 0.17.3

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.
@@ -14,7 +14,8 @@
14
14
  import { spawnSync } from "node:child_process";
15
15
  import { existsSync } from "node:fs";
16
16
  import { anthropicKeySource, checkRcCapability, resolveCliBin } from "./agent.js";
17
- import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost } from "../lib/agent.js";
17
+ import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, singleCellError, } from "../lib/agent.js";
18
+ import { getApiUrl, getGatewayUrl } from "../lib/config.js";
18
19
  import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
19
20
  import { getCodeEntitlement, MnemomApiError } from "../lib/api.js";
20
21
  import { isLoggedIn } from "../lib/auth.js";
@@ -52,6 +53,14 @@ export async function agentDoctorCommand(options = {}) {
52
53
  add("ok", "Anthropic key: found in ~/.mnemom/launcher.json");
53
54
  else
54
55
  add("miss", "Anthropic key: none found", "export MNEMOM_AGENT_ANTHROPIC_KEY=sk-ant-… (or ANTHROPIC_API_KEY), or run `mnemom agent` once and accept the save prompt");
56
+ // ── one cell: the launch door must be the CLI's own cell ──
57
+ const cellError = singleCellError({
58
+ door: resolveAnthropicDoor(env),
59
+ apiUrl: getApiUrl(),
60
+ cellGateway: getGatewayUrl(),
61
+ });
62
+ if (cellError)
63
+ add("miss", "cell: mixed — a launch would be refused", cellError);
55
64
  // ── gateway health ──
56
65
  const host = resolveGatewayHost(env);
57
66
  const cell = isProdGateway(env) ? "us-2/prod" : "custom";
@@ -26,6 +26,11 @@ export interface SessionCommandDeps {
26
26
  * one `live goal` line an implicit session gets. Optional: absent ⇒ no line.
27
27
  */
28
28
  fetchGoal?: (record: SessionRecord) => Promise<GoalShowResponse>;
29
+ /**
30
+ * The gateway of the cell this CLI reads live state from. A session that ran on another
31
+ * cell is not looked up there (it would always read as "no sealed contract"). Absent ⇒ no check.
32
+ */
33
+ cellGateway?: () => string;
29
34
  out: (line: string) => void;
30
35
  err: (line: string) => void;
31
36
  }
@@ -14,7 +14,8 @@
14
14
  * only the server's hash is shown, labelled "sealed hash".
15
15
  */
16
16
  import { readSessionRecords } from "../lib/agent-ledger.js";
17
- import { MNEMOM_DIR } from "../lib/config.js";
17
+ import { getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
18
+ import { sameOrigin } from "../lib/agent.js";
18
19
  import { getAgentSession, MnemomApiError } from "../lib/api.js";
19
20
  import { fmt, formatRelativeAge } from "../lib/format.js";
20
21
  import { goalControlFetch } from "../lib/goal-api.js";
@@ -31,6 +32,7 @@ export const liveSessionDeps = {
31
32
  const creds = await resolveLaunchCreds();
32
33
  return goalControlFetch(record, "show", { thread: "main" }, { fetch, creds });
33
34
  },
35
+ cellGateway: () => getGatewayUrl(),
34
36
  out: (line) => console.log(line),
35
37
  err: (line) => console.error(line),
36
38
  };
@@ -318,9 +320,13 @@ export async function agentShowCommand(ref, opts, deps) {
318
320
  const { record, others } = resolved;
319
321
  let remote = null;
320
322
  let reason;
323
+ const cell = deps.cellGateway?.();
321
324
  if (record.agent_id === null) {
322
325
  reason = "agent id was not resolved at launch";
323
326
  }
327
+ else if (cell !== undefined && !sameOrigin(record.gateway_host, cell)) {
328
+ reason = `this session ran on ${record.gateway_host}, not this CLI's cell (${cell}) — point the CLI at that cell to read it`;
329
+ }
324
330
  else {
325
331
  try {
326
332
  remote = await deps.fetchSession(record.conversation_id, record.agent_id, record.org_id);
@@ -26,10 +26,10 @@
26
26
  import { claimAgent, getAgentByName, getMe, getMuBalance, getMyPersonalOrg, MnemomApiError, mintProtectionCapability, putAlignmentCard, putProtectionCard, } from "../lib/api.js";
27
27
  import { getAuthInfo, isLoggedIn, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
28
28
  import { getActiveOrg } from "../lib/cli-config.js";
29
- import { getWebsiteUrl } from "../lib/config.js";
29
+ import { getApiUrl, getGatewayUrl, getWebsiteUrl } from "../lib/config.js";
30
30
  import { fmt } from "../lib/format.js";
31
31
  import { askYesNo, isInteractive } from "../lib/prompt.js";
32
- import { resolveGatewayHost, sanitizeAgentName } from "../lib/agent.js";
32
+ import { resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
33
33
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
34
34
  import { deriveHashProof } from "./agents.js";
35
35
  import { birthThroughGateway } from "./wrap.js";
@@ -273,6 +273,13 @@ async function publishDefaultCards(agentId) {
273
273
  /** `mnemom agent setup [--agent <slug>]` — the explicit provisioning entry point. */
274
274
  export async function agentSetupCommand(opts = {}) {
275
275
  const gatewayHost = resolveGatewayHost(process.env);
276
+ const cellError = singleCellError({
277
+ door: resolveAnthropicDoor(process.env),
278
+ apiUrl: getApiUrl(),
279
+ cellGateway: getGatewayUrl(),
280
+ });
281
+ if (cellError)
282
+ throw new Error(cellError.replace("mnemom agent:", "mnemom agent setup:"));
276
283
  // Sign in (offer sign-up) and enforce the MU balance before any account write.
277
284
  try {
278
285
  await ensureSessionAndBalance({});
@@ -31,7 +31,7 @@ import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
- import { MNEMOM_DIR } from "../lib/config.js";
34
+ import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
35
35
  import { getAgentByName } from "../lib/api.js";
36
36
  import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
37
37
  import { loadCliConfig } from "../lib/cli-config.js";
@@ -41,7 +41,7 @@ import { fmt } from "../lib/format.js";
41
41
  import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
42
42
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
43
43
  import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
44
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
44
+ import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
45
45
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
46
46
  import { slackChannelArgs } from "../lib/slack-launch.js";
47
47
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
@@ -109,6 +109,9 @@ export async function agentCommand(scenarioArg, options = {}) {
109
109
  // ── door ──
110
110
  const gatewayHost = resolveGatewayHost(env);
111
111
  const door = resolveAnthropicDoor(env);
112
+ const cellError = singleCellError({ door, apiUrl: getApiUrl(), cellGateway: getGatewayUrl() });
113
+ if (cellError)
114
+ throw new Error(cellError);
112
115
  const cellNote = isProdGateway(env) ? "us-2/prod" : `custom (${gatewayHost})`;
113
116
  const yolo = options.yolo !== false; // default ON
114
117
  const contextHint = options.contextHint !== false; // default ON
package/dist/index.js CHANGED
@@ -100,8 +100,8 @@ program
100
100
  // CONTRACT the gateway seals on the first turn. Three launch shapes — terminal
101
101
  // (default), --remote-control (interactive RC in this terminal), --server
102
102
  // (headless RC dispatcher) — with yolo on by default. `--cli` selects the
103
- // executable (name or full path; default claude). The us-2/prod door is the
104
- // DEFAULT; set MNEMOM_AGENT_GATEWAY (or --gateway) to point elsewhere. The
103
+ // executable (name or full path; default claude). The door is the gateway of the
104
+ // cell the CLI uses (prod by default); a door on another cell is refused. The
105
105
  // Anthropic key is resolved with zero 1Password dependency (env → ~/.mnemom store
106
106
  // → interactive prompt) inside the launched process and never logged. See
107
107
  // commands/code.ts.
@@ -139,7 +139,7 @@ const agentCmd = program
139
139
  .option("--no-implicit", "Turn implicit goal alignment OFF for this launch (overrides launcher.toml goal_mode = implicit)")
140
140
  .option("--conversation-id <id>", "x-mnemom-conversation-id (default: auto-generated)")
141
141
  .option("--cli <name|path>", "Coding-agent CLI to launch: a name or full path (default: claude)")
142
- .option("--gateway <url>", "Gateway host override (default: https://gateway.mnemom.ai; or MNEMOM_AGENT_GATEWAY)")
142
+ .option("--gateway <url>", "Gateway host override — must be the same cell as the rest of the CLI (default: that cell's gateway; or MNEMOM_AGENT_GATEWAY)")
143
143
  .option("--remote-control", "Interactive Remote Control session in THIS terminal (behind a local proxy)")
144
144
  .option("--server", "Headless `claude remote-control` dispatcher (behind a local proxy)")
145
145
  .option("--model <alias|id>", "Model to launch (CLI alias or concrete id)")
@@ -63,11 +63,27 @@ export interface ContractFlags {
63
63
  export type LaunchShape = "terminal" | "remote-control" | "server";
64
64
  /**
65
65
  * Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
66
- * `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the us-2/prod host. The default
67
- * is PROD — an unset var resolves to prod, and any other cell is reached only by
68
- * NAMING it. Trailing slash trimmed so door concatenation is clean.
69
- */
70
- export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv): string;
66
+ * `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the gateway of the cell the rest
67
+ * of the CLI uses (`getGatewayUrl()`: prod unless MNEMOM_ENV / MNEMOM_RING /
68
+ * MNEMOM_GATEWAY_URL name another). An override on a different cell is refused at
69
+ * launch — see `singleCellError`. Trailing slash trimmed so door concatenation is clean.
70
+ */
71
+ export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv, cellGateway?: string): string;
72
+ /** Same scheme+host+port. An unparseable URL never matches. */
73
+ export declare function sameOrigin(a: string, b: string): boolean;
74
+ /**
75
+ * A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
76
+ * gateway of the cell the CLI signs in, resolves the agent and reads sessions on
77
+ * (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
78
+ * and runs as another cell's, so `sessions`/`show` never find its goal state.
79
+ * Returns the refusal message, or null. A loopback door is a local dev gateway and
80
+ * is exempt.
81
+ */
82
+ export declare function singleCellError(opts: {
83
+ door: string;
84
+ apiUrl: string;
85
+ cellGateway: string;
86
+ }): string | null;
71
87
  /**
72
88
  * The Anthropic door the CLI points Claude Code at:
73
89
  * `${MNEMOM_AGENT_GATEWAY}/anthropic`, or the full-door override
package/dist/lib/agent.js CHANGED
@@ -20,19 +20,56 @@
20
20
  * NAMED), but prod is never selected silently — see `resolveGatewayHost`.
21
21
  */
22
22
  import { createHash, randomUUID } from "node:crypto";
23
+ import { getGatewayUrl } from "./config.js";
23
24
  /** The us-2/prod gateway host. The default door — never any other cell silently. */
24
25
  export const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
25
26
  /**
26
27
  * Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
27
- * `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the us-2/prod host. The default
28
- * is PROD — an unset var resolves to prod, and any other cell is reached only by
29
- * NAMING it. Trailing slash trimmed so door concatenation is clean.
28
+ * `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the gateway of the cell the rest
29
+ * of the CLI uses (`getGatewayUrl()`: prod unless MNEMOM_ENV / MNEMOM_RING /
30
+ * MNEMOM_GATEWAY_URL name another). An override on a different cell is refused at
31
+ * launch — see `singleCellError`. Trailing slash trimmed so door concatenation is clean.
30
32
  */
31
- export function resolveGatewayHost(env = process.env) {
33
+ export function resolveGatewayHost(env = process.env, cellGateway = getGatewayUrl()) {
32
34
  const raw = (env.MNEMOM_AGENT_GATEWAY ?? "").trim();
33
- const host = raw.length > 0 ? raw : PROD_GATEWAY_HOST;
35
+ const host = raw.length > 0 ? raw : cellGateway;
34
36
  return host.replace(/\/+$/, "");
35
37
  }
38
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
39
+ /** Same scheme+host+port. An unparseable URL never matches. */
40
+ export function sameOrigin(a, b) {
41
+ try {
42
+ return new URL(a).origin === new URL(b).origin;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ /**
49
+ * A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
50
+ * gateway of the cell the CLI signs in, resolves the agent and reads sessions on
51
+ * (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
52
+ * and runs as another cell's, so `sessions`/`show` never find its goal state.
53
+ * Returns the refusal message, or null. A loopback door is a local dev gateway and
54
+ * is exempt.
55
+ */
56
+ export function singleCellError(opts) {
57
+ let doorUrl;
58
+ try {
59
+ doorUrl = new URL(opts.door);
60
+ }
61
+ catch {
62
+ return `mnemom agent: the gateway '${opts.door}' is not a valid URL.`;
63
+ }
64
+ if (LOOPBACK_HOSTS.has(doorUrl.hostname))
65
+ return null;
66
+ if (sameOrigin(opts.door, opts.cellGateway))
67
+ return null;
68
+ return (`mnemom agent: the gateway ${doorUrl.origin} is on a different cell from the one this CLI uses ` +
69
+ `(API ${opts.apiUrl}, gateway ${opts.cellGateway}). A launch must use one cell. Point the whole ` +
70
+ "CLI at that cell (MNEMOM_API_URL + MNEMOM_GATEWAY_URL, or MNEMOM_RING) and drop the gateway " +
71
+ "override (--gateway, MNEMOM_AGENT_GATEWAY, MNEMOM_AGENT_ANTHROPIC_GATEWAY, or `gateway` in launcher.toml).");
72
+ }
36
73
  /**
37
74
  * The Anthropic door the CLI points Claude Code at:
38
75
  * `${MNEMOM_AGENT_GATEWAY}/anthropic`, or the full-door override
@@ -86,7 +86,7 @@ function ensureGuardrails(cfg) {
86
86
  export const CONFIG_KEYS = [
87
87
  {
88
88
  key: "gateway",
89
- describe: "Gateway host (default https://gateway.mnemom.ai — the us-2/prod cell)",
89
+ describe: "Gateway host (default: the gateway of the cell the CLI uses — prod unless MNEMOM_ENV/MNEMOM_RING/MNEMOM_GATEWAY_URL say otherwise; a different cell is refused)",
90
90
  get: (c) => c.gateway,
91
91
  set: (c, v) => {
92
92
  c.gateway = coerceNonEmpty("gateway", v);
@@ -53,7 +53,9 @@ export function ensureRouter(argv = [process.execPath, process.argv[1] ?? "mnemo
53
53
  }
54
54
  /** Run the router until the process is killed. Reconnects on disconnect. */
55
55
  export async function runRouter(opts) {
56
- const log = opts.log ?? ((l) => console.error(`[slack-router] ${l}`));
56
+ // Lines carry Slack-supplied values (channel, handles, disconnect reason);
57
+ // strip CR/LF so a crafted message cannot forge extra log lines.
58
+ const log = opts.log ?? ((l) => console.error(`[slack-router] ${l.replace(/\n|\r/g, "")}`));
57
59
  const fetchImpl = opts.fetchImpl ?? fetch;
58
60
  const WS = globalThis.WebSocket;
59
61
  if (!WS)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.3-next.6",
3
+ "version": "0.17.3",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {