Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/profiles-and-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,27 @@ Unpin a repository-level profile with:
txc config profile unpin
```

## Sign-in is manual — including when a coding agent is driving `txc`

`txc` never starts an interactive browser sign-in or a device-code flow on
anyone's behalf. `txc config auth login` and `txc config profile create --url`
only run an interactive flow when a human deliberately invokes them, in their
own terminal, and only when a browser is actually reachable — there is no
silent fallback to device-code if it isn't. If a token has expired or no
credential exists yet, every affected command fails fast with an error asking
for a **manual** `txc config auth login` (or `--device-code` explicitly, if
that's genuinely what's needed) instead of starting a flow no one is watching.

If you are scripting or delegating work to a coding agent:

- Sign in yourself, once, ahead of time — in a CI runner or agent sandbox, use
the [headless workflow](#headless-and-ci-workflow) below instead of
interactive sign-in.
- Before creating a new profile or connection, run `txc config profile list`
and `txc config connection list` first — reuse an existing one that already
targets the environment you need instead of letting an agent create one
speculatively.

## Headless and CI workflow

For CI runners and other non-interactive environments, isolate config and load the secret from an environment variable:
Expand Down
10 changes: 6 additions & 4 deletions src/TALXIS.CLI.Core/Headless/HeadlessAuthRequiredException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,18 @@ private static string BuildMessage(CredentialKind kind, string reason)
.OrderBy(s => s, StringComparer.Ordinal));

return
$"Credential kind '{ToKebab(kind)}' requires an interactive TTY, " +
$"but this process is running in headless mode ({reason}). " +
$"Interactive sign-in requires a human to run it deliberately in their own terminal — " +
$"it is never performed automatically. Credential kind '{ToKebab(kind)}' requires an interactive " +
$"TTY, but this process is running in headless mode ({reason}). " +
$"Permitted headless kinds: {permitted}. " +
"To run non-interactively, register a headless-capable credential with either " +
"To run non-interactively, ask a human to register a headless-capable credential with either " +
"`txc config auth add-service-principal --alias <alias> --tenant <tenant> " +
"--client-id <app-id> --secret-from-env <ENV_VAR_NAME>` or " +
"`txc config auth add-federated --alias <alias> --tenant <tenant> --client-id <app-id>`, and bind it to the profile, " +
"or supply the credential via environment variables " +
"(AZURE_CLIENT_ID / AZURE_CLIENT_SECRET / AZURE_TENANT_ID for SPN, " +
"AZURE_FEDERATED_TOKEN_FILE for workload-identity federation).";
"AZURE_FEDERATED_TOKEN_FILE for workload-identity federation). " +
"Otherwise, ask the human to run `txc config auth login` themselves in an interactive terminal, then retry.";
}

