Resolve Concelier/Excititor merge conflicts

This commit is contained in:
master
2025-10-20 14:19:25 +03:00
2687 changed files with 212646 additions and 85913 deletions

View File

@@ -1,14 +1,14 @@
# StellaOps.Cli — Agent Brief
## Mission
- Deliver an offline-capable command-line interface that drives StellaOps back-end operations: scanner distribution, scan execution, result uploads, and Feedser database lifecycle calls (init/resume/export).
- Deliver an offline-capable command-line interface that drives StellaOps back-end operations: scanner distribution, scan execution, result uploads, and Concelier database lifecycle calls (init/resume/export).
- Honour StellaOps principles of determinism, observability, and offline-first behaviour while providing a polished operator experience.
## Role Charter
| Role | Mandate | Collaboration |
| --- | --- | --- |
| **DevEx/CLI** | Own CLI UX, command routing, and configuration model. Ensure commands work with empty/default config and document overrides. | Coordinate with Backend/WebService for API contracts and with Docs for operator workflows. |
| **Ops Integrator** | Maintain integration paths for shell/dotnet/docker tooling. Validate that air-gapped runners can bootstrap required binaries. | Work with Feedser/Agent teams to mirror packaging and signing requirements. |
| **Ops Integrator** | Maintain integration paths for shell/dotnet/docker tooling. Validate that air-gapped runners can bootstrap required binaries. | Work with Concelier/Agent teams to mirror packaging and signing requirements. |
| **QA** | Provide command-level fixtures, golden outputs, and regression coverage (unit & smoke). Ensure commands respect cancellation and deterministic logging. | Partner with QA guild for shared harnesses and test data. |
## Working Agreements
@@ -21,7 +21,7 @@
- Update `TASKS.md` as states change (TODO → DOING → DONE/BLOCKED) and record added tests/fixtures alongside implementation notes.
## Reference Materials
- `docs/ARCHITECTURE_FEEDSER.md` for database operations surface area.
- `docs/ARCHITECTURE_CONCELIER.md` for database operations surface area.
- Backend OpenAPI/contract docs (once available) for job triggers and scanner endpoints.
- Existing module AGENTS/TASKS files for style and coordination cues.
- `docs/09_API_CLI_REFERENCE.md` (section 3) for the user-facing synopsis of the CLI verbs and flags.

View File

