@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +101 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)][ValidateSet('Admit','Record','Lookup')][string]$Command,
|
|
3
|
+
[Parameter(Mandatory = $true)][string]$StoreRoot,
|
|
4
|
+
[Parameter(Mandatory = $true)][string]$ResultPipeName
|
|
5
|
+
)
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
8
|
+
$LedgerContract = 'hellolynk-native-human-confirmation-ledger-v1'
|
|
9
|
+
$StoreContract = 'hellolynk-human-confirmation-ledger-store-v1'
|
|
10
|
+
$CredentialName = 'hellolynk-human-confirmation-ledger-v1'
|
|
11
|
+
$MaxRecords = 4096
|
|
12
|
+
$RetentionMs = 86400000
|
|
13
|
+
|
|
14
|
+
Add-Type -TypeDefinition @'
|
|
15
|
+
using System;
|
|
16
|
+
using System.Runtime.InteropServices;
|
|
17
|
+
using System.Text;
|
|
18
|
+
using Microsoft.Win32.SafeHandles;
|
|
19
|
+
public static class HelloLynkLedgerNativePath {
|
|
20
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
21
|
+
public static extern SafeFileHandle CreateFile(
|
|
22
|
+
string name, uint access, uint share, IntPtr security, uint creation,
|
|
23
|
+
uint flags, IntPtr template);
|
|
24
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
25
|
+
public static extern uint GetFinalPathNameByHandle(
|
|
26
|
+
SafeFileHandle handle, StringBuilder path, uint length, uint flags);
|
|
27
|
+
}
|
|
28
|
+
'@
|
|
29
|
+
|
|
30
|
+
function Fail([string]$Code) { [Console]::Error.WriteLine($Code); exit 1 }
|
|
31
|
+
function Exact($Value, [string[]]$Fields, [string]$Code) {
|
|
32
|
+
if ($null -eq $Value -or $Value -is [Array]) { Fail $Code }
|
|
33
|
+
$actual = @($Value.PSObject.Properties.Name | Sort-Object); $wanted = @($Fields | Sort-Object)
|
|
34
|
+
if ($actual.Count -ne $wanted.Count) { Fail $Code }
|
|
35
|
+
for ($i=0; $i -lt $wanted.Count; $i++) { if ($actual[$i] -cne $wanted[$i]) { Fail $Code } }
|
|
36
|
+
}
|
|
37
|
+
function Valid-Action([string]$Value) { return $Value -match '^human_[A-Za-z0-9_-]{16,96}$' }
|
|
38
|
+
function Valid-Digest([string]$Value) { return $Value -match '^[a-f0-9]{64}$' }
|
|
39
|
+
function Valid-Time($Value) { try { $n=[int64]$Value; return $n -ge 0 } catch { return $false } }
|
|
40
|
+
function Assert-Admission($v) {
|
|
41
|
+
Exact $v @('contract','actionId','partitionDigest','createdAtMs','expiresAtMs','rateWindowMs','rateLimit','restartTombstoneState','restartTombstoneCode') 'human_confirmation_ledger_input_invalid'
|
|
42
|
+
if ([string]$v.contract -cne $LedgerContract -or -not (Valid-Action $v.actionId) -or -not (Valid-Digest $v.partitionDigest) -or
|
|
43
|
+
-not (Valid-Time $v.createdAtMs) -or -not (Valid-Time $v.expiresAtMs) -or [int64]$v.expiresAtMs -le [int64]$v.createdAtMs -or
|
|
44
|
+
[int64]$v.rateWindowMs -ne 60000 -or [int64]$v.rateLimit -ne 3 -or [string]$v.restartTombstoneState -cne 'invalidated' -or
|
|
45
|
+
[string]$v.restartTombstoneCode -cne 'human_confirmation_restart_invalidated') { Fail 'human_confirmation_ledger_input_invalid' }
|
|
46
|
+
}
|
|
47
|
+
function Assert-Record($v) {
|
|
48
|
+
Exact $v @('contract','actionId','partitionDigest','state','code','createdAtMs','expiresAtMs','recordedAtMs') 'human_confirmation_ledger_input_invalid'
|
|
49
|
+
if ([string]$v.contract -cne $LedgerContract -or -not (Valid-Action $v.actionId) -or -not (Valid-Digest $v.partitionDigest) -or
|
|
50
|
+
@('approved','cancelled','consuming','expired','failed','invalidated') -cnotcontains [string]$v.state -or
|
|
51
|
+
($null -ne $v.code -and [string]$v.code -notmatch '^human_confirmation_[a-z0-9_]{1,100}$') -or
|
|
52
|
+
([string]$v.state -ceq 'consuming' -and $null -ne $v.code) -or -not (Valid-Time $v.createdAtMs) -or
|
|
53
|
+
-not (Valid-Time $v.expiresAtMs) -or -not (Valid-Time $v.recordedAtMs) -or [int64]$v.expiresAtMs -le [int64]$v.createdAtMs -or
|
|
54
|
+
[int64]$v.recordedAtMs -lt [int64]$v.createdAtMs) { Fail 'human_confirmation_ledger_input_invalid' }
|
|
55
|
+
}
|
|
56
|
+
function Assert-Lookup($v) {
|
|
57
|
+
Exact $v @('contract','actionId','partitionDigest') 'human_confirmation_ledger_input_invalid'
|
|
58
|
+
if ([string]$v.contract -cne $LedgerContract -or -not (Valid-Action $v.actionId) -or -not (Valid-Digest $v.partitionDigest)) { Fail 'human_confirmation_ledger_input_invalid' }
|
|
59
|
+
}
|
|
60
|
+
function Assert-Entry($v) {
|
|
61
|
+
Exact $v @('actionId','partitionDigest','createdAtMs','expiresAtMs','admittedAtMs','pending','state','code','recordedAtMs') 'human_confirmation_ledger_corrupt'
|
|
62
|
+
if (-not (Valid-Action $v.actionId) -or -not (Valid-Digest $v.partitionDigest) -or -not (Valid-Time $v.createdAtMs) -or
|
|
63
|
+
-not (Valid-Time $v.expiresAtMs) -or -not (Valid-Time $v.admittedAtMs) -or [int64]$v.expiresAtMs -le [int64]$v.createdAtMs -or $v.pending -isnot [bool] -or
|
|
64
|
+
@('approved','cancelled','consuming','expired','failed','invalidated') -cnotcontains [string]$v.state -or
|
|
65
|
+
($null -ne $v.code -and [string]$v.code -notmatch '^human_confirmation_[a-z0-9_]{1,100}$') -or -not (Valid-Time $v.recordedAtMs)) { Fail 'human_confirmation_ledger_corrupt' }
|
|
66
|
+
}
|
|
67
|
+
function Read-Store([string]$Path) {
|
|
68
|
+
if (-not (Test-Path -LiteralPath $Path)) { return [PSCustomObject]@{ contract=$StoreContract; records=@() } }
|
|
69
|
+
$item=Get-Item -LiteralPath $Path -Force
|
|
70
|
+
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or $item.Length -gt 1048576) { Fail 'human_confirmation_ledger_path_invalid' }
|
|
71
|
+
try { $cred=Import-Clixml -LiteralPath $Path; $plain=$cred.GetNetworkCredential().Password; $s=$plain|ConvertFrom-Json } catch { Fail 'human_confirmation_ledger_corrupt' }
|
|
72
|
+
Exact $s @('contract','records') 'human_confirmation_ledger_corrupt'
|
|
73
|
+
if ([string]$s.contract -cne $StoreContract -or $s.records -isnot [Array] -or $s.records.Count -gt $MaxRecords) { Fail 'human_confirmation_ledger_corrupt' }
|
|
74
|
+
$seen=@{}; foreach($r in $s.records) { Assert-Entry $r; if($seen.ContainsKey([string]$r.actionId)){Fail 'human_confirmation_ledger_corrupt'}; $seen[[string]$r.actionId]=$true }
|
|
75
|
+
$plain=$null; return $s
|
|
76
|
+
}
|
|
77
|
+
function Write-Store([string]$Path,$Store,[string]$Root,[string]$Sid) {
|
|
78
|
+
$plain=$Store|ConvertTo-Json -Depth 6 -Compress
|
|
79
|
+
if([Text.Encoding]::UTF8.GetByteCount($plain)-gt 1048576){Fail 'human_confirmation_ledger_too_large'}
|
|
80
|
+
$secure=ConvertTo-SecureString -String $plain -AsPlainText -Force; $cred=[Management.Automation.PSCredential]::new($CredentialName,$secure)
|
|
81
|
+
$temp=Join-Path $Root ('.ledger.{0}.tmp' -f [Guid]::NewGuid().ToString('N'))
|
|
82
|
+
try {
|
|
83
|
+
$cred|Export-Clixml -LiteralPath $temp -Force
|
|
84
|
+
& icacls.exe $temp '/inheritance:r' '/grant:r' "*$Sid`:F" | Out-Null; if($LASTEXITCODE-ne 0){Fail 'human_confirmation_ledger_acl_failed'}
|
|
85
|
+
$f=[IO.File]::Open($temp,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::Read); try{$f.Flush($true)}finally{$f.Dispose()}
|
|
86
|
+
for($a=0;;$a++){try{Move-Item -LiteralPath $temp -Destination $Path -Force;break}catch [IO.IOException]{if($a-ge 20){throw};Start-Sleep -Milliseconds (5*($a+1))}catch [UnauthorizedAccessException]{if($a-ge 20){throw};Start-Sleep -Milliseconds (5*($a+1))}}
|
|
87
|
+
} finally { if(Test-Path -LiteralPath $temp){Remove-Item -LiteralPath $temp -Force}; $plain=$null;$secure=$null;$cred=$null }
|
|
88
|
+
}
|
|
89
|
+
function Write-Result($v) {
|
|
90
|
+
if($ResultPipeName -notmatch '^HelloLynkHumanLedger-[A-Za-z0-9_-]{16,96}$'){Fail 'human_confirmation_ledger_pipe_invalid'}
|
|
91
|
+
$json=$v|ConvertTo-Json -Depth 6 -Compress
|
|
92
|
+
$opts=[IO.Pipes.PipeOptions]::Asynchronous -bor [IO.Pipes.PipeOptions]::CurrentUserOnly
|
|
93
|
+
$pipe=[IO.Pipes.NamedPipeServerStream]::new($ResultPipeName,[IO.Pipes.PipeDirection]::Out,1,[IO.Pipes.PipeTransmissionMode]::Byte,$opts)
|
|
94
|
+
try{$pipe.WaitForConnection();$w=[IO.StreamWriter]::new($pipe,[Text.UTF8Encoding]::new($false),1024,$true);try{$w.Write($json);$w.Flush()}finally{$w.Dispose()}}finally{$pipe.Dispose();$json=$null}
|
|
95
|
+
}
|
|
96
|
+
function Assert-NoReparse([string]$Path) {
|
|
97
|
+
$cur=[IO.Path]::GetFullPath($Path)
|
|
98
|
+
while($cur){if(Test-Path -LiteralPath $cur){$it=Get-Item -LiteralPath $cur -Force;if(($it.Attributes-band [IO.FileAttributes]::ReparsePoint)-ne 0){Fail 'human_confirmation_ledger_path_invalid'}};$parent=[IO.Directory]::GetParent($cur);if($null-eq $parent){break};$cur=$parent.FullName}
|
|
99
|
+
}
|
|
100
|
+
function Get-CanonicalLocalRoot([string]$Path) {
|
|
101
|
+
$handle=[HelloLynkLedgerNativePath]::CreateFile($Path,0,7,[IntPtr]::Zero,3,0x02000000,[IntPtr]::Zero)
|
|
102
|
+
if($handle.IsInvalid){$handle.Dispose();Fail 'human_confirmation_ledger_root_invalid'}
|
|
103
|
+
try {
|
|
104
|
+
$buffer=[Text.StringBuilder]::new(32768)
|
|
105
|
+
$length=[HelloLynkLedgerNativePath]::GetFinalPathNameByHandle($handle,$buffer,$buffer.Capacity,0)
|
|
106
|
+
if($length-eq 0 -or $length-ge $buffer.Capacity){Fail 'human_confirmation_ledger_root_invalid'}
|
|
107
|
+
$final=$buffer.ToString()
|
|
108
|
+
} finally {$handle.Dispose()}
|
|
109
|
+
if($final.StartsWith('\\?\UNC\',[StringComparison]::OrdinalIgnoreCase)){Fail 'human_confirmation_ledger_root_invalid'}
|
|
110
|
+
if($final.StartsWith('\\?\')){$final=$final.Substring(4)}
|
|
111
|
+
$final=[IO.Path]::GetFullPath($final)
|
|
112
|
+
if($final -notmatch '^[A-Za-z]:\\' -or $final.Length-lt 4){Fail 'human_confirmation_ledger_root_invalid'}
|
|
113
|
+
return $final.TrimEnd([IO.Path]::DirectorySeparatorChar)
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
if($StoreRoot -notmatch '^[A-Za-z]:[\\/]' -or $StoreRoot.StartsWith('\\') -or $StoreRoot.Contains([char]0)){Fail 'human_confirmation_ledger_root_invalid'}
|
|
117
|
+
$root=[IO.Path]::GetFullPath($StoreRoot);if($root -notmatch '^[A-Za-z]:\\' -or $root.Length-lt 4){Fail 'human_confirmation_ledger_root_invalid'}
|
|
118
|
+
try{$drive=[IO.DriveInfo]::new([IO.Path]::GetPathRoot($root))}catch{Fail 'human_confirmation_ledger_root_invalid'}
|
|
119
|
+
if($drive.DriveType-ne [IO.DriveType]::Fixed){Fail 'human_confirmation_ledger_root_invalid'}
|
|
120
|
+
Assert-NoReparse $root; New-Item -ItemType Directory -Path $root -Force|Out-Null; Assert-NoReparse $root
|
|
121
|
+
$root=Get-CanonicalLocalRoot $root; Assert-NoReparse $root
|
|
122
|
+
$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
123
|
+
& icacls.exe $root '/inheritance:r' '/grant:r' "*$Sid`:(OI)(CI)F" | Out-Null; if($LASTEXITCODE-ne 0){Fail 'human_confirmation_ledger_acl_failed'}
|
|
124
|
+
$path=Join-Path $root 'human-confirmation-ledger-v1.dpapi.clixml'; Assert-NoReparse $path
|
|
125
|
+
$mutexScope=('{0}`0{1}' -f $sid,$root.TrimEnd([IO.Path]::DirectorySeparatorChar).ToUpperInvariant())
|
|
126
|
+
$scopeHash=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($mutexScope))).Substring(0,32)
|
|
127
|
+
$mutex=[Threading.Mutex]::new($false,"Local\HelloLynkHumanConfirmationLedger-$scopeHash");$held=$false
|
|
128
|
+
try { try{$held=$mutex.WaitOne(30000)}catch [Threading.AbandonedMutexException]{$held=$true};if(-not $held){Fail 'human_confirmation_ledger_lock_timeout'}
|
|
129
|
+
Assert-NoReparse $root; Assert-NoReparse $path
|
|
130
|
+
if(Test-Path -LiteralPath $path -PathType Leaf){& icacls.exe $path '/inheritance:r' '/grant:r' "*$Sid`:F" | Out-Null;if($LASTEXITCODE-ne 0){Fail 'human_confirmation_ledger_acl_failed'}}
|
|
131
|
+
$store=Read-Store $path
|
|
132
|
+
$raw=[Console]::In.ReadToEnd();if([Text.Encoding]::UTF8.GetByteCount($raw)-gt 16384){Fail 'human_confirmation_ledger_input_too_large'};try{$request=$raw|ConvertFrom-Json}catch{Fail 'human_confirmation_ledger_input_invalid'};$raw=$null
|
|
133
|
+
$now=[DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
134
|
+
$store.records=@($store.records|Where-Object{[int64]$_.admittedAtMs-ge ($now-$RetentionMs) -or ([bool]$_.pending -and [int64]$_.expiresAtMs-gt $now)})
|
|
135
|
+
switch($Command){
|
|
136
|
+
'Admit' { Assert-Admission $request; $same=@($store.records|Where-Object actionId -CEQ $request.actionId)
|
|
137
|
+
if($same.Count){$r=$same[0];if([string]$r.partitionDigest-cne [string]$request.partitionDigest -or [int64]$r.createdAtMs-ne [int64]$request.createdAtMs -or [int64]$r.expiresAtMs-ne [int64]$request.expiresAtMs){Fail 'human_confirmation_ledger_action_conflict'};Write-Result ([PSCustomObject]@{accepted=$true});break}
|
|
138
|
+
$pending=@($store.records|Where-Object{[string]$_.partitionDigest-ceq [string]$request.partitionDigest -and [bool]$_.pending -and [int64]$_.expiresAtMs-gt [int64]$request.createdAtMs})
|
|
139
|
+
if($pending.Count){Write-Result ([PSCustomObject]@{accepted=$false;reason='partition_pending'});break}
|
|
140
|
+
$recent=@($store.records|Where-Object{[string]$_.partitionDigest-ceq [string]$request.partitionDigest -and [int64]$_.admittedAtMs-gt ($now-[int64]$request.rateWindowMs)}|Sort-Object admittedAtMs)
|
|
141
|
+
if($recent.Count-ge [int64]$request.rateLimit){$retry=[Math]::Max(0,[int64]$recent[0].admittedAtMs+[int64]$request.rateWindowMs-$now);Write-Result ([PSCustomObject]@{accepted=$false;reason='rate_limited';retryAfterMs=$retry});break}
|
|
142
|
+
if($store.records.Count-ge $MaxRecords){Fail 'human_confirmation_ledger_capacity_exceeded'}
|
|
143
|
+
$entry=[PSCustomObject]@{actionId=[string]$request.actionId;partitionDigest=[string]$request.partitionDigest;createdAtMs=[int64]$request.createdAtMs;expiresAtMs=[int64]$request.expiresAtMs;admittedAtMs=$now;pending=$true;state='invalidated';code='human_confirmation_restart_invalidated';recordedAtMs=[int64]$request.createdAtMs}
|
|
144
|
+
$store.records=@($store.records)+@($entry);Write-Store $path $store $root $sid;Write-Result ([PSCustomObject]@{accepted=$true})
|
|
145
|
+
}
|
|
146
|
+
'Record' { Assert-Record $request;$same=@($store.records|Where-Object actionId -CEQ $request.actionId);if($same.Count-ne 1){Fail 'human_confirmation_ledger_action_missing'};$r=$same[0]
|
|
147
|
+
if([string]$r.partitionDigest-cne [string]$request.partitionDigest -or [int64]$r.createdAtMs-ne [int64]$request.createdAtMs -or [int64]$r.expiresAtMs-ne [int64]$request.expiresAtMs){Fail 'human_confirmation_ledger_action_conflict'}
|
|
148
|
+
$identical=([string]$r.state-ceq [string]$request.state -and (($null-eq $r.code -and $null-eq $request.code)-or [string]$r.code-ceq [string]$request.code) -and [int64]$r.recordedAtMs-eq [int64]$request.recordedAtMs)
|
|
149
|
+
if(-not $identical){$allowed=((([bool]$r.pending -and [string]$r.state-ceq 'invalidated' -and @('consuming','cancelled','expired','failed','invalidated') -ccontains [string]$request.state)) -or ([string]$r.state-ceq 'consuming' -and @('approved','failed') -ccontains [string]$request.state)) -and [int64]$request.recordedAtMs-ge [int64]$r.recordedAtMs;if(-not $allowed){Fail 'human_confirmation_ledger_state_regression'};$r.state=[string]$request.state;$r.code=$request.code;$r.recordedAtMs=[int64]$request.recordedAtMs;$r.pending=$false;Write-Store $path $store $root $sid}
|
|
150
|
+
Write-Result ([PSCustomObject]@{recorded=$true})
|
|
151
|
+
}
|
|
152
|
+
'Lookup' { Assert-Lookup $request;$same=@($store.records|Where-Object{[string]$_.actionId-ceq [string]$request.actionId -and [string]$_.partitionDigest-ceq [string]$request.partitionDigest});if(-not $same.Count){Write-Result ([PSCustomObject]@{record=$null})}else{$r=$same[0];Write-Result ([PSCustomObject]@{record=[PSCustomObject]@{actionId=$r.actionId;state=$r.state;code=$r.code;createdAtMs=$r.createdAtMs;expiresAtMs=$r.expiresAtMs}})}}
|
|
153
|
+
}
|
|
154
|
+
} finally {if($held){$mutex.ReleaseMutex()};$mutex.Dispose()}
|
|
155
|
+
} catch { $c=[string]$_.Exception.Message;if($c-match '^human_confirmation_ledger_[a-z0-9_]{1,90}$'){Fail $c};if($_.Exception-is [IO.IOException]){Fail 'human_confirmation_ledger_io_failed'};if($_.Exception-is [UnauthorizedAccessException]){Fail 'human_confirmation_ledger_access_failed'};Fail 'human_confirmation_ledger_operation_failed' }
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)]
|
|
3
|
+
[ValidateSet("CreateOrLoad", "Load")]
|
|
4
|
+
[string]$Command,
|
|
5
|
+
|
|
6
|
+
[Parameter(Mandatory = $true)]
|
|
7
|
+
[string]$StoreRoot,
|
|
8
|
+
|
|
9
|
+
[Parameter(Mandatory = $true)]
|
|
10
|
+
[string]$ResultPipeName
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
$ErrorActionPreference = "Stop"
|
|
14
|
+
$ProgressPreference = "SilentlyContinue"
|
|
15
|
+
Set-StrictMode -Version Latest
|
|
16
|
+
|
|
17
|
+
$StoreContract = "hellolynk-local-data-key-store-v1"
|
|
18
|
+
$RecordContract = "hellolynk-local-data-key-v1"
|
|
19
|
+
$KeyIdPattern = '^ldk_[A-Za-z0-9_-]{22}$'
|
|
20
|
+
$Entropy = [Text.Encoding]::UTF8.GetBytes("HelloLynk local data key v1")
|
|
21
|
+
|
|
22
|
+
function Fail([string]$Code) {
|
|
23
|
+
[Console]::Error.WriteLine($Code)
|
|
24
|
+
exit 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Assert-ExactFields($Value, [string[]]$Expected, [string]$Code) {
|
|
28
|
+
if ($null -eq $Value -or $Value -is [Array]) { Fail $Code }
|
|
29
|
+
$actual = @($Value.PSObject.Properties.Name | Sort-Object)
|
|
30
|
+
$wanted = @($Expected | Sort-Object)
|
|
31
|
+
if ($actual.Count -ne $wanted.Count) { Fail $Code }
|
|
32
|
+
for ($index = 0; $index -lt $wanted.Count; $index += 1) {
|
|
33
|
+
if ([string]$actual[$index] -cne [string]$wanted[$index]) { Fail $Code }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ConvertTo-Base64Url([byte[]]$Bytes) {
|
|
38
|
+
return [Convert]::ToBase64String($Bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Set-CurrentUserOnlyAcl([string]$Path, [string]$Sid, [bool]$Container) {
|
|
42
|
+
$acl = Get-Acl -LiteralPath $Path
|
|
43
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
44
|
+
foreach ($rule in @($acl.Access)) { $null = $acl.RemoveAccessRuleAll($rule) }
|
|
45
|
+
$inheritance = if ($Container) {
|
|
46
|
+
[Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
47
|
+
[Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
48
|
+
} else {
|
|
49
|
+
[Security.AccessControl.InheritanceFlags]::None
|
|
50
|
+
}
|
|
51
|
+
$propagation = [Security.AccessControl.PropagationFlags]::None
|
|
52
|
+
$allow = [Security.AccessControl.AccessControlType]::Allow
|
|
53
|
+
$rights = [Security.AccessControl.FileSystemRights]::FullControl
|
|
54
|
+
$currentIdentity = [Security.Principal.SecurityIdentifier]::new($Sid)
|
|
55
|
+
$systemIdentity = [Security.Principal.SecurityIdentifier]::new("S-1-5-18")
|
|
56
|
+
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
|
57
|
+
$currentIdentity, $rights, $inheritance, $propagation, $allow
|
|
58
|
+
))
|
|
59
|
+
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
|
60
|
+
$systemIdentity, $rights, $inheritance, $propagation, $allow
|
|
61
|
+
))
|
|
62
|
+
Set-Acl -LiteralPath $Path -AclObject $acl
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function Assert-CurrentUserOnlyAcl([string]$Path, [string]$Sid, [bool]$Container, [string]$Code) {
|
|
66
|
+
try { $acl = Get-Acl -LiteralPath $Path }
|
|
67
|
+
catch { Fail $Code }
|
|
68
|
+
if (-not $acl.AreAccessRulesProtected) { Fail $Code }
|
|
69
|
+
$expectedInheritance = if ($Container) {
|
|
70
|
+
[Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
71
|
+
[Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
72
|
+
} else {
|
|
73
|
+
[Security.AccessControl.InheritanceFlags]::None
|
|
74
|
+
}
|
|
75
|
+
$expected = @($Sid, "S-1-5-18" | Sort-Object)
|
|
76
|
+
$observed = @()
|
|
77
|
+
foreach ($rule in @($acl.Access)) {
|
|
78
|
+
if ($rule.IsInherited -or
|
|
79
|
+
$rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow -or
|
|
80
|
+
$rule.InheritanceFlags -ne $expectedInheritance -or
|
|
81
|
+
$rule.PropagationFlags -ne [Security.AccessControl.PropagationFlags]::None -or
|
|
82
|
+
(($rule.FileSystemRights -band [Security.AccessControl.FileSystemRights]::FullControl) -ne
|
|
83
|
+
[Security.AccessControl.FileSystemRights]::FullControl)) {
|
|
84
|
+
Fail $Code
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
$ruleSid = $rule.IdentityReference.Translate(
|
|
88
|
+
[Security.Principal.SecurityIdentifier]
|
|
89
|
+
).Value
|
|
90
|
+
} catch { Fail $Code }
|
|
91
|
+
$observed += [string]$ruleSid
|
|
92
|
+
}
|
|
93
|
+
$observed = @($observed | Sort-Object)
|
|
94
|
+
if ($observed.Count -ne $expected.Count) { Fail $Code }
|
|
95
|
+
for ($index = 0; $index -lt $expected.Count; $index += 1) {
|
|
96
|
+
if ([string]$observed[$index] -cne [string]$expected[$index]) { Fail $Code }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function Assert-RegularLocalPath([string]$Path, [bool]$Container, [string]$Code) {
|
|
101
|
+
$item = Get-Item -LiteralPath $Path -Force
|
|
102
|
+
if ($Container -and -not $item.PSIsContainer) { Fail $Code }
|
|
103
|
+
if (-not $Container -and $item.PSIsContainer) { Fail $Code }
|
|
104
|
+
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Fail $Code }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function New-KeyRecord {
|
|
108
|
+
$key = [byte[]]::new(32)
|
|
109
|
+
$identifier = [byte[]]::new(16)
|
|
110
|
+
[Security.Cryptography.RandomNumberGenerator]::Fill($key)
|
|
111
|
+
[Security.Cryptography.RandomNumberGenerator]::Fill($identifier)
|
|
112
|
+
$record = [PSCustomObject]@{
|
|
113
|
+
contract = $RecordContract
|
|
114
|
+
keyId = "ldk_$(ConvertTo-Base64Url $identifier)"
|
|
115
|
+
key = ConvertTo-Base64Url $key
|
|
116
|
+
}
|
|
117
|
+
[Array]::Clear($key, 0, $key.Length)
|
|
118
|
+
[Array]::Clear($identifier, 0, $identifier.Length)
|
|
119
|
+
return $record
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function Assert-KeyRecord($Record) {
|
|
123
|
+
Assert-ExactFields $Record @("contract", "key", "keyId") "local_data_key_record_invalid"
|
|
124
|
+
if ([string]$Record.contract -cne $RecordContract) { Fail "local_data_key_record_invalid" }
|
|
125
|
+
if ([string]$Record.keyId -notmatch $KeyIdPattern) { Fail "local_data_key_record_invalid" }
|
|
126
|
+
$encoded = ([string]$Record.key).Replace('-', '+').Replace('_', '/')
|
|
127
|
+
while (($encoded.Length % 4) -ne 0) { $encoded += '=' }
|
|
128
|
+
try { $key = [Convert]::FromBase64String($encoded) }
|
|
129
|
+
catch { Fail "local_data_key_record_invalid" }
|
|
130
|
+
if ($key.Length -ne 32 -or (ConvertTo-Base64Url $key) -cne [string]$Record.key) {
|
|
131
|
+
Fail "local_data_key_record_invalid"
|
|
132
|
+
}
|
|
133
|
+
[Array]::Clear($key, 0, $key.Length)
|
|
134
|
+
$key = $null
|
|
135
|
+
$encoded = $null
|
|
136
|
+
return $Record
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Write-KeyStore([string]$Path, [string]$Root, [string]$Sid, $Record) {
|
|
140
|
+
$plainBytes = $null
|
|
141
|
+
$protected = $null
|
|
142
|
+
$json = $null
|
|
143
|
+
$temp = $null
|
|
144
|
+
$plain = $Record | ConvertTo-Json -Depth 3 -Compress
|
|
145
|
+
$plainBytes = [Text.Encoding]::UTF8.GetBytes($plain)
|
|
146
|
+
try {
|
|
147
|
+
$protected = [Security.Cryptography.ProtectedData]::Protect(
|
|
148
|
+
$plainBytes,
|
|
149
|
+
$Entropy,
|
|
150
|
+
[Security.Cryptography.DataProtectionScope]::CurrentUser
|
|
151
|
+
)
|
|
152
|
+
$store = [PSCustomObject]@{
|
|
153
|
+
contract = $StoreContract
|
|
154
|
+
schemaVersion = 1
|
|
155
|
+
protectedPayload = [Convert]::ToBase64String($protected)
|
|
156
|
+
}
|
|
157
|
+
$json = $store | ConvertTo-Json -Depth 3 -Compress
|
|
158
|
+
$temp = Join-Path $Root (".local-data-key.{0}.tmp" -f [Guid]::NewGuid().ToString("N"))
|
|
159
|
+
try {
|
|
160
|
+
[IO.File]::WriteAllText($temp, $json, [Text.UTF8Encoding]::new($false))
|
|
161
|
+
Set-CurrentUserOnlyAcl $temp $Sid $false
|
|
162
|
+
$stream = [IO.File]::Open($temp, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
|
|
163
|
+
try { $stream.Flush($true) } finally { $stream.Dispose() }
|
|
164
|
+
Move-Item -LiteralPath $temp -Destination $Path -Force
|
|
165
|
+
Set-CurrentUserOnlyAcl $Path $Sid $false
|
|
166
|
+
} finally {
|
|
167
|
+
if ($temp -and (Test-Path -LiteralPath $temp)) { Remove-Item -LiteralPath $temp -Force }
|
|
168
|
+
}
|
|
169
|
+
} finally {
|
|
170
|
+
if ($plainBytes) { [Array]::Clear($plainBytes, 0, $plainBytes.Length) }
|
|
171
|
+
if ($protected) { [Array]::Clear($protected, 0, $protected.Length) }
|
|
172
|
+
$plain = $null
|
|
173
|
+
$plainBytes = $null
|
|
174
|
+
$protected = $null
|
|
175
|
+
$json = $null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function Read-KeyStore([string]$Path, [string]$Sid) {
|
|
180
|
+
$protected = $null
|
|
181
|
+
$plainBytes = $null
|
|
182
|
+
$plain = $null
|
|
183
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { Fail "local_data_key_missing" }
|
|
184
|
+
Assert-RegularLocalPath $Path $false "local_data_key_store_invalid"
|
|
185
|
+
try { $store = [IO.File]::ReadAllText($Path, [Text.Encoding]::UTF8) | ConvertFrom-Json }
|
|
186
|
+
catch { Fail "local_data_key_store_invalid" }
|
|
187
|
+
Assert-ExactFields $store @("contract", "protectedPayload", "schemaVersion") "local_data_key_store_invalid"
|
|
188
|
+
if ([int]$store.schemaVersion -ne 1) {
|
|
189
|
+
Fail "local_data_key_store_invalid"
|
|
190
|
+
}
|
|
191
|
+
if ([string]$store.contract -cne $StoreContract) { Fail "local_data_key_store_invalid" }
|
|
192
|
+
Assert-CurrentUserOnlyAcl $Path $Sid $false "local_data_key_access_failed"
|
|
193
|
+
try {
|
|
194
|
+
$protected = [Convert]::FromBase64String([string]$store.protectedPayload)
|
|
195
|
+
$plainBytes = [Security.Cryptography.ProtectedData]::Unprotect(
|
|
196
|
+
$protected,
|
|
197
|
+
$Entropy,
|
|
198
|
+
[Security.Cryptography.DataProtectionScope]::CurrentUser
|
|
199
|
+
)
|
|
200
|
+
$plain = [Text.Encoding]::UTF8.GetString($plainBytes)
|
|
201
|
+
return Assert-KeyRecord ($plain | ConvertFrom-Json)
|
|
202
|
+
} catch {
|
|
203
|
+
$candidate = [string]$_.Exception.Message
|
|
204
|
+
if ($candidate -match '^local_data_[a-z0-9_]{2,127}$') { Fail $candidate }
|
|
205
|
+
Fail "local_data_key_unprotect_failed"
|
|
206
|
+
} finally {
|
|
207
|
+
if ($protected) { [Array]::Clear($protected, 0, $protected.Length) }
|
|
208
|
+
if ($plainBytes) { [Array]::Clear($plainBytes, 0, $plainBytes.Length) }
|
|
209
|
+
$protected = $null
|
|
210
|
+
$plainBytes = $null
|
|
211
|
+
$plain = $null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function Write-Result($Record) {
|
|
216
|
+
if ($ResultPipeName -notmatch '^HelloLynkLocalDataKey-[A-Za-z0-9_-]{16,96}$') {
|
|
217
|
+
Fail "local_data_key_result_pipe_invalid"
|
|
218
|
+
}
|
|
219
|
+
$json = $Record | ConvertTo-Json -Depth 3 -Compress
|
|
220
|
+
$pipeOptions = [IO.Pipes.PipeOptions]::Asynchronous -bor [IO.Pipes.PipeOptions]::CurrentUserOnly
|
|
221
|
+
$pipe = [IO.Pipes.NamedPipeServerStream]::new(
|
|
222
|
+
$ResultPipeName,
|
|
223
|
+
[IO.Pipes.PipeDirection]::Out,
|
|
224
|
+
1,
|
|
225
|
+
[IO.Pipes.PipeTransmissionMode]::Byte,
|
|
226
|
+
$pipeOptions
|
|
227
|
+
)
|
|
228
|
+
try {
|
|
229
|
+
$pipe.WaitForConnection()
|
|
230
|
+
$writer = [IO.StreamWriter]::new($pipe, [Text.UTF8Encoding]::new($false), 1024, $true)
|
|
231
|
+
try { $writer.Write($json); $writer.Flush() } finally { $writer.Dispose() }
|
|
232
|
+
} finally {
|
|
233
|
+
$pipe.Dispose()
|
|
234
|
+
$json = $null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
if (-not $StoreRoot -or $StoreRoot.Contains([char]0) -or $StoreRoot.StartsWith('\\')) {
|
|
240
|
+
Fail "local_data_key_root_invalid"
|
|
241
|
+
}
|
|
242
|
+
try { $root = [IO.Path]::GetFullPath($StoreRoot) } catch { Fail "local_data_key_root_invalid" }
|
|
243
|
+
if ($root -notmatch '^[A-Za-z]:\\' -or $root.Length -lt 4) { Fail "local_data_key_root_invalid" }
|
|
244
|
+
try { $drive = [IO.DriveInfo]::new([IO.Path]::GetPathRoot($root)) }
|
|
245
|
+
catch { Fail "local_data_key_root_invalid" }
|
|
246
|
+
if ($drive.DriveType -ne [IO.DriveType]::Fixed) { Fail "local_data_key_root_invalid" }
|
|
247
|
+
|
|
248
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
249
|
+
if ($Command -ceq "CreateOrLoad") {
|
|
250
|
+
$null = New-Item -ItemType Directory -Path $root -Force
|
|
251
|
+
Assert-RegularLocalPath $root $true "local_data_key_root_invalid"
|
|
252
|
+
Set-CurrentUserOnlyAcl $root $sid $true
|
|
253
|
+
} else {
|
|
254
|
+
if (-not (Test-Path -LiteralPath $root -PathType Container)) { Fail "local_data_key_missing" }
|
|
255
|
+
Assert-RegularLocalPath $root $true "local_data_key_root_invalid"
|
|
256
|
+
}
|
|
257
|
+
$path = Join-Path $root "local-data-key-v1.dpapi.json"
|
|
258
|
+
$rootPrefix = $root.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
|
|
259
|
+
if (-not [IO.Path]::GetFullPath($path).StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
260
|
+
Fail "local_data_key_store_invalid"
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
$mutexInput = "${sid}`0$($root.ToUpperInvariant())"
|
|
264
|
+
$mutexHash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData(
|
|
265
|
+
[Text.Encoding]::UTF8.GetBytes($mutexInput)
|
|
266
|
+
)).Substring(0, 32)
|
|
267
|
+
$mutex = [Threading.Mutex]::new($false, "Local\HelloLynkLocalDataKey-$mutexHash")
|
|
268
|
+
$acquired = $false
|
|
269
|
+
try {
|
|
270
|
+
try { $acquired = $mutex.WaitOne(10000) }
|
|
271
|
+
catch [Threading.AbandonedMutexException] { $acquired = $true }
|
|
272
|
+
if (-not $acquired) { Fail "local_data_key_lock_timeout" }
|
|
273
|
+
if (Test-Path -LiteralPath $path) {
|
|
274
|
+
$record = Read-KeyStore $path $sid
|
|
275
|
+
if ($Command -ceq "Load") {
|
|
276
|
+
Assert-CurrentUserOnlyAcl $root $sid $true "local_data_key_access_failed"
|
|
277
|
+
}
|
|
278
|
+
} elseif ($Command -ceq "CreateOrLoad") {
|
|
279
|
+
$record = New-KeyRecord
|
|
280
|
+
$null = Assert-KeyRecord $record
|
|
281
|
+
Write-KeyStore $path $root $sid $record
|
|
282
|
+
} else {
|
|
283
|
+
Fail "local_data_key_missing"
|
|
284
|
+
}
|
|
285
|
+
Write-Result $record
|
|
286
|
+
} finally {
|
|
287
|
+
if ($acquired) { $mutex.ReleaseMutex() }
|
|
288
|
+
$mutex.Dispose()
|
|
289
|
+
$record = $null
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
$candidate = [string]$_.Exception.Message
|
|
293
|
+
if ($candidate -match '^[a-z][a-z0-9_]{2,127}$') { Fail $candidate }
|
|
294
|
+
if ($_.Exception -is [UnauthorizedAccessException]) { Fail "local_data_key_access_failed" }
|
|
295
|
+
if ($_.Exception -is [IO.IOException]) { Fail "local_data_key_io_failed" }
|
|
296
|
+
Fail "local_data_key_helper_failed"
|
|
297
|
+
} finally {
|
|
298
|
+
if ($Entropy) { [Array]::Clear($Entropy, 0, $Entropy.Length) }
|
|
299
|
+
$Entropy = $null
|
|
300
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
$ErrorActionPreference = "Stop"
|
|
2
|
+
|
|
3
|
+
# Host-owned native decision surface. The request arrives only on anonymous
|
|
4
|
+
# child stdin. This script accepts no command-line decision or secret inputs, does
|
|
5
|
+
# not read environment variables, and does not write files or logs.
|
|
6
|
+
|
|
7
|
+
function ConvertFrom-Base64Url {
|
|
8
|
+
param([Parameter(Mandatory = $true)][string]$Value)
|
|
9
|
+
if ($Value -notmatch '^[A-Za-z0-9_-]{43}$') { throw "invalid input" }
|
|
10
|
+
$padded = $Value.Replace('-', '+').Replace('_', '/')
|
|
11
|
+
switch ($padded.Length % 4) {
|
|
12
|
+
2 { $padded += '==' }
|
|
13
|
+
3 { $padded += '=' }
|
|
14
|
+
}
|
|
15
|
+
return [Convert]::FromBase64String($padded)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Get-DecisionMessage {
|
|
19
|
+
param(
|
|
20
|
+
[Parameter(Mandatory = $true)]$InputRecord,
|
|
21
|
+
[Parameter(Mandatory = $true)][string]$Decision
|
|
22
|
+
)
|
|
23
|
+
$fields = @(
|
|
24
|
+
[string]$InputRecord.contract,
|
|
25
|
+
[string]$InputRecord.actionId,
|
|
26
|
+
$Decision,
|
|
27
|
+
[string]$InputRecord.principalId,
|
|
28
|
+
[string]$InputRecord.delegateId,
|
|
29
|
+
[string]$InputRecord.hostInstallationId,
|
|
30
|
+
[string]$InputRecord.adapterId,
|
|
31
|
+
[string]$InputRecord.adapterInstanceId,
|
|
32
|
+
[string]$InputRecord.hostGeneration,
|
|
33
|
+
[string]$InputRecord.recipientHelloLynkId
|
|
34
|
+
)
|
|
35
|
+
if ($InputRecord.PSObject.Properties.Name -contains 'recipientDelegateId') {
|
|
36
|
+
$fields += @([string]$InputRecord.recipientDelegateId, [string]$InputRecord.recipientInstanceDisplayName)
|
|
37
|
+
}
|
|
38
|
+
$fields += @(
|
|
39
|
+
[string]$InputRecord.clientMessageId,
|
|
40
|
+
[string]$InputRecord.textDigest,
|
|
41
|
+
[string]$InputRecord.createdAtMs,
|
|
42
|
+
[string]$InputRecord.expiresAtMs,
|
|
43
|
+
[string]$InputRecord.challenge
|
|
44
|
+
)
|
|
45
|
+
return $fields -join "`n"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
# Node sends UTF-8 bytes; the user's console code page must not change them.
|
|
50
|
+
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
|
51
|
+
$inputReader = [IO.StreamReader]::new([Console]::OpenStandardInput(), [Text.Encoding]::UTF8)
|
|
52
|
+
$raw = $inputReader.ReadToEnd()
|
|
53
|
+
if ([string]::IsNullOrWhiteSpace($raw) -or [Text.Encoding]::UTF8.GetByteCount($raw) -gt (20480 * 6 + 8192)) {
|
|
54
|
+
throw "invalid input"
|
|
55
|
+
}
|
|
56
|
+
$inputRecord = $raw | ConvertFrom-Json
|
|
57
|
+
$expected = @(
|
|
58
|
+
'actionId', 'adapterId', 'adapterInstanceId', 'challenge', 'clientMessageId',
|
|
59
|
+
'contract', 'createdAtMs', 'delegateId', 'displayText', 'expiresAtMs',
|
|
60
|
+
'hmacKey', 'hostGeneration', 'hostInstallationId', 'principalId',
|
|
61
|
+
'recipientHelloLynkId', 'textDigest'
|
|
62
|
+
)
|
|
63
|
+
$hasRecipientInstance = ($inputRecord.PSObject.Properties.Name -contains 'recipientDelegateId') -or
|
|
64
|
+
($inputRecord.PSObject.Properties.Name -contains 'recipientInstanceDisplayName')
|
|
65
|
+
if ($hasRecipientInstance) { $expected += @('recipientDelegateId', 'recipientInstanceDisplayName') }
|
|
66
|
+
$expected = $expected | Sort-Object
|
|
67
|
+
$actual = @($inputRecord.PSObject.Properties.Name | Sort-Object)
|
|
68
|
+
if ($actual.Count -ne $expected.Count -or (Compare-Object $actual $expected)) {
|
|
69
|
+
throw "invalid input"
|
|
70
|
+
}
|
|
71
|
+
if ($inputRecord.contract -ne 'hellolynk-native-human-confirmation-v1') { throw "invalid input" }
|
|
72
|
+
if ($hasRecipientInstance) {
|
|
73
|
+
if ($inputRecord.recipientDelegateId -isnot [string] -or
|
|
74
|
+
$inputRecord.recipientDelegateId -cnotmatch '^[A-Za-z0-9][A-Za-z0-9._:-]{2,198}$') { throw "invalid input" }
|
|
75
|
+
$instanceName = $inputRecord.recipientInstanceDisplayName
|
|
76
|
+
if ($instanceName -isnot [string] -or [string]::IsNullOrWhiteSpace($instanceName) -or
|
|
77
|
+
$instanceName.Trim() -cne $instanceName -or [Text.Encoding]::UTF8.GetByteCount($instanceName) -gt 512 -or
|
|
78
|
+
$instanceName -match '[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]') { throw "invalid input" }
|
|
79
|
+
}
|
|
80
|
+
if ([string]::IsNullOrEmpty([string]$inputRecord.displayText)) { throw "invalid input" }
|
|
81
|
+
if ([Text.Encoding]::UTF8.GetByteCount([string]$inputRecord.displayText) -gt 20480) {
|
|
82
|
+
throw "invalid input"
|
|
83
|
+
}
|
|
84
|
+
$hmacKey = ConvertFrom-Base64Url -Value ([string]$inputRecord.hmacKey)
|
|
85
|
+
if ($hmacKey.Length -ne 32) { throw "invalid input" }
|
|
86
|
+
|
|
87
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
88
|
+
Add-Type -AssemblyName System.Drawing
|
|
89
|
+
Add-Type -TypeDefinition @'
|
|
90
|
+
using System;
|
|
91
|
+
using System.Runtime.InteropServices;
|
|
92
|
+
public static class HelloLynkNativeConfirmationWindow {
|
|
93
|
+
[DllImport("user32.dll")]
|
|
94
|
+
public static extern bool ShowWindow(IntPtr window, int command);
|
|
95
|
+
[DllImport("user32.dll", SetLastError=true)]
|
|
96
|
+
public static extern bool SetWindowPos(IntPtr window, IntPtr insertAfter, int x, int y, int width, int height, uint flags);
|
|
97
|
+
}
|
|
98
|
+
'@
|
|
99
|
+
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
100
|
+
|
|
101
|
+
$form = New-Object System.Windows.Forms.Form
|
|
102
|
+
$form.Text = 'HelloLynk 人类确认'
|
|
103
|
+
$form.StartPosition = 'CenterScreen'
|
|
104
|
+
$form.Size = New-Object System.Drawing.Size(620, 430)
|
|
105
|
+
$form.MinimizeBox = $false
|
|
106
|
+
$form.MaximizeBox = $false
|
|
107
|
+
$form.ShowInTaskbar = $true
|
|
108
|
+
$form.TopMost = $true
|
|
109
|
+
# HELLOLYNK_VISIBLE_CONFIRMATION_START
|
|
110
|
+
# Node keeps the PowerShell console hidden with STARTF_USESHOWWINDOW/SW_HIDE.
|
|
111
|
+
# The first ShowDialog inherits that hidden state, so explicitly show the
|
|
112
|
+
# already-created native form without exposing the console window.
|
|
113
|
+
$form.Add_Shown({
|
|
114
|
+
[void][HelloLynkNativeConfirmationWindow]::ShowWindow($form.Handle, 1)
|
|
115
|
+
[void][HelloLynkNativeConfirmationWindow]::SetWindowPos(
|
|
116
|
+
$form.Handle, [IntPtr](-1), 0, 0, 0, 0, (0x0001 -bor 0x0002 -bor 0x0040)
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
# HELLOLYNK_VISIBLE_CONFIRMATION_END
|
|
120
|
+
|
|
121
|
+
$heading = New-Object System.Windows.Forms.Label
|
|
122
|
+
$heading.Text = '是否允许 HelloLynk 发送以下文本?'
|
|
123
|
+
$heading.AutoSize = $true
|
|
124
|
+
$heading.Location = New-Object System.Drawing.Point(20, 18)
|
|
125
|
+
$heading.Font = New-Object System.Drawing.Font('Segoe UI', 12, [System.Drawing.FontStyle]::Bold)
|
|
126
|
+
$form.Controls.Add($heading)
|
|
127
|
+
|
|
128
|
+
$recipient = New-Object System.Windows.Forms.Label
|
|
129
|
+
$recipient.Text = ('收件人:' + [string]$inputRecord.recipientHelloLynkId)
|
|
130
|
+
$recipient.AutoSize = $true
|
|
131
|
+
$recipient.Location = New-Object System.Drawing.Point(20, 54)
|
|
132
|
+
$form.Controls.Add($recipient)
|
|
133
|
+
|
|
134
|
+
$textTop = 82
|
|
135
|
+
if ($hasRecipientInstance) {
|
|
136
|
+
$recipientAI = New-Object System.Windows.Forms.Label
|
|
137
|
+
$recipientAI.Text = ('目标 AI:' + [string]$inputRecord.recipientInstanceDisplayName)
|
|
138
|
+
$recipientAI.UseMnemonic = $false
|
|
139
|
+
$recipientAI.AutoSize = $true
|
|
140
|
+
$recipientAI.MaximumSize = New-Object System.Drawing.Size(565, 0)
|
|
141
|
+
$recipientAI.Location = New-Object System.Drawing.Point(20, 80)
|
|
142
|
+
$form.Controls.Add($recipientAI)
|
|
143
|
+
$textTop = $recipientAI.Bottom + 16
|
|
144
|
+
}
|
|
145
|
+
$extraHeight = $textTop - 82
|
|
146
|
+
$form.Size = New-Object System.Drawing.Size(620, (430 + $extraHeight))
|
|
147
|
+
|
|
148
|
+
$textBox = New-Object System.Windows.Forms.TextBox
|
|
149
|
+
$textBox.Multiline = $true
|
|
150
|
+
$textBox.ReadOnly = $true
|
|
151
|
+
$textBox.ScrollBars = 'Vertical'
|
|
152
|
+
$textBox.WordWrap = $true
|
|
153
|
+
$textBox.Text = [string]$inputRecord.displayText
|
|
154
|
+
$textBox.Location = New-Object System.Drawing.Point(20, $textTop)
|
|
155
|
+
$textBox.Size = New-Object System.Drawing.Size(565, 245)
|
|
156
|
+
$form.Controls.Add($textBox)
|
|
157
|
+
|
|
158
|
+
$approve = New-Object System.Windows.Forms.Button
|
|
159
|
+
$approve.Text = '批准发送'
|
|
160
|
+
$approve.Location = New-Object System.Drawing.Point(385, (344 + $extraHeight))
|
|
161
|
+
$approve.Size = New-Object System.Drawing.Size(95, 32)
|
|
162
|
+
$approve.DialogResult = [System.Windows.Forms.DialogResult]::OK
|
|
163
|
+
$form.Controls.Add($approve)
|
|
164
|
+
$form.AcceptButton = $approve
|
|
165
|
+
|
|
166
|
+
$cancel = New-Object System.Windows.Forms.Button
|
|
167
|
+
$cancel.Text = '取消'
|
|
168
|
+
$cancel.Location = New-Object System.Drawing.Point(490, (344 + $extraHeight))
|
|
169
|
+
$cancel.Size = New-Object System.Drawing.Size(95, 32)
|
|
170
|
+
$cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
|
|
171
|
+
$form.Controls.Add($cancel)
|
|
172
|
+
$form.CancelButton = $cancel
|
|
173
|
+
|
|
174
|
+
# Closing the window produces Cancel; there is no third decision path.
|
|
175
|
+
$result = $form.ShowDialog()
|
|
176
|
+
$form.Dispose()
|
|
177
|
+
$decision = if ($result -eq [System.Windows.Forms.DialogResult]::OK) { 'approve' } else { 'cancel' }
|
|
178
|
+
$message = Get-DecisionMessage -InputRecord $inputRecord -Decision $decision
|
|
179
|
+
$hmac = New-Object System.Security.Cryptography.HMACSHA256 -ArgumentList (, $hmacKey)
|
|
180
|
+
try {
|
|
181
|
+
$proofBytes = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($message))
|
|
182
|
+
$proof = ([BitConverter]::ToString($proofBytes)).Replace('-', '').ToLowerInvariant()
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
$hmac.Dispose()
|
|
186
|
+
[Array]::Clear($hmacKey, 0, $hmacKey.Length)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
$response = [ordered]@{
|
|
190
|
+
contract = [string]$inputRecord.contract
|
|
191
|
+
actionId = [string]$inputRecord.actionId
|
|
192
|
+
decision = $decision
|
|
193
|
+
principalId = [string]$inputRecord.principalId
|
|
194
|
+
delegateId = [string]$inputRecord.delegateId
|
|
195
|
+
hostInstallationId = [string]$inputRecord.hostInstallationId
|
|
196
|
+
adapterId = [string]$inputRecord.adapterId
|
|
197
|
+
adapterInstanceId = [string]$inputRecord.adapterInstanceId
|
|
198
|
+
hostGeneration = [string]$inputRecord.hostGeneration
|
|
199
|
+
recipientHelloLynkId = [string]$inputRecord.recipientHelloLynkId
|
|
200
|
+
clientMessageId = [string]$inputRecord.clientMessageId
|
|
201
|
+
textDigest = [string]$inputRecord.textDigest
|
|
202
|
+
createdAtMs = [long]$inputRecord.createdAtMs
|
|
203
|
+
expiresAtMs = [long]$inputRecord.expiresAtMs
|
|
204
|
+
proof = $proof
|
|
205
|
+
}
|
|
206
|
+
if ($hasRecipientInstance) {
|
|
207
|
+
$response.recipientDelegateId = [string]$inputRecord.recipientDelegateId
|
|
208
|
+
$response.recipientInstanceDisplayName = [string]$inputRecord.recipientInstanceDisplayName
|
|
209
|
+
}
|
|
210
|
+
[Console]::Out.WriteLine(($response | ConvertTo-Json -Compress))
|
|
211
|
+
exit 0
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
# Never reflect input or exception details onto stderr/stdout.
|
|
215
|
+
exit 1
|
|
216
|
+
}
|