Compare commits
7
Commits
13eefe3309
...
d2205cb7ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2205cb7ab | ||
|
|
3676891292 | ||
|
|
2aa86ea643 | ||
|
|
9f5059ed07 | ||
|
|
d808026eec | ||
|
|
027eaa158a | ||
|
|
e2c38093e9 |
No files matched your search
@@ -25,6 +25,12 @@ Run scripts from the repository root. PowerShell and Bash commands have equivale
|
||||
| `commands/repository-context.ps1`, `commands/repository-context.sh` | Refresh the local repository-context cache. |
|
||||
| `commands/session-state.ps1`, `commands/session-state.sh` | Read or update the local agent session state. |
|
||||
| `commands/telemetry.ps1`, `commands/telemetry.sh` | Append a local telemetry event. |
|
||||
| `commands/add-instruction.ps1`, `commands/add-instruction.sh` | Append a client-specific user instruction outside managed configuration. |
|
||||
| `commands/add-credentials.ps1`, `commands/add-credentials.sh` | Store a client-scoped tool credential in the operating-system credential store. |
|
||||
|
||||
Custom instructions are read before generated rules, skills, and plugins. They remain under `~/.my-ai-configuration/instructions/`, so installation and update scripts never manage or reset them.
|
||||
|
||||
On Windows, credentials are stored in Windows Credential Manager. Use `~/.my-ai-configuration/bin/with-credential.ps1` or `.sh` to run a tool with a stored key available only in that child process. The key must be a valid environment-variable name; its value is never written to configuration.
|
||||
|
||||
Qwen retrieval uses `shared/retrieval/benchmark.py`; run it through `scripts/lib/semantic-retrieval.py benchmark --root . --home <home-directory>`.
|
||||
|
||||
@@ -53,6 +59,7 @@ Qwen retrieval uses `shared/retrieval/benchmark.py`; run it through `scripts/lib
|
||||
| Script | Purpose |
|
||||
| --- | --- |
|
||||
| `lib/install-options.ps1`, `lib/install-options.sh` | Read installation options and calculate client-specific choices. |
|
||||
| `lib/credentials.ps1`, `lib/with-credential.sh` | Write a tool credential to Windows Credential Manager and expose it to one child process. |
|
||||
| `lib/install-options.py` | Filter configuration templates according to installation options. |
|
||||
| `lib/install-targets.ps1`, `lib/install-targets.sh` | Resolve configuration targets for the selected client and shell. |
|
||||
| `lib/manifest.ps1`, `lib/manifest.sh` | Synchronize managed files and their ownership manifest. |
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Codex', 'Claude', 'Both')][string]$Client,
|
||||
[string]$Key,
|
||||
[switch]$ValueFromStdin,
|
||||
[string]$HomePath = [Environment]::GetFolderPath('UserProfile')
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
. (Join-Path $root 'scripts/lib/install-targets.ps1')
|
||||
$selectedClient = $Client
|
||||
$selectedKey = $Key
|
||||
. (Join-Path $root 'scripts/lib/credentials.ps1')
|
||||
$Client = $selectedClient
|
||||
$Key = $selectedKey
|
||||
|
||||
if (-not $Client) { $Client = Read-InstallClient -Client $Client }
|
||||
if (-not $Key) { $Key = Read-Host 'Credential key' }
|
||||
Assert-CredentialKey $Key
|
||||
if (-not [System.IO.Path]::IsPathRooted($HomePath)) { throw 'HomePath must be absolute.' }
|
||||
if ($ValueFromStdin) {
|
||||
$text = [Console]::In.ReadToEnd().TrimEnd("`r", "`n")
|
||||
$Value = ConvertTo-SecureString $text -AsPlainText -Force
|
||||
$text = $null
|
||||
} else {
|
||||
$Value = Read-Host 'Credential value' -AsSecureString
|
||||
}
|
||||
|
||||
$bin = Join-Path $HomePath '.my-ai-configuration/bin'
|
||||
New-Item -ItemType Directory -Path $bin -Force | Out-Null
|
||||
foreach ($source in @('credentials.ps1', 'with-credential.sh')) {
|
||||
$destination = Join-Path $bin $(if ($source -eq 'credentials.ps1') { 'with-credential.ps1' } else { $source })
|
||||
if (-not (Test-Path -LiteralPath $destination)) { Copy-Item (Join-Path $root "scripts/lib/$source") $destination }
|
||||
}
|
||||
foreach ($target in $(if ($Client -eq 'Both') { @('Codex', 'Claude') } else { @($Client) })) {
|
||||
Set-ManagedCredential $target $Key $Value
|
||||
Write-Output "Credential stored for $($target.ToLowerInvariant())."
|
||||
}
|
||||
$Value = $null
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
client=
|
||||
key=
|
||||
home_path=${HOME:?HOME is required}
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--client) client=${2:?--client requires a value}; shift 2 ;;
|
||||
--key) key=${2:?--key requires a value}; shift 2 ;;
|
||||
--home) home_path=${2:?--home requires a value}; shift 2 ;;
|
||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$client" ] || read -r -p 'Select client (codex, claude, or both) ' client
|
||||
[ -n "$key" ] || read -r -p 'Credential key ' key
|
||||
case "$client" in codex|Codex|claude|Claude|both|Both) ;; *) printf 'Unknown --client value: %s\n' "$client" >&2; exit 1 ;; esac
|
||||
case "$key" in [A-Za-z_][A-Za-z0-9_]* ) ;; *) printf 'Credential key must be an environment-variable-compatible name.\n' >&2; exit 1 ;; esac
|
||||
case "$home_path" in /*|[A-Za-z]:[\\/]*) ;; *) printf 'Home path must be absolute.\n' >&2; exit 1 ;; esac
|
||||
|
||||
IFS= read -r -s -p 'Credential value ' value
|
||||
printf '\n'
|
||||
powershell_command=$(command -v powershell.exe || command -v powershell || command -v pwsh || true)
|
||||
[ -n "$powershell_command" ] || { printf 'PowerShell is required to store credentials in the Windows credential store.\n' >&2; exit 1; }
|
||||
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
printf '%s' "$value" | "$powershell_command" -NoProfile -ExecutionPolicy Bypass -File "$script_dir/add-credentials.ps1" -Client "$client" -Key "$key" -ValueFromStdin -HomePath "$home_path"
|
||||
unset value
|
||||
@@ -0,0 +1,23 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Codex','Claude','Both')][string]$Client,
|
||||
[AllowEmptyString()][string]$Instruction,
|
||||
[string]$HomePath = [Environment]::GetFolderPath('UserProfile')
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
. (Join-Path $root 'scripts/lib/install-targets.ps1')
|
||||
|
||||
if (-not $Client) { $Client = Read-InstallClient -Client $Client }
|
||||
if (-not $PSBoundParameters.ContainsKey('Instruction')) { $Instruction = Read-Host 'Instruction' }
|
||||
if ([string]::IsNullOrWhiteSpace($Instruction) -or $Instruction.Length -gt 65536) { throw 'Instruction must contain between 1 and 65536 characters.' }
|
||||
if (-not [System.IO.Path]::IsPathRooted($HomePath)) { throw 'HomePath must be absolute.' }
|
||||
|
||||
$directory = Join-Path $HomePath '.my-ai-configuration/instructions'
|
||||
New-Item -ItemType Directory -Path $directory -Force | Out-Null
|
||||
foreach ($target in $(if ($Client -eq 'Both') { @('codex','claude') } else { @($Client.ToLowerInvariant()) })) {
|
||||
$path = Join-Path $directory "$target.md"
|
||||
$prefix = if ((Test-Path -LiteralPath $path) -and (Get-Item -LiteralPath $path).Length -gt 0 -and -not (Get-Content -LiteralPath $path -Raw).EndsWith([Environment]::NewLine)) { [Environment]::NewLine } else { '' }
|
||||
[System.IO.File]::AppendAllText($path, $prefix + $Instruction.TrimEnd() + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
|
||||
Write-Output "Instruction added for $target."
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
client=
|
||||
instruction=
|
||||
home_path=${HOME:?HOME is required}
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--client) client=${2:?--client requires a value}; shift 2 ;;
|
||||
--instruction) instruction=${2:?--instruction requires a value}; shift 2 ;;
|
||||
--home) home_path=${2:?--home requires a value}; shift 2 ;;
|
||||
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$client" in
|
||||
codex|Codex) targets=(codex) ;;
|
||||
claude|Claude) targets=(claude) ;;
|
||||
both|Both) targets=(codex claude) ;;
|
||||
'')
|
||||
read -r -p 'Select client (codex, claude, or both) ' client
|
||||
case "$client" in
|
||||
codex|Codex) targets=(codex) ;;
|
||||
claude|Claude) targets=(claude) ;;
|
||||
both|Both) targets=(codex claude) ;;
|
||||
*) printf 'Unknown --client value: %s\n' "$client" >&2; exit 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf 'Unknown --client value: %s\n' "$client" >&2; exit 1 ;;
|
||||
esac
|
||||
if [ -z "$instruction" ]; then read -r -p 'Instruction ' instruction; fi
|
||||
if [ -z "${instruction//[[:space:]]/}" ] || [ ${#instruction} -gt 65536 ]; then printf 'Instruction must contain between 1 and 65536 characters.\n' >&2; exit 1; fi
|
||||
case "$home_path" in /*|[A-Za-z]:[\\/]*) ;; *) printf 'Home path must be absolute.\n' >&2; exit 1 ;; esac
|
||||
|
||||
directory="$home_path/.my-ai-configuration/instructions"
|
||||
mkdir -p -- "$directory"
|
||||
for target in "${targets[@]}"; do
|
||||
path="$directory/$target.md"
|
||||
[ ! -s "$path" ] || [ "$(tail -c 1 -- "$path" | wc -l)" -eq 1 ] || printf '\n' >> "$path"
|
||||
printf '%s\n' "$instruction" >> "$path"
|
||||
printf 'Instruction added for %s.\n' "$target"
|
||||
done
|
||||
@@ -153,12 +153,13 @@ foreach ($shell in @('powershell','bash')) {
|
||||
|
||||
$sharedTemplate = Get-Content (Join-Path $shared 'global-instructions.md') -Raw
|
||||
$generalContent = Get-Content (Join-Path $shared 'rules/general.md') -Raw
|
||||
$credentialHelperExtension = if ($shell -eq 'powershell') { 'ps1' } else { 'sh' }
|
||||
|
||||
$agentsContent = $sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading)
|
||||
$agentsContent = $sharedTemplate.Replace('__RULE_LOADING__', $codexRuleLoading).Replace('__CLIENT__', 'codex').Replace('__SHELL__', $credentialHelperExtension)
|
||||
$agentsContent = $agentsContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
|
||||
Set-Content (Join-Path $output "codex-$shell/AGENTS.md") $agentsContent -Encoding UTF8
|
||||
|
||||
$claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading)
|
||||
$claudeContent = $sharedTemplate.Replace('__RULE_LOADING__', $claudeRuleLoading).Replace('__CLIENT__', 'claude').Replace('__SHELL__', $credentialHelperExtension)
|
||||
$claudeContent = $claudeContent.TrimEnd() + "`r`n`r`n---`r`n`r`n$generalContent"
|
||||
Set-Content (Join-Path $output "claude-$shell/CLAUDE.md") $claudeContent -Encoding UTF8
|
||||
|
||||
|
||||
@@ -153,11 +153,17 @@ ${claude_rule_loading_list%$'\n'}"
|
||||
|
||||
shared_template=$(<"$shared/global-instructions.md")
|
||||
general_content=$(<"$shared/rules/general.md")
|
||||
credential_helper_extension=sh
|
||||
[ "$shell" != powershell ] || credential_helper_extension=ps1
|
||||
|
||||
agents_content=${shared_template//__RULE_LOADING__/$codex_rule_loading}
|
||||
agents_content=${agents_content//__CLIENT__/codex}
|
||||
agents_content=${agents_content//__SHELL__/$credential_helper_extension}
|
||||
printf '%s\n\n---\n\n%s\n' "${agents_content%$'\n'}" "$general_content" > "$output/codex-$shell/AGENTS.md"
|
||||
|
||||
claude_content=${shared_template//__RULE_LOADING__/$claude_rule_loading}
|
||||
claude_content=${claude_content//__CLIENT__/claude}
|
||||
claude_content=${claude_content//__SHELL__/$credential_helper_extension}
|
||||
printf '%s\n\n---\n\n%s\n' "${claude_content%$'\n'}" "$general_content" > "$output/claude-$shell/CLAUDE.md"
|
||||
|
||||
cp "$root/adapters/codex/config/config.toml" "$output/codex-$shell/config.toml"
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Codex', 'Claude')][string]$Client,
|
||||
[string]$Key,
|
||||
[Parameter(Position = 2)][string]$FilePath,
|
||||
[Parameter(Position = 3, ValueFromRemainingArguments = $true)][string[]]$ArgumentList
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-CredentialKey {
|
||||
param([string]$Value)
|
||||
if ($Value -notmatch '^[A-Za-z_][A-Za-z0-9_]{0,127}$') { throw 'Credential key must be an environment-variable-compatible name.' }
|
||||
}
|
||||
|
||||
function Get-CredentialTarget {
|
||||
param([string]$Client, [string]$Key)
|
||||
Assert-CredentialKey $Key
|
||||
return "MyAiConfiguration/$($Client.ToLowerInvariant())/$Key"
|
||||
}
|
||||
|
||||
function Initialize-NativeCredentialApi {
|
||||
if ('MyAiConfiguration.NativeCredential' -as [type]) { return }
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace MyAiConfiguration {
|
||||
public static class NativeCredential {
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct CREDENTIAL {
|
||||
public UInt32 Flags;
|
||||
public UInt32 Type;
|
||||
public IntPtr TargetName;
|
||||
public IntPtr Comment;
|
||||
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
||||
public UInt32 CredentialBlobSize;
|
||||
public IntPtr CredentialBlob;
|
||||
public UInt32 Persist;
|
||||
public UInt32 AttributeCount;
|
||||
public IntPtr Attributes;
|
||||
public IntPtr TargetAlias;
|
||||
public IntPtr UserName;
|
||||
}
|
||||
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern bool CredWrite(ref CREDENTIAL credential, UInt32 flags);
|
||||
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential);
|
||||
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
|
||||
[DllImport("Advapi32.dll", SetLastError = true)]
|
||||
public static extern void CredFree(IntPtr buffer);
|
||||
}
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
function Set-ManagedCredential {
|
||||
param([ValidateSet('Codex', 'Claude')][string]$Client, [string]$Key, [securestring]$Value)
|
||||
if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) { throw 'The OS credential store is currently supported on Windows only.' }
|
||||
Initialize-NativeCredentialApi
|
||||
$plain = [System.Net.NetworkCredential]::new('', $Value).Password
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($plain)
|
||||
if ($bytes.Length -eq 0 -or $bytes.Length -gt 2560) { throw 'Credential value must contain between 1 and 2560 UTF-8 bytes.' }
|
||||
$native = New-Object MyAiConfiguration.NativeCredential+CREDENTIAL
|
||||
$native.Type = 1
|
||||
$native.Persist = 2
|
||||
$native.TargetName = [Runtime.InteropServices.Marshal]::StringToCoTaskMemUni((Get-CredentialTarget $Client $Key))
|
||||
$native.UserName = [Runtime.InteropServices.Marshal]::StringToCoTaskMemUni($Key)
|
||||
$native.CredentialBlobSize = $bytes.Length
|
||||
$native.CredentialBlob = [Runtime.InteropServices.Marshal]::AllocCoTaskMem($bytes.Length)
|
||||
try {
|
||||
[Runtime.InteropServices.Marshal]::Copy($bytes, 0, $native.CredentialBlob, $bytes.Length)
|
||||
if (-not [MyAiConfiguration.NativeCredential]::CredWrite([ref]$native, 0)) { throw [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error()) }
|
||||
} finally {
|
||||
[Array]::Clear($bytes, 0, $bytes.Length)
|
||||
$plain = $null
|
||||
if ($native.TargetName -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeCoTaskMem($native.TargetName) }
|
||||
if ($native.UserName -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeCoTaskMem($native.UserName) }
|
||||
if ($native.CredentialBlob -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeCoTaskMem($native.CredentialBlob) }
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ManagedCredential {
|
||||
param([ValidateSet('Codex', 'Claude')][string]$Client, [string]$Key)
|
||||
if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) { throw 'The OS credential store is currently supported on Windows only.' }
|
||||
Initialize-NativeCredentialApi
|
||||
$pointer = [IntPtr]::Zero
|
||||
if (-not [MyAiConfiguration.NativeCredential]::CredRead((Get-CredentialTarget $Client $Key), 1, 0, [ref]$pointer)) { throw 'Credential was not found.' }
|
||||
try {
|
||||
$native = [Runtime.InteropServices.Marshal]::PtrToStructure($pointer, [type]'MyAiConfiguration.NativeCredential+CREDENTIAL')
|
||||
$bytes = New-Object byte[] $native.CredentialBlobSize
|
||||
[Runtime.InteropServices.Marshal]::Copy($native.CredentialBlob, $bytes, 0, $bytes.Length)
|
||||
try { return [Text.Encoding]::UTF8.GetString($bytes) } finally { [Array]::Clear($bytes, 0, $bytes.Length) }
|
||||
} finally {
|
||||
[MyAiConfiguration.NativeCredential]::CredFree($pointer)
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-ManagedCredential {
|
||||
param([ValidateSet('Codex', 'Claude')][string]$Client, [string]$Key)
|
||||
if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) { throw 'The OS credential store is currently supported on Windows only.' }
|
||||
Initialize-NativeCredentialApi
|
||||
if (-not [MyAiConfiguration.NativeCredential]::CredDelete((Get-CredentialTarget $Client $Key), 1, 0)) { throw [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error()) }
|
||||
}
|
||||
|
||||
function Invoke-WithManagedCredential {
|
||||
param([ValidateSet('Codex', 'Claude')][string]$Client, [string]$Key, [string]$FilePath, [string[]]$ArgumentList)
|
||||
Assert-CredentialKey $Key
|
||||
if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { throw 'Child command was not found.' }
|
||||
$prior = [Environment]::GetEnvironmentVariable($Key, 'Process')
|
||||
$value = Get-ManagedCredential $Client $Key
|
||||
try {
|
||||
[Environment]::SetEnvironmentVariable($Key, $value, 'Process')
|
||||
& $FilePath @ArgumentList
|
||||
return $LASTEXITCODE
|
||||
} finally {
|
||||
[Environment]::SetEnvironmentVariable($Key, $prior, 'Process')
|
||||
$value = $null
|
||||
}
|
||||
}
|
||||
|
||||
if ($FilePath) {
|
||||
if (-not $Client -or -not $Key) { throw 'Client and key are required.' }
|
||||
exit (Invoke-WithManagedCredential $Client $Key $FilePath $ArgumentList)
|
||||
}
|
||||
@@ -344,10 +344,11 @@ class Manager:
|
||||
if not re.fullmatch(r'[a-z0-9@/_.-]+', package):
|
||||
raise ValueError('Invalid CLI package.')
|
||||
record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None)
|
||||
if record is not None:
|
||||
installed = shutil.which(package) is not None
|
||||
if record is not None and installed and not self.update:
|
||||
return
|
||||
self.command(['npm', 'install', '--global', package])
|
||||
if not self.dry_run:
|
||||
self.command(['npm', 'update' if record is not None and installed else 'install', '--global', package])
|
||||
if not self.dry_run and record is None:
|
||||
self.state['resources'].append(dict(client=client, name=entry['name'], kind='cli', package=package))
|
||||
self.save()
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
client=${1:?client is required}
|
||||
key=${2:?credential key is required}
|
||||
shift 2
|
||||
[ $# -gt 0 ] || { printf 'Child command is required.\n' >&2; exit 1; }
|
||||
|
||||
case "$client" in codex|Codex) client=Codex ;; claude|Claude) client=Claude ;; *) printf 'Unknown client: %s\n' "$client" >&2; exit 1 ;; esac
|
||||
case "$key" in [A-Za-z_][A-Za-z0-9_]* ) ;; *) printf 'Credential key must be an environment-variable-compatible name.\n' >&2; exit 1 ;; esac
|
||||
|
||||
powershell_command=$(command -v powershell.exe || command -v powershell || command -v pwsh || true)
|
||||
[ -n "$powershell_command" ] || { printf 'PowerShell is required to read the Windows credential store.\n' >&2; exit 1; }
|
||||
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
exec "$powershell_command" -NoProfile -ExecutionPolicy Bypass -File "$script_dir/with-credential.ps1" -Client "$client" -Key "$key" -FilePath "$1" "${@:2}"
|
||||
@@ -272,6 +272,20 @@ class ManagedExtensionTests(unittest.TestCase):
|
||||
self.assertIn(['claude', 'plugin', 'install', 'qmd@qmd', '--scope', 'user'], self.commands)
|
||||
self.assertNotIn(['codex', 'mcp', 'add', 'qmd', '--', 'qmd', 'mcp'], self.commands)
|
||||
|
||||
def test_managed_mcporter_updates_when_requested(self):
|
||||
self.manager().sync([mcporter()], ['codex'], {'MCPorter'})
|
||||
self.commands.clear()
|
||||
with patch.object(extensions.shutil, 'which', return_value='mcporter'):
|
||||
self.manager(update=True).sync([mcporter()], ['codex'], {'MCPorter'})
|
||||
self.assertEqual([['npm', 'update', '--global', 'mcporter']], self.commands)
|
||||
|
||||
def test_managed_mcporter_reinstalls_when_command_is_missing(self):
|
||||
self.manager().sync([mcporter()], ['codex'], {'MCPorter'})
|
||||
self.commands.clear()
|
||||
with patch.object(extensions.shutil, 'which', return_value=None):
|
||||
self.manager().sync([mcporter()], ['codex'], {'MCPorter'})
|
||||
self.assertEqual([['npm', 'install', '--global', 'mcporter']], self.commands)
|
||||
|
||||
def test_mcporter_replaces_direct_context7_plugin(self):
|
||||
self.manager().sync([mcp(), mcporter()], ['codex'], {'Context7', 'MCPorter'})
|
||||
config = str(self.home / '.mcporter/mcporter.json')
|
||||
|
||||
@@ -57,7 +57,8 @@ foreach ($shell in @('powershell','bash')) {
|
||||
}
|
||||
$docFile = if ($client -eq 'codex') { 'AGENTS.md' } else { 'CLAUDE.md' }
|
||||
$docContent = Get-Content -LiteralPath (Join-Path $package $docFile) -Raw
|
||||
if ($docContent -notmatch [regex]::Escape('Use MCP services only through the `mcporter` CLI.')) { throw "$package must require MCPorter for MCP services" }
|
||||
if ($docContent -notmatch [regex]::Escape('When MCPorter is active (installed and configured), use MCP services only through the `mcporter` CLI.')) { throw "$package must route active MCPorter services through its CLI" }
|
||||
if ($docContent -notmatch [regex]::Escape('If MCPorter is not active, use the direct native MCP service available to you.')) { throw "$package must allow direct native MCP without MCPorter" }
|
||||
if (([regex]::Matches($docContent, [regex]::Escape('Apply instructions in this order'))).Count -ne 1) { throw "$client-$shell must embed the priority rule exactly once" }
|
||||
if ($client -eq 'codex' -and $docContent -match 'Always load and apply `rules/general\.md`') { throw "$package must not still instruct loading general.md by path" }
|
||||
if ($client -eq 'claude' -and (Test-Path (Join-Path $package 'rules/general.md'))) { throw 'Claude must not automatically load a second copy of general rules.' }
|
||||
|
||||
@@ -75,7 +75,8 @@ for shell in powershell bash; do
|
||||
doc_file=AGENTS.md
|
||||
[ "$client" != claude ] || doc_file=CLAUDE.md
|
||||
doc_content=$(cat "$package/$doc_file")
|
||||
printf '%s' "$doc_content" | grep -Fq 'Use MCP services only through the `mcporter` CLI.'
|
||||
printf '%s' "$doc_content" | grep -Fq 'When MCPorter is active (installed and configured), use MCP services only through the `mcporter` CLI.'
|
||||
printf '%s' "$doc_content" | grep -Fq 'If MCPorter is not active, use the direct native MCP service available to you.'
|
||||
occurrences=$(printf '%s' "$doc_content" | grep -o 'Apply instructions in this order' | wc -l)
|
||||
[ "$occurrences" -eq 1 ] || { printf '%s-%s must embed the priority rule exactly once\n' "$client" "$shell" >&2; exit 1; }
|
||||
if [ "$client" = codex ] && printf '%s' "$doc_content" | grep -q 'Always load and apply `rules/general\.md`'; then
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
|
||||
|
||||
class UserCustomizationTests(unittest.TestCase):
|
||||
def test_build_references_the_shell_specific_credential_helper(self):
|
||||
powershell = shutil.which('powershell') or shutil.which('pwsh')
|
||||
if powershell is None:
|
||||
self.skipTest('PowerShell is unavailable.')
|
||||
result = subprocess.run([
|
||||
powershell, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
||||
str(ROOT / 'scripts/commands/build.ps1'), '-Summary',
|
||||
], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
self.assertIn('with-credential.ps1', (ROOT / 'generated/codex-powershell/AGENTS.md').read_text())
|
||||
self.assertIn('with-credential.sh', (ROOT / 'generated/claude-bash/CLAUDE.md').read_text())
|
||||
|
||||
def test_add_instruction_creates_client_overlays_outside_managed_configuration(self):
|
||||
powershell = shutil.which('powershell') or shutil.which('pwsh')
|
||||
if powershell is None:
|
||||
self.skipTest('PowerShell is unavailable.')
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
home = Path(directory)
|
||||
result = subprocess.run([
|
||||
powershell, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
||||
str(ROOT / 'scripts/commands/add-instruction.ps1'),
|
||||
'-Client', 'Both', '-Instruction', 'Always use the local release process.',
|
||||
'-HomePath', str(home),
|
||||
], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
for client in ('codex', 'claude'):
|
||||
self.assertEqual('Always use the local release process.\n',
|
||||
(home / '.my-ai-configuration/instructions' / f'{client}.md').read_text())
|
||||
appended = subprocess.run([
|
||||
powershell, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
||||
str(ROOT / 'scripts/commands/add-instruction.ps1'),
|
||||
'-Client', 'Codex', '-Instruction', 'Keep the release notes current.',
|
||||
'-HomePath', str(home),
|
||||
], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(0, appended.returncode, appended.stderr)
|
||||
self.assertEqual('Always use the local release process.\nKeep the release notes current.\n',
|
||||
(home / '.my-ai-configuration/instructions/codex.md').read_text())
|
||||
|
||||
def test_add_credentials_installs_a_client_scoped_helper(self):
|
||||
powershell = shutil.which('powershell') or shutil.which('pwsh')
|
||||
if powershell is None:
|
||||
self.skipTest('PowerShell is unavailable.')
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
home = Path(directory)
|
||||
key = f'MY_TEST_TOKEN_{uuid.uuid4().hex}'
|
||||
stored = False
|
||||
try:
|
||||
result = subprocess.run([
|
||||
powershell, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
||||
str(ROOT / 'scripts/commands/add-credentials.ps1'),
|
||||
'-Client', 'Codex', '-Key', key, '-ValueFromStdin',
|
||||
'-HomePath', str(home),
|
||||
], input='test-value', capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
stored = True
|
||||
helper = home / '.my-ai-configuration/bin/with-credential.ps1'
|
||||
self.assertTrue(helper.is_file())
|
||||
self.assertTrue((home / '.my-ai-configuration/bin/with-credential.sh').is_file())
|
||||
child = f"if ([Environment]::GetEnvironmentVariable('{key}', 'Process')) {{ exit 0 }}; exit 1"
|
||||
injected = subprocess.run([
|
||||
powershell, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', str(helper),
|
||||
'-Client', 'Codex', '-Key', key, '-FilePath', powershell,
|
||||
'-NoProfile', '-Command', child,
|
||||
], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(0, injected.returncode, injected.stderr)
|
||||
finally:
|
||||
cleanup = subprocess.run([
|
||||
powershell, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
|
||||
f". '{ROOT / 'scripts/lib/credentials.ps1'}'; Remove-ManagedCredential -Client Codex -Key {key}",
|
||||
], capture_output=True, text=True, timeout=20)
|
||||
if stored:
|
||||
self.assertEqual(0, cleanup.returncode, cleanup.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
This file is generated into client-specific instruction files. Do not edit generated copies.
|
||||
|
||||
Before applying generated instructions, read `~/.my-ai-configuration/instructions/__CLIENT__.md` when it exists. Treat its contents as direct user instructions that override generated rules, skills, and plugins. System and developer safety requirements remain non-overridable.
|
||||
|
||||
For a tool credential, invoke `~/.my-ai-configuration/bin/with-credential.__SHELL__` with the client, credential key, and child command. It exposes the credential only to that child process. Never print credentials or place them in commands.
|
||||
|
||||
__RULE_LOADING__
|
||||
|
||||
Use MCP services only through the `mcporter` CLI. Do not invoke direct MCP tools or create, edit, or use native MCP connections, including `/mcp`. If MCPorter or its required server is unavailable, report that limitation instead of bypassing it.
|
||||
When MCPorter is active (installed and configured), use MCP services only through the `mcporter` CLI. Do not bypass it with direct MCP tools or native MCP connections, including `/mcp`; report unavailable required servers. If MCPorter is not active, use the direct native MCP service available to you.
|
||||
|
||||
Main agent owns state and decisions. Delegate only for independent work where benefit exceeds context/tool cost. Use one implementer for contained work; subagents never spawn agents. Pass minimal context and verify handoffs.
|
||||
Reference in new issue
Block a user