Skip to content

feat: implement multi-tenant credential storage#178

Merged
marcusquinn merged 3 commits intomainfrom
feature/multi-tenant-credentials
Jan 24, 2026
Merged

feat: implement multi-tenant credential storage#178
marcusquinn merged 3 commits intomainfrom
feature/multi-tenant-credentials

Conversation

@marcusquinn
Copy link
Owner

@marcusquinn marcusquinn commented Jan 24, 2026

Summary

  • Implements multi-tenant credential storage for managing multiple accounts/clients per service (t005)
  • Adds credential-helper.sh with full tenant lifecycle: create, switch, set/get/remove keys, copy, delete
  • Backward compatible: existing mcp-env.sh auto-migrates to default tenant on first init

Architecture

~/.config/aidevops/
├── mcp-env.sh              # Loader (sources active tenant)
├── active-tenant           # Global active tenant name
└── tenants/
    ├── default/
    │   └── mcp-env.sh      # Original credentials (migrated)
    ├── client-acme/
    │   └── mcp-env.sh      # Acme Corp credentials
    └── client-globex/
        └── mcp-env.sh      # Globex Corp credentials

Key Features

  • Per-project tenant override via .aidevops-tenant (gitignored)
  • Resolution priority: Project > Global > Default
  • Copy keys between tenants (share common API keys)
  • Export command for script integration (eval $(credential-helper.sh export))
  • MCP tool updated with tenant-* actions
  • Zero ShellCheck violations
  • Security: 700 dirs, 600 files, validated tenant names

Use Cases

  • Agency/freelance: per-client credential sets
  • Environment separation: production vs staging
  • Multiple accounts: personal vs work GitHub tokens
  • Multi-org: different Cloudflare/Vercel accounts

Testing

All commands verified manually:

  • init (migrates 13 existing keys to default tenant)
  • create, set, get, keys, copy, switch, remove, delete
  • export for eval integration
  • status and list for visibility

Files Changed

File Change
.agent/scripts/credential-helper.sh New: multi-tenant management (530 lines)
.agent/tools/credentials/multi-tenant.md New: subagent documentation
.agent/scripts/setup-local-api-keys.sh Added tenant command delegation
.agent/tools/credentials/api-key-setup.md Added multi-tenant reference
.opencode/tool/api-keys.ts Added tenant-* MCP actions
.gitignore Added .aidevops-tenant, credential exceptions

Summary by CodeRabbit

  • New Features

    • Multi-tenant credential storage with create/switch/delete, global and per-project selection
    • Set/get/remove/copy keys per tenant and export tenant envs
    • Automatic migration of legacy credentials and project-level override support
    • CLI now exposes tenant management commands and routes tenant actions to the new helper
  • Documentation

    • New multi-tenant guide and updated API key setup with usage examples and best practices
  • Chores

    • Updated ignore rules to handle per-project tenant marker and credential tooling files

✏️ Tip: You can customize this high-level summary in your review settings.

Add credential-helper.sh for managing multiple credential sets (tenants)
per service, enabling agency/freelance workflows with per-client API keys.

Architecture:
- ~/.config/aidevops/tenants/{tenant}/mcp-env.sh per tenant
- Priority: project (.aidevops-tenant) > global > default
- Backward compatible: existing mcp-env.sh migrates to 'default' tenant

Features:
- create/switch/delete tenants
- set/get/remove keys per tenant
- copy keys between tenants
- per-project tenant override (.aidevops-tenant)
- export command for script integration
- MCP tool updated with tenant-* actions

Closes #t005
@gemini-code-assist
Copy link

Summary of Changes

Hello @marcusquinn, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to credential management by implementing a multi-tenant storage system. This allows users to organize and switch between distinct sets of API keys and environment variables, catering to scenarios like managing credentials for multiple clients, separating development and production environments, or handling personal and work accounts. The change provides a flexible and secure way to handle credentials, improving workflow efficiency and reducing the risk of using incorrect keys.

