fix(ui-desktop): unify path resolution around GOOSE_PATH_ROOT (#7335)
Signed-off-by: Vadim Polulyakh <bavadim@gmail.com> Co-authored-by: Jack Amadeo <jackamadeo@block.xyz>
This commit is contained in:
+25
-15
@@ -20,31 +20,41 @@ trap 'log "An error occurred. Exiting with status $?."' ERR
|
|||||||
|
|
||||||
log "Starting jbang setup script."
|
log "Starting jbang setup script."
|
||||||
|
|
||||||
# Ensure ~/.config/goose/mcp-hermit/bin exists
|
if [ -n "${GOOSE_PATH_ROOT:-}" ]; then
|
||||||
log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist."
|
RESOLVED_GOOSE_CONFIG_DIR="${GOOSE_PATH_ROOT}/config"
|
||||||
mkdir -p ~/.config/goose/mcp-hermit/bin
|
elif [ -n "${GOOSE_CONFIG_DIR:-}" ]; then
|
||||||
|
log "GOOSE_CONFIG_DIR is deprecated for desktop shims; prefer GOOSE_PATH_ROOT."
|
||||||
|
RESOLVED_GOOSE_CONFIG_DIR="${GOOSE_CONFIG_DIR}"
|
||||||
|
else
|
||||||
|
RESOLVED_GOOSE_CONFIG_DIR="${HOME}/.config/goose"
|
||||||
|
fi
|
||||||
|
MCP_HERMIT_DIR="${RESOLVED_GOOSE_CONFIG_DIR}/mcp-hermit"
|
||||||
|
|
||||||
# Change to the ~/.config/goose/mcp-hermit directory
|
# Ensure mcp-hermit/bin exists
|
||||||
log "Changing to directory ~/.config/goose/mcp-hermit."
|
log "Creating directory ${MCP_HERMIT_DIR}/bin if it does not exist."
|
||||||
cd ~/.config/goose/mcp-hermit
|
mkdir -p "${MCP_HERMIT_DIR}/bin"
|
||||||
|
|
||||||
|
# Change to the mcp-hermit directory
|
||||||
|
log "Changing to directory ${MCP_HERMIT_DIR}."
|
||||||
|
cd "${MCP_HERMIT_DIR}"
|
||||||
|
|
||||||
# Check if hermit binary exists and download if not
|
# Check if hermit binary exists and download if not
|
||||||
if [ ! -f ~/.config/goose/mcp-hermit/bin/hermit ]; then
|
if [ ! -f "${MCP_HERMIT_DIR}/bin/hermit" ]; then
|
||||||
log "Hermit binary not found. Downloading hermit binary."
|
log "Hermit binary not found. Downloading hermit binary."
|
||||||
curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \
|
curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \
|
||||||
| gzip -dc > ~/.config/goose/mcp-hermit/bin/hermit && chmod +x ~/.config/goose/mcp-hermit/bin/hermit
|
| gzip -dc > "${MCP_HERMIT_DIR}/bin/hermit" && chmod +x "${MCP_HERMIT_DIR}/bin/hermit"
|
||||||
log "Hermit binary downloaded and made executable."
|
log "Hermit binary downloaded and made executable."
|
||||||
else
|
else
|
||||||
log "Hermit binary already exists. Skipping download."
|
log "Hermit binary already exists. Skipping download."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "setting hermit cache to be local for MCP servers"
|
log "setting hermit cache to be local for MCP servers"
|
||||||
mkdir -p ~/.config/goose/mcp-hermit/cache
|
mkdir -p "${MCP_HERMIT_DIR}/cache"
|
||||||
export HERMIT_STATE_DIR=~/.config/goose/mcp-hermit/cache
|
export HERMIT_STATE_DIR="${MCP_HERMIT_DIR}/cache"
|
||||||
|
|
||||||
# Update PATH
|
# Update PATH
|
||||||
export PATH=~/.config/goose/mcp-hermit/bin:$PATH
|
export PATH="${MCP_HERMIT_DIR}/bin:${PATH}"
|
||||||
log "Updated PATH to include ~/.config/goose/mcp-hermit/bin."
|
log "Updated PATH to include ${MCP_HERMIT_DIR}/bin."
|
||||||
|
|
||||||
# Initialize hermit
|
# Initialize hermit
|
||||||
log "Initializing hermit."
|
log "Initializing hermit."
|
||||||
@@ -55,10 +65,10 @@ log "Installing OpenJDK with hermit."
|
|||||||
hermit install openjdk@17 >> "$LOG_FILE"
|
hermit install openjdk@17 >> "$LOG_FILE"
|
||||||
|
|
||||||
# Download and install jbang if not present
|
# Download and install jbang if not present
|
||||||
if [ ! -f ~/.config/goose/mcp-hermit/bin/jbang ]; then
|
if [ ! -f "${MCP_HERMIT_DIR}/bin/jbang" ]; then
|
||||||
log "Downloading and installing jbang."
|
log "Downloading and installing jbang."
|
||||||
curl -Ls https://sh.jbang.dev | bash -s - app setup
|
curl -Ls https://sh.jbang.dev | bash -s - app setup
|
||||||
cp ~/.jbang/bin/jbang ~/.config/goose/mcp-hermit/bin/
|
cp ~/.jbang/bin/jbang "${MCP_HERMIT_DIR}/bin/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Verify installations
|
# Verify installations
|
||||||
@@ -86,4 +96,4 @@ jbang --quiet trust add *
|
|||||||
log "Executing 'jbang' command with arguments: $*"
|
log "Executing 'jbang' command with arguments: $*"
|
||||||
jbang --fresh --quiet "$@" || log "Failed to execute 'jbang' with arguments: $*"
|
jbang --fresh --quiet "$@" || log "Failed to execute 'jbang' with arguments: $*"
|
||||||
|
|
||||||
log "jbang setup script completed successfully."
|
log "jbang setup script completed successfully."
|
||||||
|
|||||||
@@ -23,32 +23,43 @@ trap 'log "An error occurred. Exiting with status $?."' ERR
|
|||||||
|
|
||||||
log "Starting node setup (common)."
|
log "Starting node setup (common)."
|
||||||
|
|
||||||
|
if [ -n "${GOOSE_PATH_ROOT:-}" ]; then
|
||||||
|
RESOLVED_GOOSE_CONFIG_DIR="${GOOSE_PATH_ROOT}/config"
|
||||||
|
elif [ -n "${GOOSE_CONFIG_DIR:-}" ]; then
|
||||||
|
log "GOOSE_CONFIG_DIR is deprecated for desktop shims; prefer GOOSE_PATH_ROOT."
|
||||||
|
RESOLVED_GOOSE_CONFIG_DIR="${GOOSE_CONFIG_DIR}"
|
||||||
|
else
|
||||||
|
RESOLVED_GOOSE_CONFIG_DIR="${HOME}/.config/goose"
|
||||||
|
fi
|
||||||
|
MCP_HERMIT_DIR="${RESOLVED_GOOSE_CONFIG_DIR}/mcp-hermit"
|
||||||
|
|
||||||
# One-time cleanup for existing Linux users to fix locking issues
|
# One-time cleanup for existing Linux users to fix locking issues
|
||||||
CLEANUP_MARKER="${HOME}/.config/goose/.mcp-hermit-cleanup-v1"
|
CLEANUP_MARKER="${RESOLVED_GOOSE_CONFIG_DIR}/.mcp-hermit-cleanup-v1"
|
||||||
if [[ "$(uname -s)" == "Linux" ]] && [ ! -f "${CLEANUP_MARKER}" ]; then
|
if [[ "$(uname -s)" == "Linux" ]] && [ ! -f "${CLEANUP_MARKER}" ]; then
|
||||||
log "Performing one-time cleanup of old mcp-hermit directory to fix locking issues."
|
log "Performing one-time cleanup of old mcp-hermit directory to fix locking issues."
|
||||||
if [ -d "${HOME}/.config/goose/mcp-hermit" ]; then
|
if [ -d "${MCP_HERMIT_DIR}" ]; then
|
||||||
rm -rf "${HOME}/.config/goose/mcp-hermit"
|
rm -rf "${MCP_HERMIT_DIR}"
|
||||||
log "Removed old mcp-hermit directory."
|
log "Removed old mcp-hermit directory."
|
||||||
fi
|
fi
|
||||||
|
mkdir -p "${RESOLVED_GOOSE_CONFIG_DIR}"
|
||||||
touch "${CLEANUP_MARKER}"
|
touch "${CLEANUP_MARKER}"
|
||||||
log "Cleanup completed. Marker file created."
|
log "Cleanup completed. Marker file created."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure ${HOME}/.config/goose/mcp-hermit/bin exists
|
# Ensure mcp-hermit/bin exists
|
||||||
log "Creating directory ${HOME}/.config/goose/mcp-hermit/bin if it does not exist."
|
log "Creating directory ${MCP_HERMIT_DIR}/bin if it does not exist."
|
||||||
mkdir -p "${HOME}/.config/goose/mcp-hermit/bin"
|
mkdir -p "${MCP_HERMIT_DIR}/bin"
|
||||||
|
|
||||||
# Change to the ${HOME}/.config/goose/mcp-hermit directory
|
# Change to the mcp-hermit directory
|
||||||
log "Changing to directory ${HOME}/.config/goose/mcp-hermit."
|
log "Changing to directory ${MCP_HERMIT_DIR}."
|
||||||
cd "${HOME}/.config/goose/mcp-hermit"
|
cd "${MCP_HERMIT_DIR}"
|
||||||
|
|
||||||
|
|
||||||
# Check if hermit binary exists and download if not
|
# Check if hermit binary exists and download if not
|
||||||
if [ ! -f "${HOME}/.config/goose/mcp-hermit/bin/hermit" ]; then
|
if [ ! -f "${MCP_HERMIT_DIR}/bin/hermit" ]; then
|
||||||
log "Hermit binary not found. Downloading hermit binary."
|
log "Hermit binary not found. Downloading hermit binary."
|
||||||
curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \
|
curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \
|
||||||
| gzip -dc > "${HOME}/.config/goose/mcp-hermit/bin/hermit" && chmod +x "${HOME}/.config/goose/mcp-hermit/bin/hermit"
|
| gzip -dc > "${MCP_HERMIT_DIR}/bin/hermit" && chmod +x "${MCP_HERMIT_DIR}/bin/hermit"
|
||||||
log "Hermit binary downloaded and made executable."
|
log "Hermit binary downloaded and made executable."
|
||||||
else
|
else
|
||||||
log "Hermit binary already exists. Skipping download."
|
log "Hermit binary already exists. Skipping download."
|
||||||
@@ -56,13 +67,13 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
log "setting hermit cache to be local for MCP servers"
|
log "setting hermit cache to be local for MCP servers"
|
||||||
mkdir -p "${HOME}/.config/goose/mcp-hermit/cache"
|
mkdir -p "${MCP_HERMIT_DIR}/cache"
|
||||||
export HERMIT_STATE_DIR="${HOME}/.config/goose/mcp-hermit/cache"
|
export HERMIT_STATE_DIR="${MCP_HERMIT_DIR}/cache"
|
||||||
|
|
||||||
|
|
||||||
# Update PATH
|
# Update PATH
|
||||||
export PATH="${HOME}/.config/goose/mcp-hermit/bin:${PATH}"
|
export PATH="${MCP_HERMIT_DIR}/bin:${PATH}"
|
||||||
log "Updated PATH to include ${HOME}/.config/goose/mcp-hermit/bin."
|
log "Updated PATH to include ${MCP_HERMIT_DIR}/bin."
|
||||||
|
|
||||||
|
|
||||||
# Verify hermit installation
|
# Verify hermit installation
|
||||||
@@ -78,7 +89,7 @@ if [ ! -f "bin/activate-hermit" ]; then
|
|||||||
log "Creating temp dir with bin subdirectory for hermit copy to avoid self-update locks."
|
log "Creating temp dir with bin subdirectory for hermit copy to avoid self-update locks."
|
||||||
HERMIT_TMP_DIR="/tmp/hermit_tmp_$$/bin"
|
HERMIT_TMP_DIR="/tmp/hermit_tmp_$$/bin"
|
||||||
mkdir -p "${HERMIT_TMP_DIR}"
|
mkdir -p "${HERMIT_TMP_DIR}"
|
||||||
cp "${HOME}/.config/goose/mcp-hermit/bin/hermit" "${HERMIT_TMP_DIR}/hermit"
|
cp "${MCP_HERMIT_DIR}/bin/hermit" "${HERMIT_TMP_DIR}/hermit"
|
||||||
chmod +x "${HERMIT_TMP_DIR}/hermit"
|
chmod +x "${HERMIT_TMP_DIR}/hermit"
|
||||||
export PATH="${HERMIT_TMP_DIR}:${PATH}"
|
export PATH="${HERMIT_TMP_DIR}:${PATH}"
|
||||||
HERMIT_CLEANUP_DIR="/tmp/hermit_tmp_$$"
|
HERMIT_CLEANUP_DIR="/tmp/hermit_tmp_$$"
|
||||||
@@ -124,10 +135,10 @@ if [ -n "${GOOSE_NPM_REGISTRY:-}" ] && curl -s --head --fail "${GOOSE_NPM_REGIST
|
|||||||
# Check if GOOSE_NPM_CERT is set and accessible
|
# Check if GOOSE_NPM_CERT is set and accessible
|
||||||
if [ -n "${GOOSE_NPM_CERT:-}" ] && curl -s --head --fail "${GOOSE_NPM_CERT}" > /dev/null; then
|
if [ -n "${GOOSE_NPM_CERT:-}" ] && curl -s --head --fail "${GOOSE_NPM_CERT}" > /dev/null; then
|
||||||
log "Downloading certificate from: ${GOOSE_NPM_CERT}"
|
log "Downloading certificate from: ${GOOSE_NPM_CERT}"
|
||||||
curl -sSL -o "${HOME}/.config/goose/mcp-hermit/cert.pem" "${GOOSE_NPM_CERT}"
|
curl -sSL -o "${MCP_HERMIT_DIR}/cert.pem" "${GOOSE_NPM_CERT}"
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
log "Certificate downloaded successfully."
|
log "Certificate downloaded successfully."
|
||||||
export NODE_EXTRA_CA_CERTS="${HOME}/.config/goose/mcp-hermit/cert.pem"
|
export NODE_EXTRA_CA_CERTS="${MCP_HERMIT_DIR}/cert.pem"
|
||||||
else
|
else
|
||||||
log "Unable to download the certificate. Skipping certificate setup."
|
log "Unable to download the certificate. Skipping certificate setup."
|
||||||
fi
|
fi
|
||||||
|
|||||||
+22
-12
@@ -20,19 +20,29 @@ trap 'log "An error occurred. Exiting with status $?."' ERR
|
|||||||
|
|
||||||
log "Starting uvx setup script."
|
log "Starting uvx setup script."
|
||||||
|
|
||||||
# Ensure ~/.config/goose/mcp-hermit/bin exists
|
if [ -n "${GOOSE_PATH_ROOT:-}" ]; then
|
||||||
log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist."
|
RESOLVED_GOOSE_CONFIG_DIR="${GOOSE_PATH_ROOT}/config"
|
||||||
mkdir -p ~/.config/goose/mcp-hermit/bin
|
elif [ -n "${GOOSE_CONFIG_DIR:-}" ]; then
|
||||||
|
log "GOOSE_CONFIG_DIR is deprecated for desktop shims; prefer GOOSE_PATH_ROOT."
|
||||||
|
RESOLVED_GOOSE_CONFIG_DIR="${GOOSE_CONFIG_DIR}"
|
||||||
|
else
|
||||||
|
RESOLVED_GOOSE_CONFIG_DIR="${HOME}/.config/goose"
|
||||||
|
fi
|
||||||
|
MCP_HERMIT_DIR="${RESOLVED_GOOSE_CONFIG_DIR}/mcp-hermit"
|
||||||
|
|
||||||
# Change to the ~/.config/goose/mcp-hermit directory
|
# Ensure mcp-hermit/bin exists
|
||||||
log "Changing to directory ~/.config/goose/mcp-hermit."
|
log "Creating directory ${MCP_HERMIT_DIR}/bin if it does not exist."
|
||||||
cd ~/.config/goose/mcp-hermit
|
mkdir -p "${MCP_HERMIT_DIR}/bin"
|
||||||
|
|
||||||
|
# Change to the mcp-hermit directory
|
||||||
|
log "Changing to directory ${MCP_HERMIT_DIR}."
|
||||||
|
cd "${MCP_HERMIT_DIR}"
|
||||||
|
|
||||||
# Check if hermit binary exists and download if not
|
# Check if hermit binary exists and download if not
|
||||||
if [ ! -f ~/.config/goose/mcp-hermit/bin/hermit ]; then
|
if [ ! -f "${MCP_HERMIT_DIR}/bin/hermit" ]; then
|
||||||
log "Hermit binary not found. Downloading hermit binary."
|
log "Hermit binary not found. Downloading hermit binary."
|
||||||
curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \
|
curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \
|
||||||
| gzip -dc > ~/.config/goose/mcp-hermit/bin/hermit && chmod +x ~/.config/goose/mcp-hermit/bin/hermit
|
| gzip -dc > "${MCP_HERMIT_DIR}/bin/hermit" && chmod +x "${MCP_HERMIT_DIR}/bin/hermit"
|
||||||
log "Hermit binary downloaded and made executable."
|
log "Hermit binary downloaded and made executable."
|
||||||
else
|
else
|
||||||
log "Hermit binary already exists. Skipping download."
|
log "Hermit binary already exists. Skipping download."
|
||||||
@@ -40,12 +50,12 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
log "setting hermit cache to be local for MCP servers"
|
log "setting hermit cache to be local for MCP servers"
|
||||||
mkdir -p ~/.config/goose/mcp-hermit/cache
|
mkdir -p "${MCP_HERMIT_DIR}/cache"
|
||||||
export HERMIT_STATE_DIR=~/.config/goose/mcp-hermit/cache
|
export HERMIT_STATE_DIR="${MCP_HERMIT_DIR}/cache"
|
||||||
|
|
||||||
# Update PATH
|
# Update PATH
|
||||||
export PATH=~/.config/goose/mcp-hermit/bin:$PATH
|
export PATH="${MCP_HERMIT_DIR}/bin:${PATH}"
|
||||||
log "Updated PATH to include ~/.config/goose/mcp-hermit/bin."
|
log "Updated PATH to include ${MCP_HERMIT_DIR}/bin."
|
||||||
|
|
||||||
|
|
||||||
# Verify hermit installation
|
# Verify hermit installation
|
||||||
|
|||||||
+12
-1
@@ -496,6 +496,14 @@ const getBundledConfig = (): BundledConfig => {
|
|||||||
const { defaultProvider, defaultModel, predefinedModels, baseUrlShare, version } =
|
const { defaultProvider, defaultModel, predefinedModels, baseUrlShare, version } =
|
||||||
getBundledConfig();
|
getBundledConfig();
|
||||||
|
|
||||||
|
const resolveGoosePathRoot = (): string | undefined => {
|
||||||
|
const pathRoot = process.env.GOOSE_PATH_ROOT?.trim();
|
||||||
|
if (pathRoot) {
|
||||||
|
return expandTilde(pathRoot);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const GENERATED_SECRET = crypto.randomBytes(32).toString('hex');
|
const GENERATED_SECRET = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
const getServerSecret = (settings: Settings): string => {
|
const getServerSecret = (settings: Settings): string => {
|
||||||
@@ -513,6 +521,7 @@ let appConfig = {
|
|||||||
GOOSE_DEFAULT_MODEL: defaultModel,
|
GOOSE_DEFAULT_MODEL: defaultModel,
|
||||||
GOOSE_PREDEFINED_MODELS: predefinedModels,
|
GOOSE_PREDEFINED_MODELS: predefinedModels,
|
||||||
GOOSE_API_HOST: 'https://localhost',
|
GOOSE_API_HOST: 'https://localhost',
|
||||||
|
GOOSE_PATH_ROOT: resolveGoosePathRoot(),
|
||||||
GOOSE_WORKING_DIR: '',
|
GOOSE_WORKING_DIR: '',
|
||||||
// If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode)
|
// If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode)
|
||||||
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
|
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
|
||||||
@@ -554,7 +563,9 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
|||||||
const goosedResult = await startGoosed({
|
const goosedResult = await startGoosed({
|
||||||
serverSecret,
|
serverSecret,
|
||||||
dir: dir || os.homedir(),
|
dir: dir || os.homedir(),
|
||||||
env: { GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT },
|
env: {
|
||||||
|
GOOSE_PATH_ROOT: appConfig.GOOSE_PATH_ROOT as string | undefined,
|
||||||
|
},
|
||||||
externalGoosed: settings.externalGoosed,
|
externalGoosed: settings.externalGoosed,
|
||||||
isPackaged: app.isPackaged,
|
isPackaged: app.isPackaged,
|
||||||
resourcesPath: app.isPackaged ? process.resourcesPath : undefined,
|
resourcesPath: app.isPackaged ? process.resourcesPath : undefined,
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export const convertToLocaleDateString = (lastModified: string): string => {
|
|||||||
|
|
||||||
export const getStorageDirectory = (isGlobal: boolean): string => {
|
export const getStorageDirectory = (isGlobal: boolean): string => {
|
||||||
if (isGlobal) {
|
if (isGlobal) {
|
||||||
|
const pathRoot = window.appConfig.get('GOOSE_PATH_ROOT') as string | undefined;
|
||||||
|
if (pathRoot) {
|
||||||
|
return `${pathRoot}/config/recipes`;
|
||||||
|
}
|
||||||
|
const configDir = window.appConfig.get('GOOSE_CONFIG_DIR') as string | undefined;
|
||||||
|
if (configDir) {
|
||||||
|
return `${configDir}/recipes`;
|
||||||
|
}
|
||||||
return '~/.config/goose/recipes';
|
return '~/.config/goose/recipes';
|
||||||
} else {
|
} else {
|
||||||
// For directory recipes, build absolute path using working directory
|
// For directory recipes, build absolute path using working directory
|
||||||
|
|||||||
Reference in New Issue
Block a user