@mnemom/mnemom 0.17.2-next.3 → 0.17.2-next.5
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 +20 -0
- package/dist/commands/agent-config.js +3 -0
- package/dist/commands/agent-goal.d.ts +79 -0
- package/dist/commands/agent-goal.js +276 -0
- package/dist/commands/agent-sessions.d.ts +13 -3
- package/dist/commands/agent-sessions.js +46 -4
- package/dist/commands/agent-slack.d.ts +26 -0
- package/dist/commands/agent-slack.js +212 -0
- package/dist/commands/agent.d.ts +43 -2
- package/dist/commands/agent.js +208 -16
- package/dist/index.d.ts +7 -0
- package/dist/index.js +94 -2
- package/dist/lib/agent-ledger.d.ts +8 -0
- package/dist/lib/agent.d.ts +117 -4
- package/dist/lib/agent.js +106 -6
- package/dist/lib/goal-api.d.ts +133 -0
- package/dist/lib/goal-api.js +113 -0
- package/dist/lib/launcher-config.d.ts +12 -0
- package/dist/lib/launcher-config.js +64 -0
- package/dist/lib/launcher-store.d.ts +21 -0
- package/dist/lib/launcher-store.js +41 -0
- package/dist/lib/oauth.d.ts +26 -7
- package/dist/lib/oauth.js +53 -14
- package/dist/lib/slack-channel.d.ts +36 -0
- package/dist/lib/slack-channel.js +154 -0
- package/dist/lib/slack-launch.d.ts +15 -0
- package/dist/lib/slack-launch.js +42 -0
- package/dist/lib/slack-route.d.ts +58 -0
- package/dist/lib/slack-route.js +99 -0
- package/dist/lib/slack-router.d.ts +34 -0
- package/dist/lib/slack-router.js +185 -0
- package/dist/lib/slack-state.d.ts +36 -0
- package/dist/lib/slack-state.js +119 -0
- package/dist/lib/slack.d.ts +69 -0
- package/dist/lib/slack.js +178 -0
- package/dist/mcp/handoff-server.mjs +331 -0
- package/dist/rc-proxy.mjs +113 -37
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from "commander";
|
|
3
3
|
export { program };
|
|
4
|
+
/**
|
|
5
|
+
* The entrypoint dispatch. `mnemom agent slack …` owns its own flags (--thread,
|
|
6
|
+
* --channel, …), which the `agent` launcher options would otherwise reject as
|
|
7
|
+
* unknown — so it bypasses commander entirely and reaches the sub-verb parser
|
|
8
|
+
* verbatim. Exported (not auto-run) so the bypass is unit-testable.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runEntrypoint(argv?: string[]): void;
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyL
|
|
|
11
11
|
import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, protectionDriftCommand, } from "./commands/protection.js";
|
|
12
12
|
import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
|
|
13
13
|
import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
|
|
14
|
+
import { agentGoalPinCommand, agentGoalResetCommand, agentGoalShowCommand, liveGoalDeps, } from "./commands/agent-goal.js";
|
|
14
15
|
import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
|
|
15
16
|
import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
|
|
16
17
|
import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
|
|
@@ -23,6 +24,7 @@ import { listenCommand } from "./commands/listen.js";
|
|
|
23
24
|
import { tryMeCommand } from "./commands/try-me.js";
|
|
24
25
|
import { agentCommand } from "./commands/agent.js";
|
|
25
26
|
import { agentConfigCommand } from "./commands/agent-config.js";
|
|
27
|
+
import { agentSlackCommand } from "./commands/agent-slack.js";
|
|
26
28
|
import { agentDoctorCommand } from "./commands/agent-doctor.js";
|
|
27
29
|
import { agentSetupCommand } from "./commands/agent-setup.js";
|
|
28
30
|
import { agentInviteCommand, runAgentInvocation } from "./commands/agent-invite.js";
|
|
@@ -134,8 +136,11 @@ const agentCmd = program
|
|
|
134
136
|
.option("--no-setup", "Skip governed-agent provisioning entirely for this launch")
|
|
135
137
|
.option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
|
|
136
138
|
.option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
|
|
139
|
+
.option("--slack", "Attach the Slack channel: messages addressed to this session arrive in it, and it can reply (see `mnemom agent slack`)")
|
|
137
140
|
.option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
|
|
138
141
|
.option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
|
|
142
|
+
.option("--context-handoff", "EXPERIMENTAL, off by default: register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session")
|
|
143
|
+
.option("--no-context-handoff", "Keep context handoff off for this launch even when launcher.toml (context_handoff = true) or MNEMOM_AGENT_CONTEXT_HANDOFF turns it on")
|
|
139
144
|
.option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
|
|
140
145
|
.action(async (scenario, passthrough, opts) => {
|
|
141
146
|
try {
|
|
@@ -159,6 +164,12 @@ const agentCmd = program
|
|
|
159
164
|
await agentConfigCommand(passthrough);
|
|
160
165
|
return;
|
|
161
166
|
}
|
|
167
|
+
// Sub-verb: `mnemom agent slack …` posts/receives Slack messages as this
|
|
168
|
+
// session (lib/slack.ts) and never launches.
|
|
169
|
+
if (scenario === "slack") {
|
|
170
|
+
await agentSlackCommand(passthrough);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
162
173
|
if (opts.gateway)
|
|
163
174
|
process.env.MNEMOM_AGENT_GATEWAY = opts.gateway;
|
|
164
175
|
// Persistent settings (~/.mnemom/launcher.toml) sit UNDER env and flags:
|
|
@@ -217,7 +228,9 @@ const agentCmd = program
|
|
|
217
228
|
trimMcp: opts.trimMcp,
|
|
218
229
|
yolo: opts.yolo,
|
|
219
230
|
contextHint: opts.contextHint,
|
|
231
|
+
slack: opts.slack ?? cfg.slackListen,
|
|
220
232
|
context: opts.context,
|
|
233
|
+
contextHandoff: opts.contextHandoff,
|
|
221
234
|
setup: opts.setup,
|
|
222
235
|
keySource: cfg.keySource,
|
|
223
236
|
passthrough,
|
|
@@ -730,6 +743,69 @@ agentCmd
|
|
|
730
743
|
process.exit(1);
|
|
731
744
|
}
|
|
732
745
|
});
|
|
746
|
+
// `mnemom agent goal show|pin|reset` — the implicit goal the gateway inferred for a session
|
|
747
|
+
// (implicit-goal CLI). The handlers set process.exitCode themselves (1 / 2); the catch is for
|
|
748
|
+
// the unexpected.
|
|
749
|
+
const agentGoalCmd = agentCmd
|
|
750
|
+
.command("goal")
|
|
751
|
+
.description("Read and steer the goal the gateway inferred for an implicit-mode session (show / pin / reset). " +
|
|
752
|
+
"Authenticates with the same key and agent the session launched with");
|
|
753
|
+
const collectRepeat = (value, prev = []) => [...prev, value];
|
|
754
|
+
agentGoalCmd
|
|
755
|
+
.command("show [ref]")
|
|
756
|
+
.description("Show the session's live inferred goal: revision, pin, statement, requirements, fences and the latest verdict. " +
|
|
757
|
+
"`ref` is a session name (newest match wins) or a unique conversation-id prefix; omitted = the newest session")
|
|
758
|
+
.option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
|
|
759
|
+
.option("--all-threads", "List every goal thread of the conversation (main + subagents)")
|
|
760
|
+
.option("--history", "Add the revision history table")
|
|
761
|
+
.option("--json", "Output the gateway body (source gateway) plus the ledger row (source ledger)")
|
|
762
|
+
.action(async (ref, opts) => {
|
|
763
|
+
try {
|
|
764
|
+
await agentGoalShowCommand(ref, opts, liveGoalDeps);
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
agentGoalCmd
|
|
772
|
+
.command("pin [ref]")
|
|
773
|
+
.description("Pin the current inferred goal so new messages no longer revise it (--off unpins)")
|
|
774
|
+
.option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
|
|
775
|
+
.option("--off", "Unpin: new directions revise the goal again")
|
|
776
|
+
.option("--json", "Output the gateway body as JSON")
|
|
777
|
+
.action(async (ref, opts) => {
|
|
778
|
+
try {
|
|
779
|
+
await agentGoalPinCommand(ref, opts, liveGoalDeps);
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
783
|
+
process.exit(1);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
agentGoalCmd
|
|
787
|
+
.command("reset [ref]")
|
|
788
|
+
.description("Clear the inferred goal (the next message re-infers it), or replace it with --statement. " +
|
|
789
|
+
"Revisions keep counting up; spend and turn ceilings are never refunded")
|
|
790
|
+
.option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
|
|
791
|
+
.option("--statement <s>", "Replace the goal with this statement instead of clearing it")
|
|
792
|
+
.option("--requirement <r>", "A requirement for --statement (repeatable)", collectRepeat)
|
|
793
|
+
.option("--yes", "Skip the confirmation (required without a TTY)")
|
|
794
|
+
.option("--json", "Output the gateway body as JSON")
|
|
795
|
+
.action(async (ref, opts, cmd) => {
|
|
796
|
+
try {
|
|
797
|
+
// `mnemom agent` itself declares `--requirement` (a contract flag), and commander hands a
|
|
798
|
+
// parent's option to the parent wherever it appears, so the values land on `agent`.
|
|
799
|
+
// Read them from there when this command saw none.
|
|
800
|
+
const inherited = cmd.optsWithGlobals().requirement;
|
|
801
|
+
const requirement = opts.requirement ?? inherited;
|
|
802
|
+
await agentGoalResetCommand(ref, { ...opts, requirement }, liveGoalDeps);
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
733
809
|
// ============================================================================
|
|
734
810
|
// Organization management (ADR-044, Piece 1 of T1-3.1)
|
|
735
811
|
// ============================================================================
|
|
@@ -1807,6 +1883,22 @@ export { program };
|
|
|
1807
1883
|
// Parse argv only when invoked as the CLI entrypoint — not when imported.
|
|
1808
1884
|
// The symlink-safe guard lives in ./lib/entrypoint so it can be unit-tested
|
|
1809
1885
|
// without executing this module's top-level parse().
|
|
1810
|
-
|
|
1811
|
-
|
|
1886
|
+
/**
|
|
1887
|
+
* The entrypoint dispatch. `mnemom agent slack …` owns its own flags (--thread,
|
|
1888
|
+
* --channel, …), which the `agent` launcher options would otherwise reject as
|
|
1889
|
+
* unknown — so it bypasses commander entirely and reaches the sub-verb parser
|
|
1890
|
+
* verbatim. Exported (not auto-run) so the bypass is unit-testable.
|
|
1891
|
+
*/
|
|
1892
|
+
export function runEntrypoint(argv = process.argv.slice(2)) {
|
|
1893
|
+
if (argv[0] === "agent" && argv[1] === "slack") {
|
|
1894
|
+
agentSlackCommand(argv.slice(2)).catch((err) => {
|
|
1895
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1896
|
+
process.exit(1);
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
else {
|
|
1900
|
+
program.parse();
|
|
1901
|
+
}
|
|
1812
1902
|
}
|
|
1903
|
+
if (isEntrypoint(import.meta.url, process.argv[1]))
|
|
1904
|
+
runEntrypoint();
|
|
@@ -55,6 +55,14 @@ export interface SessionRecord {
|
|
|
55
55
|
* captured).
|
|
56
56
|
*/
|
|
57
57
|
goal_mode?: GoalMode;
|
|
58
|
+
/**
|
|
59
|
+
* The Claude Code session id the launch pinned with `--session-id` (or the one the caller
|
|
60
|
+
* passed via `--session-id` / `--resume <id>`), so the ledger joins conversation_id ↔ the
|
|
61
|
+
* `x-claude-code-session-id` the gateway logs. `null` when Claude Code picks it (`--continue`,
|
|
62
|
+
* a bare `--resume`, `--fork-session`) or the shape hosts many sessions (`--server`). Optional:
|
|
63
|
+
* records written before this field existed omit it (still `v: 1`).
|
|
64
|
+
*/
|
|
65
|
+
cc_session_id?: string | null;
|
|
58
66
|
}
|
|
59
67
|
/** Goal-alignment mode as recorded at launch. */
|
|
60
68
|
export type GoalMode = "off" | "on" | "implicit";
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -76,12 +76,32 @@ export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv): string;
|
|
|
76
76
|
export declare function resolveAnthropicDoor(env?: NodeJS.ProcessEnv): string;
|
|
77
77
|
/** True when the resolved gateway host is the us-2/prod cell (used for the launch banner). */
|
|
78
78
|
export declare function isProdGateway(env?: NodeJS.ProcessEnv): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The gateway origin the `handoff` MCP tool's own `POST /mnemom/context/handoff` must hit —
|
|
81
|
+
* always the SAME cell the launched session's own `/v1/messages` traffic goes to. Derived from
|
|
82
|
+
* the already-resolved Anthropic `door` (scheme+host only, via `new URL(...).origin`) rather
|
|
83
|
+
* than `resolveGatewayHost` directly, because `MNEMOM_AGENT_ANTHROPIC_GATEWAY` can override the
|
|
84
|
+
* door to a DIFFERENT host than `MNEMOM_AGENT_GATEWAY`/the default (e.g. a test cell) — reading
|
|
85
|
+
* `resolveGatewayHost` here would silently send handoff calls to prod (or vice versa) while the
|
|
86
|
+
* session's model traffic went elsewhere (MNE-7564 review).
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveHandoffGatewayHost(door: string): string;
|
|
79
89
|
/**
|
|
80
90
|
* Sanitise a header value into a safe wire token: lowercase, alphanumerics and
|
|
81
91
|
* single hyphens only, no leading/trailing hyphen. Returns "" when nothing
|
|
82
92
|
* survives (the caller rejects that).
|
|
83
93
|
*/
|
|
84
94
|
export declare function sanitizeAgentName(raw: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* The env var the launcher sets in the child CLI's environment to the session's
|
|
97
|
+
* display name, so tools running INSIDE the session (e.g. `mnemom agent slack
|
|
98
|
+
* post`) can identify themselves by the same name the claude.ai session list shows.
|
|
99
|
+
*/
|
|
100
|
+
export declare const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
101
|
+
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
102
|
+
export declare function sessionDisplayName(agent: string, scenario: string): string;
|
|
103
|
+
/** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
|
|
104
|
+
export declare function passthroughName(passthrough: readonly string[]): string | undefined;
|
|
85
105
|
/** A single-line, header-safe conversation id: honour a caller value, else a uuid. */
|
|
86
106
|
export declare function resolveConversationId(raw: string | undefined): string;
|
|
87
107
|
/** A strictly-positive integer (rejects 0, negatives, decimals, NaN, non-numeric strings). */
|
|
@@ -135,6 +155,15 @@ export declare const GOAL_MODE_HEADER = "x-mnemom-goal-mode";
|
|
|
135
155
|
export declare const SESSION_CONFIG_HEADER = "x-mnemom-session-config";
|
|
136
156
|
/** The gateway env key that gates context folding (allow-listed for per-session overlay). */
|
|
137
157
|
export declare const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
|
|
158
|
+
/**
|
|
159
|
+
* The gateway env key that gates the whole context-handoff capability (level lines +
|
|
160
|
+
* `/mnemom/context/handoff` route + the digest note section) for a single session
|
|
161
|
+
* (MNE-7561..7565). Off by default on the gateway; turned on ONLY when this launch's
|
|
162
|
+
* `contextHandoff` setting is true (see `resolveContextHandoff`), which also registers the
|
|
163
|
+
* `handoff` MCP tool (MNE-7564) — a session never gets the tool without the header, or the
|
|
164
|
+
* header without the tool.
|
|
165
|
+
*/
|
|
166
|
+
export declare const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
|
|
138
167
|
/**
|
|
139
168
|
* The `x-mnemom-goal-mode` header value for `--implicit`, or null when the flag
|
|
140
169
|
* is absent: `implicit[; max_turns=N][; budget_usd=X][; stall_turns=N]` — the
|
|
@@ -238,11 +267,95 @@ export declare function resolveContextFold(opts: {
|
|
|
238
267
|
env?: NodeJS.ProcessEnv;
|
|
239
268
|
}): boolean;
|
|
240
269
|
/**
|
|
241
|
-
* The `x-mnemom-session-config` value
|
|
242
|
-
*
|
|
243
|
-
*
|
|
270
|
+
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
271
|
+
* nothing to override (the gateway's own defaults stand — the common case). Two
|
|
272
|
+
* independent overlays, both OFF by default (folding is ON by default so only its OFF
|
|
273
|
+
* state is ever sent; handoff is OFF by default so only its ON state is ever sent):
|
|
274
|
+
* - `fold: false` → `CONTEXT_RUNTIME_ENABLED: "false"` (`--no-context`).
|
|
275
|
+
* - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
|
|
276
|
+
* setting) — the SAME header the `handoff` MCP tool's own call to
|
|
277
|
+
* `/mnemom/context/handoff` must also send, since the gateway resolves the flag
|
|
278
|
+
* per request (see `commands/agent.ts` handoff MCP wiring).
|
|
279
|
+
* base64url so it survives the gateway's inbound strip.
|
|
244
280
|
*/
|
|
245
|
-
export declare function buildSessionConfigB64(
|
|
281
|
+
export declare function buildSessionConfigB64(opts: {
|
|
282
|
+
fold: boolean;
|
|
283
|
+
contextHandoff: boolean;
|
|
284
|
+
}): string | undefined;
|
|
285
|
+
/**
|
|
286
|
+
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
287
|
+
* this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
|
|
288
|
+
* and an absent `context_handoff` setting are both "off", matching `resolveContextFold`'s
|
|
289
|
+
* shape but the opposite default. Precedence: an explicit `--context-handoff`/
|
|
290
|
+
* `--no-context-handoff` flag wins, else `MNEMOM_AGENT_CONTEXT_HANDOFF` (which
|
|
291
|
+
* `applyConfigToEnv` sets from the `context_handoff` config key), else OFF.
|
|
292
|
+
*/
|
|
293
|
+
export declare function resolveContextHandoff(opts: {
|
|
294
|
+
contextHandoff?: boolean;
|
|
295
|
+
env?: NodeJS.ProcessEnv;
|
|
296
|
+
}): boolean;
|
|
297
|
+
/** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
|
|
298
|
+
export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
|
|
299
|
+
/**
|
|
300
|
+
* The name of the non-`ANTHROPIC_*` env var `spawnRcCli` (commands/agent.ts) adds to Claude
|
|
301
|
+
* Code's Remote Control child env, carrying the real Anthropic key, ONLY when context handoff is
|
|
302
|
+
* on. `spawnRcCli` strips every `ANTHROPIC_*` auth var (including `ANTHROPIC_CUSTOM_HEADERS`) so
|
|
303
|
+
* the RC gate sees a clean first-party-OAuth setup, but that gate only inspects the recognized
|
|
304
|
+
* `ANTHROPIC_*` base-url/key/socket family — a custom-named var rides through ungated, exactly
|
|
305
|
+
* like `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` already do for the proxy. The handoff server inherits
|
|
306
|
+
* it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
|
|
307
|
+
*/
|
|
308
|
+
export declare const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
|
|
309
|
+
/**
|
|
310
|
+
* How the `handoff` MCP server (cli/src/mcp/handoff-server.mjs) recovers the Anthropic key
|
|
311
|
+
* WITHOUT it ever being written into the `--mcp-config` file on disk: `envName` is the NAME of a
|
|
312
|
+
* var already present in Claude Code's OWN process env, which every stdio MCP server inherits in
|
|
313
|
+
* full (verified experimentally for MNE-7564: a probe server registered via `--mcp-config`
|
|
314
|
+
* received vars set only in the parent `claude` process's env, unfiltered).
|
|
315
|
+
* - Terminal shape: `{ envName: "ANTHROPIC_CUSTOM_HEADERS" }` — that var carries the real key
|
|
316
|
+
* as its `x-api-key:` line (`buildCustomHeaders` above), so the server parses that line out
|
|
317
|
+
* of its own inherited env at request time.
|
|
318
|
+
* - Remote Control (both `--remote-control` and `--server`): `{ envName:
|
|
319
|
+
* HANDOFF_RC_KEY_ENV_NAME }` — `spawnRcCli` sets that var directly (see above) since RC's
|
|
320
|
+
* child env carries the key nowhere else (it rides the local proxy's stdin instead, deleted
|
|
321
|
+
* from Claude Code's own env by the strip list).
|
|
322
|
+
* There is deliberately no embedded-value ("rawKey") variant: every launch shape has an
|
|
323
|
+
* inherited-env channel, so the config file never needs to carry the key itself.
|
|
324
|
+
*/
|
|
325
|
+
export interface HandoffAnthropicKeySource {
|
|
326
|
+
envName: string;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The env vars the launcher hands the `handoff` MCP server so it can call
|
|
330
|
+
* `POST /mnemom/context/handoff` with the SAME identity + session config the launched
|
|
331
|
+
* coding-agent session's own `/v1/messages` requests carry. No secret rides in this object or
|
|
332
|
+
* the config file it produces, but the file is still written to a 0600 path and passed to
|
|
333
|
+
* `--mcp-config` as a PATH, never inlined into argv (see `commands/agent.ts`
|
|
334
|
+
* `writeHandoffMcpConfigFile`), since even an env-var NAME is worth protecting from a stray read.
|
|
335
|
+
*/
|
|
336
|
+
export interface HandoffMcpParams {
|
|
337
|
+
/** The gateway ORIGIN (e.g. https://gateway.mnemom.ai) — see `resolveHandoffGatewayHost`. */
|
|
338
|
+
gatewayHost: string;
|
|
339
|
+
anthropicKeySource: HandoffAnthropicKeySource;
|
|
340
|
+
agent: string;
|
|
341
|
+
conversationId: string;
|
|
342
|
+
/** `buildSessionConfigB64` output — forwarded so the route resolves the SAME per-session flag. */
|
|
343
|
+
sessionConfigB64?: string;
|
|
344
|
+
/** Absolute path to the compiled server entry point (`resolveHandoffServerScript`). */
|
|
345
|
+
serverScript: string;
|
|
346
|
+
/** The node executable to run it with (the launcher's own `process.execPath`). */
|
|
347
|
+
nodeBin: string;
|
|
348
|
+
}
|
|
349
|
+
/** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
|
|
350
|
+
export declare function buildHandoffMcpConfig(p: HandoffMcpParams): {
|
|
351
|
+
mcpServers: Record<string, {
|
|
352
|
+
command: string;
|
|
353
|
+
args: string[];
|
|
354
|
+
env: Record<string, string>;
|
|
355
|
+
}>;
|
|
356
|
+
};
|
|
357
|
+
/** `JSON.stringify(buildHandoffMcpConfig(p))` — the exact bytes written to the config file. */
|
|
358
|
+
export declare function buildHandoffMcpConfigJson(p: HandoffMcpParams): string;
|
|
246
359
|
/**
|
|
247
360
|
* The persistable goal-alignment default (`launcher.toml` `goal_mode`). Only the
|
|
248
361
|
* two statement-free modes can be a standing default: a SEALED contract needs a
|
package/dist/lib/agent.js
CHANGED
|
@@ -50,6 +50,18 @@ export function resolveAnthropicDoor(env = process.env) {
|
|
|
50
50
|
export function isProdGateway(env = process.env) {
|
|
51
51
|
return resolveGatewayHost(env) === PROD_GATEWAY_HOST;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The gateway origin the `handoff` MCP tool's own `POST /mnemom/context/handoff` must hit —
|
|
55
|
+
* always the SAME cell the launched session's own `/v1/messages` traffic goes to. Derived from
|
|
56
|
+
* the already-resolved Anthropic `door` (scheme+host only, via `new URL(...).origin`) rather
|
|
57
|
+
* than `resolveGatewayHost` directly, because `MNEMOM_AGENT_ANTHROPIC_GATEWAY` can override the
|
|
58
|
+
* door to a DIFFERENT host than `MNEMOM_AGENT_GATEWAY`/the default (e.g. a test cell) — reading
|
|
59
|
+
* `resolveGatewayHost` here would silently send handoff calls to prod (or vice versa) while the
|
|
60
|
+
* session's model traffic went elsewhere (MNE-7564 review).
|
|
61
|
+
*/
|
|
62
|
+
export function resolveHandoffGatewayHost(door) {
|
|
63
|
+
return new URL(door).origin;
|
|
64
|
+
}
|
|
53
65
|
/**
|
|
54
66
|
* Sanitise a header value into a safe wire token: lowercase, alphanumerics and
|
|
55
67
|
* single hyphens only, no leading/trailing hyphen. Returns "" when nothing
|
|
@@ -62,6 +74,25 @@ export function sanitizeAgentName(raw) {
|
|
|
62
74
|
.replace(/^-+/, "")
|
|
63
75
|
.replace(/-+$/, "");
|
|
64
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The env var the launcher sets in the child CLI's environment to the session's
|
|
79
|
+
* display name, so tools running INSIDE the session (e.g. `mnemom agent slack
|
|
80
|
+
* post`) can identify themselves by the same name the claude.ai session list shows.
|
|
81
|
+
*/
|
|
82
|
+
export const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
83
|
+
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
84
|
+
export function sessionDisplayName(agent, scenario) {
|
|
85
|
+
return sanitizeAgentName(`${agent}-${scenario}`) || agent;
|
|
86
|
+
}
|
|
87
|
+
/** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
|
|
88
|
+
export function passthroughName(passthrough) {
|
|
89
|
+
let name;
|
|
90
|
+
for (let i = 0; i < passthrough.length - 1; i++) {
|
|
91
|
+
if (passthrough[i] === "--name")
|
|
92
|
+
name = passthrough[i + 1];
|
|
93
|
+
}
|
|
94
|
+
return name;
|
|
95
|
+
}
|
|
65
96
|
/** A single-line, header-safe conversation id: honour a caller value, else a uuid. */
|
|
66
97
|
export function resolveConversationId(raw) {
|
|
67
98
|
const trimmed = (raw ?? "").replace(/[\r\n]+/g, "").trim();
|
|
@@ -195,6 +226,15 @@ export const GOAL_MODE_HEADER = "x-mnemom-goal-mode";
|
|
|
195
226
|
export const SESSION_CONFIG_HEADER = "x-mnemom-session-config";
|
|
196
227
|
/** The gateway env key that gates context folding (allow-listed for per-session overlay). */
|
|
197
228
|
export const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
|
|
229
|
+
/**
|
|
230
|
+
* The gateway env key that gates the whole context-handoff capability (level lines +
|
|
231
|
+
* `/mnemom/context/handoff` route + the digest note section) for a single session
|
|
232
|
+
* (MNE-7561..7565). Off by default on the gateway; turned on ONLY when this launch's
|
|
233
|
+
* `contextHandoff` setting is true (see `resolveContextHandoff`), which also registers the
|
|
234
|
+
* `handoff` MCP tool (MNE-7564) — a session never gets the tool without the header, or the
|
|
235
|
+
* header without the tool.
|
|
236
|
+
*/
|
|
237
|
+
export const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
|
|
198
238
|
const SEALED_ONLY_FLAGS = "--goal/--requirement/--allow/--forbid/--goal-id";
|
|
199
239
|
/**
|
|
200
240
|
* `--implicit` and a sealed contract are mutually exclusive: on the gateway a
|
|
@@ -393,16 +433,76 @@ export function resolveContextFold(opts) {
|
|
|
393
433
|
return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
|
|
394
434
|
}
|
|
395
435
|
/**
|
|
396
|
-
* The `x-mnemom-session-config` value
|
|
397
|
-
*
|
|
398
|
-
*
|
|
436
|
+
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
437
|
+
* nothing to override (the gateway's own defaults stand — the common case). Two
|
|
438
|
+
* independent overlays, both OFF by default (folding is ON by default so only its OFF
|
|
439
|
+
* state is ever sent; handoff is OFF by default so only its ON state is ever sent):
|
|
440
|
+
* - `fold: false` → `CONTEXT_RUNTIME_ENABLED: "false"` (`--no-context`).
|
|
441
|
+
* - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
|
|
442
|
+
* setting) — the SAME header the `handoff` MCP tool's own call to
|
|
443
|
+
* `/mnemom/context/handoff` must also send, since the gateway resolves the flag
|
|
444
|
+
* per request (see `commands/agent.ts` handoff MCP wiring).
|
|
445
|
+
* base64url so it survives the gateway's inbound strip.
|
|
399
446
|
*/
|
|
400
|
-
export function buildSessionConfigB64(
|
|
401
|
-
|
|
447
|
+
export function buildSessionConfigB64(opts) {
|
|
448
|
+
const config = {};
|
|
449
|
+
if (!opts.fold)
|
|
450
|
+
config[CONTEXT_RUNTIME_KEY] = "false";
|
|
451
|
+
if (opts.contextHandoff)
|
|
452
|
+
config[CONTEXT_HANDOFF_KEY] = "true";
|
|
453
|
+
if (Object.keys(config).length === 0)
|
|
402
454
|
return undefined;
|
|
403
|
-
const payload = JSON.stringify({ v: 1, config
|
|
455
|
+
const payload = JSON.stringify({ v: 1, config });
|
|
404
456
|
return Buffer.from(payload, "utf8").toString("base64url");
|
|
405
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
460
|
+
* this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
|
|
461
|
+
* and an absent `context_handoff` setting are both "off", matching `resolveContextFold`'s
|
|
462
|
+
* shape but the opposite default. Precedence: an explicit `--context-handoff`/
|
|
463
|
+
* `--no-context-handoff` flag wins, else `MNEMOM_AGENT_CONTEXT_HANDOFF` (which
|
|
464
|
+
* `applyConfigToEnv` sets from the `context_handoff` config key), else OFF.
|
|
465
|
+
*/
|
|
466
|
+
export function resolveContextHandoff(opts) {
|
|
467
|
+
if (opts.contextHandoff !== undefined)
|
|
468
|
+
return opts.contextHandoff;
|
|
469
|
+
const raw = (opts.env?.MNEMOM_AGENT_CONTEXT_HANDOFF ?? "").trim().toLowerCase();
|
|
470
|
+
if (raw === "")
|
|
471
|
+
return false;
|
|
472
|
+
return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
|
|
473
|
+
}
|
|
474
|
+
/** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
|
|
475
|
+
export const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
|
|
476
|
+
/**
|
|
477
|
+
* The name of the non-`ANTHROPIC_*` env var `spawnRcCli` (commands/agent.ts) adds to Claude
|
|
478
|
+
* Code's Remote Control child env, carrying the real Anthropic key, ONLY when context handoff is
|
|
479
|
+
* on. `spawnRcCli` strips every `ANTHROPIC_*` auth var (including `ANTHROPIC_CUSTOM_HEADERS`) so
|
|
480
|
+
* the RC gate sees a clean first-party-OAuth setup, but that gate only inspects the recognized
|
|
481
|
+
* `ANTHROPIC_*` base-url/key/socket family — a custom-named var rides through ungated, exactly
|
|
482
|
+
* like `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` already do for the proxy. The handoff server inherits
|
|
483
|
+
* it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
|
|
484
|
+
*/
|
|
485
|
+
export const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
|
|
486
|
+
/** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
|
|
487
|
+
export function buildHandoffMcpConfig(p) {
|
|
488
|
+
const env = {
|
|
489
|
+
MNEMOM_HANDOFF_GATEWAY_HOST: p.gatewayHost,
|
|
490
|
+
MNEMOM_HANDOFF_ANTHROPIC_KEY_ENV_NAME: p.anthropicKeySource.envName,
|
|
491
|
+
MNEMOM_HANDOFF_AGENT: p.agent,
|
|
492
|
+
MNEMOM_HANDOFF_CONVERSATION_ID: p.conversationId,
|
|
493
|
+
};
|
|
494
|
+
if (p.sessionConfigB64)
|
|
495
|
+
env.MNEMOM_HANDOFF_SESSION_CONFIG = p.sessionConfigB64;
|
|
496
|
+
return {
|
|
497
|
+
mcpServers: {
|
|
498
|
+
[HANDOFF_MCP_SERVER_NAME]: { command: p.nodeBin, args: [p.serverScript], env },
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** `JSON.stringify(buildHandoffMcpConfig(p))` — the exact bytes written to the config file. */
|
|
503
|
+
export function buildHandoffMcpConfigJson(p) {
|
|
504
|
+
return JSON.stringify(buildHandoffMcpConfig(p));
|
|
505
|
+
}
|
|
406
506
|
/**
|
|
407
507
|
* Is this launch implicit-mode? Precedence, highest wins:
|
|
408
508
|
* 1. an explicit flag — `--implicit` (true) or `--no-implicit` (false);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client for the gateway's implicit-goal control route (implicit-goal CLI, cli-telemetry
|
|
3
|
+
* §1.2 / §1.4; the route is gateway/src/goal-intent-control.ts):
|
|
4
|
+
*
|
|
5
|
+
* GET https://<gateway_host>/mnemom/goal/intent?conversation_id=…&thread=…
|
|
6
|
+
* POST https://<gateway_host>/mnemom/goal/intent/pin {conversation_id, thread, pinned}
|
|
7
|
+
* POST https://<gateway_host>/mnemom/goal/intent/reset {conversation_id, thread, statement?, requirements?}
|
|
8
|
+
*
|
|
9
|
+
* Auth is the SAME credential the launch used: `x-api-key` (the Anthropic key from
|
|
10
|
+
* `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, and `x-mnemom-api-key` when the
|
|
11
|
+
* launch had a Mnemom key. The gateway hashes them exactly as a proxied request and only LOOKS
|
|
12
|
+
* UP the agent. Key material is never printed: errors carry the status, the gateway's error code
|
|
13
|
+
* and its message, nothing else.
|
|
14
|
+
*
|
|
15
|
+
* Pure apart from the injected `fetch`, so every arm is unit-tested.
|
|
16
|
+
*/
|
|
17
|
+
import type { SessionRecord } from "./agent-ledger.js";
|
|
18
|
+
export type GoalOp = "show" | "pin" | "reset";
|
|
19
|
+
/**
|
|
20
|
+
* A thread id the gateway accepts (PLAN K3 plus `all` for reads, plus the `m.<sha16>` per-process
|
|
21
|
+
* main ids the gateway's goal-thread.ts THREAD_ID_RE also accepts). Validated locally so a typo
|
|
22
|
+
* never costs a round trip.
|
|
23
|
+
*/
|
|
24
|
+
export declare const GOAL_THREAD_RE: RegExp;
|
|
25
|
+
export declare function isValidGoalThread(thread: string): boolean;
|
|
26
|
+
/** The ledger fields the control call needs. */
|
|
27
|
+
export type GoalControlRecord = Pick<SessionRecord, "gateway_host" | "agent_slug" | "conversation_id">;
|
|
28
|
+
export interface GoalControlCreds {
|
|
29
|
+
/** The Anthropic key the launch sent as `x-api-key`. */
|
|
30
|
+
anthropicKey: string;
|
|
31
|
+
/** The Mnemom key, only when the launch sent `x-mnemom-api-key`. */
|
|
32
|
+
mnemomKey?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
|
|
35
|
+
export interface GoalControlDeps {
|
|
36
|
+
fetch: FetchLike;
|
|
37
|
+
creds: GoalControlCreds;
|
|
38
|
+
}
|
|
39
|
+
export interface GoalShowBodyInput {
|
|
40
|
+
thread?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface GoalPinBodyInput {
|
|
43
|
+
thread?: string;
|
|
44
|
+
pinned: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface GoalResetBodyInput {
|
|
47
|
+
thread?: string;
|
|
48
|
+
statement?: string;
|
|
49
|
+
requirements?: string[];
|
|
50
|
+
}
|
|
51
|
+
export interface GoalPinned {
|
|
52
|
+
at: string;
|
|
53
|
+
revision: number;
|
|
54
|
+
by: "cli";
|
|
55
|
+
}
|
|
56
|
+
export interface GoalHistoryEntry {
|
|
57
|
+
revision: number;
|
|
58
|
+
hash: string;
|
|
59
|
+
class: string;
|
|
60
|
+
ops: number;
|
|
61
|
+
at: string;
|
|
62
|
+
source: string | null;
|
|
63
|
+
}
|
|
64
|
+
export interface GoalIntentView {
|
|
65
|
+
intent_id: string;
|
|
66
|
+
revision: number;
|
|
67
|
+
contract_hash: string;
|
|
68
|
+
statement: string;
|
|
69
|
+
requirements: string[];
|
|
70
|
+
allowed_paths: string[];
|
|
71
|
+
forbidden_paths: string[];
|
|
72
|
+
pinned: GoalPinned | null;
|
|
73
|
+
pending_count: number;
|
|
74
|
+
history: GoalHistoryEntry[];
|
|
75
|
+
revisions_10m: number;
|
|
76
|
+
gates_10m: number;
|
|
77
|
+
updated_at: string;
|
|
78
|
+
}
|
|
79
|
+
export interface GoalVerdictView {
|
|
80
|
+
verdict: string;
|
|
81
|
+
turn: number;
|
|
82
|
+
at: string;
|
|
83
|
+
superseded: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface GoalThreadRow {
|
|
86
|
+
thread: string;
|
|
87
|
+
role: string;
|
|
88
|
+
revision: number;
|
|
89
|
+
statement: string | null;
|
|
90
|
+
pinned: boolean;
|
|
91
|
+
}
|
|
92
|
+
/** `GET /mnemom/goal/intent` (cli-telemetry §1.2 plus the `thread` echo and `last_revision`). */
|
|
93
|
+
export interface GoalShowResponse {
|
|
94
|
+
conversation_id: string;
|
|
95
|
+
thread: string;
|
|
96
|
+
mode: "implicit" | "sealed" | "none";
|
|
97
|
+
sealed_hash?: string;
|
|
98
|
+
intent: GoalIntentView | null;
|
|
99
|
+
verdict: GoalVerdictView | null;
|
|
100
|
+
last_revision: number;
|
|
101
|
+
threads?: GoalThreadRow[];
|
|
102
|
+
}
|
|
103
|
+
export interface GoalPinResponse {
|
|
104
|
+
conversation_id: string;
|
|
105
|
+
thread: string;
|
|
106
|
+
pinned: GoalPinned | null;
|
|
107
|
+
revision: number;
|
|
108
|
+
}
|
|
109
|
+
export interface GoalResetResponse {
|
|
110
|
+
conversation_id: string;
|
|
111
|
+
thread: string;
|
|
112
|
+
reset: true;
|
|
113
|
+
revision: number;
|
|
114
|
+
prior_revision: number;
|
|
115
|
+
intent: GoalIntentView | null;
|
|
116
|
+
verdict_superseded: boolean;
|
|
117
|
+
}
|
|
118
|
+
/** A non-2xx answer (status > 0) or a transport failure (status 0). Never carries key material. */
|
|
119
|
+
export declare class GoalControlError extends Error {
|
|
120
|
+
readonly status: number;
|
|
121
|
+
readonly code: string | null;
|
|
122
|
+
constructor(status: number, code: string | null, message: string);
|
|
123
|
+
}
|
|
124
|
+
/** `https://<host>` for a bare host; a host that already names a scheme is kept (trailing / dropped). */
|
|
125
|
+
export declare function gatewayOrigin(host: string): string;
|
|
126
|
+
export declare const GOAL_CONTROL_PATH = "/mnemom/goal/intent";
|
|
127
|
+
/** The route URL for one op (the show query carries the conversation id and thread). */
|
|
128
|
+
export declare function goalControlUrl(record: GoalControlRecord, op: GoalOp, thread?: string): string;
|
|
129
|
+
/** The launch's identity headers (never logged). */
|
|
130
|
+
export declare function goalControlHeaders(record: GoalControlRecord, creds: GoalControlCreds): Record<string, string>;
|
|
131
|
+
export declare function goalControlFetch(record: GoalControlRecord, op: "show", body: GoalShowBodyInput, deps: GoalControlDeps): Promise<GoalShowResponse>;
|
|
132
|
+
export declare function goalControlFetch(record: GoalControlRecord, op: "pin", body: GoalPinBodyInput, deps: GoalControlDeps): Promise<GoalPinResponse>;
|
|
133
|
+
export declare function goalControlFetch(record: GoalControlRecord, op: "reset", body: GoalResetBodyInput, deps: GoalControlDeps): Promise<GoalResetResponse>;
|