Highlights

  • Multi-Tenant Credential Management: Introduced a robust multi-tenant system for managing separate credential sets for different clients, environments, or accounts, enhancing organization and security.
  • New credential-helper.sh Script: A new Bash script provides a comprehensive command-line interface for tenant lifecycle management, including init, create, switch, set, get, remove, delete, copy, use, status, keys, and export.
  • Backward Compatibility & Migration: Existing mcp-env.sh credentials are automatically migrated to a 'default' tenant upon first initialization, ensuring a smooth transition without manual intervention.
  • Flexible Tenant Resolution: Implemented a priority system for active tenant selection: project-level override (.aidevops-tenant), global active tenant, and a 'default' fallback, offering granular control.
  • Enhanced Security: Enforced strict file permissions (700 for directories, 600 for files), validated tenant names, and automatically added .aidevops-tenant to .gitignore to prevent accidental commits.
  • MCP Tool Integration: The api-keys MCP tool now supports tenant-* actions, allowing direct interaction with the multi-tenant system from the tool's interface.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 24, 2026

Warning

Rate limit exceeded

@marcusquinn has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 7 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new multi-tenant credential manager via credential-helper.sh, integrates tenant commands into existing setup tooling and API keys tool, introduces documentation for multi-tenant workflows, updates .gitignore, and provides legacy migration and project-level tenant override behavior.

Changes

