@mnemom/mnemom 0.17.39-next.0 → 0.17.39-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.
@@ -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)).
@@ -1,5 +1,50 @@
1
+ /**
2
+ * `mnemom whoami --json`: who is logged in, and which org is active, as ONE
3
+ * JSON object for a program to read. It carries identity only, never a
4
+ * credential: no access or refresh token, no API key, no scope string.
5
+ *
6
+ * {"logged_in": bool, "user_id": str|null, "email": str|null,
7
+ * "key_id": str|null, "org_id": str|null,
8
+ * "active_org": {"org_id", "slug", "name"}|null, "expires_at": ISO|null,
9
+ * "api_url": the API origin this identity was resolved against}
10
+ *
11
+ * A person (an OAuth login) is user_id + email, read from GET /v1/auth/session.
12
+ * An API key is key_id + org_id (its own org), read from GET
13
+ * /v1/api-keys/whoami; its user_id is always null, because a key's owning
14
+ * person is not the caller and a reader must key a key on key_id, never on
15
+ * that person. When the identity read fails, user_id, email, key_id and org_id
16
+ * are null: never a stale stored value.
17
+ *
18
+ * api_url is getApiUrl() in this process (--api / MNEMOM_API_URL > MNEMOM_RING
19
+ * > MNEMOM_ENV). Identities never cross environments (test, us-1 and
20
+ * production each have their own), so a reader pins one origin and refuses a
21
+ * whoami whose api_url is not it.
22
+ *
23
+ * A reader beside the CLI must never read ~/.mnemom/auth.json or call the API
24
+ * with the stored token itself: the CLI refreshes and rotates its tokens under
25
+ * a lock (MNE-9232), and a second token handler would race that refresh. This
26
+ * is the read such a reader uses instead (develop's install and every acting
27
+ * develop verb, which take their operator and tenant from the login).
28
+ */
29
+ export interface WhoamiJson {
30
+ logged_in: boolean;
31
+ user_id: string | null;
32
+ email: string | null;
33
+ key_id: string | null;
34
+ org_id: string | null;
35
+ active_org: {
36
+ org_id: string;
37
+ slug: string;
38
+ name: string | null;
39
+ } | null;
40
+ expires_at: string | null;
41
+ api_url: string;
42
+ }
43
+ export declare function whoamiJson(): Promise<WhoamiJson>;
1
44
  export declare function loginCommand(options?: {
2
45
  noBrowser?: boolean;
3
46
  }): Promise<void>;
4
47
  export declare function logoutCommand(): Promise<void>;
5
- export declare function whoamiCommand(): Promise<void>;
48
+ export declare function whoamiCommand(options?: {
49
+ json?: boolean;
50
+ }): Promise<void>;
@@ -1,6 +1,51 @@
1
1
  import { getAuthInfo, clearAuthTokens, loginWithBrowser, loginWithDeviceFlow, resolveAuth, } from "../lib/auth.js";
2
- import { getMe } from "../lib/api.js";
2
+ import { getApiKeyWhoami, getMe, getSession } from "../lib/api.js";
3
+ import { getActiveOrg } from "../lib/cli-config.js";
4
+ import { getApiUrl } from "../lib/config.js";
3
5
  import { fmt } from "../lib/format.js";
