@mnemom/mnemom 0.17.0-next.7 → 0.17.0-next.9
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 +26 -0
- package/dist/commands/agent-config.d.ts +12 -0
- package/dist/commands/agent-config.js +22 -2
- package/dist/commands/agent-session.d.ts +26 -10
- package/dist/commands/agent-session.js +59 -23
- package/dist/commands/agent-sessions.d.ts +99 -0
- package/dist/commands/agent-sessions.js +271 -0
- package/dist/commands/agent.d.ts +55 -0
- package/dist/commands/agent.js +123 -13
- package/dist/commands/validate.js +1 -16
- package/dist/index.js +47 -5
- package/dist/lib/agent-ledger.d.ts +91 -0
- package/dist/lib/agent-ledger.js +126 -0
- package/dist/lib/agent.d.ts +43 -1
- package/dist/lib/agent.js +80 -1
- package/dist/lib/api.d.ts +62 -0
- package/dist/lib/api.js +11 -0
- package/dist/lib/auth.js +21 -1
- package/dist/lib/format.d.ts +6 -0
- package/dist/lib/format.js +20 -0
- package/dist/lib/launcher-config.d.ts +3 -1
- package/dist/lib/launcher-config.js +20 -1
- package/dist/lib/oauth.d.ts +26 -4
- package/dist/lib/oauth.js +98 -29
- package/dist/rc-proxy.mjs +14 -4
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { cardShowCommand, cardPublishCommand, cardValidateCommand, cardEditComma
|
|
|
10
10
|
import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyListCommand, policyTestCommand, policyEvaluateCommand, } from "./commands/policy.js";
|
|
11
11
|
import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, protectionDriftCommand, } from "./commands/protection.js";
|
|
12
12
|
import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
|
|
13
|
+
import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
|
|
13
14
|
import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
|
|
14
15
|
import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
|
|
15
16
|
import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
|
|
@@ -26,6 +27,7 @@ import { agentDoctorCommand } from "./commands/agent-doctor.js";
|
|
|
26
27
|
import { agentSetupCommand } from "./commands/agent-setup.js";
|
|
27
28
|
import { agentInviteCommand, runAgentInvocation } from "./commands/agent-invite.js";
|
|
28
29
|
import { applyConfigToEnv, loadLauncherConfig } from "./lib/launcher-config.js";
|
|
30
|
+
import { resolveImplicit } from "./lib/agent.js";
|
|
29
31
|
import { wrapCommand } from "./commands/wrap.js";
|
|
30
32
|
import { onboardCommand } from "./commands/onboard.js";
|
|
31
33
|
import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
|
|
@@ -102,7 +104,7 @@ const collectRepeatable = (value, previous) => {
|
|
|
102
104
|
const agentCmd = program
|
|
103
105
|
.command("agent [session] [passthrough...]")
|
|
104
106
|
.description("Launch your governed agent (Claude Code in v1) through the Mnemom gateway — governed identity + optional sealed goal contract. Plain `mnemom agent` asks for a session name and an optional goal. " +
|
|
105
|
-
"Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs.")
|
|
107
|
+
"Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state.")
|
|
106
108
|
.option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
|
|
107
109
|
.option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
|
|
108
110
|
.option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
|
|
@@ -111,6 +113,8 @@ const agentCmd = program
|
|
|
111
113
|
.option("--budget <usd>", "Guardrail: spend ceiling in USD (positive number)")
|
|
112
114
|
.option("--max-turns <n>", "Guardrail: turn ceiling (positive integer)")
|
|
113
115
|
.option("--stall <n>", "Guardrail: consecutive no-write turns (positive integer)")
|
|
116
|
+
.option("--implicit", "Goal alignment without a sealed contract: the gateway infers the goal from your messages and revises it as you steer (not combinable with --goal/--requirement/--allow/--forbid/--goal-id; the guardrail ceilings still apply)")
|
|
117
|
+
.option("--no-implicit", "Goal alignment off for this launch even when launcher.toml sets goal_mode = implicit")
|
|
114
118
|
.option("--conversation-id <id>", "x-mnemom-conversation-id (default: auto-generated)")
|
|
115
119
|
.option("--cli <name|path>", "Coding-agent CLI to launch: a name or full path (default: claude)")
|
|
116
120
|
.option("--gateway <url>", "Gateway host override (default: https://gateway.mnemom.ai; or MNEMOM_AGENT_GATEWAY)")
|
|
@@ -174,10 +178,19 @@ const agentCmd = program
|
|
|
174
178
|
return;
|
|
175
179
|
}
|
|
176
180
|
const numToStr = (n) => n === undefined ? undefined : String(n);
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
const
|
|
181
|
+
// Implicit mode: --implicit / --no-implicit win; a --goal statement seals
|
|
182
|
+
// (the standing config default never overrides it); else launcher.toml
|
|
183
|
+
// `goal_mode`; else off. Pure + unit-tested in lib/agent.ts.
|
|
184
|
+
const implicit = resolveImplicit({
|
|
185
|
+
goal: opts.goal,
|
|
186
|
+
implicit: opts.implicit,
|
|
187
|
+
configGoalMode: cfg.goalMode,
|
|
188
|
+
});
|
|
189
|
+
// Config guardrails apply only when goal alignment is actually intended (a
|
|
190
|
+
// --goal is present, or implicit mode — where they ride on the mode header),
|
|
191
|
+
// so a saved ceiling never forces an unrequested contract (which would
|
|
192
|
+
// then demand --goal and fail the launch).
|
|
193
|
+
const contractIntended = Boolean(opts.goal && opts.goal.trim()) || implicit;
|
|
181
194
|
// The governed identity rides on the program-level `--agent` (which
|
|
182
195
|
// shadows a subcommand `--agent` under commander@12 — the MNE-238 class),
|
|
183
196
|
// exactly as status/logs/onboard read it.
|
|
@@ -188,6 +201,7 @@ const agentCmd = program
|
|
|
188
201
|
allow: opts.allow,
|
|
189
202
|
forbid: opts.forbid,
|
|
190
203
|
goalId: opts.goalId,
|
|
204
|
+
implicit,
|
|
191
205
|
budget: opts.budget ?? (contractIntended ? numToStr(cfg.guardrails?.budgetUsd) : undefined),
|
|
192
206
|
maxTurns: opts.maxTurns ?? (contractIntended ? numToStr(cfg.guardrails?.maxTurns) : undefined),
|
|
193
207
|
stall: opts.stall ?? (contractIntended ? numToStr(cfg.guardrails?.stallTurns) : undefined),
|
|
@@ -685,6 +699,34 @@ agentCmd
|
|
|
685
699
|
process.exit(1);
|
|
686
700
|
}
|
|
687
701
|
});
|
|
702
|
+
agentCmd
|
|
703
|
+
.command("sessions")
|
|
704
|
+
.description("List your Mnemom Agent sessions from the local ledger (~/.mnemom/sessions.jsonl), newest first — the last 20 unless --all")
|
|
705
|
+
.option("--all", "Every session, not just the newest 20")
|
|
706
|
+
.option("--json", "Output the rows as JSON instead of a table")
|
|
707
|
+
.action(async (opts) => {
|
|
708
|
+
try {
|
|
709
|
+
await agentSessionsCommand({ all: opts.all, json: opts.json }, liveSessionDeps);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
agentCmd
|
|
717
|
+
.command("show [ref]")
|
|
718
|
+
.description("Show one session — the local launch record merged with the gateway's live goal state (sealed contract, latest verdict, guardrails). " +
|
|
719
|
+
"`ref` is a session name (newest match wins) or a unique conversation-id prefix; omitted = the newest session")
|
|
720
|
+
.option("--json", "Output the merged view as JSON (each section tagged with its source)")
|
|
721
|
+
.action(async (ref, opts) => {
|
|
722
|
+
try {
|
|
723
|
+
await agentShowCommand(ref, { json: opts.json }, liveSessionDeps);
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
688
730
|
// ============================================================================
|
|
689
731
|
// Organization management (ADR-044, Piece 1 of T1-3.1)
|
|
690
732
|
// ============================================================================
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local session ledger for `mnemom agent` — `~/.mnemom/sessions.jsonl`, one
|
|
3
|
+
* JSON line per REAL launch (never `--dry-run`), newest last.
|
|
4
|
+
*
|
|
5
|
+
* Pure module: the directory, the clock and the pid are injected so the append
|
|
6
|
+
* and the read are unit-testable with a temp dir. The record is deliberately a
|
|
7
|
+
* closed shape — the Anthropic key, the custom-headers line and the child env
|
|
8
|
+
* are never part of it (see `SessionRecord`), so nothing secret can ride along.
|
|
9
|
+
*
|
|
10
|
+
* Writes are best-effort: `appendSessionRecord` never throws — a failure comes
|
|
11
|
+
* back as `{ ok: false, error }` and the caller decides how loudly to warn.
|
|
12
|
+
*/
|
|
13
|
+
import type { LaunchShape, SealedContract } from "./agent.js";
|
|
14
|
+
/** The ledger's file name under MNEMOM_DIR. */
|
|
15
|
+
export declare const LEDGER_FILE = "sessions.jsonl";
|
|
16
|
+
/** The ledger keeps only the newest this-many launches. */
|
|
17
|
+
export declare const LEDGER_CAP = 200;
|
|
18
|
+
/** The sealed-contract fields the ledger keeps (no goal_id — the statement is the identity). */
|
|
19
|
+
export interface SessionRecordContract {
|
|
20
|
+
statement: string;
|
|
21
|
+
requirements?: string[];
|
|
22
|
+
allowed_paths?: string[];
|
|
23
|
+
forbidden_paths?: string[];
|
|
24
|
+
max_turns?: number;
|
|
25
|
+
budget_usd?: number;
|
|
26
|
+
stall_turns?: number;
|
|
27
|
+
}
|
|
28
|
+
/** One launch, as written to the ledger. */
|
|
29
|
+
export interface SessionRecord {
|
|
30
|
+
v: 1;
|
|
31
|
+
/** ISO-8601 UTC. */
|
|
32
|
+
started_at: string;
|
|
33
|
+
/** The session (scenario) name. */
|
|
34
|
+
session: string;
|
|
35
|
+
conversation_id: string;
|
|
36
|
+
/** The x-mnemom-agent slug the launch ran under. */
|
|
37
|
+
agent_slug: string;
|
|
38
|
+
/** The agent record id when it resolved at launch, else null. */
|
|
39
|
+
agent_id: string | null;
|
|
40
|
+
org_id: string;
|
|
41
|
+
org_label: string;
|
|
42
|
+
gateway_host: string;
|
|
43
|
+
launch_shape: LaunchShape;
|
|
44
|
+
model: string | null;
|
|
45
|
+
effort: string | null;
|
|
46
|
+
/** The coding-agent CLI's basename, e.g. "claude". */
|
|
47
|
+
cli: string;
|
|
48
|
+
contract: SessionRecordContract | null;
|
|
49
|
+
}
|
|
50
|
+
/** What the call site supplies — the ledger stamps `v` and `started_at`. */
|
|
51
|
+
export type SessionRecordInput = Omit<SessionRecord, "v" | "started_at">;
|
|
52
|
+
/** The injected environment of the ledger. */
|
|
53
|
+
export interface LedgerDeps {
|
|
54
|
+
/** The directory holding the ledger (MNEMOM_DIR in production). */
|
|
55
|
+
dir: string;
|
|
56
|
+
/** The clock (stamps `started_at` and names the rewrite temp file). */
|
|
57
|
+
now: () => Date;
|
|
58
|
+
/** The writing process's pid (names the rewrite temp file). */
|
|
59
|
+
pid: number;
|
|
60
|
+
}
|
|
61
|
+
export type AppendResult = {
|
|
62
|
+
ok: true;
|
|
63
|
+
record: SessionRecord;
|
|
64
|
+
path: string;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
error: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Project a sealed contract onto the ledger's contract shape (drops `goal_id`,
|
|
71
|
+
* keeps only defined fields). `null` in, `null` out.
|
|
72
|
+
*/
|
|
73
|
+
export declare function toLedgerContract(sealed: SealedContract | null): SessionRecordContract | null;
|
|
74
|
+
/** The ledger's path under `dir`. */
|
|
75
|
+
export declare function ledgerPath(dir: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Append one launch to the ledger, atomically and capped:
|
|
78
|
+
* - `dir` is created 0o700 when missing; the file is created 0o600;
|
|
79
|
+
* - the new line is appended when the ledger stays within LEDGER_CAP lines,
|
|
80
|
+
* otherwise the newest LEDGER_CAP lines (this one included) are rewritten
|
|
81
|
+
* via a temp file + rename, so a reader never sees a torn file.
|
|
82
|
+
* Never throws — a failure is returned as `{ ok: false, error }`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function appendSessionRecord(input: SessionRecordInput, deps: LedgerDeps): AppendResult;
|
|
85
|
+
/**
|
|
86
|
+
* Read the ledger, oldest first (file order). A missing file is an empty
|
|
87
|
+
* ledger; a line that is not a v1 record object is skipped, never fatal.
|
|
88
|
+
*/
|
|
89
|
+
export declare function readSessionRecords(deps: Pick<LedgerDeps, "dir">): SessionRecord[];
|
|
90
|
+
/** One ledger line → a record, or null when it is not a v1 record object. */
|
|
91
|
+
export declare function parseRecord(line: string): SessionRecord | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local session ledger for `mnemom agent` — `~/.mnemom/sessions.jsonl`, one
|
|
3
|
+
* JSON line per REAL launch (never `--dry-run`), newest last.
|
|
4
|
+
*
|
|
5
|
+
* Pure module: the directory, the clock and the pid are injected so the append
|
|
6
|
+
* and the read are unit-testable with a temp dir. The record is deliberately a
|
|
7
|
+
* closed shape — the Anthropic key, the custom-headers line and the child env
|
|
8
|
+
* are never part of it (see `SessionRecord`), so nothing secret can ride along.
|
|
9
|
+
*
|
|
10
|
+
* Writes are best-effort: `appendSessionRecord` never throws — a failure comes
|
|
11
|
+
* back as `{ ok: false, error }` and the caller decides how loudly to warn.
|
|
12
|
+
*/
|
|
13
|
+
import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
/** The ledger's file name under MNEMOM_DIR. */
|
|
16
|
+
export const LEDGER_FILE = "sessions.jsonl";
|
|
17
|
+
/** The ledger keeps only the newest this-many launches. */
|
|
18
|
+
export const LEDGER_CAP = 200;
|
|
19
|
+
/**
|
|
20
|
+
* Project a sealed contract onto the ledger's contract shape (drops `goal_id`,
|
|
21
|
+
* keeps only defined fields). `null` in, `null` out.
|
|
22
|
+
*/
|
|
23
|
+
export function toLedgerContract(sealed) {
|
|
24
|
+
if (!sealed)
|
|
25
|
+
return null;
|
|
26
|
+
const out = { statement: sealed.statement };
|
|
27
|
+
if (sealed.requirements !== undefined)
|
|
28
|
+
out.requirements = sealed.requirements;
|
|
29
|
+
if (sealed.allowed_paths !== undefined)
|
|
30
|
+
out.allowed_paths = sealed.allowed_paths;
|
|
31
|
+
if (sealed.forbidden_paths !== undefined)
|
|
32
|
+
out.forbidden_paths = sealed.forbidden_paths;
|
|
33
|
+
if (sealed.max_turns !== undefined)
|
|
34
|
+
out.max_turns = sealed.max_turns;
|
|
35
|
+
if (sealed.budget_usd !== undefined)
|
|
36
|
+
out.budget_usd = sealed.budget_usd;
|
|
37
|
+
if (sealed.stall_turns !== undefined)
|
|
38
|
+
out.stall_turns = sealed.stall_turns;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** The ledger's path under `dir`. */
|
|
42
|
+
export function ledgerPath(dir) {
|
|
43
|
+
return join(dir, LEDGER_FILE);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the ledger text, or null when the file does not exist. Read-and-catch
|
|
47
|
+
* rather than exists-then-read so there is no window in which the file can
|
|
48
|
+
* change between the check and the read (CodeQL js/file-system-race).
|
|
49
|
+
*/
|
|
50
|
+
function readLedgerText(path) {
|
|
51
|
+
try {
|
|
52
|
+
return readFileSync(path, "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err.code === "ENOENT")
|
|
56
|
+
return null;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Append one launch to the ledger, atomically and capped:
|
|
62
|
+
* - `dir` is created 0o700 when missing; the file is created 0o600;
|
|
63
|
+
* - the new line is appended when the ledger stays within LEDGER_CAP lines,
|
|
64
|
+
* otherwise the newest LEDGER_CAP lines (this one included) are rewritten
|
|
65
|
+
* via a temp file + rename, so a reader never sees a torn file.
|
|
66
|
+
* Never throws — a failure is returned as `{ ok: false, error }`.
|
|
67
|
+
*/
|
|
68
|
+
export function appendSessionRecord(input, deps) {
|
|
69
|
+
const path = ledgerPath(deps.dir);
|
|
70
|
+
try {
|
|
71
|
+
const record = { v: 1, started_at: deps.now().toISOString(), ...input };
|
|
72
|
+
mkdirSync(deps.dir, { recursive: true, mode: 0o700 });
|
|
73
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
74
|
+
const text = readLedgerText(path);
|
|
75
|
+
const existing = text === null ? [] : nonEmptyLines(text);
|
|
76
|
+
if (existing.length + 1 > LEDGER_CAP) {
|
|
77
|
+
const kept = [...existing, JSON.stringify(record)].slice(-LEDGER_CAP);
|
|
78
|
+
const tmp = join(deps.dir, `.${LEDGER_FILE}.tmp-${deps.pid}-${deps.now().getTime()}`);
|
|
79
|
+
writeFileSync(tmp, `${kept.join("\n")}\n`, { mode: 0o600 });
|
|
80
|
+
renameSync(tmp, path);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
appendFileSync(path, line, { mode: 0o600 });
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, record, path };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read the ledger, oldest first (file order). A missing file is an empty
|
|
93
|
+
* ledger; a line that is not a v1 record object is skipped, never fatal.
|
|
94
|
+
*/
|
|
95
|
+
export function readSessionRecords(deps) {
|
|
96
|
+
const path = ledgerPath(deps.dir);
|
|
97
|
+
const text = readLedgerText(path);
|
|
98
|
+
if (text === null)
|
|
99
|
+
return [];
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const line of nonEmptyLines(text)) {
|
|
102
|
+
const parsed = parseRecord(line);
|
|
103
|
+
if (parsed)
|
|
104
|
+
out.push(parsed);
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** One ledger line → a record, or null when it is not a v1 record object. */
|
|
109
|
+
export function parseRecord(line) {
|
|
110
|
+
let value;
|
|
111
|
+
try {
|
|
112
|
+
value = JSON.parse(line);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
118
|
+
return null;
|
|
119
|
+
const rec = value;
|
|
120
|
+
if (rec.v !== 1)
|
|
121
|
+
return null;
|
|
122
|
+
return rec;
|
|
123
|
+
}
|
|
124
|
+
function nonEmptyLines(text) {
|
|
125
|
+
return text.split("\n").filter((l) => l.trim().length > 0);
|
|
126
|
+
}
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -48,6 +48,14 @@ export interface ContractFlags {
|
|
|
48
48
|
maxTurns?: string | number;
|
|
49
49
|
budget?: string | number;
|
|
50
50
|
stall?: string | number;
|
|
51
|
+
/**
|
|
52
|
+
* `--implicit`: goal alignment WITHOUT a sealed contract. No `x-mnemom-contract`
|
|
53
|
+
* is sent; the gateway infers the goal from the user's own messages (revision 1
|
|
54
|
+
* after the first message) and revises it as the user steers. The guardrail
|
|
55
|
+
* ceilings still apply — they ride on the `x-mnemom-goal-mode` header instead
|
|
56
|
+
* of inside a contract. Cannot be combined with the sealed-contract flags.
|
|
57
|
+
*/
|
|
58
|
+
implicit?: boolean;
|
|
51
59
|
}
|
|
52
60
|
/** The three launch shapes the command supports (see commands/code.ts). */
|
|
53
61
|
export type LaunchShape = "terminal" | "remote-control" | "server";
|
|
@@ -116,6 +124,15 @@ export interface SealedContractEnvelope {
|
|
|
116
124
|
* here matches whatever the gateway seals for the same flags.
|
|
117
125
|
*/
|
|
118
126
|
export declare function assembleContract(flags: ContractFlags): SealedContractEnvelope | null;
|
|
127
|
+
/** The request header that opts a conversation into implicit goal mode. */
|
|
128
|
+
export declare const GOAL_MODE_HEADER = "x-mnemom-goal-mode";
|
|
129
|
+
/**
|
|
130
|
+
* The `x-mnemom-goal-mode` header value for `--implicit`, or null when the flag
|
|
131
|
+
* is absent: `implicit[; max_turns=N][; budget_usd=X][; stall_turns=N]` — the
|
|
132
|
+
* guardrail ceilings validated exactly as `assembleContract` validates them (a
|
|
133
|
+
* non-positive ceiling is refused, never silently dropped into an unguarded run).
|
|
134
|
+
*/
|
|
135
|
+
export declare function buildGoalModeHeader(flags: ContractFlags): string | null;
|
|
119
136
|
/**
|
|
120
137
|
* Resolve the launch shape from the flags and the `MNEMOM_AGENT_REMOTE_CONTROL`
|
|
121
138
|
* env var (the flags win). Default is `terminal` — a plain governed launch in
|
|
@@ -142,7 +159,8 @@ export declare function launchPreferenceExplicit(opts: {
|
|
|
142
159
|
/**
|
|
143
160
|
* Build the `ANTHROPIC_CUSTOM_HEADERS` value for the terminal-only launch: the
|
|
144
161
|
* real Anthropic key as `x-api-key`, plus the governed-identity headers and the
|
|
145
|
-
* sealed contract when present
|
|
162
|
+
* sealed contract when present (or the implicit goal-mode header when the launch
|
|
163
|
+
* is `--implicit` — never both). Newline-separated, exactly the wire shape Claude
|
|
146
164
|
* Code sends to the /anthropic door. NB: the returned string contains the key —
|
|
147
165
|
* callers must place it only in the child's env, never in a log line.
|
|
148
166
|
*/
|
|
@@ -151,4 +169,28 @@ export declare function buildCustomHeaders(opts: {
|
|
|
151
169
|
agent: string;
|
|
152
170
|
conversationId: string;
|
|
153
171
|
contractB64?: string;
|
|
172
|
+
/** `buildGoalModeHeader` output for an `--implicit` launch. */
|
|
173
|
+
goalMode?: string;
|
|
154
174
|
}): string;
|
|
175
|
+
/**
|
|
176
|
+
* The persistable goal-alignment default (`launcher.toml` `goal_mode`). Only the
|
|
177
|
+
* two statement-free modes can be a standing default: a SEALED contract needs a
|
|
178
|
+
* per-session statement, so it is always a per-launch `--goal`.
|
|
179
|
+
*/
|
|
180
|
+
export type GoalModeDefault = "implicit" | "off";
|
|
181
|
+
/**
|
|
182
|
+
* Is this launch implicit-mode? Precedence, highest wins:
|
|
183
|
+
* 1. an explicit flag — `--implicit` (true) or `--no-implicit` (false);
|
|
184
|
+
* 2. a `--goal` statement — a sealed contract is a per-launch choice that the
|
|
185
|
+
* standing config default must never silently override;
|
|
186
|
+
* 3. the config default `goal_mode = "implicit"`;
|
|
187
|
+
* 4. off.
|
|
188
|
+
* Pure so every arm is unit-tested; the caller passes the resolved value on to
|
|
189
|
+
* `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
|
|
190
|
+
* combined with a contract flag — that conflict is EXPLICIT and is reported).
|
|
191
|
+
*/
|
|
192
|
+
export declare function resolveImplicit(opts: {
|
|
193
|
+
goal?: string;
|
|
194
|
+
implicit?: boolean;
|
|
195
|
+
configGoalMode?: GoalModeDefault;
|
|
196
|
+
}): boolean;
|
package/dist/lib/agent.js
CHANGED
|
@@ -132,6 +132,12 @@ function hasText(v) {
|
|
|
132
132
|
* here matches whatever the gateway seals for the same flags.
|
|
133
133
|
*/
|
|
134
134
|
export function assembleContract(flags) {
|
|
135
|
+
// Implicit goal mode seals nothing: the ceilings ride on the mode header
|
|
136
|
+
// (`buildGoalModeHeader`), so a `--budget` alone must not demand `--goal` here.
|
|
137
|
+
if (flags.implicit) {
|
|
138
|
+
assertImplicitCompatible(flags);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
135
141
|
if (!contractFlagsPresent(flags))
|
|
136
142
|
return null;
|
|
137
143
|
const statement = (flags.goal ?? "").trim();
|
|
@@ -178,6 +184,58 @@ export function assembleContract(flags) {
|
|
|
178
184
|
sha: sha256Hex16(canonical),
|
|
179
185
|
};
|
|
180
186
|
}
|
|
187
|
+
/** The request header that opts a conversation into implicit goal mode. */
|
|
188
|
+
export const GOAL_MODE_HEADER = "x-mnemom-goal-mode";
|
|
189
|
+
const SEALED_ONLY_FLAGS = "--goal/--requirement/--allow/--forbid/--goal-id";
|
|
190
|
+
/**
|
|
191
|
+
* `--implicit` and a sealed contract are mutually exclusive: on the gateway a
|
|
192
|
+
* sealed `x-mnemom-contract` always wins and would silently switch implicit mode
|
|
193
|
+
* off, so the launch refuses the combination rather than ship a header that does
|
|
194
|
+
* nothing.
|
|
195
|
+
*/
|
|
196
|
+
function assertImplicitCompatible(flags) {
|
|
197
|
+
const sealed = hasText(flags.goal) ||
|
|
198
|
+
(flags.requirement?.length ?? 0) > 0 ||
|
|
199
|
+
(flags.allow?.length ?? 0) > 0 ||
|
|
200
|
+
(flags.forbid?.length ?? 0) > 0 ||
|
|
201
|
+
hasText(flags.goalId);
|
|
202
|
+
if (sealed) {
|
|
203
|
+
throw new Error(`--implicit cannot be combined with ${SEALED_ONLY_FLAGS}: a sealed contract always wins on ` +
|
|
204
|
+
"the gateway and would silently switch implicit mode off. Drop --implicit to seal a " +
|
|
205
|
+
"contract, or drop the contract flags to let the gateway infer the goal from your messages.");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The `x-mnemom-goal-mode` header value for `--implicit`, or null when the flag
|
|
210
|
+
* is absent: `implicit[; max_turns=N][; budget_usd=X][; stall_turns=N]` — the
|
|
211
|
+
* guardrail ceilings validated exactly as `assembleContract` validates them (a
|
|
212
|
+
* non-positive ceiling is refused, never silently dropped into an unguarded run).
|
|
213
|
+
*/
|
|
214
|
+
export function buildGoalModeHeader(flags) {
|
|
215
|
+
if (!flags.implicit)
|
|
216
|
+
return null;
|
|
217
|
+
assertImplicitCompatible(flags);
|
|
218
|
+
const parts = ["implicit"];
|
|
219
|
+
if (flags.maxTurns !== undefined) {
|
|
220
|
+
if (!isPositiveInt(flags.maxTurns)) {
|
|
221
|
+
throw new Error(`--max-turns must be a positive integer (got '${flags.maxTurns}').`);
|
|
222
|
+
}
|
|
223
|
+
parts.push(`max_turns=${Number(flags.maxTurns)}`);
|
|
224
|
+
}
|
|
225
|
+
if (flags.budget !== undefined) {
|
|
226
|
+
if (!isPositiveNumber(flags.budget)) {
|
|
227
|
+
throw new Error(`--budget must be a positive USD amount (got '${flags.budget}').`);
|
|
228
|
+
}
|
|
229
|
+
parts.push(`budget_usd=${Number(flags.budget)}`);
|
|
230
|
+
}
|
|
231
|
+
if (flags.stall !== undefined) {
|
|
232
|
+
if (!isPositiveInt(flags.stall)) {
|
|
233
|
+
throw new Error(`--stall must be a positive integer (got '${flags.stall}').`);
|
|
234
|
+
}
|
|
235
|
+
parts.push(`stall_turns=${Number(flags.stall)}`);
|
|
236
|
+
}
|
|
237
|
+
return parts.join("; ");
|
|
238
|
+
}
|
|
181
239
|
/** Trim, drop empties — matches the gateway's `stringArray` filtering. */
|
|
182
240
|
function cleanList(values) {
|
|
183
241
|
return (values ?? []).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
@@ -236,7 +294,8 @@ export function launchPreferenceExplicit(opts, env = process.env) {
|
|
|
236
294
|
/**
|
|
237
295
|
* Build the `ANTHROPIC_CUSTOM_HEADERS` value for the terminal-only launch: the
|
|
238
296
|
* real Anthropic key as `x-api-key`, plus the governed-identity headers and the
|
|
239
|
-
* sealed contract when present
|
|
297
|
+
* sealed contract when present (or the implicit goal-mode header when the launch
|
|
298
|
+
* is `--implicit` — never both). Newline-separated, exactly the wire shape Claude
|
|
240
299
|
* Code sends to the /anthropic door. NB: the returned string contains the key —
|
|
241
300
|
* callers must place it only in the child's env, never in a log line.
|
|
242
301
|
*/
|
|
@@ -248,5 +307,25 @@ export function buildCustomHeaders(opts) {
|
|
|
248
307
|
];
|
|
249
308
|
if (opts.contractB64)
|
|
250
309
|
lines.push(`x-mnemom-contract: ${opts.contractB64}`);
|
|
310
|
+
if (opts.goalMode)
|
|
311
|
+
lines.push(`${GOAL_MODE_HEADER}: ${opts.goalMode}`);
|
|
251
312
|
return lines.join("\n");
|
|
252
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Is this launch implicit-mode? Precedence, highest wins:
|
|
316
|
+
* 1. an explicit flag — `--implicit` (true) or `--no-implicit` (false);
|
|
317
|
+
* 2. a `--goal` statement — a sealed contract is a per-launch choice that the
|
|
318
|
+
* standing config default must never silently override;
|
|
319
|
+
* 3. the config default `goal_mode = "implicit"`;
|
|
320
|
+
* 4. off.
|
|
321
|
+
* Pure so every arm is unit-tested; the caller passes the resolved value on to
|
|
322
|
+
* `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
|
|
323
|
+
* combined with a contract flag — that conflict is EXPLICIT and is reported).
|
|
324
|
+
*/
|
|
325
|
+
export function resolveImplicit(opts) {
|
|
326
|
+
if (opts.implicit !== undefined)
|
|
327
|
+
return opts.implicit;
|
|
328
|
+
if (hasText(opts.goal))
|
|
329
|
+
return false;
|
|
330
|
+
return opts.configGoalMode === "implicit";
|
|
331
|
+
}
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1219,6 +1219,68 @@ export declare const CODE_NOT_ENABLED = "code_not_enabled";
|
|
|
1219
1219
|
* for membership/transport failures.
|
|
1220
1220
|
*/
|
|
1221
1221
|
export declare function getCodeEntitlement(orgId?: string): Promise<CodeEntitlement>;
|
|
1222
|
+
/** The gateway's sealed contract for a conversation (absent arrays are `[]`). */
|
|
1223
|
+
export interface AgentSessionSealed {
|
|
1224
|
+
contract: {
|
|
1225
|
+
goal_id?: string;
|
|
1226
|
+
statement: string;
|
|
1227
|
+
requirements: string[];
|
|
1228
|
+
allowed_paths: string[];
|
|
1229
|
+
forbidden_paths: string[];
|
|
1230
|
+
max_turns?: number;
|
|
1231
|
+
budget_usd?: number;
|
|
1232
|
+
stall_turns?: number;
|
|
1233
|
+
};
|
|
1234
|
+
/** The gateway's hash of the sealed contract — NOT the CLI's launch-time sha. */
|
|
1235
|
+
hash: string;
|
|
1236
|
+
sealed_at: string;
|
|
1237
|
+
}
|
|
1238
|
+
/** The latest goal-judge verdict stored for the conversation. */
|
|
1239
|
+
export interface AgentSessionVerdict {
|
|
1240
|
+
turn: number;
|
|
1241
|
+
verdict: "aligned" | "drifting" | "violation";
|
|
1242
|
+
reason: string;
|
|
1243
|
+
at: string;
|
|
1244
|
+
streak_start?: number;
|
|
1245
|
+
}
|
|
1246
|
+
export interface AgentSessionGuardrails {
|
|
1247
|
+
ceilings: {
|
|
1248
|
+
/** null = inactive */
|
|
1249
|
+
max_turns: number | null;
|
|
1250
|
+
budget_usd: number | null;
|
|
1251
|
+
stall_turns: number | null;
|
|
1252
|
+
};
|
|
1253
|
+
state: {
|
|
1254
|
+
turns: number;
|
|
1255
|
+
spend_usd: number;
|
|
1256
|
+
stall_run: number;
|
|
1257
|
+
fired: {
|
|
1258
|
+
turns?: boolean;
|
|
1259
|
+
budget?: boolean;
|
|
1260
|
+
stall?: boolean;
|
|
1261
|
+
};
|
|
1262
|
+
} | null;
|
|
1263
|
+
}
|
|
1264
|
+
/** `GET /v1/agent/sessions/{conversationId}` 200 body. Nulls mean "nothing stored" — absence is data. */
|
|
1265
|
+
export interface AgentSessionResponse {
|
|
1266
|
+
conversation_id: string;
|
|
1267
|
+
agent: {
|
|
1268
|
+
id: string;
|
|
1269
|
+
name: string | null;
|
|
1270
|
+
};
|
|
1271
|
+
org_id: string;
|
|
1272
|
+
sealed: AgentSessionSealed | null;
|
|
1273
|
+
verdict: AgentSessionVerdict | null;
|
|
1274
|
+
turn: number | null;
|
|
1275
|
+
guardrails: AgentSessionGuardrails | null;
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* The live goal state of one `mnemom agent` session, read through mnemom-api
|
|
1279
|
+
* (org-scoped; the api proxies the gateway's internal read). Non-OK responses
|
|
1280
|
+
* keep the server's `code` + `details` (`agent_not_found`, `gateway_unavailable`)
|
|
1281
|
+
* so `mnemom agent show` can name the reason.
|
|
1282
|
+
*/
|
|
1283
|
+
export declare function getAgentSession(conversationId: string, agentId: string, orgId: string): Promise<AgentSessionResponse>;
|
|
1222
1284
|
/** `POST /v1/invites/accept` 200 body. Never carries an amount — only whether the
|
|
1223
1285
|
* starter grant fired or was deferred for a later backfill. */
|
|
1224
1286
|
export interface AcceptProductInviteResult {
|
package/dist/lib/api.js
CHANGED
|
@@ -1538,6 +1538,17 @@ export async function getCodeEntitlement(orgId) {
|
|
|
1538
1538
|
const q = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
|
|
1539
1539
|
return codeFetch(`/v1/code/entitlement${q}`, {}, "Could not check Mnemom Agent access");
|
|
1540
1540
|
}
|
|
1541
|
+
/**
|
|
1542
|
+
* The live goal state of one `mnemom agent` session, read through mnemom-api
|
|
1543
|
+
* (org-scoped; the api proxies the gateway's internal read). Non-OK responses
|
|
1544
|
+
* keep the server's `code` + `details` (`agent_not_found`, `gateway_unavailable`)
|
|
1545
|
+
* so `mnemom agent show` can name the reason.
|
|
1546
|
+
*/
|
|
1547
|
+
export async function getAgentSession(conversationId, agentId, orgId) {
|
|
1548
|
+
// `org_id` is the org the launch resolved (the ledger record's), so an agent that
|
|
1549
|
+
// lives in a non-primary org resolves instead of 404ing against the caller's primary org.
|
|
1550
|
+
return codeFetch(`/v1/agent/sessions/${encodeURIComponent(conversationId)}?agent_id=${encodeURIComponent(agentId)}&org_id=${encodeURIComponent(orgId)}`, {}, "Could not read the session's live state");
|
|
1551
|
+
}
|
|
1541
1552
|
/** Redeem a `pinv_…` product invite (a Mnemom Agent invitation) for the signed-in user. */
|
|
1542
1553
|
export async function acceptProductInvite(token) {
|
|
1543
1554
|
return codeFetch("/v1/invites/accept", { method: "POST", body: JSON.stringify({ token }) }, "Could not redeem the invitation");
|
package/dist/lib/auth.js
CHANGED
|
@@ -117,7 +117,14 @@ export async function getAccessToken() {
|
|
|
117
117
|
if (auth.expiresAt > now + 60) {
|
|
118
118
|
return auth.accessToken;
|
|
119
119
|
}
|
|
120
|
-
// Auto-refresh
|
|
120
|
+
// Auto-refresh. A *thrown* error from refreshStoredTokens() (transient
|
|
121
|
+
// exhaustion / non-revoke 4xx from oauthRefreshTokens(), MNE-7394) is
|
|
122
|
+
// intentionally allowed to propagate here rather than being coerced to the
|
|
123
|
+
// `null` re-login signal — a momentary AS outage must not masquerade as
|
|
124
|
+
// "credentials revoked". Only a genuine invalid_grant/invalid_client returns
|
|
125
|
+
// null (see refreshStoredTokens). Refining caller-side handling (distinguish
|
|
126
|
+
// propagate-vs-degrade) is tracked under MNE-7394's parent lane card
|
|
127
|
+
// (keith/oauth-token-lifetime).
|
|
121
128
|
const refreshed = await refreshStoredTokens(auth);
|
|
122
129
|
if (refreshed)
|
|
123
130
|
return refreshed.accessToken;
|
|
@@ -139,6 +146,9 @@ export async function forceRefreshAccessToken() {
|
|
|
139
146
|
const auth = getAuthInfo();
|
|
140
147
|
if (!auth?.refreshToken)
|
|
141
148
|
return null;
|
|
149
|
+
// As in getAccessToken, a thrown transient-exhaustion / non-revoke error from
|
|
150
|
+
// refreshStoredTokens() propagates rather than being coerced to null (MNE-7394;
|
|
151
|
+
// caller-side refinement tracked under keith/oauth-token-lifetime).
|
|
142
152
|
const refreshed = await refreshStoredTokens(auth);
|
|
143
153
|
return refreshed?.accessToken ?? null;
|
|
144
154
|
}
|
|
@@ -226,6 +236,16 @@ export async function loginWithDeviceFlow() {
|
|
|
226
236
|
* Refresh the stored tokens via the OAuth refresh_token grant. Requires both a
|
|
227
237
|
* refresh token and the client_id they were issued to; returns null (rather
|
|
228
238
|
* than throwing) when refresh isn't possible so callers degrade to re-login.
|
|
239
|
+
*
|
|
240
|
+
* Note: oauthRefreshTokens() returns null ONLY on a definitive revoke
|
|
241
|
+
* (invalid_grant/invalid_client) or missing credentials; on transient
|
|
242
|
+
* exhaustion (repeated 5xx / network faults) or any other non-revoke 4xx it
|
|
243
|
+
* *throws* (MNE-7394 / R6). That thrown error is intentionally allowed to
|
|
244
|
+
* propagate through this function to its callers rather than being coerced to
|
|
245
|
+
* null, so a momentary blip never presents as "credentials revoked". Refining
|
|
246
|
+
* the caller-side handling (propagate-vs-degrade in
|
|
247
|
+
* getAccessToken/forceRefreshAccessToken) is tracked under MNE-7394's parent
|
|
248
|
+
* lane card (keith/oauth-token-lifetime).
|
|
229
249
|
*/
|
|
230
250
|
async function refreshStoredTokens(auth) {
|
|
231
251
|
if (!auth.refreshToken || !auth.clientId)
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -37,3 +37,9 @@ export declare const fmt: {
|
|
|
37
37
|
*/
|
|
38
38
|
badge(text: string, color?: BadgeColor): string;
|
|
39
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Human-friendly age of an ISO timestamp relative to now — "<1m ago", "42m ago",
|
|
42
|
+
* "3h 5m ago", "2d 4h ago"; a timestamp ahead of the clock reads "in the future?".
|
|
43
|
+
* Shared by `mnemom validate` and `mnemom agent sessions` / `show`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatRelativeAge(startedAt: string): string;
|
package/dist/lib/format.js
CHANGED
|
@@ -64,3 +64,23 @@ export const fmt = {
|
|
|
64
64
|
return colorFn(`[${text}]`);
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Human-friendly age of an ISO timestamp relative to now — "<1m ago", "42m ago",
|
|
69
|
+
* "3h 5m ago", "2d 4h ago"; a timestamp ahead of the clock reads "in the future?".
|
|
70
|
+
* Shared by `mnemom validate` and `mnemom agent sessions` / `show`.
|
|
71
|
+
*/
|
|
72
|
+
export function formatRelativeAge(startedAt) {
|
|
73
|
+
const ageMs = Date.now() - Date.parse(startedAt);
|
|
74
|
+
if (ageMs < 0)
|
|
75
|
+
return "in the future?";
|
|
76
|
+
const m = Math.floor(ageMs / 60_000);
|
|
77
|
+
if (m < 1)
|
|
78
|
+
return "<1m ago";
|
|
79
|
+
if (m < 60)
|
|
80
|
+
return `${m}m ago`;
|
|
81
|
+
const h = Math.floor(m / 60);
|
|
82
|
+
if (h < 24)
|
|
83
|
+
return `${h}h ${m % 60}m ago`;
|
|
84
|
+
const d = Math.floor(h / 24);
|
|
85
|
+
return `${d}d ${h % 24}h ago`;
|
|
86
|
+
}
|