@mnemom/mnemom 0.17.38 → 0.17.39-next.1

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.
@@ -72,6 +72,8 @@ export async function agentRunCommand(flags, deps) {
72
72
  apiUrl: deps.apiUrl,
73
73
  requestUrl: plan.credential.requestUrl,
74
74
  requestToken: plan.credential.requestToken,
75
+ // Amendment 66: set by the action from the wake-up's adopt-ticket input; empty sends none.
76
+ adoptTicket: deps.env.MNEMOM_ADOPT_TICKET?.trim() || null,
75
77
  });
76
78
  if (!ex.ok) {
77
79
  // No job is held yet: fail closed whatever --fail-mode says (34.12 (6)).
@@ -43,7 +43,7 @@ import { getApiUrl, getGatewayUrl, getWebsiteUrl, MNEMOM_DIR } from "../lib/conf
43
43
  import { fmt } from "../lib/format.js";
44
44
  import { askYesNo, isInteractive } from "../lib/prompt.js";
45
45
  import { malformedAgentIdMessage, parseAgentRef, resolveAnthropicDoor, resolveGatewayHost, sanitizeAgentName, singleCellError, } from "../lib/agent.js";
46
- import { readMnemomKey, resolveAgentAuth, resolveSubscriptionMnemomKey, sayRetiredConsoleNotice, } from "../lib/subscription-login.js";
46
+ import { mnemomKeyRejectedFix, readMnemomKey, resolveAgentAuth, resolveSubscriptionMnemomKey, sayRetiredConsoleNotice, } from "../lib/subscription-login.js";
47
47
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
48
48
  import { IdentityRouteMissingError, resolveServedIdentity, resolveServedIdentityDetail, } from "../lib/agent-identity-route.js";
49
49
  import { readConfirmedAgent, readServedAgent, servedFingerprint, writeConfirmedAgent, writeServedAgent, } from "../lib/served-agent-cache.js";
@@ -431,7 +431,7 @@ export async function ensureClaimedServedAgent(opts) {
431
431
  }
432
432
  catch (err) {
433
433
  if (err instanceof MnemomKeyRejectedError) {
434
- say(fmt.warn("mnemom agent: the gateway rejected your stored Mnemom API key — launching without it (fix: `mnemom agent config` / re-run subscription setup, or unset MNEMOM_AGENT_MNEMOM_KEY)."));
434
+ say(fmt.warn(`mnemom agent: the gateway rejected your Mnemom API key — launching without it. ${mnemomKeyRejectedFix(getApiUrl(), process.env)}`));
435
435
  const retried = await ensureClaimedServedAgent({ ...opts, mnemomKey: undefined });
436
436
  return { ...retried, mnemomKeyRejected: true };
437
437
  }
@@ -560,6 +560,15 @@ async function resolveServedAgent(opts, fp) {
560
560
  if (opts.anthropicKey && opts.mnemomKey && isRejectedMnemomKey(err)) {
561
561
  throw new MnemomKeyRejectedError();
562
562
  }
563
+ // A subscription launch whose Mnemom key the gateway rejected (401, e.g. invalid_mnemom_key):
564
+ // most often a key saved for another cell. Name the cell and the exact fix.
565
+ if (!opts.anthropicKey &&
566
+ opts.mnemomKey &&
567
+ err instanceof MnemomApiError &&
568
+ err.effectiveStatus === 401) {
569
+ throw new UnclaimedAgentError(`mnemom agent: the gateway rejected the Mnemom API key this launch sent (${err.code ?? "401"}), so it can't confirm '${slug}' is claimed. ` +
570
+ mnemomKeyRejectedFix(getApiUrl(), process.env));
571
+ }
563
572
  if (err instanceof IdentityRouteMissingError) {
564
573
  throw new UnclaimedAgentError(`mnemom agent: ${err.message}, so it can't confirm '${slug}' is claimed. ` +
565
574
  "Launch with an Anthropic API key instead (`--auth api-key`), or try again once the gateway is updated.");
@@ -736,7 +745,7 @@ function pinRefusal(err, pin, slug) {
736
745
  }
737
746
  if (code === "mnemom_api_key_required" || status === 401) {
738
747
  return (`${head}: a pin needs a valid Mnemom API key (${code ?? "401"}). ` +
739
- "Set MNEMOM_AGENT_MNEMOM_KEY, or delete mnemom_api_key from ~/.mnemom/launcher.json so the next pinned launch mints a fresh one.");
748
+ mnemomKeyRejectedFix(getApiUrl(), process.env));
740
749
  }
741
750
  return `${head} (${describeApiError(err)}). A pinned launch never falls back to another agent.`;
742
751
  }
@@ -265,7 +265,7 @@ export declare function recordSessionLaunch(ctx: {
265
265
  compactAt: number | null;
266
266
  }): void;
267
267
  /** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
268
- export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
268
+ export declare function defaultMnemomKeyDeps(storePath?: string, apiUrl?: string): MnemomKeyDeps;
269
269
  /**
270
270
  * Resolve the Anthropic key with NO 1Password dependency (built for external
271
271
  * customer adoption), in order:
@@ -41,7 +41,7 @@ import { createApiKey } from "../lib/api.js";
41
41
  import { recordOrgFor } from "../lib/record-org.js";
42
42
  import { httpDoorAllowed } from "../durable/test-mode.mjs";
43
43
  import { defaultSessionKeyDeps, mintSessionGatewayKey, revokeSessionGatewayKey, startSessionKeyRenewal, } from "../lib/session-key.js";
44
- import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, sayRetiredConsoleNotice, readMnemomKey, resolveSubscriptionMnemomKey, launchChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
44
+ import { mnemomKeyStorePatch, storedMnemomKeyForCell, resolveAgentAuth, sayRetiredConsoleNotice, readMnemomKey, resolveSubscriptionMnemomKey, launchChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
45
45
  import { clearLaunching, liveLaunchingDeps, markLaunching } from "../lib/durable/launching.js";
46
46
  import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, resumedContextProfile, toLedgerContract, } from "../lib/agent-ledger.js";
47
47
  import { isLoggedIn } from "../lib/auth.js";
@@ -55,7 +55,7 @@ import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, }
55
55
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
56
56
  import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, isQuietPreambleExit, readComposedProtection, resolveDefaultSlug, resolvePinnedAgent, resolvePreflightedAccount, } from "./agent-setup.js";
57
57
  import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, resolveUsageSavings, HANDOFF_RC_KEY_ENV_NAME, HANDOFF_RC_MNEMOM_KEY_ENV_NAME, malformedAgentIdMessage, parseAgentRef, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, passthroughModel, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, liftPassthroughName, passthroughName, sanitizeAgentName, resumeCommand, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, stripOwnerCredentialEnv, } from "../lib/agent.js";
58
- import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
58
+ import { LAUNCHER_STORE_PATH, mergeStore, readStore } from "../lib/launcher-store.js";
59
59
  import { slackChannelArgs } from "../lib/slack-launch.js";
60
60
  import { appendPluginDirsEnv, applyPluginEnv, pluginEnabled, pluginPlanLine, resolvePluginDir, } from "../lib/claude-plugin.js";
61
61
  import { CLI_VERSION } from "../version.js";
@@ -1146,21 +1146,17 @@ export function recordSessionLaunch(ctx) {
1146
1146
  // Credential resolution (zero 1Password)
1147
1147
  // ───────────────────────────────────────────────────────────────────────────
1148
1148
  /** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
1149
- export function defaultMnemomKeyDeps(storePath = CODE_STORE) {
1149
+ export function defaultMnemomKeyDeps(storePath = CODE_STORE, apiUrl = getApiUrl()) {
1150
1150
  return {
1151
1151
  env: process.env,
1152
+ // The key is per cell (keyed by the API origin): a key minted on one cell is rejected by
1153
+ // another cell's gateway (401), so it is never read for, or sent to, a different cell.
1152
1154
  readStored: () => {
1153
- try {
1154
- const data = JSON.parse(readFileSync(storePath, "utf8"));
1155
- const v = data[MNEMOM_KEY_STORE_FIELD];
1156
- return typeof v === "string" ? stripCrlf(v) : undefined;
1157
- }
1158
- catch {
1159
- return undefined;
1160
- }
1155
+ const v = storedMnemomKeyForCell(readStore(storePath), apiUrl);
1156
+ return v === undefined ? undefined : stripCrlf(v);
1161
1157
  },
1162
1158
  mint: async () => (await createApiKey(`mnemom-agent ${hostname()}`, ["gateway"])).key,
1163
- store: (key) => mergeStore({ [MNEMOM_KEY_STORE_FIELD]: key }, storePath),
1159
+ store: (key) => mergeStore(mnemomKeyStorePatch(readStore(storePath), apiUrl, key), storePath),
1164
1160
  say,
1165
1161
  };
1166
1162
  }
@@ -30,10 +30,15 @@ export declare function actionsIdToken(fetch: FetchFn, requestUrl: string, reque
30
30
  ok: false;
31
31
  detail: string;
32
32
  }>;
33
- /** Fetch the ID token and exchange it for a one-lease executor key. */
33
+ /**
34
+ * Fetch the ID token and exchange it for a one-lease executor key. `adoptTicket` (Amendment 66) is
35
+ * the ticket the api's wake-up passed this run as the adopt-ticket input; it rides in the body
36
+ * beside the token, and like the token it never reaches a log line or an outcome.
37
+ */
34
38
  export declare function exchangeOidc(opts: {
35
39
  fetch: FetchFn;
36
40
  apiUrl: string;
37
41
  requestUrl: string;
38
42
  requestToken: string;
43
+ adoptTicket?: string | null;
39
44
  }): Promise<OidcExchange>;
@@ -54,7 +54,11 @@ export async function actionsIdToken(fetch, requestUrl, requestToken) {
54
54
  return { ok: false, detail: `ID token request failed: ${e.message}` };
55
55
  }
56
56
  }
57
- /** Fetch the ID token and exchange it for a one-lease executor key. */
57
+ /**
58
+ * Fetch the ID token and exchange it for a one-lease executor key. `adoptTicket` (Amendment 66) is
59
+ * the ticket the api's wake-up passed this run as the adopt-ticket input; it rides in the body
60
+ * beside the token, and like the token it never reaches a log line or an outcome.
61
+ */
58
62
  export async function exchangeOidc(opts) {
59
63
  const id = await actionsIdToken(opts.fetch, opts.requestUrl, opts.requestToken);
60
64
  if (!id.ok)
@@ -65,7 +69,9 @@ export async function exchangeOidc(opts) {
65
69
  res = await opts.fetch(`${base}${routePath("exec_oidc_exchange")}`, {
66
70
  method: "POST",
67
71
  headers: { accept: "application/json", "content-type": "application/json" },
68
- body: JSON.stringify({ oidc_token: id.token }),
72
+ body: JSON.stringify(opts.adoptTicket
73
+ ? { oidc_token: id.token, adopt_ticket: opts.adoptTicket }
74
+ : { oidc_token: id.token }),
69
75
  });
70
76
  }
71
77
  catch (e) {
@@ -38,8 +38,31 @@ export declare function sayRetiredConsoleNotice(flag: string | undefined, env: R
38
38
  export declare const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
39
39
  /** Env override for the Mnemom key a subscription launch sends. */
40
40
  export declare const MNEMOM_KEY_ENV = "MNEMOM_AGENT_MNEMOM_KEY";
41
- /** Field in ~/.mnemom/launcher.json (0600) holding the minted key. */
41
+ /**
42
+ * LEGACY field in ~/.mnemom/launcher.json (0600): the single, cell-blind key every launcher
43
+ * before the per-cell store saved. Every such key was minted against production, so it is read
44
+ * ONLY when the CLI points at the production API and is never sent to another cell.
45
+ */
42
46
  export declare const MNEMOM_KEY_STORE_FIELD = "mnemom_api_key";
47
+ /** Field in ~/.mnemom/launcher.json (0600): the minted keys per cell, `{ "<api origin>": key }`. */
48
+ export declare const MNEMOM_KEYS_STORE_FIELD = "mnemom_api_keys";
49
+ /** The production API origin: the only cell the legacy flat field may be used for. */
50
+ export declare const PRODUCTION_API_ORIGIN = "https://api.mnemom.ai";
51
+ /**
52
+ * The cell a Mnemom key belongs to: the API base's origin, lowercased. An unparseable base
53
+ * gives null, and every caller then fails closed (no stored key is read, none is saved).
54
+ */
55
+ export declare function mnemomKeyCell(apiUrl: string): string | null;
56
+ /**
57
+ * The stored Mnemom key for the cell `apiUrl` names, from the launcher.json object `data`:
58
+ * the per-cell entry, else (production only) the legacy flat field, else undefined. Pure.
59
+ */
60
+ export declare function storedMnemomKeyForCell(data: Record<string, unknown>, apiUrl: string): string | undefined;
61
+ /**
62
+ * The launcher.json patch that saves `key` for the cell `apiUrl` names, keeping every other
63
+ * cell's entry. Throws on an unparseable base, so nothing is saved under a cell we can't name.
64
+ */
65
+ export declare function mnemomKeyStorePatch(data: Record<string, unknown>, apiUrl: string, key: string): Record<string, unknown>;
43
66
  export declare function coerceAgentAuth(raw: string): AgentAuth;
44
67
  /**
45
68
  * launcher.toml `auth`: the retired `console` is kept as-is (never a load error); it folds into
@@ -48,6 +71,13 @@ export declare function coerceAgentAuth(raw: string): AgentAuth;
48
71
  export declare function parseSavedAgentAuth(raw: string): SavedAgentAuth;
49
72
  /** `--auth` flag > MNEMOM_AGENT_AUTH (launcher.toml folds in here; a saved retired `console` → api-key) > api-key. */
50
73
  export declare function resolveAgentAuth(flag: string | undefined, env: Record<string, string | undefined>): AgentAuth;
74
+ /**
75
+ * The fix line for a gateway 401 on the Mnemom key this launch sent. The saved key is per cell
76
+ * (keyed by the API origin), so the line names the cell and the exact remedy: a key set in
77
+ * MNEMOM_AGENT_MNEMOM_KEY must come from this cell; a saved one is removed from this cell's
78
+ * entry so the next launch mints one here. Pure.
79
+ */
80
+ export declare function mnemomKeyRejectedFix(apiUrl: string, env: Record<string, string | undefined>): string;
51
81
  export interface MnemomKeyDeps {
52
82
  env: Record<string, string | undefined>;
53
83
  readStored(): string | undefined;
@@ -39,8 +39,64 @@ export function sayRetiredConsoleNotice(flag, env, say) {
39
39
  export const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
40
40
  /** Env override for the Mnemom key a subscription launch sends. */
41
41
  export const MNEMOM_KEY_ENV = "MNEMOM_AGENT_MNEMOM_KEY";
42
- /** Field in ~/.mnemom/launcher.json (0600) holding the minted key. */
42
+ /**
43
+ * LEGACY field in ~/.mnemom/launcher.json (0600): the single, cell-blind key every launcher
44
+ * before the per-cell store saved. Every such key was minted against production, so it is read
45
+ * ONLY when the CLI points at the production API and is never sent to another cell.
46
+ */
43
47
  export const MNEMOM_KEY_STORE_FIELD = "mnemom_api_key";
48
+ /** Field in ~/.mnemom/launcher.json (0600): the minted keys per cell, `{ "<api origin>": key }`. */
49
+ export const MNEMOM_KEYS_STORE_FIELD = "mnemom_api_keys";
50
+ /** The production API origin: the only cell the legacy flat field may be used for. */
51
+ export const PRODUCTION_API_ORIGIN = "https://api.mnemom.ai";
52
+ /**
53
+ * The cell a Mnemom key belongs to: the API base's origin, lowercased. An unparseable base
54
+ * gives null, and every caller then fails closed (no stored key is read, none is saved).
55
+ */
56
+ export function mnemomKeyCell(apiUrl) {
57
+ try {
58
+ const origin = new URL(apiUrl).origin.toLowerCase();
59
+ return origin && origin !== "null" ? origin : null;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ /**
66
+ * The stored Mnemom key for the cell `apiUrl` names, from the launcher.json object `data`:
67
+ * the per-cell entry, else (production only) the legacy flat field, else undefined. Pure.
68
+ */
69
+ export function storedMnemomKeyForCell(data, apiUrl) {
70
+ const cell = mnemomKeyCell(apiUrl);
71
+ if (cell === null)
72
+ return undefined;
73
+ const map = data[MNEMOM_KEYS_STORE_FIELD];
74
+ if (map && typeof map === "object" && !Array.isArray(map)) {
75
+ const v = map[cell];
76
+ if (typeof v === "string" && v.trim())
77
+ return v;
78
+ }
79
+ if (cell === PRODUCTION_API_ORIGIN) {
80
+ const legacy = data[MNEMOM_KEY_STORE_FIELD];
81
+ if (typeof legacy === "string")
82
+ return legacy;
83
+ }
84
+ return undefined;
85
+ }
86
+ /**
87
+ * The launcher.json patch that saves `key` for the cell `apiUrl` names, keeping every other
88
+ * cell's entry. Throws on an unparseable base, so nothing is saved under a cell we can't name.
89
+ */
90
+ export function mnemomKeyStorePatch(data, apiUrl, key) {
91
+ const cell = mnemomKeyCell(apiUrl);
92
+ if (cell === null)
93
+ throw new Error(`cannot tell which cell the API base '${apiUrl}' is`);
94
+ const current = data[MNEMOM_KEYS_STORE_FIELD];
95
+ const kept = current && typeof current === "object" && !Array.isArray(current)
96
+ ? current
97
+ : {};
98
+ return { [MNEMOM_KEYS_STORE_FIELD]: { ...kept, [cell]: key } };
99
+ }
44
100
  /**
45
101
  * The key is spliced into the newline-joined ANTHROPIC_CUSTOM_HEADERS block, so an embedded
46
102
  * CR/LF (a multi-line paste, a bad env value) would inject extra header lines.
@@ -72,6 +128,30 @@ export function resolveAgentAuth(flag, env) {
72
128
  const fromEnv = (env[AGENT_AUTH_ENV] ?? "").trim();
73
129
  return fromEnv ? coerceAgentAuth(fromEnv) : "api-key";
74
130
  }
131
+ /**
132
+ * The fix line for a gateway 401 on the Mnemom key this launch sent. The saved key is per cell
133
+ * (keyed by the API origin), so the line names the cell and the exact remedy: a key set in
134
+ * MNEMOM_AGENT_MNEMOM_KEY must come from this cell; a saved one is removed from this cell's
135
+ * entry so the next launch mints one here. Pure.
136
+ */
137
+ export function mnemomKeyRejectedFix(apiUrl, env) {
138
+ const cell = mnemomKeyCell(apiUrl);
139
+ const where = cell ?? `the API base '${apiUrl}'`;
140
+ const create = "`mnemom api-key create --name mnemom-agent --scopes gateway` while the CLI points at it";
141
+ if (headerSafe(env[MNEMOM_KEY_ENV] ?? "")) {
142
+ return (`The Mnemom API key in ${MNEMOM_KEY_ENV} is not valid on this cell (${where}); a key from another cell is rejected here. ` +
143
+ `Set ${MNEMOM_KEY_ENV} to a key from this cell (${create}), or unset it so the launcher uses or mints this cell's own key.`);
144
+ }
145
+ if (cell === null) {
146
+ return (`The Mnemom API key is not valid on ${where}, and the launcher can't tell which cell that is. ` +
147
+ `Set ${MNEMOM_KEY_ENV} to a key from that cell (${create}).`);
148
+ }
149
+ const legacy = cell === PRODUCTION_API_ORIGIN
150
+ ? ` (and the older top-level "${MNEMOM_KEY_STORE_FIELD}" field)`
151
+ : "";
152
+ return (`The saved Mnemom API key is per cell, and this cell (${cell}) rejected the one saved for it. ` +
153
+ `Set ${MNEMOM_KEY_ENV} to a key from this cell (${create}), or remove the "${cell}" entry under "${MNEMOM_KEYS_STORE_FIELD}"${legacy} in ~/.mnemom/launcher.json so the next launch mints one.`);
154
+ }
75
155
  /**
76
156
  * The Mnemom key this machine already has — MNEMOM_AGENT_MNEMOM_KEY, else the stored key — or
77
157
  * undefined. NEVER mints. An unpinned api-key launch sends it (`x-mnemom-api-key`) alongside its
@@ -108,7 +188,7 @@ export async function resolveSubscriptionMnemomKey(deps, purpose = "subscription
108
188
  throw new Error("mnemom agent: the API returned an empty Mnemom key.");
109
189
  try {
110
190
  deps.store(minted);
111
- deps.say(`mnemom agent: minted a gateway-only Mnemom API key for ${purpose} (saved to ~/.mnemom/launcher.json, 0600).`);
191
+ deps.say(`mnemom agent: minted a gateway-only Mnemom API key for ${purpose} (saved for this cell to ~/.mnemom/launcher.json, 0600).`);
112
192
  }
113
193
  catch {
114
194
  deps.say("mnemom agent: minted a gateway-only Mnemom API key for this launch but could not save it — the next launch mints another.");
@@ -238,12 +238,20 @@ var WAIT_REASONS = [
238
238
  "mnemom_unavailable",
239
239
  "ci",
240
240
  "question",
241
- "budget"
241
+ "budget",
242
+ "awaiting_issue_run",
243
+ "repo_arm_unreadable"
242
244
  ];
243
245
  var PARKED_ONLY_WAIT_REASONS = [
244
246
  "ci",
245
247
  "question",
246
- "budget"
248
+ "budget",
249
+ "awaiting_issue_run",
250
+ "repo_arm_unreadable"
251
+ ];
252
+ var DISPATCH_PARK_WAIT_REASONS = [
253
+ "awaiting_issue_run",
254
+ "repo_arm_unreadable"
247
255
  ];
248
256
  function isSessionState(v) {
249
257
  return typeof v === "string" && SESSION_STATES.includes(v);
@@ -1066,6 +1074,7 @@ var CLAIM_UNAVAILABLE_CAUSES = [
1066
1074
  "lease_cap_config"
1067
1075
  ];
1068
1076
  var SEAL_REPO_MISMATCH_CAUSES = ["no_repo_claim", "no_seal_repo", "repo_mismatch"];
1077
+ var ADOPT_REFUSED_CAUSES = ["no_parked_seal", "not_parked"];
1069
1078
  var UNSUPPORTED_FEATURES = [
1070
1079
  "tool_classes",
1071
1080
  "launch_spec_change",
@@ -1179,6 +1188,9 @@ var DURABLE_CODES = {
1179
1188
  // durable_claim_ineligible reason: the session is claimable, only not by this executor, and a
1180
1189
  // runner never skips past it to another session. cause is a SEAL_REPO_MISMATCH_CAUSES value.
1181
1190
  seal_repo_mismatch: { status: 403, surfaces: ["api"], fields: ["cause"] },
1191
+ // S1 amendment 64 (mnemom-test#1336): exec_adopt found nothing it may unpark for this run. Nothing
1192
+ // is written. cause is an ADOPT_REFUSED_CAUSES value.
1193
+ adopt_refused: { status: 409, surfaces: ["api"], fields: ["cause"] },
1182
1194
  // ---- api journal (C10-R0 (i), (j), (k)) ----
1183
1195
  /** Lost CAS race on (session_id, seq). */
1184
1196
  durable_append_conflict: { status: 409, surfaces: ["api"], fields: ["tip_seq"] },
@@ -2372,9 +2384,47 @@ var OIDC_EXCHANGE_REFUSALS = [
2372
2384
  "bad_issuer",
2373
2385
  "repo_not_enrolled",
2374
2386
  "ref_not_allowed",
2375
- "pool_flag_off"
2387
+ "pool_flag_off",
2388
+ "adopt_ticket_invalid",
2389
+ "token_replayed",
2390
+ "run_already_exchanged"
2376
2391
  ];
2392
+ var OIDC_EXCHANGE_RUN_CLAIMS = ["jti", "run_id", "run_attempt"];
2393
+ var OIDC_JTI_MAX = 200;
2394
+ var OIDC_RUN_NUMBER_RE = /^[1-9][0-9]{0,15}$/;
2395
+ var OIDC_EXCHANGE_RETENTION_DAYS = 35;
2396
+ function oidcRunClaimsOk(claims) {
2397
+ const { jti, run_id, run_attempt } = claims;
2398
+ return typeof jti === "string" && jti.length >= 1 && jti.length <= OIDC_JTI_MAX && typeof run_id === "string" && OIDC_RUN_NUMBER_RE.test(run_id) && typeof run_attempt === "string" && OIDC_RUN_NUMBER_RE.test(run_attempt);
2399
+ }
2377
2400
  var OIDC_EXCHANGE_AUDIENCE = "mnemom-executor";
2401
+ var ADOPT_TICKET_INPUT = "adopt-ticket";
2402
+ var WAKEUP_SESSION_INPUT = "session-id";
2403
+ var ADOPT_TICKET_RE = /^mnm_at_[A-Za-z0-9_-]{43}$/;
2404
+ var ADOPT_TICKET_TTL_S = 900;
2405
+ var OIDC_EXCHANGE_BODY_KEYS = ["oidc_token", "adopt_ticket"];
2406
+ var OIDC_TOKEN_MAX = 8192;
2407
+ var OIDC_WORKFLOW_ENV = "DISPATCH_WAKEUP_WORKFLOW";
2408
+ var OIDC_WORKFLOW_FILE_RE = /^[A-Za-z0-9_.-]{1,100}\.ya?ml$/;
2409
+ function adoptTicketOk(t) {
2410
+ return typeof t === "string" && ADOPT_TICKET_RE.test(t);
2411
+ }
2412
+ function oidcExchangeBodyOk(v) {
2413
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
2414
+ const o = v;
2415
+ const keys = Object.keys(o);
2416
+ if (keys.some((k) => !OIDC_EXCHANGE_BODY_KEYS.includes(k))) return false;
2417
+ if (typeof o.oidc_token !== "string" || o.oidc_token === "" || o.oidc_token.length > OIDC_TOKEN_MAX)
2418
+ return false;
2419
+ return !("adopt_ticket" in o) || typeof o.adopt_ticket === "string";
2420
+ }
2421
+ function oidcPinnedWorkflow(envValue) {
2422
+ const wf = envValue?.trim() ?? "";
2423
+ return OIDC_WORKFLOW_FILE_RE.test(wf) ? wf : null;
2424
+ }
2425
+ function jobWorkflowRefOk(jobWorkflowRef, repository, workflowFile, defaultBranch) {
2426
+ return typeof jobWorkflowRef === "string" && jobWorkflowRef === `${repository}/.github/workflows/${workflowFile}@refs/heads/${defaultBranch}`;
2427
+ }
2378
2428
  var ORG_POLICY_LIMIT_KEYS = ["per_session_budget_usd", "live_lease_cap"];
2379
2429
  var LIVE_LEASE_CAP_MIN = 1;
2380
2430
  var LIVE_LEASE_CAP_MAX = 1e3;
@@ -2408,14 +2458,15 @@ var CREATE_READY_SESSION_REFUSALS = [
2408
2458
  // 34.10.1 (4): the api-built charter v1 or its envelope does not match the seal; nothing written
2409
2459
  "charter_mismatch"
2410
2460
  ];
2411
- var CREATE_READY_SESSION_OUTCOMES = ["created", "existing", "refused"];
2461
+ var CREATE_READY_SESSION_OUTCOMES = ["created", "existing", "refused", "parked"];
2412
2462
  function isUserCreator(createdBy) {
2413
2463
  return typeof createdBy === "string" && createdBy !== "" && !createdBy.includes(":");
2414
2464
  }
2415
2465
  var CREATE_READY_SESSION_ANSWERS = Object.freeze({
2416
2466
  created: ["outcome", "session_id"],
2417
2467
  existing: ["outcome", "session_id"],
2418
- refused: ["outcome", "reason"]
2468
+ refused: ["outcome", "reason"],
2469
+ parked: ["outcome", "session_id", "wait_reason"]
2419
2470
  });
2420
2471
  function createReadySessionAnswerOk(v) {
2421
2472
  if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
@@ -2424,7 +2475,11 @@ function createReadySessionAnswerOk(v) {
2424
2475
  return false;
2425
2476
  const keys = CREATE_READY_SESSION_ANSWERS[a.outcome];
2426
2477
  if (Object.keys(a).length !== keys.length || !keys.every((k) => k in a)) return false;
2427
- return a.outcome === "refused" ? CREATE_READY_SESSION_REFUSALS.includes(a.reason) : typeof a.session_id === "string" && UUID_RE.test(a.session_id);
2478
+ if (a.outcome === "refused")
2479
+ return CREATE_READY_SESSION_REFUSALS.includes(a.reason);
2480
+ if (a.outcome === "parked" && !DISPATCH_PARK_WAIT_REASONS.includes(a.wait_reason))
2481
+ return false;
2482
+ return typeof a.session_id === "string" && UUID_RE.test(a.session_id);
2428
2483
  }
2429
2484
  function sealIdOk(sealId) {
2430
2485
  if (!UUID_RE.test(sealId)) throw new RangeError(`seal id must be a lowercase uuid: ${sealId}`);
@@ -2501,7 +2556,11 @@ var RUNNER_REFUSED_REASONS = [
2501
2556
  "oidc_ref_not_allowed",
2502
2557
  "oidc_pool_flag_off",
2503
2558
  "oidc_unavailable",
2504
- "api_unavailable"
2559
+ "api_unavailable",
2560
+ "oidc_adopt_ticket_invalid",
2561
+ // amendment 65 (mnemom-test#1331): the two single-use exchange refusals, appended
2562
+ "oidc_token_replayed",
2563
+ "oidc_run_already_exchanged"
2505
2564
  ];
2506
2565
  var RUNNER_OPEN_MODE_TOLERATED = [
2507
2566
  "bundle_unavailable",
@@ -2747,7 +2806,9 @@ var DURABLE_EVENT_KINDS = [
2747
2806
  "session.waiting",
2748
2807
  "session.wait_cleared",
2749
2808
  "executor.turn_continued",
2750
- "inbox.acked"
2809
+ "inbox.acked",
2810
+ // S1 amendment 64: dispatch parked a seal (api-written; session.waiting stays executor-only)
2811
+ "session.dispatch_parked"
2751
2812
  ];
2752
2813
  function isDurableEventKind(v) {
2753
2814
  return typeof v === "string" && DURABLE_EVENT_KINDS.includes(v);
@@ -2756,7 +2817,8 @@ var AMENDMENT_EVENT_GROUPS = Object.freeze({
2756
2817
  "session.ready": "lifecycle",
2757
2818
  "session.waiting": "lifecycle",
2758
2819
  "session.wait_cleared": "lifecycle",
2759
- "executor.turn_continued": "lease"
2820
+ "executor.turn_continued": "lease",
2821
+ "session.dispatch_parked": "lifecycle"
2760
2822
  });
2761
2823
  function eventGroupOf(kind) {
2762
2824
  if (Object.prototype.hasOwnProperty.call(AMENDMENT_EVENT_GROUPS, kind))
@@ -3006,10 +3068,16 @@ var KIND_REFS = {
3006
3068
  [
3007
3069
  { field: "wait_reason", equals: "ci", require: ["head_sha"], forbid: ["approval_id"] },
3008
3070
  { field: "wait_reason", equals: "question", require: ["approval_id"], forbid: ["head_sha"] },
3009
- { field: "wait_reason", equals: "budget", require: ["approval_id"], forbid: ["head_sha"] }
3071
+ { field: "wait_reason", equals: "budget", require: ["approval_id"], forbid: ["head_sha"] },
3072
+ // amendment 64: a dispatch park carries no commitment
3073
+ { field: "wait_reason", equals: "awaiting_issue_run", forbid: ["head_sha", "approval_id"] },
3074
+ { field: "wait_reason", equals: "repo_arm_unreadable", forbid: ["head_sha", "approval_id"] }
3010
3075
  ]
3011
3076
  ),
3012
3077
  "session.wait_cleared": obj({ wait_reason: ENUM(PARKED_ONLY_WAIT_REASONS) }),
3078
+ // S1 amendment 64: the api's dispatch park. One writer per kind: the executor's park stays
3079
+ // session.waiting; adopt and flip-to-runner journal session.wait_cleared + session.ready.
3080
+ "session.dispatch_parked": obj({ wait_reason: ENUM(DISPATCH_PARK_WAIT_REASONS) }),
3013
3081
  // ---- lead ruling 2026-10-28: the spawn that sent the continue; a separate fact, so
3014
3082
  // executor.spawned's refs stay exactly {spawn_id} ----
3015
3083
  "executor.turn_continued": obj({ spawn_id: UUID }),
@@ -4341,12 +4409,14 @@ async function approvalSubjectHash(subject) {
4341
4409
  return sha256Hex(canonicalJson(subject));
4342
4410
  }
4343
4411
  var AGENT_SESSIONS_READY_CHECK = "state <> 'ready' OR (durable_mode = 'locked' AND state_reason IS NULL AND home_executor_id IS NULL AND lease_executor_id IS NULL AND lease_expires_at IS NULL AND spawn_id IS NULL AND hello_at IS NULL)";
4344
- var AGENT_SESSIONS_WAIT_PARKED_CHECK = "CASE WHEN wait_reason IN ('ci','question','budget') THEN state = 'parked' AND wait_ref IS NOT NULL ELSE wait_ref IS NULL END";
4412
+ var AGENT_SESSIONS_WAIT_PARKED_CHECK = "CASE WHEN wait_reason IN ('ci','question','budget') THEN state = 'parked' AND wait_ref IS NOT NULL WHEN wait_reason IN ('awaiting_issue_run','repo_arm_unreadable') THEN state = 'parked' AND wait_ref IS NULL ELSE wait_ref IS NULL END";
4345
4413
  var AGENT_EXECUTORS_SCOPE_CHECK = "scope IN ('user','org')";
4346
4414
  function readyRowOk(row) {
4347
4415
  return row.state !== "ready" || row.durable_mode === "locked" && row.state_reason === null && row.home_executor_id === null && row.lease_executor_id === null && row.lease_expires_at === null && row.spawn_id === null && row.hello_at === null;
4348
4416
  }
4349
4417
  function waitParkedRowOk(row) {
4418
+ if (row.wait_reason !== null && DISPATCH_PARK_WAIT_REASONS.includes(row.wait_reason))
4419
+ return row.state === "parked" && row.wait_ref === null;
4350
4420
  return row.wait_reason !== null && PARKED_ONLY_WAIT_REASONS.includes(row.wait_reason) ? row.state === "parked" && row.wait_ref !== null : row.wait_ref === null;
4351
4421
  }
4352
4422
 
@@ -4852,6 +4922,16 @@ function executorStateOk(v) {
4852
4922
  }
4853
4923
 
4854
4924
  // ../shared/durable-contract/src/routes.ts
4925
+ var ADOPT_BODY_KEYS = ["source_ref", "nonce"];
4926
+ var ADOPT_SOURCE_REF_MAX = 500;
4927
+ function adoptBodyOk(v) {
4928
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
4929
+ const b = v;
4930
+ if (Object.keys(b).length !== 2 || typeof b.source_ref !== "string") return false;
4931
+ if (typeof b.nonce !== "string" || b.nonce.length === 0) return false;
4932
+ const r = b.source_ref;
4933
+ return r !== "" && r === r.trim() && r.length <= ADOPT_SOURCE_REF_MAX;
4934
+ }
4855
4935
  var fixed = (method, auth, path) => Object.freeze({ method, auth, path });
4856
4936
  var scoped = (method, auth, pre, post) => Object.freeze({ method, auth, pre, post });
4857
4937
  var EXECUTORS = "/v1/executors";
@@ -4882,8 +4962,21 @@ var ROUTES = Object.freeze({
4882
4962
  // ---- Doors amendment (ruling 34.03) ----
4883
4963
  /** The org-scope executor's claimable pool sessions, oldest first; `?limit=` (READY_SESSIONS_LIMIT_MIN..MAX) is not part of the match. */
4884
4964
  exec_ready_list: fixed("GET", "executor_key", "/v1/executor/ready"),
4885
- /** Body {oidc_token}; answers a one-lease org-scope executor key or an OIDC_EXCHANGE_REFUSALS refusal. */
4965
+ /**
4966
+ * Body {oidc_token, adopt_ticket?} (doors.ts oidcExchangeBodyOk, Amendment 66). adopt_ticket comes
4967
+ * only from a ticketed wake-up; it is used up before any key is minted and binds the key to one seal.
4968
+ * Amendment 65: single-use per token jti and per run attempt (token_replayed, run_already_exchanged).
4969
+ */
4886
4970
  exec_oidc_exchange: fixed("POST", "github_oidc", "/v1/executor/oidc/exchange"),
4971
+ // ---- S1 amendment 64 (mnemom-test#1336) ----
4972
+ /**
4973
+ * Body {source_ref, nonce} (adoptBodyOk). The OIDC run's key unparks the parked dispatch seal whose
4974
+ * source_ref is this run's own issue, in its own org and repo, and claims it in one transaction,
4975
+ * and only the seal its adopt ticket was minted for (Amendment 66; a key with no ticket adopts none),
4976
+ * fenced on the nonce (exec_claim's rule);
4977
+ * answers the exec_claim answer, or adopt_refused / seal_repo_mismatch, changing nothing.
4978
+ */
4979
+ exec_adopt: fixed("POST", "executor_key", "/v1/executor/adopt"),
4887
4980
  // ---- inbox, executor side (S1 amendment 60) ----
4888
4981
  /** Content-free summary {admitted, delivered, oldest_admitted_at} (inboxSummaryResponseOk); marks nothing. */
4889
4982
  exec_inbox: scoped("GET", "executor_key", EXEC_SESSION_PRE, "/inbox"),
@@ -5807,8 +5900,22 @@ var FREEZE_RECORD = Object.freeze({
5807
5900
  DEFAULT_LEASE_CAP_ENV,
5808
5901
  DISPATCH_SESSION_CID_PREFIX,
5809
5902
  DISPATCH_SESSION_NAME_PREFIX,
5903
+ // amendment 66
5904
+ ADOPT_TICKET_INPUT,
5905
+ WAKEUP_SESSION_INPUT,
5906
+ ADOPT_TICKET_PATTERN: ADOPT_TICKET_RE.source,
5907
+ ADOPT_TICKET_TTL_S,
5908
+ OIDC_TOKEN_MAX,
5909
+ OIDC_WORKFLOW_ENV,
5910
+ OIDC_WORKFLOW_FILE_PATTERN: OIDC_WORKFLOW_FILE_RE.source,
5810
5911
  READY_SESSIONS_LIMIT_MIN,
5811
5912
  READY_SESSIONS_LIMIT_MAX,
5913
+ // amendment 64
5914
+ ADOPT_SOURCE_REF_MAX,
5915
+ // amendment 65
5916
+ OIDC_JTI_MAX,
5917
+ OIDC_RUN_NUMBER_PATTERN: OIDC_RUN_NUMBER_RE.source,
5918
+ OIDC_EXCHANGE_RETENTION_DAYS,
5812
5919
  READY_USER_TEXT,
5813
5920
  RUNNER_FAILED_PHASE_PATTERN: RUNNER_FAILED_PHASE_RE.source,
5814
5921
  AGENT_SESSIONS_READY_CHECK,
@@ -5921,11 +6028,17 @@ var FREEZE_STRUCTURED = Object.freeze({
5921
6028
  READY_CAUSES,
5922
6029
  WAIT_REASONS,
5923
6030
  PARKED_ONLY_WAIT_REASONS,
6031
+ // amendment 64
6032
+ DISPATCH_PARK_WAIT_REASONS,
6033
+ ADOPT_REFUSED_CAUSES,
6034
+ ADOPT_BODY_KEYS,
5924
6035
  WAIT_REF_KEYS,
5925
6036
  CLEAR_WAIT_OUTCOMES,
5926
6037
  EXECUTOR_SCOPES,
5927
6038
  ORG_SCOPE_EXECUTOR_KINDS,
5928
6039
  OIDC_EXCHANGE_REFUSALS,
6040
+ OIDC_EXCHANGE_BODY_KEYS,
6041
+ OIDC_EXCHANGE_RUN_CLAIMS,
5929
6042
  ORG_POLICY_LIMIT_KEYS,
5930
6043
  POOL_CLAIM_FIELDS,
5931
6044
  CREATE_READY_SESSION_REFUSALS,
@@ -6234,6 +6347,12 @@ export {
6234
6347
  ADMISSION_STEP_CODE,
6235
6348
  ADMIT_SWEEP_VALUES,
6236
6349
  ADMIT_SWEEP_VECTORS_PATH,
6350
+ ADOPT_BODY_KEYS,
6351
+ ADOPT_REFUSED_CAUSES,
6352
+ ADOPT_SOURCE_REF_MAX,
6353
+ ADOPT_TICKET_INPUT,
6354
+ ADOPT_TICKET_RE,
6355
+ ADOPT_TICKET_TTL_S,
6237
6356
  ADOPT_WITH_YOU_TEXT,
6238
6357
  AGENT_CONTAINED_STATUSES,
6239
6358
  AGENT_EXECUTORS_SCOPE_CHECK,
@@ -6315,6 +6434,7 @@ export {
6315
6434
  DEVICE_STEP_VERBS,
6316
6435
  DEVICE_VERIFY_AAL_DEFAULT,
6317
6436
  DEVICE_VERIFY_AAL_VALUES,
6437
+ DISPATCH_PARK_WAIT_REASONS,
6318
6438
  DISPATCH_SESSION_CID_PREFIX,
6319
6439
  DISPATCH_SESSION_NAME_PREFIX,
6320
6440
  DONE_CLAUSE,
@@ -6487,7 +6607,15 @@ export {
6487
6607
  NON_TTY_LOCKED_TEMPLATE,
6488
6608
  NO_M1_EMITTER_KINDS,
6489
6609
  OIDC_EXCHANGE_AUDIENCE,
6610
+ OIDC_EXCHANGE_BODY_KEYS,
6490
6611
  OIDC_EXCHANGE_REFUSALS,
6612
+ OIDC_EXCHANGE_RETENTION_DAYS,
6613
+ OIDC_EXCHANGE_RUN_CLAIMS,
6614
+ OIDC_JTI_MAX,
6615
+ OIDC_RUN_NUMBER_RE,
6616
+ OIDC_TOKEN_MAX,
6617
+ OIDC_WORKFLOW_ENV,
6618
+ OIDC_WORKFLOW_FILE_RE,
6491
6619
  OLD_ROUTE_BRANCH,
6492
6620
  OLD_ROUTE_REFRESH,
6493
6621
  ORG_CAP_LOCK_PREFIX,
@@ -6665,11 +6793,14 @@ export {
6665
6793
  UUID_RE,
6666
6794
  WAIT_REASONS,
6667
6795
  WAIT_REF_KEYS,
6796
+ WAKEUP_SESSION_INPUT,
6668
6797
  admissionStatusOf,
6669
6798
  admissionTruthTable,
6670
6799
  admit,
6671
6800
  admitSweepDigest,
6672
6801
  admitSweepText,
6802
+ adoptBodyOk,
6803
+ adoptTicketOk,
6673
6804
  amendDecision,
6674
6805
  answerFromChoice,
6675
6806
  answersOk,
@@ -6797,6 +6928,7 @@ export {
6797
6928
  isValidSessionName,
6798
6929
  isValidStateReason,
6799
6930
  isWellFormedLeaseKey,
6931
+ jobWorkflowRefOk,
6800
6932
  lagBucket,
6801
6933
  lintDurableOnly,
6802
6934
  lintFixCatalogue,
@@ -6808,6 +6940,9 @@ export {
6808
6940
  normalizeCellId,
6809
6941
  normalizeQuestions,
6810
6942
  normalizeSessionName,
6943
+ oidcExchangeBodyOk,
6944
+ oidcPinnedWorkflow,
6945
+ oidcRunClaimsOk,
6811
6946
  openContract,
6812
6947
  ownerSessionViewOk,
6813
6948
  ownerVerbDecision,