@mnemom/mnemom 0.17.33 → 0.17.34-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-url-proxy.mjs +18 -4
- package/dist/commands/agent-config.js +5 -1
- package/dist/commands/agent-doctor.d.ts +9 -0
- package/dist/commands/agent-doctor.js +4 -0
- package/dist/commands/agent-session.d.ts +1 -2
- package/dist/commands/agent-session.js +1 -2
- package/dist/commands/agent-setup.d.ts +6 -0
- package/dist/commands/agent-setup.js +12 -1
- package/dist/commands/agent-share-render.d.ts +1 -1
- package/dist/commands/agent-share-render.js +1 -1
- package/dist/commands/agent.d.ts +26 -0
- package/dist/commands/agent.js +228 -53
- package/dist/commands/durable-doctor.d.ts +50 -0
- package/dist/commands/durable-doctor.js +186 -0
- package/dist/commands/durable-lock.d.ts +254 -0
- package/dist/commands/durable-lock.js +595 -0
- package/dist/commands/durable-sessions.d.ts +87 -0
- package/dist/commands/durable-sessions.js +303 -0
- package/dist/commands/durable-verbs.d.ts +116 -0
- package/dist/commands/durable-verbs.js +393 -0
- package/dist/commands/protection.js +1 -1
- package/dist/commands/scan.d.ts +97 -0
- package/dist/commands/scan.js +238 -0
- package/dist/durable/flock.d.ts +26 -0
- package/dist/durable/flock.js +80 -0
- package/dist/durable/launch-spec.d.ts +7 -1
- package/dist/durable/launch-spec.js +10 -3
- package/dist/durable/seat-proxy-entry.d.ts +11 -0
- package/dist/durable/seat-proxy-entry.js +25 -0
- package/dist/durable/seat-proxy.d.ts +107 -0
- package/dist/durable/seat-proxy.js +599 -0
- package/dist/durable/seat-socket.d.ts +46 -0
- package/dist/durable/seat-socket.js +179 -0
- package/dist/durable/seat-spec-validator.d.ts +28 -0
- package/dist/durable/seat-spec-validator.js +59 -0
- package/dist/durable/test-mode.mjs +81 -0
- package/dist/durable/turn-chain.d.ts +55 -0
- package/dist/durable/turn-chain.js +178 -0
- package/dist/host/auto-continue.d.ts +67 -0
- package/dist/host/auto-continue.js +118 -0
- package/dist/host/bootid.d.ts +13 -0
- package/dist/host/bootid.js +41 -0
- package/dist/host/client.d.ts +123 -0
- package/dist/host/client.js +171 -0
- package/dist/host/commands.d.ts +2 -0
- package/dist/host/commands.js +122 -0
- package/dist/host/control.d.ts +33 -0
- package/dist/host/control.js +144 -0
- package/dist/host/daemon.d.ts +217 -0
- package/dist/host/daemon.js +606 -0
- package/dist/host/gate.d.ts +46 -0
- package/dist/host/gate.js +82 -0
- package/dist/host/grant.d.ts +24 -0
- package/dist/host/grant.js +24 -0
- package/dist/host/instance.d.ts +35 -0
- package/dist/host/instance.js +62 -0
- package/dist/host/integrity.d.ts +35 -0
- package/dist/host/integrity.js +115 -0
- package/dist/host/launchd.d.ts +16 -0
- package/dist/host/launchd.js +45 -0
- package/dist/host/lock.d.ts +20 -0
- package/dist/host/lock.js +63 -0
- package/dist/host/owner.d.ts +55 -0
- package/dist/host/owner.js +121 -0
- package/dist/host/plist.d.ts +16 -0
- package/dist/host/plist.js +85 -0
- package/dist/host/procs.d.ts +27 -0
- package/dist/host/procs.js +105 -0
- package/dist/host/run.d.ts +66 -0
- package/dist/host/run.js +314 -0
- package/dist/host/runtime.d.ts +8 -0
- package/dist/host/runtime.js +22 -0
- package/dist/host/s1-names.d.ts +22 -0
- package/dist/host/s1-names.js +21 -0
- package/dist/host/seat-credential.d.ts +27 -0
- package/dist/host/seat-credential.js +41 -0
- package/dist/host/seat-launch-main.d.ts +1 -0
- package/dist/host/seat-launch-main.js +59 -0
- package/dist/host/seat-launch.d.ts +116 -0
- package/dist/host/seat-launch.js +302 -0
- package/dist/host/seat.d.ts +48 -0
- package/dist/host/seat.js +149 -0
- package/dist/host/state.d.ts +33 -0
- package/dist/host/state.js +117 -0
- package/dist/host/status.d.ts +101 -0
- package/dist/host/status.js +307 -0
- package/dist/host/tmux.d.ts +56 -0
- package/dist/host/tmux.js +182 -0
- package/dist/host/transcript.d.ts +30 -0
- package/dist/host/transcript.js +130 -0
- package/dist/host/verbs.d.ts +50 -0
- package/dist/host/verbs.js +271 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +232 -35
- package/dist/lib/agent.d.ts +8 -26
- package/dist/lib/agent.js +11 -26
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +8 -0
- package/dist/lib/claude-plugin.d.ts +11 -2
- package/dist/lib/claude-plugin.js +10 -1
- package/dist/lib/durable/api.d.ts +136 -0
- package/dist/lib/durable/api.js +220 -0
- package/dist/lib/durable/codes.d.ts +8 -0
- package/dist/lib/durable/codes.js +2 -0
- package/dist/lib/durable/first-turn.d.ts +50 -0
- package/dist/lib/durable/first-turn.js +102 -0
- package/dist/lib/durable/gate.d.ts +48 -0
- package/dist/lib/durable/gate.js +122 -0
- package/dist/lib/durable/handover.d.ts +101 -0
- package/dist/lib/durable/handover.js +175 -0
- package/dist/lib/durable/launch.d.ts +182 -0
- package/dist/lib/durable/launch.js +443 -0
- package/dist/lib/durable/launching.d.ts +15 -0
- package/dist/lib/durable/launching.js +70 -0
- package/dist/lib/durable/live.d.ts +194 -0
- package/dist/lib/durable/live.js +635 -0
- package/dist/lib/durable/refusal.d.ts +57 -0
- package/dist/lib/durable/refusal.js +78 -0
- package/dist/lib/durable/view.d.ts +83 -0
- package/dist/lib/durable/view.js +244 -0
- package/dist/lib/experimental.d.ts +0 -9
- package/dist/lib/experimental.js +18 -17
- package/dist/lib/launcher-config.d.ts +7 -0
- package/dist/lib/launcher-config.js +21 -0
- package/dist/lib/scan/caps.d.ts +35 -0
- package/dist/lib/scan/caps.js +67 -0
- package/dist/lib/scan/credential.d.ts +37 -0
- package/dist/lib/scan/credential.js +104 -0
- package/dist/lib/scan/failure.d.ts +10 -0
- package/dist/lib/scan/failure.js +43 -0
- package/dist/lib/scan/pr-source.d.ts +57 -0
- package/dist/lib/scan/pr-source.js +130 -0
- package/dist/lib/scan/record.d.ts +49 -0
- package/dist/lib/scan/record.js +85 -0
- package/dist/lib/scan/report-email.d.ts +92 -0
- package/dist/lib/scan/report-email.js +144 -0
- package/dist/lib/scan/report.d.ts +13 -0
- package/dist/lib/scan/report.js +30 -0
- package/dist/lib/scan/scan.d.ts +64 -0
- package/dist/lib/scan/scan.js +182 -0
- package/dist/lib/scan/share.d.ts +38 -0
- package/dist/lib/scan/share.js +47 -0
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
- package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
- package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
- package/dist/lib/scan/spend/anthropic-usage.js +244 -0
- package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
- package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
- package/dist/lib/scan/spend/prices.d.ts +33 -0
- package/dist/lib/scan/spend/prices.js +57 -0
- package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
- package/dist/lib/scan/spend/spend-estimate.js +70 -0
- package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
- package/dist/lib/scan/spend/spend-tiers.js +42 -0
- package/dist/lib/scan/spend/token-rate.d.ts +30 -0
- package/dist/lib/scan/spend/token-rate.js +54 -0
- package/dist/lib/session-key.d.ts +41 -0
- package/dist/lib/session-key.js +124 -0
- package/dist/lib/subscription-login.d.ts +15 -0
- package/dist/lib/subscription-login.js +28 -1
- package/dist/rc-key-401.mjs +75 -0
- package/dist/rc-proxy.mjs +221 -25
- package/dist/vendor/durable-contract/index.js +240 -18
- package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
- package/dist/vendor/shared/durable-contract/admission.js +533 -0
- package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
- package/dist/vendor/shared/durable-contract/arm.js +133 -0
- package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/canonical.js +98 -0
- package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
- package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
- package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
- package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
- package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
- package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
- package/dist/vendor/shared/durable-contract/codes.js +475 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
- package/dist/vendor/shared/durable-contract/constants.js +179 -0
- package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/doors.js +249 -0
- package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
- package/dist/vendor/shared/durable-contract/envelope.js +317 -0
- package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
- package/dist/vendor/shared/durable-contract/events.js +630 -0
- package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
- package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
- package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
- package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
- package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
- package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
- package/dist/vendor/shared/durable-contract/index.js +37 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
- package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
- package/dist/vendor/shared/durable-contract/keys.js +66 -0
- package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
- package/dist/vendor/shared/durable-contract/lock.js +151 -0
- package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
- package/dist/vendor/shared/durable-contract/names.js +78 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
- package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
- package/dist/vendor/shared/durable-contract/paths.js +35 -0
- package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
- package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
- package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
- package/dist/vendor/shared/durable-contract/routes.js +315 -0
- package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
- package/dist/vendor/shared/durable-contract/schema.js +441 -0
- package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
- package/dist/vendor/shared/durable-contract/seal.js +99 -0
- package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
- package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
- package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
- package/dist/vendor/shared/durable-contract/session-id.js +58 -0
- package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
- package/dist/vendor/shared/durable-contract/spec.js +286 -0
- package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
- package/dist/vendor/shared/durable-contract/states.js +218 -0
- package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
- package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
- package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
- package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
- package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
- package/dist/vendor/shared/durable-contract/turn.js +81 -0
- package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
- package/dist/vendor/shared/durable-contract/vectors.js +208 -0
- package/package.json +4 -3
- package/plugins/integrity-manifest.json +35 -0
- package/plugins/mnemom/hooks/hooks.json +15 -1
- package/plugins/mnemom/hooks/register.tsx +60 -2
- package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
- package/dist/commands/agent-implicit-notice.d.ts +0 -31
- package/dist/commands/agent-implicit-notice.js +0 -73
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host daemon's durable gate (C12-R25, R39, R40; BACKBONE Flags (CLI) carve-out).
|
|
3
|
+
*
|
|
4
|
+
* Fresh `mnemom agent host …` verbs resolve the gate like every other command (config.json and
|
|
5
|
+
* MNEMOM_EXPERIMENTAL). The daemon (`host run`) and `host install` resolve it from config.json
|
|
6
|
+
* only: the plist and the launchd domain never decide it.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import { resolveExperimental, gatedMessage } from "../lib/experimental.js";
|
|
10
|
+
import { FIX_LINES, renderFixLine } from "../vendor/shared/durable-contract/fixlines.js";
|
|
11
|
+
import { resolveInstance, hostPaths } from "./instance.js";
|
|
12
|
+
import { readExecutorState } from "./state.js";
|
|
13
|
+
export const DURABLE_ID = "durable";
|
|
14
|
+
export const HOST_WHAT = "`mnemom agent host`";
|
|
15
|
+
export const GATED_TEXT = gatedMessage(HOST_WHAT, DURABLE_ID);
|
|
16
|
+
/** The enable command (S1 FIX_LINES.daemon_idle body). */
|
|
17
|
+
export const ENABLE_FIX = FIX_LINES.daemon_idle.body;
|
|
18
|
+
/** C12-R40: durable on only through the shell. */
|
|
19
|
+
export const INSTALL_SHELL_ONLY_TEXT = `the host daemon reads your config, not this shell: ${ENABLE_FIX}`;
|
|
20
|
+
/** C12-R39: the idle line (status and the launcher). */
|
|
21
|
+
export const IDLE_TEXT = FIX_LINES.daemon_idle.lead.replace(/: $/, "");
|
|
22
|
+
/** The idle line with its fix, rendered from S1 (C12-R39). */
|
|
23
|
+
export const IDLE_FIX = renderFixLine("daemon_idle", {
|
|
24
|
+
launcherKey: "always",
|
|
25
|
+
nameHeld: false,
|
|
26
|
+
params: {},
|
|
27
|
+
});
|
|
28
|
+
export const IDLE_LOG = "durable disabled: idle";
|
|
29
|
+
/** config.json's experimental block, read fresh (a missing or corrupt file is off). */
|
|
30
|
+
export function readStoredExperimental(configFile) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = JSON.parse(fs.readFileSync(configFile, "utf8"));
|
|
33
|
+
return raw && typeof raw === "object" ? raw.experimental : undefined;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The daemon gate: config.json only, env ignored (C12-R39). */
|
|
40
|
+
export function durableOnInConfig(configFile) {
|
|
41
|
+
return resolveExperimental({
|
|
42
|
+
stored: readStoredExperimental(configFile),
|
|
43
|
+
env: {},
|
|
44
|
+
}).enabled.includes(DURABLE_ID);
|
|
45
|
+
}
|
|
46
|
+
/** The fresh-invocation gate: config.json and the environment (C12-R25). */
|
|
47
|
+
export function durableOnFresh(configFile, env) {
|
|
48
|
+
return resolveExperimental({
|
|
49
|
+
stored: readStoredExperimental(configFile),
|
|
50
|
+
env: { ...env },
|
|
51
|
+
}).enabled.includes(DURABLE_ID);
|
|
52
|
+
}
|
|
53
|
+
/** `host install`: off everywhere -> exit 2 gated; on only in the shell -> exit 1 (R40); on in config -> ok. */
|
|
54
|
+
export function installGate(configFile, env) {
|
|
55
|
+
if (durableOnInConfig(configFile))
|
|
56
|
+
return { ok: true };
|
|
57
|
+
if (durableOnFresh(configFile, env))
|
|
58
|
+
return { ok: false, exit: 1, message: INSTALL_SHELL_ONLY_TEXT };
|
|
59
|
+
return { ok: false, exit: 2, message: GATED_TEXT };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The `host` verb-word gate (C17-R10, C12-R25), applied to the raw argv before commander runs,
|
|
63
|
+
* so that neither the bare word, nor a trailing `--help`/`--json`, nor an unknown sub-word
|
|
64
|
+
* (`host breakglass`) reaches commander's help, usage or unknown-command paths while durable is
|
|
65
|
+
* off. Reads config.json only; writes nothing, sends nothing, spawns nothing.
|
|
66
|
+
*
|
|
67
|
+
* The hidden `run` keeps its carve-out: the gate comes from config.json only (env ignored, R39),
|
|
68
|
+
* and it refuses only when there is no enrollment state file; an enrolled host goes on to take
|
|
69
|
+
* the lock and idle, which `run` itself decides.
|
|
70
|
+
*/
|
|
71
|
+
export function hostArgvGate(argv, ctx) {
|
|
72
|
+
if (argv[0] !== "agent" || argv[1] !== "host")
|
|
73
|
+
return null;
|
|
74
|
+
const paths = hostPaths(ctx.home, resolveInstance(ctx.env));
|
|
75
|
+
const gated = { code: 2, message: GATED_TEXT };
|
|
76
|
+
if (argv[2] === "run") {
|
|
77
|
+
if (durableOnInConfig(paths.configFile))
|
|
78
|
+
return null;
|
|
79
|
+
return readExecutorState(paths.stateFile).kind === "absent" ? gated : null;
|
|
80
|
+
}
|
|
81
|
+
return durableOnFresh(paths.configFile, ctx.env) ? null : gated;
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The renew command (S1 FIX_LINES.grant_expired). */
|
|
2
|
+
export declare const RENEW_FIX: string;
|
|
3
|
+
/** An expired grant, with the S1 renew fix. */
|
|
4
|
+
export declare const GRANT_EXPIRED_LINE: string;
|
|
5
|
+
/** The grant as status keeps it (no rendered copy). */
|
|
6
|
+
export type GrantView = {
|
|
7
|
+
state: "ok";
|
|
8
|
+
} | {
|
|
9
|
+
state: "warning";
|
|
10
|
+
days: number;
|
|
11
|
+
} | {
|
|
12
|
+
state: "expired";
|
|
13
|
+
};
|
|
14
|
+
export type GrantStatus = {
|
|
15
|
+
state: "ok";
|
|
16
|
+
} | {
|
|
17
|
+
state: "warning";
|
|
18
|
+
days: number;
|
|
19
|
+
line: string;
|
|
20
|
+
} | {
|
|
21
|
+
state: "expired";
|
|
22
|
+
};
|
|
23
|
+
/** ok above the S1 warning window (or with no known end), warning inside it, expired at or after the end. */
|
|
24
|
+
export declare function grantStatus(grantExpiresAtMs: number | null, nowMs: number): GrantStatus;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The executor grant as `host status` and `host install` report it (C04-R18, C12-R22). The
|
|
3
|
+
* warning window, the day count and both fix lines come from S1; only the expired line's lead
|
|
4
|
+
* is C2 copy.
|
|
5
|
+
*/
|
|
6
|
+
import { renderFixLine, grantWarningDays, } from "../vendor/shared/durable-contract/fixlines.js";
|
|
7
|
+
import { GRANT_WARNING_S } from "../vendor/shared/durable-contract/constants.js";
|
|
8
|
+
const fix = (key, params = {}) => renderFixLine(key, { launcherKey: "always", nameHeld: false, params });
|
|
9
|
+
/** The renew command (S1 FIX_LINES.grant_expired). */
|
|
10
|
+
export const RENEW_FIX = fix("grant_expired");
|
|
11
|
+
/** An expired grant, with the S1 renew fix. */
|
|
12
|
+
export const GRANT_EXPIRED_LINE = `this machine's grant has expired: ${RENEW_FIX}`;
|
|
13
|
+
/** ok above the S1 warning window (or with no known end), warning inside it, expired at or after the end. */
|
|
14
|
+
export function grantStatus(grantExpiresAtMs, nowMs) {
|
|
15
|
+
if (grantExpiresAtMs === null)
|
|
16
|
+
return { state: "ok" };
|
|
17
|
+
const remaining = (grantExpiresAtMs - nowMs) / 1000;
|
|
18
|
+
if (!(remaining > 0))
|
|
19
|
+
return { state: "expired" };
|
|
20
|
+
const days = grantWarningDays(remaining, GRANT_WARNING_S);
|
|
21
|
+
if (days === null)
|
|
22
|
+
return { state: "ok" };
|
|
23
|
+
return { state: "warning", days, line: fix("grant_warning", { days: String(days) }) };
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const DEFAULT_INSTANCE = "ai.mnemom.agent-host.default";
|
|
2
|
+
export interface HostInstance {
|
|
3
|
+
/** The launchd label; also the `instance` field of host status. */
|
|
4
|
+
readonly label: string;
|
|
5
|
+
/** tmux -L socket name. */
|
|
6
|
+
readonly tmuxSocket: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class InstanceNameError extends Error {
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveInstance(env: Readonly<Record<string, string | undefined>>): HostInstance;
|
|
11
|
+
/** Every path the host daemon and its verbs use, under one HOME. */
|
|
12
|
+
export interface HostPaths {
|
|
13
|
+
readonly home: string;
|
|
14
|
+
/** ~/.mnemom */
|
|
15
|
+
readonly mnemomDir: string;
|
|
16
|
+
/** ~/.mnemom/durable (0700) */
|
|
17
|
+
readonly durableDir: string;
|
|
18
|
+
/** S1 EXECUTOR_STATE_FILE */
|
|
19
|
+
readonly stateFile: string;
|
|
20
|
+
/** The single-instance lock: a separate, never-renamed file (C12-R03). */
|
|
21
|
+
readonly lockFile: string;
|
|
22
|
+
/** ~/.mnemom/config.json, the only source of the daemon's durable gate (C12-R39). */
|
|
23
|
+
readonly configFile: string;
|
|
24
|
+
readonly logDir: string;
|
|
25
|
+
/** Per-seat launch-spec files (host status spec_path). */
|
|
26
|
+
readonly specDir: string;
|
|
27
|
+
/** S1 transcript guard dir. */
|
|
28
|
+
readonly transcriptDir: string;
|
|
29
|
+
/** Short-lived daemon runtime files (control socket). */
|
|
30
|
+
readonly runDir: string;
|
|
31
|
+
/** Daemon runtime facts for host status (no secret). */
|
|
32
|
+
readonly runtimeFile: string;
|
|
33
|
+
readonly plistFile: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function hostPaths(home: string, instance: HostInstance): HostPaths;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host instance (C12-R04): its name (the launchd label), its tmux socket name and every
|
|
3
|
+
* path the daemon owns, all pure functions of HOME and the environment. The instance name is
|
|
4
|
+
* read from config/env, never from the executor state file, so a copied state file does not
|
|
5
|
+
* carry another host's instance.
|
|
6
|
+
*
|
|
7
|
+
* Resolution, first match wins:
|
|
8
|
+
* - MNEMOM_HOST_INSTANCE: the label itself; the tmux socket is MNEMOM_HOST_TMUX_SOCKET or
|
|
9
|
+
* `mnm-host-<last label segment>`.
|
|
10
|
+
* - MNEMOM_HOST_TMUX_SOCKET `mnm-host-<L>` (the local eval lanes): label
|
|
11
|
+
* `com.mnemom.durable.dev.<L>`; any other socket name: label `ai.mnemom.agent-host.<socket>`.
|
|
12
|
+
* - neither, or the default socket `mnm-host-default` named explicitly: label
|
|
13
|
+
* `ai.mnemom.agent-host.default`, socket `mnm-host-default` (the default round-trips).
|
|
14
|
+
*/
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { DURABLE_DIR, EXECUTOR_STATE_FILE } from "../vendor/shared/durable-contract/paths.js";
|
|
17
|
+
export const DEFAULT_INSTANCE = "ai.mnemom.agent-host.default";
|
|
18
|
+
const DEFAULT_TMUX_SOCKET = "mnm-host-default";
|
|
19
|
+
const NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
20
|
+
const LANE_SOCKET_RE = /^mnm-host-([A-Za-z0-9._-]+)$/;
|
|
21
|
+
export class InstanceNameError extends Error {
|
|
22
|
+
}
|
|
23
|
+
function checkName(kind, v) {
|
|
24
|
+
if (!NAME_RE.test(v))
|
|
25
|
+
throw new InstanceNameError(`${kind} must match ${NAME_RE}: ${JSON.stringify(v)}`);
|
|
26
|
+
return v;
|
|
27
|
+
}
|
|
28
|
+
export function resolveInstance(env) {
|
|
29
|
+
const label = env.MNEMOM_HOST_INSTANCE?.trim() || undefined;
|
|
30
|
+
const sock = env.MNEMOM_HOST_TMUX_SOCKET?.trim() || undefined;
|
|
31
|
+
if (label) {
|
|
32
|
+
checkName("MNEMOM_HOST_INSTANCE", label);
|
|
33
|
+
const tail = label.split(".").pop() || "default";
|
|
34
|
+
return { label, tmuxSocket: checkName("MNEMOM_HOST_TMUX_SOCKET", sock ?? `mnm-host-${tail}`) };
|
|
35
|
+
}
|
|
36
|
+
if (sock && sock !== DEFAULT_TMUX_SOCKET) {
|
|
37
|
+
checkName("MNEMOM_HOST_TMUX_SOCKET", sock);
|
|
38
|
+
const lane = LANE_SOCKET_RE.exec(sock);
|
|
39
|
+
return {
|
|
40
|
+
label: lane ? `com.mnemom.durable.dev.${lane[1]}` : `ai.mnemom.agent-host.${sock}`,
|
|
41
|
+
tmuxSocket: sock,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { label: DEFAULT_INSTANCE, tmuxSocket: DEFAULT_TMUX_SOCKET };
|
|
45
|
+
}
|
|
46
|
+
export function hostPaths(home, instance) {
|
|
47
|
+
const durableDir = path.join(home, DURABLE_DIR);
|
|
48
|
+
return {
|
|
49
|
+
home,
|
|
50
|
+
mnemomDir: path.join(home, ".mnemom"),
|
|
51
|
+
durableDir,
|
|
52
|
+
stateFile: path.join(home, EXECUTOR_STATE_FILE),
|
|
53
|
+
lockFile: path.join(durableDir, "host.lock"),
|
|
54
|
+
configFile: path.join(home, ".mnemom", "config.json"),
|
|
55
|
+
logDir: path.join(durableDir, "logs"),
|
|
56
|
+
specDir: path.join(durableDir, "specs"),
|
|
57
|
+
transcriptDir: path.join(durableDir, "transcripts"),
|
|
58
|
+
runDir: path.join(durableDir, "run"),
|
|
59
|
+
runtimeFile: path.join(durableDir, "run", "daemon.json"),
|
|
60
|
+
plistFile: path.join(home, "Library", "LaunchAgents", `${instance.label}.plist`),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type PluginDirEntry, type PluginManifest, type PluginManifestFailure } from "../vendor/shared/durable-contract/spec.js";
|
|
2
|
+
/** The package root that holds PLUGIN_DIR, from a resolved plugin dir. */
|
|
3
|
+
export declare function packageRootOf(pluginDir: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* The bytes of `abs` when it is a regular file, else null. The open refuses a symlink
|
|
6
|
+
* (O_NOFOLLOW), and the type check and the read use that one descriptor, so the file that is
|
|
7
|
+
* checked is the file that is read even if the path is swapped in between. O_NONBLOCK keeps a
|
|
8
|
+
* FIFO from blocking the open.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readRegularFile(abs: string): Buffer | null;
|
|
11
|
+
/**
|
|
12
|
+
* Directories, relative to PLUGIN_DIR, that Claude Code writes into the loaded plugin at runtime
|
|
13
|
+
* (mnemom-test #981): `.claude-plugin/types/` holds the editor type stubs it lays when it loads
|
|
14
|
+
* the plugin (also cli/plugins/.gitignore). The walk skips only a real directory at exactly
|
|
15
|
+
* this path, on both sides (the pack-time generator and the daemon's pre-claim check), so a
|
|
16
|
+
* symlink or a file named `.claude-plugin/types`, or a `types/` dir anywhere else, is still
|
|
17
|
+
* walked and still fails the check.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RUNTIME_WRITTEN_DIRS: readonly string[];
|
|
20
|
+
/**
|
|
21
|
+
* Every non-directory entry under `dir`, POSIX-relative, symlinks and specials as non-regular,
|
|
22
|
+
* except under RUNTIME_WRITTEN_DIRS.
|
|
23
|
+
*/
|
|
24
|
+
export declare function walkPluginDir(dir: string, exclude?: (rel: string) => boolean): PluginDirEntry[];
|
|
25
|
+
export declare function buildManifest(entries: readonly PluginDirEntry[]): Promise<PluginManifest>;
|
|
26
|
+
export type IntegrityResult = {
|
|
27
|
+
ok: true;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
cause: PluginManifestFailure | "no_plugin_dir";
|
|
31
|
+
};
|
|
32
|
+
/** Verify the installed plugin dir of the package rooted at `pkgRoot`. */
|
|
33
|
+
export declare function checkIntegrity(pkgRoot: string): Promise<IntegrityResult>;
|
|
34
|
+
/** The host status line for a failed check (BACKBONE C2; C12-R36). */
|
|
35
|
+
export declare function integrityFailureLine(cliVersion: string): string;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin integrity (C12-R36; ruling 25; BACKBONE C2 "Plugin integrity"). Before each claim the
|
|
3
|
+
* daemon resolves the plugin dir from its own installed package and verifies it against the
|
|
4
|
+
* package's shipped manifest (S1 PLUGIN_MANIFEST_PATH, dir-hash recipe). The pack-time generator
|
|
5
|
+
* (scripts/gen-integrity-manifest.mjs) uses buildManifest from this module, so the writer and the
|
|
6
|
+
* reader share one recipe.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { PLUGIN_DIR, PLUGIN_MANIFEST_PATH, pluginDirHash, verifyPluginManifest, } from "../vendor/shared/durable-contract/spec.js";
|
|
12
|
+
import { PLUGIN_HASH_RECIPE_VERSION } from "../vendor/shared/durable-contract/constants.js";
|
|
13
|
+
/** The package root that holds PLUGIN_DIR, from a resolved plugin dir. */
|
|
14
|
+
export function packageRootOf(pluginDir) {
|
|
15
|
+
return path.resolve(pluginDir, "..", "..");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The bytes of `abs` when it is a regular file, else null. The open refuses a symlink
|
|
19
|
+
* (O_NOFOLLOW), and the type check and the read use that one descriptor, so the file that is
|
|
20
|
+
* checked is the file that is read even if the path is swapped in between. O_NONBLOCK keeps a
|
|
21
|
+
* FIFO from blocking the open.
|
|
22
|
+
*/
|
|
23
|
+
export function readRegularFile(abs) {
|
|
24
|
+
const c = fs.constants;
|
|
25
|
+
let fd;
|
|
26
|
+
try {
|
|
27
|
+
fd = fs.openSync(abs, c.O_RDONLY | (c.O_NOFOLLOW ?? 0) | (c.O_NONBLOCK ?? 0));
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
if (e.code === "ELOOP")
|
|
31
|
+
return null;
|
|
32
|
+
throw e;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.fstatSync(fd).isFile())
|
|
36
|
+
return null;
|
|
37
|
+
return fs.readFileSync(fd);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
fs.closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Directories, relative to PLUGIN_DIR, that Claude Code writes into the loaded plugin at runtime
|
|
45
|
+
* (mnemom-test #981): `.claude-plugin/types/` holds the editor type stubs it lays when it loads
|
|
46
|
+
* the plugin (also cli/plugins/.gitignore). The walk skips only a real directory at exactly
|
|
47
|
+
* this path, on both sides (the pack-time generator and the daemon's pre-claim check), so a
|
|
48
|
+
* symlink or a file named `.claude-plugin/types`, or a `types/` dir anywhere else, is still
|
|
49
|
+
* walked and still fails the check.
|
|
50
|
+
*/
|
|
51
|
+
export const RUNTIME_WRITTEN_DIRS = [".claude-plugin/types"];
|
|
52
|
+
/**
|
|
53
|
+
* Every non-directory entry under `dir`, POSIX-relative, symlinks and specials as non-regular,
|
|
54
|
+
* except under RUNTIME_WRITTEN_DIRS.
|
|
55
|
+
*/
|
|
56
|
+
export function walkPluginDir(dir, exclude = () => false) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const walk = (abs, rel) => {
|
|
59
|
+
for (const name of fs.readdirSync(abs)) {
|
|
60
|
+
const a = path.join(abs, name);
|
|
61
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
62
|
+
const st = fs.lstatSync(a);
|
|
63
|
+
if (st.isDirectory()) {
|
|
64
|
+
if (RUNTIME_WRITTEN_DIRS.includes(r))
|
|
65
|
+
continue;
|
|
66
|
+
walk(a, r);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (exclude(r))
|
|
70
|
+
continue;
|
|
71
|
+
const bytes = st.isFile() ? readRegularFile(a) : null;
|
|
72
|
+
if (bytes !== null)
|
|
73
|
+
out.push({
|
|
74
|
+
path: r,
|
|
75
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
76
|
+
regular: true,
|
|
77
|
+
});
|
|
78
|
+
else
|
|
79
|
+
out.push({ path: r, sha256: null, regular: false });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
walk(dir, "");
|
|
83
|
+
return out.sort((x, y) => (x.path < y.path ? -1 : x.path > y.path ? 1 : 0));
|
|
84
|
+
}
|
|
85
|
+
export async function buildManifest(entries) {
|
|
86
|
+
const files = entries.map((e) => {
|
|
87
|
+
if (!e.regular || e.sha256 === null)
|
|
88
|
+
throw new Error(`plugin entry ${e.path} is not a regular file`);
|
|
89
|
+
return { path: e.path, sha256: e.sha256 };
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
recipe_version: PLUGIN_HASH_RECIPE_VERSION,
|
|
93
|
+
plugin_dir: PLUGIN_DIR,
|
|
94
|
+
files,
|
|
95
|
+
dir_hash: await pluginDirHash(files),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Verify the installed plugin dir of the package rooted at `pkgRoot`. */
|
|
99
|
+
export async function checkIntegrity(pkgRoot) {
|
|
100
|
+
const dir = path.join(pkgRoot, PLUGIN_DIR);
|
|
101
|
+
if (!fs.existsSync(dir))
|
|
102
|
+
return { ok: false, cause: "no_plugin_dir" };
|
|
103
|
+
let text;
|
|
104
|
+
try {
|
|
105
|
+
text = fs.readFileSync(path.join(pkgRoot, PLUGIN_MANIFEST_PATH), "utf8");
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
text = null;
|
|
109
|
+
}
|
|
110
|
+
return verifyPluginManifest(text, walkPluginDir(dir));
|
|
111
|
+
}
|
|
112
|
+
/** The host status line for a failed check (BACKBONE C2; C12-R36). */
|
|
113
|
+
export function integrityFailureLine(cliVersion) {
|
|
114
|
+
return `installed CLI fails its integrity check: \`npm i -g @mnemom/mnemom@${cliVersion}\``;
|
|
115
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LaunchctlResult {
|
|
2
|
+
readonly code: number;
|
|
3
|
+
readonly stdout: string;
|
|
4
|
+
readonly stderr: string;
|
|
5
|
+
}
|
|
6
|
+
export type Launchctl = (args: readonly string[]) => Promise<LaunchctlResult>;
|
|
7
|
+
export declare const realLaunchctl: Launchctl;
|
|
8
|
+
export declare const guiDomain: (uid: number) => string;
|
|
9
|
+
export declare const serviceTarget: (uid: number, label: string) => string;
|
|
10
|
+
export declare function printJob(lc: Launchctl, uid: number, label: string): Promise<{
|
|
11
|
+
loaded: boolean;
|
|
12
|
+
pid: number | null;
|
|
13
|
+
}>;
|
|
14
|
+
/** Load (or reload) the job from its plist: bootout when loaded, then bootstrap. One job per label. */
|
|
15
|
+
export declare function loadJob(lc: Launchctl, uid: number, label: string, plistFile: string): Promise<LaunchctlResult>;
|
|
16
|
+
export declare function unloadJob(lc: Launchctl, uid: number, label: string): Promise<boolean>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* launchctl, scoped to the host instance's own label in the user's gui domain. Never touches
|
|
3
|
+
* any other job (BACKBONE C2; lane labels com.mnemom.durable.dev.<L>).
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
+
export const realLaunchctl = (args) => new Promise((resolve) => {
|
|
8
|
+
execFile("/bin/launchctl", [...args], { env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin" }, timeout: 30000 }, (err, stdout, stderr) => {
|
|
9
|
+
const code = err
|
|
10
|
+
? typeof err.code === "number"
|
|
11
|
+
? err.code
|
|
12
|
+
: 1
|
|
13
|
+
: 0;
|
|
14
|
+
resolve({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
export const guiDomain = (uid) => `gui/${uid}`;
|
|
18
|
+
export const serviceTarget = (uid, label) => `${guiDomain(uid)}/${label}`;
|
|
19
|
+
export async function printJob(lc, uid, label) {
|
|
20
|
+
const r = await lc(["print", serviceTarget(uid, label)]);
|
|
21
|
+
if (r.code !== 0)
|
|
22
|
+
return { loaded: false, pid: null };
|
|
23
|
+
const m = /\bpid = (\d+)/.exec(r.stdout);
|
|
24
|
+
return { loaded: true, pid: m ? Number(m[1]) : null };
|
|
25
|
+
}
|
|
26
|
+
/** Load (or reload) the job from its plist: bootout when loaded, then bootstrap. One job per label. */
|
|
27
|
+
export async function loadJob(lc, uid, label, plistFile) {
|
|
28
|
+
const st = await printJob(lc, uid, label);
|
|
29
|
+
if (st.loaded)
|
|
30
|
+
await lc(["bootout", serviceTarget(uid, label)]);
|
|
31
|
+
// bootout completes asynchronously: a bootstrap racing it fails with EIO (5); retry briefly.
|
|
32
|
+
let r = await lc(["bootstrap", guiDomain(uid), plistFile]);
|
|
33
|
+
for (let i = 0; i < 20 && r.code !== 0 && st.loaded; i++) {
|
|
34
|
+
await sleep(250);
|
|
35
|
+
r = await lc(["bootstrap", guiDomain(uid), plistFile]);
|
|
36
|
+
}
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
export async function unloadJob(lc, uid, label) {
|
|
40
|
+
const st = await printJob(lc, uid, label);
|
|
41
|
+
if (!st.loaded)
|
|
42
|
+
return false;
|
|
43
|
+
await lc(["bootout", serviceTarget(uid, label)]);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type LockResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
fd: number;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: "held" | "unsupported" | "error";
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function lockSupported(platform?: NodeJS.Platform): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Take the lock or report why not. Never blocks. The fd must stay open for the process lifetime
|
|
12
|
+
* (closing it releases the lock).
|
|
13
|
+
*/
|
|
14
|
+
export declare function acquireHostLock(lockFile: string, platform?: NodeJS.Platform): LockResult;
|
|
15
|
+
/**
|
|
16
|
+
* Whether `fd` still locks the file at `lockFile`. A lock file deleted or replaced under a running
|
|
17
|
+
* daemon (an `rm -rf` of the state dir) leaves it holding a lock on an unlinked inode, so a second
|
|
18
|
+
* daemon can take the new file; the first must notice and stop rather than run beside it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function lockStillHeld(fd: number, lockFile: string): boolean;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single-instance lock (C12-R01..R04): a kernel lock on a separate, never-renamed lock file
|
|
3
|
+
* in the state dir. macOS open(2) O_EXLOCK|O_NONBLOCK takes a flock(2) lock atomically with the
|
|
4
|
+
* open; the kernel drops it when the holder dies (kill -9), and a SIGSTOPped holder keeps it. No
|
|
5
|
+
* staleness timeout, no pid file.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
/** <sys/fcntl.h> on Darwin. Node does not export these. */
|
|
10
|
+
const O_EXLOCK_DARWIN = 0x20;
|
|
11
|
+
const O_NONBLOCK_DARWIN = 0x4;
|
|
12
|
+
export function lockSupported(platform = process.platform) {
|
|
13
|
+
return platform === "darwin";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Take the lock or report why not. Never blocks. The fd must stay open for the process lifetime
|
|
17
|
+
* (closing it releases the lock).
|
|
18
|
+
*/
|
|
19
|
+
export function acquireHostLock(lockFile, platform = process.platform) {
|
|
20
|
+
if (!lockSupported(platform)) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: "unsupported",
|
|
24
|
+
message: `the host daemon runs on macOS only (platform ${platform})`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(path.dirname(lockFile), { recursive: true, mode: 0o700 });
|
|
29
|
+
const flags = fs.constants.O_RDWR | fs.constants.O_CREAT | O_EXLOCK_DARWIN | O_NONBLOCK_DARWIN;
|
|
30
|
+
const fd = fs.openSync(lockFile, flags, 0o600);
|
|
31
|
+
return { ok: true, fd };
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
const code = e.code;
|
|
35
|
+
if (code === "EAGAIN" || code === "EWOULDBLOCK") {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: "held",
|
|
39
|
+
message: `another mnemom agent host daemon holds ${lockFile}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
reason: "error",
|
|
45
|
+
message: `cannot open ${lockFile}: ${e.message}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether `fd` still locks the file at `lockFile`. A lock file deleted or replaced under a running
|
|
51
|
+
* daemon (an `rm -rf` of the state dir) leaves it holding a lock on an unlinked inode, so a second
|
|
52
|
+
* daemon can take the new file; the first must notice and stop rather than run beside it.
|
|
53
|
+
*/
|
|
54
|
+
export function lockStillHeld(fd, lockFile) {
|
|
55
|
+
try {
|
|
56
|
+
const held = fs.fstatSync(fd);
|
|
57
|
+
const now = fs.statSync(lockFile);
|
|
58
|
+
return held.ino === now.ino && held.dev === now.dev;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type FetchFn } from "./client.js";
|
|
2
|
+
export declare const EXECUTOR_CONTRACT = "durable-executor/v1";
|
|
3
|
+
export interface OwnerHttp {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly body: unknown;
|
|
6
|
+
readonly networkError?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OwnerApi {
|
|
9
|
+
enrollStart(body: {
|
|
10
|
+
kind: "host";
|
|
11
|
+
runtime: string;
|
|
12
|
+
contract_version: string;
|
|
13
|
+
machine_label?: string;
|
|
14
|
+
}): Promise<OwnerHttp>;
|
|
15
|
+
enrollPoll(pollSecret: string, approvalId: string | null): Promise<OwnerHttp>;
|
|
16
|
+
list(): Promise<OwnerHttp>;
|
|
17
|
+
renew(executorId: string): Promise<OwnerHttp>;
|
|
18
|
+
}
|
|
19
|
+
export declare function ownerApi(opts: {
|
|
20
|
+
apiUrl: string;
|
|
21
|
+
jwt: () => Promise<string | null>;
|
|
22
|
+
fetch: FetchFn;
|
|
23
|
+
}): OwnerApi;
|
|
24
|
+
/** One executor as exec_list reports it (fields read defensively). */
|
|
25
|
+
export interface ExecutorRow {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly revoked: boolean;
|
|
28
|
+
readonly grantExpiresAt: number | null;
|
|
29
|
+
readonly riskLineVersion: number | null;
|
|
30
|
+
readonly label: string | null;
|
|
31
|
+
readonly orgId: string | null;
|
|
32
|
+
}
|
|
33
|
+
export declare function parseExecutorList(body: unknown): ExecutorRow[] | null;
|
|
34
|
+
export interface EnrollStart {
|
|
35
|
+
readonly approvalId: string | null;
|
|
36
|
+
readonly userCode: string;
|
|
37
|
+
readonly verificationUri: string;
|
|
38
|
+
readonly expiresIn: number;
|
|
39
|
+
readonly interval: number;
|
|
40
|
+
readonly pollSecret: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function parseEnrollStart(body: unknown): EnrollStart | null;
|
|
43
|
+
export type PollOutcome = {
|
|
44
|
+
kind: "done";
|
|
45
|
+
executorId: string;
|
|
46
|
+
executorKey: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "pending";
|
|
49
|
+
} | {
|
|
50
|
+
kind: "slow_down";
|
|
51
|
+
} | {
|
|
52
|
+
kind: "terminal";
|
|
53
|
+
error: string;
|
|
54
|
+
};
|
|
55
|
+
export declare function classifyPoll(r: OwnerHttp): PollOutcome;
|