Skip to content

Profile-based auth & config (config group, Dataverse provider, MCP contract)#12

Merged
TomProkop merged 52 commits intomasterfrom
tp/config-auth-profiles
Apr 23, 2026
Merged

Profile-based auth & config (config group, Dataverse provider, MCP contract)#12
TomProkop merged 52 commits intomasterfrom
tp/config-auth-profiles

Conversation

@TomProkop
Copy link
Copy Markdown
Member

Summary

Introduces a profile-based authentication and configuration system that decouples service endpoints (Connections), identities (Credentials), and their named pairing (Profiles). Closes out the full 20-milestone plan tracked on tp/config-auth-profiles.

Goals driving the design:

  • OS-vault-backed credentials; nothing sensitive in config files or source control.
  • Deterministic resolution order: --profile flag > TXC_PROFILE env > <repo>/.txc/workspace.json > global active pointer.
  • Headless-first: fail fast (no interactive prompts) whenever stdin is non-TTY or TXC_NON_INTERACTIVE=1.
  • TXC_CONFIG_DIR for total isolation on CI runners.
  • Dataverse-only provider in v1 with pac-parity WIF (GitHub OIDC, ADO TXC_ADO_ID_TOKEN_REQUEST_*, env-var SPN).

Command surface

txc config (alias: c)
├── auth          login | add-service-principal | list | show | delete
├── connection    create | list | show | delete
├── profile (p)   create | update | list | show | select | delete | pin | unpin | validate
└── setting       set | get | list

Every leaf command that touches a live environment takes --profile <name> (short -p). The old --environment <url> / --connection-string flags are gone — the only authoring surface for endpoints is config connection create.

Key changes

  • TALXIS.CLI.Config — core primitives (Credential/Connection/Profile records), file-backed stores, resolver with env/workspace/global layering, MSAL-backed vault (DPAPI / Keychain / libsecret).
  • TALXIS.CLI.Config.Providers.Dataverse — authority map, scope, MSAL factory, pac-parity federated assertion callbacks (GitHub OIDC + ADO WIF + env SPN).
  • TALXIS.CLI.Config.Commands — full command tree under config (20 leaf commands).
  • Dataverse refactor — package-import, solution-import, CMT commands now derive from ProfiledCliCommand and resolve ServiceClient through the runtime connection factory. Secrets are stripped from subprocess request DTOs and re-resolved in the worker via the same vault path.
  • Logging — hardened LogRedactionFilter (Bearer, Authorization header, bare JWT, connection-string keys, URL query-param secrets) applied at the JsonStderrLogger sink so no log frame escapes the process unredacted.
  • MCPTXC_NON_INTERACTIVE=1 forced on every subprocess; per-call profile argument auto-exposed on every ProfiledCliCommand-derived tool via the input-schema reflection path; auth contract documented in src/TALXIS.CLI.MCP/README.md.
  • Docsdocs/mcp-http-auth-notes.md (forward-compat design for the eventual HTTP transport: resource-server-only, RFC 9728 + RFC 8707, 6 forbidden patterns); README.md rewritten with interactive and CI walkthroughs; CONTRIBUTING.md documents the five-group taxonomy with config sub-nouns.

Tests

  • 364 unit tests (new: redaction filter hardening, MCP profile-argument contract, provider factories, vault round-trips, headless gates, resolution precedence, command handlers).
  • 26 integration tests including ProfileEndToEndTests — full cross-process lifecycle (connection create → add-service-principal via --secret-from-env → profile create/list/select/show → delete cascade → missing-profile fail-fast) under isolated TXC_CONFIG_DIR.
  • All green. 5 skipped (env-gated, Keychain/libsecret interactive prompts).

Out of scope / deferred

  • Additional provider stubs (Azure, Entra, Graph) — explicitly descoped for v1. The abstraction will be extracted when a second real provider lands, per CONTRIBUTING.md.
  • MCP HTTP/SSE transport — design-only in docs/mcp-http-auth-notes.md; no code.

Reviewer notes

  • The plan and per-milestone context live in the commit history (25 commits). Suggested review order: core → vault → providers → config commands → Dataverse refactor → redaction → MCP → docs.
  • CONTRIBUTING.md is the source of truth for taxonomy/verbs/selector conventions — please call out any command or flag that drifts from it.
  • Secrets boundary: please sanity-check that no new code path logs raw tokens or writes secrets outside the vault. The redaction filter is last-chance; primary discipline is not to construct the string in the first place.

TomProkop and others added 25 commits April 22, 2026 18:43
Milestone 1 (config-core) of the profile/connection/credential plan.

New project src/TALXIS.CLI.Config:
- Model: Profile, Connection, Credential, SecretRef (vault:// URI), ProviderKind,
  CredentialKind, CloudInstance, GlobalConfig, WorkspaceConfig.
- Abstractions: IConfigurationResolver, IProfileStore, IConnectionStore,
  ICredentialStore, IGlobalConfigStore, IConnectionProvider, ICredentialVault,
  IWorkspaceDiscovery, IHeadlessDetector.
- Storage: file-backed JSON stores with atomic write (temp + File.Replace),
  camelCase properties, kebab-case string enums, ignore-null on write,
  forward-compat [JsonExtensionData] on Connection.
- Resolution: 5-layer ConfigurationResolver (flag > TXC_PROFILE > workspace
  file > global active > throws), cwd-walk WorkspaceDiscovery,
  IEnvironmentReader for test injection.
- Headless: HeadlessDetector (TXC_NON_INTERACTIVE, CI/GITHUB_ACTIONS/TF_BUILD,
  stdin+stdout both redirected).
- DI: AddTxcConfigCore() and static TxcServices service locator (DotMake
  CliCommand instances cannot take constructor dependencies).

Also:
- Delete unreachable src/TALXIS.CLI.Workspace/ConfigCliCommand.cs stub; the
  replacement lives under 'txc config profile validate' (future milestone).
- Add Config test suite (42 tests): SecretRef parse/format, ProfileStore /
  ConnectionStore / CredentialStore / GlobalConfigStore roundtrips,
  WorkspaceDiscovery upward walk, PrecedenceTests (all 5 layers plus missing-
  reference errors), HeadlessDetectorTests.

Build: clean. Tests: 199/199 pass.

Vault registration (ICredentialVault) and MSAL cache wiring are deferred to
milestone 2 (config-vault).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ret)

Implements milestone 2 (config-vault): an ICredentialVault backed by
Microsoft.Identity.Client.Extensions.Msal, the same library pac CLI
uses. Secrets are stored as a single JSON dictionary keyed by
"{credentialId}::{slot}" per cache file, encrypted by the OS
(DPAPI / Keychain / libsecret).

- Add NuGet refs: Microsoft.Identity.Client 4.83.3 and
  Microsoft.Identity.Client.Extensions.Msal 4.74.0.
- Vault/VaultOptions: locked filenames (txc.secrets.v1.dat plus reserved
  txc.msal.tokens.v1.dat), Keychain service "com.talxis.txc", Linux
  keyring schema/label/attrs. Honors TXC_PLAINTEXT_FALLBACK on Linux
  and TXC_TOKEN_CACHE_MODE=file on macOS as explicit opt-ins.
- Vault/MsalCacheHelperFactory: builds StorageCreationProperties,
  calls VerifyPersistence, fail-fast on Win/mac, plaintext-fallback
  path on Linux via WithUnprotectedFile plus chmod 600 and a warning.
- Vault/MsalBackedCredentialVault: Get/Set/Delete over the JSON blob
  with a single SemaphoreSlim; CrossPlatLock in MsalCacheHelper handles
  cross-process. Registered as a DI singleton so the helper is built
  once per process (minimizes macOS Keychain prompts).
- Vault/VaultUnavailableException: canonical remedy message.
- 21 new tests against the plaintext path (doesn't touch host
  Keychain/DPAPI/libsecret); full suite 220/220 green.
