@mnemom/mnemom 0.17.33 → 0.17.34-next.2
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/dist/base-url-proxy.mjs +18 -4
- package/dist/commands/agent-config.js +5 -1
- package/dist/commands/agent-doctor.d.ts +9 -0
- package/dist/commands/agent-doctor.js +4 -0
- package/dist/commands/agent-session.d.ts +1 -2
- package/dist/commands/agent-session.js +1 -2
- package/dist/commands/agent-setup.d.ts +6 -0
- package/dist/commands/agent-setup.js +12 -1
- package/dist/commands/agent-share-render.d.ts +1 -1
- package/dist/commands/agent-share-render.js +1 -1
- package/dist/commands/agent.d.ts +26 -0
- package/dist/commands/agent.js +228 -53
- package/dist/commands/durable-doctor.d.ts +50 -0
- package/dist/commands/durable-doctor.js +186 -0
- package/dist/commands/durable-lock.d.ts +254 -0
- package/dist/commands/durable-lock.js +595 -0
- package/dist/commands/durable-sessions.d.ts +87 -0
- package/dist/commands/durable-sessions.js +303 -0
- package/dist/commands/durable-verbs.d.ts +116 -0
- package/dist/commands/durable-verbs.js +393 -0
- package/dist/commands/protection.js +1 -1
- package/dist/commands/scan.d.ts +97 -0
- package/dist/commands/scan.js +238 -0
- package/dist/durable/flock.d.ts +26 -0
- package/dist/durable/flock.js +80 -0
- package/dist/durable/launch-spec.d.ts +7 -1
- package/dist/durable/launch-spec.js +10 -3
- package/dist/durable/seat-proxy-entry.d.ts +11 -0
- package/dist/durable/seat-proxy-entry.js +25 -0
- package/dist/durable/seat-proxy.d.ts +107 -0
- package/dist/durable/seat-proxy.js +599 -0
- package/dist/durable/seat-socket.d.ts +46 -0
- package/dist/durable/seat-socket.js +179 -0
- package/dist/durable/seat-spec-validator.d.ts +28 -0
- package/dist/durable/seat-spec-validator.js +59 -0
- package/dist/durable/test-mode.mjs +81 -0
- package/dist/durable/turn-chain.d.ts +55 -0
- package/dist/durable/turn-chain.js +178 -0
- package/dist/host/auto-continue.d.ts +67 -0
- package/dist/host/auto-continue.js +118 -0
- package/dist/host/bootid.d.ts +13 -0
- package/dist/host/bootid.js +41 -0
- package/dist/host/client.d.ts +123 -0
- package/dist/host/client.js +171 -0
- package/dist/host/commands.d.ts +2 -0
- package/dist/host/commands.js +122 -0
- package/dist/host/control.d.ts +33 -0
- package/dist/host/control.js +144 -0
- package/dist/host/daemon.d.ts +217 -0
- package/dist/host/daemon.js +606 -0
- package/dist/host/gate.d.ts +46 -0
- package/dist/host/gate.js +82 -0
- package/dist/host/grant.d.ts +24 -0
- package/dist/host/grant.js +24 -0
- package/dist/host/instance.d.ts +35 -0
- package/dist/host/instance.js +62 -0
- package/dist/host/integrity.d.ts +35 -0
- package/dist/host/integrity.js +115 -0
- package/dist/host/launchd.d.ts +16 -0
- package/dist/host/launchd.js +45 -0
- package/dist/host/lock.d.ts +20 -0
- package/dist/host/lock.js +63 -0
- package/dist/host/owner.d.ts +55 -0
- package/dist/host/owner.js +121 -0
- package/dist/host/plist.d.ts +16 -0
- package/dist/host/plist.js +85 -0
- package/dist/host/procs.d.ts +27 -0
- package/dist/host/procs.js +105 -0
- package/dist/host/run.d.ts +66 -0
- package/dist/host/run.js +314 -0
- package/dist/host/runtime.d.ts +8 -0
- package/dist/host/runtime.js +22 -0
- package/dist/host/s1-names.d.ts +22 -0
- package/dist/host/s1-names.js +21 -0
- package/dist/host/seat-credential.d.ts +27 -0
- package/dist/host/seat-credential.js +41 -0
- package/dist/host/seat-launch-main.d.ts +1 -0
- package/dist/host/seat-launch-main.js +59 -0
- package/dist/host/seat-launch.d.ts +116 -0
- package/dist/host/seat-launch.js +302 -0
- package/dist/host/seat.d.ts +48 -0
- package/dist/host/seat.js +149 -0
- package/dist/host/state.d.ts +33 -0
- package/dist/host/state.js +117 -0
- package/dist/host/status.d.ts +101 -0
- package/dist/host/status.js +307 -0
- package/dist/host/tmux.d.ts +56 -0
- package/dist/host/tmux.js +182 -0
- package/dist/host/transcript.d.ts +30 -0
- package/dist/host/transcript.js +130 -0
- package/dist/host/verbs.d.ts +50 -0
- package/dist/host/verbs.js +271 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +232 -35
- package/dist/lib/agent.d.ts +8 -26
- package/dist/lib/agent.js +11 -26
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +8 -0
- package/dist/lib/claude-plugin.d.ts +11 -2
- package/dist/lib/claude-plugin.js +10 -1
- package/dist/lib/durable/api.d.ts +136 -0
- package/dist/lib/durable/api.js +220 -0
- package/dist/lib/durable/codes.d.ts +8 -0
- package/dist/lib/durable/codes.js +2 -0
- package/dist/lib/durable/first-turn.d.ts +50 -0
- package/dist/lib/durable/first-turn.js +102 -0
- package/dist/lib/durable/gate.d.ts +48 -0
- package/dist/lib/durable/gate.js +122 -0
- package/dist/lib/durable/handover.d.ts +101 -0
- package/dist/lib/durable/handover.js +175 -0
- package/dist/lib/durable/launch.d.ts +182 -0
- package/dist/lib/durable/launch.js +443 -0
- package/dist/lib/durable/launching.d.ts +15 -0
- package/dist/lib/durable/launching.js +70 -0
- package/dist/lib/durable/live.d.ts +194 -0
- package/dist/lib/durable/live.js +635 -0
- package/dist/lib/durable/refusal.d.ts +57 -0
- package/dist/lib/durable/refusal.js +78 -0
- package/dist/lib/durable/view.d.ts +83 -0
- package/dist/lib/durable/view.js +244 -0
- package/dist/lib/experimental.d.ts +0 -9
- package/dist/lib/experimental.js +18 -17
- package/dist/lib/launcher-config.d.ts +7 -0
- package/dist/lib/launcher-config.js +21 -0
- package/dist/lib/scan/caps.d.ts +35 -0
- package/dist/lib/scan/caps.js +67 -0
- package/dist/lib/scan/credential.d.ts +37 -0
- package/dist/lib/scan/credential.js +104 -0
- package/dist/lib/scan/failure.d.ts +10 -0
- package/dist/lib/scan/failure.js +43 -0
- package/dist/lib/scan/pr-source.d.ts +57 -0
- package/dist/lib/scan/pr-source.js +130 -0
- package/dist/lib/scan/record.d.ts +49 -0
- package/dist/lib/scan/record.js +85 -0
- package/dist/lib/scan/report-email.d.ts +92 -0
- package/dist/lib/scan/report-email.js +144 -0
- package/dist/lib/scan/report.d.ts +13 -0
- package/dist/lib/scan/report.js +30 -0
- package/dist/lib/scan/scan.d.ts +64 -0
- package/dist/lib/scan/scan.js +182 -0
- package/dist/lib/scan/share.d.ts +38 -0
- package/dist/lib/scan/share.js +47 -0
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
- package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
- package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
- package/dist/lib/scan/spend/anthropic-usage.js +244 -0
- package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
- package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
- package/dist/lib/scan/spend/prices.d.ts +33 -0
- package/dist/lib/scan/spend/prices.js +57 -0
- package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
- package/dist/lib/scan/spend/spend-estimate.js +70 -0
- package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
- package/dist/lib/scan/spend/spend-tiers.js +42 -0
- package/dist/lib/scan/spend/token-rate.d.ts +30 -0
- package/dist/lib/scan/spend/token-rate.js +54 -0
- package/dist/lib/session-key.d.ts +41 -0
- package/dist/lib/session-key.js +124 -0
- package/dist/lib/subscription-login.d.ts +15 -0
- package/dist/lib/subscription-login.js +28 -1
- package/dist/rc-key-401.mjs +75 -0
- package/dist/rc-proxy.mjs +221 -25
- package/dist/vendor/durable-contract/index.js +240 -18
- package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
- package/dist/vendor/shared/durable-contract/admission.js +533 -0
- package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
- package/dist/vendor/shared/durable-contract/arm.js +133 -0
- package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/canonical.js +98 -0
- package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
- package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
- package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
- package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
- package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
- package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
- package/dist/vendor/shared/durable-contract/codes.js +475 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
- package/dist/vendor/shared/durable-contract/constants.js +179 -0
- package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/doors.js +249 -0
- package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
- package/dist/vendor/shared/durable-contract/envelope.js +317 -0
- package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
- package/dist/vendor/shared/durable-contract/events.js +630 -0
- package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
- package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
- package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
- package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
- package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
- package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
- package/dist/vendor/shared/durable-contract/index.js +37 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
- package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
- package/dist/vendor/shared/durable-contract/keys.js +66 -0
- package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
- package/dist/vendor/shared/durable-contract/lock.js +151 -0
- package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
- package/dist/vendor/shared/durable-contract/names.js +78 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
- package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
- package/dist/vendor/shared/durable-contract/paths.js +35 -0
- package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
- package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
- package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
- package/dist/vendor/shared/durable-contract/routes.js +315 -0
- package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
- package/dist/vendor/shared/durable-contract/schema.js +441 -0
- package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
- package/dist/vendor/shared/durable-contract/seal.js +99 -0
- package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
- package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
- package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
- package/dist/vendor/shared/durable-contract/session-id.js +58 -0
- package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
- package/dist/vendor/shared/durable-contract/spec.js +286 -0
- package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
- package/dist/vendor/shared/durable-contract/states.js +218 -0
- package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
- package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
- package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
- package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
- package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
- package/dist/vendor/shared/durable-contract/turn.js +81 -0
- package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
- package/dist/vendor/shared/durable-contract/vectors.js +208 -0
- package/package.json +4 -3
- package/plugins/integrity-manifest.json +35 -0
- package/plugins/mnemom/hooks/hooks.json +15 -1
- package/plugins/mnemom/hooks/register.tsx +60 -2
- package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
- package/dist/commands/agent-implicit-notice.d.ts +0 -31
- package/dist/commands/agent-implicit-notice.js +0 -73
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// MNE-9268: tokens-per-changed-line and its spread, DERIVED from our own agents' PRs whose run cost is
|
|
2
|
+
// known (cost-corpus.v1.json, built by build-cost-corpus.mjs from the ADW "Run cost" PR comments).
|
|
3
|
+
// Nothing here is a typed-in rate: change the fixture and the rate moves, and token-rate.test.ts pins
|
|
4
|
+
// the derivation. Tokens are price-equivalent: cost / (our model's blended list price per token).
|
|
5
|
+
import rawCorpus from "./cost-corpus.v1.js";
|
|
6
|
+
import { TOOL_PRICES, blendedUsdPerToken } from "./prices.js";
|
|
7
|
+
/** Rows needed before the rate is trusted at all (data sufficiency, not a group size). */
|
|
8
|
+
export const MIN_CORPUS_ROWS = 5;
|
|
9
|
+
/** Our agents ran on Sonnet-family models in every corpus row; price their tokens as that. */
|
|
10
|
+
const BASIS_USD_PER_TOKEN = blendedUsdPerToken(TOOL_PRICES.claude_code.low);
|
|
11
|
+
const isCount = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
12
|
+
function validRow(r) {
|
|
13
|
+
if (typeof r !== "object" || r === null)
|
|
14
|
+
return false;
|
|
15
|
+
const o = r;
|
|
16
|
+
return (isCount(o.additions) &&
|
|
17
|
+
isCount(o.deletions) &&
|
|
18
|
+
o.additions + o.deletions > 0 &&
|
|
19
|
+
isCount(o.cost_usd) &&
|
|
20
|
+
o.cost_usd > 0);
|
|
21
|
+
}
|
|
22
|
+
/** Linear-interpolated quantile of a sorted array. */
|
|
23
|
+
export function quantile(sorted, q) {
|
|
24
|
+
if (sorted.length === 0)
|
|
25
|
+
return NaN;
|
|
26
|
+
const pos = (sorted.length - 1) * q;
|
|
27
|
+
const lo = Math.floor(pos);
|
|
28
|
+
const hi = Math.ceil(pos);
|
|
29
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Derive the rate from corpus rows: per-row tokens/line, then the 25th, 50th and 75th percentile.
|
|
33
|
+
* Rows that fail validation are dropped (not repaired); fewer than MIN_CORPUS_ROWS valid rows
|
|
34
|
+
* returns null, never a rate built on a handful.
|
|
35
|
+
*/
|
|
36
|
+
export function deriveTokenRate(rows) {
|
|
37
|
+
if (!Array.isArray(rows))
|
|
38
|
+
return null;
|
|
39
|
+
const per = rows
|
|
40
|
+
.filter(validRow)
|
|
41
|
+
.map((r) => r.cost_usd / (r.additions + r.deletions) / BASIS_USD_PER_TOKEN)
|
|
42
|
+
.sort((a, b) => a - b);
|
|
43
|
+
if (per.length < MIN_CORPUS_ROWS)
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
low: quantile(per, 0.25),
|
|
47
|
+
median: quantile(per, 0.5),
|
|
48
|
+
high: quantile(per, 0.75),
|
|
49
|
+
rows: per.length,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export const CORPUS_AS_OF = rawCorpus.as_of;
|
|
53
|
+
/** The shipped rate: derived once from the committed fixture. */
|
|
54
|
+
export const DERIVED_TOKEN_RATE = deriveTokenRate(rawCorpus.rows);
|
|
@@ -66,3 +66,44 @@ export declare function mintSessionGatewayKey(deps: SessionKeyDeps, args: {
|
|
|
66
66
|
* one. Never called with the owner's own key id.
|
|
67
67
|
*/
|
|
68
68
|
export declare function revokeSessionGatewayKey(deps: SessionKeyDeps, keyId: string): Promise<void>;
|
|
69
|
+
/** When to renew a key expiring at `expiresAt`, measured from `now` (ms). */
|
|
70
|
+
export declare function renewDelayMs(expiresAt: string, now: number): number;
|
|
71
|
+
/** What {@link mintSessionGatewayKey} mints for: the same arguments at launch and at each renewal. */
|
|
72
|
+
export interface SessionKeyMintArgs {
|
|
73
|
+
agentId: string;
|
|
74
|
+
orgId?: string;
|
|
75
|
+
sessionId?: string;
|
|
76
|
+
}
|
|
77
|
+
export interface SessionKeyRenewalOptions {
|
|
78
|
+
/** `say` is where renewal events go: the proxy log, never the terminal the session draws on. */
|
|
79
|
+
deps: SessionKeyDeps;
|
|
80
|
+
mintArgs: SessionKeyMintArgs;
|
|
81
|
+
initial: {
|
|
82
|
+
keyId: string;
|
|
83
|
+
expiresAt: string;
|
|
84
|
+
};
|
|
85
|
+
/** Hand the new key to the running proxy. Throws when it cannot (the pipe is gone). */
|
|
86
|
+
deliver: (minted: MintedSessionGatewayKey) => void;
|
|
87
|
+
/** Tell the proxy its re-mint request failed, so it stops waiting and answers the 401. */
|
|
88
|
+
refuse: (reason: string) => void;
|
|
89
|
+
now?: () => number;
|
|
90
|
+
}
|
|
91
|
+
export interface SessionKeyRenewal {
|
|
92
|
+
/**
|
|
93
|
+
* Re-mint now. `rejected` is the proxy's request after a gateway 401, naming the key id it sent;
|
|
94
|
+
* a request about a key that is no longer current is a no-op (the proxy already has the newer
|
|
95
|
+
* one). Resolves true when a new key was delivered. Never rejects.
|
|
96
|
+
*/
|
|
97
|
+
renew(reason: "scheduled" | "rejected", rejectedKeyId?: string): Promise<boolean>;
|
|
98
|
+
/** The key the proxy holds now: the one to revoke on exit. */
|
|
99
|
+
currentKeyId(): string;
|
|
100
|
+
stop(): void;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Keep the proxy's session key fresh: renew at ~80% of its remaining lifetime (floor 60s), retry a
|
|
104
|
+
* failed renewal every 5 minutes, and after each swap revoke the old key (best effort, like the
|
|
105
|
+
* exit revoke). Every new key goes through {@link mintSessionGatewayKey}, so a renewal is held to
|
|
106
|
+
* the same fail-closed checks as the launch: a key that is not exactly a `['gateway']` session key
|
|
107
|
+
* is never delivered. Logs key ids and expiry times only, never a key.
|
|
108
|
+
*/
|
|
109
|
+
export declare function startSessionKeyRenewal(opts: SessionKeyRenewalOptions): SessionKeyRenewal;
|
package/dist/lib/session-key.js
CHANGED
|
@@ -99,3 +99,127 @@ export async function revokeSessionGatewayKey(deps, keyId) {
|
|
|
99
99
|
deps.say(`mnemom agent: could not revoke the session key (non-fatal — it expires on its own within 24h): ${err instanceof Error ? err.message : String(err)}`);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// Renewal for long-lived Remote Control sessions
|
|
104
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
105
|
+
//
|
|
106
|
+
// The minted key is 24h-TTL'd, and a `--remote-control` seat outlives that: before this, every
|
|
107
|
+
// model turn failed with the gateway's `401 Invalid Mnemom API key` exactly 24h after launch. The
|
|
108
|
+
// launcher (the CLI PARENT, which holds ambient auth) therefore re-mints before expiry and hands
|
|
109
|
+
// the new key to the running proxy over its control pipe — never argv, env or disk — then revokes
|
|
110
|
+
// the old one. The proxy can also ask for one immediate re-mint when the gateway rejects its key.
|
|
111
|
+
/** Renew once this share of the key's remaining lifetime has passed. */
|
|
112
|
+
const RENEW_AT_FRACTION = 0.8;
|
|
113
|
+
/** Never schedule a renewal sooner than this (a key that is nearly or already expired). */
|
|
114
|
+
const RENEW_FLOOR_MS = 60_000;
|
|
115
|
+
/** The lifetime assumed when `expiresAt` does not parse: the route's documented 24h. */
|
|
116
|
+
const ASSUMED_TTL_MS = 24 * 60 * 60 * 1000;
|
|
117
|
+
/** After a failed renewal, try again this much later. */
|
|
118
|
+
const RENEW_RETRY_MS = 5 * 60 * 1000;
|
|
119
|
+
/**
|
|
120
|
+
* A re-mint the proxy asks for (its key was rejected) is refused if one was attempted this
|
|
121
|
+
* recently, so a gateway that keeps rejecting fresh keys cannot turn every request into a mint.
|
|
122
|
+
*/
|
|
123
|
+
const REJECTED_COOLDOWN_MS = 60_000;
|
|
124
|
+
/** setTimeout's ceiling (a larger delay fires at once). */
|
|
125
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
126
|
+
/** When to renew a key expiring at `expiresAt`, measured from `now` (ms). */
|
|
127
|
+
export function renewDelayMs(expiresAt, now) {
|
|
128
|
+
const exp = Date.parse(expiresAt);
|
|
129
|
+
const left = Number.isFinite(exp) ? exp - now : ASSUMED_TTL_MS;
|
|
130
|
+
return Math.min(MAX_TIMER_MS, Math.max(RENEW_FLOOR_MS, Math.floor(left * RENEW_AT_FRACTION)));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Keep the proxy's session key fresh: renew at ~80% of its remaining lifetime (floor 60s), retry a
|
|
134
|
+
* failed renewal every 5 minutes, and after each swap revoke the old key (best effort, like the
|
|
135
|
+
* exit revoke). Every new key goes through {@link mintSessionGatewayKey}, so a renewal is held to
|
|
136
|
+
* the same fail-closed checks as the launch: a key that is not exactly a `['gateway']` session key
|
|
137
|
+
* is never delivered. Logs key ids and expiry times only, never a key.
|
|
138
|
+
*/
|
|
139
|
+
export function startSessionKeyRenewal(opts) {
|
|
140
|
+
const now = opts.now ?? Date.now;
|
|
141
|
+
const say = opts.deps.say;
|
|
142
|
+
let current = { ...opts.initial };
|
|
143
|
+
let timer;
|
|
144
|
+
let inflight;
|
|
145
|
+
let lastAttempt = Number.NEGATIVE_INFINITY;
|
|
146
|
+
let stopped = false;
|
|
147
|
+
const schedule = (ms) => {
|
|
148
|
+
if (stopped)
|
|
149
|
+
return;
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
timer = setTimeout(() => void renew("scheduled"), ms);
|
|
152
|
+
timer.unref?.();
|
|
153
|
+
};
|
|
154
|
+
const attempt = async (reason) => {
|
|
155
|
+
lastAttempt = now();
|
|
156
|
+
let minted;
|
|
157
|
+
try {
|
|
158
|
+
minted = await mintSessionGatewayKey(opts.deps, opts.mintArgs);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
// mintSessionGatewayKey always throws an Error wrapping the mint's own failure in `cause`,
|
|
162
|
+
// with launch-refusal wording that is wrong mid-session: log the cause when it is an Error.
|
|
163
|
+
const e = err;
|
|
164
|
+
const msg = e.cause instanceof Error ? e.cause.message : e.message;
|
|
165
|
+
say(`session key renewal (${reason}) FAILED: ${msg}; keeping id=${current.keyId} expires=${current.expiresAt}, retrying in ${RENEW_RETRY_MS / 1000}s`);
|
|
166
|
+
schedule(RENEW_RETRY_MS);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (stopped) {
|
|
170
|
+
await revokeSessionGatewayKey(opts.deps, minted.keyId); // nobody will ever use it
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
opts.deliver(minted);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
say(`session key renewal (${reason}): could not hand id=${minted.keyId} to the proxy (${err instanceof Error ? err.message : String(err)}); revoking it and keeping id=${current.keyId}`);
|
|
178
|
+
await revokeSessionGatewayKey(opts.deps, minted.keyId);
|
|
179
|
+
schedule(RENEW_RETRY_MS);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const old = current;
|
|
183
|
+
current = { keyId: minted.keyId, expiresAt: minted.expiresAt };
|
|
184
|
+
say(`session key renewed (${reason}): id=${current.keyId} expires=${current.expiresAt}; revoking id=${old.keyId}`);
|
|
185
|
+
schedule(renewDelayMs(current.expiresAt, now()));
|
|
186
|
+
await revokeSessionGatewayKey(opts.deps, old.keyId);
|
|
187
|
+
return true;
|
|
188
|
+
};
|
|
189
|
+
const renew = (reason, rejectedKeyId) => {
|
|
190
|
+
if (stopped)
|
|
191
|
+
return Promise.resolve(false);
|
|
192
|
+
if (reason === "rejected") {
|
|
193
|
+
// The proxy sent a key that is no longer current: the newer one is already on its way.
|
|
194
|
+
if (rejectedKeyId && rejectedKeyId !== current.keyId)
|
|
195
|
+
return Promise.resolve(true);
|
|
196
|
+
if (!inflight && now() - lastAttempt < REJECTED_COOLDOWN_MS) {
|
|
197
|
+
say(`session key id=${current.keyId} rejected by the gateway; not re-minting (last attempt under ${REJECTED_COOLDOWN_MS / 1000}s ago)`);
|
|
198
|
+
opts.refuse("cooldown");
|
|
199
|
+
return Promise.resolve(false);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// One mint at a time: a rejection that arrives mid-renewal waits on that renewal.
|
|
203
|
+
inflight ??= attempt(reason).finally(() => {
|
|
204
|
+
inflight = undefined;
|
|
205
|
+
});
|
|
206
|
+
const p = inflight;
|
|
207
|
+
if (reason === "rejected") {
|
|
208
|
+
void p.then((ok) => {
|
|
209
|
+
if (!ok)
|
|
210
|
+
opts.refuse("mint failed");
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return p;
|
|
214
|
+
};
|
|
215
|
+
// The launch key's id and expiry are logged by the proxy at startup.
|
|
216
|
+
schedule(renewDelayMs(current.expiresAt, now()));
|
|
217
|
+
return {
|
|
218
|
+
renew,
|
|
219
|
+
currentKeyId: () => current.keyId,
|
|
220
|
+
stop: () => {
|
|
221
|
+
stopped = true;
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -79,6 +79,21 @@ export declare function resolveSubscriptionMnemomKey(deps: MnemomKeyDeps, purpos
|
|
|
79
79
|
* a non-entitled account to terminal; this is the fail-closed backstop.)
|
|
80
80
|
*/
|
|
81
81
|
export declare function subscriptionShape(shape: LaunchShape, shapeExplicit: boolean, rcAllowed: boolean, say: (line: string) => void): LaunchShape;
|
|
82
|
+
/**
|
|
83
|
+
* The coding agent's env for either auth arm: what `mnemom agent` launches claude with, and what a
|
|
84
|
+
* durable seat launches its claude with (host/seat-launch.ts, dogfood B6). With an Anthropic key:
|
|
85
|
+
* Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so it is force-unset;
|
|
86
|
+
* the real key rides in ANTHROPIC_CUSTOM_HEADERS (`headers`) and ANTHROPIC_AUTH_TOKEN is a fixed
|
|
87
|
+
* non-secret placeholder. Without one (a subscription launch) NO auth token is set: Claude Code
|
|
88
|
+
* then sends its own claude.ai login. Either way the owner's own Mnemom credential env is
|
|
89
|
+
* stripped (MNE-8245), as is MNEMOM_AGENT_ANTHROPIC_KEY (the key reaches the child only in the
|
|
90
|
+
* headers: C14, B6), and Claude Code's gateway hint headers are on (a value the owner set wins):
|
|
91
|
+
* the seat proxy chains each turn by x-claude-code-prompt-id, and without it a seat's first typed
|
|
92
|
+
* turn chained to the launch prompt (dogfood B8). Set here so the two callers cannot drift apart.
|
|
93
|
+
*/
|
|
94
|
+
export declare function launchChildEnv(base: NodeJS.ProcessEnv, door: string, headers: string, anthropicKey: string | undefined): NodeJS.ProcessEnv;
|
|
95
|
+
/** The fixed, non-secret ANTHROPIC_AUTH_TOKEN of an api-key launch (the key rides in a header). */
|
|
96
|
+
export declare const GATEWAY_AUTH_PLACEHOLDER = "mnemom-gateway";
|
|
82
97
|
/**
|
|
83
98
|
* The child env for a subscription launch: the gateway door, the Mnemom headers, and
|
|
84
99
|
* NOTHING that would make Claude Code send a key instead of its claude.ai login.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Pure apart from the injected deps, so every arm is unit-tested.
|
|
18
18
|
*/
|
|
19
|
-
import { stripOwnerCredentialEnv } from "./agent.js";
|
|
19
|
+
import { applyGatewayHintHeaders, stripOwnerCredentialEnv } from "./agent.js";
|
|
20
20
|
export const AGENT_AUTHS = ["api-key", "subscription"];
|
|
21
21
|
/**
|
|
22
22
|
* The retired `console` value (it read the key Claude Code's Anthropic Console /login
|
|
@@ -132,6 +132,32 @@ export function subscriptionShape(shape, shapeExplicit, rcAllowed, say) {
|
|
|
132
132
|
say("mnemom agent: subscription login runs terminal-only for now — launching in this terminal.");
|
|
133
133
|
return "terminal";
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The coding agent's env for either auth arm: what `mnemom agent` launches claude with, and what a
|
|
137
|
+
* durable seat launches its claude with (host/seat-launch.ts, dogfood B6). With an Anthropic key:
|
|
138
|
+
* Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so it is force-unset;
|
|
139
|
+
* the real key rides in ANTHROPIC_CUSTOM_HEADERS (`headers`) and ANTHROPIC_AUTH_TOKEN is a fixed
|
|
140
|
+
* non-secret placeholder. Without one (a subscription launch) NO auth token is set: Claude Code
|
|
141
|
+
* then sends its own claude.ai login. Either way the owner's own Mnemom credential env is
|
|
142
|
+
* stripped (MNE-8245), as is MNEMOM_AGENT_ANTHROPIC_KEY (the key reaches the child only in the
|
|
143
|
+
* headers: C14, B6), and Claude Code's gateway hint headers are on (a value the owner set wins):
|
|
144
|
+
* the seat proxy chains each turn by x-claude-code-prompt-id, and without it a seat's first typed
|
|
145
|
+
* turn chained to the launch prompt (dogfood B8). Set here so the two callers cannot drift apart.
|
|
146
|
+
*/
|
|
147
|
+
export function launchChildEnv(base, door, headers, anthropicKey) {
|
|
148
|
+
if (!anthropicKey)
|
|
149
|
+
return applyGatewayHintHeaders(subscriptionChildEnv(base, door, headers));
|
|
150
|
+
const env = { ...base };
|
|
151
|
+
delete env.ANTHROPIC_API_KEY;
|
|
152
|
+
delete env.MNEMOM_AGENT_ANTHROPIC_KEY;
|
|
153
|
+
env.ANTHROPIC_BASE_URL = door;
|
|
154
|
+
env.ANTHROPIC_AUTH_TOKEN = GATEWAY_AUTH_PLACEHOLDER;
|
|
155
|
+
env.ANTHROPIC_CUSTOM_HEADERS = headers;
|
|
156
|
+
stripOwnerCredentialEnv(env);
|
|
157
|
+
return applyGatewayHintHeaders(env);
|
|
158
|
+
}
|
|
159
|
+
/** The fixed, non-secret ANTHROPIC_AUTH_TOKEN of an api-key launch (the key rides in a header). */
|
|
160
|
+
export const GATEWAY_AUTH_PLACEHOLDER = "mnemom-gateway";
|
|
135
161
|
/**
|
|
136
162
|
* The child env for a subscription launch: the gateway door, the Mnemom headers, and
|
|
137
163
|
* NOTHING that would make Claude Code send a key instead of its claude.ai login.
|
|
@@ -139,6 +165,7 @@ export function subscriptionShape(shape, shapeExplicit, rcAllowed, say) {
|
|
|
139
165
|
export function subscriptionChildEnv(base, door, headers) {
|
|
140
166
|
const env = { ...base };
|
|
141
167
|
delete env.ANTHROPIC_API_KEY;
|
|
168
|
+
delete env.MNEMOM_AGENT_ANTHROPIC_KEY;
|
|
142
169
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
143
170
|
// The key already rides in the headers; the child process has no other use for it.
|
|
144
171
|
// MNE-8245 defense-in-depth: also strips MNEMOM_API_KEY/MNEMOM_TOKEN, in case the
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// rc-key-401 — the Remote Control proxy's handling of a gateway 401 it may be able to fix.
|
|
2
|
+
//
|
|
3
|
+
// A 401 on a buffered /v1/messages request may be the Mnemom session key expiring under a long
|
|
4
|
+
// session (rc-proxy.mjs, SESSION KEY RENEWAL). The proxy holds that 401 back from the client until
|
|
5
|
+
// the whole body is in, then asks isMnemomKeyRejection() whether a fresh key would fix it. Split out
|
|
6
|
+
// of rc-proxy.mjs (a script that reads stdin and listens at import) so each branch is unit-tested
|
|
7
|
+
// (rc-key-401.test.ts). Node ≥ 18, no deps.
|
|
8
|
+
import { Buffer } from "node:buffer";
|
|
9
|
+
import zlib from "node:zlib";
|
|
10
|
+
|
|
11
|
+
// A held-back 401 body (and its decompressed form) is read up to this size. The gateway's key
|
|
12
|
+
// rejection is a few hundred bytes; anything larger is not one and is streamed to the client as is.
|
|
13
|
+
export const KEY_401_BODY_MAX = 64 * 1024;
|
|
14
|
+
|
|
15
|
+
// How long a key-rejected request waits for the launcher's re-mint (its mint call is bounded at
|
|
16
|
+
// 10s): MNEMOM_RC_REMINT_WAIT_MS when set and non-empty ("0" included), else 20s.
|
|
17
|
+
export const REMINT_WAIT_DEFAULT_MS = 20_000;
|
|
18
|
+
export function remintWaitMs(env) {
|
|
19
|
+
const v = env.MNEMOM_RC_REMINT_WAIT_MS;
|
|
20
|
+
return v ? Number(v) : REMINT_WAIT_DEFAULT_MS;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Whether a held-back gateway 401 body says the Mnemom key itself was refused (unknown, revoked or
|
|
24
|
+
// expired): the only 401 a fresh key can fix. Anything it cannot decode is not one.
|
|
25
|
+
export function isMnemomKeyRejection(raw, enc, max = KEY_401_BODY_MAX) {
|
|
26
|
+
let text;
|
|
27
|
+
try {
|
|
28
|
+
const b =
|
|
29
|
+
enc === "gzip" || enc === "x-gzip"
|
|
30
|
+
? zlib.gunzipSync(raw, { maxOutputLength: max })
|
|
31
|
+
: enc === "deflate"
|
|
32
|
+
? zlib.inflateSync(raw, { maxOutputLength: max })
|
|
33
|
+
: enc === "br"
|
|
34
|
+
? zlib.brotliDecompressSync(raw, { maxOutputLength: max })
|
|
35
|
+
: raw;
|
|
36
|
+
text = b.toString("utf8");
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return /invalid[ _]mnemom[ _](api[ _])?key/i.test(text);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Hold the upstream response `ur` back from the client response `res`. Up to `max` bytes,
|
|
44
|
+
// `onBody(raw)` gets the whole body at its end and nothing has reached the client. Past `max` the
|
|
45
|
+
// body is not a key rejection: the status, headers and every byte so far go to the client, the rest
|
|
46
|
+
// streams through, and `onOverflow(bytes)` runs at the end. Every write first checks that the client
|
|
47
|
+
// is still there: a hang-up destroys `res` (and, via rc-proxy.mjs, the upstream request), and a
|
|
48
|
+
// chunk already in flight must not write to it.
|
|
49
|
+
export function holdBack401(ur, res, { max = KEY_401_BODY_MAX, onBody, onOverflow }) {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
let held = 0;
|
|
52
|
+
let overflow = false;
|
|
53
|
+
ur.on("data", (c) => {
|
|
54
|
+
if (overflow) {
|
|
55
|
+
if (!res.destroyed) res.write(c);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
chunks.push(c);
|
|
59
|
+
held += c.length;
|
|
60
|
+
if (held > max) {
|
|
61
|
+
overflow = true;
|
|
62
|
+
if (res.destroyed) return;
|
|
63
|
+
res.writeHead(ur.statusCode, ur.headers);
|
|
64
|
+
for (const x of chunks.splice(0)) res.write(x);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
ur.on("end", () => {
|
|
68
|
+
if (overflow) {
|
|
69
|
+
if (!res.destroyed) res.end();
|
|
70
|
+
onOverflow(held);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
onBody(Buffer.concat(chunks));
|
|
74
|
+
});
|
|
75
|
+
}
|