commit 4bc355216704f7dbd7313f4640646a479f4809b4 Author: Youmu Liu <105042599+Nitload-NSI@users.noreply.github.com> Date: Tue Sep 29 11:30:51 2026 +0800 Initialize cross-platform workspace host scaffold diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8a32d18 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{json,yml,yaml,props,csproj,slnx}] +indent_size = 2 + +[*.cs] +csharp_style_namespace_declarations = file_scoped:warning +dotnet_sort_system_directives_first = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6723dc2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto +*.cs text eol=lf +*.md text eol=lf +*.json text eol=lf +*.sh text eol=lf +*.ps1 text eol=lf +*.yml text eol=lf diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..13c434f --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,40 @@ +name: Verify + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + runtime: win-x64 + - os: ubuntu-latest + runtime: linux-x64 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - name: Restore build dependencies + run: dotnet restore build/Build.csproj --locked-mode + - name: Verify and publish + run: dotnet run --project build/Build.csproj --configuration Release --no-restore -- --target Publish --runtime ${{ matrix.runtime }} --locked-restore + - uses: actions/upload-artifact@v4 + with: + name: workspace-access-${{ matrix.runtime }} + path: artifacts/publish/${{ matrix.runtime }}/ + - uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-${{ matrix.runtime }} + path: artifacts/test-results/ + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d22d6d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +**/bin/ +**/obj/ +artifacts/ +.vs/ +.idea/ +.nuke/temp/ +*.user +*.suo +appsettings.Local.json +.env diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..95d81e8 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,126 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "CheckDocs", + "Compile", + "Publish", + "Restore", + "RunAgent", + "RunHost", + "Test", + "Verify" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "Build configuration (Debug or Release)" + }, + "LockedRestore": { + "type": "boolean", + "description": "Require dependency versions from packages.lock.json" + }, + "Runtime": { + "type": "string", + "description": "Publish runtime: win-x64, win-arm64, linux-x64 or linux-arm64" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..774cb26 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Configuration": "Release" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..38fcd20 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# 仓库开发约定 + +## 入口 + +阅读 `docs/overview.md`、`docs/architecture.md` 和涉及的 `docs/platforms/` 文档。当前实现状态以 `docs/implementation-status.md` 为准。 + +## 实现 + +- 使用 .NET 10、nullable 和集中包版本管理。 +- 通过 `build.ps1` 或 `build.sh` 调用 NUKE;交付前执行 `Verify`。 +- 产品项目为 `src/WorkspaceAccessHost.csproj`,代码直接在 `src/` 下通过目录和命名空间区分职责。 +- 领域逻辑保持操作系统与传输框架无关;平台调用分别放入 `Platforms/Windows`、`Platforms/Linux`。 +- 同一程序通过 `host`、`agent` 参数选择进程角色,按目标平台发布一套产品产物。 +- 连接接管、断线及输入派发遵守 host 独占和工作区归属规则。 +- 认证、授权、用户映射完成后才可调用连接控制核心。 +- 增加真实 PTY、IPC、OIDC 实现时,同步维护 readiness 和状态文档。 + +## 文档 + +文档库是面向所有仓库访问者的当前设计说明。使用中文正文和准确的 API 名称;描述目标行为、运行前提、实现状态和验收条件。保持总功能表中的每项平台差异与一份独立说明文档对应。新增页面加入导航,并通过 `CheckDocs`。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..80ba18d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# 贡献指南 + +首先阅读[总功能描述](docs/overview.md)、[架构](docs/architecture.md)与[开发流程](docs/development.md)。通过 `Verify` 后提交变更。 + +## 变更要求 + +- 产品代码直接位于 `src/`,领域逻辑按职责放入目录,原生调用位于 `Platforms/Windows` 或 `Platforms/Linux`。 +- 修改平台行为时,同步更新总功能表和对应差异说明页。 +- 文档面向仓库访问者,描述当前设计、运行条件、行为和验收标准。 +- 新增身份、连接、生命周期行为时,为权限边界和并发结果增加测试。 +- 密钥通过部署环境配置管理;示例使用占位值。 +- 包版本集中维护在 `Directory.Packages.props`,依赖升级后更新锁文件并执行 `Verify`。 + +提交说明包含具体行为变化、验证结果及尚待平台环境验证的内容。 diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..c1e01f2 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + true + latest + true + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..7f132d6 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,15 @@ + + + true + + + + + + + + + + + + diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..765346e --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..0aa5376 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# Workspace Access Host + +面向 Windows 和 Linux 的持久终端工作区服务,使用 C# / .NET 10 开发,使用 NUKE 统一构建、测试、文档校验和发布。 + +产品设计:通过 authentik OIDC 认证后,经 WebSocket 连接宿主上的用户终端。每台宿主拥有一个有效远程控制连接,终端进程在连接断开期间持续运行。 + +## 当前交付 + +当前处于框架阶段。已提供服务与用户代理入口、平台契约、单连接控制核心及测试、部署模板和文档库。OIDC、WebSocket、代理 IPC、ConPTY / POSIX PTY 和画面恢复处于待实现阶段。服务启动后,存活检查返回 200,就绪检查返回 503 并列出集成项。 + +## 快速开始 + +安装 [global.json](global.json) 指定的 .NET SDK,在仓库根目录执行: + +```powershell +# Windows PowerShell +./build.ps1 --target Verify +./build.ps1 --target RunHost +``` + +```bash +# Linux +bash ./build.sh --target Verify +bash ./build.sh --target RunHost +``` + +宿主默认监听 `http://127.0.0.1:5080`,健康检查为 `/health/live` 与 `/health/ready`。用户代理入口为 `RunAgent`。使用 Ctrl+C 结束前台开发进程。 + +发布目标平台的独立运行产物: + +```powershell +./build.ps1 --target Publish --runtime win-x64 +./build.ps1 --target Publish --runtime linux-x64 +``` + +产物位于 `artifacts/publish//`,包含 `WorkspaceAccessHost` 程序、运行时依赖、配置和 `deploy/` 部署模板。同一程序通过参数选择角色: + +```text +WorkspaceAccessHost host +WorkspaceAccessHost agent +``` + +Windows 使用 `WorkspaceAccessHost.exe`。两个角色在各自的进程和系统用户上下文中运行。 + +## 阅读入口 + +- [文档库](docs/README.md) +- [总功能与平台差异表](docs/overview.md) +- [架构与项目职责](docs/architecture.md) +- [开发与 NUKE 构建](docs/development.md) +- [交付状态与后续实现](docs/implementation-status.md) +- [贡献指南](CONTRIBUTING.md) +- [仓库开发约定](AGENTS.md) + +## 目录 + +```text +src/ 单一产品项目 + WorkspaceAccessHost.csproj 产品项目文件 + Program.cs 角色选择入口 + Core/ 共享模型与契约 + Authentication/ 外部身份与认证 + Connections/ 连接归属与控制权 + Terminals/ PTY 契约与终端逻辑 + Platforms/Windows/ Windows 适配 + Platforms/Linux/ Linux 适配 + Hosting/ 服务宿主角色 + Agent/ 用户代理角色 +tests/WorkspaceAccessHost.Tests/ 测试项目 +build/ NUKE 构建项目 +docs/ 产品、架构、平台差异及开发文档 +deploy/ Windows Service 与 systemd 部署模板 +``` diff --git a/WorkspaceAccessHost.slnx b/WorkspaceAccessHost.slnx new file mode 100644 index 0000000..d5e77ee --- /dev/null +++ b/WorkspaceAccessHost.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..7f67acd --- /dev/null +++ b/build.ps1 @@ -0,0 +1,11 @@ +$ErrorActionPreference = 'Stop' +$project = Join-Path $PSScriptRoot 'build/Build.csproj' +Push-Location $PSScriptRoot +try { + & dotnet run --project $project --configuration Release --disable-build-servers -- @args + $buildExitCode = $LASTEXITCODE +} +finally { + Pop-Location +} +exit $buildExitCode diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..be9df5a --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +cd "$repo_root" +exec dotnet run --project "$repo_root/build/Build.csproj" --configuration Release --disable-build-servers -- "$@" diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 0000000..056ef15 --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,115 @@ +using Nuke.Common; +using Nuke.Common.IO; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using Serilog; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +namespace WorkspaceAccess.Build; + +internal sealed class Build : NukeBuild +{ + public static int Main() + { + Environment.SetEnvironmentVariable("NUKE_TELEMETRY_OPTOUT", "1"); + return Execute(build => build.Verify); + } + + [Parameter("Build configuration (Debug or Release).")] + readonly string Configuration = "Release"; + + [Parameter("Publish runtime: win-x64, win-arm64, linux-x64 or linux-arm64.")] + readonly string? Runtime; + + [Parameter("Require dependency versions from packages.lock.json.")] + readonly bool LockedRestore; + + AbsolutePath SolutionFile => RootDirectory / "WorkspaceAccessHost.slnx"; + AbsolutePath ProductProject => RootDirectory / "src/WorkspaceAccessHost.csproj"; + AbsolutePath TestProject => RootDirectory / "tests/WorkspaceAccessHost.Tests/WorkspaceAccessHost.Tests.csproj"; + AbsolutePath Artifacts => RootDirectory / "artifacts"; + + Target Restore => _ => _ + .Executes(() => DotNetRestore(settings => settings + .SetProjectFile(SolutionFile) + .SetLockedMode(LockedRestore))); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => DotNetBuild(settings => settings + .SetProjectFile(SolutionFile) + .SetConfiguration(Configuration) + .SetProcessAdditionalArguments("--disable-build-servers") + .EnableNoRestore())); + + Target Test => _ => _ + .DependsOn(Compile) + .Executes(() => DotNetTest(settings => settings + .SetProjectFile(TestProject) + .SetConfiguration(Configuration) + .EnableNoBuild() + .EnableNoRestore() + .SetResultsDirectory(Artifacts / "test-results") + .SetLoggers("trx;LogFileName=workspace-access.trx"))); + + Target CheckDocs => _ => _ + .Executes(() => DocumentationChecks.Validate(RootDirectory)); + + Target Verify => _ => _ + .DependsOn(CheckDocs, Test); + + Target Publish => _ => _ + .DependsOn(Verify) + .Requires(() => Runtime) + .Executes(() => + { + string[] supported = ["win-x64", "win-arm64", "linux-x64", "linux-arm64"]; + if (!supported.Contains(Runtime, StringComparer.Ordinal)) + { + throw new ArgumentException($"Runtime must be one of: {string.Join(", ", supported)}."); + } + + var destination = Artifacts / "publish" / Runtime!; + DotNetPublish(settings => settings + .SetProject(ProductProject) + .SetConfiguration(Configuration) + .SetRuntime(Runtime) + .SetProcessAdditionalArguments("--disable-build-servers") + .SetSelfContained(true) + .SetProperty("RestoreLockedMode", LockedRestore) + .SetOutput(destination)); + + var deployment = Runtime!.StartsWith("win-", StringComparison.Ordinal) ? "windows" : "linux"; + CopyDirectory(RootDirectory / "deploy" / deployment, destination / "deploy"); + Log.Information("Published WorkspaceAccessHost to {Directory}", destination); + }); + + Target RunHost => _ => _ + .DependsOn(Compile) + .Executes(() => DotNetRun(settings => settings + .SetProjectFile(ProductProject) + .SetConfiguration(Configuration) + .SetApplicationArguments("host") + .EnableNoBuild() + .EnableNoRestore())); + + Target RunAgent => _ => _ + .DependsOn(Compile) + .Executes(() => DotNetRun(settings => settings + .SetProjectFile(ProductProject) + .SetConfiguration(Configuration) + .SetApplicationArguments("agent") + .EnableNoBuild() + .EnableNoRestore())); + + private static void CopyDirectory(string source, string destination) + { + Directory.CreateDirectory(destination); + foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)) + { + var target = Path.Combine(destination, Path.GetRelativePath(source, file)); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + File.Copy(file, target, overwrite: true); + } + } +} diff --git a/build/Build.csproj b/build/Build.csproj new file mode 100644 index 0000000..5429b8a --- /dev/null +++ b/build/Build.csproj @@ -0,0 +1,13 @@ + + + Exe + false + WorkspaceAccess.Build + $(NoWarn);CS0649 + + + + + + + diff --git a/build/DocumentationChecks.cs b/build/DocumentationChecks.cs new file mode 100644 index 0000000..abfa373 --- /dev/null +++ b/build/DocumentationChecks.cs @@ -0,0 +1,82 @@ +using System.Text.RegularExpressions; +using Serilog; + +namespace WorkspaceAccess.Build; + +internal static partial class DocumentationChecks +{ + public static void Validate(string root) + { + var documents = Directory.EnumerateFiles(Path.Combine(root, "docs"), "*.md", SearchOption.AllDirectories) + .Concat(Directory.EnumerateFiles(root, "*.md", SearchOption.TopDirectoryOnly)) + .Concat(Directory.EnumerateFiles(Path.Combine(root, "deploy"), "*.md", SearchOption.AllDirectories)) + .ToArray(); + var links = new Dictionary>(StringComparer.Ordinal); + foreach (var document in documents) + { + var content = File.ReadAllText(document); + if (!content.StartsWith("# ", StringComparison.Ordinal)) + { + throw new InvalidOperationException($"Document needs a title: {document}"); + } + + var targets = new List(); + foreach (Match match in MarkdownLink().Matches(content)) + { + var link = match.Groups[1].Value; + if (link.StartsWith('#') || Uri.TryCreate(link, UriKind.Absolute, out _)) + { + continue; + } + + var target = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(document)!, + Uri.UnescapeDataString(link.Split('#')[0]))); + if (!File.Exists(target) && !Directory.Exists(target)) + { + throw new InvalidOperationException($"Broken link in {document}: {link}"); + } + + targets.Add(target); + } + + links[Path.GetFullPath(document)] = targets; + } + + var visited = new HashSet(StringComparer.Ordinal); + var pending = new Queue(); + pending.Enqueue(Path.Combine(root, "README.md")); + while (pending.TryDequeue(out var current)) + { + if (visited.Add(current) && links.TryGetValue(current, out var targets)) + { + foreach (var target in targets) + { + pending.Enqueue(target); + } + } + } + + foreach (var document in Directory.EnumerateFiles(Path.Combine(root, "docs"), "*.md", SearchOption.AllDirectories)) + { + if (!visited.Contains(Path.GetFullPath(document))) + { + throw new InvalidOperationException($"Document must be reachable from README: {document}"); + } + } + + var overview = File.ReadAllText(Path.Combine(root, "docs", "overview.md")); + foreach (var document in Directory.EnumerateFiles(Path.Combine(root, "docs", "platforms"), "*.md")) + { + var expectedLink = $"platforms/{Path.GetFileName(document)}"; + if (!overview.Contains($"]({expectedLink})", StringComparison.Ordinal)) + { + throw new InvalidOperationException($"Platform difference needs an overview link: {expectedLink}"); + } + } + + Log.Information("Validated {Count} documents, local link targets, reachability and platform coverage.", documents.Length); + } + + [GeneratedRegex(@"\[[^\]]+\]\(([^)\s]+)\)")] + private static partial Regex MarkdownLink(); +} diff --git a/build/packages.lock.json b/build/packages.lock.json new file mode 100644 index 0000000..c242bef --- /dev/null +++ b/build/packages.lock.json @@ -0,0 +1,514 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "NuGet.Packaging": { + "type": "Direct", + "requested": "[6.12.5, )", + "resolved": "6.12.5", + "contentHash": "jQ5SCP29VgDVxxyFS/Tkg3lIngPeKoVxHdY7xfZNOyIPousbck6SnnqAEt2kZGfvjxFiSePcsKYu99CI/nE/JQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "NuGet.Configuration": "6.12.5", + "NuGet.Versioning": "6.12.5", + "System.Security.Cryptography.Pkcs": "6.0.4" + } + }, + "Nuke.Common": { + "type": "Direct", + "requested": "[10.1.0, )", + "resolved": "10.1.0", + "contentHash": "bymYGBes5vZUEu4eMkp1zDAbXcEpJQgRo6UcIGIWqsCUpSZIwfozuyRKVpk6lWP5KX6ep8x/XC08KLXlC6D6yg==", + "dependencies": { + "Azure.Identity": "1.17.1", + "Azure.Security.KeyVault.Certificates": "4.8.0", + "Azure.Security.KeyVault.Keys": "4.8.0", + "Azure.Security.KeyVault.Secrets": "4.8.0", + "Nuke.Build": "10.1.0", + "Nuke.Build.Shared": "10.1.0", + "Nuke.ProjectModel": "10.1.0", + "Nuke.SolutionModel": "10.1.0", + "Nuke.Tooling": "10.1.0", + "Nuke.Utilities": "10.1.0", + "Nuke.Utilities.IO.Compression": "10.1.0", + "Nuke.Utilities.IO.Globbing": "10.1.0", + "Nuke.Utilities.Net": "10.1.0", + "Nuke.Utilities.Text.Json": "10.1.0", + "Octokit": "14.0.0" + } + }, + "System.Security.Cryptography.Xml": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "T3snzcc04R1JDj2XmzrLyk5fHT0YroBz5o2P+YdyJKYePSMqp8JsrxEJQPvUnK6oozQBvKLki9vnI7uMuWepKQ==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "10.0.12" + } + }, + "Azure.Core": { + "type": "Transitive", + "resolved": "1.50.0", + "contentHash": "GBNKZEhdIbTXxedvD3R7I/yDVFX9jJJEz02kCziFSJxspSQ5RMHc3GktulJ1s7+ffXaXD7kMgrtdQTaggyInLw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "8.0.0", + "System.ClientModel": "1.8.0", + "System.Memory.Data": "8.0.1" + } + }, + "Azure.Identity": { + "type": "Transitive", + "resolved": "1.17.1", + "contentHash": "MSZkBrctcpiGxs9Cvr2VKKoN6qFLZlP3I6xuCWJ9iTgitI5Rgxtk5gfOSpXPZE3+CJmZ/mnqpQyGyjawFn5Vvg==", + "dependencies": { + "Azure.Core": "1.50.0", + "Microsoft.Identity.Client": "4.78.0", + "Microsoft.Identity.Client.Extensions.Msal": "4.78.0" + } + }, + "Azure.Security.KeyVault.Certificates": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "48EHvsZM0hQow0PP4PAkWZYcE/tmjfx7az07GrqU2Q/0p3O3Vm/F+bEu1iVREBYzSV7uHHPikyJyBnXClfKuJA==", + "dependencies": { + "Azure.Core": "1.46.2" + } + }, + "Azure.Security.KeyVault.Keys": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "3rueqKRzNZdW+gEWox07Hpxq3n+0AZdRw3QI4GC3GBzLCw9n00LXgy4upow00bAuQJ9QGX2xwzJPg2/wALPYDA==", + "dependencies": { + "Azure.Core": "1.46.2" + } + }, + "Azure.Security.KeyVault.Secrets": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "tmcIgo+de2K5+PTBRNlnFLQFbmSoyuT9RpDr5MwKS6mIfNxLPQpARkRAP91r3tmeiJ9j/UCO0F+hTlk1Bk7HNQ==", + "dependencies": { + "Azure.Core": "1.46.2" + } + }, + "Glob": { + "type": "Transitive", + "resolved": "1.1.9", + "contentHash": "AfK5+ECWYTP7G3AAdnU8IfVj+QpGjrh9GC2mpdcJzCvtQ4pnerAGwHsxJ9D4/RnhDUz2DSzd951O/lQjQby2Sw==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2025.2.2", + "contentHash": "0X56ZRizuHdrnPpgXjWV7f2tQO1FlQg5O1967OGKnI/4ZRNOK642J8L7brM1nYvrxTTU5TP1yRyXLRLaXLPQ8A==" + }, + "matkoch.Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.61", + "contentHash": "EfEzClmsFSy1ukbJ9MeDB0zdAZ2k9HLxd6I3Hiv9Gz14Ql26XQ0GVXJESjTda0T78K/7UiraDx3w5AwRkxb5lg==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" + }, + "Microsoft.Build": { + "type": "Transitive", + "resolved": "18.0.2", + "contentHash": "MlcYsFVJbA7S1ByOf9DbuPMQ08nlF88VkqkIjmBkIiasA/M0Zo87H6Vk44e/SuLffomGGYSr2Hh5El6UEDDgKw==", + "dependencies": { + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.NET.StringTools": "18.0.2", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Reflection.MetadataLoadContext": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Microsoft.Build.Framework": { + "type": "Transitive", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.Build.Locator": { + "type": "Transitive", + "resolved": "1.7.8", + "contentHash": "sPy10x527Ph16S2u0yGME4S6ohBKJ69WfjeGG/bvELYeZVmJdKjxgnlL8cJJJLGV/cZIRqSfB12UDB8ICakOog==" + }, + "Microsoft.Build.Tasks.Core": { + "type": "Transitive", + "resolved": "18.0.2", + "contentHash": "ff9DeP4WUVxk6Ojm7qcK4HC+zXaKl0IJYXhS+lC51Sx76QtaQfPGZDJz9Go5ERQecmPV/HZ5brZAa9U36l2laA==", + "dependencies": { + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.Build.Utilities.Core": "18.0.2", + "Microsoft.NET.StringTools": "18.0.2", + "System.CodeDom": "9.0.0", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Formats.Nrbf": "9.0.0", + "System.Resources.Extensions": "9.0.0", + "System.Security.Cryptography.Pkcs": "9.0.6", + "System.Security.Cryptography.ProtectedData": "9.0.6", + "System.Security.Cryptography.Xml": "9.0.0" + } + }, + "Microsoft.Build.Utilities.Core": { + "type": "Transitive", + "resolved": "18.0.2", + "contentHash": "qsI2Mc8tbJEyg5m4oTvxlu5wY8te0TIVxObxILvrrPdeFUwH5V5UXUT2RV054b3S9msIR+7zViTWp4nRp0YGbQ==", + "dependencies": { + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.NET.StringTools": "18.0.2", + "System.Configuration.ConfigurationManager": "9.0.0", + "System.Diagnostics.EventLog": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.6" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.3", + "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" + } + }, + "Microsoft.Identity.Client": { + "type": "Transitive", + "resolved": "4.78.0", + "contentHash": "vZ50HE9INSN+Ew8pCgTm0t7wzxQTqozF9L4MAsl64etXz0Teo0dbUvjpVzqDHRs6m1Vn8mHF04fGaxXrIvGpsg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.14.0" + } + }, + "Microsoft.Identity.Client.Extensions.Msal": { + "type": "Transitive", + "resolved": "4.78.0", + "contentHash": "DYU9o+DrDQuyZxeq91GBA9eNqBvA3ZMkLzQpF7L9dTk6FcIBM1y1IHXWqiKXTvptPF7CZE59upbyUoa+FJ5eiA==", + "dependencies": { + "Microsoft.Identity.Client": "4.78.0", + "System.Security.Cryptography.ProtectedData": "4.5.0" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==" + }, + "Microsoft.NET.StringTools": { + "type": "Transitive", + "resolved": "18.0.2", + "contentHash": "cTZw3GHkAlqZACYGeQT3niS3UfVQ8CH0O5+zUdhxstrg1Z8Q2ViXYFKjSxHmEXTX85mrOT/QnHZOeQhhSsIrkQ==" + }, + "Namotion.Reflection": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "KLk2gLR9f8scM82EiL+p9TONXXPy9+IAZVMzJOA/Wsa7soZD7UJGG6j0fq0D9ZoVnBRRnSeEC7kShhRo3Olgaw==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "NJsonSchema": { + "type": "Transitive", + "resolved": "11.5.2", + "contentHash": "CAmnt4tnylb82Ro6f2EFIGz8rmThuCsITECUNqGhVEQ5VvxV+XwsTPz6LF58MbvUEV1jVcH+uxxljgM/etgK7A==", + "dependencies": { + "NJsonSchema.Annotations": "11.5.2", + "Namotion.Reflection": "3.4.3", + "Newtonsoft.Json": "13.0.3" + } + }, + "NJsonSchema.Annotations": { + "type": "Transitive", + "resolved": "11.5.2", + "contentHash": "OfYQgNzJZb1r/gR5vza0DbBLxnmcITDhA5CXFuX1qxuP3rRdQMjbIz5VyDVHCU1QxyrfAymzajbh7iszEvyFGQ==" + }, + "NJsonSchema.NewtonsoftJson": { + "type": "Transitive", + "resolved": "11.5.2", + "contentHash": "2KuhjeP/E/hqixoKRjKUXD56V8gBkEB51gdbTU2gN2AeabCG4gW5YsAUl+ZumFgj0cbEJ7GKdl9IBdeZaLxiTA==", + "dependencies": { + "NJsonSchema": "11.5.2", + "Newtonsoft.Json": "13.0.3" + } + }, + "NuGet.Common": { + "type": "Transitive", + "resolved": "6.12.5", + "contentHash": "ceynBLlidkZIfrLdujE3f9FDYTLjs884sRL0zrX3PgTbGGfrLxZGz8KciHsqEnSBqMwpimbxdNrDcorU+Zd9oA==", + "dependencies": { + "NuGet.Frameworks": "6.12.5" + } + }, + "NuGet.Configuration": { + "type": "Transitive", + "resolved": "6.12.5", + "contentHash": "6ZHtYcHdMrnWWfeiMBrZTtytnjmb9N7eh4S26peV6mBQF71Mv/7X0gyw0VW9ZfEkm31sVD736a7mKNHf7wee1Q==", + "dependencies": { + "NuGet.Common": "6.12.5", + "System.Security.Cryptography.ProtectedData": "4.4.0" + } + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "6.12.5", + "contentHash": "myEA6gdCZLHrcMSbOFxqaIfYkMFpMePup35qZlYUxSQNrL0WG8im3/pc8trrd9ArqdO8cgKhm9DnZrnYCu33Zg==" + }, + "NuGet.Versioning": { + "type": "Transitive", + "resolved": "6.12.5", + "contentHash": "9/Sniz+tzNAjcSziMmh62PIbF1mxaSC+P5Mdw1LgMleCbeaObjkkR/RUlduu6yCL3EfruACO/uknDJ5xCH3quQ==" + }, + "Nuke.Build": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "6fW17c3K1M4jw4acp7sujpqSiN8UbZPudBvS9+LEpmH2iwf8uZ0rue1G1D+WNurAMK1p3kOH5qo1M99rwFtSrQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "NJsonSchema": "11.5.2", + "NJsonSchema.NewtonsoftJson": "11.5.2", + "Nuke.Build.Shared": "10.1.0", + "Nuke.ProjectModel": "10.1.0", + "Nuke.SolutionModel": "10.1.0", + "Nuke.Tooling": "10.1.0", + "Nuke.Utilities": "10.1.0", + "Nuke.Utilities.IO.Globbing": "10.1.0", + "Nuke.Utilities.Net": "10.1.0", + "Nuke.Utilities.Text.Json": "10.1.0", + "Nuke.Utilities.Text.Yaml": "10.1.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Formatting.Compact.Reader": "4.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Nuke.Build.Shared": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "4uwzKj5IbjOeKbVtCScPuFlBAPQvG9z/K9I7zvR7tUFFtXNqBfIZmy+lbOdgxy7YsKWO7xgvAtIC4VCem7AKLA==", + "dependencies": { + "Nuke.Utilities": "10.1.0" + } + }, + "Nuke.ProjectModel": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "qOd7BAH/dnfJPxPipjB/slQK6K9FJOPzzj/Kbq4INJtfJod6dRm6Zu9yTk/l1CLRC68CQIVM3OLb1v3NxXgejw==", + "dependencies": { + "Microsoft.Build": "18.0.2", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.Build.Locator": "1.7.8", + "Microsoft.Build.Tasks.Core": "18.0.2", + "Microsoft.Build.Utilities.Core": "18.0.2", + "Nuke.SolutionModel": "10.1.0", + "Nuke.Tooling": "10.1.0", + "Nuke.Utilities": "10.1.0" + } + }, + "Nuke.SolutionModel": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "A/jWyMVrwbT8Oh/X658/MjKLIr8BXWU9gL46FhWGRebgxiwlcuesV7AO5EXiaDH2MaR8RC6Bm0PIw+4OP7PBkQ==", + "dependencies": { + "Nuke.Utilities": "10.1.0", + "matkoch.Microsoft.VisualStudio.SolutionPersistence": "1.0.61" + } + }, + "Nuke.Tooling": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "Ilne9MWjyKV99zdlg1Fw2vylWGqCPjGxzbcpW9mdxVZhpNSgA4lSGvaoUBVAd2FjG6ynNmLCZY3Nz+g8QmuHMw==", + "dependencies": { + "Newtonsoft.Json": "13.0.4", + "NuGet.Packaging": "6.12.1", + "Nuke.Utilities": "10.1.0", + "Nuke.Utilities.Text.Json": "10.1.0", + "Serilog": "4.3.0" + } + }, + "Nuke.Utilities": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "jLjpdbP751BNiMllC4wreUtGuo436YsntNttPWV9qfthVx11gJJvxR5ASHFiI7mD1+NOXbgP8zFNH+QXxqSM+Q==", + "dependencies": { + "JetBrains.Annotations": "2025.2.2" + } + }, + "Nuke.Utilities.IO.Compression": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "6aJuGPmh34XjqGIzCWPEGlyk/7q5oH1BwvHIoc+TqeP/pgHUPHBs1EeXIcIyzTzfsOhQ5l4AYTRNwo0PBX8I7w==", + "dependencies": { + "Nuke.Utilities": "10.1.0", + "SharpZipLib": "1.4.2" + } + }, + "Nuke.Utilities.IO.Globbing": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "BDUEnifcXnpdZYOesxMdniLBxqVYXPwlH1WsKlmSEIhZ8JetII4LZcLO1sjEgMays6LmakH/SaH3p3ELEV5Fyw==", + "dependencies": { + "Glob": "1.1.9", + "Nuke.Utilities": "10.1.0" + } + }, + "Nuke.Utilities.Net": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "UcdmLosYd5PcFjTqaEnSC5UOmukXp1VXtOroDnQshPJR2Bn2SUuse38eOkvjCN7IemQIN2Q+fYceJAeuQyJ7pQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.4", + "Nuke.Utilities": "10.1.0" + } + }, + "Nuke.Utilities.Text.Json": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "MEqDU6eeum4uzLz+jgMFuFcaZy/bpbWtz8rLl9i7T4wW8Q+YMgPXZ7RHhzPhcwEG3n8TvV4jnDImfPcqNx065Q==", + "dependencies": { + "Newtonsoft.Json": "13.0.4", + "Nuke.Utilities": "10.1.0" + } + }, + "Nuke.Utilities.Text.Yaml": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "bO3dEpQcDk5He851rQzyrPyuLI3vL0q8EGZG7qFP4soMYHIPjSP68A/BQdWJOCmv4/zpMMYuKUBnhSeTD2BJAQ==", + "dependencies": { + "Nuke.Utilities": "10.1.0", + "YamlDotNet": "16.3.0" + } + }, + "Octokit": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "jGOuTH1l+TCpJH+fwYOp7USzHDuGfN1jKbLz3J2COwyn+wL08eynvpnM6rY2qkzIEXum3PN2p2QkP3BW/p9Qcw==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Formatting.Compact.Reader": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "E1gvPAx0AsQhlyzGwgcVnGe5QrdkSugwKh+6V/FUSdTMVKKPSiO6Ff5iosjBMNBvq244Zys7BhTfFmgCE0KUyQ==", + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "System.ClientModel": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "AqRzhn0v29GGGLj/Z6gKq4lGNtvPHT4nHdG5PDJh9IfVjv/nYUVmX11hwwws1vDFeIAzrvmn0dPu8IjLtu6fAw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", + "System.Memory.Data": "8.0.1" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "oTE5IfuMoET8yaZP/vdvy9xO47guAv/rOhe4DODuFBN3ySprcQOlXqO3j+e/H/YpKKR5sglrxRaZ2HYOhNJrqA==" + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "PdkuMrwDhXoKFo/JxISIi9E8L+QGn9Iquj2OKDWHB6Y/HnUOuBouF7uS3R4Hw3FoNmwwMo6hWgazQdyHIIs27A==", + "dependencies": { + "System.Diagnostics.EventLog": "9.0.0", + "System.Security.Cryptography.ProtectedData": "9.0.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qd01+AqPhbAG14KtdtIqFk+cxHQFZ/oqRSCoxU1F+Q6Kv0cl726sl7RzU9yLFGd4BUOKdN4XojXF0pQf/R6YeA==" + }, + "System.Formats.Nrbf": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "F/6tNE+ckmdFeSQAyQo26bQOqfPFKEfZcuqnp4kBE6/7jP26diP+QTHCJJ6vpEfaY6bLy+hBLiIQUSxSmNwLkA==" + }, + "System.Memory.Data": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "BVYuec3jV23EMRDeR7Dr1/qhx7369dZzJ9IWy2xylvb4YfXsrUxspWc4UWYid/tj4zZK58uGZqn2WQiaDMhmAg==" + }, + "System.Reflection.MetadataLoadContext": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "nGdCUVhEQ9/CWYqgaibYEDwIJjokgIinQhCnpmtZfSXdMS6ysLZ8p9xvcJ8VPx6Xpv5OsLIUrho4B9FN+VV/tw==" + }, + "System.Resources.Extensions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "tvhuT1D2OwPROdL1kRWtaTJliQo0WdyhvwDpd8RM997G7m3Hya5nhbYhNTS75x6Vu+ypSOgL5qxDCn8IROtCxw==", + "dependencies": { + "System.Formats.Nrbf": "9.0.0" + } + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "W6BsawU2tB2qy6u4+3cBOSOmiTUgYWW365b5WMVA1bDxQwZZeNWD+qufKJkDQNKzAKq4b6Q04DDKhmr2SNJ1eg==" + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "9.0.6", + "contentHash": "yErfw/3pZkJE/VKza/Cm5idTpIKOy/vsmVi59Ta5SruPVtubzxb8CtnE8tyUpzs5pr0Y28GUFfSVzAhCLN3F/Q==" + }, + "YamlDotNet": { + "type": "Transitive", + "resolved": "16.3.0", + "contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA==" + } + } + } +} \ No newline at end of file diff --git a/deploy/linux/README.md b/deploy/linux/README.md new file mode 100644 index 0000000..4b253a2 --- /dev/null +++ b/deploy/linux/README.md @@ -0,0 +1,32 @@ +# Linux 部署模板 + +当前产物用于服务宿主与用户代理框架验证。远程工作区各组件的状态由 readiness 报告。 + +## 发布 + +执行 `bash ./build.sh --target Publish --runtime linux-x64`。将发布目录整体部署到 `/opt/workspace-access/`。ARM64 使用 `linux-arm64`。服务与用户代理共用同一程序,通过 `host`、`agent` 参数选择角色。赋予入口文件执行权限: + +```bash +sudo chmod 0755 /opt/workspace-access/WorkspaceAccessHost +``` + +## 系统服务 + +由部署人员建立 `workspace-access` 系统用户及同名组,并赋予安装目录读取和执行权限。把 `workspace-access-host.service` 复制到 `/etc/systemd/system/` 后执行: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now workspace-access-host.service +systemctl status workspace-access-host.service +journalctl -u workspace-access-host.service +``` + +模板使用低权限 broker 账户,运行目录为 `/run/workspace-access`,状态目录为 `/var/lib/workspace-access`。用户上下文启动组件按专门权限边界接入。HTTP 健康端点位于 `127.0.0.1:5080`。 + +## 用户代理 + +`workspace-access-agent.service` 为 systemd user unit。以目标用户身份将其放入 `~/.config/systemd/user/`,通过 `systemctl --user daemon-reload` 和 `systemctl --user enable --now workspace-access-agent.service` 启动。用户管理器随登录和 linger 策略运行,部署时明确相应生命周期。 + +此模板用于验证现有用户上下文中的 Agent 入口;完整的按需账户启动流程在用户启动组件中实现。 + +源仓库设计文档:`docs/platforms/user-environment.md`、`docs/platforms/session-lifecycle.md`、`docs/implementation-status.md`。 diff --git a/deploy/linux/workspace-access-agent.service b/deploy/linux/workspace-access-agent.service new file mode 100644 index 0000000..eca3332 --- /dev/null +++ b/deploy/linux/workspace-access-agent.service @@ -0,0 +1,12 @@ +[Unit] +Description=Workspace Access User Agent + +[Service] +Type=simple +ExecStart=/opt/workspace-access/WorkspaceAccessHost agent +Restart=on-failure +RestartSec=5 +UMask=0077 + +[Install] +WantedBy=default.target diff --git a/deploy/linux/workspace-access-host.service b/deploy/linux/workspace-access-host.service new file mode 100644 index 0000000..6eb4d24 --- /dev/null +++ b/deploy/linux/workspace-access-host.service @@ -0,0 +1,23 @@ +[Unit] +Description=Workspace Access Host +After=network.target + +[Service] +Type=notify +User=workspace-access +Group=workspace-access +WorkingDirectory=/opt/workspace-access +ExecStart=/opt/workspace-access/WorkspaceAccessHost host +Restart=on-failure +RestartSec=5 +TimeoutStopSec=30 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +RuntimeDirectory=workspace-access +StateDirectory=workspace-access +UMask=0077 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/windows/README.md b/deploy/windows/README.md new file mode 100644 index 0000000..6bebf6c --- /dev/null +++ b/deploy/windows/README.md @@ -0,0 +1,27 @@ +# Windows 部署模板 + +当前产物用于服务宿主与代理框架验证,健康状态列出远程工作区的集成进度。 + +## 发布与目录 + +执行 `./build.ps1 --target Publish --runtime win-x64`,将发布目录整体部署到 `C:\Program Files\WorkspaceAccessHost`。ARM64 使用 `win-arm64`。服务与用户代理共用目录中的 `WorkspaceAccessHost.exe`,分别传入 `host` 和 `agent`。 + +服务账户需要读取安装目录和配置。框架阶段服务使用 LocalService。正式代理 IPC 的管道 ACL 配置与服务账户保持一致。 + +## 注册服务 + +在管理员 PowerShell 中执行: + +```powershell +sc.exe create WorkspaceAccessHost binPath= '"C:\Program Files\WorkspaceAccessHost\WorkspaceAccessHost.exe" host' start= auto obj= 'NT AUTHORITY\LocalService' +sc.exe start WorkspaceAccessHost +sc.exe query WorkspaceAccessHost +``` + +健康地址为 `http://127.0.0.1:5080/health/live`。就绪地址为 `/health/ready`。日志由 .NET 日志提供程序输出。 + +## 用户代理 + +以目标普通用户运行 `WorkspaceAccessHost.exe agent` 验证入口。用户登录启动任务的可执行路径指向 `WorkspaceAccessHost.exe`,参数填写 `agent`,并选择目标用户的交互登录上下文。代理 IPC 注册与终端集成完成后,按部署策略启用该启动任务。 + +源仓库设计文档:`docs/platforms/service-hosting.md`、`docs/platforms/user-environment.md`、`docs/platforms/storage.md`。 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..789bf73 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# 文档库 + +## 产品与架构 + +- [总功能描述与平台差异](overview.md) +- [组件架构](architecture.md) +- [身份认证和授权](authentication.md) +- [连接与会话协议](connection-protocol.md) +- [交付状态](implementation-status.md) + +## 平台专题 + +总功能描述中的差异表链接到每项差异的独立文档,覆盖服务宿主、用户环境、PTY、进程权限、生命周期、文件存储、系统管理和发布部署。 + +## 开发与运行 + +- [开发与构建](development.md) +- [验证与验收](testing.md) +- [Windows 部署模板](../deploy/windows/README.md) +- [Linux 部署模板](../deploy/linux/README.md) + +文档中的“设计”描述目标行为,“交付状态”描述代码已经实现的能力。接口契约与平台运行验收共同约束后续开发。 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..aa22e09 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,53 @@ +# 组件架构 + +## 运行结构 + +```mermaid +flowchart LR + Client[远程客户端] -->|OIDC 登录| Auth[authentik] + Client -->|WSS| Broker[Host / Broker] + Broker --> Policy[认证校验与系统账户映射] + Broker --> Gate[Host 独占控制权] + Broker -->|本机认证 IPC| Agent[用户会话代理] + Agent --> Terminal[PTY 与终端状态] + Terminal --> Shell[shell / TUI 进程] +``` + +## 代码组织 + +解决方案包含一个产品项目 `src/WorkspaceAccessHost.csproj`、一个测试项目 `WorkspaceAccessHost.Tests` 和一个 NUKE 构建项目 `Build`。产品代码直接在 `src/` 下通过目录和命名空间组织职责: + +| 产品目录 | 职责 | +| --- | --- | +| `Core/` | 共享模型、平台描述和契约 | +| `Authentication/` | 外部身份、认证与系统账户映射 | +| `Connections/` | 连接控制权、接管和工作区归属 | +| `Terminals/` | PTY 契约、终端状态与输入输出 | +| `Platforms/Windows/` | Windows 平台策略及 ConPTY、用户令牌、命名管道的实现位置 | +| `Platforms/Linux/` | Linux 平台策略及 PTY、UID/GID、Unix socket 的实现位置 | +| `Hosting/` | 服务生命周期、网络入口、连接仲裁和健康检查 | +| `Agent/` | 用户上下文中的终端、子进程和画面状态 | + +领域逻辑依赖接口与共享模型,系统调用集中在对应平台目录,入口负责注册平台实现。 + +## 程序角色 + +同一套发布产物以 `WorkspaceAccessHost host` 运行服务宿主,以 `WorkspaceAccessHost agent` 运行用户代理。Windows 可执行文件带 `.exe` 扩展名。角色参数后的选项传递给对应宿主构建器;无参数或 `--help` 显示使用方式,未知角色以退出码 2 结束。 + +Host 和 Agent 分别运行在服务账户与目标用户上下文中,各自拥有进程生命周期,通过本机 IPC 协作。测试与 NUKE 项目承担验证和开发控制职责。 + +## 生命周期和信任边界 + +authentik 身份、broker 连接、系统登录上下文、工作区和终端拥有独立标识。客户端的系统用户名是授权请求;最终账户由服务端映射规则确定。 + +代理 IPC 需要验证对端的系统身份、目标用户、host 实例和协议版本。Windows 使用命名管道 ACL,Linux 使用 socket 文件权限与对端凭据。代理仅接受经验证的 broker 指令。 + +broker 使用服务账户运行。Windows 用户代理通过目标用户登录启动;Linux 启动组件负责完成组、用户和会话设置后交给用户代理。系统身份切换保持在明确的本机接口中。 + +首期 broker 进程生命周期是连接控制状态的边界。用户代理在 WebSocket 断开时持续存在;broker 重启后的代理重新登记、归属恢复与控制权版本同步需要完成集成验收。 + +## 终端契约 + +`IPtySessionFactory` 在已建立的用户上下文中启动终端。`IPtySession` 提供输入、输出、尺寸变更、退出状态和显式终止。工作区管理器拥有会话,传输关闭只解除附着。 + +代理持续消费输出,维护有容量上限的历史和终端状态。快照与输出序列号形成重连边界,输出积压策略由协议规定。 diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..53653de --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,23 @@ +# 身份认证与授权 + +## 身份来源 + +authentik 提供 OIDC 身份认证、MFA 和应用访问策略。操作系统管理系统账户、组和文件权限。Host 配置受信任 issuer、客户端标识和账户映射规则,运行数据记录身份与工作区的关系。 + +稳定外部身份为经过验证的 `(iss, sub)`。Windows 账户解析为 SID,Linux 账户解析为 UID、主组和附加组。映射配置可来自管理员配置或受管理员控制的可信声明。 + +## 登录流程 + +浏览器客户端采用 Authorization Code + PKCE,应用校验 state、nonce、签名、issuer、audience 和有效期。会话密钥与客户端密钥通过部署配置管理。浏览器会话使用 Secure、HttpOnly Cookie,并在握手时校验 Origin;变更操作采用 CSRF 防护。 + +完成登录后,按 host 访问规则、目标系统账户映射和代理状态进行授权,再进入连接控制权获取阶段。每次重连与接管均重新检查授权。授权撤销时关闭传输并撤销控制权,工作区结束策略由 host 管理配置确定。 + +## 会话归属 + +host 工作区有一个外部身份所有者。断线保留所有者;同身份可重连或显式接管。工作区终止并清理后释放归属。两个外部身份即使映射到同一系统账户,也保持各自的工作区授权边界。 + +## 审计 + +记录事件时间、host、身份标识、系统账户标识、连接标识、控制权版本及结果。认证凭据、Cookie 和终端内容采用独立的数据保护策略。常规审计聚焦身份与生命周期事件。 + +参考:[authentik OIDC](https://docs.goauthentik.io/add-secure-apps/providers/oauth2)。实现阶段见[交付状态](implementation-status.md)。 diff --git a/docs/connection-protocol.md b/docs/connection-protocol.md new file mode 100644 index 0000000..3036373 --- /dev/null +++ b/docs/connection-protocol.md @@ -0,0 +1,39 @@ +# 连接与会话协议 + +本文定义传输集成时应遵守的行为契约。当前共享核心实现控制权仲裁,消息编码和端点处于集成阶段。 + +## 连接状态 + +```text +Authenticated → Authorized → Acquired → Attached + ↑ ↓ + Reconnect ← Detached +``` + +host 工作区归属随创建建立,随显式结束释放。连接断开只改变附着状态。 + +## 控制权 + +- 首次授权连接取得控制权。 +- 同身份第二连接提交显式 takeover 请求后,原连接失效。 +- 其他身份得到 host 已被占用的响应。 +- 每次控制权获取生成新的连接 ID 和递增 generation。 +- 每个 broker 实例另有独立 epoch;IPC 集成使用 `(epoch, generation, connectionId)` 标识控制权。 +- WebSocket 关闭、心跳超时或授权失效时,按连接标识撤销控制权。 + +`HostConnectionGate` 使用进程内不可伪造的 lease 对象。`TryDispatch` 在同一临界区中完成校验与同步入队,接管与输入接受存在确定顺序。接管完成后旧 lease 的输入、关闭与释放均被拒绝;接管前已接受的输入按队列顺序执行。真正传输集成应把控制消息与输入交由同一有序派发路径处理。 + +## 消息职责 + +| 消息 | 内容与行为 | +| --- | --- | +| Attach | 目标工作区、协议版本、显式接管意图 | +| Input | 二进制终端输入,绑定已验证连接与控制权 | +| Resize | 有界列数与行数,绑定当前控制权 | +| Output | 二进制终端输出与递增序列号 | +| Snapshot | 屏幕、光标、终端模式、历史边界和输出序列号 | +| Heartbeat | 检测失效连接并触发释放 | +| Detach | 结束传输,保留工作区 | +| Terminate | 授权后结束工作区进程并释放归属 | + +终端数据按字节传输,控制消息使用独立类型。输入限制、输出缓冲上限、快照一致性和客户端尺寸变化由协议实现和测试固定。传输入口先完成认证授权,再允许升级及访问工作区。 diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..a997d22 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,54 @@ +# 开发与 NUKE 构建 + +## 工具链 + +安装 `global.json` 指定的 .NET 10 SDK,SDK 补丁选择遵循 `latestPatch`。包版本集中放在 `Directory.Packages.props`,每个项目的 `packages.lock.json` 记录依赖解析结果。 + +`src/WorkspaceAccessHost.csproj` 声明完整 RID 集合,使各平台发布共享稳定的依赖锁。产品代码直接按职责组织在 `src/` 下。解决方案包含产品、测试和 NUKE 三个项目。构建入口关闭 NUKE 遥测,编译和发布使用独立构建进程。 + +Windows 使用 `./build.ps1`,Linux 使用 `bash ./build.sh`。两者都运行 `build/Build.csproj` 中的 NUKE 程序。也可直接执行: + +```text +dotnet run --project build/Build.csproj --configuration Release -- --target Verify +``` + +## 构建目标 + +| 目标 | 行为 | +| --- | --- | +| Restore | 恢复解决方案依赖;`--locked-restore` 启用锁定模式 | +| Compile | Restore 后构建整个解决方案 | +| Test | Compile 后执行核心测试并输出 TRX | +| CheckDocs | 检查标题、相对链接文件、README 可达性及差异说明覆盖 | +| Verify | CheckDocs 与 Test;默认目标 | +| Publish | Verify 后按 `--runtime` 发布单一产品和部署模板 | +| RunHost | Compile 后以 `host` 角色前台运行产品 | +| RunAgent | Compile 后以 `agent` 角色在当前用户身份下运行产品 | + +配置参数为 `--configuration Debug` 或 `Release`。发布 RID 为 `win-x64`、`win-arm64`、`linux-x64`、`linux-arm64`。 + +```powershell +./build.ps1 --target Verify --locked-restore +./build.ps1 --target Publish --runtime win-x64 +./build.ps1 --target Publish --runtime linux-x64 +``` + +## 开发检查 + +1. 修改实现及对应设计文档。 +2. 为身份边界、并发控制和资源生命周期增加测试。 +3. 执行 Verify,检查 `artifacts/test-results/`。 +4. 涉及原生行为时,在目标系统执行[平台验收](testing.md)。 + +依赖升级时先修改集中版本,再执行 Restore,检查锁文件变化并执行 Verify。CI 先以锁定模式恢复构建程序,再通过 NUKE 验证和发布。 + +## 本地运行 + +Host 默认地址为 `http://127.0.0.1:5080`,命令行 `--urls` 或环境变量 `ASPNETCORE_URLS` 可覆盖地址。直接运行示例: + +```text +dotnet run --project src -- host --urls http://127.0.0.1:5081 +dotnet run --project src -- agent +``` + +健康检查分别为 `/health/live` 和 `/health/ready`。当前行为见[交付状态](implementation-status.md)。 diff --git a/docs/implementation-status.md b/docs/implementation-status.md new file mode 100644 index 0000000..ae2a715 --- /dev/null +++ b/docs/implementation-status.md @@ -0,0 +1,31 @@ +# 交付状态 + +## 框架已实现 + +| 模块 | 可验证内容 | +| --- | --- | +| 解决方案 | 单一 .NET 10 产品项目、测试项目、NUKE 项目、集中包版本与依赖锁文件 | +| NUKE | Restore、Compile、Test、CheckDocs、Verify、Publish、RunHost、RunAgent | +| Host | `host` 角色、ASP.NET Core 入口、Windows Service / systemd 生命周期集成、loopback 健康端点 | +| Agent | `agent` 角色、目标用户进程入口、日志、宿主退出响应 | +| 平台层 | `Platforms/Windows` 与 `Platforms/Linux` 目录及平台描述 | +| 核心契约 | 外部身份、终端尺寸与 PTY 会话接口 | +| 连接仲裁 | host 独占、显式接管、断线保留归属、过期 lease 拒绝与并发控制 | +| 开发治理 | 平台差异文档、链接和导航校验、双平台 CI、服务部署模板 | + +## 集成工作 + +| 阶段 | 实现内容 | 验收入口 | +| --- | --- | --- | +| 用户代理 | Windows 登录启动、命名管道;Linux 用户启动组件与 socket | 系统账户归属及 IPC 对端校验 | +| 终端后端 | ConPTY、POSIX PTY、输入输出与进程清理 | 交互 shell、尺寸变更、退出及断线运行 | +| 认证授权 | authentik OIDC、账户映射、host 授权及撤销 | 两个外部身份和两个系统账户的访问测试 | +| 传输接入 | WSS、显式接管、心跳、有序输入 | 旧连接输入拒绝与网络恢复 | +| 终端状态 | 屏幕状态、输出历史、快照同步 | 全屏编辑器重连后的画面一致性 | +| 存储验收 | 目标 VirtioFS 版本与挂载配置 | 权限、变更通知、锁与保存行为 | + +## 健康状态 + +`/health/live` 表示 HTTP 宿主存活。`/health/ready` 在框架阶段返回 HTTP 503,列出 OIDC、代理 IPC、PTY 和 WebSocket 集成项。就绪检查在接入真实组件后按实际依赖状态计算。 + +构建和发布证明代码与依赖可生成目标平台产物。操作系统用户上下文、服务安装、PTY 和 VirtioFS 行为通过目标系统上的集成验收确认。 diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..09467ed --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,42 @@ +# 总功能描述 + +## 产品目标 + +Workspace Access Host 提供持续存在的字符终端工作区。用户完成 authentik OIDC 认证后,以已授权的系统用户身份访问 shell 与 TUI 程序。Windows 和 Linux 共享连接、授权和工作区模型,通过平台适配实现系统服务、用户执行上下文与伪终端。 + +产品使用单一 C# 项目,内部按目录组织架构。发布程序通过 `host` 和 `agent` 参数分别运行系统服务与用户代理角色,共享一套部署产物。 + +## 功能 + +| 功能 | 当前设计 | +| --- | --- | +| 身份来源 | authentik 管理外部身份,操作系统管理系统账户 | +| 身份关联 | 以 `(issuer, subject)` 标识外部身份,管理员配置系统账户映射与 host 访问规则 | +| 远程连接 | 客户端经 WSS 连接 broker,控制与终端数据按协议传输 | +| 连接独占 | 每个 host 一个有效远程连接;同身份显式接管时撤销旧控制权 | +| 工作区归属 | 归属独立于连接存在;断线后原身份可重新附着 | +| 终端持久性 | 用户代理持有 PTY 与进程,连接断开期间继续运行和读取输出 | +| 画面恢复 | 按终端状态快照与有序输出恢复光标、屏幕及滚动历史 | +| 输入控制 | 输入、终端缩放及会话变更均校验当前连接控制权 | +| 文件访问 | 遵循执行账户及实际存储后端权限;并行编辑使用独立工作目录 | +| 数据保存 | 应用负责文件保存,代理保存运行期间的终端状态;系统启动后建立新的运行会话 | +| 审计 | 记录认证结果、映射、连接获取、接管和会话结束等事件 | + +## 平台差异 + +| 项目 | Windows | Linux | 详细说明 | +| --- | --- | --- | --- | +| 服务宿主与 IPC | Windows Service、命名管道 | systemd、Unix domain socket | [服务宿主](platforms/service-hosting.md) | +| 用户环境来源 | 目标用户已登录时启动用户代理,关联 SID | 为现有系统账户建立 UID/GID 用户上下文 | [用户环境](platforms/user-environment.md) | +| 伪终端 | ConPTY 与 Windows 进程 API | POSIX PTY、控制终端与进程组 | [终端后端](platforms/terminal-backend.md) | +| 权限与进程边界 | Windows 访问令牌、对象 ACL、Job Object | UID/GID、文件权限、cgroup 和进程组 | [权限与进程](platforms/process-security.md) | +| 用户会话生命周期 | 跟随用户登录会话,锁屏期间保持工作区 | 跟随用户会话宿主与服务策略运行 | [生命周期](platforms/session-lifecycle.md) | +| 文件系统与共享目录 | NTFS 或 VirtioFS;验证 SID 与宿主权限映射 | 原生文件系统或挂载目录;验证 UID/GID 与锁行为 | [存储访问](platforms/storage.md) | +| 系统管理权限 | Windows 用户权限分配和提升策略 | sudo、polkit 与服务权限策略 | [系统管理](platforms/system-control.md) | +| 发布与注册 | Windows RID、SCM 注册和用户登录启动 | Linux RID、systemd system/user unit | [发布部署](platforms/deployment.md) | + +## 使用条件 + +Windows 标准运行方式是已登录用户代理。宿主进入等待状态后,目标用户登录即可建立代理上下文。部署可以选用专门的本地账户。Linux 的用户上下文由受控启动组件创建。两个平台的首期连接策略均为 host 级独占。 + +当前代码覆盖范围见[交付状态](implementation-status.md),验收项见[验证文档](testing.md)。 diff --git a/docs/platforms/deployment.md b/docs/platforms/deployment.md new file mode 100644 index 0000000..4b18497 --- /dev/null +++ b/docs/platforms/deployment.md @@ -0,0 +1,17 @@ +# 平台发布与部署 + +| 项目 | Windows | Linux | +| --- | --- | --- | +| 发布 RID | `win-x64`、`win-arm64` | `linux-x64`、`linux-arm64` | +| 产品入口 | `WorkspaceAccessHost.exe host` / `agent` | `WorkspaceAccessHost host` / `agent` | +| 服务管理 | SCM 注册、启动类型和账户配置 | systemd unit、服务账户和目录权限 | +| 用户代理启动 | 用户登录启动任务或用户启动项 | 用户上下文启动组件或 user unit | +| 本机通信路径 | 命名管道 | `/run` 下受控 socket 路径 | + +NUKE `Publish` 先执行 Verify,再发布单一产品的 self-contained 产物,并复制平台部署模板。输出位于 `artifacts/publish//`,根目录包含可执行文件、配置与运行时依赖,`deploy/` 包含部署模板。Host 和 Agent 使用同一可执行文件,通过启动参数选择角色,整套产物统一部署和更新。 + +服务配置使用发布目录中的 `appsettings.json` 和 .NET 环境变量覆盖。初始 HTTP 端点位于 loopback。远程传输集成时由受控 TLS 入口提供 WSS,并配置 host 白名单、认证及 Origin 策略。 + +x64 的 Windows 与 Linux 验证流程由 CI 分别执行。ARM64 产物通过指定 RID 发布,并在对应硬件或虚拟机完成运行验收。 + +部署步骤见 [Windows](../../deploy/windows/README.md) 与 [Linux](../../deploy/linux/README.md)。服务安装由部署人员执行。 diff --git a/docs/platforms/process-security.md b/docs/platforms/process-security.md new file mode 100644 index 0000000..51d14d0 --- /dev/null +++ b/docs/platforms/process-security.md @@ -0,0 +1,16 @@ +# 进程权限与隔离 + +| 层次 | Windows | Linux | +| --- | --- | --- | +| 账户权限 | 用户访问令牌、SID 与文件 ACL | UID、GID、附加组与文件权限 | +| 代理 IPC | 命名管道 ACL 与对端身份校验 | socket 路径权限与 peer credentials | +| 进程归属 | Job Object 与进程句柄 | 进程组与 systemd/cgroup | +| 系统管理 | 令牌权限及提升策略 | capabilities、sudo、polkit | + +网络宿主以服务账户执行网络与授权逻辑;用户代理以目标账户执行终端程序。身份切换或代理创建接口接受经过验证的账户映射和限定启动参数。 + +工作区清理追踪完整进程树。显式结束先执行有序退出,再按策略终止剩余进程,并释放 PTY 及本机通信资源。 + +完全复用用户环境时,终端具备该用户相应的系统访问能力。部署权限策略决定进程提升、系统管理和共享目录访问范围。文件权限在实际存储后端验收,见[存储说明](storage.md)。 + +验收包括不同账户的代理访问、伪造映射、越权工作目录、子进程清理,以及普通账户的管理权限边界。 diff --git a/docs/platforms/service-hosting.md b/docs/platforms/service-hosting.md new file mode 100644 index 0000000..da80022 --- /dev/null +++ b/docs/platforms/service-hosting.md @@ -0,0 +1,16 @@ +# 服务宿主与本机通信 + +| 平台 | 系统入口 | 用户代理通信 | +| --- | --- | --- | +| Windows | Windows Service Control Manager;`AddWindowsService` | 带 SID ACL 的命名管道 | +| Linux | systemd system unit;`AddSystemd` | 带文件权限和对端凭据检查的 Unix domain socket | + +Host 负责远程连接与控制权仲裁,用户代理持有工作区。服务账户具备配置读取与本机通信所需的权限。IPC 连接建立后验证协议版本、对端系统身份和目标账户。 + +Windows 代理在目标用户登录时启动。Linux 用户代理由用户上下文启动组件管理。本机通信路径按 host 与系统用户标识确定,进程启动后执行注册握手。 + +当前 Host 已集成系统服务生命周期与健康检查。IPC 注册属于后续集成项。部署入口见 [Windows](../../deploy/windows/README.md) 与 [Linux](../../deploy/linux/README.md)。 + +验收:服务随系统启动;服务停止完成资源清理;代理识别正确服务身份;其他账户访问 IPC 时按规则返回拒绝结果。 + +参考:[Windows Service](https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service)、[systemd 集成](https://devblogs.microsoft.com/dotnet/net-core-and-systemd/)。 diff --git a/docs/platforms/session-lifecycle.md b/docs/platforms/session-lifecycle.md new file mode 100644 index 0000000..5b64b0b --- /dev/null +++ b/docs/platforms/session-lifecycle.md @@ -0,0 +1,16 @@ +# 会话生命周期 + +| 事件 | Windows | Linux | +| --- | --- | --- | +| 创建工作区 | 已登录用户代理建立 ConPTY | 用户上下文中的代理建立 PTY | +| 网络断开 | 代理继续持有终端和进程 | 代理继续持有终端和进程 | +| 同身份重新连接 | 重新授权并附着原工作区 | 重新授权并附着原工作区 | +| 锁屏或关闭桌面连接 | 用户登录会话仍存在时保持工作区 | 按用户代理所属服务生命周期保持工作区 | +| 用户注销 | 用户代理及进程受 Windows 登录会话清理影响 | 按 user service、会话清理及 linger 配置执行 | +| 系统启动 | 等待目标用户代理注册后建立新工作区 | 服务准备用户上下文后建立新工作区 | + +持久性范围为系统本次运行期间、用户代理存活时的工作区。客户端断线仅解除连接;工作区终止是独立的授权操作。 + +连接归属在断线后保留。同一身份重连获取新控制权,接管撤销旧连接。会话管理器完成进程清理后释放工作区归属。broker 重启后的代理重新登记属于协议集成验收范围。 + +验收分别执行网络断开、浏览器关闭、锁屏、用户注销、代理退出和显式终止,记录工作区状态与进程结果。系统重启后验证新环境的建立条件。 diff --git a/docs/platforms/storage.md b/docs/platforms/storage.md new file mode 100644 index 0000000..0800d31 --- /dev/null +++ b/docs/platforms/storage.md @@ -0,0 +1,30 @@ +# 文件存储与 VirtioFS + +| 项目 | Windows | Linux | +| --- | --- | --- | +| 本地文件 | NTFS 等本地卷,使用 Windows 文件 API 与权限 | 原生文件系统,使用 POSIX 文件 API 与权限 | +| 宿主共享目录 | Windows VirtioFS 驱动与 WinFsp | VirtioFS 客户端与宿主目录 | +| 身份映射 | 验证 SID、挂载配置和宿主 UID/GID 的关系 | 验证 UID/GID、附加组及宿主权限 | +| 变更与锁 | 验证 Windows 通知、共享模式和锁的传播 | 验证文件通知、锁及跨挂载访问行为 | + +## 路径安排 + +服务配置、运行元数据及用户配置优先放在操作系统本地卷。项目文件可放在 VirtioFS 工作目录。挂载可用性和目标用户访问检查在创建工作区前执行。 + +并行编辑为本地操作与远程操作分配独立工作目录。代码项目可以采用独立 clone 或 Git worktree,通过版本控制整合修改。远程连接独占管理客户端控制权,文件写入由应用与存储权限决定。 + +## 兼容性记录 + +每个部署记录 PVE、virtiofsd、Windows virtio-win / WinFsp 版本、宿主文件系统、导出路径和挂载选项。 + +| 验证项 | 场景 | +| --- | --- | +| 可见性 | 目标用户代理上下文访问预期盘符或挂载点 | +| 权限 | 两个系统账户分别读取、创建、修改和删除文件 | +| 外部变更 | Windows 本地程序、宿主和其他 guest 修改文件后,编辑器观察变化 | +| 锁 | 不同进程及不同访问入口的独占锁和共享模式 | +| 保存 | 临时文件写入后重命名替换、刷新、重复保存 | +| 路径 | Unicode、大小写、长路径与符号链接 | +| 开发工具 | Git 操作、构建输出、文件监听和高频小文件访问 | + +参考:[Windows VirtioFS](https://github.com/virtio-win/kvm-guest-drivers-windows/wiki/Virtiofs:-Shared-file-system)、[Git worktree](https://git-scm.com/docs/git-worktree)。 diff --git a/docs/platforms/system-control.md b/docs/platforms/system-control.md new file mode 100644 index 0000000..fc6f51c --- /dev/null +++ b/docs/platforms/system-control.md @@ -0,0 +1,17 @@ +# 系统管理权限 + +| 能力 | Windows | Linux | +| --- | --- | --- | +| 本机关机和重启 | 系统用户权限分配,包含 `SeShutdownPrivilege` | 登录会话授权、polkit、sudo 或 root 权限 | +| 网络发起系统关机 | 对应远程管理权限及服务策略 | 对应远程管理服务和账户策略 | +| 提升执行权限 | Windows 用户令牌与提升机制 | sudo、capabilities 及服务管理接口 | + +终端使用目标系统账户的权限。部署阶段明确该账户的系统管理能力,并使用操作系统策略控制可执行的管理操作。通过远程终端运行的程序属于目标机器上的本地进程。 + +复用登录用户时,账户级权限策略也作用于该用户的其他程序。需要不同权限边界的部署使用专用执行账户或经过验证的受限上下文。 + +管理入口与普通终端输入分别授权。系统管理操作记录请求身份、目标、时间和执行结果。系统启动后的工作区状态由[用户环境](user-environment.md)和[生命周期](session-lifecycle.md)定义。 + +验收使用系统权限查询和具有代表性的系统管理 API,确认工作区与管理入口符合配置。PTY 输入作为终端字节流处理。 + +参考:[Windows 关机 API 权限](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-exitwindowsex)。 diff --git a/docs/platforms/terminal-backend.md b/docs/platforms/terminal-backend.md new file mode 100644 index 0000000..090b111 --- /dev/null +++ b/docs/platforms/terminal-backend.md @@ -0,0 +1,16 @@ +# 终端后端 + +| 项目 | Windows | Linux | +| --- | --- | --- | +| 伪终端 | ConPTY | POSIX PTY master/slave | +| 进程连接 | 伪控制台通过扩展启动属性关联用户进程 | session、控制终端、标准流和前台进程组 | +| 尺寸变更 | `ResizePseudoConsole` | 窗口尺寸 ioctl 与终端信号行为 | +| 生命周期 | 用户进程、管道和伪控制台句柄 | 子进程、进程组和 PTY 文件描述符 | + +共享接口为 `IPtySessionFactory` 与 `IPtySession`。终端输入输出按字节传递;用户代理负责持续读出、缓冲边界及终端状态。平台实现明确句柄所有权、取消和资源释放顺序。 + +Windows 后端在用户代理上下文中创建 shell。Linux 后端使用适合托管运行时的原生启动边界完成进程和终端设置,子进程进入目标程序前完成系统调用配置。 + +验收覆盖交互 shell、UTF-8、方向键、Ctrl+C、全屏编辑器、尺寸变更、大量输出、子进程退出及显式终止。断线后的输出持续被消费,重连恢复见[连接协议](../connection-protocol.md)。 + +参考:[ConPTY 会话](https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session)、[Linux PTY API](https://man7.org/linux/man-pages/man3/openpty.3.html)。 diff --git a/docs/platforms/user-environment.md b/docs/platforms/user-environment.md new file mode 100644 index 0000000..897f743 --- /dev/null +++ b/docs/platforms/user-environment.md @@ -0,0 +1,20 @@ +# 用户执行环境 + +| 平台 | 环境建立 | 身份标识 | 就绪条件 | +| --- | --- | --- | --- | +| Windows | 复用目标用户登录后的代理上下文 | Windows SID | 指定用户的代理已注册 | +| Linux | 为现有系统账户设置 UID、GID、附加组及用户会话 | UID 与组信息 | 账户解析、权限设置及代理启动完成 | + +## Windows + +标准部署使用目标用户登录后启动的 Agent。它继承用户权限和用户配置,终端启动时设置工作目录、环境变量和 shell 参数。专用本地账户可以作为远程工作区账户。 + +Host 根据外部身份映射匹配该用户的 SID。目标用户代理尚未注册时,工作区状态为 `WaitingForUserSession`。锁屏状态下保留用户会话;用户注销后的行为见[生命周期](session-lifecycle.md)。 + +## Linux + +系统账户由操作系统维护。启动组件按授权映射查询账户,初始化附加组、主组、UID、HOME、工作目录和登录环境;部署所需的 PAM account/session 处理归入该组件。用户代理以目标账户身份持有 PTY。 + +## 验收 + +检查终端中的系统身份、主目录、配置目录、环境变量和文件访问结果。Windows 验证登录前的等待状态与登录后的代理注册;Linux 验证无人交互登录时的用户上下文建立。认证归属与系统账户映射见[认证文档](../authentication.md)。 diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..284842f --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,29 @@ +# 验证与验收 + +## 自动验证 + +NUKE Verify 执行文档检查、解决方案构建和核心测试。测试包括同身份显式接管、旧 lease 输入与断开拒绝、跨 issuer 身份边界、断线后归属保留、工作区释放以及并发连接唯一获权。 + +CI 在 Windows 与 Linux x64 上各自执行 Verify 和 Publish,保存发布产物与 TRX。发布成功作为目标 RID 构建验证,运行能力通过以下环境验收确认。 + +## 集成验收矩阵 + +| 领域 | Windows | Linux | +| --- | --- | --- | +| 服务 | SCM 启动、停止、服务账户配置 | systemd 启动、停止、权限与日志 | +| 用户身份 | 登录代理 SID、主目录、锁屏与注销 | UID/GID/附加组、HOME、无人登录时启动 | +| 终端 | ConPTY、PowerShell、全屏 TUI、缩放 | PTY、shell、控制终端、信号与缩放 | +| 进程清理 | 子进程树和句柄释放 | 进程组/cgroup 与文件描述符释放 | +| 文件 | 实际 VirtioFS 驱动与 WinFsp 配置 | 实际挂载配置与宿主 UID/GID | + +## 跨平台场景 + +1. 两个客户端同时连接,确认 host 只有一个有效控制者。 +2. 同身份显式接管,旧客户端继续发送输入,确认过期输入被拒绝。 +3. 断线后另一身份尝试进入,确认工作区归属保持。 +4. 断线期间进程继续产生输出,重连后恢复终端状态。 +5. 认证过期或授权撤销后,确认传输关闭和控制权撤销。 +6. 全屏编辑器重连,确认光标、颜色、备用屏幕和尺寸一致。 +7. 显式终止清理完整进程树后,确认新的身份可以创建工作区。 + +存储场景细化见[VirtioFS 验收](platforms/storage.md)。每次环境验收记录系统版本、运行用户、依赖版本、配置、执行步骤与实际结果。 diff --git a/global.json b/global.json new file mode 100644 index 0000000..e1c5e99 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.401", + "rollForward": "latestPatch", + "allowPrerelease": false + } +} diff --git a/src/Agent/AgentApplication.cs b/src/Agent/AgentApplication.cs new file mode 100644 index 0000000..5c34c0b --- /dev/null +++ b/src/Agent/AgentApplication.cs @@ -0,0 +1,15 @@ +namespace WorkspaceAccessHost.Agent; + +internal static class AgentApplication +{ + public static async Task RunAsync(string[] args) + { + var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings + { + Args = args, + ContentRootPath = AppContext.BaseDirectory + }); + builder.Services.AddHostedService(); + await builder.Build().RunAsync(); + } +} diff --git a/src/Agent/AgentWorker.cs b/src/Agent/AgentWorker.cs new file mode 100644 index 0000000..28775b8 --- /dev/null +++ b/src/Agent/AgentWorker.cs @@ -0,0 +1,20 @@ +namespace WorkspaceAccessHost.Agent; + +internal sealed class AgentWorker(ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation( + "User agent scaffold running as {User}; IPC registration and PTY backend are pending integration.", + Environment.UserName); + + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + // Normal host shutdown. + } + } +} diff --git a/src/Authentication/ExternalIdentity.cs b/src/Authentication/ExternalIdentity.cs new file mode 100644 index 0000000..be02d1e --- /dev/null +++ b/src/Authentication/ExternalIdentity.cs @@ -0,0 +1,16 @@ +namespace WorkspaceAccessHost.Authentication; + +/// An identity obtained from a validated OIDC authentication result. +public sealed record ExternalIdentity +{ + public ExternalIdentity(string issuer, string subject) + { + ArgumentException.ThrowIfNullOrWhiteSpace(issuer); + ArgumentException.ThrowIfNullOrWhiteSpace(subject); + Issuer = issuer; + Subject = subject; + } + + public string Issuer { get; } + public string Subject { get; } +} diff --git a/src/Connections/HostConnectionGate.cs b/src/Connections/HostConnectionGate.cs new file mode 100644 index 0000000..9272772 --- /dev/null +++ b/src/Connections/HostConnectionGate.cs @@ -0,0 +1,124 @@ +using WorkspaceAccessHost.Authentication; + +namespace WorkspaceAccessHost.Connections; + +public enum AcquisitionStatus +{ + Acquired, + TakeoverRequired, + OwnedByAnotherIdentity +} + +/// A process-local capability. A transport must not construct one from client data. +public sealed class ConnectionLease +{ + internal ConnectionLease(ExternalIdentity owner, long generation) + { + Owner = owner; + Generation = generation; + ConnectionId = Guid.NewGuid(); + } + + public ExternalIdentity Owner { get; } + public long Generation { get; } + public Guid ConnectionId { get; } +} + +public sealed record AcquisitionResult(AcquisitionStatus Status, ConnectionLease? Lease); + +/// +/// Serializes control of one host workspace. Workspace ownership survives transport disconnection. +/// Authentication and host authorization must succeed before calling Acquire. +/// +public sealed class HostConnectionGate +{ + private readonly object _sync = new(); + private ExternalIdentity? _owner; + private ConnectionLease? _active; + private long _generation; + + public AcquisitionResult Acquire(ExternalIdentity identity, bool takeover = false) + { + ArgumentNullException.ThrowIfNull(identity); + lock (_sync) + { + if (_owner is not null && _owner != identity) + { + return new(AcquisitionStatus.OwnedByAnotherIdentity, null); + } + + if (_active is not null && !takeover) + { + return new(AcquisitionStatus.TakeoverRequired, null); + } + + var generation = checked(_generation + 1); + _owner = identity; + _generation = generation; + _active = new ConnectionLease(identity, generation); + return new(AcquisitionStatus.Acquired, _active); + } + } + + public bool IsCurrent(ConnectionLease lease) + { + ArgumentNullException.ThrowIfNull(lease); + lock (_sync) + { + return ReferenceEquals(_active, lease); + } + } + + /// + /// Atomically validates and enqueues a command relative to takeover. + /// The callback must be short, synchronous, and must not call back into this gate. + /// Already accepted commands are ordered before a later takeover. + /// + public bool TryDispatch(ConnectionLease lease, Action enqueue) + { + ArgumentNullException.ThrowIfNull(lease); + ArgumentNullException.ThrowIfNull(enqueue); + lock (_sync) + { + if (!ReferenceEquals(_active, lease)) + { + return false; + } + + enqueue(); + return true; + } + } + + public bool Disconnect(ConnectionLease lease) + { + ArgumentNullException.ThrowIfNull(lease); + lock (_sync) + { + if (!ReferenceEquals(_active, lease)) + { + return false; + } + + _active = null; + return true; + } + } + + /// Release ownership after the session manager has terminated the workspace. + public bool CloseWorkspace(ConnectionLease lease) + { + ArgumentNullException.ThrowIfNull(lease); + lock (_sync) + { + if (!ReferenceEquals(_active, lease)) + { + return false; + } + + _active = null; + _owner = null; + return true; + } + } +} diff --git a/src/Core/PlatformProfile.cs b/src/Core/PlatformProfile.cs new file mode 100644 index 0000000..db06766 --- /dev/null +++ b/src/Core/PlatformProfile.cs @@ -0,0 +1,19 @@ +namespace WorkspaceAccessHost.Core; + +public enum UserEnvironmentMode +{ + ExistingLogonSession, + SystemAccountSession +} + +/// Platform design metadata; executable backend readiness is reported separately. +public sealed record PlatformProfile( + string OperatingSystem, + string ServiceManager, + string TerminalBackend, + UserEnvironmentMode UserEnvironmentMode); + +public interface IPlatformProfileProvider +{ + PlatformProfile Profile { get; } +} diff --git a/src/Hosting/HostApplication.cs b/src/Hosting/HostApplication.cs new file mode 100644 index 0000000..29a6310 --- /dev/null +++ b/src/Hosting/HostApplication.cs @@ -0,0 +1,48 @@ +using WorkspaceAccessHost.Connections; +using WorkspaceAccessHost.Core; +using WorkspaceAccessHost.Platforms.Linux; +using WorkspaceAccessHost.Platforms.Windows; + +namespace WorkspaceAccessHost.Hosting; + +internal static class HostApplication +{ + public static async Task RunAsync(string[] args) + { + var builder = WebApplication.CreateBuilder(new WebApplicationOptions + { + Args = args, + ContentRootPath = AppContext.BaseDirectory + }); + + IPlatformProfileProvider platform; + if (OperatingSystem.IsWindows()) + { + builder.Services.AddWindowsService(options => options.ServiceName = "WorkspaceAccessHost"); + platform = new WindowsPlatformProfile(); + } + else if (OperatingSystem.IsLinux()) + { + builder.Services.AddSystemd(); + platform = new LinuxPlatformProfile(); + } + else + { + throw new PlatformNotSupportedException("WorkspaceAccessHost supports Windows and Linux."); + } + + builder.Services.AddSingleton(platform); + builder.Services.AddSingleton(); + + var app = builder.Build(); + app.MapGet("/health/live", () => Results.Ok(new { status = "alive" })); + app.MapGet("/health/ready", () => Results.Json( + new { status = "initializing", stage = "scaffold", pending = new[] { "oidc", "agent-ipc", "pty", "websocket" } }, + statusCode: StatusCodes.Status503ServiceUnavailable)); + + app.Logger.LogInformation( + "Host scaffold started for {OperatingSystem}; remote workspace readiness is pending backend integration.", + platform.Profile.OperatingSystem); + await app.RunAsync(); + } +} diff --git a/src/Platforms/Linux/LinuxPlatformProfile.cs b/src/Platforms/Linux/LinuxPlatformProfile.cs new file mode 100644 index 0000000..015d797 --- /dev/null +++ b/src/Platforms/Linux/LinuxPlatformProfile.cs @@ -0,0 +1,9 @@ +using WorkspaceAccessHost.Core; + +namespace WorkspaceAccessHost.Platforms.Linux; + +public sealed class LinuxPlatformProfile : IPlatformProfileProvider +{ + public PlatformProfile Profile { get; } = new( + "linux", "systemd", "POSIX PTY", UserEnvironmentMode.SystemAccountSession); +} diff --git a/src/Platforms/Windows/WindowsPlatformProfile.cs b/src/Platforms/Windows/WindowsPlatformProfile.cs new file mode 100644 index 0000000..0a6625b --- /dev/null +++ b/src/Platforms/Windows/WindowsPlatformProfile.cs @@ -0,0 +1,9 @@ +using WorkspaceAccessHost.Core; + +namespace WorkspaceAccessHost.Platforms.Windows; + +public sealed class WindowsPlatformProfile : IPlatformProfileProvider +{ + public PlatformProfile Profile { get; } = new( + "windows", "Windows Service", "ConPTY", UserEnvironmentMode.ExistingLogonSession); +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..75c9a47 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,34 @@ +using WorkspaceAccessHost.Agent; +using WorkspaceAccessHost.Hosting; + +const string usage = "Usage: WorkspaceAccessHost [options]"; + +if (args.Length == 0 || args[0] is "--help" or "-h") +{ + Console.WriteLine(usage); + return 0; +} + +if (args[0] is not ("host" or "agent")) +{ + Console.Error.WriteLine(usage); + return 2; +} + +if (!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux()) +{ + Console.Error.WriteLine("WorkspaceAccessHost supports Windows and Linux."); + return 1; +} + +var roleArguments = args[1..]; +if (args[0] == "host") +{ + await HostApplication.RunAsync(roleArguments); +} +else +{ + await AgentApplication.RunAsync(roleArguments); +} + +return 0; diff --git a/src/Terminals/IPtySession.cs b/src/Terminals/IPtySession.cs new file mode 100644 index 0000000..8f70f0b --- /dev/null +++ b/src/Terminals/IPtySession.cs @@ -0,0 +1,41 @@ +namespace WorkspaceAccessHost.Terminals; + +public sealed record TerminalSize +{ + public TerminalSize(int columns, int rows) + { + ArgumentOutOfRangeException.ThrowIfLessThan(columns, 1); + ArgumentOutOfRangeException.ThrowIfGreaterThan(columns, short.MaxValue); + ArgumentOutOfRangeException.ThrowIfLessThan(rows, 1); + ArgumentOutOfRangeException.ThrowIfGreaterThan(rows, short.MaxValue); + Columns = columns; + Rows = rows; + } + + public int Columns { get; } + public int Rows { get; } +} + +/// Owned by a user agent for the lifetime of a terminal, independently of its transport. +public interface IPtySession : IAsyncDisposable +{ + Guid SessionId { get; } + Stream Output { get; } + Task ExitCode { get; } + ValueTask WriteAsync(ReadOnlyMemory input, CancellationToken cancellationToken); + ValueTask ResizeAsync(TerminalSize size, CancellationToken cancellationToken); + ValueTask TerminateAsync(CancellationToken cancellationToken); +} + +/// Invoked inside an already established system user context. +public interface IPtySessionFactory +{ + ValueTask StartAsync( + TerminalStartInfo startInfo, CancellationToken cancellationToken); +} + +public sealed record TerminalStartInfo( + string Executable, + IReadOnlyList Arguments, + string WorkingDirectory, + TerminalSize Size); diff --git a/src/WorkspaceAccessHost.csproj b/src/WorkspaceAccessHost.csproj new file mode 100644 index 0000000..355f68c --- /dev/null +++ b/src/WorkspaceAccessHost.csproj @@ -0,0 +1,9 @@ + + + win-x64;win-arm64;linux-x64;linux-arm64 + + + + + + diff --git a/src/appsettings.json b/src/appsettings.json new file mode 100644 index 0000000..c9fea6b --- /dev/null +++ b/src/appsettings.json @@ -0,0 +1,10 @@ +{ + "Urls": "http://127.0.0.1:5080", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "localhost;127.0.0.1" +} diff --git a/src/packages.lock.json b/src/packages.lock.json new file mode 100644 index 0000000..56c0a7c --- /dev/null +++ b/src/packages.lock.json @@ -0,0 +1,55 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.Extensions.Hosting.Systemd": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "WC1u8RbTqJTY/JknZFHX0+TDptgtpoBRc8Kro0xPSwunMql3e1zBxnK+OJbx30ozSCfMpHaYAfaeiVQgM5sizw==" + }, + "Microsoft.Extensions.Hosting.WindowsServices": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "Za5xCWb7jkHd7+hYu8C22uIf0DGhNASIiXo1xSU450nj01RFCerP06h1Bh3kxUm+5DijfgP0gtpY6v+oI/UI4g==", + "dependencies": { + "System.ServiceProcess.ServiceController": "10.0.12" + } + }, + "System.ServiceProcess.ServiceController": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sFV+RM/yPtshiCAEIuTm8Dt4F7VuKRbL50PnGC9nXHolk/jw+z3T8zx7axKTBC8mqtQU2ZmjsoPC9b1iwOilNQ==" + } + }, + "net10.0/linux-arm64": { + "System.ServiceProcess.ServiceController": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sFV+RM/yPtshiCAEIuTm8Dt4F7VuKRbL50PnGC9nXHolk/jw+z3T8zx7axKTBC8mqtQU2ZmjsoPC9b1iwOilNQ==" + } + }, + "net10.0/linux-x64": { + "System.ServiceProcess.ServiceController": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sFV+RM/yPtshiCAEIuTm8Dt4F7VuKRbL50PnGC9nXHolk/jw+z3T8zx7axKTBC8mqtQU2ZmjsoPC9b1iwOilNQ==" + } + }, + "net10.0/win-arm64": { + "System.ServiceProcess.ServiceController": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sFV+RM/yPtshiCAEIuTm8Dt4F7VuKRbL50PnGC9nXHolk/jw+z3T8zx7axKTBC8mqtQU2ZmjsoPC9b1iwOilNQ==" + } + }, + "net10.0/win-x64": { + "System.ServiceProcess.ServiceController": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sFV+RM/yPtshiCAEIuTm8Dt4F7VuKRbL50PnGC9nXHolk/jw+z3T8zx7axKTBC8mqtQU2ZmjsoPC9b1iwOilNQ==" + } + } + } +} \ No newline at end of file diff --git a/tests/WorkspaceAccessHost.Tests/HostConnectionGateTests.cs b/tests/WorkspaceAccessHost.Tests/HostConnectionGateTests.cs new file mode 100644 index 0000000..8fd9e5c --- /dev/null +++ b/tests/WorkspaceAccessHost.Tests/HostConnectionGateTests.cs @@ -0,0 +1,78 @@ +using System.Collections.Concurrent; +using WorkspaceAccessHost.Connections; +using WorkspaceAccessHost.Authentication; + +namespace WorkspaceAccessHost.Tests; + +public sealed class HostConnectionGateTests +{ + private static readonly ExternalIdentity Alice = new("https://auth.example/application/o/workspace/", "alice-id"); + + [Fact] + public void TakeoverInvalidatesOldInputAndDisconnect() + { + var gate = new HostConnectionGate(); + var first = Assert.IsType(gate.Acquire(Alice).Lease); + Assert.Equal(AcquisitionStatus.TakeoverRequired, gate.Acquire(Alice).Status); + var replacement = Assert.IsType(gate.Acquire(Alice, takeover: true).Lease); + var inputs = new List(); + + Assert.False(gate.TryDispatch(first, () => inputs.Add("stale"))); + Assert.False(gate.Disconnect(first)); + Assert.False(gate.CloseWorkspace(first)); + Assert.True(gate.TryDispatch(replacement, () => inputs.Add("current"))); + Assert.Equal(new[] { "current" }, inputs); + Assert.True(replacement.Generation > first.Generation); + } + + [Fact] + public void DisconnectionPreservesOwnerAndAllowsOwnerToReconnect() + { + var gate = new HostConnectionGate(); + var lease = Assert.IsType(gate.Acquire(Alice).Lease); + Assert.True(gate.Disconnect(lease)); + Assert.False(gate.IsCurrent(lease)); + Assert.Equal(AcquisitionStatus.OwnedByAnotherIdentity, + gate.Acquire(new(Alice.Issuer, "bob-id"), takeover: true).Status); + Assert.Equal(AcquisitionStatus.Acquired, gate.Acquire(Alice).Status); + } + + [Fact] + public void IssuerIsPartOfIdentity() + { + var gate = new HostConnectionGate(); + gate.Acquire(Alice); + Assert.Equal(AcquisitionStatus.OwnedByAnotherIdentity, + gate.Acquire(new("https://other.example/", Alice.Subject), takeover: true).Status); + } + + [Fact] + public void ClosedWorkspaceCanBeAssignedToAnotherOwner() + { + var gate = new HostConnectionGate(); + var lease = Assert.IsType(gate.Acquire(Alice).Lease); + Assert.True(gate.CloseWorkspace(lease)); + Assert.Equal(AcquisitionStatus.Acquired, gate.Acquire(new(Alice.Issuer, "bob-id")).Status); + Assert.False(gate.IsCurrent(lease)); + } + + [Fact] + public void OnlyOneConcurrentConnectionAcquiresControl() + { + var gate = new HostConnectionGate(); + var results = new ConcurrentBag(); + Parallel.For(0, 64, _ => results.Add(gate.Acquire(Alice))); + Assert.Single(results, result => result.Status == AcquisitionStatus.Acquired); + Assert.Equal(63, results.Count(result => result.Status == AcquisitionStatus.TakeoverRequired)); + } + + [Fact] + public void LeaseFromAnotherGateCannotDispatch() + { + var gate = new HostConnectionGate(); + var other = new HostConnectionGate(); + gate.Acquire(Alice); + var foreign = Assert.IsType(other.Acquire(Alice).Lease); + Assert.False(gate.TryDispatch(foreign, () => Assert.Fail("Foreign lease dispatched input."))); + } +} diff --git a/tests/WorkspaceAccessHost.Tests/WorkspaceAccessHost.Tests.csproj b/tests/WorkspaceAccessHost.Tests/WorkspaceAccessHost.Tests.csproj new file mode 100644 index 0000000..7a7d402 --- /dev/null +++ b/tests/WorkspaceAccessHost.Tests/WorkspaceAccessHost.Tests.csproj @@ -0,0 +1,14 @@ + + + Exe + false + true + + + + + + + + + diff --git a/tests/WorkspaceAccessHost.Tests/packages.lock.json b/tests/WorkspaceAccessHost.Tests/packages.lock.json new file mode 100644 index 0000000..9cd589b --- /dev/null +++ b/tests/WorkspaceAccessHost.Tests/packages.lock.json @@ -0,0 +1,487 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.0.1, )", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.0.1", + "Microsoft.TestPlatform.TestHost": "18.0.1" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", + "dependencies": { + "xunit.v3.mtp-v1": "[3.2.2]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "e3IPP32CRNL031VZJAUTlCTG0YN7WFh4mN3fsSHTDQCJB+3+f0jGycv4fXk3rrftaY3B85XrQaj7sRthrOsavg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dAgIf1TOr8KLs+aBRIbXUZBjHoSH4rDG8+XkX/Q6AZwkQdMA0+yPDKTHsieeXdZDfpOpZVHzOnuAb5Z2nX3KsA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "hWWJzJpmg161rYJ2H6vxqCuFfh6zL2Tp9emOrfu1M4gsmW5BOlW8GldthyFwm2L+beW+0axmkMKRw3HyO1qceQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "OIFhLxKAdvSrZuecX177ZkN4AcAB6OLlbS8y0MQYL68512aMOZI8cJLb8OeuYe2X+gu5uMqbbCxhzAmSp+Q2ow==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "PNPS7yYH9U2M0dExn0tMkQFVtE53xTlCs/dgTfsWTTFyCF6qwGT3VrTgm2nnnkSRlSmvM9/J+n9xCpyzd9xTpQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Physical": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "H4u2ZeIhjmRQvbHj7S7U4MK1kXb0khx3UtOWcir32VHczdFyIVF0lvHew/VVHmCF7Uk0QseAUaO7HyFKOCSiKA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "2OtyTIt0/tkChr3inq9Y0c34xogxXW//68gvPzIWgKwyVR4x82RSGVSZfV+ZgG13zrKBzqjaI23wpUCfKpY2yQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Json": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Physical": "10.0.12" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "lXyK2O5GoYvfxW8eCFcD16JFbcoSTM1sJkAM0UHS1jZyl9NYMW64Tqm6OQFT0IDBjZi+xHt95/Zg+nxZhGFhZg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "amx0O/R4dReqKlC3T740+CHpX8D8tAIV0f5kAPfFi4XK9jYo9BSepGt9/rWfvvCoSMaZ1+XvnqPp/Vv2fmTdAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.12", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.12" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "fMp91qlRlMOuHfMyp7LuAnOS+MOeWTF1obJmyFcs4nZCXu6pr+QB1AbFJcl2qpBltBrqF/Y7+Rn9IuNcSuctig==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "iQseV6HixWzEYQH4i++84ToIroXnM5jC1TJF4EqBz3lWNG1HBTMYDLkRb6Wm3wk5V6WqSfl7NukO/LYsloGyfg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "3cY+wo2+Qe65BVwShhr2fFqZv5Ns73h3kX4mpW3WvSgfT3AWCk0hKArKW4TF7Dg3mvbJdA6RhWPW7FR+PS5/1w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "45cV+UeI0chPAIXrx9Wv7K4PJXuuIR2pnOur5MOAo6kOFclBHSLLDrvGAxMfWRMtUvpv/kQZkxlgaUN56jwzSQ==" + }, + "Microsoft.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dzPj59oALFLu9TA4YUrfxqXEcbJVen/D2kOoitgM1lXBeSvPwPR8rOrrV0NKilCV3K0l4o+TLJJ3a1wmTu40Uw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Binder": "10.0.12", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.12", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.12", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.12", + "Microsoft.Extensions.Configuration.Json": "10.0.12", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.12", + "Microsoft.Extensions.DependencyInjection": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Diagnostics": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Physical": "10.0.12", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging.Configuration": "10.0.12", + "Microsoft.Extensions.Logging.Console": "10.0.12", + "Microsoft.Extensions.Logging.Debug": "10.0.12", + "Microsoft.Extensions.Logging.EventLog": "10.0.12", + "Microsoft.Extensions.Logging.EventSource": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "ZlIxIJtWmrph0Ikj0svSaTkZ+d+ZYpTAhazpf1b3aoFRmlNnNWwkcvqnqT9yKHjKvfBRthvUrkq11OxfItOhpg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.12", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "6I46fTPfgYkrjRYfRXbho9WOvOelTnNjWuZws/hzGHDASH1LEJeA4VKK9k3wJvido8o7jJSB5WkMTonX7HM1bA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "CLHWfKQZWFE6fSzRXfjd4UlGveckkfPtwx/p5yF/o3hEkMSzGsl7MSxNAkI19hjw0rprBqrQC09Dwy6Lh6kweA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.12", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Binder": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "nHvaR8gO0CdEfG324KdUsP3Hqgdf1licZb+UlNh5pFYw7+o8QYTmiMgFmwayE/LPSpaqND/Qn3xHa1cI9b8x0w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging.Configuration": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dEvvABq9Qgfu/SgTncXDjCshcJmgO97bLOnMAuqBDSonlk4+VXlKJMHPak+S78TlOxJ47+gY7vPgVukJsr69gg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "Qn0FWPMiQzM4GQEEXxrJbh3vp3I9wrAgpMcsVH36+6FTRGXaqtwHDgf/2030FUJ4OuNSpOeIRh+9kCZGDDsfGg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "System.Diagnostics.EventLog": "10.0.12" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "nKpYoiUdBu8fdLwDxj/fjjGn//7/VwRLzujtMq99/pExrswMXOz+80hbB98QxrD8qPt9oWlFpEVrN9OBKOP7RQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Logging": "10.0.12", + "Microsoft.Extensions.Logging.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "rqpu4qj5WE9x1IHGXSIgHBKi7IUlQaHyp4aXCYIanG2OghlUMFZpZTgExaXwcvmLAJHsxKQWMPpc7D2WIbCVtA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.12", + "Microsoft.Extensions.Configuration.Binder": "10.0.12", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Options": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sOZM+VyRj1pg/ItlHsvrSbjws6oGqZveTzTNBGZ9ziwRDyKsOF3ub6vhvOdxotCIqtlnyHAIwP5sLNvgiZC+pQ==" + }, + "System.ServiceProcess.ServiceController": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "sFV+RM/yPtshiCAEIuTm8Dt4F7VuKRbL50PnGC9nXHolk/jw+z3T8zx7axKTBC8mqtQU2ZmjsoPC9b1iwOilNQ==", + "dependencies": { + "System.Diagnostics.EventLog": "10.0.12" + } + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", + "dependencies": { + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", + "dependencies": { + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", + "dependencies": { + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" + } + }, + "workspaceaccesshost": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Hosting.Systemd": "[10.0.12, )", + "Microsoft.Extensions.Hosting.WindowsServices": "[10.0.12, )" + } + }, + "Microsoft.Extensions.Hosting.Systemd": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "WC1u8RbTqJTY/JknZFHX0+TDptgtpoBRc8Kro0xPSwunMql3e1zBxnK+OJbx30ozSCfMpHaYAfaeiVQgM5sizw==", + "dependencies": { + "Microsoft.Extensions.Hosting": "10.0.12" + } + }, + "Microsoft.Extensions.Hosting.WindowsServices": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "Za5xCWb7jkHd7+hYu8C22uIf0DGhNASIiXo1xSU450nj01RFCerP06h1Bh3kxUm+5DijfgP0gtpY6v+oI/UI4g==", + "dependencies": { + "Microsoft.Extensions.Hosting": "10.0.12", + "Microsoft.Extensions.Logging.EventLog": "10.0.12", + "System.ServiceProcess.ServiceController": "10.0.12" + } + } + } + } +} \ No newline at end of file