@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.11
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/README.md +168 -371
- package/dist/adapter.d.ts +36 -12
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +12 -7
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +358 -135
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +100 -36
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +71 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +168 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +30 -24
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +613 -224
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +22 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +97 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +127 -72
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +3 -0
- package/dist/adapters/slack/session.d.ts.map +1 -0
- package/dist/adapters/slack/session.js +16 -0
- package/dist/adapters/slack/session.js.map +1 -0
- package/dist/adapters/slack/tools/attach.d.ts +1 -1
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
- package/dist/adapters/slack/tools/attach.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +193 -147
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +58 -111
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +9 -13
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +601 -567
- package/dist/agent.js.map +1 -1
- package/dist/commands/auto-reply.d.ts +16 -0
- package/dist/commands/auto-reply.d.ts.map +1 -0
- package/dist/commands/auto-reply.js +69 -0
- package/dist/commands/auto-reply.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +76 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/model.d.ts +14 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +112 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +28 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/registry.d.ts +7 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +14 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/sandbox.d.ts +10 -0
- package/dist/commands/sandbox.d.ts.map +1 -0
- package/dist/commands/sandbox.js +88 -0
- package/dist/commands/sandbox.js.map +1 -0
- package/dist/commands/session-view.d.ts +5 -0
- package/dist/commands/session-view.d.ts.map +1 -0
- package/dist/commands/session-view.js +62 -0
- package/dist/commands/session-view.js.map +1 -0
- package/dist/commands/types.d.ts +41 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.d.ts.map +1 -0
- package/dist/commands/utils.js +14 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/config.d.ts +49 -30
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +313 -75
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +10 -42
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +14 -127
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +13 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +118 -64
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +9 -5
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +82 -18
- package/dist/execution-resolver.js.map +1 -1
- package/dist/file-guards.d.ts +6 -0
- package/dist/file-guards.d.ts.map +1 -0
- package/dist/file-guards.js +48 -0
- package/dist/file-guards.js.map +1 -0
- package/dist/fs-atomic.d.ts +10 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/fs-atomic.js +45 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +4 -11
- package/dist/instrument.js.map +1 -1
- package/dist/log.d.ts +1 -5
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +13 -38
- package/dist/log.js.map +1 -1
- package/dist/{login.d.ts → login/index.d.ts} +16 -4
- package/dist/login/index.d.ts.map +1 -0
- package/dist/{login.js → login/index.js} +55 -17
- package/dist/login/index.js.map +1 -0
- package/dist/{link-server.d.ts → login/portal.d.ts} +7 -4
- package/dist/login/portal.d.ts.map +1 -0
- package/dist/login/portal.js +1453 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/{link-token.d.ts → login/session.d.ts} +4 -3
- package/dist/login/session.d.ts.map +1 -0
- package/dist/{link-token.js → login/session.js} +1 -1
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +151 -373
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +42 -52
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +256 -111
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/conversation-orchestrator.d.ts +42 -0
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
- package/dist/runtime/conversation-orchestrator.js +150 -0
- package/dist/runtime/conversation-orchestrator.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/session-runtime.d.ts +27 -0
- package/dist/runtime/session-runtime.d.ts.map +1 -0
- package/dist/runtime/session-runtime.js +211 -0
- package/dist/runtime/session-runtime.js.map +1 -0
- package/dist/sandbox/cloudflare.d.ts +15 -0
- package/dist/sandbox/cloudflare.d.ts.map +1 -0
- package/dist/sandbox/cloudflare.js +137 -0
- package/dist/sandbox/cloudflare.js.map +1 -0
- package/dist/sandbox/container.d.ts +2 -1
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +5 -1
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/firecracker.d.ts +2 -1
- package/dist/sandbox/firecracker.d.ts.map +1 -1
- package/dist/sandbox/firecracker.js +6 -0
- package/dist/sandbox/firecracker.js.map +1 -1
- package/dist/sandbox/host.d.ts +2 -3
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +5 -5
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sandbox/index.d.ts +6 -4
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +9 -6
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/path-context.d.ts +4 -0
- package/dist/sandbox/path-context.d.ts.map +1 -0
- package/dist/sandbox/path-context.js +20 -0
- package/dist/sandbox/path-context.js.map +1 -0
- package/dist/sandbox/types.d.ts +17 -1
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +4 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-policy.d.ts +13 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +23 -0
- package/dist/session-policy.js.map +1 -0
- package/dist/session-store.d.ts +34 -3
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +184 -22
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/command.d.ts +5 -0
- package/dist/session-view/command.d.ts.map +1 -0
- package/dist/session-view/command.js +11 -0
- package/dist/session-view/command.js.map +1 -0
- package/dist/session-view/portal.d.ts +16 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +1742 -0
- package/dist/session-view/portal.js.map +1 -0
- package/dist/session-view/service.d.ts +34 -0
- package/dist/session-view/service.d.ts.map +1 -0
- package/dist/session-view/service.js +427 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +18 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +39 -0
- package/dist/session-view/store.js.map +1 -0
- package/dist/store.d.ts +3 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +22 -48
- package/dist/store.js.map +1 -1
- package/dist/tool-diagnostics.d.ts +2 -0
- package/dist/tool-diagnostics.d.ts.map +1 -0
- package/dist/tool-diagnostics.js +7 -0
- package/dist/tool-diagnostics.js.map +1 -0
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/event.d.ts +43 -2
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +48 -13
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/trigger.d.ts +31 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +98 -0
- package/dist/trigger.js.map +1 -0
- package/dist/ui-copy.d.ts +1 -0
- package/dist/ui-copy.d.ts.map +1 -1
- package/dist/ui-copy.js +3 -0
- package/dist/ui-copy.js.map +1 -1
- package/dist/vault-routing.d.ts +1 -7
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +6 -48
- package/dist/vault-routing.js.map +1 -1
- package/dist/vault.d.ts +21 -55
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +144 -263
- package/dist/vault.js.map +1 -1
- package/package.json +12 -10
- package/dist/bindings.d.ts +0 -63
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js +0 -94
- package/dist/bindings.js.map +0 -1
- package/dist/link-server.d.ts.map +0 -1
- package/dist/link-server.js +0 -839
- package/dist/link-server.js.map +0 -1
- package/dist/link-token.d.ts.map +0 -1
- package/dist/link-token.js.map +0 -1
- package/dist/login.d.ts.map +0 -1
- package/dist/login.js.map +0 -1
- package/dist/vault.test.d.ts +0 -2
- package/dist/vault.test.d.ts.map +0 -1
- package/dist/vault.test.js +0 -67
- package/dist/vault.test.js.map +0 -1
package/dist/provisioner.js
CHANGED
|
@@ -1,33 +1,42 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { readFileSync, statSync } from "fs";
|
|
2
4
|
import { promisify } from "util";
|
|
3
5
|
import * as log from "./log.js";
|
|
4
6
|
const execFileAsync = promisify(execFile);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
function isDockerNotFoundError(err) {
|
|
8
|
+
if (!err || typeof err !== "object")
|
|
9
|
+
return false;
|
|
10
|
+
const stderr = err.stderr;
|
|
11
|
+
const message = err.message;
|
|
12
|
+
const haystack = `${typeof stderr === "string" ? stderr : ""}\n${typeof message === "string" ? message : ""}`.toLowerCase();
|
|
13
|
+
return (haystack.includes("no such network") ||
|
|
14
|
+
haystack.includes("no such container") ||
|
|
15
|
+
haystack.includes("no such object") ||
|
|
16
|
+
haystack.includes("network not found") ||
|
|
17
|
+
/network [^\n]+ not found/.test(haystack) ||
|
|
18
|
+
/error: no such [^\n]+/.test(haystack));
|
|
19
|
+
}
|
|
13
20
|
export class DockerContainerManager {
|
|
14
21
|
static { this.MANAGED_LABEL = "mama.managed=true"; }
|
|
15
22
|
static { this.IMAGE_MODE_LABEL = "mama.sandbox=image"; }
|
|
16
23
|
static { this.VAULT_ID_LABEL_KEY = "mama.vault-id"; }
|
|
17
|
-
|
|
24
|
+
static { this.CONVERSATION_ID_LABEL_KEY = "mama.conversation-id"; }
|
|
25
|
+
static { this.MOUNT_SIGNATURE_LABEL_KEY = "mama.mount-signature"; }
|
|
26
|
+
constructor(image, options = {}) {
|
|
18
27
|
this.image = image;
|
|
19
|
-
this.workspaceDir = workspaceDir;
|
|
20
|
-
this.execFileImpl = execFileImpl;
|
|
21
28
|
this.state = new Map();
|
|
22
|
-
/**
|
|
23
|
-
* In-flight provision() calls per vaultId. A concurrent second call for the
|
|
24
|
-
* same user piggybacks on the first docker start/run instead of racing —
|
|
25
|
-
* without this, two parallel messages from one user could produce duplicate
|
|
26
|
-
* containers or conflict on docker run.
|
|
27
|
-
*/
|
|
28
29
|
this.inflight = new Map();
|
|
30
|
+
this.boostedKeys = new Set();
|
|
31
|
+
if (typeof options === "function") {
|
|
32
|
+
this.execFileImpl = options;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.limits = options.limits;
|
|
36
|
+
this.boostLimits = options.boostLimits;
|
|
37
|
+
this.execFileImpl = options.execFileImpl ?? execFileAsync;
|
|
38
|
+
}
|
|
29
39
|
}
|
|
30
|
-
/** Sanitize an identifier segment for use in vault keys and container names. */
|
|
31
40
|
static sanitizeSegment(value) {
|
|
32
41
|
const sanitized = value
|
|
33
42
|
.toLowerCase()
|
|
@@ -35,45 +44,32 @@ export class DockerContainerManager {
|
|
|
35
44
|
.replace(/^-+|-+$/g, "");
|
|
36
45
|
return sanitized || "unknown";
|
|
37
46
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
static containerName(vaultId) {
|
|
47
|
-
return `mama-sandbox-${vaultId}`;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Ensure a container exists and is running for the given vaultId.
|
|
51
|
-
* Always inspects the actual Docker state, then acts accordingly:
|
|
52
|
-
* - running → no-op
|
|
53
|
-
* - stopped → docker start
|
|
54
|
-
* - missing → docker run
|
|
55
|
-
*
|
|
56
|
-
* Returns the container name.
|
|
57
|
-
*/
|
|
58
|
-
async provision(vaultId, options = {}) {
|
|
59
|
-
const existing = this.inflight.get(vaultId);
|
|
47
|
+
static containerName(containerKey) {
|
|
48
|
+
return `mama-sandbox-${containerKey}`;
|
|
49
|
+
}
|
|
50
|
+
static networkName(containerKey) {
|
|
51
|
+
return `mama-sandbox-net-${containerKey}`;
|
|
52
|
+
}
|
|
53
|
+
async provision(containerKey, options = {}) {
|
|
54
|
+
const existing = this.inflight.get(containerKey);
|
|
60
55
|
if (existing)
|
|
61
56
|
return existing;
|
|
62
|
-
const pending = this.provisionInner(
|
|
63
|
-
this.inflight.delete(
|
|
57
|
+
const pending = this.provisionInner(containerKey, options).finally(() => {
|
|
58
|
+
this.inflight.delete(containerKey);
|
|
64
59
|
});
|
|
65
|
-
this.inflight.set(
|
|
60
|
+
this.inflight.set(containerKey, pending);
|
|
66
61
|
return pending;
|
|
67
62
|
}
|
|
68
|
-
async provisionInner(
|
|
69
|
-
const containerName = options.containerName ?? DockerContainerManager.containerName(
|
|
63
|
+
async provisionInner(containerKey, options) {
|
|
64
|
+
const containerName = options.containerName ?? DockerContainerManager.containerName(containerKey);
|
|
70
65
|
const mounts = options.mounts ?? [];
|
|
71
66
|
const status = await this.inspectStatus(containerName);
|
|
72
67
|
try {
|
|
73
|
-
if (status !== "missing" &&
|
|
74
|
-
|
|
68
|
+
if (status !== "missing" &&
|
|
69
|
+
(await this.hasRuntimeDrift(containerKey, containerName, mounts))) {
|
|
70
|
+
log.logInfo(`Container ${containerName} configuration changed; recreating container`);
|
|
75
71
|
await this.execFileImpl("docker", ["rm", "-f", containerName]);
|
|
76
|
-
await this.runContainer(
|
|
72
|
+
await this.runContainer(containerKey, containerName, mounts, options);
|
|
77
73
|
log.logInfo(`Container ${containerName} recreated`);
|
|
78
74
|
}
|
|
79
75
|
else if (status === "running") {
|
|
@@ -84,65 +80,75 @@ export class DockerContainerManager {
|
|
|
84
80
|
log.logInfo(`Container ${containerName} started`);
|
|
85
81
|
}
|
|
86
82
|
else {
|
|
87
|
-
await this.runContainer(
|
|
83
|
+
await this.runContainer(containerKey, containerName, mounts, options);
|
|
88
84
|
log.logInfo(`Container ${containerName} created`);
|
|
89
85
|
}
|
|
90
86
|
}
|
|
91
87
|
catch (err) {
|
|
92
|
-
|
|
93
|
-
// and stopIdle doesn't keep trying to stop a container that never
|
|
94
|
-
// became running. We deliberately don't bump lastUsed here.
|
|
95
|
-
this.state.delete(vaultId);
|
|
88
|
+
this.state.delete(containerKey);
|
|
96
89
|
throw err;
|
|
97
90
|
}
|
|
98
|
-
this.setState(
|
|
91
|
+
this.setState(containerKey, "running", containerName);
|
|
92
|
+
await this.applyResourceLimits(containerKey, containerName);
|
|
99
93
|
return containerName;
|
|
100
94
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
95
|
+
async boost(containerKey) {
|
|
96
|
+
if (!this.boostLimits?.cpus && !this.boostLimits?.memory) {
|
|
97
|
+
return this.getLimitStatus(containerKey);
|
|
98
|
+
}
|
|
99
|
+
this.boostedKeys.add(containerKey);
|
|
100
|
+
const state = this.state.get(containerKey);
|
|
101
|
+
if (state?.status === "running") {
|
|
102
|
+
await this.applyResourceLimits(containerKey, state.containerName);
|
|
103
|
+
}
|
|
104
|
+
return this.getLimitStatus(containerKey);
|
|
105
|
+
}
|
|
106
|
+
getLimitStatus(containerKey) {
|
|
107
|
+
const boosted = this.boostedKeys.has(containerKey);
|
|
108
|
+
return { limits: this.effectiveLimits(containerKey), boosted };
|
|
109
|
+
}
|
|
110
|
+
getDefaultLimits() {
|
|
111
|
+
return this.limits;
|
|
112
|
+
}
|
|
113
|
+
getBoostLimits() {
|
|
114
|
+
return this.boostLimits;
|
|
115
|
+
}
|
|
116
|
+
async stop(containerKey) {
|
|
117
|
+
const containerName = this.getContainerName(containerKey);
|
|
107
118
|
try {
|
|
108
119
|
await this.execFileImpl("docker", ["stop", containerName]);
|
|
109
|
-
this.setState(
|
|
120
|
+
this.setState(containerKey, "stopped", containerName);
|
|
121
|
+
this.boostedKeys.delete(containerKey);
|
|
110
122
|
log.logInfo(`Container ${containerName} stopped (idle)`);
|
|
111
123
|
}
|
|
112
124
|
catch (err) {
|
|
113
125
|
log.logWarning(`Failed to stop container ${containerName}`, err instanceof Error ? err.message : String(err));
|
|
114
126
|
}
|
|
115
127
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
128
|
+
async remove(containerKey) {
|
|
129
|
+
const containerName = this.getContainerName(containerKey);
|
|
130
|
+
const networkName = DockerContainerManager.networkName(containerKey);
|
|
131
|
+
await this.forceRemoveContainer(containerName, `Container ${containerName} removed`, `Failed to remove container ${containerName}`);
|
|
119
132
|
try {
|
|
120
|
-
await this.execFileImpl("docker", ["
|
|
121
|
-
|
|
122
|
-
log.logInfo(`Container ${containerName} removed`);
|
|
133
|
+
await this.execFileImpl("docker", ["network", "rm", networkName]);
|
|
134
|
+
log.logInfo(`Network ${networkName} removed`);
|
|
123
135
|
}
|
|
124
136
|
catch (err) {
|
|
125
|
-
log.logWarning(`Failed to remove
|
|
137
|
+
log.logWarning(`Failed to remove network ${networkName}`, err instanceof Error ? err.message : String(err));
|
|
126
138
|
}
|
|
139
|
+
this.state.delete(containerKey);
|
|
140
|
+
this.boostedKeys.delete(containerKey);
|
|
127
141
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Stop all containers that have been idle for longer than maxIdleMs.
|
|
130
|
-
* Idle time is measured from the last provision() call.
|
|
131
|
-
*/
|
|
132
142
|
async stopIdle(maxIdleMs) {
|
|
133
143
|
const now = Date.now();
|
|
134
144
|
const toStop = [];
|
|
135
|
-
for (const [
|
|
145
|
+
for (const [containerKey, containerState] of this.state) {
|
|
136
146
|
if (containerState.status === "running" && now - containerState.lastUsed > maxIdleMs) {
|
|
137
|
-
toStop.push(
|
|
147
|
+
toStop.push(containerKey);
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
|
-
await Promise.all(toStop.map((
|
|
150
|
+
await Promise.all(toStop.map((containerKey) => this.stop(containerKey)));
|
|
141
151
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Rebuild in-memory state from existing Docker containers managed by mama image mode.
|
|
144
|
-
* Supports both new labeled containers and legacy name-prefixed containers.
|
|
145
|
-
*/
|
|
146
152
|
async reconcile() {
|
|
147
153
|
const discovered = new Set();
|
|
148
154
|
const labeledNames = await this.listContainerNamesByLabel();
|
|
@@ -152,28 +158,38 @@ export class DockerContainerManager {
|
|
|
152
158
|
for (const name of legacyNames)
|
|
153
159
|
discovered.add(name);
|
|
154
160
|
this.state.clear();
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
const inspected = await Promise.all(Array.from(discovered).map(async (containerName) => ({
|
|
162
|
+
containerName,
|
|
163
|
+
details: await this.inspectContainerDetails(containerName),
|
|
164
|
+
})));
|
|
165
|
+
const legacyRemovals = [];
|
|
166
|
+
for (const { containerName, details } of inspected) {
|
|
157
167
|
if (!details)
|
|
158
168
|
continue;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
if (!details.conversationId) {
|
|
170
|
+
legacyRemovals.push(this.removeLegacyContainer(containerName));
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const containerKey = this.containerKeyFromContainerName(containerName);
|
|
174
|
+
if (!containerKey) {
|
|
175
|
+
log.logWarning(`Skipping unmanaged-style container without container key`, containerName);
|
|
162
176
|
continue;
|
|
163
177
|
}
|
|
164
178
|
const status = details.running ? "running" : "stopped";
|
|
165
179
|
const lastUsed = details.startedAtMs ?? Date.now();
|
|
166
|
-
this.state.set(
|
|
180
|
+
this.state.set(containerKey, { status, lastUsed, containerName });
|
|
167
181
|
}
|
|
182
|
+
await Promise.all(legacyRemovals);
|
|
168
183
|
const running = Array.from(this.state.values()).filter((s) => s.status === "running").length;
|
|
169
184
|
const stopped = this.state.size - running;
|
|
170
185
|
log.logInfo(`Reconciled ${this.state.size} managed containers (running=${running}, stopped=${stopped})`);
|
|
171
186
|
}
|
|
172
|
-
setState(
|
|
173
|
-
this.state.set(
|
|
187
|
+
setState(containerKey, status, containerName) {
|
|
188
|
+
this.state.set(containerKey, { status, lastUsed: Date.now(), containerName });
|
|
174
189
|
}
|
|
175
|
-
getContainerName(
|
|
176
|
-
return this.state.get(
|
|
190
|
+
getContainerName(containerKey) {
|
|
191
|
+
return (this.state.get(containerKey)?.containerName ??
|
|
192
|
+
DockerContainerManager.containerName(containerKey));
|
|
177
193
|
}
|
|
178
194
|
mountArgs(mounts) {
|
|
179
195
|
return mounts.flatMap((mount) => ["-v", this.toBindSpec(mount)]);
|
|
@@ -181,36 +197,82 @@ export class DockerContainerManager {
|
|
|
181
197
|
toBindSpec(mount) {
|
|
182
198
|
return `${mount.source}:${mount.target}`;
|
|
183
199
|
}
|
|
184
|
-
async runContainer(
|
|
200
|
+
async runContainer(containerKey, containerName, mounts, options) {
|
|
201
|
+
const networkName = await this.ensureNetwork(containerKey);
|
|
185
202
|
log.logInfo(`Creating container ${containerName} from image ${this.image}`);
|
|
186
|
-
|
|
187
|
-
"run",
|
|
188
|
-
"-d",
|
|
189
|
-
"--name",
|
|
190
|
-
containerName,
|
|
203
|
+
const labels = [
|
|
191
204
|
"--label",
|
|
192
205
|
DockerContainerManager.MANAGED_LABEL,
|
|
193
206
|
"--label",
|
|
194
207
|
DockerContainerManager.IMAGE_MODE_LABEL,
|
|
195
208
|
"--label",
|
|
196
|
-
`${DockerContainerManager.VAULT_ID_LABEL_KEY}=${
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
`${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,
|
|
210
|
+
];
|
|
211
|
+
if (options.conversationId) {
|
|
212
|
+
labels.push("--label", `${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}=${options.conversationId}`);
|
|
213
|
+
}
|
|
214
|
+
if (mounts.length > 0) {
|
|
215
|
+
labels.push("--label", `${DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY}=${this.mountSignature(mounts)}`);
|
|
216
|
+
}
|
|
217
|
+
await this.execFileImpl("docker", [
|
|
218
|
+
"run",
|
|
219
|
+
"-d",
|
|
220
|
+
"--name",
|
|
221
|
+
containerName,
|
|
222
|
+
"--network",
|
|
223
|
+
networkName,
|
|
224
|
+
...labels,
|
|
225
|
+
...this.resourceLimitArgs(this.effectiveLimits(containerKey)),
|
|
199
226
|
...this.mountArgs(mounts),
|
|
200
227
|
this.image,
|
|
201
228
|
"sleep",
|
|
202
229
|
"infinity",
|
|
203
230
|
]);
|
|
204
231
|
}
|
|
232
|
+
effectiveLimits(containerKey) {
|
|
233
|
+
if (!this.boostedKeys.has(containerKey))
|
|
234
|
+
return this.limits;
|
|
235
|
+
return { ...this.limits, ...this.boostLimits };
|
|
236
|
+
}
|
|
237
|
+
resourceLimitArgs(limits) {
|
|
238
|
+
const args = [];
|
|
239
|
+
if (limits?.cpus)
|
|
240
|
+
args.push("--cpus", limits.cpus);
|
|
241
|
+
if (limits?.memory)
|
|
242
|
+
args.push("--memory", limits.memory);
|
|
243
|
+
return args;
|
|
244
|
+
}
|
|
245
|
+
async applyResourceLimits(containerKey, containerName) {
|
|
246
|
+
const limitArgs = this.resourceLimitArgs(this.effectiveLimits(containerKey));
|
|
247
|
+
if (limitArgs.length === 0)
|
|
248
|
+
return;
|
|
249
|
+
const args = ["update", ...limitArgs, containerName];
|
|
250
|
+
try {
|
|
251
|
+
await this.execFileImpl("docker", args);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
log.logWarning(`Failed to apply resource limits to container ${containerName}`, err instanceof Error ? err.message : String(err));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async hasRuntimeDrift(containerKey, containerName, mounts) {
|
|
258
|
+
if (await this.hasBindMountDrift(containerName, mounts)) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
if (await this.hasMountSignatureDrift(containerName, mounts)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
return this.hasNetworkModeDrift(containerKey, containerName);
|
|
265
|
+
}
|
|
205
266
|
async hasBindMountDrift(containerName, mounts) {
|
|
206
267
|
const expected = this.expectedBinds(mounts);
|
|
207
268
|
const actual = await this.inspectBindMounts(containerName);
|
|
208
269
|
return !this.sameBinds(expected, actual);
|
|
209
270
|
}
|
|
210
271
|
expectedBinds(mounts) {
|
|
211
|
-
return
|
|
272
|
+
return mounts
|
|
273
|
+
.map((mount) => this.toBindSpec(mount))
|
|
212
274
|
.slice()
|
|
213
|
-
.
|
|
275
|
+
.toSorted();
|
|
214
276
|
}
|
|
215
277
|
sameBinds(expected, actual) {
|
|
216
278
|
if (expected.length !== actual.length) {
|
|
@@ -218,6 +280,41 @@ export class DockerContainerManager {
|
|
|
218
280
|
}
|
|
219
281
|
return expected.every((bind, index) => bind === actual[index]);
|
|
220
282
|
}
|
|
283
|
+
async hasMountSignatureDrift(containerName, mounts) {
|
|
284
|
+
if (mounts.length === 0)
|
|
285
|
+
return false;
|
|
286
|
+
const expected = this.mountSignature(mounts);
|
|
287
|
+
const { stdout } = await this.execFileImpl("docker", [
|
|
288
|
+
"inspect",
|
|
289
|
+
"-f",
|
|
290
|
+
`{{index .Config.Labels "${DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY}"}}`,
|
|
291
|
+
containerName,
|
|
292
|
+
]);
|
|
293
|
+
const actual = this.normalizeDockerValue(stdout.trim());
|
|
294
|
+
return actual !== expected;
|
|
295
|
+
}
|
|
296
|
+
mountSignature(mounts) {
|
|
297
|
+
const payload = mounts
|
|
298
|
+
.map((mount) => ({
|
|
299
|
+
source: mount.source,
|
|
300
|
+
target: mount.target,
|
|
301
|
+
fingerprint: this.mountSourceFingerprint(mount.source),
|
|
302
|
+
}))
|
|
303
|
+
.toSorted((left, right) => `${left.target}\0${left.source}`.localeCompare(`${right.target}\0${right.source}`));
|
|
304
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
305
|
+
}
|
|
306
|
+
mountSourceFingerprint(source) {
|
|
307
|
+
try {
|
|
308
|
+
const stat = statSync(source);
|
|
309
|
+
if (stat.isFile()) {
|
|
310
|
+
return createHash("sha256").update(readFileSync(source)).digest("hex");
|
|
311
|
+
}
|
|
312
|
+
return `${stat.isDirectory() ? "dir" : "other"}:${stat.size}:${stat.mtimeMs}`;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return "missing";
|
|
316
|
+
}
|
|
317
|
+
}
|
|
221
318
|
async inspectBindMounts(containerName) {
|
|
222
319
|
const { stdout } = await this.execFileImpl("docker", [
|
|
223
320
|
"inspect",
|
|
@@ -233,7 +330,42 @@ export class DockerContainerManager {
|
|
|
233
330
|
if (!Array.isArray(parsed) || parsed.some((bind) => typeof bind !== "string")) {
|
|
234
331
|
throw new Error(`Unexpected docker bind mount payload for container "${containerName}"`);
|
|
235
332
|
}
|
|
236
|
-
return [...parsed].
|
|
333
|
+
return [...parsed].toSorted();
|
|
334
|
+
}
|
|
335
|
+
async hasNetworkModeDrift(containerKey, containerName) {
|
|
336
|
+
const expected = DockerContainerManager.networkName(containerKey);
|
|
337
|
+
const { stdout } = await this.execFileImpl("docker", [
|
|
338
|
+
"inspect",
|
|
339
|
+
"-f",
|
|
340
|
+
"{{.HostConfig.NetworkMode}}",
|
|
341
|
+
containerName,
|
|
342
|
+
]);
|
|
343
|
+
return stdout.trim() !== expected;
|
|
344
|
+
}
|
|
345
|
+
async ensureNetwork(containerKey) {
|
|
346
|
+
const networkName = DockerContainerManager.networkName(containerKey);
|
|
347
|
+
try {
|
|
348
|
+
await this.execFileImpl("docker", ["network", "inspect", networkName]);
|
|
349
|
+
return networkName;
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
if (!isDockerNotFoundError(err))
|
|
353
|
+
throw err;
|
|
354
|
+
}
|
|
355
|
+
await this.execFileImpl("docker", [
|
|
356
|
+
"network",
|
|
357
|
+
"create",
|
|
358
|
+
"--driver",
|
|
359
|
+
"bridge",
|
|
360
|
+
"--label",
|
|
361
|
+
DockerContainerManager.MANAGED_LABEL,
|
|
362
|
+
"--label",
|
|
363
|
+
DockerContainerManager.IMAGE_MODE_LABEL,
|
|
364
|
+
"--label",
|
|
365
|
+
`${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,
|
|
366
|
+
networkName,
|
|
367
|
+
]);
|
|
368
|
+
return networkName;
|
|
237
369
|
}
|
|
238
370
|
async inspectStatus(containerName) {
|
|
239
371
|
try {
|
|
@@ -245,8 +377,10 @@ export class DockerContainerManager {
|
|
|
245
377
|
]);
|
|
246
378
|
return stdout.trim() === "true" ? "running" : "stopped";
|
|
247
379
|
}
|
|
248
|
-
catch {
|
|
249
|
-
|
|
380
|
+
catch (err) {
|
|
381
|
+
if (isDockerNotFoundError(err))
|
|
382
|
+
return "missing";
|
|
383
|
+
throw err;
|
|
250
384
|
}
|
|
251
385
|
}
|
|
252
386
|
async listContainerNamesByLabel() {
|
|
@@ -296,14 +430,15 @@ export class DockerContainerManager {
|
|
|
296
430
|
const { stdout } = await this.execFileImpl("docker", [
|
|
297
431
|
"inspect",
|
|
298
432
|
"-f",
|
|
299
|
-
`{{.State.Running}}\t{{.State.StartedAt}}\t{{index .Config.Labels "${DockerContainerManager.VAULT_ID_LABEL_KEY}"}}`,
|
|
433
|
+
`{{.State.Running}}\t{{.State.StartedAt}}\t{{index .Config.Labels "${DockerContainerManager.VAULT_ID_LABEL_KEY}"}}\t{{index .Config.Labels "${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}"}}`,
|
|
300
434
|
containerName,
|
|
301
435
|
]);
|
|
302
|
-
const [runningRaw, startedAtRaw, vaultIdRaw] = stdout.trim().split("\t");
|
|
436
|
+
const [runningRaw, startedAtRaw, vaultIdRaw, conversationIdRaw] = stdout.trim().split("\t");
|
|
303
437
|
const running = runningRaw === "true";
|
|
304
438
|
const startedAtMs = this.parseDockerTimestamp(startedAtRaw);
|
|
305
439
|
const vaultId = this.normalizeDockerValue(vaultIdRaw);
|
|
306
|
-
|
|
440
|
+
const conversationId = this.normalizeDockerValue(conversationIdRaw);
|
|
441
|
+
return { running, startedAtMs, vaultId, conversationId };
|
|
307
442
|
}
|
|
308
443
|
catch (err) {
|
|
309
444
|
log.logWarning(`Failed to inspect container ${containerName} during reconcile`, err instanceof Error ? err.message : String(err));
|
|
@@ -323,14 +458,24 @@ export class DockerContainerManager {
|
|
|
323
458
|
const parsed = Date.parse(normalized);
|
|
324
459
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
325
460
|
}
|
|
326
|
-
|
|
461
|
+
containerKeyFromContainerName(containerName) {
|
|
327
462
|
const prefix = DockerContainerManager.containerName("");
|
|
328
463
|
if (!containerName.startsWith(prefix))
|
|
329
464
|
return undefined;
|
|
330
|
-
const
|
|
331
|
-
return
|
|
465
|
+
const containerKey = containerName.slice(prefix.length);
|
|
466
|
+
return containerKey.length > 0 ? containerKey : undefined;
|
|
467
|
+
}
|
|
468
|
+
async forceRemoveContainer(containerName, successLog, failureLog) {
|
|
469
|
+
try {
|
|
470
|
+
await this.execFileImpl("docker", ["rm", "-f", containerName]);
|
|
471
|
+
log.logInfo(successLog);
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
log.logWarning(failureLog, err instanceof Error ? err.message : String(err));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async removeLegacyContainer(containerName) {
|
|
478
|
+
await this.forceRemoveContainer(containerName, `Removed legacy mama container ${containerName} (pre-channel-isolation scheme)`, `Failed to remove legacy mama container ${containerName}`);
|
|
332
479
|
}
|
|
333
480
|
}
|
|
334
|
-
/** @deprecated Use DockerContainerManager */
|
|
335
|
-
export const DockerProvisioner = DockerContainerManager;
|
|
336
481
|
//# sourceMappingURL=provisioner.js.map
|