private static string ToKebab(CredentialKind kind)
Expand Down
7 changes: 5 additions & 2 deletions src/TALXIS.CLI.Core/Resolution/ConfigurationResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ public async Task<ResolvedProfileContext> ResolveAsync(string? profileName, Canc
if (name is null)
{
throw new ConfigurationResolutionException(
"No txc profile could be resolved. Pass --profile <name>, set TXC_PROFILE, "
+ "pin a workspace default with 'txc config profile pin', or select a global default with 'txc config profile select'.",
"No txc profile could be resolved. Run 'txc config profile list' and 'txc config connection list' "
+ "first to check whether a suitable profile or connection already exists before creating a new "
+ "one — confirm the target environment with the user if it's unclear. To resolve: pass --profile <name>, "
+ "set TXC_PROFILE, pin a workspace default with 'txc config profile pin', or select a global default "
+ "with 'txc config profile select'.",
ConfigurationResolutionFailureReason.NoProfile);
}

Expand Down
2 changes: 1 addition & 1 deletion src/TALXIS.CLI.Features.Config/Auth/AuthCliCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TALXIS.CLI.Features.Config.Auth;
/// </summary>
[CliCommand(
Name = "auth",
Description = "Manage Entra / Dataverse credentials stored in the OS vault.",
Description = "Manage Entra / Dataverse credentials stored in the OS vault — the \"who\". Interactive sign-in ('login') and device-code sign-in are manual actions performed deliberately by a human in their own terminal; they are never triggered automatically on a caller's behalf.",
Children = new[]
{
typeof(AuthLoginCliCommand),
Expand Down
38 changes: 27 additions & 11 deletions src/TALXIS.CLI.Features.Config/Auth/AuthLoginCliCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ namespace TALXIS.CLI.Features.Config.Auth;
/// <remarks>
/// Fails fast with exit 1 in headless contexts — interactive browser is
/// never a permitted headless kind. See <see cref="HeadlessAuthRequiredException"/>.
/// When a local browser is not available (Codespaces, SSH, no DISPLAY),
/// the command automatically falls back to device code flow unless
/// <c>--device-code</c> was already specified.
/// Sign-in is always a deliberate, manual action taken by a human in their
/// own terminal — this command never guesses on the caller's behalf. When a
/// local browser is not available (Codespaces, SSH, no DISPLAY) it does
/// <b>not</b> silently switch to device code; it fails fast and tells the
/// human to re-run with <c>--device-code</c> themselves. This mirrors how
/// real scripted workflows already use this command (they always pass
/// <c>--device-code</c> explicitly rather than relying on auto-detection).
/// </remarks>
[CliIdempotent]
[McpIgnore]
[CliCommand(
Name = "login",
Description = "Interactive sign-in. Uses browser login by default; falls back to device code in browser-isolated environments (Codespaces, SSH)."
Description = "Interactive sign-in, run manually by a human in their own terminal — never by an automated caller. Uses browser login by default; pass --device-code yourself when no local browser can reach localhost (Codespaces, SSH, containers)."
)]
public class AuthLoginCliCommand : TxcLeafCommand
{
Expand All @@ -43,7 +47,7 @@ public class AuthLoginCliCommand : TxcLeafCommand
[CliOption(Name = "--cloud", Description = "Sovereign cloud. Default: public.", Required = false)]
public CloudInstance? Cloud { get; set; }

[CliOption(Name = "--device-code", Description = "Use device code flow instead of browser login. Required when no local browser can reach localhost (Codespaces, SSH, containers).", Required = false)]
[CliOption(Name = "--device-code", Description = "Use device code flow instead of browser login. Pass this yourself — deliberately, as the human running this command — when no local browser can reach localhost (Codespaces, SSH, containers). Never inferred automatically.", Required = false)]
public bool DeviceCode { get; set; }

protected override async Task<int> ExecuteAsync()
Expand All @@ -53,15 +57,27 @@ protected override async Task<int> ExecuteAsync()
var browserProbe = TxcServices.Get<IBrowserAvailabilityProbe>();
var cloud = Cloud ?? CloudInstance.Public;

// Determine whether to use device code flow: explicit flag or
// or automatic detection of browser-isolated environments.
var useDeviceCode = DeviceCode || !browserProbe.IsBrowserAvailable;
// Sign-in is always a deliberate, manual choice made by a human.
// Never silently switch flows on the caller's behalf: if no local
// browser is reachable and --device-code wasn't explicitly passed,
// fail fast instead of starting an unattended device-code prompt
// that nobody will complete (see HeadlessAuthRequiredException for
// the equivalent fully-headless case).
if (!DeviceCode && !browserProbe.IsBrowserAvailable)
{
Logger.LogError(
"No local browser is available ({Reason}). Interactive sign-in requires a human to run it " +
"deliberately in their own terminal. Re-run this command yourself with '--device-code' to sign in " +
"using the device code flow. This is never chosen automatically.",
browserProbe.UnavailableReason ?? "unknown reason");
return ExitError;
}

if (useDeviceCode)
if (DeviceCode)
{
var deviceCodeLogin = TxcServices.Get<IDeviceCodeLoginService>();
Logger.LogInformation("Starting device code sign-in (browser unavailable: {Reason})...",
browserProbe.UnavailableReason ?? "--device-code flag");
Logger.LogInformation("Starting device code sign-in (requested via --device-code; browser reachable: {BrowserAvailable})...",
browserProbe.IsBrowserAvailable);

var result = await DeviceCodeCredentialBootstrapper.AcquireAndPersistAsync(
deviceCodeLogin, store, headless, Tenant, cloud, Alias, CancellationToken.None).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace TALXIS.CLI.Features.Config.Connection;
/// </summary>
[CliCommand(
Name = "connection",
Description = "Manage service endpoint metadata (Dataverse environments, etc.).",
Description = "Manage service endpoint metadata (Dataverse environments, etc.) — the \"where\", not the \"who\". Run 'connection list' before 'connection create' to check for an existing one targeting the same environment.",
Children = new[]
{
typeof(ConnectionCreateCliCommand),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace TALXIS.CLI.Features.Config.Profile;
/// </summary>
[CliCommand(
Name = "profile",
Description = "Manage profiles (bind one auth to one connection).",
Description = "Manage profiles (bind one auth to one connection). Profile = context (\"which environment, as whom\"); Connection = where; Credential = who. Run 'profile list' before 'profile create' — reuse an existing profile instead of guessing a new one.",
Children = new[]
{
typeof(ProfileCreateCliCommand),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ namespace TALXIS.CLI.Features.Config.Profile;
[CliIdempotent]
[CliCommand(
Name = "create",
Description = "Create a profile. Quickstart: --url <env>. Advanced: --auth <alias> --connection <name>."
Description = "Create a profile. Check 'txc config profile list' and 'txc config connection list' first — reuse an existing profile/connection when one already targets the environment you need instead of creating a new one. Quickstart: --url <env>. Advanced: --auth <alias> --connection <name>."
)]
public class ProfileCreateCliCommand : TxcLeafCommand
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ protected override async Task<int> ExecuteAsync()

if (context.Profile is null)
{
Logger.LogError("Resolved configuration is ephemeral. 'txc config profile validate' requires a stored profile.");
Logger.LogError(
"Resolved configuration is ephemeral. 'txc config profile validate' requires a stored profile. " +
"Run 'txc config profile list' and 'txc config connection list' to check for an existing profile " +
"before creating a new one — confirm the target environment with the user if it's unclear.");
return ExitValidationError;
}

Expand Down
17 changes: 15 additions & 2 deletions src/TALXIS.CLI.MCP/CliSubprocessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ private static async Task ReadLinesAsync(
}

private static Process StartProcess(IReadOnlyList<string> cliArgs, string? workingDirectory = null)
{
ProcessStartInfo startInfo = BuildStartInfo(cliArgs, workingDirectory);
return Process.Start(startInfo)
?? throw new InvalidOperationException("Failed to start the txc CLI subprocess.");
}

/// <summary>
/// Builds the <see cref="ProcessStartInfo"/> for a txc CLI subprocess without
/// starting it. Extracted from <see cref="StartProcess"/> so the
/// forced-headless contract (<c>TXC_NON_INTERACTIVE=1</c>) can be covered by a
/// unit test that doesn't spawn a real process — see
/// <c>CliSubprocessRunnerTests</c>.
/// </summary>
internal static ProcessStartInfo BuildStartInfo(IReadOnlyList<string> cliArgs, string? workingDirectory = null)
{
(string fileName, string? assemblyPath) = ResolveCliHost();
ProcessStartInfo startInfo = new()
Expand Down Expand Up @@ -159,8 +173,7 @@ private static Process StartProcess(IReadOnlyList<string> cliArgs, string? worki
startInfo.ArgumentList.Add(cliArg);
}

return Process.Start(startInfo)
?? throw new InvalidOperationException("Failed to start the txc CLI subprocess.");
return startInfo;
}

private static (string FileName, string? AssemblyPath) ResolveCliHost()
Expand Down
12 changes: 12 additions & 0 deletions src/TALXIS.CLI.MCP/CopilotInstructionsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ public class CopilotInstructionsManager
# Instructions for performing tasks over the repository
Use the TALXIS CLI MCP Server (txc-mcp) for all development tasks. Call `guide_workspace`, `guide_environment`, `guide_deployment`, `guide_data`, `guide_config`, or `get_skill_details` to discover available tools and guidance.

## Authentication

- Never attempt to sign in yourself. `txc`/txc-mcp structurally refuses to start
an interactive browser or device-code flow on your behalf — there is no
headless fallback. If a tool call fails with an auth-required/expired-token
error, stop and ask the user to run `txc config auth login` manually in
their own terminal, then retry.
- Never create a new profile or connection speculatively. Run
`config_profile_list` / `config_connection_list` (or `txc config profile
list` / `txc config connection list`) first and reuse an existing one that
already targets the environment you need.

## Project Structure and Naming Conventions

**Note**: These are recommended naming conventions. Users may choose different naming styles based on their preferences or organizational standards.
Expand Down
10 changes: 10 additions & 0 deletions src/TALXIS.CLI.MCP/Skills/Internal/troubleshooting-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ config_profile_validate → result?
├─ Invalid → config_profile_get → config_connection_get → fix credentials
└─ Valid but failing → check security roles in Power Platform admin center (outside txc)
```
→ If the failure is an expired/missing credential (AUTH_REQUIRED-style error), do
NOT attempt to sign in yourself and do NOT create a new profile/connection to work
around it. Sign-in is always a manual, human action — `txc` structurally refuses to
start an interactive or device-code flow on your behalf (there is no headless
fallback). Run `config_profile_list` / `config_connection_list` to confirm nothing
existing already covers the target, then STOP and ask the user to run
`txc config auth login` themselves in their own terminal, and retry once they confirm.


## Diagnostic Priority Order
When unsure where to start:
Expand All @@ -53,3 +61,5 @@ When unsure where to start:
- ❌ Querying the `asyncoperation` table via `environment_data_query_sql` to check import status → call `environment_deployment_get --async-operation-id <id>` instead — raw SQL gives unstructured statuscodes, the proper tool returns parsed findings and human-readable errors
- ❌ Jumping to layer inspection before checking basic auth/connectivity
- ❌ Using environment schema tools to "fix" what should be fixed locally and redeployed
- ❌ Creating a new profile or connection speculatively on an auth/"no profile" error → list first (`config_profile_list`, `config_connection_list`) and reuse an existing one
- ❌ Trying to trigger or work around sign-in yourself (interactive browser, device-code, or otherwise) → stop and ask the user to run `txc config auth login` manually
Loading