@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.99

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.
Files changed (123) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/LICENSE +92 -0
  3. package/README.md +86 -2
  4. package/bin/hellolynk-client-setup.mjs +2748 -0
  5. package/bin/hellolynk-mcp-config-shape.mjs +23 -0
  6. package/hellolynk-client-pack-manifest.json +619 -0
  7. package/hellolynk-client-pack-pin.json +4 -0
  8. package/package.json +58 -4
  9. package/plugins/hellolynk-codex/.app.json +3 -0
  10. package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
  11. package/plugins/hellolynk-codex/.mcp.json +19 -0
  12. package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
  13. package/plugins/hellolynk-codex/package.json +11 -0
  14. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
  15. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
  16. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
  17. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
  18. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
  19. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
  20. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  21. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
  22. package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
  23. package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  24. package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
  25. package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
  26. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
  27. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
  28. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
  29. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
  30. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
  31. package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
  32. package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
  33. package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
  34. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
  35. package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
  36. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
  37. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
  38. package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
  39. package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
  40. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
  41. package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
  42. package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
  43. package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
  44. package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
  45. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
  46. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
  47. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
  48. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
  49. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
  50. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
  51. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
  52. package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
  53. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
  54. package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
  55. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
  56. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
  57. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
  58. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
  59. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
  60. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
  61. package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
  62. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
  63. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
  64. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
  65. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
  66. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
  67. package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
  68. package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
  69. package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
  70. package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +160 -0
  71. package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
  72. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
  73. package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
  74. package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
  75. package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
  76. package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
  77. package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
  78. package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
  79. package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
  80. package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
  81. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
  82. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
  83. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
  84. package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
  85. package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
  86. package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
  87. package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
  88. package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
  89. package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
  90. package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
  91. package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
  92. package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
  93. package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
  94. package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
  95. package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
  96. package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
  97. package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
  98. package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
  99. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
  100. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
  101. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
  102. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
  103. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
  104. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
  105. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
  106. package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
  107. package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
  108. package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
  109. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
  110. package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
  111. package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
  112. package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
  113. package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
  114. package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
  115. package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
  116. package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
  117. package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
  118. package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
  119. package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
  120. package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
  121. package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
  122. package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
  123. package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
@@ -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
+ }