mcp-pacemaker 1.3.1 → 2.0.2

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.
@@ -10,16 +10,16 @@ public sealed class PoolingSecurityHelper
10
10
  private readonly AccessControlSections sections =
11
11
  AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group;
12
12
 
13
- /// <summary>Inspects or preserves Windows file security.</summary>
14
- /// <param name="args">One action: inspect or copy. File paths come from MCP_POOL environment variables.</param>
13
+ /// <summary>Inspects, stages, moves, or preserves Windows file security.</summary>
14
+ /// <param name="args">One action. File paths come from MCP_POOL environment variables.</param>
15
15
  /// <returns>Zero on success, three on a source security conflict, or one on failure.</returns>
16
16
  public static int Main(string[] args)
17
17
  {
18
18
  return new PoolingSecurityHelper().Run(args);
19
19
  }
20
20
 
21
- /// <summary>Runs a security operation without changing file contents.</summary>
22
- /// <param name="args">One action: inspect or copy.</param>
21
+ /// <summary>Runs a native file operation or a legacy security operation.</summary>
22
+ /// <param name="args">One of inspect-access, stage, move-no-replace, inspect, or copy.</param>
23
23
  /// <returns>Zero on success, three on a source security conflict, or one on failure.</returns>
24
24
  public int Run(string[] args)
25
25
  {
@@ -31,6 +31,15 @@ public sealed class PoolingSecurityHelper
31
31
  }
32
32
 
33
33
  var sourcePath = this.RequiredEnvironment("MCP_POOL_SOURCE");
34
+ if (args[0] == "inspect-access" ||
35
+ args[0] == "stage" ||
36
+ args[0] == "move-no-replace")
37
+ {
38
+ var files = new PoolingNativeFiles();
39
+ Console.WriteLine(files.Run(args[0], sourcePath));
40
+ return 0;
41
+ }
42
+
34
43
  if (args[0] == "inspect")
35
44
  {
36
45
  Console.WriteLine(this.ReadSecurity(sourcePath).Fingerprint);
@@ -53,7 +62,11 @@ public sealed class PoolingSecurityHelper
53
62
  Console.Error.WriteLine("MCPERR nativeError=" + nativeError.NativeErrorCode);
54
63
  }
55
64
 
56
- return 1;
65
+ bool conflict = error is PoolingConflictException ||
66
+ (nativeError != null &&
67
+ (nativeError.NativeErrorCode == 80 ||
68
+ nativeError.NativeErrorCode == 183));
69
+ return conflict ? 3 : 1;
57
70
  }
58
71
  }
59
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-pacemaker",
3
- "version": "1.3.1",
3
+ "version": "2.0.2",
4
4
  "description": "Keeps your MCP servers alive across editor restarts, reboots, and token expiry — pluggable auth, cross-platform auto-start, one-command setup. Zero-dependency bridge; a small CLI for setup.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,7 +56,7 @@
56
56
  "compat:clean": "node tools/compatibility/prepare.mjs --clean",
57
57
  "test:compat": "node --test --test-concurrency=1 test/compat/gates.test.mjs test/compat/cli-api.test.mjs",
58
58
  "test:compat:browser": "node --test --test-concurrency=1 test/compat/browser.test.mjs",