- Manual smoke on macOS: round-trip Set/Get/Delete succeeded; Keychain
  entry (svce=com.talxis.txc, acct=secrets) verified via
  `security find-generic-password`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…elper

Completes milestone 3 (config-headless): standardized fail-fast error
for interactive-only credential kinds attempted in headless / CI
contexts. Detector itself was already present from milestone 1.

- Headless/HeadlessAuthRequiredException: carries the attempted
  CredentialKind + headless reason. Deterministic message lists all
  permitted headless kinds (client-secret, client-certificate,
  managed-identity, workload-identity-federation, azure-cli, pat) and
  the exact env vars / profile command needed to re-run non-interactively.
- Abstractions/IHeadlessDetector: adds EnsureKindAllowed extension
  that throws HeadlessAuthRequiredException when headless + kind is
  interactive-browser or device-code.
- 7 new tests; full suite 227/227 green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…SAL factory)

Completes milestone 4 (provider-dataverse). New project
'TALXIS.CLI.Config.Providers.Dataverse' wires the Dataverse IConnectionProvider
with all pac 2.6.3 parity constants pinned in one place.

- Authority/DataverseCloudMap: CloudInstance -> authority host
  (Public/Gcc, GccHigh/Dod, China) + host-suffix inference from
  EnvironmentUrl. Mirrors bolt.authentication.AuthorityInfo.
- Authority/AuthorityChallengeResolver: unauthenticated WhoAmI probe,
  parses authorization_uri from WWW-Authenticate Bearer challenge
  (pac IAuthorityResolver behaviour).
- Scopes/DataverseScope: builds the mandatory '//.default' double-slash
  scope for Dataverse audiences.
- Msal/DataverseMsalClientFactory: pinned public client id
  9cee029c-6210-4654-90bb-17e6e9d36617 + http://localhost redirect;
  confidential builder supports secret, certificate (sendX5C:true),
  and client-assertion callback for GitHub OIDC / ADO federation.
  validateAuthority:false throughout.
- Msal/DataverseTokenCacheBinder: singleton MsalCacheHelper around the
  reserved txc.msal.tokens.v1.dat file (separate from generic secret
  vault by design — see keychain-prompt-research.md).
- DataverseConnectionProvider: IConnectionProvider impl with structural
  ValidateAsync (url + kind + authority buildability). Token acquisition
  and WhoAmI land when auth commands come online.
- DependencyInjection/AddTxcDataverseProvider().

InternalsVisibleTo to Providers.Dataverse on TALXIS.CLI.Config so the
shared MsalCacheHelperFactory can be reused.