6
+ export async function whoamiJson() {
7
+ const active = getActiveOrg();
8
+ const active_org = active
9
+ ? { org_id: active.org_id, slug: active.slug, name: active.name ?? null }
10
+ : null;
11
+ const out = {
12
+ logged_in: false,
13
+ user_id: null,
14
+ email: null,
15
+ key_id: null,
16
+ org_id: null,
17
+ active_org,
18
+ expires_at: null,
19
+ api_url: getApiUrl(),
20
+ };
21
+ const cred = await resolveAuth();
22
+ if (cred.type === "none")
23
+ return out;
24
+ if (cred.type === "api-key") {
25
+ // A key is keyed on its own id and org, never on its owner's user id.
26
+ const key = await getApiKeyWhoami().catch(() => null);
27
+ return { ...out, logged_in: true, key_id: key?.key_id ?? null, org_id: key?.org_id ?? null };
28
+ }
29
+ if (!getAuthInfo())
30
+ return out;
31
+ // The OAuth token carries no identity: ask the API (GET /v1/auth/session, a
32
+ // pure read, unlike /v1/auth/me's billing and SSO JIT writes), which goes
33
+ // through the CLI's own refresh. A failed read leaves the identity null: a
34
+ // stored legacy value could belong to another session or environment.
35
+ const session = await getSession().catch(() => null);
36
+ const me = session ? { user_id: session.user.id, email: session.user.email ?? null } : null;
37
+ // Read the grant after that call: a refresh inside it moves expiresAt.
38
+ const auth = getAuthInfo();
39
+ if (!auth)
40
+ return out;
41
+ return {
42
+ ...out,
43
+ logged_in: true,
44
+ user_id: me?.user_id ?? null,
45
+ email: me?.email ?? null,
46
+ expires_at: new Date(auth.expiresAt * 1000).toISOString(),
47
+ };
48
+ }
4
49
  export async function loginCommand(options = {}) {
5
50
  try {
6
51
  // --no-browser → RFC 8628 device flow (headless / SSH / no local browser).
@@ -53,7 +98,11 @@ export async function logoutCommand() {
53
98
  clearAuthTokens();
54
99
  console.log(fmt.success("Logged out."));
55
100
  }
56
- export async function whoamiCommand() {
101
+ export async function whoamiCommand(options = {}) {
102
+ if (options.json) {
103
+ console.log(JSON.stringify(await whoamiJson()));
104
+ return;
105
+ }
57
106
  const cred = await resolveAuth();
58
107
  if (cred.type === "none") {
59
108
  console.log("\nNot logged in. Run `mnemom login` to authenticate.\n");
@@ -86,7 +86,9 @@ export function entryProducer(e) {
86
86
  return typeof p === "string" && TURN_PRODUCER_RE.test(p) ? p : undefined;
87
87
  }
88
88
  /** Claude Code's open tag for a bracketed paste, which may lead a pasted prompt's text. */
89
- const PASTE_OPEN_RE = /^<pasted_content id="[^"\n]{0,128}">\n?/;
89
+ // Claude Code may record the paste after blank lines ("\n\n<pasted_content id=...>"); a delivery the marker
90
+ // misses there is never confirmed and gets pasted again (seen live: 4 pastes of one message).
91
+ const PASTE_OPEN_RE = /^\n*<pasted_content id="[^"\n]{0,128}">\n?/;
90
92
  /**
91
93
  * The inbox sha an entry answers (S1 amendment 60): the `[mnemom inbox <sha>]` marker on the first
92
94
  * line of the entry's first text (after Claude Code's paste open tag, as the host daemon delivers by
@@ -72,7 +72,9 @@ export const HANDLED_MAX = 32;
72
72
  /** claude's input line, and the footer it shows while a turn runs. */
73
73
  const PROMPT_MARK = "❯";
74
74
  const RUNNING_RE = /esc to interrupt/i;
75
- const PASTE_OPEN_RE = /^<pasted_content id="[^"\n]{0,128}">\n?/;
75
+ // Claude Code may record the paste after blank lines ("\n\n<pasted_content id=...>"); a delivery the marker
76
+ // misses there is never confirmed and gets pasted again (seen live: 4 pastes of one message).
77
+ const PASTE_OPEN_RE = /^\n*<pasted_content id="[^"\n]{0,128}">\n?/;
76
78
  function firstText(r) {
77
79
  const c = r.message?.content;
78
80
  if (typeof c === "string")
package/dist/index.js CHANGED
@@ -1976,9 +1976,10 @@ program
1976
1976
  program
1977
1977
  .command("whoami")
1978
1978
  .description("Show current authentication status")
1979
- .action(async () => {
1979
+ .option("--json", "Print who is logged in as one JSON object: {logged_in, user_id, email, active_org, expires_at} (never a token)")
1980
+ .action(async (options) => {
1980
1981
  try {
1981
- await whoamiCommand();
1982
+ await whoamiCommand({ json: options.json });
1982
1983
  }
1983
1984
  catch (error) {
1984
1985
  console.error("Error:", error instanceof Error ? error.message : error);
package/dist/lib/api.d.ts CHANGED
@@ -426,6 +426,25 @@ export interface MeResponse {
426
426
  } | null;
427
427
  }
428
428
  export declare function getMe(): Promise<MeResponse>;
429
+ /** Who the credential belongs to — `GET /v1/auth/session`. A pure read for a
430
+ * bearer (no billing-account ensure and no SSO JIT provisioning, which
431
+ * `GET /v1/auth/me` runs on every call), so an identity check changes nothing. */
432
+ export interface SessionResponse {
433
+ user: {
434
+ id: string;
435
+ email?: string | null;
436
+ };
437
+ method?: string;
438
+ }
439
+ export declare function getSession(): Promise<SessionResponse>;
440
+ /** What an API key is — `GET /v1/api-keys/whoami` (ADR-049). A read; the key's
441
+ * own id and org, which is how a reader keys a key caller. */
442
+ export interface ApiKeyWhoamiResponse {
443
+ key_id: string | null;
444
+ org_id: string | null;
445
+ auth_method?: string;
446
+ }
447
+ export declare function getApiKeyWhoami(): Promise<ApiKeyWhoamiResponse>;
429
448
  /**
430
449
  * The org's Mnemom Units (MU) balance — `GET /v1/billing/mu` (the frozen mu
431
450
  * v0.1 balance, values as decimal strings). Amounts come in both whole MU
package/dist/lib/api.js CHANGED
@@ -440,6 +440,14 @@ export async function getMyPersonalOrg() {
440
440
  export async function getMe() {
441
441
  return gFetch("/v1/auth/me", {}, { fallback: "Could not read your identity" });
442
442
  }
443
+ export async function getSession() {
444
+ return gFetch("/v1/auth/session", {}, { fallback: "Could not read your identity" });
445
+ }
446
+ export async function getApiKeyWhoami() {
447
+ return gFetch("/v1/api-keys/whoami", {}, {
448
+ fallback: "Could not read your API key's identity",
449
+ });
450
+ }
443
451
  export async function getRecordTotals(orgId, lineageKey, signal) {
444
452
  return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/record/totals?lineage_key=${encodeURIComponent(lineageKey)}`, signal ? { signal } : {}, { notFoundLabel: "Record totals", fallback: "Could not read record totals" });
445
453
  }
@@ -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) {
@@ -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,62 @@ 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
+ var OIDC_WORKFLOW_SET_ENV = "DURABLE_OIDC_WORKFLOWS";
2410
+ var OIDC_WORKFLOW_SET_MAX = 8;
2411
+ function adoptTicketOk(t) {
2412
+ return typeof t === "string" && ADOPT_TICKET_RE.test(t);
2413
+ }
2414
+ function oidcExchangeBodyOk(v) {
2415
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
2416
+ const o = v;
2417
+ const keys = Object.keys(o);
2418
+ if (keys.some((k) => !OIDC_EXCHANGE_BODY_KEYS.includes(k))) return false;
2419
+ if (typeof o.oidc_token !== "string" || o.oidc_token === "" || o.oidc_token.length > OIDC_TOKEN_MAX)
2420
+ return false;
2421
+ return !("adopt_ticket" in o) || typeof o.adopt_ticket === "string";
2422
+ }
2423
+ function oidcPinnedWorkflow(envValue) {
2424
+ const wf = envValue?.trim() ?? "";
2425
+ return OIDC_WORKFLOW_FILE_RE.test(wf) ? wf : null;
2426
+ }
2427
+ function oidcWorkflowSet(setValue, pinValue) {
2428
+ if (setValue === void 0) {
2429
+ const pin = oidcPinnedWorkflow(pinValue);
2430
+ return pin === null ? null : [pin];
2431
+ }
2432
+ const names = setValue.split(",").map((n) => n.trim());
2433
+ if (names.some((n) => !OIDC_WORKFLOW_FILE_RE.test(n) || n.includes(".."))) return null;
2434
+ const set = [...new Set(names)];
2435
+ return set.length > OIDC_WORKFLOW_SET_MAX ? null : set;
2436
+ }
2437
+ function jobWorkflowRefOk(jobWorkflowRef, repository, workflowFile, defaultBranch) {
2438
+ return typeof jobWorkflowRef === "string" && jobWorkflowRef === `${repository}/.github/workflows/${workflowFile}@refs/heads/${defaultBranch}`;
2439
+ }
2440
+ function jobWorkflowRefInSet(jobWorkflowRef, repository, workflowFiles, defaultBranch) {
2441
+ return workflowFiles.some((f2) => jobWorkflowRefOk(jobWorkflowRef, repository, f2, defaultBranch));
2442
+ }
2378
2443
  var ORG_POLICY_LIMIT_KEYS = ["per_session_budget_usd", "live_lease_cap"];
2379
2444
  var LIVE_LEASE_CAP_MIN = 1;
2380
2445
  var LIVE_LEASE_CAP_MAX = 1e3;
@@ -2408,14 +2473,15 @@ var CREATE_READY_SESSION_REFUSALS = [
2408
2473
  // 34.10.1 (4): the api-built charter v1 or its envelope does not match the seal; nothing written
2409
2474
  "charter_mismatch"
2410
2475
  ];
2411
- var CREATE_READY_SESSION_OUTCOMES = ["created", "existing", "refused"];
2476
+ var CREATE_READY_SESSION_OUTCOMES = ["created", "existing", "refused", "parked"];
2412
2477
  function isUserCreator(createdBy) {
2413
2478
  return typeof createdBy === "string" && createdBy !== "" && !createdBy.includes(":");
2414
2479
  }
2415
2480
  var CREATE_READY_SESSION_ANSWERS = Object.freeze({
2416
2481
  created: ["outcome", "session_id"],
2417
2482
  existing: ["outcome", "session_id"],
2418
- refused: ["outcome", "reason"]
2483
+ refused: ["outcome", "reason"],
2484
+ parked: ["outcome", "session_id", "wait_reason"]
2419
2485
  });
2420
2486
  function createReadySessionAnswerOk(v) {
2421
2487
  if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
@@ -2424,7 +2490,11 @@ function createReadySessionAnswerOk(v) {
2424
2490
  return false;
2425
2491
  const keys = CREATE_READY_SESSION_ANSWERS[a.outcome];
2426
2492
  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);
2493
+ if (a.outcome === "refused")
2494
+ return CREATE_READY_SESSION_REFUSALS.includes(a.reason);
2495
+ if (a.outcome === "parked" && !DISPATCH_PARK_WAIT_REASONS.includes(a.wait_reason))
2496
+ return false;
2497
+ return typeof a.session_id === "string" && UUID_RE.test(a.session_id);
2428
2498
  }
2429
2499
  function sealIdOk(sealId) {
2430
2500
  if (!UUID_RE.test(sealId)) throw new RangeError(`seal id must be a lowercase uuid: ${sealId}`);
@@ -2501,7 +2571,11 @@ var RUNNER_REFUSED_REASONS = [
2501
2571
  "oidc_ref_not_allowed",
2502
2572
  "oidc_pool_flag_off",
2503
2573
  "oidc_unavailable",
2504
- "api_unavailable"
2574
+ "api_unavailable",
2575
+ "oidc_adopt_ticket_invalid",
2576
+ // amendment 65 (mnemom-test#1331): the two single-use exchange refusals, appended
2577
+ "oidc_token_replayed",
2578
+ "oidc_run_already_exchanged"
2505
2579
  ];
2506
2580
  var RUNNER_OPEN_MODE_TOLERATED = [
2507
2581
  "bundle_unavailable",
@@ -2747,7 +2821,9 @@ var DURABLE_EVENT_KINDS = [
2747
2821
  "session.waiting",
2748
2822
  "session.wait_cleared",
2749
2823
  "executor.turn_continued",
2750
- "inbox.acked"
2824
+ "inbox.acked",
2825
+ // S1 amendment 64: dispatch parked a seal (api-written; session.waiting stays executor-only)
2826
+ "session.dispatch_parked"
2751
2827
  ];
2752
2828
  function isDurableEventKind(v) {
2753
2829
  return typeof v === "string" && DURABLE_EVENT_KINDS.includes(v);
@@ -2756,7 +2832,8 @@ var AMENDMENT_EVENT_GROUPS = Object.freeze({
2756
2832
  "session.ready": "lifecycle",
2757
2833
  "session.waiting": "lifecycle",
2758
2834
  "session.wait_cleared": "lifecycle",
2759
- "executor.turn_continued": "lease"
2835
+ "executor.turn_continued": "lease",
2836
+ "session.dispatch_parked": "lifecycle"
2760
2837
  });
2761
2838
  function eventGroupOf(kind) {
2762
2839
  if (Object.prototype.hasOwnProperty.call(AMENDMENT_EVENT_GROUPS, kind))
@@ -3006,10 +3083,16 @@ var KIND_REFS = {
3006
3083
  [
3007
3084
  { field: "wait_reason", equals: "ci", require: ["head_sha"], forbid: ["approval_id"] },
3008
3085
  { field: "wait_reason", equals: "question", require: ["approval_id"], forbid: ["head_sha"] },
3009
- { field: "wait_reason", equals: "budget", require: ["approval_id"], forbid: ["head_sha"] }
3086
+ { field: "wait_reason", equals: "budget", require: ["approval_id"], forbid: ["head_sha"] },
3087
+ // amendment 64: a dispatch park carries no commitment
3088
+ { field: "wait_reason", equals: "awaiting_issue_run", forbid: ["head_sha", "approval_id"] },
3089
+ { field: "wait_reason", equals: "repo_arm_unreadable", forbid: ["head_sha", "approval_id"] }
3010
3090
  ]
3011
3091
  ),
3012
3092
  "session.wait_cleared": obj({ wait_reason: ENUM(PARKED_ONLY_WAIT_REASONS) }),
3093
+ // S1 amendment 64: the api's dispatch park. One writer per kind: the executor's park stays
3094
+ // session.waiting; adopt and flip-to-runner journal session.wait_cleared + session.ready.
3095
+ "session.dispatch_parked": obj({ wait_reason: ENUM(DISPATCH_PARK_WAIT_REASONS) }),
3013
3096
  // ---- lead ruling 2026-10-28: the spawn that sent the continue; a separate fact, so
3014
3097
  // executor.spawned's refs stay exactly {spawn_id} ----
3015
3098
  "executor.turn_continued": obj({ spawn_id: UUID }),
@@ -4341,12 +4424,14 @@ async function approvalSubjectHash(subject) {
4341
4424
  return sha256Hex(canonicalJson(subject));
4342
4425
  }
4343
4426
  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";
4427
+ 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
4428
  var AGENT_EXECUTORS_SCOPE_CHECK = "scope IN ('user','org')";
4346
4429
  function readyRowOk(row) {
4347
4430
  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
4431
  }
4349
4432
  function waitParkedRowOk(row) {
4433
+ if (row.wait_reason !== null && DISPATCH_PARK_WAIT_REASONS.includes(row.wait_reason))
4434
+ return row.state === "parked" && row.wait_ref === null;
4350
4435
  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
4436
  }
4352
4437
 
@@ -4852,6 +4937,16 @@ function executorStateOk(v) {
4852
4937
  }
4853
4938
 
4854
4939
  // ../shared/durable-contract/src/routes.ts
4940
+ var ADOPT_BODY_KEYS = ["source_ref", "nonce"];
4941
+ var ADOPT_SOURCE_REF_MAX = 500;
4942
+ function adoptBodyOk(v) {
4943
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
4944
+ const b = v;
4945
+ if (Object.keys(b).length !== 2 || typeof b.source_ref !== "string") return false;
4946
+ if (typeof b.nonce !== "string" || b.nonce.length === 0) return false;
4947
+ const r = b.source_ref;
4948
+ return r !== "" && r === r.trim() && r.length <= ADOPT_SOURCE_REF_MAX;
4949
+ }
4855
4950
  var fixed = (method, auth, path) => Object.freeze({ method, auth, path });
4856
4951
  var scoped = (method, auth, pre, post) => Object.freeze({ method, auth, pre, post });
4857
4952
  var EXECUTORS = "/v1/executors";
@@ -4882,8 +4977,21 @@ var ROUTES = Object.freeze({
4882
4977
  // ---- Doors amendment (ruling 34.03) ----
4883
4978
  /** The org-scope executor's claimable pool sessions, oldest first; `?limit=` (READY_SESSIONS_LIMIT_MIN..MAX) is not part of the match. */
4884
4979
  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. */
4980
+ /**
4981
+ * Body {oidc_token, adopt_ticket?} (doors.ts oidcExchangeBodyOk, Amendment 66). adopt_ticket comes
4982
+ * only from a ticketed wake-up; it is used up before any key is minted and binds the key to one seal.
4983
+ * Amendment 65: single-use per token jti and per run attempt (token_replayed, run_already_exchanged).
4984
+ */
4886
4985
  exec_oidc_exchange: fixed("POST", "github_oidc", "/v1/executor/oidc/exchange"),
4986
+ // ---- S1 amendment 64 (mnemom-test#1336) ----
4987
+ /**
4988
+ * Body {source_ref, nonce} (adoptBodyOk). The OIDC run's key unparks the parked dispatch seal whose
4989
+ * source_ref is this run's own issue, in its own org and repo, and claims it in one transaction,
4990
+ * and only the seal its adopt ticket was minted for (Amendment 66; a key with no ticket adopts none),
4991
+ * fenced on the nonce (exec_claim's rule);
4992
+ * answers the exec_claim answer, or adopt_refused / seal_repo_mismatch, changing nothing.
4993
+ */
4994
+ exec_adopt: fixed("POST", "executor_key", "/v1/executor/adopt"),
4887
4995
  // ---- inbox, executor side (S1 amendment 60) ----
4888
4996
  /** Content-free summary {admitted, delivered, oldest_admitted_at} (inboxSummaryResponseOk); marks nothing. */
4889
4997
  exec_inbox: scoped("GET", "executor_key", EXEC_SESSION_PRE, "/inbox"),
@@ -5807,8 +5915,24 @@ var FREEZE_RECORD = Object.freeze({
5807
5915
  DEFAULT_LEASE_CAP_ENV,
5808
5916
  DISPATCH_SESSION_CID_PREFIX,
5809
5917
  DISPATCH_SESSION_NAME_PREFIX,
5918
+ // amendment 66
5919
+ ADOPT_TICKET_INPUT,
5920
+ WAKEUP_SESSION_INPUT,
5921
+ ADOPT_TICKET_PATTERN: ADOPT_TICKET_RE.source,
5922
+ ADOPT_TICKET_TTL_S,
5923
+ OIDC_TOKEN_MAX,
5924
+ OIDC_WORKFLOW_ENV,
5925
+ OIDC_WORKFLOW_FILE_PATTERN: OIDC_WORKFLOW_FILE_RE.source,
5926
+ OIDC_WORKFLOW_SET_ENV,
5927
+ OIDC_WORKFLOW_SET_MAX,
5810
5928
  READY_SESSIONS_LIMIT_MIN,
5811
5929
  READY_SESSIONS_LIMIT_MAX,
5930
+ // amendment 64
5931
+ ADOPT_SOURCE_REF_MAX,
5932
+ // amendment 65
5933
+ OIDC_JTI_MAX,
5934
+ OIDC_RUN_NUMBER_PATTERN: OIDC_RUN_NUMBER_RE.source,
5935
+ OIDC_EXCHANGE_RETENTION_DAYS,
5812
5936
  READY_USER_TEXT,
5813
5937
  RUNNER_FAILED_PHASE_PATTERN: RUNNER_FAILED_PHASE_RE.source,
5814
5938
  AGENT_SESSIONS_READY_CHECK,
@@ -5921,11 +6045,17 @@ var FREEZE_STRUCTURED = Object.freeze({
5921
6045
  READY_CAUSES,
5922
6046
  WAIT_REASONS,
5923
6047
  PARKED_ONLY_WAIT_REASONS,
6048
+ // amendment 64
6049
+ DISPATCH_PARK_WAIT_REASONS,
6050
+ ADOPT_REFUSED_CAUSES,
6051
+ ADOPT_BODY_KEYS,
5924
6052
  WAIT_REF_KEYS,
5925
6053
  CLEAR_WAIT_OUTCOMES,
5926
6054
  EXECUTOR_SCOPES,
5927
6055
  ORG_SCOPE_EXECUTOR_KINDS,
5928
6056
  OIDC_EXCHANGE_REFUSALS,
6057
+ OIDC_EXCHANGE_BODY_KEYS,
6058
+ OIDC_EXCHANGE_RUN_CLAIMS,
5929
6059
  ORG_POLICY_LIMIT_KEYS,
5930
6060
  POOL_CLAIM_FIELDS,
5931
6061
  CREATE_READY_SESSION_REFUSALS,
@@ -6234,6 +6364,12 @@ export {
6234
6364
  ADMISSION_STEP_CODE,
6235
6365
  ADMIT_SWEEP_VALUES,
6236
6366
  ADMIT_SWEEP_VECTORS_PATH,
6367
+ ADOPT_BODY_KEYS,
6368
+ ADOPT_REFUSED_CAUSES,
6369
+ ADOPT_SOURCE_REF_MAX,
6370
+ ADOPT_TICKET_INPUT,
6371
+ ADOPT_TICKET_RE,
6372
+ ADOPT_TICKET_TTL_S,
6237
6373
  ADOPT_WITH_YOU_TEXT,
6238
6374
  AGENT_CONTAINED_STATUSES,
6239
6375
  AGENT_EXECUTORS_SCOPE_CHECK,
@@ -6315,6 +6451,7 @@ export {
6315
6451
  DEVICE_STEP_VERBS,
6316
6452
  DEVICE_VERIFY_AAL_DEFAULT,
6317
6453
  DEVICE_VERIFY_AAL_VALUES,
6454
+ DISPATCH_PARK_WAIT_REASONS,
6318
6455
  DISPATCH_SESSION_CID_PREFIX,
6319
6456
  DISPATCH_SESSION_NAME_PREFIX,
6320
6457
  DONE_CLAUSE,
@@ -6487,7 +6624,17 @@ export {
6487
6624
  NON_TTY_LOCKED_TEMPLATE,
6488
6625
  NO_M1_EMITTER_KINDS,
6489
6626
  OIDC_EXCHANGE_AUDIENCE,
6627
+ OIDC_EXCHANGE_BODY_KEYS,
6490
6628
  OIDC_EXCHANGE_REFUSALS,
6629
+ OIDC_EXCHANGE_RETENTION_DAYS,
6630
+ OIDC_EXCHANGE_RUN_CLAIMS,
6631
+ OIDC_JTI_MAX,
6632
+ OIDC_RUN_NUMBER_RE,
6633
+ OIDC_TOKEN_MAX,
6634
+ OIDC_WORKFLOW_ENV,
6635
+ OIDC_WORKFLOW_FILE_RE,
6636
+ OIDC_WORKFLOW_SET_ENV,
6637
+ OIDC_WORKFLOW_SET_MAX,
6491
6638
  OLD_ROUTE_BRANCH,
6492
6639
  OLD_ROUTE_REFRESH,
6493
6640
  ORG_CAP_LOCK_PREFIX,
@@ -6665,11 +6812,14 @@ export {
6665
6812
  UUID_RE,
6666
6813
  WAIT_REASONS,
6667
6814
  WAIT_REF_KEYS,
6815
+ WAKEUP_SESSION_INPUT,
6668
6816
  admissionStatusOf,
6669
6817
  admissionTruthTable,
6670
6818
  admit,
6671
6819
  admitSweepDigest,
6672
6820
  admitSweepText,
6821
+ adoptBodyOk,
6822
+ adoptTicketOk,
6673
6823
  amendDecision,
6674
6824
  answerFromChoice,
6675
6825
  answersOk,
@@ -6797,6 +6947,8 @@ export {
6797
6947
  isValidSessionName,
6798
6948
  isValidStateReason,
6799
6949
  isWellFormedLeaseKey,
6950
+ jobWorkflowRefInSet,
6951
+ jobWorkflowRefOk,
6800
6952
  lagBucket,
6801
6953
  lintDurableOnly,
6802
6954
  lintFixCatalogue,
@@ -6808,6 +6960,10 @@ export {
6808
6960
  normalizeCellId,
6809
6961
  normalizeQuestions,
6810
6962
  normalizeSessionName,
6963
+ oidcExchangeBodyOk,
6964
+ oidcPinnedWorkflow,
6965
+ oidcRunClaimsOk,
6966
+ oidcWorkflowSet,
6811
6967
  openContract,
6812
6968
  ownerSessionViewOk,
6813
6969
  ownerVerbDecision,
@@ -15,6 +15,14 @@ export type ClaimUnavailableCause = (typeof CLAIM_UNAVAILABLE_CAUSES)[number];
15
15
  */
16
16
  export declare const SEAL_REPO_MISMATCH_CAUSES: readonly ["no_repo_claim", "no_seal_repo", "repo_mismatch"];
17
17
  export type SealRepoMismatchCause = (typeof SEAL_REPO_MISMATCH_CAUSES)[number];
18
+ /**
19
+ * adopt_refused details.cause (S1 amendment 64, mnemom-test#1336; exec_adopt): no parked dispatch
20
+ * seal names this source_ref in the caller's own org and repo (never says whether one exists
21
+ * elsewhere), or the seal's session is not parked and the caller is not the OIDC run that already
22
+ * holds its lease. A repo or binding mismatch is seal_repo_mismatch, not this code.
23
+ */
24
+ export declare const ADOPT_REFUSED_CAUSES: readonly ["no_parked_seal", "not_parked"];
25
+ export type AdoptRefusedCause = (typeof ADOPT_REFUSED_CAUSES)[number];
18
26
  export declare const UNSUPPORTED_FEATURES: readonly ["tool_classes", "launch_spec_change", "above_limit_raise", "remote_control"];
19
27
  export type UnsupportedFeature = (typeof UNSUPPORTED_FEATURES)[number];
20
28
  export declare const CWD_KINDS: readonly ["subdirectory", "not_git", "symlink_transcript"];
@@ -273,6 +281,11 @@ export declare const DURABLE_CODES: {
273
281
  readonly surfaces: readonly ["api"];
274
282
  readonly fields: readonly ["cause"];
275
283
  };
284
+ readonly adopt_refused: {
285
+ readonly status: 409;
286
+ readonly surfaces: readonly ["api"];
287
+ readonly fields: readonly ["cause"];
288
+ };
276
289
  /** Lost CAS race on (session_id, seq). */
277
290
  readonly durable_append_conflict: {
278
291
  readonly status: 409;
@@ -24,6 +24,13 @@ export const CLAIM_UNAVAILABLE_CAUSES = [
24
24
  * another repo than the executor's (owner/name, case-insensitive).
25
25
  */
26
26
  export const SEAL_REPO_MISMATCH_CAUSES = ["no_repo_claim", "no_seal_repo", "repo_mismatch"];
27
+ /**
28
+ * adopt_refused details.cause (S1 amendment 64, mnemom-test#1336; exec_adopt): no parked dispatch
29
+ * seal names this source_ref in the caller's own org and repo (never says whether one exists
30
+ * elsewhere), or the seal's session is not parked and the caller is not the OIDC run that already
31
+ * holds its lease. A repo or binding mismatch is seal_repo_mismatch, not this code.
32
+ */
33
+ export const ADOPT_REFUSED_CAUSES = ["no_parked_seal", "not_parked"];
27
34
  export const UNSUPPORTED_FEATURES = [
28
35
  "tool_classes",
29
36
  "launch_spec_change",
@@ -145,6 +152,9 @@ export const DURABLE_CODES = {
145
152
  // durable_claim_ineligible reason: the session is claimable, only not by this executor, and a
146
153
  // runner never skips past it to another session. cause is a SEAL_REPO_MISMATCH_CAUSES value.
147
154
  seal_repo_mismatch: { status: 403, surfaces: ["api"], fields: ["cause"] },
155
+ // S1 amendment 64 (mnemom-test#1336): exec_adopt found nothing it may unpark for this run. Nothing
156
+ // is written. cause is an ADOPT_REFUSED_CAUSES value.
157
+ adopt_refused: { status: 409, surfaces: ["api"], fields: ["cause"] },
148
158
  // ---- api journal (C10-R0 (i), (j), (k)) ----
149
159
  /** Lost CAS race on (session_id, seq). */
150
160
  durable_append_conflict: { status: 409, surfaces: ["api"], fields: ["tip_seq"] },