59
- "test": "node --test test/bridge.test.mjs test/bridge-controls.test.mjs test/kill-tree.test.mjs test/keepalive.test.mjs test/auth-token.test.mjs test/cold-start.test.mjs test/crash-report.test.mjs test/resume.test.mjs test/config-checks.test.mjs test/config-reload.test.mjs test/health.test.mjs test/cli.test.mjs test/logs.test.mjs test/prewarm.test.mjs test/pooling-config.test.mjs test/pooling-writer.test.mjs test/pooling-api.test.mjs test/pooling-deadlines.test.mjs test/pooling-queue-budget.test.mjs test/windows-security-helper.test.mjs test/tui.test.mjs test/warm-pool.test.mjs test/server-metrics.test.mjs test/admission.test.mjs test/shared-sessions.test.mjs test/shared-http.test.mjs test/cross-host.test.mjs test/suite-integrity.test.mjs test/consumer-package.test.mjs test/notices.test.mjs test/notices-runtime.test.mjs test/npm-publication.test.mjs test/npm-publication-proof.test.mjs test/update-check.test.mjs test/service-lifecycle.test.mjs test/service-supervisor.test.mjs test/npm-publication-gates.test.mjs test/publication-scanners-advisories.test.mjs test/publication-scanners.test.mjs test/service-replacement.test.mjs test/consumer-environment.test.mjs test/npm-publication-external.test.mjs test/npm-publication-matrix.test.mjs test/npm-publication-peer.test.mjs test/publication-scanners-diagnostics.test.mjs test/publication-scanners-publication.test.mjs test/publication-scanners-paths.test.mjs test/npm-publication-local-enforcement.test.mjs test/publication-scanners-admission.test.mjs",
59
+ "test": "node --test test/bridge.test.mjs test/bridge-controls.test.mjs test/kill-tree.test.mjs test/keepalive.test.mjs test/auth-token.test.mjs test/cold-start.test.mjs test/crash-report.test.mjs test/resume.test.mjs test/config-checks.test.mjs test/config-reload.test.mjs test/health.test.mjs test/cli.test.mjs test/logs.test.mjs test/prewarm.test.mjs test/pooling-batch-scheduler.test.mjs test/pooling-batch-api.test.mjs test/pooling-batch-store.test.mjs test/pooling-batches.test.mjs test/pooling-stage-publication.test.mjs test/pooling-batch-startup.test.mjs test/pooling-postcommit-recovery.test.mjs test/pooling-config.test.mjs test/pooling-writer.test.mjs test/pooling-api.test.mjs test/pooling-deadlines.test.mjs test/pooling-queue-budget.test.mjs test/windows-security-helper.test.mjs test/windows-batch-security.test.mjs test/tui.test.mjs test/warm-pool.test.mjs test/server-metrics.test.mjs test/admission.test.mjs test/shared-sessions.test.mjs test/shared-http.test.mjs test/cross-host.test.mjs test/suite-integrity.test.mjs test/consumer-package.test.mjs test/notices.test.mjs test/notices-runtime.test.mjs test/npm-publication.test.mjs test/npm-publication-proof.test.mjs test/update-check.test.mjs test/service-lifecycle.test.mjs test/service-supervisor.test.mjs test/npm-publication-gates.test.mjs test/npm-publication-matrix.test.mjs test/publication-scanners-advisories.test.mjs test/publication-scanners.test.mjs test/service-replacement.test.mjs test/consumer-environment.test.mjs test/npm-publication-external.test.mjs test/npm-publication-peer.test.mjs test/publication-scanners-diagnostics.test.mjs test/publication-scanners-publication.test.mjs test/publication-scanners-paths.test.mjs test/npm-publication-local-enforcement.test.mjs test/publication-scanners-admission.test.mjs",
60
60
  "notices:check": "node tools/third-party-notices/check.mjs",
61
61
  "publication:source-gates": "node tools/npm-publication/source-gates.mjs",
62
62
  "publication:artifact-gates": "node tools/npm-publication/artifact-gates.mjs",
@@ -16,7 +16,113 @@ AnyCPU uses the installed Framework's process architecture; native calls use
16
16
  pointer-sized handles. Windows ARM64 execution has not been validated.
17
17
 
18
18
  The Node caller must launch the package-relative executable directly (no shell)
19
- with exactly one argument, `inspect` or `copy`, and supply:
19
+ with exactly one action argument. Paths are passed in the environment, never in
20
+ arguments. No action enables privileges.
21
+
22
+ ### Batch file operations
23
+
24
+ | Action | Environment | UTF-8 stdin |
25
+ | --- | --- | --- |
26
+ | `inspect-access` | `MCP_POOL_SOURCE` | Unused |
27
+ | `stage` | `MCP_POOL_SOURCE`, `MCP_POOL_TEMP` (new destination) | `{"expected":{"identity":"…","revision":"…","security":"…"},"bytes":"<base64>"}` |
28
+ | `move-no-replace` | `MCP_POOL_SOURCE`, `MCP_POOL_TEMP` (destination) | `{"expected":{"identity":"…","revision":"…","security":"…"}}` |
29
+
30
+ Each successful action writes one JSON line with exactly
31
+ `{identity, revision, security, size}`. Pass only the first three fields back in
32
+ `expected`, not the complete result. `identity` is lowercase hexadecimal
33
+ `volumeSerial:fileIndexHighAndLow` (8 digits, colon, 16 digits). `revision` is
34
+ lowercase SHA256 of the complete bytes; `size` is their integer byte count.
35
+ `security` is a lowercase SHA256 comparison value, not a descriptor to log.
36
+ Custom config paths, including spaces and Unicode, use the same environment API.
37
+
38
+ All three actions require a disk file, no final-component reparse point, exactly
39
+ one hard link, and at most 1 MiB. Only Normal/Archive file attributes are supported;
40
+ read-only, compressed, encrypted, sparse, hidden, and other attributes fail closed.
41
+ The input envelope is limited to 1,400,000 bytes, strict UTF-8 and depth 8.
42
+ Edited bytes must be canonical base64 decoding to at most 1 MiB of UTF-8.
43
+ The helper does not parse the edited configuration; complete JSON validation and
44
+ allowed field edits remain the Node coordinator's responsibility.
45
+
46
+ Native `GetSecurityInfo` queries the held handle with `READ_CONTROL` sections
47
+ `0x1f7`. The new actions never call the legacy privileged audit reader.
48
+ The security hash frames `mcp-access-v1`, native owner SID, group SID,
49
+ DACL-protection bit, raw DACL, native label ACL, and attributes with `BinaryWriter`.
50
+ Byte arrays have signed Int32 lengths (`-1` for null). The discriminator is a
51
+ BinaryWriter UTF-8 string; protection and attributes are UInt32, all little-endian.
52
+ The DACL's `SE_DACL_AUTO_INHERITED` (`0x400`) bookkeeping is not hashed: Windows can
53
+ clear it while retaining the exact grants. Defaulted/auto-inherit-request and
54
+ other unsupported descriptor control state is refused. Audit control flags are
55
+ outside this non-audit fingerprint; label ACE bytes and flags remain exact.
56
+
57
+ Supported DACLs have non-null raw ACLs containing ordinary non-callback allow/deny
58
+ ACEs. Unsupported object/callback/policy ACEs fail closed. No label, or one
59
+ low/medium label with a nonzero mask composed only of NW/NR/NX (`1` through `7`),
60
+ is supported. Only the inherited label ACE flag is allowed. Other native non-audit
61
+ policy is refused.
62
+
63
+ `stage` opens the source with actual `FILE_READ_DATA | FILE_WRITE_DATA |
64
+ READ_CONTROL` access. Parent DeleteChild authority does not bypass source write
65
+ denial. The held source handle denies other data writers/deleters during staging.
66
+ Expected identity, bytes, and security are checked before creation and again
67
+ around writing. When the source owner equals the caller's user SID or current
68
+ token default owner, that owner is retained. Retaining an already assignable
69
+ default owner avoids an unnecessary ownership change across a version roundtrip.
70
+ Otherwise the caller must already obtain source `READ_CONTROL | WRITE_DAC` through
71
+ `ReOpenFile` on the same object; only simple allow DACLs without OWNER RIGHTS or
72
+ CREATOR special SIDs qualify. The candidate owner then becomes the creator.
73
+ The source group must remain exact or creation/verification fails.
74
+
75
+ The candidate uses native `CREATE_NEW` with `SECURITY_ATTRIBUTES` containing the
76
+ accepted owner/group/DACL/protection and supported labels **before any bytes**.
77
+ It never clones audit entries or sets SACL protection. A label-bearing creation
78
+ descriptor marks the SACL defaulted (`SE_SACL_DEFAULTED`); this lets Windows inherit
79
+ folder auditing while retaining the supplied mandatory label. The label remains
80
+ part of the exact empty-file verification. Windows inherits the
81
+ destination folder's auditing, including when the source protects its audit ACL.
82
+ File-specific auditing is not detected or promised to carry forward. The UI must
83
+ provide the agreed nonblocking audit-inheritance warning without claiming
84
+ custom rules were detected.
85
+
86
+ The empty candidate's security must equal the accepted fingerprint before
87
+ `WriteFile`. Bytes are written through that owned handle, flushed using
88
+ `FlushFileBuffers`, and then hashed and security-checked again. Security mismatch
89
+ before writing can leave an empty, securely created candidate. A later failure
90
+ can leave a partially/fully written candidate; callers must validate ownership
91
+ before cleanup, never blindly delete a destination on any failure.
92
+
93
+ `move-no-replace` holds a source handle with share-read/share-delete, denies data
94
+ writers, and checks identity/content/security before `MoveFileExW(..., 0)`.
95
+ It checks the destination and held object afterward and returns the moved
96
+ descriptor. Existing destinations are never overwritten. There is no exists-check
97
+ followed by an overwriting rename.
98
+
99
+ **Limits:** the rename API is path-based. Another actor with delete/rename or
100
+ directory/security authority can still swap the source path/ancestors between
101
+ validation and the move, or change security during an operation. Postchecks
102
+ detect some races but cannot undo them. This is not CAS, an atomic two-file
103
+ transaction, a power-loss guarantee, or a complete metadata clone. A failed move
104
+ postcheck can mean the move already happened; the coordinator must inspect
105
+ transaction state before recovery. Deadline/commit gating, rollback/recovery,
106
+ watcher coordination, and the missing-active interval belong to Node.
107
+
108
+ | Exit | New-action meaning |
109
+ | --- | --- |
110
+ | `0` | One descriptor JSON line; empty stderr |
111
+ | `3` | `PoolingConflictException` for an expected/state mismatch, or `Win32Exception` with native `80`/`183` for an existing destination |
112
+ | `1` | Policy, request, I/O, access, or other failure |
113
+
114
+ Failures have empty stdout. Stderr is exactly `MCPERR type=<exception type>` and,
115
+ for a `Win32Exception`, a second line `MCPERR nativeError=<integer>`. Relevant
116
+ types are `PoolingPolicyException` (unsupported or mismatched creation security),
117
+ `PoolingConflictException`, `Win32Exception`, `ArgumentException`,
118
+ `FormatException`, `DecoderFallbackException`, and `IOException`. Native `5` is
119
+ access denied; `32` is a sharing violation; `2`/`3` are missing path components.
120
+ No exception messages, paths, token information, config bytes, or descriptors are
121
+ printed. Legacy stale-security conflicts retain their empty-stderr behavior.
122
+
123
+ ### Legacy compatibility
124
+
125
+ `inspect` and `copy` remain available with their existing contract:
20
126
 
