docs: Campaign LA — pinned launch-contract schema COMMITTED into plan LA1
The LA3 Opus review process note was right: the contract both sides implement lived only in orchestrator prompts, which is exactly the drift mode the pin exists to prevent (and it produced the paths-key CRITICAL). The schema, field rules, probe-mode discriminator, and status vocabulary are now a binding plan section; amendments change this text first, implementations second. Ledger: LA3 fix round dispatched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0bcc7ba3a3
commit
db9ad53c1c
38 changed files with 2397 additions and 40 deletions
155
src/AcDream.App/Credentials/AppCredentialResolver.cs
Normal file
155
src/AcDream.App/Credentials/AppCredentialResolver.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
using AcDream.App.Configuration;
|
||||
using AcDream.App.Platform;
|
||||
|
||||
namespace AcDream.App.Credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA1: resolves a <c>--session-config</c> session's
|
||||
/// credential reference — the App-side mirror of
|
||||
/// <c>AcDream.Headless.Credentials.HeadlessCredentialResolver</c> (see that
|
||||
/// type's own file for why this is an independent port rather than a shared
|
||||
/// reference). Supports the same three providers with the same semantics:
|
||||
/// <c>environment</c> (read an env var), <c>standardInput</c> (read one line
|
||||
/// from stdin, mirroring <c>HeadlessCredentialResolver.ResolveStandardInput</c>),
|
||||
/// and <c>file</c> (read a credential file relative to a base directory,
|
||||
/// rejecting symlinks and, on Linux, group/other-readable permissions).
|
||||
/// </summary>
|
||||
internal sealed class AppCredentialResolver
|
||||
{
|
||||
private const UnixFileMode NonUserPermissionMask =
|
||||
UnixFileMode.GroupRead
|
||||
| UnixFileMode.GroupWrite
|
||||
| UnixFileMode.GroupExecute
|
||||
| UnixFileMode.OtherRead
|
||||
| UnixFileMode.OtherWrite
|
||||
| UnixFileMode.OtherExecute;
|
||||
|
||||
private readonly TextReader _standardInput;
|
||||
private readonly string _credentialBaseDirectory;
|
||||
private readonly bool _isLinux;
|
||||
|
||||
/// <summary>
|
||||
/// <paramref name="isLinux"/> is caller-supplied, never detected in this
|
||||
/// file — <c>LinuxPlatformBoundaryTests</c>'s platform-owner guard
|
||||
/// requires every OS-family check to live under <c>Platform/</c>;
|
||||
/// callers pass <c>GraphicalHostPlatformServices</c>'s already-detected
|
||||
/// value instead of this file re-detecting it itself.
|
||||
/// </summary>
|
||||
internal AppCredentialResolver(
|
||||
TextReader standardInput,
|
||||
string credentialBaseDirectory,
|
||||
bool isLinux)
|
||||
{
|
||||
_standardInput = standardInput
|
||||
?? throw new ArgumentNullException(nameof(standardInput));
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(credentialBaseDirectory);
|
||||
_credentialBaseDirectory = Path.GetFullPath(credentialBaseDirectory);
|
||||
_isLinux = isLinux;
|
||||
}
|
||||
|
||||
internal AppCredentialSecret Resolve(
|
||||
string sessionId,
|
||||
SessionCredentialDescriptor credential)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
|
||||
ArgumentNullException.ThrowIfNull(credential);
|
||||
|
||||
string value;
|
||||
try
|
||||
{
|
||||
value = credential.Provider switch
|
||||
{
|
||||
SessionCredentialProviderKind.Environment =>
|
||||
ResolveEnvironment(credential.Reference),
|
||||
SessionCredentialProviderKind.StandardInput =>
|
||||
ResolveStandardInput(credential.Reference),
|
||||
SessionCredentialProviderKind.File =>
|
||||
ResolveFile(credential.Reference),
|
||||
_ => throw new AppCredentialException(
|
||||
$"Session '{sessionId}' uses an unsupported credential provider."),
|
||||
};
|
||||
}
|
||||
catch (AppCredentialException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception error)
|
||||
when (error is IOException
|
||||
or UnauthorizedAccessException
|
||||
or ArgumentException
|
||||
or NotSupportedException)
|
||||
{
|
||||
throw new AppCredentialException(
|
||||
$"Credential '{credential.Reference}' for session '{sessionId}' could not be resolved.",
|
||||
error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new AppCredentialSecret(credential.Reference, value.AsSpan());
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The BCL returns immutable strings from environment, TextReader,
|
||||
// and File APIs. Do not retain another copy in the resolver; the
|
||||
// erasable char[] owner becomes the sole explicit retained copy.
|
||||
value = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveEnvironment(string reference)
|
||||
{
|
||||
string? value = Environment.GetEnvironmentVariable(reference);
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
throw new AppCredentialException(
|
||||
$"Credential environment reference '{reference}' is unavailable.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private string ResolveStandardInput(string reference)
|
||||
{
|
||||
string? value = _standardInput.ReadLine();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
throw new AppCredentialException(
|
||||
$"Credential standard-input reference '{reference}' is unavailable.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private string ResolveFile(string reference)
|
||||
{
|
||||
string path = Path.GetFullPath(reference, _credentialBaseDirectory);
|
||||
var file = new FileInfo(path);
|
||||
if (file.LinkTarget is not null)
|
||||
{
|
||||
throw new AppCredentialException(
|
||||
$"Credential file reference '{reference}' cannot be a symbolic link.");
|
||||
}
|
||||
|
||||
// RuntimePlatformGuard.IsLinuxRuntime is the CA1416-recognized guard
|
||||
// for File.GetUnixFileMode below; _isLinux is the separate,
|
||||
// caller-injected value tests use for deterministic cross-platform
|
||||
// coverage (see the constructor's own doc).
|
||||
if (RuntimePlatformGuard.IsLinuxRuntime && _isLinux)
|
||||
{
|
||||
UnixFileMode mode = File.GetUnixFileMode(path);
|
||||
if ((mode & NonUserPermissionMask) != 0
|
||||
|| (mode & UnixFileMode.UserRead) == 0)
|
||||
{
|
||||
throw new AppCredentialException(
|
||||
$"Credential file reference '{reference}' must be readable only by its owner.");
|
||||
}
|
||||
}
|
||||
|
||||
string value = File.ReadAllText(path).TrimEnd('\r', '\n');
|
||||
if (value.Length == 0)
|
||||
{
|
||||
throw new AppCredentialException(
|
||||
$"Credential file reference '{reference}' is empty.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue