@behalfid/cli 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/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +159 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +67 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +25 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +71 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +45 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +33 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +32 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +150 -0
- package/dist/commands/passport.d.ts +2 -0
- package/dist/commands/passport.js +41 -0
- package/dist/commands/permissions.d.ts +2 -0
- package/dist/commands/permissions.js +74 -0
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +106 -0
- package/dist/commands/verify.d.ts +2 -0
- package/dist/commands/verify.js +37 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +68 -0
- package/dist/lib/client.d.ts +13 -0
- package/dist/lib/client.js +62 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.js +48 -0
- package/dist/lib/context-generator.d.ts +3 -0
- package/dist/lib/context-generator.js +70 -0
- package/dist/lib/mcp-server.d.ts +5 -0
- package/dist/lib/mcp-server.js +140 -0
- package/dist/lib/output.d.ts +10 -0
- package/dist/lib/output.js +48 -0
- package/dist/lib/passport-cache.d.ts +31 -0
- package/dist/lib/passport-cache.js +43 -0
- package/dist/lib/prompt.d.ts +3 -0
- package/dist/lib/prompt.js +53 -0
- package/package.json +33 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { apiRequest, resolveApiKey, resolveBaseUrl } from "../lib/client.js";
|
|
3
|
+
import { isJsonMode, printJson, printKv, printSuccess, runAction } from "../lib/output.js";
|
|
4
|
+
export function permissionsCommand() {
|
|
5
|
+
const cmd = new Command("permissions").description("manage agent permissions");
|
|
6
|
+
cmd
|
|
7
|
+
.command("create <agentId>")
|
|
8
|
+
.description("create a permission for an agent (requires agent API key)")
|
|
9
|
+
.requiredOption("-a, --action <action>", "action to permit (e.g. purchase, access_data, schedule)")
|
|
10
|
+
.option("-r, --resource <resource>", "resource or vendor (e.g. amazon.com, gmail.com)")
|
|
11
|
+
.option("-s, --scope <scope>", "plain-English scope description")
|
|
12
|
+
.option("-d, --description <desc>", "permission description")
|
|
13
|
+
.option("--template <template>", "template: purchase, access_data, create_content, schedule, custom")
|
|
14
|
+
.option("--max-amount <n>", "maximum transaction amount (for purchase permissions)")
|
|
15
|
+
.option("--expires <date>", "expiry date (ISO 8601, e.g. 2027-01-01)")
|
|
16
|
+
.option("--allowed <actions>", "comma-separated allowed actions")
|
|
17
|
+
.option("--blocked <actions>", "comma-separated blocked actions")
|
|
18
|
+
.option("--requires-approval", "require human approval before acting")
|
|
19
|
+
.option("-k, --api-key <key>", "agent API key (overrides config)")
|
|
20
|
+
.action(runAction(async (agentId, opts) => {
|
|
21
|
+
const apiKey = opts.apiKey ?? resolveApiKey();
|
|
22
|
+
if (!apiKey)
|
|
23
|
+
throw new Error("An agent API key is required. Set it with `behalf config set api-key <key>` or pass --api-key.");
|
|
24
|
+
const baseUrl = resolveBaseUrl();
|
|
25
|
+
const body = { agentId, action: opts.action };
|
|
26
|
+
if (opts.resource)
|
|
27
|
+
body.resource = opts.resource;
|
|
28
|
+
if (opts.scope)
|
|
29
|
+
body.scope = opts.scope;
|
|
30
|
+
if (opts.description)
|
|
31
|
+
body.description = opts.description;
|
|
32
|
+
if (opts.template)
|
|
33
|
+
body.template = opts.template;
|
|
34
|
+
if (opts.requiresApproval)
|
|
35
|
+
body.requiresApproval = true;
|
|
36
|
+
if (opts.allowed)
|
|
37
|
+
body.allowedActions = opts.allowed.split(",").map(s => s.trim()).filter(Boolean);
|
|
38
|
+
if (opts.blocked)
|
|
39
|
+
body.blockedActions = opts.blocked.split(",").map(s => s.trim()).filter(Boolean);
|
|
40
|
+
if (opts.maxAmount || opts.expires || opts.resource) {
|
|
41
|
+
const constraints = {};
|
|
42
|
+
if (opts.maxAmount)
|
|
43
|
+
constraints.maxAmount = Number(opts.maxAmount);
|
|
44
|
+
if (opts.expires)
|
|
45
|
+
constraints.expiresAt = new Date(opts.expires).toISOString();
|
|
46
|
+
if (opts.resource)
|
|
47
|
+
constraints.allowedVendors = [opts.resource];
|
|
48
|
+
body.constraints = constraints;
|
|
49
|
+
}
|
|
50
|
+
const data = await apiRequest("/api/permissions", {
|
|
51
|
+
method: "POST", body, apiKey, baseUrl,
|
|
52
|
+
});
|
|
53
|
+
if (isJsonMode())
|
|
54
|
+
printJson(data);
|
|
55
|
+
else
|
|
56
|
+
printKv({ permissionId: data.permissionId, status: data.status });
|
|
57
|
+
}));
|
|
58
|
+
cmd
|
|
59
|
+
.command("revoke <permissionId>")
|
|
60
|
+
.description("revoke a permission (requires agent API key)")
|
|
61
|
+
.option("-k, --api-key <key>", "agent API key (overrides config)")
|
|
62
|
+
.action(runAction(async (permissionId, opts) => {
|
|
63
|
+
const apiKey = opts.apiKey ?? resolveApiKey();
|
|
64
|
+
if (!apiKey)
|
|
65
|
+
throw new Error("An agent API key is required. Set it with `behalf config set api-key <key>` or pass --api-key.");
|
|
66
|
+
const baseUrl = resolveBaseUrl();
|
|
67
|
+
const data = await apiRequest(`/api/permissions/${encodeURIComponent(permissionId)}/revoke`, { method: "POST", apiKey, baseUrl });
|
|
68
|
+
if (isJsonMode())
|
|
69
|
+
printJson(data);
|
|
70
|
+
else
|
|
71
|
+
printSuccess(`Permission ${permissionId} revoked.`);
|
|
72
|
+
}));
|
|
73
|
+
return cmd;
|
|
74
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { resolveBaseUrl } from "../lib/client.js";
|
|
6
|
+
import { readConfig } from "../lib/config.js";
|
|
7
|
+
import { generateContextMd, generateMcpJson } from "../lib/context-generator.js";
|
|
8
|
+
import { fetchAndCacheDetail, readCachedDetail } from "../lib/passport-cache.js";
|
|
9
|
+
import { runAction } from "../lib/output.js";
|
|
10
|
+
import { mkdirSync } from "node:fs";
|
|
11
|
+
const TOOLS = {
|
|
12
|
+
claude: {
|
|
13
|
+
binary: "claude",
|
|
14
|
+
contextFiles: ["CLAUDE.md"],
|
|
15
|
+
injectLine: "@.behalf/context.md",
|
|
16
|
+
},
|
|
17
|
+
codex: {
|
|
18
|
+
binary: "codex",
|
|
19
|
+
contextFiles: ["AGENTS.md"],
|
|
20
|
+
injectLine: "@.behalf/context.md",
|
|
21
|
+
},
|
|
22
|
+
cursor: {
|
|
23
|
+
binary: "cursor",
|
|
24
|
+
contextFiles: [".cursorrules"],
|
|
25
|
+
injectLine: "@.behalf/context.md",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
async function launchTool(toolKey, extraArgs) {
|
|
29
|
+
const tool = TOOLS[toolKey];
|
|
30
|
+
if (!tool)
|
|
31
|
+
throw new Error(`Unknown tool "${toolKey}". Supported: ${Object.keys(TOOLS).join(", ")}`);
|
|
32
|
+
const config = readConfig();
|
|
33
|
+
const agentId = config.agentId ?? process.env.BEHALFID_AGENT_ID;
|
|
34
|
+
const baseUrl = resolveBaseUrl();
|
|
35
|
+
if (!agentId) {
|
|
36
|
+
throw new Error("Agent ID not configured.\nRun: behalf config set agent-id <agentId>");
|
|
37
|
+
}
|
|
38
|
+
const cwd = process.cwd();
|
|
39
|
+
const behalfDir = join(cwd, ".behalf");
|
|
40
|
+
const contextFile = join(behalfDir, "context.md");
|
|
41
|
+
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
42
|
+
// Fetch or refresh permissions
|
|
43
|
+
let detail = readCachedDetail(agentId);
|
|
44
|
+
if (!detail) {
|
|
45
|
+
process.stderr.write("Fetching BehalfID permissions… ");
|
|
46
|
+
try {
|
|
47
|
+
detail = await fetchAndCacheDetail(agentId, baseUrl, false);
|
|
48
|
+
process.stderr.write("done.\n");
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
process.stderr.write(`failed (${err instanceof Error ? err.message : String(err)}).\n`);
|
|
52
|
+
process.stderr.write("Continuing without live permissions. Run `behalf mcp init` to populate the cache.\n");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Write .behalf/context.md
|
|
56
|
+
if (detail) {
|
|
57
|
+
if (!existsSync(behalfDir))
|
|
58
|
+
mkdirSync(behalfDir, { recursive: true });
|
|
59
|
+
writeFileSync(contextFile, generateContextMd(detail));
|
|
60
|
+
}
|
|
61
|
+
// Write .mcp.json (merge with existing)
|
|
62
|
+
let existingMcp = null;
|
|
63
|
+
if (existsSync(mcpJsonPath)) {
|
|
64
|
+
try {
|
|
65
|
+
existingMcp = JSON.parse(readFileSync(mcpJsonPath, "utf-8"));
|
|
66
|
+
}
|
|
67
|
+
catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
writeFileSync(mcpJsonPath, generateMcpJson(existingMcp ?? undefined));
|
|
70
|
+
// Inject include into tool config files (idempotent)
|
|
71
|
+
for (const fileName of tool.contextFiles) {
|
|
72
|
+
const filePath = join(cwd, fileName);
|
|
73
|
+
if (!existsSync(filePath))
|
|
74
|
+
continue;
|
|
75
|
+
const content = readFileSync(filePath, "utf-8");
|
|
76
|
+
if (!content.includes(tool.injectLine)) {
|
|
77
|
+
writeFileSync(filePath, content + `\n\n${tool.injectLine}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Launch the tool
|
|
81
|
+
const result = spawnSync(tool.binary, extraArgs, { stdio: "inherit" });
|
|
82
|
+
process.exit(result.status ?? 0);
|
|
83
|
+
}
|
|
84
|
+
function toolCommand(toolKey, description) {
|
|
85
|
+
return new Command(toolKey)
|
|
86
|
+
.description(description)
|
|
87
|
+
.allowUnknownOption(true)
|
|
88
|
+
.passThroughOptions(true)
|
|
89
|
+
.argument("[args...]", `arguments to pass to ${toolKey}`)
|
|
90
|
+
.action(runAction(async (args) => {
|
|
91
|
+
await launchTool(toolKey, args);
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
export function runCommand() {
|
|
95
|
+
return new Command("run")
|
|
96
|
+
.description("launch an AI tool with BehalfID enforcement active")
|
|
97
|
+
.allowUnknownOption(true)
|
|
98
|
+
.passThroughOptions(true)
|
|
99
|
+
.argument("<tool>", `tool to launch: ${Object.keys(TOOLS).join(", ")}`)
|
|
100
|
+
.argument("[args...]", "arguments to pass through to the tool")
|
|
101
|
+
.action(runAction(async (toolKey, args) => {
|
|
102
|
+
await launchTool(toolKey, args);
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
export function claudeCommand() { return toolCommand("claude", "launch Claude Code with BehalfID enforcement"); }
|
|
106
|
+
export function codexCommand() { return toolCommand("codex", "launch Codex CLI with BehalfID enforcement"); }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { apiRequest, resolveApiKey, resolveBaseUrl } from "../lib/client.js";
|
|
3
|
+
import { isJsonMode, printJson, printKv, runAction } from "../lib/output.js";
|
|
4
|
+
export function verifyCommand() {
|
|
5
|
+
return new Command("verify")
|
|
6
|
+
.description("verify whether an agent may perform an action")
|
|
7
|
+
.argument("<agentId>", "agent ID")
|
|
8
|
+
.requiredOption("-a, --action <action>", "action to verify (e.g. purchase, access_data)")
|
|
9
|
+
.option("-v, --vendor <vendor>", "vendor or resource (e.g. amazon.com, gmail.com)")
|
|
10
|
+
.option("-r, --resource <resource>", "alias for --vendor")
|
|
11
|
+
.option("--amount <n>", "transaction amount (for purchase actions)")
|
|
12
|
+
.option("-k, --api-key <key>", "agent API key (overrides config)")
|
|
13
|
+
.action(runAction(async (agentId, opts) => {
|
|
14
|
+
const apiKey = opts.apiKey ?? resolveApiKey();
|
|
15
|
+
if (!apiKey)
|
|
16
|
+
throw new Error("An agent API key is required. Set it with `behalf config set api-key <key>` or pass --api-key.");
|
|
17
|
+
const baseUrl = resolveBaseUrl();
|
|
18
|
+
const body = { agentId, action: opts.action };
|
|
19
|
+
const vendor = opts.vendor ?? opts.resource;
|
|
20
|
+
if (vendor)
|
|
21
|
+
body.vendor = vendor;
|
|
22
|
+
if (opts.amount)
|
|
23
|
+
body.amount = Number(opts.amount);
|
|
24
|
+
const data = await apiRequest("/api/verify", {
|
|
25
|
+
method: "POST", body, apiKey, baseUrl,
|
|
26
|
+
});
|
|
27
|
+
if (isJsonMode()) {
|
|
28
|
+
printJson(data);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(`\n${data.allowed ? "✓ ALLOWED" : "✗ DENIED"}`);
|
|
32
|
+
printKv({ requestId: data.requestId, reason: data.reason, risk: data.risk });
|
|
33
|
+
console.log("");
|
|
34
|
+
if (!data.allowed)
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readConfig, readSession } from "../lib/config.js";
|
|
3
|
+
import { apiRequest, resolveBaseUrl } from "../lib/client.js";
|
|
4
|
+
import { isJsonMode, printJson, printKv, runAction } from "../lib/output.js";
|
|
5
|
+
export function whoamiCommand() {
|
|
6
|
+
return new Command("whoami")
|
|
7
|
+
.description("show current authentication status")
|
|
8
|
+
.action(runAction(async function () {
|
|
9
|
+
const config = readConfig();
|
|
10
|
+
const session = readSession();
|
|
11
|
+
const baseUrl = resolveBaseUrl();
|
|
12
|
+
let user = null;
|
|
13
|
+
if (session) {
|
|
14
|
+
try {
|
|
15
|
+
const data = await apiRequest("/api/auth/me", { baseUrl });
|
|
16
|
+
user = data.user;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// session may be expired
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (isJsonMode()) {
|
|
23
|
+
printJson({
|
|
24
|
+
loggedIn: !!user,
|
|
25
|
+
email: user?.email ?? null,
|
|
26
|
+
userId: user?.userId ?? null,
|
|
27
|
+
apiKey: config.apiKey ? `${config.apiKey.slice(0, 15)}…` : null,
|
|
28
|
+
baseUrl: config.baseUrl ?? null,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (user) {
|
|
33
|
+
printKv({
|
|
34
|
+
email: user.email,
|
|
35
|
+
userId: user.userId,
|
|
36
|
+
"api key": config.apiKey ? `${config.apiKey.slice(0, 15)}…` : "(not set)",
|
|
37
|
+
"base url": baseUrl,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log("Not logged in.");
|
|
42
|
+
if (config.apiKey) {
|
|
43
|
+
console.log(`API key: ${config.apiKey.slice(0, 15)}…`);
|
|
44
|
+
}
|
|
45
|
+
console.log(`Base URL: ${baseUrl}`);
|
|
46
|
+
console.log('\nRun `behalf login` to authenticate.');
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { setJsonMode } from "./lib/output.js";
|
|
7
|
+
import { configCommand } from "./commands/config.js";
|
|
8
|
+
import { initCommand } from "./commands/init.js";
|
|
9
|
+
import { loginCommand } from "./commands/login.js";
|
|
10
|
+
import { logoutCommand } from "./commands/logout.js";
|
|
11
|
+
import { whoamiCommand } from "./commands/whoami.js";
|
|
12
|
+
import { agentsCommand } from "./commands/agents.js";
|
|
13
|
+
import { permissionsCommand } from "./commands/permissions.js";
|
|
14
|
+
import { verifyCommand } from "./commands/verify.js";
|
|
15
|
+
import { logsCommand } from "./commands/logs.js";
|
|
16
|
+
import { passportCommand } from "./commands/passport.js";
|
|
17
|
+
import { healthCommand } from "./commands/health.js";
|
|
18
|
+
import { mcpCommand } from "./commands/mcp.js";
|
|
19
|
+
import { runCommand, claudeCommand, codexCommand } from "./commands/run.js";
|
|
20
|
+
const rawArgs = process.argv.slice(2);
|
|
21
|
+
const jsonMode = rawArgs.includes("--json");
|
|
22
|
+
if (jsonMode)
|
|
23
|
+
setJsonMode(true);
|
|
24
|
+
const filteredArgs = rawArgs.filter(a => a !== "--json");
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = dirname(__filename);
|
|
27
|
+
const { version } = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
28
|
+
const program = new Command();
|
|
29
|
+
program
|
|
30
|
+
.name("behalf")
|
|
31
|
+
.description("Official CLI for BehalfID — agent permission management and enforcement")
|
|
32
|
+
.version(version)
|
|
33
|
+
.addHelpText("after", `
|
|
34
|
+
Examples:
|
|
35
|
+
behalf init interactive setup wizard
|
|
36
|
+
behalf login log in to your account
|
|
37
|
+
behalf agents create --name "My Bot" --save create agent and save credentials
|
|
38
|
+
behalf mcp init set up BehalfID enforcement in this directory
|
|
39
|
+
behalf claude launch Claude Code with enforcement active
|
|
40
|
+
behalf verify agent_xxx --action purchase -v amazon.com --amount 25
|
|
41
|
+
behalf permissions create agent_xxx --action purchase -r amazon.com --max-amount 50
|
|
42
|
+
behalf logs agent_xxx view recent verification logs
|
|
43
|
+
`);
|
|
44
|
+
program.enablePositionalOptions();
|
|
45
|
+
program.addCommand(initCommand());
|
|
46
|
+
program.addCommand(configCommand());
|
|
47
|
+
program.addCommand(loginCommand());
|
|
48
|
+
program.addCommand(logoutCommand());
|
|
49
|
+
program.addCommand(whoamiCommand());
|
|
50
|
+
program.addCommand(agentsCommand());
|
|
51
|
+
program.addCommand(permissionsCommand());
|
|
52
|
+
program.addCommand(verifyCommand());
|
|
53
|
+
program.addCommand(logsCommand());
|
|
54
|
+
program.addCommand(passportCommand());
|
|
55
|
+
program.addCommand(healthCommand());
|
|
56
|
+
program.addCommand(mcpCommand());
|
|
57
|
+
program.addCommand(runCommand());
|
|
58
|
+
program.addCommand(claudeCommand());
|
|
59
|
+
program.addCommand(codexCommand());
|
|
60
|
+
program.parseAsync(["", "", ...filteredArgs]).catch(err => {
|
|
61
|
+
if (jsonMode) {
|
|
62
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.error(`Error: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DEFAULT_BASE_URL = "https://behalfid.com";
|
|
2
|
+
export type RequestOptions = {
|
|
3
|
+
method?: "GET" | "POST" | "PATCH" | "DELETE";
|
|
4
|
+
body?: unknown;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
skipAuth?: boolean;
|
|
8
|
+
onHeaders?: (headers: Headers) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare function resolveBaseUrl(override?: string): string;
|
|
11
|
+
export declare function resolveApiKey(override?: string): string | undefined;
|
|
12
|
+
export declare function originOf(baseUrl: string): string;
|
|
13
|
+
export declare function apiRequest<T = unknown>(path: string, opts?: RequestOptions): Promise<T>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readConfig, readSession } from "./config.js";
|
|
2
|
+
export const DEFAULT_BASE_URL = "https://behalfid.com";
|
|
3
|
+
export function resolveBaseUrl(override) {
|
|
4
|
+
const config = readConfig();
|
|
5
|
+
return (override ??
|
|
6
|
+
process.env.BEHALFID_BASE_URL ??
|
|
7
|
+
config.baseUrl ??
|
|
8
|
+
DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
9
|
+
}
|
|
10
|
+
export function resolveApiKey(override) {
|
|
11
|
+
const config = readConfig();
|
|
12
|
+
return override ?? process.env.BEHALFID_API_KEY ?? config.apiKey;
|
|
13
|
+
}
|
|
14
|
+
export function originOf(baseUrl) {
|
|
15
|
+
try {
|
|
16
|
+
return new URL(baseUrl).origin;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return baseUrl;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function apiRequest(path, opts = {}) {
|
|
23
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
24
|
+
const apiKey = opts.skipAuth ? undefined : opts.apiKey ?? resolveApiKey();
|
|
25
|
+
const session = opts.skipAuth ? null : readSession();
|
|
26
|
+
const headers = {
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
Origin: originOf(baseUrl),
|
|
29
|
+
};
|
|
30
|
+
if (apiKey)
|
|
31
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
32
|
+
if (session)
|
|
33
|
+
headers.Cookie = session;
|
|
34
|
+
if (opts.body !== undefined)
|
|
35
|
+
headers["Content-Type"] = "application/json";
|
|
36
|
+
let response;
|
|
37
|
+
try {
|
|
38
|
+
response = await fetch(`${baseUrl}${path}`, {
|
|
39
|
+
method: opts.method ?? "GET",
|
|
40
|
+
headers,
|
|
41
|
+
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error("Network request failed. Check your connection and base URL.");
|
|
46
|
+
}
|
|
47
|
+
if (opts.onHeaders)
|
|
48
|
+
opts.onHeaders(response.headers);
|
|
49
|
+
const body = await response.json().catch(() => null);
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const message = typeof body === "object" &&
|
|
52
|
+
body !== null &&
|
|
53
|
+
"error" in body &&
|
|
54
|
+
typeof body.error === "string"
|
|
55
|
+
? body.error
|
|
56
|
+
: `Request failed with status ${response.status}.`;
|
|
57
|
+
throw new Error(message);
|
|
58
|
+
}
|
|
59
|
+
if (body === null)
|
|
60
|
+
throw new Error("Expected JSON response.");
|
|
61
|
+
return body;
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Config = {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
agentId?: string;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function readConfig(): Config;
|
|
7
|
+
export declare function writeConfig(config: Config): void;
|
|
8
|
+
export declare function patchConfig(patch: Partial<Config>): void;
|
|
9
|
+
export declare function readSession(): string | null;
|
|
10
|
+
export declare function writeSession(cookie: string): void;
|
|
11
|
+
export declare function clearSession(): void;
|
|
12
|
+
export declare const CONFIG_FILE_PATH: string;
|
|
13
|
+
export declare const CONFIG_DIR_PATH: string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".behalf");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
|
+
const SESSION_FILE = join(CONFIG_DIR, "session");
|
|
7
|
+
function ensureDir() {
|
|
8
|
+
if (!existsSync(CONFIG_DIR))
|
|
9
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
export function readConfig() {
|
|
12
|
+
if (!existsSync(CONFIG_FILE))
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function writeConfig(config) {
|
|
22
|
+
ensureDir();
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
24
|
+
}
|
|
25
|
+
export function patchConfig(patch) {
|
|
26
|
+
writeConfig({ ...readConfig(), ...patch });
|
|
27
|
+
}
|
|
28
|
+
export function readSession() {
|
|
29
|
+
if (!existsSync(SESSION_FILE))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const val = readFileSync(SESSION_FILE, "utf-8").trim();
|
|
33
|
+
return val || null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function writeSession(cookie) {
|
|
40
|
+
ensureDir();
|
|
41
|
+
writeFileSync(SESSION_FILE, cookie, { mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
export function clearSession() {
|
|
44
|
+
if (existsSync(SESSION_FILE))
|
|
45
|
+
writeFileSync(SESSION_FILE, "", { mode: 0o600 });
|
|
46
|
+
}
|
|
47
|
+
export const CONFIG_FILE_PATH = CONFIG_FILE;
|
|
48
|
+
export const CONFIG_DIR_PATH = CONFIG_DIR;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function formatConstraints(p) {
|
|
2
|
+
const lines = [];
|
|
3
|
+
if (p.constraints?.maxAmount)
|
|
4
|
+
lines.push(`max $${p.constraints.maxAmount}`);
|
|
5
|
+
if (p.constraints?.allowedVendors?.length)
|
|
6
|
+
lines.push(`vendors: ${p.constraints.allowedVendors.join(", ")}`);
|
|
7
|
+
if (p.constraints?.expiresAt)
|
|
8
|
+
lines.push(`expires: ${p.constraints.expiresAt.slice(0, 10)}`);
|
|
9
|
+
return lines;
|
|
10
|
+
}
|
|
11
|
+
export function generateContextMd(detail, updatedAt) {
|
|
12
|
+
const { agent, permissions } = detail;
|
|
13
|
+
const active = permissions.filter(p => p.status === "active");
|
|
14
|
+
const timestamp = updatedAt ?? new Date().toISOString().slice(0, 16).replace("T", " ");
|
|
15
|
+
const lines = [
|
|
16
|
+
`<!-- BehalfID Agent Context — auto-generated by \`behalf mcp init\` -->`,
|
|
17
|
+
`<!-- Run \`behalf mcp init --refresh\` to update -->`,
|
|
18
|
+
``,
|
|
19
|
+
`## BehalfID Agent: ${agent.name}`,
|
|
20
|
+
``,
|
|
21
|
+
`You are operating as BehalfID agent **\`${agent.agentId}\`** ("${agent.name}").`,
|
|
22
|
+
``,
|
|
23
|
+
`The \`behalfid\` MCP server is active in this session.`,
|
|
24
|
+
``,
|
|
25
|
+
`**ENFORCEMENT RULE: Before executing any action that touches an external service,`,
|
|
26
|
+
`you MUST call the \`verify_action\` tool from the \`behalfid\` MCP server.**`,
|
|
27
|
+
`If \`verify_action\` returns \`"allowed": false\`, stop immediately and tell the user`,
|
|
28
|
+
`why the action was blocked. Never bypass or skip this check.`,
|
|
29
|
+
``,
|
|
30
|
+
];
|
|
31
|
+
if (active.length === 0) {
|
|
32
|
+
lines.push(`### Permissions`, ``, `No active permissions. All external actions will be denied.`, ``);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
lines.push(`### Active Permissions`, ``);
|
|
36
|
+
for (const p of active) {
|
|
37
|
+
const constraints = formatConstraints(p);
|
|
38
|
+
const constraintStr = constraints.length ? ` (${constraints.join(", ")})` : "";
|
|
39
|
+
lines.push(`- **${p.action}**${p.resource ? ` → \`${p.resource}\`` : ""}${constraintStr}${p.scope ? `: ${p.scope}` : ""}`);
|
|
40
|
+
if (p.allowedActions?.length) {
|
|
41
|
+
lines.push(` - Allowed: ${p.allowedActions.map(a => `"${a}"`).join(", ")}`);
|
|
42
|
+
}
|
|
43
|
+
if (p.blockedActions?.length) {
|
|
44
|
+
lines.push(` - Blocked: ${p.blockedActions.map(a => `"${a}"`).join(", ")}`);
|
|
45
|
+
}
|
|
46
|
+
if (p.requiresApproval) {
|
|
47
|
+
lines.push(` - ⚠ Requires human approval before proceeding`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
lines.push(``);
|
|
51
|
+
}
|
|
52
|
+
lines.push(`### How to call \`verify_action\``, ``, `\`\`\``, `verify_action(action: string, vendor?: string, amount?: number)`, `// action: "purchase" | "access_data" | "browse_web" | "schedule" | "create_content" | ...`, `// vendor: the service or domain being accessed (e.g. "amazon.com", "gmail.com")`, `// amount: only for purchase actions`, `\`\`\``, ``, `---`, `*Updated ${timestamp} · Agent \`${agent.agentId}\` · [BehalfID](https://behalfid.com)*`);
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
export function generateMcpJson(existing) {
|
|
56
|
+
const base = existing ?? {};
|
|
57
|
+
const servers = base.mcpServers ?? {};
|
|
58
|
+
const updated = {
|
|
59
|
+
...base,
|
|
60
|
+
mcpServers: {
|
|
61
|
+
...servers,
|
|
62
|
+
behalfid: {
|
|
63
|
+
type: "stdio",
|
|
64
|
+
command: "behalf",
|
|
65
|
+
args: ["mcp", "start"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
return JSON.stringify(updated, null, 2) + "\n";
|
|
70
|
+
}
|