35 new tests; full suite 262/262 green. Legacy DataverseAuthTokenProvider
retained for now (retired in refactor-dataverse-commands, milestone 12).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add FederatedAssertionCallbacks with three WIF flavours for Dataverse
confidential clients:

  * ADO pipelines: reads TXC_ADO_ID_TOKEN_REQUEST_URL/TOKEN
    (pac-compatible PAC_ADO_* env vars also honored, matching
    microsoft/powerplatform-build-tools#884 and #906). POSTs with
    System.AccessToken bearer and parses oidcToken from the response.
  * GitHub Actions: reads ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN, appends
    audience=api://AzureADTokenExchange query param.
  * Workload-identity file: AZURE_FEDERATED_TOKEN_FILE content.

AutoSelect() picks the first configured source in that priority. All
return raw JWTs that feed MSAL WithClientAssertion.

6 unit tests (HttpMessageHandler stub, tmp file for file-based),
268/268 green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create TALXIS.CLI.Config.Commands project with the 'config' and
'config auth' command groups plus three no-MSAL verbs:

  * auth list      - JSON dump of all credentials (no secrets)
  * auth show <alias> - single credential as JSON; exit 2 if missing
  * auth delete <alias> - removes entry + vault secret; leaves
    dependent profiles orphaned with a warning (pac-auth-clear parity)

The command tree is defined but not yet wired into TxcCliCommand.Children
(deferred to the wire-toplevel milestone per CONTRIBUTING's 'invisible
scaffolding' convention). Commands are tested directly by constructing
the handler classes and driving TxcServices with an in-memory fake
vault.

Login / add-service-principal / add-federated verbs land in follow-up
commits.

6 tests; 274/274 green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Interactive browser sign-in via MSAL (Dataverse provider). Persists an
InteractiveBrowser credential whose refresh token lives in the shared
MSAL cache; no secret enters the txc credential vault.

- IInteractiveLoginService abstraction + DataverseInteractiveLoginService
  (scopes: openid profile offline_access, UserTokenCache bound via
  DataverseTokenCacheBinder so silent acquires work later).
- AuthLoginCliCommand with --tenant / --alias / --cloud. Fails fast in
  headless contexts via IHeadlessDetector.EnsureKindAllowed.
- Default alias = lowercased UPN, collision-resolved via tenant short
  name then numeric suffix.
- Tests cover happy path, explicit overrides, collision resolution, and
  headless refusal. CommandTestHost gains fakes for IHeadlessDetector
  and IInteractiveLoginService.
- TxcServicesSerial collection serialises command tests that touch the
  process-global TxcServices.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Register a client-secret service principal. The secret value is read
from --secret-from-env, then piped stdin, then an interactive masked
TTY prompt — and only a SecretRef handle is persisted in the
credential store (secret itself goes to the OS credential vault).

- AuthAddServicePrincipalCliCommand with --alias, --tenant,
  --application-id (required) plus --cloud, --description,
  --secret-from-env (optional). Aliased as 'add-sp'.
- ReadSecret helper with a StdinOverride test seam.
- Tests cover env-var happy path, missing env var, piped stdin, and
  headless-allowed behaviour (ClientSecret is a permitted headless kind).
- Wired into AuthCliCommand.Children.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Dataverse-only in v1: --provider rejects non-dataverse with exit 1.
create validates http(s) URL, trims trailing slash, validates GUID org id.
delete fails exit 3 when referenced by a profile; --force-orphan-profiles
opts into orphan-with-warning (pac-auth-clear parity). show/delete exit 2
when missing. 13 new tests; 302/302 green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…elete

Refs-only create (--auth + --connection reference existing primitives;
inline-create shortcuts deferred per scope). First profile is
auto-promoted to the global active pointer; subsequent creates do not
touch it. show without <name> reports the active profile and inline
expands connection+credential so scripts don't need three round-trips.
delete clears the active pointer when the deleted profile was active;
--cascade removes auth+connection only if no other profile uses them
(vault secret purged best-effort).

20 new tests; 322/322 green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pin writes <cwd>/.txc/workspace.json with {defaultProfile}. With no
<name> it pins the global active profile; with <name> it pins the
specified profile (must exist). The workspace pin sits below --profile
and TXC_PROFILE in the precedence chain but beats the global active
pointer.

unpin removes the workspace file and cleans up an empty .txc
directory. Idempotent when no pin exists.

8 new tests exercise cwd isolation, idempotency, sibling-file
preservation, and exit-2 branches. 330/330 green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds ValidationMode (Structural | Live) to IConnectionProvider so the
profile validate command can explicitly request a live authenticated
round-trip. Default behavior: live check. Pass --skip-live for
structural-only (fast, offline-safe).

Dataverse provider now delegates live-mode to an injectable
IDataverseLiveChecker. Default registration is a placeholder that
fails with a precise remedy message until the real WhoAmI HTTP path
lands in milestone refactor-dataverse-commands. Tests inject a
FakeConnectionProvider via a new CommandTestHost seam, asserting exit
0/1/2 across missing profile, missing connection, provider throw,
named-override, structural+live modes, and JSON shape.

TxcServices gains GetAll<T>() for provider lookup by kind.

8 new tests; 338/338 green. Completes milestone 8 (cmd-profile):
create/list/show/update/select/pin/unpin/validate/delete.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements milestone 9 (cmd-setting): narrow whitelist-driven verbs over
${TXC_CONFIG_DIR}/config.json for tool-wide preferences.

- SettingRegistry: one descriptor per whitelisted key (log.level,
  log.format, telemetry.enabled) with per-key value validation.
- set: unknown keys and invalid values exit 2 with remedy.
- get: prints current value (default if unset); exit 2 for unknown key.
- list: JSON dump of all known keys with current values + allowed set.
- ConfigCliCommand now exposes the setting group.

Tests: 18 new (356 total).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Updates CONTRIBUTING.md for milestone 10 (contributing-md):
- Top-level groups go from four to five; config joins workspace,
  environment, data, docs as a first-class group.
- New 'config sub-nouns' section documents the auth/connection/profile/
  setting split and why the separation is deliberate.
- Alias table adds config=c and config profile=p; documents the
  --profile=-p short-alias exception (the only flag-level short in the
  CLI, justified by the frequency of profile-switching).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements milestone 11 (wire-toplevel):
- TxcCliCommand.Children now includes ConfigCliCommand so 'txc config'
  is invocable and MCP tools/list auto-discovers config_auth_*,
  config_connection_*, config_profile_*, config_setting_* tools.
- ConfigCliCommand grows alias 'c' (per CONTRIBUTING taxonomy).
- Program.Main bootstraps TxcServices with AddTxcConfigCore() and
  AddTxcDataverseProvider() before DotMake dispatch; the
  PackageDeployer subprocess branch keeps its own init path.
- TALXIS.CLI.csproj picks up ProjectReferences for Config,
  Config.Commands, Config.Providers.Dataverse.

Also fixes a latent bug that made every config leaf command
unreachable via the CLI: DotMake v2.6.7 does not auto-bind
CancellationToken parameters on handler methods, so
'public async Task<int> RunAsync(CancellationToken ct = default)'
was generating a handler that couldn't bind and DotMake silently
fell back to showing help. All 21 config commands now use
'RunAsync()' and pass CancellationToken.None downstream. Tests
(which call RunAsync() directly) unaffected: 356/356 green.

End-to-end smoke (verified):
  txc config setting set log.level debug   -> exit 0, value written
  txc config setting get log.level         -> prints 'debug'
  txc c p list                             -> prints '[]'
  txc c setting set bogus x                -> exit 2, structured error

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ProfiledCliCommand: abstract CliCommand base with --profile (-p) + --verbose
- IDataverseAccessTokenService / DataverseAccessTokenService: MSAL-driven
  token acquisition for InteractiveBrowser, ClientSecret, ClientCertificate,
  and WorkloadIdentityFederation credential kinds
- IDataverseConnectionFactory / DataverseConnectionFactory: build a
  ServiceClient-backed DataverseConnection from a ResolvedProfileContext
- DataverseLiveChecker: real WhoAmI-based implementation that replaces
  the NotYetImplementedDataverseLiveChecker stub
- DataverseConnection.FromServiceClient public factory on TALXIS.CLI.Dataverse
- DI wiring: Config.Providers.Dataverse registers the new services; the
  stub live checker is removed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erviceClientFactory surface

- Move DataverseCommandBridge from Environment/Platforms/Dataverse to
  Config.Providers.Dataverse/Runtime so Data project can reuse it without
  circular refs. Public static, exposes ConnectAsync/ResolveAsync/
  BuildConnectionStringAsync (SP-only, pending package-deployer-subprocess
  milestone).
- Refactor 8 Dataverse-touching leaf commands to ProfiledCliCommand base:
  Solution List/Import/Uninstall, Deployment List/Show, Package Import/
  Uninstall, Data Package Import. Removes --connection-string,
  --environment, --device-code, --verbose-plumbed-as-auth from each.
- Package Import + Data Package Import synthesize SP connection strings
  via BuildConnectionStringAsync; other credential kinds throw
  NotSupportedException with pointer to package-deployer-subprocess.
- Delete ServiceClientFactory legacy surface (Connect, ResolveConnectionString,
  ResolveEnvironmentUrl, Create) + four DATAVERSE_*/TXC_DATAVERSE_* env var
  constants. ServiceClientFactory.cs now holds only the DataverseConnection
  wrapper + FromServiceClient factory used by IDataverseConnectionFactory.

All 356 unit + 24 integration tests remain green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…subprocess

Milestone 13 (package-deployer-subprocess). The coordinator JSON previously
carried a fully-materialised Dataverse connection string (including
ClientSecret) in plaintext on disk at
/tmp/txc/package-deployer-process/<guid>/request.json. Replaced with an
identity-neutral transport shape:

  PackageDeployerRequest {
    PackagePath, ProfileId, ConfigDirectory?, Settings?, LogFile?,
    LogConsole, Verbose, TemporaryArtifactsDirectory?, ParentProcessId,
    NuGetPackageName?, NuGetPackageVersion?
  }

The child:
1. Sets TXC_CONFIG_DIR from request.ConfigDirectory so vault + profile
   files resolve identically to the parent.
2. Calls TxcServicesBootstrap.EnsureInitialized() — new shared helper
   extracted from Program.InitializeConfigServices so both the main CLI
   pipeline and the subprocess wire the same DI graph.
3. Re-resolves the SP connection string in-memory via
   DataverseCommandBridge.BuildConnectionStringAsync(profileId, ct).
4. Hands it to PackageDeployerRunner.RunAsync(request, connectionString, ct)
   — new signature; the runner no longer reads ConnectionString /
   EnvironmentUrl / DeviceCode off the request.

Parent-side (PackageImportCliCommand) now just forwards the --profile
value as ProfileId; no in-process SP resolution. Additionally, the
request.json file is chmod 0600'd on Unix; Windows relies on per-user
%TEMP% ACLs.

All 356 unit + 24 integration tests remain green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Milestone 14 (cmt-import-symmetry). CmtImportRequest drops
ConnectionString / EnvironmentUrl / DeviceCode. CmtImportRunner.RunAsync
now takes (request, connectionString, ct) mirroring
PackageDeployerRunner.RunAsync. Parallel-import clone loop uses the same
in-memory connectionString parameter instead of re-reading it off the
request. Dead DataverseInteractiveAuthHook plumbing removed.

DataPackageImportCliCommand still resolves the SP connection string via
DataverseCommandBridge.BuildConnectionStringAsync (in-process — no
subprocess to bridge) and forwards it to the runner.

356 unit + 24 integration tests remain green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er sink

Milestone 15 (redaction-hardening). Patterns added:
- Bearer <token> (case-insensitive)
- Authorization: <value> (full line value)
- Bare JWTs (header.payload.signature)
- Extended connection-string secret keys: ApplicationSecret,
  ApplicationPassword, AccessToken, RefreshToken, IdToken, SasToken,
  ApiKey, Api_Key
- Extended query-param secret keys: access_token, refresh_token, id_token

JsonStderrLogger now runs both the composed exception message and string
values in the structured-data dictionary through Redact before emitting
the JSON line to stderr. This is the belt-and-braces layer that catches
accidental 'logger.LogError(ex, ...)' leaks where the Dataverse /
Microsoft.Identity SDKs put a connection string or token into the
exception Message or StackTrace.

Added 6 new LogRedactionFilter tests covering the new patterns. Total
test count: 356 -> 362, all green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Milestone 16 (tests). Exercises the full 'txc config' lifecycle end-to-end
through the real CLI subprocess with an isolated TXC_CONFIG_DIR:
connection create -> auth add-service-principal (via --secret-from-env)
-> profile create -> profile list -> profile select -> profile show ->
delete cascade.

Config unit test coverage (resolution, storage, vault, headless,
Dataverse provider, commands) was already extensive under
tests/TALXIS.CLI.Tests/Config -- no gaps identified there. The
integration gap was the absence of a cross-process test that proves the
actual CLI binary wires the command tree, MSAL vault, and file-backed
stores together correctly under TXC_CONFIG_DIR isolation. This test
plugs that gap without requiring a live Dataverse environment (it
stops short of 'profile validate', which would dial out).

CliRunner gains an optional 'env' overrides dictionary on RunAsync /
RunRawAsync so tests can inject TXC_CONFIG_DIR / TXC_NON_INTERACTIVE /
TXC_PLAINTEXT_FALLBACK / TXC_TOKEN_CACHE_MODE without touching the
developer's real ~/.txc.

Integration test count: 24 -> 26.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tract

Milestone 17 (mcp-auth-contract).

CliSubprocessRunner now unconditionally sets TXC_NON_INTERACTIVE=1 on
every CLI subprocess it spawns. This is the single hard guarantee that
MCP tool handlers cannot invoke interactive MSAL (browser / device code)
or console prompts (masked secret read). The stdio transport reserves
stdout for JSON-RPC frames, so any interactive flow would either hang
the session or corrupt the transport. Headless detection in the CLI
then rejects interactive credential kinds and fails fast with a clear
remedy string instead.

src/TALXIS.CLI.MCP/README.md gains an 'Auth contract (stdio)' section
documenting:
- the non-interactive invariant and its rationale,
- the three prerequisite steps (login / add-sp -> connection create ->
  profile create + select),
- per-call 'profile' override shape (forwarded as --profile),
- env allow-list consumed from mcpServers.<name>.env (TXC_PROFILE,
  TXC_CONFIG_DIR, AZURE_*, ACTIONS_ID_TOKEN_*, TXC_ADO_ID_TOKEN_*),
- log redaction guarantees (Bearer / Authorization / JWT / conn-string
  secret keys / URL query-param secrets),
- forbidden patterns for future HTTP transport (token passthrough,
  tokens-in-URIs, missing PKCE, missing audience check).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Milestone 18 (mcp-profile-argument). No production code changes needed:
the MCP adapter reflects [CliOption] properties via
BindingFlags.Public | BindingFlags.Instance, which already includes
inherited properties. So every command deriving from ProfiledCliCommand
(all eight Dataverse-touching leaves) automatically surfaces 'profile'
(and 'verbose') in its MCP tool input schema, and BuildCliArgs forwards
them as --profile / --verbose.

Added CliCommandAdapterProfileArgumentTests pinning this contract:
1. BuildInputSchema on a ProfiledCliCommand-derived command exposes
   'profile' as an optional string property (not in 'required').
2. BuildCliArgs forwards { profile: 'customer-a-dev' } as
   '--profile customer-a-dev' to the CLI subprocess.

This prevents a future refactor from silently breaking per-session
profile-switching for MCP clients. The (sessionId, profile) credential-
resolution shape from the plan is effectively (constant, profile)
today under stdio; when HTTP transport lands, only sessionId needs to
become dynamic -- the profile axis is already in place.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Milestone 19 (mcp-http-readiness). docs/mcp-http-auth-notes.md records
the invariants + forbidden patterns that will apply when the HTTP/SSE
MCP transport eventually ships, so today's stdio implementation does
not paint future work into a corner.

Covers: resource-server-only role (Entra is the AS), RFC 9728 protected-
resource metadata, RFC 8707 audience binding, JWT validation rules +
WWW-Authenticate challenge, Mcp-Session-Id + MCP-Protocol-Version,
per-session (sessionId, profile) credential resolution, and the six
hard-forbidden patterns (token passthrough, tokens-in-URIs, non-HTTPS
redirects, missing PKCE, missing audience check, caching federation
tokens). Also documents transport-security defaults (127.0.0.1 bind,
Origin validation, HSTS, CORS off).

No production code changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Milestone 20 (docs). README had stale --environment URL flags on every
example — these flags no longer exist on leaf commands. Replace the
smoke-test-style snippets with the profile-based flow documented in
CONTRIBUTING and the MCP README.

New sections:
- 'Identity, Connections & Profiles' — explains the three primitives,
  the resolution order (--profile > TXC_PROFILE > .txc/workspace.json >
  global pointer), and where secrets live (OS vault, not config files).
- Interactive workflow: auth login -> connection create -> profile
  create -> profile select, plus pin/unpin for per-repo defaults and
  profile validate for end-to-end sanity check.
- Headless / CI workflow: TXC_CONFIG_DIR isolation, TXC_NON_INTERACTIVE,
  add-service-principal with --secret-from-env (never --secret inline),
  and a note about WIF auto-detection via env vars.
- Updated example commands to drop --environment everywhere and show
  the per-call '-p <profile>' override once.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a profile-based auth/config system for txc, splitting endpoint metadata (Connections), identities (Credentials), and their binding (Profiles), and refactors Dataverse-touching commands to resolve runtime connections via the new resolver/vault pipeline (including MCP headless behavior and log redaction).

Changes:

  • Adds new TALXIS.CLI.Config* projects (core model/stores/resolution/headless/vault + Dataverse provider + txc config command tree).
  • Refactors Dataverse commands and subprocess contracts to remove secrets from request DTOs and re-resolve credentials via profile/vault.
  • Hardens logging redaction and MCP subprocess behavior (forced headless + documented auth contract), plus extensive unit/integration tests.

Reviewed changes

Copilot reviewed 147 out of 147 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/TALXIS.CLI.Tests/TALXIS.CLI.Tests.csproj Adds references so tests can cover new config/provider/command projects.
tests/TALXIS.CLI.Tests/MCP/CliCommandAdapterProfileArgumentTests.cs Verifies MCP schema/arg forwarding exposes per-call profile.
tests/TALXIS.CLI.Tests/Logging/LogRedactionFilterTests.cs Expands coverage for bearer/header/JWT/connection-string/query redaction.
tests/TALXIS.CLI.Tests/Config/Vault/VaultUnavailableExceptionTests.cs Adds tests for canonical vault-unavailable remedy messaging.
tests/TALXIS.CLI.Tests/Config/Vault/VaultOptionsTests.cs Tests env-var-driven vault plaintext fallback selection.
tests/TALXIS.CLI.Tests/Config/TempConfigDir.cs Test helper for isolated TXC_CONFIG_DIR layouts.
tests/TALXIS.CLI.Tests/Config/Storage/SecretRefTests.cs Tests SecretRef URI formatting/parsing/validation.
tests/TALXIS.CLI.Tests/Config/Storage/ProfileStoreRoundtripTests.cs Roundtrip/case-insensitivity tests for profile store.
tests/TALXIS.CLI.Tests/Config/Storage/GlobalConfigStoreRoundtripTests.cs Roundtrip tests for global active-profile pointer.
tests/TALXIS.CLI.Tests/Config/Storage/CredentialStoreRoundtripTests.cs Ensures SecretRef serializes as URI and nulls are omitted.
tests/TALXIS.CLI.Tests/Config/Storage/ConnectionStoreRoundtripTests.cs Roundtrip tests incl. forward-compat extension-data retention.
tests/TALXIS.CLI.Tests/Config/Resolution/WorkspaceDiscoveryTests.cs Tests .txc/workspace.json upward discovery semantics.
tests/TALXIS.CLI.Tests/Config/Providers/Dataverse/FederatedAssertionCallbacksTests.cs Tests ADO/GitHub/file federated assertion acquisition behavior.
tests/TALXIS.CLI.Tests/Config/Providers/Dataverse/DataverseScopeTests.cs Verifies Dataverse //.default scope construction.
tests/TALXIS.CLI.Tests/Config/Providers/Dataverse/DataverseMsalClientFactoryTests.cs Tests MSAL client creation/authority selection rules.
tests/TALXIS.CLI.Tests/Config/Providers/Dataverse/DataverseConnectionProviderTests.cs Validates provider kind + supported credential kinds + structural checks.
tests/TALXIS.CLI.Tests/Config/Providers/Dataverse/DataverseCloudMapTests.cs Tests sovereign-cloud inference/mapping.
tests/TALXIS.CLI.Tests/Config/Providers/Dataverse/AuthorityChallengeResolverTests.cs Tests parsing/behavior for WWW-Authenticate authority challenge resolution.
tests/TALXIS.CLI.Tests/Config/Headless/HeadlessDetectorTests.cs Tests headless detection (redirect/env/CI signals).
tests/TALXIS.CLI.Tests/Config/Headless/HeadlessAuthRequiredExceptionTests.cs Tests permitted kinds + deterministic failure messaging.
tests/TALXIS.CLI.Tests/Config/Commands/TxcServicesSerialCollection.cs Ensures tests touching process-global service locator run serially.
tests/TALXIS.CLI.Tests/Config/Commands/Auth/AuthCrudCommandsTests.cs Tests CRUD behaviors for txc config auth list/show/delete.
tests/TALXIS.CLI.Tests/Config/Commands/Auth/AuthAddServicePrincipalCommandTests.cs Tests SP credential creation via env-var/pipe + vault persistence.
tests/TALXIS.CLI.IntegrationTests/CliRunner.cs Adds env override support for cross-process integration tests.
src/TALXIS.CLI/TxcCliCommand.cs Wires config command group into top-level CLI taxonomy.
src/TALXIS.CLI/TALXIS.CLI.csproj Adds references to config/provider/command projects.
src/TALXIS.CLI/Program.cs Bootstraps config DI (service locator) once per process.
src/TALXIS.CLI.XrmTools/PackageDeployerRunner.cs Removes interactive auth; runner now requires an explicit connection string input.
src/TALXIS.CLI.XrmTools/PackageDeployerRequest.cs Replaces secret-bearing fields with profile/config-dir pointers for subprocess contract.
src/TALXIS.CLI.XrmTools/CmtImportRunner.cs Removes interactive auth; runner now requires explicit connection string input.
src/TALXIS.CLI.XrmTools/CmtImportRequest.cs Removes secret-bearing fields from request DTO; moves connection string to runner param.
src/TALXIS.CLI.Workspace/ConfigCliCommand.cs Deletes legacy workspace config placeholder group.
src/TALXIS.CLI.MCP/README.md Documents stdio auth contract, per-call profile override, env allow-list, and redaction.
src/TALXIS.CLI.MCP/CliSubprocessRunner.cs Forces TXC_NON_INTERACTIVE=1 for MCP-spawned subprocesses.
src/TALXIS.CLI.Logging/LogRedactionFilter.cs Expands redaction patterns (bearer/header/JWT/connection-string/query-param secrets).
src/TALXIS.CLI.Logging/JsonStderrLogger.cs Applies redaction at stderr sink (message + string state values).
src/TALXIS.CLI.Environment/TALXIS.CLI.Environment.csproj Adds references to config/provider/command projects for env commands.
src/TALXIS.CLI.Environment/Solution/SolutionUninstallCliCommand.cs Converts to ProfiledCliCommand and resolves Dataverse via profile bridge.
src/TALXIS.CLI.Environment/Solution/SolutionListCliCommand.cs Converts to ProfiledCliCommand and resolves Dataverse via profile bridge.
src/TALXIS.CLI.Environment/Solution/SolutionImportCliCommand.cs Converts to ProfiledCliCommand and resolves Dataverse via profile bridge.
src/TALXIS.CLI.Environment/Platforms/Dataverse/TxcServicesBootstrap.cs Adds shared DI bootstrap for main process and subprocess helpers.
src/TALXIS.CLI.Environment/Platforms/Dataverse/PackageDeployerSubprocess.cs Re-resolves connection string in child via profile/vault and scopes TXC_CONFIG_DIR.
src/TALXIS.CLI.Environment/Package/PackageUninstallCliCommand.cs Converts to ProfiledCliCommand and resolves Dataverse via profile bridge.
src/TALXIS.CLI.Environment/Package/PackageImportCliCommand.cs Uses profile-based subprocess contract for package deployer invocation.
src/TALXIS.CLI.Environment/Deployment/DeploymentShowCliCommand.cs Converts to ProfiledCliCommand and resolves Dataverse via profile bridge.
src/TALXIS.CLI.Environment/Deployment/DeploymentListCliCommand.cs Converts to ProfiledCliCommand and resolves Dataverse via profile bridge.
src/TALXIS.CLI.Data/TALXIS.CLI.Data.csproj Adds references to config/provider/command projects.
src/TALXIS.CLI.Data/DataPackageImportCliCommand.cs Builds Dataverse connection string from profile and passes it to CMT runner.
src/TALXIS.CLI.Config/Vault/VaultUnavailableException.cs Adds deterministic vault failure exception with user-facing remedy.
src/TALXIS.CLI.Config/Vault/MsalCacheHelperFactory.cs Centralizes MSAL cache helper creation and plaintext fallback policy.
src/TALXIS.CLI.Config/TALXIS.CLI.Config.csproj New config core project (stores/resolution/headless/vault primitives).
src/TALXIS.CLI.Config/Storage/TxcJsonOptions.cs Introduces canonical JSON serialization options for config/system output.
src/TALXIS.CLI.Config/Storage/SecretRefJsonConverter.cs Serializes SecretRef as canonical URI string.
src/TALXIS.CLI.Config/Storage/ProfileStore.cs File-backed profile store with atomic writes + in-process lock.
src/TALXIS.CLI.Config/Storage/JsonFile.cs Shared atomic JSON read/write helpers.
src/TALXIS.CLI.Config/Storage/GlobalConfigStore.cs File-backed global config store (active pointer, prefs).
src/TALXIS.CLI.Config/Storage/CredentialStore.cs File-backed credential store (non-secret fields + secret refs).
src/TALXIS.CLI.Config/Storage/ConnectionStore.cs File-backed connection store with extension-data retention.
src/TALXIS.CLI.Config/Storage/ConfigPaths.cs Resolves config root via TXC_CONFIG_DIR or default ~/.txc.
src/TALXIS.CLI.Config/Resolution/WorkspaceDiscovery.cs Locates .txc/workspace.json by upward directory walk.
src/TALXIS.CLI.Config/Resolution/IEnvironmentReader.cs Testable abstraction over environment and current directory.
src/TALXIS.CLI.Config/Resolution/ConfigurationResolver.cs Implements profile resolution precedence and loads profile/conn/cred triples.
src/TALXIS.CLI.Config/Model/WorkspaceConfig.cs Models workspace default profile pointer file shape.
src/TALXIS.CLI.Config/Model/SecretRef.cs Defines vault URI handles for secrets.
src/TALXIS.CLI.Config/Model/ResolvedProfileContext.cs Represents resolved profile context + source for diagnostics.
src/TALXIS.CLI.Config/Model/ProviderKind.cs Defines provider enum for connections.
src/TALXIS.CLI.Config/Model/ProfileCollection.cs Defines JSON collection wrappers for stores.
src/TALXIS.CLI.Config/Model/Profile.cs Defines Profile record (connectionRef + credentialRef).
src/TALXIS.CLI.Config/Model/GlobalConfig.cs Defines global settings shape (active profile, log, telemetry).
src/TALXIS.CLI.Config/Model/CredentialKind.cs Defines canonical credential kinds.
src/TALXIS.CLI.Config/Model/Credential.cs Defines credential metadata + optional secret ref.
src/TALXIS.CLI.Config/Model/Connection.cs Defines connection metadata + forward-compat extension data.
src/TALXIS.CLI.Config/Model/CloudInstance.cs Defines sovereign cloud enum.
src/TALXIS.CLI.Config/Headless/HeadlessDetector.cs Implements headless/non-interactive detection rules.
src/TALXIS.CLI.Config/Headless/HeadlessAuthRequiredException.cs Adds deterministic exception for forbidden interactive auth in headless mode.
src/TALXIS.CLI.Config/DependencyInjection/TxcServices.cs Adds process-global service locator to bridge command framework DI limitations.
src/TALXIS.CLI.Config/DependencyInjection/ConfigServiceCollectionExtensions.cs Registers config core services (stores/resolver/headless/vault).
src/TALXIS.CLI.Config/Abstractions/IWorkspaceDiscovery.cs Adds workspace discovery abstraction.
src/TALXIS.CLI.Config/Abstractions/IStores.cs Adds store interfaces for profiles/connections/credentials/global config.
src/TALXIS.CLI.Config/Abstractions/IInteractiveLoginService.cs Defines interactive login service boundary.
src/TALXIS.CLI.Config/Abstractions/IHeadlessDetector.cs Defines headless detector + fail-fast extension method.
src/TALXIS.CLI.Config/Abstractions/ICredentialVault.cs Defines OS-backed credential vault interface.
src/TALXIS.CLI.Config/Abstractions/IConnectionProvider.cs Defines provider interface + validation modes.
src/TALXIS.CLI.Config/Abstractions/IConfigurationResolver.cs Defines resolver interface + canonical resolution precedence doc.
src/TALXIS.CLI.Config.Providers.Dataverse/TALXIS.CLI.Config.Providers.Dataverse.csproj New Dataverse provider project.
src/TALXIS.CLI.Config.Providers.Dataverse/Scopes/DataverseScope.cs Builds Dataverse MSAL scope string (//.default).
src/TALXIS.CLI.Config.Providers.Dataverse/Runtime/IDataverseConnectionFactory.cs Defines runtime Dataverse connection factory interface.
src/TALXIS.CLI.Config.Providers.Dataverse/Runtime/IDataverseAccessTokenService.cs Defines access-token acquisition service interface.
src/TALXIS.CLI.Config.Providers.Dataverse/Runtime/DataverseConnectionFactory.cs Builds ServiceClient using token callback from access-token service.
src/TALXIS.CLI.Config.Providers.Dataverse/Runtime/DataverseCommandBridge.cs Bridges CLI commands to resolver + provider runtime (and connection-string transitional path).
src/TALXIS.CLI.Config.Providers.Dataverse/Msal/DataverseTokenCacheBinder.cs Provides process-wide MSAL token cache helper binding.
src/TALXIS.CLI.Config.Providers.Dataverse/Msal/DataverseInteractiveLoginService.cs Implements interactive browser login that primes MSAL cache.
src/TALXIS.CLI.Config.Providers.Dataverse/IDataverseLiveChecker.cs Adds seam for WhoAmI/live validation.
src/TALXIS.CLI.Config.Providers.Dataverse/DependencyInjection/DataverseProviderServiceCollectionExtensions.cs Registers Dataverse provider, token cache binder, runtime services.
src/TALXIS.CLI.Config.Providers.Dataverse/DataverseConnectionProvider.cs Structural/live validation for Dataverse connections/credentials.
src/TALXIS.CLI.Config.Providers.Dataverse/Authority/DataverseCloudMap.cs Maps sovereign clouds and infers cloud from environment URL host suffix.
src/TALXIS.CLI.Config.Providers.Dataverse/Authority/AuthorityChallengeResolver.cs Resolves authority from Dataverse WWW-Authenticate challenge.
src/TALXIS.CLI.Config.Commands/TALXIS.CLI.Config.Commands.csproj New config command project wiring.
src/TALXIS.CLI.Config.Commands/Setting/SettingSetCliCommand.cs Implements txc config setting set.
src/TALXIS.CLI.Config.Commands/Setting/SettingRegistry.cs Central registry of allowed setting keys and validation logic.
src/TALXIS.CLI.Config.Commands/Setting/SettingListCliCommand.cs Implements txc config setting list.
src/TALXIS.CLI.Config.Commands/Setting/SettingGetCliCommand.cs Implements txc config setting get.
src/TALXIS.CLI.Config.Commands/Setting/SettingCliCommand.cs Adds txc config setting group.
src/TALXIS.CLI.Config.Commands/Profile/ProfileValidateCliCommand.cs Implements structural/live txc config profile validate.
src/TALXIS.CLI.Config.Commands/Profile/ProfileUpdateCliCommand.cs Implements txc config profile update.
src/TALXIS.CLI.Config.Commands/Profile/ProfileUnpinCliCommand.cs Implements workspace unpin behavior.
src/TALXIS.CLI.Config.Commands/Profile/ProfileShowCliCommand.cs Implements txc config profile show with expanded refs.
src/TALXIS.CLI.Config.Commands/Profile/ProfileSelectCliCommand.cs Implements txc config profile select.
src/TALXIS.CLI.Config.Commands/Profile/ProfilePinCliCommand.cs Implements workspace pin behavior.
src/TALXIS.CLI.Config.Commands/Profile/ProfileListCliCommand.cs Implements txc config profile list.
src/TALXIS.CLI.Config.Commands/Profile/ProfileDeleteCliCommand.cs Implements txc config profile delete with optional cascade.
src/TALXIS.CLI.Config.Commands/Profile/ProfileCreateCliCommand.cs Implements txc config profile create.
src/TALXIS.CLI.Config.Commands/Profile/ProfileCliCommand.cs Adds txc config profile group.
src/TALXIS.CLI.Config.Commands/Connection/ConnectionShowCliCommand.cs Implements txc config connection show.
src/TALXIS.CLI.Config.Commands/Connection/ConnectionListCliCommand.cs Implements txc config connection list.
src/TALXIS.CLI.Config.Commands/Connection/ConnectionDeleteCliCommand.cs Implements txc config connection delete with orphaning policy.
src/TALXIS.CLI.Config.Commands/Connection/ConnectionCreateCliCommand.cs Implements txc config connection create.
src/TALXIS.CLI.Config.Commands/Connection/ConnectionCliCommand.cs Adds txc config connection group.
src/TALXIS.CLI.Config.Commands/ConfigCliCommand.cs Adds top-level txc config group (alias c).
src/TALXIS.CLI.Config.Commands/Auth/AuthShowCliCommand.cs Implements txc config auth show.
src/TALXIS.CLI.Config.Commands/Auth/AuthListCliCommand.cs Implements txc config auth list.
src/TALXIS.CLI.Config.Commands/Auth/AuthDeleteCliCommand.cs Implements txc config auth delete.
src/TALXIS.CLI.Config.Commands/Auth/AuthCliCommand.cs Adds txc config auth group.
src/TALXIS.CLI.Config.Commands/Abstractions/ProfiledCliCommand.cs Adds shared --profile/-p and --verbose options for profile-bound commands.
CONTRIBUTING.md Updates taxonomy/aliases conventions to include config group and -p exception.

Comment thread src/TALXIS.CLI.Config.Providers.Dataverse/Runtime/DataverseCommandBridge.cs Outdated
Comment thread src/TALXIS.CLI.Config.Commands/Connection/ConnectionCreateCliCommand.cs Outdated
Comment thread src/TALXIS.CLI.Config.Commands/Auth/AuthListCliCommand.cs Outdated
Comment thread src/TALXIS.CLI.Config.Commands/Auth/AuthShowCliCommand.cs Outdated
Comment thread src/TALXIS.CLI.Environment/Package/PackageImportCliCommand.cs Outdated
Comment thread src/TALXIS.CLI.Config.Commands/Profile/ProfilePinCliCommand.cs Outdated
Comment thread src/TALXIS.CLI.Core/Headless/HeadlessAuthRequiredException.cs
…doc fixes

Seven findings from copilot-pull-request-reviewer; all valid.

* Consistent JSON shape across every 'txc config' command. Five
  command sites were constructing 'new JsonSerializerOptions' from
  scratch, which dropped the kebab-case enum converter registered in
  TxcJsonOptions.Default. Result: 'provider', 'cloud', and 'kind'
  serialized as integers instead of the canonical string form used
  everywhere else. Reviewer flagged three; audit found two more
  (auth login, auth add-service-principal). All five now clone
  TxcJsonOptions.Default while keeping the human-readable pretty-print
  + null-omit behavior they want.

* DataverseCommandBridge XML doc no longer overclaims support.
  BuildConnectionStringAsync actually rejects ClientCertificate (it
  needs cert-file plumbing that doesn't exist yet). Doc now says
  ClientSecret only (v1) and points at the follow-up milestone.

* PackageImportCliCommand had an empty 'fail-fast' if block - all
  comments, no behavior. Empty profile IS permitted (resolver falls
  through to TXC_PROFILE / active-profile pointer), so there's
  nothing to fail on. Deleted.

* ProfilePinCliCommand.WriteWorkspaceConfigAsync now honors its 'ct'
  parameter instead of passing CancellationToken.None to SerializeAsync
  / FlushAsync.

* HeadlessAuthRequiredException remedy text no longer points at
  'txc config auth create --kind client-secret', which doesn't exist.
  Rewrote to reference 'txc config auth add-service-principal --alias
  ... --secret-from-env ...' (the real command) and kept the env-var
  fallback list for WIF/SPN.

Test baseline green: 364 unit + 26 integration + 5 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TomProkop
Copy link
Copy Markdown
Member Author

All 7 review comments addressed in 19ea619.

Fixes

  • JSON enum stability (5 files) — ConnectionCreate, AuthList, AuthShow (all flagged) plus AuthLogin and AuthAddServicePrincipal (audit turned up the same bug). Every config command now clones TxcJsonOptions.Default so provider/cloud/kind serialize as canonical kebab-case strings.
  • DataverseCommandBridge XML doc — tightened to "ClientSecret only (v1)" with a TODO pointer. The switch already rejects ClientCertificate; doc no longer overclaims.
  • PackageImportCliCommand — deleted the empty fail-fast block (empty profile is legitimately permitted).
  • ProfilePinCliCommandct now flows into SerializeAsync/FlushAsync.
  • HeadlessAuthRequiredException — remedy now references the real txc config auth add-service-principal command.

Test baseline green: 364 unit + 26 integration + 5 skipped.

TomProkop and others added 23 commits April 23, 2026 15:56
Introduces McpIgnoreAttribute (in TALXIS.CLI.Shared) as a marker that
suppresses a command — and its entire descendant sub-tree — from the MCP
tool registry. No new project references needed; TALXIS.CLI.Shared is
already referenced by every command assembly.

CliCommandLookupService.EnumerateRecursive now returns early when
[McpIgnore] is detected, so a single attribute on a parent group hides
all its children too.

DocsCliCommand removed from TxcCliCommand.Children — hidden from both
the CLI help tree and the MCP registry.

Commands marked [McpIgnore] (9):
- TransformServerStartCliCommand  — blocks a local HTTP server process
- AuthLoginCliCommand             — interactive browser flow (headless fails fast)
- AuthDeleteCliCommand            — destructive credential maintenance
- ConnectionDeleteCliCommand      — destructive connection maintenance
- ProfileDeleteCliCommand         — destructive profile maintenance
- ProfileUpdateCliCommand         — profile maintenance, not agent task
- ProfileValidateCliCommand       — redundant: create + show cover the checks
- ProfilePinCliCommand            — workspace-local; agents use profile select
- ProfileUnpinCliCommand          — same

MCP tool count: ~34 → ~25.

Tests:
- New CliCommandLookupServiceTests covering ignored leaves, ignored
  sub-trees, and the real command tree.
- McpServerProtocolTests updated: replaced docs assertion (docs removed)
  with workspace_explain (stable short-lived tool).

Build: 0 errors. Tests: 368 unit + 26 integration + 5 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes 'Credential kind InteractiveBrowser cannot currently be used with
subprocess-based commands' for both 'package import' and 'data package
import'. Also moves the CMT data-import runner out-of-process for the
same legacy-patched-Xrm.Tooling-assembly isolation reason as Package
Deployer.

Auth (DataverseCommandBridge):
- New PrimeTokenAsync(profile) for parent-side cache priming.
- New BuildTokenProviderAsync returning (envUrl, Func<string,Task<string>>),
  honouring the resource URI requested by the SDK (canonicalized/redirected
  org URL) rather than hardcoding Connection.EnvironmentUrl.
- IDataverseAccessTokenService.AcquireForResourceAsync overload; existing
  AcquireAsync delegates to it.
- BuildConnectionStringAsync now generically rejects non-ClientSecret
  kinds with a pointer to the token-provider path.

Runners:
- PackageDeployerRunner.RunAsync(envUrl, tokenProvider, ct) overload
  using the per-instance CrmServiceClient(Uri, Func, ...) ctor (no
  static AuthOverrideHook race).
- CmtImportRunner.RunAsync(envUrl, tokenProvider, ct) overload doing the
  same for the primary client and every clone.

Subprocess isolation:
- Replaced PackageDeployerSubprocess with shared LegacyAssemblyHostSubprocess
  coordinator. Generic RunJobAsync<TRequest,TResult>; dispatches
  __txc_internal_package_deployer, __txc_internal_cmt_import, and
  the existing cleanup helper command.
- New CmtImportJob IPC envelope (request + profile + config dir +
  parent pid) so CmtImportRequest stays a leaf record.
- DataPackageImportCliCommand now primes the cache and submits via
  RunCmtImportAsync; drops in-process CmtImportRunner usage and the
  connection-string path.
- PackageImportCliCommand calls PrimeTokenAsync before spawning and
  uses the new shared coordinator.

Workspace + Data: qualified ambiguous Environment.* references with
System.Environment.* now that TALXIS.CLI.Data transitively pulls in the
TALXIS.CLI.Environment namespace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Delete DataverseAuthTokenProvider (legacy ~/.txc/auth MSAL cache).
  All Dataverse token acquisition now flows through
  IDataverseAccessTokenService backed by DataverseMsalClientFactory and
  the per-profile cache under TXC_CONFIG_DIR.
- Delete DataverseInteractiveAuthHook (IOverrideAuthHookWrapper adapter).
  CrmServiceClient is now constructed via the per-instance
  Func<string,Task<string>> ctor returned by
  DataverseCommandBridge.BuildTokenProviderAsync — no static auth-hook
  state.
- Simplify DataverseConnection: drop the always-null TokenProvider
  property and the wrapping ServiceClientFactory file. The class is now
  a single-responsibility ServiceClient wrapper.
- Drop two redundant integration-test methods that exercised
  DataverseAuthTokenProvider statics; equivalent coverage already lives
  in DataverseScopeTests + DataverseCloudMapTests.
- Tidy IDataverseConnectionFactory doc comment that referenced the
  deleted ServiceClientFactory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Collapse the three-step dev onboarding (auth login -> connection create ->
profile create --auth --connection) into a single command:

    txc c p create --url https://contoso.crm4.dynamics.com/

Provider is inferred from the URL host (Dataverse commercial / gov / DoD /
China), the default name is derived from the first DNS label, and the
interactive sign-in + credential upsert + connection upsert + profile
binding happen in one orchestrated step. The primitive commands stay
for advanced and service-principal flows.

- Extract shared helpers: CredentialAliasResolver, ConnectionUpsertService,
  ProviderUrlResolver.
- Introduce IConnectionProviderBootstrapper with DataverseConnectionProviderBootstrapper.
- Rewrite ProfileCreateCliCommand with dual-mode (OneLiner / Explicit) option
  classification, --name replaces positional; auto-activation preserved.
- Headless guard honors HeadlessAuthRequiredException in --url mode.
- Register new services in core + Dataverse DI extensions.
- Tests: ProviderUrlResolverTests (inference + name derivation) and
  ProfileCreateOneLinerTests (happy path, name collision, unknown host,
  mixed-mode rejection, headless failure, explicit --name override).
- README: document quickstart vs advanced flows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both auth login and the Dataverse one-liner bootstrapper were doing the
same dance: headless guard -> login -> alias resolve -> build Credential
with identical fields -> upsert. Extract it into InteractiveCredentialBootstrapper
so there is exactly one code path that creates interactive-browser
credentials.

Also remove the back-compat thin wrappers on AuthLoginCliCommand
(ResolveDefaultAliasAsync, ExtractTenantShortName) - pre-release, so
tests call CredentialAliasResolver directly now.

All 392 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves conflict with master where .DS_Store was deleted (e1d92b6).
Fix malformed gitignore entry 'temp.DS_Store' which ignored nothing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…iles

# Conflicts:
#	tests/TALXIS.CLI.IntegrationTests/CliRunner.cs
Relocates TxcServicesBootstrap from TALXIS.CLI.Environment/Platforms/Dataverse/
(a feature project) to TALXIS.CLI.Config.Providers.Dataverse/DependencyInjection/
(the platform adapter that actually owns the Dataverse DI graph). Host and the
PackageDeployer/CMT subprocess now import it from the platform namespace.

Also drops the unused Config.Commands -> Config.Providers.Dataverse
ProjectReference (no 'using' statements referenced it in source). This is the
first dependency edge cleanup toward the Features / Core / Platform layering
described in plan.md.

Part of the repo architecture reorg (Phase 1).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce ISolutionInventoryService + InstalledSolutionRecord in
TALXIS.CLI.Config so feature commands no longer import Dataverse SDK or
Config.Providers.Dataverse.Runtime directly. The Dataverse-bound reader
moves into the provider project as the service implementation.

SolutionListCliCommand now resolves the service via TxcServices and
stays focused on argument parsing and output formatting.

First of seven commands in the per-command-abstractions refactor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Relocate 14 Dataverse-specific files (readers, subprocess host,
importers, history, schema, NuGet installer) from the Environment
feature project into the Dataverse platform adapter.

Namespace updated from TALXIS.CLI.Environment.Platforms.Dataverse to
TALXIS.CLI.Config.Providers.Dataverse.Platforms. Consumer using
directives rewritten across Environment/Data commands, Program.cs,
and the unit + integration test suites. XrmTools ProjectReference
moved into the provider project; TryDeleteDirectory exposed publicly
so PackageImport can finish cleaning up its temp directory during
the transitional period (will be encapsulated by the upcoming
IPackageImportService abstraction).

Eliminates the circular-reference obstacle blocking per-command
abstractions. 392/392 unit tests green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DataPackageImportCliCommand and SolutionUninstallCliCommand now resolve
their platform work through service-locator abstractions in
TALXIS.CLI.Config. MSAL and Dataverse SDK types no longer leak into
these feature commands.

Lifts DTOs (DataPackageImportResult, SolutionUninstallStatus,
SolutionUninstallOutcome) into Core. PackageUninstallCliCommand
using directives updated for the relocated DTOs pending its own
service lift.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SolutionImportCliCommand now resolves solution-import work through
ISolutionImportService; DTOs (SolutionInfo, SolutionImportPath,
SolutionImportOptions, SolutionImportResult) lifted into Core.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lifts PackageHistoryRecord, SolutionHistoryRecord, DeploymentHistorySnapshot,
and DeploymentRelativeTimeParser into Core; adds DataverseDeploymentHistoryService
implementation. DeploymentListCliCommand is now a thin shell over the service.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds DeploymentDetailResult / DeploymentRunKind / AsyncOperationSummary to Core.
DataverseDeploymentDetailService encapsulates all platform lookups (package / solution /
async-op / latest) behind a single provider-agnostic interface; DeploymentShowCliCommand
is now a thin renderer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DataversePackageImportService encapsulates token priming, Package Deployer
subprocess, and temp-working-directory cleanup. PackageImportCliCommand becomes
a thin shell handling only NuGet restore, arg validation, and result rendering.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DataversePackageUninstallService owns package-config reading, reverse-order
computation, connection, history-record lifecycle, and per-solution uninstall.
PackageUninstallCliCommand becomes a thin shell.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move NuGetPackageInstallerService/Options/Result from Config.Providers.Dataverse
into Config.Platforms.Packaging (NuGet restore is provider-agnostic). Drop
ProjectReferences from Environment and Data to Dataverse, XrmTools, and
Config.Providers.Dataverse. Feature projects now compose only against Core and
Logging; provider wiring remains host-side in DI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Workspace never imported any TALXIS.CLI.Data type; the edge was dead. Add
direct Logging + Shared references (previously transitive via Data).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
All consumers live inside TALXIS.CLI.Config.Providers.Dataverse now.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- TALXIS.CLI.Config.Commands -> TALXIS.CLI.Features.Config
- TALXIS.CLI.Data            -> TALXIS.CLI.Features.Data
- TALXIS.CLI.Environment     -> TALXIS.CLI.Features.Environment
- TALXIS.CLI.Workspace       -> TALXIS.CLI.Features.Workspace
- TALXIS.CLI.Docs            -> TALXIS.CLI.Features.Docs
- TALXIS.CLI.XrmTools.XrmShim -> TALXIS.CLI.Platform.XrmShim
- TALXIS.CLI.XrmTools        -> TALXIS.CLI.Platform.Xrm

Project names now reflect role (Hosts / Features / Platform). XrmShim retains
Microsoft.Xrm.Tooling.Connector AssemblyName for legacy-assembly shim behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Dataverse

Consolidates the Dataverse domain project and the Config.Providers.Dataverse
provider project into a single TALXIS.CLI.Platform.Dataverse project.
Platform.Xrm's dependency on the old domain project is dropped (unused in code),
breaking the new circular reference.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consolidates TALXIS.CLI.Shared (OutputWriter, McpIgnoreAttribute) into
the Config project, then renames Config -> TALXIS.CLI.Core to reflect its
role as the shared core/abstractions layer for the CLI.

Completes Phase 2 of the repository architecture reorganization: projects
now follow the Hosts / Features.* / Platform.* / Core / Logging taxonomy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the old 'Platforms/<Name>/ inside the owning project' guidance
with the new Platform.* project model. Adds an explicit Project layout
section documenting the architectural tiers (Hosts / Features / Core /
Platform / Cross-cutting) and the dependency rules between them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TomProkop TomProkop requested a review from Copilot April 23, 2026 19:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 283 out of 304 changed files in this pull request and generated 4 comments.

Comment thread src/TALXIS.CLI.Platform.Dataverse/Runtime/DataverseConnectionFactory.cs Outdated
Comment thread src/TALXIS.CLI.Core/TALXIS.CLI.Core.csproj Outdated
Comment thread tests/TALXIS.CLI.Tests/MCP/CliCommandAdapterProfileArgumentTests.cs Outdated
Comment thread src/TALXIS.CLI.Core/DependencyInjection/TxcServices.cs
TomProkop and others added 2 commits April 23, 2026 21:54
- DataverseConnectionFactory: token callback now honors the resource
  parameter passed by Xrm Tooling ServiceClient (it may canonicalize or
  redirect the org URL), falling back to AcquireAsync only when the
  callback provides no/invalid resource.
- Core.csproj: align Microsoft.Identity.Client.Extensions.Msal to 4.83.3
  to match Platform.Dataverse and avoid NuGet version downgrade.
- TxcServices.Initialize: guard against double-initialization. Throws if
  called a second time with a different provider; still allows tests to
  replace the provider by calling Reset() first.
- Profile-argument schema test: use TryGetProperty for 'required' so the
  test tolerates JSON Schemas that legitimately omit the array when no
  members are required.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…taverse

It was the last concrete Dataverse implementation living in Core. Core
now only holds the IConnectionProviderBootstrapper abstraction and
shared bootstrapping helpers (ConnectionUpsertService, InteractiveCredentialBootstrapper).
The Dataverse-specific orchestration lives in Platform.Dataverse where
every other Dataverse impl sits, keeping the dependency graph strictly
inward-pointing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TomProkop TomProkop merged commit e635340 into master Apr 23, 2026
TomProkop added a commit that referenced this pull request Apr 23, 2026
First release includes profile-based auth & config system (PR #12).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TomProkop added a commit that referenced this pull request Apr 23, 2026
First release includes profile-based auth & config system (PR #12).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants