Skip to content

Commit 7041044

Browse files
🩹 [Patch]: Enable Context Sharing Across PowerShell Processes and Runspaces (#88)
This release refactors context management to be entirely **disk-based**, enabling robust context sharing and synchronization across scripts, modules, jobs, and processes. ### Key Changes * **Shared context vault:** Contexts are now stored and managed on disk. All scripts, runspaces, and processes interact with the same set of context files, ensuring that context data is always current and consistent. * **Improved support for parallelism:** Multiple PowerShell jobs or sessions can safely create, update, or remove contexts simultaneously, without risking stale or conflicting data. * **Accurate tab completion:** Argument completers now enumerate context IDs directly from disk, providing up-to-date options for tab completion. * **Optimized metadata access:** Commands like `Get-ContextInfo` perform quick metadata lookups by reading only the necessary file information, improving performance. * **Lean initialization:** The module no longer loads contexts into memory on import, reducing resource usage and simplifying module behavior. ### Impact for Integrators * Modules and scripts can now rely on a single, shared context vault across all PowerShell processes. * There is no risk of working with outdated or inconsistent context data. * No changes are needed to existing command usage; standard commands (`Get-Context`, `Set-Context`, etc.) will now always reflect the true state on disk. * These improvements make the module suitable for use in advanced automation, CI/CD workflows, and other scenarios involving parallel or distributed execution. **This update ensures reliable, up-to-date context management for all usage patterns, including concurrent and multi-process environments.** ### Core Changes - **`Get-Context`**: Now reads and decrypts context files directly from the vault directory each time called - **`Get-ContextInfo`**: Reads only file metadata from disk without decryption for fast queries - **`Set-Context`**: No longer updates `$script:Contexts` memory cache, writes only to disk - **`Remove-Context`**: Scans disk files directly instead of iterating memory cache - **`Set-ContextVault`**: No longer auto-imports contexts into memory on initialization - **Argument completers**: Updated to enumerate context IDs from disk for accurate tab completion --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: MariusStorhaug <[email protected]> Co-authored-by: Marius Storhaug <[email protected]>
1 parent 5b1e2de commit 7041044

File tree

9 files changed

+92
-134
lines changed

9 files changed

+92
-134
lines changed

‎src/completers.ps1

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
Register-ArgumentCompleter -CommandName 'Get-Context', 'Set-Context', 'Remove-Context', 'Rename-Context' -ParameterName 'ID' -ScriptBlock {
1+
Register-ArgumentCompleter -CommandName ($script:PSModuleInfo.FunctionsToExport) -ParameterName 'ID' -ScriptBlock {
22
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
33
$null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter
44

5-
$script:Contexts.Values.ID | Where-Object { $_ -like "$wordToComplete*" } |
6-
ForEach-Object {
7-
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
8-
}
5+
$contextInfos = Get-ContextInfo -ErrorAction SilentlyContinue -Verbose:$false -Debug:$false
6+
$contextInfos | Where-Object { $_.ID -like "$wordToComplete*" } | ForEach-Object {
7+
[System.Management.Automation.CompletionResult]::new($_.ID, $_.ID, 'ParameterValue', $_.ID)
8+
}
99
}

‎src/functions/private/Import-Context.ps1

Lines changed: 0 additions & 76 deletions
This file was deleted.

‎src/functions/private/Set-ContextVault.ps1

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.2' }
1+
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.0' }
22

33
function Set-ContextVault {
44
<#
@@ -7,7 +7,7 @@ function Set-ContextVault {
77
88
.DESCRIPTION
99
Sets the context vault. If the vault does not exist, it will be initialized.
10-
Once the context vault is set, it will be imported into memory.
10+
Once the context vault is set, the keys will be prepared for use.
1111
The vault consists of multiple security shards, including a machine-specific shard,
1212
a user-specific shard, and a seed shard stored within the vault directory.
1313
@@ -20,7 +20,7 @@ function Set-ContextVault {
2020
None.
2121
2222
.NOTES
23-
This function modifies the script-scoped configuration and imports the vault.
23+
This function modifies the script-scoped configuration and prepares the vault for use.
2424
2525
.LINK
2626
https://psmodule.io/Context/Functions/Set-ContextVault
@@ -59,7 +59,7 @@ function Set-ContextVault {
5959
$machineShard = [System.Environment]::MachineName
6060
$userShard = [System.Environment]::UserName
6161
#$userInputShard = Read-Host -Prompt 'Enter a seed shard' # Eventually 4 shards. +1 for user input.
62-
$seed = $machineShard + $userShard + $seedShard + $userInputShard
62+
$seed = $machineShard + $userShard + $seedShard # + $userInputShard
6363
$keys = New-SodiumKeyPair -Seed $seed
6464
$script:Config.PrivateKey = $keys.PrivateKey
6565
$script:Config.PublicKey = $keys.PublicKey
@@ -73,6 +73,5 @@ function Set-ContextVault {
7373

7474
end {
7575
Write-Debug "[$stackPath] - End"
76-
Import-Context
7776
}
7877
}

‎src/functions/public/Get-Context.ps1

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
function Get-Context {
1+
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.0' }
2+
3+
function Get-Context {
24
<#
35
.SYNOPSIS
4-
Retrieves a context from the in-memory context vault.
6+
Retrieves a context from the context vault.
57
68
.DESCRIPTION
7-
Retrieves a context from the loaded contexts stored in memory.
9+
Retrieves a context by reading and decrypting context files directly from the vault directory.
810
If no ID is specified, all available contexts will be returned.
911
Wildcards are supported to match multiple contexts.
1012
@@ -42,7 +44,7 @@
4244
LoginTime : 2/9/2025 10:45:11 AM
4345
```
4446
45-
Retrieves all contexts from the context vault (in memory).
47+
Retrieves all contexts from the context vault (directly from disk).
4648
4749
.EXAMPLE
4850
Get-Context -ID 'MySecret'
@@ -68,7 +70,7 @@
6870
YourData : {DataKey=DataValue}
6971
```
7072
71-
Retrieves all contexts that start with 'My' from the context vault (in memory).
73+
Retrieves all contexts that start with 'My' from the context vault (directly from disk).
7274
7375
.OUTPUTS
7476
[System.Object]
@@ -106,7 +108,25 @@
106108
Write-Verbose "Retrieving contexts - ID: [$($ID -join ', ')]"
107109
foreach ($item in $ID) {
108110
Write-Verbose "Retrieving contexts - ID: [$item]"
109-
$script:Contexts.Values | Where-Object { $_.ID -like $item } | Select-Object -ExpandProperty Context
111+
# Read context files directly from disk instead of using in-memory cache
112+
$contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse
113+
foreach ($file in $contextFiles) {
114+
try {
115+
$contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json
116+
if ($contextInfo.ID -like $item) {
117+
# Decrypt and return the context
118+
$params = @{
119+
SealedBox = $contextInfo.Context
120+
PublicKey = $script:Config.PublicKey
121+
PrivateKey = $script:Config.PrivateKey
122+
}
123+
$contextObj = ConvertFrom-SodiumSealedBox @params
124+
ConvertFrom-ContextJson -JsonString $contextObj
125+
}
126+
} catch {
127+
Write-Warning "Failed to read or decrypt context file: $($file.FullName). Error: $_"
128+
}
129+
}
110130
}
111131
}
112132

‎src/functions/public/Get-ContextInfo.ps1

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
function Get-ContextInfo {
22
<#
33
.SYNOPSIS
4-
Retrieves info about a context from the in-memory context vault.
4+
Retrieves info about a context from the context vault.
55
66
.DESCRIPTION
7-
Retrieves info about context from the loaded contexts stored in memory.
7+
Retrieves info about context files directly from the vault directory on disk.
88
If no ID is specified, all available info on contexts will be returned.
99
Wildcards are supported to match multiple contexts.
10+
Only metadata (ID and Path) is returned without decrypting the context contents.
1011
1112
.EXAMPLE
1213
Get-ContextInfo
@@ -29,7 +30,7 @@
2930
Path : ...\feacc853-5bea-48d1-b751-41ce9768d48e.json
3031
```
3132
32-
Retrieves all contexts from the context vault (in memory).
33+
Retrieves all contexts from the context vault (directly from disk).
3334
3435
.EXAMPLE
3536
Get-ContextInfo -ID 'MySecret'
@@ -40,7 +41,7 @@
4041
Path : ...\3e223259-f242-4e97-91c8-f0fd054cfea7.json
4142
```
4243
43-
Retrieves the context called 'MySecret' from the context vault (in memory).
44+
Retrieves the context called 'MySecret' from the context vault (directly from disk).
4445
4546
.EXAMPLE
4647
'My*' | Get-ContextInfo
@@ -57,7 +58,7 @@
5758
Path : .../b7c01dbe-bccd-4c7e-b075-c5aac1c43b1a.json
5859
```
5960
60-
Retrieves all contexts that start with 'My' from the context vault (in memory).
61+
Retrieves all contexts that start with 'My' from the context vault (directly from disk).
6162
6263
.OUTPUTS
6364
[System.Object]
@@ -94,7 +95,22 @@
9495
process {
9596
Write-Verbose "Retrieving context info - ID: [$ID]"
9697
foreach ($item in $ID) {
97-
$script:Contexts.Values | Where-Object { $_.ID -like $item } | Select-Object -ExcludeProperty Context
98+
# Read context files directly from disk instead of using in-memory cache
99+
$contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse
100+
foreach ($file in $contextFiles) {
101+
try {
102+
$contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json
103+
if ($contextInfo.ID -like $item) {
104+
# Return only metadata (ID and Path), don't decrypt the Context property
105+
[PSCustomObject]@{
106+
ID = $contextInfo.ID
107+
Path = $contextInfo.Path
108+
}
109+
}
110+
} catch {
111+
Write-Warning "Failed to read context file: $($file.FullName). Error: $_"
112+
}
113+
}
98114
}
99115
}
100116

‎src/functions/public/Remove-Context.ps1

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,20 @@
9898
process {
9999
foreach ($item in $ID) {
100100
Write-Verbose "Processing ID [$item]"
101-
$script:Contexts.Keys | Where-Object { $_ -like $item } | ForEach-Object {
102-
Write-Verbose "Removing context [$_]"
103-
if ($PSCmdlet.ShouldProcess($_, 'Remove secret')) {
104-
$script:Contexts[$_].Path | Remove-Item -Force
105-
106-
Write-Verbose "Attempting to remove context: $_"
107-
[PSCustomObject]$removedItem = $null
108-
if ($script:Contexts.TryRemove($_, [ref]$removedItem)) {
109-
Write-Verbose "Removed item: $removedItem"
110-
} else {
111-
Write-Verbose 'Key not found'
101+
# Find contexts by scanning disk files instead of using in-memory cache
102+
$contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse
103+
foreach ($file in $contextFiles) {
104+
try {
105+
$contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json
106+
if ($contextInfo.ID -like $item) {
107+
Write-Verbose "Removing context [$($contextInfo.ID)]"
108+
if ($PSCmdlet.ShouldProcess($contextInfo.ID, 'Remove secret')) {
109+
$file.FullName | Remove-Item -Force
110+
Write-Verbose "Removed context file: $($file.FullName)"
111+
}
112112
}
113+
} catch {
114+
Write-Warning "Failed to read context file: $($file.FullName). Error: $_"
113115
}
114116
}
115117
}

‎src/functions/public/Set-Context.ps1

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.2' }
1+
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.0' }
22

33
function Set-Context {
44
<#
@@ -7,8 +7,8 @@ function Set-Context {
77
88
.DESCRIPTION
99
If the context does not exist, it will be created. If it already exists, it will be updated.
10-
The context is cached in memory for faster access. This function ensures that the context
11-
is securely stored using encryption mechanisms.
10+
The context is securely stored on disk using encryption mechanisms.
11+
Each context operation reads the current state from disk to ensure consistency across processes.
1212
1313
.EXAMPLE
1414
Set-Context -ID 'PSModule.GitHub' -Context @{ Name = 'MySecret' }
@@ -84,16 +84,30 @@ function Set-Context {
8484
$ID = $Context.ID
8585
}
8686
if (-not $ID) {
87-
throw "An ID is required, either as a parameter or as a property of the context object."
87+
throw 'An ID is required, either as a parameter or as a property of the context object.'
8888
}
89-
$existingContextInfo = $script:Contexts[$ID]
90-
if (-not $existingContextInfo) {
89+
$existingContextFile = $null
90+
# Check if context already exists by scanning disk files
91+
$contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse
92+
foreach ($file in $contextFiles) {
93+
try {
94+
$contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json
95+
if ($contextInfo.ID -eq $ID) {
96+
$existingContextFile = $file
97+
$Path = $contextInfo.Path
98+
break
99+
}
100+
} catch {
101+
Write-Warning "Failed to read context file: $($file.FullName). Error: $_"
102+
}
103+
}
104+
105+
if (-not $existingContextFile) {
91106
Write-Verbose "Context [$ID] not found in vault"
92107
$Guid = [Guid]::NewGuid().ToString()
93108
$Path = Join-Path -Path $script:Config.VaultPath -ChildPath "$Guid.json"
94109
} else {
95110
Write-Verbose "Context [$ID] found in vault"
96-
$Path = $existingContextInfo.Path
97111
}
98112

99113
$contextJson = ConvertTo-ContextJson -Context $Context -ID $ID
@@ -108,20 +122,6 @@ function Set-Context {
108122
if ($PSCmdlet.ShouldProcess($ID, 'Set context')) {
109123
Write-Verbose "Setting context [$ID] in vault"
110124
Set-Content -Path $Path -Value $param
111-
$content = Get-Content -Path $Path
112-
$contextInfoObj = $content | ConvertFrom-Json
113-
$params = @{
114-
SealedBox = $contextInfoObj.Context
115-
PublicKey = $script:Config.PublicKey
116-
PrivateKey = $script:Config.PrivateKey
117-
}
118-
$contextObj = ConvertFrom-SodiumSealedBox @params
119-
Write-Verbose ($contextObj | Format-List | Out-String)
120-
$script:Contexts[$ID] = [PSCustomObject]@{
121-
ID = $ID
122-
Path = $Path
123-
Context = ConvertFrom-ContextJson -JsonString $contextObj
124-
}
125125
}
126126

127127
if ($PassThru) {

‎src/variables/private/Config.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
$script:Config = [pscustomobject]@{
22
Initialized = $false # Has the vault been initialized?
33
VaultPath = Join-Path -Path $HOME -ChildPath '.contextvault' # Vault directory path
4-
SeedShardPath = 'vault.shard' # Seed shard path (relative to VaultPath)
4+
SeedShardPath = 'vault.shard' # Seed shard path (relative to VaultPath)
55
PrivateKey = $null # Private key (populated on init)
66
PublicKey = $null # Public key (populated on init)
77
}

‎src/variables/private/Contexts.ps1

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)