@mnemom/mnemom 0.17.7-next.1 → 0.17.7
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/README.md +11 -23
- package/dist/commands/agent-doctor.d.ts +4 -21
- package/dist/commands/agent-doctor.js +5 -51
- package/dist/commands/agent-goal.d.ts +1 -1
- package/dist/commands/agent-goal.js +3 -6
- package/dist/commands/agent-setup.d.ts +5 -16
- package/dist/commands/agent-setup.js +12 -29
- package/dist/commands/agent.d.ts +9 -32
- package/dist/commands/agent.js +43 -131
- package/dist/commands/wrap.d.ts +0 -8
- package/dist/commands/wrap.js +3 -36
- package/dist/index.js +1 -1
- package/dist/lib/agent-ledger.d.ts +3 -22
- package/dist/lib/agent-ledger.js +0 -23
- package/dist/lib/agent.d.ts +27 -64
- package/dist/lib/agent.js +26 -58
- package/dist/lib/api.d.ts +2 -27
- package/dist/lib/api.js +2 -64
- package/dist/lib/launcher-config.d.ts +4 -5
- package/dist/lib/launcher-config.js +1 -1
- package/dist/lib/subscription-login.d.ts +20 -152
- package/dist/lib/subscription-login.js +28 -215
- package/dist/mcp/handoff-server.mjs +13 -35
- package/dist/rc-proxy.mjs +3 -6
- package/package.json +1 -1
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `mnemom agent`
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* with scopes exactly ["gateway"] (POST /v1/orgs/:id/api-keys; any owner/admin/member may mint
|
|
14
|
-
* one), so the gateway's agent-traffic org check (#2289: the agent's org must be the key's org)
|
|
15
|
-
* passes. There is no personal-key fallback: a personal key has no org, so the gateway would
|
|
16
|
-
* refuse every governed turn with it. When the mint is refused (an API that predates member org
|
|
17
|
-
* keys, or a viewer/auditor role) the launch stops with guidance — ask an org owner/admin for a
|
|
18
|
-
* gateway-scoped org key, or set MNEMOM_AGENT_MNEMOM_KEY to one. Each saved key records the org
|
|
19
|
-
* it was minted for; a launch in another org mints and saves a key for that org.
|
|
20
|
-
*
|
|
21
|
-
* When the launcher replaces a key it minted and saved (proven dead, minted for another org, or
|
|
22
|
-
* the legacy single-field key), it revokes the old one best-effort. An env-provided key is never
|
|
23
|
-
* replaced and never revoked.
|
|
24
|
-
*
|
|
25
|
-
* Birth org: the launcher's gateway probe/birth call carries the key AND a no-op
|
|
26
|
-
* `x-mnemom-session-config`, which makes it Mnemom agent traffic: the gateway verifies the key
|
|
27
|
-
* and births a new agent in the key's org. The launcher's claim step then records the claim in
|
|
28
|
-
* that same org.
|
|
29
|
-
*
|
|
30
|
-
* With a Claude subscription login (`auth = subscription`) — Team and Enterprise plans only;
|
|
31
|
-
* the gateway checks the plan with Anthropic and refuses anything else
|
|
32
|
-
* (gateway/src/subscription-auth.ts) — Claude Code authenticates to the gateway with its own
|
|
33
|
-
* claude.ai OAuth login: the launch sets ANTHROPIC_BASE_URL and leaves ANTHROPIC_API_KEY /
|
|
34
|
-
* ANTHROPIC_AUTH_TOKEN unset, so Claude Code sends `Authorization: Bearer <its login>`. No
|
|
35
|
-
* Anthropic key is involved; the gateway anchors the governed identity on the Mnemom key and
|
|
36
|
-
* creates the agent in that key's org on the first turn, so there is no CLI-side birth or
|
|
37
|
-
* claim.
|
|
2
|
+
* `mnemom agent` with a Claude subscription login (`auth = subscription`) — Team and
|
|
3
|
+
* Enterprise plans only; the gateway checks the plan with Anthropic and refuses anything
|
|
4
|
+
* else (gateway/src/subscription-auth.ts).
|
|
5
|
+
*
|
|
6
|
+
* Claude Code then authenticates to the gateway with its own claude.ai OAuth login: the
|
|
7
|
+
* launch sets ANTHROPIC_BASE_URL and leaves ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN unset,
|
|
8
|
+
* so Claude Code sends `Authorization: Bearer <its login>`. No Anthropic key is involved.
|
|
9
|
+
* The gateway anchors the governed identity on a Mnemom API key instead
|
|
10
|
+
* (`x-mnemom-api-key`), which this module resolves — env, then the 0600 store, then a
|
|
11
|
+
* freshly minted gateway-only key — and the agent is created in that key's org on the
|
|
12
|
+
* first turn, so there is no CLI-side birth or claim.
|
|
38
13
|
*
|
|
39
14
|
* Remote Control is internal-only (see subscriptionShape); the context-handoff tool
|
|
40
15
|
* authenticates with an Anthropic key, so it is off on a subscription launch.
|
|
@@ -47,133 +22,26 @@ export type AgentAuth = "api-key" | "subscription" | "console";
|
|
|
47
22
|
export declare const AGENT_AUTHS: readonly AgentAuth[];
|
|
48
23
|
/** Env override for the setting (the launcher.toml `auth` key folds into it). */
|
|
49
24
|
export declare const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
|
|
50
|
-
/** Env override for the Mnemom key
|
|
25
|
+
/** Env override for the Mnemom key a subscription launch sends. */
|
|
51
26
|
export declare const MNEMOM_KEY_ENV = "MNEMOM_AGENT_MNEMOM_KEY";
|
|
52
|
-
/**
|
|
53
|
-
* Legacy field in ~/.mnemom/launcher.json (0600): the single minted key, from before keys were
|
|
54
|
-
* stored per org. Read as the no-org slot; retired when that slot is written.
|
|
55
|
-
*/
|
|
27
|
+
/** Field in ~/.mnemom/launcher.json (0600) holding the minted key. */
|
|
56
28
|
export declare const MNEMOM_KEY_STORE_FIELD = "mnemom_api_key";
|
|
57
|
-
/**
|
|
58
|
-
* Field in ~/.mnemom/launcher.json (0600): `{ <active org id | "_none">: { key, key_id, org_id } }`
|
|
59
|
-
* — one slot per `mnemom org use` setting; `org_id` is the org the key was minted for.
|
|
60
|
-
*/
|
|
61
|
-
export declare const MNEMOM_KEYS_BY_ORG_FIELD = "mnemom_api_keys";
|
|
62
29
|
export declare function coerceAgentAuth(raw: string): AgentAuth;
|
|
63
30
|
/** `--auth` flag > MNEMOM_AGENT_AUTH (launcher.toml folds in here) > api-key. */
|
|
64
31
|
export declare function resolveAgentAuth(flag: string | undefined, env: Record<string, string | undefined>): AgentAuth;
|
|
65
|
-
/** A key in the 0600 store: the secret, and its API key_id when this CLI minted it. */
|
|
66
|
-
export interface StoredMnemomKey {
|
|
67
|
-
key: string;
|
|
68
|
-
/** Absent on a key saved before key ids were stored. */
|
|
69
|
-
keyId?: string;
|
|
70
|
-
/** The launch org this key was minted for. Absent on a key saved before orgs were stored. */
|
|
71
|
-
orgId?: string;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Is a Mnemom key live? `invalid` only when it is PROVEN dead (checkMnemomKey); anything unproven
|
|
75
|
-
* (offline, slow, no session, a gateway-only key the API cannot introspect) is `unknown` — a
|
|
76
|
-
* launch then proceeds with the key, and the gateway refuses a dead one with a clear message.
|
|
77
|
-
*/
|
|
78
|
-
export type MnemomKeyCheck = "valid" | "invalid" | "unknown";
|
|
79
32
|
export interface MnemomKeyDeps {
|
|
80
33
|
env: Record<string, string | undefined>;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
activeOrgId: string | null;
|
|
86
|
-
/**
|
|
87
|
-
* The org this launch runs in — the active org, else the caller's personal org (the org the
|
|
88
|
-
* launcher claims the agent into) — or null when unknown. Keys are minted for it; with none
|
|
89
|
-
* known, a launch that needs a new key refuses (it has no org to mint one for).
|
|
90
|
-
*/
|
|
91
|
-
launchOrgId: string | null;
|
|
92
|
-
readStored(orgId: string | null): StoredMnemomKey | undefined;
|
|
93
|
-
/**
|
|
94
|
-
* Mint an org-owned key with scopes ["gateway"] for `orgId`. Throws when the API refuses (a
|
|
95
|
-
* refusal carries `status`/`effectiveStatus` 403). Any `orgId` on the result is ignored: the
|
|
96
|
-
* store records the org the key was minted FOR.
|
|
97
|
-
*/
|
|
98
|
-
mint(orgId: string): Promise<StoredMnemomKey>;
|
|
99
|
-
store(orgId: string | null, key: StoredMnemomKey): void;
|
|
100
|
-
/** checkMnemomKey over the real API, with short timeouts. Never throws. */
|
|
101
|
-
check(key: string, keyId: string | undefined): Promise<MnemomKeyCheck>;
|
|
102
|
-
/** Revoke a key this CLI minted, by key_id (needs a session). May throw; callers ignore it. */
|
|
103
|
-
revoke(keyId: string): Promise<void>;
|
|
34
|
+
readStored(): string | undefined;
|
|
35
|
+
/** Mint a gateway-only personal key for this machine; returns the secret. */
|
|
36
|
+
mint(): Promise<string>;
|
|
37
|
+
store(key: string): void;
|
|
104
38
|
say(line: string): void;
|
|
105
39
|
}
|
|
106
|
-
/** A Mnemom key that is already on this machine, and where it came from. */
|
|
107
|
-
export interface LocalMnemomKey {
|
|
108
|
-
key: string;
|
|
109
|
-
source: "env" | "store";
|
|
110
|
-
/** The stored key's API key_id, when known. */
|
|
111
|
-
keyId?: string;
|
|
112
|
-
/** The org the stored key was minted for, when known. */
|
|
113
|
-
orgId?: string;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* The Mnemom key already on this machine — MNEMOM_AGENT_MNEMOM_KEY, else the key stored for the
|
|
117
|
-
* active org — or null. Never mints. Shared by the launch (which mints on null), `mnemom agent
|
|
118
|
-
* doctor` (which reports the source) and the goal commands (which send the key the launch sent).
|
|
119
|
-
*/
|
|
120
|
-
export declare function readLocalMnemomKey(deps: Pick<MnemomKeyDeps, "env" | "readStored" | "activeOrgId">): LocalMnemomKey | null;
|
|
121
|
-
/** The I/O behind checkMnemomKey (lib/api.ts apiAcceptsMnemomKey / listOwnApiKeysForCheck). */
|
|
122
|
-
export interface MnemomKeyCheckIo {
|
|
123
|
-
/** true = the API accepted the key on its own (whoami 200); false = it refused it (401/403). */
|
|
124
|
-
apiAccepts(key: string): Promise<boolean>;
|
|
125
|
-
/** The signed-in account's active keys (incl. org keys it minted), or null with no session. */
|
|
126
|
-
listOwnKeys(): Promise<Array<{
|
|
127
|
-
key_id: string;
|
|
128
|
-
}> | null>;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Is `key` a live Mnemom key? A key counts as dead (`invalid`) only when that is PROVEN;
|
|
132
|
-
* anything unproven proceeds, and the gateway refuses a dead key with a clear message.
|
|
133
|
-
*
|
|
134
|
-
* - A key whose key_id we know (this CLI minted and saved it): the signed-in account's active
|
|
135
|
-
* keys — which include the org keys it minted — either list that key_id (`valid`) or do not:
|
|
136
|
-
* the key was revoked or rotated (`invalid`). No usable session ⇒ `unknown`.
|
|
137
|
-
* - Any other key (MNEMOM_AGENT_MNEMOM_KEY, a legacy saved key): whoami with the key itself.
|
|
138
|
-
* 200 ⇒ `valid`. A refusal proves nothing — the API refuses every key without an `api:*`
|
|
139
|
-
* scope (ADR-049), so a live gateway-only key is refused exactly like a dead one ⇒ `unknown`.
|
|
140
|
-
* Key prefixes are never matched: an 8-character prefix collides across keys.
|
|
141
|
-
*
|
|
142
|
-
* Any error or timeout ⇒ `unknown`.
|
|
143
|
-
*/
|
|
144
|
-
export declare function checkMnemomKey(key: string, keyId: string | undefined, io: MnemomKeyCheckIo): Promise<MnemomKeyCheck>;
|
|
145
|
-
/**
|
|
146
|
-
* The Mnemom key every launch sends — api-key, console and subscription alike: the local key
|
|
147
|
-
* (readLocalMnemomKey); else mint one (scopes ["gateway"], org-owned, for the launch's org) and
|
|
148
|
-
* store it. The key is never printed.
|
|
149
|
-
*
|
|
150
|
-
* - MNEMOM_AGENT_MNEMOM_KEY is used as given: not checked here (nothing could prove it dead —
|
|
151
|
-
* see checkMnemomKey), never replaced, never revoked. The gateway refuses a dead one.
|
|
152
|
-
* - A saved key minted for another org than the launch's (or saved before orgs were recorded,
|
|
153
|
-
* e.g. the legacy single-field key) is replaced by one for the launch's org, unchecked.
|
|
154
|
-
* - A saved key checked once (`deps.check`) and proven dead is replaced by a fresh one.
|
|
155
|
-
* - Replacing a saved key this CLI minted revokes the old one, best-effort.
|
|
156
|
-
*
|
|
157
|
-
* THROWS when no key can be had (no launch org, or the mint is refused or fails): the gateway
|
|
158
|
-
* would refuse the session, so the launch must not start.
|
|
159
|
-
*/
|
|
160
|
-
export declare function resolveLaunchMnemomKey(deps: MnemomKeyDeps): Promise<string>;
|
|
161
|
-
/** Did the API refuse the mint (403: an API that predates member org keys, or a viewer/auditor)? */
|
|
162
|
-
export declare function isMintRefusal(err: unknown): boolean;
|
|
163
|
-
/** Store slot for a key minted with no active org (for the caller's personal org). */
|
|
164
|
-
export declare const NO_ORG_SLOT = "_none";
|
|
165
|
-
/**
|
|
166
|
-
* The key stored for `orgId` in the store document: `mnemom_api_keys[<org id | _none>]`. The
|
|
167
|
-
* pre-per-org field `mnemom_api_key` (a bare string, minted personal) still serves the no-org
|
|
168
|
-
* slot until that slot is written. CR/LF stripped; anything malformed reads as no key.
|
|
169
|
-
*/
|
|
170
|
-
export declare function readStoredMnemomKey(data: Record<string, unknown>, orgId: string | null): StoredMnemomKey | undefined;
|
|
171
40
|
/**
|
|
172
|
-
* The
|
|
173
|
-
*
|
|
174
|
-
* removes it), so a stale legacy key can never shadow the new one.
|
|
41
|
+
* The Mnemom key a subscription launch sends: MNEMOM_AGENT_MNEMOM_KEY, else the stored
|
|
42
|
+
* key, else mint one (gateway scope only) and store it. The key is never printed.
|
|
175
43
|
*/
|
|
176
|
-
export declare function
|
|
44
|
+
export declare function resolveSubscriptionMnemomKey(deps: MnemomKeyDeps): Promise<string>;
|
|
177
45
|
/**
|
|
178
46
|
* Remote Control on a subscription login is open to accounts entitled to Remote Control
|
|
179
47
|
* (internal Mnemom accounts): the RC proxy then forwards the user's own OAuth bearer to the
|
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `mnemom agent`
|
|
3
|
-
*
|
|
2
|
+
* `mnemom agent` with a Claude subscription login (`auth = subscription`) — Team and
|
|
3
|
+
* Enterprise plans only; the gateway checks the plan with Anthropic and refuses anything
|
|
4
|
+
* else (gateway/src/subscription-auth.ts).
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* caller's personal org, i.e. the org the launcher claims the agent into — as an org-owned key
|
|
13
|
-
* with scopes exactly ["gateway"] (POST /v1/orgs/:id/api-keys; any owner/admin/member may mint
|
|
14
|
-
* one), so the gateway's agent-traffic org check (#2289: the agent's org must be the key's org)
|
|
15
|
-
* passes. There is no personal-key fallback: a personal key has no org, so the gateway would
|
|
16
|
-
* refuse every governed turn with it. When the mint is refused (an API that predates member org
|
|
17
|
-
* keys, or a viewer/auditor role) the launch stops with guidance — ask an org owner/admin for a
|
|
18
|
-
* gateway-scoped org key, or set MNEMOM_AGENT_MNEMOM_KEY to one. Each saved key records the org
|
|
19
|
-
* it was minted for; a launch in another org mints and saves a key for that org.
|
|
20
|
-
*
|
|
21
|
-
* When the launcher replaces a key it minted and saved (proven dead, minted for another org, or
|
|
22
|
-
* the legacy single-field key), it revokes the old one best-effort. An env-provided key is never
|
|
23
|
-
* replaced and never revoked.
|
|
24
|
-
*
|
|
25
|
-
* Birth org: the launcher's gateway probe/birth call carries the key AND a no-op
|
|
26
|
-
* `x-mnemom-session-config`, which makes it Mnemom agent traffic: the gateway verifies the key
|
|
27
|
-
* and births a new agent in the key's org. The launcher's claim step then records the claim in
|
|
28
|
-
* that same org.
|
|
29
|
-
*
|
|
30
|
-
* With a Claude subscription login (`auth = subscription`) — Team and Enterprise plans only;
|
|
31
|
-
* the gateway checks the plan with Anthropic and refuses anything else
|
|
32
|
-
* (gateway/src/subscription-auth.ts) — Claude Code authenticates to the gateway with its own
|
|
33
|
-
* claude.ai OAuth login: the launch sets ANTHROPIC_BASE_URL and leaves ANTHROPIC_API_KEY /
|
|
34
|
-
* ANTHROPIC_AUTH_TOKEN unset, so Claude Code sends `Authorization: Bearer <its login>`. No
|
|
35
|
-
* Anthropic key is involved; the gateway anchors the governed identity on the Mnemom key and
|
|
36
|
-
* creates the agent in that key's org on the first turn, so there is no CLI-side birth or
|
|
37
|
-
* claim.
|
|
6
|
+
* Claude Code then authenticates to the gateway with its own claude.ai OAuth login: the
|
|
7
|
+
* launch sets ANTHROPIC_BASE_URL and leaves ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN unset,
|
|
8
|
+
* so Claude Code sends `Authorization: Bearer <its login>`. No Anthropic key is involved.
|
|
9
|
+
* The gateway anchors the governed identity on a Mnemom API key instead
|
|
10
|
+
* (`x-mnemom-api-key`), which this module resolves — env, then the 0600 store, then a
|
|
11
|
+
* freshly minted gateway-only key — and the agent is created in that key's org on the
|
|
12
|
+
* first turn, so there is no CLI-side birth or claim.
|
|
38
13
|
*
|
|
39
14
|
* Remote Control is internal-only (see subscriptionShape); the context-handoff tool
|
|
40
15
|
* authenticates with an Anthropic key, so it is off on a subscription launch.
|
|
@@ -44,18 +19,10 @@
|
|
|
44
19
|
export const AGENT_AUTHS = ["api-key", "subscription", "console"];
|
|
45
20
|
/** Env override for the setting (the launcher.toml `auth` key folds into it). */
|
|
46
21
|
export const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
|
|
47
|
-
/** Env override for the Mnemom key
|
|
22
|
+
/** Env override for the Mnemom key a subscription launch sends. */
|
|
48
23
|
export const MNEMOM_KEY_ENV = "MNEMOM_AGENT_MNEMOM_KEY";
|
|
49
|
-
/**
|
|
50
|
-
* Legacy field in ~/.mnemom/launcher.json (0600): the single minted key, from before keys were
|
|
51
|
-
* stored per org. Read as the no-org slot; retired when that slot is written.
|
|
52
|
-
*/
|
|
24
|
+
/** Field in ~/.mnemom/launcher.json (0600) holding the minted key. */
|
|
53
25
|
export const MNEMOM_KEY_STORE_FIELD = "mnemom_api_key";
|
|
54
|
-
/**
|
|
55
|
-
* Field in ~/.mnemom/launcher.json (0600): `{ <active org id | "_none">: { key, key_id, org_id } }`
|
|
56
|
-
* — one slot per `mnemom org use` setting; `org_id` is the org the key was minted for.
|
|
57
|
-
*/
|
|
58
|
-
export const MNEMOM_KEYS_BY_ORG_FIELD = "mnemom_api_keys";
|
|
59
26
|
/**
|
|
60
27
|
* The key is spliced into the newline-joined ANTHROPIC_CUSTOM_HEADERS block, so an embedded
|
|
61
28
|
* CR/LF (a multi-line paste, a bad env value) would inject extra header lines.
|
|
@@ -77,188 +44,35 @@ export function resolveAgentAuth(flag, env) {
|
|
|
77
44
|
return fromEnv ? coerceAgentAuth(fromEnv) : "api-key";
|
|
78
45
|
}
|
|
79
46
|
/**
|
|
80
|
-
* The Mnemom key
|
|
81
|
-
*
|
|
82
|
-
* doctor` (which reports the source) and the goal commands (which send the key the launch sent).
|
|
47
|
+
* The Mnemom key a subscription launch sends: MNEMOM_AGENT_MNEMOM_KEY, else the stored
|
|
48
|
+
* key, else mint one (gateway scope only) and store it. The key is never printed.
|
|
83
49
|
*/
|
|
84
|
-
export function
|
|
50
|
+
export async function resolveSubscriptionMnemomKey(deps) {
|
|
85
51
|
const fromEnv = headerSafe(deps.env[MNEMOM_KEY_ENV] ?? "");
|
|
86
52
|
if (fromEnv)
|
|
87
|
-
return
|
|
88
|
-
const stored = deps.readStored(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return null;
|
|
92
|
-
const local = { key, source: "store" };
|
|
93
|
-
if (stored?.keyId)
|
|
94
|
-
local.keyId = stored.keyId;
|
|
95
|
-
if (stored?.orgId)
|
|
96
|
-
local.orgId = stored.orgId;
|
|
97
|
-
return local;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Is `key` a live Mnemom key? A key counts as dead (`invalid`) only when that is PROVEN;
|
|
101
|
-
* anything unproven proceeds, and the gateway refuses a dead key with a clear message.
|
|
102
|
-
*
|
|
103
|
-
* - A key whose key_id we know (this CLI minted and saved it): the signed-in account's active
|
|
104
|
-
* keys — which include the org keys it minted — either list that key_id (`valid`) or do not:
|
|
105
|
-
* the key was revoked or rotated (`invalid`). No usable session ⇒ `unknown`.
|
|
106
|
-
* - Any other key (MNEMOM_AGENT_MNEMOM_KEY, a legacy saved key): whoami with the key itself.
|
|
107
|
-
* 200 ⇒ `valid`. A refusal proves nothing — the API refuses every key without an `api:*`
|
|
108
|
-
* scope (ADR-049), so a live gateway-only key is refused exactly like a dead one ⇒ `unknown`.
|
|
109
|
-
* Key prefixes are never matched: an 8-character prefix collides across keys.
|
|
110
|
-
*
|
|
111
|
-
* Any error or timeout ⇒ `unknown`.
|
|
112
|
-
*/
|
|
113
|
-
export async function checkMnemomKey(key, keyId, io) {
|
|
114
|
-
try {
|
|
115
|
-
if (!keyId)
|
|
116
|
-
return (await io.apiAccepts(key)) ? "valid" : "unknown";
|
|
117
|
-
const own = await io.listOwnKeys();
|
|
118
|
-
if (own === null)
|
|
119
|
-
return "unknown";
|
|
120
|
-
return own.some((k) => k.key_id === keyId) ? "valid" : "invalid";
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
return "unknown";
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* The Mnemom key every launch sends — api-key, console and subscription alike: the local key
|
|
128
|
-
* (readLocalMnemomKey); else mint one (scopes ["gateway"], org-owned, for the launch's org) and
|
|
129
|
-
* store it. The key is never printed.
|
|
130
|
-
*
|
|
131
|
-
* - MNEMOM_AGENT_MNEMOM_KEY is used as given: not checked here (nothing could prove it dead —
|
|
132
|
-
* see checkMnemomKey), never replaced, never revoked. The gateway refuses a dead one.
|
|
133
|
-
* - A saved key minted for another org than the launch's (or saved before orgs were recorded,
|
|
134
|
-
* e.g. the legacy single-field key) is replaced by one for the launch's org, unchecked.
|
|
135
|
-
* - A saved key checked once (`deps.check`) and proven dead is replaced by a fresh one.
|
|
136
|
-
* - Replacing a saved key this CLI minted revokes the old one, best-effort.
|
|
137
|
-
*
|
|
138
|
-
* THROWS when no key can be had (no launch org, or the mint is refused or fails): the gateway
|
|
139
|
-
* would refuse the session, so the launch must not start.
|
|
140
|
-
*/
|
|
141
|
-
export async function resolveLaunchMnemomKey(deps) {
|
|
142
|
-
const local = readLocalMnemomKey(deps);
|
|
143
|
-
if (!local)
|
|
144
|
-
return mintAndStore(deps, null);
|
|
145
|
-
if (local.source === "env")
|
|
146
|
-
return local.key;
|
|
147
|
-
if (deps.launchOrgId && local.orgId !== deps.launchOrgId) {
|
|
148
|
-
deps.say("mnemom agent: the saved Mnemom API key was minted for another org — minting one for this launch's org.");
|
|
149
|
-
return mintAndStore(deps, local);
|
|
150
|
-
}
|
|
151
|
-
if ((await deps.check(local.key, local.keyId)) !== "invalid")
|
|
152
|
-
return local.key;
|
|
153
|
-
deps.say("mnemom agent: the saved Mnemom API key is no longer active (revoked or rotated) — minting a new one.");
|
|
154
|
-
return mintAndStore(deps, local);
|
|
155
|
-
}
|
|
156
|
-
/** Did the API refuse the mint (403: an API that predates member org keys, or a viewer/auditor)? */
|
|
157
|
-
export function isMintRefusal(err) {
|
|
158
|
-
if (!err || typeof err !== "object")
|
|
159
|
-
return false;
|
|
160
|
-
const { status, effectiveStatus } = err;
|
|
161
|
-
return (effectiveStatus ?? status) === 403;
|
|
162
|
-
}
|
|
163
|
-
const ORG_KEY_GUIDANCE = ` Ask an org owner or admin for an org API key with the \`gateway\` scope for this org, and set ${MNEMOM_KEY_ENV} to it.`;
|
|
164
|
-
/**
|
|
165
|
-
* Mint and save a key for the launch's org, then revoke `replaced` — the saved key it supersedes
|
|
166
|
-
* — best-effort: only once the new key is saved (the store never points at a revoked key) and
|
|
167
|
-
* only when its key_id is known (a legacy saved key has none; it is left as is).
|
|
168
|
-
*/
|
|
169
|
-
async function mintAndStore(deps, replaced) {
|
|
170
|
-
const orgId = deps.launchOrgId;
|
|
171
|
-
if (!orgId) {
|
|
172
|
-
throw new Error("mnemom agent: every launch needs a Mnemom API key for the org it runs in, and this launch's org is unknown.\n" +
|
|
173
|
-
` Pick one with \`mnemom org use <org>\`, or:\n${ORG_KEY_GUIDANCE}`);
|
|
174
|
-
}
|
|
53
|
+
return fromEnv;
|
|
54
|
+
const stored = headerSafe(deps.readStored() ?? "");
|
|
55
|
+
if (stored)
|
|
56
|
+
return stored;
|
|
175
57
|
let minted;
|
|
176
58
|
try {
|
|
177
|
-
minted = await deps.mint(
|
|
59
|
+
minted = headerSafe(await deps.mint());
|
|
178
60
|
}
|
|
179
61
|
catch (err) {
|
|
180
62
|
const msg = err instanceof Error ? err.message : String(err);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
: `minting one for this org failed (${msg})`;
|
|
184
|
-
throw new Error(`mnemom agent: every launch needs a Mnemom API key (the gateway refuses governed traffic without one) and ${why}.\n${ORG_KEY_GUIDANCE}`, { cause: err });
|
|
63
|
+
throw new Error(`mnemom agent: subscription login needs a Mnemom API key and minting one failed (${msg}).\n` +
|
|
64
|
+
` Create one with \`mnemom api-key create --name mnemom-agent --scopes gateway\` and set ${MNEMOM_KEY_ENV}.`, { cause: err });
|
|
185
65
|
}
|
|
186
|
-
|
|
187
|
-
if (!key)
|
|
66
|
+
if (!minted)
|
|
188
67
|
throw new Error("mnemom agent: the API returned an empty Mnemom key.");
|
|
189
|
-
const saved = { key, orgId };
|
|
190
|
-
if (minted.keyId)
|
|
191
|
-
saved.keyId = minted.keyId;
|
|
192
68
|
try {
|
|
193
|
-
deps.store(
|
|
69
|
+
deps.store(minted);
|
|
70
|
+
deps.say("mnemom agent: minted a gateway-only Mnemom API key for subscription login (saved to ~/.mnemom/launcher.json, 0600).");
|
|
194
71
|
}
|
|
195
72
|
catch {
|
|
196
73
|
deps.say("mnemom agent: minted a gateway-only Mnemom API key for this launch but could not save it — the next launch mints another.");
|
|
197
|
-
return key;
|
|
198
|
-
}
|
|
199
|
-
deps.say("mnemom agent: minted a gateway-only Mnemom API key for this org (saved to ~/.mnemom/launcher.json, 0600).");
|
|
200
|
-
if (replaced?.keyId) {
|
|
201
|
-
try {
|
|
202
|
-
await deps.revoke(replaced.keyId);
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
// Best-effort: the old key may already be revoked, or not this account's to revoke.
|
|
206
|
-
}
|
|
207
74
|
}
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
// ── The 0600 store's shape (~/.mnemom/launcher.json) ─────────────────────────
|
|
211
|
-
/** Store slot for a key minted with no active org (for the caller's personal org). */
|
|
212
|
-
export const NO_ORG_SLOT = "_none";
|
|
213
|
-
function slotOf(orgId) {
|
|
214
|
-
return orgId || NO_ORG_SLOT;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* The key stored for `orgId` in the store document: `mnemom_api_keys[<org id | _none>]`. The
|
|
218
|
-
* pre-per-org field `mnemom_api_key` (a bare string, minted personal) still serves the no-org
|
|
219
|
-
* slot until that slot is written. CR/LF stripped; anything malformed reads as no key.
|
|
220
|
-
*/
|
|
221
|
-
export function readStoredMnemomKey(data, orgId) {
|
|
222
|
-
const byOrg = data[MNEMOM_KEYS_BY_ORG_FIELD];
|
|
223
|
-
const entry = byOrg && typeof byOrg === "object" && !Array.isArray(byOrg)
|
|
224
|
-
? byOrg[slotOf(orgId)]
|
|
225
|
-
: undefined;
|
|
226
|
-
if (entry && typeof entry === "object") {
|
|
227
|
-
const { key, key_id, org_id } = entry;
|
|
228
|
-
if (typeof key !== "string")
|
|
229
|
-
return undefined;
|
|
230
|
-
const stored = { key: headerSafe(key) };
|
|
231
|
-
if (typeof key_id === "string" && key_id)
|
|
232
|
-
stored.keyId = key_id;
|
|
233
|
-
if (typeof org_id === "string" && org_id)
|
|
234
|
-
stored.orgId = org_id;
|
|
235
|
-
return stored;
|
|
236
|
-
}
|
|
237
|
-
const legacy = data[MNEMOM_KEY_STORE_FIELD];
|
|
238
|
-
if (orgId === null && typeof legacy === "string")
|
|
239
|
-
return { key: headerSafe(legacy) };
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* The mergeStore patch that saves `key` for `orgId` — replacing that org's key, keeping every
|
|
244
|
-
* other org's. Writing the no-org slot retires the legacy `mnemom_api_key` field (a null value
|
|
245
|
-
* removes it), so a stale legacy key can never shadow the new one.
|
|
246
|
-
*/
|
|
247
|
-
export function storePatchForMnemomKey(data, orgId, key) {
|
|
248
|
-
const prev = data[MNEMOM_KEYS_BY_ORG_FIELD];
|
|
249
|
-
const byOrg = prev && typeof prev === "object" && !Array.isArray(prev)
|
|
250
|
-
? { ...prev }
|
|
251
|
-
: {};
|
|
252
|
-
const entry = { key: key.key };
|
|
253
|
-
if (key.keyId)
|
|
254
|
-
entry.key_id = key.keyId;
|
|
255
|
-
if (key.orgId)
|
|
256
|
-
entry.org_id = key.orgId;
|
|
257
|
-
byOrg[slotOf(orgId)] = entry;
|
|
258
|
-
const patch = { [MNEMOM_KEYS_BY_ORG_FIELD]: byOrg };
|
|
259
|
-
if (orgId === null)
|
|
260
|
-
patch[MNEMOM_KEY_STORE_FIELD] = null;
|
|
261
|
-
return patch;
|
|
75
|
+
return minted;
|
|
262
76
|
}
|
|
263
77
|
/**
|
|
264
78
|
* Remote Control on a subscription login is open to accounts entitled to Remote Control
|
|
@@ -285,9 +99,8 @@ export function subscriptionChildEnv(base, door, headers) {
|
|
|
285
99
|
const env = { ...base };
|
|
286
100
|
delete env.ANTHROPIC_API_KEY;
|
|
287
101
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
288
|
-
// The key already rides in the headers; the child process has no other use for it.
|
|
289
|
-
|
|
290
|
-
Reflect.deleteProperty(env, MNEMOM_KEY_ENV);
|
|
102
|
+
// The key already rides in the headers; the child process has no other use for it.
|
|
103
|
+
delete env.MNEMOM_AGENT_MNEMOM_KEY;
|
|
291
104
|
env.ANTHROPIC_BASE_URL = door;
|
|
292
105
|
env.ANTHROPIC_CUSTOM_HEADERS = headers;
|
|
293
106
|
return env;
|
|
@@ -25,11 +25,8 @@
|
|
|
25
25
|
// request time instead. A terminal launch names `ANTHROPIC_CUSTOM_HEADERS` (parsed for its
|
|
26
26
|
// `x-api-key:` line); Remote Control names a dedicated `MNEMOM_HANDOFF_KEY` its launcher sets
|
|
27
27
|
// directly, since RC's child env strips every ANTHROPIC_* var (see `HandoffAnthropicKeySource`
|
|
28
|
-
// in cli/src/lib/agent.ts for the full rationale).
|
|
29
|
-
//
|
|
30
|
-
// `MNEMOM_HANDOFF_MNEMOM_KEY_ENV_NAME`: the `x-mnemom-api-key:` line of `ANTHROPIC_CUSTOM_HEADERS`
|
|
31
|
-
// on a terminal launch, `MNEMOM_HANDOFF_MNEMOM_KEY` under Remote Control. This process never logs
|
|
32
|
-
// any of these vars' names or values.
|
|
28
|
+
// in cli/src/lib/agent.ts for the full rationale). This process never logs either var's name or
|
|
29
|
+
// value.
|
|
33
30
|
|
|
34
31
|
import { createInterface } from "node:readline";
|
|
35
32
|
import { pathToFileURL } from "node:url";
|
|
@@ -70,33 +67,29 @@ const REQUIRED_ENV = [
|
|
|
70
67
|
];
|
|
71
68
|
|
|
72
69
|
/**
|
|
73
|
-
* Recover
|
|
74
|
-
* value written into the `--mcp-config` file). `ANTHROPIC_CUSTOM_HEADERS` is a
|
|
75
|
-
* BLOCK (`buildCustomHeaders`, cli/src/lib/agent.ts)
|
|
76
|
-
* `x-
|
|
77
|
-
*
|
|
70
|
+
* Recover the Anthropic key from an env var NAME this process itself inherited from its parent
|
|
71
|
+
* (never a value written into the `--mcp-config` file). `ANTHROPIC_CUSTOM_HEADERS` is a
|
|
72
|
+
* multi-line header BLOCK (`buildCustomHeaders`, cli/src/lib/agent.ts) whose first line is
|
|
73
|
+
* `x-api-key: <key>` — extract just that. Any other name is treated as carrying the key value
|
|
74
|
+
* directly.
|
|
78
75
|
* @param {string} name
|
|
79
76
|
* @param {NodeJS.ProcessEnv} env
|
|
80
|
-
* @param {RegExp} header the header line to read out of a header block
|
|
81
77
|
* @returns {string} the key, or "" if the named var is absent/unusable.
|
|
82
78
|
*/
|
|
83
|
-
function resolveKeyFromEnvName(name, env
|
|
79
|
+
function resolveKeyFromEnvName(name, env) {
|
|
84
80
|
const value = env[name] ?? "";
|
|
85
81
|
if (name === "ANTHROPIC_CUSTOM_HEADERS") {
|
|
86
|
-
const m = value.match(
|
|
82
|
+
const m = value.match(/^x-api-key:\s*(.+)$/im);
|
|
87
83
|
return m ? m[1].trim() : "";
|
|
88
84
|
}
|
|
89
85
|
return value.trim();
|
|
90
86
|
}
|
|
91
87
|
|
|
92
|
-
const ANTHROPIC_KEY_LINE = /^x-api-key:\s*(.+)$/im;
|
|
93
|
-
const MNEMOM_KEY_LINE = /^x-mnemom-api-key:\s*(.+)$/im;
|
|
94
|
-
|
|
95
88
|
/**
|
|
96
89
|
* Read the tool's config from env. Throws (message never contains the key value) when a
|
|
97
90
|
* required var is missing, or when the named key-source var resolves to no usable key.
|
|
98
91
|
* @param {NodeJS.ProcessEnv} env
|
|
99
|
-
* @returns {{gatewayHost: string, anthropicKey: string,
|
|
92
|
+
* @returns {{gatewayHost: string, anthropicKey: string, agent: string, conversationId: string, sessionConfigB64?: string}}
|
|
100
93
|
*/
|
|
101
94
|
export function configFromEnv(env) {
|
|
102
95
|
const missing = REQUIRED_ENV.filter((k) => !(env[k] ?? "").trim());
|
|
@@ -104,30 +97,16 @@ export function configFromEnv(env) {
|
|
|
104
97
|
throw new Error(`mnemom-handoff: missing required env var(s): ${missing.join(", ")}`);
|
|
105
98
|
}
|
|
106
99
|
const keyEnvName = env.MNEMOM_HANDOFF_ANTHROPIC_KEY_ENV_NAME.trim();
|
|
107
|
-
const anthropicKey = resolveKeyFromEnvName(keyEnvName, env
|
|
100
|
+
const anthropicKey = resolveKeyFromEnvName(keyEnvName, env);
|
|
108
101
|
if (!anthropicKey) {
|
|
109
102
|
throw new Error(
|
|
110
103
|
`mnemom-handoff: MNEMOM_HANDOFF_ANTHROPIC_KEY_ENV_NAME names '${keyEnvName}', but this ` +
|
|
111
104
|
"process's own inherited environment has no usable value for it.",
|
|
112
105
|
);
|
|
113
106
|
}
|
|
114
|
-
// The Mnemom key the gateway requires on every `mnemom agent` request. A launcher that names
|
|
115
|
-
// no source sends none; a named source that resolves to nothing is a config error.
|
|
116
|
-
const mnemomKeyEnvName = (env.MNEMOM_HANDOFF_MNEMOM_KEY_ENV_NAME ?? "").trim();
|
|
117
|
-
let mnemomKey;
|
|
118
|
-
if (mnemomKeyEnvName) {
|
|
119
|
-
mnemomKey = resolveKeyFromEnvName(mnemomKeyEnvName, env, MNEMOM_KEY_LINE);
|
|
120
|
-
if (!mnemomKey) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`mnemom-handoff: MNEMOM_HANDOFF_MNEMOM_KEY_ENV_NAME names '${mnemomKeyEnvName}', but this ` +
|
|
123
|
-
"process's own inherited environment has no usable Mnemom key in it.",
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
107
|
return {
|
|
128
108
|
gatewayHost: env.MNEMOM_HANDOFF_GATEWAY_HOST.trim(),
|
|
129
109
|
anthropicKey,
|
|
130
|
-
mnemomKey,
|
|
131
110
|
agent: env.MNEMOM_HANDOFF_AGENT.trim(),
|
|
132
111
|
conversationId: env.MNEMOM_HANDOFF_CONVERSATION_ID.trim(),
|
|
133
112
|
sessionConfigB64: env.MNEMOM_HANDOFF_SESSION_CONFIG?.trim() || undefined,
|
|
@@ -136,7 +115,7 @@ export function configFromEnv(env) {
|
|
|
136
115
|
|
|
137
116
|
/**
|
|
138
117
|
* Build the fetch call. Exported so a test can assert the wire shape without mocking fetch.
|
|
139
|
-
* @param {{gatewayHost: string, anthropicKey: string,
|
|
118
|
+
* @param {{gatewayHost: string, anthropicKey: string, agent: string, conversationId: string, sessionConfigB64?: string}} cfg
|
|
140
119
|
* @param {string} note
|
|
141
120
|
*/
|
|
142
121
|
export function buildHandoffRequest(cfg, note) {
|
|
@@ -145,7 +124,6 @@ export function buildHandoffRequest(cfg, note) {
|
|
|
145
124
|
"x-api-key": cfg.anthropicKey,
|
|
146
125
|
"x-mnemom-agent": cfg.agent,
|
|
147
126
|
};
|
|
148
|
-
if (cfg.mnemomKey) headers["x-mnemom-api-key"] = cfg.mnemomKey;
|
|
149
127
|
if (cfg.sessionConfigB64) headers["x-mnemom-session-config"] = cfg.sessionConfigB64;
|
|
150
128
|
return {
|
|
151
129
|
url: `${cfg.gatewayHost.replace(/\/+$/, "")}${HANDOFF_PATH}`,
|
|
@@ -172,7 +150,7 @@ function extractGatewayError(body) {
|
|
|
172
150
|
* Call the route and render the result. NEVER throws: a network failure, a non-2xx status, or
|
|
173
151
|
* an unparseable body all come back as `{ok:false, message}` for the caller to surface as an
|
|
174
152
|
* MCP tool error — the tool must never crash the server or the session over a bad response.
|
|
175
|
-
* @param {{gatewayHost: string, anthropicKey: string,
|
|
153
|
+
* @param {{gatewayHost: string, anthropicKey: string, agent: string, conversationId: string, sessionConfigB64?: string}} cfg
|
|
176
154
|
* @param {string} note
|
|
177
155
|
* @param {typeof fetch} fetchImpl
|
|
178
156
|
*/
|
package/dist/rc-proxy.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// byte-for-byte (telemetry, Datadog, MCP servers…).
|
|
20
20
|
// * Inside api.anthropic.com, only `POST /v1/messages[?…]` is rewritten to the Mnemom
|
|
21
21
|
// gateway door: the real Anthropic key rides as `x-api-key` (API-key billing), plus
|
|
22
|
-
// `x-mnemom-agent`, `x-mnemom-conversation-id`, `x-mnemom-api-key
|
|
22
|
+
// `x-mnemom-agent`, `x-mnemom-conversation-id`, and optionally `x-mnemom-api-key`,
|
|
23
23
|
// `x-mnemom-provider`, `x-mnemom-egress-key`, `x-mnemom-contract` / `x-mnemom-goal-mode` — the exact header set
|
|
24
24
|
// the non-RC launch sends via ANTHROPIC_CUSTOM_HEADERS. The OAuth `authorization` bearer
|
|
25
25
|
// is STRIPPED on that leg (it never leaves the machine) and `oauth-*` tokens are removed
|
|
@@ -51,11 +51,8 @@
|
|
|
51
51
|
// (diagnostic only: it forwards continue-deltas the API rejects).
|
|
52
52
|
//
|
|
53
53
|
// SECRETS
|
|
54
|
-
// Arrive on stdin as three newline-terminated lines: Anthropic key, Mnemom key
|
|
55
|
-
// key (may be empty).
|
|
56
|
-
// the gateway refuses its traffic without one — and the proxy adds it as `x-mnemom-api-key` on
|
|
57
|
-
// every gateway request whenever line 2 is set (required in subscription mode, where it also
|
|
58
|
-
// anchors the identity). Never argv, never env, never disk, never logged.
|
|
54
|
+
// Arrive on stdin as three newline-terminated lines: Anthropic key, Mnemom key (may be
|
|
55
|
+
// empty), OpenAI egress key (may be empty). Never argv, never env, never disk, never logged.
|
|
59
56
|
// The log (MNEMOM_AGENT_RC_PROXY_LOG, else stderr) carries method/path/status/latency and the
|
|
60
57
|
// gateway's `x-mnemom-agent` / `x-mnemom-goal-verdict` response headers only. Gateway error
|
|
61
58
|
// bodies (status ≥ 400) are logged truncated so a 4xx is diagnosable (compressed ones as
|