@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
|
@@ -0,0 +1,2866 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)]
|
|
3
|
+
[ValidateSet(
|
|
4
|
+
"Issue", "Load", "Revoke", "Rotate", "Status", "DisconnectLocalClient",
|
|
5
|
+
"SaveCodexNeutralBinding", "ResolveCodexNeutralBinding", "VerifyCodexNeutralBinding", "RemoveCodexNeutralBinding", "ReplaceCodexNeutralBinding", "MigrateCodexNeutralBinding",
|
|
6
|
+
"SaveOpenClawNeutralBinding", "ResolveOpenClawNeutralBinding", "VerifyOpenClawNeutralBinding", "RemoveOpenClawNeutralBinding", "MigrateOpenClawNeutralBinding",
|
|
7
|
+
"SaveTraeWorkNeutralBinding", "ResolveTraeWorkNeutralBinding", "VerifyTraeWorkNeutralBinding", "RemoveTraeWorkNeutralBinding", "MigrateTraeWorkNeutralBinding",
|
|
8
|
+
"SaveWorkBuddyNeutralBinding", "ResolveWorkBuddyNeutralBinding", "VerifyWorkBuddyNeutralBinding", "RemoveWorkBuddyNeutralBinding", "MigrateWorkBuddyNeutralBinding",
|
|
9
|
+
"SaveClaudeCodeNeutralBinding", "ResolveClaudeCodeNeutralBinding", "VerifyClaudeCodeNeutralBinding", "RemoveClaudeCodeNeutralBinding", "MigrateClaudeCodeNeutralBinding",
|
|
10
|
+
"SaveCursorNeutralBinding", "ResolveCursorNeutralBinding", "VerifyCursorNeutralBinding", "RemoveCursorNeutralBinding", "MigrateCursorNeutralBinding",
|
|
11
|
+
"SaveDshNeutralBinding", "ResolveDshNeutralBinding", "VerifyDshNeutralBinding", "RemoveDshNeutralBinding", "MigrateDshNeutralBinding", "RestoreDshNeutralBinding",
|
|
12
|
+
"SaveTransportCredential", "LoadTransportCredential", "RevokeTransportCredential",
|
|
13
|
+
"VerifyTransportCredentialLifetimeMigration", "MigrateTransportCredentialLifetime",
|
|
14
|
+
"SaveTransportSession", "LoadTransportSession", "RemoveTransportSession",
|
|
15
|
+
"ReplaceTransportSession", "ResolveTransport", "VerifyTransport", "VerifyPath",
|
|
16
|
+
"LoadDeviceEnrollment", "SaveDeviceEnrollment", "SaveEnrollmentTransportCredential"
|
|
17
|
+
)]
|
|
18
|
+
[string]$Command,
|
|
19
|
+
|
|
20
|
+
[Parameter(Mandatory = $true)]
|
|
21
|
+
[string]$StoreRoot,
|
|
22
|
+
|
|
23
|
+
[string]$CapabilityId,
|
|
24
|
+
[string]$CredentialId,
|
|
25
|
+
[string]$SessionId,
|
|
26
|
+
[string]$PrincipalId,
|
|
27
|
+
[string]$HostInstallationId,
|
|
28
|
+
[int64]$Timestamp = -1,
|
|
29
|
+
[string]$PathToVerify,
|
|
30
|
+
[switch]$VerifyDirectory,
|
|
31
|
+
[switch]$VerifyOnly,
|
|
32
|
+
[string]$ResultPipeName
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
$ErrorActionPreference = "Stop"
|
|
36
|
+
$ProgressPreference = "SilentlyContinue"
|
|
37
|
+
# The Node caller writes UTF-8 to a pipe. Decode that protocol explicitly,
|
|
38
|
+
# independent of the inherited Windows console code page (including names).
|
|
39
|
+
[Console]::SetIn([IO.StreamReader]::new([Console]::OpenStandardInput(), [Text.UTF8Encoding]::new($false, $true)))
|
|
40
|
+
$CapabilityIdPattern = '^cap_[A-Za-z0-9_-]{32}$'
|
|
41
|
+
$IdentifierPattern = '^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,126}[A-Za-z0-9])?$'
|
|
42
|
+
$SecretPattern = '^[A-Za-z0-9_-]{43}$'
|
|
43
|
+
$Contract = 'hellolynk-host-protected-store-v1'
|
|
44
|
+
$RecordContract = 'hellolynk-host-capability-v1'
|
|
45
|
+
$TransportCredentialContract = 'hellolynk-host-transport-credential-v1'
|
|
46
|
+
$TransportSessionContract = 'hellolynk-host-transport-session-v1'
|
|
47
|
+
$AuthorizationHistoryContract = 'hellolynk-host-authorization-history-v1'
|
|
48
|
+
$CodexNeutralBindingContract = 'hellolynk-codex-neutral-binding-v1'
|
|
49
|
+
$OpenClawNeutralBindingContract = 'hellolynk-openclaw-neutral-binding-v1'
|
|
50
|
+
$TraeWorkNeutralBindingContract = 'hellolynk-trae-work-neutral-binding-v1'
|
|
51
|
+
$WorkBuddyNeutralBindingContract = 'hellolynk-workbuddy-neutral-binding-v1'
|
|
52
|
+
$ClaudeCodeNeutralBindingContract = 'hellolynk-claude-code-neutral-binding-v1'
|
|
53
|
+
$CursorNeutralBindingContract = 'hellolynk-cursor-neutral-binding-v1'
|
|
54
|
+
$DshNeutralBindingContract = 'hellolynk-dsh-neutral-binding-v1'
|
|
55
|
+
$CodexNeutralScopes = @(
|
|
56
|
+
'app-data:authorized-messaging',
|
|
57
|
+
'app-data:changes-read',
|
|
58
|
+
'app-data:contact-requests-read',
|
|
59
|
+
'app-data:contacts-manage',
|
|
60
|
+
'app-data:contacts-read',
|
|
61
|
+
'app-data:human-send-request',
|
|
62
|
+
'app-data:inbox-manage',
|
|
63
|
+
'app-data:inbox-read',
|
|
64
|
+
'app-data:outbox-read',
|
|
65
|
+
'app-data:outbox-retry',
|
|
66
|
+
'attachments:manage',
|
|
67
|
+
'surface:human-ui',
|
|
68
|
+
'ui:launch'
|
|
69
|
+
)
|
|
70
|
+
$OpenClawNeutralScopes = @($CodexNeutralScopes)
|
|
71
|
+
$CodexNeutralPreviousScopes = @($CodexNeutralScopes | Where-Object { $_ -cne 'app-data:authorized-messaging' })
|
|
72
|
+
$OpenClawNeutralPreviousScopes = @($CodexNeutralPreviousScopes)
|
|
73
|
+
$TraeWorkNeutralScopes = @(
|
|
74
|
+
'app-data:authorized-messaging',
|
|
75
|
+
'app-data:changes-read',
|
|
76
|
+
'app-data:contact-requests-read',
|
|
77
|
+
'app-data:contacts-manage',
|
|
78
|
+
'app-data:contacts-read',
|
|
79
|
+
'app-data:human-send-request',
|
|
80
|
+
'app-data:inbox-manage',
|
|
81
|
+
'app-data:inbox-read',
|
|
82
|
+
'app-data:outbox-read',
|
|
83
|
+
'app-data:outbox-retry',
|
|
84
|
+
'attachments:manage',
|
|
85
|
+
'status:read',
|
|
86
|
+
'surface:human-ui',
|
|
87
|
+
'ui:launch'
|
|
88
|
+
)
|
|
89
|
+
$WorkBuddyNeutralScopes = @($TraeWorkNeutralScopes)
|
|
90
|
+
$ClaudeCodeNeutralScopes = @($WorkBuddyNeutralScopes)
|
|
91
|
+
$TraeWorkNeutralPreviousScopes = @($TraeWorkNeutralScopes | Where-Object { $_ -cne 'app-data:authorized-messaging' })
|
|
92
|
+
$WorkBuddyNeutralPreviousScopes = @($TraeWorkNeutralPreviousScopes)
|
|
93
|
+
$TraeWorkNeutralLegacyScopes = @(
|
|
94
|
+
'app-data:human-send-request',
|
|
95
|
+
'attachments:manage',
|
|
96
|
+
'status:read',
|
|
97
|
+
'surface:human-ui'
|
|
98
|
+
)
|
|
99
|
+
$WorkBuddyNeutralLegacyScopes = @($TraeWorkNeutralLegacyScopes)
|
|
100
|
+
$CursorNeutralScopes = @(
|
|
101
|
+
'app-data:authorized-messaging',
|
|
102
|
+
'app-data:changes-read',
|
|
103
|
+
'app-data:contact-requests-read',
|
|
104
|
+
'app-data:contacts-manage',
|
|
105
|
+
'app-data:contacts-read',
|
|
106
|
+
'app-data:human-send-request',
|
|
107
|
+
'app-data:inbox-manage',
|
|
108
|
+
'app-data:inbox-read',
|
|
109
|
+
'app-data:outbox-read',
|
|
110
|
+
'app-data:outbox-retry',
|
|
111
|
+
'attachments:manage',
|
|
112
|
+
'status:read',
|
|
113
|
+
'surface:human-ui',
|
|
114
|
+
'ui:launch'
|
|
115
|
+
)
|
|
116
|
+
$CursorNeutralPreviousScopes = @($CursorNeutralScopes | Where-Object { $_ -cne 'app-data:authorized-messaging' })
|
|
117
|
+
$DshNeutralScopes = @($CursorNeutralScopes)
|
|
118
|
+
$DshNeutralPreviousScopes = @($CursorNeutralPreviousScopes)
|
|
119
|
+
$TransportCredentialSecretPattern = '^hellolynk_host_transport_v1_([A-Za-z0-9_-]{3,127})\.([A-Za-z0-9_-]{32,})$'
|
|
120
|
+
$TransportSessionIdPattern = '^hosttransportsession_[A-Za-z0-9_-]{16,}$'
|
|
121
|
+
$TransportSessionSecretPattern = '^hostsession_[a-f0-9]{48}$'
|
|
122
|
+
$CredentialName = 'hellolynk-neutral-host-protected-store-v1'
|
|
123
|
+
$EnvelopeContract = 'hellolynk-host-protected-envelope-v2'
|
|
124
|
+
$RetiredIndexContract = 'hellolynk-retired-transport-index-v1'
|
|
125
|
+
|
|
126
|
+
Add-Type -TypeDefinition @'
|
|
127
|
+
using System;
|
|
128
|
+
using System.Runtime.InteropServices;
|
|
129
|
+
using System.Text;
|
|
130
|
+
using Microsoft.Win32.SafeHandles;
|
|
131
|
+
public static class HelloLynkProtectedStoreNativePath {
|
|
132
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
133
|
+
public static extern SafeFileHandle CreateFile(
|
|
134
|
+
string name, uint access, uint share, IntPtr security, uint creation,
|
|
135
|
+
uint flags, IntPtr template);
|
|
136
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
137
|
+
public static extern uint GetFinalPathNameByHandle(
|
|
138
|
+
SafeFileHandle handle, StringBuilder path, uint length, uint flags);
|
|
139
|
+
}
|
|
140
|
+
'@
|
|
141
|
+
|
|
142
|
+
function Fail([string]$Code) {
|
|
143
|
+
[Console]::Error.WriteLine($Code)
|
|
144
|
+
exit 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function Test-ClaudeCodeStoreRoot([string]$Value) {
|
|
148
|
+
return $Value -match '(?i)[\\/]host$'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Test-ExactScopeSequence($Actual, [string[]]$Expected) {
|
|
152
|
+
if ($Actual -isnot [Array] -or $Actual.Count -ne $Expected.Count) { return $false }
|
|
153
|
+
for ($index = 0; $index -lt $Expected.Count; $index += 1) {
|
|
154
|
+
if ([string]$Actual[$index] -cne [string]$Expected[$index]) { return $false }
|
|
155
|
+
}
|
|
156
|
+
return $true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Test-FullyQualifiedCanonicalPath([string]$Value) {
|
|
160
|
+
try {
|
|
161
|
+
if (-not $Value -or -not [IO.Path]::IsPathFullyQualified($Value)) { return $false }
|
|
162
|
+
$null = [IO.Path]::GetFullPath($Value)
|
|
163
|
+
return $true
|
|
164
|
+
} catch { return $false }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function Assert-ExactFields($Value, [string[]]$Expected, [string]$Code) {
|
|
168
|
+
if ($null -eq $Value -or $Value -is [Array]) { Fail $Code }
|
|
169
|
+
$actual = @($Value.PSObject.Properties.Name | Sort-Object)
|
|
170
|
+
$wanted = @($Expected | Sort-Object)
|
|
171
|
+
if ($actual.Count -ne $wanted.Count) { Fail $Code }
|
|
172
|
+
for ($index = 0; $index -lt $wanted.Count; $index += 1) {
|
|
173
|
+
if ($actual[$index] -cne $wanted[$index]) { Fail $Code }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function Assert-Record($Record) {
|
|
178
|
+
Assert-ExactFields $Record @(
|
|
179
|
+
'adapterId', 'capabilityId', 'contract', 'delegateId', 'hostInstallationId',
|
|
180
|
+
'issuedAt', 'principalId', 'region', 'revokedAt', 'scopes', 'secret',
|
|
181
|
+
'state', 'version'
|
|
182
|
+
) 'host_capability_fields_invalid'
|
|
183
|
+
if (
|
|
184
|
+
[string]$Record.contract -cne $RecordContract -or
|
|
185
|
+
[string]$Record.capabilityId -notmatch $CapabilityIdPattern -or
|
|
186
|
+
[string]$Record.region -notmatch $IdentifierPattern -or
|
|
187
|
+
[string]$Record.principalId -notmatch $IdentifierPattern -or
|
|
188
|
+
[string]$Record.delegateId -notmatch $IdentifierPattern -or
|
|
189
|
+
[string]$Record.hostInstallationId -notmatch $IdentifierPattern -or
|
|
190
|
+
[string]$Record.adapterId -notmatch $IdentifierPattern -or
|
|
191
|
+
[int64]$Record.version -lt 1 -or
|
|
192
|
+
@('active', 'revoked') -notcontains [string]$Record.state -or
|
|
193
|
+
[int64]$Record.issuedAt -lt 0 -or
|
|
194
|
+
[string]$Record.secret -notmatch $SecretPattern -or
|
|
195
|
+
$Record.scopes -isnot [Array] -or
|
|
196
|
+
$Record.scopes.Count -lt 1 -or
|
|
197
|
+
$Record.scopes.Count -gt 64
|
|
198
|
+
) { Fail 'host_capability_invalid' }
|
|
199
|
+
if (
|
|
200
|
+
([string]$Record.state -eq 'active' -and $null -ne $Record.revokedAt) -or
|
|
201
|
+
([string]$Record.state -eq 'revoked' -and [int64]$Record.revokedAt -lt [int64]$Record.issuedAt)
|
|
202
|
+
) { Fail 'host_capability_state_invalid' }
|
|
203
|
+
$previous = $null
|
|
204
|
+
foreach ($scope in $Record.scopes) {
|
|
205
|
+
if ([string]$scope -notmatch $IdentifierPattern) { Fail 'host_capability_scope_invalid' }
|
|
206
|
+
if ($null -ne $previous -and [string]::CompareOrdinal([string]$previous, [string]$scope) -ge 0) {
|
|
207
|
+
Fail 'host_capability_scopes_noncanonical'
|
|
208
|
+
}
|
|
209
|
+
$previous = [string]$scope
|
|
210
|
+
}
|
|
211
|
+
return $Record
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function Assert-CodexNeutralBinding($Binding, [bool]$ValidatePaths) {
|
|
215
|
+
Assert-ExactFields $Binding @(
|
|
216
|
+
'adapterId', 'contract', 'delegateId', 'hostInstallationId', 'principalId',
|
|
217
|
+
'profileId', 'profileManifestPath', 'region', 'releaseManifestSha256', 'releaseRoot', 'runnerRelativePath',
|
|
218
|
+
'runtimeRoot', 'scopes', 'version'
|
|
219
|
+
) 'neutral_codex_binding_invalid'
|
|
220
|
+
if (
|
|
221
|
+
[string]$Binding.contract -cne $CodexNeutralBindingContract -or
|
|
222
|
+
[string]$Binding.region -notmatch $IdentifierPattern -or
|
|
223
|
+
[string]$Binding.principalId -notmatch $IdentifierPattern -or
|
|
224
|
+
[string]$Binding.delegateId -notmatch $IdentifierPattern -or
|
|
225
|
+
[string]$Binding.hostInstallationId -notmatch $IdentifierPattern -or
|
|
226
|
+
[string]$Binding.profileId -notmatch $IdentifierPattern -or
|
|
227
|
+
[string]$Binding.adapterId -cne 'codex' -or
|
|
228
|
+
[int64]$Binding.version -ne 2 -or
|
|
229
|
+
$Binding.scopes -isnot [Array] -or
|
|
230
|
+
(-not (Test-ExactScopeSequence $Binding.scopes $CodexNeutralScopes) -and
|
|
231
|
+
-not (Test-ExactScopeSequence $Binding.scopes $CodexNeutralPreviousScopes)) -or
|
|
232
|
+
[string]$Binding.runnerRelativePath -notmatch '^(?:[A-Za-z0-9][A-Za-z0-9._-]*/)*[A-Za-z0-9][A-Za-z0-9._-]*\.mjs$' -or
|
|
233
|
+
[string]$Binding.releaseManifestSha256 -notmatch '^[a-f0-9]{64}$'
|
|
234
|
+
) { Fail 'neutral_codex_binding_invalid' }
|
|
235
|
+
if ($ValidatePaths) { Assert-CodexNeutralPaths $Binding }
|
|
236
|
+
return $Binding
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function Assert-CodexNeutralPaths($Binding) {
|
|
240
|
+
$releaseRoot = Assert-AbsoluteNonReparsePath ([string]$Binding.releaseRoot) $true 0
|
|
241
|
+
$runtimeRoot = Assert-AbsoluteNonReparsePath ([string]$Binding.runtimeRoot) $true 0
|
|
242
|
+
$profileManifestPath = Assert-AbsoluteNonReparsePath ([string]$Binding.profileManifestPath) $false 65536
|
|
243
|
+
$runnerPath = [IO.Path]::GetFullPath((Join-Path $releaseRoot ([string]$Binding.runnerRelativePath)))
|
|
244
|
+
$releasePrefix = $releaseRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
|
|
245
|
+
if (-not $runnerPath.StartsWith($releasePrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
246
|
+
Fail 'neutral_codex_runner_integrity_invalid'
|
|
247
|
+
}
|
|
248
|
+
$null = Assert-AbsoluteNonReparsePath $runnerPath $false 1048576
|
|
249
|
+
if (
|
|
250
|
+
-not [string]::Equals($runtimeRoot, [IO.Path]::GetFullPath([string]$Binding.runtimeRoot), [StringComparison]::OrdinalIgnoreCase) -or
|
|
251
|
+
-not [string]::Equals($profileManifestPath, [IO.Path]::GetFullPath([string]$Binding.profileManifestPath), [StringComparison]::OrdinalIgnoreCase)
|
|
252
|
+
) { Fail 'neutral_codex_binding_invalid' }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function Assert-AbsoluteNonReparsePath(
|
|
256
|
+
[string]$Value,
|
|
257
|
+
[bool]$Directory,
|
|
258
|
+
[int64]$MaxBytes,
|
|
259
|
+
[string]$BindingInvalidCode = 'neutral_codex_binding_invalid',
|
|
260
|
+
[string]$PathInvalidCode = 'neutral_codex_launch_manifest_invalid'
|
|
261
|
+
) {
|
|
262
|
+
if (-not $Value -or -not [IO.Path]::IsPathFullyQualified($Value)) { Fail $BindingInvalidCode }
|
|
263
|
+
$full = [IO.Path]::GetFullPath($Value)
|
|
264
|
+
$rootPart = [IO.Path]::GetPathRoot($full)
|
|
265
|
+
$cursor = $rootPart
|
|
266
|
+
$relative = $full.Substring($rootPart.Length)
|
|
267
|
+
foreach ($segment in $relative.Split([IO.Path]::DirectorySeparatorChar, [StringSplitOptions]::RemoveEmptyEntries)) {
|
|
268
|
+
$cursor = Join-Path $cursor $segment
|
|
269
|
+
if (-not (Test-Path -LiteralPath $cursor)) { Fail $PathInvalidCode }
|
|
270
|
+
$item = Get-Item -LiteralPath $cursor -Force
|
|
271
|
+
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
272
|
+
Fail $PathInvalidCode
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
$target = Get-Item -LiteralPath $full -Force
|
|
276
|
+
if ($Directory) {
|
|
277
|
+
if (-not $target.PSIsContainer) { Fail $BindingInvalidCode }
|
|
278
|
+
} else {
|
|
279
|
+
if ($target.PSIsContainer -or $target.Length -gt $MaxBytes) { Fail $PathInvalidCode }
|
|
280
|
+
}
|
|
281
|
+
return $full
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function Assert-ProtectedStoreNoReparse([string]$Path) {
|
|
285
|
+
$current = [IO.Path]::GetFullPath($Path)
|
|
286
|
+
while ($current) {
|
|
287
|
+
if (Test-Path -LiteralPath $current) {
|
|
288
|
+
$item = Get-Item -LiteralPath $current -Force
|
|
289
|
+
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
290
|
+
Fail 'host_protected_store_root_invalid'
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
$parent = [IO.Directory]::GetParent($current)
|
|
294
|
+
if ($null -eq $parent) { break }
|
|
295
|
+
$current = $parent.FullName
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function Get-ProtectedStoreCanonicalLocalRoot([string]$Path) {
|
|
300
|
+
$handle = [HelloLynkProtectedStoreNativePath]::CreateFile(
|
|
301
|
+
$Path, 0, 7, [IntPtr]::Zero, 3, 0x02000000, [IntPtr]::Zero
|
|
302
|
+
)
|
|
303
|
+
if ($handle.IsInvalid) { $handle.Dispose(); Fail 'host_protected_store_root_invalid' }
|
|
304
|
+
try {
|
|
305
|
+
$buffer = [Text.StringBuilder]::new(32768)
|
|
306
|
+
$length = [HelloLynkProtectedStoreNativePath]::GetFinalPathNameByHandle(
|
|
307
|
+
$handle, $buffer, $buffer.Capacity, 0
|
|
308
|
+
)
|
|
309
|
+
if ($length -eq 0 -or $length -ge $buffer.Capacity) { Fail 'host_protected_store_root_invalid' }
|
|
310
|
+
$final = $buffer.ToString()
|
|
311
|
+
} finally { $handle.Dispose() }
|
|
312
|
+
if ($final.StartsWith('\\?\UNC\', [StringComparison]::OrdinalIgnoreCase)) {
|
|
313
|
+
Fail 'host_protected_store_root_invalid'
|
|
314
|
+
}
|
|
315
|
+
if ($final.StartsWith('\\?\')) { $final = $final.Substring(4) }
|
|
316
|
+
$final = [IO.Path]::GetFullPath($final)
|
|
317
|
+
if ($final -notmatch '^[A-Za-z]:\\' -or $final.Length -lt 4) {
|
|
318
|
+
Fail 'host_protected_store_root_invalid'
|
|
319
|
+
}
|
|
320
|
+
return $final.TrimEnd([IO.Path]::DirectorySeparatorChar)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function Set-ProtectedStoreAcl([string]$Path, [string]$Sid, [bool]$Directory) {
|
|
324
|
+
try {
|
|
325
|
+
$identity = [Security.Principal.SecurityIdentifier]::new($Sid)
|
|
326
|
+
if ($Directory) {
|
|
327
|
+
$security = [Security.AccessControl.DirectorySecurity]::new()
|
|
328
|
+
$security.SetOwner($identity)
|
|
329
|
+
$security.SetAccessRuleProtection($true, $false)
|
|
330
|
+
$inheritance = [Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
331
|
+
[Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
332
|
+
$trustedIdentities = @(
|
|
333
|
+
$identity,
|
|
334
|
+
[Security.Principal.SecurityIdentifier]::new(
|
|
335
|
+
[Security.Principal.WellKnownSidType]::LocalSystemSid,
|
|
336
|
+
$null
|
|
337
|
+
),
|
|
338
|
+
[Security.Principal.SecurityIdentifier]::new(
|
|
339
|
+
[Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid,
|
|
340
|
+
$null
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
foreach ($trustedIdentity in $trustedIdentities) {
|
|
344
|
+
$rule = [Security.AccessControl.FileSystemAccessRule]::new(
|
|
345
|
+
$trustedIdentity,
|
|
346
|
+
[Security.AccessControl.FileSystemRights]::FullControl,
|
|
347
|
+
$inheritance,
|
|
348
|
+
[Security.AccessControl.PropagationFlags]::None,
|
|
349
|
+
[Security.AccessControl.AccessControlType]::Allow
|
|
350
|
+
)
|
|
351
|
+
$security.AddAccessRule($rule)
|
|
352
|
+
}
|
|
353
|
+
[IO.FileSystemAclExtensions]::SetAccessControl([IO.DirectoryInfo]::new($Path), $security)
|
|
354
|
+
} else {
|
|
355
|
+
$security = [Security.AccessControl.FileSecurity]::new()
|
|
356
|
+
$security.SetOwner($identity)
|
|
357
|
+
$security.SetAccessRuleProtection($true, $false)
|
|
358
|
+
$rule = [Security.AccessControl.FileSystemAccessRule]::new(
|
|
359
|
+
$identity,
|
|
360
|
+
[Security.AccessControl.FileSystemRights]::FullControl,
|
|
361
|
+
[Security.AccessControl.InheritanceFlags]::None,
|
|
362
|
+
[Security.AccessControl.PropagationFlags]::None,
|
|
363
|
+
[Security.AccessControl.AccessControlType]::Allow
|
|
364
|
+
)
|
|
365
|
+
$security.AddAccessRule($rule)
|
|
366
|
+
[IO.FileSystemAclExtensions]::SetAccessControl([IO.FileInfo]::new($Path), $security)
|
|
367
|
+
}
|
|
368
|
+
} catch { Fail 'host_protected_store_acl_failed' }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function Test-ProtectedStoreAcl([string]$Path, [string]$Sid, [bool]$Directory) {
|
|
372
|
+
# Returns instead of calling Fail: Fail exits the process, so callers that
|
|
373
|
+
# repair an invalid ACL must be able to observe the result first.
|
|
374
|
+
try {
|
|
375
|
+
$security = if ($Directory) {
|
|
376
|
+
[IO.FileSystemAclExtensions]::GetAccessControl(
|
|
377
|
+
[IO.DirectoryInfo]::new($Path),
|
|
378
|
+
[Security.AccessControl.AccessControlSections]::Access -bor
|
|
379
|
+
[Security.AccessControl.AccessControlSections]::Owner
|
|
380
|
+
)
|
|
381
|
+
} else {
|
|
382
|
+
[IO.FileSystemAclExtensions]::GetAccessControl(
|
|
383
|
+
[IO.FileInfo]::new($Path),
|
|
384
|
+
[Security.AccessControl.AccessControlSections]::Access -bor
|
|
385
|
+
[Security.AccessControl.AccessControlSections]::Owner
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
if (-not $security.AreAccessRulesProtected -or
|
|
389
|
+
$security.GetOwner([Security.Principal.SecurityIdentifier]).Value -cne $Sid) {
|
|
390
|
+
return $false
|
|
391
|
+
}
|
|
392
|
+
$rules = @($security.GetAccessRules(
|
|
393
|
+
$true, $true, [Security.Principal.SecurityIdentifier]
|
|
394
|
+
))
|
|
395
|
+
$expectedInheritance = if ($Directory) {
|
|
396
|
+
[Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
|
|
397
|
+
[Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
398
|
+
} else { [Security.AccessControl.InheritanceFlags]::None }
|
|
399
|
+
$expectedSids = if ($Directory) {
|
|
400
|
+
@(
|
|
401
|
+
$Sid,
|
|
402
|
+
[Security.Principal.SecurityIdentifier]::new(
|
|
403
|
+
[Security.Principal.WellKnownSidType]::LocalSystemSid,
|
|
404
|
+
$null
|
|
405
|
+
).Value,
|
|
406
|
+
[Security.Principal.SecurityIdentifier]::new(
|
|
407
|
+
[Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid,
|
|
408
|
+
$null
|
|
409
|
+
).Value
|
|
410
|
+
)
|
|
411
|
+
} else { @($Sid) }
|
|
412
|
+
if ($rules.Count -ne $expectedSids.Count) { return $false }
|
|
413
|
+
$observedSids = @()
|
|
414
|
+
foreach ($rule in $rules) {
|
|
415
|
+
$observedSids += $rule.IdentityReference.Value
|
|
416
|
+
if ($expectedSids -notcontains $rule.IdentityReference.Value -or
|
|
417
|
+
$rule.IsInherited -or
|
|
418
|
+
$rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow -or
|
|
419
|
+
$rule.FileSystemRights -ne [Security.AccessControl.FileSystemRights]::FullControl -or
|
|
420
|
+
$rule.InheritanceFlags -ne $expectedInheritance -or
|
|
421
|
+
$rule.PropagationFlags -ne [Security.AccessControl.PropagationFlags]::None) {
|
|
422
|
+
return $false
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
foreach ($expectedSid in $expectedSids) {
|
|
426
|
+
if (@($observedSids | Where-Object { $_ -ceq $expectedSid }).Count -ne 1) {
|
|
427
|
+
return $false
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return $true
|
|
431
|
+
} catch { return $false }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function Assert-ProtectedStoreAcl([string]$Path, [string]$Sid, [bool]$Directory) {
|
|
435
|
+
if (-not (Test-ProtectedStoreAcl $Path $Sid $Directory)) { Fail 'host_protected_store_acl_invalid' }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function Repair-ProtectedStoreAcl([string]$Path, [string]$Sid, [bool]$Directory) {
|
|
439
|
+
# Rewriting an identical ACL still updates the change time, which every
|
|
440
|
+
# Host's path cache treats as a change. Write only an ACL that is not exact.
|
|
441
|
+
if (-not (Test-ProtectedStoreAcl $Path $Sid $Directory)) { Set-ProtectedStoreAcl $Path $Sid $Directory }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function Get-PublicCapabilityRecord($Record) {
|
|
445
|
+
return [PSCustomObject]@{
|
|
446
|
+
contract = [string]$Record.contract
|
|
447
|
+
capabilityId = [string]$Record.capabilityId
|
|
448
|
+
region = [string]$Record.region
|
|
449
|
+
principalId = [string]$Record.principalId
|
|
450
|
+
delegateId = [string]$Record.delegateId
|
|
451
|
+
hostInstallationId = [string]$Record.hostInstallationId
|
|
452
|
+
adapterId = [string]$Record.adapterId
|
|
453
|
+
version = [int]$Record.version
|
|
454
|
+
state = [string]$Record.state
|
|
455
|
+
scopes = @($Record.scopes)
|
|
456
|
+
issuedAt = [int64]$Record.issuedAt
|
|
457
|
+
revokedAt = $Record.revokedAt
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function Test-CodexNeutralBindingEqual($Left, $Right) {
|
|
462
|
+
return (($Left | ConvertTo-Json -Depth 8 -Compress) -ceq ($Right | ConvertTo-Json -Depth 8 -Compress))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function Test-CodexCapabilityMatch($Capability, $Binding) {
|
|
466
|
+
if (
|
|
467
|
+
[string]$Capability.contract -cne $RecordContract -or
|
|
468
|
+
[string]$Capability.state -cne 'active' -or
|
|
469
|
+
[string]$Capability.region -cne [string]$Binding.region -or
|
|
470
|
+
[string]$Capability.principalId -cne [string]$Binding.principalId -or
|
|
471
|
+
[string]$Capability.delegateId -cne [string]$Binding.delegateId -or
|
|
472
|
+
[string]$Capability.hostInstallationId -cne [string]$Binding.hostInstallationId -or
|
|
473
|
+
[string]$Capability.adapterId -cne 'codex' -or
|
|
474
|
+
[int64]$Capability.version -ne 2 -or
|
|
475
|
+
$Capability.scopes.Count -ne $Binding.scopes.Count
|
|
476
|
+
) { return $false }
|
|
477
|
+
for ($index = 0; $index -lt $Binding.scopes.Count; $index += 1) {
|
|
478
|
+
if ([string]$Capability.scopes[$index] -cne [string]$Binding.scopes[$index]) { return $false }
|
|
479
|
+
}
|
|
480
|
+
return $true
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function Assert-OpenClawNeutralBinding($Binding, [bool]$ValidatePaths, [bool]$RequireLaunch = $false) {
|
|
484
|
+
if ($null -eq $Binding -or $Binding -is [Array]) { Fail 'neutral_openclaw_binding_invalid' }
|
|
485
|
+
$baseFields = @(
|
|
486
|
+
'adapterId', 'contract', 'delegateId', 'hostInstallationId', 'principalId',
|
|
487
|
+
'profileId', 'region', 'runtimeRoot', 'scopes', 'version'
|
|
488
|
+
)
|
|
489
|
+
$releaseFields = @(
|
|
490
|
+
'profileManifestPath', 'releaseManifestSha256', 'releaseRoot', 'runnerRelativePath'
|
|
491
|
+
)
|
|
492
|
+
$releaseCount = 0
|
|
493
|
+
foreach ($field in $releaseFields) {
|
|
494
|
+
if ($null -ne $Binding.PSObject.Properties[$field]) { $releaseCount += 1 }
|
|
495
|
+
}
|
|
496
|
+
$expectedFields = @($baseFields)
|
|
497
|
+
if ($releaseCount -gt 0) { $expectedFields += $releaseFields }
|
|
498
|
+
Assert-ExactFields $Binding $expectedFields 'neutral_openclaw_binding_invalid'
|
|
499
|
+
if (($RequireLaunch -and $releaseCount -ne $releaseFields.Count) -or
|
|
500
|
+
($releaseCount -ne 0 -and $releaseCount -ne $releaseFields.Count)) {
|
|
501
|
+
Fail 'neutral_openclaw_binding_invalid'
|
|
502
|
+
}
|
|
503
|
+
if (
|
|
504
|
+
[string]$Binding.contract -cne $OpenClawNeutralBindingContract -or
|
|
505
|
+
[string]$Binding.region -notmatch $IdentifierPattern -or
|
|
506
|
+
[string]$Binding.principalId -notmatch $IdentifierPattern -or
|
|
507
|
+
[string]$Binding.delegateId -notmatch $IdentifierPattern -or
|
|
508
|
+
[string]$Binding.hostInstallationId -notmatch $IdentifierPattern -or
|
|
509
|
+
[string]$Binding.profileId -notmatch $IdentifierPattern -or
|
|
510
|
+
[string]$Binding.adapterId -cne 'openclaw' -or
|
|
511
|
+
[int64]$Binding.version -ne 2 -or
|
|
512
|
+
$Binding.scopes -isnot [Array] -or
|
|
513
|
+
(-not (Test-ExactScopeSequence $Binding.scopes $OpenClawNeutralScopes) -and
|
|
514
|
+
-not (Test-ExactScopeSequence $Binding.scopes $OpenClawNeutralPreviousScopes))
|
|
515
|
+
) { Fail 'neutral_openclaw_binding_invalid' }
|
|
516
|
+
if ($releaseCount -eq $releaseFields.Count -and (
|
|
517
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.releaseRoot)) -or
|
|
518
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.profileManifestPath)) -or
|
|
519
|
+
[string]$Binding.releaseManifestSha256 -notmatch '^[a-f0-9]{64}$' -or
|
|
520
|
+
[string]$Binding.runnerRelativePath -cne 'runtime/neutral-host-bootstrap.mjs'
|
|
521
|
+
)) { Fail 'neutral_openclaw_launch_configuration_invalid' }
|
|
522
|
+
if ($ValidatePaths) {
|
|
523
|
+
$runtimeRoot = Assert-AbsoluteNonReparsePath ([string]$Binding.runtimeRoot) $true 0
|
|
524
|
+
if (-not [string]::Equals(
|
|
525
|
+
$runtimeRoot,
|
|
526
|
+
[IO.Path]::GetFullPath([string]$Binding.runtimeRoot),
|
|
527
|
+
[StringComparison]::OrdinalIgnoreCase
|
|
528
|
+
)) { Fail 'neutral_openclaw_binding_invalid' }
|
|
529
|
+
}
|
|
530
|
+
return $Binding
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function Test-OpenClawNeutralBindingEqual($Left, $Right) {
|
|
534
|
+
return (($Left | ConvertTo-Json -Depth 8 -Compress) -ceq ($Right | ConvertTo-Json -Depth 8 -Compress))
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function Test-OpenClawCapabilityMatch($Capability, $Binding) {
|
|
538
|
+
if (
|
|
539
|
+
[string]$Capability.contract -cne $RecordContract -or
|
|
540
|
+
[string]$Capability.state -cne 'active' -or
|
|
541
|
+
[string]$Capability.region -cne [string]$Binding.region -or
|
|
542
|
+
[string]$Capability.principalId -cne [string]$Binding.principalId -or
|
|
543
|
+
[string]$Capability.delegateId -cne [string]$Binding.delegateId -or
|
|
544
|
+
[string]$Capability.hostInstallationId -cne [string]$Binding.hostInstallationId -or
|
|
545
|
+
[string]$Capability.adapterId -cne 'openclaw' -or
|
|
546
|
+
[int64]$Capability.version -ne 2 -or
|
|
547
|
+
$Capability.scopes.Count -ne $Binding.scopes.Count
|
|
548
|
+
) { return $false }
|
|
549
|
+
for ($index = 0; $index -lt $Binding.scopes.Count; $index += 1) {
|
|
550
|
+
if ([string]$Capability.scopes[$index] -cne [string]$Binding.scopes[$index]) { return $false }
|
|
551
|
+
}
|
|
552
|
+
return $true
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function Assert-TraeWorkNeutralBinding($Binding, [bool]$ValidatePaths) {
|
|
556
|
+
if ($null -eq $Binding -or $Binding -is [Array]) { Fail 'neutral_trae_work_binding_invalid' }
|
|
557
|
+
$baseFields = @(
|
|
558
|
+
'adapterId', 'contract', 'delegateId', 'hostInstallationId', 'principalId',
|
|
559
|
+
'profileId', 'region', 'runtimeRoot', 'scopes', 'version'
|
|
560
|
+
)
|
|
561
|
+
$releaseFields = @(
|
|
562
|
+
'profileManifestPath', 'releaseManifestSha256', 'releaseRoot', 'runnerRelativePath'
|
|
563
|
+
)
|
|
564
|
+
$releaseCount = 0
|
|
565
|
+
foreach ($field in $releaseFields) {
|
|
566
|
+
if ($null -ne $Binding.PSObject.Properties[$field]) { $releaseCount += 1 }
|
|
567
|
+
}
|
|
568
|
+
$expectedFields = @($baseFields)
|
|
569
|
+
if ($releaseCount -gt 0) { $expectedFields += $releaseFields }
|
|
570
|
+
Assert-ExactFields $Binding $expectedFields 'neutral_trae_work_binding_invalid'
|
|
571
|
+
if ($releaseCount -ne 0 -and $releaseCount -ne $releaseFields.Count) {
|
|
572
|
+
Fail 'neutral_trae_work_binding_invalid'
|
|
573
|
+
}
|
|
574
|
+
if (
|
|
575
|
+
[string]$Binding.contract -cne $TraeWorkNeutralBindingContract -or
|
|
576
|
+
[string]$Binding.region -notmatch $IdentifierPattern -or
|
|
577
|
+
[string]$Binding.principalId -notmatch $IdentifierPattern -or
|
|
578
|
+
[string]$Binding.delegateId -notmatch $IdentifierPattern -or
|
|
579
|
+
[string]$Binding.hostInstallationId -notmatch $IdentifierPattern -or
|
|
580
|
+
[string]$Binding.profileId -notmatch $IdentifierPattern -or
|
|
581
|
+
[string]$Binding.adapterId -cne 'trae-work' -or
|
|
582
|
+
[int64]$Binding.version -ne 2 -or
|
|
583
|
+
$Binding.scopes -isnot [Array]
|
|
584
|
+
) { Fail 'neutral_trae_work_binding_invalid' }
|
|
585
|
+
if (-not (Test-ExactScopeSequence $Binding.scopes $TraeWorkNeutralScopes) -and
|
|
586
|
+
-not (Test-ExactScopeSequence $Binding.scopes $TraeWorkNeutralPreviousScopes) -and
|
|
587
|
+
-not (Test-ExactScopeSequence $Binding.scopes $TraeWorkNeutralLegacyScopes)) {
|
|
588
|
+
Fail 'neutral_trae_work_capability_binding_mismatch'
|
|
589
|
+
}
|
|
590
|
+
if ($releaseCount -eq $releaseFields.Count -and (
|
|
591
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.releaseRoot)) -or
|
|
592
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.profileManifestPath)) -or
|
|
593
|
+
[string]$Binding.releaseManifestSha256 -notmatch '^[a-f0-9]{64}$' -or
|
|
594
|
+
[string]$Binding.runnerRelativePath -cne 'runtime/neutral-host-bootstrap.mjs'
|
|
595
|
+
)) { Fail 'neutral_trae_work_launch_configuration_invalid' }
|
|
596
|
+
if ($ValidatePaths) {
|
|
597
|
+
$runtimeRoot = Assert-AbsoluteNonReparsePath ([string]$Binding.runtimeRoot) $true 0
|
|
598
|
+
if (-not [string]::Equals(
|
|
599
|
+
$runtimeRoot,
|
|
600
|
+
[IO.Path]::GetFullPath([string]$Binding.runtimeRoot),
|
|
601
|
+
[StringComparison]::OrdinalIgnoreCase
|
|
602
|
+
)) { Fail 'neutral_trae_work_binding_invalid' }
|
|
603
|
+
}
|
|
604
|
+
return $Binding
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function Test-TraeWorkNeutralBindingEqual($Left, $Right) {
|
|
608
|
+
return (($Left | ConvertTo-Json -Depth 8 -Compress) -ceq ($Right | ConvertTo-Json -Depth 8 -Compress))
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function Test-TraeWorkCapabilityMatch($Capability, $Binding) {
|
|
612
|
+
if (
|
|
613
|
+
[string]$Capability.contract -cne $RecordContract -or
|
|
614
|
+
[string]$Capability.state -cne 'active' -or
|
|
615
|
+
[string]$Capability.region -cne [string]$Binding.region -or
|
|
616
|
+
[string]$Capability.principalId -cne [string]$Binding.principalId -or
|
|
617
|
+
[string]$Capability.delegateId -cne [string]$Binding.delegateId -or
|
|
618
|
+
[string]$Capability.hostInstallationId -cne [string]$Binding.hostInstallationId -or
|
|
619
|
+
[string]$Capability.adapterId -cne 'trae-work' -or
|
|
620
|
+
[int64]$Capability.version -ne 2 -or
|
|
621
|
+
$Capability.scopes.Count -ne $Binding.scopes.Count
|
|
622
|
+
) { return $false }
|
|
623
|
+
for ($index = 0; $index -lt $Binding.scopes.Count; $index += 1) {
|
|
624
|
+
if ([string]$Capability.scopes[$index] -cne [string]$Binding.scopes[$index]) { return $false }
|
|
625
|
+
}
|
|
626
|
+
return $true
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function Assert-WorkBuddyNeutralBinding($Binding, [bool]$ValidatePaths) {
|
|
630
|
+
if ($null -eq $Binding -or $Binding -is [Array]) { Fail 'neutral_workbuddy_binding_invalid' }
|
|
631
|
+
$baseFields = @(
|
|
632
|
+
'adapterId', 'contract', 'delegateId', 'hostInstallationId', 'principalId',
|
|
633
|
+
'profileId', 'region', 'runtimeRoot', 'scopes', 'version'
|
|
634
|
+
)
|
|
635
|
+
$releaseFields = @(
|
|
636
|
+
'profileManifestPath', 'releaseManifestSha256', 'releaseRoot', 'runnerRelativePath'
|
|
637
|
+
)
|
|
638
|
+
$releaseCount = 0
|
|
639
|
+
foreach ($field in $releaseFields) {
|
|
640
|
+
if ($null -ne $Binding.PSObject.Properties[$field]) { $releaseCount += 1 }
|
|
641
|
+
}
|
|
642
|
+
$expectedFields = @($baseFields)
|
|
643
|
+
if ($releaseCount -gt 0) { $expectedFields += $releaseFields }
|
|
644
|
+
Assert-ExactFields $Binding $expectedFields 'neutral_workbuddy_binding_invalid'
|
|
645
|
+
if ($releaseCount -ne 0 -and $releaseCount -ne $releaseFields.Count) {
|
|
646
|
+
Fail 'neutral_workbuddy_binding_invalid'
|
|
647
|
+
}
|
|
648
|
+
if (
|
|
649
|
+
[string]$Binding.contract -cne $WorkBuddyNeutralBindingContract -or
|
|
650
|
+
[string]$Binding.region -notmatch $IdentifierPattern -or
|
|
651
|
+
[string]$Binding.principalId -notmatch $IdentifierPattern -or
|
|
652
|
+
[string]$Binding.delegateId -notmatch $IdentifierPattern -or
|
|
653
|
+
[string]$Binding.hostInstallationId -notmatch $IdentifierPattern -or
|
|
654
|
+
[string]$Binding.profileId -notmatch $IdentifierPattern -or
|
|
655
|
+
[string]$Binding.adapterId -cne 'workbuddy' -or
|
|
656
|
+
[int64]$Binding.version -ne 2 -or
|
|
657
|
+
$Binding.scopes -isnot [Array]
|
|
658
|
+
) { Fail 'neutral_workbuddy_binding_invalid' }
|
|
659
|
+
if (-not (Test-ExactScopeSequence $Binding.scopes $WorkBuddyNeutralScopes) -and
|
|
660
|
+
-not (Test-ExactScopeSequence $Binding.scopes $WorkBuddyNeutralPreviousScopes) -and
|
|
661
|
+
-not (Test-ExactScopeSequence $Binding.scopes $WorkBuddyNeutralLegacyScopes)) {
|
|
662
|
+
Fail 'neutral_workbuddy_capability_binding_mismatch'
|
|
663
|
+
}
|
|
664
|
+
if ($releaseCount -eq $releaseFields.Count -and (
|
|
665
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.releaseRoot)) -or
|
|
666
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.profileManifestPath)) -or
|
|
667
|
+
[string]$Binding.releaseManifestSha256 -notmatch '^[a-f0-9]{64}$' -or
|
|
668
|
+
[string]$Binding.runnerRelativePath -cne 'runtime/neutral-host-bootstrap.mjs'
|
|
669
|
+
)) { Fail 'neutral_workbuddy_launch_configuration_invalid' }
|
|
670
|
+
if ($ValidatePaths) {
|
|
671
|
+
$runtimeRoot = Assert-AbsoluteNonReparsePath ([string]$Binding.runtimeRoot) $true 0
|
|
672
|
+
if (-not [string]::Equals(
|
|
673
|
+
$runtimeRoot,
|
|
674
|
+
[IO.Path]::GetFullPath([string]$Binding.runtimeRoot),
|
|
675
|
+
[StringComparison]::OrdinalIgnoreCase
|
|
676
|
+
)) { Fail 'neutral_workbuddy_binding_invalid' }
|
|
677
|
+
}
|
|
678
|
+
return $Binding
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function Test-WorkBuddyNeutralBindingEqual($Left, $Right) {
|
|
682
|
+
return (($Left | ConvertTo-Json -Depth 8 -Compress) -ceq ($Right | ConvertTo-Json -Depth 8 -Compress))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function Test-WorkBuddyCapabilityMatch($Capability, $Binding) {
|
|
686
|
+
if (
|
|
687
|
+
[string]$Capability.contract -cne $RecordContract -or
|
|
688
|
+
[string]$Capability.state -cne 'active' -or
|
|
689
|
+
[string]$Capability.region -cne [string]$Binding.region -or
|
|
690
|
+
[string]$Capability.principalId -cne [string]$Binding.principalId -or
|
|
691
|
+
[string]$Capability.delegateId -cne [string]$Binding.delegateId -or
|
|
692
|
+
[string]$Capability.hostInstallationId -cne [string]$Binding.hostInstallationId -or
|
|
693
|
+
[string]$Capability.adapterId -cne 'workbuddy' -or
|
|
694
|
+
[int64]$Capability.version -ne 2 -or
|
|
695
|
+
$Capability.scopes.Count -ne $Binding.scopes.Count
|
|
696
|
+
) { return $false }
|
|
697
|
+
for ($index = 0; $index -lt $Binding.scopes.Count; $index += 1) {
|
|
698
|
+
if ([string]$Capability.scopes[$index] -cne [string]$Binding.scopes[$index]) { return $false }
|
|
699
|
+
}
|
|
700
|
+
return $true
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function Assert-ClaudeCodeNeutralBinding($Binding, [bool]$ValidatePaths) {
|
|
704
|
+
if ($null -eq $Binding -or $Binding -is [Array]) { Fail 'neutral_claude_code_binding_invalid' }
|
|
705
|
+
$baseFields = @(
|
|
706
|
+
'adapterId', 'contract', 'delegateId', 'hostInstallationId', 'principalId',
|
|
707
|
+
'profileId', 'region', 'runtimeRoot', 'scopes', 'version'
|
|
708
|
+
)
|
|
709
|
+
$releaseFields = @(
|
|
710
|
+
'profileManifestPath', 'releaseManifestSha256', 'releaseRoot', 'runnerRelativePath'
|
|
711
|
+
)
|
|
712
|
+
$releaseCount = 0
|
|
713
|
+
foreach ($field in $releaseFields) {
|
|
714
|
+
if ($null -ne $Binding.PSObject.Properties[$field]) { $releaseCount += 1 }
|
|
715
|
+
}
|
|
716
|
+
$expectedFields = @($baseFields)
|
|
717
|
+
if ($releaseCount -gt 0) { $expectedFields += $releaseFields }
|
|
718
|
+
Assert-ExactFields $Binding $expectedFields 'neutral_claude_code_binding_invalid'
|
|
719
|
+
if ($releaseCount -ne 0 -and $releaseCount -ne $releaseFields.Count) {
|
|
720
|
+
Fail 'neutral_claude_code_binding_invalid'
|
|
721
|
+
}
|
|
722
|
+
if (
|
|
723
|
+
[string]$Binding.contract -cne $ClaudeCodeNeutralBindingContract -or
|
|
724
|
+
[string]$Binding.region -notmatch $IdentifierPattern -or
|
|
725
|
+
[string]$Binding.principalId -notmatch $IdentifierPattern -or
|
|
726
|
+
[string]$Binding.delegateId -notmatch $IdentifierPattern -or
|
|
727
|
+
[string]$Binding.hostInstallationId -notmatch $IdentifierPattern -or
|
|
728
|
+
[string]$Binding.profileId -notmatch $IdentifierPattern -or
|
|
729
|
+
[string]$Binding.adapterId -cne 'claude-code' -or
|
|
730
|
+
[int64]$Binding.version -ne 2 -or
|
|
731
|
+
$Binding.scopes -isnot [Array]
|
|
732
|
+
) { Fail 'neutral_claude_code_binding_invalid' }
|
|
733
|
+
if (-not (Test-ExactScopeSequence $Binding.scopes $ClaudeCodeNeutralScopes)) {
|
|
734
|
+
Fail 'neutral_claude_code_capability_binding_mismatch'
|
|
735
|
+
}
|
|
736
|
+
if ($releaseCount -eq $releaseFields.Count -and (
|
|
737
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.releaseRoot)) -or
|
|
738
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.profileManifestPath)) -or
|
|
739
|
+
[string]$Binding.releaseManifestSha256 -notmatch '^[a-f0-9]{64}$' -or
|
|
740
|
+
[string]$Binding.runnerRelativePath -cne 'runtime/neutral-host-bootstrap.mjs'
|
|
741
|
+
)) { Fail 'neutral_claude_code_launch_configuration_invalid' }
|
|
742
|
+
if ($ValidatePaths) {
|
|
743
|
+
$runtimeRoot = Assert-AbsoluteNonReparsePath ([string]$Binding.runtimeRoot) $true 0
|
|
744
|
+
if (-not [string]::Equals(
|
|
745
|
+
$runtimeRoot,
|
|
746
|
+
[IO.Path]::GetFullPath([string]$Binding.runtimeRoot),
|
|
747
|
+
[StringComparison]::OrdinalIgnoreCase
|
|
748
|
+
)) { Fail 'neutral_claude_code_binding_invalid' }
|
|
749
|
+
}
|
|
750
|
+
return $Binding
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function Test-ClaudeCodeNeutralBindingEqual($Left, $Right) {
|
|
754
|
+
return (($Left | ConvertTo-Json -Depth 8 -Compress) -ceq ($Right | ConvertTo-Json -Depth 8 -Compress))
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function Test-ClaudeCodeCapabilityMatch($Capability, $Binding) {
|
|
758
|
+
if (
|
|
759
|
+
[string]$Capability.contract -cne $RecordContract -or
|
|
760
|
+
[string]$Capability.state -cne 'active' -or
|
|
761
|
+
[string]$Capability.region -cne [string]$Binding.region -or
|
|
762
|
+
[string]$Capability.principalId -cne [string]$Binding.principalId -or
|
|
763
|
+
[string]$Capability.delegateId -cne [string]$Binding.delegateId -or
|
|
764
|
+
[string]$Capability.hostInstallationId -cne [string]$Binding.hostInstallationId -or
|
|
765
|
+
[string]$Capability.adapterId -cne 'claude-code' -or
|
|
766
|
+
[int64]$Capability.version -ne 2 -or
|
|
767
|
+
$Capability.scopes.Count -ne $Binding.scopes.Count
|
|
768
|
+
) { return $false }
|
|
769
|
+
for ($index = 0; $index -lt $Binding.scopes.Count; $index += 1) {
|
|
770
|
+
if ([string]$Capability.scopes[$index] -cne [string]$Binding.scopes[$index]) { return $false }
|
|
771
|
+
}
|
|
772
|
+
return $true
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function Assert-CursorNeutralBinding($Binding, [bool]$ValidatePaths, [bool]$RequireCurrentScopes = $false) {
|
|
776
|
+
if ($null -eq $Binding -or $Binding -is [Array]) { Fail 'neutral_cursor_binding_invalid' }
|
|
777
|
+
$baseFields = @(
|
|
778
|
+
'adapterId', 'contract', 'delegateId', 'hostInstallationId', 'principalId',
|
|
779
|
+
'profileId', 'region', 'runtimeRoot', 'scopes', 'version'
|
|
780
|
+
)
|
|
781
|
+
$releaseFields = @(
|
|
782
|
+
'profileManifestPath', 'releaseManifestSha256', 'releaseRoot', 'runnerRelativePath'
|
|
783
|
+
)
|
|
784
|
+
$releaseCount = 0
|
|
785
|
+
foreach ($field in $releaseFields) {
|
|
786
|
+
if ($null -ne $Binding.PSObject.Properties[$field]) { $releaseCount += 1 }
|
|
787
|
+
}
|
|
788
|
+
$expectedFields = @($baseFields)
|
|
789
|
+
if ($releaseCount -gt 0) { $expectedFields += $releaseFields }
|
|
790
|
+
Assert-ExactFields $Binding $expectedFields 'neutral_cursor_binding_invalid'
|
|
791
|
+
if ($releaseCount -ne 0 -and $releaseCount -ne $releaseFields.Count) {
|
|
792
|
+
Fail 'neutral_cursor_binding_invalid'
|
|
793
|
+
}
|
|
794
|
+
if (
|
|
795
|
+
[string]$Binding.contract -cne $CursorNeutralBindingContract -or
|
|
796
|
+
[string]$Binding.region -notmatch $IdentifierPattern -or
|
|
797
|
+
[string]$Binding.principalId -notmatch $IdentifierPattern -or
|
|
798
|
+
[string]$Binding.delegateId -notmatch $IdentifierPattern -or
|
|
799
|
+
[string]$Binding.hostInstallationId -notmatch $IdentifierPattern -or
|
|
800
|
+
[string]$Binding.profileId -notmatch $IdentifierPattern -or
|
|
801
|
+
[string]$Binding.adapterId -cne 'cursor' -or
|
|
802
|
+
[int64]$Binding.version -ne 2 -or
|
|
803
|
+
$Binding.scopes -isnot [Array] -or
|
|
804
|
+
(-not (Test-ExactScopeSequence $Binding.scopes $CursorNeutralScopes) -and
|
|
805
|
+
($RequireCurrentScopes -or -not (Test-ExactScopeSequence $Binding.scopes $CursorNeutralPreviousScopes)))
|
|
806
|
+
) { Fail 'neutral_cursor_binding_invalid' }
|
|
807
|
+
if ($releaseCount -eq $releaseFields.Count -and (
|
|
808
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.releaseRoot)) -or
|
|
809
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.profileManifestPath)) -or
|
|
810
|
+
[string]$Binding.releaseManifestSha256 -notmatch '^[a-f0-9]{64}$' -or
|
|
811
|
+
[string]$Binding.runnerRelativePath -cne 'runtime/neutral-host-bootstrap.mjs'
|
|
812
|
+
)) { Fail 'neutral_cursor_launch_configuration_invalid' }
|
|
813
|
+
if ($ValidatePaths) {
|
|
814
|
+
$runtimeRoot = Assert-AbsoluteNonReparsePath (
|
|
815
|
+
[string]$Binding.runtimeRoot
|
|
816
|
+
) $true 0 'neutral_cursor_binding_invalid' 'neutral_cursor_launch_configuration_invalid'
|
|
817
|
+
if (-not [string]::Equals(
|
|
818
|
+
$runtimeRoot,
|
|
819
|
+
[IO.Path]::GetFullPath([string]$Binding.runtimeRoot),
|
|
820
|
+
[StringComparison]::OrdinalIgnoreCase
|
|
821
|
+
)) { Fail 'neutral_cursor_binding_invalid' }
|
|
822
|
+
}
|
|
823
|
+
return $Binding
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function Test-CursorNeutralBindingEqual($Left, $Right) {
|
|
827
|
+
return (($Left | ConvertTo-Json -Depth 8 -Compress) -ceq ($Right | ConvertTo-Json -Depth 8 -Compress))
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function Test-CursorCapabilityMatch($Capability, $Binding) {
|
|
831
|
+
if (
|
|
832
|
+
[string]$Capability.contract -cne $RecordContract -or
|
|
833
|
+
[string]$Capability.state -cne 'active' -or
|
|
834
|
+
[string]$Capability.region -cne [string]$Binding.region -or
|
|
835
|
+
[string]$Capability.principalId -cne [string]$Binding.principalId -or
|
|
836
|
+
[string]$Capability.delegateId -cne [string]$Binding.delegateId -or
|
|
837
|
+
[string]$Capability.hostInstallationId -cne [string]$Binding.hostInstallationId -or
|
|
838
|
+
[string]$Capability.adapterId -cne 'cursor' -or
|
|
839
|
+
[int64]$Capability.version -ne 2 -or
|
|
840
|
+
$Capability.scopes.Count -ne $Binding.scopes.Count
|
|
841
|
+
) { return $false }
|
|
842
|
+
for ($index = 0; $index -lt $Binding.scopes.Count; $index += 1) {
|
|
843
|
+
if ([string]$Capability.scopes[$index] -cne [string]$Binding.scopes[$index]) { return $false }
|
|
844
|
+
}
|
|
845
|
+
return $true
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function Assert-DshNeutralBinding($Binding, [bool]$ValidatePaths, [bool]$RequireCurrentScopes = $false) {
|
|
849
|
+
if ($null -eq $Binding -or $Binding -is [Array]) { Fail 'neutral_dsh_binding_invalid' }
|
|
850
|
+
Assert-ExactFields $Binding @(
|
|
851
|
+
'adapterId', 'contract', 'delegateId', 'hostInstallationId', 'principalId',
|
|
852
|
+
'profileId', 'profileManifestPath', 'region', 'releaseManifestSha256', 'releaseRoot',
|
|
853
|
+
'runnerRelativePath', 'runtimeRoot', 'scopes', 'version'
|
|
854
|
+
) 'neutral_dsh_binding_invalid'
|
|
855
|
+
if ([string]$Binding.contract -cne $DshNeutralBindingContract -or
|
|
856
|
+
[string]$Binding.region -notmatch $IdentifierPattern -or
|
|
857
|
+
[string]$Binding.principalId -notmatch $IdentifierPattern -or
|
|
858
|
+
[string]$Binding.delegateId -notmatch $IdentifierPattern -or
|
|
859
|
+
[string]$Binding.hostInstallationId -notmatch $IdentifierPattern -or
|
|
860
|
+
[string]$Binding.profileId -notmatch $IdentifierPattern -or
|
|
861
|
+
[string]$Binding.adapterId -cne 'dsh' -or [int64]$Binding.version -ne 2 -or
|
|
862
|
+
$Binding.scopes -isnot [Array] -or
|
|
863
|
+
(-not (Test-ExactScopeSequence $Binding.scopes $DshNeutralScopes) -and
|
|
864
|
+
($RequireCurrentScopes -or -not (Test-ExactScopeSequence $Binding.scopes $DshNeutralPreviousScopes)))) {
|
|
865
|
+
Fail 'neutral_dsh_binding_invalid'
|
|
866
|
+
}
|
|
867
|
+
if (-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.releaseRoot)) -or
|
|
868
|
+
-not (Test-FullyQualifiedCanonicalPath ([string]$Binding.profileManifestPath)) -or
|
|
869
|
+
[string]$Binding.releaseManifestSha256 -notmatch '^[a-f0-9]{64}$' -or
|
|
870
|
+
[string]$Binding.runnerRelativePath -cne 'runtime/neutral-host-bootstrap.mjs') { Fail 'neutral_dsh_launch_configuration_invalid' }
|
|
871
|
+
if ($ValidatePaths) {
|
|
872
|
+
$runtimeRoot = Assert-AbsoluteNonReparsePath ([string]$Binding.runtimeRoot) $true 0 'neutral_dsh_binding_invalid' 'neutral_dsh_launch_configuration_invalid'
|
|
873
|
+
if (-not [string]::Equals($runtimeRoot, [IO.Path]::GetFullPath([string]$Binding.runtimeRoot), [StringComparison]::OrdinalIgnoreCase)) { Fail 'neutral_dsh_binding_invalid' }
|
|
874
|
+
}
|
|
875
|
+
return $Binding
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function Test-DshNeutralBindingEqual($Left, $Right) { return (($Left | ConvertTo-Json -Depth 8 -Compress) -ceq ($Right | ConvertTo-Json -Depth 8 -Compress)) }
|
|
879
|
+
function Test-DshCapabilityMatch($Capability, $Binding) {
|
|
880
|
+
if ([string]$Capability.contract -cne $RecordContract -or [string]$Capability.state -cne 'active' -or
|
|
881
|
+
[string]$Capability.region -cne [string]$Binding.region -or [string]$Capability.principalId -cne [string]$Binding.principalId -or
|
|
882
|
+
[string]$Capability.delegateId -cne [string]$Binding.delegateId -or [string]$Capability.hostInstallationId -cne [string]$Binding.hostInstallationId -or
|
|
883
|
+
[string]$Capability.adapterId -cne 'dsh' -or [int64]$Capability.version -ne 2 -or $Capability.scopes.Count -ne $Binding.scopes.Count) { return $false }
|
|
884
|
+
for ($index = 0; $index -lt $Binding.scopes.Count; $index += 1) { if ([string]$Capability.scopes[$index] -cne [string]$Binding.scopes[$index]) { return $false } }
|
|
885
|
+
return $true
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function Test-NeutralBindingOwnerEqual($Left, $Right) {
|
|
889
|
+
foreach ($field in @(
|
|
890
|
+
'contract', 'region', 'principalId', 'delegateId', 'hostInstallationId',
|
|
891
|
+
'profileId', 'adapterId', 'version', 'runtimeRoot'
|
|
892
|
+
)) {
|
|
893
|
+
if ([string]$Left.$field -cne [string]$Right.$field) { return $false }
|
|
894
|
+
}
|
|
895
|
+
return $true
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function Assert-TransportCredential($Record) {
|
|
899
|
+
Assert-ExactFields $Record @(
|
|
900
|
+
'contract', 'credentialId', 'principalId', 'hostInstallationId', 'state',
|
|
901
|
+
'secret', 'issuedAt', 'expiresAt', 'revokedAt'
|
|
902
|
+
) 'host_transport_credential_fields_invalid'
|
|
903
|
+
if (
|
|
904
|
+
[string]$Record.contract -cne $TransportCredentialContract -or
|
|
905
|
+
[string]$Record.credentialId -notmatch $IdentifierPattern -or
|
|
906
|
+
[string]$Record.principalId -notmatch $IdentifierPattern -or
|
|
907
|
+
[string]$Record.hostInstallationId -notmatch $IdentifierPattern -or
|
|
908
|
+
[string]$Record.secret -notmatch $TransportCredentialSecretPattern -or
|
|
909
|
+
[string]$Matches[1] -cne [string]$Record.credentialId -or
|
|
910
|
+
@('active', 'revoked') -notcontains [string]$Record.state -or
|
|
911
|
+
[int64]$Record.issuedAt -lt 0 -or
|
|
912
|
+
[int64]$Record.expiresAt -le [int64]$Record.issuedAt
|
|
913
|
+
) { Fail 'host_transport_credential_invalid' }
|
|
914
|
+
if (
|
|
915
|
+
([string]$Record.state -eq 'active' -and $null -ne $Record.revokedAt) -or
|
|
916
|
+
([string]$Record.state -eq 'revoked' -and (
|
|
917
|
+
$null -eq $Record.revokedAt -or [int64]$Record.revokedAt -lt [int64]$Record.issuedAt
|
|
918
|
+
))
|
|
919
|
+
) { Fail 'host_transport_credential_state_invalid' }
|
|
920
|
+
return $Record
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function Assert-TransportSession($Record) {
|
|
924
|
+
Assert-ExactFields $Record @(
|
|
925
|
+
'contract', 'sessionId', 'sessionSecret', 'credentialId', 'principalId',
|
|
926
|
+
'hostInstallationId', 'issuedAt', 'expiresAt'
|
|
927
|
+
) 'host_transport_session_fields_invalid'
|
|
928
|
+
if (
|
|
929
|
+
[string]$Record.contract -cne $TransportSessionContract -or
|
|
930
|
+
[string]$Record.sessionId -notmatch $TransportSessionIdPattern -or
|
|
931
|
+
[string]$Record.sessionSecret -notmatch $TransportSessionSecretPattern -or
|
|
932
|
+
[string]$Record.credentialId -notmatch $IdentifierPattern -or
|
|
933
|
+
[string]$Record.principalId -notmatch $IdentifierPattern -or
|
|
934
|
+
[string]$Record.hostInstallationId -notmatch $IdentifierPattern -or
|
|
935
|
+
[int64]$Record.issuedAt -lt 0 -or
|
|
936
|
+
[int64]$Record.expiresAt -le [int64]$Record.issuedAt
|
|
937
|
+
) { Fail 'host_transport_session_invalid' }
|
|
938
|
+
return $Record
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function Assert-QueryOwner {
|
|
942
|
+
if (
|
|
943
|
+
[string]$PrincipalId -notmatch $IdentifierPattern -or
|
|
944
|
+
[string]$HostInstallationId -notmatch $IdentifierPattern
|
|
945
|
+
) { Fail 'host_transport_owner_invalid' }
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function Test-TransportCredentialEqual($Left, $Right) {
|
|
949
|
+
return (
|
|
950
|
+
[string]$Left.contract -ceq [string]$Right.contract -and
|
|
951
|
+
[string]$Left.credentialId -ceq [string]$Right.credentialId -and
|
|
952
|
+
[string]$Left.principalId -ceq [string]$Right.principalId -and
|
|
953
|
+
[string]$Left.hostInstallationId -ceq [string]$Right.hostInstallationId -and
|
|
954
|
+
[string]$Left.state -ceq [string]$Right.state -and
|
|
955
|
+
[string]$Left.secret -ceq [string]$Right.secret -and
|
|
956
|
+
[int64]$Left.issuedAt -eq [int64]$Right.issuedAt -and
|
|
957
|
+
[int64]$Left.expiresAt -eq [int64]$Right.expiresAt -and
|
|
958
|
+
(($null -eq $Left.revokedAt -and $null -eq $Right.revokedAt) -or
|
|
959
|
+
[int64]$Left.revokedAt -eq [int64]$Right.revokedAt)
|
|
960
|
+
)
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function Read-DeviceEnrollment([string]$EnrollmentPath, [string]$Sid) {
|
|
964
|
+
$enrollment = [PSCustomObject]@{ contract = 'hellolynk-device-enrollment-v1'; revision = 0; value = $null }
|
|
965
|
+
Assert-ProtectedStoreNoReparse $EnrollmentPath
|
|
966
|
+
if (Test-Path -LiteralPath $EnrollmentPath) {
|
|
967
|
+
Assert-ProtectedStoreAcl $EnrollmentPath $Sid $false
|
|
968
|
+
$encrypted = Import-Clixml -LiteralPath $EnrollmentPath
|
|
969
|
+
if ($encrypted -isnot [Management.Automation.PSCredential] -or $encrypted.UserName -cne $CredentialName) {
|
|
970
|
+
Fail 'device_enrollment_store_invalid'
|
|
971
|
+
}
|
|
972
|
+
$plainEnrollment = $encrypted.GetNetworkCredential().Password
|
|
973
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plainEnrollment) -gt 65536) { Fail 'device_enrollment_store_invalid' }
|
|
974
|
+
try { $enrollment = $plainEnrollment | ConvertFrom-Json } catch { Fail 'device_enrollment_store_invalid' }
|
|
975
|
+
$plainEnrollment = $null
|
|
976
|
+
Assert-ExactFields $enrollment @('contract', 'revision', 'value') 'device_enrollment_store_invalid'
|
|
977
|
+
if ($enrollment.contract -cne 'hellolynk-device-enrollment-v1' -or
|
|
978
|
+
$enrollment.revision -isnot [long] -and $enrollment.revision -isnot [int] -or
|
|
979
|
+
$enrollment.revision -lt 0 -or $enrollment.revision -ge 9007199254740991) {
|
|
980
|
+
Fail 'device_enrollment_store_invalid'
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return $enrollment
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function Test-TransportSessionEqual($Left, $Right) {
|
|
987
|
+
return (
|
|
988
|
+
[string]$Left.contract -ceq [string]$Right.contract -and
|
|
989
|
+
[string]$Left.sessionId -ceq [string]$Right.sessionId -and
|
|
990
|
+
[string]$Left.sessionSecret -ceq [string]$Right.sessionSecret -and
|
|
991
|
+
[string]$Left.credentialId -ceq [string]$Right.credentialId -and
|
|
992
|
+
[string]$Left.principalId -ceq [string]$Right.principalId -and
|
|
993
|
+
[string]$Left.hostInstallationId -ceq [string]$Right.hostInstallationId -and
|
|
994
|
+
[int64]$Left.issuedAt -eq [int64]$Right.issuedAt -and
|
|
995
|
+
[int64]$Left.expiresAt -eq [int64]$Right.expiresAt
|
|
996
|
+
)
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function Get-ActiveTransportCredential($Store, [int64]$Now) {
|
|
1000
|
+
Assert-QueryOwner
|
|
1001
|
+
$matching = @($Store.transportCredentials | Where-Object {
|
|
1002
|
+
[string]$_.principalId -ceq $PrincipalId -and
|
|
1003
|
+
[string]$_.hostInstallationId -ceq $HostInstallationId -and
|
|
1004
|
+
[string]$_.state -ceq 'active'
|
|
1005
|
+
})
|
|
1006
|
+
if ($matching.Count -eq 0) { Fail 'host_transport_credential_required' }
|
|
1007
|
+
if ($matching.Count -gt 1) { Fail 'host_transport_credential_ambiguous' }
|
|
1008
|
+
if ([int64]$matching[0].expiresAt -le $Now) { Fail 'host_transport_credential_expired' }
|
|
1009
|
+
return $matching[0]
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
# Immutable, authenticated radix pages keep retirement evidence out of the
|
|
1013
|
+
# current-state envelope. Page limits bound individual reads, not lifetime use.
|
|
1014
|
+
function Get-RetiredDigest([byte[]]$Bytes) {
|
|
1015
|
+
return [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($Bytes)).ToLowerInvariant()
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function Assert-RetiredReference($Reference) {
|
|
1019
|
+
if ($null -eq $Reference) { return }
|
|
1020
|
+
Assert-ExactFields $Reference @('hash', 'count') 'host_retired_index_invalid'
|
|
1021
|
+
if ([string]$Reference.hash -cnotmatch '^[a-f0-9]{64}$' -or
|
|
1022
|
+
($Reference.count -isnot [long] -and $Reference.count -isnot [int]) -or $Reference.count -le 0) {
|
|
1023
|
+
Fail 'host_retired_index_invalid'
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function Get-RetiredDirectory([string]$Root) {
|
|
1028
|
+
$directory = Join-Path $Root 'transport-retired-v1'
|
|
1029
|
+
Assert-ProtectedStoreNoReparse $directory
|
|
1030
|
+
return $directory
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function Protect-RetiredJson($Value) {
|
|
1034
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes(($Value | ConvertTo-Json -Depth 12 -Compress))
|
|
1035
|
+
try {
|
|
1036
|
+
return ,([Security.Cryptography.ProtectedData]::Protect($bytes,
|
|
1037
|
+
[Text.Encoding]::UTF8.GetBytes($RetiredIndexContract), [Security.Cryptography.DataProtectionScope]::CurrentUser))
|
|
1038
|
+
} finally { [Array]::Clear($bytes, 0, $bytes.Length) }
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function Read-RetiredJson([string]$Path, [int64]$MaxBytes, [string]$Hash = '') {
|
|
1042
|
+
$null = Assert-AbsoluteNonReparsePath $Path $false $MaxBytes 'host_retired_index_invalid' 'host_retired_index_unavailable'
|
|
1043
|
+
$cipher = [IO.File]::ReadAllBytes($Path)
|
|
1044
|
+
if ($Hash -and (Get-RetiredDigest $cipher) -cne $Hash) { Fail 'host_retired_index_integrity_failed' }
|
|
1045
|
+
$plain = $null
|
|
1046
|
+
try {
|
|
1047
|
+
$plain = [Security.Cryptography.ProtectedData]::Unprotect($cipher,
|
|
1048
|
+
[Text.Encoding]::UTF8.GetBytes($RetiredIndexContract), [Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
1049
|
+
return ([Text.UTF8Encoding]::new($false, $true).GetString($plain) | ConvertFrom-Json)
|
|
1050
|
+
} catch { Fail 'host_retired_index_decryption_failed' }
|
|
1051
|
+
finally { if ($null -ne $plain) { [Array]::Clear($plain, 0, $plain.Length) } }
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function Read-RetiredNode([string]$Root, $Reference, [string]$Prefix, [hashtable]$Nodes = $null) {
|
|
1055
|
+
Assert-RetiredReference $Reference
|
|
1056
|
+
if ($null -eq $Reference) { return $null }
|
|
1057
|
+
# Only Read-Store supplies this map, scoped to its one mutex-protected read.
|
|
1058
|
+
# Content-addressed nodes are immutable; retain every validation context in
|
|
1059
|
+
# the key so a different root, count or prefix cannot reuse another result.
|
|
1060
|
+
$cacheKey = "$Root`0$($Reference.hash)`0$($Reference.count)`0$Prefix"
|
|
1061
|
+
if ($null -ne $Nodes -and $Nodes.ContainsKey($cacheKey)) { return $Nodes[$cacheKey] }
|
|
1062
|
+
$node = Read-RetiredJson (Join-Path (Get-RetiredDirectory $Root) ($Reference.hash + '.dpapi')) 131072 $Reference.hash
|
|
1063
|
+
if ($node.kind -ceq 'leaf') {
|
|
1064
|
+
Assert-ExactFields $node @('contract', 'prefix', 'kind', 'hashes') 'host_retired_index_invalid'
|
|
1065
|
+
if ($node.hashes -isnot [Array] -or $node.hashes.Count -ne $Reference.count -or $node.hashes.Count -gt 512) {
|
|
1066
|
+
Fail 'host_retired_index_invalid'
|
|
1067
|
+
}
|
|
1068
|
+
$seen = @{}
|
|
1069
|
+
foreach ($hash in $node.hashes) {
|
|
1070
|
+
if ($hash -isnot [string] -or $hash -cnotmatch '^[a-f0-9]{64}$' -or -not $hash.StartsWith($Prefix) -or $seen.ContainsKey($hash)) {
|
|
1071
|
+
Fail 'host_retired_index_invalid'
|
|
1072
|
+
}
|
|
1073
|
+
$seen[$hash] = $true
|
|
1074
|
+
}
|
|
1075
|
+
} elseif ($node.kind -ceq 'branch') {
|
|
1076
|
+
Assert-ExactFields $node @('contract', 'prefix', 'kind', 'children') 'host_retired_index_invalid'
|
|
1077
|
+
$children = @($node.children.PSObject.Properties)
|
|
1078
|
+
if ($Prefix.Length -ge 64 -or $children.Count -lt 1 -or $children.Count -gt 16) { Fail 'host_retired_index_invalid' }
|
|
1079
|
+
[long]$count = 0
|
|
1080
|
+
foreach ($child in $children) {
|
|
1081
|
+
if ($child.Name -cnotmatch '^[a-f0-9]$' -or $null -eq $child.Value) { Fail 'host_retired_index_invalid' }
|
|
1082
|
+
Assert-RetiredReference $child.Value
|
|
1083
|
+
if ($count -gt [long]::MaxValue - $child.Value.count) { Fail 'host_retired_index_invalid' }
|
|
1084
|
+
$count += $child.Value.count
|
|
1085
|
+
}
|
|
1086
|
+
if ($count -ne $Reference.count) { Fail 'host_retired_index_invalid' }
|
|
1087
|
+
} else { Fail 'host_retired_index_invalid' }
|
|
1088
|
+
if ($node.contract -cne $RetiredIndexContract -or $node.prefix -cne $Prefix) { Fail 'host_retired_index_invalid' }
|
|
1089
|
+
if ($null -ne $Nodes) { $Nodes[$cacheKey] = $node }
|
|
1090
|
+
return $node
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function Test-RetiredTransportSession($Store, [string]$Root, [string]$SessionId, [hashtable]$Nodes = $null) {
|
|
1094
|
+
# Preserve the conservative case-insensitive conflict check of Read-Store.
|
|
1095
|
+
if (@($Store.retiredTransportSessionIds | Where-Object { [string]$_ -ieq $SessionId }).Count) { return $true }
|
|
1096
|
+
$hash = Get-RetiredDigest ([Text.Encoding]::UTF8.GetBytes($SessionId.ToLowerInvariant()))
|
|
1097
|
+
return Test-RetiredHash $Store $Root $hash $Nodes
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function Test-RetiredHash($Store, [string]$Root, [string]$Hash, [hashtable]$Nodes = $null) {
|
|
1101
|
+
if ($Hash -cnotmatch '^[a-f0-9]{64}$') { Fail 'host_retired_index_invalid' }
|
|
1102
|
+
$reference = $Store.retiredTransportSessionIndex
|
|
1103
|
+
$prefix = ''
|
|
1104
|
+
while ($null -ne $reference) {
|
|
1105
|
+
$node = Read-RetiredNode $Root $reference $prefix $Nodes
|
|
1106
|
+
if ($node.kind -ceq 'leaf') { return $node.hashes -ccontains $hash }
|
|
1107
|
+
$digit = $hash.Substring($prefix.Length, 1)
|
|
1108
|
+
$reference = $node.children.$digit
|
|
1109
|
+
$prefix += $digit
|
|
1110
|
+
}
|
|
1111
|
+
return $false
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
# The existing retirement tree and transaction protect both session tombstones
|
|
1115
|
+
# and minimal authorization history. NUL-delimited domains cannot overlap a
|
|
1116
|
+
# valid session ID. Each history ciphertext is bound to the authenticated root.
|
|
1117
|
+
function Get-AuthorizationHistoryKey([string]$Kind, [string]$Id) {
|
|
1118
|
+
if ($Kind -cnotin @('capability','credential') -or
|
|
1119
|
+
($Kind -ceq 'capability' -and $Id -notmatch $CapabilityIdPattern) -or
|
|
1120
|
+
($Kind -ceq 'credential' -and $Id -notmatch $IdentifierPattern)) { Fail 'host_authorization_history_invalid' }
|
|
1121
|
+
return Get-RetiredDigest ([Text.Encoding]::UTF8.GetBytes("authorization`0$Kind`0" + $Id.ToLowerInvariant()))
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function Get-AuthorizationHistoryProof([string]$Key, [string]$CipherHash) {
|
|
1125
|
+
return Get-RetiredDigest ([Text.Encoding]::UTF8.GetBytes("authorization-proof`0$Key`0$CipherHash"))
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function New-AuthorizationHistoryRecord([string]$Kind, $Record) {
|
|
1129
|
+
if ($Kind -ceq 'capability') { $null = Assert-Record $Record; $idField = 'capabilityId' }
|
|
1130
|
+
elseif ($Kind -ceq 'credential') { $null = Assert-TransportCredential $Record; $idField = 'credentialId' }
|
|
1131
|
+
else { Fail 'host_authorization_history_invalid' }
|
|
1132
|
+
if ($Record.state -cne 'revoked') { Fail 'host_authorization_history_invalid' }
|
|
1133
|
+
$summary = [ordered]@{ state='revoked'; issuedAt=[long]$Record.issuedAt; revokedAt=[long]$Record.revokedAt }
|
|
1134
|
+
$summary[$idField] = [string]$Record.$idField
|
|
1135
|
+
if ($Kind -ceq 'credential') {
|
|
1136
|
+
$summary.principalId = [string]$Record.principalId
|
|
1137
|
+
$summary.hostInstallationId = [string]$Record.hostInstallationId
|
|
1138
|
+
}
|
|
1139
|
+
return [PSCustomObject]@{ contract=$AuthorizationHistoryContract; kind=$Kind; record=[PSCustomObject]$summary }
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function Read-AuthorizationHistory($Store, [string]$Root, [string]$Kind, [string]$Id) {
|
|
1143
|
+
$key = Get-AuthorizationHistoryKey $Kind $Id
|
|
1144
|
+
if (-not (Test-RetiredHash $Store $Root $key)) { return $null }
|
|
1145
|
+
if ($Store.authorizationHistoryContract -cne $AuthorizationHistoryContract) { Fail 'host_authorization_history_invalid' }
|
|
1146
|
+
$path = Join-Path (Get-RetiredDirectory $Root) ($key + '.dpapi')
|
|
1147
|
+
$null = Assert-AbsoluteNonReparsePath $path $false 8192 'host_authorization_history_invalid' 'host_authorization_history_unavailable'
|
|
1148
|
+
$cipherHash = Get-RetiredDigest ([IO.File]::ReadAllBytes($path))
|
|
1149
|
+
if (-not (Test-RetiredHash $Store $Root (Get-AuthorizationHistoryProof $key $cipherHash))) { Fail 'host_authorization_history_integrity_failed' }
|
|
1150
|
+
$value = Read-RetiredJson $path 8192 $cipherHash
|
|
1151
|
+
Assert-ExactFields $value @('contract','kind','record') 'host_authorization_history_invalid'
|
|
1152
|
+
if ($value.contract -cne $AuthorizationHistoryContract -or $value.kind -cne $Kind) { Fail 'host_authorization_history_invalid' }
|
|
1153
|
+
$record = $value.record
|
|
1154
|
+
$fields = @('issuedAt','revokedAt','state')
|
|
1155
|
+
if ($Kind -ceq 'capability') { $fields += 'capabilityId'; $idField='capabilityId' }
|
|
1156
|
+
else { $fields += @('credentialId','principalId','hostInstallationId'); $idField='credentialId' }
|
|
1157
|
+
Assert-ExactFields $record $fields 'host_authorization_history_invalid'
|
|
1158
|
+
if ($record.$idField -cne $Id -or $record.state -cne 'revoked' -or
|
|
1159
|
+
($record.issuedAt -isnot [long] -and $record.issuedAt -isnot [int]) -or
|
|
1160
|
+
($record.revokedAt -isnot [long] -and $record.revokedAt -isnot [int]) -or
|
|
1161
|
+
$record.issuedAt -lt 0 -or $record.revokedAt -lt $record.issuedAt -or
|
|
1162
|
+
($Kind -ceq 'credential' -and ($record.principalId -notmatch $IdentifierPattern -or $record.hostInstallationId -notmatch $IdentifierPattern))) {
|
|
1163
|
+
Fail 'host_authorization_history_invalid'
|
|
1164
|
+
}
|
|
1165
|
+
return $record
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function Test-AuthorizationIdUsed($Store, [string]$Root, [string]$Kind, [string]$Id) {
|
|
1169
|
+
$key = Get-AuthorizationHistoryKey $Kind $Id
|
|
1170
|
+
$records = if ($Kind -ceq 'capability') { @($Store.capabilities) } else { @($Store.transportCredentials) }
|
|
1171
|
+
$field = if ($Kind -ceq 'capability') { 'capabilityId' } else { 'credentialId' }
|
|
1172
|
+
if (@($records | Where-Object { [string]$_.$field -ieq $Id }).Count) { return $true }
|
|
1173
|
+
return Test-RetiredHash $Store $Root $key
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function New-RetiredNode($Context, [string]$Prefix, [string]$Kind, $Content, [long]$Count) {
|
|
1177
|
+
$node = [ordered]@{ contract = $RetiredIndexContract; prefix = $Prefix; kind = $Kind }
|
|
1178
|
+
if ($Kind -ceq 'leaf') { $node.hashes = @($Content) } else { $node.children = $Content }
|
|
1179
|
+
$bytes = Protect-RetiredJson $node
|
|
1180
|
+
$hash = Get-RetiredDigest $bytes
|
|
1181
|
+
$Context.pages[$hash] = $bytes
|
|
1182
|
+
return [PSCustomObject]@{ hash = $hash; count = $Count }
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function Merge-RetiredNode($Context, $Reference, [string]$Prefix, [string[]]$Hashes) {
|
|
1186
|
+
if (-not $Hashes.Count) { return $Reference }
|
|
1187
|
+
$node = Read-RetiredNode $Context.root $Reference $Prefix
|
|
1188
|
+
if ($null -eq $node -or $node.kind -ceq 'leaf') {
|
|
1189
|
+
$combined = @(@($node.hashes) + $Hashes | Where-Object { $_ } | Sort-Object -Unique)
|
|
1190
|
+
if ($null -ne $Reference -and $combined.Count -eq $Reference.count) { return $Reference }
|
|
1191
|
+
if ($null -ne $Reference) { $Context.obsolete[$Reference.hash] = $true }
|
|
1192
|
+
if ($combined.Count -le 512) { return New-RetiredNode $Context $Prefix 'leaf' $combined $combined.Count }
|
|
1193
|
+
$Hashes = $combined
|
|
1194
|
+
$node = $null
|
|
1195
|
+
}
|
|
1196
|
+
$children = [ordered]@{}
|
|
1197
|
+
[long]$count = 0
|
|
1198
|
+
$changed = $null -eq $node
|
|
1199
|
+
$buckets = @{}
|
|
1200
|
+
foreach ($hash in $Hashes) {
|
|
1201
|
+
$key = $hash.Substring($Prefix.Length, 1)
|
|
1202
|
+
if (-not $buckets.ContainsKey($key)) { $buckets[$key] = [Collections.Generic.List[string]]::new() }
|
|
1203
|
+
$buckets[$key].Add($hash)
|
|
1204
|
+
}
|
|
1205
|
+
foreach ($digit in '0123456789abcdef'.ToCharArray()) {
|
|
1206
|
+
$key = [string]$digit
|
|
1207
|
+
$old = if ($null -ne $node) { $node.children.$key } else { $null }
|
|
1208
|
+
$subset = if ($buckets.ContainsKey($key)) { $buckets[$key].ToArray() } else { @() }
|
|
1209
|
+
$next = Merge-RetiredNode $Context $old ($Prefix + $key) $subset
|
|
1210
|
+
if ($null -ne $next) { $children[$key] = $next; $count += $next.count }
|
|
1211
|
+
if ($next.hash -cne $old.hash) { $changed = $true }
|
|
1212
|
+
}
|
|
1213
|
+
if (-not $changed) { return $Reference }
|
|
1214
|
+
if ($null -ne $node) { $Context.obsolete[$Reference.hash] = $true }
|
|
1215
|
+
return New-RetiredNode $Context $Prefix 'branch' $children $count
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function Write-RetiredBytes([string]$Path, [byte[]]$Bytes, [string]$Sid) {
|
|
1219
|
+
Assert-ProtectedStoreNoReparse $Path
|
|
1220
|
+
$stream = [IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
|
1221
|
+
try { $stream.Write($Bytes, 0, $Bytes.Length); $stream.Flush($true) } finally { $stream.Dispose() }
|
|
1222
|
+
Set-ProtectedStoreAcl $Path $Sid $false
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function Test-RetiredReferenceEqual($Left, $Right) {
|
|
1226
|
+
if ($null -eq $Left -or $null -eq $Right) { return $null -eq $Left -and $null -eq $Right }
|
|
1227
|
+
return $Left.hash -ceq $Right.hash -and $Left.count -eq $Right.count
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function Complete-RetiredTransaction([string]$Root, $Current, $PreparedJournal = $null) {
|
|
1231
|
+
$directory = Get-RetiredDirectory $Root
|
|
1232
|
+
$path = Join-Path $directory 'pending.dpapi'
|
|
1233
|
+
if (-not (Test-Path -LiteralPath $path)) { return $true }
|
|
1234
|
+
$journal = if ($null -ne $PreparedJournal) { $PreparedJournal } else { Read-RetiredJson $path 1048576 }
|
|
1235
|
+
Assert-ExactFields $journal @('contract', 'before', 'after', 'added', 'obsolete') 'host_retired_journal_invalid'
|
|
1236
|
+
if ($journal.contract -cne ($RetiredIndexContract + '-transaction') -or $null -eq $journal.after -or
|
|
1237
|
+
$journal.added -isnot [Array] -or $journal.obsolete -isnot [Array]) { Fail 'host_retired_journal_invalid' }
|
|
1238
|
+
Assert-RetiredReference $journal.before
|
|
1239
|
+
Assert-RetiredReference $journal.after
|
|
1240
|
+
$seen = @{}
|
|
1241
|
+
foreach ($hash in @($journal.added) + @($journal.obsolete)) {
|
|
1242
|
+
if ($hash -isnot [string] -or $hash -cnotmatch '^[a-f0-9]{64}$' -or $seen.ContainsKey($hash)) { Fail 'host_retired_journal_invalid' }
|
|
1243
|
+
$seen[$hash] = $true
|
|
1244
|
+
}
|
|
1245
|
+
if (Test-RetiredReferenceEqual $Current $journal.after) { $remove = @($journal.obsolete) }
|
|
1246
|
+
elseif (Test-RetiredReferenceEqual $Current $journal.before) { $remove = @($journal.added) }
|
|
1247
|
+
else { Fail 'host_retired_journal_conflict' }
|
|
1248
|
+
# Cleanup is retryable after commit. Never turn a committed replacement into
|
|
1249
|
+
# an apparent failed operation, or delete any page from an unknown revision.
|
|
1250
|
+
try {
|
|
1251
|
+
foreach ($hash in $remove) {
|
|
1252
|
+
$target = Join-Path $directory ($hash + '.dpapi')
|
|
1253
|
+
Assert-ProtectedStoreNoReparse $target
|
|
1254
|
+
if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Force }
|
|
1255
|
+
}
|
|
1256
|
+
Remove-Item -LiteralPath $path -Force
|
|
1257
|
+
return $true
|
|
1258
|
+
} catch { return $false }
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function Read-Store([string]$Path) {
|
|
1262
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
1263
|
+
return [PSCustomObject]@{
|
|
1264
|
+
contract = $Contract
|
|
1265
|
+
capabilities = @()
|
|
1266
|
+
transportCredentials = @()
|
|
1267
|
+
transportSessions = @()
|
|
1268
|
+
retiredTransportSessionIds = @()
|
|
1269
|
+
codexNeutralBindings = @()
|
|
1270
|
+
openClawNeutralBindings = @()
|
|
1271
|
+
traeWorkNeutralBindings = @()
|
|
1272
|
+
workBuddyNeutralBindings = @()
|
|
1273
|
+
claudeCodeNeutralBindings = @()
|
|
1274
|
+
cursorNeutralBindings = @()
|
|
1275
|
+
dshNeutralBindings = @()
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
$item = Get-Item -LiteralPath $Path -Force
|
|
1279
|
+
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or $item.Length -gt 2097152) {
|
|
1280
|
+
Fail 'host_protected_store_path_invalid'
|
|
1281
|
+
}
|
|
1282
|
+
$credential = Import-Clixml -LiteralPath $Path
|
|
1283
|
+
if ($credential -is [Management.Automation.PSCredential]) {
|
|
1284
|
+
if ($credential.UserName -cne $CredentialName) { Fail 'host_protected_store_binding_invalid' }
|
|
1285
|
+
$plain = $credential.GetNetworkCredential().Password
|
|
1286
|
+
} elseif ([string]$credential.contract -ceq $EnvelopeContract) {
|
|
1287
|
+
Assert-ExactFields $credential @('contract', 'ciphertext') 'host_protected_store_binding_invalid'
|
|
1288
|
+
$plainBytes = $null
|
|
1289
|
+
try {
|
|
1290
|
+
$ciphertext = [Convert]::FromBase64String([string]$credential.ciphertext)
|
|
1291
|
+
$plainBytes = [Security.Cryptography.ProtectedData]::Unprotect(
|
|
1292
|
+
$ciphertext, [Text.Encoding]::UTF8.GetBytes($EnvelopeContract),
|
|
1293
|
+
[Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
1294
|
+
if ($plainBytes.Length -gt 1048576) { Fail 'host_protected_store_too_large' }
|
|
1295
|
+
$plain = [Text.UTF8Encoding]::new($false, $true).GetString($plainBytes)
|
|
1296
|
+
} catch { Fail 'host_protected_store_decryption_failed' }
|
|
1297
|
+
finally { if ($null -ne $plainBytes) { [Array]::Clear($plainBytes, 0, $plainBytes.Length) } }
|
|
1298
|
+
} else {
|
|
1299
|
+
Fail 'host_protected_store_binding_invalid'
|
|
1300
|
+
}
|
|
1301
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plain) -gt 1048576) { Fail 'host_protected_store_too_large' }
|
|
1302
|
+
try { $store = $plain | ConvertFrom-Json } catch { Fail 'host_protected_store_invalid' }
|
|
1303
|
+
$plain = $null
|
|
1304
|
+
$authorizationHistory = $store.authorizationHistoryContract
|
|
1305
|
+
if ($null -ne $store.PSObject.Properties['authorizationHistoryContract']) {
|
|
1306
|
+
if ($authorizationHistory -cne $AuthorizationHistoryContract) { Fail 'host_authorization_history_invalid' }
|
|
1307
|
+
$store.PSObject.Properties.Remove('authorizationHistoryContract')
|
|
1308
|
+
}
|
|
1309
|
+
$retiredIndex = $store.retiredTransportSessionIndex
|
|
1310
|
+
if ($null -ne $store.PSObject.Properties['retiredTransportSessionIndex']) {
|
|
1311
|
+
if ($null -eq $retiredIndex) { Fail 'host_retired_index_invalid' }
|
|
1312
|
+
Assert-RetiredReference $retiredIndex
|
|
1313
|
+
$store.PSObject.Properties.Remove('retiredTransportSessionIndex')
|
|
1314
|
+
}
|
|
1315
|
+
$fields = @($store.PSObject.Properties.Name | Sort-Object)
|
|
1316
|
+
if ($fields.Count -eq 2) {
|
|
1317
|
+
Assert-ExactFields $store @('capabilities', 'contract') 'host_protected_store_fields_invalid'
|
|
1318
|
+
$store | Add-Member -NotePropertyName transportCredentials -NotePropertyValue @()
|
|
1319
|
+
$store | Add-Member -NotePropertyName transportSessions -NotePropertyValue @()
|
|
1320
|
+
$store | Add-Member -NotePropertyName retiredTransportSessionIds -NotePropertyValue @()
|
|
1321
|
+
$store | Add-Member -NotePropertyName codexNeutralBindings -NotePropertyValue @()
|
|
1322
|
+
$store | Add-Member -NotePropertyName openClawNeutralBindings -NotePropertyValue @()
|
|
1323
|
+
$store | Add-Member -NotePropertyName traeWorkNeutralBindings -NotePropertyValue @()
|
|
1324
|
+
$store | Add-Member -NotePropertyName workBuddyNeutralBindings -NotePropertyValue @()
|
|
1325
|
+
$store | Add-Member -NotePropertyName cursorNeutralBindings -NotePropertyValue @()
|
|
1326
|
+
} elseif ($fields.Count -eq 5) {
|
|
1327
|
+
Assert-ExactFields $store @(
|
|
1328
|
+
'capabilities', 'contract', 'retiredTransportSessionIds',
|
|
1329
|
+
'transportCredentials', 'transportSessions'
|
|
1330
|
+
) 'host_protected_store_fields_invalid'
|
|
1331
|
+
$store | Add-Member -NotePropertyName codexNeutralBindings -NotePropertyValue @()
|
|
1332
|
+
$store | Add-Member -NotePropertyName openClawNeutralBindings -NotePropertyValue @()
|
|
1333
|
+
$store | Add-Member -NotePropertyName traeWorkNeutralBindings -NotePropertyValue @()
|
|
1334
|
+
$store | Add-Member -NotePropertyName workBuddyNeutralBindings -NotePropertyValue @()
|
|
1335
|
+
$store | Add-Member -NotePropertyName cursorNeutralBindings -NotePropertyValue @()
|
|
1336
|
+
} elseif ($fields.Count -eq 6) {
|
|
1337
|
+
Assert-ExactFields $store @(
|
|
1338
|
+
'capabilities', 'codexNeutralBindings', 'contract', 'retiredTransportSessionIds',
|
|
1339
|
+
'transportCredentials', 'transportSessions'
|
|
1340
|
+
) 'host_protected_store_fields_invalid'
|
|
1341
|
+
$store | Add-Member -NotePropertyName openClawNeutralBindings -NotePropertyValue @()
|
|
1342
|
+
$store | Add-Member -NotePropertyName traeWorkNeutralBindings -NotePropertyValue @()
|
|
1343
|
+
$store | Add-Member -NotePropertyName workBuddyNeutralBindings -NotePropertyValue @()
|
|
1344
|
+
$store | Add-Member -NotePropertyName cursorNeutralBindings -NotePropertyValue @()
|
|
1345
|
+
} elseif ($fields.Count -eq 7) {
|
|
1346
|
+
Assert-ExactFields $store @(
|
|
1347
|
+
'capabilities', 'codexNeutralBindings', 'contract', 'openClawNeutralBindings', 'retiredTransportSessionIds',
|
|
1348
|
+
'transportCredentials', 'transportSessions'
|
|
1349
|
+
) 'host_protected_store_fields_invalid'
|
|
1350
|
+
$store | Add-Member -NotePropertyName traeWorkNeutralBindings -NotePropertyValue @()
|
|
1351
|
+
$store | Add-Member -NotePropertyName workBuddyNeutralBindings -NotePropertyValue @()
|
|
1352
|
+
$store | Add-Member -NotePropertyName cursorNeutralBindings -NotePropertyValue @()
|
|
1353
|
+
} elseif ($fields.Count -eq 8) {
|
|
1354
|
+
Assert-ExactFields $store @(
|
|
1355
|
+
'capabilities', 'codexNeutralBindings', 'contract', 'openClawNeutralBindings', 'retiredTransportSessionIds',
|
|
1356
|
+
'traeWorkNeutralBindings', 'transportCredentials', 'transportSessions'
|
|
1357
|
+
) 'host_protected_store_fields_invalid'
|
|
1358
|
+
$store | Add-Member -NotePropertyName workBuddyNeutralBindings -NotePropertyValue @()
|
|
1359
|
+
$store | Add-Member -NotePropertyName cursorNeutralBindings -NotePropertyValue @()
|
|
1360
|
+
} elseif ($fields.Count -eq 9) {
|
|
1361
|
+
Assert-ExactFields $store @(
|
|
1362
|
+
'capabilities', 'codexNeutralBindings', 'contract', 'openClawNeutralBindings', 'retiredTransportSessionIds',
|
|
1363
|
+
'traeWorkNeutralBindings', 'transportCredentials', 'transportSessions', 'workBuddyNeutralBindings'
|
|
1364
|
+
) 'host_protected_store_fields_invalid'
|
|
1365
|
+
$store | Add-Member -NotePropertyName cursorNeutralBindings -NotePropertyValue @()
|
|
1366
|
+
} elseif ($fields.Count -eq 10) {
|
|
1367
|
+
if ($null -ne $store.PSObject.Properties['dshNeutralBindings']) {
|
|
1368
|
+
Assert-ExactFields $store @(
|
|
1369
|
+
'capabilities', 'codexNeutralBindings', 'contract', 'dshNeutralBindings', 'openClawNeutralBindings',
|
|
1370
|
+
'retiredTransportSessionIds', 'traeWorkNeutralBindings', 'transportCredentials', 'transportSessions',
|
|
1371
|
+
'workBuddyNeutralBindings'
|
|
1372
|
+
) 'host_protected_store_fields_invalid'
|
|
1373
|
+
$store | Add-Member -NotePropertyName cursorNeutralBindings -NotePropertyValue @()
|
|
1374
|
+
} else {
|
|
1375
|
+
Assert-ExactFields $store @(
|
|
1376
|
+
'capabilities', 'codexNeutralBindings', 'contract', 'cursorNeutralBindings', 'openClawNeutralBindings',
|
|
1377
|
+
'retiredTransportSessionIds', 'traeWorkNeutralBindings', 'transportCredentials', 'transportSessions',
|
|
1378
|
+
'workBuddyNeutralBindings'
|
|
1379
|
+
) 'host_protected_store_fields_invalid'
|
|
1380
|
+
}
|
|
1381
|
+
} elseif ($fields.Count -eq 11) {
|
|
1382
|
+
Assert-ExactFields $store @(
|
|
1383
|
+
'capabilities', 'codexNeutralBindings', 'contract', 'cursorNeutralBindings', 'dshNeutralBindings',
|
|
1384
|
+
'openClawNeutralBindings', 'retiredTransportSessionIds', 'traeWorkNeutralBindings',
|
|
1385
|
+
'transportCredentials', 'transportSessions', 'workBuddyNeutralBindings'
|
|
1386
|
+
) 'host_protected_store_fields_invalid'
|
|
1387
|
+
} else {
|
|
1388
|
+
Assert-ExactFields $store @(
|
|
1389
|
+
'capabilities', 'claudeCodeNeutralBindings', 'codexNeutralBindings', 'contract', 'cursorNeutralBindings',
|
|
1390
|
+
'dshNeutralBindings', 'openClawNeutralBindings', 'retiredTransportSessionIds',
|
|
1391
|
+
'traeWorkNeutralBindings', 'transportCredentials', 'transportSessions', 'workBuddyNeutralBindings'
|
|
1392
|
+
) 'host_protected_store_fields_invalid'
|
|
1393
|
+
}
|
|
1394
|
+
if ($null -eq $store.PSObject.Properties['dshNeutralBindings']) {
|
|
1395
|
+
$store | Add-Member -NotePropertyName dshNeutralBindings -NotePropertyValue @()
|
|
1396
|
+
}
|
|
1397
|
+
if ($null -eq $store.PSObject.Properties['claudeCodeNeutralBindings']) {
|
|
1398
|
+
$store | Add-Member -NotePropertyName claudeCodeNeutralBindings -NotePropertyValue @()
|
|
1399
|
+
}
|
|
1400
|
+
if (
|
|
1401
|
+
[string]$store.contract -cne $Contract -or
|
|
1402
|
+
$store.capabilities -isnot [Array] -or
|
|
1403
|
+
$store.codexNeutralBindings -isnot [Array] -or $store.codexNeutralBindings.Count -gt 64 -or
|
|
1404
|
+
$store.openClawNeutralBindings -isnot [Array] -or $store.openClawNeutralBindings.Count -gt 64 -or
|
|
1405
|
+
$store.traeWorkNeutralBindings -isnot [Array] -or $store.traeWorkNeutralBindings.Count -gt 64 -or
|
|
1406
|
+
$store.workBuddyNeutralBindings -isnot [Array] -or $store.workBuddyNeutralBindings.Count -gt 64 -or
|
|
1407
|
+
$store.claudeCodeNeutralBindings -isnot [Array] -or $store.claudeCodeNeutralBindings.Count -gt 64 -or
|
|
1408
|
+
$store.cursorNeutralBindings -isnot [Array] -or $store.cursorNeutralBindings.Count -gt 64 -or
|
|
1409
|
+
$store.dshNeutralBindings -isnot [Array] -or $store.dshNeutralBindings.Count -gt 64 -or
|
|
1410
|
+
$store.transportCredentials -isnot [Array] -or
|
|
1411
|
+
$store.transportSessions -isnot [Array] -or $store.transportSessions.Count -gt 1024 -or
|
|
1412
|
+
$store.retiredTransportSessionIds -isnot [Array]
|
|
1413
|
+
) {
|
|
1414
|
+
Fail 'host_protected_store_invalid'
|
|
1415
|
+
}
|
|
1416
|
+
$seen = @{}
|
|
1417
|
+
foreach ($record in $store.capabilities) {
|
|
1418
|
+
$null = Assert-Record $record
|
|
1419
|
+
if ($seen.ContainsKey([string]$record.capabilityId)) { Fail 'host_capability_duplicate' }
|
|
1420
|
+
$seen[[string]$record.capabilityId] = $true
|
|
1421
|
+
}
|
|
1422
|
+
foreach ($binding in $store.codexNeutralBindings) {
|
|
1423
|
+
$null = Assert-CodexNeutralBinding $binding $false
|
|
1424
|
+
}
|
|
1425
|
+
foreach ($binding in $store.openClawNeutralBindings) {
|
|
1426
|
+
$null = Assert-OpenClawNeutralBinding $binding $false
|
|
1427
|
+
}
|
|
1428
|
+
foreach ($binding in $store.traeWorkNeutralBindings) {
|
|
1429
|
+
$null = Assert-TraeWorkNeutralBinding $binding $false
|
|
1430
|
+
}
|
|
1431
|
+
foreach ($binding in $store.workBuddyNeutralBindings) {
|
|
1432
|
+
$null = Assert-WorkBuddyNeutralBinding $binding $false
|
|
1433
|
+
}
|
|
1434
|
+
foreach ($binding in $store.claudeCodeNeutralBindings) {
|
|
1435
|
+
$null = Assert-ClaudeCodeNeutralBinding $binding $false
|
|
1436
|
+
}
|
|
1437
|
+
foreach ($binding in $store.cursorNeutralBindings) {
|
|
1438
|
+
$null = Assert-CursorNeutralBinding $binding $false
|
|
1439
|
+
}
|
|
1440
|
+
foreach ($binding in $store.dshNeutralBindings) {
|
|
1441
|
+
$null = Assert-DshNeutralBinding $binding $false
|
|
1442
|
+
}
|
|
1443
|
+
$credentialSeen = @{}
|
|
1444
|
+
foreach ($record in $store.transportCredentials) {
|
|
1445
|
+
$null = Assert-TransportCredential $record
|
|
1446
|
+
if ($credentialSeen.ContainsKey([string]$record.credentialId)) { Fail 'host_transport_credential_duplicate' }
|
|
1447
|
+
$credentialSeen[[string]$record.credentialId] = $record
|
|
1448
|
+
}
|
|
1449
|
+
$retiredSeen = @{}
|
|
1450
|
+
foreach ($retiredId in $store.retiredTransportSessionIds) {
|
|
1451
|
+
if ([string]$retiredId -notmatch $TransportSessionIdPattern) { Fail 'host_transport_session_tombstone_invalid' }
|
|
1452
|
+
if ($retiredSeen.ContainsKey([string]$retiredId)) { Fail 'host_transport_session_tombstone_duplicate' }
|
|
1453
|
+
$retiredSeen[[string]$retiredId] = $true
|
|
1454
|
+
}
|
|
1455
|
+
$retiredNodes = @{}
|
|
1456
|
+
if ($null -ne $retiredIndex) {
|
|
1457
|
+
$store | Add-Member -NotePropertyName retiredTransportSessionIndex -NotePropertyValue $retiredIndex
|
|
1458
|
+
$null = Read-RetiredNode ([IO.Path]::GetDirectoryName($Path)) $retiredIndex '' $retiredNodes
|
|
1459
|
+
}
|
|
1460
|
+
if ($null -ne $authorizationHistory) {
|
|
1461
|
+
if ($null -eq $retiredIndex) { Fail 'host_authorization_history_invalid' }
|
|
1462
|
+
$store | Add-Member -NotePropertyName authorizationHistoryContract -NotePropertyValue $authorizationHistory
|
|
1463
|
+
foreach ($pair in @(@('capability','capabilities','capabilityId'),@('credential','transportCredentials','credentialId'))) {
|
|
1464
|
+
foreach ($record in $store.($pair[1])) {
|
|
1465
|
+
if ($record.state -ceq 'active' -and (Test-RetiredHash $store ([IO.Path]::GetDirectoryName($Path)) (Get-AuthorizationHistoryKey $pair[0] $record.($pair[2])) $retiredNodes)) {
|
|
1466
|
+
Fail 'host_authorization_history_active_conflict'
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
$sessionSeen = @{}
|
|
1472
|
+
foreach ($record in $store.transportSessions) {
|
|
1473
|
+
$null = Assert-TransportSession $record
|
|
1474
|
+
if ($sessionSeen.ContainsKey([string]$record.sessionId)) { Fail 'host_transport_session_duplicate' }
|
|
1475
|
+
if (Test-RetiredTransportSession $store ([IO.Path]::GetDirectoryName($Path)) ([string]$record.sessionId) $retiredNodes) { Fail 'host_transport_session_retired_conflict' }
|
|
1476
|
+
$sessionSeen[[string]$record.sessionId] = $true
|
|
1477
|
+
$credential = $credentialSeen[[string]$record.credentialId]
|
|
1478
|
+
if (
|
|
1479
|
+
$null -eq $credential -or [string]$credential.state -cne 'active' -or
|
|
1480
|
+
[string]$credential.principalId -cne [string]$record.principalId -or
|
|
1481
|
+
[string]$credential.hostInstallationId -cne [string]$record.hostInstallationId -or
|
|
1482
|
+
[int64]$record.expiresAt -gt [int64]$credential.expiresAt
|
|
1483
|
+
) { Fail 'host_transport_session_credential_mismatch' }
|
|
1484
|
+
}
|
|
1485
|
+
return $store
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function Write-Store([string]$Path, $Store, [string]$Root, [string]$Sid) {
|
|
1489
|
+
$retired = $null
|
|
1490
|
+
$preparedJournal = $null
|
|
1491
|
+
$nextIndex = $Store.retiredTransportSessionIndex
|
|
1492
|
+
$hasAuthorizationHistory = $Store.authorizationHistoryContract -ceq $AuthorizationHistoryContract
|
|
1493
|
+
if ([string]$Store.contract -ceq $Contract) {
|
|
1494
|
+
if (-not (Complete-RetiredTransaction $Root $nextIndex)) { Fail 'host_retired_cleanup_pending' }
|
|
1495
|
+
if ([Text.Encoding]::UTF8.GetByteCount((ConvertTo-Json -InputObject @($Store.retiredTransportSessionIds) -Compress)) -gt 1048576) {
|
|
1496
|
+
Fail 'host_protected_store_too_large'
|
|
1497
|
+
}
|
|
1498
|
+
$hashes = @($Store.retiredTransportSessionIds | ForEach-Object {
|
|
1499
|
+
if ($_ -isnot [string] -or $_ -notmatch $TransportSessionIdPattern) { Fail 'host_transport_session_tombstone_invalid' }
|
|
1500
|
+
Get-RetiredDigest ([Text.Encoding]::UTF8.GetBytes($_.ToLowerInvariant()))
|
|
1501
|
+
} | Sort-Object -Unique)
|
|
1502
|
+
$retired = @{ root = $Root; pages = @{}; obsolete = @{} }
|
|
1503
|
+
foreach ($pair in @(@('capability','capabilities','capabilityId'),@('credential','transportCredentials','credentialId'))) {
|
|
1504
|
+
foreach ($record in @($Store.($pair[1]) | Where-Object state -CEQ 'revoked')) {
|
|
1505
|
+
$history = New-AuthorizationHistoryRecord $pair[0] $record
|
|
1506
|
+
$key = Get-AuthorizationHistoryKey $pair[0] $record.($pair[2])
|
|
1507
|
+
if (Test-RetiredHash $Store $Root $key) {
|
|
1508
|
+
$saved = Read-AuthorizationHistory $Store $Root $pair[0] $record.($pair[2])
|
|
1509
|
+
if (($saved | ConvertTo-Json -Compress) -cne ($history.record | ConvertTo-Json -Compress)) { Fail 'host_authorization_history_conflict' }
|
|
1510
|
+
} else {
|
|
1511
|
+
if ($retired.pages.ContainsKey($key) -or (Test-Path -LiteralPath (Join-Path (Get-RetiredDirectory $Root) ($key + '.dpapi')))) { Fail 'host_authorization_history_conflict' }
|
|
1512
|
+
$bytes = Protect-RetiredJson $history
|
|
1513
|
+
$retired.pages[$key] = $bytes
|
|
1514
|
+
$hashes += @($key, (Get-AuthorizationHistoryProof $key (Get-RetiredDigest $bytes)))
|
|
1515
|
+
}
|
|
1516
|
+
$hasAuthorizationHistory = $true
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
$nextIndex = Merge-RetiredNode $retired $nextIndex '' $hashes
|
|
1520
|
+
}
|
|
1521
|
+
# All six adapters share this store. A write for another client must retain
|
|
1522
|
+
# Claude's binding, just as it retains the other adapters' bindings.
|
|
1523
|
+
$serializedStore = $Store | Select-Object -Property *
|
|
1524
|
+
if ($null -ne $nextIndex) {
|
|
1525
|
+
$serializedStore.retiredTransportSessionIds = @()
|
|
1526
|
+
$serializedStore | Add-Member -NotePropertyName retiredTransportSessionIndex -NotePropertyValue $nextIndex -Force
|
|
1527
|
+
}
|
|
1528
|
+
if ($hasAuthorizationHistory) {
|
|
1529
|
+
$serializedStore.capabilities = @($Store.capabilities | Where-Object state -CNE 'revoked')
|
|
1530
|
+
$serializedStore.transportCredentials = @($Store.transportCredentials | Where-Object state -CNE 'revoked')
|
|
1531
|
+
# Earlier development writers must reject this state, not reuse a revoked
|
|
1532
|
+
# capability ID after silently ignoring its indexed history.
|
|
1533
|
+
$serializedStore | Add-Member -NotePropertyName authorizationHistoryContract -NotePropertyValue $AuthorizationHistoryContract -Force
|
|
1534
|
+
}
|
|
1535
|
+
$plain = $serializedStore | ConvertTo-Json -Depth 8 -Compress
|
|
1536
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plain) -gt 1048576) { Fail 'host_protected_store_too_large' }
|
|
1537
|
+
if ([string]$Store.contract -ceq $Contract) {
|
|
1538
|
+
# Protect bytes directly: SecureString's 65536-character ceiling must not
|
|
1539
|
+
# turn accumulated anti-replay evidence into a connection failure. Keep
|
|
1540
|
+
# every record. This envelope deliberately fails closed in old writers.
|
|
1541
|
+
$plainBytes = [Text.Encoding]::UTF8.GetBytes($plain)
|
|
1542
|
+
try {
|
|
1543
|
+
$ciphertext = [Security.Cryptography.ProtectedData]::Protect(
|
|
1544
|
+
$plainBytes, [Text.Encoding]::UTF8.GetBytes($EnvelopeContract),
|
|
1545
|
+
[Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
1546
|
+
$credential = [PSCustomObject]@{
|
|
1547
|
+
contract = $EnvelopeContract
|
|
1548
|
+
ciphertext = [Convert]::ToBase64String($ciphertext)
|
|
1549
|
+
}
|
|
1550
|
+
} finally { [Array]::Clear($plainBytes, 0, $plainBytes.Length) }
|
|
1551
|
+
} else {
|
|
1552
|
+
# Device enrollment uses its separate, small PSCredential contract.
|
|
1553
|
+
$secure = ConvertTo-SecureString -String $plain -AsPlainText -Force
|
|
1554
|
+
$credential = [Management.Automation.PSCredential]::new($CredentialName, $secure)
|
|
1555
|
+
}
|
|
1556
|
+
$temp = Join-Path $Root ('.capabilities.{0}.tmp' -f [Guid]::NewGuid().ToString('N'))
|
|
1557
|
+
try {
|
|
1558
|
+
$credential | Export-Clixml -LiteralPath $temp -Force
|
|
1559
|
+
Set-ProtectedStoreAcl $temp $Sid $false
|
|
1560
|
+
$durable = [IO.File]::Open($temp, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::Read)
|
|
1561
|
+
try { $durable.Flush($true) } finally { $durable.Dispose() }
|
|
1562
|
+
if ($null -ne $retired -and $retired.pages.Count) {
|
|
1563
|
+
$directory = Get-RetiredDirectory $Root
|
|
1564
|
+
if (-not (Test-Path -LiteralPath $directory)) { $null = New-Item -ItemType Directory -Path $directory }
|
|
1565
|
+
Set-ProtectedStoreAcl $directory $Sid $true
|
|
1566
|
+
$preparedJournal = [PSCustomObject]@{ contract = $RetiredIndexContract + '-transaction'; before = $Store.retiredTransportSessionIndex;
|
|
1567
|
+
after = $nextIndex; added = @($retired.pages.Keys); obsolete = @($retired.obsolete.Keys) }
|
|
1568
|
+
# Publish the complete write set before creating pages. A terminated write
|
|
1569
|
+
# can then clean either the unpublished pages or the superseded pages.
|
|
1570
|
+
$journalTemp = Join-Path $directory 'pending.tmp'
|
|
1571
|
+
if (Test-Path -LiteralPath $journalTemp) { Remove-Item -LiteralPath $journalTemp -Force }
|
|
1572
|
+
Write-RetiredBytes $journalTemp (Protect-RetiredJson $preparedJournal) $Sid
|
|
1573
|
+
Move-Item -LiteralPath $journalTemp -Destination (Join-Path $directory 'pending.dpapi')
|
|
1574
|
+
foreach ($hash in $retired.pages.Keys) { Write-RetiredBytes (Join-Path $directory ($hash + '.dpapi')) $retired.pages[$hash] $Sid }
|
|
1575
|
+
}
|
|
1576
|
+
for ($attempt = 0; ; $attempt += 1) {
|
|
1577
|
+
try {
|
|
1578
|
+
Move-Item -LiteralPath $temp -Destination $Path -Force
|
|
1579
|
+
break
|
|
1580
|
+
} catch [System.IO.IOException] {
|
|
1581
|
+
if ($attempt -ge 20) { throw }
|
|
1582
|
+
Start-Sleep -Milliseconds (5 * ($attempt + 1))
|
|
1583
|
+
} catch [System.UnauthorizedAccessException] {
|
|
1584
|
+
if ($attempt -ge 20) { throw }
|
|
1585
|
+
Start-Sleep -Milliseconds (5 * ($attempt + 1))
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if ($null -ne $nextIndex) {
|
|
1589
|
+
$Store.retiredTransportSessionIds = @()
|
|
1590
|
+
$Store | Add-Member -NotePropertyName retiredTransportSessionIndex -NotePropertyValue $nextIndex -Force
|
|
1591
|
+
if ($hasAuthorizationHistory) {
|
|
1592
|
+
$Store.capabilities = $serializedStore.capabilities
|
|
1593
|
+
$Store.transportCredentials = $serializedStore.transportCredentials
|
|
1594
|
+
$Store | Add-Member -NotePropertyName authorizationHistoryContract -NotePropertyValue $AuthorizationHistoryContract -Force
|
|
1595
|
+
}
|
|
1596
|
+
# The just-published journal is already in memory. A transient read or
|
|
1597
|
+
# cleanup I/O failure must not misreport the committed state as failed.
|
|
1598
|
+
try { $null = Complete-RetiredTransaction $Root $nextIndex $preparedJournal } catch { }
|
|
1599
|
+
}
|
|
1600
|
+
} finally {
|
|
1601
|
+
if (Test-Path -LiteralPath $temp) { Remove-Item -LiteralPath $temp -Force }
|
|
1602
|
+
$plain = $null
|
|
1603
|
+
$secure = $null
|
|
1604
|
+
$credential = $null
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function Write-Result($Value) {
|
|
1609
|
+
if (-not $ResultPipeName -or $ResultPipeName -notmatch '^HelloLynkHostProtected-[A-Za-z0-9_-]{16,96}$') {
|
|
1610
|
+
Fail 'host_protected_result_pipe_invalid'
|
|
1611
|
+
}
|
|
1612
|
+
$json = $Value | ConvertTo-Json -Depth 8 -Compress
|
|
1613
|
+
$pipeOptions = [IO.Pipes.PipeOptions]::Asynchronous -bor [IO.Pipes.PipeOptions]::CurrentUserOnly
|
|
1614
|
+
$pipe = [IO.Pipes.NamedPipeServerStream]::new(
|
|
1615
|
+
$ResultPipeName,
|
|
1616
|
+
[IO.Pipes.PipeDirection]::Out,
|
|
1617
|
+
1,
|
|
1618
|
+
[IO.Pipes.PipeTransmissionMode]::Byte,
|
|
1619
|
+
$pipeOptions
|
|
1620
|
+
)
|
|
1621
|
+
try {
|
|
1622
|
+
$pipe.WaitForConnection()
|
|
1623
|
+
$writer = [IO.StreamWriter]::new($pipe, [Text.UTF8Encoding]::new($false), 1024, $true)
|
|
1624
|
+
try { $writer.Write($json); $writer.Flush() } finally { $writer.Dispose() }
|
|
1625
|
+
} finally {
|
|
1626
|
+
$pipe.Dispose()
|
|
1627
|
+
$json = $null
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
try {
|
|
1632
|
+
if (-not $StoreRoot) { Fail 'host_protected_store_root_unavailable' }
|
|
1633
|
+
$verificationCommands = @(
|
|
1634
|
+
'LoadDeviceEnrollment',
|
|
1635
|
+
'Status',
|
|
1636
|
+
'VerifyCodexNeutralBinding', 'VerifyOpenClawNeutralBinding',
|
|
1637
|
+
'VerifyTraeWorkNeutralBinding', 'VerifyWorkBuddyNeutralBinding', 'VerifyClaudeCodeNeutralBinding',
|
|
1638
|
+
'VerifyCursorNeutralBinding', 'VerifyDshNeutralBinding',
|
|
1639
|
+
'VerifyTransport', 'VerifyPath', 'VerifyTransportCredentialLifetimeMigration',
|
|
1640
|
+
'ResolveCodexNeutralBinding', 'ResolveOpenClawNeutralBinding', 'ResolveTraeWorkNeutralBinding',
|
|
1641
|
+
'ResolveWorkBuddyNeutralBinding', 'ResolveClaudeCodeNeutralBinding', 'ResolveCursorNeutralBinding', 'ResolveDshNeutralBinding'
|
|
1642
|
+
)
|
|
1643
|
+
$readOnlyBindingResolution = $VerifyOnly -and [string]$Command -match '^Resolve(?:Codex|OpenClaw|TraeWork|WorkBuddy|ClaudeCode|Cursor|Dsh)NeutralBinding$'
|
|
1644
|
+
$verificationMode = [string]$Command -match '^Verify' -or
|
|
1645
|
+
($VerifyOnly -and [string]$Command -in @('Status', 'LoadDeviceEnrollment')) -or $readOnlyBindingResolution
|
|
1646
|
+
if ($VerifyOnly -and -not ($verificationCommands -contains [string]$Command)) {
|
|
1647
|
+
Fail 'host_protected_store_verify_only_command_invalid'
|
|
1648
|
+
}
|
|
1649
|
+
if (
|
|
1650
|
+
$StoreRoot -notmatch '^[A-Za-z]:[\\/]' -or
|
|
1651
|
+
$StoreRoot.StartsWith('\\') -or
|
|
1652
|
+
$StoreRoot.Contains([char]0)
|
|
1653
|
+
) { Fail 'host_protected_store_root_invalid' }
|
|
1654
|
+
$root = [IO.Path]::GetFullPath($StoreRoot)
|
|
1655
|
+
if ([string]$Command -match 'ClaudeCodeNeutralBinding$' -and -not (Test-ClaudeCodeStoreRoot $root)) {
|
|
1656
|
+
Fail 'neutral_claude_code_store_root_invalid'
|
|
1657
|
+
}
|
|
1658
|
+
if ($root -notmatch '^[A-Za-z]:\\' -or $root.Length -lt 4) {
|
|
1659
|
+
Fail 'host_protected_store_root_invalid'
|
|
1660
|
+
}
|
|
1661
|
+
try { $drive = [IO.DriveInfo]::new([IO.Path]::GetPathRoot($root)) } catch {
|
|
1662
|
+
Fail 'host_protected_store_root_invalid'
|
|
1663
|
+
}
|
|
1664
|
+
if ($drive.DriveType -ne [IO.DriveType]::Fixed) { Fail 'host_protected_store_root_invalid' }
|
|
1665
|
+
Assert-ProtectedStoreNoReparse $root
|
|
1666
|
+
if ($verificationMode) {
|
|
1667
|
+
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
|
1668
|
+
Fail 'host_protected_store_root_invalid'
|
|
1669
|
+
}
|
|
1670
|
+
} else {
|
|
1671
|
+
$null = New-Item -ItemType Directory -Path $root -Force
|
|
1672
|
+
}
|
|
1673
|
+
Assert-ProtectedStoreNoReparse $root
|
|
1674
|
+
$root = Get-ProtectedStoreCanonicalLocalRoot $root
|
|
1675
|
+
Assert-ProtectedStoreNoReparse $root
|
|
1676
|
+
$rootItem = Get-Item -LiteralPath $root -Force
|
|
1677
|
+
if (-not $rootItem.PSIsContainer -or ($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
1678
|
+
Fail 'host_protected_store_root_invalid'
|
|
1679
|
+
}
|
|
1680
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
1681
|
+
$path = Join-Path $root 'capabilities-v1.dpapi.clixml'
|
|
1682
|
+
if ($Command -in @('LoadDeviceEnrollment', 'SaveDeviceEnrollment')) {
|
|
1683
|
+
# Separate from the capability format so existing installed readers can
|
|
1684
|
+
# continue opening their store during the explicit enrollment transition.
|
|
1685
|
+
$path = Join-Path $root 'device-enrollment-v1.dpapi.clixml'
|
|
1686
|
+
}
|
|
1687
|
+
$rootPrefix = $root.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
|
|
1688
|
+
if (-not [IO.Path]::GetFullPath($path).StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
1689
|
+
Fail 'host_protected_store_path_invalid'
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
# The directory is not atomically replaced, so its idempotent ACL hardening
|
|
1693
|
+
# can happen before the per-root data mutex. The main file is hardened again
|
|
1694
|
+
# under the mutex immediately before it is read.
|
|
1695
|
+
if ($verificationMode) {
|
|
1696
|
+
Assert-ProtectedStoreAcl $root $sid $true
|
|
1697
|
+
} else {
|
|
1698
|
+
Repair-ProtectedStoreAcl $root $sid $true
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
if ([string]$Command -eq 'VerifyPath') {
|
|
1702
|
+
if (-not $PathToVerify -or -not [IO.Path]::IsPathFullyQualified($PathToVerify)) {
|
|
1703
|
+
Fail 'host_protected_store_path_invalid'
|
|
1704
|
+
}
|
|
1705
|
+
$verifiedPath = [IO.Path]::GetFullPath($PathToVerify)
|
|
1706
|
+
if (-not [string]::Equals($verifiedPath, $root, [StringComparison]::OrdinalIgnoreCase) -and
|
|
1707
|
+
-not $verifiedPath.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
1708
|
+
Fail 'host_protected_store_path_invalid'
|
|
1709
|
+
}
|
|
1710
|
+
Assert-ProtectedStoreNoReparse $verifiedPath
|
|
1711
|
+
if ($VerifyDirectory) {
|
|
1712
|
+
if (-not (Test-Path -LiteralPath $verifiedPath -PathType Container)) {
|
|
1713
|
+
Fail 'host_protected_store_path_invalid'
|
|
1714
|
+
}
|
|
1715
|
+
} elseif (-not (Test-Path -LiteralPath $verifiedPath -PathType Leaf)) {
|
|
1716
|
+
Fail 'host_protected_store_path_invalid'
|
|
1717
|
+
}
|
|
1718
|
+
$verifiedItem = Get-Item -LiteralPath $verifiedPath -Force
|
|
1719
|
+
if (($verifiedItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
1720
|
+
Fail 'host_protected_store_path_invalid'
|
|
1721
|
+
}
|
|
1722
|
+
Assert-ProtectedStoreAcl $verifiedPath $sid ([bool]$VerifyDirectory)
|
|
1723
|
+
Write-Result ([PSCustomObject]@{ verified = $true })
|
|
1724
|
+
exit 0
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
$mutexScope = ('{0}`0{1}' -f $sid, $root.ToUpperInvariant())
|
|
1728
|
+
$scopeHash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData(
|
|
1729
|
+
[Text.Encoding]::UTF8.GetBytes($mutexScope)
|
|
1730
|
+
)).Substring(0, 32)
|
|
1731
|
+
$mutex = [Threading.Mutex]::new($false, "Local\HelloLynkNeutralHostProtected-$scopeHash")
|
|
1732
|
+
$acquired = $false
|
|
1733
|
+
try {
|
|
1734
|
+
try { $acquired = $mutex.WaitOne(10000) } catch [Threading.AbandonedMutexException] { $acquired = $true }
|
|
1735
|
+
if (-not $acquired) { Fail 'host_protected_store_lock_timeout' }
|
|
1736
|
+
if (Test-Path -LiteralPath $path -PathType Leaf) {
|
|
1737
|
+
$existingItem = Get-Item -LiteralPath $path -Force
|
|
1738
|
+
if (($existingItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
1739
|
+
Fail 'host_protected_store_path_invalid'
|
|
1740
|
+
}
|
|
1741
|
+
if ($verificationMode) { Assert-ProtectedStoreAcl $path $sid $false }
|
|
1742
|
+
else { Repair-ProtectedStoreAcl $path $sid $false }
|
|
1743
|
+
} elseif ($verificationMode) {
|
|
1744
|
+
Fail 'host_protected_store_path_invalid'
|
|
1745
|
+
}
|
|
1746
|
+
if ([string]$Command -eq 'Status') {
|
|
1747
|
+
Write-Result ([PSCustomObject]@{ contract = $Contract; protected = $true })
|
|
1748
|
+
exit 0
|
|
1749
|
+
}
|
|
1750
|
+
if ($Command -in @('LoadDeviceEnrollment', 'SaveDeviceEnrollment')) {
|
|
1751
|
+
$enrollment = Read-DeviceEnrollment $path $sid
|
|
1752
|
+
if ($Command -eq 'SaveDeviceEnrollment') {
|
|
1753
|
+
$plainInput = [Console]::In.ReadToEnd()
|
|
1754
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plainInput) -gt 65536) { Fail 'device_enrollment_store_invalid' }
|
|
1755
|
+
try { $inputRecord = $plainInput | ConvertFrom-Json } catch { Fail 'device_enrollment_store_invalid' }
|
|
1756
|
+
$plainInput = $null
|
|
1757
|
+
Assert-ExactFields $inputRecord @('expectedRevision', 'value') 'device_enrollment_store_invalid'
|
|
1758
|
+
if ($inputRecord.expectedRevision -isnot [long] -and $inputRecord.expectedRevision -isnot [int]) {
|
|
1759
|
+
Fail 'device_enrollment_store_invalid'
|
|
1760
|
+
}
|
|
1761
|
+
if ($inputRecord.expectedRevision -cne $enrollment.revision) { Fail 'device_enrollment_revision_conflict' }
|
|
1762
|
+
if ($null -eq $inputRecord.value -or $inputRecord.value -isnot [PSCustomObject]) { Fail 'device_enrollment_store_invalid' }
|
|
1763
|
+
$enrollment.revision = [long]$enrollment.revision + 1
|
|
1764
|
+
$enrollment.value = $inputRecord.value
|
|
1765
|
+
Write-Store $path $enrollment $root $sid
|
|
1766
|
+
}
|
|
1767
|
+
Write-Result $enrollment
|
|
1768
|
+
exit 0
|
|
1769
|
+
}
|
|
1770
|
+
$store = Read-Store $path
|
|
1771
|
+
$inputRecord = $null
|
|
1772
|
+
if (@(
|
|
1773
|
+
'DisconnectLocalClient', 'Issue', 'Rotate', 'SaveCodexNeutralBinding', 'RemoveCodexNeutralBinding', 'ReplaceCodexNeutralBinding', 'MigrateCodexNeutralBinding', 'SaveTransportCredential',
|
|
1774
|
+
'SaveOpenClawNeutralBinding', 'RemoveOpenClawNeutralBinding', 'MigrateOpenClawNeutralBinding',
|
|
1775
|
+
'SaveTraeWorkNeutralBinding', 'RemoveTraeWorkNeutralBinding', 'MigrateTraeWorkNeutralBinding',
|
|
1776
|
+
'SaveWorkBuddyNeutralBinding', 'RemoveWorkBuddyNeutralBinding', 'MigrateWorkBuddyNeutralBinding',
|
|
1777
|
+
'SaveClaudeCodeNeutralBinding', 'RemoveClaudeCodeNeutralBinding', 'MigrateClaudeCodeNeutralBinding',
|
|
1778
|
+
'SaveCursorNeutralBinding', 'RemoveCursorNeutralBinding', 'MigrateCursorNeutralBinding',
|
|
1779
|
+
'SaveDshNeutralBinding', 'RemoveDshNeutralBinding', 'MigrateDshNeutralBinding', 'RestoreDshNeutralBinding',
|
|
1780
|
+
'SaveTransportSession', 'ReplaceTransportSession', 'SaveEnrollmentTransportCredential', 'MigrateTransportCredentialLifetime'
|
|
1781
|
+
) -contains $Command) {
|
|
1782
|
+
$plainInput = [Console]::In.ReadToEnd()
|
|
1783
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plainInput) -gt 65536) { Fail 'host_capability_input_too_large' }
|
|
1784
|
+
try { $inputRecord = $plainInput | ConvertFrom-Json } catch { Fail 'host_capability_input_invalid' }
|
|
1785
|
+
$plainInput = $null
|
|
1786
|
+
if ($Command -eq 'DisconnectLocalClient') {
|
|
1787
|
+
Assert-ExactFields $inputRecord @('client') 'workbench_disconnect_input_invalid'
|
|
1788
|
+
} elseif (@('Issue', 'Rotate') -contains $Command) {
|
|
1789
|
+
$null = Assert-Record $inputRecord
|
|
1790
|
+
if ([string]$inputRecord.state -ne 'active') { Fail 'host_capability_input_invalid' }
|
|
1791
|
+
} elseif ([string]$Command -eq 'SaveCodexNeutralBinding') {
|
|
1792
|
+
$null = Assert-CodexNeutralBinding $inputRecord $true
|
|
1793
|
+
} elseif ([string]$Command -eq 'RemoveCodexNeutralBinding') {
|
|
1794
|
+
$null = Assert-CodexNeutralBinding $inputRecord $true
|
|
1795
|
+
} elseif ([string]$Command -eq 'ReplaceCodexNeutralBinding') {
|
|
1796
|
+
Assert-ExactFields $inputRecord @('previousBinding', 'replacementBinding') 'neutral_codex_binding_invalid'
|
|
1797
|
+
$null = Assert-CodexNeutralBinding $inputRecord.previousBinding $true
|
|
1798
|
+
$null = Assert-CodexNeutralBinding $inputRecord.replacementBinding $true
|
|
1799
|
+
} elseif ([string]$Command -eq 'MigrateCodexNeutralBinding') {
|
|
1800
|
+
Assert-ExactFields $inputRecord @(
|
|
1801
|
+
'previousBinding', 'replacementBinding', 'replacementRecord'
|
|
1802
|
+
) 'neutral_codex_binding_invalid'
|
|
1803
|
+
$null = Assert-CodexNeutralBinding $inputRecord.previousBinding $true
|
|
1804
|
+
$null = Assert-CodexNeutralBinding $inputRecord.replacementBinding $true
|
|
1805
|
+
$null = Assert-Record $inputRecord.replacementRecord
|
|
1806
|
+
if ([string]$inputRecord.replacementRecord.state -ne 'active') { Fail 'host_capability_input_invalid' }
|
|
1807
|
+
} elseif ([string]$Command -eq 'SaveOpenClawNeutralBinding') {
|
|
1808
|
+
$null = Assert-OpenClawNeutralBinding $inputRecord $true $true
|
|
1809
|
+
} elseif ([string]$Command -eq 'RemoveOpenClawNeutralBinding') {
|
|
1810
|
+
$null = Assert-OpenClawNeutralBinding $inputRecord $true
|
|
1811
|
+
} elseif ([string]$Command -eq 'MigrateOpenClawNeutralBinding') {
|
|
1812
|
+
Assert-ExactFields $inputRecord @(
|
|
1813
|
+
'previousBinding', 'replacementBinding', 'replacementRecord'
|
|
1814
|
+
) 'neutral_openclaw_binding_invalid'
|
|
1815
|
+
$null = Assert-OpenClawNeutralBinding $inputRecord.previousBinding $true $false
|
|
1816
|
+
$null = Assert-OpenClawNeutralBinding $inputRecord.replacementBinding $true $true
|
|
1817
|
+
$null = Assert-Record $inputRecord.replacementRecord
|
|
1818
|
+
if ([string]$inputRecord.replacementRecord.state -ne 'active') { Fail 'host_capability_input_invalid' }
|
|
1819
|
+
} elseif ([string]$Command -eq 'SaveTraeWorkNeutralBinding') {
|
|
1820
|
+
$null = Assert-TraeWorkNeutralBinding $inputRecord $true
|
|
1821
|
+
} elseif ([string]$Command -eq 'RemoveTraeWorkNeutralBinding') {
|
|
1822
|
+
$null = Assert-TraeWorkNeutralBinding $inputRecord $true
|
|
1823
|
+
} elseif ([string]$Command -eq 'MigrateTraeWorkNeutralBinding') {
|
|
1824
|
+
Assert-ExactFields $inputRecord @(
|
|
1825
|
+
'previousBinding', 'replacementBinding', 'replacementRecord'
|
|
1826
|
+
) 'neutral_trae_work_binding_invalid'
|
|
1827
|
+
$null = Assert-TraeWorkNeutralBinding $inputRecord.previousBinding $true
|
|
1828
|
+
$null = Assert-TraeWorkNeutralBinding $inputRecord.replacementBinding $true
|
|
1829
|
+
$null = Assert-Record $inputRecord.replacementRecord
|
|
1830
|
+
if ([string]$inputRecord.replacementRecord.state -ne 'active') { Fail 'host_capability_input_invalid' }
|
|
1831
|
+
} elseif ([string]$Command -eq 'SaveWorkBuddyNeutralBinding') {
|
|
1832
|
+
$null = Assert-WorkBuddyNeutralBinding $inputRecord $true
|
|
1833
|
+
} elseif ([string]$Command -eq 'RemoveWorkBuddyNeutralBinding') {
|
|
1834
|
+
$null = Assert-WorkBuddyNeutralBinding $inputRecord $true
|
|
1835
|
+
} elseif ([string]$Command -eq 'MigrateWorkBuddyNeutralBinding') {
|
|
1836
|
+
Assert-ExactFields $inputRecord @(
|
|
1837
|
+
'previousBinding', 'replacementBinding', 'replacementRecord'
|
|
1838
|
+
) 'neutral_workbuddy_binding_invalid'
|
|
1839
|
+
$null = Assert-WorkBuddyNeutralBinding $inputRecord.previousBinding $true
|
|
1840
|
+
$null = Assert-WorkBuddyNeutralBinding $inputRecord.replacementBinding $true
|
|
1841
|
+
$null = Assert-Record $inputRecord.replacementRecord
|
|
1842
|
+
if ([string]$inputRecord.replacementRecord.state -ne 'active') { Fail 'host_capability_input_invalid' }
|
|
1843
|
+
} elseif ([string]$Command -eq 'SaveClaudeCodeNeutralBinding') {
|
|
1844
|
+
$null = Assert-ClaudeCodeNeutralBinding $inputRecord $true
|
|
1845
|
+
} elseif ([string]$Command -eq 'RemoveClaudeCodeNeutralBinding') {
|
|
1846
|
+
$null = Assert-ClaudeCodeNeutralBinding $inputRecord $true
|
|
1847
|
+
} elseif ([string]$Command -eq 'MigrateClaudeCodeNeutralBinding') {
|
|
1848
|
+
Assert-ExactFields $inputRecord @(
|
|
1849
|
+
'previousBinding', 'replacementBinding', 'replacementRecord'
|
|
1850
|
+
) 'neutral_claude_code_binding_invalid'
|
|
1851
|
+
$null = Assert-ClaudeCodeNeutralBinding $inputRecord.previousBinding $true
|
|
1852
|
+
$null = Assert-ClaudeCodeNeutralBinding $inputRecord.replacementBinding $true
|
|
1853
|
+
$null = Assert-Record $inputRecord.replacementRecord
|
|
1854
|
+
if ([string]$inputRecord.replacementRecord.state -ne 'active') { Fail 'host_capability_input_invalid' }
|
|
1855
|
+
} elseif ([string]$Command -eq 'SaveCursorNeutralBinding') {
|
|
1856
|
+
$null = Assert-CursorNeutralBinding $inputRecord $true $true
|
|
1857
|
+
} elseif ([string]$Command -eq 'RemoveCursorNeutralBinding') {
|
|
1858
|
+
$null = Assert-CursorNeutralBinding $inputRecord $true
|
|
1859
|
+
} elseif ([string]$Command -eq 'MigrateCursorNeutralBinding') {
|
|
1860
|
+
Assert-ExactFields $inputRecord @(
|
|
1861
|
+
'previousBinding', 'replacementBinding', 'replacementRecord'
|
|
1862
|
+
) 'neutral_cursor_binding_invalid'
|
|
1863
|
+
$null = Assert-CursorNeutralBinding $inputRecord.previousBinding $true
|
|
1864
|
+
$null = Assert-CursorNeutralBinding $inputRecord.replacementBinding $true $true
|
|
1865
|
+
$null = Assert-Record $inputRecord.replacementRecord
|
|
1866
|
+
if ([string]$inputRecord.replacementRecord.state -ne 'active') { Fail 'host_capability_input_invalid' }
|
|
1867
|
+
} elseif ([string]$Command -eq 'SaveDshNeutralBinding') {
|
|
1868
|
+
$null = Assert-DshNeutralBinding $inputRecord $true $true
|
|
1869
|
+
} elseif ([string]$Command -eq 'RemoveDshNeutralBinding') {
|
|
1870
|
+
$null = Assert-DshNeutralBinding $inputRecord $true
|
|
1871
|
+
} elseif ([string]$Command -in @('MigrateDshNeutralBinding', 'RestoreDshNeutralBinding')) {
|
|
1872
|
+
Assert-ExactFields $inputRecord @('previousBinding', 'replacementBinding', 'replacementRecord') 'neutral_dsh_binding_invalid'
|
|
1873
|
+
$null = Assert-DshNeutralBinding $inputRecord.previousBinding $true
|
|
1874
|
+
$restoringPrevious = [string]$Command -eq 'RestoreDshNeutralBinding'
|
|
1875
|
+
$null = Assert-DshNeutralBinding $inputRecord.replacementBinding $true (-not $restoringPrevious)
|
|
1876
|
+
if ($restoringPrevious -and -not (Test-ExactScopeSequence $inputRecord.replacementBinding.scopes $DshNeutralPreviousScopes)) {
|
|
1877
|
+
Fail 'neutral_dsh_capability_binding_mismatch'
|
|
1878
|
+
}
|
|
1879
|
+
$null = Assert-Record $inputRecord.replacementRecord
|
|
1880
|
+
if ([string]$inputRecord.replacementRecord.state -ne 'active') { Fail 'host_capability_input_invalid' }
|
|
1881
|
+
} elseif ($Command -eq 'MigrateTransportCredentialLifetime') {
|
|
1882
|
+
Assert-ExactFields $inputRecord @('previous', 'proof') 'host_transport_migration_input_invalid'
|
|
1883
|
+
$null = Assert-TransportCredential $inputRecord.previous
|
|
1884
|
+
Assert-ExactFields $inputRecord.proof @('credentialId', 'principalId', 'hostId', 'credentialCreatedAt', 'credentialExpiresAt') 'host_transport_migration_proof_invalid'
|
|
1885
|
+
} elseif ($Command -in @('SaveTransportCredential', 'SaveEnrollmentTransportCredential')) {
|
|
1886
|
+
$null = Assert-TransportCredential $inputRecord
|
|
1887
|
+
if ([string]$inputRecord.state -ne 'active') { Fail 'host_transport_credential_input_invalid' }
|
|
1888
|
+
} else {
|
|
1889
|
+
$null = Assert-TransportSession $inputRecord
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
$bindingVerification = [string]$Command -match '^Verify(?:Codex|OpenClaw|TraeWork|WorkBuddy|ClaudeCode|Cursor|Dsh)NeutralBinding$'
|
|
1894
|
+
$operationCommand = switch ([string]$Command) {
|
|
1895
|
+
'VerifyCodexNeutralBinding' { 'ResolveCodexNeutralBinding' }
|
|
1896
|
+
'VerifyOpenClawNeutralBinding' { 'ResolveOpenClawNeutralBinding' }
|
|
1897
|
+
'VerifyTraeWorkNeutralBinding' { 'ResolveTraeWorkNeutralBinding' }
|
|
1898
|
+
'VerifyWorkBuddyNeutralBinding' { 'ResolveWorkBuddyNeutralBinding' }
|
|
1899
|
+
'VerifyClaudeCodeNeutralBinding' { 'ResolveClaudeCodeNeutralBinding' }
|
|
1900
|
+
'VerifyCursorNeutralBinding' { 'ResolveCursorNeutralBinding' }
|
|
1901
|
+
'VerifyDshNeutralBinding' { 'ResolveDshNeutralBinding' }
|
|
1902
|
+
'RestoreDshNeutralBinding' { 'MigrateDshNeutralBinding' }
|
|
1903
|
+
default { [string]$Command }
|
|
1904
|
+
}
|
|
1905
|
+
switch ($operationCommand) {
|
|
1906
|
+
'DisconnectLocalClient' {
|
|
1907
|
+
Assert-ExactFields $inputRecord @('client') 'workbench_disconnect_input_invalid'
|
|
1908
|
+
$fields = @{
|
|
1909
|
+
'codex' = 'codexNeutralBindings'; 'openclaw' = 'openClawNeutralBindings'
|
|
1910
|
+
'trae-work' = 'traeWorkNeutralBindings'; 'workbuddy' = 'workBuddyNeutralBindings'
|
|
1911
|
+
'claude-code' = 'claudeCodeNeutralBindings'
|
|
1912
|
+
'cursor' = 'cursorNeutralBindings'; 'dsh' = 'dshNeutralBindings'
|
|
1913
|
+
}
|
|
1914
|
+
$client = [string]$inputRecord.client
|
|
1915
|
+
if (-not $fields.ContainsKey($client)) { Fail 'workbench_account_client_invalid' }
|
|
1916
|
+
if ($client -ceq 'claude-code' -and -not (Test-ClaudeCodeStoreRoot $root)) {
|
|
1917
|
+
Fail 'neutral_claude_code_store_root_invalid'
|
|
1918
|
+
}
|
|
1919
|
+
foreach ($record in @($store.capabilities)) {
|
|
1920
|
+
if ([string]$record.adapterId -ceq $client -and [string]$record.state -ceq 'active') {
|
|
1921
|
+
$record.state = 'revoked'
|
|
1922
|
+
$record.revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
$field = $fields[$client]
|
|
1926
|
+
$store.$field = @()
|
|
1927
|
+
Write-Store $path $store $root $sid
|
|
1928
|
+
Write-Result ([PSCustomObject]@{ disconnected = $true; client = $client })
|
|
1929
|
+
}
|
|
1930
|
+
'Issue' {
|
|
1931
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.capabilityId) {
|
|
1932
|
+
Fail 'host_capability_id_conflict'
|
|
1933
|
+
}
|
|
1934
|
+
$store.capabilities = @($store.capabilities) + @($inputRecord)
|
|
1935
|
+
Write-Store $path $store $root $sid
|
|
1936
|
+
Write-Result ([PSCustomObject]@{ record = $inputRecord })
|
|
1937
|
+
}
|
|
1938
|
+
'Load' {
|
|
1939
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
1940
|
+
$records = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
1941
|
+
if ($records.Count -ne 1 -or [string]$records[0].state -ne 'active') { Fail 'host_capability_inactive' }
|
|
1942
|
+
Write-Result ([PSCustomObject]@{ record = $records[0] })
|
|
1943
|
+
}
|
|
1944
|
+
'Revoke' {
|
|
1945
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
1946
|
+
$records = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
1947
|
+
if ($records.Count -eq 0) {
|
|
1948
|
+
$archived = Read-AuthorizationHistory $store $root 'capability' $CapabilityId
|
|
1949
|
+
if ($null -ne $archived) { $records = @($archived) }
|
|
1950
|
+
}
|
|
1951
|
+
if ($records.Count -ne 1) { Fail 'host_capability_missing' }
|
|
1952
|
+
if ([string]$records[0].state -eq 'active') {
|
|
1953
|
+
$records[0].state = 'revoked'
|
|
1954
|
+
$records[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
1955
|
+
Write-Store $path $store $root $sid
|
|
1956
|
+
}
|
|
1957
|
+
Write-Result ([PSCustomObject]@{ capabilityId = $CapabilityId; state = 'revoked'; revokedAt = $records[0].revokedAt })
|
|
1958
|
+
}
|
|
1959
|
+
'Rotate' {
|
|
1960
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
1961
|
+
$old = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
1962
|
+
if ($old.Count -ne 1 -or [string]$old[0].state -ne 'active') { Fail 'host_capability_inactive' }
|
|
1963
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.capabilityId) {
|
|
1964
|
+
Fail 'host_capability_id_conflict'
|
|
1965
|
+
}
|
|
1966
|
+
$old[0].state = 'revoked'
|
|
1967
|
+
$old[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
1968
|
+
$store.capabilities = @($store.capabilities) + @($inputRecord)
|
|
1969
|
+
Write-Store $path $store $root $sid
|
|
1970
|
+
Write-Result ([PSCustomObject]@{ record = $inputRecord; replacedCapabilityId = $CapabilityId })
|
|
1971
|
+
}
|
|
1972
|
+
'Status' {
|
|
1973
|
+
Fail 'host_protected_store_status_invalid'
|
|
1974
|
+
}
|
|
1975
|
+
'SaveCodexNeutralBinding' {
|
|
1976
|
+
$existing = @($store.codexNeutralBindings | Where-Object adapterId -CEQ 'codex')
|
|
1977
|
+
if ($existing.Count -gt 1) { Fail 'neutral_codex_binding_invalid' }
|
|
1978
|
+
if ($existing.Count -eq 1) {
|
|
1979
|
+
if (-not (Test-CodexNeutralBindingEqual $existing[0] $inputRecord)) {
|
|
1980
|
+
Fail 'neutral_codex_binding_invalid'
|
|
1981
|
+
}
|
|
1982
|
+
} else {
|
|
1983
|
+
$store.codexNeutralBindings = @($store.codexNeutralBindings) + @($inputRecord)
|
|
1984
|
+
Write-Store $path $store $root $sid
|
|
1985
|
+
}
|
|
1986
|
+
Write-Result ([PSCustomObject]@{ binding = $inputRecord })
|
|
1987
|
+
}
|
|
1988
|
+
'ResolveCodexNeutralBinding' {
|
|
1989
|
+
$bindings = @($store.codexNeutralBindings | Where-Object adapterId -CEQ 'codex')
|
|
1990
|
+
if ($bindings.Count -eq 0) { Fail 'neutral_codex_binding_configuration_required' }
|
|
1991
|
+
if ($bindings.Count -gt 1) { Fail 'neutral_codex_binding_invalid' }
|
|
1992
|
+
$binding = Assert-CodexNeutralBinding $bindings[0] $true
|
|
1993
|
+
$activeCodex = @($store.capabilities | Where-Object {
|
|
1994
|
+
[string]$_.state -ceq 'active' -and [string]$_.adapterId -ceq 'codex'
|
|
1995
|
+
})
|
|
1996
|
+
$matching = @($activeCodex | Where-Object { Test-CodexCapabilityMatch $_ $binding })
|
|
1997
|
+
if ($matching.Count -eq 0) {
|
|
1998
|
+
if ($activeCodex.Count -eq 0) { Fail 'neutral_codex_capability_missing' }
|
|
1999
|
+
Fail 'neutral_codex_capability_binding_mismatch'
|
|
2000
|
+
}
|
|
2001
|
+
if ($matching.Count -gt 1) { Fail 'neutral_codex_capability_ambiguous' }
|
|
2002
|
+
$record = if ($bindingVerification) { Get-PublicCapabilityRecord $matching[0] } else { $matching[0] }
|
|
2003
|
+
Write-Result ([PSCustomObject]@{ binding = $binding; record = $record })
|
|
2004
|
+
}
|
|
2005
|
+
'RemoveCodexNeutralBinding' {
|
|
2006
|
+
$matching = @($store.codexNeutralBindings | Where-Object {
|
|
2007
|
+
Test-CodexNeutralBindingEqual $_ $inputRecord
|
|
2008
|
+
})
|
|
2009
|
+
if ($matching.Count -eq 0) { Fail 'neutral_codex_binding_invalid' }
|
|
2010
|
+
if ($matching.Count -gt 1) { Fail 'neutral_codex_binding_invalid' }
|
|
2011
|
+
$store.codexNeutralBindings = @($store.codexNeutralBindings | Where-Object {
|
|
2012
|
+
-not (Test-CodexNeutralBindingEqual $_ $matching[0])
|
|
2013
|
+
})
|
|
2014
|
+
Write-Store $path $store $root $sid
|
|
2015
|
+
Write-Result ([PSCustomObject]@{ removed = $true })
|
|
2016
|
+
}
|
|
2017
|
+
'ReplaceCodexNeutralBinding' {
|
|
2018
|
+
$previous = $inputRecord.previousBinding
|
|
2019
|
+
$replacement = $inputRecord.replacementBinding
|
|
2020
|
+
if (-not (Test-NeutralBindingOwnerEqual $previous $replacement) -or
|
|
2021
|
+
-not (Test-ExactScopeSequence $previous.scopes $replacement.scopes)) {
|
|
2022
|
+
Fail 'neutral_codex_capability_binding_mismatch'
|
|
2023
|
+
}
|
|
2024
|
+
$bindings = @($store.codexNeutralBindings | Where-Object adapterId -CEQ 'codex')
|
|
2025
|
+
if ($bindings.Count -ne 1 -or
|
|
2026
|
+
-not (Test-CodexNeutralBindingEqual $bindings[0] $previous)) {
|
|
2027
|
+
Fail 'neutral_codex_binding_invalid'
|
|
2028
|
+
}
|
|
2029
|
+
$matching = @($store.capabilities | Where-Object {
|
|
2030
|
+
[string]$_.state -ceq 'active' -and (Test-CodexCapabilityMatch $_ $replacement)
|
|
2031
|
+
})
|
|
2032
|
+
if ($matching.Count -ne 1) { Fail 'neutral_codex_capability_ambiguous' }
|
|
2033
|
+
$store.codexNeutralBindings = @($replacement)
|
|
2034
|
+
Write-Store $path $store $root $sid
|
|
2035
|
+
Write-Result ([PSCustomObject]@{ binding = $replacement })
|
|
2036
|
+
}
|
|
2037
|
+
'MigrateCodexNeutralBinding' {
|
|
2038
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
2039
|
+
$previous = $inputRecord.previousBinding
|
|
2040
|
+
$replacement = $inputRecord.replacementBinding
|
|
2041
|
+
if (-not (Test-NeutralBindingOwnerEqual $previous $replacement)) {
|
|
2042
|
+
Fail 'neutral_codex_capability_binding_mismatch'
|
|
2043
|
+
}
|
|
2044
|
+
$bindings = @($store.codexNeutralBindings | Where-Object adapterId -CEQ 'codex')
|
|
2045
|
+
if ($bindings.Count -ne 1 -or
|
|
2046
|
+
-not (Test-CodexNeutralBindingEqual $bindings[0] $previous)) {
|
|
2047
|
+
Fail 'neutral_codex_binding_invalid'
|
|
2048
|
+
}
|
|
2049
|
+
$old = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
2050
|
+
if ($old.Count -ne 1 -or [string]$old[0].state -cne 'active' -or
|
|
2051
|
+
-not (Test-CodexCapabilityMatch $old[0] $previous)) {
|
|
2052
|
+
Fail 'neutral_codex_capability_binding_mismatch'
|
|
2053
|
+
}
|
|
2054
|
+
if (-not (Test-CodexCapabilityMatch $inputRecord.replacementRecord $replacement)) {
|
|
2055
|
+
Fail 'neutral_codex_capability_binding_mismatch'
|
|
2056
|
+
}
|
|
2057
|
+
$candidates = @($store.capabilities | Where-Object {
|
|
2058
|
+
[string]$_.capabilityId -cne $CapabilityId -and
|
|
2059
|
+
[string]$_.state -ceq 'active' -and
|
|
2060
|
+
(Test-CodexCapabilityMatch $_ $replacement)
|
|
2061
|
+
})
|
|
2062
|
+
if ($candidates.Count -gt 1) { Fail 'neutral_codex_capability_ambiguous' }
|
|
2063
|
+
if ($candidates.Count -eq 1) {
|
|
2064
|
+
$selected = $candidates[0]
|
|
2065
|
+
} else {
|
|
2066
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.replacementRecord.capabilityId) {
|
|
2067
|
+
Fail 'host_capability_id_conflict'
|
|
2068
|
+
}
|
|
2069
|
+
$selected = $inputRecord.replacementRecord
|
|
2070
|
+
$store.capabilities = @($store.capabilities) + @($selected)
|
|
2071
|
+
}
|
|
2072
|
+
$old[0].state = 'revoked'
|
|
2073
|
+
$old[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
2074
|
+
$store.codexNeutralBindings = @($replacement)
|
|
2075
|
+
Write-Store $path $store $root $sid
|
|
2076
|
+
Write-Result ([PSCustomObject]@{
|
|
2077
|
+
binding = $replacement; record = $selected; replacedCapabilityId = $CapabilityId
|
|
2078
|
+
})
|
|
2079
|
+
}
|
|
2080
|
+
'SaveOpenClawNeutralBinding' {
|
|
2081
|
+
$existing = @($store.openClawNeutralBindings | Where-Object adapterId -CEQ 'openclaw')
|
|
2082
|
+
if ($existing.Count -gt 1) { Fail 'neutral_openclaw_binding_invalid' }
|
|
2083
|
+
if ($existing.Count -eq 1) {
|
|
2084
|
+
if (-not (Test-OpenClawNeutralBindingEqual $existing[0] $inputRecord)) {
|
|
2085
|
+
Fail 'neutral_openclaw_binding_invalid'
|
|
2086
|
+
}
|
|
2087
|
+
} else {
|
|
2088
|
+
$store.openClawNeutralBindings = @($store.openClawNeutralBindings) + @($inputRecord)
|
|
2089
|
+
Write-Store $path $store $root $sid
|
|
2090
|
+
}
|
|
2091
|
+
Write-Result ([PSCustomObject]@{ binding = $inputRecord })
|
|
2092
|
+
}
|
|
2093
|
+
'ResolveOpenClawNeutralBinding' {
|
|
2094
|
+
$bindings = @($store.openClawNeutralBindings | Where-Object adapterId -CEQ 'openclaw')
|
|
2095
|
+
if ($bindings.Count -eq 0) { Fail 'neutral_openclaw_binding_configuration_required' }
|
|
2096
|
+
if ($bindings.Count -gt 1) { Fail 'neutral_openclaw_binding_invalid' }
|
|
2097
|
+
$binding = Assert-OpenClawNeutralBinding $bindings[0] $true
|
|
2098
|
+
$activeOpenClaw = @($store.capabilities | Where-Object {
|
|
2099
|
+
[string]$_.state -ceq 'active' -and [string]$_.adapterId -ceq 'openclaw'
|
|
2100
|
+
})
|
|
2101
|
+
$matching = @($activeOpenClaw | Where-Object { Test-OpenClawCapabilityMatch $_ $binding })
|
|
2102
|
+
if ($matching.Count -eq 0) {
|
|
2103
|
+
if ($activeOpenClaw.Count -eq 0) { Fail 'neutral_openclaw_capability_missing' }
|
|
2104
|
+
Fail 'neutral_openclaw_capability_binding_mismatch'
|
|
2105
|
+
}
|
|
2106
|
+
if ($matching.Count -gt 1) { Fail 'neutral_openclaw_capability_ambiguous' }
|
|
2107
|
+
$record = if ($bindingVerification) { Get-PublicCapabilityRecord $matching[0] } else { $matching[0] }
|
|
2108
|
+
Write-Result ([PSCustomObject]@{ binding = $binding; record = $record })
|
|
2109
|
+
}
|
|
2110
|
+
'RemoveOpenClawNeutralBinding' {
|
|
2111
|
+
$matching = @($store.openClawNeutralBindings | Where-Object {
|
|
2112
|
+
Test-OpenClawNeutralBindingEqual $_ $inputRecord
|
|
2113
|
+
})
|
|
2114
|
+
if ($matching.Count -ne 1) { Fail 'neutral_openclaw_binding_invalid' }
|
|
2115
|
+
$store.openClawNeutralBindings = @($store.openClawNeutralBindings | Where-Object {
|
|
2116
|
+
-not (Test-OpenClawNeutralBindingEqual $_ $matching[0])
|
|
2117
|
+
})
|
|
2118
|
+
Write-Store $path $store $root $sid
|
|
2119
|
+
Write-Result ([PSCustomObject]@{ removed = $true })
|
|
2120
|
+
}
|
|
2121
|
+
'MigrateOpenClawNeutralBinding' {
|
|
2122
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
2123
|
+
$previous = $inputRecord.previousBinding
|
|
2124
|
+
$replacement = $inputRecord.replacementBinding
|
|
2125
|
+
if (-not (Test-NeutralBindingOwnerEqual $previous $replacement)) {
|
|
2126
|
+
Fail 'neutral_openclaw_capability_binding_mismatch'
|
|
2127
|
+
}
|
|
2128
|
+
$bindings = @($store.openClawNeutralBindings | Where-Object adapterId -CEQ 'openclaw')
|
|
2129
|
+
if ($bindings.Count -ne 1 -or
|
|
2130
|
+
-not (Test-OpenClawNeutralBindingEqual $bindings[0] $previous)) {
|
|
2131
|
+
Fail 'neutral_openclaw_binding_invalid'
|
|
2132
|
+
}
|
|
2133
|
+
$old = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
2134
|
+
if ($old.Count -ne 1 -or [string]$old[0].state -cne 'active' -or
|
|
2135
|
+
-not (Test-OpenClawCapabilityMatch $old[0] $previous)) {
|
|
2136
|
+
Fail 'neutral_openclaw_capability_binding_mismatch'
|
|
2137
|
+
}
|
|
2138
|
+
if (-not (Test-OpenClawCapabilityMatch $inputRecord.replacementRecord $replacement)) {
|
|
2139
|
+
Fail 'neutral_openclaw_capability_binding_mismatch'
|
|
2140
|
+
}
|
|
2141
|
+
$candidates = @($store.capabilities | Where-Object {
|
|
2142
|
+
[string]$_.capabilityId -cne $CapabilityId -and
|
|
2143
|
+
[string]$_.state -ceq 'active' -and
|
|
2144
|
+
(Test-OpenClawCapabilityMatch $_ $replacement)
|
|
2145
|
+
})
|
|
2146
|
+
if ($candidates.Count -gt 1) { Fail 'neutral_openclaw_capability_ambiguous' }
|
|
2147
|
+
if ($candidates.Count -eq 1) {
|
|
2148
|
+
$selected = $candidates[0]
|
|
2149
|
+
} else {
|
|
2150
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.replacementRecord.capabilityId) {
|
|
2151
|
+
Fail 'host_capability_id_conflict'
|
|
2152
|
+
}
|
|
2153
|
+
$selected = $inputRecord.replacementRecord
|
|
2154
|
+
$store.capabilities = @($store.capabilities) + @($selected)
|
|
2155
|
+
}
|
|
2156
|
+
$old[0].state = 'revoked'
|
|
2157
|
+
$old[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
2158
|
+
$store.openClawNeutralBindings = @($replacement)
|
|
2159
|
+
Write-Store $path $store $root $sid
|
|
2160
|
+
Write-Result ([PSCustomObject]@{
|
|
2161
|
+
binding = $replacement; record = $selected; replacedCapabilityId = $CapabilityId
|
|
2162
|
+
})
|
|
2163
|
+
}
|
|
2164
|
+
'SaveTraeWorkNeutralBinding' {
|
|
2165
|
+
$existing = @($store.traeWorkNeutralBindings | Where-Object adapterId -CEQ 'trae-work')
|
|
2166
|
+
if ($existing.Count -gt 1) { Fail 'neutral_trae_work_binding_invalid' }
|
|
2167
|
+
if ($existing.Count -eq 1) {
|
|
2168
|
+
if (-not (Test-TraeWorkNeutralBindingEqual $existing[0] $inputRecord)) {
|
|
2169
|
+
Fail 'neutral_trae_work_binding_invalid'
|
|
2170
|
+
}
|
|
2171
|
+
} else {
|
|
2172
|
+
$store.traeWorkNeutralBindings = @($store.traeWorkNeutralBindings) + @($inputRecord)
|
|
2173
|
+
Write-Store $path $store $root $sid
|
|
2174
|
+
}
|
|
2175
|
+
Write-Result ([PSCustomObject]@{ binding = $inputRecord })
|
|
2176
|
+
}
|
|
2177
|
+
'ResolveTraeWorkNeutralBinding' {
|
|
2178
|
+
$bindings = @($store.traeWorkNeutralBindings | Where-Object adapterId -CEQ 'trae-work')
|
|
2179
|
+
if ($bindings.Count -eq 0) { Fail 'neutral_trae_work_binding_configuration_required' }
|
|
2180
|
+
if ($bindings.Count -gt 1) { Fail 'neutral_trae_work_binding_invalid' }
|
|
2181
|
+
$binding = Assert-TraeWorkNeutralBinding $bindings[0] $true
|
|
2182
|
+
$activeTraeWork = @($store.capabilities | Where-Object {
|
|
2183
|
+
[string]$_.state -ceq 'active' -and [string]$_.adapterId -ceq 'trae-work'
|
|
2184
|
+
})
|
|
2185
|
+
$matching = @($activeTraeWork | Where-Object { Test-TraeWorkCapabilityMatch $_ $binding })
|
|
2186
|
+
if ($matching.Count -eq 0) {
|
|
2187
|
+
if ($activeTraeWork.Count -eq 0) { Fail 'neutral_trae_work_capability_missing' }
|
|
2188
|
+
Fail 'neutral_trae_work_capability_binding_mismatch'
|
|
2189
|
+
}
|
|
2190
|
+
if ($matching.Count -gt 1) { Fail 'neutral_trae_work_capability_ambiguous' }
|
|
2191
|
+
$record = if ($bindingVerification) { Get-PublicCapabilityRecord $matching[0] } else { $matching[0] }
|
|
2192
|
+
Write-Result ([PSCustomObject]@{ binding = $binding; record = $record })
|
|
2193
|
+
}
|
|
2194
|
+
'RemoveTraeWorkNeutralBinding' {
|
|
2195
|
+
$matching = @($store.traeWorkNeutralBindings | Where-Object {
|
|
2196
|
+
Test-TraeWorkNeutralBindingEqual $_ $inputRecord
|
|
2197
|
+
})
|
|
2198
|
+
if ($matching.Count -ne 1) { Fail 'neutral_trae_work_binding_invalid' }
|
|
2199
|
+
$store.traeWorkNeutralBindings = @($store.traeWorkNeutralBindings | Where-Object {
|
|
2200
|
+
-not (Test-TraeWorkNeutralBindingEqual $_ $matching[0])
|
|
2201
|
+
})
|
|
2202
|
+
Write-Store $path $store $root $sid
|
|
2203
|
+
Write-Result ([PSCustomObject]@{ removed = $true })
|
|
2204
|
+
}
|
|
2205
|
+
'MigrateTraeWorkNeutralBinding' {
|
|
2206
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
2207
|
+
$previous = $inputRecord.previousBinding
|
|
2208
|
+
$replacement = $inputRecord.replacementBinding
|
|
2209
|
+
if (-not (Test-NeutralBindingOwnerEqual $previous $replacement)) {
|
|
2210
|
+
Fail 'neutral_trae_work_capability_binding_mismatch'
|
|
2211
|
+
}
|
|
2212
|
+
$bindings = @($store.traeWorkNeutralBindings | Where-Object adapterId -CEQ 'trae-work')
|
|
2213
|
+
if ($bindings.Count -ne 1 -or
|
|
2214
|
+
-not (Test-TraeWorkNeutralBindingEqual $bindings[0] $previous)) {
|
|
2215
|
+
Fail 'neutral_trae_work_binding_invalid'
|
|
2216
|
+
}
|
|
2217
|
+
$old = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
2218
|
+
if ($old.Count -ne 1 -or [string]$old[0].state -cne 'active' -or
|
|
2219
|
+
-not (Test-TraeWorkCapabilityMatch $old[0] $previous)) {
|
|
2220
|
+
Fail 'neutral_trae_work_capability_binding_mismatch'
|
|
2221
|
+
}
|
|
2222
|
+
if (-not (Test-TraeWorkCapabilityMatch $inputRecord.replacementRecord $replacement)) {
|
|
2223
|
+
Fail 'neutral_trae_work_capability_binding_mismatch'
|
|
2224
|
+
}
|
|
2225
|
+
$candidates = @($store.capabilities | Where-Object {
|
|
2226
|
+
[string]$_.capabilityId -cne $CapabilityId -and
|
|
2227
|
+
[string]$_.state -ceq 'active' -and
|
|
2228
|
+
(Test-TraeWorkCapabilityMatch $_ $replacement)
|
|
2229
|
+
})
|
|
2230
|
+
if ($candidates.Count -gt 1) { Fail 'neutral_trae_work_capability_ambiguous' }
|
|
2231
|
+
if ($candidates.Count -eq 1) {
|
|
2232
|
+
$selected = $candidates[0]
|
|
2233
|
+
} else {
|
|
2234
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.replacementRecord.capabilityId) {
|
|
2235
|
+
Fail 'host_capability_id_conflict'
|
|
2236
|
+
}
|
|
2237
|
+
$selected = $inputRecord.replacementRecord
|
|
2238
|
+
$store.capabilities = @($store.capabilities) + @($selected)
|
|
2239
|
+
}
|
|
2240
|
+
$old[0].state = 'revoked'
|
|
2241
|
+
$old[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
2242
|
+
$store.traeWorkNeutralBindings = @($replacement)
|
|
2243
|
+
Write-Store $path $store $root $sid
|
|
2244
|
+
Write-Result ([PSCustomObject]@{
|
|
2245
|
+
binding = $replacement; record = $selected; replacedCapabilityId = $CapabilityId
|
|
2246
|
+
})
|
|
2247
|
+
}
|
|
2248
|
+
'SaveWorkBuddyNeutralBinding' {
|
|
2249
|
+
$existing = @($store.workBuddyNeutralBindings | Where-Object adapterId -CEQ 'workbuddy')
|
|
2250
|
+
if ($existing.Count -gt 1) { Fail 'neutral_workbuddy_binding_invalid' }
|
|
2251
|
+
if ($existing.Count -eq 1) {
|
|
2252
|
+
if (-not (Test-WorkBuddyNeutralBindingEqual $existing[0] $inputRecord)) {
|
|
2253
|
+
Fail 'neutral_workbuddy_binding_invalid'
|
|
2254
|
+
}
|
|
2255
|
+
} else {
|
|
2256
|
+
$store.workBuddyNeutralBindings = @($store.workBuddyNeutralBindings) + @($inputRecord)
|
|
2257
|
+
Write-Store $path $store $root $sid
|
|
2258
|
+
}
|
|
2259
|
+
Write-Result ([PSCustomObject]@{ binding = $inputRecord })
|
|
2260
|
+
}
|
|
2261
|
+
'ResolveWorkBuddyNeutralBinding' {
|
|
2262
|
+
$bindings = @($store.workBuddyNeutralBindings | Where-Object adapterId -CEQ 'workbuddy')
|
|
2263
|
+
if ($bindings.Count -eq 0) { Fail 'neutral_workbuddy_binding_configuration_required' }
|
|
2264
|
+
if ($bindings.Count -gt 1) { Fail 'neutral_workbuddy_binding_invalid' }
|
|
2265
|
+
$binding = Assert-WorkBuddyNeutralBinding $bindings[0] $true
|
|
2266
|
+
$activeWorkBuddy = @($store.capabilities | Where-Object {
|
|
2267
|
+
[string]$_.state -ceq 'active' -and [string]$_.adapterId -ceq 'workbuddy'
|
|
2268
|
+
})
|
|
2269
|
+
$matching = @($activeWorkBuddy | Where-Object { Test-WorkBuddyCapabilityMatch $_ $binding })
|
|
2270
|
+
if ($matching.Count -eq 0) {
|
|
2271
|
+
if ($activeWorkBuddy.Count -eq 0) { Fail 'neutral_workbuddy_capability_missing' }
|
|
2272
|
+
Fail 'neutral_workbuddy_capability_binding_mismatch'
|
|
2273
|
+
}
|
|
2274
|
+
if ($matching.Count -gt 1) { Fail 'neutral_workbuddy_capability_ambiguous' }
|
|
2275
|
+
$record = if ($bindingVerification) { Get-PublicCapabilityRecord $matching[0] } else { $matching[0] }
|
|
2276
|
+
Write-Result ([PSCustomObject]@{ binding = $binding; record = $record })
|
|
2277
|
+
}
|
|
2278
|
+
'RemoveWorkBuddyNeutralBinding' {
|
|
2279
|
+
$matching = @($store.workBuddyNeutralBindings | Where-Object {
|
|
2280
|
+
Test-WorkBuddyNeutralBindingEqual $_ $inputRecord
|
|
2281
|
+
})
|
|
2282
|
+
if ($matching.Count -ne 1) { Fail 'neutral_workbuddy_binding_invalid' }
|
|
2283
|
+
$store.workBuddyNeutralBindings = @($store.workBuddyNeutralBindings | Where-Object {
|
|
2284
|
+
-not (Test-WorkBuddyNeutralBindingEqual $_ $matching[0])
|
|
2285
|
+
})
|
|
2286
|
+
Write-Store $path $store $root $sid
|
|
2287
|
+
Write-Result ([PSCustomObject]@{ removed = $true })
|
|
2288
|
+
}
|
|
2289
|
+
'MigrateWorkBuddyNeutralBinding' {
|
|
2290
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
2291
|
+
$previous = $inputRecord.previousBinding
|
|
2292
|
+
$replacement = $inputRecord.replacementBinding
|
|
2293
|
+
if (-not (Test-NeutralBindingOwnerEqual $previous $replacement)) {
|
|
2294
|
+
Fail 'neutral_workbuddy_capability_binding_mismatch'
|
|
2295
|
+
}
|
|
2296
|
+
$bindings = @($store.workBuddyNeutralBindings | Where-Object adapterId -CEQ 'workbuddy')
|
|
2297
|
+
if ($bindings.Count -ne 1 -or
|
|
2298
|
+
-not (Test-WorkBuddyNeutralBindingEqual $bindings[0] $previous)) {
|
|
2299
|
+
Fail 'neutral_workbuddy_binding_invalid'
|
|
2300
|
+
}
|
|
2301
|
+
$old = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
2302
|
+
if ($old.Count -ne 1 -or [string]$old[0].state -cne 'active' -or
|
|
2303
|
+
-not (Test-WorkBuddyCapabilityMatch $old[0] $previous)) {
|
|
2304
|
+
Fail 'neutral_workbuddy_capability_binding_mismatch'
|
|
2305
|
+
}
|
|
2306
|
+
if (-not (Test-WorkBuddyCapabilityMatch $inputRecord.replacementRecord $replacement)) {
|
|
2307
|
+
Fail 'neutral_workbuddy_capability_binding_mismatch'
|
|
2308
|
+
}
|
|
2309
|
+
$candidates = @($store.capabilities | Where-Object {
|
|
2310
|
+
[string]$_.capabilityId -cne $CapabilityId -and
|
|
2311
|
+
[string]$_.state -ceq 'active' -and
|
|
2312
|
+
(Test-WorkBuddyCapabilityMatch $_ $replacement)
|
|
2313
|
+
})
|
|
2314
|
+
if ($candidates.Count -gt 1) { Fail 'neutral_workbuddy_capability_ambiguous' }
|
|
2315
|
+
if ($candidates.Count -eq 1) {
|
|
2316
|
+
$selected = $candidates[0]
|
|
2317
|
+
} else {
|
|
2318
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.replacementRecord.capabilityId) {
|
|
2319
|
+
Fail 'host_capability_id_conflict'
|
|
2320
|
+
}
|
|
2321
|
+
$selected = $inputRecord.replacementRecord
|
|
2322
|
+
$store.capabilities = @($store.capabilities) + @($selected)
|
|
2323
|
+
}
|
|
2324
|
+
$old[0].state = 'revoked'
|
|
2325
|
+
$old[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
2326
|
+
$store.workBuddyNeutralBindings = @($replacement)
|
|
2327
|
+
Write-Store $path $store $root $sid
|
|
2328
|
+
Write-Result ([PSCustomObject]@{
|
|
2329
|
+
binding = $replacement; record = $selected; replacedCapabilityId = $CapabilityId
|
|
2330
|
+
})
|
|
2331
|
+
}
|
|
2332
|
+
'SaveClaudeCodeNeutralBinding' {
|
|
2333
|
+
$existing = @($store.claudeCodeNeutralBindings | Where-Object adapterId -CEQ 'claude-code')
|
|
2334
|
+
if ($existing.Count -gt 1) { Fail 'neutral_claude_code_binding_invalid' }
|
|
2335
|
+
if ($existing.Count -eq 1) {
|
|
2336
|
+
if (-not (Test-ClaudeCodeNeutralBindingEqual $existing[0] $inputRecord)) {
|
|
2337
|
+
Fail 'neutral_claude_code_binding_invalid'
|
|
2338
|
+
}
|
|
2339
|
+
} else {
|
|
2340
|
+
$store.claudeCodeNeutralBindings = @($store.claudeCodeNeutralBindings) + @($inputRecord)
|
|
2341
|
+
Write-Store $path $store $root $sid
|
|
2342
|
+
}
|
|
2343
|
+
Write-Result ([PSCustomObject]@{ binding = $inputRecord })
|
|
2344
|
+
}
|
|
2345
|
+
'ResolveClaudeCodeNeutralBinding' {
|
|
2346
|
+
$bindings = @($store.claudeCodeNeutralBindings | Where-Object adapterId -CEQ 'claude-code')
|
|
2347
|
+
if ($bindings.Count -eq 0) { Fail 'neutral_claude_code_binding_configuration_required' }
|
|
2348
|
+
if ($bindings.Count -gt 1) { Fail 'neutral_claude_code_binding_invalid' }
|
|
2349
|
+
$binding = Assert-ClaudeCodeNeutralBinding $bindings[0] $true
|
|
2350
|
+
$activeClaudeCode = @($store.capabilities | Where-Object {
|
|
2351
|
+
[string]$_.state -ceq 'active' -and [string]$_.adapterId -ceq 'claude-code'
|
|
2352
|
+
})
|
|
2353
|
+
$matching = @($activeClaudeCode | Where-Object { Test-ClaudeCodeCapabilityMatch $_ $binding })
|
|
2354
|
+
if ($matching.Count -eq 0) {
|
|
2355
|
+
if ($activeClaudeCode.Count -eq 0) { Fail 'neutral_claude_code_capability_missing' }
|
|
2356
|
+
Fail 'neutral_claude_code_capability_binding_mismatch'
|
|
2357
|
+
}
|
|
2358
|
+
if ($matching.Count -gt 1) { Fail 'neutral_claude_code_capability_ambiguous' }
|
|
2359
|
+
$record = if ($bindingVerification) { Get-PublicCapabilityRecord $matching[0] } else { $matching[0] }
|
|
2360
|
+
Write-Result ([PSCustomObject]@{ binding = $binding; record = $record })
|
|
2361
|
+
}
|
|
2362
|
+
'RemoveClaudeCodeNeutralBinding' {
|
|
2363
|
+
$matching = @($store.claudeCodeNeutralBindings | Where-Object {
|
|
2364
|
+
Test-ClaudeCodeNeutralBindingEqual $_ $inputRecord
|
|
2365
|
+
})
|
|
2366
|
+
if ($matching.Count -ne 1) { Fail 'neutral_claude_code_binding_invalid' }
|
|
2367
|
+
$store.claudeCodeNeutralBindings = @($store.claudeCodeNeutralBindings | Where-Object {
|
|
2368
|
+
-not (Test-ClaudeCodeNeutralBindingEqual $_ $matching[0])
|
|
2369
|
+
})
|
|
2370
|
+
Write-Store $path $store $root $sid
|
|
2371
|
+
Write-Result ([PSCustomObject]@{ removed = $true })
|
|
2372
|
+
}
|
|
2373
|
+
'MigrateClaudeCodeNeutralBinding' {
|
|
2374
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
2375
|
+
$previous = $inputRecord.previousBinding
|
|
2376
|
+
$replacement = $inputRecord.replacementBinding
|
|
2377
|
+
if (-not (Test-NeutralBindingOwnerEqual $previous $replacement)) {
|
|
2378
|
+
Fail 'neutral_claude_code_capability_binding_mismatch'
|
|
2379
|
+
}
|
|
2380
|
+
$bindings = @($store.claudeCodeNeutralBindings | Where-Object adapterId -CEQ 'claude-code')
|
|
2381
|
+
if ($bindings.Count -ne 1 -or
|
|
2382
|
+
-not (Test-ClaudeCodeNeutralBindingEqual $bindings[0] $previous)) {
|
|
2383
|
+
Fail 'neutral_claude_code_binding_invalid'
|
|
2384
|
+
}
|
|
2385
|
+
$old = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
2386
|
+
if ($old.Count -ne 1 -or [string]$old[0].state -cne 'active' -or
|
|
2387
|
+
-not (Test-ClaudeCodeCapabilityMatch $old[0] $previous)) {
|
|
2388
|
+
Fail 'neutral_claude_code_capability_binding_mismatch'
|
|
2389
|
+
}
|
|
2390
|
+
if (-not (Test-ClaudeCodeCapabilityMatch $inputRecord.replacementRecord $replacement)) {
|
|
2391
|
+
Fail 'neutral_claude_code_capability_binding_mismatch'
|
|
2392
|
+
}
|
|
2393
|
+
$candidates = @($store.capabilities | Where-Object {
|
|
2394
|
+
[string]$_.capabilityId -cne $CapabilityId -and
|
|
2395
|
+
[string]$_.state -ceq 'active' -and
|
|
2396
|
+
(Test-ClaudeCodeCapabilityMatch $_ $replacement)
|
|
2397
|
+
})
|
|
2398
|
+
if ($candidates.Count -gt 1) { Fail 'neutral_claude_code_capability_ambiguous' }
|
|
2399
|
+
if ($candidates.Count -eq 1) {
|
|
2400
|
+
$selected = $candidates[0]
|
|
2401
|
+
} else {
|
|
2402
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.replacementRecord.capabilityId) {
|
|
2403
|
+
Fail 'host_capability_id_conflict'
|
|
2404
|
+
}
|
|
2405
|
+
$selected = $inputRecord.replacementRecord
|
|
2406
|
+
$store.capabilities = @($store.capabilities) + @($selected)
|
|
2407
|
+
}
|
|
2408
|
+
$old[0].state = 'revoked'
|
|
2409
|
+
$old[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
2410
|
+
$store.claudeCodeNeutralBindings = @($replacement)
|
|
2411
|
+
Write-Store $path $store $root $sid
|
|
2412
|
+
Write-Result ([PSCustomObject]@{
|
|
2413
|
+
binding = $replacement; record = $selected; replacedCapabilityId = $CapabilityId
|
|
2414
|
+
})
|
|
2415
|
+
}
|
|
2416
|
+
'SaveCursorNeutralBinding' {
|
|
2417
|
+
$existing = @($store.cursorNeutralBindings | Where-Object adapterId -CEQ 'cursor')
|
|
2418
|
+
if ($existing.Count -gt 1) { Fail 'neutral_cursor_binding_invalid' }
|
|
2419
|
+
if ($existing.Count -eq 1) {
|
|
2420
|
+
if (-not (Test-CursorNeutralBindingEqual $existing[0] $inputRecord)) {
|
|
2421
|
+
Fail 'neutral_cursor_binding_invalid'
|
|
2422
|
+
}
|
|
2423
|
+
} else {
|
|
2424
|
+
$store.cursorNeutralBindings = @($store.cursorNeutralBindings) + @($inputRecord)
|
|
2425
|
+
Write-Store $path $store $root $sid
|
|
2426
|
+
}
|
|
2427
|
+
Write-Result ([PSCustomObject]@{ binding = $inputRecord })
|
|
2428
|
+
}
|
|
2429
|
+
'ResolveCursorNeutralBinding' {
|
|
2430
|
+
$bindings = @($store.cursorNeutralBindings | Where-Object adapterId -CEQ 'cursor')
|
|
2431
|
+
if ($bindings.Count -eq 0) { Fail 'neutral_cursor_binding_configuration_required' }
|
|
2432
|
+
if ($bindings.Count -gt 1) { Fail 'neutral_cursor_binding_invalid' }
|
|
2433
|
+
$binding = Assert-CursorNeutralBinding $bindings[0] $true
|
|
2434
|
+
$activeCursor = @($store.capabilities | Where-Object {
|
|
2435
|
+
[string]$_.state -ceq 'active' -and [string]$_.adapterId -ceq 'cursor'
|
|
2436
|
+
})
|
|
2437
|
+
$matching = @($activeCursor | Where-Object { Test-CursorCapabilityMatch $_ $binding })
|
|
2438
|
+
if ($matching.Count -eq 0) {
|
|
2439
|
+
if ($activeCursor.Count -eq 0) { Fail 'neutral_cursor_capability_missing' }
|
|
2440
|
+
Fail 'neutral_cursor_capability_binding_mismatch'
|
|
2441
|
+
}
|
|
2442
|
+
if ($matching.Count -gt 1) { Fail 'neutral_cursor_capability_ambiguous' }
|
|
2443
|
+
$record = if ($bindingVerification) { Get-PublicCapabilityRecord $matching[0] } else { $matching[0] }
|
|
2444
|
+
Write-Result ([PSCustomObject]@{ binding = $binding; record = $record })
|
|
2445
|
+
}
|
|
2446
|
+
'RemoveCursorNeutralBinding' {
|
|
2447
|
+
$matching = @($store.cursorNeutralBindings | Where-Object {
|
|
2448
|
+
Test-CursorNeutralBindingEqual $_ $inputRecord
|
|
2449
|
+
})
|
|
2450
|
+
if ($matching.Count -ne 1) { Fail 'neutral_cursor_binding_invalid' }
|
|
2451
|
+
$store.cursorNeutralBindings = @($store.cursorNeutralBindings | Where-Object {
|
|
2452
|
+
-not (Test-CursorNeutralBindingEqual $_ $matching[0])
|
|
2453
|
+
})
|
|
2454
|
+
Write-Store $path $store $root $sid
|
|
2455
|
+
Write-Result ([PSCustomObject]@{ removed = $true })
|
|
2456
|
+
}
|
|
2457
|
+
'MigrateCursorNeutralBinding' {
|
|
2458
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
2459
|
+
$previous = $inputRecord.previousBinding
|
|
2460
|
+
$replacement = $inputRecord.replacementBinding
|
|
2461
|
+
if (-not (Test-NeutralBindingOwnerEqual $previous $replacement)) {
|
|
2462
|
+
Fail 'neutral_cursor_capability_binding_mismatch'
|
|
2463
|
+
}
|
|
2464
|
+
$bindings = @($store.cursorNeutralBindings | Where-Object adapterId -CEQ 'cursor')
|
|
2465
|
+
if ($bindings.Count -ne 1 -or
|
|
2466
|
+
-not (Test-CursorNeutralBindingEqual $bindings[0] $previous)) {
|
|
2467
|
+
Fail 'neutral_cursor_binding_invalid'
|
|
2468
|
+
}
|
|
2469
|
+
$old = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
2470
|
+
if ($old.Count -ne 1 -or [string]$old[0].state -cne 'active' -or
|
|
2471
|
+
-not (Test-CursorCapabilityMatch $old[0] $previous)) {
|
|
2472
|
+
Fail 'neutral_cursor_capability_binding_mismatch'
|
|
2473
|
+
}
|
|
2474
|
+
if (-not (Test-CursorCapabilityMatch $inputRecord.replacementRecord $replacement)) {
|
|
2475
|
+
Fail 'neutral_cursor_capability_binding_mismatch'
|
|
2476
|
+
}
|
|
2477
|
+
$candidates = @($store.capabilities | Where-Object {
|
|
2478
|
+
[string]$_.capabilityId -cne $CapabilityId -and
|
|
2479
|
+
[string]$_.state -ceq 'active' -and
|
|
2480
|
+
(Test-CursorCapabilityMatch $_ $replacement)
|
|
2481
|
+
})
|
|
2482
|
+
if ($candidates.Count -gt 1) { Fail 'neutral_cursor_capability_ambiguous' }
|
|
2483
|
+
if ($candidates.Count -eq 1) {
|
|
2484
|
+
$selected = $candidates[0]
|
|
2485
|
+
} else {
|
|
2486
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.replacementRecord.capabilityId) {
|
|
2487
|
+
Fail 'host_capability_id_conflict'
|
|
2488
|
+
}
|
|
2489
|
+
$selected = $inputRecord.replacementRecord
|
|
2490
|
+
$store.capabilities = @($store.capabilities) + @($selected)
|
|
2491
|
+
}
|
|
2492
|
+
$old[0].state = 'revoked'
|
|
2493
|
+
$old[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
2494
|
+
$store.cursorNeutralBindings = @($replacement)
|
|
2495
|
+
Write-Store $path $store $root $sid
|
|
2496
|
+
Write-Result ([PSCustomObject]@{
|
|
2497
|
+
binding = $replacement; record = $selected; replacedCapabilityId = $CapabilityId
|
|
2498
|
+
})
|
|
2499
|
+
}
|
|
2500
|
+
'SaveDshNeutralBinding' {
|
|
2501
|
+
$existing = @($store.dshNeutralBindings | Where-Object adapterId -CEQ 'dsh')
|
|
2502
|
+
if ($existing.Count -gt 1) { Fail 'neutral_dsh_binding_invalid' }
|
|
2503
|
+
if ($existing.Count -eq 1) {
|
|
2504
|
+
if (-not (Test-DshNeutralBindingEqual $existing[0] $inputRecord)) { Fail 'neutral_dsh_binding_invalid' }
|
|
2505
|
+
} else { $store.dshNeutralBindings = @($store.dshNeutralBindings) + @($inputRecord); Write-Store $path $store $root $sid }
|
|
2506
|
+
Write-Result ([PSCustomObject]@{ binding = $inputRecord })
|
|
2507
|
+
}
|
|
2508
|
+
'ResolveDshNeutralBinding' {
|
|
2509
|
+
$bindings = @($store.dshNeutralBindings | Where-Object adapterId -CEQ 'dsh')
|
|
2510
|
+
if ($bindings.Count -eq 0) { Fail 'neutral_dsh_binding_configuration_required' }
|
|
2511
|
+
if ($bindings.Count -gt 1) { Fail 'neutral_dsh_binding_invalid' }
|
|
2512
|
+
$binding = Assert-DshNeutralBinding $bindings[0] $true
|
|
2513
|
+
$activeDsh = @($store.capabilities | Where-Object { [string]$_.state -ceq 'active' -and [string]$_.adapterId -ceq 'dsh' })
|
|
2514
|
+
$matching = @($activeDsh | Where-Object { Test-DshCapabilityMatch $_ $binding })
|
|
2515
|
+
if ($matching.Count -eq 0) { if ($activeDsh.Count -eq 0) { Fail 'neutral_dsh_capability_missing' }; Fail 'neutral_dsh_capability_binding_mismatch' }
|
|
2516
|
+
if ($matching.Count -gt 1) { Fail 'neutral_dsh_capability_ambiguous' }
|
|
2517
|
+
$record = if ($bindingVerification) { Get-PublicCapabilityRecord $matching[0] } else { $matching[0] }
|
|
2518
|
+
Write-Result ([PSCustomObject]@{ binding = $binding; record = $record })
|
|
2519
|
+
}
|
|
2520
|
+
'RemoveDshNeutralBinding' {
|
|
2521
|
+
$matching = @($store.dshNeutralBindings | Where-Object { Test-DshNeutralBindingEqual $_ $inputRecord })
|
|
2522
|
+
if ($matching.Count -ne 1) { Fail 'neutral_dsh_binding_invalid' }
|
|
2523
|
+
$store.dshNeutralBindings = @($store.dshNeutralBindings | Where-Object { -not (Test-DshNeutralBindingEqual $_ $matching[0]) })
|
|
2524
|
+
Write-Store $path $store $root $sid; Write-Result ([PSCustomObject]@{ removed = $true })
|
|
2525
|
+
}
|
|
2526
|
+
'MigrateDshNeutralBinding' {
|
|
2527
|
+
if ($CapabilityId -notmatch $CapabilityIdPattern) { Fail 'host_capability_id_invalid' }
|
|
2528
|
+
$previous = $inputRecord.previousBinding; $replacement = $inputRecord.replacementBinding
|
|
2529
|
+
if (-not (Test-NeutralBindingOwnerEqual $previous $replacement)) { Fail 'neutral_dsh_capability_binding_mismatch' }
|
|
2530
|
+
$bindings = @($store.dshNeutralBindings | Where-Object adapterId -CEQ 'dsh')
|
|
2531
|
+
if ($bindings.Count -ne 1 -or -not (Test-DshNeutralBindingEqual $bindings[0] $previous)) { Fail 'neutral_dsh_binding_invalid' }
|
|
2532
|
+
$old = @($store.capabilities | Where-Object capabilityId -CEQ $CapabilityId)
|
|
2533
|
+
if ($old.Count -ne 1 -or [string]$old[0].state -cne 'active' -or -not (Test-DshCapabilityMatch $old[0] $previous) -or -not (Test-DshCapabilityMatch $inputRecord.replacementRecord $replacement)) { Fail 'neutral_dsh_capability_binding_mismatch' }
|
|
2534
|
+
if (@($store.capabilities | Where-Object { [string]$_.capabilityId -cne $CapabilityId -and [string]$_.state -ceq 'active' -and (Test-DshCapabilityMatch $_ $replacement) }).Count -ne 0) { Fail 'neutral_dsh_capability_ambiguous' }
|
|
2535
|
+
if (Test-AuthorizationIdUsed $store $root 'capability' $inputRecord.replacementRecord.capabilityId) { Fail 'host_capability_id_conflict' }
|
|
2536
|
+
$store.capabilities = @($store.capabilities) + @($inputRecord.replacementRecord); $old[0].state = 'revoked'; $old[0].revokedAt = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(); $store.dshNeutralBindings = @($replacement)
|
|
2537
|
+
Write-Store $path $store $root $sid; Write-Result ([PSCustomObject]@{ binding = $replacement; record = $inputRecord.replacementRecord; replacedCapabilityId = $CapabilityId })
|
|
2538
|
+
}
|
|
2539
|
+
{ $_ -in @('SaveTransportCredential', 'SaveEnrollmentTransportCredential') } {
|
|
2540
|
+
$enrollmentUpgrade = $Command -eq 'SaveEnrollmentTransportCredential'
|
|
2541
|
+
if ($enrollmentUpgrade) {
|
|
2542
|
+
# This is the same per-root mutex used by enrollment CAS. Approval
|
|
2543
|
+
# and replacement cannot race with another local setup operation.
|
|
2544
|
+
$enrollment = Read-DeviceEnrollment (Join-Path $root 'device-enrollment-v1.dpapi.clixml') $sid
|
|
2545
|
+
Assert-ExactFields $enrollment.value @('baseUrl', 'hellolynkId', 'hostId', 'deviceLabel', 'requestId', 'proof', 'approval', 'intents') 'device_enrollment_transport_approval_mismatch'
|
|
2546
|
+
$approval = $enrollment.value.approval
|
|
2547
|
+
Assert-ExactFields $approval @('status', 'principalId', 'hellolynkId', 'region', 'hostId', 'hostTransportCredential') 'device_enrollment_transport_approval_mismatch'
|
|
2548
|
+
Assert-ExactFields $approval.hostTransportCredential @('credentialId', 'credential', 'createdAt', 'expiresAt') 'device_enrollment_transport_approval_mismatch'
|
|
2549
|
+
if ($null -eq $approval -or [string]$approval.status -cne 'approved' -or
|
|
2550
|
+
[string]$approval.principalId -cne [string]$inputRecord.principalId -or
|
|
2551
|
+
[string]$approval.hostId -cne [string]$inputRecord.hostInstallationId -or
|
|
2552
|
+
[string]$enrollment.value.hostId -cne [string]$inputRecord.hostInstallationId -or
|
|
2553
|
+
[string]$approval.hellolynkId -cne [string]$enrollment.value.hellolynkId -or
|
|
2554
|
+
[string]$approval.hostTransportCredential.credentialId -cne [string]$inputRecord.credentialId -or
|
|
2555
|
+
[string]$approval.hostTransportCredential.credential -cne [string]$inputRecord.secret) {
|
|
2556
|
+
Fail 'device_enrollment_transport_approval_mismatch'
|
|
2557
|
+
}
|
|
2558
|
+
try {
|
|
2559
|
+
$approvedCreatedAt = ([DateTimeOffset]$approval.hostTransportCredential.createdAt).ToUnixTimeMilliseconds()
|
|
2560
|
+
$approvedExpiresAt = ([DateTimeOffset]$approval.hostTransportCredential.expiresAt).ToUnixTimeMilliseconds()
|
|
2561
|
+
} catch { Fail 'device_enrollment_transport_approval_mismatch' }
|
|
2562
|
+
if ($approvedCreatedAt -ne [int64]$inputRecord.issuedAt -or $approvedExpiresAt -ne [int64]$inputRecord.expiresAt) {
|
|
2563
|
+
Fail 'device_enrollment_transport_approval_mismatch'
|
|
2564
|
+
}
|
|
2565
|
+
if ([int64]$inputRecord.expiresAt -le [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()) {
|
|
2566
|
+
Fail 'host_transport_credential_expired'
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
$existing = @($store.transportCredentials | Where-Object credentialId -CEQ $inputRecord.credentialId)
|
|
2570
|
+
if ($existing.Count -eq 0 -and (Test-AuthorizationIdUsed $store $root 'credential' $inputRecord.credentialId)) {
|
|
2571
|
+
Fail 'host_transport_credential_immutable_conflict'
|
|
2572
|
+
}
|
|
2573
|
+
if ($existing.Count -gt 1) { Fail 'host_transport_credential_duplicate' }
|
|
2574
|
+
if ($existing.Count -eq 1) {
|
|
2575
|
+
if (-not (Test-TransportCredentialEqual $existing[0] $inputRecord)) {
|
|
2576
|
+
Fail 'host_transport_credential_immutable_conflict'
|
|
2577
|
+
}
|
|
2578
|
+
} else {
|
|
2579
|
+
$sameOwner = @($store.transportCredentials | Where-Object {
|
|
2580
|
+
[string]$_.principalId -ceq [string]$inputRecord.principalId -and
|
|
2581
|
+
[string]$_.hostInstallationId -ceq [string]$inputRecord.hostInstallationId -and
|
|
2582
|
+
[string]$_.state -ceq 'active'
|
|
2583
|
+
})
|
|
2584
|
+
if ($sameOwner.Count -gt 1 -or ($sameOwner.Count -gt 0 -and -not $enrollmentUpgrade)) {
|
|
2585
|
+
Fail 'host_transport_credential_ambiguous'
|
|
2586
|
+
}
|
|
2587
|
+
if ($sameOwner.Count -eq 1) {
|
|
2588
|
+
$old = $sameOwner[0]
|
|
2589
|
+
$replacementTime = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
2590
|
+
if ($replacementTime -lt [int64]$old.issuedAt) { Fail 'host_transport_credential_revoke_timestamp_invalid' }
|
|
2591
|
+
$sessions = @($store.transportSessions | Where-Object credentialId -CEQ $old.credentialId)
|
|
2592
|
+
$store.retiredTransportSessionIds = @(
|
|
2593
|
+
@($store.retiredTransportSessionIds) + @($sessions | ForEach-Object sessionId) | Sort-Object -Unique
|
|
2594
|
+
)
|
|
2595
|
+
$store.transportSessions = @($store.transportSessions | Where-Object credentialId -CNE $old.credentialId)
|
|
2596
|
+
$old.state = 'revoked'; $old.revokedAt = $replacementTime
|
|
2597
|
+
}
|
|
2598
|
+
$store.transportCredentials = @($store.transportCredentials) + @($inputRecord)
|
|
2599
|
+
Write-Store $path $store $root $sid
|
|
2600
|
+
}
|
|
2601
|
+
Write-Result ([PSCustomObject]@{ record = $inputRecord })
|
|
2602
|
+
}
|
|
2603
|
+
{ $_ -in @('VerifyTransportCredentialLifetimeMigration', 'MigrateTransportCredentialLifetime') } {
|
|
2604
|
+
Assert-QueryOwner
|
|
2605
|
+
$records = @($store.transportCredentials | Where-Object {
|
|
2606
|
+
[string]$_.principalId -ceq $PrincipalId -and
|
|
2607
|
+
[string]$_.hostInstallationId -ceq $HostInstallationId -and
|
|
2608
|
+
[string]$_.state -ceq 'active' -and $null -eq $_.revokedAt
|
|
2609
|
+
})
|
|
2610
|
+
if ($records.Count -ne 1) { Fail 'host_transport_migration_credential_inactive_or_ambiguous' }
|
|
2611
|
+
$record = $records[0]
|
|
2612
|
+
$permanent = [int64]253402300799999
|
|
2613
|
+
if ([int64]$record.expiresAt -ne $permanent -and
|
|
2614
|
+
([int64]$record.expiresAt - [int64]$record.issuedAt) -ne [int64]2592000000) {
|
|
2615
|
+
Fail 'host_transport_migration_lifetime_invalid'
|
|
2616
|
+
}
|
|
2617
|
+
if ($Command -eq 'VerifyTransportCredentialLifetimeMigration') {
|
|
2618
|
+
Write-Result ([PSCustomObject]@{ record = $record })
|
|
2619
|
+
} else {
|
|
2620
|
+
$previous = $inputRecord.previous
|
|
2621
|
+
if ([string]$previous.state -cne 'active' -or $null -ne $previous.revokedAt -or
|
|
2622
|
+
([int64]$previous.expiresAt - [int64]$previous.issuedAt) -ne [int64]2592000000 -or
|
|
2623
|
+
-not (Test-TransportCredentialEqual $record $previous)) {
|
|
2624
|
+
Fail 'host_transport_migration_compare_failed'
|
|
2625
|
+
}
|
|
2626
|
+
$proof = $inputRecord.proof
|
|
2627
|
+
try {
|
|
2628
|
+
$createdAt = ([DateTimeOffset]$proof.credentialCreatedAt).ToUnixTimeMilliseconds()
|
|
2629
|
+
$expiresAt = ([DateTimeOffset]$proof.credentialExpiresAt).ToUnixTimeMilliseconds()
|
|
2630
|
+
} catch { Fail 'host_transport_migration_proof_invalid' }
|
|
2631
|
+
if ([string]$proof.credentialId -cne [string]$record.credentialId -or
|
|
2632
|
+
[string]$proof.principalId -cne $PrincipalId -or [string]$proof.hostId -cne $HostInstallationId -or
|
|
2633
|
+
$createdAt -ne [int64]$record.issuedAt -or $expiresAt -ne $permanent) {
|
|
2634
|
+
Fail 'host_transport_migration_proof_invalid'
|
|
2635
|
+
}
|
|
2636
|
+
$record.expiresAt = $permanent
|
|
2637
|
+
Write-Store $path $store $root $sid
|
|
2638
|
+
Write-Result ([PSCustomObject]@{ credentialId = $record.credentialId; expiresAt = $permanent; status = 'migrated' })
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
'LoadTransportCredential' {
|
|
2642
|
+
Assert-QueryOwner
|
|
2643
|
+
if ($CredentialId -notmatch $IdentifierPattern) { Fail 'host_transport_credential_id_invalid' }
|
|
2644
|
+
if ($Timestamp -lt 0) { Fail 'host_timestamp_invalid' }
|
|
2645
|
+
$records = @($store.transportCredentials | Where-Object credentialId -CEQ $CredentialId)
|
|
2646
|
+
if ($records.Count -ne 1 -or [string]$records[0].state -ne 'active') {
|
|
2647
|
+
Fail 'host_transport_credential_inactive'
|
|
2648
|
+
}
|
|
2649
|
+
if (
|
|
2650
|
+
[string]$records[0].principalId -cne $PrincipalId -or
|
|
2651
|
+
[string]$records[0].hostInstallationId -cne $HostInstallationId
|
|
2652
|
+
) { Fail 'host_transport_credential_owner_mismatch' }
|
|
2653
|
+
if ([int64]$records[0].expiresAt -le $Timestamp) { Fail 'host_transport_credential_expired' }
|
|
2654
|
+
Write-Result ([PSCustomObject]@{ record = $records[0] })
|
|
2655
|
+
}
|
|
2656
|
+
'RevokeTransportCredential' {
|
|
2657
|
+
Assert-QueryOwner
|
|
2658
|
+
if ($CredentialId -notmatch $IdentifierPattern) { Fail 'host_transport_credential_id_invalid' }
|
|
2659
|
+
if ($Timestamp -lt 0) { Fail 'host_timestamp_invalid' }
|
|
2660
|
+
$records = @($store.transportCredentials | Where-Object credentialId -CEQ $CredentialId)
|
|
2661
|
+
if ($records.Count -eq 0) {
|
|
2662
|
+
$archived = Read-AuthorizationHistory $store $root 'credential' $CredentialId
|
|
2663
|
+
if ($null -ne $archived) { $records = @($archived) }
|
|
2664
|
+
}
|
|
2665
|
+
if ($records.Count -ne 1) { Fail 'host_transport_credential_missing' }
|
|
2666
|
+
if (
|
|
2667
|
+
[string]$records[0].principalId -cne $PrincipalId -or
|
|
2668
|
+
[string]$records[0].hostInstallationId -cne $HostInstallationId
|
|
2669
|
+
) { Fail 'host_transport_credential_owner_mismatch' }
|
|
2670
|
+
if ($Timestamp -lt [int64]$records[0].issuedAt) {
|
|
2671
|
+
Fail 'host_transport_credential_revoke_timestamp_invalid'
|
|
2672
|
+
}
|
|
2673
|
+
$sessions = @($store.transportSessions | Where-Object credentialId -CEQ $CredentialId)
|
|
2674
|
+
if ([string]$records[0].state -eq 'active') {
|
|
2675
|
+
$records[0].state = 'revoked'
|
|
2676
|
+
$records[0].revokedAt = $Timestamp
|
|
2677
|
+
$store.retiredTransportSessionIds = @(
|
|
2678
|
+
@($store.retiredTransportSessionIds) + @($sessions | ForEach-Object sessionId) |
|
|
2679
|
+
Sort-Object -Unique
|
|
2680
|
+
)
|
|
2681
|
+
$store.transportSessions = @($store.transportSessions | Where-Object credentialId -CNE $CredentialId)
|
|
2682
|
+
Write-Store $path $store $root $sid
|
|
2683
|
+
}
|
|
2684
|
+
Write-Result ([PSCustomObject]@{
|
|
2685
|
+
credentialId = $CredentialId
|
|
2686
|
+
state = 'revoked'
|
|
2687
|
+
revokedAt = $records[0].revokedAt
|
|
2688
|
+
cascadedSessionCount = $sessions.Count
|
|
2689
|
+
})
|
|
2690
|
+
}
|
|
2691
|
+
'SaveTransportSession' {
|
|
2692
|
+
if ($Timestamp -lt 0) { Fail 'host_timestamp_invalid' }
|
|
2693
|
+
$credential = @($store.transportCredentials | Where-Object credentialId -CEQ $inputRecord.credentialId)
|
|
2694
|
+
if ($credential.Count -ne 1 -or [string]$credential[0].state -ne 'active') {
|
|
2695
|
+
Fail 'host_transport_credential_inactive'
|
|
2696
|
+
}
|
|
2697
|
+
if (
|
|
2698
|
+
[string]$credential[0].principalId -cne [string]$inputRecord.principalId -or
|
|
2699
|
+
[string]$credential[0].hostInstallationId -cne [string]$inputRecord.hostInstallationId -or
|
|
2700
|
+
[int64]$credential[0].expiresAt -le $Timestamp -or
|
|
2701
|
+
[int64]$inputRecord.expiresAt -gt [int64]$credential[0].expiresAt
|
|
2702
|
+
) { Fail 'host_transport_session_credential_mismatch' }
|
|
2703
|
+
if (Test-RetiredTransportSession $store $root ([string]$inputRecord.sessionId)) {
|
|
2704
|
+
Fail 'host_transport_session_retired'
|
|
2705
|
+
}
|
|
2706
|
+
$existing = @($store.transportSessions | Where-Object sessionId -CEQ $inputRecord.sessionId)
|
|
2707
|
+
if ($existing.Count -gt 1) { Fail 'host_transport_session_duplicate' }
|
|
2708
|
+
if ($existing.Count -eq 1) {
|
|
2709
|
+
if (-not (Test-TransportSessionEqual $existing[0] $inputRecord)) {
|
|
2710
|
+
Fail 'host_transport_session_immutable_conflict'
|
|
2711
|
+
}
|
|
2712
|
+
} else {
|
|
2713
|
+
$sameOwner = @($store.transportSessions | Where-Object {
|
|
2714
|
+
[string]$_.principalId -ceq [string]$inputRecord.principalId -and
|
|
2715
|
+
[string]$_.hostInstallationId -ceq [string]$inputRecord.hostInstallationId
|
|
2716
|
+
})
|
|
2717
|
+
if ($sameOwner.Count -gt 0) { Fail 'host_transport_session_ambiguous' }
|
|
2718
|
+
$store.transportSessions = @($store.transportSessions) + @($inputRecord)
|
|
2719
|
+
Write-Store $path $store $root $sid
|
|
2720
|
+
}
|
|
2721
|
+
Write-Result ([PSCustomObject]@{ record = $inputRecord })
|
|
2722
|
+
}
|
|
2723
|
+
'ReplaceTransportSession' {
|
|
2724
|
+
if ($Timestamp -lt 0) { Fail 'host_timestamp_invalid' }
|
|
2725
|
+
$credential = @($store.transportCredentials | Where-Object credentialId -CEQ $inputRecord.credentialId)
|
|
2726
|
+
if ($credential.Count -ne 1 -or [string]$credential[0].state -ne 'active') {
|
|
2727
|
+
Fail 'host_transport_credential_inactive'
|
|
2728
|
+
}
|
|
2729
|
+
if (
|
|
2730
|
+
[string]$credential[0].principalId -cne [string]$inputRecord.principalId -or
|
|
2731
|
+
[string]$credential[0].hostInstallationId -cne [string]$inputRecord.hostInstallationId -or
|
|
2732
|
+
[int64]$credential[0].expiresAt -le $Timestamp -or
|
|
2733
|
+
[int64]$inputRecord.expiresAt -gt [int64]$credential[0].expiresAt
|
|
2734
|
+
) { Fail 'host_transport_session_credential_mismatch' }
|
|
2735
|
+
if (Test-RetiredTransportSession $store $root ([string]$inputRecord.sessionId)) {
|
|
2736
|
+
Fail 'host_transport_session_retired'
|
|
2737
|
+
}
|
|
2738
|
+
$sameId = @($store.transportSessions | Where-Object sessionId -CEQ $inputRecord.sessionId)
|
|
2739
|
+
if ($sameId.Count -gt 0 -and -not (Test-TransportSessionEqual $sameId[0] $inputRecord)) {
|
|
2740
|
+
Fail 'host_transport_session_immutable_conflict'
|
|
2741
|
+
}
|
|
2742
|
+
if ($sameId.Count -eq 0) {
|
|
2743
|
+
$old = @($store.transportSessions | Where-Object {
|
|
2744
|
+
[string]$_.principalId -ceq [string]$inputRecord.principalId -and
|
|
2745
|
+
[string]$_.hostInstallationId -ceq [string]$inputRecord.hostInstallationId
|
|
2746
|
+
})
|
|
2747
|
+
$store.retiredTransportSessionIds = @(
|
|
2748
|
+
@($store.retiredTransportSessionIds) + @($old | ForEach-Object sessionId) |
|
|
2749
|
+
Sort-Object -Unique
|
|
2750
|
+
)
|
|
2751
|
+
$store.transportSessions = @($store.transportSessions | Where-Object {
|
|
2752
|
+
-not (
|
|
2753
|
+
[string]$_.principalId -ceq [string]$inputRecord.principalId -and
|
|
2754
|
+
[string]$_.hostInstallationId -ceq [string]$inputRecord.hostInstallationId
|
|
2755
|
+
)
|
|
2756
|
+
}) + @($inputRecord)
|
|
2757
|
+
Write-Store $path $store $root $sid
|
|
2758
|
+
}
|
|
2759
|
+
Write-Result ([PSCustomObject]@{ record = $inputRecord })
|
|
2760
|
+
}
|
|
2761
|
+
'LoadTransportSession' {
|
|
2762
|
+
Assert-QueryOwner
|
|
2763
|
+
if ($SessionId -notmatch $TransportSessionIdPattern) { Fail 'host_transport_session_id_invalid' }
|
|
2764
|
+
if ($Timestamp -lt 0) { Fail 'host_timestamp_invalid' }
|
|
2765
|
+
$records = @($store.transportSessions | Where-Object sessionId -CEQ $SessionId)
|
|
2766
|
+
if ($records.Count -ne 1) { Fail 'host_transport_session_invalid' }
|
|
2767
|
+
$record = $records[0]
|
|
2768
|
+
if (
|
|
2769
|
+
[string]$record.principalId -cne $PrincipalId -or
|
|
2770
|
+
[string]$record.hostInstallationId -cne $HostInstallationId
|
|
2771
|
+
) { Fail 'host_transport_session_owner_mismatch' }
|
|
2772
|
+
$credential = @($store.transportCredentials | Where-Object credentialId -CEQ $record.credentialId)
|
|
2773
|
+
if (
|
|
2774
|
+
$credential.Count -ne 1 -or [string]$credential[0].state -ne 'active' -or
|
|
2775
|
+
[int64]$credential[0].expiresAt -le $Timestamp
|
|
2776
|
+
) { Fail 'host_transport_credential_inactive' }
|
|
2777
|
+
if ([int64]$record.expiresAt -le $Timestamp) { Fail 'host_transport_session_expired' }
|
|
2778
|
+
Write-Result ([PSCustomObject]@{ record = $record })
|
|
2779
|
+
}
|
|
2780
|
+
'RemoveTransportSession' {
|
|
2781
|
+
Assert-QueryOwner
|
|
2782
|
+
if ($SessionId -notmatch $TransportSessionIdPattern) { Fail 'host_transport_session_id_invalid' }
|
|
2783
|
+
$records = @($store.transportSessions | Where-Object sessionId -CEQ $SessionId)
|
|
2784
|
+
if ($records.Count -gt 0 -and (
|
|
2785
|
+
[string]$records[0].principalId -cne $PrincipalId -or
|
|
2786
|
+
[string]$records[0].hostInstallationId -cne $HostInstallationId
|
|
2787
|
+
)) { Fail 'host_transport_session_owner_mismatch' }
|
|
2788
|
+
$removed = $records.Count -eq 1
|
|
2789
|
+
if ($removed) {
|
|
2790
|
+
$store.transportSessions = @($store.transportSessions | Where-Object sessionId -CNE $SessionId)
|
|
2791
|
+
$store.retiredTransportSessionIds = @(
|
|
2792
|
+
@($store.retiredTransportSessionIds) + @($SessionId) | Sort-Object -Unique
|
|
2793
|
+
)
|
|
2794
|
+
Write-Store $path $store $root $sid
|
|
2795
|
+
}
|
|
2796
|
+
Write-Result ([PSCustomObject]@{ sessionId = $SessionId; removed = $removed })
|
|
2797
|
+
}
|
|
2798
|
+
'ResolveTransport' {
|
|
2799
|
+
if ($Timestamp -lt 0) { Fail 'host_timestamp_invalid' }
|
|
2800
|
+
$credential = Get-ActiveTransportCredential $store $Timestamp
|
|
2801
|
+
$sessions = @($store.transportSessions | Where-Object {
|
|
2802
|
+
[string]$_.credentialId -ceq [string]$credential.credentialId -and
|
|
2803
|
+
[string]$_.principalId -ceq $PrincipalId -and
|
|
2804
|
+
[string]$_.hostInstallationId -ceq $HostInstallationId
|
|
2805
|
+
})
|
|
2806
|
+
if ($sessions.Count -gt 1) { Fail 'host_transport_session_ambiguous' }
|
|
2807
|
+
$session = $null
|
|
2808
|
+
if ($sessions.Count -eq 1) {
|
|
2809
|
+
if ([int64]$sessions[0].expiresAt -le $Timestamp) {
|
|
2810
|
+
$store.transportSessions = @($store.transportSessions | Where-Object sessionId -CNE $sessions[0].sessionId)
|
|
2811
|
+
$store.retiredTransportSessionIds = @(
|
|
2812
|
+
@($store.retiredTransportSessionIds) + @($sessions[0].sessionId) | Sort-Object -Unique
|
|
2813
|
+
)
|
|
2814
|
+
Write-Store $path $store $root $sid
|
|
2815
|
+
} else {
|
|
2816
|
+
$session = $sessions[0]
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
Write-Result ([PSCustomObject]@{ credential = $credential; session = $session })
|
|
2820
|
+
}
|
|
2821
|
+
'VerifyTransport' {
|
|
2822
|
+
if ($Timestamp -lt 0) { Fail 'host_timestamp_invalid' }
|
|
2823
|
+
$credential = Get-ActiveTransportCredential $store $Timestamp
|
|
2824
|
+
$sessions = @($store.transportSessions | Where-Object {
|
|
2825
|
+
[string]$_.credentialId -ceq [string]$credential.credentialId -and
|
|
2826
|
+
[string]$_.principalId -ceq $PrincipalId -and
|
|
2827
|
+
[string]$_.hostInstallationId -ceq $HostInstallationId
|
|
2828
|
+
})
|
|
2829
|
+
if ($sessions.Count -gt 1) { Fail 'host_transport_session_ambiguous' }
|
|
2830
|
+
$session = $null
|
|
2831
|
+
if ($sessions.Count -eq 1) {
|
|
2832
|
+
if ([int64]$sessions[0].expiresAt -gt $Timestamp) {
|
|
2833
|
+
$session = [PSCustomObject]@{
|
|
2834
|
+
sessionId = [string]$sessions[0].sessionId
|
|
2835
|
+
credentialId = [string]$sessions[0].credentialId
|
|
2836
|
+
principalId = [string]$sessions[0].principalId
|
|
2837
|
+
hostInstallationId = [string]$sessions[0].hostInstallationId
|
|
2838
|
+
expiresAt = [int64]$sessions[0].expiresAt
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
Write-Result ([PSCustomObject]@{
|
|
2843
|
+
credential = [PSCustomObject]@{
|
|
2844
|
+
credentialId = [string]$credential.credentialId
|
|
2845
|
+
principalId = [string]$credential.principalId
|
|
2846
|
+
hostInstallationId = [string]$credential.hostInstallationId
|
|
2847
|
+
expiresAt = [int64]$credential.expiresAt
|
|
2848
|
+
}
|
|
2849
|
+
session = $session
|
|
2850
|
+
})
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
$inputRecord = $null
|
|
2854
|
+
$store = $null
|
|
2855
|
+
} finally {
|
|
2856
|
+
if ($acquired) { $mutex.ReleaseMutex() }
|
|
2857
|
+
$mutex.Dispose()
|
|
2858
|
+
}
|
|
2859
|
+
} catch {
|
|
2860
|
+
$candidate = [string]$_.Exception.Message
|
|
2861
|
+
if ($candidate -match '^[a-z][a-z0-9_]{2,127}$') { Fail $candidate }
|
|
2862
|
+
if ($_.Exception -is [System.UnauthorizedAccessException]) { Fail 'host_protected_store_access_failed' }
|
|
2863
|
+
if ($_.Exception -is [System.IO.IOException]) { Fail 'host_protected_store_io_failed' }
|
|
2864
|
+
if ($_.Exception -is [System.Management.Automation.RuntimeException]) { Fail 'host_protected_store_runtime_failed' }
|
|
2865
|
+
Fail 'host_protected_store_operation_failed'
|
|
2866
|
+
}
|