Cohort / File(s) Summary
Credential Storage & Management
\.agent/scripts/credential-helper\.sh
New ~873-line Bash script implementing multi-tenant credential storage and lifecycle: init, create, switch, list, use (project override), set/get/remove/keys/copy/export, delete, legacy migration (migrate_legacy), and helper utilities. Tenants stored under ~/.config/aidevops/tenants/{tenant}/mcp-env.sh.
Script Integration
\.agent/scripts/setup-local-api-keys\.sh, \.opencode/tool/api-keys\.ts
setup-local-api-keys.sh: adds tenant/tenants commands that delegate to credential-helper.sh. .opencode/tool/api-keys.ts: adds tenant-related actions (tenant-status, tenant-switch, tenant-list, tenant-create, tenant-keys) and routes tenant-* actions to credential-helper.sh (bypassing prior flow).
Documentation
\.agent/tools/credentials/api-key-setup\.md, \.agent/tools/credentials/multi-tenant\.md
api-key-setup.md updated with multi-tenant guidance and best-practices; new multi-tenant.md documents architecture, storage layout, commands, migration, examples, CI/shell integration, and security notes.
Configuration
\.gitignore
Added entries to include credential helper and docs (!.agent/scripts/credential-helper.sh, !.agent/tools/credentials/**) and added .aidevops-tenant rule for per-project tenant marker.

Sequence Diagrams

sequenceDiagram
    participant User
    participant Shell
    participant CredHelper as credential-helper.sh
    participant Storage as ~/.config/aidevops/
    participant Legacy as Legacy mcp-env.sh

    User->>Shell: credential-helper.sh init
    Shell->>CredHelper: run init
    CredHelper->>Legacy: check legacy mcp-env.sh
    alt legacy exists
        CredHelper->>CredHelper: parse legacy
        CredHelper->>Storage: create default tenant dir & write mcp-env.sh
        CredHelper->>Legacy: rewrite to source active tenant
    else no legacy
        CredHelper->>Storage: create config structure and default tenant
    end
    CredHelper->>Shell: print completion
Loading
sequenceDiagram
    participant User
    participant Shell
    participant CredHelper as credential-helper.sh
    participant ProjFile as .aidevops-tenant
    participant GlobalFile as ~/.config/aidevops/active-tenant
    participant TenantStorage as tenants/{tenant}/mcp-env.sh

    User->>Shell: credential-helper.sh set KEY VALUE
    Shell->>CredHelper: run set
    CredHelper->>ProjFile: check project override
    alt project override exists
        CredHelper->>TenantStorage: write KEY=VALUE to project tenant file
    else
        CredHelper->>GlobalFile: read active tenant
        CredHelper->>TenantStorage: write KEY=VALUE to global tenant file (or default)
    end
    CredHelper->>Shell: confirm stored

    User->>Shell: credential-helper.sh get KEY
    Shell->>CredHelper: run get
    CredHelper->>ProjFile: check override
    CredHelper->>GlobalFile: fallback if no override
    CredHelper->>TenantStorage: read mcp-env.sh and output KEY value
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

"A tiny vault of tenants, arranged with care,
Legacy keys folded into a neat lair,
Per-project flags whisper which secrets to bear,
Scripts hum in concert — credentials everywhere."

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement multi-tenant credential storage' directly and clearly summarizes the main change—adding a complete multi-tenant credential management system across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 85.19% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 407 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Sat Jan 24 16:33:53 UTC 2026: Code review monitoring started
Sat Jan 24 16:33:53 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 407
Sat Jan 24 16:33:54 UTC 2026: Qlty - 0 issues found, auto-formatting applied
Sat Jan 24 16:33:56 UTC 2026: Codacy analysis completed with auto-fixes
Sat Jan 24 16:33:57 UTC 2026: Applied 2 automatic fixes

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 407
  • VULNERABILITIES: 0

Generated on: Sat Jan 24 16:35:15 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust multi-tenant credential management system, centered around the new credential-helper.sh script. The implementation is comprehensive, covering tenant lifecycle management, backward compatibility with legacy credentials, and integration with existing tools. The code is well-structured and includes thorough documentation.

My review focuses on improving the robustness and maintainability of the new bash script. I've identified a high-severity issue in how credential values are updated, which could fail with special characters. I've also included a couple of medium-severity suggestions to improve code clarity and efficiency. Overall, this is a great feature addition.

# Update or append
if grep -q "^export ${env_var}=" "$env_file" 2>/dev/null; then
local tmp_file="${env_file}.tmp"
sed "s|^export ${env_var}=.*|export ${env_var}=\"${value}\"|" "$env_file" > "$tmp_file"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The sed command used here is not safe for arbitrary values. If the <value> contains the sed delimiter (|) or other special characters like &, the command will fail or have unexpected behavior. A more robust approach is to use grep -v to remove the existing line and then echo to append the new one. This avoids issues with special characters in the value, though it may change the key's position in the file, which should be an acceptable trade-off for correctness.

Suggested change
sed "s|^export ${env_var}=.*|export ${env_var}=\"${value}\"|" "$env_file" > "$tmp_file"
{ grep -v "^export ${env_var}=" "$env_file"; echo "export ${env_var}=\"${value}\""; } > "$tmp_file"

print_success() {
local msg="$1"
echo -e "${GREEN}[OK] $msg${NC}"
return 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The explicit return 0 is redundant in this and the other print_* functions (print_info, print_warning, print_error). Shell functions implicitly return the exit status of the last command executed (in this case, echo). The calling code is already responsible for the script's overall exit status. Removing these return 0 statements would make the functions slightly cleaner.

fi

local result
result=$(grep "^export ${env_var}=" "$env_file" 2>/dev/null | sed 's/^export [^=]*="//' | sed 's/"$//' || true)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This line uses grep piped to two separate sed commands to extract the value. This can be made more efficient and readable by using a single sed command with a capturing group to extract the value in one step.

Suggested change
result=$(grep "^export ${env_var}=" "$env_file" 2>/dev/null | sed 's/^export [^=]*="//' | sed 's/"$//' || true)
result=$(grep "^export ${env_var}=" "$env_file" 2>/dev/null | sed 's/^export [^=]*="\(.*\)"$/\1/' || true)

Replace sed substitution with grep -v + append pattern to safely handle
values containing pipe (|) and ampersand (&) characters. Also simplify
the get command's value extraction to a single sed expression.
@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 407 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Sat Jan 24 16:39:21 UTC 2026: Code review monitoring started
Sat Jan 24 16:39:22 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 407
Sat Jan 24 16:39:22 UTC 2026: Qlty - 0 issues found, auto-formatting applied
Sat Jan 24 16:39:24 UTC 2026: Codacy analysis completed with auto-fixes
Sat Jan 24 16:39:24 UTC 2026: Applied 2 automatic fixes

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 407
  • VULNERABILITIES: 0

Generated on: Sat Jan 24 16:40:35 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @.agent/scripts/credential-helper.sh:
- Around line 58-91: The tenant read paths (get_active_tenant reading
PROJECT_TENANT_FILE and ACTIVE_TENANT_FILE) currently return unchecked values;
ensure every tenant value is validated with the existing validate_tenant_name
function (or a single centralized validate_tenant_name wrapper) before being
used to construct or read paths and before honoring any --tenant CLI input or
project/global file content; if validation fails, log/print an error and fall
back to "default" rather than returning the untrusted value (apply same
validation to cmd_* entrypoints and any callers that consume tenant names).
- Around line 292-412: The current cmd_set writes raw secret values into the
tenant env file and uses sed/echo unescaped, which corrupts or executes values
containing $, \, ", &, etc.; update cmd_set to safely escape or encode values
before writing/updating (e.g. escape backslashes, double-quotes, dollar signs
and ampersands or encode with base64) and perform the file update by writing a
new line with the already-escaped/encoded payload to a temp file then atomically
mv it over env_file (use ensure_tenant_dir/get_tenant_env_file and refer to
env_var for the var name); likewise change the update path that does sed
"s|^export ${env_var}=.*|...|" to use the safe temp-file write with the escaped
value. In cmd_get, do not parse with brittle sed pipelines—read the real value
by sourcing the tenant env file in a subshell or by decoding the stored encoding
(if you opted for base64) and then output the variable (use env_var to locate
the variable). Ensure chmod 600 is preserved and both cmd_set and cmd_get handle
empty/missing files the same as before.

In @.agent/scripts/setup-local-api-keys.sh:
- Around line 317-322: The tenant branch currently calls credential-helper.sh
but ignores its exit status, causing main to return 0; capture the helper's exit
code (e.g., after bash "$script_dir/credential-helper.sh" "$@") and propagate it
by exiting or returning that code from the script (e.g., store "$?" in a
variable and call exit with that variable) so the "tenant"|"tenants") case and
the overall main return the helper’s status.

In @.agent/tools/credentials/api-key-setup.md:
- Around line 218-235: Update the Multi-Tenant section and the Quick
Reference/Storage sections to clarify that when tenants are enabled credentials
live in per-tenant files under ~/.config/aidevops/tenants/{tenant}/mcp-env.sh
and that ~/.config/aidevops/mcp-env.sh acts only as a loader for the active
tenant; add a short note (exact wording like the suggested clarification) and
link to multi-tenant.md so readers aren’t misled about where secrets are stored
after running credential-helper.sh init/create/switch/use.

In @.agent/tools/credentials/multi-tenant.md:
- Around line 68-105: The examples invoke credential-helper.sh directly which
assumes it is on PATH; update the documentation examples and the Initialize
section to invoke the script explicitly (e.g. bash
.agent/scripts/credential-helper.sh init and bash
.agent/scripts/credential-helper.sh create <tenant>, set, switch, use) or add a
short note before the first usage explaining how to run it if not on PATH
(alternatively reference setup-local-api-keys.sh tenant as an option); adjust
every occurrence of credential-helper.sh in the Create Tenants, Add Credentials,
and Switch Tenants blocks to the explicit invocation or add the single NOTE line
so new clones can run the commands without PATH assumptions.

Comment on lines +58 to +91
# Validate tenant name (alphanumeric, hyphens, underscores)
validate_tenant_name() {
local name="$1"
if [[ ! "$name" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then
print_error "Invalid tenant name: '$name'. Use alphanumeric, hyphens, underscores."
return 1
fi
return 0
}

# Get the active tenant name
get_active_tenant() {
# Priority: 1) Project override, 2) Global active, 3) "default"
if [[ -f "$PROJECT_TENANT_FILE" ]]; then
local project_tenant
project_tenant=$(cat "$PROJECT_TENANT_FILE" 2>/dev/null | tr -d '[:space:]')
if [[ -n "$project_tenant" ]]; then
echo "$project_tenant"
return 0
fi
fi

if [[ -f "$ACTIVE_TENANT_FILE" ]]; then
local active
active=$(cat "$ACTIVE_TENANT_FILE" 2>/dev/null | tr -d '[:space:]')
if [[ -n "$active" ]]; then
echo "$active"
return 0
fi
fi

echo "default"
return 0
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate tenant names everywhere before constructing paths.

Project/global tenant values (and --tenant inputs) are used without validation, so a crafted name like ../... could write or source outside ~/.config/aidevops/tenants. Please validate on read and on all tenant inputs (or centralize validation) and fall back to default when invalid.

🔒 Proposed hardening (apply similarly in cmd_* entrypoints)
 validate_tenant_name() {
     local name="$1"
     if [[ ! "$name" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then
         print_error "Invalid tenant name: '$name'. Use alphanumeric, hyphens, underscores."
         return 1
     fi
     return 0
 }
+
+require_valid_tenant() {
+    local name="$1"
+    validate_tenant_name "$name" || return 1
+    return 0
+}
 
 # Get the active tenant name
 get_active_tenant() {
     # Priority: 1) Project override, 2) Global active, 3) "default"
     if [[ -f "$PROJECT_TENANT_FILE" ]]; then
         local project_tenant
         project_tenant=$(cat "$PROJECT_TENANT_FILE" 2>/dev/null | tr -d '[:space:]')
         if [[ -n "$project_tenant" ]]; then
-            echo "$project_tenant"
-            return 0
+            if require_valid_tenant "$project_tenant"; then
+                echo "$project_tenant"
+                return 0
+            fi
+            print_warning "Ignoring invalid project tenant '$project_tenant'"
         fi
     fi
 
     if [[ -f "$ACTIVE_TENANT_FILE" ]]; then
         local active
         active=$(cat "$ACTIVE_TENANT_FILE" 2>/dev/null | tr -d '[:space:]')
         if [[ -n "$active" ]]; then
-            echo "$active"
-            return 0
+            if require_valid_tenant "$active"; then
+                echo "$active"
+                return 0
+            fi
+            print_warning "Ignoring invalid active tenant '$active'"
         fi
     fi
🧰 Tools
🪛 GitHub Check: Codacy Static Code Analysis

[notice] 73-73: .agent/scripts/credential-helper.sh#L73
Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.


[notice] 82-82: .agent/scripts/credential-helper.sh#L82
Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.

🤖 Prompt for AI Agents
In @.agent/scripts/credential-helper.sh around lines 58 - 91, The tenant read
paths (get_active_tenant reading PROJECT_TENANT_FILE and ACTIVE_TENANT_FILE)
currently return unchecked values; ensure every tenant value is validated with
the existing validate_tenant_name function (or a single centralized
validate_tenant_name wrapper) before being used to construct or read paths and
before honoring any --tenant CLI input or project/global file content; if
validation fails, log/print an error and fall back to "default" rather than
returning the untrusted value (apply same validation to cmd_* entrypoints and
any callers that consume tenant names).

Comment on lines 292 to 412
# Set a key for a tenant
cmd_set() {
local key=""
local value=""
local tenant=""

# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--tenant|-t)
tenant="$2"
shift 2
;;
*)
if [[ -z "$key" ]]; then
key="$1"
elif [[ -z "$value" ]]; then
value="$1"
fi
shift
;;
esac
done

if [[ -z "$key" || -z "$value" ]]; then
print_error "Usage: credential-helper.sh set <KEY_NAME> <value> [--tenant <name>]"
return 1
fi

# Default to active tenant
if [[ -z "$tenant" ]]; then
tenant=$(get_active_tenant)
fi

migrate_legacy
ensure_tenant_dir "$tenant"

local env_file
env_file=$(get_tenant_env_file "$tenant")

# Convert service name to env var if needed
local env_var
if [[ "$key" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then
env_var="$key"
else
env_var=$(echo "$key" | tr '[:lower:]-' '[:upper:]_')
fi

# Update or append
if grep -q "^export ${env_var}=" "$env_file" 2>/dev/null; then
local tmp_file="${env_file}.tmp"
sed "s|^export ${env_var}=.*|export ${env_var}=\"${value}\"|" "$env_file" > "$tmp_file"
mv "$tmp_file" "$env_file"
chmod 600 "$env_file"
print_success "Updated $env_var in tenant '$tenant'"
else
echo "export ${env_var}=\"${value}\"" >> "$env_file"
chmod 600 "$env_file"
print_success "Added $env_var to tenant '$tenant'"
fi

return 0
}

# Get a key from a tenant
cmd_get() {
local key=""
local tenant=""

# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--tenant|-t)
tenant="$2"
shift 2
;;
*)
if [[ -z "$key" ]]; then
key="$1"
fi
shift
;;
esac
done

if [[ -z "$key" ]]; then
print_error "Usage: credential-helper.sh get <KEY_NAME> [--tenant <name>]"
return 1
fi

if [[ -z "$tenant" ]]; then
tenant=$(get_active_tenant)
fi

local env_file
env_file=$(get_tenant_env_file "$tenant")

if [[ ! -f "$env_file" ]]; then
print_error "Tenant '$tenant' not found"
return 1
fi

# Convert service name to env var if needed
local env_var
if [[ "$key" =~ ^[A-Z_][A-Z0-9_]*$ ]]; then
env_var="$key"
else
env_var=$(echo "$key" | tr '[:lower:]-' '[:upper:]_')
fi

local result
result=$(grep "^export ${env_var}=" "$env_file" 2>/dev/null | sed 's/^export [^=]*="//' | sed 's/"$//' || true)

if [[ -n "$result" ]]; then
echo "$result"
return 0
else
print_error "Key $env_var not found in tenant '$tenant'"
return 1
fi
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape secret values before writing/updating env files.

Values are interpolated into sed/echo without escaping, so tokens containing $, \", &, or backslashes can be corrupted or even executed when sourced. Use shell-safe escaping and a safer replace path; adjust cmd_get to read the real value.

🛡️ Safer write/read path for secrets
     # Update or append
+    local escaped_value
+    escaped_value=$(printf '%q' "$value")
     if grep -q "^export ${env_var}=" "$env_file" 2>/dev/null; then
         local tmp_file="${env_file}.tmp"
-        sed "s|^export ${env_var}=.*|export ${env_var}=\"${value}\"|" "$env_file" > "$tmp_file"
+        awk -v k="$env_var" -v v="$escaped_value" '
+          $0 ~ "^export "k"=" { print "export "k"="v; next }
+          { print }
+        ' "$env_file" > "$tmp_file"
         mv "$tmp_file" "$env_file"
         chmod 600 "$env_file"
         print_success "Updated $env_var in tenant '$tenant'"
     else
-        echo "export ${env_var}=\"${value}\"" >> "$env_file"
+        printf 'export %s=%s\n' "$env_var" "$escaped_value" >> "$env_file"
         chmod 600 "$env_file"
         print_success "Added $env_var to tenant '$tenant'"
     fi
@@
-    local result
-    result=$(grep "^export ${env_var}=" "$env_file" 2>/dev/null | sed 's/^export [^=]*="//' | sed 's/"$//' || true)
-
-    if [[ -n "$result" ]]; then
-        echo "$result"
+    # shellcheck disable=SC1090
+    source "$env_file"
+    local result="${!env_var-}"
+    if [[ -n "$result" ]]; then
+        echo "$result"
         return 0
🤖 Prompt for AI Agents
In @.agent/scripts/credential-helper.sh around lines 292 - 412, The current
cmd_set writes raw secret values into the tenant env file and uses sed/echo
unescaped, which corrupts or executes values containing $, \, ", &, etc.; update
cmd_set to safely escape or encode values before writing/updating (e.g. escape
backslashes, double-quotes, dollar signs and ampersands or encode with base64)
and perform the file update by writing a new line with the
already-escaped/encoded payload to a temp file then atomically mv it over
env_file (use ensure_tenant_dir/get_tenant_env_file and refer to env_var for the
var name); likewise change the update path that does sed "s|^export
${env_var}=.*|...|" to use the safe temp-file write with the escaped value. In
cmd_get, do not parse with brittle sed pipelines—read the real value by sourcing
the tenant env file in a subshell or by decoding the stored encoding (if you
opted for base64) and then output the variable (use env_var to locate the
variable). Ensure chmod 600 is preserved and both cmd_set and cmd_get handle
empty/missing files the same as before.

Comment on lines +317 to +322
"tenant"|"tenants")
# Delegate to multi-tenant credential helper
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
bash "$script_dir/credential-helper.sh" "$@"
;;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate tenant-helper exit codes.

Errors from credential-helper.sh are currently swallowed and main returns 0, which breaks automation that relies on exit status. Please return the helper’s status.

✅ Propagate the helper status
         "tenant"|"tenants")
             # Delegate to multi-tenant credential helper
             local script_dir
             script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-            bash "$script_dir/credential-helper.sh" "$@"
+            bash "$script_dir/credential-helper.sh" "$@" || return $?
             ;;

As per coding guidelines, preserve proper exit codes for automation.

🤖 Prompt for AI Agents
In @.agent/scripts/setup-local-api-keys.sh around lines 317 - 322, The tenant
branch currently calls credential-helper.sh but ignores its exit status, causing
main to return 0; capture the helper's exit code (e.g., after bash
"$script_dir/credential-helper.sh" "$@") and propagate it by exiting or
returning that code from the script (e.g., store "$?" in a variable and call
exit with that variable) so the "tenant"|"tenants") case and the overall main
return the helper’s status.

Comment on lines +218 to +235
## Multi-Tenant Support

For managing multiple accounts (clients, environments, organizations):

```bash
# Initialize multi-tenant storage
credential-helper.sh init

# Create per-client tenants
credential-helper.sh create client-acme
credential-helper.sh set GITHUB_TOKEN ghp_xxx --tenant client-acme

# Switch globally or per-project
credential-helper.sh switch client-acme
credential-helper.sh use client-acme # per-project override
```

See `multi-tenant.md` for full documentation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify where secrets live after enabling tenants.

This new section introduces tenant storage, but the doc still states keys live only in ~/.config/aidevops/mcp-env.sh; with multi-tenant, that file is a loader and secrets live in per-tenant files. Please add a short note (and update the Quick Reference/Storage sections) to avoid confusion.

✏️ Suggested clarification
 # Switch globally or per-project
 credential-helper.sh switch client-acme
 credential-helper.sh use client-acme  # per-project override

+Note: with multi-tenant enabled, credentials live in
+~/.config/aidevops/tenants/{tenant}/mcp-env.sh; ~/.config/aidevops/mcp-env.sh
+is now a loader for the active tenant.
+
See multi-tenant.md for full documentation.

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @.agent/tools/credentials/api-key-setup.md around lines 218 - 235, Update the
Multi-Tenant section and the Quick Reference/Storage sections to clarify that
when tenants are enabled credentials live in per-tenant files under
~/.config/aidevops/tenants/{tenant}/mcp-env.sh and that
~/.config/aidevops/mcp-env.sh acts only as a loader for the active tenant; add a
short note (exact wording like the suggested clarification) and link to
multi-tenant.md so readers aren’t misled about where secrets are stored after
running credential-helper.sh init/create/switch/use.


</details>

<!-- fingerprinting:phantom:medusa:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +68 to +105
### Initialize

```bash
# First time: migrates existing mcp-env.sh to 'default' tenant
credential-helper.sh init
```

### Create Tenants

```bash
# Create tenants for different contexts
credential-helper.sh create personal
credential-helper.sh create work
credential-helper.sh create client-acme
```

### Add Credentials

```bash
# Add to specific tenant
credential-helper.sh set GITHUB_TOKEN ghp_personal_xxx --tenant personal
credential-helper.sh set GITHUB_TOKEN ghp_work_xxx --tenant work
credential-helper.sh set GITHUB_TOKEN ghp_acme_xxx --tenant client-acme

# Add to active tenant (no --tenant flag)
credential-helper.sh set OPENAI_API_KEY sk-xxx
```

### Switch Tenants

```bash
# Global switch (affects all terminals after reload)
credential-helper.sh switch client-acme

# Per-project (overrides global, stays in this directory)
cd ~/projects/acme-webapp
credential-helper.sh use client-acme
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make command invocation explicit to avoid PATH assumptions.

The examples call credential-helper.sh directly, but new clones won’t have it on PATH. Consider using bash .agent/scripts/credential-helper.sh ... (or setup-local-api-keys.sh tenant ...) in examples, or add a short note near the first usage.

✏️ Example tweak
 # First time: migrates existing mcp-env.sh to 'default' tenant
-credential-helper.sh init
+bash .agent/scripts/credential-helper.sh init
🤖 Prompt for AI Agents
In @.agent/tools/credentials/multi-tenant.md around lines 68 - 105, The examples
invoke credential-helper.sh directly which assumes it is on PATH; update the
documentation examples and the Initialize section to invoke the script
explicitly (e.g. bash .agent/scripts/credential-helper.sh init and bash
.agent/scripts/credential-helper.sh create <tenant>, set, switch, use) or add a
short note before the first usage explaining how to run it if not on PATH
(alternatively reference setup-local-api-keys.sh tenant as an option); adjust
every occurrence of credential-helper.sh in the Create Tenants, Add Credentials,
and Switch Tenants blocks to the explicit invocation or add the single NOTE line
so new clones can run the commands without PATH assumptions.

During setup, prompts user to enable multi-tenant credential storage.
Shows existing key count, explains benefits (agency/freelance, environments,
multiple accounts), and migrates on confirmation. Non-breaking - skipping
leaves everything unchanged.
@sonarqubecloud
Copy link

@github-actions
Copy link

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 407 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Sat Jan 24 16:55:26 UTC 2026: Code review monitoring started
Sat Jan 24 16:55:27 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 407
Sat Jan 24 16:55:27 UTC 2026: Qlty - 0 issues found, auto-formatting applied
Sat Jan 24 16:55:29 UTC 2026: Codacy analysis completed with auto-fixes
Sat Jan 24 16:55:30 UTC 2026: Applied 2 automatic fixes

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 407
  • VULNERABILITIES: 0

Generated on: Sat Jan 24 16:56:47 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

@marcusquinn marcusquinn merged commit 9eec453 into main Jan 24, 2026
8 of 9 checks passed
@marcusquinn marcusquinn deleted the feature/multi-tenant-credentials branch January 24, 2026 19:00
marcusquinn added a commit that referenced this pull request Jan 25, 2026
- Add WaterCrawl to browser automation tools (7 tools now)
- Add LibPDF and Unstract to document processing section
- Add Cloudron app packaging enhancement note
- Add multi-tenant credential storage documentation
- Update MCP count to 19 (added Unstract)
- Update subagent count to 560+ and scripts to 146+
- Document MCP lazy-loading optimization (12-24s startup savings)
- Add WaterCrawl to tool selection guide

Based on PRs #178-#192
marcusquinn added a commit that referenced this pull request Jan 25, 2026
- Add WaterCrawl to browser automation tools (7 tools now)
- Add LibPDF and Unstract to document processing section
- Add Cloudron app packaging enhancement note
- Add multi-tenant credential storage documentation
- Update MCP count to 19 (added Unstract)
- Update subagent count to 560+ and scripts to 146+
- Document MCP lazy-loading optimization (12-24s startup savings)
- Add WaterCrawl to tool selection guide

Based on PRs #178-#192
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant