@macaw-ai/client 0.0.0-stage → 0.9.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/LICENSE.md +339 -0
- package/bin/install.js +422 -0
- package/package.json +37 -4
- package/payload/README.md +56 -0
- package/payload/secureAI/LICENSE +190 -0
- package/payload/secureAI/NOTICE +5 -0
- package/payload/secureAI/demos/policy-controls/1_uncontrolled.py +139 -0
- package/payload/secureAI/demos/policy-controls/2_with_macaw.py +139 -0
- package/payload/secureAI/demos/policy-controls/3_with_user_policies.py +162 -0
- package/payload/secureAI/demos/policy-controls/credentials.txt +12 -0
- package/payload/secureAI/demos/policy-controls/malicious_report.txt +21 -0
- package/payload/secureAI/demos/policy-controls/sales_report.txt +20 -0
- package/payload/secureAI/demos/tutorial-1-rbac/README.md +143 -0
- package/payload/secureAI/demos/tutorial-1-rbac/app.py +147 -0
- package/payload/secureAI/demos/tutorial-1-rbac/authprompts_demo.py +306 -0
- package/payload/secureAI/demos/tutorial-1-rbac/config/claims-config.yaml +152 -0
- package/payload/secureAI/demos/tutorial-1-rbac/demo_secureopenai_with_policies.py +150 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/bu_Analytics.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/company_FinTech_Corp.json +70 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/llm-base.json +69 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/team_Reporting.json +8 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_alice.json +32 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_bob.json +28 -0
- package/payload/secureAI/demos/tutorial-1-rbac/policies/user_carol.json +34 -0
- package/payload/secureAI/demos/tutorial-1-rbac/requirements.txt +6 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/auth0_complete_setup.sh +392 -0
- package/payload/secureAI/demos/tutorial-1-rbac/setup/keycloak_complete_setup.sh +320 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/alation-remote-proxy.json +155 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/bu.json +22 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/company.json +17 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_alice.json +48 -0
- package/payload/secureAI/demos/tutorial-2-intent/Policies/user_bob.json +83 -0
- package/payload/secureAI/demos/tutorial-2-intent/README.md +213 -0
- package/payload/secureAI/demos/tutorial-2-intent/script.py +188 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/alation_verifier.py +177 -0
- package/payload/secureAI/demos/tutorial-2-intent/setup/get_alation_user_token.py +180 -0
- package/payload/secureAI/examples/README.md +244 -0
- package/payload/secureAI/examples/anthropic/anthropic_1a_dropin_simple.py +103 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1b_multiuser_bind_streaming.py +190 -0
- package/payload/secureAI/examples/anthropic/anthropic_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/attestations/1a_trade_alice.py +215 -0
- package/payload/secureAI/examples/attestations/1a_trade_bob.py +164 -0
- package/payload/secureAI/examples/attestations/1b_grant_admin.py +170 -0
- package/payload/secureAI/examples/attestations/1b_grant_researcher.py +231 -0
- package/payload/secureAI/examples/attestations/README.md +128 -0
- package/payload/secureAI/examples/langchain/langchain_1a_dropin_simple.py +199 -0
- package/payload/secureAI/examples/langchain/langchain_1b_multiuser.py +262 -0
- package/payload/secureAI/examples/langchain/langchain_1c_orchestration.py +332 -0
- package/payload/secureAI/examples/langchain/langchain_1d_llm_openai.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1e_llm_anthropic.py +130 -0
- package/payload/secureAI/examples/langchain/langchain_1f_memory.py +214 -0
- package/payload/secureAI/examples/litellm/litellm_1a_dropin_simple.py +131 -0
- package/payload/secureAI/examples/mcp/1a_simple_invocation.py +115 -0
- package/payload/secureAI/examples/mcp/1b_discovery_and_resources.py +142 -0
- package/payload/secureAI/examples/mcp/1c_logging_client.py +100 -0
- package/payload/secureAI/examples/mcp/1d_progress_client.py +100 -0
- package/payload/secureAI/examples/mcp/1e_sampling_client.py +150 -0
- package/payload/secureAI/examples/mcp/1e_sampling_server.py +90 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_client.py +127 -0
- package/payload/secureAI/examples/mcp/1f_elicitation_server.py +73 -0
- package/payload/secureAI/examples/mcp/1g_roots_client.py +122 -0
- package/payload/secureAI/examples/mcp/1g_roots_server.py +85 -0
- package/payload/secureAI/examples/mcp/2a_proxy_snowflake.py +212 -0
- package/payload/secureAI/examples/mcp/2b_proxy_databricks.py +299 -0
- package/payload/secureAI/examples/mcp/2c_proxy_stdio_fetch.py +91 -0
- package/payload/secureAI/examples/mcp/2d_proxy_stdio_salesforce.py +119 -0
- package/payload/secureAI/examples/mcp/2e_proxy_multiuser.py +136 -0
- package/payload/secureAI/examples/mcp/2f_proxy_stdio_databricks.py +144 -0
- package/payload/secureAI/examples/mcp/3a_native_mcp_client.py +104 -0
- package/payload/secureAI/examples/mcp/3b_native_mcp_proxy.py +114 -0
- package/payload/secureAI/examples/mcp/3c_native_mcp_sampling.py +180 -0
- package/payload/secureAI/examples/mcp/3d_native_mcp_2026.py +135 -0
- package/payload/secureAI/examples/mcp/securemcp_calculator.py +248 -0
- package/payload/secureAI/examples/mcp/securemcp_server.py +75 -0
- package/payload/secureAI/examples/openai/openai_1a_dropin_simple.py +102 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind.py +193 -0
- package/payload/secureAI/examples/openai/openai_1b_multiuser_bind_streaming.py +199 -0
- package/payload/secureAI/examples/openai/openai_1c_a2a_invoke.py +156 -0
- package/payload/secureAI/examples/openai/openai_1d_invocation_patterns.py +337 -0
- package/payload/secureAI/examples/phishing-resistance/1a_login_alice.py +180 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_alice.py +241 -0
- package/payload/secureAI/examples/phishing-resistance/1b_trade_bob.py +164 -0
- package/payload/secureAI/examples/phishing-resistance/README.md +124 -0
- package/payload/secureAI/examples/phishing-resistance/get_token.py +114 -0
- package/payload/secureAI/examples/pydantic_ai/README.md +88 -0
- package/payload/secureAI/examples/pydantic_ai/policies/app_pydantic-agent.json +10 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1a_dropin_simple.py +90 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1b_model_routing.py +85 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1c_native_tools.py +83 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1d_multiuser_bind.py +104 -0
- package/payload/secureAI/examples/pydantic_ai/pydanticai_1e_mcp_compose.py +92 -0
- package/payload/secureAI/macaw_adapters/__init__.py +55 -0
- package/payload/secureAI/macaw_adapters/anthropic/REFERENCE.md +374 -0
- package/payload/secureAI/macaw_adapters/anthropic/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/anthropic/secure_anthropic.py +792 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/anthropic_ext_tools/secure_anthropic.py +840 -0
- package/payload/secureAI/macaw_adapters/langchain/REFERENCE.md +234 -0
- package/payload/secureAI/macaw_adapters/langchain/__init__.py +100 -0
- package/payload/secureAI/macaw_adapters/langchain/_utils.py +83 -0
- package/payload/secureAI/macaw_adapters/langchain/agents.py +296 -0
- package/payload/secureAI/macaw_adapters/langchain/anthropic.py +456 -0
- package/payload/secureAI/macaw_adapters/langchain/callbacks.py +306 -0
- package/payload/secureAI/macaw_adapters/langchain/memory.py +352 -0
- package/payload/secureAI/macaw_adapters/langchain/openai.py +410 -0
- package/payload/secureAI/macaw_adapters/langchain/tools.py +174 -0
- package/payload/secureAI/macaw_adapters/litellm/README.md +216 -0
- package/payload/secureAI/macaw_adapters/litellm/__init__.py +143 -0
- package/payload/secureAI/macaw_adapters/litellm/secure_litellm.py +686 -0
- package/payload/secureAI/macaw_adapters/mcp/REFERENCE.md +460 -0
- package/payload/secureAI/macaw_adapters/mcp/__init__.py +65 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint.py +193 -0
- package/payload/secureAI/macaw_adapters/mcp/_endpoint2.py +225 -0
- package/payload/secureAI/macaw_adapters/mcp/_serve.py +25 -0
- package/payload/secureAI/macaw_adapters/mcp/client.py +721 -0
- package/payload/secureAI/macaw_adapters/mcp/decorators.py +228 -0
- package/payload/secureAI/macaw_adapters/mcp/fastmcp_compat.py +353 -0
- package/payload/secureAI/macaw_adapters/mcp/mcp.py +785 -0
- package/payload/secureAI/macaw_adapters/mcp/proxy.py +626 -0
- package/payload/secureAI/macaw_adapters/mcp/server.py +317 -0
- package/payload/secureAI/macaw_adapters/mcp/subscriptions.py +450 -0
- package/payload/secureAI/macaw_adapters/openai/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai/__init__.py +22 -0
- package/payload/secureAI/macaw_adapters/openai/secure_openai.py +833 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/REFERENCE.md +368 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/__init__.py +25 -0
- package/payload/secureAI/macaw_adapters/openai_ext_tools/secure_openai.py +876 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/__init__.py +36 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/_core.py +257 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/agent.py +218 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/model.py +99 -0
- package/payload/secureAI/macaw_adapters/pydantic_ai/toolset.py +86 -0
- package/payload/secureAI/test_harness_quick.py +296 -0
- package/wheels/macaw_adapters-0.9.10.1-py3-none-any.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp312-cp312-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-macosx_11_0_arm64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-manylinux_2_17_x86_64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp314-cp314-win_amd64.whl +0 -0
- package/wheels/macaw_client-0.9.10.1-cp39-cp39-macosx_11_0_arm64.whl +0 -0
- package/README.md +0 -3
package/bin/install.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Set up MACAW for a Python project: pick an interpreter we have a wheel for,
|
|
3
|
+
// install the client and the adapters into it, get a key, and leave the
|
|
4
|
+
// examples somewhere the user can run them.
|
|
5
|
+
//
|
|
6
|
+
// Everything lands in one directory the user chooses, config included, so a
|
|
7
|
+
// second install elsewhere is a second workspace rather than a collision.
|
|
8
|
+
// That is also why MACAW_HOME is mentioned at the end: the client searches
|
|
9
|
+
// $MACAW_HOME, then the current directory, then home - so from inside the
|
|
10
|
+
// install directory it already works, and the variable is only for running
|
|
11
|
+
// from somewhere else.
|
|
12
|
+
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { createInterface } from "node:readline/promises";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
|
|
20
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const PKG = path.join(HERE, "..");
|
|
22
|
+
const API = process.env.MACAW_API_URL || "https://api.macawsecurity.ai";
|
|
23
|
+
|
|
24
|
+
const C = process.stdout.isTTY
|
|
25
|
+
? { dim: "\x1b[2m", b: "\x1b[1m", g: "\x1b[32m", y: "\x1b[33m", r: "\x1b[31m", c: "\x1b[36m", x: "\x1b[0m" }
|
|
26
|
+
: { dim: "", b: "", g: "", y: "", r: "", c: "", x: "" };
|
|
27
|
+
|
|
28
|
+
const ok = (s) => console.log(` ${C.g}✓${C.x} ${s}`);
|
|
29
|
+
const no = (s) => console.log(` ${C.r}✗${C.x} ${s}`);
|
|
30
|
+
const info = (s) => console.log(` ${C.c}→${C.x} ${s}`);
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------- wheels ---
|
|
33
|
+
|
|
34
|
+
// The matrix is whatever shipped in this package, so the installer never
|
|
35
|
+
// offers a Python we stopped building or misses one we added.
|
|
36
|
+
function bundledWheels() {
|
|
37
|
+
const dir = path.join(PKG, "wheels");
|
|
38
|
+
if (!fs.existsSync(dir)) return [];
|
|
39
|
+
return fs.readdirSync(dir)
|
|
40
|
+
.filter((f) => f.startsWith("macaw_client-") && f.endsWith(".whl"))
|
|
41
|
+
.map((f) => {
|
|
42
|
+
const m = f.match(/-(cp\d+)-\1-(.+)\.whl$/);
|
|
43
|
+
return m ? { file: path.join(dir, f), tag: m[1], platform: m[2] } : null;
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function platformTag() {
|
|
49
|
+
if (process.platform === "darwin") return process.arch === "arm64" ? "macosx_11_0_arm64" : "macosx_10_9_x86_64";
|
|
50
|
+
if (process.platform === "linux") return "manylinux_2_17_x86_64";
|
|
51
|
+
if (process.platform === "win32") return "win_amd64";
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// cp314 -> "3.14", for both display and the interpreter name we look for.
|
|
56
|
+
const version = (tag) => `${tag.slice(2, 3)}.${tag.slice(3)}`;
|
|
57
|
+
|
|
58
|
+
// Newest first. Not parseFloat: that reads 3.9 as larger than 3.14 and would
|
|
59
|
+
// hand a machine with both the older interpreter.
|
|
60
|
+
const byVersionDesc = (a, b) => {
|
|
61
|
+
const [am, an] = a.split(".").map(Number);
|
|
62
|
+
const [bm, bn] = b.split(".").map(Number);
|
|
63
|
+
return bm - am || bn - an;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------- python ---
|
|
67
|
+
|
|
68
|
+
// An interpreter is a command plus leading arguments, because the reliable way
|
|
69
|
+
// to name a version on Windows is the launcher: `py -3.14`. Everything that
|
|
70
|
+
// runs Python here goes through this shape.
|
|
71
|
+
const interp = (cmd, ...pre) => ({ cmd, pre, display: [cmd, ...pre].join(" ") });
|
|
72
|
+
|
|
73
|
+
function pythonVersionOf(py) {
|
|
74
|
+
const r = spawnSync(py.cmd, [...py.pre, "-c", "import sys;print('%d.%d' % sys.version_info[:2])"],
|
|
75
|
+
{ encoding: "utf8" });
|
|
76
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// How to ask for a specific version, per platform. Unix names the binary;
|
|
80
|
+
// Windows has no python3.14 on PATH and uses the launcher instead, with bare
|
|
81
|
+
// `python` as the fallback - `python3` there is often a Store alias that is
|
|
82
|
+
// absent or stubbed.
|
|
83
|
+
function candidatesFor(v) {
|
|
84
|
+
const minor = v.split(".")[1];
|
|
85
|
+
return process.platform === "win32"
|
|
86
|
+
? [interp("py", `-${v}`), interp(`python${v}`), interp("python")]
|
|
87
|
+
: [interp(`python${v}`), interp(`python3.${minor}`)];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const defaultInterp = () => (process.platform === "win32" ? interp("python") : interp("python3"));
|
|
91
|
+
|
|
92
|
+
// Newest first, so a machine with several gets the one we would rather it use.
|
|
93
|
+
//
|
|
94
|
+
// MACAW_PYTHON names an interpreter outright, arguments included, so
|
|
95
|
+
// MACAW_PYTHON="py -3.12" works. That is the way out of the mismatch this
|
|
96
|
+
// preference can cause: on a machine where the default is 3.12 and 3.14 also
|
|
97
|
+
// exists, the library lands where `python3 foo.py` cannot import it.
|
|
98
|
+
function findPython(wheels) {
|
|
99
|
+
const wanted = [...new Set(wheels.map((w) => version(w.tag)))].sort(byVersionDesc);
|
|
100
|
+
|
|
101
|
+
const named = process.env.MACAW_PYTHON;
|
|
102
|
+
if (named) {
|
|
103
|
+
const py = interp(...named.trim().split(/\s+/));
|
|
104
|
+
const v = pythonVersionOf(py);
|
|
105
|
+
if (!v) return { error: `MACAW_PYTHON=${named} will not run.` };
|
|
106
|
+
if (!wanted.includes(v)) {
|
|
107
|
+
return { error: `MACAW_PYTHON=${named} is Python ${v}. We ship ${wanted.join(", ")}.` };
|
|
108
|
+
}
|
|
109
|
+
return { ...py, version: v };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const v of wanted) {
|
|
113
|
+
for (const py of candidatesFor(v)) {
|
|
114
|
+
if (pythonVersionOf(py) === v) return { ...py, version: v };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Whatever the bare interpreter is may still be one we ship for.
|
|
118
|
+
const d = defaultInterp();
|
|
119
|
+
const v = pythonVersionOf(d);
|
|
120
|
+
if (v && wanted.includes(v)) return { ...d, version: v };
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --------------------------------------------------------------- the pip ---
|
|
125
|
+
|
|
126
|
+
// This is a client to a service the whole machine talks to, so it should be
|
|
127
|
+
// importable from anywhere, not pinned inside one project. No venv is created:
|
|
128
|
+
// one the user did not ask for is worse than none, and it would put the library
|
|
129
|
+
// where their own code cannot see it.
|
|
130
|
+
//
|
|
131
|
+
// Homebrew and Debian mark their interpreters externally managed and refuse a
|
|
132
|
+
// plain install. Their own message says what to do - --break-system-packages,
|
|
133
|
+
// and "STRONGLY recommend that you additionally pass --user". The pair is the
|
|
134
|
+
// point: --user writes to ~/.local and never into the managed prefix, which is
|
|
135
|
+
// the thing the error exists to protect.
|
|
136
|
+
function pipInstall(py, files) {
|
|
137
|
+
const base = ["-m", "pip", "install", "--disable-pip-version-check", "-q"];
|
|
138
|
+
const run = (args) =>
|
|
139
|
+
spawnSync(py.cmd, [...py.pre, ...base, ...args, ...files],
|
|
140
|
+
{ stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
141
|
+
|
|
142
|
+
const inVenv = spawnSync(py.cmd, [...py.pre, "-c", "import sys;print(sys.prefix!=sys.base_prefix)"],
|
|
143
|
+
{ encoding: "utf8" }).stdout?.trim() === "True";
|
|
144
|
+
|
|
145
|
+
// Windows has no externally-managed interpreters and no ~/.local; --user
|
|
146
|
+
// there writes to %APPDATA%\Python, which is what we want and is the default
|
|
147
|
+
// behaviour, so the wording follows the platform.
|
|
148
|
+
const userDir = process.platform === "win32" ? "your user site-packages" : "~/.local";
|
|
149
|
+
|
|
150
|
+
let r = run(inVenv ? [] : ["--user"]);
|
|
151
|
+
if (r.status === 0) return { ok: true, where: inVenv ? "the active virtualenv" : userDir };
|
|
152
|
+
|
|
153
|
+
if (/externally-managed-environment/i.test(r.stderr || "")) {
|
|
154
|
+
info(`this Python is externally managed - installing to ${userDir}, which leaves it untouched`);
|
|
155
|
+
r = run(["--user", "--break-system-packages"]);
|
|
156
|
+
if (r.status === 0) return { ok: true, where: userDir };
|
|
157
|
+
}
|
|
158
|
+
return { ok: false, err: (r.stderr || r.stdout || "").trim() };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ------------------------------------------------------------------ key ---
|
|
162
|
+
|
|
163
|
+
// Read from four places, write to one. The last two are read and never
|
|
164
|
+
// written: a key already on the machine joins that workspace instead of
|
|
165
|
+
// minting a second, and a config kept in ~/.macaw by hand stays as it is.
|
|
166
|
+
//
|
|
167
|
+
// The current directory is in the list because the coding-agents installer
|
|
168
|
+
// writes $PWD/.macaw/config.json, so `npx macaw-ai install` choosing both
|
|
169
|
+
// surfaces activates once rather than twice.
|
|
170
|
+
function existingKey(dir) {
|
|
171
|
+
// --activate asks for a new workspace outright. Without it the only way to
|
|
172
|
+
// re-key was to delete this install's config, which the next run refills
|
|
173
|
+
// from $HOME or the current directory - so the instruction looped and anyone
|
|
174
|
+
// who had ever used MACAW could not reach activation. Nothing is deleted
|
|
175
|
+
// here; the existing files are simply not read.
|
|
176
|
+
if (process.argv.includes("--activate")) return null;
|
|
177
|
+
|
|
178
|
+
// Handed to us rather than discovered, so there is nothing to confirm. This
|
|
179
|
+
// is how `macaw-ai install` choosing both surfaces passes the key the first
|
|
180
|
+
// installer just created, instead of asking for it a second time.
|
|
181
|
+
const fromEnv = process.env.MACAW_API_KEY;
|
|
182
|
+
if (fromEnv) {
|
|
183
|
+
return {
|
|
184
|
+
key: fromEnv.trim(),
|
|
185
|
+
from: "MACAW_API_KEY",
|
|
186
|
+
workspace: process.env.MACAW_WORKSPACE || "",
|
|
187
|
+
explicit: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const places = [
|
|
192
|
+
process.env.MACAW_HOME && path.join(process.env.MACAW_HOME, ".macaw", "config.json"),
|
|
193
|
+
path.join(dir, ".macaw", "config.json"),
|
|
194
|
+
path.join(process.cwd(), ".macaw", "config.json"),
|
|
195
|
+
path.join(os.homedir(), ".macaw", "config.json"),
|
|
196
|
+
].filter(Boolean);
|
|
197
|
+
|
|
198
|
+
for (const p of places) {
|
|
199
|
+
try {
|
|
200
|
+
const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
201
|
+
if (typeof cfg?.api_key === "string" && cfg.api_key.startsWith("mw_")) {
|
|
202
|
+
return { key: cfg.api_key, from: p, workspace: cfg.workspace || "" };
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Unreadable or not JSON. Try the next one.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ------------------------------------------------------------ activation ---
|
|
212
|
+
|
|
213
|
+
async function activate() {
|
|
214
|
+
const start = await fetch(`${API}/api/device/code`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
// 'default' is the regular console. secCC sends 'seccc' and gets its own
|
|
218
|
+
// view; a Python SDK install has no business landing there.
|
|
219
|
+
body: JSON.stringify({ device_name: `${os.hostname()} (client)`, platform: "default" }),
|
|
220
|
+
}).then((r) => r.json()).catch(() => null);
|
|
221
|
+
|
|
222
|
+
if (!start?.device_code) {
|
|
223
|
+
no(`Could not reach ${API}`);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// One clickable link, whatever the plane returned. A control plane older
|
|
228
|
+
// than verification_uri_complete still gives us both halves, so compose it
|
|
229
|
+
// rather than falling back to "here is a URL, now type this code".
|
|
230
|
+
const url = start.verification_uri_complete
|
|
231
|
+
|| (start.verification_uri && start.user_code
|
|
232
|
+
&& `${start.verification_uri}?user_code=${encodeURIComponent(start.user_code)}`)
|
|
233
|
+
|| start.verification_uri;
|
|
234
|
+
console.log("");
|
|
235
|
+
console.log(` Approve here: ${C.c}${url}${C.x}`);
|
|
236
|
+
// The code is in the link. Worth printing separately only when it is not -
|
|
237
|
+
// an older control plane, or someone reading it off another machine.
|
|
238
|
+
if (!url.includes("user_code=")) console.log(` Code ${C.b}${start.user_code}${C.x}`);
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log(" Waiting for approval...");
|
|
241
|
+
|
|
242
|
+
const deadline = Date.now() + (start.expires_in ?? 600) * 1000;
|
|
243
|
+
const every = (start.interval ?? 5) * 1000;
|
|
244
|
+
while (Date.now() < deadline) {
|
|
245
|
+
await new Promise((r) => setTimeout(r, every));
|
|
246
|
+
const poll = await fetch(`${API}/api/device/poll?device_code=${encodeURIComponent(start.device_code)}`)
|
|
247
|
+
.then((r) => r.json()).catch(() => null);
|
|
248
|
+
if (poll?.status === "complete") return { key: poll.api_key, workspace: poll.workspace };
|
|
249
|
+
if (poll?.status === "expired") { no("That code expired. Run this again."); return null; }
|
|
250
|
+
}
|
|
251
|
+
no("Timed out waiting for approval");
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ------------------------------------------------------------------ main ---
|
|
256
|
+
|
|
257
|
+
async function main() {
|
|
258
|
+
console.log("");
|
|
259
|
+
console.log(`${C.b} MACAW${C.x}`);
|
|
260
|
+
console.log("");
|
|
261
|
+
|
|
262
|
+
const wheels = bundledWheels();
|
|
263
|
+
const plat = platformTag();
|
|
264
|
+
const usable = wheels.filter((w) => w.platform === plat);
|
|
265
|
+
|
|
266
|
+
if (!usable.length) {
|
|
267
|
+
no(`No build for ${process.platform}/${process.arch}.`);
|
|
268
|
+
info("Builds we ship:");
|
|
269
|
+
for (const w of wheels) console.log(` Python ${version(w.tag)} ${w.platform}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --check answers "would this work here, and with what" and changes nothing.
|
|
274
|
+
// Useful before installing, and the first thing to ask when it did not work.
|
|
275
|
+
if (process.argv.includes("--check")) {
|
|
276
|
+
const py = findPython(usable);
|
|
277
|
+
const d = defaultInterp();
|
|
278
|
+
const dflt = pythonVersionOf(d);
|
|
279
|
+
console.log(` platform ${process.platform}/${process.arch} → ${plat}`);
|
|
280
|
+
const shipped = [...new Set(usable.map((w) => version(w.tag)))].sort(byVersionDesc);
|
|
281
|
+
console.log(` we ship ${shipped.map((v) => "Python " + v).join(", ")}`);
|
|
282
|
+
console.log(` ${d.display.padEnd(9)} ${dflt || "not found"}`);
|
|
283
|
+
console.log(py?.error ? ` would use ${py.error}`
|
|
284
|
+
: py ? ` would use ${py.display} (${py.version})`
|
|
285
|
+
: ` would use nothing we have a build for`);
|
|
286
|
+
if (py && !py.error && dflt && dflt !== py.version) {
|
|
287
|
+
console.log(` ${C.y}⚠${C.x} your ${d.display} cannot import what that installs.`);
|
|
288
|
+
console.log(` MACAW_PYTHON=${d.display} to install into it instead`);
|
|
289
|
+
}
|
|
290
|
+
const haveKey = existingKey(path.resolve("./macaw"));
|
|
291
|
+
console.log(` key ${haveKey ? "reusing " + haveKey.from : "none found; would activate"}`);
|
|
292
|
+
console.log(` endpoint ${API}`);
|
|
293
|
+
process.exit(py && !py.error ? 0 : 1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
297
|
+
const ask = async (q, dflt) => {
|
|
298
|
+
if (!process.stdin.isTTY) return dflt; // nobody to answer
|
|
299
|
+
const a = (await rl.question(q)).trim();
|
|
300
|
+
return a === "" ? dflt : a;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// 1 — where
|
|
304
|
+
const chosen = await ask(` Where should MACAW go? ${C.dim}[./macaw]${C.x} `, "./macaw");
|
|
305
|
+
const dir = path.resolve(chosen.replace(/^~(?=$|\/)/, os.homedir()));
|
|
306
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
307
|
+
ok(dir);
|
|
308
|
+
|
|
309
|
+
// 2 — python
|
|
310
|
+
const py = findPython(usable);
|
|
311
|
+
if (!py || py.error) {
|
|
312
|
+
no(py?.error || "No Python here that we have a build for.");
|
|
313
|
+
info(`On ${plat} we ship: ${[...new Set(usable.map((w) => version(w.tag)))].sort(byVersionDesc).join(", ")}`);
|
|
314
|
+
rl.close();
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
ok(`Python ${py.version} (${py.display})`);
|
|
318
|
+
|
|
319
|
+
// We prefer the newest interpreter we have a wheel for, which is not always
|
|
320
|
+
// the one a bare `python3` resolves to. When they differ the install succeeds
|
|
321
|
+
// and the user's default interpreter still cannot import the library, so say
|
|
322
|
+
// so here rather than letting them find out from a traceback.
|
|
323
|
+
const dflt = defaultInterp();
|
|
324
|
+
const dfltVersion = pythonVersionOf(dflt);
|
|
325
|
+
if (dfltVersion && dfltVersion !== py.version) {
|
|
326
|
+
console.log(` ${C.y}⚠${C.x} Your ${dflt.display} is ${dfltVersion}; this installs into Python ${py.version}.`);
|
|
327
|
+
console.log(` Run MACAW code with ${C.b}${py.display}${C.x}, or install into ${dflt.display} instead:`);
|
|
328
|
+
console.log(` ${C.c}MACAW_PYTHON=${dflt.display} npx @macaw-ai/client install${C.x}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 3 — a key, or a workspace
|
|
332
|
+
console.log("");
|
|
333
|
+
const found = existingKey(dir);
|
|
334
|
+
let workspace = found?.workspace || "";
|
|
335
|
+
let key;
|
|
336
|
+
|
|
337
|
+
if (found?.explicit) {
|
|
338
|
+
key = found.key;
|
|
339
|
+
ok(found.workspace ? `Same workspace: ${found.workspace}` : "Using the key passed in");
|
|
340
|
+
} else if (found) {
|
|
341
|
+
info(`A key is already on this machine: ${found.from}`);
|
|
342
|
+
key = await ask(` API Key ${C.dim}[Enter joins that workspace]${C.x} `, found.key);
|
|
343
|
+
} else if (!process.stdin.isTTY) {
|
|
344
|
+
// Without this the default is "", which starts a device activation,
|
|
345
|
+
// prints a link nobody is watching, and blocks for ten minutes.
|
|
346
|
+
no("No key found, and nothing is attached to approve a new workspace.");
|
|
347
|
+
info("Run this in a terminal, or pass a key: MACAW_API_KEY=mw_... npx @macaw-ai/client install");
|
|
348
|
+
rl.close();
|
|
349
|
+
process.exit(1);
|
|
350
|
+
} else {
|
|
351
|
+
key = await ask(` API Key ${C.dim}[Enter creates a new workspace]${C.x} `, "");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (key === "") {
|
|
355
|
+
const got = await activate();
|
|
356
|
+
if (!got) { rl.close(); process.exit(1); }
|
|
357
|
+
key = got.key;
|
|
358
|
+
workspace = got.workspace || "";
|
|
359
|
+
ok(`Activated in workspace ${workspace}`);
|
|
360
|
+
} else if (!key.startsWith("mw_")) {
|
|
361
|
+
no("An API key starts with 'mw_'");
|
|
362
|
+
rl.close();
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
rl.close();
|
|
366
|
+
|
|
367
|
+
// 4 — install
|
|
368
|
+
console.log("");
|
|
369
|
+
const wheel = usable.find((w) => version(w.tag) === py.version);
|
|
370
|
+
const adapters = fs.existsSync(path.join(PKG, "wheels"))
|
|
371
|
+
? fs.readdirSync(path.join(PKG, "wheels")).filter((f) => f.startsWith("macaw_adapters-"))
|
|
372
|
+
.map((f) => path.join(PKG, "wheels", f))
|
|
373
|
+
: [];
|
|
374
|
+
const r = pipInstall(py, [wheel.file, ...adapters]);
|
|
375
|
+
if (!r.ok) { no("pip install failed"); console.log(r.err); process.exit(1); }
|
|
376
|
+
|
|
377
|
+
// pip exiting 0 does not mean the library is importable: a --user install
|
|
378
|
+
// whose site directory is not on this interpreter's sys.path passes, and so
|
|
379
|
+
// does one shadowed by an older copy earlier on the path. Ask the question
|
|
380
|
+
// the user will ask.
|
|
381
|
+
const imported = spawnSync(py.cmd, [...py.pre, "-c", "import macaw_client, macaw_adapters"],
|
|
382
|
+
{ encoding: "utf8" });
|
|
383
|
+
if (imported.status !== 0) {
|
|
384
|
+
no(`pip reported success, but ${py.display} cannot import what it installed.`);
|
|
385
|
+
console.log((imported.stderr || imported.stdout || "").trim());
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
ok(`macaw-client and macaw-adapters installed into ${r.where}, importable by ${py.display}`);
|
|
389
|
+
|
|
390
|
+
// 5 — the tree, and the config beside it
|
|
391
|
+
// Whatever build.sh assembled, verbatim. A list here would be a second
|
|
392
|
+
// place to keep in step, and it already fell out of step once.
|
|
393
|
+
const payload = path.join(PKG, "payload");
|
|
394
|
+
if (fs.existsSync(payload)) fs.cpSync(payload, dir, { recursive: true });
|
|
395
|
+
const cfgDir = path.join(dir, ".macaw");
|
|
396
|
+
fs.mkdirSync(cfgDir, { recursive: true, mode: 0o700 });
|
|
397
|
+
// The config holds an API key and the install directory may be inside a
|
|
398
|
+
// repository.
|
|
399
|
+
fs.writeFileSync(path.join(cfgDir, ".gitignore"), "*\n!.gitignore\n");
|
|
400
|
+
fs.writeFileSync(path.join(cfgDir, "config.json"), JSON.stringify({
|
|
401
|
+
transport: "http",
|
|
402
|
+
http_enabled: true,
|
|
403
|
+
macaw_endpoint: `${API}/rpc`,
|
|
404
|
+
api_key: key,
|
|
405
|
+
workspace,
|
|
406
|
+
multi_tenancy_enabled: true,
|
|
407
|
+
}, null, 2) + "\n", { mode: 0o600 });
|
|
408
|
+
ok("examples, demos and config written");
|
|
409
|
+
|
|
410
|
+
// 6 — what to do with it
|
|
411
|
+
console.log("");
|
|
412
|
+
console.log(`${C.b} Check it${C.x}`);
|
|
413
|
+
console.log(` cd ${dir}`);
|
|
414
|
+
console.log(` ${py.display} secureAI/test_harness_quick.py`);
|
|
415
|
+
console.log("");
|
|
416
|
+
console.log(` ${C.dim}To use MACAW from other directories, point MACAW_HOME at${C.x}`);
|
|
417
|
+
console.log(` ${C.dim}this directory in your shell:${C.x}`);
|
|
418
|
+
console.log(` MACAW_HOME=${dir}`);
|
|
419
|
+
console.log("");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
main().catch((e) => { no(e.message); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@macaw-ai/client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.9.11",
|
|
4
|
+
"description": "Set up the MACAW client for a Python project: installs the SDK and adapters, creates a workspace, leaves the examples ready to run.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"macaw-client-install": "bin/install.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/install.js",
|
|
11
|
+
"wheels",
|
|
12
|
+
"payload",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
20
|
+
"homepage": "https://macawsecurity.ai",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/mohanua/macaw-client-distro.git"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"macaw",
|
|
27
|
+
"agent",
|
|
28
|
+
"policy",
|
|
29
|
+
"security",
|
|
30
|
+
"governance",
|
|
31
|
+
"openai",
|
|
32
|
+
"anthropic",
|
|
33
|
+
"langchain",
|
|
34
|
+
"mcp"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# MACAW
|
|
2
|
+
|
|
3
|
+
Installed here by `npx @macaw-ai/client install`.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
secureAI/ the open-source half, Apache-2.0
|
|
7
|
+
examples/ one per framework - openai, anthropic, langchain, mcp,
|
|
8
|
+
litellm, pydantic_ai, attestations, phishing-resistance
|
|
9
|
+
demos/ three tutorials: policy controls, RBAC, intent
|
|
10
|
+
macaw_adapters/ the adapter source, to read
|
|
11
|
+
.macaw/ your key and endpoint. Treat it like a credential.
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Check it works
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python secureAI/test_harness_quick.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Runs the MCP examples and 1a. No API keys needed for the MCP path.
|
|
21
|
+
|
|
22
|
+
## What is installed where
|
|
23
|
+
|
|
24
|
+
The adapter directory above is **source, for reading**. What actually runs is
|
|
25
|
+
the wheel pip installed into your Python:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -c "import macaw_client, macaw_adapters; print(macaw_client.__file__)"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Both are importable from anywhere on this machine. The directory you are in
|
|
32
|
+
holds what you *run*; Python holds what you *import*.
|
|
33
|
+
|
|
34
|
+
## Using it from somewhere else
|
|
35
|
+
|
|
36
|
+
The client looks for its config in `$MACAW_HOME`, then the current directory,
|
|
37
|
+
then your home directory. So from here it already works. To use it from another
|
|
38
|
+
directory, point `MACAW_HOME` at this one in your shell.
|
|
39
|
+
|
|
40
|
+
## The adapters
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from macaw_adapters.openai import SecureOpenAI
|
|
44
|
+
from macaw_adapters.mcp import SecureMCP
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Imports are lazy — `macaw_adapters.mcp` works without `openai` installed, and
|
|
48
|
+
the reverse. You need whichever framework SDK you are actually adapting, which
|
|
49
|
+
you will already have.
|
|
50
|
+
|
|
51
|
+
Source, issues and new adapters: https://github.com/macawsecurity/secureAI
|
|
52
|
+
|
|
53
|
+
## A different workspace
|
|
54
|
+
|
|
55
|
+
Run the installer again with an API key from your admin to join theirs, or
|
|
56
|
+
delete `.macaw/config.json` and run it again to create a new one.
|