@@ -24,6 +24,8 @@ internal static class CommandFactory
root.Add(BuildScannerCommand(services, verboseOption, cancellationToken));
root.Add(BuildScanCommand(services, options, verboseOption, cancellationToken));
root.Add(BuildDatabaseCommand(services, verboseOption, cancellationToken));
root.Add(BuildExcititorCommand(services, verboseOption, cancellationToken));
root.Add(BuildRuntimeCommand(services, verboseOption, cancellationToken));
root.Add(BuildAuthCommand(services, options, verboseOption, cancellationToken));
root.Add(BuildConfigCommand(options));
@@ -137,7 +139,7 @@ internal static class CommandFactory
private static Command BuildDatabaseCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
{
var db = new Command("db", "Trigger Feedser database operations via backend jobs.");
var db = new Command("db", "Trigger Concelier database operations via backend jobs.");
var fetch = new Command("fetch", "Trigger connector fetch/parse/map stages.");
var sourceOption = new Option<string>("--source")
@@ -174,7 +176,7 @@ internal static class CommandFactory
return CommandHandlers.HandleMergeJobAsync(services, verbose, cancellationToken);
});
var export = new Command("export", "Run Feedser export jobs.");
var export = new Command("export", "Run Concelier export jobs.");
var formatOption = new Option<string>("--format")
{
Description = "Export format: json or trivy-db."
@@ -220,10 +222,304 @@ internal static class CommandFactory
db.Add(fetch);
db.Add(merge);
db.Add(export);
db.Add(export);
return db;
}
private static Command BuildExcititorCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
{
var excititor = new Command("excititor", "Manage Excititor ingest, exports, and reconciliation workflows.");
var init = new Command("init", "Initialize Excititor ingest state.");
var initProviders = new Option<string[]>("--provider", new[] { "-p" })
{
Description = "Optional provider identifier(s) to initialize.",
Arity = ArgumentArity.ZeroOrMore
};
var resumeOption = new Option<bool>("--resume")
{
Description = "Resume ingest from the last persisted checkpoint instead of starting fresh."
};
init.Add(initProviders);
init.Add(resumeOption);
init.SetAction((parseResult, _) =>
{
var providers = parseResult.GetValue(initProviders) ?? Array.Empty<string>();
var resume = parseResult.GetValue(resumeOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleExcititorInitAsync(services, providers, resume, verbose, cancellationToken);
});
var pull = new Command("pull", "Trigger Excititor ingest for configured providers.");
var pullProviders = new Option<string[]>("--provider", new[] { "-p" })
{
Description = "Optional provider identifier(s) to ingest.",
Arity = ArgumentArity.ZeroOrMore
};
var sinceOption = new Option<DateTimeOffset?>("--since")
{
Description = "Optional ISO-8601 timestamp to begin the ingest window."
};
var windowOption = new Option<TimeSpan?>("--window")
{
Description = "Optional window duration (e.g. 24:00:00)."
};
var forceOption = new Option<bool>("--force")
{
Description = "Force ingestion even if the backend reports no pending work."
};
pull.Add(pullProviders);
pull.Add(sinceOption);
pull.Add(windowOption);
pull.Add(forceOption);
pull.SetAction((parseResult, _) =>
{
var providers = parseResult.GetValue(pullProviders) ?? Array.Empty<string>();
var since = parseResult.GetValue(sinceOption);
var window = parseResult.GetValue(windowOption);
var force = parseResult.GetValue(forceOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleExcititorPullAsync(services, providers, since, window, force, verbose, cancellationToken);
});
var resume = new Command("resume", "Resume Excititor ingest using a checkpoint token.");
var resumeProviders = new Option<string[]>("--provider", new[] { "-p" })
{
Description = "Optional provider identifier(s) to resume.",
Arity = ArgumentArity.ZeroOrMore
};
var checkpointOption = new Option<string?>("--checkpoint")
{
Description = "Optional checkpoint identifier to resume from."
};
resume.Add(resumeProviders);
resume.Add(checkpointOption);
resume.SetAction((parseResult, _) =>
{
var providers = parseResult.GetValue(resumeProviders) ?? Array.Empty<string>();
var checkpoint = parseResult.GetValue(checkpointOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleExcititorResumeAsync(services, providers, checkpoint, verbose, cancellationToken);
});
var list = new Command("list-providers", "List Excititor providers and their ingest status.");
var includeDisabledOption = new Option<bool>("--include-disabled")
{
Description = "Include disabled providers in the listing."
};
list.Add(includeDisabledOption);
list.SetAction((parseResult, _) =>
{
var includeDisabled = parseResult.GetValue(includeDisabledOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleExcititorListProvidersAsync(services, includeDisabled, verbose, cancellationToken);
});
var export = new Command("export", "Trigger Excititor export generation.");
var formatOption = new Option<string>("--format")
{
Description = "Export format (e.g. openvex, json)."
};
var exportDeltaOption = new Option<bool>("--delta")
{
Description = "Request a delta export when supported."
};
var exportScopeOption = new Option<string?>("--scope")
{
Description = "Optional policy scope or tenant identifier."
};
var exportSinceOption = new Option<DateTimeOffset?>("--since")
{
Description = "Optional ISO-8601 timestamp to restrict export contents."
};
var exportProviderOption = new Option<string?>("--provider")
{
Description = "Optional provider identifier when requesting targeted exports."
};
var exportOutputOption = new Option<string?>("--output")
{
Description = "Optional path to download the export artifact."
};
export.Add(formatOption);
export.Add(exportDeltaOption);
export.Add(exportScopeOption);
export.Add(exportSinceOption);
export.Add(exportProviderOption);
export.Add(exportOutputOption);
export.SetAction((parseResult, _) =>
{
var format = parseResult.GetValue(formatOption) ?? "openvex";
var delta = parseResult.GetValue(exportDeltaOption);
var scope = parseResult.GetValue(exportScopeOption);
var since = parseResult.GetValue(exportSinceOption);
var provider = parseResult.GetValue(exportProviderOption);
var output = parseResult.GetValue(exportOutputOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleExcititorExportAsync(services, format, delta, scope, since, provider, output, verbose, cancellationToken);
});
var backfill = new Command("backfill-statements", "Replay historical raw documents into Excititor statements.");
var backfillRetrievedSinceOption = new Option<DateTimeOffset?>("--retrieved-since")
{
Description = "Only process raw documents retrieved on or after the provided ISO-8601 timestamp."
};
var backfillForceOption = new Option<bool>("--force")
{
Description = "Reprocess documents even if statements already exist."
};
var backfillBatchSizeOption = new Option<int>("--batch-size")
{
Description = "Number of raw documents to fetch per batch (default 100)."
};
var backfillMaxDocumentsOption = new Option<int?>("--max-documents")
{
Description = "Optional maximum number of raw documents to process."
};
backfill.Add(backfillRetrievedSinceOption);
backfill.Add(backfillForceOption);
backfill.Add(backfillBatchSizeOption);
backfill.Add(backfillMaxDocumentsOption);
backfill.SetAction((parseResult, _) =>
{
var retrievedSince = parseResult.GetValue(backfillRetrievedSinceOption);
var force = parseResult.GetValue(backfillForceOption);
var batchSize = parseResult.GetValue(backfillBatchSizeOption);
if (batchSize <= 0)
{
batchSize = 100;
}
var maxDocuments = parseResult.GetValue(backfillMaxDocumentsOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleExcititorBackfillStatementsAsync(
services,
retrievedSince,
force,
batchSize,
maxDocuments,
verbose,
cancellationToken);
});
var verify = new Command("verify", "Verify Excititor exports or attestations.");
var exportIdOption = new Option<string?>("--export-id")
{
Description = "Export identifier to verify."
};
var digestOption = new Option<string?>("--digest")
{
Description = "Expected digest for the export or attestation."
};
var attestationOption = new Option<string?>("--attestation")
{
Description = "Path to a local attestation file to verify (base64 content will be uploaded)."
};
verify.Add(exportIdOption);
verify.Add(digestOption);
verify.Add(attestationOption);
verify.SetAction((parseResult, _) =>
{
var exportId = parseResult.GetValue(exportIdOption);
var digest = parseResult.GetValue(digestOption);
var attestation = parseResult.GetValue(attestationOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleExcititorVerifyAsync(services, exportId, digest, attestation, verbose, cancellationToken);
});
var reconcile = new Command("reconcile", "Trigger Excititor reconciliation against canonical advisories.");
var reconcileProviders = new Option<string[]>("--provider", new[] { "-p" })
{
Description = "Optional provider identifier(s) to reconcile.",
Arity = ArgumentArity.ZeroOrMore
};
var maxAgeOption = new Option<TimeSpan?>("--max-age")
{
Description = "Optional maximum age window (e.g. 7.00:00:00)."
};
reconcile.Add(reconcileProviders);
reconcile.Add(maxAgeOption);
reconcile.SetAction((parseResult, _) =>
{
var providers = parseResult.GetValue(reconcileProviders) ?? Array.Empty<string>();
var maxAge = parseResult.GetValue(maxAgeOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleExcititorReconcileAsync(services, providers, maxAge, verbose, cancellationToken);
});
excititor.Add(init);
excititor.Add(pull);
excititor.Add(resume);
excititor.Add(list);
excititor.Add(export);
excititor.Add(backfill);
excititor.Add(verify);
excititor.Add(reconcile);
return excititor;
}
private static Command BuildRuntimeCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
{
var runtime = new Command("runtime", "Interact with runtime admission policy APIs.");
var policy = new Command("policy", "Runtime policy operations.");
var test = new Command("test", "Evaluate runtime policy decisions for image digests.");
var namespaceOption = new Option<string?>("--namespace", new[] { "--ns" })
{
Description = "Namespace or logical scope for the evaluation."
};
var imageOption = new Option<string[]>("--image", new[] { "-i", "--images" })
{
Description = "Image digests to evaluate (repeatable).",
Arity = ArgumentArity.ZeroOrMore
};
var fileOption = new Option<string?>("--file", new[] { "-f" })
{
Description = "Path to a file containing image digests (one per line)."
};
var labelOption = new Option<string[]>("--label", new[] { "-l", "--labels" })
{
Description = "Pod labels in key=value format (repeatable).",
Arity = ArgumentArity.ZeroOrMore
};
var jsonOption = new Option<bool>("--json")
{
Description = "Emit the raw JSON response."
};
test.Add(namespaceOption);
test.Add(imageOption);
test.Add(fileOption);
test.Add(labelOption);
test.Add(jsonOption);
test.SetAction((parseResult, _) =>
{
var nsValue = parseResult.GetValue(namespaceOption);
var images = parseResult.GetValue(imageOption) ?? Array.Empty<string>();
var file = parseResult.GetValue(fileOption);
var labels = parseResult.GetValue(labelOption) ?? Array.Empty<string>();
var outputJson = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleRuntimePolicyTestAsync(
services,
nsValue,
images,
file,
labels,
outputJson,
verbose,
cancellationToken);
});
policy.Add(test);
runtime.Add(policy);
return runtime;
}
private static Command BuildAuthCommand(IServiceProvider services, StellaOpsCliOptions options, Option<bool> verboseOption, CancellationToken cancellationToken)
{
var auth = new Command("auth", "Manage authentication with StellaOps Authority.");

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,34 @@
using System;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Cli.Configuration;
internal static class AuthorityTokenUtilities
{
public static string ResolveScope(StellaOpsCliOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var scope = options.Authority?.Scope;
return string.IsNullOrWhiteSpace(scope)
? StellaOpsScopes.FeedserJobsTrigger
: scope.Trim();
}
public static string BuildCacheKey(StellaOpsCliOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.Authority is null)
{
return string.Empty;
}
var scope = ResolveScope(options);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"
: $"client:{options.Authority.ClientId}";
return $"{options.Authority.Url}|{credential}|{scope}";
}
}
using System;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Cli.Configuration;
internal static class AuthorityTokenUtilities
{
public static string ResolveScope(StellaOpsCliOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var scope = options.Authority?.Scope;
return string.IsNullOrWhiteSpace(scope)
? StellaOpsScopes.ConcelierJobsTrigger
: scope.Trim();
}
public static string BuildCacheKey(StellaOpsCliOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.Authority is null)
{
return string.Empty;
}
var scope = ResolveScope(options);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"
: $"client:{options.Authority.ClientId}";
return $"{options.Authority.Url}|{credential}|{scope}";
}
}

View File

@@ -113,7 +113,7 @@ public static class CliBootstrapper
authority.ClientSecret = string.IsNullOrWhiteSpace(authority.ClientSecret) ? null : authority.ClientSecret.Trim();
authority.Username = authority.Username?.Trim() ?? string.Empty;
authority.Password = string.IsNullOrWhiteSpace(authority.Password) ? null : authority.Password.Trim();
authority.Scope = string.IsNullOrWhiteSpace(authority.Scope) ? StellaOpsScopes.FeedserJobsTrigger : authority.Scope.Trim();
authority.Scope = string.IsNullOrWhiteSpace(authority.Scope) ? StellaOpsScopes.ConcelierJobsTrigger : authority.Scope.Trim();
authority.Resilience ??= new StellaOpsCliAuthorityResilienceOptions();
authority.Resilience.RetryDelays ??= new List<TimeSpan>();

View File

@@ -37,7 +37,7 @@ public sealed class StellaOpsCliAuthorityOptions
public string? Password { get; set; }
public string Scope { get; set; } = StellaOpsScopes.FeedserJobsTrigger;
public string Scope { get; set; } = StellaOpsScopes.ConcelierJobsTrigger;
public string TokenCacheDirectory { get; set; } = string.Empty;

View File

@@ -46,7 +46,7 @@ internal static class Program
clientOptions.ClientSecret = options.Authority.ClientSecret;
clientOptions.DefaultScopes.Clear();
clientOptions.DefaultScopes.Add(string.IsNullOrWhiteSpace(options.Authority.Scope)
? StellaOps.Auth.Abstractions.StellaOpsScopes.FeedserJobsTrigger
? StellaOps.Auth.Abstractions.StellaOpsScopes.ConcelierJobsTrigger
: options.Authority.Scope);
var resilience = options.Authority.Resilience ?? new StellaOpsCliAuthorityResilienceOptions();

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,25 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal interface IBackendOperationsClient
{
Task<ScannerArtifactResult> DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken);
Task UploadScanResultsAsync(string filePath, CancellationToken cancellationToken);
Task<JobTriggerResult> TriggerJobAsync(string jobKind, IDictionary<string, object?> parameters, CancellationToken cancellationToken);
}
Task<ScannerArtifactResult> DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken);
Task UploadScanResultsAsync(string filePath, CancellationToken cancellationToken);
Task<JobTriggerResult> TriggerJobAsync(string jobKind, IDictionary<string, object?> parameters, CancellationToken cancellationToken);
Task<ExcititorOperationResult> ExecuteExcititorOperationAsync(string route, HttpMethod method, object? payload, CancellationToken cancellationToken);
Task<ExcititorExportDownloadResult> DownloadExcititorExportAsync(string exportId, string destinationPath, string? expectedDigestAlgorithm, string? expectedDigest, CancellationToken cancellationToken);
Task<IReadOnlyList<ExcititorProviderSummary>> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken);
Task<RuntimePolicyEvaluationResult> EvaluateRuntimePolicyAsync(RuntimePolicyEvaluationRequest request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,6 @@
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorExportDownloadResult(
string Path,
long SizeBytes,
bool FromCache);

View File

@@ -0,0 +1,9 @@
using System.Text.Json;
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorOperationResult(
bool Success,
string Message,
string? Location,
JsonElement? Payload);

View File

@@ -0,0 +1,11 @@
using System;
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorProviderSummary(
string Id,
string Kind,
string DisplayName,
string TrustTier,
bool Enabled,
DateTimeOffset? LastIngestedAt);

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record RuntimePolicyEvaluationRequest(
string? Namespace,
IReadOnlyDictionary<string, string> Labels,
IReadOnlyList<string> Images);
internal sealed record RuntimePolicyEvaluationResult(
int TtlSeconds,
DateTimeOffset? ExpiresAtUtc,
string? PolicyRevision,
IReadOnlyDictionary<string, RuntimePolicyImageDecision> Decisions);
internal sealed record RuntimePolicyImageDecision(
string PolicyVerdict,
bool? Signed,
bool? HasSbomReferrers,
IReadOnlyList<string> Reasons,
RuntimePolicyRekorReference? Rekor,
IReadOnlyDictionary<string, object?> AdditionalProperties);
internal sealed record RuntimePolicyRekorReference(string? Uuid, string? Url, bool? Verified);

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class RuntimePolicyEvaluationRequestDocument
{
[JsonPropertyName("namespace")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Namespace { get; set; }
[JsonPropertyName("labels")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string>? Labels { get; set; }
[JsonPropertyName("images")]
public List<string> Images { get; set; } = new();
}
internal sealed class RuntimePolicyEvaluationResponseDocument
{
[JsonPropertyName("ttlSeconds")]
public int? TtlSeconds { get; set; }
[JsonPropertyName("expiresAtUtc")]
public DateTimeOffset? ExpiresAtUtc { get; set; }
[JsonPropertyName("policyRevision")]
public string? PolicyRevision { get; set; }
[JsonPropertyName("results")]
public Dictionary<string, RuntimePolicyEvaluationImageDocument>? Results { get; set; }
}
internal sealed class RuntimePolicyEvaluationImageDocument
{
[JsonPropertyName("policyVerdict")]
public string? PolicyVerdict { get; set; }
[JsonPropertyName("signed")]
public bool? Signed { get; set; }
[JsonPropertyName("hasSbomReferrers")]
public bool? HasSbomReferrers { get; set; }
// Legacy field kept for pre-contract-sync services.
[JsonPropertyName("hasSbom")]
public bool? HasSbomLegacy { get; set; }
[JsonPropertyName("reasons")]
public List<string>? Reasons { get; set; }
[JsonPropertyName("rekor")]
public RuntimePolicyRekorDocument? Rekor { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement>? ExtensionData { get; set; }
}
internal sealed class RuntimePolicyRekorDocument
{
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("verified")]
public bool? Verified { get; set; }
}

View File

@@ -1,4 +1,4 @@
If you are working on this file you need to read docs/ARCHITECTURE_VEXER.md and ./AGENTS.md).
If you are working on this file you need to read docs/ARCHITECTURE_EXCITITOR.md and ./AGENTS.md).
# TASKS
| Task | Owner(s) | Depends on | Notes |
|---|---|---|---|
@@ -6,7 +6,7 @@ If you are working on this file you need to read docs/ARCHITECTURE_VEXER.md and
|Introduce command host & routing skeleton|DevEx/CLI|Configuration|**DONE** System.CommandLine (v2.0.0-beta5) router stitched with `scanner`, `scan`, `db`, and `config` verbs.|
|Scanner artifact download/install commands|Ops Integrator|Backend contracts|**DONE** `scanner download` caches bundles, validates SHA-256 (plus optional RSA signature), installs via `docker load`, persists metadata, and retries with exponential backoff.|
|Scan execution & result upload workflow|Ops Integrator, QA|Scanner cmd|**DONE** `scan run` drives container scans against directories, emits artefacts in `ResultsDirectory`, auto-uploads on success, and `scan upload` covers manual retries.|
|Feedser DB operations passthrough|DevEx/CLI|Backend, Feedser APIs|**DONE** `db fetch|merge|export` trigger `/jobs/*` endpoints with parameter binding and consistent exit codes.|
|Concelier DB operations passthrough|DevEx/CLI|Backend, Concelier APIs|**DONE** `db fetch|merge|export` trigger `/jobs/*` endpoints with parameter binding and consistent exit codes.|
|CLI observability & tests|QA|Command host|**DONE** Added console logging defaults & configuration bootstrap tests; future metrics hooks tracked separately.|
|Authority auth commands|DevEx/CLI|Auth libraries|**DONE** `auth login/logout/status` wrap the shared auth client, manage token cache, and surface status messages.|
|Document authority workflow in CLI help & quickstart|Docs/CLI|Authority auth commands|**DONE (2025-10-10)** CLI help now surfaces Authority config fields and docs/09 + docs/10 describe env vars, auth login/status flow, and cache location.|
@@ -14,6 +14,11 @@ If you are working on this file you need to read docs/ARCHITECTURE_VEXER.md and
|Expose auth client resilience settings|DevEx/CLI|Auth libraries LIB5|**DONE (2025-10-10)** CLI options now bind resilience knobs, `AddStellaOpsAuthClient` honours them, and tests cover env overrides.|
|Document advanced Authority tuning|Docs/CLI|Expose auth client resilience settings|**DONE (2025-10-10)** docs/09 and docs/10 describe retry/offline settings with env examples and point to the integration guide.|
|Surface password policy diagnostics in CLI output|DevEx/CLI, Security Guild|AUTHSEC-CRYPTO-02-004|**DONE (2025-10-15)** CLI startup runs the Authority plug-in analyzer, logs weakened password policy warnings with manifest paths, added unit tests (`dotnet test src/StellaOps.Cli.Tests`) and updated docs/09 with remediation guidance.|
|VEXER-CLI-01-001 Add `vexer` command group|DevEx/CLI|VEXER-WEB-01-001|TODO Introduce `vexer` verb hierarchy (init/pull/resume/list-providers/export/verify/reconcile) forwarding to WebService with token auth and consistent exit codes.|
|VEXER-CLI-01-002 Export download & attestation UX|DevEx/CLI|VEXER-CLI-01-001, VEXER-EXPORT-01-001|TODO Display export metadata (sha256, size, Rekor link), support optional artifact download path, and handle cache hits gracefully.|
|VEXER-CLI-01-003 CLI docs & examples for Vexer|Docs/CLI|VEXER-CLI-01-001|TODO Update docs/09_API_CLI_REFERENCE.md and quickstart snippets to cover Vexer verbs, offline guidance, and attestation verification workflow.|
|EXCITITOR-CLI-01-001 Add `excititor` command group|DevEx/CLI|EXCITITOR-WEB-01-001|DONE (2025-10-18) Introduced `excititor` verbs (init/pull/resume/list-providers/export/verify/reconcile) with token-auth backend calls, provenance-friendly logging, and regression coverage.|
|EXCITITOR-CLI-01-002 Export download & attestation UX|DevEx/CLI|EXCITITOR-CLI-01-001, EXCITITOR-EXPORT-01-001|DONE (2025-10-19) CLI export prints digest/size/Rekor metadata, `--output` downloads with SHA-256 verification + cache reuse, and unit coverage validated via `dotnet test src/StellaOps.Cli.Tests`.|
|EXCITITOR-CLI-01-003 CLI docs & examples for Excititor|Docs/CLI|EXCITITOR-CLI-01-001|**DOING (2025-10-19)** Update docs/09_API_CLI_REFERENCE.md and quickstart snippets to cover Excititor verbs, offline guidance, and attestation verification workflow.|
|CLI-RUNTIME-13-005 Runtime policy test verbs|DevEx/CLI|SCANNER-RUNTIME-12-302, ZASTAVA-WEBHOOK-12-102|**DONE (2025-10-19)** Added `runtime policy test` command (stdin/file support, JSON output), backend client method + typed models, verdict table output, docs/tests updated (`dotnet test src/StellaOps.Cli.Tests`).|
|CLI-OFFLINE-13-006 Offline kit workflows|DevEx/CLI|DEVOPS-OFFLINE-14-002|TODO Implement `offline kit pull/import/status` commands with integrity checks, resumable downloads, and doc updates.|
|CLI-PLUGIN-13-007 Plugin packaging|DevEx/CLI|CLI-RUNTIME-13-005, CLI-OFFLINE-13-006|TODO Package non-core verbs as restart-time plug-ins (manifest + loader updates, tests ensuring no hot reload).|
|CLI-RUNTIME-13-008 Runtime policy contract sync|DevEx/CLI, Scanner WebService Guild|SCANNER-RUNTIME-12-302|**DONE (2025-10-19)** CLI runtime table/JSON now align with SCANNER-RUNTIME-12-302 (SBOM referrers, quieted provenance, confidence, verified Rekor); docs/09 updated with joint sign-off note.|
|CLI-RUNTIME-13-009 Runtime policy smoke fixture|DevEx/CLI, QA Guild|CLI-RUNTIME-13-005|**DONE (2025-10-19)** Spectre console harness + regression tests cover table and `--json` output paths for `runtime policy test`, using stubbed backend and integrated into `dotnet test` suite.|