@mnemom/mnemom 0.17.39-next.1 → 0.17.39-next.3
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/commands/auth.d.ts +46 -1
- package/dist/commands/auth.js +51 -2
- package/dist/durable/turn-chain.js +3 -1
- package/dist/host/commands.js +2 -1
- package/dist/host/inbox-delivery.js +3 -1
- package/dist/host/owner.d.ts +2 -0
- package/dist/host/verbs.d.ts +10 -0
- package/dist/host/verbs.js +44 -2
- package/dist/index.js +3 -2
- package/dist/lib/api.d.ts +19 -0
- package/dist/lib/api.js +8 -0
- package/dist/vendor/durable-contract/index.js +25 -3
- package/dist/vendor/shared/durable-contract/cli-surface.js +3 -2
- package/dist/vendor/shared/durable-contract/doors.d.ts +27 -2
- package/dist/vendor/shared/durable-contract/doors.js +39 -2
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +3 -1
- package/dist/vendor/shared/durable-contract/freeze-record.js +2 -0
- package/package.json +1 -1
package/dist/commands/auth.d.ts
CHANGED
|
@@ -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(
|
|
48
|
+
export declare function whoamiCommand(options?: {
|
|
49
|
+
json?: boolean;
|
|
50
|
+
}): Promise<void>;
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/host/commands.js
CHANGED
|
@@ -69,7 +69,8 @@ export function registerHostCommands(agent, hidden) {
|
|
|
69
69
|
.command("enroll")
|
|
70
70
|
.description("Enroll this machine as an executor (device step); `install` does this when needed")
|
|
71
71
|
.option("--org <org_id>", "Join this organization (you must be a member); defaults to the active org, else your personal org")
|
|
72
|
-
.
|
|
72
|
+
.option("--scope <scope>", "personal (default): run your own sessions. org: run the org's pool jobs (`mnemom agent run`); an org owner or admin approves it, and the org must have org runners turned on", "personal")
|
|
73
|
+
.action(async (o) => guarded(false, (io) => hostEnroll(io, { org: o.org, scope: o.scope })));
|
|
73
74
|
host
|
|
74
75
|
.command("renew")
|
|
75
76
|
.description("Renew this machine's executor grant (device step)")
|
|
@@ -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
|
-
|
|
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/host/owner.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface OwnerApi {
|
|
|
13
13
|
machine_label?: string;
|
|
14
14
|
/** Ruling 34.03.4: the org this host joins; the api checks membership. */
|
|
15
15
|
org_id?: string;
|
|
16
|
+
/** The any-machine amendment: "org" enrolls this host as a runner of org_id's pool. */
|
|
17
|
+
scope?: "org";
|
|
16
18
|
}): Promise<OwnerHttp>;
|
|
17
19
|
enrollPoll(pollSecret: string, approvalId: string | null): Promise<OwnerHttp>;
|
|
18
20
|
list(): Promise<OwnerHttp>;
|
package/dist/host/verbs.d.ts
CHANGED
|
@@ -52,16 +52,26 @@ export declare function enrollOrg(io: VerbIo, opts: {
|
|
|
52
52
|
orgId: string;
|
|
53
53
|
from: "flag" | "active";
|
|
54
54
|
} | null;
|
|
55
|
+
/**
|
|
56
|
+
* `host enroll --scope`: personal (the default) enrolls this machine for its user's own sessions;
|
|
57
|
+
* org enrolls it as a runner of the org's pool (the any-machine amendment, Wassim 2026-11-09:
|
|
58
|
+
* S1 ORG_SCOPE_EXECUTOR_KINDS includes host). The org owner or admin's device-step approval is the
|
|
59
|
+
* guard, and the api also requires the org on DURABLE_POOL_CLAIM.
|
|
60
|
+
*/
|
|
61
|
+
export declare const ENROLL_SCOPES: readonly ["personal", "org"];
|
|
62
|
+
export type EnrollScope = (typeof ENROLL_SCOPES)[number];
|
|
55
63
|
/** The device step: start, show the code, poll, write the state file. Returns an exit code. */
|
|
56
64
|
export declare function enrollDevice(io: VerbIo, opts?: {
|
|
57
65
|
label?: string;
|
|
58
66
|
org?: string;
|
|
67
|
+
scope?: EnrollScope;
|
|
59
68
|
}): Promise<number>;
|
|
60
69
|
/** Write the plist and (re)load the job. */
|
|
61
70
|
export declare function writeAndLoad(io: VerbIo): Promise<number>;
|
|
62
71
|
export declare function hostInstall(io: VerbIo): Promise<number>;
|
|
63
72
|
export declare function hostEnroll(io: VerbIo, opts?: {
|
|
64
73
|
org?: string;
|
|
74
|
+
scope?: EnrollScope;
|
|
65
75
|
}): Promise<number>;
|
|
66
76
|
export declare function hostRenew(io: VerbIo): Promise<number>;
|
|
67
77
|
export declare function hostUninstall(io: VerbIo): Promise<number>;
|
package/dist/host/verbs.js
CHANGED
|
@@ -80,8 +80,26 @@ function errorCode(body) {
|
|
|
80
80
|
const code = e && typeof e === "object" ? e.code : undefined;
|
|
81
81
|
return typeof code === "string" ? code : null;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* `host enroll --scope`: personal (the default) enrolls this machine for its user's own sessions;
|
|
85
|
+
* org enrolls it as a runner of the org's pool (the any-machine amendment, Wassim 2026-11-09:
|
|
86
|
+
* S1 ORG_SCOPE_EXECUTOR_KINDS includes host). The org owner or admin's device-step approval is the
|
|
87
|
+
* guard, and the api also requires the org on DURABLE_POOL_CLAIM.
|
|
88
|
+
*/
|
|
89
|
+
export const ENROLL_SCOPES = ["personal", "org"];
|
|
83
90
|
/** The device step: start, show the code, poll, write the state file. Returns an exit code. */
|
|
84
91
|
export async function enrollDevice(io, opts = {}) {
|
|
92
|
+
const orgScope = opts.scope === "org";
|
|
93
|
+
if (opts.scope !== undefined && !ENROLL_SCOPES.includes(opts.scope)) {
|
|
94
|
+
io.err(`mnemom agent host: --scope must be ${ENROLL_SCOPES.join(" or ")}`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
// An org runner names its org: --org, else the active org. The api requires it (34.03.3).
|
|
98
|
+
if (orgScope && !enrollOrg(io, opts)) {
|
|
99
|
+
io.err("mnemom agent host: --scope org needs the organization this machine will run jobs for.");
|
|
100
|
+
io.err("fix: `mnemom agent host enroll --scope org --org <org_id>` (`mnemom org list` shows them)");
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
85
103
|
const { paths } = ctx(io);
|
|
86
104
|
const lock = (io.acquireLock ?? acquireHostLock)(path.join(paths.durableDir, "enroll.lock"));
|
|
87
105
|
if (!lock.ok) {
|
|
@@ -96,13 +114,14 @@ export async function enrollDevice(io, opts = {}) {
|
|
|
96
114
|
contract_version: EXECUTOR_CONTRACT,
|
|
97
115
|
...(opts.label ? { machine_label: opts.label } : {}),
|
|
98
116
|
...(o ? { org_id: o.orgId } : {}),
|
|
117
|
+
...(orgScope ? { scope: "org" } : {}),
|
|
99
118
|
});
|
|
100
119
|
let st = await startWith(org);
|
|
101
120
|
// An api from before 34.03.4 refuses org_id on a host start with its plain 400 invalid_scope
|
|
102
121
|
// (34.03.2 (1); mnemom-api enroll.ts "org_id is only accepted with scope org"; recorded beside
|
|
103
122
|
// exec_enroll in S1 codes.ts). An org taken from the active org then falls back to the start this
|
|
104
123
|
// CLI made before; an explicit --org cannot.
|
|
105
|
-
if (org && st.status === 400 && errorCode(st.body) === "invalid_scope") {
|
|
124
|
+
if (!orgScope && org && st.status === 400 && errorCode(st.body) === "invalid_scope") {
|
|
106
125
|
if (org.from === "flag") {
|
|
107
126
|
io.err(`mnemom agent host: this Mnemom server cannot enroll a machine into ${org.orgId} yet.`);
|
|
108
127
|
io.err("fix: `mnemom agent host enroll` without --org (joins your personal org)");
|
|
@@ -115,6 +134,21 @@ export async function enrollDevice(io, opts = {}) {
|
|
|
115
134
|
const start = st.status === 202 || st.status === 200 || st.status === 201
|
|
116
135
|
? parseEnrollStart(st.body)
|
|
117
136
|
: null;
|
|
137
|
+
if (!start && orgScope && org) {
|
|
138
|
+
const code = errorCode(st.body);
|
|
139
|
+
if (code === CODE.durable_approval_not_eligible) {
|
|
140
|
+
// 34.03.3: only an owner or admin of the org may enroll an org runner.
|
|
141
|
+
io.err(`mnemom agent host: only an owner or admin of ${org.orgId} can enroll a machine as its runner.`);
|
|
142
|
+
io.err(`fix: ask an owner or admin of ${org.orgId} to run \`mnemom agent host enroll --scope org --org ${org.orgId}\` on this machine`);
|
|
143
|
+
return 1;
|
|
144
|
+
}
|
|
145
|
+
if (code === CODE.kind_not_allowed) {
|
|
146
|
+
// The org is not on the server's pool-claim list, or the server predates host org runners.
|
|
147
|
+
io.err(`mnemom agent host: this Mnemom server does not accept this machine as a runner for ${org.orgId} (org runners are not turned on for it, or the server does not take host runners yet).`);
|
|
148
|
+
io.err("fix: `mnemom agent host enroll` without --scope org enrolls it for your own sessions");
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
118
152
|
if (!start && org && errorCode(st.body) === CODE.durable_approval_not_eligible) {
|
|
119
153
|
// 34.03.4: only a member of the org may enroll a machine into it.
|
|
120
154
|
io.err(`mnemom agent host: you are not a member of ${org.orgId}, so this machine cannot join it.`);
|
|
@@ -173,7 +207,15 @@ export async function enrollDevice(io, opts = {}) {
|
|
|
173
207
|
boot_id: io.bootId(),
|
|
174
208
|
});
|
|
175
209
|
if (io.json)
|
|
176
|
-
io.out(JSON.stringify({
|
|
210
|
+
io.out(JSON.stringify({
|
|
211
|
+
event: "enrolled",
|
|
212
|
+
executor_id: p.executorId,
|
|
213
|
+
...(orgScope ? { scope: "org" } : {}),
|
|
214
|
+
}));
|
|
215
|
+
else if (orgScope && org) {
|
|
216
|
+
io.out(`Enrolled this machine as a runner for ${org.orgId} (executor ${p.executorId.slice(0, 8)}).`);
|
|
217
|
+
io.out(`To claim its pool jobs: \`mnemom agent run --watch\` with MNEMOM_EXECUTOR_KEY set to executor_key in ${paths.stateFile}`);
|
|
218
|
+
}
|
|
177
219
|
else
|
|
178
220
|
io.out(`Enrolled this machine (executor ${p.executorId.slice(0, 8)}).`);
|
|
179
221
|
return 0;
|
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
|
-
.
|
|
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
|
}
|
|
@@ -1664,8 +1664,9 @@ var M1_CLI_SURFACE = Object.freeze([
|
|
|
1664
1664
|
entry(["agent", "done"], ["--json"]),
|
|
1665
1665
|
// BACKBONE C2 (`mnemom agent host` replaces `mnemom host`, amendment 2; design-v3 row 7).
|
|
1666
1666
|
entry(["agent", "host"]),
|
|
1667
|
-
// BACKBONE C2 enroll (device step); --org: ruling 34.03.4 (a host joins an org its user belongs to)
|
|
1668
|
-
|
|
1667
|
+
// BACKBONE C2 enroll (device step); --org: ruling 34.03.4 (a host joins an org its user belongs to);
|
|
1668
|
+
// --scope: the any-machine amendment (Wassim 2026-11-09; `--scope org` enrolls an org runner).
|
|
1669
|
+
entry(["agent", "host", "enroll"], ["--org", "--scope"]),
|
|
1669
1670
|
// BACKBONE C2 install/status/uninstall; --json: C04 (`mnemom agent host install --json`),
|
|
1670
1671
|
// ruling 32.31.
|
|
1671
1672
|
entry(["agent", "host", "install"], ["--json"]),
|
|
@@ -2379,7 +2380,7 @@ function poolClaimOrgs(flagValue) {
|
|
|
2379
2380
|
return [...new Set(orgs)];
|
|
2380
2381
|
}
|
|
2381
2382
|
var EXECUTOR_SCOPES = ["user", "org"];
|
|
2382
|
-
var ORG_SCOPE_EXECUTOR_KINDS = ["container", "fleet"];
|
|
2383
|
+
var ORG_SCOPE_EXECUTOR_KINDS = ["container", "fleet", "host"];
|
|
2383
2384
|
var OIDC_EXCHANGE_REFUSALS = [
|
|
2384
2385
|
"bad_issuer",
|
|
2385
2386
|
"repo_not_enrolled",
|
|
@@ -2406,6 +2407,8 @@ var OIDC_EXCHANGE_BODY_KEYS = ["oidc_token", "adopt_ticket"];
|
|
|
2406
2407
|
var OIDC_TOKEN_MAX = 8192;
|
|
2407
2408
|
var OIDC_WORKFLOW_ENV = "DISPATCH_WAKEUP_WORKFLOW";
|
|
2408
2409
|
var OIDC_WORKFLOW_FILE_RE = /^[A-Za-z0-9_.-]{1,100}\.ya?ml$/;
|
|
2410
|
+
var OIDC_WORKFLOW_SET_ENV = "DURABLE_OIDC_WORKFLOWS";
|
|
2411
|
+
var OIDC_WORKFLOW_SET_MAX = 8;
|
|
2409
2412
|
function adoptTicketOk(t) {
|
|
2410
2413
|
return typeof t === "string" && ADOPT_TICKET_RE.test(t);
|
|
2411
2414
|
}
|
|
@@ -2422,9 +2425,22 @@ function oidcPinnedWorkflow(envValue) {
|
|
|
2422
2425
|
const wf = envValue?.trim() ?? "";
|
|
2423
2426
|
return OIDC_WORKFLOW_FILE_RE.test(wf) ? wf : null;
|
|
2424
2427
|
}
|
|
2428
|
+
function oidcWorkflowSet(setValue, pinValue) {
|
|
2429
|
+
if (setValue === void 0) {
|
|
2430
|
+
const pin = oidcPinnedWorkflow(pinValue);
|
|
2431
|
+
return pin === null ? null : [pin];
|
|
2432
|
+
}
|
|
2433
|
+
const names = setValue.split(",").map((n) => n.trim());
|
|
2434
|
+
if (names.some((n) => !OIDC_WORKFLOW_FILE_RE.test(n) || n.includes(".."))) return null;
|
|
2435
|
+
const set = [...new Set(names)];
|
|
2436
|
+
return set.length > OIDC_WORKFLOW_SET_MAX ? null : set;
|
|
2437
|
+
}
|
|
2425
2438
|
function jobWorkflowRefOk(jobWorkflowRef, repository, workflowFile, defaultBranch) {
|
|
2426
2439
|
return typeof jobWorkflowRef === "string" && jobWorkflowRef === `${repository}/.github/workflows/${workflowFile}@refs/heads/${defaultBranch}`;
|
|
2427
2440
|
}
|
|
2441
|
+
function jobWorkflowRefInSet(jobWorkflowRef, repository, workflowFiles, defaultBranch) {
|
|
2442
|
+
return workflowFiles.some((f2) => jobWorkflowRefOk(jobWorkflowRef, repository, f2, defaultBranch));
|
|
2443
|
+
}
|
|
2428
2444
|
var ORG_POLICY_LIMIT_KEYS = ["per_session_budget_usd", "live_lease_cap"];
|
|
2429
2445
|
var LIVE_LEASE_CAP_MIN = 1;
|
|
2430
2446
|
var LIVE_LEASE_CAP_MAX = 1e3;
|
|
@@ -5908,6 +5924,8 @@ var FREEZE_RECORD = Object.freeze({
|
|
|
5908
5924
|
OIDC_TOKEN_MAX,
|
|
5909
5925
|
OIDC_WORKFLOW_ENV,
|
|
5910
5926
|
OIDC_WORKFLOW_FILE_PATTERN: OIDC_WORKFLOW_FILE_RE.source,
|
|
5927
|
+
OIDC_WORKFLOW_SET_ENV,
|
|
5928
|
+
OIDC_WORKFLOW_SET_MAX,
|
|
5911
5929
|
READY_SESSIONS_LIMIT_MIN,
|
|
5912
5930
|
READY_SESSIONS_LIMIT_MAX,
|
|
5913
5931
|
// amendment 64
|
|
@@ -6616,6 +6634,8 @@ export {
|
|
|
6616
6634
|
OIDC_TOKEN_MAX,
|
|
6617
6635
|
OIDC_WORKFLOW_ENV,
|
|
6618
6636
|
OIDC_WORKFLOW_FILE_RE,
|
|
6637
|
+
OIDC_WORKFLOW_SET_ENV,
|
|
6638
|
+
OIDC_WORKFLOW_SET_MAX,
|
|
6619
6639
|
OLD_ROUTE_BRANCH,
|
|
6620
6640
|
OLD_ROUTE_REFRESH,
|
|
6621
6641
|
ORG_CAP_LOCK_PREFIX,
|
|
@@ -6928,6 +6948,7 @@ export {
|
|
|
6928
6948
|
isValidSessionName,
|
|
6929
6949
|
isValidStateReason,
|
|
6930
6950
|
isWellFormedLeaseKey,
|
|
6951
|
+
jobWorkflowRefInSet,
|
|
6931
6952
|
jobWorkflowRefOk,
|
|
6932
6953
|
lagBucket,
|
|
6933
6954
|
lintDurableOnly,
|
|
@@ -6943,6 +6964,7 @@ export {
|
|
|
6943
6964
|
oidcExchangeBodyOk,
|
|
6944
6965
|
oidcPinnedWorkflow,
|
|
6945
6966
|
oidcRunClaimsOk,
|
|
6967
|
+
oidcWorkflowSet,
|
|
6946
6968
|
openContract,
|
|
6947
6969
|
ownerSessionViewOk,
|
|
6948
6970
|
ownerVerbDecision,
|
|
@@ -36,8 +36,9 @@ export const M1_CLI_SURFACE = Object.freeze([
|
|
|
36
36
|
entry(["agent", "done"], ["--json"]),
|
|
37
37
|
// BACKBONE C2 (`mnemom agent host` replaces `mnemom host`, amendment 2; design-v3 row 7).
|
|
38
38
|
entry(["agent", "host"]),
|
|
39
|
-
// BACKBONE C2 enroll (device step); --org: ruling 34.03.4 (a host joins an org its user belongs to)
|
|
40
|
-
|
|
39
|
+
// BACKBONE C2 enroll (device step); --org: ruling 34.03.4 (a host joins an org its user belongs to);
|
|
40
|
+
// --scope: the any-machine amendment (Wassim 2026-11-09; `--scope org` enrolls an org runner).
|
|
41
|
+
entry(["agent", "host", "enroll"], ["--org", "--scope"]),
|
|
41
42
|
// BACKBONE C2 install/status/uninstall; --json: C04 (`mnemom agent host install --json`),
|
|
42
43
|
// ruling 32.31.
|
|
43
44
|
entry(["agent", "host", "install"], ["--json"]),
|
|
@@ -31,8 +31,13 @@ export declare function isPoolSession(s: PoolSessionFacts, flagValue: string | u
|
|
|
31
31
|
export declare function poolClaimOrgs(flagValue: string | undefined): string[];
|
|
32
32
|
export declare const EXECUTOR_SCOPES: readonly ["user", "org"];
|
|
33
33
|
export type ExecutorScope = (typeof EXECUTOR_SCOPES)[number];
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Only these executor kinds may enroll with scope org. The any-machine amendment (Wassim, 2026-11-09):
|
|
36
|
+
* any enrolled machine may be an org runner, so `host` (a Mac mini, laptop or VM) joins container and
|
|
37
|
+
* fleet, appended. The guard is the org owner or admin's aal2 device-step approval at enrollment
|
|
38
|
+
* and the org's DURABLE_POOL_CLAIM entry, not the machine kind.
|
|
39
|
+
*/
|
|
40
|
+
export declare const ORG_SCOPE_EXECUTOR_KINDS: readonly ["container", "fleet", "host"];
|
|
36
41
|
/**
|
|
37
42
|
* exec_oidc_exchange refusals (ruling 34.03's S1 changes), each 403, checked in this order.
|
|
38
43
|
* Amendment 65 (mnemom-test#1331) appends the two single-use codes, checked in the same transaction
|
|
@@ -83,6 +88,16 @@ export declare const OIDC_TOKEN_MAX = 8192;
|
|
|
83
88
|
export declare const OIDC_WORKFLOW_ENV = "DISPATCH_WAKEUP_WORKFLOW";
|
|
84
89
|
/** A workflow file name under .github/workflows: no path, no query. */
|
|
85
90
|
export declare const OIDC_WORKFLOW_FILE_RE: RegExp;
|
|
91
|
+
/**
|
|
92
|
+
* Amendment 66, part 2 (Durable lead ruling (a), 2026-11-09): the api env var naming the set of
|
|
93
|
+
* workflow files the exchange accepts, comma-separated bare file names. Unset, the set is
|
|
94
|
+
* OIDC_WORKFLOW_ENV's one file, exactly as before. Set, it replaces that pin for the exchange only
|
|
95
|
+
* (the wake-up still dispatches OIDC_WORKFLOW_ENV's file), and a set that is empty or holds any
|
|
96
|
+
* entry that is not a workflow file name (a wildcard, a "/", a "..") turns the exchange dark.
|
|
97
|
+
*/
|
|
98
|
+
export declare const OIDC_WORKFLOW_SET_ENV = "DURABLE_OIDC_WORKFLOWS";
|
|
99
|
+
/** The most workflow files the set may name. */
|
|
100
|
+
export declare const OIDC_WORKFLOW_SET_MAX = 8;
|
|
86
101
|
/** True when `t` has an adopt ticket's shape. */
|
|
87
102
|
export declare function adoptTicketOk(t: unknown): t is string;
|
|
88
103
|
/**
|
|
@@ -93,6 +108,14 @@ export declare function adoptTicketOk(t: unknown): t is string;
|
|
|
93
108
|
export declare function oidcExchangeBodyOk(v: unknown): boolean;
|
|
94
109
|
/** The pinned workflow file from the env value, or null (then the exchange is dark). */
|
|
95
110
|
export declare function oidcPinnedWorkflow(envValue: string | undefined): string | null;
|
|
111
|
+
/**
|
|
112
|
+
* The workflow files the exchange accepts, or null (then the exchange is dark).
|
|
113
|
+
* `setValue` is OIDC_WORKFLOW_SET_ENV's value, `pinValue` OIDC_WORKFLOW_ENV's. With the set unset
|
|
114
|
+
* the answer is the one pinned file, as oidcPinnedWorkflow reads it. With it set, every
|
|
115
|
+
* comma-separated entry, trimmed, must be a workflow file name with no "..", and there must be one
|
|
116
|
+
* to OIDC_WORKFLOW_SET_MAX of them; anything else is null, never a partial set. Duplicates collapse.
|
|
117
|
+
*/
|
|
118
|
+
export declare function oidcWorkflowSet(setValue: string | undefined, pinValue: string | undefined): readonly string[] | null;
|
|
96
119
|
/**
|
|
97
120
|
* The workflow pin: the token's job_workflow_ref must be exactly
|
|
98
121
|
* `<repository>/.github/workflows/<pinned file>@refs/heads/<default branch>`, with `repository`
|
|
@@ -100,6 +123,8 @@ export declare function oidcPinnedWorkflow(envValue: string | undefined): string
|
|
|
100
123
|
* branch, a tag or sha ref) is refused ref_not_allowed.
|
|
101
124
|
*/
|
|
102
125
|
export declare function jobWorkflowRefOk(jobWorkflowRef: unknown, repository: string, workflowFile: string, defaultBranch: string): boolean;
|
|
126
|
+
/** jobWorkflowRefOk for any one file of an oidcWorkflowSet answer (amendment 66, part 2). */
|
|
127
|
+
export declare function jobWorkflowRefInSet(jobWorkflowRef: unknown, repository: string, workflowFiles: readonly string[], defaultBranch: string): boolean;
|
|
103
128
|
/** durable_org_policy.limits keys (34.05 (1)); live_lease_cap is optional. */
|
|
104
129
|
export declare const ORG_POLICY_LIMIT_KEYS: readonly ["per_session_budget_usd", "live_lease_cap"];
|
|
105
130
|
/** live_lease_cap and the registry default share this range. */
|
|
@@ -43,8 +43,13 @@ export function poolClaimOrgs(flagValue) {
|
|
|
43
43
|
}
|
|
44
44
|
// ---- Executor scope (ruling 34.03 (1)) ------------------------------------------------------
|
|
45
45
|
export const EXECUTOR_SCOPES = ["user", "org"];
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Only these executor kinds may enroll with scope org. The any-machine amendment (Wassim, 2026-11-09):
|
|
48
|
+
* any enrolled machine may be an org runner, so `host` (a Mac mini, laptop or VM) joins container and
|
|
49
|
+
* fleet, appended. The guard is the org owner or admin's aal2 device-step approval at enrollment
|
|
50
|
+
* and the org's DURABLE_POOL_CLAIM entry, not the machine kind.
|
|
51
|
+
*/
|
|
52
|
+
export const ORG_SCOPE_EXECUTOR_KINDS = ["container", "fleet", "host"];
|
|
48
53
|
/**
|
|
49
54
|
* exec_oidc_exchange refusals (ruling 34.03's S1 changes), each 403, checked in this order.
|
|
50
55
|
* Amendment 65 (mnemom-test#1331) appends the two single-use codes, checked in the same transaction
|
|
@@ -113,6 +118,16 @@ export const OIDC_TOKEN_MAX = 8192;
|
|
|
113
118
|
export const OIDC_WORKFLOW_ENV = "DISPATCH_WAKEUP_WORKFLOW";
|
|
114
119
|
/** A workflow file name under .github/workflows: no path, no query. */
|
|
115
120
|
export const OIDC_WORKFLOW_FILE_RE = /^[A-Za-z0-9_.-]{1,100}\.ya?ml$/;
|
|
121
|
+
/**
|
|
122
|
+
* Amendment 66, part 2 (Durable lead ruling (a), 2026-11-09): the api env var naming the set of
|
|
123
|
+
* workflow files the exchange accepts, comma-separated bare file names. Unset, the set is
|
|
124
|
+
* OIDC_WORKFLOW_ENV's one file, exactly as before. Set, it replaces that pin for the exchange only
|
|
125
|
+
* (the wake-up still dispatches OIDC_WORKFLOW_ENV's file), and a set that is empty or holds any
|
|
126
|
+
* entry that is not a workflow file name (a wildcard, a "/", a "..") turns the exchange dark.
|
|
127
|
+
*/
|
|
128
|
+
export const OIDC_WORKFLOW_SET_ENV = "DURABLE_OIDC_WORKFLOWS";
|
|
129
|
+
/** The most workflow files the set may name. */
|
|
130
|
+
export const OIDC_WORKFLOW_SET_MAX = 8;
|
|
116
131
|
/** True when `t` has an adopt ticket's shape. */
|
|
117
132
|
export function adoptTicketOk(t) {
|
|
118
133
|
return typeof t === "string" && ADOPT_TICKET_RE.test(t);
|
|
@@ -138,6 +153,24 @@ export function oidcPinnedWorkflow(envValue) {
|
|
|
138
153
|
const wf = envValue?.trim() ?? "";
|
|
139
154
|
return OIDC_WORKFLOW_FILE_RE.test(wf) ? wf : null;
|
|
140
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The workflow files the exchange accepts, or null (then the exchange is dark).
|
|
158
|
+
* `setValue` is OIDC_WORKFLOW_SET_ENV's value, `pinValue` OIDC_WORKFLOW_ENV's. With the set unset
|
|
159
|
+
* the answer is the one pinned file, as oidcPinnedWorkflow reads it. With it set, every
|
|
160
|
+
* comma-separated entry, trimmed, must be a workflow file name with no "..", and there must be one
|
|
161
|
+
* to OIDC_WORKFLOW_SET_MAX of them; anything else is null, never a partial set. Duplicates collapse.
|
|
162
|
+
*/
|
|
163
|
+
export function oidcWorkflowSet(setValue, pinValue) {
|
|
164
|
+
if (setValue === undefined) {
|
|
165
|
+
const pin = oidcPinnedWorkflow(pinValue);
|
|
166
|
+
return pin === null ? null : [pin];
|
|
167
|
+
}
|
|
168
|
+
const names = setValue.split(",").map((n) => n.trim());
|
|
169
|
+
if (names.some((n) => !OIDC_WORKFLOW_FILE_RE.test(n) || n.includes("..")))
|
|
170
|
+
return null;
|
|
171
|
+
const set = [...new Set(names)];
|
|
172
|
+
return set.length > OIDC_WORKFLOW_SET_MAX ? null : set;
|
|
173
|
+
}
|
|
141
174
|
/**
|
|
142
175
|
* The workflow pin: the token's job_workflow_ref must be exactly
|
|
143
176
|
* `<repository>/.github/workflows/<pinned file>@refs/heads/<default branch>`, with `repository`
|
|
@@ -148,6 +181,10 @@ export function jobWorkflowRefOk(jobWorkflowRef, repository, workflowFile, defau
|
|
|
148
181
|
return (typeof jobWorkflowRef === "string" &&
|
|
149
182
|
jobWorkflowRef === `${repository}/.github/workflows/${workflowFile}@refs/heads/${defaultBranch}`);
|
|
150
183
|
}
|
|
184
|
+
/** jobWorkflowRefOk for any one file of an oidcWorkflowSet answer (amendment 66, part 2). */
|
|
185
|
+
export function jobWorkflowRefInSet(jobWorkflowRef, repository, workflowFiles, defaultBranch) {
|
|
186
|
+
return workflowFiles.some((f) => jobWorkflowRefOk(jobWorkflowRef, repository, f, defaultBranch));
|
|
187
|
+
}
|
|
151
188
|
// ---- Per-org live-lease cap (rulings 34.05, 34.05.1) ----------------------------------------
|
|
152
189
|
/** durable_org_policy.limits keys (34.05 (1)); live_lease_cap is optional. */
|
|
153
190
|
export const ORG_POLICY_LIMIT_KEYS = ["per_session_budget_usd", "live_lease_cap"];
|
|
@@ -128,6 +128,8 @@ export declare const FREEZE_RECORD: Readonly<{
|
|
|
128
128
|
OIDC_TOKEN_MAX: 8192;
|
|
129
129
|
OIDC_WORKFLOW_ENV: "DISPATCH_WAKEUP_WORKFLOW";
|
|
130
130
|
OIDC_WORKFLOW_FILE_PATTERN: string;
|
|
131
|
+
OIDC_WORKFLOW_SET_ENV: "DURABLE_OIDC_WORKFLOWS";
|
|
132
|
+
OIDC_WORKFLOW_SET_MAX: 8;
|
|
131
133
|
READY_SESSIONS_LIMIT_MIN: 1;
|
|
132
134
|
READY_SESSIONS_LIMIT_MAX: 50;
|
|
133
135
|
ADOPT_SOURCE_REF_MAX: 500;
|
|
@@ -748,7 +750,7 @@ export declare const FREEZE_STRUCTURED: Readonly<{
|
|
|
748
750
|
}>;
|
|
749
751
|
CLEAR_WAIT_OUTCOMES: readonly ["cleared", "stale_ref", "already_clear"];
|
|
750
752
|
EXECUTOR_SCOPES: readonly ["user", "org"];
|
|
751
|
-
ORG_SCOPE_EXECUTOR_KINDS: readonly ["container", "fleet"];
|
|
753
|
+
ORG_SCOPE_EXECUTOR_KINDS: readonly ["container", "fleet", "host"];
|
|
752
754
|
OIDC_EXCHANGE_REFUSALS: readonly ["bad_issuer", "repo_not_enrolled", "ref_not_allowed", "pool_flag_off", "adopt_ticket_invalid", "token_replayed", "run_already_exchanged"];
|
|
753
755
|
OIDC_EXCHANGE_BODY_KEYS: readonly ["oidc_token", "adopt_ticket"];
|
|
754
756
|
OIDC_EXCHANGE_RUN_CLAIMS: readonly ["jti", "run_id", "run_attempt"];
|
|
@@ -159,6 +159,8 @@ export const FREEZE_RECORD = Object.freeze({
|
|
|
159
159
|
OIDC_TOKEN_MAX: D.OIDC_TOKEN_MAX,
|
|
160
160
|
OIDC_WORKFLOW_ENV: D.OIDC_WORKFLOW_ENV,
|
|
161
161
|
OIDC_WORKFLOW_FILE_PATTERN: D.OIDC_WORKFLOW_FILE_RE.source,
|
|
162
|
+
OIDC_WORKFLOW_SET_ENV: D.OIDC_WORKFLOW_SET_ENV,
|
|
163
|
+
OIDC_WORKFLOW_SET_MAX: D.OIDC_WORKFLOW_SET_MAX,
|
|
162
164
|
READY_SESSIONS_LIMIT_MIN: D.READY_SESSIONS_LIMIT_MIN,
|
|
163
165
|
READY_SESSIONS_LIMIT_MAX: D.READY_SESSIONS_LIMIT_MAX,
|
|
164
166
|
// amendment 64
|