@bazaar.ai/mcp-human-agents 0.1.0
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/.env.example +15 -0
- package/README.md +178 -0
- package/dist/mcp-server/src/bin.d.ts +3 -0
- package/dist/mcp-server/src/bin.d.ts.map +1 -0
- package/dist/mcp-server/src/bin.js +10 -0
- package/dist/mcp-server/src/bin.js.map +1 -0
- package/dist/mcp-server/src/cli/setup.d.ts +2 -0
- package/dist/mcp-server/src/cli/setup.d.ts.map +1 -0
- package/dist/mcp-server/src/cli/setup.js +274 -0
- package/dist/mcp-server/src/cli/setup.js.map +1 -0
- package/dist/mcp-server/src/config/defaults.d.ts +16 -0
- package/dist/mcp-server/src/config/defaults.d.ts.map +1 -0
- package/dist/mcp-server/src/config/defaults.js +19 -0
- package/dist/mcp-server/src/config/defaults.js.map +1 -0
- package/dist/mcp-server/src/config/env.d.ts +73 -0
- package/dist/mcp-server/src/config/env.d.ts.map +1 -0
- package/dist/mcp-server/src/config/env.js +72 -0
- package/dist/mcp-server/src/config/env.js.map +1 -0
- package/dist/mcp-server/src/config/index.d.ts +3 -0
- package/dist/mcp-server/src/config/index.d.ts.map +1 -0
- package/dist/mcp-server/src/config/index.js +22 -0
- package/dist/mcp-server/src/config/index.js.map +1 -0
- package/dist/mcp-server/src/context/generator.d.ts +16 -0
- package/dist/mcp-server/src/context/generator.d.ts.map +1 -0
- package/dist/mcp-server/src/context/generator.js +61 -0
- package/dist/mcp-server/src/context/generator.js.map +1 -0
- package/dist/mcp-server/src/context/index.d.ts +3 -0
- package/dist/mcp-server/src/context/index.d.ts.map +1 -0
- package/dist/mcp-server/src/context/index.js +10 -0
- package/dist/mcp-server/src/context/index.js.map +1 -0
- package/dist/mcp-server/src/context/templates.d.ts +8 -0
- package/dist/mcp-server/src/context/templates.d.ts.map +1 -0
- package/dist/mcp-server/src/context/templates.js +41 -0
- package/dist/mcp-server/src/context/templates.js.map +1 -0
- package/dist/mcp-server/src/git/branch.d.ts +13 -0
- package/dist/mcp-server/src/git/branch.d.ts.map +1 -0
- package/dist/mcp-server/src/git/branch.js +49 -0
- package/dist/mcp-server/src/git/branch.js.map +1 -0
- package/dist/mcp-server/src/git/diff.d.ts +10 -0
- package/dist/mcp-server/src/git/diff.d.ts.map +1 -0
- package/dist/mcp-server/src/git/diff.js +39 -0
- package/dist/mcp-server/src/git/diff.js.map +1 -0
- package/dist/mcp-server/src/git/index.d.ts +5 -0
- package/dist/mcp-server/src/git/index.d.ts.map +1 -0
- package/dist/mcp-server/src/git/index.js +16 -0
- package/dist/mcp-server/src/git/index.js.map +1 -0
- package/dist/mcp-server/src/git/merge.d.ts +6 -0
- package/dist/mcp-server/src/git/merge.d.ts.map +1 -0
- package/dist/mcp-server/src/git/merge.js +30 -0
- package/dist/mcp-server/src/git/merge.js.map +1 -0
- package/dist/mcp-server/src/git/worktree.d.ts +11 -0
- package/dist/mcp-server/src/git/worktree.d.ts.map +1 -0
- package/dist/mcp-server/src/git/worktree.js +38 -0
- package/dist/mcp-server/src/git/worktree.js.map +1 -0
- package/dist/mcp-server/src/http-wrapper.d.ts +6 -0
- package/dist/mcp-server/src/http-wrapper.d.ts.map +1 -0
- package/dist/mcp-server/src/http-wrapper.js +85 -0
- package/dist/mcp-server/src/http-wrapper.js.map +1 -0
- package/dist/mcp-server/src/index.d.ts +2 -0
- package/dist/mcp-server/src/index.d.ts.map +1 -0
- package/dist/mcp-server/src/index.js +28 -0
- package/dist/mcp-server/src/index.js.map +1 -0
- package/dist/mcp-server/src/platform-client/client.d.ts +17 -0
- package/dist/mcp-server/src/platform-client/client.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/client.js +68 -0
- package/dist/mcp-server/src/platform-client/client.js.map +1 -0
- package/dist/mcp-server/src/platform-client/index.d.ts +5 -0
- package/dist/mcp-server/src/platform-client/index.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/index.js +10 -0
- package/dist/mcp-server/src/platform-client/index.js.map +1 -0
- package/dist/mcp-server/src/platform-client/mock-client.d.ts +28 -0
- package/dist/mcp-server/src/platform-client/mock-client.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/mock-client.js +75 -0
- package/dist/mcp-server/src/platform-client/mock-client.js.map +1 -0
- package/dist/mcp-server/src/platform-client/polling.d.ts +9 -0
- package/dist/mcp-server/src/platform-client/polling.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/polling.js +40 -0
- package/dist/mcp-server/src/platform-client/polling.js.map +1 -0
- package/dist/mcp-server/src/platform-client/types.d.ts +2 -0
- package/dist/mcp-server/src/platform-client/types.d.ts.map +1 -0
- package/dist/mcp-server/src/platform-client/types.js +3 -0
- package/dist/mcp-server/src/platform-client/types.js.map +1 -0
- package/dist/mcp-server/src/provisioning/authorized-keys.d.ts +14 -0
- package/dist/mcp-server/src/provisioning/authorized-keys.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/authorized-keys.js +48 -0
- package/dist/mcp-server/src/provisioning/authorized-keys.js.map +1 -0
- package/dist/mcp-server/src/provisioning/cleanup.d.ts +19 -0
- package/dist/mcp-server/src/provisioning/cleanup.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/cleanup.js +96 -0
- package/dist/mcp-server/src/provisioning/cleanup.js.map +1 -0
- package/dist/mcp-server/src/provisioning/index.d.ts +6 -0
- package/dist/mcp-server/src/provisioning/index.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/index.js +24 -0
- package/dist/mcp-server/src/provisioning/index.js.map +1 -0
- package/dist/mcp-server/src/provisioning/linux-user.d.ts +15 -0
- package/dist/mcp-server/src/provisioning/linux-user.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/linux-user.js +62 -0
- package/dist/mcp-server/src/provisioning/linux-user.js.map +1 -0
- package/dist/mcp-server/src/provisioning/privileged.d.ts +40 -0
- package/dist/mcp-server/src/provisioning/privileged.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/privileged.js +123 -0
- package/dist/mcp-server/src/provisioning/privileged.js.map +1 -0
- package/dist/mcp-server/src/provisioning/ssh-config.d.ts +21 -0
- package/dist/mcp-server/src/provisioning/ssh-config.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/ssh-config.js +161 -0
- package/dist/mcp-server/src/provisioning/ssh-config.js.map +1 -0
- package/dist/mcp-server/src/provisioning/tmux-session.d.ts +37 -0
- package/dist/mcp-server/src/provisioning/tmux-session.d.ts.map +1 -0
- package/dist/mcp-server/src/provisioning/tmux-session.js +123 -0
- package/dist/mcp-server/src/provisioning/tmux-session.js.map +1 -0
- package/dist/mcp-server/src/server.d.ts +3 -0
- package/dist/mcp-server/src/server.d.ts.map +1 -0
- package/dist/mcp-server/src/server.js +67 -0
- package/dist/mcp-server/src/server.js.map +1 -0
- package/dist/mcp-server/src/state/gig-store.d.ts +19 -0
- package/dist/mcp-server/src/state/gig-store.d.ts.map +1 -0
- package/dist/mcp-server/src/state/gig-store.js +52 -0
- package/dist/mcp-server/src/state/gig-store.js.map +1 -0
- package/dist/mcp-server/src/state/index.d.ts +4 -0
- package/dist/mcp-server/src/state/index.d.ts.map +1 -0
- package/dist/mcp-server/src/state/index.js +8 -0
- package/dist/mcp-server/src/state/index.js.map +1 -0
- package/dist/mcp-server/src/state/persistence.d.ts +13 -0
- package/dist/mcp-server/src/state/persistence.d.ts.map +1 -0
- package/dist/mcp-server/src/state/persistence.js +48 -0
- package/dist/mcp-server/src/state/persistence.js.map +1 -0
- package/dist/mcp-server/src/state/types.d.ts +15 -0
- package/dist/mcp-server/src/state/types.d.ts.map +1 -0
- package/dist/mcp-server/src/state/types.js +3 -0
- package/dist/mcp-server/src/state/types.js.map +1 -0
- package/dist/mcp-server/src/tools/dismiss-human.d.ts +25 -0
- package/dist/mcp-server/src/tools/dismiss-human.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/dismiss-human.js +78 -0
- package/dist/mcp-server/src/tools/dismiss-human.js.map +1 -0
- package/dist/mcp-server/src/tools/index.d.ts +9 -0
- package/dist/mcp-server/src/tools/index.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/index.js +20 -0
- package/dist/mcp-server/src/tools/index.js.map +1 -0
- package/dist/mcp-server/src/tools/list-humans.d.ts +18 -0
- package/dist/mcp-server/src/tools/list-humans.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/list-humans.js +35 -0
- package/dist/mcp-server/src/tools/list-humans.js.map +1 -0
- package/dist/mcp-server/src/tools/message-human.d.ts +10 -0
- package/dist/mcp-server/src/tools/message-human.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/message-human.js +19 -0
- package/dist/mcp-server/src/tools/message-human.js.map +1 -0
- package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.d.ts +19 -0
- package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.js +22 -0
- package/dist/mcp-server/src/tools/schemas/dismiss-human.schema.js.map +1 -0
- package/dist/mcp-server/src/tools/schemas/list-humans.schema.d.ts +4 -0
- package/dist/mcp-server/src/tools/schemas/list-humans.schema.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/schemas/list-humans.schema.js +7 -0
- package/dist/mcp-server/src/tools/schemas/list-humans.schema.js.map +1 -0
- package/dist/mcp-server/src/tools/schemas/message-human.schema.d.ts +13 -0
- package/dist/mcp-server/src/tools/schemas/message-human.schema.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/schemas/message-human.schema.js +9 -0
- package/dist/mcp-server/src/tools/schemas/message-human.schema.js.map +1 -0
- package/dist/mcp-server/src/tools/schemas/summon-human.schema.d.ts +22 -0
- package/dist/mcp-server/src/tools/schemas/summon-human.schema.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/schemas/summon-human.schema.js +18 -0
- package/dist/mcp-server/src/tools/schemas/summon-human.schema.js.map +1 -0
- package/dist/mcp-server/src/tools/summon-human.d.ts +31 -0
- package/dist/mcp-server/src/tools/summon-human.d.ts.map +1 -0
- package/dist/mcp-server/src/tools/summon-human.js +137 -0
- package/dist/mcp-server/src/tools/summon-human.js.map +1 -0
- package/dist/mcp-server/src/tunnel/client.d.ts +16 -0
- package/dist/mcp-server/src/tunnel/client.d.ts.map +1 -0
- package/dist/mcp-server/src/tunnel/client.js +100 -0
- package/dist/mcp-server/src/tunnel/client.js.map +1 -0
- package/dist/mcp-server/src/tunnel/index.d.ts +6 -0
- package/dist/mcp-server/src/tunnel/index.d.ts.map +1 -0
- package/dist/mcp-server/src/tunnel/index.js +28 -0
- package/dist/mcp-server/src/tunnel/index.js.map +1 -0
- package/dist/mcp-server/src/utils/errors.d.ts +28 -0
- package/dist/mcp-server/src/utils/errors.d.ts.map +1 -0
- package/dist/mcp-server/src/utils/errors.js +66 -0
- package/dist/mcp-server/src/utils/errors.js.map +1 -0
- package/dist/mcp-server/src/utils/exec.d.ts +7 -0
- package/dist/mcp-server/src/utils/exec.d.ts.map +1 -0
- package/dist/mcp-server/src/utils/exec.js +22 -0
- package/dist/mcp-server/src/utils/exec.js.map +1 -0
- package/dist/mcp-server/src/utils/ip.d.ts +6 -0
- package/dist/mcp-server/src/utils/ip.d.ts.map +1 -0
- package/dist/mcp-server/src/utils/ip.js +33 -0
- package/dist/mcp-server/src/utils/ip.js.map +1 -0
- package/dist/mcp-server/src/utils/logger.d.ts +20 -0
- package/dist/mcp-server/src/utils/logger.d.ts.map +1 -0
- package/dist/mcp-server/src/utils/logger.js +41 -0
- package/dist/mcp-server/src/utils/logger.js.map +1 -0
- package/dist/shared/src/contractor.types.d.ts +20 -0
- package/dist/shared/src/contractor.types.d.ts.map +1 -0
- package/dist/shared/src/contractor.types.js +3 -0
- package/dist/shared/src/contractor.types.js.map +1 -0
- package/dist/shared/src/gig.types.d.ts +32 -0
- package/dist/shared/src/gig.types.d.ts.map +1 -0
- package/dist/shared/src/gig.types.js +21 -0
- package/dist/shared/src/gig.types.js.map +1 -0
- package/dist/shared/src/index.d.ts +5 -0
- package/dist/shared/src/index.d.ts.map +1 -0
- package/dist/shared/src/index.js +21 -0
- package/dist/shared/src/index.js.map +1 -0
- package/dist/shared/src/mcp-tool.types.d.ts +45 -0
- package/dist/shared/src/mcp-tool.types.d.ts.map +1 -0
- package/dist/shared/src/mcp-tool.types.js +3 -0
- package/dist/shared/src/mcp-tool.types.js.map +1 -0
- package/dist/shared/src/platform-api.types.d.ts +73 -0
- package/dist/shared/src/platform-api.types.d.ts.map +1 -0
- package/dist/shared/src/platform-api.types.js +3 -0
- package/dist/shared/src/platform-api.types.js.map +1 -0
- package/package.json +41 -0
- package/src/bin.ts +7 -0
- package/src/cli/setup.ts +317 -0
- package/src/config/defaults.ts +21 -0
- package/src/config/env.ts +74 -0
- package/src/config/index.ts +2 -0
- package/src/context/generator.ts +71 -0
- package/src/context/index.ts +6 -0
- package/src/context/templates.ts +41 -0
- package/src/git/branch.ts +46 -0
- package/src/git/diff.ts +34 -0
- package/src/git/index.ts +4 -0
- package/src/git/merge.ts +36 -0
- package/src/git/worktree.ts +42 -0
- package/src/http-wrapper.ts +94 -0
- package/src/index.ts +32 -0
- package/src/platform-client/client.ts +93 -0
- package/src/platform-client/index.ts +4 -0
- package/src/platform-client/mock-client.ts +92 -0
- package/src/platform-client/polling.ts +53 -0
- package/src/platform-client/types.ts +9 -0
- package/src/provisioning/authorized-keys.ts +52 -0
- package/src/provisioning/cleanup.ts +106 -0
- package/src/provisioning/index.ts +13 -0
- package/src/provisioning/linux-user.ts +66 -0
- package/src/provisioning/privileged.ts +128 -0
- package/src/provisioning/ssh-config.ts +197 -0
- package/src/provisioning/tmux-session.ts +136 -0
- package/src/server.ts +111 -0
- package/src/state/gig-store.ts +56 -0
- package/src/state/index.ts +3 -0
- package/src/state/persistence.ts +42 -0
- package/src/state/types.ts +14 -0
- package/src/tools/dismiss-human.ts +103 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/list-humans.ts +54 -0
- package/src/tools/message-human.ts +28 -0
- package/src/tools/schemas/dismiss-human.schema.ts +21 -0
- package/src/tools/schemas/list-humans.schema.ts +6 -0
- package/src/tools/schemas/message-human.schema.ts +8 -0
- package/src/tools/schemas/summon-human.schema.ts +19 -0
- package/src/tools/summon-human.ts +180 -0
- package/src/tunnel/client.ts +116 -0
- package/src/tunnel/index.ts +26 -0
- package/src/utils/errors.ts +64 -0
- package/src/utils/exec.ts +29 -0
- package/src/utils/ip.ts +31 -0
- package/src/utils/logger.ts +55 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { exec } from "../utils/exec.js";
|
|
2
|
+
import { GitError } from "../utils/errors.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a git worktree for "clean" mode.
|
|
7
|
+
* Gives the contractor a fresh checkout of the branch
|
|
8
|
+
* without the AI's dirty working state.
|
|
9
|
+
*/
|
|
10
|
+
export async function createWorktree(
|
|
11
|
+
branch: string,
|
|
12
|
+
targetDir: string,
|
|
13
|
+
cwd?: string,
|
|
14
|
+
): Promise<string> {
|
|
15
|
+
try {
|
|
16
|
+
await exec(`git worktree add ${targetDir} ${branch}`, { cwd });
|
|
17
|
+
logger.info("Git worktree created", { branch, targetDir });
|
|
18
|
+
return targetDir;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new GitError(
|
|
21
|
+
`Failed to create worktree at ${targetDir}: ${error}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Remove a git worktree.
|
|
28
|
+
*/
|
|
29
|
+
export async function removeWorktree(
|
|
30
|
+
targetDir: string,
|
|
31
|
+
cwd?: string,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
await exec(`git worktree remove ${targetDir} --force`, { cwd });
|
|
35
|
+
logger.info("Git worktree removed", { targetDir });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logger.warn("Failed to remove worktree (may not exist)", {
|
|
38
|
+
targetDir,
|
|
39
|
+
error: String(error),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick HTTP wrapper around the MCP tools for testing.
|
|
3
|
+
* Exposes summon_human, dismiss_human, list_humans, message_human as REST endpoints.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express from "express";
|
|
7
|
+
import { getEnv } from "./config/env.js";
|
|
8
|
+
import { GigStore } from "./state/gig-store.js";
|
|
9
|
+
import { StatePersistence } from "./state/persistence.js";
|
|
10
|
+
import { MockPlatformClient } from "./platform-client/mock-client.js";
|
|
11
|
+
import { HttpPlatformClient } from "./platform-client/client.js";
|
|
12
|
+
import type { PlatformClient } from "./platform-client/client.js";
|
|
13
|
+
import {
|
|
14
|
+
summonHuman,
|
|
15
|
+
dismissHuman,
|
|
16
|
+
listHumans,
|
|
17
|
+
messageHuman,
|
|
18
|
+
} from "./tools/index.js";
|
|
19
|
+
|
|
20
|
+
const env = getEnv();
|
|
21
|
+
const gigStore = new GigStore();
|
|
22
|
+
const persistence = new StatePersistence(env.STATE_FILE_PATH);
|
|
23
|
+
|
|
24
|
+
const platformClient: PlatformClient = env.USE_MOCK_PLATFORM
|
|
25
|
+
? new MockPlatformClient()
|
|
26
|
+
: new HttpPlatformClient(env.PLATFORM_API_URL, env.PLATFORM_API_KEY);
|
|
27
|
+
|
|
28
|
+
const deps = { platformClient, gigStore };
|
|
29
|
+
|
|
30
|
+
const app = express();
|
|
31
|
+
app.use(express.json());
|
|
32
|
+
|
|
33
|
+
app.get("/health", (_req, res) => {
|
|
34
|
+
res.json({ status: "ok", service: "human-agents-http", gigs: gigStore.list().length });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.post("/summon", async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const result = await summonHuman(req.body, deps);
|
|
40
|
+
await persistence.save(gigStore);
|
|
41
|
+
res.json(result);
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
res.status(500).json({ error: err.message });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.post("/dismiss", async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await dismissHuman(req.body, deps);
|
|
50
|
+
await persistence.save(gigStore);
|
|
51
|
+
res.json(result);
|
|
52
|
+
} catch (err: any) {
|
|
53
|
+
res.status(500).json({ error: err.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.get("/list", async (_req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const result = await listHumans(gigStore);
|
|
60
|
+
res.json(result);
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
res.status(500).json({ error: err.message });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
app.post("/message", async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await messageHuman(req.body, gigStore);
|
|
69
|
+
res.json(result);
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
res.status(500).json({ error: err.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const PORT = parseInt(process.env.HTTP_PORT ?? "9999", 10);
|
|
76
|
+
|
|
77
|
+
async function start() {
|
|
78
|
+
await persistence.load(gigStore);
|
|
79
|
+
app.listen(PORT, () => {
|
|
80
|
+
console.log(`Human-agents HTTP wrapper on port ${PORT}`);
|
|
81
|
+
console.log(` Mock platform: ${env.USE_MOCK_PLATFORM}`);
|
|
82
|
+
console.log(` VM IP: ${env.VM_EXTERNAL_IP}`);
|
|
83
|
+
console.log(`\nEndpoints:`);
|
|
84
|
+
console.log(` POST /summon — summon a contractor`);
|
|
85
|
+
console.log(` POST /dismiss — dismiss a contractor`);
|
|
86
|
+
console.log(` GET /list — list active contractors`);
|
|
87
|
+
console.log(` POST /message — message a contractor`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
start().catch((err) => {
|
|
92
|
+
console.error("Failed to start:", err);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
import { logger } from "./utils/logger.js";
|
|
4
|
+
|
|
5
|
+
async function main(): Promise<void> {
|
|
6
|
+
logger.info("Starting human-agents MCP server");
|
|
7
|
+
|
|
8
|
+
const server = await createServer();
|
|
9
|
+
const transport = new StdioServerTransport();
|
|
10
|
+
|
|
11
|
+
await server.connect(transport);
|
|
12
|
+
|
|
13
|
+
logger.info("MCP server running on stdio transport");
|
|
14
|
+
|
|
15
|
+
// Handle graceful shutdown
|
|
16
|
+
process.on("SIGINT", async () => {
|
|
17
|
+
logger.info("Received SIGINT, shutting down");
|
|
18
|
+
await server.close();
|
|
19
|
+
process.exit(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.on("SIGTERM", async () => {
|
|
23
|
+
logger.info("Received SIGTERM, shutting down");
|
|
24
|
+
await server.close();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main().catch((error) => {
|
|
30
|
+
logger.error("Fatal error", { error: String(error) });
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import axios, { type AxiosInstance } from "axios";
|
|
2
|
+
import { PlatformApiError } from "../utils/errors.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
import type {
|
|
5
|
+
CreateGigRequest,
|
|
6
|
+
CreateGigResponse,
|
|
7
|
+
GigStatusResponse,
|
|
8
|
+
CompleteGigRequest,
|
|
9
|
+
CompleteGigResponse,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
export interface PlatformClient {
|
|
13
|
+
createGig(request: CreateGigRequest): Promise<CreateGigResponse>;
|
|
14
|
+
getGigStatus(gigId: string): Promise<GigStatusResponse>;
|
|
15
|
+
markReady(gigId: string, sshCommand: string): Promise<void>;
|
|
16
|
+
completeGig(request: CompleteGigRequest): Promise<CompleteGigResponse>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class HttpPlatformClient implements PlatformClient {
|
|
20
|
+
private http: AxiosInstance;
|
|
21
|
+
|
|
22
|
+
constructor(baseUrl: string, apiKey: string) {
|
|
23
|
+
this.http = axios.create({
|
|
24
|
+
baseURL: baseUrl,
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${apiKey}`,
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
},
|
|
29
|
+
timeout: 10_000,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async createGig(request: CreateGigRequest): Promise<CreateGigResponse> {
|
|
34
|
+
try {
|
|
35
|
+
const { data } = await this.http.post<CreateGigResponse>(
|
|
36
|
+
"/gigs",
|
|
37
|
+
request,
|
|
38
|
+
);
|
|
39
|
+
logger.info("Gig created on platform", { gigId: data.gigId });
|
|
40
|
+
return data;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw this.wrapError("Failed to create gig", error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getGigStatus(gigId: string): Promise<GigStatusResponse> {
|
|
47
|
+
try {
|
|
48
|
+
const { data } = await this.http.get<GigStatusResponse>(
|
|
49
|
+
`/gigs/${gigId}/status`,
|
|
50
|
+
);
|
|
51
|
+
return data;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw this.wrapError(`Failed to get gig status: ${gigId}`, error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async markReady(gigId: string, sshCommand: string): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
await this.http.post(`/gigs/${gigId}/ready`, { sshCommand });
|
|
60
|
+
logger.info("Gig marked as ready", { gigId });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw this.wrapError(`Failed to mark gig ready: ${gigId}`, error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async completeGig(
|
|
67
|
+
request: CompleteGigRequest,
|
|
68
|
+
): Promise<CompleteGigResponse> {
|
|
69
|
+
try {
|
|
70
|
+
const { data } = await this.http.post<CompleteGigResponse>(
|
|
71
|
+
`/gigs/${request.gigId}/complete`,
|
|
72
|
+
request,
|
|
73
|
+
);
|
|
74
|
+
logger.info("Gig completed on platform", { gigId: request.gigId });
|
|
75
|
+
return data;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw this.wrapError(
|
|
78
|
+
`Failed to complete gig: ${request.gigId}`,
|
|
79
|
+
error,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private wrapError(message: string, error: unknown): PlatformApiError {
|
|
85
|
+
if (axios.isAxiosError(error)) {
|
|
86
|
+
return new PlatformApiError(
|
|
87
|
+
`${message}: ${error.message}`,
|
|
88
|
+
error.response?.status,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return new PlatformApiError(message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
import type { PlatformClient } from "./client.js";
|
|
3
|
+
import type {
|
|
4
|
+
CreateGigRequest,
|
|
5
|
+
CreateGigResponse,
|
|
6
|
+
GigStatusResponse,
|
|
7
|
+
CompleteGigRequest,
|
|
8
|
+
CompleteGigResponse,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
interface MockGig {
|
|
12
|
+
id: string;
|
|
13
|
+
status: "OPEN" | "ASSIGNED" | "IN_PROGRESS" | "COMPLETED";
|
|
14
|
+
assignedAt?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* In-memory mock platform client for Phase 1 standalone testing.
|
|
19
|
+
* A "mock contractor" auto-accepts gigs after a configurable delay.
|
|
20
|
+
*/
|
|
21
|
+
export class MockPlatformClient implements PlatformClient {
|
|
22
|
+
private gigs = new Map<string, MockGig>();
|
|
23
|
+
private nextId = 1;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
/** Delay in ms before a mock contractor auto-accepts a gig. */
|
|
27
|
+
private readonly autoAcceptDelayMs = 5_000,
|
|
28
|
+
/** Mock contractor info. */
|
|
29
|
+
private readonly mockContractor = {
|
|
30
|
+
name: "mock-contractor",
|
|
31
|
+
pubkey:
|
|
32
|
+
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleMockKeyForTesting mock@test",
|
|
33
|
+
rate: 50,
|
|
34
|
+
},
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
async createGig(request: CreateGigRequest): Promise<CreateGigResponse> {
|
|
38
|
+
const id = `mock-gig-${this.nextId++}`;
|
|
39
|
+
const gig: MockGig = { id, status: "OPEN" };
|
|
40
|
+
this.gigs.set(id, gig);
|
|
41
|
+
|
|
42
|
+
// Auto-accept after delay
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
const g = this.gigs.get(id);
|
|
45
|
+
if (g && g.status === "OPEN") {
|
|
46
|
+
g.status = "ASSIGNED";
|
|
47
|
+
g.assignedAt = Date.now();
|
|
48
|
+
logger.info("[MOCK] Contractor auto-accepted gig", { gigId: id });
|
|
49
|
+
}
|
|
50
|
+
}, this.autoAcceptDelayMs);
|
|
51
|
+
|
|
52
|
+
logger.info("[MOCK] Gig created", {
|
|
53
|
+
gigId: id,
|
|
54
|
+
reason: request.reason,
|
|
55
|
+
});
|
|
56
|
+
return { gigId: id, status: "OPEN" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getGigStatus(gigId: string): Promise<GigStatusResponse> {
|
|
60
|
+
const gig = this.gigs.get(gigId);
|
|
61
|
+
if (!gig) {
|
|
62
|
+
return { gigId, status: "CANCELLED" };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
gigId,
|
|
66
|
+
status: gig.status,
|
|
67
|
+
contractor:
|
|
68
|
+
gig.status === "ASSIGNED" || gig.status === "IN_PROGRESS"
|
|
69
|
+
? this.mockContractor
|
|
70
|
+
: undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async markReady(gigId: string, sshCommand: string): Promise<void> {
|
|
75
|
+
const gig = this.gigs.get(gigId);
|
|
76
|
+
if (gig) {
|
|
77
|
+
gig.status = "IN_PROGRESS";
|
|
78
|
+
logger.info("[MOCK] Gig marked ready", { gigId, sshCommand });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async completeGig(
|
|
83
|
+
request: CompleteGigRequest,
|
|
84
|
+
): Promise<CompleteGigResponse> {
|
|
85
|
+
const gig = this.gigs.get(request.gigId);
|
|
86
|
+
if (gig) {
|
|
87
|
+
gig.status = "COMPLETED";
|
|
88
|
+
}
|
|
89
|
+
logger.info("[MOCK] Gig completed", { gigId: request.gigId });
|
|
90
|
+
return { gigId: request.gigId, status: "COMPLETED" };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PlatformClient } from "./client.js";
|
|
2
|
+
import type { GigStatusResponse } from "./types.js";
|
|
3
|
+
import { PlatformApiError } from "../utils/errors.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { getEnv } from "../config/env.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Poll GET /gigs/{id}/status until a contractor is assigned.
|
|
9
|
+
* Returns the status response with contractor info.
|
|
10
|
+
* Throws PlatformApiError if polling times out.
|
|
11
|
+
*/
|
|
12
|
+
export async function pollForAssignment(
|
|
13
|
+
client: PlatformClient,
|
|
14
|
+
gigId: string,
|
|
15
|
+
): Promise<GigStatusResponse> {
|
|
16
|
+
const env = getEnv();
|
|
17
|
+
const intervalMs = env.POLL_INTERVAL_MS;
|
|
18
|
+
const timeoutMs = env.POLL_TIMEOUT_MS;
|
|
19
|
+
|
|
20
|
+
const deadline = Date.now() + timeoutMs;
|
|
21
|
+
|
|
22
|
+
while (Date.now() < deadline) {
|
|
23
|
+
const status = await client.getGigStatus(gigId);
|
|
24
|
+
|
|
25
|
+
// Both the real API (Prisma enums) and the mock client return
|
|
26
|
+
// uppercase values per `human-layer/shared/src/gig.types.ts`.
|
|
27
|
+
if (
|
|
28
|
+
(status.status === "ASSIGNED" || status.status === "IN_PROGRESS") &&
|
|
29
|
+
status.contractor
|
|
30
|
+
) {
|
|
31
|
+
logger.info("Contractor assigned", {
|
|
32
|
+
gigId,
|
|
33
|
+
contractorName: status.contractor.name,
|
|
34
|
+
});
|
|
35
|
+
return status;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (status.status === "CANCELLED") {
|
|
39
|
+
throw new PlatformApiError(`Gig was cancelled: ${gigId}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logger.debug("Polling for contractor assignment...", { gigId });
|
|
43
|
+
await sleep(intervalMs);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new PlatformApiError(
|
|
47
|
+
`Timed out waiting for contractor assignment after ${timeoutMs}ms`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sleep(ms: number): Promise<void> {
|
|
52
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { writePrivilegedFile, chownPrivileged } from "./privileged.js";
|
|
2
|
+
import { ProvisioningError } from "../utils/errors.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Write a public key to a user's authorized_keys file.
|
|
7
|
+
*
|
|
8
|
+
* Goes through the privileged helpers because the per-contractor home
|
|
9
|
+
* directory is owned by root immediately after `useradd`. After
|
|
10
|
+
* writing the file (as root) we chown it to the contractor so sshd
|
|
11
|
+
* accepts it.
|
|
12
|
+
*/
|
|
13
|
+
export async function writeAuthorizedKey(
|
|
14
|
+
username: string,
|
|
15
|
+
pubkey: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const keyPath = `/home/${username}/.ssh/authorized_keys`;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Validate pubkey format (basic check)
|
|
21
|
+
if (!pubkey.startsWith("ssh-") && !pubkey.startsWith("ecdsa-")) {
|
|
22
|
+
throw new Error(`Invalid public key format: ${pubkey.slice(0, 20)}...`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await writePrivilegedFile(keyPath, pubkey.trim() + "\n", 0o600);
|
|
26
|
+
await chownPrivileged(`${username}:${username}`, keyPath);
|
|
27
|
+
|
|
28
|
+
logger.info("Authorized key written", { user: username });
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new ProvisioningError(
|
|
31
|
+
`Failed to write authorized key for ${username}: ${error}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remove the authorized_keys file for a user.
|
|
38
|
+
*/
|
|
39
|
+
export async function removeAuthorizedKey(username: string): Promise<void> {
|
|
40
|
+
const keyPath = `/home/${username}/.ssh/authorized_keys`;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await writePrivilegedFile(keyPath, "", 0o600);
|
|
44
|
+
logger.info("Authorized key removed", { user: username });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// File might not exist if provisioning was partial
|
|
47
|
+
logger.warn("Failed to remove authorized key (may not exist)", {
|
|
48
|
+
user: username,
|
|
49
|
+
error: String(error),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { killSession } from "./tmux-session.js";
|
|
2
|
+
import { removeAuthorizedKey } from "./authorized-keys.js";
|
|
3
|
+
import { removeForceCommand } from "./ssh-config.js";
|
|
4
|
+
import { killUserProcesses, lockUser } from "./linux-user.js";
|
|
5
|
+
import { CleanupError } from "../utils/errors.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { getEnv } from "../config/env.js";
|
|
8
|
+
|
|
9
|
+
export interface CleanupTarget {
|
|
10
|
+
username: string;
|
|
11
|
+
tmuxSession: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Aggressive cleanup sequence for dismissing a contractor.
|
|
16
|
+
* Each step is independent and wrapped in try/catch — partial failures
|
|
17
|
+
* don't prevent the remaining steps from executing.
|
|
18
|
+
*
|
|
19
|
+
* Order matters:
|
|
20
|
+
* 1. Kill tmux session (with warning if connected)
|
|
21
|
+
* 2. Kill all user processes
|
|
22
|
+
* 3. Remove SSH pubkey
|
|
23
|
+
* 4. Remove ForceCommand from sshd_config
|
|
24
|
+
* 5. Reload sshd (happens inside removeForceCommand)
|
|
25
|
+
* 6. Lock the account
|
|
26
|
+
*/
|
|
27
|
+
export async function cleanupContractor(
|
|
28
|
+
target: CleanupTarget,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const { username, tmuxSession } = target;
|
|
31
|
+
const failures: string[] = [];
|
|
32
|
+
const env = getEnv();
|
|
33
|
+
|
|
34
|
+
logger.info("Starting contractor cleanup", {
|
|
35
|
+
user: username,
|
|
36
|
+
session: tmuxSession,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// 1. Kill tmux session
|
|
40
|
+
try {
|
|
41
|
+
await killSession(tmuxSession, env.CLEANUP_WARNING_SECONDS);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
failures.push(`tmux kill: ${error}`);
|
|
44
|
+
logger.warn("Cleanup: failed to kill tmux session", {
|
|
45
|
+
user: username,
|
|
46
|
+
error: String(error),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Kill all user processes
|
|
51
|
+
try {
|
|
52
|
+
await killUserProcesses(username);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
failures.push(`kill processes: ${error}`);
|
|
55
|
+
logger.warn("Cleanup: failed to kill user processes", {
|
|
56
|
+
user: username,
|
|
57
|
+
error: String(error),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. Remove SSH pubkey
|
|
62
|
+
try {
|
|
63
|
+
await removeAuthorizedKey(username);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
failures.push(`remove key: ${error}`);
|
|
66
|
+
logger.warn("Cleanup: failed to remove authorized key", {
|
|
67
|
+
user: username,
|
|
68
|
+
error: String(error),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 4. Remove ForceCommand (also reloads sshd)
|
|
73
|
+
try {
|
|
74
|
+
await removeForceCommand(username);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
failures.push(`remove ForceCommand: ${error}`);
|
|
77
|
+
logger.warn("Cleanup: failed to remove ForceCommand", {
|
|
78
|
+
user: username,
|
|
79
|
+
error: String(error),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 5. Lock account
|
|
84
|
+
try {
|
|
85
|
+
await lockUser(username);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
failures.push(`lock user: ${error}`);
|
|
88
|
+
logger.warn("Cleanup: failed to lock user", {
|
|
89
|
+
user: username,
|
|
90
|
+
error: String(error),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (failures.length > 0) {
|
|
95
|
+
logger.warn("Cleanup completed with partial failures", {
|
|
96
|
+
user: username,
|
|
97
|
+
failureCount: failures.length,
|
|
98
|
+
});
|
|
99
|
+
throw new CleanupError(
|
|
100
|
+
`Cleanup of ${username} had ${failures.length} failures`,
|
|
101
|
+
failures,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
logger.info("Cleanup completed successfully", { user: username });
|
|
106
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { createUser, lockUser, killUserProcesses } from "./linux-user.js";
|
|
2
|
+
export { writeAuthorizedKey, removeAuthorizedKey } from "./authorized-keys.js";
|
|
3
|
+
export { addForceCommand, removeForceCommand } from "./ssh-config.js";
|
|
4
|
+
export {
|
|
5
|
+
sessionName,
|
|
6
|
+
createSession,
|
|
7
|
+
hasClientsAttached,
|
|
8
|
+
sessionExists,
|
|
9
|
+
displayMessage,
|
|
10
|
+
killSession,
|
|
11
|
+
getLastActivity,
|
|
12
|
+
} from "./tmux-session.js";
|
|
13
|
+
export { cleanupContractor, type CleanupTarget } from "./cleanup.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { runPrivileged } from "./privileged.js";
|
|
2
|
+
import { ProvisioningError } from "../utils/errors.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a new Linux user for a contractor.
|
|
7
|
+
* Creates home directory and .ssh directory.
|
|
8
|
+
*/
|
|
9
|
+
export async function createUser(username: string): Promise<void> {
|
|
10
|
+
try {
|
|
11
|
+
const u = shellEscape(username);
|
|
12
|
+
await runPrivileged(`useradd -m -s /bin/bash ${u}`);
|
|
13
|
+
await runPrivileged(
|
|
14
|
+
`mkdir -p /home/${u}/.ssh && ` +
|
|
15
|
+
`chmod 700 /home/${u}/.ssh && ` +
|
|
16
|
+
`chown ${u}:${u} /home/${u}/.ssh`,
|
|
17
|
+
);
|
|
18
|
+
logger.info("Linux user created", { user: username });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new ProvisioningError(
|
|
21
|
+
`Failed to create user ${username}: ${error}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lock a user account so they can no longer log in.
|
|
28
|
+
*/
|
|
29
|
+
export async function lockUser(username: string): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
await runPrivileged(`usermod -L ${shellEscape(username)}`);
|
|
32
|
+
logger.info("Linux user locked", { user: username });
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new ProvisioningError(
|
|
35
|
+
`Failed to lock user ${username}: ${error}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Kill all processes owned by a user.
|
|
42
|
+
* Does not throw if user has no processes.
|
|
43
|
+
*/
|
|
44
|
+
export async function killUserProcesses(username: string): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
await runPrivileged(`pkill -u ${shellEscape(username)}`);
|
|
47
|
+
logger.info("Killed processes for user", { user: username });
|
|
48
|
+
} catch {
|
|
49
|
+
// pkill returns non-zero if no processes found — that's fine
|
|
50
|
+
logger.debug("No processes to kill for user", { user: username });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Basic shell escaping to prevent injection.
|
|
57
|
+
* Only allows alphanumeric, hyphen, underscore.
|
|
58
|
+
*/
|
|
59
|
+
function shellEscape(value: string): string {
|
|
60
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
|
61
|
+
throw new ProvisioningError(
|
|
62
|
+
`Invalid value for shell command: ${value}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|