21
127
  | Environment variable | Used by | Value |
22
128
  | --- | --- | --- |
@@ -71,8 +177,12 @@ From the repository root:
71
177
  .\tools\windows-security-helper\build.ps1 -Verify -CompilerPath 'C:\path\to\Roslyn\csc.exe'
72
178
  ```
73
179
 
74
- The build uses `/noconfig /nostdlib+` with explicit `mscorlib.dll` and `System.dll`
75
- references, fixed options, LF-normalized UTF-8 source, and mapped source paths.
180
+ The build uses `/noconfig /nostdlib+` with explicit `mscorlib.dll`, `System.dll`,
181
+ `System.Core.dll`, and `System.Web.Extensions.dll` references. JSON serialization
182
+ uses .NET Framework's built-in `JavaScriptSerializer`, not an npm dependency.
183
+ Sources are `AssemblyInfo.cs`, `PoolingSecurityReader.cs`,
184
+ `PoolingSecurityHelper.cs`, and `PoolingNativeFiles.cs`, with fixed options,
185
+ LF-normalized UTF-8 source, and mapped source paths.
76
186
  It compiles twice under different temporary paths and refuses unequal bytes.
77
187
  Temporary build files are removed by explicit path. No `dotnet build`, runtime
78
188
  Framework compiler, NuGet restore, or external binary is involved.
@@ -93,3 +203,36 @@ Audit-denial/delay test variants belong in disposable directories outside `bin/`
93
203
  and must never be included in an npm tarball. Release validation must also run
94
204
  the real package on supported Windows/Node versions and verify packaged file
95
205
  inclusion. This build alone does not establish those platform results.
206
+
207
+ ## Targeted tests
208
+
209
+ ```powershell
210
+ npm exec --no --offline --package=node@20.20.2 -- node --test test/windows-security-helper.test.mjs test/windows-batch-security.test.mjs
211
+ npm exec --no --offline --package=node@22.23.2 -- node --test test/windows-security-helper.test.mjs test/windows-batch-security.test.mjs
212
+ ```
213
+
214
+ The real-helper tests cover descriptor stability, full-byte staging, exact 1 MiB
215
+ bounds, stale identity/revision/security, malformed/bounded input, held writers,
216
+ hard links, and both collision points. There are no successful-path mocks.
217
+
218
+ The isolated `TEST-SETUP` audit fixture requires an audit-reading oracle token
219
+ and an existing Explorer window. Otherwise that one test explicitly skips.
220
+ Explorer discovery first checks for a process in the current Windows session,
221
+ before COM access or fixture policy changes. An absent process/window produces
222
+ a structured capability reason; discovery exceptions and later worker failures
223
+ remain failures. A skip does not establish audit-oracle coverage: that requires
224
+ a non-skipped result on a machine with both prerequisites.
225
+ It launches the **same Node executable** through Explorer, verifies medium
226
+ integrity and absence of SeSecurityPrivilege, then runs the packaged helper
227
+ directly. The elevated oracle separately verifies raw DACL/group/labels,
228
+ owner rules, unchanged failed sources, and the retained original's complete
229
+ audit descriptor. The mixed audit case retains one explicit plus one inherited
230
+ source rule on `previous`; its new candidate has only the parent's inherited
231
+ rule. A protected audit source also permits folder inheritance on the candidate.
232
+ An incompatible inherited label must leave the candidate empty.
233
+
234
+ `test/fixtures/windows-batch-security/FixturePolicy.cs` is compiled only by
235
+ TEST-SETUP to establish synthetic raw DACLs and labels; it is not linked into,
236
+ loaded by, or shipped as a dependency of the production helper. All fixture
237
+ policy changes are confined to a fresh owned temporary directory, removed by
238
+ the test. No live files, tasks, bridge, or token privileges are changed.
@@ -70,8 +70,8 @@ $sourceDirectory = Join-Path $binaryDirectory 'src'
70
70
  $binaryName = 'PoolingSecurityHelper.exe'
71
71
  $binaryPath = Join-Path $binaryDirectory $binaryName
72
72
  $metadataPath = Join-Path $binaryDirectory 'PoolingSecurityHelper.build.json'
73
- $sourceNames = @('AssemblyInfo.cs', 'PoolingSecurityReader.cs', 'PoolingSecurityHelper.cs')
74
- $referenceNames = @('mscorlib.dll', 'System.dll')
73
+ $sourceNames = @('AssemblyInfo.cs', 'PoolingSecurityReader.cs', 'PoolingSecurityHelper.cs', 'PoolingNativeFiles.cs')
74
+ $referenceNames = @('mscorlib.dll', 'System.dll', 'System.Core.dll', 'System.Web.Extensions.dll')
75
75
 
76
76
  if (-not $CompilerPath) {
77
77
  $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!visible{visibility:visible!important}.visible{visibility:visible}.sticky{position:sticky}.top-0{top:0}.z-10{z-index:10}.col-span-full{grid-column:1 / -1}.m-0{margin:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.flex{display:flex}.table{display:table}.grid{display:grid}.h-2\.5{height:.625rem}.max-h-52{max-height:13rem}.min-h-screen{min-height:100vh}.w-2\.5{width:.625rem}.w-40{width:10rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[260px\]{min-width:260px}.max-w-\[1160px\]{max-width:1160px}.max-w-\[1400px\]{max-width:1400px}.max-w-\[900px\]{max-width:900px}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.grid-cols-\[14px_160px_1fr\]{grid-template-columns:14px 160px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-\[18px\]{gap:18px}.overflow-auto{overflow:auto}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-accent{--tw-border-opacity: 1;border-color:rgb(110 168 254 / var(--tw-border-opacity, 1))}.border-amber{--tw-border-opacity: 1;border-color:rgb(240 180 41 / var(--tw-border-opacity, 1))}.border-amber\/50{border-color:#f0b42980}.border-danger{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity, 1))}.border-danger\/50{border-color:#f8717180}.border-line{--tw-border-opacity: 1;border-color:rgb(42 47 58 / var(--tw-border-opacity, 1))}.border-ok{--tw-border-opacity: 1;border-color:rgb(52 211 153 / var(--tw-border-opacity, 1))}.bg-accent\/10{background-color:#6ea8fe1a}.bg-amber{--tw-bg-opacity: 1;background-color:rgb(240 180 41 / var(--tw-bg-opacity, 1))}.bg-amber\/5{background-color:#f0b4290d}.bg-danger{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-danger\/5{background-color:#f871710d}.bg-ok{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-panel{--tw-bg-opacity: 1;background-color:rgb(23 26 35 / var(--tw-bg-opacity, 1))}.bg-panel2{--tw-bg-opacity: 1;background-color:rgb(30 34 45 / var(--tw-bg-opacity, 1))}.p-16{padding:4rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.pb-3{padding-bottom:.75rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.align-top{vertical-align:top}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.leading-relaxed{line-height:1.625}.text-accent{--tw-text-opacity: 1;color:rgb(110 168 254 / var(--tw-text-opacity, 1))}.text-amber{--tw-text-opacity: 1;color:rgb(240 180 41 / var(--tw-text-opacity, 1))}.text-danger{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-fg{--tw-text-opacity: 1;color:rgb(230 232 238 / var(--tw-text-opacity, 1))}.text-muted{--tw-text-opacity: 1;color:rgb(154 163 178 / var(--tw-text-opacity, 1))}.text-muted\/40{color:#9aa3b266}.text-muted\/60{color:#9aa3b299}.text-ok{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.shadow-\[0_0_6px\]{--tw-shadow: 0 0 6px;--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-ok{--tw-shadow-color: #34d399;--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\[grid-template-columns\:repeat\(auto-fit\,minmax\(320px\,1fr\)\)\]{grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}:root{color-scheme:dark}body{margin:0;background:#0f1117;color:#e6e8ee;font:14px/1.5 ui-sans-serif,system-ui,Segoe UI,Roboto,sans-serif}.hover\:border-accent:hover{--tw-border-opacity: 1;border-color:rgb(110 168 254 / var(--tw-border-opacity, 1))}.hover\:bg-accent\/20:hover{background-color:#6ea8fe33}.hover\:text-fg:hover{--tw-text-opacity: 1;color:rgb(230 232 238 / var(--tw-text-opacity, 1))}.focus\:border-accent:focus{--tw-border-opacity: 1;border-color:rgb(110 168 254 / var(--tw-border-opacity, 1))}.disabled\:cursor-default:disabled{cursor:default}.disabled\:opacity-40:disabled{opacity:.4}